@blamejs/core 0.8.43 → 0.8.49

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 (222) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
package/lib/audit-sign.js CHANGED
@@ -1,60 +1,62 @@
1
1
  "use strict";
2
2
  /**
3
- * Audit signing key — separate PQC keypair for periodic checkpoint
4
- * signatures over the audit chain.
5
- *
6
- * Algorithm: SLH-DSA-SHAKE-256f (FIPS 205) by default. ML-DSA-87
7
- * (FIPS 204) supported as an opt-in alternative for throughput-
8
- * sensitive deployments. Both are NIST PQC Category 5 (~256-bit
9
- * symmetric security). SLH-DSA-SHAKE-256f is hash-only — its
10
- * security depends solely on the underlying hash function, with no
11
- * lattice / module-hardness assumptions and matches the framework's
12
- * SHAKE256 KDF + SHA3-512 hash family. Audit checkpoints are long-
13
- * lived integrity attestations (must verify for the data retention
14
- * period years for HIPAA / SOX), so the conservative-PQC posture
15
- * carries more weight here than the smaller ML-DSA-87 signature
16
- * (~5 KB) and faster sign (0.6 ms vs 76 ms).
17
- *
18
- * The algorithm is recorded in the on-disk key file's `algorithm`
19
- * field. Older key files that predate the algorithm field are loaded
20
- * as ML-DSA-87 (the previous implicit default) and continue to verify
21
- * their checkpoint history. Operators who want to migrate rotate
22
- * their audit-signing key.
23
- *
24
- * Design:
25
- * - Different keypair from the vault encryption keys. Compromise of the
26
- * vault DOES NOT let an attacker forge audit checkpoints.
27
- * - Stored at <dataDir>/audit-sign.key.sealed (default 'wrapped' mode)
28
- * or <dataDir>/audit-sign.key (opt-out 'plaintext' mode with warning).
29
- * - Wrapped under its OWN passphrase, sourced via:
30
- * BLAMEJS_AUDIT_SIGNING_PASSPHRASE (env)
31
- * BLAMEJS_AUDIT_SIGNING_PASSPHRASE_FILE (file)
32
- * BLAMEJS_AUDIT_SIGNING_PASSPHRASE_SOURCE (selector: auto|env|file|stdin)
33
- * These are intentionally distinct from BLAMEJS_VAULT_PASSPHRASE so
34
- * operator-error reuse of the same passphrase is at least explicit.
35
- * - First-run generates the keypair automatically.
36
- *
37
- * Threat model:
38
- * - Vault key compromised + DB write access:
39
- * attacker can read sealed values + rewrite audit_log rows + recompute
40
- * per-row chain hashes. They CANNOT forge new audit_checkpoint rows
41
- * because each checkpoint requires the audit-signing private key.
42
- * - Audit signing key compromised:
43
- * attacker can forge new checkpoints but cannot read sealed values.
44
- * Existing checkpoints still anchor history that pre-dated the
45
- * compromise (operator should rotate signing key on detection).
46
- * - Both compromised:
47
- * framework cannot defend against this by design, the operator's
48
- * physical / administrative controls (HIPAA §164.310, GDPR Art. 32(1)(d))
49
- * cover this case.
50
- *
51
- * Public API:
52
- * await auditSign.init({ dataDir, mode? }) ← call at db.init()
53
- * auditSign.sign(payload) ← Buffer/string Buffer signature
54
- * auditSign.verify(payload, signature, publicKey?) bool
55
- * auditSign.getPublicKey() ← PEM string
56
- * auditSign.getPublicKeyFingerprint() sha3 hex (stable id)
57
- * auditSign.getMode() ← 'wrapped' | 'plaintext'
3
+ * @module b.auditSign
4
+ * @nav Crypto
5
+ * @title Audit Signing
6
+ *
7
+ * @intro
8
+ * SLH-DSA-SHAKE-256f post-quantum signature for audit-chain
9
+ * checkpoints. Wrapped vs plaintext on-disk modes, key derivation
10
+ * from an operator passphrase, periodic checkpoint sign / verify,
11
+ * multiple-key support so a key rotation doesn't strand history.
12
+ *
13
+ * Algorithm: SLH-DSA-SHAKE-256f (FIPS 205) by default. ML-DSA-87
14
+ * (FIPS 204 Category 5) and ML-DSA-65 (FIPS 204 Category 3, ~192-bit
15
+ * symmetric security, smaller signatures + faster verify than 87)
16
+ * ship as opt-in alternatives for throughput-sensitive deployments.
17
+ * SLH-DSA-SHAKE-256f is hash-only — its security depends solely on
18
+ * the underlying hash function, with no lattice / module-hardness
19
+ * assumptions and matches the framework's SHAKE256 KDF + SHA3-512
20
+ * hash family. Audit checkpoints are long-lived integrity
21
+ * attestations (must verify for the data retention period years
22
+ * for HIPAA / SOX), so the conservative-PQC posture carries more
23
+ * weight here than the smaller ML-DSA signatures (~5 KB at 87,
24
+ * ~3.3 KB at 65) and faster sign (~0.6 ms vs 76 ms).
25
+ *
26
+ * The algorithm is recorded in the on-disk key file's `algorithm`
27
+ * field. The framework refuses to load a key file that lacks it.
28
+ * Operators upgrading the algorithm rotate their audit-signing key
29
+ * via `b.auditSign.rotateSigningKey({ algorithm })`.
30
+ *
31
+ * Design:
32
+ * - Different keypair from the vault encryption keys. Compromise
33
+ * of the vault DOES NOT let an attacker forge audit checkpoints.
34
+ * - Stored at <dataDir>/audit-sign.key.sealed (default 'wrapped'
35
+ * mode) or <dataDir>/audit-sign.key (opt-out 'plaintext' mode
36
+ * with warning).
37
+ * - Wrapped under its OWN passphrase, sourced via:
38
+ * BLAMEJS_AUDIT_SIGNING_PASSPHRASE (env)
39
+ * BLAMEJS_AUDIT_SIGNING_PASSPHRASE_FILE (file)
40
+ * BLAMEJS_AUDIT_SIGNING_PASSPHRASE_SOURCE (auto|env|file|stdin)
41
+ * Intentionally distinct from BLAMEJS_VAULT_PASSPHRASE so
42
+ * operator-error reuse of the same passphrase is explicit.
43
+ * - First-run generates the keypair automatically.
44
+ *
45
+ * Threat model:
46
+ * - Vault key compromised + DB write access: attacker can read
47
+ * sealed values + rewrite audit_log rows + recompute per-row
48
+ * chain hashes. They CANNOT forge new audit_checkpoint rows
49
+ * each checkpoint requires the audit-signing private key.
50
+ * - Audit signing key compromised: attacker can forge new
51
+ * checkpoints but cannot read sealed values. Existing
52
+ * checkpoints still anchor history that pre-dated the compromise
53
+ * (operator should rotate signing key on detection).
54
+ * - Both compromised: framework cannot defend against this — the
55
+ * operator's physical / administrative controls (HIPAA §164.310,
56
+ * GDPR Art. 32(1)(d)) cover this case.
57
+ *
58
+ * @card
59
+ * SLH-DSA-SHAKE-256f post-quantum signature for audit-chain checkpoints.
58
60
  */
