@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
package/AGENTS.md ADDED
@@ -0,0 +1,77 @@
1
+ # Agent and Contributor Guide
2
+
3
+ This file is the operational entry point for AI agents and human contributors working on
4
+ `@tgoliveira/vault-core`.
5
+
6
+ ## Language
7
+
8
+ - Source code, identifiers, error messages, tests, comments, and documentation must be written in
9
+ English.
10
+ - Consumer-facing API examples must be complete TypeScript that reflects the current signatures.
11
+
12
+ ## Read first
13
+
14
+ 1. `README.md` for package scope and the shortest working example.
15
+ 2. `docs/IMPLEMENTATION_GUIDE.md` for the complete consumer workflow.
16
+ 3. `SECURITY.md` before changing crypto, persistence, validation, or session behavior.
17
+ 4. `API_REFERENCE.md` for public entry points and security preconditions.
18
+ 5. `CHANGELOG.md` before modifying a public contract.
19
+ 6. `docs/RELEASING.md` before changing the package version or publishing.
20
+
21
+ ## Public package boundaries
22
+
23
+ | Import | Responsibility |
24
+ | --- | --- |
25
+ | `@tgoliveira/vault-core` | Crypto, envelopes, recovery, schemas, AAD, and validation |
26
+ | `@tgoliveira/vault-core/browser` | Browser session lifecycle, storage inspection, PRF salt, recovery kit DOM helpers |
27
+ | `@tgoliveira/vault-core/react` | React session provider, hooks, and client status derivation |
28
+ | `@tgoliveira/vault-core/testing` | Plaintext sentinels and leak-detection helpers |
29
+
30
+ Do not add framework, persistence, database, route, authentication, or product payload concerns to
31
+ the core entry.
32
+
33
+ ## Non-negotiable security invariants
34
+
35
+ - Vault passwords, recovery phrases, UVKs, PRF output, and decrypted payloads never go to the server.
36
+ - Decrypted vault state is never persisted to localStorage or IndexedDB.
37
+ - High-level decrypt and unlock calls always receive and validate the expected scope and profile.
38
+ - Persisted KDF metadata is untrusted and bounded before work begins.
39
+ - Session key changes use lifecycle-aware lock and unlock APIs; public direct setters are forbidden.
40
+ - Account authentication and vault unlock remain separate security domains.
41
+
42
+ ## Change protocol
43
+
44
+ For every user-visible change:
45
+
46
+ 1. Update implementation and tests together.
47
+ 2. Add an entry under the appropriate `CHANGELOG.md` `Unreleased` heading.
48
+ 3. Update every affected example and signature in consumer documentation.
49
+ 4. Preserve deprecated aliases only when the migration path remains safe and explicit.
50
+ 5. Run `npm run validate`.
51
+ 6. Run `npm pack --dry-run` when exports, package files, or documentation change.
52
+
53
+ The test suite enforces that the current `package.json` version has a released changelog entry. A
54
+ version bump without a changelog release section must fail validation.
55
+
56
+ Do not bump versions, create release tags, or publish manually. Start the `Publish package to npmjs`
57
+ workflow on `main`; it owns version selection, release metadata, npm publication, and tag creation.
58
+
59
+ ## Required validation
60
+
61
+ ```bash
62
+ npm ci
63
+ npm run validate
64
+ npm pack --dry-run
65
+ ```
66
+
67
+ Coverage thresholds are enforced per production file at 90% for statements, branches, functions,
68
+ and lines. Do not lower or bypass thresholds to merge a change.
69
+
70
+ ## Definition of done
71
+
72
+ - Public types and runtime schemas agree.
73
+ - Security failures have regression tests.
74
+ - All code and documentation are in English.
75
+ - `CHANGELOG.md` describes the consumer-visible effect.
76
+ - Documentation examples typecheck conceptually against current APIs.
77
+ - `npm run validate` and package dry-run pass.
package/API_REFERENCE.md CHANGED
@@ -1,36 +1,206 @@
1
1
  # API Reference
2
2
 
3
- See TypeScript exports from:
3
+ The package exposes four supported entry points. Internal `dist/*` paths are not public APIs.
4
4
 
5
- - `@tgoliveira/vault-core`
6
- - `@tgoliveira/vault-core/browser`
7
- - `@tgoliveira/vault-core/testing`
8
- - `@tgoliveira/vault-core/react`
5
+ For complete workflows, use [`docs/IMPLEMENTATION_GUIDE.md`](docs/IMPLEMENTATION_GUIDE.md).
9
6
 
10
- ## Core types
7
+ ## Core: `@tgoliveira/vault-core`
11
8
 
12
- - `VaultCryptoProfile`, `VaultCryptoVersion`
13
- - `EncryptedVaultPayload`, `VaultEnvelope`, `PasswordEnvelope`, `RecoveryPhraseEnvelope`, `PasskeyPrfEnvelope`
14
- - `RecoveryPhraseWordCount` (`12 | 24`)
15
- - `VaultUnlockResult<TPayload>`, `VaultCoreError`
9
+ ### Protocol constants and profile
16
10
 
