@tokenoftrust/storefront-runner 1.3.2 → 1.3.4-rc.2

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.
Files changed (79) hide show
  1. package/apps/storefront/astro.config.mjs +59 -0
  2. package/apps/storefront/env.d.ts +24 -8
  3. package/apps/storefront/package.json +1 -1
  4. package/apps/storefront/src/components/admin/{AdminFoxyCartSettingsPanel.astro → AdminCheckoutSettingsPanel.astro} +50 -51
  5. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +2 -2
  6. package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +613 -0
  7. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +3 -3
  8. package/apps/storefront/src/components/chrome/SiteHeader.astro +3 -4
  9. package/apps/storefront/src/components/commerce/{FoxySidecartStyles.astro → CartDrawerStyles.astro} +13 -14
  10. package/apps/storefront/src/components/commerce/CheckoutLoader.astro +21 -0
  11. package/apps/storefront/src/components/islands/VariantSelector.tsx +101 -20
  12. package/apps/storefront/src/config/adminTargetTenant.ts +2 -2
  13. package/apps/storefront/src/config/devTenantSeed.ts +65 -16
  14. package/apps/storefront/src/config/resolver.ts +48 -1
  15. package/apps/storefront/src/layouts/Layout.astro +14 -12
  16. package/apps/storefront/src/lib/admin/README.md +2 -2
  17. package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +5 -5
  18. package/apps/storefront/src/lib/admin/orderClient.ts +20 -20
  19. package/apps/storefront/src/lib/admin/services/mock/{foxyCartSettings.ts → checkoutSettings.ts} +21 -22
  20. package/apps/storefront/src/lib/admin/services/mock/index.ts +3 -2
  21. package/apps/storefront/src/lib/admin/services/mock/settings.ts +1 -1
  22. package/apps/storefront/src/lib/admin/services/mock/subscriptions.ts +398 -0
  23. package/apps/storefront/src/lib/admin/services/orders.ts +21 -21
  24. package/apps/storefront/src/lib/admin/viewmodels/subscriptions.ts +106 -0
  25. package/apps/storefront/src/lib/auth/loginGate.ts +14 -3
  26. package/apps/storefront/src/lib/{foxyCommerce.ts → checkoutCommerce.ts} +20 -18
  27. package/apps/storefront/src/lib/commerce/{foxyCartSettings.ts → checkoutSettings.ts} +65 -66
  28. package/apps/storefront/src/lib/dev/activityIngestToken.ts +9 -0
  29. package/apps/storefront/src/lib/dev/activityStore.ts +12 -0
  30. package/apps/storefront/src/lib/dev/cliSignInCode.ts +23 -4
  31. package/apps/storefront/src/lib/dev/runtimeStore.ts +193 -22
  32. package/apps/storefront/src/lib/edgeCache.ts +7 -6
  33. package/apps/storefront/src/lib/health.ts +53 -0
  34. package/apps/storefront/src/lib/privacy/emailHint.ts +33 -0
  35. package/apps/storefront/src/lib/rawMarketingHtml.ts +1 -0
  36. package/apps/storefront/src/lib/sections.ts +19 -23
  37. package/apps/storefront/src/lib/securityHeaders.ts +1 -1
  38. package/apps/storefront/src/lib/storyblok/content-model.ts +3 -2
  39. package/apps/storefront/src/middleware/index.ts +29 -12
  40. package/apps/storefront/src/pages/[...slug].astro +5 -6
  41. package/apps/storefront/src/pages/admin.astro +10 -3
  42. package/apps/storefront/src/pages/api/admin/orders/[id].ts +4 -4
  43. package/apps/storefront/src/pages/api/admin/orders.ts +5 -5
  44. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +14 -7
  45. package/apps/storefront/src/pages/api/checkout-style.css.ts +5 -5
  46. package/apps/storefront/src/pages/auth/login.astro +4 -1
  47. package/apps/storefront/src/pages/auth/magic.astro +263 -43
  48. package/apps/storefront/src/pages/capabilities.astro +1 -1
  49. package/apps/storefront/src/pages/cockpit.astro +2365 -0
  50. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +19 -0
  51. package/apps/storefront/src/pages/index.astro +23 -29
  52. package/apps/storefront/src/pages/products/[handle].astro +50 -20
  53. package/package.json +6 -2
  54. package/packages/public-runtime/src/capabilities.ts +97 -9
  55. package/packages/public-runtime/src/{foxy-orders.ts → checkout-orders.ts} +28 -27
  56. package/packages/public-runtime/src/checkout-style-publish.ts +25 -25
  57. package/packages/public-runtime/src/checkout-style.ts +13 -13
  58. package/packages/public-runtime/src/{foxy.ts → checkout.ts} +111 -34
  59. package/packages/public-runtime/src/csp.ts +11 -11
  60. package/packages/public-runtime/src/customization-scripts.ts +28 -0
  61. package/packages/public-runtime/src/extension-contract.ts +73 -0
  62. package/packages/public-runtime/src/index.ts +4 -2
  63. package/packages/public-runtime/src/product.ts +57 -0
  64. package/packages/public-runtime/src/raw-html-policy.ts +35 -0
  65. package/packages/public-runtime/src/tenant.ts +14 -11
  66. package/pnpm-workspace.yaml +1 -0
  67. package/scripts/dev/checkout-watch.mjs +9 -0
  68. package/scripts/dev/file-browser.mjs +5 -1
  69. package/scripts/dev/transient-files.mjs +34 -0
  70. package/scripts/tot-dev.mjs +16 -3
  71. package/apps/storefront/src/components/commerce/FoxyLoader.astro +0 -23
  72. package/apps/storefront/src/pages/api/dev/activity.ts +0 -124
  73. package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +0 -73
  74. package/apps/storefront/src/pages/api/dev/diff.ts +0 -68
  75. package/apps/storefront/src/pages/api/dev/file.ts +0 -57
  76. package/apps/storefront/src/pages/api/dev/locate.ts +0 -125
  77. package/apps/storefront/src/pages/api/dev/publish.ts +0 -128
  78. package/apps/storefront/src/pages/api/dev/tree.ts +0 -70
  79. package/apps/storefront/src/pages/api/dev/write.ts +0 -116
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Checkout-styling data model + published-artifact read plumbing.
3
3
  *
4
- * The FoxyCart-hosted checkout is styled by CSS pushed to Foxy's `/theme/foxy`
4
+ * The hosted checkout is styled by CSS pushed through the commerce service's theme
5
5
  * endpoint (U11). This module owns the *source of truth* for that styling: a
6
6
  * small, structured, tenant-editable descriptor that later units render into
7
7
  * ASCII-safe CSS. It sits on the SAME versioned publish spine as theme/content
@@ -12,7 +12,7 @@
12
12
  * Two colocated artifacts under `tenants/<id>/`:
13
13
  * - `checkout-style.json` — the structured `CheckoutStyle` descriptor (below).
14
14
  * - `checkout.css` — an optional raw-CSS escape hatch a merchant edits
15
- * directly; hosted + pushed to Foxy as-is (U11).
15
+ * directly; hosted + pushed to checkout as-is (U11).
16
16
  *
17
17
  * Pure + dependency-free (node-free, edge-safe): the caller injects the same
18
18
  * `KvGet` used for theme/content, keeping this unit-testable and storage-agnostic.
@@ -27,7 +27,7 @@ import { readPublishedArtifact, type KvGet } from "./customization-runtime.js";
27
27
  * subset of the full theme palette — the checkout is a focused conversion
28
28
  * surface, not the whole store. All values are CSS colors; hex is validated for
29
29
  * WCAG contrast downstream (U13 gate), non-hex (e.g. `rgb(...)`) is passed
30
- * through. Keep values ASCII-safe — Foxy's cache pass isn't Unicode-safe.
30
+ * through. Keep values ASCII-safe — the checkout cache pass isn't Unicode-safe.
31
31
  */
32
32
  export interface CheckoutStyleColors {
33
33
  /** Page background behind the checkout card. */
@@ -46,7 +46,7 @@ export interface CheckoutStyleColors {
46
46
 
47
47
  /**
48
48
  * Typography for the checkout page. `fontFamily` is a full CSS font-family stack
49
- * (a web-safe stack, or a Foxy-supported webfont name plus fallbacks) — kept as
49
+ * (a web-safe stack, or a provider-supported webfont name plus fallbacks) — kept as
50
50
  * an opaque string so a merchant can bring their own stack without a schema bump.
51
51
  */
52
52
  export interface CheckoutStyleTypography {
@@ -65,7 +65,7 @@ export interface CheckoutStyleButton {
65
65
  /**
66
66
  * The structured checkout-styling descriptor — the `checkout-style.json`
67
67
  * artifact. This is the model a merchant edits (U12) and that U11 renders into
68
- * the CSS pushed to Foxy. A published artifact is a `DeepPartial` of this
68
+ * the CSS pushed to checkout. A published artifact is a `DeepPartial` of this
69
69
  * (a merchant may set one field without restating the rest); `resolveCheckout
70
70
  * Style` merges it over `DEFAULT_CHECKOUT_STYLE` so the resolved value is always
71
71
  * complete.
@@ -114,12 +114,12 @@ export const DEFAULT_CHECKOUT_STYLE: CheckoutStyle = {
114
114
  * stack (the checkout is body copy + a CTA, not display type)
115
115
  * - button.radius ← `shape.radius.md` (the standard control radius)
116
116
  * - logoUrl ← `brand.logoLight`, but ONLY when it is an absolute https URL —
117
- * Foxy fetches the checkout server-side, so a repo-relative logo path
117
+ * Checkout fetches the template server-side, so a repo-relative logo path
118
118
  * (e.g. `/brand/logo.svg`) can't resolve there and is dropped.
119
119
  *
120
120
  * Input is a `DeepPartial` (a tenant's `theme.json` override is sparse), so every
121
121
  * field falls back to `DEFAULT_CHECKOUT_STYLE` when the theme omits it — the result
122
- * is always a complete `CheckoutStyle`. ASCII discipline (Foxy's cache is not
122
+ * is always a complete `CheckoutStyle`. ASCII discipline (the checkout cache is not
123
123
  * Unicode-safe): any derived string leaf that isn't `isAsciiSafe` is dropped in
124
124
  * favor of the default, so a theme can never produce a checkout-blanking value.
125
125
  * Pure + node-free.
@@ -193,7 +193,7 @@ export function checkoutStyleArtifactPath(tenantId: string): string {
193
193
  /**
194
194
  * Canonical path for the optional raw-CSS override artifact — the escape hatch a
195
195
  * merchant edits directly (or that advanced templates ship). Sibling to the
196
- * structured descriptor; hosted + pushed to Foxy verbatim by U11.
196
+ * structured descriptor; hosted + pushed to checkout verbatim by U11.
197
197
  */
198
198
  export function checkoutStyleCssPath(tenantId: string): string {
199
199
  return `tenants/${tenantId}/checkout.css`;
@@ -289,11 +289,11 @@ export async function resolveCheckoutStyleCss(
289
289
  }
290
290
 
291
291
  // ---------------------------------------------------------------------------
292
- // CSS serialization (the artifact pushed to FoxyCart) — U11
292
+ // CSS serialization (the artifact pushed to checkout) — U11
293
293
  // ---------------------------------------------------------------------------
294
294
 
295
295
  /**
296
- * True iff every character is 7-bit ASCII (code point < 128). FoxyCart's
296
+ * True iff every character is 7-bit ASCII (code point < 128). The checkout
297
297
  * template cache pass is NOT Unicode-safe — a non-ASCII byte in the cached
298
298
  * checkout content yields a BLANK page. So the publisher (U11) MUST gate both
299
299
  * the generated token CSS and any raw override through this before pushing a
@@ -319,7 +319,7 @@ const CHECKOUT_COLOR_VARS: Record<keyof CheckoutStyleColors, string> = {
319
319
 
320
320
  /**
321
321
  * Serialize a resolved `CheckoutStyle` into a `:root { … }` block of CSS custom
322
- * properties (`--tot-checkout-*`) that a FoxyCart checkout template references.
322
+ * properties (`--tot-checkout-*`) that the hosted checkout template references.
323
323
  * Deterministic (stable key order) so an unchanged style produces byte-identical
324
324
  * output — the publisher can hash it to skip no-op pushes. The `logoUrl`, when
325
325
  * set, is emitted as `--tot-checkout-logo: url("…")`.
@@ -348,10 +348,10 @@ export function checkoutStyleToCss(style: CheckoutStyle): string {
348
348
 
349
349
  /**
350
350
  * Assemble the full checkout CSS served at the stable storefront URL that becomes
351
- * FoxyCart's `content_url`: the generated token block (`checkoutStyleToCss` over
351
+ * checkout's `content_url`: the generated token block (`checkoutStyleToCss` over
352
352
  * the resolved `CheckoutStyle`) followed by the tenant's optional raw-CSS override.
353
353
  *
354
- * ASCII discipline (Foxy's cache pass is not Unicode-safe → blank checkout): the
354
+ * ASCII discipline (the checkout cache pass is not Unicode-safe → blank checkout): the
355
355
  * raw override is appended ONLY when it is `isAsciiSafe` — a non-ASCII override is
356
356
  * DROPPED here so a bad byte can never blank the live checkout (the authoritative
357
357
  * reject-at-publish gate is U13's promote GateReport; this is defense-in-depth at
@@ -1,35 +1,39 @@
1
1
  /**
2
- * FoxyCart cart engine — renderer-safe helpers (Track A).
2
+ * Managed checkout cart engine — renderer-safe helpers (Track A).
3
3
  *
4
- * The storefront's whole job as a FoxyCart front end is small: include Foxy's
5
- * loader.js, and emit add-to-cart links/forms whose product name/value pairs are
6
- * HMAC-signed with the store's "cart validation" secret so the price can't be
7
- * tampered. This module builds the fields for a catalog variant and (in signed
8
- * mode) signs them with WebCrypto HMAC-SHA256 — pure, dependency-free, and safe
9
- * in both `astro dev` (Node 20) and the Cloudflare Worker runtime.
4
+ * The storefront's job is intentionally small: include the managed checkout
5
+ * loader, and emit add-to-cart links/forms whose product name/value pairs are
6
+ * HMAC-signed with the store's cart-validation secret so the price can't be
7
+ * tampered with. This module builds the fields for a catalog variant and signs
8
+ * them with WebCrypto HMAC-SHA256 in signed mode — pure, dependency-free, and
9
+ * safe in both `astro dev` (Node 20) and the Cloudflare Worker runtime.
10
10
  *
11
- * The excise-tax line is NOT computed here — it materializes inside the FoxyCart
12
- * cart via the store's pre-cart webhook + `tot_excise_tax` coupon (Track B, the
13
- * tot-foxycart service). See docs / the excise-tax-checkout recipe.
11
+ * The excise-tax line is NOT computed here — it materializes inside the managed
12
+ * cart via the store's pre-cart webhook + `tot_excise_tax` coupon (Track B).
14
13
  *
15
- * Signing reference (Foxy "HMAC product verification"):
14
+ * Signing reference (checkout HMAC product verification):
16
15
  * hash = HMAC-SHA256( productcode + fieldname + fieldvalue , storeSecret )
17
16
  * link/form field name becomes `fieldname||hash` (value unchanged).
18
17
  * Editable fields (quantity) hash the value `--OPEN--` and emit
19
18
  * `fieldname||hash||open`. Cart-level params (cart, coupon, output, …) and
20
19
  * `h:`/`x:`/`__`/`utm_`-prefixed params are never signed.
21
20
  */
22
- import type { CatalogProduct, CatalogVariant } from "./product.js";
21
+ import type {
22
+ CatalogProduct,
23
+ CatalogVariant,
24
+ ProductPurchaseOption,
25
+ SubscriptionPurchaseOption,
26
+ } from "./product.js";
23
27
 
24
28
  /**
25
- * Per-tenant FoxyCart wiring. Public config only — the signing SECRET is never
29
+ * Per-tenant managed-checkout wiring. Public config only — the signing SECRET is never
26
30
  * stored here (it's an env-injected Worker binding, resolved per request). In
27
31
  * `unsigned-demo` mode there is no secret at all: links go to a demo store that
28
32
  * has cart validation turned OFF, so a developer can run the full cart locally
29
33
  * (and on our shared preview worker) with zero secrets.
30
34
  */
31
- export interface FoxyCommerceConfig {
32
- /** Foxy store subdomain, e.g. "tot-demo" → tot-demo.foxycart.com. */
35
+ export interface ManagedCheckoutConfig {
36
+ /** Checkout store subdomain, e.g. "tot-demo". */
33
37
  storeSubDomain: string;
34
38
  /**
35
39
  * Full cart/checkout host. Defaults to `${storeSubDomain}.foxycart.com`; set
@@ -42,26 +46,25 @@ export interface FoxyCommerceConfig {
42
46
  */
43
47
  mode: "signed" | "unsigned-demo";
44
48
  /**
45
- * Optional per-tenant CSS to brand Foxy's sidecart. Foxy's v2.0 sidecart
49
+ * Optional per-tenant CSS to brand the in-page cart drawer. The checkout loader
46
50
  * renders into our own page DOM (root `#fc`, light DOM — no iframe/shadow), so
47
51
  * it inherits the theme's CSS custom properties and can be restyled with plain
48
- * CSS. `<FoxySidecartStyles>` already emits a brand baseline derived from the
52
+ * CSS. `<CartDrawerStyles>` already emits a brand baseline derived from the
49
53
  * theme tokens; this string is APPENDED after it for bespoke overrides. Scope
50
- * every rule under `#fc` and use `!important` to beat Foxy's own
51
- * responsive_styles.css (it loads after our <head> style). NB: this styles the
52
- * in-page sidecart only — the hosted checkout page is a separate origin,
53
- * themed in the Foxy admin template.
54
+ * every rule under `#fc` and use `!important` to beat the checkout loader's
55
+ * responsive styles. NB: this styles the in-page cart drawer only the hosted
56
+ * checkout page is a separate origin, themed through checkout settings.
54
57
  */
55
- sidecartCss?: string;
58
+ cartDrawerCss?: string;
56
59
  }
57
60
 
58
61
  /** Resolved, request-scoped commerce facts stashed on `Astro.locals`. */
59
- export interface ResolvedFoxyCommerce extends FoxyCommerceConfig {
62
+ export interface ResolvedManagedCheckout extends ManagedCheckoutConfig {
60
63
  /** `https://<storeDomain>` — the loader + cart origin (for CSP + links). */
61
64
  origin: string;
62
65
  /** `https://<storeDomain>/cart` — the add-to-cart form action / cart link. */
63
66
  cartUrl: string;
64
- /** `https://cdn.foxycart.com/<storeSubDomain>/loader.js`. */
67
+ /** Managed checkout loader script URL. */
65
68
  loaderSrc: string;
66
69
  }
67
70
 
@@ -88,14 +91,22 @@ export interface VariantCartPayload {
88
91
  quantityName: string;
89
92
  }
90
93
 
91
- const FOXY_HOST_SUFFIX = ".foxycart.com";
94
+ export interface BuildVariantCartPayloadOptions {
95
+ mode: ManagedCheckoutConfig["mode"];
96
+ secret?: string;
97
+ purchaseOption?: ProductPurchaseOption;
98
+ }
99
+
100
+ const DEFAULT_CHECKOUT_HOST_SUFFIX = ".foxycart.com";
101
+ const SUBSCRIPTION_FREQUENCY_RE = /^(?:\d+(?:\.\d+)?|\.\d+)[dwmy]$/;
92
102
 
93
103
  /** Resolve the full request-scoped commerce facts from a tenant's config. */
94
- export function resolveFoxyCommerce(
95
- config: FoxyCommerceConfig,
96
- ): ResolvedFoxyCommerce {
104
+ export function resolveManagedCheckout(
105
+ config: ManagedCheckoutConfig,
106
+ ): ResolvedManagedCheckout {
97
107
  const storeDomain =
98
- config.storeDomain ?? `${config.storeSubDomain}${FOXY_HOST_SUFFIX}`;
108
+ config.storeDomain ??
109
+ `${config.storeSubDomain}${DEFAULT_CHECKOUT_HOST_SUFFIX}`;
99
110
  const origin = `https://${storeDomain}`;
100
111
  return {
101
112
  ...config,
@@ -108,19 +119,21 @@ export function resolveFoxyCommerce(
108
119
 
109
120
  /**
110
121
  * The raw (unsigned) cart fields for a variant: name, price, code, image, the
111
- * variant's options, and an editable quantity. `code` is the Foxy "productcode"
122
+ * variant's options, and an editable quantity. `code` is the checkout product code
112
123
  * — the salt every other field is hashed against — so it must be stable per SKU.
113
124
  */
114
125
  export function cartFieldsForVariant(
115
126
  product: CatalogProduct,
116
127
  variant: CatalogVariant,
128
+ purchaseOption?: ProductPurchaseOption,
117
129
  ): { code: string; fields: CartField[] } {
118
130
  const code = variant.sku && variant.sku.length ? variant.sku : variant.id;
119
131
  const image = product.images.find((i) => i.id === variant.image_id)?.url ??
120
132
  product.images[0]?.url;
133
+ const price = purchaseOptionPrice(variant.price, purchaseOption);
121
134
  const fields: CartField[] = [
122
135
  { name: "name", value: variant.title || product.title },
123
- { name: "price", value: variant.price.toFixed(2) },
136
+ { name: "price", value: price.toFixed(2) },
124
137
  { name: "code", value: code },
125
138
  { name: "quantity", value: "1", open: true },
126
139
  ];
@@ -130,9 +143,69 @@ export function cartFieldsForVariant(
130
143
  for (const [optName, optVal] of Object.entries(variant.option_values)) {
131
144
  if (optName && optVal) fields.push({ name: optName, value: optVal });
132
145
  }
146
+ if (purchaseOption?.kind === "subscription") {
147
+ fields.push(...subscriptionCartFields(purchaseOption));
148
+ }
133
149
  return { code, fields };
134
150
  }
135
151
 
152
+ export function purchaseOptionPrice(
153
+ basePrice: number,
154
+ purchaseOption?: ProductPurchaseOption,
155
+ ): number {
156
+ if (purchaseOption?.kind !== "subscription") return basePrice;
157
+ const adjustment = purchaseOption.priceAdjustment;
158
+ if (!adjustment) return basePrice;
159
+ if (!Number.isFinite(adjustment.value) || adjustment.value < 0) {
160
+ throw new Error(
161
+ `purchase option "${purchaseOption.id}" has invalid price adjustment`,
162
+ );
163
+ }
164
+ if (adjustment.type === "percent_off" && adjustment.value > 100) {
165
+ throw new Error(
166
+ `purchase option "${purchaseOption.id}" has invalid percent discount`,
167
+ );
168
+ }
169
+ if (adjustment.type === "amount_off" && adjustment.value > basePrice) {
170
+ throw new Error(
171
+ `purchase option "${purchaseOption.id}" has invalid amount discount`,
172
+ );
173
+ }
174
+ const adjusted =
175
+ adjustment.type === "percent_off"
176
+ ? basePrice * (1 - adjustment.value / 100)
177
+ : basePrice - adjustment.value;
178
+ return Number(adjusted.toFixed(2));
179
+ }
180
+
181
+ export function isSubscriptionFrequency(value: string): boolean {
182
+ if (!SUBSCRIPTION_FREQUENCY_RE.test(value)) return false;
183
+ const amount = Number.parseFloat(value.slice(0, -1));
184
+ return Number.isFinite(amount) && amount > 0;
185
+ }
186
+
187
+ function subscriptionCartFields(
188
+ option: SubscriptionPurchaseOption,
189
+ ): CartField[] {
190
+ if (!isSubscriptionFrequency(option.frequency)) {
191
+ throw new Error(
192
+ `subscription purchase option "${option.id}" has invalid frequency "${option.frequency}"`,
193
+ );
194
+ }
195
+
196
+ const fields: CartField[] = [
197
+ { name: "Purchase option", value: option.label },
198
+ { name: "sub_frequency", value: option.frequency },
199
+ ];
200
+ if (option.startDate) {
201
+ fields.push({ name: "sub_startdate", value: option.startDate });
202
+ }
203
+ if (option.endDate) {
204
+ fields.push({ name: "sub_enddate", value: option.endDate });
205
+ }
206
+ return fields;
207
+ }
208
+
136
209
  /** Lowercase hex HMAC-SHA256(message) under `secret`, via WebCrypto. */
137
210
  async function hmacHex(secret: string, message: string): Promise<string> {
138
211
  const enc = new TextEncoder();
@@ -151,7 +224,7 @@ async function hmacHex(secret: string, message: string): Promise<string> {
151
224
 
152
225
  /**
153
226
  * Sign one field for product `code`. Editable fields hash the sentinel
154
- * `--OPEN--` and gain the `||open` suffix so Foxy accepts a user-entered value.
227
+ * `--OPEN--` and gain the `||open` suffix so checkout accepts a user-entered value.
155
228
  */
156
229
  async function signField(
157
230
  secret: string,
@@ -174,9 +247,13 @@ async function signField(
174
247
  export async function buildVariantCartPayload(
175
248
  product: CatalogProduct,
176
249
  variant: CatalogVariant,
177
- opts: { mode: FoxyCommerceConfig["mode"]; secret?: string },
250
+ opts: BuildVariantCartPayloadOptions,
178
251
  ): Promise<VariantCartPayload> {
179
- const { code, fields } = cartFieldsForVariant(product, variant);
252
+ const { code, fields } = cartFieldsForVariant(
253
+ product,
254
+ variant,
255
+ opts.purchaseOption,
256
+ );
180
257
  const signed = opts.mode === "signed";
181
258
  if (signed && !opts.secret) {
182
259
  throw new Error(
@@ -22,8 +22,8 @@ export interface CspOptions {
22
22
  /** Sandboxed-widget origins. */
23
23
  frameSrc?: readonly string[];
24
24
  /**
25
- * Extra `form-action` targets. Default is `'self'`; the FoxyCart add-to-cart
26
- * form POSTs cross-origin to the store's `/cart`, so that origin is added here.
25
+ * Extra `form-action` targets. Default is `'self'`; add-to-cart forms POST
26
+ * cross-origin to the checkout store's `/cart`, so that origin is added here.
27
27
  */
28
28
  formAction?: readonly string[];
29
29
  /** Reporting group name for `report-to` (paired with a Reporting-Endpoints header). */
@@ -78,13 +78,13 @@ export const STOREFRONT_CSP_ALLOWLIST = {
78
78
  } as const;
79
79
 
80
80
  /**
81
- * CSP additions for a FoxyCart-enabled tenant. The loader.js comes from
82
- * `cdn.foxycart.com`; the sidecart XHR + iframe + the add-to-cart form POST all
83
- * hit the store's own origin (`<store>.foxycart.com` or a branded checkout
84
- * domain). Merge these into `buildCsp` per request when `locals.commerce.foxy`
85
- * is present so the strict policy still passes.
81
+ * CSP additions for a managed-checkout tenant. The loader comes from the checkout
82
+ * CDN; cart requests, frames, and add-to-cart form POSTs hit the store's own
83
+ * origin (`<store>.foxycart.com` or a branded checkout domain). Merge these into
84
+ * `buildCsp` per request when `locals.commerce` is present so the strict policy
85
+ * still passes.
86
86
  */
87
- export function foxyCspAdditions(storeOrigin: string): {
87
+ export function checkoutCspAdditions(storeOrigin: string): {
88
88
  scriptSrc: string[];
89
89
  styleSrc: string[];
90
90
  fontSrc: string[];
@@ -94,9 +94,9 @@ export function foxyCspAdditions(storeOrigin: string): {
94
94
  imgSrc: string[];
95
95
  } {
96
96
  const cdn = "https://cdn.foxycart.com";
97
- // Foxy's loader pulls its own JS, CSS, and fonts from the CDN and renders the
98
- // sidecart/checkout from the store origin so it needs script/style/font from
99
- // the CDN and connect/frame/form to the store.
97
+ // The checkout loader pulls its own JS, CSS, and fonts from the CDN and renders
98
+ // cart/checkout from the store origin, so it needs script/style/font from the
99
+ // CDN and connect/frame/form to the store.
100
100
  return {
101
101
  scriptSrc: [cdn, storeOrigin],
102
102
  styleSrc: [cdn, storeOrigin],
@@ -18,6 +18,34 @@ export type ScriptSlot = (typeof SCRIPT_SLOTS)[number];
18
18
  export type ScriptStrategy = (typeof SCRIPT_STRATEGIES)[number];
19
19
  export type ScriptIsolation = (typeof SCRIPT_ISOLATIONS)[number];
20
20
 
21
+ export interface ScriptSlotDescriptor {
22
+ slot: ScriptSlot;
23
+ label: string;
24
+ description: string;
25
+ defaultStrategy: ScriptStrategy;
26
+ }
27
+
28
+ export const SCRIPT_SLOT_DESCRIPTORS: Record<ScriptSlot, ScriptSlotDescriptor> = {
29
+ "product.aside": {
30
+ slot: "product.aside",
31
+ label: "Product Aside",
32
+ description: "A sandboxed widget beside product purchase controls.",
33
+ defaultStrategy: "on-interaction",
34
+ },
35
+ "home.section": {
36
+ slot: "home.section",
37
+ label: "Home Section",
38
+ description: "A sandboxed widget between composed homepage sections.",
39
+ defaultStrategy: "on-idle",
40
+ },
41
+ "global.footer": {
42
+ slot: "global.footer",
43
+ label: "Global Footer",
44
+ description: "A sandboxed widget near the page footer on every route.",
45
+ defaultStrategy: "defer",
46
+ },
47
+ };
48
+
21
49
  export interface ScriptRegistration {
22
50
  src: string;
23
51
  slot: ScriptSlot;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Public storefront extension contract.
3
+ *
4
+ * Astro components remain platform-owned implementation detail; tenants compose
5
+ * with versioned JSON blocks and sources. This file is renderer-safe and can be
6
+ * used by docs, validators, CLI checks, and the storefront app itself.
7
+ */
8
+
9
+ export const BLOCK_PALETTE_CURRENT_CONTRACT = "block-palette@3" as const;
10
+ export const BLOCK_PALETTE_SUPPORTED_CONTRACTS = [
11
+ "block-palette@1",
12
+ "block-palette@2",
13
+ BLOCK_PALETTE_CURRENT_CONTRACT,
14
+ ] as const;
15
+
16
+ export type BlockPaletteContract =
17
+ (typeof BLOCK_PALETTE_SUPPORTED_CONTRACTS)[number];
18
+
19
+ export const FEATURED_PRODUCT_SOURCE_KEYS = [
20
+ "featured",
21
+ "newest",
22
+ "bestSelling",
23
+ "onSale",
24
+ ] as const;
25
+
26
+ export type FeaturedProductSourceKey =
27
+ (typeof FEATURED_PRODUCT_SOURCE_KEYS)[number];
28
+
29
+ export type ProductSectionSource =
30
+ | FeaturedProductSourceKey
31
+ | { collection: string }
32
+ | { tag: string }
33
+ | { featured: true }
34
+ | { newest: true }
35
+ | { bestSelling: true }
36
+ | { onSale: true }
37
+ | { handles: string[] }
38
+ | { related: string }
39
+ | { recentlyViewed: true };
40
+
41
+ export function isFeaturedProductSourceKey(
42
+ value: unknown,
43
+ ): value is FeaturedProductSourceKey {
44
+ return (
45
+ typeof value === "string" &&
46
+ (FEATURED_PRODUCT_SOURCE_KEYS as readonly string[]).includes(value)
47
+ );
48
+ }
49
+
50
+ /**
51
+ * Normalize the authored shortcut syntax into the object shape consumed by
52
+ * section renderers. `handles` is accepted as a backwards-compatible sibling
53
+ * prop for existing `featured_products` blocks.
54
+ */
55
+ export function normalizeProductSectionSource(
56
+ source?: ProductSectionSource,
57
+ handles?: string[],
58
+ ): Exclude<ProductSectionSource, FeaturedProductSourceKey> {
59
+ if (handles?.length) return { handles };
60
+ if (!source) return { featured: true };
61
+ if (typeof source !== "string") return source;
62
+ switch (source) {
63
+ case "newest":
64
+ return { newest: true };
65
+ case "bestSelling":
66
+ return { bestSelling: true };
67
+ case "onSale":
68
+ return { onSale: true };
69
+ case "featured":
70
+ default:
71
+ return { featured: true };
72
+ }
73
+ }
@@ -15,8 +15,8 @@
15
15
  // preview / script types the renderer needs. Read/apply only.
16
16
  export * from "./product.js";
17
17
  export * from "./tenant.js";
18
- export * from "./foxy.js";
19
- export * from "./foxy-orders.js";
18
+ export * from "./checkout.js";
19
+ export * from "./checkout-orders.js";
20
20
  export * from "./capabilities.js";
21
21
  export * from "./checkout-style.js";
22
22
  export * from "./checkout-style-publish.js";
@@ -29,6 +29,8 @@ export * from "./customization-runtime.js";
29
29
  export * from "./customization-versioning.js";
30
30
  export * from "./customization-scripts.js";
31
31
  export * from "./customization-reconcile.js";
32
+ export * from "./extension-contract.js";
33
+ export * from "./raw-html-policy.js";
32
34
  export * from "./tenant-assets.js";
33
35
  export * from "./binary-path.js";
34
36
  export * from "./hot-standby-readiness.js";
@@ -43,6 +43,49 @@ export interface ProductOption {
43
43
  values: string[];
44
44
  }
45
45
 
46
+ export type PurchaseOptionKind = "one_time" | "subscription";
47
+
48
+ export interface OneTimePurchaseOption {
49
+ id: string;
50
+ kind: "one_time";
51
+ label: string;
52
+ description?: string;
53
+ }
54
+
55
+ export type PurchaseOptionPriceAdjustment =
56
+ | { type: "percent_off"; value: number }
57
+ | { type: "amount_off"; value: number };
58
+
59
+ export interface SubscriptionPurchaseOption {
60
+ id: string;
61
+ kind: "subscription";
62
+ label: string;
63
+ description?: string;
64
+ /** Shopper-facing savings copy, e.g. "Save 15%". Display only. */
65
+ savingsLabel?: string;
66
+ /** Optional signed-cart price adjustment, e.g. 15% off every recurring order. */
67
+ priceAdjustment?: PurchaseOptionPriceAdjustment;
68
+ /**
69
+ * Recurrence interval accepted by the checkout engine, e.g. "2w", "1m",
70
+ * "60d", "1y", ".5m".
71
+ */
72
+ frequency: string;
73
+ /** Optional future start/free-trial date or fixed renewal day. */
74
+ startDate?: string;
75
+ /** Optional subscription/payment-plan stop date. */
76
+ endDate?: string;
77
+ }
78
+
79
+ export type ProductPurchaseOption =
80
+ | OneTimePurchaseOption
81
+ | SubscriptionPurchaseOption;
82
+
83
+ export const DEFAULT_ONE_TIME_PURCHASE_OPTION: OneTimePurchaseOption = {
84
+ id: "one_time",
85
+ kind: "one_time",
86
+ label: "One-time purchase",
87
+ };
88
+
46
89
  export interface CatalogImage {
47
90
  id: string;
48
91
  /** Final URL after rehost (R2/Cloudflare Images) OR original Shopify CDN URL. */
@@ -111,6 +154,12 @@ export interface CatalogProduct {
111
154
  options: ProductOption[];
112
155
  images: CatalogImage[];
113
156
  variants: CatalogVariant[];
157
+ /**
158
+ * Optional purchase modes for this product. Omitted/empty means a standard
159
+ * one-time purchase. Subscription options are authored as catalog data; the
160
+ * storefront converts them into provider-specific cart parameters at render.
161
+ */
162
+ purchase_options?: ProductPurchaseOption[];
114
163
  price_range: PriceRange;
115
164
  /** Collection handles (or ids) this product belongs to. */
116
165
  collections: string[];
@@ -122,6 +171,14 @@ export interface CatalogProduct {
122
171
  source_checksum: string;
123
172
  }
124
173
 
174
+ export function normalizePurchaseOptions(
175
+ product: Pick<CatalogProduct, "purchase_options">,
176
+ ): ProductPurchaseOption[] {
177
+ return product.purchase_options?.length
178
+ ? product.purchase_options
179
+ : [DEFAULT_ONE_TIME_PURCHASE_OPTION];
180
+ }
181
+
125
182
  /**
126
183
  * A product review (full content), pulled from the merchant's reviews app
127
184
  * (e.g. Stamped.io public widget API) during migration. The aggregate
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Policy for the raw tenant-authored HTML path.
3
+ *
4
+ * Raw HTML is a useful migration/marketing bridge, but it bypasses Astro Layout
5
+ * composition. Regulated tenants must stay on a platform-composed path
6
+ * so age/identity verification, warnings, taxes, checkout signing, and publish
7
+ * gates remain non-removable.
8
+ */
9
+ import type { TenantConfig } from "./tenant.js";
10
+ import { resolveComplianceObligations } from "./capabilities.js";
11
+
12
+ export interface RawHtmlPolicyDecision {
13
+ allowed: boolean;
14
+ regulated: boolean;
15
+ reason?: string;
16
+ }
17
+
18
+ export function rawHtmlPolicy(
19
+ tenant: Pick<TenantConfig, "siteType" | "compliance">,
20
+ ): RawHtmlPolicyDecision {
21
+ const obligations = resolveComplianceObligations(tenant);
22
+ const regulated =
23
+ obligations.ageVerification.required || obligations.exciseTax.required;
24
+
25
+ if (regulated) {
26
+ return {
27
+ allowed: false,
28
+ regulated,
29
+ reason:
30
+ "Raw HTML bypasses platform Layout compliance; regulated commerce tenants must use block composition or an extracted runtime.",
31
+ };
32
+ }
33
+
34
+ return { allowed: true, regulated };
35
+ }