@blamejs/core 0.6.69 → 0.7.0

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 (164) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +11 -1
  3. package/index.js +2 -5
  4. package/lib/api-key.js +24 -55
  5. package/lib/api-snapshot.js +4 -1
  6. package/lib/app-shutdown.js +20 -16
  7. package/lib/app.js +6 -5
  8. package/lib/archive.js +58 -50
  9. package/lib/atomic-file.js +11 -5
  10. package/lib/audit-chain.js +12 -4
  11. package/lib/audit-sign.js +38 -13
  12. package/lib/audit-tools.js +4 -4
  13. package/lib/audit.js +8 -4
  14. package/lib/auth/jwt.js +3 -2
  15. package/lib/auth/lockout.js +3 -13
  16. package/lib/auth/oauth.js +27 -17
  17. package/lib/auth/passkey.js +1 -5
  18. package/lib/auth/password.js +23 -13
  19. package/lib/backup/bundle.js +2 -4
  20. package/lib/backup/crypto.js +8 -6
  21. package/lib/backup/index.js +18 -14
  22. package/lib/backup/manifest.js +6 -2
  23. package/lib/break-glass.js +12 -7
  24. package/lib/bundler.js +32 -21
  25. package/lib/cache-redis.js +8 -3
  26. package/lib/cache.js +39 -59
  27. package/lib/canonical-json.js +17 -2
  28. package/lib/chain-writer.js +2 -2
  29. package/lib/cli-helpers.js +9 -4
  30. package/lib/cli.js +27 -5
  31. package/lib/cluster-provider-db.js +2 -1
  32. package/lib/cluster-storage.js +71 -11
  33. package/lib/cluster.js +30 -25
  34. package/lib/config-drift.js +3 -4
  35. package/lib/constants.js +8 -3
  36. package/lib/cookies.js +16 -4
  37. package/lib/credential-hash.js +4 -7
  38. package/lib/crypto.js +9 -9
  39. package/lib/csv.js +11 -8
  40. package/lib/db-declare-row-policy.js +4 -9
  41. package/lib/db-declare-view.js +3 -6
  42. package/lib/db-query.js +9 -3
  43. package/lib/db-schema.js +75 -10
  44. package/lib/db.js +116 -70
  45. package/lib/deprecate.js +3 -8
  46. package/lib/dev.js +30 -19
  47. package/lib/dual-control.js +2 -4
  48. package/lib/error-page.js +32 -39
  49. package/lib/external-db-migrate.js +27 -20
  50. package/lib/external-db.js +11 -3
  51. package/lib/file-type.js +16 -8
  52. package/lib/forms.js +48 -12
  53. package/lib/framework-error.js +1 -2
  54. package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
  55. package/lib/handlers.js +1 -1
  56. package/lib/http-client-cookie-jar.js +25 -18
  57. package/lib/http-client.js +46 -23
  58. package/lib/i18n.js +6 -16
  59. package/lib/jobs.js +5 -1
  60. package/lib/log-stream-cloudwatch.js +31 -30
  61. package/lib/log-stream-local.js +12 -3
  62. package/lib/log-stream-otlp-grpc.js +29 -23
  63. package/lib/log-stream-otlp.js +13 -19
  64. package/lib/log-stream-syslog.js +27 -22
  65. package/lib/log-stream-webhook.js +8 -17
  66. package/lib/log-stream.js +13 -2
  67. package/lib/log.js +83 -5
  68. package/lib/mail-bounce.js +22 -15
  69. package/lib/mail-dkim.js +3 -6
  70. package/lib/mail.js +62 -34
  71. package/lib/metrics.js +61 -25
  72. package/lib/middleware/api-encrypt.js +15 -13
  73. package/lib/middleware/body-parser.js +46 -39
  74. package/lib/middleware/bot-guard.js +25 -4
  75. package/lib/middleware/compression.js +26 -11
  76. package/lib/middleware/cors.js +6 -5
  77. package/lib/middleware/csp-nonce.js +17 -4
  78. package/lib/middleware/csrf-protect.js +1 -1
  79. package/lib/middleware/db-role-for.js +7 -30
  80. package/lib/middleware/health.js +42 -8
  81. package/lib/middleware/index.js +55 -36
  82. package/lib/middleware/rate-limit.js +11 -8
  83. package/lib/middleware/request-id.js +10 -2
  84. package/lib/middleware/request-log.js +3 -2
  85. package/lib/middleware/require-auth.js +5 -2
  86. package/lib/middleware/sse.js +5 -4
  87. package/lib/migration-files.js +37 -0
  88. package/lib/migrations.js +42 -17
  89. package/lib/mtls-ca.js +38 -17
  90. package/lib/mtls-engine-default.js +15 -7
  91. package/lib/network-dns.js +49 -20
  92. package/lib/network-heartbeat.js +7 -7
  93. package/lib/network-nts.js +70 -48
  94. package/lib/network-proxy.js +34 -15
  95. package/lib/network-tls.js +7 -5
  96. package/lib/network.js +3 -3
  97. package/lib/notify.js +14 -36
  98. package/lib/ntp-check.js +7 -6
  99. package/lib/numeric-bounds.js +35 -3
  100. package/lib/object-store/azure-blob-bucket-ops.js +73 -15
  101. package/lib/object-store/azure-blob.js +31 -24
  102. package/lib/object-store/gcs-bucket-ops.js +41 -17
  103. package/lib/object-store/gcs.js +41 -34
  104. package/lib/object-store/http-put.js +2 -11
  105. package/lib/object-store/http-request.js +38 -0
  106. package/lib/object-store/index.js +1 -1
  107. package/lib/object-store/sigv4-bucket-ops.js +33 -37
  108. package/lib/object-store/sigv4.js +50 -30
  109. package/lib/observability.js +14 -3
  110. package/lib/otel-export.js +16 -14
  111. package/lib/pagination.js +23 -8
  112. package/lib/parsers/index.js +11 -5
  113. package/lib/parsers/safe-env.js +43 -14
  114. package/lib/parsers/safe-ini.js +29 -10
  115. package/lib/parsers/safe-toml.js +55 -25
  116. package/lib/parsers/safe-xml.js +22 -11
  117. package/lib/parsers/safe-yaml.js +55 -17
  118. package/lib/permissions.js +24 -33
  119. package/lib/pqc-agent.js +4 -2
  120. package/lib/pqc-gate.js +28 -15
  121. package/lib/protobuf-encoder.js +9 -3
  122. package/lib/protocol-dispatcher.js +29 -12
  123. package/lib/pubsub-redis.js +25 -8
  124. package/lib/pubsub.js +84 -12
  125. package/lib/queue-local.js +13 -4
  126. package/lib/queue-redis.js +2 -2
  127. package/lib/queue-sqs.js +21 -16
  128. package/lib/queue.js +8 -4
  129. package/lib/redis-client.js +13 -10
  130. package/lib/request-helpers.js +33 -2
  131. package/lib/restore-bundle.js +2 -4
  132. package/lib/restore-rollback.js +4 -1
  133. package/lib/restore.js +12 -10
  134. package/lib/retry.js +58 -9
  135. package/lib/router.js +96 -27
  136. package/lib/safe-async.js +97 -4
  137. package/lib/safe-buffer.js +47 -0
  138. package/lib/safe-json.js +18 -9
  139. package/lib/safe-schema.js +54 -18
  140. package/lib/safe-url.js +6 -2
  141. package/lib/scheduler.js +25 -14
  142. package/lib/security-assert.js +31 -26
  143. package/lib/seeders.js +71 -60
  144. package/lib/session.js +13 -7
  145. package/lib/slug.js +35 -8
  146. package/lib/ssrf-guard.js +58 -43
  147. package/lib/static.js +11 -7
  148. package/lib/storage.js +2 -1
  149. package/lib/template.js +17 -7
  150. package/lib/testing.js +28 -8
  151. package/lib/time.js +26 -5
  152. package/lib/totp.js +18 -9
  153. package/lib/tracing.js +15 -5
  154. package/lib/uuid.js +47 -20
  155. package/lib/validate-opts.js +213 -0
  156. package/lib/vault/index.js +36 -27
  157. package/lib/vault/rotate.js +41 -22
  158. package/lib/vault/wrap.js +26 -19
  159. package/lib/webhook.js +37 -68
  160. package/lib/websocket.js +60 -46
  161. package/package.json +1 -1
  162. package/sbom.cyclonedx.json +6 -6
  163. package/lib/auth/totp.js +0 -17
  164. package/lib/object-store/retry.js +0 -15
