@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/SECURITY.md +22 -9
  3. package/lib/asset-manifest.json +1 -1
  4. package/lib/gift-card-ledger.js +72 -15
  5. package/lib/store-credit.js +340 -89
  6. package/lib/vendor/MANIFEST.json +53 -31
  7. package/lib/vendor/blamejs/.clusterfuzzlite/Dockerfile +7 -2
  8. package/lib/vendor/blamejs/.github/dependabot.yml +12 -0
  9. package/lib/vendor/blamejs/.github/workflows/ci.yml +16 -12
  10. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +3 -1
  11. package/lib/vendor/blamejs/.github/workflows/release-container.yml +23 -0
  12. package/lib/vendor/blamejs/CHANGELOG.md +6 -0
  13. package/lib/vendor/blamejs/api-snapshot.json +10 -2
  14. package/lib/vendor/blamejs/examples/wiki/Dockerfile +19 -2
  15. package/lib/vendor/blamejs/lib/atomic-file.js +32 -9
  16. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +105 -40
  17. package/lib/vendor/blamejs/lib/middleware/dpop.js +54 -31
  18. package/lib/vendor/blamejs/lib/middleware/span-http-server.js +7 -0
  19. package/lib/vendor/blamejs/lib/network-tls.js +12 -2
  20. package/lib/vendor/blamejs/lib/queue-local.js +123 -46
  21. package/lib/vendor/blamejs/lib/queue.js +13 -9
  22. package/lib/vendor/blamejs/lib/request-helpers.js +90 -0
  23. package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +69 -7
  24. package/lib/vendor/blamejs/lib/self-update.js +11 -2
  25. package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +11 -11
  26. package/lib/vendor/blamejs/lib/vendor/public-suffix-list.dat +6 -2
  27. package/lib/vendor/blamejs/lib/vendor/public-suffix-list.data.js +689 -688
  28. package/lib/vendor/blamejs/oss-fuzz/projects/blamejs/Dockerfile +5 -0
  29. package/lib/vendor/blamejs/package.json +1 -1
  30. package/lib/vendor/blamejs/release-notes/v0.15.17.json +52 -0
  31. package/lib/vendor/blamejs/release-notes/v0.15.18.json +49 -0
  32. package/lib/vendor/blamejs/release-notes/v0.15.19.json +18 -0
  33. package/lib/vendor/blamejs/scripts/check-vendor-currency.js +24 -0
  34. package/lib/vendor/blamejs/test/integration/queue-cluster-mysql.test.js +419 -0
  35. package/lib/vendor/blamejs/test/integration/queue-cluster-pg.test.js +471 -0
  36. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt-rejection-envelope.test.js +309 -0
  37. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +35 -8
  38. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-fd-read-errorfor-bypass.test.js +138 -0
  39. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +26 -0
  40. package/lib/vendor/blamejs/test/layer-0-primitives/dpop-htu-peergating.test.js +227 -0
  41. package/lib/vendor/blamejs/test/layer-0-primitives/queue-flow-repeat.test.js +83 -0
  42. package/lib/vendor/blamejs/test/layer-0-primitives/self-update-poll-asset-digest.test.js +90 -0
  43. package/lib/vendor/blamejs/test/layer-0-primitives/self-update-standalone-verifier-ecdsa-encoding.test.js +274 -0
  44. package/lib/vendor/blamejs/test/layer-0-primitives/tls-ocsp-freshness.test.js +192 -0
  45. package/lib/vendor/blamejs/test/layer-0-primitives/vendor-currency-classify.test.js +36 -0
  46. package/package.json +1 -1
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 ledger is hash-chained, fork-proof, and verifiable.**
315
- Every ledger entry — credits, debits, and expirations alike — links to
316
- its predecessor through a per-card SHA3-512 chain, so a direct edit or
317
- deletion of a row breaks the linkage. A uniqueness fence (one child per
318
- chain tip) makes concurrent writes serialize rather than fork the
319
- chain, and `giftCardLedger.verifyChain(cardId)` recomputes a card's
320
- chain end to end and reports the first divergence — run it whenever a
321
- card's balance is disputed. The overdraft refusal stays inside the same
322
- guarded insert, so the integrity device never weakens the balance gate.
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.87",
2
+ "version": "0.4.89",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-imfe0otYErcB8rr2h6KLSGTtStirysptpXETSPY4zLv3bZoIT75Lo1dOvkOav+xL",
@@ -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 retry re-reads
246
- // the tip, so contention resolves in one extra round trip per racing
247
- // writer; five attempts is far beyond any realistic burst.
248
- var CHAIN_WRITE_ATTEMPTS = 5;
249
-
250
- function _isChainFenceCollision(err) {
251
- var msg = (err && err.message) || "";
252
- return /UNIQUE constraint failed/i.test(msg) && /(prev_hash|chain_parent)/i.test(msg);
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
- if (_isChainFenceCollision(e)) return { collided: true };
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: true,
556
- rows_verified: rows.length - legacyPrefix,
557
- legacy_prefix: legacyPrefix,
558
- last_hash: prevHash,
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