@blamejs/core 0.15.13 → 0.15.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/lib/a2a-tasks.js +38 -6
  3. package/lib/agent-event-bus.js +13 -0
  4. package/lib/agent-idempotency.js +5 -1
  5. package/lib/agent-snapshot.js +32 -2
  6. package/lib/ai-aedt-bias-audit.js +2 -1
  7. package/lib/ai-content-detect.js +1 -3
  8. package/lib/ai-frontier-protocol.js +1 -1
  9. package/lib/ai-model-manifest.js +1 -1
  10. package/lib/ai-output.js +16 -7
  11. package/lib/api-snapshot.js +4 -1
  12. package/lib/app-shutdown.js +7 -1
  13. package/lib/archive-gz.js +9 -0
  14. package/lib/archive-read.js +9 -7
  15. package/lib/archive-tar-read.js +51 -8
  16. package/lib/archive.js +4 -2
  17. package/lib/asn1-der.js +70 -22
  18. package/lib/atomic-file.js +204 -2
  19. package/lib/audit-chain.js +54 -5
  20. package/lib/audit-daily-review.js +12 -2
  21. package/lib/audit-sign.js +2 -1
  22. package/lib/audit-tools.js +108 -23
  23. package/lib/audit.js +7 -2
  24. package/lib/auth/access-lock.js +2 -2
  25. package/lib/auth/bot-challenge.js +1 -1
  26. package/lib/auth/ciba.js +43 -4
  27. package/lib/auth/dpop.js +6 -1
  28. package/lib/auth/fido-mds3.js +5 -1
  29. package/lib/auth/jwt.js +2 -2
  30. package/lib/auth/lockout.js +18 -2
  31. package/lib/auth/passkey.js +1 -1
  32. package/lib/auth/password.js +1 -1
  33. package/lib/auth/saml.js +33 -13
  34. package/lib/auth/sd-jwt-vc.js +24 -4
  35. package/lib/auth/status-list.js +14 -2
  36. package/lib/auth/step-up.js +9 -1
  37. package/lib/auth-bot-challenge.js +21 -2
  38. package/lib/backup/bundle.js +7 -2
  39. package/lib/backup/crypto.js +23 -8
  40. package/lib/backup/index.js +41 -20
  41. package/lib/backup/manifest.js +7 -1
  42. package/lib/break-glass.js +41 -22
  43. package/lib/cbor.js +34 -11
  44. package/lib/cdn-cache-control.js +7 -3
  45. package/lib/cert.js +5 -3
  46. package/lib/cli.js +5 -1
  47. package/lib/cloud-events.js +3 -2
  48. package/lib/cluster-storage.js +7 -3
  49. package/lib/codepoint-class.js +17 -0
  50. package/lib/compliance-eaa.js +1 -1
  51. package/lib/compliance-sanctions.js +9 -7
  52. package/lib/compliance.js +1 -1
  53. package/lib/config-drift.js +22 -8
  54. package/lib/content-credentials.js +11 -8
  55. package/lib/content-digest.js +11 -4
  56. package/lib/cookies.js +10 -2
  57. package/lib/cose.js +20 -0
  58. package/lib/crdt.js +2 -1
  59. package/lib/crypto-field.js +29 -23
  60. package/lib/crypto.js +18 -4
  61. package/lib/csp.js +4 -0
  62. package/lib/daemon.js +4 -1
  63. package/lib/data-act.js +27 -4
  64. package/lib/db-file-lifecycle.js +14 -6
  65. package/lib/db-query.js +69 -9
  66. package/lib/db.js +32 -15
  67. package/lib/dora.js +43 -13
  68. package/lib/dr-runbook.js +1 -1
  69. package/lib/dsa.js +2 -1
  70. package/lib/dsr.js +22 -8
  71. package/lib/early-hints.js +19 -0
  72. package/lib/eat.js +5 -1
  73. package/lib/external-db.js +60 -4
  74. package/lib/fda-21cfr11.js +30 -5
  75. package/lib/flag-providers.js +6 -2
  76. package/lib/forms.js +1 -1
  77. package/lib/gate-contract.js +46 -5
  78. package/lib/gdpr-ropa.js +18 -9
  79. package/lib/graphql-federation.js +17 -4
  80. package/lib/guard-all.js +2 -2
  81. package/lib/guard-dsn.js +1 -1
  82. package/lib/guard-envelope.js +1 -1
  83. package/lib/guard-html.js +9 -11
  84. package/lib/guard-imap-command.js +1 -1
  85. package/lib/guard-jmap.js +1 -1
  86. package/lib/guard-json.js +14 -6
  87. package/lib/guard-mail-move.js +1 -1
  88. package/lib/guard-managesieve-command.js +1 -1
  89. package/lib/guard-pop3-command.js +1 -1
  90. package/lib/guard-smtp-command.js +1 -1
  91. package/lib/guard-svg.js +8 -9
  92. package/lib/html-balance.js +7 -3
  93. package/lib/http-client-cookie-jar.js +33 -12
  94. package/lib/http-client.js +225 -53
  95. package/lib/iab-tcf.js +3 -2
  96. package/lib/importmap-integrity.js +41 -1
  97. package/lib/incident-report.js +9 -6
  98. package/lib/json-patch.js +1 -1
  99. package/lib/json-path.js +24 -3
  100. package/lib/jtd.js +2 -2
  101. package/lib/legal-hold.js +24 -8
  102. package/lib/log.js +2 -2
  103. package/lib/mail-agent.js +2 -2
  104. package/lib/mail-arf.js +1 -1
  105. package/lib/mail-auth.js +3 -3
  106. package/lib/mail-bimi.js +16 -16
  107. package/lib/mail-bounce.js +3 -3
  108. package/lib/mail-crypto-smime.js +71 -6
  109. package/lib/mail-deploy.js +9 -5
  110. package/lib/mail-greylist.js +2 -4
  111. package/lib/mail-helo.js +2 -4
  112. package/lib/mail-journal.js +11 -8
  113. package/lib/mail-mdn.js +8 -4
  114. package/lib/mail-rbl.js +2 -4
  115. package/lib/mail-scan.js +3 -5
  116. package/lib/mail-server-jmap.js +4 -1
  117. package/lib/mail-server-registry.js +1 -1
  118. package/lib/mail-server-tls.js +9 -2
  119. package/lib/mail-spam-score.js +2 -4
  120. package/lib/mail-store-fts.js +1 -1
  121. package/lib/mail.js +22 -2
  122. package/lib/markup-tokenizer.js +24 -0
  123. package/lib/mcp.js +6 -4
  124. package/lib/mdoc.js +26 -3
  125. package/lib/metrics.js +14 -3
  126. package/lib/middleware/api-encrypt.js +2 -2
  127. package/lib/middleware/body-parser.js +10 -4
  128. package/lib/middleware/bot-guard.js +26 -18
  129. package/lib/middleware/clear-site-data.js +5 -1
  130. package/lib/middleware/compression.js +9 -0
  131. package/lib/middleware/cors.js +32 -23
  132. package/lib/middleware/csrf-protect.js +60 -21
  133. package/lib/middleware/daily-byte-quota.js +6 -4
  134. package/lib/middleware/fetch-metadata.js +28 -4
  135. package/lib/middleware/network-allowlist.js +61 -30
  136. package/lib/middleware/rate-limit.js +25 -16
  137. package/lib/middleware/scim-server.js +2 -1
  138. package/lib/middleware/security-headers.js +24 -6
  139. package/lib/middleware/speculation-rules.js +6 -3
  140. package/lib/middleware/tus-upload.js +2 -2
  141. package/lib/money.js +1 -1
  142. package/lib/mtls-ca.js +10 -6
  143. package/lib/network-dns-resolver.js +1 -1
  144. package/lib/network-dns.js +9 -2
  145. package/lib/network-dnssec.js +2 -1
  146. package/lib/network-smtp-policy.js +23 -5
  147. package/lib/network-tls.js +27 -5
  148. package/lib/network-tsig.js +2 -2
  149. package/lib/nis2-report.js +1 -1
  150. package/lib/nist-crosswalk.js +1 -1
  151. package/lib/ntp-check.js +28 -0
  152. package/lib/numeric-bounds.js +9 -0
  153. package/lib/object-store/azure-blob.js +1 -2
  154. package/lib/object-store/gcs-bucket-ops.js +4 -2
  155. package/lib/object-store/gcs.js +6 -4
  156. package/lib/object-store/http-put.js +1 -2
  157. package/lib/object-store/http-request.js +30 -1
  158. package/lib/object-store/local.js +37 -17
  159. package/lib/object-store/sigv4.js +1 -2
  160. package/lib/observability-otlp-exporter.js +20 -4
  161. package/lib/outbox.js +11 -4
  162. package/lib/parsers/safe-xml.js +1 -1
  163. package/lib/parsers/safe-yaml.js +21 -3
  164. package/lib/queue-local.js +10 -3
  165. package/lib/redact.js +7 -3
  166. package/lib/request-helpers.js +201 -23
  167. package/lib/resource-access-lock.js +3 -3
  168. package/lib/restore-bundle.js +46 -18
  169. package/lib/restore-rollback.js +10 -4
  170. package/lib/restore.js +19 -0
  171. package/lib/retention.js +20 -4
  172. package/lib/router.js +17 -4
  173. package/lib/safe-ical.js +2 -2
  174. package/lib/safe-icap.js +1 -1
  175. package/lib/safe-json.js +44 -0
  176. package/lib/safe-sieve.js +1 -1
  177. package/lib/safe-vcard.js +1 -1
  178. package/lib/sandbox-worker.js +6 -0
  179. package/lib/sandbox.js +1 -1
  180. package/lib/scheduler.js +17 -1
  181. package/lib/self-update-standalone-verifier.js +16 -0
  182. package/lib/session.js +27 -3
  183. package/lib/sql.js +3 -3
  184. package/lib/static.js +65 -13
  185. package/lib/template.js +7 -5
  186. package/lib/tenant-quota.js +52 -19
  187. package/lib/tsa.js +5 -2
  188. package/lib/vault/index.js +5 -0
  189. package/lib/vault/passphrase-ops.js +22 -26
  190. package/lib/vault/passphrase-source.js +8 -3
  191. package/lib/vault/rotate.js +13 -18
  192. package/lib/vault/seal-pem-file.js +4 -1
  193. package/lib/vc.js +1 -1
  194. package/lib/vendor/MANIFEST.json +10 -10
  195. package/lib/vendor/public-suffix-list.dat +23 -10
  196. package/lib/vendor/public-suffix-list.data.js +5498 -5494
  197. package/lib/webhook.js +16 -1
  198. package/lib/websocket.js +1 -1
  199. package/lib/worm.js +1 -1
  200. package/lib/ws-client.js +57 -46
  201. package/lib/x509-chain.js +44 -0
  202. package/package.json +1 -1
  203. package/sbom.cdx.json +6 -6
