@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,449 @@
1
+ /**
2
+ * StaffVendorAccessClient — the storefront's seam to tot20's two ToT-STAFF
3
+ * cross-vendor endpoints (#8425 staff access). A ToT-staff user isn't a member of
4
+ * a bounded set of vendors; they reach ANY vendor by explicit SEARCH-and-ENTER.
5
+ * Both endpoints are operator-authed (storefront's OWN per-tier credentials) and
6
+ * PROVE the staff actor with a storefront-signed BROKER ASSERTION in the
7
+ * `X-ToT-Broker-Assertion` header (#8453 `staff-invite-proof-via-broker`) — NOT a
8
+ * plaintext email field (spoofable, removed). tot20 verifies the assertion, derives
9
+ * the staff actor from its `sub`, scopes the capability to that user's staffRoles,
10
+ * and writes the server-side AUDIT row.
11
+ *
12
+ * GET /api/clients/vendorSearch?q=<prefix>&limit=<n> (operator secretKey; staff via header)
13
+ * → { content: { code, vendors: [{ appDomain, displayName }] } }
14
+ * POST /api/clients/staffVendorAccess { vendorAppDomain } (operator secretKey; staff via header)
15
+ * → { content: { code, capability } } (WRITES the audit row; capability is
16
+ * staffRoles-scoped)
17
+ *
18
+ * AUTH WIRE (mirrors `@/lib/grants/developerInvites` + `@/lib/auth/totAccessClient`,
19
+ * verified against live qa): tot20's swagger `normalizeSecureUserRequest` extracts
20
+ * machine creds from the request BODY/QUERY fields `totApiKey`/`totSecretKey`, NOT
21
+ * custom `x-tot-*` headers. That is the ONLY integration-sensitive spot
22
+ * ({@link credentialFields}). tot20 wraps every response in a `{ content, metadata }`
23
+ * envelope.
24
+ *
25
+ * TIER: host + operator credential are per-DEPLOY env (`TOT_CORE_URL` +
26
+ * `TOT_API_KEY`/`TOT_SECRET_KEY`) — the SAME env the sibling access + invite
27
+ * clients read; no hardcoded hosts, no secrets in code.
28
+ *
29
+ * Built like `@/lib/grants/developerInvites` (interface + Http impl + Mock + env
30
+ * factory) so the routes develop/test against `MockStaffVendorAccessClient` and
31
+ * swap in the Http client with no call-site change.
32
+ */
33
+ import { readEnv } from "@/lib/env";
34
+ import { decodeJwt } from "jose";
35
+
36
+ /**
37
+ * The header carrying the storefront-signed staff BROKER ASSERTION to tot20 (#8453
38
+ * `staff-invite-proof-via-broker`). The staff actor is PROVEN by this signed JWT,
39
+ * not by a plaintext `email` field (which was spoofable and has been removed from
40
+ * the wire). Minted server-side (`@/lib/auth/brokerAssertion`) from the verified
41
+ * `session.email`; tot20 verifies signature + replay and derives the staff actor
42
+ * from the VERIFIED `sub`, scopes the capability to their staffRoles, and writes
43
+ * the audit row. The literal is inlined (not imported) to keep this module's
44
+ * transport seam self-contained, mirroring the sibling grants client.
45
+ */
46
+ const BROKER_ASSERTION_HEADER = "X-ToT-Broker-Assertion";
47
+
48
+ /** One vendor hit from a search — the minimum needed to render + select. */
49
+ export interface VendorHit {
50
+ appDomain: string;
51
+ displayName: string;
52
+ }
53
+
54
+ /** Result of a vendor search (typed failure, fail-closed). */
55
+ export type VendorSearchResult =
56
+ | { ok: true; vendors: VendorHit[] }
57
+ | { ok: false; status: number; reason: StaffAccessFailureReason; message: string; code?: string };
58
+
59
+ /** Result of a staff-vendor-access grant — the staffRoles-scoped capability. */
60
+ export type StaffVendorAccessResult =
61
+ | { ok: true; capability: string }
62
+ | { ok: false; status: number; reason: StaffAccessFailureReason; message: string; code?: string };
63
+
64
+ /** Typed failure reasons mapped from tot20's error codes / HTTP status. */
65
+ export type StaffAccessFailureReason =
66
+ | "notStaff" // 403 — the email holds no ToT-staff scope (server-authoritative)
67
+ | "notFound" // 404/422 — the vendor appDomain isn't a resolvable app
68
+ | "invalid" // 400/422 — bad/empty query or vendor
69
+ | "notAuthenticated" // 401/403 — storefront's operator credential was rejected
70
+ | "error"; // transport / unexpected upstream fault
71
+
72
+ export interface StaffVendorAccessClient {
73
+ /**
74
+ * Search vendors by appDomain prefix. `brokerAssertion` is the storefront-signed
75
+ * staff proof (presented in the `X-ToT-Broker-Assertion` header); tot20 derives
76
+ * the staff actor from its verified `sub` and scopes results to their staffRoles.
77
+ */
78
+ searchVendors(brokerAssertion: string, query: string, limit?: number): Promise<VendorSearchResult>;
79
+ /**
80
+ * Admit the staff actor (proven by `brokerAssertion`) to `vendorAppDomain`: tot20
81
+ * verifies the assertion, writes the audit row, and returns the staffRoles-scoped
82
+ * capability. Called on explicit enter, never a silent per-host grant.
83
+ */
84
+ grantStaffAccess(brokerAssertion: string, vendorAppDomain: string): Promise<StaffVendorAccessResult>;
85
+ }
86
+
87
+ // --- HTTP transport seam (injected by tests) --------------------------------
88
+
89
+ export interface HttpResponseLike {
90
+ status: number;
91
+ json(): Promise<unknown>;
92
+ }
93
+ export interface HttpTransport {
94
+ get(url: string, headers: Record<string, string>): Promise<HttpResponseLike>;
95
+ post(url: string, headers: Record<string, string>, body: unknown): Promise<HttpResponseLike>;
96
+ }
97
+
98
+ /** Default transport: JSON `fetch` GET/POST. */
99
+ export class FetchHttpTransport implements HttpTransport {
100
+ async get(url: string, headers: Record<string, string>) {
101
+ return fetch(url, { method: "GET", headers: { accept: "application/json", ...headers } });
102
+ }
103
+ async post(url: string, headers: Record<string, string>, body: unknown) {
104
+ return fetch(url, {
105
+ method: "POST",
106
+ headers: { "content-type": "application/json", accept: "application/json", ...headers },
107
+ body: JSON.stringify(body),
108
+ });
109
+ }
110
+ }
111
+
112
+ export interface HttpStaffVendorAccessClientConfig {
113
+ /** tot20 core API base (no trailing slash), e.g. https://qa.tokenoftrust.com */
114
+ baseUrl: string;
115
+ /** Storefront's OWN per-tier ToT credentials (machine auth to the operator API). */
116
+ apiKey: string;
117
+ secretKey?: string;
118
+ transport?: HttpTransport;
119
+ /** Default result cap when a caller passes no limit. */
120
+ defaultLimit?: number;
121
+ }
122
+
123
+ export class StaffVendorAccessClientError extends Error {}
124
+
125
+ const DEFAULT_LIMIT = 10;
126
+ const MAX_LIMIT = 50;
127
+
128
+ /** Clamp a caller-supplied limit into (0, MAX_LIMIT], defaulting when absent/bad. */
129
+ function clampLimit(limit: number | undefined, fallback: number): number {
130
+ const n = typeof limit === "number" ? Math.floor(limit) : NaN;
131
+ if (!Number.isFinite(n) || n <= 0) return fallback;
132
+ return Math.min(n, MAX_LIMIT);
133
+ }
134
+
135
+ // --- HTTP impl --------------------------------------------------------------
136
+
137
+ export class HttpStaffVendorAccessClient implements StaffVendorAccessClient {
138
+ private readonly baseUrl: string;
139
+ private readonly apiKey: string;
140
+ private readonly secretKey?: string;
141
+ private readonly transport: HttpTransport;
142
+ private readonly defaultLimit: number;
143
+
144
+ constructor(config: HttpStaffVendorAccessClientConfig) {
145
+ if (!config.baseUrl) {
146
+ throw new StaffVendorAccessClientError("staffVendorAccess: TOT_CORE_URL is not configured");
147
+ }
148
+ if (!config.apiKey) {
149
+ throw new StaffVendorAccessClientError(
150
+ "staffVendorAccess: storefront ToT credentials are not configured",
151
+ );
152
+ }
153
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
154
+ this.apiKey = config.apiKey;
155
+ this.secretKey = config.secretKey;
156
+ this.transport = config.transport ?? new FetchHttpTransport();
157
+ this.defaultLimit = clampLimit(config.defaultLimit, DEFAULT_LIMIT);
158
+ }
159
+
160
+ /**
161
+ * The one integration-sensitive spot: tot20 reads `totApiKey`/`totSecretKey`
162
+ * from the request body/query (`normalizeSecureUserRequest`). Swap for a
163
+ * machine-JWT `Authorization: Bearer` here if tot20 ever requires it.
164
+ */
165
+ private credentialFields(): Record<string, string> {
166
+ const f: Record<string, string> = { totApiKey: this.apiKey };
167
+ if (this.secretKey) f.totSecretKey = this.secretKey;
168
+ return f;
169
+ }
170
+
171
+ async searchVendors(brokerAssertion: string, query: string, limit?: number): Promise<VendorSearchResult> {
172
+ const q = (query ?? "").trim();
173
+ if (!q) return { ok: true, vendors: [] }; // empty prefix → nothing to search
174
+ // #8453 `staff-invite-proof-via-broker`: the staff actor is proven by the signed
175
+ // assertion header — NEVER a plaintext `email` query param.
176
+ const params = new URLSearchParams({
177
+ q,
178
+ limit: String(clampLimit(limit, this.defaultLimit)),
179
+ ...this.credentialFields(),
180
+ });
181
+ const headers: Record<string, string> = {};
182
+ if (brokerAssertion) headers[BROKER_ASSERTION_HEADER] = brokerAssertion;
183
+ let res: HttpResponseLike;
184
+ try {
185
+ res = await this.transport.get(`${this.baseUrl}/api/clients/vendorSearch?${params.toString()}`, headers);
186
+ } catch (cause) {
187
+ return {
188
+ ok: false,
189
+ status: 0,
190
+ reason: "error",
191
+ message:
192
+ "staffVendorAccess: transport failure searching vendors " +
193
+ (cause instanceof Error ? cause.message : String(cause)),
194
+ };
195
+ }
196
+ let parsed: unknown;
197
+ try {
198
+ parsed = await res.json();
199
+ } catch {
200
+ return {
201
+ ok: false,
202
+ status: res.status,
203
+ reason: "error",
204
+ message: `staffVendorAccess: tot20 returned a non-JSON body (status ${res.status})`,
205
+ };
206
+ }
207
+ return normalizeVendorSearch(res.status, parsed);
208
+ }
209
+
210
+ async grantStaffAccess(brokerAssertion: string, vendorAppDomain: string): Promise<StaffVendorAccessResult> {
211
+ const vendor = (vendorAppDomain ?? "").trim();
212
+ if (!vendor) {
213
+ return { ok: false, status: 400, reason: "invalid", message: "a vendor appDomain is required" };
214
+ }
215
+ // #8453 `staff-invite-proof-via-broker`: the staff actor is proven by the signed
216
+ // assertion header — NEVER a plaintext `email` body field.
217
+ const headers: Record<string, string> = {};
218
+ if (brokerAssertion) headers[BROKER_ASSERTION_HEADER] = brokerAssertion;
219
+ let res: HttpResponseLike;
220
+ try {
221
+ res = await this.transport.post(
222
+ `${this.baseUrl}/api/clients/staffVendorAccess`,
223
+ headers,
224
+ { vendorAppDomain: vendor, ...this.credentialFields() },
225
+ );
226
+ } catch (cause) {
227
+ return {
228
+ ok: false,
229
+ status: 0,
230
+ reason: "error",
231
+ message:
232
+ "staffVendorAccess: transport failure granting access " +
233
+ (cause instanceof Error ? cause.message : String(cause)),
234
+ };
235
+ }
236
+ let parsed: unknown;
237
+ try {
238
+ parsed = await res.json();
239
+ } catch {
240
+ return {
241
+ ok: false,
242
+ status: res.status,
243
+ reason: "error",
244
+ message: `staffVendorAccess: tot20 returned a non-JSON body (status ${res.status})`,
245
+ };
246
+ }
247
+ return normalizeStaffAccess(res.status, parsed);
248
+ }
249
+ }
250
+
251
+ /** Unwrap tot20's `{ content, metadata }` envelope (fall back to the raw body). */
252
+ function unwrap(body: unknown): Record<string, unknown> {
253
+ const raw = (body ?? {}) as Record<string, unknown>;
254
+ return (raw.content && typeof raw.content === "object" ? raw.content : raw) as Record<string, unknown>;
255
+ }
256
+
257
+ /** Parse tot20's vendorSearch response into a typed result (fail closed). */
258
+ export function normalizeVendorSearch(status: number, body: unknown): VendorSearchResult {
259
+ const c = unwrap(body);
260
+ const code = typeof c.code === "string" ? c.code : undefined;
261
+ const isOk =
262
+ code === "vendorSearch:ok" || (status >= 200 && status < 300 && Array.isArray(c.vendors));
263
+ if (isOk) {
264
+ const vendors: VendorHit[] = (Array.isArray(c.vendors) ? c.vendors : [])
265
+ .map((raw2) => {
266
+ const r = (raw2 ?? {}) as Record<string, unknown>;
267
+ const appDomain = typeof r.appDomain === "string" ? r.appDomain : "";
268
+ return {
269
+ appDomain,
270
+ displayName: typeof r.displayName === "string" && r.displayName ? r.displayName : appDomain,
271
+ };
272
+ })
273
+ .filter((v) => v.appDomain);
274
+ return { ok: true, vendors };
275
+ }
276
+ return failure(status, c);
277
+ }
278
+
279
+ /** Parse tot20's staffVendorAccess response into a typed result (fail closed). */
280
+ export function normalizeStaffAccess(status: number, body: unknown): StaffVendorAccessResult {
281
+ const c = unwrap(body);
282
+ const code = typeof c.code === "string" ? c.code : undefined;
283
+ const isOk =
284
+ code === "staffVendorAccess:granted" ||
285
+ (status >= 200 && status < 300 && typeof c.capability === "string");
286
+ if (isOk) {
287
+ return { ok: true, capability: typeof c.capability === "string" ? c.capability : "staff" };
288
+ }
289
+ return failure(status, c);
290
+ }
291
+
292
+ /** Map an error envelope onto a typed failure by tot20 `code` then HTTP status. */
293
+ function failure(
294
+ status: number,
295
+ c: Record<string, unknown>,
296
+ ): { ok: false; status: number; reason: StaffAccessFailureReason; message: string; code?: string } {
297
+ const code = typeof c.code === "string" ? c.code : undefined;
298
+ const message = typeof c.message === "string" ? c.message : undefined;
299
+ const reason = reasonFor(code, status);
300
+ return { ok: false, status, reason, message: message ?? defaultMessage(reason, status), ...(code ? { code } : {}) };
301
+ }
302
+
303
+ function reasonFor(code: string | undefined, status: number): StaffAccessFailureReason {
304
+ switch (code) {
305
+ case "staffVendorAccess:notStaff":
306
+ return "notStaff";
307
+ case "staffVendorAccess:vendorNotFound":
308
+ case "vendorSearch:vendorNotFound":
309
+ return "notFound";
310
+ case "staffVendorAccess:invalid":
311
+ case "vendorSearch:invalid":
312
+ return "invalid";
313
+ case "operatorGrants:notAuthenticated":
314
+ return "notAuthenticated";
315
+ default:
316
+ if (status === 401) return "notAuthenticated";
317
+ if (status === 403) return "notStaff";
318
+ if (status === 404) return "notFound";
319
+ if (status === 400 || status === 422) return "invalid";
320
+ return "error";
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Map a typed failure onto the HTTP status a storefront endpoint should return:
326
+ * notStaff/notAuthenticated → 403; notFound → 404; invalid → 400; a transport /
327
+ * unexpected fault → the upstream status if it's an error, else 502.
328
+ */
329
+ export function staffAccessHttpStatus(reason: StaffAccessFailureReason, upstreamStatus: number): number {
330
+ switch (reason) {
331
+ case "notStaff":
332
+ case "notAuthenticated":
333
+ return 403;
334
+ case "notFound":
335
+ return 404;
336
+ case "invalid":
337
+ return 400;
338
+ default:
339
+ return upstreamStatus >= 400 ? upstreamStatus : 502;
340
+ }
341
+ }
342
+
343
+ function defaultMessage(reason: StaffAccessFailureReason, status: number): string {
344
+ switch (reason) {
345
+ case "notStaff":
346
+ return "this account doesn't hold ToT-staff access to that vendor";
347
+ case "notAuthenticated":
348
+ return "storefront's operator credential was rejected";
349
+ case "notFound":
350
+ return "that vendor is not a resolvable Token of Trust app";
351
+ case "invalid":
352
+ return "a valid vendor is required";
353
+ default:
354
+ return `staffVendorAccess: tot20 returned status ${status}`;
355
+ }
356
+ }
357
+
358
+ // --- Mock (tests + local dev) -----------------------------------------------
359
+
360
+ /**
361
+ * In-memory StaffVendorAccessClient. Seeded with a directory of vendors and the
362
+ * set of staff emails, it drives route + client tests deterministically and lets
363
+ * local dev exercise the search+enter flow before/without the live tot20 endpoint.
364
+ */
365
+ export class MockStaffVendorAccessClient implements StaffVendorAccessClient {
366
+ private readonly vendors: VendorHit[];
367
+ private readonly staff: Set<string>;
368
+ private readonly capability: string;
369
+ /** Recorded admissions, for assertions (the audit row tot20 would write). */
370
+ granted: Array<{ email: string; vendorAppDomain: string }> = [];
371
+
372
+ constructor(opts: { vendors?: VendorHit[]; staffEmails?: string[]; capability?: string } = {}) {
373
+ this.vendors = opts.vendors ?? [];
374
+ this.staff = new Set((opts.staffEmails ?? []).map((e) => e.toLowerCase()));
375
+ this.capability = opts.capability ?? "staff";
376
+ }
377
+
378
+ /**
379
+ * Derive the staff actor's email from the broker assertion's `sub` — simulating
380
+ * tot20 deriving it from the VERIFIED assertion. The Mock only DECODES (never
381
+ * verifies the signature); the real tot20 verifies. Returns "" for an
382
+ * absent/malformed assertion so a proofless call fails the staff check.
383
+ */
384
+ private actorEmail(brokerAssertion: string): string {
385
+ if (!brokerAssertion || !brokerAssertion.trim()) return "";
386
+ try {
387
+ const sub = decodeJwt(brokerAssertion).sub;
388
+ return typeof sub === "string" ? sub.toLowerCase() : "";
389
+ } catch {
390
+ return "";
391
+ }
392
+ }
393
+
394
+ async searchVendors(_brokerAssertion: string, query: string, limit?: number): Promise<VendorSearchResult> {
395
+ const q = (query ?? "").trim().toLowerCase();
396
+ if (!q) return { ok: true, vendors: [] };
397
+ const cap = clampLimit(limit, DEFAULT_LIMIT);
398
+ const vendors = this.vendors
399
+ .filter(
400
+ (v) =>
401
+ v.appDomain.toLowerCase().startsWith(q) || v.displayName.toLowerCase().includes(q),
402
+ )
403
+ .slice(0, cap);
404
+ return { ok: true, vendors };
405
+ }
406
+
407
+ async grantStaffAccess(brokerAssertion: string, vendorAppDomain: string): Promise<StaffVendorAccessResult> {
408
+ const vendor = (vendorAppDomain ?? "").trim();
409
+ if (!vendor) return { ok: false, status: 400, reason: "invalid", message: "a vendor appDomain is required" };
410
+ const email = this.actorEmail(brokerAssertion);
411
+ if (!email || !this.staff.has(email)) {
412
+ return {
413
+ ok: false,
414
+ status: 403,
415
+ reason: "notStaff",
416
+ message: "this account doesn't hold ToT-staff access to that vendor",
417
+ code: "staffVendorAccess:notStaff",
418
+ };
419
+ }
420
+ if (!this.vendors.some((v) => v.appDomain === vendor)) {
421
+ return {
422
+ ok: false,
423
+ status: 404,
424
+ reason: "notFound",
425
+ message: "that vendor is not a resolvable Token of Trust app",
426
+ code: "staffVendorAccess:vendorNotFound",
427
+ };
428
+ }
429
+ this.granted.push({ email, vendorAppDomain: vendor });
430
+ return { ok: true, capability: this.capability };
431
+ }
432
+ }
433
+
434
+ // --- Factory ----------------------------------------------------------------
435
+
436
+ /**
437
+ * Build the env-configured client:
438
+ * - `TOT_CORE_URL` — tot20 core API base (tier-matching per deploy).
439
+ * - `TOT_API_KEY` (+ optional `TOT_SECRET_KEY`) — storefront's own operator creds.
440
+ * Throws {@link StaffVendorAccessClientError} when unconfigured (routes map that to 503).
441
+ */
442
+ export async function createStaffVendorAccessClient(
443
+ overrides: Partial<HttpStaffVendorAccessClientConfig> = {},
444
+ ): Promise<HttpStaffVendorAccessClient> {
445
+ const baseUrl = overrides.baseUrl ?? (await readEnv("TOT_CORE_URL")) ?? "";
446
+ const apiKey = overrides.apiKey ?? (await readEnv("TOT_API_KEY")) ?? "";
447
+ const secretKey = overrides.secretKey ?? (await readEnv("TOT_SECRET_KEY"));
448
+ return new HttpStaffVendorAccessClient({ ...overrides, baseUrl, apiKey, secretKey });
449
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Vendor (tenant/appDomain) selection — the L3 consumer pattern from
3
+ * docs/architecture/identity-and-authorization.md: an app renders tenant
4
+ * selection FROM the verified id_token's tenant claims. Single membership → go
5
+ * straight into that vendor; multiple → show a picker; none → nothing to manage.
6
+ *
7
+ * PURE (no I/O, no Astro) so the single-vs-picker routing and the "does this
8
+ * viewer have access to vendor X" authorization are unit-testable in isolation.
9
+ * The session's `tenants: {resource, role}[]` (minted from the offline-verified
10
+ * assertion — see `@/lib/auth/identityToken` `sessionFromClaims`) is the sole
11
+ * input; the storefront holds no membership state of its own (L3 constraint #4).
12
+ *
13
+ * STAGE-0 NOTE (tot20 dependency): today tot20's access-code id_token carries only
14
+ * the ONE resource the code was verified against (identityAssertion.buildClaims),
15
+ * so `tenants[]` has a single entry and this resolves to `single` in practice. The
16
+ * `picker` branch is built to the locked contract and activates unchanged once
17
+ * tot20 populates all of the email's memberships. Flagged to the workstream lead.
18
+ */
19
+
20
+ import type { StaffSelection } from "@/lib/auth/session";
21
+
22
+ /** One tenant membership carried in the verified session. */
23
+ export interface TenantMembership {
24
+ resource: string;
25
+ role: string;
26
+ }
27
+
28
+ /**
29
+ * The ToT-staff admission path — DISTINCT from `tenants[]`. A staff viewer reaches
30
+ * a vendor by explicit search-and-enter (never by membership), so access is
31
+ * decided from their active {@link StaffSelection} + the current time, NOT from a
32
+ * synthesized membership. `viewerCanAccess`/`membershipFor` take this as an
33
+ * optional extra source; when omitted they behave exactly as before (membership
34
+ * only), so non-staff callers are unchanged.
35
+ */
36
+ export interface StaffAdmission {
37
+ /** The staff viewer's active selection (from the session), if any. */
38
+ selection: StaffSelection | undefined;
39
+ /** Current time (epoch seconds) for the selection's expiry check. */
40
+ nowSeconds: number;
41
+ }
42
+
43
+ /**
44
+ * True when a staff admission is LIVE for `resource`: a selection exists, targets
45
+ * this exact vendor, and has not expired. Deny-closed on every other case. This
46
+ * is the sole staff-side authorization primitive — a distinct branch from the
47
+ * membership check, and it never consults `tenants[]`.
48
+ */
49
+ export function staffAdmits(
50
+ admission: StaffAdmission | undefined,
51
+ resource: string,
52
+ ): boolean {
53
+ if (!admission?.selection) return false;
54
+ const target = (resource ?? "").trim();
55
+ if (!target) return false;
56
+ return (
57
+ admission.selection.resource === target &&
58
+ admission.selection.exp > admission.nowSeconds
59
+ );
60
+ }
61
+
62
+ /** Where a signed-in viewer should land based on their memberships. */
63
+ export type TenantSelection =
64
+ | { mode: "none" }
65
+ | { mode: "single"; tenant: TenantMembership }
66
+ | { mode: "picker"; tenants: TenantMembership[] };
67
+
68
+ /** De-dupe memberships by resource (keep first), dropping malformed entries. */
69
+ function normalizeMemberships(
70
+ tenants: readonly TenantMembership[] | undefined,
71
+ ): TenantMembership[] {
72
+ const seen = new Set<string>();
73
+ const out: TenantMembership[] = [];
74
+ for (const t of tenants ?? []) {
75
+ const resource = typeof t?.resource === "string" ? t.resource.trim() : "";
76
+ if (!resource || seen.has(resource)) continue;
77
+ seen.add(resource);
78
+ out.push({ resource, role: typeof t.role === "string" ? t.role : "member" });
79
+ }
80
+ return out;
81
+ }
82
+
83
+ /**
84
+ * Decide the vendor-selection outcome for a viewer's memberships:
85
+ * 0 → `none`, 1 → `single` (navigate straight in), ≥2 → `picker`.
86
+ */
87
+ export function selectTenant(
88
+ tenants: readonly TenantMembership[] | undefined,
89
+ ): TenantSelection {
90
+ const members = normalizeMemberships(tenants);
91
+ const [first, ...rest] = members;
92
+ if (!first) return { mode: "none" };
93
+ if (rest.length === 0) return { mode: "single", tenant: first };
94
+ return { mode: "picker", tenants: members };
95
+ }
96
+
97
+ /**
98
+ * Whether the viewer may act on `resource` — i.e. holds a membership for that
99
+ * vendor. The near-side authorization for every dashboard action; the far side
100
+ * (tot20 `hasOperatorGrant(storefront, tenant)`) is enforced independently.
101
+ * Deny-by-default: no membership, no access.
102
+ */
103
+ export function viewerCanAccess(
104
+ tenants: readonly TenantMembership[] | undefined,
105
+ resource: string,
106
+ staff?: StaffAdmission,
107
+ ): boolean {
108
+ const target = (resource ?? "").trim();
109
+ if (!target) return false;
110
+ if (normalizeMemberships(tenants).some((t) => t.resource === target)) return true;
111
+ // DISTINCT staff path: a live staff selection for THIS vendor admits, without
112
+ // any `tenants[]` membership. Omitted `staff` → membership-only (unchanged).
113
+ return staffAdmits(staff, target);
114
+ }
115
+
116
+ /**
117
+ * The viewer's membership for `resource`, or null. When a `staff` admission is
118
+ * supplied and no real membership exists, a LIVE staff selection for this vendor
119
+ * yields a staff-derived stand-in `{ resource, role: capability }` so the guard
120
+ * has a role/capability to render — this is NOT a `tenants[]` entry and is never
121
+ * persisted as one (the distinct-path invariant).
122
+ */
123
+ export function membershipFor(
124
+ tenants: readonly TenantMembership[] | undefined,
125
+ resource: string,
126
+ staff?: StaffAdmission,
127
+ ): TenantMembership | null {
128
+ const target = (resource ?? "").trim();
129
+ if (!target) return null;
130
+ const member = normalizeMemberships(tenants).find((t) => t.resource === target);
131
+ if (member) return member;
132
+ if (staffAdmits(staff, target)) {
133
+ return { resource: target, role: staff!.selection!.capability };
134
+ }
135
+ return null;
136
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Anchor syntax for the ToT-STAFF vendor search (#8425). tot20's vendorSearch does a
3
+ * plain SUBSTRING match over appDomain OR displayName, so a common term like
4
+ * `tokenoftrust.com` surfaces every subdomain/displayName that contains it and buries
5
+ * (or caps out) the one exact hit. These anchors let a staffer narrow it, mirroring
6
+ * regex/grep intuition — but they are LITERAL anchors, not a regex engine:
7
+ *
8
+ * ^acme.com → appDomain STARTS WITH "acme.com"
9
+ * acme.com$ → appDomain ENDS WITH "acme.com"
10
+ * ^acme.com$ → appDomain EXACTLY "acme.com"
11
+ * acme → unchanged substring search (appDomain OR displayName, tot20-side)
12
+ *
13
+ * Anchors match the **appDomain** only (the domain the operator is targeting), case-
14
+ * insensitively. Only a leading `^` and a trailing `$` are treated as anchors; the
15
+ * characters elsewhere are literal. The core term (anchors stripped) is what we send
16
+ * upstream, so tot20 still returns a superset we filter locally.
17
+ */
18
+
19
+ /** Upstream fetch cap for an anchored query — widened so the exact hit is in the set we filter. */
20
+ export const ANCHORED_FETCH_LIMIT = 50;
21
+
22
+ /** Max rows returned to the client after an anchored post-filter (the visible autocomplete list). */
23
+ export const ANCHORED_DISPLAY_LIMIT = 10;
24
+
25
+ export interface ParsedVendorQuery {
26
+ /** The search term with any anchors stripped — what we send upstream to tot20. */
27
+ core: string;
28
+ /** True when `^`/`$` anchors are present AND there is a non-empty core to match. */
29
+ anchored: boolean;
30
+ /** Predicate applied to a vendor `appDomain` when `anchored` (always true otherwise). */
31
+ matches: (appDomain: string) => boolean;
32
+ }
33
+
34
+ /**
35
+ * Parse a raw search string into its core term + an anchor predicate. Pure + total:
36
+ * a bare `^`, `$`, or `^$` (no core) yields `anchored:false` and an empty core, so the
37
+ * caller sends an empty query upstream (→ no results) rather than matching everything.
38
+ */
39
+ export function parseVendorQuery(raw: string): ParsedVendorQuery {
40
+ const q = (raw ?? "").trim();
41
+ const start = q.startsWith("^");
42
+ const end = q.endsWith("$");
43
+ let core = q;
44
+ if (start) core = core.slice(1);
45
+ if (end) core = core.slice(0, core.length - 1);
46
+ core = core.trim();
47
+ const needle = core.toLowerCase();
48
+ const anchored = (start || end) && core.length > 0;
49
+
50
+ let matches: (appDomain: string) => boolean;
51
+ if (start && end) {
52
+ matches = (d) => (d ?? "").toLowerCase() === needle;
53
+ } else if (start) {
54
+ matches = (d) => (d ?? "").toLowerCase().startsWith(needle);
55
+ } else if (end) {
56
+ matches = (d) => (d ?? "").toLowerCase().endsWith(needle);
57
+ } else {
58
+ matches = () => true;
59
+ }
60
+ return { core, anchored, matches };
61
+ }