@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.
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,577 @@
1
+ # Complete Implementation Guide
2
+
3
+ This guide is the end-to-end consumer contract for `@tgoliveira/vault-core`. It is written so a
4
+ developer or coding agent can implement the package without reading its source code.
5
+
6
+ ## 1. What the package owns
7
+
8
+ The package owns:
9
+
10
+ - Generation and import/export of the 256-bit User Vault Key (UVK).
11
+ - AES-GCM encryption of generic JSON payloads with authenticated context.
12
+ - Password and recovery phrase envelopes using bounded Argon2id.
13
+ - Passkey PRF envelopes when the application provides WebAuthn PRF output bytes.
14
+ - BIP39 English recovery phrase generation, validation, confirmation, and recovery kit text.
15
+ - Runtime schemas for encrypted payloads and stored envelopes.
16
+ - Browser-only in-memory session, inactivity lock, storage namespace inspection, and recovery kit UI
17
+ helpers.
18
+ - Optional React session and status helpers.
19
+ - Plaintext leak guards and testing sentinels.
20
+
21
+ The consuming application owns:
22
+
23
+ - Account authentication and authorization.
24
+ - API routes, database schemas, persistence, rate limiting, and audit logging.
25
+ - The product-specific plaintext payload schema and migrations.
26
+ - WebAuthn registration and authentication ceremonies.
27
+ - UI, password policy, recovery education, and destructive recovery decisions.
28
+
29
+ Account login must never unlock the vault. Account password reset must never replace vault recovery.
30
+
31
+ ## 2. Requirements and installation
32
+
33
+ - Node.js 20 or newer for build, SSR, and tests.
34
+ - Web Crypto (`globalThis.crypto.subtle`) in the runtime performing encryption.
35
+ - React 18 or newer only when using `@tgoliveira/vault-core/react`.
36
+
37
+ ```bash
38
+ npm install @tgoliveira/vault-core
39
+ ```
40
+
41
+ Use only documented package entry points:
42
+
43
+ ```ts
44
+ import { createUserVaultKey } from "@tgoliveira/vault-core";
45
+ import { unlockVaultSession } from "@tgoliveira/vault-core/browser";
46
+ import { VaultSessionProvider } from "@tgoliveira/vault-core/react";
47
+ import { SENTINEL_VAULT_PASSWORD } from "@tgoliveira/vault-core/testing";
48
+ ```
49
+
50
+ Do not import internal `dist/*` files. They are not stable public APIs.
51
+
52
+ ## 3. Freeze the application crypto profile
53
+
54
+ Choose profile strings once, before production data exists:
55
+
56
+ ```ts
57
+ import type { VaultCryptoProfile } from "@tgoliveira/vault-core";
58
+
59
+ export const VAULT_PROFILE: VaultCryptoProfile = {
60
+ cryptoVersion: "vault-v1",
61
+ aadContextVault: "acme:vault:v1",
62
+ aadContextEnvelope: "acme:vault-envelope:v1",
63
+ };
64
+
65
+ export function vaultScope(userId: string) {
66
+ return { userId, resourceId: userId };
67
+ }
68
+ ```
69
+
70
+ Both identifiers must be UUID strings when data is validated by `encryptedPayloadSchema`. A profile
71
+ change makes existing high-level decrypt and unlock operations fail by design. Treat profile strings
72
+ as persisted protocol constants, not environment labels.
73
+
74
+ For multi-resource vaults, use the authenticated resource identifier as `resourceId`. Always pass the
75
+ same expected scope back during decrypt or unlock.
76
+
77
+ ## 4. Persisted data model
78
+
79
+ A minimal server record contains only encrypted structures and non-secret status metadata:
80
+
81
+ ```ts
82
+ import type {
83
+ EncryptedVaultPayload,
84
+ PasskeyPrfEnvelope,
85
+ PasswordEnvelope,
86
+ RecoveryPhraseEnvelope,
87
+ } from "@tgoliveira/vault-core";
88
+
89
+ export type StoredVaultRecord = {
90
+ cryptoVersion: "vault-v1";
91
+ encryptedBlob: EncryptedVaultPayload;
92
+ passwordEnvelope: PasswordEnvelope;
93
+ recoveryEnvelope: RecoveryPhraseEnvelope;
94
+ passkeyPrfEnvelope?: PasskeyPrfEnvelope | null;
95
+ };
96
+ ```
97
+
98
+ The server may store these structures because ciphertext, IV, salt, bounded KDF metadata, and AAD are
99
+ not plaintext secrets. The server must never receive the vault password, recovery phrase, UVK, PRF
100
+ output, or decrypted payload.
101
+
102
+ Validate records at every untrusted boundary:
103
+
104
+ ```ts
105
+ import { vaultSetupEnvelopeFieldsSchema } from "@tgoliveira/vault-core";
106
+
107
+ const record = vaultSetupEnvelopeFieldsSchema.parse(untrustedDatabaseValue);
108
+ ```
109
+
110
+ The envelope schemas are discriminated by `method`. Password and recovery envelopes require
111
+ Argon2id metadata; passkey PRF envelopes require `kdfMetadata: null`.
112
+
113
+ ## 5. Initial vault setup
114
+
115
+ Run the complete setup flow in a trusted client runtime:
116
+
117
+ ```ts
118
+ import {
119
+ createPasskeyPrfEnvelope,
120
+ createPasswordEnvelope,
121
+ createRecoveryEnvelope,
122
+ createRecoveryPhrase,
123
+ createUserVaultKey,
124
+ encryptVaultPayload,
125
+ vaultSetupEnvelopeFieldsSchema,
126
+ } from "@tgoliveira/vault-core";
127
+ import { VAULT_PROFILE, vaultScope } from "./vault-profile.js";
128
+
129
+ export async function createInitialVault<T>(input: {
130
+ userId: string;
131
+ vaultPassword: string;
132
+ initialPayload: T;
133
+ passkeyPrfOutput?: Uint8Array;
134
+ }) {
135
+ const scope = vaultScope(input.userId);
136
+ const vaultKey = await createUserVaultKey();
137
+ const recoveryPhrase = createRecoveryPhrase({ wordCount: 24 });
138
+
139
+ const { envelope: passwordEnvelope } = await createPasswordEnvelope(
140
+ vaultKey,
141
+ input.vaultPassword,
142
+ scope,
143
+ VAULT_PROFILE
144
+ );
145
+
146
+ const { envelope: recoveryEnvelope } = await createRecoveryEnvelope(
147
+ vaultKey,
148
+ recoveryPhrase,
149
+ scope,
150
+ VAULT_PROFILE,
151
+ { phraseLength: 24 }
152
+ );
153
+
154
+ const passkeyPrfEnvelope = input.passkeyPrfOutput
155
+ ? await createPasskeyPrfEnvelope(
156
+ vaultKey,
157
+ input.passkeyPrfOutput,
158
+ scope,
159
+ VAULT_PROFILE
160
+ )
161
+ : null;
162
+
163
+ const encryptedBlob = await encryptVaultPayload(
164
+ input.initialPayload,
165
+ vaultKey,
166
+ scope,
167
+ VAULT_PROFILE
168
+ );
169
+
170
+ const serverRecord = vaultSetupEnvelopeFieldsSchema.parse({
171
+ cryptoVersion: "vault-v1",
172
+ encryptedBlob,
173
+ passwordEnvelope,
174
+ recoveryEnvelope,
175
+ passkeyPrfEnvelope,
176
+ });
177
+
178
+ return {
179
+ serverRecord,
180
+ recoveryPhrase,
181
+ clientOnlyVaultKey: vaultKey,
182
+ };
183
+ }
184
+ ```
185
+
186
+ Send only `serverRecord` to the server. Keep `recoveryPhrase` in the recovery confirmation UI and
187
+ `clientOnlyVaultKey` in the in-memory session. Never serialize either value into analytics, logs,
188
+ URLs, cookies, localStorage, IndexedDB, server actions, or API requests.
189
+
190
+ Argon2id work is deliberately sequential in this example to avoid doubling peak browser memory.
191
+
192
+ ## 6. Recovery phrase confirmation and kit
193
+
194
+ Generate the required confirmation prompts and reject partial answers:
195
+
196
+ ```ts
197
+ import {
198
+ assertRecoveryPhraseWordConfirmation,
199
+ createRecoveryKitText,
200
+ getRecoveryConfirmationPromptCount,
201
+ pickRecoveryConfirmationIndices,
202
+ } from "@tgoliveira/vault-core";
203
+
204
+ const words = recoveryPhrase.split(" ");
205
+ const count = getRecoveryConfirmationPromptCount(24);
206
+ const requiredIndices = pickRecoveryConfirmationIndices(words.length, count);
207
+
208
+ assertRecoveryPhraseWordConfirmation(
209
+ recoveryPhrase,
210
+ answersByOneBasedIndex,
211
+ requiredIndices
212
+ );
213
+
214
+ const recoveryKit = createRecoveryKitText({
215
+ recoveryPhrase,
216
+ wordCount: 24,
217
+ productName: "Acme",
218
+ });
219
+ ```
220
+
221
+ In a browser, `createRecoveryKitDownload()` and `printRecoveryKitContent()` are available from the
222
+ browser entry. Explain that anyone holding the phrase can unlock the vault. Do not automatically save
223
+ the kit to cloud storage.
224
+
225
+ ## 7. Password unlock
226
+
227
+ ```ts
228
+ import {
229
+ decryptVaultPayload,
230
+ encryptedPayloadSchema,
231
+ passwordEnvelopeSchema,
232
+ unlockWithPasswordEnvelope,
233
+ } from "@tgoliveira/vault-core";
234
+ import { VAULT_PROFILE, vaultScope } from "./vault-profile.js";
235
+
236
+ export async function unlockWithPassword<T>(input: {
237
+ userId: string;
238
+ vaultPassword: string;
239
+ passwordEnvelope: unknown;
240
+ encryptedBlob: unknown;
241
+ }) {
242
+ const scope = vaultScope(input.userId);
243
+ const envelope = passwordEnvelopeSchema.parse(input.passwordEnvelope);
244
+ const encryptedBlob = encryptedPayloadSchema.parse(input.encryptedBlob);
245
+ const vaultKey = await unlockWithPasswordEnvelope(
246
+ input.vaultPassword,
247
+ envelope,
248
+ scope,
249
+ VAULT_PROFILE
250
+ );
251
+ const payload = await decryptVaultPayload<T>(
252
+ encryptedBlob,
253
+ vaultKey,
254
+ scope,
255
+ VAULT_PROFILE
256
+ );
257
+ return { vaultKey, payload };
258
+ }
259
+ ```
260
+
261
+ Do not expose whether a password failed during KDF derivation versus AES-GCM authentication. Present
262
+ a generic unlock failure to the user. UI throttling can improve local UX but cannot prevent offline
263
+ attacks against copied envelopes, so require a strong vault password and protect ciphertext access.
264
+
265
+ ## 8. Recovery phrase unlock
266
+
267
+ ```ts
268
+ import {
269
+ decryptVaultPayload,
270
+ encryptedPayloadSchema,
271
+ parseRecoveryPhraseWordCount,
272
+ recoveryPhraseEnvelopeSchema,
273
+ unlockWithRecoveryEnvelope,
274
+ } from "@tgoliveira/vault-core";
275
+
276
+ const envelope = recoveryPhraseEnvelopeSchema.parse(serverRecord.recoveryEnvelope);
277
+ const expectedWordCount = parseRecoveryPhraseWordCount(envelope.publicMetadata);
278
+ const vaultKey = await unlockWithRecoveryEnvelope(
279
+ enteredRecoveryPhrase,
280
+ envelope,
281
+ vaultScope(userId),
282
+ VAULT_PROFILE,
283
+ { expectedWordCount }
284
+ );
285
+ const payload = await decryptVaultPayload(
286
+ encryptedPayloadSchema.parse(serverRecord.encryptedBlob),
287
+ vaultKey,
288
+ vaultScope(userId),
289
+ VAULT_PROFILE
290
+ );
291
+ ```
292
+
293
+ After successful recovery, let the user create a new password envelope around the same UVK and
294
+ replace the old password envelope atomically on the server.
295
+
296
+ ## 9. Passkey PRF integration
297
+
298
+ The package does not run WebAuthn ceremonies. The application must request the PRF extension and pass
299
+ the first PRF result to vault-core.
300
+
301
+ Use a stable, application-specific PRF salt:
302
+
303
+ ```ts
304
+ import {
305
+ buildPrfSaltBytes,
306
+ extractPasskeyPrfOutput,
307
+ isPasskeySupported,
308
+ isPrfExtensionSupported,
309
+ } from "@tgoliveira/vault-core/browser";
310
+
311
+ const salt = await buildPrfSaltBytes("acme-passkey-prf-v1:", userId);
312
+
313
+ if (!isPasskeySupported() || !isPrfExtensionSupported()) {
314
+ // Offer password or recovery phrase unlock instead.
315
+ }
316
+
317
+ const credential = await navigator.credentials.get({
318
+ publicKey: {
319
+ ...applicationOwnedRequestOptions,
320
+ extensions: { prf: { eval: { first: salt } } },
321
+ },
322
+ });
323
+
324
+ if (!(credential instanceof PublicKeyCredential)) {
325
+ throw new Error("Passkey ceremony did not return a public-key credential");
326
+ }
327
+
328
+ const prfOutput = extractPasskeyPrfOutput(
329
+ credential.getClientExtensionResults()
330
+ );
331
+ ```
332
+
333
+ Exact WebAuthn option typing and credential verification belong to the application. Never send
334
+ `prfOutput` to the server.
335
+
336
+ Unlock after obtaining the PRF output:
337
+
338
+ ```ts
339
+ import {
340
+ passkeyPrfEnvelopeSchema,
341
+ unlockWithPasskeyPrfEnvelope,
342
+ } from "@tgoliveira/vault-core";
343
+
344
+ const envelope = passkeyPrfEnvelopeSchema.parse(serverRecord.passkeyPrfEnvelope);
345
+ const vaultKey = await unlockWithPasskeyPrfEnvelope(
346
+ envelope,
347
+ prfOutput,
348
+ vaultScope(userId),
349
+ VAULT_PROFILE
350
+ );
351
+ ```
352
+
353
+ Treat PRF support as an optional unlock method. Always preserve password or recovery fallback.
354
+
355
+ ## 10. Save and update encrypted payloads
356
+
357
+ Keep the typed product schema in the application:
358
+
359
+ ```ts
360
+ import { z } from "zod";
361
+ import { encryptVaultPayload } from "@tgoliveira/vault-core";
362
+
363
+ const appVaultPayloadSchema = z.object({
364
+ version: z.literal(1),
365
+ entries: z.array(z.object({ id: z.string(), secret: z.string() })),
366
+ });
367
+
368
+ const validatedPayload = appVaultPayloadSchema.parse(nextPayload);
369
+ const encryptedBlob = await encryptVaultPayload(
370
+ validatedPayload,
371
+ inMemoryVaultKey,
372
+ vaultScope(userId),
373
+ VAULT_PROFILE
374
+ );
375
+ ```
376
+
377
+ Persist only `encryptedBlob`. Use application-owned optimistic concurrency or record versions to
378
+ prevent lost updates and ciphertext rollback. Vault-core authenticates content and AAD but does not
379
+ provide server freshness or synchronization.
380
+
381
+ ## 11. Browser session without React
382
+
383
+ ```ts
384
+ import {
385
+ configureVaultSession,
386
+ getSessionVaultKey,
387
+ lockVaultSession,
388
+ registerVaultActivityGuard,
389
+ registerVaultUnloadGuard,
390
+ unlockVaultSession,
391
+ } from "@tgoliveira/vault-core/browser";
392
+
393
+ configureVaultSession({ autoLockMinutes: 15 });
394
+
395
+ const removeActivityGuard = registerVaultActivityGuard();
396
+ const removeUnloadGuard = registerVaultUnloadGuard();
397
+
398
+ unlockVaultSession(vaultKey);
399
+ const currentKey = getSessionVaultKey();
400
+
401
+ // On explicit lock or logout:
402
+ lockVaultSession();
403
+
404
+ // On application teardown:
405
+ removeActivityGuard();
406
+ removeUnloadGuard();
407
+ ```
408
+
409
+ There is no public direct key setter. This ensures unlock, lock, timers, and subscribers remain in
410
+ sync. Call `touchVaultSession()` manually only for meaningful activity not represented by the default
411
+ pointer, keyboard, touch, or focus events.
412
+
413
+ ## 12. React session integration
414
+
415
+ Mount one provider near the client application root:
416
+
417
+ ```tsx
418
+ import type { ReactNode } from "react";
419
+ import { VaultSessionProvider } from "@tgoliveira/vault-core/react";
420
+
421
+ export function AppProviders({ children }: { children: ReactNode }) {
422
+ return (
423
+ <VaultSessionProvider
424
+ sessionConfig={{ autoLockMinutes: 15 }}
425
+ registerActivityGuard
426
+ registerUnloadGuard
427
+ >
428
+ {children}
429
+ </VaultSessionProvider>
430
+ );
431
+ }
432
+ ```
433
+
434
+ Read and control state:
435
+
436
+ ```tsx
437
+ import {
438
+ useVaultClientStatus,
439
+ useVaultSession,
440
+ useVaultUnlocked,
441
+ } from "@tgoliveira/vault-core/react";
442
+
443
+ const unlocked = useVaultUnlocked();
444
+ const { lock, touch } = useVaultSession({
445
+ registerActivityGuard: false,
446
+ registerUnloadGuard: false,
447
+ });
448
+ const status = useVaultClientStatus(serverStatus, browserSupportsPrf);
449
+ ```
450
+
451
+ Avoid mounting both `VaultSessionProvider` and a default `useVaultSession()` solely to register the
452
+ same guards. When the provider owns guards, use the hook with guard registration disabled or call the
453
+ browser lifecycle functions directly.
454
+
455
+ ## 13. Storage policy and inspection
456
+
457
+ Do not persist decrypted payloads or the UVK. Storage helpers inspect namespace presence; they cannot
458
+ classify arbitrary record contents:
459
+
460
+ ```ts
461
+ import {
462
+ inspectIndexedDBPrefix,
463
+ inspectLocalStoragePrefix,
464
+ } from "@tgoliveira/vault-core/browser";
465
+
466
+ const localResult = inspectLocalStoragePrefix("acme:vault:");
467
+ const idbResult = await inspectIndexedDBPrefix("acme-vault-");
468
+
469
+ if (localResult !== "clear" || idbResult !== "clear") {
470
+ // Investigate "found" and treat "unavailable" as a failed security check.
471
+ }
472
+ ```
473
+
474
+ `inspectIndexedDBPrefix()` checks database names, not object-store contents. Enforce the real no-
475
+ plaintext rule through architecture, code review, CSP/XSS controls, and sentinel-based integration
476
+ tests.
477
+
478
+ ## 14. Server request validation
479
+
480
+ Reject known plaintext fields recursively before accepting an encrypted vault request:
481
+
482
+ ```ts
483
+ import {
484
+ assertNoVaultPlaintextFields,
485
+ vaultSetupEnvelopeFieldsSchema,
486
+ } from "@tgoliveira/vault-core";
487
+
488
+ export function parseVaultSetupRequest(body: unknown) {
489
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
490
+ throw new Error("Vault request body must be an object");
491
+ }
492
+ assertNoVaultPlaintextFields(body as Record<string, unknown>);
493
+ return vaultSetupEnvelopeFieldsSchema.parse(body);
494
+ }
495
+ ```
496
+
497
+ This guard is defense in depth, not a complete data-loss-prevention system. Use closed route schemas,
498
+ never log request bodies, and verify the authenticated account owns the AAD `userId` and resource.
499
+
500
+ ## 15. Password and recovery rotation
501
+
502
+ To change a vault password:
503
+
504
+ 1. Unlock the existing envelope and obtain the UVK in memory.
505
+ 2. Call `createPasswordEnvelope()` with the new password, same expected scope, and same profile.
506
+ 3. Atomically replace only the password envelope on the server.
507
+ 4. Keep the encrypted payload and other envelopes unchanged.
508
+
509
+ To rotate a recovery phrase, generate a new phrase and recovery envelope around the same UVK. Require
510
+ confirmation and replace the old recovery envelope atomically. Deleting an envelope is an application
511
+ authorization decision.
512
+
513
+ ## 16. Error handling
514
+
515
+ Expected domain errors include:
516
+
517
+ - `VaultPlaintextRejectionError`
518
+ - `PasskeyPrfRequiredError`
519
+ - `PasskeyUnlockError`
520
+ - `RecoveryPhraseConfirmationError`
521
+ - `VaultConflictError`
522
+ - `VaultNotFoundError`
523
+
524
+ Web Crypto, JSON parsing, Zod, and Argon2id validation may also throw standard errors. Convert detailed
525
+ internal failures into generic user-facing unlock messages. Never include entered secrets, decrypted
526
+ data, PRF bytes, or full encrypted request bodies in logs.
527
+
528
+ ## 17. Testing a consuming application
529
+
530
+ Use the testing entry to prove plaintext never crosses persistence or network boundaries:
531
+
532
+ ```ts
533
+ import {
534
+ SENTINEL_PRIVATE_NOTE,
535
+ SENTINEL_VAULT_PASSWORD,
536
+ validateNoPlaintextLeak,
537
+ } from "@tgoliveira/vault-core/testing";
538
+
539
+ const result = validateNoPlaintextLeak(capturedRequestBody);
540
+ expect(result.ok).toBe(true);
541
+ ```
542
+
543
+ Required integration tests:
544
+
545
+ - Password, recovery, and passkey round trips.
546
+ - Wrong password, wrong phrase, missing PRF, tampered ciphertext, and wrong expected AAD.
547
+ - API bodies contain no password, phrase, UVK, PRF output, or product plaintext sentinels.
548
+ - localStorage and IndexedDB contain no decrypted vault state.
549
+ - Auto-lock clears the in-memory key and updates subscribed UI.
550
+ - Stored legacy fixtures still decrypt through the documented migration path.
551
+
552
+ ## 18. Legacy ciphertext
553
+
554
+ High-level APIs require the configured AAD context. For a legacy record with missing context:
555
+
556
+ 1. Use `decryptField()` only inside an explicit migration path.
557
+ 2. Validate every available AAD field against the authenticated user and resource.
558
+ 3. Parse and validate the decrypted product payload.
559
+ 4. Re-encrypt immediately with `encryptVaultPayload()` and the frozen profile.
560
+ 5. Remove the compatibility path after migration completes.
561
+
562
+ Never make missing AAD context a permanent high-level fallback.
563
+
564
+ ## 19. Production readiness checklist
565
+
566
+ - [ ] Profile strings are unique, stable, documented, and frozen.
567
+ - [ ] User and resource IDs passed to AAD match authenticated ownership.
568
+ - [ ] Product payloads are validated before encryption and after decryption.
569
+ - [ ] Server routes accept only runtime-validated encrypted structures.
570
+ - [ ] Password, phrase, UVK, PRF output, and decrypted payload never reach the server or logs.
571
+ - [ ] Decrypted data is absent from localStorage, IndexedDB, cookies, URLs, and analytics.
572
+ - [ ] Password and recovery unlock remain available if passkey PRF is unsupported.
573
+ - [ ] Recovery confirmation and offline storage education are implemented.
574
+ - [ ] In-memory sessions auto-lock and clear on `pagehide`.
575
+ - [ ] Rotation and recovery updates are atomic and authorization-protected.
576
+ - [ ] Wrong-AAD, tamper, leak, storage, and auto-lock tests pass.
577
+ - [ ] The application pins a compatible package version and reviews `CHANGELOG.md` before upgrades.
package/docs/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # Documentation Index
2
+
3
+ Use this page as the documentation router for `@tgoliveira/vault-core`.
4
+
5
+ ## Consumers
6
+
7
+ - [`IMPLEMENTATION_GUIDE.md`](IMPLEMENTATION_GUIDE.md): complete greenfield implementation from
8
+ setup through password, recovery phrase, passkey PRF, persistence boundaries, and sessions.
9
+ - [`ADOPTING_VAULT_CORE_IN_EXISTING_APPS.md`](ADOPTING_VAULT_CORE_IN_EXISTING_APPS.md): phased
10
+ migration for an application that already has vault code or stored ciphertext.
11
+ - [`../API_REFERENCE.md`](../API_REFERENCE.md): public entry points and their security preconditions.
12
+ - [`../SECURITY.md`](../SECURITY.md): threat model, secret boundaries, and storage rules.
13
+
14
+ ## Maintainers and agents
15
+
16
+ - [`../AGENTS.md`](../AGENTS.md): repository operating rules and definition of done.
17
+ - [`RELEASING.md`](RELEASING.md): versioning, changelog, tag, validation, and publication procedure.
18
+ - [`../CHANGELOG.md`](../CHANGELOG.md): released and unreleased consumer-visible changes.
19
+ - [`../ARCHITECTURE.md`](../ARCHITECTURE.md): package layers and cryptographic data flow.
20
+
21
+ ## Topic references
22
+
23
+ - [`../PASSWORD_ENVELOPES.md`](../PASSWORD_ENVELOPES.md)
24
+ - [`../RECOVERY_PHRASE.md`](../RECOVERY_PHRASE.md)
25
+ - [`../PASSKEY_PRF_ENVELOPES.md`](../PASSKEY_PRF_ENVELOPES.md)
26
+ - [`../MIGRATION_FROM_LIQSENSE.md`](../MIGRATION_FROM_LIQSENSE.md)
27
+
28
+ If documentation and implementation disagree, treat the TypeScript declarations and runtime tests as
29
+ the immediate source of truth, then fix the documentation in the same change.
30
+
@@ -0,0 +1,102 @@
1
+ # Release Process
2
+
3
+ Releases are initiated manually, but version calculation, validation, npm publication, Git commit,
4
+ Git tag, and GitHub release creation are automated. Do not create release tags manually.
5
+
6
+ ## Version policy
7
+
8
+ This package follows Semantic Versioning. While the major version is `0`:
9
+
10
+ - Patch releases contain backward-compatible fixes or documentation-only changes.
11
+ - Minor releases contain new features or any breaking public API change.
12
+ - After `1.0.0`, breaking changes increment the major version.
13
+
14
+ Every consumer-visible change belongs under `CHANGELOG.md` → `Unreleased` in one of these sections:
15
+
16
+ - `Added`
17
+ - `Changed`
18
+ - `Deprecated`
19
+ - `Removed`
20
+ - `Fixed`
21
+ - `Security`
22
+
23
+ Mark breaking changes with `**Breaking:**` and include a migration path.
24
+
25
+ ## One-time repository and npm setup
26
+
27
+ 1. Create a protected GitHub environment named `npmjs`.
28
+ 2. Add required reviewers to that environment if publication needs human approval.
29
+ 3. Allow the GitHub Actions bot to push the release commit and `vault-core-v*` tags to `main`, or
30
+ provide an equivalently scoped GitHub App token if branch protection forbids `GITHUB_TOKEN` pushes.
31
+ 4. In the npm package settings, configure a GitHub Actions trusted publisher with:
32
+ - Repository owner: the GitHub owner of this repository.
33
+ - Repository: `vault-core`.
34
+ - Workflow filename: `publish-vault-core.yml`.
35
+ - Environment: `npmjs`.
36
+ - Allowed action: `npm publish`.
37
+ 5. After one successful OIDC publication, remove the legacy `NPM_TOKEN` secret and disallow token
38
+ publishing in npm package settings. The workflow retains token fallback only for migration.
39
+
40
+ Trusted publishing requires a GitHub-hosted runner, Node 22.14 or newer, npm 11.5.1 or newer, and
41
+ `id-token: write`. The workflow uses Node 24 and verifies the npm version before continuing.
42
+
43
+ ## Start a release
44
+
45
+ Use the GitHub Actions UI:
46
+
47
+ 1. Open **Actions** → **Publish package to npmjs**.
48
+ 2. Select **Run workflow** on `main`.
49
+ 3. Leave `version` blank for automatic versioning, or enter one of:
50
+ - An exact stable version such as `0.3.0`.
51
+ - `patch`, `minor`, or `major`.
52
+
53
+ Equivalent GitHub CLI commands:
54
+
55
+ ```bash
56
+ # Automatic version
57
+ gh workflow run publish-vault-core.yml --ref main
58
+
59
+ # Exact or explicit bump
60
+ gh workflow run publish-vault-core.yml --ref main -f version=0.3.0
61
+ gh workflow run publish-vault-core.yml --ref main -f version=patch
62
+ ```
63
+
64
+ ## Automatic version calculation
65
+
66
+ When `version` is blank or `auto`, `scripts/prepare-release.mjs` reads the `Unreleased` changelog:
67
+
68
+ 1. Any `**Breaking:**` entry selects major, or minor while the current major version is `0`.
69
+ 2. Otherwise, at least one `Added` entry selects minor.
70
+ 3. Otherwise, the release selects patch.
71
+
72
+ When `Unreleased` is empty, the workflow enters recovery mode for the current released version. It
73
+ completes missing npm, tag, or GitHub release state without publishing a duplicate.
74
+
75
+ ## Publication gates and ordering
76
+
77
+ The workflow serializes releases so two versions cannot publish concurrently. It then:
78
+
79
+ 1. Checks out `main` with full tag history.
80
+ 2. Installs the exact lockfile with `npm ci`.
81
+ 3. Runs `npm audit` at the `high` threshold, which also blocks critical vulnerabilities.
82
+ 4. Calculates the version and moves `Unreleased` entries to a dated release section.
83
+ 5. Runs typecheck, all tests, per-file coverage gates, and the production build.
84
+ 6. Builds one tarball that becomes the exact validated publication artifact.
85
+ 7. Rejects version collisions or inconsistent pre-existing tags.
86
+ 8. Commits `package.json`, `package-lock.json`, and `CHANGELOG.md` as `Release x.y.z`.
87
+ 9. Publishes to npm with OIDC/provenance when trusted publishing is configured.
88
+ 10. Creates and pushes `vault-core-vx.y.z` only after npm publication succeeds.
89
+ 11. Creates GitHub release notes and a workflow summary.
90
+
91
+ The npm registry is immutable: a published version cannot be overwritten. If publication succeeds
92
+ but tag or GitHub release creation fails, rerun the workflow with a blank version. Recovery mode
93
+ detects the released changelog version, skips duplicate npm publication, and completes missing Git
94
+ metadata.
95
+
96
+ ## Post-release verification
97
+
98
+ - Confirm npmjs shows the expected version and provenance badge.
99
+ - Confirm all four package entry points resolve.
100
+ - Confirm README, changelog, and implementation guide render on npmjs.
101
+ - Confirm `vault-core-vx.y.z` and the GitHub release point to the release commit.
102
+ - Confirm `CHANGELOG.md` has a new empty `Unreleased` section.