Skip to content

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

Open
matt2e wants to merge 1 commit into
onboarding/03-native-backup-statusfrom
onboarding/04-builderlab-tenants
Open

matt2e wants to merge 1 commit into
onboarding/03-native-backup-statusfrom
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 — sign-in without a plugin session, kgoose reads, link/unlink, backup, saved communities — and its dev seed #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
…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>
@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.

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