@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.
- package/CHANGELOG.md +4 -0
- package/README.md +11 -1
- package/index.js +2 -5
- package/lib/api-key.js +24 -55
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +20 -16
- package/lib/app.js +6 -5
- package/lib/archive.js +58 -50
- package/lib/atomic-file.js +11 -5
- package/lib/audit-chain.js +12 -4
- package/lib/audit-sign.js +38 -13
- package/lib/audit-tools.js +4 -4
- package/lib/audit.js +8 -4
- package/lib/auth/jwt.js +3 -2
- package/lib/auth/lockout.js +3 -13
- package/lib/auth/oauth.js +27 -17
- package/lib/auth/passkey.js +1 -5
- package/lib/auth/password.js +23 -13
- package/lib/backup/bundle.js +2 -4
- package/lib/backup/crypto.js +8 -6
- package/lib/backup/index.js +18 -14
- package/lib/backup/manifest.js +6 -2
- package/lib/break-glass.js +12 -7
- package/lib/bundler.js +32 -21
- package/lib/cache-redis.js +8 -3
- package/lib/cache.js +39 -59
- package/lib/canonical-json.js +17 -2
- package/lib/chain-writer.js +2 -2
- package/lib/cli-helpers.js +9 -4
- package/lib/cli.js +27 -5
- package/lib/cluster-provider-db.js +2 -1
- package/lib/cluster-storage.js +71 -11
- package/lib/cluster.js +30 -25
- package/lib/config-drift.js +3 -4
- package/lib/constants.js +8 -3
- package/lib/cookies.js +16 -4
- package/lib/credential-hash.js +4 -7
- package/lib/crypto.js +9 -9
- package/lib/csv.js +11 -8
- package/lib/db-declare-row-policy.js +4 -9
- package/lib/db-declare-view.js +3 -6
- package/lib/db-query.js +9 -3
- package/lib/db-schema.js +75 -10
- package/lib/db.js +116 -70
- package/lib/deprecate.js +3 -8
- package/lib/dev.js +30 -19
- package/lib/dual-control.js +2 -4
- package/lib/error-page.js +32 -39
- package/lib/external-db-migrate.js +23 -26
- package/lib/external-db.js +11 -3
- package/lib/file-type.js +16 -8
- package/lib/forms.js +48 -12
- package/lib/framework-error.js +1 -2
- package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
- package/lib/handlers.js +1 -1
- package/lib/http-client-cookie-jar.js +25 -18
- package/lib/http-client.js +46 -23
- package/lib/i18n.js +6 -16
- package/lib/jobs.js +5 -1
- package/lib/log-stream-cloudwatch.js +31 -30
- package/lib/log-stream-local.js +12 -3
- package/lib/log-stream-otlp-grpc.js +29 -23
- package/lib/log-stream-otlp.js +13 -19
- package/lib/log-stream-syslog.js +27 -22
- package/lib/log-stream-webhook.js +8 -17
- package/lib/log-stream.js +13 -2
- package/lib/log.js +83 -5
- package/lib/mail-bounce.js +22 -15
- package/lib/mail-dkim.js +3 -6
- package/lib/mail.js +62 -34
- package/lib/metrics.js +61 -25
- package/lib/middleware/api-encrypt.js +15 -13
- package/lib/middleware/body-parser.js +46 -39
- package/lib/middleware/bot-guard.js +25 -4
- package/lib/middleware/compression.js +26 -11
- package/lib/middleware/cors.js +6 -5
- package/lib/middleware/csp-nonce.js +4 -3
- package/lib/middleware/csrf-protect.js +1 -1
- package/lib/middleware/db-role-for.js +7 -30
- package/lib/middleware/health.js +42 -8
- package/lib/middleware/index.js +55 -36
- package/lib/middleware/rate-limit.js +11 -8
- package/lib/middleware/request-id.js +10 -2
- package/lib/middleware/request-log.js +3 -2
- package/lib/middleware/require-auth.js +5 -2
- package/lib/middleware/sse.js +5 -4
- package/lib/migration-files.js +37 -0
- package/lib/migrations.js +26 -15
- package/lib/mtls-ca.js +38 -17
- package/lib/mtls-engine-default.js +15 -7
- package/lib/network-dns.js +49 -20
- package/lib/network-heartbeat.js +7 -7
- package/lib/network-nts.js +70 -48
- package/lib/network-proxy.js +34 -15
- package/lib/network-tls.js +7 -5
- package/lib/network.js +3 -3
- package/lib/notify.js +14 -36
- package/lib/ntp-check.js +7 -6
- package/lib/numeric-bounds.js +35 -3
- package/lib/object-store/azure-blob-bucket-ops.js +73 -15
- package/lib/object-store/azure-blob.js +31 -24
- package/lib/object-store/gcs-bucket-ops.js +41 -17
- package/lib/object-store/gcs.js +41 -34
- package/lib/object-store/http-put.js +2 -11
- package/lib/object-store/http-request.js +38 -0
- package/lib/object-store/index.js +1 -1
- package/lib/object-store/sigv4-bucket-ops.js +33 -37
- package/lib/object-store/sigv4.js +50 -30
- package/lib/observability.js +14 -3
- package/lib/otel-export.js +16 -14
- package/lib/pagination.js +23 -8
- package/lib/parsers/index.js +11 -5
- package/lib/parsers/safe-env.js +43 -14
- package/lib/parsers/safe-ini.js +29 -10
- package/lib/parsers/safe-toml.js +55 -25
- package/lib/parsers/safe-xml.js +22 -11
- package/lib/parsers/safe-yaml.js +55 -17
- package/lib/permissions.js +24 -33
- package/lib/pqc-agent.js +4 -2
- package/lib/pqc-gate.js +28 -15
- package/lib/protobuf-encoder.js +9 -3
- package/lib/protocol-dispatcher.js +29 -12
- package/lib/pubsub-redis.js +25 -8
- package/lib/pubsub.js +84 -12
- package/lib/queue-local.js +13 -4
- package/lib/queue-redis.js +2 -2
- package/lib/queue-sqs.js +21 -16
- package/lib/queue.js +8 -4
- package/lib/redis-client.js +13 -10
- package/lib/request-helpers.js +33 -2
- package/lib/restore-bundle.js +2 -4
- package/lib/restore-rollback.js +4 -1
- package/lib/restore.js +12 -10
- package/lib/retry.js +58 -9
- package/lib/router.js +96 -27
- package/lib/safe-async.js +97 -4
- package/lib/safe-buffer.js +47 -0
- package/lib/safe-json.js +18 -9
- package/lib/safe-schema.js +54 -18
- package/lib/safe-url.js +6 -2
- package/lib/scheduler.js +25 -14
- package/lib/security-assert.js +31 -26
- package/lib/seeders.js +71 -60
- package/lib/session.js +13 -7
- package/lib/slug.js +35 -8
- package/lib/ssrf-guard.js +58 -43
- package/lib/static.js +11 -7
- package/lib/storage.js +2 -1
- package/lib/template.js +17 -7
- package/lib/testing.js +28 -8
- package/lib/time.js +26 -5
- package/lib/totp.js +18 -9
- package/lib/tracing.js +15 -5
- package/lib/uuid.js +47 -20
- package/lib/validate-opts.js +213 -0
- package/lib/vault/index.js +36 -27
- package/lib/vault/rotate.js +41 -22
- package/lib/vault/wrap.js +26 -19
- package/lib/webhook.js +37 -68
- package/lib/websocket.js +60 -46
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
- package/lib/auth/totp.js +0 -17
- 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 =
|
|
47
|
-
var DEFAULT_MAX_CLIENTHELLO_BYTES =
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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();
|
package/lib/protobuf-encoder.js
CHANGED
|
@@ -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 /
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
137
|
-
deferred:
|
|
153
|
+
protocols: protocolNames,
|
|
154
|
+
deferred: deferredNames,
|
|
138
155
|
};
|
|
139
156
|
}
|
|
140
157
|
|
package/lib/pubsub-redis.js
CHANGED
|
@@ -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
|
|
25
|
-
var
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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(); }
|
|
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(); }
|
|
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
|
|
108
|
+
return pubsubCluster().create(opts);
|
|
103
109
|
}
|
|
104
110
|
if (requested === "redis") {
|
|
105
|
-
return
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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) {
|
package/lib/queue-local.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 ?
|
|
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,
|
package/lib/queue-redis.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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:
|
|
119
|
-
url:
|
|
120
|
-
headers:
|
|
121
|
-
payloadHash:
|
|
122
|
-
region:
|
|
123
|
-
service:
|
|
124
|
-
accessKeyId:
|
|
125
|
-
secretAccessKey:
|
|
126
|
-
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(
|
|
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
|
-
?
|
|
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
|
|
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 ||
|
|
184
|
-
var pollIntervalMs = opts.pollIntervalMs ||
|
|
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(); }
|
|
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 },
|