@@ -58,6 +58,7 @@ var nodeFs = require("node:fs");
58
58
  var nodePath = require("node:path");
59
59
  var pkg = require("../package.json");
60
60
  var atomicFile = require("./atomic-file");
61
+ var C = require("./constants");
61
62
  var auditChain = require("./audit-chain");
62
63
  var canonicalJson = require("./canonical-json");
63
64
  var auditSign = require("./audit-sign");
@@ -349,6 +350,12 @@ async function _defaultReadPredecessorRowHash(firstCounter) {
349
350
  async function _buildBundle(args) {
350
351
  var kind = args.kind;
351
352
  var rows = args.rows;
353
+ // Verification witnesses: the rows between the purgeable slice's tip and
354
+ // the covering checkpoint's anchored counter. They ride the bundle so the
355
+ // chain can be walked up to the SIGNED atRowHash, but they are NOT part of
356
+ // the purgeable range (the manifest range below is computed from `rows`,
357
+ // and purge() only deletes [first..lastCounter]).
358
+ var witnessRows = args.witnessRows || [];
352
359
  var checkpoint = args.checkpoint || null;
353
360
  var passphrase = args.passphrase;
354
361
  var predecessorRowHash = args.predecessorRowHash;
@@ -357,8 +364,9 @@ async function _buildBundle(args) {
357
364
  var lastRow = rows[rows.length - 1];
358
365
  var files = {};
359
366
 
360
- // 1. Encrypt the rows JSONL
361
- var jsonl = rows.map(function (r) {
367
+ // 1. Encrypt the rows JSONL — purgeable slice followed by any witnesses
368
+ // (contiguous, ascending) so the verifier walks one unbroken chain.
369
+ var jsonl = rows.concat(witnessRows).map(function (r) {
362
370
  return JSON.stringify(_rowToWireForm(r));
363
371
  }).join("\n") + "\n";
364
372
  var rowsEnc = await backupCrypto.encryptWithFreshSalt(jsonl, passphrase);
@@ -437,26 +445,39 @@ async function _readBundle(inDir, passphrase) {
437
445
  "bundle directory does not exist: " + inDir);
438
446
  }
439
447
  var manifestPath = nodePath.join(inDir, "manifest.json");
440
- if (!nodeFs.existsSync(manifestPath)) {
441
- throw new AuditToolsError("audit-tools/no-manifest",
442
- "manifest.json missing in " + inDir);
443
- }
444
- var manifest = safeJson.parse(nodeFs.readFileSync(manifestPath, "utf8"));
448
+ // Capped fd-bound read (no existsSync check-then-read window): an externally-
449
+ // supplied bundle manifest is parsed before verification, so an oversized
450
+ // manifest.json would OOM the verifier before safeJson sees it. 4 MiB is far
451
+ // above any real manifest.
452
+ var manifest = safeJson.parse(atomicFile.fdSafeReadSync(manifestPath, {
453
+ maxBytes: C.BYTES.mib(4), encoding: "utf8",
454
+ errorFor: function (kind, detail) {
455
+ if (kind === "enoent") return new AuditToolsError("audit-tools/no-manifest", "manifest.json missing in " + inDir);
456
+ if (kind === "too-large") return new AuditToolsError("audit-tools/bad-format", "manifest.json too large (" + detail.size + " > " + detail.max + ")");
457
+ return new AuditToolsError("audit-tools/bad-format", "manifest.json unreadable: " + kind);
458
+ },
459
+ }), { maxBytes: C.BYTES.mib(4) });
445
460
  if (!manifest || manifest.format !== BUNDLE_FORMAT) {
446
461
  throw new AuditToolsError("audit-tools/bad-format",
447
462
  "manifest.format is not " + BUNDLE_FORMAT);
448
463
  }
449
- if (!VALID_KINDS[manifest.kind]) {
464
+ if (!Object.prototype.hasOwnProperty.call(VALID_KINDS, manifest.kind)) {
450
465
  throw new AuditToolsError("audit-tools/bad-kind",
451
466
  "manifest.kind must be one of " + Object.keys(VALID_KINDS).join(", "));
452
467
  }
453
468
 
454
469
  var rowsEncPath = nodePath.join(inDir, frameworkFiles.fileName("rowsEnc"));
455
- if (!nodeFs.existsSync(rowsEncPath)) {
456
- throw new AuditToolsError("audit-tools/no-rows-blob",
457
- "rows.enc missing in " + inDir);
458
- }
459
- var rowsEnc = nodeFs.readFileSync(rowsEncPath);
470
+ // Capped fd-bound read: rows.enc is the PQC-encrypted archived audit slice
471
+ // (can be large); a hostile multi-GB blob would be read + decrypted in memory
472
+ // before the checksum check. 512 MiB ceiling bounds it (opt-tunable).
473
+ var rowsEnc = atomicFile.fdSafeReadSync(rowsEncPath, {
474
+ maxBytes: C.BYTES.mib(512),
475
+ errorFor: function (kind) {
476
+ if (kind === "enoent") return new AuditToolsError("audit-tools/no-rows-blob", "rows.enc missing in " + inDir);
477
+ if (kind === "too-large") return new AuditToolsError("audit-tools/rows-too-large", "rows.enc exceeds the bundle size cap");
478
+ return new AuditToolsError("audit-tools/no-rows-blob", "rows.enc unreadable: " + kind);
479
+ },
480
+ });
460
481
  if (manifest.checksum && manifest.checksum.rowsSha3_512 &&
461
482
  backupCrypto.checksum(rowsEnc) !== manifest.checksum.rowsSha3_512) {
462
483
  throw new AuditToolsError("audit-tools/rows-checksum-mismatch",
@@ -470,11 +491,16 @@ async function _readBundle(inDir, passphrase) {
470
491
  var checkpoint = null;
471
492
  if (manifest.kind === KIND_ARCHIVE) {
472
493
  var ckptPath = nodePath.join(inDir, frameworkFiles.fileName("checkpointEnc"));
473
- if (!nodeFs.existsSync(ckptPath)) {
474
- throw new AuditToolsError("audit-tools/no-checkpoint-blob",
475
- "checkpoint.enc missing in " + inDir + " (archive bundles must include the covering checkpoint)");
476
- }
477
- var ckptEnc = nodeFs.readFileSync(ckptPath);
494
+ // Capped fd-bound read: checkpoint.enc is a single PQC-encrypted
495
+ // audit_checkpoints row (a few KiB); 4 MiB bounds a hostile blob.
496
+ var ckptEnc = atomicFile.fdSafeReadSync(ckptPath, {
497
+ maxBytes: C.BYTES.mib(4),
498
+ errorFor: function (kind) {
499
+ if (kind === "enoent") return new AuditToolsError("audit-tools/no-checkpoint-blob", "checkpoint.enc missing in " + inDir + " (archive bundles must include the covering checkpoint)");
500
+ if (kind === "too-large") return new AuditToolsError("audit-tools/checkpoint-too-large", "checkpoint.enc exceeds the cap");
501
+ return new AuditToolsError("audit-tools/no-checkpoint-blob", "checkpoint.enc unreadable: " + kind);
502
+ },
503
+ });
478
504
  if (manifest.checksum && manifest.checksum.checkpointSha3_512 &&
479
505
  backupCrypto.checksum(ckptEnc) !== manifest.checksum.checkpointSha3_512) {
480
506
  throw new AuditToolsError("audit-tools/checkpoint-checksum-mismatch",
@@ -562,10 +588,31 @@ async function archive(opts) {
562
588
 
563
589
  var predecessorRowHash = await readPredecessorHash(firstCounter);
564
590
 
591
+ // The signed checkpoint is the bundle's only unforgeable anchor; it commits
592
+ // the row at checkpoint.atMonotonicCounter. When that counter sits BEYOND
593
+ // the purgeable slice's tip (the operator archived a subset older than the
594
+ // last checkpoint), carry the in-between rows as verification witnesses so
595
+ // verifyBundle can chain-walk up to the anchored row and bind atRowHash to
596
+ // it. Without them an attacker could pair any genuine high-counter
597
+ // checkpoint with a wholly fabricated slice. The witnesses are NOT purged.
598
+ var anchorCounter = Number(checkpoint.atMonotonicCounter);
599
+ var witnessRows = [];
600
+ if (anchorCounter > lastCounter) {
601
+ witnessRows = await readRows({ firstCounter: lastCounter + 1, lastCounter: anchorCounter });
602
+ var witnessTip = witnessRows.length ? Number(witnessRows[witnessRows.length - 1].monotonicCounter) : null;
603
+ if (witnessTip !== anchorCounter) {
604
+ throw new AuditToolsError("audit-tools/anchor-rows-missing",
605
+ "archive: covering checkpoint anchors counter=" + anchorCounter +
606
+ " but the rows up to it are not all available (read up to " + witnessTip +
607
+ ") — cannot prove the slice chains to the signed anchor");
608
+ }
609
+ }
610
+
565
611
  if (returnBytes) {
566
612
  var built = await _buildBundle({
567
613
  kind: KIND_ARCHIVE,
568
614
  rows: rows,
615
+ witnessRows: witnessRows,
569
616
  checkpoint: checkpoint,
570
617
  passphrase: opts.passphrase,
571
618
  predecessorRowHash: predecessorRowHash,
@@ -582,6 +629,7 @@ async function archive(opts) {
582
629
  outDir: opts.out,
583
630
  kind: KIND_ARCHIVE,
584
631
  rows: rows,
632
+ witnessRows: witnessRows,
585
633
  checkpoint: checkpoint,
586
634
  passphrase: opts.passphrase,
587
635
  predecessorRowHash: predecessorRowHash,
@@ -769,26 +817,41 @@ async function verifyBundle(opts) {
769
817
  };
770
818
  }
771
819
 
772
- // 2. Confirm the stored firstRowHash + lastRowHash match the slice
820
+ // 2. Confirm the stored firstRowHash + lastRowHash match the PURGEABLE
821
+ // slice boundary. The slice tip is the row at range.lastCounter — not
822
+ // necessarily the physical last row, which may be a verification witness
823
+ // carried past the slice so the chain can reach the signed checkpoint.
824
+ var lastCounterN = Number(read.manifest.range.lastCounter);
825
+ var sliceLastRow = null;
826
+ for (var ri = 0; ri < read.rows.length; ri++) {
827
+ if (Number(read.rows[ri].monotonicCounter) === lastCounterN) { sliceLastRow = read.rows[ri]; break; }
828
+ }
773
829
  if (read.rows[0].rowHash !== read.manifest.range.firstRowHash) {
774
830
  return {
775
831
  ok: false, kind: read.manifest.kind, rowsVerified: read.rows.length,
776
832
  reason: "manifest.range.firstRowHash does not match first row's rowHash",
777
833
  };
778
834
  }
779
- if (read.rows[read.rows.length - 1].rowHash !== read.manifest.range.lastRowHash) {
835
+ if (!sliceLastRow || sliceLastRow.rowHash !== read.manifest.range.lastRowHash) {
780
836
  return {
781
837
  ok: false, kind: read.manifest.kind, rowsVerified: read.rows.length,
782
- reason: "manifest.range.lastRowHash does not match last row's rowHash",
838
+ reason: "manifest.range.lastRowHash does not match the slice row at lastCounter",
783
839
  };
784
840
  }
785
841
 
786
- // 3. (archive only) verify the covering checkpoint signature
842
+ // 3. (archive only) verify the covering checkpoint signature AND bind its
843
+ // anchored rowHash to the slice. The signature alone proves only that a
844
+ // checkpoint exists for some (counter, rowHash); without binding atRowHash
845
+ // to the archived rows, any genuine high-counter checkpoint could be paired
846
+ // with a wholly fabricated slice. The row at checkpoint.atMonotonicCounter
847
+ // must be present in the bundle (slice or witness) and its rowHash must
848
+ // equal checkpoint.atRowHash — mirroring b.audit.verifyCheckpoints against
849
+ // the live table.
787
850
  if (read.manifest.kind === KIND_ARCHIVE) {
788
851
  if (!read.checkpoint) {
789
852
  return { ok: false, kind: KIND_ARCHIVE, reason: "checkpoint missing from archive bundle" };
790
853
  }
791
- if (Number(read.checkpoint.atMonotonicCounter) < Number(read.manifest.range.lastCounter)) {
854
+ if (Number(read.checkpoint.atMonotonicCounter) < lastCounterN) {
792
855
  return {
793
856
  ok: false, kind: KIND_ARCHIVE,
794
857
  reason: "checkpoint atMonotonicCounter (" + read.checkpoint.atMonotonicCounter +
@@ -805,6 +868,28 @@ async function verifyBundle(opts) {
805
868
  };
806
869
  }
807
870
  }
871
+ // Bind: the row the signature anchors must be in the bundle and match.
872
+ var anchorCounterN = Number(read.checkpoint.atMonotonicCounter);
873
+ var anchoredRow = null;
874
+ for (var ai = 0; ai < read.rows.length; ai++) {
875
+ if (Number(read.rows[ai].monotonicCounter) === anchorCounterN) { anchoredRow = read.rows[ai]; break; }
876
+ }
877
+ if (!anchoredRow) {
878
+ return {
879
+ ok: false, kind: KIND_ARCHIVE,
880
+ reason: "checkpoint anchors counter=" + anchorCounterN +
881
+ " but no such row is present in the bundle — checkpoint not bound to the archived slice",
882
+ };
883
+ }
884
+ if (anchoredRow.rowHash !== read.checkpoint.atRowHash) {
885
+ return {
886
+ ok: false, kind: KIND_ARCHIVE,
887
+ reason: "checkpoint atRowHash does not match the bundle row at counter=" + anchorCounterN +
888
+ " — the signed anchor does not bind this slice",
889
+ expected: read.checkpoint.atRowHash,
890
+ actual: anchoredRow.rowHash,
891
+ };
892
+ }
808
893
  }
809
894
 
810
895
  return {
package/lib/audit.js CHANGED
@@ -797,7 +797,12 @@ async function query(criteria) {
797
797
  if (criteria.resourceKind) q = q.where({ resourceKind: criteria.resourceKind });
798
798
  if (criteria.outcome) q = q.where({ outcome: criteria.outcome });
799
799
 
800
- q.orderBy("monotonicCounter", "asc");
800
+ // order: "asc" (default, chronological) | "desc" (newest-first). A capped
801
+ // query (limit set) returns the FIRST `limit` rows in this order — so a
802
+ // consumer that wants the most RECENT events under a cap (e.g. the daily
803
+ // review) must pass order:"desc", else `limit` keeps the OLDEST and drops the
804
+ // newest events in the window.
805
+ q.orderBy("monotonicCounter", criteria.order === "desc" ? "desc" : "asc");
801
806
  if (criteria.limit != null) q.limit(criteria.limit);
802
807
  if (criteria.offset != null) q.offset(criteria.offset);
803
808
 
@@ -834,7 +839,7 @@ async function _queryCluster(criteria) {
834
839
  if (criteria.resourceKind) qb.where("resourceKind", criteria.resourceKind);
835
840
  if (criteria.outcome) qb.where("outcome", criteria.outcome);
836
841
 
837
- qb.orderBy("monotonicCounter", "asc");
842
+ qb.orderBy("monotonicCounter", criteria.order === "desc" ? "desc" : "asc");
838
843
  if (criteria.limit != null) qb.limit(criteria.limit);
839
844
  if (criteria.offset != null) qb.offset(criteria.offset);
840
845
 
@@ -54,7 +54,7 @@ var SAFE_METHODS = Object.freeze({ GET: 1, HEAD: 1, OPTIONS: 1 });
54
54
  function _normalizeMode(mode) {
55
55
  if (typeof mode !== "string") return null;
56
56
  var m = mode.toLowerCase();
57
- return VALID_MODES[m] ? m : null;
57
+ return Object.prototype.hasOwnProperty.call(VALID_MODES, m) ? m : null;
58
58
  }
59
59
 
60
60
  function create(opts) {
@@ -164,7 +164,7 @@ function create(opts) {
164
164
  if (_hasUnlockRole(req)) return next();
165
165
  if (currentMode === "read-only") {
166
166
  var method = (req.method || "GET").toUpperCase();
167
- if (SAFE_METHODS[method]) return next();
167
+ if (Object.prototype.hasOwnProperty.call(SAFE_METHODS, method)) return next();
168
168
  _emitAudit("refused", "denied", { mode: currentMode, method: method, path: req.url });
169
169
  _emitMetric("refused", 1, { mode: currentMode, reason: "non-safe-method" });
170
170
  return _refuse(res, "non-safe-method-in-read-only");
@@ -343,7 +343,7 @@ function create(opts) {
343
343
  var secret = opts.secret;
344
344
 
345
345
  var providerKey = opts.provider !== undefined ? opts.provider : DEFAULT_PROVIDER;
346
- if (typeof providerKey !== "string" || !PROVIDERS[providerKey]) {
346
+ if (typeof providerKey !== "string" || !Object.prototype.hasOwnProperty.call(PROVIDERS, providerKey)) {
347
347
  var supported = Object.keys(PROVIDERS).join(", ");
348
348
  throw new BotChallengeError("bot-challenge/bad-opt",
349
349
  "provider: expected one of [" + supported + "], got " + JSON.stringify(providerKey));
package/lib/auth/ciba.js CHANGED
@@ -452,6 +452,23 @@ function create(opts) {
452
452
  });
453
453
  }
454
454
 
455
+ // Verify an OIDC ID token (if present) via the composed inner OAuth client,
456
+ // returning its verified claims, or null when no id_token was supplied. A
457
+ // present-but-invalid id_token throws (fail-closed) so a forged token's
458
+ // sub/acr/amr can never be returned as trusted.
459
+ async function _verifyIdTokenIfPresent(idToken, auditKey, op) {
460
+ if (!idToken) return null;
461
+ try {
462
+ return await inner.verifyIdToken(idToken);
463
+ } catch (e) {
464
+ _emitAudit("id_token_verify_fail", "failure",
465
+ auditKey ? { authReqIdHash: sha3Hash(auditKey) } : {});
466
+ throw new AuthError("auth-ciba/id-token-invalid",
467
+ "ciba." + op + ": id_token failed verification: " +
468
+ ((e && e.code) || (e && e.message) || String(e)));
469
+ }
470
+ }
471
+
455
472
  async function pollToken(popts) {
456
473
  popts = popts || {};
457
474
  if (typeof popts.authReqId !== "string" || popts.authReqId.length === 0) {
@@ -514,6 +531,12 @@ function create(opts) {
514
531
  }
515
532
  // Token issued — clear interval tracking for this authReqId.
516
533
  _intervalState.delete(popts.authReqId);
534
+ // Verify the ID token (OIDC Core §3.1.3.7) via the composed inner OAuth
535
+ // client — it applies the create()-level issuer / clientId / JWKS /
536
+ // accepted-algorithms and enforces signature + iss/aud/exp. Returning an
537
+ // unverified id_token let a forged token's sub/acr/amr be trusted.
538
+ var pollClaims = await _verifyIdTokenIfPresent(rv.id_token,
539
+ "auth-ciba:" + popts.authReqId, "pollToken");
517
540
  _emitAudit("token_received", "success", {
518
541
  authReqIdHash: sha3Hash("auth-ciba:" + popts.authReqId),
519
542
  });
@@ -521,6 +544,7 @@ function create(opts) {
521
544
  return {
522
545
  accessToken: rv.access_token || null,
523
546
  idToken: rv.id_token || null,
547
+ claims: pollClaims,
524
548
  refreshToken: rv.refresh_token || null,
525
549
  tokenType: rv.token_type || null,
526
550
  scope: rv.scope || null,
@@ -545,17 +569,23 @@ function create(opts) {
545
569
  * - In **push** mode the body carries the full token-response
546
570
  * object; no follow-up call needed.
547
571
  *
572
+ * Async because a pushed `id_token` is verified (signature + iss/aud/exp)
573
+ * via the composed inner OAuth client before it is returned — the
574
+ * verified claims are surfaced as `claims`. A present-but-invalid
575
+ * id_token throws `auth-ciba/id-token-invalid` (the notification-token
576
+ * bearer authenticates the caller, never the token itself).
577
+ *
548
578
  * @opts
549
579
  * { body?: object } // pre-parsed body; defaults to req.body
550
580
  *
551
581
  * @example
552
- * app.post("/ciba/notify", function (req, res) {
553
- * var info = ciba.parseNotification(req, { body: req.body });
554
- * // → { authReqId, accessToken, idToken, ... }
582
+ * app.post("/ciba/notify", async function (req, res) {
583
+ * var info = await ciba.parseNotification(req, { body: req.body });
584
+ * // → { authReqId, accessToken, idToken, claims, ... }
555
585
  * res.statusCode = 204; res.end();
556
586
  * });
557
587
  */
558
- function parseNotification(req, popts) {
588
+ async function parseNotification(req, popts) {
559
589
  popts = popts || {};
560
590
  if (!req || !req.headers) {
561
591
  throw new AuthError("auth-ciba/bad-notification-req",
@@ -605,6 +635,14 @@ function create(opts) {
605
635
  throw new AuthError("auth-ciba/no-auth-req-id-in-body",
606
636
  "ciba.parseNotification: body missing auth_req_id");
607
637
  }
638
+ // Verify the pushed ID token (CIBA §10.2 / OIDC Core MUST) via the
639
+ // composed inner OAuth client before returning it as trusted. The
640
+ // notification-token bearer authenticates the CALLER, not the token; a
641
+ // leaked/observed notification token previously let an attacker inject
642
+ // arbitrary id_token claims the RP would trust on the documented
643
+ // "no follow-up call" push path.
644
+ var pushClaims = await _verifyIdTokenIfPresent(body.id_token,
645
+ "auth-ciba:" + body.auth_req_id, "parseNotification");
608
646
  _emitAudit("notification_received", "success", {
609
647
  authReqIdHash: sha3Hash("auth-ciba:" + body.auth_req_id),
610
648
  mode: deliveryMode,
@@ -614,6 +652,7 @@ function create(opts) {
614
652
  authReqId: body.auth_req_id,
615
653
  accessToken: body.access_token || null,
616
654
  idToken: body.id_token || null,
655
+ claims: pushClaims,
617
656
  refreshToken: body.refresh_token || null,
618
657
  tokenType: body.token_type || null,
619
658
  scope: body.scope || null,
package/lib/auth/dpop.js CHANGED
@@ -92,7 +92,7 @@ function thumbprint(key) {
92
92
  if (!key || typeof key !== "object" || typeof key.kty !== "string" || key.kty.length === 0) {
93
93
  throw new AuthError("auth-dpop/bad-jwk", "jwk must be an object with a kty");
94
94
  }
95
- if (!DPOP_KTY[key.kty]) {
95
+ if (!Object.prototype.hasOwnProperty.call(DPOP_KTY, key.kty)) {
96
96
  throw new AuthError("auth-dpop/refused-kty", "jwk.kty='" + key.kty + "' is not allowed (DPoP requires asymmetric kty)");
97
97
  }
98
98
  // The RFC 7638 thumbprint itself is computed by b.jwk.
@@ -348,6 +348,11 @@ async function verify(proof, opts) {
348
348
  "DPoP proof declares 'crit' header — refused");
349
349
  }
350
350
 
351
+ // Bind the declared alg to the embedded JWK's kty/curve before handing
352
+ // the self-asserted key to node:crypto — every other JWS verifier in the
353
+ // framework enforces this (alg-confusion family, CVE-2026-22817 class).
354
+ jwtExternal._assertAlgKtyMatch(header.alg, header.jwk);
355
+
351
356
  // Verify signature against the embedded jwk
352
357
  var key = _jwkToKeyObject(header.jwk);
353
358
  var params = _signParamsForAlg(header.alg);
@@ -43,6 +43,7 @@ var safeJson = require("../safe-json");
43
43
  var safeBuffer = require("../safe-buffer");
44
44
  var lazyRequire = require("../lazy-require");
45
45
  var validateOpts = require("../validate-opts");
46
+ var x509Chain = require("../x509-chain");
46
47
  var jwtExternal = require("./jwt-external");
47
48
  var _wa = require("../vendor/simplewebauthn-server.cjs");
48
49
  var { FidoMds3Error } = require("../framework-error");
@@ -239,7 +240,10 @@ function _validateChain(x5c, rootPems) {
239
240
  var root;
240
241
  try { root = new nodeCrypto.X509Certificate(rootPems[r]); }
241
242
  catch (_e) { continue; }
242
- if (tail.checkIssued(root) && tail.verify(root.publicKey)) {
243
+ // issuerValidlyIssued enforces basicConstraints cA:TRUE on the root in
244
+ // addition to the issuance + signature linkage — a non-CA cert cannot
245
+ // anchor the x5c chain (basicConstraints bypass, CVE-2002-0862 class).
246
+ if (x509Chain.issuerValidlyIssued(root, tail)) {
243
247
  anchored = true;
244
248
  break;
245
249
  }
package/lib/auth/jwt.js CHANGED
@@ -117,7 +117,7 @@ function _toKeyObject(pemOrKey, kind) {
117
117
  }
118
118
 
119
119
  function _resolveAlgorithm(alg) {
120
- if (typeof alg !== "string" || !ALGORITHM_TO_NODE[alg]) {
120
+ if (typeof alg !== "string" || !Object.prototype.hasOwnProperty.call(ALGORITHM_TO_NODE, alg)) {
121
121
  throw new AuthError("auth-jwt/unsupported-algorithm",
122
122
  "algorithm must be one of " + SUPPORTED_ALGORITHMS.join(", ") + " (got: " + alg + ")");
123
123
  }
@@ -222,7 +222,7 @@ async function verify(token, opts) {
222
222
  // Validate the allowlist itself — typoed entries should surface here,
223
223
  // not as silent "every token rejected."
224
224
  for (var i = 0; i < allowed.length; i++) {
225
- if (!ALGORITHM_TO_NODE[allowed[i]]) {
225
+ if (!Object.prototype.hasOwnProperty.call(ALGORITHM_TO_NODE, allowed[i])) {
226
226
  throw new AuthError("auth-jwt/unsupported-algorithm",
227
227
  "opts.algorithms[" + i + "] = '" + allowed[i] + "' is not in the supported list (" +
228
228
  SUPPORTED_ALGORITHMS.join(", ") + ")");
@@ -80,6 +80,7 @@
80
80
  */
81
81
 
82
82
  var C = require("../constants");
83
+ var numericBounds = require("../numeric-bounds");
83
84
  var lazyRequire = require("../lazy-require");
84
85
  var requestHelpers = require("../request-helpers");
85
86
  var validateOpts = require("../validate-opts");
@@ -119,7 +120,7 @@ function _requireString(name, val) {
119
120
  }
120
121
 
121
122
  function _requirePositiveInt(name, val) {
122
- if (typeof val !== "number" || !isFinite(val) || val < 1 || Math.floor(val) !== val) {
123
+ if (!numericBounds.isPositiveFiniteInt(val)) {
123
124
  throw _err("BAD_OPT", name + ": expected positive integer, got " +
124
125
  typeof val + " " + JSON.stringify(val));
125
126
  }
@@ -261,7 +262,22 @@ function create(opts) {
261
262
 
262
263
  // ---- Public surface ----
263
264
 
264
- async function recordFailure(key, callOpts) {
265
+ // Per-key serialization of the failure counter (read→increment→write on an
266
+ // async store): concurrent recordFailure calls for the same key would lose
267
+ // updates, letting parallel failures stay under the lockout threshold. A
268
+ // per-key promise chain applies them sequentially in-process.
269
+ var _recordChains = new Map();
270
+ function recordFailure(key, callOpts) {
271
+ var prev = _recordChains.get(key) || Promise.resolve();
272
+ var run = prev.then(function () { return _doRecordFailure(key, callOpts); },
273
+ function () { return _doRecordFailure(key, callOpts); });
274
+ var tail = run.then(function () {}, function () {});
275
+ _recordChains.set(key, tail);
276
+ tail.then(function () { if (_recordChains.get(key) === tail) _recordChains.delete(key); });
277
+ return run;
278
+ }
279
+
280
+ async function _doRecordFailure(key, callOpts) {
265
281
  _requireKey(key);
266
282
  callOpts = callOpts || {};
267
283
  var now = clock();
@@ -385,7 +385,7 @@ function _largeBlobExt(args) {
385
385
  var SUPPORT = { preferred: 1, required: 1 };
386
386
  var modes = 0;
387
387
  if (args.support !== undefined) {
388
- if (!SUPPORT[args.support]) {
388
+ if (!Object.prototype.hasOwnProperty.call(SUPPORT, args.support)) {
389
389
  throw new AuthError("auth-passkey/bad-largeblob-support",
390
390
  "extensions.largeBlob support must be 'preferred' or 'required'");
391
391
  }
@@ -282,7 +282,7 @@ function policy(opts) {
282
282
  // Apply named profile FIRST, then operator opts on top so the
283
283
  // operator can override profile defaults per-field.
284
284
  if (typeof opts.profile === "string" && opts.profile.length > 0) {
285
- if (!POLICY_PROFILES[opts.profile]) {
285
+ if (!Object.prototype.hasOwnProperty.call(POLICY_PROFILES, opts.profile)) {
286
286
  throw new AuthError("auth-password/bad-policy",
287
287
  "policy.profile must be one of " + Object.keys(POLICY_PROFILES).join("/") +
288
288
  ", got " + JSON.stringify(opts.profile));
package/lib/auth/saml.js CHANGED
@@ -170,7 +170,7 @@ function _verifyXmldsig(envelope, signatureNode, certPem) {
170
170
  }
171
171
  var sigMethodNode = _findChild(signedInfo, "SignatureMethod");
172
172
  var sigAlgo = sigMethodNode && _attr(sigMethodNode, "Algorithm");
173
- if (!SUPPORTED_SIG[sigAlgo]) {
173
+ if (!Object.prototype.hasOwnProperty.call(SUPPORTED_SIG, sigAlgo)) {
174
174
  throw new AuthError("auth-saml/unsupported-sig-alg",
175
175
  "Unsupported SignatureMethod: " + sigAlgo);
176
176
  }
@@ -184,7 +184,7 @@ function _verifyXmldsig(envelope, signatureNode, certPem) {
184
184
  var refId = refUri.substring(1);
185
185
  var digestMethodNode = _findChild(refNode, "DigestMethod");
186
186
  var digestAlgo = digestMethodNode && _attr(digestMethodNode, "Algorithm");
187
- if (!SUPPORTED_DIGEST[digestAlgo]) {
187
+ if (!Object.prototype.hasOwnProperty.call(SUPPORTED_DIGEST, digestAlgo)) {
188
188
  throw new AuthError("auth-saml/unsupported-digest",
189
189
  "Unsupported DigestMethod: " + digestAlgo);
190
190
  }
@@ -745,26 +745,46 @@ function create(opts) {
745
745
  var cNotOnOrAfter = _attr(conditions, "NotOnOrAfter");
746
746
  if (cNotBefore) {
747
747
  var cnb = Date.parse(cNotBefore) / 1000; // ms→s
748
- if (isFinite(cnb) && cnb > nowSec + clockSkewSec) {
748
+ // Fail CLOSED on a present-but-unparseable bound (mirrors the Bearer
749
+ // SCD path at line ~708) instead of skipping the window via isFinite().
750
+ if (!isFinite(cnb)) {
751
+ throw new AuthError("auth-saml/conditions-bad-timestamp",
752
+ "Conditions NotBefore is present but unparseable");
753
+ }
754
+ if (cnb > nowSec + clockSkewSec) {
749
755
  throw new AuthError("auth-saml/conditions-not-yet-valid",
750
756
  "Conditions NotBefore is in the future");
751
757
  }
752
758
  }
753
759
  if (cNotOnOrAfter) {
754
760
  var cnoa = Date.parse(cNotOnOrAfter) / 1000; // ms→s
755
- if (isFinite(cnoa) && cnoa < nowSec - clockSkewSec) {
761
+ if (!isFinite(cnoa)) {
762
+ throw new AuthError("auth-saml/conditions-bad-timestamp",
763
+ "Conditions NotOnOrAfter is present but unparseable");
764
+ }
765
+ if (cnoa < nowSec - clockSkewSec) {
756
766
  throw new AuthError("auth-saml/conditions-expired",
757
767
  "Conditions NotOnOrAfter has passed");
758
768
  }
759
769
  }
760
- var ar = _findChild(conditions, "AudienceRestriction", SAML_NS.assertion);
761
- if (ar) {
762
- var audiences = _findAllChildren(ar, "Audience", SAML_NS.assertion).map(_textContent);
763
- if (audiences.indexOf(audience) === -1) {
764
- throw new AuthError("auth-saml/wrong-audience",
765
- "Audience \"" + audience + "\" not in assertion's AudienceRestriction (got " +
766
- JSON.stringify(audiences) + ")");
767
- }
770
+ }
771
+ // Audience binding — a signed assertion is bound to THIS SP via
772
+ // AudienceRestriction. A missing Conditions or AudienceRestriction means
773
+ // it is not bound here (audience-confusion: an assertion minted for another
774
+ // SP). Fail closed when an audience is configured; opt out only via
775
+ // vopts.requireAudienceRestriction === false.
776
+ if (audience && vopts.requireAudienceRestriction !== false) {
777
+ var ar = conditions && _findChild(conditions, "AudienceRestriction", SAML_NS.assertion);
778
+ if (!ar) {
779
+ throw new AuthError("auth-saml/no-audience-restriction",
780
+ "verifyResponse: assertion has no AudienceRestriction binding it to \"" +
781
+ audience + "\" (audience-confusion defense; set requireAudienceRestriction:false to opt out)");
782
+ }
783
+ var audiences = _findAllChildren(ar, "Audience", SAML_NS.assertion).map(_textContent);
784
+ if (audiences.indexOf(audience) === -1) {
785
+ throw new AuthError("auth-saml/wrong-audience",
786
+ "Audience \"" + audience + "\" not in assertion's AudienceRestriction (got " +
787
+ JSON.stringify(audiences) + ")");
768
788
  }
769
789
  }
770
790
 
@@ -1897,7 +1917,7 @@ function _verifyEmbeddedXmlDsig(xml, idpVerifyKey, idpVerifyAlg, expectedRootLoc
1897
1917
  // Allow either sha3-512 (framework default) or the SHA-2 family.
1898
1918
  var digestAlgName;
1899
1919
  if (digestUri === "http://www.w3.org/2007/05/xmldsig-more#sha3-512") digestAlgName = "sha3-512";
1900
- else if (SUPPORTED_DIGEST[digestUri]) digestAlgName = SUPPORTED_DIGEST[digestUri];
1920
+ else if (Object.prototype.hasOwnProperty.call(SUPPORTED_DIGEST, digestUri)) digestAlgName = SUPPORTED_DIGEST[digestUri];
1901
1921
  else {
1902
1922
  throw new AuthError("auth-saml/unsupported-digest",
1903
1923
  "_verifyEmbeddedXmlDsig: DigestMethod " + digestUri + " not supported");
@@ -212,7 +212,7 @@ function issue(opts) {
212
212
  "issue: algorithm must be one of " + SUPPORTED_ALGS.join(", "));
213
213
  }
214
214
  var hashAlg = opts.hashAlg || DEFAULT_HASH_ALG;
215
- if (!SUPPORTED_HASH_ALGS[hashAlg]) {
215
+ if (!Object.prototype.hasOwnProperty.call(SUPPORTED_HASH_ALGS, hashAlg)) {
216
216
  throw new AuthError("auth-sd-jwt-vc/bad-hash",
217
217
  "issue: hashAlg must be one of " + Object.keys(SUPPORTED_HASH_ALGS).join(", "));
218
218
  }
@@ -413,8 +413,8 @@ async function verify(presentation, opts) {
413
413
  validateOpts.requireObject(opts, "auth.sdJwtVc.verify", AuthError);
414
414
  validateOpts(opts, [
415
415
  "issuerKeyResolver", "audience", "nonce",
416
- "now", "expectedVct", "maxClockSkewSec",
417
- "requireKeyBinding",
416
+ "now", "expectedVct", "expectedIssuer", "maxClockSkewSec",
417
+ "requireKeyBinding", "requireExp",
418
418
  "keyAttestationVerifier", "requireKeyAttestation",
419
419
  ], "auth.sdJwtVc.verify");
420
420
 
@@ -528,6 +528,15 @@ async function verify(presentation, opts) {
528
528
  throw new AuthError("auth-sd-jwt-vc/iat-future",
529
529
  "verify: iat is in the future (clock skew?)");
530
530
  }
531
+ // SD-JWT-VC makes `exp` OPTIONAL, so the default only checks it when present.
532
+ // Operators with a time-bounded trust expectation (esp. for FOREIGN issuers
533
+ // resolved via issuerKeyResolver) pass requireExp:true to fail closed on a
534
+ // missing / non-numeric exp — mirroring jwt-external's unconditional exp
535
+ // requirement (a credential that never expires is accepted otherwise).
536
+ if (opts.requireExp === true && typeof jwtParsed.payload.exp !== "number") {
537
+ throw new AuthError("auth-sd-jwt-vc/missing-exp",
538
+ "verify: requireExp is set but the credential has no numeric exp claim");
539
+ }
531
540
  if (typeof jwtParsed.payload.exp === "number" && jwtParsed.payload.exp < nowSec - skew) {
532
541
  throw new AuthError("auth-sd-jwt-vc/expired",
533
542
  "verify: token is expired");
@@ -556,7 +565,7 @@ async function verify(presentation, opts) {
556
565
  // to its own DEFAULT_HASH_ALG (`sha3-512`) which broke verification
557
566
  // against spec-conformant issuers when `_sd_alg` was omitted.
558
567
  var hashAlg = jwtParsed.payload._sd_alg || "sha-256";
559
- if (!SUPPORTED_HASH_ALGS[hashAlg]) {
568
+ if (!Object.prototype.hasOwnProperty.call(SUPPORTED_HASH_ALGS, hashAlg)) {
560
569
  throw new AuthError("auth-sd-jwt-vc/bad-hash",
561
570
  "verify: _sd_alg \"" + hashAlg + "\" not supported");
562
571
  }
@@ -638,6 +647,17 @@ async function verify(presentation, opts) {
638
647
  jwtExternal._assertAlgKtyMatch(kbAlg, holderKey);
639
648
  var holderKeyObj = bCrypto.importPublicJwk(holderKey);
640
649
  var kbParsed = _verifyJwt(maybeKbJwt, holderKeyObj, kbAlg);
650
+ // A KB-JWT proves holder possession of THIS presentation's bytes (sd_hash),
651
+ // but binds it to a verifier + a fresh challenge only via aud + nonce. If
652
+ // the caller processes a KB-JWT without supplying both, the aud/nonce
653
+ // compares below would silently skip — making the presentation replayable
654
+ // and acceptable at any verifier. Fail CLOSED: require both (oid4vp always
655
+ // supplies them). RFC: SD-JWT-VC §KB-JWT / OIDC4VP nonce binding.
656
+ if (typeof opts.audience !== "string" || opts.audience.length === 0 ||
657
+ typeof opts.nonce !== "string" || opts.nonce.length === 0) {
658
+ throw new AuthError("auth-sd-jwt-vc/missing-replay-binding",
659
+ "verify: a KB-JWT requires opts.audience + opts.nonce to bind the presentation to this verifier and a fresh challenge (replay / audience-redirection defense)");
660
+ }
641
661
  // Constant-time compares: the nonce is a verifier-issued replay-defense
642
662
  // value, so a short-circuiting !== leaks a matching-prefix timing oracle.
643
663
  // Matches the sd_hash check below (the framework's hash/token discipline).