@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,107 @@
1
+ /**
2
+ * POST /api/auth/magic-exchange — exchange a magic-link token for a signed-in session.
3
+ *
4
+ * The POST half of the magic-link flow (see /auth/magic for the GET-safe landing). The
5
+ * one-time `signInToken` (minted by tot20 via developerInvites `issueSignInToken:true`,
6
+ * embedded in the storefront's branded email) is exchanged HERE — only on a POST, only
7
+ * from the human clicking "Confirm sign in", never on a GET (inbox link-scanners fire
8
+ * GET and would burn the token).
9
+ *
10
+ * tot20 verifies + BURNS the token (single-use), resolves memberships fresh, and returns
11
+ * a signed federated `id_token` — identical contract to /api/auth/verify. We verify that
12
+ * assertion OFFLINE against tot's JWKS and derive the session from its claims (never trust
13
+ * unsigned JSON). Fail closed on any gap.
14
+ */
15
+ import type { APIContext } from "astro";
16
+ import {
17
+ getAccessClient,
18
+ getSessionStore,
19
+ setSessionCookie,
20
+ safeNext,
21
+ } from "@/lib/auth/route";
22
+ import {
23
+ getFederatedVerifier,
24
+ sessionFromFederatedClaims,
25
+ } from "@/lib/auth/identityToken";
26
+ import { createSession, DEFAULT_SESSION_TTL_SECONDS } from "@/lib/auth/session";
27
+
28
+ export const prerender = false;
29
+
30
+ /** Back to the landing page carrying an error (no token in the URL on failure). */
31
+ function magicRedirect(
32
+ context: APIContext,
33
+ params: { error?: string; next?: string },
34
+ ): Response {
35
+ const q = new URLSearchParams();
36
+ if (params.error) q.set("error", params.error);
37
+ if (params.next && params.next !== "/dashboard") q.set("next", params.next);
38
+ const qs = q.toString();
39
+ return context.redirect(`/auth/magic${qs ? `?${qs}` : ""}`, 303);
40
+ }
41
+
42
+ export async function POST(context: APIContext): Promise<Response> {
43
+ let token = "";
44
+ let next = "/dashboard";
45
+ try {
46
+ const form = await context.request.formData();
47
+ token = String(form.get("token") ?? "").trim();
48
+ next = safeNext(String(form.get("next") ?? "/dashboard"));
49
+ } catch {
50
+ return magicRedirect(context, { error: "invalid", next });
51
+ }
52
+ if (!token) return magicRedirect(context, { error: "missing", next });
53
+
54
+ const client = await getAccessClient();
55
+ const store = await getSessionStore();
56
+ if (!client || !store) {
57
+ return magicRedirect(context, { error: "unconfigured", next });
58
+ }
59
+
60
+ // Consume the token (single-use; burned server-side by tot20).
61
+ const result = await client.exchangeSignInToken(token);
62
+ if (!result.ok) {
63
+ // reason ∈ invalid|expired|error (tot20's "used" normalizes to invalid).
64
+ const error = result.reason === "expired" ? "expired" : "invalid";
65
+ return magicRedirect(context, { error, next });
66
+ }
67
+
68
+ // Prove authorization offline — fail closed with no flat-JSON fallback.
69
+ const idToken = result.id_token;
70
+ if (!idToken) {
71
+ console.error("auth/magic-exchange: no id_token on exchange; failing closed");
72
+ return magicRedirect(context, { error: "verify_failed", next });
73
+ }
74
+ const verifier = await getFederatedVerifier();
75
+ if (!verifier) {
76
+ console.error("auth/magic-exchange: federated verifier unconfigured");
77
+ return magicRedirect(context, { error: "unconfigured", next });
78
+ }
79
+
80
+ let derived;
81
+ try {
82
+ const claims = await verifier.verify(idToken); // enforces aud === STOREFRONT_APP_DOMAIN
83
+ derived = sessionFromFederatedClaims(claims);
84
+ } catch (err) {
85
+ console.error("auth/magic-exchange: id_token verification failed; failing closed", {
86
+ error: err instanceof Error ? err.message : String(err),
87
+ });
88
+ return magicRedirect(context, { error: "verify_failed", next });
89
+ }
90
+
91
+ const nowSeconds = Math.floor(Date.now() / 1000);
92
+ const { id } = await createSession(
93
+ store,
94
+ {
95
+ email: derived.email,
96
+ resource: derived.resource,
97
+ roles: [],
98
+ capability: "member",
99
+ tenants: derived.tenants,
100
+ staff: derived.staff,
101
+ },
102
+ DEFAULT_SESSION_TTL_SECONDS,
103
+ nowSeconds,
104
+ );
105
+ setSessionCookie(context, id, DEFAULT_SESSION_TTL_SECONDS);
106
+ return context.redirect(next, 303);
107
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * POST /api/auth/send — ask tot20 to email a one-time access code (FEDERATED).
3
+ *
4
+ * Federated multi-tenant sign-in: storefront authenticates as its OWN operator
5
+ * identity and asks tot20 to mail a code to the email if it holds a role on ANY
6
+ * tenant — no `resource` is sent (the resource-scoped `/access-code/send` stays
7
+ * for preview-host gating; console sign-in is federated). ENUMERATION-RESISTANT:
8
+ * the response is identical whether or not the email is eligible — we always
9
+ * redirect to the "enter your code" step. Accepts an HTML form post (no JS needed).
10
+ */
11
+ import type { APIContext } from "astro";
12
+ import { readKv } from "@/lib/env";
13
+ import {
14
+ EMAIL_RE,
15
+ getAccessClient,
16
+ safeNext,
17
+ loginRedirect,
18
+ } from "@/lib/auth/route";
19
+ import {
20
+ devAuthEnabled,
21
+ devAuthCode,
22
+ storeDevCode,
23
+ DEV_AUTH_CODE_TTL_SECONDS,
24
+ } from "@/lib/auth/devAuth";
25
+
26
+ export const prerender = false;
27
+
28
+ export async function POST(context: APIContext): Promise<Response> {
29
+ let email = "";
30
+ let next = "/";
31
+ try {
32
+ const form = await context.request.formData();
33
+ email = String(form.get("email") ?? "").trim().toLowerCase();
34
+ next = safeNext(String(form.get("next") ?? "/"));
35
+ } catch {
36
+ return loginRedirect(context, { error: "invalid", next });
37
+ }
38
+
39
+ // Bad address → send them back to step 1 with a gentle error (no code step).
40
+ if (!EMAIL_RE.test(email)) {
41
+ return loginRedirect(context, { error: "bad_email", next });
42
+ }
43
+
44
+ // LOCAL DEV AUTH (send side). `import.meta.env.DEV` is a STATIC boolean — this
45
+ // whole branch is tree-shaken out of the production build (same mechanism as the
46
+ // dev guards in middleware/index.ts). It NEVER weakens the hosted path.
47
+ //
48
+ // On localhost tot20 isn't reachable, so no email can be sent. When a developer
49
+ // opts in with `DEV_AUTH=1` in `.dev.vars`, we instead mint a one-time code,
50
+ // store it in the SESSION KV, and PRINT it to the server console so the developer
51
+ // can complete sign-in locally. (The verify side that ACCEPTS this code + mints a
52
+ // session without tot20 is the security-sensitive half — see the handoff
53
+ // `2026-07-09-storefront-local-dev-auth-mode.md`; until it lands, the code is
54
+ // stored + printed but sign-in completes only once that verify path is wired.)
55
+ if (import.meta.env.DEV && (await devAuthEnabled())) {
56
+ const kv = await readKv("SESSION");
57
+ if (kv) {
58
+ const code = devAuthCode();
59
+ await storeDevCode(kv, email, code);
60
+ // eslint-disable-next-line no-console -- dev-only server-console code delivery
61
+ console.log(
62
+ `\n 🔑 [DEV AUTH] sign-in code for ${email}: ${code}\n` +
63
+ ` valid ${DEV_AUTH_CODE_TTL_SECONDS / 60}m · printed here only (no email in local dev)\n`,
64
+ );
65
+ } else {
66
+ // eslint-disable-next-line no-console -- dev-only diagnostic
67
+ console.warn(
68
+ "[DEV AUTH] DEV_AUTH=1 but no SESSION KV is bound — cannot store a dev code.",
69
+ );
70
+ }
71
+ // Same opaque redirect as the real path (keeps the UX identical).
72
+ return loginRedirect(context, { sent: true, email, next });
73
+ }
74
+
75
+ const client = await getAccessClient();
76
+ if (!client) {
77
+ // Integration not configured — don't pretend we sent a code.
78
+ return loginRedirect(context, { error: "unconfigured", email, next });
79
+ }
80
+
81
+ // Fire-and-neutral: even a failure here resolves opaque so we never leak
82
+ // whether the address is a role-holder.
83
+ await client.sendFederatedCode(email);
84
+ return loginRedirect(context, { sent: true, email, next });
85
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * POST /api/auth/verify — verify the one-time code and mint the FEDERATED,
3
+ * multi-tenant signed-in session.
4
+ *
5
+ * Federated sign-in: storefront calls tot20's `/access-code/federated-verify`
6
+ * (as its own operator identity, no `resource`). tot20 checks the code and returns
7
+ * a signed `id_token` whose `aud` = the STOREFRONT app identity (not a tenant) and
8
+ * whose `tenants[]` carries ALL of the email's memberships.
9
+ *
10
+ * We verify that assertion OFFLINE against tot's JWKS with `aud` pinned to the
11
+ * storefront's configured app identity (`STOREFRONT_APP_DOMAIN`), then bind EVERY
12
+ * membership into the session — so the dashboard picker sees every vendor. Fail
13
+ * closed if the signed assertion is missing or verification fails (never trust the
14
+ * flat JSON). See `@/lib/auth/identityToken`.
15
+ */
16
+ import type { APIContext } from "astro";
17
+ import { readKv } from "@/lib/env";
18
+ import {
19
+ getAccessClient,
20
+ getSessionStore,
21
+ setSessionCookie,
22
+ safeNext,
23
+ loginRedirect,
24
+ } from "@/lib/auth/route";
25
+ import {
26
+ getFederatedVerifier,
27
+ sessionFromFederatedClaims,
28
+ } from "@/lib/auth/identityToken";
29
+ import { createSession, DEFAULT_SESSION_TTL_SECONDS } from "@/lib/auth/session";
30
+ import { devAuthEnabled, consumeDevCode, devSessionFields } from "@/lib/auth/devAuth";
31
+
32
+ export const prerender = false;
33
+
34
+ export async function POST(context: APIContext): Promise<Response> {
35
+ let email = "";
36
+ let code = "";
37
+ let next = "/";
38
+ try {
39
+ const form = await context.request.formData();
40
+ email = String(form.get("email") ?? "").trim().toLowerCase();
41
+ code = String(form.get("code") ?? "").trim();
42
+ next = safeNext(String(form.get("next") ?? "/"));
43
+ } catch {
44
+ return loginRedirect(context, { error: "invalid", next });
45
+ }
46
+
47
+ if (!email || !code) {
48
+ return loginRedirect(context, { sent: true, email, error: "invalid", next });
49
+ }
50
+
51
+ // LOCAL DEV AUTH (verify side). `import.meta.env.DEV` is a STATIC boolean — this
52
+ // whole branch (and the devAuth imports) are tree-shaken out of the production
53
+ // build, so the hosted path below (offline JWKS verification, fail-closed) is
54
+ // never weakened. Second gate: the DEV_AUTH opt-in. Consume the console-printed
55
+ // code stored by /api/auth/send and mint a local session WITHOUT tot20 — the
56
+ // localhost completion of the invite→sign-in loop. See @/lib/auth/devAuth.
57
+ if (import.meta.env.DEV && (await devAuthEnabled())) {
58
+ const store = await getSessionStore();
59
+ const kv = await readKv("SESSION");
60
+ if (!store || !kv) {
61
+ return loginRedirect(context, { sent: true, email, error: "unconfigured", next });
62
+ }
63
+ if (!(await consumeDevCode(kv, email, code))) {
64
+ // Same "code didn't match" UX as the real path (wrong/expired/replayed code).
65
+ return loginRedirect(context, { sent: true, email, error: "invalid", next });
66
+ }
67
+ const nowSeconds = Math.floor(Date.now() / 1000);
68
+ const { id } = await createSession(
69
+ store,
70
+ await devSessionFields(email),
71
+ DEFAULT_SESSION_TTL_SECONDS,
72
+ nowSeconds,
73
+ );
74
+ setSessionCookie(context, id, DEFAULT_SESSION_TTL_SECONDS);
75
+ return context.redirect(next, 303);
76
+ }
77
+
78
+ const client = await getAccessClient();
79
+ const store = await getSessionStore();
80
+ if (!client || !store) {
81
+ return loginRedirect(context, { sent: true, email, error: "unconfigured", next });
82
+ }
83
+
84
+ const result = await client.verifyFederatedCode(email, code);
85
+ if (!result.ok) {
86
+ // Show the code step again with the specific reason (invalid/expired/too_many_attempts).
87
+ return loginRedirect(context, { sent: true, email, error: result.reason, next });
88
+ }
89
+
90
+ // Prove authorization offline. Fail closed (no flat-JSON fallback) if the signed
91
+ // assertion is missing or fails verification.
92
+ const idToken = result.id_token;
93
+ if (!idToken) {
94
+ console.error("auth/verify: no id_token on federated result; failing closed");
95
+ return loginRedirect(context, { sent: true, email, error: "verify_failed", next });
96
+ }
97
+
98
+ // aud is the STOREFRONT app identity (STOREFRONT_APP_DOMAIN), not a tenant.
99
+ const verifier = await getFederatedVerifier();
100
+ if (!verifier) {
101
+ console.error("auth/verify: federated verifier unconfigured (TOT_CORE_URL/STOREFRONT_APP_DOMAIN)");
102
+ return loginRedirect(context, { sent: true, email, error: "unconfigured", next });
103
+ }
104
+
105
+ let derived;
106
+ try {
107
+ const claims = await verifier.verify(idToken); // enforces aud === STOREFRONT_APP_DOMAIN
108
+ // Bind EVERY membership — no single-tenant scoping (multi-vendor picker input).
109
+ derived = sessionFromFederatedClaims(claims);
110
+ } catch (err) {
111
+ console.error("auth/verify: federated id_token verification failed; failing closed", {
112
+ error: err instanceof Error ? err.message : String(err),
113
+ });
114
+ return loginRedirect(context, { sent: true, email, error: "verify_failed", next });
115
+ }
116
+
117
+ const nowSeconds = Math.floor(Date.now() / 1000);
118
+ const { id } = await createSession(
119
+ store,
120
+ {
121
+ email: derived.email,
122
+ // The verified aud (storefront app identity) — the console session's scope.
123
+ resource: derived.resource,
124
+ // Per-tenant roles live in tenants[]; the console session itself is role-neutral.
125
+ roles: [],
126
+ capability: "member",
127
+ tenants: derived.tenants,
128
+ staff: derived.staff,
129
+ },
130
+ DEFAULT_SESSION_TTL_SECONDS,
131
+ nowSeconds,
132
+ );
133
+ setSessionCookie(context, id, DEFAULT_SESSION_TTL_SECONDS);
134
+ return context.redirect(next, 303);
135
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * POST /api/cache-purge — bump a tenant's edge page-cache version.
3
+ *
4
+ * The ETL load step calls this per tenant after a (re)migration so freshly
5
+ * migrated content is visible immediately, rather than waiting out the page
6
+ * TTL. This closes the loop that HttpToTClient.purgeTenant() (catalog API cache)
7
+ * only half-covered: that purges the data cache; this purges the rendered HTML.
8
+ *
9
+ * Auth: `Authorization: Bearer <CACHE_PURGE_SECRET>`. JSON bodies are exempt from
10
+ * Astro's CSRF origin check, so the server-to-server ETL caller works.
11
+ */
12
+ import type { APIContext } from "astro";
13
+ import { readEnv, readKv } from "@/lib/env";
14
+ import { bumpCacheVersion } from "@/lib/edgeCache";
15
+
16
+ export const prerender = false;
17
+
18
+ export async function POST(context: APIContext): Promise<Response> {
19
+ const secret = await readEnv("CACHE_PURGE_SECRET");
20
+ const auth = context.request.headers.get("authorization");
21
+ if (!secret || auth !== `Bearer ${secret}`) {
22
+ return new Response("Unauthorized", { status: 401 });
23
+ }
24
+
25
+ const kv = await readKv("TENANT_CACHE");
26
+ if (!kv) {
27
+ return new Response("No cache store bound", { status: 503 });
28
+ }
29
+
30
+ let tenantId: string | undefined;
31
+ try {
32
+ tenantId = ((await context.request.json()) as { tenantId?: string })?.tenantId;
33
+ } catch {
34
+ /* fall through to 400 */
35
+ }
36
+ if (!tenantId) {
37
+ return new Response("tenantId required", { status: 400 });
38
+ }
39
+
40
+ const version = await bumpCacheVersion(kv, tenantId);
41
+ return new Response(JSON.stringify({ tenantId, version }), {
42
+ headers: { "content-type": "application/json" },
43
+ });
44
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * POST /api/content-edit/ai-edit — the `/dev` page's "Build with Claude" live demo
3
+ * (WS5 G4). Same shape as `tot dev`'s inline-AI loop (G1): a prompt turns into
4
+ * a reviewable per-file diff via tot-mcp's `content_edit` tool.
5
+ *
6
+ * AUTH (see the G4 design note — pending WS2/beta confirmation): `/dev`'s own
7
+ * signed-in-viewer session (`readViewerSession`) is the developer-level gate,
8
+ * same as the rest of `/dev`; the MCP call itself authenticates as the
9
+ * storefront's own TENANT-OPERATOR credential (`getContentEditClient`, the
10
+ * SAME credential_validate path `/api/changes/accept` already uses) — a
11
+ * server-to-server trust boundary, not the individual developer's identity.
12
+ *
13
+ * CONTENT SOURCE (flagged, not yet real): this demo edits a small, fixed
14
+ * SANDBOX fixture below — NOT the caller's real tenant content. Wiring a real
15
+ * sandbox tenant's live content/theme through the customization/publish
16
+ * pipeline (LookFoundation) is follow-up work; this proves the live loop
17
+ * (prompt → content_edit → diff) end to end without that dependency.
18
+ */
19
+ import type { APIContext } from "astro";
20
+ import { readViewerSession } from "@/lib/auth/route";
21
+ import { getContentEditClient } from "@/lib/content-edit";
22
+ import { json } from "@/lib/grants/route.js";
23
+
24
+ export const prerender = false;
25
+
26
+ /** The `/dev` demo's fixed sandbox content — see the module header. */
27
+ const SANDBOX_FILES = [
28
+ {
29
+ path: "content/home.html",
30
+ content: "<!doctype html>\n<title>Sandbox Store</title>\n<h1>Welcome to Sandbox Store</h1>\n",
31
+ },
32
+ {
33
+ path: "theme.json",
34
+ content: JSON.stringify({ color: { bg: "#faf9f5", text: "#16191d", accent: "#e07a5f" } }, null, 2),
35
+ },
36
+ ];
37
+
38
+ export async function POST(context: APIContext): Promise<Response> {
39
+ const session = await readViewerSession({ cookies: context.cookies });
40
+ if (!session) return json({ error: "sign in required" }, 401);
41
+ if (!session.capability || session.capability === "none") {
42
+ return json({ error: "no developer capability on this account" }, 403);
43
+ }
44
+
45
+ let body: { prompt?: unknown };
46
+ try {
47
+ body = (await context.request.json()) as typeof body;
48
+ } catch {
49
+ return json({ error: "invalid JSON body" }, 400);
50
+ }
51
+ const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
52
+ if (!prompt) return json({ error: "prompt is required" }, 400);
53
+
54
+ const client = await getContentEditClient();
55
+ if (client instanceof Response) return client;
56
+
57
+ try {
58
+ const result = await client.editContent(prompt, SANDBOX_FILES);
59
+ return json({ ok: true, ...result });
60
+ } catch (err) {
61
+ const message = err instanceof Error ? err.message : String(err);
62
+ return json({ ok: false, error: message }, 502);
63
+ }
64
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * CSP violation collector (enables the report-only → enforce
3
+ * rollout). Browsers POST violation reports here (via `report-to` /
4
+ * `report-uri`); we log them so platform + tenant-script CSP violations surface
5
+ * before we flip the policy from Report-Only to enforcing. Returns 204.
6
+ *
7
+ * Phase 1: log to the worker console. A later iteration can forward to an
8
+ * analytics/observability sink.
9
+ */
10
+ import type { APIRoute } from "astro";
11
+
12
+ export const prerender = false;
13
+
14
+ export const POST: APIRoute = async ({ request }) => {
15
+ try {
16
+ const body = await request.text();
17
+ if (body) {
18
+ // Reports can be `application/csp-report` or `application/reports+json`.
19
+ console.warn("[csp-report]", body.slice(0, 4000));
20
+ }
21
+ } catch {
22
+ /* never fail a report submission */
23
+ }
24
+ return new Response(null, { status: 204 });
25
+ };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * POST /api/dashboard/enter-vendor { vendorAppDomain } — the ToT-STAFF admission
3
+ * endpoint (#8425 staff access). Phase 2 of two-phase admission: on the staff
4
+ * viewer explicitly selecting a searched vendor, tot20 writes the server-side
5
+ * AUDIT row and returns the staffRoles-scoped capability; we then stamp a
6
+ * short-lived {@link StaffSelection} on the session and admit the viewer to
7
+ * `/dashboard/<appDomain>` scoped to that capability.
8
+ *
9
+ * AUTHORIZATION — two independent axes:
10
+ * NEAR (this endpoint): a valid `tot_session` carrying ToT-STAFF scope
11
+ * (`staff[]` non-empty). No session → 401; no staff scope → 403.
12
+ * FAR (tot20): `/clients/staffVendorAccess`, operator-authed as storefront,
13
+ * presenting a storefront-signed staff broker assertion in the
14
+ * `X-ToT-Broker-Assertion` header (minted from the verified session email).
15
+ * tot20 verifies it, re-checks staff scope from its `sub`, scopes the capability,
16
+ * and writes the audit row — it is the authority. No plaintext email on the wire.
17
+ *
18
+ * The staff selection is a DISTINCT access path from `tenants[]`: we NEVER
19
+ * synthesize a membership. The dashboard guard + login gate consult the marker
20
+ * separately (see tenantSelection.staffAdmits / loginGate.sessionAdmitsHost).
21
+ */
22
+ import type { APIContext } from "astro";
23
+ import { readViewerSession, getSessionMutationHandle } from "@/lib/auth/route";
24
+ import { updateSession } from "@/lib/auth/session";
25
+ import { isStaffSession, buildStaffSelection } from "@/lib/dashboard/staffAdmission";
26
+ import {
27
+ createStaffVendorAccessClient,
28
+ staffAccessHttpStatus,
29
+ StaffVendorAccessClientError,
30
+ } from "@/lib/dashboard/staffVendorAccess";
31
+ import { mintBrokerAssertion, BrokerAssertionError } from "@/lib/auth/brokerAssertion";
32
+
33
+ export const prerender = false;
34
+
35
+ /** JSON response with no-store (staff access state must never be edge-cached). */
36
+ function json(body: unknown, status = 200): Response {
37
+ return new Response(JSON.stringify(body), {
38
+ status,
39
+ headers: { "content-type": "application/json", "cache-control": "no-store" },
40
+ });
41
+ }
42
+
43
+ export async function POST(context: APIContext): Promise<Response> {
44
+ const session = await readViewerSession(context);
45
+ if (!session) return json({ error: "not signed in" }, 401);
46
+ if (!isStaffSession(session)) {
47
+ return json({ error: "staff access required", reason: "notStaff" }, 403);
48
+ }
49
+
50
+ let body: { vendorAppDomain?: unknown };
51
+ try {
52
+ body = (await context.request.json()) as typeof body;
53
+ } catch {
54
+ return json({ error: "invalid JSON body" }, 400);
55
+ }
56
+ const vendorAppDomain =
57
+ typeof body.vendorAppDomain === "string" ? body.vendorAppDomain.trim() : "";
58
+ if (!vendorAppDomain) return json({ error: "vendorAppDomain is required" }, 400);
59
+
60
+ let client;
61
+ try {
62
+ client = await createStaffVendorAccessClient();
63
+ } catch (err) {
64
+ if (err instanceof StaffVendorAccessClientError) {
65
+ return json({ error: "staff-access integration not configured" }, 503);
66
+ }
67
+ throw err;
68
+ }
69
+
70
+ // #8453 `staff-invite-proof-via-broker`: prove the staff actor with a fresh
71
+ // storefront-signed assertion minted from the VERIFIED session email. Fail closed
72
+ // (503) if the signing key is unconfigured.
73
+ let brokerAssertion: string;
74
+ try {
75
+ brokerAssertion = await mintBrokerAssertion(session.email);
76
+ } catch (err) {
77
+ if (err instanceof BrokerAssertionError) {
78
+ return json({ error: "staff-proof signing not configured" }, 503);
79
+ }
80
+ throw err;
81
+ }
82
+
83
+ // FAR authority: tot20 verifies the assertion, derives the staff actor, scopes the
84
+ // capability, and writes the audit row. We admit ONLY on its ok.
85
+ const result = await client.grantStaffAccess(brokerAssertion, vendorAppDomain);
86
+ if (!result.ok) {
87
+ return json(
88
+ { error: result.message, reason: result.reason, code: result.code },
89
+ staffAccessHttpStatus(result.reason, result.status),
90
+ );
91
+ }
92
+
93
+ // Stamp the distinct staff-selection marker on the session (short-lived, scoped
94
+ // to the returned capability). Never a tenants[] entry.
95
+ const nowSeconds = Math.floor(Date.now() / 1000);
96
+ const selection = buildStaffSelection({
97
+ resource: vendorAppDomain,
98
+ capability: result.capability,
99
+ nowSeconds,
100
+ });
101
+ const handle = await getSessionMutationHandle(context);
102
+ if (!handle) {
103
+ // No SESSION KV / no cookie (e.g. plain `astro dev`) — the audit row is
104
+ // written but we can't persist the marker. Fail closed so the viewer isn't
105
+ // told they're admitted when the guard can't see it.
106
+ return json({ error: "session store unavailable; cannot persist admission" }, 503);
107
+ }
108
+ const updated = await updateSession(
109
+ handle.store,
110
+ handle.id,
111
+ (rec) => ({ ...rec, staffSelection: selection }),
112
+ nowSeconds,
113
+ );
114
+ if (!updated) {
115
+ return json({ error: "session expired; sign in again" }, 401);
116
+ }
117
+
118
+ return json({
119
+ ok: true,
120
+ appDomain: vendorAppDomain,
121
+ capability: result.capability,
122
+ redirect: `/dashboard/${encodeURIComponent(vendorAppDomain)}`,
123
+ });
124
+ }