@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,87 @@
1
+ /**
2
+ * KV-backed tenant resolver — the many-tenant path documented in
3
+ * src/config/tenants.ts and wrangler.toml ([[kv_namespaces]] TENANT_CACHE).
4
+ *
5
+ * It implements the SAME `TenantResolver` interface as StaticTenantResolver, so
6
+ * middleware, pages, and the /<domain>/* path routing are untouched — only the
7
+ * backing store changes. Tenants are added by WRITING to KV (see putTenant),
8
+ * not by editing code, so onboarding/migration can register a store at runtime.
9
+ *
10
+ * KV layout:
11
+ * domain:<host> -> tenant_id (one per claimed hostname)
12
+ * tenant:<tenant_id> -> JSON TenantConfig
13
+ * config:default_tenant -> tenant_id (unknown-host fallback)
14
+ *
15
+ * A static `fallback` resolver is consulted when KV has no entry, so the system
16
+ * keeps working before KV is seeded and during a gradual cutover from static.
17
+ */
18
+ import type { TenantConfig, TenantResolver } from "@tot/public-runtime";
19
+
20
+ const DOMAIN_PREFIX = "domain:";
21
+ const TENANT_PREFIX = "tenant:";
22
+ const DEFAULT_KEY = "config:default_tenant";
23
+ // Edge-cache KV reads briefly; tenant config changes are rare and propagate
24
+ // within this window. Bump down if you need faster propagation after a write.
25
+ const CACHE_TTL = 300;
26
+
27
+ function cleanHost(host: string): string {
28
+ return host.toLowerCase().split(":")[0]?.trim() ?? "";
29
+ }
30
+
31
+ export class KvTenantResolver implements TenantResolver {
32
+ constructor(
33
+ private readonly kv: KVNamespace,
34
+ /** Consulted when KV has no record (pre-seed / gradual cutover). */
35
+ private readonly fallback?: TenantResolver,
36
+ ) {}
37
+
38
+ private async readTenant(id: string): Promise<TenantConfig | null> {
39
+ const raw = await this.kv.get(TENANT_PREFIX + id, { cacheTtl: CACHE_TTL });
40
+ if (!raw) return null;
41
+ try {
42
+ return JSON.parse(raw) as TenantConfig;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ async resolveStrict(host: string): Promise<TenantConfig | null> {
49
+ const clean = cleanHost(host);
50
+ if (!clean) return null;
51
+ const id = await this.kv.get(DOMAIN_PREFIX + clean, { cacheTtl: CACHE_TTL });
52
+ if (id) {
53
+ const tenant = await this.readTenant(id);
54
+ if (tenant) return tenant;
55
+ }
56
+ return this.fallback ? this.fallback.resolveStrict(host) : null;
57
+ }
58
+
59
+ async resolveByHost(host: string): Promise<TenantConfig | null> {
60
+ const strict = await this.resolveStrict(host);
61
+ if (strict) return strict;
62
+ const defaultId = await this.kv.get(DEFAULT_KEY, { cacheTtl: CACHE_TTL });
63
+ if (defaultId) {
64
+ const tenant = await this.readTenant(defaultId);
65
+ if (tenant) return tenant;
66
+ }
67
+ return this.fallback ? this.fallback.resolveByHost(host) : null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Register (or update) a tenant in KV. Idempotent: writes the config blob and a
73
+ * domain->id index entry for each claimed hostname. Pass `makeDefault` to point
74
+ * the unknown-host fallback at this tenant. Call this from onboarding/migration.
75
+ */
76
+ export async function putTenant(
77
+ kv: KVNamespace,
78
+ config: TenantConfig,
79
+ opts: { makeDefault?: boolean } = {},
80
+ ): Promise<void> {
81
+ await kv.put(TENANT_PREFIX + config.tenant_id, JSON.stringify(config));
82
+ for (const domain of config.domains) {
83
+ const host = cleanHost(domain);
84
+ if (host) await kv.put(DOMAIN_PREFIX + host, config.tenant_id);
85
+ }
86
+ if (opts.makeDefault) await kv.put(DEFAULT_KEY, config.tenant_id);
87
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Tenant-resolver factory. Picks the backing store at request time:
3
+ *
4
+ * - TENANT_CACHE KV bound (prod / wired dev) -> KvTenantResolver over real KV.
5
+ * - No KV binding (plain `astro dev`) -> KvTenantResolver over an
6
+ * in-memory KV seeded from the static registry, so the dynamic path and a
7
+ * data-driven default are exercised locally without a Cloudflare account.
8
+ *
9
+ * Either way the caller gets a TenantResolver and nothing downstream changes.
10
+ * The static registry is passed as a fallback so unseeded hosts still resolve.
11
+ *
12
+ * The unknown-host default is data-driven: in prod it's the KV
13
+ * `config:default_tenant` key (set via putTenant({makeDefault:true})); in dev
14
+ * it's seeded from DEFAULT_TENANT_ID (falling back to the first static tenant).
15
+ * Changing that value flips which tenant a bare/unknown host serves — proof the
16
+ * default is configurable, not hardcoded.
17
+ */
18
+ import type { TenantResolver } from "@tot/public-runtime";
19
+ import { readEnv, readKv } from "@/lib/env";
20
+ import { tenantResolver as staticResolver, staticTenants } from "@/config/tenants";
21
+ import { KvTenantResolver, putTenant } from "@/config/kvTenantResolver";
22
+ import { MemoryKv } from "@/lib/memoryKv";
23
+
24
+ let devKvPromise: Promise<KVNamespace> | null = null;
25
+
26
+ /** Build (once) an in-memory KV seeded from the static registry for dev. */
27
+ function devKv(): Promise<KVNamespace> {
28
+ if (!devKvPromise) {
29
+ devKvPromise = (async () => {
30
+ const kv = new MemoryKv();
31
+ const configured = (await readEnv("DEFAULT_TENANT_ID"))?.trim();
32
+ const defaultId = configured || staticTenants[0]?.tenant_id;
33
+ for (const tenant of staticTenants) {
34
+ await putTenant(kv, tenant, { makeDefault: tenant.tenant_id === defaultId });
35
+ }
36
+ return kv;
37
+ })();
38
+ }
39
+ return devKvPromise;
40
+ }
41
+
42
+ export async function getTenantResolver(): Promise<TenantResolver> {
43
+ const kv = await readKv("TENANT_CACHE");
44
+ if (kv) return new KvTenantResolver(kv, staticResolver);
45
+ return new KvTenantResolver(await devKv(), staticResolver);
46
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Static tenant registry (Phase 1).
3
+ *
4
+ * ASSUMPTION: one pilot tenant lives in code here — when we go many-tenant this
5
+ * map moves to the TENANT_CACHE KV namespace (wrangler.toml) and the resolver
6
+ * gains an async KV read; the StaticTenantResolver interface stays identical so
7
+ * pages/middleware don't change. — to change: implement KvTenantResolver and
8
+ * select it by env in middleware.
9
+ */
10
+ import type { TenantConfig, TenantResolver } from "@tot/public-runtime";
11
+ import { getTenantTheme } from "@/themes/tenants";
12
+
13
+ // Default landing tenant — the fallback for unmatched hosts / bare localhost.
14
+ // Placeholder "coming soon" content in content/home/* + a minimal theme; a real
15
+ // homepage is built out later. This replaces the old "pilot" fixture tenant.
16
+ // PRODUCT INTENT: "/" should route to the logged-in user's home — until that
17
+ // auth-aware routing lands, an unmatched host falls back HERE, never to a fake
18
+ // merchant. Registered first in REGISTRY so it is the resolver default (registry[0]).
19
+ export const HOME_TENANT: TenantConfig = {
20
+ tenant_id: "home",
21
+ appDomain: "home",
22
+ domains: ["home", "tot-storefront.workers.dev"],
23
+ canonicalDomain: "home",
24
+ displayName: "Storefront",
25
+ storyblok_space_token:
26
+ import.meta.env.STORYBLOK_HOME_TOKEN ?? "STORYBLOK_HOME_DELIVERY_TOKEN_PLACEHOLDER",
27
+ tot_data_scope: "home",
28
+ currency: "USD",
29
+ locale: "en-US",
30
+ // Per-tenant theme override from src/themes/tenants/<tenant_id>.theme.json
31
+ // (auto-discovered). Empty {} falls back to the reference theme.
32
+ theme_tokens: getTenantTheme("home"),
33
+ // Non-commerce placeholder: no catalog/compliance until the real home is built.
34
+ siteType: "marketing",
35
+ };
36
+
37
+ // Second pilot tenant — Big D Vapor (bigdvapor.net). Catalog migrated locally
38
+ // from Shopify (big-d-vapor.myshopify.com) into out/catalog/bigdvapor/; brand
39
+ // adopted into themes/tenants/bigdvapor.theme.json. tot_data_scope "bigdvapor"
40
+ // matches the fixture dir + each record's tenant_id.
41
+ export const BIGDVAPOR_TENANT: TenantConfig = {
42
+ tenant_id: "bigdvapor",
43
+ appDomain: "bigdvapor.net",
44
+ // + bigdvapor.tokenoftrust.store — the per-tenant hosted-store subdomain on our
45
+ // CF zone (reached via the *.tokenoftrust.store wildcard route; ADR-0007).
46
+ domains: ["bigdvapor.net", "www.bigdvapor.net", "bigdvapor.tokenoftrust.store"],
47
+ canonicalDomain: "bigdvapor.net",
48
+ displayName: "Big D Vapor",
49
+ storyblok_space_token:
50
+ import.meta.env.STORYBLOK_BIGDVAPOR_TOKEN ??
51
+ "STORYBLOK_BIGDVAPOR_DELIVERY_TOKEN_PLACEHOLDER",
52
+ tot_data_scope: "bigdvapor",
53
+ currency: "USD",
54
+ locale: "en-US",
55
+ theme_tokens: getTenantTheme("bigdvapor"),
56
+ // Regulated vape/nicotine merchant (21+, PACT) — same compliance shape as pilot.
57
+ compliance: {
58
+ minAge: 21,
59
+ nicotineWarning: true,
60
+ shippingRestriction:
61
+ "Adult signature (21+) required on delivery. We can't ship vapor/nicotine products to some states due to PACT Act and local restrictions — verified at checkout.",
62
+ },
63
+ // Cart/checkout (Track A). Demo build points at the shared ToT demo FoxyCart
64
+ // store (test mode, cart validation OFF → no secret needed, so it runs on
65
+ // localhost AND the shared preview worker). Override the subdomain with
66
+ // FOXY_STORE_SUBDOMAIN; switch mode:"signed" + a real store secret for live.
67
+ // In live each merchant gets its own store + a branded checkout domain via
68
+ // `storeDomain` (e.g. secure.bigdvapor.net) — customer-visible at checkout.
69
+ commerce: {
70
+ foxy: { storeSubDomain: "tot-preview", mode: "unsigned-demo" },
71
+ },
72
+ // PR-gated capabilities (build-time default; overridden per-env by the
73
+ // published capabilities artifact). Regulated vape store → all three on.
74
+ capabilities: {
75
+ cartCheckout: { enabled: true },
76
+ ageVerification: { enabled: true },
77
+ exciseTax: { enabled: true },
78
+ },
79
+ };
80
+
81
+ // Token of Trust's own corporate/marketing site — the first NON-COMMERCE tenant.
82
+ // siteType "marketing" flips the platform's commerce affordances off (no
83
+ // catalog, search, saved, quick-view, product JSON-LD, or commerce sitemap; see
84
+ // resolveTenantFeatures) so the shared component library renders a B2B SaaS site.
85
+ // No `compliance` object: this is the corporate site, not a regulated store.
86
+ // tot_data_scope has no catalog fixtures on purpose — the client returns empty.
87
+ export const TOKENOFTRUST_TENANT: TenantConfig = {
88
+ tenant_id: "tokenoftrust",
89
+ appDomain: "tokenoftrust.com",
90
+ domains: ["tokenoftrust.com", "www.tokenoftrust.com"],
91
+ canonicalDomain: "tokenoftrust.com",
92
+ displayName: "Token of Trust",
93
+ // Marketing content lives locally under src/content/tokenoftrust/*.json; no
94
+ // Storyblok space is wired, so the LocalContentProvider renders it.
95
+ storyblok_space_token:
96
+ import.meta.env.STORYBLOK_TOKENOFTRUST_TOKEN ??
97
+ "STORYBLOK_TOKENOFTRUST_DELIVERY_TOKEN_PLACEHOLDER",
98
+ tot_data_scope: "tokenoftrust",
99
+ currency: "USD",
100
+ locale: "en-US",
101
+ theme_tokens: getTenantTheme("tokenoftrust"),
102
+ siteType: "marketing",
103
+ };
104
+
105
+ // Third pilot tenant — Giant Vapes (giantvapes.com). Online vape shop since 2013,
106
+ // reconstituted from the old "pilot" fixture into the colocated tenants/giantvapes/
107
+ // layout. Brand adopted into tenants/giantvapes/theme.json (giant-navy). Marketing
108
+ // chrome ported HYBRID (raw home.html + pages-html/** + public assets); commerce
109
+ // routes are platform-served. tot_data_scope "giantvapes" seeded with a 50-product
110
+ // sample pulled from the live giantvapes.com Shopify store into
111
+ // out/catalog/giantvapes/ (products + collections), so /collections + /products
112
+ // resolve. Same 21+/PACT compliance shape as bigdvapor.
113
+ export const GIANTVAPES_TENANT: TenantConfig = {
114
+ tenant_id: "giantvapes",
115
+ appDomain: "giantvapes.com",
116
+ // + giantvapes.tokenoftrust.store — per-tenant hosted-store subdomain (see bigdvapor).
117
+ domains: ["giantvapes.com", "www.giantvapes.com", "giantvapes.tokenoftrust.store"],
118
+ canonicalDomain: "giantvapes.com",
119
+ displayName: "Giant Vapes",
120
+ storyblok_space_token:
121
+ import.meta.env.STORYBLOK_GIANTVAPES_TOKEN ??
122
+ "STORYBLOK_GIANTVAPES_DELIVERY_TOKEN_PLACEHOLDER",
123
+ tot_data_scope: "giantvapes",
124
+ currency: "USD",
125
+ locale: "en-US",
126
+ theme_tokens: getTenantTheme("giantvapes"),
127
+ // Regulated vape/nicotine merchant (21+, PACT) — same compliance shape as bigdvapor.
128
+ compliance: {
129
+ minAge: 21,
130
+ nicotineWarning: true,
131
+ shippingRestriction:
132
+ "Adult signature (21+) required on delivery. We can't ship vapor/nicotine products to some states due to PACT Act and local restrictions — verified at checkout.",
133
+ },
134
+ // Cart/checkout (Track A) — shared demo FoxyCart store, see BIGDVAPOR_TENANT.
135
+ commerce: {
136
+ foxy: { storeSubDomain: "tot-preview", mode: "unsigned-demo" },
137
+ },
138
+ // PR-gated capabilities — regulated vape store → all three on.
139
+ capabilities: {
140
+ cartCheckout: { enabled: true },
141
+ ageVerification: { enabled: true },
142
+ exciseTax: { enabled: true },
143
+ },
144
+ };
145
+
146
+ // DEMO (storefront-runner): a renamed variant of an existing tenant — one object.
147
+ // Reuses tokenoftrust's theme + content scope, served at its own path prefix
148
+ // /tokenoftrust-preview.com/. This is the whole edit needed to add a tenant.
149
+ export const TOKENOFTRUST_PREVIEW_TENANT: TenantConfig = {
150
+ ...TOKENOFTRUST_TENANT,
151
+ tenant_id: "tokenoftrust-preview",
152
+ appDomain: "tokenoftrust-preview.com",
153
+ domains: ["tokenoftrust-preview.com"],
154
+ canonicalDomain: "tokenoftrust-preview.com",
155
+ displayName: "Token of Trust (Preview)",
156
+ };
157
+
158
+ const REGISTRY: TenantConfig[] = [
159
+ HOME_TENANT,
160
+ BIGDVAPOR_TENANT,
161
+ TOKENOFTRUST_TENANT,
162
+ GIANTVAPES_TENANT,
163
+ TOKENOFTRUST_PREVIEW_TENANT,
164
+ ];
165
+
166
+ /** The built-in tenants — seed source for the KV store (dev MemoryKv / prod). */
167
+ export const staticTenants: TenantConfig[] = REGISTRY;
168
+
169
+ export class StaticTenantResolver implements TenantResolver {
170
+ constructor(private readonly registry: TenantConfig[] = REGISTRY) {}
171
+
172
+ /** Strict domain match — null when no tenant explicitly claims `host`. */
173
+ async resolveStrict(host: string): Promise<TenantConfig | null> {
174
+ const clean = host.toLowerCase().split(":")[0]?.trim() ?? "";
175
+ if (!clean) return null;
176
+ return (
177
+ this.registry.find((t) =>
178
+ t.domains.some((d) => d.toLowerCase() === clean),
179
+ ) ?? null
180
+ );
181
+ }
182
+
183
+ async resolveByHost(host: string): Promise<TenantConfig | null> {
184
+ const match = await this.resolveStrict(host);
185
+ // Unmatched hosts / bare localhost fall back to the default HOME_TENANT
186
+ // (registry[0]) — the "coming soon" placeholder home, NOT a fake merchant.
187
+ // TODO: when auth-aware routing lands, "/" should go to the logged-in user's
188
+ // home instead of a static default.
189
+ return match ?? this.registry[0] ?? null;
190
+ }
191
+ }
192
+
193
+ export const tenantResolver: TenantResolver = new StaticTenantResolver();
@@ -0,0 +1,303 @@
1
+ ---
2
+ /**
3
+ * Root layout. Injects per-tenant theme CSS vars at the edge, loads the font
4
+ * pairing, mounts header + footer chrome, and wires privacy-friendly analytics.
5
+ * Every page renders inside this.
6
+ */
7
+ import "@/styles/global.css";
8
+ import Seo from "@/components/Seo.astro";
9
+ import Header from "@/components/nav/Header.astro";
10
+ import Footer from "@/components/nav/Footer.astro";
11
+ import QuickView from "@/components/islands/QuickView.tsx";
12
+ import FoxyLoader from "@/components/commerce/FoxyLoader.astro";
13
+ import NicotineWarning from "@/components/compliance/NicotineWarning.astro";
14
+ import IsolatedAgeGate from "@/components/islands/IsolatedAgeGate.tsx";
15
+ import AuthBadge from "@/components/auth/AuthBadge.astro";
16
+ import { referenceTheme } from "@/themes/reference";
17
+ import { mergeTheme, themeFontHref } from "@/themes/schema";
18
+ import { organizationLd, websiteLd } from "@/lib/jsonld";
19
+ import { nonceInlineTags, CHROME_BODY_MARKER } from "@/lib/rawMarketingHtml";
20
+
21
+ interface Props {
22
+ title: string;
23
+ description?: string;
24
+ path: string;
25
+ ogType?: "website" | "product" | "article";
26
+ ogImage?: string;
27
+ noindex?: boolean;
28
+ jsonLd?: Record<string, unknown> | Record<string, unknown>[];
29
+ /** Wider pages (PLP/home) drop the prose max-width. */
30
+ fullBleed?: boolean;
31
+ }
32
+
33
+ const { title, description, path, ogType, ogImage, noindex, jsonLd } =
34
+ Astro.props;
35
+ const { tenant, themeStyle, canonicalBase, basePath, cspNonce, features, capabilities } =
36
+ Astro.locals;
37
+
38
+ // Organization JSON-LD is emitted site-wide (brief §8); pages add their own
39
+ // graphs (Product, BreadcrumbList…) via the jsonLd prop.
40
+ const pageGraphs = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : [];
41
+ const allJsonLd = [
42
+ organizationLd(tenant, canonicalBase),
43
+ websiteLd(tenant, canonicalBase),
44
+ ...pageGraphs,
45
+ ];
46
+
47
+ const theme = mergeTheme(referenceTheme, tenant.theme_tokens);
48
+ const fontHref = themeFontHref(theme);
49
+
50
+ const chrome = await Astro.locals.content.getChrome();
51
+
52
+ // SHARED CHROME: if the tenant ships content/<id>/chrome.html (the pixel-perfect
53
+ // header/footer the raw marketing pages use), render THAT chrome around the
54
+ // Layout content too — so commerce pages (collections/products/search/…) share
55
+ // ONE chrome instead of the generic Header/Footer. Split the wrapper at
56
+ // <!--PAGE_BODY-->: everything before = header, after = footer; pull its tenant
57
+ // stylesheet links into <head>; nonce its inline tags for CSP. Tenants without a
58
+ // chrome.html keep the token-driven Header/Footer (below).
59
+ const chromeHtml = await Astro.locals.content.getChromeHtml();
60
+ let sharedChrome: { header: string; footer: string; css: string[] } | null = null;
61
+ if (chromeHtml && chromeHtml.includes(CHROME_BODY_MARKER)) {
62
+ const headEnd = chromeHtml.indexOf("</head>");
63
+ const head = headEnd >= 0 ? chromeHtml.slice(0, headEnd) : "";
64
+ const css = [...head.matchAll(/<link[^>]+href="([^"]+\.css)"[^>]*>/gi)]
65
+ .map((m) => m[1] ?? "")
66
+ .filter((h) => h.startsWith("/tenants/"));
67
+ const bodyOpen = chromeHtml.indexOf(">", chromeHtml.indexOf("<body")) + 1;
68
+ const marker = chromeHtml.indexOf(CHROME_BODY_MARKER);
69
+ const bodyEnd = chromeHtml.lastIndexOf("</body>");
70
+ sharedChrome = {
71
+ header: nonceInlineTags(chromeHtml.slice(bodyOpen, marker), cspNonce),
72
+ footer: nonceInlineTags(chromeHtml.slice(marker + CHROME_BODY_MARKER.length, bodyEnd >= 0 ? bodyEnd : undefined), cspNonce),
73
+ css,
74
+ };
75
+ }
76
+
77
+ const compliance = tenant.compliance;
78
+
79
+ const analyticsToken = import.meta.env.PUBLIC_CF_ANALYTICS_TOKEN;
80
+ const analyticsEnabled =
81
+ analyticsToken && !/REPLACE|PLACEHOLDER/i.test(analyticsToken);
82
+ ---
83
+
84
+ <!doctype html>
85
+ <html lang={tenant.locale.split("-")[0] ?? "en"} data-base-path={basePath || undefined}>
86
+ <head>
87
+ <meta charset="utf-8" />
88
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
89
+ <link rel="icon" type="image/svg+xml" href={theme.brand.favicon} />
90
+
91
+ {/* Per-tenant theme tokens injected at the edge (brief §7). */}
92
+ <style set:html={themeStyle} nonce={cspNonce}></style>
93
+
94
+ {/* Shared chrome's own stylesheet(s) (bigd-navy.css + mkt.css) so the ported
95
+ chrome renders in-brand on Layout (commerce) pages too. */}
96
+ {sharedChrome?.css.map((href) => <link rel="stylesheet" href={href} />)}
97
+
98
+ {
99
+ fontHref && (
100
+ <>
101
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
102
+ <link
103
+ rel="preconnect"
104
+ href="https://fonts.gstatic.com"
105
+ crossorigin
106
+ />
107
+ <link rel="stylesheet" href={fontHref} />
108
+ </>
109
+ )
110
+ }
111
+
112
+ <Seo
113
+ title={title}
114
+ description={description}
115
+ path={path}
116
+ ogType={ogType}
117
+ ogImage={ogImage}
118
+ noindex={noindex}
119
+ jsonLd={allJsonLd}
120
+ />
121
+
122
+ {
123
+ analyticsEnabled && (
124
+ <script
125
+ defer
126
+ src="https://static.cloudflareinsights.com/beacon.min.js"
127
+ data-cf-beacon={`{"token": "${analyticsToken}"}`}
128
+ />
129
+ )
130
+ }
131
+ </head>
132
+ <body>
133
+ <a
134
+ href="#main"
135
+ class="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:m-3 focus:rounded-md focus:bg-[var(--color-primary)] focus:px-4 focus:py-2 focus:text-[var(--color-primary-contrast)]"
136
+ >
137
+ Skip to content
138
+ </a>
139
+
140
+ <AuthBadge />
141
+
142
+ {sharedChrome ? (
143
+ <Fragment set:html={sharedChrome.header} />
144
+ ) : (
145
+ <>
146
+ {compliance?.nicotineWarning && <NicotineWarning variant="bar" />}
147
+ <Header chrome={chrome} />
148
+ </>
149
+ )}
150
+
151
+ <main id="main">
152
+ <slot />
153
+ </main>
154
+
155
+ {sharedChrome ? (
156
+ <Fragment set:html={sharedChrome.footer} />
157
+ ) : (
158
+ <>
159
+ {compliance?.shippingRestriction && (
160
+ <div class="border-t border-[var(--color-border)] bg-[var(--color-bg)]" data-compliance="shipping-restriction">
161
+ <p class="container-prose py-3 text-center text-xs text-[var(--color-muted)]">
162
+ {compliance.shippingRestriction}
163
+ </p>
164
+ </div>
165
+ )}
166
+ <Footer chrome={chrome} />
167
+ </>
168
+ )}
169
+
170
+ {capabilities.ageVerification.enabled && compliance?.minAge && (
171
+ <IsolatedAgeGate client:load minAge={compliance.minAge} brand={tenant.displayName} />
172
+ )}
173
+
174
+ {/* Global quick-view dialog (opened by any [data-quickview] card button).
175
+ Commerce-only — marketing tenants ship no product UI. */}
176
+ {features.quickView && <QuickView client:idle locale={tenant.locale} />}
177
+
178
+ {/* Excise tax (Track B) — when enabled for this environment, the excise line
179
+ is calculated + added inside the FoxyCart cart (tot-foxycart pre-cart
180
+ webhook). This is the storefront's honest shopper-facing signal of it. */}
181
+ {capabilities.exciseTax.enabled && features.catalog && (
182
+ <p
183
+ class="border-t border-[var(--color-border)] bg-[var(--color-bg)] py-2.5 text-center text-xs text-[var(--color-muted)]"
184
+ data-capability="excise-tax"
185
+ >
186
+ Applicable excise tax is calculated at checkout.
187
+ </p>
188
+ )}
189
+
190
+ {/* FoxyCart loader (Powered by FoxyCart) — turns add-to-cart forms into the
191
+ sidecart + hosted checkout. Only for commerce tenants with a store. */}
192
+ <FoxyLoader />
193
+
194
+ {/* Wishlist hearts: event-delegated toggle + state hydration + count sync.
195
+ Cards/PDP carry [data-wishlist]; the header shows [data-wishlist-count].
196
+ Kept as one tiny script (no per-card hydration). Commerce-only — a
197
+ marketing tenant has no saved-items surface, so this never renders. */}
198
+ {features.savedItems && (
199
+ <script>
200
+ import {
201
+ readWishlist,
202
+ toggleSaved,
203
+ isSaved,
204
+ WISHLIST_EVENT,
205
+ } from "@/lib/wishlist";
206
+
207
+ function syncCount() {
208
+ const n = readWishlist().length;
209
+ document.querySelectorAll<HTMLElement>("[data-wishlist-count]").forEach((el) => {
210
+ el.textContent = n > 0 ? String(n) : "";
211
+ el.toggleAttribute("hidden", n === 0);
212
+ });
213
+ }
214
+
215
+ function syncHearts() {
216
+ document.querySelectorAll<HTMLButtonElement>(".wishlist-toggle").forEach((btn) => {
217
+ try {
218
+ const { handle } = JSON.parse(btn.getAttribute("data-wishlist") || "{}");
219
+ if (handle) btn.setAttribute("aria-pressed", String(isSaved(handle)));
220
+ } catch {}
221
+ });
222
+ }
223
+
224
+ document.addEventListener("click", (e) => {
225
+ const btn = (e.target as HTMLElement)?.closest?.<HTMLButtonElement>(".wishlist-toggle");
226
+ if (!btn) return;
227
+ e.preventDefault();
228
+ try {
229
+ const item = JSON.parse(btn.getAttribute("data-wishlist") || "{}");
230
+ if (!item.handle) return;
231
+ const saved = toggleSaved(item);
232
+ btn.setAttribute("aria-pressed", String(saved));
233
+ } catch {}
234
+ });
235
+
236
+ window.addEventListener(WISHLIST_EVENT, syncCount);
237
+ syncHearts();
238
+ syncCount();
239
+ </script>
240
+ )}
241
+
242
+ {/* Path-prefix tenant routing: when this request was selected via a
243
+ /<domain>/ path segment (locals.basePath), keep same-origin
244
+ root-absolute links inside that prefix so navigation stays on the
245
+ tenant. Host-routed requests have no base path and skip this entirely. */}
246
+ {basePath && (
247
+ <script is:inline nonce={cspNonce} define:vars={{ basePath }}>
248
+ (function () {
249
+ var base = basePath;
250
+ if (!base) return;
251
+ function fix(a) {
252
+ var href = a.getAttribute("href");
253
+ if (!href || href.charAt(0) !== "/" || href.charAt(1) === "/") return;
254
+ if (href === base || href.indexOf(base + "/") === 0) return;
255
+ a.setAttribute("href", base + href);
256
+ }
257
+ function fixAll() {
258
+ document.querySelectorAll('a[href^="/"]').forEach(fix);
259
+ }
260
+ if (document.readyState === "loading") {
261
+ document.addEventListener("DOMContentLoaded", fixAll);
262
+ } else {
263
+ fixAll();
264
+ }
265
+ // Late/island-rendered anchors: fix just-in-time before navigation.
266
+ document.addEventListener(
267
+ "pointerdown",
268
+ function (e) {
269
+ var a = e.target && e.target.closest && e.target.closest('a[href^="/"]');
270
+ if (a) fix(a);
271
+ },
272
+ true,
273
+ );
274
+ })();
275
+ </script>
276
+ )}
277
+
278
+ {/* Scroll-reveal + analytics pageview hook. ~0.5KB, no framework. */}
279
+ <script>
280
+ // Reveal-on-scroll (respects reduced motion via CSS).
281
+ const els = document.querySelectorAll("[data-reveal]");
282
+ if (
283
+ els.length &&
284
+ !window.matchMedia("(prefers-reduced-motion: reduce)").matches
285
+ ) {
286
+ const io = new IntersectionObserver(
287
+ (entries) => {
288
+ for (const e of entries) {
289
+ if (e.isIntersecting) {
290
+ e.target.classList.add("is-in");
291
+ io.unobserve(e.target);
292
+ }
293
+ }
294
+ },
295
+ { rootMargin: "0px 0px -8% 0px", threshold: 0.05 },
296
+ );
297
+ els.forEach((el) => io.observe(el));
298
+ } else {
299
+ els.forEach((el) => el.classList.add("is-in"));
300
+ }
301
+ </script>
302
+ </body>
303
+ </html>
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Privacy-friendly analytics (brief §5 Analytics).
3
+ *
4
+ * Phase 1 ships TWO layers:
5
+ * 1. Pageviews — Cloudflare Web Analytics, a single beacon script injected in
6
+ * Layout.astro when PUBLIC_CF_ANALYTICS_TOKEN is set (no cookies, no PII).
7
+ * 2. Commerce events — product impression/click/PDP-view instrumented NOW to a
8
+ * NO-OP sink, so Phase 2 conversion work inherits the call sites for free.
9
+ *
10
+ * The event API is intentionally tiny and synchronous-looking; swap the sink
11
+ * (e.g. to a Workers Analytics Engine writer or a beacon) without touching call
12
+ * sites. See DECISIONS.md.
13
+ */
14
+
15
+ export type CommerceEvent =
16
+ | { type: "product_impression"; handle: string; list?: string; position?: number }
17
+ | { type: "product_click"; handle: string; list?: string }
18
+ | { type: "pdp_view"; handle: string; variant_id?: string }
19
+ | { type: "search"; query: string; results: number }
20
+ | { type: "collection_view"; handle: string }
21
+ | { type: "newsletter_signup"; source: string };
22
+
23
+ export interface AnalyticsSink {
24
+ track(event: CommerceEvent): void;
25
+ }
26
+
27
+ /** Default sink: discard. Visible in dev console when ?debugAnalytics is set. */
28
+ export class NoOpSink implements AnalyticsSink {
29
+ constructor(private readonly debug = false) {}
30
+ track(event: CommerceEvent): void {
31
+ if (this.debug && typeof console !== "undefined") {
32
+ // eslint-disable-next-line no-console
33
+ console.debug("[analytics]", event);
34
+ }
35
+ }
36
+ }
37
+
38
+ let sink: AnalyticsSink = new NoOpSink();
39
+
40
+ /** Phase 2: call setAnalyticsSink(new RealSink()) once at boot. */
41
+ export function setAnalyticsSink(next: AnalyticsSink): void {
42
+ sink = next;
43
+ }
44
+
45
+ export function track(event: CommerceEvent): void {
46
+ sink.track(event);
47
+ }