@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,136 @@
1
+ /**
2
+ * Raw marketing-HTML serving — the "vendor injects their own HTML" path.
3
+ *
4
+ * A marketing tenant can ship a self-contained document (`content/<tenant>/home.html`
5
+ * or `content/<tenant>/pages-html/<slug>.html`) that we serve VERBATIM, bypassing the
6
+ * block Layout. Two transforms are applied at serve time:
7
+ * 1. nonce-stamp the document's inline <style>/<script> so a strict CSP admits them;
8
+ * 2. under path-prefix tenant routing (locals.basePath set), prefix same-origin
9
+ * root-absolute anchor hrefs with the base path so navigation stays in-tenant —
10
+ * the raw doc bypasses the Layout's client base-path fixer, so we do it here.
11
+ *
12
+ * Single home for both routes (`index.astro`, `[...slug].astro`).
13
+ *
14
+ * SECURITY: this trusts the tenant's HTML (first-party marketing). The nonce
15
+ * injection is a deliberate string replace — robust for first-party documents, not a
16
+ * sanitizer. Untrusted vendor HTML must be sandboxed/sanitized instead.
17
+ */
18
+
19
+ /**
20
+ * Marker a body-only page fragment's chrome wrapper uses to say "the page body
21
+ * goes here". A tenant's `content/<tenant>/chrome.html` is a full document with
22
+ * this comment where the fragment is spliced in. Kept as an HTML comment so the
23
+ * wrapper is still valid, previewable HTML on its own.
24
+ */
25
+ export const CHROME_BODY_MARKER = "<!--PAGE_BODY-->";
26
+
27
+ /**
28
+ * Marker in the chrome wrapper's <head> where a fragment's page-specific <head>
29
+ * (title / meta / canonical / OG / JSON-LD) is spliced in — so shared chrome does
30
+ * NOT flatten per-page SEO. A fragment may lead with a `<head>…</head>` block; its
31
+ * contents go here, the rest goes to CHROME_BODY_MARKER.
32
+ */
33
+ export const CHROME_HEAD_MARKER = "<!--PAGE_HEAD-->";
34
+
35
+ /**
36
+ * True when `html` is a complete document (has <html> or a doctype) rather than a
37
+ * body-only fragment. Full documents are served verbatim; fragments get wrapped
38
+ * in the tenant's shared chrome. This is what keeps the mechanism backward-
39
+ * compatible — every existing raw home/subpage is a full document.
40
+ */
41
+ export function isFullDocument(html: string): boolean {
42
+ return /^\s*<!doctype/i.test(html) || /<html[\s>]/i.test(html);
43
+ }
44
+
45
+ /**
46
+ * Splice a body-only fragment into a tenant chrome wrapper at CHROME_BODY_MARKER.
47
+ * If the wrapper has no marker (misconfigured), the fragment is returned unwrapped
48
+ * so a page never disappears.
49
+ */
50
+ export function wrapFragmentInChrome(fragment: string, chromeWrapper: string): string {
51
+ if (!chromeWrapper.includes(CHROME_BODY_MARKER)) return fragment;
52
+ // A fragment may lead with a <head>…</head> of page-specific SEO tags. Split it
53
+ // out so the shared wrapper keeps per-page title/meta/canonical/JSON-LD.
54
+ let head = "";
55
+ let body = fragment;
56
+ const m = fragment.match(/^\s*<head[^>]*>([\s\S]*?)<\/head>/i);
57
+ if (m) {
58
+ head = m[1] ?? "";
59
+ body = fragment.slice(m[0].length);
60
+ }
61
+ let out = chromeWrapper.replace(CHROME_BODY_MARKER, body);
62
+ if (out.includes(CHROME_HEAD_MARKER)) out = out.replace(CHROME_HEAD_MARKER, head);
63
+ return out;
64
+ }
65
+
66
+ /** Stamp `nonce` onto every inline `<style>`/`<script>` so a strict CSP admits them. */
67
+ export function nonceInlineTags(html: string, nonce: string): string {
68
+ return html
69
+ .replaceAll("<style>", `<style nonce="${nonce}">`)
70
+ .replaceAll("<script", `<script nonce="${nonce}"`);
71
+ }
72
+
73
+ /**
74
+ * Prefix same-origin root-absolute hrefs (`href="/…"`) with the tenant base path,
75
+ * so `/pricing/` becomes `/<domain>/pricing/` under path-prefix routing. No-op when
76
+ * basePath is empty (host-routed prod). Skips protocol-relative `//…` and, crucially,
77
+ * the GLOBAL static-asset namespaces served at root under both routing modes, so they
78
+ * must NOT get the tenant prefix:
79
+ * - `/tenants/…` — per-tenant static assets (logo, favicon, theme/mkt css);
80
+ * - `/shared/…` — platform-shipped shared assets (e.g. the shared commerce-chrome
81
+ * stylesheet, unit F2) that every tenant links.
82
+ * Asset `src="…"` isn't matched at all (href-only).
83
+ */
84
+ export function prefixTenantLinks(html: string, basePath: string): string {
85
+ if (!basePath) return html;
86
+ return html.replace(/href="\/(?!\/|tenants\/|shared\/)/g, `href="${basePath}/`);
87
+ }
88
+
89
+ /**
90
+ * Build the HTML Response for a raw marketing page (nonce + base-path).
91
+ *
92
+ * Shared-chrome mechanism: a page is served in one of two ways —
93
+ * - FULL DOCUMENT (has <html>/doctype): served verbatim, as before.
94
+ * - BODY FRAGMENT + a tenant `chromeWrapper`: the fragment is spliced into the
95
+ * wrapper's CHROME_BODY_MARKER so chrome lives in ONE place, not copied into
96
+ * every page. A fragment with no wrapper still serves (unwrapped) for safety.
97
+ * Either way, the FINAL document gets nonce-stamped and base-path-prefixed, so the
98
+ * chrome's own inline <style>/<script> and root-absolute nav links are handled too.
99
+ */
100
+ export function rawHtmlResponse(
101
+ html: string,
102
+ opts: { nonce: string; basePath: string; chromeWrapper?: string | null },
103
+ ): Response {
104
+ let composed =
105
+ isFullDocument(html) || !opts.chromeWrapper
106
+ ? html
107
+ : wrapFragmentInChrome(html, opts.chromeWrapper);
108
+ // DEV ONLY: these pages are served verbatim, bypassing Astro's render pipeline,
109
+ // so the Vite HMR client is never injected — a save would reach the server (see
110
+ // the tenantHotReload plugin in astro.config.mjs) but the browser would have no
111
+ // listener to act on the full-reload. Inject the client so save→reload works.
112
+ // Stamped with the page nonce by nonceInlineTags below; tree-shaken from prod.
113
+ if (import.meta.env.DEV) {
114
+ const client = `<script type="module" src="/@vite/client"></script>`;
115
+ composed = composed.includes("</head>")
116
+ ? composed.replace("</head>", `${client}</head>`)
117
+ : `${client}${composed}`;
118
+ // Inline-AI edit loop's floating "✦ ask" affordance (WS5 G1) — same reason
119
+ // as the HMR client above, these pages bypass the Astro pipeline that
120
+ // inlineAiEditPlugin's transformIndexHtml hook (astro.config.mjs) covers
121
+ // for everything else, so it's injected here too. Unconditional in dev,
122
+ // matching the Vite client above: whether `tot dev` set an active tenant
123
+ // (TOT_DEV_TENANT) is a Node-process env var, invisible to this SSR code
124
+ // when it runs inside the Cloudflare/workerd dev runtime (see tot-dev.mjs's
125
+ // own note on this exact gap) — so the gate lives server-side instead, in
126
+ // inlineAiEditPlugin's Vite middleware, which DOES see the real env.
127
+ const widget = `<script type="module" src="/__tot/ai-widget-client.js"></script>`;
128
+ composed = composed.includes("</body>")
129
+ ? composed.replace("</body>", `${widget}</body>`)
130
+ : `${composed}${widget}`;
131
+ }
132
+ const doc = prefixTenantLinks(nonceInlineTags(composed, opts.nonce), opts.basePath);
133
+ return new Response(doc, {
134
+ headers: { "content-type": "text/html; charset=utf-8" },
135
+ });
136
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * In-memory Fuse search index — the OFFLINE search floor.
3
+ *
4
+ * Used by FixtureToTClient.search (no D1 binding): typo-tolerant, relevance-ranked
5
+ * search over the bundled catalog snapshot, zero infra. When a D1 binding is wired
6
+ * (D1ToTClient), search instead goes through FTS5 — see lib/d1/catalog.ts and
7
+ * epics/edge-read-replica.md. This module is the portable floor an extracted
8
+ * merchant gets out of the box (docs/extraction-runbook.md).
9
+ */
10
+ import Fuse, { type IFuseOptions } from "fuse.js";
11
+ import type { CatalogProduct } from "@tot/public-runtime";
12
+
13
+ export interface SearchHit {
14
+ product: CatalogProduct;
15
+ score: number;
16
+ }
17
+
18
+ export interface SearchIndex {
19
+ search(query: string, limit?: number): SearchHit[];
20
+ }
21
+
22
+ const FUSE_OPTIONS: IFuseOptions<CatalogProduct> = {
23
+ includeScore: true,
24
+ threshold: 0.38, // typo tolerance: lower = stricter
25
+ ignoreLocation: true,
26
+ minMatchCharLength: 2,
27
+ keys: [
28
+ { name: "title", weight: 0.5 },
29
+ { name: "vendor", weight: 0.15 },
30
+ { name: "product_type", weight: 0.15 },
31
+ { name: "tags", weight: 0.12 },
32
+ { name: "description_text", weight: 0.08 },
33
+ ],
34
+ };
35
+
36
+ export function buildSearchIndex(products: CatalogProduct[]): SearchIndex {
37
+ // Defensive: only ever index active products (the draft canary must never leak).
38
+ const active = products.filter((p) => p.status === "active");
39
+ const fuse = new Fuse(active, FUSE_OPTIONS);
40
+ return {
41
+ search(query: string, limit = 12): SearchHit[] {
42
+ const q = query.trim();
43
+ if (!q) return [];
44
+ return fuse.search(q, { limit }).map((r) => ({ product: r.item, score: r.score ?? 1 }));
45
+ },
46
+ };
47
+ }
48
+
49
+ /** Compact shape sent to the typeahead island (keeps client JSON tiny). */
50
+ export interface TypeaheadDoc {
51
+ handle: string;
52
+ title: string;
53
+ vendor?: string;
54
+ type?: string;
55
+ price: number;
56
+ currency: string;
57
+ image?: string;
58
+ tags: string[];
59
+ }
60
+
61
+ export function toTypeaheadDoc(p: CatalogProduct): TypeaheadDoc {
62
+ const doc: TypeaheadDoc = {
63
+ handle: p.handle,
64
+ title: p.title,
65
+ price: p.price_range.min,
66
+ currency: p.price_range.currency,
67
+ tags: p.tags,
68
+ };
69
+ if (p.vendor) doc.vendor = p.vendor;
70
+ if (p.product_type) doc.type = p.product_type;
71
+ const img = p.images[0];
72
+ if (img?.url) doc.image = img.url;
73
+ return doc;
74
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * SectionSource — the single, typed contract every merchandising section binds
3
+ * to. A section component (ProductCarousel, ProductGrid, …) never talks to the
4
+ * catalog directly; it declares WHAT set of products it wants as a SectionSource
5
+ * and calls resolveSectionSource() to get them. This keeps the components
6
+ * tenant-agnostic and data-source-agnostic: swap the ToTClient backend (D1 vs
7
+ * fixture) and every section keeps working.
8
+ *
9
+ * Fidelity to a merchant's storefront comes from theme tokens + variant props +
10
+ * the source they point a section at — NEVER from forking a component per
11
+ * merchant. That's what keeps a tenant single-tenant-extractable.
12
+ */
13
+ import type { CatalogProduct } from "@tot/public-runtime";
14
+ import type { ToTClient } from "@/lib/tot/ToTClient";
15
+ import { isOnSale } from "@/lib/tot/query";
16
+
17
+ /**
18
+ * Where a merchandising section gets its products. Exactly one shape per source.
19
+ * `handle` placeholders are product/collection handles (unique per tenant).
20
+ */
21
+ export type SectionSource =
22
+ | { collection: string } // products in a collection (by handle)
23
+ | { tag: string } // products carrying a tag
24
+ | { featured: true } // the curated/featured set (getFeatured)
25
+ | { newest: true } // newest-first
26
+ | { bestSelling: true } // bestseller-tagged
27
+ | { onSale: true } // anything with a live compare-at markdown
28
+ | { handles: string[] } // an explicit, ordered handle list
29
+ | { related: string } // products related to a base product (by handle)
30
+ | { recentlyViewed: true }; // client-personalized — resolves empty server-side
31
+
32
+ export const DEFAULT_SECTION_LIMIT = 12;
33
+
34
+ /** Bestseller signal is tag-based in Phase 1 (no order history at the edge). */
35
+ const BESTSELLER_TAGS = new Set(["bestseller", "best-seller", "best-sellers"]);
36
+
37
+ const isActive = (p: CatalogProduct) => p.status === "active";
38
+
39
+ /**
40
+ * Resolve a SectionSource to a concrete, active product list via the tenant's
41
+ * ToTClient. Always returns at most `limit` ACTIVE products (drafts/archived are
42
+ * excluded from every public surface — see ToTClient docs). Backend-agnostic:
43
+ * tag/onSale/bestSelling filter over the client's list methods rather than
44
+ * assuming a SQL facet, so the fixture and D1 backends behave identically.
45
+ */
46
+ export async function resolveSectionSource(
47
+ source: SectionSource,
48
+ tot: ToTClient,
49
+ limit: number = DEFAULT_SECTION_LIMIT,
50
+ ): Promise<CatalogProduct[]> {
51
+ if ("collection" in source) {
52
+ const { products } = await tot.listProducts({
53
+ collection: source.collection,
54
+ pageSize: limit,
55
+ });
56
+ return products.slice(0, limit);
57
+ }
58
+
59
+ if ("featured" in source) {
60
+ return (await tot.getFeatured(limit)).slice(0, limit);
61
+ }
62
+
63
+ if ("newest" in source) {
64
+ const { products } = await tot.listProducts({ sort: "newest", pageSize: limit });
65
+ return products.slice(0, limit);
66
+ }
67
+
68
+ if ("handles" in source) {
69
+ const found = await Promise.all(
70
+ source.handles.map((h) => tot.getProductByHandle(h)),
71
+ );
72
+ return found
73
+ .filter((p): p is CatalogProduct => !!p && isActive(p))
74
+ .slice(0, limit);
75
+ }
76
+
77
+ if ("tag" in source) {
78
+ const all = await tot.getAllActiveProducts();
79
+ return all.filter((p) => p.tags.includes(source.tag)).slice(0, limit);
80
+ }
81
+
82
+ if ("onSale" in source) {
83
+ const all = await tot.getAllActiveProducts();
84
+ return all.filter(isOnSale).slice(0, limit);
85
+ }
86
+
87
+ if ("bestSelling" in source) {
88
+ const all = await tot.getAllActiveProducts();
89
+ return all
90
+ .filter((p) => p.tags.some((t) => BESTSELLER_TAGS.has(t)))
91
+ .slice(0, limit);
92
+ }
93
+
94
+ if ("related" in source) {
95
+ const base = await tot.getProductByHandle(source.related);
96
+ if (!base) return [];
97
+ const baseCollections = new Set(base.collections);
98
+ const baseTags = new Set(base.tags);
99
+ const all = await tot.getAllActiveProducts();
100
+ const scored = all
101
+ .filter((p) => p.handle !== base.handle)
102
+ .map((p) => {
103
+ const shared =
104
+ p.collections.filter((c) => baseCollections.has(c)).length +
105
+ p.tags.filter((t) => baseTags.has(t)).length;
106
+ return { p, shared };
107
+ })
108
+ .filter((s) => s.shared > 0)
109
+ .sort((a, b) => b.shared - a.shared);
110
+ return scored.slice(0, limit).map((s) => s.p);
111
+ }
112
+
113
+ // recentlyViewed: server has no per-visitor history; the client island
114
+ // (RecentlyViewed.tsx) hydrates this. Resolve empty so the section can decide
115
+ // to hide or defer to its island.
116
+ return [];
117
+ }
118
+
119
+ /**
120
+ * Ready-made sources for the common merchandising rows. A preset carousel is
121
+ * just `<ProductCarousel source={SECTION_PRESETS.bestSellers} />` — there are no
122
+ * separate BestSellers/NewArrivals/OnSale components to maintain.
123
+ */
124
+ export const SECTION_PRESETS = {
125
+ featured: { featured: true } as SectionSource,
126
+ bestSellers: { bestSelling: true } as SectionSource,
127
+ newArrivals: { newest: true } as SectionSource,
128
+ onSale: { onSale: true } as SectionSource,
129
+ } as const;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Baseline security response headers (Epic Phase-2 S1).
3
+ *
4
+ * Applied to every storefront response in middleware and baked into cached pages.
5
+ * The Content-Security-Policy itself (default-src/frame-ancestors/base-uri/…) is
6
+ * built separately in @tot/private-controlplane `csp.ts`; these are the complementary
7
+ * transport / sniffing / referrer / feature-permission headers that the appraisal
8
+ * flagged as missing.
9
+ */
10
+ export const SECURITY_HEADERS: Readonly<Record<string, string>> = {
11
+ // 2-year HSTS with preload — production is HTTPS-only behind Cloudflare;
12
+ // browsers ignore this header on plain http (localhost dev), so it's safe to
13
+ // always set.
14
+ "Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload",
15
+ "X-Content-Type-Options": "nosniff",
16
+ // Legacy backstop; modern browsers honour the CSP `frame-ancestors 'self'`.
17
+ "X-Frame-Options": "SAMEORIGIN",
18
+ "Referrer-Policy": "strict-origin-when-cross-origin",
19
+ // Lock powerful features off for the browse storefront. Verification and
20
+ // checkout (Powered by FoxyCart) run in their own surfaces that grant only
21
+ // what they need via per-frame Permissions-Policy.
22
+ "Permissions-Policy":
23
+ "camera=(), microphone=(), geolocation=(), browsing-topics=(), interest-cohort=()",
24
+ };
25
+
26
+ /** Set the baseline headers on a response, without clobbering anything already set. */
27
+ export function applySecurityHeaders(headers: Headers): void {
28
+ for (const [key, value] of Object.entries(SECURITY_HEADERS)) {
29
+ if (!headers.has(key)) headers.set(key, value);
30
+ }
31
+ }
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Editorial content model (Storyblok seam).
3
+ *
4
+ * These are OUR types, decoupled from Storyblok's wire shape. The provider maps
5
+ * either source (Storyblok delivery API OR local JSON) into these, so pages
6
+ * never see a raw Storyblok story. The block schemas here mirror the component
7
+ * schemas that live in the Storyblok PARENT space (shared components); the
8
+ * pilot CHILD space holds the content instances. See DECISIONS.md for the
9
+ * parent/child + Shared Components plan.
10
+ */
11
+
12
+ export type HomeBlock =
13
+ | HeroBlock
14
+ | PromoTilesBlock
15
+ | FeaturedCollectionsBlock
16
+ | FeaturedProductsBlock
17
+ | EditorialBlock
18
+ | NewsletterBlock;
19
+
20
+ export interface PromoTilesBlock {
21
+ component: "promo_tiles";
22
+ title?: string;
23
+ /** A row of promotional tiles (e.g. "New Additions", "On Sale") that link to
24
+ * collections — captured from the merchant's homepage. */
25
+ tiles: { label: string; href: string; image?: string }[];
26
+ }
27
+
28
+ export interface HeroBlock {
29
+ component: "hero";
30
+ /** The "thesis" line — the most characteristic thing about the catalog. */
31
+ eyebrow?: string;
32
+ headline: string;
33
+ /** Split second line gets the signature oversized treatment. */
34
+ headlineAccent?: string;
35
+ subhead?: string;
36
+ ctaLabel?: string;
37
+ ctaHref?: string;
38
+ /** Optional handle of a product/collection to feature visually. */
39
+ featureCollection?: string;
40
+ /** Direct hero image URL (e.g. the merchant's own homepage hero slide). */
41
+ image?: string;
42
+ imageAlt?: string;
43
+ }
44
+
45
+ export interface FeaturedCollectionsBlock {
46
+ component: "featured_collections";
47
+ title?: string;
48
+ /** Collection handles to surface; empty = all. */
49
+ handles: string[];
50
+ }
51
+
52
+ export interface FeaturedProductsBlock {
53
+ component: "featured_products";
54
+ title?: string;
55
+ /** "featured" | "newest" | explicit handles. */
56
+ source: "featured" | "newest";
57
+ handles?: string[];
58
+ limit?: number;
59
+ }
60
+
61
+ export interface EditorialBlock {
62
+ component: "editorial";
63
+ eyebrow?: string;
64
+ title: string;
65
+ body_html: string;
66
+ imageSeed?: string;
67
+ align?: "left" | "right";
68
+ ctaLabel?: string;
69
+ ctaHref?: string;
70
+ }
71
+
72
+ export interface NewsletterBlock {
73
+ component: "newsletter";
74
+ title: string;
75
+ body?: string;
76
+ placeholder?: string;
77
+ ctaLabel?: string;
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Marketing blocks (non-commerce tenants). These render WITHOUT catalog data —
82
+ // a marketing tenant composes its pages entirely from these. They're
83
+ // tenant-agnostic: every color/space comes from theme tokens, and the copy
84
+ // lives in tenant content JSON. See components/marketing/*.
85
+ // ---------------------------------------------------------------------------
86
+
87
+ /** A call-to-action button. `variant` picks primary/secondary styling; an
88
+ * `external` href (starts with http) opens the app/help/etc. surfaces. */
89
+ export interface MarketingCta {
90
+ label: string;
91
+ href: string;
92
+ variant?: "primary" | "secondary" | "ghost";
93
+ }
94
+
95
+ export interface MarketingHeroBlock {
96
+ component: "marketing_hero";
97
+ eyebrow?: string;
98
+ headline: string;
99
+ subhead?: string;
100
+ body?: string;
101
+ /** Short trust chips shown under the CTAs (e.g. "200+ countries"). */
102
+ trust?: string[];
103
+ ctas?: MarketingCta[];
104
+ /** Optional right-hand "product moment": a titled list of demo/log lines. */
105
+ panel?: { title?: string; lines: string[] };
106
+ }
107
+
108
+ /** A horizontal strip of short proof tokens (countries, standards, ratings). */
109
+ export interface TrustBarBlock {
110
+ component: "trust_bar";
111
+ items: string[];
112
+ }
113
+
114
+ /** Before/after comparison — the "one platform vs a patchwork" section. */
115
+ export interface SplitCompareBlock {
116
+ component: "split_compare";
117
+ eyebrow?: string;
118
+ title: string;
119
+ subhead?: string;
120
+ before: { title: string; items: string[] };
121
+ after: { title: string; items: string[] };
122
+ }
123
+
124
+ /** Numbered/emoji journey steps. */
125
+ export interface StepsBlock {
126
+ component: "steps";
127
+ eyebrow?: string;
128
+ title: string;
129
+ subhead?: string;
130
+ steps: { icon?: string; label: string; detail?: string }[];
131
+ }
132
+
133
+ /** A grid of capability/industry cards. `variant` tunes density/label style. */
134
+ export interface CardGridBlock {
135
+ component: "card_grid";
136
+ /** Optional anchor id so header nav can jump to this section (e.g. "platform"). */
137
+ anchor?: string;
138
+ eyebrow?: string;
139
+ title: string;
140
+ subhead?: string;
141
+ variant?: "capability" | "industry" | "feature";
142
+ cards: {
143
+ icon?: string;
144
+ title: string;
145
+ body?: string;
146
+ /** Small meta line (e.g. "Use case: account opening"). */
147
+ meta?: string;
148
+ href?: string;
149
+ }[];
150
+ /** Optional footnote of extra capabilities under the grid. */
151
+ footnote?: string;
152
+ }
153
+
154
+ /** A single fenced code sample. NEVER put secret keys here — server-only. */
155
+ export interface CodeSample {
156
+ language?: string;
157
+ code: string;
158
+ /** Optional caption/warning shown above the block. */
159
+ caption?: string;
160
+ }
161
+
162
+ /** Integration story: platform logos/labels + an optional safe code sample. */
163
+ export interface IntegrationsBlock {
164
+ component: "integrations";
165
+ eyebrow?: string;
166
+ title: string;
167
+ subhead?: string;
168
+ platforms: { label: string; href?: string }[];
169
+ points?: string[];
170
+ code?: CodeSample;
171
+ }
172
+
173
+ /** Privacy / trust proof strip (e.g. "We will never sell your data"). */
174
+ export interface ProofStripBlock {
175
+ component: "proof_strip";
176
+ eyebrow?: string;
177
+ title: string;
178
+ body?: string;
179
+ items?: string[];
180
+ }
181
+
182
+ export interface TestimonialsBlock {
183
+ component: "testimonials";
184
+ eyebrow?: string;
185
+ title?: string;
186
+ quotes: { stat?: string; quote: string; attribution?: string }[];
187
+ ratings?: string[];
188
+ }
189
+
190
+ export interface FaqBlock {
191
+ component: "faq";
192
+ eyebrow?: string;
193
+ title: string;
194
+ items: { q: string; a: string }[];
195
+ }
196
+
197
+ /** Final conversion band. */
198
+ export interface CtaBandBlock {
199
+ component: "cta_band";
200
+ title: string;
201
+ subhead?: string;
202
+ ctas?: MarketingCta[];
203
+ }
204
+
205
+ export type MarketingBlock =
206
+ | MarketingHeroBlock
207
+ | TrustBarBlock
208
+ | SplitCompareBlock
209
+ | StepsBlock
210
+ | CardGridBlock
211
+ | IntegrationsBlock
212
+ | ProofStripBlock
213
+ | TestimonialsBlock
214
+ | FaqBlock
215
+ | CtaBandBlock;
216
+
217
+ /** Discriminators that identify a marketing block (vs a commerce HomeBlock). */
218
+ export const MARKETING_COMPONENTS = new Set<MarketingBlock["component"]>([
219
+ "marketing_hero",
220
+ "trust_bar",
221
+ "split_compare",
222
+ "steps",
223
+ "card_grid",
224
+ "integrations",
225
+ "proof_strip",
226
+ "testimonials",
227
+ "faq",
228
+ "cta_band",
229
+ ]);
230
+
231
+ export function isMarketingBlock(
232
+ block: { component: string },
233
+ ): block is MarketingBlock {
234
+ return MARKETING_COMPONENTS.has(block.component as MarketingBlock["component"]);
235
+ }
236
+
237
+ export interface HomeContent {
238
+ /** SEO + social. */
239
+ seoTitle?: string;
240
+ seoDescription?: string;
241
+ /** Commerce home blocks OR marketing blocks — the page branches on tenant
242
+ * siteType/features and renders the matching set. */
243
+ blocks: (HomeBlock | MarketingBlock)[];
244
+ }
245
+
246
+ export interface EditorialPage {
247
+ slug: string;
248
+ title: string;
249
+ seoTitle?: string;
250
+ seoDescription?: string;
251
+ /** Sanitized HTML body. */
252
+ body_html: string;
253
+ /** Optional hero eyebrow/intro. */
254
+ eyebrow?: string;
255
+ intro?: string;
256
+ /** When true, listed in the footer policy column. */
257
+ isPolicy?: boolean;
258
+ }
259
+
260
+ export interface SiteNavItem {
261
+ label: string;
262
+ href: string;
263
+ /** Mega-menu children, e.g. collection links grouped under a category. */
264
+ children?: { label: string; href: string }[];
265
+ }
266
+
267
+ export interface SiteChrome {
268
+ nav: SiteNavItem[];
269
+ /**
270
+ * Header conversion buttons (marketing tenants). Rendered in the header's
271
+ * right cluster in place of the commerce search/saved controls. Commerce
272
+ * tenants omit this and keep the search + saved affordances.
273
+ */
274
+ headerCtas?: MarketingCta[];
275
+ footer: {
276
+ columns: { title: string; links: { label: string; href: string }[] }[];
277
+ social: { label: string; href: string }[];
278
+ tagline?: string;
279
+ /** Bottom-bar legal/compliance line (marketing tenants). Plain text. */
280
+ legal?: string;
281
+ };
282
+ announcement?: string;
283
+ /** Default legal/safety disclaimer HTML (fallback for unknown categories). */
284
+ disclaimer?: string;
285
+ /** Per-product-category disclaimer HTML, keyed by product_type (or
286
+ * compliance.product_category) — the source store renders a different legal
287
+ * block per type (sub-ohm on salts, battery on devices). */
288
+ disclaimers?: Record<string, string>;
289
+ }