diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index e383313452a..c951650eb1f 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -4,6 +4,10 @@ about: Report a reproducible bug in Buzz labels: bug --- +> [!IMPORTANT] +> Do not include security vulnerabilities in a public issue. [Report them +> privately through a GitHub security advisory](https://github.com/block/buzz/security/advisories/new). + **Describe the bug** A clear and concise description of what the bug is. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 0086358db1e..67bfbe0ce46 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1 +1,5 @@ blank_issues_enabled: true +contact_links: + - name: Report a security vulnerability + url: https://github.com/block/buzz/security/advisories/new + about: Report security vulnerabilities privately to the Buzz maintainers. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db2c12f03a6..6d6a48ed1bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,6 +87,10 @@ jobs: scripts/test-mobile-release-candidate-publisher.sh - name: Mobile worktree identity contract run: scripts/test-mobile-worktree-overrides.sh + - name: Rust cache contract + run: | + scripts/test-rust-cache-contract.sh + scripts/test-rust-cache-contract-regressions.sh - name: File size policy run: just file-size-check @@ -101,7 +105,7 @@ jobs: steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 with: save-if: ${{ github.event_name != 'pull_request' }} - name: Format check @@ -123,8 +127,9 @@ jobs: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 with: + key: sherpa-cache-v1 save-if: ${{ github.event_name != 'pull_request' }} - name: Install cargo-nextest uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15 @@ -147,7 +152,7 @@ jobs: fetch-depth: 2 - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 with: workspaces: desktop/src-tauri save-if: ${{ github.event_name != 'pull_request' }} @@ -340,7 +345,7 @@ jobs: key: relay-artifacts-${{ runner.os }}-${{ hashFiles('crates/**', 'migrations/**', 'Dockerfile', 'Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml', '.cargo/config.toml', '.github/workflows/ci.yml') }} - uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 if: steps.relay-artifacts-cache.outputs.cache-hit != 'true' - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 if: steps.relay-artifacts-cache.outputs.cache-hit != 'true' with: workspaces: | @@ -674,6 +679,19 @@ jobs: VALUES ('00000000-0000-4000-8000-00000000c0de', 'localhost:3000') ON CONFLICT (lower(host)) DO NOTHING ;" + - name: Replaceable persistence PostgreSQL tests + # Transaction, concurrency, and mention-index coverage for the + # replaceable-event store seam. These tests require real Postgres and + # are ignored by the infrastructure-free unit-test job. + run: | + filter='package(buzz-db) and test(/tests::(parameterized_|concurrent_parameterized_)/)' + cargo nextest run \ + --archive-file target/ci/backend-integration-tests.tar.zst \ + -E "${filter}" \ + --run-ignored ignored-only + env: + DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz + TEST_DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz - name: Start relay run: | chmod +x ./target/ci/buzz-relay @@ -763,7 +781,7 @@ jobs: steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 with: save-if: ${{ github.event_name != 'pull_request' }} # Reuse the relay + git-credential-nostr built by Desktop E2E Relay @@ -952,7 +970,7 @@ jobs: steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 with: key: cross-${{ matrix.target }} save-if: ${{ github.event_name != 'pull_request' }} @@ -993,7 +1011,7 @@ jobs: # toolchain (1.95.0 + clippy via profile = default) comes from the # repo-root rust-toolchain.toml, which the runner's preinstalled rustup # honors on demand; the host triple already is x86_64-pc-windows-msvc. - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 with: workspaces: | . @@ -1070,7 +1088,7 @@ jobs: steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 with: workspaces: desktop/src-tauri save-if: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/mesh-lifecycle.yml b/.github/workflows/mesh-lifecycle.yml index b5f9660dfe3..c699439980b 100644 --- a/.github/workflows/mesh-lifecycle.yml +++ b/.github/workflows/mesh-lifecycle.yml @@ -55,7 +55,7 @@ jobs: - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 with: save-if: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b8bc7d24ddb..e6db44fede3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -833,7 +833,7 @@ jobs: - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 with: workspaces: desktop/src-tauri lookup-only: true diff --git a/AGENTS.md b/AGENTS.md index 4917c08d721..ee0d09bdba8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -183,7 +183,7 @@ place. | `migrations/0029_channels_id_lookup_index.sql`, `0030_long_reaction_payloads.sql`, `0031_community_deletion.sql`, `0032_community_deletion_recovery.sql`, `0033_workflow_run_error_codes.sql`, `0034_channel_roster_snapshot_fence.sql` | upstream's `0027_channels_id_lookup_index.sql`, `0028_long_reaction_payloads.sql`, `0029_community_deletion.sql`, `0030_community_deletion_recovery.sql`, `0031_workflow_run_error_codes.sql` and `0032_channel_roster_snapshot_fence.sql`, **renumbered**; contents byte-identical | The fork holds 0027 and 0028, so upstream's own new migrations have to arrive above them. Five syncs running, six migrations, and it has now fired on every sync that touched `migrations/` — treat this as the standing cost of the fork's migration block rather than a special case. See [Upstream migrations arrive renumbered](#upstream-migrations-arrive-renumbered) | | `crates/buzz-db/src/migration.rs` | `migrations.len()` assertion is 34, not upstream's 32; upstream's channel-index assertion reads `migrations[28].version == 29`, long-reaction `migrations[29].version == 30`, deletion `migrations[30].version == 31`, deletion-recovery `migrations[31].version == 32`, workflow-error-codes `migrations[32].version == 33`, roster-fence `migrations[33].version == 34`; and `deletion_surface_parity_between_migration_0029_and_schema_sql` looks up `version == 31` | Counts embedded migrations, so it moves whenever *either* side adds one. `0027` landed without bumping it and left the test failing on `main`; fixed in PR #9. Beyond the count, every upstream assertion that indexes `migrations[…]` past 25 or names a version above 26 has to be shifted by the fork's two — see the section below for why the test suite will *not* catch it if you forget. **The highest-applied-version assertion is no longer a fork patch**: upstream's 2026-08-13 range replaced the hardcoded `Some(30)` with a `latest_version` derived from `MIGRATOR`, so it now tracks the renumber on its own — take upstream's version if it ever conflicts again | | `crates/buzz-db/src/lib.rs` | `create_scratch_db_through(&admin, "roster_fence_unmigrated", Some(33))`, not upstream's `Some(31)` | **A fourth renumber shape, and the first one that lives outside `migration.rs`.** Upstream #6251 added `unmigrated_roster_fence_blocks_startup_until_0032_is_applied`, which migrates to the version *immediately below* its own roster fence (`0032` → target 31) and asserts the startup gate is closed, then runs the rest and asserts it opens. The fence is `0034` here, so the version below it is **33**; left at 31 the fixture stops two migrations early and the test proves a weaker premise than upstream wrote. It is not a count, not an index, and not a `version == N` lookup — it is a *bounded-migration target*, so the three-shape sweep in the section below does not find it. It is also `#[ignore = "requires Postgres"]`, so `just test-unit` is green either way. Grep `create_scratch_db_through` and `run_to(` whenever a migration is renumbered | -| `.github/workflows/macos-canary.yml` | new; `push` trigger on `main` with desktop path filters | Unsigned macOS canary; upstream only has a *signed* one, which a fork cannot run. Builds automatically when `desktop/**`, `crates/**` or the root `Cargo.*` change, so the newest artifact always matches `main` — it was dispatch-only, and the sole artifact went 13 commits stale. Free: the repo is public, so GitHub-hosted macOS runners are unbilled. Stages the artifact and the usage notes under the product name read from `tauri.conf.json`, not a hardcoded one, so the brand rename below cannot publish a build under the old name. Sets `signingIdentity: "-"` in its inline config and runs **without** `--no-sign`, which would silently discard it; asserts the bundle signature of the `.app` inside the mounted DMG. Its **sidecar list must track upstream's non-Windows lanes**: `tauri.conf.json`'s `externalBin` is shared, and `scripts/bundle-sidecars.sh` exits 1 on a missing binary, so a sidecar upstream adds breaks this workflow without ever conflicting — `buzz-backend-kubernetes` (#4289) did exactly that in the 2026-08-03 sync. **The same blind spot applies to its pinned action SHAs**: upstream's Renovate sweeps bump only upstream's own workflow files, so this one drifts behind without conflicting. The 2026-08-22 sync bumped `Swatinem/rust-cache` to `6323deb1` across `ci.yml`, `release.yml` and `linux-canary.yml` and left this file on `e18b4977`. A stale-but-valid pin breaks nothing — it is drift, not a failure — so it was deliberately left rather than swept in as a drive-by; re-check it when upstream bumps an action | +| `.github/workflows/macos-canary.yml` | new; `push` trigger on `main` with desktop path filters | Unsigned macOS canary; upstream only has a *signed* one, which a fork cannot run. Builds automatically when `desktop/**`, `crates/**` or the root `Cargo.*` change, so the newest artifact always matches `main` — it was dispatch-only, and the sole artifact went 13 commits stale. Free: the repo is public, so GitHub-hosted macOS runners are unbilled. Stages the artifact and the usage notes under the product name read from `tauri.conf.json`, not a hardcoded one, so the brand rename below cannot publish a build under the old name. Sets `signingIdentity: "-"` in its inline config and runs **without** `--no-sign`, which would silently discard it; asserts the bundle signature of the `.app` inside the mounted DMG. Its **sidecar list must track upstream's non-Windows lanes**: `tauri.conf.json`'s `externalBin` is shared, and `scripts/bundle-sidecars.sh` exits 1 on a missing binary, so a sidecar upstream adds breaks this workflow without ever conflicting — `buzz-backend-kubernetes` (#4289) did exactly that in the 2026-08-03 sync. **The same blind spot applies to its pinned action SHAs**: upstream's Renovate sweeps bump only upstream's own workflow files, so this one drifts behind without conflicting. The 2026-08-22 sync bumped `Swatinem/rust-cache` to `6323deb1` across `ci.yml`, `release.yml` and `linux-canary.yml` and left this file on `e18b4977`. A stale-but-valid pin breaks nothing — it is drift, not a failure — so it was deliberately left rather than swept in as a drive-by; re-check it when upstream bumps an action. **That drift reversed a sync later, and the pin is now load-bearing.** Upstream #6618 (2026-08-25 sync) reverted every one of its own `rust-cache` refs back to `e18b4977` — v2.9.2 can preserve sherpa's prebuilt directory after removing its static archive, poisoning warm caches — and added `scripts/test-rust-cache-contract.sh`, which parses **every file in `.github/workflows/`** and aborts unless each `Swatinem/rust-cache@` ref is exactly that digest. It also pins `renovate.json` to `<=2.9.1`. So this workflow's stale pin became the correct one by accident, and from now on a fork-local workflow carrying any other digest fails a repository-wide contract in the `Detect Changed Paths` job. Never bump `rust-cache` here alone | | `Dockerfile` | `buzz-paymaster` and `buzz-avnu-proxy` added to the cargo build, the strip step, and both `COPY` stages | The sponsor and the AVNU API-key proxy ship in the relay's image so there is one publish pipeline and one immutable `:sha-<7>` tag for `deploy-aws.yml` to pin. Four-to-eight one-line additions, each inside an existing parallel list, so a conflict resolves as *keep ours, take upstream's*. Neither is the `ENTRYPOINT` — ECS tasks override `command`. `AVNU_API_KEY` is injected at runtime from secrets, never baked into the image | | `Cargo.toml`, `crates/buzz-core/Cargo.toml`, `crates/buzz-cli/Cargo.toml`, `crates/buzz-core/src/lib.rs`, `crates/buzz-cli/src/lib.rs`, `crates/buzz-cli/src/commands/mod.rs`, `crates/buzz-cli/TESTING.md` | Starknet/paymaster wiring: `crates/buzz-paymaster` joins the workspace members; `starknet-crypto`/`starknet-core`/`starknet-providers`/`starknet-accounts`/`secp256k1`/`async-trait` deps; `pub mod` lines for `outside_execution`, `sponsorship`, `starknet_account`, `commands::wallet`; the `WalletCmd` enum and its dispatch | The declaration surface for [Starknet accounts](#starknet-accounts) and [sponsorship](#sponsorship-buzz-paymaster). Each is a one-to-few-line insertion into an existing alphabetical list, so conflicts resolve as *keep ours, take upstream's*. **None carries an in-file `FORK-LOCAL` marker** — a manifest list takes a comment badly and the module lines are self-evidently fork-local from the module names — so this row is the only record. The new deps are all genuinely reachable: `secp256k1` from `starknet_account::sign_tx_hash` and `starknet_factory`, the `starknet-*` crates from `buzz-paymaster` and `wallet.rs`. `buzz-core` keeps its "zero I/O dependencies" contract — none of these pull tokio/sqlx/redis/axum | | `crates/buzz-relay/src/handlers/ingest.rs` | `KIND_SPONSOR_REQUEST`, `KIND_SPONSOR_RESULT` and `KIND_SPONSOR_DEPLOY_REQUEST` added to the `required_scope_for_kind` `UsersWrite` arm and to `is_global_only_kind` | The relay half of [sponsorship](#sponsorship-buzz-paymaster). `UsersWrite` because sponsorship is a user acting on their own Starknet account; global-only because that account belongs to the author and never to a channel. Both hunks land inside long `|`-separated match arms that upstream extends regularly, so expect conflicts here whenever upstream adds a kind — resolve as *keep our three, take upstream's* | @@ -206,7 +206,7 @@ place. | `desktop/src/features/markets/**`, `desktop/src/app/routes/markets.tsx`, `desktop/src-tauri/src/commands/markets.rs`, `crates/buzz-core/src/markets.rs`, `crates/buzz-avnu-proxy/` | new | The [Bitcoin markets](#bitcoin-markets-fork-local-feature-31) implementation. All additive paths upstream has no counterpart for, so they should never conflict. Their *declaration* sites do — `commands/mod.rs`, `lib.rs`'s invoke handler, `crates/buzz-core/src/lib.rs`, `desktop/package.json`, `tsconfig.json`, `vite.config.ts`, `routes.ts`, `routeTree.gen.ts` — each a one-to-few-line insertion into an existing list | | `desktop/src-tauri/src/relay/allowlist.rs` | new | Single-relay host allowlist. Upstream is multi-community by design; this fork ships a client that reaches only `relay.bitcoinmarkets.app`. **Lives under `relay/`, not at the crate root** — see the `relay.rs` row | | `desktop/src-tauri/src/native_websocket.rs` | allowlist call in `open_connection` | The transport is the one path every relay session takes, so a host restriction there cannot be bypassed from the UI | -| `desktop/src-tauri/src/relay.rs` | release builds default to the allowlisted relay; also declares `pub mod allowlist;` | Without the default a release build uses `ws://localhost:3000`, which the allowlist then rejects — a client that cannot connect at all. The module is declared *here* because `lib.rs`'s sorted module list is a permanent conflict site, and because `lib.rs` was itself at the 1000-line desktop ratchet when the move was made in the 2026-08-01 sync. `lib.rs` now carries no fork patch at all. **`relay.rs` has since become the constrained file, and the ratchet is how you find out — as a red check, not a merge conflict.** Upstream #6187 (2026-08-19 sync) made the file-size policy a first-class gate: it is now `just file-size-check`, run repository-wide as the **`File size policy`** step of the `scripts` CI job, and it no longer hangs off the per-surface `desktop`/`web`/`mobile` path filters. So an overflow here fails on every PR regardless of which paths it touched, and it surfaces under `scripts` rather than `Desktop Core` — run `just file-size-check` locally to reproduce. The 2026-08-14 sync merged cleanly and pushed it 987 → 1002 against a hard limit of 1000 (`desktop/scripts/check-file-sizes.mjs`; upstream's own `mod get;` was +3, the fork's block +14). Fixed by condensing the fork's two comment blocks to 995, since AGENTS.md is where the reasoning belongs — **do not split or reorganise upstream's `relay.rs` to make room**, that trades 5 lines for a permanent conflict surface. Upstream is extracting submodules from this file on its own (`mod get;`, `mod submit;`), so the pressure should ease; if it does not, the fork's ~11 lines here are the budget to work within. **It has not eased, and the budget is now nearly spent.** The 2026-08-20 sync merged cleanly and pushed it 996 → 1003; condensing the fork's two comment blocks again — 5 comment lines down to 2, one per block — brought it to 999. That leaves ~4 fork lines in this file (2 comments, 2 code) and no further comment slack, so the next overflow cannot be absorbed the same way. **When it recurs, move `pub mod allowlist;` back to `lib.rs` rather than touching upstream's code here** — `lib.rs` was the reason for the original move and is now 936 lines with 64 to spare, so the conflict-surface trade has reversed. Re-check both line counts before deciding; do not split upstream's `relay.rs` | +| `desktop/src-tauri/src/relay.rs` | release builds default to the allowlisted relay; also declares `pub mod allowlist;` | Without the default a release build uses `ws://localhost:3000`, which the allowlist then rejects — a client that cannot connect at all. The module is declared *here* because `lib.rs`'s sorted module list is a permanent conflict site, and because `lib.rs` was itself at the 1000-line desktop ratchet when the move was made in the 2026-08-01 sync. `lib.rs` now carries no fork patch at all. **`relay.rs` has since become the constrained file, and the ratchet is how you find out — as a red check, not a merge conflict.** Upstream #6187 (2026-08-19 sync) made the file-size policy a first-class gate: it is now `just file-size-check`, run repository-wide as the **`File size policy`** step of the `scripts` CI job, and it no longer hangs off the per-surface `desktop`/`web`/`mobile` path filters. So an overflow here fails on every PR regardless of which paths it touched, and it surfaces under `scripts` rather than `Desktop Core` — run `just file-size-check` locally to reproduce. The 2026-08-14 sync merged cleanly and pushed it 987 → 1002 against a hard limit of 1000 (`desktop/scripts/check-file-sizes.mjs`; upstream's own `mod get;` was +3, the fork's block +14). Fixed by condensing the fork's two comment blocks to 995, since AGENTS.md is where the reasoning belongs — **do not split or reorganise upstream's `relay.rs` to make room**, that trades 5 lines for a permanent conflict surface. Upstream is extracting submodules from this file on its own (`mod get;`, `mod submit;`), so the pressure should ease; if it does not, the fork's ~11 lines here are the budget to work within. **It has not eased, and the budget is now nearly spent.** The 2026-08-20 sync merged cleanly and pushed it 996 → 1003; condensing the fork's two comment blocks again — 5 comment lines down to 2, one per block — brought it to 999. That leaves ~4 fork lines in this file (2 comments, 2 code) and no further comment slack, so the next overflow cannot be absorbed the same way. **When it recurs, move `pub mod allowlist;` back to `lib.rs` rather than touching upstream's code here** — `lib.rs` was the reason for the original move and is now 936 lines with 64 to spare, so the conflict-surface trade has reversed. Re-check both line counts before deciding; do not split upstream's `relay.rs`. **The pressure has now eased and no relocation is needed.** The 2026-08-25 sync extracted more submodules out of this file (#6447 and neighbours, −406/+95) and left it at **688 lines**, ~310 under the limit, with `lib.rs` at 942. Both fork hunks stayed put and the gate is clear on this file; treat the condensed comments as the floor, not as something to re-expand | | `mobile/lib/shared/relay/relay_allowlist.dart` | new | Mobile counterpart. Skips enforcement under `flutter test` (`FLUTTER_TEST`) because upstream tests use `wss://relay.example.com`; editing those 13 files would be a large permanent conflict surface | | `mobile/lib/shared/relay/relay_socket.dart` | allowlist call in `connect()` | Transport choke point, as on desktop | | `mobile/lib/shared/relay/relay_validation.dart` | allowlist call after the shape checks | One hunk covers all four invite/deep-link call sites; placed after the existing checks so malformed input keeps its original error | @@ -219,11 +219,11 @@ place. | `desktop/src/features/messages/lib/messageLink.ts` + `remarkMessageLinks.ts` + `shared/api/inviteHelpers.ts` | emit `bitcoinmarkets:`, accept both | `messageLink.ts` builds "Copy link" URLs, so it must emit the registered scheme or copied links open the wrong app. The other two are acceptors: `remarkMessageLinks` detects bare URLs in message text (a miss renders a valid link as inert text) and `inviteHelpers` parses invite links. Upstream's `(?:buzz\|buzz)` alternation was degenerate — leftover from its own rename | | `web/src/shared/lib/deep-link.ts` | new | Single source of truth for the scheme in the web client, which is what hands these URLs to the OS. Additive so it cannot conflict; the three call sites (`InvitePage.tsx` ×2, `ConnectButton.tsx`) import it rather than inlining fork-local strings into upstream components. `InvitePage` also says "Accept invite in BitcoinMarkets" | | `desktop/src-tauri/Info.plist` | `CFBundleDisplayName`, `CFBundleName` and the three `NS*UsageDescription` strings → `BitcoinMarkets` | `productName` only renames the `.app` directory, the DMG and the mounted volume. These keys are what macOS displays: Finder reads `CFBundleDisplayName`, the menu bar reads `CFBundleName`, and the usage descriptions are quoted verbatim in system permission prompts. Verified against a built canary before patching — the bundle was `BitcoinMarkets.app` while `CFBundleName` was still `Buzz`, so the app asked for the microphone as "Buzz". `CFBundleIdentifier` and the `buzz-desktop` executable name stay | -| `mobile/ios/Runner/Info.plist` | `CFBundleName` and the three `NS*UsageDescription` strings → `BitcoinMarkets` | The xcconfigs below set `CFBundleDisplayName` (home-screen label); `CFBundleName` is the shorter name iOS falls back to in Settings, and it was still `Buzz`. Usage descriptions appear verbatim in iOS permission prompts | +| `mobile/ios/Runner/Info.plist` | `CFBundleName` and the **five** `NS*UsageDescription` strings → `BitcoinMarkets` | The xcconfigs below set `CFBundleDisplayName` (home-screen label); `CFBundleName` is the shorter name iOS falls back to in Settings, and it was still `Buzz`. Usage descriptions appear verbatim in iOS permission prompts. The in-file comment predicts that a sync adding a permission key conflicts here, and the 2026-08-25 sync was the first: mobile Huddles (#6056) added `NSMicrophoneUsageDescription` and reworded the camera and photo-library strings. Resolution as the comment says — take upstream's new key and its new wording, rewrite `Buzz` to `BitcoinMarkets` | | `mobile/ios/Flutter/Debug.xcconfig`, `Release.xcconfig` | `APP_DISPLAY_NAME = BitcoinMarkets` | iOS home-screen name, debug and release | -| `mobile/android/app/build.gradle.kts` | `app_name` resValue → `BitcoinMarkets`, in `defaultConfig` and the worktree-debug branch | Android launcher label. Two hunks because the worktree label composes onto the same string. **Upstream now writes the same resource from a third place**: #6049's `debugAppName` (read from the override file's `appName` property) was extended in the 2026-08-19 sync into an `if (debugAppName != null) … else if (worktreeLabel != null)` chain, so upstream's branch runs *before* the fork's. That conflicts every time upstream touches the chain, and the resolution is *take upstream's new branch, keep the fork's brand in the fallback* — never replace the fallback with upstream's `"Buzz ($worktreeLabel)"`. The fork's literal is only reachable when no explicit `appName` override is set, which is the normal worktree case | -| `scripts/mobile-worktree-overrides.sh` | branch-labelled debug name | Generates the gitignored per-worktree `APP_DISPLAY_NAME` | -| `scripts/test-mobile-worktree-overrides.sh` | assertions derive the production name from `mobile-worktree-overrides.sh` instead of matching the literal `Buzz` | Four assertions hardcoded `Buzz` and **failed CI on `main` for three commits** after the rename (`ff5e83c28`…`684a15f50`), cascading into `Desktop` and `Desktop E2E Integration` through their gate steps. Deriving the name tests the contract the file is for — release unlabelled, debug labelled, iOS and Android agreeing — so a future rename cannot fail it for the wrong reason | +| `mobile/android/app/build.gradle.kts` | `app_name` resValue → `BitcoinMarkets`, in `defaultConfig` and the worktree-debug fallback | Android launcher label. Two hunks because the worktree label composes onto the same string. **Upstream keeps adding sources that write the same resource, and each one runs *before* the fork's fallback.** #6049 added `debugAppName` (from `AppOverrides.properties`); the 2026-08-19 sync made it an `if/else if` chain; the 2026-08-25 sync (#6056/#6558 huddles work) collapsed the chain into a `resolvedAppName` of `debugAppName ?: worktreeAppName ?: worktreeLabel?.let { … }` and added `worktreeAppName`, read from the *generated* `worktree.properties`. It conflicts every time upstream touches the chain, and the resolution is always *take upstream's structure, keep the fork's brand in the last fallback* — never restore upstream's `"Buzz ($it)"`. **The 2026-08-25 sync is the one to learn from: taking upstream's structure correctly was not enough.** `scripts/mobile-worktree-overrides.sh` now *always* writes `appName=Buzz (