The SPA (web/) and the web gateway (cmd/webgw) move to a dedicated app
projects/message_bus/apps/uniweb (its own Gitea sub-repo). unibus is now
strictly the bus plane: membership/keys, the client library and demo peers.
uniweb consumes unibus as a Go module via replace => ../unibus.
No capability lost; same SPA and gateway, in their own service folder.
go build/vet/test green after extraction.
Add the device-local wallet onboarding to the SPA. The user's identity
is derived deterministically from a 12-word BIP39 mnemonic and lives on
the device; the browser never signs, never talks NATS, and never sends
the seed to the server.
Wallet layer (web/src/wallet/):
- derive.ts: deterministic identity from a mnemonic. seed = BIP39 seed,
then HKDF-SHA256 domain-separated into an Ed25519 signing key
(info "unibus-sign-v1") and an X25519 key-exchange key (info
"unibus-kex-v1"). The same mnemonic always yields the same sign_pub,
which is what makes recovery possible without admin intervention. The
four halves match cs.Identity on the Go side exactly.
- bip39.ts: thin wrappers over @scure/bip39 (generate, validate,
normalize) so the checksum logic stays in the audited library.
- crypto.ts: at-rest encryption of the private key with WebCrypto only
(PBKDF2-SHA256 210k iters -> AES-256-GCM). The password never leaves
the device and only protects the local key copy.
- store.ts: IndexedDB persistence of the encrypted identity (private key
encrypted; public halves + handle in the clear for display).
- account.ts: saveAndOpen / unlockAndOpen / localIdentity compose the
primitives with the gateway session API.
Screens:
- Welcome: choose invite link or recover-with-seed on an empty device.
- Join: generate seed, show it once behind an acknowledge gate, confirm
3 random words, set a local password, register the PUBLIC key with the
bus via the invite token, then open the session.
- Recover: paste the 12 words, validate, show the reconstructed sign_pub,
set a new local password, open the session. No register (the identity
is already in the allowlist).
- WalletLogin: unlock the device's stored identity with the password.
- AuthShell: shared card/header for all pre-chat screens.
- App.tsx: route between join / welcome / login / recover / chat based on
the invite link, a live gateway session, and any stored identity.
api.ts/types.ts: add register() and session() against the gateway
contract; vite dev server on :5183.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the mock data source with a real data layer that talks to the webgw
gateway over REST + SSE. The UI components keep their look and props; only
where the data comes from changed.
- src/api.ts: the single repository layer. fetch wrappers (same-origin cookie)
for login/logout/me and rooms list/create/join/send, plus streamRoom() which
opens an EventSource and yields each decrypted message. Wire->UI mappers
(roomFromWire, messageFromWire).
- src/types.ts: add the gateway wire shapes (MeInfo, RoomWire, MsgWire) next to
the existing UI types.
- App.tsx: probe /api/me on mount to resume an existing session; otherwise show
Login. Logout calls the gateway.
- Login.tsx: the password field now unlocks the gateway session (operator
passphrase); shows a basic error and a loading state. Wallet-per-browser is
phase 2.
- ChatShell.tsx: load rooms from /api/rooms with loading / empty / error states;
same Flex layout.
- ChatPanel.tsx: stream messages over SSE for the active room (dedup by id),
composer sends through the gateway; no optimistic insert (the peer's own echo
returns over SSE with the real frame id).
- vite.config.ts: dev proxy /api (REST + SSE) -> the gateway on :8481.
mock.ts is left untouched (no longer imported) to avoid churn with the parallel
styling work on master.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SPA React 19 + Vite + Mantine v9 en modo oscuro (acento índigo), datos mock para
iterar el diseño antes de cablear el gateway. Login con identidad + contraseña
(la contraseña desbloqueará la identidad Ed25519 cifrada en el dispositivo).
Sidebar: avatar de usuario, buscador (rooms/usuarios/mensajes) y lista de rooms
con candado E2E / hash cleartext / badges de no leídos. Panel de chat estilo
Element (avatar+nombre+hora+texto) con composer interactivo.
Limpieza de los frontends de prueba (SPA React, app Kotlin, gateway playground,
binding gomobile) tras la fase de exploración. El bus (cmd/membershipd + pkg/*)
queda intacto y verde. Empezamos un frontend web nuevo desde cero, construido
de forma incremental. Todo lo borrado permanece en el historial git por si hay
que recuperar algo.
Cliente web sobre el gateway (REST + SSE). El navegador no habla NATS ni
cripto: el peer Go del gateway lo hace.
- Pantalla de conexión: gateway URL + identidad (persistidas en localStorage).
- Navbar: crear room (con toggle de cifrado E2E), unirse por id, lista de rooms.
- Centro: mensajes en vivo por SSE, burbujas con autor y hora, composer.
- Lateral: miembros (rol owner), invitar por peer conectado, expulsar (owner).
- Mantine v9 (createTheme + MantineProvider), @tabler/icons-react, layout con
AppShell/Stack/Group; sin Tailwind ni CSS manual. React 19 (peer dep de v9).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>