{"version":3,"sources":["../src/storage.ts"],"sourcesContent":["// Private Pool V2 — local key cache (IndexedDB).\n//\n// Caches `PrivatePoolKeyMaterial` so the user doesn't have to sign the\n// challenge message every time they reload `/app/private`. Two modes:\n//\n//   1. PLAINTEXT (default — opt-out via setupPin)\n//      The cache holds the key material as plain JSON in IndexedDB.\n//      Anyone with read access to the user's browser disk can recover\n//      all V2 keys (spending_sk, viewing_sk, nullifier_sk). The\n//      Settings UI labels this honestly: \"this device-only cache, click\n//      Reset cached keys before leaving a shared computer.\"\n//\n//   2. PIN-ENCRYPTED (opt-in via setupPin)\n//      The cache is AES-GCM encrypted with a key derived via PBKDF2\n//      (200k iterations) from a user-supplied PIN. Each session prompts\n//      for PIN once; the derived key sits in module-level memory until\n//      either (a) the configurable idle timeout elapses, (b) lockNow()\n//      is called, (c) the tab is closed/refreshed.\n//\n// Threat model coverage:\n//   - PLAINTEXT mode: protects against NOTHING beyond browser-storage\n//     isolation (per-origin sandbox). Honest about it.\n//   - PIN-ENCRYPTED mode: protects against stolen-laptop / compromised\n//     device, AS LONG AS the PIN has enough entropy and the idle\n//     timeout is short. PBKDF2 200k iters means a brute-force attempt\n//     on a stolen laptop costs ~50ms per guess (modern CPU) — a 6-digit\n//     numeric PIN falls in ~14 hours, an 8-char alphanumeric PIN takes\n//     ~25 years. We recommend ≥8 chars in the UI.\n//\n// Per ADR-006 amendment (\"silent re-derivation when cache missing\")\n// the wallet signature is always the recovery path: forgot PIN →\n// `clearCachedKeys` → next visit re-derives from a fresh wallet sig.\n// Cache loss is annoying, never funds-loss.\n\nimport { chacha20poly1305 } from '@noble/ciphers/chacha';\nimport { blake2b } from '@noble/hashes/blake2b';\nimport type { PrivatePoolKeyMaterial } from './keys';\n\nconst DB_NAME = 'relai-private-pool-v2'\n// v1 → v2: ACK_STORE for backup-acknowledgement gate.\n// v2 → v3: cache entries gain a `mode` discriminator (plaintext / pin-encrypted).\n//          Existing v1/v2 plaintext entries auto-upgrade on next read by\n//          re-saving as `{ mode: 'plaintext', ... }`.\nconst DB_VERSION = 2\nconst STORE = 'keys'\nconst ACK_STORE = 'backup-ack'\n\nconst CACHE_VERSION = 2  // bumped to 2 with mode-aware entries\nconst PBKDF2_ITERATIONS = 200_000  // ~50ms per guess on modern CPU\nconst PBKDF2_HASH = 'SHA-256'\nconst AES_KEY_BITS = 256\nconst AES_NAME = 'AES-GCM'\nconst SALT_LEN = 16\nconst IV_LEN = 12\n\n// localStorage keys for non-secret settings (lock interval, last-mode hint).\nconst LS_LOCK_INTERVAL_PREFIX = 'relai-pp-v2:lock-interval:'\n\nconst TEXT_ENCODER = new TextEncoder()\nconst TEXT_DECODER = new TextDecoder()\n\n// ── IndexedDB plumbing ──────────────────────────────────────────────────\n\nfunction isAvailable(): boolean {\n  return typeof globalThis !== 'undefined' && typeof globalThis.indexedDB !== 'undefined'\n}\n\nfunction openDb(): Promise<IDBDatabase> {\n  return new Promise((resolve, reject) => {\n    if (!isAvailable()) return reject(new Error('IndexedDB not available'))\n    const req = indexedDB.open(DB_NAME, DB_VERSION)\n    req.onupgradeneeded = () => {\n      const db = req.result\n      // Both stores created on every upgrade — `createObjectStore` is\n      // idempotent in practice via the `contains` guard, and tagging\n      // both here means a fresh-install user gets v2 schema on first\n      // open without needing the lazy-upgrade dance below.\n      if (!db.objectStoreNames.contains(STORE)) {\n        db.createObjectStore(STORE)\n      }\n      if (!db.objectStoreNames.contains(ACK_STORE)) {\n        db.createObjectStore(ACK_STORE)\n      }\n    }\n    req.onsuccess = () => resolve(req.result)\n    req.onerror = () => reject(req.error || new Error('IndexedDB open failed'))\n  })\n}\n\nasync function withStore<T>(mode: IDBTransactionMode, fn: (store: IDBObjectStore) => Promise<T> | T): Promise<T> {\n  const db = await openDb()\n  return new Promise<T>((resolve, reject) => {\n    const tx = db.transaction(STORE, mode)\n    const store = tx.objectStore(STORE)\n    Promise.resolve(fn(store)).then((v) => {\n      tx.oncomplete = () => resolve(v)\n      tx.onerror = () => reject(tx.error || new Error('IndexedDB tx error'))\n    }).catch((err) => {\n      try { tx.abort() } catch {}\n      reject(err)\n    })\n  })\n}\n\nfunction idbGet<T>(store: IDBObjectStore, key: IDBValidKey): Promise<T | undefined> {\n  return new Promise((resolve, reject) => {\n    const r = store.get(key)\n    r.onsuccess = () => resolve(r.result as T | undefined)\n    r.onerror = () => reject(r.error)\n  })\n}\n\nfunction idbPut(store: IDBObjectStore, key: IDBValidKey, value: unknown): Promise<void> {\n  return new Promise((resolve, reject) => {\n    const r = store.put(value, key)\n    r.onsuccess = () => resolve()\n    r.onerror = () => reject(r.error)\n  })\n}\n\nfunction idbDelete(store: IDBObjectStore, key: IDBValidKey): Promise<void> {\n  return new Promise((resolve, reject) => {\n    const r = store.delete(key)\n    r.onsuccess = () => resolve()\n    r.onerror = () => reject(r.error)\n  })\n}\n\n// ── Encryption (deterministic from wallet pubkey) ───────────────────────\n\nfunction deriveCacheKey(walletPubkey: string): Uint8Array {\n  // 32-byte key, blake2b-derived from the wallet pubkey + cache version\n  // tag. NOT a security boundary — see threat model in module doc.\n  return blake2b(TEXT_ENCODER.encode(`relai-pool-v2-cache:v${CACHE_VERSION}:${walletPubkey}`), { dkLen: 32 })\n}\n\nfunction deriveCacheNonce(walletPubkey: string): Uint8Array {\n  // 12-byte deterministic nonce (acceptable: same key never used twice\n  // for different plaintexts because each upsert overwrites the\n  // ciphertext at rest, so \"nonce reuse with same plaintext\" never\n  // happens at observable boundary).\n  return blake2b(TEXT_ENCODER.encode(`relai-pool-v2-cache-nonce:v${CACHE_VERSION}:${walletPubkey}`), { dkLen: 12 })\n}\n\n// ── Serialisation ───────────────────────────────────────────────────────\n\nfunction bytesToHex(bytes: Uint8Array): string {\n  return Buffer.from(bytes).toString('hex')\n}\nfunction hexToBytes(hex: string): Uint8Array {\n  return Uint8Array.from(Buffer.from(hex, 'hex'))\n}\n\ntype SerializedKeyMaterial = {\n  v: number\n  rootSk: string\n  spendingSk: string\n  viewingSk: string\n  nullifierSk: string\n  spendingSkField: string\n  nullifierSkField: string\n  spendingPkField: string\n  viewingPk: string\n  paymentAddress: string\n}\n\nfunction serialize(km: PrivatePoolKeyMaterial): SerializedKeyMaterial {\n  return {\n    v: CACHE_VERSION,\n    rootSk: bytesToHex(km.rootSk),\n    spendingSk: bytesToHex(km.spendingSk),\n    viewingSk: bytesToHex(km.viewingSk),\n    nullifierSk: bytesToHex(km.nullifierSk),\n    spendingSkField: km.spendingSkField.toString(10),\n    nullifierSkField: km.nullifierSkField.toString(10),\n    spendingPkField: km.spendingPkField.toString(10),\n    viewingPk: bytesToHex(km.viewingPk),\n    paymentAddress: km.paymentAddress,\n  }\n}\n\nfunction deserialize(s: SerializedKeyMaterial): PrivatePoolKeyMaterial | null {\n  if (!s || s.v !== CACHE_VERSION) return null\n  try {\n    return {\n      rootSk: hexToBytes(s.rootSk),\n      spendingSk: hexToBytes(s.spendingSk),\n      viewingSk: hexToBytes(s.viewingSk),\n      nullifierSk: hexToBytes(s.nullifierSk),\n      spendingSkField: BigInt(s.spendingSkField),\n      nullifierSkField: BigInt(s.nullifierSkField),\n      spendingPkField: BigInt(s.spendingPkField),\n      viewingPk: hexToBytes(s.viewingPk),\n      paymentAddress: s.paymentAddress,\n    }\n  } catch {\n    return null\n  }\n}\n\n// ── Mode-aware entry shape ──────────────────────────────────────────────\n\ntype PlaintextEntry = {\n  mode: 'plaintext'\n  v: number\n  data: SerializedKeyMaterial\n  stored_at: number\n}\n\ntype PinEncryptedEntry = {\n  mode: 'pin-encrypted'\n  v: number\n  ciphertext_b64: string\n  salt_b64: string\n  iv_b64: string\n  iter: number\n  stored_at: number\n}\n\n// Backward-compat: v1 entries from the previous \"deterministic-key\n// chacha20poly1305\" scheme get treated as legacy and decrypted via\n// the same path on first load, then re-saved as plaintext mode.\ntype LegacyV1Entry = { v: 1; ciphertext: string; stored_at: number }\n\ntype CacheEntry = PlaintextEntry | PinEncryptedEntry | LegacyV1Entry\n\n// ── In-memory unlock state (PIN mode only) ──────────────────────────────\n//\n// When a PIN-protected cache is unlocked, the derived AES key sits in\n// module-level memory until either:\n//   - lockNow() is called explicitly\n//   - the configurable idle interval elapses (each operation refreshes it)\n//   - the tab is closed / page refreshes (memory wiped naturally)\n// Refresh = re-prompt PIN.\n\nlet unlockedKey: CryptoKey | null = null\nlet unlockedForWallet: string | null = null\nlet lastActivityAt = 0\n\n/// Lock interval is per-wallet, persisted in localStorage. Defaults to\n/// 8h (one working day) — matches what most users will actively work\n/// on V2 in a single session. Configurable via `setLockInterval`.\n///\n/// Allowed presets (ms): 15min / 1h / 8h / 24h / 0 (never).\n/// Use `LOCK_INTERVAL_PRESETS` for the runtime list.\nexport type LockIntervalMs = number\n\nexport const LOCK_INTERVAL_PRESETS = [\n  15 * 60 * 1000,        // 15 min\n  60 * 60 * 1000,        // 1h\n  8 * 60 * 60 * 1000,    // 8h (default)\n  24 * 60 * 60 * 1000,   // 24h\n  0,                      // never\n] as const\nconst DEFAULT_LOCK_INTERVAL_MS = 8 * 60 * 60 * 1000\n\nexport function getLockInterval(walletPubkey: string): number {\n  if (typeof localStorage === 'undefined') return DEFAULT_LOCK_INTERVAL_MS\n  const v = localStorage.getItem(LS_LOCK_INTERVAL_PREFIX + walletPubkey)\n  if (!v) return DEFAULT_LOCK_INTERVAL_MS\n  const n = Number(v)\n  return Number.isFinite(n) && LOCK_INTERVAL_PRESETS.includes(n) ? n : DEFAULT_LOCK_INTERVAL_MS\n}\n\nexport function setLockInterval(walletPubkey: string, intervalMs: number): void {\n  if (typeof localStorage === 'undefined') return\n  if (!LOCK_INTERVAL_PRESETS.includes(intervalMs)) {\n    throw new Error(`setLockInterval: ${intervalMs} is not one of the allowed presets (15m, 1h, 8h, 24h, never=0)`)\n  }\n  localStorage.setItem(LS_LOCK_INTERVAL_PREFIX + walletPubkey, String(intervalMs))\n}\n\n/// Manually wipe the in-memory unlock key. Next access prompts PIN.\nexport function lockNow(): void {\n  unlockedKey = null\n  unlockedForWallet = null\n  lastActivityAt = 0\n}\n\n/// Returns true when the in-memory unlock key is fresh enough to\n/// satisfy the configured idle window. `false` means PIN is needed.\nfunction isUnlockedFor(walletPubkey: string): boolean {\n  if (!unlockedKey || unlockedForWallet !== walletPubkey) return false\n  const interval = getLockInterval(walletPubkey)\n  if (interval === 0) {\n    // \"Never\" — only manual lockNow / page refresh wipes.\n    return true\n  }\n  if (Date.now() - lastActivityAt > interval) {\n    lockNow()\n    return false\n  }\n  return true\n}\n\nfunction bumpActivity(): void {\n  lastActivityAt = Date.now()\n}\n\n// ── Web Crypto helpers (Subtle API) ─────────────────────────────────────\n\nfunction subtle(): SubtleCrypto {\n  if (typeof globalThis === 'undefined' || !globalThis.crypto?.subtle) {\n    throw new Error('Web Crypto SubtleCrypto not available — PIN protection requires a modern browser')\n  }\n  return globalThis.crypto.subtle\n}\n\nfunction randomBytes(len: number): Uint8Array {\n  const out = new Uint8Array(len)\n  globalThis.crypto.getRandomValues(out)\n  return out\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n  if (typeof Buffer !== 'undefined') return Buffer.from(bytes).toString('base64')\n  let s = ''\n  for (const b of bytes) s += String.fromCharCode(b)\n  return btoa(s)\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n  if (typeof Buffer !== 'undefined') return Uint8Array.from(Buffer.from(b64, 'base64'))\n  const s = atob(b64)\n  const out = new Uint8Array(s.length)\n  for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i)\n  return out\n}\n\nasync function deriveAesKeyFromPin(\n  pin: string,\n  salt: Uint8Array,\n  iterations: number,\n): Promise<CryptoKey> {\n  const pinBytes = TEXT_ENCODER.encode(pin)\n  const baseKey = await subtle().importKey(\n    'raw',\n    pinBytes,\n    { name: 'PBKDF2' },\n    false,\n    ['deriveKey'],\n  )\n  return subtle().deriveKey(\n    {\n      name: 'PBKDF2',\n      salt,\n      iterations,\n      hash: PBKDF2_HASH,\n    },\n    baseKey,\n    { name: AES_NAME, length: AES_KEY_BITS },\n    false,  // not extractable\n    ['encrypt', 'decrypt'],\n  )\n}\n\nasync function aesEncrypt(key: CryptoKey, iv: Uint8Array, plaintext: Uint8Array): Promise<Uint8Array> {\n  const ct = await subtle().encrypt({ name: AES_NAME, iv }, key, plaintext)\n  return new Uint8Array(ct)\n}\n\nasync function aesDecrypt(key: CryptoKey, iv: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array> {\n  const pt = await subtle().decrypt({ name: AES_NAME, iv }, key, ciphertext)\n  return new Uint8Array(pt)\n}\n\n// ── Public surface ──────────────────────────────────────────────────────\n\n/// Save the derived key tree to local cache. Mode is determined by\n/// what's already stored:\n///   - PIN-protected entry exists AND in-memory key is unlocked → re-encrypt\n///   - PIN-protected entry exists BUT locked → THROW (caller must unlock first)\n///   - plaintext entry OR no entry → write plaintext\n///\n/// Plaintext is the default for new caches. Users opt in to PIN\n/// protection via `setupPin()`.\nexport async function saveCachedKeys(walletPubkey: string, keys: PrivatePoolKeyMaterial): Promise<void> {\n  if (!isAvailable()) return\n  try {\n    const existing = await readRawEntry(walletPubkey)\n    if (existing && 'mode' in existing && existing.mode === 'pin-encrypted') {\n      if (!isUnlockedFor(walletPubkey)) {\n        throw new Error('saveCachedKeys: cache is PIN-protected and locked — call unlockWithPin first')\n      }\n      const salt = base64ToBytes(existing.salt_b64)\n      const iv = randomBytes(IV_LEN)\n      const plaintext = TEXT_ENCODER.encode(JSON.stringify(serialize(keys)))\n      const ciphertext = await aesEncrypt(unlockedKey!, iv, plaintext)\n      bumpActivity()\n      await writeEntry(walletPubkey, {\n        mode: 'pin-encrypted',\n        v: CACHE_VERSION,\n        ciphertext_b64: bytesToBase64(ciphertext),\n        salt_b64: bytesToBase64(salt),\n        iv_b64: bytesToBase64(iv),\n        iter: PBKDF2_ITERATIONS,\n        stored_at: Date.now(),\n      })\n      return\n    }\n    // Plaintext path — default for new caches and for entries that\n    // were never PIN-protected.\n    await writeEntry(walletPubkey, {\n      mode: 'plaintext',\n      v: CACHE_VERSION,\n      data: serialize(keys),\n      stored_at: Date.now(),\n    })\n  } catch (err) {\n    // Cache failure must NEVER block deposit/spend (ADR-006). Log and\n    // move on; caller falls back to re-derivation from wallet sig.\n    // eslint-disable-next-line no-console\n    console.warn('[private-pool-v2 cache] saveCachedKeys failed:', err)\n  }\n}\n\n/// Try to load cached keys.\n///\n/// Return semantics:\n///   - `null` if NO cache entry, OR cache is PIN-protected and locked\n///     (caller checks `isPinProtected` to distinguish), OR decryption\n///     fails (caller should re-derive from wallet sig).\n///   - `PrivatePoolKeyMaterial` if plaintext OR PIN-unlocked.\n///\n/// To avoid awkward null-checks in callers, see `loadCachedKeysWithStatus`\n/// which returns a richer enum.\nexport async function loadCachedKeys(walletPubkey: string): Promise<PrivatePoolKeyMaterial | null> {\n  const status = await loadCachedKeysWithStatus(walletPubkey)\n  return status.kind === 'ok' ? status.keys : null\n}\n\n/// Rich-status variant — UI uses this to decide whether to show a\n/// PIN prompt, an empty-cache \"sign to derive\" CTA, or just render\n/// the balance.\nexport type LoadCacheStatus =\n  | { kind: 'ok'; keys: PrivatePoolKeyMaterial }\n  | { kind: 'no-cache' }\n  | { kind: 'pin-required' }\n  | { kind: 'corrupted' }\n\nexport async function loadCachedKeysWithStatus(walletPubkey: string): Promise<LoadCacheStatus> {\n  if (!isAvailable()) return { kind: 'no-cache' }\n  try {\n    const entry = await readRawEntry(walletPubkey)\n    if (!entry) return { kind: 'no-cache' }\n\n    // Legacy v1 (pre-mode-aware, deterministic-key chacha20poly1305).\n    // Decrypt with the old scheme, then re-save as plaintext so future\n    // loads take the fast path.\n    if (!('mode' in entry) && entry.v === 1) {\n      try {\n        const key = deriveCacheKey(walletPubkey)\n        const nonce = deriveCacheNonce(walletPubkey)\n        const cipher = chacha20poly1305(key, nonce)\n        const plaintext = cipher.decrypt(hexToBytes(entry.ciphertext))\n        const parsed = JSON.parse(TEXT_DECODER.decode(plaintext)) as SerializedKeyMaterial\n        const keys = deserialize(parsed)\n        if (!keys) return { kind: 'corrupted' }\n        // Lazy upgrade to plaintext mode (honest about it).\n        await writeEntry(walletPubkey, {\n          mode: 'plaintext',\n          v: CACHE_VERSION,\n          data: parsed,\n          stored_at: Date.now(),\n        })\n        return { kind: 'ok', keys }\n      } catch {\n        return { kind: 'corrupted' }\n      }\n    }\n\n    if ('mode' in entry && entry.mode === 'plaintext') {\n      const keys = deserialize(entry.data)\n      return keys ? { kind: 'ok', keys } : { kind: 'corrupted' }\n    }\n\n    if ('mode' in entry && entry.mode === 'pin-encrypted') {\n      if (!isUnlockedFor(walletPubkey)) {\n        return { kind: 'pin-required' }\n      }\n      try {\n        const iv = base64ToBytes(entry.iv_b64)\n        const ciphertext = base64ToBytes(entry.ciphertext_b64)\n        const plaintext = await aesDecrypt(unlockedKey!, iv, ciphertext)\n        const parsed = JSON.parse(TEXT_DECODER.decode(plaintext)) as SerializedKeyMaterial\n        const keys = deserialize(parsed)\n        bumpActivity()\n        return keys ? { kind: 'ok', keys } : { kind: 'corrupted' }\n      } catch {\n        // Decryption failure with a present key usually means corruption.\n        // If the key is wrong (wrong PIN), unlockWithPin would have failed\n        // already — but be defensive here.\n        return { kind: 'corrupted' }\n      }\n    }\n\n    return { kind: 'corrupted' }\n  } catch (err) {\n    console.warn('[private-pool-v2 cache] loadCachedKeysWithStatus failed:', err)\n    return { kind: 'corrupted' }\n  }\n}\n\n/// True if the cache for this wallet is PIN-protected (regardless of\n/// current unlock state). UI uses this to render the right CTA.\nexport async function isPinProtected(walletPubkey: string): Promise<boolean> {\n  if (!isAvailable()) return false\n  try {\n    const entry = await readRawEntry(walletPubkey)\n    return !!(entry && 'mode' in entry && entry.mode === 'pin-encrypted')\n  } catch {\n    return false\n  }\n}\n\n/// True if the in-memory unlock state is fresh (PIN was entered\n/// recently and the idle timer hasn't expired).\nexport function isUnlocked(walletPubkey: string): boolean {\n  return isUnlockedFor(walletPubkey)\n}\n\n/// Convert an existing plaintext cache (or freshly-derived in-memory\n/// keys) into a PIN-protected cache. After success, subsequent loads\n/// require `unlockWithPin` and the in-memory key is set so the current\n/// session can keep using `saveCachedKeys` without re-prompting.\n///\n/// Throws if:\n///   - No cache exists yet (caller must save plaintext first via the\n///     normal flow, then call setupPin)\n///   - PIN protection is already enabled (use `changePin` instead)\n///   - PIN length < 4 chars\nexport async function setupPin(args: {\n  walletPubkey: string\n  pin: string\n  intervalMs?: number\n}): Promise<void> {\n  const { walletPubkey, pin } = args\n  if (!isAvailable()) throw new Error('IndexedDB not available')\n  if (!pin || pin.length < 4) {\n    throw new Error('PIN must be at least 4 characters')\n  }\n  const existing = await readRawEntry(walletPubkey)\n  if (!existing) throw new Error('setupPin: no cache to protect (sign in first)')\n  if ('mode' in existing && existing.mode === 'pin-encrypted') {\n    throw new Error('setupPin: PIN already enabled — use changePin instead')\n  }\n  // Pull plaintext keys out of whatever the current entry is.\n  let keys: PrivatePoolKeyMaterial | null = null\n  if (!('mode' in existing) && existing.v === 1) {\n    // Legacy entry — decrypt via the old deterministic scheme.\n    const key = deriveCacheKey(walletPubkey)\n    const nonce = deriveCacheNonce(walletPubkey)\n    const cipher = chacha20poly1305(key, nonce)\n    const plaintext = cipher.decrypt(hexToBytes(existing.ciphertext))\n    const parsed = JSON.parse(TEXT_DECODER.decode(plaintext)) as SerializedKeyMaterial\n    keys = deserialize(parsed)\n  } else if ('mode' in existing && existing.mode === 'plaintext') {\n    keys = deserialize(existing.data)\n  }\n  if (!keys) throw new Error('setupPin: failed to read existing cache')\n\n  const salt = randomBytes(SALT_LEN)\n  const iv = randomBytes(IV_LEN)\n  const aesKey = await deriveAesKeyFromPin(pin, salt, PBKDF2_ITERATIONS)\n  const plaintext = TEXT_ENCODER.encode(JSON.stringify(serialize(keys)))\n  const ciphertext = await aesEncrypt(aesKey, iv, plaintext)\n  await writeEntry(walletPubkey, {\n    mode: 'pin-encrypted',\n    v: CACHE_VERSION,\n    ciphertext_b64: bytesToBase64(ciphertext),\n    salt_b64: bytesToBase64(salt),\n    iv_b64: bytesToBase64(iv),\n    iter: PBKDF2_ITERATIONS,\n    stored_at: Date.now(),\n  })\n  // Persist the chosen interval (or default) and put the unlock state\n  // in memory so the user doesn't have to re-enter PIN immediately.\n  if (typeof args.intervalMs === 'number') {\n    setLockInterval(walletPubkey, args.intervalMs)\n  }\n  unlockedKey = aesKey\n  unlockedForWallet = walletPubkey\n  bumpActivity()\n}\n\n/// Verify PIN and put the derived AES key in memory for the configured\n/// idle window. Returns true on success, false on wrong PIN. Never\n/// throws on auth failure; callers branch on the boolean.\n///\n/// Implementation: derive the AES key from PIN+salt+iter, attempt to\n/// decrypt the entry. Decryption success = correct PIN.\nexport async function unlockWithPin(args: {\n  walletPubkey: string\n  pin: string\n}): Promise<boolean> {\n  const { walletPubkey, pin } = args\n  if (!isAvailable()) return false\n  const entry = await readRawEntry(walletPubkey)\n  if (!entry || !('mode' in entry) || entry.mode !== 'pin-encrypted') return false\n  try {\n    const salt = base64ToBytes(entry.salt_b64)\n    const iv = base64ToBytes(entry.iv_b64)\n    const ciphertext = base64ToBytes(entry.ciphertext_b64)\n    const aesKey = await deriveAesKeyFromPin(pin, salt, entry.iter || PBKDF2_ITERATIONS)\n    // Trial decrypt — if PIN is wrong, AES-GCM throws on tag mismatch.\n    await aesDecrypt(aesKey, iv, ciphertext)\n    unlockedKey = aesKey\n    unlockedForWallet = walletPubkey\n    bumpActivity()\n    return true\n  } catch {\n    return false\n  }\n}\n\n/// Remove PIN protection — decrypts the cache back to plaintext mode.\n/// Requires the current PIN to authorise. Idempotent on plaintext caches.\nexport async function removePin(args: {\n  walletPubkey: string\n  pin: string\n}): Promise<void> {\n  const ok = await unlockWithPin(args)\n  if (!ok) throw new Error('removePin: PIN incorrect')\n  // After unlock succeeded, decrypt and re-save as plaintext.\n  const status = await loadCachedKeysWithStatus(args.walletPubkey)\n  if (status.kind !== 'ok') throw new Error('removePin: failed to decrypt cache after unlock')\n  await writeEntry(args.walletPubkey, {\n    mode: 'plaintext',\n    v: CACHE_VERSION,\n    data: serialize(status.keys),\n    stored_at: Date.now(),\n  })\n  lockNow()\n}\n\n/// Change PIN — validates old PIN, then re-encrypts with new PIN.\n/// New idle interval is OPTIONAL; if omitted the existing interval is preserved.\nexport async function changePin(args: {\n  walletPubkey: string\n  oldPin: string\n  newPin: string\n  intervalMs?: number\n}): Promise<void> {\n  if (!args.newPin || args.newPin.length < 4) {\n    throw new Error('PIN must be at least 4 characters')\n  }\n  // Verify old PIN by unlocking + reading the keys.\n  const ok = await unlockWithPin({ walletPubkey: args.walletPubkey, pin: args.oldPin })\n  if (!ok) throw new Error('changePin: old PIN incorrect')\n  const status = await loadCachedKeysWithStatus(args.walletPubkey)\n  if (status.kind !== 'ok') throw new Error('changePin: failed to decrypt cache')\n  // Wipe old in-memory state, then re-setup with the new PIN.\n  lockNow()\n  const salt = randomBytes(SALT_LEN)\n  const iv = randomBytes(IV_LEN)\n  const aesKey = await deriveAesKeyFromPin(args.newPin, salt, PBKDF2_ITERATIONS)\n  const plaintext = TEXT_ENCODER.encode(JSON.stringify(serialize(status.keys)))\n  const ciphertext = await aesEncrypt(aesKey, iv, plaintext)\n  await writeEntry(args.walletPubkey, {\n    mode: 'pin-encrypted',\n    v: CACHE_VERSION,\n    ciphertext_b64: bytesToBase64(ciphertext),\n    salt_b64: bytesToBase64(salt),\n    iv_b64: bytesToBase64(iv),\n    iter: PBKDF2_ITERATIONS,\n    stored_at: Date.now(),\n  })\n  if (typeof args.intervalMs === 'number') {\n    setLockInterval(args.walletPubkey, args.intervalMs)\n  }\n  unlockedKey = aesKey\n  unlockedForWallet = args.walletPubkey\n  bumpActivity()\n}\n\n// ── Internal IndexedDB read/write wrappers (mode-aware) ────────────────\n\nasync function readRawEntry(walletPubkey: string): Promise<CacheEntry | null> {\n  const raw = await withStore('readonly', (store) => idbGet<CacheEntry>(store, walletPubkey))\n  return raw ?? null\n}\n\nasync function writeEntry(walletPubkey: string, entry: PlaintextEntry | PinEncryptedEntry): Promise<void> {\n  await withStore('readwrite', (store) => idbPut(store, walletPubkey, entry))\n}\n\n// ── Backup / restore (privacy-wallet pattern) ─────────────────────────────────────\n//\n// Exports the full key tree as a portable JSON file. User keeps it\n// safely (cloud drive, USB, encrypted vault — their choice). Restoring\n// on a fresh device (or after IndexedDB wipe) bypasses the wallet\n// signature challenge — keys flow directly into the in-memory state +\n// IndexedDB cache.\n//\n// Format is plaintext JSON (no extra password) — same as other privacy wallets. The user\n// is responsible for protecting the file. We include a clear warning in\n// the UI before download. Adding password-based encryption is an obvious\n// future enhancement (Faza 6 backlog) — out of scope for the first ship.\n\nconst BACKUP_FORMAT_NAME = 'relai-private-pool-v2-backup'\nconst BACKUP_FORMAT_VERSION = 1\n\nexport type BackupFile = {\n  format: typeof BACKUP_FORMAT_NAME\n  version: number\n  walletPubkey: string\n  paymentAddress: string\n  createdAt: string\n  /// Plaintext serialization of `PrivatePoolKeyMaterial`. Whoever holds\n  /// this file can spend the user's private balance — treat like a seed.\n  keys: SerializedKeyMaterial\n  /// Optional metadata to make UX clearer when restoring (e.g. \"this\n  /// backup is from May 2026, you have 5 notes worth ~3 USDC\").\n  meta?: {\n    note?: string\n  }\n}\n\n/// Build a portable backup object for the given wallet + keys. Returns\n/// the JSON string ready for download. Caller wraps in a Blob and\n/// triggers the file save via DOM (no IO from this module).\nexport function buildBackupFileJson(args: {\n  walletPubkey: string\n  keys: PrivatePoolKeyMaterial\n  note?: string\n}): string {\n  const file: BackupFile = {\n    format: BACKUP_FORMAT_NAME,\n    version: BACKUP_FORMAT_VERSION,\n    walletPubkey: args.walletPubkey,\n    paymentAddress: args.keys.paymentAddress,\n    createdAt: new Date().toISOString(),\n    keys: serialize(args.keys),\n    meta: args.note ? { note: args.note } : undefined,\n  }\n  return JSON.stringify(file, null, 2)\n}\n\n/// Parse a backup JSON file back into `PrivatePoolKeyMaterial`. Returns\n/// `{ ok: false, error }` on malformed input, `{ ok: true, keys, walletPubkey }`\n/// otherwise. Caller validates `walletPubkey` against the connected\n/// wallet (mismatch = warn or block — depends on UX choice).\nexport function parseBackupFileJson(raw: string): {\n  ok: true\n  keys: PrivatePoolKeyMaterial\n  walletPubkey: string\n  paymentAddress: string\n  createdAt: string\n} | { ok: false; error: string } {\n  let parsed: unknown\n  try {\n    parsed = JSON.parse(raw)\n  } catch (e: unknown) {\n    return { ok: false, error: `not a valid JSON file: ${(e as Error)?.message ?? String(e)}` }\n  }\n  if (typeof parsed !== 'object' || parsed === null) {\n    return { ok: false, error: 'backup file must be a JSON object' }\n  }\n  const file = parsed as Partial<BackupFile>\n  if (file.format !== BACKUP_FORMAT_NAME) {\n    return { ok: false, error: `unrecognised format: \"${file.format}\" (expected \"${BACKUP_FORMAT_NAME}\")` }\n  }\n  if (typeof file.version !== 'number' || file.version > BACKUP_FORMAT_VERSION) {\n    return { ok: false, error: `unsupported backup version ${file.version} (max ${BACKUP_FORMAT_VERSION})` }\n  }\n  if (!file.keys || typeof file.keys !== 'object') {\n    return { ok: false, error: 'backup file is missing the keys block' }\n  }\n  if (!file.walletPubkey || typeof file.walletPubkey !== 'string') {\n    return { ok: false, error: 'backup file is missing walletPubkey' }\n  }\n  const km = deserialize(file.keys as SerializedKeyMaterial)\n  if (!km) {\n    return { ok: false, error: 'backup keys block failed deserialisation (corrupt or wrong version)' }\n  }\n  return {\n    ok: true,\n    keys: km,\n    walletPubkey: file.walletPubkey,\n    paymentAddress: file.paymentAddress ?? km.paymentAddress,\n    createdAt: file.createdAt ?? '',\n  }\n}\n\n// ── Backup-acknowledgement gate (per-wallet flag in IndexedDB) ──────────\n//\n// \"Has this user explicitly acknowledged the backup risk?\" — flag stored\n// in the same IndexedDB so the gate doesn't reappear after a refresh.\n// Cleared by `clearCachedKeys` (Reset cached keys) so the gate re-shows\n// after a deliberate wipe.\n\nasync function withAckStore<T>(mode: IDBTransactionMode, fn: (store: IDBObjectStore) => Promise<T> | T): Promise<T> {\n  // ACK_STORE is created in `openDb`'s onupgradeneeded at DB v2, so by\n  // the time we get here the store always exists. Same shape as\n  // `withStore` for the keys store — symmetric.\n  const db = await openDb()\n  return new Promise<T>((resolve, reject) => {\n    const tx = db.transaction(ACK_STORE, mode)\n    const store = tx.objectStore(ACK_STORE)\n    Promise.resolve(fn(store)).then((v) => {\n      tx.oncomplete = () => resolve(v)\n      tx.onerror = () => reject(tx.error || new Error('IDB tx error'))\n    }).catch((err) => { try { tx.abort() } catch {}; reject(err) })\n  })\n}\n\n/// Persist that the user has acknowledged the backup risk for this\n/// wallet. Returns `true` on success, `false` on storage failure (in\n/// which case we just don't gate — better UX than blocking on a\n/// flaky IndexedDB).\nexport async function setBackupAcknowledged(walletPubkey: string): Promise<boolean> {\n  if (!isAvailable()) return false\n  try {\n    await withAckStore('readwrite', (store) => idbPut(store, walletPubkey, {\n      acknowledged_at: new Date().toISOString(),\n    }))\n    return true\n  } catch (err) {\n    // eslint-disable-next-line no-console\n    console.warn('[private-pool-v2 cache] setBackupAcknowledged failed:', err)\n    return false\n  }\n}\n\n/// Has this wallet acknowledged the backup risk in a previous session?\n/// Default: `false`. Returns `false` on storage failure too — that\n/// means the gate shows again, not that we silently let users through.\nexport async function hasBackupAcknowledged(walletPubkey: string): Promise<boolean> {\n  if (!isAvailable()) return false\n  try {\n    const entry = await withAckStore('readonly', (store) =>\n      idbGet<{ acknowledged_at: string }>(store, walletPubkey),\n    )\n    return !!entry?.acknowledged_at\n  } catch {\n    return false\n  }\n}\n\n/// Wipe the cached keys for a given wallet (invoked from a \"Reset keys\"\n/// button in Settings, or after a security incident, or \"forgot PIN\"\n/// recovery flow). The wallet signature can always be re-derive the\n/// keys; the cache is just a UX nicety.\n///\n/// Also clears: in-memory unlock state, lock-interval preference,\n/// backup acknowledgement (so user re-confirms the backup gate after\n/// Reset, never silently-skipping).\nexport async function clearCachedKeys(walletPubkey: string): Promise<void> {\n  if (!isAvailable()) return\n  try {\n    await withStore('readwrite', (store) => idbDelete(store, walletPubkey))\n    await withAckStore('readwrite', (store) => idbDelete(store, walletPubkey))\n    // Wipe the in-memory unlock state too — if the user clicks Reset\n    // while a PIN-protected cache was unlocked, the next access should\n    // see \"no cache\" not \"unlocked-key-without-cache\".\n    if (unlockedForWallet === walletPubkey) lockNow()\n    // Lock-interval preference is per-wallet; reset to default by\n    // removing the localStorage key.\n    if (typeof localStorage !== 'undefined') {\n      localStorage.removeItem(LS_LOCK_INTERVAL_PREFIX + walletPubkey)\n    }\n  } catch (err) {\n    // eslint-disable-next-line no-console\n    console.warn('[private-pool-v2 cache] clearCachedKeys failed:', err)\n  }\n}\n"],"mappings":"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","names":["plaintext"]}