@tgoliveira/vault-core 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/AGENTS.md +77 -0
  2. package/API_REFERENCE.md +196 -26
  3. package/ARCHITECTURE.md +5 -0
  4. package/CHANGELOG.md +51 -0
  5. package/MIGRATION_FROM_LIQSENSE.md +3 -1
  6. package/PASSKEY_PRF_ENVELOPES.md +2 -1
  7. package/PASSWORD_ENVELOPES.md +3 -1
  8. package/README.md +42 -2
  9. package/RECOVERY_PHRASE.md +2 -1
  10. package/SECURITY.md +22 -2
  11. package/dist/browser.d.ts +12 -1
  12. package/dist/browser.d.ts.map +1 -1
  13. package/dist/browser.js +46 -18
  14. package/dist/browser.js.map +1 -1
  15. package/dist/envelopes/passkey-prf.d.ts +3 -3
  16. package/dist/envelopes/passkey-prf.d.ts.map +1 -1
  17. package/dist/envelopes/passkey-prf.js +7 -5
  18. package/dist/envelopes/passkey-prf.js.map +1 -1
  19. package/dist/envelopes/password.d.ts +1 -1
  20. package/dist/envelopes/password.d.ts.map +1 -1
  21. package/dist/envelopes/password.js +3 -1
  22. package/dist/envelopes/password.js.map +1 -1
  23. package/dist/envelopes/recovery.d.ts +2 -2
  24. package/dist/envelopes/recovery.d.ts.map +1 -1
  25. package/dist/envelopes/recovery.js +15 -6
  26. package/dist/envelopes/recovery.js.map +1 -1
  27. package/dist/kdf/argon2id.d.ts.map +1 -1
  28. package/dist/kdf/argon2id.js +15 -2
  29. package/dist/kdf/argon2id.js.map +1 -1
  30. package/dist/kdf/params.d.ts +24 -0
  31. package/dist/kdf/params.d.ts.map +1 -1
  32. package/dist/kdf/params.js +22 -0
  33. package/dist/kdf/params.js.map +1 -1
  34. package/dist/payload/encrypted-payload.d.ts +4 -2
  35. package/dist/payload/encrypted-payload.d.ts.map +1 -1
  36. package/dist/payload/encrypted-payload.js +3 -1
  37. package/dist/payload/encrypted-payload.js.map +1 -1
  38. package/dist/react/session/use-vault-session.d.ts +1 -0
  39. package/dist/react/session/use-vault-session.d.ts.map +1 -1
  40. package/dist/react/session/use-vault-session.js +7 -2
  41. package/dist/react/session/use-vault-session.js.map +1 -1
  42. package/dist/react/session/vault-session-provider.d.ts +2 -1
  43. package/dist/react/session/vault-session-provider.d.ts.map +1 -1
  44. package/dist/react/session/vault-session-provider.js +7 -2
  45. package/dist/react/session/vault-session-provider.js.map +1 -1
  46. package/dist/session/auto-lock.d.ts +2 -1
  47. package/dist/session/auto-lock.d.ts.map +1 -1
  48. package/dist/session/auto-lock.js +15 -1
  49. package/dist/session/auto-lock.js.map +1 -1
  50. package/dist/validation/aad-assert.d.ts +5 -3
  51. package/dist/validation/aad-assert.d.ts.map +1 -1
  52. package/dist/validation/aad-assert.js +15 -8
  53. package/dist/validation/aad-assert.js.map +1 -1
  54. package/dist/validation/plaintext-reject.d.ts.map +1 -1
  55. package/dist/validation/plaintext-reject.js +18 -4
  56. package/dist/validation/plaintext-reject.js.map +1 -1
  57. package/dist/validation/schemas.d.ts +148 -56
  58. package/dist/validation/schemas.d.ts.map +1 -1
  59. package/dist/validation/schemas.js +29 -10
  60. package/dist/validation/schemas.js.map +1 -1
  61. package/docs/ADOPTING_VAULT_CORE_IN_EXISTING_APPS.md +575 -0
  62. package/docs/IMPLEMENTATION_GUIDE.md +577 -0
  63. package/docs/README.md +30 -0
  64. package/docs/RELEASING.md +102 -0
  65. package/package.json +10 -3
