@tokenoftrust/storefront-runner 1.3.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 (310) hide show
  1. package/LICENSE +58 -0
  2. package/README.md +42 -0
  3. package/apps/storefront/astro.config.mjs +66 -0
  4. package/apps/storefront/dev-plugins/dev-loop-state.mjs +147 -0
  5. package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +238 -0
  6. package/apps/storefront/env.d.ts +162 -0
  7. package/apps/storefront/migrations/0000_baseline.sql +54 -0
  8. package/apps/storefront/migrations/0001_products_fts.sql +9 -0
  9. package/apps/storefront/migrations/README.md +54 -0
  10. package/apps/storefront/migrations/meta/0000_snapshot.json +347 -0
  11. package/apps/storefront/migrations/meta/0001_snapshot.json +347 -0
  12. package/apps/storefront/migrations/meta/_journal.json +20 -0
  13. package/apps/storefront/package.json +42 -0
  14. package/apps/storefront/public/brand/northwind-logo-dark.svg +4 -0
  15. package/apps/storefront/public/brand/northwind-logo-light.svg +4 -0
  16. package/apps/storefront/public/brand/northwind-og.svg +13 -0
  17. package/apps/storefront/public/favicon.svg +5 -0
  18. package/apps/storefront/public/js/dashboard-team.js +147 -0
  19. package/apps/storefront/public/js/dashboard-vendor-search.js +115 -0
  20. package/apps/storefront/public/shared/commerce-chrome.css +369 -0
  21. package/apps/storefront/src/components/Badge.astro +34 -0
  22. package/apps/storefront/src/components/Breadcrumbs.astro +45 -0
  23. package/apps/storefront/src/components/CollectionCard.astro +56 -0
  24. package/apps/storefront/src/components/EmptyState.astro +55 -0
  25. package/apps/storefront/src/components/Img.astro +68 -0
  26. package/apps/storefront/src/components/ProductCard.astro +228 -0
  27. package/apps/storefront/src/components/Section.astro +39 -0
  28. package/apps/storefront/src/components/Seo.astro +70 -0
  29. package/apps/storefront/src/components/WidgetFrame.astro +34 -0
  30. package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +531 -0
  31. package/apps/storefront/src/components/admin/AdminCustomersTab.astro +597 -0
  32. package/apps/storefront/src/components/admin/AdminFoxyCartSettingsPanel.astro +227 -0
  33. package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +465 -0
  34. package/apps/storefront/src/components/admin/AdminProductsTab.astro +901 -0
  35. package/apps/storefront/src/components/admin/AdminReportingTab.astro +435 -0
  36. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +782 -0
  37. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +1223 -0
  38. package/apps/storefront/src/components/auth/AuthBadge.astro +44 -0
  39. package/apps/storefront/src/components/chrome/AnnouncementBar.astro +108 -0
  40. package/apps/storefront/src/components/chrome/MegaMenu.astro +26 -0
  41. package/apps/storefront/src/components/chrome/NavDropdown.astro +201 -0
  42. package/apps/storefront/src/components/chrome/SiteFooter.astro +201 -0
  43. package/apps/storefront/src/components/chrome/SiteHeader.astro +234 -0
  44. package/apps/storefront/src/components/chrome/UtilityNav.astro +79 -0
  45. package/apps/storefront/src/components/commerce/ArticleCard.astro +242 -0
  46. package/apps/storefront/src/components/commerce/BrandGrid.astro +175 -0
  47. package/apps/storefront/src/components/commerce/FoxyLoader.astro +23 -0
  48. package/apps/storefront/src/components/commerce/FoxySidecartStyles.astro +61 -0
  49. package/apps/storefront/src/components/commerce/PriceDisplay.astro +91 -0
  50. package/apps/storefront/src/components/commerce/ProductCarousel.astro +197 -0
  51. package/apps/storefront/src/components/commerce/ProductGrid.astro +75 -0
  52. package/apps/storefront/src/components/commerce/RatingStars.astro +58 -0
  53. package/apps/storefront/src/components/commerce/ReviewList.astro +54 -0
  54. package/apps/storefront/src/components/commerce/ReviewSummary.astro +44 -0
  55. package/apps/storefront/src/components/commerce/TagFilterCloud.astro +149 -0
  56. package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +37 -0
  57. package/apps/storefront/src/components/compliance/ComplianceNotice.astro +99 -0
  58. package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +45 -0
  59. package/apps/storefront/src/components/compliance/NicotineWarning.astro +33 -0
  60. package/apps/storefront/src/components/compliance/PactActNotice.astro +35 -0
  61. package/apps/storefront/src/components/compliance/Prop65Warning.astro +80 -0
  62. package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +38 -0
  63. package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +35 -0
  64. package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +53 -0
  65. package/apps/storefront/src/components/home/Hero.astro +164 -0
  66. package/apps/storefront/src/components/islands/AgeGate.tsx +109 -0
  67. package/apps/storefront/src/components/islands/ImageGallery.tsx +237 -0
  68. package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +73 -0
  69. package/apps/storefront/src/components/islands/MobileNav.tsx +239 -0
  70. package/apps/storefront/src/components/islands/QuickView.tsx +138 -0
  71. package/apps/storefront/src/components/islands/RecentlyViewed.tsx +122 -0
  72. package/apps/storefront/src/components/islands/SavedList.tsx +99 -0
  73. package/apps/storefront/src/components/islands/SearchTypeahead.tsx +248 -0
  74. package/apps/storefront/src/components/islands/VariantSelector.tsx +375 -0
  75. package/apps/storefront/src/components/marketing/CardGrid.astro +72 -0
  76. package/apps/storefront/src/components/marketing/CodeSample.astro +20 -0
  77. package/apps/storefront/src/components/marketing/CtaBand.astro +24 -0
  78. package/apps/storefront/src/components/marketing/Faq.astro +37 -0
  79. package/apps/storefront/src/components/marketing/Integrations.astro +63 -0
  80. package/apps/storefront/src/components/marketing/MarketingBlocks.astro +53 -0
  81. package/apps/storefront/src/components/marketing/MarketingCtas.astro +48 -0
  82. package/apps/storefront/src/components/marketing/MarketingHero.astro +72 -0
  83. package/apps/storefront/src/components/marketing/ProofStrip.astro +37 -0
  84. package/apps/storefront/src/components/marketing/SplitCompare.astro +53 -0
  85. package/apps/storefront/src/components/marketing/Steps.astro +36 -0
  86. package/apps/storefront/src/components/marketing/Testimonials.astro +38 -0
  87. package/apps/storefront/src/components/marketing/TrustBar.astro +22 -0
  88. package/apps/storefront/src/components/membership/IllustrativeLabel.astro +56 -0
  89. package/apps/storefront/src/components/membership/TierCards.astro +164 -0
  90. package/apps/storefront/src/components/membership/TierComparisonTable.astro +148 -0
  91. package/apps/storefront/src/components/plp/ActiveFilters.astro +79 -0
  92. package/apps/storefront/src/components/plp/FacetSidebar.astro +199 -0
  93. package/apps/storefront/src/components/plp/Pagination.astro +99 -0
  94. package/apps/storefront/src/components/plp/SortSelect.astro +64 -0
  95. package/apps/storefront/src/components/style-guide/StyleGuideNav.astro +95 -0
  96. package/apps/storefront/src/components/subscription/HowItWorksSteps.astro +67 -0
  97. package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +67 -0
  98. package/apps/storefront/src/components/subscription/SavingsExplainer.astro +101 -0
  99. package/apps/storefront/src/config/adminTargetTenant.ts +28 -0
  100. package/apps/storefront/src/config/devTenantSeed.ts +78 -0
  101. package/apps/storefront/src/config/devTenants.ts +16 -0
  102. package/apps/storefront/src/config/kvTenantResolver.ts +87 -0
  103. package/apps/storefront/src/config/resolver.ts +102 -0
  104. package/apps/storefront/src/config/tenants.ts +87 -0
  105. package/apps/storefront/src/layouts/Layout.astro +368 -0
  106. package/apps/storefront/src/lib/admin/README.md +122 -0
  107. package/apps/storefront/src/lib/admin/ai/approvalMode.ts +21 -0
  108. package/apps/storefront/src/lib/admin/ai/askEnvelope.ts +48 -0
  109. package/apps/storefront/src/lib/admin/ai/audit.ts +14 -0
  110. package/apps/storefront/src/lib/admin/ai/index.ts +6 -0
  111. package/apps/storefront/src/lib/admin/ai/operationalRuns.ts +155 -0
  112. package/apps/storefront/src/lib/admin/ai/recommendation.ts +20 -0
  113. package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +172 -0
  114. package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +65 -0
  115. package/apps/storefront/src/lib/admin/envelope.ts +10 -0
  116. package/apps/storefront/src/lib/admin/evidence/evidenceReference.ts +32 -0
  117. package/apps/storefront/src/lib/admin/memoizeByEnvelope.ts +27 -0
  118. package/apps/storefront/src/lib/admin/orderClient.ts +146 -0
  119. package/apps/storefront/src/lib/admin/seed.ts +54 -0
  120. package/apps/storefront/src/lib/admin/services/mock/aiWorkflows.ts +59 -0
  121. package/apps/storefront/src/lib/admin/services/mock/customers.ts +86 -0
  122. package/apps/storefront/src/lib/admin/services/mock/evidence.ts +99 -0
  123. package/apps/storefront/src/lib/admin/services/mock/foxyCartSettings.ts +78 -0
  124. package/apps/storefront/src/lib/admin/services/mock/fulfillment.ts +62 -0
  125. package/apps/storefront/src/lib/admin/services/mock/index.ts +12 -0
  126. package/apps/storefront/src/lib/admin/services/mock/orders.ts +184 -0
  127. package/apps/storefront/src/lib/admin/services/mock/products.ts +105 -0
  128. package/apps/storefront/src/lib/admin/services/mock/reporting.ts +51 -0
  129. package/apps/storefront/src/lib/admin/services/mock/settings.ts +70 -0
  130. package/apps/storefront/src/lib/admin/services/mock/tenants.ts +29 -0
  131. package/apps/storefront/src/lib/admin/services/orders.ts +300 -0
  132. package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +67 -0
  133. package/apps/storefront/src/lib/admin/viewmodels/customers.ts +56 -0
  134. package/apps/storefront/src/lib/admin/viewmodels/fulfillment.ts +49 -0
  135. package/apps/storefront/src/lib/admin/viewmodels/orders.ts +92 -0
  136. package/apps/storefront/src/lib/admin/viewmodels/products.ts +74 -0
  137. package/apps/storefront/src/lib/admin/viewmodels/reporting.ts +34 -0
  138. package/apps/storefront/src/lib/admin/viewmodels/settings.ts +64 -0
  139. package/apps/storefront/src/lib/adminTenantEnvelope.ts +90 -0
  140. package/apps/storefront/src/lib/analytics/index.ts +47 -0
  141. package/apps/storefront/src/lib/auth/brokerAssertion.ts +136 -0
  142. package/apps/storefront/src/lib/auth/devAuth.ts +90 -0
  143. package/apps/storefront/src/lib/auth/gatePage.ts +81 -0
  144. package/apps/storefront/src/lib/auth/identityToken.ts +341 -0
  145. package/apps/storefront/src/lib/auth/loginGate.ts +119 -0
  146. package/apps/storefront/src/lib/auth/route.ts +146 -0
  147. package/apps/storefront/src/lib/auth/session.ts +203 -0
  148. package/apps/storefront/src/lib/auth/totAccessClient.ts +503 -0
  149. package/apps/storefront/src/lib/basePath.ts +41 -0
  150. package/apps/storefront/src/lib/cfImage.ts +55 -0
  151. package/apps/storefront/src/lib/chrome/model.ts +244 -0
  152. package/apps/storefront/src/lib/chrome/parseConfig.ts +34 -0
  153. package/apps/storefront/src/lib/colors.ts +97 -0
  154. package/apps/storefront/src/lib/commerce/foxyCartSettings.ts +253 -0
  155. package/apps/storefront/src/lib/content-edit/applyDiff.ts +91 -0
  156. package/apps/storefront/src/lib/content-edit/client.ts +230 -0
  157. package/apps/storefront/src/lib/content-edit/index.ts +14 -0
  158. package/apps/storefront/src/lib/content-edit/route.ts +25 -0
  159. package/apps/storefront/src/lib/content-edit/types.ts +19 -0
  160. package/apps/storefront/src/lib/cspStaticHashes.ts +24 -0
  161. package/apps/storefront/src/lib/d1/catalog.ts +366 -0
  162. package/apps/storefront/src/lib/dashboard/staffAdmission.ts +43 -0
  163. package/apps/storefront/src/lib/dashboard/staffVendorAccess.ts +449 -0
  164. package/apps/storefront/src/lib/dashboard/tenantSelection.ts +136 -0
  165. package/apps/storefront/src/lib/dashboard/vendorQuery.ts +61 -0
  166. package/apps/storefront/src/lib/demoGate.ts +113 -0
  167. package/apps/storefront/src/lib/dev/activityIngestToken.ts +78 -0
  168. package/apps/storefront/src/lib/dev/activityStore.ts +72 -0
  169. package/apps/storefront/src/lib/dev/ai-widget-client.js +308 -0
  170. package/apps/storefront/src/lib/dev/apiBase.ts +14 -0
  171. package/apps/storefront/src/lib/dev/cliSignInCode.ts +155 -0
  172. package/apps/storefront/src/lib/dev/devDraft.ts +133 -0
  173. package/apps/storefront/src/lib/dev/hostedCockpit.ts +169 -0
  174. package/apps/storefront/src/lib/dev/livePreview.ts +77 -0
  175. package/apps/storefront/src/lib/dev/runtimeStore.ts +120 -0
  176. package/apps/storefront/src/lib/dev/storeStatus.ts +142 -0
  177. package/apps/storefront/src/lib/diag/redact.ts +20 -0
  178. package/apps/storefront/src/lib/edgeCache.ts +128 -0
  179. package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +62 -0
  180. package/apps/storefront/src/lib/env.ts +60 -0
  181. package/apps/storefront/src/lib/fixtures/sampleProducts.ts +130 -0
  182. package/apps/storefront/src/lib/format.ts +38 -0
  183. package/apps/storefront/src/lib/foxyCommerce.ts +49 -0
  184. package/apps/storefront/src/lib/imageAttrs.ts +55 -0
  185. package/apps/storefront/src/lib/jsonld.ts +166 -0
  186. package/apps/storefront/src/lib/maintenance.ts +71 -0
  187. package/apps/storefront/src/lib/membership/model.ts +97 -0
  188. package/apps/storefront/src/lib/memoryKv.ts +32 -0
  189. package/apps/storefront/src/lib/messaging/messagesClient.ts +143 -0
  190. package/apps/storefront/src/lib/placeholder.ts +99 -0
  191. package/apps/storefront/src/lib/previewReload.ts +125 -0
  192. package/apps/storefront/src/lib/priceBuckets.ts +33 -0
  193. package/apps/storefront/src/lib/rawChrome.ts +505 -0
  194. package/apps/storefront/src/lib/rawMarketingHtml.ts +136 -0
  195. package/apps/storefront/src/lib/search/index.ts +74 -0
  196. package/apps/storefront/src/lib/sections.ts +129 -0
  197. package/apps/storefront/src/lib/securityHeaders.ts +31 -0
  198. package/apps/storefront/src/lib/storyblok/content-model.ts +289 -0
  199. package/apps/storefront/src/lib/storyblok/provider.ts +607 -0
  200. package/apps/storefront/src/lib/subscription/model.ts +68 -0
  201. package/apps/storefront/src/lib/tenantRouting.ts +91 -0
  202. package/apps/storefront/src/lib/tot/ToTClient.ts +177 -0
  203. package/apps/storefront/src/lib/tot/d1Client.ts +62 -0
  204. package/apps/storefront/src/lib/tot/query.ts +249 -0
  205. package/apps/storefront/src/lib/tot/totClientInterface.ts +30 -0
  206. package/apps/storefront/src/lib/url.ts +142 -0
  207. package/apps/storefront/src/lib/wishlist.ts +63 -0
  208. package/apps/storefront/src/middleware/index.ts +455 -0
  209. package/apps/storefront/src/pages/404.astro +61 -0
  210. package/apps/storefront/src/pages/[...slug].astro +90 -0
  211. package/apps/storefront/src/pages/admin.astro +1675 -0
  212. package/apps/storefront/src/pages/api/admin/orders/[id].ts +53 -0
  213. package/apps/storefront/src/pages/api/admin/orders.ts +71 -0
  214. package/apps/storefront/src/pages/api/auth/logout.ts +21 -0
  215. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +107 -0
  216. package/apps/storefront/src/pages/api/auth/send.ts +85 -0
  217. package/apps/storefront/src/pages/api/auth/verify.ts +135 -0
  218. package/apps/storefront/src/pages/api/cache-purge.ts +44 -0
  219. package/apps/storefront/src/pages/api/checkout-style.css.ts +43 -0
  220. package/apps/storefront/src/pages/api/cli/policy.ts +70 -0
  221. package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +164 -0
  222. package/apps/storefront/src/pages/api/csp-report.ts +25 -0
  223. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +124 -0
  224. package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +120 -0
  225. package/apps/storefront/src/pages/api/dev/activity.ts +124 -0
  226. package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +73 -0
  227. package/apps/storefront/src/pages/api/dev/diff.ts +68 -0
  228. package/apps/storefront/src/pages/api/dev/file.ts +57 -0
  229. package/apps/storefront/src/pages/api/dev/locate.ts +125 -0
  230. package/apps/storefront/src/pages/api/dev/publish.ts +128 -0
  231. package/apps/storefront/src/pages/api/dev/tree.ts +70 -0
  232. package/apps/storefront/src/pages/api/dev/write.ts +116 -0
  233. package/apps/storefront/src/pages/api/newsletter.ts +104 -0
  234. package/apps/storefront/src/pages/api/request-access.ts +172 -0
  235. package/apps/storefront/src/pages/auth/login.astro +149 -0
  236. package/apps/storefront/src/pages/auth/magic.astro +105 -0
  237. package/apps/storefront/src/pages/capabilities.astro +292 -0
  238. package/apps/storefront/src/pages/collections/[handle].astro +177 -0
  239. package/apps/storefront/src/pages/collections/index.astro +59 -0
  240. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +100 -0
  241. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +154 -0
  242. package/apps/storefront/src/pages/dashboard/index.astro +160 -0
  243. package/apps/storefront/src/pages/index.astro +290 -0
  244. package/apps/storefront/src/pages/llms.txt.ts +92 -0
  245. package/apps/storefront/src/pages/products/[handle].astro +402 -0
  246. package/apps/storefront/src/pages/robots.txt.ts +24 -0
  247. package/apps/storefront/src/pages/saved.astro +35 -0
  248. package/apps/storefront/src/pages/search.astro +150 -0
  249. package/apps/storefront/src/pages/sitemap.xml.ts +78 -0
  250. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +1280 -0
  251. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +229 -0
  252. package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +144 -0
  253. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +116 -0
  254. package/apps/storefront/src/pages/style-guide/index.astro +98 -0
  255. package/apps/storefront/src/pages/tenants/[id]/[...path].ts +57 -0
  256. package/apps/storefront/src/styles/global.css +292 -0
  257. package/apps/storefront/src/themes/reference.ts +68 -0
  258. package/apps/storefront/src/themes/schema.ts +111 -0
  259. package/apps/storefront/src/themes/tenants/index.ts +51 -0
  260. package/apps/storefront/tsconfig.json +16 -0
  261. package/apps/storefront/vitest.config.ts +22 -0
  262. package/package.json +26 -0
  263. package/packages/public-runtime/LICENSE +58 -0
  264. package/packages/public-runtime/package.json +22 -0
  265. package/packages/public-runtime/src/binary-path.ts +68 -0
  266. package/packages/public-runtime/src/capabilities.ts +89 -0
  267. package/packages/public-runtime/src/catalog-api.ts +81 -0
  268. package/packages/public-runtime/src/catalog-d1.ts +192 -0
  269. package/packages/public-runtime/src/checkout-style-publish.ts +200 -0
  270. package/packages/public-runtime/src/checkout-style.ts +375 -0
  271. package/packages/public-runtime/src/csp.ts +109 -0
  272. package/packages/public-runtime/src/customization-preview.ts +197 -0
  273. package/packages/public-runtime/src/customization-reconcile.ts +63 -0
  274. package/packages/public-runtime/src/customization-runtime.ts +157 -0
  275. package/packages/public-runtime/src/customization-scripts.ts +64 -0
  276. package/packages/public-runtime/src/customization-versioning.ts +95 -0
  277. package/packages/public-runtime/src/foxy-orders.ts +136 -0
  278. package/packages/public-runtime/src/foxy.ts +196 -0
  279. package/packages/public-runtime/src/hot-standby-readiness.ts +140 -0
  280. package/packages/public-runtime/src/index.ts +34 -0
  281. package/packages/public-runtime/src/product.ts +159 -0
  282. package/packages/public-runtime/src/search.ts +45 -0
  283. package/packages/public-runtime/src/tenant-assets.ts +444 -0
  284. package/packages/public-runtime/src/tenant.ts +384 -0
  285. package/packages/public-runtime/tsconfig.json +8 -0
  286. package/packages/public-runtime/vitest.config.ts +8 -0
  287. package/pnpm-workspace.yaml +7 -0
  288. package/scripts/build/copy-tenant-assets.mjs +69 -0
  289. package/scripts/dev/ai-edit.mjs +183 -0
  290. package/scripts/dev/checkout-watch.mjs +116 -0
  291. package/scripts/dev/file-browser.mjs +159 -0
  292. package/scripts/dev/file-writer.mjs +141 -0
  293. package/scripts/dev/git-status.mjs +171 -0
  294. package/scripts/dev/locate-handler.mjs +163 -0
  295. package/scripts/dev/locate.mjs +112 -0
  296. package/scripts/dev/port-check.mjs +48 -0
  297. package/scripts/dev/safe-path.mjs +109 -0
  298. package/scripts/dev/shot.mjs +46 -0
  299. package/scripts/dev/tenant-source-root.mjs +76 -0
  300. package/scripts/dev/unified-diff.mjs +81 -0
  301. package/scripts/tot-dev.mjs +376 -0
  302. package/tenants/README.md +14 -0
  303. package/tenants/home/content/chrome.json +7 -0
  304. package/tenants/home/content/home.html +58 -0
  305. package/tenants/home/content/home.json +11 -0
  306. package/tenants/home/public/js/storefront.js +210 -0
  307. package/tenants/home/public/logo-wordmark.png +0 -0
  308. package/tenants/home/public/pages/storefront.css +212 -0
  309. package/tenants/home/theme.json +6 -0
  310. package/tsconfig.base.json +20 -0
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Signed storefront → FoxyCart theme-publish client (U11b, the PUSH half of U11).
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`
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).
8
+ *
9
+ * AUTH: each request carries an `x-tot-signature` header = lowercase-hex
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
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`).
14
+ *
15
+ * Edge-safe + node-free (WebCrypto HMAC); the network call is injected so the
16
+ * flow is fully unit-testable offline. The caller (the checkout-styling promote
17
+ * hook — U13) supplies the resolved secret + base URL from server context; the
18
+ * secret is never shipped to the client.
19
+ */
20
+ import type { CustomizationEnv } from "./customization-preview.js";
21
+
22
+ /** Header tot-foxycart reads the HMAC from (matches foxyThemeEndpoint SIGNATURE_HEADER). */
23
+ export const TOT_SIGNATURE_HEADER = "x-tot-signature";
24
+
25
+ /** Foxy caps content_url at 300 chars; '' detaches the template (revert to default). */
26
+ export const MAX_CONTENT_URL = 300;
27
+
28
+ /** Fetch shape we depend on — the platform `fetch`, injectable for tests. */
29
+ export type FetchLike = (
30
+ input: string,
31
+ init?: {
32
+ method?: string;
33
+ headers?: Record<string, string>;
34
+ body?: string;
35
+ },
36
+ ) => Promise<{ ok: boolean; status: number; text: () => Promise<string> }>;
37
+
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). */
42
+ appDomain: string;
43
+ /** The hosted stylesheet URL Foxy will fetch (U11's `/api/checkout-style.css`). */
44
+ contentUrl: string;
45
+ /** The store's ToT secret key (storefront `TOT_SECRET_KEY`) — signs the request. */
46
+ secret: string;
47
+ /** Foxy template to target; checkout styling uses `checkout` (the default). */
48
+ templateType?: string;
49
+ }
50
+
51
+ export interface ThemePublishResult {
52
+ ok: boolean;
53
+ /** Parsed PUT response (pointer set), when it succeeded. */
54
+ set?: unknown;
55
+ /** Parsed refresh response (cache re-pulled), when it succeeded. */
56
+ refreshed?: boolean;
57
+ /** Failure detail: which step + the endpoint's status/message. */
58
+ error?: string;
59
+ }
60
+
61
+ /** Lowercase-hex HMAC-SHA256(message) under `secret` (WebCrypto). Matches foxy.ts. */
62
+ async function hmacHex(secret: string, message: string): Promise<string> {
63
+ const enc = new TextEncoder();
64
+ const key = await crypto.subtle.importKey(
65
+ "raw",
66
+ enc.encode(secret),
67
+ { name: "HMAC", hash: "SHA-256" },
68
+ false,
69
+ ["sign"],
70
+ );
71
+ const sig = await crypto.subtle.sign("HMAC", key, enc.encode(message));
72
+ return [...new Uint8Array(sig)]
73
+ .map((b) => b.toString(16).padStart(2, "0"))
74
+ .join("");
75
+ }
76
+
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
80
+ * compute/assert it identically.
81
+ */
82
+ export function signThemeRequestBody(
83
+ secret: string,
84
+ body: unknown,
85
+ ): Promise<string> {
86
+ return hmacHex(secret, JSON.stringify(body));
87
+ }
88
+
89
+ /**
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).
94
+ */
95
+ export function checkoutStyleContentUrl(
96
+ host: string,
97
+ basePath = "",
98
+ ): string | null {
99
+ const url = `https://${host}${basePath}/api/checkout-style.css`;
100
+ return url.length <= MAX_CONTENT_URL ? url : null;
101
+ }
102
+
103
+ async function readErr(
104
+ res: { status: number; text: () => Promise<string> },
105
+ step: string,
106
+ ): Promise<string> {
107
+ let detail = "";
108
+ try {
109
+ detail = (await res.text()).slice(0, 300);
110
+ } catch {
111
+ /* ignore */
112
+ }
113
+ return `${step} failed (HTTP ${res.status})${detail ? `: ${detail}` : ""}`;
114
+ }
115
+
116
+ /**
117
+ * Publish a store's checkout styling to Foxy: signed PUT `/theme/foxy` then
118
+ * signed POST `/theme/foxy/refresh`. Returns a discriminated result — we do NOT
119
+ * fail open: if the PUT fails we skip the refresh and report the error, so the
120
+ * caller never reports "published" for something that didn't take.
121
+ *
122
+ * `fetchImpl` defaults to the global `fetch`; tests inject a stub.
123
+ */
124
+ export async function publishCheckoutTheme(
125
+ target: ThemePublishTarget,
126
+ fetchImpl?: FetchLike,
127
+ ): Promise<ThemePublishResult> {
128
+ const doFetch = fetchImpl ?? (globalThis.fetch as unknown as FetchLike);
129
+ const templateType = target.templateType ?? "checkout";
130
+ const base = target.foxyBaseUrl.replace(/\/+$/, "");
131
+
132
+ if (
133
+ target.contentUrl !== "" &&
134
+ (!target.contentUrl.startsWith("https://") ||
135
+ target.contentUrl.length > MAX_CONTENT_URL)
136
+ ) {
137
+ return {
138
+ ok: false,
139
+ error: `invalid contentUrl (must be https and <= ${MAX_CONTENT_URL} chars)`,
140
+ };
141
+ }
142
+
143
+ // 1. PUT the pointer.
144
+ const setBody = {
145
+ appDomain: target.appDomain,
146
+ templateType,
147
+ contentUrl: target.contentUrl,
148
+ };
149
+ const setSig = await signThemeRequestBody(target.secret, setBody);
150
+ const setRes = await doFetch(`${base}/theme/foxy`, {
151
+ method: "PUT",
152
+ headers: {
153
+ "content-type": "application/json",
154
+ [TOT_SIGNATURE_HEADER]: setSig,
155
+ },
156
+ body: JSON.stringify(setBody),
157
+ });
158
+ if (!setRes.ok) {
159
+ return { ok: false, error: await readErr(setRes, "PUT /theme/foxy") };
160
+ }
161
+ let set: unknown = undefined;
162
+ try {
163
+ set = JSON.parse(await setRes.text());
164
+ } catch {
165
+ /* non-JSON ok-body — leave undefined */
166
+ }
167
+
168
+ // 2. POST refresh to make it live.
169
+ const refreshBody = { appDomain: target.appDomain, templateType };
170
+ const refreshSig = await signThemeRequestBody(target.secret, refreshBody);
171
+ const refreshRes = await doFetch(`${base}/theme/foxy/refresh`, {
172
+ method: "POST",
173
+ headers: {
174
+ "content-type": "application/json",
175
+ [TOT_SIGNATURE_HEADER]: refreshSig,
176
+ },
177
+ body: JSON.stringify(refreshBody),
178
+ });
179
+ if (!refreshRes.ok) {
180
+ return {
181
+ ok: false,
182
+ set,
183
+ error: await readErr(refreshRes, "POST /theme/foxy/refresh"),
184
+ };
185
+ }
186
+
187
+ return { ok: true, set, refreshed: true };
188
+ }
189
+
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.
194
+ */
195
+ export function foxyProxyBaseUrl(env: CustomizationEnv): string {
196
+ // "test" is the qa target's env; "live" is prod.
197
+ return env === "live"
198
+ ? "https://foxycart.tokenoftrust.com"
199
+ : "https://foxycart-test.tokenoftrust.com";
200
+ }
@@ -0,0 +1,375 @@
1
+ /**
2
+ * Checkout-styling data model + published-artifact read plumbing.
3
+ *
4
+ * The FoxyCart-hosted checkout is styled by CSS pushed to Foxy's `/theme/foxy`
5
+ * endpoint (U11). This module owns the *source of truth* for that styling: a
6
+ * small, structured, tenant-editable descriptor that later units render into
7
+ * ASCII-safe CSS. It sits on the SAME versioned publish spine as theme/content
8
+ * (D1VersionStore + KV pointers) — resolution is per-environment via
9
+ * `readPublishedArtifact`, so a merchant's draft previews on `test` and the
10
+ * promoted style serves on `live`, exactly like the theme artifact.
11
+ *
12
+ * Two colocated artifacts under `tenants/<id>/`:
13
+ * - `checkout-style.json` — the structured `CheckoutStyle` descriptor (below).
14
+ * - `checkout.css` — an optional raw-CSS escape hatch a merchant edits
15
+ * directly; hosted + pushed to Foxy as-is (U11).
16
+ *
17
+ * Pure + dependency-free (node-free, edge-safe): the caller injects the same
18
+ * `KvGet` used for theme/content, keeping this unit-testable and storage-agnostic.
19
+ * No UI here — that's U12.
20
+ */
21
+ import type { CustomizationEnv } from "./customization-preview.js";
22
+ import type { DeepPartial, ThemeTokens } from "./tenant.js";
23
+ import { readPublishedArtifact, type KvGet } from "./customization-runtime.js";
24
+
25
+ /**
26
+ * Color levers exposed on the checkout page. Deliberately a small, opinionated
27
+ * subset of the full theme palette — the checkout is a focused conversion
28
+ * surface, not the whole store. All values are CSS colors; hex is validated for
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.
31
+ */
32
+ export interface CheckoutStyleColors {
33
+ /** Page background behind the checkout card. */
34
+ background: string;
35
+ /** Surface color of the checkout card / form panels. */
36
+ surface: string;
37
+ /** Primary brand color — section headings and the pay button fill. */
38
+ primary: string;
39
+ /** Foreground on `primary` (button label / on-primary text) — contrast pair. */
40
+ primaryContrast: string;
41
+ /** Accent color — links, focus rings, selected states. */
42
+ accent: string;
43
+ /** Body text color. */
44
+ text: string;
45
+ }
46
+
47
+ /**
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
50
+ * an opaque string so a merchant can bring their own stack without a schema bump.
51
+ */
52
+ export interface CheckoutStyleTypography {
53
+ /** CSS font-family stack, e.g. `"Inter", system-ui, sans-serif`. */
54
+ fontFamily: string;
55
+ /** Base font size as a CSS length, e.g. `16px`. */
56
+ baseSize: string;
57
+ }
58
+
59
+ /** Button/control shape on the checkout. */
60
+ export interface CheckoutStyleButton {
61
+ /** Corner radius as a CSS length, e.g. `6px` (`0` for square). */
62
+ radius: string;
63
+ }
64
+
65
+ /**
66
+ * The structured checkout-styling descriptor — the `checkout-style.json`
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
69
+ * (a merchant may set one field without restating the rest); `resolveCheckout
70
+ * Style` merges it over `DEFAULT_CHECKOUT_STYLE` so the resolved value is always
71
+ * complete.
72
+ */
73
+ export interface CheckoutStyle {
74
+ colors: CheckoutStyleColors;
75
+ typography: CheckoutStyleTypography;
76
+ button: CheckoutStyleButton;
77
+ /** Optional logo URL shown on the checkout header (absolute https URL). */
78
+ logoUrl?: string;
79
+ }
80
+
81
+ /**
82
+ * Neutral, accessible baseline used when a tenant publishes nothing (or omits a
83
+ * field). Chosen for AA contrast on the default pairs so an un-styled checkout
84
+ * is never broken — a merchant only overrides what they want to change.
85
+ */
86
+ export const DEFAULT_CHECKOUT_STYLE: CheckoutStyle = {
87
+ colors: {
88
+ background: "#ffffff",
89
+ surface: "#f7f7f8",
90
+ primary: "#111111",
91
+ primaryContrast: "#ffffff",
92
+ accent: "#2563eb",
93
+ text: "#1a1a1a",
94
+ },
95
+ typography: {
96
+ fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
97
+ baseSize: "16px",
98
+ },
99
+ button: {
100
+ radius: "6px",
101
+ },
102
+ };
103
+
104
+ /**
105
+ * Derive a checkout style FROM a tenant's brand `ThemeTokens`, so the checkout is
106
+ * consistent with the look/feel the merchant already chose for the storefront
107
+ * instead of a neutral default. This is the "from theme" seam: the settings editor
108
+ * (U12) seeds a merchant's starting style from it, and a tenant that ships a brand
109
+ * theme gets an in-brand checkout with no hand-authoring.
110
+ *
111
+ * It maps the small, opinionated checkout surface onto the full theme palette:
112
+ * - colors ← `color.{bg,surface,primary,primary-contrast,accent,text}`
113
+ * - typography.fontFamily ← the theme's BODY family, expanded into a web-safe
114
+ * stack (the checkout is body copy + a CTA, not display type)
115
+ * - button.radius ← `shape.radius.md` (the standard control radius)
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
118
+ * (e.g. `/brand/logo.svg`) can't resolve there and is dropped.
119
+ *
120
+ * Input is a `DeepPartial` (a tenant's `theme.json` override is sparse), so every
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
123
+ * Unicode-safe): any derived string leaf that isn't `isAsciiSafe` is dropped in
124
+ * favor of the default, so a theme can never produce a checkout-blanking value.
125
+ * Pure + node-free.
126
+ */
127
+ export function checkoutStyleFromTheme(
128
+ theme: DeepPartial<ThemeTokens> | null | undefined,
129
+ ): CheckoutStyle {
130
+ const d = DEFAULT_CHECKOUT_STYLE;
131
+ // Take a theme string only when it is a non-empty, ASCII-safe scalar; otherwise
132
+ // fall back so a missing/exotic token never yields a bad checkout value.
133
+ const take = (value: unknown, fallback: string): string =>
134
+ typeof value === "string" && value.trim() && isAsciiSafe(value)
135
+ ? value
136
+ : fallback;
137
+
138
+ const color = theme?.color;
139
+ const shape = theme?.shape;
140
+ const bodyFamily = theme?.type?.body?.family;
141
+ const logo = theme?.brand?.logoLight;
142
+
143
+ return {
144
+ colors: {
145
+ background: take(color?.bg, d.colors.background),
146
+ surface: take(color?.surface, d.colors.surface),
147
+ primary: take(color?.primary, d.colors.primary),
148
+ primaryContrast: take(color?.["primary-contrast"], d.colors.primaryContrast),
149
+ accent: take(color?.accent, d.colors.accent),
150
+ text: take(color?.text, d.colors.text),
151
+ },
152
+ typography: {
153
+ fontFamily: fontStackFromFamily(bodyFamily, d.typography.fontFamily),
154
+ baseSize: d.typography.baseSize,
155
+ },
156
+ button: {
157
+ radius: take(shape?.radius?.md, d.button.radius),
158
+ },
159
+ ...(typeof logo === "string" &&
160
+ isAsciiSafe(logo) &&
161
+ /^https:\/\/\S+$/.test(logo.trim())
162
+ ? { logoUrl: logo.trim() }
163
+ : {}),
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Expand a single theme font FAMILY (`"Inter"`) into a full CSS font-family stack
169
+ * with web-safe fallbacks — the checkout must render legibly even where the brand
170
+ * webfont isn't loaded. A value that already looks like a stack (has a comma) is
171
+ * used as-is. Falls back to `fallback` when the family is absent or non-ASCII.
172
+ */
173
+ function fontStackFromFamily(family: unknown, fallback: string): string {
174
+ if (typeof family !== "string" || !family.trim() || !isAsciiSafe(family)) {
175
+ return fallback;
176
+ }
177
+ const f = family.trim();
178
+ if (f.includes(",")) return f;
179
+ const quoted = /\s/.test(f) ? `"${f}"` : f;
180
+ return `${quoted}, system-ui, -apple-system, Segoe UI, Roboto, sans-serif`;
181
+ }
182
+
183
+ /**
184
+ * Canonical published-artifact path for a tenant's structured checkout style.
185
+ * Same colocated `tenants/<id>/…` contract as theme/capabilities, so the
186
+ * reconcile pipeline keys the KV artifact by it and the storefront reads it back
187
+ * per environment.
188
+ */
189
+ export function checkoutStyleArtifactPath(tenantId: string): string {
190
+ return `tenants/${tenantId}/checkout-style.json`;
191
+ }
192
+
193
+ /**
194
+ * Canonical path for the optional raw-CSS override artifact — the escape hatch a
195
+ * merchant edits directly (or that advanced templates ship). Sibling to the
196
+ * structured descriptor; hosted + pushed to Foxy verbatim by U11.
197
+ */
198
+ export function checkoutStyleCssPath(tenantId: string): string {
199
+ return `tenants/${tenantId}/checkout.css`;
200
+ }
201
+
202
+ /**
203
+ * Safely parse a published `checkout-style.json` string into a partial override.
204
+ * Never throws — returns null on malformed JSON or a non-object top level, so the
205
+ * caller falls back to the default (mirrors the theme read path's tolerance).
206
+ */
207
+ export function parseCheckoutStyle(
208
+ raw: string | null,
209
+ ): DeepPartial<CheckoutStyle> | null {
210
+ if (!raw) return null;
211
+ try {
212
+ const parsed = JSON.parse(raw) as unknown;
213
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
214
+ return null;
215
+ }
216
+ return parsed as DeepPartial<CheckoutStyle>;
217
+ } catch {
218
+ return null;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Deep-merge a partial override over a complete base, so a merchant can set one
224
+ * field without restating the rest. Objects merge recursively; scalars replace.
225
+ * Node-free and self-contained (mirrors `mergeTheme`'s deepMerge).
226
+ */
227
+ export function mergeCheckoutStyle(
228
+ base: CheckoutStyle,
229
+ override?: DeepPartial<CheckoutStyle> | null,
230
+ ): CheckoutStyle {
231
+ if (!override) return base;
232
+ return deepMerge(base, override) as CheckoutStyle;
233
+ }
234
+
235
+ function deepMerge<T>(base: T, over: DeepPartial<T>): T {
236
+ if (typeof base === "object" && base !== null && !Array.isArray(base)) {
237
+ const out: Record<string, unknown> = { ...(base as Record<string, unknown>) };
238
+ for (const [k, v] of Object.entries(over as Record<string, unknown>)) {
239
+ if (v === undefined) continue;
240
+ const bv = (base as Record<string, unknown>)[k];
241
+ out[k] =
242
+ bv && typeof bv === "object" && !Array.isArray(bv) && typeof v === "object"
243
+ ? deepMerge(bv, v as DeepPartial<unknown>)
244
+ : v;
245
+ }
246
+ return out as T;
247
+ }
248
+ return (over as unknown as T) ?? base;
249
+ }
250
+
251
+ /**
252
+ * Resolve a tenant's effective checkout style for an environment: the
253
+ * `DEFAULT_CHECKOUT_STYLE` baseline with any published `checkout-style.json`
254
+ * override merged on top. Reads through the versioned publish spine (channel →
255
+ * version → artifact) via `readPublishedArtifact`; a malformed/absent artifact
256
+ * degrades to the default. Never throws.
257
+ */
258
+ export async function resolveCheckoutStyle(
259
+ get: KvGet,
260
+ tenantId: string,
261
+ env: CustomizationEnv,
262
+ ): Promise<CheckoutStyle> {
263
+ const raw = await readPublishedArtifact(
264
+ get,
265
+ tenantId,
266
+ env,
267
+ checkoutStyleArtifactPath(tenantId),
268
+ );
269
+ return mergeCheckoutStyle(DEFAULT_CHECKOUT_STYLE, parseCheckoutStyle(raw));
270
+ }
271
+
272
+ /**
273
+ * Resolve a tenant's published raw checkout CSS override for an environment, or
274
+ * null when none is published (so U11 pushes only the generated tokens). Reads
275
+ * the sibling `checkout.css` artifact through the same versioned spine. Never
276
+ * throws.
277
+ */
278
+ export async function resolveCheckoutStyleCss(
279
+ get: KvGet,
280
+ tenantId: string,
281
+ env: CustomizationEnv,
282
+ ): Promise<string | null> {
283
+ return readPublishedArtifact(
284
+ get,
285
+ tenantId,
286
+ env,
287
+ checkoutStyleCssPath(tenantId),
288
+ );
289
+ }
290
+
291
+ // ---------------------------------------------------------------------------
292
+ // CSS serialization (the artifact pushed to FoxyCart) — U11
293
+ // ---------------------------------------------------------------------------
294
+
295
+ /**
296
+ * True iff every character is 7-bit ASCII (code point < 128). FoxyCart's
297
+ * template cache pass is NOT Unicode-safe — a non-ASCII byte in the cached
298
+ * checkout content yields a BLANK page. So the publisher (U11) MUST gate both
299
+ * the generated token CSS and any raw override through this before pushing a
300
+ * `content_url` to `/theme/foxy`. We reject rather than transliterate, so a
301
+ * merchant is told to fix their input instead of getting silently-mangled CSS.
302
+ */
303
+ export function isAsciiSafe(s: string): boolean {
304
+ for (let i = 0; i < s.length; i++) {
305
+ if (s.charCodeAt(i) > 127) return false;
306
+ }
307
+ return true;
308
+ }
309
+
310
+ /** CSS custom-property name for each checkout-style color token. */
311
+ const CHECKOUT_COLOR_VARS: Record<keyof CheckoutStyleColors, string> = {
312
+ background: "--tot-checkout-bg",
313
+ surface: "--tot-checkout-surface",
314
+ primary: "--tot-checkout-primary",
315
+ primaryContrast: "--tot-checkout-primary-contrast",
316
+ accent: "--tot-checkout-accent",
317
+ text: "--tot-checkout-text",
318
+ };
319
+
320
+ /**
321
+ * Serialize a resolved `CheckoutStyle` into a `:root { … }` block of CSS custom
322
+ * properties (`--tot-checkout-*`) that a FoxyCart checkout template references.
323
+ * Deterministic (stable key order) so an unchanged style produces byte-identical
324
+ * output — the publisher can hash it to skip no-op pushes. The `logoUrl`, when
325
+ * set, is emitted as `--tot-checkout-logo: url("…")`.
326
+ *
327
+ * Output is ASCII by construction for the default palette; a merchant-supplied
328
+ * value (e.g. an exotic font-family or logo URL) could introduce non-ASCII, so
329
+ * the publisher still validates the final string with `isAsciiSafe` — this
330
+ * function does not throw, it just serializes.
331
+ */
332
+ export function checkoutStyleToCss(style: CheckoutStyle): string {
333
+ const lines: string[] = [];
334
+ const push = (name: string, value: string) => lines.push(` ${name}: ${value};`);
335
+
336
+ for (const key of Object.keys(CHECKOUT_COLOR_VARS) as (keyof CheckoutStyleColors)[]) {
337
+ push(CHECKOUT_COLOR_VARS[key], style.colors[key]);
338
+ }
339
+ push("--tot-checkout-font-family", style.typography.fontFamily);
340
+ push("--tot-checkout-font-size", style.typography.baseSize);
341
+ push("--tot-checkout-button-radius", style.button.radius);
342
+ if (style.logoUrl) {
343
+ push("--tot-checkout-logo", `url("${style.logoUrl}")`);
344
+ }
345
+
346
+ return `:root {\n${lines.join("\n")}\n}`;
347
+ }
348
+
349
+ /**
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
352
+ * the resolved `CheckoutStyle`) followed by the tenant's optional raw-CSS override.
353
+ *
354
+ * ASCII discipline (Foxy's cache pass is not Unicode-safe → blank checkout): the
355
+ * raw override is appended ONLY when it is `isAsciiSafe` — a non-ASCII override is
356
+ * DROPPED here so a bad byte can never blank the live checkout (the authoritative
357
+ * reject-at-publish gate is U13's promote GateReport; this is defense-in-depth at
358
+ * serve time). The token block is ASCII for the default palette and any sane
359
+ * merchant value. Reads both artifacts per env through the versioned spine; never
360
+ * throws.
361
+ */
362
+ export async function buildCheckoutStyleCss(
363
+ get: KvGet,
364
+ tenantId: string,
365
+ env: CustomizationEnv,
366
+ ): Promise<string> {
367
+ const tokenCss = checkoutStyleToCss(
368
+ await resolveCheckoutStyle(get, tenantId, env),
369
+ );
370
+ const raw = await resolveCheckoutStyleCss(get, tenantId, env);
371
+ if (raw && isAsciiSafe(raw)) {
372
+ return `${tokenCss}\n\n${raw}`;
373
+ }
374
+ return tokenCss;
375
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Content-Security-Policy builder. Pure + dep-free so it's unit-testable and
3
+ * reusable across the storefront.
4
+ *
5
+ * Produces a strict policy: `default-src 'self'`, no `unsafe-inline`. The
6
+ * platform's own inline `<style>`/`<script is:inline>` are allowed via a
7
+ * per-request nonce; everything else must be `'self'`, an explicitly allowlisted
8
+ * host, or (for tenant scripts) a per-version hash passed in `scriptSrc`.
9
+ *
10
+ * The middleware decides the header name — `Content-Security-Policy` (enforce)
11
+ * vs `Content-Security-Policy-Report-Only` (rollout). This only builds the value.
12
+ */
13
+ export interface CspOptions {
14
+ /** Per-request nonce; the same value goes on the platform's inline tags. */
15
+ nonce: string;
16
+ /** Extra script-src entries: allowlisted CDNs + per-version tenant hashes. */
17
+ scriptSrc?: readonly string[];
18
+ styleSrc?: readonly string[];
19
+ fontSrc?: readonly string[];
20
+ imgSrc?: readonly string[];
21
+ connectSrc?: readonly string[];
22
+ /** Sandboxed-widget origins. */
23
+ frameSrc?: readonly string[];
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.
27
+ */
28
+ formAction?: readonly string[];
29
+ /** Reporting group name for `report-to` (paired with a Reporting-Endpoints header). */
30
+ reportGroup?: string;
31
+ /** Legacy `report-uri` path (broader browser support during rollout). */
32
+ reportUri?: string;
33
+ }
34
+
35
+ export function buildCsp(opts: CspOptions): string {
36
+ const nonce = `'nonce-${opts.nonce}'`;
37
+ const directives: Record<string, string[]> = {
38
+ "default-src": ["'self'"],
39
+ "base-uri": ["'self'"],
40
+ "object-src": ["'none'"],
41
+ "frame-ancestors": ["'self'"],
42
+ "form-action": ["'self'", ...(opts.formAction ?? [])],
43
+ "script-src": ["'self'", nonce, ...(opts.scriptSrc ?? [])],
44
+ "style-src": ["'self'", nonce, ...(opts.styleSrc ?? [])],
45
+ // Inline style ATTRIBUTES (style="…") can't carry a nonce; CSS can't execute
46
+ // JS, so this is the one tolerated relaxation. style-src-elem still inherits
47
+ // the strict 'self'+nonce from style-src; script-src stays fully strict.
48
+ "style-src-attr": ["'unsafe-inline'"],
49
+ "font-src": ["'self'", ...(opts.fontSrc ?? [])],
50
+ "img-src": ["'self'", "data:", ...(opts.imgSrc ?? [])],
51
+ "connect-src": ["'self'", ...(opts.connectSrc ?? [])],
52
+ "frame-src": ["'self'", ...(opts.frameSrc ?? [])],
53
+ };
54
+ const parts = Object.entries(directives).map(
55
+ ([name, values]) => `${name} ${values.join(" ")}`,
56
+ );
57
+ if (opts.reportUri) parts.push(`report-uri ${opts.reportUri}`);
58
+ if (opts.reportGroup) parts.push(`report-to ${opts.reportGroup}`);
59
+ return parts.join("; ");
60
+ }
61
+
62
+ /** Endpoint that collects CSP violation reports during the report-only rollout. */
63
+ export const CSP_REPORT_PATH = "/api/csp-report";
64
+ export const CSP_REPORT_GROUP = "csp-endpoint";
65
+
66
+ /**
67
+ * The storefront's known-good external origins. The middleware passes these to
68
+ * `buildCsp` and may extend `scriptSrc` (tenant hashes) + `frameSrc` (the widget
69
+ * origin) per request.
70
+ */
71
+ export const STOREFRONT_CSP_ALLOWLIST = {
72
+ scriptSrc: ["https://static.cloudflareinsights.com"],
73
+ styleSrc: ["https://fonts.googleapis.com"],
74
+ fontSrc: ["https://fonts.gstatic.com"],
75
+ // Product imagery is rehosted to R2 or served from remote CDNs.
76
+ imgSrc: ["https:"],
77
+ connectSrc: ["https://cloudflareinsights.com"],
78
+ } as const;
79
+
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.
86
+ */
87
+ export function foxyCspAdditions(storeOrigin: string): {
88
+ scriptSrc: string[];
89
+ styleSrc: string[];
90
+ fontSrc: string[];
91
+ connectSrc: string[];
92
+ frameSrc: string[];
93
+ formAction: string[];
94
+ imgSrc: string[];
95
+ } {
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.
100
+ return {
101
+ scriptSrc: [cdn, storeOrigin],
102
+ styleSrc: [cdn, storeOrigin],
103
+ fontSrc: [cdn, storeOrigin],
104
+ connectSrc: [cdn, storeOrigin],
105
+ frameSrc: [cdn, storeOrigin],
106
+ formAction: [storeOrigin],
107
+ imgSrc: [cdn, storeOrigin],
108
+ };
109
+ }