@blamejs/blamejs-shop 0.4.87 → 0.4.89
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/SECURITY.md +22 -9
- package/lib/asset-manifest.json +1 -1
- package/lib/gift-card-ledger.js +72 -15
- package/lib/store-credit.js +340 -89
- package/lib/vendor/MANIFEST.json +53 -31
- package/lib/vendor/blamejs/.clusterfuzzlite/Dockerfile +7 -2
- package/lib/vendor/blamejs/.github/dependabot.yml +12 -0
- package/lib/vendor/blamejs/.github/workflows/ci.yml +16 -12
- package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +3 -1
- package/lib/vendor/blamejs/.github/workflows/release-container.yml +23 -0
- package/lib/vendor/blamejs/CHANGELOG.md +6 -0
- package/lib/vendor/blamejs/api-snapshot.json +10 -2
- package/lib/vendor/blamejs/examples/wiki/Dockerfile +19 -2
- package/lib/vendor/blamejs/lib/atomic-file.js +32 -9
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +105 -40
- package/lib/vendor/blamejs/lib/middleware/dpop.js +54 -31
- package/lib/vendor/blamejs/lib/middleware/span-http-server.js +7 -0
- package/lib/vendor/blamejs/lib/network-tls.js +12 -2
- package/lib/vendor/blamejs/lib/queue-local.js +123 -46
- package/lib/vendor/blamejs/lib/queue.js +13 -9
- package/lib/vendor/blamejs/lib/request-helpers.js +90 -0
- package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +69 -7
- package/lib/vendor/blamejs/lib/self-update.js +11 -2
- package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +11 -11
- package/lib/vendor/blamejs/lib/vendor/public-suffix-list.dat +6 -2
- package/lib/vendor/blamejs/lib/vendor/public-suffix-list.data.js +689 -688
- package/lib/vendor/blamejs/oss-fuzz/projects/blamejs/Dockerfile +5 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.17.json +52 -0
- package/lib/vendor/blamejs/release-notes/v0.15.18.json +49 -0
- package/lib/vendor/blamejs/release-notes/v0.15.19.json +18 -0
- package/lib/vendor/blamejs/scripts/check-vendor-currency.js +24 -0
- package/lib/vendor/blamejs/test/integration/queue-cluster-mysql.test.js +419 -0
- package/lib/vendor/blamejs/test/integration/queue-cluster-pg.test.js +471 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt-rejection-envelope.test.js +309 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +35 -8
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-fd-read-errorfor-bypass.test.js +138 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +26 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dpop-htu-peergating.test.js +227 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-flow-repeat.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/self-update-poll-asset-digest.test.js +90 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/self-update-standalone-verifier-ecdsa-encoding.test.js +274 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tls-ocsp-freshness.test.js +192 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vendor-currency-classify.test.js +36 -0
- 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.89 (2026-06-24) — **Store-credit wallet balances are now tamper-evident, and concurrent gift-card ledger writes retry reliably in production.** The store-credit ledger gains the same per-customer cryptographic hash chain the gift-card ledger already carries, so a balance can no longer be silently altered by a direct database edit. Each wallet row now links to the prior one with a SHA3-512 hash over its contents, and a new verifyChain check recomputes the chain and reports the first row that doesn't link — a denormalized balance snapshot was previously a value, not a proof, so a row edit that inflated a balance or a deletion that dropped a debit passed a balance read undetected. The same change makes the balance computation safe under concurrency through a chain-parent fence and a re-read-and-retry on contention. Separately, a latent reliability bug in the gift-card ledger's concurrent-write retry is fixed: it detected a write collision by matching the database's error text, but the production data-layer redacts that text to a generic error, so a genuine collision between two writes against the same gift card could surface as a failed request instead of retrying — both ledgers now detect a collision by re-reading the chain tip, which is robust regardless of how the error is reported. No configuration changes; the store-credit schema additions apply automatically on upgrade. **Added:** *Store-credit ledger is tamper-evident with a per-customer hash chain* — Every store-credit wallet row now carries prev_hash + row_hash columns forming a per-customer SHA3-512 chain: row_hash = SHA3-512(prev_hash || canonical-json(row fields)), anchored at a genesis sentinel for a wallet's first row. A direct database edit that inflates a balance snapshot, or a deletion that drops a debit, breaks the chain at that row. A new storeCredit.verifyChain(customer_id) walks the wallet and returns the first divergence (or confirms the chain is intact); a wallet whose hash columns have all been cleared reads as unanchored rather than trusting the unverifiable rows. Because deleting a chain's most-recent rows leaves the remaining prefix internally consistent — a truncation an append-only chain can't detect on its own — verifyChain optionally accepts a trusted anchor (a row count and head hash captured from an earlier snapshot) and reports a possible truncation when the chain has fallen below it. The balance and occurred-at values are now computed in the application and protected under concurrency by a chain-parent uniqueness fence — a write derived from a stale view of the wallet collides and is retried against the current tip, so the denormalized balance stays exact under concurrent credits, debits, expirations, and the scheduled expiry sweep. **Fixed:** *Concurrent gift-card and store-credit ledger writes retry reliably in production* — The ledger retries a write that loses a race against a concurrent write to the same entity. It previously recognized that collision by matching the database engine's constraint-violation text — but the production data-layer redacts that text to a generic error, so in production a genuine collision was not recognized and the write surfaced as a failed request instead of retrying (the path only worked under the in-memory test engine, where the full error text survives). Both the gift-card ledger and the new store-credit chain now detect a collision by re-reading the chain tip after a failed write: if the tip has advanced past the row the write tried to extend, a competing write claimed the slot and the write is retried; otherwise the original error is surfaced. The retry is now robust regardless of how the underlying error is reported.
|
|
12
|
+
|
|
13
|
+
- v0.4.88 (2026-06-24) — **Refreshes the vendored framework to 0.15.19 and makes the vendored-dependency SBOM a committed, drift-guarded artifact.** Two supply-chain changes, no application configuration changes. The vendored blamejs framework moves from 0.15.16 to 0.15.19, picking up a set of upstream correctness and confidentiality fixes that ride inside the primitives the store already composes: a rejection on an established encrypted-API session is now returned inside the encrypted session envelope instead of disclosing which check failed in cleartext; stapled-OCSP response-freshness validation is restored; the DPoP proof's request-URI reconstruction now peer-gates forwarded scheme/host headers; the bundled public-suffix list is refreshed; and a family of credential/certificate verifiers continues to fail closed on an unparseable timestamp. Separately, the vendored-dependency SBOM (sbom.vendored.cdx.json) is now committed to the repository and mechanically projected from the vendored manifest, so its component versions can never silently fall behind what actually ships under lib/vendor/: a build gate fails on any drift, and the published, signed SBOM is the same verified file rather than one regenerated at publish time. Upgrade to pick both up. **Added:** *Vendored-dependency SBOM is a committed, drift-guarded artifact* — sbom.vendored.cdx.json — the CycloneDX 1.6 SBOM describing what ships under lib/vendor/ — is now committed to the repository and generated as a deterministic projection of lib/vendor/MANIFEST.json (the source of truth for the vendored version, license, and source). The vendor refresh regenerates it automatically, and a build gate fails if the committed SBOM drifts from the vendored manifest, so its component versions can never silently fall behind the vendored tree — the same protection the changelog, the release-notes rollup, and the asset-manifest already carry. The signed SBOM published as a release asset is now this same verified file rather than one regenerated at publish time, so the artifact downstream scanners consume is byte-reproducible from the committed source. **Changed:** *Vendored blamejs framework refreshed to 0.15.19* — The vendored framework moves from 0.15.16 to 0.15.19. The changes are upstream correctness and confidentiality fixes that ride inside the primitives the store already uses, with no API or configuration change: a rejection on an established encrypted-API session is now encrypted inside the session envelope rather than returned as a cleartext reason that disclosed which check failed; stapled-OCSP response-freshness enforcement (RFC 6960) is restored after a parsing regression had it reject every response; the DPoP htu reconstruction and the new request-host resolver only honor forwarded scheme/host headers from a declared trusted proxy; the bundled Mozilla public-suffix list (used for cookie-scope and same-site decisions) is refreshed to current upstream; and the credential/certificate validity-window verifiers continue to refuse a present-but-unparseable timestamp instead of skipping the check.
|
|
14
|
+
|
|
11
15
|
- v0.4.87 (2026-06-22) — **Adding a bundle now charges the advertised bundle price even when a member item is already in the cart.** Fixes a pricing bug when adding a product bundle to a cart that already contains one of the bundle's items. Adding a bundle allocates the discounted bundle price across its members and writes each member to the cart at its allocated per-unit price. But when a member variant was already in the cart on its own, the add merged into the existing line and kept that line's standalone list price, so the cart subtotal came out above the advertised bundle price — the shopper was quoted the discounted bundle but charged closer to the full sum of parts. Adding a bundle now adds the bundle's units at the bundle price while preserving any units already in the cart: a fresh bundle add realizes exactly the advertised bundle price, and a bundle added on top of a standalone item keeps that item and adds the bundle at its discounted price (rather than dropping the item or extending the discount to it). No configuration changes; upgrade to pick it up. **Fixed:** *Bundle add honors the bundle price without disturbing existing cart units* — Adding a bundle now adds each member's units at the bundle's allocated per-unit price. On a fresh line the realized subtotal is exactly the advertised bundle price. When the variant is already in the cart, its existing units are kept at their value and the bundle's units are added at the bundle price, so the shopper is neither overcharged (the previous behavior charged the standalone list price for the bundle unit) nor has their existing quantity dropped. Adding the same bundle twice adds two bundles' worth of units. Direct add-to-cart of a standalone item is unchanged.
|
|
12
16
|
|
|
13
17
|
- v0.4.86 (2026-06-22) — **Failed outbound webhook deliveries are now retried automatically on the backoff schedule, not only by a manual click.** Restores the automatic retry guarantee for outbound webhooks. When an order-lifecycle or low-stock delivery to a subscriber endpoint fails, the delivery already records its next retry time and walks a backoff schedule toward the dead-letter queue — but nothing was ever driving those retries, so a delivery only re-attempted when an operator hand-clicked retry in the admin console. A briefly-unreachable receiver (a deploy, a load-balancer reload) therefore lost every event in the gap. A scheduled tick now re-attempts every due delivery once a minute, so a failed delivery is retried automatically — at one minute, five minutes, thirty minutes, four hours, then a day — until it succeeds or lands in the dead-letter queue, with no manual intervention. The manual retry remains available as an immediate override. No configuration changes; upgrade to pick it up. **Fixed:** *Outbound webhook retries now run automatically* — A scheduled retry pass re-attempts every delivery whose backoff has come due, advancing it through the retry schedule to success or to the dead-letter queue without an operator click. Each delivery is claimed atomically before it is attempted, so two overlapping passes can never re-send the same delivery twice, and a receiver that recovers within the backoff window now receives the events it missed. An operator can still force an immediate retry from the admin console. **Security:** *Bundled framework refreshed to blamejs 0.15.16* — Picks up a broad fail-closed and resource-exhaustion hardening pass that rides inside the composed primitives: a present-but-unparseable timestamp on a credential or certificate verifier (DKIM, ARC, SAML NotBefore, FIDO/BIMI/S-MIME validity windows) now refuses the credential instead of silently skipping the check; DMARC refuses a record with no usable policy rather than defaulting a failing message to delivery; outbound clients enforce an overall wall-clock timeout (not only an idle timer) so a byte-trickling peer can no longer hold a request open indefinitely; DNS lookups gain a default deadline; and append-only log sinks refuse a symlinked path. No configuration changes.
|
package/SECURITY.md
CHANGED
|
@@ -59,6 +59,14 @@ cosign verify-blob sbom.cdx.json \
|
|
|
59
59
|
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com'
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
`sbom.vendored.cdx.json` is committed to the repository and generated
|
|
63
|
+
as a deterministic projection of `lib/vendor/MANIFEST.json`. The vendor
|
|
64
|
+
refresh regenerates it, and a build gate (`build-vendored-sbom.js
|
|
65
|
+
--check`) fails if it drifts from the vendored manifest, so the signed
|
|
66
|
+
SBOM you verify is byte-reproducible from the committed source — its
|
|
67
|
+
component versions can never silently fall behind what ships under
|
|
68
|
+
`lib/vendor/`.
|
|
69
|
+
|
|
62
70
|
### 4. Byte digests + ML-DSA-65 signature (PQC-first)
|
|
63
71
|
|
|
64
72
|
The release tarball ships with three digest / signature sidecars
|
|
@@ -311,15 +319,20 @@ node -e "
|
|
|
311
319
|
(success / failure / denied) and paginated. Opening the log is itself
|
|
312
320
|
recorded (an `audit.read` row), so reviewing the audit trail leaves
|
|
313
321
|
its own forensic mark.
|
|
314
|
-
- **The gift-card
|
|
315
|
-
Every ledger entry — credits, debits, and
|
|
316
|
-
its predecessor through a
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
chain
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
322
|
+
- **The gift-card and store-credit ledgers are hash-chained, fork-proof,
|
|
323
|
+
and verifiable.** Every ledger entry — credits, debits, and
|
|
324
|
+
expirations alike — links to its predecessor through a SHA3-512 chain,
|
|
325
|
+
keyed per card (gift cards) or per customer (store credit), so editing a
|
|
326
|
+
row or deleting one from the middle breaks the linkage. A uniqueness
|
|
327
|
+
fence (one child per chain tip) makes concurrent writes serialize rather
|
|
328
|
+
than fork the chain. `giftCardLedger.verifyChain(cardId)` /
|
|
329
|
+
`storeCredit.verifyChain(customerId)` recompute a ledger's chain end to
|
|
330
|
+
end and report the first divergence — run them whenever a balance is
|
|
331
|
+
disputed; pass a trusted anchor (a row count + head hash from an earlier
|
|
332
|
+
snapshot) to also catch a truncation that deletes the most-recent rows,
|
|
333
|
+
which an append-only chain can't detect on its own. The overdraft
|
|
334
|
+
refusal stays inside the same guarded insert, so the integrity device
|
|
335
|
+
never weakens the balance gate.
|
|
323
336
|
- **Privacy exports hold the whole record; erasure states a basis per
|
|
324
337
|
domain.** A subject-access export walks every table that keys a row
|
|
325
338
|
by the customer — identity, orders, subscriptions, addresses, saved
|
package/lib/asset-manifest.json
CHANGED
package/lib/gift-card-ledger.js
CHANGED
|
@@ -242,15 +242,14 @@ function create(opts) {
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
// How many times a writer re-derives the chain tip when the parent fence
|
|
245
|
-
// refuses its INSERT (another write landed first). Each
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
245
|
+
// refuses its INSERT (another write landed first). Each fence round lets
|
|
246
|
+
// exactly ONE racing writer win, so a burst of N concurrent writes to the
|
|
247
|
+
// SAME card needs up to N rounds — the cap is sized well beyond any
|
|
248
|
+
// realistic same-card fan-in so a legitimate burst is never dropped with
|
|
249
|
+
// GIFT_CARD_LEDGER_CONTENTION. A genuine non-collision insert error
|
|
250
|
+
// re-throws on the first attempt (see _attemptChainedWrite), so a high cap
|
|
251
|
+
// never spins on a real failure.
|
|
252
|
+
var CHAIN_WRITE_ATTEMPTS = 64;
|
|
254
253
|
|
|
255
254
|
// One fenced write attempt against a tip the caller just read. EVERY
|
|
256
255
|
// event kind writes through here so every row participates in the
|
|
@@ -295,7 +294,16 @@ function create(opts) {
|
|
|
295
294
|
if (Number(res.rowCount || 0) === 0) return { refused: true };
|
|
296
295
|
return { written: { id: id, balance_after_minor: d.after, occurred_at: d.ts } };
|
|
297
296
|
} catch (e) {
|
|
298
|
-
|
|
297
|
+
// State-agnostic collision detection — never error-message matching:
|
|
298
|
+
// the production D1 service-binding redacts the SQLite "UNIQUE
|
|
299
|
+
// constraint failed" text to a generic "HTTP 500", so a message regex
|
|
300
|
+
// is blind in prod (it would only match under node:sqlite, where the
|
|
301
|
+
// full text survives). Re-read the tip — if it advanced past the
|
|
302
|
+
// parent we tried to chain off, a competing write claimed our slot (a
|
|
303
|
+
// genuine fence collision) and we retry; if the tip is unchanged the
|
|
304
|
+
// insert failed for another reason, so re-throw.
|
|
305
|
+
var after = await _readLatest(giftCardId);
|
|
306
|
+
if (after.row_hash !== latest.row_hash) return { collided: true };
|
|
299
307
|
throw e;
|
|
300
308
|
}
|
|
301
309
|
}
|
|
@@ -483,8 +491,26 @@ function create(opts) {
|
|
|
483
491
|
// leaves behind) fails: an all-NULL chain is unanchored, not valid.
|
|
484
492
|
// An empty ledger (zero rows) is the only no-anchor case that passes.
|
|
485
493
|
// O(n) per card; operator-audit use, not hot-path.
|
|
486
|
-
verifyChain: async function (giftCardId) {
|
|
494
|
+
verifyChain: async function (giftCardId, opts) {
|
|
487
495
|
_uuid(giftCardId, "gift_card_id");
|
|
496
|
+
opts = opts || {};
|
|
497
|
+
// Optional trusted anchor (opts.anchor = { count, head } from a prior
|
|
498
|
+
// snapshot's total_rows + last_hash) — the ONLY way to detect a TAIL
|
|
499
|
+
// TRUNCATION or whole-chain replacement, since an append-only chain
|
|
500
|
+
// stays internally consistent after its tail is removed. The row at the
|
|
501
|
+
// snapshot's position must still carry the snapshot's head hash (it
|
|
502
|
+
// survives later valid appends). Without an anchor, truncation is not
|
|
503
|
+
// ruled out and anchor_checked:false is reported (operator-audit-log
|
|
504
|
+
// closes the same gap with signed checkpoints).
|
|
505
|
+
var anchor = null;
|
|
506
|
+
if (opts.anchor != null) {
|
|
507
|
+
if (typeof opts.anchor !== "object"
|
|
508
|
+
|| typeof opts.anchor.count !== "number" || !Number.isInteger(opts.anchor.count) || opts.anchor.count < 1
|
|
509
|
+
|| typeof opts.anchor.head !== "string" || opts.anchor.head.length !== SHA3_512_HEX_LEN) {
|
|
510
|
+
throw new TypeError("giftCardLedger.verifyChain: anchor must be { count: positive integer, head: " + SHA3_512_HEX_LEN + "-hex-char string }");
|
|
511
|
+
}
|
|
512
|
+
anchor = opts.anchor;
|
|
513
|
+
}
|
|
488
514
|
var r = await query(
|
|
489
515
|
"SELECT * FROM gift_card_ledger WHERE gift_card_id = ?1 " +
|
|
490
516
|
"ORDER BY occurred_at ASC, id ASC",
|
|
@@ -551,11 +577,42 @@ function create(opts) {
|
|
|
551
577
|
reason: "unanchored chain (no hashed row in a populated ledger)",
|
|
552
578
|
};
|
|
553
579
|
}
|
|
580
|
+
// Internal walk is clean. With a trusted anchor, also rule out a tail
|
|
581
|
+
// truncation below the snapshot: the row at the snapshot's position
|
|
582
|
+
// must still carry the snapshot's head hash, and the chain must not be
|
|
583
|
+
// shorter than the snapshot.
|
|
584
|
+
if (anchor) {
|
|
585
|
+
if (rows.length < anchor.count) {
|
|
586
|
+
return {
|
|
587
|
+
ok: false,
|
|
588
|
+
rows_verified: rows.length - legacyPrefix,
|
|
589
|
+
legacy_prefix: legacyPrefix,
|
|
590
|
+
anchor_checked: true,
|
|
591
|
+
reason: "row count below anchor (possible tail truncation)",
|
|
592
|
+
expected_count: anchor.count,
|
|
593
|
+
actual_count: rows.length,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
var anchorRow = rows[anchor.count - 1];
|
|
597
|
+
if (!anchorRow || anchorRow.row_hash !== anchor.head) {
|
|
598
|
+
return {
|
|
599
|
+
ok: false,
|
|
600
|
+
rows_verified: rows.length - legacyPrefix,
|
|
601
|
+
legacy_prefix: legacyPrefix,
|
|
602
|
+
anchor_checked: true,
|
|
603
|
+
reason: "anchor row hash mismatch (possible tail truncation or divergence)",
|
|
604
|
+
expected: anchor.head,
|
|
605
|
+
actual: anchorRow ? anchorRow.row_hash : null,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
}
|
|
554
609
|
return {
|
|
555
|
-
ok:
|
|
556
|
-
rows_verified:
|
|
557
|
-
legacy_prefix:
|
|
558
|
-
|
|
610
|
+
ok: true,
|
|
611
|
+
rows_verified: rows.length - legacyPrefix,
|
|
612
|
+
legacy_prefix: legacyPrefix,
|
|
613
|
+
total_rows: rows.length,
|
|
614
|
+
last_hash: prevHash,
|
|
615
|
+
anchor_checked: anchor != null,
|
|
559
616
|
};
|
|
560
617
|
},
|
|
561
618
|
|