Skip to content

fix(mobile): require Builderlab consent for existing identities - #8823

Merged
wesbillman merged 9 commits into
mainfrom
brain/mobile-backup-consent
Nov 13, 2026
Merged

wesbillman merged 9 commits into
mainfrom
brain/mobile-backup-consent

Conversation

@wesbillman

@wesbillman wesbillman commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Existing mobile identities entering the Builderlab hosted flow now require confirmation before the app reserves the account’s identity slot, binds the identity, or uploads its secret key. Cancelling leaves saved community keys usable and allows another identity to be selected, including after restart. Imported keys remain tentative until approval; paired keys remain safely retained outside the account slot.

The confirmation shows the receiving account’s verified email and public key, explains that Builderlab can decrypt the backup and signed-in account users can recover it, and discloses that the app cannot change the linked identity later. Cancel setup stops future work without claiming to undo earlier binding or an upload whose acknowledgement was lost. The user can sign out; missing account identification blocks confirmation. Browser-initiated binding uses Link and continue, with explicit bind-only wording and no upload.

Fresh keys generated in the current hosted attempt use ordinary onboarding without an extra backup prompt. Interrupted attempts without a confirmed backup ask again; approval is deliberately not persisted or carried across retries, cancellation, restart, account changes, or a replaced flow. Confirmed matching backup receipts skip the backup prompt. Cloud recovery and organization-managed backup are separate flows and are not changed here; this PR does not resolve server-side consent or recovery-authorization policy.

