@blamejs/core 0.15.12 → 0.15.14

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 (365) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +45 -29
  4. package/lib/acme.js +6 -5
  5. package/lib/agent-event-bus.js +18 -4
  6. package/lib/agent-idempotency.js +7 -6
  7. package/lib/agent-orchestrator.js +2 -5
  8. package/lib/agent-saga.js +3 -5
  9. package/lib/agent-snapshot.js +32 -2
  10. package/lib/agent-tenant.js +2 -5
  11. package/lib/ai-adverse-decision.js +2 -15
  12. package/lib/ai-aedt-bias-audit.js +2 -1
  13. package/lib/ai-capability.js +1 -6
  14. package/lib/ai-content-detect.js +1 -3
  15. package/lib/ai-dp.js +1 -5
  16. package/lib/ai-frontier-protocol.js +1 -1
  17. package/lib/ai-input.js +2 -2
  18. package/lib/ai-model-manifest.js +1 -1
  19. package/lib/ai-output.js +16 -7
  20. package/lib/ai-pref.js +3 -8
  21. package/lib/ai-quota.js +3 -14
  22. package/lib/api-key.js +37 -28
  23. package/lib/api-snapshot.js +4 -1
  24. package/lib/app-shutdown.js +7 -1
  25. package/lib/archive-adapters.js +2 -4
  26. package/lib/archive-entry-policy.js +32 -0
  27. package/lib/archive-gz.js +9 -0
  28. package/lib/archive-read.js +14 -24
  29. package/lib/archive-tar-read.js +56 -24
  30. package/lib/archive.js +6 -12
  31. package/lib/arg-parser.js +7 -6
  32. package/lib/asn1-der.js +70 -22
  33. package/lib/asyncapi-traits.js +2 -6
  34. package/lib/atomic-file.js +312 -33
  35. package/lib/audit-chain.js +183 -54
  36. package/lib/audit-daily-review.js +36 -16
  37. package/lib/audit-emit.js +82 -0
  38. package/lib/audit-sign.js +258 -0
  39. package/lib/audit-tools.js +108 -23
  40. package/lib/audit.js +91 -2
  41. package/lib/auth/access-lock.js +7 -29
  42. package/lib/auth/bot-challenge.js +1 -1
  43. package/lib/auth/ciba.js +43 -4
  44. package/lib/auth/dpop.js +29 -36
  45. package/lib/auth/fido-mds3.js +14 -16
  46. package/lib/auth/jwt-external.js +26 -8
  47. package/lib/auth/jwt.js +15 -17
  48. package/lib/auth/lockout.js +24 -27
  49. package/lib/auth/oauth.js +67 -45
  50. package/lib/auth/oid4vci.js +55 -32
  51. package/lib/auth/oid4vp.js +3 -2
  52. package/lib/auth/openid-federation.js +6 -6
  53. package/lib/auth/passkey.js +4 -4
  54. package/lib/auth/password.js +8 -2
  55. package/lib/auth/saml.js +70 -40
  56. package/lib/auth/sd-jwt-vc-holder.js +2 -14
  57. package/lib/auth/sd-jwt-vc-issuer.js +2 -14
  58. package/lib/auth/sd-jwt-vc.js +25 -5
  59. package/lib/auth/status-list.js +21 -9
  60. package/lib/auth/step-up.js +15 -13
  61. package/lib/auth-bot-challenge.js +27 -39
  62. package/lib/backup/bundle.js +18 -20
  63. package/lib/backup/crypto.js +23 -8
  64. package/lib/backup/index.js +55 -67
  65. package/lib/backup/manifest.js +7 -1
  66. package/lib/bounded-map.js +112 -1
  67. package/lib/breach-deadline.js +1 -11
  68. package/lib/break-glass.js +41 -22
  69. package/lib/cache.js +7 -18
  70. package/lib/cbor.js +36 -12
  71. package/lib/cdn-cache-control.js +15 -12
  72. package/lib/cert.js +8 -13
  73. package/lib/chain-writer.js +162 -47
  74. package/lib/cli.js +10 -2
  75. package/lib/client-hints.js +7 -9
  76. package/lib/cloud-events.js +43 -33
  77. package/lib/cluster-storage.js +9 -41
  78. package/lib/cms-codec.js +2 -1
  79. package/lib/codepoint-class.js +84 -1
  80. package/lib/compliance-ai-act-logging.js +1 -6
  81. package/lib/compliance-ai-act-transparency.js +2 -4
  82. package/lib/compliance-eaa.js +2 -12
  83. package/lib/compliance-sanctions-fetcher.js +21 -28
  84. package/lib/compliance-sanctions.js +11 -21
  85. package/lib/compliance.js +3 -10
  86. package/lib/config-drift.js +24 -20
  87. package/lib/content-credentials.js +11 -8
  88. package/lib/content-digest.js +21 -12
  89. package/lib/cookies.js +15 -13
  90. package/lib/cose.js +39 -11
  91. package/lib/cra-report.js +1 -11
  92. package/lib/crdt.js +2 -1
  93. package/lib/crypto-field.js +34 -33
  94. package/lib/crypto.js +138 -7
  95. package/lib/csp.js +239 -3
  96. package/lib/daemon.js +46 -42
  97. package/lib/data-act.js +46 -13
  98. package/lib/db-file-lifecycle.js +14 -6
  99. package/lib/db-query.js +75 -49
  100. package/lib/db.js +66 -27
  101. package/lib/dbsc.js +5 -6
  102. package/lib/ddl-change-control.js +18 -14
  103. package/lib/deprecate.js +4 -5
  104. package/lib/did.js +6 -2
  105. package/lib/dora.js +43 -13
  106. package/lib/dr-runbook.js +1 -1
  107. package/lib/dsa.js +2 -1
  108. package/lib/dsr.js +30 -20
  109. package/lib/early-hints.js +19 -0
  110. package/lib/eat.js +5 -1
  111. package/lib/external-db-migrate.js +21 -22
  112. package/lib/external-db.js +74 -30
  113. package/lib/fda-21cfr11.js +42 -13
  114. package/lib/fdx.js +22 -18
  115. package/lib/file-upload.js +80 -66
  116. package/lib/flag-evaluation-context.js +5 -8
  117. package/lib/flag-providers.js +6 -2
  118. package/lib/forms.js +1 -1
  119. package/lib/framework-error.js +12 -0
  120. package/lib/framework-schema.js +19 -0
  121. package/lib/fsm.js +7 -12
  122. package/lib/gate-contract.js +911 -39
  123. package/lib/gdpr-ropa.js +22 -22
  124. package/lib/graphql-federation.js +18 -5
  125. package/lib/guard-agent-registry.js +2 -1
  126. package/lib/guard-all.js +3 -2
  127. package/lib/guard-archive.js +9 -30
  128. package/lib/guard-auth.js +23 -80
  129. package/lib/guard-cidr.js +20 -96
  130. package/lib/guard-csv.js +135 -196
  131. package/lib/guard-domain.js +23 -106
  132. package/lib/guard-dsn.js +17 -14
  133. package/lib/guard-email.js +46 -53
  134. package/lib/guard-envelope.js +2 -2
  135. package/lib/guard-event-bus-topic.js +2 -1
  136. package/lib/guard-filename.js +12 -60
  137. package/lib/guard-graphql.js +28 -75
  138. package/lib/guard-html-wcag.js +15 -2
  139. package/lib/guard-html.js +74 -128
  140. package/lib/guard-idempotency-key.js +2 -1
  141. package/lib/guard-image.js +280 -77
  142. package/lib/guard-imap-command.js +9 -10
  143. package/lib/guard-jmap.js +1 -1
  144. package/lib/guard-json.js +101 -109
  145. package/lib/guard-jsonpath.js +20 -88
  146. package/lib/guard-jwt.js +32 -114
  147. package/lib/guard-list-id.js +2 -7
  148. package/lib/guard-list-unsubscribe.js +2 -7
  149. package/lib/guard-mail-compose.js +5 -6
  150. package/lib/guard-mail-move.js +3 -2
  151. package/lib/guard-mail-query.js +5 -3
  152. package/lib/guard-mail-sieve.js +6 -7
  153. package/lib/guard-managesieve-command.js +6 -5
  154. package/lib/guard-markdown.js +76 -110
  155. package/lib/guard-message-id.js +5 -6
  156. package/lib/guard-mime.js +20 -99
  157. package/lib/guard-oauth.js +19 -73
  158. package/lib/guard-pdf.js +65 -72
  159. package/lib/guard-pop3-command.js +13 -14
  160. package/lib/guard-posture-chain.js +2 -1
  161. package/lib/guard-regex.js +24 -99
  162. package/lib/guard-saga-config.js +2 -1
  163. package/lib/guard-shell.js +22 -87
  164. package/lib/guard-smtp-command.js +9 -12
  165. package/lib/guard-sql.js +15 -13
  166. package/lib/guard-stream-args.js +2 -1
  167. package/lib/guard-svg.js +103 -149
  168. package/lib/guard-template.js +23 -80
  169. package/lib/guard-tenant-id.js +2 -1
  170. package/lib/guard-text.js +592 -0
  171. package/lib/guard-time.js +27 -95
  172. package/lib/guard-uuid.js +21 -93
  173. package/lib/guard-xml.js +76 -106
  174. package/lib/guard-yaml.js +24 -60
  175. package/lib/html-balance.js +7 -3
  176. package/lib/http-client-cache.js +5 -12
  177. package/lib/http-client-cookie-jar.js +35 -16
  178. package/lib/http-client.js +233 -74
  179. package/lib/http-message-signature.js +3 -2
  180. package/lib/i18n-messageformat.js +5 -7
  181. package/lib/i18n.js +83 -26
  182. package/lib/iab-tcf.js +3 -2
  183. package/lib/importmap-integrity.js +41 -1
  184. package/lib/inbox.js +8 -8
  185. package/lib/incident-report.js +12 -27
  186. package/lib/ip-utils.js +49 -6
  187. package/lib/jobs.js +3 -2
  188. package/lib/json-patch.js +1 -1
  189. package/lib/json-path.js +24 -3
  190. package/lib/jtd.js +2 -2
  191. package/lib/keychain.js +6 -18
  192. package/lib/legal-hold.js +30 -23
  193. package/lib/log-stream-cloudwatch.js +44 -112
  194. package/lib/log-stream-otlp-grpc.js +17 -14
  195. package/lib/log-stream-otlp.js +16 -80
  196. package/lib/log-stream-webhook.js +20 -92
  197. package/lib/log.js +2 -2
  198. package/lib/mail-agent.js +2 -2
  199. package/lib/mail-arc-sign.js +8 -3
  200. package/lib/mail-arf.js +4 -3
  201. package/lib/mail-auth.js +43 -69
  202. package/lib/mail-bimi.js +35 -55
  203. package/lib/mail-bounce.js +3 -3
  204. package/lib/mail-crypto-pgp.js +3 -2
  205. package/lib/mail-crypto-smime.js +71 -6
  206. package/lib/mail-dav.js +8 -35
  207. package/lib/mail-deploy.js +15 -14
  208. package/lib/mail-dkim.js +19 -38
  209. package/lib/mail-greylist.js +17 -30
  210. package/lib/mail-helo.js +4 -7
  211. package/lib/mail-journal.js +13 -9
  212. package/lib/mail-mdn.js +12 -7
  213. package/lib/mail-rbl.js +7 -12
  214. package/lib/mail-scan.js +5 -7
  215. package/lib/mail-send-deliver.js +33 -20
  216. package/lib/mail-server-imap.js +32 -87
  217. package/lib/mail-server-jmap.js +39 -52
  218. package/lib/mail-server-managesieve.js +29 -83
  219. package/lib/mail-server-mx.js +33 -74
  220. package/lib/mail-server-net.js +177 -0
  221. package/lib/mail-server-pop3.js +30 -83
  222. package/lib/mail-server-rate-limit.js +30 -6
  223. package/lib/mail-server-registry.js +1 -1
  224. package/lib/mail-server-submission.js +34 -73
  225. package/lib/mail-server-tls.js +98 -2
  226. package/lib/mail-spam-score.js +9 -12
  227. package/lib/mail-store-fts.js +1 -1
  228. package/lib/mail-store.js +3 -2
  229. package/lib/mail.js +28 -13
  230. package/lib/markup-escape.js +31 -0
  231. package/lib/markup-tokenizer.js +78 -0
  232. package/lib/mcp-tool-registry.js +26 -23
  233. package/lib/mcp.js +7 -5
  234. package/lib/mdoc.js +36 -14
  235. package/lib/metrics.js +46 -33
  236. package/lib/middleware/age-gate.js +3 -23
  237. package/lib/middleware/api-encrypt.js +13 -24
  238. package/lib/middleware/assetlinks.js +2 -7
  239. package/lib/middleware/bearer-auth.js +2 -1
  240. package/lib/middleware/body-parser.js +41 -52
  241. package/lib/middleware/bot-disclose.js +7 -10
  242. package/lib/middleware/bot-guard.js +28 -28
  243. package/lib/middleware/clear-site-data.js +5 -1
  244. package/lib/middleware/compression.js +9 -0
  245. package/lib/middleware/cors.js +32 -23
  246. package/lib/middleware/csrf-protect.js +73 -23
  247. package/lib/middleware/daily-byte-quota.js +12 -30
  248. package/lib/middleware/deny-response.js +19 -1
  249. package/lib/middleware/fetch-metadata.js +35 -4
  250. package/lib/middleware/idempotency-key.js +4 -20
  251. package/lib/middleware/network-allowlist.js +61 -30
  252. package/lib/middleware/rate-limit.js +45 -44
  253. package/lib/middleware/scim-server.js +5 -3
  254. package/lib/middleware/security-headers.js +33 -7
  255. package/lib/middleware/security-txt.js +2 -6
  256. package/lib/middleware/speculation-rules.js +6 -3
  257. package/lib/middleware/tus-upload.js +14 -29
  258. package/lib/middleware/web-app-manifest.js +2 -7
  259. package/lib/migrations.js +4 -13
  260. package/lib/mime-parse.js +34 -17
  261. package/lib/module-loader.js +44 -0
  262. package/lib/money.js +105 -0
  263. package/lib/mtls-ca.js +125 -41
  264. package/lib/network-byte-quota.js +4 -18
  265. package/lib/network-dane.js +8 -6
  266. package/lib/network-dns-resolver.js +98 -7
  267. package/lib/network-dns.js +9 -2
  268. package/lib/network-dnssec.js +18 -17
  269. package/lib/network-heartbeat.js +3 -2
  270. package/lib/network-smtp-policy.js +52 -46
  271. package/lib/network-tls.js +67 -47
  272. package/lib/network-tsig.js +2 -2
  273. package/lib/nis2-report.js +2 -12
  274. package/lib/nist-crosswalk.js +1 -1
  275. package/lib/nonce-store.js +81 -3
  276. package/lib/ntp-check.js +28 -0
  277. package/lib/numeric-bounds.js +31 -8
  278. package/lib/object-store/azure-blob-bucket-ops.js +2 -6
  279. package/lib/object-store/azure-blob.js +6 -47
  280. package/lib/object-store/gcs-bucket-ops.js +4 -2
  281. package/lib/object-store/gcs.js +14 -75
  282. package/lib/object-store/http-put.js +2 -7
  283. package/lib/object-store/http-request.js +157 -0
  284. package/lib/object-store/local.js +37 -17
  285. package/lib/object-store/sigv4-bucket-ops.js +2 -1
  286. package/lib/object-store/sigv4.js +10 -79
  287. package/lib/observability-otlp-exporter.js +29 -29
  288. package/lib/observability.js +95 -0
  289. package/lib/openapi-paths-builder.js +7 -3
  290. package/lib/otel-export.js +7 -10
  291. package/lib/outbox.js +54 -41
  292. package/lib/pagination.js +11 -15
  293. package/lib/parsers/safe-env.js +5 -4
  294. package/lib/parsers/safe-ini.js +10 -4
  295. package/lib/parsers/safe-toml.js +11 -9
  296. package/lib/parsers/safe-xml.js +3 -3
  297. package/lib/parsers/safe-yaml.js +28 -9
  298. package/lib/pick.js +63 -5
  299. package/lib/pipl-cn.js +69 -59
  300. package/lib/privacy-pass.js +8 -6
  301. package/lib/problem-details.js +2 -5
  302. package/lib/pubsub.js +4 -8
  303. package/lib/queue-local.js +10 -3
  304. package/lib/redact.js +9 -4
  305. package/lib/render.js +4 -2
  306. package/lib/request-helpers.js +334 -29
  307. package/lib/resource-access-lock.js +3 -3
  308. package/lib/restore-bundle.js +46 -18
  309. package/lib/restore-rollback.js +10 -4
  310. package/lib/restore.js +24 -22
  311. package/lib/retention.js +23 -9
  312. package/lib/router.js +17 -4
  313. package/lib/safe-async.js +457 -0
  314. package/lib/safe-buffer.js +137 -6
  315. package/lib/safe-decompress.js +2 -1
  316. package/lib/safe-dns.js +2 -1
  317. package/lib/safe-ical.js +14 -28
  318. package/lib/safe-icap.js +1 -1
  319. package/lib/safe-json.js +51 -8
  320. package/lib/safe-jsonpath.js +6 -5
  321. package/lib/safe-mime.js +25 -29
  322. package/lib/safe-redirect.js +2 -5
  323. package/lib/safe-schema.js +7 -6
  324. package/lib/safe-sieve.js +1 -1
  325. package/lib/safe-sql.js +126 -0
  326. package/lib/safe-vcard.js +13 -27
  327. package/lib/sandbox-worker.js +15 -2
  328. package/lib/sandbox.js +4 -3
  329. package/lib/scheduler.js +22 -13
  330. package/lib/sec-cyber.js +82 -37
  331. package/lib/seeders.js +4 -11
  332. package/lib/self-update-standalone-verifier.js +16 -0
  333. package/lib/self-update.js +92 -69
  334. package/lib/session-device-binding.js +112 -66
  335. package/lib/session.js +219 -7
  336. package/lib/sql.js +228 -81
  337. package/lib/sse.js +6 -10
  338. package/lib/static.js +201 -111
  339. package/lib/storage.js +4 -2
  340. package/lib/structured-fields.js +275 -16
  341. package/lib/template.js +7 -5
  342. package/lib/tenant-quota.js +53 -38
  343. package/lib/tsa.js +16 -17
  344. package/lib/validate-opts.js +154 -8
  345. package/lib/vault/index.js +5 -0
  346. package/lib/vault/passphrase-ops.js +22 -26
  347. package/lib/vault/passphrase-source.js +8 -3
  348. package/lib/vault/rotate.js +13 -18
  349. package/lib/vault/seal-pem-file.js +34 -49
  350. package/lib/vault-aad.js +3 -2
  351. package/lib/vc.js +3 -8
  352. package/lib/vendor/MANIFEST.json +10 -10
  353. package/lib/vendor/public-suffix-list.dat +23 -10
  354. package/lib/vendor/public-suffix-list.data.js +5498 -5494
  355. package/lib/vex.js +35 -13
  356. package/lib/web-push-vapid.js +23 -20
  357. package/lib/webhook-dispatcher.js +706 -0
  358. package/lib/webhook.js +59 -19
  359. package/lib/websocket-channels.js +9 -7
  360. package/lib/websocket.js +8 -4
  361. package/lib/worm.js +3 -4
  362. package/lib/ws-client.js +65 -56
  363. package/lib/x509-chain.js +44 -0
  364. package/package.json +1 -1
  365. package/sbom.cdx.json +6 -6
