@blamejs/core 0.15.11 → 0.15.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +7 -23
  4. package/lib/acme.js +13 -16
  5. package/lib/agent-event-bus.js +5 -4
  6. package/lib/agent-idempotency.js +2 -5
  7. package/lib/agent-orchestrator.js +2 -5
  8. package/lib/agent-saga.js +3 -5
  9. package/lib/agent-tenant.js +2 -5
  10. package/lib/ai-adverse-decision.js +2 -15
  11. package/lib/ai-capability.js +1 -6
  12. package/lib/ai-dp.js +1 -5
  13. package/lib/ai-input.js +2 -2
  14. package/lib/ai-pref.js +3 -8
  15. package/lib/ai-quota.js +3 -14
  16. package/lib/api-key.js +37 -28
  17. package/lib/archive-adapters.js +2 -4
  18. package/lib/archive-entry-policy.js +32 -0
  19. package/lib/archive-read.js +5 -17
  20. package/lib/archive-tar-read.js +5 -16
  21. package/lib/archive.js +2 -10
  22. package/lib/arg-parser.js +7 -6
  23. package/lib/asyncapi-traits.js +2 -6
  24. package/lib/atomic-file.js +108 -31
  25. package/lib/audit-chain.js +133 -53
  26. package/lib/audit-daily-review.js +24 -14
  27. package/lib/audit-emit.js +82 -0
  28. package/lib/audit-sign.js +257 -0
  29. package/lib/audit.js +84 -0
  30. package/lib/auth/access-lock.js +5 -27
  31. package/lib/auth/dpop.js +23 -35
  32. package/lib/auth/fido-mds3.js +9 -15
  33. package/lib/auth/jwt-external.js +26 -8
  34. package/lib/auth/jwt.js +13 -15
  35. package/lib/auth/lockout.js +6 -25
  36. package/lib/auth/oauth.js +67 -45
  37. package/lib/auth/oid4vci.js +55 -32
  38. package/lib/auth/oid4vp.js +3 -2
  39. package/lib/auth/openid-federation.js +6 -6
  40. package/lib/auth/passkey.js +3 -3
  41. package/lib/auth/password.js +7 -1
  42. package/lib/auth/saml.js +37 -27
  43. package/lib/auth/sd-jwt-vc-holder.js +2 -14
  44. package/lib/auth/sd-jwt-vc-issuer.js +2 -14
  45. package/lib/auth/sd-jwt-vc.js +1 -1
  46. package/lib/auth/status-list.js +7 -7
  47. package/lib/auth/step-up.js +6 -12
  48. package/lib/auth-bot-challenge.js +6 -37
  49. package/lib/backup/bundle.js +11 -18
  50. package/lib/backup/index.js +14 -47
  51. package/lib/bounded-map.js +112 -1
  52. package/lib/breach-deadline.js +1 -11
  53. package/lib/cache.js +7 -18
  54. package/lib/cbor.js +2 -1
  55. package/lib/cdn-cache-control.js +8 -9
  56. package/lib/cert.js +3 -10
  57. package/lib/chain-writer.js +162 -47
  58. package/lib/cli.js +5 -1
  59. package/lib/client-hints.js +10 -10
  60. package/lib/cloud-events.js +40 -31
  61. package/lib/cluster-storage.js +2 -38
  62. package/lib/cluster.js +4 -2
  63. package/lib/cms-codec.js +2 -1
  64. package/lib/codepoint-class.js +67 -1
  65. package/lib/compliance-ai-act-logging.js +1 -6
  66. package/lib/compliance-ai-act-transparency.js +2 -4
  67. package/lib/compliance-eaa.js +1 -11
  68. package/lib/compliance-sanctions-fetcher.js +21 -28
  69. package/lib/compliance-sanctions.js +2 -14
  70. package/lib/compliance.js +2 -9
  71. package/lib/config-drift.js +2 -12
  72. package/lib/content-digest.js +10 -8
  73. package/lib/cookies.js +5 -11
  74. package/lib/cose.js +19 -11
  75. package/lib/cra-report.js +1 -11
  76. package/lib/crypto-field.js +5 -10
  77. package/lib/crypto.js +120 -3
  78. package/lib/csp.js +235 -3
  79. package/lib/daemon.js +42 -41
  80. package/lib/data-act.js +19 -9
  81. package/lib/db-query.js +6 -40
  82. package/lib/db.js +34 -12
  83. package/lib/dbsc.js +5 -6
  84. package/lib/ddl-change-control.js +18 -14
  85. package/lib/deprecate.js +4 -5
  86. package/lib/did.js +6 -2
  87. package/lib/dsr.js +8 -12
  88. package/lib/external-db-migrate.js +21 -22
  89. package/lib/external-db.js +14 -26
  90. package/lib/fda-21cfr11.js +12 -8
  91. package/lib/fdx.js +22 -18
  92. package/lib/file-upload.js +80 -66
  93. package/lib/flag-evaluation-context.js +5 -8
  94. package/lib/framework-error.js +12 -0
  95. package/lib/framework-schema.js +19 -0
  96. package/lib/fsm.js +7 -12
  97. package/lib/gate-contract.js +869 -38
  98. package/lib/gdpr-ropa.js +4 -13
  99. package/lib/graphql-federation.js +1 -1
  100. package/lib/guard-agent-registry.js +2 -1
  101. package/lib/guard-all.js +1 -0
  102. package/lib/guard-archive.js +9 -30
  103. package/lib/guard-auth.js +23 -80
  104. package/lib/guard-cidr.js +20 -96
  105. package/lib/guard-csv.js +135 -196
  106. package/lib/guard-domain.js +23 -106
  107. package/lib/guard-dsn.js +16 -13
  108. package/lib/guard-email.js +46 -53
  109. package/lib/guard-envelope.js +1 -1
  110. package/lib/guard-event-bus-topic.js +2 -1
  111. package/lib/guard-filename.js +18 -62
  112. package/lib/guard-graphql.js +28 -75
  113. package/lib/guard-html-wcag.js +15 -2
  114. package/lib/guard-html.js +65 -117
  115. package/lib/guard-idempotency-key.js +2 -1
  116. package/lib/guard-image.js +280 -77
  117. package/lib/guard-imap-command.js +8 -9
  118. package/lib/guard-json.js +87 -103
  119. package/lib/guard-jsonpath.js +20 -88
  120. package/lib/guard-jwt.js +32 -114
  121. package/lib/guard-list-id.js +2 -7
  122. package/lib/guard-list-unsubscribe.js +2 -7
  123. package/lib/guard-mail-compose.js +5 -6
  124. package/lib/guard-mail-move.js +2 -1
  125. package/lib/guard-mail-query.js +5 -3
  126. package/lib/guard-mail-sieve.js +6 -7
  127. package/lib/guard-managesieve-command.js +5 -4
  128. package/lib/guard-markdown.js +76 -110
  129. package/lib/guard-message-id.js +5 -6
  130. package/lib/guard-mime.js +20 -99
  131. package/lib/guard-oauth.js +19 -73
  132. package/lib/guard-pdf.js +65 -72
  133. package/lib/guard-pop3-command.js +12 -13
  134. package/lib/guard-posture-chain.js +2 -1
  135. package/lib/guard-regex.js +24 -99
  136. package/lib/guard-saga-config.js +2 -1
  137. package/lib/guard-shell.js +22 -87
  138. package/lib/guard-smtp-command.js +8 -11
  139. package/lib/guard-sql.js +15 -13
  140. package/lib/guard-stream-args.js +2 -1
  141. package/lib/guard-svg.js +95 -140
  142. package/lib/guard-template.js +23 -80
  143. package/lib/guard-tenant-id.js +2 -1
  144. package/lib/guard-text.js +592 -0
  145. package/lib/guard-time.js +27 -95
  146. package/lib/guard-uuid.js +21 -93
  147. package/lib/guard-xml.js +76 -106
  148. package/lib/guard-yaml.js +24 -60
  149. package/lib/http-client-cache.js +8 -13
  150. package/lib/http-client-cookie-jar.js +2 -4
  151. package/lib/http-client.js +8 -21
  152. package/lib/http-message-signature.js +26 -8
  153. package/lib/i18n-messageformat.js +5 -7
  154. package/lib/i18n.js +83 -26
  155. package/lib/inbox.js +8 -8
  156. package/lib/incident-report.js +3 -21
  157. package/lib/ip-utils.js +49 -6
  158. package/lib/jobs.js +3 -2
  159. package/lib/keychain.js +6 -18
  160. package/lib/legal-hold.js +6 -15
  161. package/lib/log-stream-cloudwatch.js +44 -112
  162. package/lib/log-stream-otlp-grpc.js +28 -14
  163. package/lib/log-stream-otlp.js +16 -80
  164. package/lib/log-stream-syslog.js +6 -0
  165. package/lib/log-stream-webhook.js +20 -92
  166. package/lib/log.js +24 -2
  167. package/lib/mail-arc-sign.js +8 -3
  168. package/lib/mail-arf.js +3 -2
  169. package/lib/mail-auth.js +40 -66
  170. package/lib/mail-bimi.js +19 -39
  171. package/lib/mail-crypto-pgp.js +3 -2
  172. package/lib/mail-dav.js +8 -35
  173. package/lib/mail-deploy.js +6 -9
  174. package/lib/mail-dkim.js +19 -38
  175. package/lib/mail-greylist.js +15 -26
  176. package/lib/mail-helo.js +2 -3
  177. package/lib/mail-journal.js +2 -1
  178. package/lib/mail-mdn.js +4 -3
  179. package/lib/mail-rbl.js +5 -8
  180. package/lib/mail-scan.js +2 -2
  181. package/lib/mail-send-deliver.js +33 -20
  182. package/lib/mail-server-imap.js +32 -87
  183. package/lib/mail-server-jmap.js +35 -51
  184. package/lib/mail-server-managesieve.js +29 -83
  185. package/lib/mail-server-mx.js +33 -74
  186. package/lib/mail-server-net.js +177 -0
  187. package/lib/mail-server-pop3.js +30 -83
  188. package/lib/mail-server-rate-limit.js +30 -6
  189. package/lib/mail-server-submission.js +34 -73
  190. package/lib/mail-server-tls.js +89 -0
  191. package/lib/mail-spam-score.js +7 -8
  192. package/lib/mail-store.js +3 -2
  193. package/lib/mail.js +11 -11
  194. package/lib/markup-escape.js +31 -0
  195. package/lib/markup-tokenizer.js +54 -0
  196. package/lib/mcp-tool-registry.js +26 -23
  197. package/lib/mcp.js +1 -1
  198. package/lib/mdoc.js +11 -12
  199. package/lib/metrics.js +32 -30
  200. package/lib/middleware/age-gate.js +3 -23
  201. package/lib/middleware/api-encrypt.js +11 -22
  202. package/lib/middleware/assetlinks.js +2 -7
  203. package/lib/middleware/bearer-auth.js +2 -1
  204. package/lib/middleware/body-parser.js +79 -54
  205. package/lib/middleware/bot-disclose.js +7 -10
  206. package/lib/middleware/bot-guard.js +2 -10
  207. package/lib/middleware/csrf-protect.js +13 -2
  208. package/lib/middleware/daily-byte-quota.js +6 -26
  209. package/lib/middleware/deny-response.js +19 -1
  210. package/lib/middleware/fetch-metadata.js +7 -0
  211. package/lib/middleware/idempotency-key.js +4 -20
  212. package/lib/middleware/rate-limit.js +20 -28
  213. package/lib/middleware/scim-server.js +3 -2
  214. package/lib/middleware/security-headers.js +9 -1
  215. package/lib/middleware/security-txt.js +2 -6
  216. package/lib/middleware/tus-upload.js +12 -27
  217. package/lib/middleware/web-app-manifest.js +2 -7
  218. package/lib/migrations.js +4 -13
  219. package/lib/mime-parse.js +34 -17
  220. package/lib/module-loader.js +44 -0
  221. package/lib/money.js +105 -0
  222. package/lib/mtls-ca.js +116 -36
  223. package/lib/network-byte-quota.js +4 -18
  224. package/lib/network-dane.js +8 -6
  225. package/lib/network-dns-resolver.js +97 -6
  226. package/lib/network-dns.js +22 -26
  227. package/lib/network-dnssec.js +16 -16
  228. package/lib/network-heartbeat.js +6 -5
  229. package/lib/network-proxy.js +3 -7
  230. package/lib/network-smtp-policy.js +29 -41
  231. package/lib/network-tls.js +74 -55
  232. package/lib/network.js +2 -6
  233. package/lib/nis2-report.js +1 -11
  234. package/lib/nonce-store.js +81 -3
  235. package/lib/notify.js +7 -12
  236. package/lib/numeric-bounds.js +22 -8
  237. package/lib/object-store/azure-blob-bucket-ops.js +2 -6
  238. package/lib/object-store/azure-blob.js +5 -45
  239. package/lib/object-store/gcs.js +8 -71
  240. package/lib/object-store/http-put.js +1 -5
  241. package/lib/object-store/http-request.js +128 -0
  242. package/lib/object-store/sigv4-bucket-ops.js +2 -1
  243. package/lib/object-store/sigv4.js +9 -77
  244. package/lib/observability-otlp-exporter.js +9 -25
  245. package/lib/observability.js +95 -0
  246. package/lib/openapi-paths-builder.js +7 -3
  247. package/lib/otel-export.js +7 -10
  248. package/lib/outbox.js +43 -37
  249. package/lib/pagination.js +11 -15
  250. package/lib/parsers/safe-env.js +5 -4
  251. package/lib/parsers/safe-ini.js +10 -4
  252. package/lib/parsers/safe-toml.js +11 -9
  253. package/lib/parsers/safe-xml.js +2 -2
  254. package/lib/parsers/safe-yaml.js +7 -6
  255. package/lib/pick.js +63 -5
  256. package/lib/pipl-cn.js +69 -59
  257. package/lib/privacy-pass.js +8 -6
  258. package/lib/problem-details.js +2 -5
  259. package/lib/pubsub.js +4 -8
  260. package/lib/redact.js +2 -1
  261. package/lib/render.js +4 -2
  262. package/lib/request-helpers.js +133 -6
  263. package/lib/restore.js +5 -22
  264. package/lib/retention.js +3 -5
  265. package/lib/safe-async.js +457 -0
  266. package/lib/safe-buffer.js +137 -6
  267. package/lib/safe-decompress.js +2 -1
  268. package/lib/safe-dns.js +2 -1
  269. package/lib/safe-ical.js +12 -26
  270. package/lib/safe-json.js +7 -8
  271. package/lib/safe-jsonpath.js +6 -5
  272. package/lib/safe-mime.js +25 -29
  273. package/lib/safe-redirect.js +2 -5
  274. package/lib/safe-schema.js +7 -6
  275. package/lib/safe-sql.js +126 -0
  276. package/lib/safe-vcard.js +12 -26
  277. package/lib/sandbox-worker.js +9 -2
  278. package/lib/sandbox.js +3 -2
  279. package/lib/scheduler.js +5 -12
  280. package/lib/sec-cyber.js +82 -37
  281. package/lib/seeders.js +9 -21
  282. package/lib/self-update.js +92 -69
  283. package/lib/session-device-binding.js +112 -66
  284. package/lib/session.js +194 -6
  285. package/lib/sql.js +225 -78
  286. package/lib/sse.js +6 -10
  287. package/lib/static.js +136 -98
  288. package/lib/storage.js +4 -2
  289. package/lib/structured-fields.js +313 -17
  290. package/lib/tenant-quota.js +2 -20
  291. package/lib/tsa.js +11 -15
  292. package/lib/validate-opts.js +154 -8
  293. package/lib/vault/seal-pem-file.js +30 -48
  294. package/lib/vault-aad.js +3 -2
  295. package/lib/vc.js +2 -7
  296. package/lib/vex.js +35 -13
  297. package/lib/web-push-vapid.js +23 -20
  298. package/lib/webhook-dispatcher.js +706 -0
  299. package/lib/webhook.js +43 -18
  300. package/lib/websocket-channels.js +9 -7
  301. package/lib/websocket.js +7 -3
  302. package/lib/worm.js +2 -3
  303. package/lib/ws-client.js +8 -10
  304. package/package.json +1 -1
  305. package/sbom.cdx.json +6 -6
