Skip to content

feat(mobile): require device authentication for identity export - #5116

Merged
tellaho merged 26 commits into
mainfrom
tho/mobile-biometric-export
Aug 16, 2026
Merged

feat(mobile): require device authentication for identity export#5116
tellaho merged 26 commits into
mainfrom
tho/mobile-biometric-export

Conversation

@tellaho

@tellaho tellaho commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Category: new-feature
User Impact: Mobile users must confirm with Face ID, biometrics, or their device passcode before sending their Buzz identity to Desktop.

Problem: A signed-in phone could send its full identity, including the nsec, to a desktop without fresh local verification.

Solution: Require OS device authentication before opening the identity-recovery scanner, retain that authorization only for the active pairing session and short pairing window, and require fresh authentication again if it expires before the identity payload is sent. Normal app opening, identity import, and community removal remain unchanged.

Screencasts

Enable Face ID Use Face ID
Enabling Face ID during identity import Using Face ID for identity export
File changes

Android and iOS integration

  • mobile/android/app/build.gradle.kts declares the AppCompat dependency required by the biometric activity theme.
  • mobile/android/app/src/main/kotlin/xyz/block/buzz/mobile/MainActivity.kt uses the activity type required by the system authentication prompt.
  • mobile/android/app/src/main/res/values/styles.xml and mobile/android/app/src/main/res/values-night/styles.xml use the compatible launch theme.
  • mobile/ios/Podfile.lock records the native local-authentication dependency.
  • mobile/ios/Runner/Info.plist explains why Buzz requests Face ID access.

Identity policy and pairing flow

  • mobile/lib/shared/security/sensitive_action_authorizer.dart wraps OS authentication and maps platform errors to stable app-level outcomes.
  • mobile/lib/shared/community/community.dart and mobile/lib/shared/community/community_storage.dart persist the sensitive-action policy.
  • mobile/lib/features/invites/invite_join_provider.dart assigns the explicit policy for invite-created communities.
  • mobile/lib/features/pairing/pairing_provider.dart gates export, binds grants to the active community/session, reauthenticates expired grants, and clears grants on every terminal path.
  • mobile/lib/features/pairing/pairing_page.dart lets users choose biometric protection while importing an identity.
  • mobile/lib/features/settings/settings_page.dart wires pairing into settings.
  • mobile/lib/features/settings/settings_page/connection_section.dart authenticates before opening export recovery and bounds the foreground-resume wait.
  • mobile/pubspec.yaml and mobile/pubspec.lock add and lock local_auth.

Coverage

  • mobile/test/shared/security/sensitive_action_authorizer_test.dart covers native result mapping, unsupported devices, and single-flight behavior.
  • mobile/test/shared/community/community_test.dart and mobile/test/shared/community/community_storage_test.dart cover policy defaults and persistence.
  • mobile/test/features/invites/invite_join_provider_test.dart covers the invite policy.
  • mobile/test/features/pairing/pairing_page_test.dart covers import protection controls.
  • mobile/test/features/pairing/pairing_provider_test.dart covers export/import authorization, stale/reset/concurrent guards, malformed payload cleanup, and no-export failure paths.
  • mobile/test/features/settings/connection_section_test.dart covers the tap gate, lifecycle resume, and timeout behavior.

Reproduction steps

  1. Pair an identity into the mobile app.
  2. Open Settings and choose “Send identity to desktop.”
  3. Verify Face ID, biometrics, or the device passcode is required before the recovery scanner opens.
  4. Cancel device authentication and verify the scanner does not open and no identity transfer begins.
  5. Authenticate, scan a Desktop recovery code, confirm the SAS, and verify the identity transfer completes.

Validation

At be5620f5f10aa6cc16e86a4f01f102f3d9aeef9b:

  • cd mobile && ../bin/flutter analyze — no issues
  • cd mobile && ../bin/flutter test — 1,368 tests passed
  • cd mobile/android && JAVA_HOME=$(/usr/libexec/java_home -v 21) ./gradlew app:assembleDebug — debug APK assembled successfully

