@blamejs/core 0.15.13 → 0.15.15

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 (208) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +38 -6
  4. package/lib/agent-event-bus.js +13 -0
  5. package/lib/agent-idempotency.js +5 -1
  6. package/lib/agent-snapshot.js +32 -2
  7. package/lib/ai-aedt-bias-audit.js +2 -1
  8. package/lib/ai-content-detect.js +1 -3
  9. package/lib/ai-frontier-protocol.js +1 -1
  10. package/lib/ai-model-manifest.js +1 -1
  11. package/lib/ai-output.js +16 -7
  12. package/lib/api-snapshot.js +4 -1
  13. package/lib/app-shutdown.js +7 -1
  14. package/lib/archive-gz.js +9 -0
  15. package/lib/archive-read.js +9 -7
  16. package/lib/archive-tar-read.js +51 -8
  17. package/lib/archive.js +4 -2
  18. package/lib/asn1-der.js +70 -22
  19. package/lib/atomic-file.js +204 -2
  20. package/lib/audit-chain.js +54 -5
  21. package/lib/audit-daily-review.js +12 -2
  22. package/lib/audit-sign.js +2 -1
  23. package/lib/audit-tools.js +108 -23
  24. package/lib/audit.js +7 -2
  25. package/lib/auth/access-lock.js +2 -2
  26. package/lib/auth/bot-challenge.js +1 -1
  27. package/lib/auth/ciba.js +71 -8
  28. package/lib/auth/dpop.js +15 -1
  29. package/lib/auth/fido-mds3.js +30 -13
  30. package/lib/auth/jwt.js +21 -5
  31. package/lib/auth/lockout.js +18 -2
  32. package/lib/auth/oauth.js +8 -2
  33. package/lib/auth/passkey.js +1 -1
  34. package/lib/auth/password.js +1 -1
  35. package/lib/auth/saml.js +42 -12
  36. package/lib/auth/sd-jwt-vc.js +24 -4
  37. package/lib/auth/status-list.js +14 -2
  38. package/lib/auth/step-up.js +9 -1
  39. package/lib/auth-bot-challenge.js +21 -2
  40. package/lib/backup/bundle.js +7 -2
  41. package/lib/backup/crypto.js +23 -8
  42. package/lib/backup/index.js +41 -20
  43. package/lib/backup/manifest.js +7 -1
  44. package/lib/break-glass.js +41 -22
  45. package/lib/cbor.js +34 -11
  46. package/lib/cdn-cache-control.js +7 -3
  47. package/lib/cert.js +5 -3
  48. package/lib/cli.js +5 -1
  49. package/lib/cloud-events.js +3 -2
  50. package/lib/cluster-storage.js +7 -3
  51. package/lib/codepoint-class.js +17 -0
  52. package/lib/compliance-eaa.js +1 -1
  53. package/lib/compliance-sanctions.js +9 -7
  54. package/lib/compliance.js +1 -1
  55. package/lib/config-drift.js +22 -8
  56. package/lib/content-credentials.js +11 -8
  57. package/lib/content-digest.js +11 -4
  58. package/lib/cookies.js +10 -2
  59. package/lib/cose.js +20 -0
  60. package/lib/crdt.js +2 -1
  61. package/lib/crypto-field.js +48 -24
  62. package/lib/crypto.js +18 -4
  63. package/lib/csp.js +13 -0
  64. package/lib/daemon.js +4 -1
  65. package/lib/data-act.js +27 -4
  66. package/lib/db-file-lifecycle.js +14 -6
  67. package/lib/db-query.js +102 -11
  68. package/lib/db.js +32 -15
  69. package/lib/dora.js +43 -13
  70. package/lib/dr-runbook.js +1 -1
  71. package/lib/dsa.js +2 -1
  72. package/lib/dsr.js +22 -8
  73. package/lib/early-hints.js +19 -0
  74. package/lib/eat.js +5 -1
  75. package/lib/external-db.js +60 -4
  76. package/lib/fda-21cfr11.js +30 -5
  77. package/lib/flag-providers.js +6 -2
  78. package/lib/forms.js +1 -1
  79. package/lib/gate-contract.js +46 -5
  80. package/lib/gdpr-ropa.js +18 -9
  81. package/lib/graphql-federation.js +17 -4
  82. package/lib/guard-all.js +2 -2
  83. package/lib/guard-dsn.js +1 -1
  84. package/lib/guard-envelope.js +1 -1
  85. package/lib/guard-html.js +9 -11
  86. package/lib/guard-imap-command.js +1 -1
  87. package/lib/guard-jmap.js +1 -1
  88. package/lib/guard-json.js +14 -6
  89. package/lib/guard-mail-move.js +1 -1
  90. package/lib/guard-managesieve-command.js +1 -1
  91. package/lib/guard-pop3-command.js +1 -1
  92. package/lib/guard-smtp-command.js +1 -1
  93. package/lib/guard-svg.js +8 -9
  94. package/lib/html-balance.js +7 -3
  95. package/lib/http-client-cookie-jar.js +33 -12
  96. package/lib/http-client.js +225 -53
  97. package/lib/iab-tcf.js +3 -2
  98. package/lib/importmap-integrity.js +41 -1
  99. package/lib/incident-report.js +9 -6
  100. package/lib/json-patch.js +1 -1
  101. package/lib/json-path.js +24 -3
  102. package/lib/jtd.js +2 -2
  103. package/lib/legal-hold.js +24 -8
  104. package/lib/log.js +2 -2
  105. package/lib/mail-agent.js +2 -2
  106. package/lib/mail-arf.js +1 -1
  107. package/lib/mail-auth.js +27 -4
  108. package/lib/mail-bimi.js +16 -16
  109. package/lib/mail-bounce.js +3 -3
  110. package/lib/mail-crypto-smime.js +71 -6
  111. package/lib/mail-deploy.js +9 -5
  112. package/lib/mail-dkim.js +20 -7
  113. package/lib/mail-greylist.js +2 -4
  114. package/lib/mail-helo.js +2 -4
  115. package/lib/mail-journal.js +11 -8
  116. package/lib/mail-mdn.js +8 -4
  117. package/lib/mail-rbl.js +2 -4
  118. package/lib/mail-scan.js +3 -5
  119. package/lib/mail-server-jmap.js +4 -1
  120. package/lib/mail-server-registry.js +1 -1
  121. package/lib/mail-server-tls.js +9 -2
  122. package/lib/mail-spam-score.js +2 -4
  123. package/lib/mail-store-fts.js +1 -1
  124. package/lib/mail.js +22 -2
  125. package/lib/markup-tokenizer.js +24 -0
  126. package/lib/mcp.js +6 -4
  127. package/lib/mdoc.js +26 -3
  128. package/lib/metrics.js +14 -3
  129. package/lib/middleware/api-encrypt.js +2 -2
  130. package/lib/middleware/body-parser.js +10 -4
  131. package/lib/middleware/bot-guard.js +26 -18
  132. package/lib/middleware/clear-site-data.js +5 -1
  133. package/lib/middleware/compose-pipeline.js +39 -5
  134. package/lib/middleware/compression.js +9 -0
  135. package/lib/middleware/cors.js +32 -23
  136. package/lib/middleware/csrf-protect.js +60 -21
  137. package/lib/middleware/daily-byte-quota.js +6 -4
  138. package/lib/middleware/fetch-metadata.js +28 -4
  139. package/lib/middleware/network-allowlist.js +61 -30
  140. package/lib/middleware/rate-limit.js +25 -16
  141. package/lib/middleware/scim-server.js +2 -1
  142. package/lib/middleware/security-headers.js +24 -6
  143. package/lib/middleware/speculation-rules.js +6 -3
  144. package/lib/middleware/tus-upload.js +2 -2
  145. package/lib/money.js +1 -1
  146. package/lib/mtls-ca.js +10 -6
  147. package/lib/network-dns-resolver.js +1 -1
  148. package/lib/network-dns.js +9 -2
  149. package/lib/network-dnssec.js +2 -1
  150. package/lib/network-smtp-policy.js +23 -5
  151. package/lib/network-tls.js +27 -5
  152. package/lib/network-tsig.js +2 -2
  153. package/lib/nis2-report.js +1 -1
  154. package/lib/nist-crosswalk.js +1 -1
  155. package/lib/ntp-check.js +28 -0
  156. package/lib/numeric-bounds.js +9 -0
  157. package/lib/object-store/azure-blob.js +1 -2
  158. package/lib/object-store/gcs-bucket-ops.js +4 -2
  159. package/lib/object-store/gcs.js +6 -4
  160. package/lib/object-store/http-put.js +1 -2
  161. package/lib/object-store/http-request.js +30 -1
  162. package/lib/object-store/local.js +37 -17
  163. package/lib/object-store/sigv4.js +1 -2
  164. package/lib/observability-otlp-exporter.js +20 -4
  165. package/lib/outbox.js +11 -4
  166. package/lib/parsers/safe-xml.js +1 -1
  167. package/lib/parsers/safe-yaml.js +21 -3
  168. package/lib/pipl-cn.js +11 -8
  169. package/lib/queue-local.js +10 -3
  170. package/lib/redact.js +7 -3
  171. package/lib/request-helpers.js +347 -36
  172. package/lib/resource-access-lock.js +3 -3
  173. package/lib/restore-bundle.js +46 -18
  174. package/lib/restore-rollback.js +10 -4
  175. package/lib/restore.js +19 -0
  176. package/lib/retention.js +20 -4
  177. package/lib/router.js +17 -4
  178. package/lib/safe-ical.js +2 -2
  179. package/lib/safe-icap.js +1 -1
  180. package/lib/safe-json.js +70 -0
  181. package/lib/safe-sieve.js +1 -1
  182. package/lib/safe-vcard.js +1 -1
  183. package/lib/sandbox-worker.js +6 -0
  184. package/lib/sandbox.js +1 -1
  185. package/lib/scheduler.js +17 -1
  186. package/lib/self-update-standalone-verifier.js +16 -0
  187. package/lib/session.js +62 -120
  188. package/lib/sql.js +25 -3
  189. package/lib/static.js +65 -13
  190. package/lib/template.js +7 -5
  191. package/lib/tenant-quota.js +52 -19
  192. package/lib/tsa.js +5 -2
  193. package/lib/vault/index.js +5 -0
  194. package/lib/vault/passphrase-ops.js +22 -26
  195. package/lib/vault/passphrase-source.js +8 -3
  196. package/lib/vault/rotate.js +13 -18
  197. package/lib/vault/seal-pem-file.js +4 -1
  198. package/lib/vc.js +1 -1
  199. package/lib/vendor/MANIFEST.json +10 -10
  200. package/lib/vendor/public-suffix-list.dat +23 -10
  201. package/lib/vendor/public-suffix-list.data.js +5498 -5494
  202. package/lib/webhook.js +16 -1
  203. package/lib/websocket.js +1 -1
  204. package/lib/worm.js +1 -1
  205. package/lib/ws-client.js +83 -46
  206. package/lib/x509-chain.js +91 -0
  207. package/package.json +1 -1
  208. 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
