@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.js CHANGED
@@ -1,39 +1,52 @@
1
1
  "use strict";
2
2
  /**
3
- * Audit log — tamper-evident, append-only record of every privileged action.
4
- *
5
- * audit_log table is baked into db.js's schema runner — apps cannot opt out.
6
- * Every row is hash-chained (lib/audit-chain.js); the chain is verified at
7
- * boot in db.init(); a chain break refuses-to-boot per the compliance stance.
8
- *
9
- * Action namespaces:
10
- * - Framework owns: 'auth.*', 'system.*', 'audit.*', 'consent.*', 'subject.*'
11
- * - Apps register their own via audit.registerNamespace('orders'), then
12
- * can record 'orders.created', 'orders.shipped', etc.
13
- * - Unregistered namespaces are rejected prevents typos becoming silent
14
- * unobservable events.
15
- *
16
- * Hash chain:
17
- * - rowHash is computed over the *sealed* form of the row + the nonce.
18
- * The sealed form is what's stored on disk; verification recomputes
19
- * directly from disk without unsealing anything (faster + lets auditors
20
- * verify integrity even without the vault key).
21
- *
22
- * Public API:
23
- * audit.registerNamespace(name)
24
- * audit.record({ actor, action, resource, outcome, reason, metadata, requestId }) → row
25
- * audit.query(criteria) rows [auto-self-logs an 'audit.read' event before returning]
26
- * audit.verify(opts?) { ok, rowsVerified, breakAt? }
27
- * audit.beginTrace() → traceId (32 hex chars)
28
- *
29
- * Conventions for `metadata` (apps SHOULD follow these keys for cross-app
30
- * tooling and RoPA correlation; framework's own subject.* events do):
31
- * traceId — cross-request correlation; same value across linked events
32
- * parentEventId — immediate parent event in the causation chain
33
- * before — state before a change (object), for change events
34
- * after — state after the change
35
- * evidenceRef pointer to evidence (signed PDF hash, ticket URL, etc.)
36
- * App-defined keys are also welcome; don't shadow these reserved ones.
3
+ * @module b.audit
4
+ * @featured true
5
+ * @nav Observability
6
+ * @title Audit
7
+ *
8
+ * @intro
9
+ * Tamper-evident, append-only record of every privileged action — the
10
+ * forensic surface every compliance posture (HIPAA / PCI-DSS / SOC 2 /
11
+ * GDPR / SOX / DORA) bottoms out on. The `audit_log` table is baked
12
+ * into db.js's schema runner so apps cannot opt out; the chain is
13
+ * verified at boot and a break refuses-to-boot.
14
+ *
15
+ * Hash chain: every row carries `prevHash` + `rowHash` computed over
16
+ * the SEALED form of the row plus a nonce. Verification recomputes
17
+ * directly from disk without unsealing auditors can confirm
18
+ * integrity without holding the vault key. Periodic SLH-DSA-SHAKE-256f
19
+ * checkpoints (post-quantum signatures over the chain tip) anchor the
20
+ * chain to off-line evidence; tampering that recomputes hashes still
21
+ * fails checkpoint verification.
22
+ *
23
+ * Namespaces: framework owns `auth.*` / `system.*` / `audit.*` /
24
+ * `consent.*` / `subject.*`; apps call `registerNamespace("orders")`
25
+ * at boot before emitting `orders.created`. Unregistered namespaces
26
+ * are rejected so typos don't become silent unobservable events.
27
+ *
28
+ * Action shape — the 5W form: WHO (`actor.userId` / sessionId / ip /
29
+ * userAgent), WHAT (`action` = "namespace.verb[.qualifier]"), WHEN
30
+ * (`recordedAt` ms epoch + monotonic counter), WHERE (`resource.kind`
31
+ * / id), HOW (`outcome` {success, failure, denied} + `reason` +
32
+ * `metadata`).
33
+ *
34
+ * Two emit paths:
35
+ * - `record(event)` async, throws on bad input, awaits the chain
36
+ * append. Use when the caller needs durability before continuing.
37
+ * - `emit(event)` / `safeEmit(event)` — synchronous fire-and-forget;
38
+ * events buffer in an AsyncHandler and drain serially through
39
+ * record(). `safeEmit` is drop-silent on malformed input by
40
+ * design: it runs in request hot paths where throwing would crash
41
+ * the request that triggered the audit attempt.
42
+ *
43
+ * Reserved metadata keys: `traceId` (cross-request correlation,
44
+ * `beginTrace()` mints), `parentEventId`, `before` / `after` (state
45
+ * diff for change events), `evidenceRef` (pointer to signed PDF /
46
+ * ticket).
47
+ *
48
+ * @card
49
+ * Tamper-evident, append-only record of every privileged action — the forensic surface every compliance posture (HIPAA / PCI-DSS / SOC 2 / GDPR / SOX / DORA) bottoms out on.
37
50
  */
38
51
  var auditChain = require("./audit-chain");
39
52
  var auditSign = require("./audit-sign");
@@ -42,6 +55,7 @@ var cluster = require("./cluster");
42
55
  var clusterStorage = require("./cluster-storage");
43
56
  var { generateToken } = require("./crypto");
44
57
  var cryptoField = require("./crypto-field");
58
+ var dbRoleContext = require("./db-role-context");
45
59
  var handlers = require("./handlers");
46
60
  var { boot } = require("./log");