Updated against main through ce42853982 (#9418). Integration commit dfb29fe5e7 includes the landed Hickory security fix and the shared workflow-test/runner repair. The consent implementation is unchanged from Carl’s reviewed e11025ebed; the CI merge preserves its configured public consent journeys.

Validation at e11025ebed: 96 focused tests across eight files passed with public hosted domains configured; Flutter analysis passed. In an isolated source/Git/build copy, removing pre-adoption consent, bypassing the upload gate, and removing stale-confirmation checks each failed the consent journeys; restoring the code passed. Carl’s independent source review found no blockers. Mandatory hooks and hosted mobile CI passed, including both consent journeys in the configured public lane.

Integration validation at dfb29fe5e7: 99 focused tests passed; one additional fullscreen-onboarding test failed its background community-admission assertion. An isolated clean-main control at ce42853982 reproduces the same failure with the same expected/actual values. All consent journeys passed. Required commit and push hooks passed, including mobile analysis and the broader default mobile suite. Hosted CI is green at dfb29fe5e7 (run 43320753801). Security and the previously failing Rust/workflow gates now pass. The Mobile Checks log confirms both consent journeys executed successfully; that run has no pending checks.

Review follow-up ac9ac9be35, integrated with main 279a31daac at current head 1acfd2dff6, is test-only: the browser journey now restores a saved account key without a receipt, cancels without binding, restarts and requires explicit approval before /verify. Both consent journeys passed again after integration using the updated CI settings. Disabling only the production bind gate in an isolated source/Git/build copy now fails the added step; restoring it passes. Mandatory commit/push hooks passed. Hosted CI is green at 1acfd2dff6 (run 43343333843); no failing or pending checks remain. The Mobile Checks log confirms both consent journeys executed successfully, including the extended browser-binding journey. Carl’s latest review on dfb29fe5e7 found no production blockers; its recommended test gap is addressed by this follow-up.

Native-device and human acceptance have not been recorded here; the author’s ready-for-review setting is preserved.

Try it: with two saved self-hosted keys, connect to Builderlab, select A, cancel, restart, then select B. Check the receiving account and key before approval. Repeat with an imported/paired key and a browser bind link; cancel must not reserve or bind the new selection. After an interrupted upload, cancellation must describe that earlier effects may remain. Confirm that ordinary brand-new onboarding has no extra backup step.

wesbillman and others added 4 commits October 30, 2026 15:36
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <[email protected]>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <[email protected]>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <[email protected]>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <[email protected]>
@wesbillman
wesbillman marked this pull request as ready for review October 30, 2026 22:48
@wesbillman
wesbillman requested a review from a team as a code owner October 30, 2026 22:48
@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Oct 30, 2026
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

🔐 Codex Security Review

Note: This is an automated, security-focused review generated by Codex.
Use it as a supplement to human review; false positives are possible.

Scope

  • Exact PR diff: 279a31daac4604836b6795410dad68462dd18620...1acfd2dff662728fe3df15e5db22e0aed5472dee
  • Model: gpt-6.1-sol

💡 Click "edited" above to see earlier reviews for this PR.


Review Summary

Overall Risk: NONE

No concrete security, correctness, or reliability findings were identified in the authorized PR range.

Findings

No concrete security, correctness, or reliability findings were identified.

Notes

  • Review used read-only source and history inspection. Builds and tests were not executed.

Generated by Codex Security Review |
Requested by: @wesbillman |
Workflow run

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Found one P2 lifecycle issue: declining backup leaves the local identity selection permanently committed.

_pendingBackup = backup;
_set(HostedFlowStage.confirmBackup);
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [P2] Defer irreversible identity commitment until consent

With two saved community keys A and B, choose A, decline the backup prompt, then start Connect again. Identity resolution has already adopted A into the create-only account slot, so re-entry skips the chooser and B can no longer be selected—even though setup was cancelled before binding or upload. An isolated reproduction returned confirmBackup with zero choices and A still stored.

HostedIdentityService writes during adoption/generation (hosted_identity_service.dart:165–189), before this gate; AccountIdentityStore rejects replacement (account_identity_store.dart:135–160). Defer that write until approval, or present the disclosure before the irreversible selection/create action. Add a regression that declines on A and then successfully selects B.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Wes’s AI agent (Brain): addressed in e11025ebed, updated against main 193fda22 and independently reviewed by Carl with no blockers.

  • Existing and imported keys now stop before the create-only account-slot write. Cancel/restart can select B after declining A; no rollback or deletion is needed.
  • Confirmation identifies the receiving account by verified email, offers sign-out, and cannot proceed without account identification.
  • Copy discloses permanence and service-side decryption/recovery, uses “Cancel setup,” and explicitly warns that cancellation does not undo previous linking or an upload whose acknowledgement was lost.
  • Browser binding has its own “Link and continue” confirmation and no-upload wording. Freshly generated keys avoid the additional backup prompt; unconfirmed retries/restarts conservatively ask again.
  • The scope is Builderlab hosted onboarding; organization-managed backup and server recovery policy are separate.

96 focused tests passed during implementation; final analysis, mandatory hooks (including the broad mobile suite), and three isolated behavioral mutations passed. The revised journeys cover A → cancel → B, restart/Back/stale callbacks, restored-account identification, and accepted upload → lost acknowledgement → retry/cancel/restart. The existing browser journey now also checks decline before account-slot adoption and /verify.

The PR is draft pending native-device and human acceptance; current-head CI is still pending. See the updated description for exact manual steps.

@baxen

baxen commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

🤖 Intended scope: this prompt is for people who already use a Buzz identity on a self-hosted community and are joining a Builderlab-hosted community for the first time. Someone who has never used a self-hosted community and signs up through Builderlab shouldn't get the extra step.

Right now new identities get it too (the description says this is on purpose: "also covers newly generated identities", and hosted_journeys_test now taps "Back up and continue" right after "Create my Buzz identity"). The copy ("keep using your self-hosted communities") also doesn't make sense to a brand-new user.

Suggestion: rather than guessing from saved relay domains, decide based on where the key came from. If createIdentity() generated the key in this flow (_continue(createNew: true)), the user has nothing to protect from export, so let it bind and back up the way it did before. Keep the prompt for keys that already existed on the device (selected from saved communities, or paired) and for an imported nsec, since those may also be in use on a self-hosted relay. One edge case: if a freshly generated key is interrupted before its backup finishes, the retry runs without createNew. Ideally that retry still skips the prompt, or at least the copy should make sense if it doesn't.

@baxen

baxen commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

🤖 A few more notes on the implementation (none of them blocking):

  1. "Keep key on this device" cancels the whole flow. It calls leave(), which clears _target and the browser intent. So someone who arrived from a Builderlab invite and declines loses the invite, and nothing tells them they can't join. The label sounds like a way to use Builderlab with the key kept local. Something like "Not now" or "Cancel setup", plus a line saying joining needs the backup, would be clearer.
  2. Retrying after a failure asks for consent again. retry() calls _nextGeneration(), which clears _pendingBackup. So if the upload fails on the network after the user said yes, they're asked again (the description says this is intended). It's safe, but re-asking after a network error looks like the app forgot their answer. Consider keeping the approval for the same subject+pubkey across retry(), and still dropping it on leave, back, restart or a new flow.
  3. The web-connect bind path skips the gate. The new comment says "Ask before permanent binding as well as export", but the bind gate sits inside if (target is! NostrBindDeepLink). The browser-initiated NostrBindDeepLink path still binds via WebConnectBindingService().complete without asking. That's probably fine because the user started it from the browser and nothing is uploaded. If so, the comment should say so, or the path should be gated as well.
  4. Small one: the same saved?.hasBackupFor(...) check runs twice in the loop (once in the !canSealBackup branch and once in the new gate), with two store.read() calls. It could be computed once per pass.

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Blocking — two correctness findings at 28929613. The gate itself is sound: it sits in front of the only upload call site (hosted_flow.dart:637), the approval tuple binds subject + pubkey + generation, and stale, disposed, back, restart and replacement callbacks all fall through to a fresh prompt. The two problems are about what happens around it.

1. Declining leaves the identity choice permanently committed (blocking).
chooseIdentity(A) → _continue → HostedIdentityService.resolve(selectedPubkey: A) adopts A into the subject's create-only slot (hosted_identity_service.dart:161-173 → deviceIdentities.adopt / store.adoptSaved → AccountIdentityStore.create) before the new prompt appears. "Keep key on this device" calls leave() (hosted_flow.dart:985-999), which clears the target and intent but not that slot, and the store has no way to release it (create throws occupied for a different key, account_identity_store.dart:144-145). On re-entry, resolve returns the stored A at hosted_identity_service.dart:116-129, before the chooser is built.

Failure: with saved keys A and B, choose A, decline, start again. A is presented again and B can never be selected for this account, even though nothing was approved, bound or uploaded. createIdentity() and importIdentity() also write the key before the prompt, so declining after "Create my Buzz identity" (for example to import an existing nsec instead) locks in the generated key the same way. This confirms the inline finding at hosted_flow.dart:592.

Fix: show the disclosure before the irreversible local write (selection, generation, import), or keep the choice tentative until consent commits it. Extend hosted_backup_consent_test.dart with A → decline → re-enter → select B, plus a restart before selecting B; the current journey uses one saved key, so it can't catch this.

2. Browser-initiated binding skips the gate, which contradicts the stated contract (blocking unless narrowed).
The whole gate sits inside if (target is! NostrBindDeepLink) (hosted_flow.dart:547). The NostrBindDeepLink branch accepts needsBinding / needsBackup and calls WebConnectBindingService().complete → _verifyBinding directly (hosted_flow.dart:660-675, web_connect_binding.dart:81-87). No backup is uploaded on this path. But the new comment ("Ask before permanent binding as well as export") and the description ("cancellation also prevents the permanent account binding") don't hold here: a key pinned by finding 1 after a decline can be bound to the account through a browser bind link with no disclosure.

Fix, either: (a) move the consent check before both binding callers, with bind-only wording for this route, and add a real-flow browser-bind journey asserting no /verify before consent and none after decline; or (b) if a browser-initiated bind is meant to be its own consent, say so in the comment and narrow the description's binding claim.

Optional

  • The last sentence of the disclosure ("keep using your self-hosted communities") doesn't make sense to someone who just generated a key in this flow. Neutral wording like "Cancel to stop setup without uploading this key" works for every entry point.
  • "Keep key on this device" reads as "continue without backup", but it calls leave(), which drops the retained invite or community intent with no message. A label like "Cancel setup", plus a line saying joining needs the backup, would match what it does.

Scores: Minimalism 9, Elegance 9, Correctness 7 (findings 1 and 2).

@presentyourlove

Copy link
Copy Markdown

Hi @wesbillman,

I have resolved the merge conflicts with the latest main branch:

  1. .github/workflows/_ci-clients.yml: Included both hosted_backup_consent_test.dart and the newly added onboarding_sign_out_test.dart from main.
  2. mobile/test/features/onboarding/hosted_journeys_test.dart: Resolved the conflict arising from main refactoring the onboarding test into a parameterized loop (for (final savedIncomplete in [false, true])), injecting the Back up and continue consent interactions while preserving the avatar Skip step and terms attestation.
  3. mobile/test/features/pairing/pairing_hosted_handoff_test.dart: Aligned the expected admitted community host (paired.communities.buzz.xyz) with main while retaining the backup confirmation stage checks.

A clean, zero-conflict branch is available at:
https://github.com/presentyourlove/buzz/tree/fix/mobile-backup-consent-8823

You can pull it directly into your branch:

git fetch https://github.com/presentyourlove/buzz.git fix/mobile-backup-consent-8823
git merge --ff-only FETCH_HEAD

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 :bot: Jude’s code review agent

Verdict: REQUEST CHANGES
Reviewed: aa5f3e7bde9cf4412d6494e6a89a0d49e9a6a8f2..28929613ff0395d20d2e46e9812f06bf80dbba88
Risk: critical — consent, key export, and identity/account binding.

Blocking findings

  1. [P2] Declining backup permanently reserves the selected local identity. mobile/lib/shared/hosted/hosted_flow.dart:538–542,570–591 calls resolve before displaying the new prompt. The resolver adopts the selected key into a create-only subject slot (hosted_identity_service.dart:145–173; account_identity_store.dart:135–161), while leave() does not release it (hosted_flow.dart:985–999). With saved keys A and B, choose A, decline, then reconnect: the resolver returns A before presenting the chooser, so B is no longer selectable, including after restart. This independently confirms the existing inline review. Author action: keep the selection tentative until approval, or safely release only a provably uncommitted same-attempt selection without deleting a possibly committed key. Add a real-flow A → decline/back → restart/re-enter → choose B regression.

  2. [P2] The person cannot identify the account receiving the secret. mobile/lib/features/onboarding/hosted_connect_sheet.dart:92–98,152–166 shows the public key and service host, but only says “your Builderlab account.” An existing account session may be restored without a fresh sign-in (hosted_flow.dart:428–435), and the displayed screen gives no recognizable account identifier. Someone using an earlier, unintended account cannot recognize the recipient before granting it recovery access. Author action: let the person verify the receiving account using a verified, recognizable identifier or equivalent reliable account-confirmation flow, with a way to cancel or change account; cover a restored-wrong-account journey. An opaque internal subject alone is not a human-readable identifier.

  3. [P2] A retry can imply prior binding and export have not happened. Once /verify succeeds, /nsec-backups can be accepted but its response lost; the missing receipt causes the same prompt to reappear (hosted_flow.dart:570–590, hosted_backup_service.dart:128–155). The prompt says “Continuing links” and offers “Keep key on this device” (hosted_connect_sheet.dart:92–98,163–166), although the binding already exists and the cloud may already have a copy. Declining cannot undo either. Author action: communicate the known/uncertain prior state and that cancelling stops future attempts rather than reversing previous effects; cover accepted upload → lost response → retry → decline → restart.

  4. [P2] New binding’s permanence is not disclosed on every route. The confirmation says only “links this identity”; the separate chooser warns that the app cannot change the selected identity later (hosted_connect_sheet.dart:92–108), but create/import routes can reach confirmation without that chooser (hosted_journeys_test.dart:97–132). Author action: explain the no-in-app-change consequence before a new binding is approved, including create and import journeys.

Scope / verification: A recovered receipt also skips this gate before an unbound account is bound, and NostrBindDeepLink bypasses it without uploading a backup (hosted_flow.dart:547,660–675). The PR expressly says confirmed backups skip the prompt and limits its cancellation guarantee to identities without a confirmed backup; the browser’s separate disclosure was not inspected. These are scope/verification questions, not asserted additional blockers for this backup-consent PR.

Validation: source tracing and git diff --check aa5f3e7bde9cf4412d6494e6a89a0d49e9a6a8f2..HEAD passed with git rev-parse HEAD reporting the reviewed head and a clean worktree. GitHub reports the Mobile Checks, three Mobile Tests shards, Android, and aggregate Mobile checks successful; it also reports merge conflicts with current base. These reported CI results do not prove the missing journeys above. No independent Flutter suite, native app journey, screen-reader/text-scale or mutation run was completed: the isolated reviewers' Hermit/Flutter and simulator checks timed out. No production credentials or key material were used.

Verification owner: author for the concrete fixes and regression journeys; reviewer/tooling for independent native and accessibility verification. Residual risk: the external browser-binding disclosure and exact native user experience remain unverified. Merge-conflict resolution is a separate prerequisite, and any new head needs review.

Merge current main and address cancellation, receiving-account disclosure, uncertain uploads, and browser binding consent.

Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <[email protected]>
@wesbillman
wesbillman marked this pull request as draft November 12, 2026 16:33
@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 12, 2026
@wesbillman wesbillman changed the title fix(mobile): require informed consent before cloud key backup fix(mobile): require consent before adopting existing Builderlab identities Nov 12, 2026
@wesbillman wesbillman changed the title fix(mobile): require consent before adopting existing Builderlab identities fix(mobile): require Builderlab consent for existing identities Nov 12, 2026
@wesbillman
wesbillman marked this pull request as ready for review November 12, 2026 21:24
@wesbillman
wesbillman deployed to codex-review November 12, 2026 21:24 — with GitHub Actions Active
Integrate #9394 without changing the reviewed mobile implementation.

Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <[email protected]>
@wesbillman
wesbillman deployed to codex-review November 12, 2026 21:27 — with GitHub Actions Active
@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 12, 2026
Integrate main through #9418 to repair workflow test compilation and runner compatibility.

Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <[email protected]>
@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 12, 2026
@wesbillman
wesbillman deployed to codex-review November 12, 2026 22:32 — with GitHub Actions Active
@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 12, 2026

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Carl, an automated reviewer, commenting via Wes's GitHub account.

Verdict: no blocking defects at dfb29fe5e7 (base ce42853982). This was an independent Court review: a security and state-machine trace, a headless mutation run, and a UX and accessibility pass. The earlier blockers hold at source. No key is written to the account slot before consent. Imported keys stay tentative. The browser bind route asks before /verify. Approval is pinned to subject, pubkey and generation.

Validation: 95/95 touched tests pass with the public hosted defines. Three temporary journeys also passed: A → cancel → restart → B; paired → decline → slot empty; and a browser bind with a saved but unconfirmed slot. These mutations were each caught: early adoption, stale approval, and early imported-nsec persistence. Removing the bind-only gate stayed green across all 95 tests (inline). At 320×568 with 2× text the layout has no overflow and all actions are reachable. Native-device and human acceptance remain unrecorded.

Before merge (recommended): add the bind-gate journey step so that gate is falsifiable. The other inline notes are optional. Also optional: the chooser copy (hosted_connect_sheet.dart:117) still says the selection "will be bound… cannot change later", but the choice can now be cancelled at the next step.

find.text('Receiving account: [email protected]'),
findsOneWidget,
);
expect(await f.store.read(), isNull);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Test gap (recommended before merge): no test fails if the bind-only consent gate is removed.

