@tokenoftrust/storefront-runner 4.0.0 → 4.1.0

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.
@@ -32,6 +32,7 @@ import {
32
32
  withShippingPolicy,
33
33
  withReturnUrl,
34
34
  trustedCartReturnUrl,
35
+ CHECKOUT_NAVIGATION_REFERRER_POLICY,
35
36
  SHIPPING_UNAVAILABLE_CART_MESSAGE,
36
37
  ShippingPolicyUnavailableError,
37
38
  type CartDenialCode,
@@ -183,6 +184,7 @@ async function cartFormPost(context: APIContext): Promise<Response> {
183
184
  headers: {
184
185
  location,
185
186
  "cache-control": "no-store",
187
+ "referrer-policy": CHECKOUT_NAVIGATION_REFERRER_POLICY,
186
188
  "x-tot-payment-authorized": "false",
187
189
  },
188
190
  });
@@ -233,7 +235,7 @@ function sampleResponse(body: string | null, status: number, location?: string):
233
235
  return new Response(body, { status, headers: {
234
236
  "content-type": "application/json; charset=utf-8",
235
237
  "cache-control": "private, no-store",
236
- "referrer-policy": "no-referrer",
238
+ "referrer-policy": location ? CHECKOUT_NAVIGATION_REFERRER_POLICY : "no-referrer",
237
239
  "x-robots-tag": "noindex, nofollow",
238
240
  "x-tot-payment-authorized": "false",
239
241
  ...(location ? { location } : {}),
@@ -38,6 +38,7 @@
38
38
  * published (CloudFront/S3) output — both render Layout.astro. The session id
39
39
  * is not part of that HTML.
40
40
  */
41
+ import { CHECKOUT_NAVIGATION_REFERRER_POLICY } from "@tot/public-runtime";
41
42
  import { CART_UNAVAILABLE_MESSAGE } from "@/lib/checkout/commerce/cartSession";
42
43
 
43
44
  export const LAZY_CHECKOUT_LOADER_IDLE_MS = 8000;
@@ -77,6 +78,7 @@ export function lazyCheckoutLoaderJs(options: LazyCheckoutLoaderOptions): string
77
78
  var cartOrigin = ${literal(options.cartOrigin)};
78
79
  var sessionHref = ${literal(options.sessionHref)};
79
80
  var cookiePath = ${literal(options.cookiePath)};
81
+ var referrerPolicy = ${literal(CHECKOUT_NAVIGATION_REFERRER_POLICY)};
80
82
  var storeNotPublic = ${options.storeNotPublic ? "true" : "false"};
81
83
  var events = ${literal(LAZY_CHECKOUT_LOADER_EVENTS)};
82
84
  var state = 'idle';
@@ -87,6 +89,44 @@ export function lazyCheckoutLoaderJs(options: LazyCheckoutLoaderOptions): string
87
89
  if (!url) return false;
88
90
  try { return new URL(url, document.baseURI).origin === cartOrigin; } catch (e) { return false; }
89
91
  }
92
+ // Foxy copies the Referer onto cart_cancel_and_continue_link and a later
93
+ // origin-only Referer overwrites it. The document policy stays strict-origin.
94
+ // Checkout requests opt into the full store-home URL, then the address is put back
95
+ // when the request snapshots it synchronously (a cart script). A navigation
96
+ // snapshots after this turn, so that path leaves the store-home URL in place.
97
+ function storeHome() {
98
+ return location.origin + cookiePath;
99
+ }
100
+ function revealStoreHome() {
101
+ var home = storeHome();
102
+ if (location.href === home) return;
103
+ window.history.replaceState(window.history.state, "", home);
104
+ }
105
+ function restoreHref(current) {
106
+ if (location.href !== current) window.history.replaceState(window.history.state, "", current);
107
+ }
108
+ var origCreate = document.createElement;
109
+ document.createElement = function (name) {
110
+ var el = origCreate.apply(document, arguments);
111
+ if (String(name).toLowerCase() !== "script") return el;
112
+ var proto = window.HTMLScriptElement && window.HTMLScriptElement.prototype;
113
+ var srcDesc = proto && Object.getOwnPropertyDescriptor(proto, "src");
114
+ if (!srcDesc || !srcDesc.get || !srcDesc.set) return el;
115
+ Object.defineProperty(el, "src", {
116
+ configurable: true,
117
+ enumerable: true,
118
+ get: function () { return srcDesc.get.call(this); },
119
+ set: function (value) {
120
+ if (!targetsCart(value)) return srcDesc.set.call(this, value);
121
+ this.referrerPolicy = referrerPolicy;
122
+ var current = location.href;
123
+ revealStoreHome();
124
+ try { return srcDesc.set.call(this, value); }
125
+ finally { restoreHref(current); }
126
+ }
127
+ });
128
+ return el;
129
+ };
90
130
  function commerceReachable() {
91
131
  var nodes = document.querySelectorAll(${managedCart} + ', a[href], form[action]');
92
132
  for (var i = 0; i < nodes.length; i++) {
@@ -209,8 +249,10 @@ export function lazyCheckoutLoaderJs(options: LazyCheckoutLoaderOptions): string
209
249
  if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
210
250
  var anchor = event.target && event.target.closest ? event.target.closest('a[href]') : null;
211
251
  if (!anchor || anchor.target === '_blank' || !targetsCart(anchor.href)) return;
252
+ anchor.referrerPolicy = referrerPolicy;
212
253
  if (state === 'ready') {
213
254
  stampAnchor(anchor);
255
+ if (!event.defaultPrevented) revealStoreHome();
214
256
  return;
215
257
  }
216
258
  if (event.defaultPrevented) return;
@@ -268,7 +310,13 @@ export function lazyCheckoutLoaderJs(options: LazyCheckoutLoaderOptions): string
268
310
  var cartForm = targetsCart(form.getAttribute('action'));
269
311
  if (!managed && !cartForm) return;
270
312
  if (form.dataset.cartSession === '1') {
271
- if (!cartForm || state === 'ready') return;
313
+ if (!cartForm || state === 'ready') {
314
+ if (!event.defaultPrevented && state === 'ready') {
315
+ form.referrerPolicy = referrerPolicy;
316
+ revealStoreHome();
317
+ }
318
+ return;
319
+ }
272
320
  event.preventDefault();
273
321
  whenReady(function () { stampForm(form); resubmit(form); });
274
322
  return;
@@ -40,14 +40,28 @@ function detailsPair(block: string): FaqItem | null {
40
40
  return { q, a: textOf(rest) };
41
41
  }
42
42
 
43
- /** FAQ pairs from sanitized body HTML: question-phrased h2–h4, plus every details/summary pair. */
43
+ /** A heading inside details is that pair's question, not a second heading pair. */
44
+ function insideDetails(index: number, ranges: Array<{ start: number; end: number }>): boolean {
45
+ return ranges.some((range) => index >= range.start && index < range.end);
46
+ }
47
+
48
+ /** FAQ pairs from sanitized body HTML: question-phrased h2–h4 outside details, plus every details/summary pair. */
44
49
  export function faqFromHtml(html: string | undefined): FaqItem[] {
45
50
  if (!html) return [];
51
+ const ranges = [...html.matchAll(/<details\b[^>]*>[\s\S]*?<\/details>/gi)].map((match) => ({
52
+ start: match.index ?? 0,
53
+ end: (match.index ?? 0) + match[0].length,
54
+ }));
46
55
  const parts = html.split(/(<h[2-4][^>]*>[\s\S]*?<\/h[2-4]>)/i);
47
56
  const items: FaqItem[] = [];
48
- for (let i = 1; i < parts.length; i += 2) {
49
- const q = textOf(parts[i]!);
50
- if (isQuestion(q)) items.push({ q, a: textOf(parts[i + 1] ?? "") });
57
+ let offset = 0;
58
+ for (let i = 0; i < parts.length; i++) {
59
+ const part = parts[i] ?? "";
60
+ if (i % 2 === 1) {
61
+ const q = textOf(part);
62
+ if (isQuestion(q) && !insideDetails(offset, ranges)) items.push({ q, a: textOf(parts[i + 1] ?? "") });
63
+ }
64
+ offset += part.length;
51
65
  }
52
66
  for (const block of html.matchAll(/<details\b[^>]*>[\s\S]*?<\/details>/gi)) {
53
67
  const pair = detailsPair(block[0]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -212,6 +212,22 @@ export const INTERNAL_CART_FIELDS = ["currency", "tenant", "verification", "ship
212
212
  /** The signed cart option the checkout store's Continue Shopping link reads (`withReturnUrl`). */
213
213
  export const RETURN_URL_CART_FIELD = "return_url";
214
214
 
215
+ /**
216
+ * Referrer policy for a request the browser makes to the managed checkout (a cart
217
+ * script, a cart navigation, or the hop that follows a 303 onto the cart).
218
+ *
219
+ * Measured 2026-10-24 against tot-preview: Foxy sets `cart_cancel_and_continue_link`
220
+ * from the Referer and ignores a `continue` parameter. An origin-only Referer
221
+ * overwrites a fuller one; the first request with no Referer falls back to the
222
+ * store's configured store URL (the host root). `strict-origin-when-cross-origin`
223
+ * sends only that origin on a cross-origin checkout request, so empty-cart and
224
+ * receipt links drop the tenant path. This value sends the full URL on https and
225
+ * nothing on a downgrade. It is not the document policy: a redirect response's
226
+ * Referrer-Policy replaces the element's, and `no-referrer` on that response
227
+ * clears the path. The site-wide header stays strict-origin.
228
+ */
229
+ export const CHECKOUT_NAVIGATION_REFERRER_POLICY = "no-referrer-when-downgrade";
230
+
215
231
  /** The signed cart option the checkout store reads the shipping charge from. */
216
232
  export const SHIPPING_POLICY_CART_FIELD = "shipping_policy";
217
233