@@ -114,11 +114,14 @@ var path = require("path");
114
114
  var nodeCrypto = require("node:crypto");
115
115
  var atomicFile = require("../atomic-file");
116
116
  var crypto = require("../crypto");
117
+ var requestHelpers = require("../request-helpers");
118
+ var safeBuffer = require("../safe-buffer");
117
119
  var safeJson = require("../safe-json");
118
120
  var validateOpts = require("../validate-opts");
119
121
  var C = require("../constants");
120
122
  var { defineClass } = require("../framework-error");
121
123
 
124
+ var HTTP_STATUS = requestHelpers.HTTP_STATUS;
122
125
  var BodyParserError = defineClass("BodyParserError", { withStatusCode: true });
123
126
 
124
127
  // Mirrors safe-json.js + safe-schema.js. Field names that match these
@@ -167,6 +170,11 @@ var DEFAULTS = Object.freeze({
167
170
 
168
171
  var BODY_BEARING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
169
172
 
173
+ // Headroom on top of arrayLimit before declaring a key-bomb. Sized so a
174
+ // reasonable form (a few hundred fields with repeated names) fits without
175
+ // tripping the cap.
176
+ var URLENCODED_KEY_HEADROOM = C.BYTES.bytes(1000);
177
+
170
178
  // ---- helpers ----
171
179
 
172
180
  function _contentType(req) {
@@ -255,7 +263,7 @@ function _bufferBody(req, limit) {
255
263
  reject(new BodyParserError(
256
264
  "body-parser/bad-content-length",
257
265
  "Content-Length is not a sequence of decimal digits: " + JSON.stringify(cl),
258
- true, 400
266
+ true, HTTP_STATUS.BAD_REQUEST
259
267
  ));
260
268
  return;
261
269
  }
@@ -263,31 +271,30 @@ function _bufferBody(req, limit) {
263
271
  reject(new BodyParserError(
264
272
  "body-parser/too-large",
265
273
  "request body exceeds limit (" + clNum + " > " + limit + ")",
266
- true, 413
274
+ true, HTTP_STATUS.PAYLOAD_TOO_LARGE
267
275
  ));
268
276
  return;
269
277
  }
270
278
  }
271
- var chunks = [];
272
- var total = 0;
279
+ var collector = safeBuffer.boundedChunkCollector({
280
+ maxBytes: limit,
281
+ errorClass: BodyParserError,
282
+ sizeCode: "body-parser/too-large",
283
+ sizeMessage: "request body exceeds limit",
284
+ });
273
285
  var done = false;
274
286
  req.on("data", function (chunk) {
275
287
  if (done) return;
276
- total += chunk.length;
277
- if (total > limit) {
288
+ try { collector.push(chunk); }
289
+ catch (e) {
278
290
  done = true;
279
291
  try { req.destroy(); } catch (_e) { /* socket already closed */ }
280
- var err = new BodyParserError(
281
- "body-parser/too-large",
282
- "request body exceeds limit (streamed " + total + " > " + limit + ")",
283
- true, 413
284
- );
285
- reject(err);
292
+ if (e && e.isBodyParserError) e.statusCode = HTTP_STATUS.PAYLOAD_TOO_LARGE;
293
+ reject(e);
286
294
  return;
287
295
  }
288
- chunks.push(chunk);
289
296
  });
290
- req.on("end", function () { if (!done) { done = true; resolve(Buffer.concat(chunks)); } });
297
+ req.on("end", function () { if (!done) { done = true; resolve(collector.result()); } });
291
298
  req.on("error", function (e) { if (!done) { done = true; reject(e); } });
292
299
  });
293
300
  }
