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>