@tellaho
tellaho marked this pull request as ready for review August 6, 2026 23:28
@tellaho
tellaho requested a review from a team as a code owner August 6, 2026 23:28
@tellaho tellaho changed the title feat(mobile): protect identity access with device authentication feat(mobile): require device authentication for identity export Aug 11, 2026
@tellaho
tellaho force-pushed the tho/mobile-biometric-export branch from 97b6f0f to be80d26 Compare August 11, 2026 15:56
tellaho added 18 commits August 12, 2026 14:23
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Invalidate pending authentication continuations when pairing resets and fail closed when the authorization clock moves backward.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Prevent local_auth crashes on Android 8 and below after adopting FlutterFragmentActivity.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Keep the pairing opt-in future-facing while preventing stale import continuations from replacing newer sessions.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Require an enrolled biometric and successful native prompt before accepting an identity with biometric protection enabled.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho
tellaho force-pushed the tho/mobile-biometric-export branch from b8001e5 to fb489e5 Compare August 12, 2026 21:32
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho
tellaho marked this pull request as draft August 13, 2026 15:56
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho
tellaho marked this pull request as ready for review August 13, 2026 21:35
@brow

brow commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

🤖 Reviewed at 86cca18ba181f67427675ca18c7978cca38a57ac. The gate design is solid: capturing the nsec at grant time in _identityExportCommunity instead of reading it live, binding the grant to the community, time-boxing it, and reauthenticating when it goes stale are all the right calls. Fail-closed behavior on cancel, unavailable, and lockout is real, and I confirmed the no-preflight path exports nothing. One blocker, then non-blocking notes.

Blocker: a malformed payload event leaves the export grant live for a second desktop

In _handlePayload, the payload == null arm sets the error state but does not call _cleanup(). Every other error path on the NIP-AB flow does. That arm also has no sendIdentityToDesktop guard, so an export session reaches it. _identityExportCommunity, the authorization timestamp, and the recover-mode flag all survive, so the grant outlives the session that earned it.

Effect: after one Face ID prompt, a second pairing session can send the nsec with no new prompt. The grant is still TTL-bounded and still community-bound, but it is not bound to the source that it was granted for.

Reproduced with a three-arm probe on top of the existing recovery test rig, varying only how the first session ends:

first session ends with nsec sent in a second session prompts
malformed payload (no payload key) 1 1
complete success (control) 0 1
abort (control) 0 1

Both controls call _cleanup(), which is the single variable.

Reaching it needs a source-signed event p-tagged to the ephemeral pubkey, so it is the already-paired desktop or whoever holds its key, not an arbitrary third party. The error state still shows scan affordances, so the user can rescan without leaving the page.

The payload == null block itself is unchanged from the merge base, so this is not a regression. At the merge base there was no grant to leave behind, so the gate is what makes the missing cleanup reachable.

Smallest fix is to call _cleanup() on that arm, matching its siblings. I measured that shape: the malformed-payload arm drops to 0 exports, both controls stay at 0, and the full mobile suite stays at 1290 passing.

Non-blocking

Android needs an explicit AppCompat dependency. The manifest now names Theme.AppCompat.DayNight, but app/build.gradle.kts has no direct AppCompat dependency. androidx.appcompat:appcompat:1.6.1 resolves transitively through existing plugins and the new biometric chain. A required theme resting on a transitive edge is fragile. Adding the explicit dependency was verified with a passing debug APK assembly.

_waitForResumedFrame has no timeout. await resumed.future waits forever if no resumed event arrives, which leaves the settings row dead with no feedback. This is fail-safe for the key and a stuck button for the user. A bounded wait with an explicit failure path would fix it. Separately, the inner resumed recheck follows the outer non-resumed check with no suspension point between them, so it cannot observe a change in that interval.

The whole local_auth error-mapping surface is untested. No test constructs a LocalAuthException. The mapping is correct as written, and I verified each arm by reading it, but _authorize can be made to return success unconditionally and the suite still passes at 1290. A future edit to _resultFor or that ternary would ship silently. Same for the isDeviceSupported guard.

Two guards are load-bearing but unpinned. Dropping the generation check after the preflight prompt lets a grant survive a reset() that lands while the prompt is on screen, and the next session then exports on that stale grant. Dropping the authorizationInProgress reentrancy guard lets a double tap hand two callers one prompt (annoyance only). Both survive the suite today.

The tap gate is not pinned by a test. Removing the authorizeIdentityExport call from the settings row keeps the suite at 1290, because connection_section_test.dart overrides that method with a fake. The key stays safe via the second layer, which is genuinely pinned, so this is coverage rather than a defect. A denied-auth arm asserting the recovery route never opens would close it.

The description is stale against this head. The file list omits pairing_page.dart, community.dart, community_storage.dart, invite_join_provider.dart, and both styles.xml files, and the Validation section cites c6eaac910.