@@ -304,7 +311,7 @@ async function _parseJson(req, opts) {
304
311
  throw new BodyParserError(
305
312
  "body-parser/json-strict",
306
313
  "JSON body must start with '{' or '[' (strict mode)",
307
- true, 400
314
+ true, HTTP_STATUS.BAD_REQUEST
308
315
  );
309
316
  }
310
317
  }
@@ -316,7 +323,7 @@ async function _parseJson(req, opts) {
316
323
  throw new BodyParserError(
317
324
  "body-parser/json-malformed",
318
325
  "JSON parse failed: " + ((e && e.message) || String(e)),
319
- true, 400
326
+ true, HTTP_STATUS.BAD_REQUEST
320
327
  );
321
328
  }
322
329
  if (typeof opts.parseHook === "function") {
@@ -325,7 +332,7 @@ async function _parseJson(req, opts) {
325
332
  throw new BodyParserError(
326
333
  "body-parser/json-hook",
327
334
  "JSON parseHook failed: " + ((e && e.message) || String(e)),
328
- true, 400
335
+ true, HTTP_STATUS.BAD_REQUEST
329
336
  );
330
337
  }
331
338
  }
@@ -344,7 +351,7 @@ async function _parseUrlencoded(req, opts) {
344
351
  throw new BodyParserError(
345
352
  "body-parser/urlencoded-malformed",
346
353
  "urlencoded parse failed: " + ((e && e.message) || String(e)),
347
- true, 400
354
+ true, HTTP_STATUS.BAD_REQUEST
348
355
  );
349
356
  }
350
357
  var out = {};
@@ -360,11 +367,11 @@ async function _parseUrlencoded(req, opts) {
360
367
  throw new BodyParserError(
361
368
  "body-parser/urlencoded-poisoned-key",
362
369
  "urlencoded body contains forbidden key '" + k + "' (prototype-pollution defense)",
363
- true, 400
370
+ true, HTTP_STATUS.BAD_REQUEST
364
371
  );
365
372
  }
366
373
  keyCount++;
