@blamejs/blamejs-shop 0.4.86 → 0.4.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/SECURITY.md +8 -0
  3. package/lib/asset-manifest.json +1 -1
  4. package/lib/cart.js +75 -0
  5. package/lib/storefront.js +8 -1
  6. package/lib/vendor/MANIFEST.json +53 -31
  7. package/lib/vendor/blamejs/.clusterfuzzlite/Dockerfile +7 -2
  8. package/lib/vendor/blamejs/.github/dependabot.yml +12 -0
  9. package/lib/vendor/blamejs/.github/workflows/ci.yml +16 -12
  10. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +3 -1
  11. package/lib/vendor/blamejs/.github/workflows/release-container.yml +23 -0
  12. package/lib/vendor/blamejs/CHANGELOG.md +6 -0
  13. package/lib/vendor/blamejs/api-snapshot.json +10 -2
  14. package/lib/vendor/blamejs/examples/wiki/Dockerfile +19 -2
  15. package/lib/vendor/blamejs/lib/atomic-file.js +32 -9
  16. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +105 -40
  17. package/lib/vendor/blamejs/lib/middleware/dpop.js +54 -31
  18. package/lib/vendor/blamejs/lib/middleware/span-http-server.js +7 -0
  19. package/lib/vendor/blamejs/lib/network-tls.js +12 -2
  20. package/lib/vendor/blamejs/lib/queue-local.js +123 -46
  21. package/lib/vendor/blamejs/lib/queue.js +13 -9
  22. package/lib/vendor/blamejs/lib/request-helpers.js +90 -0
  23. package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +69 -7
  24. package/lib/vendor/blamejs/lib/self-update.js +11 -2
  25. package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +11 -11
  26. package/lib/vendor/blamejs/lib/vendor/public-suffix-list.dat +6 -2
  27. package/lib/vendor/blamejs/lib/vendor/public-suffix-list.data.js +689 -688
  28. package/lib/vendor/blamejs/oss-fuzz/projects/blamejs/Dockerfile +5 -0
  29. package/lib/vendor/blamejs/package.json +1 -1
  30. package/lib/vendor/blamejs/release-notes/v0.15.17.json +52 -0
  31. package/lib/vendor/blamejs/release-notes/v0.15.18.json +49 -0
  32. package/lib/vendor/blamejs/release-notes/v0.15.19.json +18 -0
  33. package/lib/vendor/blamejs/scripts/check-vendor-currency.js +24 -0
  34. package/lib/vendor/blamejs/test/integration/queue-cluster-mysql.test.js +419 -0
  35. package/lib/vendor/blamejs/test/integration/queue-cluster-pg.test.js +471 -0
  36. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt-rejection-envelope.test.js +309 -0
  37. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +35 -8
  38. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-fd-read-errorfor-bypass.test.js +138 -0
  39. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +26 -0
  40. package/lib/vendor/blamejs/test/layer-0-primitives/dpop-htu-peergating.test.js +227 -0
  41. package/lib/vendor/blamejs/test/layer-0-primitives/queue-flow-repeat.test.js +83 -0
  42. package/lib/vendor/blamejs/test/layer-0-primitives/self-update-poll-asset-digest.test.js +90 -0
  43. package/lib/vendor/blamejs/test/layer-0-primitives/self-update-standalone-verifier-ecdsa-encoding.test.js +274 -0
  44. package/lib/vendor/blamejs/test/layer-0-primitives/tls-ocsp-freshness.test.js +192 -0
  45. package/lib/vendor/blamejs/test/layer-0-primitives/vendor-currency-classify.test.js +36 -0
  46. package/package.json +1 -1
@@ -144,33 +144,19 @@ function _nonceManager(rotateSec) {
144
144
  };
145
145
  }
146
146
 
147
- function _reconstructHtu(req, mopts) {
148
- // The proof's htu is the request URI WITHOUT query/fragment. Behind
149
- // a reverse proxy the operator may need to override via opts.htu /
150
- // opts.getHtu. X-Forwarded-* headers are ATTACKER-CONTROLLED when
151
- // the origin is reachable directly; an attacker who can hit the
152
- // origin while spoofing X-Forwarded-Proto: https can trick this
153
- // function into building an `https` htu that the DPoP proof was
154
- // signed for when the origin is actually serving HTTP. RFC 9449
155
- // §4.3 says htu MUST be the absolute URL the request was sent to.
156
- //
157
- // Default: ignore X-Forwarded-* and derive proto/host from the
158
- // socket. Operators with a confirmed-trusted front proxy opt in
159
- // via opts.trustForwardedHeaders: true.
160
- mopts = mopts || {};
161
- var trustForwarded = mopts.trustForwardedHeaders === true;
162
- var proto;
163
- if (trustForwarded && req.headers["x-forwarded-proto"]) {
164
- proto = String(req.headers["x-forwarded-proto"]).split(",")[0].trim();
165
- } else {
166
- proto = req.socket && req.socket.encrypted ? "https" : "http";
167
- }
168
- var host;
169
- if (trustForwarded && req.headers["x-forwarded-host"]) {
170
- host = String(req.headers["x-forwarded-host"]).split(",")[0].trim();
171
- } else {
172
- host = req.headers.host;
173
- }
147
+ function _reconstructHtu(req, protoResolver, hostResolver) {
148
+ // The proof's htu is the request URI WITHOUT query/fragment. Behind a
149
+ // reverse proxy the operator may override via opts.getHtu. RFC 9449 §4.3
150
+ // says htu MUST be the absolute URL the request was sent to — and it is
151
+ // cryptographically bound in the proof, so a forged scheme/authority lets a
152
+ // proof signed for one origin validate against another. proto + host are
153
+ // resolved through the peer-gated requestHelpers resolvers built in create():
154
+ // X-Forwarded-Proto / -Host are honored only from a declared trusted-proxy
155
+ // peer; otherwise the real TLS socket scheme + the request's own Host are
156
+ // used and forged forwarded headers are ignored.
157
+ if (!req || !req.headers) return null;
158
+ var proto = protoResolver.resolve(req);
159
+ var host = hostResolver.resolve(req);
174
160
  if (!host) return null;
175
161
  var path = req.url || "/";
176
162
  var qIdx = path.indexOf("?");
@@ -205,6 +191,9 @@ function _reconstructHtu(req, mopts) {
205
191
  * getAccessToken: function(req): string|null,
206
192
  * getNonce: async function(req): string|null,
207
193
  * getHtu: function(req): string,
194
+ * trustedProxies: string|string[], // CIDRs of your reverse proxies — peer-gates X-Forwarded-Proto + X-Forwarded-Host for htu reconstruction
195
+ * protocolResolver: function(req): "http"|"https", // own the scheme decision
196
+ * hostResolver: function(req): string|null, // own the authority decision
208
197
  * nonceStore: object,
209
198
  * nonceWindowSec: number,
210
199
  * nonceRotateSec: number,
@@ -228,9 +217,11 @@ function create(opts) {
228
217
  "replayStore", "algorithms", "iatWindowSec",
229
218
  "getAccessToken", "getNonce", "getHtu", "audit",
230
219
  "nonceStore", "nonceWindowSec", "nonceRotateSec", "requireNonce",
231
- // v0.9.4 opt-in trust gate for X-Forwarded-Proto/Host when
232
- // reconstructing htu. Default off; operators
233
- // with a confirmed-trusted front proxy set this to `true`.
220
+ // htu reconstruction trust. trustedProxies (CIDRs) peer-gates
221
+ // X-Forwarded-Proto + X-Forwarded-Host; protocolResolver/hostResolver let
222
+ // the operator own each. trustForwardedHeaders (legacy boolean) is refused
223
+ // on its own — see the peer-gating block below.
224
+ "trustedProxies", "protocolResolver", "hostResolver",
234
225
  "trustForwardedHeaders", "onDeny", "problemDetails",
235
226
  ], "middleware.dpop");
236
227
 
@@ -282,6 +273,38 @@ function create(opts) {
282
273
  validateOpts.optionalFunction(opts.getHtu,
283
274
  "middleware.dpop: getHtu", AuthError, "auth-dpop/bad-opt");
284
275
 
276
+ // htu reconstruction (RFC 9449 §4.3) builds the absolute request URL —
277
+ // proto + host — that the proof's cryptographically-bound `htu` claim is
278
+ // verified against. Behind a proxy both come from forgeable X-Forwarded-*
279
+ // headers, so resolve them through the peer-gated requestHelpers primitives
280
+ // (the same fail-closed model csrf-protect / security-headers / cors use):
281
+ // X-Forwarded-Proto / -Host are honored ONLY when the immediate peer is a
282
+ // declared trusted proxy. The legacy trustForwardedHeaders:true trusted the
283
+ // headers from ANY caller — a direct attacker could forge XFP:https / a
284
+ // victim XFH to make a proof signed for one origin validate against another
285
+ // (htu confusion). It is refused on its own; migrate to trustedProxies.
286
+ var _proto = requestHelpers.trustedProtocol({
287
+ trustedProxies: opts.trustedProxies,
288
+ protocolResolver: opts.protocolResolver,
289
+ });
290
+ var _host = requestHelpers.trustedHost({
291
+ trustedProxies: opts.trustedProxies,
292
+ hostResolver: opts.hostResolver,
293
+ });
294
+ // Only refuse the spoofable legacy flag when the htu is actually
295
+ // reconstructed from the request. When the operator supplies getHtu they own
296
+ // the entire URI, _reconstructHtu (and the forwarded headers) is never
297
+ // consulted, so a leftover trustForwardedHeaders is moot — don't fail
298
+ // construction on it (the error text even offers getHtu as a migration path).
299
+ if (typeof opts.getHtu !== "function" && opts.trustForwardedHeaders === true && !_proto.peerGated) {
300
+ throw new AuthError("auth-dpop/bad-opt",
301
+ "middleware.dpop: trustForwardedHeaders is spoofable for the htu reconstruction " +
302
+ "(a direct caller can forge X-Forwarded-Proto / X-Forwarded-Host) and is no longer " +
303
+ "honored on its own. Declare your reverse proxies via trustedProxies: [\"10.0.0.0/8\", …] " +
304
+ "(peer-gates X-Forwarded-Proto + X-Forwarded-Host), or own the decision via " +
305
+ "protocolResolver(req) / hostResolver(req) / getHtu(req).");
306
+ }
307
+
285
308
  function _freshNonce() { return nonceMgr ? nonceMgr.issue() : null; }
286
309
 
287
310
  var middleware = async function dpopMiddleware(req, res, next) {
@@ -309,7 +332,7 @@ function create(opts) {
309
332
  "multiple DPoP proofs in one header value are not allowed", null, onDeny, problemMode);
310
333
  }
311
334
 
312
- var htu = (typeof opts.getHtu === "function" ? opts.getHtu(req) : _reconstructHtu(req, opts));
335
+ var htu = (typeof opts.getHtu === "function" ? opts.getHtu(req) : _reconstructHtu(req, _proto, _host));
313
336
  if (!htu) {
314
337
  return _writeUnauthorized(req, res, "invalid_dpop_proof", "could not reconstruct htu", null, onDeny, problemMode);
315
338
  }
@@ -68,6 +68,11 @@ function _splitUrl(url) {
68
68
  }
69
69
 
70
70
  function _scheme(req) {
71
+ // Display-only: the OTel url.scheme span attribute reflects the scheme the
72
+ // client used (forwarded), NOT a Secure/HSTS/origin trust decision. Routing
73
+ // through trustedProtocol would drop the forwarded scheme from spans behind a
74
+ // proxy (less accurate telemetry) for no security gain.
75
+ // allow:raw-xfp — telemetry label, not a trust sink (see above).
71
76
  var x = req.headers && (req.headers["x-forwarded-proto"] || "");
72
77
  if (typeof x === "string" && x.length > 0) {
73
78
  var first = x.split(",")[0].trim().toLowerCase();
@@ -77,6 +82,8 @@ function _scheme(req) {
77
82
  }
78
83
 
79
84
  function _serverAddress(req) {
85
+ // allow:raw-xfp — display-only: server.address span attribute (telemetry),
86
+ // not an authority trust decision. Same rationale as _scheme above.
80
87
  var hostHeader = req.headers && (req.headers["x-forwarded-host"] || req.headers.host);
81
88
  if (typeof hostHeader === "string" && hostHeader.length > 0) {
82
89
  return hostHeader.split(",")[0].trim();
@@ -1187,8 +1187,18 @@ function evaluateOcspResponse(ocspDer, opts) {
1187
1187
  var clockSkewMs = typeof opts.clockSkewMs === "number" && opts.clockSkewMs >= 0 // allow:numeric-opt-Infinity — operator-supplied skew, default 5 min if absent or invalid
1188
1188
  ? opts.clockSkewMs : C.TIME.minutes(5);
1189
1189
  var now = typeof opts.now === "number" ? opts.now : Date.now();
1190
- var thisUpdateMs = match.thisUpdate ? Date.parse(match.thisUpdate) : NaN;
1191
- var nextUpdateMs = match.nextUpdate ? Date.parse(match.nextUpdate) : NaN;
1190
+ // thisUpdate / nextUpdate are already unix-ms NUMBERS (parseOcspResponse
1191
+ // _parseTime returns Date.UTC(...)). Do NOT Date.parse() them: Date.parse
1192
+ // coerces its argument to a string, and a bare-integer string is not a
1193
+ // recognized date format, so Date.parse(<number>) is NaN — which made the
1194
+ // !isFinite guard below reject every response (fresh or stale) with a
1195
+ // misleading "missing thisUpdate", leaving the real staleness window checks
1196
+ // (future thisUpdate, past nextUpdate) as unreachable dead code. Read the
1197
+ // numbers directly; the typeof guard keeps a legitimate epoch-0 thisUpdate
1198
+ // (Date.UTC(1970,0,1)===0, which is falsy) and maps a null nextUpdate to NaN
1199
+ // so the optional-field branch still no-ops.
1200
+ var thisUpdateMs = typeof match.thisUpdate === "number" ? match.thisUpdate : NaN;
1201
+ var nextUpdateMs = typeof match.nextUpdate === "number" ? match.nextUpdate : NaN;
1192
1202
  if (!isFinite(thisUpdateMs)) {
1193
1203
  return { ok: false, status: parsed.status, signatureValid: true,
1194
1204
  certStatus: match.certStatus,
@@ -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
- function _select() { return sql.select(ref.name, ref.opts); }
234
- function _insert() { return sql.insert(ref.name, ref.opts); }
235
- function _update() { return sql.update(ref.name, ref.opts); }
236
- function _delete() { return sql.delete(ref.name, ref.opts); }
237
- // Quoted column expression for a setRaw RHS that references the column's
238
- // own pre-update value (attempts/availableAt). dialect-sqlite quoting is
239
- // the double-quote form clusterStorage's Postgres path keeps.
240
- function _qc(col) { return safeSql.quoteIdentifier(col, "sqlite", { allowReserved: true }); }
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
- async function lease(queueName, leaseMs, count) {
328
- cluster.requireLeader();
329
- var nowMs = Date.now();
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
- var leaseBuilt = _update()
352
- .set("status", "inflight")
353
- .set("leasedAt", nowMs)
354
- .set("leaseExpiresAt", leaseExpiresAt)
355
- .setRaw("attempts", _qc("attempts") + " + 1", [])
356
- .whereIn("_id", leaseInner)
357
- .returning(LEASE_RETURN_COLS)
358
- .toSql();
359
- var result = await store.execute(leaseBuilt.sql, leaseBuilt.params);
360
- var leased = [];
361
- for (var i = 0; i < result.rows.length; i++) {
362
- leased.push(_shapeLeasedRow(result.rows[i]));
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 leased;
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. Writes the resolved
637
- // dependsOn jobIds and parks availableAt at MAX_SAFE_INTEGER for a flow
638
- // child that has dependencies. Lives on the backend (not in queue.js)
639
- // so it targets THIS backend's configured store + table — a
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
- // default jobs table behind the backend's back. depIds is serialized
643
- // to JSON for the dependsOn column.
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);
@@ -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 with their
942
- // names attached so the second pass can write dependsOn jobIds
943
- // resolved by name. Children with deps land at MAX_SAFE_INTEGER
944
- // availableAt automatically (see queue-local enqueue logic).
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
- // Hold off setting dependsOn until we know all sibling jobIds.
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
- // dependsOn intentionally omitted on first pass will be patched
958
- // in via the backend's patchFlowDeps after all jobIds are known.
959
- // Root children (no deps) are immediately leaseable; deps-bearing
960
- // children get parked at MAX_SAFE_INTEGER via the second pass.
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;
@@ -636,6 +636,94 @@ function requestProtocol(req, opts) {
636
636
  return "http";
637
637
  }
638
638
 
639
+ /**
640
+ * @primitive b.requestHelpers.trustedHost
641
+ * @signature b.requestHelpers.trustedHost(opts?)
642
+ * @since 0.15.18
643
+ * @related b.requestHelpers.requestHost, b.requestHelpers.trustedProtocol
644
+ *
645
+ * Peer-gated companion to trustedProtocol for the request authority (host).
646
+ * Reconstructing the absolute request URL — the DPoP `htu`, an origin/issuer
647
+ * string, a redirect base — depends on the host the client addressed; behind a
648
+ * proxy that comes from X-Forwarded-Host, which is forgeable unless the
649
+ * immediate peer is a trusted proxy. Returns `{ resolve(req)=>string|null,
650
+ * peerGated }`. With `trustedProxies` (CIDRs) X-Forwarded-Host is honored only
651
+ * from a trusted peer; with `hostResolver(req)` the operator owns it; with
652
+ * neither only the request's own Host header is used (forwarded host ignored).
653
+ *
654
+ * @opts
655
+ * trustedProxies: string | string[],
656
+ * hostResolver: function(req): string|null,
657
+ *
658
+ * @example
659
+ * var th = b.requestHelpers.trustedHost({ trustedProxies: ["10.0.0.0/8"] });
660
+ * th.resolve(req); // X-Forwarded-Host only when it came via a trusted peer
661
+ */
662
+ function trustedHost(opts) {
663
+ opts = opts || {};
664
+ var resolver = opts.hostResolver;
665
+ if (resolver != null && typeof resolver !== "function") {
666
+ throw new TypeError("trustedHost: hostResolver must be a function(req) => string|null");
667
+ }
668
+ var predicate = _trustedProxyPredicate(_normTrustedProxies(opts), "trustedHost");
669
+ return {
670
+ peerGated: !!(resolver || predicate),
671
+ resolve: function (req) {
672
+ if (resolver) return resolver(req);
673
+ if (predicate) return requestHost(req, { trustProxy: predicate });
674
+ return requestHost(req, { trustProxy: false });
675
+ },
676
+ };
677
+ }
678
+
679
+ /**
680
+ * @primitive b.requestHelpers.requestHost
681
+ * @signature b.requestHelpers.requestHost(req, opts?)
682
+ * @since 0.15.18
683
+ * @related b.requestHelpers.requestProtocol, b.requestHelpers.trustedHost
684
+ *
685
+ * Resolve the inbound authority (host[:port]). Default returns the request's
686
+ * own `Host` header. Behind a trusted reverse proxy that rewrites the host,
687
+ * pass `trustProxy` as a PREDICATE `function(addr)=>boolean` (build it via
688
+ * `b.requestHelpers.trustedHost`): `X-Forwarded-Host` is then honored only when
689
+ * the immediate peer is a trusted proxy, so a direct caller can't forge it. The
690
+ * legacy `trustProxy: true` reads the leftmost forwarded hop without checking
691
+ * the peer — forgeable. Returns the host string, or `null` when absent.
692
+ *
693
+ * @opts
694
+ * trustProxy: boolean | function // false (default) | predicate (peer-gated) | legacy true
695
+ *
696
+ * @example
697
+ * b.requestHelpers.requestHost({ headers: { host: "app.example.com" } });
698
+ * // → "app.example.com"
699
+ */
700
+ function requestHost(req, opts) {
701
+ if (!req || !req.headers) return null;
702
+ var trust = opts && opts.trustProxy;
703
+ if (trust) {
704
+ var fwd = req.headers["x-forwarded-host"];
705
+ if (typeof fwd === "string" && fwd.length > 0) {
706
+ var hops = parseListHeader(fwd);
707
+ if (hops.length > 0) {
708
+ if (typeof trust === "function") {
709
+ // Peer-gated: honor X-Forwarded-Host only when the immediate TCP peer
710
+ // is a trusted proxy. A direct caller's forged header is ignored —
711
+ // fall through to the request's own Host header.
712
+ var peer =
713
+ (req.socket && typeof req.socket.remoteAddress === "string" && req.socket.remoteAddress) ? req.socket.remoteAddress
714
+ : (req.connection && typeof req.connection.remoteAddress === "string" && req.connection.remoteAddress) ? req.connection.remoteAddress
715
+ : null;
716
+ if (peer && trust(peer)) return hops[0];
717
+ // peer not a trusted proxy → ignore forgeable header, fall through
718
+ } else {
719
+ return hops[0]; // legacy true — spoofable, see docstring
720
+ }
721
+ }
722
+ }
723
+ }
724
+ return typeof req.headers.host === "string" ? req.headers.host : null;
725
+ }
726
+
639
727
  // RFC 9110 §5.6.2 token grammar — letters, digits, and the
640
728
  // punctuation set `!#$%&'*+-.^_`|~`. Used by header-list parsers
641
729
  // that consume protocol tokens (Connection, Sec-WebSocket-Protocol,
@@ -1221,6 +1309,8 @@ module.exports = {
1221
1309
  ipPrefix: ipPrefix,
1222
1310
  requestProtocol: requestProtocol,
1223
1311
  trustedProtocol: trustedProtocol,
1312
+ requestHost: requestHost,
1313
+ trustedHost: trustedHost,
1224
1314
  appendVary: appendVary,
1225
1315
  // CVE-2026-21710 wrap — safe alternative to req.headersDistinct
1226
1316
  safeHeadersDistinct: safeHeadersDistinct,
@@ -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
- // P-384 IEEE-P1363 sigs are exactly 96 bytes (48-byte r || 48-byte s).
289
- // P-384 DER sigs are variable (~100-104 bytes ASN.1 SEQUENCE
290
- // wrapping two INTEGERs). Detect by length so we only call
291
- // verifier.verify ONCEcalling it a second time after a failed
292
- // verify returns stale state and silently passes tampered assets.
293
- // 96 = P-384 IEEE-P1363 signature length; protocol constant, not a byte-size.
294
- var dsaEncoding = signature.length === 96 ? "ieee-p1363" : "der"; // IEEE-P1363 P-384 signature length
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);
@@ -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