@blamejs/core 0.6.70 → 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 +4 -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 +23 -26
  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 +4 -3
  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 +26 -15
  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/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
 
package/lib/queue-sqs.js CHANGED
@@ -45,6 +45,7 @@
45
45
  * local + redis backends.
46
46
  */
47
47
  var sigv4 = require("./object-store/sigv4");
48
+ var C = require("./constants");
48
49
  var httpClient = require("./http-client");
49
50
  var cryptoField = require("./crypto-field");
50
51
  var safeJson = require("./safe-json");
@@ -90,13 +91,16 @@ function create(opts) {
90
91
 
91
92
  var region = opts.region;
92
93
  var endpoint = _resolveEndpoint(opts);
93
- var endpointUrl = new URL(endpoint);
94
+ var allowedProtocols = opts.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
95
+ var endpointUrl = safeUrl.parse(endpoint, {
96
+ errorClass: QueueError,
97
+ allowedProtocols: allowedProtocols,
98
+ });
94
99
  var accessKeyId = opts.accessKeyId;
95
100
  var secretAccessKey = opts.secretAccessKey;
96
101
  var sessionToken = opts.sessionToken || null;
97
102
  var accountId = opts.accountId ? String(opts.accountId) : null;
98
103
  var timeoutMs = opts.timeoutMs;
99
- var allowedProtocols = opts.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
100
104
  var allowInternal = opts.allowInternal != null ? opts.allowInternal : null;
101
105
  var visibilityTimeoutSec = Number(opts.visibilityTimeoutSec) || DEFAULT_VISIBILITY_TIMEOUT_SEC;
102
106
  var waitTimeSec = Number(opts.waitTimeSec) || DEFAULT_WAIT_TIME_SEC;
@@ -115,15 +119,16 @@ function create(opts) {
115
119
  "Content-Length": String(bodyBuf.length),
116
120
  };
117
121
  var signed = sigv4.signRequest({
118
- method: "POST",
119
- url: endpointUrl,
120
- headers: headers,
121
- payloadHash: _payloadHash(bodyBuf),
122
- region: region,
123
- service: "sqs",
124
- accessKeyId: accessKeyId,
125
- secretAccessKey: secretAccessKey,
126
- sessionToken: sessionToken,
122
+ method: "POST",
123
+ url: endpointUrl,
124
+ headers: headers,
125
+ payloadHash: _payloadHash(bodyBuf),
126
+ region: region,
127
+ service: "sqs",
128
+ accessKeyId: accessKeyId,
129
+ secretAccessKey: secretAccessKey,
130
+ sessionToken: sessionToken,
131
+ allowedProtocols: allowedProtocols,
127
132
  });
128
133
  var reqOpts = {
129
134
  method: "POST",
@@ -151,7 +156,7 @@ function create(opts) {
151
156
  async function enqueue(queueName, payload, enqueueOpts) {
152
157
  enqueueOpts = enqueueOpts || {};
153
158
  var queueUrl = queueUrlResolver(queueName);
154
- var jobId = generateToken(16);
159
+ var jobId = generateToken(C.BYTES.bytes(16));
155
160
  var sealed = cryptoField.sealRow("_blamejs_jobs", {
156
161
  _id: jobId,
157
162
  queueName: queueName,
@@ -168,8 +173,8 @@ function create(opts) {
168
173
  };
169
174
  var delaySeconds = enqueueOpts.delaySeconds;
170
175
  if (typeof delaySeconds === "number" && delaySeconds > 0) {
171
- // SQS hard cap is 900s.
172
- sqsBody.DelaySeconds = Math.min(900, Math.floor(delaySeconds));
176
+ // SQS hard cap is 900s (15 min).
177
+ sqsBody.DelaySeconds = Math.min(C.TIME.minutes(15) / C.TIME.seconds(1), Math.floor(delaySeconds));
173
178
  }
174
179
  var rv = await _post("SendMessage", sqsBody);
175
180
  return rv && (rv.MessageId || jobId);
@@ -203,7 +208,7 @@ function create(opts) {
203
208
  var payload;
204
209
  try {
205
210
  payload = unsealed.payload != null
206
- ? JSON.parse(unsealed.payload) : null;
211
+ ? safeJson.parse(unsealed.payload, { maxBytes: C.BYTES.mib(64) }) : null;
207
212
  } catch (_e) { payload = unsealed.payload; }
208
213
  out.push({
209
214
  jobId: unsealed._id,
@@ -211,7 +216,7 @@ function create(opts) {
211
216
  payload: payload,
212
217
  attempts: Number(unsealed.attempts) || 0,
213
218
  enqueuedAt: Number(unsealed.enqueuedAt) || null,
214
- leaseExpiresAt: Date.now() + (visTimeout * 1000),
219
+ leaseExpiresAt: Date.now() + C.TIME.seconds(visTimeout),
215
220
  // SQS-specific: receipt handle is the only way to delete /
216
221
  // change visibility / extend — surface it for the framework
217
222
  // wrapper.
package/lib/queue.js CHANGED
@@ -34,6 +34,7 @@ var C = require("./constants");
34
34
  var clusterStorage = require("./cluster-storage");
35
35
  var crypto = require("./crypto");
36
36
  var lazyRequire = require("./lazy-require");
37
+ var { boot } = require("./log");
37
38
  var numericChecks = require("./numeric-checks");
38
39
  var observability = require("./observability");
39
40
  var protocolDispatcher = require("./protocol-dispatcher");
@@ -44,6 +45,8 @@ var retryHelper = require("./retry");
44
45
  var safeAsync = require("./safe-async");
45
46
  var { QueueError } = require("./framework-error");
46
47
 
48
+ var log = boot("queue");
49
+
47
50
  var dispatcher = protocolDispatcher.create({
48
51
  name: "queue",
49
52
  errorClass: QueueError,
@@ -180,8 +183,8 @@ function consume(queueName, handler, opts) {
180
183
  opts = opts || {};
181
184
  var b = _backendFor(opts);
182
185
  var concurrency = opts.concurrency || 1;
183
- var leaseDurationMs = opts.leaseDurationMs || 30000;
184
- var pollIntervalMs = opts.pollIntervalMs || 1000;
186
+ var leaseDurationMs = opts.leaseDurationMs || C.TIME.seconds(30);
187
+ var pollIntervalMs = opts.pollIntervalMs || C.TIME.seconds(1);
185
188
  var fastPollMs = opts.fastPollMs || 50;
186
189
 
187
190
  // Rate-limit: { max, perSeconds } caps how many handler INVOCATIONS
@@ -235,7 +238,8 @@ function consume(queueName, handler, opts) {
235
238
  abortCtrl: abortCtrl,
236
239
  cancel: function () {
237
240
  state.cancelled = true;
238
- try { abortCtrl.abort(); } catch (_e) {}
241
+ try { abortCtrl.abort(); }
242
+ catch (e) { log.debug("cancel-cleanup-failed", { op: "abortCtrl.abort", error: e.message }); }
239
243
  },
240
244
  };
241
245
  consumers.push(state);
@@ -575,7 +579,7 @@ function enqueueFlow(spec) {
575
579
  return Promise.reject(e);
576
580
  }
577
581
 
578
- var flowId = "flow-" + crypto.generateToken(8);
582
+ var flowId = "flow-" + crypto.generateToken(C.BYTES.bytes(8));
579
583
 
580
584
  return observability.tap("queue.enqueueFlow",
581
585
  { queueName: spec.queueName, flowId: flowId, childCount: spec.children.length },