367
- if (keyCount > opts.arrayLimit + 1000) { // soft cap on total keys
374
+ if (keyCount > opts.arrayLimit + URLENCODED_KEY_HEADROOM) { // soft cap on total keys
368
375
  throw new BodyParserError(
369
376
  "body-parser/urlencoded-too-many-fields",
370
377
  "urlencoded body has too many fields",
@@ -486,7 +493,7 @@ async function _parseMultipart(req, opts, ctParams) {
486
493
  throw new BodyParserError(
487
494
  "body-parser/multipart-no-boundary",
488
495
  "multipart Content-Type missing boundary parameter",
489
- true, 400
496
+ true, HTTP_STATUS.BAD_REQUEST
490
497
  );
491
498
  }
492
499
  // Resolve tmpDir per-request so directory-creation failure surfaces as a
@@ -542,7 +549,7 @@ async function _parseMultipart(req, opts, ctParams) {
542
549
  currentFilename = null;
543
550
  currentMime = null;
544
551
  currentTmpPath = null;
545
- if (currentFd !== null) { try { fs.closeSync(currentFd); } catch (_e) {} currentFd = null; }
552
+ if (currentFd !== null) { try { fs.closeSync(currentFd); } catch (_e) { /* fd already closed */ } currentFd = null; }
546
553
  currentSize = 0;
547
554
  currentHash = null;
548
555
  currentBuf = null;
@@ -571,10 +578,10 @@ async function _parseMultipart(req, opts, ctParams) {
571
578
  }
572
579
 
573
580
  function _cleanup() {
574
- if (currentFd !== null) { try { fs.closeSync(currentFd); } catch (_e) {} currentFd = null; }
575
- if (currentTmpPath) { try { fs.unlinkSync(currentTmpPath); } catch (_e) {} }
581
+ if (currentFd !== null) { try { fs.closeSync(currentFd); } catch (_e) { /* fd already closed */ } currentFd = null; }
582
+ if (currentTmpPath) { try { fs.unlinkSync(currentTmpPath); } catch (_e) { /* tmp file already removed */ } }
576
583
  for (var i = 0; i < files.length; i++) {
577
- try { fs.unlinkSync(files[i].path); } catch (_e) {}
584
+ try { fs.unlinkSync(files[i].path); } catch (_e) { /* tmp file already removed */ }
578
585
  }
579
586
  }
580
587
 
@@ -632,14 +639,14 @@ async function _parseMultipart(req, opts, ctParams) {
632
639
  continue;
633
640
  }
634
641
  done(new BodyParserError("body-parser/multipart-malformed",
635
- "expected --, \\r\\n, or \\n after boundary", true, 400));
642
+ "expected --, \\r\\n, or \\n after boundary", true, HTTP_STATUS.BAD_REQUEST));
636
643
  return;
637
644
  }
638
645
  if (state === MP_HEADERS) {
639
646
  // Read until \r\n\r\n.
640
647
  var headEnd = pending.indexOf("\r\n\r\n");
641
648
  if (headEnd === -1) {
642
- if (pending.length > 16384) {
649
+ if (pending.length > C.BYTES.kib(16)) {
643
650
  done(new BodyParserError("body-parser/multipart-headers-too-large",
644
651
  "multipart part headers exceed 16KB", true, 413));
645
652
  return;
@@ -652,13 +659,13 @@ async function _parseMultipart(req, opts, ctParams) {
652
659
  var cd = _parseHeaderParams(currentHeaders["content-disposition"]);
653
660
  if (cd._value !== "form-data" || typeof cd.name !== "string" || cd.name.length === 0) {
654
661
  done(new BodyParserError("body-parser/multipart-bad-disposition",
655
- "multipart part missing form-data Content-Disposition", true, 400));
662
+ "multipart part missing form-data Content-Disposition", true, HTTP_STATUS.BAD_REQUEST));
656
663
  return;
657
664
  }
658
665
  if (POISONED_KEYS.has(cd.name)) {
659
666
  done(new BodyParserError("body-parser/multipart-poisoned-field",
660
667
  "multipart field '" + cd.name + "' is forbidden (prototype-pollution defense)",
661
- true, 400));
668
+ true, HTTP_STATUS.BAD_REQUEST));
662
669
  return;
663
670
  }
664
671
  currentField = cd.name;
@@ -667,7 +674,7 @@ async function _parseMultipart(req, opts, ctParams) {
667
674
  if (!currentFilename) {
668
675
  done(new BodyParserError("body-parser/multipart-bad-filename",
669
676
  "multipart part filename did not survive sanitization (path traversal or empty)",
670
- true, 400));
677
+ true, HTTP_STATUS.BAD_REQUEST));
671
678
  return;
672
679
  }
673
680
  currentMime = currentHeaders["content-type"] || "application/octet-stream";
@@ -736,7 +743,7 @@ async function _parseMultipart(req, opts, ctParams) {
736
743
 
737
744
  // Generate the tmp path — never derived from the
738
745
  // operator-supplied filename.
739
- var unique = crypto.generateToken(16);
746
+ var unique = crypto.generateToken(C.BYTES.bytes(16));
740
747
  currentTmpPath = path.join(tmpDir, "blamejs-up-" + unique);
741
748
  try {
742
749
  currentFd = fs.openSync(currentTmpPath, "wx", 0o600);
@@ -859,7 +866,7 @@ async function _parseMultipart(req, opts, ctParams) {
859
866
  // fileFilter rejected — already recorded in filesRejected; no
860
867
  // tmp file was opened, nothing to clean up here.
861
868
  } else if (currentFd !== null) {
862
- try { fs.closeSync(currentFd); } catch (_e) {}
869
+ try { fs.closeSync(currentFd); } catch (_e) { /* fd already closed */ }
863
870
  currentFd = null;
864
871
  files.push({
865
872
  field: currentField,
@@ -912,14 +919,14 @@ async function _parseMultipart(req, opts, ctParams) {
912
919
  if (resolved) return;
913
920
  if (state !== MP_DONE) {
914
921
  done(new BodyParserError("body-parser/multipart-truncated",
915
- "multipart stream ended before final boundary", true, 400));
922
+ "multipart stream ended before final boundary", true, HTTP_STATUS.BAD_REQUEST));
916
923
  return;
917
924
  }
918
925
  });
919
926
  req.on("error", function (e) {
920
927
  if (resolved) return;
921
928
  done(new BodyParserError("body-parser/multipart-stream",
922
- "multipart stream error: " + ((e && e.message) || String(e)), true, 400));
929
+ "multipart stream error: " + ((e && e.message) || String(e)), true, HTTP_STATUS.BAD_REQUEST));
923
930
  });
924
931
  });
925
932
  } catch (e) {
@@ -981,7 +988,7 @@ function create(opts) {
981
988
  if (cleanedUp) return;
982
989
  cleanedUp = true;
983
990
  for (var i = 0; i < mpResult.files.length; i++) {
984
- try { fs.unlinkSync(mpResult.files[i].path); } catch (_e) {}
991
+ try { fs.unlinkSync(mpResult.files[i].path); } catch (_e) { /* tmp file already removed */ }
985
992
  }
986
993
  }
987
994
  res.on("finish", cleanup);
@@ -1006,7 +1013,7 @@ function create(opts) {
1006
1013
  "body-parser/unsupported-content-type"
1007
1014
  );
1008
1015
  } catch (e) {
1009
- var status = (e && typeof e.statusCode === "number") ? e.statusCode : 400;
1016
+ var status = (e && typeof e.statusCode === "number") ? e.statusCode : HTTP_STATUS.BAD_REQUEST;
1010
1017
  var code = (e && typeof e.code === "string") ? e.code : "body-parser/error";
1011
1018
  var message = (e && e.message) ? e.message : String(e);
1012
1019
  _writeError(res, status, message, code);
@@ -1022,20 +1029,20 @@ async function _parseJsonFromBuf(buf, opts) {
1022
1029
  var head = text.replace(/^[\s\u00A0\uFEFF]+/, "")[0];
1023
1030
  if (head !== "{" && head !== "[") {
1024
1031
  throw new BodyParserError("body-parser/json-strict",
1025
- "JSON body must start with '{' or '[' (strict mode)", true, 400);
1032
+ "JSON body must start with '{' or '[' (strict mode)", true, HTTP_STATUS.BAD_REQUEST);
1026
1033
  }
1027
1034
  }
