@blamejs/core 0.14.5 → 0.14.7

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 (93) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +4 -2
  3. package/lib/agent-event-bus.js +4 -4
  4. package/lib/agent-idempotency.js +6 -6
  5. package/lib/agent-orchestrator.js +9 -9
  6. package/lib/agent-posture-chain.js +10 -10
  7. package/lib/agent-saga.js +6 -7
  8. package/lib/agent-snapshot.js +8 -8
  9. package/lib/agent-stream.js +3 -3
  10. package/lib/agent-tenant.js +4 -4
  11. package/lib/agent-trace.js +5 -5
  12. package/lib/ai-disclosure.js +3 -3
  13. package/lib/app.js +2 -2
  14. package/lib/archive-read.js +1 -1
  15. package/lib/archive-tar-read.js +1 -1
  16. package/lib/archive-wrap.js +5 -5
  17. package/lib/audit-tools.js +65 -5
  18. package/lib/audit.js +2 -2
  19. package/lib/auth/ciba.js +1 -1
  20. package/lib/auth/dpop.js +1 -1
  21. package/lib/auth/fal.js +1 -1
  22. package/lib/auth/fido-mds3.js +2 -3
  23. package/lib/auth/jwt-external.js +2 -2
  24. package/lib/auth/oauth.js +9 -9
  25. package/lib/auth/oid4vci.js +7 -7
  26. package/lib/auth/oid4vp.js +1 -1
  27. package/lib/auth/openid-federation.js +5 -5
  28. package/lib/auth/passkey.js +6 -6
  29. package/lib/auth/saml.js +1 -1
  30. package/lib/auth/sd-jwt-vc.js +3 -6
  31. package/lib/backup/index.js +18 -18
  32. package/lib/cache.js +4 -4
  33. package/lib/calendar.js +5 -5
  34. package/lib/circuit-breaker.js +1 -1
  35. package/lib/cms-codec.js +2 -2
  36. package/lib/compliance.js +14 -14
  37. package/lib/cra-report.js +3 -3
  38. package/lib/crypto-field.js +58 -21
  39. package/lib/crypto.js +5 -6
  40. package/lib/db-query.js +131 -9
  41. package/lib/db.js +106 -22
  42. package/lib/external-db.js +64 -16
  43. package/lib/framework-schema.js +4 -4
  44. package/lib/guard-list-id.js +2 -2
  45. package/lib/guard-list-unsubscribe.js +1 -2
  46. package/lib/incident-report.js +150 -0
  47. package/lib/mail-crypto-smime.js +1 -1
  48. package/lib/mail-deploy.js +3 -3
  49. package/lib/mail-server-managesieve.js +2 -2
  50. package/lib/mail-server-pop3.js +2 -2
  51. package/lib/mail-store.js +1 -1
  52. package/lib/metrics.js +8 -8
  53. package/lib/middleware/age-gate.js +20 -7
  54. package/lib/middleware/bearer-auth.js +36 -35
  55. package/lib/middleware/bot-guard.js +17 -5
  56. package/lib/middleware/cors.js +28 -12
  57. package/lib/middleware/csrf-protect.js +23 -15
  58. package/lib/middleware/daily-byte-quota.js +27 -13
  59. package/lib/middleware/deny-response.js +140 -0
  60. package/lib/middleware/dpop.js +37 -24
  61. package/lib/middleware/fetch-metadata.js +21 -12
  62. package/lib/middleware/host-allowlist.js +19 -8
  63. package/lib/middleware/idempotency-key.js +21 -22
  64. package/lib/middleware/index.js +3 -0
  65. package/lib/middleware/network-allowlist.js +24 -10
  66. package/lib/middleware/protected-resource-metadata.js +2 -2
  67. package/lib/middleware/rate-limit.js +22 -5
  68. package/lib/middleware/require-aal.js +25 -10
  69. package/lib/middleware/require-auth.js +32 -16
  70. package/lib/middleware/require-bound-key.js +49 -18
  71. package/lib/middleware/require-content-type.js +19 -8
  72. package/lib/middleware/require-methods.js +17 -7
  73. package/lib/middleware/require-mtls.js +27 -14
  74. package/lib/network-dns-resolver.js +2 -2
  75. package/lib/network-dns.js +1 -2
  76. package/lib/network-tls.js +0 -1
  77. package/lib/network.js +4 -4
  78. package/lib/outbox.js +1 -1
  79. package/lib/pqc-agent.js +1 -1
  80. package/lib/retention.js +1 -1
  81. package/lib/retry.js +1 -1
  82. package/lib/safe-archive.js +2 -2
  83. package/lib/safe-ical.js +2 -2
  84. package/lib/safe-mime.js +1 -1
  85. package/lib/self-update-standalone-verifier.js +1 -1
  86. package/lib/self-update.js +2 -2
  87. package/lib/static.js +1 -1
  88. package/lib/subject.js +2 -2
  89. package/lib/vault/index.js +64 -1
  90. package/lib/vault/rotate.js +19 -0
  91. package/lib/vendor-data.js +1 -1
  92. package/package.json +1 -1
  93. package/sbom.cdx.json +6 -6