@@ -0,0 +1,575 @@
1
+ # Adopting `@tgoliveira/vault-core` in Existing Apps
2
+
3
+ Guide for replacing a local, app-specific vault implementation with the reusable npm package [`@tgoliveira/vault-core`](https://www.npmjs.com/package/@tgoliveira/vault-core).
4
+
5
+ **Audience:** Future AI agents (Cursor and similar) performing incremental vault migrations without weakening security boundaries.
6
+
7
+ ---
8
+
9
+ ## Package name
10
+
11
+ | Name | Status |
12
+ | --- | --- |
13
+ | `@tgoliveira/vault-core` | **Official npm package name** (verified in `vault-core/package.json` and on npm) |
14
+ | `@tgoliveira/core-vault` | **Not published** — occasional shorthand; do not use in `package.json` imports |
15
+
16
+ Always install and import `@tgoliveira/vault-core`.
17
+
18
+ ---
19
+
20
+ ## 1. Purpose
21
+
22
+ This guide explains how to migrate an existing app from duplicated local vault crypto (AES-GCM helpers, Argon2id envelopes, recovery phrases, passkey PRF wrap/unwrap, session helpers) to `@tgoliveira/vault-core`.
23
+
24
+ The package provides **reusable vault primitives**, not product logic:
25
+
26
+ - User Vault Key (UVK) generation
27
+ - Password, recovery phrase, and passkey PRF envelopes
28
+ - Generic encrypted payload helpers (with profile-driven AAD)
29
+ - Recovery phrase utilities and recovery kit text helpers
30
+ - No-plaintext validation helpers
31
+ - Optional browser session helpers (`@tgoliveira/vault-core/browser`)
32
+ - Optional headless React session hooks (`@tgoliveira/vault-core/react`)
33
+
34
+ It does **not** replace your app’s note schemas, API routes, database layer, auth package, or UI.
35
+
36
+ ---
37
+
38
+ ## 2. When to use this guide
39
+
40
+ ### Good candidates
41
+
42
+ - The app has local AES-GCM encrypt/decrypt helpers
43
+ - The app has local password, recovery phrase, or passkey PRF envelopes
44
+ - The app generates BIP39 recovery phrases client-side
45
+ - The app stores encrypted user-private payloads in the database
46
+ - The app has an explicit client-side vault unlock flow
47
+ - Account authentication and vault unlock are already separate concerns
48
+ - The app has (or needs) no-plaintext guarantees on API/server boundaries
49
+
50
+ ### Not a good fit
51
+
52
+ - The app only needs account login (use `@tgoliveira/secure-auth` or your auth stack)
53
+ - The app does not encrypt user-private payloads on the client
54
+ - The app requires server-side plaintext processing of vault secrets
55
+ - The app cannot preserve existing encrypted vault records (no compatibility fixtures, no migration plan)
56
+ - You expect vault-core to own product payload schemas or Next.js routes
57
+
58
+ ---
59
+
60
+ ## 3. Package boundary
61
+
62
+ ### Belongs in `@tgoliveira/vault-core`
63
+
64
+ | Area | Examples |
65
+ | --- | --- |
66
+ | Crypto primitives | AES-GCM, canonical AAD byte candidates, base64url encoding |
67
+ | Key generation | `createUserVaultKey()` |
68
+ | KDF | Argon2id with default params (`memory: 65536`, `iterations: 3`, `parallelism: 1`) |
69
+ | Envelopes | `createPasswordEnvelope`, `unlockWithPasswordEnvelope`, recovery and passkey PRF equivalents |
70
+ | Generic payload | `encryptVaultPayload` / `decryptVaultPayload` (profile + scope) |
71
+ | Recovery | `createRecoveryPhrase`, normalization, confirmation helpers |
72
+ | Recovery kit | `createRecoveryKitText`, browser download/print helpers |
73
+ | Validation | `assertNoVaultPlaintextFields`, AAD assert helpers |
74
+ | Errors | `VaultCoreError`, `RecoveryPhraseConfirmationError`, passkey PRF errors |
75
+ | Browser session | `@tgoliveira/vault-core/browser` — in-memory UVK, auto-lock, PRF salt helper |
76
+ | React (optional) | `@tgoliveira/vault-core/react` — headless session/status hooks |
77
+ | Testing | `@tgoliveira/vault-core/testing` — sentinels, scan helpers |
78
+
79
+ ### Stays in the consuming app
80
+
81
+ | Area | Examples |
82
+ | --- | --- |
83
+ | Database | Drizzle/Prisma schemas, repositories, migrations |
84
+ | API routes | Vault init/setup/status, envelope persistence, note CRUD |
85
+ | Auth/session checks | `@tgoliveira/secure-auth`, NextAuth, middleware |
86
+ | App payload schemas | Note metadata/body, vault index shape, vault settings plaintext |
87
+ | Domain encryption | Per-note keys, category/tag crypto, app-specific AAD `field` values |
88
+ | UI | Setup wizard, unlock panels, recovery UX copy |
89
+ | Business rules | Unlock behavior, moderation, sharing |
90
+ | WebAuthn ceremony | Account passkey login (separate from PRF vault unlock) |
91
+ | Payload migrations | v1 → v2 index format, legacy recovery code support |
92
+
93
+ **Rule:** `vault-core` must not import or know your product domain (letters, notes, subscriptions, etc.).
94
+
95
+ ---
96
+
97
+ ## 4. Migration mental model
98
+
99
+ Treat this as a **strangler migration**, not a rewrite.
100
+
101
+ ```text
102
+ 1. Add package dependency
103
+ 2. Freeze current app crypto profile/constants
104
+ 3. Add compatibility fixtures from the existing local vault
105
+ 4. Replace pure crypto helpers first
106
+ 5. Replace envelope helpers
107
+ 6. Replace recovery phrase helpers
108
+ 7. Replace generic encrypted payload helpers (where field types align)
109
+ 8. Keep app payload schemas local
110
+ 9. Keep UI / API / routes mostly unchanged
111
+ 10. Remove duplicated local code only after tests pass
112
+ ```
113
+
114
+ During transition, keep **thin compatibility wrappers** in the app (e.g. `wrapVaultKeyForPassword` → delegates to `createPasswordEnvelope` with a frozen profile). This reduces churn and preserves import paths for tests.
115
+
116
+ ---
117
+
118
+ ## 5. App crypto profile
119
+
120
+ Every consuming app needs an explicit **`VaultCryptoProfile`** passed into vault-core envelope and payload helpers.
121
+
122
+ ```ts
123
+ import type { VaultCryptoProfile } from "@tgoliveira/vault-core";
124
+
125
+ export const APP_VAULT_PROFILE: VaultCryptoProfile = {
126
+ cryptoVersion: "vault-v1",
127
+ aadContextVault: "my-app:vault:v1",
128
+ aadContextEnvelope: "my-app:vault-envelope:v1",
129
+ };
130
+ ```
131
+
132
+ ### Rules
133
+
134
+ - **Profile preserves AAD compatibility** for *new* encryption using vault-core (`context` is embedded in the stored `aad` object).
135
+ - **Do not change** profile strings after production data exists unless you have a deliberate breaking migration.
136
+ - **Existing apps:** derive profile values from the current implementation. If legacy records have **no** `context` in stored `aad`, use the low-level `decryptField` compatibility path in an explicit migration, validate all available AAD fields, and re-encrypt with the configured context. High-level decrypt and unlock APIs intentionally reject missing contexts.
137
+ - **New apps:** pick stable, product-neutral strings before first production release.
138
+ - **Passkey PRF salt prefix** (if used) is app-specific and lives beside the profile, e.g. `buildPrfSaltBytes("my-app-passkey-prf-v1:", userId)` from `@tgoliveira/vault-core/browser`.
139
+
140
+ ---
141
+
142
+ ## 6. Import mapping
143
+
144
+ Use the **actual** vault-core API (see `API_REFERENCE.md`). Deprecated aliases exist for LiqSense-era names.
145
+
146
+ | Local concept | `@tgoliveira/vault-core` replacement |
147
+ | --- | --- |
148
+ | `generateUserVaultKey` / `generateAesKey` for UVK | `createUserVaultKey()` |
149
+ | `encryptField` for generic vault payload | `encryptVaultPayload(payload, key, scope, profile)` / `decryptVaultPayload(encrypted, key, expectedScope, profile)` **or** exported low-level `encryptField` / `decryptField` |
150
+ | `wrapVaultKeyForPassword` | `createPasswordEnvelope(vaultKey, password, scope, profile)` |
151
+ | `unwrapVaultKeyFromPassword` | `unlockWithPasswordEnvelope(password, envelope, expectedScope, profile)` |
152
+ | `generateRecoveryPhrase` | `createRecoveryPhrase({ wordCount: 12 \| 24 })` |
153
+ | `wrapVaultKeyForRecoveryPhrase` | `createRecoveryEnvelope(vaultKey, phrase, scope, profile)` |
154
+ | `unwrapVaultKeyFromRecoveryPhrase` | `unlockWithRecoveryEnvelope(phrase, envelope, expectedScope, profile)` |
155
+ | `wrapVaultKeyForPasskey` | `createPasskeyPrfEnvelope(vaultKey, prfOutput, scope, profile)` |
156
+ | `unwrapVaultKeyFromPasskey` | `unlockWithPasskeyPrfEnvelope(envelope, prfOutput, expectedScope, profile)` |
157
+ | `extractPasskeyPrfOutput` | `@tgoliveira/vault-core/browser` (also re-exported from passkey module) |
158
+ | Recovery kit text | `createRecoveryKitText` / `buildRecoveryKitContent` |
159
+ | Plaintext guard | `assertNoVaultPlaintextFields`, `validateNoPlaintextLeak` |
160
+ | In-memory session / auto-lock | `@tgoliveira/vault-core/browser` |
161
+ | React vault status hooks | `@tgoliveira/vault-core/react` |
162
+
163
+ ### Entry points
164
+
165
+ | Import path | Use when |
166
+ | --- | --- |
167
+ | `@tgoliveira/vault-core` | Core crypto, envelopes, validation (safe for shared isomorphic code that does not touch `window`) |
168
+ | `@tgoliveira/vault-core/browser` | Session, activity-aware auto-lock, PRF salt, recovery kit DOM, storage namespace inspection |
169
+ | `@tgoliveira/vault-core/testing` | Test sentinels and plaintext scans |
170
+ | `@tgoliveira/vault-core/react` | Optional React hooks (peer: `react >= 18`) |
171
+
172
+ ---
173
+
174
+ ## 7. Compatibility fixture strategy
175
+
176
+ Create fixtures **before** replacing call sites.
177
+
178
+ ### Required fixture categories
179
+
180
+ | Fixture | Proves |
181
+ | --- | --- |
182
+ | Existing password envelope unlock | UVK unwrap + Argon2id metadata round-trip |
183
+ | Existing recovery phrase envelope unlock | BIP39 + envelope |
184
+ | 12-word recovery unlock | If app supports 12-word phrases |
185
+ | 24-word recovery unlock | If app supports 24-word phrases |
186
+ | Legacy recovery **code** unlock | **Only if app still has vault-v1 `recovery_code` envelopes** |
187
+ | Encrypted vault index decrypt | App-specific blob still decrypts |
188
+ | Encrypted vault settings decrypt | App-specific blob still decrypts |
189
+ | Encrypted note payload decrypt | Stays app-local crypto; optional cross-check |
190
+ | Wrong AAD failure | Tampered `userId` / `resourceId` / `field` rejected |
191
+ | Wrong key failure | Bad password/phrase fails closed |
192
+ | Tampered ciphertext failure | Bit flip fails decrypt |
193
+ | No plaintext persistence sample | Server payload / storage scan tests |
194
+
195
+ ### Random IV warning
196
+
197
+ AES-GCM uses a random 12-byte IV per encryption. **Do not** expect byte-identical ciphertext for newly generated blobs.
198
+
199
+ - Fixtures should store **real ciphertext from production or a captured setup** and assert **decryptability** and UVK equality.
200
+ - For deterministic **envelope** tests only, pass a fixed Argon2 salt into `createPasswordEnvelope` / `createRecoveryEnvelope` (see `vault-core/src/testing/fixtures/liqsense-compat.ts`).
201
+ - **Never** weaken production `crypto.getRandomValues` to make tests pass.
202
+
203
+ ---
204
+
205
+ ## 8. Step-by-step migration checklist
206
+
207
+ ```text
208
+ [ ] Inspect local vault implementation (rg vault|encrypt|recovery|Argon2|PRF)
209
+ [ ] Classify files: core crypto | envelopes | recovery | session | app schema | API | UI | auth
210
+ [ ] Add @tgoliveira/vault-core to package.json (semver, not file: link for production)
211
+ [ ] Define APP_VAULT_PROFILE (frozen; match legacy AAD behavior via fixtures)
212
+ [ ] Define PRF salt prefix constant if passkey PRF is used
213
+ [ ] Capture golden encrypted blobs from staging or test setup into JSON fixtures
214
+ [ ] Port tests to unlock/decrypt fixtures with vault-core APIs
215
+ [ ] Add thin re-export wrappers in app (preserve old function names temporarily)
216
+ [ ] Migrate call sites incrementally (crypto → envelopes → recovery → session)
217
+ [ ] Keep app EncryptedPayload schema / extended AAD field enums local if needed
218
+ [ ] Keep API request/response contracts unchanged
219
+ [ ] Keep database columns unchanged unless a proven migration exists
220
+ [ ] Run app unit + security tests after each phase
221
+ [ ] Run vault-core boundary tests pattern in app (no server importing browser entry wrongly)
222
+ [ ] Delete duplicated local files only when all tests green
223
+ [ ] Update app docs and remove stale crypto ADRs that duplicate vault-core
224
+ ```
225
+
226
+ ---
227
+
228
+ ## 9. Security checklist
229
+
230
+ Agents must verify **all** items before declaring migration complete:
231
+
232
+ ```text
233
+ [ ] No vault password sent to server
234
+ [ ] No recovery phrase sent to server
235
+ [ ] No User Vault Key sent to server
236
+ [ ] No raw PRF output sent to server
237
+ [ ] No decrypted vault payload sent to server
238
+ [ ] No decrypted vault state in localStorage
239
+ [ ] No decrypted vault state in IndexedDB (note drafts may use encrypted or opaque storage — verify app policy)
240
+ [ ] No vault secrets in logs
241
+ [ ] No Math.random for cryptographic values
242
+ [ ] AAD bindings preserved (userId, resourceId, field; legacy key order still decrypts)
243
+ [ ] KDF params preserved for existing envelopes (Argon2id memory/iterations/parallelism/salt)
244
+ [ ] Existing encrypted payloads still decrypt
245
+ [ ] Account login does not unlock vault
246
+ [ ] Account password reset does not unlock vault
247
+ [ ] TOTP / OAuth login do not unlock vault
248
+ [ ] Account passkey login and passkey PRF vault unlock remain separate code paths
249
+ [ ] WebAuthn signatures are not used as encryption keys (only PRF extension output)
250
+ ```
251
+
252
+ ---
253
+
254
+ ## 10. Test plan for consuming apps
255
+
256
+ ### Functional
257
+
258
+ ```text
259
+ [ ] Vault setup creates UVK and envelopes client-side
260
+ [ ] Password unlock restores UVK
261
+ [ ] Recovery phrase unlock restores UVK (12- and 24-word if supported)
262
+ [ ] Legacy recovery code unlock still works (if vault-v1 data retained)
263
+ [ ] Passkey PRF unlock works when supported
264
+ [ ] Vault settings round-trip encrypted
265
+ [ ] Vault index round-trip encrypted
266
+ [ ] Note encrypt/decrypt still works
267
+ [ ] Vault lock clears in-memory UVK and decrypted note caches
268
+ [ ] Auto-lock fires after inactivity
269
+ [ ] Server rejects plaintext vault secrets on all vault API routes
270
+ ```
271
+
272
+ ### Boundary / static
273
+
274
+ ```text
275
+ [ ] Account session alone does not set session UVK (see vault-session-account-separation pattern)
276
+ [ ] App no longer duplicates argon2id/aes-gcm/envelope implementations covered by vault-core
277
+ [ ] @tgoliveira/vault-core/browser not imported from server-only modules
278
+ [ ] App-specific payload schemas remain in app repo, not in vault-core
279
+ [ ] API integration tests still pass with encrypted blobs only in POST bodies
280
+ ```
281
+
282
+ Use `@tgoliveira/vault-core/testing` sentinels (`SENTINEL_VAULT_PASSWORD`, etc.) in security tests.
283
+
284
+ ---
285
+
286
+ ## 11. Case study: [letter-to-god](https://github.com/tgoliveira11/letter-to-god)
287
+
288
+ **Inspected:** Local clone at `/Users/thiago.oliveira/Projects/letter-to-god` (npm package name `letters-to-god`, product UI branded **SelahKeep**).
289
+
290
+ **Current state:** Full local vault stack under `src/lib/crypto-client/*`. **No** `@tgoliveira/vault-core` dependency yet. Account auth via `@tgoliveira/secure-auth`.
291
+
292
+ ---
293
+
294
+ ### 11.1 Current local vault inventory
295
+
296
+ | File / path | Current role | Migration classification | Recommended action |
297
+ | --- | --- | --- | --- |
298
+ | `src/lib/crypto-client/aes-gcm.ts` | AES-GCM encrypt/decrypt, UVK import/export | Reusable crypto helper | Replace with vault-core `encryptField` / `decryptField` + profile **or** keep thin wrapper for app-specific `field` enum |
299
+ | `src/lib/crypto-client/aad.ts` | Canonical AAD string + legacy byte candidates | Reusable crypto helper | Replace with vault-core `canonicalAadString` / `aadByteCandidates` behavior; note legacy LTG records omit `context` |
300
+ | `src/lib/crypto-client/aad-verify.ts` | Client-side AAD binding checks before decrypt | App-specific integration | **Keep local** (binds app field names) |
301
+ | `src/lib/crypto-client/encoding.ts` | base64url, string bytes | Reusable crypto helper | Remove after vault-core import |
302
+ | `src/lib/crypto-client/argon2id.ts` | Argon2id KDF + metadata | Reusable crypto helper | Replace with vault-core `kdf/argon2id` exports |
303
+ | `src/lib/crypto-client/vault-kdf.ts` | Vault password NFKC + Argon2id | Reusable crypto helper | Replace with vault-core password KDF (same NFKC normalization) |
304
+ | `src/lib/crypto-client/vault-envelope.ts` | Password + recovery phrase envelopes | Local envelope implementation | Replace with `createPasswordEnvelope` / `createRecoveryEnvelope` + profile wrappers |
305
+ | `src/lib/crypto-client/recovery-phrase.ts` | BIP39 12/24 phrase generate/validate/KDF | Local recovery helper | Replace with vault-core recovery exports |
306
+ | `src/lib/crypto-client/recovery-code.ts` | Legacy hyphenated recovery **code** (not BIP39), Argon2id + PBKDF2 fallback | Local recovery helper | **Do not migrate yet** — vault-core has no recovery-code envelope; keep for vault-v1 legacy |
307
+ | `src/lib/crypto-client/passkey-vault.ts` | PRF wrap/unwrap, passkey unlock errors | Local envelope implementation | Replace PRF wrap/unwrap with vault-core; keep WebAuthn ceremony in app |
308
+ | `src/lib/passkey/prf.ts` | PRF salt: `letters-passkey-prf-v1:{userId}` | App-specific integration | Keep constant; use `buildPrfSaltBytes` from vault-core/browser |
309
+ | `src/lib/crypto-client/vault.ts` | UVK session pointer, legacy recovery code wrap, vault version constants | Browser session + legacy | Split: UVK gen → vault-core; session → vault-core/browser or gradual merge with local note cache hooks |
310
+ | `src/lib/crypto-client/vault-session.ts` | Auto-lock, manual lock, activity timers | Browser session helper | Mostly replace with `@tgoliveira/vault-core/browser`; **keep** LTG-specific hooks (`clearNoteBodyCache`, `registerVaultBeforeAutoLock`) as app layer |
311
+ | `src/lib/crypto-client/vault-settings.ts` | Encrypted vault settings plaintext schema | App-specific payload schema | **Keep schema local**; encrypt via app wrapper (field `vault_settings` not in vault-core enum) |
312
+ | `src/lib/crypto-client/vault-index.ts` | Encrypted vault index (categories, tags, entries) | App-specific payload schema | **Keep schema local**; field `vault_index` aligns with vault-core enum — candidate for shared encrypt helper + profile later |
313
+ | `src/lib/crypto-client/vault-index-types.ts` | Index plaintext types | App-specific payload schema | **Keep local** |
314
+ | `src/lib/crypto-client/notes.ts` | Note metadata/body encryption, per-note keys | App-specific payload schema | **Keep local** (fields: `title`, `body`, `note_key`, etc.) |
315
+ | `src/lib/crypto-client/note-key.ts` | Per-note AES key wrap | App-specific integration | **Keep local** |
316
+ | `src/lib/crypto-client/note-drafts.ts` | Draft encryption (IndexedDB via `idb`) | App-specific integration | **Keep local**; verify drafts stay encrypted |
317
+ | `src/lib/crypto-client/vault-idb-cleanup.ts` | Purge legacy trusted-device IDB | App-specific integration | **Keep local** |
318
+ | `src/lib/validation/encrypted-payload.ts` | Zod schema, extended AAD `field` enum, KDF union incl. PBKDF2 | App-specific payload schema | **Keep local** (superset of vault-core schema) |
319
+ | `src/modules/vault/repositories/vault-repository.ts` | DB persistence for vaults/envelopes | App-specific API/server route | **Keep local** |
320
+ | `src/modules/vault/services/vault-service.ts` | Vault setup, status, recovery replacement | App-specific API/server route | **Keep local** |
321
+ | `src/modules/vault/services/vault-security-service.ts` | Security events | App-specific API/server route | **Keep local** |
322
+ | `src/app/api/vault/**` | REST routes (init, setup, status, envelopes, …) | App-specific API/server route | **Keep local** |
323
+ | `src/features/vault/**` | Setup wizard, unlock UI, status dock | App-specific UI | **Keep local** (update imports only) |
324
+ | `src/lib/secure-auth/**` | Account auth integration | Auth integration | **Keep separate** — do not couple vault-core to secure-auth |
325
+ | `src/server/services/passkey-login-vault-service.ts` | Account passkey login + optional PRF for vault | Auth + vault integration | **Keep local** — preserve login vs PRF-unlock separation |
326
+ | `src/test/unit/crypto-*.ts`, `src/test/security/vault-*.ts` | Golden paths, plaintext rejection | Tests | Port to fixtures + vault-core APIs incrementally |
327
+
328
+ ---
329
+
330
+ ### 11.2 Current crypto / profile assumptions
331
+
332
+ | Topic | letter-to-god value / behavior | Notes |
333
+ | --- | --- | --- |
334
+ | Product npm name | `letters-to-god` | GitHub repo: `letter-to-god` |
335
+ | Vault record versions | `vault-v1` (legacy), `vault-v2` (current setup) | Constants in `vault.ts` |
336
+ | Payload encryption version | `enc-v1`, alg `AES-GCM` | Matches vault-core |
337
+ | Vault-core crypto version | N/A today | vault-core uses `cryptoVersion: "vault-v1"` for profile (distinct from LTG `vault-v2` **record** version) |
338
+ | AAD shape (legacy) | JSON `{ field, resourceId, userId }` — **no `context` key** | Stored in DB jsonb as encrypted payloads |
339
+ | AAD canonical order | `{ field, resourceId, userId }` | vault-core uses `{ context, field, resourceId, userId }` for **new** encrypts |
340
+ | Argon2id defaults | `memory: 65536`, `iterations: 3`, `parallelism: 1`, `hashLength: 32`, `saltLength: 16` | Matches vault-core `DEFAULT_ARGON2ID_PARAMS` |
341
+ | Vault password normalization | NFKC (`vault-kdf.ts`) | Matches vault-core |
342
+ | Recovery phrase | BIP39 English, 12 or 24 words, lowercase normalized | Matches vault-core |
343
+ | Legacy recovery **code** | Custom English wordlist, hyphen-separated, ~17 words; PBKDF2 fallback in `recovery-code.ts` | **Not in vault-core** — vault-v1 only |
344
+ | Passkey PRF | Required for passkey vault envelopes; PRF salt prefix `letters-passkey-prf-v1:` | App-specific; not the LiqSense prefix |
345
+ | Envelope methods (v2) | `password`, `recovery_phrase`, optional `passkey_prf` | Aligns with vault-core method names |
346
+ | DB: `user_vaults` | `vault_version`, `encrypted_vault_settings`, `encrypted_vault_index` jsonb | Server stores ciphertext only |
347
+ | DB: `vault_envelopes` | `method`, `encrypted_vault_key`, `kdf_metadata`, `public_metadata` | Server stores ciphertext only |
348
+ | Session UVK | In-memory module variable in `vault.ts` | Not persisted to localStorage |
349
+ | UI preferences | `localStorage` keys like `selahkeep:vault-status-dock:collapsed` | Non-secret UI state only |
350
+ | Note drafts | IndexedDB (`idb`) — encrypted draft pattern | Security tests assert no plaintext drafts in localStorage |
351
+ | Recovery kit | **Not implemented** in LTG today | Optional future use of vault-core kit helpers |
352
+
353
+ **Uncertainties**
354
+
355
+ - No captured production golden blobs in repo yet — agents must add fixtures from a controlled setup run before deleting local crypto.
356
+ - Whether all live `vault-v2` envelopes omit `context` in stored `aad` should be confirmed against production/staging exports; decrypt tests must cover both with and without `context` if mixed generations exist.
357
+
358
+ ---
359
+
360
+ ### 11.3 Proposed letter-to-god crypto profile
361
+
362
+ Use this profile **only after fixtures prove** existing envelopes and payloads decrypt via vault-core. Adjust strings if inspection of live data shows embedded `context` values already in use (none found in local LTG encrypt paths).
363
+
364
+ ```ts
365
+ import type { VaultCryptoProfile } from "@tgoliveira/vault-core";
366
+
367
+ /** Frozen before migration — do not edit after production cutover. */
368
+ export const LETTER_TO_GOD_VAULT_PROFILE: VaultCryptoProfile = {
369
+ cryptoVersion: "vault-v1",
370
+ aadContextVault: "letter-to-god:vault:v1",
371
+ aadContextEnvelope: "letter-to-god:vault-envelope:v1",
372
+ };
373
+
374
+ /** Passkey PRF salt prefix — must match src/lib/passkey/prf.ts today. */
375
+ export const LETTER_TO_GOD_PRF_SALT_PREFIX = "letters-passkey-prf-v1:";
376
+ ```
377
+
378
+ **Important:** New encryption with vault-core will embed `context` in stored `aad`. Legacy LTG ciphertext without `context` must still decrypt via `aadByteCandidates`. Do **not** re-encrypt production rows unless a migration project explicitly requires it.
379
+
380
+ ---
381
+
382
+ ### 11.4 Migration phases for letter-to-god
383
+
384
+ #### Phase A — Baseline local vault tests
385
+
386
+ | | |
387
+ | --- | --- |
388
+ | **Goal** | Green baseline before any dependency change |
389
+ | **Files** | None changed |
390
+ | **Tests** | `npm test` — especially `crypto-vault-envelope-ltg.test.ts`, `crypto-recovery-*.test.ts`, `vault-session.test.ts`, `src/test/security/vault-*` |
391
+ | **Rollback** | N/A |
392
+ | **Risk** | Low |
393
+
394
+ #### Phase B — Add vault-core dependency and profile
395
+
396
+ | | |
397
+ | --- | --- |
398
+ | **Goal** | Add `@tgoliveira/vault-core`, create `src/modules/vault/ltg-vault-profile.ts` (or similar) |
399
+ | **Files** | `package.json`, lockfile, new profile module |
400
+ | **Tests** | Typecheck; optional smoke import test |
401
+ | **Rollback** | Remove dependency |
402
+ | **Risk** | Low |
403
+
404
+ #### Phase C — Compatibility fixtures
405
+
406
+ | | |
407
+ | --- | --- |
408
+ | **Goal** | JSON fixtures for password envelope, recovery phrase envelope, vault index, vault settings from LTG setup flow |
409
+ | **Files** | `src/test/fixtures/vault-core-compat/*.json`, new vitest suite |
410
+ | **Tests** | Unlock/decrypt fixtures using vault-core APIs + `LETTER_TO_GOD_VAULT_PROFILE` |
411
+ | **Rollback** | Delete fixture tests only |
412
+ | **Risk** | Medium — blocks migration if decrypt fails |
413
+
414
+ #### Phase D — Replace pure crypto helpers
415
+
416
+ | | |
417
+ | --- | --- |
418
+ | **Goal** | Route `argon2id.ts`, `encoding.ts`, UVK generation through vault-core |
419
+ | **Files** | `src/lib/crypto-client/argon2id.ts`, `encoding.ts`, `vault.ts` (partial) |
420
+ | **Tests** | `crypto-argon2id.test.ts`, `crypto-aes-gcm.test.ts` |
421
+ | **Rollback** | Revert wrappers |
422
+ | **Risk** | Medium |
423
+
424
+ #### Phase E — Replace envelope / recovery helpers
425
+
426
+ | | |
427
+ | --- | --- |
428
+ | **Goal** | `vault-envelope.ts`, `recovery-phrase.ts`, `passkey-vault.ts` delegate to vault-core |
429
+ | **Files** | Envelope modules, `use-ltg-vault-setup.ts`, unlock panels |
430
+ | **Tests** | `crypto-vault-envelope-ltg.test.ts`, `crypto-passkey-vault.test.ts`, security setup tests |
431
+ | **Rollback** | Restore local envelope implementations |
432
+ | **Risk** | High — affects unlock |
433
+
434
+ #### Phase F — Generic payload encryption (partial)
435
+
436
+ | | |
437
+ | --- | --- |
438
+ | **Goal** | Use vault-core for `vault_index` encryption if wrappers align; keep `vault_settings` and notes on app-local `encryptField` until field enum extended or wrapped |
439
+ | **Files** | `vault-index.ts` (optional), **not** `notes.ts` in first pass |
440
+ | **Tests** | `vault-index.test.ts`, note tests unchanged |
441
+ | **Rollback** | Revert index wrapper |
442
+ | **Risk** | Medium |
443
+
444
+ #### Phase G — Keep UI / API / auth unchanged
445
+
446
+ | | |
447
+ | --- | --- |
448
+ | **Goal** | No route or schema changes; API still accepts same encrypted jsonb shapes |
449
+ | **Files** | Import paths only in features/modules |
450
+ | **Tests** | API route tests, `vault-setup-plaintext.test.ts` |
451
+ | **Rollback** | N/A |
452
+ | **Risk** | Low if Phase C passed |
453
+
454
+ #### Phase H — Remove duplicated local code
455
+
456
+ | | |
457
+ | --- | --- |
458
+ | **Goal** | Delete superseded files (`argon2id.ts`, duplicate aes helpers, etc.) |
459
+ | **Files** | Remove after zero references |
460
+ | **Tests** | Full `npm test` |
461
+ | **Rollback** | Git revert |
462
+ | **Risk** | Medium |
463
+
464
+ #### Phase I — Update docs
465
+
466
+ | | |
467
+ | --- | --- |
468
+ | **Goal** | Point ADRs/docs to vault-core; document profile + PRF prefix |
469
+ | **Files** | App `docs/` or ADRs |
470
+ | **Tests** | Doc lint if available |
471
+ | **Rollback** | N/A |
472
+ | **Risk** | Low |
473
+
474
+ **Explicitly out of scope for early phases:** removing `recovery-code.ts` (vault-v1 legacy), rewriting note encryption, changing `vault-v2` server schema.
475
+
476
+ ---
477
+
478
+ ### 11.5 What must remain in letter-to-god
479
+
480
+ - **Note domain:** encrypted note metadata/body, per-note keys, templates, reflection/lifecycle fields
481
+ - **Vault index plaintext schema:** categories, tags, entries, saved views, recently viewed
482
+ - **Vault settings plaintext:** `unlockBehavior`, `recoveryPhraseLength`
483
+ - **Spiritual / reflective copy:** setup prompts, recovery UX, SelahKeep branding
484
+ - **Routes:** `/vault/setup`, `/vault/unlock`, `/vault/recovery`, `/vault/settings`, `/vault/security`
485
+ - **Database:** `user_vaults`, `vault_envelopes`, `notes` tables
486
+ - **Auth checks:** secure-auth session, passkey **login** ceremony
487
+ - **Legacy recovery codes:** vault-v1 users until formally sunset
488
+ - **Security policies:** `plaintext-rejection`, passkey-vault plaintext guards, audit logging
489
+ - **App-specific AAD fields:** `title`, `body`, `note_key`, `vault_settings`, etc.
490
+
491
+ ---
492
+
493
+ ### 11.6 Agent-ready implementation prompt for letter-to-god
494
+
495
+ Copy the block below into a future Cursor agent task:
496
+
497
+ ```text
498
+ Migrate letter-to-god (SelahKeep) from local src/lib/crypto-client vault code to @tgoliveira/vault-core incrementally.
499
+
500
+ Rules:
501
+ - Do NOT rewrite the app or change UX copy unnecessarily.
502
+ - Do NOT change database schemas or API contracts.
503
+ - Do NOT change existing encrypted database records without proven compatibility fixtures.
504
+ - Account login (secure-auth) must NOT unlock the vault.
505
+ - Vault password, recovery phrase, UVK, PRF output, and decrypted payloads must NEVER be sent to the server.
506
+ - Keep recovery-code.ts for vault-v1 legacy until explicitly removed.
507
+ - Keep notes.ts, note-key.ts, vault-settings schema, and extended EncryptedPayload Zod schema in the app.
508
+ - Freeze LETTER_TO_GOD_VAULT_PROFILE and LETTER_TO_GOD_PRF_SALT_PREFIX before replacing call sites.
509
+
510
+ Steps:
511
+ 1. Read docs/ADOPTING_VAULT_CORE_IN_EXISTING_APPS.md in @tgoliveira/vault-core.
512
+ 2. Phase A: run full test suite baseline.
513
+ 3. Phase B: add @tgoliveira/vault-core dependency and profile module.
514
+ 4. Phase C: capture golden fixtures from existing LTG setup tests; prove decrypt with vault-core.
515
+ 5. Phases D–F: replace crypto/envelope/recovery/passkey-prf helpers via thin wrappers; keep app field-specific encryption local.
516
+ 6. Phase G: leave UI/API/auth routes unchanged except imports.
517
+ 7. Phase H: delete duplicated files only when tests pass.
518
+ 8. Run npm test after each phase; run security tests under src/test/security/vault-*.
519
+
520
+ Reference repo: https://github.com/tgoliveira11/letter-to-god
521
+ Package: @tgoliveira/vault-core (NOT @tgoliveira/core-vault)
522
+ ```
523
+
524
+ ---
525
+
526
+ ## 12. Common mistakes to avoid
527
+
528
+ | Mistake | Why it fails |
529
+ | --- | --- |
530
+ | Moving app payload schemas into vault-core | Breaks package boundary; couples release cycles |
531
+ | Coupling vault-core to secure-auth | Account auth ≠ vault unlock |
532
+ | Treating account login as vault unlock | Security boundary violation |
533
+ | Using password reset as vault recovery | Different secrets, different threat model |
534
+ | POSTing recovery phrase to API | Plaintext secret on server |
535
+ | Persisting decrypted payloads in localStorage/IDB | Expand XSS blast radius |
536
+ | Changing AAD profile strings casually | Bricks existing ciphertext |
537
+ | Changing KDF params without migration | Bricks existing envelopes |
538
+ | Using WebAuthn assertion signatures as AES keys | Wrong primitive — use PRF extension output only |
539
+ | Importing `@tgoliveira/vault-core/browser` from server modules | Breaks SSR / pulls `window` assumptions |
540
+ | Deleting local crypto before fixture tests pass | No rollback, production data at risk |
541
+ | Expecting deterministic AES-GCM ciphertext | IV is random — test decrypt, not bytes |
542
+ | Migrating legacy recovery **codes** to vault-core | Not supported — keep app-local until vault-v1 sunset |
543
+
544
+ ---
545
+
546
+ ## 13. Final agent handoff checklist
547
+
548
+ ```text
549
+ [ ] Current vault implementation inventoried (files classified)
550
+ [ ] Crypto profile frozen and documented
551
+ [ ] PRF salt prefix documented (if applicable)
552
+ [ ] Compatibility fixtures added and passing
553
+ [ ] Security boundaries verified (§9)
554
+ [ ] Core imports migrated (UVK, KDF, envelopes, recovery, PRF wrap)
555
+ [ ] App-specific schemas stayed local
556
+ [ ] Server never receives plaintext vault secrets
557
+ [ ] Decrypted state not persisted in localStorage/IDB
558
+ [ ] Account auth and vault unlock tests still pass
559
+ [ ] Unit + security tests passing
560
+ [ ] Duplicated local crypto removed only after green CI
561
+ [ ] App docs / ADRs updated
562
+ [ ] Rollback path documented (git revert per phase)
563
+ ```
564
+
565
+ ---
566
+
567
+ ## Related reading
568
+
569
+ - [`README.md`](../README.md) — install and exports
570
+ - [`docs/IMPLEMENTATION_GUIDE.md`](IMPLEMENTATION_GUIDE.md) — complete greenfield and operational workflows
571
+ - [`API_REFERENCE.md`](../API_REFERENCE.md) — function list
572
+ - [`SECURITY.md`](../SECURITY.md) — threat model
573
+ - [`ARCHITECTURE.md`](../ARCHITECTURE.md) — module layout
574
+ - [`MIGRATION_FROM_LIQSENSE.md`](../MIGRATION_FROM_LIQSENSE.md) — first consumer reference migration
575
+ - [`CHANGELOG.md`](../CHANGELOG.md) — versioned changes and upgrade impact