17
- ## Core functions
11
+ | Export | Purpose |
12
+ | --- | --- |
13
+ | `ENCRYPTION_VERSION` | Stored payload version, currently `enc-v1` |
14
+ | `ENCRYPTION_ALG` | Stored algorithm identifier, currently `AES-GCM` |
15
+ | `VAULT_CRYPTO_VERSION` | Vault protocol version, currently `vault-v1` |
16
+ | `DEFAULT_VAULT_AUTO_LOCK_MINUTES` | Default browser inactivity timeout |
17
+ | `VaultCryptoProfile` | Stable application AAD contexts |
18
+ | `VaultAadScope`, `VaultAadField` | Authenticated user/resource/field scope |
19
+ | `RecoveryPhraseWordCount` | `12 | 24` |
20
+ | `resolveAadContext(scope, profile)` | Resolves explicit or profile-derived AAD context |
18
21
 
19
- - `createUserVaultKey()`
20
- - `encryptVaultPayload<T>(payload, key, scope, profile)`
21
- - `decryptVaultPayload<T>(encrypted, key)`
22
- - `createPasswordEnvelope` / `unlockWithPasswordEnvelope`
23
- - `createRecoveryPhrase` / `createRecoveryEnvelope` / `unlockWithRecoveryEnvelope`
24
- - `createPasskeyPrfEnvelope` / `unlockWithPasskeyPrfEnvelope`
25
- - `createRecoveryKitText(...)`
26
- - `assertVaultKeyAad` / `assertVaultPayloadAad`
27
- - `assertNoVaultPlaintextFields` / `validateNoPlaintextLeak`
22
+ ### User Vault Key and AES-GCM
28
23
 
29
- Deprecated aliases (`wrapVaultKeyForPassword`, etc.) remain for migration.
24
+ | Export | Purpose |
25
+ | --- | --- |
26
+ | `createUserVaultKey()` | Generates an extractable 256-bit AES-GCM UVK |
27
+ | `importUserVaultKey(bytes)` | Imports raw UVK bytes |
28
+ | `exportUserVaultKey(key)` | Exports raw UVK bytes; keep client-only |
29
+ | `generateAesKey()`, `importAesKey()`, `exportAesKey()` | Low-level AES key primitives |
30
+ | `encryptVaultPayload(payload, key, scope, profile)` | Serializes and encrypts generic JSON |
31
+ | `decryptVaultPayload(encrypted, key, expectedScope, profile)` | Validates expected AAD, decrypts, and parses JSON |
32
+ | `encryptField(plaintext, key, aad, profile)` | Low-level string encryption |
33
+ | `decryptField(encrypted, key)` | Low-level compatibility decrypt without expected-scope validation |
34
+ | `canonicalAadString(aad)` | Produces canonical AAD JSON |
35
+ | `aadByteCandidates(aad)` | Produces canonical and legacy AAD byte candidates |
30
36
 
31
- ## React entry (`@tgoliveira/vault-core/react`)
37
+ Use the high-level payload APIs for application data. `decryptField()` is appropriate only when the
38
+ caller separately validates expected AAD, such as a bounded legacy migration.
32
39
 
