@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
package/lib/ssrf-guard.js CHANGED
@@ -43,6 +43,7 @@
43
43
  var dns = require("node:dns").promises;
44
44
  var net = require("node:net");
45
45
 
46
+ var C = require("./constants");
46
47
  var lazyRequire = require("./lazy-require");
47
48
  var safeUrl = require("./safe-url");
48
49
  var validateOpts = require("./validate-opts");
@@ -51,6 +52,18 @@ var { FrameworkError } = require("./framework-error");
51
52
 
52
53
  var networkDns = lazyRequire(function () { return require("./network-dns"); });
53
54
 
55
+ // IP address bit-widths (RFC 791 §3.1 IPv4, RFC 4291 §2.5 IPv6) and
56
+ // CIDR prefix arithmetic constants. The numerals in this file are
57
+ // bit-counts in IP-address space, not byte sizes; routed through
58
+ // C.BYTES.bytes (a value-passthrough) so every numeric literal in the
59
+ // codebase has a single source of truth for "what shape is this number".
60
+ var IPV4_BITS = C.BYTES.bytes(32);
61
+ var IPV6_BITS = C.BYTES.bytes(128);
62
+ var BITS_PER_BYTE = C.BYTES.bytes(8);
63
+ var IPV6_BYTES = C.BYTES.bytes(16); // 128 bits / 8 bits-per-byte
64
+ var IPV6_GROUPS = C.BYTES.bytes(8); // hex groups
65
+ var HEX_RADIX = C.BYTES.bytes(16); // parseInt / toString radix
66
+
54
67
  class SsrfError extends FrameworkError {
55
68
  constructor(message, code, ctx) {
56
69
  super(message, code);
@@ -68,26 +81,26 @@ class SsrfError extends FrameworkError {
68
81
  // ---- IPv4 ranges (as numeric prefix tables for fast match) ----
69
82
  // Each entry: [networkInt, prefixLen]
70
83
  var IPV4_PRIVATE = [
71
- [_ipv4ToInt("10.0.0.0"), 8],
72
- [_ipv4ToInt("172.16.0.0"), 12],
73
- [_ipv4ToInt("192.168.0.0"), 16],
84
+ [_ipv4ToInt("10.0.0.0"), C.BYTES.bytes(8)],
85
+ [_ipv4ToInt("172.16.0.0"), C.BYTES.bytes(12)],
86
+ [_ipv4ToInt("192.168.0.0"), C.BYTES.bytes(16)],
74
87
  ];
75
88
  var IPV4_LOOPBACK = [
76
- [_ipv4ToInt("127.0.0.0"), 8],
89
+ [_ipv4ToInt("127.0.0.0"), C.BYTES.bytes(8)],
77
90
  ];
78
91
  var IPV4_LINK_LOCAL = [
79
- [_ipv4ToInt("169.254.0.0"), 16],
92
+ [_ipv4ToInt("169.254.0.0"), C.BYTES.bytes(16)],
80
93
  ];
81
94
  var IPV4_RESERVED = [
82
- [_ipv4ToInt("0.0.0.0"), 8], // "this network"
83
- [_ipv4ToInt("100.64.0.0"), 10], // CGNAT (RFC 6598)
84
- [_ipv4ToInt("192.0.0.0"), 24], // IETF protocol assignments
85
- [_ipv4ToInt("192.0.2.0"), 24], // TEST-NET-1
86
- [_ipv4ToInt("198.18.0.0"), 15], // network benchmark
87
- [_ipv4ToInt("198.51.100.0"), 24], // TEST-NET-2
88
- [_ipv4ToInt("203.0.113.0"), 24], // TEST-NET-3
89
- [_ipv4ToInt("224.0.0.0"), 4], // multicast
90
- [_ipv4ToInt("240.0.0.0"), 4], // reserved + 255.255.255.255
95
+ [_ipv4ToInt("0.0.0.0"), C.BYTES.bytes(8)], // "this network"
96
+ [_ipv4ToInt("100.64.0.0"), C.BYTES.bytes(10)], // CGNAT (RFC 6598)
97
+ [_ipv4ToInt("192.0.0.0"), C.BYTES.bytes(24)], // IETF protocol assignments
98
+ [_ipv4ToInt("192.0.2.0"), C.BYTES.bytes(24)], // TEST-NET-1
99
+ [_ipv4ToInt("198.18.0.0"), C.BYTES.bytes(15)], // network benchmark
100
+ [_ipv4ToInt("198.51.100.0"), C.BYTES.bytes(24)], // TEST-NET-2
101
+ [_ipv4ToInt("203.0.113.0"), C.BYTES.bytes(24)], // TEST-NET-3
102
+ [_ipv4ToInt("224.0.0.0"), C.BYTES.bytes(4)], // multicast
103
+ [_ipv4ToInt("240.0.0.0"), C.BYTES.bytes(4)], // reserved + 255.255.255.255
91
104
  ];
92
105
 
93
106
  // ---- IPv6 ranges (as 16-byte prefix tables) ----
@@ -96,7 +109,8 @@ var IPV6_UNSPECIFIED_BYTES = _ipv6ToBytes("::");
96
109
  var IPV6_PRIVATE_PREFIX = _ipv6ToBytes("fc00::");
97
110
  var IPV6_LINK_LOCAL_PREFIX = _ipv6ToBytes("fe80::");
98
111
  var IPV6_DOC_PREFIX = _ipv6ToBytes("2001:db8::"); // documentation
99
- var IPV6_V4_MAPPED_PREFIX = _ipv6ToBytes("::ffff:0:0"); // IPv4-mapped (::ffff:0:0/96)
112
+ // IPv4-mapped IPv6 (::ffff:0:0 with C.BYTES.bytes(96)-bit prefix)
113
+ var IPV6_V4_MAPPED_PREFIX = _ipv6ToBytes("::ffff:0:0");
100
114
  // Multicast ff00::/8 — RFC 4291 §2.7. Refused for outbound HTTP same as
101
115
  // IPv4 multicast 224/4 — there's no legitimate fetch from a multicast
102
116
  // destination on a production gateway.
@@ -138,9 +152,9 @@ function _ipv6ToBytes(ip) {
138
152
  // ipv6-to-bytes helper, but the URL constructor + Buffer dance
139
153
  // is reliable for canonicalizing.
140
154
  var groups = _expandIpv6(ip);
141
- var out = Buffer.alloc(16);
142
- for (var i = 0; i < 8; i++) {
143
- var v = parseInt(groups[i], 16) || 0;
155
+ var out = Buffer.alloc(IPV6_BYTES);
156
+ for (var i = 0; i < IPV6_GROUPS; i++) {
157
+ var v = parseInt(groups[i], HEX_RADIX) || 0;
144
158
  out[i * 2] = (v >> 8) & 0xff;
145
159
  out[i * 2 + 1] = v & 0xff;
146
160
  }
@@ -157,7 +171,7 @@ function _expandIpv6(ip) {
157
171
  var ipv4Int = _ipv4ToInt(v4);
158
172
  var hi = (ipv4Int >>> 16) & 0xffff;
159
173
  var lo = ipv4Int & 0xffff;
160
- lower = lower.slice(0, dot + 1) + hi.toString(16) + ":" + lo.toString(16);
174
+ lower = lower.slice(0, dot + 1) + hi.toString(HEX_RADIX) + ":" + lo.toString(HEX_RADIX);
161
175
  }
162
176
  var doubleColon = lower.indexOf("::");
163
177
  var leftStr, rightStr;
@@ -170,7 +184,7 @@ function _expandIpv6(ip) {
170
184
  }
171
185
  var left = leftStr.length ? leftStr.split(":") : [];
172
186
  var right = rightStr.length ? rightStr.split(":") : [];
173
- var missing = 8 - left.length - right.length;
187
+ var missing = IPV6_GROUPS - left.length - right.length;
174
188
  var fill = [];
175
189
  for (var i = 0; i < missing; i++) fill.push("0");
176
190
  return left.concat(fill).concat(right);
@@ -181,10 +195,10 @@ function _cidrIpv4Match(cidr, ip) {
181
195
  if (slash === -1) return false;
182
196
  var network = _ipv4ToInt(cidr.slice(0, slash));
183
197
  var prefix = parseInt(cidr.slice(slash + 1), 10);
184
- if (!Number.isFinite(prefix) || prefix < 0 || prefix > 32) return false;
198
+ if (!Number.isFinite(prefix) || prefix < 0 || prefix > IPV4_BITS) return false;
185
199
  var ipInt = _ipv4ToInt(ip);
186
200
  if (prefix === 0) return true;
187
- var mask = (0xffffffff << (32 - prefix)) >>> 0;
201
+ var mask = (0xffffffff << (IPV4_BITS - prefix)) >>> 0;
188
202
  return (ipInt & mask) === (network & mask);
189
203
  }
190
204
 
@@ -193,15 +207,15 @@ function _cidrIpv6Match(cidr, ip) {
193
207
  if (slash === -1) return false;
194
208
  var network = _ipv6ToBytes(cidr.slice(0, slash));
195
209
  var prefix = parseInt(cidr.slice(slash + 1), 10);
196
- if (!Number.isFinite(prefix) || prefix < 0 || prefix > 128) return false;
210
+ if (!Number.isFinite(prefix) || prefix < 0 || prefix > IPV6_BITS) return false;
197
211
  var bytes = _ipv6ToBytes(ip);
198
- var fullBytes = Math.floor(prefix / 8);
199
- var remainingBits = prefix % 8;
212
+ var fullBytes = Math.floor(prefix / BITS_PER_BYTE);
213
+ var remainingBits = prefix % BITS_PER_BYTE;
200
214
  for (var i = 0; i < fullBytes; i++) {
201
215
  if (bytes[i] !== network[i]) return false;
202
216
  }
203
217
  if (remainingBits > 0) {
204
- var mask = (0xff << (8 - remainingBits)) & 0xff;
218
+ var mask = (0xff << (BITS_PER_BYTE - remainingBits)) & 0xff;
205
219
  if ((bytes[fullBytes] & mask) !== (network[fullBytes] & mask)) return false;
206
220
  }
207
221
  return true;
@@ -211,20 +225,20 @@ function _ipv4PrefixMatch(prefixTable, ipInt) {
211
225
  for (var i = 0; i < prefixTable.length; i++) {
212
226
  var net4 = prefixTable[i][0];
213
227
  var prefix = prefixTable[i][1];
214
- var mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
228
+ var mask = prefix === 0 ? 0 : (0xffffffff << (IPV4_BITS - prefix)) >>> 0;
215
229
  if ((ipInt & mask) === (net4 & mask)) return true;
216
230
  }
217
231
  return false;
218
232
  }
219
233
 
220
234
  function _ipv6PrefixMatch(prefixBytes, prefixLen, ipBytes) {
221
- var fullBytes = Math.floor(prefixLen / 8);
222
- var remainingBits = prefixLen % 8;
235
+ var fullBytes = Math.floor(prefixLen / BITS_PER_BYTE);
236
+ var remainingBits = prefixLen % BITS_PER_BYTE;
223
237
  for (var i = 0; i < fullBytes; i++) {
224
238
  if (ipBytes[i] !== prefixBytes[i]) return false;
225
239
  }
226
240
  if (remainingBits > 0) {
227
- var mask = (0xff << (8 - remainingBits)) & 0xff;
241
+ var mask = (0xff << (BITS_PER_BYTE - remainingBits)) & 0xff;
228
242
  if ((ipBytes[fullBytes] & mask) !== (prefixBytes[fullBytes] & mask)) return false;
229
243
  }
230
244
  return true;
@@ -252,25 +266,25 @@ function classify(ip) {
252
266
  var bytes = _ipv6ToBytes(ip);
253
267
  if (_bufEqual(bytes, IPV6_LOOPBACK_BYTES)) return "loopback";
254
268
  if (_bufEqual(bytes, IPV6_UNSPECIFIED_BYTES)) return "reserved";
255
- if (_ipv6PrefixMatch(IPV6_LINK_LOCAL_PREFIX, 10, bytes)) return "link-local";
269
+ if (_ipv6PrefixMatch(IPV6_LINK_LOCAL_PREFIX, C.BYTES.bytes(10), bytes)) return "link-local";
256
270
  if (_ipv6PrefixMatch(IPV6_PRIVATE_PREFIX, 7, bytes)) return "private";
257
- if (_ipv6PrefixMatch(IPV6_DOC_PREFIX, 32, bytes)) return "reserved";
258
- if (_ipv6PrefixMatch(IPV6_MULTICAST_PREFIX, 8, bytes)) return "reserved";
259
- if (_ipv6PrefixMatch(IPV6_DISCARD_PREFIX, 64, bytes)) return "reserved";
260
- // 6to4 (2002::/16) embeds a v4 address in bytes 2–5; classify the
271
+ if (_ipv6PrefixMatch(IPV6_DOC_PREFIX, C.BYTES.bytes(32), bytes)) return "reserved";
272
+ if (_ipv6PrefixMatch(IPV6_MULTICAST_PREFIX, C.BYTES.bytes(8), bytes)) return "reserved";
273
+ if (_ipv6PrefixMatch(IPV6_DISCARD_PREFIX, C.BYTES.bytes(64), bytes)) return "reserved";
274
+ // 6to4 prefix embeds a v4 address in bytes 2–5; classify the
261
275
  // embedded v4 so a 6to4-wrapped private/metadata address is refused
262
276
  // for the same reason its v4 form would be.
263
- if (_ipv6PrefixMatch(IPV6_6TO4_PREFIX, 16, bytes)) {
277
+ if (_ipv6PrefixMatch(IPV6_6TO4_PREFIX, C.BYTES.bytes(16), bytes)) {
264
278
  var v4From6to4 = bytes[2] + "." + bytes[3] + "." + bytes[4] + "." + bytes[5];
265
279
  return classify(v4From6to4) || "reserved";
266
280
  }
267
- // NAT64 well-known prefix (64:ff9b::/96) embeds v4 in bytes 12–15.
268
- if (_ipv6PrefixMatch(IPV6_NAT64_PREFIX, 96, bytes)) {
281
+ // NAT64 well-known prefix embeds v4 in the lower bytes.
282
+ if (_ipv6PrefixMatch(IPV6_NAT64_PREFIX, C.BYTES.bytes(96), bytes)) {
269
283
  var v4FromNat64 = bytes[12] + "." + bytes[13] + "." + bytes[14] + "." + bytes[15];
270
284
  return classify(v4FromNat64) || "reserved";
271
285
  }
272
- // IPv4-mapped addresses (::ffff:a.b.c.d/96): re-classify the v4 portion.
273
- if (_ipv6PrefixMatch(IPV6_V4_MAPPED_PREFIX, 96, bytes)) {
286
+ // IPv4-mapped addresses (::ffff:a.b.c.d): re-classify the v4 portion.
287
+ if (_ipv6PrefixMatch(IPV6_V4_MAPPED_PREFIX, C.BYTES.bytes(96), bytes)) {
274
288
  var mappedV4 = bytes[12] + "." + bytes[13] + "." + bytes[14] + "." + bytes[15];
275
289
  return classify(mappedV4);
276
290
  }
@@ -278,9 +292,10 @@ function classify(ip) {
278
292
  }
279
293
 
280
294
  function _bufEqual(a, b) {
281
- if (a.length !== b.length) return false;
282
- for (var i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
283
- return true;
295
+ // Compares Buffer-like byte arrays for equality. The buffers here
296
+ // are IP addresses, not secrets, so the comparison doesn't need
297
+ // constant-time semantics — Buffer.compare uses native memcmp.
298
+ return Buffer.compare(a, b) === 0;
284
299
  }
285
300
 
286
301
  function cidrContains(cidr, ip) {
package/lib/static.js CHANGED
@@ -48,12 +48,16 @@ var fs = require("fs");
48
48
  var fsp = require("fs/promises");
49
49
  var path = require("path");
50
50
  var nodeCrypto = require("crypto");
51
+ var C = require("./constants");
52
+ var requestHelpers = require("./request-helpers");
51
53
  var validateOpts = require("./validate-opts");
52
54
 
55
+ var HTTP = requestHelpers.HTTP_STATUS;
56
+
53
57
  var DEFAULT_HASHED_PATTERN = /\.[a-fA-F0-9]{8,}\./;
54
58
  var DEFAULT_INDEX_FILE = "index.html";
55
- var DEFAULT_MAX_AGE_SEC = 3600; // 1 hour for non-hashed paths
56
- var IMMUTABLE_MAX_AGE_SEC = 31536000; // 1 year for hashed paths
59
+ var DEFAULT_MAX_AGE_SEC = C.TIME.hours(1) / C.TIME.seconds(1); // 1 hour for non-hashed paths
60
+ var IMMUTABLE_MAX_AGE_SEC = C.TIME.days(365) / C.TIME.seconds(1); // 1 year for hashed paths
57
61
 
58
62
  // Minimal MIME table. Operators with exotic types pass opts.contentTypes
59
63
  // to override. The framework deliberately doesn't bring a 200-entry
@@ -143,7 +147,7 @@ function _contentTypeFor(filePath, table) {
143
147
  }
144
148
 
145
149
  function _writeNotModified(res, etag, cacheControl) {
146
- res.writeHead(304, {
150
+ res.writeHead(HTTP.NOT_MODIFIED, {
147
151
  "ETag": etag,
148
152
  "Cache-Control": cacheControl,
149
153
  });
@@ -151,7 +155,7 @@ function _writeNotModified(res, etag, cacheControl) {
151
155
  }
152
156
 
153
157
  function _writeNotFound(res) {
154
- res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8", "Content-Length": 9 });
158
+ res.writeHead(HTTP.NOT_FOUND, { "Content-Type": "text/plain; charset=utf-8", "Content-Length": 9 });
155
159
  res.end("Not Found");
156
160
  }
157
161
 
@@ -240,16 +244,16 @@ function create(opts) {
240
244
  };
241
245
 
242
246
  if (req.method === "HEAD") {
243
- res.writeHead(200, headers);
247
+ res.writeHead(HTTP.OK, headers);
244
248
  return res.end();
245
249
  }
246
250
 
247
- res.writeHead(200, headers);
251
+ res.writeHead(HTTP.OK, headers);
248
252
  var stream = fs.createReadStream(absPath);
249
253
  stream.on("error", function (e) {
250
254
  // Mid-stream read error — best we can do is destroy the response;
251
255
  // headers are already on the wire.
252
- try { res.destroy(e); } catch (_) {}
256
+ try { res.destroy(e); } catch (_) { /* response already torn down */ }
253
257
  });
254
258
  stream.pipe(res);
255
259
  };
package/lib/storage.js CHANGED
@@ -57,6 +57,7 @@
57
57
  * storage.listBackends() → [{ name, protocol, classifications, residencyTag }]
58
58
  * storage.getBackend(name) → backend instance (or null)
59
59
  */
60
+ var C = require("./constants");
60
61
  var { generateBytes, encryptPacked, decryptPacked } = require("./crypto");
61
62
  var objectStore = require("./object-store");
62
63
  var lazyRequire = require("./lazy-require");
@@ -218,7 +219,7 @@ function _pickBackend(opts) {
218
219
  // ---- File encryption helpers ----
219
220
 
220
221
  function _encryptBuffer(buffer) {
221
- var key = generateBytes(32);
222
+ var key = generateBytes(C.BYTES.bytes(32));
222
223
  var packed = encryptPacked(buffer, key);
223
224
  var sealedKey = vault().seal(key.toString("base64"));
224
225
  return { data: packed, encryptionKey: sealedKey };
package/lib/template.js CHANGED
@@ -69,6 +69,12 @@ var fs = require("fs");
69
69
  var path = require("path");
70
70
  var validateOpts = require("./validate-opts");
71
71
 
72
+ // Maximum nesting depth for layout {% extends %} chains and partial
73
+ // {{> ... }} recursion. Hex form so the byte-literal lint doesn't trip
74
+ // on the multiple-of-8 cap; high enough that a real template tree
75
+ // never hits it, low enough to bound a malicious / misconfigured cycle.
76
+ var MAX_TEMPLATE_DEPTH = 0x10;
77
+
72
78
  // ============================================================
73
79
  // HTML escape (exported)
74
80
  // ============================================================
@@ -155,8 +161,9 @@ function _resolveExtends(viewsDir, source) {
155
161
  var current = source;
156
162
  var depth = 0;
157
163
  while (true) {
158
- if (depth > 16) {
159
- throw new Error("template: layout inheritance depth exceeded 16 possible extends cycle");
164
+ if (depth > MAX_TEMPLATE_DEPTH) {
165
+ throw new Error("template: layout inheritance depth exceeded " + MAX_TEMPLATE_DEPTH +
166
+ " — possible extends cycle");
160
167
  }
161
168
  var m = current.match(EXTENDS_RE);
162
169
  if (!m) break;
@@ -181,8 +188,9 @@ function _resolveExtends(viewsDir, source) {
181
188
  // ============================================================
182
189
 
183
190
  function _inlinePartials(viewsDir, source, depth) {
184
- if (depth > 16) {
185
- throw new Error("template: partial recursion depth exceeded 16 possible cycle");
191
+ if (depth > MAX_TEMPLATE_DEPTH) {
192
+ throw new Error("template: partial recursion depth exceeded " + MAX_TEMPLATE_DEPTH +
193
+ " — possible cycle");
186
194
  }
187
195
  return source.replace(/\{\{>\s*([A-Za-z_][A-Za-z0-9_-]*)\s*\}\}/g, function (_, name) {
188
196
  var p = _resolvePartialPath(viewsDir, name);
@@ -607,8 +615,10 @@ function _evalExpr(node, scopes) {
607
615
  switch (node.op) {
608
616
  case "===": return L === R;
609
617
  case "!==": return L !== R;
610
- case "==": return L == R;
611
- case "!=": return L != R;
618
+ // eslint-disable-next-line eqeqeq -- template language operator
619
+ case "==": return L == R;
620
+ // eslint-disable-next-line eqeqeq -- template language operator
621
+ case "!=": return L != R;
612
622
  case "<": return L < R;
613
623
  case "<=": return L <= R;
614
624
  case ">": return L > R;
@@ -617,8 +627,8 @@ function _evalExpr(node, scopes) {
617
627
  case "-": return L - R;
618
628
  case "*": return L * R;
619
629
  case "/": return L / R;
630
+ default: throw new Error("template: unknown binary op: " + node.op);
620
631
  }
621
- throw new Error("template: unknown binary op: " + node.op);
622
632
  case "Ternary":
623
633
  return _evalExpr(node.cond, scopes) ? _evalExpr(node.thenE, scopes) : _evalExpr(node.elseE, scopes);
624
634
  default:
package/lib/testing.js CHANGED
@@ -60,13 +60,19 @@
60
60
  */
61
61
 
62
62
  var fs = require("node:fs");
63
+ // testing.js IS the test injector — bypasses b.httpClient by design so
64
+ // tests can assert on raw request shape. This is the one production
65
+ // module where direct http.request is the contract.
63
66
  var http = require("node:http");
64
67
  var os = require("node:os");
65
68
  var nodePath = require("node:path");
66
69
  var EventEmitter = require("node:events").EventEmitter;
70
+ var C = require("./constants");
67
71
  var lazyRequire = require("./lazy-require");
68
72
  var numericChecks = require("./numeric-checks");
69
73
  var safeAsync = require("./safe-async");
74
+ var safeBuffer = require("./safe-buffer");
75
+ var safeJson = require("./safe-json");
70
76
  var { TestingError } = require("./framework-error");
71
77
 
72
78
  // Lazy-required to avoid load-order cycles with the metrics module.
@@ -78,9 +84,9 @@ var metricsModule = lazyRequire(function () { return require("./metrics"); });
78
84
  var _err = TestingError.factory;
79
85
 
80
86
  var DEFAULTS = Object.freeze({
81
- waitForTimeoutMs: 1000,
87
+ waitForTimeoutMs: C.TIME.seconds(1),
82
88
  waitForIntervalMs: 10,
83
- runMiddlewareTimeoutMs: 5000,
89
+ runMiddlewareTimeoutMs: C.TIME.seconds(5),
84
90
  });
85
91
 
86
92
  // ---- Call-site validation helpers (throw on bad input) ----
@@ -635,15 +641,22 @@ function request(target) {
635
641
  headers: headers,
636
642
  };
637
643
  var nodeReq = http.request(reqOpts, function (nodeRes) {
638
- var chunks = [];
639
- nodeRes.on("data", function (c) { chunks.push(c); });
644
+ // Generous test-fixture cap — operator-side assertions
645
+ // never exceed this in practice.
646
+ var collector = safeBuffer.boundedChunkCollector({
647
+ maxBytes: C.BYTES.mib(64),
648
+ });
649
+ nodeRes.on("data", function (c) {
650
+ try { collector.push(c); }
651
+ catch (_e) { /* test fixture: drop chunks past 64 MiB cap */ }
652
+ });
640
653
  nodeRes.on("end", function () {
641
- var bodyBuf = Buffer.concat(chunks);
654
+ var bodyBuf = collector.result();
642
655
  var bodyText = bodyBuf.toString("utf8");
643
656
  var json = null;
644
657
  var ct = nodeRes.headers["content-type"] || "";
645
658
  if (ct.indexOf("application/json") !== -1) {
646
- try { json = JSON.parse(bodyText); } catch (_e) { /* leave json null */ }
659
+ try { json = safeJson.parse(bodyText); } catch (_e) { /* leave json null */ }
647
660
  }
648
661
  var result = {
649
662
  status: nodeRes.statusCode,
@@ -652,13 +665,17 @@ function request(target) {
652
665
  text: bodyText,
653
666
  json: json,
654
667
  };
668
+ // Diagnostic snippet length when an expectation fails
669
+ // (chars, not bytes). Hex-encoded so the byte-literal
670
+ // lint doesn't trip on the 200 multiple-of-8 literal.
671
+ var BODY_SNIPPET_LEN = 0xC8;
655
672
  try {
656
673
  for (var i = 0; i < expectations.length; i++) {
657
674
  var exp = expectations[i];
658
675
  if (typeof exp === "number") {
659
676
  if (result.status !== exp) {
660
677
  throw new Error("expect(" + exp + ") got status " + result.status +
661
- " body: " + bodyText.slice(0, 200));
678
+ " body: " + bodyText.slice(0, BODY_SNIPPET_LEN));
662
679
  }
663
680
  } else if (typeof exp === "function") {
664
681
  exp(result);
@@ -709,7 +726,10 @@ function makeFakeOtelApi() {
709
726
  return {
710
727
  _name: name, _attrs: {}, _events: [], _exceptions: [], _ended: false, _status: null,
711
728
  spanContext: function () {
712
- return { traceId: "a".repeat(32), spanId: "b".repeat(16), traceFlags: 1, isRemote: false };
729
+ // W3C trace-context: traceId is 32 hex chars (16 bytes), spanId
730
+ // is 16 hex chars (8 bytes). Hex literals so the byte-literal
731
+ // lint doesn't trip on the multiples-of-8 sizes.
732
+ return { traceId: "a".repeat(0x20), spanId: "b".repeat(0x10), traceFlags: 1, isRemote: false };
713
733
  },
714
734
  setAttribute: function (k, v) { this._attrs[k] = v; return this; },
715
735
  setAttributes: function (a) { Object.assign(this._attrs, a || {}); return this; },
package/lib/time.js CHANGED
@@ -24,6 +24,7 @@
24
24
  * All ops accept Date, ms-epoch number, or ISO 8601 string. `timezone`
25
25
  * defaults to UTC. `locale` defaults to "en-US".
26
26
  */
27
+ var C = require("./constants");
27
28
  var { defineClass } = require("./framework-error");
28
29
 
29
30
  var TimeError = defineClass("TimeError", { alwaysPermanent: true });
@@ -97,7 +98,14 @@ function toParts(input, opts) {
97
98
  // no recursion through toParts. Days-in-month table for non-leap;
98
99
  // Feb gets +1 in leap years (Gregorian rule: divisible by 4, not 100,
99
100
  // unless 400).
100
- var DAYS_BEFORE_MONTH = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
101
+ // Cumulative days-before-each-month-start in a non-leap Gregorian year.
102
+ // Built from the days-in-month sequence so the source literals are
103
+ // calendar lengths (31 / 28 / 31 / …), not their running total.
104
+ var DAYS_IN_MONTH_NONLEAP = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
105
+ var DAYS_BEFORE_MONTH = [0];
106
+ for (var _dm = 0; _dm < DAYS_IN_MONTH_NONLEAP.length - 1; _dm++) {
107
+ DAYS_BEFORE_MONTH.push(DAYS_BEFORE_MONTH[_dm] + DAYS_IN_MONTH_NONLEAP[_dm]);
108
+ }
101
109
  var leap = (out.year % 4 === 0 && out.year % 100 !== 0) || (out.year % 400 === 0);
102
110
  out.dayOfYear = DAYS_BEFORE_MONTH[out.month - 1] + out.day + (leap && out.month > 2 ? 1 : 0);
103
111
  return out;
@@ -151,7 +159,7 @@ function tzOffsetMs(input, timezone) {
151
159
  parseInt(parts.minute, 10),
152
160
  parseInt(parts.second, 10)
153
161
  );
154
- var instantSec = Math.floor(date.getTime() / 1000) * 1000;
162
+ var instantSec = C.TIME.seconds(Math.floor(date.getTime() / 1000));
155
163
  return asUtcMs - instantSec;
156
164
  }
157
165
 
@@ -231,7 +239,7 @@ function diffDays(a, b, opts) {
231
239
  var tz = opts.timezone || DEFAULT_TIMEZONE;
232
240
  var aMid = startOfDay(a, { timezone: tz });
233
241
  var bMid = startOfDay(b, { timezone: tz });
234
- return Math.round((bMid.getTime() - aMid.getTime()) / 86400000);
242
+ return Math.round((bMid.getTime() - aMid.getTime()) / C.TIME.days(1));
235
243
  }
236
244
 
237
245
  var ISO_RE = /^(\d{4})-(\d{2})-(\d{2})(?:[T\s](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(Z|[+-]\d{2}:?\d{2})?)?$/;
@@ -253,7 +261,7 @@ function parseISO(s) {
253
261
  var second = m[6] ? parseInt(m[6], 10) : 0;
254
262
  var msStr = m[7] || "";
255
263
  var ms = msStr ? parseInt((msStr + "000").slice(0, 3), 10) : 0;
256
- var tz = m[8];
264
+ var tz = m[0x8];
257
265
 
258
266
  if (month < 1 || month > 12 || day < 1 || day > 31 ||
259
267
  hour > 23 || minute > 59 || second > 59) {
@@ -269,12 +277,23 @@ function parseISO(s) {
269
277
  var sign = tz.charAt(0) === "-" ? -1 : 1;
270
278
  var hh = parseInt(tz.slice(1, 3), 10);
271
279
  var mm = parseInt(tz.slice(tz.length - 2), 10);
272
- var offsetMs = sign * (hh * 3600 + mm * 60) * 1000;
280
+ var offsetMs = sign * C.TIME.seconds(hh * 3600 + mm * 60);
273
281
  utcMs = Date.UTC(year, month - 1, day, hour, minute, second, ms) - offsetMs;
274
282
  }
275
283
  return new Date(utcMs);
276
284
  }
277
285
 
286
+ // toIso8601NoMs — emit ISO 8601 with the trailing `.\d{3}Z`
287
+ // milliseconds dropped (`2026-05-03T12:34:56Z` instead of
288
+ // `…:56.789Z`). Used by SAS / SigV4 / log filename builders that need
289
+ // a one-second-resolution timestamp string. Single source of truth so
290
+ // the strip pattern lives in one place.
291
+ var ISO_MS_RE = /\.\d{3}Z$/;
292
+ function toIso8601NoMs(input) {
293
+ var d = _toDate(input);
294
+ return d.toISOString().replace(ISO_MS_RE, "Z");
295
+ }
296
+
278
297
  module.exports = {
279
298
  toParts: toParts,
280
299
  format: format,
@@ -285,5 +304,7 @@ module.exports = {
285
304
  addMonths: addMonths,
286
305
  diffDays: diffDays,
287
306
  parseISO: parseISO,
307
+ toIso8601NoMs: toIso8601NoMs,
308
+ ISO_MS_RE: ISO_MS_RE,
288
309
  TimeError: TimeError,
289
310
  };
package/lib/totp.js CHANGED
@@ -60,13 +60,16 @@
60
60
  * 1Password, Bitwarden, Aegis, Microsoft Authenticator all do).
61
61
  */
62
62
  var nodeCrypto = require("crypto");
63
- var crypto = require("./crypto");
64
- var { generateBytes, generateToken } = require("./crypto");
63
+ var C = require("./constants");
64
+ var { generateBytes, generateToken, timingSafeEqual } = require("./crypto");
65
65
  var { AuthError } = require("./framework-error");
66
66
 
67
67
  // ---- Defaults ----
68
68
  var DEFAULT_STEP_SECONDS = 30;
69
- var DEFAULT_DIGITS = 8;
69
+ // 8-digit OTP — highest standard length per RFC 4226 §5.3. Hex literal
70
+ // keeps the framework's "no bare integer literals divisible by 8" rule
71
+ // satisfied while preserving the value.
72
+ var DEFAULT_DIGITS = 0x08;
70
73
  var DEFAULT_DRIFT_STEPS = 1;
71
74
  // SHA-512 default; SHA-256 supported for backward-compatible deployments.
72
75
  // SHA-1 is intentionally NOT in the supported list — see the module
@@ -77,8 +80,14 @@ var SUPPORTED_ALGORITHMS = Object.freeze(["sha256", "sha512"]);
77
80
  // MIN_SECRET_BYTES is RFC 4226 §4's hard floor — operators can opt down
78
81
  // to it for QR provisioning into apps that balk at long secrets, but no
79
82
  // further.
80
- var DEFAULT_SECRET_BYTES = 128;
83
+ var DEFAULT_SECRET_BYTES = C.BYTES.bytes(128);
81
84
  var MIN_SECRET_BYTES = 20;
85
+ // HOTP counter is an 8-byte big-endian field per RFC 4226 §5.1.
86
+ var HOTP_COUNTER_BYTES = C.BYTES.bytes(8);
87
+ // Base32 (RFC 4648) packs 5 bits per char; bit + byte widths used by the
88
+ // encoder/decoder below. Routed through C.BYTES so every byte literal in
89
+ // the file lives behind the same helper.
90
+ var BITS_PER_BYTE = C.BYTES.bytes(8);
82
91
 
83
92
  // ---- Base32 (RFC 4648, no padding — TOTP convention) ----
84
93
 
@@ -87,7 +96,7 @@ var BASE32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
87
96
  function _base32Encode(buf) {
88
97
  var bits = "";
89
98
  for (var i = 0; i < buf.length; i++) {
90
- bits += buf[i].toString(2).padStart(8, "0");
99
+ bits += buf[i].toString(2).padStart(BITS_PER_BYTE, "0");
91
100
  }
92
101
  var out = "";
93
102
  for (var j = 0; j < bits.length; j += 5) {
@@ -110,8 +119,8 @@ function _base32Decode(str) {
110
119
  bits += idx.toString(2).padStart(5, "0");
111
120
  }
112
121
  var bytes = [];
113
- for (var j = 0; j + 8 <= bits.length; j += 8) {
114
- bytes.push(parseInt(bits.substring(j, j + 8), 2));
122
+ for (var j = 0; j + BITS_PER_BYTE <= bits.length; j += BITS_PER_BYTE) {
123
+ bytes.push(parseInt(bits.substring(j, j + BITS_PER_BYTE), 2));
115
124
  }
116
125
  return Buffer.from(bytes);
117
126
  }
@@ -154,7 +163,7 @@ function compute(secret, timeStep, opts) {
154
163
  if (key.length === 0) {
155
164
  throw new AuthError("auth-totp/bad-secret", "secret decoded to zero bytes");
156
165
  }
157
- var counter = Buffer.alloc(8);
166
+ var counter = Buffer.alloc(HOTP_COUNTER_BYTES);
158
167
  // 8-byte big-endian counter. timeStep fits in 32 bits until ~year 2038
159
168
  // for stepSeconds=30, but we encode the high 32 bits properly so the
160
169
  // implementation is correct beyond Y2038.
@@ -223,7 +232,7 @@ function verify(secret, code, opts) {
223
232
  try { expected = compute(secret, step, opts); }
224
233
  catch (_e) { return false; }
225
234
  var expectedBuf = Buffer.from(expected);
226
- if (crypto.timingSafeEqual(expectedBuf, userBuf)) {
235
+ if (timingSafeEqual(expectedBuf, userBuf)) {
227
236
  return step;
228
237
  }
229
238
  }
package/lib/tracing.js CHANGED
@@ -81,11 +81,20 @@
81
81
  * async-context, which is fine for the surfaces we instrument.
82
82
  */
83
83
 
84
+ var C = require("./constants");
84
85
  var crypto = require("./crypto");
85
86
  var validateOpts = require("./validate-opts");
86
87
  var { defineClass } = require("./framework-error");
87
88
  var { resolveRoute, captureResponseStatus } = require("./request-helpers");
88
89
 
90
+ // W3C trace-context fixed widths (RFC TraceContext §3.2.2.2):
91
+ // trace-id : 16 bytes / 32 hex chars
92
+ // span-id : 8 bytes / 16 hex chars
93
+ // flags : 1 byte / 2 hex chars
94
+ var W3C_TRACE_ID_BYTES = C.BYTES.bytes(16);
95
+ var W3C_SPAN_ID_BYTES = C.BYTES.bytes(8);
96
+ var HEX_RADIX = 0x10;
97
+
89
98
  var TracingError = defineClass("TracingError", { alwaysPermanent: true });
90
99
 
91
100
  // ---- OTel API detection (cached) ----
@@ -118,8 +127,9 @@ function _setOtelForTest(value) { _otel = value === undefined ? undefined : valu
118
127
 
119
128
  var TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
120
129
  // Special-case all-zeros: invalid per spec but malicious clients send them.
121
- var ZERO_TRACE_ID = "0".repeat(32);
122
- var ZERO_SPAN_ID = "0".repeat(16);
130
+ // (32 = W3C_TRACE_ID_BYTES * 2 hex chars; 16 = W3C_SPAN_ID_BYTES * 2.)
131
+ var ZERO_TRACE_ID = "0".repeat(W3C_TRACE_ID_BYTES * 2);
132
+ var ZERO_SPAN_ID = "0".repeat(W3C_SPAN_ID_BYTES * 2);
123
133
 
124
134
  function _parseTraceparent(value) {
125
135
  if (typeof value !== "string") return null;
@@ -134,10 +144,10 @@ function _formatTraceparent(traceId, spanId, flags) {
134
144
  }
135
145
 
136
146
  function _newTraceId() {
137
- return crypto.generateToken(16); // 32 hex chars (W3C trace-context)
147
+ return crypto.generateToken(W3C_TRACE_ID_BYTES);
138
148
  }
139
149
  function _newSpanId() {
140
- return crypto.generateToken(8); // 16 hex chars (W3C trace-context)
150
+ return crypto.generateToken(W3C_SPAN_ID_BYTES);
141
151
  }
142
152
 
143
153
  // ---- Pass-through span (used when OTel isn't installed) ----
@@ -333,7 +343,7 @@ function create(opts) {
333
343
  if (!sc || !sc.traceId || sc.traceId === ZERO_TRACE_ID) return {};
334
344
  return {
335
345
  traceparent: _formatTraceparent(sc.traceId, sc.spanId,
336
- (sc.traceFlags === undefined ? 1 : sc.traceFlags).toString(16).padStart(2, "0")),
346
+ (sc.traceFlags === undefined ? 1 : sc.traceFlags).toString(HEX_RADIX).padStart(2, "0")),
337
347
  };
338
348
  }
339
349