Commit Graph

6 Commits

Author SHA1 Message Date
egutierrez c142b3a025 feat(uniweb): room history client (fetchHistory + BusClient.history)
NATS delivers live only, so reloading the page lost a room's history. Add the
client half of the new history endpoint:

- ControlPlane.fetchHistory(roomID, limit): signed GET /rooms/{id}/history?limit=N,
  decoding each base64-std frame to the raw bytes the live subscription delivers.
- BusClient.history(roomID, limit): opens each replayed frame (verify + decrypt)
  exactly like subscribe, dropping any that fail, oldest -> newest.
- Extract BusClient.openFrame as the shared envelope-opening core for subscribe
  and history (no duplication; subscribe behavior unchanged).
- ulidTime(id): decode the ms-epoch a ULID encodes in its first 10 Crockford
  chars (inverse of newULID), so a frame's timestamp comes from its id (the wire
  carries none). Covered by ulid.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 19:39:57 +02:00
egutierrez 5e9bf4e777 feat(uniweb): readable handle instead of endpoint id in messages
Resolve a message sender's endpoint id to a human handle using a new
control-plane directory endpoint.

- ControlPlane.fetchDirectory(): signed GET /api/directory, mapped to
  DirectoryEntry { signPub, endpoint, handle, role }. The server's endpoint
  matches endpointID(signPub) byte for byte.
- busService keeps an endpoint -> handle Map, loaded once after a session
  opens and refreshed after createRoom (where the ACL is already refreshed).
  Exposes a pure displayName(endpoint) resolver: handle when known, the
  session user's own handle for their messages, short id fallback otherwise.
- Resilience: loadDirectory never throws. A missing endpoint (404 on older
  clusters) or a transient error leaves the map empty and the UI falls back to
  the short id, so the chat keeps working exactly as before.
- ChatPanel renders displayName(msg.sender) in the message header and derives
  the avatar initials from the handle; the raw endpoint stays in a title
  tooltip for debugging.
- types: Message.sender comment updated (this is the "phase 2" readable name).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:27:15 +02:00
egutierrez 103a7f2f05 feat: persistent session (no re-unlock on reload) + reconnect ACL after createRoom
Session persistence (web/src/session.ts): the unlocked wallet identity is kept
across reloads so an F5 no longer forces a password re-unlock. By default it lives
in sessionStorage (survives F5, cleared with the tab); with 'keep me signed in' it
lives in localStorage (survives closing the browser) bounded by a 30-day absolute
TTL and a 12-hour inactivity auto-lock. logout clears it; activity (send/createRoom)
refreshes the idle timer. No cookie is ever used — the private key never travels to
any server. WalletLogin gains the 'keep me signed in' checkbox; Recover/Join keep
the session by default (recovering/creating on a device implies it is yours).
App.tsx restores the session on mount before falling back to the unlock screen.

ACL reconnect: a room created while connected was not in the NATS per-subject ACL
grant (subjects are frozen at connect time), so its first messages silently did not
deliver until a re-login. WsNatsTransport gains reconnect(); BusClient.refresh()
calls it; busService.createRoom reconnects after creating so the new room is usable
immediately. Bumps uniweb to 0.4.0.
2026-06-14 13:58:06 +02:00
agent 3f52167b04 feat: browser-native client — wire SPA to the SDK, delete the Go gateway
Phase 2 of issue 0001. uniweb becomes a pure frontend (web/ only), like
unibus_android: the SPA talks directly to the bus and the Go gateway is gone.

- busService.ts: the new data layer over the bus SDK, replacing the old api module.
  It holds the user's wallet identity and a connected BusClient IN THE BROWSER and
  opens the session locally — the private key is never sent anywhere (closes the
  gateway-era hole where the browser POSTed its private key to /api/session).
- Wire account/App/ChatShell/ChatPanel/WalletLogin/Recover/Join to busService;
  subscribeRoom replaces the SSE streamRoom; ApiError -> SessionError.
- SDK: ControlPlane.createRoom + listMemberRooms, and fetchRoom mapped to the real
  control-plane wire shape (snake_case, no id) — all verified by the live round-trip.
- Delete cmd/webgw, go.mod, go.sum, src/api.ts and the orphan operator Login. uniweb
  now has zero Go and no dependency on unibus as a module.
- vite: drop the /api proxy, dev server on 5173 to match the bus CORS allowlist; add
  vite-env typings. app.md: lang ts, no uses_functions, e2e_checks are now web-only.
  Bump 0.3.0.

Onboarding by token is now admin-side (the bus has no self-register endpoint; the
gateway only mocked it). tsc + pnpm build + 19/19 unit green.
2026-06-14 11:39:06 +02:00
agent 2960b0984a feat(bus): createRoom + control-plane shape fixes, verified by live E2E round-trip
Validating the SDK against the real cluster surfaced the control-plane wire shapes:
the room/policy JSON is snake_case (sign_msgs) and GET /rooms/{id} omits the id.
Fix ControlPlane.fetchRoom to map the wire shape to the SDK Room type, and add
ControlPlane.createRoom (mint a room key, seal it to the owner via sealed box, POST
/rooms) so a browser peer can own an encrypted room.

The live smoke now does a full end-to-end round-trip against the 3-node cluster:
create an encrypted+signed room, connect over nats.ws, publish, and receive the
SDK's own message decrypted with the signature verified. Verified 2026-06-14:
room 01KV2Q…, plaintext round-tripped intact. The whole seal/sign/open happens in
the client; the private key never leaves it.

Exclude *.test.ts from the app tsconfig so the Node-API integration test does not
break the production build (vitest transpiles tests independently). Issue 0001,
Phase 3.
2026-06-14 11:26:13 +02:00
agent b72976e06c feat(bus): complete TypeScript SDK — auth, room envelope, client, transport
Second half of the browser-native bus SDK (issue 0001, Phase 1), making uniweb a
peer of the bus in its own right (like unibus_android) without the Go gateway:

- busauth.ts: NATS user nkey from the Ed25519 key (base32 + crc16, no nkeys dep)
  and control-plane request signing (CanonicalRequest + X-Unibus-* headers).
- room.ts: Policy / Room types (ModeNATS, ModeMatrix).
- client.ts: the pure room ENVELOPE (sealRoomMessage/openRoomMessage — AEAD with
  the subject as AAD, Ed25519 sign, drop on verify/decrypt failure), a transport-
  agnostic BusClient, and a signed ControlPlane HTTP client (fetch room/key/members,
  open the sealed room key locally).
- wstransport.ts: concrete nats.ws WebSocket transport (validated E2E in Phase 3).
- index.ts: public SDK surface.

Parity pinned by vectors from unibus cmd/busvectors (extended with nkey + signed
control-request vectors): 19/19 green. The user's private key signs everything in
the browser and is never sent to any server. Bumps uniweb to 0.2.0.

Remaining for Phase 1 completion: the live nats.ws connection + control-plane,
which need a running unibus with the WebSocket listener — exercised in Phase 3.
2026-06-13 22:54:54 +02:00