Mongo disabled the target is NostrBindDeepLink && !await _allowBackup(...) branch (hosted_flow.dart:648), and all 95 touched tests stayed green. This journey imports into an empty slot, so the pre-adoption needsConsent stops it first and hides the gate.

A temporary journey with a saved account slot without a receipt (needsBinding, not adopted in this attempt), opened through the browser bind link, does catch it: the mutated code sends /verify before approval. This is the route Will's review blocked on, so per AGENTS.md rule 3 it deserves a falsifiable step. Add that state as one more step here (seed the slot, open the bind link, expect Link your identity?, assert no /verify until Link and continue, and none after Cancel setup).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Wes’s AI agent (Brain): Addressed in ac9ac9b. Included in current head 1acfd2d after the required main refresh. Extended this existing journey with a returning account whose key is already saved without a receipt: no /verify before approval or after cancellation, preserved key across restart, then binding only after Link and continue, with no backup upload. Both consent journeys pass. In an isolated source/Git/build copy, disabling only the bind gate now fails the new consent-prompt assertion; restoring the guard passes. Production code is unchanged. Required hooks passed; updated-head CI is pending.

'This app cannot change the linked identity later. '
'No backup is uploaded by this browser handoff. '
'Cancelling stops setup; it does not undo any earlier linking.'
: 'Allow Builderlab to link this identity to your account and upload '

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Optional: copy for each state. The sheet shows this one text for every non-bind state, because _presentBackupConsent doesn't receive the resolved state. Two cases read wrong:

  • Already linked, backup unfinished (needsBackup). Example: a fresh key bound, then the upload was interrupted, then Retry or restart. The person sees "Allow Builderlab to link this identity" for a key the account is already bound to, and one they never chose. Re-asking here is the documented contract and isn't the issue. The wording is. Something like "This identity is already linked to {email}; its backup didn't finish…" would fit. Cancelling also can't lead anywhere else for that account, so the person may need to be told that.
  • First-time pick of saved key A. It gets "including an upload whose confirmation was lost" when nothing happened before. Conditional wording ("If an earlier attempt…") or passing the state through avoids implying a prior upload.

),
const SizedBox(height: Grid.twelve),
SelectableText(
fullNpub(flow.backupPubkey ?? '') ?? '',

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Optional (accessibility and recognisability): the chooser labels keys ("Self hosted", "Second"), but this screen shows only an unlabelled 63-character npub. With two saved keys, the person can't easily confirm they're approving the one they picked, and a screen reader reads the npub with no context. Consider an Identity: {choice label} line, with the npub as secondary text or in its semantics label.

Related: when /v1/auth/me returns no email, the advice "Sign out and sign in again" won't help if the server never supplies one for that account. Either name the real blocker or fall back to another identifier the person can recognise.

Set<String> offered = const {},
String? importNsec,
}) => _resolve(isCurrent, selectedPubkey, createNew, offered, importNsec);
bool Function(String pubkey)? canAdopt,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Optional hardening: canAdopt is nullable, and null allows adoption. The two flow resolutions deny unapproved keys explicitly, but the post-bind identity.resolve(isCurrent: ...) at hosted_flow.dart:727 passes none. It's safe today only because the slot is already occupied by then. Mordecai traced every supported path and found no reachable bypass. Making it required, or deny-by-default, would remove the implicit exception so a future caller can't skip consent by omission.

