@tokenoftrust/storefront-runner 3.9.2 → 3.10.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.
@@ -30,6 +30,8 @@ import {
30
30
  takeVerificationRef,
31
31
  withVerificationRef,
32
32
  withShippingPolicy,
33
+ withReturnUrl,
34
+ trustedCartReturnUrl,
33
35
  SHIPPING_UNAVAILABLE_CART_MESSAGE,
34
36
  ShippingPolicyUnavailableError,
35
37
  type CartDenialCode,
@@ -108,6 +110,16 @@ async function boundShippingPolicy(
108
110
  }
109
111
  }
110
112
 
113
+ /**
114
+ * The page the shopper submitted from, when it is served by this request's host or the tenant's
115
+ * declared domain. The browser's Referer is the only source; a foreign or absent one yields none.
116
+ */
117
+ function submittedFromUrl(request: Request, tenant: TenantConfig): string | null {
118
+ const domain = tenant.appDomain?.toLowerCase();
119
+ const hosts = [new URL(request.url).host, ...(domain ? [domain, `www.${domain}`] : [])];
120
+ return trustedCartReturnUrl(request.headers.get("referer"), hosts);
121
+ }
122
+
111
123
  /** Native cart POST. Browser preflight is not consulted. Payment is never authorized. */
112
124
  async function cartFormPost(context: APIContext): Promise<Response> {
113
125
  const { tenant, commerce } = context.locals;
@@ -145,8 +157,11 @@ async function cartFormPost(context: APIContext): Promise<Response> {
145
157
  return cartDenial(result.code, shopperCartDenialMessage(result.code, result.message), status);
146
158
  }
147
159
  const verified = await withVerificationRef(result.fields, { mode: commerce.mode, secret: signing.secret, ref });
148
- const shipped = await boundShippingPolicy(verified, tenant, commerce.mode, signing.secret);
149
- if (!shipped) return cartDenial("shipping", SHIPPING_UNAVAILABLE_CART_MESSAGE, 409);
160
+ const shippable = await boundShippingPolicy(verified, tenant, commerce.mode, signing.secret);
161
+ if (!shippable) return cartDenial("shipping", SHIPPING_UNAVAILABLE_CART_MESSAGE, 409);
162
+ const shipped = await withReturnUrl(shippable, {
163
+ mode: commerce.mode, secret: signing.secret, returnUrl: submittedFromUrl(context.request, tenant),
164
+ });
150
165
  return new Response(null, {
151
166
  status: 303,
152
167
  headers: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "3.9.2",
3
+ "version": "3.10.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",
@@ -204,9 +204,13 @@ export class CartSigningUnavailableError extends Error {
204
204
  * item option: `enforceCartSubmission` and the commerce pre-payment callback
205
205
  * both bind the cart to it. `verification` carries the shopper's verification
206
206
  * ref when the gate produced one (`withVerificationRef`). `shipping_policy` carries the
207
- * store's declared shipping charge (`withShippingPolicy`).
207
+ * store's declared shipping charge (`withShippingPolicy`). `return_url` carries the page the
208
+ * shopper submitted from (`withReturnUrl`).
208
209
  */
209
- export const INTERNAL_CART_FIELDS = ["currency", "tenant", "verification", "shipping_policy"] as const;
210
+ export const INTERNAL_CART_FIELDS = ["currency", "tenant", "verification", "shipping_policy", "return_url"] as const;
211
+
212
+ /** The signed cart option the checkout store's Continue Shopping link reads (`withReturnUrl`). */
213
+ export const RETURN_URL_CART_FIELD = "return_url";
210
214
 
211
215
  /** The signed cart option the checkout store reads the shipping charge from. */
212
216
  export const SHIPPING_POLICY_CART_FIELD = "shipping_policy";
@@ -930,6 +934,51 @@ export async function withShippingPolicy(
930
934
  return [...kept, await signField(opts.secret, code, field)];
931
935
  }
932
936
 
937
+ const MAX_RETURN_URL_LENGTH = 1500;
938
+
939
+ /**
940
+ * The page a cart was submitted from, or `null` when it is not one this store serves. Only an
941
+ * http(s) URL without credentials whose host is in `servedHosts` qualifies, so a form posted from
942
+ * another site, or a crafted cart link, can never make the checkout domain link to a foreign
943
+ * origin. The fragment is dropped. `servedHosts` comes from the request's own host and the
944
+ * tenant's declared domain, never from the client.
945
+ */
946
+ export function trustedCartReturnUrl(
947
+ candidate: string | null | undefined,
948
+ servedHosts: readonly string[],
949
+ ): string | null {
950
+ if (!candidate || candidate.length > MAX_RETURN_URL_LENGTH) return null;
951
+ if (!URL.canParse(candidate)) return null;
952
+ const url = new URL(candidate);
953
+ if (url.protocol !== "https:" && url.protocol !== "http:") return null;
954
+ if (url.username || url.password) return null;
955
+ const served = servedHosts.map((host) => host.trim().toLowerCase()).filter(Boolean);
956
+ if (!served.includes(url.host.toLowerCase())) return null;
957
+ url.hash = "";
958
+ const href = url.toString();
959
+ return href.length <= MAX_RETURN_URL_LENGTH ? href : null;
960
+ }
961
+
962
+ /**
963
+ * Bind the originating page into an enforced cart as the internal `return_url` option, signed
964
+ * against the cart's product code. Any `return_url` the shopper submitted is dropped first, so only
965
+ * the server-derived, host-checked value reaches the cart. No trusted URL leaves the cart without
966
+ * one, and the checkout falls back to its own Continue Shopping target.
967
+ */
968
+ export async function withReturnUrl(
969
+ fields: readonly CartSubmissionField[],
970
+ opts: { mode: ManagedCheckoutConfig["mode"]; secret?: string; returnUrl: string | null },
971
+ ): Promise<CartSubmissionField[]> {
972
+ const kept = fields.filter((field) => field.name.split("||")[0] !== RETURN_URL_CART_FIELD);
973
+ if (!opts.returnUrl) return kept;
974
+ const code = kept.find((field) => field.name.split("||")[0] === "code")?.value;
975
+ if (!code) return kept;
976
+ const field: CartField = { name: RETURN_URL_CART_FIELD, value: opts.returnUrl };
977
+ if (opts.mode !== "signed") return [...kept, field];
978
+ if (!opts.secret) throw new CartSigningUnavailableError();
979
+ return [...kept, await signField(opts.secret, code, field)];
980
+ }
981
+
933
982
  /** Cart URL the browser may navigate to after enforcement. Not a payment. */
934
983
  export function foxyCartRedirectUrl(
935
984
  cartUrl: string,