@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,384 @@
1
+ /**
2
+ * Tenant configuration + theme token contract.
3
+ *
4
+ * Phase 1 resolves tenants from a static config map/KV by hostname. The lookup
5
+ * interface (resolveTenant) is intentionally async + many-ready so the same seam
6
+ * scales to Cloudflare-for-SaaS custom hostnames later without a rewrite.
7
+ */
8
+ import type { FoxyCommerceConfig } from "./foxy.js";
9
+ import type { CapabilityConfig } from "./capabilities.js";
10
+
11
+ /**
12
+ * The shared semantic COLOR contract — the widget library's public API.
13
+ *
14
+ * Every platform component reads ONLY these tokens (as `var(--color-<key>)`,
15
+ * bridged into Tailwind by `styles/global.css` `@theme`). A tenant restyles the
16
+ * whole library by supplying token values — or aliasing its own brand vocabulary
17
+ * onto them — and NEVER edits a component. `themeToCssVars()` serializes each
18
+ * key `k` to `--color-<k>`.
19
+ *
20
+ * The REQUIRED keys are the core palette every theme states. The OPTIONAL keys
21
+ * are semantic/state + interaction extensions with platform-default fallbacks in
22
+ * the `@theme` bridge, so `var(--color-warning)`, `var(--color-primary-hover)`,
23
+ * etc. always resolve even when a theme omits them; a theme sets one only to
24
+ * brand-tune it. Canonical reference values live in `themes/reference.ts`.
25
+ * Full spec + tenant-alias guidance: `docs/architecture/token-contract.md`.
26
+ */
27
+ export interface ThemeColorTokens {
28
+ /** Page background. */
29
+ bg: string;
30
+ /** Raised card/panel surface. */
31
+ surface: string;
32
+ /** Primary body + heading text. */
33
+ text: string;
34
+ /** Secondary / caption text. */
35
+ muted: string;
36
+ /** Brand anchor — primary CTAs, active nav, hero fill. */
37
+ primary: string;
38
+ /** Readable text/icon color ON `primary`. */
39
+ "primary-contrast": string;
40
+ /** Single brand spark — badges, marks, large accent type. */
41
+ accent: string;
42
+ /** Hairline borders, dividers, input outlines. */
43
+ border: string;
44
+ /** Success / in-stock / savings state. */
45
+ success: string;
46
+ /** Sale / markdown price emphasis. */
47
+ sale: string;
48
+
49
+ // --- Optional extensions — always resolvable via the @theme bridge default;
50
+ // a theme sets one only to brand-tune it (see token-contract.md). ---
51
+ /** Subtle inset/raised fill between `bg` and `surface` (wells, table stripes). */
52
+ "surface-2"?: string;
53
+ /** Warning / low-stock state. */
54
+ warning?: string;
55
+ /** Error / out-of-stock / destructive state. */
56
+ error?: string;
57
+ /** Informational state (notices, help). */
58
+ info?: string;
59
+ /** Accent tuned for SMALL text/links on light surfaces (AA). Default: a
60
+ * derived darken of `accent`; set to hand-tune contrast. */
61
+ "accent-ink"?: string;
62
+ /** Hover/active shade of `primary`. Default: a derived darken of `primary`. */
63
+ "primary-hover"?: string;
64
+ /** Hover/active shade of `accent`. Default: a derived darken of `accent`. */
65
+ "accent-hover"?: string;
66
+ }
67
+
68
+ export interface ThemeFontFamily {
69
+ family: string;
70
+ weights: number[];
71
+ }
72
+
73
+ export interface ThemeTypeTokens {
74
+ display: ThemeFontFamily;
75
+ body: ThemeFontFamily;
76
+ mono?: ThemeFontFamily;
77
+ /** Type scale ratio, e.g. 1.25 (major third). */
78
+ scale: number;
79
+ tracking?: string;
80
+ }
81
+
82
+ export interface ThemeSpaceTokens {
83
+ /** Base spacing unit in px. */
84
+ base: number;
85
+ /** Multiplier scale, e.g. [0, 0.25, 0.5, 1, 1.5, 2, 3, 4, 6, 8]. */
86
+ scale: number[];
87
+ containerMaxWidth: string;
88
+ sectionRhythm: string;
89
+ }
90
+
91
+ export interface ThemeShapeTokens {
92
+ radius: { sm: string; md: string; lg: string; full: string };
93
+ borderWeights: { thin: string; medium: string; thick: string };
94
+ shadow: { sm: string; md: string; lg: string };
95
+ }
96
+
97
+ export interface ThemeBrandTokens {
98
+ logoLight: string;
99
+ logoDark: string;
100
+ favicon: string;
101
+ ogImage: string;
102
+ /** "full" | "reduced" — tenant-level default motion preference. */
103
+ motion: "full" | "reduced";
104
+ /**
105
+ * Header treatment. "dark" renders the bar in the primary color carrying the
106
+ * light logo (matches brands whose header is dark + white logo, e.g. vape
107
+ * stores); "light" (default) is a light bar with the dark logo/wordmark.
108
+ */
109
+ headerVariant?: "light" | "dark";
110
+ /**
111
+ * Hero treatment. "bold" (default) paints the hero full-bleed in the primary
112
+ * color (one signature moment). "light" renders the hero on the page
113
+ * background with the brand color used only as an accent on the headline/CTA —
114
+ * matches merchants whose own site is a light page with color accents. The
115
+ * brand adopter sets "light" when it captures a real hero image.
116
+ */
117
+ heroVariant?: "bold" | "light";
118
+ }
119
+
120
+ export interface ThemeTokens {
121
+ color: ThemeColorTokens;
122
+ type: ThemeTypeTokens;
123
+ space: ThemeSpaceTokens;
124
+ shape: ThemeShapeTokens;
125
+ brand: ThemeBrandTokens;
126
+ }
127
+
128
+ /**
129
+ * California Prop 65 warning content. The safe-harbor warning FORMAT is mandated
130
+ * (Cal. Code Regs. tit. 27 § 25601+): the component owns the fixed skeleton
131
+ * ("WARNING: … known to the State of California to cause …") and the official
132
+ * warnings URL; only the variable slots — which harm endpoint(s) the warning
133
+ * cites and the chemical name(s) named in the long form — come from tenant data.
134
+ * Never author the sentence in tenant/marketing copy; drive it from here.
135
+ */
136
+ export interface Prop65Config {
137
+ /** Harm endpoint(s) the mandated clause cites. */
138
+ harm: "cancer" | "reproductive" | "cancer-and-reproductive";
139
+ /**
140
+ * Chemical name(s) named in the LONG-form warning (e.g. ["nicotine"]). When
141
+ * present the component renders the long form ("…including {chemicals}, which
142
+ * is/are known…"); omit for the short-form warning.
143
+ */
144
+ chemicals?: string[];
145
+ /** Official warnings URL. Defaults to www.P65Warnings.ca.gov. */
146
+ url?: string;
147
+ }
148
+
149
+ /**
150
+ * Purchase-quantity-limit DISPLAY. Display surface only — the limit is surfaced
151
+ * for the shopper; enforcement stays in the product/commerce layer, never hand-
152
+ * built here (per decision compliance-widgets-platform-owned).
153
+ */
154
+ export interface PurchaseLimitConfig {
155
+ /** Human-readable limit, e.g. "2 units per order". Display only. */
156
+ text: string;
157
+ /** Optional short reason/citation, e.g. "per state law". */
158
+ reason?: string;
159
+ }
160
+
161
+ /**
162
+ * State/jurisdiction sale-eligibility DISPLAY. Display surface only — the widget
163
+ * SHOWS the tenant-configured eligibility messaging; it does NOT compute whether
164
+ * a given cart/address is eligible (that stays in the product/commerce layer,
165
+ * per decision compliance-widgets-platform-owned). List the restricted OR the
166
+ * eligible jurisdictions (or supply a custom note); the widget composes a
167
+ * default sentence when `note` is omitted.
168
+ */
169
+ export interface StateEligibilityConfig {
170
+ /** State/jurisdiction codes or names where sale/shipment is NOT available. */
171
+ restrictedStates?: string[];
172
+ /** Allowlist — sale/shipment available ONLY in these. */
173
+ eligibleStates?: string[];
174
+ /** Custom display message; when omitted a sentence is composed from the lists. */
175
+ note?: string;
176
+ }
177
+
178
+ /**
179
+ * Excise-tax DISPLAY cue. Display surface only — it surfaces that applicable
180
+ * excise taxes apply and (optionally) a per-jurisdiction breakdown for shopper
181
+ * transparency; the actual tax CALCULATION is product/commerce-owned (never
182
+ * computed here), per decision compliance-widgets-platform-owned.
183
+ */
184
+ export interface ExciseTaxConfig {
185
+ /** Display note, e.g. "State vapor excise taxes are calculated at checkout." */
186
+ note: string;
187
+ /** Optional per-jurisdiction lines (display only), e.g. { label: "CA", detail: "$0.05/ml" }. */
188
+ jurisdictions?: Array<{ label: string; detail: string }>;
189
+ }
190
+
191
+ /**
192
+ * Regulated-commerce compliance (e.g. vape/nicotine). Browse-surface only —
193
+ * the actual identity/age VERIFICATION is Token of Trust's product and is wired
194
+ * in a later phase; this config drives storefront affordances (FDA warning, the
195
+ * soft 21+ affirmation gate, shipping-restriction / jurisdiction notices).
196
+ * Omit/undefined for unregulated tenants and none of it renders.
197
+ *
198
+ * Every field below is data-driven: the matching platform widget in
199
+ * components/compliance/ renders ONLY when its field is present and NEVER
200
+ * fabricates or hand-authors mandated legal copy (per decision
201
+ * compliance-widgets-platform-owned). Additive — a tenant lists only what applies.
202
+ */
203
+ export interface ComplianceConfig {
204
+ /** Minimum age to enter. Drives the soft affirmation gate (e.g. 21). */
205
+ minAge?: number;
206
+ /** Render the FDA nicotine warning sitewide + reinforced on PDP. */
207
+ nicotineWarning?: boolean;
208
+ /** Short PACT-Act / no-ship-states / adult-signature notice (plain text). */
209
+ shippingRestriction?: string;
210
+ /**
211
+ * Render the PACT Act compliance notice (ENDS/vapor shipments): age
212
+ * verification (21+), adult signature on delivery, no USPS carriage. The
213
+ * federal-requirement wording is fixed in PactActNotice; this is the on/off.
214
+ */
215
+ pactAct?: boolean;
216
+ /**
217
+ * Render the adult-signature-on-delivery notice. Wording is fixed in
218
+ * AdultSignatureNotice; the age reflects `minAge` (defaults to 21).
219
+ */
220
+ adultSignature?: boolean;
221
+ /**
222
+ * California Prop 65 warning. `true` renders the short cancer+reproductive
223
+ * safe-harbor warning; a Prop65Config selects the harm endpoint(s), chemical
224
+ * name(s), and URL. Absent → nothing renders.
225
+ */
226
+ prop65?: Prop65Config | boolean;
227
+ /** Purchase-quantity-limit display (display only; enforcement is product-owned). */
228
+ purchaseLimit?: PurchaseLimitConfig;
229
+ /**
230
+ * State/jurisdiction sale-eligibility display (display only; the eligibility
231
+ * DECISION for a given cart/address stays product-owned).
232
+ */
233
+ stateEligibility?: StateEligibilityConfig;
234
+ /**
235
+ * Excise-tax display cue (display only; the tax CALCULATION stays
236
+ * product/commerce-owned).
237
+ */
238
+ exciseTax?: ExciseTaxConfig;
239
+ }
240
+
241
+ /**
242
+ * What a tenant's site is FOR. "commerce" (default) is the browse/discover
243
+ * ecommerce storefront the platform was born as. "marketing" is a non-commerce
244
+ * B2B/SaaS site — chrome, rich page composition, and conversion CTAs, with the
245
+ * catalog/cart affordances switched off. The mode picks feature DEFAULTS
246
+ * (see resolveTenantFeatures); `features` overrides individual flags.
247
+ */
248
+ export type SiteType = "commerce" | "marketing";
249
+
250
+ /**
251
+ * Per-tenant capability switches. These gate whole surfaces so the same
252
+ * component library renders both a store and a marketing site without any
253
+ * tenant-specific `if` in shared code. Every flag has a sensible default keyed
254
+ * off `siteType`; a tenant only lists the ones it wants to flip.
255
+ */
256
+ export interface TenantFeatures {
257
+ /** Catalog surfaces: /collections, /products/*, listing grids, PDPs. */
258
+ catalog: boolean;
259
+ /** Header search typeahead island. */
260
+ productSearch: boolean;
261
+ /** Wishlist / "saved items" surface + header heart control. */
262
+ savedItems: boolean;
263
+ /** Product quick-view dialog island. */
264
+ quickView: boolean;
265
+ /** Newsletter capture forms (footer + homepage block). */
266
+ newsletter: boolean;
267
+ /** Product/collection URLs in the XML sitemap. */
268
+ commerceSitemap: boolean;
269
+ /** Product-offer JSON-LD on product/collection pages. */
270
+ productJsonLd: boolean;
271
+ /** Soft age-affirmation gate + regulated-commerce chrome (also needs `compliance`). */
272
+ ageGate: boolean;
273
+ }
274
+
275
+ const COMMERCE_FEATURES: TenantFeatures = {
276
+ catalog: true,
277
+ productSearch: true,
278
+ savedItems: true,
279
+ quickView: true,
280
+ newsletter: true,
281
+ commerceSitemap: true,
282
+ productJsonLd: true,
283
+ ageGate: true,
284
+ };
285
+
286
+ const MARKETING_FEATURES: TenantFeatures = {
287
+ catalog: false,
288
+ productSearch: false,
289
+ savedItems: false,
290
+ quickView: false,
291
+ newsletter: false,
292
+ commerceSitemap: false,
293
+ productJsonLd: false,
294
+ ageGate: false,
295
+ };
296
+
297
+ /** Feature defaults for a site type (returns a fresh copy — safe to mutate). */
298
+ export function siteTypeDefaults(siteType: SiteType): TenantFeatures {
299
+ return { ...(siteType === "marketing" ? MARKETING_FEATURES : COMMERCE_FEATURES) };
300
+ }
301
+
302
+ /**
303
+ * Resolve a tenant's effective features: siteType defaults, with any explicit
304
+ * `features` overrides layered on top (undefined entries ignored, so a tenant
305
+ * can flip one flag without restating the rest). Unset `siteType` == "commerce",
306
+ * preserving the platform's original behavior for existing tenants.
307
+ */
308
+ export function resolveTenantFeatures(
309
+ tenant: Pick<TenantConfig, "siteType" | "features">,
310
+ ): TenantFeatures {
311
+ const base = siteTypeDefaults(tenant.siteType ?? "commerce");
312
+ if (!tenant.features) return base;
313
+ for (const [k, v] of Object.entries(tenant.features)) {
314
+ if (v !== undefined) base[k as keyof TenantFeatures] = v;
315
+ }
316
+ return base;
317
+ }
318
+
319
+ export interface TenantConfig {
320
+ tenant_id: string;
321
+ /** ToT-side merchant identifier (appDomain), e.g. "pilot.tokenoftrust.com". */
322
+ appDomain: string;
323
+ /** The hostname(s) that resolve to this tenant. */
324
+ domains: string[];
325
+ /** Primary canonical domain for SEO/canonical URLs. */
326
+ canonicalDomain: string;
327
+ displayName: string;
328
+ /** Storyblok delivery token for this tenant's child space. */
329
+ storyblok_space_token: string;
330
+ /** Data scope passed to the ToT catalog API (usually == appDomain). */
331
+ tot_data_scope: string;
332
+ /** Per-tenant theme overrides; merged over the reference theme defaults. */
333
+ theme_tokens: DeepPartial<ThemeTokens>;
334
+ /** Currency for display formatting. */
335
+ currency: string;
336
+ locale: string;
337
+ /**
338
+ * What the site is for: "commerce" (default) or "marketing". Drives feature
339
+ * defaults (resolveTenantFeatures) and non-commerce chrome. Omitted == commerce.
340
+ */
341
+ siteType?: SiteType;
342
+ /** Explicit feature overrides layered atop the siteType defaults. */
343
+ features?: Partial<TenantFeatures>;
344
+ /** Regulated-commerce affordances; undefined for unregulated tenants. */
345
+ compliance?: ComplianceConfig;
346
+ /**
347
+ * Cart/checkout engine wiring (FoxyCart, Track A). Present on commerce tenants
348
+ * that have a FoxyCart store; drives the loader + signed add-to-cart. The
349
+ * `storeSubDomain` here is a default — middleware may override it from env
350
+ * (FOXY_STORE_SUBDOMAIN) so the demo store swaps with zero code change. The
351
+ * signing SECRET is never stored on the tenant; it's an env Worker binding.
352
+ */
353
+ commerce?: {
354
+ foxy?: FoxyCommerceConfig;
355
+ };
356
+ /**
357
+ * PR-gated capability switches (cart/checkout, age/identity verification,
358
+ * excise tax). Source of truth is the tenant repo's `.tot/config.json`; this
359
+ * build-time default is overridden per environment by the published
360
+ * capabilities artifact (localhost=working tree, preview=PR head, live=promoted).
361
+ * Undefined entries default from siteType/compliance — see resolveCapabilities.
362
+ */
363
+ capabilities?: Partial<CapabilityConfig>;
364
+ }
365
+
366
+ export type DeepPartial<T> = {
367
+ [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
368
+ };
369
+
370
+ /** The interface the tenant-resolution middleware implements. */
371
+ export interface TenantResolver {
372
+ /**
373
+ * Resolve a tenant for a hostname, falling back to a default tenant for
374
+ * unknown hosts (Phase 1 convenience so preview/local hosts still render).
375
+ */
376
+ resolveByHost(host: string): Promise<TenantConfig | null>;
377
+ /**
378
+ * Strict match: return the tenant that explicitly claims `host` in its
379
+ * `domains`, or null. Unlike resolveByHost it never falls back to a default,
380
+ * so callers can detect "this host/segment owns no tenant" — used by the
381
+ * `/<domain>/*` path-prefix routing fallback.
382
+ */
383
+ resolveStrict(host: string): Promise<TenantConfig | null>;
384
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ["tests/**/*.test.ts"],
6
+ environment: "node",
7
+ },
8
+ });
@@ -0,0 +1,7 @@
1
+ packages:
2
+ - "apps/*"
3
+ - "packages/*"
4
+ onlyBuiltDependencies:
5
+ - esbuild
6
+ - sharp
7
+ - workerd
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Prebuild step for the colocated tenant layout.
4
+ *
5
+ * A tenant IS the repo-root directory `tenants/<id>/` (holding content, public,
6
+ * theme.json). Astro serves static assets from `apps/storefront/public/`, so a
7
+ * tenant's public files must land at `apps/storefront/public/tenants/<id>/`
8
+ * for the `/tenants/<id>/...` URLs to resolve. This script mirrors every tenant's
9
+ * public directory into that generated location.
10
+ *
11
+ * The destination (`apps/storefront/public/tenants/`) is a BUILD ARTIFACT
12
+ * (gitignored) — the source of truth is `tenants/<id>/public/`. Wired into the
13
+ * storefront `prebuild`/`predev` lifecycle (package.json) and invoked directly by
14
+ * `scripts/tot-dev.mjs` before it boots `astro dev`.
15
+ *
16
+ * Idempotent: it wipes+rewrites the destination on every run. `TENANT_ASSETS_DEST`
17
+ * overrides the destination (used by tests / spikes).
18
+ *
19
+ * VERSIONED TENANTS (decision `assets-r2-versioned`): a tenant whose assets are
20
+ * served version-scoped from R2 (src/pages/tenants/[id]/[...path].ts) must NOT be
21
+ * baked here — otherwise the static file would shadow the R2 route. Opt a tenant
22
+ * out by adding a `tenants/<id>/.tot/assets-versioned` marker file, or by listing
23
+ * its id in the `TENANT_ASSETS_VERSIONED` env (comma-separated). Non-versioned
24
+ * tenants keep the baked path unchanged (back-compat).
25
+ */
26
+ import { cp, mkdir, readdir, rm } from "node:fs/promises";
27
+ import { existsSync } from "node:fs";
28
+ import { join, resolve, dirname } from "node:path";
29
+ import { fileURLToPath } from "node:url";
30
+
31
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
32
+ const src = resolve(repoRoot, "tenants");
33
+ const dest = process.env.TENANT_ASSETS_DEST
34
+ ? resolve(process.env.TENANT_ASSETS_DEST)
35
+ : resolve(repoRoot, "apps/storefront/public/tenants");
36
+
37
+ if (!existsSync(src)) process.exit(0);
38
+
39
+ await rm(dest, { recursive: true, force: true });
40
+
41
+ const versionedEnv = new Set(
42
+ (process.env.TENANT_ASSETS_VERSIONED ?? "")
43
+ .split(",")
44
+ .map((s) => s.trim())
45
+ .filter(Boolean),
46
+ );
47
+ const isVersioned = (id) =>
48
+ versionedEnv.has(id) ||
49
+ existsSync(join(src, id, ".tot", "assets-versioned"));
50
+
51
+ let copied = 0;
52
+ let skipped = 0;
53
+ for (const id of await readdir(src)) {
54
+ const pub = join(src, id, "public");
55
+ if (!existsSync(pub)) continue;
56
+ // Versioned tenants are served from R2 — baking would shadow the R2 route.
57
+ if (isVersioned(id)) {
58
+ skipped++;
59
+ continue;
60
+ }
61
+ await mkdir(join(dest, id), { recursive: true });
62
+ await cp(pub, join(dest, id), { recursive: true });
63
+ copied++;
64
+ }
65
+
66
+ const skipNote = skipped ? ` (skipped ${skipped} versioned → R2)` : "";
67
+ console.log(
68
+ `copy-tenant-assets: mirrored ${copied} tenant public/ tree(s) → ${dest}${skipNote}`,
69
+ );
@@ -0,0 +1,183 @@
1
+ /**
2
+ * The inline-AI edit loop's dev-server-side handlers (WS5 G1): the floating
3
+ * "✦ ask" box (see ai-widget-client.js) posts a prompt here; this resolves the
4
+ * developer's MCP session, calls the `content_edit` tool with the tenant's
5
+ * current content/theme, applies the returned unified diffs to disk (which
6
+ * fires the existing tenantHotReload/Vite HMR — see astro.config.mjs), and
7
+ * records the edit for one-key undo. Wired into `astro dev` as a Vite
8
+ * middleware plugin (astro.config.mjs's `inlineAiEditPlugin`), so it only
9
+ * ever runs in `astro dev` — never bundled into the production Cloudflare
10
+ * Worker (see the plugin's `apply: "serve"`).
11
+ *
12
+ * Reuses the CLI's own MCP transport + developer-session resolution
13
+ * (packages/cli/src/mcp.mjs, auth.mjs) via a plain relative import — both are
14
+ * dependency-free and the astro dev process runs on the same host/user as the
15
+ * `tot` CLI, so `~/.tot/credentials.json` is directly readable. No token
16
+ * plumbing needed between the CLI and the dev server.
17
+ *
18
+ * Scope (decision `d2-resolved-inline-ai-scope`): content + theme.json ONLY.
19
+ * The MCP tool enforces this server-side; nothing here widens it.
20
+ */
21
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from "node:fs";
22
+ import { randomUUID } from "node:crypto";
23
+ import { join, dirname } from "node:path";
24
+ import { createMcpClient } from "../../packages/cli/src/mcp.mjs";
25
+ import { resolveDeveloperSession, AuthUnavailableError } from "../../packages/cli/src/auth.mjs";
26
+ import { applyUnifiedDiff } from "./unified-diff.mjs";
27
+
28
+ const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
29
+
30
+ /** Where a tenant's editable files + AI-edit history live (monorepo layout). */
31
+ export function tenantPaths(repoRoot, tenantId) {
32
+ const dir = join(repoRoot, "tenants", tenantId);
33
+ return {
34
+ dir,
35
+ home: join(dir, "content", "home.html"),
36
+ theme: join(dir, "theme.json"),
37
+ historyFile: join(dir, ".tot", "ai-history.jsonl"),
38
+ };
39
+ }
40
+
41
+ function readFileIfPresent(path) {
42
+ return existsSync(path) ? readFileSync(path, "utf8") : null;
43
+ }
44
+
45
+ function appendHistory(historyFile, entry) {
46
+ mkdirSync(dirname(historyFile), { recursive: true });
47
+ appendFileSync(historyFile, `${JSON.stringify(entry)}\n`);
48
+ }
49
+
50
+ function readHistory(historyFile) {
51
+ if (!existsSync(historyFile)) return [];
52
+ return readFileSync(historyFile, "utf8")
53
+ .split("\n")
54
+ .filter(Boolean)
55
+ .map((line) => JSON.parse(line));
56
+ }
57
+
58
+ /**
59
+ * Resolve an authenticated MCP client for the developer signed in to THIS
60
+ * machine (`tot login`). Never refreshes an expired token — mirrors the
61
+ * "silently refresh" case as an explicit ask to re-run `tot login`, matching
62
+ * the CLI's own AuthUnavailableError guidance rather than an obscure 500.
63
+ */
64
+ async function resolveClient(mcpBaseUrl) {
65
+ const client = createMcpClient(mcpBaseUrl || process.env.TOT_MCP_URL || DEFAULT_MCP_URL);
66
+ await client.initialize();
67
+ try {
68
+ await resolveDeveloperSession(client, process.env);
69
+ } catch (e) {
70
+ if (e instanceof AuthUnavailableError) {
71
+ throw new Error(`${e.message} ${e.hint || ""}`.trim());
72
+ }
73
+ throw e;
74
+ }
75
+ return client;
76
+ }
77
+
78
+ /**
79
+ * Handle a "✦ ask" submission: prompt + tenant → MCP content_edit → diffs
80
+ * applied to disk + one history entry. Returns a plain object the widget
81
+ * renders directly (never throws for expected failure modes — auth/refusal/
82
+ * not-configured all come back as `{ ok:false, status, message }`).
83
+ */
84
+ export async function handleAiEdit({ repoRoot, tenantId, prompt, mcpBaseUrl }, deps = {}) {
85
+ const { resolveClientImpl = resolveClient } = deps;
86
+ const paths = tenantPaths(repoRoot, tenantId);
87
+ const files = [
88
+ { path: "content/home.html", content: readFileIfPresent(paths.home) },
89
+ { path: "theme.json", content: readFileIfPresent(paths.theme) },
90
+ ].filter((f) => f.content !== null);
91
+
92
+ if (files.length === 0) {
93
+ return { ok: false, status: "no_files", message: `No content/home.html or theme.json found under tenants/${tenantId}/.` };
94
+ }
95
+
96
+ let client;
97
+ try {
98
+ client = await resolveClientImpl(mcpBaseUrl);
99
+ } catch (e) {
100
+ return { ok: false, status: "unauthenticated", message: e.message };
101
+ }
102
+
103
+ let result;
104
+ try {
105
+ result = await client.callTool("content_edit", { prompt, files });
106
+ } catch (e) {
107
+ return { ok: false, status: "error", message: e.message };
108
+ }
109
+ if (result?.status && result.status !== "ok") {
110
+ return { ok: false, status: result.status, message: result.message || "content_edit refused the request." };
111
+ }
112
+
113
+ const diffs = Array.isArray(result?.diffs) ? result.diffs : [];
114
+ if (diffs.length === 0) {
115
+ return { ok: true, status: "noop", summary: result?.summary || "No change was needed.", diffs: [] };
116
+ }
117
+
118
+ const pathFor = (relPath) => (relPath === "content/home.html" ? paths.home : relPath === "theme.json" ? paths.theme : null);
119
+ const applied = [];
120
+ for (const d of diffs) {
121
+ const target = pathFor(d.path);
122
+ if (!target) {
123
+ return { ok: false, status: "out_of_scope", message: `content_edit returned a diff for an unexpected path: ${d.path}` };
124
+ }
125
+ const before = readFileIfPresent(target) ?? "";
126
+ let after;
127
+ try {
128
+ after = applyUnifiedDiff(before, d.diff);
129
+ } catch (e) {
130
+ return { ok: false, status: "diff_apply_failed", message: `Could not apply the diff for ${d.path}: ${e.message}` };
131
+ }
132
+ applied.push({ path: d.path, target, before, after, diff: d.diff });
133
+ }
134
+
135
+ // Write only after every diff in the batch applies cleanly — a partial
136
+ // apply would leave content/theme inconsistent with each other.
137
+ for (const a of applied) writeFileSync(a.target, a.after);
138
+
139
+ const historyId = randomUUID();
140
+ appendHistory(paths.historyFile, {
141
+ id: historyId,
142
+ at: new Date().toISOString(),
143
+ kind: "edit",
144
+ prompt,
145
+ summary: result.summary,
146
+ files: applied.map(({ path, before, after, diff }) => ({ path, before, after, diff })),
147
+ });
148
+
149
+ return {
150
+ ok: true,
151
+ status: "ok",
152
+ summary: result.summary,
153
+ diffs: applied.map(({ path, diff }) => ({ path, diff })),
154
+ historyId,
155
+ };
156
+ }
157
+
158
+ /**
159
+ * One-key undo: restore the most recent (non-undone) edit's files to their
160
+ * pre-edit content, and record the undo as its own history entry so the log
161
+ * stays an honest append-only account of what happened.
162
+ */
163
+ export function handleAiUndo({ repoRoot, tenantId }) {
164
+ const paths = tenantPaths(repoRoot, tenantId);
165
+ const history = readHistory(paths.historyFile);
166
+ const undoneIds = new Set(history.filter((h) => h.kind === "undo").map((h) => h.undoes));
167
+ const lastEdit = [...history].reverse().find((h) => h.kind === "edit" && !undoneIds.has(h.id));
168
+
169
+ if (!lastEdit) {
170
+ return { ok: false, status: "nothing_to_undo", message: "No AI edit to undo." };
171
+ }
172
+
173
+ for (const f of lastEdit.files) writeFileSync(f.path === "content/home.html" ? paths.home : paths.theme, f.before);
174
+
175
+ appendHistory(paths.historyFile, {
176
+ id: randomUUID(),
177
+ at: new Date().toISOString(),
178
+ kind: "undo",
179
+ undoes: lastEdit.id,
180
+ });
181
+
182
+ return { ok: true, status: "ok", restoredPaths: lastEdit.files.map((f) => f.path), undid: lastEdit.summary };
183
+ }