59
61
  var fs = require("fs");
60
62
  var path = require("path");
@@ -82,7 +84,13 @@ var _err = AuditSignError.factory;
82
84
  // that lacks it. The legacy implicit-default-to-ml-dsa-87 fallback was
83
85
  // removed as part of the pre-v1 compat-shim sweep.
84
86
  var DEFAULT_SIGNING_ALG = "slh-dsa-shake-256f";
85
- var SUPPORTED_SIGNING_ALGS = Object.freeze(["slh-dsa-shake-256f", "ml-dsa-87"]);
87
+ // ml-dsa-65 (FIPS 204 Category 3, ~192-bit symmetric security) is opt-
88
+ // in alongside ml-dsa-87 — same code path (both auto-detected by
89
+ // node:crypto from the PEM), smaller signatures (~3.3 KB vs ~5 KB at
90
+ // 87 / ~29.5 KB at SLH-DSA-SHAKE-256f), faster verify. Operators with
91
+ // throughput-sensitive checkpoint streams or audit-feed shippers
92
+ // elect ml-dsa-65 explicitly via opts.algorithm.
93
+ var SUPPORTED_SIGNING_ALGS = Object.freeze(["slh-dsa-shake-256f", "ml-dsa-87", "ml-dsa-65"]);
86
94
 