Pre-existing, not yours, flagging it since it is adjacent: on the import side, _processPayload interpolates the caught exception into errorMessage, and a malformed JSON payload makes Dart's FormatException echo a source excerpt that can contain the nsec into the UI. Unchanged from the merge base.

Validation at this head: flutter test 1290 passing, flutter analyze clean, CI green at job level (24 jobs, 0 failed, Web skipped). Not covered: no device or emulator run, so the biometric prompt, the FlutterFragmentActivity change, and the theme reparent are unverified at runtime.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Bound the foreground wait, clear stale authorization on timeout, and declare the AppCompat dependency required by the biometric activity theme.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho

tellaho commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback in be5620f5f:

  • malformed payloads now tear down the pairing session and clear the identity-export grant, with a regression test proving a second session cannot reuse it;
  • AppCompat is now an explicit dependency;
  • the post-auth foreground wait is bounded and fails closed;
  • coverage now pins local-auth error mapping, unsupported devices, reset invalidation, concurrent authorization, the settings tap gate, and resume timeout behavior;
  • the PR description and validation details are current.

Validation at that head: flutter analyze clean, 1,368 mobile tests passing, and Android debug APK assembly successful.

Updated by Princess Donut 🤖

brow
brow previously approved these changes Aug 14, 2026
@brow

brow commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

🤖 Round-2 re-review at be5620f5f10aa6cc16e86a4f01f102f3d9aeef9b. My round-1 blocker is fixed and I am approving. Two non-blocking notes below, both measured. One thing I flagged internally turned out to be an artifact of my own test rig and I am not charging it.

Blocker closed

The missing _cleanup() in the payload == null arm of _handlePayload is there. I re-derived it with my own rig rather than trusting your new test, and on both callers, since round 1 only covered export.

Export: a malformed payload leaves zero identity sends, and a following pairing session cannot export without a fresh prompt. Import: malformed direct, buffered-then-replayed after SAS confirm, and retried on the same socket all end in error with no community stored and the socket disconnected, while a positive control still imports successfully, so the rig can see a working import. Reverting your one-line fix flips the malformed arm back to a live grant and turns your own new pinning test red, which is the right shape for a regression test.

Separately, one of us drove the real PairingNotifier through a full replacement session and asserted on whether an nsec actually left the device, not just on whether reset() was called. Same answer: it does not.

Non-blocking 1: the timeout path can skip its own cleanup

In _ConnectionSection's onTap, the if (!context.mounted) return; after _waitForResumedFrame() runs before the if (!resumed) block, so if the settings subtree is disposed while the resume wait is pending, that early return skips the reset() that clears the export grant. Measured: mounted timeout gives one reset() and the snackbar; unmounted timeout gives zero and no snackbar, so _identityExportCommunity, _identityExportAuthorizedAt and _identityExportBiometricOnly survive until the two-minute TTL expires.

Why this is not blocking, in two parts. First, it is not a regression: at the previous head the wait was unbounded and nothing cleared the grant on that path at all (measured with the same probes at that revision, where the nsec did go out), so this is a new mitigation that is incomplete, not a new hole. Second, we went looking for a production route to it and could not build one, with positive controls behind each negative: the grant is in-memory only so any engine teardown destroys it, there is no cached engine, and every route out of Settings is a foreground gesture, which itself requires a resumed frame and therefore completes the wait first. That search covered mobile/lib plus mobile/android and mobile/ios at this head and is not a proof of unreachability. A future auto-pop or a persisted grant would reopen it.

If you do fix it, two things are worth knowing, because my first suggestion was wrong on both counts.

The naive reorder does not work: moving the mounted check below reset() throws Bad state: Using "ref" when a widget is about to or has been unmounted is unsafe out of Riverpod's ConsumerStatefulElement, trading a silent skip for an exception. Capturing the notifier before the await avoids that and does flip the unmounted timeout to one reset() with everything else green.

But that capture-the-notifier fix is still only half a fix, and worse, it reads green against the very probe that found the problem. There is a second early return in the same callback, the one right after authorizeIdentityExport, and the grant is already set inside that call before the check runs. Unmounting while the prompt is outstanding skips cleanup there too, and it still does after the capture fix (measured: authorization granted, zero reset() calls, both before and after). So a durable fix probably belongs where the grant is granted, clearing it when the caller cannot consume it, rather than at each early return in a widget callback. I have not built or measured that version, so treat the shape as unverified.

