@blamejs/blamejs-shop 0.3.62 → 0.3.64

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 CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.3.x
10
10
 
11
+ - v0.3.64 (2026-06-04) — **Google Pay and Apple Pay express buttons can run on the payment page.** The app-level Permissions-Policy denies the Payment Request API everywhere — including the payment page, the one surface whose job is payment — so Stripe's express wallet buttons (Google Pay, Apple Pay) could not operate and the browser console filled with policy violations. The payment page now sends a route-scoped Permissions-Policy that grants the payment feature to the page itself, Stripe, and Google Pay, while every other feature stays denied and every other route keeps the fully strict header — the same route-scoping pattern the payment pages already use for their Content-Security-Policy. Card payments and 3-D Secure challenges were verified live before and after; a remaining cosmetic Trusted-Types console message from Stripe's loader is documented in code with the test recipe for future verification, and does not affect card, wallet, or 3-D Secure flows. **Fixed:** *Payment page grants the Payment Request API to Stripe and Google Pay* — The default Permissions-Policy sets payment=() sitewide, which blocks the Payment Request API that Stripe's express wallet buttons need. The /pay route now emits the same policy with exactly one change — payment=(self "https://js.stripe.com" "https://pay.google.com") — derived from the same source as the default header so the two can never drift apart. Camera, microphone, geolocation, and every other feature remain denied on the payment page, and all other routes keep payment=(). Tests pin the scoped header on the payment route, the strict header elsewhere, and that exactly one feature differs from the default.
12
+
13
+ - v0.3.63 (2026-06-04) — **Digital-only carts can check out, and the outbound TLS posture is documented end to end.** A cart containing only digital goods could never complete checkout: the shipping resolver correctly filters physical services out for a cart that ships nothing, but checkout then demanded the configured default physical service resolve anyway and refused the order. Digital-only carts now select the operator's digital-shipping service when one is configured, and otherwise complete with a synthesized zero-cost no-shipping selection; a physical cart with a misconfigured service id still fails loudly, since that is a real configuration error. Separately, every outbound TLS connection has been audited against the framework's post-quantum-first key-exchange posture: of all fixed external hosts, only the Stripe API edge refuses the post-quantum groups (already handled by the payment module's explicit agent) — the CAPTCHA providers, PayPal, and Cloudflare all negotiate hybrids cleanly. Operator-supplied destinations — webhook receiver URLs and admin media source URLs — deliberately keep the post-quantum-first default, with the failure mode documented where operators will look for it: a receiver that cannot negotiate a hybrid group sees deliveries fail with a TLS handshake failure, retry on the backoff schedule, and land in the dead-letter queue; a media source URL that cannot fails as the existing clean fetch error. A regression test pins that these dials never silently downgrade. **Changed:** *Outbound TLS posture audited and documented* — Every outbound connection was audited against the framework's post-quantum-first TLS key-exchange list. Fixed external hosts: the Stripe API edge is the only one that refuses the post-quantum groups, and the payment module's explicitly scoped agent already covers it (PayPal currently negotiates hybrids; the agent remains as defense). Operator-supplied destinations keep the post-quantum-first default deliberately: an outbound-webhook receiver that cannot negotiate a hybrid group sees deliveries fail with a TLS handshake failure (alert 40), retry on the backoff schedule, and reach the dead-letter queue after the fifth attempt — front such a receiver with a post-quantum-capable proxy or upgrade its TLS stack; an admin media source URL that cannot negotiate fails as the existing clean fetch error, and the file can be uploaded directly instead. Module documentation now states this where the integrations are configured, and a regression test pins that these dials never silently downgrade. **Fixed:** *Digital-only carts complete checkout* — Shipping rate resolution offers only digital-shipping services to a cart in which no line requires shipping — so the configured default physical service id could never resolve, and checkout refused every all-digital order. Checkout now falls back for all-digital carts: the operator's digital-shipping service is selected when configured, and a zero-cost "No shipping required" selection is synthesized otherwise, so the order completes and records zero shipping. Physical carts are unchanged: an unresolvable service id still fails at the boundary, because that is a real configuration typo.
14
+
11
15
  - v0.3.62 (2026-06-04) — **Live card payments work, and back-in-stock unsubscribe is token-gated.** Checkout failed at the charge step on Stripe-configured deploys: the framework's outbound HTTP client offers only post-quantum hybrid TLS key-exchange groups, and the Stripe and PayPal API edges do not negotiate any of them yet, so every processor call died with a TLS handshake failure (alert 40) and checkout rendered a server error. The payment module now dials the processors through an explicitly constructed agent — TLS 1.3 minimum, the platform's default key-exchange groups — which is the framework's prescribed pattern for a peer that cannot meet the post-quantum list: the downgrade is visible in the code, scoped to the processor connections only, and every other outbound connection keeps the post-quantum-first default. Verified against the live Stripe API. The downgrade reverts when the processor edges add hybrid key-exchange support. Separately, the checkout error page no longer echoes upstream failure detail (such as TLS library internals) to the customer — that detail goes to the audit log, and the page states plainly that nothing was charged. This release also token-gates back-in-stock unsubscribe: cancelling a back-in-stock alert previously took only the subscriber's email address and the product SKU — guessable values, so a third party could silently cancel someone else's restock notification. Unsubscribe is now authorized by an opaque per-subscription token, carried in the confirmation and notification emails as a one-click link, exactly like the newsletter's unsubscribe. Tokens are stored only as keyed hashes, compared in constant time, and unknown tokens get the same response as valid ones, so the endpoint is not a subscription-existence oracle. A schema migration adds the token column; it applies automatically on deploy. **Changed:** *Back-in-stock unsubscribe requires the emailed token* — POST /stock-alert/unsubscribe no longer accepts an email + SKU pair; it requires the per-subscription token from the unsubscribe link in the confirmation or back-in-stock email. The token is minted at subscribe time for the confirmation mail and rotated per notification send, stored only as a keyed hash, and verified with a constant-time compare; bad or unknown tokens return the same uniform response as valid ones. The one-click link works without cookies or a signed-in session, so mail clients can fire it directly; each email carries the token that is current for its send. Operators upgrading: migration 0207 adds the token column and applies with the normal deploy. **Fixed:** *Stripe and PayPal connections complete their TLS handshake* — The outbound HTTP client's default TLS posture offers exclusively post-quantum hybrid key-exchange groups. Processor API edges currently answer that ClientHello with handshake_failure (TLS alert 40), so the first real charge attempt on a Stripe-configured deploy failed checkout with a server error. The payment module now passes its own connection agent on every processor call — TLS 1.3 minimum is kept, key exchange falls back to the platform defaults — while the client's SSRF pinning, retries, and response caps stay in force. The agent is scoped to the payment-processor dials; all other outbound traffic keeps the post-quantum-first posture. A regression test pins the agent's presence and configuration on the charge path. · *Checkout server errors no longer echo upstream detail to the customer* — When the charge step failed, the checkout error page interpolated the underlying error string — in this case raw TLS library internals — into the customer-facing message. Server-side failure detail now goes to the audit log; the customer sees a plain recoverable message that confirms nothing was charged. Field-validation messages, which are written for customers, are unaffected.
12
16
 
13
17
  - v0.3.60 (2026-06-04) — **Exact integer math for percentage discounts and insurance premiums, and an honest Add-a-card state under partial Stripe configuration.** Every place that computes a percentage of a money amount — automatic percent-off discounts, bundle discounts and their per-line allocation, partial-refund policy amounts, and shipping-insurance premiums — now uses exact integer arithmetic instead of a floating-point intermediate. On very large amounts the float product loses precision past 2^53 and can drift the rounded result by a minor unit; charged totals were never affected (the charge path was already integer end-to-end), but displayed and clamped values now match exact math everywhere, with each site's rounding direction preserved. Separately, the account Payment-methods screen no longer links into a 503 when the Stripe publishable key is missing: the Add-a-card affordance now renders as a disabled control with an explanatory note until the configuration is complete, matching how the unconfigured checkout behaves. **Fixed:** *Percentage-of-amount math is exact integer arithmetic* — Automatic percent-off discounts, bundle discounts, the cart's per-line bundle allocation, partial-refund policy amounts, and shipping-insurance premiums all computed `amount × basis_points / 10000` through a floating-point multiply. Amounts and rates are both integers, so the computation is now exact integer arithmetic with each site's rounding direction unchanged — half-away-from-zero for discounts, floor for allocations and refunds, ceiling for insurance premiums (the insurer is never short). The drift was only reachable on very large amounts (products beyond 2^53) and never touched a charged total, which was already integer end-to-end; tests now pin exact results on amounts past the float-precision boundary. · *Add a card renders honestly when Stripe configuration is incomplete* — Saving a card needs the Stripe publishable key in addition to the server-side API keys. With the server keys set but the publishable key missing, the Payment-methods screen rendered a live "Add a card" link that dead-ended on a 503. The link now gates on the same condition the add route enforces: when the publishable key is absent it renders as a disabled control with a note explaining the missing configuration, while listing, choosing a default, and removing saved cards keep working. The route's own 503 remains as defense in depth for a directly typed URL.
package/lib/admin.js CHANGED
@@ -1172,6 +1172,18 @@ function mount(router, deps) {
1172
1172
  // records the media row. Endpoint is omitted entirely when no
1173
1173
  // r2_bridge is wired (operator hasn't set D1_BRIDGE_URL +
1174
1174
  // D1_BRIDGE_SECRET).
1175
+ //
1176
+ // TLS posture — `source_url` is operator-pasted and arbitrary, so the
1177
+ // fetch keeps b.httpClient's PQC-first default (ML-KEM hybrid groups,
1178
+ // TLS 1.3 minimum). A `source_url` host that doesn't negotiate an
1179
+ // ML-KEM hybrid group answers with a handshake_failure (TLS alert 40);
1180
+ // that surfaces below as the same clean 502 `source-fetch-failed`
1181
+ // (`e.message` carries the alert) the operator already retries by
1182
+ // re-submitting. We do NOT pin a classical-downgrade agent here the
1183
+ // way the PSP adapters do for their two fixed endpoints — an arbitrary
1184
+ // operator-supplied URL is not a fixed processor edge, so holding the
1185
+ // PQC default is the deliberate posture. Operators sourcing from a
1186
+ // host that can't meet the PQC list upload the file directly instead.
1175
1187
  if (r2) {
1176
1188
  // Fetch → store → attach, shared by the JSON upload route and the
1177
1189
  // browser POST alias. Throws TypeError on bad input (mapped to 400);
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.3.62",
2
+ "version": "0.3.64",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-6k53cvkRrxMgmeStLIoLjVXZQHqIJgTmv1Izd8TYhh1HOC4POgE6GCvx1bsalyEP",
package/lib/checkout.js CHANGED
@@ -631,8 +631,25 @@ function create(deps) {
631
631
  }
632
632
  }
633
633
  if (!selected) {
634
- throw new TypeError("checkout: selected_shipping_id " + JSON.stringify(input.selected_shipping_id) +
635
- " not available for ship_to");
634
+ // All-digital cart: shipping.rates() deliberately filters the
635
+ // physical services out, so the caller's default (a physical
636
+ // service id like "std") can never resolve — that is the cart's
637
+ // shape, not a config error. Prefer the operator's digital_only
638
+ // offering when one is configured; otherwise synthesize the
639
+ // zero-cost no-shipping selection so a digital-only order
640
+ // completes. A physical cart with an unresolvable id still
641
+ // throws — that IS a config typo and must surface.
642
+ var anyShippable = enrichedLines.some(function (l) {
643
+ return l.requires_shipping === undefined ? true : !!l.requires_shipping;
644
+ });
645
+ if (!anyShippable) {
646
+ selected = ratesRow.services.length
647
+ ? ratesRow.services[0]
648
+ : { id: "digital_none", label: "No shipping required", amount_minor: 0, digital_only: true };
649
+ } else {
650
+ throw new TypeError("checkout: selected_shipping_id " + JSON.stringify(input.selected_shipping_id) +
651
+ " not available for ship_to");
652
+ }
636
653
  }
637
654
  }
638
655
 
@@ -371,6 +371,59 @@ function scopedCsp(keys) {
371
371
  }).join("; ") + ";";