package/lib/audit-sign.js CHANGED
@@ -59,6 +59,7 @@
59
59
  * SLH-DSA-SHAKE-256f post-quantum signature for audit-chain checkpoints.
60
60
  */
61
61
  var nodeFs = require("node:fs");
62
+ var numericBounds = require("./numeric-bounds");
62
63
  var nodePath = require("node:path");
63
64
  var nodeCrypto = require("node:crypto");
64
65
  var atomicFile = require("./atomic-file");
@@ -190,6 +191,33 @@ function _computeFingerprint(publicKeyPem) {
190
191
  return sha3Hash(publicKeyPem);
191
192
  }
192
193
 
194
+ // ---- Store-independent chain anchor (the checkpoint protocol lifted off the
195
+ // framework audit_log / audit_checkpoints store so a consumer can anchor THEIR
196
+ // OWN hash chain) ----
197
+
198
+ // Domain-separation magic for a consumer anchor's signed bytes. DISTINCT from
199
+ // the framework audit-checkpoint format on purpose: a checkpoint signature can
200
+ // never be replayed as an anchor (or vice versa), and the framework
201
+ // audit_checkpoints wire format stays byte-stable (b.audit.checkpoint is
202
+ // untouched). Changing this invalidates every prior anchor.
203
+ var ANCHOR_FORMAT = "blamejs-chain-anchor-v1";
204
+
205
+ // Build the canonical signed bytes for one anchor. Fixed multi-line layout (no
206
+ // JSON serializer quirks). prevTipHash IS part of the signed payload, so the
207
+ // link between consecutive anchors is tamper-evident — an attacker cannot
208
+ // rewrite the linkage without breaking the signature. `format` lets a consumer
209
+ // domain-separate their own anchors from any other anchored chain.
210
+ function anchorPayload(counter, tipHash, prevTipHash, createdAt, format) {
211
+ return Buffer.from(
212
+ (format || ANCHOR_FORMAT) + "\n" +
213
+ String(counter) + "\n" +
214
+ tipHash + "\n" +
215
+ (prevTipHash || "") + "\n" +
216
+ String(createdAt),
217
+ "utf8"
218
+ );
219
+ }
220
+
193
221
  // ---- Passphrase sourcing (delegates to lib/passphrase-source.js with
