Repository navigation
feat(identity): Set up your key — managed, own-key reveal, import — and Welcome back for a linked account - #1805
Closed
matt2e wants to merge 3 commits into
Conversation
…th a managed backup path and an nsec reveal page
After Builderlab sign-in on first run (Connect with Builderlab, or a tenant's
community URL on the landing page), setup no longer runs a fixed Downloading →
Creating → Uploading list. It now runs its prelude, pauses for the user to say
who looks after the key, and resumes with that choice's steps:
sign-in ─► Finding communities… [TODO] (Connect only)
─► Checking for a backed-up key… [TODO] ─► Set up your key
Manage my keys for me ─► Creating keys… ─► Backing up your key… [TODO] ─► Join …
I want to manage my own key ─► Creating keys… ─► Your private identity key
Please back this up for me ─► Backing up your key… [TODO] ─► Join …
Continue without backing up (gated by the checkbox) ─────► Join …
I already have a key — disabled, "Coming soon"
any step fails ─► Couldn’t finish setup ─► Back ─► landing (unchanged)
"Join …" is Join Community for a landing URL or Join a community otherwise,
exactly as before. The key-only path (a URL no tenant hosts) is unchanged:
Creating keys… alone, then Join Community, with no choice or reveal page.
- `setup.ts`: `SetupSnapshot` gains `{ status: "paused"; at: "key-choice" |
"key-reveal" }`. `start(steps, then = { status: "done" })` rests at `then`
and is legal from `idle` or `paused`, so a choice resumes the same store; a
double click is absorbed by the `running` guard, and `start([])` is the
"nothing more to do" exit. `reset()` is unchanged. `setupSteps()` is
replaced by `firstRunSteps(identity, delay)`, a named table
(`findCommunities`, `checkBackup`, `createKey`, `backUp`) `Welcome` composes
into phases, plus the two fakes with their `TODO(kgoose)` seams:
`lookupBackedUpKey()` always answers `{ found: false }` and `backUpKey()`
only waits. The `[TODO]` label marker stays on every placeholder;
"Downloading keys…" and "Uploading keys…" are gone.
- `Welcome.tsx`: sign-in starts `[findCommunities?, checkBackup]` paused at
`key-choice`; a `paused` branch renders `KeyChoice` / `KeyReveal`, whose
handlers call `setup.start` with the rows above. The existing gate (children
only when identity is ready and setup idle) needs no change: during the
reveal the key exists but setup is paused, so the app stays hidden. Every
back-to-landing path still runs `backToLanding`, and `reset()` is fine from
a pause.
- `KeyChoice.tsx` (new, stateless): "Set up your key" — says no backed-up key
was found for the signed-in email and that Builderlab can keep a copy for
other devices; primary `size="lg"` **Manage my keys for me**, then two
visibly smaller `size="sm"` buttons, **I want to manage my own key** and
**I already have a key — Coming soon** (disabled, like Scan a QR code).
- `KeyReveal.tsx` (new): "Your private identity key" with the agreed body
("This key is how you sign in to Buzz. Don’t share it … nobody — not Buzz,
not Builderlab — can reset or recover it if it’s lost. Copy it into a
password manager like 1Password now."), the branch's wireframe box holding
`<PrivateKey initiallyRevealed>`, a `Checkbox` "I’ve saved my private key
somewhere safe", and the footer **Continue without backing up** (secondary,
enabled only once the box is ticked; no other interaction tracking) /
**Please back this up for me** (primary). No save-to-file; the code comment
records why (no save dialog or file plugin in the webview — a key file would
have to be written natively, deferred).
- `PrivateKey.tsx`: optional `initiallyRevealed`. On mount it runs the same
`access(false)` Reveal runs (via `useEffectEvent`, once), renders the secret
in a large monospace well (`<code>`, `font-mono text-body-lg`: no mono type
role is larger than 12px yet, noted in a comment) instead of the `Input`, and
makes **Copy private key** the primary button. The hygiene is untouched:
window blur, document hiding, Hide and unmount still clear it through the
same generation guard, and **Reveal private key** is offered again once
hidden. The nsec comes from the existing `identity.exportKey()` →
`identity_export`, which serves the key `identity_create` just stored; it is
never shown as hex. Settings → Profile and Sign out render the default
(masked) mode exactly as before.
No FOUNDATION file (App.tsx, services.ts, main.tsx, communities/service.ts),
no Rust, no new dependencies. Production diff ≈ +115/−45 lines across
setup.ts, Welcome.tsx, PrivateKey.tsx and the two new pages.
Tests (identity.test.tsx): `landingRelay` and `nativeRelay` learn
`identity_export` → the fixture nsec; a `manageKeysForMe()` helper carries the
join/profile journeys (`joinCommunityPage` and the three inline Join a
community cases) through the new pause unchanged. The Connect journey now
asserts Finding → Checking → Set up your key (with the email in the body, no
`identity_create` yet, the own-key button enabled and the import button
disabled) → Manage → Creating keys… → Backing up your key… → Join a community,
`delay` still `[[2000],[2000],[2000]]`; the tenant-URL journey asserts
Checking → choice → Manage → Backing up → Join Community with two delays; the
creation-failure journey releases two placeholders and clicks Manage before
the rejected `identity_create`, with no backup delay after it. New journeys:
own key → Creating keys… → reveal page with the nsec in a `font-mono` well on
arrival, `identity_export` once, Copy writing it to the clipboard without a
second export, window blur hiding it and offering Reveal, Continue disabled
until the box is ticked, then Join a community with exactly two delays and
`invoke` = restore, create, export; own key on the tenant path → Please back
this up for me → Backing up your key… → Join Community; own-path creation
failure → Couldn’t finish setup → Back → landing with identity missing and
`identity_export` never invoked. All six changed/new journeys fail against the
previous Welcome/setup/PrivateKey and pass now. One jsdom quirk is worked
around in the own-key test and commented: after the clicked choice button
unmounts, jsdom aims the next focus move's `blur` at `window`, which
`PrivateKey` rightly treats as "hide"; the test parks focus on `body` first.
docs/identity.md rewrites the first-run paragraph for the new order and pages
and adds the first-run exception to "UI and sensitive data"; the Builderlab
README's first-launch sentence matches.
Validation: `bin/pnpm vitest run src/features/identity src/features/communities
src/app` (59 files, 740 tests), `tsc --noEmit`, `pnpm lint`, `biome check` on
the changed files, `pnpm design:check`, `git diff --check`, and
tests/browser/window-chrome.spec.mjs in Chromium (passes; the "Buzz" heading
after the launch handoff is unchanged). Not exercised in the native app: no
real first run was performed, so the reveal page against a key `identity_create`
just stored in the Keychain (one `identity_export` on arrival, no second ACL
prompt expected since the key is in memory), the clipboard write from
WKWebView on a first-run page, and the choice/reveal pages' appearance in the
packaged window in both themes remain to be tried by hand. Reset this
branch's isolated dev slot before such a run:
`security delete-generic-password -s dev.local.buzz.foundation.identity.debug.onboarding -a human`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
… key choice
**I already have a key** on the first-run **Set up your key** page (5d0c3818)
is live: it drops its "— Coming soon" suffix and opens a new **Enter your
private key** page, modelled on the old desktop app's key-import form, while
setup stays paused. An imported key skips the backup and reveal pages and
continues straight to **Join Community** (landing URL) or **Join a
community**, exactly where the created-key paths end:
Set up your key ─► I already have a key ─► Enter your private key
Back ─────────────────────────────────► Set up your key (all three live)
Continue with this key ─► Importing your key… ─► Join …
native refusal ─► same page, key kept, message under the field
The key-only path (a URL no tenant hosts) is unchanged and never shows the
choice page.
- `key-import.ts` (new, pure): `classifyKeyImport(text)` trims and returns
`empty`, `encrypted` (`ncryptsec1…` in any case — a NIP-49 backup; the app
has no NIP-49 decryption and the native parser rejects the prefix, so it is
refused with a clear message rather than adding Rust), `nsec` with the text
as typed plus the npub preview (`nostr-tools/nip19` `decode` → `getPublicKey`
→ `npubEncode`; derivation also rejects an out-of-range secret, as native
import does), or `invalid`. `readKeyFile(file)` refuses anything over
`KEY_FILE_MAX_BYTES` (1024) before reading and returns the first non-blank
line, trimmed.
- `KeyImport.tsx` (new): "Enter your private key" / "Paste your private key to
sign in to Buzz. You can also drop a text file holding it here, or choose
one." A `Field` "Private key (nsec)" with a masked `Input`
(`type="password"`, `autoComplete="off"`, `autoCorrect`/`autoCapitalize`
off, `spellCheck={false}`, focused on arrival), **Reveal private key** /
**Hide private key** (disabled while empty; masking re-asserts when the
field empties), **Choose a key file** opening a hidden `type="file"` input,
and the whole form as a drop target through the existing `useFileDrop`
(`src/features/messages`, already imported across features), with a hint
line that flips while dragging. The field's description is the npub preview
("Public identity: npub1…", never hex); its error is the classification —
"Password-protected backups aren’t supported here yet. Paste the nsec1 key
itself." / "That doesn’t look like a valid nsec. Paste an nsec1 key." — or
a file/native failure. Footer in branch order: **Back** (secondary) then
**Continue with this key** (primary, enabled only for a valid nsec; Enter in
the field submits). While `onImport` is pending the component renders the
"Setting up Buzz" progress page with "Importing your key…" but stays
mounted, so a native refusal (an invalid key, a key already in the slot)
returns to the form with the typed key kept and the message under the
field — never the fatal "Couldn’t finish setup" page. The key lives only in
this component's state: leaving the page discards it; nothing logs or
persists it. Wireframe box like the sibling pages.
- `SetupProgress.tsx` (new, 23 lines): the "Setting up Buzz" heading and
`CircleNotchIcon` spinner recipe extracted verbatim from `Welcome`'s running
branch so both `Welcome` and `KeyImport` render the same page.
- `setup.ts`: `SetupPause` gains `"key-import"`. `createKey`'s snapshot check
becomes `saved(identity)`, shared with new `importKey(identity, nsec)`,
which calls the existing `identity.importKey` (native `identity_import`,
the same command `IdentitySetup` uses) and re-raises the snapshot's error.
It is deliberately not a step: a refusal belongs under the field. No new
store method: moving between pauses is `start([], { status: "paused", at })`,
which rests there synchronously and runs nothing, since no key exists yet.
- `KeyChoice.tsx`: `onImport` prop; the button is enabled and reads
"I already have a key".
- `Welcome.tsx`: `pause(at)` helper; the paused branch renders `KeyImport` for
`key-import` with `onBack={() => pause("key-choice")}` and
`onImport={async nsec => { await importKey(identity, nsec); void
setup.start([]); }}`; the running branch uses `SetupProgress`. The gate
(children only when identity is ready and setup idle) is unchanged.
Native import takes the bech32 `nsec1…` string as typed (`Key::parse` in
`src-tauri/src/identity.rs`: 63 chars, `nsec1`/`NSEC1`, bech32-checked), so no
TS hex decoding and no Rust change. The profile step already prefers an
existing relay kind 0 (`read.exists ? read.profile : client.profile`) and the
join reads `memberships.length` from the live service, so nothing assumes the
key was created. No FOUNDATION file (App.tsx, services.ts, main.tsx,
communities/service.ts), no Rust, no new dependencies (`nostr-tools/pure` is
the already-installed package's existing entry point). Production diff ≈
+50/−30 in existing files plus 241 new lines across the three new files.
Tests: `key-import.test.ts` (node) covers a valid nsec as typed, trimmed of
surrounding whitespace/newlines, and all-uppercase; `ncryptsec` in lower and
mixed case; garbage, a bare `nsec1`, a bad checksum, an npub and raw hex as
invalid; empty/whitespace; and `readKeyFile` returning the first non-blank
CRLF line, "" for a blank file, and refusing an oversize file without reading
it. `identity.test.tsx` adds a real keypair (`owned`/`ownedNsec`) and five
journeys: choice → I already have a key → masked/focused field with
autocomplete and spellcheck off, invalid → error + Continue disabled,
ncryptsec → unsupported message, valid → npub preview as the accessible
description with no hex, Reveal/Hide, Back → choice with all three buttons
enabled, re-open → field empty, `invoke` = restore only; Continue → exact
`invoke("identity_import", { nsec })`, "Setting up Buzz" / "Importing your
key…" with the form and children hidden, then Join a community with identity
ready as the imported pubkey, no reveal heading, `delay` = `[[2000],[2000]]`,
`invoke` = restore, import; a rejected `identity_import` → message under the
field, form kept with the key, Continue enabled, identity still missing,
editing replaces the message, Back → choice; a dropped file fills the field
from its first line (masked, preview shown), an oversize chosen file is
refused with the field unchanged, a chosen file with other text classifies
as invalid; and the tenant-URL path → import → Join Community with one delay
and `invoke` = restore, 2× relay_http, import, 2× relay_http. The Connect
journey now asserts the import button enabled. All five new journeys fail
against the previous Welcome/KeyChoice/setup (checked by swapping HEAD's
files in) and pass now. `landingRelay` learns `identity_import`.
docs/identity.md describes the page in the first-run section and adds it to
"UI and sensitive data"; the Builderlab README sentence mentions bringing an
existing key.
Validation: `bin/pnpm vitest run src/features/identity src/features/communities
src/app` (60 files, 748 tests), `tsc --noEmit`, `pnpm lint`, `biome check` on
the eight changed files, `pnpm design:check`, `git diff --check`, and
tests/browser/window-chrome.spec.mjs in Chromium (passes). Not exercised in
the native app: no real `identity_import` was run from this page against the
Keychain, so the native refusal copy for a key already in the slot, the
WKWebView file-drop and file-picker paths, `autoFocus` and the masked field's
behaviour with the system password manager, and the page's appearance in the
packaged window in both themes remain to be tried by hand. Reset this
branch's isolated dev slot before such a run:
`security delete-generic-password -s dev.local.buzz.foundation.identity.debug.onboarding -a human`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
… instead of offering a new one
After Builderlab sign-in on first run (Connect with Builderlab, or a tenant's
community URL on the landing page), setup no longer assumes a new user. It now
asks kgoose which Nostr key is bound to the signed-in account and forks:
sign-in ─► Checking your account… (REAL)
not linked ─► Checking for a backed-up key… [TODO] ─► Set up your key
(Manage / own key / I already have a key, unchanged)
linked ─────► Checking for a backed-up key… [TODO] (silent) ─► Welcome back
Enter your private key ─► Importing your key… ─► Back up this key?
Not now ───────────────────────────► Finding communities… (REAL) ─► Join …
Back it up ─► Backing up your key… [TODO] ─► Finding communities… ─► Join …
Recover from another device — disabled, "Coming soon"
I lost this key ─► Finding communities… (REAL) ─► Start over with a new key
Back ─► Welcome back
Start over ─► Unlinking your old key… [TODO] ─► Set up your key (as above)
"Join …" is Join Community for a landing URL or Join a community otherwise, as
before. Binding is 1:1 and enforced on the server, so the linked lane never
offers Manage my keys for me or I want to manage my own key, and the import page
accepts only the linked key. The key-only path (a URL no tenant hosts) is
unchanged.
What is real and what is faked:
- REAL: `CurrentNostrIdentity` (`POST <loginOrigin>/api/goose/v1/buzz/nostr-identities/current`)
and `ListBuzzCommunities` (`POST …/v1/buzz/communities/list`), in new
`src/features/identity/builderlab-account.ts`. Both are POSTs with `{}`,
`X-BB-Session-Credential` from the first-run OAuth session and `Origin` set
to the login origin `discoverAuthentication` chose (the build URL otherwise,
through the same `oauthTarget` check sign-in uses), sent through
`bundledHost.request` — the bundled plugin's native grant `first-run.ts`
already uses for sign-in. Reply shapes and parsers are the hosted-communities
card's (`boundKey`, `relayUrl`, `check`, `Reply` from `api.ts`; the route
paths from `native.ts` ROUTES `identity`/`list`); the card's own backend and
behaviour are untouched. A 400 `MISSING_MAPPING` / `setup_needed` reply is
"not linked" (and an empty owned list); a bound identity without a usable key
is a failure, never "not linked"; any other failure of the account check is
the existing "Couldn’t finish setup" page. Owned communities exclude archived
ones (no relay to join) and hosts `enteredRelayOrigin` refuses.
- FAKED, still `[TODO]` and only waiting: the backed-up-key lookup (a linked
key does not imply a backup — the nsec-backup record is a separate opt-in
table no shipped client writes — and no copy ever implies the user lost
one); "Backing up your key…" (linked lane) and the renamed "Backing up and
linking your key…" (created-key lanes), whose seam comment now names the
HPKE envelope upload and the kind-24243 challenge/verify binding the
hosted-communities card drives in `native.ts` `bind`; and "Unlinking your
old key…", a seam for kgoose `DeleteNostrIdentity` (`/nostr-identities/delete`,
the card's `unbind` route) that is deliberately not called.
Pages and behaviour:
- `WelcomeBack.tsx` (new): "This Builderlab account is already linked to a Buzz
identity — the key you use on your other device. Enter that key to continue."
with the full linked npub under *Linked public key (npub)* and **Copy npub**
plus inline `role="status"`; **Enter your private key** (primary),
**Recover from another device — Coming soon** (disabled; comment names
NIP-AB pairing as the future entry), **I lost this key** (ghost, sm).
- `PublicKeyPanel.tsx` (new): the invite page's npub block extracted from
`Welcome`'s `Failure` so both pages render the same recipe; `Failure` shrinks.
- `KeyImport`: optional `expectedPubkey` (hex). A valid nsec whose npub differs
shows "That isn’t the key linked to this account. It expects npub1… (the key
on your other device)." under the field, Continue stays disabled and Enter
does nothing; the comparison happens in the page, so a mismatched key never
reaches `identity_import`. Back returns to Welcome back on this lane.
- `BackupOffer.tsx` (new): "Back up this key?" — Not now / Back it up.
Importing never uploads silently.
- `KeyLost.tsx` (new): "Start over with a new key" with the loss explanation,
a *Communities you own* wireframe box (hostnames, "None", or a could-not-load
line when the read failed) and the note that memberships in other people's
communities cannot be listed and are lost too; Back / destructive Start over.
- `JoinCommunity`: optional `owned` rows above the URL field — `Avatar`
placeholder, hostname, **Join** — where Join calls `onFind(origin)`, exactly
the typed-URL path, so Find/Join Community do the work. Empty or failed list
renders the page as before. The "Finding communities… [TODO]" placeholder is
gone from the Connect prelude; the real read runs at the end of the linked
lane, after a key is on the device, and before the start-over page. The old
key's list is cleared on Start over, so it is never offered to the new key.
- `setup.ts`: `SetupStep.run` may resolve to a resting state, which overrides
the caller's `then` (the seam the backed-up-key TODO had reserved); new
pauses `welcome-back` / `backup-offer` / `key-lost`; step factories
`checkAccount(account, onLinked)` and `findCommunities(account, onFound)`;
table rows `backUp`, `backUpAndLink`, `unlink`. `createSetup` is otherwise
unchanged.
- `Welcome.tsx`: new injectable `account` prop (default: the dev seed when set,
else `builderlabAccount` over `bundledHost.request`); `linked` / `owned`
state and a `reads` ref, all cleared on every return to the landing; the
paused branch becomes a `switch` over the six pauses.
Dev seed for demos: `BUZZ_DEV_FIRST_RUN_ACCOUNT` — `unlinked`, or a JSON object
`{"npub":"npub1…","communities":["host",…]}` — replaces the two kgoose reads and
nothing else. `vite.config.ts` validates it (`parseFirstRunAccountSeed` in new
leaf module `first-run-seed.ts`, with explicit `.ts` imports so Node's native
config loader stays warning-free) and defines `VITE_BUZZ_FIRST_RUN_ACCOUNT` for
`serve` only; builds define it empty, so packaged apps always ask kgoose.
Documented in docs/configuration.md and .env.example. Matt's staging account is
bound to his real key, which this never touches.
No FOUNDATION file (App.tsx, services.ts, main.tsx, communities/service.ts), no
Rust, no new dependencies. Production diff ≈ +330/−100 across Welcome.tsx,
setup.ts, KeyImport.tsx, JoinCommunity.tsx and vite.config.ts, plus ~360 lines
in the six new source files.
Tests: `builderlab-account.test.ts` (request shape at a tenant and at the build
URL, bound key, MISSING_MAPPING/setup_needed ⇒ null, unauthorized / keyless /
non-JSON / unstructured / oversize / string-refusal / transport failures,
owned-list filtering) and `first-run-seed.test.ts` (unlinked, npub + hosts,
rejections without echo). `identity.test.tsx`: every existing render gets an
`unlinked` account stub, the Connect journey gates the real check and asserts
"Checking your account…" first, placeholder counts drop by the removed Finding
step, and the created-key label is "Backing up and linking…". New journeys:
Welcome back with the full npub and Copy → import Back → mismatch refused with
the expected npub and Continue disabled → linked key → Importing → Back up this
key? → Not now → Join a community as the linked key; Back it up running the
backup step; I lost this key → owned hosts listed → Back → Start over →
Unlinking → Set up your key → Manage → Join a community with no rows (fails
without the owned-list clear); owned rows whose Join opens Finding community for
that host with the two GETs; tenant URL + linked → Welcome back → import → Join
Community with the reads at the tenant origin; account-check failure → Couldn’t
finish setup → Back → landing; the env seed answering in place of kgoose.
`vite-config.test.mjs` checks the define for serve vs build and the non-echoing
rejection. docs/identity.md rewrites the first-run section for the fork and
adds the lane to "UI and sensitive data"; the Builderlab README sentence names
the reads.
Validation: `bin/pnpm vitest run src/features/identity src/features/communities
src/app src/bundled/builderlab browser-host/vite-config.test.mjs` (75 files,
922 tests), `tsc --noEmit`, `pnpm lint`, `biome check` on the changed files,
`pnpm design:check`, `git diff --check`, and
tests/browser/window-chrome.spec.mjs in Chromium (passes). Nothing was exercised
natively: no real `nostr-identities/current` or `communities/list` call was made
from the bundled grant against kgoose, so the staging portal accepting the
first-run credential on those routes, the native `plugin_host_request` path for
POSTs with a body, and the new pages' appearance in the packaged window remain
to be tried by hand (with `BUZZ_DEV_FIRST_RUN_ACCOUNT` for the linked lane).
Squashed for the stacked PR — 6adbb9a (fix(identity): read kgoose's unlinked answer, name the failing account read, and keep Back off the app after a saved key):
fix(identity): read kgoose's unlinked answer, name the failing account read, and keep Back off the app after a saved key
Three fixes in src/features/identity from the native walkthrough of c67ecfc49
("Take 0" stopped on "Couldn’t finish setup — Builderlab returned an invalid
response") and that commit's review.
1. `builderlab-account.ts` `linkedKey`: kgoose's
`POST …/v1/buzz/nostr-identities/current` (`NostrIdentityService.current`)
answers **200 with `identity` null or absent** for an account with no Nostr
mapping; it never sends `MISSING_MAPPING` — only `communities/list` does.
The check read that 200 as "a bound identity without a usable key" and
failed setup for every genuinely new user. A 2xx reply with no `identity`
is now *not linked* and continues to Set up your key. A present identity
whose `pubkey_hex` is not 64 lowercase hex still fails (a linked account
must never be given a second key), and every non-2xx / non-JSON answer
still fails closed to the setup-failed page. `notLinked` keeps the
`MISSING_MAPPING` / `setup_needed` reading for the list route and now
checks `typeof code === "string"` first, so a malformed body yields the
fixed fallback message instead of a TypeError's text (review nit).
2. Diagnosability: the failure copy names the step and HTTP status — never
the response body, per the never-forward-provider-bodies rule —
"Builderlab account check failed (HTTP 403: not JSON)." /
"… (HTTP 500)." / "… (HTTP 200: response too large)." /
"… (HTTP 200: unusable linked key)." and "Builderlab community list failed
(…)" for the list, so a 403 text/plain is told from a JSON shape problem
and the account check from the list. `call` takes the step name and a
`read` callback so every refusal of an answer passes one `catch`, which in
development builds (`import.meta.env.DEV`) `console.warn`s
"Builderlab <step>: POST <route> answered HTTP <status>" plus our own
message, again without the body. The card's structured-error messages
(`check`) are unchanged.
3. `Welcome.tsx` (review warning): when a step failed *after* a key was saved
— backup after an import, backup-and-link after creation, the unlink seam
once real — the failed page's Back ran `backToLanding` → `setup.reset()` →
`idle`, and with identity `ready` the gate returned `children`: the app
opened with no community, skipping Join. Back now rests on the Join page
(`setup.start([])` → `done`: Join Community for a landing URL, Join a
community otherwise) when `state.status === "ready"`, and still goes to
the landing when no key exists. `createSetup().start` admits `failed` as
well as `idle`/`paused` (the guard is now "not running or done"), so a
failed setup can be resumed rather than reset first.
No FOUNDATION file (App.tsx, services.ts, main.tsx, communities/service.ts),
no Rust, no new dependencies. Production diff ≈ +75/−25 across
builderlab-account.ts, Welcome.tsx and setup.ts.
Tests: builderlab-account.test.ts reads `{ identity: null }`, `{}`, a 400
`MISSING_MAPPING` and `setup_needed` as not linked; pins the new copy for an
unusable and a malformed linked key, a 403 text/plain, an unstructured 500, a
non-string error `code` (fallback message, no TypeError) and an oversize
body; checks the dev warning carries the route and status and not the body;
and gives the list route its own not-JSON message. identity.test.tsx gains a
`reject` on `deferred` and a `fail(index, error)` on `gatedDelay`, and two
journeys: a rejected backup after the linked key is imported → Couldn’t
finish setup with the key ready and children hidden → Back → Join a
community, children still hidden, no landing; and a rejected unlink before
any key → Back → the first-launch screen with identity missing. The
account-check failure journey's injected message takes the new shape. Against
HEAD's three source files 11 of the changed/new cases fail (the no-key unlink
case passes on both, as a guard for the unchanged branch) and all pass now.
docs/identity.md describes the 200-no-identity reading, the named failure
copy, and where Back lands with and without a key.
Validation: `bin/pnpm vitest run src/features/identity src/features/communities
src/app` (64 files, 806 tests), `tsc --noEmit`, `pnpm lint`, `biome check` on
the changed files, `git diff --check`. The real `nostr-identities/current`
read is still unverified natively: no call was made from the bundled grant
against kgoose, so the 200-with-null-identity reading rests on the walkthrough
and the cash-server source, and the next native first run should confirm it
(the dev warning now names the route and status if it does not).
Co-Authored-By: Claude Fable 5.1 <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 4 of 12 of the
onboarding-flowstack (the first-run onboarding work reviewed on #1061, split into stacked PRs per the plan). Base:onboarding/03-builderlab-tenants. 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-tenants4. feat(identity): Set up your key — managed, own-key reveal, import — and Welcome back for a linked account #1805
onboarding/04-key-choice-import-welcome-back← this PRonboarding/05-link-and-native-backuponboarding/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
createSetuppauses (key-choice,key-reveal,key-import,welcome-back, …),KeyChoice,KeyReveal+PrivateKey initiallyRevealed,KeyImport+key-import.ts+SetupProgress,WelcomeBack,PublicKeyPanel,BackupOffer,KeyLost, owned rows on Join a community, the real kgoose reads (builderlab-account.ts:nostr-identities/current,communities/list) and theBUZZ_DEV_FIRST_RUN_ACCOUNTseed (first-run-seed.ts).Transitional code, stated plainly:
KeyReveal.tsxand thePrivateKeychange land here and are removed again in PR 06 of the stack (plan P,aa8c19da4). Rewriting that out of history was judged not worth the conflict risk.Squash vs the original branch:
6adbb9a6a(reads kgoose's 200-with-null-identity as "not linked", names the failing read, keeps Back off the app after a saved key) folded into99e508806.Checks at this level (worktree
/tmp/onb-stack, head ofonboarding/04-key-choice-import-welcome-back)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 browser-host/vite-config.test.mjs— 12 files, 203 tests passed, 0 failedNative 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