@blamejs/blamejs-shop 0.3.11 → 0.3.13

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 (94) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/admin.js +157 -117
  3. package/lib/asset-manifest.json +1 -1
  4. package/lib/currency-rounding.js +2 -14
  5. package/lib/index.js +1 -0
  6. package/lib/text-guard.js +227 -0
  7. package/lib/vendor/MANIFEST.json +2 -2
  8. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  9. package/lib/vendor/blamejs/README.md +3 -2
  10. package/lib/vendor/blamejs/SECURITY.md +3 -0
  11. package/lib/vendor/blamejs/api-snapshot.json +14 -2
  12. package/lib/vendor/blamejs/lib/agent-event-bus.js +4 -4
  13. package/lib/vendor/blamejs/lib/agent-idempotency.js +6 -6
  14. package/lib/vendor/blamejs/lib/agent-orchestrator.js +9 -9
  15. package/lib/vendor/blamejs/lib/agent-posture-chain.js +10 -10
  16. package/lib/vendor/blamejs/lib/agent-saga.js +6 -7
  17. package/lib/vendor/blamejs/lib/agent-snapshot.js +8 -8
  18. package/lib/vendor/blamejs/lib/agent-stream.js +3 -3
  19. package/lib/vendor/blamejs/lib/agent-tenant.js +4 -4
  20. package/lib/vendor/blamejs/lib/agent-trace.js +5 -5
  21. package/lib/vendor/blamejs/lib/ai-disclosure.js +3 -3
  22. package/lib/vendor/blamejs/lib/app.js +2 -2
  23. package/lib/vendor/blamejs/lib/archive-read.js +1 -1
  24. package/lib/vendor/blamejs/lib/archive-tar-read.js +1 -1
  25. package/lib/vendor/blamejs/lib/archive-wrap.js +5 -5
  26. package/lib/vendor/blamejs/lib/audit-tools.js +65 -5
  27. package/lib/vendor/blamejs/lib/audit.js +2 -2
  28. package/lib/vendor/blamejs/lib/auth/ciba.js +1 -1
  29. package/lib/vendor/blamejs/lib/auth/dpop.js +1 -1
  30. package/lib/vendor/blamejs/lib/auth/fal.js +1 -1
  31. package/lib/vendor/blamejs/lib/auth/fido-mds3.js +2 -3
  32. package/lib/vendor/blamejs/lib/auth/jwt-external.js +2 -2
  33. package/lib/vendor/blamejs/lib/auth/oauth.js +9 -9
  34. package/lib/vendor/blamejs/lib/auth/oid4vci.js +7 -7
  35. package/lib/vendor/blamejs/lib/auth/oid4vp.js +1 -1
  36. package/lib/vendor/blamejs/lib/auth/openid-federation.js +5 -5
  37. package/lib/vendor/blamejs/lib/auth/passkey.js +6 -6
  38. package/lib/vendor/blamejs/lib/auth/saml.js +1 -1
  39. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -6
  40. package/lib/vendor/blamejs/lib/backup/index.js +18 -18
  41. package/lib/vendor/blamejs/lib/cache.js +4 -4
  42. package/lib/vendor/blamejs/lib/calendar.js +5 -5
  43. package/lib/vendor/blamejs/lib/circuit-breaker.js +1 -1
  44. package/lib/vendor/blamejs/lib/cms-codec.js +2 -2
  45. package/lib/vendor/blamejs/lib/compliance.js +14 -14
  46. package/lib/vendor/blamejs/lib/crypto-field.js +58 -21
  47. package/lib/vendor/blamejs/lib/crypto.js +5 -6
  48. package/lib/vendor/blamejs/lib/db-query.js +131 -9
  49. package/lib/vendor/blamejs/lib/db.js +106 -22
  50. package/lib/vendor/blamejs/lib/external-db.js +64 -16
  51. package/lib/vendor/blamejs/lib/framework-schema.js +4 -4
  52. package/lib/vendor/blamejs/lib/guard-list-id.js +2 -2
  53. package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +1 -2
  54. package/lib/vendor/blamejs/lib/incident-report.js +150 -0
  55. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +1 -1
  56. package/lib/vendor/blamejs/lib/mail-deploy.js +3 -3
  57. package/lib/vendor/blamejs/lib/mail-server-managesieve.js +2 -2
  58. package/lib/vendor/blamejs/lib/mail-server-pop3.js +2 -2
  59. package/lib/vendor/blamejs/lib/mail-store.js +1 -1
  60. package/lib/vendor/blamejs/lib/metrics.js +8 -8
  61. package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +1 -1
  62. package/lib/vendor/blamejs/lib/middleware/dpop.js +5 -5
  63. package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +21 -22
  64. package/lib/vendor/blamejs/lib/middleware/protected-resource-metadata.js +2 -2
  65. package/lib/vendor/blamejs/lib/network-dns-resolver.js +2 -2
  66. package/lib/vendor/blamejs/lib/network-dns.js +1 -2
  67. package/lib/vendor/blamejs/lib/network-tls.js +0 -1
  68. package/lib/vendor/blamejs/lib/outbox.js +1 -1
  69. package/lib/vendor/blamejs/lib/pqc-agent.js +1 -1
  70. package/lib/vendor/blamejs/lib/retention.js +1 -1
  71. package/lib/vendor/blamejs/lib/retry.js +1 -1
  72. package/lib/vendor/blamejs/lib/safe-archive.js +2 -2
  73. package/lib/vendor/blamejs/lib/safe-ical.js +2 -2
  74. package/lib/vendor/blamejs/lib/safe-mime.js +1 -1
  75. package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +1 -1
  76. package/lib/vendor/blamejs/lib/self-update.js +2 -2
  77. package/lib/vendor/blamejs/lib/static.js +1 -1
  78. package/lib/vendor/blamejs/lib/subject.js +2 -2
  79. package/lib/vendor/blamejs/lib/vault/index.js +64 -1
  80. package/lib/vendor/blamejs/lib/vault/rotate.js +19 -0
  81. package/lib/vendor/blamejs/lib/vendor-data.js +1 -1
  82. package/lib/vendor/blamejs/package.json +1 -1
  83. package/lib/vendor/blamejs/release-notes/v0.14.7.json +77 -0
  84. package/lib/vendor/blamejs/scripts/release.js +28 -3
  85. package/lib/vendor/blamejs/test/layer-0-primitives/audit-tools-dual-control.test.js +115 -0
  86. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +163 -3
  87. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +102 -0
  88. package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +150 -0
  89. package/lib/vendor/blamejs/test/layer-0-primitives/db-key-aad.test.js +109 -0
  90. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +53 -0
  91. package/lib/vendor/blamejs/test/layer-0-primitives/incident-report.test.js +65 -0
  92. package/lib/webhook-subscriptions.js +11 -24
  93. package/lib/webhooks.js +12 -33
  94. package/package.json +1 -1