194
222
  // audit-signing-specific env var names) ----
195
223
 
@@ -798,6 +826,233 @@ async function rotateSigningKey(rotOpts) {
798
826
  };
799
827
  }
800
828
 
829
+ // Defensive coercion of an operator-supplied tip into the canonical fields.
830
+ // Config-time tier: throws a typed AuditSignError on a malformed tip so the
831
+ // consumer catches the bug at the call site, not as an opaque "signature
832
+ // failed" later. prevTipHash is OPTIONAL (absent for the genesis anchor).
833
+ function _normalizeTip(tip, fnLabel) {
834
+ if (!tip || typeof tip !== "object") {
835
+ throw _err("ANCHOR_BAD_TIP",
836
+ "auditSign." + fnLabel + ": tip must be an object { counter, tipHash }");
837
+ }
838
+ var counter = tip.counter;
839
+ if (!numericBounds.isNonNegativeFiniteInt(counter)) {
840
+ throw _err("ANCHOR_BAD_COUNTER",
841
+ "auditSign." + fnLabel + ": tip.counter must be a non-negative integer (got: " + counter + ")");
842
+ }
843
+ if (typeof tip.tipHash !== "string" || tip.tipHash.length === 0) {
844
+ throw _err("ANCHOR_BAD_TIPHASH",
845
+ "auditSign." + fnLabel + ": tip.tipHash must be a non-empty string");
846
+ }
847
+ if (tip.prevTipHash != null && typeof tip.prevTipHash !== "string") {
848
+ throw _err("ANCHOR_BAD_PREV",
849
+ "auditSign." + fnLabel + ": tip.prevTipHash, when present, must be a string");
850
+ }
851
+ return {
852
+ counter: counter,
853
+ tipHash: tip.tipHash,
854
+ prevTipHash: tip.prevTipHash != null ? tip.prevTipHash : null,
855
+ };
856
+ }
857
+
858
+ /**
859
+ * @primitive b.auditSign.anchor
860
+ * @signature b.auditSign.anchor(tip, opts?)
861
+ * @since 0.15.13
862
+ * @status stable
863
+ * @compliance hipaa, pci-dss, gdpr, soc2, sox-404
864
+ * @related b.auditSign.verifyAnchor, b.auditSign.verifyAnchorChain, b.audit.checkpoint
865
+ *
866
+ * Sign a hash-chain tip with the in-memory PQC key, returning a
867
+ * self-describing anchor object the consumer persists in THEIR OWN store. This
868
+ * is the `b.audit.checkpoint()` protocol lifted off the framework `audit_log` /
869
+ * `audit_checkpoints` tables: a consumer running their own append-only chain
870
+ * anchors its tip the same tamper-evident way, with no framework table, no
871
+ * `clusterStorage`, and no leader requirement. A full-chain rewrite that
872
+ * recomputes every row hash still cannot forge the signature without the
873
+ * audit-signing private key, so a later `verifyAnchorChain` detects it.
874
+ *
875
+ * `tip.prevTipHash` (optional) is bound into the signed bytes, so truncation /
876
+ * reorder of a stored anchor sequence is caught by the signature, not just a
877
+ * plaintext compare. `opts.format` domain-separates a consumer's anchors
878
+ * (default `"blamejs-chain-anchor-v1"`).
879
+ *
880
+ * Verification resolves the public key from the recorded fingerprint via the
881
+ * key-history file under the `init({ dataDir })` directory, so it is bound to
882
+ * that key store (not fully store-free) — keep the history with the anchors.
883
+ *
884
+ * Throws `AuditSignError` (`ANCHOR_BAD_TIP` / `ANCHOR_BAD_COUNTER` /
885
+ * `ANCHOR_BAD_TIPHASH` / `ANCHOR_BAD_PREV`) on a malformed tip;
886
+ * `audit-sign/not-initialized` when `init()` has not been awaited.
887
+ *
888
+ * @opts
889
+ * format: string, // default "blamejs-chain-anchor-v1" — domain-separation magic in the signed payload
890
+ * createdAt: number, // default Date.now() — the anchor timestamp (also signed)
891
+ *
892
+ * @example
893
+ * await b.auditSign.init({ dataDir: "/var/lib/blamejs/data" });
894
+ * var a = b.auditSign.anchor({ counter: 42, tipHash: "9f4e", prevTipHash: "1b7d" },
895
+ * { format: "my-app-ledger-v1" });
896
+ * // → { format, counter, tipHash, prevTipHash, createdAt, algorithm,
897
+ * // publicKeyFingerprint, signature }
898
+ */
899
+ function anchor(tip, opts) {
900
+ _requireInit();
901
+ opts = opts || {};
902
+ var t = _normalizeTip(tip, "anchor");
903
+ var format = (typeof opts.format === "string" && opts.format.length > 0) ? opts.format : ANCHOR_FORMAT;
904
+ var createdAt = (typeof opts.createdAt === "number" && isFinite(opts.createdAt)) ? opts.createdAt : Date.now();
905
+ var payload = anchorPayload(t.counter, t.tipHash, t.prevTipHash, createdAt, format);
906
+ var sigBuf = sign(payload);
907
+ return {
908
+ format: format,
909
+ counter: t.counter,
910
+ tipHash: t.tipHash,
911
+ prevTipHash: t.prevTipHash,
912
+ createdAt: createdAt,
913
+ algorithm: keys.algorithm,
914
+ publicKeyFingerprint: keys.fingerprint,
915
+ signature: sigBuf.toString("hex"),
916
+ };
917
+ }
918
+
919
+ // Resolve + verify ONE anchor's signature. Returns { ok, reason? }. Never
920
+ // throws on a forgery / unknown key (defensive-reader tier) — the caller
921
+ // branches on ok.
922
+ function _verifyOneAnchor(a) {
923
+ if (!a || typeof a !== "object") return { ok: false, reason: "anchor is not an object" };
924
+ if (typeof a.tipHash !== "string" || typeof a.signature !== "string" ||
925
+ typeof a.publicKeyFingerprint !== "string") {
926
+ return { ok: false, reason: "anchor missing tipHash / signature / publicKeyFingerprint" };
927
+ }
928
+ if (typeof a.counter !== "number" || !isFinite(a.counter)) {
929
+ return { ok: false, reason: "anchor counter is not a finite number" };
930
+ }
931
+ var pub = getPublicKeyByFingerprint(a.publicKeyFingerprint);
932
+ if (!pub) {
933
+ return { ok: false, reason: "no audit-signing key on record for this anchor's fingerprint" };
934
+ }
935
+ var format = (typeof a.format === "string" && a.format.length > 0) ? a.format : ANCHOR_FORMAT;
936
+ // prevTipHash is part of the signed payload — rebuild it the same way anchor()
937
+ // did (absent / null → "") so the bytes are byte-identical on a clean anchor.
938
+ var prevTipHash = (a.prevTipHash != null && typeof a.prevTipHash === "string") ? a.prevTipHash : "";
939
+ var payload = anchorPayload(Number(a.counter), a.tipHash, prevTipHash, Number(a.createdAt), format);
940
+ // Buffer.from(hex) silently truncates on non-hex / odd-length; reject when
941
+ // the parsed bytes don't round-trip to the original lowercased hex.
942
+ var sigBuf = Buffer.from(a.signature, "hex");
943
+ if (sigBuf.toString("hex") !== String(a.signature).toLowerCase()) {
944
+ return { ok: false, reason: "anchor signature is not valid hex" };
945
+ }
946
+ if (!verify(payload, sigBuf, pub)) {
947
+ return { ok: false, reason: "post-quantum signature failed" };
948
+ }
949
+ return { ok: true };
950
+ }
951
+
952
+ /**
953
+ * @primitive b.auditSign.verifyAnchor
954
+ * @signature b.auditSign.verifyAnchor(anchor)
955
+ * @since 0.15.13
956
+ * @status stable
957
+ * @compliance hipaa, pci-dss, gdpr, soc2, sox-404
958
+ * @related b.auditSign.anchor, b.auditSign.verifyAnchorChain
959
+ *
960
+ * Verify a single anchor produced by `b.auditSign.anchor`. Resolves the public
961
+ * key by the anchor's recorded fingerprint (live key or a rotated-out key from
962
+ * the unsealed history), rebuilds the canonical payload, and checks the
963
+ * post-quantum signature. Returns `{ ok: true }` when valid, or
964
+ * `{ ok: false, reason }` for a forgery, an unknown signing key, malformed hex,
965
+ * or a missing field. Never throws on adversarial content.
966
+ *
967
+ * @example
968
+ * var a = b.auditSign.anchor({ counter: 1, tipHash: "ab12" });
969
+ * b.auditSign.verifyAnchor(a); // → { ok: true }
970
+ */
971
+ function verifyAnchor(a) {
972
+ _requireInit();
973
+ return _verifyOneAnchor(a);
974
+ }
975
+
976
+ /**
977
+ * @primitive b.auditSign.verifyAnchorChain
978
+ * @signature b.auditSign.verifyAnchorChain(anchors, opts?)
979
+ * @since 0.15.13
980
+ * @status stable
981
+ * @compliance hipaa, pci-dss, gdpr, soc2, sox-404
982
+ * @related b.auditSign.anchor, b.auditSign.verifyAnchor, b.audit.verifyCheckpoints
983
+ *
984
+ * Walk an ordered array of anchors (oldest first) and verify each one's
985
+ * signature AND that the sequence is internally consistent: counters strictly
986
+ * increase, and each anchor's `prevTipHash` equals the previous anchor's
987
+ * `tipHash`. This catches the two attacks a single-anchor check cannot — a
988
+ * stored-anchor truncation / reorder (link break) and a full-chain rewrite
989
+ * (signature break). Returns `{ ok: true, anchorsVerified }`, or
990
+ * `{ ok: false, anchorsVerified, breakAt, reason }` at the first break.
991
+ *
992
+ * `requireLinkage` (default true) makes a non-genesis anchor that omits
993
+ * `prevTipHash` a break, so an attacker can't drop the link to bypass the
994
+ * check. Pass `requireLinkage: false` for unlinked anchors.
995
+ *
996
+ * @opts
997
+ * requireLinkage: boolean, // default true — every non-genesis anchor must carry a matching prevTipHash
998
+ *
999
+ * @example
1000
+ * var a1 = b.auditSign.anchor({ counter: 1, tipHash: "h1" });
1001
+ * var a2 = b.auditSign.anchor({ counter: 2, tipHash: "h2", prevTipHash: "h1" });
1002
+ * b.auditSign.verifyAnchorChain([a1, a2]); // → { ok: true, anchorsVerified: 2 }
1003
+ */
1004
+ function verifyAnchorChain(anchors, opts) {
1005
+ _requireInit();
1006
+ opts = opts || {};
1007
+ var requireLinkage = opts.requireLinkage !== false;
1008
+ if (!Array.isArray(anchors)) {
1009
+ return { ok: false, anchorsVerified: 0, breakAt: 0, reason: "anchors must be an array" };
1010
+ }
1011
+ if (anchors.length === 0) return { ok: true, anchorsVerified: 0 };
1012
+
1013
+ var prev = null;
1014
+ for (var i = 0; i < anchors.length; i += 1) {
1015
+ var a = anchors[i];
1016
+ var sigResult = _verifyOneAnchor(a);
1017
+ if (!sigResult.ok) {
1018
+ return { ok: false, anchorsVerified: i, breakAt: i, reason: sigResult.reason };
1019
+ }
1020
+ if (prev === null) {
1021
+ // Genesis: a prevTipHash on the FIRST anchor means a predecessor was
1022
+ // dropped — fail closed.
1023
+ if (a.prevTipHash != null) {
1024
+ return {
1025
+ ok: false, anchorsVerified: i, breakAt: i,
1026
+ reason: "non-genesis anchor missing predecessor (prevTipHash set on first anchor)",
1027
+ };
1028
+ }
1029
+ } else {
1030
+ if (!(Number(a.counter) > Number(prev.counter))) {
1031
+ return {
1032
+ ok: false, anchorsVerified: i, breakAt: i,
1033
+ reason: "anchor counter not strictly increasing",
1034
+ };
1035
+ }
1036
+ if (requireLinkage) {
1037
+ if (a.prevTipHash == null) {
1038
+ return {
1039
+ ok: false, anchorsVerified: i, breakAt: i,
1040
+ reason: "non-genesis anchor missing prevTipHash linkage",
1041
+ };
1042
+ }
1043
+ if (a.prevTipHash !== prev.tipHash) {
1044
+ return {
1045
+ ok: false, anchorsVerified: i, breakAt: i,
1046
+ reason: "anchor prevTipHash does not match the previous tipHash (truncation / reorder)",
1047
+ };
1048
+ }
1049
+ }
1050
+ }
1051
+ prev = a;
1052
+ }
1053
+ return { ok: true, anchorsVerified: anchors.length };
1054
+ }
1055
+
801
1056
  function _resetForTest() {
802
1057
  keys = null;
803
1058
  initialized = false;
@@ -810,6 +1065,9 @@ module.exports = {
810
1065
  init: init,
811
1066
  sign: sign,
812
1067
  verify: verify,
1068
+ anchor: anchor,
1069
+ verifyAnchor: verifyAnchor,
1070
+ verifyAnchorChain: verifyAnchorChain,
813
1071
  rotateSigningKey: rotateSigningKey,
814
1072
  reSignAll: reSignAll,
815
1073
  getPublicKey: getPublicKey,
@@ -58,6 +58,7 @@ var nodeFs = require("node:fs");
58
58
  var nodePath = require("node:path");
59
59
  var pkg = require("../package.json");
60
60
  var atomicFile = require("./atomic-file");
61
+ var C = require("./constants");
61
62
  var auditChain = require("./audit-chain");
62
63
  var canonicalJson = require("./canonical-json");
63
64
  var auditSign = require("./audit-sign");
@@ -349,6 +350,12 @@ async function _defaultReadPredecessorRowHash(firstCounter) {
349
350
  async function _buildBundle(args) {
350
351
  var kind = args.kind;
351
352
  var rows = args.rows;
353
+ // Verification witnesses: the rows between the purgeable slice's tip and
354
+ // the covering checkpoint's anchored counter. They ride the bundle so the
355
+ // chain can be walked up to the SIGNED atRowHash, but they are NOT part of
356
+ // the purgeable range (the manifest range below is computed from `rows`,
357
+ // and purge() only deletes [first..lastCounter]).
358
+ var witnessRows = args.witnessRows || [];
352
359
  var checkpoint = args.checkpoint || null;
353
360
  var passphrase = args.passphrase;
354
361
  var predecessorRowHash = args.predecessorRowHash;
@@ -357,8 +364,9 @@ async function _buildBundle(args) {
357
364
  var lastRow = rows[rows.length - 1];
358
365
  var files = {};
359
366
 
360
- // 1. Encrypt the rows JSONL
361
- var jsonl = rows.map(function (r) {
367
+ // 1. Encrypt the rows JSONL — purgeable slice followed by any witnesses
368
+ // (contiguous, ascending) so the verifier walks one unbroken chain.
369
+ var jsonl = rows.concat(witnessRows).map(function (r) {
362
370
  return JSON.stringify(_rowToWireForm(r));
363
371
  }).join("\n") + "\n";
364
372
  var rowsEnc = await backupCrypto.encryptWithFreshSalt(jsonl, passphrase);
@@ -437,26 +445,39 @@ async function _readBundle(inDir, passphrase) {
437
445
  "bundle directory does not exist: " + inDir);
438
446
  }
439
447
  var manifestPath = nodePath.join(inDir, "manifest.json");
440
- if (!nodeFs.existsSync(manifestPath)) {
441
- throw new AuditToolsError("audit-tools/no-manifest",
442
- "manifest.json missing in " + inDir);
443
- }
444
- var manifest = safeJson.parse(nodeFs.readFileSync(manifestPath, "utf8"));
448
+ // Capped fd-bound read (no existsSync check-then-read window): an externally-
449
+ // supplied bundle manifest is parsed before verification, so an oversized
450
+ // manifest.json would OOM the verifier before safeJson sees it. 4 MiB is far
451
+ // above any real manifest.
452
+ var manifest = safeJson.parse(atomicFile.fdSafeReadSync(manifestPath, {
453
+ maxBytes: C.BYTES.mib(4), encoding: "utf8",
454
+ errorFor: function (kind, detail) {
455
+ if (kind === "enoent") return new AuditToolsError("audit-tools/no-manifest", "manifest.json missing in " + inDir);
456
+ if (kind === "too-large") return new AuditToolsError("audit-tools/bad-format", "manifest.json too large (" + detail.size + " > " + detail.max + ")");
457
+ return new AuditToolsError("audit-tools/bad-format", "manifest.json unreadable: " + kind);
458
+ },
459
+ }), { maxBytes: C.BYTES.mib(4) });
445
460
  if (!manifest || manifest.format !== BUNDLE_FORMAT) {
446
461
  throw new AuditToolsError("audit-tools/bad-format",
447
462
  "manifest.format is not " + BUNDLE_FORMAT);
448
463
  }
449
- if (!VALID_KINDS[manifest.kind]) {
464
+ if (!Object.prototype.hasOwnProperty.call(VALID_KINDS, manifest.kind)) {
450
465
  throw new AuditToolsError("audit-tools/bad-kind",
451
466
  "manifest.kind must be one of " + Object.keys(VALID_KINDS).join(", "));
452
467
  }
453
468
 
454
469
  var rowsEncPath = nodePath.join(inDir, frameworkFiles.fileName("rowsEnc"));
455
- if (!nodeFs.existsSync(rowsEncPath)) {
456
- throw new AuditToolsError("audit-tools/no-rows-blob",
457
- "rows.enc missing in " + inDir);
458
- }
459
- var rowsEnc = nodeFs.readFileSync(rowsEncPath);
470
+ // Capped fd-bound read: rows.enc is the PQC-encrypted archived audit slice
471
+ // (can be large); a hostile multi-GB blob would be read + decrypted in memory
472
+ // before the checksum check. 512 MiB ceiling bounds it (opt-tunable).
473
+ var rowsEnc = atomicFile.fdSafeReadSync(rowsEncPath, {
474
+ maxBytes: C.BYTES.mib(512),
475
+ errorFor: function (kind) {
476
+ if (kind === "enoent") return new AuditToolsError("audit-tools/no-rows-blob", "rows.enc missing in " + inDir);
477
+ if (kind === "too-large") return new AuditToolsError("audit-tools/rows-too-large", "rows.enc exceeds the bundle size cap");
478
+ return new AuditToolsError("audit-tools/no-rows-blob", "rows.enc unreadable: " + kind);
479
+ },
480
+ });
460
481
  if (manifest.checksum && manifest.checksum.rowsSha3_512 &&
461
482
  backupCrypto.checksum(rowsEnc) !== manifest.checksum.rowsSha3_512) {
462
483
  throw new AuditToolsError("audit-tools/rows-checksum-mismatch",
@@ -470,11 +491,16 @@ async function _readBundle(inDir, passphrase) {
470
491
  var checkpoint = null;
471
492
  if (manifest.kind === KIND_ARCHIVE) {
472
493
  var ckptPath = nodePath.join(inDir, frameworkFiles.fileName("checkpointEnc"));
473
- if (!nodeFs.existsSync(ckptPath)) {
474
- throw new AuditToolsError("audit-tools/no-checkpoint-blob",
475
- "checkpoint.enc missing in " + inDir + " (archive bundles must include the covering checkpoint)");
476
- }
477
- var ckptEnc = nodeFs.readFileSync(ckptPath);
494
+ // Capped fd-bound read: checkpoint.enc is a single PQC-encrypted
495
+ // audit_checkpoints row (a few KiB); 4 MiB bounds a hostile blob.
496
+ var ckptEnc = atomicFile.fdSafeReadSync(ckptPath, {
497
+ maxBytes: C.BYTES.mib(4),
498
+ errorFor: function (kind) {
499
+ if (kind === "enoent") return new AuditToolsError("audit-tools/no-checkpoint-blob", "checkpoint.enc missing in " + inDir + " (archive bundles must include the covering checkpoint)");
500
+ if (kind === "too-large") return new AuditToolsError("audit-tools/checkpoint-too-large", "checkpoint.enc exceeds the cap");
501
+ return new AuditToolsError("audit-tools/no-checkpoint-blob", "checkpoint.enc unreadable: " + kind);
502
+ },
503
+ });
478
504
  if (manifest.checksum && manifest.checksum.checkpointSha3_512 &&
479
505
  backupCrypto.checksum(ckptEnc) !== manifest.checksum.checkpointSha3_512) {
480
506
  throw new AuditToolsError("audit-tools/checkpoint-checksum-mismatch",
@@ -562,10 +588,31 @@ async function archive(opts) {
562
588
 
563
589
  var predecessorRowHash = await readPredecessorHash(firstCounter);
564
590
 
591
+ // The signed checkpoint is the bundle's only unforgeable anchor; it commits
592
+ // the row at checkpoint.atMonotonicCounter. When that counter sits BEYOND
593
+ // the purgeable slice's tip (the operator archived a subset older than the
594
+ // last checkpoint), carry the in-between rows as verification witnesses so
595
+ // verifyBundle can chain-walk up to the anchored row and bind atRowHash to
596
+ // it. Without them an attacker could pair any genuine high-counter
597
+ // checkpoint with a wholly fabricated slice. The witnesses are NOT purged.
598
+ var anchorCounter = Number(checkpoint.atMonotonicCounter);
599
+ var witnessRows = [];
600
+ if (anchorCounter > lastCounter) {
601
+ witnessRows = await readRows({ firstCounter: lastCounter + 1, lastCounter: anchorCounter });
602
+ var witnessTip = witnessRows.length ? Number(witnessRows[witnessRows.length - 1].monotonicCounter) : null;
603
+ if (witnessTip !== anchorCounter) {
604
+ throw new AuditToolsError("audit-tools/anchor-rows-missing",
605
+ "archive: covering checkpoint anchors counter=" + anchorCounter +
606
+ " but the rows up to it are not all available (read up to " + witnessTip +
607
+ ") — cannot prove the slice chains to the signed anchor");
608
+ }
609
+ }
610
+
565
611
  if (returnBytes) {
566
612
  var built = await _buildBundle({
567
613
  kind: KIND_ARCHIVE,
568
614
  rows: rows,
615
+ witnessRows: witnessRows,
569
616
  checkpoint: checkpoint,
570
617
  passphrase: opts.passphrase,
571
618
  predecessorRowHash: predecessorRowHash,
@@ -582,6 +629,7 @@ async function archive(opts) {
582
629
  outDir: opts.out,
583
630
  kind: KIND_ARCHIVE,
584
631
  rows: rows,
632
+ witnessRows: witnessRows,
585
633
  checkpoint: checkpoint,
586
634
  passphrase: opts.passphrase,
587
635
  predecessorRowHash: predecessorRowHash,
@@ -769,26 +817,41 @@ async function verifyBundle(opts) {
769
817
  };
770
818
  }
771
819
 
772
- // 2. Confirm the stored firstRowHash + lastRowHash match the slice
820
+ // 2. Confirm the stored firstRowHash + lastRowHash match the PURGEABLE
821
+ // slice boundary. The slice tip is the row at range.lastCounter — not
822
+ // necessarily the physical last row, which may be a verification witness
823
+ // carried past the slice so the chain can reach the signed checkpoint.
824
+ var lastCounterN = Number(read.manifest.range.lastCounter);
825
+ var sliceLastRow = null;
826
+ for (var ri = 0; ri < read.rows.length; ri++) {
827
+ if (Number(read.rows[ri].monotonicCounter) === lastCounterN) { sliceLastRow = read.rows[ri]; break; }
828
+ }
773
829
  if (read.rows[0].rowHash !== read.manifest.range.firstRowHash) {
774
830
  return {
775
831
  ok: false, kind: read.manifest.kind, rowsVerified: read.rows.length,
776
832
  reason: "manifest.range.firstRowHash does not match first row's rowHash",
777
833
  };
778
834
  }
779
- if (read.rows[read.rows.length - 1].rowHash !== read.manifest.range.lastRowHash) {
835
+ if (!sliceLastRow || sliceLastRow.rowHash !== read.manifest.range.lastRowHash) {
780
836
  return {
781
837
  ok: false, kind: read.manifest.kind, rowsVerified: read.rows.length,
782
- reason: "manifest.range.lastRowHash does not match last row's rowHash",
838
+ reason: "manifest.range.lastRowHash does not match the slice row at lastCounter",
783
839
  };
784
840
  }
785
841
 
786
- // 3. (archive only) verify the covering checkpoint signature
842
+ // 3. (archive only) verify the covering checkpoint signature AND bind its
843
+ // anchored rowHash to the slice. The signature alone proves only that a
844
+ // checkpoint exists for some (counter, rowHash); without binding atRowHash
845
+ // to the archived rows, any genuine high-counter checkpoint could be paired
846
+ // with a wholly fabricated slice. The row at checkpoint.atMonotonicCounter
847
+ // must be present in the bundle (slice or witness) and its rowHash must
848
+ // equal checkpoint.atRowHash — mirroring b.audit.verifyCheckpoints against
849
+ // the live table.
787
850
  if (read.manifest.kind === KIND_ARCHIVE) {
788
851
  if (!read.checkpoint) {
789
852
  return { ok: false, kind: KIND_ARCHIVE, reason: "checkpoint missing from archive bundle" };
790
853
  }
791
- if (Number(read.checkpoint.atMonotonicCounter) < Number(read.manifest.range.lastCounter)) {
854
+ if (Number(read.checkpoint.atMonotonicCounter) < lastCounterN) {
792
855
  return {
793
856
  ok: false, kind: KIND_ARCHIVE,
794
857
  reason: "checkpoint atMonotonicCounter (" + read.checkpoint.atMonotonicCounter +
@@ -805,6 +868,28 @@ async function verifyBundle(opts) {
805
868
  };
806
869
  }
807
870
  }
871
+ // Bind: the row the signature anchors must be in the bundle and match.
872
+ var anchorCounterN = Number(read.checkpoint.atMonotonicCounter);
873
+ var anchoredRow = null;
874
+ for (var ai = 0; ai < read.rows.length; ai++) {
875
+ if (Number(read.rows[ai].monotonicCounter) === anchorCounterN) { anchoredRow = read.rows[ai]; break; }
876
+ }
877
+ if (!anchoredRow) {
878
+ return {
879
+ ok: false, kind: KIND_ARCHIVE,
880
+ reason: "checkpoint anchors counter=" + anchorCounterN +
881
+ " but no such row is present in the bundle — checkpoint not bound to the archived slice",
882
+ };
883
+ }
884
+ if (anchoredRow.rowHash !== read.checkpoint.atRowHash) {
885
+ return {
886
+ ok: false, kind: KIND_ARCHIVE,
887
+ reason: "checkpoint atRowHash does not match the bundle row at counter=" + anchorCounterN +
888
+ " — the signed anchor does not bind this slice",
889
+ expected: read.checkpoint.atRowHash,
890
+ actual: anchoredRow.rowHash,
891
+ };
892
+ }
808
893
  }
809
894
 
810
895
  return {
package/lib/audit.js CHANGED
@@ -424,6 +424,13 @@ var FRAMEWORK_NAMESPACES = [
424
424
  "bootgates", // b.bootGates (bootgates.passed / bootgates.failed / bootgates.onfail_threw — boot-invariant runner)
425
425
  "metrics", // b.metrics.snapshot.shadowRegistry (metrics.shadow.cardinality_dropped — namespaced metrics export)
426
426
  "jose", // b.jose.jwe.experimental (jose.jwe.experimental.encrypt / .decrypt — ML-KEM-JWE pre-IANA)
427
+ "ai", // b.ai.adverseDecision (ai.adverse_decision.* — FCRA adverse-action decisioning)
428
+ "breach", // b.breachDeadline (breach.report.* — breach-notification deadline clock)
429
+ "cra", // b.craReport (cra.report.* — EU Cyber Resilience Act conformity)
430
+ "gdpr", // b.gdprRopa (gdpr.ropa.* — GDPR Art. 30 Records of Processing Activities)
431
+ "incident", // b.incidentReport (incident.report.* — incident lifecycle)
432
+ "middleware", // b.middleware.ageGate / dailyByteQuota (middleware.age_gate.* / middleware.daily_byte_quota.*)
433
+ "nis2", // b.nis2Report (nis2.report.* — NIS2 Directive incident reporting)
427
434
  ];
428
435
  var registeredNamespaces = new Set(FRAMEWORK_NAMESPACES);
429
436
 
@@ -790,7 +797,12 @@ async function query(criteria) {
790
797
  if (criteria.resourceKind) q = q.where({ resourceKind: criteria.resourceKind });
791
798
  if (criteria.outcome) q = q.where({ outcome: criteria.outcome });
792
799
 
793
- q.orderBy("monotonicCounter", "asc");
800
+ // order: "asc" (default, chronological) | "desc" (newest-first). A capped
801
+ // query (limit set) returns the FIRST `limit` rows in this order — so a
802
+ // consumer that wants the most RECENT events under a cap (e.g. the daily
803
+ // review) must pass order:"desc", else `limit` keeps the OLDEST and drops the
804
+ // newest events in the window.
805
+ q.orderBy("monotonicCounter", criteria.order === "desc" ? "desc" : "asc");
794
806
  if (criteria.limit != null) q.limit(criteria.limit);
795
807
  if (criteria.offset != null) q.offset(criteria.offset);
796
808
 
@@ -827,7 +839,7 @@ async function _queryCluster(criteria) {
827
839
  if (criteria.resourceKind) qb.where("resourceKind", criteria.resourceKind);
828
840
  if (criteria.outcome) qb.where("outcome", criteria.outcome);
829
841
 
830
- qb.orderBy("monotonicCounter", "asc");
842
+ qb.orderBy("monotonicCounter", criteria.order === "desc" ? "desc" : "asc");
831
843
  if (criteria.limit != null) qb.limit(criteria.limit);
832
844
  if (criteria.offset != null) qb.offset(criteria.offset);
833
845
 
@@ -944,6 +956,13 @@ async function checkpoint(opts) {
944
956
  cluster.requireLeader();
945
957
  opts = opts || {};
946
958
 
959
+ // Bind this checkpoint to the database it reads the tip from. checkpoint()
960
+ // spans async boundaries (tip read → sign → insert); a fire-and-forget call
961
+ // launched by db.close() can have its insert resume AFTER the database it
962
+ // read closed and a fresh one opened, anchoring the old tip into the wrong
963
+ // database. Capture the live db generation now and re-check before the write.
964
+ var dbGenAtEntry = db()._dbGeneration();
965
+
947
966
  var tipReadBuilt = sql.select("audit_log", _sqlOpts())
948
967
  .columns(["_id", "monotonicCounter", "rowHash"])
949
968
  .orderBy("monotonicCounter", "desc")
@@ -972,6 +991,12 @@ async function checkpoint(opts) {
972
991
  var signature = auditSign.sign(payload);
973
992
  var pubFp = auditSign.getPublicKeyFingerprint();
974
993
 
994
+ // Fail closed if the database changed under us between the tip read and here
995
+ // (e.g. a close()-launched checkpoint resuming after a fresh db opened). The
996
+ // tip we signed belongs to a database that is gone; anchoring it into the
997
+ // current one would forge a checkpoint. Write nothing.
998
+ if (db()._dbGeneration() !== dbGenAtEntry) return null;
999
+
975
1000
  var ckptId = generateToken(TRACE_ID_BYTES);
976
1001
  var fencingToken = cluster.fencingToken();
977
1002
  await _insertCheckpoint(
@@ -1445,6 +1470,69 @@ function safeEmit(event) {
1445
1470
  } catch (_e) { /* audit best-effort — never break the caller */ }
1446
1471
  }
1447
1472
 
1473
+ /**
1474
+ * @primitive b.audit.namespaced
1475
+ * @signature b.audit.namespaced(prefix, opts?)
1476
+ * @since 0.15.13
1477
+ * @status stable
1478
+ * @compliance hipaa, pci-dss, gdpr, soc2
1479
+ * @related b.audit.safeEmit, b.audit.emit, b.observability.namespaced
1480
+ *
1481
+ * Build a drop-silent emitter bound to one action namespace — the shape every
1482
+ * framework primitive hand-rolled as a private `_emitAudit(action, outcome,
1483
+ * metadata)` closure (or inline) (`if (!on) return; try { safeEmit({ action:
1484
+ * "ns." + action, outcome, metadata }); } catch {}`). The returned function
1485
+ * prefixes `action` with `prefix + "."`, fills `metadata` with `{}` when
1486
+ * omitted, and routes through `safeEmit` (so the same redaction + outcome
1487
+ * normalization applies).
1488
+ *
1489
+ * Every caller drives the SAME 4-argument emitter `(action, outcome, metadata,
1490
+ * extra?)`: `extra` is an object whose fields are merged onto the event, which
1491
+ * carries the only per-emit variations seen across the framework — `actor`
1492
+ * (constant `{ type: "system" }` for an unattended worker, or a per-request
1493
+ * `ctx.actor`) and `resource`. So a hand-rolled emitter with extra event fields
1494
+ * is never an exception — pass them through `extra`. `opts` is the gate flag for
1495
+ * the common case OR `{ audit, sink }`, where `sink` emits to an
1496
+ * operator-supplied audit object instead of the framework chain (the emitter is
1497
+ * a no-op if that sink has no `safeEmit`, matching the hand-rolled sink guard).
1498
+ *
1499
+ * A falsy `prefix` (`null` / `""`) builds the no-namespace variant: `action`
1500
+ * passes through verbatim (no `prefix + "."`). This serves the primitives whose
1501
+ * audit actions are already fully-qualified at the call site (`emitAudit(
1502
+ * "system.outbox.started", …)`) — the same gated drop-silent passthrough,
1503
+ * without re-homing the qualifier.
1504
+ *
1505
+ * @opts
1506
+ * audit: boolean, // false disables the emitter (default on); passing a bare boolean === { audit }
1507
+ * sink: object, // alternate audit target with a .safeEmit(event) (defaults to b.audit)
1508
+ *
1509
+ * @example
1510
+ * var emitAudit = b.audit.namespaced("gdpr.ropa", opts.audit);
1511
+ * emitAudit("activity_added", "success", { activityId: id });
1512
+ * // → safeEmit({ action: "gdpr.ropa.activity_added", outcome: "success",
1513
+ * // metadata: { activityId: id } })
1514
+ *
1515
+ * var emitGate = b.audit.namespaced("guardSql.gate");
1516
+ * emitGate("refused", "denied", { route: r }, { actor: ctx.actor }); // per-call actor
1517
+ */
1518
+ function namespaced(prefix, opts) {
1519
+ // Back-compat: a bare boolean/undefined is the gate; an object carries the
1520
+ // gate plus the sink axis.
1521
+ var cfg = (opts && typeof opts === "object") ? opts : { audit: opts };
1522
+ var on = cfg.audit !== false;
1523
+ return function (action, outcome, metadata, extra) {
1524
+ if (!on) return;
1525
+ // module.exports.safeEmit (late-bound) so a test that stubs b.audit.safeEmit
1526
+ // still observes the emit; cfg.sink routes to an operator-supplied audit
1527
+ // object (no-op if it lacks safeEmit, matching the hand-rolled sink guard).
1528
+ var sink = cfg.sink || module.exports;
1529
+ if (!sink || typeof sink.safeEmit !== "function") return;
1530
+ var evt = { action: prefix ? prefix + "." + action : action, outcome: outcome, metadata: metadata || {} };
1531
+ if (extra) { for (var k in extra) { if (Object.prototype.hasOwnProperty.call(extra, k)) evt[k] = extra[k]; } }
1532
+ try { sink.safeEmit(evt); } catch (_e) { /* drop-silent — audit is best-effort */ }
1533
+ };
1534
+ }
1535
+
1448
1536
  /**
1449
1537
  * @primitive b.audit.flush
1450
1538
  * @signature b.audit.flush()
@@ -1814,6 +1902,7 @@ module.exports = {
1814
1902
  useStore: useStore,
1815
1903
  emit: emit,
1816
1904
  safeEmit: safeEmit,
1905
+ namespaced: namespaced,
1817
1906
  applyPosture: applyPosture,
1818
1907
  activePosture: activePosture,
1819
1908
  bindActor: bindActor,