@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
@@ -45,6 +45,7 @@
45
45
  * (deny is a security signal), `auditSuccess: false` (per-request noise).
46
46
  */
47
47
 
48
+ var C = require("./constants");
48
49
  var lazyRequire = require("./lazy-require");
49
50
  var requestHelpers = require("./request-helpers");
50
51
  var safeSql = require("./safe-sql");
@@ -55,14 +56,15 @@ var _err = PermissionsError.factory;
55
56
 
56
57
  var observability = lazyRequire(function () { return require("./observability"); });
57
58
 
58
- function _emitEvent(name, value, labels) {
59
- try { observability().event(name, value, labels || {}); }
60
- catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
61
- }
59
+ function _emitEvent(n, v, l) { observability().safeEvent(n, v, l || {}); }
62
60
 
63
61
  // Lowercase tokens, digits, dash, underscore, and `*` allowed per
64
62
  // segment. Scope format is segments separated by `:`.
65
63
  var SCOPE_RE = /^[a-z0-9_*-]+(:[a-z0-9_*-]+)*$/;
64
+ // Bound the regex engine on operator-supplied scope strings. 256 chars
65
+ // holds any realistic real-world scope (typical scopes run 8-32 chars);
66
+ // rejecting longer keeps the regex linear regardless of input shape.
67
+ var SCOPE_MAX_LENGTH = C.BYTES.bytes(256);
66
68
 
67
69
  // Audit defaults: BOTH success and failure default ON for permissions.
68
70
  // Unlike api-key.verify (which is gate-keeping for a downstream action
@@ -109,9 +111,12 @@ function _validateScopePattern(scope, ctx) {
109
111
  if (typeof scope !== "string" || scope.length === 0) {
110
112
  throw _err("BAD_SCOPE", ctx + ": scope must be a non-empty string, got " + typeof scope);
111
113
  }
112
- if (!SCOPE_RE.test(scope)) {
114
+ // Length cap before the regex test — bound the engine on hostile
115
+ // input lengths even though SCOPE_RE is anchored.
116
+ if (scope.length > SCOPE_MAX_LENGTH || !SCOPE_RE.test(scope)) {
113
117
  throw _err("BAD_SCOPE", ctx + ": scope '" + scope +
114
- "' must match " + SCOPE_RE + " (lowercase tokens with optional `*`)");
118
+ "' is empty, too long, or doesn't match " + SCOPE_RE +
119
+ " (lowercase tokens with optional `*`)");
115
120
  }
116
121
  }
117
122
 
@@ -241,23 +246,11 @@ function _defaultResolver(req) {
241
246
  // ---- Validation: create opts ----
242
247
 
243
248
  function _validateCreateOpts(opts) {
244
- if (!opts || typeof opts !== "object") {
245
- throw _err("BAD_OPT", "permissions.create: opts must be an object");
246
- }
247
- if (opts.resolver !== undefined && typeof opts.resolver !== "function") {
248
- throw _err("BAD_OPT", "permissions.create: resolver must be a function");
249
- }
250
- if (opts.audit !== undefined && opts.audit !== null) {
251
- if (typeof opts.audit !== "object" || typeof opts.audit.safeEmit !== "function") {
252
- throw _err("BAD_OPT", "permissions.create: audit must be a b.audit-shaped object (safeEmit fn)");
253
- }
254
- }
255
- if (opts.auditFailures !== undefined && typeof opts.auditFailures !== "boolean") {
256
- throw _err("BAD_OPT", "permissions.create: auditFailures must be a boolean");
257
- }
258
- if (opts.auditSuccess !== undefined && typeof opts.auditSuccess !== "boolean") {
259
- throw _err("BAD_OPT", "permissions.create: auditSuccess must be a boolean");
260
- }
249
+ validateOpts.requireObject(opts, "permissions.create", PermissionsError);
250
+ validateOpts.optionalFunction(opts.resolver, "permissions.create: resolver", PermissionsError);
251
+ validateOpts.auditShape(opts.audit, "permissions.create", PermissionsError);
252
+ validateOpts.optionalBoolean(opts.auditFailures, "permissions.create: auditFailures", PermissionsError);
253
+ validateOpts.optionalBoolean(opts.auditSuccess, "permissions.create: auditSuccess", PermissionsError);
261
254
  if (opts.denyStatus !== undefined &&
262
255
  (typeof opts.denyStatus !== "number" || !isFinite(opts.denyStatus) || opts.denyStatus < 100 || opts.denyStatus > 599)) {
263
256
  throw _err("BAD_OPT", "permissions.create: denyStatus must be an HTTP status code (100-599)");
@@ -267,9 +260,7 @@ function _validateCreateOpts(opts) {
267
260
  opts.missingActorStatus < 100 || opts.missingActorStatus > 599)) {
268
261
  throw _err("BAD_OPT", "permissions.create: missingActorStatus must be an HTTP status code (100-599)");
269
262
  }
270
- if (opts.responder !== undefined && typeof opts.responder !== "function") {
271
- throw _err("BAD_OPT", "permissions.create: responder must be a function");
272
- }
263
+ validateOpts.optionalFunction(opts.responder, "permissions.create: responder", PermissionsError);
273
264
  }
274
265
 
275
266
  // ---- Registry ----
@@ -281,13 +272,14 @@ function create(opts) {
281
272
  "denyStatus", "missingActorStatus", "responder",
282
273
  ], "permissions");
