@tokenoftrust/storefront-runner 1.3.2

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 (310) hide show
  1. package/LICENSE +58 -0
  2. package/README.md +42 -0
  3. package/apps/storefront/astro.config.mjs +66 -0
  4. package/apps/storefront/dev-plugins/dev-loop-state.mjs +147 -0
  5. package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +238 -0
  6. package/apps/storefront/env.d.ts +162 -0
  7. package/apps/storefront/migrations/0000_baseline.sql +54 -0
  8. package/apps/storefront/migrations/0001_products_fts.sql +9 -0
  9. package/apps/storefront/migrations/README.md +54 -0
  10. package/apps/storefront/migrations/meta/0000_snapshot.json +347 -0
  11. package/apps/storefront/migrations/meta/0001_snapshot.json +347 -0
  12. package/apps/storefront/migrations/meta/_journal.json +20 -0
  13. package/apps/storefront/package.json +42 -0
  14. package/apps/storefront/public/brand/northwind-logo-dark.svg +4 -0
  15. package/apps/storefront/public/brand/northwind-logo-light.svg +4 -0
  16. package/apps/storefront/public/brand/northwind-og.svg +13 -0
  17. package/apps/storefront/public/favicon.svg +5 -0
  18. package/apps/storefront/public/js/dashboard-team.js +147 -0
  19. package/apps/storefront/public/js/dashboard-vendor-search.js +115 -0
  20. package/apps/storefront/public/shared/commerce-chrome.css +369 -0
  21. package/apps/storefront/src/components/Badge.astro +34 -0
  22. package/apps/storefront/src/components/Breadcrumbs.astro +45 -0
  23. package/apps/storefront/src/components/CollectionCard.astro +56 -0
  24. package/apps/storefront/src/components/EmptyState.astro +55 -0
  25. package/apps/storefront/src/components/Img.astro +68 -0
  26. package/apps/storefront/src/components/ProductCard.astro +228 -0
  27. package/apps/storefront/src/components/Section.astro +39 -0
  28. package/apps/storefront/src/components/Seo.astro +70 -0
  29. package/apps/storefront/src/components/WidgetFrame.astro +34 -0
  30. package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +531 -0
  31. package/apps/storefront/src/components/admin/AdminCustomersTab.astro +597 -0
  32. package/apps/storefront/src/components/admin/AdminFoxyCartSettingsPanel.astro +227 -0
  33. package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +465 -0
  34. package/apps/storefront/src/components/admin/AdminProductsTab.astro +901 -0
  35. package/apps/storefront/src/components/admin/AdminReportingTab.astro +435 -0
  36. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +782 -0
  37. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +1223 -0
  38. package/apps/storefront/src/components/auth/AuthBadge.astro +44 -0
  39. package/apps/storefront/src/components/chrome/AnnouncementBar.astro +108 -0
  40. package/apps/storefront/src/components/chrome/MegaMenu.astro +26 -0
  41. package/apps/storefront/src/components/chrome/NavDropdown.astro +201 -0
  42. package/apps/storefront/src/components/chrome/SiteFooter.astro +201 -0
  43. package/apps/storefront/src/components/chrome/SiteHeader.astro +234 -0
  44. package/apps/storefront/src/components/chrome/UtilityNav.astro +79 -0
  45. package/apps/storefront/src/components/commerce/ArticleCard.astro +242 -0
  46. package/apps/storefront/src/components/commerce/BrandGrid.astro +175 -0
  47. package/apps/storefront/src/components/commerce/FoxyLoader.astro +23 -0
  48. package/apps/storefront/src/components/commerce/FoxySidecartStyles.astro +61 -0
  49. package/apps/storefront/src/components/commerce/PriceDisplay.astro +91 -0
  50. package/apps/storefront/src/components/commerce/ProductCarousel.astro +197 -0
  51. package/apps/storefront/src/components/commerce/ProductGrid.astro +75 -0
  52. package/apps/storefront/src/components/commerce/RatingStars.astro +58 -0
  53. package/apps/storefront/src/components/commerce/ReviewList.astro +54 -0
  54. package/apps/storefront/src/components/commerce/ReviewSummary.astro +44 -0
  55. package/apps/storefront/src/components/commerce/TagFilterCloud.astro +149 -0
  56. package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +37 -0
  57. package/apps/storefront/src/components/compliance/ComplianceNotice.astro +99 -0
  58. package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +45 -0
  59. package/apps/storefront/src/components/compliance/NicotineWarning.astro +33 -0
  60. package/apps/storefront/src/components/compliance/PactActNotice.astro +35 -0
  61. package/apps/storefront/src/components/compliance/Prop65Warning.astro +80 -0
  62. package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +38 -0
  63. package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +35 -0
  64. package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +53 -0
  65. package/apps/storefront/src/components/home/Hero.astro +164 -0
  66. package/apps/storefront/src/components/islands/AgeGate.tsx +109 -0
  67. package/apps/storefront/src/components/islands/ImageGallery.tsx +237 -0
  68. package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +73 -0
  69. package/apps/storefront/src/components/islands/MobileNav.tsx +239 -0
  70. package/apps/storefront/src/components/islands/QuickView.tsx +138 -0
  71. package/apps/storefront/src/components/islands/RecentlyViewed.tsx +122 -0
  72. package/apps/storefront/src/components/islands/SavedList.tsx +99 -0
  73. package/apps/storefront/src/components/islands/SearchTypeahead.tsx +248 -0
  74. package/apps/storefront/src/components/islands/VariantSelector.tsx +375 -0
  75. package/apps/storefront/src/components/marketing/CardGrid.astro +72 -0
  76. package/apps/storefront/src/components/marketing/CodeSample.astro +20 -0
  77. package/apps/storefront/src/components/marketing/CtaBand.astro +24 -0
  78. package/apps/storefront/src/components/marketing/Faq.astro +37 -0
  79. package/apps/storefront/src/components/marketing/Integrations.astro +63 -0
  80. package/apps/storefront/src/components/marketing/MarketingBlocks.astro +53 -0
  81. package/apps/storefront/src/components/marketing/MarketingCtas.astro +48 -0
  82. package/apps/storefront/src/components/marketing/MarketingHero.astro +72 -0
  83. package/apps/storefront/src/components/marketing/ProofStrip.astro +37 -0
  84. package/apps/storefront/src/components/marketing/SplitCompare.astro +53 -0
  85. package/apps/storefront/src/components/marketing/Steps.astro +36 -0
  86. package/apps/storefront/src/components/marketing/Testimonials.astro +38 -0
  87. package/apps/storefront/src/components/marketing/TrustBar.astro +22 -0
  88. package/apps/storefront/src/components/membership/IllustrativeLabel.astro +56 -0
  89. package/apps/storefront/src/components/membership/TierCards.astro +164 -0
  90. package/apps/storefront/src/components/membership/TierComparisonTable.astro +148 -0
  91. package/apps/storefront/src/components/plp/ActiveFilters.astro +79 -0
  92. package/apps/storefront/src/components/plp/FacetSidebar.astro +199 -0
  93. package/apps/storefront/src/components/plp/Pagination.astro +99 -0
  94. package/apps/storefront/src/components/plp/SortSelect.astro +64 -0
  95. package/apps/storefront/src/components/style-guide/StyleGuideNav.astro +95 -0
  96. package/apps/storefront/src/components/subscription/HowItWorksSteps.astro +67 -0
  97. package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +67 -0
  98. package/apps/storefront/src/components/subscription/SavingsExplainer.astro +101 -0
  99. package/apps/storefront/src/config/adminTargetTenant.ts +28 -0
  100. package/apps/storefront/src/config/devTenantSeed.ts +78 -0
  101. package/apps/storefront/src/config/devTenants.ts +16 -0
  102. package/apps/storefront/src/config/kvTenantResolver.ts +87 -0
  103. package/apps/storefront/src/config/resolver.ts +102 -0
  104. package/apps/storefront/src/config/tenants.ts +87 -0
  105. package/apps/storefront/src/layouts/Layout.astro +368 -0
  106. package/apps/storefront/src/lib/admin/README.md +122 -0
  107. package/apps/storefront/src/lib/admin/ai/approvalMode.ts +21 -0
  108. package/apps/storefront/src/lib/admin/ai/askEnvelope.ts +48 -0
  109. package/apps/storefront/src/lib/admin/ai/audit.ts +14 -0
  110. package/apps/storefront/src/lib/admin/ai/index.ts +6 -0
  111. package/apps/storefront/src/lib/admin/ai/operationalRuns.ts +155 -0
  112. package/apps/storefront/src/lib/admin/ai/recommendation.ts +20 -0
  113. package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +172 -0
  114. package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +65 -0
  115. package/apps/storefront/src/lib/admin/envelope.ts +10 -0
  116. package/apps/storefront/src/lib/admin/evidence/evidenceReference.ts +32 -0
  117. package/apps/storefront/src/lib/admin/memoizeByEnvelope.ts +27 -0
  118. package/apps/storefront/src/lib/admin/orderClient.ts +146 -0
  119. package/apps/storefront/src/lib/admin/seed.ts +54 -0
  120. package/apps/storefront/src/lib/admin/services/mock/aiWorkflows.ts +59 -0
  121. package/apps/storefront/src/lib/admin/services/mock/customers.ts +86 -0
  122. package/apps/storefront/src/lib/admin/services/mock/evidence.ts +99 -0
  123. package/apps/storefront/src/lib/admin/services/mock/foxyCartSettings.ts +78 -0
  124. package/apps/storefront/src/lib/admin/services/mock/fulfillment.ts +62 -0
  125. package/apps/storefront/src/lib/admin/services/mock/index.ts +12 -0
  126. package/apps/storefront/src/lib/admin/services/mock/orders.ts +184 -0
  127. package/apps/storefront/src/lib/admin/services/mock/products.ts +105 -0
  128. package/apps/storefront/src/lib/admin/services/mock/reporting.ts +51 -0
  129. package/apps/storefront/src/lib/admin/services/mock/settings.ts +70 -0
  130. package/apps/storefront/src/lib/admin/services/mock/tenants.ts +29 -0
  131. package/apps/storefront/src/lib/admin/services/orders.ts +300 -0
  132. package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +67 -0
  133. package/apps/storefront/src/lib/admin/viewmodels/customers.ts +56 -0
  134. package/apps/storefront/src/lib/admin/viewmodels/fulfillment.ts +49 -0
  135. package/apps/storefront/src/lib/admin/viewmodels/orders.ts +92 -0
  136. package/apps/storefront/src/lib/admin/viewmodels/products.ts +74 -0
  137. package/apps/storefront/src/lib/admin/viewmodels/reporting.ts +34 -0
  138. package/apps/storefront/src/lib/admin/viewmodels/settings.ts +64 -0
  139. package/apps/storefront/src/lib/adminTenantEnvelope.ts +90 -0
  140. package/apps/storefront/src/lib/analytics/index.ts +47 -0
  141. package/apps/storefront/src/lib/auth/brokerAssertion.ts +136 -0
  142. package/apps/storefront/src/lib/auth/devAuth.ts +90 -0
  143. package/apps/storefront/src/lib/auth/gatePage.ts +81 -0
  144. package/apps/storefront/src/lib/auth/identityToken.ts +341 -0
  145. package/apps/storefront/src/lib/auth/loginGate.ts +119 -0
  146. package/apps/storefront/src/lib/auth/route.ts +146 -0
  147. package/apps/storefront/src/lib/auth/session.ts +203 -0
  148. package/apps/storefront/src/lib/auth/totAccessClient.ts +503 -0
  149. package/apps/storefront/src/lib/basePath.ts +41 -0
  150. package/apps/storefront/src/lib/cfImage.ts +55 -0
  151. package/apps/storefront/src/lib/chrome/model.ts +244 -0
  152. package/apps/storefront/src/lib/chrome/parseConfig.ts +34 -0
  153. package/apps/storefront/src/lib/colors.ts +97 -0
  154. package/apps/storefront/src/lib/commerce/foxyCartSettings.ts +253 -0
  155. package/apps/storefront/src/lib/content-edit/applyDiff.ts +91 -0
  156. package/apps/storefront/src/lib/content-edit/client.ts +230 -0
  157. package/apps/storefront/src/lib/content-edit/index.ts +14 -0
  158. package/apps/storefront/src/lib/content-edit/route.ts +25 -0
  159. package/apps/storefront/src/lib/content-edit/types.ts +19 -0
  160. package/apps/storefront/src/lib/cspStaticHashes.ts +24 -0
  161. package/apps/storefront/src/lib/d1/catalog.ts +366 -0
  162. package/apps/storefront/src/lib/dashboard/staffAdmission.ts +43 -0
  163. package/apps/storefront/src/lib/dashboard/staffVendorAccess.ts +449 -0
  164. package/apps/storefront/src/lib/dashboard/tenantSelection.ts +136 -0
  165. package/apps/storefront/src/lib/dashboard/vendorQuery.ts +61 -0
  166. package/apps/storefront/src/lib/demoGate.ts +113 -0
  167. package/apps/storefront/src/lib/dev/activityIngestToken.ts +78 -0
  168. package/apps/storefront/src/lib/dev/activityStore.ts +72 -0
  169. package/apps/storefront/src/lib/dev/ai-widget-client.js +308 -0
  170. package/apps/storefront/src/lib/dev/apiBase.ts +14 -0
  171. package/apps/storefront/src/lib/dev/cliSignInCode.ts +155 -0
  172. package/apps/storefront/src/lib/dev/devDraft.ts +133 -0
  173. package/apps/storefront/src/lib/dev/hostedCockpit.ts +169 -0
  174. package/apps/storefront/src/lib/dev/livePreview.ts +77 -0
  175. package/apps/storefront/src/lib/dev/runtimeStore.ts +120 -0
  176. package/apps/storefront/src/lib/dev/storeStatus.ts +142 -0
  177. package/apps/storefront/src/lib/diag/redact.ts +20 -0
  178. package/apps/storefront/src/lib/edgeCache.ts +128 -0
  179. package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +62 -0
  180. package/apps/storefront/src/lib/env.ts +60 -0
  181. package/apps/storefront/src/lib/fixtures/sampleProducts.ts +130 -0
  182. package/apps/storefront/src/lib/format.ts +38 -0
  183. package/apps/storefront/src/lib/foxyCommerce.ts +49 -0
  184. package/apps/storefront/src/lib/imageAttrs.ts +55 -0
  185. package/apps/storefront/src/lib/jsonld.ts +166 -0
  186. package/apps/storefront/src/lib/maintenance.ts +71 -0
  187. package/apps/storefront/src/lib/membership/model.ts +97 -0
  188. package/apps/storefront/src/lib/memoryKv.ts +32 -0
  189. package/apps/storefront/src/lib/messaging/messagesClient.ts +143 -0
  190. package/apps/storefront/src/lib/placeholder.ts +99 -0
  191. package/apps/storefront/src/lib/previewReload.ts +125 -0
  192. package/apps/storefront/src/lib/priceBuckets.ts +33 -0
  193. package/apps/storefront/src/lib/rawChrome.ts +505 -0
  194. package/apps/storefront/src/lib/rawMarketingHtml.ts +136 -0
  195. package/apps/storefront/src/lib/search/index.ts +74 -0
  196. package/apps/storefront/src/lib/sections.ts +129 -0
  197. package/apps/storefront/src/lib/securityHeaders.ts +31 -0
  198. package/apps/storefront/src/lib/storyblok/content-model.ts +289 -0
  199. package/apps/storefront/src/lib/storyblok/provider.ts +607 -0
  200. package/apps/storefront/src/lib/subscription/model.ts +68 -0
  201. package/apps/storefront/src/lib/tenantRouting.ts +91 -0
  202. package/apps/storefront/src/lib/tot/ToTClient.ts +177 -0
  203. package/apps/storefront/src/lib/tot/d1Client.ts +62 -0
  204. package/apps/storefront/src/lib/tot/query.ts +249 -0
  205. package/apps/storefront/src/lib/tot/totClientInterface.ts +30 -0
  206. package/apps/storefront/src/lib/url.ts +142 -0
  207. package/apps/storefront/src/lib/wishlist.ts +63 -0
  208. package/apps/storefront/src/middleware/index.ts +455 -0
  209. package/apps/storefront/src/pages/404.astro +61 -0
  210. package/apps/storefront/src/pages/[...slug].astro +90 -0
  211. package/apps/storefront/src/pages/admin.astro +1675 -0
  212. package/apps/storefront/src/pages/api/admin/orders/[id].ts +53 -0
  213. package/apps/storefront/src/pages/api/admin/orders.ts +71 -0
  214. package/apps/storefront/src/pages/api/auth/logout.ts +21 -0
  215. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +107 -0
  216. package/apps/storefront/src/pages/api/auth/send.ts +85 -0
  217. package/apps/storefront/src/pages/api/auth/verify.ts +135 -0
  218. package/apps/storefront/src/pages/api/cache-purge.ts +44 -0
  219. package/apps/storefront/src/pages/api/checkout-style.css.ts +43 -0
  220. package/apps/storefront/src/pages/api/cli/policy.ts +70 -0
  221. package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +164 -0
  222. package/apps/storefront/src/pages/api/csp-report.ts +25 -0
  223. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +124 -0
  224. package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +120 -0
  225. package/apps/storefront/src/pages/api/dev/activity.ts +124 -0
  226. package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +73 -0
  227. package/apps/storefront/src/pages/api/dev/diff.ts +68 -0
  228. package/apps/storefront/src/pages/api/dev/file.ts +57 -0
  229. package/apps/storefront/src/pages/api/dev/locate.ts +125 -0
  230. package/apps/storefront/src/pages/api/dev/publish.ts +128 -0
  231. package/apps/storefront/src/pages/api/dev/tree.ts +70 -0
  232. package/apps/storefront/src/pages/api/dev/write.ts +116 -0
  233. package/apps/storefront/src/pages/api/newsletter.ts +104 -0
  234. package/apps/storefront/src/pages/api/request-access.ts +172 -0
  235. package/apps/storefront/src/pages/auth/login.astro +149 -0
  236. package/apps/storefront/src/pages/auth/magic.astro +105 -0
  237. package/apps/storefront/src/pages/capabilities.astro +292 -0
  238. package/apps/storefront/src/pages/collections/[handle].astro +177 -0
  239. package/apps/storefront/src/pages/collections/index.astro +59 -0
  240. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +100 -0
  241. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +154 -0
  242. package/apps/storefront/src/pages/dashboard/index.astro +160 -0
  243. package/apps/storefront/src/pages/index.astro +290 -0
  244. package/apps/storefront/src/pages/llms.txt.ts +92 -0
  245. package/apps/storefront/src/pages/products/[handle].astro +402 -0
  246. package/apps/storefront/src/pages/robots.txt.ts +24 -0
  247. package/apps/storefront/src/pages/saved.astro +35 -0
  248. package/apps/storefront/src/pages/search.astro +150 -0
  249. package/apps/storefront/src/pages/sitemap.xml.ts +78 -0
  250. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +1280 -0
  251. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +229 -0
  252. package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +144 -0
  253. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +116 -0
  254. package/apps/storefront/src/pages/style-guide/index.astro +98 -0
  255. package/apps/storefront/src/pages/tenants/[id]/[...path].ts +57 -0
  256. package/apps/storefront/src/styles/global.css +292 -0
  257. package/apps/storefront/src/themes/reference.ts +68 -0
  258. package/apps/storefront/src/themes/schema.ts +111 -0
  259. package/apps/storefront/src/themes/tenants/index.ts +51 -0
  260. package/apps/storefront/tsconfig.json +16 -0
  261. package/apps/storefront/vitest.config.ts +22 -0
  262. package/package.json +26 -0
  263. package/packages/public-runtime/LICENSE +58 -0
  264. package/packages/public-runtime/package.json +22 -0
  265. package/packages/public-runtime/src/binary-path.ts +68 -0
  266. package/packages/public-runtime/src/capabilities.ts +89 -0
  267. package/packages/public-runtime/src/catalog-api.ts +81 -0
  268. package/packages/public-runtime/src/catalog-d1.ts +192 -0
  269. package/packages/public-runtime/src/checkout-style-publish.ts +200 -0
  270. package/packages/public-runtime/src/checkout-style.ts +375 -0
  271. package/packages/public-runtime/src/csp.ts +109 -0
  272. package/packages/public-runtime/src/customization-preview.ts +197 -0
  273. package/packages/public-runtime/src/customization-reconcile.ts +63 -0
  274. package/packages/public-runtime/src/customization-runtime.ts +157 -0
  275. package/packages/public-runtime/src/customization-scripts.ts +64 -0
  276. package/packages/public-runtime/src/customization-versioning.ts +95 -0
  277. package/packages/public-runtime/src/foxy-orders.ts +136 -0
  278. package/packages/public-runtime/src/foxy.ts +196 -0
  279. package/packages/public-runtime/src/hot-standby-readiness.ts +140 -0
  280. package/packages/public-runtime/src/index.ts +34 -0
  281. package/packages/public-runtime/src/product.ts +159 -0
  282. package/packages/public-runtime/src/search.ts +45 -0
  283. package/packages/public-runtime/src/tenant-assets.ts +444 -0
  284. package/packages/public-runtime/src/tenant.ts +384 -0
  285. package/packages/public-runtime/tsconfig.json +8 -0
  286. package/packages/public-runtime/vitest.config.ts +8 -0
  287. package/pnpm-workspace.yaml +7 -0
  288. package/scripts/build/copy-tenant-assets.mjs +69 -0
  289. package/scripts/dev/ai-edit.mjs +183 -0
  290. package/scripts/dev/checkout-watch.mjs +116 -0
  291. package/scripts/dev/file-browser.mjs +159 -0
  292. package/scripts/dev/file-writer.mjs +141 -0
  293. package/scripts/dev/git-status.mjs +171 -0
  294. package/scripts/dev/locate-handler.mjs +163 -0
  295. package/scripts/dev/locate.mjs +112 -0
  296. package/scripts/dev/port-check.mjs +48 -0
  297. package/scripts/dev/safe-path.mjs +109 -0
  298. package/scripts/dev/shot.mjs +46 -0
  299. package/scripts/dev/tenant-source-root.mjs +76 -0
  300. package/scripts/dev/unified-diff.mjs +81 -0
  301. package/scripts/tot-dev.mjs +376 -0
  302. package/tenants/README.md +14 -0
  303. package/tenants/home/content/chrome.json +7 -0
  304. package/tenants/home/content/home.html +58 -0
  305. package/tenants/home/content/home.json +11 -0
  306. package/tenants/home/public/js/storefront.js +210 -0
  307. package/tenants/home/public/logo-wordmark.png +0 -0
  308. package/tenants/home/public/pages/storefront.css +212 -0
  309. package/tenants/home/theme.json +6 -0
  310. package/tsconfig.base.json +20 -0
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Offline verification of tot's identity assertion (`id_token`) — the consuming
3
+ * half of the decoupled-authZ architecture (Phase 3).
4
+ *
5
+ * tot signs a short-lived ES256 assertion ONCE at sign-in with its PRIVATE key
6
+ * (`/api/internal/access-code/verify` → `id_token`). This module verifies it
7
+ * OFFLINE using tot's published JWKS (fetched once, cached, key-selected by
8
+ * `kid`) and derives the storefront session from the VERIFIED claims — no shared
9
+ * secret, no per-request callback to tot. Removing tot's network reachability
10
+ * after sign-in must not break authorization; the token's own `exp` bounds
11
+ * staleness.
12
+ *
13
+ * This mirrors the Phase-2 launchpad kernel's lightweight offline verifier
14
+ * (`launchpad src/modules/authz/jwks-verifier.ts` + `identity-claims.ts`) and the
15
+ * tot-internal verify path proven in
16
+ * `tot20 services/InternalAuthService/identityTokenService.jest.js`.
17
+ *
18
+ * PROTOTYPE SCOPE: this replicates ONLY the JWKS offline-verify + a coarse
19
+ * claims→session derivation. The full Cedar PDP (`can()` policy engine, staff
20
+ * sandbox) and consuming the launchpad kernel as an installed package are
21
+ * Phase 4 — see branch `feat/authz-cedar-kernel`. Do NOT vendor the Cedar-wasm
22
+ * PDP into the Worker; it is unnecessary for the current coarse gate.
23
+ *
24
+ * Contract: /tmp/authz-token-claims-contract.md (AS-BUILT). Key points mirrored
25
+ * here: §2 (ES256 + jose), §3 (claim set), §5 (consumer verify algorithm),
26
+ * §6 (field name `id_token`), §6b (JWKS host is per-env app config; jose
27
+ * cross-version 4↔6 is wire-compatible by construction).
28
+ */
29
+ import {
30
+ jwtVerify,
31
+ createRemoteJWKSet,
32
+ createLocalJWKSet,
33
+ decodeProtectedHeader,
34
+ type JWTVerifyGetKey,
35
+ type JSONWebKeySet,
36
+ } from "jose";
37
+ import { readEnv } from "@/lib/env";
38
+
39
+ /** tot's issuer — consumers pin/allowlist this (`auth.jwt.issuer`). */
40
+ export const TOT_ISSUER = "tokenoftrust-internal-ops";
41
+
42
+ /** The only signing algorithm tot uses. Consumers MUST reject anything else. */
43
+ export const TOT_ALG = "ES256";
44
+
45
+ /**
46
+ * JWKS path (host is per-env app config — §6b). Contract also serves
47
+ * `/api/internal/auth/jwks`; the well-known path is the canonical one.
48
+ */
49
+ export const JWKS_PATH = "/api/.well-known/jwks.json";
50
+
51
+ /**
52
+ * Coarse tenant role, collapsed by tot from its per-tenant capability
53
+ * (`developer`/`inviter`/none → `member`). Org standing only; finer granularity
54
+ * is the storefront's own job to derive locally (Phase-4 PDP work).
55
+ */
56
+ export type CoarseRole = "owner" | "admin" | "member";
57
+
58
+ /** A single tenant membership carried in the assertion. */
59
+ export interface TenantMembership {
60
+ resource: string;
61
+ role: CoarseRole;
62
+ }
63
+
64
+ /**
65
+ * The verified identity assertion payload (tot-authoritative + registered
66
+ * claims). Everything here is an INPUT to the storefront's policy, not a
67
+ * decision. Mirrors the launchpad kernel's `IdentityClaims`.
68
+ */
69
+ export interface IdentityClaims {
70
+ // Registered claims
71
+ iss: string;
72
+ sub: string;
73
+ aud: string;
74
+ iat: number;
75
+ exp: number;
76
+ jti?: string;
77
+ // tot-authoritative claims
78
+ email: string;
79
+ /** Baseline verification signal today (email control proven at sign-in). */
80
+ email_verified: boolean;
81
+ tenants: TenantMembership[];
82
+ /**
83
+ * ToT-staff cross-domain scope (present for ToT staff). CARRIED as a principal
84
+ * attribute; the coarse gate grants NOTHING from it — staff must never elevate
85
+ * app privileges (enforced by the Phase-4 PDP staff sandbox).
86
+ */
87
+ staff: string[];
88
+ }
89
+
90
+ /** Coarse-role collapse guard: anything tot didn't send as owner/admin → member. */
91
+ function coerceRole(role: unknown): CoarseRole {
92
+ return role === "owner" || role === "admin" ? role : "member";
93
+ }
94
+
95
+ /**
96
+ * Normalize a jose JWT payload into typed {@link IdentityClaims}, coercing the
97
+ * loosely-typed pieces and filling contract defaults (`staff: []`). Throws if a
98
+ * required tot-authoritative claim is missing or malformed. Mirrors the kernel's
99
+ * `toIdentityClaims`.
100
+ */
101
+ export function toIdentityClaims(payload: Record<string, unknown>): IdentityClaims {
102
+ const email = payload.email;
103
+ if (typeof email !== "string" || !email) {
104
+ throw new Error("identity assertion missing `email` claim");
105
+ }
106
+ const tenantsRaw = Array.isArray(payload.tenants) ? payload.tenants : [];
107
+ const tenants: TenantMembership[] = tenantsRaw.map((t): TenantMembership => {
108
+ const entry = t as { resource?: unknown; role?: unknown };
109
+ if (typeof entry.resource !== "string") {
110
+ throw new Error("identity assertion `tenants[].resource` must be a string");
111
+ }
112
+ return { resource: entry.resource, role: coerceRole(entry.role) };
113
+ });
114
+ const staff = Array.isArray(payload.staff)
115
+ ? payload.staff.filter((s): s is string => typeof s === "string")
116
+ : [];
117
+
118
+ return {
119
+ iss: String(payload.iss ?? ""),
120
+ sub: String(payload.sub ?? email),
121
+ aud: String(payload.aud ?? ""),
122
+ iat: Number(payload.iat ?? 0),
123
+ exp: Number(payload.exp ?? 0),
124
+ jti: typeof payload.jti === "string" ? payload.jti : undefined,
125
+ email: email.toLowerCase(),
126
+ email_verified: payload.email_verified === true,
127
+ tenants,
128
+ staff,
129
+ };
130
+ }
131
+
132
+ /** Derive tot's JWKS URL from the configured tot API base (§6b: path fixed, host app-config). */
133
+ export function deriveJwksUri(baseUrl: string): string {
134
+ return `${baseUrl.replace(/\/+$/, "")}${JWKS_PATH}`;
135
+ }
136
+
137
+ export interface IdentityVerifierConfig {
138
+ /**
139
+ * This host's tenant appDomain / `resource` — enforced as the `aud` claim (tot
140
+ * sets `aud` per-assertion to the target app). REQUIRED: a token minted for
141
+ * another tenant must be rejected.
142
+ */
143
+ audience: string;
144
+ /** Allowlisted issuer. Defaults to tot's. */
145
+ issuer?: string;
146
+ /**
147
+ * tot's JWKS endpoint, e.g. `https://qa.tokenoftrust.com/api/.well-known/jwks.json`.
148
+ * Provide in production. jose caches the fetch, selects the key by `kid`, and
149
+ * re-fetches once on an unknown `kid` before failing closed.
150
+ */
151
+ jwksUri?: string;
152
+ /**
153
+ * Pre-resolved JWKS (test / air-gapped path). The storefront holds ONLY the
154
+ * published public material — no private key, no secret. Exactly one of
155
+ * {@link jwksUri} or {@link jwks} must be supplied.
156
+ */
157
+ jwks?: JSONWebKeySet;
158
+ }
159
+
160
+ /**
161
+ * Verifies tot identity assertions offline against the published JWKS. One
162
+ * instance per (audience, JWKS host); its `audience` is fixed. Never calls tot
163
+ * per request — the JWKS fetch is cached inside jose's key resolver, so once the
164
+ * key material is in-process, verification survives tot being unreachable.
165
+ */
166
+ export class IdentityAssertionVerifier {
167
+ private readonly audience: string;
168
+ private readonly issuer: string;
169
+ private readonly keys: JWTVerifyGetKey;
170
+
171
+ constructor(config: IdentityVerifierConfig) {
172
+ if (!config.audience) {
173
+ throw new Error("IdentityAssertionVerifier requires an `audience` (this host's resource)");
174
+ }
175
+ if (!!config.jwksUri === !!config.jwks) {
176
+ throw new Error("IdentityAssertionVerifier requires exactly one of `jwksUri` or `jwks`");
177
+ }
178
+ this.audience = config.audience;
179
+ this.issuer = config.issuer ?? TOT_ISSUER;
180
+ this.keys = config.jwks
181
+ ? createLocalJWKSet(config.jwks)
182
+ : createRemoteJWKSet(new URL(config.jwksUri as string));
183
+ }
184
+
185
+ /**
186
+ * Verify a compact JWS and return typed claims. Rejects (throws) on: any
187
+ * `alg` ≠ ES256 (blocks `alg:none` and every HS*), bad signature, wrong/absent
188
+ * `aud`, unlisted `iss`, or expired/not-yet-valid `exp`/`iat`.
189
+ */
190
+ async verify(token: string): Promise<IdentityClaims> {
191
+ // Fail fast + loud on the algorithm before touching keys. jose's `algorithms`
192
+ // allowlist below is the authoritative guard; this makes the reason explicit.
193
+ const header = decodeProtectedHeader(token);
194
+ if (header.alg !== TOT_ALG) {
195
+ throw new Error(`unexpected JWT alg ${String(header.alg)}; only ${TOT_ALG} is accepted`);
196
+ }
197
+
198
+ const { payload } = await jwtVerify(token, this.keys, {
199
+ issuer: this.issuer,
200
+ audience: this.audience,
201
+ algorithms: [TOT_ALG],
202
+ });
203
+
204
+ return toIdentityClaims(payload as Record<string, unknown>);
205
+ }
206
+ }
207
+
208
+ // --- Verifier cache -----------------------------------------------------------
209
+ //
210
+ // In-memory, module-scoped cache keyed by (jwksUri | audience). Good enough for
211
+ // the prototype: each Worker isolate builds its verifier once and reuses jose's
212
+ // cached JWKS across requests. FOLLOW-UP (multi-instance): promote the JWKS cache
213
+ // to KV or a Durable Object so isolates share fetched key material and honour a
214
+ // single rotation signal. Tracked with the Phase-4 kernel adoption.
215
+ const verifierCache = new Map<string, IdentityAssertionVerifier>();
216
+
217
+ /**
218
+ * Get (or build + cache) the verifier for a resource, deriving the JWKS URL from
219
+ * the configured tot API base (`TOT_CORE_URL` — the same base `totAccessClient`
220
+ * posts the access-code endpoints to). Returns `null` when tot's base URL is not
221
+ * configured (routes fail closed). Pass `overrides.jwks`/`overrides.jwksUri` in
222
+ * tests to inject key material without a network fetch.
223
+ */
224
+ export async function getIdentityVerifier(
225
+ resource: string,
226
+ overrides: { jwksUri?: string; jwks?: JSONWebKeySet; baseUrl?: string } = {},
227
+ ): Promise<IdentityAssertionVerifier | null> {
228
+ if (overrides.jwks) {
229
+ // Test / air-gapped: never cached (keys are per-test) and never networked.
230
+ return new IdentityAssertionVerifier({ audience: resource, jwks: overrides.jwks });
231
+ }
232
+ const jwksUri =
233
+ overrides.jwksUri ??
234
+ deriveJwksUri(overrides.baseUrl ?? (await readEnv("TOT_CORE_URL")) ?? "");
235
+ if (jwksUri === JWKS_PATH) return null; // empty base → unconfigured
236
+
237
+ const cacheKey = `${jwksUri}|${resource}`;
238
+ let v = verifierCache.get(cacheKey);
239
+ if (!v) {
240
+ v = new IdentityAssertionVerifier({ audience: resource, jwksUri });
241
+ verifierCache.set(cacheKey, v);
242
+ }
243
+ return v;
244
+ }
245
+
246
+ /**
247
+ * Get (or build + cache) the FEDERATED verifier — the one for multi-tenant
248
+ * sign-in. Its audience is the STOREFRONT'S OWN app identity (`STOREFRONT_APP_DOMAIN`),
249
+ * because tot20's federated `id_token` sets `aud` = the calling app (storefront),
250
+ * NOT a tenant. The token then carries ALL of the email's memberships in
251
+ * `tenants[]`. Returns null when the tot base URL (`TOT_CORE_URL`) or the
252
+ * storefront app identity (`STOREFRONT_APP_DOMAIN`) is unconfigured — the route
253
+ * fails closed. Pass overrides in tests to inject key material + audience.
254
+ */
255
+ export async function getFederatedVerifier(
256
+ overrides: { jwksUri?: string; jwks?: JSONWebKeySet; baseUrl?: string; audience?: string } = {},
257
+ ): Promise<IdentityAssertionVerifier | null> {
258
+ const audience = overrides.audience ?? (await readEnv("STOREFRONT_APP_DOMAIN")) ?? "";
259
+ if (!audience) return null; // storefront's own app identity must be configured
260
+ if (overrides.jwks) {
261
+ return new IdentityAssertionVerifier({ audience, jwks: overrides.jwks });
262
+ }
263
+ const jwksUri =
264
+ overrides.jwksUri ??
265
+ deriveJwksUri(overrides.baseUrl ?? (await readEnv("TOT_CORE_URL")) ?? "");
266
+ if (jwksUri === JWKS_PATH) return null; // empty base → unconfigured
267
+ const cacheKey = `${jwksUri}|${audience}`;
268
+ let v = verifierCache.get(cacheKey);
269
+ if (!v) {
270
+ v = new IdentityAssertionVerifier({ audience, jwksUri });
271
+ verifierCache.set(cacheKey, v);
272
+ }
273
+ return v;
274
+ }
275
+
276
+ // --- Session derivation from verified claims ---------------------------------
277
+
278
+ /** The session fields derived from the VERIFIED token — the Phase-3 source of truth. */
279
+ export interface DerivedSessionFields {
280
+ email: string;
281
+ resource: string;
282
+ /** Coarse standing on THIS resource, surfaced as the display capability. */
283
+ capability: CoarseRole | "none";
284
+ /** Display roles derived from the coarse claim (not tot's raw app roles). */
285
+ roles: string[];
286
+ /** Full multi-tenant memberships carried in the token (policy input). */
287
+ tenants: TenantMembership[];
288
+ /** ToT-staff scope carried in the token (sandboxed; grants nothing here). */
289
+ staff: string[];
290
+ }
291
+
292
+ /**
293
+ * Derive the storefront session from VERIFIED claims for a given resource — the
294
+ * Phase-3 replacement for trusting tot's flat `{roles, capability}`. Picks the
295
+ * membership matching `resource` and collapses its coarse role to the display
296
+ * capability. Throws if the verified token carries no membership for `resource`
297
+ * (a token for another tenant must not open this gate — belt-and-braces on top
298
+ * of the `aud` check).
299
+ */
300
+ export function sessionFromClaims(claims: IdentityClaims, resource: string): DerivedSessionFields {
301
+ const membership = claims.tenants.find((t) => t.resource === resource);
302
+ if (!membership) {
303
+ throw new Error(`verified assertion carries no membership for resource "${resource}"`);
304
+ }
305
+ return {
306
+ email: claims.email,
307
+ resource,
308
+ capability: membership.role,
309
+ roles: [membership.role],
310
+ tenants: claims.tenants,
311
+ staff: claims.staff,
312
+ };
313
+ }
314
+
315
+ /** The multi-tenant fields derived from a verified FEDERATED assertion. */
316
+ export interface FederatedSessionFields {
317
+ email: string;
318
+ /** The verified `aud` — the storefront app identity this console session is for. */
319
+ resource: string;
320
+ /** EVERY tenant membership the token carries (the dashboard picker's input). */
321
+ tenants: TenantMembership[];
322
+ /** ToT-staff scope carried in the token (sandboxed; grants nothing here). */
323
+ staff: string[];
324
+ }
325
+
326
+ /**
327
+ * Derive the storefront CONSOLE session from a verified FEDERATED assertion. Unlike
328
+ * {@link sessionFromClaims}, this does NOT require (or scope to) a single tenant:
329
+ * it binds ALL of `claims.tenants` so a user with N memberships yields an N-vendor
330
+ * session. `resource` is the verified `aud` (the storefront app identity), not a
331
+ * tenant — it exists only to satisfy the session record shape; the dashboard reads
332
+ * `tenants[]`, and the tenant-scoped preview gate never matches it.
333
+ */
334
+ export function sessionFromFederatedClaims(claims: IdentityClaims): FederatedSessionFields {
335
+ return {
336
+ email: claims.email,
337
+ resource: claims.aud,
338
+ tenants: claims.tenants,
339
+ staff: claims.staff,
340
+ };
341
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Login-gate decision (pure) — the "Sign in with ToT" enforcement mode for a
3
+ * gated demo host, the eventual replacement for the interim Basic-Auth password
4
+ * (see `@/lib/demoGate`). Same `DEMO_GATE_HOSTS` list, switched by `DEMO_GATE_MODE`:
5
+ *
6
+ * DEMO_GATE_MODE = "password" → interim Basic-Auth gate (evaluateDemoGate)
7
+ * DEMO_GATE_MODE = "login" → this: redirect to /auth/login unless a valid,
8
+ * resource-bound tot_session cookie is present.
9
+ *
10
+ * This module is PURE (no KV, no cookies, no env I/O): the middleware reads the
11
+ * session (KV) and passes the resolved record in. Keeping the decision pure makes
12
+ * the exemption + redirect logic unit-testable and keeps the two gate modes from
13
+ * drifting apart. The auth surface itself (/auth/*, /api/auth/*) is always exempt
14
+ * so a visitor can actually sign in.
15
+ */
16
+ import { hostInGateList } from "@/lib/demoGate";
17
+ import { viewerCanAccess } from "@/lib/dashboard/tenantSelection";
18
+ import type { SessionRecord } from "./session.js";
19
+
20
+ export type GateMode = "password" | "login";
21
+
22
+ /**
23
+ * The platform's own "self" home tenant (tenants.ts `HOME_TENANT`, registry
24
+ * default). It is NEVER login-gated — `storefront.tokenoftrust.store` renders its
25
+ * public home so people can browse + sign up. Every OTHER tenant on a gated host
26
+ * (path-prefix or per-tenant subdomain) is gated behind sign-in.
27
+ */
28
+ export const PUBLIC_HOME_TENANT_ID = "home";
29
+
30
+ /**
31
+ * Whether a (federated) viewer session admits the gated HOST — i.e. the signed-in
32
+ * user holds a membership for the host's tenant (`hostResource`, the host's
33
+ * appDomain). This UNIFIES the preview-host gate onto the single federated session:
34
+ * federated sign-in mints a multi-tenant session whose `aud`/`resource` is the
35
+ * storefront app (not a tenant), so the old resource-scoped session match never
36
+ * fit; membership in `tenants[]` is the right test. Deny-closed: no session, or a
37
+ * session without a membership for `hostResource`, is not admitted. (Backward-
38
+ * compatible with a legacy resource-scoped session, whose `tenants[]` still lists
39
+ * its own resource.)
40
+ */
41
+ export function sessionAdmitsHost(
42
+ record: Pick<SessionRecord, "tenants" | "staffSelection" | "staff"> | null,
43
+ hostResource: string,
44
+ nowSeconds: number = Math.floor(Date.now() / 1000),
45
+ ): boolean {
46
+ if (!record) return false;
47
+ // ToT STAFF get in everywhere: any session carrying a verified `staff` scope is
48
+ // admitted to EVERY gated demo host with just a login — no per-vendor selection
49
+ // needed. This is a GATE-level admission for demo BROWSING only; dashboard-action
50
+ // authorization (viewerCanAccess) stays membership/selection-bound + audited.
51
+ if (Array.isArray(record.staff) && record.staff.length > 0) return true;
52
+ // Otherwise: a membership OR a live ToT-staff selection for this exact host admits
53
+ // — the staff-selection path is distinct from tenants[] (never a synthesized member).
54
+ return viewerCanAccess(record.tenants, hostResource, {
55
+ selection: record.staffSelection,
56
+ nowSeconds,
57
+ });
58
+ }
59
+
60
+ /** Parse `DEMO_GATE_MODE` (default "password"). */
61
+ export function parseGateMode(raw: string | null | undefined): GateMode {
62
+ return (raw ?? "").trim().toLowerCase() === "login" ? "login" : "password";
63
+ }
64
+
65
+ /**
66
+ * Paths that MUST stay reachable without a session, or the visitor could never
67
+ * authenticate: the login page and the send/verify endpoints. Everything else on
68
+ * a login-gated host requires a session.
69
+ */
70
+ export function isAuthPath(pathname: string): boolean {
71
+ return pathname === "/auth/login" || pathname.startsWith("/api/auth/");
72
+ }
73
+
74
+ /**
75
+ * Genuinely-public API paths that must answer without a session even on a
76
+ * login-gated host — the CLI version-support policy is fetched by an
77
+ * unauthenticated `tot` install (src/pages/api/cli/policy.ts). Kept exact (not a
78
+ * broad `/api/cli/` prefix) so a future authed CLI endpoint isn't accidentally
79
+ * exposed.
80
+ */
81
+ export function isPublicPath(pathname: string): boolean {
82
+ return pathname === "/api/cli/policy";
83
+ }
84
+
85
+ export type LoginGateDecision =
86
+ | { action: "pass" }
87
+ | { action: "redirect"; location: string };
88
+
89
+ export interface LoginGateInput {
90
+ host: string;
91
+ pathname: string;
92
+ search: string;
93
+ mode: GateMode;
94
+ /** The gated-host list (`DEMO_GATE_HOSTS`). */
95
+ gatedHosts: string | null | undefined;
96
+ /** True when a valid, resource-bound session was resolved for this request. */
97
+ hasValidSession: boolean;
98
+ /**
99
+ * True when the resolved tenant is the platform's own public home (self) —
100
+ * exempt from the gate so it renders publicly. See {@link PUBLIC_HOME_TENANT_ID}.
101
+ */
102
+ tenantExempt?: boolean;
103
+ }
104
+
105
+ /**
106
+ * Decide what the login gate should do. Returns `pass` when the host isn't
107
+ * login-gated, the path is an auth path, or a valid session exists; otherwise a
108
+ * `redirect` to /auth/login carrying the intended destination as `next`.
109
+ */
110
+ export function evaluateLoginGate(input: LoginGateInput): LoginGateDecision {
111
+ if (input.mode !== "login") return { action: "pass" };
112
+ if (!hostInGateList(input.host, input.gatedHosts)) return { action: "pass" };
113
+ if (input.tenantExempt) return { action: "pass" };
114
+ if (isAuthPath(input.pathname)) return { action: "pass" };
115
+ if (isPublicPath(input.pathname)) return { action: "pass" };
116
+ if (input.hasValidSession) return { action: "pass" };
117
+ const next = input.pathname + (input.search ?? "");
118
+ return { action: "redirect", location: `/auth/login?next=${encodeURIComponent(next)}` };
119
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Shared helpers for the `/auth/*` + `/api/auth/*` sign-in surface. Kept out of
3
+ * the pure session/client modules so those stay transport- and Astro-free and
4
+ * unit-test against the in-memory impls.
5
+ *
6
+ * The flow (no-JS-friendly, all on the tenant's own branded host):
7
+ * GET /auth/login → email card (step 1) / code card (step 2)
8
+ * POST /api/auth/send {email} → tot20 sendAccessCode → 303 back to step 2
9
+ * POST /api/auth/verify{email,code}→ tot20 verifyAccessCode → session cookie → 303 next
10
+ *
11
+ * The gate ("login" mode) redirects unauthenticated viewers of a gated host to
12
+ * /auth/login and lets a valid `tot_session` cookie back through. The `resource`
13
+ * (the tot20 appDomain the grant is checked against) comes from the resolved
14
+ * tenant — never from request input.
15
+ */
16
+ import type { APIContext } from "astro";
17
+ import { readKv } from "@/lib/env";
18
+ import { createTotAccessClient, type TotAccessClient } from "./totAccessClient.js";
19
+ import {
20
+ KvSessionStore,
21
+ readSession,
22
+ type SessionRecord,
23
+ type SessionStore,
24
+ SESSION_COOKIE,
25
+ } from "./session.js";
26
+ import {
27
+ getIdentityVerifier as buildIdentityVerifier,
28
+ type IdentityAssertionVerifier,
29
+ } from "./identityToken.js";
30
+
31
+ /** Permissive email shape — tot20 is the real authority on eligibility. */
32
+ export const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
33
+
34
+ /**
35
+ * The tot20 `resource` (appDomain) the email's roles are checked against for this
36
+ * host. Derived from the resolved tenant, never from input.
37
+ *
38
+ * INTEGRATION NOTE: which tenant field equals tot20's appDomain for a store is
39
+ * confirmed at integration (task #4). We use `appDomain` (the tenant's app
40
+ * identity) and allow a per-tenant override via `commerce`/config later.
41
+ */
42
+ export function resolveResource(context: APIContext): string | null {
43
+ const t = context.locals.tenant;
44
+ if (!t) return null;
45
+ return t.appDomain || t.tenant_id || null;
46
+ }
47
+
48
+ /** The access client from env, or null when the integration isn't configured. */
49
+ export async function getAccessClient(): Promise<TotAccessClient | null> {
50
+ try {
51
+ return await createTotAccessClient();
52
+ } catch {
53
+ return null; // unconfigured → routes return 503
54
+ }
55
+ }
56
+
57
+ /** KV-backed session store, or null in runtimes without the SESSION binding (dev). */
58
+ export async function getSessionStore(): Promise<SessionStore | null> {
59
+ const kv = await readKv("SESSION");
60
+ return kv ? new KvSessionStore(kv) : null;
61
+ }
62
+
63
+ /**
64
+ * Read the signed-in viewer's FULL session (email + all tenant memberships), or
65
+ * null when there's no valid session. Unlike the gate's read, this is NOT scoped
66
+ * to a single host `resource` — the vendor dashboard needs every membership in
67
+ * `tenants[]` to route (single → in, multiple → picker) and to authorize actions
68
+ * against the SELECTED vendor. Returns null in runtimes without the SESSION KV.
69
+ */
70
+ export async function readViewerSession(
71
+ context: { cookies: APIContext["cookies"] },
72
+ ): Promise<SessionRecord | null> {
73
+ const store = await getSessionStore();
74
+ if (!store) return null;
75
+ const sid = context.cookies.get(SESSION_COOKIE)?.value ?? null;
76
+ return readSession(store, sid, Math.floor(Date.now() / 1000));
77
+ }
78
+
79
+ /**
80
+ * The session store + the current request's opaque session id, for callers that
81
+ * need to MUTATE the stored record (e.g. stamping a staff vendor selection). Null
82
+ * when there's no SESSION KV (dev) or no session cookie. Kept alongside
83
+ * {@link readViewerSession} so mutation and read share the same store/cookie seam.
84
+ */
85
+ export async function getSessionMutationHandle(
86
+ context: { cookies: APIContext["cookies"] },
87
+ ): Promise<{ store: SessionStore; id: string } | null> {
88
+ const store = await getSessionStore();
89
+ if (!store) return null;
90
+ const id = context.cookies.get(SESSION_COOKIE)?.value ?? null;
91
+ if (!id) return null;
92
+ return { store, id };
93
+ }
94
+
95
+ /**
96
+ * The offline `id_token` verifier for this host's `resource`, or null when the
97
+ * tot JWKS host (`TOT_CORE_URL`) isn't configured. Derives the JWKS URL from the
98
+ * same tot API base the access-code client posts to; jose caches the fetch so
99
+ * verification survives tot being unreachable after sign-in (Phase-3 property).
100
+ */
101
+ export async function getVerifier(resource: string): Promise<IdentityAssertionVerifier | null> {
102
+ return buildIdentityVerifier(resource);
103
+ }
104
+
105
+ export { getVerifier as getIdentityVerifier };
106
+
107
+ /** Set the session cookie (HttpOnly/Secure/SameSite=Lax) for `maxAgeSeconds`. */
108
+ export function setSessionCookie(context: APIContext, id: string, maxAgeSeconds: number): void {
109
+ context.cookies.set(SESSION_COOKIE, id, {
110
+ httpOnly: true,
111
+ secure: true,
112
+ sameSite: "lax",
113
+ path: "/",
114
+ maxAge: maxAgeSeconds,
115
+ });
116
+ }
117
+
118
+ /** Clear the session cookie (logout). */
119
+ export function clearSessionCookie(context: APIContext): void {
120
+ context.cookies.delete(SESSION_COOKIE, { path: "/" });
121
+ }
122
+
123
+ /**
124
+ * Sanitize a `next` redirect target to a same-origin PATH only (starts with a
125
+ * single "/", not "//" or a scheme) — closes the open-redirect hole. Falls back
126
+ * to "/".
127
+ */
128
+ export function safeNext(next: string | null | undefined): string {
129
+ if (!next) return "/";
130
+ if (!next.startsWith("/") || next.startsWith("//")) return "/";
131
+ return next;
132
+ }
133
+
134
+ /** Redirect back to the login page carrying step-2 state (email/error), no-JS-safe. */
135
+ export function loginRedirect(
136
+ context: APIContext,
137
+ params: { sent?: boolean; email?: string; error?: string; next?: string },
138
+ ): Response {
139
+ const q = new URLSearchParams();
140
+ if (params.sent) q.set("sent", "1");
141
+ if (params.email) q.set("email", params.email);
142
+ if (params.error) q.set("error", params.error);
143
+ if (params.next && params.next !== "/") q.set("next", params.next);
144
+ const qs = q.toString();
145
+ return context.redirect(`/auth/login${qs ? `?${qs}` : ""}`, 303);
146
+ }