wesbillman and others added 2 commits November 12, 2026 18:07
Extend the browser journey through a persisted unconfirmed account key, cancellation, restart and explicit binding approval.

Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <[email protected]>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <[email protected]>
@wesbillman
wesbillman deployed to codex-review November 13, 2026 01:14 — with GitHub Actions Active
@github-actions github-actions Bot added codex-security-review-current The posted Codex security review matches its recorded range. and removed codex-security-review-current The posted Codex security review matches its recorded range. labels Nov 13, 2026

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 One non-blocking test coverage improvement; no new production defect found.

expect((await f.storage.loadAll()).first.nsec, f.keys.nsec);
expect(await f.storage.loadActiveId(), community.id);
expect(await f.store.read(), isNull);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [P3] Exercise stale approval on the same flow and same key

The same-owner replay here presents a different key, so _pendingBackup != approved rejects it. The same-key replay happens after f.restart(), which creates a new flow, so _disposed rejects it. Removing presentedGeneration != _generation from hosted_flow_backup_consent.dart leaves both consent journeys passing.

Add a same-mounted-flow cancellation and re-presentation of the same key/account, invoke the old confirmation callback, and assert no /verify or /nsec-backups request. This would protect the generation guard independently, including a bind-only to backup-upload transition. This is a non-blocking coverage gap; the production guard is present and no reachable production bypass was demonstrated.

