@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,113 @@
1
+ /**
2
+ * Demo password gate (HTTP Basic Auth) for named hosts.
3
+ *
4
+ * A lightweight "put a password in front of it before we show it" gate for demo
5
+ * subdomains (e.g. bigdvapor.tokenoftrust.store). Deliberately DISTINCT from the
6
+ * ShipLoop customization-preview gate (resolveCustomizationEnv / `preview.` hosts
7
+ * + signed, tenant-bound tokens): this is a single shared static password,
8
+ * browser-native, covering every path + subresource on the host. It runs first
9
+ * in the middleware so a blocked host never reaches tenant resolution, the edge
10
+ * cache, or any render.
11
+ *
12
+ * Config is env-driven so gating/ungating a host or rotating the password needs
13
+ * NO code change:
14
+ * DEMO_GATE_HOSTS comma-separated hostnames to gate ("" / unset = none)
15
+ * DEMO_GATE_PASSWORD the shared password (set via `wrangler secret`; unset = off)
16
+ * DEMO_GATE_REALM optional Basic-Auth realm label (default "Demo")
17
+ *
18
+ * The username is IGNORED — share it as "any username, password: …", the usual
19
+ * demo pattern. If no password is configured the gate is a no-op everywhere.
20
+ */
21
+
22
+ const REALM_FALLBACK = "Demo";
23
+
24
+ /** Lowercase, trim, and drop any `:port` suffix so host matching is exact. */
25
+ export function normalizeHost(host: string): string {
26
+ return host.trim().toLowerCase().replace(/:\d+$/, "");
27
+ }
28
+
29
+ /**
30
+ * True when `host` matches the comma-separated gate list (case/port-insensitive).
31
+ *
32
+ * Entries are either an exact host (`storefront.tokenoftrust.workers.dev`) or a
33
+ * leading-`*.` wildcard (`*.tokenoftrust.store`) that matches ANY subdomain of
34
+ * that zone at any depth — `bigdvapor.tokenoftrust.store`,
35
+ * `storefront.tokenoftrust.store`, `preview.acme.tokenoftrust.store` — but NOT
36
+ * the bare apex. Wildcards are what let us gate every tenant (present + future)
37
+ * on a shared zone without enumerating each host.
38
+ */
39
+ export function hostInGateList(host: string, hostsCsv: string | null | undefined): boolean {
40
+ const h = normalizeHost(host);
41
+ const list = (hostsCsv ?? "").split(",").map(normalizeHost).filter(Boolean);
42
+ for (const entry of list) {
43
+ if (entry.startsWith("*.")) {
44
+ const suffix = entry.slice(1); // ".tokenoftrust.store"
45
+ if (h.length > suffix.length && h.endsWith(suffix)) return true;
46
+ } else if (h === entry) {
47
+ return true;
48
+ }
49
+ }
50
+ return false;
51
+ }
52
+
53
+ /** Parse the password out of an `Authorization: Basic …` header; null if absent/malformed. */
54
+ function passwordFromAuthHeader(header: string | null): string | null {
55
+ if (!header) return null;
56
+ const [scheme, encoded] = header.split(" ");
57
+ if (!scheme || scheme.toLowerCase() !== "basic" || !encoded) return null;
58
+ let decoded: string;
59
+ try {
60
+ decoded = atob(encoded);
61
+ } catch {
62
+ return null;
63
+ }
64
+ const colon = decoded.indexOf(":");
65
+ if (colon === -1) return null;
66
+ return decoded.slice(colon + 1);
67
+ }
68
+
69
+ /** Length-hiding constant-time-ish string equality (avoids early-exit timing leaks). */
70
+ function safeEqual(a: string, b: string): boolean {
71
+ const len = Math.max(a.length, b.length);
72
+ let diff = a.length ^ b.length;
73
+ for (let i = 0; i < len; i++) {
74
+ diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
75
+ }
76
+ return diff === 0;
77
+ }
78
+
79
+ export interface DemoGateEnv {
80
+ hosts?: string | null;
81
+ password?: string | null;
82
+ realm?: string | null;
83
+ }
84
+
85
+ /**
86
+ * Decide whether a request to `host` is allowed past the demo gate.
87
+ * Returns a 401 Response to send when the gate blocks; null when the host is
88
+ * ungated, the gate is disabled, or valid credentials were supplied.
89
+ */
90
+ export function evaluateDemoGate(
91
+ host: string,
92
+ authorization: string | null,
93
+ env: DemoGateEnv,
94
+ ): Response | null {
95
+ const password = env.password?.trim();
96
+ if (!password) return null; // no password configured → gate disabled globally
97
+
98
+ if (!hostInGateList(host, env.hosts)) return null; // this host isn't gated
99
+
100
+ const supplied = passwordFromAuthHeader(authorization);
101
+ if (supplied !== null && safeEqual(supplied, password)) return null; // authorized
102
+
103
+ const realm = (env.realm?.trim() || REALM_FALLBACK).replace(/"/g, "");
104
+ return new Response("Authentication required.", {
105
+ status: 401,
106
+ headers: {
107
+ "WWW-Authenticate": `Basic realm="${realm}", charset="UTF-8"`,
108
+ "Content-Type": "text/plain; charset=utf-8",
109
+ // Never let a browser/CDN cache the challenge or a would-be authorized body.
110
+ "Cache-Control": "no-store",
111
+ },
112
+ });
113
+ }
@@ -0,0 +1,127 @@
1
+ // The floating "✦ ask" affordance for `tot dev`'s inline-AI edit loop (WS5 G1).
2
+ // Injected into every page ONLY in `astro dev` by inlineAiEditPlugin in
3
+ // astro.config.mjs (never bundled into the production build — this file isn't
4
+ // imported by any Astro page/component, only served verbatim by that dev-only
5
+ // Vite middleware). Talks to /__tot/ai-edit and /__tot/ai-undo, which
6
+ // scripts/dev/ai-edit.mjs implements against the tenant this `tot dev` run is
7
+ // serving (TOT_DEV_TENANT).
8
+ //
9
+ // Plain vanilla JS, no build step, no framework — this never ships to users.
10
+
11
+ (function () {
12
+ const ROOT_ID = "__tot-ai-widget";
13
+ if (document.getElementById(ROOT_ID)) return; // one instance per page
14
+
15
+ const root = document.createElement("div");
16
+ root.id = ROOT_ID;
17
+ root.innerHTML = `
18
+ <style>
19
+ #${ROOT_ID} { position: fixed; right: 1rem; bottom: 1rem; z-index: 2147483647;
20
+ font: 14px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
21
+ #${ROOT_ID} .toggle { width: 3rem; height: 3rem; border-radius: 999px; border: none;
22
+ background: #16181d; color: #e8e6e0; font-size: 1.25rem; cursor: pointer;
23
+ box-shadow: 0 8px 24px -8px rgba(0,0,0,.45); }
24
+ #${ROOT_ID} .toggle:hover { background: #23262d; }
25
+ #${ROOT_ID} .panel { display: none; position: absolute; right: 0; bottom: 3.75rem;
26
+ width: 22rem; max-width: 90vw; background: #fff; color: #16191d; border-radius: 12px;
27
+ box-shadow: 0 16px 40px -12px rgba(0,0,0,.35); border: 1px solid #e4e0d6; padding: .9rem; }
28
+ #${ROOT_ID}.open .panel { display: block; }
29
+ #${ROOT_ID} textarea { width: 100%; box-sizing: border-box; resize: vertical; min-height: 3.5rem;
30
+ border: 1px solid #d8d4c8; border-radius: 8px; padding: .5rem .6rem; font: inherit; }
31
+ #${ROOT_ID} .row { display: flex; gap: .4rem; margin-top: .5rem; }
32
+ #${ROOT_ID} button.primary { flex: 1; background: #0f7b6c; color: #fff; border: none;
33
+ border-radius: 8px; padding: .5rem .7rem; font: 600 .85rem/1 inherit; cursor: pointer; }
34
+ #${ROOT_ID} button.primary:disabled { opacity: .5; cursor: default; }
35
+ #${ROOT_ID} button.secondary { background: #ece9e0; color: #2a2d31; border: none;
36
+ border-radius: 8px; padding: .5rem .7rem; font: 600 .8rem/1 inherit; cursor: pointer; }
37
+ #${ROOT_ID} .status { margin-top: .6rem; font-size: .82rem; white-space: pre-wrap; }
38
+ #${ROOT_ID} .status.error { color: #a83a3a; }
39
+ #${ROOT_ID} .status.ok { color: #0f7b6c; }
40
+ #${ROOT_ID} details { margin-top: .5rem; }
41
+ #${ROOT_ID} pre { max-height: 12rem; overflow: auto; background: #16181d; color: #dbe3ea;
42
+ border-radius: 8px; padding: .5rem .6rem; font-size: .75rem; }
43
+ </style>
44
+ <button class="toggle" type="button" aria-label="Ask AI to edit this site" title="Ask AI to edit this site">✦</button>
45
+ <div class="panel">
46
+ <textarea placeholder="e.g. make it darker but still friendly" aria-label="Describe the change"></textarea>
47
+ <div class="row">
48
+ <button class="primary" type="button" data-action="ask">Ask</button>
49
+ <button class="secondary" type="button" data-action="undo">Undo last</button>
50
+ </div>
51
+ <div class="status" role="status" aria-live="polite"></div>
52
+ </div>
53
+ `;
54
+ document.body.appendChild(root);
55
+
56
+ const toggle = root.querySelector(".toggle");
57
+ const textarea = root.querySelector("textarea");
58
+ const askBtn = root.querySelector('[data-action="ask"]');
59
+ const undoBtn = root.querySelector('[data-action="undo"]');
60
+ const status = root.querySelector(".status");
61
+
62
+ toggle.addEventListener("click", () => root.classList.toggle("open"));
63
+
64
+ function setStatus(text, kind) {
65
+ status.textContent = text;
66
+ status.className = "status" + (kind ? " " + kind : "");
67
+ }
68
+
69
+ function renderResult(result) {
70
+ if (!result.ok) {
71
+ setStatus(result.message || `Refused (${result.status}).`, "error");
72
+ return;
73
+ }
74
+ if (result.status === "noop") {
75
+ setStatus(result.summary || "No change was needed.", "ok");
76
+ return;
77
+ }
78
+ setStatus(`✔ ${result.summary} — reloading…`, "ok");
79
+ // The file write already fires the existing content/theme HMR watch (see
80
+ // astro.config.mjs's tenantHotReload); nothing else to do here.
81
+ }
82
+
83
+ async function postJson(path, body) {
84
+ const res = await fetch(path, {
85
+ method: "POST",
86
+ headers: { "content-type": "application/json" },
87
+ body: JSON.stringify(body || {}),
88
+ });
89
+ return res.json();
90
+ }
91
+
92
+ askBtn.addEventListener("click", async () => {
93
+ const prompt = textarea.value.trim();
94
+ if (!prompt) return;
95
+ askBtn.disabled = true;
96
+ setStatus("Asking…");
97
+ try {
98
+ const result = await postJson("/__tot/ai-edit", { prompt });
99
+ renderResult(result);
100
+ } catch (e) {
101
+ setStatus(`Request failed: ${e.message}`, "error");
102
+ } finally {
103
+ askBtn.disabled = false;
104
+ }
105
+ });
106
+
107
+ undoBtn.addEventListener("click", async () => {
108
+ undoBtn.disabled = true;
109
+ setStatus("Undoing…");
110
+ try {
111
+ const result = await postJson("/__tot/ai-undo", {});
112
+ if (!result.ok) {
113
+ setStatus(result.message || "Nothing to undo.", "error");
114
+ } else {
115
+ setStatus(`↺ Reverted "${result.undid}" — reloading…`, "ok");
116
+ }
117
+ } catch (e) {
118
+ setStatus(`Request failed: ${e.message}`, "error");
119
+ } finally {
120
+ undoBtn.disabled = false;
121
+ }
122
+ });
123
+
124
+ textarea.addEventListener("keydown", (e) => {
125
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) askBtn.click();
126
+ });
127
+ })();
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Mint a browserless "get going" command for an ALREADY-AUTHENTICATED developer — the
3
+ * second half of the invite story. The developer accepted the magic link (OTP-free web
4
+ * sign-in) and landed on /dev; this hands them a single-copy, TWO-LINE paste — line 1 signs
5
+ * the terminal in with the one-time code, line 2 runs `start` (checkout → run → open the
6
+ * browser on the working site) — so they go from web sign-in to a running store with NO
7
+ * second browser round-trip and NO OTP.
8
+ *
9
+ * The token is minted by the MCP (POST /api/internal/dev-access/cli-signin-code), bound
10
+ * to the MCP's OWN audience, so it's redeemable only by that same MCP's /oauth/redeem-code
11
+ * — exactly what `tot login --code` does. We reuse the SAME seam as the app-binding writer:
12
+ * - MCP host ← the ORIGIN of MCP_APP_BINDING_URL (one source, so the mint host and the
13
+ * --mcp host the developer runs are provably the same MCP tier).
14
+ * - auth ← header x-mcp-service-secret: MCP_SERVICE_SHARED_SECRET (the storefront
15
+ * never holds the MCP's core credential).
16
+ *
17
+ * Line 1 appends `--mcp <origin>` ONLY when the MCP is not the CLI's built-in prod default —
18
+ * on prod the shortest form (`npx @tokenoftrust/cli login --code <token>`) is emitted.
19
+ */
20
+ import { readEnv } from "@/lib/env";
21
+
22
+ // The CLI's built-in default MCP (packages/cli login.mjs DEFAULT_MCP_URL). When the derived
23
+ // MCP origin equals this, --mcp is redundant noise, so we omit it.
24
+ const PROD_MCP_ORIGIN = "https://mcp.tokenoftrust.com";
25
+
26
+ export interface HttpTransport {
27
+ post(
28
+ url: string,
29
+ headers: Record<string, string>,
30
+ body: unknown,
31
+ ): Promise<{ status: number; json(): Promise<unknown> }>;
32
+ }
33
+
34
+ export class FetchHttpTransport implements HttpTransport {
35
+ async post(url: string, headers: Record<string, string>, body: unknown) {
36
+ const res = await fetch(url, {
37
+ method: "POST",
38
+ headers: { "Content-Type": "application/json", ...headers },
39
+ body: JSON.stringify(body),
40
+ });
41
+ return { status: res.status, json: () => res.json() };
42
+ }
43
+ }
44
+
45
+ export type CliSignInCodeResult =
46
+ | { ok: true; command: string; expiresAt: string | null }
47
+ | { ok: false; status: 502 | 503; reason: string };
48
+
49
+ /**
50
+ * Mint the ready-to-paste CLI sign-in command for `email`. The caller MUST pass the
51
+ * VERIFIED session email (never a client-supplied value) — a signed-in developer can only
52
+ * mint a code for themselves. Fail-closed: an unconfigured seam (no URL/secret) or a 401
53
+ * (MCP route unmounted / bad secret) is a 503; any other non-2xx / transport / parse fault
54
+ * is a 502. Never throws.
55
+ */
56
+ export async function mintCliSignInCommand(
57
+ email: string,
58
+ deps: { transport?: HttpTransport } = {},
59
+ ): Promise<CliSignInCodeResult> {
60
+ const bindingUrl = await readEnv("MCP_APP_BINDING_URL");
61
+ const secret = await readEnv("MCP_SERVICE_SHARED_SECRET");
62
+ if (!bindingUrl || !secret) {
63
+ return { ok: false, status: 503, reason: "not-configured" };
64
+ }
65
+ let mcpOrigin: string;
66
+ let endpoint: string;
67
+ try {
68
+ mcpOrigin = new URL(bindingUrl).origin;
69
+ // Mounted OUTSIDE /api on the MCP (the /api surface there is session-guarded); use the
70
+ // origin of MCP_APP_BINDING_URL, not its full path.
71
+ endpoint = `${mcpOrigin}/internal/dev-access/cli-signin-code`;
72
+ } catch {
73
+ return { ok: false, status: 503, reason: "bad-mcp-url" };
74
+ }
75
+
76
+ const transport = deps.transport ?? new FetchHttpTransport();
77
+ let res: { status: number; json(): Promise<unknown> };
78
+ try {
79
+ res = await transport.post(
80
+ endpoint,
81
+ { "x-mcp-service-secret": secret },
82
+ { email: email.toLowerCase() },
83
+ );
84
+ } catch (cause) {
85
+ return {
86
+ ok: false,
87
+ status: 502,
88
+ reason: "mcp request failed: " + (cause instanceof Error ? cause.message : String(cause)),
89
+ };
90
+ }
91
+
92
+ if (res.status < 200 || res.status >= 300) {
93
+ // 401 = MCP route unmounted or bad shared secret → treat as not-configured (503).
94
+ return {
95
+ ok: false,
96
+ status: res.status === 401 ? 503 : 502,
97
+ reason: `mcp cli-signin-code returned status ${res.status}`,
98
+ };
99
+ }
100
+
101
+ let payload: { token?: string; expiresAt?: string };
102
+ try {
103
+ payload = (await res.json()) as typeof payload;
104
+ } catch {
105
+ return { ok: false, status: 502, reason: "unparseable mcp response" };
106
+ }
107
+ if (!payload.token) {
108
+ return { ok: false, status: 502, reason: "mcp returned no token" };
109
+ }
110
+
111
+ const mcpFlag = mcpOrigin === PROD_MCP_ORIGIN ? "" : ` --mcp ${mcpOrigin}`;
112
+ // A single copy → a two-line paste that takes the developer all the way to a running
113
+ // store, no browser: line 1 signs the terminal in with the single-use code; line 2 checks
114
+ // out the store, runs it natively, and opens the browser on the working site — reusing the
115
+ // session line 1 just cached, so `start` never opens its own sign-in. Both lines use
116
+ // `npx @tokenoftrust/cli` (not bare `tot`): line 1 caches the package to the npx store, not
117
+ // onto PATH, so a bare `tot start` wouldn't resolve on a fresh machine.
118
+ //
119
+ // The trailing `&&` chains them: `start` runs ONLY if the code redeemed. Without it, a
120
+ // stale/burned code (single-use, short-TTL) would fail line 1 but line 2 would still run
121
+ // and fall back to `start`'s BROWSER sign-in — the exact surprise we're avoiding. With
122
+ // `&&`, an expired code stops with `login`'s clear "ask for a fresh invite" error instead.
123
+ const command = [
124
+ `npx @tokenoftrust/cli login --code ${payload.token}${mcpFlag} &&`,
125
+ `npx @tokenoftrust/cli start`,
126
+ ].join("\n");
127
+ return {
128
+ ok: true,
129
+ command,
130
+ expiresAt: payload.expiresAt ?? null,
131
+ };
132
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Redaction helpers for diagnostic logs — keep traces correlatable without persisting PII.
3
+ */
4
+
5
+ /** Mask an email for logs: keep the first char + the domain (`d***@example.com`). */
6
+ export function maskEmail(email: string): string {
7
+ const at = email.indexOf("@");
8
+ if (at <= 0) return "***";
9
+ return `${email[0]}***${email.slice(at)}`;
10
+ }
11
+
12
+ /**
13
+ * Email for a diagnostic line: the FULL address in local dev only (`import.meta.env.DEV`
14
+ * is a static boolean → this branch is tree-shaken from the production worker), where logs
15
+ * are local + gitignored and the raw address is needed for debugging; MASKED everywhere the
16
+ * build is deployed, so prod/worker logs never persist raw recipient PII.
17
+ */
18
+ export function diagEmail(email: string): string {
19
+ return import.meta.env.DEV ? email : maskEmail(email);
20
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Full-page edge cache for production browse reads (Epic Phase-2 D1).
3
+ *
4
+ * Phase 1 cached only the catalog *API* calls (HttpToTClient); the rendered HTML
5
+ * was re-built on every request even though browse pages change rarely. This
6
+ * caches the final HTML response in the Workers Cache API and short-circuits the
7
+ * whole render on a hit.
8
+ *
9
+ * Invalidation is a per-tenant **cache version** integer in TENANT_CACHE KV. Bump
10
+ * it (see api/cache-purge.ts, called by the ETL after a re-migration) and every
11
+ * old key becomes unreachable — tag-like purge with no URL enumeration, and no
12
+ * dependency on Enterprise cache-tags. Stale keys age out on their own.
13
+ *
14
+ * Deliberately NOT cached: preview/test output (per-developer, noindex), any
15
+ * response with Set-Cookie, and anything non-200/non-HTML. In `astro dev` there
16
+ * is no `caches` global, so every helper is a clean no-op.
17
+ *
18
+ * Note on the CSP nonce: a stored page carries the nonce it was rendered with, in
19
+ * both its inline tags and its CSP header, and is served to many users. That is
20
+ * safe — the nonce is not a secret; it only has to match within one identical,
21
+ * non-personalized document, which it does.
22
+ */
23
+
24
+ /** Synthetic internal origin for cache keys — never hits the network. */
25
+ const CACHE_ORIGIN = "https://page-cache.tot.internal";
26
+ const versionKey = (tenantId: string) => `cachever:${tenantId}`;
27
+
28
+ /** Default freshness window (s-maxage) and stale window for cached pages. */
29
+ const DEFAULT_TTL = 300;
30
+ const DEFAULT_SWR = 86_400;
31
+
32
+ /**
33
+ * Routes worth caching. Excludes the JSON typeahead (`?format=json`, high-q
34
+ * cardinality), `/api/*`, and anything with a preview token (handled upstream).
35
+ * Single-segment slugs (e.g. /about, /shipping-returns) cover editorial pages;
36
+ * non-200 responses are filtered at store time, so unknown slugs never persist.
37
+ */
38
+ export function isCacheablePath(pathname: string, params: URLSearchParams): boolean {
39
+ if (params.get("format") === "json") return false;
40
+ if (pathname.startsWith("/api/")) return false;
41
+ // Authenticated operator surfaces are per-viewer and MUST NOT be edge-cached —
42
+ // the bare `/dashboard` index is a single-segment slug that would otherwise
43
+ // match the editorial-page rule below and be served across viewers.
44
+ if (pathname === "/dashboard" || pathname.startsWith("/dashboard/")) return false;
45
+ // `/dev` is the authenticated developer surface — per-viewer (it renders the
46
+ // signed-in developer's own store, email, and gated demo), so like /dashboard
47
+ // it must never be shared across viewers. It's also the loudest failure if
48
+ // stale: its styles build to a CONTENT-HASHED asset (/_astro/dev.<hash>.css),
49
+ // so a cached old HTML keeps pointing at a hash that no longer exists after the
50
+ // next deploy → the stylesheet 404s and the page renders unstyled (its inline
51
+ // nonce'd <script> tags break the same way). Never cache it.
52
+ if (pathname === "/dev") return false;
53
+ if (pathname === "/" || pathname === "/search") return true;
54
+ if (pathname.startsWith("/products/")) return true;
55
+ if (pathname.startsWith("/collections/")) return true;
56
+ return /^\/[a-z0-9][a-z0-9-]*\/?$/.test(pathname);
57
+ }
58
+
59
+ export function pageCacheKey(
60
+ tenantId: string,
61
+ pathname: string,
62
+ search: string,
63
+ version: number,
64
+ ): Request {
65
+ return new Request(`${CACHE_ORIGIN}/${tenantId}/v${version}${pathname}${search}`);
66
+ }
67
+
68
+ /** Current cache version for a tenant (defaults to 1). KV read is edge-cached. */
69
+ export async function readCacheVersion(
70
+ kv: KVNamespace,
71
+ tenantId: string,
72
+ ): Promise<number> {
73
+ const raw = await kv.get(versionKey(tenantId), { cacheTtl: 60 });
74
+ const n = raw ? Number.parseInt(raw, 10) : Number.NaN;
75
+ return Number.isFinite(n) && n > 0 ? n : 1;
76
+ }
77
+
78
+ /** Bump a tenant's cache version (logical purge of all its pages). Returns the new version. */
79
+ export async function bumpCacheVersion(
80
+ kv: KVNamespace,
81
+ tenantId: string,
82
+ ): Promise<number> {
83
+ const next = (await readCacheVersion(kv, tenantId)) + 1;
84
+ await kv.put(versionKey(tenantId), String(next));
85
+ return next;
86
+ }
87
+
88
+ /** Return a cached page response (fresh clone), or null on miss. */
89
+ export async function getCachedPage(
90
+ cache: Cache,
91
+ key: Request,
92
+ ): Promise<Response | null> {
93
+ const hit = await cache.match(key);
94
+ if (!hit) return null;
95
+ const res = new Response(hit.body, hit);
96
+ res.headers.set("X-ToT-Cache", "hit");
97
+ return res;
98
+ }
99
+
100
+ /**
101
+ * Store a rendered page if it is safe to cache. No-op for non-200, non-HTML, or
102
+ * cookie-bearing responses. Sets a public Cache-Control so any fronting CDN can
103
+ * help too. Caller passes the FINAL response (after CSP headers are set).
104
+ */
105
+ export async function putCachedPage(
106
+ cache: Cache,
107
+ key: Request,
108
+ response: Response,
109
+ opts: { ttl?: number; swr?: number } = {},
110
+ ): Promise<void> {
111
+ if (response.status !== 200) return;
112
+ if (!(response.headers.get("content-type") ?? "").includes("text/html")) return;
113
+ if (response.headers.has("set-cookie")) return;
114
+
115
+ const ttl = opts.ttl ?? DEFAULT_TTL;
116
+ const swr = opts.swr ?? DEFAULT_SWR;
117
+ const cloned = response.clone();
118
+ const toStore = new Response(cloned.body, cloned);
119
+ toStore.headers.set(
120
+ "Cache-Control",
121
+ `public, s-maxage=${ttl}, stale-while-revalidate=${swr}`,
122
+ );
123
+ toStore.headers.set("X-ToT-Cache", "stored");
124
+ await cache.put(key, toStore).catch(() => void 0);
125
+ }
126
+
127
+ export const PAGE_CACHE_TTL = DEFAULT_TTL;
128
+ export const PAGE_CACHE_SWR = DEFAULT_SWR;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The storefront's OWN branded developer-invite email (magic-link flow). The storefront
3
+ * owns presentation now — this is our template, our brand, rendered here and handed to
4
+ * tot20 only for transport (see lib/messaging/messagesClient.ts). A single primary CTA
5
+ * links to the GET-safe /auth/magic landing carrying the one-time token.
6
+ *
7
+ * Email HTML rules: inline styles only (no <style>/external CSS — mail clients strip them),
8
+ * table-free simple layout, and a visible fallback URL under the button (some clients don't
9
+ * render buttons). The token rides in the URL but is only consumed by a POST on the landing
10
+ * page, so a scanner GET can't burn it.
11
+ */
12
+
13
+ /** Minimal HTML escape for untrusted interpolation (the inviter note). */
14
+ function esc(s: string): string {
15
+ return s
16
+ .replace(/&/g, "&amp;")
17
+ .replace(/</g, "&lt;")
18
+ .replace(/>/g, "&gt;")
19
+ .replace(/"/g, "&quot;");
20
+ }
21
+
22
+ export interface MagicLinkInviteEmailInput {
23
+ /** The full magic-link URL (…/auth/magic?token=…) — already token-carrying. */
24
+ magicUrl: string;
25
+ /** Display name of the store/experience. Default "Regulated Storefront". */
26
+ storeName?: string;
27
+ /** Optional personal note from the inviter (untrusted → escaped). */
28
+ note?: string;
29
+ }
30
+
31
+ export function renderMagicLinkInviteEmail(input: MagicLinkInviteEmailInput): {
32
+ subject: string;
33
+ html: string;
34
+ } {
35
+ const store = input.storeName?.trim() || "Regulated Storefront";
36
+ const subject = `Your invitation to ${store}`;
37
+ const url = input.magicUrl;
38
+ const note = input.note?.trim() ? esc(input.note.trim()) : null;
39
+
40
+ const html = `<!doctype html>
41
+ <html lang="en">
42
+ <head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /></head>
43
+ <body style="margin:0;padding:0;background:#f5f4f0;">
44
+ <div style="max-width:520px;margin:0 auto;padding:32px 20px;font:16px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#1c1c1c;">
45
+ <div style="font:600 12px/1 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.12em;text-transform:uppercase;color:#0f7b6c;margin:0 0 24px;">
46
+ Token of Trust&#169; &middot; ${esc(store)}
47
+ </div>
48
+ <h1 style="font-size:22px;line-height:1.3;letter-spacing:-.01em;margin:0 0 12px;color:#0b1f3a;">You've been invited</h1>
49
+ <p style="margin:0 0 8px;color:#3a3a3a;">You've been given developer access to <strong>${esc(store)}</strong>. Click below to sign in — no password needed.</p>
50
+ ${note ? `<div style="margin:16px 0;padding:12px 14px;background:#eef4ff;border:1px solid #c3d4f5;border-radius:8px;color:#234;font-size:14px;"><strong>Note from your inviter:</strong><br>${note}</div>` : ""}
51
+ <div style="margin:28px 0;">
52
+ <a href="${url}" style="display:inline-block;background:#0f7b6c;color:#ffffff;text-decoration:none;font-weight:600;font-size:16px;padding:14px 28px;border-radius:9px;">Sign in to ${esc(store)}</a>
53
+ </div>
54
+ <p style="margin:0 0 4px;font-size:13px;color:#6a6a6a;">Or paste this link into your browser:</p>
55
+ <p style="margin:0 0 24px;font-size:13px;word-break:break-all;"><a href="${url}" style="color:#0f7b6c;">${url}</a></p>
56
+ <p style="margin:0;font-size:12px;color:#9a9a9a;border-top:1px solid #e6e4de;padding-top:16px;">This is a single-use sign-in link. If you didn't expect this invitation, you can ignore this email.</p>
57
+ </div>
58
+ </body>
59
+ </html>`;
60
+
61
+ return { subject, html };
62
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Runtime env access that works in BOTH contexts:
3
+ * - `astro dev` (Node): reads import.meta.env / process.env.
4
+ * - Cloudflare Workers (prod): reads the `cloudflare:workers` env module.
5
+ *
6
+ * Astro v6+/@astrojs/cloudflare v14 REMOVED `Astro.locals.runtime.env`; the
7
+ * supported path is `import { env } from "cloudflare:workers"`. That module only
8
+ * exists in the worker runtime, so we import it lazily + guarded and fall back
9
+ * to build-time env in dev. See DECISIONS.md.
10
+ */
11
+ import type { D1Like } from "./d1/catalog.js";
12
+
13
+ let workerEnv: Record<string, unknown> | null | undefined;
14
+
15
+ async function getWorkerEnv(): Promise<Record<string, unknown> | null> {
16
+ if (workerEnv !== undefined) return workerEnv;
17
+ try {
18
+ // @ts-expect-error — virtual module, only resolvable in the Workers runtime.
19
+ const mod = await import("cloudflare:workers");
20
+ workerEnv = (mod.env ?? null) as Record<string, unknown> | null;
21
+ } catch {
22
+ workerEnv = null;
23
+ }
24
+ return workerEnv;
25
+ }
26
+
27
+ function fromBuild(key: string): string | undefined {
28
+ const meta = import.meta.env as Record<string, string | undefined>;
29
+ if (meta[key] != null) return meta[key];
30
+ if (typeof process !== "undefined" && process.env && process.env[key] != null) {
31
+ return process.env[key];
32
+ }
33
+ return undefined;
34
+ }
35
+
36
+ export async function readEnv(key: string): Promise<string | undefined> {
37
+ const w = await getWorkerEnv();
38
+ const wv = w?.[key];
39
+ if (typeof wv === "string" && wv.length) return wv;
40
+ return fromBuild(key);
41
+ }
42
+
43
+ /** KV binding for the newsletter stub / tenant cache, if present in the runtime. */
44
+ export async function readKv(binding: string): Promise<KVNamespace | undefined> {
45
+ const w = await getWorkerEnv();
46
+ const b = w?.[binding];
47
+ return (b as KVNamespace | undefined) ?? undefined;
48
+ }
49
+
50
+ /** D1 catalog binding for the edge read replica, if present in the runtime. */
51
+ export async function readD1(binding: string): Promise<D1Like | undefined> {
52
+ const w = await getWorkerEnv();
53
+ return (w?.[binding] as D1Like | undefined) ?? undefined;
54
+ }
55
+
56
+ /** R2 bucket binding (e.g. TENANT_ASSETS, PRODUCT_IMAGES), if present. */
57
+ export async function readR2(binding: string): Promise<R2Bucket | undefined> {
58
+ const w = await getWorkerEnv();
59
+ return (w?.[binding] as R2Bucket | undefined) ?? undefined;
60
+ }