33
- - `useVaultUnlocked()`, `useVaultLockState()`
34
- - `useVaultSession()`, `VaultSessionProvider`
35
- - `resolveVaultClientStatus()`, `useVaultClientStatus()`
36
- - `VaultClientStatus`, `VaultServerStatusSnapshot`
40
+ ### Encoding, random, and serialization utilities
41
+
42
+ - `bytesToBase64Url(bytes)` / `base64UrlToBytes(value)`
43
+ - `stringToBytes(value)` / `bytesToString(bytes)`
44
+ - `toBufferSource(bytes)`
45
+ - `randomBytes(length)`
46
+ - `serializeVaultPayload(payload)` / `parseVaultPayload<T>(json)`
47
+
48
+ ### Argon2id
49
+
50
+ | Export | Purpose |
51
+ | --- | --- |
52
+ | `DEFAULT_ARGON2ID_PARAMS` | Creation defaults |
53
+ | `ARGON2ID_LIMITS` | Accepted persisted resource bounds |
54
+ | `assertSafeArgon2idParams(params)` | Validates memory, iteration, and parallelism bounds |
55
+ | `assertSafeArgon2idSalt(salt)` | Validates salt size |
56
+ | `serializeArgon2idMetadata(salt, params?)` | Builds persisted metadata |
57
+ | `parseArgon2idMetadata(metadata)` | Decodes and validates persisted metadata |
58
+ | `deriveArgon2idAesKey(...)` | Low-level byte-based derivation |
59
+ | `deriveArgon2idAesKeyFromMetadata(...)` | Low-level derivation from stored metadata |
60
+ | `deriveVaultPasswordKey(password, salt?)` | NFKC-normalized password derivation |
61
+ | `deriveVaultPasswordKeyFromMetadata(password, metadata)` | Password derivation from stored metadata |
62
+
63
+ Applications normally use envelope APIs instead of direct derivation functions.
64
+
65
+ ### Password envelopes
66
+
67
+ - `createPasswordEnvelope(vaultKey, password, scope, profile, salt?)`
68
+ - `unlockWithPasswordEnvelope(password, envelope, expectedScope, profile)`
69
+
70
+ ### Recovery phrases and envelopes
71
+
72
+ - `createRecoveryPhrase({ wordCount })`
73
+ - `normalizeRecoveryPhrase(phrase)`
74
+ - `validateRecoveryPhraseFormat(phrase)`
75
+ - `getRecoveryPhraseWordCount(phrase)`
76
+ - `parseRecoveryPhraseWordCount(publicMetadata)`
77
+ - `assertRecoveryPhraseUnlockInput(phrase, expectedWordCount?)`
78
+ - `getRecoveryConfirmationPromptCount(wordCount)`
79
+ - `pickRecoveryConfirmationIndices(wordCount, count?)`
80
+ - `assertRecoveryPhraseConfirmation(original, confirmation)`
81
+ - `assertRecoveryPhraseWordConfirmation(phrase, answers, requiredIndices?)`
82
+ - `deriveRecoveryPhraseKey(...)` / `deriveRecoveryPhraseKeyFromMetadata(...)`
83
+ - `createRecoveryEnvelope(vaultKey, phrase, scope, profile, publicMetadata?, salt?)`
84
+ - `unlockWithRecoveryEnvelope(phrase, envelope, expectedScope, profile, options?)`
85
+ - `createRecoveryKitText(input)`
86
+ - `RECOVERY_PHRASE_WORDLIST_SOURCE`, `DEFAULT_RECOVERY_PHRASE_WORD_COUNT`
87
+
88
+ Word confirmation requires all deterministic default indices unless explicit required indices are
89
+ provided.
90
+
91
+ ### Passkey PRF envelopes
92
+
93
+ - `createPasskeyPrfEnvelope(vaultKey, prfOutput, scope, profile, publicMetadata?)`
94
+ - `unlockWithPasskeyPrfEnvelope(envelope, prfOutput, expectedScope, profile, options?)`
95
+ - `unwrapVaultKeyFromPasskey(encryptedVaultKey, prfOutput, expectedScope, profile)`
96
+ - `extractPasskeyPrfOutput(extensionResults)`
97
+ - `isPasskeySupported()` / `isPrfExtensionSupported()`
98
+
99
+ The application owns WebAuthn ceremonies. Capability probes are preliminary; the actual ceremony may
100
+ still return no PRF output. PRF output must remain client-only.
101
+
102
+ ### Runtime schemas and types
103
+
104
+ | Export | Runtime contract |
105
+ | --- | --- |
106
+ | `encryptedPayloadSchema` | `enc-v1` AES-GCM payload with UUID AAD identifiers |
107
+ | `argon2idKdfMetadataSchema` / `kdfMetadataSchema` | Bounded `kdf-v1` Argon2id metadata |
108
+ | `passwordEnvelopeSchema` | Password method plus required Argon2id metadata |
109
+ | `recoveryPhraseEnvelopeSchema` | Recovery method plus required Argon2id metadata |
110
+ | `passkeyPrfEnvelopeSchema` | Passkey PRF method plus null KDF metadata |
111
+ | `storedEnvelopeSchema` | Method-discriminated union of all envelopes |
112
+ | `vaultSetupEnvelopeFieldsSchema` | Complete encrypted setup record |
113
+
114
+ Associated inferred types include `EncryptedVaultPayload`, `Argon2idKdfMetadata`, `VaultEnvelope`,
115
+ `PasswordEnvelope`, `RecoveryPhraseEnvelope`, `PasskeyPrfEnvelope`, and `VaultEnvelopeMethod`.
116
+
117
+ ### AAD and plaintext validation
118
+
119
+ - `assertVaultKeyAad(expectedScope, payload, profile)`
120
+ - `assertVaultPayloadAad(expectedScope, payload, profile)`
121
+ - `rejectVaultPlaintextFields(body)`
122
+ - `assertNoVaultPlaintextFields(body)`
123
+ - `validateNoPlaintextLeak(data)`
124
+ - `scanForSentinels(data, sentinels?)`
125
+ - `containsSentinel(value, sentinels?)`
126
+ - `PLAINTEXT_FORBIDDEN_VAULT_FIELDS`, `ALL_SENTINELS`, and named `SENTINEL_*` constants
127
+
128
+ The plaintext field guard is recursive and cycle-safe. It is defense in depth; closed API schemas are
129
+ still required.
130
+
131
+ ### Errors
132
+
133
+ - `VaultPlaintextRejectionError`
134
+ - `VaultConflictError`
135
+ - `VaultNotFoundError`
136
+ - `PasskeyPrfRequiredError`
137
+ - `PasskeyUnlockError`
138
+ - `RecoveryPhraseConfirmationError`
139
+ - `VaultCoreError`
140
+
141
+ ### Deprecated migration aliases
142
+
143
+ - `generateUserVaultKey`
144
+ - `generateRecoveryPhrase`
145
+ - `wrapVaultKeyForPassword` / `unwrapVaultKeyFromPassword`
146
+ - `wrapVaultKeyForRecoveryPhrase` / `unwrapVaultKeyFromRecoveryPhrase`
147
+ - `wrapVaultKeyForPasskey` / `unlockVaultFromPasskeyEnvelope`
148
+ - `buildRecoveryKitContent`
149
+ - `EncryptedPayload`, `StoredEnvelope`
150
+
151
+ New code should use the canonical APIs. Deprecated unlock aliases use the current secure signatures.
152
+
153
+ ## Browser: `@tgoliveira/vault-core/browser`
154
+
155
+ ### Session lifecycle
156
+
157
+ - `configureVaultSession(config)`
158
+ - `unlockVaultSession(vaultKey)` / `lockVaultSession()`
159
+ - `lockVaultSessionManually()` / `isVaultManuallyLocked()`
160
+ - `touchVaultSession()` / `scheduleVaultAutoLock()` / `clearVaultAutoLockTimer()`
161
+ - `getVaultAutoLockRemainingMs()`
162
+ - `getSessionVaultKey()` / `isVaultUnlocked()`
163
+ - `subscribeVaultSession(listener)`
164
+ - `registerVaultActivityGuard(events?)`
165
+ - `registerVaultUnloadGuard()`
166
+ - `resetVaultSessionLockState()`
167
+ - `VaultSessionConfig`
168
+
169
+ Direct session-key setters are intentionally not exported.
170
+
171
+ ### Storage inspection
172
+
173
+ - `VaultStorageInspectionResult`: `"clear" | "found" | "unavailable"`
174
+ - `inspectLocalStoragePrefix(prefix)`
175
+ - `inspectIndexedDBPrefix(prefix)`
176
+ - `persistVaultRecordLocally()` always throws to prevent accidental plaintext persistence
177
+
178
+ Namespace inspection does not inspect record contents. Treat `"unavailable"` as a failed security
179
+ check. `assertNoDecryptedVaultInLocalStorage` and `assertNoDecryptedVaultInIndexedDB` are deprecated
180
+ boolean aliases that fail closed.
181
+
182
+ ### Browser UX and passkey helpers
183
+
184
+ - `buildPrfSaltBytes(prefix, userId)`
185
+ - `createRecoveryKitDownload(content, filename)`
186
+ - `printRecoveryKitContent(content)`
187
+ - `extractPasskeyPrfOutput`, `isPasskeySupported`, `isPrfExtensionSupported`
188
+ - `createRecoveryKitText`, `buildRecoveryKitContent`
189
+
190
+ ## React: `@tgoliveira/vault-core/react`
191
+
192
+ - `VaultSessionProvider` / `VaultSessionProviderProps`
193
+ - `useVaultSession(options)` / `UseVaultSessionOptions`
194
+ - `useVaultUnlocked()` / `useVaultLockState()`
195
+ - `resolveVaultClientStatus(status, unlocked, prfSupported)`
196
+ - `useVaultClientStatus(serverStatus, prfSupported)`
197
+ - `VaultClientStatus` / `VaultServerStatusSnapshot`
198
+
199
+ Provider and session hook guard options are `registerActivityGuard` and `registerUnloadGuard`, both
200
+ defaulting to `true`.
201
+
202
+ ## Testing: `@tgoliveira/vault-core/testing`
203
+
204
+ This entry exports the plaintext validation functions, forbidden field list, `ALL_SENTINELS`, and all
205
+ named `SENTINEL_*` values. Use it in network, persistence, logging, and fixture tests. It does not
206
+ export internal LiqSense compatibility fixtures.
package/ARCHITECTURE.md CHANGED
@@ -13,6 +13,8 @@
13
13
  | Passkey PRF | PRF output → AES key | UVK |