@@ -115,6 +115,8 @@
115
115
  */
116
116
 
117
117
  var nodeFs = require("node:fs");
118
+ var pick = require("../pick");
119
+ var numericBounds = require("../numeric-bounds");
118
120
  var os = require("node:os");
119
121
  var nodePath = require("node:path");
120
122
  var nodeCrypto = require("node:crypto");
@@ -164,7 +166,6 @@ var BodyParserError = defineClass("BodyParserError", { withStatusCode: true });
164
166
  // Mirrors safe-json.js + safe-schema.js. Field names that match these
165
167
  // are refused at the parse boundary regardless of which sub-parser is
166
168
  // in play — consistent prototype-pollution defense across the framework.
167
- var POISONED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
168
169
 
169
170
  // Materialize a header/parameter map from request-derived [key, value]
170
171
  // pairs WITHOUT a computed member write (`target[key] = value`). A
@@ -183,7 +184,7 @@ var POISONED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
183
184
  function _mapFromPairs(pairs) {
184
185
  var safe = [];
185
186
  for (var i = 0; i < pairs.length; i++) {
186
- if (POISONED_KEYS.has(pairs[i][0])) continue;
187
+ if (pick.isPoisonedKey(pairs[i][0])) continue;
187
188
  safe.push(pairs[i]);
188
189
  }
189
190
  return Object.assign(Object.create(null), Object.fromEntries(safe));
@@ -256,17 +257,13 @@ function _contentType(req) {
256
257
  // would slice through quoted commas/semicolons and corrupt the
257
258
  // multipart boundary. Use the shared quote-aware splitter that
258
259
  // tracks RFC 8941 §3.3.3 quoted-string state with backslash-escape.
259
- var parts = structuredFields.splitTopLevel(rest, ";");
260
- for (var i = 0; i < parts.length; i++) {
261
- var p = parts[i].trim();
262
- var eq = p.indexOf("=");
263
- if (eq === -1) continue;
264
- var k = p.slice(0, eq).trim().toLowerCase();
265
- var v = p.slice(eq + 1).trim();
260
+ var kvps = structuredFields.parseKeyValuePieces(
261
+ structuredFields.splitTopLevel(rest, ";"));
262
+ structuredFields.forEachKeyValue(kvps, function (key, v) {
266
263
  var _unq = structuredFields.unquoteSfString(v);
267
264
  if (_unq !== null) v = _unq;
268
- paramPairs.push([k, v]);
269
- }
265
+ paramPairs.push([key, v]);
266
+ });
270
267
  }
271
268
  return { type: type, params: _mapFromPairs(paramPairs) };
272
269
  }