283
274
  _validateCreateOpts(opts);
275
+ var cfg = validateOpts.applyDefaults(opts, DEFAULTS);
284
276
  var roleTable = _validateRoles(opts.roles);
285
277
  var resolver = opts.resolver || _defaultResolver;
286
278
  var audit = opts.audit || null;
287
- var auditFailures = (opts.auditFailures === undefined) ? DEFAULTS.auditFailures : opts.auditFailures;
288
- var auditSuccess = (opts.auditSuccess === undefined) ? DEFAULTS.auditSuccess : opts.auditSuccess;
289
- var denyStatus = opts.denyStatus || DEFAULTS.denyStatus;
290
- var missingActorStatus = opts.missingActorStatus || DEFAULTS.missingActorStatus;
279
+ var auditFailures = cfg.auditFailures;
280
+ var auditSuccess = cfg.auditSuccess;
281
+ var denyStatus = cfg.denyStatus;
282
+ var missingActorStatus = cfg.missingActorStatus;
291
283
  var responder = opts.responder || _defaultResponder;
292
284
 
293
285
  // ABAC predicate registry. Each entry: scope-string → async predicate
@@ -317,12 +309,11 @@ function create(opts) {
317
309
  return policies[requestedScope] || null;
318
310
  }
319
311
 
312
+ var _emitRaw = validateOpts.makeAuditEmitter(audit);
320
313
  function _auditEmit(action, info) {
321
- if (!audit) return;
322
314
  if (info && info.outcome === "success" && !auditSuccess) return;
323
315
  if (info && info.outcome !== "success" && !auditFailures) return;
324
- try { audit.safeEmit(Object.assign({ action: action }, info || {})); }
325
- catch (_e) { /* audit best-effort */ }
316
+ _emitRaw(action, info);
326
317
  }
327
318
 