87
95
  var SIGNING_KEY_SCHEMA = {
88
96
  type: "object",
@@ -136,6 +144,37 @@ function _getPassphrase(promptText) {
136
144
  // files take their algorithm from the file itself, ignoring this.
137
145
  var pendingNewKeyAlg = null;
138
146
 
147
+ /**
148
+ * @primitive b.auditSign.init
149
+ * @signature b.auditSign.init(opts)
150
+ * @since 0.1.0
151
+ * @status stable
152
+ * @compliance hipaa, pci-dss, gdpr, soc2, sox-404
153
+ * @related b.auditSign.sign, b.auditSign.verify, b.auditSign.rotateSigningKey
154
+ *
155
+ * Boot the audit-signing keypair. Called once during `b.db.init()`;
156
+ * later calls are no-ops. First run generates a fresh PQC keypair and
157
+ * either seals it under an operator passphrase ('wrapped' mode,
158
+ * default) or writes it plaintext at 0600 ('plaintext' mode, opt-out
159
+ * with stderr warning). Subsequent boots load the existing key file
160
+ * and refuse if both wrapped + plaintext copies exist on disk
161
+ * (KEY_FILE_CONFLICT) or the on-disk mode disagrees with `opts.mode`
162
+ * (MODE_MISMATCH).
163
+ *
164
+ * @opts
165
+ * dataDir: string, // required — directory holding the key file
166
+ * mode: "wrapped" | "plaintext", // default "wrapped"
167
+ * algorithm: "slh-dsa-shake-256f" | "ml-dsa-87" | "ml-dsa-65" // default "slh-dsa-shake-256f"; only consulted when generating a fresh key
168
+ *
169
+ * @example
170
+ * await b.auditSign.init({
171
+ * dataDir: "/var/lib/blamejs/data",
172
+ * mode: "wrapped",
173
+ * algorithm: "slh-dsa-shake-256f",
174
+ * });
175
+ * b.auditSign.getMode(); // → "wrapped"
176
+ * b.auditSign.getAlgorithm(); // → "slh-dsa-shake-256f"
177
+ */
139
178
  async function init(opts) {
140
179
  if (initialized) return;
141
180
  if (!opts || !opts.dataDir) {
@@ -307,12 +346,66 @@ function _requireInit() {
307
346
  }
308
347
  }
309
348
 
349
+ /**
350
+ * @primitive b.auditSign.sign
351
+ * @signature b.auditSign.sign(payload)
352
+ * @since 0.1.0
353
+ * @status stable
354
+ * @compliance hipaa, pci-dss, gdpr, soc2, sox-404
355
+ * @related b.auditSign.verify, b.audit.checkpoint
356
+ *
357
+ * Sign a payload (Buffer or string) with the in-memory PQC private
358
+ * key. Returns the raw signature bytes as a Buffer. Throws if `init()`
359
+ * has not been awaited. Used by `b.audit.checkpoint()` to anchor the
360
+ * chain tip; operators normally don't call it directly.
361
+ *
362
+ * @example
363
+ * await b.auditSign.init({ dataDir: "/var/lib/blamejs/data" });
364
+ *
365
+ * // Sign a chain checkpoint payload (the audit module passes the
366
+ * // chain tip's row hash + monotonic counter as canonical bytes).
367
+ * var tip = { rowHash: "9f4e2c3a", counter: 1042 };
368
+ * var payload = Buffer.from(JSON.stringify(tip), "utf8");
369
+ * var signature = b.auditSign.sign(payload);
370
+ * // → <Buffer ...> roughly 29.5 KB for SLH-DSA-SHAKE-256f
371
+ */
310
372
  function sign(payload) {
311
373
  _requireInit();
312
374
  var buf = Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload), "utf8");
313
375
  return nodeCrypto.sign(null, buf, keys.privateKey);
314
376
  }
315
377
 
378
+ /**
379
+ * @primitive b.auditSign.verify
380
+ * @signature b.auditSign.verify(payload, signature, publicKeyPem)
381
+ * @since 0.1.0
382
+ * @status stable
383
+ * @compliance hipaa, pci-dss, gdpr, soc2, sox-404
384
+ * @related b.auditSign.sign, b.audit.verifyCheckpoints
385
+ *
386
+ * Verify a signature against the supplied (or current) public key.
387
+ * Returns `true` when the signature is valid, `false` otherwise; never
388
+ * throws on a forgery — callers branch on the boolean. The third
389
+ * argument lets verification use a HISTORICAL key (read from
390
+ * `audit-sign.key.sealed.history-*`) so a checkpoint signed years
391
+ * earlier still verifies after rotation.
392
+ *
393
+ * @example
394
+ * await b.auditSign.init({ dataDir: "/var/lib/blamejs/data" });
395
+ *
396
+ * // Re-walk every checkpoint to confirm chain integrity.
397
+ * var tip = { rowHash: "9f4e2c3a", counter: 1042 };
398
+ * var payload = Buffer.from(JSON.stringify(tip), "utf8");
399
+ * var signature = b.auditSign.sign(payload);
400
+ *
401
+ * var ok = b.auditSign.verify(payload, signature);
402
+ * // → true
403
+ *
404
+ * // A historical checkpoint signed under an old key:
405
+ * var oldPubPem = "-----BEGIN PUBLIC KEY-----\nMII...\n-----END PUBLIC KEY-----";
406
+ * b.auditSign.verify(payload, signature, oldPubPem);
407
+ * // → true (when payload + signature were produced under that key)
408
+ */
316
409
  function verify(payload, signature, publicKeyPem) {
317
410
  _requireInit();
318
411
  var buf = Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload), "utf8");
