Browser cockpit: serve the buzz desktop frontend as a NIP-07 web client #16

Open
opened 2026-07-30 05:04:52 +01:00 by vnprc-agent · 0 comments

Serve Block's buzz desktop frontend as a browser cockpit: a plain web page against the buzz relay, signing through a NIP-07 browser extension, with no Tauri backend and no page-held private key. This is the public half of the work — a small adapter patch against the pinned upstream source, a reproducible package, and an opt-in static-serving NixOS interface. Deployment composition lives downstream.

The relay already carries the whole protocol client-side: desktop/src/shared/api/relayClientSession.ts is a complete NIP-01 + NIP-42 client in TypeScript, every read is POST /query and every write POST /events, and desktop/src/testing/e2eBridge.ts covers 195 of 247 Tauri commands with a real browser WebSocket, real fetch, and real secp256k1 in its relay mode. The Rust backend supplies only socket transport, event signing, and OS keyring — so a browser adapter is thin, and upstream CI keeps the seam it rides from rotting.

Milestones

M1 — adapter patch (this issue's first PR). A patch set against the pinned upstream revision, plus its test coverage:

  1. A cockpit build flag (VITE_BUZZ_COCKPIT=1 layered on build:e2e) and a pre-render bootstrap that fetches same-origin /buzz-cockpit-config.json, discovers a NIP-07 provider, validates the signer, seeds the required localStorage records, and renders an actionable fatal gate before app render when config or signer is unavailable. Unflagged build:e2e stays upstream's test harness and build still tree-shakes the bridge out.
  2. A discriminated relay-mode signer: raw private-key signing remains only for upstream fixtures; cockpit signing delegates to window.nostr, re-checks the provider pubkey per call, and validates the returned event byte-for-byte against the unsigned template, expected pubkey, recomputed event ID, and Schnorr signature.
  3. NIP-98 on both central relay HTTP paths: a fresh kind 27235 event over the exact URL, method, exact body digest, and a nonce, sent as Authorization: Nostr <base64(event)>. No relay-mode request relies on the dev-mode X-Pubkey header.
  4. Real NIP-44 encrypt/decrypt-to-self through the extension, failing closed; plaintext passthrough stays mock-test-only and unreachable in cockpit mode.
  5. Truthful key custody: get_nsec and import_identity reject as extension-managed, persist_current_identity never creates a page key, sign-out clears page state without claiming to delete the extension key, and the baked fixture key is compiled out of cockpit bundles.
  6. Narrow relay behaviors: relay-origin media display, signed kind 7 reaction publication and kind 5 removal, is_shared_identity returning false without a warning, and a string description at every relay-mode channel boundary where the native converter promises one.
  7. A deny-by-default cockpit dispatcher ahead of the bridge's command switch. Its audited allowlist holds only relay-backed chat operations plus the plugin:websocket|* relay transport; browser-native and custody commands are answered by the dispatcher itself; everything else rejects unavailable in browser cockpit, and native-only UI entry points are hidden rather than left to dead-end.
  8. Unit coverage for the signer validator, config contract, and dispatcher classification, plus a Firefox Playwright project qualifying the above against a live relay. Upstream's own relay integration spec runs unchanged as a regression guard.

M2 — package and static-service interface. A packages.<system>.buzz-cockpit output built from the same pinned source object as the relay and CLI with the locked pnpm graph, installed at share/buzz-cockpit/; a required canonical services.buzz.relayUrl rendered into RELAY_URL with the HTTP origin and media base derived from it; and an opt-in services.buzz.cockpit serving the immutable package plus generated non-secret runtime JSON at an origin root through its own nginx listener, defaulting to 127.0.0.1:3001 and adding no firewall port. Positive and mutation-style negative checks cover pin equality, required config, URL derivation, loopback binding, firewall absence, asset 404 behavior, and generated unit text.

M2 coordinates with #15 so there is one upstream pin and one buzz module: extend its accepted contract if it has not merged, consume its landed names if it has.

Notable constraints

The allowlist admission rule is subtle. The cockpit dispatcher runs before the bridge switch, so allowlisting a command merely forwards to whatever case exists there. A command may join the allowlist only when that branch is already real in relay mode. Anything whose only case is a mock must be answered by the dispatcher itself or denied — allowlisting a mock-only command reintroduces fabricated native success, which is exactly what this boundary exists to prevent. A set-equality test against the switch parsed out of the bridge source makes a newly added upstream command fail review rather than become silently reachable.

Firefox only, by dependency, and it is a tripwire not a preference. The relay emits a literal Access-Control-Allow-Headers: *. The Fetch spec excludes Authorization from that wildcard; Chromium and Safari enforce the exclusion and Firefox currently does not, so the cross-origin NIP-98 header only reaches the relay in Firefox. If a Firefox update starts enforcing it, /query and /events preflight will fail; the remedy is an explicit relay allow-header list or a same-origin proxy, and no cockpit-side workaround exists. The Firefox project's NIP-98 assertions are how that regression gets caught.

Deliberately absent. Huddle/voice, managed agents and ACP runtimes, projects/git, mesh compute, local archive, and OS-keyring custody are Rust-native and stay unavailable — for a cockpit that is the point. Browser media upload, native pickers and downloads, and message deletion are also out of scope: the bridge's existing upload descriptors are fabricated and must not be presented as successful uploads. Relay-side agent participants and their messages remain ordinary, visible chat data.

Out of scope. No relay protocol or router changes, no reuse of the relay's own SPA serving, no new web client or fork of the desktop UI, and no cleanup of the 11k-line bridge beyond the narrow adapter seams.

Open upstream defect

An intermittent TypeError: Cannot read properties of null (reading 'trim') has been reported near channel creation and is not reproduced deterministically. The string-contract correction in item 6 is interface hardening, not a diagnosis: a bounded instrumented reproduction run is the gate for calling anything a crash fix, and a clean run leaves the intermittent cause open rather than closed.

Serve Block's [buzz](https://github.com/block/buzz) desktop frontend as a browser cockpit: a plain web page against the buzz relay, signing through a NIP-07 browser extension, with no Tauri backend and no page-held private key. This is the public half of the work — a small adapter patch against the pinned upstream source, a reproducible package, and an opt-in static-serving NixOS interface. Deployment composition lives downstream. The relay already carries the whole protocol client-side: `desktop/src/shared/api/relayClientSession.ts` is a complete NIP-01 + NIP-42 client in TypeScript, every read is `POST /query` and every write `POST /events`, and `desktop/src/testing/e2eBridge.ts` covers 195 of 247 Tauri commands with a real browser `WebSocket`, real `fetch`, and real secp256k1 in its relay mode. The Rust backend supplies only socket transport, event signing, and OS keyring — so a browser adapter is thin, and upstream CI keeps the seam it rides from rotting. ### Milestones **M1 — adapter patch (this issue's first PR).** A patch set against the pinned upstream revision, plus its test coverage: 1. A cockpit build flag (`VITE_BUZZ_COCKPIT=1` layered on `build:e2e`) and a pre-render bootstrap that fetches same-origin `/buzz-cockpit-config.json`, discovers a NIP-07 provider, validates the signer, seeds the required localStorage records, and renders an actionable fatal gate before app render when config or signer is unavailable. Unflagged `build:e2e` stays upstream's test harness and `build` still tree-shakes the bridge out. 2. A discriminated relay-mode signer: raw private-key signing remains only for upstream fixtures; cockpit signing delegates to `window.nostr`, re-checks the provider pubkey per call, and validates the returned event byte-for-byte against the unsigned template, expected pubkey, recomputed event ID, and Schnorr signature. 3. NIP-98 on both central relay HTTP paths: a fresh kind `27235` event over the exact URL, method, exact body digest, and a nonce, sent as `Authorization: Nostr <base64(event)>`. No relay-mode request relies on the dev-mode `X-Pubkey` header. 4. Real NIP-44 encrypt/decrypt-to-self through the extension, failing closed; plaintext passthrough stays mock-test-only and unreachable in cockpit mode. 5. Truthful key custody: `get_nsec` and `import_identity` reject as extension-managed, `persist_current_identity` never creates a page key, sign-out clears page state without claiming to delete the extension key, and the baked fixture key is compiled out of cockpit bundles. 6. Narrow relay behaviors: relay-origin media display, signed kind `7` reaction publication and kind `5` removal, `is_shared_identity` returning false without a warning, and a string `description` at every relay-mode channel boundary where the native converter promises one. 7. A deny-by-default cockpit dispatcher ahead of the bridge's command switch. Its audited allowlist holds only relay-backed chat operations plus the `plugin:websocket|*` relay transport; browser-native and custody commands are answered by the dispatcher itself; everything else rejects `unavailable in browser cockpit`, and native-only UI entry points are hidden rather than left to dead-end. 8. Unit coverage for the signer validator, config contract, and dispatcher classification, plus a Firefox Playwright project qualifying the above against a live relay. Upstream's own relay integration spec runs unchanged as a regression guard. **M2 — package and static-service interface.** A `packages.<system>.buzz-cockpit` output built from the same pinned source object as the relay and CLI with the locked pnpm graph, installed at `share/buzz-cockpit/`; a required canonical `services.buzz.relayUrl` rendered into `RELAY_URL` with the HTTP origin and media base derived from it; and an opt-in `services.buzz.cockpit` serving the immutable package plus generated non-secret runtime JSON at an origin root through its own nginx listener, defaulting to `127.0.0.1:3001` and adding no firewall port. Positive and mutation-style negative checks cover pin equality, required config, URL derivation, loopback binding, firewall absence, asset 404 behavior, and generated unit text. M2 coordinates with #15 so there is one upstream pin and one buzz module: extend its accepted contract if it has not merged, consume its landed names if it has. ### Notable constraints **The allowlist admission rule is subtle.** The cockpit dispatcher runs before the bridge switch, so allowlisting a command merely forwards to whatever case exists there. A command may join the allowlist only when that branch is already real in relay mode. Anything whose only case is a mock must be answered by the dispatcher itself or denied — allowlisting a mock-only command reintroduces fabricated native success, which is exactly what this boundary exists to prevent. A set-equality test against the switch parsed out of the bridge source makes a newly added upstream command fail review rather than become silently reachable. **Firefox only, by dependency, and it is a tripwire not a preference.** The relay emits a literal `Access-Control-Allow-Headers: *`. The Fetch spec excludes `Authorization` from that wildcard; Chromium and Safari enforce the exclusion and Firefox currently does not, so the cross-origin NIP-98 header only reaches the relay in Firefox. If a Firefox update starts enforcing it, `/query` and `/events` preflight will fail; the remedy is an explicit relay allow-header list or a same-origin proxy, and no cockpit-side workaround exists. The Firefox project's NIP-98 assertions are how that regression gets caught. **Deliberately absent.** Huddle/voice, managed agents and ACP runtimes, projects/git, mesh compute, local archive, and OS-keyring custody are Rust-native and stay unavailable — for a cockpit that is the point. Browser media upload, native pickers and downloads, and message deletion are also out of scope: the bridge's existing upload descriptors are fabricated and must not be presented as successful uploads. Relay-side agent participants and their messages remain ordinary, visible chat data. **Out of scope.** No relay protocol or router changes, no reuse of the relay's own SPA serving, no new web client or fork of the desktop UI, and no cleanup of the 11k-line bridge beyond the narrow adapter seams. ### Open upstream defect An intermittent `TypeError: Cannot read properties of null (reading 'trim')` has been reported near channel creation and is not reproduced deterministically. The string-contract correction in item 6 is interface hardening, not a diagnosis: a bounded instrumented reproduction run is the gate for calling anything a crash fix, and a clean run leaves the intermittent cause open rather than closed.
Sign in to join this conversation.
No description provided.