Add the gateway backend for the wallet onboarding flow so each browser
session carries its OWN bus identity instead of sharing the single
operator client.
- POST /api/session (session.go): the browser hands its full wallet
keypair (unlocked from the local encrypted key, over TLS) and the
gateway spins up a dedicated bus client that acts AS that user. The
private key lives only in process memory for the life of the session
and is dropped on logout/shutdown. identityFromHex enforces the exact
key sizes (sign_pub 32, sign_priv 64, kex_pub 32, kex_priv 32) that
match cs.Identity on the Go side.
- POST /api/register (register.go): unauthenticated onboarding gated by
a one-shot invite token. Validates the two PUBLIC key halves, then
either consumes a configured --mock-tokens invite (local testing) or
proxies to the bus POST /register (--register-url, bus >= 0.12.0). The
handle/role come from the invite, never from the client.
- server.go: sessions move from a token->time map to a sessionStore of
per-user *session records; auth() now resolves the session and passes
its gateway to each handler. The legacy operator passphrase login
(POST /api/login) is kept, bound to the shared operator gateway.
- main.go: build a busTemplate config that wallet sessions clone with
their own Identity; wire --register-url / --mock-tokens.
- webgw_test.go: identity-size validation, hex-key validation, mock
token parsing, and single-use register (201 then 409) using a fixed
browser-derived wallet vector.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add cmd/webgw: a single Go binary that holds the operator's bus identity,
connects to the bus as a real authenticated peer (pkg/client), and exposes a
small REST + SSE API the browser consumes. The browser never signs, never
speaks NATS, and never sees a private key.
Endpoints (all under /api, gated by a session cookie except login):
POST /api/login unlock a session with the operator passphrase
POST /api/logout
GET /api/me operator identity the gateway acts as
GET /api/rooms ListMyRooms
POST /api/rooms CreateRoom (default policy: encrypted+persisted+signed)
POST /api/rooms/{id}/join Join (fetch room key)
POST /api/rooms/{id}/send Publish (sealed + signed by the peer)
GET /api/rooms/{id}/stream SSE of decrypted frames (history then live)
Design notes:
- One fan-out hub per room: a single bus subscription is multiplexed to N SSE
clients, avoiding the per-(room,endpoint) durable-consumer contention that
multiple Subscribe calls would cause.
- Posture seam mirrors unibus_admin/clientcheck: empty --ca = plaintext dev,
non-empty = TLS+nkey on both planes; RefreshSession after a membership change
only under the secured (ACL) posture.
- Identity loaded from `pass` or a 0600 file, held only in memory.
- Session auth: passphrase compared in constant time; opaque HttpOnly cookie
so EventSource (which cannot set headers) can authenticate the stream.
TRUST MODEL: room content stays end-to-end encrypted on the bus. The gateway
reads plaintext only because it acts AS the operator's client — a legitimate
member of each room holding the room key. The per-browser wallet (WebCrypto)
that moves decryption into the browser is phase 2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>