Repository navigation
feat(identity): the first-run Welcome flow replaces the key-setup gate (FOUNDATION: App.tsx) - #1820
Conversation
f5752d2 to
d35338d
Compare
787e2b9 to
141b42b
Compare
d35338d to
4972a16
Compare
4972a16 to
5c9a654
Compare
141b42b to
e2f928f
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source review via Wes’s account.
Three changes requested inline: fence canceled sign-in continuations, preserve the legacy-key migration entry point, and remove deployment rollout details from public documentation. One optional retry-focus improvement is also inline.
Rocket, Mantis and Groot supplied independent review lanes. Reviewed head e2f928f8b8d89f9506ba1b1e703d22fd0eba9da2, base 5c9a65404d7d0dd929650f6f7098e7aa5936841d. Source-only: tests, app/native flows, rendered accessibility and CI were not executed or verified; the author’s deferred validation remains open.
5c9a654 to
32731cd
Compare
e2f928f to
64a5f0b
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source follow-up via Wes’s account.
No new findings in the tenant-removal/rebase changes. Three earlier items still need changes:
- Sign-in continuation: the previous P1 cross-origin consequence is resolved by removing tenant routing. A P2 remains at
Welcome.tsx:247–260: a canceled sign-in can settle after a newer sign-in reaches key import, restart account checks, replace that page and discard its typed draft. Fence the continuation to its attempt before reading the shared session; cover cancellation followed by a newer sign-in and a late old completion. - Legacy-key migration (P2) remains absent from Welcome. Keeping
LegacyCommunityImportinside the gate preserves community import, but does not restore the explicit old-key import action promised bydocs/identity.md:427. Restore that action or explicitly resolve the product contract and update its documentation/tests. - Public rollout details (P2) remain in
docs/identity.md:66–67,151; replace environment-specific rollout statements with the portable capability contract.
The retry-focus suggestion remains optional. Existing comments are linked rather than duplicated.
Rocket, Mantis and Groot completed independent lanes. Reviewed head 64a5f0bad98c51935229f39458a4489247ce0ce7, base 32731cd75ccc4cacfea3249f9016fc894435747f. Source-only follow-up: no tests, app/native flows, rendered accessibility, performance or CI verification. The PR’s stated native-validation gap remains open.
64a5f0b to
75127a4
Compare
32731cd to
235e33e
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source follow-up via Wes’s account.
No new findings in the hosted-community recognition and automatic-sign-in changes. The following earlier P2 items still need changes:
- Canceled sign-in continuation:
Welcome.tsx:248–260still lets a canceled attempt resume after a newer sign-in reaches key import, replacing that page and losing its draft. Fence the continuation to its attempt on new sign-in, cancellation and unmount. The earlier cross-origin consequence remains resolved. - Legacy-key migration: the active Welcome gate still omits the opt-in legacy-key import promised by
docs/identity.md:432–438. Restore the action or explicitly resolve the product contract and update its documentation/tests. - Public rollout details: deployment rollout statements remain at
docs/identity.md:72–73,157. Replace them with the portable capability contract. The new public-host/key classifier is not an additional disclosure finding.
The retry-focus suggestion remains optional. Existing comments are linked rather than duplicated.
Rocket, Mantis and Groot completed independent lanes. Reviewed head 75127a4d427c6831fa4723ffe0e92677d2344719, base 235e33ead40427dcf9d905783e33105c9894a2b5. Source-only follow-up: no tests, app/native flows, rendered accessibility, performance or CI verification. The PR’s stated native-validation gap remains open.
235e33e to
17c1081
Compare
17c1081 to
71ef1e1
Compare
75127a4 to
a8185e9
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source follow-up via Wes’s account.
One new P2 configuration-parity finding is inline. The earlier findings still need changes:
- Canceled sign-in continuation is P1 again: tenant routing restores its cross-origin consequence. At
Welcome.tsx:267–274, canceled attempt A can finish after B has signed in and paused at key setup. The session owner discards A’s result but resolves A’s caller, which combines B’s shared credential with A’s captured origin. The account-check requests then send B’s credential to A’s configured portal. Fence this continuation on new attempts, cancellation and unmount before reading the credential or replacing account state; cover A settling after B succeeds. The abort checks inside the OAuth client do not fence Welcome’s continuation. - Legacy-key migration (P2) remains missing from the active Welcome gate. Restore the opt-in migration action through the existing identity service, or explicitly resolve the product contract and update the retained documentation/tests.
- Public rollout details (P2) remain in
docs/identity.md:76–77,160–162. Replace deployment-specific availability statements with the portable capability contract.
The existing retry-focus suggestion remains optional. Earlier comments are linked rather than duplicated.
Rocket, Mantis and Groot completed independent lanes; Star Lord integrated the source evidence. Reviewed head a8185e95a3f62739ba7dcf90dfcc84bd000be124, base af6e8993d4f090b4fd4e7526a0dec1e42e51090f. Public PR description inspected; no description attachments. Source-only: no tests, builds, app/native sign-in, rendered accessibility or performance checks were run. One hosted-CI snapshot showed 21 successful, 17 running and 2 skipped checks; it does not establish acceptance. The PR’s stated native-validation gap remains open.
8663469 to
480bf96
Compare
…nted natively and accepted by the binding signer
Pure configuration plumbing; no first-run page uses it yet. A build-time
table `BUZZ_BUILDERLAB_TENANTS` — rows `{ relayHost, self?, loginOrigin }`
— says which relays each Builderlab deployment hosts and where its people
sign in, so a later first-launch screen can send a typed community URL's
sign-in to the right portal instead of one hard-coded Builderlab.
Frontend (`src/features/identity/builderlab-tenants.ts`, new):
`parseBuilderlabTenants` validates the table at Vite configuration time
(hostname shape, ASCII-only folding, 253-byte cap, unique hosts, unique
64-hex `self`, `loginOrigin` equal to its own canonical https origin, no
unknown fields) and fails without echoing the value;
`discoverAuthentication(found, tenants)` answers `{ kind: "builderlab",
origin }` or `{ kind: "none" }`: hostname match (exact, or a suffix at a
label boundary — `block.builderlab.xyz` covers `buzz.block.builderlab.xyz`
but not `notblock.builderlab.xyz` — default port only, longest host wins),
then NIP-11 `self` equality (a known deployment survives a DNS move; it can
only select an origin already in the table), else `none`.
`vite.config.ts` parses the table and defines
`VITE_BUZZ_BUILDERLAB_TENANTS`. `browserCredential(host, signal, bridge,
origin?)` (`oauth/browser.ts`) runs the whole OAuth round-trip —
`/v1/auth/login`, `/login/exchange`, `/auth/me` — at `oauthTarget(origin)`,
the tenant origin going through the same https/no-credential/no-query
check as the build URL; `nativeBridge` is exported for the next PR. The
Settings card is unchanged and always uses the build URL.
Native (`crates/plugin-manager/build.rs`, `src/lib.rs`): `build.rs` loads
`BUZZ_BUILDERLAB_TENANTS` beside `BUZZ_BUILDERLAB_URL` through
`build_env::load`, validates every row with the same rules as the frontend
parser (at most 16 origins), and emits `BUZZ_BUILDERLAB_ORIGINS` (build URL
origin first, then each distinct tenant origin); `builderlab_origins()`
feeds the `block.builderlab`/`bundled` grant's `network_origins`, and
`BUILDERLAB_ORIGIN` stays for `identity.rs`. Malformed input fails the
build with a fixed message. `serde_json` joins that crate's
`[build-dependencies]` (already in the lockfile; no Cargo.lock change).
`src-tauri/src/identity.rs`: the kind-24243 binding signer accepts a
challenge for production Builderlab or any login origin the build grants
(`builderlab_origins()`) instead of only production or the build URL;
unknown, case-changed, trailing-slash, port-changed and empty origins are
still refused.
Config and docs: `.env.example` and `docs/configuration.md` document the
input, the matching rules and the grant, with the three verified rows
(`communities.buzz.xyz` → `https://app.builderlab.xyz`,
`block.builderlab.xyz` → `https://block.builderlab.xyz`,
`test.blockstaging.build` → `https://test.blockstaging.build`, each with its
relay key). The repository ships no deployment default. The Builderlab
README's two grant sentences name the tenant origins.
Tests: `builderlab-tenants.test.ts` (the three seed rows, subdomain,
case/final-dot, lookalikes, non-default port, `self` fallback and its
limits, nested-row precedence, parser rejections without echo incl. an
over-long host and a Kelvin sign); `oauth/browser.test.ts` (an explicit
origin for the whole round-trip and the same refusal of `http://`);
the tenant lines of `browser-host/vite-config.test.mjs` (the define and the
non-echoing rejection); Rust `build.rs` tests (origin union, file-sourced
table, malformed rows), `host_request.rs` (every granted origin),
`identity/tests.rs` (granted, production, ungranted and malformed origins;
the IPC test signs and verifies for every granted origin and refuses a
foreign one).
Not in this PR: `src-tauri/src/identity.rs` hosted-communities binding for
other tenants stays as on main apart from the origin rule;
`src/bundled/hosted-communities/native.ts` still hard-codes the production
API.
Stack: PR 04 of the onboarding stack, on 02 (`builderlab-tenants.ts`
imports `type FoundCommunity` from `communities/find-community.ts`);
independent of 03. 05–07 build on it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
… a plugin session, kgoose reads, link/unlink, backup, saved communities — and its dev seed
All of first run's network logic, no UI. Nothing on main calls it yet; the
Welcome flow (PR 07) is the caller.
`src/bundled/builderlab/oauth/first-run.ts` (new): binds a `Host` to the
Builderlab plugin's own bundled native grant (`block.builderlab`/`bundled`,
granted at build time from `BUZZ_BUILDERLAB_URL` and the tenant table) and
exposes `firstRunCredential(signal, origin?)`, the plugin's
`browserCredential` run before any plugin session exists. That credential
is separate from the Settings session and is never persisted.
`src/features/identity/builderlab-account.ts` (new): `FirstRunAccount`, the
kgoose reads and writes first run needs, every request a POST with `{}` or
a body, `X-BB-Session-Credential` from the first-run session and `Origin`
set to the login origin that signed the user in, through `bundledHost.request`:
- `linkedKey` (`/v1/buzz/nostr-identities/current`): a 2xx with `identity`
null or absent is *not linked* (kgoose never sends `MISSING_MAPPING` on
this route); a present identity whose `pubkey_hex` is not 64 lowercase
hex fails — a linked account must never be given a second key.
- `ownedCommunities` (`/v1/buzz/communities/list`, archived and insecure
hosts excluded) and `knownCommunities`
(`/v1/buzz/known-communities/list`, read with the Builderlab plugin's own
`listedAddress`, exported from `known-communities/client.ts` for this; a
row this client cannot read rejects the whole list).
- `bind(pubkey)`: reads `current` first and returns when the account already
holds this key, else challenge → native `identity_sign_builderlab_binding`
→ verify, which must confirm this key. 409 `identity_already_bound` /
`pubkey_already_bound` take the hosted-communities card's copy.
- `unbind(old, new)` (`/nostr-identities/delete`): the route takes no key
and deletes whatever is bound, so it sends the delete only while `current`
still answers the old key, counts as done when the delete echoes the old
key or a re-read shows none or the new key, and fails without Try again
if the old key is still bound. The real fix is an `expected_pubkey_hex`
on the delete, noted in a comment.
- `backUp(pubkey)`: refuses with `BackupError("unavailable")` unless
`backupOfferedFor(backup, origin)` holds — this build's uploader backs up
to Builderlab and the sign-in is at the build's own portal (a tenant's
credential is never handed to the native uploader) — then `backUpWith`.
- `backupStatus()` (`/v1/buzz/nsec-backups/status`): the listed hex keys;
"unavailable" without a request unless offered, and on a 404; other
answers map to the backup failure kinds.
- `recovery`: the native recovery driver, so the dev seed can replace it.
- `saveCommunity(url)` (`/v1/buzz/known-communities/add`,
`wss://host[:port]`).
- Shared `call`/`post`: transient failures (no status, 429, 5xx) are
retryable, 4xx refusals and rejected shapes are not; any HTTP 401 is
"Your Builderlab sign-in is no longer valid."; failure copy names the
step and status, never a response body; kgoose codes the card's table
lacks get first-run copy (`invalid_origin`, `invalid_challenge`,
`challenge_*`, `too_many_attempts`; `challenge_expired/consumed/not_found`
retryable since Try again fetches a fresh challenge); the correlation ID
stays off the page and goes to a development-only `console.warn`.
Dev seed (`first-run-seed.ts`, new; `seededAccount`):
`BUZZ_DEV_FIRST_RUN_ACCOUNT` — `unlinked`, or `{"npub","communities",
"known","backups","recovery","backup":"<kind>"|"fail-once","unlink":<status>}`
— replaces the kgoose reads, the binding, the unlink, the backup and backup
recovery, and nothing else, on a dev server. `vite.config.ts` validates it
(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. Bad values are rejected without being echoed. A developer's real
account binding never changes under a seed.
Docs: `docs/configuration.md` (the seed row and "First-run account seed
(development)" section), `.env.example`, the Builderlab README's first-run
paragraph.
Tests: `builderlab-account.test.ts` (1,337: every route's exact request at
a tenant and at the build URL; linked/not-linked readings; the bind
sequences incl. the already-bound no-op and 409 from verify; unbind's
current-then-delete with echo and re-read outcomes; backUp's tenant guard
and handoff; backupStatus mappings; saveCommunity; the retryability, 401
and correlation-ID rules; the seeded account), `first-run-seed.test.ts`
(268: every field, rejections without echo, the seeded backup/unlink/
recovery behaviours), `oauth/first-run.test.ts` (the bundled grant request
shape with and without an origin), and the seed lines of
`browser-host/vite-config.test.mjs` (serve vs build define, rejections).
Stack: PR 05 of the onboarding stack, on 04; imports 02 (`destination`),
03 (`backup`, `backup-failure`, `service`) and 04 (`oauth/browser`). 06–08
build on it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ion follows in the next PR)
The stateless building blocks of the first-run flow, reviewable as
components against DESIGN.md and Kenny's onboarding layouts. Nothing
imports them until PR 07 (`Welcome.tsx`); the repo has no unused-export
check, so lint and typecheck pass with them standing alone.
Setup store and steps (`setup.ts`): `createSetup()` with `start(steps,
then)`, `retry()` (re-runs only the failed step, then the rest) and
`reset()`, over snapshots `idle` / `running{label}` / `paused{at}` /
`failed{error, retryable, skipsLink}` / `done`; a step's `run` may resolve
to a resting state that overrides the caller's `then`. Step factories:
`checkAccount(account, onLinked, onKnown)` (the linked-key and
known-communities reads in parallel under one label; the list read never
fails setup), `checkBackup(account, onListed)` (never fails setup),
`createKey`, `importKey`, `linkKey`, `unlinkKey(account, old, new)`,
`restoreCommunities(known, communities, decide)` and `finish()`.
`restore-communities.ts`: `restoreKnown(known, communities, { concurrency:
3, startBy: 45_000 })` — the same signed profile read the Join Community
page uses, at most three at once, nothing started after 45 s, each admitted
destination saved through `communities.joined`; outcomes `admitted` /
`denied` / `unavailable` in list order; never throws. `key-import.ts`:
`classifyKeyImport` (empty / `ncryptsec` refused with a clear message /
`nsec` with its npub preview / invalid) and `readKeyFile` (1 KiB cap, first
non-blank line).
Pages (all `src/features/identity/`, stateless, props-driven):
`CommunityUrlField` (bare host, `wss://`/`https://`, or an invite link —
`https://<host>/invite/<code>`, `buzz://join?relay=…&code=…` — via
`enteredCommunity`; Continue stays enabled while empty/invalid and shows one
inline error), `InviteCodeField`, `JoinCommunity` (owned rows named "Join
{host}" for assistive technology, the URL field, the disabled "Scan a QR
code" / "Create a community — Coming soon" buttons, on the onboarding
layout), `KeyChoice` (Create my key / I already have a key / Use a
different account, the signed-in address, the backed-up-key sentence only
when the lookup listed none), `KeyImport` (masked field, Reveal/Hide, file
drop/picker, npub preview, `expectedPubkey` mismatch refused in the page,
"Importing your key…" on the same screen), `KeyLost`, `KeyRestore` (the K3
column; "Your key is stored securely on this device." only while the
restore may still install the key; the unavailable/refused page),
`BackupOffer`, `ConnectChoice` ("Connect to your community" with the host
on its own mono line), `WelcomeBack` (the linked npub with Copy, Restore
from backup when listed, Enter your private key, I lost this key, Use a
different account), `PublicKeyPanel`, `SetupProgress`, `Wireframe`
(`WireframeScreen` on Kenny's frame, `WireframePanel` as the one stacking
group), `ProfileStep` (a pure form over `ProfileFields`: Continue hands the
trimmed draft out, Skip for now leaves). Every `h1` carries `tabIndex={-1}`
and the Back triggers `data-return-focus`, for the heading-focus handoff
`Welcome` performs. Shared owners only: `Button`, `Field`, `Input`,
`Checkbox`, `Avatar`, `LoadingRegion`, `OnboardingScreen` and Kenny's
`IdentitySetup.module.css` classes (unchanged); role colours and type roles
only, no new tokens or variants.
Tests: `setup.test.ts` (retry/rest/idle semantics, `checkAccount` and
`checkBackup` never failing setup, `unlinkKey` sending nothing while the
old key is on the device), `restore-communities.test.ts` (three reads at
once, outcomes in list order, dedupe and insecure skip, throwing `joined`,
the 45 s cutoff under fake timers), `key-import.test.ts`,
`CommunityUrlField.test.ts` (8 accepted and 10 refused forms),
`ProfileStep.test.tsx`. The pages' rendered checks (Chromium/WebKit, light/
dark, 480/1100 px, 150 % text) were done on the branch in scratch fixtures
and are recorded in the branch's commit history; mount a page with stub
props to repeat them.
Stack: PR 06 of the onboarding stack, on 05; imports 02 (`destination`,
`find-community` types, `api`, `ProfileFields autoFocus`), 03 (`service`,
`RestoreIdentity` `expectedPubkey`/`onImport`, `BackupError`) and 05
(`builderlab-account` types). 07 composes these.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…e (FOUNDATION: App.tsx)
The composition and the product flow. On native desktop with no human key
in the credential store, the app now opens on `Welcome` instead of the
"Your Buzz identity" key import/create screen, and stays there until a key
exists and setup has nothing left to do.
FOUNDATION edit (`src/app/App.tsx`), the three approved lines: import
`Welcome` from `../features/identity/Welcome` instead of `IdentitySetup`,
and wrap `ConnectedApp` in `<Welcome identity={services.identity}
communities={services.communities} invites={services.invites}>`. No other
FOUNDATION file (`services.ts`, `main.tsx`, `communities/service.ts`,
`plugins/api.ts`) is touched. `IdentitySetup` stays for the credential-free
browser fixture and gets a one-line comment.
`Welcome.tsx` (new, 940 lines) owns the flow; its state (`finding`,
`inviteCode`, `entered`, `reads`, `known`, `linked`, `owned`, `backups` and
the `setup` store) is shared by every lane, which is why it lands whole:
- Landing: "Buzz" on Kenny's onboarding screen — Sign in with email,
Community URL (bare host, origin or invite link; Continue starts the
lookup) and Scan a QR code — Coming soon.
- A typed address is looked up before any sign-in (`FindCommunity`, two
unauthenticated GETs) and fails fast; a found community decides the
portal through `discoverAuthentication`: a tenant's relay signs in at that
tenant's portal, an unmatched one pauses on "Connect to your community"
(Sign in with email at the build portal, or I already have a key, which
imports without any account and lands on Join Community).
- After sign-in: Checking your account… (linked key + saved destinations in
parallel) → Checking for a backed-up key… → Set up your key (Create my
key: Creating → Linking → Backing up; I already have a key: Importing →
Linking → Back up this key?; Use a different account) or, for a linked
account, Welcome back (Restore from backup when listed, Enter your private
key accepting only the linked key, I lost this key → Start over with a new
key: Creating → Unlinking the old key → Linking → Backing up, Use a
different account). Both post-sign-in pages show the signed-in address.
- Couldn’t finish setup: Try again for retryable failures; once a key
exists a failed backup offers Continue without backup, a failed link or
unlink blocks with Try again only (nothing links the key later); before a
key exists, Back to the landing. The key choice and reveal pages never
show hex; nothing is signed to a relay before the user chooses to join.
- Restoring your communities…: the account's saved destinations are read
with the signed profile read and admitted ones saved; setup rests on Join
Community (typed URL) or Join a community when nothing was admitted,
enters the app on the first admitted one otherwise, and hands a typed
address or invite code that was not among them to the rail's
Add-a-community dialog through `invites.open`, the intent OS deep links
already use.
- Join Community joins through `admitCommunity`/`completeJoin` (journal
before any effect, read before reclaim, `accept-policy` → `claim` with an
invite code, the signed read); a denied read opens "This community needs
an invite" with the npub, Copy, an invite-code field, Back and Try again;
a user with no profile anywhere gets the Set up your profile step before
the membership is saved; a join after a Builderlab sign-in is also saved
to the account (`saveCommunity`, best effort).
- Invariants: every return to the landing goes through `backToLanding`
(clears discovery, the account reads and the first-run session; keeps the
typed address); `enterApp()` fires a pending handoff, clears what `Welcome`
kept and resets setup. Each page change moves focus to the new page's
heading, or back to the choice that opened it on Back.
Tests: `identity.test.tsx` (whole file, +4,837/−109 vs main: the landing,
discovery, Connect, Join/profile/invite, key import, Welcome back/restore,
Start over/owned, failure and seed journeys over a `nativeRelay` fixture
with a real keypair, `relay_sign`, per-origin `relay_http` and a real
`createCommunities`; the exact `invoke`/`relay_http` sequences are asserted
throughout). This PR's `Welcome.tsx` is the `19df106d5` version, which
turns these six journeys green — they are red against the earlier
`572d69398` Welcome: "a link that fails after the key is created blocks
setup with only Try again…", "a link that keeps failing after a key is
imported…", "a transient unlink failure after Start over…", "an unlink
refused after Start over offers only Try again…", "a transient link failure
after a successful unlink…", "a development seed's unlink status fails Start
over's unlink…". `src/app/first-run-join-handoff.test.tsx` (new): the real
`App` + `createServices()` under mocked Tauri commands, the gate lifting
with the Add-a-community dialog open on a typed address or invite link, the
dialog's claim with the code, and Cancel. `src/app/first-run-navigation.test.tsx`
(new): proves PR 01's deadline rule through the real first-run gate.
Docs: `docs/identity.md` first-run block (every page, what is signed and
what is not, where Back lands, the failed page), `docs/communities.md`
(`Welcome` as the second caller of the join owner and the invite intent),
`docs/deep-links.md`.
Stack: PR 07 of the onboarding stack, on 06; depends on 01–06. 08 is based
on it only to keep the stack linear.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…r one Review follow-up on PR 1820, thread 1 (canceled sign-in continuation resumes). `Welcome`'s `signIn` awaited `login.signIn()` and then read the session's global state. The session store resolves a canceled attempt's promise normally, and native HTTP does not observe the abort, so a tenant sign-in canceled mid-exchange could settle after a later sign-in at the build portal succeeded. Its continuation then saw `signed-in`, built the account from the later credential with the canceled tenant's origin (sending that credential to the tenant's portal) and restarted setup over whatever page the user was on. `signIn` now records the cancel count when it starts and continues only if no cancel happened since. `backToLanding`, the only cancel path while mounted, bumps the count. The count is not bumped on entry, so a repeated call while one sign-in is pending cannot strand it. Unmount needs no fence: it cancels a pending sign-in, which then settles `signed-out`. Test: a new `identity.test.tsx` journey. A tenant sign-in is canceled with its exchange still pending, then a build-portal sign-in reaches Enter your private key with a draft typed. The canceled exchange then resolves; the test waits on that attempt's account digest and drains microtasks. The page and draft stay, `account` is built once (later credential, no origin), and no relay call follows the lookup. It fails on the previous `Welcome.tsx` (the page is replaced by a restarted setup) and passes now. Docs: `docs/identity.md` states that a canceled sign-in finishing later is ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…t today's rollout Review follow-up on PR 1820, thread 3 (deployment rollout details in public docs). Three sentences in `docs/identity.md` said which deployments enable a feature today. Each now states what the build and service must provide, and keeps the fallback: - The backed-up-key lookup: kgoose answers `404` wherever backups are not configured. Dropped "which today is everywhere but staging's public Builderlab". - Welcome back's restore: the unavailable page applies where recovery is unavailable for the deployment or refused for the account (public organizations are refused by design). Dropped "no tenant enables it today". - Automatic backup: anywhere else (a tenant sign-in, or a build without backup configured) the step fails at once. Dropped "which today is". `grep today docs/identity.md` now returns nothing. The tenant example rows in `docs/configuration.md` stay; the reviewer excluded them. Docs only, no behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…eated denial
Review follow-up on PR 1820, thread 4 (Try again loses focus on a
repeated denial). `Failure` disabled Try again while its read ran. Both
Chromium and WebKit move focus off a focused button when it becomes
natively disabled. If the relay refused the key again, the page key
(`done:join-error:<title>`) did not change, so `Welcome`'s focus effect
did not run and keyboard users were left on `body`.
Try again now uses the shared Button's loading state while its own read
runs (`loading={busy === "read"}`). Base UI keeps it focusable with
`aria-disabled` and `aria-busy`, and the Button still blocks activation.
While an invite code is being claimed it stays plainly disabled. The
label stays "Try again": DESIGN.md's Button rule keeps the label's
geometry and accessible name while loading, so "Trying again…" is gone.
Back and the invite code field are unchanged.
Test: the invite-page Try again journey now holds a retry that the relay
refuses again. While the read is pending, Try again is focused,
`aria-busy` and `aria-disabled`, has no native `disabled`, and Enter
starts nothing. After the refusal, focus is still on Try again. Enter
then retries and is admitted, and the relay calls show each attempt is
the same signed read. jsdom keeps focus on a disabled button, so on the
previous `Welcome.tsx` the journey fails at the `aria-busy` check, not
at the focus check. Browser focus loss was confirmed with a standalone
Playwright probe (plain buttons, not the app): native `disabled` sent
focus to `body` in both engines, `aria-disabled` kept it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
… landing Review follow-up on PR 1820, thread 2 (preserve the explicit legacy-key migration entry point). Replacing the key-setup gate with `Welcome` left `identity.legacyAvailable()` / `identity.importLegacy()` reachable only from `IdentitySetup`, which only the browser fixture still mounts. Someone upgrading from old Buzz could no longer bring its identity over in one click. The offer comes back as an alert dialog over the Welcome landing. The landing, its actions, the pages, the steps and the setup store are unchanged; no FOUNDATION file is touched. `LegacyIdentityOffer.tsx` (new) exports `useLegacyIdentityOffer(identity, open)`. `Welcome` calls it with its other hooks and renders the result only in the landing JSX, so the folder check and a dismissal outlive a visit to another page (+6 lines in `Welcome.tsx`): - `identity.legacyAvailable()` runs once per entry into `missing`, with `.catch(() => false)` and an `active` guard, as in `IdentitySetup`. The landing's buttons never wait for it. - The dialog opens only when the check answered true, the landing is the current page (so setup is idle), no sign-in is pending, and it was not dismissed during this entry. A check that answers off the landing or during a sign-in waits until the landing is back. - It uses the shared `AlertDialog` and `Button`, with no new tokens or variants. Title **Continue with your Buzz identity?**, then **Not now** and the prominent **Continue with your Buzz identity**, which calls `identity.importLegacy()`. While the import runs, Continue shows the Button's loading state, Not now is disabled, and `pending` blocks Escape and Close. - A failed import shows its error inside the dialog (role alert), and Continue tries again. Only errors after the click are shown, so a setup error from another page never appears in the dialog. - Success lifts the gate into the app with no sign-in, link or backup, where `LegacyCommunityImport` takes over as on main. - Not now and Escape close it for this entry. `finalFocus` returns focus to the landing heading, since launch focuses nothing. Base UI's deferred initial focus still wins over `Welcome`'s heading-focus effect when the dialog opens as the landing returns. - After Sign out, the next launch is a new entry into `missing`, so the offer returns while old Buzz's folder exists. Tests (`identity.test.tsx`): the `IdentitySetup` migration journey moves onto the live `Welcome` gate: - no dialog when the check answers false or fails; - no import before the click; Continue imports, stays pending, and enters the app with `connect` and `account` never called; - Not now and Escape keep the landing, focus its heading, import nothing, and the offer stays closed after a failed sign-in and Back; - a failed import shows its alert in the dialog, and Continue retries; - no dialog while a sign-in is pending or setup runs; after a failed Create my key and Back it opens with focus and without the create error; - the offer returns on the launch after Sign out. Each guard was checked fail-before by mutation. Removing the render, the pending-sign-in condition, `finalFocus`, the error scoping, `pending`, or the dismissal each fails its targeted test. Mechanical updates: 36 exact command sequences gain `identity_legacy_available`; 29 `invoke` mocks (including the shared `nativeRelay` and `landingRelay`) and five `mockResolvedValueOnce` chains answer it `false`. So do the `first-run-join-handoff`, `first-run-navigation` and `window-chrome` fixture mocks. Docs: `docs/identity.md` describes the dialog over the welcome screen: when it opens, the click-only read, the error and retry, entering the app with no Builderlab sign-in, link or backup, dismissal, and its return after Sign out. The first-launch bullet in `docs/buzz-help.md` now describes the Welcome flow and the offer. Validation: lint, typecheck and `design:check` pass; the full Vitest suite passes (854 files, 14,737 tests); `window-chrome.spec.mjs` passes in Chromium. Rendered in a scratch fixture (not committed) in Chromium and WebKit, light and dark, at 1100 and 480 px and with 150 % text. At 480 px with 150 % text the primary label is wider than the compact dialog, and the shared footer scrolls horizontally. Not run: the native app and a real keychain prompt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The dialog's title already names the identity, and the long button label wrapped the footer onto two rows at 1100 px and clipped at 480 px with enlarged text. Not now and Continue now share one row at both sizes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…n is always at BUZZ_BUILDERLAB_URL
PR 1817 (the Builderlab tenant table, stack PR 04) was closed on
2026-10-29 and PRs 05 and 06 merged into main without it; this PR was
rebased onto main without it on 10-30 (head `75127a4d4`). The 11-02
force-push rebuilt the branch from the pre-closure 10-28 commits, so
`5b78e9de8`'s tenant table and the stale 05/06 variants built on it came
back: `builderlab-tenants.ts`, the `BUZZ_BUILDERLAB_TENANTS` input in
`vite.config.ts`, `build.rs` and `.env.example`/`docs/configuration.md`,
the `builderlab_origins()` grant union in `host_request.rs`, the
binding-signer origin rule in `identity.rs`, the per-origin
`browserCredential`/`firstRunCredential`/`builderlabAccount` signatures,
and `Welcome`'s per-tenant routing. The review thread on
`vite.config.ts:70` (the renderer reading the table through Vite's
mode-aware `loadEnv` while the native grant reads only the process and
`.env.local`) was against that reintroduced code.
This commit puts main's (`af6e8993d`) version of those 20 files back and
restores the tenant-free PR 07 pieces from `75127a4d4`:
`builderlab-hosted.ts` (the fixed host/`self` list; a hosted community
signs in at once, always at `BUZZ_BUILDERLAB_URL`) and its test, the
`Welcome.tsx` hunks (`connect(signal)`, `account(credential, identity)`,
no `tenants` prop or `loginOrigin`), and the ConnectChoice/setup
comments. Every later fix stays: the canceled-sign-in cancel-count
fence (`0dc22c0d4`, hand-merged into the tenant-free `signIn`), the
capability-contract docs (`3854b5a18`, minus its tenant clause), Try
again's loading state (`86a4b4c30`) and the old-Buzz offer dialog
(`b3ccd00e3`, `f2a94e076`). No FOUNDATION file changes; Rust is back to
main.
Tests: the canceled-sign-in journey is now two build-portal sign-ins
(Sign in with email, Cancel sign-in, Sign in with email, I already have
a key, a typed draft, then the stale exchange resolves); it still fails
at the Enter your private key heading without the fence. The 14
`tenants={tenants}` journeys follow `75127a4d4`'s connect-choice forms
again, and the hosted-URL journey returns. The tenant lines of
`browser-host/vite-config.test.mjs`, `builderlab-account.test.ts`,
`oauth/*.test.ts` and the Rust tests go with their code.
Docs: `docs/identity.md` describes the hosted list and the connect
choice without a tenant table.
Validation: `tsc --noEmit`; `identity.test.tsx` (132),
`first-run-join-handoff.test.tsx`, `first-run-navigation.test.tsx`,
`builderlab-hosted.test.ts` and `vite-config.test.mjs` (23) pass. The
full suite, lint, design checks and native builds were not run.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…Builderlab hosts it Correction to `50bad0cdb`, whose message called sign-in "always at BUZZ_BUILDERLAB_URL" for a hosted URL as intended: for another deployment's relay that auto-sign-in was wrong. A public release (`app.builderlab.xyz`, bb-public) given a `block.builderlab.xyz` community URL signed in at once at bb-public, then linked the user's new or imported key to that bb-public account and offered to back it up there. `main` cannot do this (its key-setup gate never calls Builderlab); the auto-sign-in came in with `builderlab-hosted.ts` in `75127a4d4`, after #1817 closed saying a typed address always gets the connect choice. Rule: a known Builderlab relay signs in at once only when its owning portal is the origin of `VITE_BUZZ_BUILDERLAB_URL`. Host x build: typed host \ build app.builderlab block.builderlab test.blockstaging *.communities.buzz.xyz sign in key only key only *.block.builderlab.xyz key only sign in key only *.test.blockstaging.build key only key only sign in any other host Connect Connect Connect (was: every hosted cell signed in at the build portal). A relay publishing a listed `self` key follows its row; an FI or unconfigured build matches no portal, so every known host is key only. - `builderlab-hosted.ts`: each row names its portal; `builderlabHosting(found, build)` answers `{ portal, here }` or `undefined` (hostname first, then `self`; default port only). - `Welcome.tsx`: `onFound` signs in only when `here`; otherwise it keeps the other portal's host in `elsewhere` and pauses on the connect choice. A join admitted after a landing sign-in is not saved to this account when another Builderlab hosts it (`ProfileStepState.found` keeps `self` so the guard can use the relay-key fallback). - `ConnectChoice.tsx`: with `elsewhere`, the page names that portal and offers only a prominent "I already have a key" (and Back); title, host line, page key and `data-return-focus="import"` are unchanged. Shared `Button` only, no new tokens or variants. Tests: `builderlab-hosted.test.ts` asserts each accepted host's portal, the 3x3 host x build `here` matrix, empty/invalid/path-bearing build URLs and the `self` fallback. `identity.test.tsx`: the hosted journey is now a 3-case `it.each` with the build URL stubbed to each portal; a new 2-case journey (bb-block URL in a public build, bb-public URL in a Block build) shows the key-only page, returns focus to "I already have a key" from the import page's Back, imports, reaches Join Community with `connect` and `account` never called and exactly `identity_restore, identity_legacy_available, relay_http x2, identity_import, relay_http x2`, all to the typed origin. It fails on `50bad0cdb` (no "Connect to your community" heading: sign-in started) and passes now. A third journey joins a bb-block community after signing in at a public build and asserts nothing is saved to the account; it fails with the guard removed. Docs: `docs/identity.md` (portal per deployment, sign in only for this build's, key only for another's, no save) and one sentence in `docs/buzz-help.md`. Validation: `tsc --noEmit`; Biome on the changed files; `vitest run` of `identity.test.tsx`, `builderlab-hosted.test.ts`, `first-run-join-handoff.test.tsx` and `first-run-navigation.test.tsx` (4 files, 171 tests) pass. Not run: the full suite, hooks, `design:check`, and a rendered check of the key-only page (themes, 480/1100 px, 150 % text, keyboard and pointer). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The owner's decision: block/buzz-app supports one Builderlab per build
for now, the public deployment (`app.builderlab.xyz`, hosting
`communities.buzz.xyz`). This supersedes `d2e2b7ee9`'s multi-deployment
routing.
- `builderlab-hosted.ts`: keeps only the `communities.buzz.xyz` row
(host, relay `self` key, portal) and drops the `block.builderlab.xyz`
and `test.blockstaging.build` rows. `builderlabHosting(found, build)`
(`{ portal, here } | undefined`) becomes
`hostedByThisBuilderlab(found, build): boolean`. It is true only when
the listed deployment hosts the relay (hostname exact or label-boundary
suffix on the default port, else its `self` key) and its portal is the
origin of `VITE_BUZZ_BUILDERLAB_URL`. An empty, invalid or other-origin
build URL, as in an unconfigured or federated-identity build, answers
false, so such builds never sign in at once. The table stays, so a
second deployment could be added later.
- `Welcome.tsx`: `onFound` signs in only when `hostedByThisBuilderlab`
holds and otherwise pauses on the connect choice. The `elsewhere` state
and prop are gone. `finishJoin` saves an admitted join to the account
as it did before `d2e2b7ee9`, without the other-Builderlab guard.
- `ConnectChoice.tsx`: back to its single layout. "Buzz relies on a key
for your identity.", prominent Sign in with email, small I already
have a key, and Back. The key-only layout and its "different
Builderlab" copy are gone.
So a community another Builderlab deployment runs, such as a
`block.builderlab.xyz` relay typed into a public build, now gets the
ordinary Connect to {host} choice.
Tests: `builderlab-hosted.test.ts` is rewritten for the boolean. It
covers the `communities.buzz.xyz` apex, subdomain, case, final-dot and
`:443` forms, and every lookalike and non-default-port refusal (now
including a bb-block host). It also covers build URLs of "", "not a
url", the API-path form and `https://block.builderlab.xyz`, and the
`self` fallback with bb-public's key, on the default port only. A new
assertion makes sure an unknown relay with no build URL never matches.
`identity.test.tsx`: the hosted journey runs once, with a public build
URL stubbed. The two key-only "another Builderlab" journeys and the
not-saved join journey are removed. A new 2-case journey covers a bb-block
URL in a public build and a bb-public URL in a build with no Builderlab
URL: each pauses on the ordinary choice (Sign in with email, I already
have a key, Back) with no sign-in started.
Docs: `docs/identity.md` describes one Builderlab per build and the
ordinary choice for every other community. It adds a note that
federated-identity builds do not yet support the first-run flow: Sign in
with email needs the bundled Builderlab plugin's own network grant,
which an FI build does not have (#1461), so sign-in fails after the
browser step. `docs/configuration.md` has the same note under the FI
build input. `docs/buzz-help.md` drops the "different Builderlab"
sentence.
Validation: `vitest run` of `builderlab-hosted.test.ts`,
`identity.test.tsx`, `first-run-join-handoff.test.tsx` and
`first-run-navigation.test.tsx` (4 files, 158 tests) pass.
`tsc --noEmit` and Biome on the changed source files pass. Not run: the
full suite, hooks, lint, `design:check` and native builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…rom first run
The owner asked to remove these placeholders for now; they will come
back once the features exist.
- Welcome landing: the disabled "Scan a QR code — Coming soon" button
is gone. Sign in with email, the Community URL field and the old-Buzz
offer are unchanged.
- Join a community (`JoinCommunity.tsx`): the disabled "Scan a QR code"
and "Create a community" buttons are gone. The action column is empty
(`{null}`, as other onboarding screens already do). The owned rows and
the URL field are unchanged.
Welcome back's "Recover from another device — Coming soon" stays; it
was not part of the request.
Tests: `identity.test.tsx` drops the two assertions that the disabled
buttons were present, and the landing test's title no longer mentions
QR scanning.
Docs: `docs/identity.md` no longer lists either button on the welcome
screen or on Join a community.
Validation: `vitest run` of `identity.test.tsx`,
`first-run-join-handoff.test.tsx` and `first-run-navigation.test.tsx`
(3 files, 141 tests) pass. `tsc --noEmit`, Biome on the changed source
files and `git diff --check` pass. Not run: a rendered check of the
landing and Join a community without the buttons (themes, 480/1100 px,
150 % text).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
becb0bd to
b15fa60
Compare
…ion (FOUNDATION: App.tsx, services.ts) (#2249) ## Summary New users of FI builds (the internal Block build, 0.7.7) couldn't finish first run. **Sign in with email** opened the browser, and then the app showed *Couldn't sign in. Could not complete Builderlab sign-in.* Welcome exchanged the code through the bundled Builderlab plugin's network grant. FI builds don't have that grant: Builderlab calls go through the native NIP-FI session instead ([#1461](#1461)). [#1820](#1820) documented this as a known gap. With this change, Welcome in FI builds signs in through the native NIP-FI session and sends setup's account calls through its forwarder. Non-FI builds are unchanged. - **Sign-in:** `firstRunCredential` calls `federatedIdentity.signIn()`. The browser step, code exchange and token all stay native. It then reads `GET v1/auth/me` through the forwarder. The returned credential's value is the session's public revision. If a session already exists, it signs in again so the browser can choose the account. A renewal that doesn't finish (canceled or timed out in the browser) gets back the old session at the same revision, so first run refuses rather than continuing as the old account. Native's bare failure codes map to the same copy as the community sign-in. - **Account calls:** identity check, link and unlink, owned communities, and known communities use the same routes through the forwarder via `federatedBuilderlabAccount`. Each call is checked against the session revision that sign-in returned, so a session that changed refuses without sending anything. - **Acting as the bundled installation:** native FI admission checks only the grant for the (id, revision) pair, not whether the plugin is running. So Welcome calls as `block.builderlab`/`bundled` through `ctx.extend({ pluginOwner })`, the same installation `bundledHost` already acts as. No native or grant changes. - **Backup:** `backUpWith` hands the uploader the FI session revision (`handoffFederatedBackupSession`). Backup status returns "unavailable". The forwarder has no route for it, and the FI plugin doesn't read one either, so Welcome back offers no restore and an imported key is offered a backup. The Block build has no backup configured today. - **After Welcome:** the native session persists, so `LaunchSignIn` finds it already signed in and doesn't prompt again. **Known gap:** after a first-run backup, `backUpWith` clears the uploader's `builderlab` slot as it does today. The plugin shares the FI session, so it won't hand that slot off again until the session changes. This only matters where backup is configured, and the Block build doesn't configure it. **FOUNDATION:** `services.ts` gets one line to create the bundled-owner FI view, only in FI builds. `App.tsx` gets one prop to pass that view to Welcome. Out of scope: `browser.ts` still hides native error strings. This is deliberate, so provider bodies are never forwarded. The "Sign in with email" label also stays as is in FI builds. ## Validation - Full `vitest run`: 873 files and 15,427 tests passed (7 files and 21 tests skipped). `tsc --noEmit` and `biome check` are clean. These ran on the tree committed as `41388fb64`. - New tests: - `first-run.test.ts`: the bundled-owner view registers and requests as `block.builderlab`/`bundled` with a real cordis `Context`. The FI sign-in reads the account and never opens the plugin's browser handoff. Failures map to the right copy, including a renewal that doesn't finish, a native failure code, and an account read that isn't JSON. Cancel calls `cancelSignIn`. - `builderlab-account.test.ts`: forwarder paths and headers, revision fencing, backup status "unavailable", and the federated backup handoff. - `identity.test.tsx`: Welcome's defaults route through the FI session. - I mutation-checked the owner and Welcome-wiring tests: each fails when the change is reverted. - **Real FI build, step 2 only:** I ran a debug desktop build with the production bb-block descriptor, on a fresh profile with its own HOME and keychain. Sign-in now reaches **Welcome back** (the test account already has a linked key). On `main`, the same binary shows *Couldn't sign in*. I didn't run steps 3–4 below (Create my key, Cancel sign-in) in a real FI build. ## Before and after Both runs use the same FI debug build (bb-block descriptor) on a fresh profile. Only the frontend differs. I cut the browser sign-in step, and blacked out the account's email and npub. ### Before: `main` (`90ce5e37a`) shows *Couldn't sign in* https://github.com/user-attachments/assets/3c04ff79-f020-4123-b226-09aeafa15c24 ### After: this PR (`373d7484d`) lands on Welcome back https://github.com/user-attachments/assets/b109f807-7a4a-4dd3-8fef-f199ec76523c ## Human test (FI build, bb-block staging) 1. Build and run with the staging descriptor from `squareup/buzz-releases` `scripts/build-macos-app-staging.sh`, and use a fresh profile with no key: `BUZZ_BUILD_FEDERATED_IDENTITY_CONFIG='<staging descriptor>' BUZZ_BUILDERLAB_URL= just desktop` 2. Click **Sign in with email** and finish the browser step. You should land on **Create my key** (or **Welcome back** for an account that already has a linked key), not *Couldn't sign in*. 3. Click **Create my key**. Setup should link the key and continue. The app should open without a second organization sign-in prompt. 4. Run it again on a fresh profile and on one already signed in to the organization. On each, click **Cancel sign-in** during the browser step. You should return to the landing page with no key created. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Jitter <2b2e6415e748c35180847a37aadae06a11e30dddc76b784e7fd9354c4eb42e7a@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ion (FOUNDATION: App.tsx, services.ts) (block#2249) ## Summary New users of FI builds (the internal Block build, 0.7.7) couldn't finish first run. **Sign in with email** opened the browser, and then the app showed *Couldn't sign in. Could not complete Builderlab sign-in.* Welcome exchanged the code through the bundled Builderlab plugin's network grant. FI builds don't have that grant: Builderlab calls go through the native NIP-FI session instead ([block#1461](block#1461)). [block#1820](block#1820) documented this as a known gap. With this change, Welcome in FI builds signs in through the native NIP-FI session and sends setup's account calls through its forwarder. Non-FI builds are unchanged. - **Sign-in:** `firstRunCredential` calls `federatedIdentity.signIn()`. The browser step, code exchange and token all stay native. It then reads `GET v1/auth/me` through the forwarder. The returned credential's value is the session's public revision. If a session already exists, it signs in again so the browser can choose the account. A renewal that doesn't finish (canceled or timed out in the browser) gets back the old session at the same revision, so first run refuses rather than continuing as the old account. Native's bare failure codes map to the same copy as the community sign-in. - **Account calls:** identity check, link and unlink, owned communities, and known communities use the same routes through the forwarder via `federatedBuilderlabAccount`. Each call is checked against the session revision that sign-in returned, so a session that changed refuses without sending anything. - **Acting as the bundled installation:** native FI admission checks only the grant for the (id, revision) pair, not whether the plugin is running. So Welcome calls as `block.builderlab`/`bundled` through `ctx.extend({ pluginOwner })`, the same installation `bundledHost` already acts as. No native or grant changes. - **Backup:** `backUpWith` hands the uploader the FI session revision (`handoffFederatedBackupSession`). Backup status returns "unavailable". The forwarder has no route for it, and the FI plugin doesn't read one either, so Welcome back offers no restore and an imported key is offered a backup. The Block build has no backup configured today. - **After Welcome:** the native session persists, so `LaunchSignIn` finds it already signed in and doesn't prompt again. **Known gap:** after a first-run backup, `backUpWith` clears the uploader's `builderlab` slot as it does today. The plugin shares the FI session, so it won't hand that slot off again until the session changes. This only matters where backup is configured, and the Block build doesn't configure it. **FOUNDATION:** `services.ts` gets one line to create the bundled-owner FI view, only in FI builds. `App.tsx` gets one prop to pass that view to Welcome. Out of scope: `browser.ts` still hides native error strings. This is deliberate, so provider bodies are never forwarded. The "Sign in with email" label also stays as is in FI builds. ## Validation - Full `vitest run`: 873 files and 15,427 tests passed (7 files and 21 tests skipped). `tsc --noEmit` and `biome check` are clean. These ran on the tree committed as `41388fb64`. - New tests: - `first-run.test.ts`: the bundled-owner view registers and requests as `block.builderlab`/`bundled` with a real cordis `Context`. The FI sign-in reads the account and never opens the plugin's browser handoff. Failures map to the right copy, including a renewal that doesn't finish, a native failure code, and an account read that isn't JSON. Cancel calls `cancelSignIn`. - `builderlab-account.test.ts`: forwarder paths and headers, revision fencing, backup status "unavailable", and the federated backup handoff. - `identity.test.tsx`: Welcome's defaults route through the FI session. - I mutation-checked the owner and Welcome-wiring tests: each fails when the change is reverted. - **Real FI build, step 2 only:** I ran a debug desktop build with the production bb-block descriptor, on a fresh profile with its own HOME and keychain. Sign-in now reaches **Welcome back** (the test account already has a linked key). On `main`, the same binary shows *Couldn't sign in*. I didn't run steps 3–4 below (Create my key, Cancel sign-in) in a real FI build. ## Before and after Both runs use the same FI debug build (bb-block descriptor) on a fresh profile. Only the frontend differs. I cut the browser sign-in step, and blacked out the account's email and npub. ### Before: `main` (`90ce5e37a`) shows *Couldn't sign in* https://github.com/user-attachments/assets/3c04ff79-f020-4123-b226-09aeafa15c24 ### After: this PR (`373d7484d`) lands on Welcome back https://github.com/user-attachments/assets/b109f807-7a4a-4dd3-8fef-f199ec76523c ## Human test (FI build, bb-block staging) 1. Build and run with the staging descriptor from `squareup/buzz-releases` `scripts/build-macos-app-staging.sh`, and use a fresh profile with no key: `BUZZ_BUILD_FEDERATED_IDENTITY_CONFIG='<staging descriptor>' BUZZ_BUILDERLAB_URL= just desktop` 2. Click **Sign in with email** and finish the browser step. You should land on **Create my key** (or **Welcome back** for an account that already has a linked key), not *Couldn't sign in*. 3. Click **Create my key**. Setup should link the key and continue. The app should open without a second organization sign-in prompt. 4. Run it again on a fresh profile and on one already signed in to the organization. On each, click **Cancel sign-in** during the browser step. You should return to the landing page with no key created. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Jitter <2b2e6415e748c35180847a37aadae06a11e30dddc76b784e7fd9354c4eb42e7a@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Onboarding stack (merge in order, top to bottom):
#1817 (the Builderlab tenant table) was closed on 2026-10-29 and is not in the stack. #1821 and #1822 are still based on this PR's pre-rebase head
86634692, which is the same change as480bf96b. They do not havebecb0bd7yet and need rebasing onto this head.Builds on (all merged): #1814–#1816, #1818 and #1819.
Welcome.tsximports #1815'sFindCommunity/find-community/join/join-journal/destination, #1816'sservice, #1818'sbuilderlab-account/first-run-seed/oauth/first-run, and all of #1819.first-run-navigation.test.tsxneeds #1814.Diff (18 files, +7568/−207): production +1064/−16; tests +5993/−160; docs +511/−31. No Rust, build configuration or FOUNDATION file other than
App.tsxchanges.CI status: the latest run fails only Rust and tool integration, and so CI required. The cause is a new
zipadvisory thatmainhas already fixed in #2137. This branch needsmainmerged in before CI can pass. Details are under Checks below.What this layer is
The composition and the product flow.
Welcome.tsx(936 lines) covers:FindCommunity;builderlab-hosted.ts(48 lines) matches hosts undercommunities.buzz.xyzon the default port, or a relay publishing that deployment's NIP-11selfkey.hostedByThisBuilderlabholds only when that deployment's portal,https://app.builderlab.xyz, is the origin of this build'sBUZZ_BUILDERLAB_URL. An empty, invalid or other-origin build URL (an unconfigured or federated-identity build) never signs in at once. The table keeps room for a second row, but none is added;BUZZ_BUILDERLAB_URL, or bring a key;admitCommunity/completeJoin, the profile step, the known-communities restore and the invite-intent handoff;backToLanding/enterAppinvariants and the heading focus handoff.LegacyIdentityOffer.tsx(64 lines) offers old Buzz's identity as a dialog over the landing (see the review follow-ups below).The one FOUNDATION edit:
App.tsximportsWelcomeinstead ofIdentitySetupand passesidentity,communitiesandinvites(+7/−3).IdentitySetup.tsxgets a one-line comment.JoinCommunity.tsx(from #1819, which nothing onmainmounts) loses its disabled Scan a QR code and Create a community buttons. Docs describe the whole flow.Not supported: federated-identity builds. Welcome's Sign in with email uses the bundled Builderlab plugin's own network grant. Builds with
BUZZ_BUILD_FEDERATED_IDENTITY_CONFIGdo not have that grant (#1461), so sign-in fails after the browser step. FI builds need their own first-run path before they adopt this flow. This is documented indocs/identity.md(first-run section) anddocs/configuration.md(federated identity build input).Review follow-ups
SHAs here are the current ones. Threads and comments from before the 2026-11-02 rebase use the old SHAs; the mapping is in the next section.
e65782e9: a canceled sign-in that settles after a later one is ignored.signIncontinues only if no cancel happened since it started.ad85304f:docs/identity.mdstates the backup and recovery capability contract instead of which deployments enable them.a80d2f1c: Try again on the invite page uses the shared Button's loading state, so focus stays on it through a repeated denial.d647876f+f8a1f9db: the old-Buzz key bring-over is back, as an alert dialog over the landing. Title Continue with your Buzz identity?, actions Not now and Continue.identity.legacyAvailable()runs once per entry intomissing, andidentity.importLegacy()runs only on Continue.LegacyCommunityImporttakes over as onmain.e1841cf4: removes the tenant table that re-entered this PR (below). Its message, and an earlier version of this description, said hosted sign-in was "always atBUZZ_BUILDERLAB_URL" as intended. That was wrong for a community another Builderlab hosts: a public release (app.builderlab.xyz) given ablock.builderlab.xyzcommunity URL signed in at the public Builderlab and linked the new or imported key to that account.mainnever did this, because its key-setup gate never calls Builderlab.7bec8638: the first fix. It gave each listed deployment (public, Block, staging) its portal, and another deployment's community got a connect page offering only a key. Superseded by480bf96b.480bf96b: one Builderlab per build for now, the public deployment (owner decision).block.builderlab.xyzandtest.blockstaging.buildrows, the key-only connect page (elsewhere) and the join save guard.builderlabHosting→{ portal, here }becomeshostedByThisBuilderlab→boolean. It is true only for acommunities.buzz.xyzrelay (or itsselfkey) in a build whoseBUZZ_BUILDERLAB_URLorigin ishttps://app.builderlab.xyz.becb0bd7: the disabled "Coming soon" placeholders are removed (owner request; they come back once the features exist).docs/identity.mdmentions are updated.Rebase onto
main(2026-11-02)At 10:37 UTC this branch was rebased from
af6e8993dontomainat67e404917. The PR's diff is the same before and after apart from hunk offsets (17 files, +7580/−199 at the time;becb0bd7came after). Every commit SHA changed:5b78e9deaea3fe700dc22c0de65782e93854b5a1ad85304f86a4b4c3a80d2f1cb3ccd00e/f2a94e07d647876f/f8a1f9db50bad0cde1841cf4d2e2b7ee7bec863886634692480bf96bThe tenant table re-entered and is removed again
The 2026-11-02 01:04 UTC force-push rebuilt this branch from the pre-closure 10-28 commits. That brought back #1817's tenant table (
aea3fe70) and the 05/06 variants built on it (171c8051,5cf1b65d):builderlab-tenants.ts;BUZZ_BUILDERLAB_TENANTSinput invite.config.tsandbuild.rs;host_request.rsand the binding-signer origin rule inidentity.rs;Welcome's per-tenant routing.e1841cf4puts backmain's (af6e8993d) version of those 20 files. It restores the tenant-free pieces from75127a4d4:builderlab-hosted.tsand its test, andWelcome'sconnect(signal)andaccount(credential, identity). Every review fix above stays.7bec8638later gave eachbuilderlab-hosted.tsrow its portal, with no tenant table and no build input, and480bf96bkeeps only the public row. The three stale commits still appear in this PR's commit list. Aftere1841cf4their net diff againstmainis zero, and the repository squash-merges.Review focus
src/app/App.tsx. The import swap and the<Welcome identity communities invites>wrapper are the only three approved lines. After the rebase ontomain, theLegacyCommunityImportthatmainplaced insideIdentitySetupstays inside<Welcome>, so it still renders only once setup is complete (resolution approved by the author). No other FOUNDATION file (services.ts,main.tsx,communities/service.ts,plugins/api.ts) is in the stack.readyand setup isidle.enterApp()is the only way setup becomes idle with a key.backToLandingis the only path back to the landing: it clears discovery, reads and the first-run session, and keeps the typed address.Welcome's other hooks, so the folder check and a dismissal outlive visits to other pages.identity.test.tsxjourneys are grouped by lane in file order: landing/discovery and the old-Buzz offer, Connect, Join/profile/invite, key import, Welcome back/restore, Start over/owned, then failures and seed. They assert exactinvoke/relay_httpsequences.first-run-join-handoff.test.tsxruns the realApp+createServices().first-run-navigation.test.tsxproves fix(navigation): run the visit deadline only while a presenter is attached #1814's rule through the real gate.tests/fixtures/window-chrome.tsxgains one line that answersidentity_legacy_availablewithfalse.Checks run on this branch's tree
becb0bd7(this head): run 41430912324 has 37 checks passing, 2 skipped and 2 failing.pnpm check(Biome, typecheck,design:check), the frontend build and a quarter of the Vitest suite.cargo-deny --locked check), and so CI required.zip4.6.1, which the app reaches only throughtauri-plugin-updater2.13.3.Cargo.locknordeny.toml.mainat 11:33 UTC, moves the updater to 2.14.1 andzipto 9. This branch's base (67e404917) predates it, so a re-run fails again. It needsmainmerged in, or a rebase.becb0bd7:tsc --noEmit, Biome on the changed source files,git diff --check, and three Vitest files with 141 tests (identity.test.tsx,first-run-join-handoff.test.tsxandfirst-run-navigation.test.tsx).480bf96b:tsc --noEmit, plus four Vitest files with 158 tests, run withBUZZ_TEST_WORKERS=2:builderlab-hosted.test.ts,identity.test.tsx,first-run-join-handoff.test.tsxandfirst-run-navigation.test.tsx. Biome passed on the five changed source and test files.7bec8638'sWelcome.tsx,ConnectChoice.tsxandbuilderlab-hosted.tsin place (2 failed). A bb-block URL in a public build, and a bb-public URL with no build URL, each got the key-only page.row !== undefinedguard removed fromhostedByThisBuilderlab, itsself-fallback test fails: an unknown relay matched when there was no build URL.--no-verify, as the owner asked), any native build and a rendered check.ConnectChoiceis back to itse1841cf4layout.7bec8638ande1841cf4:tsc --noEmitand the first-run Vitest files passed: 171 tests in four files at7bec8638. Ate1841cf4, 132 tests inidentity.test.tsx, the other first-run files andvite-config.test.mjs(23 tests).480bf96blater removed7bec8638's key-only journeys along with that behaviour. The canceled-sign-in journey, rewritten with two build-portal sign-ins, still failed without the cancel-count check.e1841cf4):d647876f: lint, typecheck anddesign:checkpassed; the full Vitest suite passed (854 files, 14,737 tests);window-chrome.spec.mjspassed in Chromium.f8a1f9dbshortened the primary label, Not now and Continue share one row at both widths.f2a94e07passed.LegacyIdentityOffer.tsxor whereWelcomerenders it.Native evidence
None on this tree, and the native-validation gap is still open. Native first-run runs use the full tree in the DO-NOT-MERGE #1822, which adds the isolated debug Keychain slot. #1822 is still on the pre-rebase head, and no native run has been made since the 10-30 rebase, so the old-Buzz dialog's real keychain prompt has not been seen either. Treat every PR before #1822 as unit/integration-tested only.
Why this branch is green where
572d69398was red. This PR'sWelcome.tsxis the19df106d5version: a failed link blocks first run with Try again only, instead of offering Continue without linking. Sixidentity.test.tsxjourneys depend on it and fail against the earlier Welcome:#1791's quiet read retries. After the rebase onto
mainwith #1791, theidentity.test.tsxrelay fixture's unreachable saved community answers a web page instead of a 503.inspectProfilewould retry a 503 for seconds, and those retries leaked into later tests. With the web page, the restore test keeps asserting exactly one discovery read without waiting out a retry schedule.🤖 Generated with Claude Code