@@ -321,9 +414,80 @@ function verify(payload, signature, publicKeyPem) {
321
414
  return nodeCrypto.verify(null, buf, pub, sigBuf);
322
415
  }
323
416
 
417
+ /**
418
+ * @primitive b.auditSign.getPublicKey
419
+ * @signature b.auditSign.getPublicKey()
420
+ * @since 0.1.0
421
+ * @status stable
422
+ * @related b.auditSign.getPublicKeyFingerprint, b.auditSign.verify
423
+ *
424
+ * Return the in-memory public key as a SPKI PEM string. Operators
425
+ * publish this so external auditors can verify checkpoint signatures
426
+ * without holding any private material.
427
+ *
428
+ * @example
429
+ * await b.auditSign.init({ dataDir: "/var/lib/blamejs/data" });
430
+ * var pem = b.auditSign.getPublicKey();
431
+ * // → "-----BEGIN PUBLIC KEY-----\nMII...\n-----END PUBLIC KEY-----\n"
432
+ */
324
433
  function getPublicKey() { _requireInit(); return keys.publicKey; }
434
+
435
+ /**
436
+ * @primitive b.auditSign.getPublicKeyFingerprint
437
+ * @signature b.auditSign.getPublicKeyFingerprint()
438
+ * @since 0.1.0
439
+ * @status stable
440
+ * @related b.auditSign.getPublicKey, b.auditSign.rotateSigningKey
441
+ *
442
+ * Return the SHA3-512 fingerprint of the public key as a lowercase
443
+ * hex string. Stable across boots for the same keypair; a different
444
+ * fingerprint after `rotateSigningKey()` is the signal that the
445
+ * rotation actually changed material.
446
+ *
447
+ * @example
448
+ * await b.auditSign.init({ dataDir: "/var/lib/blamejs/data" });
449
+ * var fp = b.auditSign.getPublicKeyFingerprint();
450
+ * // → "9f4e2c3a..." (128 hex chars, SHA3-512)
451
+ */
325
452
  function getPublicKeyFingerprint() { _requireInit(); return keys.fingerprint; }