@@ -58,8 +58,7 @@
58
58
  *
59
59
  * ## No saga-level retry
60
60
  *
61
- * Per the substrate playbook decision (operator-confirmed
62
- * 2026-05-14): saga's value-add is compensation, not retry. If a
61
+ * Saga's value-add is compensation, not retry. If a
63
62
  * step needs retry-with-backoff, the operator wraps `step.run`
64
63
  * with `b.retry` inside the step body. With v0.9.22 idempotency
65
64
  * available, internal retry inside step.run is side-effect-safe.
@@ -109,7 +108,7 @@ var SAGA_ID_RAND_BYTES = 8;
109
108
  function create(config) {
110
109
  guardSagaConfig.validate(config);
111
110
  var auditImpl = config.audit || audit();
112
- // SUBSTRATE-7 — operator wires a stateStore for crash-safe resume.
111
+ // Operator wires a stateStore for crash-safe resume.
113
112
  // Interface: { saveStep, loadResumePoint, markCompleted, markFailed }.
114
113
  // saveStep({sagaId, stepIndex, stepName, state, status}) commits
115
114
  // after each step.run; loadResumePoint(sagaId) returns the resume
@@ -189,7 +188,7 @@ async function _runFrom(config, auditImpl, stateStore, ctx, state, opts, sagaId,
189
188
  });
190
189
  await step.run(ctx, state);
191
190
  completedSteps.push({ step: step, index: i });
192
- // SUBSTRATE-7 — checkpoint after the step.run returns. saveStep
191
+ // Checkpoint after the step.run returns. saveStep
193
192
  // commits the post-step state so a crash before the NEXT step
194
193
  // resumes from i+1. The audit chain records the checkpoint
195
194
  // independently of the operator's stateStore — operator can
@@ -232,7 +231,7 @@ async function _runFrom(config, auditImpl, stateStore, ctx, state, opts, sagaId,
232
231
  message: (stepErr && stepErr.message) || String(stepErr),
233
232
  });
234
233
  var compensationError = null;
235
- // BUG-5 — capture the compensation step that ACTUALLY failed,
234
+ // Capture the compensation step that ACTUALLY failed,
236
235
  // not "completedSteps[completedSteps.length-1].name" which
237
236
  // names the last-COMPLETED step regardless of which compensation
238
237
  // threw. CWE-209-adjacent (information disclosure via wrong
@@ -278,7 +277,7 @@ async function _runFrom(config, auditImpl, stateStore, ctx, state, opts, sagaId,
278
277
  });
279
278
  } catch (_e) { /* drop-silent — audit already records */ }
280
279
  }
281
- // SUBSTRATE-15 — attach cause:stepErr so the original step
280
+ // Attach cause:stepErr so the original step
282
281
  // error stack survives. ES2022 Error.cause is the standard
283
282
  // mechanism; the framework's defineClass-built AgentSagaError
284
283
  // accepts cause via the third arg.
@@ -290,7 +289,7 @@ async function _runFrom(config, auditImpl, stateStore, ctx, state, opts, sagaId,
290
289
  ((compensationError.message) || String(compensationError));
291
290
  }
292
291
  var sagaErr = new AgentSagaError("agent-saga/failed", detailMsg);
293
- // SUBSTRATE-15 — ES2022 Error.cause attaches the originating
292
+ // ES2022 Error.cause attaches the originating
294
293
  // stepErr so operator stack-trace tooling can walk the chain.
295
294
  // defineClass({alwaysPermanent:true}) doesn't accept cause in
296
295
  // its constructor signature; the property assignment after