@@ -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
 
package/lib/audit.js CHANGED
@@ -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
package/lib/auth/ciba.js CHANGED
@@ -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
package/lib/auth/dpop.js CHANGED
@@ -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
package/lib/auth/fal.js CHANGED
@@ -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
 
package/lib/auth/oauth.js CHANGED
@@ -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
  }
@@ -117,7 +117,7 @@ function _verifyProofJwt(proofJwt, expectedAud, expectedCNonce, expectedClientId
117
117
  "credential issuance: proof JWT alg \"" + header.alg + "\" not in issuer-supported set " +
118
118
  "(alg-allowlist gate — refused before key lookup)");
119
119
  }
120
- // AUTH-5 / RFC 7515 §4.1.11 — refuse non-empty `crit`. Pre-v0.9.x
120
+ // RFC 7515 §4.1.11 — refuse non-empty `crit`. Pre-v0.9.x
121
121
  // silently ignored, letting an attacker-controlled wallet declare
122
122
  // critical extensions the verifier doesn't understand.
123
123
  if (header.crit !== undefined && header.crit !== null) {
@@ -136,7 +136,7 @@ function _verifyProofJwt(proofJwt, expectedAud, expectedCNonce, expectedClientId
136
136
  }
137
137
  if (expectedCNonce !== null) {
138
138
  // Constant-time c_nonce compare — secret-shaped value vs
139
- // attacker-controlled wallet payload. (Audit 2026-05-11.)
139
+ // attacker-controlled wallet payload.
140
140
  if (typeof payload.nonce !== "string" ||
141
141
  !timingSafeEqual(payload.nonce, expectedCNonce)) {
142
142
  throw new AuthError("auth-oid4vci/wrong-proof-nonce",
@@ -148,14 +148,14 @@ function _verifyProofJwt(proofJwt, expectedAud, expectedCNonce, expectedClientId
148
148
  "credential issuance: proof JWT must include iat");
149
149
  }
150
150
  var nowSec = Math.floor(Date.now() / C.TIME.seconds(1));
151
- // AUTH-34 — use C.TIME for the 60s skew tolerance rather than a bare
151
+ // Use C.TIME for the 60s skew tolerance rather than a bare
152
152
  // 60 literal; matches the framework's constants discipline.
153
153
  var iatSkewSec = C.TIME.seconds(60) / C.TIME.seconds(1);
154
154
  if (payload.iat > nowSec + iatSkewSec) {
155
155
  throw new AuthError("auth-oid4vci/proof-iat-future",
156
156
  "credential issuance: proof JWT iat is in the future");
157
157
  }
158
- // AUTH-26 — operator-tunable proof max-age. Default 10 minutes per
158
+ // Operator-tunable proof max-age. Default 10 minutes per
159
159
  // OID4VCI §7.2.1.1; operators with longer-lived wallet flows raise.
160
160
  var effectiveMaxAgeMs = (typeof proofMaxAgeMs === "number" && isFinite(proofMaxAgeMs) && proofMaxAgeMs > 0)
161
161
  ? proofMaxAgeMs
@@ -304,12 +304,12 @@ function create(opts) {
304
304
  var preAuthTtl = opts.preAuthCodeTtlMs || DEFAULT_PRE_AUTH_TTL_MS;
305
305
  var accessTokenTtl = opts.accessTokenTtlMs || DEFAULT_ACCESS_TOKEN_TTL;
306
306
  var cNonceTtl = opts.cNonceTtlMs || DEFAULT_C_NONCE_TTL_MS;
307
- // AUTH-26 — operator-tunable proof iat-too-old window. Default 10
307
+ // Operator-tunable proof iat-too-old window. Default 10
308
308
  // minutes per OID4VCI §7.2.1.1.
309
309
  var proofMaxAgeMs = (typeof opts.proofMaxAgeMs === "number" && isFinite(opts.proofMaxAgeMs) && opts.proofMaxAgeMs > 0)
310
310
  ? opts.proofMaxAgeMs
311
311
  : C.TIME.minutes(10);
312
- // AUTH-6 — access-token single-use. OID4VCI §7's credential endpoint
312
+ // Access-token single-use. OID4VCI §7's credential endpoint
313
313
  // does NOT inherently make the access token single-use; pre-v0.9.x
314
314
  // c_nonce rotation alone defended against proof replay, but a stolen
315
315
  // access token combined with a fresh proof could re-mint
@@ -575,7 +575,7 @@ function create(opts) {
575
575
  var newCNonce = generateToken(16); // 128-bit c_nonce
576
576
  await cNonceStore.set(iopts.accessToken, newCNonce);
577
577
 
578
- // AUTH-6 — when single-use is on (default), DELETE the access token
578
+ // When single-use is on (default), DELETE the access token
579
579
  // after successful credential mint. A stolen access token paired
580
580
  // with a fresh proof would otherwise re-mint credentials; the
581
581
  // c_nonce rotation alone defends against proof replay but not
@@ -452,7 +452,7 @@ function create(opts) {
452
452
  continue;
453
453
  }
454
454
  try {
455
- // Per-presentation vct enforcement (audit 2026-05-11): when
455
+ // Per-presentation vct enforcement: when
456
456
  // DCQL's `vct_values` has 1 entry, `expectedVct` pins it.
457
457
  // With 2+ entries the verifier's expectedVct opt can't hold
458
458
  // a list, so we verify-without-expected and then validate
@@ -169,7 +169,7 @@ function verifyEntityStatement(jwt, jwks, vopts) {
169
169
  "verifyEntityStatement: no JWKS key matches kid \"" + parsed.header.kid + "\"");
170
170
  }
171
171
  } else {
172
- // AUTH-10 — refuse kid-less entity statements unless the operator
172
+ // Refuse kid-less entity statements unless the operator
173
173
  // explicitly opts in. JWKS rotation creates a window where the
174
174
  // rotated-out key is still cached but the rotated-in key is already
175
175
  // published; a kid-less statement during that window gets the
@@ -218,7 +218,7 @@ function verifyEntityStatement(jwt, jwks, vopts) {
218
218
  }
219
219
 
220
220
  var nowSec = Math.floor(Date.now() / C.TIME.seconds(1));
221
- // AUTH-30 — operator-tunable clock skew (sibling primitives accept
221
+ // Operator-tunable clock skew (sibling primitives accept
222
222
  // tunable). Default matches the prior fixed 60s.
223
223
  var skew = (typeof vopts.maxClockSkewSec === "number" && isFinite(vopts.maxClockSkewSec) && vopts.maxClockSkewSec >= 0)
224
224
  ? vopts.maxClockSkewSec
@@ -436,7 +436,7 @@ async function buildTrustChain(opts) {
436
436
  var chain = [];
437
437
  var current = opts.leafEntityId;
438
438
  var depth = 0;
439
- // AUTH-9 — visited-set cycle guard. The maxDepth cap alone caps the
439
+ // Visited-set cycle guard. The maxDepth cap alone caps the
440
440
  // loop count but doesn't distinguish "long chain" from "cyclic
441
441
  // chain"; a hostile authority that lists itself in authority_hints
442
442
  // walks the verifier until depth runs out and then surfaces as
@@ -486,7 +486,7 @@ async function buildTrustChain(opts) {
486
486
  // operators with multiple federations usually have one anchor
487
487
  // active; we walk in order and pick the first success.
488
488
  // Track every per-authority failure reason and surface them on
489
- // `no-ascent` rather than masking. Audit 2026-05-11 — silently
489
+ // `no-ascent` rather than masking — silently
490
490
  // swallowing `catch (_e) {}` lets a hostile intermediate that
491
491
  // serves a malformed-then-valid pair shape-walk the verifier.
492
492
  // We continue past 404 / fetch errors but refuse on
@@ -513,7 +513,7 @@ async function buildTrustChain(opts) {
513
513
  chain[chain.length - 1].claims.jwks = parsedSub.claims.jwks || chain[chain.length - 1].claims.jwks;
514
514
  chain[chain.length - 1].subordinateJwt = subordinateJwt;
515
515
  chain[chain.length - 1].subordinate = parsedSub.claims;
516
- // AUTH-9 — refuse revisit. A trust anchor terminates the loop
516
+ // Refuse revisit. A trust anchor terminates the loop
517
517
  // before re-entry, so a revisit here ALWAYS means a cyclic
518
518
  // authority_hints graph.
519
519
  if (visited[authority]) {
@@ -77,7 +77,7 @@ function _requireString(v, name) {
77
77
  }
78
78
  }
79
79
 
80
- // AUTH-28 — WebAuthn extensions allowlist. Pre-v0.9.x `opts.extensions`
80
+ // WebAuthn extensions allowlist. Pre-v0.9.x `opts.extensions`
81
81
  // was forwarded verbatim to the vendor, letting an operator (or a
82
82
  // caller threading user-input through opts) ship arbitrary extension
83
83
  // keys to the authenticator. Restrict to the framework-supported
@@ -234,7 +234,7 @@ async function verifyRegistration(opts) {
234
234
  // <input autocomplete="webauthn">.
235
235
  // Null-prototype map so `opts.mediation === "__proto__"` /
236
236
  // `"constructor"` can't truthy-match an inherited property and slip
237
- // past the allowlist (audit 2026-05-11).
237
+ // past the allowlist.
238
238
  var ALLOWED_MEDIATION = Object.assign(Object.create(null),
239
239
  { silent: 1, optional: 1, required: 1, conditional: 1 });
240
240
 
@@ -314,7 +314,7 @@ function _b64urlExtInput(value, name, maxBytes) {
314
314
  // browser turns it into an ArrayBuffer before passing to the
315
315
  // authenticator).
316
316
  //
317
- // AUTH-29 — when `maxBytes` is set, refuse decoded inputs longer than
317
+ // When `maxBytes` is set, refuse decoded inputs longer than
318
318
  // the cap. Per CTAP2.1 §6.5 PRF salts are 32 bytes; pre-v0.9.x the
319
319
  // framework accepted arbitrary length, which is undefined behavior on
320
320
  // authenticators that may truncate / reject / behave inconsistently.
@@ -364,7 +364,7 @@ function _prfExt(args) {
364
364
  throw new AuthError("auth-passkey/missing-prf-first",
365
365
  "extensions.prf eval.first is required");
366
366
  }
367
- // AUTH-29 — CTAP2.1 §6.5 caps PRF salts at 32 bytes.
367
+ // CTAP2.1 §6.5 caps PRF salts at 32 bytes.
368
368
  var out = { prf: { eval: { first: _b64urlExtInput(args.eval.first, "eval.first", MAX_EXT_INPUT_BYTES) } } };
369
369
  if (args.eval.second !== undefined && args.eval.second !== null) {
370
370
  out.prf.eval.second = _b64urlExtInput(args.eval.second, "eval.second", MAX_EXT_INPUT_BYTES);
@@ -461,7 +461,7 @@ async function verifyAuthentication(opts) {
461
461
  throw new AuthError("auth-passkey/missing-credential",
462
462
  "opts.credential { id, publicKey, counter? } is required");
463
463
  }
464
- // Counter regression bypass fix (audit 2026-05-11) — pre-v0.9.2
464
+ // Counter regression bypass fix — pre-v0.9.2
465
465
  // shape `opts.credential.counter || 0` silently zeroed an
466
466
  // undefined / null / NaN counter, defeating CTAP 2.1 clone-
467
467
  // detection on credentials whose stored counter is > 0. An
@@ -525,7 +525,7 @@ async function verifyAuthentication(opts) {
525
525
  * @signature b.auth.passkey.compareBackupState(prev, current)
526
526
  * @since 0.9.57
527
527
  *
528
- * AUTH-27 — WebAuthn L3 §6.1.3. Inspect the credential's persisted BE
528
+ * WebAuthn L3 §6.1.3. Inspect the credential's persisted BE
529
529
  * (backupEligible) + BS (backupState) flags against the values
530
530
  * surfaced on a fresh assertion. Returns a normalized verdict the
531
531
  * operator routes into audit / step-up decisions:
package/lib/auth/saml.js CHANGED
@@ -717,7 +717,7 @@ function create(opts) {
717
717
  // Constant-time compare against the AuthnRequest ID the
718
718
  // operator stored — protects against timing-based InResponseTo
719
719
  // probing. timingSafeEqual returns false for missing /
720
- // length-mismatch without leaking. (Audit 2026-05-11.)
720
+ // length-mismatch without leaking.
721
721
  if (inResponseTo === null || inResponseTo === undefined ||
722
722
  !timingSafeEqual(inResponseTo, vopts.expectedInResponseTo)) {
723
723
  throw new AuthError("auth-saml/bad-in-response-to",
@@ -492,7 +492,7 @@ async function verify(presentation, opts) {
492
492
  jwtExternal._assertAlgKtyMatch(alg, issuerKey);
493
493
  }
494
494
  var jwtParsed = _verifyJwt(jwt, issuerKey, alg);
495
- // AUTH-25 — post-verify header compare. Pre-verify we parsed the
495
+ // Post-verify header compare. Pre-verify we parsed the
496
496
  // header bytes to look up the key; _verifyJwt parses again from the
497
497
  // cryptographically-verified signing input. Both decodes MUST yield
498
498
  // the same JSON; a mismatch indicates a JWS-canonicalization or
@@ -538,7 +538,6 @@ async function verify(presentation, opts) {
538
538
  // selective-disclosure-jwt §4.1.1). Earlier the framework defaulted
539
539
  // to its own DEFAULT_HASH_ALG (`sha3-512`) which broke verification
540
540
  // against spec-conformant issuers when `_sd_alg` was omitted.
541
- // (Audit 2026-05-11.)
542
541
  var hashAlg = jwtParsed.payload._sd_alg || "sha-256";
543
542
  if (!SUPPORTED_HASH_ALGS[hashAlg]) {
544
543
  throw new AuthError("auth-sd-jwt-vc/bad-hash",
@@ -566,7 +565,7 @@ async function verify(presentation, opts) {
566
565
  // Disclosure-replay defense — a holder presenting the same _sd
567
566
  // digest twice (with the same or different values) is malformed
568
567
  // per spec and is the shape of a partial-disclosure smuggling
569
- // attack. Refuse on duplicate digest. (Audit 2026-05-11.)
568
+ // attack. Refuse on duplicate digest.
570
569
  if (seenDigests[digest]) {
571
570
  throw new AuthError("auth-sd-jwt-vc/disclosure-replay",
572
571
  "verify: disclosure digest \"" + digest.slice(0, 12) +
@@ -622,9 +621,7 @@ async function verify(presentation, opts) {
622
621
  "verify: KB-JWT nonce mismatch (replay defense)");
623
622
  }
624
623
  // Validate KB-JWT sd_hash matches the presentation, using the
625
- // credential's declared `_sd_alg` (audit 2026-05-11 — was
626
- // hardcoded sha256 regardless of issuer's choice, breaking
627
- // verification when issuer used sha3-512).
624
+ // credential's declared `_sd_alg`.
628
625
  var kbHashInput = jwt + "~";
629
626
  if (disclosureParts.length > 0) kbHashInput += disclosureParts.join("~") + "~";
630
627
  var kbNodeHash = SUPPORTED_HASH_ALGS[hashAlg];