@tokenoftrust/storefront-runner 2.4.48 → 2.4.50

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.
@@ -0,0 +1,166 @@
1
+ -- Commerce orders, inbox/outbox, allocations, label purchases, and shipment audit.
2
+ -- Tenant and stage are on every row. Money is integer minor units.
3
+ -- The SQL directory is the apply set. One transaction per file.
4
+ CREATE TABLE `scrappy_commerce_orders` (
5
+ `tenant_id` text NOT NULL,
6
+ `stage` text NOT NULL CHECK (stage IN ('preview', 'publication')),
7
+ `order_id` text NOT NULL,
8
+ `source` text NOT NULL,
9
+ `source_order_id` text NOT NULL,
10
+ `currency` text NOT NULL,
11
+ `placed_at` text NOT NULL,
12
+ `payment_state` text NOT NULL CHECK (payment_state IN ('UNKNOWN', 'pending', 'authorized', 'captured', 'capture_unknown', 'voided', 'refund_requested', 'refunded', 'refund_unknown')),
13
+ `compliance_state` text NOT NULL CHECK (compliance_state IN ('UNKNOWN', 'pending', 'fresh', 'stale', 'unreadable', 'refused')),
14
+ `stock_state` text NOT NULL CHECK (stock_state IN ('UNKNOWN', 'unallocated', 'reserved', 'confirmed', 'released', 'expired')),
15
+ `fulfillment_state` text NOT NULL CHECK (fulfillment_state IN ('UNKNOWN', 'held', 'ready', 'handed_off', 'cancelled')),
16
+ `label_state` text NOT NULL CHECK (label_state IN ('UNKNOWN', 'none', 'intent', 'purchased', 'printed', 'void_requested', 'voided')),
17
+ `amount_minor` integer NOT NULL CHECK (amount_minor >= 0),
18
+ `event_version` integer NOT NULL CHECK (event_version >= 1),
19
+ PRIMARY KEY (`tenant_id`, `stage`, `order_id`)
20
+ );
21
+ --> statement-breakpoint
22
+ CREATE UNIQUE INDEX `uq_scrappy_commerce_source_order`
23
+ ON `scrappy_commerce_orders` (`tenant_id`, `stage`, `source`, `source_order_id`);
24
+ --> statement-breakpoint
25
+ CREATE TABLE `scrappy_order_addresses` (
26
+ `tenant_id` text NOT NULL,
27
+ `stage` text NOT NULL CHECK (stage IN ('preview', 'publication')),
28
+ `order_id` text NOT NULL,
29
+ `address_json` text NOT NULL,
30
+ PRIMARY KEY (`tenant_id`, `stage`, `order_id`),
31
+ FOREIGN KEY (`tenant_id`, `stage`, `order_id`) REFERENCES `scrappy_commerce_orders` (`tenant_id`, `stage`, `order_id`)
32
+ );
33
+ --> statement-breakpoint
34
+ CREATE TABLE `scrappy_commerce_inbox` (
35
+ `tenant_id` text NOT NULL,
36
+ `stage` text NOT NULL CHECK (stage IN ('preview', 'publication')),
37
+ `source` text NOT NULL,
38
+ `source_event_id` text NOT NULL,
39
+ `event_version` integer NOT NULL CHECK (event_version >= 1),
40
+ `idempotency_key` text NOT NULL,
41
+ `payload_digest` text NOT NULL,
42
+ `received_at` text NOT NULL,
43
+ PRIMARY KEY (`tenant_id`, `stage`, `source`, `source_event_id`)
44
+ );
45
+ --> statement-breakpoint
46
+ CREATE UNIQUE INDEX `uq_scrappy_inbox_idempotency`
47
+ ON `scrappy_commerce_inbox` (`tenant_id`, `stage`, `idempotency_key`);
48
+ --> statement-breakpoint
49
+ CREATE TABLE `scrappy_commerce_outbox` (
50
+ `outbox_id` text NOT NULL,
51
+ `tenant_id` text NOT NULL,
52
+ `stage` text NOT NULL CHECK (stage IN ('preview', 'publication')),
53
+ `order_id` text NOT NULL,
54
+ `source` text NOT NULL,
55
+ `source_event_id` text NOT NULL,
56
+ `event_version` integer NOT NULL CHECK (event_version >= 1),
57
+ `event_type` text NOT NULL,
58
+ `payload` text NOT NULL,
59
+ `available_at` text NOT NULL,
60
+ `attempt_count` integer NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
61
+ `state` text NOT NULL CHECK (state IN ('pending', 'delivered', 'dead')),
62
+ PRIMARY KEY (`outbox_id`),
63
+ UNIQUE (`tenant_id`, `stage`, `source`, `source_event_id`)
64
+ );
65
+ --> statement-breakpoint
66
+ CREATE INDEX `idx_scrappy_outbox_due`
67
+ ON `scrappy_commerce_outbox` (`state`, `available_at`);
68
+ --> statement-breakpoint
69
+ CREATE TABLE `scrappy_allocations` (
70
+ `tenant_id` text NOT NULL,
71
+ `stage` text NOT NULL CHECK (stage IN ('preview', 'publication')),
72
+ `order_id` text NOT NULL,
73
+ `sku` text NOT NULL,
74
+ `location_id` text NOT NULL,
75
+ `quantity` integer NOT NULL CHECK (quantity > 0),
76
+ `state` text NOT NULL CHECK (state IN ('reserved', 'confirmed', 'released', 'expired')),
77
+ PRIMARY KEY (`tenant_id`, `stage`, `order_id`, `sku`, `location_id`),
78
+ FOREIGN KEY (`tenant_id`, `stage`, `order_id`) REFERENCES `scrappy_commerce_orders` (`tenant_id`, `stage`, `order_id`)
79
+ );
80
+ --> statement-breakpoint
81
+ CREATE TABLE `scrappy_final_cart_authorizations` (
82
+ `tenant_id` text NOT NULL,
83
+ `stage` text NOT NULL CHECK (stage IN ('preview', 'publication')),
84
+ `authorization_id` text NOT NULL,
85
+ `cart_digest` text NOT NULL,
86
+ `expires_at` text NOT NULL,
87
+ `state` text NOT NULL CHECK (state IN ('active', 'consumed', 'expired', 'denied')),
88
+ PRIMARY KEY (`tenant_id`, `stage`, `authorization_id`)
89
+ );
90
+ --> statement-breakpoint
91
+ CREATE UNIQUE INDEX `uq_scrappy_active_final_cart`
92
+ ON `scrappy_final_cart_authorizations` (`tenant_id`, `stage`, `cart_digest`)
93
+ WHERE `state` = 'active';
94
+ --> statement-breakpoint
95
+ CREATE TABLE `scrappy_provider_operations` (
96
+ `tenant_id` text NOT NULL,
97
+ `stage` text NOT NULL CHECK (stage IN ('preview', 'publication')),
98
+ `provider` text NOT NULL,
99
+ `purchase_id` text NOT NULL,
100
+ `order_id` text NOT NULL,
101
+ `kind` text NOT NULL CHECK (kind IN ('label_purchase', 'payment_capture', 'refund')),
102
+ `outcome` text NOT NULL CHECK (outcome IN ('intent', 'accepted', 'unknown', 'voided', 'refunded')),
103
+ `amount_minor` integer NOT NULL CHECK (amount_minor >= 0),
104
+ PRIMARY KEY (`tenant_id`, `stage`, `provider`, `purchase_id`),
105
+ FOREIGN KEY (`tenant_id`, `stage`, `order_id`) REFERENCES `scrappy_commerce_orders` (`tenant_id`, `stage`, `order_id`)
106
+ );
107
+ --> statement-breakpoint
108
+ CREATE UNIQUE INDEX `uq_scrappy_open_label_purchase`
109
+ ON `scrappy_provider_operations` (`tenant_id`, `stage`, `order_id`)
110
+ WHERE `kind` = 'label_purchase' AND `outcome` IN ('intent', 'accepted', 'unknown');
111
+ --> statement-breakpoint
112
+ CREATE TABLE `scrappy_shipments` (
113
+ `tenant_id` text NOT NULL,
114
+ `stage` text NOT NULL CHECK (stage IN ('preview', 'publication')),
115
+ `order_id` text NOT NULL,
116
+ `scope` text NOT NULL CHECK (scope = 'whole-order'),
117
+ `label_purchase_state` text NOT NULL CHECK (label_purchase_state IN ('none', 'intent', 'purchased', 'void_requested', 'voided', 'unknown')),
118
+ `label_print_state` text NOT NULL CHECK (label_print_state IN ('none', 'printed')),
119
+ `handoff_state` text NOT NULL CHECK (handoff_state IN ('none', 'handed_off', 'unknown')),
120
+ CHECK (handoff_state = 'none' OR label_purchase_state = 'purchased'),
121
+ CHECK (label_print_state = 'none' OR label_purchase_state = 'purchased'),
122
+ PRIMARY KEY (`tenant_id`, `stage`, `order_id`),
123
+ FOREIGN KEY (`tenant_id`, `stage`, `order_id`) REFERENCES `scrappy_commerce_orders` (`tenant_id`, `stage`, `order_id`)
124
+ );
125
+ --> statement-breakpoint
126
+ CREATE TABLE `scrappy_shipment_audit` (
127
+ `audit_id` text NOT NULL,
128
+ `tenant_id` text NOT NULL,
129
+ `stage` text NOT NULL CHECK (stage IN ('preview', 'publication')),
130
+ `order_id` text NOT NULL,
131
+ `at` text NOT NULL,
132
+ `actor_id` text NOT NULL,
133
+ `kind` text NOT NULL,
134
+ `from_state` text NOT NULL,
135
+ `to_state` text NOT NULL,
136
+ PRIMARY KEY (`audit_id`),
137
+ FOREIGN KEY (`tenant_id`, `stage`, `order_id`) REFERENCES `scrappy_commerce_orders` (`tenant_id`, `stage`, `order_id`)
138
+ );
139
+ --> statement-breakpoint
140
+ CREATE TRIGGER `scrappy_shipment_audit_no_update`
141
+ BEFORE UPDATE ON `scrappy_shipment_audit`
142
+ BEGIN
143
+ SELECT RAISE(ABORT, 'append-only');
144
+ END;
145
+ --> statement-breakpoint
146
+ CREATE TRIGGER `scrappy_shipment_audit_no_delete`
147
+ BEFORE DELETE ON `scrappy_shipment_audit`
148
+ BEGIN
149
+ SELECT RAISE(ABORT, 'append-only');
150
+ END;
151
+ --> statement-breakpoint
152
+ CREATE TABLE `scrappy_caller_replay` (
153
+ `tenant_id` text NOT NULL,
154
+ `stage` text NOT NULL CHECK (stage IN ('preview', 'publication')),
155
+ `issuer` text NOT NULL,
156
+ `jti` text NOT NULL,
157
+ `seen_at` text NOT NULL,
158
+ PRIMARY KEY (`tenant_id`, `stage`, `issuer`, `jti`)
159
+ );
160
+ --> statement-breakpoint
161
+ CREATE TABLE `scrappy_sweep_cursor` (
162
+ `tenant_id` text NOT NULL,
163
+ `stage` text NOT NULL CHECK (stage IN ('preview', 'publication')),
164
+ `cursor` text NOT NULL,
165
+ PRIMARY KEY (`tenant_id`, `stage`)
166
+ );
@@ -1,15 +1,11 @@
1
1
  /**
2
- * <CheckoutComplianceGate> — the storefront's pre-handoff compliance ENFORCEMENT
3
- * seam for regulated (age/nicotine) tenants.
2
+ * <CheckoutComplianceGate> — browser preflight for a regulated add-to-cart submit.
4
3
  *
5
- * Checkout completion happens in the EXTERNAL managed cart, so the last moment the
6
- * storefront controls is the add-to-cart submit. This island intercepts that
7
- * submit for any `form[data-managed-cart]`, collects the ship-to state + 21+
8
- * affirmation, and asks the server-authoritative gate (`/api/compliance/preflight`,
9
- * F12) whether the cart may proceed. Only a `clear` decision releases the handoff;
10
- * a hard block (restricted destination, flavored ban, per-order limit, failed /
11
- * unconfigured verification) STOPS it and shows why. Fail-closed by construction:
12
- * if the preflight itself errors on a regulated tenant, we do NOT hand off.
4
+ * The dialog collects the ship-to state and the age affirmation, then asks
5
+ * `/api/compliance/preflight` whether this browser may submit the form. Only a
6
+ * server `clear` releases that submit. A hard block (restricted destination,
7
+ * flavored ban, per-order limit, failed or unconfigured verification) stops it.
8
+ * If the preflight cannot be read, the dialog does not submit.
13
9
  *
14
10
  * On a tenant wired to the LIVE Token of Trust verification provider, preflight
15
11
  * additionally echoes a `verification` block (ref / provider status / continuation
@@ -18,15 +14,13 @@
18
14
  * continuation URL in a new tab, then re-read the SERVER's decision on an interval
19
15
  * until it clears or denies. Sandbox / no-provider tenants never receive the block
20
16
  * and see exactly the flow they always did. The client never decides a pass:
21
- * `decision.status === "clear"` from the server is the only thing that releases the
22
- * handoff, here as everywhere else. See `checkoutVerificationFlow.ts`.
17
+ * `decision.status === "clear"` from the server is the only thing that releases
18
+ * the submit. See `checkoutVerificationFlow.ts`.
23
19
  *
24
- * This is progressive enhancement: the underlying form still posts the real
25
- * (server-pre-signed) cart fields. If this island never hydrates, the form submits
26
- * as before — so the gate hardens the flow without being a single point of failure
27
- * for rendering the buy button. The REAL production enforcement is defense-in-depth
28
- * in the managed-cart pre-cart webhook (Track B); this is the storefront-side check
29
- * and the demo/preview's visible compliance checkpoint.
20
+ * This preflight is browser UX. It is not payment authorization. A `clear`
21
+ * decision only lets the native form submit; the server checks the cart again
22
+ * and never treats this dialog as authority to take payment. If the island
23
+ * never hydrates, or scripts are disabled, the form posts on its own.
30
24
  */
31
25
  import { useCallback, useEffect, useRef, useState } from "preact/hooks";
32
26
  import type { CartComplianceDecision } from "@tot/public-runtime";
@@ -177,11 +171,12 @@ export default function CheckoutComplianceGate({ preflightUrl, minAge }: Props)
177
171
  const proceed = useCallback((line: PendingLine) => {
178
172
  stopPolling();
179
173
  clearStoredRef(verificationRefKey(preflightUrl, line));
174
+ // UX latch so this dialog does not intercept the resubmit. Not payment
175
+ // authority — the form's server action checks the cart.
180
176
  line.form.dataset.complianceCleared = "1";
177
+ line.form.dataset.paymentAuthority = "none";
181
178
  document.documentElement.style.overflow = "";
182
179
  setPending(null);
183
- // requestSubmit re-runs submit; our listener now passes it through, and the
184
- // managed-cart loader takes over the (still server-pre-signed) POST.
185
180
  if (typeof line.form.requestSubmit === "function") line.form.requestSubmit();
186
181
  else line.form.submit();
187
182
  }, [preflightUrl, stopPolling]);
@@ -377,6 +372,7 @@ export default function CheckoutComplianceGate({ preflightUrl, minAge }: Props)
377
372
  <div
378
373
  class="fixed inset-0 z-[110] bg-[color-mix(in_srgb,var(--color-text)_75%,transparent)] backdrop-blur-sm"
379
374
  role="dialog"
375
+ data-payment-authority="none"
380
376
  aria-modal="true"
381
377
  aria-labelledby="cgate-title"
382
378
  onClick={(e) => {
@@ -24,7 +24,11 @@
24
24
  * wins; this only fills in when `commerce.checkout` is absent.
25
25
  */
26
26
  import type { TenantConfig, ResolvedManagedCheckout } from "@tot/public-runtime";
27
- import { resolveManagedCheckout, SHARED_DEMO_CHECKOUT } from "@tot/public-runtime";
27
+ import {
28
+ CART_SIGNING_OPERATOR_ERROR,
29
+ resolveManagedCheckout,
30
+ SHARED_DEMO_CHECKOUT,
31
+ } from "@tot/public-runtime";
28
32
  import { readEnv } from "@/lib/runtimeEnv";
29
33
 
30
34
  /**
@@ -86,6 +90,8 @@ export async function readCartSecret(
86
90
  export interface CartSigningResolution {
87
91
  enabled: boolean;
88
92
  secret?: string;
93
+ /** Set when signed mode has no secret. Actionable for the operator. No secret value. */
94
+ operatorError?: string;
89
95
  }
90
96
 
91
97
  /**
@@ -108,5 +114,5 @@ export async function resolveCartSigning(
108
114
  "[storefront] signed checkout is configured but FOXY_CART_SECRET is missing — " +
109
115
  "buy forms disabled (fail-closed). Set the cart-validation secret for this environment.",
110
116
  );
111
- return { enabled: false };
117
+ return { enabled: false, operatorError: CART_SIGNING_OPERATOR_ERROR };
112
118
  }
@@ -18,8 +18,15 @@
18
18
  * unattributed response; the handoff proceeds, checkout is never blocked.
19
19
  */
20
20
  import type { APIContext } from "astro";
21
- import { parseConsentSnapshot, defaultConsentSnapshot } from "@tot/public-runtime";
21
+ import {
22
+ enforceCartSubmission,
23
+ foxyCartRedirectUrl,
24
+ parseConsentSnapshot,
25
+ defaultConsentSnapshot,
26
+ type CartDenialCode,
27
+ } from "@tot/public-runtime";
22
28
  import { resolveCheckoutHandoffCorrelation } from "@/lib/commerce/checkoutHandoffCorrelation";
29
+ import { resolveCartSigning } from "@/lib/checkoutCommerce";
23
30
 
24
31
  export const prerender = false;
25
32
 
@@ -38,7 +45,78 @@ interface HandoffBody {
38
45
  consent?: unknown;
39
46
  }
40
47
 
48
+ function escapeHtml(value: string): string {
49
+ return value.replace(/[&<>"']/g, (ch) => {
50
+ if (ch === "&") return "&amp;";
51
+ if (ch === "<") return "&lt;";
52
+ if (ch === ">") return "&gt;";
53
+ if (ch === '"') return "&quot;";
54
+ return "&#39;";
55
+ });
56
+ }
57
+
58
+ function cartDenial(code: CartDenialCode, message: string, status: number): Response {
59
+ const body =
60
+ `<!doctype html><html><body><p data-cart-denial="${code}" data-payment-authorized="false">${escapeHtml(message)}</p></body></html>`;
61
+ return new Response(body, {
62
+ status,
63
+ headers: {
64
+ "content-type": "text/html; charset=utf-8",
65
+ "cache-control": "no-store",
66
+ "x-tot-payment-authorized": "false",
67
+ },
68
+ });
69
+ }
70
+
71
+ function isCartForm(request: Request): boolean {
72
+ const type = request.headers.get("content-type") ?? "";
73
+ return (
74
+ type.includes("application/x-www-form-urlencoded") ||
75
+ type.includes("multipart/form-data")
76
+ );
77
+ }
78
+
79
+ /** Native cart POST. Browser preflight is not consulted. Payment is never authorized. */
80
+ async function cartFormPost(context: APIContext): Promise<Response> {
81
+ const { tenant, commerce } = context.locals;
82
+ if (!commerce) {
83
+ return cartDenial(
84
+ "signing-unavailable",
85
+ "Checkout is unavailable because this store has no cart configured.",
86
+ 409,
87
+ );
88
+ }
89
+ const signing = await resolveCartSigning(commerce);
90
+ if (!signing.enabled) {
91
+ return cartDenial("signing-unavailable", signing.operatorError ?? "Signed checkout is unavailable.", 503);
92
+ }
93
+ const form = await context.request.formData();
94
+ const entries: Array<[string, string]> = [];
95
+ for (const [name, value] of form.entries()) {
96
+ if (typeof value === "string") entries.push([name, value]);
97
+ }
98
+ const result = await enforceCartSubmission({
99
+ entries,
100
+ mode: commerce.mode,
101
+ secret: signing.secret,
102
+ trustedTenantId: tenant.tenant_id,
103
+ });
104
+ if (!result.ok) {
105
+ const status = result.code === "signing-unavailable" ? 503 : 422;
106
+ return cartDenial(result.code, result.message, status);
107
+ }
108
+ return new Response(null, {
109
+ status: 303,
110
+ headers: {
111
+ location: foxyCartRedirectUrl(commerce.cartUrl, result.fields),
112
+ "cache-control": "no-store",
113
+ "x-tot-payment-authorized": "false",
114
+ },
115
+ });
116
+ }
117
+
41
118
  export async function POST(context: APIContext): Promise<Response> {
119
+ if (isCartForm(context.request)) return cartFormPost(context);
42
120
  const { tenant, browserEnvironment, commerce } = context.locals;
43
121
 
44
122
  // No managed checkout on this tenant → nothing to attribute.
@@ -21,7 +21,9 @@ import ReviewSummary from "@/components/commerce/ReviewSummary.astro";
21
21
  import ReviewList from "@/components/commerce/ReviewList.astro";
22
22
  import { isOnSale, isNew, isAvailable, fromPrice } from "@/lib/tot/query";
23
23
  import {
24
- buildMemberSignedCartPayload,
24
+ buildVariantCartPayload,
25
+ priceForPurchaseOption,
26
+ workerCartApiPath,
25
27
  normalizePurchaseOptions,
26
28
  visiblePurchaseOptions,
27
29
  isAutopilotEnabled,
@@ -208,6 +210,13 @@ const autopilotEnabled = isAutopilotEnabled(tenant.commerce?.autopilot);
208
210
  // list/subscription price; a Phase-2 member request signs their member price.
209
211
  const pricingCtx: PurchaseOptionPricingContext = { member, program: membershipProgram };
210
212
 
213
+ const cartApiPath = workerCartApiPath({
214
+ provider: tenant.serving?.provider ?? "worker-r2-pointer",
215
+ phase: Astro.locals.browserEnvironment.environment === "live" ? "publication" : "preview",
216
+ apiBasePath,
217
+ endpoint: "/api/commerce/checkout-handoff",
218
+ });
219
+
211
220
  let checkoutCart:
212
221
  | {
213
222
  action: string;
@@ -215,12 +224,15 @@ let checkoutCart:
215
224
  byVariantId: Record<string, Record<string, VariantCartPayload>>;
216
225
  }
217
226
  | undefined;
227
+ let cartOperatorError: string | undefined;
218
228
  if (commerce) {
219
229
  // Fail closed for production signed checkout: a signed tenant with no
220
230
  // cart-validation secret builds NO cart (never a tamperable form). See
221
231
  // resolveCartSigning — it also logs the misconfiguration loudly.
222
- const { enabled, secret } = await resolveCartSigning(commerce);
223
- if (enabled) {
232
+ const signing = await resolveCartSigning(commerce);
233
+ if (!signing.enabled) {
234
+ cartOperatorError = signing.operatorError;
235
+ } else {
224
236
  try {
225
237
  const purchaseOptions = visiblePurchaseOptions(
226
238
  normalizePurchaseOptions(product),
@@ -230,33 +242,41 @@ if (commerce) {
230
242
  product.variants.map(
231
243
  async (v) => {
232
244
  const payloads = await Promise.all(
233
- purchaseOptions.map(
234
- async (purchaseOption) =>
235
- [
236
- purchaseOption.id,
237
- await buildMemberSignedCartPayload(product, v, {
238
- mode: commerce.mode,
239
- secret,
240
- purchaseOption,
241
- pricing: pricingCtx,
242
- autopilotEnabled,
243
- }),
244
- ] as const,
245
- ),
245
+ purchaseOptions.map(async (purchaseOption) => {
246
+ const effective = priceForPurchaseOption(
247
+ v.price,
248
+ purchaseOption,
249
+ pricingCtx,
250
+ );
251
+ return [
252
+ purchaseOption.id,
253
+ await buildVariantCartPayload(product, v, {
254
+ mode: commerce.mode,
255
+ secret: signing.secret,
256
+ purchaseOption,
257
+ signedPrice: effective.price,
258
+ autopilotEnabled,
259
+ tenantId: tenant.tenant_id,
260
+ }),
261
+ ] as const;
262
+ }),
246
263
  );
247
264
  return [v.id, Object.fromEntries(payloads)] as const;
248
265
  },
249
266
  ),
250
267
  );
251
268
  checkoutCart = {
252
- action: commerce.cartUrl,
269
+ action: cartApiPath,
253
270
  purchaseOptions,
254
271
  byVariantId: Object.fromEntries(entries),
255
272
  };
256
273
  } catch (err) {
274
+ cartOperatorError =
275
+ err instanceof Error && err.name === "CartSigningUnavailableError"
276
+ ? err.message
277
+ : undefined;
257
278
  console.warn(
258
279
  `[storefront] Invalid checkout purchase options for ${product.handle}; cart disabled for this product.`,
259
- err,
260
280
  );
261
281
  }
262
282
  }
@@ -368,8 +388,15 @@ const jsonLd = [productJsonLd, breadcrumbLd(crumbs, canonicalBase)];
368
388
  </div>
369
389
  )}
370
390
 
371
- {/* Variant selector island owns live price/SKU/availability + buy CTA. */}
372
- <div class="mt-7">
391
+ {/* Variant selector island owns live price/SKU/availability + buy CTA.
392
+ The form action is the worker cart route, so a native submit reaches
393
+ the server check. A clear browser preflight is not payment authority. */}
394
+ <div class="mt-7" data-payment-authority="none">
395
+ {cartOperatorError && (
396
+ <p class="mb-3 text-sm text-[var(--color-text)]" role="alert" data-cart-operator-error>
397
+ {cartOperatorError}
398
+ </p>
399
+ )}
373
400
  <VariantSelector
374
401
  client:load
375
402
  variants={publicSelectorVariants(product.variants)}
@@ -380,7 +407,7 @@ const jsonLd = [productJsonLd, breadcrumbLd(crumbs, canonicalBase)];
380
407
  productTitle={product.title}
381
408
  checkoutCart={checkoutCart}
382
409
  pricing={pricing}
383
- handoffUrl={withBase(apiBasePath, "/api/commerce/checkout-handoff")}
410
+ handoffUrl={cartApiPath}
384
411
  />
385
412
  {/* Regulated-tenant checkout ENFORCEMENT gate. Intercepts the
386
413
  managed-cart handoff to clear age + ship-to compliance server-side
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.4.48",
3
+ "version": "2.4.50",
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",