@blamejs/core 0.15.12 → 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 (365) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +45 -29
  4. package/lib/acme.js +6 -5
  5. package/lib/agent-event-bus.js +18 -4
  6. package/lib/agent-idempotency.js +7 -6
  7. package/lib/agent-orchestrator.js +2 -5
  8. package/lib/agent-saga.js +3 -5
  9. package/lib/agent-snapshot.js +32 -2
  10. package/lib/agent-tenant.js +2 -5
  11. package/lib/ai-adverse-decision.js +2 -15
  12. package/lib/ai-aedt-bias-audit.js +2 -1
  13. package/lib/ai-capability.js +1 -6
  14. package/lib/ai-content-detect.js +1 -3
  15. package/lib/ai-dp.js +1 -5
  16. package/lib/ai-frontier-protocol.js +1 -1
  17. package/lib/ai-input.js +2 -2
  18. package/lib/ai-model-manifest.js +1 -1
  19. package/lib/ai-output.js +16 -7
  20. package/lib/ai-pref.js +3 -8
  21. package/lib/ai-quota.js +3 -14
  22. package/lib/api-key.js +37 -28
  23. package/lib/api-snapshot.js +4 -1
  24. package/lib/app-shutdown.js +7 -1
  25. package/lib/archive-adapters.js +2 -4
  26. package/lib/archive-entry-policy.js +32 -0
  27. package/lib/archive-gz.js +9 -0
  28. package/lib/archive-read.js +14 -24
  29. package/lib/archive-tar-read.js +56 -24
  30. package/lib/archive.js +6 -12
  31. package/lib/arg-parser.js +7 -6
  32. package/lib/asn1-der.js +70 -22
  33. package/lib/asyncapi-traits.js +2 -6
  34. package/lib/atomic-file.js +312 -33
  35. package/lib/audit-chain.js +183 -54
  36. package/lib/audit-daily-review.js +36 -16
  37. package/lib/audit-emit.js +82 -0
  38. package/lib/audit-sign.js +258 -0
  39. package/lib/audit-tools.js +108 -23
  40. package/lib/audit.js +91 -2
  41. package/lib/auth/access-lock.js +7 -29
  42. package/lib/auth/bot-challenge.js +1 -1
  43. package/lib/auth/ciba.js +43 -4
  44. package/lib/auth/dpop.js +29 -36
  45. package/lib/auth/fido-mds3.js +14 -16
  46. package/lib/auth/jwt-external.js +26 -8
  47. package/lib/auth/jwt.js +15 -17
  48. package/lib/auth/lockout.js +24 -27
  49. package/lib/auth/oauth.js +67 -45
  50. package/lib/auth/oid4vci.js +55 -32
  51. package/lib/auth/oid4vp.js +3 -2
  52. package/lib/auth/openid-federation.js +6 -6
  53. package/lib/auth/passkey.js +4 -4
  54. package/lib/auth/password.js +8 -2
  55. package/lib/auth/saml.js +70 -40
  56. package/lib/auth/sd-jwt-vc-holder.js +2 -14
  57. package/lib/auth/sd-jwt-vc-issuer.js +2 -14
  58. package/lib/auth/sd-jwt-vc.js +25 -5
  59. package/lib/auth/status-list.js +21 -9
  60. package/lib/auth/step-up.js +15 -13
  61. package/lib/auth-bot-challenge.js +27 -39
  62. package/lib/backup/bundle.js +18 -20
  63. package/lib/backup/crypto.js +23 -8
  64. package/lib/backup/index.js +55 -67
  65. package/lib/backup/manifest.js +7 -1
  66. package/lib/bounded-map.js +112 -1
  67. package/lib/breach-deadline.js +1 -11
  68. package/lib/break-glass.js +41 -22
  69. package/lib/cache.js +7 -18
  70. package/lib/cbor.js +36 -12
  71. package/lib/cdn-cache-control.js +15 -12
  72. package/lib/cert.js +8 -13
  73. package/lib/chain-writer.js +162 -47
  74. package/lib/cli.js +10 -2
  75. package/lib/client-hints.js +7 -9
  76. package/lib/cloud-events.js +43 -33
  77. package/lib/cluster-storage.js +9 -41
  78. package/lib/cms-codec.js +2 -1
  79. package/lib/codepoint-class.js +84 -1
  80. package/lib/compliance-ai-act-logging.js +1 -6
  81. package/lib/compliance-ai-act-transparency.js +2 -4
  82. package/lib/compliance-eaa.js +2 -12
  83. package/lib/compliance-sanctions-fetcher.js +21 -28
  84. package/lib/compliance-sanctions.js +11 -21
  85. package/lib/compliance.js +3 -10
  86. package/lib/config-drift.js +24 -20
  87. package/lib/content-credentials.js +11 -8
  88. package/lib/content-digest.js +21 -12
  89. package/lib/cookies.js +15 -13
  90. package/lib/cose.js +39 -11
  91. package/lib/cra-report.js +1 -11
  92. package/lib/crdt.js +2 -1
  93. package/lib/crypto-field.js +34 -33
  94. package/lib/crypto.js +138 -7
  95. package/lib/csp.js +239 -3
  96. package/lib/daemon.js +46 -42
  97. package/lib/data-act.js +46 -13
  98. package/lib/db-file-lifecycle.js +14 -6
  99. package/lib/db-query.js +75 -49
  100. package/lib/db.js +66 -27
  101. package/lib/dbsc.js +5 -6
  102. package/lib/ddl-change-control.js +18 -14
  103. package/lib/deprecate.js +4 -5
  104. package/lib/did.js +6 -2
  105. package/lib/dora.js +43 -13
  106. package/lib/dr-runbook.js +1 -1
  107. package/lib/dsa.js +2 -1
  108. package/lib/dsr.js +30 -20
  109. package/lib/early-hints.js +19 -0
  110. package/lib/eat.js +5 -1
  111. package/lib/external-db-migrate.js +21 -22
  112. package/lib/external-db.js +74 -30
  113. package/lib/fda-21cfr11.js +42 -13
  114. package/lib/fdx.js +22 -18
  115. package/lib/file-upload.js +80 -66
  116. package/lib/flag-evaluation-context.js +5 -8
  117. package/lib/flag-providers.js +6 -2
  118. package/lib/forms.js +1 -1
  119. package/lib/framework-error.js +12 -0
  120. package/lib/framework-schema.js +19 -0
  121. package/lib/fsm.js +7 -12
  122. package/lib/gate-contract.js +911 -39
  123. package/lib/gdpr-ropa.js +22 -22
  124. package/lib/graphql-federation.js +18 -5
  125. package/lib/guard-agent-registry.js +2 -1
  126. package/lib/guard-all.js +3 -2
  127. package/lib/guard-archive.js +9 -30
  128. package/lib/guard-auth.js +23 -80
  129. package/lib/guard-cidr.js +20 -96
  130. package/lib/guard-csv.js +135 -196
  131. package/lib/guard-domain.js +23 -106
  132. package/lib/guard-dsn.js +17 -14
  133. package/lib/guard-email.js +46 -53
  134. package/lib/guard-envelope.js +2 -2
  135. package/lib/guard-event-bus-topic.js +2 -1
  136. package/lib/guard-filename.js +12 -60
  137. package/lib/guard-graphql.js +28 -75
  138. package/lib/guard-html-wcag.js +15 -2
  139. package/lib/guard-html.js +74 -128
  140. package/lib/guard-idempotency-key.js +2 -1
  141. package/lib/guard-image.js +280 -77
  142. package/lib/guard-imap-command.js +9 -10
  143. package/lib/guard-jmap.js +1 -1
  144. package/lib/guard-json.js +101 -109
  145. package/lib/guard-jsonpath.js +20 -88
  146. package/lib/guard-jwt.js +32 -114
  147. package/lib/guard-list-id.js +2 -7
  148. package/lib/guard-list-unsubscribe.js +2 -7
  149. package/lib/guard-mail-compose.js +5 -6
  150. package/lib/guard-mail-move.js +3 -2
  151. package/lib/guard-mail-query.js +5 -3
  152. package/lib/guard-mail-sieve.js +6 -7
  153. package/lib/guard-managesieve-command.js +6 -5
  154. package/lib/guard-markdown.js +76 -110
  155. package/lib/guard-message-id.js +5 -6
  156. package/lib/guard-mime.js +20 -99
  157. package/lib/guard-oauth.js +19 -73
  158. package/lib/guard-pdf.js +65 -72
  159. package/lib/guard-pop3-command.js +13 -14
  160. package/lib/guard-posture-chain.js +2 -1
  161. package/lib/guard-regex.js +24 -99
  162. package/lib/guard-saga-config.js +2 -1
  163. package/lib/guard-shell.js +22 -87
  164. package/lib/guard-smtp-command.js +9 -12
  165. package/lib/guard-sql.js +15 -13
  166. package/lib/guard-stream-args.js +2 -1
  167. package/lib/guard-svg.js +103 -149
  168. package/lib/guard-template.js +23 -80
  169. package/lib/guard-tenant-id.js +2 -1
  170. package/lib/guard-text.js +592 -0
  171. package/lib/guard-time.js +27 -95
  172. package/lib/guard-uuid.js +21 -93
  173. package/lib/guard-xml.js +76 -106
  174. package/lib/guard-yaml.js +24 -60
  175. package/lib/html-balance.js +7 -3
  176. package/lib/http-client-cache.js +5 -12
  177. package/lib/http-client-cookie-jar.js +35 -16
  178. package/lib/http-client.js +233 -74
  179. package/lib/http-message-signature.js +3 -2
  180. package/lib/i18n-messageformat.js +5 -7
  181. package/lib/i18n.js +83 -26
  182. package/lib/iab-tcf.js +3 -2
  183. package/lib/importmap-integrity.js +41 -1
  184. package/lib/inbox.js +8 -8
  185. package/lib/incident-report.js +12 -27
  186. package/lib/ip-utils.js +49 -6
  187. package/lib/jobs.js +3 -2
  188. package/lib/json-patch.js +1 -1
  189. package/lib/json-path.js +24 -3
  190. package/lib/jtd.js +2 -2
  191. package/lib/keychain.js +6 -18
  192. package/lib/legal-hold.js +30 -23
  193. package/lib/log-stream-cloudwatch.js +44 -112
  194. package/lib/log-stream-otlp-grpc.js +17 -14
  195. package/lib/log-stream-otlp.js +16 -80
  196. package/lib/log-stream-webhook.js +20 -92
  197. package/lib/log.js +2 -2
  198. package/lib/mail-agent.js +2 -2
  199. package/lib/mail-arc-sign.js +8 -3
  200. package/lib/mail-arf.js +4 -3
  201. package/lib/mail-auth.js +43 -69
  202. package/lib/mail-bimi.js +35 -55
  203. package/lib/mail-bounce.js +3 -3
  204. package/lib/mail-crypto-pgp.js +3 -2
  205. package/lib/mail-crypto-smime.js +71 -6
  206. package/lib/mail-dav.js +8 -35
  207. package/lib/mail-deploy.js +15 -14
  208. package/lib/mail-dkim.js +19 -38
  209. package/lib/mail-greylist.js +17 -30
  210. package/lib/mail-helo.js +4 -7
  211. package/lib/mail-journal.js +13 -9
  212. package/lib/mail-mdn.js +12 -7
  213. package/lib/mail-rbl.js +7 -12
  214. package/lib/mail-scan.js +5 -7
  215. package/lib/mail-send-deliver.js +33 -20
  216. package/lib/mail-server-imap.js +32 -87
  217. package/lib/mail-server-jmap.js +39 -52
  218. package/lib/mail-server-managesieve.js +29 -83
  219. package/lib/mail-server-mx.js +33 -74
  220. package/lib/mail-server-net.js +177 -0
  221. package/lib/mail-server-pop3.js +30 -83
  222. package/lib/mail-server-rate-limit.js +30 -6
  223. package/lib/mail-server-registry.js +1 -1
  224. package/lib/mail-server-submission.js +34 -73
  225. package/lib/mail-server-tls.js +98 -2
  226. package/lib/mail-spam-score.js +9 -12
  227. package/lib/mail-store-fts.js +1 -1
  228. package/lib/mail-store.js +3 -2
  229. package/lib/mail.js +28 -13
  230. package/lib/markup-escape.js +31 -0
  231. package/lib/markup-tokenizer.js +78 -0
  232. package/lib/mcp-tool-registry.js +26 -23
  233. package/lib/mcp.js +7 -5
  234. package/lib/mdoc.js +36 -14
  235. package/lib/metrics.js +46 -33
  236. package/lib/middleware/age-gate.js +3 -23
  237. package/lib/middleware/api-encrypt.js +13 -24
  238. package/lib/middleware/assetlinks.js +2 -7
  239. package/lib/middleware/bearer-auth.js +2 -1
  240. package/lib/middleware/body-parser.js +41 -52
  241. package/lib/middleware/bot-disclose.js +7 -10
  242. package/lib/middleware/bot-guard.js +28 -28
  243. package/lib/middleware/clear-site-data.js +5 -1
  244. package/lib/middleware/compression.js +9 -0
  245. package/lib/middleware/cors.js +32 -23
  246. package/lib/middleware/csrf-protect.js +73 -23
  247. package/lib/middleware/daily-byte-quota.js +12 -30
  248. package/lib/middleware/deny-response.js +19 -1
  249. package/lib/middleware/fetch-metadata.js +35 -4
  250. package/lib/middleware/idempotency-key.js +4 -20
  251. package/lib/middleware/network-allowlist.js +61 -30
  252. package/lib/middleware/rate-limit.js +45 -44
  253. package/lib/middleware/scim-server.js +5 -3
  254. package/lib/middleware/security-headers.js +33 -7
  255. package/lib/middleware/security-txt.js +2 -6
  256. package/lib/middleware/speculation-rules.js +6 -3
  257. package/lib/middleware/tus-upload.js +14 -29
  258. package/lib/middleware/web-app-manifest.js +2 -7
  259. package/lib/migrations.js +4 -13
  260. package/lib/mime-parse.js +34 -17
  261. package/lib/module-loader.js +44 -0
  262. package/lib/money.js +105 -0
  263. package/lib/mtls-ca.js +125 -41
  264. package/lib/network-byte-quota.js +4 -18
  265. package/lib/network-dane.js +8 -6
  266. package/lib/network-dns-resolver.js +98 -7
  267. package/lib/network-dns.js +9 -2
  268. package/lib/network-dnssec.js +18 -17
  269. package/lib/network-heartbeat.js +3 -2
  270. package/lib/network-smtp-policy.js +52 -46
  271. package/lib/network-tls.js +67 -47
  272. package/lib/network-tsig.js +2 -2
  273. package/lib/nis2-report.js +2 -12
  274. package/lib/nist-crosswalk.js +1 -1
  275. package/lib/nonce-store.js +81 -3
  276. package/lib/ntp-check.js +28 -0
  277. package/lib/numeric-bounds.js +31 -8
  278. package/lib/object-store/azure-blob-bucket-ops.js +2 -6
  279. package/lib/object-store/azure-blob.js +6 -47
  280. package/lib/object-store/gcs-bucket-ops.js +4 -2
  281. package/lib/object-store/gcs.js +14 -75
  282. package/lib/object-store/http-put.js +2 -7
  283. package/lib/object-store/http-request.js +157 -0
  284. package/lib/object-store/local.js +37 -17
  285. package/lib/object-store/sigv4-bucket-ops.js +2 -1
  286. package/lib/object-store/sigv4.js +10 -79
  287. package/lib/observability-otlp-exporter.js +29 -29
  288. package/lib/observability.js +95 -0
  289. package/lib/openapi-paths-builder.js +7 -3
  290. package/lib/otel-export.js +7 -10
  291. package/lib/outbox.js +54 -41
  292. package/lib/pagination.js +11 -15
  293. package/lib/parsers/safe-env.js +5 -4
  294. package/lib/parsers/safe-ini.js +10 -4
  295. package/lib/parsers/safe-toml.js +11 -9
  296. package/lib/parsers/safe-xml.js +3 -3
  297. package/lib/parsers/safe-yaml.js +28 -9
  298. package/lib/pick.js +63 -5
  299. package/lib/pipl-cn.js +69 -59
  300. package/lib/privacy-pass.js +8 -6
  301. package/lib/problem-details.js +2 -5
  302. package/lib/pubsub.js +4 -8
  303. package/lib/queue-local.js +10 -3
  304. package/lib/redact.js +9 -4
  305. package/lib/render.js +4 -2
  306. package/lib/request-helpers.js +334 -29
  307. package/lib/resource-access-lock.js +3 -3
  308. package/lib/restore-bundle.js +46 -18
  309. package/lib/restore-rollback.js +10 -4
  310. package/lib/restore.js +24 -22
  311. package/lib/retention.js +23 -9
  312. package/lib/router.js +17 -4
  313. package/lib/safe-async.js +457 -0
  314. package/lib/safe-buffer.js +137 -6
  315. package/lib/safe-decompress.js +2 -1
  316. package/lib/safe-dns.js +2 -1
  317. package/lib/safe-ical.js +14 -28
  318. package/lib/safe-icap.js +1 -1
  319. package/lib/safe-json.js +51 -8
  320. package/lib/safe-jsonpath.js +6 -5
  321. package/lib/safe-mime.js +25 -29
  322. package/lib/safe-redirect.js +2 -5
  323. package/lib/safe-schema.js +7 -6
  324. package/lib/safe-sieve.js +1 -1
  325. package/lib/safe-sql.js +126 -0
  326. package/lib/safe-vcard.js +13 -27
  327. package/lib/sandbox-worker.js +15 -2
  328. package/lib/sandbox.js +4 -3
  329. package/lib/scheduler.js +22 -13
  330. package/lib/sec-cyber.js +82 -37
  331. package/lib/seeders.js +4 -11
  332. package/lib/self-update-standalone-verifier.js +16 -0
  333. package/lib/self-update.js +92 -69
  334. package/lib/session-device-binding.js +112 -66
  335. package/lib/session.js +219 -7
  336. package/lib/sql.js +228 -81
  337. package/lib/sse.js +6 -10
  338. package/lib/static.js +201 -111
  339. package/lib/storage.js +4 -2
  340. package/lib/structured-fields.js +275 -16
  341. package/lib/template.js +7 -5
  342. package/lib/tenant-quota.js +53 -38
  343. package/lib/tsa.js +16 -17
  344. package/lib/validate-opts.js +154 -8
  345. package/lib/vault/index.js +5 -0
  346. package/lib/vault/passphrase-ops.js +22 -26
  347. package/lib/vault/passphrase-source.js +8 -3
  348. package/lib/vault/rotate.js +13 -18
  349. package/lib/vault/seal-pem-file.js +34 -49
  350. package/lib/vault-aad.js +3 -2
  351. package/lib/vc.js +3 -8
  352. package/lib/vendor/MANIFEST.json +10 -10
  353. package/lib/vendor/public-suffix-list.dat +23 -10
  354. package/lib/vendor/public-suffix-list.data.js +5498 -5494
  355. package/lib/vex.js +35 -13
  356. package/lib/web-push-vapid.js +23 -20
  357. package/lib/webhook-dispatcher.js +706 -0
  358. package/lib/webhook.js +59 -19
  359. package/lib/websocket-channels.js +9 -7
  360. package/lib/websocket.js +8 -4
  361. package/lib/worm.js +3 -4
  362. package/lib/ws-client.js +65 -56
  363. package/lib/x509-chain.js +44 -0
  364. package/package.json +1 -1
  365. package/sbom.cdx.json +6 -6