@@ -375,6 +372,19 @@ function _detectSmuggling(req) {
375
372
  return null;
376
373
  }
377
374
 
375
+ // Generic, operator-safe status phrase for a client error body — used when the
376
+ // thrown error is NOT a framework-classified BodyParserError (or is a 5xx), so
377
+ // an internal exception's message (fs errno + tmp path, a parse hook's thrown
378
+ // detail) is never echoed to the client (CWE-209 / CodeQL js/stack-trace-exposure).
379
+ var _GENERIC_REASON = {};
380
+ _GENERIC_REASON[HTTP_STATUS.BAD_REQUEST] = "Bad Request";
381
+ _GENERIC_REASON[HTTP_STATUS.PAYLOAD_TOO_LARGE] = "Payload Too Large";
382
+ _GENERIC_REASON[HTTP_STATUS.UNSUPPORTED_MEDIA_TYPE] = "Unsupported Media Type";
383
+ _GENERIC_REASON[HTTP_STATUS.INTERNAL_SERVER_ERROR] = "Internal Server Error";
384
+ function _genericReason(status) {
385
+ return _GENERIC_REASON[status] || (status >= 500 ? "Internal Server Error" : "Bad Request");
386
+ }
387
+
378
388
  function _writeError(res, status, message, code) {
379
389
  if (res.headersSent) return;
380
390
  var body = JSON.stringify({ error: message, code: code });
@@ -417,26 +427,17 @@ function _bufferBody(req, limit) {
417
427
  return;
418
428
  }
419
429
  }
