Repository navigation
feat(identity): link the first-run key to the account, and back it up through the observable native uploader - #1806
Closed
matt2e wants to merge 5 commits into
Conversation
…ilderlab account ("Linking your key…")
Implements C1 from the onboarding plan (§7): first run now binds the key
it creates or imports to the signed-in Builderlab account instead of
leaving binding inside the "Backing up and linking your key" placeholder.
FirstRunAccount gains bind(pubkey). It reads kgoose `current` first and
returns without another request when the account already holds that
key (the plan's amendment), so running it again after a failure never
meets 409 `pubkey_already_bound`. Otherwise it requests a challenge for
the portal origin, has the native identity_sign_builderlab_binding
command sign it as a kind-24243 event, and posts it to verify, which
must confirm this key. Every request carries the portal `Origin`.
Refused links (409 `identity_already_bound` / `pubkey_already_bound`)
show the hosted-communities card's copy; other failures name the step
and HTTP status, as the account check does, and a native signer refusal
surfaces as-is.
setup.ts replaces backUpAndLink with linkKey ("Linking your key…"),
which binds the saved viewer key; backUp is now only "Backing up your
key… [TODO]". Welcome runs it:
- Manage: Creating keys → Linking your key → Backing up your key.
- Own key: Creating keys → Linking your key, then the key reveal.
- Import on the unlinked (new-account) lane: Importing your key… →
Linking your key → Join a community.
- Import on the Welcome back (linked) lane: no link step; the imported
key is the linked one.
The development seed (seededAccount) fakes bind() with no request and
no native signature, so a developer's real binding never changes.
The native signer (src-tauri/src/identity.rs) now accepts a challenge
for production Builderlab or any login origin the build grants
(buzzodz_plugins::builderlab_origins(): the BUZZ_BUILDERLAB_URL origin
and each tenant's) instead of only production or the build URL origin.
Unknown, case-changed, trailing-slash, port-changed and empty origins
are still refused.
Tests:
- Rust: the template allow-list test covers granted, production,
ungranted and malformed origins; the production-IPC test signs and
verifies for every granted origin and still refuses a foreign one
(fails on the old rule with "https://block.builderlab.xyz was
refused").
- builderlab-account.test.ts: bind's full request shapes at a tenant
and at the build URL, the exact signer invoke, the already-bound
no-op, the card's 409 copy, non-JSON and unconfirmed verify answers,
and a signer refusal.
- identity.test.tsx: the Manage, own-key and unlinked-import journeys
hold bind on a gate and assert "Linking your key…" and its key; the
linked lane never binds; a failed link stops setup before the backup
and Back rests on Join a community without entering the app; the
seed links with only identity_restore/identity_create invokes.
17 TypeScript cases fail against the previous builderlab-account.ts,
setup.ts and Welcome.tsx; the seed journey was checked as a guard by
removing the fake bind.
Docs: identity.md, configuration.md and the builderlab README describe
the link step, the seed's fake binding and the signer's origin rule.
No FOUNDATION file changed. The "Create my key" / Try again / Continue
anyway product rule has not landed on this branch and is not
implemented here.
Validation: bin/pnpm vitest run src/features/identity
src/features/communities src/app src/bundled (4090 passed, 8 skipped),
tsc --noEmit, pnpm lint, biome check on changed files, cargo test
--manifest-path src-tauri/Cargo.toml --locked identity (33 passed),
cargo fmt --check, git diff --check.
Known interim gaps:
- A link failure on the Manage, own-key or unlinked-import lane ends at
Couldn't finish setup; Back lands on Join a community with the key
saved but unlinked, and on the own-key lane never revealed. C2's
fallback closes this.
- Start over → Set up your key links a new key while kgoose still holds
the old one, so a real kgoose refuses it (409
`identity_already_bound`) until C4's real unlink lands.
Not exercised in the native app: the Builderlab/Okta sign-in is
human-only, so plugin_host_request forwarding `Origin`, the staging
portal accepting challenge/verify from the first-run credential, and a
real bind remain unverified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ket for each handoff First half of C2 (note 4f94a6a2 §7), on top of C1 (e850e92): the native uploader becomes observable, so the first-run waiter and a Settings row can follow up on a specific backup pass. Neither of those is in this commit. State model (C2.1, src-tauri/src/nsec_backup.rs) - `BackupStatus { seq, pass, builderlab, status… }`, where `status` is one of: - `idle`, `running`, `complete`; - `waiting { reason: no_consent | no_session | not_linked }`; - `failed { kind: busy | signed_out | not_offered | other_key_linked | invalid_service | refused | unreachable | unconfirmed | storage }`. - `builderlab` is true when a storage root exists and the Builderlab source is allowed for this build. - `backup()` now returns `Result<Outcome, Stop>`. Failures follow C2.4's native rows: - 401 → signed_out. - 403 → other_key_linked on upload, not_offered on enrollment; 404 → not_offered. - 429, 5xx and transport errors → unreachable; other 4xx → refused. - Enrollment 409 → waiting{not_linked}. - An invalid URL, credential or enrollment → invalid_service. - An upload response that isn't a stored, matching receipt → unconfirmed. - `Api` reads fold every status into one error, so they all report unreachable. - Another copy holding the consent lock → busy (`nsec_backup_choice::BUSY`). - Other local consent, record or key I/O → storage. No response body crosses IPC. - Publish points, all under the state lock so pushes leave in seq order: - A spawned pass publishes `running`, then its outcome. - A queued rerun publishes `running` under the latest ticket. - A null handoff that retires a running pass publishes waiting{no_session}. - `document_load` turns a running pass into `idle`. - A retired or closed pass publishes nothing; the next pass's `running` replaces it. Tickets - `trigger`, `session` and `retry` return `Option<u64>`: the number of a pass that starts after the call, or None when no pass will. - A wake during a pass gets a ticket covered by the single queued rerun. - `nsec_backup_session` and `nsec_backup_retry` return `number | null`. Exposure (C2.2) - `nsec_backup_state` returns the snapshot. - `nsec_backup_watch(Channel)` replaces the single watcher slot and refuses any window but main, as `deep_link_watch` does. A stale channel's failed send is ignored. - Both commands are registered in lib.rs, build.rs, capabilities/default.json and browser_permissions_tests.rs. TypeScript - `createBackupStatus` (src/features/identity/backup.ts) registers nothing until the first `subscribe` or `ready()`. Then it watches before reading the snapshot and keeps the higher `seq`. - After a failed registration, the next reader registers again. `dispose` silences the channel. - `identity.backup` exposes the store. It stays out of `IdentitySnapshot`, so the identity journeys' exact invoke lists are unchanged. - `handoffBackupSession` resolves to the ticket, or null on a refusal or rejection, and never rejects. Existing callers (Settings plugin, hosted card, agents client) still ignore the result; the worker behaves as before for them. - docs/identity.md describes the observable status. Tests - Rust cases 1–12 from C2.9: - Running, then the outcome, under the returned ticket. - The full waiting/failure table, including busy from a held consent lock. - Rerun tickets; supersede by key install; null handoff; document_load with a stale and a new watcher; no consent; not linked, then retried. - Ticket returns; retry after a timeout reusing the same bytes; flat serialization and `builderlab`; permissions. - Four existing backup tests now assert typed `Stop` kinds instead of `is_err()`. - New tests wait at most 5 s for an upload to start, then fail with the statuses the pass reported, so they don't hang. - backup.test.ts covers: - Lazy registration and watch-then-state order. - Keeping the higher seq from the channel or the snapshot. - Registering again after a failed watch, and dispose. - Handoff tickets and null. - backup-choice.test.ts checks that creating an identity never invokes the watch or state commands. - Mutation checks: - Breaking four Rust publish points failed four targeted tests. - Six TS store mutations each failed a backup.test.ts case: equal seq accepted, failed watch cached, dispose leaving the channel live, state read before watch, rejection propagating, ready() returning the first status. Validation run on this snapshot (macOS): - `cargo test --manifest-path src-tauri/Cargo.toml --locked`: - 47/47 nsec_backup tests in 15 isolated runs, and again after the last test edit; the permissions test passes. - Full-suite runs pass apart from the flakiness below. The last one failed once: case 6 reported failed{storage}. - cargo fmt --check, and `cargo clippy --workspace --locked --all-targets -- -D warnings`. - `vitest run src/features/identity src/bundled src/app` (3807 passed). - tsc --noEmit, pnpm lint, biome check on changed files, git diff --check. Known flakiness (pre-existing, not fixed here) - Under the full suite on macOS, nsec_backup tests sometimes see a spurious busy or storage result. Some of these are new tests. - Cause: `fs2` flock locks are inherited by children that other tests in the same process spawn, until each child execs. A scratch probe, run 3000 times, found the shared lock still busy right after the exclusive lock was released: - 0 times with no spawner; - 31 times with a `posix_spawn` spawner; - 97 times with a `fork` + `pre_exec` spawner. - The base commit shows the same: `absent_or_invalid_consent…` failed in 2 of 8 full runs. - Fixing it means changing how the consent and record locks treat contention, which this commit leaves alone. It needs a decision, because in the app a concurrent agent or terminal spawn could surface as a spurious failed{busy}. Linux CI exposure was not measured. Diff budget: about +338/−57 production lines (+240/−46 in nsec_backup.rs) against the plan's ≈+150/−30 for Rust. The overrun is rustfmt expansion, doc comments on the specified types, and the `From`/`classify` helpers the classification table requires. No machinery beyond the plan. No FOUNDATION files. No rebase. The native app was not run: no installed-app, WebView-channel or live-service check was performed. Next: the first-run waiter (`backUpWith`), backup-failure.ts, setup.ts / Welcome.tsx wiring, and the Settings row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…d cannot hold them Fixes the pre-existing nsec_backup lock flake noted in 27ee14d. The consent lock (`nsec_backup_choice::lock`) and the per-identity record lock (`persistence::LockedRecord`) relied on closing their file to release the `flock`. Rust opens files close-on-exec, but a child spawned through fork or posix_spawn shares the parent's open file description until it calls exec. Closing our descriptor does not release a `flock` while another descriptor still refers to that description, so the next `try_lock` saw contention. The pass then reported a spurious `failed{busy}` (consent) or `failed{storage}` (record). Root-cause fix instead of the agreed bounded retry: an explicit `flock(LOCK_UN)` releases the lock for every descriptor that shares the description, including the child's inherited copy. New `nsec_backup_choice::Lease` holds the locked file and unlocks it in `Drop`, and `lease(file, exclusive)` is the only way to build one. Both sites use it. `BackupChoice._lease`, `set`'s exclusive lease and `LockedRecord._lock` now hold a `Lease`. This is the pattern `crates/credential-store` (`Lock`) and agent-controller's `bundle_lock.rs` already use, with the same comment. Why not the retry: once the lock is released at drop, there is no transient contention left to wait out. A retry would add a timer and delay for no remaining cause. Contention from a lease that is genuinely held (another app copy, or a choice change during an upload) still reports `busy` / the record failure at once, exactly as before. Error messages and every caller are unchanged. Windows handles are not inheritable and already released on close; the explicit unlock is harmless there. Scratch probe (outside the repo, 3000 trials each, four spawner threads, "lock exclusive, release, then try_lock_shared on a new descriptor"): | spawner | close only | unlock, then close | |--------------------------------|------------|--------------------| | none | 0 | 0 | | posix_spawn (`std::process`) | 29 | 0 | | fork + `pre_exec` | 57 | 0 | Tests: one per lock site. A `dup` of the lease's descriptor (`try_clone`, via the test-only `Lease::inherited()`) stands in for a spawned child's inherited copy, since it is the same kernel relationship and needs no spawn. With that copy still open, a held lease makes another acquisition fail at once (`BUSY` / the record's `FAILED`). Once the lease drops, the lock is acquired. - nsec_backup_choice/tests.rs: a_released_lease_is_free_even_while_a_spawned_child_still_shares_it - nsec_backup/persistence.rs: a_released_record_lock_is_free_even_while_a_spawned_child_still_shares_it With the `Drop` unlock removed, both fail at the post-release assertion. Full-suite before/after (macOS, this machine, same snapshot otherwise): copies of the base (27ee14d) and fixed lib test binaries, run in alternation, 25 runs each, with a 120 s watchdog per run so a hang counts as a failure: - base: 14/25 runs failed (13 failed runs, 1 hung run). Every failure but one was an nsec_backup test reporting a spurious `Busy` or "Backup retry state is unavailable". The hang was completion_persistence_failure_retains_the_same_pending_bytes_for_restart on its unbounded `upload_started.notified()` wait. - fixed: 1/25 runs failed, 0 nsec_backup failures, 0 hangs. The remaining failure in each arm (1/25 base, 1/25 fixed) is terminal::tests::foreground_job_receives_ctrl_c_and_close_stops_job_control_group ("Ctrl+C did not reach foreground job" at its 3 s deadline). It is unrelated to these locks and not touched here. The bounded `upload_started()` helper from 27ee14d is kept. It hides nothing: it turns a pass that never reaches upload into a failure with the pass's statuses instead of a hang. Not in scope, same latent pattern: `app_agents.rs` (`lock()`, "closing the file lets go") and `sign_out.rs`'s agent-ownership check rely on close to release std `File` locks. No FOUNDATION file, no TypeScript, no docs (behaviour is unchanged), no new dependencies. Production diff: +31/−12 in nsec_backup_choice.rs and persistence.rs (7 of the added lines are the test-only accessor). Validation on this snapshot: `cargo test --manifest-path src-tauri/Cargo.toml --locked` 5 times in a row (540 passed, 0 failed each), `cargo clippy --workspace --locked --all-targets -- -D warnings`, `cargo fmt --check`, `git diff --check`. Not run natively. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…g up your key…", with Try again / Show my key / Not now The second half of C2 (plan note 4f94a6a2 §7), on top of 27ee14d (native status store and tickets) and bd25b74 (lock fix). The first-run "Backing up your key…" step is no longer a two-second `[TODO]` wait. It waits on the pass the native uploader runs for its own handoff, and fails the step when that pass fails. C2.3, the waiter and the account seam - `backUpWith` (backup.ts) does five things in order: - reads `identity.backup.ready()`; - turns consent on with `setBackupChoice(pubkey, true)`; - hands the first-run credential to the shared `builderlab` slot; - waits for the first status with `pass >= ticket` and not `running`; - hands the slot back (`null`) on every exit after the handoff. An earlier pass never answers, so a stale `failed` from before sign-in is ignored. - Status mapping: - `complete` → resolves; - `failed{kind}` → that kind; - `waiting{not_linked}` → `not_linked`; - other `waiting`, or `idle` → `interrupted`. A pass that would only be retried later counts as a failure, since nothing wakes it during first run. - A 60 s deadline → `timeout`. The null handoff also retires the pass still running. - Renderer-side failures: - status read fails → `unknown`; - `builderlab: false` or no ticket → `unavailable`; - consent write fails → `interrupted`. No native text reaches the page. - `FirstRunAccount` gains three members: - `backupOffered()`: true only for the build's own portal (a tenant's credential is never handed over) and only when the status says Builderlab backup runs; any error answers false. - `backUp(pubkey)`. - `declineBackup(pubkey)`: `setBackupChoice(pubkey, false)`, best effort, never rejects. `builderlabAccount` takes the identity's backup status and consent as a fourth argument. C2.4, the failure table - New leaf module backup-failure.ts holds `BACKUP_FAILURES`: the plan's kinds, retryability and copy. - `BackupError(kind)` carries the kind, the table's message and `retryable`. C2.5 / C2.6, setup and Welcome - setup.ts gains: - per-step `fallback`s; - a failed snapshot carrying `retryable` (read structurally from the error) and the fallback label; - `retry()`, which re-runs only the failed step, then the rest; - `fallBack()`, which runs the fallback, then rests at its page or continues after the failed step. Both consume the failed tail, so a second click does nothing. - Fallbacks by lane: - Created key: `linkKey` and `backUp` get "Show my key" (rest on the reveal), so a key neither backed up nor seen never leaves first run. This applies to Manage and to Please back this up. - Imported key, new-account lane: a failed link continues to Join with "Not now". I chose "Not now" to match the table and the backup offer's own skip. - Imported key, linked lane: Back it up's backup uses "Not now". - A backup fallback also runs `declineBackup`. Consent otherwise stays on after a failure, as decided. - `Failure` gains a `backLabel`, and "Your key is saved on this device." once a key exists. Try again appears only for retryable errors. - When `backupOffered()` is false: - KeyChoice drops Manage my keys for me and Builderlab's copy sentence; - KeyReveal drops Please back this up for me; - the linked lane's import skips the Back up this key? offer. - `firstRunSteps().backUp` and `backUpKey` are removed. §8 dev seed - `BUZZ_DEV_FIRST_RUN_ACCOUNT` accepts `"backup": "<kind>"`, validated against the table without echoing the value. `npub` is now optional, so `{"backup":"unreachable"}` seeds an unlinked account whose backups fail. - The seeded account offers backup and never invokes a native backup command. Docs: identity.md covers: - the real backup step; - the failed page's options; - consent until declined; - the portal-only offer. configuration.md and .env.example describe the seed field. The Builderlab README no longer lists key backup as a placeholder. Not in this commit: the Settings → Identity status row, unlink (C4) and lookup/restore (C3). No FOUNDATION files were touched. Tests (C2.9) - backup.test.ts: the waiter against the real store over a scripted host: - invoke order watch → state → set_choice → session → session(null); - a stale lower-pass failure and `running` do not answer; - each status → kind; - a snapshot that settled before the subscription; - deadline → timeout; - `builderlab: false` (no consent write) and a null ticket (`unavailable`); - unread status / unwritten consent, with no native text and no credential handoff. - builderlab-account.test.ts: - `backUp` hands over the session credential; - `backupOffered` on the build portal / tenant / `builderlab: false` / unreadable status / unconfigured build; - `declineBackup` writes `false` and never rejects; - the reads and binding never touch backup. - setup.test.ts (new): `retry` re-runs from the failed step; `fallBack` rests or continues; structural `retryable`; no-ops unless failed, and on a second click. - first-run-seed.test.ts and vite-config.test.mjs: the `backup` field through the native loader, and an unknown or inherited kind rejected without echo. - identity.test.tsx journeys, updated: - Connect waits on `backUp(viewer)`; - the reveal backup times out, Try again re-runs only the backup, then Join Community; - Back it up is gated; - a link failure after Manage → Show my key → reveal → Join; - a refused backup after import → no Try again → Not now declines and still lists owned communities. - identity.test.tsx journeys, new: - Manage + `not_offered` → Show my key waits for the decline, then shows the reveal; - a link failure on the import lane → Not now → Join; - not offered: no Manage, no reveal backup, no backup offer on the linked lane; - the real `builderlabAccount` and identity store over mocked native commands, past a stale failure, with the credential then the null handoff; - a seeded `unreachable` backup fails with Try again and Show my key, without any `nsec_backup_*` invoke. Validation (this snapshot) - `bin/pnpm vitest run src/features/identity src/features/communities src/app src/bundled browser-host/vite-config.test.mjs`: 273 files / 4125 tests passed (2 files / 8 tests skipped). - `tsc --noEmit`, `pnpm lint`, `biome check` on the changed files, `pnpm design:check` and `git diff --check` are clean. - `tests/browser/window-chrome.spec.mjs` passes in Chromium. The first-launch heading is unchanged. - Against the previous sources (HEAD versions restored, new tests kept), 24 cases fail: every new and changed journey, plus every new backup/setup/seed case. backup.test.ts fails at import, since backup-failure.ts does not exist there. Not exercised in the native app: no Tauri build or run, no live Builderlab sign-in, and no real uploader pass. The waiter is checked only against mocked native commands. Design note for review: with backup not offered, Set up your key has no primary-variant button (only the two small secondary choices). The plan only says to hide Manage. Shared owners reused: `Button` and the existing `Failure`, `KeyChoice` and `KeyReveal` layouts. There are no new tokens or components. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
… Identity, and make own key primary when Manage is hidden C2c from the onboarding plan (note 4f94a6a2, §C2.7 / P's decision 5), on top of 27ee14d (native status store), bd25b74 (lock fix) and 93b8ad8 (first-run waiter). First run now leaves consent on after a failed backup, so a later Builderlab sign-in may finish it in the background; this row makes that visible. Settings → Identity "Key backup" row (src/app/IdentitySettings.tsx) - `BackupRow` reads `identity.backup` through `useSyncExternalStore`, the way Settings reads its other stores, so it follows every higher `seq` the store accepts. It sits in the existing rows `SettingsGroup` above "Sign out of Buzz", as a `PreferenceRow` with no trailing control. The sentence is wrapped in `role="status"` so a change is announced. - One sentence per status: - idle: "Buzz hasn’t checked your key’s backup since it opened." (idle is what a launch or a reload reports, not "no backup") - running: "Backing up your key…" (the first-run step label) - complete: "Your key is backed up to Builderlab." - waiting{no_consent}: "Automatic backup is off for this key." - waiting{no_session}: "Sign in to Builderlab to back up your key." - waiting{not_linked} and every failed kind: the message from `BACKUP_FAILURES` in backup-failure.ts, so it is the same copy as first run's failed page. There is no second table. - Read-only: no consent toggle, no retry, no other action. The consent wording still needs the backup owner's sign-off. - Hidden unless backup is offered, by the same rule first run uses. That rule moves out of `builderlabAccount().backupOffered` into the exported `backupOfferedFor(backup, origin?)` in builderlab-account.ts. It answers false unless the sign-in origin is the build's own Builderlab portal, which it checks before reading the status so a tenant's status is never read, and then returns the status's `builderlab`. Any error answers false. First run passes its login origin. Settings passes none, because the Settings sign-in is always at the build portal. `builderlabAccount().backupOffered` now just delegates to it, with no change in behaviour. - Known difference: under the `BUZZ_DEV_FIRST_RUN_ACCOUNT` dev seed, first run always offers backup while the row still follows the real rule. The seed fakes the whole account and is serve-only. Key choice (src/features/identity/KeyChoice.tsx) - When Manage my keys for me is hidden because backup isn't offered, "I want to manage my own key" becomes the primary, at the same `lg` size the Manage button uses, so the page keeps a primary action. The order is unchanged and "I already have a key" stays small and secondary. Before this, the page had no primary-variant button in that case. No FOUNDATION file (App.tsx, services.ts, main.tsx, communities/service.ts, src/plugins/api.ts), no Rust, no new dependencies, no new styling primitives. Shared owners reused: `SettingsGroup`, `PreferenceRow`, `Button`. Production diff: +46/−2 in IdentitySettings.tsx, +21/−9 in builderlab-account.ts, +6/−2 in KeyChoice.tsx. Tests - IdentitySettings.test.tsx drives the real `createBackupStatus` store over a scripted native side, with a mocked Tauri `Channel`: - the sentence for complete, running, waiting{no_session}, waiting{not_linked} and failed{unreachable}; - hidden when the status says `builderlab: false`, and when the build has no Builderlab portal; - following pushes at seq 2, 3 and 4, a late seq-2 push changing nothing, and the row holding no button or switch. - The barrier for the negative cases is awaiting `backupOfferedFor` on the same store. The row's own check started first, and the positive cases use the same barrier with synchronous queries. - Mutation checks, each failing its intended test(s) and restored: ignoring `offered`; replacing the shared rule with `status.builderlab` alone; reading the snapshot once instead of subscribing; writing own failure copy. - The existing reveal test, and Settings.test.tsx's identity case, gain a `backup` store, since the row now reads it (an inert one in Settings). - identity.test.tsx's not-offered journey asserts the two remaining choices in order, own key `prominent` and import `subtle`. It fails against HEAD's KeyChoice.tsx. - docs/identity.md describes the row and the primary fallback. Validation (this snapshot, macOS) - `bin/pnpm vitest run src/features/identity src/app`: 55 files, 741 tests passed. - `tsc --noEmit`, `pnpm lint` (exit 0; 22 pre-existing infos), `biome check` on the six changed source/test files, `pnpm design:check` and `git diff --check` are clean. Nothing was run in the native app: no Tauri build, no Settings → Identity visit against the real uploader, and no rendered check in either theme, at narrow or wide widths, or with enlarged text. Open design and copy points for review: - Retryable failure messages end in "Try again." even though the row has no button. - The row appears after the first status read, so "Sign out of Buzz" moves down by one row once the IPC answers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
This was referenced Oct 28, 2026
Closed
matt2e
marked this pull request as ready for review
October 28, 2026 12:26
Contributor
Author
|
🤖 Closing: this stack was cut as chronological slices of the branch history rather than logical, independently reviewable layers. It will be replaced by a stack built from the squashed diff. Nothing from this PR was merged. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part 5 of 12 of the
onboarding-flowstack (the first-run onboarding work reviewed on #1061, split into stacked PRs per the plan). Base:onboarding/04-key-choice-import-welcome-back. Each PR is a contiguous slice of the original branch's history, cherry-picked ontomain; the combined tree of the whole stack equalsonboarding-flowat572d69398rebased ontomain(verified by diff, see the stack notes in the last PR). Built onmainatdf5180b4a(2026-10-28, theorigin/mainof the build);mainhas moved on since and the stack is not rebased onto the newer tip.Stack
onboarding/01-welcome-find-joinonboarding/02-profile-step-landing-urlonboarding/03-builderlab-tenantsonboarding/04-key-choice-import-welcome-back5. feat(identity): link the first-run key to the account, and back it up through the observable native uploader #1806
onboarding/05-link-and-native-backup← this PRonboarding/06-unlink-restore-create-my-keyonboarding/07-connect-choiceonboarding/08-invite-links-known-communitiesonboarding/09-onboarding-layouts-and-copyonboarding/10-navigation-deadlineonboarding/11-review-fixesonboarding/12-do-not-merge-keychain-slotScope
bind()+ the native binding signer's origin allow-list (src-tauri/src/identity.rs), the nativeBackupStatusstore with tickets (nsec_backup.rs;nsec_backup_watch/nsec_backup_stateregistered inlib.rs,build.rsandcapabilities), the explicitflock(LOCK_UN)lease fix, the first-run waiterbackUpWith+backup-failure.ts+ setupretry/fallBack, and the read-only Settings → Identity "Key backup" row.Commits kept as on the original branch: each carries its own fail-before evidence, and the lock fix (
2154b2b7a) is a standalone correctness change a Rust reviewer will want to see alone. The Settings-row commit also makes "I want to manage my own key" primary when Manage is hidden (a PR 04 page) — accepted overlap.Checks at this level (worktree
/tmp/onb-stack, head ofonboarding/05-link-and-native-backup)bin/pnpm exec tsc --noEmit— 0 errorsbin/pnpm lint— 0 errors (21 pre-existing infos)bin/pnpm design:check— all checks passgit diff --check origin/main...HEAD— cleanbin/pnpm vitest run src/features/identity src/app browser-host/vite-config.test.mjs— 58 files, 813 tests passed, 0 failedbin/cargo fmt --check— cleanbin/cargo test --manifest-path src-tauri/Cargo.toml --locked identity— 36 passed, 0 failedbin/cargo test --manifest-path src-tauri/Cargo.toml --locked nsec_backup— 49 passed, 0 failedbin/cargo clippy --workspace --locked --all-targets -- -D warnings— cleanNative evidence — not claimed here
No per-level native run was performed for this PR. Native first-run testing happens on the tree of the final DO-NOT-MERGE PR of the stack (#1813), which carries the isolated debug Keychain slot; the human in-app check for this PR's behaviour is owed there, so this PR stays a draft without
buzz-review-completeduntil Matt confirms it.🤖 Generated with Claude Code