@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.
- package/CHANGELOG.md +6 -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 +27 -20
- 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 +17 -4
- 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 +42 -17
- 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/permissions.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
"'
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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 =
|
|
288
|
-
var auditSuccess =
|
|
289
|
-
var denyStatus =
|
|
290
|
-
var 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
|
-
|
|
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:
|
|
40
|
+
keepAliveMsecs: C.TIME.seconds(30),
|
|
41
41
|
maxSockets: 50,
|
|
42
|
-
|
|
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 =
|
|
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
|
|