14
14
 
15
15
  Envelope AAD field: `vault_key` with app `aadContextEnvelope`.
16
+ Persisted envelopes are validated as a method-discriminated union, so password and recovery
17
+ envelopes require Argon2id metadata while passkey PRF envelopes require `null` KDF metadata.
16
18
 
17
19
  ## Encrypted payload
18
20
 
@@ -30,3 +32,6 @@ Format: `enc-v1` / `AES-GCM` / `kdf-v1`.
30
32
  ```
31
33
 
32
34
  Apps own: persistence, routes, product UI, product payload schema, WebAuthn ceremony.
35
+
36
+ Browser and React session layers keep the UVK in memory, renew auto-lock on activity, and clear it on
37
+ lock or `pagehide`. Direct key mutation is not part of the public browser entry.
package/CHANGELOG.md ADDED
@@ -0,0 +1,51 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
6
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Because the package is pre-1.0, breaking
7
+ API changes increment the minor version.
8
+
9
+ ## [Unreleased]
10
+
11
+ ## [0.2.0] - 2026-06-19
12
+
13
+ ### Added
14
+
15
+ - Per-file coverage enforcement at 90% for statements, branches, functions, and lines.
16
+ - Method-specific Zod schemas for password, recovery phrase, and passkey PRF envelopes.
17
+ - Explicit `clear`, `found`, and `unavailable` storage namespace inspection results.
18
+ - Automatic browser activity tracking for React session hooks and providers.
19
+ - Complete implementation, adoption, security, and release documentation for humans and agents.
20
+ - Continuous validation for pull requests and pushes, including the per-file coverage gate.
21
+ - Manually dispatched npm publication with changelog-based automatic versioning, provenance, release
22
+ commits, generated Git tags, and GitHub release notes.
23
+
24
+ ### Changed
25
+
26
+ - **Breaking:** high-level payload decrypt and envelope unlock APIs now require the expected AAD
27
+ scope and crypto profile.
28
+ - **Breaking:** direct session-key mutation helpers are no longer exported from the browser entry.
29
+ - **Breaking:** recovery word confirmation now requires every expected answer.
30
+ - Deprecated boolean storage checks now fail closed when inspection is unavailable.
31
+ - The published package now includes the complete `docs` directory and this changelog.
32
+
33
+ ### Security
34
+
35
+ - Plaintext request guards now inspect nested objects and arrays with cycle protection.
36
+ - Persisted Argon2id parameters, salt sizes, and hash length are bounded before derivation.
37
+ - High-level decrypt and unlock operations reject AAD belonging to another user, resource, field,
38
+ or application context.
39
+ - Envelope schemas reject invalid method and KDF metadata combinations.
40
+
41
+ ## [0.1.1] - 2026-06-18
42
+
43
+ ### Added
44
+
45
+ - Initial public release of `@tgoliveira/vault-core`.
46
+ - AES-GCM payload encryption with canonical AAD.
47
+ - Password and BIP39 recovery phrase envelopes using Argon2id.
48
+ - Passkey PRF envelope primitives.
49
+ - Browser in-memory session and React integration helpers.
50
+ - Plaintext rejection and sentinel-based testing utilities.
51
+ - LiqSense compatibility fixtures and migration aliases.
@@ -17,11 +17,13 @@ export const LIQSENSE_PRF_SALT_PREFIX = "liqsense-passkey-prf-v1:";
17
17
  | --- | --- |
18
18
  | `generateUserVaultKey` | `createUserVaultKey` |
19
19
  | `wrapVaultKeyForPassword` | `createPasswordEnvelope` + profile |
20
- | `unwrapVaultKeyFromPassword` | `unlockWithPasswordEnvelope` |
20
+ | `unwrapVaultKeyFromPassword` | `unlockWithPasswordEnvelope(password, envelope, expectedScope, profile)` |
21
21
  | `wrapVaultKeyForRecoveryPhrase` | `createRecoveryEnvelope` + profile |
22
22
  | `generateRecoveryPhrase` | `createRecoveryPhrase` |
23
23
 
24
24
  LiqSense keeps thin wrappers in `src/modules/vault/core/` binding `LIQSENSE_VAULT_PROFILE`.
25
+ High-level decrypt and unlock calls must also bind the expected scope so authenticated AAD cannot be
26
+ replayed under another user or resource.
25
27
 
26
28
  ## Local dev
27
29
 
@@ -3,7 +3,8 @@
3
3
  - Separate from account passkey login
4
4
  - App provides PRF output bytes (≥ 32 bytes) from WebAuthn ceremony
5
5
  - Package wraps UVK with PRF-derived AES key
6
- - API: `createPasskeyPrfEnvelope` / `unlockWithPasskeyPrfEnvelope`
6
+ - API: `createPasskeyPrfEnvelope(vaultKey, prfOutput, scope, profile)` / `unlockWithPasskeyPrfEnvelope(envelope, prfOutput, expectedScope, profile)`
7
+ - Unlock rejects envelopes whose authenticated AAD does not match the expected scope and profile
7
8
  - Browser helpers: `buildPrfSaltBytes(prefix, userId)`, capability probes
8
9
 
9
10
  PRF output never sent to server. WebAuthn ceremony stays in the app.
@@ -2,6 +2,8 @@
2
2
 
3
3
  - Vault password normalized with NFKC before Argon2id
4
4
  - Default params: memory 65536 KiB, iterations 3, parallelism 1, 32-byte hash, 16-byte salt
5
- - API: `createPasswordEnvelope` / `unlockWithPasswordEnvelope`
5
+ - Persisted Argon2id parameters are bounded before derivation to prevent client-side resource exhaustion
6
+ - API: `createPasswordEnvelope(vaultKey, password, scope, profile)` / `unlockWithPasswordEnvelope(password, envelope, expectedScope, profile)`
7
+ - Unlock rejects envelopes whose authenticated AAD does not match the expected scope and profile
6
8
 
7
9
  Vault password never sent to server.
package/README.md CHANGED
@@ -29,6 +29,26 @@ Build vault-core before consuming:
29
29
  cd ../vault-core && npm run validate
30
30
  ```