328
319
  function expand(roleNames) {
package/lib/pqc-agent.js CHANGED
@@ -37,9 +37,11 @@ var networkTls = require("./network-tls");
37
37
  // only the cryptographic posture (ecdhCurve / minVersion) is locked.
38
38
  var DEFAULT_OPTS = {
39
39
  keepAlive: true,
40
- keepAliveMsecs: 30000,
40
+ keepAliveMsecs: C.TIME.seconds(30),
41
41
  maxSockets: 50,
42
- maxFreeSockets: 16,
42
+ // Pool counts are protocol-side concurrency tunables, not byte
43
+ // quantities — hex form keeps the byte-shape detector out of it.
44
+ maxFreeSockets: 0x10,
43
45
  scheduling: "lifo",
44
46
  };
45
47
 
package/lib/pqc-gate.js CHANGED
@@ -37,14 +37,16 @@
37
37
  * connection lifecycle deterministically.
38
38
  */
39
39
  var net = require("node:net");
40
+ var C = require("./constants");
40
41
  var { PQC_GROUPS } = require("./constants");
42
+ var nb = require("./numeric-bounds");
41
43
  var validateOpts = require("./validate-opts");
42
44
  var { boot } = require("./log");
43
45
 
44
46
  var DEFAULT_LOG = boot("pqc-gate");
45
47
  var DEFAULT_BYPASS = Object.freeze(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
46
- var DEFAULT_CLIENTHELLO_TIMEOUT_MS = 5000;
47
- var DEFAULT_MAX_CLIENTHELLO_BYTES = 16384;
48
+ var DEFAULT_CLIENTHELLO_TIMEOUT_MS = C.TIME.seconds(5);
49
+ var DEFAULT_MAX_CLIENTHELLO_BYTES = C.BYTES.kib(16);
48
50
 
49
51
  // TLS handshake_failure alert (fatal). Sent before destroying a socket
50
52
  // so a strict TLS client logs a precise reason rather than a vague
@@ -73,7 +75,7 @@ function clientHelloHasPQC(buf) {
73
75
  if (buf[5] !== 0x01) return false; // not ClientHello
74
76
 
75
77
  // Body: version(2) + random(32) = 34 bytes after handshake header
76
- var offset = 9 + 2 + 32;
78
+ var offset = 9 + 2 + C.BYTES.bytes(32);
77
79
  if (offset + 1 > recordEnd) return false;
78
80
 
79
81
  // Session ID: length(1) + data
@@ -153,10 +155,16 @@ function create(opts) {
153
155
  }
154
156
  var internalHost = typeof opts.internalHost === "string" ? opts.internalHost : "127.0.0.1";
155
157
  var bypass = Array.isArray(opts.bypass) ? opts.bypass.slice() : DEFAULT_BYPASS.slice();
156
- var clientHelloTimeoutMs = typeof opts.clientHelloTimeoutMs === "number" && opts.clientHelloTimeoutMs > 0
157
- ? opts.clientHelloTimeoutMs : DEFAULT_CLIENTHELLO_TIMEOUT_MS;
158
- var maxClientHelloBytes = typeof opts.maxClientHelloBytes === "number" && opts.maxClientHelloBytes > 0
159
- ? opts.maxClientHelloBytes : DEFAULT_MAX_CLIENTHELLO_BYTES;
158
+ if (opts.clientHelloTimeoutMs !== undefined && !nb.isPositiveFiniteInt(opts.clientHelloTimeoutMs)) {
159
+ throw new Error("pqc-gate: clientHelloTimeoutMs must be a positive finite integer; got " +
160
+ nb.shape(opts.clientHelloTimeoutMs));
161
+ }
162
+ var clientHelloTimeoutMs = opts.clientHelloTimeoutMs || DEFAULT_CLIENTHELLO_TIMEOUT_MS;
163
+ if (opts.maxClientHelloBytes !== undefined && !nb.isPositiveFiniteInt(opts.maxClientHelloBytes)) {
164
+ throw new Error("pqc-gate: maxClientHelloBytes must be a positive finite integer; got " +
165
+ nb.shape(opts.maxClientHelloBytes));
166
+ }
167
+ var maxClientHelloBytes = opts.maxClientHelloBytes || DEFAULT_MAX_CLIENTHELLO_BYTES;
160
168
  var log = opts.log || null;
161
169
 
162
170
  // Test seams
@@ -204,22 +212,27 @@ function create(opts) {
204
212
 
205
213
  if (totalLen > maxClientHelloBytes) {
206
214
  resolved = true;
207
- try { clearTimeoutFn(timeout); } catch (_e) {}
215
+ try { clearTimeoutFn(timeout); } catch (_e) { /* timer may already have fired */ }
208
216
  _logVia(log, "warn", "ClientHello too large", { ip: clientIp, size: totalLen });
209
- try { socket.destroy(); } catch (_e) {}
217
+ try { socket.destroy(); } catch (_e) { /* socket may already be torn down */ }
210
218
  return;
211
219
  }
212
220
 
213
221
  // Reject non-handshake first byte immediately
214
222
  if (totalLen >= 1 && chunks[0][0] !== 0x16) {
215
223
  resolved = true;
216
- try { clearTimeoutFn(timeout); } catch (_e) {}
217
- try { socket.destroy(); } catch (_e) {}
224
+ try { clearTimeoutFn(timeout); } catch (_e) { /* timer may already have fired */ }
225
+ try { socket.destroy(); } catch (_e) { /* socket may already be torn down */ }
218
226
  return;
219
227
  }
220
228
 
221
229
  if (totalLen < 5) return; // need at least the record header
222
230
 
231
+ // TLS ClientHello parser needs streaming peek (re-concat on
232
+ // each chunk to read the record-length prefix) before deciding
233
+ // whether more data is needed. boundedChunkCollector is append-
234
+ // only with no peek, doesn't fit.
235
+ // allow:handrolled-buffer-collect — see comment above
223
236
  var buf = Buffer.concat(chunks);
224
237
  var recordLen = buf.readUInt16BE(3);
225
238
  var neededLen = 5 + recordLen;
@@ -227,7 +240,7 @@ function create(opts) {
227
240
  if (buf.length < Math.min(neededLen, maxClientHelloBytes)) return;
228
241
 
229
242
  resolved = true;
230
- try { clearTimeoutFn(timeout); } catch (_e) {}
243
+ try { clearTimeoutFn(timeout); } catch (_e) { /* timer may already have fired */ }
231
244
  socket.removeListener("data", onData);
232
245
  socket.pause();
233
246
 
@@ -238,17 +251,17 @@ function create(opts) {
238
251
  "connection rejected — no PQC group in ClientHello", { ip: clientIp });
239
252
  try {
240
253
  socket.write(TLS_ALERT_HANDSHAKE_FAILURE, function () {
241
- try { socket.destroy(); } catch (_e) {}
254
+ try { socket.destroy(); } catch (_e) { /* socket may already be torn down */ }
242
255
  });
243
256
  } catch (_e) {
244
- try { socket.destroy(); } catch (_e2) {}
257
+ try { socket.destroy(); } catch (_e2) { /* socket may already be torn down */ }
245
258
  }
246
259
  }
247
260
  });
248
261
 
249
262
  socket.on("error", function () {
250
263
  resolved = true;
251
- try { clearTimeoutFn(timeout); } catch (_e) {}
264
+ try { clearTimeoutFn(timeout); } catch (_e) { /* timer may already have fired */ }
252
265
  });
253
266
 
254
267
  socket.resume();
@@ -27,9 +27,15 @@
27
27
  * the caller can splice in any order.
28
28
  */
29
29
 
30
+ var C = require("./constants");
31
+
30
32
  var WIRE_VARINT = 0;
31
33
  var WIRE_64BIT = 1;
32
34
  var WIRE_LDELIM = 2;
35
+ // Varint base — each byte carries 7 payload bits + 1 continuation bit.
36
+ var VARINT_BASE = 128;
37
+ // Wire-format byte counts for fixed-size fields.
38
+ var FIXED64_BYTES = C.BYTES.bytes(8);
33
39
  // WIRE_32BIT (5) is unused — fixed32 / sfixed32 / float aren't on the
34
40
  // OTel logs schema. Reserved here as documentation; un-comment + add
35
41
  // helpers when a future caller (metrics histograms?) needs them.
@@ -67,7 +73,7 @@ function _writeVarint(value) {
67
73
  var n = value;
68
74
  do {
69
75
  var byte = n & 0x7f;
70
- n = Math.floor(n / 128);
76
+ n = Math.floor(n / VARINT_BASE);
71
77
  if (n > 0) byte |= 0x80;
72
78
  bytes.push(byte);
73
79
  } while (n > 0);
@@ -97,7 +103,7 @@ function bool(fieldNumber, value) {
97
103
  function fixed64(fieldNumber, value) {
98
104
  // For OTel: time_unix_nano fields are fixed64. Accept BigInt or
99
105
  // Number; encode as little-endian 8 bytes.
100
- var buf = Buffer.alloc(8);
106
+ var buf = Buffer.alloc(FIXED64_BYTES);
101
107
  if (typeof value === "bigint") {
102
108
  buf.writeBigUInt64LE(value, 0);
103
109
  } else {
@@ -115,7 +121,7 @@ function fixed64(fieldNumber, value) {
115
121
 
116
122
  function double(fieldNumber, value) {
117
123
  if (value === 0) return Buffer.alloc(0); // proto3 default
118
- var buf = Buffer.alloc(8);
124
+ var buf = Buffer.alloc(FIXED64_BYTES);
119
125
  buf.writeDoubleLE(value, 0);
120
126
  return Buffer.concat([_tag(fieldNumber, WIRE_64BIT), buf]);
121
127
  }
@@ -45,6 +45,7 @@
45
45
  * operator with multiple dispatchers (queue + log-stream) reading the
46
46
  * error message knows which one rejected the protocol.
47
47
  */
48
+ var validateOpts = require("./validate-opts");
48
49
  var { defineClass } = require("./framework-error");
49
50
 
50
51
  // Default error class — operators / test fixtures that don't want to
@@ -55,13 +56,13 @@ var ProtocolDispatcherError = defineClass("ProtocolDispatcherError", { withStatu
55
56
  // boot, not first request.
56
57
  function _validateConfig(opts) {
57
58
  if (!opts || typeof opts !== "object") {
58
- throw new Error("protocolDispatcher.create: opts is required");
59
- }
60
- if (typeof opts.name !== "string" || opts.name.length === 0) {
61
- throw new Error("protocolDispatcher.create: opts.name (string) is required");
59
+ throw new ProtocolDispatcherError("protocol-dispatcher/bad-opts",
60
+ "protocolDispatcher.create: opts is required");
62
61
  }
62
+ validateOpts.requireNonEmptyString(opts.name, "protocolDispatcher.create: opts.name", ProtocolDispatcherError, "protocol-dispatcher/bad-name");
63
63
  if (!opts.protocols || typeof opts.protocols !== "object" || Array.isArray(opts.protocols)) {
64
- throw new Error("protocolDispatcher.create: opts.protocols (object) is required");
64
+ throw new ProtocolDispatcherError("protocol-dispatcher/bad-protocols",
65
+ "protocolDispatcher.create: opts.protocols (object) is required");
65
66
  }
66
67
  // Each protocol value should look like a module with .create — operators
67
68
  // wiring a typo'd reference get a clearer error than "undefined is not
@@ -70,24 +71,28 @@ function _validateConfig(opts) {
70
71
  for (var i = 0; i < pkeys.length; i++) {
71
72
  var p = opts.protocols[pkeys[i]];
72
73
  if (!p || typeof p !== "object" || typeof p.create !== "function") {
73
- throw new Error("protocolDispatcher.create: opts.protocols['" + pkeys[i] +
74
+ throw new ProtocolDispatcherError("protocol-dispatcher/bad-protocol-entry",
75
+ "protocolDispatcher.create: opts.protocols['" + pkeys[i] +
74
76
  "'] must be an object with a .create function (got " +
75
77
  (p === null ? "null" : typeof p) + ")");
76
78
  }
77
79
  }
78
80
  if (opts.deferred !== undefined && opts.deferred !== null) {
79
81
  if (typeof opts.deferred !== "object" || Array.isArray(opts.deferred)) {
80
- throw new Error("protocolDispatcher.create: opts.deferred must be an object (or omitted)");
82
+ throw new ProtocolDispatcherError("protocol-dispatcher/bad-deferred",
83
+ "protocolDispatcher.create: opts.deferred must be an object (or omitted)");
81
84
  }
82
85
  }
83
86
  if (opts.fallbackProtocol !== undefined && opts.fallbackProtocol !== null) {
84
87
  if (typeof opts.fallbackProtocol !== "string" || opts.fallbackProtocol.length === 0) {
85
- throw new Error("protocolDispatcher.create: opts.fallbackProtocol must be a non-empty string (or omitted)");
88
+ throw new ProtocolDispatcherError("protocol-dispatcher/bad-fallback",
89
+ "protocolDispatcher.create: opts.fallbackProtocol must be a non-empty string (or omitted)");
86
90
  }
87
91
  }
88
92
  if (opts.errorClass !== undefined && opts.errorClass !== null) {
89
93
  if (typeof opts.errorClass !== "function") {
90
- throw new Error("protocolDispatcher.create: opts.errorClass must be a constructor (or omitted)");
94
+ throw new ProtocolDispatcherError("protocol-dispatcher/bad-error-class",
95
+ "protocolDispatcher.create: opts.errorClass must be a constructor (or omitted)");
91
96
  }
92
97
  }
93
98
  }
@@ -122,7 +127,12 @@ function create(opts) {
122
127
  throw _err("PROTOCOL_NOT_IMPLEMENTED", msg);
123
128
  }
124
129
  if (!Object.prototype.hasOwnProperty.call(protocols, protocol)) {
125
- var known = Object.keys(protocols).sort().join(", ");
130
+ // Diagnostic listing only — sorted protocol-name list for the
131
+ // operator-facing error message; not a canonicalization-for-hash
132
+ // walk, so the bare keys-then-sort is correct here.
133
+ var protoKeys = Object.keys(protocols);
134
+ protoKeys.sort();
135
+ var known = protoKeys.join(", ");
126
136
  throw _err("UNKNOWN_PROTOCOL",
127
137
  "unknown " + name + " protocol: '" + protocol +
128
138
  "' (known: " + (known || "[none]") + ")");
@@ -130,11 +140,18 @@ function create(opts) {
130
140
  return protocols[protocol];
131
141
  }
132
142
 
143
+ // Same diagnostic-only sort for the dispatcher's introspection
144
+ // surface — operators read these to confirm wiring at boot.
145
+ var protocolNames = Object.keys(protocols);
146
+ protocolNames.sort();
147
+ var deferredNames = Object.keys(deferred);
148
+ deferredNames.sort();
149
+
133
150
  return {
134
151
  name: name,
135
152
  resolve: resolve,
136
- protocols: Object.keys(protocols).sort(),
137
- deferred: Object.keys(deferred).sort(),
153
+ protocols: protocolNames,
154
+ deferred: deferredNames,
138
155
  };
139
156
  }
140
157
 
@@ -21,9 +21,11 @@
21
21
  * applied by `lib/pubsub.js`; this backend doesn't add any naming
22
22
  * conventions of its own.
23
23
  */
24
- var crypto = require("node:crypto");
25
- var redisClient = require("./redis-client");
24
+ var C = require("./constants");
25
+ var fwCrypto = require("./crypto");
26
26
  var lazyRequire = require("./lazy-require");
27
+ var redisClient = require("./redis-client");
28
+ var safeJson = require("./safe-json");
27
29
 
28
30
  var logger = lazyRequire(function () { return require("./log").boot("pubsub-redis"); });
29
31
 
@@ -37,7 +39,7 @@ function create(opts) {
37
39
  // the local dispatch synchronously before awaiting the remote
38
40
  // write — without this filter every same-instance publish would
39
41
  // double-fire local handlers).
40
- var instanceNonce = crypto.randomBytes(8).toString("hex");
42
+ var instanceNonce = fwCrypto.generateToken(C.BYTES.bytes(8));
41
43
 
42
44
  var clientOpts = {
43
45
  url: opts.redisUrl,
@@ -69,16 +71,21 @@ function create(opts) {
69
71
  var payloadStr = Buffer.isBuffer(rawPayload)
70
72
  ? rawPayload.toString("utf8") : String(rawPayload);
71
73
  var inner = payloadStr;
74
+ // Redis pubsub frames are bounded by Redis's own bulk-string limit;
75
+ // cap the envelope parse at 16 MiB to keep this hot path fast and
76
+ // defeat a hostile publisher inflating one frame to 512 MB.
72
77
  try {
73
- var envelope = JSON.parse(payloadStr);
78
+ var envelope = safeJson.parse(payloadStr, { maxBytes: C.BYTES.mib(16) });
74
79
  if (envelope && typeof envelope === "object" &&
75
80
  typeof envelope._psnode === "string") {
76
81
  if (envelope._psnode === instanceNonce) return; // own publish
77
82
  inner = JSON.stringify(envelope.p);
78
83
  }
79
- } catch (_e) {
84
+ } catch (e) {
80
85
  // Not an envelope — forward as-is for operators publishing raw
81
- // strings via redis CLI etc.
86
+ // strings via redis CLI etc. (Parse failure is the expected path
87
+ // for raw-string publishers; not log-worthy.)
88
+ void e;
82
89
  }
83
90
  try {
84
91
  savedOnRemoteMessage(ev.channel, inner, {
@@ -138,11 +145,21 @@ function create(opts) {
138
145
  stopped = true;
139
146
  savedOnRemoteMessage = null;
140
147
  if (subscriberConn) {
141
- try { await subscriberConn.close(); } catch (_e) {}
148
+ try { await subscriberConn.close(); }
149
+ catch (e) {
150
+ try { logger().debug("subscriber-close-failed: " +
151
+ ((e && e.message) || String(e))); }
152
+ catch (_e) { /* logger best-effort */ }
153
+ }
142
154
  subscriberConn = null;
143
155
  }
144
156
  if (publisherConn) {
145
- try { await publisherConn.close(); } catch (_e) {}
157
+ try { await publisherConn.close(); }
158
+ catch (e) {
159
+ try { logger().debug("publisher-close-failed: " +
160
+ ((e && e.message) || String(e))); }
161
+ catch (_e) { /* logger best-effort */ }
162
+ }
146
163
  publisherConn = null;
147
164
  }
148
165
  }
package/lib/pubsub.js CHANGED
@@ -72,6 +72,7 @@
72
72
  * matches client-side, redis backend uses PSUBSCRIBE, local backend
73
73
  * matches client-side too).
74
74
  */
75
+ var C = require("./constants");
75
76
  var lazyRequire = require("./lazy-require");
76
77
  var safeJson = require("./safe-json");
77
78
  var validateOpts = require("./validate-opts");
@@ -79,6 +80,11 @@ var { defineClass } = require("./framework-error");
79
80
 
80
81
  var audit = lazyRequire(function () { return require("./audit"); });
81
82
  var logger = lazyRequire(function () { return require("./log").boot("pubsub"); });
83
+ // Backend dispatch — lazy so pubsub-cluster / pubsub-redis (which depend
84
+ // on this file's exports for their own framework integration) don't
85
+ // load-cycle on first import.
86
+ var pubsubCluster = lazyRequire(function () { return require("./pubsub-cluster"); });
87
+ var pubsubRedis = lazyRequire(function () { return require("./pubsub-redis"); });
82
88
 
83
89
  var PubsubError = defineClass("PubsubError");
84
90
 
@@ -99,29 +105,95 @@ function _resolveBackend(opts) {
99
105
  return { name: "local", publishRemote: null, start: null, stop: null };
100
106
  }
101
107
  if (requested === "cluster") {
102
- return require("./pubsub-cluster").create(opts);
108
+ return pubsubCluster().create(opts);
103
109
  }
104
110
  if (requested === "redis") {
105
- return require("./pubsub-redis").create(opts);
111
+ return pubsubRedis().create(opts);
106
112
  }
107
113
  throw _err("UNKNOWN_BACKEND",
108
114
  "pubsub: unknown backend '" + requested +
109
115
  "' (must be 'local', 'cluster', 'redis', or { publishRemote, start, stop })");
110
116
  }
111
117
 
118
+ // Glob-style: '*' matches any single segment (no '.'); '**' matches any
119
+ // suffix. Operators wanting full Redis-pattern semantics use the redis
120
+ // backend's PSUBSCRIBE which the redis client routes natively.
121
+ //
122
+ // Hand-rolled matcher (no `new RegExp` from operator input) so a hostile
123
+ // operator-supplied pattern can't compile a backtracking regex (ReDoS).
124
+ // Channel length is bounded to defeat pathological inputs even on
125
+ // well-behaved patterns; pubsub channel names in practice are well under
126
+ // 1 KiB and the framework's own callers never exceed it.
127
+ var _MAX_CHANNEL_LEN = C.BYTES.kib(1);
128
+
129
+ function _matchGlobPart(part, channel, fromIdx) {
130
+ // Within one '**'-bounded part, match a sequence of '*'-separated
131
+ // literals against channel[fromIdx..] WITHOUT crossing '.' boundaries.
132
+ // Returns the matched length, or -1 on no match.
133
+ var segments = part.split("*");
134
+ var pos = fromIdx;
135
+ for (var i = 0; i < segments.length; i++) {
136
+ var lit = segments[i];
137
+ if (i === 0) {
138
+ if (channel.substr(pos, lit.length) !== lit) return -1;
139
+ pos += lit.length;
140
+ } else if (i === segments.length - 1) {
141
+ // Trailing literal: search forward in this segment (no '.' jump).
142
+ var hardStop = channel.indexOf(".", pos);
143
+ var searchEnd = hardStop === -1 ? channel.length : hardStop;
144
+ if (lit === "") { pos = searchEnd; break; }
145
+ var found = channel.lastIndexOf(lit, searchEnd - lit.length);
146
+ if (found < pos) return -1;
147
+ pos = found + lit.length;
148
+ } else {
149
+ // Middle literal: find next occurrence within current segment.
150
+ var hardStop2 = channel.indexOf(".", pos);
151
+ var searchEnd2 = hardStop2 === -1 ? channel.length : hardStop2;
152
+ var f2 = channel.indexOf(lit, pos);
153
+ if (f2 < 0 || f2 + lit.length > searchEnd2) return -1;
154
+ pos = f2 + lit.length;
155
+ }
156
+ }
157
+ return pos - fromIdx;
158
+ }
159
+
112
160
  function _matchPattern(pattern, channel) {
113
- // Glob-style: '*' matches any single segment; '**' matches any
114
- // suffix. Operators wanting full Redis-pattern semantics use the
115
- // redis backend's PSUBSCRIBE which the redis client routes natively.
116
161
  if (pattern === channel) return true;
162
+ if (typeof channel !== "string" || channel.length > _MAX_CHANNEL_LEN) return false;
163
+ if (typeof pattern !== "string" || pattern.length > _MAX_CHANNEL_LEN) return false;
117
164
  if (pattern.indexOf("*") === -1) return false;
118
- var rx = "^" + pattern
119
- .split("**").map(function (p) {
120
- return p.split("*").map(function (s) {
121
- return s.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
122
- }).join("[^.]*");
123
- }).join(".*") + "$";
124
- return new RegExp(rx).test(channel);
165
+ // Split on '**' each part is a glob that matches without crossing '.';
166
+ // between parts any char (including '.') is consumed greedily.
167
+ var parts = pattern.split("**");
168
+ if (parts.length === 1) {
169
+ // No '**'; pattern must consume the entire channel and only '*'
170
+ // (single-segment) wildcards are allowed.
171
+ var len = _matchGlobPart(parts[0], channel, 0);
172
+ return len === channel.length;
173
+ }
174
+ // Anchor the head.
175
+ var headLen = _matchGlobPart(parts[0], channel, 0);
176
+ if (headLen < 0) return false;
177
+ var idx = headLen;
178
+ // Match middle parts: each must appear somewhere after idx, in order,
179
+ // separated by arbitrary content (since '**' consumes anything).
180
+ for (var p = 1; p < parts.length - 1; p++) {
181
+ var found = -1;
182
+ for (var probe = idx; probe <= channel.length; probe++) {
183
+ var n = _matchGlobPart(parts[p], channel, probe);
184
+ if (n >= 0) { found = probe + n; break; }
185
+ }
186
+ if (found < 0) return false;
187
+ idx = found;
188
+ }
189
+ // Tail: must reach end of channel.
190
+ var tail = parts[parts.length - 1];
191
+ if (tail === "") return true;
192
+ for (var t = idx; t <= channel.length; t++) {
193
+ var tn = _matchGlobPart(tail, channel, t);
194
+ if (tn >= 0 && t + tn === channel.length) return true;
195
+ }
196
+ return false;
125
197
  }
126
198
 
127
199
  function create(opts) {
@@ -35,6 +35,7 @@ var C = require("./constants");
35
35
  var { generateToken } = require("./crypto");
36
36
  var cryptoField = require("./crypto-field");
37
37
  var lazyRequire = require("./lazy-require");
38
+ var nb = require("./numeric-bounds");
38
39
  var safeJson = require("./safe-json");
39
40
  var scheduler = require("./scheduler");
40
41
  var { QueueError } = require("./framework-error");
@@ -149,7 +150,7 @@ function create(_config) {
149
150
  var effectiveAvailableAt = (dependsOn ? FLOW_BLOCKED_AVAILABLE_AT : availableAt);
150
151
 
151
152
  var row = {
152
- _id: generateToken(16),
153
+ _id: generateToken(C.BYTES.bytes(16)),
153
154
  queueName: queueName,
154
155
  payload: payload === undefined ? null : JSON.stringify(payload),
155
156
  status: "pending",
@@ -305,7 +306,7 @@ function create(_config) {
305
306
  var sib = siblings[i];
306
307
  if (!sib.dependsOn) continue;
307
308
  var deps;
308
- try { deps = JSON.parse(sib.dependsOn); }
309
+ try { deps = safeJson.parse(sib.dependsOn, { maxBytes: C.BYTES.mib(1) }); }
309
310
  catch (_e) { continue; }
310
311
  if (!Array.isArray(deps) || deps.length === 0) continue;
311
312
  // Resolve which deps are satisfied. Each dep is either a jobId
@@ -385,7 +386,15 @@ function create(_config) {
385
386
 
386
387
  async function dlqList(queueName, opts) {
387
388
  opts = opts || {};
388
- var limit = (typeof opts.limit === "number" && opts.limit > 0) ? opts.limit : 100;
389
+ var limit = 100;
390
+ if (opts.limit !== undefined) {
391
+ if (!nb.isPositiveFiniteInt(opts.limit)) {
392
+ throw new QueueError("queue/bad-opt",
393
+ "queue.dlqList: limit must be a positive finite integer; got " +
394
+ nb.shape(opts.limit), true);
395
+ }
396
+ limit = opts.limit;
397
+ }
389
398
  var rows = await clusterStorage.executeAll(
390
399
  "SELECT _id, queueName, payload, status, enqueuedAt, finishedAt, " +
391
400
  " attempts, maxAttempts, lastError, traceId, classification " +
@@ -399,7 +408,7 @@ function create(_config) {
399
408
  return {
400
409
  jobId: row._id,
401
410
  queueName: row.queueName,
402
- payload: unsealed.payload ? JSON.parse(unsealed.payload) : null,
411
+ payload: unsealed.payload ? safeJson.parse(unsealed.payload, { maxBytes: C.BYTES.mib(64) }) : null,
403
412
  status: row.status,
404
413
  enqueuedAt: Number(row.enqueuedAt),
405
414
  finishedAt: row.finishedAt ? Number(row.finishedAt) : null,
@@ -350,7 +350,7 @@ function create(opts) {
350
350
  } else {
351
351
  availableAt = nowMs + (opts2.delaySeconds ? C.TIME.seconds(opts2.delaySeconds) : 0);
352
352
  }
353
- var jobId = generateToken(16);
353
+ var jobId = generateToken(C.BYTES.bytes(16));
354
354
  var row = {
355
355
  _id: jobId,
356
356
  queueName: queueName,
@@ -540,7 +540,7 @@ function create(opts) {
540
540
  var status = rv[1] && (Buffer.isBuffer(rv[1]) ? rv[1].toString("utf8") : String(rv[1]));
541
541
  if (!rawDeps || status !== "pending") continue;
542
542
  var deps;
543
- try { deps = JSON.parse(rawDeps); }
543
+ try { deps = safeJson.parse(rawDeps, { maxBytes: C.BYTES.mib(1) }); }
544
544
  catch (_e) { continue; }
545
545
  if (!Array.isArray(deps) || deps.length === 0) continue;
546
546