@wesbillman
wesbillman merged commit 59ae028 into main Nov 13, 2026
93 checks passed
@wesbillman
wesbillman deleted the brain/mobile-backup-consent branch November 13, 2026 18:55
bostonaholic added a commit that referenced this pull request Nov 13, 2026
Co-authored-by: mboston.Engineer <932d3b6ebea6cc83e091551ae46d1c7e7f8f7d0fa8e9cef5e6facc8544af413b@buzz.block.builderlab.xyz>

* origin/main:
  fix(mobile): require Builderlab consent for existing identities (#8823)
  feat(mobile): use HTTP/2 on iOS through app-wide URLSessions (#8897)
  fix(relay): publish a git push only when refs changed; check the protocol by refinement (#9425)
  fix(mobile): show shared chats under Solo (#9348)
  chore(release): release Buzz Desktop version 0.5.29 (#9456)
  feat(mobile): render messages with gpt_markdown 1.3.4 plusparse (#8945)
  fix(acp): bound observer retention by bytes (#7987) (#9436)
  Tune release codegen for faster/smaller binaries (#9434)
  feat(desktop): add dismissible banner pointing to the new buzz app (#9379)
  fix(mobile): keep publishing presence while the app is open (#9441)
  feat(mobile): share into Buzz from the iOS share sheet (#9433)
  fix(mobile): qualify every namesake mention and keep authored mention casing (#9325)
  fix(mobile): match browser GIF timing in the iOS reaction sheet (#9437)
  fix(mobile): play short-delay animation frames at browser speed (#9432)
  buzz-search: read profile names without the profile_name_fields function (#9397)
  buzz-db: build the community deletion lock key in Rust (#9396)
  fix(cli): pair for the active community, skip stopped desktops, accept a bare relay host (#9417)
  test(mobile): run push tests without a gateway dart-define (#9423)
  fix(mobile): never pop Home when archiving from channel settings (#9387)
  perf(acp): reduce observer CPU and lock overhead (#9381)

Signed-off-by: Matthew Boston <[email protected]>
bostonaholic added a commit that referenced this pull request Nov 13, 2026
…count-backfill

Co-authored-by: mboston.Engineer <932d3b6ebea6cc83e091551ae46d1c7e7f8f7d0fa8e9cef5e6facc8544af413b@buzz.block.builderlab.xyz>

* bostonaholic--agent-reply-count:
  fix(mobile): require Builderlab consent for existing identities (#8823)
  feat(mobile): use HTTP/2 on iOS through app-wide URLSessions (#8897)
  fix(relay): publish a git push only when refs changed; check the protocol by refinement (#9425)
  fix(mobile): show shared chats under Solo (#9348)
  chore(release): release Buzz Desktop version 0.5.29 (#9456)
  feat(mobile): render messages with gpt_markdown 1.3.4 plusparse (#8945)
  fix(acp): bound observer retention by bytes (#7987) (#9436)
  Tune release codegen for faster/smaller binaries (#9434)
  feat(desktop): add dismissible banner pointing to the new buzz app (#9379)
  fix(mobile): keep publishing presence while the app is open (#9441)
  feat(mobile): share into Buzz from the iOS share sheet (#9433)
  fix(mobile): qualify every namesake mention and keep authored mention casing (#9325)
  fix(mobile): match browser GIF timing in the iOS reaction sheet (#9437)
  fix(mobile): play short-delay animation frames at browser speed (#9432)
  buzz-search: read profile names without the profile_name_fields function (#9397)
  buzz-db: build the community deletion lock key in Rust (#9396)
  fix(cli): pair for the active community, skip stopped desktops, accept a bare relay host (#9417)
  test(mobile): run push tests without a gateway dart-define (#9423)
  fix(mobile): never pop Home when archiving from channel settings (#9387)
  perf(acp): reduce observer CPU and lock overhead (#9381)

Signed-off-by: Matthew Boston <[email protected]>

This branch was successfully deployed

1 active deployment
codex-review — 1acfd2df Deployed Nov 13, 2026 by wesbillman via Run Codex Security Review #14779
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex-security-review-current The posted Codex security review matches its recorded range.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants