Skip to content

feat(builderlab): tenant table — a login origin per hosted relay, granted natively and accepted by the binding signer - #1817

Closed
matt2e wants to merge 2 commits into
mainfrom
onboarding/04-builderlab-tenants
Closed

matt2e wants to merge 2 commits into
mainfrom
onboarding/04-builderlab-tenants

Conversation

@matt2e

@matt2e matt2e commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Onboarding stack (merge in order, top to bottom):

  1. fix(navigation): run the visit deadline only while a presenter is attached #1814 navigation: visit deadline only while a presenter is attached
  2. refactor(communities): extract the join owner and add the pre-join relay lookup #1815 communities: join owner and pre-join relay lookup
  3. feat(identity): observable nsec backup status with pass tickets, explicit lock release, and refused-recovery copy #1816 identity: native nsec backup status
  4. feat(builderlab): tenant table — a login origin per hosted relay, granted natively and accepted by the binding signer #1817 builderlab: tenant table and granted native origins ← this PR
  5. feat(identity): first-run Builderlab account client and dev seed — bundled-grant sign-in, account reads, key link/unlink, backup, saved communities #1818 identity: first-run Builderlab account client and dev seed
  6. feat(identity): first-run setup store, step table and pages (composition follows in the next PR) #1819 identity: first-run setup store, step table and pages
  7. feat(identity): the first-run Welcome flow replaces the key-setup gate (FOUNDATION: App.tsx) #1820 identity: Welcome flow replaces the key-setup gate
  8. feat(settings): read-only Key backup row in Settings → Identity #1821 settings: read-only Key backup row
  9. [DO NOT MERGE] chore(identity): DO NOT MERGE — isolated debug Keychain slot (full stack for native testing) #1822 DO NOT MERGE: isolated debug Keychain slot (full stack for native testing)

Depends on: PR 02 (builderlab-tenants.ts imports type FoundCommunity from communities/find-community.ts). Independent of 03. PRs 05–07 build on it.

Diff: production +409/−73; tests +295/−28; docs +56/−2

What this layer is

BUZZ_BUILDERLAB_TENANTS ([{relayHost, self?, loginOrigin}]) parsed identically on both hosts: builderlab-tenants.ts (parseBuilderlabTenants, discoverAuthentication(found, tenants) — hostname suffix at a label boundary, default port only, longest host wins, then NIP-11 self, else none) and crates/plugin-manager/build.rs (same rules, emits BUZZ_BUILDERLAB_ORIGINS); builderlab_origins() feeds the bundled plugin's network_origins; identity.rs's kind-24243 signer accepts any granted origin instead of only the build URL; browserCredential(host, signal, bridge, origin?) runs the whole OAuth round-trip at a tenant portal (nativeBridge exported for the next layer). Pure config plumbing; no first-run page uses it yet.

Review focus

  • The two parsers must agree: every row build.rs accepts, parseBuilderlabTenants accepts, and vice versa (ASCII-only folding, 253-byte host cap, unique hosts/self, canonical https loginOrigin, ≤16 origins). Both fail without echoing the value.
  • discoverAuthentication: label-boundary suffix matching (notblock.builderlab.xyz must not match block.builderlab.xyz), default port only, longest host wins, self can only select an origin already in the table.
  • src-tauri/src/identity.rs: the signer's allow-list is now builderlab_origins() (build URL + tenants) plus production; the tests cover granted, production, ungranted, case-changed, trailing-slash, port-changed and empty origins.
  • oauth/browser.ts: the tenant origin goes through the same oauthTarget check as the build URL; the Settings card is unchanged. The straddling files (vite.config.ts, vite-config.test.mjs, .env.example, docs/configuration.md, the README) carry only their tenant hunks here; their seed hunks land in PR 05.

Checks run on this branch's tree

  • Deferred to CI at the author's request (the build machine was under a deliberate stress load from another session, load avg 58–110, so local vitest/cargo runs were timing out in untouched files). Pushed with --no-verify; the hosted checks on this PR are the verification. The plan's checks for this layer were: cargo test -p buzzodz-plugins --locked, cargo test --manifest-path src-tauri/Cargo.toml --locked identity host_request, cargo fmt --check, vitest run src/features/identity/builderlab-tenants.test.ts src/bundled/builderlab browser-host/vite-config.test.mjs, tsc --noEmit.

