@tgoliveira/vault-core 0.1.0 → 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.
- package/AGENTS.md +77 -0
- package/API_REFERENCE.md +196 -26
- package/ARCHITECTURE.md +5 -0
- package/CHANGELOG.md +51 -0
- package/MIGRATION_FROM_LIQSENSE.md +3 -1
- package/PASSKEY_PRF_ENVELOPES.md +2 -1
- package/PASSWORD_ENVELOPES.md +3 -1
- package/README.md +42 -2
- package/RECOVERY_PHRASE.md +2 -1
- package/SECURITY.md +22 -2
- package/dist/browser.d.ts +12 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +46 -18
- package/dist/browser.js.map +1 -1
- package/dist/envelopes/passkey-prf.d.ts +3 -3
- package/dist/envelopes/passkey-prf.d.ts.map +1 -1
- package/dist/envelopes/passkey-prf.js +7 -5
- package/dist/envelopes/passkey-prf.js.map +1 -1
- package/dist/envelopes/password.d.ts +1 -1
- package/dist/envelopes/password.d.ts.map +1 -1
- package/dist/envelopes/password.js +3 -1
- package/dist/envelopes/password.js.map +1 -1
- package/dist/envelopes/recovery.d.ts +2 -2
- package/dist/envelopes/recovery.d.ts.map +1 -1
- package/dist/envelopes/recovery.js +15 -6
- package/dist/envelopes/recovery.js.map +1 -1
- package/dist/kdf/argon2id.d.ts.map +1 -1
- package/dist/kdf/argon2id.js +15 -2
- package/dist/kdf/argon2id.js.map +1 -1
- package/dist/kdf/params.d.ts +24 -0
- package/dist/kdf/params.d.ts.map +1 -1
- package/dist/kdf/params.js +22 -0
- package/dist/kdf/params.js.map +1 -1
- package/dist/payload/encrypted-payload.d.ts +4 -2
- package/dist/payload/encrypted-payload.d.ts.map +1 -1
- package/dist/payload/encrypted-payload.js +3 -1
- package/dist/payload/encrypted-payload.js.map +1 -1
- package/dist/react/session/use-vault-session.d.ts +1 -0
- package/dist/react/session/use-vault-session.d.ts.map +1 -1
- package/dist/react/session/use-vault-session.js +7 -2
- package/dist/react/session/use-vault-session.js.map +1 -1
- package/dist/react/session/vault-session-provider.d.ts +2 -1
- package/dist/react/session/vault-session-provider.d.ts.map +1 -1
- package/dist/react/session/vault-session-provider.js +7 -2
- package/dist/react/session/vault-session-provider.js.map +1 -1
- package/dist/session/auto-lock.d.ts +2 -1
- package/dist/session/auto-lock.d.ts.map +1 -1
- package/dist/session/auto-lock.js +15 -1
- package/dist/session/auto-lock.js.map +1 -1
- package/dist/validation/aad-assert.d.ts +5 -3
- package/dist/validation/aad-assert.d.ts.map +1 -1
- package/dist/validation/aad-assert.js +15 -8
- package/dist/validation/aad-assert.js.map +1 -1
- package/dist/validation/plaintext-reject.d.ts.map +1 -1
- package/dist/validation/plaintext-reject.js +18 -4
- package/dist/validation/plaintext-reject.js.map +1 -1
- package/dist/validation/schemas.d.ts +148 -56
- package/dist/validation/schemas.d.ts.map +1 -1
- package/dist/validation/schemas.js +29 -10
- package/dist/validation/schemas.js.map +1 -1
- package/docs/ADOPTING_VAULT_CORE_IN_EXISTING_APPS.md +575 -0
- package/docs/IMPLEMENTATION_GUIDE.md +577 -0
- package/docs/README.md +30 -0
- package/docs/RELEASING.md +102 -0
- 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
|