@@ -41,6 +41,7 @@
41
41
  var defineClass = require("../framework-error").defineClass;
42
42
  var lazyRequire = require("../lazy-require");
43
43
  var validateOpts = require("../validate-opts");
44
+ var requestHelpers = require("../request-helpers");
44
45
 
45
46
  var audit = lazyRequire(function () { return require("../audit"); });
46
47
  var observability = lazyRequire(function () { return require("../observability"); });
@@ -53,7 +54,7 @@ var SAFE_METHODS = Object.freeze({ GET: 1, HEAD: 1, OPTIONS: 1 });
53
54
  function _normalizeMode(mode) {
54
55
  if (typeof mode !== "string") return null;
55
56
  var m = mode.toLowerCase();
56
- return VALID_MODES[m] ? m : null;
57
+ return Object.prototype.hasOwnProperty.call(VALID_MODES, m) ? m : null;
57
58
  }
58
59
 
59
60
  function create(opts) {
@@ -77,7 +78,6 @@ function create(opts) {
77
78
  }
78
79
  var passthroughPaths = Array.isArray(opts.passthroughPaths)
79
80
  ? opts.passthroughPaths.slice() : [];
80
- var auditOn = opts.audit !== false;
81
81
  var getRole = typeof opts.getRole === "function" ? opts.getRole : null;
82
82
  var errorMessage = typeof opts.errorMessage === "string" && opts.errorMessage.length > 0
83
83
  ? opts.errorMessage : "service in restricted access mode";
@@ -87,33 +87,11 @@ function create(opts) {
87
87
  var modeSetBy = "boot";
88
88
  var modeReason = "initial mode at boot";
89
89
 
90
- function _emitAudit(action, outcome, metadata) {
91
- if (!auditOn) return;
92
- try {
93
- audit().safeEmit({
94
- action: "auth.access_lock." + action,
95
- outcome: outcome,
96
- metadata: metadata || {},
97
- });
98
- } catch (_e) { /* drop-silent — audit is best-effort */ }
99
- }
100
- function _emitMetric(verb, n, labels) {
101
- try { observability().safeEvent("auth.access_lock." + verb, n || 1, labels || {}); }
102
- catch (_e) { /* drop-silent */ }
103
- }
90
+ var _emitAudit = audit().namespaced("auth.access_lock", opts.audit);
91
+ var _emitMetric = observability().namespaced("auth.access_lock");
104
92
 
105
- function _isPassthrough(req) {
106
- if (passthroughPaths.length === 0) return false;
107
- var p = req.url || "";
108
- var qpos = p.indexOf("?");
109
- if (qpos !== -1) p = p.slice(0, qpos);
110
- for (var i = 0; i < passthroughPaths.length; i++) {
111
- var entry = passthroughPaths[i];
112
- if (typeof entry === "string" && (p === entry || p.indexOf(entry + "/") === 0)) return true;
113
- if (entry instanceof RegExp && entry.test(p)) return true;
114
- }
115
- return false;
116
- }
93
+ var _isPassthrough = requestHelpers.makeSkipMatcher(
94
+ { skipPaths: passthroughPaths }, "auth.accessLock");
117
95
 
118
96
  function _hasUnlockRole(req) {
119
97
  if (!getRole || unlockRoles.length === 0) return false;
@@ -186,7 +164,7 @@ function create(opts) {
186
164
  if (_hasUnlockRole(req)) return next();
187
165
  if (currentMode === "read-only") {
188
166
  var method = (req.method || "GET").toUpperCase();
189
- if (SAFE_METHODS[method]) return next();
167
+ if (Object.prototype.hasOwnProperty.call(SAFE_METHODS, method)) return next();
190
168
  _emitAudit("refused", "denied", { mode: currentMode, method: method, path: req.url });
191
169
  _emitMetric("refused", 1, { mode: currentMode, reason: "non-safe-method" });
192
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
@@ -29,9 +29,11 @@
29
29
  var nodeCrypto = require("node:crypto");
30
30
  var bCrypto = require("../crypto");
31
31
  var jwk = require("../jwk");
32
+ var jwtExternal = require("./jwt-external");
32
33
  var safeJson = require("../safe-json");
33
34
  var safeUrl = require("../safe-url");
34
35
  var validateOpts = require("../validate-opts");
36
+ var nonceStore = require("../nonce-store");
35
37
  var C = require("../constants");
36
38
  var { AuthError } = require("../framework-error");
37
39
 
@@ -74,16 +76,12 @@ var REFUSED_ALGS = ["HS256", "HS384", "HS512", "none"];
74
76
 
75
77
  function _b64urlEncode(buf) { return bCrypto.toBase64Url(buf); }
76
78
 
77
- function _b64urlDecode(s) {
78
- if (typeof s !== "string") {
79
- throw new AuthError("auth-dpop/bad-base64", "expected base64url string");
80
- }
81
- try { return bCrypto.fromBase64Url(s); }
82
- catch (_e) {
83
- throw new AuthError("auth-dpop/bad-base64",
84
- "DPoP segment is not valid base64url");
85
- }
86
- }
79
+ var _b64urlDecode = bCrypto.makeBase64UrlDecoder({
80
+ errorClass: AuthError,
81
+ code: "auth-dpop/bad-base64",
82
+ typeMessage: "expected base64url string",
83
+ badMessage: "DPoP segment is not valid base64url",
84
+ });
87
85
 
88
86
  // Asymmetric key types DPoP accepts (its proof model relies on a
89
87
  // signature, so symmetric "oct" keys are refused). AKP is the IANA key
@@ -94,7 +92,7 @@ function thumbprint(key) {
94
92
  if (!key || typeof key !== "object" || typeof key.kty !== "string" || key.kty.length === 0) {
95
93
  throw new AuthError("auth-dpop/bad-jwk", "jwk must be an object with a kty");
96
94
  }
97
- if (!DPOP_KTY[key.kty]) {
95
+ if (!Object.prototype.hasOwnProperty.call(DPOP_KTY, key.kty)) {
98
96
  throw new AuthError("auth-dpop/refused-kty", "jwk.kty='" + key.kty + "' is not allowed (DPoP requires asymmetric kty)");
99
97
  }
100
98
  // The RFC 7638 thumbprint itself is computed by b.jwk.
@@ -128,16 +126,10 @@ function _normalizeHtu(htu) {
128
126
  // Pick alg-specific node:crypto verify params. PQC algs use
129
127
  // signWithoutAlgorithm shape (`null` algorithm).
130
128
  function _signParamsForAlg(alg) {
131
- if (alg === "RS256") return { hash: "sha256", padding: nodeCrypto.constants.RSA_PKCS1_PADDING };
132
- if (alg === "RS384") return { hash: "sha384", padding: nodeCrypto.constants.RSA_PKCS1_PADDING };
133
- if (alg === "RS512") return { hash: "sha512", padding: nodeCrypto.constants.RSA_PKCS1_PADDING };
134
- if (alg === "PS256") return { hash: "sha256", padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 32 }; // RFC 7518 PS256 salt length
135
- if (alg === "PS384") return { hash: "sha384", padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 48 }; // RFC 7518 PS384 salt length
136
- if (alg === "PS512") return { hash: "sha512", padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 64 }; // RFC 7518 PS512 salt length
137
- if (alg === "ES256") return { hash: "sha256", dsaEncoding: "ieee-p1363" };
138
- if (alg === "ES384") return { hash: "sha384", dsaEncoding: "ieee-p1363" };
139
- if (alg === "ES512") return { hash: "sha512", dsaEncoding: "ieee-p1363" };
140
- if (alg === "EdDSA") return { hash: null };
129
+ // Classical-JOSE params (RS/PS/ES/EdDSA) from the shared table; DPoP layers
130
+ // PQC ML-DSA-87 on top (the only verifier that accepts it).
131
+ var params = jwtExternal.algParams(alg);
132
+ if (params) return params;
141
133
  if (alg === "ML-DSA-87") return { hash: null, pqc: true };
142
134
  throw new AuthError("auth-dpop/unsupported-alg",
143
135
  "alg '" + alg + "' is not supported by DPoP");
@@ -195,11 +187,11 @@ function _detectAlgFromKey(key) {
195
187
  }
196
188
 
197
189
  function _jwkToKeyObject(jwk) {
198
- try { return nodeCrypto.createPublicKey({ key: jwk, format: "jwk" }); }
199
- catch (e) {
200
- throw new AuthError("auth-dpop/bad-jwk",
201
- "could not import jwk: " + ((e && e.message) || String(e)));
202
- }
190
+ return bCrypto.importPublicJwk(jwk, {
191
+ errorClass: AuthError,
192
+ code: "auth-dpop/bad-jwk",
193
+ messagePrefix: "could not import jwk: ",
194
+ });
203
195
  }
204
196
 
205
197
  // ---- buildProof ----
@@ -356,6 +348,11 @@ async function verify(proof, opts) {
356
348
  "DPoP proof declares 'crit' header — refused");
357
349
  }
358
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
+
359
356
  // Verify signature against the embedded jwk
360
357
  var key = _jwkToKeyObject(header.jwk);
361
358
  var params = _signParamsForAlg(header.alg);
@@ -460,16 +457,12 @@ async function verify(proof, opts) {
460
457
  "verify: replayStore", AuthError, "auth-dpop/bad-replay-store",
461
458
  "must expose checkAndInsert(jti, expireAtMs) — use b.nonceStore.create()");
462
459
  var expireAtMs = nowMs + iatWindowSec * C.TIME.seconds(1) * 2;
463
- var inserted;
464
- try { inserted = await opts.replayStore.checkAndInsert(payload.jti, expireAtMs); }
465
- catch (e) {
466
- throw new AuthError("auth-dpop/replay-store-failed",
467
- "replayStore.checkAndInsert threw: " + ((e && e.message) || String(e)));
468
- }
469
- if (inserted === false) {
470
- throw new AuthError("auth-dpop/replay",
471
- "DPoP proof jti='" + payload.jti + "' has been seen before — replay refused");
472
- }
460
+ await nonceStore.enforceReplay(opts.replayStore, payload.jti, expireAtMs, {
461
+ errorClass: AuthError,
462
+ storeFailedCode: "auth-dpop/replay-store-failed",
463
+ replayCode: "auth-dpop/replay",
464
+ tokenLabel: "DPoP proof",
465
+ });
473
466
  }
474
467
 
475
468
  return { header: header, payload: payload, jkt: jkt };
@@ -43,6 +43,8 @@ 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");
47
+ var jwtExternal = require("./jwt-external");
46
48
  var _wa = require("../vendor/simplewebauthn-server.cjs");
47
49
  var { FidoMds3Error } = require("../framework-error");
48
50
 
@@ -141,21 +143,14 @@ function _parseJws(token) {
141
143
  // in practice; PS* and EdDSA are listed for completeness so future
142
144
  // BLOBs over the same surface validate without a code edit.
143
145
  function _verifyParamsForAlg(alg) {
144
- switch (alg) {
145
- case "RS256": return { hash: "sha256", padding: nodeCrypto.constants.RSA_PKCS1_PADDING };
146
- case "RS384": return { hash: "sha384", padding: nodeCrypto.constants.RSA_PKCS1_PADDING };
147
- case "RS512": return { hash: "sha512", padding: nodeCrypto.constants.RSA_PKCS1_PADDING };
148
- case "PS256": return { hash: "sha256", padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 32 }; // SHA-256 hash length
149
- case "PS384": return { hash: "sha384", padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 48 }; // SHA-384 hash length
150
- case "PS512": return { hash: "sha512", padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 64 }; // SHA-512 hash length
151
- case "ES256": return { hash: "sha256", dsaEncoding: "ieee-p1363" };
152
- case "ES384": return { hash: "sha384", dsaEncoding: "ieee-p1363" };
153
- case "ES512": return { hash: "sha512", dsaEncoding: "ieee-p1363" };
154
- case "EdDSA": return { hash: null };
155
- default:
156
- throw new FidoMds3Error("fido-mds3/unsupported-alg",
157
- "JWS alg '" + alg + "' is not supported");
158
- }
146
+ // Classical-JOSE alg→crypto params (RS/PS/ES/EdDSA) shared with the rest of
147
+ // the JOSE verifiers jwtExternal.algParams owns the single table.
148
+ var params = jwtExternal.algParams(alg);
149
+ if (!params) {
150
+ throw new FidoMds3Error("fido-mds3/unsupported-alg",
151
+ "JWS alg '" + alg + "' is not supported");
152
+ }
153
+ return params;
159
154
  }
160
155
 
161
156
  // Resolve the trust roots. Operator override via caCertificate lets
@@ -245,7 +240,10 @@ function _validateChain(x5c, rootPems) {
245
240
  var root;
246
241
  try { root = new nodeCrypto.X509Certificate(rootPems[r]); }
247
242
  catch (_e) { continue; }
248
- 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)) {
249
247
  anchored = true;
250
248
  break;
251
249
  }
@@ -117,7 +117,16 @@ function _b64urlDecode(s) {
117
117
  // supplied alg the key can't actually produce.
118
118
  var _EC_CURVE_ALG = { prime256v1: "ES256", secp384r1: "ES384", secp521r1: "ES512" };
119
119
 
120
- function _verifyParamsForAlg(alg) {
120
+ // algParams — JOSE alg → node:crypto sign/verify parameters (hash + RSA
121
+ // PKCS1 padding / RSASSA-PSS saltLength / ECDSA dsaEncoding). The classical-
122
+ // JOSE table (RS/PS/ES per RFC 7518 §3, EdDSA per RFC 8037) lives HERE once,
123
+ // in the classical-JOSE domain owner: dpop proofs, fido-mds3 MDS3 BLOBs,
124
+ // oauth ID-token verify, and this module's own sign/verify all read the
125
+ // identical mapping. Returns null for an unrecognised alg so every caller
126
+ // throws its OWN typed error and keeps its OWN supported set — oauth omits
127
+ // EdDSA, dpop layers the PQC ML-DSA-87 on top. The values are sign/verify-
128
+ // symmetric (node:crypto uses the same params for both).
129
+ function algParams(alg) {
121
130
  if (alg === "RS256") return { hash: "sha256", padding: nodeCrypto.constants.RSA_PKCS1_PADDING };
122
131
  if (alg === "RS384") return { hash: "sha384", padding: nodeCrypto.constants.RSA_PKCS1_PADDING };
123
132
  if (alg === "RS512") return { hash: "sha512", padding: nodeCrypto.constants.RSA_PKCS1_PADDING };
@@ -128,8 +137,16 @@ function _verifyParamsForAlg(alg) {
128
137
  if (alg === "ES384") return { hash: "sha384", dsaEncoding: "ieee-p1363" };
129
138
  if (alg === "ES512") return { hash: "sha512", dsaEncoding: "ieee-p1363" };
130
139
  if (alg === "EdDSA") return { hash: null };
131
- throw new AuthError("auth-jwt-external/unsupported-alg",
132
- "alg '" + alg + "' is not supported by verifyExternal");
140
+ return null;
141
+ }
142
+
143
+ function _verifyParamsForAlg(alg) {
144
+ var params = algParams(alg);
145
+ if (!params) {
146
+ throw new AuthError("auth-jwt-external/unsupported-alg",
147
+ "alg '" + alg + "' is not supported by verifyExternal");
148
+ }
149
+ return params;
133
150
  }
134
151
 
135
152
  // _toPrivateKey — import the operator's classical signing key from any of
@@ -231,11 +248,11 @@ function _signCompactJws(header, payload, privateKey, alg) {
231
248
  }
232
249
 
233
250
  function _jwkToKey(jwk) {
234
- try { return nodeCrypto.createPublicKey({ key: jwk, format: "jwk" }); }
235
- catch (e) {
236
- throw new AuthError("auth-jwt-external/bad-jwk",
237
- "could not import JWK (kid=" + (jwk && jwk.kid) + "): " + ((e && e.message) || String(e)));
238
- }
251
+ return bCrypto.importPublicJwk(jwk, {
252
+ errorClass: AuthError,
253
+ code: "auth-jwt-external/bad-jwk",
254
+ messagePrefix: "could not import JWK (kid=" + (jwk && jwk.kid) + "): ",
255
+ });
239
256
  }
240
257
 
241
258
  // CVE-2026-22817 — RS256→HS256 alg-confusion + the broader alg/kty
@@ -725,6 +742,7 @@ module.exports = {
725
742
  REFUSED_ALGS: REFUSED_ALGS,
726
743
  // Shared JOSE defenses — routed from oauth.verifyIdToken /
727
744
  // oid4vci proof verify / sd-jwt-vc.verify / openid-federation.
745
+ algParams: algParams,
728
746
  _assertAlgKtyMatch: _assertAlgKtyMatch,
729
747
  _issuerMatches: _issuerMatches,
730
748
  // Classical-JWS signer internals — composed by oauth.js's attestation
package/lib/auth/jwt.js CHANGED
@@ -78,6 +78,7 @@ var C = require("../constants");
78
78
  var bCrypto = require("../crypto");
79
79
  var safeJson = require("../safe-json");
80
80
  var validateOpts = require("../validate-opts");
81
+ var nonceStore = require("../nonce-store");
81
82
  var { AuthError } = require("../framework-error");
82
83
 
83
84
  // Algorithm registry. The string keys are the JWT header `alg` values
@@ -92,11 +93,12 @@ var SUPPORTED_ALGORITHMS = Object.freeze(Object.keys(ALGORITHM_TO_NODE));
92
93
 
93
94
  function _b64urlEncode(buf) { return bCrypto.toBase64Url(buf); }
94
95
 
95
- function _b64urlDecode(s) {
96
- if (typeof s !== "string") throw new AuthError("auth-jwt/malformed", "expected base64url string");
97
- try { return bCrypto.fromBase64Url(s); }
98
- catch (_e) { throw new AuthError("auth-jwt/malformed", "JWT segment is not valid base64url"); }
99
- }
96
+ var _b64urlDecode = bCrypto.makeBase64UrlDecoder({
97
+ errorClass: AuthError,
98
+ code: "auth-jwt/malformed",
99
+ typeMessage: "expected base64url string",
100
+ badMessage: "JWT segment is not valid base64url",
101
+ });
100
102
 
101
103
  function _toKeyObject(pemOrKey, kind) {
102
104
  // kind is 'private' or 'public' — passes through if already a KeyObject;
@@ -115,7 +117,7 @@ function _toKeyObject(pemOrKey, kind) {
115
117
  }
116
118
 
117
119
  function _resolveAlgorithm(alg) {
118
- if (typeof alg !== "string" || !ALGORITHM_TO_NODE[alg]) {
120
+ if (typeof alg !== "string" || !Object.prototype.hasOwnProperty.call(ALGORITHM_TO_NODE, alg)) {
119
121
  throw new AuthError("auth-jwt/unsupported-algorithm",
120
122
  "algorithm must be one of " + SUPPORTED_ALGORITHMS.join(", ") + " (got: " + alg + ")");
121
123
  }
@@ -220,7 +222,7 @@ async function verify(token, opts) {
220
222
  // Validate the allowlist itself — typoed entries should surface here,
221
223
  // not as silent "every token rejected."
222
224
  for (var i = 0; i < allowed.length; i++) {
223
- if (!ALGORITHM_TO_NODE[allowed[i]]) {
225
+ if (!Object.prototype.hasOwnProperty.call(ALGORITHM_TO_NODE, allowed[i])) {
224
226
  throw new AuthError("auth-jwt/unsupported-algorithm",
225
227
  "opts.algorithms[" + i + "] = '" + allowed[i] + "' is not in the supported list (" +
226
228
  SUPPORTED_ALGORITHMS.join(", ") + ")");
@@ -409,16 +411,12 @@ async function verify(token, opts) {
409
411
  if (typeof p.exp === "number") {
410
412
  expireAtMs = p.exp * C.TIME.seconds(1);
411
413
  }
412
- var inserted;
413
- try { inserted = await opts.replayStore.checkAndInsert(p.jti, expireAtMs); }
414
- catch (e) {
415
- throw new AuthError("auth-jwt/replay-store-failed",
416
- "replayStore.checkAndInsert threw: " + ((e && e.message) || String(e)));
417
- }
418
- if (inserted === false) {
419
- throw new AuthError("auth-jwt/replay",
420
- "token jti='" + p.jti + "' has been seen before — replay refused");
421
- }
414
+ await nonceStore.enforceReplay(opts.replayStore, p.jti, expireAtMs, {
415
+ errorClass: AuthError,
416
+ storeFailedCode: "auth-jwt/replay-store-failed",
417
+ replayCode: "auth-jwt/replay",
418
+ tokenLabel: "token",
419
+ });
422
420
  }
423
421
 
424
422
  return p;
@@ -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
  }
@@ -200,32 +201,13 @@ function create(opts) {
200
201
 
201
202
  function _scopedKey(key) { return namespace + ":" + key; }
202
203
 
203
- function _emitObs(name, labels) {
204
- var sink = obsInst || _safeGlobalObs();
205
- if (!sink) return;
206
- try { sink.event(name, 1, labels); } catch (_e) { /* observability is best-effort */ }
207
- }
208
-
209
- // Fall back to the framework's global observability registry when the
210
- // operator didn't pass their own. event() is a no-op when no registry
211
- // is wired, so this is zero-cost in apps without observability.
212
- function _safeGlobalObs() {
213
- try { return observability(); } catch (_e) { return null; }
214
- }
204
+ // Emit to the operator's configured observability instance, else the
205
+ // framework's global registry (a no-op when none is wired), drop-silent.
206
+ var _emitObs = observability().makeCounterEmitter(obsInst);
215
207
 
216
- function _emitAudit(action, key, outcome, metadata, req) {
217
- if (!auditInst) return;
218
- try {
219
- var event = {
220
- action: action,
221
- outcome: outcome,
222
- resource: { kind: "auth.lockout", id: namespace + ":" + key },
223
- metadata: metadata || {},
224
- };
225
- if (req) event.actor = requestHelpers.extractActorContext(req);
226
- auditInst.safeEmit(event);
227
- } catch (_e) { /* audit best-effort */ }
228
- }
208
+ var _emitAudit = requestHelpers.makeResourceAuditEmitter(auditInst, "auth.lockout", function (key) {
209
+ return namespace + ":" + key;
210
+ });
229
211
 
230
212
  // Cache failures fail-OPEN by design (per the framework's
231
213
  // documented brute-force-lockout posture — rather than crash the
@@ -280,7 +262,22 @@ function create(opts) {
280
262
 
281
263
  // ---- Public surface ----
282
264
 
283
- 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) {
284
281
  _requireKey(key);
285
282
  callOpts = callOpts || {};
286
283
  var now = clock();