@@ -162,6 +162,10 @@ function create(opts) {
162
162
  tokenEndpoint: opts.tokenEndpoint,
163
163
  httpClientOpts: opts.httpClientOpts,
164
164
  allowHttp: opts.allowHttp === true,
165
+ // Thread the SSRF opt-in through to the inner client's discovery / JWKS /
166
+ // token fetches — an operator with an internal-network IdP needs it, and
167
+ // dropping it silently made allowInternal an accepted-but-ignored option.
168
+ allowInternal: opts.allowInternal,
165
169
  isOidc: true,
166
170
  });
167
171
 
@@ -452,6 +456,45 @@ function create(opts) {
452
456
  });
453
457
  }
454
458
 
459
+ // Verify an OIDC ID token (if present) via the composed inner OAuth client,
460
+ // returning its verified claims, or null when no id_token was supplied. A
461
+ // present-but-invalid id_token throws (fail-closed) so a forged token's
462
+ // sub/acr/amr can never be returned as trusted.
463
+ async function _verifyIdTokenIfPresent(idToken, auditKey, op, expectedAuthReqId) {
464
+ if (!idToken) return null;
465
+ var verified;
466
+ try {
467
+ verified = await inner.verifyIdToken(idToken);
468
+ } catch (e) {
469
+ _emitAudit("id_token_verify_fail", "failure",
470
+ auditKey ? { authReqIdHash: sha3Hash(auditKey) } : {});
471
+ throw new AuthError("auth-ciba/id-token-invalid",
472
+ "ciba." + op + ": id_token failed verification: " +
473
+ ((e && e.code) || (e && e.message) || String(e)));
474
+ }
475
+ // OpenID CIBA Core §7.3 / §10.1: the ID Token MUST carry the
476
+ // urn:openid:params:jwt:claim:auth_req_id claim and the RP MUST verify it
477
+ // equals the auth_req_id this flow used. Without it, an id_token minted for
478
+ // a DIFFERENT auth_req_id (another user's CIBA flow at the same RP) could be
479
+ // substituted — cross-user token substitution. verifyIdToken returns
480
+ // { header, claims }, so the claim lives on `.claims`. Constant-time compare.
481
+ // idToken is present (we returned early if not), so the binding is
482
+ // MANDATORY and ALWAYS checked — never gated behind a truthiness test that
483
+ // an empty string could slip. Both call sites pass a non-empty auth_req_id
484
+ // (validated at the parseNotification / pollToken entry), so a falsy value
485
+ // can't reach here to skip the comparison.
486
+ var payload = verified && verified.claims;
487
+ var boundId = payload && payload["urn:openid:params:jwt:claim:auth_req_id"];
488
+ if (typeof boundId !== "string" || !timingSafeEqual(boundId, expectedAuthReqId)) {
489
+ _emitAudit("id_token_authreqid_mismatch", "failure",
490
+ auditKey ? { authReqIdHash: sha3Hash(auditKey) } : {});
491
+ throw new AuthError("auth-ciba/id-token-authreqid-mismatch",
492
+ "ciba." + op + ": id_token urn:openid:params:jwt:claim:auth_req_id does not " +
493
+ "match the expected auth_req_id (cross-user token-substitution defense)");
494
+ }
495
+ return verified;
496
+ }
497
+
455
498
  async function pollToken(popts) {
456
499
  popts = popts || {};
457
500
  if (typeof popts.authReqId !== "string" || popts.authReqId.length === 0) {
@@ -514,6 +557,12 @@ function create(opts) {
514
557
  }
515
558
  // Token issued — clear interval tracking for this authReqId.
516
559
  _intervalState.delete(popts.authReqId);
560
+ // Verify the ID token (OIDC Core §3.1.3.7) via the composed inner OAuth
561
+ // client — it applies the create()-level issuer / clientId / JWKS /
562
+ // accepted-algorithms and enforces signature + iss/aud/exp. Returning an
563
+ // unverified id_token let a forged token's sub/acr/amr be trusted.
564
+ var pollClaims = await _verifyIdTokenIfPresent(rv.id_token,
565
+ "auth-ciba:" + popts.authReqId, "pollToken", popts.authReqId);
517
566
  _emitAudit("token_received", "success", {
518
567
  authReqIdHash: sha3Hash("auth-ciba:" + popts.authReqId),
519
568
  });
@@ -521,6 +570,7 @@ function create(opts) {
521
570
  return {
522
571
  accessToken: rv.access_token || null,
523
572
  idToken: rv.id_token || null,
573
+ claims: pollClaims,
524
574
  refreshToken: rv.refresh_token || null,
525
575
  tokenType: rv.token_type || null,
526
576
  scope: rv.scope || null,
@@ -545,17 +595,23 @@ function create(opts) {
545
595
  * - In **push** mode the body carries the full token-response
546
596
  * object; no follow-up call needed.
547
597
  *
598
+ * Async because a pushed `id_token` is verified (signature + iss/aud/exp)
599
+ * via the composed inner OAuth client before it is returned — the
600
+ * verified claims are surfaced as `claims`. A present-but-invalid
601
+ * id_token throws `auth-ciba/id-token-invalid` (the notification-token
602
+ * bearer authenticates the caller, never the token itself).
603
+ *
548
604
  * @opts
549
605
  * { body?: object } // pre-parsed body; defaults to req.body
550
606
  *
551
607
  * @example
552
- * app.post("/ciba/notify", function (req, res) {
553
- * var info = ciba.parseNotification(req, { body: req.body });
554
- * // → { authReqId, accessToken, idToken, ... }
608
+ * app.post("/ciba/notify", async function (req, res) {
609
+ * var info = await ciba.parseNotification(req, { body: req.body });
610
+ * // → { authReqId, accessToken, idToken, claims, ... }
555
611
  * res.statusCode = 204; res.end();
556
612
  * });
557
613
  */
558
- function parseNotification(req, popts) {
614
+ async function parseNotification(req, popts) {
559
615
  popts = popts || {};
560
616
  if (!req || !req.headers) {
561
617
  throw new AuthError("auth-ciba/bad-notification-req",
@@ -601,10 +657,16 @@ function create(opts) {
601
657
  throw new AuthError("auth-ciba/no-notification-body",
602
658
  "ciba.parseNotification: body required (Buffer/string parsed by middleware)");
603
659
  }
604
- if (typeof body.auth_req_id !== "string") {
605
- throw new AuthError("auth-ciba/no-auth-req-id-in-body",
606
- "ciba.parseNotification: body missing auth_req_id");
607
- }
660
+ validateOpts.requireNonEmptyString(body.auth_req_id,
661
+ "ciba.parseNotification: auth_req_id", AuthError, "auth-ciba/no-auth-req-id-in-body");
662
+ // Verify the pushed ID token (CIBA §10.2 / OIDC Core MUST) via the
663
+ // composed inner OAuth client before returning it as trusted. The
664
+ // notification-token bearer authenticates the CALLER, not the token; a
665
+ // leaked/observed notification token previously let an attacker inject
666
+ // arbitrary id_token claims the RP would trust on the documented
667
+ // "no follow-up call" push path.
668
+ var pushClaims = await _verifyIdTokenIfPresent(body.id_token,
669
+ "auth-ciba:" + body.auth_req_id, "parseNotification", body.auth_req_id);
608
670
  _emitAudit("notification_received", "success", {
609
671
  authReqIdHash: sha3Hash("auth-ciba:" + body.auth_req_id),
610
672
  mode: deliveryMode,
@@ -614,6 +676,7 @@ function create(opts) {
614
676
  authReqId: body.auth_req_id,
615
677
  accessToken: body.access_token || null,
616
678
  idToken: body.id_token || null,
679
+ claims: pushClaims,
617
680
  refreshToken: body.refresh_token || null,
618
681
  tokenType: body.token_type || null,
619
682
  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.
@@ -317,6 +317,15 @@ async function verify(proof, opts) {
317
317
  catch (_e) { throw new AuthError("auth-dpop/malformed", "header is not valid base64url-JSON"); }
318
318
  try { payload = safeJson.parse(_b64urlDecode(parts[1]).toString("utf8")); }
319
319
  catch (_e) { throw new AuthError("auth-dpop/malformed", "payload is not valid base64url-JSON"); }
320
+ // safeJson.parse accepts the literal `null` / scalars, so a header segment of
321
+ // base64url("null") would otherwise reach `header.typ` and throw a raw
322
+ // TypeError rather than the typed malformed error. Require JSON objects.
323
+ if (!safeJson.isJsonObject(header)) {
324
+ throw new AuthError("auth-dpop/malformed", "header is not a JSON object");
325
+ }
326
+ if (!safeJson.isJsonObject(payload)) {
327
+ throw new AuthError("auth-dpop/malformed", "payload is not a JSON object");
328
+ }
320
329
 
321
330
  // Header checks
322
331
  if (header.typ !== "dpop+jwt") {
@@ -348,6 +357,11 @@ async function verify(proof, opts) {
348
357
  "DPoP proof declares 'crit' header — refused");
349
358
  }
350
359
 
360
+ // Bind the declared alg to the embedded JWK's kty/curve before handing
361
+ // the self-asserted key to node:crypto — every other JWS verifier in the
362
+ // framework enforces this (alg-confusion family, CVE-2026-22817 class).
363
+ jwtExternal._assertAlgKtyMatch(header.alg, header.jwk);
364
+
351
365
  // Verify signature against the embedded jwk
352
366
  var key = _jwkToKeyObject(header.jwk);
353
367
  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
  }
@@ -546,24 +550,37 @@ function lookupAaguid(blob, aaguid) {
546
550
  return null;
547
551
  }
548
552
 
549
- // Pull the certified-level token out of a list of status reports. The
550
- // most recent FIDO_CERTIFIED_L{N}[_PLUS] report wins; if none exist,
551
- // the authenticator is uncertified (level 0).
553
+ // Resolve the CURRENT certified level from the status-report history. FIDO MDS3
554
+ // status reports are chronological; the level is whatever the MOST RECENT
555
+ // certification-status report says a FIDO_CERTIFIED_L{N}[_PLUS], or 0 when the
556
+ // latest such report is NOT_FIDO_CERTIFIED (a decertification). Taking the
557
+ // highest-ever level instead let a step-up / risk policy that requires L{N}
558
+ // accept an authenticator that was later decertified or downgraded (the
559
+ // certifiedLevel is the policy input, so a stale max is an authenticator-
560
+ // assurance bypass). Ordering is by effectiveDate (ISO YYYY-MM-DD, so lexical ==
561
+ // chronological); ties and missing dates fall back to array order (append
562
+ // order, which the spec defines as chronological).
552
563
  function _certifiedLevel(statusReports) {
553
564
  if (!Array.isArray(statusReports)) return { level: 0, plus: false };
554
- var best = { level: 0, plus: false };
565
+ var latest = null;
566
+ var latestDate = null;
555
567
  for (var i = 0; i < statusReports.length; i++) {
556
568
  var sr = statusReports[i];
557
- if (!sr || typeof sr.status !== "string") continue;
558
- var m = CERT_LEVEL_RE.exec(sr.status);
559
- if (!m) continue;
560
- var level = parseInt(m[1], 10);
561
- var plus = !!m[2];
562
- if (level > best.level || (level === best.level && plus && !best.plus)) {
563
- best = { level: level, plus: plus };
569
+ // Only certification-status reports move the level: a level grant, or its
570
+ // explicit revocation (NOT_FIDO_CERTIFIED). Other statuses (UPDATE_AVAILABLE,
571
+ // REVOKED, …) are handled elsewhere and must not be read as a level. The
572
+ // status length is bounded before the regex test below — FIDO status tokens
573
+ // are short enums; bounding input before any .test() is the convention.
574
+ if (!sr || typeof sr.status !== "string" || sr.status.length > 64) continue;
575
+ if (!CERT_LEVEL_RE.test(sr.status) && sr.status !== "NOT_FIDO_CERTIFIED") continue;
576
+ var d = typeof sr.effectiveDate === "string" ? sr.effectiveDate : "";
577
+ if (latest === null || d >= latestDate) {
578
+ latest = sr; latestDate = d;
564
579
  }
565
580
  }
566
- return best;
581
+ if (!latest || latest.status === "NOT_FIDO_CERTIFIED") return { level: 0, plus: false };
582
+ var m = CERT_LEVEL_RE.exec(latest.status);
583
+ return { level: parseInt(m[1], 10), plus: !!m[2] };
567
584
  }
568
585
 
569
586
  /**
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
  }
@@ -193,6 +193,16 @@ function decode(token) {
193
193
  catch (_e) { throw new AuthError("auth-jwt/malformed", "header is not valid base64url-JSON"); }
194
194
  try { payload = safeJson.parse(_b64urlDecode(parts[1])); }
195
195
  catch (_e) { throw new AuthError("auth-jwt/malformed", "payload is not valid base64url-JSON"); }
196
+ // The JOSE header and the claims set MUST each be a JSON object. safeJson.parse
197
+ // accepts the literal `null` (a valid JSON document) and scalars/arrays, so a
198
+ // header segment of base64url("null") would otherwise survive to a `.crit` /
199
+ // `.kid` dereference and throw a raw TypeError instead of a typed AuthError.
200
+ if (!safeJson.isJsonObject(header)) {
201
+ throw new AuthError("auth-jwt/malformed", "header is not a JSON object");
202
+ }
203
+ if (!safeJson.isJsonObject(payload)) {
204
+ throw new AuthError("auth-jwt/malformed", "payload is not a JSON object");
205
+ }
196
206
  var signature;
197
207
  try { signature = _b64urlDecode(parts[2]); }
198
208
  catch (_e) { throw new AuthError("auth-jwt/malformed", "signature is not valid base64url"); }
@@ -222,7 +232,7 @@ async function verify(token, opts) {
222
232
  // Validate the allowlist itself — typoed entries should surface here,
223
233
  // not as silent "every token rejected."
224
234
  for (var i = 0; i < allowed.length; i++) {
225
- if (!ALGORITHM_TO_NODE[allowed[i]]) {
235
+ if (!Object.prototype.hasOwnProperty.call(ALGORITHM_TO_NODE, allowed[i])) {
226
236
  throw new AuthError("auth-jwt/unsupported-algorithm",
227
237
  "opts.algorithms[" + i + "] = '" + allowed[i] + "' is not in the supported list (" +
228
238
  SUPPORTED_ALGORITHMS.join(", ") + ")");
@@ -351,10 +361,16 @@ async function verify(token, opts) {
351
361
  "token not yet valid: nbf=" + p.nbf + " (now=" + nowSec + ", tolerance=" + tol + "s)");
352
362
  }
353
363
 
354
- // String-claim assertions
355
- if (opts.issuer !== undefined && !_matchClaim(p.iss, opts.issuer, "iss")) {
364
+ // String-claim assertions. `iss` is StringOrURI (RFC 7519 §4.1.1) — a single
365
+ // value, not a list: reject a non-string iss before matching. Routing it
366
+ // through the aud-style any-of _matchClaim let an attacker-influenced
367
+ // multi-issuer array (iss:["evil","trusted"]) satisfy a single-issuer
368
+ // expectation (CVE-2025-30144 / fast-jwt iss-array class) — the same defense
369
+ // jwtExternal.verifyExternal and oauth.verifyIdToken already apply.
370
+ if (opts.issuer !== undefined &&
371
+ (typeof p.iss !== "string" || !_matchClaim(p.iss, opts.issuer, "iss"))) {
356
372
  throw new AuthError("auth-jwt/iss-mismatch",
357
- "iss='" + p.iss + "' does not match expected " + JSON.stringify(opts.issuer));
373
+ "iss=" + JSON.stringify(p.iss) + " does not match expected " + JSON.stringify(opts.issuer));
358
374
  }
359
375
  if (opts.audience !== undefined && !_matchClaim(p.aud, opts.audience, "aud")) {
360
376
  throw new AuthError("auth-jwt/aud-mismatch",
@@ -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();
package/lib/auth/oauth.js CHANGED
@@ -1276,9 +1276,15 @@ function create(opts) {
1276
1276
  // browser session could be replayed without detection. Throw
1277
1277
  // loudly so the operator sees the bug at config time, not at
1278
1278
  // first-replay-attempt time.
1279
- if (isOidc && eopts.nonce === undefined && eopts.skipNonceCheck !== true) {
1279
+ // Require a NON-EMPTY STRING nonce, not merely a defined one: a falsy
1280
+ // nonce (null / "" — e.g. a session field that was never set) would slip a
1281
+ // strict `=== undefined` guard, and the downstream verifier only checks the
1282
+ // nonce when vopts.nonce is truthy, so the ID-token nonce check would be
1283
+ // silently skipped and a token captured from another session replayed.
1284
+ if (isOidc && eopts.skipNonceCheck !== true &&
1285
+ (typeof eopts.nonce !== "string" || eopts.nonce.length === 0)) {
1280
1286
  throw new OAuthError("auth-oauth/no-nonce",
1281
- "exchangeCode: nonce is required on OIDC flows. Pass the " +
1287
+ "exchangeCode: a non-empty nonce is required on OIDC flows. Pass the " +
1282
1288
  "value returned from authorizationUrl() through to exchangeCode " +
1283
1289
  "({ code, state, verifier, nonce }). Operators with a deliberate " +
1284
1290
  "no-nonce flow must pass `skipNonceCheck: true` (audited reason).");
@@ -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
  }