420
- var collector = safeBuffer.boundedChunkCollector({
430
+ safeBuffer.collectStream(req, {
421
431
  maxBytes: limit,
422
432
  errorClass: BodyParserError,
423
433
  sizeCode: "body-parser/too-large",
424
434
  sizeMessage: "request body exceeds limit",
435
+ }).then(resolve, function (e) {
436
+ // The drain-overflow guard (a second line of defense behind the
437
+ // Content-Length pre-check above) carries the 413 status code.
438
+ if (e && e.isBodyParserError) e.statusCode = HTTP_STATUS.PAYLOAD_TOO_LARGE;
439
+ reject(e);
425
440
  });
426
- var done = false;
427
- req.on("data", function (chunk) {
428
- if (done) return;
429
- try { collector.push(chunk); }
430
- catch (e) {
431
- done = true;
432
- try { req.destroy(); } catch (_e) { /* socket already closed */ }
433
- if (e && e.isBodyParserError) e.statusCode = HTTP_STATUS.PAYLOAD_TOO_LARGE;
434
- reject(e);
435
- return;
436
- }
437
- });
438
- req.on("end", function () { if (!done) { done = true; resolve(collector.result()); } });
439
- req.on("error", function (e) { if (!done) { done = true; reject(e); } });
440
441
  });
441
442
  }
442
443
 
@@ -469,10 +470,13 @@ async function _parseJson(req, opts) {
469
470
  }
