@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,164 @@
1
+ /**
2
+ * POST /api/content-edit/ai-edit — the `/dev` page's "Build with Claude" live loop
3
+ * (WS5 G4 + decision draft-overlay-same-origin). A prompt becomes a reviewable
4
+ * per-file diff via tot-mcp's `content_edit` tool, applied to the signed-in
5
+ * developer's OWN tenant content — and materialized into their private DRAFT
6
+ * overlay so the /dev live preview shows the change instantly (visible AND
7
+ * interactive), while live/test channels and every other viewer are untouched.
8
+ *
9
+ * AUTH: `/dev`'s own signed-in-viewer session (`readViewerSession`) is the
10
+ * developer-level gate; the MCP call authenticates as the storefront's own
11
+ * TENANT-OPERATOR credential (`getContentEditClient` — the same
12
+ * credential_validate path `/api/changes/accept` uses). The tenant edited is the
13
+ * SESSION'S OWN first membership — never a caller-supplied tenant.
14
+ *
15
+ * DRAFT MATERIALIZATION: content_edit returns unified DIFFS, not contents, so we
16
+ * re-apply each diff to the exact content we sent (applyUnifiedDiff, strict +
17
+ * fail-closed) and write the results to `dev-draft:` KV keys with a TTL, minting
18
+ * the signed tot_dev_draft cookie the middleware's overlay verifies. When KV or
19
+ * the signing secret is absent (e.g. bare `astro dev`), the edit still runs and
20
+ * returns diffs — `applied:false` tells the page the preview won't change.
21
+ */
22
+ import type { APIContext } from "astro";
23
+ import { readViewerSession } from "@/lib/auth/route";
24
+ import { getContentEditClient } from "@/lib/content-edit";
25
+ import { applyUnifiedDiff, DiffApplyError } from "@/lib/content-edit/applyDiff";
26
+ import { json } from "@/lib/grants/route.js";
27
+ import { readEnv, readKv } from "@/lib/env";
28
+ // Arbitrary-tenant lookup lives behind the config-layer provider so this handler
29
+ // doesn't import the concrete resolver/static-map directly (tenancy-resolver-fence).
30
+ import { resolvePreviewTenant } from "@/config/previewTenantLookup";
31
+ import { createContentProvider } from "@/lib/storyblok/provider";
32
+ import { readPublishedArtifact, themeArtifactPath } from "@tot/public-runtime";
33
+ import {
34
+ DEV_DRAFT_COOKIE,
35
+ DEV_DRAFT_TTL_SECONDS,
36
+ devDraftKey,
37
+ signDevDraftToken,
38
+ verifyDevDraftToken,
39
+ } from "@/lib/dev/devDraft";
40
+ import type { ContentEditFile } from "@/lib/content-edit/types";
41
+
42
+ export const prerender = false;
43
+
44
+ export async function POST(context: APIContext): Promise<Response> {
45
+ const session = await readViewerSession({ cookies: context.cookies });
46
+ if (!session) return json({ error: "sign in required" }, 401);
47
+ if (!session.capability || session.capability === "none") {
48
+ return json({ error: "no developer capability on this account" }, 403);
49
+ }
50
+
51
+ let body: { prompt?: unknown };
52
+ try {
53
+ body = (await context.request.json()) as typeof body;
54
+ } catch {
55
+ return json({ error: "invalid JSON body" }, 400);
56
+ }
57
+ const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
58
+ if (!prompt) return json({ error: "prompt is required" }, 400);
59
+
60
+ // The tenant is the session's OWN store (first membership, else the session's
61
+ // primary resource) — the same resolution /dev renders with. No caller-supplied
62
+ // tenant is ever honored here.
63
+ const wanted = (session.tenants?.[0]?.resource ?? session.resource ?? "").toLowerCase();
64
+ const tenant = wanted ? await resolvePreviewTenant(wanted) : null;
65
+ if (!tenant) return json({ error: "no store linked to this account" }, 403);
66
+
67
+ const kv = await readKv("TENANT_CACHE");
68
+ const kvGet = kv ? (key: string) => kv.get(key) : async () => null;
69
+ const secret = (await readEnv("CUSTOMIZATION_PREVIEW_SECRET")) ?? null;
70
+ const nowSeconds = Math.floor(Date.now() / 1000);
71
+
72
+ // Reuse the caller's existing draft (so successive edits stack) or start a new one.
73
+ const priorCookie = context.cookies.get(DEV_DRAFT_COOKIE)?.value ?? null;
74
+ const priorClaims =
75
+ priorCookie && secret ? await verifyDevDraftToken(secret, priorCookie, nowSeconds) : null;
76
+ const draftId =
77
+ priorClaims?.tenantId === tenant.tenant_id ? priorClaims.draftId : crypto.randomUUID();
78
+
79
+ // Current content = the developer's draft when present, else what actually serves
80
+ // (published overlay → build-time), so edits stack and always start from reality.
81
+ const provider = createContentProvider(tenant, kv ? { get: kvGet, env: "live" } : undefined);
82
+ const draftHome = await kvGet(devDraftKey(tenant.tenant_id, draftId, "content/home.html"));
83
+ const homeHtml = draftHome ?? (await provider.getHomeHtml());
84
+ const draftTheme = await kvGet(devDraftKey(tenant.tenant_id, draftId, "theme.json"));
85
+ const themeJson =
86
+ draftTheme ??
87
+ (await readPublishedArtifact(kvGet, tenant.tenant_id, "live", themeArtifactPath(tenant.tenant_id))) ??
88
+ JSON.stringify(tenant.theme_tokens ?? {}, null, 2);
89
+
90
+ // content_edit's allowlist: home document + theme. A block-based tenant with no raw
91
+ // home.html still gets theme edits (themeStyle drives its pages).
92
+ const files: ContentEditFile[] = [];
93
+ if (homeHtml) files.push({ path: "content/home.html", content: homeHtml });
94
+ files.push({ path: "theme.json", content: themeJson });
95
+
96
+ const client = await getContentEditClient();
97
+ if (client instanceof Response) return client;
98
+
99
+ let result;
100
+ try {
101
+ result = await client.editContent(prompt, files);
102
+ } catch (err) {
103
+ const message = err instanceof Error ? err.message : String(err);
104
+ return json({ ok: false, error: message }, 502);
105
+ }
106
+
107
+ // Materialize the draft: re-apply each returned diff to the exact content we sent.
108
+ // Strict + fail-closed per file — a bad diff writes nothing for that file. Theme
109
+ // must still parse as JSON before it may land (a broken draft theme would degrade
110
+ // the whole preview).
111
+ const sent = new Map(files.map((f) => [f.path, f.content]));
112
+ const appliedPaths: string[] = [];
113
+ const canApply = !!kv && !!secret;
114
+ if (canApply) {
115
+ for (const d of result.diffs ?? []) {
116
+ const base = sent.get(d.path);
117
+ if (base == null) continue; // never write a path we didn't offer
118
+ let next: string;
119
+ try {
120
+ next = applyUnifiedDiff(base, d.diff);
121
+ } catch (err) {
122
+ if (err instanceof DiffApplyError) continue;
123
+ throw err;
124
+ }
125
+ if (next === base) continue;
126
+ if (d.path === "theme.json") {
127
+ try {
128
+ JSON.parse(next);
129
+ } catch {
130
+ continue;
131
+ }
132
+ }
133
+ await kv!.put(devDraftKey(tenant.tenant_id, draftId, d.path), next, {
134
+ expirationTtl: DEV_DRAFT_TTL_SECONDS,
135
+ });
136
+ appliedPaths.push(d.path);
137
+ }
138
+ if (appliedPaths.length > 0) {
139
+ // (Re)issue the signed draft cookie so the middleware overlay serves this draft.
140
+ const token = await signDevDraftToken(
141
+ secret!,
142
+ tenant.tenant_id,
143
+ draftId,
144
+ DEV_DRAFT_TTL_SECONDS,
145
+ nowSeconds,
146
+ );
147
+ context.cookies.set(DEV_DRAFT_COOKIE, token, {
148
+ httpOnly: true,
149
+ secure: true,
150
+ sameSite: "lax",
151
+ path: "/",
152
+ maxAge: DEV_DRAFT_TTL_SECONDS,
153
+ });
154
+ }
155
+ }
156
+
157
+ return json({
158
+ ok: true,
159
+ ...result,
160
+ // True when the draft landed and the live preview will show it on reload.
161
+ applied: appliedPaths.length > 0,
162
+ appliedPaths,
163
+ });
164
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * CSP violation collector (enables the report-only → enforce
3
+ * rollout). Browsers POST violation reports here (via `report-to` /
4
+ * `report-uri`); we log them so platform + tenant-script CSP violations surface
5
+ * before we flip the policy from Report-Only to enforcing. Returns 204.
6
+ *
7
+ * Phase 1: log to the worker console. A later iteration can forward to an
8
+ * analytics/observability sink.
9
+ */
10
+ import type { APIRoute } from "astro";
11
+
12
+ export const prerender = false;
13
+
14
+ export const POST: APIRoute = async ({ request }) => {
15
+ try {
16
+ const body = await request.text();
17
+ if (body) {
18
+ // Reports can be `application/csp-report` or `application/reports+json`.
19
+ console.warn("[csp-report]", body.slice(0, 4000));
20
+ }
21
+ } catch {
22
+ /* never fail a report submission */
23
+ }
24
+ return new Response(null, { status: 204 });
25
+ };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * POST /api/dashboard/enter-vendor { vendorAppDomain } — the ToT-STAFF admission
3
+ * endpoint (#8425 staff access). Phase 2 of two-phase admission: on the staff
4
+ * viewer explicitly selecting a searched vendor, tot20 writes the server-side
5
+ * AUDIT row and returns the staffRoles-scoped capability; we then stamp a
6
+ * short-lived {@link StaffSelection} on the session and admit the viewer to
7
+ * `/dashboard/<appDomain>` scoped to that capability.
8
+ *
9
+ * AUTHORIZATION — two independent axes:
10
+ * NEAR (this endpoint): a valid `tot_session` carrying ToT-STAFF scope
11
+ * (`staff[]` non-empty). No session → 401; no staff scope → 403.
12
+ * FAR (tot20): `/clients/staffVendorAccess`, operator-authed as storefront,
13
+ * presenting a storefront-signed staff broker assertion in the
14
+ * `X-ToT-Broker-Assertion` header (minted from the verified session email).
15
+ * tot20 verifies it, re-checks staff scope from its `sub`, scopes the capability,
16
+ * and writes the audit row — it is the authority. No plaintext email on the wire.
17
+ *
18
+ * The staff selection is a DISTINCT access path from `tenants[]`: we NEVER
19
+ * synthesize a membership. The dashboard guard + login gate consult the marker
20
+ * separately (see tenantSelection.staffAdmits / loginGate.sessionAdmitsHost).
21
+ */
22
+ import type { APIContext } from "astro";
23
+ import { readViewerSession, getSessionMutationHandle } from "@/lib/auth/route";
24
+ import { updateSession } from "@/lib/auth/session";
25
+ import { isStaffSession, buildStaffSelection } from "@/lib/dashboard/staffAdmission";
26
+ import {
27
+ createStaffVendorAccessClient,
28
+ staffAccessHttpStatus,
29
+ StaffVendorAccessClientError,
30
+ } from "@/lib/dashboard/staffVendorAccess";
31
+ import { mintBrokerAssertion, BrokerAssertionError } from "@/lib/auth/brokerAssertion";
32
+
33
+ export const prerender = false;
34
+
35
+ /** JSON response with no-store (staff access state must never be edge-cached). */
36
+ function json(body: unknown, status = 200): Response {
37
+ return new Response(JSON.stringify(body), {
38
+ status,
39
+ headers: { "content-type": "application/json", "cache-control": "no-store" },
40
+ });
41
+ }
42
+
43
+ export async function POST(context: APIContext): Promise<Response> {
44
+ const session = await readViewerSession(context);
45
+ if (!session) return json({ error: "not signed in" }, 401);
46
+ if (!isStaffSession(session)) {
47
+ return json({ error: "staff access required", reason: "notStaff" }, 403);
48
+ }
49
+
50
+ let body: { vendorAppDomain?: unknown };
51
+ try {
52
+ body = (await context.request.json()) as typeof body;
53
+ } catch {
54
+ return json({ error: "invalid JSON body" }, 400);
55
+ }
56
+ const vendorAppDomain =
57
+ typeof body.vendorAppDomain === "string" ? body.vendorAppDomain.trim() : "";
58
+ if (!vendorAppDomain) return json({ error: "vendorAppDomain is required" }, 400);
59
+
60
+ let client;
61
+ try {
62
+ client = await createStaffVendorAccessClient();
63
+ } catch (err) {
64
+ if (err instanceof StaffVendorAccessClientError) {
65
+ return json({ error: "staff-access integration not configured" }, 503);
66
+ }
67
+ throw err;
68
+ }
69
+
70
+ // #8453 `staff-invite-proof-via-broker`: prove the staff actor with a fresh
71
+ // storefront-signed assertion minted from the VERIFIED session email. Fail closed
72
+ // (503) if the signing key is unconfigured.
73
+ let brokerAssertion: string;
74
+ try {
75
+ brokerAssertion = await mintBrokerAssertion(session.email);
76
+ } catch (err) {
77
+ if (err instanceof BrokerAssertionError) {
78
+ return json({ error: "staff-proof signing not configured" }, 503);
79
+ }
80
+ throw err;
81
+ }
82
+
83
+ // FAR authority: tot20 verifies the assertion, derives the staff actor, scopes the
84
+ // capability, and writes the audit row. We admit ONLY on its ok.
85
+ const result = await client.grantStaffAccess(brokerAssertion, vendorAppDomain);
86
+ if (!result.ok) {
87
+ return json(
88
+ { error: result.message, reason: result.reason, code: result.code },
89
+ staffAccessHttpStatus(result.reason, result.status),
90
+ );
91
+ }
92
+
93
+ // Stamp the distinct staff-selection marker on the session (short-lived, scoped
94
+ // to the returned capability). Never a tenants[] entry.
95
+ const nowSeconds = Math.floor(Date.now() / 1000);
96
+ const selection = buildStaffSelection({
97
+ resource: vendorAppDomain,
98
+ capability: result.capability,
99
+ nowSeconds,
100
+ });
101
+ const handle = await getSessionMutationHandle(context);
102
+ if (!handle) {
103
+ // No SESSION KV / no cookie (e.g. plain `astro dev`) — the audit row is
104
+ // written but we can't persist the marker. Fail closed so the viewer isn't
105
+ // told they're admitted when the guard can't see it.
106
+ return json({ error: "session store unavailable; cannot persist admission" }, 503);
107
+ }
108
+ const updated = await updateSession(
109
+ handle.store,
110
+ handle.id,
111
+ (rec) => ({ ...rec, staffSelection: selection }),
112
+ nowSeconds,
113
+ );
114
+ if (!updated) {
115
+ return json({ error: "session expired; sign in again" }, 401);
116
+ }
117
+
118
+ return json({
119
+ ok: true,
120
+ appDomain: vendorAppDomain,
121
+ capability: result.capability,
122
+ redirect: `/dashboard/${encodeURIComponent(vendorAppDomain)}`,
123
+ });
124
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * GET|POST /api/dashboard/vendor-search?q=<prefix> — the ToT-STAFF vendor search
3
+ * proxy (#8425 staff access). Phase 1 of two-phase staff admission: a staff viewer
4
+ * finds a vendor by prefix; there is no silent access to every host.
5
+ *
6
+ * AUTHORIZATION — two independent axes:
7
+ * NEAR (this endpoint): a valid `tot_session` that carries ToT-STAFF scope
8
+ * (`staff[]` non-empty). No session → 401; a session without staff scope →
9
+ * 403 (a plain member has nothing to search across — they use their picker).
10
+ * FAR (tot20): storefront calls `/clients/vendorSearch` as its OWN operator
11
+ * identity, presenting a storefront-signed staff broker assertion in the
12
+ * `X-ToT-Broker-Assertion` header (minted from the verified session email);
13
+ * tot20 verifies it, derives the staff actor from its `sub`, and scopes results
14
+ * to that user's staffRoles. No spoofable plaintext email crosses the wire.
15
+ *
16
+ * GET reads `q` (+ optional `limit`) from the query; POST reads them from a JSON
17
+ * body — the autocomplete client uses GET, but POST is accepted so a no-JS/form
18
+ * path works too. Returns `{ vendors: [{ appDomain, displayName }] }`.
19
+ */
20
+ import type { APIContext } from "astro";
21
+ import { readViewerSession } from "@/lib/auth/route";
22
+ import { isStaffSession } from "@/lib/dashboard/staffAdmission";
23
+ import {
24
+ createStaffVendorAccessClient,
25
+ staffAccessHttpStatus,
26
+ StaffVendorAccessClientError,
27
+ } from "@/lib/dashboard/staffVendorAccess";
28
+ import { mintBrokerAssertion, BrokerAssertionError } from "@/lib/auth/brokerAssertion";
29
+ import {
30
+ parseVendorQuery,
31
+ ANCHORED_FETCH_LIMIT,
32
+ ANCHORED_DISPLAY_LIMIT,
33
+ } from "@/lib/dashboard/vendorQuery";
34
+
35
+ export const prerender = false;
36
+
37
+ /** JSON response with no-store (staff access state must never be edge-cached). */
38
+ function json(body: unknown, status = 200): Response {
39
+ return new Response(JSON.stringify(body), {
40
+ status,
41
+ headers: { "content-type": "application/json", "cache-control": "no-store" },
42
+ });
43
+ }
44
+
45
+ async function handle(context: APIContext, q: string, limit: number | undefined): Promise<Response> {
46
+ const session = await readViewerSession(context);
47
+ if (!session) return json({ error: "not signed in" }, 401);
48
+ if (!isStaffSession(session)) {
49
+ return json({ error: "staff access required", reason: "notStaff" }, 403);
50
+ }
51
+
52
+ let client;
53
+ try {
54
+ client = await createStaffVendorAccessClient();
55
+ } catch (err) {
56
+ if (err instanceof StaffVendorAccessClientError) {
57
+ return json({ error: "staff-access integration not configured" }, 503);
58
+ }
59
+ throw err;
60
+ }
61
+
62
+ // #8453 `staff-invite-proof-via-broker`: prove the staff actor with a fresh
63
+ // storefront-signed assertion minted from the VERIFIED session email — tot20
64
+ // derives the actor + scopes results from it. Fail closed (503) if unconfigured.
65
+ let brokerAssertion: string;
66
+ try {
67
+ brokerAssertion = await mintBrokerAssertion(session.email);
68
+ } catch (err) {
69
+ if (err instanceof BrokerAssertionError) {
70
+ return json({ error: "staff-proof signing not configured" }, 503);
71
+ }
72
+ throw err;
73
+ }
74
+
75
+ // Anchor syntax (^prefix / suffix$ / ^exact$) narrows tot20's substring search.
76
+ // Send the anchor-stripped CORE term upstream (with a widened cap so the exact hit
77
+ // is in the set), then post-filter by the anchor against appDomain. A plain query
78
+ // is unchanged: sent as-is, no post-filter. See @/lib/dashboard/vendorQuery.
79
+ const parsed = parseVendorQuery(q);
80
+ const upstreamQuery = parsed.anchored ? parsed.core : q;
81
+ const upstreamLimit = parsed.anchored ? ANCHORED_FETCH_LIMIT : limit;
82
+
83
+ const result = await client.searchVendors(brokerAssertion, upstreamQuery, upstreamLimit);
84
+ if (!result.ok) {
85
+ return json(
86
+ { error: result.message, reason: result.reason, code: result.code },
87
+ staffAccessHttpStatus(result.reason, result.status),
88
+ );
89
+ }
90
+ const vendors = parsed.anchored
91
+ ? result.vendors
92
+ .filter((v) => parsed.matches(v.appDomain))
93
+ .slice(0, limit && limit > 0 ? limit : ANCHORED_DISPLAY_LIMIT)
94
+ : result.vendors;
95
+ return json({ vendors });
96
+ }
97
+
98
+ function parseLimit(raw: string | null | undefined): number | undefined {
99
+ if (raw == null || raw === "") return undefined;
100
+ const n = Number(raw);
101
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
102
+ }
103
+
104
+ export async function GET(context: APIContext): Promise<Response> {
105
+ const url = new URL(context.request.url);
106
+ const q = (url.searchParams.get("q") ?? "").trim();
107
+ return handle(context, q, parseLimit(url.searchParams.get("limit")));
108
+ }
109
+
110
+ export async function POST(context: APIContext): Promise<Response> {
111
+ let body: { q?: unknown; limit?: unknown };
112
+ try {
113
+ body = (await context.request.json()) as typeof body;
114
+ } catch {
115
+ return json({ error: "invalid JSON body" }, 400);
116
+ }
117
+ const q = typeof body.q === "string" ? body.q.trim() : "";
118
+ const limit = typeof body.limit === "number" ? body.limit : undefined;
119
+ return handle(context, q, limit);
120
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * GET/POST /api/dev/activity — the hosted /dev panel's file-level activity
3
+ * feed (local→hosted activity bridge). See handoff
4
+ * 2026-07-14-hosted-dev-activity-bridge-and-stepper.
5
+ *
6
+ * GET: the signed-in developer's own browser polls this (same capability gate
7
+ * as every other /dev route) to render the activity log and feed the Step-2
8
+ * gate's completion signal. POLL, not a held-open stream — Cloudflare Workers
9
+ * are stateless per request, so a poll means a Worker redeploy mid-session
10
+ * needs no reconnect logic on either side.
11
+ *
12
+ * POST: the developer's LOCAL dev-loop process reports one file save,
13
+ * authorized by the opaque bearer minted alongside their cli-signin-code
14
+ * paste (lib/dev/activityIngestToken.ts) — NOT the browser session cookie,
15
+ * since the local process has no cookie.
16
+ */
17
+ import type { APIContext } from "astro";
18
+ import { readViewerSession } from "@/lib/auth/route";
19
+ import { readKv } from "@/lib/env";
20
+ import { resolvePreviewTenant } from "@/config/previewTenantLookup";
21
+ import { verifyActivityIngestToken } from "@/lib/dev/activityIngestToken";
22
+ import {
23
+ appendActivity,
24
+ readActivity,
25
+ describeActivity,
26
+ type ActivityEvent,
27
+ type ActivityEventKind,
28
+ } from "@/lib/dev/activityStore";
29
+ import { readRuntime, writeRuntime, shapeRuntime, toRuntimeView } from "@/lib/dev/runtimeStore";
30
+
31
+ export const prerender = false;
32
+
33
+ function json(body: unknown, status = 200): Response {
34
+ return new Response(JSON.stringify(body), {
35
+ status,
36
+ headers: { "content-type": "application/json" },
37
+ });
38
+ }
39
+
40
+ export async function GET(context: APIContext): Promise<Response> {
41
+ const session = await readViewerSession({ cookies: context.cookies });
42
+ if (!session) return json({ error: "sign in required" }, 401);
43
+ if (!session.capability || session.capability === "none") {
44
+ return json({ error: "no developer capability on this account" }, 403);
45
+ }
46
+
47
+ // Same tenant resolution every other /dev route uses — the session's own store,
48
+ // never caller input.
49
+ const wanted = (session.tenants?.[0]?.resource ?? session.resource ?? "").toLowerCase();
50
+ const tenant = wanted ? await resolvePreviewTenant(wanted) : null;
51
+ if (!tenant) return json({ error: "no store linked to this account" }, 403);
52
+
53
+ const kv = await readKv("TENANT_CACHE");
54
+ const events = kv ? await readActivity(kv, tenant.tenant_id) : [];
55
+ // The LIVE local-dev runtime (latest heartbeat), separate from the file-save
56
+ // feed — the cockpit renders it as a clickable local link + CLI version, with
57
+ // `live` computed server-side so a skewed local clock can't fake liveness.
58
+ const runtime = kv ? await readRuntime(kv, tenant.tenant_id) : null;
59
+ return json({
60
+ events: events.map((e) => ({ ...e, description: describeActivity(e.file, e.event) })),
61
+ runtime: runtime ? toRuntimeView(runtime) : null,
62
+ });
63
+ }
64
+
65
+ export async function POST(context: APIContext): Promise<Response> {
66
+ const auth = context.request.headers.get("authorization") || "";
67
+ const m = /^Bearer\s+(.+)$/i.exec(auth);
68
+ if (!m) return json({ error: "missing bearer token" }, 401);
69
+
70
+ const sessionKv = await readKv("SESSION");
71
+ if (!sessionKv) return json({ error: "activity bridge not configured" }, 503);
72
+ const claims = await verifyActivityIngestToken(sessionKv, m[1] ?? "");
73
+ if (!claims) return json({ error: "invalid or expired token" }, 401);
74
+
75
+ let body: {
76
+ file?: unknown;
77
+ event?: unknown;
78
+ at?: unknown;
79
+ cliVersion?: unknown;
80
+ runnerVersion?: unknown;
81
+ url?: unknown;
82
+ };
83
+ try {
84
+ body = (await context.request.json()) as typeof body;
85
+ } catch {
86
+ return json({ error: "invalid JSON body" }, 400);
87
+ }
88
+
89
+ // The token's own bound tenant, never a caller-supplied one — a stolen/expired
90
+ // token can't be redirected at a different vendor's runtime/activity feed.
91
+ const tenant = await resolvePreviewTenant(claims.tenant.toLowerCase());
92
+ if (!tenant) return json({ error: "no store linked to this token" }, 403);
93
+ const tenantCache = await readKv("TENANT_CACHE");
94
+
95
+ // Heartbeat: latest-wins local-dev runtime (CLI version + live localhost URL),
96
+ // stored SEPARATELY from the file-save ring buffer. Not a file event, so it
97
+ // takes no `file`; liveness is stamped from the SERVER clock (aliveAt), never
98
+ // the client's `at`.
99
+ if (body.event === "heartbeat") {
100
+ if (!tenantCache) return json({ ok: true, stored: false }); // no KV in this runtime (dev)
101
+ await writeRuntime(tenantCache, tenant.tenant_id, shapeRuntime(body, Date.now()));
102
+ return json({ ok: true, stored: true, kind: "runtime" });
103
+ }
104
+
105
+ const isUp = body.event === "up";
106
+ const file = typeof body.file === "string" ? body.file : "";
107
+ if (!file && !isUp) return json({ error: "file is required" }, 400);
108
+ // A file-relative path never needs to be long; cap it so a misbehaving or
109
+ // hostile bridge-token holder can't inflate the per-tenant KV ring-buffer
110
+ // entry toward KV's per-value size limit.
111
+ if (file.length > 512) return json({ error: "file path too long" }, 400);
112
+ const event: ActivityEventKind = isUp
113
+ ? "up"
114
+ : body.event === "add" || body.event === "unlink"
115
+ ? body.event
116
+ : "change";
117
+ const at = typeof body.at === "number" && Number.isFinite(body.at) ? body.at : Date.now();
118
+
119
+ if (!tenantCache) return json({ ok: true, stored: false }); // no KV in this runtime (dev) — accepted, not stored
120
+
121
+ const ev: ActivityEvent = { file, event, at };
122
+ await appendActivity(tenantCache, tenant.tenant_id, ev);
123
+ return json({ ok: true, stored: true });
124
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * POST /api/dev/cli-signin-code — mint a browserless CLI sign-in command for the
3
+ * SIGNED-IN developer (DevAccess #8425). The /dev getting-started page calls this to
4
+ * render a copyable `tot login --code <token>` so the developer's terminal signs in with
5
+ * no second browser round-trip and no OTP.
6
+ *
7
+ * AUTHORIZATION: the viewer's own session (the `tot_session` cookie). 401 if not signed
8
+ * in, 403 if the session carries no developer capability (nothing to sign a CLI into).
9
+ * The minted-for email is the VERIFIED session email — NEVER read from the body — so a
10
+ * developer can only mint a code for themselves.
11
+ *
12
+ * TENANT-SCOPED CODE (roadtoa3 postmortem, 2026-07-12): the mint also carries the
13
+ * developer's OWN tenant (their invite's vendor, the same resolution /dev renders with —
14
+ * first membership, else the session resource; never client input). The MCP stores it
15
+ * with the code so `tot login --code` lands the terminal ALREADY SCOPED to that vendor —
16
+ * `tot start` goes straight to their store's source, no client_list round-trip, no
17
+ * "no stores you can build on yet". See handoff
18
+ * `2026-07-12-tot-mcp-cli-code-tenant-binding` for the MCP half (store tenant with the
19
+ * code + write the durable subject-capability binding at mint).
20
+ *
21
+ * The response `command` CONTAINS the single-use token; it is returned only to this
22
+ * authenticated developer's own browser and is never logged server-side.
23
+ */
24
+ import type { APIContext } from "astro";
25
+ import { readViewerSession } from "@/lib/auth/route";
26
+ import { mintCliSignInCommand } from "@/lib/dev/cliSignInCode";
27
+ import { mintActivityIngestToken } from "@/lib/dev/activityIngestToken";
28
+ import { readKv } from "@/lib/env";
29
+
30
+ export const prerender = false;
31
+
32
+ function json(body: unknown, status = 200): Response {
33
+ return new Response(JSON.stringify(body), {
34
+ status,
35
+ headers: { "content-type": "application/json" },
36
+ });
37
+ }
38
+
39
+ export async function POST(context: APIContext): Promise<Response> {
40
+ const session = await readViewerSession(context);
41
+ if (!session) return json({ error: "not signed in" }, 401);
42
+ // Same gate the /dev page applies (hasStoreAccess): a real developer capability. A
43
+ // no-capability session has no scoped CLI session to mint.
44
+ if (!session.capability || session.capability === "none") {
45
+ return json({ error: "no developer access on this session", reason: "notEligible" }, 403);
46
+ }
47
+
48
+ // The developer's own vendor — the tenant their invite granted (same resolution /dev
49
+ // renders with). Rides with the code so the CLI session lands on this store directly.
50
+ const tenant = session.tenants?.[0]?.resource ?? session.resource ?? null;
51
+
52
+ // Mint the local→hosted activity-bridge credential alongside the CLI code (best
53
+ // effort — a runtime without the SESSION KV binding just omits it, and the local
54
+ // loop runs with no hosted signal, same as before this existed).
55
+ let activityBridge: { token: string; url: string } | null = null;
56
+ const sessionKv = await readKv("SESSION");
57
+ if (sessionKv && tenant) {
58
+ const { token } = await mintActivityIngestToken(sessionKv, {
59
+ email: session.email,
60
+ tenant,
61
+ capability: session.capability,
62
+ });
63
+ activityBridge = { token, url: context.url.origin };
64
+ }
65
+
66
+ const result = await mintCliSignInCommand(session.email, tenant, {}, activityBridge);
67
+ if (!result.ok) {
68
+ // 503 = seam not configured / MCP route unmounted; 502 = MCP fault. Surface neither
69
+ // the token nor internal detail beyond a terse reason.
70
+ return json({ error: "CLI sign-in code unavailable", reason: result.reason }, result.status);
71
+ }
72
+ return json({ command: result.command, expiresAt: result.expiresAt });
73
+ }