Native evidence

None on this tree. The native first-run recordings and the hands-on checks on the branch were made on the full tree, which only the DO-NOT-MERGE PR 09 reproduces (it adds the isolated debug Keychain slot). Treat every layer below 09 as unit/integration-tested only.

🤖 Generated with Claude Code

@matt2e
matt2e marked this pull request as ready for review October 28, 2026 13:40
@matt2e
matt2e requested review from a team as code owners October 28, 2026 13:40
@matt2e
matt2e force-pushed the onboarding/04-builderlab-tenants branch from c133edb to eccd287 Compare October 28, 2026 14:18
@matt2e
matt2e force-pushed the onboarding/03-native-backup-status branch 2 times, most recently from 28c3c73 to c1bfe83 Compare October 28, 2026 14:21
@matt2e
matt2e force-pushed the onboarding/04-builderlab-tenants branch from eccd287 to 1ee6dc5 Compare October 28, 2026 14:21
@matt2e
matt2e force-pushed the onboarding/03-native-backup-status branch from c1bfe83 to b353bdf Compare October 29, 2026 01:41
matt2e and others added 2 commits October 29, 2026 14:03
…icit lock release, and refused-recovery copy

A cross-host change reviewed as one contract: the native nsec backup
uploader becomes observable so a caller can follow up on a specific backup
pass, and its locks release correctly.

Rust (`src-tauri/src/nsec_backup.rs`):
- `BackupStatus { seq, pass, builderlab, status }`, `status` one of `idle`,
  `running`, `complete`, `waiting { reason: no_consent | no_session |
  not_linked }`, `failed { kind: busy | signed_out | not_offered |
  other_key_linked | invalid_service | refused | unreachable | unconfirmed |
  storage }`. `builderlab` is true when a storage root exists and the
  Builderlab source is allowed for this build. Published under the state
  lock so pushes leave in `seq` order: a spawned pass publishes `running`
  then its outcome; a queued rerun publishes `running` under the latest
  ticket; a null handoff that retires a running pass publishes
  `waiting{no_session}` numbered as the latest ticket, so a queued rerun's
  ticket is settled too; `document_load` turns a running pass into `idle`.
- `classify`: 401 ⇒ `signed_out`; 403 ⇒ `other_key_linked` on upload,
  `not_offered` on enrollment and on the `current` read; 404 ⇒
  `not_offered`; 429, 5xx and transport ⇒ `unreachable`; other 4xx ⇒
  `refused`; enrollment 409 ⇒ `waiting{not_linked}`; an upload response that
  is not a stored, matching receipt ⇒ `unconfirmed`; another pass holding
  the consent lock or this key's record lock ⇒ `busy`; other local I/O ⇒
  `storage`. No response body crosses IPC.
- `trigger`/`session`/`retry` return the ticket (`Option<u64>`) of the pass
  that starts after the call; a wake during a pass gets a ticket covered by
  the single queued rerun. `nsec_backup_state` returns the snapshot;
  `nsec_backup_watch(Channel)` replaces the single watcher slot and refuses
  any window but main. Both are registered in `lib.rs`, `build.rs`,
  `capabilities/default.json` and `browser_permissions_tests.rs`.
- Recovery: a 403 on the `current` read (no `buzz_users` capability) or on
  recovery itself (not a private org, or no Buzz user role) answers the new
  `REFUSED` string "Backup recovery is not offered for this account."
  instead of the generic retry string; 401 keeps it, since a new sign-in
  may clear a rejected session.
- Lock fix (`nsec_backup_choice.rs`, `nsec_backup/persistence.rs`): the
  consent and per-identity record locks relied on closing the file to
  release the `flock`; a child spawned through fork/posix_spawn shares the
  open file description until exec, so the next `try_lock` saw contention
  and a pass reported a spurious `busy`/`storage`. A `Lease` now unlocks
  explicitly in `Drop` (the pattern `crates/credential-store` already uses).
  Probe, 3000 trials: close-only 29–57 spurious holds, unlock-then-close 0.

