@centient/secrets 0.10.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 (99) hide show
  1. package/README.md +355 -7
  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/client.d.ts +26 -0
  18. package/dist/client.d.ts.map +1 -0
  19. package/dist/client.js +129 -0
  20. package/dist/client.js.map +1 -0
  21. package/dist/compatibility.d.ts +199 -0
  22. package/dist/compatibility.d.ts.map +1 -0
  23. package/dist/compatibility.js +419 -0
  24. package/dist/compatibility.js.map +1 -0
  25. package/dist/index.d.ts +19 -5
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +16 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/policies/audit-trail.d.ts +25 -0
  30. package/dist/policies/audit-trail.d.ts.map +1 -0
  31. package/dist/policies/audit-trail.js +31 -0
  32. package/dist/policies/audit-trail.js.map +1 -0
  33. package/dist/sinks/index.d.ts +9 -0
  34. package/dist/sinks/index.d.ts.map +1 -0
  35. package/dist/sinks/index.js +5 -0
  36. package/dist/sinks/index.js.map +1 -0
  37. package/dist/sinks/jsonl.d.ts +16 -0
  38. package/dist/sinks/jsonl.d.ts.map +1 -0
  39. package/dist/sinks/jsonl.js +51 -0
  40. package/dist/sinks/jsonl.js.map +1 -0
  41. package/dist/sinks/otel.d.ts +22 -0
  42. package/dist/sinks/otel.d.ts.map +1 -0
  43. package/dist/sinks/otel.js +155 -0
  44. package/dist/sinks/otel.js.map +1 -0
  45. package/dist/sinks/queue.d.ts +12 -0
  46. package/dist/sinks/queue.d.ts.map +1 -0
  47. package/dist/sinks/queue.js +89 -0
  48. package/dist/sinks/queue.js.map +1 -0
  49. package/dist/sinks/syslog.d.ts +17 -0
  50. package/dist/sinks/syslog.d.ts.map +1 -0
  51. package/dist/sinks/syslog.js +103 -0
  52. package/dist/sinks/syslog.js.map +1 -0
  53. package/dist/sinks/types.d.ts +27 -0
  54. package/dist/sinks/types.d.ts.map +1 -0
  55. package/dist/sinks/types.js +18 -0
  56. package/dist/sinks/types.js.map +1 -0
  57. package/dist/vault/policy.d.ts +64 -34
  58. package/dist/vault/policy.d.ts.map +1 -1
  59. package/dist/vault/policy.js +109 -67
  60. package/dist/vault/policy.js.map +1 -1
  61. package/dist/vault/session-vault.d.ts +43 -0
  62. package/dist/vault/session-vault.d.ts.map +1 -1
  63. package/dist/vault/session-vault.js +323 -36
  64. package/dist/vault/session-vault.js.map +1 -1
  65. package/dist/vault/sidecar.d.ts +9 -0
  66. package/dist/vault/sidecar.d.ts.map +1 -1
  67. package/dist/vault/sidecar.js +72 -3
  68. package/dist/vault/sidecar.js.map +1 -1
  69. package/dist/vault/types.d.ts +16 -9
  70. package/dist/vault/types.d.ts.map +1 -1
  71. package/dist/vault/vault-env.d.ts +41 -62
  72. package/dist/vault/vault-env.d.ts.map +1 -1
  73. package/dist/vault/vault-env.js +139 -75
  74. package/dist/vault/vault-env.js.map +1 -1
  75. package/dist/vault/vault-gpg.d.ts +1 -0
  76. package/dist/vault/vault-gpg.d.ts.map +1 -1
  77. package/dist/vault/vault-gpg.js +1 -0
  78. package/dist/vault/vault-gpg.js.map +1 -1
  79. package/dist/vault/vault-libsecret.d.ts +1 -0
  80. package/dist/vault/vault-libsecret.d.ts.map +1 -1
  81. package/dist/vault/vault-libsecret.js +7 -6
  82. package/dist/vault/vault-libsecret.js.map +1 -1
  83. package/dist/vault/vault-onepassword.d.ts +1 -0
  84. package/dist/vault/vault-onepassword.d.ts.map +1 -1
  85. package/dist/vault/vault-onepassword.js +1 -0
  86. package/dist/vault/vault-onepassword.js.map +1 -1
  87. package/dist/vault/vault-utils.d.ts +7 -6
  88. package/dist/vault/vault-utils.d.ts.map +1 -1
  89. package/dist/vault/vault-utils.js +8 -7
  90. package/dist/vault/vault-utils.js.map +1 -1
  91. package/dist/vault/vault-windows.d.ts +2 -1
  92. package/dist/vault/vault-windows.d.ts.map +1 -1
  93. package/dist/vault/vault-windows.js +12 -8
  94. package/dist/vault/vault-windows.js.map +1 -1
  95. package/dist/vault/vault.d.ts +6 -7
  96. package/dist/vault/vault.d.ts.map +1 -1
  97. package/dist/vault/vault.js +37 -193
  98. package/dist/vault/vault.js.map +1 -1
  99. package/package.json +8 -2
package/README.md CHANGED
@@ -6,6 +6,11 @@ Cross-platform secrets vault with AES-256-GCM encryption and platform-native key
6
6
 
7
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.
8
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
+
9
14
  ## Installation
10
15
 
11
16
  ```bash
@@ -24,22 +29,187 @@ pnpm add @centient/secrets
24
29
  - Platform-native key storage (macOS Keychain, Linux secret-service)
25
30
  - Pluggable key providers (Keychain, 1Password, passphrase)
26
31
  - Credential vault with session management, plus an opt-in 1Password credential backend
32
+ - Instance-scoped `SecretsClient` factories with composable policy middleware
27
33
  - Environment detection (CI, Docker, SSH, headless, agent)
28
34
  - Built-in CLI for interactive secret management
29
35
 
30
36
  ## Quick Start
31
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
+
32
43
  ```typescript
33
- 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
+ ```
74
+
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.
79
+
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";
98
+
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
+ ```
34
139
 
35
- // Store a credential
36
- await storeCredential("my-service", "api-key", "<your-api-key>");
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`.
37
158
 
38
- // Retrieve it
39
- const value = await getCredential("my-service", "api-key");
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
+ ```
40
186
 
41
- // Delete when no longer needed
42
- await deleteCredential("my-service", "api-key");
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");
43
213
  ```
44
214
 
45
215
  ### Encryption Utilities
@@ -62,6 +232,34 @@ if (isCIEnvironment()) {
62
232
  }
63
233
  ```
64
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
+
65
263
  ## Key Providers
66
264
 
67
265
  | Provider | Platform | Description |
@@ -135,6 +333,39 @@ read into a different item and field, so the write would be silently unreadable.
135
333
  Refusing is the better failure; a caller believing a credential is saved when it
136
334
  cannot be read back is worse than a caller told no.
137
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
+
138
369
  ### Per-consumer vault keys
139
370
 
140
371
  By default `KeychainProvider` targets a single shared Keychain item
@@ -190,6 +421,13 @@ const vault = await openVault({ keyProvider: stub });
190
421
  A custom provider can also wrap any backend (remote KMS, HSM, env-injected key)
191
422
  as long as it implements the `KeyProvider` interface.
192
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
+
193
431
  ### Passphrase provider
194
432
 
195
433
  For hosts without an OS keychain or 1Password CLI (e.g. a headless Linux box
@@ -221,6 +459,116 @@ Security tradeoffs vs OS-backed providers — choose deliberately:
221
459
  of ms); daemons should call `openVault()` once at startup, before entering
222
460
  their hot loop.
223
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
+
224
572
  ## License
225
573
 
226
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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"}