47
61
  var redact = require("./redact");
@@ -49,7 +63,7 @@ var safeAsync = require("./safe-async");
49
63
  var C = require("./constants");
50
64
  var lazyRequire = require("./lazy-require");
51
65
  var observability = require("./observability");
52
- var { ClusterError } = require("./framework-error");
66
+ var { AuditSegregationError, ClusterError } = require("./framework-error");
53
67
 
54
68
  var log = boot("audit");
55
69
 
@@ -226,6 +240,7 @@ var FRAMEWORK_NAMESPACES = [
226
240
  "inbox", // b.inbox (inbox.received / handled / handle_failed / swept)
227
241
  "flag", // b.flag (flag.evaluated / flag.evaluation.error / flag.cache.bust)
228
242
  "permissions", // b.permissions
243
+ "pqcagent", // b.pqcAgent (pqcagent.operator_group.accepted)
229
244
  "restore", // b.restore
230
245
  "retention", // b.retention (retention.rule.declared / sweep.started / row.processed / sweep.completed / sweep.failed)
231
246
  "scheduler", // b.scheduler (lifecycle: scheduler.start / scheduler.stop;
@@ -252,6 +267,27 @@ var FRAMEWORK_NAMESPACES = [
252
267
  "csp", // b.middleware.cspReport (csp.violation)
253
268
  "resourceaccesslock", // b.resourceAccessLock (resourceaccesslock.mode_changed / refused)
254
269
  "process", // b.processSpawn (process.spawn / process.spawn.failed)
270
+ "keychain", // b.keychain (keychain.stored / keychain.retrieved / keychain.removed)
271
+ "fda21cfr11", // b.fda21cfr11 (signature.created / verified / gxp.assert_failed / audit.refused / posture.installed)
272
+ "ddl", // b.ddlChangeControl (ddl.change.proposed / approved / rejected / applied / apply_refused)
273
+ "migrations", // b.migrations + b.externalDb.migrate (migrations.history.appended / verified / tampered)
274
+ "dlp", // b.redact.installOutboundDlp (dlp.outbound.refused / redacted / scanned / installed)
275
+ "session", // b.sessionDeviceBinding (session.device.bound / drift / refused)
276
+ "sandbox", // b.sandbox (sandbox.run / sandbox.run.refused — operator-supplied transform isolation)
277
+ "safeurl", // b.safeUrl.parse (safeurl.idn_homograph.refused — UTS #39 mixed-script host-label refusal)
278
+ "http", // b.middleware.bodyParser (http.chunked.malformed.refused — RFC 9112 §7.1 chunked-decode failure with Connection: close) // allow:raw-byte-literal — RFC number in prose
279
+ "cryptofield", // b.cryptoField.eraseRow (cryptofield.vacuum.skipped — F-RTBF-2 vacuum-after-erase signal when DB not initialized at erase time)
280
+ "acme", // b.acme (acme.account.registered / order.* / cert.issued / cert.renewed / cert.renew.skipped — RFC 8555 + RFC 9773 ARI workflow)
281
+ "tls", // b.router 0-RTT posture (tls.0rtt.refused / tls.0rtt.replayed) — RFC 8446 §8 anti-replay surface // allow:raw-byte-literal — RFC number in prose
282
+ "workerpool", // b.workerPool (workerpool.created / terminated / task.completed / task.failed / task.timeout / spawn.failed — generic worker_threads pool)
283
+ "jwt", // b.auth.jwt-external (jwt.jwe.refused — RFC 7516 5-segment JWE refusal)
284
+ "dr", // b.drRunbook (dr.runbook.emitted)
285
+ "guardfilename", // b.guardFilename (guardfilename.sanitize.stripped)
286
+ "legalhold", // b.legalHold (legalhold.placed / released / place_rejected / release_rejected)
287
+ "networkheartbeat", // b.network.heartbeat.passive (networkheartbeat.passive.timeout)
288
+ "router", // b.router (router.redirect.cross_origin.refused / allowed)
289
+ "http2", // b.router h2 GOAWAY tracker (http2.window_update.refused — CVE-2026-21714)
290
+ "tenant", // b.tenantQuota (tenant.quota.exceeded / tenant.budget.exceeded / tenant.crossover)
255
291
  ];
256
292
  var registeredNamespaces = new Set(FRAMEWORK_NAMESPACES);
257
293
 
@@ -293,6 +329,25 @@ var _chainWriter = chainWriter.create({
293
329
 
294
330
  // ---- Public API ----
295
331
 
332
+ /**
333
+ * @primitive b.audit.registerNamespace
334
+ * @signature b.audit.registerNamespace(name)
335
+ * @since 0.1.0
336
+ * @related b.audit.record, b.audit.safeEmit
337
+ *
338
+ * Register an action namespace at app bootstrap so `record()` / `emit()`
339
+ * accept events under it. Names must match `[a-z][a-z0-9_]*`. Calling
340
+ * twice is a no-op. Framework namespaces (auth / system / audit /
341
+ * consent / subject + every per-primitive namespace) are pre-registered.
342
+ *
343
+ * @example
344
+ * b.audit.registerNamespace("orders");
345
+ * b.audit.safeEmit({
346
+ * action: "orders.shipped",
347
+ * actor: { userId: "u-42" },
348
+ * outcome: "success",
349
+ * });
350
+ */
296
351
  function registerNamespace(name) {
297
352
  if (typeof name !== "string" || !/^[a-z][a-z0-9_]*$/.test(name)) {
298
353
  throw new Error("audit namespace must match [a-z][a-z0-9_]* — got: " + name);
@@ -316,6 +371,42 @@ function _validateAction(action) {
316
371
  }
317
372
  }
318
373
 
374
+ /**
375
+ * @primitive b.audit.record
376
+ * @signature b.audit.record(event)
377
+ * @since 0.1.0
378
+ * @compliance hipaa, pci-dss, gdpr, soc2, sox-404
379
+ * @related b.audit.safeEmit, b.audit.emit, b.audit.flush
380
+ *
381
+ * Append one event to the audit chain and await durability. Throws on a
382
+ * bad action shape, an unregistered namespace, or an outcome outside
383
+ * {success, failure, denied}. The chain-writer serializes the actual
384
+ * INSERT under a mutex so concurrent record() calls produce a strictly
385
+ * monotonic counter and a valid prevHash → rowHash chain.
386
+ *
387
+ * Use record() when the caller must know the row landed before
388
+ * continuing (consent grants, break-glass unseals, change-control
389
+ * approvals). For request hot paths where best-effort is acceptable,
390
+ * prefer safeEmit().
391
+ *
392
+ * @opts
393
+ * actor: { userId, ip, userAgent, sessionId },
394
+ * action: "namespace.verb[.qualifier]",
395
+ * resource: { kind, id },
396
+ * outcome: "success" | "failure" | "denied",
397
+ * reason: string,
398
+ * metadata: object, // serialized to JSON
399
+ * requestId: string,
400
+ *
401
+ * @example
402
+ * await b.audit.record({
403
+ * actor: { userId: "u-42", ip: "10.0.0.1" },
404
+ * action: "consent.granted",
405
+ * resource: { kind: "purpose", id: "marketing" },
406
+ * outcome: "success",
407
+ * metadata: { traceId: b.audit.beginTrace() },
408
+ * });
409
+ */
319
410
  async function record(event) {
320
411
  if (!event || typeof event !== "object") {
321
412
  throw new Error("audit.record requires an event object");
@@ -364,6 +455,39 @@ async function record(event) {
364
455
  // (otherwise legitimate audit auditing produces a Russell-set spiral).
365
456
  var _selfLogging = false;
366
457
 
458
+ /**
459
+ * @primitive b.audit.query
460
+ * @signature b.audit.query(criteria)
461
+ * @since 0.1.0
462
+ * @compliance pci-dss, soc2
463
+ * @related b.audit.verify, b.audit.verifyCheckpoints
464
+ *
465
+ * Read audit rows matching the criteria, returning unsealed rows for
466
+ * the auditor's view. Every call self-logs an `audit.read` event before
467
+ * returning (PCI DSS 10.2.3) so exfiltration attempts are forensically
468
+ * visible; recursion is guarded so the self-log doesn't trigger its own
469
+ * self-log. Plain-field criteria translate into derived-hash equality
470
+ * where the column is sealed.
471
+ *
472
+ * @opts
473
+ * from: number | Date | string, // recordedAt >=
474
+ * to: number | Date | string, // recordedAt <=
475
+ * actorUserId: string,
476
+ * resourceId: string,
477
+ * action: string,
478
+ * resourceKind: string,
479
+ * outcome: "success" | "failure" | "denied",
480
+ * limit: number,
481
+ * offset: number,
482
+ *
483
+ * @example
484
+ * var rows = await b.audit.query({
485
+ * action: "consent.granted",
486
+ * from: Date.now() - 86400000,
487
+ * limit: 100,
488
+ * });
489
+ * rows.length; // → 42
490
+ */
367
491
  async function query(criteria) {
368
492
  criteria = criteria || {};
369
493
  if (!_selfLogging && criteria.action !== "audit.read") {
@@ -462,6 +586,30 @@ function _redactCriteria(c) {
462
586
  // bytes hex-encoded → 32 chars). Routed through C.BYTES so the byte
463
587
  // count has a single source of truth.
464
588
  var TRACE_ID_BYTES = C.BYTES.bytes(16);
589
+ /**
590
+ * @primitive b.audit.beginTrace
591
+ * @signature b.audit.beginTrace()
592
+ * @since 0.1.0
593
+ * @related b.audit.record, b.audit.query
594
+ *
595
+ * Mint a fresh 32-hex-char trace id apps thread through linked events
596
+ * via `metadata.traceId`. Width matches the W3C traceparent trace-id
597
+ * format (16 random bytes hex-encoded), so the id is interoperable with
598
+ * OpenTelemetry / W3C Trace Context propagation.
599
+ *
600
+ * @example
601
+ * var traceId = b.audit.beginTrace();
602
+ * await b.audit.record({
603
+ * action: "subject.export.requested",
604
+ * outcome: "success",
605
+ * metadata: { traceId: traceId },
606
+ * });
607
+ * await b.audit.record({
608
+ * action: "subject.export.delivered",
609
+ * outcome: "success",
610
+ * metadata: { traceId: traceId, parentEventId: "..." },
611
+ * });
612
+ */
465
613
  function beginTrace() {
466
614
  return generateToken(TRACE_ID_BYTES);
467
615
  }
@@ -491,6 +639,32 @@ function _checkpointPayload(atMonotonicCounter, atRowHash, createdAt) {
491
639
  // opts:
492
640
  // skipIfUnchanged: bool — return null without inserting if the chain tip
493
641
  // hasn't advanced since the most recent checkpoint
642
+ /**
643
+ * @primitive b.audit.checkpoint
644
+ * @signature b.audit.checkpoint(opts)
645
+ * @since 0.4.0
646
+ * @compliance soc2, pci-dss, sox-404
647
+ * @related b.audit.verifyCheckpoints, b.audit.verify
648
+ *
649
+ * Anchor the current chain tip with a fresh ML-DSA-87 (post-quantum)
650
+ * signature. Inserts a row into `audit_checkpoints` and updates the
651
+ * boot-time rollback-detection sidecar (single-node) or the cluster
652
+ * audit-tip row (cluster mode, fencing-token guarded). Cluster mode
653
+ * requires the caller hold leader status — `cluster.requireLeader()`
654
+ * throws otherwise.
655
+ *
656
+ * Returns the inserted checkpoint row, or `null` when the chain is
657
+ * empty / `skipIfUnchanged` and the tip hasn't advanced.
658
+ *
659
+ * @opts
660
+ * skipIfUnchanged: boolean, // null-return when tip didn't move
661
+ *
662
+ * @example
663
+ * var ckpt = await b.audit.checkpoint({ skipIfUnchanged: true });
664
+ * if (ckpt) {
665
+ * console.log("anchored at counter", ckpt.atMonotonicCounter);
666
+ * }
667
+ */
494
668
  async function checkpoint(opts) {
495
669
  cluster.requireLeader();
496
670
  opts = opts || {};
@@ -571,6 +745,32 @@ async function checkpoint(opts) {
571
745
  // audit_log row at atMonotonicCounter still has the recorded rowHash.
572
746
  //
573
747
  // Returns { ok, checkpointsVerified, breakAt? }.
748
+ /**
749
+ * @primitive b.audit.verifyCheckpoints
750
+ * @signature b.audit.verifyCheckpoints()
751
+ * @since 0.4.0
752
+ * @compliance soc2, pci-dss, sox-404
753
+ * @related b.audit.checkpoint, b.audit.verify
754
+ *
755
+ * Walk every checkpoint and verify (a) the public-key fingerprint
756
+ * matches the current signing key, (b) the ML-DSA-87 signature over the
757
+ * payload still verifies, (c) the audit_log row at the anchored counter
758
+ * still has the recorded rowHash. Catches tampering that recomputed
759
+ * chain hashes after holding the vault key, because the off-chain
760
+ * signature anchor is unforgeable without the signing key.
761
+ *
762
+ * Returns `{ ok: true, checkpointsVerified }` on success, or
763
+ * `{ ok: false, checkpointsVerified, breakAt, checkpointId, reason }`
764
+ * at the first break.
765
+ *
766
+ * @example
767
+ * var result = await b.audit.verifyCheckpoints();
768
+ * if (!result.ok) {
769
+ * throw new Error("audit checkpoint break at " + result.breakAt +
770
+ * ": " + result.reason);
771
+ * }
772
+ * result.checkpointsVerified; // → 17
773
+ */
574
774
  async function verifyCheckpoints() {
575
775
  var rows = await _readAllCheckpointsAsc();
576
776
 
@@ -648,6 +848,33 @@ function _toMs(value) {
648
848
 
649
849
  // ---- Verify ----
650
850
 
851
+ /**
852
+ * @primitive b.audit.verify
853
+ * @signature b.audit.verify(opts)
854
+ * @since 0.1.0
855
+ * @compliance hipaa, pci-dss, gdpr, soc2, sox-404
856
+ * @related b.audit.verifyCheckpoints, b.audit.query
857
+ *
858
+ * Walk every audit_log row in monotonic order and recompute each
859
+ * `rowHash` against the canonicalized columns + nonce, confirming each
860
+ * row's `prevHash` matches the previous row's `rowHash`. Catches any
861
+ * insert / delete / mutation between checkpoints. Runs at boot in
862
+ * `db.init()`; operators also call it from a periodic job.
863
+ *
864
+ * Returns `{ ok: true, rowsVerified }` on a clean chain, or
865
+ * `{ ok: false, rowsVerified, breakAt, reason }` at the first break.
866
+ *
867
+ * @opts
868
+ * from: number, // start counter (incremental verify after a known-good checkpoint)
869
+ * to: number, // end counter
870
+ *
871
+ * @example
872
+ * var result = await b.audit.verify();
873
+ * if (!result.ok) {
874
+ * console.error("audit chain break at row", result.breakAt);
875
+ * process.exit(1);
876
+ * }
877
+ */
651
878
  async function verify(opts) {
652
879
  // verifyChain just needs an executeAll; route through the same
653
880
  // resilience-wrapped reader the rest of audit uses.
@@ -752,6 +979,28 @@ function _ensureHandler() {
752
979
  return _auditHandler;
753
980
  }
754
981
 
982
+ /**
983
+ * @primitive b.audit.emit
984
+ * @signature b.audit.emit(event)
985
+ * @since 0.1.0
986
+ * @related b.audit.safeEmit, b.audit.record, b.audit.flush
987
+ *
988
+ * Synchronous fire-and-forget emit — events buffer in an AsyncHandler
989
+ * and drain serially through `record()`. Returns immediately; never
990
+ * returns a Promise. Unlike `safeEmit()`, emit() does NOT normalize
991
+ * outcome / action and does NOT redact metadata — callers pass already-
992
+ * shaped events. Most call sites should prefer `safeEmit` instead;
993
+ * `emit` is the lower-level surface the framework's own bound-actor
994
+ * wrapper uses.
995
+ *
996
+ * @example
997
+ * b.audit.emit({
998
+ * actor: { userId: "u-42" },
999
+ * action: "system.config.reloaded",
1000
+ * outcome: "success",
1001
+ * metadata: { source: "SIGHUP" },
1002
+ * });
1003
+ */
755
1004
  function emit(event) {
756
1005
  _ensureHandler().emit(event);
757
1006
  }
@@ -818,6 +1067,43 @@ function _normalizeAction(action) {
818
1067
  // record() and await it with their own error handling. The audit chain
819
1068
  // itself is verified at boot, so a silently dropped row shows up in the
820
1069
  // next chain integrity sweep.
1070
+ /**
1071
+ * @primitive b.audit.safeEmit
1072
+ * @signature b.audit.safeEmit(event)
1073
+ * @since 0.1.0
1074
+ * @compliance hipaa, pci-dss, gdpr, soc2
1075
+ * @related b.audit.emit, b.audit.record, b.audit.flush
1076
+ *
1077
+ * Hot-path-safe fire-and-forget audit emit. Drop-silent on malformed
1078
+ * input by design — safeEmit runs from request middleware, log-stream
1079
+ * hooks, and finalizers where throwing on a missing `action` would
1080
+ * crash the request that triggered the audit attempt. Operators who
1081
+ * need durability guarantees call `record()` and await it.
1082
+ *
1083
+ * Built-in normalization: action segments with hyphens become
1084
+ * underscores ("biometric-id" → "biometric_id"); outcome aliases
1085
+ * collapse to {success, failure, denied} ("ok" → "success", "error" →
1086
+ * "failure", "refused" → "denied"). Actor / reason / metadata pass
1087
+ * through `b.redact.redact()` so connection strings, JWTs, PEM blocks,
1088
+ * AWS keys, and SSNs are scrubbed before they reach the chain.
1089
+ *
1090
+ * @opts
1091
+ * actor: { userId, ip, userAgent, sessionId },
1092
+ * action: "namespace.verb[.qualifier]",
1093
+ * resource: { kind, id },
1094
+ * outcome: string, // normalized
1095
+ * reason: string, // redacted
1096
+ * metadata: object, // redacted
1097
+ * requestId: string,
1098
+ *
1099
+ * @example
1100
+ * b.audit.safeEmit({
1101
+ * actor: { userId: req.user && req.user.id },
1102
+ * action: "auth.login",
1103
+ * outcome: "success",
1104
+ * metadata: { traceId: req.traceId, ua: req.headers["user-agent"] },
1105
+ * });
1106
+ */
821
1107
  function safeEmit(event) {
822
1108
  if (!event || typeof event !== "object") return;
823
1109
  if (typeof event.action !== "string") return; // can't emit without an action
@@ -851,16 +1137,351 @@ function safeEmit(event) {
851
1137
  } catch (_e) { /* audit best-effort — never break the caller */ }
852
1138
  }
853
1139
 
1140
+ /**
1141
+ * @primitive b.audit.flush
1142
+ * @signature b.audit.flush()
1143
+ * @since 0.1.0
1144
+ * @related b.audit.emit, b.audit.safeEmit
1145
+ *
1146
+ * Drain the AsyncHandler buffer — every queued `emit()` / `safeEmit()`
1147
+ * lands in the audit chain before the returned Promise resolves. Tests,
1148
+ * graceful shutdown, and any code that needs to read audit_log
1149
+ * immediately after emitting awaits flush().
1150
+ *
1151
+ * @example
1152
+ * b.audit.safeEmit({ action: "system.shutdown.requested", outcome: "success" });
1153
+ * await b.audit.flush();
1154
+ * var rows = await b.audit.query({ action: "system.shutdown.requested" });
1155
+ * rows.length; // → 1
1156
+ */
854
1157
  async function flush() {
855
1158
  if (!_auditHandler) return;
856
1159
  await _auditHandler.drain();
857
1160
  }
858
1161
 
1162
+ // ---- SOX §404 / SOC 2 CC1.3 — actor-binding + segregation of duties ----
1163
+ //
1164
+ // Anyone with write access to the audit_log table can INSERT a row
1165
+ // claiming any actor identity. The framework already records actor
1166
+ // from the request context, but a privileged caller (operator script,
1167
+ // migration runner, anyone with the externalDb credentials) can claim
1168
+ // a different actor.
1169
+ //
1170
+ // bindActor(actorId, opts) returns a wrapper around audit.safeEmit /
1171
+ // audit.record that refuses any event whose actor.userId mismatches
1172
+ // the bound identity OR the SQL-bound role (when db-role-context is
1173
+ // active).
1174
+ //
1175
+ // SQL-side enforcement lives in lib/cluster-storage.js's framework-
1176
+ // schema generator — see generateActorBindingTriggerSql() below for
1177
+ // the Postgres trigger DDL. Operators apply that DDL in a migration
1178
+ // when they boot under sox-404 / soc2 / pci-dss posture so a non-
1179
+ // framework writer can't INSERT rows under a different role.
1180
+ function _checkActorBinding(actorId, eventActorId, opts) {
1181
+ if (!actorId) return true; // unbound — no enforcement
1182
+ if (!eventActorId) {
1183
+ return { ok: false, reason: "event missing actor.userId — refused under bound emit" };
1184
+ }
1185
+ if (eventActorId !== actorId) {
1186
+ return { ok: false,
1187
+ reason: "actor mismatch: bound='" + actorId + "', event='" + eventActorId + "'" };
1188
+ }
1189
+ // db-role-context check — when the caller is inside a runWithRole
1190
+ // scope, the SQL-bound role and the bound actor must agree (subject
1191
+ // to the operator-supplied `roleEquivalent` mapping).
1192
+ if (opts && typeof opts.roleEquivalent === "function") {
1193
+ var role = dbRoleContext.getRole();
1194
+ if (role && !opts.roleEquivalent(actorId, role)) {
1195
+ return { ok: false,
1196
+ reason: "db-role mismatch: bound actor '" + actorId +
1197
+ "' is not equivalent to SQL role '" + role + "'" };
1198
+ }
1199
+ }
1200
+ return { ok: true };
1201
+ }
1202
+
1203
+ /**
1204
+ * @primitive b.audit.bindActor
1205
+ * @signature b.audit.bindActor(actorId, opts)
1206
+ * @since 0.7.0
1207
+ * @compliance sox-404, soc2
1208
+ * @related b.audit.assertSegregation, b.audit.generateActorBindingTriggerSql
1209
+ *
1210
+ * Wrap `safeEmit` / `record` so any event whose `actor.userId` doesn't
1211
+ * match the bound id is refused (and an `audit.actor_binding.violation`
1212
+ * event is recorded under the bound actor). When `opts.roleEquivalent`
1213
+ * is provided and the caller is inside a `db-role-context.runWithRole`
1214
+ * scope, the SQL-bound role and bound actor must agree per the
1215
+ * operator-supplied mapping.
1216
+ *
1217
+ * Pair with `generateActorBindingTriggerSql()` for SQL-side enforcement
1218
+ * — application-layer binding catches typos; the trigger catches
1219
+ * privileged callers bypassing the framework.
1220
+ *
1221
+ * @opts
1222
+ * roleEquivalent: function (actorId, sqlRole) -> boolean,
1223
+ *
1224
+ * @example
1225
+ * var bound = b.audit.bindActor("u-42");
1226
+ * bound.safeEmit({
1227
+ * actor: { userId: "u-42" },
1228
+ * action: "orders.shipped",
1229
+ * outcome: "success",
1230
+ * });
1231
+ * bound.safeEmit({
1232
+ * actor: { userId: "u-other" },
1233
+ * action: "orders.shipped",
1234
+ * outcome: "success",
1235
+ * });
1236
+ * // → drops + records "audit.actor_binding.violation" under u-42
1237
+ */
1238
+ function bindActor(actorId, opts) {
1239
+ if (typeof actorId !== "string" || actorId.length === 0) {
1240
+ throw new AuditSegregationError("audit/bind-actor-missing",
1241
+ "audit.bindActor: actorId must be a non-empty string");
1242
+ }
1243
+ opts = opts || {};
1244
+ function _violationEmit(eventAction, reason) {
1245
+ try {
1246
+ // Surface via the un-bound _ensureHandler so the violation row
1247
+ // lands in the chain regardless of bind state.
1248
+ _ensureHandler().emit({
1249
+ action: "audit.actor_binding.violation",
1250
+ outcome: "denied",
1251
+ actor: { userId: actorId },
1252
+ metadata: { attemptedAction: eventAction, reason: reason },
1253
+ });
1254
+ } catch (_e) { /* drop-silent — never break the caller */ }
1255
+ }
1256
+ function boundSafeEmit(event) {
1257
+ var rv = _checkActorBinding(actorId,
1258
+ event && event.actor && event.actor.userId, opts);
1259
+ if (rv !== true && !rv.ok) {
1260
+ _violationEmit(event && event.action, rv.reason);
1261
+ return;
1262
+ }
1263
+ safeEmit(event);
1264
+ }
1265
+ async function boundRecord(event) {
1266
+ var rv = _checkActorBinding(actorId,
1267
+ event && event.actor && event.actor.userId, opts);
1268
+ if (rv !== true && !rv.ok) {
1269
+ _violationEmit(event && event.action, rv.reason);
1270
+ throw new AuditSegregationError("audit/actor-binding-violation",
1271
+ "audit.bindActor.record: " + rv.reason);
1272
+ }
1273
+ return await record(event);
1274
+ }
1275
+ return {
1276
+ actorId: actorId,
1277
+ safeEmit: boundSafeEmit,
1278
+ record: boundRecord,
1279
+ };
1280
+ }
1281
+
1282
+ // Trigger-SQL generator — operators apply the returned DDL via
1283
+ // b.externalDb.migrate so the database itself refuses INSERTs into
1284
+ // _blamejs_audit_log where the row's stored actor mismatches the
1285
+ // SQL session's current_user.
1286
+ //
1287
+ // opts.column — defaults to "actorUserId"; operators with a separate
1288
+ // role mapping table pass an explicit column name.
1289
+ // opts.roleMappingFn — Postgres function name that maps the row's
1290
+ // actorUserId to the expected SQL role; defaults to identity match
1291
+ // (current_user must equal the actorUserId).
1292
+ // opts.tableName — defaults to "_blamejs_audit_log".
1293
+ // opts.allowRoles — array of roles allowed to insert ANY actor (e.g.
1294
+ // the framework's own service account); skipped checks for those
1295
+ // roles.
1296
+ //
1297
+ // Returns { up: ddl, down: ddl } so the migration runner can install +
1298
+ // uninstall.
1299
+ /**
1300
+ * @primitive b.audit.generateActorBindingTriggerSql
1301
+ * @signature b.audit.generateActorBindingTriggerSql(opts)
1302
+ * @since 0.7.0
1303
+ * @compliance sox-404, soc2
1304
+ * @related b.audit.bindActor, b.audit.assertSegregation
1305
+ *
1306
+ * Emit Postgres trigger DDL that refuses INSERTs into the audit_log
1307
+ * table whose stored `actorUserId` column doesn't match the SQL
1308
+ * session's `current_user`. Operators apply the returned `up` script
1309
+ * via `b.externalDb.migrate` under sox-404 / soc2 posture so a
1310
+ * privileged caller (operator script, migration runner) can't write
1311
+ * audit rows under a different actor identity.
1312
+ *
1313
+ * Returns `{ up, down, functionName, triggerName }` for migration
1314
+ * runner symmetry.
1315
+ *
1316
+ * @opts
1317
+ * column: string, // default "actorUserId"
1318
+ * tableName: string, // default "_blamejs_audit_log"
1319
+ * roleMappingFn: string, // SQL fn name mapping actor → role
1320
+ * allowRoles: string[], // roles that bypass the check
1321
+ *
1322
+ * @example
1323
+ * var ddl = b.audit.generateActorBindingTriggerSql({
1324
+ * allowRoles: ["blamejs_service"],
1325
+ * });
1326
+ * await db.query(ddl.up);
1327
+ */
1328
+ function generateActorBindingTriggerSql(opts) {
1329
+ opts = opts || {};
1330
+ var column = opts.column || "actorUserId";
1331
+ var tableName = opts.tableName || "_blamejs_audit_log";
1332
+ var allowRoles = Array.isArray(opts.allowRoles) ? opts.allowRoles : [];
1333
+ var fnName = "_blamejs_audit_actor_binding_check";
1334
+ var trigName = "_blamejs_audit_actor_binding_trig";
1335
+ var allowList = allowRoles.length === 0 ? "" :
1336
+ " IF current_user IN (" +
1337
+ allowRoles.map(function (r) { return "'" + r.replace(/'/g, "''") + "'"; }).join(", ") +
1338
+ ") THEN RETURN NEW; END IF;\n";
1339
+ var roleMatch = opts.roleMappingFn
1340
+ ? " IF " + opts.roleMappingFn + "(NEW.\"" + column + "\") IS DISTINCT FROM current_user THEN\n"
1341
+ : " IF NEW.\"" + column + "\" IS DISTINCT FROM current_user THEN\n";
1342
+ var up =
1343
+ "CREATE OR REPLACE FUNCTION " + fnName + "() RETURNS trigger AS $$\n" +
1344
+ "BEGIN\n" +
1345
+ allowList +
1346
+ roleMatch +
1347
+ " RAISE EXCEPTION 'segregation-of-duties violation: actor=% does not match current_user=%', NEW.\"" + column + "\", current_user\n" +
1348
+ " USING ERRCODE = 'P0001';\n" +
1349
+ " END IF;\n" +
1350
+ " RETURN NEW;\n" +
1351
+ "END;\n" +
1352
+ "$$ LANGUAGE plpgsql;\n" +
1353
+ "DROP TRIGGER IF EXISTS " + trigName + " ON " + tableName + ";\n" +
1354
+ "CREATE TRIGGER " + trigName + "\n" +
1355
+ " BEFORE INSERT ON " + tableName + "\n" +
1356
+ " FOR EACH ROW EXECUTE FUNCTION " + fnName + "();\n";
1357
+ var down =
1358
+ "DROP TRIGGER IF EXISTS " + trigName + " ON " + tableName + ";\n" +
1359
+ "DROP FUNCTION IF EXISTS " + fnName + "();\n";
1360
+ return { up: up, down: down, functionName: fnName, triggerName: trigName };
1361
+ }
1362
+
1363
+ // Boot-time check operators wire under sox-404 / soc2 posture. Verifies
1364
+ // the trigger function + trigger row are present in the externalDb
1365
+ // information_schema. Returns { ok, missing? } so the caller decides
1366
+ // whether to refuse boot.
1367
+ /**
1368
+ * @primitive b.audit.assertSegregation
1369
+ * @signature b.audit.assertSegregation(opts)
1370
+ * @since 0.7.0
1371
+ * @compliance sox-404, soc2
1372
+ * @related b.audit.generateActorBindingTriggerSql, b.audit.bindActor
1373
+ *
1374
+ * Boot-time check that confirms the actor-binding trigger function and
1375
+ * trigger row exist in the externalDb's `pg_proc` / `pg_trigger`
1376
+ * catalogs. Throws `AuditSegregationError` with the missing artifacts
1377
+ * named when either is absent — operators wire this into the
1378
+ * sox-404 / soc2 boot sequence so a forgotten migration refuses-to-boot
1379
+ * instead of silently shipping without enforcement.
1380
+ *
1381
+ * @opts
1382
+ * db: { query(sql, params) -> { rows } }, // required
1383
+ * functionName: string,
1384
+ * triggerName: string,
1385
+ *
1386
+ * @example
1387
+ * await b.audit.assertSegregation({ db: externalDb });
1388
+ * // throws if the trigger DDL hasn't been applied
1389
+ */
1390
+ async function assertSegregation(opts) {
1391
+ opts = opts || {};
1392
+ var db = opts.db || null;
1393
+ if (!db || typeof db.query !== "function") {
1394
+ throw new AuditSegregationError("audit/segregation-no-db",
1395
+ "audit.assertSegregation: opts.db with a query() method is required");
1396
+ }
1397
+ var fnName = opts.functionName || "_blamejs_audit_actor_binding_check";
1398
+ var trigName = opts.triggerName || "_blamejs_audit_actor_binding_trig";
1399
+ var fnRes = await db.query(
1400
+ "SELECT 1 FROM pg_proc WHERE proname = $1 LIMIT 1", [fnName]
1401
+ );
1402
+ var fnPresent = !!(fnRes && fnRes.rows && fnRes.rows.length > 0);
1403
+ var trigRes = await db.query(
1404
+ "SELECT 1 FROM pg_trigger WHERE tgname = $1 LIMIT 1", [trigName]
1405
+ );
1406
+ var trigPresent = !!(trigRes && trigRes.rows && trigRes.rows.length > 0);
1407
+ var missing = [];
1408
+ if (!fnPresent) missing.push("function:" + fnName);
1409
+ if (!trigPresent) missing.push("trigger:" + trigName);
1410
+ var ok = missing.length === 0;
1411
+ if (!ok) {
1412
+ safeEmit({
1413
+ action: "audit.actor_binding.violation",
1414
+ outcome: "denied",
1415
+ metadata: {
1416
+ reason: "boot-time segregation check failed",
1417
+ missing: missing,
1418
+ },
1419
+ });
1420
+ throw new AuditSegregationError("audit/segregation-not-installed",
1421
+ "audit.assertSegregation: SQL-side actor-binding trigger missing — " +
1422
+ "apply the DDL from audit.generateActorBindingTriggerSql() under sox-404 / soc2 posture. " +
1423
+ "Missing: " + missing.join(", "));
1424
+ }
1425
+ return { ok: ok, missing: missing };
1426
+ }
1427
+
1428
+ // applyPosture — F-POSTURE-1 cascade hook. b.compliance.set(posture)
1429
+ // calls this to record the active posture so audit emissions can
1430
+ // surface the regulatory regime in metadata where downstream tooling
1431
+ // (forensic export, SIEM correlation) needs it. The chain itself is
1432
+ // posture-agnostic (every posture audits with the same SLH-DSA-SHAKE-
1433
+ // 256f signing key); this hook captures the posture name so query()
1434
+ // callers that filter by-posture have a stable column to look at.
1435
+ var _activePosture = null;
1436
+ /**
1437
+ * @primitive b.audit.applyPosture
1438
+ * @signature b.audit.applyPosture(posture)
1439
+ * @since 0.7.27
1440
+ * @compliance hipaa, pci-dss, gdpr, soc2, sox-404
1441
+ * @related b.audit.activePosture, b.compliance
1442
+ *
1443
+ * Cascade hook called by `b.compliance.set(posture)` to record the
1444
+ * active regulatory regime. The chain itself is posture-agnostic —
1445
+ * every posture audits with the same SLH-DSA-SHAKE-256f signing key —
1446
+ * but downstream tooling (forensic export, SIEM correlation) reads the
1447
+ * stored posture to filter / route. Returns `{ posture }` on accept,
1448
+ * `null` on a non-string / empty argument.
1449
+ *
1450
+ * @example
1451
+ * b.audit.applyPosture("hipaa");
1452
+ * b.audit.activePosture(); // → "hipaa"
1453
+ */
1454
+ function applyPosture(posture) {
1455
+ if (typeof posture !== "string" || posture.length === 0) return null;
1456
+ _activePosture = posture;
1457
+ return { posture: posture };
1458
+ }
1459
+ /**
1460
+ * @primitive b.audit.activePosture
1461
+ * @signature b.audit.activePosture()
1462
+ * @since 0.7.27
1463
+ * @related b.audit.applyPosture
1464
+ *
1465
+ * Return the posture string most recently passed to `applyPosture()`,
1466
+ * or `null` if none has been set. Read-only accessor for downstream
1467
+ * tooling that wants to tag audit-derived artifacts with the regime.
1468
+ *
1469
+ * @example
1470
+ * b.audit.applyPosture("pci-dss");
1471
+ * b.audit.activePosture(); // → "pci-dss"
1472
+ */
1473
+ function activePosture() { return _activePosture; }
1474
+
859
1475
  module.exports = {
860
1476
  registerNamespace: registerNamespace,
861
1477
  record: record,
862
1478
  emit: emit,
863
1479
  safeEmit: safeEmit,
1480
+ applyPosture: applyPosture,
1481
+ activePosture: activePosture,
1482
+ bindActor: bindActor,
1483
+ assertSegregation: assertSegregation,
1484
+ generateActorBindingTriggerSql: generateActorBindingTriggerSql,
864
1485
  flush: flush,
865
1486
  query: query,
866
1487
  verify: verify,