TypeScript consumer:
- `createBackupStatus` (`backup.ts`), exposed as `identity.backup`
  (`service.ts`): registers nothing until the first subscribe/`ready()`,
  watches before reading the snapshot, keeps the higher `seq`, re-registers
  after a failed watch. It stays out of `IdentitySnapshot`.
  `handoffBackupSession` resolves to the ticket or null and never rejects.
- `backUpWith(identity, pubkey, credential, opts)`: the waiter a first-run
  step will use — `ready()`, consent on, hand the credential to the
  `builderlab` slot, wait for the first status with `pass >= ticket` and not
  `running`, hand the slot back on every exit; `complete` resolves,
  `failed{kind}` ⇒ that kind, `waiting{not_linked}` ⇒ `not_linked`, other
  waits/idle ⇒ `interrupted`, 60 s ⇒ `timeout`; renderer-side failures are
  `unknown`/`unavailable`/`interrupted`. `BackupError(kind)` carries the
  kind, the table's message and `retryable`.
- `backup-failure.ts` (new): `BACKUP_FAILURES`, one entry per kind with
  first-run copy (cause + instruction) and whether Try again can help. The
  Settings `cause` variant and the seed's kind guard arrive with their
  callers (PRs 08 and 05).
- `RECOVERY_UNAVAILABLE`/`RECOVERY_REFUSED`/`recoveryOff()` in `service.ts`
  match the two native strings exactly; `RestoreIdentity` hides *Try
  recovery again* when recovery is off; its `expectedPubkey`/`onImport`
  props arrive with their caller (`KeyRestore`, PR 06). In
  `identity.test.tsx` the two UNAVAILABLE rows move from the Try-again
  `it.each` into a new no-retry `it.each` that adds a REFUSED row.

Tests: Rust `nsec_backup/tests.rs` (+541: running-then-outcome under the
returned ticket, the full waiting/failure table incl. busy from a held
consent or record lock, rerun tickets, supersede by key install, a null
handoff settling a rerun ticket, a 403 on `current` refusing recovery,
`document_load` with stale and new watchers, retry after timeout, flat
serialization, permissions) and `nsec_backup_choice/tests.rs` (a released
lease is free while a dup'd descriptor still shares it; a `dup` stands in
for the spawned child). TS `backup.test.ts` (new, 347: the store's lazy
registration and seq rule; the waiter's invoke order watch → state →
set_choice → session → session(null), each status → kind, stale pass
ignored, deadline, `builderlab: false`, null ticket, no native text
reaching the page), `backup-choice.test.ts` (creating an identity never
invokes watch/state), and the recovery hunks of `identity.test.tsx`: the
3-row `it.each` "offers no Try recovery again when %s answers %s" and
"matches native recovery's unavailable and refused messages exactly",
which reads `nsec_backup.rs?raw`. docs/identity.md: the observable status
paragraph and the refused-recovery line.

Stack: PR 03 of the onboarding stack; depends on nothing. 05–08 build on
it.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
…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]>
@matt2e
matt2e force-pushed the onboarding/03-native-backup-status branch from b353bdf to 6546483 Compare October 29, 2026 03:36
@matt2e
matt2e force-pushed the onboarding/04-builderlab-tenants branch from 1ee6dc5 to 1d6ceda Compare October 29, 2026 03:36

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Star Lord’s automated source review via Wes’s account.

Please replace the internal deployment data identified inline. Optional inline suggestions cover configuration-limit parity and lossless origin serialization. Align the description’s origin cap with the enforced limit: 15 distinct login origins, including the build URL.

Rocket, Mantis and Groot supplied independent source-review lanes. Reviewed head 1d6cedae14db488ec7ef86ccf16325823128226c, base 6546483b07fd588ed2972beb47ab0029a6403836. Source-only: no tests, app execution, native sign-in, live services or CI verification; first-run UI consumers arrive in later PRs.

