@centient/secrets 0.9.0 → 0.11.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 (152) hide show
  1. package/README.md +424 -8
  2. package/dist/audit/hmac-chain.d.ts +117 -0
  3. package/dist/audit/hmac-chain.d.ts.map +1 -0
  4. package/dist/audit/hmac-chain.js +811 -0
  5. package/dist/audit/hmac-chain.js.map +1 -0
  6. package/dist/audit/ocsf.d.ts +108 -0
  7. package/dist/audit/ocsf.d.ts.map +1 -0
  8. package/dist/audit/ocsf.js +204 -0
  9. package/dist/audit/ocsf.js.map +1 -0
  10. package/dist/cli/hidden-prompt.d.ts.map +1 -1
  11. package/dist/cli/hidden-prompt.js +55 -10
  12. package/dist/cli/hidden-prompt.js.map +1 -1
  13. package/dist/cli/messages.d.ts +40 -8
  14. package/dist/cli/messages.d.ts.map +1 -1
  15. package/dist/cli/messages.js +34 -8
  16. package/dist/cli/messages.js.map +1 -1
  17. package/dist/cli/secrets-cli.d.ts.map +1 -1
  18. package/dist/cli/secrets-cli.js +191 -32
  19. package/dist/cli/secrets-cli.js.map +1 -1
  20. package/dist/client.d.ts +26 -0
  21. package/dist/client.d.ts.map +1 -0
  22. package/dist/client.js +129 -0
  23. package/dist/client.js.map +1 -0
  24. package/dist/compatibility.d.ts +199 -0
  25. package/dist/compatibility.d.ts.map +1 -0
  26. package/dist/compatibility.js +419 -0
  27. package/dist/compatibility.js.map +1 -0
  28. package/dist/crypto/darwin-keychain.d.ts +394 -0
  29. package/dist/crypto/darwin-keychain.d.ts.map +1 -0
  30. package/dist/crypto/darwin-keychain.js +793 -0
  31. package/dist/crypto/darwin-keychain.js.map +1 -0
  32. package/dist/crypto/vault-common.d.ts +55 -12
  33. package/dist/crypto/vault-common.d.ts.map +1 -1
  34. package/dist/crypto/vault-common.js +74 -71
  35. package/dist/crypto/vault-common.js.map +1 -1
  36. package/dist/environment/EnvironmentManager.d.ts.map +1 -1
  37. package/dist/environment/EnvironmentManager.js +5 -10
  38. package/dist/environment/EnvironmentManager.js.map +1 -1
  39. package/dist/index.d.ts +37 -9
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +38 -6
  42. package/dist/index.js.map +1 -1
  43. package/dist/key-providers/keychain-provider.d.ts +30 -2
  44. package/dist/key-providers/keychain-provider.d.ts.map +1 -1
  45. package/dist/key-providers/keychain-provider.js +29 -5
  46. package/dist/key-providers/keychain-provider.js.map +1 -1
  47. package/dist/key-providers/onepassword-provider.d.ts +65 -5
  48. package/dist/key-providers/onepassword-provider.d.ts.map +1 -1
  49. package/dist/key-providers/onepassword-provider.js +159 -67
  50. package/dist/key-providers/onepassword-provider.js.map +1 -1
  51. package/dist/key-providers/op-cli.d.ts +78 -0
  52. package/dist/key-providers/op-cli.d.ts.map +1 -0
  53. package/dist/key-providers/op-cli.js +123 -0
  54. package/dist/key-providers/op-cli.js.map +1 -0
  55. package/dist/key-providers/passphrase-provider.d.ts +20 -0
  56. package/dist/key-providers/passphrase-provider.d.ts.map +1 -1
  57. package/dist/key-providers/passphrase-provider.js +49 -0
  58. package/dist/key-providers/passphrase-provider.js.map +1 -1
  59. package/dist/key-providers/probe.d.ts +46 -0
  60. package/dist/key-providers/probe.d.ts.map +1 -0
  61. package/dist/key-providers/probe.js +70 -0
  62. package/dist/key-providers/probe.js.map +1 -0
  63. package/dist/key-providers/types.d.ts +51 -1
  64. package/dist/key-providers/types.d.ts.map +1 -1
  65. package/dist/platform/agent-detect.d.ts +39 -3
  66. package/dist/platform/agent-detect.d.ts.map +1 -1
  67. package/dist/platform/agent-detect.js +55 -9
  68. package/dist/platform/agent-detect.js.map +1 -1
  69. package/dist/platform/session-context.d.ts +77 -0
  70. package/dist/platform/session-context.d.ts.map +1 -0
  71. package/dist/platform/session-context.js +83 -0
  72. package/dist/platform/session-context.js.map +1 -0
  73. package/dist/policies/audit-trail.d.ts +25 -0
  74. package/dist/policies/audit-trail.d.ts.map +1 -0
  75. package/dist/policies/audit-trail.js +31 -0
  76. package/dist/policies/audit-trail.js.map +1 -0
  77. package/dist/secret-state.d.ts +54 -0
  78. package/dist/secret-state.d.ts.map +1 -0
  79. package/dist/secret-state.js +2 -0
  80. package/dist/secret-state.js.map +1 -0
  81. package/dist/sinks/index.d.ts +9 -0
  82. package/dist/sinks/index.d.ts.map +1 -0
  83. package/dist/sinks/index.js +5 -0
  84. package/dist/sinks/index.js.map +1 -0
  85. package/dist/sinks/jsonl.d.ts +16 -0
  86. package/dist/sinks/jsonl.d.ts.map +1 -0
  87. package/dist/sinks/jsonl.js +51 -0
  88. package/dist/sinks/jsonl.js.map +1 -0
  89. package/dist/sinks/otel.d.ts +22 -0
  90. package/dist/sinks/otel.d.ts.map +1 -0
  91. package/dist/sinks/otel.js +155 -0
  92. package/dist/sinks/otel.js.map +1 -0
  93. package/dist/sinks/queue.d.ts +12 -0
  94. package/dist/sinks/queue.d.ts.map +1 -0
  95. package/dist/sinks/queue.js +89 -0
  96. package/dist/sinks/queue.js.map +1 -0
  97. package/dist/sinks/syslog.d.ts +17 -0
  98. package/dist/sinks/syslog.d.ts.map +1 -0
  99. package/dist/sinks/syslog.js +103 -0
  100. package/dist/sinks/syslog.js.map +1 -0
  101. package/dist/sinks/types.d.ts +27 -0
  102. package/dist/sinks/types.d.ts.map +1 -0
  103. package/dist/sinks/types.js +18 -0
  104. package/dist/sinks/types.js.map +1 -0
  105. package/dist/vault/policy.d.ts +64 -34
  106. package/dist/vault/policy.d.ts.map +1 -1
  107. package/dist/vault/policy.js +109 -67
  108. package/dist/vault/policy.js.map +1 -1
  109. package/dist/vault/session-vault-errors.d.ts +72 -4
  110. package/dist/vault/session-vault-errors.d.ts.map +1 -1
  111. package/dist/vault/session-vault-errors.js +95 -4
  112. package/dist/vault/session-vault-errors.js.map +1 -1
  113. package/dist/vault/session-vault.d.ts +145 -2
  114. package/dist/vault/session-vault.d.ts.map +1 -1
  115. package/dist/vault/session-vault.js +548 -72
  116. package/dist/vault/session-vault.js.map +1 -1
  117. package/dist/vault/sidecar.d.ts +9 -0
  118. package/dist/vault/sidecar.d.ts.map +1 -1
  119. package/dist/vault/sidecar.js +72 -3
  120. package/dist/vault/sidecar.js.map +1 -1
  121. package/dist/vault/types.d.ts +58 -10
  122. package/dist/vault/types.d.ts.map +1 -1
  123. package/dist/vault/types.js.map +1 -1
  124. package/dist/vault/vault-env.d.ts +43 -50
  125. package/dist/vault/vault-env.d.ts.map +1 -1
  126. package/dist/vault/vault-env.js +141 -62
  127. package/dist/vault/vault-env.js.map +1 -1
  128. package/dist/vault/vault-gpg.d.ts +6 -1
  129. package/dist/vault/vault-gpg.d.ts.map +1 -1
  130. package/dist/vault/vault-gpg.js +10 -8
  131. package/dist/vault/vault-gpg.js.map +1 -1
  132. package/dist/vault/vault-libsecret.d.ts +6 -1
  133. package/dist/vault/vault-libsecret.d.ts.map +1 -1
  134. package/dist/vault/vault-libsecret.js +16 -14
  135. package/dist/vault/vault-libsecret.js.map +1 -1
  136. package/dist/vault/vault-onepassword.d.ts +119 -0
  137. package/dist/vault/vault-onepassword.d.ts.map +1 -0
  138. package/dist/vault/vault-onepassword.js +271 -0
  139. package/dist/vault/vault-onepassword.js.map +1 -0
  140. package/dist/vault/vault-utils.d.ts +37 -3
  141. package/dist/vault/vault-utils.d.ts.map +1 -1
  142. package/dist/vault/vault-utils.js +65 -3
  143. package/dist/vault/vault-utils.js.map +1 -1
  144. package/dist/vault/vault-windows.d.ts +7 -2
  145. package/dist/vault/vault-windows.d.ts.map +1 -1
  146. package/dist/vault/vault-windows.js +21 -16
  147. package/dist/vault/vault-windows.js.map +1 -1
  148. package/dist/vault/vault.d.ts +120 -9
  149. package/dist/vault/vault.d.ts.map +1 -1
  150. package/dist/vault/vault.js +222 -139
  151. package/dist/vault/vault.js.map +1 -1
  152. package/package.json +8 -2