453
+
454
+ /**
455
+ * @primitive b.auditSign.getMode
456
+ * @signature b.auditSign.getMode()
457
+ * @since 0.1.0
458
+ * @status stable
459
+ * @related b.auditSign.init
460
+ *
461
+ * Return the on-disk storage mode chosen at `init()` — `"wrapped"`
462
+ * (passphrase-sealed, default) or `"plaintext"` (0600 file, opt-out).
463
+ * Returns `null` before `init()` runs.
464
+ *
465
+ * @example
466
+ * await b.auditSign.init({ dataDir: "/var/lib/blamejs/data" });
467
+ * b.auditSign.getMode();
468
+ * // → "wrapped"
469
+ */
326
470
  function getMode() { return currentMode; }
471
+
472
+ /**
473
+ * @primitive b.auditSign.getAlgorithm
474
+ * @signature b.auditSign.getAlgorithm()
475
+ * @since 0.7.0
476
+ * @status stable
477
+ * @related b.auditSign.init, b.auditSign.rotateSigningKey
478
+ *
479
+ * Return the algorithm of the currently-loaded keypair —
480
+ * `"slh-dsa-shake-256f"`, `"ml-dsa-87"`, or `"ml-dsa-65"`. Read from
481
+ * the on-disk key file, not from the operator's `init()` opts (the
482
+ * file's algorithm wins so a key generated under one alg keeps
483
+ * verifying under that alg even when a later boot passes a different
484
+ * default).
485
+ *
486
+ * @example
487
+ * await b.auditSign.init({ dataDir: "/var/lib/blamejs/data" });
488
+ * b.auditSign.getAlgorithm();
489
+ * // → "slh-dsa-shake-256f"
490
+ */
327
491
  function getAlgorithm() { _requireInit(); return keys.algorithm; }
328
492
 
329
493
  // Re-sign every payload in the operator-supplied iterable using the
@@ -339,6 +503,45 @@ function getAlgorithm() { _requireInit(); return keys.algorithm; }
339
503
  // internal key-history. The caller persists the new signature in
340
504
  // place — this primitive returns the new bytes without touching
341
505
  // storage.
