@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,413 @@
1
+ /**
2
+ * Tenant-resolution middleware (brief §7).
3
+ *
4
+ * Per request: Host -> TenantConfig (via TenantResolver) -> resolve theme ->
5
+ * bind a ToTClient to the tenant's data scope -> stash everything on
6
+ * Astro.locals so pages/components stay tenant-agnostic. The serialized theme
7
+ * CSS-var block is injected into <head> by Layout.astro from locals.themeStyle.
8
+ */
9
+ import { defineMiddleware } from "astro:middleware";
10
+ import { getTenantResolver } from "@/config/resolver";
11
+ import { referenceTheme } from "@/themes/reference";
12
+ import { mergeTheme, themeToCssVars } from "@/themes/schema";
13
+ import { createToTClient } from "@/lib/tot/ToTClient";
14
+ import { readEnv, readKv, readD1 } from "@/lib/env";
15
+ import { resolveTenantRouting } from "@/lib/tenantRouting";
16
+ import { createContentProvider } from "@/lib/storyblok/provider";
17
+ import {
18
+ isCacheablePath,
19
+ pageCacheKey,
20
+ readCacheVersion,
21
+ getCachedPage,
22
+ putCachedPage,
23
+ } from "@/lib/edgeCache";
24
+ import { STATIC_SCRIPT_HASHES, STATIC_STYLE_HASHES } from "@/lib/cspStaticHashes";
25
+ import { applySecurityHeaders } from "@/lib/securityHeaders";
26
+ import { maybeInjectPreviewReload } from "@/lib/previewReload";
27
+ import {
28
+ resolveCustomizationEnv,
29
+ buildCsp,
30
+ STOREFRONT_CSP_ALLOWLIST,
31
+ foxyCspAdditions,
32
+ CSP_REPORT_GROUP,
33
+ CSP_REPORT_PATH,
34
+ readPublishedArtifact,
35
+ themeArtifactPath,
36
+ resolveTenantFeatures,
37
+ resolveCapabilities,
38
+ capabilitiesArtifactPath,
39
+ } from "@tot/public-runtime";
40
+ import type { CapabilityConfig } from "@tot/public-runtime";
41
+ import { resolveTenantCommerce } from "@/lib/foxyCommerce";
42
+ import { evaluateDemoGate, hostInGateList } from "@/lib/demoGate";
43
+ import {
44
+ evaluateLoginGate,
45
+ parseGateMode,
46
+ isAuthPath,
47
+ sessionAdmitsHost,
48
+ PUBLIC_HOME_TENANT_ID,
49
+ } from "@/lib/auth/loginGate";
50
+ import { gatedHoldingResponse } from "@/lib/auth/gatePage";
51
+ import { readSession, KvSessionStore, SESSION_COOKIE } from "@/lib/auth/session";
52
+ import { isMaintenanceOn, maintenanceResponse } from "@/lib/maintenance";
53
+ import { membershipFor } from "@/lib/dashboard/tenantSelection";
54
+ import type { DeepPartial, ThemeTokens } from "@tot/public-runtime";
55
+
56
+ export const onRequest = defineMiddleware(async (context, next) => {
57
+ const { request, locals } = context;
58
+ const url = new URL(request.url);
59
+ const host =
60
+ request.headers.get("host") ?? url.host ?? "localhost";
61
+
62
+ // ---- Site maintenance kill-switch (fail-safe full close) ------------------
63
+ // MAINTENANCE_MODE=1 takes the ENTIRE site offline (every host + path) with a
64
+ // static "Coming Soon" holding page, BEFORE tenant resolution, the access
65
+ // gate, the edge cache, or any render. Not a login gate — auth paths are not
66
+ // exempt. Back online: set MAINTENANCE_MODE=0 (or unset) and redeploy.
67
+ if (isMaintenanceOn(await readEnv("MAINTENANCE_MODE"))) {
68
+ return maintenanceResponse();
69
+ }
70
+
71
+ // Demo access gate for named hosts (DEMO_GATE_HOSTS). Two modes, switched by
72
+ // DEMO_GATE_MODE:
73
+ // "password" (default) — interim Basic-Auth gate, enforced HERE (host-level,
74
+ // no tenant/session needed) so a blocked host never reaches tenant
75
+ // resolution, the edge cache, or any render.
76
+ // "login" — "Sign in with ToT" gate, enforced just below (needs the resolved
77
+ // tenant as the grant `resource` + the SESSION KV), redirecting to
78
+ // /auth/login unless a valid, resource-bound tot_session cookie is present.
79
+ const gateMode = parseGateMode(await readEnv("DEMO_GATE_MODE"));
80
+ const demoGateHosts = await readEnv("DEMO_GATE_HOSTS");
81
+ if (gateMode === "password") {
82
+ const demoGate = evaluateDemoGate(host, request.headers.get("authorization"), {
83
+ hosts: demoGateHosts,
84
+ password: await readEnv("DEMO_GATE_PASSWORD"),
85
+ realm: await readEnv("DEMO_GATE_REALM"),
86
+ });
87
+ if (demoGate) return demoGate;
88
+ }
89
+
90
+ // Host first; if the host owns no tenant, a leading /<domain>/ path segment
91
+ // selects one (DNS-free tenant selection on localhost/preview). The resolver
92
+ // is KV-backed in prod (many-tenant) and static in plain dev.
93
+ const resolver = await getTenantResolver();
94
+ const { tenant, basePath, renderPath } = await resolveTenantRouting(
95
+ host,
96
+ url.pathname,
97
+ resolver,
98
+ );
99
+ if (!tenant) {
100
+ return new Response("Unknown host", { status: 404 });
101
+ }
102
+
103
+ // Workers Cache API: backs the full-page edge cache below. Undefined in
104
+ // `astro dev` (Node) — every use is a no-op there.
105
+ const cache =
106
+ typeof caches !== "undefined" && "default" in caches
107
+ ? (caches as unknown as { default: Cache }).default
108
+ : undefined;
109
+ const kv = await readKv("TENANT_CACHE");
110
+
111
+ // Login-gate enforcement (DEMO_GATE_MODE="login"): runs before the edge cache
112
+ // so a gated page is never served to an unauthenticated viewer. The platform's
113
+ // own "self" home tenant is EXEMPT — it renders publicly (browse + sign up);
114
+ // every other tenant on a gated host is withheld behind sign-in. A valid,
115
+ // resource-bound tot_session sets locals.viewer (email + roles) for chrome;
116
+ // otherwise an unauthenticated visitor gets a "Coming soon" holding page with
117
+ // a Sign-in button (auth paths are exempt so sign-in itself is reachable). The
118
+ // grant `resource` is the tenant's appDomain — never input.
119
+ const tenantExempt = tenant.tenant_id === PUBLIC_HOME_TENANT_ID;
120
+ if (gateMode === "login" && hostInGateList(host, demoGateHosts)) {
121
+ // UNIFIED on the federated session: read the FULL session (no resource filter —
122
+ // federated sign-in's aud is the storefront app, not this tenant) and admit the
123
+ // viewer iff they hold a MEMBERSHIP for the host's tenant. One session model for
124
+ // the console + the preview gate. Deny-closed: no session / no membership → gate.
125
+ let viewerRecord = null;
126
+ if (!isAuthPath(url.pathname)) {
127
+ const sessionKv = await readKv("SESSION");
128
+ if (sessionKv) {
129
+ const sid = context.cookies.get(SESSION_COOKIE)?.value ?? null;
130
+ viewerRecord = await readSession(
131
+ new KvSessionStore(sessionKv),
132
+ sid,
133
+ Math.floor(Date.now() / 1000),
134
+ );
135
+ }
136
+ }
137
+ const nowSeconds = Math.floor(Date.now() / 1000);
138
+ const admitted = sessionAdmitsHost(viewerRecord, tenant.appDomain, nowSeconds);
139
+ const decision = evaluateLoginGate({
140
+ host,
141
+ pathname: url.pathname,
142
+ search: url.search,
143
+ mode: gateMode,
144
+ gatedHosts: demoGateHosts,
145
+ hasValidSession: admitted,
146
+ tenantExempt,
147
+ });
148
+ if (decision.action === "redirect") {
149
+ // Withhold content but offer sign-in (carrying the intended destination).
150
+ return gatedHoldingResponse(decision.location);
151
+ }
152
+ if (admitted && viewerRecord) {
153
+ // Surface the viewer's role FOR THIS host's tenant as chrome. A member sees
154
+ // their tenants[] role; a ToT-staff viewer admitted via an explicit vendor
155
+ // selection sees the staffRoles-scoped capability (distinct path).
156
+ const role = membershipFor(viewerRecord.tenants, tenant.appDomain, {
157
+ selection: viewerRecord.staffSelection,
158
+ nowSeconds,
159
+ })?.role;
160
+ locals.viewer = {
161
+ email: viewerRecord.email,
162
+ roles: role ? [role] : viewerRecord.roles,
163
+ capability: role ?? viewerRecord.capability,
164
+ };
165
+ }
166
+ }
167
+
168
+ // Preview/test detection (also reused for the customization env below). A
169
+ // production read is one with no preview host, query token, or preview cookie.
170
+ const isPreviewHost = host.toLowerCase().startsWith("preview.");
171
+ const queryToken = url.searchParams.get("__preview");
172
+ const cookieToken = context.cookies.get("tot_preview")?.value ?? null;
173
+
174
+ // ---- Edge page cache (D1): short-circuit production browse reads on a hit,
175
+ // BEFORE building the client / theme / rendering. Misses store the final
176
+ // response at the end of this middleware.
177
+ //
178
+ // NEVER in dev (`astro dev`): the workerd dev runtime persists the Workers Cache
179
+ // API in .wrangler/state, so a cached page would be served stale across edits AND
180
+ // restarts — silently defeating hot-reload (edits look "lost"). Dev must always
181
+ // re-render. import.meta.env.DEV statically drops this guard from the prod build.
182
+ const pageCacheEligible =
183
+ !import.meta.env.DEV &&
184
+ request.method.toUpperCase() === "GET" &&
185
+ !isPreviewHost &&
186
+ !queryToken &&
187
+ !cookieToken &&
188
+ isCacheablePath(url.pathname, url.searchParams);
189
+ let pageKey: Request | null = null;
190
+ if (pageCacheEligible && cache && kv) {
191
+ const version = await readCacheVersion(kv, tenant.tenant_id);
192
+ pageKey = pageCacheKey(tenant.tenant_id, url.pathname, url.search, version);
193
+ const hit = await getCachedPage(cache, pageKey);
194
+ if (hit) return hit;
195
+ }
196
+
197
+ // Edge read replica: opt-in via USE_D1_CATALOG=1 (+ a CATALOG_DB binding). Off
198
+ // by default, so the bundled fixture snapshot serves reads.
199
+ const d1 =
200
+ (await readEnv("USE_D1_CATALOG")) === "1" ? await readD1("CATALOG_DB") : undefined;
201
+ const tot = await createToTClient(tenant, d1);
202
+
203
+ const proto = url.protocol === "http:" ? "http" : "https";
204
+ // Canonical base uses the tenant's canonical domain in prod; for local dev we
205
+ // keep the actual host (plus any /<domain> base path) so links resolve.
206
+ const isLocal = host.startsWith("localhost") || host.startsWith("127.0.0.1");
207
+ const canonicalBase = isLocal
208
+ ? `${proto}://${host}${basePath}`
209
+ : `https://${tenant.canonicalDomain}`;
210
+
211
+ // Live vs test customization (D4). Production hosts always serve "live" and
212
+ // ignore any token; a "preview." host is auth-gated by a signed, tenant-bound,
213
+ // short-TTL preview token (?__preview= on first hit, then the tot_preview
214
+ // cookie). Test is never indexable (X-Robots-Tag below).
215
+ const previewToken = queryToken ?? cookieToken;
216
+ const customization = await resolveCustomizationEnv({
217
+ isPreviewHost,
218
+ tenantId: tenant.tenant_id,
219
+ token: previewToken,
220
+ secret: (await readEnv("CUSTOMIZATION_PREVIEW_SECRET")) ?? null,
221
+ nowSeconds: Math.floor(Date.now() / 1000),
222
+ });
223
+ if (customization.reject) {
224
+ return new Response(customization.reject.message, {
225
+ status: customization.reject.status,
226
+ });
227
+ }
228
+ // Persist a query-supplied token as a cookie so later navigations stay in test.
229
+ if (customization.env === "test" && queryToken) {
230
+ context.cookies.set("tot_preview", queryToken, {
231
+ httpOnly: true,
232
+ secure: true,
233
+ sameSite: "lax",
234
+ path: "/",
235
+ maxAge: 3600,
236
+ });
237
+ }
238
+
239
+ // Apply a published theme override for the resolved environment (store->app).
240
+ // The live domain serves `live`; a gated preview serves `test`. When nothing
241
+ // is published the build-time tenant theme is used unchanged.
242
+ //
243
+ // Resolution is versioned-aware inside readPublishedArtifact: env maps to a
244
+ // publish channel (live->live, test->preview); if the tenant has a channel
245
+ // pointer, the artifact is resolved through the immutable site version
246
+ // (channel -> versionId -> manifest -> content). Tenants with no channel
247
+ // pointer fall back to the legacy per-path selection — no behavior change.
248
+ const kvGet = kv ? (key: string) => kv.get(key) : async () => null;
249
+ let theme = mergeTheme(referenceTheme, tenant.theme_tokens);
250
+ const publishedTheme = await readPublishedArtifact(
251
+ kvGet,
252
+ tenant.tenant_id,
253
+ customization.env,
254
+ themeArtifactPath(tenant.tenant_id),
255
+ );
256
+ if (publishedTheme) {
257
+ try {
258
+ theme = mergeTheme(theme, JSON.parse(publishedTheme) as DeepPartial<ThemeTokens>);
259
+ } catch {
260
+ /* malformed published artifact — keep the build-time theme */
261
+ }
262
+ }
263
+
264
+ // Per-environment capabilities (Track A/B). Config-as-code: the build-time
265
+ // TenantConfig default is overridden by the published capabilities artifact for
266
+ // THIS environment — materialized from the tenant repo's .tot/config.json per
267
+ // commit (localhost=working tree, preview=PR head, live=promoted). So a
268
+ // capability change is PR-gated and separable per environment.
269
+ let publishedCaps: Partial<CapabilityConfig> | null = null;
270
+ const publishedCapsRaw = await readPublishedArtifact(
271
+ kvGet,
272
+ tenant.tenant_id,
273
+ customization.env,
274
+ capabilitiesArtifactPath(tenant.tenant_id),
275
+ );
276
+ if (publishedCapsRaw) {
277
+ try {
278
+ publishedCaps = JSON.parse(publishedCapsRaw) as Partial<CapabilityConfig>;
279
+ } catch {
280
+ /* malformed published artifact — keep the build-time default */
281
+ }
282
+ }
283
+ const capabilities = resolveCapabilities(tenant, publishedCaps);
284
+
285
+ // Per-request CSP nonce (D2): goes on the platform's own inline tags in
286
+ // Layout.astro so a strict CSP can allow them without 'unsafe-inline'.
287
+ const cspNonce = crypto.randomUUID();
288
+
289
+ locals.tenant = tenant;
290
+ // Effective capability flags for this tenant (commerce defaults unless the
291
+ // tenant declares siteType:"marketing" / explicit features). Pages and chrome
292
+ // read these instead of hard-coding tenant identity checks.
293
+ locals.features = resolveTenantFeatures(tenant);
294
+ locals.capabilities = capabilities;
295
+ // FoxyCart cart/checkout facts (Track A) — only when the tenant has a catalog
296
+ // AND the cartCheckout capability is enabled for THIS environment. Drives the
297
+ // loader + signed add-to-cart, and the CSP additions below.
298
+ const commerce =
299
+ locals.features.catalog && capabilities.cartCheckout.enabled
300
+ ? await resolveTenantCommerce(tenant)
301
+ : null;
302
+ locals.commerce = commerce ?? undefined;
303
+ locals.tot = tot;
304
+ locals.content = createContentProvider(tenant, {
305
+ get: kvGet,
306
+ env: customization.env,
307
+ });
308
+ locals.themeStyle = themeToCssVars(theme);
309
+ locals.canonicalBase = canonicalBase;
310
+ locals.customizationEnv = customization.env;
311
+ locals.cspNonce = cspNonce;
312
+ // Prefix that path-routed pages live under ("" for host-routed). Pages stay
313
+ // tenant-agnostic; the Layout uses this to keep root-absolute links inside
314
+ // the tenant prefix on the client.
315
+ locals.basePath = basePath;
316
+
317
+ // When a /<domain>/ prefix selected the tenant, rewrite to the stripped route
318
+ // so the existing file-based routes match. next(path) rewrites WITHOUT
319
+ // re-running middleware, so the locals set above are preserved.
320
+ let response = basePath
321
+ ? await next(`${renderPath}${url.search}`)
322
+ : await next();
323
+
324
+ // On-publish auto-refresh: inject the live-reload client into SHARED preview
325
+ // (`test`) HTML pages ONLY — never `live` (keeps live clean + edge-cacheable).
326
+ // It polls /__preview/version and reloads when this tenant's cache-version
327
+ // bumps on publish/reconcile. Injection happens before the CSP header is set so
328
+ // the script's per-request nonce (cspNonce) matches the enforced policy.
329
+ response = await maybeInjectPreviewReload(response, {
330
+ env: customization.env,
331
+ tenantId: tenant.tenant_id,
332
+ basePath,
333
+ nonce: cspNonce,
334
+ });
335
+
336
+ // Baseline security headers (S1) on every response — set before the page-cache
337
+ // store below so cached copies carry them too.
338
+ applySecurityHeaders(response.headers);
339
+
340
+ // Preview (test) output must never be indexed.
341
+ if (customization.noindex) {
342
+ response.headers.set("X-Robots-Tag", "noindex, nofollow");
343
+ }
344
+
345
+ // Strict CSP — now ENFORCED in every environment (Epic S2). The report-only
346
+ // rollout is complete: platform output is CSP-clean — inline styles are
347
+ // externalized (`inlineStylesheets: "never"`), Astro's static inline scripts +
348
+ // island style are hash-pinned (cspStaticHashes), our dynamic inline tags carry
349
+ // the per-request nonce, and the last inline event handler (SortSelect's
350
+ // `onchange`) is now a bundled 'self' script. Reporting endpoints stay attached
351
+ // (report-to/report-uri work in enforce mode), so we keep telemetry as tenant
352
+ // client JS arrives — those per-version tenant hashes extend `scriptSrc` per
353
+ // request (the mechanism below already supports it).
354
+ //
355
+ // DEV EXCEPTION: `astro dev` serves CSS/HMR as UN-nonced inline <style>/<script>
356
+ // (Vite injects them); the strict nonce policy blocks those, leaving the dev
357
+ // site unstyled. The BUILD is CSP-clean (styles are externalized via
358
+ // `inlineStylesheets:"never"`), so we enforce there and only REPORT in dev —
359
+ // violations are still surfaced, but styling/HMR work locally.
360
+ const cspHeader = import.meta.env.DEV
361
+ ? "Content-Security-Policy-Report-Only"
362
+ : "Content-Security-Policy";
363
+ response.headers.set(
364
+ "Reporting-Endpoints",
365
+ `${CSP_REPORT_GROUP}="${CSP_REPORT_PATH}"`,
366
+ );
367
+ // FoxyCart origins (loader CDN + the store's cart/checkout host) added to the
368
+ // strict policy only when this tenant has a store — so add-to-cart, the
369
+ // sidecart iframe/XHR, and the cross-origin form POST all pass.
370
+ const foxyCsp = commerce ? foxyCspAdditions(commerce.origin) : null;
371
+ response.headers.set(
372
+ cspHeader,
373
+ buildCsp({
374
+ nonce: cspNonce,
375
+ ...STOREFRONT_CSP_ALLOWLIST,
376
+ // Pin Astro's static inline hydration scripts + island style by hash
377
+ // (they're identical every request); my dynamic inline tags use the nonce.
378
+ scriptSrc: [
379
+ ...STOREFRONT_CSP_ALLOWLIST.scriptSrc,
380
+ ...STATIC_SCRIPT_HASHES,
381
+ ...(foxyCsp?.scriptSrc ?? []),
382
+ ],
383
+ styleSrc: [
384
+ ...STOREFRONT_CSP_ALLOWLIST.styleSrc,
385
+ ...STATIC_STYLE_HASHES,
386
+ ...(foxyCsp?.styleSrc ?? []),
387
+ ],
388
+ fontSrc: [...STOREFRONT_CSP_ALLOWLIST.fontSrc, ...(foxyCsp?.fontSrc ?? [])],
389
+ connectSrc: [
390
+ ...STOREFRONT_CSP_ALLOWLIST.connectSrc,
391
+ ...(foxyCsp?.connectSrc ?? []),
392
+ ],
393
+ imgSrc: [...STOREFRONT_CSP_ALLOWLIST.imgSrc, ...(foxyCsp?.imgSrc ?? [])],
394
+ frameSrc: foxyCsp?.frameSrc ?? [],
395
+ formAction: foxyCsp?.formAction ?? [],
396
+ reportGroup: CSP_REPORT_GROUP,
397
+ reportUri: CSP_REPORT_PATH,
398
+ }),
399
+ );
400
+
401
+ // D1: store the fully-rendered, CSP-stamped response for the next reader. Only
402
+ // production (no preview token reached here) cacheable GETs have a pageKey;
403
+ // putCachedPage additionally skips non-200, non-HTML, and cookie-bearing
404
+ // responses. Also expose Cache-Control to any fronting CDN.
405
+ if (pageKey && cache && customization.env === "live") {
406
+ response.headers.set(
407
+ "Cache-Control",
408
+ "public, s-maxage=300, stale-while-revalidate=86400",
409
+ );
410
+ await putCachedPage(cache, pageKey, response);
411
+ }
412
+ return response;
413
+ });
@@ -0,0 +1,61 @@
1
+ ---
2
+ /**
3
+ * 404 — written in the Northwind voice, not a stub. Offers a way home and a
4
+ * few collections to wander into so a dead link still lands somewhere useful.
5
+ */
6
+ import Layout from "@/layouts/Layout.astro";
7
+ import CollectionCard from "@/components/CollectionCard.astro";
8
+ import { withBase } from "@/lib/basePath";
9
+
10
+ const { tenant, tot, basePath } = Astro.locals;
11
+
12
+ // A handful of collections to redirect lost visitors toward.
13
+ const collections = (await tot.listCollections()).slice(0, 3);
14
+ ---
15
+
16
+ <Layout
17
+ title="Page not found"
18
+ description="The page you were after isn't here."
19
+ path="/404"
20
+ noindex={true}
21
+ >
22
+ <section class="bg-[var(--color-bg)]">
23
+ <div class="container-prose py-[clamp(4rem,10vw,8rem)] text-center" data-reveal>
24
+ <p class="eyebrow mb-4">404</p>
25
+ <h1 class="display-xl mx-auto max-w-[18ch]">
26
+ This page wandered off in the fog.
27
+ </h1>
28
+ <p class="mx-auto mt-5 max-w-[44ch] text-[1.1rem] text-[var(--color-muted)]">
29
+ The link you followed leads nowhere we keep. No harm done — let's get you
30
+ back on the trail.
31
+ </p>
32
+ <div class="mt-9 flex flex-wrap items-center justify-center gap-3">
33
+ <a
34
+ href={withBase(basePath, "/")}
35
+ class="inline-flex items-center rounded-full bg-[var(--color-primary)] px-6 py-3 font-display text-sm font-medium text-[var(--color-primary-contrast)] transition-transform hover:-translate-y-0.5"
36
+ >
37
+ Back home
38
+ </a>
39
+ <a
40
+ href={withBase(basePath, "/collections")}
41
+ class="inline-flex items-center rounded-full border border-[var(--color-border)] px-6 py-3 font-display text-sm font-medium text-[var(--color-text)] transition-colors hover:border-[var(--color-primary)]"
42
+ >
43
+ Browse collections
44
+ </a>
45
+ </div>
46
+ </div>
47
+ </section>
48
+
49
+ {collections.length > 0 && (
50
+ <section class="bg-[var(--color-surface)] py-[clamp(2.5rem,6vw,4.5rem)]">
51
+ <div class="container-prose">
52
+ <p class="eyebrow mb-6 text-center">Maybe you were looking for</p>
53
+ <div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
54
+ {collections.map((c) => (
55
+ <CollectionCard collection={c} productCount={c.product_handles.length} />
56
+ ))}
57
+ </div>
58
+ </div>
59
+ </section>
60
+ )}
61
+ </Layout>
@@ -0,0 +1,90 @@
1
+ ---
2
+ /**
3
+ * Editorial pages from the ContentProvider (Storyblok seam; local fallback in
4
+ * Phase 1). Catch-all, lowest routing priority — Astro resolves /products,
5
+ * /collections, /search first. We still guard defensively against those
6
+ * prefixes so a stray slug can never shadow a real route.
7
+ */
8
+ import Layout from "@/layouts/Layout.astro";
9
+ import Breadcrumbs from "@/components/Breadcrumbs.astro";
10
+ import { breadcrumbLd } from "@/lib/jsonld";
11
+ import { truncate } from "@/lib/format";
12
+ import { rawHtmlResponse } from "@/lib/rawMarketingHtml";
13
+
14
+ const { tenant, canonicalBase, content, basePath } = Astro.locals;
15
+ const slug = (Astro.params.slug ?? "").replace(/^\/+|\/+$/g, "");
16
+
17
+ // Defensive: never let the catch-all answer for first-class route namespaces.
18
+ // (Keeps the commerce boundary hard — a marketing subpage can never shadow a
19
+ // product/collection route.)
20
+ if (
21
+ !slug ||
22
+ slug.startsWith("products/") ||
23
+ slug.startsWith("collections/")
24
+ ) {
25
+ return new Response(null, { status: 404 });
26
+ }
27
+
28
+ // Preview/dev-only style-guide surfaces are requested at `/_style-guide/<tenant>/…`
29
+ // (the leading underscore signals internal). Astro won't route a `_`-prefixed pages
30
+ // dir, so the pages live under `/style-guide/…` and we rewrite the underscored URL
31
+ // onto them. These are noindex + sitemap-excluded and never reskin the live site.
32
+ if (slug === "_style-guide" || slug.startsWith("_style-guide/")) {
33
+ return Astro.rewrite("/style-guide/" + slug.slice("_style-guide".length + 1));
34
+ }
35
+
36
+ // Raw tenant-authored marketing subpage (vendor-injected HTML) — serve the
37
+ // self-contained document VERBATIM, bypassing the block Layout, with the request
38
+ // CSP nonce stamped onto its first-party inline <style>/<script>. Same path as the
39
+ // raw home in index.astro. Middleware still applies canonical host, security
40
+ // headers, CSP, and (prod) the edge cache.
41
+ const rawHtml = await content.getPageHtml(slug);
42
+ if (rawHtml) {
43
+ // Shared chrome: a body-only fragment gets wrapped in the tenant's chrome.html;
44
+ // a full document serves verbatim (backward-compatible). See rawMarketingHtml.ts.
45
+ const chromeWrapper = await content.getChromeHtml();
46
+ return rawHtmlResponse(rawHtml, { nonce: Astro.locals.cspNonce, basePath, chromeWrapper });
47
+ }
48
+
49
+ const page = await content.getPage(slug);
50
+ if (!page) return new Response(null, { status: 404 });
51
+
52
+ const crumbs = [
53
+ { label: tenant.displayName, href: "/" },
54
+ { label: page.title, href: `/${page.slug}` },
55
+ ];
56
+
57
+ const description =
58
+ page.seoDescription ||
59
+ (page.intro ? truncate(page.intro, 160) : undefined);
60
+ ---
61
+
62
+ <Layout
63
+ title={page.seoTitle || page.title}
64
+ description={description}
65
+ path={`/${page.slug}`}
66
+ ogType="article"
67
+ jsonLd={breadcrumbLd(crumbs, canonicalBase)}
68
+ >
69
+ <div class="container-prose pt-6">
70
+ <Breadcrumbs crumbs={crumbs} />
71
+ </div>
72
+
73
+ {/* Page hero */}
74
+ <header class="container-prose pb-2 pt-8">
75
+ {page.eyebrow && <p class="eyebrow eyebrow--tick mb-3">{page.eyebrow}</p>}
76
+ <h1 class="display-xl max-w-[20ch]">
77
+ {page.title}
78
+ </h1>
79
+ {page.intro && (
80
+ <p class="mt-5 max-w-[60ch] text-[1.1rem] text-[var(--color-muted)]">
81
+ {page.intro}
82
+ </p>
83
+ )}
84
+ </header>
85
+
86
+ {/* Body — constrained to a readable measure. */}
87
+ <div class="container-prose pb-[clamp(3rem,7vw,6rem)] pt-8">
88
+ <div class="rte mx-auto max-w-[68ch] text-[var(--color-text)]" set:html={page.body_html} />
89
+ </div>
90
+ </Layout>
@@ -0,0 +1,21 @@
1
+ /**
2
+ * POST /api/auth/logout — end the signed-in session.
3
+ *
4
+ * Revokes the server-side record (so the session can't be reused even if the
5
+ * cookie is replayed) and clears the cookie, then bounces to the login page.
6
+ * Exempt from the login gate (it's under /api/auth/), so a viewer can always
7
+ * sign out.
8
+ */
9
+ import type { APIContext } from "astro";
10
+ import { getSessionStore, clearSessionCookie } from "@/lib/auth/route";
11
+ import { revokeSession, SESSION_COOKIE } from "@/lib/auth/session";
12
+
13
+ export const prerender = false;
14
+
15
+ export async function POST(context: APIContext): Promise<Response> {
16
+ const sid = context.cookies.get(SESSION_COOKIE)?.value ?? null;
17
+ const store = await getSessionStore();
18
+ if (store) await revokeSession(store, sid);
19
+ clearSessionCookie(context);
20
+ return context.redirect("/auth/login", 303);
21
+ }