package/README.md CHANGED
@@ -2,8 +2,15 @@
2
2
 
3
3
  Cross-platform secrets vault with AES-256-GCM encryption and platform-native key storage.
4
4
 
5
+ > **Before you deploy this:** read the [threat model](../../docs/threat-model.md). It states what the library defends against, what it explicitly does **not** defend against (including two gaps that are live on the default path — the shared `centient-vault` Keychain master-key item, and the master key transiting argv on the macOS Keychain key write), where the trust boundaries are, and a deployment checklist.
6
+
5
7
  > **Daemons / long-running processes:** see [Session-backed vault (`openVault`)](./docs/session-vault.md) for the recommended API — single master-key unlock per session, in-memory cached reads, mtime-check coherence with the CLI, rollback protection via monotonic version + sidecar.
6
8
 
9
+ > **Creating a session vault:** use `createVault({ path, key })`. It composes the
10
+ > initial AAD-bound ciphertext and rollback sidecar without exposing their
11
+ > private wire format. It is non-destructive, never manages provider state, and
12
+ > refuses incomplete vault/sidecar pairs. See the [first-use guide](./docs/session-vault.md#first-use-creation).
13
+
7
14
  ## Installation
8
15
 
9
16
  ```bash
@@ -21,23 +28,188 @@ pnpm add @centient/secrets
21
28
  - AES-256-GCM authenticated encryption for secrets at rest
22
29
  - Platform-native key storage (macOS Keychain, Linux secret-service)
23
30
  - Pluggable key providers (Keychain, 1Password, passphrase)
24
- - Credential vault with session management
31
+ - Credential vault with session management, plus an opt-in 1Password credential backend
32
+ - Instance-scoped `SecretsClient` factories with composable policy middleware
25
33
  - Environment detection (CI, Docker, SSH, headless, agent)
26
34
  - Built-in CLI for interactive secret management
27
35
 
28
36
  ## Quick Start
29
37
 
38
+ The client factory is the primary API when a consumer needs an explicit storage
39
+ provider, caller identity, or policies. Each client snapshots its policy-stack
40
+ membership, so two consumers in one process cannot accidentally add or remove
41
+ one another's enforcement. Policy instances may retain their own internal state.
42
+
30
43
  ```typescript
31
- import { storeCredential, getCredential, deleteCredential } from "@centient/secrets";
44
+ import {
45
+ KeychainVault,
46
+ auditTrail,
47
+ createSecretsClient,
48
+ type CredentialAuditEvent,
49
+ } from "@centient/secrets";
50
+
51
+ const auditBuffer: CredentialAuditEvent[] = [];
52
+ const client = createSecretsClient({
53
+ provider: new KeychainVault(),
54
+ policies: [
55
+ auditTrail({
56
+ sink: (event) => auditBuffer.push(event),
57
+ includeReads: true,
58
+ }),
59
+ ],
60
+ });
61
+
62
+ await client.storeCredential("my-service.api-key", "<your-api-key>", {
63
+ caller: { id: "my-service", kind: "service" },
64
+ });
65
+
66
+ const value = await client.getCredential("my-service.api-key", {
67
+ caller: { id: "my-service", kind: "service" },
68
+ });
69
+
70
+ await client.deleteCredential("my-service.api-key", {
71
+ caller: { id: "my-service", kind: "service" },
72
+ });
73
+ ```
32
74
 
33
- // Store a credential
34
- await storeCredential("my-service", "api-key", "sk-abc123");
75
+ `SecretsProvider` is the credential-*value* storage SPI used by the factory.
76
+ The older `VaultBackend` name remains as a deprecated type alias, so existing
77
+ implementations continue to compile. This is separate from `KeyProvider`, which
78
+ locates the master key used to open an encrypted session vault.
35
79
 
36
- // Retrieve it
37
- const value = await getCredential("my-service", "api-key");
80
+ Policies use onion ordering: `before` runs top-to-bottom and `after` runs
81
+ bottom-to-top. A throwing `before` fails closed without contacting the provider;
82
+ the rejecting policy and every previously entered policy still receive the
83
+ rejection event while later policies do not run. Operation metadata includes the
84
+ operation, key or prefix, and optional caller identity, but never the credential
85
+ value. Because `auditTrail()` promises to observe policy denials, place it before
86
+ every policy that has a `before` hook. Client construction and legacy policy
87
+ replacement reject an unsafe order instead of silently leaving denials unaudited.
88
+
89
+ ### Audit telemetry: OpenTelemetry + OCSF
90
+
91
+ Installing an `auditTrail()` policy maps every value-free credential outcome —
92
+ including provider failures and policy denials — to an OCSF 1.9.0 Entity
93
+ Management record. `auditTrail()` defaults to an OpenTelemetry sink; audit is
94
+ not globally enabled merely by importing the package.
95
+
96
+ ```typescript
97
+ import { auditTrail, createSecretsClient } from "@centient/secrets";
38
98
 
39
- // Delete when no longer needed
40
- await deleteCredential("my-service", "api-key");
99
+ const client = createSecretsClient({
100
+ provider,
101
+ policies: [auditTrail()],
102
+ });
103
+ ```
104
+
105
+ `@centient/secrets` depends on `@opentelemetry/api`, not an SDK or exporter.
106
+ The application owns tracer-provider, sampler, exporter, collector, and shutdown
107
+ configuration. The sink reports a non-recording or unsampled span as degraded;
108
+ for an audit pipeline, configure an always-on audit sampler and force-flush the
109
+ application's SDK before shutdown.
110
+
111
+ The released OCSF schema corrects an older ADR-002 label: `3` is the Identity &
112
+ Access Management category and `3004` is the **Entity Management class**; there
113
+ is no released class named Credential Activity. Stores are upserts and
114
+ enumeration has no 3004 activity, so both use `activity_id: 99` (`Store` or
115
+ `Enumerate`) instead of claiming a create/update distinction the provider SPI
116
+ cannot observe. Policy denial is `action_id: 2` with `status_id: 2`.
117
+
118
+ Sink migration: the pre-1.0 audit seam passed its internal `SecretsEvent`
119
+ directly to a callback. A sink now receives `CredentialAuditEvent`. Read the
120
+ credential key from `entity.name`, provider from `entity.data.vault_name`, raw
121
+ event classification from `unmapped["centient.secrets.event_type"]`, and latency
122
+ from `unmapped["centient.secrets.operation.duration_ms"]`. Credential values and
123
+ provider/policy exception text are not copied into the OCSF record.
124
+
125
+ Non-OTel deployments can use the JSONL or RFC 5424 syslog sinks. Both enqueue in
126
+ emission order, bound their queues, report background failures, and expose
127
+ `flush()`/`close()` so a caller can observe delivery failure:
128
+
129
+ ```typescript
130
+ import { auditTrail, createSecretsClient } from "@centient/secrets";
131
+ import { jsonlAuditSink } from "@centient/secrets/sinks";
132
+
133
+ const sink = jsonlAuditSink({ filePath: "/var/log/my-service/secrets.jsonl" });
134
+ const client = createSecretsClient({ provider, policies: [auditTrail({ sink })] });
135
+
136
+ // ...credential operations...
137
+ await sink.flush?.();
138
+ ```
139
+
140
+ JSONL contains bare OCSF records. Syslog serializes the same record as the RFC
141
+ 5424 message body. New JSONL files/directories default to owner-only modes
142
+ (`0o600`/`0o700`); existing permissions are not silently changed. UDP syslog's
143
+ `flush()` confirms local socket acceptance, not remote persistence, and UDP does
144
+ not provide transport encryption or authentication—use a local syslog daemon or
145
+ inject a protected sender for a remote hop. Neither sink routes through
146
+ `@centient/logger`'s
147
+ `AuditWriter` or the SDK's remote `AuditResource`: both existing seams have a
148
+ different closed event schema and async/failure contract. Keeping the
149
+ `AuditSink` boundary structural prevents either package from silently reshaping
150
+ or truncating a signed record.
151
+
152
+ #### HMAC record-integrity chains
153
+
154
+ Create an optional process-local chain with a dedicated 32-byte audit key. The
155
+ consumer must load that key from its deployment trust store and inject it; the
156
+ library never reads a file/environment/provider implicitly, silently generates
157
+ a key, or reuses/derives from the vault-encryption key managed by `KeyProvider`.
158
+
159
+ ```typescript
160
+ import {
161
+ auditTrail,
162
+ createHmacAuditChain,
163
+ createSecretsClient,
164
+ verifyHmacAuditChain,
165
+ } from "@centient/secrets";
166
+ import { jsonlAuditSink } from "@centient/secrets/sinks";
167
+
168
+ const chain = createHmacAuditChain({
169
+ chainId: "my-service-secrets",
170
+ keyId: "audit-key-2026-08",
171
+ key: auditKeyBytes, // exactly 32 bytes, loaded by the application
172
+ });
173
+ const sink = jsonlAuditSink({ filePath: "/var/log/my-service/secrets.jsonl" });
174
+ const client = createSecretsClient({
175
+ provider,
176
+ policies: [auditTrail({ sink, chain })],
177
+ });
178
+
179
+ // Rotate only at an explicit emission boundary; IDs and key material may never be reused.
180
+ chain.rotate({ keyId: "audit-key-2026-09", key: nextAuditKeyBytes });
181
+
182
+ await sink.flush?.();
183
+ const anchor = chain.checkpoint();
184
+ // Persist `anchor` outside the audit log's trust domain.
185
+ ```
186
+
187
+ Each chained event uses OCSF 1.9's Record Integrity profile:
188
+ `metadata.sequence`, a chain UID, the current HMAC fingerprint, and the previous
189
+ event reference. `verifyHmacAuditChain()` accepts a trusted key resolver; bind
190
+ verification to the deployment's trusted `expectedChainId` so another valid
191
+ chain cannot be substituted. The verifier
192
+ returns a discriminated result rather than throwing on hostile records. It
193
+ detects content mutation (including the final event), insertion, duplication,
194
+ internal deletion, reorder, unknown/wrong keys, and invalid rotations. Supply a
195
+ trusted initial checkpoint when verifying a resumed segment and an expected
196
+ final checkpoint to detect prefix/tail deletion; HMAC alone cannot prove that
197
+ an attacker did not delete the whole log. Checkpoints carry chain-scoped,
198
+ HMAC-derived key fingerprints (never key bytes), so historical key material
199
+ cannot be reintroduced under a fresh ID after restart. A global multi-process sequence needs
200
+ an external serialized coordinator—each independent process should otherwise
201
+ use its own `chainId`.
202
+
203
+ The module-level functions remain the simple, backwards-compatible path. They
204
+ delegate through the process's default client and the legacy global policy
205
+ configuration:
206
+
207
+ ```typescript
208
+ import { storeCredential, getCredential, deleteCredential } from "@centient/secrets";
209
+
210
+ await storeCredential("my-service.api-key", "<your-api-key>");
211
+ const value = await getCredential("my-service.api-key");
212
+ await deleteCredential("my-service.api-key");
41
213
  ```
42
214
 
43
215
  ### Encryption Utilities
@@ -60,6 +232,34 @@ if (isCIEnvironment()) {
60
232
  }
61
233
  ```
62
234
 
235
+ ### Auth CLI messages
236
+
237
+ `AUTH_MESSAGES` is the shared, i18n-ready catalog of user-visible strings for
238
+ the credential lifecycle a CLI drives on top of this vault — `login`, `logout`,
239
+ `auth status`, `auth refresh`, device flow, and api-key entry. Import it; do not
240
+ copy it. It is public precisely so a consuming CLI stops maintaining a fork that
241
+ drifts from this one.
242
+
243
+ ```typescript
244
+ import { AUTH_MESSAGES, type AuthMessages } from "@centient/secrets";
245
+
246
+ process.stderr.write(AUTH_MESSAGES.error.vaultWriteFailed + "\n");
247
+ process.stderr.write(AUTH_MESSAGES.info.loginPrompt(verificationUri) + "\n");
248
+ process.stderr.write(AUTH_MESSAGES.warning.tokenExpiringSoon(5) + "\n");
249
+ ```
250
+
251
+ No value interpolates at the call site: fixed messages are string constants and
252
+ parameterized ones are template functions returning the finished string, so a
253
+ translation layer can replace the catalog wholesale. `AuthMessages` is the
254
+ catalog's type, for typing such a table.
255
+
256
+ **Scope.** The catalog covers the auth lifecycle above, plus the one
257
+ credential-storage warning this package itself emits
258
+ (`warning.envVaultNoStorage`, from the read-only `EnvVault` fallback). It does
259
+ **not** cover the `centient secrets …` operator CLI (`runSecrets`) or the
260
+ `[secrets] …` vault diagnostics — those are command-specific and carry their own
261
+ next-step lines, so they stay at their call sites by design.
262
+
63
263
  ## Key Providers
64
264
 
65
265
  | Provider | Platform | Description |
@@ -72,6 +272,100 @@ Provider auto-detection prefers OS-backed storage: 1Password, then Keychain,
72
272
  then passphrase as the last fallback. Set `secrets.provider: "passphrase"` in
73
273
  `~/.centient/config.json` to select it explicitly.
74
274
 
275
+ ## Credential storage backends
276
+
277
+ The **key** layer above decides where the vault *encryption key* lives. This is
278
+ the separate **credential** layer: where secret *values* are stored.
279
+
280
+ | Backend | Platform | Selection |
281
+ |---|---|---|
282
+ | `KeychainVault` | macOS | auto |
283
+ | `WindowsVault` | Windows / WSL | auto |
284
+ | `LibsecretVault` | Linux | auto |
285
+ | `GpgVault` | Linux / WSL | auto |
286
+ | `EnvVault` | Any | auto (last resort) |
287
+ | `OnePasswordVault` | Any (needs `op`) | **explicit opt-in only** |
288
+
289
+ The first five form an auto-cascade, picked by `detect()` in that order.
290
+ `OnePasswordVault` is deliberately **outside** it: having the 1Password CLI
291
+ installed is not consent to route credentials into your personal vault, so it is
292
+ reachable only when you ask for it by name (ADR-004).
293
+
294
+ ```jsonc
295
+ // centient config file
296
+ {
297
+ "secrets": {
298
+ "provider": "keychain", // KEY layer — key stays in the Keychain
299
+ "backend": "1password", // VALUE layer — credentials in 1Password
300
+ "onePasswordBackend": {
301
+ "vault": "centient-credentials", // REQUIRED — no default
302
+ "tag": "centient" // optional
303
+ }
304
+ }
305
+ }
306
+ ```
307
+
308
+ Environment equivalents, which take precedence:
309
+ `CENTIENT_SECRETS_BACKEND=1password` and `CENTIENT_OP_VAULT=<name>`.
310
+
311
+ Two behaviours worth knowing:
312
+
313
+ - **No default vault, and it fails closed.** Unlike the key block (which defaults
314
+ to `Private`), an unset `onePasswordBackend.vault` under an explicit
315
+ `backend: "1password"` is an error. Guessing could write credentials into a
316
+ vault you did not intend.
317
+ - **An explicit choice is never silently substituted.** If you name `1password`
318
+ and `op` turns out to be missing or unauthenticated, startup throws rather than
319
+ quietly falling back to the Keychain — otherwise your secrets would land
320
+ somewhere other than where you said.
321
+
322
+ Secret values are written over **stdin** (`op item create -`), never as argv, so
323
+ they never appear in `ps`. Only key *names* are cached (5s TTL, mirroring the
324
+ Keychain backend); values are never cached, so a rotated or revoked credential is
325
+ never served from memory.
326
+
327
+ **Key constraint.** This backend enforces `isValidKey` (lowercase alphanumeric
328
+ with `-` or `.` separators, 2–64 chars) on every operation, and refuses anything
329
+ else rather than storing it. Reads address the value as
330
+ `op://<vault>/<key>/password`, which is path-structured: a key containing `/`
331
+ would store fine — a 1Password item title is just a string — and then re-parse on
332
+ read into a different item and field, so the write would be silently unreadable.
333
+ Refusing is the better failure; a caller believing a credential is saved when it
334
+ cannot be read back is worse than a caller told no.
335
+
336
+ ### Environment fallback key mapping
337
+
338
+ `EnvVault` is the read-only terminal backend for hosts with no secure credential
339
+ store. Every valid logical credential key has exactly one environment-variable
340
+ name, so a miss means that variable is genuinely unset rather than that the
341
+ backend does not know how to address the key. The historical mapping is retained:
342
+
343
+ | Credential key | Environment variable |
344
+ |---|---|
345
+ | `auth-token` | `ENGRAM_API_KEY` |
346
+ | `sync-peers.dek` | `CENTIENT_SECRET_SYNC_2DPEERS_2EDEK` |
347
+
348
+ For non-auth keys, alphanumerics are uppercased, `-` is escaped as `_2D`, `.` as
349
+ `_2E`, and the result is prefixed with `CENTIENT_SECRET_`. `_` is forbidden by
350
+ the credential-key grammar, so the mapping stays reversible even for adjacent
351
+ separators. Use the exported helper instead of reimplementing it:
352
+
353
+ ```ts
354
+ import { EnvVault, createSecretsClient, credentialKeyToEnvName } from "@centient/secrets";
355
+
356
+ credentialKeyToEnvName("sync-peers.dek");
357
+ // => "CENTIENT_SECRET_SYNC_2DPEERS_2EDEK"
358
+
359
+ const client = createSecretsClient({ provider: new EnvVault() });
360
+ ```
361
+
362
+ An unset variable probes as `absent`. A defined-but-empty variable returns
363
+ `null` on retrieval and probes as `failed`, because zero bytes are not a usable
364
+ credential. `store()` remains read-only and returns `false`; `delete()` remains
365
+ an idempotent no-op. Environment values are unencrypted, inherited by child
366
+ processes, and may be readable through `/proc/<pid>/environ`; this fallback is
367
+ configuration plumbing, not secure storage.
368
+
75
369
  ### Per-consumer vault keys
76
370
 
77
371
  By default `KeychainProvider` targets a single shared Keychain item
@@ -81,6 +375,11 @@ let each consumer use its own key (issue #80). Both are additive — with no
81
375
  options the behaviour is byte-identical to before, and existing vaults keep
82
376
  opening.
83
377
 
378
+ **This is opt-in, and the shared item is still the default.** Until you pass one
379
+ of the options below, anything that can read the `centient-vault` Keychain item
380
+ unlocks every consumer's vault on the machine — see
381
+ [threat model §4.1](../../docs/threat-model.md#41-the-shared-centient-vault-master-key-item-is-still-the-default-issue-80).
382
+
84
383
  **Name your own Keychain item** — the lightweight path. Pass `keychain` to
85
384
  `openVault()` (threaded into internal provider resolution) so your consumer's
86
385
  master key lives under its own Keychain item:
@@ -122,6 +421,13 @@ const vault = await openVault({ keyProvider: stub });
122
421
  A custom provider can also wrap any backend (remote KMS, HSM, env-injected key)
123
422
  as long as it implements the `KeyProvider` interface.
124
423
 
424
+ `KeyProvider` is intentionally separate from `SecretsProvider`, the
425
+ credential-value storage SPI. A key provider establishes or retrieves the one
426
+ master key used to unlock an encrypted session vault; a secrets provider stores
427
+ the individual credential values. The same technology may fill both roles—for
428
+ example, `OnePasswordProvider` and `OnePasswordVault`—but their instances,
429
+ configuration, key spaces, and operation lifecycles remain independent.
430
+
125
431
  ### Passphrase provider
126
432
 
127
433
  For hosts without an OS keychain or 1Password CLI (e.g. a headless Linux box
@@ -153,6 +459,116 @@ Security tradeoffs vs OS-backed providers — choose deliberately:
153
459
  of ms); daemons should call `openVault()` once at startup, before entering
154
460
  their hot loop.
155
461
 
462
+ ## Compatibility floor for consumers
463
+
464
+ Several releases of this package closed defects whose only symptom, at the old
465
+ version, is **silence**: a denied credential operation that leaves no audit
466
+ trace, a keychain write that reports success into a keychain the current
467
+ context's reader cannot see, a malformed key that stores on one backend and
468
+ reads as absent on the next. A consumer running such a version cannot learn
469
+ that from the package — the behaviour looks fine and the logs look clean.
470
+
471
+ So the provider publishes the list. `COMPATIBILITY_FLOOR` is a data table of
472
+ every known behavioural gap, keyed by the version that closed it, and
473
+ `assessCompatibility()` turns an installed version into a grade a `doctor`
474
+ command can print.
475
+
476
+ ```ts
477
+ import { assessCompatibility, SECRETS_PACKAGE_VERSION } from "@centient/secrets";
478
+
479
+ const assessment = assessCompatibility(SECRETS_PACKAGE_VERSION);
480
+
481
+ if (assessment.grade !== "ok") {
482
+ console.warn(assessment.summary);
483
+ for (const gap of assessment.openGaps) {
484
+ console.warn(` [${gap.severity}] ${gap.symptom}`);
485
+ console.warn(` fixed in ${gap.closedIn} (${gap.reference})`);
486
+ if (gap.adoptionNote) console.warn(` on adoption: ${gap.adoptionNote}`);
487
+ }
488
+ }
489
+ ```
490
+
491
+ `SECRETS_PACKAGE_VERSION` is the version of the build you actually imported, so
492
+ the assessment describes the code in your tree rather than whatever is latest on
493
+ the registry. It is kept in step with `package.json` by the release flow and
494
+ asserted by a test, so it cannot drift into a wrong answer.
495
+
496
+ ### The grades
497
+
498
+ | `grade` | Meaning |
499
+ |---|---|
500
+ | `ok` | At or above every known fix. `openGaps` is empty. |
501
+ | `degraded` | Assessable, and one or more known gaps are open. `openGaps` lists them, oldest fix first; `highestSeverity` is the worst. |
502
+ | `unsupported` | Below `MIN_ASSESSABLE_VERSION` — older than the table describes. The gaps it *does* know about are still returned, but the list is not claimed to be complete. |
503
+ | `unknown` | The version could not be parsed. `reason` says why. |
504
+
505
+ ### This is a graded floor, never a boot-time refusal
506
+
507
+ Nothing in this surface runs at import, nothing throws from a constructor, and
508
+ no vault path consults it. Every function is pure, total and advisory:
509
+ malformed input comes back as a typed `unknown` grade carrying the reason, not
510
+ as an exception. A package that refuses to load because its caller is old turns
511
+ a documentation problem into an outage — so grade, report, and let the consumer
512
+ decide.
513
+
514
+ ### Auditing a manifest pin instead of an install
515
+
516
+ `assessCompatibility()` answers "what is open in the build I imported".
517
+ `assessPin()` answers "what could be open in any build this pin admits", by
518
+ grading the lowest version the range allows — which is also what a lockfile
519
+ that has never been refreshed is most likely holding:
520
+
521
+ ```ts
522
+ assessPin("^0.6.0").openGaps.map((g) => g.id);
523
+ ```
524
+
525
+ #### The range grammar `assessPin()` accepts
526
+
527
+ The whole range is matched against an anchored grammar, so an unsupported form
528
+ is rejected *as* unsupported rather than partially interpreted. Exactly four
529
+ forms resolve:
530
+
531
+ | Form | Example | Resolves to |
532
+ |---|---|---|
533
+ | caret | `^0.6.0` | `0.6.0` |
534
+ | tilde | `~0.9.1` | `0.9.1` |
535
+ | inclusive lower bound | `>=0.8.0` | `0.8.0` |
536
+ | exact | `0.10.0` | `0.10.0` |
537
+
538
+ The version component is a full `major.minor.patch`. A conventional leading `v`
539
+ (`v0.5.0`), horizontal whitespace after the operator (`>= 0.8.0`) and around the
540
+ whole range, and a `-prerelease` suffix are all accepted; the pre-release is
541
+ **kept**, because rounding `^1.0.0-rc.1` up to `1.0.0` would credit a consumer
542
+ for a release it is not running.
543
+
544
+ Everything else returns `grade: "unknown"` with a `reason` naming the accepted
545
+ grammar: composite ranges (`>=0.6.0 <0.8.0`), `||` unions, hyphen ranges
546
+ (`1.0.0 - 2.0.0`), wildcards (`*`), x-ranges (`1.x`, `1.2.x`), partial versions
547
+ (`1.2`), upper and exclusive bounds (`<0.9.0`, `>0.9.0`), protocol pins
548
+ (`workspace:*`, `npm:…`, `file:…`, a git URL), build metadata (`^1.2.3+build`,
549
+ which a parser *discards* rather than interprets), and any form carrying a
550
+ trailing token. A guess would grade the wrong version and hand back a clean bill
551
+ of health for a pin nobody actually checked.
552
+
553
+ #### Malformed input is graded, never thrown
554
+
555
+ `assessCompatibility()` and `assessPin()` narrow their argument through the same
556
+ guard before anything parses it, so a JS caller — or a `JSON.parse`d manifest,
557
+ or an `any` at a module boundary — that passes `null`, `undefined`, a number or
558
+ an object gets the documented `grade: "unknown"` result carrying a `reason`,
559
+ never an exception. `installedVersion` and `summary` are strings in that case
560
+ too, so the echo cannot hand back a value its declared type forbids.
561
+
562
+ ### Adding an entry
563
+
564
+ A release earns a row when it closes a defect whose pre-fix symptom is silent.
565
+ A fix a consumer would notice on its own does not need one. Each entry carries
566
+ an `id` (stable — consumers may suppress by it), the `closedIn` version, a
567
+ `severity`, a one-line consumer-facing `symptom`, the `reference` issue, and an
568
+ `adoptionNote` whenever adopting the fix is not a pure no-op. The table is
569
+ ordered oldest fix first; `tests/compatibility.test.ts` pins the ordering, the
570
+ version parity, and the grade each known consumer pin earns.
571
+
156
572
  ## License
157
573
 
158
574
  MIT
@@ -0,0 +1,117 @@
1
+ import type { CredentialAuditEvent, CredentialAuditMetadata, CredentialAuditTypeUid, CredentialAuditUnmapped, OcsfAttestation } from "./ocsf.js";
2
+ export type AuditChainErrorCode = "INVALID_CHAIN_ID" | "INVALID_KEY_ID" | "INVALID_KEY" | "INVALID_CHECKPOINT" | "CHAIN_ID_MISMATCH" | "CHECKPOINT_KEY_MISMATCH" | "KEY_ID_REUSE" | "KEY_MATERIAL_REUSE" | "EVENT_ALREADY_CHAINED" | "SEQUENCE_EXHAUSTED" | "CHAIN_DESTROYED" | "CANONICALIZATION_FAILED";
3
+ /** Configuration/state errors produced by the writer side of the chain. */
4
+ export declare class AuditChainError extends Error {
5
+ readonly code: AuditChainErrorCode;
6
+ constructor(code: AuditChainErrorCode, message: string);
7
+ }
8
+ export interface AuditChainKey {
9
+ /** Stable, non-secret key version identifier. Never reuse an ID. */
10
+ readonly keyId: string;
11
+ /** Exactly 32 bytes. The chain copies the bytes and never owns this input. */
12
+ readonly key: Uint8Array;
13
+ }
14
+ /**
15
+ * Value-free state needed to resume or externally anchor a chain.
16
+ *
17
+ * A checkpoint is trustworthy only when the caller stores it outside the
18
+ * audit log's own trust domain. Supplying a checkpoint read from the same
19
+ * mutable log cannot prove that a prefix or suffix was not deleted.
20
+ */
21
+ export interface AuditChainCheckpoint {
22
+ readonly chainId: string;
23
+ readonly sequenceNumber: number;
24
+ readonly eventUid: string;
25
+ readonly eventHash: string;
26
+ readonly typeUid: CredentialAuditTypeUid;
27
+ readonly keyId: string;
28
+ /** Ordered key IDs already used by this chain, including `keyId`. */
29
+ readonly usedKeyIds: readonly string[];
30
+ /**
31
+ * Chain-scoped, HMAC-derived key identifiers aligned with `usedKeyIds`.
32
+ * These are not key bytes; they let a trusted checkpoint prevent historical
33
+ * key material from being reintroduced under a fresh ID after restart.
34
+ */
35
+ readonly usedKeyFingerprints: readonly string[];
36
+ }
37
+ export type ChainedCredentialAuditEvent = CredentialAuditEvent & {
38
+ readonly metadata: CredentialAuditMetadata & {
39
+ readonly sequence: number;
40
+ readonly profiles: readonly ["record_integrity"];
41
+ };
42
+ readonly unmapped: CredentialAuditUnmapped & {
43
+ readonly "centient.audit_chain.key_id": string;
44
+ };
45
+ readonly attestation_list: readonly [OcsfAttestation];
46
+ };
47
+ export interface CreateHmacAuditChainOptions extends AuditChainKey {
48
+ readonly chainId: string;
49
+ /** Trusted external state from the last durable event, if resuming. */
50
+ readonly checkpoint?: AuditChainCheckpoint;
51
+ }
52
+ export interface HmacAuditChain {
53
+ /** Append and sign one previously unchained OCSF event synchronously. */
54
+ append(event: CredentialAuditEvent): ChainedCredentialAuditEvent;
55
+ /**
56
+ * Switch the signing key at an emission boundary.
57
+ *
58
+ * The first later event uses the new key ID while linking to the last event
59
+ * produced with the old key. IDs and key material can never be reused within
60
+ * one chain, including after resuming from a trusted checkpoint.
61
+ */
62
+ rotate(next: AuditChainKey): void;
63
+ /** Latest value-free state, or `null` before the first event. */
64
+ checkpoint(): AuditChainCheckpoint | null;
65
+ /** Zero the chain's private key copy and permanently close the writer. */
66
+ destroy(): void;
67
+ }
68
+ /**
69
+ * RFC 8785 JSON Canonicalization Scheme for the JSON data model used by OCSF.
70
+ *
71
+ * Objects are plain data objects, names are ordered by UTF-16 code units, and
72
+ * strings/numbers use ECMAScript JSON serialization. Values outside I-JSON
73
+ * (cycles, sparse arrays, accessors, non-finite numbers, lone surrogates,
74
+ * `undefined`, `bigint`, functions, symbols) fail closed.
75
+ */
76
+ export declare function canonicalizeAuditEvent(value: unknown): string;
77
+ /** Create one synchronous, process-local HMAC-SHA-256 audit chain. */
78
+ export declare function createHmacAuditChain(options: CreateHmacAuditChainOptions): HmacAuditChain;
79
+ export type AuditChainVerificationFailureCode = "MALFORMED_EVENT" | "UNSUPPORTED_ALGORITHM" | "UNSUPPORTED_CANONICALIZATION" | "UNANCHORED_SEGMENT" | "CHAIN_ID_MISMATCH" | "INVALID_SEQUENCE" | "SEQUENCE_GAP_OR_REORDER" | "PREVIOUS_EVENT_MISMATCH" | "PREVIOUS_HASH_MISMATCH" | "UNKNOWN_KEY_ID" | "KEY_RESOLUTION_FAILED" | "INVALID_KEY" | "KEY_ID_REUSE" | "KEY_ID_MATERIAL_MISMATCH" | "KEY_MATERIAL_REUSE" | "HASH_MISMATCH" | "FINAL_CHECKPOINT_MISMATCH";
80
+ export interface AuditChainVerificationFailure {
81
+ readonly code: AuditChainVerificationFailureCode;
82
+ readonly message: string;
83
+ readonly index: number;
84
+ readonly sequenceNumber?: number;
85
+ }
86
+ export type AuditChainVerificationResult = {
87
+ readonly ok: true;
88
+ readonly value: {
89
+ readonly verifiedCount: number;
90
+ readonly rotations: number;
91
+ readonly checkpoint: AuditChainCheckpoint | null;
92
+ };
93
+ } | {
94
+ readonly ok: false;
95
+ readonly error: AuditChainVerificationFailure;
96
+ };
97
+ export interface VerifyAuditChainOptions {
98
+ /** Trusted key lookup. Returned key bytes remain owned by the resolver. */
99
+ readonly resolveKey: (keyId: string) => Uint8Array | undefined;
100
+ /** Expected chain identity, required to reject whole-chain substitution. */
101
+ readonly expectedChainId?: string;
102
+ /** Required when verification starts after the genesis event. */
103
+ readonly initialCheckpoint?: AuditChainCheckpoint;
104
+ /** External terminal anchor used to detect deletion of a chain suffix. */
105
+ readonly expectedFinalCheckpoint?: AuditChainCheckpoint;
106
+ }
107
+ /**
108
+ * Verify one ordered chain segment without mutating records or key material.
109
+ *
110
+ * The function returns a discriminated failure for hostile input. It requires
111
+ * genesis (`sequence=1`, no `prev_event`) unless a trusted initial checkpoint
112
+ * anchors a later segment. An optional trusted final checkpoint detects suffix
113
+ * deletion; no hash-chain algorithm can detect deletion of the entire log by
114
+ * looking only at that log.
115
+ */
116
+ export declare function verifyHmacAuditChain(events: readonly unknown[], options: VerifyAuditChainOptions): AuditChainVerificationResult;
117
+ //# sourceMappingURL=hmac-chain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hmac-chain.d.ts","sourceRoot":"","sources":["../../src/audit/hmac-chain.ts"],"names":[],"mappings":"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"}