@@ -60,7 +60,7 @@ var vault = lazyRequire(function () { return require("./vault");
60
60
 
61
61
  var AgentSnapshotError = defineClass("AgentSnapshotError", { alwaysPermanent: true });
62
62
 
63
- // SUBSTRATE-2 — sealed envelopes start with this prefix on disk; the
63
+ // Sealed envelopes start with this prefix on disk; the
64
64
  // loader sniffs it and routes through unseal before guardSnapshotEnvelope
65
65
  // validation. Compatible with operator backends that store the value
66
66
  // as a string (JSON DBs, k/v stores) or wrap it in `{ value: "..." }`.
@@ -113,19 +113,19 @@ function create(opts) {
113
113
  var snapshotIntervalMs = typeof policy.snapshotIntervalMs === "number" ? policy.snapshotIntervalMs : DEFAULT_SNAPSHOT_INTERVAL_MS;
114
114
  var maxSnapshotBytes = typeof policy.maxSnapshotBytes === "number" ? policy.maxSnapshotBytes : DEFAULT_MAX_SNAPSHOT_BYTES;
115
115
  var auditImpl = opts.audit || audit();
116
- // SUBSTRATE-1 — operator may inject `signer` (interface
116
+ // Operator may inject `signer` (interface
117
117
  // `{ sign(bytes) → Buffer, verify(bytes, sig, pubKey?) → boolean }`)
118
118
  // for testing / alternate key custody. Default = b.auditSign when
119
119
  // initialized at boot; refuses persist() with a clear error if
120
120
  // neither is wired so secure-by-default holds.
121
121
  var signer = opts.signer || null;
122
- // SUBSTRATE-2 — operator may inject `sealer` (interface
122
+ // Operator may inject `sealer` (interface
123
123
  // `{ seal(plaintext, aadParts) → string, unseal(value, aadParts) → string }`)
124
124
  // for alternate KMS integration. Default = b.vault.aad. Refused if
125
125
  // neither is wired AND opts.allowPlaintext is not explicitly true
126
126
  // (operator-justified dev / single-tenant deployments only).
127
127
  var sealer = opts.sealer || null;
128
- // SUBSTRATE-18 — operator-supplied restoreHandlers walk the
128
+ // Operator-supplied restoreHandlers walk the
129
129
  // snapshot inFlight + idempotencyCache + orchestratorState segments
130
130
  // and hydrate the corresponding consumer module. Map shape:
131
131
  // { streams, sagas, outboxJobs, busSubscribers, pendingDeliveries,
@@ -300,7 +300,7 @@ async function _takeSnapshot(ctx, snapshotOpts) {
300
300
 
301
301
  async function _persist(ctx, snap) {
302
302
  guardSnapshotEnvelope.validate(snap);
303
- // SUBSTRATE-1 — sign first so a backend that mutates on put() (very
303
+ // Sign first so a backend that mutates on put() (very
304
304
  // common for k/v stores adding metadata) doesn't poison the signed
305
305
  // bytes downstream readers verify.
306
306
  var signer = _resolveSigner(ctx);
@@ -314,7 +314,7 @@ async function _persist(ctx, snap) {
314
314
  // pubkey at verify time).
315
315
  snap.sigPubKey = (typeof signer.getPublicKey === "function" && signer.getPublicKey()) || null;
316
316
 
317
- // SUBSTRATE-2 — seal the entire envelope under AAD that pins
317
+ // Seal the entire envelope under AAD that pins
318
318
  // snapshotId + schemaVersion + tenantId. AAD mismatch on unseal (a
319
319
  // copy-paste attack from one snapshotId's row into another) fails
320
320
  // the Poly1305 tag check; tampered bytes also fail. The sealed
@@ -405,7 +405,7 @@ async function _unwrapAndVerify(ctx, raw, expectedId) {
405
405
  throw new AgentSnapshotError("agent-snapshot/snapshot-id-mismatch",
406
406
  "load: wrapper snapshotId='" + expectedId + "' does not match envelope='" + snap.snapshotId + "'");
407
407
  }
408
- // SUBSTRATE-1 — verify the signature before returning the envelope
408
+ // Verify the signature before returning the envelope
409
409
  // to the caller. Restore-side trust derives from this gate. The
410
410
  // allowPlaintext escape hatch (operator-acknowledged dev mode)
411
411
  // also waives signature verification because there's no key custody
@@ -496,7 +496,7 @@ async function _restore(ctx, snap, restoreOpts) {
496
496
  affectedStreams: (snap.inFlight && snap.inFlight.streams || []).length,
497
497
  });
498
498
  }
499
- // SUBSTRATE-18 — invoke operator-supplied restoreHandlers across
499
+ // Invoke operator-supplied restoreHandlers across
500
500
  // every segment the snapshot envelope carries. Handlers are
501
501
  // declared at create() time; the snapshot primitive owns ordering
502
502
  // (orchestratorState first so live agents register before consumers
@@ -138,7 +138,7 @@ function _makeIterator(ctx) {
138
138
  var done = false;
139
139
  var closed = false;
140
140
  var drained = false;
141
- // SUBSTRATE-14 — track the cursor of the LAST row actually yielded
141
+ // Track the cursor of the LAST row actually yielded
142
142
  // to the consumer. The prior shape called cursor.lastSeenCursor()
143
143
  // at drain-marker emit, which returned the position of the last
144
144
  // FETCHED batch — clients resuming from that cursor SKIPPED every
@@ -167,7 +167,7 @@ function _makeIterator(ctx) {
167
167
  try {
168
168
  if (buffer.length > 0) {
169
169
  var row = buffer.shift();
170
- // SUBSTRATE-14 — record the cursor for this yielded row so a
170
+ // Record the cursor for this yielded row so a
171
171
  // drain that fires BETWEEN buffered yields emits a marker
172
172
  // whose lastSeenCursor matches what the client actually
173
173
  // received. The cursor extraction shape mirrors the
@@ -248,7 +248,7 @@ function _safeAudit(auditImpl, action, actor, metadata) {
248
248
  agentAudit.safeAudit(auditImpl, action, actor, metadata);
249
249
  }
250
250
 
251
- // SUBSTRATE-14 — resolve the resume cursor for a row about to be
251
+ // Resolve the resume cursor for a row about to be
252
252
  // yielded. Operators may attach the cursor per-row (`row._cursor` /
253
253
  // `row.cursor`) OR rely on the cursor's own per-row tracker
254
254
  // (`cursor.cursorForRow(row)`) — both shapes supported.
@@ -222,7 +222,7 @@ async function _unregister(ctx, tenantId, args) {
222
222
  }
223
223
  // Archive default — retain the key + metadata for retention-mandated
224
224
  // restoration. Operator's compliance regime drives archivePolicy.
225
- // SUBSTRATE-19 — persist as a `status: "archived"` row in the same
225
+ // Persist as a `status: "archived"` row in the same
226
226
  // backend rather than only the process-local Map. GDPR Art. 17 +
227
227
  // HIPAA §164.530(j) require the archived state to survive process
228
228
  // restart (auditor pulls a deleted tenant's archival record years
@@ -373,7 +373,7 @@ function _check(ctx, actor, agentTenantId) {
373
373
 
374
374
  // ---- Per-tenant derived key -----------------------------------------------
375
375
  //
376
- // SUBSTRATE-5 — `namespaceHash(label, tenantId)` is a PUBLIC function
376
+ // `namespaceHash(label, tenantId)` is a PUBLIC function
377
377
  // over PUBLIC inputs; an attacker who learns `tenantId` (an account id
378
378
  // surfaced in URLs / API responses) reconstructs every per-tenant key
379
379
  // without any secret material. The defense the docstring promises —
@@ -555,7 +555,7 @@ var TENANT_FIELD_PREFIX = "tnt-v1:";
555
555
  function _tenantFieldKey(tenantId, table) {
556
556
  // 32-byte symmetric key for XChaCha20-Poly1305. _deriveTenantKeyBytes
557
557
  // returns the raw key bound to the vault master + tenantId + purpose
558
- // — see SUBSTRATE-5 commentary above _derivedKey for the threat
558
+ // — see the commentary above _derivedKey for the threat
559
559
  // model that drove this away from public-input-only derivation.
560
560
  return _deriveTenantKeyBytes(tenantId, "cryptoField:" + table);
561
561
  }
@@ -646,7 +646,7 @@ function _unsealRowForTenant(ctx, tenantId, table, row) {
646
646
  if (out[f] !== undefined && out[f] !== null) {
647
647
  try { out[f] = _unsealField(tenantId, table, f, out[f]); }
648
648
  catch (e) {
649
- // BUG-4 — null-on-decrypt-failure was silent; the docstring
649
+ // Null-on-decrypt-failure was silent; the docstring
650
650
  // promised "audit chain surfaces the failure" but no emit ever
651
651
  // ran. Cross-tenant ciphertext replay / tampered row / wrong-
652
652
  // prefix all hit this path; operator audit pipelines need the
@@ -70,7 +70,7 @@ var audit = lazyRequire(function () { return require("./audit"); })
70
70
 
71
71
  var AgentTraceError = defineClass("AgentTraceError", { alwaysPermanent: true });
72
72
 
73
- // SUBSTRATE-24 — once-per-process audit emit on the first tracer
73
+ // Once-per-process audit emit on the first tracer
74
74
  // failure each install fires. Operators get the signal even when
75
75
  // individual span calls are best-effort suppressed.
76
76
  var _failureAuditEmittedFor = Object.create(null);
@@ -122,7 +122,7 @@ function create(opts) {
122
122
  injectIntoEnvelope: function (envelope, span) { return _injectIntoEnvelope(opts.tracing, envelope, span); },
123
123
  extractFromEnvelope: function (envelope) { return _extractFromEnvelope(envelope); },
124
124
  recordResult: function (span, result, error) { return _recordResult(span, result, error, auditImpl); },
125
- // SUBSTRATE-17 — `shouldSample` now takes a traceId so the same
125
+ // `shouldSample` now takes a traceId so the same
126
126
  // trace gets the same decision across hops. Operator-supplied
127
127
  // traceId comes from the W3C `traceparent` header at request-
128
128
  // entry; absent that, falls back to Math.random (start of trace).
@@ -150,7 +150,7 @@ function _startSpan(tracing, name, sopts, auditImpl) {
150
150
  return tracing.startSpan(name, sopts);
151
151
  }
152
152
  } catch (e) {
153
- // SUBSTRATE-24 — tracer failures should not crash the agent's
153
+ // Tracer failures should not crash the agent's
154
154
  // method call; surface the first failure to the audit chain
155
155
  // (rate-limited) so operators get the signal.
156
156
  _emitFirstFailureAudit(auditImpl, "startSpan", e && e.message);
@@ -206,7 +206,7 @@ function _extractFromEnvelope(envelope) {
206
206
 
207
207
  function _recordResult(span, result, error, auditImpl) {
208
208
  if (!span || typeof span !== "object") return;
209
- // SUBSTRATE-24 — surface first occurrence of each span-op failure
209
+ // Surface first occurrence of each span-op failure
210
210
  // via audit so the operator gets the signal. Subsequent failures
211
211
  // stay silent (best-effort) per the operational spec.
212
212
  if (error) {
@@ -228,7 +228,7 @@ function _recordResult(span, result, error, auditImpl) {
228
228
  }
229
229
  }
230
230
 
231
- // SUBSTRATE-17 — deterministic sampling per W3C Trace Context §3.2.3.1.
231
+ // Deterministic sampling per W3C Trace Context §3.2.3.1.
232
232
  // `Math.random` makes child-vs-parent sampling decisions non-coherent:
233
233
  // a parent span sampled OUT can still have child spans sampled IN,
234
234
  // producing orphaned spans operators can't correlate. Hashing the
@@ -133,7 +133,7 @@ function chatbot(session, opts) {
133
133
  var text = typeof opts.text === "string" && opts.text.length > 0
134
134
  ? opts.text
135
135
  : DEFAULT_CHATBOT_TEXT;
136
- // Codex P1A on v0.12.12 PR #163 — "on-request" placement gates on
136
+ // "on-request" placement gates on
137
137
  // the operator's explicit `opts.requested: true` signal. Without
138
138
  // it, "on-request" collapsed into "always" semantics and emitted
139
139
  // every call. The operator wires this from an explicit user
@@ -155,7 +155,7 @@ function chatbot(session, opts) {
155
155
  regulation: "Regulation (EU) 2024/1689",
156
156
  };
157
157
  if (shouldEmit) {
158
- // Codex P1B on v0.12.12 PR #163 — mark the session so subsequent
158
+ // Mark the session so subsequent
159
159
  // calls with the same session under "first-message" placement
160
160
  // see `aiDisclosureEmitted: true` and return shouldEmit=false.
161
161
  // Without this mutation operators had to remember to flip the
@@ -385,7 +385,7 @@ function applyAll(scenario) {
385
385
  "applyAll: scenario.kinds must be a non-empty array of " +
386
386
  "\"chatbot\" / \"deepfake\" / \"emotion\"");
387
387
  }
388
- // Codex P1 on v0.12.25 PR #176 — validate every kind +
388
+ // Validate every kind +
389
389
  // per-kind required field UP FRONT, before any emission.
390
390
  // Previously a later-kind failure (e.g. deepfake missing
391
391
  // contentType, unknown trailing kind) ran AFTER earlier kinds
@@ -113,7 +113,7 @@ function _resolveMiddlewareOpt(value, allowDefault, name) {
113
113
  if (value === false) {
114
114
  // Operator explicitly disabled this middleware. When it's one of the
115
115
  // security-on-by-default layers (allowDefault), leave an audit trace
116
- // so the weakened posture is visible — Core Rule §3 security defaults
116
+ // so the weakened posture is visible — security defaults
117
117
  // shouldn't be silently opt-out-able. Drop-silent observability sink.
118
118
  if (allowDefault && name) {
119
119
  try {
@@ -227,7 +227,7 @@ async function createApp(opts) {
227
227
  var rateLimitOpts = _resolveMiddlewareOpt(mwConfig.rateLimit, false, "rateLimit");
228
228
  if (rateLimitOpts) router.use(middleware.rateLimit(rateLimitOpts));
229
229
 
230
- // Security middleware wired ON by default (Core Rule §3). Each reads its
230
+ // Security middleware wired ON by default. Each reads its
231
231
  // config from opts.middleware.<name>: pass `false` to opt out (audited
232
232
  // via _resolveMiddlewareOpt), or an object to customize — operator cookie
233
233
  // / field names flow straight through, nothing static is baked in.
@@ -260,7 +260,7 @@ async function _readCentralDirectory(adapter, eocd) {
260
260
  "central directory entry " + n + " carries ZIP64 sentinel sizes (unsupported — use tar for >4 GiB / >65535 entries)");
261
261
  }
262
262
  // ZIP names are CP437 or UTF-8 (per FLAG_UTF8_NAME bit). Decode
263
- // as UTF-8 unconditionally — Codex P2 territory if operators in
263
+ // as UTF-8 unconditionally — a concern if operators in
264
264
  // the wild rely on CP437; v0.12.7 ships UTF-8 only and operators
265
265
  // with legacy CP437-only producers reach for an external decoder.
266
266
  var name = cdBytes.slice(nameStart, nameStart + nameLen).toString("utf8");
@@ -237,7 +237,7 @@ function tar(adapter, opts) {
237
237
  }
238
238
  var bodyStart = pos;
239
239
  var paddedSize = Math.ceil(hdr.size / BLOCK_SIZE) * BLOCK_SIZE;
240
- // Codex P1 on v0.12.8 PR #159 — refuse truncated archives upfront.
240
+ // Refuse truncated archives upfront.
241
241
  // The walker advances `pos` by the declared padded block size; if
242
242
  // the buffer ends mid-body, extract() would silently slice a
243
243
  // partial payload (header says 11 bytes, buffer holds 8 — without
@@ -272,7 +272,7 @@ function _encryptForRecipient(bytes, opts) {
272
272
  };
273
273
  }
274
274
  if (r.publicKey) {
275
- // Codex P2 on v0.12.10 PR #161 — b.crypto.encrypt falls back to
275
+ // B.crypto.encrypt falls back to
276
276
  // ML-KEM-only when ecPublicKey is undefined (with a one-shot
277
277
  // audit). For archive-wrap's recipient contract the hybrid leg
278
278
  // (P-384 ECDH defence-in-depth backstop on top of ML-KEM-1024)
@@ -343,7 +343,7 @@ function sniffEnvelope(bytes) {
343
343
  if (!Buffer.isBuffer(bytes) && !(bytes instanceof Uint8Array)) {
344
344
  return "none";
345
345
  }
346
- // Codex P2A on v0.12.14 PR #165 — `Buffer.from(uint8Array)` copies
346
+ // `Buffer.from(uint8Array)` copies
347
347
  // the entire input, turning a constant-time 5-byte probe into an
348
348
  // O(n) allocation. Use the zero-copy view form so the sniff is
349
349
  // truly cheap regardless of input size.
@@ -351,7 +351,7 @@ function sniffEnvelope(bytes) {
351
351
  ? bytes
352
352
  : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
353
353
  if (buf.length < 5) return "none";
354
- // Codex P2B on v0.12.14 PR #165 — match on the 5-byte ASCII magic
354
+ // Match on the 5-byte ASCII magic
355
355
  // alone, NOT on the full header (which requires version + saltLen
356
356
  // bytes). A truncated envelope (`BAWRP` + nothing else) is still a
357
357
  // recipient envelope; the unwrap call surfaces the truncation with
@@ -413,7 +413,7 @@ async function wrapWithPassphrase(bytes, opts) {
413
413
  // alphabet. Operators sourcing passphrases from a random-bytes
414
414
  // generator (high entropy density) pass without issue; operators
415
415
  // typing dictionary phrases trip the gate.
416
- // Codex P1 on v0.12.11 PR #162 — typeof NaN === "number" passes
416
+ // Typeof NaN === "number" passes
417
417
  // typeof gate but bypasses downstream comparisons. Use isFinite
418
418
  // so NaN / Infinity can't slip past the entropy gate.
419
419
  var minEntropy;
@@ -516,7 +516,7 @@ async function unwrapWithPassphrase(sealed, opts) {
516
516
  }
517
517
 
518
518
  function _estimatePassphraseEntropyBits(passphrase) {
519
- // Codex P2 on v0.12.11 PR #162 — Buffer passphrases (CSPRNG-
519
+ // Buffer passphrases (CSPRNG-
520
520
  // generated random bytes) shouldn't be UTF-8 decoded for entropy
521
521
  // estimation; the decoding artifacts (invalid sequences, BOM,
522
522
  // surrogate pairs) make the alphabet-class measure unstable and
@@ -75,9 +75,39 @@ var FRAMEWORK_VERSION = (pkg && pkg.version) || "unknown";
75
75
  // so importing db at audit-tools' top would close the cycle. Lazy
76
76
  // keeps the load order one-way.
77
77
  var db = lazyRequire(function () { return require("./db"); });
78
+ var audit = lazyRequire(function () { return require("./audit"); });
78
79
 
79
80
  var AuditToolsError = defineClass("AuditToolsError", { alwaysPermanent: true });
80
81
 
82
+ // Dual-control gate constants for the audit_log physical purge. The
83
+ // purge erases signed audit history, so when an operator has declared
84
+ // audit_log under b.db.declareRequireDualControl the deletion requires
85
+ // a consumed m-of-n grant whose action matches AUDIT_LOG_PURGE_ACTION —
86
+ // the same separation-of-duties control b.db.eraseHard enforces (NIST
87
+ // SP 800-53 AU-9 + AC-5, HIPAA 45 CFR 164.312(b), PCI-DSS v4.0 10.5.1 /
88
+ // 10.7, SEC 17a-4(f), CWE-778).
89
+ var AUDIT_LOG_GATE_TABLE = "audit_log";
90
+ var AUDIT_LOG_PURGE_ACTION = "auditTools.purge";
91
+
92
+ function _resolveDualControlGate(opts) {
93
+ var checker = typeof opts.checkDualControlGate === "function"
94
+ ? opts.checkDualControlGate
95
+ : function (t) { return db()._checkDualControlGate(t); };
96
+ try { return checker(AUDIT_LOG_GATE_TABLE); }
97
+ catch (_e) { return null; }
98
+ }
99
+
100
+ function _emitPurgeDenied(gate, reason) {
101
+ try {
102
+ audit().safeEmit({
103
+ action: "auditTools.purge.denied",
104
+ outcome: "denied",
105
+ reason: reason,
106
+ metadata: { table: AUDIT_LOG_GATE_TABLE, m: gate.m, n: gate.n, posture: gate.posture || null },
107
+ });
108
+ } catch (_e) { /* drop-silent — denial audit is best-effort */ }
109
+ }
110
+
81
111
  var BUNDLE_FORMAT = "blamejs-audit-bundle-v1";
82
112
  var KIND_ARCHIVE = "archive";
83
113
  var KIND_EXPORT = "export";
@@ -149,7 +179,7 @@ function _rowToWireForm(row) {
149
179
  return out;
150
180
  }
151
181
 
152
- // F-AUD-4 — operator-facing wire helper that surfaces recordedAt as
182
+ // Operator-facing wire helper that surfaces recordedAt as
153
183
  // ISO-8601 / RFC 3339 alongside the existing Unix-ms integer.
154
184
  // Auditors comparing rows against external SIEM events expect ISO
155
185
  // with explicit Z; the framework's primary ms storage stays
@@ -806,10 +836,11 @@ function _defaultVerifyCheckpointSignature(checkpoint) {
806
836
  * `lastPurgedRowHash` becomes the new chain origin.
807
837
  *
808
838
  * @opts
809
- * confirm: true, // exact `true` required
810
- * archive: string, // path to a verified archive bundle
811
- * passphrase: Buffer|string, // bundle decryption passphrase
812
- * verifySignature: function(checkpoint),// auditor pubkey override
839
+ * confirm: true, // exact `true` required
840
+ * archive: string, // path to a verified archive bundle
841
+ * passphrase: Buffer|string, // bundle decryption passphrase
842
+ * verifySignature: function(checkpoint),// auditor pubkey override
843
+ * dualControlGrant: object, // required when audit_log is declared under b.db.declareRequireDualControl — from b.dualControl.consume({ action: "auditTools.purge" })
813
844
  *
814
845
  * @example
815
846
  * var result = await b.auditTools.purge({
@@ -846,6 +877,34 @@ async function purge(opts) {
846
877
  "purge: bundle kind is '" + v.kind + "', must be 'archive'");
847
878
  }
848
879
 
880
+ // Dual-control gate. When audit_log is declared under
881
+ // b.db.declareRequireDualControl, the physical purge requires a
882
+ // consumed m-of-n grant — confirm:true alone is not enough. Mirrors
883
+ // b.db.eraseHard, and additionally binds the grant's action so a
884
+ // grant minted for a different operation can't be replayed here.
885
+ var dcGate = _resolveDualControlGate(opts);
886
+ if (dcGate) {
887
+ var grant = opts.dualControlGrant;
888
+ if (!grant) {
889
+ _emitPurgeDenied(dcGate, "no-grant");
890
+ throw new AuditToolsError("audit-tools/dual-control-required",
891
+ "purge: audit_log is under dual control (m=" + dcGate.m + ", n=" + dcGate.n +
892
+ "); pass opts.dualControlGrant from b.dualControl.consume({ action: \"" +
893
+ AUDIT_LOG_PURGE_ACTION + "\" }).");
894
+ }
895
+ if (grant.ready !== true) {
896
+ _emitPurgeDenied(dcGate, "grant-not-ready");
897
+ throw new AuditToolsError("audit-tools/dual-control-grant-not-ready",
898
+ "purge: opts.dualControlGrant.ready must be true (a consumed m-of-n grant)");
899
+ }
900
+ if (grant.action !== AUDIT_LOG_PURGE_ACTION) {
901
+ _emitPurgeDenied(dcGate, "grant-action-mismatch");
902
+ throw new AuditToolsError("audit-tools/dual-control-grant-mismatch",
903
+ "purge: dualControlGrant.action is '" + grant.action + "', must be '" +
904
+ AUDIT_LOG_PURGE_ACTION + "'");
905
+ }
906
+ }
907
+
849
908
  // 2. Refuse if the archive doesn't start at the next purge point. Keeps
850
909
  // the chain anchor monotonic — operators can't jump-purge a middle range.
851
910
  var readAnchor = opts.readAnchor || _defaultReadPurgeAnchor;
@@ -881,6 +940,7 @@ async function purge(opts) {
881
940
  lastPurgedCounter: Number(v.range.lastCounter),
882
941
  lastPurgedRowHash: v.range.lastRowHash,
883
942
  archiveBundleId: result.archiveBundleId,
943
+ dualControlConsumed: !!dcGate,
884
944
  };
885
945
  }
886
946
 
@@ -305,7 +305,7 @@ var FRAMEWORK_NAMESPACES = [
305
305
  "sandbox", // b.sandbox (sandbox.run / sandbox.run.refused — operator-supplied transform isolation)
306
306
  "safeurl", // b.safeUrl.parse (safeurl.idn_homograph.refused — UTS #39 mixed-script host-label refusal)
307
307
  "http", // b.middleware.bodyParser (http.chunked.malformed.refused — RFC 9112 §7.1 chunked-decode failure with Connection: close) // RFC number in prose
308
- "cryptofield", // b.cryptoField.eraseRow (cryptofield.vacuum.skipped — F-RTBF-2 vacuum-after-erase signal when DB not initialized at erase time)
308
+ "cryptofield", // b.cryptoField.eraseRow (cryptofield.vacuum.skipped — vacuum-after-erase signal when DB not initialized at erase time)
309
309
  "acme", // b.acme (acme.account.registered / order.* / cert.issued / cert.renewed / cert.renew.skipped — RFC 8555 + RFC 9773 ARI workflow)
310
310
  "cert", // b.cert (cert.account.generated / cert.issued / cert.renewed / cert.renew-failed / cert.challenge-cleanup — turnkey cert-manager lifecycle)
311
311
  "tls", // b.router 0-RTT posture (tls.0rtt.refused / tls.0rtt.replayed) — RFC 8446 §8 anti-replay surface // RFC number in prose
@@ -1620,7 +1620,7 @@ async function assertSegregation(opts) {
1620
1620
  return { ok: ok, missing: missing };
1621
1621
  }
1622
1622
 
1623
- // applyPosture — F-POSTURE-1 cascade hook. b.compliance.set(posture)
1623
+ // applyPosture — cascade hook. b.compliance.set(posture)
1624
1624
  // calls this to record the active posture so audit emissions can
1625
1625
  // surface the regulatory regime in metadata where downstream tooling
1626
1626
  // (forensic export, SIEM correlation) needs it. The chain itself is
@@ -577,7 +577,7 @@ function create(opts) {
577
577
  "ciba.parseNotification: empty bearer or no expected token configured");
578
578
  }
579
579
  // Constant-time compare on the SHA3 hash of both tokens —
580
- // matches the project-wide discipline (audit 2026-05-11). Both
580
+ // matches the project-wide discipline. Both
581
581
  // sides are fixed-width sha3-512 hex strings; timingSafeEqual
582
582
  // adds explicit defense-in-depth over `!==` even though equal-
583
583
  // length JS string compare is already broadly understood as
@@ -437,7 +437,7 @@ async function verify(proof, opts) {
437
437
  }
438
438
 
439
439
  // nonce — when caller supplies expected nonce, payload MUST match.
440
- // Constant-time compare (audit 2026-05-15): the nonce is a server-
440
+ // Constant-time compare: the nonce is a server-
441
441
  // issued secret-shaped value matched against attacker-controlled
442
442
  // payload bytes. RFC 9449 §8 mandates the value be unpredictable;
443
443
  // a leaking compare reveals prefix bytes over many attempts. ath
@@ -179,7 +179,7 @@ function fromAssertion(opts) {
179
179
  return FAL3;
180
180
  }
181
181
 
182
- // AUTH-19 — FAL2 per NIST SP 800-63C-4 §5.2 requires "injection
182
+ // FAL2 per NIST SP 800-63C-4 §5.2 requires "injection
183
183
  // protection" on the back-channel: either the back-channel itself is
184
184
  // encrypted-and-authenticated (mTLS / signed transport) OR the
185
185
  // assertion is encrypted to the RP. A plain HTTP back-channel with
@@ -73,7 +73,7 @@ var REFUSE_STATUS = {
73
73
  // FIDO MDS3 §3.1.4 — attestation-key compromise means the
74
74
  // manufacturer's batch-signing key is suspect; every credential
75
75
  // attested under that key MUST be refused. Pre-v0.9.2 this token
76
- // was missing from the refuse-list (audit 2026-05-11).
76
+ // was missing from the refuse-list.
77
77
  ATTESTATION_KEY_COMPROMISE: 1,
78
78
  };
79
79
 
@@ -362,7 +362,6 @@ function _verifyAndParseBlob(token) {
362
362
  // cache; an attacker serving an ancient signed-but-expired BLOB
363
363
  // could keep operators on a revoked-authenticator-list-frozen-at-X.
364
364
  // Refuse at parse time so neither fetch nor cache lookup honors it.
365
- // (Audit 2026-05-11.)
366
365
  if (nextUpdate.getTime() < Date.now()) {
367
366
  throw new FidoMds3Error("fido-mds3/blob-stale",
368
367
  "BLOB payload nextUpdate \"" + payload.nextUpdate +
@@ -619,7 +618,7 @@ function verifyAuthenticator(blob, registrationInfo, vopts) {
619
618
  }
620
619
  var entry = lookupAaguid(blob, registrationInfo.aaguid);
621
620
  if (!entry) {
622
- // Fail-CLOSED default for unknown AAGUIDs (audit 2026-05-11).
621
+ // Fail-CLOSED default for unknown AAGUIDs.
623
622
  // Pre-v0.9.2 default was `ok: true, reason: "aaguid-not-in-blob"`
624
623
  // — an attacker registering a credential with an AAGUID not in
625
624
  // the BLOB (rogue authenticator, fake hardware) silently passed.
@@ -271,7 +271,7 @@ function _selectKey(keys, header, vopts) {
271
271
  throw new AuthError("auth-jwt-external/no-matching-kid",
272
272
  "no JWKS key matches header.kid='" + header.kid + "'");
273
273
  }
274
- // Refuse kid-less tokens by default (audit 2026-05-11). JWKS
274
+ // Refuse kid-less tokens by default. JWKS
275
275
  // rotation creates a window where the rotated-out key is still
276
276
  // cached but the rotated-in key is already published; an
277
277
  // attacker shipping a kid-less token gets the lone-key path
@@ -301,7 +301,7 @@ async function verifyExternal(token, opts) {
301
301
  "audience", "issuer", "subject", "clockSkewMs",
302
302
  // v0.9.4 — opt-out for the kid-less-token JWKS-of-one refusal
303
303
  // (default refuses; non-conforming IdPs that emit kid-less tokens
304
- // set this true). Audit 2026-05-11.
304
+ // set this true).
305
305
  "allowKidlessJwks",
306
306
  ], "auth.jwt.verifyExternal");
307
307
 
@@ -621,7 +621,7 @@ function create(opts) {
621
621
  // constrained tokens (DPoP / mTLS) can opt out by NOT supplying
622
622
  // a seen callback.
623
623
  //
624
- // Atomic check-and-insert (audit 2026-05-11) — pre-v0.9.3 the
624
+ // Atomic check-and-insert — pre-v0.9.3 the
625
625
  // check ran via `ropts.seen(token)` which was a check-then-act
626
626
  // race: two concurrent refresh requests landed on the same
627
627
  // event-loop tick could both see `seen === false` and both POST
@@ -648,7 +648,7 @@ function create(opts) {
648
648
  // Spec contract: inserted===true → first sighting (OK);
649
649
  // inserted===false → replay. v0.9.3 had this inverted, which
650
650
  // broke every first refresh attempt for operators reusing an
651
- // existing b.nonceStore-style backend. (Reported 2026-05-12.)
651
+ // existing b.nonceStore-style backend.
652
652
  alreadySeen = inserted === false;
653
653
  } else if (typeof ropts.seen === "function") {
654
654
  // Legacy non-atomic path. Documented as a check-then-act race;
@@ -773,7 +773,7 @@ function create(opts) {
773
773
  // Constant-time compare on the CSRF state token. Project
774
774
  // discipline (auth/dpop.js, mail-srs.js, webhook.js) is
775
775
  // timingSafeEqual for any secret-shaped value compared
776
- // against attacker-controlled input. (Audit 2026-05-11.)
776
+ // against attacker-controlled input.
777
777
  if (typeof query.state !== "string" ||
778
778
  !cryptoTimingSafeEqual(query.state, popts.expectedState)) {
779
779
  throw new OAuthError("auth-oauth/state-mismatch",
@@ -971,7 +971,7 @@ function create(opts) {
971
971
  // surface as `["admin", "read"]` and the operator's scope
972
972
  // allowlist saw two distinct scopes. Spec-strict split on
973
973
  // single-space + reject scope tokens that contain non-token
974
- // chars. (Audit 2026-05-11.)
974
+ // chars.
975
975
  scope: raw.scope ? raw.scope.split(" ").filter(function (s) { return s.length > 0; }) : scope.slice(),
976
976
  raw: raw,
977
977
  };
@@ -1052,7 +1052,7 @@ function create(opts) {
1052
1052
  // verifier in the framework (jwt.js, jwt-external.js, dpop.js)
1053
1053
  // refuses; verifyIdToken previously silently ignored, letting an
1054
1054
  // attacker-controlled OP ship critical extensions the verifier
1055
- // doesn't understand. (Audit 2026-05-11.)
1055
+ // doesn't understand.
1056
1056
  if (header.crit !== undefined && header.crit !== null) {
1057
1057
  throw new OAuthError("auth-oauth/crit-not-supported",
1058
1058
  "ID token JWS header carries 'crit' extension list; this verifier does not " +
@@ -1072,7 +1072,7 @@ function create(opts) {
1072
1072
  // key was still cached at the IdP but the rotated-in key is
1073
1073
  // already published. Refuse kid-less tokens unconditionally —
1074
1074
  // every modern IdP includes kid; absent kid is a spec smell.
1075
- // (Audit 2026-05-11.) Operators with non-conforming IdPs that
1075
+ // Operators with non-conforming IdPs that
1076
1076
  // genuinely emit kid-less tokens can opt out via
1077
1077
  // vopts.allowKidlessJwks = true with a logged warning.
1078
1078
  if (!match) {
@@ -1117,7 +1117,7 @@ function create(opts) {
1117
1117
  // ES256 signature attempted against an RS256 key returned by a
1118
1118
  // hostile or buggy IdP with duplicate kids). Wrap so the panic
1119
1119
  // becomes a typed AuthError, matching the discipline in
1120
- // jwt-external.js + dpop.js. (Audit 2026-05-11.)
1120
+ // jwt-external.js + dpop.js.
1121
1121
  var verified;
1122
1122
  try {
1123
1123
  verified = nodeCrypto.verify(params.hash, Buffer.from(signingInput, "ascii"), verifyOpts, sig);
@@ -1179,7 +1179,7 @@ function create(opts) {
1179
1179
  }
1180
1180
  if (vopts.nonce && !vopts.skipNonceCheck) {
1181
1181
  // Constant-time nonce compare — secret-shaped value matched
1182
- // against attacker-controlled payload. (Audit 2026-05-11.)
1182
+ // against attacker-controlled payload.
1183
1183
  if (typeof payload.nonce !== "string" ||
1184
1184
  !cryptoTimingSafeEqual(payload.nonce, vopts.nonce)) {
1185
1185
  throw new OAuthError("auth-oauth/nonce-mismatch",
@@ -1212,7 +1212,7 @@ function create(opts) {
1212
1212
  // supplied; an operator typo could ship `http://` or
1213
1213
  // `javascript:`. Route through the framework's URL gate before
1214
1214
  // emitting so the URL is validated the same way as every other
1215
- // operator-supplied OAuth URL (audit 2026-05-15).
1215
+ // operator-supplied OAuth URL.
1216
1216
  _validateUrl(uopts.postLogoutRedirectUri, allowHttp, "postLogoutRedirectUri");
1217
1217
  params.set("post_logout_redirect_uri", uopts.postLogoutRedirectUri);
1218
1218
  }