@blamejs/blamejs-shop 0.4.93 → 0.4.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/order.js +90 -23
- package/lib/print-on-demand.js +41 -10
- package/lib/vendor/MANIFEST.json +15 -9
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/examples/wiki/Dockerfile +14 -14
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.24.json +22 -0
- package/lib/vendor/blamejs/scripts/check-esbuild-pin.js +134 -0
- package/lib/vendor/blamejs/scripts/esbuild-binary-pin.json +10 -0
- package/lib/vendor/blamejs/scripts/release.js +1 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +16 -38
- package/lib/vendor/blamejs/test/layer-5-integration/bundler-output.test.js +6 -6
- 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.4.x
|
|
10
10
|
|
|
11
|
+
- v0.4.95 (2026-06-25) — **An order state change no longer logs a phantom transition when it loses a concurrent race, and a transient failure writing the history row no longer strands a stock hold.** Advancing an order through its lifecycle reads the order's current status, resolves the next state, then writes it with an UPDATE guarded on the row still being in the state it was read in — so when two writers race the same order (the stale-order reaper cancelling while a payment webhook marks it paid), exactly one wins and the loser collapses to a no-op. Two issues in that path are fixed. First, the state machine's transition audit was emitted before the guarded write resolved, so a writer that LOST the race still recorded a 'success' transition into the audit trail for a state change that never landed; the audit is now written only for the writer that actually moved the row. Second, the denormalized history-row insert ran between the committed status write and the inventory settlement with no failure handling: a transient error writing that row aborted the whole call after the status had already advanced, so the stock hold was never converted to a shelf debit (or released) and a webhook retry — finding the order already advanced — skipped settlement entirely, stranding the hold. The history insert is now best-effort (the state-machine audit is the authoritative record), so a failure writing it never prevents the hold from settling. No configuration changes. **Changed:** *Vendored framework refreshed to 0.15.24* — Updates the vendored blamejs framework to 0.15.24, an upstream build supply-chain hardening release with no runtime API changes. No operator action required. **Fixed:** *A lost order-transition race no longer records a phantom 'success' in the audit trail* — The order state machine emitted its transition audit during the in-memory replay, before the guarded status write that decides the race. A writer that lost the race (its conditional update matched zero rows and it no-opped) had nonetheless already written a 'success' transition into the audit trail — a record an auditor could not tell apart from a real one. The destination state is now resolved side-effect-free, and the transition audit is emitted only after the guarded write wins, so the audit trail records exactly the transitions that actually landed. · *A transient failure writing an order's history row no longer strands its stock hold* — After an order's status write committed, the order_transitions history row was inserted with no failure handling, ahead of the inventory settlement. A transient error on that insert aborted the call with the order already advanced, so the held stock was never debited or released; a webhook retry then found the order past its starting state, no-opped at the concurrency guard, and skipped settlement — leaving the hold stranded with no automatic recovery. A history-row write failure no longer aborts settlement, so the hold always settles once the state change commits. Because that row is also where a PayPal capture id is recovered from, a write failure is not silently dropped: the row's full payload — including any capture metadata — is recorded to the durable error feed (and the audit chain) for reconciliation.
|
|
12
|
+
|
|
13
|
+
- v0.4.94 (2026-06-25) — **A print-on-demand fulfillment status change is now an atomic claim — concurrent updates can no longer clobber each other or record a phantom transition.** Advancing a print-on-demand fulfillment through its lifecycle (submitted → shipped / failed / cancelled) read the row's current status, validated the move in memory, and then wrote the new status with an UPDATE keyed on the row id alone. Two updates racing the same fulfillment — a worker marking it shipped while an operator marks it failed, or a re-delivered supplier webhook — both read the same starting status, both passed the in-memory check, and both wrote unconditionally, so the later write silently overwrote the earlier one's status and columns (a lost update), and each emitted a 'success' transition into the audit trail even though only one write reflected reality. The status write is now a single-statement atomic claim gated on the row still being in the state it was read in (the same compare-and-swap the dropship-forwarding and returns lifecycles already use): exactly one update wins, the loser changes zero rows and is refused, and the audit record is written only for the update that actually changed the row. No configuration changes. **Fixed:** *Concurrent print-on-demand fulfillment transitions no longer clobber each other or log a phantom transition* — The fulfillment status update was keyed on the row id with no guard on the starting state, so two concurrent transitions both wrote unconditionally — last-writer-wins overwrote the other's status and stamped columns, and both emitted a 'success' transition audit. The update is now an atomic compare-and-swap gated on the row still being in the from-state, with the destination resolved side-effect-free and the audit emitted only on the winning write. The losing transition changes zero rows and is refused with a clear concurrency error instead of corrupting the record.
|
|
14
|
+
|
|
11
15
|
- v0.4.93 (2026-06-25) — **Per-IP rate limits now key an IPv6 client by its /64 prefix, closing an address-rotation bypass of the login, checkout, and webhook throttles.** The request-throttling layer derived its per-client key from the full client IP address. For an IPv4 client that is one host, but an IPv6 end-site is allocated an entire /64 prefix (RFC 6177 / RFC 4291) and can freely rotate the low 64 bits, presenting a fresh address — and therefore a brand-new rate-limit bucket — on every request. That let a single IPv6 site walk straight through every per-IP ceiling: the global request limiter, the tight per-IP limiter on login / register / passkey / checkout, the PayPal webhook verification budget, and the per-IP captcha budget. The client key now collapses an IPv6 address to its routing-significant /64 (an IPv4 address is still keyed verbatim, and an IPv4-mapped IPv6 keys as its dotted host), so one end-site shares one bucket while distinct sites stay distinct. A value that is not a parseable IP falls back to itself, so two unidentifiable clients never collapse into one shared bucket. No configuration changes. **Fixed:** *IPv6 clients can no longer rotate addresses to bypass per-IP rate limits* — Every per-IP throttle — the global request limiter, the tight limiter on authentication and checkout routes, the PayPal webhook verification budget, and the captcha budget — keyed off the full client IP. An IPv6 client controls a whole /64 and could rotate the low 64 bits to mint an unlimited supply of fresh buckets, defeating each ceiling. The per-client key now masks an IPv6 address to its /64 prefix (IPv4 is unchanged), so a single end-site is held to one bucket. The fix lives in the client-key derivation itself, because the framework's rate-limit key mode is bypassed whenever a custom key function is supplied — as every limiter here does.
|
|
12
16
|
|
|
13
17
|
- v0.4.92 (2026-06-25) — **Refresh the vendored framework to 0.15.23 — a state transition whose entry hook throws is now always recorded in the audit trail.** Updates the vendored blamejs framework from 0.15.20 to 0.15.23. The behavioural change operators will notice is in the order, return, redemption, and dropship state machines, which compose the framework's finite-state-machine primitive: previously, if a destination state's entry hook threw, the state change committed but no audit record was written — leaving a privileged transition unaudited. The framework now always records the transition (stamped with a failure outcome and the hook error) and still re-raises the error, so a state change can never be silently unaudited. The refresh also carries upstream correctness and hardening work in primitives the storefront does not currently reach (a websocket-client reconnect fix, a cross-platform path-containment fix), plus additive primitives the application will adopt in follow-up changes. No configuration changes. **Changed:** *State transitions are always audited, even when an entry hook fails* — The order, return, redemption, and dropship-forwarding state machines record every transition on the framework's tamper-evident audit chain. A transition whose destination entry hook threw previously committed the state change without emitting that record. The refreshed framework always writes the audit record — stamped with a failure outcome and the entry-hook error — and still re-raises the error to the caller, so a privileged state change is never left unaudited.
|
package/lib/asset-manifest.json
CHANGED
package/lib/order.js
CHANGED
|
@@ -642,16 +642,36 @@ function create(opts) {
|
|
|
642
642
|
history: [],
|
|
643
643
|
context: {},
|
|
644
644
|
});
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
645
|
+
// Resolve the destination state side-effect-free with `target` — it runs
|
|
646
|
+
// the same edge check `transition` does but emits NO audit and mutates no
|
|
647
|
+
// state (the order FSM is guard-free, so target() and transition() always
|
|
648
|
+
// agree). We must NOT call `transition` here: it emits the
|
|
649
|
+
// `fsm.order.transition=success` audit event, and emitting BEFORE the
|
|
650
|
+
// atomic claim below would record a PHANTOM success for a caller that goes
|
|
651
|
+
// on to lose the race (the conditional UPDATE matches zero rows and we
|
|
652
|
+
// no-op). The real audit emit is deferred to the winning branch.
|
|
653
|
+
var toState = instance.target(event);
|
|
654
|
+
if (toState == null) {
|
|
655
|
+
// Illegal edge / unknown event. Re-run through `transition` purely to
|
|
656
|
+
// surface the framework's typed FsmError unchanged — `transition` throws
|
|
657
|
+
// on a refusal WITHOUT emitting an audit (the success event only fires on
|
|
658
|
+
// a landed edge), so a caller that switches on err.code (the storefront
|
|
659
|
+
// order actions key on ORDER_TRANSITION_REFUSED) sees exactly the
|
|
660
|
+
// code/cause it always did.
|
|
661
|
+
try {
|
|
662
|
+
await instance.transition(event, { metadata: (opts2 && opts2.metadata) || null });
|
|
663
|
+
} catch (e) {
|
|
664
|
+
var err = new Error("order.transition: refused — " + (e && e.message || e));
|
|
665
|
+
err.code = (e && e.code) || "ORDER_TRANSITION_REFUSED";
|
|
666
|
+
err.cause = e;
|
|
667
|
+
throw err;
|
|
668
|
+
}
|
|
669
|
+
// Unreachable (target() said illegal yet transition() didn't throw) —
|
|
670
|
+
// refuse rather than proceed on an unresolved edge.
|
|
671
|
+
var errUnresolved = new Error("order.transition: refused — no transition for '" +
|
|
672
|
+
event + "' from '" + current.status + "'");
|
|
673
|
+
errUnresolved.code = "ORDER_TRANSITION_REFUSED";
|
|
674
|
+
throw errUnresolved;
|
|
655
675
|
}
|
|
656
676
|
var ts = _now();
|
|
657
677
|
// Conditional claim guard — the state write only lands if the row is
|
|
@@ -669,27 +689,74 @@ function create(opts) {
|
|
|
669
689
|
// status = 'pending'` claim guard elsewhere in this file.
|
|
670
690
|
var upd = await query(
|
|
671
691
|
"UPDATE orders SET status = ?1, updated_at = ?2 WHERE id = ?3 AND status = ?4",
|
|
672
|
-
[
|
|
692
|
+
[toState, ts, orderId, current.status],
|
|
673
693
|
);
|
|
674
694
|
if (Number(upd.rowCount || 0) === 0) {
|
|
675
695
|
// Lost the race: another writer already transitioned this order out of
|
|
676
696
|
// `current.status`. Our edge was computed from a now-stale snapshot, so
|
|
677
697
|
// we apply nothing — no order_transitions row, no inventory settlement
|
|
678
698
|
// (the winning edge settled its own holds), no webhook / loyalty /
|
|
679
|
-
// referral fan-out.
|
|
680
|
-
//
|
|
699
|
+
// referral fan-out. We never emitted an audit either (target() is
|
|
700
|
+
// side-effect-free), so no phantom success record is left behind.
|
|
701
|
+
// Collapse to a no-op and return the order as the winner left it, so the
|
|
702
|
+
// caller observes the authoritative state.
|
|
681
703
|
return await this.get(orderId);
|
|
682
704
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
705
|
+
// We won the atomic claim — the row really advanced current.status ->
|
|
706
|
+
// toState, which is the authoritative edge for the settlement below.
|
|
707
|
+
var result = { from: current.status, to: toState, on: event };
|
|
708
|
+
// NOW drive the edge through b.fsm so its `fsm.order.transition=success`
|
|
709
|
+
// audit fires exactly once, only for the writer that changed the row.
|
|
710
|
+
// `target` already validated the edge, so this cannot refuse; the row is
|
|
711
|
+
// persisted, so a throw here must never fail the caller.
|
|
712
|
+
try { await instance.transition(event, { metadata: (opts2 && opts2.metadata) || null }); }
|
|
713
|
+
catch (_emitErr) { /* edge pre-validated by target(); audit emit is best-effort */ }
|
|
714
|
+
// History row. A failure writing it must NOT abort the inventory / value
|
|
715
|
+
// settlement below: the status is already committed, and a retry no-ops
|
|
716
|
+
// at the claim guard above (skipping settlement) — which would strand the
|
|
717
|
+
// hold. But this row is NOT purely cosmetic: it is the recovery source
|
|
718
|
+
// for a PayPal capture id (order.paypalCaptureId scans order_transitions,
|
|
719
|
+
// and the checkout path falls back to this metadata when setPaypalCapture
|
|
720
|
+
// itself failed). So on a write failure we do NOT silently drop it —
|
|
721
|
+
// which would lose the capture id and block a later refund — we persist
|
|
722
|
+
// the row's full payload (including any capture metadata) to the durable
|
|
723
|
+
// audit chain + error feed for reconciliation, then let settlement
|
|
724
|
+
// proceed. Mirrors _settleSku's capture-on-failure.
|
|
725
|
+
try {
|
|
726
|
+
await query(
|
|
727
|
+
"INSERT INTO order_transitions (id, order_id, from_state, to_state, on_event, reason, metadata_json, occurred_at) " +
|
|
728
|
+
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
|
729
|
+
[
|
|
730
|
+
b.uuid.v7(), orderId, result.from, result.to, event,
|
|
731
|
+
(opts2 && opts2.reason) || null,
|
|
732
|
+
JSON.stringify((opts2 && opts2.metadata) || {}),
|
|
733
|
+
ts,
|
|
734
|
+
],
|
|
735
|
+
);
|
|
736
|
+
} catch (histErr) {
|
|
737
|
+
var _histMsg = "order.transition.history_write_failed — order=" + orderId +
|
|
738
|
+
" event=" + event + " from=" + result.from + " to=" + result.to +
|
|
739
|
+
" metadata=" + JSON.stringify((opts2 && opts2.metadata) || {}) +
|
|
740
|
+
": " + ((histErr && histErr.message) || histErr);
|
|
741
|
+
try {
|
|
742
|
+
b.audit.safeEmit({
|
|
743
|
+
action: "order.transition.history_write_error",
|
|
744
|
+
outcome: "failure",
|
|
745
|
+
metadata: {
|
|
746
|
+
order_id: orderId, on_event: event, from_state: result.from, to_state: result.to,
|
|
747
|
+
caller_meta: (opts2 && opts2.metadata) || null,
|
|
748
|
+
message: (histErr && histErr.message) || String(histErr),
|
|
749
|
+
},
|
|
750
|
+
});
|
|
751
|
+
} catch (_auditErr) { /* drop-silent — the error-feed capture below is the durable record */ }
|
|
752
|
+
if (errorLog && typeof errorLog.captureServerError === "function") {
|
|
753
|
+
try {
|
|
754
|
+
await errorLog.captureServerError({
|
|
755
|
+
route: "/order/" + orderId + "/transition", method: "POST", status: 500, message: _histMsg,
|
|
756
|
+
});
|
|
757
|
+
} catch (_logErr) { /* drop-silent — never let the error-feed write mask the original failure */ }
|
|
758
|
+
}
|
|
759
|
+
}
|
|
693
760
|
var refreshed = await this.get(orderId);
|
|
694
761
|
// Inventory settlement — SYNCHRONOUS, before the fire-and-forget
|
|
695
762
|
// fan-outs below. Stock truth is not best-effort: a paid order debits
|
package/lib/print-on-demand.js
CHANGED
|
@@ -282,17 +282,24 @@ function create(opts) {
|
|
|
282
282
|
history: [],
|
|
283
283
|
context: {},
|
|
284
284
|
});
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
285
|
+
// Resolve the destination state IN MEMORY, side-effect-free, with
|
|
286
|
+
// `target` — it runs the same edge + guard check `transition` does but
|
|
287
|
+
// emits NO audit and mutates no state (returns null for an illegal edge
|
|
288
|
+
// or a guard-refused one). We must NOT call `transition` here: it emits a
|
|
289
|
+
// "success" `fsm.pod_fulfillment.transition` audit, and emitting before
|
|
290
|
+
// the atomic claim below would record a phantom success for a caller that
|
|
291
|
+
// goes on to LOSE the race (the conditional UPDATE matches zero rows). The
|
|
292
|
+
// real `transition` emit is deferred to the winning branch.
|
|
293
|
+
var toState = instance.target(event);
|
|
294
|
+
if (toState == null) {
|
|
295
|
+
var err = new Error("print-on-demand: fulfillment transition refused — illegal edge '" +
|
|
296
|
+
event + "' from state '" + current.status + "'");
|
|
297
|
+
err.code = "POD_FULFILLMENT_TRANSITION_REFUSED";
|
|
291
298
|
throw err;
|
|
292
299
|
}
|
|
293
300
|
var ts = _now();
|
|
294
301
|
var sets = ["status = ?1", "updated_at = ?2"];
|
|
295
|
-
var params = [
|
|
302
|
+
var params = [toState, ts];
|
|
296
303
|
var p = 3;
|
|
297
304
|
var keys = Object.keys(patch || {});
|
|
298
305
|
for (var i = 0; i < keys.length; i += 1) {
|
|
@@ -310,11 +317,35 @@ function create(opts) {
|
|
|
310
317
|
params.push(patch[k]);
|
|
311
318
|
p += 1;
|
|
312
319
|
}
|
|
313
|
-
params.push(fulfillmentId);
|
|
314
|
-
|
|
315
|
-
|
|
320
|
+
params.push(fulfillmentId); // ?p
|
|
321
|
+
params.push(current.status); // ?(p+1)
|
|
322
|
+
// Cross-instance atomic claim — the transaction substitute on the D1
|
|
323
|
+
// bridge. `target` above only validated the edge IN MEMORY (its lock is
|
|
324
|
+
// per-instance, and each request restores a fresh instance), so two
|
|
325
|
+
// concurrent transitions on the same row both pass it. Gating the write on
|
|
326
|
+
// the row STILL being in the from-state we resolved against
|
|
327
|
+
// (`AND status = <from>`) is the single-statement serialization point:
|
|
328
|
+
// exactly one writer changes the row, the loser changes zero rows and is
|
|
329
|
+
// refused (no last-writer-wins clobber of the winner's state + columns).
|
|
330
|
+
var upd = await query(
|
|
331
|
+
"UPDATE pod_fulfillments SET " + sets.join(", ") +
|
|
332
|
+
" WHERE id = ?" + p + " AND status = ?" + (p + 1),
|
|
316
333
|
params,
|
|
317
334
|
);
|
|
335
|
+
if (Number(upd.rowCount || 0) !== 1) {
|
|
336
|
+
var raced = new Error("print-on-demand: fulfillment " + fulfillmentId +
|
|
337
|
+
" changed state concurrently (no longer " + current.status + ") — transition refused");
|
|
338
|
+
raced.code = "POD_FULFILLMENT_TRANSITION_REFUSED";
|
|
339
|
+
throw raced;
|
|
340
|
+
}
|
|
341
|
+
// We won the atomic claim — the row really did advance current.status ->
|
|
342
|
+
// toState. NOW drive the transition through b.fsm so its
|
|
343
|
+
// `fsm.pod_fulfillment.transition=success` audit event fires exactly once
|
|
344
|
+
// and only for the writer that actually changed the row. `target` above
|
|
345
|
+
// already validated this edge, so `transition` cannot refuse; the row is
|
|
346
|
+
// already persisted, so a throw here must never fail the caller.
|
|
347
|
+
try { await instance.transition(event, null); }
|
|
348
|
+
catch (_emitErr) { /* edge pre-validated by target(); audit emit is best-effort */ }
|
|
318
349
|
return await _getFulfillmentRow(fulfillmentId);
|
|
319
350
|
}
|
|
320
351
|
|
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -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.15.
|
|
7
|
-
"tag": "v0.15.
|
|
6
|
+
"version": "0.15.24",
|
|
7
|
+
"tag": "v0.15.24",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -845,6 +845,7 @@
|
|
|
845
845
|
"release-notes/v0.15.21.json": "lib/vendor/blamejs/release-notes/v0.15.21.json",
|
|
846
846
|
"release-notes/v0.15.22.json": "lib/vendor/blamejs/release-notes/v0.15.22.json",
|
|
847
847
|
"release-notes/v0.15.23.json": "lib/vendor/blamejs/release-notes/v0.15.23.json",
|
|
848
|
+
"release-notes/v0.15.24.json": "lib/vendor/blamejs/release-notes/v0.15.24.json",
|
|
848
849
|
"release-notes/v0.15.3.json": "lib/vendor/blamejs/release-notes/v0.15.3.json",
|
|
849
850
|
"release-notes/v0.15.4.json": "lib/vendor/blamejs/release-notes/v0.15.4.json",
|
|
850
851
|
"release-notes/v0.15.5.json": "lib/vendor/blamejs/release-notes/v0.15.5.json",
|
|
@@ -864,10 +865,12 @@
|
|
|
864
865
|
"scripts/check-actions-currency.js": "lib/vendor/blamejs/scripts/check-actions-currency.js",
|
|
865
866
|
"scripts/check-api-snapshot.js": "lib/vendor/blamejs/scripts/check-api-snapshot.js",
|
|
866
867
|
"scripts/check-changelog-extract.js": "lib/vendor/blamejs/scripts/check-changelog-extract.js",
|
|
868
|
+
"scripts/check-esbuild-pin.js": "lib/vendor/blamejs/scripts/check-esbuild-pin.js",
|
|
867
869
|
"scripts/check-pack-against-gitignore.js": "lib/vendor/blamejs/scripts/check-pack-against-gitignore.js",
|
|
868
870
|
"scripts/check-services.js": "lib/vendor/blamejs/scripts/check-services.js",
|
|
869
871
|
"scripts/check-vendor-currency.js": "lib/vendor/blamejs/scripts/check-vendor-currency.js",
|
|
870
872
|
"scripts/consolidate-release-notes.js": "lib/vendor/blamejs/scripts/consolidate-release-notes.js",
|
|
873
|
+
"scripts/esbuild-binary-pin.json": "lib/vendor/blamejs/scripts/esbuild-binary-pin.json",
|
|
871
874
|
"scripts/gen-migrating.js": "lib/vendor/blamejs/scripts/gen-migrating.js",
|
|
872
875
|
"scripts/generate-changelog-entry.js": "lib/vendor/blamejs/scripts/generate-changelog-entry.js",
|
|
873
876
|
"scripts/generate-release-signing-key.js": "lib/vendor/blamejs/scripts/generate-release-signing-key.js",
|
|
@@ -1552,7 +1555,7 @@
|
|
|
1552
1555
|
".npmrc": "sha256:66f104e7d07c496d2d0409988225e8c0e4ceb8d247dbcac3be75b2128d20ce66",
|
|
1553
1556
|
".pinact.yaml": "sha256:0213ffda55961dc49b64c0a5dfa3c0567419633b1499d57eaf7c8d842d7da6c7",
|
|
1554
1557
|
"ARCHITECTURE.md": "sha256:9b1c8d2b1b7a41838eb348b0a008e4b4369718fd72bfe2974b37155f7536d35b",
|
|
1555
|
-
"CHANGELOG.md": "sha256:
|
|
1558
|
+
"CHANGELOG.md": "sha256:0f52707a6551cd64b013b5449b9cdd2c30e544477fcaea89f01bc3cfed64ebdb",
|
|
1556
1559
|
"CODE_OF_CONDUCT.md": "sha256:148a281960fff7c2fe6554dab66da572c72245ddeb00b0d14811558397bff386",
|
|
1557
1560
|
"CONTRIBUTING.md": "sha256:bb4dbdbc8598da31dbce653a8ed322e08ff46560173f2eb67a4d684653948332",
|
|
1558
1561
|
"GOVERNANCE.md": "sha256:906df6afb1f552b27b9acb50f7f96c47b917a2f1021cd4e987dbf4ee0e0a821b",
|
|
@@ -1562,7 +1565,7 @@
|
|
|
1562
1565
|
"NOTICE": "sha256:f487fa47a11aca0f89e2615cdd3c713e9842abf7a30d8d328eeeae1c864aa774",
|
|
1563
1566
|
"README.md": "sha256:3ddcc197b003da0b02db8bdd1aef1e943c94f7eab613c633d6a45bb11d0a80e9",
|
|
1564
1567
|
"SECURITY.md": "sha256:77d8c2bcc04b425a08ef30e51204cebddbd48954b028a259c7a8412dbfeab40a",
|
|
1565
|
-
"api-snapshot.json": "sha256:
|
|
1568
|
+
"api-snapshot.json": "sha256:fc076b3e37ce704e4e47c8a57bf4e004481874f7078f2a7216d761285a5a4abc",
|
|
1566
1569
|
"assets/BlameJS_Logo.png": "sha256:3c65699753c771b48ef9ac7f45bb40815ec19a23afcdd0cd30ef4601bbbe293e",
|
|
1567
1570
|
"assets/BlameJS_Logo.svg": "sha256:dda44f3fb1343d5de9db6b1fcdb75fc649c57e7a99a8e8239fcf852e3841e1a8",
|
|
1568
1571
|
"bench/README.md": "sha256:74202f2507fd840bfc1ac6c681975d9273cf36cca6e0f72655f138337304033c",
|
|
@@ -1600,7 +1603,7 @@
|
|
|
1600
1603
|
"eslint.config.mjs": "sha256:532a857418bf2cf0b709e0227414b2ec6f73de450d2a6e27904924fd43d08a2d",
|
|
1601
1604
|
"examples/wiki/Caddyfile": "sha256:6428e568e2c30c5cc014822725df6998b5eb102c92e0bf7f556ab39532c96690",
|
|
1602
1605
|
"examples/wiki/DEPLOY.md": "sha256:a6d7a3b0edc37ccf767146816137aa03b09bef66051e89f7e062aa379e6d0bf3",
|
|
1603
|
-
"examples/wiki/Dockerfile": "sha256:
|
|
1606
|
+
"examples/wiki/Dockerfile": "sha256:a70ed2f1744d222264ba879d3e131937b7cfa9889ea542b77b51eb580dfe5c40",
|
|
1604
1607
|
"examples/wiki/README.md": "sha256:74319ff6cbdb16c7d0eb2aa3ef1b33f9692f778f36a3688b0ee88d6154ea3ae5",
|
|
1605
1608
|
"examples/wiki/cli-snapshot.json": "sha256:9fc00825db2a12172d8a91a687c0e8b96a276191db36f1d437bf7e5cb45b8954",
|
|
1606
1609
|
"examples/wiki/docker-compose.prod.yml": "sha256:33280a0039be004195ef0197db94c233a12742cb653f2d7473c8265cf4ee3faf",
|
|
@@ -2333,7 +2336,7 @@
|
|
|
2333
2336
|
"oss-fuzz/projects/blamejs/README.md": "sha256:ae13b7bb79ed8d69b1b3276e5562807a0349fb6e6b7d11cf1f683aad1eafdb4b",
|
|
2334
2337
|
"oss-fuzz/projects/blamejs/build.sh": "sha256:0ced1cf21782c97be7f8d74faf5e27a308b60b2f858836fb5ca3b8c4e939a8f7",
|
|
2335
2338
|
"oss-fuzz/projects/blamejs/project.yaml": "sha256:59f2cb83aa622325a175b77416fe155be15b70a9c798bd1a78bba05763b1b03d",
|
|
2336
|
-
"package.json": "sha256:
|
|
2339
|
+
"package.json": "sha256:26bfa0cd4ec6f8fe3b6a9331204d1560574df47446d4dcc8f83d9ab6f6521e2e",
|
|
2337
2340
|
"release-notes/v0.0.x.json": "sha256:7a49819f30068ee119000cad7010194882bb8bfaa12acbdab4dfc066efb7982f",
|
|
2338
2341
|
"release-notes/v0.1.x.json": "sha256:6742a8c17f947c5cb76f69dead7eea86b942d80621d914b774ba5488e09937e5",
|
|
2339
2342
|
"release-notes/v0.10.x.json": "sha256:fe498045daf88337bd3d987e5964aa42c99a50e1685b6f09e51f698b8687726f",
|
|
@@ -2358,6 +2361,7 @@
|
|
|
2358
2361
|
"release-notes/v0.15.21.json": "sha256:02b6e86d81f4d3ab5785ccdb2b0d2623c3ba406232dd9a5b9fbf8e3287c22c69",
|
|
2359
2362
|
"release-notes/v0.15.22.json": "sha256:3e794d767699e38d06ac3566117009e104fde8623f64ffd8f7ce682105d25341",
|
|
2360
2363
|
"release-notes/v0.15.23.json": "sha256:bb25738ab9701fb90a287e0b0370fe28f06396481664922450d1178c2a191b19",
|
|
2364
|
+
"release-notes/v0.15.24.json": "sha256:748b633f90de8f41b4f26f7ab8f3c31baaa0acd3d9ece2b44f0308724c8765a1",
|
|
2361
2365
|
"release-notes/v0.15.3.json": "sha256:19a0074c445545468ca3cc411b21ec8bdb27be2669ae1950347cc244f6aa348c",
|
|
2362
2366
|
"release-notes/v0.15.4.json": "sha256:6ac7fa0ef1728c27e71b2050d1b07a810f9b4b1440ccddbf28ad56e2f54d8585",
|
|
2363
2367
|
"release-notes/v0.15.5.json": "sha256:cca1d0edd5d6fc41b512d19d98be224b990dcab41478622c11962f0fcb1bb09a",
|
|
@@ -2377,10 +2381,12 @@
|
|
|
2377
2381
|
"scripts/check-actions-currency.js": "sha256:cb2c81ae9c2c67518e15b3b8b185fe84e1e8e9feeaf251ed5a5c9da63d881b45",
|
|
2378
2382
|
"scripts/check-api-snapshot.js": "sha256:f3cf55b5eb43f28df9893c2b125f46ca246c00d1a1c95cfc624158603fe48cf0",
|
|
2379
2383
|
"scripts/check-changelog-extract.js": "sha256:ac1e74b090cb8b5e0b3fc9d65a1484ce184eca9ba9ccc512a61540d854bfc81d",
|
|
2384
|
+
"scripts/check-esbuild-pin.js": "sha256:e9d6cd5595b8db19bceb83e01862f78dbd0a276af0901a6c8b09567096f2f613",
|
|
2380
2385
|
"scripts/check-pack-against-gitignore.js": "sha256:42cbd1e329ab7469984a30d6bc3e9b0af4defffced6dcf970c4f8cb7ad35da4c",
|
|
2381
2386
|
"scripts/check-services.js": "sha256:8c07b049fc899827c2d1ef2c136a3d0c3c43143b1546f28e44739ecec187f777",
|
|
2382
2387
|
"scripts/check-vendor-currency.js": "sha256:48c88ab874cabb6e8eb2ffa373037ae870279ff38061cf96fe49b3f07cfcacd4",
|
|
2383
2388
|
"scripts/consolidate-release-notes.js": "sha256:620de442c834fa99a7b668b93cf8788384a0aa802ce419953459c88451d8ec73",
|
|
2389
|
+
"scripts/esbuild-binary-pin.json": "sha256:2ad23d97af670343cee13bae4c2fe93053b1ab0cd80ca094fe725cf56000f424",
|
|
2384
2390
|
"scripts/gen-migrating.js": "sha256:2177f6b9bf60d6e9c161a0419caa248d2bef9eb7beb5223a10e8cab3ecc555b1",
|
|
2385
2391
|
"scripts/generate-changelog-entry.js": "sha256:2c0a1a395d2d1b30c1679883c694e27dc6f8e73d950c4deeb118ef5a4f01fd12",
|
|
2386
2392
|
"scripts/generate-release-signing-key.js": "sha256:3d4cc30a446a8a358b6180a5be7e4e88f2e1722fb1567f4379a335ffc03a50d9",
|
|
@@ -2388,7 +2394,7 @@
|
|
|
2388
2394
|
"scripts/publish-dep-confusion-placeholder.sh": "sha256:f6ea193c7f79fe08ba86df8533fad093381b4786fcac71aecdfff47fb115c9b6",
|
|
2389
2395
|
"scripts/refresh-api-snapshot.js": "sha256:1f6830a5d23aeeb3c93cb2eea00a016a2ed0de201d350e891b21374010613a6b",
|
|
2390
2396
|
"scripts/refresh-vendor-manifest.js": "sha256:48fc297ff66ed907a979bc07ffc747ccd7c2aa63c3bb3001d729239ee1e967d5",
|
|
2391
|
-
"scripts/release.js": "sha256:
|
|
2397
|
+
"scripts/release.js": "sha256:fa91cca8ea905c0b076a298a9b8f2b21a9c2bf3fe99a0e3dd66395b4355c99c2",
|
|
2392
2398
|
"scripts/sha3-digest.js": "sha256:a559d7dce435e37a078112ec5ace5f94bff012f67e682f4f096612f4480754c1",
|
|
2393
2399
|
"scripts/sign-release-artifact.js": "sha256:baa568e2291eaa3a35d8cd93a8d8c4add7e1d6d9c22f3c45ce6e9d660f3cf840",
|
|
2394
2400
|
"scripts/test-integration.js": "sha256:b63393fbdfd3883d53ba14b83b04ca2ba0987101e6fda86176812de8f721ee7a",
|
|
@@ -2603,7 +2609,7 @@
|
|
|
2603
2609
|
"test/layer-0-primitives/cluster-storage.test.js": "sha256:238b3b3db0eba3e6312a863710533178f566347b90e161e564481aa826707647",
|
|
2604
2610
|
"test/layer-0-primitives/cluster-vault-rotation.test.js": "sha256:3514e9e71d6c39e805248f58ad2f41528d091e196c0f3766a032675677161b2d",
|
|
2605
2611
|
"test/layer-0-primitives/cms-codec.test.js": "sha256:7e46078ed82be5b69d22c48f22dba37ea5015371c2a8cf5f94fb1a792fb7bb78",
|
|
2606
|
-
"test/layer-0-primitives/codebase-patterns.test.js": "sha256:
|
|
2612
|
+
"test/layer-0-primitives/codebase-patterns.test.js": "sha256:e4f6ea64058d7bff80a553181dd9af60c3c8b37ae3ecf92fc5e00db61d318a36",
|
|
2607
2613
|
"test/layer-0-primitives/codepoint-class.test.js": "sha256:19d1b69efa7e0e9f7ef2392ca264167767a5ec5890ee339b2cfd8a7818035d27",
|
|
2608
2614
|
"test/layer-0-primitives/compliance-ai-act.test.js": "sha256:5ee4ad05d12233cb3c5457ef10a727833710bbc1ce1318838f9f9ef5d2cb8d4b",
|
|
2609
2615
|
"test/layer-0-primitives/compliance-cascade.test.js": "sha256:ee02cf14541a837a9d7977c6ea6bf7f9210bed293925d93c976e31f270aebec4",
|
|
@@ -3027,7 +3033,7 @@
|
|
|
3027
3033
|
"test/layer-0-primitives/ws-client.test.js": "sha256:e08ee87dc913390dd2f099e5677806a89e9c7f36701265a31212446dedac9753",
|
|
3028
3034
|
"test/layer-0-primitives/x509-chain-ca-enforcement.test.js": "sha256:8d68ffe4f8655af99682927524ae5a6c5336f32a57eb7c5c78fef5f2d1b2e6e3",
|
|
3029
3035
|
"test/layer-1-state/api-key.test.js": "sha256:48d641b673a181746adf80dfa995f0ca29187af17aa4735590557fda4350dcb8",
|
|
3030
|
-
"test/layer-5-integration/bundler-output.test.js": "sha256:
|
|
3036
|
+
"test/layer-5-integration/bundler-output.test.js": "sha256:023f0504a5ae60fb39d347d05e8fe7e9d8fee685eb1bb89dd40406cf01691d22",
|
|
3031
3037
|
"test/layer-5-integration/external-db-residency.test.js": "sha256:3ff1de115d465f1f35decf207d9263bb1bb5327892662bd992adddb8af9e0f49",
|
|
3032
3038
|
"test/layer-5-integration/guard-host-integration.test.js": "sha256:5c57da239285ebf04ee34cb5ab2bc000e61e7b13a91fc15750ca1db49c431808",
|
|
3033
3039
|
"test/layer-5-integration/security-chaos.test.js": "sha256:7459c282a2169b54afb1f6fe36649116553f3f62b6acb454fb4f90cb3b02aa87",
|
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.15.x
|
|
10
10
|
|
|
11
|
+
- v0.15.24 (2026-06-25) — **Supply-chain hardening for the build: the example wiki container's base images are digest-pinned (and tracked for patches by Dependabot), and the SEA-build esbuild binary pin is centralized and release-enforced so a version bump can't silently lose its reviewed hash.** A build supply-chain hardening release; no runtime API changes. The example wiki container's Chainguard base images are now pinned by digest (tag@sha256) for reproducible builds and provenance, with Dependabot's docker updater bumping the digests so they keep tracking Chainguard's continuous CVE rebuilds; the published multi-arch image is unaffected because the release workflow still resolves the rolling tag to a fresh digest at build time, so the scanned image equals the published image and the committed digest never reaches the image-scan gate. The esbuild native-binary supply-chain pin — the reviewed per-platform SHA-256 the bundler smoke gate checks the on-disk compiler against — is centralized in one file and enforced both at release time and in the pattern gate by a single shared verifier: the version package.json and the CI/publish workflows install must carry a complete reviewed-hash entry, so bumping esbuild without re-reviewing and re-pinning the binary now fails closed instead of silently degrading the verification to a skip. **Security:** *Wiki container base images are digest-pinned and patch-tracked* — examples/wiki/Dockerfile now pins its Chainguard base images by digest (cgr.dev/chainguard/node:<tag>@sha256:...) for reproducible local builds and supply-chain provenance, and Dependabot's docker updater bumps those digests so they keep tracking Chainguard's continuously-rebuilt CVE fixes instead of freezing. The published multi-arch image is unchanged: the release workflow still resolves the rolling tag to a fresh digest at build time and passes it as a build-arg, so the scanned image equals the published image and always carries current patches — the committed digest never reaches the image-scan gate. · *esbuild SEA-build binary pin is centralized and fails closed on an un-reviewed bump* — The reviewed per-platform SHA-256 hashes that the bundler smoke gate verifies the esbuild native compiler against are now a single source of truth (scripts/esbuild-binary-pin.json), checked by one shared verifier that runs both at release time and in the pattern gate. It enforces that the esbuild version package.json declares and the CI/publish workflows install carries a complete reviewed-hash entry for every required platform. Previously a bump to a version with no reviewed hash made the smoke gate note-and-skip the binary verification; that omission now fails the release, so the binary pin can no longer be silently lost. The hashes remain captured by hand on each bump (diff the published tarballs, sha256 the binary) — the verifier checks the pin is present, it never derives it.
|
|
12
|
+
|
|
11
13
|
- v0.15.23 (2026-06-24) — **`b.wsClient` error objects now carry a usable terminal-vs-transient signal (and the client's auto-reconnect, previously dead, works again), and `b.safePath` resolves cross-platform containment with the target platform's path semantics — fixing both a false refusal of in-base paths and a backslash-traversal escape when validating for Windows on a POSIX host.** Two correctness fixes. b.wsClient marked every WsClientError permanent, so a consumer driving its own reconnect loop could not tell a terminal handshake failure (a bad URL, a 4xx rejection, an accept-mismatch, a protocol-violation frame) from a transient one (a 5xx handshake rejection, a pong/handshake timeout, a dropped socket) and had to re-derive the taxonomy from error codes. err.permanent now reflects the actual transience of each error, the single bad-status code is split by the carried HTTP status (4xx terminal, 5xx transient) with the status exposed as err.statusCode, and — because the client's own auto-reconnect keyed off the same always-true flag — auto-reconnect was silently disabled for every transient failure and now fires correctly. Separately, b.safePath.resolve / resolveOrNull / validate performed its lexical containment with the runtime path module while the per-segment naming walk used opts.platform. The two disagreeing broke cross-platform validation both ways: every legitimate in-base path was refused with safe-path/escapes-base when opts.platform differed from the host, and — more seriously — a POSIX host validating opts.platform: "windows" accepted a backslash traversal (ok\..\..\outside), because the runtime resolver treats \ as an ordinary filename character and never collapsed the .. segments, so the path escaped the base once a Windows consumer read the backslashes. The lexical resolve and containment boundary now use the target platform's path module (node:path.win32 / node:path.posix), matching the segment walk, so in-base paths resolve and cross-platform traversals are refused under any opts.platform override; the realpath check, which touches the live filesystem, keeps its runtime resolve. **Fixed:** *b.wsClient: WsClientError carries a real terminal/transient signal, and auto-reconnect works again* — WsClientError was declared always-permanent, so err.permanent was true for every error — a consumer's reconnect loop could not distinguish a terminal handshake failure (bad URL, bad subprotocol, malformed handshake header, accept-mismatch, bad upgrade, bad status line, a 4xx rejection, an oversized/protocol-violation frame) from a transient one (a 5xx handshake rejection, a pong- or handshake-timeout, a dropped socket) and had to maintain its own error-code list tracking the framework's taxonomy. err.permanent now reflects each error's actual transience, derived from its code (a new/unknown code defaults to terminal, so it fails closed rather than redialing a hopeless target forever); the single ws-client/bad-status code is split by the response status (4xx terminal, 5xx transient) and the status is exposed as err.statusCode (the existing err.status alias is preserved). The same always-permanent flag also drove the client's built-in auto-reconnect, which therefore never retried any framework-surfaced transient failure (a 5xx handshake or a keepalive timeout); reconnect now fires for transient failures and still skips terminal ones. · *b.safePath: cross-platform containment resolves with the target platform's path semantics* — b.safePath.resolve / resolveOrNull / validate performed its lexical containment (resolve rel under base, then bound the result with a separator slice) using the runtime path module, while the per-segment naming walk used opts.platform. When the two disagreed, validation broke both ways. Benign direction: a Linux service validating server-origin names against the stricter Windows ruleset (the recommended cross-platform pattern) had every legitimate in-base path refused with safe-path/escapes-base, because the runtime-separated resolved path could never match a Windows-separator boundary. Security direction: a POSIX host validating opts.platform: "windows" accepted a backslash traversal such as ok\..\..\outside — the segment walk splits on \ for Windows, but the runtime resolver on POSIX treats \ as an ordinary filename character and never collapsed the .. segments, so the path passed containment and resolved to <base>/ok\..\..\outside, which escapes the base once a Windows consumer interprets the backslashes. The lexical resolve and the containment boundary now use the target platform's path module (node:path.win32 when validating for Windows, node:path.posix otherwise), matching the segment walk, so in-base paths resolve correctly and a genuine traversal is refused under any opts.platform override. The realpath check, which resolves symlinks on the live filesystem, keeps a separate runtime resolve because a foreign-platform path cannot be symlink-resolved on the host. opts.platform continues to gate the per-segment naming rules (reserved names, trailing dot/space, NTFS ADS colon).
|
|
12
14
|
|
|
13
15
|
- v0.15.22 (2026-06-24) — **Internal test-harness reliability only — the published library is byte-for-byte identical to 0.15.21.** This release changes only the repository's own test harness and test files; nothing under the shipped tarball (index.js, bin/, lib/) is touched, so the API, behavior, and published files are identical to 0.15.21 and operators have nothing to do. The smoke worker now attributes a fire-and-forget failure — an unawaited promise or an unreleased handle (a retry timer, a shutdown wait, a watcher/reload restart) that throws AFTER a test's assertions already passed — to the exact test that caused it and retries it once, instead of the prior unattributable, intermittent failure that only surfaced on a resource-starved CI runner. Five test files that invoked their run() at module scope (re-running and, in some cases, exiting the worker before it could report a result) are corrected to run only under require.main === module, and a test-discipline check keeps the pattern from returning. An opt-in handle-leak audit (SMOKE_AUDIT_HANDLES) surfaces tests that hold a timer / socket / server / worker past completion, for a follow-up cleanup pass. **Changed:** *Smoke test harness attributes late-async-error failures and fixes module-level test self-execution* — The forked smoke worker installs unhandledRejection / uncaughtException handlers and a settle tick so a failure that fires after a test's assertions pass — a leaked handle's callback or an unawaited promise from retry / shutdown / reload logic — is reported against the test that caused it and retried once (a transient passes, a persistent one fails again and names the bug), replacing the prior intermittent, unattributable 'fork failed' that only appeared on a starved CI runner. Five test files (two integration, three layer-0, plus two wiki-suite harnesses) that called run() at module scope are corrected to execute only under require.main === module so they no longer double-run or exit the worker prematurely when required, and a codebase-patterns test-discipline detector prevents the unguarded-module-level-run shape from recurring. A SMOKE_AUDIT_HANDLES opt-in reports per-file handle leaks for a tracked cleanup. None of this is in the published package.
|
|
@@ -22,20 +22,20 @@
|
|
|
22
22
|
# The :latest runtime image runs as `nonroot` (different user, lower
|
|
23
23
|
# privilege); --chown=nonroot:nonroot on the COPY --from=deps rewrites
|
|
24
24
|
# ownership at copy time into the runtime image.
|
|
25
|
-
# Base images are
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
ARG BASE_DEPS=cgr.dev/chainguard/node:latest-dev
|
|
38
|
-
ARG BASE_RUNTIME=cgr.dev/chainguard/node:latest
|
|
25
|
+
# Base images are digest-pinned in source (tag@sha256) for reproducible local
|
|
26
|
+
# builds and supply-chain provenance. Dependabot's docker updater bumps these
|
|
27
|
+
# digests weekly so they track Chainguard's continuous CVE rebuilds instead of
|
|
28
|
+
# freezing — the historical objection to a committed pin (it goes stale and the
|
|
29
|
+
# Trivy CRITICAL/HIGH gate rejects the build) does not apply here, because the
|
|
30
|
+
# committed digest never reaches that gate: release-container.yml resolves the
|
|
31
|
+
# rolling tag to a FRESH digest at build time and passes it via the BASE_DEPS /
|
|
32
|
+
# BASE_RUNTIME build-args below, overriding these defaults, so the scanned image
|
|
33
|
+
# always carries current patches and scan == publish. A local `docker build`
|
|
34
|
+
# with no build-args uses the pinned (Dependabot-current) digest; the
|
|
35
|
+
# human-readable tag is retained alongside the digest for readability and as the
|
|
36
|
+
# rolling source the release workflow resolves.
|
|
37
|
+
ARG BASE_DEPS=cgr.dev/chainguard/node:latest-dev@sha256:b165c3da2bc85bb4d7a4749854a087996bf51f540bc0655689ab5c43409bb741
|
|
38
|
+
ARG BASE_RUNTIME=cgr.dev/chainguard/node:latest@sha256:05d2ed6a0b3e6d3b9ca1d7f0b76e88dd06b91fa95c4f1e849f99bba7bd5a21b8
|
|
39
39
|
|
|
40
40
|
FROM ${BASE_DEPS} AS deps
|
|
41
41
|
WORKDIR /home/node
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.15.24",
|
|
4
|
+
"date": "2026-06-25",
|
|
5
|
+
"headline": "Supply-chain hardening for the build: the example wiki container's base images are digest-pinned (and tracked for patches by Dependabot), and the SEA-build esbuild binary pin is centralized and release-enforced so a version bump can't silently lose its reviewed hash",
|
|
6
|
+
"summary": "A build supply-chain hardening release; no runtime API changes. The example wiki container's Chainguard base images are now pinned by digest (tag@sha256) for reproducible builds and provenance, with Dependabot's docker updater bumping the digests so they keep tracking Chainguard's continuous CVE rebuilds; the published multi-arch image is unaffected because the release workflow still resolves the rolling tag to a fresh digest at build time, so the scanned image equals the published image and the committed digest never reaches the image-scan gate. The esbuild native-binary supply-chain pin — the reviewed per-platform SHA-256 the bundler smoke gate checks the on-disk compiler against — is centralized in one file and enforced both at release time and in the pattern gate by a single shared verifier: the version package.json and the CI/publish workflows install must carry a complete reviewed-hash entry, so bumping esbuild without re-reviewing and re-pinning the binary now fails closed instead of silently degrading the verification to a skip.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Security",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "Wiki container base images are digest-pinned and patch-tracked",
|
|
13
|
+
"body": "examples/wiki/Dockerfile now pins its Chainguard base images by digest (cgr.dev/chainguard/node:<tag>@sha256:...) for reproducible local builds and supply-chain provenance, and Dependabot's docker updater bumps those digests so they keep tracking Chainguard's continuously-rebuilt CVE fixes instead of freezing. The published multi-arch image is unchanged: the release workflow still resolves the rolling tag to a fresh digest at build time and passes it as a build-arg, so the scanned image equals the published image and always carries current patches — the committed digest never reaches the image-scan gate."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "esbuild SEA-build binary pin is centralized and fails closed on an un-reviewed bump",
|
|
17
|
+
"body": "The reviewed per-platform SHA-256 hashes that the bundler smoke gate verifies the esbuild native compiler against are now a single source of truth (scripts/esbuild-binary-pin.json), checked by one shared verifier that runs both at release time and in the pattern gate. It enforces that the esbuild version package.json declares and the CI/publish workflows install carries a complete reviewed-hash entry for every required platform. Previously a bump to a version with no reviewed hash made the smoke gate note-and-skip the binary verification; that omission now fails the release, so the binary pin can no longer be silently lost. The hashes remain captured by hand on each bump (diff the published tarballs, sha256 the binary) — the verifier checks the pin is present, it never derives it."
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// Enforces that the SEA-build tool versions installed across the build artifacts
|
|
5
|
+
// agree, and that the pinned esbuild version carries a COMPLETE reviewed
|
|
6
|
+
// binary-hash entry. Single shared checker, two callers: release.js regen runs
|
|
7
|
+
// the CLI; the codebase-patterns esbuild-pin-cross-artifact-drift detector
|
|
8
|
+
// requires checkEsbuildPin() so the same logic gates every PR.
|
|
9
|
+
//
|
|
10
|
+
// Why this exists: the smoke gate (bundler-output.test.js) verifies the esbuild
|
|
11
|
+
// native binary against the pin, but on a version it has no hash for it NOTES +
|
|
12
|
+
// SKIPS rather than failing (it can only verify what was reviewed). That is the
|
|
13
|
+
// correct runtime behaviour, but it means bumping esbuild while forgetting to
|
|
14
|
+
// capture + commit the new version's hashes silently downgrades the pin to a
|
|
15
|
+
// skip. This turns that omission into a hard failure: the version package.json /
|
|
16
|
+
// ci.yml / npm-publish.yml install MUST have a complete reviewed-hash entry. It
|
|
17
|
+
// VERIFIES the pin is present; it never derives it (deriving-and-trusting would
|
|
18
|
+
// record whatever npm served, defeating the pin).
|
|
19
|
+
//
|
|
20
|
+
// package.json devDependencies is the single version source of truth (Dependabot
|
|
21
|
+
// tracks it); esbuild-binary-pin.json carries only the reviewed hashes.
|
|
22
|
+
|
|
23
|
+
var fs = require("node:fs");
|
|
24
|
+
var path = require("node:path");
|
|
25
|
+
|
|
26
|
+
var ROOT = path.resolve(__dirname, "..");
|
|
27
|
+
var PIN_PATH = path.join(__dirname, "esbuild-binary-pin.json");
|
|
28
|
+
var HEX64_RE = /^[0-9a-f]{64}$/;
|
|
29
|
+
|
|
30
|
+
// Workflows whose SEA-build step installs the pinned tool versions. Each MUST
|
|
31
|
+
// carry an exact `<tool>@<version>` spec for every required tool: an install
|
|
32
|
+
// line that drops the version (e.g. `npm install esbuild postject`) would let CI
|
|
33
|
+
// resolve an unreviewed esbuild and silently fall back to the binary-hash smoke
|
|
34
|
+
// gate's skip path, so a missing pinned spec is itself a violation (fail-closed).
|
|
35
|
+
// If the SEA build legitimately moves out of one of these, update this list in
|
|
36
|
+
// the same change.
|
|
37
|
+
var WORKFLOWS = [
|
|
38
|
+
".github/workflows/ci.yml",
|
|
39
|
+
".github/workflows/npm-publish.yml",
|
|
40
|
+
];
|
|
41
|
+
var REQUIRED_TOOLS = ["esbuild", "postject"];
|
|
42
|
+
|
|
43
|
+
// Returns { ok: boolean, violations: [{ file, line, content }] }. Never throws on
|
|
44
|
+
// expected-missing inputs — a missing file is reported as a violation so the
|
|
45
|
+
// caller surfaces it rather than passing silently.
|
|
46
|
+
function checkEsbuildPin() {
|
|
47
|
+
var bad = [];
|
|
48
|
+
var push = function (file, content) { bad.push({ file: file, line: 0, content: content }); };
|
|
49
|
+
|
|
50
|
+
var pkg;
|
|
51
|
+
try { pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")); }
|
|
52
|
+
catch (e) { push("package.json", "unreadable: " + (e && e.message)); return { ok: false, violations: bad }; }
|
|
53
|
+
var dev = pkg.devDependencies || {};
|
|
54
|
+
var esbuildVer = dev.esbuild;
|
|
55
|
+
var postjectVer = dev.postject;
|
|
56
|
+
if (!esbuildVer) push("package.json", "devDependencies.esbuild missing — it is the SEA-build version pin");
|
|
57
|
+
if (!postjectVer) push("package.json", "devDependencies.postject missing — it is the SEA-build version pin");
|
|
58
|
+
|
|
59
|
+
var pin;
|
|
60
|
+
try { pin = JSON.parse(fs.readFileSync(PIN_PATH, "utf8")); }
|
|
61
|
+
catch (e) { push("scripts/esbuild-binary-pin.json", "unreadable: " + (e && e.message)); return { ok: false, violations: bad }; }
|
|
62
|
+
var requiredPlatforms = pin.requiredPlatforms || [];
|
|
63
|
+
var binarySha256 = pin.binarySha256 || {};
|
|
64
|
+
|
|
65
|
+
// The pinned esbuild version carries a reviewed hash for every required
|
|
66
|
+
// platform floor, each a 64-char lowercase hex digest.
|
|
67
|
+
if (esbuildVer) {
|
|
68
|
+
var verMap = binarySha256[esbuildVer];
|
|
69
|
+
if (!verMap || typeof verMap !== "object") {
|
|
70
|
+
push("scripts/esbuild-binary-pin.json",
|
|
71
|
+
"no binarySha256 entry for the pinned esbuild " + esbuildVer +
|
|
72
|
+
" (package.json devDep) — re-review the published-tarball diff + add the per-platform binary hashes on bump");
|
|
73
|
+
} else {
|
|
74
|
+
requiredPlatforms.forEach(function (plat) {
|
|
75
|
+
var h = verMap[plat];
|
|
76
|
+
if (typeof h !== "string" || !HEX64_RE.test(h)) {
|
|
77
|
+
push("scripts/esbuild-binary-pin.json",
|
|
78
|
+
"esbuild " + esbuildVer + " is missing a valid reviewed SHA-256 for required platform " +
|
|
79
|
+
plat + " (got " + JSON.stringify(h) + ")");
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Every scoped workflow installs the exact package.json versions — and MUST
|
|
86
|
+
// pin each required tool with a `<tool>@<version>` spec (a missing spec means
|
|
87
|
+
// an unpinned, unreviewed install slips through).
|
|
88
|
+
var expectedFor = { esbuild: esbuildVer, postject: postjectVer };
|
|
89
|
+
WORKFLOWS.forEach(function (rel) {
|
|
90
|
+
var src;
|
|
91
|
+
try { src = fs.readFileSync(path.join(ROOT, rel), "utf8"); }
|
|
92
|
+
catch (e) { push(rel, "unreadable: " + (e && e.message)); return; }
|
|
93
|
+
REQUIRED_TOOLS.forEach(function (tool) {
|
|
94
|
+
var expected = expectedFor[tool];
|
|
95
|
+
if (!expected) return; // already reported as a missing package.json devDep above
|
|
96
|
+
checkSpec(bad, rel, src, tool, expected);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return { ok: bad.length === 0, violations: bad };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function checkSpec(bad, rel, src, tool, expected) {
|
|
104
|
+
var re = new RegExp(tool + "@([0-9][\\w.-]*)", "g");
|
|
105
|
+
var m;
|
|
106
|
+
var found = [];
|
|
107
|
+
while ((m = re.exec(src)) !== null) { found.push(m[1]); }
|
|
108
|
+
if (found.length === 0) {
|
|
109
|
+
bad.push({ file: rel, line: 0,
|
|
110
|
+
content: rel + " has no pinned " + tool + "@<version> spec — the SEA-build install must pin " +
|
|
111
|
+
tool + "@" + expected + " (an unpinned install resolves an unreviewed version and the " +
|
|
112
|
+
"binary-hash smoke gate falls back to its skip path)" });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
found.forEach(function (ver) {
|
|
116
|
+
if (ver !== expected) {
|
|
117
|
+
bad.push({ file: rel, line: 0,
|
|
118
|
+
content: rel + " installs " + tool + "@" + ver + " but package.json devDependencies pins " +
|
|
119
|
+
tool + "@" + expected + " — bump the pin (and re-review the esbuild binary hash) or fix the workflow" });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { checkEsbuildPin: checkEsbuildPin };
|
|
125
|
+
|
|
126
|
+
if (require.main === module) {
|
|
127
|
+
var res = checkEsbuildPin();
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
res.violations.forEach(function (v) { console.error("[check-esbuild-pin] FAIL — " + v.content); });
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
console.log("[check-esbuild-pin] OK — esbuild/postject pinned consistently across package.json + " +
|
|
133
|
+
WORKFLOWS.length + " workflow(s); reviewed per-platform hashes present.");
|
|
134
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Single source of truth for the SEA-build esbuild native-binary hash pin. The pinned VERSION is package.json devDependencies.esbuild (+ postject); this file carries only the reviewed per-platform SHA-256 hashes for the versions actually reviewed. The smoke gate (test/layer-5-integration/bundler-output.test.js) verifies the on-disk binary against binarySha256; scripts/check-esbuild-pin.js (release.js regen + the codebase-patterns esbuild-pin-cross-artifact-drift detector) enforces that the version package.json/ci.yml/npm-publish.yml install has a COMPLETE reviewed-hash entry here. Hashes are captured BY HAND on every esbuild bump via the dependency-review discipline (diff the published tarballs, sha256 each native binary) and never auto-derived: deriving-and-trusting would record whatever npm served, defeating the pin. requiredPlatforms is the floor that MUST be present for the pinned version (CI ubuntu = linux-x64 authoritative; maintainer host = win32-x64).",
|
|
3
|
+
"requiredPlatforms": ["linux-x64", "win32-x64"],
|
|
4
|
+
"binarySha256": {
|
|
5
|
+
"0.28.1": {
|
|
6
|
+
"linux-x64": "0c6588b092a2c291a72bab90659f3c9e0e25e0fe59c9ac12b4dae4d945e5548c",
|
|
7
|
+
"win32-x64": "ec02ee9b14ab332416fedd10614dfb80eed5304d94f67745067c011934a8c3c3"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -230,6 +230,7 @@ function _regenArtifacts(opts) {
|
|
|
230
230
|
_run("node", ["scripts/refresh-api-snapshot.js"]);
|
|
231
231
|
_run("node", ["scripts/check-api-snapshot.js"]);
|
|
232
232
|
_run("node", ["scripts/check-changelog-extract.js"]);
|
|
233
|
+
_run("node", ["scripts/check-esbuild-pin.js"]);
|
|
233
234
|
_ok("CHANGELOG + api-snapshot regenerated");
|
|
234
235
|
}
|
|
235
236
|
|
|
@@ -12253,45 +12253,23 @@ function testWikiPortAgreesAcrossArtifacts() {
|
|
|
12253
12253
|
bad);
|
|
12254
12254
|
}
|
|
12255
12255
|
|
|
12256
|
-
// The esbuild dev-tool
|
|
12257
|
-
//
|
|
12258
|
-
//
|
|
12259
|
-
// bundler-output
|
|
12260
|
-
//
|
|
12261
|
-
// one and not the others is the v0.11.40 silent-drift
|
|
12262
|
-
// 0.28.0 while package.json declared 0.28.1, so CI verified
|
|
12263
|
-
// version.
|
|
12256
|
+
// The esbuild dev-tool is pinned across artifacts that carry no lockfile to keep
|
|
12257
|
+
// them in sync: package.json devDependencies (the version source-of-truth, also
|
|
12258
|
+
// postject), ci.yml + npm-publish.yml's exact `npm install esbuild@<v>` for the
|
|
12259
|
+
// SEA / bundler-output build, and scripts/esbuild-binary-pin.json's reviewed
|
|
12260
|
+
// per-platform binary hashes (verified on disk by the bundler-output smoke
|
|
12261
|
+
// gate). A bump that updates one and not the others is the v0.11.40 silent-drift
|
|
12262
|
+
// class: ci.yml tested 0.28.0 while package.json declared 0.28.1, so CI verified
|
|
12263
|
+
// an unreviewed version. The agreement + per-platform hash COMPLETENESS is owned
|
|
12264
|
+
// by one shared checker (scripts/check-esbuild-pin.js), called here and by
|
|
12265
|
+
// release.js regen so neither path can silently drift — a bump that forgets the
|
|
12266
|
+
// reviewed hashes fails closed instead of degrading the smoke pin to a skip.
|
|
12264
12267
|
function testEsbuildPinAgreesAcrossArtifacts() {
|
|
12265
|
-
var
|
|
12266
|
-
var
|
|
12267
|
-
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
if (!pkgVer) return;
|
|
12271
|
-
|
|
12272
|
-
var ci;
|
|
12273
|
-
try { ci = fs.readFileSync(".github/workflows/ci.yml", "utf8"); }
|
|
12274
|
-
catch (_e) { return; }
|
|
12275
|
-
var ciMatch = /esbuild@([0-9][^\s'"]*)/.exec(ci);
|
|
12276
|
-
if (ciMatch && ciMatch[1] !== pkgVer) {
|
|
12277
|
-
bad.push({ file: ".github/workflows/ci.yml", line: 0,
|
|
12278
|
-
content: "ci.yml installs esbuild@" + ciMatch[1] +
|
|
12279
|
-
" but package.json devDependencies pins esbuild " + pkgVer +
|
|
12280
|
-
" — CI would test an unreviewed version" });
|
|
12281
|
-
}
|
|
12282
|
-
|
|
12283
|
-
var bundlerTest;
|
|
12284
|
-
try { bundlerTest = fs.readFileSync("test/layer-5-integration/bundler-output.test.js", "utf8"); }
|
|
12285
|
-
catch (_e) { return; }
|
|
12286
|
-
if (bundlerTest.indexOf('"' + pkgVer + '":') === -1 && bundlerTest.indexOf("'" + pkgVer + "':") === -1) {
|
|
12287
|
-
bad.push({ file: "test/layer-5-integration/bundler-output.test.js", line: 0,
|
|
12288
|
-
content: "ESBUILD_BINARY_SHA256 has no reviewed-hash entry for esbuild " + pkgVer +
|
|
12289
|
-
" (package.json devDep) — re-review the published-tarball diff + add the binary hashes on bump" });
|
|
12290
|
-
}
|
|
12291
|
-
|
|
12292
|
-
bad = _filterMarkers(bad, "esbuild-pin-cross-artifact-drift");
|
|
12293
|
-
_report("esbuild pin agrees across package.json devDep + ci.yml install + bundler-output.test.js " +
|
|
12294
|
-
"binary-hash map (prevent a workflow / test pin silently drifting from the reviewed version)",
|
|
12268
|
+
var checkEsbuildPin = require("../../scripts/check-esbuild-pin.js").checkEsbuildPin;
|
|
12269
|
+
var bad = _filterMarkers(checkEsbuildPin().violations, "esbuild-pin-cross-artifact-drift");
|
|
12270
|
+
_report("esbuild pin agrees across package.json devDep + ci.yml/npm-publish.yml install + " +
|
|
12271
|
+
"esbuild-binary-pin.json reviewed per-platform hashes (prevent a workflow / pin silently " +
|
|
12272
|
+
"drifting from the reviewed version)",
|
|
12295
12273
|
bad);
|
|
12296
12274
|
}
|
|
12297
12275
|
|
|
@@ -161,12 +161,12 @@ function _esbuildBinaryMatchesPlatform() {
|
|
|
161
161
|
// skip otherwise — never fail on a version we never diffed, and never claim a
|
|
162
162
|
// match we can't make. On every esbuild bump, re-review the diff and add the
|
|
163
163
|
// new version's hashes here.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
164
|
+
//
|
|
165
|
+
// The pin map is the single source of truth in scripts/esbuild-binary-pin.json,
|
|
166
|
+
// shared with scripts/check-esbuild-pin.js (release.js regen), which fails the
|
|
167
|
+
// release if the version the workflows install lacks a reviewed-hash entry — so
|
|
168
|
+
// a bump can never silently degrade this verification to the skip branch below.
|
|
169
|
+
var ESBUILD_BINARY_SHA256 = require("../../scripts/esbuild-binary-pin.json").binarySha256;
|
|
170
170
|
|
|
171
171
|
function testEsbuildBinaryHashPinned() {
|
|
172
172
|
var ver = require("esbuild/package.json").version;
|
package/package.json
CHANGED