31
31
 
32
+ ## Testing
33
+
34
+ ```bash
35
+ npm test
36
+ npm run test:coverage
37
+ ```
38
+
39
+ Coverage is enforced per production file at 90% for statements, branches, functions, and lines.
40
+ `npm run validate` includes the coverage gate.
41
+
42
+ ## Documentation
43
+
44
+ - [Complete implementation guide](docs/IMPLEMENTATION_GUIDE.md)
45
+ - [Documentation index](docs/README.md)
46
+ - [API reference](API_REFERENCE.md)
47
+ - [Security model](SECURITY.md)
48
+ - [Changelog](CHANGELOG.md)
49
+ - [Release process](docs/RELEASING.md)
50
+ - [Agent and contributor guide](AGENTS.md)
51
+
32
52
  ## Quick start
33
53
 
34
54
  ```ts
@@ -57,14 +77,33 @@ const { envelope } = await createPasswordEnvelope(
57
77
  scope,
58
78
  profile
59
79
  );
80
+
81
+ const encryptedPayload = await encryptVaultPayload(
82
+ { version: 1, entries: [] },
83
+ vaultKey,
84
+ scope,
85
+ profile
86
+ );
87
+
88
+ const unlockedKey = await unlockWithPasswordEnvelope(
89
+ userVaultPassword,
90
+ envelope,
91
+ scope,
92
+ profile
93
+ );
94
+
95
+ const payload = await decryptVaultPayload(encryptedPayload, unlockedKey, scope, profile);
60
96
  ```
