feat(web): wallet join/recover/login (BIP39 seed identity)

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>
This commit is contained in:
2026-06-08 21:21:50 +02:00
parent 7d93d550d1
commit 4994ea1483
16 changed files with 1303 additions and 29 deletions
+36
View File
@@ -14,6 +14,15 @@ importers:
'@mantine/hooks':
specifier: ^9.3.0
version: 9.3.0(react@19.2.7)
'@noble/curves':
specifier: ^2.2.0
version: 2.2.0
'@noble/hashes':
specifier: ^2.2.0
version: 2.2.0
'@scure/bip39':
specifier: ^2.2.0
version: 2.2.0
'@tabler/icons-react':
specifier: ^3.36.0
version: 3.44.0(react@19.2.7)
@@ -339,6 +348,14 @@ packages:
peerDependencies:
react: ^19.2.0
'@noble/curves@2.2.0':
resolution: {integrity: sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==}
engines: {node: '>= 20.19.0'}
'@noble/hashes@2.2.0':
resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==}
engines: {node: '>= 20.19.0'}
'@rolldown/pluginutils@1.0.0-beta.27':
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
@@ -480,6 +497,12 @@ packages:
cpu: [x64]
os: [win32]
'@scure/base@2.2.0':
resolution: {integrity: sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==}
'@scure/bip39@2.2.0':
resolution: {integrity: sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A==}
'@tabler/icons-react@3.44.0':
resolution: {integrity: sha512-8+rvzBbVm/1Z3sG3x7GUNAaxIKxwgz8xaMhRs23nrCnMTKRFAhEC+82zAIFeAA0seXdrAGX5HFCkaLpGK2rVHg==}
peerDependencies:
@@ -1086,6 +1109,12 @@ snapshots:
dependencies:
react: 19.2.7
'@noble/curves@2.2.0':
dependencies:
'@noble/hashes': 2.2.0
'@noble/hashes@2.2.0': {}
'@rolldown/pluginutils@1.0.0-beta.27': {}
'@rollup/rollup-android-arm-eabi@4.61.1':
@@ -1163,6 +1192,13 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.61.1':
optional: true
'@scure/base@2.2.0': {}
'@scure/bip39@2.2.0':
dependencies:
'@noble/hashes': 2.2.0
'@scure/base': 2.2.0
'@tabler/icons-react@3.44.0(react@19.2.7)':
dependencies:
'@tabler/icons': 3.44.0