Añade un control "Nueva room" en el header del sidebar (botón "+") y CTAs
en los estados vacíos del sidebar y del panel. Abren un modal que pide el
asunto, crea la room con bus.createRoom contra el bus real, la inserta en
la lista (dedup por id, sin recargar) y la activa.
- NewRoomModal: modal de Mantine con loading, manejo de SessionError/Error
en español, crear con Enter o botón, formulario limpio en cada apertura.
- ChatShell: estado del modal con useDisclosure, handleRoomCreated centraliza
inserción + selección, empty state del panel rediseñado con botón crear.
- Sidebar: prop onNewRoom, botón "+" con tooltip, empty state distingue
"sin resultados" de "sin rooms" (con CTA crear primera room).
No toca la capa de datos (busService.ts ni web/src/bus/): usa los métodos de
bus tal como están. Verificado end-to-end contra el cluster real: crear room
desde la UI, enviar mensaje y verlo aparecer por la suscripción.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SPA is now served behind a same-origin reverse proxy (Caddy) that
fronts both bus planes, so the data layer reaches them through the page's
own origin instead of a hardcoded cluster node IP. This removes CORS
entirely and hides the cluster IPs behind the proxy.
- BUS_HTTP falls back to the relative path /api (the signed HTTPS control
plane), resolved against the page origin by ControlPlane's fetch.
- BUS_WS falls back to a wss URL derived from window.location (same host,
scheme mirroring https/http, path /nats), since a browser WebSocket needs
an absolute ws(s) URL.
- The raw self-signed-IP fallback (https://51.91.100.142:8470, wss://...:8480)
is gone. The VITE_BUS_HTTP / VITE_BUS_WS build-time overrides remain for a
dev setup that points straight at a cluster node.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
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.
Extend the live smoke: when a registered identity is present (its sign_pub added
to the bus allowlist), assert the SAME SDK that a fresh identity gets rejected with
is now ACCEPTED — control plane no longer 401s and the nats.ws data-plane
connection succeeds. Verified 2026-06-14 against the 3-node cluster: random
identity -> 401 / 'authorization violation'; registered identity -> 403 'not a
member of this room' / connected=true. The allowlist is the only gate; the SDK
speaks both planes correctly end-to-end. Issue 0001, Phase 3.
A network smoke (self-skips unless BUS_HTTP/BUS_WS are set) that points the SDK
at the live 3-node cluster. With a fresh, unregistered identity it asserts BOTH
planes reject with an AUTHORIZATION error (not a signature/protocol error),
proving the SDK speaks the control plane (signed canonical request) and the data
plane (nats.ws + nkey) correctly end-to-end. Verified 2026-06-14 against datardos:
control-plane 401 'identity not authorized', data-plane 'authorization violation'.
Issue 0001, Phase 3.
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.
First half of the browser-native bus SDK (issue 0001, Phase 1):
- crypto.ts: Ed25519 sign/verify (@noble), ChaCha20-Poly1305 AEAD (@noble),
endpoint id (sha256+base64url), and the anonymous sealed box for room-key
distribution. The sealed-box nonce is BLAKE2b-192 over ephPub||recipientPub,
matching Go's nacl/box.SealAnonymous (NOT SHA-512) so a Go-sealed key opens here.
- frame.ts: the Frame wire format, reproducing Go encoding/json byte-for-byte —
struct field order, omitempty rules, base64-std byte fields, and the default
HTML escaping (<, >, &, U+2028/U+2029) — plus sign/verify over canonical bytes.
vectors.test.ts checks all of it against the golden vectors generated by unibus
cmd/busvectors. 11/11 green: endpoint id, Ed25519 (incl. frame signature),
AEAD seal+open, sealed box open + round-trip, and frame signing-bytes + wire
marshal. This pins cross-language interop with Go/Kotlin peers.
Adds @noble/ciphers, tweetnacl (runtime) and vitest (dev).
Golden vectors generated by unibus cmd/busvectors: the contract the TypeScript
bus SDK must match byte-for-byte (Ed25519 sign, ChaCha20-Poly1305 AEAD, sealed
box, Frame wire format). Issue 0001, Phase 0.
Extracted from unibus v0.13.0: the chat SPA (web/, React+Mantine, per-user
BIP39 wallet) and the web gateway (cmd/webgw, REST+SSE) that acts as a bus
peer for the browser. Consumes unibus as a Go module via replace => ../unibus,
keeping its own replace fn-registry for the cybersecurity primitives.
go build/vet/test and pnpm build green in the new location.