Commit Graph

73 Commits

Author SHA1 Message Date
klopez4212
3f2f32641f Polish desktop onboarding flow (#5310)
## Summary
- standardize onboarding navigation and horizontal step transitions
- refine the avatar editor with live preview, segmented modes, search,
skin tones, and reduced-motion-safe feedback
- simplify harness/default-model actions and supporting copy

## Testing
- desktop typecheck and static guards
- desktop E2E build
- 9 focused onboarding smoke tests
- 4 focused onboarding/profile integration walkthroughs
- 4,535 desktop unit tests

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
2026-08-10 14:33:23 -07:00
Wes
97aa9e3185 fix(desktop): preserve Welcome banner dismissal (#5406)
## Summary

- remove the complete Welcome guidance surface when dismissal reaches
`hidden`
- preserve dismissal across the private and starter Welcome channels for
the active identity
- assert the starter channel's actual `welcome-everyone` title on
re-entry

## Why

PR #5330 introduced two deterministic Desktop E2E failures:

- the inner banner unmounted, but `welcome-composer-guidance-layer`
remained
- the re-entry test expected case-sensitive `Welcome` while navigating
to `welcome-everyone`

The state hook also scoped completion to channel IDs while `ChannelPane`
remounts during navigation. The Welcome guidance is one experience
spanning both Welcome channels, so completion now survives that remount
while remaining identity-scoped.

## Validation

At `b577eb42edffe889f63566f2457eacea720f3593`:

- `pnpm -C desktop typecheck`
- focused Biome check for all four changed files
- E2E build
- both `welcome-everywhere banner` integration tests repeated three
times: **6/6 passed**
- mandatory pre-push desktop check, typecheck, and full desktop unit
suite: **4,535 passed**
- `git diff --check`

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-09 09:05:47 -07:00
Will Pfleger
f029deafae fix(desktop): welcome banner overlap and missing dismiss control (#5330)
## Problem

The `WelcomeComposerGuidanceLayer` in the `#Welcome` channel was
positioned with `absolute inset-x-0 bottom-full z-[-1]` — outside the
`composerWrapperRef` measurement boundary. `useComposerHeightPadding`
observes `composerWrapperRef`'s block size to set `paddingBottom` on the
timeline scroll container, but the absolutely-positioned layer didn't
contribute to that size. The banner sat directly on top of the newest
message, blocking the thread affordance on that message, and had no
manual dismiss control.

## Fix

**Overlap**: Changed `WelcomeComposerGuidanceLayer` from `absolute
inset-x-0 bottom-full z-[-1]` to `relative` (in normal flow). As a
normal-flow child of `composer-dock`, the layer's full height is now
measured by the ResizeObserver and fed into the timeline's
`paddingBottom`, so the newest message is always fully visible and its
thread affordance is always clickable while the banner shows.

**Dismiss**: Added an `X` close button
(`data-testid="welcome-composer-dismiss-button"`) on the prompt state.
Clicking fires `onDismiss`, which drives `dismissing → hidden`
immediately (same slide-down animation as the auto-dismiss path) and
marks the channel ID as completed in the session ref so the banner does
not reappear on channel re-entry within the session.

**Refactor**: Extracted the banner state machine (refs, timers,
`useEffect`s, and callbacks) from `ChannelPane.tsx` into
`useWelcomeComposerBanner.ts`. This keeps `ChannelPane.tsx` well under
the 1000-line file-size ratchet and makes the state machine
independently testable.

## Changed files

- `desktop/src/features/channels/ui/WelcomeComposerBanner.tsx` —
`WelcomeComposerGuidanceLayer` positioning fix; `onDismiss` prop;
dismiss button; `overflow-hidden` / `mb-0` / `flex-1` cleanup
- `desktop/src/features/channels/ui/ChannelPane.tsx` — remove inline
banner state machine, use `useWelcomeComposerBanner` hook, pass
`onDismiss`
- `desktop/src/features/channels/ui/useWelcomeComposerBanner.ts` — new
hook owning all banner state

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
2026-08-08 13:00:09 -04:00
Taylor Ho
a5a9240241 fix(desktop): let imported and recovered identities finish onboarding (#5228)
**Category:** fix
**User Impact:** People who onboard by importing an existing key or
recovering from a phone can now use "Skip for now" (and Next) on the
harness setup and model config steps, instead of getting stuck.

**Problem:** On the "Set up your agent harnesses" and "Configure your
default model settings" onboarding steps, clicking **Skip for now** — or
**Next** — did nothing for anyone who reached those steps by importing
an existing key or recovering an identity from a phone. The app stayed
frozen on the step.

**Solution:** The onboarding state machine sets `continuingPubkeyRef` to
the current pubkey on import/recovery to keep the flow on `onboarding`
until setup finishes (added in #4845). But `complete()` never cleared
that ref, so once it matched the current pubkey the stage stayed pinned
to `onboarding` forever — completion could never win. `complete()` now
clears the ref so finishing/skipping actually settles the flow.
Fresh-generated keys never set the ref, which is why first-run fresh-key
skip already worked and the gap went unnoticed.

<details>
<summary>File changes</summary>

**desktop/src/features/onboarding/machineOnboarding.ts**
Clear `continuingPubkeyRef` inside `complete()` so an imported/recovered
identity's "continuing" marker no longer outlives completion and pin the
stage to `onboarding`.

**desktop/tests/e2e/onboarding.spec.ts**
Add a regression test that imports an existing key, reaches harness
setup, clicks **Skip for now**, and asserts onboarding exits (reaches
community onboarding). This fails without the fix. The existing skip
tests only exercised the fresh-key path, which never set the ref — hence
the gap.

</details>

## Reproduction steps

1. Start onboarding and choose **Use an existing key** (or recover from
a phone); import a key and continue to **Set up your agent harnesses**.
2. Click **Skip for now** (or **Next**). Before this change, nothing
happens — the step is stuck. The same trap hits **Configure your default
model settings**.
3. With this change, Skip/Next advances out of onboarding as intended.
4. Automated: `pnpm build:e2e && pnpm exec playwright test
onboarding.spec.ts --project=integration -g "imported-key users can skip
out of harness setup"` — passes with the fix, fails without it.

## Root cause

Introduced by #4845 (`feat(identity): recover desktop identity from a
signed-in phone`), which added `continuingPubkeyRef.current ===
currentPubkey` as an independent condition selecting the `onboarding`
stage. That guard has no off switch: `complete()` set the completion
flag but never cleared the ref, so the OR'd condition kept the stage
pinned. Not a revert candidate — the guard's intent (keep a
just-published identity in onboarding until setup finishes) is correct;
it just needed to release on completion.

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-07 15:03:43 -07:00
Wes
daa8877581 Make public starter channels best effort (#5192)
## Summary
- treat public starter-channel provisioning as best-effort after
preserving the required private Welcome path
- let community onboarding complete and focus Welcome when the reported
metadata lookup error occurs
- remove the now-obsolete retry-toast expectations for optional starter
provisioning

## Scope
This intentionally does not change relay tombstone semantics or
auto-join existing public channels.

## Test plan
- `pnpm exec playwright test tests/e2e/deep-link-invite.spec.ts` (8
passed)
- `pnpm exec playwright test tests/e2e/onboarding.spec.ts --grep "failed
public starter channel setup"` (1 passed)
- `pnpm typecheck`
- `pnpm check`
- `pnpm test` (4483 passed)
- pre-push hook: branch-skew, desktop-check, desktop-typecheck,
desktop-test passed on `4658a07beb1e1d54443da5cd2e4a28fae0232f24`

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-07 09:21:29 -07:00
klopez4212
c71f658539 Polish advanced agent setup and Welcome composer (#4926)
## Summary
- move **Run on** into Advanced, directly after **Who can send
instructions**
- reuse the modal’s shared dropdown styling
- give the Welcome guidance and composer matching glass treatment while
preserving the corrected exit layering

## Validation
- `pnpm -C desktop typecheck`
- focused Playwright: Run on configuration (3 passed)
- focused Playwright: Welcome onboarding flow (1 passed)
- desktop unit suite (4,290 passed)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Watcher <bb7abfd757d0af7b66569d02ab9c0316b616f9d0c151ecf5b964344c462e7f8f@buzz.block.builderlab.xyz>
Co-authored-by: Watcher <bb7abfd757d0af7b66569d02ab9c0316b616f9d0c151ecf5b964344c462e7f8f@buzz.block.builderlab.xyz>
2026-08-07 15:15:34 +01:00
Taylor Ho
6eb65919f1 feat(identity): recover desktop identity from a signed-in phone (#4845)
**Category:** new-feature
**User Impact:** People who lose a desktop identity can securely restore
it from a signed-in Buzz phone without creating a replacement identity.

**Problem:** A fresh or identity-lost desktop could not recover its
existing full Buzz identity from an already-authorized phone.

**Solution:** Add a SAS-confirmed reverse NIP-AB transfer, durable
desktop import, a dedicated mobile recovery entry point, and clearer
desktop recovery dialogs with tested loading, drag-and-drop, and failure
states.


https://github.com/user-attachments/assets/e9215c9c-80d0-462f-9161-0fa184ca2f74

<details>
<summary>File changes</summary>

**crates/buzz-core/src/pairing/session.rs**
Adds the reverse encrypted payload and source-completion state
transitions used for phone-to-desktop recovery.

**desktop/src-tauri/src/commands/identity.rs**
Exposes the existing guarded identity commit path for recovery imports.

**desktop/src-tauri/src/commands/pairing.rs**
Adds recovery-mode pairing, durable nsec import, start serialization,
stale-task protection, and explicit rejection of unsupported recovery
payloads.

**desktop/src-tauri/src/lib.rs**
Registers the recovery pairing command.

**desktop/src/app/App.tsx**
Refreshes the recovered identity before continuing onboarding.

**desktop/src/features/onboarding/machineOnboarding.ts**
Adds recovery transitions to the onboarding state machine.

**desktop/src/features/onboarding/ui/BackupPasswordTimeline.tsx**
Adds the visual backup-to-password-to-unlock progression.

**desktop/src/features/onboarding/ui/IdentityRecoveryPairing.tsx**
Implements QR generation, copy fallback, SAS confirmation, cancellation,
expiry, and completion UI.

**desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx**
Connects private-key, phone, and backup recovery paths to the onboarding
flow.

**desktop/src/features/onboarding/ui/NostrKeyImportForm.tsx**
Polishes recovery dialogs, backup drag-and-drop, loading stability, and
security copy.

**desktop/src/shared/api/tauri.ts**
Keeps the existing pairing API surface focused on standard
desktop-to-mobile pairing.

**desktop/src/shared/api/tauriPairing.ts**
Adds the recovery pairing invoke without growing the ratcheted shared
API file.

**desktop/src/testing/e2eBridge.ts**
Mocks recovery pairing commands and lifecycle events for browser tests.

**desktop/tests/e2e/identity-lost.spec.ts**
Covers lost-identity entry, QR/copy recovery, SAS, cancellation, expiry,
success, errors, backup import, drag-and-drop, and screenshots.

**desktop/tests/e2e/onboarding.spec.ts**
Verifies recovered identities continue through harness setup without
replacement-key side effects.

**mobile/lib/features/pairing/pairing_page.dart**
Adds recovery-only scanning and explicit identity-handoff warnings.

**mobile/lib/features/pairing/pairing_provider.dart**
Recognizes recovery codes, returns the signed-in nsec after mutual SAS
approval, and waits for desktop completion.

**mobile/lib/features/settings/settings_page.dart**
Accepts the recovery route builder at the app composition boundary to
preserve feature isolation.

**mobile/lib/features/settings/settings_page/connection_section.dart**
Adds the signed-in “Send identity to desktop” settings action.

**mobile/test/features/pairing/pairing_page_test.dart**
Covers recovery-only validation and handoff messaging.

**mobile/test/features/pairing/pairing_provider_test.dart**
Covers reverse payload encryption, confirmation ordering, success,
failure, timeout, and cleanup.

</details>

## Reproduction steps

1. Launch Buzz Desktop with identity-lost state and choose **Recover
from your phone**.
2. Confirm the QR and persistent **Copy pairing code** fallback appear
without layout shift.
3. On a signed-in phone, open **Settings → Send identity to desktop**,
scan or paste the recovery code, and compare the six-digit SAS on both
devices.
4. Confirm on both sides and verify Desktop restores the identity and
continues to harness setup.
5. Repeat from identity-lost state with **Recover from a backup file**;
verify picker and drag-and-drop both advance to password entry and
restore the encrypted backup.
6. Exercise cancellation, mismatched/unsupported codes, expired
sessions, and an invalid backup; verify each returns actionable,
non-stuck UI.

## Screenshots

### Desktop phone recovery — complete flow

| Recovery entry | Pairing QR | Code match | Receiving identity |
|---|---|---|---|
| ![Desktop recovery
entry](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/desktop-phone-01-recovery-entry.png)
| ![Desktop phone recovery
QR](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/desktop-phone-02-qr.png)
| ![Desktop security-code
match](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/desktop-phone-03-sas.png)
| ![Desktop receiving
identity](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/desktop-phone-04-receiving.png)
|

### iOS Simulator — complete handoff flow

| Settings entry | Recovery scanner | Manual recovery code | Code
confirmation |
|---|---|---|---|
| ![iOS Settings entry for Send identity to
desktop](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/ios-01-settings-entry.png)
| ![iOS recovery scanner
entry](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/ios-02-recovery-entry.png)
| ![iOS manual recovery code
entry](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/ios-03-manual-code.png)
| ![iOS security-code
confirmation](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/ios-04-sas-verification.png)
|

### Encrypted backup recovery — adjusted file flow

| File picker | Drag-and-drop target | Password step |
|---|---|---|
| ![Desktop encrypted-backup file
picker](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/desktop-backup-01-file-picker-settled.png)
| ![Desktop encrypted-backup drag-and-drop
target](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/desktop-backup-02-drag-drop.png)
| ![Desktop backup password
step](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/desktop-backup-03-enter-password.png)
|

## Verification

- `cargo test -p buzz-core pairing` — 71 passed
- `just mobile-test` — 1,169 passed
- `pnpm build:e2e && pnpm exec playwright test identity-lost.spec.ts
--project=smoke` — 15 passed
- Full pre-push gates — desktop checks, desktop unit tests, Rust tests,
Tauri checks, and mobile tests passed

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-06 11:47:18 -07:00
klopez4212
d0af845a1d Remove blur from Welcome composer guidance (#4691)
## Summary
- Keep the Welcome composer prompt above the dock blur so it stays
readable.
- Remove blur from the prompt and persona-motion paths.
- Cover the crisp, correctly layered banner in the onboarding browser
test.

## Validation
- `pnpm -C desktop exec biome check
src/features/channels/ui/WelcomeComposerBanner.tsx
tests/e2e/onboarding.spec.ts`
- `pnpm -C desktop build:e2e`
- `pnpm -C desktop exec playwright test tests/e2e/onboarding.spec.ts
--grep "finishing onboarding creates starter channels and focuses
welcome-everyone for a new member" --project=integration`

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-04 07:14:09 -07:00
Tyler
468647a51f feat(desktop): locally stored NIP-49 encrypted key backup (#2937)
## Summary

Adds a locally stored **NIP-49 encrypted key backup** (`ncryptsec`) to
the desktop app, per the plan reviewed in buzz-development (Rev 3,
approved 9/10 by Wren; implementation also reviewed and approved 9/10).

**Two-artifact design — canonical bytes originate entirely in Rust:**
- `create_ncryptsec_backup` runs under the `identity_mutation` lock:
encrypt → decrypt-verify against the live pubkey → atomic `0o600` write
to `{app_data_dir}/identity.ncryptsec` → reread/byte-compare → return
the exact persisted bytes. The frontend never re-derives or re-encrypts.
- `save_ncryptsec_copy` writes a portable copy via the save dialog
(parse-gated, secret-file semantics) and never mutates canonical state.
- `generate_backup_passphrase`: 6 words from the EFF short wordlist via
`OsRng` (custom passphrases min 12 chars).
- Import accepts `ncryptsec1` with optional password; the raw-`nsec`
path is untouched. Different-pubkey import and sign-out wipe the
app-managed backup (post-commit, best-effort — a failed import can never
destroy the still-live identity's backup; regression-tested).

**Never-relay guarantee (egress guard + tripwires):**
- `egress_guard.rs` fail-closed at all 8 `/events` submission boundaries
(relay submit funnel, 3× `relay.rs`, huddle STT, both engram submitters,
native WS choke point), rejecting `ncryptsec1`/`NCRYPTSEC1` in text and
binary frames. Scope is deliberately ncryptsec-only: pairing
intentionally carries raw nsec inside its encrypted session.
- Site-granular `/events` inventory tripwire: per-file (`/events` count,
guard-call count) pairs; unlisted files expect zero. Mutation-style
tests prove a ninth site in an existing file, a removed guard, and a new
unlisted file all fail the scan.
- ncryptsec source-allowlist scans in **both** trees (Rust + TS).

**Frontend:** onboarding `BackupStep` is encrypted-by-default — the
default path never invokes `get_nsec` (e2e asserts the command log).
Raw-nsec export stays behind an explicit click with prior semantics.
Shared `EncryptedBackupCreator` powers onboarding + a new settings row;
the import form auto-switches to encrypted mode on `ncryptsec1` paste
(case-insensitive HRP).

**Open product call for @tlongwell-block:** onboarding default is
*encrypted* in this PR; flipping to raw-default is a small change either
way (documented in the plan).

Review history: plan Rev 3 and the implementation were both iterated
with Wren to 9/10 (two blockers from round 1 — import ordering,
inventory granularity — plus an uppercase-bech32 hardening gap, all
fixed in `dde37183e`). Thread: buzz-development.

### Related issue

Follow-up to the direction explored in #385 (NIP-PB, closed) — this
ships local NIP-49 (the standard) instead of a new NIP. No open
duplicate found.

### Testing

All at exactly `dde37183e` (same shell, HEAD verified):

- `cargo test` — 1680 passed / 0 failed / 14 ignored (includes a
deliberate ~70s log_n-18 NIP-49 round trip, spec vector, wrong-password,
NFKC, uppercase-vector decrypt, injection test per egress boundary,
inventory mutation tests, import-ordering regression tests)
- `cargo clippy --all-targets -- -D warnings` — clean; `cargo fmt
--check` — clean
- `pnpm typecheck` — clean; JS unit suite 3529/3529; biome (repo-pinned
2.4.16) clean
- Playwright `onboarding-backup` / `onboarding` /
`onboarding-agent-defaults` / `profile-nsec-reveal` — 86 passed, 1 known
avatar-reservation flake (passed on rerun; untouched by this diff).
`passThroughBackupStep` now exercises the encrypted default, so every
downstream onboarding spec covers the new path.
- Note: browser e2e fakes the crypto via the mock bridge (fixed
spec-vector blob); decryption correctness is proven in the Rust tests.

## Latest onboarding integration

The current head adds an additive `IdentityInfo.storage` field
(`ephemeral`, `system-keyring`, `local-file`, or `environment`) so
onboarding can accurately explain where the active identity is
protected. It surfaces storage metadata only—never key material—and
leaves the existing lost/keyring-locked recovery behavior intact.

---------

Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
2026-07-30 20:58:03 -04:00
Bradley Axen
f44b5a2477 fix(desktop): reuse profiles when joining communities (#2155)
## Why
People joining a community with an existing relay profile should not be
asked to recreate their name and avatar.

## What
- Check the active identity's relay profile after the joined community
becomes active
- Skip directly to the starter-team step when a kind-0 profile event
exists
- Preserve the profile setup path when no event exists or discovery
fails
- Cover both new-profile and existing-profile join paths in E2E tests

## Risk Assessment
Low — the lookup is scoped to the community onboarding profile stage,
runs once per transaction, and fails open to the existing flow.

## References
- `pnpm build:e2e && pnpm exec playwright test --project=integration
tests/e2e/onboarding.spec.ts --grep 'first-community direct join reaches
profile|community onboarding reuses an existing relay profile'` (2
passed)

Generated with Codex

Signed-off-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co>
2026-07-30 10:48:54 -07:00
John Matthew Tennant
0019f80765 fix(desktop): fetch join policies through native networking (#2862)
## Context

Adding an existing community by relay URL could fail with `Community
rejected: Load failed` even when its WebSocket endpoint was reachable.
The Add Community flow fetched `/api/join-policy` from the WebView, so a
relay without a matching CORS allowance blocked the policy request
before the app could join it.

## Summary

This bug fix fetches join policies through Tauri's native networking
layer for direct URL joins. Invite-code discovery, policy acceptance,
and signed invite claims remain on the WebView path so those operations
can migrate together later.

## Changes

- Uses native networking for Add Community and first-community direct
URL join-policy requests.
- Validates relay schemes, rejects URLs containing credentials, and
refuses redirects.
- Bounds declared and chunked native responses before JSON parsing.
- Preserves existing `404`, non-success status, malformed JSON, and
absent-policy behavior.
- Requires every join-policy caller to choose its transport explicitly.

Public relays using Buzz's default permissive CORS configuration are not
known to be affected.

### Related issue

Related to #2872.

### Testing

#### Reviewer-reproducible examples

End-to-end red/green requires a relay with restrictive CORS and a Buzz
identity authorized to join it.

##### Red: `main`

From a clean checkout of `main`:

```bash
. ./bin/activate-hermit
just staging
```

In Buzz Desktop:

1. Add another community so the restrictive-CORS relay can be removed.
2. Remove that relay.
3. Open Add Community and enter the relay's WebSocket URL.
4. Select Add Community.

Observed result:

```text
Community rejected: Load failed
```

##### Green: this PR

From a clean checkout of this branch:

```bash
. ./bin/activate-hermit
just staging
```

Repeat the same steps above.

Observed result:

```text
The community rejoins successfully.
```

Supporting checks:

- Six native join-policy tests, including oversized declared and chunked
responses.
- Four TypeScript API tests, including the native command contract.
- E2E build and four focused onboarding and sidebar Playwright tests.
- Full `just ci` and pre-push suites.
- Builderbot, Kalvin, and minimize-diff review fanout found no
actionable issues after the final rebase.
2026-07-27 17:52:36 -04:00
klopez4212
384c72dee6 Refine community management flows (#2738)
## Summary
- simplify Add community into clear create and join paths
- align community actions and icon editing across the rail, profile
menu, and settings
- consolidate invites around the member list and a reusable link dialog

## Testing
- `pnpm check`
- `pnpm test` (3,483 passed)
- `pnpm run build:e2e`
- focused Playwright coverage (34 passed)

## Snapshots

| Add a community | Join an existing community |
| --- | --- |
| ![Add a
community](https://raw.githubusercontent.com/block/buzz/fae692faa77186df7702fb025ca3082c7cc93a8c/pr-2738--01-add-community.png)
| ![Join an existing
community](https://raw.githubusercontent.com/block/buzz/fae692faa77186df7702fb025ca3082c7cc93a8c/pr-2738--02-join-community.png)
|

| Create a new community | Hosted community icon |
| --- | --- |
| ![Create a new
community](https://raw.githubusercontent.com/block/buzz/fae692faa77186df7702fb025ca3082c7cc93a8c/pr-2738--03-create-community.png)
| ![Hosted community
icon](https://raw.githubusercontent.com/block/buzz/fae692faa77186df7702fb025ca3082c7cc93a8c/pr-2738--04-community-icon.png)
|

| Invites | Invite link |
| --- | --- |
|
![Invites](https://raw.githubusercontent.com/block/buzz/fae692faa77186df7702fb025ca3082c7cc93a8c/pr-2738--05-invites.png)
| ![Invite
link](https://raw.githubusercontent.com/block/buzz/fae692faa77186df7702fb025ca3082c7cc93a8c/pr-2738--06-invite-link.png)
|

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-07-24 19:30:33 -07:00
Wes
e67303f603 Gate default relay auto-connect behind release flag (#2589)
Signed-off-by: npub1cl47vfhsqpqy9pwndphpm36vcp7vvz5h2js4qpqm5yewzj7nutkq7xyw8c <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: Bradley Axen <baxen@squareup.com>
Co-authored-by: npub1cl47vfhsqpqy9pwndphpm36vcp7vvz5h2js4qpqm5yewzj7nutkq7xyw8c <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-07-23 13:59:10 -07:00
Logan Johnson
80244f8231 Fix avatar upload lifecycle edge cases (#2277)
Signed-off-by: Logan Johnson <loganj@squareup.com>
Signed-off-by: npub1dpf98sl35hm9k65t8h6cvh5n6knn2msugh5k5nmysxwnw5wlh7uqn2wgp4 <685253c3f1a5f65b6a8b3df5865e93d5a7356e1c45e96a4f64819d3751dfbfb8@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Co-authored-by: npub1dpf98sl35hm9k65t8h6cvh5n6knn2msugh5k5nmysxwnw5wlh7uqn2wgp4 <685253c3f1a5f65b6a8b3df5865e93d5a7356e1c45e96a4f64819d3751dfbfb8@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
2026-07-23 12:45:00 -07:00
cynfria
f609fcee02 Keep avatar preview visible during upload (#2237)
Signed-off-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Logan Johnson <loganj@squareup.com>
2026-07-21 12:59:57 -07:00
morgmart
50f4412167 Hide bundled harnesses from onboarding (#2233) 2026-07-21 18:54:42 +00:00
morgmart
9cd13f3d5a Restore npub copy option for private community joins (#2232) 2026-07-21 16:43:21 +00:00
morgmart
6ab45d75de Simplify first-community onboarding choices (UI only) (#2194) 2026-07-20 19:19:52 -07:00
Bradley Axen
8a65d81fab fix(desktop): modal sign-in + branded communities onboarding page (#2147)
Signed-off-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co>
2026-07-20 09:59:40 -07:00
Kalvin C
fc1db46937 fix: skip membership lookup for open relays (#2107)
Signed-off-by: npub1ntr8jjcqq6gt06q5avvqttfjgpwshmra22pcmcagdnukw4ja4nqqsa9g54 <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: npub1ntr8jjcqq6gt06q5avvqttfjgpwshmra22pcmcagdnukw4ja4nqqsa9g54 <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
2026-07-20 15:35:40 +00:00
cynfria
e84f3fe8ed feat(desktop): add fuzzy onboarding cards (#2141)
Signed-off-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co>
2026-07-19 14:18:32 -07:00
cynfria
1342fc564a Add back action to community profile onboarding (#2136)
Signed-off-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co>
2026-07-19 14:18:19 -07:00
Bradley Axen
cd0e7402d7 Add owned community onboarding (#2123)
Signed-off-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co>
2026-07-19 12:44:18 -07:00
cynfria
3881bff619 remove relay field from invite link step (#2131)
Signed-off-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co>
2026-07-19 11:26:53 -07:00
cynfria
46a63a864b Polish onboarding avatar capture (#2118)
Signed-off-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
2026-07-19 11:14:06 -07:00
cynfria
b6aa71789d Align intro-to-your-team onboarding (#2102)
Signed-off-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co>
2026-07-19 09:45:33 -06:00
morgmart
af4b7087dc Open community creation signup from desktop (#2110) 2026-07-18 20:44:23 -07:00
cynfria
c81806d1c8 Polish community profile onboarding UI (#2088)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
2026-07-18 18:45:08 -07:00
Wes
775cf672d1 fix(desktop): recover first community joins (#2087)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
2026-07-18 15:31:17 -07:00
Tyler
4cf5694075 fix(desktop): own native WebSocket lifecycle (#2076)
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
2026-07-18 18:52:56 +00:00
Wes
f347dcac25 Fix membership-denied community onboarding recovery (#2072)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
2026-07-18 17:13:00 +00:00
morgmart
1fdb73e17b Speed up the Welcome kickoff and show the team arriving (#2066)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-18 09:13:22 -07:00
morgmart
1bdd938491 Add identity key help and reveal toggle to machine onboarding (#2064)
Signed-off-by: npub1d6t84ajeg9skp2609l2k6axgcme8x7g7u7luj352r03hcwreg7lqnxcsex <6e967af659416160ab4f2fd56d74c8c6f273791ee7bfc9468a1be37c387947be@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: npub1d6t84ajeg9skp2609l2k6axgcme8x7g7u7luj352r03hcwreg7lqnxcsex <6e967af659416160ab4f2fd56d74c8c6f273791ee7bfc9468a1be37c387947be@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
2026-07-18 15:45:41 +00:00
Wes
385dde1138 Fix community profile onboarding back navigation (#2058)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
2026-07-17 17:16:09 -07:00
Bradley Axen
738d456367 BOT-1359: Continue onboarding after key import (#2056)
Signed-off-by: npub1t490dek5vslqxv0ft0jlft6j5fjlp74jery2tyj7ngf8ggd0pcnst8w25n <5d4af6e6d4643e0331e95be5f4af52a265f0fab2c8c8a5925e9a127421af0e27@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1t490dek5vslqxv0ft0jlft6j5fjlp74jery2tyj7ngf8ggd0pcnst8w25n <5d4af6e6d4643e0331e95be5f4af52a265f0fab2c8c8a5925e9a127421af0e27@sprout-oss.stage.blox.sqprod.co>
2026-07-17 16:37:32 -07:00
cynfria
2ea71de24c Polish community onboarding flow (#2048) 2026-07-17 15:38:26 -07:00
morgmart
85fc64835e Welcome new users with a live agent team kickoff (#1998)
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
2026-07-17 07:39:03 -04:00
Tyler
74f3f29758 feat(desktop): ensure starter channels during onboarding (#1937)
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
2026-07-16 15:40:48 -04:00
Tyler
6c2d667575 Apply optional relay join policy across join flows (#1894)
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: klopez4212 <klopez4212@gmail.com>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
2026-07-16 13:59:53 -04:00
Wes
a84fe13a8c fix(desktop): restore default community join option (#1969)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
2026-07-16 10:02:38 -07:00
morgmart
f20d79658c Welcome channel: Browse channels card, browser sorting, shortcut hint placement (#1948) 2026-07-16 08:42:57 -07:00
Matt Toohey
2c08271ba9 feat(desktop): remove theme selection from onboarding, default to Buzz + System (#1947)
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 06:54:16 -07:00
Wes
8688c0416d Rework first-launch community onboarding (#1936)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
2026-07-15 22:01:51 -07:00
Matt Toohey
dd39b6249f fix(desktop): variant-aware connecting gate for first-run handoffs (#1938)
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 14:26:33 +10:00
morgmart
54e174b5e1 Chat-first agent creation via portable buzz CLI drafts (#1878) 2026-07-15 09:24:57 -07:00
Will Pfleger
5bb6192951 fix(desktop): remediate all onboarding dead paths (D1-D7) (#1856)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-07-14 19:31:28 -04:00
Wes
3e76481a14 refactor(clients): standardize product naming on community (#1858)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
2026-07-14 11:11:02 -07:00
Tyler
b0ad2b66dd Retain a stable message window in channel timelines (#1698)
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
2026-07-12 21:41:38 -04:00
Will Pfleger
d2e87e1ca7 feat(desktop): add key backup and agent defaults to onboarding, make avatar optional (#1767) 2026-07-12 18:46:02 -04:00
Will Pfleger
e5d215e3c2 fix(desktop): isolate dev keyring from production and make BUZZ_SHARE_IDENTITY keyring-aware (#1680)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
2026-07-09 17:33:52 -04:00