470
471
  if (typeof opts.parseHook === "function") {
471
472
  try { parsed = opts.parseHook(parsed); }
472
- catch (e) {
473
+ catch (_e) {
473
474
  throw new BodyParserError(
474
475
  "body-parser/json-hook",
475
- "JSON parseHook failed: " + ((e && e.message) || String(e)),
476
+ // Operator parseHook threw surface a fixed, safe message; the hook's
477
+ // own thrown detail (which may carry secrets) is the operator's to log,
478
+ // never echoed to the client (CWE-209).
479
+ "request body rejected by parse hook",
476
480
  true, HTTP_STATUS.BAD_REQUEST
477
481
  );
478
482
  }
@@ -504,7 +508,7 @@ async function _parseUrlencoded(req, opts) {
504
508
  for (var i = 0; i < keys.length; i++) {
505
509
  var k = keys[i][0];
506
510
  var v = keys[i][1];
507
- if (POISONED_KEYS.has(k)) {
511
+ if (pick.isPoisonedKey(k)) {
508
512
  throw new BodyParserError(
509
513
  "body-parser/urlencoded-poisoned-key",
510
514
  "urlencoded body contains forbidden key '" + k + "' (prototype-pollution defense)",
@@ -628,10 +632,10 @@ function _parseMultipartHeaders(rawHeaders) {
628
632
  true, HTTP_STATUS.BAD_REQUEST
629
633
  );
630
634
  }
631
- var idx = line.indexOf(":");
632
- if (idx === -1) continue;
633
- var k = line.slice(0, idx).trim().toLowerCase();
634
- var v = line.slice(idx + 1).trim();
635
+ var khv = structuredFields.parseKeyValuePiece(line, ":");
636
+ if (khv.value === null) continue;
637
+ var k = khv.key;
638
+ var v = khv.value.trim();
635
639
  for (var j = 0; j < v.length; j++) {
636
640
  var c = v.charCodeAt(j);
637
641
  if (c === 0 || c === 10 || c === 13) { // NUL/LF/CR forbidden in field-value (RFC 9110 §5.5)
@@ -720,12 +724,8 @@ function _parseHeaderParams(headerValue, filenameCharsets) {
720
724
  // it last so Object.fromEntries' last-wins resolves it.
721
725
  var paramPairs = [["_value", parts[0].trim().toLowerCase()]];
722
726
  var extName = null;
723
- for (var i = 1; i < parts.length; i++) {
724
- var p = parts[i].trim();
725
- var eq = p.indexOf("=");
726
- if (eq === -1) continue;
727
- var k = p.slice(0, eq).trim().toLowerCase();
728
- var v = p.slice(eq + 1).trim();
727
+ var kvps = structuredFields.parseKeyValuePieces(parts, 1);
728
+ structuredFields.forEachKeyValue(kvps, function (k, v) {
729
729
  var _unq = structuredFields.unquoteSfString(v);
730
730
  if (_unq !== null) v = _unq;
731
731
  if (k.charAt(k.length - 1) === "*") {
@@ -735,10 +735,10 @@ function _parseHeaderParams(headerValue, filenameCharsets) {
735
735
  if (bareKey === "filename") extName = decoded;
736
736
  paramPairs.push([bareKey, decoded]);
737
737
  }
738
- continue;
738
+ return;
739
739
  }
740
740
  paramPairs.push([k, v]);
741
- }
741
+ });
742
742
  if (extName !== null) paramPairs.push(["filename", extName]);
743
743
  return _mapFromPairs(paramPairs);
744
744
  }
@@ -936,7 +936,7 @@ async function _parseMultipart(req, opts, ctParams) {
936
936
  // Read until \r\n\r\n.
937
937
  var headEnd = pending.indexOf("\r\n\r\n");
938
938
  if (headEnd === -1) {
939
- if (pending.length > C.BYTES.kib(16)) {
939
+ if (safeBuffer.byteLengthOf(pending) > C.BYTES.kib(16)) {
940
940
  done(new BodyParserError("body-parser/multipart-headers-too-large",
941
941
  "multipart part headers exceed 16KB", true, 413));
942
942
  return;
@@ -970,7 +970,7 @@ async function _parseMultipart(req, opts, ctParams) {
970
970
  "multipart part missing form-data Content-Disposition", true, HTTP_STATUS.BAD_REQUEST));
971
971
  return;
972
972
  }
973
- if (POISONED_KEYS.has(cd.name)) {
973
+ if (pick.isPoisonedKey(cd.name)) {
974
974
  done(new BodyParserError("body-parser/multipart-poisoned-field",
975
975
  "multipart field '" + cd.name + "' is forbidden (prototype-pollution defense)",
976
976
  true, HTTP_STATUS.BAD_REQUEST));
@@ -1476,8 +1476,32 @@ function create(opts) {
1476
1476
  }
1477
1477
  var status = (e && typeof e.statusCode === "number") ? e.statusCode : HTTP_STATUS.BAD_REQUEST;
1478
1478
  var code = (e && typeof e.code === "string") ? e.code : "body-parser/error";
1479
- var message = (e && e.message) ? e.message : String(e);
1480
- _writeError(res, status, message, code);
1479
+ // Only a framework-classified 4xx BodyParserError carries a curated,
1480
+ // operator-safe message (malformed JSON, poisoned key, smuggling). Any
1481
+ // other thrown error — and every 5xx — gets a generic status phrase, so
1482
+ // an internal exception (fs errno + tmp path, a parse hook's thrown
1483
+ // secret) is never echoed to the client (CWE-209). The full diagnostic
1484
+ // stays on the audit chain, redacted by safeEmit.
1485
+ // Object(e) tolerates a non-object throw (throw null, or a string from
1486
+ // an operator parse hook) without a truthiness guard on the caught
1487
+ // binding — the caught value is always defined to CodeQL, so `e && …`
1488
+ // reads as a dead sub-condition; this keeps the null-safety explicit.
1489
+ var eo = Object(e);
1490
+ var clientMessage = (eo.isBodyParserError === true && status < 500 && typeof eo.message === "string")
1491
+ ? eo.message
1492
+ : _genericReason(status);
1493
+ try {
1494
+ audit().safeEmit({
1495
+ action: "body-parser.error",
1496
+ outcome: status >= 500 ? "failure" : "denied",
1497
+ metadata: {
1498
+ status: status,
1499
+ code: code,
1500
+ message: (e && e.message) ? String(e.message).slice(0, 256) : "",
1501
+ },
1502
+ });
1503
+ } catch (_e) { /* audit best-effort — never mask the response */ }
1504
+ _writeError(res, status, clientMessage, code);
1481
1505
  }
1482
1506
  };
1483
1507
  }
@@ -1501,9 +1525,11 @@ async function _parseJsonFromBuf(buf, opts) {
1501
1525
  }
1502
1526
  if (typeof opts.parseHook === "function") {
1503
1527
  try { parsed = opts.parseHook(parsed); }
1504
- catch (e) {
1528
+ catch (_e) {
1529
+ // Operator parseHook threw — fixed safe message; the hook's thrown
1530
+ // detail (possible secrets) is never echoed to the client (CWE-209).
1505
1531
  throw new BodyParserError("body-parser/json-hook",
1506
- "JSON parseHook failed: " + ((e && e.message) || String(e)), true, HTTP_STATUS.BAD_REQUEST);
1532
+ "request body rejected by parse hook", true, HTTP_STATUS.BAD_REQUEST);
1507
1533
  }
1508
1534
  }
1509
1535
  return parsed;
@@ -1612,12 +1638,9 @@ function _resolveStandaloneMultipartOpts(opts, ct) {
1612
1638
  }
1613
1639
  if (opts.maxFiles !== undefined) {
1614
1640
  var mf = opts.maxFiles;
1615
- var mfBad = typeof mf !== "number" || !isFinite(mf) || mf <= 0 || Math.floor(mf) !== mf;
1616
- if (mfBad) {
1617
- throw new BodyParserError("body-parser/bad-max-files",
1618
- "parsers.multipart: opts.maxFiles must be a positive integer",
1619
- true, HTTP_STATUS.BAD_REQUEST);
1620
- }
1641
+ numericBounds.requirePositiveFiniteInt(mf,
1642
+ "parsers.multipart: opts.maxFiles", BodyParserError, "body-parser/bad-max-files",
1643
+ null, { permanent: true, statusCode: HTTP_STATUS.BAD_REQUEST });
1621
1644
  resolved.fileCount = mf;
1622
1645
  }
1623
1646
  // Pass-through overrides for the multipart-specific knobs the middleware
@@ -1663,5 +1686,7 @@ module.exports = {
1663
1686
  _contentType: _contentType,
1664
1687
  _hasBody: _hasBody,
1665
1688
  _sanitizeFilename: _sanitizeFilename,
1666
- POISONED_KEYS: POISONED_KEYS,
1689
+ // Sourced from the canonical pick primitive (no hand-rolled set); a fresh
1690
+ // Set so the long-standing `instanceof Set` shape of this export is kept.
1691
+ POISONED_KEYS: new Set(pick.POISONED_KEYS),
1667
1692
  };
@@ -30,6 +30,7 @@ var defineClass = require("../framework-error").defineClass;
30
30
  var lazyRequire = require("../lazy-require");
31
31
  var validateOpts = require("../validate-opts");
32
32
  var safeBuffer = require("../safe-buffer");
33
+ var requestHelpers = require("../request-helpers");
33
34
 
34
35
  var audit = lazyRequire(function () { return require("../audit"); });
35
36
 
@@ -93,17 +94,13 @@ function create(opts) {
93
94
  var actionBase = typeof opts.auditAction === "string" && opts.auditAction.length > 0
94
95
  ? opts.auditAction : "middleware.bot_disclose";
95
96
 
97
+ // null mountPaths = apply to every route; otherwise reuse the shared
98
+ // segment-boundary matcher (built once).
99
+ var _mountMatch = mountPaths
100
+ ? requestHelpers.makeSkipMatcher({ skipPaths: mountPaths }, "middleware.botDisclose")
101
+ : null;
96
102
  function _matches(req) {
97
- if (!mountPaths) return true; // null = match every path
98
- var p = req.url || "";
99
- var qpos = p.indexOf("?");
100
- if (qpos !== -1) p = p.slice(0, qpos);
101
- for (var i = 0; i < mountPaths.length; i++) {
102
- var m = mountPaths[i];
103
- if (typeof m === "string" && (p === m || p.indexOf(m + "/") === 0)) return true;
104
- if (m instanceof RegExp && m.test(p)) return true;
105
- }
106
- return false;
103
+ return _mountMatch ? _mountMatch(req) : true;
107
104
  }
108
105
 
109
106
  function _emitAudit(action, outcome, metadata) {
@@ -144,21 +144,13 @@ function create(opts) {
144
144
  var blockedAgents = DEFAULT_BLOCKED_AGENTS.concat((opts.blockedAgents || []).map(function (r, i) {
145
145
  return _coerceAgentPattern(r, "middleware.botGuard: blockedAgents[" + i + "]");
146
146
  }));
147
- var skipPaths = opts.skipPaths || [];
148
147
  var statusOnBlock = opts.statusOnBlock || 403;
149
148
  var bodyOnBlock = opts.bodyOnBlock !== undefined ? opts.bodyOnBlock : "Forbidden";
150
149
  var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
151
150
  var problemMode = opts.problemDetails === true;
152
151
 
153
- function _shouldSkip(req) {
154
- var path = req.pathname || req.url || "/";
155
- for (var i = 0; i < skipPaths.length; i++) {
156
- if (typeof skipPaths[i] === "string" ? path.indexOf(skipPaths[i]) === 0 : skipPaths[i].test(path)) {
157
- return true;
158
- }
159
- }
160
- return false;
161
- }
152
+ // Path-exemption predicate (string-prefix or RegExp), validated at create().
153
+ var _shouldSkip = requestHelpers.makeSkipMatcher(opts, "middleware.botGuard");
162
154
 
163
155
  function _looksLikeApi(req) {
164
156
  var path = req.pathname || req.url || "/";
@@ -67,6 +67,7 @@
67
67
  * }
68
68
  */
69
69
  var lazyRequire = require("../lazy-require");
70
+ var pick = require("../pick");
70
71
  var forms = require("../forms");
71
72
  var requestHelpers = require("../request-helpers");
72
73
  var validateOpts = require("../validate-opts");
@@ -111,7 +112,7 @@ function _parseCookieHeader(header) {
111
112
  if (eq === -1) continue;
112
113
  var k = p.slice(0, eq).trim();
113
114
  if (k.length === 0) continue;
114
- if (k === "__proto__" || k === "constructor" || k === "prototype") continue;
115
+ if (pick.isPoisonedKey(k)) continue;
115
116
  if (seen.has(k)) continue; // first-occurrence wins
116
117
  seen.add(k);
117
118
  var v = p.slice(eq + 1).trim();
@@ -296,7 +297,7 @@ function create(opts) {
296
297
  validateOpts(opts, [
297
298
  "cookie", "tokenLookup", "fieldName", "headerName", "methods", "audit",
298
299
  "trustProxy", "checkOrigin", "allowedOrigins", "requireJsonContentType",
299
- "requireOrigin", "skipStateless", "onDeny", "problemDetails",
300
+ "requireOrigin", "skipStateless", "skipPaths", "skip", "onDeny", "problemDetails",
300
301
  ], "middleware.csrfProtect");
301
302
  var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
302
303
  var problemMode = opts.problemDetails === true;
@@ -368,6 +369,13 @@ function create(opts) {
368
369
  // request always carries a Cookie header and is validated.
369
370
  var skipStateless = opts.skipStateless === true;
370
371
 
372
+ // Per-path exemption (string-prefix / RegExp / skip predicate), validated at
373
+ // create(). Lets one route (an RFC 8058 List-Unsubscribe-Post endpoint, a
374
+ // webhook, a bearer-token API path on the same app) opt out of the
375
+ // double-submit check while the app-level mount stays in place — the token is
376
+ // still issued for any later browser flow.
377
+ var _shouldSkip = requestHelpers.makeSkipMatcher(opts, "middleware.csrfProtect");
378
+
371
379
  // Cookie issuance config (only when opts.cookie is set).
372
380
  var cookieCfg = null;
373
381
  if (hasCookie) {
@@ -480,6 +488,9 @@ function create(opts) {
480
488
  // (e.g. error response) still need req.csrfToken populated.
481
489
  var expected = _issueIfNeeded(req, res);
482
490
 
491
+ // Exempt routes skip the double-submit check (token still issued above).
492
+ if (_shouldSkip(req)) return next();
493
+
483
494
  if (methods.indexOf(req.method) === -1) return next();
484
495
 
485
496
  // Stateless / token-authenticated requests are not CSRF-able — the
@@ -107,7 +107,6 @@ function create(opts) {
107
107
  }
108
108
  var bytesPerDay = opts.bytesPerDay;
109
109
  var getKey = typeof opts.getKey === "function" ? opts.getKey : _defaultGetKey;
110
- var auditOn = opts.audit !== false;
111
110
  // onDeny is the canonical hook across the deny-path middleware
112
111
  // family; onExceeded is the original name kept working as an alias.
113
112
  var onDeny = typeof opts.onDeny === "function" ? opts.onDeny
@@ -127,33 +126,14 @@ function create(opts) {
127
126
  });
128
127
  var backend = quota._backend;
129
128
 
130
- function _shouldSkip(req) {
131
- if (skipPaths.length === 0) return false;
132
- var p = req.url || req.originalUrl || "";
133
- var qpos = p.indexOf("?");
134
- if (qpos !== -1) p = p.slice(0, qpos);
135
- for (var i = 0; i < skipPaths.length; i++) {
136
- if (typeof skipPaths[i] === "string" && p === skipPaths[i]) return true;
137
- if (skipPaths[i] instanceof RegExp && skipPaths[i].test(p)) return true;
138
- }
139
- return false;
140
- }
129
+ // exact:true preserves this guard's whole-path skip semantics (no descendant
130
+ // match); makeSkipMatcher strips the query + resolves url || originalUrl.
131
+ var _shouldSkip = requestHelpers().makeSkipMatcher(
132
+ { skipPaths: skipPaths, exact: true }, "middleware.dailyByteQuota");
141
133
 
142
- function _emitAudit(action, outcome, metadata) {
143
- if (!auditOn) return;
144
- try {
145
- audit().safeEmit({
146
- action: "middleware.daily_byte_quota." + action,
147
- outcome: outcome,
148
- metadata: metadata || {},
149
- });
150
- } catch (_e) { /* drop-silent — audit is best-effort */ }
151
- }
134
+ var _emitAudit = audit().namespaced("middleware.daily_byte_quota", opts.audit);
152
135
 
153
- function _emitMetric(verb, n, labels) {
154
- try { observability().safeEvent("middleware.daily_byte_quota." + verb, n || 1, labels || {}); }
155
- catch (_e) { /* drop-silent */ }
156
- }
136
+ var _emitMetric = observability().namespaced("middleware.daily_byte_quota");
157
137
 
158
138
  return async function dailyByteQuotaMiddleware(req, res, next) {
159
139
  if (_shouldSkip(req)) return next();
@@ -147,6 +147,24 @@ function denyResponse(req, res, ctx) {
147
147
  return undefined;
148
148
  }
149
149
 
150
+ // methodNotAllowed(res, allow) — write the bare HTTP 405 refusal the
151
+ // single-file static-content middlewares (assetlinks / security.txt / web-app-
152
+ // manifest) share: an `Allow` header plus a `text/plain` "Method Not Allowed"
153
+ // body with an explicit Content-Length. Kept separate from denyResponse() (the
154
+ // hook-aware, problem+json-capable refusal path) so the static handlers emit a
155
+ // fixed, minimal response — `allow` is the comma-joined method list (e.g.
156
+ // "GET, HEAD").
157
+ function methodNotAllowed(res, allow) {
158
+ var bodyMsg = "Method Not Allowed";
159
+ res.writeHead(405, { // HTTP 405 status
160
+ "Allow": allow,
161
+ "Content-Type": "text/plain; charset=utf-8",
162
+ "Content-Length": Buffer.byteLength(bodyMsg),
163
+ });
164
+ res.end(bodyMsg);
165
+ }
166
+
150
167
  module.exports = {
151
- denyResponse: denyResponse,
168
+ denyResponse: denyResponse,
169
+ methodNotAllowed: methodNotAllowed,
152
170
  };
@@ -165,6 +165,7 @@ function create(opts) {
165
165
  "allowSameSite", "allowCrossSite", "allowMissing",
166
166
  "allowedDest", "deniedDest", "allowStorageAccess", "strictDest",
167
167
  "allowedNavigate", "methods", "audit", "onDeny", "problemDetails",
168
+ "skipPaths", "skip",
168
169
  ], "middleware.fetchMetadata");
169
170
  validateOpts.optionalBoolean(opts.allowStorageAccess, "middleware.fetchMetadata: allowStorageAccess");
170
171
  validateOpts.optionalBoolean(opts.strictDest, "middleware.fetchMetadata: strictDest");
@@ -174,6 +175,11 @@ function create(opts) {
174
175
  _validateDestList(opts.deniedDest, "deniedDest");
175
176
  }
176
177
 
178
+ // Per-path exemption (string-prefix / RegExp / skip predicate), validated at
179
+ // create() — exempt a webhook / cookieless edge-cached route from the
180
+ // fetch-metadata gate without disabling the app-level mount.
181
+ var _shouldSkip = requestHelpers.makeSkipMatcher(opts, "middleware.fetchMetadata");
182
+
177
183
  var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
178
184
  var problemMode = opts.problemDetails === true;
179
185
  var allowSameSite = opts.allowSameSite !== false;
@@ -220,6 +226,7 @@ function create(opts) {
220
226
  // Idempotent: a second fetch-metadata mount this request is a no-op.
221
227
  if (req._fetchMetadataChecked) return next();
222
228
  req._fetchMetadataChecked = true;
229
+ if (_shouldSkip(req)) return next();
223
230
  if (methods.indexOf(req.method) === -1) return next();
224
231
 
225
232
  var headers = req.headers || {};
@@ -53,7 +53,7 @@ var cryptoField = require("../crypto-field");
53
53
  var vault = require("../vault");
54
54
  var { defineClass } = require("../framework-error");
55
55
 
56
- var audit = lazyRequire(function () { return require("../audit"); });
56
+ var auditEmit = require("../audit-emit");
57
57
  var problemDetails = lazyRequire(function () { return require("../problem-details"); });
58
58
  var C = require("../constants");
59
59
 
@@ -533,16 +533,8 @@ function dbStore(opts) {
533
533
  }
534
534
 
535
535
  function _validateStore(store, where) {
536
- if (!store || typeof store !== "object") {
537
- throw new IdempotencyError("idempotency/bad-store",
538
- where + ": store must be an object", true);
539
- }
540
- if (typeof store.get !== "function" ||
541
- typeof store.set !== "function" ||
542
- typeof store.delete !== "function") {
543
- throw new IdempotencyError("idempotency/bad-store",
544
- where + ": store must implement { get, set, delete }", true);
545
- }
536
+ validateOpts.requireMethods(store, ["get", "set", "delete"],
537
+ where + ": store", IdempotencyError, "idempotency/bad-store", true);
546
538
  }
547
539
 
548
540
  function _fingerprintRequest(req, bodyBytes, store) {
@@ -568,15 +560,7 @@ function _fingerprintRequest(req, bodyBytes, store) {
568
560
  return nodeCrypto.createHash("sha3-256").update(preimage).digest("hex");
569
561
  }
570
562
 
571
- function _emitAudit(action, metadata, outcome) {
572
- try {
573
- audit().safeEmit({
574
- action: action,
575
- outcome: outcome || "success",
576
- metadata: metadata,
577
- });
578
- } catch (_e) { /* best-effort */ }
579
- }
563
+ var _emitAudit = auditEmit.emit;
580
564
 
581
565
  /**
582
566
  * @primitive b.middleware.idempotencyKey
@@ -56,6 +56,7 @@
56
56
  *
57
57
  * Audit: every limit hit emits system.ratelimit.block with the key + path.
58
58
  */
59
+ var boundedMap = require("../bounded-map");
59
60
  var C = require("../constants");
60
61
  var frameworkSchema = require("../framework-schema");
61
62
  var lazyRequire = require("../lazy-require");
@@ -178,11 +179,14 @@ function _memoryTokenBucketBackend(opts) {
178
179
  // microtask cost per request.
179
180
  function take(key, _cost) {
180
181
  var now = Date.now();
181
- var b = buckets.get(key);
182
- if (!b) {
183
- b = { tokens: burst, lastRefillAt: now };
184
- buckets.set(key, b);
185
- } else {
182
+ // Insert a fresh full bucket on first sight; only an already-present
183
+ // bucket refills (a brand-new bucket starts at `burst`, so refilling
184
+ // it would be a no-op clamped to `burst` anyway).
185
+ var existed = buckets.has(key);
186
+ var b = boundedMap.getOrInsert(buckets, key, function () {
187
+ return { tokens: burst, lastRefillAt: now };
188
+ });
189
+ if (existed) {
186
190
  var elapsed = (now - b.lastRefillAt) / C.TIME.seconds(1);
187
191
  b.tokens = Math.min(burst, b.tokens + elapsed * refillPerSecond);
188
192
  b.lastRefillAt = now;
@@ -254,10 +258,15 @@ function _memoryFixedWindowBackend(opts) {
254
258
  function take(key, _cost) {
255
259
  var now = Date.now();
256
260
  var windowStart = Math.floor(now / windowMs) * windowMs;
257
- var c = counters.get(key);
258
- if (!c || c.windowStart !== windowStart) {
259
- c = { windowStart: windowStart, count: 0 };
260
- counters.set(key, c);
261
+ var c = boundedMap.getOrInsert(counters, key, function () {
262
+ return { windowStart: windowStart, count: 0 };
263
+ });
264
+ // A key carried over from a prior window re-seeds to the current
265
+ // window (count restarts) — getOrInsert only handles first-sight, so
266
+ // the rollover case resets the existing record in place.
267
+ if (c.windowStart !== windowStart) {
268
+ c.windowStart = windowStart;
269
+ c.count = 0;
261
270
  }
262
271
  c.count += 1;
263
272
  if (c.count <= max) {
@@ -489,29 +498,12 @@ function create(opts) {
489
498
  ? opts.headerPrefix : "X-RateLimit-";
490
499
  var limitHeader = headerPrefix + "Limit"; // allow:hand-rolled-sql — HTTP response-header name (X-RateLimit-Limit), not a SQL LIMIT clause
491
500
  var remainingHeader = headerPrefix + "Remaining";
492
- var skipPaths = opts.skipPaths || [];
493
- // Throw at create(): each entry must be a string prefix or a RegExp.
494
- // Anything else would crash _shouldSkip with TypeError on the first request.
495
- for (var sp = 0; sp < skipPaths.length; sp++) {
496
- if (typeof skipPaths[sp] !== "string" && !(skipPaths[sp] instanceof RegExp)) {
497
- throw new Error("middleware.rateLimit: skipPaths[" + sp +
498
- "] must be a string prefix or RegExp, got " + typeof skipPaths[sp]);
499
- }
500
- }
501
+ // Path-exemption predicate (string-prefix or RegExp), validated at create().
502
+ var _shouldSkip = requestHelpers.makeSkipMatcher(opts, "middleware.rateLimit");
501
503
  var scope = opts.scope || "global";
502
504
 
503
505
  var backend = _resolveBackend(opts);
504
506
 
505
- function _shouldSkip(req) {
506
- var path = req.pathname || req.url || "/";
507
- for (var i = 0; i < skipPaths.length; i++) {
508
- if (typeof skipPaths[i] === "string" ? path.indexOf(skipPaths[i]) === 0 : skipPaths[i].test(path)) {
509
- return true;
510
- }
511
- }
512
- return false;
513
- }
514
-
515
507
  function _writeBlocked(req, res, k, verdict) {
516
508
  if (emitHeaders && typeof res.setHeader === "function") {
517
509
  res.setHeader(limitHeader, String(verdict.limit));
@@ -4,6 +4,7 @@
4
4
  // + /Schemas surfaces backed by operator-supplied CRUD callbacks.
5
5
 
6
6
  var framework_error = require("../framework-error");
7
+ var pick = require("../pick");
7
8
  var validateOpts = require("../validate-opts");
8
9
  var safeJson = require("../safe-json");
9
10
  var safeBuffer = require("../safe-buffer");
@@ -516,7 +517,7 @@ function _walkBulkIdRefs(value, out) {
516
517
  if (value && typeof value === "object") {
517
518
  var keys = Object.keys(value);
518
519
  for (var k = 0; k < keys.length; k++) {
519
- if (keys[k] === "__proto__" || keys[k] === "constructor" || keys[k] === "prototype") continue;
520
+ if (pick.isPoisonedKey(keys[k])) continue;
520
521
  _walkBulkIdRefs(value[keys[k]], out);
521
522
  }
522
523
  }
@@ -753,7 +754,7 @@ function _resolveBulkIdRefs(value, bulkIdMap) {
753
754
  var out = {};
754
755
  var keys = Object.keys(value);
755
756
  for (var k = 0; k < keys.length; k++) {
756
- if (keys[k] === "__proto__" || keys[k] === "constructor" || keys[k] === "prototype") continue;
757
+ if (pick.isPoisonedKey(keys[k])) continue;
757
758
  out[keys[k]] = _resolveBulkIdRefs(value[keys[k]], bulkIdMap);
758
759
  }
759
760
  return out;
@@ -151,7 +151,15 @@ var PP_POLICY_RE =
151
151
  /^[a-z][a-z0-9-]*=(?:\*|\([^)]*\)|self)$/;
152
152
  function _validatePermissionsPolicy(value) {
153
153
  if (typeof value !== "string" || value.length === 0) return;
154
- var parts = String(value).split(/\s*,\s*/);
154
+ // Split on the literal comma, then strip whitespace adjacent to each comma —
155
+ // the semantics of /\s*,\s*/ without that pattern's O(n^2) backtracking on a
156
+ // long comma-less run of whitespace. trimStart/trimEnd use the same Unicode
157
+ // whitespace set as \s.
158
+ var parts = String(value).split(",");
159
+ for (var s = 0; s < parts.length; s += 1) {
160
+ if (s > 0) parts[s] = parts[s].trimStart();
161
+ if (s < parts.length - 1) parts[s] = parts[s].trimEnd();
162
+ }
155
163
  for (var i = 0; i < parts.length; i += 1) {
156
164
  var p = parts[i];
157
165
  if (!p) continue;