506
+ /**
507
+ * @primitive b.auditSign.reSignAll
508
+ * @signature b.auditSign.reSignAll(iter, opts)
509
+ * @since 0.7.0
510
+ * @status stable
511
+ * @compliance hipaa, pci-dss, gdpr, soc2, sox-404
512
+ * @related b.auditSign.rotateSigningKey, b.auditSign.sign
513
+ *
514
+ * Re-sign every payload in `iter` under the CURRENT in-memory key.
515
+ * Each iteration yields `{ id, payload, signature, oldPublicKeyPem }`
516
+ * — payloads whose old signature fails to verify under
517
+ * `oldPublicKeyPem` are skipped (already tampered or never signed
518
+ * under that key) rather than aborting the whole walk. Returns
519
+ * `{ reSigned, skipped, errors }`. The caller (typically the audit
520
+ * module's checkpoint store) persists the new bytes; this primitive
521
+ * does not touch storage.
522
+ *
523
+ * @opts
524
+ * onProgress: function (entry), // called with { id, newSignature } per re-sign; errors in the hook are drop-silent
525
+ *
526
+ * @example
527
+ * await b.auditSign.init({ dataDir: "/var/lib/blamejs/data" });
528
+ *
529
+ * async function* allCheckpoints() {
530
+ * yield {
531
+ * id: 1,
532
+ * payload: Buffer.from("{\"counter\":1}", "utf8"),
533
+ * signature: Buffer.from("00", "hex"),
534
+ * oldPublicKeyPem: b.auditSign.getPublicKey(),
535
+ * };
536
+ * }
537
+ *
538
+ * var summary = await b.auditSign.reSignAll(allCheckpoints(), {
539
+ * onProgress: function (entry) {
540
+ * // persist entry.newSignature against entry.id atomically
541
+ * },
542
+ * });
543
+ * // → { reSigned: 1, skipped: 0, errors: 0 }
544
+ */
342
545
  async function reSignAll(iter, opts) {
343
546
  _requireInit();
344
547
  opts = opts || {};
@@ -382,6 +585,49 @@ async function reSignAll(iter, opts) {
382
585
  // 2. Call rotateSigningKey() — gets new keys live
383
586
  // 3. Walk checkpoints through reSignAll()
384
587
  // 4. Write back the new signatures atomically
588
+ /**
589
+ * @primitive b.auditSign.rotateSigningKey
590
+ * @signature b.auditSign.rotateSigningKey(opts)
591
+ * @since 0.7.0
592
+ * @status stable
593
+ * @compliance hipaa, pci-dss, gdpr, soc2, sox-404
594
+ * @related b.auditSign.reSignAll, b.auditSign.init
595
+ *
596
+ * Generate (or accept) a fresh keypair, copy the existing sealed /
597
+ * plaintext key file to a timestamped `*.history-<iso>-<fp>` path, and
598
+ * persist the new key to disk through the same wrap path as boot. The
599
+ * in-memory swap happens last so a write failure leaves the framework
600
+ * with the OLD key still in memory + on disk. Refuses (`ROTATE_NOOP`)
601
+ * when the new keypair has the same fingerprint as the current one.
602
+ * Operators rotating the audit-signing key in production typically:
603
+ * read existing checkpoints, call `rotateSigningKey()`, walk the
604
+ * checkpoints through `reSignAll()`, then write the new signatures
605
+ * back atomically. Returns metadata about the rotation including the
606
+ * `historyPath` so external tools can verify pre-rotation checkpoints
607
+ * later.
608
+ *
609
+ * @opts
610
+ * privateKeyPem: string, // BYO keypair (pair with publicKeyPem); when omitted the framework generates fresh material
611
+ * publicKeyPem: string,
612
+ * algorithm: "slh-dsa-shake-256f" | "ml-dsa-87" | "ml-dsa-65" // defaults to the current keypair's algorithm
613
+ *
614
+ * @example
615
+ * await b.auditSign.init({ dataDir: "/var/lib/blamejs/data" });
616
+ *
617
+ * // Annual rotation — same algorithm, framework-generated material:
618
+ * var result = await b.auditSign.rotateSigningKey();
619
+ * // → {
620
+ * // previousFingerprint: "9f4e...",
621
+ * // newFingerprint: "3a7c...",
622
+ * // algorithm: "slh-dsa-shake-256f",
623
+ * // rotatedAt: "2026-05-09T12:00:00.000Z",
624
+ * // historyPath: "/var/lib/blamejs/data/audit-sign.key.sealed.history-2026-05-09T12-00-00-000Z-9f4e2c3aabbccdd0",
625
+ * // ...
626
+ * // }
627
+ *
628
+ * // Algorithm upgrade — same call, with explicit `algorithm`:
629
+ * await b.auditSign.rotateSigningKey({ algorithm: "ml-dsa-65" });
630
+ */
385
631
  async function rotateSigningKey(rotOpts) {
386
632
  _requireInit();
387
633
  rotOpts = rotOpts || {};