@blamejs/core 0.15.15 → 0.15.17
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/lib/atomic-file.js +66 -9
- package/lib/auth/fido-mds3.js +10 -0
- package/lib/auth/password.js +1 -0
- package/lib/auth/saml.js +11 -3
- package/lib/daemon.js +4 -1
- package/lib/external-db.js +131 -0
- package/lib/graphql-federation.js +25 -15
- package/lib/log-stream-cloudwatch.js +1 -0
- package/lib/log-stream-local.js +14 -1
- package/lib/log-stream-otlp.js +1 -0
- package/lib/log-stream-webhook.js +1 -0
- package/lib/mail-auth.js +69 -14
- package/lib/mail-bimi.js +6 -0
- package/lib/mail-crypto-smime.js +10 -0
- package/lib/mail-dkim.js +68 -15
- package/lib/mail.js +39 -0
- package/lib/middleware/api-encrypt.js +111 -42
- package/lib/network-dns-resolver.js +61 -11
- package/lib/network-dns.js +47 -2
- package/lib/network-nts.js +16 -0
- package/lib/network-proxy.js +55 -2
- package/lib/object-store/azure-blob-bucket-ops.js +1 -0
- package/lib/object-store/gcs-bucket-ops.js +1 -0
- package/lib/object-store/http-request.js +4 -0
- package/lib/outbox.js +29 -0
- package/lib/queue-local.js +123 -46
- package/lib/queue-sqs.js +1 -0
- package/lib/queue.js +13 -9
- package/lib/request-helpers.js +25 -6
- package/lib/self-update-standalone-verifier.js +69 -7
- package/lib/self-update.js +11 -2
- package/lib/session-device-binding.js +46 -24
- package/lib/session.js +85 -28
- package/lib/vendor/MANIFEST.json +11 -11
- package/lib/vendor/public-suffix-list.dat +6 -2
- package/lib/vendor/public-suffix-list.data.js +689 -688
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/network-proxy.js
CHANGED
|
@@ -7,6 +7,7 @@ var nodeTls = require("node:tls");
|
|
|
7
7
|
|
|
8
8
|
var C = require("./constants");
|
|
9
9
|
var lazyRequire = require("./lazy-require");
|
|
10
|
+
var safeBuffer = require("./safe-buffer");
|
|
10
11
|
var safeUrl = require("./safe-url");
|
|
11
12
|
var validateOpts = require("./validate-opts");
|
|
12
13
|
var { defineClass } = require("./framework-error");
|
|
@@ -19,6 +20,23 @@ var IPV4_PREFIX_MAX_BITS = C.BYTES.bytes(32); // RFC 791 §3.1 IPv4 address bi
|
|
|
19
20
|
var DEFAULT_HTTPS_PORT = 443; // RFC 9110 §4.2.2
|
|
20
21
|
var DEFAULT_HTTP_PORT = C.BYTES.bytes(80); // RFC 9110 §4.2.1
|
|
21
22
|
|
|
23
|
+
// Bound the CONNECT-reply framing buffer (mirrors ws-client's handshake
|
|
24
|
+
// cap). A proxy that streams bytes without ever sending the CRLFCRLF
|
|
25
|
+
// header terminator would otherwise grow `buf` without limit and OOM the
|
|
26
|
+
// process; CONNECT replies are tiny, 64 KiB is generous headroom.
|
|
27
|
+
var TUNNEL_HEADER_MAX_BYTES = C.BYTES.kib(64);
|
|
28
|
+
// Wall-clock bound on the proxy connect + CONNECT handshake. Without it a
|
|
29
|
+
// proxy that accepts the socket but never replies (or trickles forever)
|
|
30
|
+
// hangs the tunnel indefinitely.
|
|
31
|
+
var TUNNEL_CONNECT_TIMEOUT_MS = C.TIME.seconds(30);
|
|
32
|
+
// Test-only override of the connect deadline so a unit test can prove the
|
|
33
|
+
// absolute-timeout behavior without a 30s wall-clock wait. null = use the
|
|
34
|
+
// real default. Cleared by _resetForTest.
|
|
35
|
+
var _testConnectTimeoutMs = null;
|
|
36
|
+
function _connectTimeoutMs() {
|
|
37
|
+
return typeof _testConnectTimeoutMs === "number" ? _testConnectTimeoutMs : TUNNEL_CONNECT_TIMEOUT_MS;
|
|
38
|
+
}
|
|
39
|
+
|
|
22
40
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
23
41
|
// Lazy so pqc-agent's TLS/audit graph isn't pulled into every process that
|
|
24
42
|
// imports network-proxy but never proxies an https upstream. Used only to audit
|
|
@@ -169,7 +187,27 @@ function _connectThroughTunnel(proxyUrl, targetHost, targetPort, callback) {
|
|
|
169
187
|
})
|
|
170
188
|
: net.connect({ host: proxyUrl.hostname, port: proxyPort });
|
|
171
189
|
var settled = false;
|
|
172
|
-
|
|
190
|
+
var connectDeadline = null;
|
|
191
|
+
function done(err, sock) {
|
|
192
|
+
if (settled) return;
|
|
193
|
+
settled = true;
|
|
194
|
+
if (connectDeadline) { try { clearTimeout(connectDeadline); } catch (_e) { /* best-effort */ } connectDeadline = null; }
|
|
195
|
+
callback(err, sock);
|
|
196
|
+
}
|
|
197
|
+
// ABSOLUTE wall-clock deadline on the proxy connect + CONNECT handshake. A
|
|
198
|
+
// proxy that accepts the socket but never sends the CRLFCRLF terminator —
|
|
199
|
+
// or trickles partial bytes just inside every idle window — must not hang
|
|
200
|
+
// the tunnel forever. socket.setTimeout is an IDLE timer that such a trickle
|
|
201
|
+
// resets indefinitely, so a fixed setTimeout (cleared in done()) enforces a
|
|
202
|
+
// real total-time bound regardless of how the bytes arrive.
|
|
203
|
+
var connectTimeoutMs = _connectTimeoutMs();
|
|
204
|
+
connectDeadline = setTimeout(function () {
|
|
205
|
+
done(new ProxyError("proxy/connect-timeout",
|
|
206
|
+
"proxy CONNECT to " + targetHost + ":" + targetPort + " timed out after " +
|
|
207
|
+
connectTimeoutMs + "ms"));
|
|
208
|
+
try { proxySocket.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
209
|
+
}, connectTimeoutMs);
|
|
210
|
+
if (connectDeadline && typeof connectDeadline.unref === "function") connectDeadline.unref();
|
|
173
211
|
proxySocket.on("error", function (e) { done(e); });
|
|
174
212
|
proxySocket.on(proxyUrl.protocol === "https:" ? "secureConnect" : "connect", function () {
|
|
175
213
|
if (proxyUrl.protocol === "https:") {
|
|
@@ -191,7 +229,18 @@ function _connectThroughTunnel(proxyUrl, targetHost, targetPort, callback) {
|
|
|
191
229
|
function onData(chunk) {
|
|
192
230
|
buf = Buffer.concat([buf, chunk]);
|
|
193
231
|
var idx = buf.indexOf("\r\n\r\n");
|
|
194
|
-
if (idx === -1)
|
|
232
|
+
if (idx === -1) {
|
|
233
|
+
// No header terminator yet — bound the accumulator so a proxy
|
|
234
|
+
// that trickles non-CRLFCRLF bytes forever can't OOM the process.
|
|
235
|
+
if (safeBuffer.byteLengthOf(buf) > TUNNEL_HEADER_MAX_BYTES) {
|
|
236
|
+
proxySocket.removeListener("data", onData);
|
|
237
|
+
done(new ProxyError("proxy/connect-headers-too-large",
|
|
238
|
+
"proxy CONNECT reply exceeded " + TUNNEL_HEADER_MAX_BYTES +
|
|
239
|
+
" bytes before CRLFCRLF"));
|
|
240
|
+
try { proxySocket.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
195
244
|
proxySocket.removeListener("data", onData);
|
|
196
245
|
var head = buf.slice(0, idx).toString("ascii");
|
|
197
246
|
var status = head.split("\r\n")[0] || "";
|
|
@@ -271,8 +320,11 @@ function snapshot() {
|
|
|
271
320
|
function _resetForTest() {
|
|
272
321
|
STATE.http = null; STATE.https = null; STATE.noProxy = []; STATE.auth = null;
|
|
273
322
|
STATE.agentCache.clear();
|
|
323
|
+
_testConnectTimeoutMs = null;
|
|
274
324
|
}
|
|
275
325
|
|
|
326
|
+
function _setConnectTimeoutForTest(ms) { _testConnectTimeoutMs = ms; }
|
|
327
|
+
|
|
276
328
|
module.exports = {
|
|
277
329
|
set: set,
|
|
278
330
|
fromEnv: fromEnv,
|
|
@@ -281,4 +333,5 @@ module.exports = {
|
|
|
281
333
|
snapshot: snapshot,
|
|
282
334
|
ProxyError: ProxyError,
|
|
283
335
|
_resetForTest: _resetForTest,
|
|
336
|
+
_setConnectTimeoutForTest: _setConnectTimeoutForTest,
|
|
284
337
|
};
|
|
@@ -36,6 +36,10 @@ function request(method, url, headers, body, opts) {
|
|
|
36
36
|
url: url,
|
|
37
37
|
headers: headers,
|
|
38
38
|
body: body,
|
|
39
|
+
// Both caps from the operator's configured timeout: timeoutMs bounds the
|
|
40
|
+
// whole request (no slow-trickle hold-open), idleTimeoutMs the zero-progress
|
|
41
|
+
// window. Undefined leaves httpClient's defaults unchanged.
|
|
42
|
+
timeoutMs: opts.timeoutMs,
|
|
39
43
|
idleTimeoutMs: opts.timeoutMs,
|
|
40
44
|
errorClass: opts.errorClass || ObjectStoreError,
|
|
41
45
|
allowedProtocols: opts.allowedProtocols,
|
package/lib/outbox.js
CHANGED
|
@@ -214,6 +214,35 @@ function create(opts) {
|
|
|
214
214
|
throw new OutboxError("outbox/bad-externaldb",
|
|
215
215
|
"outbox.create: externalDb must be the b.externalDb namespace (with transaction/query)");
|
|
216
216
|
}
|
|
217
|
+
// The outbox dual-write guarantee (enqueue runs in the SAME
|
|
218
|
+
// transaction as the operator's business write) is VOID on a
|
|
219
|
+
// stateless / autocommit-per-statement backend: BEGIN / the
|
|
220
|
+
// business write / the enqueue INSERT / COMMIT each land on a
|
|
221
|
+
// different session, so a crash or rollback can drop the business
|
|
222
|
+
// row while keeping the event (or vice versa). Refuse at create()
|
|
223
|
+
// when the backend declares it cannot provide interactive
|
|
224
|
+
// transactions, rather than constructing an outbox whose core
|
|
225
|
+
// guarantee silently does not hold. supportsTransactions may be a
|
|
226
|
+
// boolean (custom externalDb object) or a function (the
|
|
227
|
+
// b.externalDb namespace probe); only an explicit false refuses —
|
|
228
|
+
// an absent flag preserves the historical stateful assumption.
|
|
229
|
+
var cap = v.supportsTransactions;
|
|
230
|
+
var atomic = true;
|
|
231
|
+
if (typeof cap === "function") {
|
|
232
|
+
try { atomic = !!cap(); } catch (_e) { atomic = true; }
|
|
233
|
+
} else if (cap === false) {
|
|
234
|
+
atomic = false;
|
|
235
|
+
}
|
|
236
|
+
if (!atomic) {
|
|
237
|
+
throw new OutboxError("outbox/non-atomic-backend",
|
|
238
|
+
"outbox.create: externalDb backend cannot provide interactive " +
|
|
239
|
+
"transactions (supportsTransactions: false — a stateless / " +
|
|
240
|
+
"autocommit-per-statement adapter). The outbox dual-write guarantee " +
|
|
241
|
+
"requires the business write and the enqueue INSERT to commit " +
|
|
242
|
+
"atomically in one transaction; on this backend they would not. " +
|
|
243
|
+
"Supply interactive beginTx / commit / rollback hooks, or a batch " +
|
|
244
|
+
"adapter, on the externalDb backend first.");
|
|
245
|
+
}
|
|
217
246
|
},
|
|
218
247
|
table: function (v) {
|
|
219
248
|
validateOpts.requireNonEmptyString(v,
|
package/lib/queue-local.js
CHANGED
|
@@ -230,14 +230,28 @@ function create(config) {
|
|
|
230
230
|
// open each verb builder pre-bound to this table so the table reference
|
|
231
231
|
// is resolved in exactly one place.
|
|
232
232
|
var ref = _resolveTableRef(config);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
|
|
233
|
+
// Resolve the ACTIVE backend dialect every verb builds for — sqlite in
|
|
234
|
+
// single-node, the operator-configured postgres/mysql in cluster mode — so
|
|
235
|
+
// b.sql emits dialect-correct identifier quoting (backticks on MySQL, not
|
|
236
|
+
// double-quotes that MySQL reads as string literals) and its own dialect
|
|
237
|
+
// guards fire. The default store IS clusterStorage (it knows the live
|
|
238
|
+
// dialect); a bring-your-own store declares config.dialect (or exposes its
|
|
239
|
+
// own dialect()), defaulting to sqlite. Resolved per call (lazy) like the
|
|
240
|
+
// sibling clusterStorage data-layer files, since cluster.init may run after
|
|
241
|
+
// queue.create.
|
|
242
|
+
function _dialect() {
|
|
243
|
+
if (store === clusterStorage) return clusterStorage.dialect();
|
|
244
|
+
if (typeof store.dialect === "function") return store.dialect();
|
|
245
|
+
return (typeof config.dialect === "string" && config.dialect) ? config.dialect : "sqlite";
|
|
246
|
+
}
|
|
247
|
+
function _opts() { return Object.assign({}, ref.opts, { dialect: _dialect() }); }
|
|
248
|
+
function _select() { return sql.select(ref.name, _opts()); }
|
|
249
|
+
function _insert() { return sql.insert(ref.name, _opts()); }
|
|
250
|
+
function _update() { return sql.update(ref.name, _opts()); }
|
|
251
|
+
function _delete() { return sql.delete(ref.name, _opts()); }
|
|
252
|
+
// Quoted column expression for a setRaw RHS that references the column's own
|
|
253
|
+
// pre-update value (attempts/availableAt), quoted for the ACTIVE dialect.
|
|
254
|
+
function _qc(col) { return safeSql.quoteIdentifier(col, _dialect(), { allowReserved: true }); }
|
|
241
255
|
|
|
242
256
|
async function enqueue(queueName, payload, opts) {
|
|
243
257
|
cluster.requireLeader();
|
|
@@ -324,22 +338,9 @@ function create(config) {
|
|
|
324
338
|
};
|
|
325
339
|
}
|
|
326
340
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
var leaseExpiresAt = nowMs + leaseMs;
|
|
331
|
-
var maxRows = count != null ? count : 1;
|
|
332
|
-
|
|
333
|
-
// Single-statement atomic lease. The IN-subquery picks the head of
|
|
334
|
-
// the queue; the outer UPDATE locks those rows and only updates
|
|
335
|
-
// rows that still match status='pending' after the lock acquires
|
|
336
|
-
// (Postgres EvalPlanQual; SQLite is single-writer so the same row
|
|
337
|
-
// can't be picked twice). RETURNING hands back the leased columns
|
|
338
|
-
// so we don't need a separate SELECT after the UPDATE. maxRows is a
|
|
339
|
-
// framework-computed integer emitted inline via b.sql's .limit() (a
|
|
340
|
-
// bound LIMIT param has no portable form across the subquery path);
|
|
341
|
-
// attempts = attempts + 1 is a setRaw over the column's own value.
|
|
342
|
-
var leaseInner = _select()
|
|
341
|
+
// Build the head-of-queue candidate SELECT (shared by both lease paths).
|
|
342
|
+
function _leaseCandidates(queueName, nowMs, maxRows) {
|
|
343
|
+
return _select()
|
|
343
344
|
.columns(["_id"])
|
|
344
345
|
.where("queueName", queueName)
|
|
345
346
|
.where("status", "pending")
|
|
@@ -348,20 +349,87 @@ function create(config) {
|
|
|
348
349
|
.orderBy("availableAt", "asc")
|
|
349
350
|
.orderBy("enqueuedAt", "asc")
|
|
350
351
|
.limit(maxRows);
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
var
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function lease(queueName, leaseMs, count) {
|
|
355
|
+
cluster.requireLeader();
|
|
356
|
+
var nowMs = Date.now();
|
|
357
|
+
var leaseExpiresAt = nowMs + leaseMs;
|
|
358
|
+
var maxRows = count != null ? count : 1;
|
|
359
|
+
var dialect = _dialect();
|
|
360
|
+
var i;
|
|
361
|
+
|
|
362
|
+
// SQLite (single-writer): one self-contained statement is atomic. The
|
|
363
|
+
// IN-subquery picks the head of the queue, the outer UPDATE flips it, and
|
|
364
|
+
// RETURNING hands the leased rows back. This shape is valid ONLY on
|
|
365
|
+
// sqlite — Postgres freezes the materialized subquery qual (so a
|
|
366
|
+
// concurrent leaser double-claims the same row) and MySQL refuses both
|
|
367
|
+
// RETURNING and updating a table named in its own subquery (error 1093).
|
|
368
|
+
if (dialect === "sqlite") {
|
|
369
|
+
var leaseBuilt = _update()
|
|
370
|
+
.set("status", "inflight")
|
|
371
|
+
.set("leasedAt", nowMs)
|
|
372
|
+
.set("leaseExpiresAt", leaseExpiresAt)
|
|
373
|
+
.setRaw("attempts", _qc("attempts") + " + 1", [])
|
|
374
|
+
.whereIn("_id", _leaseCandidates(queueName, nowMs, maxRows))
|
|
375
|
+
.returning(LEASE_RETURN_COLS)
|
|
376
|
+
.toSql();
|
|
377
|
+
var result = await store.execute(leaseBuilt.sql, leaseBuilt.params);
|
|
378
|
+
var leased = [];
|
|
379
|
+
for (i = 0; i < result.rows.length; i++) leased.push(_shapeLeasedRow(result.rows[i]));
|
|
380
|
+
return leased;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Postgres / MySQL: claim inside a transaction. SELECT ... FOR UPDATE SKIP
|
|
384
|
+
// LOCKED row-locks the head-of-queue ids so concurrent leasers see
|
|
385
|
+
// disjoint sets (no double-lease); the guarded UPDATE ... WHERE
|
|
386
|
+
// status='pending' AND _id IN (locked ids) uses a LITERAL id list (not a
|
|
387
|
+
// self-referencing subquery — avoids MySQL 1093), with the status guard as
|
|
388
|
+
// belt-and-suspenders. Postgres reads the leased rows back via RETURNING;
|
|
389
|
+
// MySQL (no RETURNING) re-selects them by the locked ids. Mirrors
|
|
390
|
+
// outbox._claimBatch — the framework's canonical competing-consumer claim.
|
|
391
|
+
if (typeof store.transaction !== "function") {
|
|
392
|
+
throw _err("CLUSTER_TX_UNSUPPORTED",
|
|
393
|
+
"queue lease on a '" + dialect + "' backend requires an interactive transaction, but the " +
|
|
394
|
+
"configured store exposes no transaction(); use the default cluster store or supply a " +
|
|
395
|
+
"transaction-capable store", true);
|
|
363
396
|
}
|
|
364
|
-
return
|
|
397
|
+
return await store.transaction(async function (tx) {
|
|
398
|
+
var selBuilt = _leaseCandidates(queueName, nowMs, maxRows)
|
|
399
|
+
.forUpdate({ skipLocked: true })
|
|
400
|
+
.toSql();
|
|
401
|
+
var selRes = await tx.execute(selBuilt.sql, selBuilt.params);
|
|
402
|
+
var ids = ((selRes && selRes.rows) || []).map(function (r) { return r._id; });
|
|
403
|
+
if (ids.length === 0) return [];
|
|
404
|
+
|
|
405
|
+
var upd = _update()
|
|
406
|
+
.set("status", "inflight")
|
|
407
|
+
.set("leasedAt", nowMs)
|
|
408
|
+
.set("leaseExpiresAt", leaseExpiresAt)
|
|
409
|
+
.setRaw("attempts", _qc("attempts") + " + 1", [])
|
|
410
|
+
.where("status", "pending")
|
|
411
|
+
.whereInArray("_id", ids);
|
|
412
|
+
|
|
413
|
+
var rows;
|
|
414
|
+
if (dialect === "postgres") {
|
|
415
|
+
var updBuilt = upd.returning(LEASE_RETURN_COLS).toSql();
|
|
416
|
+
var updRes = await tx.execute(updBuilt.sql, updBuilt.params);
|
|
417
|
+
rows = (updRes && updRes.rows) || [];
|
|
418
|
+
} else {
|
|
419
|
+
var u = upd.toSql();
|
|
420
|
+
await tx.execute(u.sql, u.params);
|
|
421
|
+
var rbBuilt = _select()
|
|
422
|
+
.columns(LEASE_RETURN_COLS)
|
|
423
|
+
.where("status", "inflight")
|
|
424
|
+
.whereInArray("_id", ids)
|
|
425
|
+
.toSql();
|
|
426
|
+
var rbRes = await tx.execute(rbBuilt.sql, rbBuilt.params);
|
|
427
|
+
rows = (rbRes && rbRes.rows) || [];
|
|
428
|
+
}
|
|
429
|
+
var out = [];
|
|
430
|
+
for (i = 0; i < rows.length; i++) out.push(_shapeLeasedRow(rows[i]));
|
|
431
|
+
return out;
|
|
432
|
+
});
|
|
365
433
|
}
|
|
366
434
|
|
|
367
435
|
// extendLease — push the lease expiry forward for a long-running job.
|
|
@@ -633,19 +701,28 @@ function create(config) {
|
|
|
633
701
|
return result.rowCount || 0;
|
|
634
702
|
}
|
|
635
703
|
|
|
636
|
-
// patchFlowDeps — the second pass of enqueueFlow.
|
|
637
|
-
// dependsOn
|
|
638
|
-
//
|
|
639
|
-
// so it targets THIS backend's configured store +
|
|
640
|
-
// bring-your-own table receives the flow graph the same way the
|
|
641
|
-
// first-pass enqueue did, instead of the dispatcher writing to the
|
|
642
|
-
//
|
|
643
|
-
//
|
|
704
|
+
// patchFlowDeps — the second pass of enqueueFlow. Rewrites the child's
|
|
705
|
+
// dependsOn from the dependency NAMES the first pass wrote to the resolved
|
|
706
|
+
// sibling jobIds (now that every sibling's jobId is known). Lives on the
|
|
707
|
+
// backend (not in queue.js) so it targets THIS backend's configured store +
|
|
708
|
+
// table — a bring-your-own table receives the flow graph the same way the
|
|
709
|
+
// first-pass enqueue did, instead of the dispatcher writing to the default
|
|
710
|
+
// jobs table behind the backend's back. depIds is serialized to JSON.
|
|
711
|
+
//
|
|
712
|
+
// It must NOT touch availableAt: the first pass already parked the child at
|
|
713
|
+
// FLOW_BLOCKED_AVAILABLE_AT (it enqueues deps-bearing children WITH their
|
|
714
|
+
// dependsOn), and a dependency that completes in the window between the two
|
|
715
|
+
// passes drives complete() → _maybeReleaseFlowChildren, which bumps the
|
|
716
|
+
// child's availableAt to now. Re-parking here would clobber that release,
|
|
717
|
+
// and since the dependency is already done it never completes again — the
|
|
718
|
+
// child would sit pending-but-unleaseable forever. Parking is owned by the
|
|
719
|
+
// first-pass enqueue; releasing is owned by completion. This pass only
|
|
720
|
+
// resolves names → ids (harmless to rewrite even on an already-released
|
|
721
|
+
// child: a leased child's own dependsOn is never re-read).
|
|
644
722
|
async function patchFlowDeps(jobId, depIds) {
|
|
645
723
|
cluster.requireLeader();
|
|
646
724
|
var built = _update()
|
|
647
725
|
.set("dependsOn", JSON.stringify(depIds))
|
|
648
|
-
.set("availableAt", FLOW_BLOCKED_AVAILABLE_AT)
|
|
649
726
|
.where("_id", jobId)
|
|
650
727
|
.toSql();
|
|
651
728
|
var result = await store.execute(built.sql, built.params);
|
package/lib/queue-sqs.js
CHANGED
package/lib/queue.js
CHANGED
|
@@ -938,14 +938,16 @@ function enqueueFlow(spec) {
|
|
|
938
938
|
{ queueName: spec.queueName, flowId: flowId, childCount: spec.children.length },
|
|
939
939
|
async function () {
|
|
940
940
|
var jobs = [];
|
|
941
|
-
// Two-pass insert: first pass enqueues all children
|
|
942
|
-
//
|
|
943
|
-
//
|
|
944
|
-
//
|
|
941
|
+
// Two-pass insert: first pass enqueues all children (so the second pass
|
|
942
|
+
// can resolve dependsOn names → sibling jobIds); a deps-bearing child is
|
|
943
|
+
// PARKED at enqueue time by passing its dependsOn through, so it is never
|
|
944
|
+
// leaseable in the window between the two passes (a concurrent consumer
|
|
945
|
+
// could otherwise lease it before its deps run). The second pass only
|
|
946
|
+
// rewrites the parked child's dependsOn from names to the resolved jobIds.
|
|
945
947
|
var nameToJobId = {};
|
|
946
948
|
for (var p = 0; p < spec.children.length; p++) {
|
|
947
949
|
var ch = spec.children[p];
|
|
948
|
-
|
|
950
|
+
var hasDeps = Array.isArray(ch.dependsOn) && ch.dependsOn.length > 0;
|
|
949
951
|
var enqOpts = {
|
|
950
952
|
backend: flowBackend.name,
|
|
951
953
|
flowId: flowId,
|
|
@@ -954,10 +956,12 @@ function enqueueFlow(spec) {
|
|
|
954
956
|
classification: ch.classification || null,
|
|
955
957
|
traceId: ch.traceId || null,
|
|
956
958
|
maxAttempts: ch.maxAttempts,
|
|
957
|
-
//
|
|
958
|
-
//
|
|
959
|
-
//
|
|
960
|
-
//
|
|
959
|
+
// Park a deps-bearing child immediately (queue-local parks when
|
|
960
|
+
// opts.dependsOn is present); the second pass replaces these dep
|
|
961
|
+
// NAMES with the resolved sibling jobIds, keeping it parked until
|
|
962
|
+
// completion bumps availableAt. Root children (no deps) stay
|
|
963
|
+
// immediately leaseable.
|
|
964
|
+
dependsOn: hasDeps ? ch.dependsOn : undefined,
|
|
961
965
|
};
|
|
962
966
|
var result = await enqueue(spec.queueName, ch.payload, enqOpts);
|
|
963
967
|
nameToJobId[ch.name] = result.jobId;
|
package/lib/request-helpers.js
CHANGED
|
@@ -476,7 +476,7 @@ function _maskIpv6(ip, prefix) {
|
|
|
476
476
|
|
|
477
477
|
/**
|
|
478
478
|
* @primitive b.requestHelpers.ipPrefix
|
|
479
|
-
* @signature b.requestHelpers.ipPrefix(ip)
|
|
479
|
+
* @signature b.requestHelpers.ipPrefix(ip, opts?)
|
|
480
480
|
* @since 0.15.15
|
|
481
481
|
* @related b.requestHelpers.clientIp, b.requestHelpers.trustedClientIp
|
|
482
482
|
*
|
|
@@ -493,22 +493,41 @@ function _maskIpv6(ip, prefix) {
|
|
|
493
493
|
* public IP within a subnet don't log a user out). Exposed so an operator who
|
|
494
494
|
* drops to a function-form fingerprint field — for a custom mask width, or to
|
|
495
495
|
* combine the prefix with other signals — reuses this exact algorithm instead
|
|
496
|
-
* of re-deriving the /24 + /64 masking (and silently diverging).
|
|
496
|
+
* of re-deriving the /24 + /64 masking (and silently diverging). Pass
|
|
497
|
+
* <code>opts.v4Bits</code> / <code>opts.v6Bits</code> to override the mask
|
|
498
|
+
* widths (e.g. a device fingerprint that buckets at <code>/48</code> so a
|
|
499
|
+
* client roaming within its allocation but across a <code>/64</code> doesn't
|
|
500
|
+
* drift); an out-of-range or absent value falls back to the /24 + /64 default.
|
|
501
|
+
*
|
|
502
|
+
* @opts
|
|
503
|
+
* v4Bits: number, // IPv4 mask width in bits (default 24; valid 0..32)
|
|
504
|
+
* v6Bits: number, // IPv6 mask width in bits (default 64; valid 0..128)
|
|
497
505
|
*
|
|
498
506
|
* @example
|
|
499
507
|
* b.requestHelpers.ipPrefix("203.0.113.47"); // → "203.0.113.0/24"
|
|
500
508
|
* b.requestHelpers.ipPrefix("2001:db8::1"); // → "2001:db8:0:0/64"
|
|
501
509
|
*/
|
|
502
|
-
function
|
|
510
|
+
function _resolvePrefixBits(bits, def, max) {
|
|
511
|
+
if (typeof bits !== "number" || !isFinite(bits) || bits < 0 || bits > max) return def;
|
|
512
|
+
return bits;
|
|
513
|
+
}
|
|
514
|
+
function ipPrefix(ip, opts) {
|
|
503
515
|
if (typeof ip !== "string" || ip.length === 0) return "";
|
|
516
|
+
opts = opts || {};
|
|
517
|
+
// Configurable mask widths (default /24 + /64); an absent or out-of-range
|
|
518
|
+
// value falls back to the default. This preserves a caller's documented
|
|
519
|
+
// prefix width (e.g. a device fingerprint bucketing at /48) instead of
|
|
520
|
+
// silently forcing /64 — while still canonicalizing the address.
|
|
521
|
+
var v4 = _resolvePrefixBits(opts.v4Bits, IPV4_DEFAULT_PREFIX, 32); // IPv4 max prefix length in bits
|
|
522
|
+
var v6 = _resolvePrefixBits(opts.v6Bits, IPV6_DEFAULT_PREFIX, 128); // IPv6 max prefix length in bits
|
|
504
523
|
// IPv4-mapped IPv6 (::ffff:1.2.3.4) — strip the wrapper so the v4 mask
|
|
505
524
|
// applies. Same bucket regardless of how the proxy reported it.
|
|
506
525
|
var lower = ip.toLowerCase();
|
|
507
526
|
if (lower.indexOf(V4_MAPPED_V6_PREFIX) === 0 && lower.indexOf(".") !== -1) {
|
|
508
|
-
return _maskIpv4(lower.substring(V4_MAPPED_V6_PREFIX.length),
|
|
527
|
+
return _maskIpv4(lower.substring(V4_MAPPED_V6_PREFIX.length), v4) || "";
|
|
509
528
|
}
|
|
510
|
-
if (ip.indexOf(":") !== -1) return _maskIpv6(ip,
|
|
511
|
-
if (ip.indexOf(".") !== -1) return _maskIpv4(ip,
|
|
529
|
+
if (ip.indexOf(":") !== -1) return _maskIpv6(ip, v6) || "";
|
|
530
|
+
if (ip.indexOf(".") !== -1) return _maskIpv4(ip, v4) || "";
|
|
512
531
|
return "";
|
|
513
532
|
}
|
|
514
533
|
|
|
@@ -116,6 +116,49 @@ function _detectAlg(pubkeyPem) {
|
|
|
116
116
|
"(need ecdsa-p384, ed25519, or ml-dsa-87)");
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// _looksLikeDerEcdsa — true iff `sig` is a structurally-well-formed ASN.1
|
|
120
|
+
// DER ECDSA signature: SEQUENCE { INTEGER r, INTEGER s }. We dispatch the
|
|
121
|
+
// dsaEncoding by STRUCTURE, never by length: a P-384 DER signature whose r
|
|
122
|
+
// and s both encode short can total exactly 96 bytes — the same length as a
|
|
123
|
+
// raw IEEE-P1363 P-384 signature (48-byte r || 48-byte s) — so a length-only
|
|
124
|
+
// test mis-decodes the valid DER signature as raw and spuriously rejects an
|
|
125
|
+
// otherwise-valid update. This is a shape check only; cryptographic validity
|
|
126
|
+
// is still decided by the single verifier.verify() call below.
|
|
127
|
+
//
|
|
128
|
+
// DER layout: 0x30 <seqLen> 0x02 <rLen> <r...> 0x02 <sLen> <s...>, where each
|
|
129
|
+
// declared length must exactly frame the bytes that follow (definite-form,
|
|
130
|
+
// short or long encoding) and the SEQUENCE body must consume the whole sig.
|
|
131
|
+
function _readDerLen(buf, off) {
|
|
132
|
+
if (off >= buf.length) return null;
|
|
133
|
+
var first = buf[off];
|
|
134
|
+
if (first < 0x80) return { len: first, next: off + 1 }; // short form
|
|
135
|
+
var numBytes = first & 0x7f;
|
|
136
|
+
if (numBytes === 0 || numBytes > 4) return null; // indefinite / oversized: reject
|
|
137
|
+
if (off + 1 + numBytes > buf.length) return null;
|
|
138
|
+
var len = 0;
|
|
139
|
+
for (var i = 0; i < numBytes; i++) len = (len * 256) + buf[off + 1 + i];
|
|
140
|
+
return { len: len, next: off + 1 + numBytes };
|
|
141
|
+
}
|
|
142
|
+
function _readDerInteger(buf, off) {
|
|
143
|
+
if (off >= buf.length || buf[off] !== 0x02) return null; // INTEGER tag
|
|
144
|
+
var l = _readDerLen(buf, off + 1);
|
|
145
|
+
if (l === null || l.len === 0) return null;
|
|
146
|
+
var end = l.next + l.len;
|
|
147
|
+
if (end > buf.length) return null;
|
|
148
|
+
return { next: end };
|
|
149
|
+
}
|
|
150
|
+
function _looksLikeDerEcdsa(sig) {
|
|
151
|
+
if (sig.length < 8 || sig[0] !== 0x30) return false; // SEQUENCE tag
|
|
152
|
+
var seq = _readDerLen(sig, 1);
|
|
153
|
+
if (seq === null) return false;
|
|
154
|
+
if (seq.next + seq.len !== sig.length) return false; // body must frame to end
|
|
155
|
+
var r = _readDerInteger(sig, seq.next);
|
|
156
|
+
if (r === null) return false;
|
|
157
|
+
var s = _readDerInteger(sig, r.next);
|
|
158
|
+
if (s === null) return false;
|
|
159
|
+
return s.next === sig.length; // exactly two INTEGERs, no trailing bytes
|
|
160
|
+
}
|
|
161
|
+
|
|
119
162
|
/**
|
|
120
163
|
* @primitive b.selfUpdate.standaloneVerifier.verify
|
|
121
164
|
* @signature b.selfUpdate.standaloneVerifier.verify(assetPath, signaturePath, pubkeyPem)
|
|
@@ -285,13 +328,32 @@ function verify(assetPath, signaturePath, pubkeyPem) {
|
|
|
285
328
|
|
|
286
329
|
var ok = false;
|
|
287
330
|
if (alg === "ecdsa-p384") {
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
|
|
331
|
+
// Pick the ECDSA signature encoding by STRUCTURE, not by length. A P-384
|
|
332
|
+
// DER signature whose r and s both encode short can total exactly 96
|
|
333
|
+
// bytes — the same length as a raw IEEE-P1363 P-384 signature (48-byte r
|
|
334
|
+
// || 48-byte s) — so a length-only test mis-decodes the valid DER
|
|
335
|
+
// signature as raw and spuriously rejects an otherwise-valid update.
|
|
336
|
+
//
|
|
337
|
+
// A well-formed ASN.1 DER ECDSA signature is a SEQUENCE { INTEGER r,
|
|
338
|
+
// INTEGER s }; raw IEEE-P1363 is exactly 2*coordLen bytes (coordLen =
|
|
339
|
+
// 48 for the P-384 field). If it parses as DER, treat as DER; else if it
|
|
340
|
+
// is exactly 2*coordLen, treat as raw; else fail closed. We call
|
|
341
|
+
// verifier.verify() ONCE — a second call after a failed verify returns
|
|
342
|
+
// stale state and can silently pass tampered assets.
|
|
343
|
+
var coordLen = 48; // P-384 field element width in bytes; protocol constant, not a byte-size cap
|
|
344
|
+
var dsaEncoding;
|
|
345
|
+
if (_looksLikeDerEcdsa(signature)) {
|
|
346
|
+
dsaEncoding = "der";
|
|
347
|
+
} else if (signature.length === coordLen * 2) {
|
|
348
|
+
dsaEncoding = "ieee-p1363";
|
|
349
|
+
} else {
|
|
350
|
+
// assetFd was already closed by the read loop's `finally`; this is a
|
|
351
|
+
// pure fail-closed refusal, same as the `if (!ok)` path below.
|
|
352
|
+
throw new Error("standalone-verifier.verify: ecdsa-p384 signature is neither a " +
|
|
353
|
+
"well-formed DER SEQUENCE nor a raw " + (coordLen * 2) +
|
|
354
|
+
"-byte IEEE-P1363 pair (length " + signature.length +
|
|
355
|
+
") — refusing to guess the encoding");
|
|
356
|
+
}
|
|
295
357
|
ok = verifier.verify({ key: key, dsaEncoding: dsaEncoding }, signature);
|
|
296
358
|
} else if (alg === "ed25519") {
|
|
297
359
|
// fullBuf may be shorter than allocated (sparse files / size-races);
|
package/lib/self-update.js
CHANGED
|
@@ -297,6 +297,13 @@ function _matchAsset(name, pattern, fallback) {
|
|
|
297
297
|
* with conservative fallbacks. Throws SelfUpdateError on a non-2xx
|
|
298
298
|
* upstream, malformed JSON, or unexpected shape.
|
|
299
299
|
*
|
|
300
|
+
* Each matched asset / signature is reported as
|
|
301
|
+
* `{ name, url, size, digest }`. `digest` carries the release API's
|
|
302
|
+
* published `assets[].digest` (e.g. `"sha256:<hex>"`) verbatim when the
|
|
303
|
+
* upstream supplies it, or `null` when absent — a consumer can use it
|
|
304
|
+
* for a defense-in-depth in-flight integrity check of the downloaded
|
|
305
|
+
* bytes alongside the detached-signature verify.
|
|
306
|
+
*
|
|
300
307
|
* @opts
|
|
301
308
|
* releasesUrl: string, // required — feed URL
|
|
302
309
|
* currentVersion: string, // required — e.g. "0.8.43" or "v0.8.43"
|
|
@@ -446,11 +453,13 @@ async function poll(opts) {
|
|
|
446
453
|
var a = assets[i] || {};
|
|
447
454
|
if (typeof a.name !== "string" || typeof a.browser_download_url !== "string") continue;
|
|
448
455
|
if (signatureMatch === null && _matchAsset(a.name, opts.signaturePattern, /\.sig$|\.asc$|\.sig\.bin$/i)) {
|
|
449
|
-
signatureMatch = { name: a.name, url: a.browser_download_url, size: a.size || null
|
|
456
|
+
signatureMatch = { name: a.name, url: a.browser_download_url, size: a.size || null,
|
|
457
|
+
digest: typeof a.digest === "string" ? a.digest : null };
|
|
450
458
|
continue;
|
|
451
459
|
}
|
|
452
460
|
if (assetMatch === null && _matchAsset(a.name, opts.assetPattern, /\.(tar\.gz|tgz|zip|node|exe|bin)$/i)) {
|
|
453
|
-
assetMatch = { name: a.name, url: a.browser_download_url, size: a.size || null
|
|
461
|
+
assetMatch = { name: a.name, url: a.browser_download_url, size: a.size || null,
|
|
462
|
+
digest: typeof a.digest === "string" ? a.digest : null };
|
|
454
463
|
}
|
|
455
464
|
}
|
|
456
465
|
|