372
372
  }
373
373
 
374
+ // ---- route-scoped Permissions-Policy (payment surface) ------------------
375
+ //
376
+ // The app-level Permissions-Policy is the vendored strict denylist
377
+ // (DEFAULT_PERMISSIONS) — every powerful API, including `payment=()`,
378
+ // disabled in every document. That is correct everywhere EXCEPT the one
379
+ // page whose entire job is taking a payment: GET /pay/:order_id mounts
380
+ // Stripe's Express Checkout Element, whose Google Pay / Apple Pay buttons
381
+ // drive the Payment Request API. Under `payment=()` the browser refuses
382
+ // the API inside the cross-origin pay.google.com / Stripe wallet frames
383
+ // ("Permissions policy violation: payment is not allowed in this
384
+ // document"), so the wallet express buttons are degraded on the payment
385
+ // page. The card form is unaffected (it doesn't use the Payment Request
386
+ // API), which is why card captures complete while wallets don't.
387
+ //
388
+ // `scopedPermissionsPolicy()` returns a Permissions-Policy string for
389
+ // `res.setHeader("permissions-policy", ...)` on the /pay route's response:
390
+ // byte-identical to the vendored default EXCEPT the `payment=()` entry,
391
+ // which becomes `payment=(self "https://js.stripe.com" "https://pay.google.com")`
392
+ // — granting the feature ONLY to the same origin + the Stripe SDK frame +
393
+ // the Google Pay frame, ONLY on that one response. setHeader OVERWRITES, so
394
+ // the app-level strict header still governs every OTHER route. Every other
395
+ // feature in the denylist stays `()`. Derived from the vendored
396
+ // DEFAULT_PERMISSIONS array (the single source the app-level header is also
397
+ // built from in `securityHeadersOpts` → the vendored default), never a
398
+ // hand-forked copy of the list.
399
+
400
+ // The allowlist that replaces `payment=()` on the pay surface: same origin
401
+ // (the pay page's own form), the Stripe SDK frame, and the Google Pay frame
402
+ // the Stripe Express Checkout Element spawns. Apple Pay rides the same
403
+ // `payment` feature and needs no extra origin (Safari grants it to the
404
+ // top-level same-origin document). Quote-wrapped per RFC 9651 structured
405
+ // fields. Single source — every page that enables the payment feature names
406
+ // it here.
407
+ var _PAYMENT_ALLOWLIST = 'payment=(self "https://js.stripe.com" "https://pay.google.com")';
408
+
409
+ /**
410
+ * Build a route-scoped Permissions-Policy string from the vendored strict
411
+ * denylist with the `payment` feature re-enabled for the same origin + the
412
+ * Stripe / Google Pay wallet frames. Every other feature is carried through
413
+ * verbatim as `feature=()`. Returns the string for the pay route's
414
+ * `res.setHeader("permissions-policy", ...)`; the app-level strict header is
415
+ * unchanged on every other route (setHeader overwrites this one response).
416
+ *
417
+ * Called per-response on the pay render path, so it never throws — it maps
418
+ * over the vendored default array and swaps the one entry, failing safe (a
419
+ * missing `payment` entry simply yields the strict default for that feature).
420
+ */
421
+ function scopedPermissionsPolicy() {
422
+ return _vendoredSecurityHeaders.DEFAULT_PERMISSIONS.map(function (entry) {
423
+ return entry.indexOf("payment=") === 0 ? _PAYMENT_ALLOWLIST : entry;
424
+ }).join(", ");
425
+ }
426
+
374
427
  /**
375
428
  * Build the GLOBAL rate-limit options for createApp's
376
429
  * `middleware.rateLimit`. Token-bucket so a bursty-but-bounded browsing
@@ -519,6 +572,7 @@ module.exports = {
519
572
  globalRateLimitOpts: globalRateLimitOpts,
520
573
  mountRouteGuards: mountRouteGuards,
521
574
  scopedCsp: scopedCsp,
575
+ scopedPermissionsPolicy: scopedPermissionsPolicy,
522
576
  CSP_HOSTS: CSP_HOSTS,
523
577
  WEBHOOK_PATHS: WEBHOOK_PATHS,
524
578
  HEALTH_PATH: HEALTH_PATH,
package/lib/storefront.js CHANGED
@@ -12553,7 +12553,46 @@ function mount(router, deps) {
12553
12553
  // pay.js island load — without relaxing the app-level strict CSP that
12554
12554
  // governs every OTHER route. setHeader OVERWRITES the app-level header
12555
12555
  // for this response only.
12556
+ //
12557
+ // KNOWN, NON-BLOCKING Trusted Types violation on this route (operator
12558
+ // follow-up, intentionally NOT fixed here). The app-level CSP carried
12559
+ // through verbatim by scopedCsp keeps `require-trusted-types-for
12560
+ // 'script'` + `trusted-types 'allow-duplicates' default`. Stripe.js v3
12561
+ // does NOT register a named Trusted Types policy of its own; instead it
12562
+ // expects the APPLICATION to define a `default` policy whose
12563
+ // createScriptURL vets Stripe's own hosts (per Stripe's integration
12564
+ // security guide). When Stripe.js dynamically injects its sub-resource
12565
+ // <script> (the frame-spawning performance path on *.js.stripe.com), the
12566
+ // browser refuses the TrustedScriptURL assignment because no `default`
12567
+ // policy is registered — the console logs "This document requires
12568
+ // 'TrustedScriptURL' assignment ... @ https://js.stripe.com/v3/". The
12569
+ // card form is unaffected: it loads from the STATIC <script
12570
+ // src="https://js.stripe.com/v3/"> tag, which is a direct HTML src (not
12571
+ // a JS-driven sink), so require-trusted-types-for does not gate it —
12572
+ // which is why card captures complete with the violation present. The
12573
+ // blocked path is Stripe's dynamic sub-frame/3DS loader.
12574
+ //
12575
+ // We do NOT loosen Trusted Types to silence it. Naming a Stripe policy
12576
+ // in the trusted-types directive does nothing (Stripe registers none);
12577
+ // the only fix Stripe documents is the app shipping a same-origin
12578
+ // `default` createScriptURL policy that allows js.stripe.com /
12579
+ // *.js.stripe.com AND widening this scoped CSP's script-src/frame-src to
12580
+ // *.js.stripe.com. A `default` TT policy is page-global (it vets EVERY
12581
+ // TrustedScriptURL/TrustedHTML/TrustedScript sink, including any future
12582
+ // app sink), so adopting it is a deliberate posture change, not a
12583
+ // drive-by — left as an operator follow-up. To reproduce + verify a fix,
12584
+ // pay with the Stripe 3-D Secure test card 4000002760003184 (forces an
12585
+ // authentication challenge, which exercises the dynamic challenge-frame
12586
+ // loader that trips this) and confirm the console violation is gone and
12587
+ // the challenge frame renders.
12556
12588
  res.setHeader && res.setHeader("content-security-policy", securityMiddleware.scopedCsp(["stripe"]));
12589
+ // Route-scoped Permissions-Policy that re-enables the `payment` feature
12590
+ // for this one response (same origin + the Stripe / Google Pay wallet
12591
+ // frames) so the Express Checkout Element's Google Pay / Apple Pay
12592
+ // buttons can use the Payment Request API. The app-level strict denylist
12593
+ // (payment=()) still governs every OTHER route; setHeader overwrites
12594
+ // only this response. Every other feature stays denied.
12595
+ res.setHeader && res.setHeader("permissions-policy", securityMiddleware.scopedPermissionsPolicy());
12557
12596
  // Operator trust badges at the checkout placement (container-only;
12558
12597
  // drop-silent → "" on any failure).
12559
12598
  var payTrustBadges = await _trustBadgesHtml("checkout", req);
package/lib/webhooks.js CHANGED
@@ -34,6 +34,23 @@
34
34
  * `b.uuid.v7()` per delivery — re-deliveries (manual retry) carry a
35
35
  * new id so the consumer's nonce store doesn't reject the replay as
36
36
  * a duplicate.
37
+ *
38
+ * TLS posture — outbound deliveries dial through `b.httpClient`,
39
+ * which offers ONLY the framework's PQC-hybrid key-exchange groups
40
+ * (ML-KEM hybrid, `constants.TLS_GROUP_PREFERENCE`) with a TLS 1.3
41
+ * minimum. A receiver whose TLS stack does not yet negotiate an
42
+ * ML-KEM hybrid group answers the ClientHello with a
43
+ * handshake_failure (TLS alert 40); the framework surfaces that as a
44
+ * transport error, so the delivery row records `last_error`, runs the
45
+ * backoff retry schedule, and — if the endpoint never negotiates a
46
+ * supported group — lands in `webhook_dlq` after the fifth attempt.
47
+ * This is a deliberate posture choice, NOT a silent classical
48
+ * downgrade: the PSP adapters (lib/payment.js) pin a downgraded agent
49
+ * for their two FIXED endpoints, but a webhook receiver URL is
50
+ * operator-arbitrary, so the framework's PQC-first default is held.
51
+ * An operator whose receiver can't meet the PQC list terminates TLS
52
+ * for it on a fronting proxy (Cloudflare, a load balancer) that does,
53
+ * or upgrades the receiver's TLS stack.
37
54
  */
38
55
 
39
56
  var b = require("./vendor/blamejs");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.3.62",
3
+ "version": "0.3.64",
4
4
  "description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {