@blamejs/blamejs-shop 0.4.6 → 0.4.7

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,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.4.x
10
10
 
11
+ - v0.4.7 (2026-06-05) — **Payment page: Trusted Types now permits Stripe's loader through a real allowlist, and the Amazon Pay button loads.** Two payment-page fixes. The site enforces Trusted Types, and Stripe's browser library assigns script URLs without registering a policy — the console logged a TrustedScriptURL violation on every payment page, the failure class that can break a 3-D Secure challenge mid-payment. Rather than relaxing enforcement, the page now registers the default Trusted Types policy before Stripe loads: it admits script URLs from Stripe's own origins and throws for anything else, so enforcement stays on with a genuine gate. Second, the express-wallet row offered an Amazon Pay button whose frame the payment page's policy refused to load; the page's frame allowance now includes the Stripe-operated CDN that serves wallet buttons, scoped to the payment page only. Every other page keeps the strict policy unchanged. **Fixed:** *Trusted Types policy for the Stripe loader* — The payment page registers the default Trusted Types policy before the Stripe script tag, synchronously, integrity-pinned like every other script. The policy admits script URLs whose origin is js.stripe.com or a subdomain of it and throws for any other host — lookalike domains, plain http, and non-Stripe CDNs are refused. Browsers without Trusted Types are unaffected. Pages outside the payment route keep require-trusted-types-for enforcement with no registered loader policy, exactly as before. · *Express wallet buttons can load their frames* — The payment page's frame allowance adds Stripe's wallet-button CDN (b.stripecdn.com) and Stripe's dynamic subdomains (*.js.stripe.com) alongside the existing js.stripe.com and hooks.stripe.com entries, restoring the Amazon Pay express button. The allowance is scoped to the payment page; the storefront-wide policy still permits no third-party frames at all.
12
+
11
13
  - v0.4.6 (2026-06-05) — **Guest order pages require proof of purchase, and guests can save their order to an account in one tap.** Two changes to what happens after a guest checks out. First, the order confirmation page — which shows the buyer's name, address, and items — is no longer reachable by anyone who learns the order id. A guest order now admits exactly three readers: the browser that placed it (a sealed device cookie set at checkout, surviving the payment redirect), anyone opening the signed link in the order-receipt email (an order-scoped, expiring signed token that works on any device), and the signed-in account that owns it. Everything else sees a 404 indistinguishable from a missing order, and the reorder action is gated the same way. Second, the confirmation page now offers a guest one tap to save the order to an account: it emails a sign-in link to the address they checked out with — shown masked on screen, held only in a short-lived encrypted cookie, never written to the database — and redeeming the link creates the account if needed and attaches their guest orders by a hashed-email match. The response is identical whether or not an account already exists, the trigger is rate-limited, and a second click sends nothing. **Added:** *One-tap account setup after checkout* — A guest buyer's confirmation page offers to save the order to an account: one click emails a magic sign-in link to the checkout address (masked on screen, e.g. r***@e***.com), creating the account on redemption if none exists and linking the buyer's guest orders by hashed-email match at the moment they prove control of the inbox. Signed-in buyers never see the offer; buyers whose address already has an account get sign-in wording with the identical send path. The plaintext address lives only in an encrypted cookie for fifteen minutes — the database keeps only the one-way hash it already kept. **Changed:** *Guest order confirmation pages are access-gated* — A guest order's page requires the placing browser's sealed device cookie (set at checkout, capped to recent orders, 30-day refresh), the signed token in the order-receipt email's view-order link (order-scoped, roughly 90-day expiry, constant-time verified), or the signed-in owner. Unauthorized requests — including a signed-in customer who does not own the order — receive a 404 identical to a missing order. The reorder action carries the same gate; cancel, rating, and return actions were already owner-only. The payment-provider return flow and order analytics are unaffected: the device cookie is set before the redirect to the payment page, so the buyer lands back on their confirmation with access intact.
12
14
 
13
15
  - v0.4.5 (2026-06-05) — **One sign-in screen: passkey first, email sign-in link as the built-in fallback.** The sign-in page previously offered the passkey ceremony with the email magic-link alternative a page away behind a link. Both now live on one screen: passkey stays the primary action, and an "Email me a sign-in link" form sits inline beneath it — a plain server-rendered form that works with JavaScript disabled. The page meets failure gracefully: a browser without WebAuthn support disables the passkey button and points to the email form, and a failed or cancelled passkey ceremony scrolls the shopper to the fallback with their typed email carried over, one tap from a sign-in link instead of a dead end. Responses are identical whether or not an account exists for the address, the trigger is rate-limited, and a visitor who is already signed in is redirected to their account instead of seeing a login form. **Changed:** *Sign-in page unifies passkey and email-link paths* — Passkey remains the primary action; the email sign-in link form renders inline on the same screen when transactional email is configured, works without JavaScript, and inherits the existing magic-link token semantics unchanged (single-use, expiring). Browsers without WebAuthn support are steered to the email form up front, and any passkey ceremony failure lands the shopper on the fallback with their email pre-filled. The email form's responses do not reveal whether an address has an account, the trigger endpoint is tightly rate-limited, and signed-in visitors are redirected away from the login form.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.6",
2
+ "version": "0.4.7",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-6k53cvkRrxMgmeStLIoLjVXZQHqIJgTmv1Izd8TYhh1HOC4POgE6GCvx1bsalyEP",
@@ -37,6 +37,10 @@
37
37
  "integrity": "sha384-BjuUhbPZ18pHFMyOwT+309BEXu+VAc54RSj8lvN93jfFGDydEgmapiAOKTQM1yma",
38
38
  "fingerprinted": "js/passkey-register.02b0e196fb9608d8.js"
39
39
  },
40
+ "js/pay-trusted-types.js": {
41
+ "integrity": "sha384-jD90N8l7J3Vcu+aPSFRi6AE/5XKHQl58/avwHE9PH0mHwMavX991OCsEcznEf3n5",
42
+ "fingerprinted": "js/pay-trusted-types.53d0df13f9480a66.js"
43
+ },
40
44
  "js/pay.js": {
41
45
  "integrity": "sha384-W11JVQhv1RZq4WhsAOglu56gTZTDz1ByLd+b1HFQBCi8xGoEhJ0PZ2yI3FqbYzt6",
42
46
  "fingerprinted": "js/pay.683a905563e54a47.js"
@@ -265,9 +265,27 @@ function securityHeadersOpts() {
265
265
  // what the provider's own CDN requires.
266
266
  var CSP_HOSTS = {
267
267
  stripe: {
268
- script: ["https://js.stripe.com"],
268
+ // Stripe.js v3 starts sub-frames on per-origin `*.js.stripe.com` hosts
269
+ // for performance (Stripe security guide), and its dynamic loader
270
+ // injects those sub-resource <script>s at runtime — so BOTH the apex
271
+ // and the wildcard must be admitted on script-src, or the
272
+ // TrustedScriptURL/dynamic-load path is refused (the 3DS challenge
273
+ // loader rides this).
274
+ script: ["https://js.stripe.com", "https://*.js.stripe.com"],
269
275
  connect: ["https://api.stripe.com"],
270
- frame: ["https://js.stripe.com", "https://hooks.stripe.com"],
276
+ // frame-src: the apex + `*.js.stripe.com` carry the Payment Element /
277
+ // 3DS challenge frames; hooks.stripe.com carries redirect-style
278
+ // confirmations. `b.stripecdn.com` serves the Express Checkout wallet
279
+ // button assets (e.g. the Amazon Pay button iframe at
280
+ // b.stripecdn.com/stripethirdparty-srv/...) — without it the express
281
+ // wallet button iframe is blocked (net::ERR_ABORTED) while Link /
282
+ // Google Pay, which render from *.js.stripe.com, load fine.
283
+ frame: [
284
+ "https://js.stripe.com",
285
+ "https://*.js.stripe.com",
286
+ "https://hooks.stripe.com",
287
+ "https://b.stripecdn.com",
288
+ ],
271
289
  },
272
290
  paypal: {
273
291
  script: ["https://www.paypal.com", "https://www.paypalobjects.com"],
package/lib/storefront.js CHANGED
@@ -452,8 +452,14 @@ function _islandScript(name, opts) {
452
452
  // construction at the call site, so they go in without escaping.
453
453
  var idAttr = (opts && opts.id) ? " id=\"" + opts.id + "\"" : "";
454
454
  var policyAttr = (opts && opts.policy) ? " data-consent-policy=\"" + opts.policy + "\"" : "";
455
+ // Islands `defer` by default (run after parse, in document order). The
456
+ // Trusted Types `default` policy is the exception: it MUST register
457
+ // synchronously BEFORE the parser-inserted Stripe SDK <script> executes,
458
+ // so `opts.sync` drops `defer` (and adds no `async`) — a classic blocking
459
+ // <script> that runs the instant the parser reaches it.
460
+ var timing = (opts && opts.sync) ? "" : " defer";
455
461
  return "<script" + idAttr + " src=\"" + _assetUrl("js/" + name) + "\"" +
456
- (sri ? " integrity=\"" + sri + "\"" : "") + " defer" + policyAttr + "></script>";
462
+ (sri ? " integrity=\"" + sri + "\"" : "") + timing + policyAttr + "></script>";
457
463
  }
458
464
 
459
465
  // ---- announcement bar --------------------------------------------------
@@ -7170,6 +7176,11 @@ var PAY_PAGE =
7170
7176
  " <button id=\"submit\" type=\"button\" class=\"btn-primary pay-card__submit\">Pay {{grand_total}}</button>\n" +
7171
7177
  " <p id=\"payment-message\" class=\"pay-card__message\"></p>\n" +
7172
7178
  " </div>\n" +
7179
+ // Trusted Types `default` policy — MUST be registered before the Stripe
7180
+ // SDK loads (emitted sync/blocking, no defer) so Stripe.js's dynamic
7181
+ // script-URL injections (perf sub-frames + the 3DS challenge loader) pass
7182
+ // the createScriptURL gate instead of being refused as un-typed.
7183
+ " RAW_PAY_TT_SCRIPT\n" +
7173
7184
  " <script src=\"https://js.stripe.com/v3/\"></script>\n" +
7174
7185
  " RAW_PAY_SCRIPT\n" +
7175
7186
  "</section>\n";
@@ -7195,6 +7206,10 @@ function renderPayPage(opts) {
7195
7206
  pk: opts.publishable_key,
7196
7207
  client_secret: opts.client_secret,
7197
7208
  pay_script: opts.theme.assetUrl("js/pay.js"),
7209
+ // Trusted Types `default` policy asset — rendered (sync, no defer)
7210
+ // BEFORE the Stripe SDK tag in pay.html so it registers the
7211
+ // createScriptURL gate ahead of Stripe.js's first dynamic load.
7212
+ pay_tt_script: opts.theme.assetUrl("js/pay-trusted-types.js"),
7198
7213
  asset_css_main: opts.theme.assetUrl("css/main.css"),
7199
7214
  });
7200
7215
  }
@@ -7206,7 +7221,9 @@ function renderPayPage(opts) {
7206
7221
  grand_total: grandTotal,
7207
7222
  pk: opts.publishable_key,
7208
7223
  client_secret: opts.client_secret,
7209
- }).replace("RAW_PAY_SCRIPT", _islandScript("pay.js"));
7224
+ })
7225
+ .replace("RAW_PAY_TT_SCRIPT", _islandScript("pay-trusted-types.js", { sync: true }))
7226
+ .replace("RAW_PAY_SCRIPT", _islandScript("pay.js"));
7210
7227
  // Operator trust badges at the checkout placement (container-only — the pay
7211
7228
  // page isn't edge-cached). Pre-resolved + sanitized by the route; appended
7212
7229
  // after the pay card. Empty string when no badges / no dep.
@@ -13661,43 +13678,40 @@ function mount(router, deps) {
13661
13678
  res.status(503);
13662
13679
  return res.end ? res.end("Stripe publishable key not configured") : res.send("Stripe publishable key not configured");
13663
13680
  }
13664
- // Route-scoped CSP that admits js.stripe.com on script/connect/frame-src
13665
- // (and Trusted Types stays on) so the Stripe SDK + the same-origin
13681
+ // Route-scoped CSP that admits Stripe's hosts on script/connect/frame-src
13682
+ // (and Trusted Types stays ON) so the Stripe SDK + the same-origin
13666
13683
  // pay.js island load — without relaxing the app-level strict CSP that
13667
13684
  // governs every OTHER route. setHeader OVERWRITES the app-level header
13668
13685
  // for this response only.
13669
13686
  //
13670
- // KNOWN, NON-BLOCKING Trusted Types violation on this route (operator
13671
- // follow-up, intentionally NOT fixed here). The app-level CSP carried
13672
- // through verbatim by scopedCsp keeps `require-trusted-types-for
13673
- // 'script'` + `trusted-types 'allow-duplicates' default`. Stripe.js v3
13674
- // does NOT register a named Trusted Types policy of its own; instead it
13675
- // expects the APPLICATION to define a `default` policy whose
13676
- // createScriptURL vets Stripe's own hosts (per Stripe's integration
13677
- // security guide). When Stripe.js dynamically injects its sub-resource
13678
- // <script> (the frame-spawning performance path on *.js.stripe.com), the
13679
- // browser refuses the TrustedScriptURL assignment because no `default`
13680
- // policy is registeredthe console logs "This document requires
13681
- // 'TrustedScriptURL' assignment ... @ https://js.stripe.com/v3/". The
13682
- // card form is unaffected: it loads from the STATIC <script
13683
- // src="https://js.stripe.com/v3/"> tag, which is a direct HTML src (not
13684
- // a JS-driven sink), so require-trusted-types-for does not gate it —
13685
- // which is why card captures complete with the violation present. The
13686
- // blocked path is Stripe's dynamic sub-frame/3DS loader.
13687
+ // TRUSTED TYPES, RESOLVED. The app-level CSP carried through verbatim by
13688
+ // scopedCsp keeps `require-trusted-types-for 'script'` +
13689
+ // `trusted-types 'allow-duplicates' default`. Stripe.js v3 registers no
13690
+ // Trusted Types policy of its own; it expects the APPLICATION to define
13691
+ // a `default` policy whose createScriptURL vets Stripe's hosts (Stripe
13692
+ // security guide). The pay page now ships that policy in
13693
+ // `pay-trusted-types.js`, emitted (sync, no defer) BEFORE the Stripe SDK
13694
+ // tag, so when Stripe.js dynamically injects its sub-resource <script>
13695
+ // (the perf sub-frames on *.js.stripe.com and the 3-D Secure
13696
+ // challenge-frame loader) the browser routes the URL through the policy
13697
+ // and admits itinstead of refusing it with "This document requires
13698
+ // 'TrustedScriptURL' assignment". The policy throws for any non-Stripe
13699
+ // origin, so Trusted Types stays a real gate, not a relaxation. The
13700
+ // scoped CSP's script-src/frame-src admit both `js.stripe.com` and
13701
+ // `*.js.stripe.com` (the sub-origins the dynamic loader uses).
13702
+ //
13703
+ // EXPRESS WALLET FRAMES. The scoped frame-src also admits
13704
+ // `b.stripecdn.com`, where the Express Checkout Element loads its wallet
13705
+ // button iframes (e.g. the Amazon Pay button at
13706
+ // b.stripecdn.com/stripethirdparty-srv/...); without it that iframe is
13707
+ // blocked (net::ERR_ABORTED) while Link / Google Pay, served from
13708
+ // *.js.stripe.com, render fine.
13687
13709
  //
13688
- // We do NOT loosen Trusted Types to silence it. Naming a Stripe policy
13689
- // in the trusted-types directive does nothing (Stripe registers none);
13690
- // the only fix Stripe documents is the app shipping a same-origin
13691
- // `default` createScriptURL policy that allows js.stripe.com /
13692
- // *.js.stripe.com AND widening this scoped CSP's script-src/frame-src to
13693
- // *.js.stripe.com. A `default` TT policy is page-global (it vets EVERY
13694
- // TrustedScriptURL/TrustedHTML/TrustedScript sink, including any future
13695
- // app sink), so adopting it is a deliberate posture change, not a
13696
- // drive-by — left as an operator follow-up. To reproduce + verify a fix,
13697
- // pay with the Stripe 3-D Secure test card 4000002760003184 (forces an
13698
- // authentication challenge, which exercises the dynamic challenge-frame
13699
- // loader that trips this) and confirm the console violation is gone and
13700
- // the challenge frame renders.
13710
+ // A live 3-D Secure path still warrants a production verification: pay
13711
+ // with the Stripe 3DS test card 4000002760003184 (forces an
13712
+ // authentication challenge that exercises the dynamic challenge-frame
13713
+ // loader) and confirm the challenge frame renders with no TrustedScriptURL
13714
+ // console violation.
13701
13715
  res.setHeader && res.setHeader("content-security-policy", securityMiddleware.scopedCsp(["stripe"]));
13702
13716
  // Route-scoped Permissions-Policy that re-enables the `payment` feature
13703
13717
  // for this one response (same origin + the Stripe / Google Pay wallet
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
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": {