1028
1035
  var parsed;
1029
1036
  try { parsed = safeJson.parse(text, { maxBytes: opts.limit }); }
1030
1037
  catch (e) {
1031
1038
  throw new BodyParserError("body-parser/json-malformed",
1032
- "JSON parse failed: " + ((e && e.message) || String(e)), true, 400);
1039
+ "JSON parse failed: " + ((e && e.message) || String(e)), true, HTTP_STATUS.BAD_REQUEST);
1033
1040
  }
1034
1041
  if (typeof opts.parseHook === "function") {
1035
1042
  try { parsed = opts.parseHook(parsed); }
1036
1043
  catch (e) {
1037
1044
  throw new BodyParserError("body-parser/json-hook",
1038
- "JSON parseHook failed: " + ((e && e.message) || String(e)), true, 400);
1045
+ "JSON parseHook failed: " + ((e && e.message) || String(e)), true, HTTP_STATUS.BAD_REQUEST);
1039
1046
  }
1040
1047
  }
1041
1048
  return parsed;
@@ -47,8 +47,25 @@ var DEFAULT_BLOCKED_AGENTS = [
47
47
  var lazyRequire = require("../lazy-require");
48
48
  var requestHelpers = require("../request-helpers");
49
49
  var validateOpts = require("../validate-opts");
50
+ var { defineClass } = require("../framework-error");
50
51
  var audit = lazyRequire(function () { return require("../audit"); });
51
52
 
53
+ var BotGuardError = defineClass("BotGuardError", { alwaysPermanent: true });
54
+
55
+ // allowedAgents / blockedAgents are operator-supplied at create() time
56
+ // (NOT request bytes). The framework requires RegExp instances rather
57
+ // than strings — string-to-regex compilation from an operator-supplied
58
+ // value is an avoidable ReDoS vector at framework boot. Operators
59
+ // constructing patterns dynamically compile at their own call site so
60
+ // the pattern source is visible in their code.
61
+ function _coerceAgentPattern(r, where) {
62
+ if (r instanceof RegExp) return r;
63
+ throw new BotGuardError("bot-guard/bad-pattern",
64
+ where + " must be a RegExp instance; got " + (typeof r) +
65
+ " (compile the pattern at the call site so the source is visible " +
66
+ "in operator code)");
67
+ }
68
+
52
69
  // Bot-guard's "trust the proxy header" semantics for actor.ip — the
53
70
  // audit event records the apparent source even when behind a CDN, but
54
71
  // only when the operator opts in to trustProxy. Without the opt, we
@@ -71,8 +88,12 @@ function create(opts) {
71
88
  var _xffIp = _xffIpFor(trustProxy);
72
89
  var mode = opts.mode || "block";
73
90
  var onlyForHtml = opts.onlyForHtml !== false;
74
- var allowedAgents = (opts.allowedAgents || []).map(function (r) { return r instanceof RegExp ? r : new RegExp(r); });
75
- var blockedAgents = DEFAULT_BLOCKED_AGENTS.concat((opts.blockedAgents || []).map(function (r) { return r instanceof RegExp ? r : new RegExp(r); }));
91
+ var allowedAgents = (opts.allowedAgents || []).map(function (r, i) {
92
+ return _coerceAgentPattern(r, "middleware.botGuard: allowedAgents[" + i + "]");
93
+ });
94
+ var blockedAgents = DEFAULT_BLOCKED_AGENTS.concat((opts.blockedAgents || []).map(function (r, i) {
95
+ return _coerceAgentPattern(r, "middleware.botGuard: blockedAgents[" + i + "]");
96
+ }));
76
97
  var skipPaths = opts.skipPaths || [];
77
98
  var statusOnBlock = opts.statusOnBlock || 403;
78
99
  var bodyOnBlock = opts.bodyOnBlock !== undefined ? opts.bodyOnBlock : "Forbidden";
@@ -127,7 +148,7 @@ function create(opts) {
127
148
  metadata: { method: req.method, path: req.pathname || req.url, requestId: req.requestId },
128
149
  requestId: req.requestId,
129
150
  });
130
- } catch (_e) {}
151
+ } catch (_e) { /* audit best-effort */ }
131
152
  return next();
132
153
  }
133
154
 
@@ -140,7 +161,7 @@ function create(opts) {
140
161
  metadata: { method: req.method, path: req.pathname || req.url, requestId: req.requestId },
141
162
  requestId: req.requestId,
142
163
  });
143
- } catch (_e) {}
164
+ } catch (_e) { /* audit best-effort */ }
144
165
 
145
166
  if (res.writableEnded) return;
146
167
  if (typeof res.writeHead === "function") {
@@ -89,6 +89,8 @@
89
89
  */
90
90
 
91
91
  var zlib = require("node:zlib");
92
+ var C = require("../constants");
93
+ var nb = require("../numeric-bounds");
92
94
  var requestHelpers = require("../request-helpers");
93
95
  var validateOpts = require("../validate-opts");
94
96
  var { defineClass } = require("../framework-error");
@@ -96,7 +98,7 @@ var { defineClass } = require("../framework-error");
96
98
  var CompressionError = defineClass("CompressionError", { alwaysPermanent: true });
97
99
 
98
100
  var DEFAULT_OPTS = Object.freeze({
99
- threshold: 1024,
101
+ threshold: C.BYTES.kib(1),
100
102
  encodings: ["br", "gzip"],
101
103
  contentTypes: [
102
104
  "text/*",
@@ -116,8 +118,13 @@ var DEFAULT_OPTS = Object.freeze({
116
118
  // the encodings default.
117
119
  var SUPPORTED_ENCODINGS = new Set(["br", "gzip"]);
118
120
 
121
+ var HTTP_STATUS = requestHelpers.HTTP_STATUS;
122
+ // 205 Reset Content is rarely-used — local hex constant rather than
123
+ // expanding the framework's HTTP_STATUS table for a single call site.
124
+ var HTTP_RESET_CONTENT = 0xCD;
125
+
119
126
  // Status codes that NEVER carry a body — RFC 7230 §3.3.3.
120
- var NO_BODY_STATUS = new Set([204, 205, 304]);
127
+ var NO_BODY_STATUS = new Set([HTTP_STATUS.NO_CONTENT, HTTP_RESET_CONTENT, HTTP_STATUS.NOT_MODIFIED]);
121
128
 
122
129
  // Parse Accept-Encoding into [{ encoding, q }] sorted by q descending.
123
130
  // Built on the shared `requestHelpers.parseQualityList` (RFC 9110
@@ -224,8 +231,16 @@ function create(opts) {
224
231
  "threshold", "encodings", "contentTypes",
225
232
  "gzipLevel", "brotliQuality", "filter",
226
233
  ], "middleware.compression");
227
- var threshold = typeof opts.threshold === "number" && opts.threshold >= 0
228
- ? opts.threshold : DEFAULT_OPTS.threshold;
234
+ var threshold;
235
+ if (opts.threshold === undefined) {
236
+ threshold = DEFAULT_OPTS.threshold;
237
+ } else if (nb.isNonNegativeFiniteInt(opts.threshold)) {
238
+ threshold = opts.threshold;
239
+ } else {
240
+ throw new CompressionError("compression/bad-opt",
241
+ "middleware.compression: threshold must be a non-negative finite integer; got " +
242
+ nb.shape(opts.threshold));
243
+ }
229
244
  var encodings = Array.isArray(opts.encodings) && opts.encodings.length > 0
230
245
  ? opts.encodings.slice() : DEFAULT_OPTS.encodings.slice();
231
246
  var contentTypes = Array.isArray(opts.contentTypes) && opts.contentTypes.length > 0
@@ -245,7 +260,7 @@ function create(opts) {
245
260
  return function compression(req, res, next) {
246
261
  if (filter) {
247
262
  try { if (!filter(req, res)) return next(); }
248
- catch (_e) { return next(); }
263
+ catch (_e) { /* operator filter threw — fall through and skip compression */ return next(); }
249
264
  }
250
265
 
251
266
  // Negotiate encoding up-front from Accept-Encoding. If the client
@@ -313,7 +328,7 @@ function create(opts) {
313
328
  // On compressor error the response is already half-written;
314
329
  // there's not much we can do beyond closing the underlying
315
330
  // socket. Best-effort end the response.
316
- try { originalEnd.call(res); } catch (_e) {}
331
+ try { originalEnd.call(res); } catch (_e) { /* response already ended */ }
317
332
  });
318
333
  // Forward drain. Without this, source streams piped into our
319
334
  // wrapped res (e.g. fs.createReadStream from staticServe) stall:
@@ -353,12 +368,12 @@ function create(opts) {
353
368
  headersObj["Vary"] = _appendVary(existingVary, "Accept-Encoding");
354
369
  } else {
355
370
  if (originalRemoveHdr) {
356
- try { originalRemoveHdr("Content-Length"); } catch (_e) {}
371
+ try { originalRemoveHdr("Content-Length"); } catch (_e) { /* header may not be set */ }
357
372
  }
358
373
  if (originalSetHeader) {
359
- try { originalSetHeader("Content-Encoding", encoding); } catch (_e) {}
374
+ try { originalSetHeader("Content-Encoding", encoding); } catch (_e) { /* headers already sent */ }
360
375
  var existing = originalGetHeader && originalGetHeader("Vary");
361
- try { originalSetHeader("Vary", _appendVary(existing, "Accept-Encoding")); } catch (_e) {}
376
+ try { originalSetHeader("Vary", _appendVary(existing, "Accept-Encoding")); } catch (_e) { /* headers already sent */ }
362
377
  }
363
378
  }
364
379
  }
@@ -383,7 +398,7 @@ function create(opts) {
383
398
  // res.writeHead(). Use the implicit Content-Type from setHeader
384
399
  // (or absence) to decide. Default Node behavior triggers
385
400
  // writeHead with statusCode=200 here; we mirror that.
386
- _decide(200, null);
401
+ _decide(HTTP_STATUS.OK, null);
387
402
  if (compress) _applyCompressedHeaders(null);
388
403
  }
389
404
  if (!compress) return originalWrite.call(res, chunk, encArg, cbArg);
@@ -403,7 +418,7 @@ function create(opts) {
403
418
 
404
419
  res.end = function (chunk, encArg, cbArg) {
405
420
  if (!decided) {
406
- _decide(200, null);
421
+ _decide(HTTP_STATUS.OK, null);
407
422
  if (compress) _applyCompressedHeaders(null);
408
423
  }
409
424
  if (!compress) return originalEnd.call(res, chunk, encArg, cbArg);
@@ -49,6 +49,7 @@
49
49
  * opts.origins entries must be strings or RegExp. Bad config surfaces at
50
50
  * create() not at first cross-origin request.
51
51
  */
52
+ var C = require("../constants");
52
53
  var lazyRequire = require("../lazy-require");
53
54
  var audit = lazyRequire(function () { return require("../audit"); });
54
55
  var requestHelpers = require("../request-helpers");
@@ -221,7 +222,7 @@ function create(opts) {
221
222
  var allowedHeadersSet = headersList.map(function (h) { return String(h).trim().toLowerCase(); });
222
223
  var exposeHeaders = (opts.exposeHeaders || ["X-Request-Id"]).join(", ");
223
224
  var credentials = !!opts.credentials;
224
- var maxAge = String(opts.maxAgeSeconds || 600);
225
+ var maxAge = String(opts.maxAgeSeconds || (C.TIME.minutes(10) / C.TIME.seconds(1)));
225
226
  var refuseUnknown = opts.refuseUnknown !== false;
226
227
  // strictNullOrigin defaults true: refuse Origin: null even with
227
228
  // Sec-Fetch-Site: same-origin (non-browser callers can forge that
@@ -250,9 +251,9 @@ function create(opts) {
250
251
  metadata: { origin: origin, method: req.method, path: req.pathname || req.url, requestId: req.requestId },
251
252
  requestId: req.requestId,
252
253
  });
253
- } catch (_e) {}
254
+ } catch (_e) { /* audit best-effort */ }
254
255
  if (typeof res.writeHead === "function") {
255
- res.writeHead(403, { "Content-Type": "text/plain" });
256
+ res.writeHead(requestHelpers.HTTP_STATUS.FORBIDDEN, { "Content-Type": "text/plain" });
256
257
  res.end("CORS: origin not allowed");
257
258
  return;
258
259
  }
@@ -283,7 +284,7 @@ function create(opts) {
283
284
  for (var ah = 0; ah < asked.length; ah++) {
284
285
  if (allowedHeadersSet.indexOf(asked[ah]) === -1) {
285
286
  if (typeof res.writeHead === "function") {
286
- res.writeHead(403, { "Content-Type": "text/plain" });
287
+ res.writeHead(requestHelpers.HTTP_STATUS.FORBIDDEN, { "Content-Type": "text/plain" });
287
288
  res.end("CORS: requested header '" + asked[ah] + "' not in allow-list");
288
289
  }
289
290
  return;
@@ -297,7 +298,7 @@ function create(opts) {
297
298
  res.setHeader("Access-Control-Max-Age", maxAge);
298
299
  }
299
300
  if (typeof res.writeHead === "function") {
300
- res.writeHead(204);
301
+ res.writeHead(requestHelpers.HTTP_STATUS.NO_CONTENT);
301
302
  res.end();
302
303
  }
303
304
  return;
@@ -112,20 +112,22 @@
112
112
  * pass `always: true`.
113
113
  */
114
114
 
115
+ var C = require("../constants");
115
116
  var crypto = require("../crypto");
117
+ var numericBounds = require("../numeric-bounds");
116
118
  var validateOpts = require("../validate-opts");
117
119
  var { defineClass } = require("../framework-error");
118
120
 
119
121
  var CspNonceError = defineClass("CspNonceError", { alwaysPermanent: true });
120
122
 
121
123
  var DEFAULT_DIRECTIVES = Object.freeze(["script-src", "style-src"]);
122
- var DEFAULT_NONCE_BYTES = 16;
123
- var MIN_NONCE_BYTES = 16;
124
+ var DEFAULT_NONCE_BYTES = C.BYTES.bytes(16);
125
+ var MIN_NONCE_BYTES = C.BYTES.bytes(16);
124
126
  var PLACEHOLDER_PREFIX = "__BLAMEJS_CSP_NONCE_";
125
127
  var PLACEHOLDER_SUFFIX = "__";
126
128
  // 16 bytes / 128 bits — matches the default nonce strength so the
127
129
  // placeholder is no weaker than the value it stands in for.
128
- var PLACEHOLDER_RAND_BYTES = 16;
130
+ var PLACEHOLDER_RAND_BYTES = C.BYTES.bytes(16);
129
131
 
130
132
  // Build the substitute helper for a specific placeholder + property.
131
133
  // Each cspNonce instance has its own placeholder (random by default,
@@ -237,7 +239,18 @@ function create(opts) {
237
239
  }
238
240
  directives[i] = directives[i].toLowerCase();
239
241
  }
240
- var nonceBytes = typeof opts.nonceBytes === "number" ? opts.nonceBytes : DEFAULT_NONCE_BYTES;
242
+ var nonceBytes = opts.nonceBytes !== undefined ? opts.nonceBytes : DEFAULT_NONCE_BYTES;
243
+ // Pre-fix the typeof-only check accepted Infinity / NaN — both
244
+ // bypassed the `< MIN_NONCE_BYTES` guard (NaN < N is always false,
245
+ // Infinity < N is always false), then crashed per-request when
246
+ // `crypto.generateBytes(Infinity)` hit ERR_OUT_OF_RANGE. Route through
247
+ // shared numeric-bounds (positive finite int) before the lower-bound
248
+ // check so the typo / coercion is caught at create() time.
249
+ if (!numericBounds.isPositiveFiniteInt(nonceBytes)) {
250
+ throw new CspNonceError("csp-nonce/bad-nonce-bytes",
251
+ "nonceBytes must be a positive finite integer; got " +
252
+ numericBounds.shape(nonceBytes));
253
+ }
241
254
  if (nonceBytes < MIN_NONCE_BYTES) {
242
255
  throw new CspNonceError("csp-nonce/bad-nonce-bytes",
243
256
  "nonceBytes must be >= " + MIN_NONCE_BYTES + " (got " + nonceBytes + "). " +
@@ -158,7 +158,7 @@ function _appendSetCookie(res, value) {
158
158
  function _writeReject(res, message) {
159
159
  if (typeof res.writeHead === "function") {
160
160
  var body = JSON.stringify({ error: message });
161
- res.writeHead(403, {
161
+ res.writeHead(requestHelpers.HTTP_STATUS.FORBIDDEN, {
162
162
  "Content-Type": "application/json; charset=utf-8",
163
163
  "Content-Length": Buffer.byteLength(body),
164
164
  });
@@ -90,10 +90,7 @@ var ALLOWED_OPTS = [
90
90
  "audit", "auditFailures", "auditSuccess",
91
91
  ];
92
92
 
93
- function _emitEvent(name, value, labels) {
94
- try { observability().event(name, value, labels || {}); }
95
- catch (_e) { /* hot-path observability sink — drop silent by design */ }
96
- }
93
+ function _emitEvent(n, v, l) { observability().safeEvent(n, v, l || {}); }
97
94
 
98
95
  function _validateRoleIdentifier(role, where) {
99
96
  try {
@@ -114,14 +111,8 @@ function create(opts) {
114
111
  opts = opts || {};
115
112
  validateOpts(opts, ALLOWED_OPTS, "middleware.dbRoleFor");
116
113
 
117
- if (opts.resolve !== undefined && typeof opts.resolve !== "function") {
118
- throw _err("db-role-for/bad-opt",
119
- "middleware.dbRoleFor: resolve must be a function");
120
- }
121
- if (opts.responder !== undefined && typeof opts.responder !== "function") {
122
- throw _err("db-role-for/bad-opt",
123
- "middleware.dbRoleFor: responder must be a function");
124
- }
114
+ validateOpts.optionalFunction(opts.resolve, "middleware.dbRoleFor: resolve", DbRoleForError, "db-role-for/bad-opt");
115
+ validateOpts.optionalFunction(opts.responder, "middleware.dbRoleFor: responder", DbRoleForError, "db-role-for/bad-opt");
125
116
  if (opts.permissions !== undefined && opts.permissions !== null) {
126
117
  if (typeof opts.permissions !== "object" ||
127
118
  typeof opts.permissions.dbRoleFor !== "function") {
@@ -137,10 +128,7 @@ function create(opts) {
137
128
  }
138
129
  _validateRoleIdentifier(opts.defaultRole, "middleware.dbRoleFor: defaultRole");
139
130
  }
140
- if (opts.requireRole !== undefined && typeof opts.requireRole !== "boolean") {
141
- throw _err("db-role-for/bad-opt",
142
- "middleware.dbRoleFor: requireRole must be a boolean");
143
- }
131
+ validateOpts.optionalBoolean(opts.requireRole, "middleware.dbRoleFor: requireRole", DbRoleForError, "db-role-for/bad-opt");
144
132
  if (opts.missingRoleStatus !== undefined) {
145
133
  if (typeof opts.missingRoleStatus !== "number" ||
146
134
  !isFinite(opts.missingRoleStatus) ||
@@ -149,20 +137,9 @@ function create(opts) {
149
137
  "middleware.dbRoleFor: missingRoleStatus must be an HTTP status code (100-599)");
150
138
  }
151
139
  }
152
- if (opts.audit !== undefined && opts.audit !== null) {
153
- if (typeof opts.audit !== "object" || typeof opts.audit.safeEmit !== "function") {
154
- throw _err("db-role-for/bad-opt",
155
- "middleware.dbRoleFor: audit must be a b.audit-shaped object (safeEmit fn)");
156
- }
157
- }
158
- if (opts.auditFailures !== undefined && typeof opts.auditFailures !== "boolean") {
159
- throw _err("db-role-for/bad-opt",
160
- "middleware.dbRoleFor: auditFailures must be a boolean");
161
- }
162
- if (opts.auditSuccess !== undefined && typeof opts.auditSuccess !== "boolean") {
163
- throw _err("db-role-for/bad-opt",
164
- "middleware.dbRoleFor: auditSuccess must be a boolean");
165
- }
140
+ validateOpts.auditShape(opts.audit, "middleware.dbRoleFor", DbRoleForError, "db-role-for/bad-opt");
141
+ validateOpts.optionalBoolean(opts.auditFailures, "middleware.dbRoleFor: auditFailures", DbRoleForError, "db-role-for/bad-opt");
142
+ validateOpts.optionalBoolean(opts.auditSuccess, "middleware.dbRoleFor: auditSuccess", DbRoleForError, "db-role-for/bad-opt");
166
143
 
167
144
  var resolveFn = opts.resolve || null;
168
145
  var perms = opts.permissions || null;