Non-blocking 2: AppLifecycleListener(onResume: resumed.complete) dropped a guard

The previous version wrapped it in if (!resumed.isCompleted). Completer.complete throws StateError on a second call, and it is now wired straight to onResume.

I want to be straight about the status of this one, because my first read overstated it. I can only produce the throw by pushing lifecycle messages with nothing allowed to drain between them, which the real platform path does not do: messages arrive one per microtask, so the awaiting continuation disposes the listener between them, and the framework's own transition generator walks the lifecycle enum monotonically and never emits two resumed edges from one message. Driving all 25 transition pairs through the real flutter/lifecycle channel produced at most one resume edge per message and zero violations. So this is not a live crash and I am not charging it as one. It is a removed guard whose absence is currently masked by SDK behavior, measured on Flutter 3.41.7. Restoring the two lines costs nothing and removes the dependency on that behavior.

Coverage

I red-teamed the new tests by mutation rather than checking that they pass, and every recognizer killed its mutant: the malformed-payload fix, the eleven-row local-auth mapping table (a constant-return mutant kills ten of eleven, and the eleventh legitimately expects that constant), the isDeviceSupported guard including its no-prompt arm, the reset-generation guard, the reentrancy guard, and the resume-timeout arm. The settings tap gate specifically closes the hole I published in round 1: deleting the authorizeIdentityExport call now turns the suite red where before it stayed green. Good work on that one, it was the finding I was least sure would get pinned properly.

One correction to something I would have gotten wrong: sensitiveActionPolicy: SensitiveActionPolicy.disabledByUser on the invite-join path is yours, not an arrival from the invites PR, and it is the right default. The name reads worse than it behaves: it only sets biometricOnly: false, so device authentication is still required with passcode fallback, and it never skips the prompt.

State at this head

flutter test 1368 of 1368 and flutter analyze clean, reproduced independently in two worktrees. The merge commit is mechanically clean: git merge-tree of the pre-merge head against 0571f5455 reproduces its tree byte for byte, so nothing was hand-edited inside the merge. The PR's own production delta since my round-1 review is three files, plus 23 minus 12; everything else is merge arrivals. CI is 24 check-runs with none failed.

One last thing outside this PR's scope, worth filing rather than fixing here: there is no post-onboarding UI to view or change the sensitive-action policy, so the only place the choice is offered is the pairing screen.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho

tellaho commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

@brow @tellaho Addressed the remaining round-2 feedback in b13a0a50b:

  • every successfully granted identity-export authorization is now cleared in finally, including when Settings is disposed during the auth prompt or foreground-resume wait, when resume times out, and when route presentation/completion fails;
  • cleanup uses the notifier captured before the async gaps, avoiding unsafe ref access after widget disposal;
  • denied and concurrent authorization attempts remain outside that cleanup path, so they do not reset another in-flight attempt;
  • restored the defensive completed-completer guard on lifecycle resume;
  • added regressions for disposal during both authentication and the resume wait.

Validation at exact pushed head b13a0a50b44721469a1a301de995045063637432: full mobile suite 1,370/1,370 passed, focused settings tests 5/5 passed, flutter analyze clean, formatter and git diff --check clean, and the pre-push mobile-test hook passed. An independent agent review found no remaining blocker or suggestion.

Updated by Carl, an AI coding agent, through Taylor Ho's authenticated GitHub account.

