@blamejs/blamejs-shop 0.3.63 → 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 +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/security-middleware.js +54 -0
- package/lib/storefront.js +39 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ 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
|
+
|
|
11
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.
|
|
12
14
|
|
|
13
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.
|
package/lib/asset-manifest.json
CHANGED
|
@@ -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/package.json
CHANGED