@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,64 @@
1
+ ---
2
+ /**
3
+ * Sort control. A plain <form> select that auto-submits on change — works without
4
+ * a framework, stays URL-addressable. The current query's other params ride
5
+ * along as hidden inputs so sort doesn't drop active filters.
6
+ *
7
+ * Auto-submit is wired by a nonce'd `is:inline` <script> rather than an inline
8
+ * `onchange=` handler, so a strict, ENFORCED CSP allows it without 'unsafe-inline'
9
+ * / 'unsafe-hashes' (Epic S2). The <noscript> Apply button covers the no-JS case.
10
+ */
11
+ import type { ListProductsQuery } from "@tot/public-runtime";
12
+
13
+ interface Props {
14
+ query: ListProductsQuery;
15
+ basePath: string;
16
+ }
17
+ const { query, basePath } = Astro.props;
18
+
19
+ const OPTIONS: { value: NonNullable<ListProductsQuery["sort"]>; label: string }[] = [
20
+ { value: "featured", label: "Featured" },
21
+ { value: "newest", label: "Newest" },
22
+ { value: "price-asc", label: "Price: Low to High" },
23
+ { value: "price-desc", label: "Price: High to Low" },
24
+ { value: "title-asc", label: "Alphabetical (A–Z)" },
25
+ ];
26
+ const current = query.sort ?? "featured";
27
+ ---
28
+
29
+ <form method="get" action={basePath} class="flex items-center gap-2 text-sm">
30
+ {/* Preserve filters/price/availability/q across a sort change. */}
31
+ {query.q && <input type="hidden" name="q" value={query.q} />}
32
+ {query.availability && query.availability !== "all" && (
33
+ <input type="hidden" name="availability" value={query.availability} />
34
+ )}
35
+ {query.priceMin != null && <input type="hidden" name="priceMin" value={query.priceMin} />}
36
+ {query.priceMax != null && <input type="hidden" name="priceMax" value={query.priceMax} />}
37
+ {Object.entries(query.filters ?? {}).flatMap(([name, values]) =>
38
+ values.map((v) => (
39
+ <input type="hidden" name={name === "tags" ? "tag" : `opt.${name}`} value={v} />
40
+ )),
41
+ )}
42
+
43
+ <label for="sort" class="text-[var(--color-muted)]">Sort</label>
44
+ <select
45
+ id="sort"
46
+ name="sort"
47
+ data-sort-autosubmit
48
+ class="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1.5 font-display"
49
+ >
50
+ {OPTIONS.map((o) => (
51
+ <option value={o.value} selected={o.value === current}>{o.label}</option>
52
+ ))}
53
+ </select>
54
+ <noscript><button type="submit" class="underline">Apply</button></noscript>
55
+ </form>
56
+
57
+ <script is:inline nonce={Astro.locals.cspNonce}>
58
+ // Progressive enhancement: submit the form when the sort changes. Nonce'd
59
+ // inline (the platform pattern for app-authored inline JS) so a strict,
60
+ // ENFORCED CSP allows it without 'unsafe-inline'.
61
+ for (const el of document.querySelectorAll("select[data-sort-autosubmit]")) {
62
+ el.addEventListener("change", () => el.form && el.form.submit());
63
+ }
64
+ </script>
@@ -0,0 +1,95 @@
1
+ ---
2
+ /**
3
+ * Persistent left-hand navigation for the preview/dev style-guide surfaces.
4
+ *
5
+ * Markup-only (no <style>): the host page owns the `.sg-nav*` styles inside its
6
+ * nonce'd <style> block, so this component stays CSP-clean under the strict
7
+ * inline-style policy (build.inlineStylesheets:"never"). Rendered on the tenant
8
+ * hub, the widget-library guide, and any per-theme guide that wants a persistent
9
+ * rail so the Widget Library Guide is one click away from the top page.
10
+ *
11
+ * Accessibility: every entry is a real <a>; the active entry carries
12
+ * aria-current="page". When `headings` is supplied (the guide's H2 outline) a
13
+ * nested table of contents is rendered as in-page anchor links.
14
+ */
15
+ type Active = "index" | "tenant" | "components" | "chrome" | "guide";
16
+
17
+ interface Heading {
18
+ depth: number;
19
+ slug: string;
20
+ text: string;
21
+ }
22
+
23
+ interface Props {
24
+ tenant: string;
25
+ /** Current theme id — needed to build the per-theme component/chrome/guide links. */
26
+ theme?: string;
27
+ active: Active;
28
+ /** Optional H2 outline of the guide, rendered as an in-page TOC under the guide entry. */
29
+ headings?: Heading[];
30
+ }
31
+
32
+ const { tenant, theme, active, headings = [] } = Astro.props;
33
+
34
+ const base = `/_style-guide`;
35
+ // Per-theme surfaces only resolve when we know which theme to skin with.
36
+ const componentsHref = theme ? `${base}/${tenant}/${theme}/` : undefined;
37
+ const chromeHref = theme ? `${base}/${tenant}/chrome/${theme}/` : undefined;
38
+ const guideHref = theme ? `${base}/${tenant}/guide/${theme}/` : undefined;
39
+
40
+ const toc = headings.filter((h) => h.depth === 2);
41
+ ---
42
+
43
+ <nav class="sg-nav" aria-label="Style guide">
44
+ <p class="sg-nav__eyebrow">Style guide</p>
45
+ <ul class="sg-nav__list">
46
+ <li>
47
+ <a
48
+ href={`${base}/`}
49
+ class:list={["sg-nav__link", { "is-active": active === "index" }]}
50
+ aria-current={active === "index" ? "page" : undefined}
51
+ >All client guides</a>
52
+ </li>
53
+ <li>
54
+ <a
55
+ href={`${base}/${tenant}/`}
56
+ class:list={["sg-nav__link", { "is-active": active === "tenant" }]}
57
+ aria-current={active === "tenant" ? "page" : undefined}
58
+ >{tenant} — overview</a>
59
+ </li>
60
+ {componentsHref && (
61
+ <li>
62
+ <a
63
+ href={componentsHref}
64
+ class:list={["sg-nav__link", "sg-nav__link--sub", { "is-active": active === "components" }]}
65
+ aria-current={active === "components" ? "page" : undefined}
66
+ >Components</a>
67
+ </li>
68
+ )}
69
+ {chromeHref && (
70
+ <li>
71
+ <a
72
+ href={chromeHref}
73
+ class:list={["sg-nav__link", "sg-nav__link--sub", { "is-active": active === "chrome" }]}
74
+ aria-current={active === "chrome" ? "page" : undefined}
75
+ >Shared chrome</a>
76
+ </li>
77
+ )}
78
+ {guideHref && (
79
+ <li>
80
+ <a
81
+ href={guideHref}
82
+ class:list={["sg-nav__link", "sg-nav__link--sub", { "is-active": active === "guide" }]}
83
+ aria-current={active === "guide" ? "page" : undefined}
84
+ >Widget Library Guide</a>
85
+ {active === "guide" && toc.length > 0 && (
86
+ <ul class="sg-nav__toc">
87
+ {toc.map((h) => (
88
+ <li><a class="sg-nav__toc-link" href={`#${h.slug}`}>{h.text}</a></li>
89
+ ))}
90
+ </ul>
91
+ )}
92
+ </li>
93
+ )}
94
+ </ul>
95
+ </nav>
@@ -0,0 +1,67 @@
1
+ ---
2
+ /**
3
+ * <HowItWorksSteps> — the Autopilot/subscription "how it works" explainer
4
+ * (e.g. Choose products → Set your schedule → We ship on autopilot → Pause or
5
+ * cancel anytime). Presentational + EDUCATION ONLY: it walks through the
6
+ * concept, but ships NO scheduling, billing, or subscription mechanics — a
7
+ * tenant wires the real subscribe flow in the product/commerce layer.
8
+ *
9
+ * Token-styled only — re-skins entirely from the theme's contract tokens
10
+ * (--color-…, --radius-…); no hardcoded brand color.
11
+ * data-widget="subscription-how-it-works" is the audit/test hook.
12
+ */
13
+ import type { SubscriptionStep } from "@/lib/subscription/model";
14
+
15
+ interface Props {
16
+ steps: SubscriptionStep[];
17
+ /** Optional small eyebrow above the heading. */
18
+ eyebrow?: string;
19
+ /** Optional section heading. */
20
+ heading?: string;
21
+ /** Optional supporting sentence under the heading. */
22
+ subhead?: string;
23
+ }
24
+
25
+ const { steps, eyebrow, heading, subhead } = Astro.props;
26
+ ---
27
+
28
+ {
29
+ steps.length > 0 && (
30
+ <section class="container-prose" data-widget="subscription-how-it-works">
31
+ {(eyebrow || heading || subhead) && (
32
+ <header class="mb-8 text-center">
33
+ {eyebrow && (
34
+ <p class="mb-2 text-xs font-semibold uppercase tracking-[0.12em] text-[var(--color-accent-ink)]">
35
+ {eyebrow}
36
+ </p>
37
+ )}
38
+ {heading && (
39
+ <h2 class="font-display text-2xl font-bold text-[var(--color-text)] sm:text-3xl">
40
+ {heading}
41
+ </h2>
42
+ )}
43
+ {subhead && (
44
+ <p class="mx-auto mt-3 max-w-[60ch] text-[var(--color-muted)]">{subhead}</p>
45
+ )}
46
+ </header>
47
+ )}
48
+
49
+ <ol class="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
50
+ {steps.map((step, i) => (
51
+ <li class="flex flex-col rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface)] p-5">
52
+ <span
53
+ class="mb-3 flex h-8 w-8 items-center justify-center rounded-[var(--radius-full)] bg-[var(--color-primary)] text-sm font-bold text-[var(--color-primary-contrast)]"
54
+ aria-hidden="true"
55
+ >
56
+ {i + 1}
57
+ </span>
58
+ <h3 class="font-display text-base font-semibold text-[var(--color-text)]">
59
+ {step.title}
60
+ </h3>
61
+ <p class="mt-1.5 text-sm text-[var(--color-muted)]">{step.description}</p>
62
+ </li>
63
+ ))}
64
+ </ol>
65
+ </section>
66
+ )
67
+ }
@@ -0,0 +1,67 @@
1
+ ---
2
+ /**
3
+ * <ManageSubscriptionEntry> — a set of manage-subscription entry points (e.g.
4
+ * "Update delivery date", "Pause or cancel anytime"). Presentational +
5
+ * EDUCATION/HANDOFF ONLY: each entry is a plain link into the tenant's own
6
+ * (product-owned) manage-subscription flow — the widget never renders
7
+ * scheduling, billing, or cancellation controls of its own.
8
+ *
9
+ * Token-styled only; data-widget="subscription-manage-entry" is the
10
+ * audit/test hook.
11
+ */
12
+ import type { SubscriptionManageEntry } from "@/lib/subscription/model";
13
+
14
+ interface Props {
15
+ entries: SubscriptionManageEntry[];
16
+ /** Optional small eyebrow above the heading. */
17
+ eyebrow?: string;
18
+ /** Optional section heading. */
19
+ heading?: string;
20
+ /** Optional supporting sentence under the heading. */
21
+ subhead?: string;
22
+ }
23
+
24
+ const { entries, eyebrow, heading, subhead } = Astro.props;
25
+ ---
26
+
27
+ {
28
+ entries.length > 0 && (
29
+ <section class="container-prose" data-widget="subscription-manage-entry">
30
+ {(eyebrow || heading || subhead) && (
31
+ <header class="mb-8 text-center">
32
+ {eyebrow && (
33
+ <p class="mb-2 text-xs font-semibold uppercase tracking-[0.12em] text-[var(--color-accent-ink)]">
34
+ {eyebrow}
35
+ </p>
36
+ )}
37
+ {heading && (
38
+ <h2 class="font-display text-2xl font-bold text-[var(--color-text)] sm:text-3xl">
39
+ {heading}
40
+ </h2>
41
+ )}
42
+ {subhead && (
43
+ <p class="mx-auto mt-3 max-w-[60ch] text-[var(--color-muted)]">{subhead}</p>
44
+ )}
45
+ </header>
46
+ )}
47
+
48
+ <ul class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
49
+ {entries.map((entry) => (
50
+ <li>
51
+ <a
52
+ href={entry.href}
53
+ class="flex min-h-[44px] flex-col rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface)] p-4 transition-colors hover:border-[var(--color-primary)]"
54
+ >
55
+ <span class="font-display text-base font-semibold text-[var(--color-text)]">
56
+ {entry.label}
57
+ </span>
58
+ {entry.description && (
59
+ <span class="mt-1 text-sm text-[var(--color-muted)]">{entry.description}</span>
60
+ )}
61
+ </a>
62
+ </li>
63
+ ))}
64
+ </ul>
65
+ </section>
66
+ )
67
+ }
@@ -0,0 +1,101 @@
1
+ ---
2
+ /**
3
+ * <SavingsExplainer> — the Autopilot/subscription savings headline (e.g.
4
+ * "Save 15% — vs. one-time purchase") with supporting bullets and a plain
5
+ * handoff CTA. Presentational + EDUCATION ONLY: no pricing, billing, or
6
+ * subscription mechanics — the CTA is a link into the tenant's own
7
+ * (product-owned) subscribe/learn flow.
8
+ *
9
+ * Illustrative-figure affordance (decision compliance-widgets-platform-owned):
10
+ * the figure is treated as an EXAMPLE by default (isIllustrative). When
11
+ * illustrative, the figure carries an "e.g." affordance + a first-class
12
+ * <IllustrativeLabel> badge/footnote so an example number is never presented
13
+ * as a live offer. A tenant opts out only by asserting `real: true` on a
14
+ * confirmed live value.
15
+ *
16
+ * Token-styled only; data-widget="subscription-savings-explainer" is the
17
+ * audit/test hook.
18
+ */
19
+ import type { SubscriptionFigure } from "@/lib/subscription/model";
20
+ import { isIllustrative } from "@/lib/subscription/model";
21
+ import IllustrativeLabel from "@/components/membership/IllustrativeLabel.astro";
22
+
23
+ interface Props {
24
+ figure: SubscriptionFigure;
25
+ /** Optional small eyebrow above the heading. */
26
+ eyebrow?: string;
27
+ /** Optional section heading. */
28
+ heading?: string;
29
+ /** Supporting bullets, e.g. "Free shipping on every order". */
30
+ bullets?: string[];
31
+ /** Education/handoff CTA (never a checkout mechanic). */
32
+ cta?: { label: string; href: string };
33
+ }
34
+
35
+ const { figure, eyebrow, heading, bullets, cta } = Astro.props;
36
+ const illustrative = isIllustrative(figure);
37
+ ---
38
+
39
+ <section class="container-prose" data-widget="subscription-savings-explainer">
40
+ <div class="flex flex-col items-center rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface)] p-8 text-center">
41
+ {eyebrow && (
42
+ <p class="mb-2 text-xs font-semibold uppercase tracking-[0.12em] text-[var(--color-accent-ink)]">
43
+ {eyebrow}
44
+ </p>
45
+ )}
46
+ {heading && (
47
+ <h2 class="font-display text-2xl font-bold text-[var(--color-text)] sm:text-3xl">
48
+ {heading}
49
+ </h2>
50
+ )}
51
+
52
+ <p
53
+ class="mt-4 flex items-baseline gap-1.5"
54
+ data-illustrative={illustrative ? "figure" : undefined}
55
+ >
56
+ {illustrative && (
57
+ <span class="text-xs font-medium text-[var(--color-muted)]" title="Illustrative example">
58
+ e.g.
59
+ </span>
60
+ )}
61
+ <span class="font-display text-4xl font-bold leading-none text-[var(--color-text)]">
62
+ {figure.value}
63
+ </span>
64
+ {figure.caption && (
65
+ <span class="text-sm text-[var(--color-muted)]">{figure.caption}</span>
66
+ )}
67
+ </p>
68
+
69
+ {illustrative && (
70
+ <div class="mt-3">
71
+ <IllustrativeLabel as="badge" />
72
+ </div>
73
+ )}
74
+
75
+ {bullets && bullets.length > 0 && (
76
+ <ul class="mt-6 space-y-2 text-left">
77
+ {bullets.map((b) => (
78
+ <li class="flex items-start gap-2.5 text-sm text-[var(--color-text)]">
79
+ <span aria-hidden="true" class="mt-0.5 text-[var(--color-success)]">✓</span>
80
+ <span>{b}</span>
81
+ </li>
82
+ ))}
83
+ </ul>
84
+ )}
85
+
86
+ {cta && (
87
+ <a
88
+ href={cta.href}
89
+ class="mt-6 inline-flex min-h-[44px] items-center justify-center rounded-[var(--radius-full)] bg-[var(--color-primary)] px-5 py-2.5 text-sm font-semibold text-[var(--color-primary-contrast)]"
90
+ >
91
+ {cta.label}
92
+ </a>
93
+ )}
94
+
95
+ {illustrative && (
96
+ <div class="mt-5">
97
+ <IllustrativeLabel as="note" />
98
+ </div>
99
+ )}
100
+ </div>
101
+ </section>
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Resolve an admin console TARGET tenant's full config through the sanctioned
3
+ * TenantResolver seam.
4
+ *
5
+ * The admin console can operate on a target tenant that differs from the routed
6
+ * (adminApp) tenant on `Astro.locals.tenant` — a real ToT-staff session may point
7
+ * `targetTenant` at another tenant it has access to (see lib/adminTenantEnvelope).
8
+ * Panels that need the target's real config (e.g. the FoxyCart cart-appearance
9
+ * panel, which derives from `theme_tokens` + `commerce.foxy`) resolve it HERE,
10
+ * in the config layer, rather than reaching into the static tenant map — so they
11
+ * stay behind the `tenancy-resolver-fence` (.dependency-cruiser.cjs) and a
12
+ * single-tenant extraction stays a drop-in (the concrete resolver is swapped, not
13
+ * the callers). Mirrors the config-layer `previewTenantLookup` seam.
14
+ *
15
+ * Resolution is by the tenant's canonical host: the envelope's `targetTenant` is
16
+ * an appDomain, which the registered tenants also claim in `domains`, so
17
+ * `resolveStrict` matches it and never falls back to a default (an id owning no
18
+ * tenant returns null — the panel then renders its inert "no store" state).
19
+ */
20
+ import type { TenantConfig } from "@tot/public-runtime";
21
+ import { getTenantResolver } from "@/config/resolver";
22
+
23
+ export async function resolveAdminTargetTenant(
24
+ targetTenant: string,
25
+ ): Promise<TenantConfig | null> {
26
+ const resolver = await getTenantResolver();
27
+ return resolver.resolveStrict(targetTenant);
28
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Dev-loop tenant synthesis (LOCAL dev only).
3
+ *
4
+ * The local dev loop (`tot dev` — scripts/tot-dev.mjs) serves a developer's OWN
5
+ * checked-out tenant by grafting the checkout into the colocated `tenants/<id>/`
6
+ * layout the app globs (content/, public/, theme.json). But the tenant RESOLVER
7
+ * only knows the hardcoded `staticTenants` registry — so an ARBITRARY invited
8
+ * developer's tenant (never in that list) 404s at `/<appDomain>/`, even though
9
+ * its content + theme are already on disk. That coupling is wrong for the dev
10
+ * loop: staticTenants is for the bundled demos, not for whoever runs `tot dev`.
11
+ *
12
+ * This synthesizes a MINIMAL, servable TenantConfig for each local tenant DIR
13
+ * that isn't already a static tenant, so the dev resolver seeds + resolves it.
14
+ * The tenant's identity IS its dir name (the colocation convention: dir ==
15
+ * tenant_id == appDomain == tot_data_scope). Content (content/home.html) and
16
+ * theme (theme.json) come straight from the checkout via the existing dir globs;
17
+ * catalog/compliance data it lacks DEGRADES to empty — siteType "marketing"
18
+ * means no catalog is required, and index.astro serves the raw home.html before
19
+ * any product resolution, so the tenant root renders instead of 404ing.
20
+ *
21
+ * DEV ONLY: in prod the TENANT_CACHE KV drives the resolver and staticTenants is
22
+ * the seed; this path is never taken. Kept inside `src/config` per the
23
+ * tenancy-resolver-fence — the concrete registry stays behind the config boundary.
24
+ */
25
+ import type { DeepPartial, TenantConfig, ThemeTokens } from "@tot/public-runtime";
26
+
27
+ /**
28
+ * A minimal servable TenantConfig whose identity is its colocated dir name.
29
+ * No Storyblok space (a "PLACEHOLDER" token → LocalContentProvider serves the
30
+ * checkout's content/), no catalog/compliance (siteType "marketing" degrades
31
+ * commerce to empty). Theme tokens come from the checkout's theme.json.
32
+ */
33
+ export function synthesizeDevTenant(
34
+ id: string,
35
+ themeTokens: DeepPartial<ThemeTokens>,
36
+ ): TenantConfig {
37
+ return {
38
+ tenant_id: id,
39
+ appDomain: id,
40
+ domains: [id],
41
+ canonicalDomain: id,
42
+ displayName: id,
43
+ // Contains "PLACEHOLDER" → createContentProvider picks LocalContentProvider,
44
+ // which serves the checkout's content/home.html (globbed by dir == this id).
45
+ storyblok_space_token: "STORYBLOK_DEV_TENANT_TOKEN_PLACEHOLDER",
46
+ tot_data_scope: id,
47
+ currency: "USD",
48
+ locale: "en-US",
49
+ theme_tokens: themeTokens,
50
+ // No bundled catalog for an arbitrary dev tenant → serve the raw marketing
51
+ // home and degrade commerce to empty rather than 404 the tenant root.
52
+ siteType: "marketing",
53
+ };
54
+ }
55
+
56
+ /**
57
+ * The dev tenants to seed ALONGSIDE staticTenants: one synthesized config per
58
+ * local tenant dir that isn't already a static tenant (those keep their richer
59
+ * hardcoded config). `themeFor` supplies each dir's theme tokens.
60
+ *
61
+ * @param localDirIds dir names discovered under `tenants/*` (dir == tenant id)
62
+ * @param staticIds tenant_ids already in the static registry (skipped)
63
+ * @param themeFor theme-token lookup by dir/tenant id
64
+ */
65
+ export function devTenantsToSeed(
66
+ localDirIds: Iterable<string>,
67
+ staticIds: ReadonlySet<string>,
68
+ themeFor: (id: string) => DeepPartial<ThemeTokens>,
69
+ ): TenantConfig[] {
70
+ const out: TenantConfig[] = [];
71
+ const seen = new Set<string>();
72
+ for (const id of localDirIds) {
73
+ if (!id || staticIds.has(id) || seen.has(id)) continue;
74
+ seen.add(id);
75
+ out.push(synthesizeDevTenant(id, themeFor(id)));
76
+ }
77
+ return out;
78
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Dev-only tenant-registry seam for the localhost sign-in bypass.
3
+ *
4
+ * `lib/auth/devAuth` can't import the concrete static tenant map directly
5
+ * (tenancy-resolver-fence — the registry stays behind the `config` boundary so a
6
+ * single-tenant extraction stays a drop-in). This exposes exactly the one thing
7
+ * that bypass needs: the resources a local dev session is granted membership on.
8
+ * In a single-tenant extraction this collapses to that one tenant with no change
9
+ * to devAuth. Same shape of config seam as {@link ./previewTenantLookup}.
10
+ */
11
+ import { staticTenants } from "@/config/tenants";
12
+
13
+ /** Every registered tenant's resource id (appDomain, falling back to tenant_id). */
14
+ export function devTenantResources(): string[] {
15
+ return staticTenants.map((t) => t.appDomain ?? t.tenant_id);
16
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * KV-backed tenant resolver — the many-tenant path documented in
3
+ * src/config/tenants.ts and wrangler.toml ([[kv_namespaces]] TENANT_CACHE).
4
+ *
5
+ * It implements the SAME `TenantResolver` interface as StaticTenantResolver, so
6
+ * middleware, pages, and the /<domain>/* path routing are untouched — only the
7
+ * backing store changes. Tenants are added by WRITING to KV (see putTenant),
8
+ * not by editing code, so onboarding/migration can register a store at runtime.
9
+ *
10
+ * KV layout:
11
+ * domain:<host> -> tenant_id (one per claimed hostname)
12
+ * tenant:<tenant_id> -> JSON TenantConfig
13
+ * config:default_tenant -> tenant_id (unknown-host fallback)
14
+ *
15
+ * A static `fallback` resolver is consulted when KV has no entry, so the system
16
+ * keeps working before KV is seeded and during a gradual cutover from static.
17
+ */
18
+ import type { TenantConfig, TenantResolver } from "@tot/public-runtime";
19
+
20
+ const DOMAIN_PREFIX = "domain:";
21
+ const TENANT_PREFIX = "tenant:";
22
+ const DEFAULT_KEY = "config:default_tenant";
23
+ // Edge-cache KV reads briefly; tenant config changes are rare and propagate
24
+ // within this window. Bump down if you need faster propagation after a write.
25
+ const CACHE_TTL = 300;
26
+
27
+ function cleanHost(host: string): string {
28
+ return host.toLowerCase().split(":")[0]?.trim() ?? "";
29
+ }
30
+
31
+ export class KvTenantResolver implements TenantResolver {
32
+ constructor(
33
+ private readonly kv: KVNamespace,
34
+ /** Consulted when KV has no record (pre-seed / gradual cutover). */
35
+ private readonly fallback?: TenantResolver,
36
+ ) {}
37
+
38
+ private async readTenant(id: string): Promise<TenantConfig | null> {
39
+ const raw = await this.kv.get(TENANT_PREFIX + id, { cacheTtl: CACHE_TTL });
40
+ if (!raw) return null;
41
+ try {
42
+ return JSON.parse(raw) as TenantConfig;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ async resolveStrict(host: string): Promise<TenantConfig | null> {
49
+ const clean = cleanHost(host);
50
+ if (!clean) return null;
51
+ const id = await this.kv.get(DOMAIN_PREFIX + clean, { cacheTtl: CACHE_TTL });
52
+ if (id) {
53
+ const tenant = await this.readTenant(id);
54
+ if (tenant) return tenant;
55
+ }
56
+ return this.fallback ? this.fallback.resolveStrict(host) : null;
57
+ }
58
+
59
+ async resolveByHost(host: string): Promise<TenantConfig | null> {
60
+ const strict = await this.resolveStrict(host);
61
+ if (strict) return strict;
62
+ const defaultId = await this.kv.get(DEFAULT_KEY, { cacheTtl: CACHE_TTL });
63
+ if (defaultId) {
64
+ const tenant = await this.readTenant(defaultId);
65
+ if (tenant) return tenant;
66
+ }
67
+ return this.fallback ? this.fallback.resolveByHost(host) : null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Register (or update) a tenant in KV. Idempotent: writes the config blob and a
73
+ * domain->id index entry for each claimed hostname. Pass `makeDefault` to point
74
+ * the unknown-host fallback at this tenant. Call this from onboarding/migration.
75
+ */
76
+ export async function putTenant(
77
+ kv: KVNamespace,
78
+ config: TenantConfig,
79
+ opts: { makeDefault?: boolean } = {},
80
+ ): Promise<void> {
81
+ await kv.put(TENANT_PREFIX + config.tenant_id, JSON.stringify(config));
82
+ for (const domain of config.domains) {
83
+ const host = cleanHost(domain);
84
+ if (host) await kv.put(DOMAIN_PREFIX + host, config.tenant_id);
85
+ }
86
+ if (opts.makeDefault) await kv.put(DEFAULT_KEY, config.tenant_id);
87
+ }