@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
@@ -14,10 +14,22 @@ export const prerender = false;
14
14
 
15
15
  import { readViewerSession } from "@/lib/auth/route";
16
16
  import { membershipFor } from "@/lib/dashboard/tenantSelection";
17
+ import { readEnv } from "@/lib/env";
17
18
 
18
19
  const cspNonce = Astro.locals.cspNonce;
19
20
  const appDomain = decodeURIComponent(Astro.params.appDomain ?? "");
20
21
 
22
+ // qa-tier warn: derived from the SAME MCP origin the invite preflight uses
23
+ // (MCP_APP_BINDING_URL). We don't want people onboarding developers via qa now that
24
+ // prod+qa share data — so warn (non-blocking; qa is legitimate for testing).
25
+ let isQaTier = false;
26
+ try {
27
+ const bindingUrl = await readEnv("MCP_APP_BINDING_URL");
28
+ isQaTier = !!bindingUrl && new URL(bindingUrl).hostname.startsWith("mcp.qa.");
29
+ } catch {
30
+ isQaTier = false;
31
+ }
32
+
21
33
  const session = await readViewerSession(Astro);
22
34
  if (!session) {
23
35
  return Astro.redirect(
@@ -89,6 +101,9 @@ const backHref = `/dashboard/${encodeURIComponent(appDomain)}`;
89
101
  .msg.ok { background: #16311f; border-color: #2f6b41; color: #a7e0ba; }
90
102
  .msg.err { background: #3a1f22; border-color: #6b2f34; color: #f6b9b9; }
91
103
  }
104
+ .warn { margin: 0 0 1rem; font-size: .88rem; border-radius: 9px; padding: .6rem .8rem;
105
+ background: #fff7e6; border: 1px solid #f0d999; color: #7a5b12; }
106
+ @media (prefers-color-scheme: dark) { .warn { background: #332a12; border-color: #6b5a2f; color: #e8d39a; } }
92
107
  .muted { color: #9a9a9a; font-size: .85rem; }
93
108
  .who { margin-top: 2rem; font-size: .82rem; color: #8a8a8a; }
94
109
  .who a, a.back { color: inherit; }
@@ -105,6 +120,10 @@ const backHref = `/dashboard/${encodeURIComponent(appDomain)}`;
105
120
  : " Viewing as Token of Trust staff — invite by email to grant a developer access."}</p>
106
121
 
107
122
  <h2>Invite a developer</h2>
123
+ {isQaTier && (
124
+ <p class="warn">⚠ This invite connects the developer to the <strong>QA</strong> environment
125
+ — likely not what you want.</p>
126
+ )}
108
127
  <div class="card">
109
128
  <label for="email">Developer email</label>
110
129
  <input id="email" type="email" placeholder="dev@agency.com" autocomplete="off" />
@@ -14,7 +14,7 @@ import CollectionCard from "@/components/CollectionCard.astro";
14
14
  import Img from "@/components/Img.astro";
15
15
  import MarketingBlocks from "@/components/marketing/MarketingBlocks.astro";
16
16
  import { withBase } from "@/lib/basePath";
17
- import { rawHtmlResponse } from "@/lib/rawMarketingHtml";
17
+ import { rawHtmlPolicy, rawHtmlResponse } from "@/lib/rawMarketingHtml";
18
18
  import type { CatalogCollection, CatalogProduct } from "@tot/public-runtime";
19
19
  import {
20
20
  isMarketingBlock,
@@ -23,7 +23,12 @@ import {
23
23
  type NewsletterBlock,
24
24
  type PromoTilesBlock,
25
25
  type MarketingBlock,
26
+ type FeaturedProductsBlock,
26
27
  } from "@/lib/storyblok/content-model";
28
+ import {
29
+ normalizeProductSectionSource,
30
+ } from "@tot/public-runtime";
31
+ import { resolveSectionSource } from "@/lib/sections";
27
32
 
28
33
  const { tenant, tot, content, basePath, features } = Astro.locals;
29
34
  const marketing = !features.catalog;
@@ -37,22 +42,17 @@ const marketing = !features.catalog;
37
42
  // OpenGraph/JSON-LD come from the document's own <head>. The homepage `/` is in
38
43
  // the marketing sitemap (sitemap.xml.ts).
39
44
  //
40
- // Served regardless of siteType: a tenant that ships home.html gets its raw
41
- // marketing home whether or not commerce is on. This is the HYBRID tenant —
42
- // pixel-perfect marketing chrome at `/` surrounding a live commerce core at
43
- // /collections + /products/* (see playbook/site-migration/commerce-handoff.md §1
44
- // and product-boundaries.md). Commerce tenants with no home.html fall through to
45
- // the block-composed home below, unchanged. Mirrors the raw-subpage path in
46
- // [...slug].astro (getPageHtml is already siteType-agnostic).
47
- {
48
- const rawHtml = await content.getHomeHtml();
49
- if (rawHtml) {
50
- // Shared chrome: if the tenant ships a chrome.html wrapper AND this home is a
51
- // body-only fragment, rawHtmlResponse splices it in; a full document serves
52
- // verbatim (backward-compatible).
53
- const chromeWrapper = await content.getChromeHtml();
54
- return rawHtmlResponse(rawHtml, { nonce: Astro.locals.cspNonce, basePath, chromeWrapper });
55
- }
45
+ // Allowed only when rawHtmlPolicy says the tenant can safely bypass Layout.
46
+ // Regulated tenants fall through to the block-composed home so platform
47
+ // age/identity, warning, tax, and checkout guardrails remain non-removable.
48
+ // Mirrors the raw-subpage path in [...slug].astro.
49
+ const rawHtml = await content.getHomeHtml();
50
+ if (rawHtml && rawHtmlPolicy(tenant).allowed) {
51
+ // Shared chrome: if the tenant ships a chrome.html wrapper AND this home is a
52
+ // body-only fragment, rawHtmlResponse splices it in; a full document serves
53
+ // verbatim (backward-compatible).
54
+ const chromeWrapper = await content.getChromeHtml();
55
+ return rawHtmlResponse(rawHtml, { nonce: Astro.locals.cspNonce, basePath, chromeWrapper });
56
56
  }
57
57
 
58
58
  const home = await content.getHome();
@@ -75,21 +75,15 @@ type Resolved =
75
75
  | { kind: "newsletter"; block: NewsletterBlock };
76
76
 
77
77
  async function resolveProducts(
78
- source: "featured" | "newest",
78
+ source: FeaturedProductsBlock["source"],
79
79
  handles: string[] | undefined,
80
80
  limit: number,
81
81
  ): Promise<CatalogProduct[]> {
82
- if (handles?.length) {
83
- const all = await Promise.all(handles.map((h) => tot.getProductByHandle(h)));
84
- return all
85
- .filter((p): p is CatalogProduct => !!p && p.status === "active")
86
- .slice(0, limit);
87
- }
88
- if (source === "newest") {
89
- const { products } = await tot.listProducts({ sort: "newest", pageSize: limit });
90
- return products;
91
- }
92
- return tot.getFeatured(limit);
82
+ return resolveSectionSource(
83
+ normalizeProductSectionSource(source, handles),
84
+ tot,
85
+ limit,
86
+ );
93
87
  }
94
88
 
95
89
  const resolved: Resolved[] = [];
@@ -17,9 +17,15 @@ import NicotineWarning from "@/components/compliance/NicotineWarning.astro";
17
17
  import ReviewSummary from "@/components/commerce/ReviewSummary.astro";
18
18
  import ReviewList from "@/components/commerce/ReviewList.astro";
19
19
  import { isOnSale, isNew, isAvailable, fromPrice } from "@/lib/tot/query";
20
- import { buildVariantCartPayload } from "@tot/public-runtime";
21
- import type { VariantCartPayload } from "@tot/public-runtime";
22
- import { readCartSecret } from "@/lib/foxyCommerce";
20
+ import {
21
+ buildVariantCartPayload,
22
+ normalizePurchaseOptions,
23
+ } from "@tot/public-runtime";
24
+ import type {
25
+ ProductPurchaseOption,
26
+ VariantCartPayload,
27
+ } from "@tot/public-runtime";
28
+ import { readCartSecret } from "@/lib/checkoutCommerce";
23
29
  import { truncate } from "@/lib/format";
24
30
  import { productLd, breadcrumbLd, reviewLd } from "@/lib/jsonld";
25
31
  import { isUsableImageUrl } from "@/lib/placeholder";
@@ -138,29 +144,53 @@ const reviews = await tot.getReviews(product.handle);
138
144
  const onSale = isOnSale(product);
139
145
  const available = isAvailable(product);
140
146
 
141
- // FoxyCart add-to-cart (Track A). Pre-build each variant's cart payload server-
147
+ // Managed add-to-cart (Track A). Pre-build each variant's cart payload server-
142
148
  // side so the island can render a signed, tamper-proof form. In `signed` mode a
143
149
  // missing secret means we render NO cart (never a tamperable form); the
144
150
  // `unsigned-demo` mode needs no secret. Drafts still get a cart (preview links).
145
- let foxyCart:
146
- | { action: string; byVariantId: Record<string, VariantCartPayload> }
151
+ let checkoutCart:
152
+ | {
153
+ action: string;
154
+ purchaseOptions: ProductPurchaseOption[];
155
+ byVariantId: Record<string, Record<string, VariantCartPayload>>;
156
+ }
147
157
  | undefined;
148
158
  if (commerce) {
149
159
  const secret = await readCartSecret(commerce);
150
160
  if (commerce.mode !== "signed" || secret) {
151
- const entries = await Promise.all(
152
- product.variants.map(
153
- async (v) =>
154
- [
155
- v.id,
156
- await buildVariantCartPayload(product, v, {
157
- mode: commerce.mode,
158
- secret,
159
- }),
160
- ] as const,
161
- ),
162
- );
163
- foxyCart = { action: commerce.cartUrl, byVariantId: Object.fromEntries(entries) };
161
+ try {
162
+ const purchaseOptions = normalizePurchaseOptions(product);
163
+ const entries = await Promise.all(
164
+ product.variants.map(
165
+ async (v) => {
166
+ const payloads = await Promise.all(
167
+ purchaseOptions.map(
168
+ async (purchaseOption) =>
169
+ [
170
+ purchaseOption.id,
171
+ await buildVariantCartPayload(product, v, {
172
+ mode: commerce.mode,
173
+ secret,
174
+ purchaseOption,
175
+ }),
176
+ ] as const,
177
+ ),
178
+ );
179
+ return [v.id, Object.fromEntries(payloads)] as const;
180
+ },
181
+ ),
182
+ );
183
+ checkoutCart = {
184
+ action: commerce.cartUrl,
185
+ purchaseOptions,
186
+ byVariantId: Object.fromEntries(entries),
187
+ };
188
+ } catch (err) {
189
+ console.warn(
190
+ `[storefront] Invalid checkout purchase options for ${product.handle}; cart disabled for this product.`,
191
+ err,
192
+ );
193
+ }
164
194
  }
165
195
  }
166
196
 
@@ -268,7 +298,7 @@ const jsonLd = [productJsonLd, breadcrumbLd(crumbs, canonicalBase)];
268
298
  options={product.options}
269
299
  currency={product.price_range.currency}
270
300
  locale={tenant.locale}
271
- foxyCart={foxyCart}
301
+ checkoutCart={checkoutCart}
272
302
  />
273
303
  <button
274
304
  type="button"
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "1.3.2",
3
+ "version": "1.3.4-rc.2",
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",
7
7
  "engines": {
8
- "node": ">=24"
8
+ "node": ">=22.12.0"
9
9
  },
10
+ "workspaces": [
11
+ "apps/*",
12
+ "packages/*"
13
+ ],
10
14
  "scripts": {
11
15
  "dev": "pnpm --filter @tot/storefront-runner dev",
12
16
  "build": "pnpm --filter @tot/storefront-runner build",
@@ -1,10 +1,16 @@
1
1
  /**
2
2
  * Tenant capability switches — config that travels with the PR (Track A/B).
3
3
  *
4
- * Three independently-toggleable capabilities:
5
- * - cartCheckout — FoxyCart cart/checkout ("Powered by FoxyCart", Track A)
4
+ * Three capability states:
5
+ * - cartCheckout — managed cart/checkout (Track A)
6
6
  * - ageVerification — Token of Trust age/identity gate (21+ regulated)
7
- * - exciseTax — excise tax collection at checkout (Track B, tot-foxycart)
7
+ * - exciseTax — excise tax collection at checkout (Track B, commerce service)
8
+ *
9
+ * Important: these are configurable surfaces, not compliance obligations. A
10
+ * tenant repo may request that a surface turn off, but `resolveCapabilities`
11
+ * reapplies the compliance floor after every override. Developers can disable
12
+ * optional commerce affordances; they cannot disable required age/identity or
13
+ * excise-tax enforcement for regulated tenants.
8
14
  *
9
15
  * The source of truth is the tenant repo's `.tot/config.json` `capabilities`
10
16
  * block — so a change is a code change that travels with a PR. Resolution is
@@ -23,16 +29,28 @@ export interface CapabilityState {
23
29
  }
24
30
 
25
31
  export interface CapabilityConfig {
26
- /** FoxyCart cart/checkout: loader + signed add-to-cart. */
32
+ /** Managed cart/checkout: loader + signed add-to-cart. */
27
33
  cartCheckout: CapabilityState;
28
34
  /** Token of Trust age/identity verification (soft 21+ gate now; full ToT later). */
29
35
  ageVerification: CapabilityState;
30
- /** Excise tax collection at checkout (materialized in the FoxyCart cart). */
36
+ /** Excise tax collection at checkout (materialized in the managed cart). */
31
37
  exciseTax: CapabilityState;
32
38
  }
33
39
 
34
40
  export type CapabilityKey = keyof CapabilityConfig;
35
41
 
42
+ export interface CapabilityObligation {
43
+ required: boolean;
44
+ reason?: string;
45
+ }
46
+
47
+ export type CapabilityObligations = Record<CapabilityKey, CapabilityObligation>;
48
+
49
+ export interface CapabilityFloorViolation {
50
+ capability: CapabilityKey;
51
+ reason: string;
52
+ }
53
+
36
54
  /** Stable capability order + labels for any surface that lists them. */
37
55
  export const CAPABILITY_KEYS: CapabilityKey[] = [
38
56
  "cartCheckout",
@@ -49,6 +67,58 @@ export function capabilitiesArtifactPath(tenantId: string): string {
49
67
  return `tenants/${tenantId}/capabilities.json`;
50
68
  }
51
69
 
70
+ /**
71
+ * Resolve the non-negotiable compliance floor for a tenant. This is intentionally
72
+ * narrower than feature defaults: it says only what MUST remain on for the site
73
+ * to be publishable.
74
+ */
75
+ export function resolveComplianceObligations(
76
+ tenant: Pick<TenantConfig, "compliance" | "siteType">,
77
+ ): CapabilityObligations {
78
+ return {
79
+ cartCheckout: { required: false },
80
+ ageVerification: tenant.compliance?.minAge
81
+ ? {
82
+ required: true,
83
+ reason: `Tenant requires age/identity verification for ${tenant.compliance.minAge}+ access.`,
84
+ }
85
+ : { required: false },
86
+ exciseTax: tenant.compliance?.nicotineWarning
87
+ ? {
88
+ required: true,
89
+ reason:
90
+ "Tenant sells nicotine/vape products and must keep excise-tax collection enabled at checkout.",
91
+ }
92
+ : { required: false },
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Find attempted overrides that weaken the compliance floor. This is used by
98
+ * validators to fail a publish early; `resolveCapabilities` also enforces the
99
+ * same floor at runtime as defense in depth.
100
+ */
101
+ export function capabilityFloorViolations(
102
+ tenant: Pick<TenantConfig, "compliance" | "siteType">,
103
+ requested?: Partial<CapabilityConfig> | null,
104
+ ): CapabilityFloorViolation[] {
105
+ if (!requested) return [];
106
+ const obligations = resolveComplianceObligations(tenant);
107
+ const out: CapabilityFloorViolation[] = [];
108
+ for (const key of CAPABILITY_KEYS) {
109
+ if (
110
+ obligations[key].required &&
111
+ requested[key]?.enabled === false
112
+ ) {
113
+ out.push({
114
+ capability: key,
115
+ reason: obligations[key].reason ?? `${key} is required for this tenant.`,
116
+ });
117
+ }
118
+ }
119
+ return out;
120
+ }
121
+
52
122
  /**
53
123
  * Resolve a tenant's effective capabilities: build-time `TenantConfig.capabilities`
54
124
  * default (itself defaulted from siteType/compliance), then any per-environment
@@ -74,16 +144,34 @@ export function resolveCapabilities(
74
144
  enabled: c?.exciseTax?.enabled ?? !!tenant.compliance?.nicotineWarning,
75
145
  },
76
146
  };
77
- if (!override) return base;
147
+ const requested = override
148
+ ? {
149
+ cartCheckout: {
150
+ enabled: override.cartCheckout?.enabled ?? base.cartCheckout.enabled,
151
+ },
152
+ ageVerification: {
153
+ enabled:
154
+ override.ageVerification?.enabled ?? base.ageVerification.enabled,
155
+ },
156
+ exciseTax: {
157
+ enabled: override.exciseTax?.enabled ?? base.exciseTax.enabled,
158
+ },
159
+ }
160
+ : base;
161
+
162
+ const obligations = resolveComplianceObligations(tenant);
78
163
  return {
79
164
  cartCheckout: {
80
- enabled: override.cartCheckout?.enabled ?? base.cartCheckout.enabled,
165
+ enabled:
166
+ obligations.cartCheckout.required || requested.cartCheckout.enabled,
81
167
  },
82
168
  ageVerification: {
83
- enabled: override.ageVerification?.enabled ?? base.ageVerification.enabled,
169
+ enabled:
170
+ obligations.ageVerification.required ||
171
+ requested.ageVerification.enabled,
84
172
  },
85
173
  exciseTax: {
86
- enabled: override.exciseTax?.enabled ?? base.exciseTax.enabled,
174
+ enabled: obligations.exciseTax.required || requested.exciseTax.enabled,
87
175
  },
88
176
  };
89
177
  }
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Order READ DTO — the FROZEN v1 contract shape the storefront consumes from
3
- * tot-foxycart's `GET /commerce/orders[/:orderId]` (card 8585 / U1). Kept in
3
+ * the checkout order API's `GET /commerce/orders[/:orderId]` (card 8585 / U1). Kept in
4
4
  * `@tot/public-runtime` (types only, no secrets, edge-safe) so the server-side
5
5
  * order client (U2), the Orders admin route (U2), and the view-model mapping (U3)
6
6
  * all code against one source of truth.
7
7
  *
8
- * Authoritative spec: `docs/storefront-foxycart-contract.md` §9 (tot-foxycart).
8
+ * Authoritative API shape lives with the checkout order service; this file
9
+ * freezes the storefront-facing v1 DTO.
9
10
  *
10
11
  * READ-ONLY (scope-locks): no write/mutation shapes here. Two fields are
11
12
  * ENRICHED-later by U4 (the tot20 identity/age join) and are `null`/`[]` until
@@ -15,52 +16,52 @@
15
16
  */
16
17
 
17
18
  /** Payment state of an order. */
18
- export type FoxyOrderPayment = "authorized" | "captured" | "refunded";
19
+ export type CheckoutOrderPayment = "authorized" | "captured" | "refunded";
19
20
 
20
21
  /** Fulfillment / queue state of an order. */
21
- export type FoxyOrderFulfillment =
22
+ export type CheckoutOrderFulfillment =
22
23
  | "unfulfilled"
23
24
  | "ready_to_ship"
24
25
  | "hold_review_required"
25
26
  | "completed";
26
27
 
27
- export interface FoxyOrderCustomer {
28
+ export interface CheckoutOrderCustomer {
28
29
  /** ToT-core customer id, or null for a guest / unlinked order. */
29
30
  id: string | null;
30
31
  name: string;
31
32
  }
32
33
 
33
34
  /** A row in the orders list. All fields populated by U1 EXCEPT `verification`. */
34
- export interface FoxyOrderSummary {
35
+ export interface CheckoutOrderSummary {
35
36
  /** Order / transaction id (string). */
36
37
  id: string;
37
38
  /** ISO 8601 placed-at timestamp. */
38
39
  placedAt: string;
39
40
  currency: string;
40
- customer: FoxyOrderCustomer;
41
+ customer: CheckoutOrderCustomer;
41
42
  /** Human one-liner, e.g. "Vape Pen Starter Kit +1 more". */
42
43
  itemsSummary: string;
43
44
  itemCount: number;
44
45
  /** Order total in MAJOR currency units (e.g. dollars). */
45
46
  total: number;
46
- payment: FoxyOrderPayment;
47
- fulfillment: FoxyOrderFulfillment;
47
+ payment: CheckoutOrderPayment;
48
+ fulfillment: CheckoutOrderFulfillment;
48
49
  /** Why the order is held, or null. */
49
50
  blocker: string | null;
50
51
  /**
51
52
  * Identity/age verification summary — ENRICHED by U4 (tot20 join). `null` from
52
53
  * U1; consumers render risk as "pending" until it is filled.
53
54
  */
54
- verification: FoxyOrderVerification | null;
55
+ verification: CheckoutOrderVerification | null;
55
56
  }
56
57
 
57
58
  /** U4-filled verification summary (shape reserved; opaque to U1/U2/U3). */
58
- export interface FoxyOrderVerification {
59
+ export interface CheckoutOrderVerification {
59
60
  riskLevel?: string;
60
61
  [k: string]: unknown;
61
62
  }
62
63
 
63
- export interface FoxyOrderLineItem {
64
+ export interface CheckoutOrderLineItem {
64
65
  sku: string;
65
66
  name: string;
66
67
  quantity: number;
@@ -68,7 +69,7 @@ export interface FoxyOrderLineItem {
68
69
  unitPrice: number;
69
70
  }
70
71
 
71
- export interface FoxyOrderTotals {
72
+ export interface CheckoutOrderTotals {
72
73
  subtotal: number;
73
74
  shipping: number;
74
75
  estimatedTax: number;
@@ -76,7 +77,7 @@ export interface FoxyOrderTotals {
76
77
  total: number;
77
78
  }
78
79
 
79
- export interface FoxyOrderShippingAddress {
80
+ export interface CheckoutOrderShippingAddress {
80
81
  line1: string;
81
82
  line2: string;
82
83
  city: string;
@@ -87,21 +88,21 @@ export interface FoxyOrderShippingAddress {
87
88
  }
88
89
 
89
90
  /** U4-filled per-order verification detail; `readinessChecks`/`evidenceTimeline` empty from U1. */
90
- export interface FoxyOrderVerificationDetail {
91
+ export interface CheckoutOrderVerificationDetail {
91
92
  readinessChecks: unknown[];
92
93
  evidenceTimeline: unknown[];
93
94
  }
94
95
 
95
96
  /** A single order's full detail (summary + line items, totals, address, verification detail). */
96
- export interface FoxyOrderDetail extends FoxyOrderSummary {
97
+ export interface CheckoutOrderDetail extends CheckoutOrderSummary {
97
98
  /** The excise line (TOT_EXCISE_TAX) is NOT here — it surfaces in totals.taxesAndFees. */
98
- lineItems: FoxyOrderLineItem[];
99
- totals: FoxyOrderTotals;
100
- shippingAddress: FoxyOrderShippingAddress | null;
101
- verificationDetail: FoxyOrderVerificationDetail;
99
+ lineItems: CheckoutOrderLineItem[];
100
+ totals: CheckoutOrderTotals;
101
+ shippingAddress: CheckoutOrderShippingAddress | null;
102
+ verificationDetail: CheckoutOrderVerificationDetail;
102
103
  }
103
104
 
104
- export interface FoxyOrderPage {
105
+ export interface CheckoutOrderPage {
105
106
  limit: number;
106
107
  cursor: string | null;
107
108
  /** Opaque cursor for the next page, or null on the last page. */
@@ -111,18 +112,18 @@ export interface FoxyOrderPage {
111
112
  }
112
113
 
113
114
  /** `GET /commerce/orders` response envelope. */
114
- export interface FoxyOrderListResponse {
115
- orders: FoxyOrderSummary[];
116
- page: FoxyOrderPage;
115
+ export interface CheckoutOrderListResponse {
116
+ orders: CheckoutOrderSummary[];
117
+ page: CheckoutOrderPage;
117
118
  }
118
119
 
119
120
  /** `GET /commerce/orders/:orderId` response envelope. */
120
- export interface FoxyOrderDetailResponse {
121
- order: FoxyOrderDetail;
121
+ export interface CheckoutOrderDetailResponse {
122
+ order: CheckoutOrderDetail;
122
123
  }
123
124
 
124
125
  /** Optional list filters (all server-side; see contract §9). */
125
- export interface FoxyOrderListFilters {
126
+ export interface CheckoutOrderListFilters {
126
127
  queue?: string;
127
128
  status?: string;
128
129
  /** ISO datetime lower bound. */
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Signed storefront → FoxyCart theme-publish client (U11b, the PUSH half of U11).
2
+ * Signed storefront → checkout theme-publish client (U11b, the PUSH half of U11).
3
3
  *
4
- * Points a store's Foxy checkout template at the storefront-hosted stylesheet
5
- * (`GET /api/checkout-style.css`, U11) by driving tot-foxycart's `/theme/foxy`
4
+ * Points a store's hosted checkout template at the storefront-hosted stylesheet
5
+ * (`GET /api/checkout-style.css`, U11) by driving the commerce service's theme
6
6
  * endpoint: PUT the `content_url`, then POST `/refresh` (PUT alone only sets the
7
- * pointer — Foxy caches the template, so nothing goes live until refresh).
7
+ * pointer — checkout caches the template, so nothing goes live until refresh).
8
8
  *
9
9
  * AUTH: each request carries an `x-tot-signature` header = lowercase-hex
10
10
  * HMAC-SHA256 of `JSON.stringify(body)` keyed by the STORE'S ToT SECRET KEY —
11
- * byte-identical to tot-foxycart's `foxyStore.verifyWebhookSignature`. That
11
+ * byte-identical to the commerce service's webhook-signature verifier. That
12
12
  * secret is the storefront's `TOT_SECRET_KEY`; this is independent of the
13
- * FoxyCart hAPI credentials (see decision `u11b-not-blocked-by-u7`).
13
+ * provider API credentials (see decision `u11b-not-blocked-by-u7`).
14
14
  *
15
15
  * Edge-safe + node-free (WebCrypto HMAC); the network call is injected so the
16
16
  * flow is fully unit-testable offline. The caller (the checkout-styling promote
@@ -19,10 +19,10 @@
19
19
  */
20
20
  import type { CustomizationEnv } from "./customization-preview.js";
21
21
 
22
- /** Header tot-foxycart reads the HMAC from (matches foxyThemeEndpoint SIGNATURE_HEADER). */
22
+ /** Header the commerce service reads the HMAC from. */
23
23
  export const TOT_SIGNATURE_HEADER = "x-tot-signature";
24
24
 
25
- /** Foxy caps content_url at 300 chars; '' detaches the template (revert to default). */
25
+ /** The provider caps content_url at 300 chars; '' detaches the template. */
26
26
  export const MAX_CONTENT_URL = 300;
27
27
 
28
28
  /** Fetch shape we depend on — the platform `fetch`, injectable for tests. */
@@ -36,15 +36,15 @@ export type FetchLike = (
36
36
  ) => Promise<{ ok: boolean; status: number; text: () => Promise<string> }>;
37
37
 
38
38
  export interface ThemePublishTarget {
39
- /** tot-foxycart proxy base, e.g. `https://foxycart-test.tokenoftrust.com`. */
40
- foxyBaseUrl: string;
41
- /** The store's app domain / subdomain (tot-foxycart resolves the store from it). */
39
+ /** Commerce service base URL for the checkout proxy. */
40
+ checkoutServiceBaseUrl: string;
41
+ /** The store's app domain / subdomain (the service resolves the store from it). */
42
42
  appDomain: string;
43
- /** The hosted stylesheet URL Foxy will fetch (U11's `/api/checkout-style.css`). */
43
+ /** The hosted stylesheet URL checkout will fetch (U11's `/api/checkout-style.css`). */
44
44
  contentUrl: string;
45
45
  /** The store's ToT secret key (storefront `TOT_SECRET_KEY`) — signs the request. */
46
46
  secret: string;
47
- /** Foxy template to target; checkout styling uses `checkout` (the default). */
47
+ /** Checkout template to target; checkout styling uses `checkout` (the default). */
48
48
  templateType?: string;
49
49
  }
50
50
 
@@ -58,7 +58,7 @@ export interface ThemePublishResult {
58
58
  error?: string;
59
59
  }
60
60
 
61
- /** Lowercase-hex HMAC-SHA256(message) under `secret` (WebCrypto). Matches foxy.ts. */
61
+ /** Lowercase-hex HMAC-SHA256(message) under `secret` (WebCrypto). */
62
62
  async function hmacHex(secret: string, message: string): Promise<string> {
63
63
  const enc = new TextEncoder();
64
64
  const key = await crypto.subtle.importKey(
@@ -75,8 +75,8 @@ async function hmacHex(secret: string, message: string): Promise<string> {
75
75
  }
76
76
 
77
77
  /**
78
- * The exact signature tot-foxycart verifies: hex HMAC-SHA256 of the JSON body
79
- * string under the store secret. Exported so the promote hook and tests can
78
+ * The exact commerce-service signature: hex HMAC-SHA256 of the JSON body string
79
+ * under the store secret. Exported so the promote hook and tests can
80
80
  * compute/assert it identically.
81
81
  */
82
82
  export function signThemeRequestBody(
@@ -88,9 +88,9 @@ export function signThemeRequestBody(
88
88
 
89
89
  /**
90
90
  * The canonical, stable content_url a store's checkout template points at — the
91
- * U11 route on the store's own host, so Foxy's server-side fetch self-identifies
92
- * the tenant + env. Returns null if it would exceed Foxy's 300-char cap (so the
93
- * caller surfaces a config error rather than pushing a URL Foxy will reject).
91
+ * U11 route on the store's own host, so the server-side fetch self-identifies
92
+ * the tenant + env. Returns null if it would exceed the provider's 300-char cap
93
+ * so the caller surfaces a config error before pushing a URL checkout will reject.
94
94
  */
95
95
  export function checkoutStyleContentUrl(
96
96
  host: string,
@@ -114,7 +114,7 @@ async function readErr(
114
114
  }
115
115
 
116
116
  /**
117
- * Publish a store's checkout styling to Foxy: signed PUT `/theme/foxy` then
117
+ * Publish a store's checkout styling: signed PUT `/theme/foxy` then
118
118
  * signed POST `/theme/foxy/refresh`. Returns a discriminated result — we do NOT
119
119
  * fail open: if the PUT fails we skip the refresh and report the error, so the
120
120
  * caller never reports "published" for something that didn't take.
@@ -127,7 +127,7 @@ export async function publishCheckoutTheme(
127
127
  ): Promise<ThemePublishResult> {
128
128
  const doFetch = fetchImpl ?? (globalThis.fetch as unknown as FetchLike);
129
129
  const templateType = target.templateType ?? "checkout";
130
- const base = target.foxyBaseUrl.replace(/\/+$/, "");
130
+ const base = target.checkoutServiceBaseUrl.replace(/\/+$/, "");
131
131
 
132
132
  if (
133
133
  target.contentUrl !== "" &&
@@ -188,11 +188,11 @@ export async function publishCheckoutTheme(
188
188
  }
189
189
 
190
190
  /**
191
- * Convenience: the env → Foxy-proxy base URL for this workstream's targets.
192
- * qa runs NODE_ENV=test (proxy `foxycart-test`); sandbox `foxycart-sandbox`;
193
- * live → the prod proxy. Kept here so the promote hook (U13) resolves one place.
191
+ * Convenience: the env → checkout service base URL for this workstream's targets.
192
+ * qa/test use the non-prod proxy; live uses the prod proxy. Kept here so the
193
+ * promote hook (U13) resolves one place.
194
194
  */
195
- export function foxyProxyBaseUrl(env: CustomizationEnv): string {
195
+ export function checkoutServiceBaseUrl(env: CustomizationEnv): string {
196
196
  // "test" is the qa target's env; "live" is prod.
197
197
  return env === "live"
198
198
  ? "https://foxycart.tokenoftrust.com"