managarten/apps
Till JS 6de01937cf feat(vault-client): phase 9 milestone 3 — recovery + zero-knowledge flows
Extends the browser-side vault client with five new methods that
mirror the server-side Phase 9 routes, plus a new
`awaiting-recovery-code` state that pauses the unlock mid-flow
when the server is in zero-knowledge mode.

VaultUnlockState gains a fourth variant
---------------------------------------
  | { status: 'awaiting-recovery-code' }

This is the state the client sits in between calling unlock()
(which received a recovery blob from GET /key) and the user typing
their recovery code into the UI. The settings page status badge
got updated to render this case as "🔑 Recovery-Code erforderlich".

New closure state inside createVaultClient
------------------------------------------
  - pendingRecoveryBlob: stash for the recovery wrap returned by
    GET /key in zero-knowledge mode. unlockWithRecoveryCode reads
    from here so the second round of input doesn't need a re-fetch.
  - cachedUnwrappedMkBytes: kept ONLY when the vault was unlocked
    via the recovery code path AND the user might want to disable
    zero-knowledge later (which needs to hand the MK back to the
    server for KEK re-wrapping). The standard unlock path leaves
    this null because the server already has the KEK wrap. Wiped
    on lock(), on disable success, and on any state transition
    that destroys the master key.

Modified existing methods
-------------------------
  - unlock(): branches on the response shape. If the server returns
    a recovery blob (zero-knowledge mode), stash it via
    awaitRecoveryCode() and return state='awaiting-recovery-code'.
    Otherwise unwrap as before. Same fork applies to the /init
    fallback path.
  - rotate(): if the server somehow returned a ZK shape (it should
    never — rotate is forbidden in ZK mode server-side), bail with
    a server error instead of silently misinterpreting bytes.
  - lock(): also clears pendingRecoveryBlob + wipes
    cachedUnwrappedMkBytes.

New methods (all wired into the returned VaultClient)
-----------------------------------------------------
  - setupRecoveryCode(): generates a fresh 32-byte recovery secret,
    derives the wrap key, re-fetches the active master key in
    extractable form, seals it, posts to /recovery-wrap, returns
    the formatted recovery code for the UI to display. Wipes both
    raw byte references after the seal. Caller is responsible for
    clearing the formatted string from memory once the user has
    confirmed they backed it up.

  - clearRecoveryCode(): DELETE /recovery-wrap. Server enforces the
    "not while ZK is active" rule.

  - enableZeroKnowledge(): POST /zero-knowledge { enable: true }.
    Maps RECOVERY_WRAP_MISSING server response to a clear "set up
    a recovery code first" client error.

  - disableZeroKnowledge(): POST /zero-knowledge { enable: false,
    masterKey: base64 }. Reads the cached MK bytes, base64-encodes,
    sends. Wipes the cache after success.

  - unlockWithRecoveryCode(code): completes the flow that started
    in unlock(). Parses the user-typed code (RecoveryCodeFormatError
    bubbles up if the shape is wrong), derives the wrap key, runs a
    single inline AES-GCM decrypt on the stashed blob (yields both
    the raw bytes for the cache AND a non-extractable runtime key
    for the provider), wipes raw bytes, transitions to 'unlocked'.

    Generic error message on failure ("wrong recovery code or
    corrupted vault") so an attacker can't distinguish wrong-code
    from tampered-blob. Stays in 'awaiting-recovery-code' on
    failure so the user can retry without a re-fetch.

Drive-by stale test fix
-----------------------
aes.test.ts had an assertion from Phase 1 that `tasks` and `events`
return null because they were on enabled:false. Phase 7.1 flipped
both tables on, so the assertion has been failing since that
commit. Replaced the test with a stable negative case
(non-existent table name) that doesn't shift with each rollout
phase.

Test results: 78/78 crypto tests pass after the fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:01:16 +02:00
..
api feat: rename ManaCore to Mana across entire codebase 2026-04-05 20:00:13 +02:00
calc/packages/shared chore: delete 25 web-archived directories, remove stale stubs, clean workspace config 2026-04-03 13:03:49 +02:00
calendar chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
cards chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
chat chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
citycorners chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
contacts chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
context chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
docs chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
guides chore: delete 25 web-archived directories, remove stale stubs, clean workspace config 2026-04-03 13:03:49 +02:00
inventar chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
mana feat(vault-client): phase 9 milestone 3 — recovery + zero-knowledge flows 2026-04-07 23:01:16 +02:00
manacore/apps/web/src/lib feat: rename ManaCore to Mana across entire codebase 2026-04-05 20:00:13 +02:00
manavoxel chore(workspace): unify vitest to ^4.1.2 across all packages 2026-04-07 13:58:29 +02:00
matrix chore(workspace): unify vitest to ^4.1.2 across all packages 2026-04-07 13:58:29 +02:00
memoro chore(workspace): unify vitest to ^4.1.2 across all packages 2026-04-07 13:58:29 +02:00
moodlit feat: rename ManaCore to Mana across entire codebase 2026-04-05 20:00:13 +02:00
mukke feat: rename ManaCore to Mana across entire codebase 2026-04-05 20:00:13 +02:00
news chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
nutriphi chore(workspace): unify vitest to ^4.1.2 across all packages 2026-04-07 13:58:29 +02:00
photos chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
picture chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
planta chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
presi chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
questions feat: rename ManaCore to Mana across entire codebase 2026-04-05 20:00:13 +02:00
skilltree chore: delete 25 web-archived directories, remove stale stubs, clean workspace config 2026-04-03 13:03:49 +02:00
storage chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
times chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
todo chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
traces feat: rename ManaCore to Mana across entire codebase 2026-04-05 20:00:13 +02:00
uload chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
zitare/packages/content chore: delete 25 web-archived directories, remove stale stubs, clean workspace config 2026-04-03 13:03:49 +02:00