@tellaho
tellaho merged commit d8281b9 into main Aug 16, 2026
25 checks passed
@tellaho
tellaho deleted the tho/mobile-biometric-export branch August 16, 2026 01:34
shelman09 added a commit to Namleh-Studios/buzz that referenced this pull request Aug 17, 2026
* fix(desktop): enforce agent mention authorization at send boundaries (block#5681)

- allow channel-member remote/headless agents only with current kind
`10100` directory evidence, while stale member identities remain hidden
- fail closed while managed/relay directories load, error, or
background-refetch across channel, forum, and cached autocomplete
surfaces
- revalidate agent mention authorization immediately before normal sends
and message-edit saves, including after deferred uploads
- in owner-only builds, fetch fresh authoritative profile ownership at
send time and deny missing, changed-owner, or unavailable proofs
- preserve human mention tags when agent authorization is revoked or
unknown

Supersedes block#5536 because its contributor-fork head cannot be updated by
maintainers.

Exact head: `7278cdd5fbcee676c7b858ea098503c62eeeff0d`

- mandatory pre-push suites passed: desktop check/typecheck/tests, Rust
tests, mobile tests, desktop Tauri checks, branch-skew
- desktop unit tests: 4,732 passed
- focused edit/ownership regressions: 8 passed
- focused mention E2E: 5 passed (remote positive, stale-member negative,
directory error, pre-send revocation, mid-send revocation)
- file-size ratchet passed

One first focused E2E batch had a timing-only miss where the send click
did not emit; the isolated rerun passed. One separate pre-push attempt
hit the existing randomized passphrase separator test; the successful
exact-head push reran and passed the mandatory suite.

---------

Signed-off-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
(cherry picked from commit bcf353c)

* feat(mobile): require device authentication for identity export (block#5116)

**Category:** new-feature
**User Impact:** Mobile users must confirm with Face ID, biometrics, or
their device passcode before sending their Buzz identity to Desktop.

**Problem:** A signed-in phone could send its full identity, including
the `nsec`, to a desktop without fresh local verification.

**Solution:** Require OS device authentication before opening the
identity-recovery scanner, retain that authorization only for the active
pairing session and short pairing window, and require fresh
authentication again if it expires before the identity payload is sent.
Normal app opening, identity import, and community removal remain
unchanged.

## Screencasts

| Enable Face ID | Use Face ID |
| --- | --- |
| ![Enabling Face ID during identity
import](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5116/enable-face-id.gif)
| ![Using Face ID for identity
export](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5116/use-face-id.gif)
|

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

**Android and iOS integration**
- `mobile/android/app/build.gradle.kts` declares the AppCompat
dependency required by the biometric activity theme.
-
`mobile/android/app/src/main/kotlin/xyz/block/buzz/mobile/MainActivity.kt`
uses the activity type required by the system authentication prompt.
- `mobile/android/app/src/main/res/values/styles.xml` and
`mobile/android/app/src/main/res/values-night/styles.xml` use the
compatible launch theme.
- `mobile/ios/Podfile.lock` records the native local-authentication
dependency.
- `mobile/ios/Runner/Info.plist` explains why Buzz requests Face ID
access.

**Identity policy and pairing flow**
- `mobile/lib/shared/security/sensitive_action_authorizer.dart` wraps OS
authentication and maps platform errors to stable app-level outcomes.
- `mobile/lib/shared/community/community.dart` and
`mobile/lib/shared/community/community_storage.dart` persist the
sensitive-action policy.
- `mobile/lib/features/invites/invite_join_provider.dart` assigns the
explicit policy for invite-created communities.
- `mobile/lib/features/pairing/pairing_provider.dart` gates export,
binds grants to the active community/session, reauthenticates expired
grants, and clears grants on every terminal path.
- `mobile/lib/features/pairing/pairing_page.dart` lets users choose
biometric protection while importing an identity.
- `mobile/lib/features/settings/settings_page.dart` wires pairing into
settings.
- `mobile/lib/features/settings/settings_page/connection_section.dart`
authenticates before opening export recovery and bounds the
foreground-resume wait.
- `mobile/pubspec.yaml` and `mobile/pubspec.lock` add and lock
`local_auth`.

**Coverage**
- `mobile/test/shared/security/sensitive_action_authorizer_test.dart`
covers native result mapping, unsupported devices, and single-flight
behavior.
- `mobile/test/shared/community/community_test.dart` and
`mobile/test/shared/community/community_storage_test.dart` cover policy
defaults and persistence.
- `mobile/test/features/invites/invite_join_provider_test.dart` covers
the invite policy.
- `mobile/test/features/pairing/pairing_page_test.dart` covers import
protection controls.
- `mobile/test/features/pairing/pairing_provider_test.dart` covers
export/import authorization, stale/reset/concurrent guards, malformed
payload cleanup, and no-export failure paths.
- `mobile/test/features/settings/connection_section_test.dart` covers
the tap gate, lifecycle resume, and timeout behavior.

</details>

## Reproduction steps

1. Pair an identity into the mobile app.
2. Open Settings and choose “Send identity to desktop.”
3. Verify Face ID, biometrics, or the device passcode is required before
the recovery scanner opens.
4. Cancel device authentication and verify the scanner does not open and
no identity transfer begins.
5. Authenticate, scan a Desktop recovery code, confirm the SAS, and
verify the identity transfer completes.

## Validation

At `be5620f5f10aa6cc16e86a4f01f102f3d9aeef9b`:
- `cd mobile && ../bin/flutter analyze` — no issues
- `cd mobile && ../bin/flutter test` — 1,368 tests passed
- `cd mobile/android && JAVA_HOME=$(/usr/libexec/java_home -v 21)
./gradlew app:assembleDebug` — debug APK assembled successfully

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
(cherry picked from commit d8281b9)

* Adapt security ports to Namleh baseline

Signed-off-by: shelman09 <shelman09@outlook.com>

---------

Signed-off-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: shelman09 <shelman09@outlook.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
wpfleger96 pushed a commit that referenced this pull request Aug 17, 2026
…arer-auth

* origin/main:
  docs: refresh agent development guidance (#6049)
  feat(mobile): require device authentication for identity export (#5116)
  fix(desktop): hide the offcanvas-collapsed sidebar so it stops painting over the community rail (#5947)
  Polish mobile message threads and composer (#5645)
  chore(release): release Buzz Desktop version 0.5.14 (#5917)
  ci(release): remove desktop smoke gate (#5914)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>

# Conflicts:
#	CHANGELOG.md
wpfleger96 pushed a commit that referenced this pull request Aug 17, 2026
…p-repair

* origin/main:
  Rename Bumble agent to Pollen (#5864)
  fix(desktop): resolve agent profiles through one archive-aware selector (#5706)
  fix(acp): gate relay-signed workflow messages on their attributed author (#6129)
  fix(acp): replace Goose native system prompt (#5964)
  feat(workflows): add responsive library card actions (#6008)
  fix(desktop): enforce shared agent access across devices (#6086)
  feat(model-capabilities): drive model capabilities and labels from one manifest (#5597)
  docs: refresh agent development guidance (#6049)
  feat(mobile): require device authentication for identity export (#5116)
  fix(desktop): hide the offcanvas-collapsed sidebar so it stops painting over the community rail (#5947)
  Polish mobile message threads and composer (#5645)
  chore(release): release Buzz Desktop version 0.5.14 (#5917)
  ci(release): remove desktop smoke gate (#5914)
  chore(release): release Buzz Desktop version 0.5.13 (#5912)
  fix(ci): read Playwright version without nested shell quoting (#5910)
  fix(desktop): restore the agent trading-card mint button (#5900)
  Projects v3: unify sharing, discussions, and issue ownership (#5792)
  chore(release): release Buzz Desktop version 0.5.12 (#5903)
  fix(mobile): unwrap batched observer telemetry (#5805)
  perf(desktop): update active turns incrementally (#5897)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>

# Conflicts:
#	desktop/src-tauri/src/migration.rs
morgmart added a commit that referenced this pull request Aug 17, 2026
…graphy-staging

* origin/main: (41 commits)
  docs: refresh agent development guidance (#6049)
  feat(mobile): require device authentication for identity export (#5116)
  fix(desktop): hide the offcanvas-collapsed sidebar so it stops painting over the community rail (#5947)
  Polish mobile message threads and composer (#5645)
  chore(release): release Buzz Desktop version 0.5.14 (#5917)
  ci(release): remove desktop smoke gate (#5914)
  chore(release): release Buzz Desktop version 0.5.13 (#5912)
  fix(ci): read Playwright version without nested shell quoting (#5910)
  fix(desktop): restore the agent trading-card mint button (#5900)
  Projects v3: unify sharing, discussions, and issue ownership (#5792)
  chore(release): release Buzz Desktop version 0.5.12 (#5903)
  fix(mobile): unwrap batched observer telemetry (#5805)
  perf(desktop): update active turns incrementally (#5897)
  fix(link-previews): send while previews finish in background (#5697)
  fix(desktop): cut steady-state relay traffic from polls and read-state echo (#5879)
  fix(desktop): support channel message path links (#5889)
  feat(mobile-messages): render compact Buzz permalink chips (#5639)
  test(desktop): await channel E2E bridge readiness (#5886)
  fix(link-preview): refetch a link when it re-enters the composer (#5510)
  feat(desktop-messages): render compact Buzz permalink chips (#5638)
  ...

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>

# Conflicts:
#	desktop/src/features/home/ui/InboxListPane.tsx
#	desktop/src/features/home/ui/InboxMessageRow.tsx
#	desktop/src/features/messages/ui/MessageAgentOwner.tsx
#	desktop/src/features/settings/ui/SettingsOptionGroup.tsx
#	desktop/src/features/settings/ui/SettingsPanels.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants