@blamejs/blamejs-shop 0.5.6 → 0.5.8

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 (28) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/cost-layers.js +86 -49
  4. package/lib/plan-changes.js +14 -0
  5. package/lib/storefront.js +30 -17
  6. package/lib/vendor/MANIFEST.json +31 -15
  7. package/lib/vendor/blamejs/.github/workflows/release-container.yml +2 -2
  8. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  9. package/lib/vendor/blamejs/api-snapshot.json +2 -2
  10. package/lib/vendor/blamejs/examples/wiki/package-lock.json +1 -1
  11. package/lib/vendor/blamejs/lib/content-credentials.js +43 -2
  12. package/lib/vendor/blamejs/lib/guard-sql.js +29 -0
  13. package/lib/vendor/blamejs/lib/mcp.js +37 -4
  14. package/lib/vendor/blamejs/lib/metrics.js +15 -3
  15. package/lib/vendor/blamejs/lib/middleware/tus-upload.js +9 -2
  16. package/lib/vendor/blamejs/lib/parsers/safe-yaml.js +33 -2
  17. package/lib/vendor/blamejs/lib/safe-buffer.js +11 -1
  18. package/lib/vendor/blamejs/package-lock.json +2 -2
  19. package/lib/vendor/blamejs/package.json +1 -1
  20. package/lib/vendor/blamejs/release-notes/v0.16.3.json +71 -0
  21. package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials-coverage.test.js +319 -0
  22. package/lib/vendor/blamejs/test/layer-0-primitives/cycle2-bugfixes.test.js +142 -0
  23. package/lib/vendor/blamejs/test/layer-0-primitives/guard-sql-coverage.test.js +432 -0
  24. package/lib/vendor/blamejs/test/layer-0-primitives/mcp-coverage.test.js +443 -0
  25. package/lib/vendor/blamejs/test/layer-0-primitives/metrics-coverage.test.js +395 -0
  26. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-tus-upload-coverage.test.js +402 -0
  27. package/lib/vendor/blamejs/test/layer-0-primitives/parsers-safe-yaml-coverage.test.js +292 -0
  28. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.5.x
10
10
 
11
+ - v0.5.8 (2026-07-03) — **Inventory cost-layer consumption and subscription plan-change execution are now correct under concurrent requests; vendored framework refreshed to 0.16.3.** Two ledger-atomicity fixes for concurrent requests, plus a vendored framework security refresh. Cost-layer (COGS) consumption debited each inventory layer with an unguarded read-modify-write against a snapshot, so two overlapping sales or write-offs of the same SKU could both draw from the same layer — driving a layer's remaining quantity negative, stranding a later layer, and understating cost of goods sold. Each layer debit is now an atomic guarded claim (the decrement only applies while the layer still holds the planned quantity); a losing claim reverts its own partial work and re-plans against a fresh snapshot, and the cost attribution rows are written only after every layer in the plan is claimed, so the combined consumption is exact regardless of interleaving. Separately, an immediate subscription plan change wrote its history row as executed before claiming the plan transition, so a double-submitted or retried change that lost the transition claim left a phantom executed row — carrying charge and credit amounts even though no plan moved, no invoice was recorded, and no credit was issued. The losing change now voids that row, so subscription history and any revenue rollup over executed changes reflect only changes that actually happened. The vendored framework is refreshed to 0.16.3, which redacts credential-shaped values in the rendered /metrics exposition (they were masked only in the internal cardinality key), closes a SQL-guard fragment-mode bypass for PostgreSQL dollar-quoted strings, and fails closed on MCP tool input whose JSON Schema omits an explicit object type — all inherited without shop code changes. **Changed:** *Vendored framework refreshed to 0.16.3* — Refreshes the framework the shop builds on from 0.16.2 to 0.16.3, a security and robustness patch across the observability, SQL-guard, MCP, YAML-parser, content-credentials, and resumable-upload primitives. Most consequential: a credential-shaped metric label value was redacted only in the internal cardinality key, not in the rendered exposition, so a raw token on a label leaked to every /metrics scrape — redaction now covers the rendered labels; and the SQL guard's raw-fragment escape hatch now rejects a PostgreSQL dollar-quoted string and a lone statement terminator, closing a bypass of the placeholder-binding floor. The minimum Node.js engine is unchanged at 24.18.0. **Fixed:** *Concurrent cost-layer consumption no longer double-debits a layer* — FIFO, LIFO, and weighted-average COGS consumption planned each layer debit from a snapshot and then applied an unconditional decrement, so two concurrent consumptions of the same SKU could both debit the same front layer — driving its remaining quantity negative and understating COGS while a later layer was left stranded. Each debit is now an atomic guarded claim that only applies while the layer still holds the planned quantity; a losing claim reverts the attempt's earlier claims and re-plans against a re-read of the active layers, within a bounded retry. Cost attribution rows are persisted only after every layer in the plan is claimed, so a mid-plan contention loss leaves no orphaned rows and the summed consumption and attributed cost are exact. · *A lost immediate plan-change claim voids its row instead of leaving a phantom executed change* — An immediate subscription plan change inserted its history row as executed and then claimed the plan transition with a conditional update. A double-submit or retry could have two changes both insert an executed row while only one wins the transition; the loser previously returned leaving a phantom executed change with a charge and credit amount even though no plan moved, no proration invoice was recorded, and no store credit was issued. The losing change now voids its row (cancelled, reason recorded), matching how the same path already handles a failed processor push, so history and any rollup over executed changes count only changes that actually settled.
12
+
13
+ - v0.5.7 (2026-07-03) — **Storefront redirect targets route through the framework open-redirect guard, closing a TAB-injection open redirect on return_to / redirect_to.** Closes an open-redirect (CWE-601) in the storefront's post-action redirects. The cookie-consent submit, currency and locale switchers, and the wishlist and compare toggles validated their return_to / redirect_to target with a hand-rolled leading-slash regex that accepted a horizontal TAB (0x09) at the second byte. A user agent strips ASCII TAB, CR, and LF from a URL before resolving it, so a target like /<TAB>/evil.example collapsed to the protocol-relative //evil.example and navigated the shopper off-origin under the shop's trust — a phishing bounce. Every request-derived redirect target now routes through the framework's b.safeRedirect.resolve primitive (exposed once as a shared same-origin helper), which rejects every control character including TAB, protocol-relative //host and \\host forms, and full URLs — only genuine same-origin relative paths survive; anything else falls back to a safe default. The two guards that already rejected control characters are consolidated onto the same primitive so there is one redirect-safety path to reason about, and a codebase check refuses any future hand-rolled leading-slash redirect guard. No operator action and no configuration change. **Security:** *Open-redirect on storefront return_to / redirect_to closed* — The post-action redirect guards (cookie consent, currency switch, locale switch, wishlist toggle, compare toggle, announcement dismiss) accepted a leading-slash target whose second byte was a control character. A horizontal TAB there is stripped by the browser before URL parsing, turning /<TAB>/evil.example into the protocol-relative //evil.example — an off-origin redirect an attacker could use to launder a phishing link through the shop's origin. All these targets now resolve through b.safeRedirect.resolve, which rejects control characters (TAB, CR, LF, NUL), protocol-relative and backslash forms, and off-origin absolute URLs; an invalid target falls back to a same-origin default. A codebase-patterns check prevents a hand-rolled leading-slash redirect guard from reappearing.
14
+
11
15
  - v0.5.6 (2026-07-03) — **Minimum Node is now 24.18.0; the vendored framework refresh brings a broad batch of request-path and mail-authentication security hardening.** Refreshes the vendored framework to 0.16.2 and raises the minimum supported Node.js to 24.18.0 — operators must run Node 24.18.0 or newer before upgrading. The floor moves with the framework, which requires 24.18.0 for its bundled SQLite 3.53.1, a backup-path fix, and the native OpenSSL 3.5 SLH-DSA surface. The refresh carries a large batch of security and correctness fixes that harden the request lifecycle the storefront and account portal already run on, at no code cost to the shop: the router refuses an encoded path separator or null byte and decodes the request path once, so a path-scoped security gate and the resource it guards can no longer disagree; res.redirect rejects a horizontal TAB that user agents strip before URL parsing, closing an open-redirect bypass; OAuth refresh-token and step-up replay gates treat any truthy store result as seen and bind grants to the authenticated principal; SAML Single-Logout fails closed when a verification key arrives without its algorithm; the mail-authentication builders (Authentication-Results, delivery-status, ARC) refuse CR/LF/NUL header injection and SPF evaluation fails closed on a malformed ip4 CIDR; file uploads enforce per-upload ownership and reject path-traversal ids; recursive parsers refuse pathologically deep input instead of overflowing the stack; and shared-cache counters (lockout, byte quotas, rate caps) accumulate with an atomic compare-and-set so distributed requests cannot lose increments. Every Node-version pin — engines, .nvmrc, the container base image, CI, and the docs — is synced to the new floor from the framework's own engines constraint. **Changed:** *Minimum Node.js raised to 24.18.0* — engines.node is now >=24.18.0 (was >=24.16.0). Operators must run Node 24.18.0 or newer. The floor is dictated by the vendored framework, which requires 24.18.0 for its bundled SQLite 3.53.1, a database backup-path fix, and the native OpenSSL 3.5 SLH-DSA sign/verify surface. The .nvmrc, the container base image, the CI runner pins, and the README/architecture docs are all synced to the new floor. · *Vendored framework refreshed to 0.16.2* — The framework the shop builds on is refreshed from 0.15.38 to 0.16.2. Beyond the request-path and mail hardening in this release's summary, the refresh includes: the router rejects percent-encoded path separators so a security middleware and the route it guards agree on the path; ARC and DKIM verification reject forged or body-length-truncated signatures; OCSP validation binds the response to the certificate's issuer; status-list verification binds the token type against replay; and OAuth authorization-URL builders refuse operator parameters that collide with framework-managed ones. These land in the request lifecycle the storefront and account portal already compose, with no shop code change required.
12
16
 
13
17
  - v0.5.5 (2026-06-27) — **Customers can change their subscription plan — proration-aware upgrades and downgrades from the account portal.** Adds a self-serve plan-change surface to the customer subscription portal. From /account/subscriptions a customer can switch an active subscription to any other plan in the same currency: the page previews the proration for each candidate plan and offers two timings. An immediate change applies now and prorates the rest of the current period — an upgrade is charged the prorated difference (recorded on the subscription invoice ledger), and a downgrade is issued the unused remainder as store credit. A next-billing-cycle change is queued and applied automatically when the period rolls over, with the new plan starting clean and no proration. The transition is exactly-once: the immediate path claims the plan switch atomically before recording the charge or credit, and the scheduled path claims each due change with a single guarded update, so a retry or an overlapping scheduler run cannot double-charge or double-credit. A downgrade that owes a credit is never applied without paying it — if the credit write fails the change rolls back and retries. The surface mounts automatically when subscriptions and store credit are composed (the default for a configured storefront); the scheduler runs on the existing cron, and the tables it uses already shipped, so there are no schema changes and no operator action is required. **Added:** *Self-serve subscription plan changes with proration* — Customers can change the plan on an active subscription from /account/subscriptions/:id/change. The page lists every plan in the subscription's currency (excluding the current and archived plans) with a proration preview, and offers an immediate switch or a switch at the next billing date. An immediate upgrade charges the prorated difference for the rest of the period and records it on the subscription invoice ledger; an immediate downgrade issues the unused remainder of the outgoing plan as store credit. A still-pending change is shown on the subscription with a one-click cancel. Cross-currency, same-plan, and archived-plan requests are refused server-side, and the surface enforces subscription ownership before any read or write. · *Scheduler applies next-billing-cycle changes automatically* — A change queued for the next billing cycle is applied without operator involvement: a minute-cadence tick flips every due change to its new plan, records the proration, and issues any owed credit. Each due change is claimed with a single guarded update so an overlapping or retried run cannot apply it twice, and a change that owes a credit is left queued rather than applied if no credit can be issued — the credit is never dropped. The scheduler is inert on a deploy without subscriptions or store credit composed. **Changed:** *Dunning step failures are logged with their cause* — When a dunning step (reminder send, retry-charge record, pause, or cancel) fails, the schedule still advances and the step is recorded as failed — but the underlying error is now written to the server log instead of being discarded, so an operator can see why a step failed rather than only that it did.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.5.6",
2
+ "version": "0.5.8",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-imfe0otYErcB8rr2h6KLSGTtStirysptpXETSPY4zLv3bZoIT75Lo1dOvkOav+xL",
@@ -388,6 +388,21 @@ function create(opts) {
388
388
 
389
389
  var method = await _methodFor(sku);
390
390
  var direction = method === "lifo" ? "lifo" : "fifo";
391
+ // Consuming is a check-then-write against a snapshot of the active
392
+ // layers, so it runs under a bounded claim-retry loop. Each planned
393
+ // layer debit is an ATOMIC guarded UPDATE (`... AND quantity_remaining
394
+ // >= take`); the cost attribution rows are written only after EVERY
395
+ // layer in the plan has been claimed. If a concurrent consumer moved a
396
+ // layer since the snapshot, the losing attempt reverts its own winning
397
+ // claims — a compensating add restores exactly what it subtracted — and
398
+ // re-reads. Without the guard, two concurrent consumes of the same SKU
399
+ // both debit the same front layer(s) from a stale snapshot, driving
400
+ // quantity_remaining negative and misstating COGS.
401
+ var MAX_CLAIM_ATTEMPTS = 8;
402
+ var consumed, totalCogs, currency;
403
+ var attributionTs = requested;
404
+
405
+ for (var attempt = 1; ; attempt += 1) {
391
406
  var layers = await _activeLayers(sku, direction);
392
407
 
393
408
  // Pre-flight: confirm on-hand >= requested. No partial
@@ -403,7 +418,7 @@ function create(opts) {
403
418
  // same currency. Mixed currencies are refused at the primitive
404
419
  // layer (the operator reconciles via a manual receipt rather
405
420
  // than getting a mysteriously-FX-blended COGS row).
406
- var currency = layers[0].currency;
421
+ currency = layers[0].currency;
407
422
  for (var c = 1; c < layers.length; c += 1) {
408
423
  if (layers[c].currency !== currency) {
409
424
  throw new TypeError("cost-layers.consumeForSale: layers for sku " +
@@ -412,14 +427,11 @@ function create(opts) {
412
427
  }
413
428
  }
414
429
 
415
- // The sale's `occurred_at` is the operator-supplied timestamp
416
- // (defaulting to now when omitted). `cogsForPeriod` filters
417
- // attribution rows by this column using the sale time, not
418
- // the wall-clock at write time, keeps backfilled imports
419
- // accurate.
420
- var consumed = [];
421
- var totalCogs = 0;
422
- var attributionTs = requested;
430
+ // Build the debit PLAN for this attempt without writing anything.
431
+ // Each entry: { layer_id, take, attrs: [{ qty, unit_cost_minor }] }.
432
+ // `planCogs` is the line's total COGS in minor units.
433
+ var plan = [];
434
+ var planCogs = 0;
423
435
 
424
436
  if (method === "weighted_average") {
425
437
  // Compute weighted-average unit cost in minor units. Round
@@ -493,11 +505,6 @@ function create(opts) {
493
505
  if (attrCogs > residual) attrCogs = residual;
494
506
  residual -= attrCogs;
495
507
  }
496
- // Debit the layer by the full consumed quantity (one update).
497
- await query(
498
- "UPDATE cost_layers SET quantity_remaining = quantity_remaining - ?1 WHERE id = ?2",
499
- [dRow.qty, dRow.layer.id],
500
- );
501
508
  // Attribute the debit's cost so `qty * unit_cost_minor` summed over
502
509
  // its attribution rows reconstructs attrCogs EXACTLY — the COGS
503
510
  // reports (cogsForOrder / cogsForPeriod) recover each line's cost
@@ -513,60 +520,90 @@ function create(opts) {
513
520
  var floorUnit = dRow.qty > 0 ? Math.floor(attrCogs / dRow.qty) : avgUnitCost;
514
521
  var rem = dRow.qty > 0 ? attrCogs - (floorUnit * dRow.qty) : 0;
515
522
  var parts = [];
516
- if (dRow.qty - rem > 0) parts.push({ qty: dRow.qty - rem, unit: floorUnit });
517
- if (rem > 0) parts.push({ qty: rem, unit: floorUnit + 1 });
518
- for (var p = 0; p < parts.length; p += 1) {
519
- var attrId = b.uuid.v7();
520
- await query(
521
- "INSERT INTO cogs_attributions " +
522
- "(id, order_id, line_id, sku, layer_id, qty, unit_cost_minor, " +
523
- "currency, reversed, reversal_reason, occurred_at) " +
524
- "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 0, NULL, ?9)",
525
- [attrId, orderId, lineId, sku, dRow.layer.id, parts[p].qty, parts[p].unit, currency, attributionTs],
526
- );
527
- consumed.push({
528
- layer_id: dRow.layer.id,
529
- qty: parts[p].qty,
530
- unit_cost_minor: parts[p].unit,
531
- });
532
- }
533
- totalCogs += attrCogs;
523
+ if (dRow.qty - rem > 0) parts.push({ qty: dRow.qty - rem, unit_cost_minor: floorUnit });
524
+ if (rem > 0) parts.push({ qty: rem, unit_cost_minor: floorUnit + 1 });
525
+ plan.push({ layer_id: dRow.layer.id, take: dRow.qty, attrs: parts });
534
526
  }
535
- // Edge case: residual unspent (impossible given the math
536
- // above, but defensive). Push it onto the last attribution.
537
- if (residual !== 0 && consumed.length) {
538
- totalCogs += residual;
539
- }
540
- totalCogs = lineCogs;
527
+ planCogs = lineCogs;
541
528
  } else {
542
- // FIFO / LIFO: walk layers in order, debit each one until
543
- // the requested quantity is satisfied.
529
+ // FIFO / LIFO: walk layers in order, planning a debit against each
530
+ // one until the requested quantity is satisfied.
544
531
  var remaining = quantity;
545
532
  for (var f = 0; f < layers.length && remaining > 0; f += 1) {
546
533
  var lay = layers[f];
547
534
  var take = lay.quantity_remaining;
548
535
  if (take > remaining) take = remaining;
536
+ plan.push({
537
+ layer_id: lay.id,
538
+ take: take,
539
+ attrs: [{ qty: take, unit_cost_minor: lay.unit_cost_minor }],
540
+ });
541
+ planCogs += take * lay.unit_cost_minor;
542
+ remaining -= take;
543
+ }
544
+ }
545
+
546
+ // Claim every planned layer atomically. A guarded decrement loses (0
547
+ // rows) when a concurrent consumer has drawn the layer below `take`
548
+ // since this attempt's snapshot was read.
549
+ var claimed = [];
550
+ var lostClaim = false;
551
+ for (var g = 0; g < plan.length; g += 1) {
552
+ var claimRes = await query(
553
+ "UPDATE cost_layers SET quantity_remaining = quantity_remaining - ?1 " +
554
+ "WHERE id = ?2 AND quantity_remaining >= ?1",
555
+ [plan[g].take, plan[g].layer_id],
556
+ );
557
+ if (b.sql.casWon(claimRes).won) {
558
+ claimed.push(plan[g]);
559
+ } else {
560
+ lostClaim = true;
561
+ break;
562
+ }
563
+ }
564
+
565
+ if (lostClaim) {
566
+ // Revert this attempt's winning claims — a compensating add restores
567
+ // exactly what was subtracted — then re-read the layers and re-plan.
568
+ for (var rv = 0; rv < claimed.length; rv += 1) {
549
569
  await query(
550
- "UPDATE cost_layers SET quantity_remaining = quantity_remaining - ?1 WHERE id = ?2",
551
- [take, lay.id],
570
+ "UPDATE cost_layers SET quantity_remaining = quantity_remaining + ?1 WHERE id = ?2",
571
+ [claimed[rv].take, claimed[rv].layer_id],
552
572
  );
553
- var attrIdF = b.uuid.v7();
573
+ }
574
+ if (attempt >= MAX_CLAIM_ATTEMPTS) {
575
+ throw new Error("cost-layers.consumeForSale: layer contention on sku " +
576
+ JSON.stringify(sku) + " — claim retries exhausted after " + attempt + " attempts");
577
+ }
578
+ continue;
579
+ }
580
+
581
+ // Every planned layer was claimed — persist the attribution rows. The
582
+ // row set (layer / qty / unit_cost_minor) is identical to the pre-fix
583
+ // interleaved write; only the ordering (claim-all, then insert-all)
584
+ // changed so a lost mid-plan claim reverts cleanly with no orphan rows.
585
+ consumed = [];
586
+ totalCogs = planCogs;
587
+ for (var pj = 0; pj < plan.length; pj += 1) {
588
+ for (var pa = 0; pa < plan[pj].attrs.length; pa += 1) {
589
+ var attrRow = plan[pj].attrs[pa];
590
+ var attrRowId = b.uuid.v7();
554
591
  await query(
555
592
  "INSERT INTO cogs_attributions " +
556
593
  "(id, order_id, line_id, sku, layer_id, qty, unit_cost_minor, " +
557
594
  "currency, reversed, reversal_reason, occurred_at) " +
558
595
  "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 0, NULL, ?9)",
559
- [attrIdF, orderId, lineId, sku, lay.id, take, lay.unit_cost_minor, currency, attributionTs],
596
+ [attrRowId, orderId, lineId, sku, plan[pj].layer_id, attrRow.qty, attrRow.unit_cost_minor, currency, attributionTs],
560
597
  );
561
598
  consumed.push({
562
- layer_id: lay.id,
563
- qty: take,
564
- unit_cost_minor: lay.unit_cost_minor,
599
+ layer_id: plan[pj].layer_id,
600
+ qty: attrRow.qty,
601
+ unit_cost_minor: attrRow.unit_cost_minor,
565
602
  });
566
- totalCogs += take * lay.unit_cost_minor;
567
- remaining -= take;
568
603
  }
569
604
  }
605
+ break;
606
+ } // end bounded claim-retry loop
570
607
 
571
608
  return {
572
609
  consumed_layers: consumed,
@@ -635,6 +635,20 @@ function create(opts) {
635
635
  periodEnd: sub.current_period_end,
636
636
  planChangeId: id,
637
637
  });
638
+ } else {
639
+ // Lost the transition claim — a concurrent executeChange (double-submit
640
+ // or retry) already flipped this subscription's plan. This row was
641
+ // inserted 'executed' before the claim, but no plan moved, no credit
642
+ // was issued, and no invoice was recorded under it. Void it rather than
643
+ // leave a phantom 'executed' history row that carries money columns yet
644
+ // reflects nothing — a naive revenue rollup
645
+ // (SUM(first_charge_minor) WHERE status='executed') would double-count
646
+ // it. Mirrors the stripe-push-failed / credit-settlement-failed voids.
647
+ await query(
648
+ "UPDATE subscription_plan_changes SET status = 'cancelled', cancelled_at = ?1, " +
649
+ "cancel_reason = ?2 WHERE id = ?3",
650
+ [_now(), "transition-lost", id],
651
+ );
638
652
  }
639
653
 
640
654
  return await _getById(id);
package/lib/storefront.js CHANGED
@@ -59,6 +59,25 @@ var _localeAls = new AsyncLocalStorage();
59
59
  // the client only ever sees a generic message + the correlating id.
60
60
  var _log = b.log.create({});
61
61
 
62
+ // Same-origin redirect-target guard shared by every post-action redirect
63
+ // (return_to / redirect_to / next). Delegates to the framework open-redirect
64
+ // (CWE-601) primitive rather than a hand-rolled path regex: it rejects
65
+ // protocol-relative targets ("//host", "\\host"), refuses full URLs (no
66
+ // base/allowlist is passed, so only same-origin relatives survive), and
67
+ // rejects EVERY control char including a horizontal TAB. A hand-rolled
68
+ // `/^\/[^/]/` guard misses the TAB: a user agent strips ASCII TAB/CR/LF from
69
+ // a URL before resolving it, so a `Location: /\t/evil.example` collapses to
70
+ // the protocol-relative `//evil.example` and navigates off-origin. Returns
71
+ // the safe same-origin path (query/fragment preserved) or `fallback` — pass
72
+ // "" as the fallback to use it as an accepted/rejected sentinel.
73
+ function _sameOriginRedirect(raw, fallback) {
74
+ var fb = typeof fallback === "string" ? fallback : "/";
75
+ // A pathological over-length target (header bloat) falls back before the
76
+ // primitive even parses it.
77
+ if (typeof raw === "string" && raw.length > 2048) return fb;
78
+ return b.safeRedirect.resolve(raw, { fallback: fb });
79
+ }
80
+
62
81
  // Generic 500 for an auth/ceremony route: log the real error server-side
63
82
  // (correlated by the framework request id) and return a fixed message to
64
83
  // the client so no internal error string (stack frame, DB column, vault
@@ -12485,10 +12504,7 @@ function mount(router, deps) {
12485
12504
  router.post("/announcements/:slug/dismiss", async function (req, res) {
12486
12505
  var slug = (req.params && typeof req.params.slug === "string") ? req.params.slug : "";
12487
12506
  var body = req.body || {};
12488
- var to = (typeof body.return_to === "string") ? body.return_to : "/";
12489
- if (to.charAt(0) !== "/" || to.charAt(1) === "/" || to.indexOf("\\") !== -1 || /[\x00-\x1f\x7f]/.test(to)) {
12490
- to = "/";
12491
- }
12507
+ var to = _sameOriginRedirect(body.return_to, "/");
12492
12508
  if (/^[a-z0-9-]{1,64}$/.test(slug)) {
12493
12509
  // Durable record (best-effort — keyed on the cart session id; absent
12494
12510
  // a session the cookie still carries the dismissal for this browser).
@@ -13125,13 +13141,9 @@ function mount(router, deps) {
13125
13141
  // setting a cookie, so a hostile link can't 500 the route.
13126
13142
  router.get("/locale", function (req, res) {
13127
13143
  var q = req.query || {};
13128
- var to = (typeof q.to === "string") ? q.to : "/";
13129
- // Same-origin path only: must start with a single "/" (not "//" — a
13130
- // protocol-relative URL and not a scheme). Anything else falls
13131
- // back to the home path.
13132
- if (to.charAt(0) !== "/" || to.charAt(1) === "/" || to.indexOf("\\") !== -1 || /[\x00-\x1f\x7f]/.test(to)) {
13133
- to = "/";
13134
- }
13144
+ // Same-origin path only (rejects protocol-relative "//", backslash, and
13145
+ // control chars including TAB); anything else falls back to home.
13146
+ var to = _sameOriginRedirect(q.to, "/");
13135
13147
  var lang = (typeof q.lang === "string") ? q.lang : "";
13136
13148
  if (lang && LOCALE_COOKIE_RE.test(lang) && lang.length <= LOCALE_COOKIE_MAX && supportedTags.indexOf(lang) !== -1) {
13137
13149
  _setLocaleCookie(res, lang);
@@ -13898,7 +13910,7 @@ function mount(router, deps) {
13898
13910
  // slash, no scheme / host / protocol-relative `//`) so the switcher
13899
13911
  // can't be turned into an open redirect.
13900
13912
  var rawTo = typeof body.redirect_to === "string" ? body.redirect_to : "/";
13901
- var to = (/^\/(?!\/)/.test(rawTo)) ? rawTo : "/";
13913
+ var to = _sameOriginRedirect(rawTo, "/");
13902
13914
  if (chosen === _ccyBase || _ccyOptions.indexOf(chosen) === -1) {
13903
13915
  _clearCurrencyCookie(res);
13904
13916
  } else {
@@ -14445,7 +14457,8 @@ function mount(router, deps) {
14445
14457
  // failure still lands the shopper somewhere sane.
14446
14458
  var rt = (req.body || {}).return_to;
14447
14459
  var dest = null;
14448
- if (typeof rt === "string" && /^\/[^/]/.test(rt)) dest = rt;
14460
+ var safeRt = _sameOriginRedirect(rt, "");
14461
+ if (safeRt) dest = safeRt;
14449
14462
 
14450
14463
  var notice;
14451
14464
  try {
@@ -17801,10 +17814,11 @@ function mount(router, deps) {
17801
17814
  var okKind = removed ? "removed" : "added";
17802
17815
  var rt = (req.body || {}).return_to;
17803
17816
  var dest;
17804
- if (typeof rt === "string" && /^\/[^/]/.test(rt)) {
17817
+ var safeRt = _sameOriginRedirect(rt, "");
17818
+ if (safeRt) {
17805
17819
  // Only thread the success marker when returning to the wishlist
17806
17820
  // page (the PDP heart toggle is its own visible cue).
17807
- dest = rt.indexOf("/account/wishlist") === 0 ? "/account/wishlist?ok=" + okKind : rt;
17821
+ dest = safeRt.indexOf("/account/wishlist") === 0 ? "/account/wishlist?ok=" + okKind : safeRt;
17808
17822
  } else {
17809
17823
  var product = null;
17810
17824
  try { product = await deps.catalog.products.get(productId); } catch (_e) { product = null; }
@@ -22131,8 +22145,7 @@ function mount(router, deps) {
22131
22145
  // refused), capping length defensively. Anything else collapses to the
22132
22146
  // safe default the caller passes.
22133
22147
  function _consentReturnTo(raw, fallback) {
22134
- if (typeof raw === "string" && raw.length <= 512 && /^\/[^/]/.test(raw)) return raw;
22135
- return fallback;
22148
+ return _sameOriginRedirect(raw, fallback);
22136
22149
  }
22137
22150
 
22138
22151
  // Translate a posted choice + per-category checkboxes into the four
@@ -3,8 +3,8 @@
3
3
  "_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
4
4
  "packages": {
5
5
  "blamejs": {
6
- "version": "0.16.2",
7
- "tag": "v0.16.2",
6
+ "version": "0.16.3",
7
+ "tag": "v0.16.3",
8
8
  "license": "Apache-2.0",
9
9
  "author": "blamejs contributors",
10
10
  "source": "https://github.com/blamejs/blamejs",
@@ -838,6 +838,7 @@
838
838
  "release-notes/v0.16.0.json": "lib/vendor/blamejs/release-notes/v0.16.0.json",
839
839
  "release-notes/v0.16.1.json": "lib/vendor/blamejs/release-notes/v0.16.1.json",
840
840
  "release-notes/v0.16.2.json": "lib/vendor/blamejs/release-notes/v0.16.2.json",
841
+ "release-notes/v0.16.3.json": "lib/vendor/blamejs/release-notes/v0.16.3.json",
841
842
  "release-notes/v0.2.x.json": "lib/vendor/blamejs/release-notes/v0.2.x.json",
842
843
  "release-notes/v0.3.x.json": "lib/vendor/blamejs/release-notes/v0.3.x.json",
843
844
  "release-notes/v0.4.x.json": "lib/vendor/blamejs/release-notes/v0.4.x.json",
@@ -1100,6 +1101,7 @@
1100
1101
  "test/layer-0-primitives/config-drift.test.js": "lib/vendor/blamejs/test/layer-0-primitives/config-drift.test.js",
1101
1102
  "test/layer-0-primitives/config.test.js": "lib/vendor/blamejs/test/layer-0-primitives/config.test.js",
1102
1103
  "test/layer-0-primitives/consent-purposes.test.js": "lib/vendor/blamejs/test/layer-0-primitives/consent-purposes.test.js",
1104
+ "test/layer-0-primitives/content-credentials-coverage.test.js": "lib/vendor/blamejs/test/layer-0-primitives/content-credentials-coverage.test.js",
1103
1105
  "test/layer-0-primitives/content-credentials.test.js": "lib/vendor/blamejs/test/layer-0-primitives/content-credentials.test.js",
1104
1106
  "test/layer-0-primitives/content-digest.test.js": "lib/vendor/blamejs/test/layer-0-primitives/content-digest.test.js",
1105
1107
  "test/layer-0-primitives/cors.test.js": "lib/vendor/blamejs/test/layer-0-primitives/cors.test.js",
@@ -1132,6 +1134,7 @@
1132
1134
  "test/layer-0-primitives/csrf-protect.test.js": "lib/vendor/blamejs/test/layer-0-primitives/csrf-protect.test.js",
1133
1135
  "test/layer-0-primitives/csv.test.js": "lib/vendor/blamejs/test/layer-0-primitives/csv.test.js",
1134
1136
  "test/layer-0-primitives/cwt.test.js": "lib/vendor/blamejs/test/layer-0-primitives/cwt.test.js",
1137
+ "test/layer-0-primitives/cycle2-bugfixes.test.js": "lib/vendor/blamejs/test/layer-0-primitives/cycle2-bugfixes.test.js",
1135
1138
  "test/layer-0-primitives/daemon.test.js": "lib/vendor/blamejs/test/layer-0-primitives/daemon.test.js",
1136
1139
  "test/layer-0-primitives/daily-byte-quota.test.js": "lib/vendor/blamejs/test/layer-0-primitives/daily-byte-quota.test.js",
1137
1140
  "test/layer-0-primitives/dane.test.js": "lib/vendor/blamejs/test/layer-0-primitives/dane.test.js",
@@ -1235,6 +1238,7 @@
1235
1238
  "test/layer-0-primitives/guard-saga-config.test.js": "lib/vendor/blamejs/test/layer-0-primitives/guard-saga-config.test.js",
1236
1239
  "test/layer-0-primitives/guard-smtp-command.test.js": "lib/vendor/blamejs/test/layer-0-primitives/guard-smtp-command.test.js",
1237
1240
  "test/layer-0-primitives/guard-snapshot-envelope.test.js": "lib/vendor/blamejs/test/layer-0-primitives/guard-snapshot-envelope.test.js",
1241
+ "test/layer-0-primitives/guard-sql-coverage.test.js": "lib/vendor/blamejs/test/layer-0-primitives/guard-sql-coverage.test.js",
1238
1242
  "test/layer-0-primitives/guard-stream-args.test.js": "lib/vendor/blamejs/test/layer-0-primitives/guard-stream-args.test.js",
1239
1243
  "test/layer-0-primitives/guard-svg.test.js": "lib/vendor/blamejs/test/layer-0-primitives/guard-svg.test.js",
1240
1244
  "test/layer-0-primitives/guard-tenant-id.test.js": "lib/vendor/blamejs/test/layer-0-primitives/guard-tenant-id.test.js",
@@ -1324,12 +1328,15 @@
1324
1328
  "test/layer-0-primitives/mail-store.test.js": "lib/vendor/blamejs/test/layer-0-primitives/mail-store.test.js",
1325
1329
  "test/layer-0-primitives/mail-unsubscribe.test.js": "lib/vendor/blamejs/test/layer-0-primitives/mail-unsubscribe.test.js",
1326
1330
  "test/layer-0-primitives/mail.test.js": "lib/vendor/blamejs/test/layer-0-primitives/mail.test.js",
1331
+ "test/layer-0-primitives/mcp-coverage.test.js": "lib/vendor/blamejs/test/layer-0-primitives/mcp-coverage.test.js",
1327
1332
  "test/layer-0-primitives/mcp-tool-registry.test.js": "lib/vendor/blamejs/test/layer-0-primitives/mcp-tool-registry.test.js",
1328
1333
  "test/layer-0-primitives/mcp.test.js": "lib/vendor/blamejs/test/layer-0-primitives/mcp.test.js",
1329
1334
  "test/layer-0-primitives/mdoc.test.js": "lib/vendor/blamejs/test/layer-0-primitives/mdoc.test.js",
1335
+ "test/layer-0-primitives/metrics-coverage.test.js": "lib/vendor/blamejs/test/layer-0-primitives/metrics-coverage.test.js",
1330
1336
  "test/layer-0-primitives/metrics-shadow-registry.test.js": "lib/vendor/blamejs/test/layer-0-primitives/metrics-shadow-registry.test.js",
1331
1337
  "test/layer-0-primitives/metrics-snapshot.test.js": "lib/vendor/blamejs/test/layer-0-primitives/metrics-snapshot.test.js",
1332
1338
  "test/layer-0-primitives/middleware-compose-pipeline.test.js": "lib/vendor/blamejs/test/layer-0-primitives/middleware-compose-pipeline.test.js",
1339
+ "test/layer-0-primitives/middleware-tus-upload-coverage.test.js": "lib/vendor/blamejs/test/layer-0-primitives/middleware-tus-upload-coverage.test.js",
1333
1340
  "test/layer-0-primitives/mime-parse.test.js": "lib/vendor/blamejs/test/layer-0-primitives/mime-parse.test.js",
1334
1341
  "test/layer-0-primitives/money.test.js": "lib/vendor/blamejs/test/layer-0-primitives/money.test.js",
1335
1342
  "test/layer-0-primitives/mtls-ca-paths.test.js": "lib/vendor/blamejs/test/layer-0-primitives/mtls-ca-paths.test.js",
@@ -1367,6 +1374,7 @@
1367
1374
  "test/layer-0-primitives/output-header-hardening.test.js": "lib/vendor/blamejs/test/layer-0-primitives/output-header-hardening.test.js",
1368
1375
  "test/layer-0-primitives/pagination.test.js": "lib/vendor/blamejs/test/layer-0-primitives/pagination.test.js",
1369
1376
  "test/layer-0-primitives/parser-verify-hardening.test.js": "lib/vendor/blamejs/test/layer-0-primitives/parser-verify-hardening.test.js",
1377
+ "test/layer-0-primitives/parsers-safe-yaml-coverage.test.js": "lib/vendor/blamejs/test/layer-0-primitives/parsers-safe-yaml-coverage.test.js",
1370
1378
  "test/layer-0-primitives/parsers-standalone.test.js": "lib/vendor/blamejs/test/layer-0-primitives/parsers-standalone.test.js",
1371
1379
  "test/layer-0-primitives/passkey-real-vectors.test.js": "lib/vendor/blamejs/test/layer-0-primitives/passkey-real-vectors.test.js",
1372
1380
  "test/layer-0-primitives/passkey.test.js": "lib/vendor/blamejs/test/layer-0-primitives/passkey.test.js",
@@ -1552,7 +1560,7 @@
1552
1560
  ".github/workflows/ci.yml": "sha256:c9cd093403b410d14f6cf61bfc864b8862cc0d8f5c8dbf01a979881a4204fe83",
1553
1561
  ".github/workflows/codeql.yml": "sha256:9622f111e4da9028dc94507fe5a7c9ee4cd19dfcfbffeed1edbefde284133fea",
1554
1562
  ".github/workflows/npm-publish.yml": "sha256:cac26b6d72101e882a59d8d25fb6977973e1f8d67c87b491297712fca479b93d",
1555
- ".github/workflows/release-container.yml": "sha256:17ccd9081d26e69d258efeb37ba135e68949b7d4e1f4444697071ac57ccee75e",
1563
+ ".github/workflows/release-container.yml": "sha256:89c6722c30f1a69f2367086f46eeb213fe384f0aa44a44e81f64866f4c19a6e2",
1556
1564
  ".github/workflows/scorecard.yml": "sha256:8a3a8a25eafe6e7c965983fe550a3e7a945386a5e9e88341c801b10b504d94be",
1557
1565
  ".github/workflows/sha-to-tag-verify.yml": "sha256:6ed546bdb518514c79a69aa9afe87c92903a8663afb57d4d07b5e81fbda8b844",
1558
1566
  ".github/zizmor.yml": "sha256:fe08e66c5763dfb4edf1ea33e715f04e514b1baceb3a242241d8a60793db51d2",
@@ -1562,7 +1570,7 @@
1562
1570
  ".npmrc": "sha256:66f104e7d07c496d2d0409988225e8c0e4ceb8d247dbcac3be75b2128d20ce66",
1563
1571
  ".pinact.yaml": "sha256:0213ffda55961dc49b64c0a5dfa3c0567419633b1499d57eaf7c8d842d7da6c7",
1564
1572
  "ARCHITECTURE.md": "sha256:9b1c8d2b1b7a41838eb348b0a008e4b4369718fd72bfe2974b37155f7536d35b",
1565
- "CHANGELOG.md": "sha256:62fb356e0451a9500a2e74e1cfc49bdfacf99c579c0ead43b9282f22603c736c",
1573
+ "CHANGELOG.md": "sha256:4fa997c021b50ee725c66753a8ee6ea86b26fa4233101dd9070d5702b1adaed9",
1566
1574
  "CODE_OF_CONDUCT.md": "sha256:148a281960fff7c2fe6554dab66da572c72245ddeb00b0d14811558397bff386",
1567
1575
  "CONTRIBUTING.md": "sha256:3e4575c3cd57e72317773a80faabcabb611133ace5ee523db4b8d631e80bdaa8",
1568
1576
  "GOVERNANCE.md": "sha256:906df6afb1f552b27b9acb50f7f96c47b917a2f1021cd4e987dbf4ee0e0a821b",
@@ -1573,7 +1581,7 @@
1573
1581
  "README.md": "sha256:e1c6d401d81c959a0f264d73b3640a26465921f0ceb1103be6936816d99d21b4",
1574
1582
  "ROADMAP.md": "sha256:d66be1de21603794608395c57ecc17381b673452ea74159bd805c85684821ecd",
1575
1583
  "SECURITY.md": "sha256:e47271ef5094205c0ce38c10c8674c42a59b7210311f0e27ecc028c7a387c2cc",
1576
- "api-snapshot.json": "sha256:626f0081f7985e9d85b9135eec545a12aeb950c9e7983e706b9dc24488057a55",
1584
+ "api-snapshot.json": "sha256:765b6929a2fde973e67fb49fd49942d4053eabd6e858b0b5f3942cf06e3400ec",
1577
1585
  "assets/BlameJS_Logo.png": "sha256:3c65699753c771b48ef9ac7f45bb40815ec19a23afcdd0cd30ef4601bbbe293e",
1578
1586
  "assets/BlameJS_Logo.svg": "sha256:dda44f3fb1343d5de9db6b1fcdb75fc649c57e7a99a8e8239fcf852e3841e1a8",
1579
1587
  "bench/README.md": "sha256:74202f2507fd840bfc1ac6c681975d9273cf36cca6e0f72655f138337304033c",
@@ -1632,7 +1640,7 @@
1632
1640
  "examples/wiki/lib/source-doc-parser.js": "sha256:e39a1701dbe6940987b11e9318e535039e8a5d1dbc67ba7b7e6bac4ef87ef638",
1633
1641
  "examples/wiki/lib/symbol-index.js": "sha256:abd133b4aa8e582cf72b7c92fafe7f342b2d06d1a9f3d1082eb0c4df8a743b0e",
1634
1642
  "examples/wiki/migrations/0001-pages-schema.js": "sha256:b4459ee6d1d4f1130dbf0c7a22b39ffa0f972a7ca3a0ef19215abe4bf35e46a5",
1635
- "examples/wiki/package-lock.json": "sha256:66cf7211967af7f63784a84db80287b63a7ccd4db69fdc400af16986753f459e",
1643
+ "examples/wiki/package-lock.json": "sha256:ceb2632eea43f0b167da990fee70ae16dbd24577673c0f5e0f9c3c0781ce1170",
1636
1644
  "examples/wiki/package.json": "sha256:8efb9ee3012107f9631ce23e7846ff2b10cadf643ec449e7940de496728aaedb",
1637
1645
  "examples/wiki/public/img/blamejs-logo.png": "sha256:3c65699753c771b48ef9ac7f45bb40815ec19a23afcdd0cd30ef4601bbbe293e",
1638
1646
  "examples/wiki/public/img/blamejs-logo.svg": "sha256:dda44f3fb1343d5de9db6b1fcdb75fc649c57e7a99a8e8239fcf852e3841e1a8",
@@ -1904,7 +1912,7 @@
1904
1912
  "lib/config.js": "sha256:7f28de45331b39de8bb3b9281ef0641c4b3fdf928088a22fdb51ecac2a1c21d7",
1905
1913
  "lib/consent.js": "sha256:0eb93f281d05ac35895cd1e0b8bf7c0cf121bf6c72901f844b52028c6bd3a14c",
1906
1914
  "lib/constants.js": "sha256:31897caa2604b53c5365e9e054373b29c753c226beefd3a47e515a9f441c105c",
1907
- "lib/content-credentials.js": "sha256:d548860bb5c658698509c7245df8e6c117a855bfdd2f43a1566c1eb3f2a333ce",
1915
+ "lib/content-credentials.js": "sha256:df200fe8945fcbf4783c81a52b552dc9fc5630fdc4674275a1521f88175bbeb8",
1908
1916
  "lib/content-digest.js": "sha256:ae9052c1d3f54a314c3d25b2e45a173459efc12a470b5b632b8c31db0e89c5ff",
1909
1917
  "lib/cookies.js": "sha256:5230ca525e8d618a0cb9df607235b5fd194b2138915e8089dc4ea3824aa92675",
1910
1918
  "lib/cose.js": "sha256:89c8672c0a5a019bc70e7889f4441a8ba6a6a3a52d288f0515199982046810cc",
@@ -2014,7 +2022,7 @@
2014
2022
  "lib/guard-shell.js": "sha256:bc866a05afba17004ce5ce7497d884d0a59fbec3aa8b81c76840eeddd2c3b9a3",
2015
2023
  "lib/guard-smtp-command.js": "sha256:a761c4f5d5d8f5083339e095dca2ede035330b760735bcf76693b9e6484ab4ae",
2016
2024
  "lib/guard-snapshot-envelope.js": "sha256:13eab8a92ef38c9d0a58adf30844a0a41030e9d0b67e447480a2d21c6db052ff",
2017
- "lib/guard-sql.js": "sha256:1f2a18e1f7e603377d898900eef0636d49f2a6c2f7a2d8fd746982e5b25c201f",
2025
+ "lib/guard-sql.js": "sha256:fef254470a15a7b9be78300c8fa86d5dd26ae88f654f5230f5f3051597f6f108",
2018
2026
  "lib/guard-stream-args.js": "sha256:3d20d6a57639c38fb406b40540d4bd738387ec2f1eca3e63c3a586a75527e61a",
2019
2027
  "lib/guard-svg.js": "sha256:c9fbca850273d43fddaf6232949b963fbe48cfa73068540fb30aee8ce8d6f017",
2020
2028
  "lib/guard-template.js": "sha256:861a566fd25a7abaf541c0e7875f8dc30ae5b34832ee81ee37f1ee26ae96b58b",
@@ -2107,9 +2115,9 @@
2107
2115
  "lib/markup-escape.js": "sha256:7cb287780b9bb8a5ab37b1f8c36427df432331def15e2ef4d63c4b29809357ee",
2108
2116
  "lib/markup-tokenizer.js": "sha256:b02048184fb1a55e410cdf15049935b3f1ab4895c595b0bd2a6589bcdab9f87e",
2109
2117
  "lib/mcp-tool-registry.js": "sha256:57722b488a0977704997acbb61d2fad13155040007516f996b6b574c64d482ee",
2110
- "lib/mcp.js": "sha256:d914512443b33bbe047f68e1ae5358ac5b9a1b25f6801f2bc360adbea1b857dc",
2118
+ "lib/mcp.js": "sha256:d0d40b1ee3cc4d66d249f6ad15ba5e1bdb0db66c689a940206c25a1d51a2c694",
2111
2119
  "lib/mdoc.js": "sha256:536d6ad327e15f11f3279781f93a779bffeddfec06222b0e601728f905561d21",
2112
- "lib/metrics.js": "sha256:f0f677ac7b2c82ab89059ed2d1bbe580a84bea0e93f6ae37990314a123a3e503",
2120
+ "lib/metrics.js": "sha256:f12e30587a8a2c7f74be480dbb7c8f5e11a8e2ddca644cb88a225367168b241e",
2113
2121
  "lib/middleware/age-gate.js": "sha256:ef9113093151494eca51bbcd60bd334aa2e8865969e2f3ad34cc6d8ce180fc25",
2114
2122
  "lib/middleware/ai-act-disclosure.js": "sha256:e6f5bb9aab37bcec5ecef2ec5b3688ed0eb069eb61279d297e1ddd8460bc4568",
2115
2123
  "lib/middleware/api-encrypt.js": "sha256:48c59f52ffc9b05e38c58e193e2df743b62c3f80ac61336bf2c4abade18437a7",
@@ -2164,7 +2172,7 @@
2164
2172
  "lib/middleware/sse.js": "sha256:5bf672e6a9f2988719b820ed8d67bda34b9b7a4d54d170620614ed24bc688bfc",
2165
2173
  "lib/middleware/trace-log-correlation.js": "sha256:abf2339adddc2a2b3f61b26e9c1cb6ebffae5fc02ca51bf9ee002df99dfe622f",
2166
2174
  "lib/middleware/trace-propagate.js": "sha256:ddfebd2947bca77be6023fd4833bd80f8d03bef7ca2d63c7fc27f447108cd451",
2167
- "lib/middleware/tus-upload.js": "sha256:bc9a7d002f36b3d121d606e4b2f1b683461f003f6d33f6e3f44c5c99d10effac",
2175
+ "lib/middleware/tus-upload.js": "sha256:0e85b893fdd1e081a3d32fd6a1e47ddf56730c7e855c670b1dfdb50ed7dd6e46",
2168
2176
  "lib/middleware/web-app-manifest.js": "sha256:d2e50696b4c49d4a089a79b7514fb89fe4c765b9e7edfd9bc7e0e3bf31ca1d23",
2169
2177
  "lib/migration-files.js": "sha256:6381dd46fdcb5796ac094ec9353ce6c4996d56f826f70bc4939349b934a7cef1",
2170
2178
  "lib/migrations.js": "sha256:229a067f388db89d74d7f1c6b2a5b4c48840966bf90d918fc786f489995e4b83",
@@ -2218,7 +2226,7 @@
2218
2226
  "lib/parsers/safe-ini.js": "sha256:58287b28aa6f0dbeb0f45526e671790a8647b3b3e4c78ae8961042e11ff22ae9",
2219
2227
  "lib/parsers/safe-toml.js": "sha256:148e874454ae1ed6f882f450b38e08e6b73b64b100b499b5eaa739c0fd8064be",
2220
2228
  "lib/parsers/safe-xml.js": "sha256:82a3bb0c0e08d7437c478d74a50b7ba949a2e090c8e4600f5f18c69de681a48a",
2221
- "lib/parsers/safe-yaml.js": "sha256:47702d747136220b2edaae310274b328c589d32cdf27c651f8f107742179e23e",
2229
+ "lib/parsers/safe-yaml.js": "sha256:bb91ee487c4599333d7d87ed2aa670bea939ed776108eed5f8e92093e2d36353",
2222
2230
  "lib/permissions.js": "sha256:c18f53a53c4e9b0c9a212bc39703000ea844a7beb407d19f82455bb1d4a48cdf",
2223
2231
  "lib/pick.js": "sha256:7e4f5aca12ed39311afbb71885332884cb7fbaeb02dd9a2d6ecf9cb53dba1b57",
2224
2232
  "lib/pipl-cn.js": "sha256:4855a8653ef6c91fc2dec0a1c997ac7ba4d38ec737add5f77a5733ee2d56b1b2",
@@ -2254,7 +2262,7 @@
2254
2262
  "lib/router.js": "sha256:1225dbe9a51d846bbfdaa29041b19675b4d0c01084b70e9ae795026c0b382651",
2255
2263
  "lib/safe-archive.js": "sha256:43a81f432e1cf9f294cbcf60636514cf2a5f3c67ec8e36aa8f8521c4cc657d39",
2256
2264
  "lib/safe-async.js": "sha256:4095c238c1fb0ca788946c757aea2371d832648865ef1810bd2f3a95dbe30bf4",
2257
- "lib/safe-buffer.js": "sha256:99f64bc544a46037cc43fe3faadd81e2757e039dade46469f3d274591c8434c3",
2265
+ "lib/safe-buffer.js": "sha256:881cf46b90f17f0e36d360a627aa4d10b9d9928472fd2254ae8ddff7bdf49789",
2258
2266
  "lib/safe-decompress.js": "sha256:f06a4d1e31453a94b6722cbb305111b10d0a683fe36c186a174954f45f4a521a",
2259
2267
  "lib/safe-dns.js": "sha256:e1c2c381bae7fa7e0ee2b84a7f09870b95760a007aa0ae0793679f78982cb902",
2260
2268
  "lib/safe-ical.js": "sha256:ad3cc9810d538aba86fed57b2e4bc79eaa8760bc9ef146dae898b1d496058ea9",
@@ -2348,8 +2356,8 @@
2348
2356
  "oss-fuzz/projects/blamejs/README.md": "sha256:ae13b7bb79ed8d69b1b3276e5562807a0349fb6e6b7d11cf1f683aad1eafdb4b",
2349
2357
  "oss-fuzz/projects/blamejs/build.sh": "sha256:0ced1cf21782c97be7f8d74faf5e27a308b60b2f858836fb5ca3b8c4e939a8f7",
2350
2358
  "oss-fuzz/projects/blamejs/project.yaml": "sha256:59f2cb83aa622325a175b77416fe155be15b70a9c798bd1a78bba05763b1b03d",
2351
- "package-lock.json": "sha256:a993222d15820ddfccda00518ea1dcc788873a851a8258799dd0995c5b34b1bf",
2352
- "package.json": "sha256:b82adb1c4fd61687844feeb477960e33787de640bde9680e962152ae2122f838",
2359
+ "package-lock.json": "sha256:e09ec4b072f8f1f38ddb0b59b855e0ca1bca293a9568daa4be3e765dd237047e",
2360
+ "package.json": "sha256:ed80932595218a5a1da1c3f405a1dfc989ab3910f27b39df5f15b601f95225e9",
2353
2361
  "release-notes/v0.0.x.json": "sha256:7a49819f30068ee119000cad7010194882bb8bfaa12acbdab4dfc066efb7982f",
2354
2362
  "release-notes/v0.1.x.json": "sha256:6742a8c17f947c5cb76f69dead7eea86b942d80621d914b774ba5488e09937e5",
2355
2363
  "release-notes/v0.10.x.json": "sha256:fe498045daf88337bd3d987e5964aa42c99a50e1685b6f09e51f698b8687726f",
@@ -2361,6 +2369,7 @@
2361
2369
  "release-notes/v0.16.0.json": "sha256:70def9578a6ae184dfb2f82c2f2b1bda15c4d9b8f987f0047da7f68f06a7e41b",
2362
2370
  "release-notes/v0.16.1.json": "sha256:67f5833cf682266b0ea93d410fdeedfa427e80781532d15e64c7d440dfefa95c",
2363
2371
  "release-notes/v0.16.2.json": "sha256:54d139821c154cffcd5b87b2cad1cff696f1db2af49742a144cd86c3194b3ece",
2372
+ "release-notes/v0.16.3.json": "sha256:0de1e0284e41575b8480d26f99b49d9dea550ef035f5c45e0b1723620d84f49d",
2364
2373
  "release-notes/v0.2.x.json": "sha256:985e27ff5de04cfc7869a3986dd0b9f0fcdfcddfb67d3fae3f4ae70856722d7e",
2365
2374
  "release-notes/v0.3.x.json": "sha256:e2db5eae66977b272bb185cad668386afb8fd33998a17c22eb6e411c0f8ca588",
2366
2375
  "release-notes/v0.4.x.json": "sha256:c3d19cb9c50a976432fc0bb612c87741d8728ae37562b501e6f1eccd01dd574d",
@@ -2623,6 +2632,7 @@
2623
2632
  "test/layer-0-primitives/config-drift.test.js": "sha256:75371225fef23f472e26444e672c7822668e0400407e05652bc73f3a368b97aa",
2624
2633
  "test/layer-0-primitives/config.test.js": "sha256:6adc4278b64f960ea2649779d7048c90ff54dbd849832c088700fbdca150ef94",
2625
2634
  "test/layer-0-primitives/consent-purposes.test.js": "sha256:a1669c7fb9c999ced399a4d93dccf5195008c422f28a669c25a9832f2114ce2d",
2635
+ "test/layer-0-primitives/content-credentials-coverage.test.js": "sha256:81a1942e6306e69159d3da8da97c55e70e3c66d19d8d6ad187e6f2a627e57ddc",
2626
2636
  "test/layer-0-primitives/content-credentials.test.js": "sha256:10014ff2e44cd5185d6c3401cb797db4813c4aa794cd01f07a221953cd4f029f",
2627
2637
  "test/layer-0-primitives/content-digest.test.js": "sha256:7b28731f3806576e04669a62919be1d79df3881b1b80b6eaf7cf101a2aa56c91",
2628
2638
  "test/layer-0-primitives/cors.test.js": "sha256:74b0feee302861b3a6385cf43dfb286b008a10c37794e7074f34236124d924f2",
@@ -2655,6 +2665,7 @@
2655
2665
  "test/layer-0-primitives/csrf-protect.test.js": "sha256:4b7d07492a1f1a218aa8106a387401cd3d6f3b43d3c6833ee4db5386bc2bc712",
2656
2666
  "test/layer-0-primitives/csv.test.js": "sha256:1367035130388f4cf75c9bc03cc88b4d4ac1aca3fb6c62d7a98c34d256f89e7a",
2657
2667
  "test/layer-0-primitives/cwt.test.js": "sha256:2e992ab7445db02581275ae5fd8b38de210e21f38a1594f27942f1bb1f9495e4",
2668
+ "test/layer-0-primitives/cycle2-bugfixes.test.js": "sha256:d23919a13e58a84d61a1a3d2eb95ec6f9d24ab94e3f2221e7002e07438d48341",
2658
2669
  "test/layer-0-primitives/daemon.test.js": "sha256:5e75c56ad03579293561ce8708a4289f2b8967a6b28bec31d5e3138bc49c17b3",
2659
2670
  "test/layer-0-primitives/daily-byte-quota.test.js": "sha256:f8ac102047451f6dd04050096674b739f012117b1845215bf4f1b47df25be72f",
2660
2671
  "test/layer-0-primitives/dane.test.js": "sha256:0fff14dfa609fa04a30f6a5a644288bcfafb3292044797cdc89674e5dc9485e2",
@@ -2758,6 +2769,7 @@
2758
2769
  "test/layer-0-primitives/guard-saga-config.test.js": "sha256:f35eb034a7b53cb7f3219998d546834cfc2b0c7b6af71a5f6d127e59152f8648",
2759
2770
  "test/layer-0-primitives/guard-smtp-command.test.js": "sha256:a31c1f379ff873253e165359ad251a6574b8aff532b6c177e0af1c52caca7702",
2760
2771
  "test/layer-0-primitives/guard-snapshot-envelope.test.js": "sha256:7fb6a0c76febd6576ca47c0712eb8f2db4305e7000fbee1450f98450a58caae9",
2772
+ "test/layer-0-primitives/guard-sql-coverage.test.js": "sha256:8ce355a7ff8b1923090b2f2409a0a0726a418221eb4b901920216f387784ecde",
2761
2773
  "test/layer-0-primitives/guard-stream-args.test.js": "sha256:7c7401b88eccfbd50964d41c826a1f75be3ac3bdb4caa1ac929add180ca01976",
2762
2774
  "test/layer-0-primitives/guard-svg.test.js": "sha256:057b281464de02d03e5e1a5c76a0a7efb23904bbcc7be43095924d6a73e5bbfe",
2763
2775
  "test/layer-0-primitives/guard-tenant-id.test.js": "sha256:4083f32ea58e4b350fd6b40e7ada751e4b213ae4a03b85500505863fb72d800a",
@@ -2847,12 +2859,15 @@
2847
2859
  "test/layer-0-primitives/mail-store.test.js": "sha256:80f98a73baf628158df0bc879ca9f35c4c534e3e5562e2a25fae41b0b5a1bbc1",
2848
2860
  "test/layer-0-primitives/mail-unsubscribe.test.js": "sha256:b745189403f7574e4845c2366a05fcdb9a3fcaf21c6930e2fe53ac94f1d1dec8",
2849
2861
  "test/layer-0-primitives/mail.test.js": "sha256:c83e3d7a9d22ad072104ac6d3c360a2b272f16f364ad8206b5eff7043fe97e48",
2862
+ "test/layer-0-primitives/mcp-coverage.test.js": "sha256:b27f9f19be15c0f5308d1fe88534bd979d4b84439f1a6ecb117bc085cae4626b",
2850
2863
  "test/layer-0-primitives/mcp-tool-registry.test.js": "sha256:57bc395acf4e7bac1bd4385ba6bf3d749ecee20dacd20b97ae1a6ca7b439a201",
2851
2864
  "test/layer-0-primitives/mcp.test.js": "sha256:21d45e7d498b5d7404294709b0031fa1882505ee81681090190aaeeece066a20",
2852
2865
  "test/layer-0-primitives/mdoc.test.js": "sha256:d89818a74820c8729d9c9b6024311468fcc72c3333474a5658f57fa79f514e07",
2866
+ "test/layer-0-primitives/metrics-coverage.test.js": "sha256:1ee32f903567bc9451d7f0257b3fb830a438283e0714c5ee38af37ec5cdeffef",
2853
2867
  "test/layer-0-primitives/metrics-shadow-registry.test.js": "sha256:60e3a71c5ec83f83901a9484db09079d1d8e32768538fb953fd3732ce1c15076",
2854
2868
  "test/layer-0-primitives/metrics-snapshot.test.js": "sha256:57b4686c1d472c07e85ee32ab8f08e7ba9332ccdb1e6000363be87815e4eef32",
2855
2869
  "test/layer-0-primitives/middleware-compose-pipeline.test.js": "sha256:012ccb0976f5fddbc6ef370a7c2dabed7c24a9619e0cfb0ea1532551b40fc0c0",
2870
+ "test/layer-0-primitives/middleware-tus-upload-coverage.test.js": "sha256:c8a54bc3116804a8ca6b7a8d8ddce75de6771af421af531b5c8043918e8bffed",
2856
2871
  "test/layer-0-primitives/mime-parse.test.js": "sha256:d6d843a250c6dccac9c125668de5cb3885bf043a0c24eb176ab871d9cadbe878",
2857
2872
  "test/layer-0-primitives/money.test.js": "sha256:76fb9d1071d72cdf7f26bbd0a69dc5329a5a35f436d52b0a041c30afef602831",
2858
2873
  "test/layer-0-primitives/mtls-ca-paths.test.js": "sha256:e770ce593b1f7c271be6aea5c4245fbda32ba22b66466c9cff9c35b0886262e9",
@@ -2890,6 +2905,7 @@
2890
2905
  "test/layer-0-primitives/output-header-hardening.test.js": "sha256:3c31c56bb1ac63783c40832cf35d0748c801ad7a74e385fc8d46f63ad2907206",
2891
2906
  "test/layer-0-primitives/pagination.test.js": "sha256:19364aa49ce2808b5ab20cc16d59ffe6dc41a2d05ff868cdd83b076f0adb7793",
2892
2907
  "test/layer-0-primitives/parser-verify-hardening.test.js": "sha256:cba2f34efc631efe95c5cc10ffb8af75d4fbd7f8586520149cf6f05b96bede19",
2908
+ "test/layer-0-primitives/parsers-safe-yaml-coverage.test.js": "sha256:5717cc0b7d36264f40f833d3284245c30f09573942218731702450c7f1341633",
2893
2909
  "test/layer-0-primitives/parsers-standalone.test.js": "sha256:21ea49f2722bf595f4d04491f08626dcbee6d1be4241667a33ac6f9450671ef7",
2894
2910
  "test/layer-0-primitives/passkey-real-vectors.test.js": "sha256:5ac058d841807fe15a9d44f823e6cfbcf028efbedd14c3cc7e902eb5e31aaa92",
2895
2911
  "test/layer-0-primitives/passkey.test.js": "sha256:a46e1f12c969efaa3d1fd04bcc248a0d8f80f976f52404cb772683d56091a80d",
@@ -135,7 +135,7 @@ jobs:
135
135
 
136
136
  - name: Log in to GHCR
137
137
  if: ${{ !inputs.dry_run }}
138
- uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
138
+ uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
139
139
  with:
140
140
  registry: ghcr.io
141
141
  username: ${{ github.actor }}
@@ -340,7 +340,7 @@ jobs:
340
340
  # against a public package, so this is the right shape regardless
341
341
  # of visibility.
342
342
  - name: Log in to GHCR
343
- uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
343
+ uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
344
344
  with:
345
345
  registry: ghcr.io
346
346
  username: ${{ github.actor }}