Brings the BIP-39 mnemonic surface (CLI + libs landed in 0058d9b /
b0cc1a7) into the chat app's user-facing account flow. Match the same
SHA-256 domain-tag derivation as Rust / Node / Python — a phrase
generated in the browser verifies against the spec vectors in
python/MNEMONIC-TEST-VECTORS.md byte-for-byte.
• New lib/mnemonic.ts: browser-native helpers (generateMnemonic12,
seedFromMnemonic, mnemonicFromSeed24, ed25519FromMnemonic,
generateIdentityWithMnemonic, isValidMnemonic). Uses @scure/bip39
(same lib as Node impl) + the same domain tag "kez-bip39-12-v1".
12-word phrases by default; restore accepts 24-word too for parity
with the CLI.
• lib/identity-store.ts: StoredIdentity gains optional
phrase_nonce + phrase_ciphertext, encrypted under the SAME
PBKDF2-derived key as the seed (fresh nonce — AES-GCM reuse is
fatal). unlockIdentity returns the phrase when present. New
hasStoredPhrase() helper distinguishes "phrase exists but not
accessible in this session" (biometric unlock) from "truly legacy
hex-only account".
• CreateAccount: generates via generateIdentityWithMnemonic. Step 2
now shows the 12 words in a numbered grid with a "copy all" button
and a real ack checkbox before continuing. Step indicator updated
to "2. Back up phrase".
• Restore: was previously a stub that always threw "v0.1 limitation".
Now actually works — accepts either a 12/24-word phrase OR a
legacy 64-char hex seed (auto-detected), looks up the handle via
/v1/by-primary, derives the seed, saves identity, unlocks, routes
to /welcome.
• Settings: "Reveal seed" → "Reveal phrase". Three-state output:
- phrase in session → show 12 words
- phrase stored but biometric session → tell user to passphrase-
unlock to reveal
- truly legacy → show hex seed with explanation
• Welcome (onboarding): "Back up your recovery seed" step renders the
phrase as a numbered grid when available, falls back to the hex
block with a "Legacy 64-char hex" caption for pre-mnemonic accounts.
Biometric unlock continues to surface only the seed (the phrase blob is
encrypted under the passphrase-derived key, not the PRF-derived key) —
documented in the Settings UX. Encrypting under PRF too is a v0.3
follow-up.
Backwards compatible: existing accounts (which have only the
seed-ciphertext) unlock fine; their phrase fields stay undefined; the
UI falls back to the hex flow throughout.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>