61
97
 
98
+ High-level decrypt and unlock APIs require the expected scope and profile. This binds authenticated
99
+ AAD to the user, resource, field, and application context expected by the caller.
100
+
62
101
  ## Exports
63
102
 
64
103
  | Entry | Purpose |
65
104
  | --- | --- |
66
105
  | `@tgoliveira/vault-core` | Core crypto, envelopes, payload, validation |
67
- | `@tgoliveira/vault-core/browser` | In-memory session, auto-lock, PRF salt, recovery kit DOM helpers |
106
+ | `@tgoliveira/vault-core/browser` | In-memory session, activity-aware auto-lock, storage inspection, PRF salt, recovery kit DOM helpers |
68
107
  | `@tgoliveira/vault-core/testing` | Sentinels and plaintext scan helpers |
69
108
  | `@tgoliveira/vault-core/react` | Headless React session/status hooks (optional peer: `react`) |
70
109
 
@@ -74,5 +113,6 @@ const { envelope } = await createPasswordEnvelope(
74
113
  - Does **not** require React, Next.js, or product payload schemas on the default entry
75
114
  - `./react` is optional and requires `react >= 18`
76
115
  - Vault password, recovery phrase, UVK, PRF output, and decrypted payload must stay client-side
116
+ - Persisted envelope schemas enforce method-specific KDF metadata at runtime
77
117
 
78
- See `SECURITY.md`, `ARCHITECTURE.md`, and `MIGRATION_FROM_LIQSENSE.md`.
118
+ See `SECURITY.md`, `ARCHITECTURE.md`, `MIGRATION_FROM_LIQSENSE.md`, and [`docs/ADOPTING_VAULT_CORE_IN_EXISTING_APPS.md`](docs/ADOPTING_VAULT_CORE_IN_EXISTING_APPS.md) for migrating other apps (including [letter-to-god](https://github.com/tgoliveira11/letter-to-god)).
@@ -3,7 +3,8 @@
3
3
  - BIP39 English wordlist via `@scure/bip39`
4
4
  - Supported lengths: **12 words** (128-bit) and **24 words** (256-bit, default)
5
5
  - Normalization: trim, lowercase, single-space separated
6
- - Confirmation helpers: deterministic word indices (3 for 12 words, 4 for 24)
6
+ - Confirmation helpers: deterministic word indices (3 for 12 words, 4 for 24); every requested answer is required
7
7
  - Recovery kit text via `createRecoveryKitText({ productName, ... })`
8
+ - Envelope unlock requires the expected scope and profile and validates authenticated AAD before KDF work
8
9
 
9
10
  Recovery phrase never sent to server.
package/SECURITY.md CHANGED
@@ -14,18 +14,38 @@ Vault unlock requires a separate vault password, recovery phrase, or passkey PRF
14
14
  - PRF output
15
15
  - Decrypted vault payload
16
16
 
17
- Use `assertNoVaultPlaintextFields()` on API request bodies.
17
+ Use `assertNoVaultPlaintextFields()` on API request bodies. The guard recursively checks nested
18
+ objects and arrays and safely handles cyclic in-memory objects.
18
19
 
19
20
  ## Client must never persist
20
21
 
21
22
  - Decrypted vault payload in localStorage or IndexedDB
22
23
 
23
- Browser session helpers clear UVK on lock and `pagehide`.
24
+ Browser session helpers clear UVK on lock and `pagehide`. React session helpers also renew the
25
+ inactivity timer on pointer, keyboard, touch, and focus activity by default. Public browser exports
26
+ do not expose direct session-key setters; use `unlockVaultSession()` and `lockVaultSession()` so
27
+ timers and subscribers remain consistent.
28
+
29
+ `inspectLocalStoragePrefix()` and `inspectIndexedDBPrefix()` are namespace inspections, not content
30
+ scanners. They return `"unavailable"` when inspection is blocked or unsupported. Treat that result
31
+ as a failed security check. IndexedDB inspection checks database names and cannot prove that records
32
+ inside an unrelated database contain no plaintext.
24
33
 
25
34
  ## Crypto constants (per app profile)
26
35
 
27
36
  Apps define `VaultCryptoProfile` with stable AAD contexts. Existing ciphertext breaks if contexts change.
28
37
 
38
+ High-level decrypt and envelope-unlock APIs require the expected scope and profile. They reject a
39
+ valid ciphertext when its authenticated AAD belongs to a different user, resource, field, or app
40
+ context. Treat `decryptField()` as a low-level compatibility primitive: callers that use it directly
41
+ must validate the expected AAD separately.
42
+
43
+ ## Untrusted persisted data
44
+
45
+ Treat encrypted payloads, envelopes, AAD, and KDF metadata loaded from a server or local storage as
46
+ untrusted. Argon2id metadata is bounded before derivation to prevent excessive client memory or CPU
47
+ consumption. Do not bypass the high-level APIs or their runtime validation for persisted data.
48
+
29
49
  ## Logging
30
50
 
31
51
  Never log vault secrets, request bodies containing envelopes, or decrypted payloads.
package/dist/browser.d.ts CHANGED
@@ -2,10 +2,21 @@ import { extractPasskeyPrfOutput, isPasskeySupported, isPrfExtensionSupported }
2
2
  export declare function buildPrfSaltBytes(prefix: string, userId: string): Promise<ArrayBuffer>;
3
3
  export declare function createRecoveryKitDownload(content: string, filename: string): void;
4
4
  export declare function printRecoveryKitContent(content: string): void;
5
+ export type VaultStorageInspectionResult = "clear" | "found" | "unavailable";
6
+ export declare function inspectLocalStoragePrefix(storagePrefix: string): VaultStorageInspectionResult;
7
+ export declare function inspectIndexedDBPrefix(storagePrefix: string): Promise<VaultStorageInspectionResult>;
8
+ /**
9
+ * @deprecated This is a namespace-level, fail-closed check. Use inspectLocalStoragePrefix
10
+ * and handle all three result states explicitly.
11
+ */
5
12
  export declare function assertNoDecryptedVaultInLocalStorage(storagePrefix: string): boolean;
13
+ /**
14
+ * @deprecated This checks database names, not record contents. Use inspectIndexedDBPrefix
15
+ * and handle all three result states explicitly.
16
+ */
6
17
  export declare function assertNoDecryptedVaultInIndexedDB(storagePrefix: string): Promise<boolean>;
7
18
  export declare function persistVaultRecordLocally(): never;
8
19
  export { extractPasskeyPrfOutput, isPasskeySupported, isPrfExtensionSupported, };
9
- export { configureVaultSession, subscribeVaultSession, isVaultManuallyLocked, clearVaultAutoLockTimer, scheduleVaultAutoLock, touchVaultSession, unlockVaultSession, lockVaultSession, lockVaultSessionManually, resetVaultSessionLockState, registerVaultUnloadGuard, getVaultAutoLockRemainingMs, getSessionVaultKey, setSessionVaultKey, lockVault, isVaultUnlocked, clearVaultClientState, type VaultSessionConfig, } from "./session/auto-lock.js";
20
+ export { configureVaultSession, subscribeVaultSession, isVaultManuallyLocked, clearVaultAutoLockTimer, scheduleVaultAutoLock, touchVaultSession, unlockVaultSession, lockVaultSession, lockVaultSessionManually, resetVaultSessionLockState, registerVaultUnloadGuard, registerVaultActivityGuard, getVaultAutoLockRemainingMs, getSessionVaultKey, isVaultUnlocked, type VaultSessionConfig, } from "./session/auto-lock.js";
10
21
  export { createRecoveryKitText, buildRecoveryKitContent } from "./recovery/kit.js";
11
22
  //# sourceMappingURL=browser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AACA,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,uBAAuB,EACxB,MAAM,4BAA4B,CAAC;AAEpC,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAG5F;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,IAAI,CASN;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAU7D;AASD,wBAAgB,oCAAoC,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAWnF;AAED,wBAAsB,iCAAiC,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAgB/F;AAED,wBAAgB,yBAAyB,IAAI,KAAK,CAEjD;AAED,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,uBAAuB,GACxB,CAAC;AAEF,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,wBAAwB,EACxB,0BAA0B,EAC1B,wBAAwB,EACxB,2BAA2B,EAC3B,kBAAkB,EAClB,kBAAkB,EAClB,SAAS,EACT,eAAe,EACf,qBAAqB,EACrB,KAAK,kBAAkB,GACxB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AACA,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,uBAAuB,EACxB,MAAM,4BAA4B,CAAC;AAEpC,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAG5F;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,IAAI,CASN;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAU7D;AASD,MAAM,MAAM,4BAA4B,GAAG,OAAO,GAAG,OAAO,GAAG,aAAa,CAAC;AAE7E,wBAAgB,yBAAyB,CACvC,aAAa,EAAE,MAAM,GACpB,4BAA4B,CAiB9B;AAED,wBAAsB,sBAAsB,CAC1C,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,4BAA4B,CAAC,CAwBvC;AAED;;;GAGG;AACH,wBAAgB,oCAAoC,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAEnF;AAED;;;GAGG;AACH,wBAAsB,iCAAiC,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAE/F;AAED,wBAAgB,yBAAyB,IAAI,KAAK,CAEjD;AAED,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,uBAAuB,GACxB,CAAC;AAEF,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,wBAAwB,EACxB,0BAA0B,EAC1B,wBAAwB,EACxB,0BAA0B,EAC1B,2BAA2B,EAC3B,kBAAkB,EAClB,eAAe,EACf,KAAK,kBAAkB,GACxB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/browser.js CHANGED
@@ -32,40 +32,68 @@ function escapeHtml(value) {
32
32
  .replace(/</g, "&lt;")
33
33
  .replace(/>/g, "&gt;");
34
34
  }
35
- export function assertNoDecryptedVaultInLocalStorage(storagePrefix) {
36
- if (typeof window === "undefined")
37
- return true;
38
- for (let i = 0; i < localStorage.length; i++) {
39
- const key = localStorage.key(i);
40
- if (!key)
41
- continue;
42
- if (key.startsWith(storagePrefix)) {
43
- return false;
35
+ export function inspectLocalStoragePrefix(storagePrefix) {
36
+ if (typeof window === "undefined" || typeof localStorage === "undefined") {
37
+ return "unavailable";
38
+ }
39
+ try {
40
+ for (let i = 0; i < localStorage.length; i++) {
41
+ const key = localStorage.key(i);
42
+ if (!key)
43
+ continue;
44
+ if (key.startsWith(storagePrefix)) {
45
+ return "found";
46
+ }
44
47
  }
48
+ return "clear";
49
+ }
50
+ catch {
51
+ return "unavailable";
45
52
  }
46
- return true;
47
53
  }
48
- export async function assertNoDecryptedVaultInIndexedDB(storagePrefix) {
49
- if (typeof window === "undefined" || typeof indexedDB === "undefined")
50
- return true;
54
+ export async function inspectIndexedDBPrefix(storagePrefix) {
55
+ if (typeof window === "undefined" || typeof indexedDB === "undefined") {
56
+ return "unavailable";
57
+ }
51
58
  return new Promise((resolve) => {
52
- const request = indexedDB.databases?.();
59
+ let request;
60
+ try {
61
+ request = indexedDB.databases?.();
62
+ }
63
+ catch {
64
+ resolve("unavailable");
65
+ return;
66
+ }
53
67
  if (!request) {
54
- resolve(true);
68
+ resolve("unavailable");
55
69
  return;
56
70
  }
57
71
  void request
58
72
  .then((databases) => {
59
73
  const hasVaultDb = databases.some((db) => db.name?.startsWith(storagePrefix));
60
- resolve(!hasVaultDb);
74
+ resolve(hasVaultDb ? "found" : "clear");
61
75
  })
62
- .catch(() => resolve(true));
76
+ .catch(() => resolve("unavailable"));
63
77
  });
64
78
  }
79
+ /**
80
+ * @deprecated This is a namespace-level, fail-closed check. Use inspectLocalStoragePrefix
81
+ * and handle all three result states explicitly.
82
+ */
83
+ export function assertNoDecryptedVaultInLocalStorage(storagePrefix) {
84
+ return inspectLocalStoragePrefix(storagePrefix) === "clear";
85
+ }
86
+ /**
87
+ * @deprecated This checks database names, not record contents. Use inspectIndexedDBPrefix
88
+ * and handle all three result states explicitly.
89
+ */
90
+ export async function assertNoDecryptedVaultInIndexedDB(storagePrefix) {
91
+ return (await inspectIndexedDBPrefix(storagePrefix)) === "clear";
92
+ }
65
93
  export function persistVaultRecordLocally() {
66
94
  throw new Error("Decrypted vault state must not be persisted to localStorage or IndexedDB");
67
95
  }
68
96
  export { extractPasskeyPrfOutput, isPasskeySupported, isPrfExtensionSupported, };
69
- export { configureVaultSession, subscribeVaultSession, isVaultManuallyLocked, clearVaultAutoLockTimer, scheduleVaultAutoLock, touchVaultSession, unlockVaultSession, lockVaultSession, lockVaultSessionManually, resetVaultSessionLockState, registerVaultUnloadGuard, getVaultAutoLockRemainingMs, getSessionVaultKey, setSessionVaultKey, lockVault, isVaultUnlocked, clearVaultClientState, } from "./session/auto-lock.js";
97
+ export { configureVaultSession, subscribeVaultSession, isVaultManuallyLocked, clearVaultAutoLockTimer, scheduleVaultAutoLock, touchVaultSession, unlockVaultSession, lockVaultSession, lockVaultSessionManually, resetVaultSessionLockState, registerVaultUnloadGuard, registerVaultActivityGuard, getVaultAutoLockRemainingMs, getSessionVaultKey, isVaultUnlocked, } from "./session/auto-lock.js";
70
98
  export { createRecoveryKitText, buildRecoveryKitContent } from "./recovery/kit.js";
71
99
  //# sourceMappingURL=browser.js.map