Comment thread .env.example
# plugin-manager build, which grants every loginOrigin to the bundled Builderlab
# plugin, so set it where BUZZ_BUILDERLAB_URL is set. Public configuration; no
# deployment table is supplied by the repo. Rows: {"relayHost","self"?,"loginOrigin"}.
# BUZZ_BUILDERLAB_TENANTS='[{"relayHost":"communities.buzz.xyz","self":"12f6870117eff1a6318bd38c82a65d51dd19879b7489f57247114d0ee8a96de3","loginOrigin":"https://app.builderlab.xyz"},{"relayHost":"block.builderlab.xyz","self":"76aa9daa7edc9c2e1dc6a2a90dbce10c1bd52f060fa6506ba7bad4f50240fc01","loginOrigin":"https://block.builderlab.xyz"},{"relayHost":"test.blockstaging.build","self":"f7f4df9401ecf46cc608b0889a61d957b306721cdefe42cb24519cb5a473dcf1","loginOrigin":"https://test.blockstaging.build"}]'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 P2 — Replace internal deployment data with synthetic examples. This newly added table publishes internal tenant/staging hostnames and their relay-key mappings in a public repository. The same data appears in docs/configuration.md, the new TS tenant tests, Rust build/signing fixtures, and commit prose; the PR description also uses the internal host in its matching example. Use reserved example domains and clearly synthetic 64-hex keys throughout these public surfaces, preserving the suffix/identity test cases and legitimate attribution trailers. This is deployment-information exposure, not a claim that public relay keys are credentials.

Comment thread vite.config.ts
parseCommunityAliases(aliases);
// Public too: the same table the native build grants to the Builderlab plugin.
const tenants = env.BUZZ_BUILDERLAB_TENANTS ?? "";
parseBuilderlabTenants(tenants);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Optional P3 — Validate the same distinct-origin limit in Vite. parseBuilderlabTenants(tenants) checks rows but never counts login origins or includes BUZZ_BUILDERLAB_URL. Consequently 15 distinct tenant origins plus a different build URL pass Vite configuration, while crates/plugin-manager/build.rs:114–128 rejects that same configuration because its union exceeds 15. Check the same deduplicated union here and add boundary cases for a shared versus distinct build origin. Native enforcement already fails closed; this fixes the promised cross-host build-input parity.

if origins.len() > MAX_ORIGINS {
return Err("Too many Builderlab login origins");
}
Ok((origin, origins.join(",")))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Optional P3 — Preserve origins across serialization. Canonical HTTPS URL validation does not exclude commas in a hostname: the pinned url 2.5.8 host parser uses the WHATWG/IDNA URL deny list, which permits them. Joining a permitted origin such as https://a,b.example here and splitting it in builderlab_origins() turns it into https://a and b.example, so the compiled grant/signing list no longer represents the configured origin. Consider rejecting commas in both build-input validators or using an unambiguous encoding, with a round-trip case. This is trusted configuration handling, not an untrusted runtime bypass.

Base automatically changed from onboarding/03-native-backup-status to main October 29, 2026 20:58
@matt2e

matt2e commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Closing: the tenant table is out of the onboarding stack. Release builds don't set BUZZ_BUILDERLAB_TENANTS, so it did nothing in shipped apps, and signing in to several Builderlab servers from one build is being designed separately in #buzz-enterprise-identity (alongside #1424 and #1461). The rest of the stack (#1818–#1822) is rebased on main without it: a typed community address now always gets the connect choice, and first-run sign-in, link, backup and saved communities all use BUZZ_BUILDERLAB_URL.

@matt2e matt2e closed this Oct 29, 2026
matt2e added a commit that referenced this pull request Nov 2, 2026
…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]>
matt2e added a commit that referenced this pull request Nov 2, 2026
…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]>
matt2e added a commit that referenced this pull request Nov 2, 2026
…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]>
@matt2e
matt2e deleted the onboarding/04-builderlab-tenants branch November 5, 2026 03:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants