@tokenoftrust/storefront-runner 0.1.0

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 (337) hide show
  1. package/LICENSE +58 -0
  2. package/README.md +40 -0
  3. package/apps/storefront/.dev.vars.example +41 -0
  4. package/apps/storefront/.env.example +24 -0
  5. package/apps/storefront/astro.config.mjs +245 -0
  6. package/apps/storefront/env.d.ts +155 -0
  7. package/apps/storefront/lib/ensure-preview-reconcile-secret.sh +31 -0
  8. package/apps/storefront/lib/resolve-cf-token.sh +48 -0
  9. package/apps/storefront/lib/resolve-tot-credentials.sh +69 -0
  10. package/apps/storefront/migrations/0000_baseline.sql +54 -0
  11. package/apps/storefront/migrations/0001_products_fts.sql +9 -0
  12. package/apps/storefront/migrations/README.md +54 -0
  13. package/apps/storefront/migrations/meta/0000_snapshot.json +347 -0
  14. package/apps/storefront/migrations/meta/0001_snapshot.json +347 -0
  15. package/apps/storefront/migrations/meta/_journal.json +20 -0
  16. package/apps/storefront/package.json +41 -0
  17. package/apps/storefront/public/brand/northwind-logo-dark.svg +4 -0
  18. package/apps/storefront/public/brand/northwind-logo-light.svg +4 -0
  19. package/apps/storefront/public/brand/northwind-og.svg +13 -0
  20. package/apps/storefront/public/favicon.svg +5 -0
  21. package/apps/storefront/public/js/dashboard-team.js +147 -0
  22. package/apps/storefront/public/js/dashboard-vendor-search.js +115 -0
  23. package/apps/storefront/src/components/Badge.astro +34 -0
  24. package/apps/storefront/src/components/Breadcrumbs.astro +45 -0
  25. package/apps/storefront/src/components/CollectionCard.astro +56 -0
  26. package/apps/storefront/src/components/EmptyState.astro +35 -0
  27. package/apps/storefront/src/components/Img.astro +68 -0
  28. package/apps/storefront/src/components/ProductCard.astro +228 -0
  29. package/apps/storefront/src/components/Section.astro +39 -0
  30. package/apps/storefront/src/components/Seo.astro +70 -0
  31. package/apps/storefront/src/components/WidgetFrame.astro +34 -0
  32. package/apps/storefront/src/components/auth/AuthBadge.astro +44 -0
  33. package/apps/storefront/src/components/chrome/AnnouncementBar.astro +108 -0
  34. package/apps/storefront/src/components/chrome/MegaMenu.astro +26 -0
  35. package/apps/storefront/src/components/chrome/NavDropdown.astro +116 -0
  36. package/apps/storefront/src/components/chrome/SiteFooter.astro +181 -0
  37. package/apps/storefront/src/components/chrome/SiteHeader.astro +181 -0
  38. package/apps/storefront/src/components/chrome/UtilityNav.astro +79 -0
  39. package/apps/storefront/src/components/commerce/FoxyLoader.astro +23 -0
  40. package/apps/storefront/src/components/commerce/PriceDisplay.astro +91 -0
  41. package/apps/storefront/src/components/commerce/ProductCarousel.astro +197 -0
  42. package/apps/storefront/src/components/commerce/ProductGrid.astro +75 -0
  43. package/apps/storefront/src/components/commerce/RatingStars.astro +58 -0
  44. package/apps/storefront/src/components/compliance/NicotineWarning.astro +33 -0
  45. package/apps/storefront/src/components/home/Hero.astro +164 -0
  46. package/apps/storefront/src/components/islands/AgeGate.tsx +105 -0
  47. package/apps/storefront/src/components/islands/ImageGallery.tsx +237 -0
  48. package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +73 -0
  49. package/apps/storefront/src/components/islands/MobileNav.tsx +239 -0
  50. package/apps/storefront/src/components/islands/QuickView.tsx +138 -0
  51. package/apps/storefront/src/components/islands/RecentlyViewed.tsx +122 -0
  52. package/apps/storefront/src/components/islands/SavedList.tsx +99 -0
  53. package/apps/storefront/src/components/islands/SearchTypeahead.tsx +248 -0
  54. package/apps/storefront/src/components/islands/VariantSelector.tsx +378 -0
  55. package/apps/storefront/src/components/marketing/CardGrid.astro +72 -0
  56. package/apps/storefront/src/components/marketing/CodeSample.astro +20 -0
  57. package/apps/storefront/src/components/marketing/CtaBand.astro +24 -0
  58. package/apps/storefront/src/components/marketing/Faq.astro +37 -0
  59. package/apps/storefront/src/components/marketing/Integrations.astro +63 -0
  60. package/apps/storefront/src/components/marketing/MarketingBlocks.astro +53 -0
  61. package/apps/storefront/src/components/marketing/MarketingCtas.astro +48 -0
  62. package/apps/storefront/src/components/marketing/MarketingHero.astro +72 -0
  63. package/apps/storefront/src/components/marketing/ProofStrip.astro +37 -0
  64. package/apps/storefront/src/components/marketing/SplitCompare.astro +53 -0
  65. package/apps/storefront/src/components/marketing/Steps.astro +36 -0
  66. package/apps/storefront/src/components/marketing/Testimonials.astro +38 -0
  67. package/apps/storefront/src/components/marketing/TrustBar.astro +22 -0
  68. package/apps/storefront/src/components/nav/Footer.astro +148 -0
  69. package/apps/storefront/src/components/nav/Header.astro +190 -0
  70. package/apps/storefront/src/components/plp/ActiveFilters.astro +79 -0
  71. package/apps/storefront/src/components/plp/FacetSidebar.astro +146 -0
  72. package/apps/storefront/src/components/plp/Pagination.astro +99 -0
  73. package/apps/storefront/src/components/plp/SortSelect.astro +64 -0
  74. package/apps/storefront/src/config/devTenants.ts +16 -0
  75. package/apps/storefront/src/config/kvTenantResolver.ts +87 -0
  76. package/apps/storefront/src/config/resolver.ts +46 -0
  77. package/apps/storefront/src/config/tenants.ts +193 -0
  78. package/apps/storefront/src/layouts/Layout.astro +303 -0
  79. package/apps/storefront/src/lib/analytics/index.ts +47 -0
  80. package/apps/storefront/src/lib/auth/brokerAssertion.ts +136 -0
  81. package/apps/storefront/src/lib/auth/devAuth.ts +90 -0
  82. package/apps/storefront/src/lib/auth/gatePage.ts +81 -0
  83. package/apps/storefront/src/lib/auth/identityToken.ts +341 -0
  84. package/apps/storefront/src/lib/auth/loginGate.ts +107 -0
  85. package/apps/storefront/src/lib/auth/route.ts +146 -0
  86. package/apps/storefront/src/lib/auth/session.ts +203 -0
  87. package/apps/storefront/src/lib/auth/totAccessClient.ts +503 -0
  88. package/apps/storefront/src/lib/basePath.ts +41 -0
  89. package/apps/storefront/src/lib/cfImage.ts +55 -0
  90. package/apps/storefront/src/lib/chrome/model.ts +65 -0
  91. package/apps/storefront/src/lib/colors.ts +97 -0
  92. package/apps/storefront/src/lib/content-edit/client.ts +230 -0
  93. package/apps/storefront/src/lib/content-edit/index.ts +14 -0
  94. package/apps/storefront/src/lib/content-edit/route.ts +25 -0
  95. package/apps/storefront/src/lib/content-edit/types.ts +19 -0
  96. package/apps/storefront/src/lib/cspStaticHashes.ts +24 -0
  97. package/apps/storefront/src/lib/d1/catalog.ts +289 -0
  98. package/apps/storefront/src/lib/dashboard/staffAdmission.ts +43 -0
  99. package/apps/storefront/src/lib/dashboard/staffVendorAccess.ts +449 -0
  100. package/apps/storefront/src/lib/dashboard/tenantSelection.ts +136 -0
  101. package/apps/storefront/src/lib/dashboard/vendorQuery.ts +61 -0
  102. package/apps/storefront/src/lib/demoGate.ts +113 -0
  103. package/apps/storefront/src/lib/dev/ai-widget-client.js +127 -0
  104. package/apps/storefront/src/lib/dev/cliSignInCode.ts +132 -0
  105. package/apps/storefront/src/lib/diag/redact.ts +20 -0
  106. package/apps/storefront/src/lib/edgeCache.ts +128 -0
  107. package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +62 -0
  108. package/apps/storefront/src/lib/env.ts +60 -0
  109. package/apps/storefront/src/lib/fixtures/sampleProducts.ts +130 -0
  110. package/apps/storefront/src/lib/format.ts +29 -0
  111. package/apps/storefront/src/lib/foxyCommerce.ts +49 -0
  112. package/apps/storefront/src/lib/imageAttrs.ts +55 -0
  113. package/apps/storefront/src/lib/jsonld.ts +136 -0
  114. package/apps/storefront/src/lib/maintenance.ts +71 -0
  115. package/apps/storefront/src/lib/memoryKv.ts +32 -0
  116. package/apps/storefront/src/lib/messaging/messagesClient.ts +143 -0
  117. package/apps/storefront/src/lib/placeholder.ts +99 -0
  118. package/apps/storefront/src/lib/previewReload.ts +125 -0
  119. package/apps/storefront/src/lib/rawMarketingHtml.ts +133 -0
  120. package/apps/storefront/src/lib/search/index.ts +74 -0
  121. package/apps/storefront/src/lib/sections.ts +129 -0
  122. package/apps/storefront/src/lib/securityHeaders.ts +31 -0
  123. package/apps/storefront/src/lib/storyblok/content-model.ts +289 -0
  124. package/apps/storefront/src/lib/storyblok/provider.ts +524 -0
  125. package/apps/storefront/src/lib/tenantRouting.ts +91 -0
  126. package/apps/storefront/src/lib/tot/ToTClient.ts +177 -0
  127. package/apps/storefront/src/lib/tot/d1Client.ts +62 -0
  128. package/apps/storefront/src/lib/tot/query.ts +187 -0
  129. package/apps/storefront/src/lib/tot/totClientInterface.ts +30 -0
  130. package/apps/storefront/src/lib/url.ts +119 -0
  131. package/apps/storefront/src/lib/wishlist.ts +63 -0
  132. package/apps/storefront/src/middleware/index.ts +413 -0
  133. package/apps/storefront/src/pages/404.astro +61 -0
  134. package/apps/storefront/src/pages/[...slug].astro +90 -0
  135. package/apps/storefront/src/pages/api/auth/logout.ts +21 -0
  136. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +107 -0
  137. package/apps/storefront/src/pages/api/auth/send.ts +85 -0
  138. package/apps/storefront/src/pages/api/auth/verify.ts +135 -0
  139. package/apps/storefront/src/pages/api/cache-purge.ts +44 -0
  140. package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +64 -0
  141. package/apps/storefront/src/pages/api/csp-report.ts +25 -0
  142. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +124 -0
  143. package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +120 -0
  144. package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +44 -0
  145. package/apps/storefront/src/pages/api/newsletter.ts +104 -0
  146. package/apps/storefront/src/pages/api/request-access.ts +172 -0
  147. package/apps/storefront/src/pages/auth/login.astro +149 -0
  148. package/apps/storefront/src/pages/auth/magic.astro +105 -0
  149. package/apps/storefront/src/pages/capabilities.astro +292 -0
  150. package/apps/storefront/src/pages/collections/[handle].astro +154 -0
  151. package/apps/storefront/src/pages/collections/index.astro +59 -0
  152. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +100 -0
  153. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +154 -0
  154. package/apps/storefront/src/pages/dashboard/index.astro +160 -0
  155. package/apps/storefront/src/pages/dev.astro +653 -0
  156. package/apps/storefront/src/pages/index.astro +290 -0
  157. package/apps/storefront/src/pages/llms.txt.ts +92 -0
  158. package/apps/storefront/src/pages/products/[handle].astro +443 -0
  159. package/apps/storefront/src/pages/robots.txt.ts +24 -0
  160. package/apps/storefront/src/pages/saved.astro +35 -0
  161. package/apps/storefront/src/pages/search.astro +150 -0
  162. package/apps/storefront/src/pages/sitemap.xml.ts +78 -0
  163. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +646 -0
  164. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +85 -0
  165. package/apps/storefront/src/pages/style-guide/index.astro +94 -0
  166. package/apps/storefront/src/pages/tenants/[id]/[...path].ts +57 -0
  167. package/apps/storefront/src/styles/global.css +277 -0
  168. package/apps/storefront/src/themes/reference.ts +61 -0
  169. package/apps/storefront/src/themes/schema.ts +111 -0
  170. package/apps/storefront/src/themes/tenants/index.ts +51 -0
  171. package/apps/storefront/tsconfig.json +16 -0
  172. package/apps/storefront/vitest.config.ts +22 -0
  173. package/apps/storefront/wrangler.toml +175 -0
  174. package/package.json +23 -0
  175. package/packages/public-runtime/LICENSE +58 -0
  176. package/packages/public-runtime/package.json +22 -0
  177. package/packages/public-runtime/src/binary-path.ts +68 -0
  178. package/packages/public-runtime/src/capabilities.ts +89 -0
  179. package/packages/public-runtime/src/catalog-api.ts +81 -0
  180. package/packages/public-runtime/src/catalog-d1.ts +192 -0
  181. package/packages/public-runtime/src/csp.ts +109 -0
  182. package/packages/public-runtime/src/customization-preview.ts +197 -0
  183. package/packages/public-runtime/src/customization-reconcile.ts +63 -0
  184. package/packages/public-runtime/src/customization-runtime.ts +157 -0
  185. package/packages/public-runtime/src/customization-scripts.ts +64 -0
  186. package/packages/public-runtime/src/customization-versioning.ts +95 -0
  187. package/packages/public-runtime/src/foxy.ts +184 -0
  188. package/packages/public-runtime/src/index.ts +30 -0
  189. package/packages/public-runtime/src/product.ts +159 -0
  190. package/packages/public-runtime/src/search.ts +45 -0
  191. package/packages/public-runtime/src/tenant-assets.ts +444 -0
  192. package/packages/public-runtime/src/tenant.ts +243 -0
  193. package/packages/public-runtime/tsconfig.json +8 -0
  194. package/packages/public-runtime/vitest.config.ts +8 -0
  195. package/pnpm-workspace.yaml +7 -0
  196. package/scripts/build/copy-tenant-assets.mjs +69 -0
  197. package/scripts/tot-dev.mjs +439 -0
  198. package/tenants/bigdvapor/content/blog.json +52 -0
  199. package/tenants/bigdvapor/content/chrome.html +149 -0
  200. package/tenants/bigdvapor/content/chrome.json +47 -0
  201. package/tenants/bigdvapor/content/home.html +463 -0
  202. package/tenants/bigdvapor/content/home.json +73 -0
  203. package/tenants/bigdvapor/content/pages/about.json +5 -0
  204. package/tenants/bigdvapor/content/pages/privacy.json +6 -0
  205. package/tenants/bigdvapor/content/pages/shipping-returns.json +6 -0
  206. package/tenants/bigdvapor/content/pages-html/blogs/news.html +86 -0
  207. package/tenants/bigdvapor/content/pages-html/pages/about-us.html +106 -0
  208. package/tenants/bigdvapor/content/pages-html/pages/contact-us.html +61 -0
  209. package/tenants/bigdvapor/content/pages-html/pages/privacy-policy.html +72 -0
  210. package/tenants/bigdvapor/content/pages-html/pages/shipping-returns.html +172 -0
  211. package/tenants/bigdvapor/content/themes/bigd-navy.json +109 -0
  212. package/tenants/bigdvapor/public/img/brands/adjust-vape-disposable.png +0 -0
  213. package/tenants/bigdvapor/public/img/brands/again-vape-disposable.png +0 -0
  214. package/tenants/bigdvapor/public/img/brands/al-fakher-vape-disposable.png +0 -0
  215. package/tenants/bigdvapor/public/img/brands/arro-vape-disposable.png +0 -0
  216. package/tenants/bigdvapor/public/img/brands/cookies-vape-disposable.png +0 -0
  217. package/tenants/bigdvapor/public/img/brands/core-vape-disposable.png +0 -0
  218. package/tenants/bigdvapor/public/img/brands/cyclone-vape-disposable.png +0 -0
  219. package/tenants/bigdvapor/public/img/brands/dinner-lady-vape-disposable.png +0 -0
  220. package/tenants/bigdvapor/public/img/brands/extre-bar-vape-disposable.png +0 -0
  221. package/tenants/bigdvapor/public/img/brands/fifty-bar-vape-disposable.jpg +0 -0
  222. package/tenants/bigdvapor/public/img/brands/firerose-vape-disposable.jpg +0 -0
  223. package/tenants/bigdvapor/public/img/brands/flavor-beast-vape-disposable.jpg +0 -0
  224. package/tenants/bigdvapor/public/img/brands/foger-vape-disposable.png +0 -0
  225. package/tenants/bigdvapor/public/img/brands/kk-energy-vape-disposable.png +0 -0
  226. package/tenants/bigdvapor/public/img/brands/kumi-vape-disposable.png +0 -0
  227. package/tenants/bigdvapor/public/img/brands/lost-vape-dispoable.png +0 -0
  228. package/tenants/bigdvapor/public/img/brands/melo-labs-vape-disposable.png +0 -0
  229. package/tenants/bigdvapor/public/img/brands/mixo-vape-disposable.webp +0 -0
  230. package/tenants/bigdvapor/public/img/brands/one-tank-vape-disposable.jpg +0 -0
  231. package/tenants/bigdvapor/public/img/brands/oxbar-vape-disposable.webp +0 -0
  232. package/tenants/bigdvapor/public/img/brands/pillow-talk-vape-disposable.jpg +0 -0
  233. package/tenants/bigdvapor/public/img/brands/pod-king-vape-disposable.jpg +0 -0
  234. package/tenants/bigdvapor/public/img/brands/raz-vape-disposable.png +0 -0
  235. package/tenants/bigdvapor/public/img/hero-texas.jpg +0 -0
  236. package/tenants/bigdvapor/public/img/og.jpg +0 -0
  237. package/tenants/bigdvapor/public/img/pages/AdobeStock_202106312_web.jpg +0 -0
  238. package/tenants/bigdvapor/public/img/pages/CREW_web.jpg +0 -0
  239. package/tenants/bigdvapor/public/img/pages/Hero-Image.jpg +0 -0
  240. package/tenants/bigdvapor/public/img/tiles/adjust.png +0 -0
  241. package/tenants/bigdvapor/public/img/tiles/again.png +0 -0
  242. package/tenants/bigdvapor/public/img/tiles/al-fakher.png +0 -0
  243. package/tenants/bigdvapor/public/img/tiles/arro.png +0 -0
  244. package/tenants/bigdvapor/public/img/tiles/cookies.png +0 -0
  245. package/tenants/bigdvapor/public/img/tiles/core.png +0 -0
  246. package/tenants/bigdvapor/public/img/tiles/cyclone.png +0 -0
  247. package/tenants/bigdvapor/public/img/tiles/disposables.svg +23 -0
  248. package/tenants/bigdvapor/public/logo-wordmark.png +0 -0
  249. package/tenants/bigdvapor/public/pages/home.css +118 -0
  250. package/tenants/bigdvapor/public/pages/mkt.css +185 -0
  251. package/tenants/bigdvapor/public/pages/page.css +148 -0
  252. package/tenants/bigdvapor/public/themes/bigd-navy.css +73 -0
  253. package/tenants/bigdvapor/theme.json +12 -0
  254. package/tenants/giantvapes/content/chrome.html +152 -0
  255. package/tenants/giantvapes/content/chrome.json +94 -0
  256. package/tenants/giantvapes/content/home.html +194 -0
  257. package/tenants/giantvapes/content/home.json +50 -0
  258. package/tenants/giantvapes/content/pages/about.json +10 -0
  259. package/tenants/giantvapes/content/pages/privacy.json +6 -0
  260. package/tenants/giantvapes/content/pages/shipping-returns.json +6 -0
  261. package/tenants/giantvapes/content/pages-html/blogs/news.html +68 -0
  262. package/tenants/giantvapes/content/pages-html/pages/about-us.html +95 -0
  263. package/tenants/giantvapes/content/pages-html/pages/contact-us.html +68 -0
  264. package/tenants/giantvapes/content/pages-html/pages/privacy-policy.html +65 -0
  265. package/tenants/giantvapes/content/pages-html/pages/shipping-returns.html +77 -0
  266. package/tenants/giantvapes/content/themes/giant-navy.json +73 -0
  267. package/tenants/giantvapes/public/img/hero-suicide-bunny.jpg +0 -0
  268. package/tenants/giantvapes/public/img/hero.webp +0 -0
  269. package/tenants/giantvapes/public/img/og.jpg +0 -0
  270. package/tenants/giantvapes/public/logo-wordmark.png +0 -0
  271. package/tenants/giantvapes/public/pages/home.css +120 -0
  272. package/tenants/giantvapes/public/pages/mkt.css +185 -0
  273. package/tenants/giantvapes/public/pages/page.css +155 -0
  274. package/tenants/giantvapes/public/themes/giant-navy.css +76 -0
  275. package/tenants/giantvapes/theme.json +39 -0
  276. package/tenants/home/content/chrome.json +11 -0
  277. package/tenants/home/content/home.html +304 -0
  278. package/tenants/home/content/home.json +13 -0
  279. package/tenants/home/public/js/storefront.js +210 -0
  280. package/tenants/home/public/logo-wordmark.png +0 -0
  281. package/tenants/home/public/pages/storefront.css +212 -0
  282. package/tenants/home/theme.json +6 -0
  283. package/tenants/tokenoftrust/.tot/config.json +9 -0
  284. package/tenants/tokenoftrust/content/chrome.json +55 -0
  285. package/tenants/tokenoftrust/content/home.html +723 -0
  286. package/tenants/tokenoftrust/content/home.json +233 -0
  287. package/tenants/tokenoftrust/content/pages-html/careers.html +231 -0
  288. package/tenants/tokenoftrust/content/pages-html/company.html +275 -0
  289. package/tenants/tokenoftrust/content/pages-html/contact/contact-sales.html +188 -0
  290. package/tenants/tokenoftrust/content/pages-html/contact/startup-apply.html +206 -0
  291. package/tenants/tokenoftrust/content/pages-html/contact.html +178 -0
  292. package/tenants/tokenoftrust/content/pages-html/industries/adult-content-age-verification.html +286 -0
  293. package/tenants/tokenoftrust/content/pages-html/industries/cannabis.html +277 -0
  294. package/tenants/tokenoftrust/content/pages-html/pact-act-pricing.html +289 -0
  295. package/tenants/tokenoftrust/content/pages-html/pricing.html +483 -0
  296. package/tenants/tokenoftrust/content/pages-html/product/age-assurance/age-estimation.html +278 -0
  297. package/tenants/tokenoftrust/content/pages-html/product/age-assurance/age-gate.html +279 -0
  298. package/tenants/tokenoftrust/content/pages-html/product/age-assurance/age-verification.html +265 -0
  299. package/tenants/tokenoftrust/content/pages-html/product/age-assurance.html +173 -0
  300. package/tenants/tokenoftrust/content/pages-html/product/compliance/aml.html +214 -0
  301. package/tenants/tokenoftrust/content/pages-html/product/compliance/pact-act.html +226 -0
  302. package/tenants/tokenoftrust/content/pages-html/product/fraud-prevention.html +254 -0
  303. package/tenants/tokenoftrust/content/pages-html/product/social-profile-verification.html +212 -0
  304. package/tenants/tokenoftrust/content/pages-html/product/tax/excise-tax.html +213 -0
  305. package/tenants/tokenoftrust/content/pages-html/product/verification/aml.html +259 -0
  306. package/tenants/tokenoftrust/content/pages-html/product/verification/biometric-selfie.html +305 -0
  307. package/tenants/tokenoftrust/content/pages-html/product/verification/coverage.html +254 -0
  308. package/tenants/tokenoftrust/content/pages-html/product/verification/document-verification.html +306 -0
  309. package/tenants/tokenoftrust/content/pages-html/product/verification/electronic-identity-verification.html +304 -0
  310. package/tenants/tokenoftrust/content/pages-html/product/verification/government-id-verification.html +308 -0
  311. package/tenants/tokenoftrust/content/pages-html/product/verification.html +183 -0
  312. package/tenants/tokenoftrust/content/pages-html/product.html +228 -0
  313. package/tenants/tokenoftrust/content/pages-html/resources/integrations/bigcommerce-integration.html +220 -0
  314. package/tenants/tokenoftrust/content/pages-html/resources/integrations/shopify-integration.html +221 -0
  315. package/tenants/tokenoftrust/content/pages-html/resources/integrations/wordpress.html +220 -0
  316. package/tenants/tokenoftrust/content/pages-html/resources/integrations.html +172 -0
  317. package/tenants/tokenoftrust/content/pages-html/resources/refer-a-friend-program.html +317 -0
  318. package/tenants/tokenoftrust/content/pages-html/resources.html +177 -0
  319. package/tenants/tokenoftrust/content/pages-html/reviews.html +273 -0
  320. package/tenants/tokenoftrust/content/pages-html/solutions.html +292 -0
  321. package/tenants/tokenoftrust/content/pages-html/vlp-alcohol.html +289 -0
  322. package/tenants/tokenoftrust/content/pages-html/vlp-cigars-and-tobacco.html +286 -0
  323. package/tenants/tokenoftrust/content/pages-html/vlp-firearm-accessories.html +267 -0
  324. package/tenants/tokenoftrust/content/pages-html/vlp-peptides.html +273 -0
  325. package/tenants/tokenoftrust/content/themes/tot-navy.json +88 -0
  326. package/tenants/tokenoftrust/public/favicon.png +0 -0
  327. package/tenants/tokenoftrust/public/logo-icon.png +0 -0
  328. package/tenants/tokenoftrust/public/logo-wordmark.png +0 -0
  329. package/tenants/tokenoftrust/public/pages/company.css +108 -0
  330. package/tenants/tokenoftrust/public/pages/home.css +457 -0
  331. package/tenants/tokenoftrust/public/pages/mkt.css +482 -0
  332. package/tenants/tokenoftrust/public/pages/pricing.css +214 -0
  333. package/tenants/tokenoftrust/public/pages/solutions.css +66 -0
  334. package/tenants/tokenoftrust/public/themes/tot-navy-1.0.0.css +136 -0
  335. package/tenants/tokenoftrust/public/themes/tot-navy.css +358 -0
  336. package/tenants/tokenoftrust/theme.json +31 -0
  337. package/tsconfig.base.json +20 -0
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Deterministic local placeholder art (brief: images note).
3
+ *
4
+ * Fixture image URLs are Shopify-CDN paths that 404 and we ship offline. Rather
5
+ * than a grey box, we synthesize a tasteful, aspect-correct SVG keyed by a
6
+ * stable seed (image id or product handle) so each product gets a consistent,
7
+ * on-brand gradient + monogram. The <Img> component swaps these out for real
8
+ * rehosted URLs when CatalogImage.rehosted === true.
9
+ *
10
+ * Palette is pulled from the theme's CSS vars at render time where possible, but
11
+ * the SVG is self-contained (inlines colors) so it works as a data URI / <img src>.
12
+ */
13
+
14
+ // Quiet, on-brand duotone pairs (spruce/greige/amber family). Picked by seed.
15
+ const PAIRS: Array<[string, string]> = [
16
+ ["#243B35", "#C9C2B2"], // spruce -> sand
17
+ ["#33514A", "#E3DCCB"], // pine -> paper
18
+ ["#1F3A34", "#B5612F"], // anchor -> amber
19
+ ["#3A4A45", "#D8D1C2"], // slate-sage -> light sand
20
+ ["#2C3E39", "#A88B5C"], // deep green -> ochre
21
+ ["#1C2624", "#7E8B86"], // ink -> stone
22
+ ];
23
+
24
+ function hash(seed: string): number {
25
+ let h = 2166136261;
26
+ for (let i = 0; i < seed.length; i++) {
27
+ h ^= seed.charCodeAt(i);
28
+ h = Math.imul(h, 16777619);
29
+ }
30
+ return Math.abs(h);
31
+ }
32
+
33
+ export interface PlaceholderOpts {
34
+ seed: string;
35
+ width: number;
36
+ height: number;
37
+ /** Short label drawn faintly (e.g. product initials). */
38
+ label?: string;
39
+ }
40
+
41
+ /** Returns a complete SVG string (aspect-correct, deterministic). */
42
+ export function placeholderSvg({ seed, width, height, label }: PlaceholderOpts): string {
43
+ const h = hash(seed);
44
+ const pair = PAIRS[h % PAIRS.length] ?? PAIRS[0]!;
45
+ const [a, b] = pair;
46
+ const angle = (h % 6) * 30; // 0..150deg
47
+ const cx = 20 + (h % 60);
48
+ const cy = 20 + ((h >> 3) % 60);
49
+ const initials = (label ?? seed)
50
+ .split(/[\s-]+/)
51
+ .map((w) => w[0])
52
+ .filter(Boolean)
53
+ .slice(0, 2)
54
+ .join("")
55
+ .toUpperCase();
56
+
57
+ // Two soft radial blooms over a linear base — reads as considered, not noisy.
58
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-hidden="true">
59
+ <defs>
60
+ <linearGradient id="g" gradientTransform="rotate(${angle})">
61
+ <stop offset="0%" stop-color="${a}"/>
62
+ <stop offset="100%" stop-color="${b}"/>
63
+ </linearGradient>
64
+ <radialGradient id="r" cx="${cx}%" cy="${cy}%" r="70%">
65
+ <stop offset="0%" stop-color="${b}" stop-opacity="0.55"/>
66
+ <stop offset="100%" stop-color="${a}" stop-opacity="0"/>
67
+ </radialGradient>
68
+ </defs>
69
+ <rect width="100%" height="100%" fill="url(#g)"/>
70
+ <rect width="100%" height="100%" fill="url(#r)"/>
71
+ <text x="50%" y="50%" dy="0.35em" text-anchor="middle"
72
+ font-family="'Space Grotesk', system-ui, sans-serif"
73
+ font-size="${Math.round(Math.min(width, height) * 0.16)}"
74
+ font-weight="600" letter-spacing="0.04em"
75
+ fill="${b}" fill-opacity="0.22">${initials || "•"}</text>
76
+ </svg>`;
77
+ }
78
+
79
+ /** Base64 data URI form (for <img src> and OG images). */
80
+ export function placeholderDataUri(opts: PlaceholderOpts): string {
81
+ const svg = placeholderSvg(opts);
82
+ // btoa is available in Workers + browsers; encodeURIComponent path is the
83
+ // SSR-safe fallback that also handles non-latin labels.
84
+ const encoded = encodeURIComponent(svg)
85
+ .replace(/'/g, "%27")
86
+ .replace(/"/g, "%22");
87
+ return `data:image/svg+xml,${encoded}`;
88
+ }
89
+
90
+ /** True if a CatalogImage URL is usable vs a dead/synthetic fixture path. */
91
+ export function isUsableImageUrl(url: string | undefined, rehosted?: boolean): boolean {
92
+ if (!url) return false;
93
+ if (rehosted) return true;
94
+ // The synthetic apparel fixtures use a fake shop id (…/s/files/1/0001/…) whose
95
+ // URLs 404 — draw placeholders for those. REAL store CDN URLs (e.g. a live POC
96
+ // migration) are usable as-is.
97
+ if (/\/s\/files\/1\/0001\//.test(url)) return false;
98
+ return /^https?:\/\//.test(url);
99
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * On-publish auto-refresh for the SHARED preview (reviewer/owner "it updates
3
+ * itself" experience). NOT the developer's local `tot dev` HMR loop — this is the
4
+ * shared `test`-env preview that others review.
5
+ *
6
+ * A tiny live-reload client is injected (by src/middleware) into `test`-env HTML
7
+ * pages ONLY — never `live`. It polls `GET /__preview/version?tenant=<id>` (the
8
+ * per-tenant edge cache-version integer, which bumps on every reconcile/publish;
9
+ * see lib/edgeCache.ts + api/preview/reconcile.ts) and reloads the page when the
10
+ * version changes. Poll-based v1; an SSE / Durable-Object PUSH is the later
11
+ * upgrade (TODO below) that removes the ~1s poll latency.
12
+ *
13
+ * CSP: the injected <script> carries the per-request nonce (same one the strict
14
+ * CSP header allows), so it is CSP-clean with no `unsafe-inline` and no inline
15
+ * event handlers.
16
+ */
17
+
18
+ const DEFAULT_POLL_MS = 1000;
19
+
20
+ /** Escape a value for safe embedding inside an inline <script> as JSON. */
21
+ function jsonForScript(value: unknown): string {
22
+ // JSON.stringify covers quoting; escaping `<` defends against a `</script>`
23
+ // (or `<!--`) breakout even though tenant ids / base paths never contain it.
24
+ return JSON.stringify(value).replace(/</g, "\\u003c");
25
+ }
26
+
27
+ /**
28
+ * The live-reload client body (no <script> wrapper). Kept dependency-free and
29
+ * tiny so it is cheap to inline on every preview page.
30
+ */
31
+ export function buildPreviewReloadScript(opts: {
32
+ tenantId: string;
33
+ basePath: string;
34
+ pollMs?: number;
35
+ }): string {
36
+ const tenant = jsonForScript(opts.tenantId);
37
+ const base = jsonForScript(opts.basePath ?? "");
38
+ const pollMs = Math.max(250, Math.floor(opts.pollMs ?? DEFAULT_POLL_MS));
39
+ // The version endpoint is fetched UNDER basePath so the same tenant-routing the
40
+ // page used resolves it (path-routed previews carry a /<domain> prefix).
41
+ return `(() => {
42
+ const TENANT = ${tenant}, BASE = ${base}, POLL_MS = ${pollMs};
43
+ const url = BASE + "/__preview/version?tenant=" + encodeURIComponent(TENANT);
44
+ let known = null;
45
+ const tick = async () => {
46
+ try {
47
+ const res = await fetch(url, { headers: { accept: "application/json" }, cache: "no-store" });
48
+ if (res.ok) {
49
+ const v = (await res.json()).version;
50
+ if (known === null) { known = v; }
51
+ // TODO(theme-hot-swap): when the change is theme-only, swap the CSS custom
52
+ // properties in place (no reload). Needs the reconcile to report a
53
+ // theme-only delta; gated behind this same version signal until then.
54
+ else if (v !== known) { location.reload(); return; }
55
+ }
56
+ } catch (_e) { /* transient — keep polling */ }
57
+ setTimeout(tick, POLL_MS);
58
+ };
59
+ // TODO(push): replace this poll with an SSE / Durable-Object stream so the
60
+ // reload is instant instead of within POLL_MS of a publish.
61
+ setTimeout(tick, POLL_MS);
62
+ })();`;
63
+ }
64
+
65
+ /** True when a response is HTML we can inject into. */
66
+ export function isHtmlResponse(response: Response): boolean {
67
+ if (response.status !== 200) return false;
68
+ return (response.headers.get("content-type") ?? "").includes("text/html");
69
+ }
70
+
71
+ /**
72
+ * Inject the live-reload <script> into an HTML string, before </body> (or
73
+ * appended when there is no </body>). Pure + unit-testable.
74
+ */
75
+ export function injectPreviewReloadHtml(
76
+ html: string,
77
+ opts: { tenantId: string; basePath: string; nonce: string; pollMs?: number },
78
+ ): string {
79
+ const script = `<script nonce="${opts.nonce}">${buildPreviewReloadScript(opts)}</script>`;
80
+ if (/<\/body>/i.test(html)) {
81
+ return html.replace(/<\/body>/i, `${script}</body>`);
82
+ }
83
+ return html + script;
84
+ }
85
+
86
+ /**
87
+ * Wrap a Response, returning a new one with the reload client injected. No-op
88
+ * (returns the original) for non-HTML responses. The caller must have already
89
+ * decided this is a `test`-env response — this helper does not check the env.
90
+ */
91
+ export async function injectPreviewReload(
92
+ response: Response,
93
+ opts: { tenantId: string; basePath: string; nonce: string; pollMs?: number },
94
+ ): Promise<Response> {
95
+ if (!isHtmlResponse(response)) return response;
96
+ const html = await response.text();
97
+ const injected = injectPreviewReloadHtml(html, opts);
98
+ // Preserve status + headers; the body length changed but we don't set a manual
99
+ // Content-Length anywhere (Workers/Astro stream), so no header fix-up needed.
100
+ return new Response(injected, {
101
+ status: response.status,
102
+ statusText: response.statusText,
103
+ headers: response.headers,
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Env-gated entrypoint the middleware calls: inject the reload client ONLY for
109
+ * the SHARED preview (`test`) — NEVER `live`. Returns the response unchanged for
110
+ * `live` (or any non-HTML response). This is the single guardrail seam, so the
111
+ * "present in test, absent in live" contract is unit-testable here.
112
+ */
113
+ export async function maybeInjectPreviewReload(
114
+ response: Response,
115
+ opts: {
116
+ env: "live" | "test";
117
+ tenantId: string;
118
+ basePath: string;
119
+ nonce: string;
120
+ pollMs?: number;
121
+ },
122
+ ): Promise<Response> {
123
+ if (opts.env !== "test") return response;
124
+ return injectPreviewReload(response, opts);
125
+ }
@@ -0,0 +1,133 @@
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
+ * `/tenants/…` — those are GLOBAL static assets (logo, favicon) served at root under
78
+ * both routing modes, so they must NOT get the tenant prefix. Asset `src="…"` isn't
79
+ * matched at all (href-only).
80
+ */
81
+ export function prefixTenantLinks(html: string, basePath: string): string {
82
+ if (!basePath) return html;
83
+ return html.replace(/href="\/(?!\/|tenants\/)/g, `href="${basePath}/`);
84
+ }
85
+
86
+ /**
87
+ * Build the HTML Response for a raw marketing page (nonce + base-path).
88
+ *
89
+ * Shared-chrome mechanism: a page is served in one of two ways —
90
+ * - FULL DOCUMENT (has <html>/doctype): served verbatim, as before.
91
+ * - BODY FRAGMENT + a tenant `chromeWrapper`: the fragment is spliced into the
92
+ * wrapper's CHROME_BODY_MARKER so chrome lives in ONE place, not copied into
93
+ * every page. A fragment with no wrapper still serves (unwrapped) for safety.
94
+ * Either way, the FINAL document gets nonce-stamped and base-path-prefixed, so the
95
+ * chrome's own inline <style>/<script> and root-absolute nav links are handled too.
96
+ */
97
+ export function rawHtmlResponse(
98
+ html: string,
99
+ opts: { nonce: string; basePath: string; chromeWrapper?: string | null },
100
+ ): Response {
101
+ let composed =
102
+ isFullDocument(html) || !opts.chromeWrapper
103
+ ? html
104
+ : wrapFragmentInChrome(html, opts.chromeWrapper);
105
+ // DEV ONLY: these pages are served verbatim, bypassing Astro's render pipeline,
106
+ // so the Vite HMR client is never injected — a save would reach the server (see
107
+ // the tenantHotReload plugin in astro.config.mjs) but the browser would have no
108
+ // listener to act on the full-reload. Inject the client so save→reload works.
109
+ // Stamped with the page nonce by nonceInlineTags below; tree-shaken from prod.
110
+ if (import.meta.env.DEV) {
111
+ const client = `<script type="module" src="/@vite/client"></script>`;
112
+ composed = composed.includes("</head>")
113
+ ? composed.replace("</head>", `${client}</head>`)
114
+ : `${client}${composed}`;
115
+ // Inline-AI edit loop's floating "✦ ask" affordance (WS5 G1) — same reason
116
+ // as the HMR client above, these pages bypass the Astro pipeline that
117
+ // inlineAiEditPlugin's transformIndexHtml hook (astro.config.mjs) covers
118
+ // for everything else, so it's injected here too. Unconditional in dev,
119
+ // matching the Vite client above: whether `tot dev` set an active tenant
120
+ // (TOT_DEV_TENANT) is a Node-process env var, invisible to this SSR code
121
+ // when it runs inside the Cloudflare/workerd dev runtime (see tot-dev.mjs's
122
+ // own note on this exact gap) — so the gate lives server-side instead, in
123
+ // inlineAiEditPlugin's Vite middleware, which DOES see the real env.
124
+ const widget = `<script type="module" src="/__tot/ai-widget-client.js"></script>`;
125
+ composed = composed.includes("</body>")
126
+ ? composed.replace("</body>", `${widget}</body>`)
127
+ : `${composed}${widget}`;
128
+ }
129
+ const doc = prefixTenantLinks(nonceInlineTags(composed, opts.nonce), opts.basePath);
130
+ return new Response(doc, {
131
+ headers: { "content-type": "text/html; charset=utf-8" },
132
+ });
133
+ }
@@ -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
+ }