@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,102 @@
1
+ /**
2
+ * Tenant-resolver factory. Picks the backing store at request time:
3
+ *
4
+ * - TENANT_CACHE KV bound (prod / wired dev) -> KvTenantResolver over real KV.
5
+ * - No KV binding (plain `astro dev`) -> KvTenantResolver over an
6
+ * in-memory KV seeded from the static registry, so the dynamic path and a
7
+ * data-driven default are exercised locally without a Cloudflare account.
8
+ *
9
+ * Either way the caller gets a TenantResolver and nothing downstream changes.
10
+ * The static registry is passed as a fallback so unseeded hosts still resolve —
11
+ * and in DEV that fallback ALSO covers the local dev loop's own checked-out
12
+ * tenant (see devFallbackResolver below). Note the @astrojs/cloudflare dev
13
+ * runtime proxies a (local, empty) TENANT_CACHE KV from wrangler.toml, so plain
14
+ * `astro dev` usually takes the KV-bound branch and resolves static/dev tenants
15
+ * through this shared FALLBACK — not the seeded MemoryKv.
16
+ *
17
+ * The unknown-host default is data-driven: in prod it's the KV
18
+ * `config:default_tenant` key (set via putTenant({makeDefault:true})); in dev
19
+ * it's seeded from DEFAULT_TENANT_ID (falling back to the first static tenant).
20
+ * Changing that value flips which tenant a bare/unknown host serves — proof the
21
+ * default is configurable, not hardcoded.
22
+ */
23
+ import type { TenantResolver } from "@tot/public-runtime";
24
+ import { readEnv, readKv } from "@/lib/env";
25
+ import {
26
+ tenantResolver as staticResolver,
27
+ staticTenants,
28
+ StaticTenantResolver,
29
+ } from "@/config/tenants";
30
+ import { KvTenantResolver, putTenant } from "@/config/kvTenantResolver";
31
+ import { getTenantTheme } from "@/themes/tenants";
32
+ import { devTenantsToSeed } from "@/config/devTenantSeed";
33
+ import { MemoryKv } from "@/lib/memoryKv";
34
+
35
+ // LOCAL dev loop discovery: enumerate the colocated `tenants/<id>/` dirs on disk,
36
+ // INCLUDING a developer's checkout grafted in by `tot dev` (scripts/tot-dev.mjs).
37
+ // Keys only — the loaders are never invoked. Vite resolves this glob at
38
+ // dev-server boot (after the graft) and re-resolves on HMR, so a newly grafted
39
+ // tenant dir is discovered. Mirrors the content/theme dir globs the app already
40
+ // uses (provider.ts, themes/tenants/index.ts). Depth: from src/config/ the repo
41
+ // root is four levels up (config→src→storefront→apps→root).
42
+ const localTenantFiles = import.meta.glob([
43
+ "../../../../tenants/*/theme.json",
44
+ "../../../../tenants/*/content/home.html",
45
+ "../../../../tenants/*/content/home.json",
46
+ ]);
47
+
48
+ /** Dir names under `tenants/*` (dir == tenant id), from the discovery glob keys. */
49
+ function localTenantDirIds(): string[] {
50
+ const ids = new Set<string>();
51
+ for (const key of Object.keys(localTenantFiles)) {
52
+ const id = /\/tenants\/([^/]+)\//.exec(key)?.[1];
53
+ if (id) ids.add(id);
54
+ }
55
+ return [...ids];
56
+ }
57
+
58
+ /**
59
+ * The fallback resolver consulted on a KV miss. In PROD it's the plain static
60
+ * registry. In DEV it's the static registry EXTENDED with a synthesized,
61
+ * servable tenant for each local `tenants/<id>/` dir that isn't already static —
62
+ * so a developer's OWN checked-out tenant (grafted by `tot dev`, never in
63
+ * staticTenants) resolves at `/<appDomain>/` from its checkout instead of
64
+ * 404ing. Static tenants keep their richer hardcoded config (they're skipped by
65
+ * devTenantsToSeed). Used as the fallback in BOTH resolver branches, so it works
66
+ * whether or not the dev runtime binds a local TENANT_CACHE KV.
67
+ */
68
+ function devFallbackResolver(): TenantResolver {
69
+ if (!import.meta.env.DEV) return staticResolver;
70
+ const staticIds = new Set(staticTenants.map((t) => t.tenant_id));
71
+ const dev = devTenantsToSeed(localTenantDirIds(), staticIds, getTenantTheme);
72
+ if (dev.length === 0) return staticResolver;
73
+ // registry[0] stays the static default (HOME_TENANT) for unknown-host fallback.
74
+ return new StaticTenantResolver([...staticTenants, ...dev]);
75
+ }
76
+
77
+ let devKvPromise: Promise<KVNamespace> | null = null;
78
+
79
+ /** Build (once) an in-memory KV seeded from the static registry for dev. */
80
+ function devKv(): Promise<KVNamespace> {
81
+ if (!devKvPromise) {
82
+ devKvPromise = (async () => {
83
+ const kv = new MemoryKv();
84
+ const configured = (await readEnv("DEFAULT_TENANT_ID"))?.trim();
85
+ const defaultId = configured || staticTenants[0]?.tenant_id;
86
+ for (const tenant of staticTenants) {
87
+ await putTenant(kv, tenant, { makeDefault: tenant.tenant_id === defaultId });
88
+ }
89
+ return kv;
90
+ })();
91
+ }
92
+ return devKvPromise;
93
+ }
94
+
95
+ export async function getTenantResolver(): Promise<TenantResolver> {
96
+ // Synthesized dev tenants live in the shared fallback, so both branches resolve
97
+ // the dev loop's checked-out tenant on a KV miss (prod fallback is unchanged).
98
+ const fallback = devFallbackResolver();
99
+ const kv = await readKv("TENANT_CACHE");
100
+ if (kv) return new KvTenantResolver(kv, fallback);
101
+ return new KvTenantResolver(await devKv(), fallback);
102
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Static tenant registry — SANITIZED for the world-shareable storefront runner.
3
+ *
4
+ * This file is REWRITTEN at build time (scripts/build/build-runner.mjs) so the
5
+ * public npm runner ships NO real merchant roster. It carries only a neutral
6
+ * "home" default and one GENERIC sample tenant (with a compliance block, so the
7
+ * age-gate / PACT affordance is demonstrable off a non-merchant identity).
8
+ *
9
+ * At runtime the dev loop's checkout-glob resolver (config/resolver.ts) synthesizes
10
+ * a servable tenant for whatever the developer grafts into tenants/<id>/, so the
11
+ * real staticTenants are not needed here. The public API (staticTenants,
12
+ * StaticTenantResolver, tenantResolver) is unchanged — only the data is generic.
13
+ */
14
+ import type { TenantConfig, TenantResolver } from "@tot/public-runtime";
15
+ import { getTenantTheme } from "@/themes/tenants";
16
+
17
+ // Default landing tenant — the fallback for unmatched hosts / bare localhost.
18
+ // Non-commerce "coming soon" placeholder; never a fake merchant. Registered first
19
+ // so it is the resolver default (registry[0]).
20
+ export const HOME_TENANT: TenantConfig = {
21
+ tenant_id: "home",
22
+ appDomain: "home",
23
+ // NB: do NOT claim "localhost" here — a bare localhost host must fall THROUGH to
24
+ // path-prefix routing (/<domain>/…) so grafted dev tenants + the sample resolve.
25
+ // Unknown/bare hosts still land on HOME via resolveByHost (registry[0]).
26
+ domains: ["home"],
27
+ canonicalDomain: "home",
28
+ displayName: "Storefront",
29
+ storyblok_space_token: "STORYBLOK_HOME_DELIVERY_TOKEN_PLACEHOLDER",
30
+ tot_data_scope: "home",
31
+ currency: "USD",
32
+ locale: "en-US",
33
+ theme_tokens: getTenantTheme("home"),
34
+ siteType: "marketing",
35
+ };
36
+
37
+ // Generic SAMPLE tenant — a placeholder regulated store used to demonstrate the
38
+ // compliance affordance (age-gate + nicotine warning) WITHOUT any real merchant.
39
+ // Routed at /sample-store.example/. Content comes from the developer's grafted
40
+ // checkout (tenants/sample-store.example/); with none present it degrades to the
41
+ // default scope, and the compliance overlay still renders from this config.
42
+ export const SAMPLE_TENANT: TenantConfig = {
43
+ tenant_id: "sample-store.example",
44
+ appDomain: "sample-store.example",
45
+ domains: ["sample-store.example"],
46
+ canonicalDomain: "sample-store.example",
47
+ displayName: "Sample Store",
48
+ storyblok_space_token: "STORYBLOK_SAMPLE_DELIVERY_TOKEN_PLACEHOLDER",
49
+ tot_data_scope: "sample-store.example",
50
+ currency: "USD",
51
+ locale: "en-US",
52
+ theme_tokens: getTenantTheme("sample-store.example"),
53
+ // Regulated-store demo: shows the 21+/PACT compliance gate is CONFIG-driven.
54
+ compliance: {
55
+ minAge: 21,
56
+ nicotineWarning: true,
57
+ shippingRestriction:
58
+ "Adult signature (21+) required on delivery. Some destinations are restricted — verified at checkout.",
59
+ },
60
+ siteType: "marketing",
61
+ };
62
+
63
+ const REGISTRY: TenantConfig[] = [HOME_TENANT, SAMPLE_TENANT];
64
+
65
+ /** The built-in tenants — seed source for the KV store (dev MemoryKv / prod). */
66
+ export const staticTenants: TenantConfig[] = REGISTRY;
67
+
68
+ export class StaticTenantResolver implements TenantResolver {
69
+ constructor(private readonly registry: TenantConfig[] = REGISTRY) {}
70
+
71
+ /** Strict domain match — null when no tenant explicitly claims `host`. */
72
+ async resolveStrict(host: string): Promise<TenantConfig | null> {
73
+ const clean = host.toLowerCase().split(":")[0]?.trim() ?? "";
74
+ if (!clean) return null;
75
+ return (
76
+ this.registry.find((t) => t.domains.some((d) => d.toLowerCase() === clean)) ?? null
77
+ );
78
+ }
79
+
80
+ async resolveByHost(host: string): Promise<TenantConfig | null> {
81
+ const match = await this.resolveStrict(host);
82
+ // Unmatched hosts / bare localhost fall back to the default HOME_TENANT.
83
+ return match ?? this.registry[0] ?? null;
84
+ }
85
+ }
86
+
87
+ export const tenantResolver: TenantResolver = new StaticTenantResolver();
@@ -0,0 +1,368 @@
1
+ ---
2
+ /**
3
+ * Root layout. Injects per-tenant theme CSS vars at the edge, loads the font
4
+ * pairing, mounts header + footer chrome, and wires privacy-friendly analytics.
5
+ * Every page renders inside this.
6
+ */
7
+ import "@/styles/global.css";
8
+ import Seo from "@/components/Seo.astro";
9
+ import AnnouncementBar from "@/components/chrome/AnnouncementBar.astro";
10
+ import SiteHeader from "@/components/chrome/SiteHeader.astro";
11
+ import SiteFooter from "@/components/chrome/SiteFooter.astro";
12
+ import QuickView from "@/components/islands/QuickView.tsx";
13
+ import FoxyLoader from "@/components/commerce/FoxyLoader.astro";
14
+ import FoxySidecartStyles from "@/components/commerce/FoxySidecartStyles.astro";
15
+ import NicotineWarning from "@/components/compliance/NicotineWarning.astro";
16
+ import ShippingRestrictionNotice from "@/components/compliance/ShippingRestrictionNotice.astro";
17
+ import IsolatedAgeGate from "@/components/islands/IsolatedAgeGate.tsx";
18
+ import AuthBadge from "@/components/auth/AuthBadge.astro";
19
+ import { referenceTheme } from "@/themes/reference";
20
+ import { mergeTheme, themeFontHref } from "@/themes/schema";
21
+ import { organizationLd, websiteLd } from "@/lib/jsonld";
22
+ import { nonceInlineTags, CHROME_BODY_MARKER } from "@/lib/rawMarketingHtml";
23
+ import { readViewerSession } from "@/lib/auth/route";
24
+
25
+ interface Props {
26
+ title: string;
27
+ description?: string;
28
+ path: string;
29
+ ogType?: "website" | "product" | "article";
30
+ ogImage?: string;
31
+ noindex?: boolean;
32
+ jsonLd?: Record<string, unknown> | Record<string, unknown>[];
33
+ /** Wider pages (PLP/home) drop the prose max-width. */
34
+ fullBleed?: boolean;
35
+ }
36
+
37
+ const { title, description, path, ogType, ogImage, noindex, jsonLd } =
38
+ Astro.props;
39
+ const { tenant, themeStyle, canonicalBase, basePath, cspNonce, features, capabilities } =
40
+ Astro.locals;
41
+
42
+ // Organization JSON-LD is emitted site-wide (brief §8); pages add their own
43
+ // graphs (Product, BreadcrumbList…) via the jsonLd prop.
44
+ const pageGraphs = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : [];
45
+ const allJsonLd = [
46
+ organizationLd(tenant, canonicalBase),
47
+ websiteLd(tenant, canonicalBase),
48
+ ...pageGraphs,
49
+ ];
50
+
51
+ const theme = mergeTheme(referenceTheme, tenant.theme_tokens);
52
+ const fontHref = themeFontHref(theme);
53
+
54
+ const chrome = await Astro.locals.content.getChrome();
55
+
56
+ // SHARED CHROME: if the tenant ships content/<id>/chrome.html (the pixel-perfect
57
+ // header/footer the raw marketing pages use), render THAT chrome around the
58
+ // Layout content too — so commerce pages (collections/products/search/…) share
59
+ // ONE chrome instead of the generic Header/Footer. Split the wrapper at
60
+ // <!--PAGE_BODY-->: everything before = header, after = footer; pull its tenant
61
+ // stylesheet links into <head>; nonce its inline tags for CSP. Tenants without a
62
+ // chrome.html keep the token-driven Header/Footer (below).
63
+ const chromeHtml = await Astro.locals.content.getChromeHtml();
64
+ let sharedChrome: { header: string; footer: string; css: string[] } | null = null;
65
+ if (chromeHtml && chromeHtml.includes(CHROME_BODY_MARKER)) {
66
+ const headEnd = chromeHtml.indexOf("</head>");
67
+ const head = headEnd >= 0 ? chromeHtml.slice(0, headEnd) : "";
68
+ const css = [...head.matchAll(/<link[^>]+href="([^"]+\.css)"[^>]*>/gi)]
69
+ .map((m) => m[1] ?? "")
70
+ .filter((h) => h.startsWith("/tenants/") || h.startsWith("/shared/"));
71
+ const bodyOpen = chromeHtml.indexOf(">", chromeHtml.indexOf("<body")) + 1;
72
+ const marker = chromeHtml.indexOf(CHROME_BODY_MARKER);
73
+ const bodyEnd = chromeHtml.lastIndexOf("</body>");
74
+ sharedChrome = {
75
+ header: nonceInlineTags(chromeHtml.slice(bodyOpen, marker), cspNonce),
76
+ footer: nonceInlineTags(chromeHtml.slice(marker + CHROME_BODY_MARKER.length, bodyEnd >= 0 ? bodyEnd : undefined), cspNonce),
77
+ css,
78
+ };
79
+ }
80
+
81
+ const compliance = tenant.compliance;
82
+
83
+ // Site-chrome data resolution (unit I4 — chrome/* is the canonical, prop-driven
84
+ // component set; this Layout is the ONE place that resolves session/feature/
85
+ // theme state into their props. Skipped entirely when sharedChrome (a raw
86
+ // content/<id>/chrome.html override) is present — that path wins outright.
87
+ //
88
+ // Auth entry point: "Dashboard" when a session exists, "Sign in" otherwise.
89
+ // Without this there's no discoverable path from the storefront into sign-in /
90
+ // the vendor dashboard (#8425). On login-gated hosts the middleware already
91
+ // resolved the viewer (locals.viewer); reuse it and only read the session KV on
92
+ // open hosts where the gate never ran. Signed-out "Sign in" carries the current
93
+ // path as `next`, so after the email-code round-trip the visitor lands back
94
+ // where they were instead of the site root.
95
+ const viewer = Astro.locals.viewer ?? (await readViewerSession(Astro));
96
+ const nextParam = encodeURIComponent(Astro.url.pathname + Astro.url.search);
97
+ const authLink = {
98
+ label: viewer ? "Dashboard" : "Sign in",
99
+ href: viewer ? "/dashboard" : `/auth/login?next=${nextParam}`,
100
+ };
101
+
102
+ // Per-vendor header treatment from the theme file: a "dark" header renders the
103
+ // bar in the primary color carrying the light logo (e.g. Giant Vapes' navy +
104
+ // white logo); a light header always shows the text wordmark (no logo image).
105
+ const headerDark = theme.brand.headerVariant === "dark";
106
+ const headerLogoSrc = headerDark ? theme.brand.logoLight : theme.brand.logoDark;
107
+ const headerLogo =
108
+ headerDark && headerLogoSrc && /^(https?:|\/)/.test(headerLogoSrc)
109
+ ? { src: headerLogoSrc, alt: tenant.displayName }
110
+ : undefined;
111
+
112
+ // Footer bottom bar: a "Legal"-titled column's links win; otherwise every
113
+ // column's links flatten into the policy-link row (mirrors the retired
114
+ // nav/Footer.astro exactly).
115
+ const legalCol = chrome.footer.columns.find((c) => /legal/i.test(c.title));
116
+ const bottomLinks = legalCol ? legalCol.links : chrome.footer.columns.flatMap((c) => c.links);
117
+ const legalLine = `© ${new Date().getFullYear()} ${tenant.displayName}${
118
+ chrome.footer.legal ? ` · ${chrome.footer.legal}` : ""
119
+ }`;
120
+
121
+ const analyticsToken = import.meta.env.PUBLIC_CF_ANALYTICS_TOKEN;
122
+ const analyticsEnabled =
123
+ analyticsToken && !/REPLACE|PLACEHOLDER/i.test(analyticsToken);
124
+ ---
125
+
126
+ <!doctype html>
127
+ <html lang={tenant.locale.split("-")[0] ?? "en"} data-base-path={basePath || undefined}>
128
+ <head>
129
+ <meta charset="utf-8" />
130
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
131
+ <link rel="icon" type="image/svg+xml" href={theme.brand.favicon} />
132
+
133
+ {/* Per-tenant theme tokens injected at the edge (brief §7). */}
134
+ <style set:html={themeStyle} nonce={cspNonce}></style>
135
+
136
+ {/* Shared chrome's own stylesheet(s) — the tenant theme (/tenants/...) plus
137
+ either the tenant's own mkt.css or the platform /shared/commerce-chrome.css
138
+ — so the ported chrome renders in-brand on Layout (commerce) pages too. */}
139
+ {sharedChrome?.css.map((href) => <link rel="stylesheet" href={href} />)}
140
+
141
+ {
142
+ fontHref && (
143
+ <>
144
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
145
+ <link
146
+ rel="preconnect"
147
+ href="https://fonts.gstatic.com"
148
+ crossorigin
149
+ />
150
+ <link rel="stylesheet" href={fontHref} />
151
+ </>
152
+ )
153
+ }
154
+
155
+ <Seo
156
+ title={title}
157
+ description={description}
158
+ path={path}
159
+ ogType={ogType}
160
+ ogImage={ogImage}
161
+ noindex={noindex}
162
+ jsonLd={allJsonLd}
163
+ />
164
+
165
+ {
166
+ analyticsEnabled && (
167
+ <script
168
+ defer
169
+ src="https://static.cloudflareinsights.com/beacon.min.js"
170
+ data-cf-beacon={`{"token": "${analyticsToken}"}`}
171
+ />
172
+ )
173
+ }
174
+ </head>
175
+ <body>
176
+ <a
177
+ href="#main"
178
+ class="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:m-3 focus:rounded-md focus:bg-[var(--color-primary)] focus:px-4 focus:py-2 focus:text-[var(--color-primary-contrast)]"
179
+ >
180
+ Skip to content
181
+ </a>
182
+
183
+ <AuthBadge />
184
+
185
+ {sharedChrome ? (
186
+ <Fragment set:html={sharedChrome.header} />
187
+ ) : (
188
+ <>
189
+ {compliance?.nicotineWarning && <NicotineWarning variant="bar" />}
190
+ <div class="sticky top-0 z-40">
191
+ {chrome.announcement && <AnnouncementBar text={chrome.announcement} variant="static" />}
192
+ <SiteHeader
193
+ logo={headerLogo}
194
+ wordmark={tenant.displayName}
195
+ nav={chrome.nav}
196
+ variant={headerDark ? "dark" : "light"}
197
+ announcement={chrome.announcement}
198
+ searchAction={features.productSearch ? "/search" : undefined}
199
+ savedItemsHref={features.savedItems ? "/saved" : undefined}
200
+ headerCtas={chrome.headerCtas}
201
+ authLink={authLink}
202
+ sticky={false}
203
+ />
204
+ </div>
205
+ </>
206
+ )}
207
+
208
+ <main id="main">
209
+ <slot />
210
+ </main>
211
+
212
+ {sharedChrome ? (
213
+ <Fragment set:html={sharedChrome.footer} />
214
+ ) : (
215
+ <>
216
+ <ShippingRestrictionNotice text={compliance?.shippingRestriction} />
217
+ <SiteFooter
218
+ brand={{
219
+ wordmark: tenant.displayName,
220
+ contact: chrome.footer.tagline ? [chrome.footer.tagline] : undefined,
221
+ }}
222
+ columns={chrome.footer.columns}
223
+ newsletter={features.newsletter ? { title: "Newsletter", placeholder: "you@example.com" } : undefined}
224
+ social={chrome.footer.social}
225
+ legal={legalLine}
226
+ bottomLinks={bottomLinks}
227
+ />
228
+ </>
229
+ )}
230
+
231
+ {capabilities.ageVerification.enabled && compliance?.minAge && (
232
+ <IsolatedAgeGate client:load minAge={compliance.minAge} brand={tenant.displayName} />
233
+ )}
234
+
235
+ {/* Global quick-view dialog (opened by any [data-quickview] card button).
236
+ Commerce-only — marketing tenants ship no product UI. */}
237
+ {features.quickView && <QuickView client:idle locale={tenant.locale} />}
238
+
239
+ {/* Excise tax (Track B) — when enabled for this environment, the excise line
240
+ is calculated + added inside the FoxyCart cart (tot-foxycart pre-cart
241
+ webhook). This is the storefront's honest shopper-facing signal of it. */}
242
+ {capabilities.exciseTax.enabled && features.catalog && (
243
+ <p
244
+ class="border-t border-[var(--color-border)] bg-[var(--color-bg)] py-2.5 text-center text-xs text-[var(--color-muted)]"
245
+ data-capability="excise-tax"
246
+ >
247
+ Applicable excise tax is calculated at checkout.
248
+ </p>
249
+ )}
250
+
251
+ {/* FoxyCart loader (Powered by FoxyCart) — turns add-to-cart forms into the
252
+ sidecart + hosted checkout. Only for commerce tenants with a store. */}
253
+ <FoxyLoader />
254
+
255
+ {/* Brand Foxy's in-page sidecart to the tenant theme (baseline + optional
256
+ per-tenant commerce.foxy.sidecartCss override). Commerce tenants only. */}
257
+ <FoxySidecartStyles />
258
+
259
+ {/* Wishlist hearts: event-delegated toggle + state hydration + count sync.
260
+ Cards/PDP carry [data-wishlist]; the header shows [data-wishlist-count].
261
+ Kept as one tiny script (no per-card hydration). Commerce-only — a
262
+ marketing tenant has no saved-items surface, so this never renders. */}
263
+ {features.savedItems && (
264
+ <script>
265
+ import {
266
+ readWishlist,
267
+ toggleSaved,
268
+ isSaved,
269
+ WISHLIST_EVENT,
270
+ } from "@/lib/wishlist";
271
+
272
+ function syncCount() {
273
+ const n = readWishlist().length;
274
+ document.querySelectorAll<HTMLElement>("[data-wishlist-count]").forEach((el) => {
275
+ el.textContent = n > 0 ? String(n) : "";
276
+ el.toggleAttribute("hidden", n === 0);
277
+ });
278
+ }
279
+
280
+ function syncHearts() {
281
+ document.querySelectorAll<HTMLButtonElement>(".wishlist-toggle").forEach((btn) => {
282
+ try {
283
+ const { handle } = JSON.parse(btn.getAttribute("data-wishlist") || "{}");
284
+ if (handle) btn.setAttribute("aria-pressed", String(isSaved(handle)));
285
+ } catch {}
286
+ });
287
+ }
288
+
289
+ document.addEventListener("click", (e) => {
290
+ const btn = (e.target as HTMLElement)?.closest?.<HTMLButtonElement>(".wishlist-toggle");
291
+ if (!btn) return;
292
+ e.preventDefault();
293
+ try {
294
+ const item = JSON.parse(btn.getAttribute("data-wishlist") || "{}");
295
+ if (!item.handle) return;
296
+ const saved = toggleSaved(item);
297
+ btn.setAttribute("aria-pressed", String(saved));
298
+ } catch {}
299
+ });
300
+
301
+ window.addEventListener(WISHLIST_EVENT, syncCount);
302
+ syncHearts();
303
+ syncCount();
304
+ </script>
305
+ )}
306
+
307
+ {/* Path-prefix tenant routing: when this request was selected via a
308
+ /<domain>/ path segment (locals.basePath), keep same-origin
309
+ root-absolute links inside that prefix so navigation stays on the
310
+ tenant. Host-routed requests have no base path and skip this entirely. */}
311
+ {basePath && (
312
+ <script is:inline nonce={cspNonce} define:vars={{ basePath }}>
313
+ (function () {
314
+ var base = basePath;
315
+ if (!base) return;
316
+ function fix(a) {
317
+ var href = a.getAttribute("href");
318
+ if (!href || href.charAt(0) !== "/" || href.charAt(1) === "/") return;
319
+ if (href === base || href.indexOf(base + "/") === 0) return;
320
+ a.setAttribute("href", base + href);
321
+ }
322
+ function fixAll() {
323
+ document.querySelectorAll('a[href^="/"]').forEach(fix);
324
+ }
325
+ if (document.readyState === "loading") {
326
+ document.addEventListener("DOMContentLoaded", fixAll);
327
+ } else {
328
+ fixAll();
329
+ }
330
+ // Late/island-rendered anchors: fix just-in-time before navigation.
331
+ document.addEventListener(
332
+ "pointerdown",
333
+ function (e) {
334
+ var a = e.target && e.target.closest && e.target.closest('a[href^="/"]');
335
+ if (a) fix(a);
336
+ },
337
+ true,
338
+ );
339
+ })();
340
+ </script>
341
+ )}
342
+
343
+ {/* Scroll-reveal + analytics pageview hook. ~0.5KB, no framework. */}
344
+ <script>
345
+ // Reveal-on-scroll (respects reduced motion via CSS).
346
+ const els = document.querySelectorAll("[data-reveal]");
347
+ if (
348
+ els.length &&
349
+ !window.matchMedia("(prefers-reduced-motion: reduce)").matches
350
+ ) {
351
+ const io = new IntersectionObserver(
352
+ (entries) => {
353
+ for (const e of entries) {
354
+ if (e.isIntersecting) {
355
+ e.target.classList.add("is-in");
356
+ io.unobserve(e.target);
357
+ }
358
+ }
359
+ },
360
+ { rootMargin: "0px 0px -8% 0px", threshold: 0.05 },
361
+ );
362
+ els.forEach((el) => io.observe(el));
363
+ } else {
364
+ els.forEach((el) => el.classList.add("is-in"));
365
+ }
366
+ </script>
367
+ </body>
368
+ </html>
@@ -0,0 +1,122 @@
1
+ # `lib/admin` — admin data/view + AI/evidence contracts
2
+
3
+ Read-oriented view models and deterministic mock services for the 7 admin
4
+ domains (orders, products, customers, fulfillment, reporting, settings,
5
+ ai-workflows), plus the AI/evidence substrate (`ai/`, `evidence/`) those
6
+ domains' assist surfaces build on. See
7
+ `docs/handoffs/admin-app-implementation-baseline.md` for the product spec
8
+ these implement.
9
+
10
+ ## Tenant envelope
11
+
12
+ The canonical tenant envelope is `AdminTenantEnvelope`, defined in
13
+ `lib/adminTenantEnvelope.ts` (admin-tenant-mode) and re-exported from
14
+ `lib/admin/envelope.ts` so everything under `lib/admin/**` can import it from
15
+ a stable relative path:
16
+
17
+ ```ts
18
+ interface AdminTenantEnvelope {
19
+ adminAppTenant: string; // the tenant that actually served this request
20
+ targetTenant: string; // the merchant tenant this page's data/actions are scoped to
21
+ operator: string | null; // the signed-in viewer's email, or null when anonymous
22
+ }
23
+ ```
24
+
25
+ For merchant-facing admin `adminAppTenant` and `targetTenant` are equal; only
26
+ a real ToT-staff session may move `targetTenant` away from the routed tenant
27
+ (see `resolveAdminTenantMode` for the authorization rule — deny-by-default,
28
+ refused not hidden-only). `targetTenant` is the sole key mock services use to
29
+ select/derive data. **Authorization must always be evaluated against
30
+ `targetTenant`, never `adminAppTenant`** (decision `tenant-envelope-contract`).
31
+
32
+ (`lib/admin/envelope.ts` used to declare its own 2-field copy of this
33
+ interface — data-view-contracts and admin-tenant-mode were built in parallel
34
+ per that decision, with no live coordination. Reconciled here per decision
35
+ `envelope-type-duplication-follow-up`: one canonical type, re-exported.)
36
+
37
+ ## Layout
38
+
39
+ ```
40
+ lib/admin/
41
+ envelope.ts # re-exports the canonical AdminTenantEnvelope
42
+ seed.ts # deterministic PRNG + timestamp helpers
43
+ viewmodels/<domain>.ts # types only, no I/O
44
+ services/mock/<domain>.ts # getXViewModel(envelope) -> X ViewModel
45
+ services/mock/evidence.ts # getEvidenceViewModel(envelope) -> EvidenceReference[]
46
+ services/mock/tenants.ts # seeded tenant registry + fallback profile
47
+ services/mock/index.ts # barrel re-export
48
+ ai/
49
+ approvalMode.ts # canonical ApprovalMode + isDraftOnly/requiresHumanApproval/isAutomationAllowed
50
+ audit.ts # canonical AuditEvent (operator + targetTenant + action + at)
51
+ recommendation.ts # AiRecommendation — the recommendation-shaped record every AI suggestion should use
52
+ askEnvelope.ts # AskEnvelope + buildAskEnvelope() — the floating Ask button's context
53
+ evidence/
54
+ evidenceReference.ts # EvidenceReference + EvidenceKind — the evidence graph's typed pointer
55
+ ```
56
+
57
+ ## AI / evidence substrate
58
+
59
+ - **Approval state machine** (`ai/approvalMode.ts`): `draft_only` |
60
+ `approval_required` | `guarded_automation`. `settings.ts` and
61
+ `aiWorkflows.ts` both import this rather than declaring their own copy.
62
+ - **Audit events** (`ai/audit.ts`): `{ operator, targetTenant, action, at }`.
63
+ `SettingsAuditEvent` extends it with a human-readable `change` string;
64
+ `WorkflowRecord.auditTrail` uses it directly.
65
+ - **Recommendations** (`ai/recommendation.ts`): `AiRecommendation` is the
66
+ shape every AI-suggested action should carry — `summary`, `confidence`
67
+ (0–1), `evidenceRefs` (ids into the evidence graph), `owner`,
68
+ `approvalMode`, `auditTrail`. `WorkflowRecord.suggestedAction` uses this
69
+ today; later per-tab assist surfaces (Order Assist, Fulfillment Assist,
70
+ etc.) should reuse it instead of inventing a parallel "suggestion" shape.
71
+ - **Evidence graph** (`evidence/evidenceReference.ts` +
72
+ `services/mock/evidence.ts`): a typed, tenant-scoped pointer
73
+ (`EvidenceReference`) connecting orders, customers, products, payments,
74
+ verification events, fulfillment/tracking, support/contact events, policy
75
+ acceptance, tax/excise evidence, and admin/user actions
76
+ (`EvidenceKind`). The mock service builds real edges by cross-referencing
77
+ the orders/customers/products mock services already in this directory
78
+ (an order's evidence ref links to its customer and product refs via
79
+ `relatedRefs`) rather than inventing disconnected fixture data.
80
+ - **Ask envelope** (`ai/askEnvelope.ts`): `buildAskEnvelope()` is the pure,
81
+ testable source of truth for what the floating Ask button emits — the full
82
+ tenant envelope, active tab, selected record id (if any), role +
83
+ permissions (derived from `isStaff`), active filters, and evidence context.
84
+ `admin.astro`'s `is:inline` script mirrors this logic in plain JS (inline
85
+ scripts can't import modules) and emits it on Ask-button click as
86
+ `window.__toTAdminAsk` and a `tot:admin-ask` `CustomEvent` — **dev-inspectable
87
+ only, no chat backend yet.** The envelope changes whenever the active tab
88
+ changes (kept current by `setAskContext`) and is rebuilt fresh on click so a
89
+ selection made via `data-admin-selected-id` (a hook future tab units can set
90
+ on their selected row/detail panel — none do yet) is picked up.
91
+
92
+ ## Determinism
93
+
94
+ Every mock service seeds its randomness from `seededRandom(`<domain>:${targetTenant}`)`
95
+ (`seed.ts`), so **the same `targetTenant` always returns the same data** within
96
+ a process — no reliance on `Date.now()` or other wall-clock state. Two tenants
97
+ are curated with vertical-flavored data (`northridge-vapor.com` — vape,
98
+ `cascade-tobacco.com` — tobacco); any other `targetTenant` string still gets a
99
+ stable fallback profile via `tenantProfile()`, so callers never need to
100
+ special-case an "unknown" tenant.
101
+
102
+ ## Usage
103
+
104
+ ```ts
105
+ import { getOrdersViewModel } from "@/lib/admin/services/mock";
106
+
107
+ const orders = getOrdersViewModel({
108
+ adminAppTenant: tenant.tenant_id,
109
+ targetTenant: tenant.tenant_id,
110
+ operator: viewerEmail,
111
+ });
112
+ ```
113
+
114
+ ## Boundaries
115
+
116
+ - **Read-only.** No write/mutation paths exist yet — that is a later unit.
117
+ - **Tabs/pages → lib, never lib → tabs/pages.** Nothing under `lib/admin/**`
118
+ imports from `components/admin/**` or `pages/admin.astro`. Tab components
119
+ and `admin.astro` are free to import from here (`admin.astro` already does,
120
+ for `resolveAdminTenantMode` and the Ask-envelope logic it mirrors inline).
121
+ - Gaps in these contracts should go back to the workstream board as a
122
+ decision, not get patched from a tab's unit.