Repository navigation
Conversation
d5a5ef2 to
c1e4548
Compare
4823851 to
787e2b9
Compare
c1e4548 to
954be58
Compare
787e2b9 to
141b42b
Compare
954be58 to
b4e66e5
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, integrating Rocket, Mantis and Groot: no production defects found; one low-priority documentation correction is inline. Optional: remove the PR description’s stale claim that retryable Settings messages end in “Try again”; they now describe retrying on the next Builderlab sign-in.
Head b4e66e5944b1b16ba0e1b28baa12eb996a65fa02, base e2f928f8b8d89f9506ba1b1e703d22fd0eba9da2. Source-only: tests, CI, native backup behavior and rendered accessibility were not validated.
| that a failure first run's Try again could clear, and a lapsed sign-in, say | ||
| instead that Buzz tries again the next time the user signs in to Builderlab, | ||
| since a new sign-in hands the uploader a session and starts another pass. Idle is what a fresh launch or | ||
| a window reload reports, not evidence that no backup exists. The row has no |
There was a problem hiding this comment.
🤖 [P3] Qualify when a window reload reports idle
NsecBackupHost::document_load in src-tauri/src/nsec_backup.rs:302–312 changes the status to Idle only when it interrupts a Running pass. Settled statuses such as Complete or Failed are retained and returned by the next status read, so reloading Settings does not generally produce the idle sentence described here. Please say that a fresh launch reports idle and a reload does so when it interrupts an active pass, while preserving the explanation that idle does not prove absence of a backup.
e2f928f to
64a5f0b
Compare
b4e66e5 to
4ffb5a0
Compare
64a5f0b to
75127a4
Compare
4ffb5a0 to
1b6313a
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source review via Wes’s account, integrating Rocket, Mantis and Groot: no new production findings in this follow-up and its affected base integration. The existing P3 reload documentation correction remains: document_load resets a running pass to idle, while settled results survive reload. The earlier optional PR-description wording cleanup also remains optional; no duplicate inline comments added.
Reviewed head 4ffb5a0cd449affa8be0bce685920ed926972212, base 64a5f0bad98c51935229f39458a4489247ce0ce7, compared with prior reviewed b4e66e5944b1b16ba0e1b28baa12eb996a65fa02. The Settings feature patch is unchanged in behavior; this pass checked the revised backup offer gate and native status integration. Public description and commit material inspected; no attached images or new disclosure finding.
Source-only: no tests or app execution, CI assessment, native backup acceptance, rendered accessibility/focus or performance validation. This is a non-blocking comment, not approval.
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source review via Wes’s account, integrating Rocket, Mantis and Groot: no new production findings in this follow-up or its affected integration. The existing P3 reload-documentation correction remains: document_load resets an interrupted running pass to idle, while settled results survive reload. The earlier optional cleanup of the PR description’s “Try again” wording also remains. No duplicate inline comments added.
Reviewed head 1b6313acdc99d71a2dd587d57917ad9461b18061, base 75127a4d427c6831fa4723ffe0e92677d2344719, compared with prior reviewed 4ffb5a0cd449affa8be0bce685920ed926972212. The Settings feature implementation is unchanged; this pass checked the inherited hosted-community sign-in routing and spare-process change alongside the backup-status integration. Existing findings on the earlier stack layers remain tracked on their own PRs. Public description and commit material inspected; no attached images or new disclosure finding.
Source-only: no tests, app execution, CI assessment, native backup acceptance, rendered accessibility/focus or performance validation. The PR body’s test claims were not independently verified. This is a non-blocking comment, not approval.
…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 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
… 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 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
…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 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
…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 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
75127a4 to
a8185e9
Compare
…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 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
…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 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
…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 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
… 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 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
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 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
1b6313a to
23e9e52
Compare
…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 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
23e9e52 to
f98b7e9
Compare
…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 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
f98b7e9 to
417b71d
Compare
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 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
First run leaves the automatic backup choice on after a failed backup, so a
later Builderlab sign-in may finish the backup in the background; this row
makes that visible. `BackupRow` (`src/app/IdentitySettings.tsx`) reads
`identity.backup` through `useSyncExternalStore`, as Settings reads its
other stores, and sits in the existing rows `SettingsGroup` above "Sign out
of Buzz" as a `PreferenceRow` with no trailing control; the sentence is
wrapped in `role="status"`.
One sentence per status — idle: "Buzz hasn’t checked your key’s backup
since it opened." (what a launch or reload reports, not "no backup");
running: "Backing up your key…"; complete: "Your key is backed up to
Builderlab."; waiting{no_consent}: "Automatic backup is off for this key.";
waiting{no_session}: "Sign in to Builderlab to back up your key.";
waiting{not_linked} and every failed kind: the entry from `BACKUP_FAILURES`
(`backup-failure.ts`), so it is the same copy as first run's failed page,
with the Settings tail "Buzz tries again when you next sign in to
Builderlab." for the causes a new sign-in clears (busy, unreachable,
unconfirmed, storage, signed_out). There is no second table. Read-only: no
consent toggle, no retry.
Hidden unless backup is offered, by the same rule first run uses:
`backupOfferedFor(backup)` (`builderlab-account.ts`) answers false unless
the build has a Builderlab URL, and then returns the status's `builderlab`;
any error answers false. Known differences, documented: the
row appears after the first status read, so "Sign out of Buzz" moves down
one row once the IPC answers; retryable messages end in "Try again." though
the row has no button; under the `BUZZ_DEV_FIRST_RUN_ACCOUNT` seed first run
offers backup while the row follows the real rule.
Tests: `IdentitySettings.test.tsx` drives the real `createBackupStatus`
store over a scripted native side with a mocked Tauri `Channel`: the
sentence for complete, running, waiting{no_session}, waiting{not_linked},
failed{unreachable} and the signed_out tail; hidden when the status says
`builderlab: false` and when the build has no Builderlab portal; following
pushes at seq 2, 3 and 4 with a late seq-2 push changing nothing; no button
or switch in the row. `Settings.test.tsx`'s identity case gains an inert
`backup` store. docs/identity.md describes the row.
Stack: on the Welcome flow to keep the stack linear, but it depends only on
the merged `backup.ts`, `backup-failure.ts` and `identity.backup` and the
account client's `backupOfferedFor` — it can be reviewed before the pages
and the Welcome flow.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
417b71d to
3bee161
Compare
becb0bd to
b15fa60
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source follow-up via Wes’s account, integrating independent Rocket, Mantis and Groot reviews.
No new production findings in the backup row or the affected onboarding integration. The existing P3 reload-documentation correction still applies at docs/identity.md:553: a window reload resets the status to idle only when it interrupts an active backup pass; settled results survive. No duplicate inline comment added.
Reviewed head 3bee161d053c78e5dee82507e090ad4fe8f7c501 against base 8b5b4e675bc358cefdff4419b969212d27e97a56, including the carried backup status subscription, legacy-identity offer, canceled-sign-in fencing, retry focus handling and build-specific hosted-community routing. The read-only row and existing failure-recovery product decisions are preserved.
Source analysis only. No tests, builds, app launches or live native workflows were run, and CI was not assessed. Native backup/reload behavior, real sign-in, rendered focus/accessibility and human acceptance remain unverified. The public PR description was inspected and has no attached images. This COMMENT is not an approval.
Onboarding stack (merge in order, top to bottom):
Depends on: the merged #1816 (
backup.tstypes,backup-failure.ts,identity.backup) and #1818 (backupOfferedFor) only. It is based on #1820 to keep the stack linear; it does not use #1819 or #1820 and can be reviewed first.Diff: production +72/−14; tests +141/−3; docs +10/−0
What this layer is
BackupRowreadsidentity.backupviauseSyncExternalStore, one sentence per status (first run'sBACKUP_FAILUREScopy, with a "Buzz tries again when you next sign in to Builderlab." tail for causes a new sign-in clears), hidden unlessbackupOfferedFor(backup)holds. No consent or retry control.Review focus
backup-failure.ts, the same entries first run shows; only the Settings tail differs. This layer adds the table'scausefield (the sentence without first run's instruction) thatBackupRowreads, since it is the first caller.backupOfferedFor): the row shows only when the build has a Builderlab URL and native backup is on; any error hides the row.Checks run on this branch's tree
9818f778ewithout feat(builderlab): tenant table — a login origin per hosted relay, granted natively and accepted by the binding signer #1817 (2026-10-30).tsc --noEmitis clean at each of feat(identity): first-run Builderlab account client and dev seed — bundled-grant sign-in, account reads, key link/unlink, backup, saved communities #1818–feat(settings): read-only Key backup row in Settings → Identity #1821. At the stack head5daa01ffd([DO NOT MERGE] chore(identity): DO NOT MERGE — isolated debug Keychain slot (full stack for native testing) #1822; same tree that was tested): fullvitest run13,915 passed, 1 failed (codex-sh.test.ts, outside the stack, passes 3/3 alone);biome checkon the changed files and the design push check pass.--no-verify: the pre-push unit check failed only on 5 s timeouts in files the stack doesn't touch while the machine was at load ≈45; all 7 of those files pass when run alone. Rust is unchanged below [DO NOT MERGE] chore(identity): DO NOT MERGE — isolated debug Keychain slot (full stack for native testing) #1822 and was not re-run locally.Native evidence
None on this tree. Native first-run runs use the full tree in the DO-NOT-MERGE #1822 (it adds the isolated debug Keychain slot), and none has been made since the rebase. Treat every PR before #1822 as unit/integration-tested only.
🤖 Generated with Claude Code