@tokenoftrust/storefront-runner 2.0.2 → 2.0.4

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 (254) hide show
  1. package/apps/storefront/.astro/content.d.ts +159 -0
  2. package/apps/storefront/.astro/integrations/_astrojs_cloudflare/cloudflare.d.ts +1 -0
  3. package/apps/storefront/.astro/types.d.ts +3 -0
  4. package/apps/storefront/node_modules/.bin/acorn +43 -0
  5. package/apps/storefront/node_modules/.bin/astro +43 -0
  6. package/apps/storefront/node_modules/.bin/astro-check +43 -0
  7. package/apps/storefront/node_modules/.bin/browserslist +43 -0
  8. package/apps/storefront/node_modules/.bin/cf-wrangler +43 -0
  9. package/apps/storefront/node_modules/.bin/depcruise +43 -0
  10. package/apps/storefront/node_modules/.bin/depcruise-baseline +43 -0
  11. package/apps/storefront/node_modules/.bin/depcruise-fmt +43 -0
  12. package/apps/storefront/node_modules/.bin/depcruise-wrap-stream-in-html +43 -0
  13. package/apps/storefront/node_modules/.bin/dependency-cruise +43 -0
  14. package/apps/storefront/node_modules/.bin/dependency-cruiser +43 -0
  15. package/apps/storefront/node_modules/.bin/drizzle-kit +43 -0
  16. package/apps/storefront/node_modules/.bin/esbuild +43 -0
  17. package/apps/storefront/node_modules/.bin/jiti +43 -0
  18. package/apps/storefront/node_modules/.bin/playwright +43 -0
  19. package/apps/storefront/node_modules/.bin/playwright-core +43 -0
  20. package/apps/storefront/node_modules/.bin/prettier +43 -0
  21. package/apps/storefront/node_modules/.bin/rollup +43 -0
  22. package/apps/storefront/node_modules/.bin/tsc +43 -0
  23. package/apps/storefront/node_modules/.bin/tsserver +43 -0
  24. package/apps/storefront/node_modules/.bin/tsx +43 -0
  25. package/apps/storefront/node_modules/.bin/vite +43 -0
  26. package/apps/storefront/node_modules/.bin/vitest +43 -0
  27. package/apps/storefront/node_modules/.bin/workerd +43 -0
  28. package/apps/storefront/node_modules/.bin/wrangler +43 -0
  29. package/apps/storefront/node_modules/.bin/wrangler2 +43 -0
  30. package/apps/storefront/node_modules/.bin/yaml +43 -0
  31. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_client__js.js +58 -0
  32. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_client__js.js.map +1 -0
  33. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_n_@preact_signals.js +741 -0
  34. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_n_@preact_signals.js.map +1 -0
  35. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_server__js.js +2549 -0
  36. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_server__js.js.map +1 -0
  37. package/apps/storefront/node_modules/.vite/deps/_metadata.json +150 -0
  38. package/apps/storefront/node_modules/.vite/deps/astro-BIHuWFxm.js +434 -0
  39. package/apps/storefront/node_modules/.vite/deps/astro-BIHuWFxm.js.map +1 -0
  40. package/apps/storefront/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  41. package/apps/storefront/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  42. package/apps/storefront/node_modules/.vite/deps/astro_n_axobject-query.js +2705 -0
  43. package/apps/storefront/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  44. package/apps/storefront/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  45. package/apps/storefront/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  46. package/apps/storefront/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  47. package/apps/storefront/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  48. package/apps/storefront/node_modules/.vite/deps/audit-DKDpcdEI.js +1535 -0
  49. package/apps/storefront/node_modules/.vite/deps/audit-DKDpcdEI.js.map +1 -0
  50. package/apps/storefront/node_modules/.vite/deps/highlight-B9UoX1wa.js +48 -0
  51. package/apps/storefront/node_modules/.vite/deps/highlight-B9UoX1wa.js.map +1 -0
  52. package/apps/storefront/node_modules/.vite/deps/icons-BWi2wI9n.js +42 -0
  53. package/apps/storefront/node_modules/.vite/deps/icons-BWi2wI9n.js.map +1 -0
  54. package/apps/storefront/node_modules/.vite/deps/package.json +3 -0
  55. package/apps/storefront/node_modules/.vite/deps/preact.js +310 -0
  56. package/apps/storefront/node_modules/.vite/deps/preact.js.map +1 -0
  57. package/apps/storefront/node_modules/.vite/deps/preact_compat.js +420 -0
  58. package/apps/storefront/node_modules/.vite/deps/preact_compat.js.map +1 -0
  59. package/apps/storefront/node_modules/.vite/deps/preact_compat_jsx-runtime.js +4 -0
  60. package/apps/storefront/node_modules/.vite/deps/preact_debug.js +236 -0
  61. package/apps/storefront/node_modules/.vite/deps/preact_debug.js.map +1 -0
  62. package/apps/storefront/node_modules/.vite/deps/preact_devtools.js +12 -0
  63. package/apps/storefront/node_modules/.vite/deps/preact_devtools.js.map +1 -0
  64. package/apps/storefront/node_modules/.vite/deps/preact_hooks.js +192 -0
  65. package/apps/storefront/node_modules/.vite/deps/preact_hooks.js.map +1 -0
  66. package/apps/storefront/node_modules/.vite/deps/preact_jsx-dev-runtime.js +3 -0
  67. package/apps/storefront/node_modules/.vite/deps/preact_jsx-runtime.js +92 -0
  68. package/apps/storefront/node_modules/.vite/deps/preact_jsx-runtime.js.map +1 -0
  69. package/apps/storefront/node_modules/.vite/deps/preact_test-utils.js +59 -0
  70. package/apps/storefront/node_modules/.vite/deps/preact_test-utils.js.map +1 -0
  71. package/apps/storefront/node_modules/.vite/deps/rolldown-runtime-BPOCksWG.js +24 -0
  72. package/apps/storefront/node_modules/.vite/deps/settings-BVDV0Roj.js +189 -0
  73. package/apps/storefront/node_modules/.vite/deps/settings-BVDV0Roj.js.map +1 -0
  74. package/apps/storefront/node_modules/.vite/deps/static-html-DveYqwMo.js +13 -0
  75. package/apps/storefront/node_modules/.vite/deps/static-html-DveYqwMo.js.map +1 -0
  76. package/apps/storefront/node_modules/.vite/deps/toolbar-B3VIHCXg.js +458 -0
  77. package/apps/storefront/node_modules/.vite/deps/toolbar-B3VIHCXg.js.map +1 -0
  78. package/apps/storefront/node_modules/.vite/deps/ui-library-BK6ricUd.js +1016 -0
  79. package/apps/storefront/node_modules/.vite/deps/ui-library-BK6ricUd.js.map +1 -0
  80. package/apps/storefront/node_modules/.vite/deps/window-BvxDenKN.js +32 -0
  81. package/apps/storefront/node_modules/.vite/deps/window-BvxDenKN.js.map +1 -0
  82. package/apps/storefront/node_modules/.vite/deps/xray-YFiRUU0Z.js +113 -0
  83. package/apps/storefront/node_modules/.vite/deps/xray-YFiRUU0Z.js.map +1 -0
  84. package/apps/storefront/node_modules/.vite/deps_ssr/_metadata.json +8 -0
  85. package/apps/storefront/node_modules/.vite/deps_ssr/package.json +3 -0
  86. package/apps/storefront/src/layouts/Layout.astro +1 -1
  87. package/apps/storefront/src/lib/apps/registryAccess.ts +45 -0
  88. package/apps/storefront/src/lib/auth/adminEntry.ts +4 -4
  89. package/apps/storefront/src/lib/shared/capability.ts +68 -0
  90. package/apps/storefront/src/lib/shared/developerInvites.ts +1250 -0
  91. package/apps/storefront/src/lib/shared/invitePreflight.ts +123 -0
  92. package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +38 -4
  93. package/apps/storefront/src/lib/webhooks/emit.ts +1 -1
  94. package/apps/storefront/src/lib/webhooks/getDispatcher.ts +1 -1
  95. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +1 -1
  96. package/apps/storefront/src/pages/index.astro +1 -1
  97. package/apps/storefront/src/pages/products/[handle].astro +1 -1
  98. package/package.json +1 -1
  99. package/packages/public-runtime/node_modules/.bin/tsc +43 -0
  100. package/packages/public-runtime/node_modules/.bin/tsserver +43 -0
  101. package/packages/public-runtime/node_modules/.bin/vitest +43 -0
  102. package/pnpm-workspace.yaml +3 -0
  103. package/scripts/lib/tenant-dirs.mjs +80 -0
  104. package/scripts/tenant/lint-embeds.mjs +180 -0
  105. package/scripts/tenant/materialize-tenant-content.mjs +533 -0
  106. package/scripts/tenant/validate.mjs +837 -0
  107. package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +0 -68
  108. package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +0 -111
  109. package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +0 -2
  110. package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +0 -247
  111. package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +0 -73
  112. package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +0 -79
  113. package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +0 -10982
  114. package/apps/storefront/scripts/import-v84-through-ws2.ts +0 -38
  115. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +0 -2
  116. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +0 -4
  117. package/apps/storefront/scripts/install-v84-native-through-ws1.ts +0 -185
  118. package/apps/storefront/scripts/prove-clone-isolation.ts +0 -506
  119. package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +0 -148
  120. package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +0 -35
  121. package/apps/storefront/scripts/replay-v84-clean-clone.ts +0 -214
  122. package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +0 -18
  123. package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +0 -18
  124. package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +0 -18
  125. package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +0 -17
  126. package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +0 -15
  127. package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +0 -21
  128. package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +0 -42
  129. package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +0 -145
  130. package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +0 -18
  131. package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +0 -531
  132. package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +0 -878
  133. package/apps/storefront/src/components/admin/AdminBlogTab.astro +0 -1435
  134. package/apps/storefront/src/components/admin/AdminCheckoutSettingsPanel.astro +0 -226
  135. package/apps/storefront/src/components/admin/AdminCustomersTab.astro +0 -593
  136. package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +0 -465
  137. package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +0 -105
  138. package/apps/storefront/src/components/admin/AdminProductsTab.astro +0 -901
  139. package/apps/storefront/src/components/admin/AdminPublishTab.astro +0 -2983
  140. package/apps/storefront/src/components/admin/AdminReportingTab.astro +0 -435
  141. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +0 -782
  142. package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +0 -611
  143. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +0 -1223
  144. package/apps/storefront/src/lib/apps/adminService.ts +0 -116
  145. package/apps/storefront/src/lib/apps/adminSession.ts +0 -214
  146. package/apps/storefront/src/lib/apps/apiAuth.ts +0 -91
  147. package/apps/storefront/src/lib/apps/apiRoute.ts +0 -35
  148. package/apps/storefront/src/lib/apps/catalogMapper.ts +0 -39
  149. package/apps/storefront/src/lib/apps/consentConfigStore.ts +0 -228
  150. package/apps/storefront/src/lib/apps/gtmMigrationInstall.ts +0 -447
  151. package/apps/storefront/src/lib/apps/gtmMigrationReport.ts +0 -367
  152. package/apps/storefront/src/lib/apps/gtmMigrationTab.ts +0 -315
  153. package/apps/storefront/src/lib/apps/healthAggregate.ts +0 -66
  154. package/apps/storefront/src/lib/apps/integrationLifecycle.ts +0 -614
  155. package/apps/storefront/src/lib/apps/integrationLifecycleController.ts +0 -125
  156. package/apps/storefront/src/lib/apps/integrationReadModel.ts +0 -563
  157. package/apps/storefront/src/lib/apps/integrationsTab.ts +0 -547
  158. package/apps/storefront/src/lib/apps/integrationsView.ts +0 -106
  159. package/apps/storefront/src/lib/apps/managedAnalyticsService.ts +0 -599
  160. package/apps/storefront/src/lib/apps/tenantIntegrationMaterialization.ts +0 -170
  161. package/apps/storefront/src/lib/apps/tokenIssuer.ts +0 -121
  162. package/apps/storefront/src/lib/apps/tokenVerifier.ts +0 -148
  163. package/apps/storefront/src/lib/apps/v84CleanCloneReplayContext.ts +0 -16
  164. package/apps/storefront/src/lib/apps/v84CloneLifecycleCacheEvidence.ts +0 -105
  165. package/apps/storefront/src/lib/apps/v84CloneLifecycleEvidence.ts +0 -177
  166. package/apps/storefront/src/lib/apps/v84CloneLifecycleMaterialization.ts +0 -251
  167. package/apps/storefront/src/lib/apps/v84CloneLifecycleSemantics.ts +0 -241
  168. package/apps/storefront/src/lib/apps/v84CloneMigrationEvidence.ts +0 -211
  169. package/apps/storefront/src/lib/apps/v84CloneNativeInstallEvidence.ts +0 -409
  170. package/apps/storefront/src/lib/apps/v84NonportableCodeCoaching.ts +0 -368
  171. package/apps/storefront/src/lib/apps/v84NonportableRouteContract.ts +0 -188
  172. package/apps/storefront/src/lib/apps/v84NonportableRouteDogfood.ts +0 -291
  173. package/apps/storefront/src/lib/apps/ws1ManagedAnalyticsClient.ts +0 -95
  174. package/apps/storefront/src/lib/content-edit/applyDiff.ts +0 -91
  175. package/apps/storefront/src/lib/content-edit/client.ts +0 -286
  176. package/apps/storefront/src/lib/content-edit/index.ts +0 -14
  177. package/apps/storefront/src/lib/content-edit/route.ts +0 -25
  178. package/apps/storefront/src/lib/content-edit/types.ts +0 -19
  179. package/apps/storefront/src/lib/dev/cockpitStore.ts +0 -57
  180. package/apps/storefront/src/lib/publish/adminPublishTab.ts +0 -4625
  181. package/apps/storefront/src/lib/publish/apex-readiness.ts +0 -360
  182. package/apps/storefront/src/lib/publish/buildStream.ts +0 -269
  183. package/apps/storefront/src/lib/publish/collapsePersistence.ts +0 -72
  184. package/apps/storefront/src/lib/publish/dispatchHealth.ts +0 -267
  185. package/apps/storefront/src/lib/publish/domainState.ts +0 -360
  186. package/apps/storefront/src/lib/publish/lifecycleState.ts +0 -369
  187. package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +0 -29
  188. package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +0 -78
  189. package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +0 -62
  190. package/apps/storefront/src/lib/publish/mergeRowFindings.ts +0 -124
  191. package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +0 -75
  192. package/apps/storefront/src/lib/publish/panelPolling.ts +0 -78
  193. package/apps/storefront/src/lib/publish/publish-guard.ts +0 -88
  194. package/apps/storefront/src/lib/publish/publishFormat.ts +0 -165
  195. package/apps/storefront/src/lib/publish/publishHero.ts +0 -177
  196. package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +0 -66
  197. package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +0 -141
  198. package/apps/storefront/src/lib/publish/reviewRelease/index.ts +0 -41
  199. package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +0 -47
  200. package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +0 -42
  201. package/apps/storefront/src/lib/publish/reviewRelease/types.ts +0 -59
  202. package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +0 -440
  203. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +0 -161
  204. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +0 -325
  205. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +0 -241
  206. package/apps/storefront/src/lib/publish/shipWorkspace.ts +0 -439
  207. package/apps/storefront/src/lib/publish/sseRoute.ts +0 -119
  208. package/apps/storefront/src/lib/publish/stageBundleUpload.ts +0 -215
  209. package/apps/storefront/src/lib/publish/streamLogConsole.ts +0 -142
  210. package/apps/storefront/src/lib/publish/versionNumber.ts +0 -52
  211. package/apps/storefront/src/pages/admin/ops-timeline.astro +0 -420
  212. package/apps/storefront/src/pages/admin/review-release.astro +0 -265
  213. package/apps/storefront/src/pages/admin/select.astro +0 -96
  214. package/apps/storefront/src/pages/admin.astro +0 -570
  215. package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +0 -86
  216. package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +0 -174
  217. package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +0 -96
  218. package/apps/storefront/src/pages/api/apps/admin/health.ts +0 -44
  219. package/apps/storefront/src/pages/api/apps/admin/install.ts +0 -125
  220. package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +0 -118
  221. package/apps/storefront/src/pages/api/apps/admin/integrations.ts +0 -89
  222. package/apps/storefront/src/pages/api/apps/admin/list.ts +0 -26
  223. package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +0 -85
  224. package/apps/storefront/src/pages/api/apps/admin/resume.ts +0 -79
  225. package/apps/storefront/src/pages/api/apps/admin/suspend.ts +0 -80
  226. package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +0 -98
  227. package/apps/storefront/src/pages/api/apps/admin/update.ts +0 -126
  228. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +0 -70
  229. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +0 -51
  230. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +0 -255
  231. package/apps/storefront/src/pages/api/apps/oauth/token.ts +0 -87
  232. package/apps/storefront/src/pages/api/apps/v1/attribution.ts +0 -162
  233. package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +0 -39
  234. package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +0 -49
  235. package/apps/storefront/src/pages/api/apps/v1/health.ts +0 -32
  236. package/apps/storefront/src/pages/api/apps/v1/inventory.ts +0 -59
  237. package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +0 -48
  238. package/apps/storefront/src/pages/api/apps/v1/orders.ts +0 -73
  239. package/apps/storefront/src/pages/api/apps/v1/reports.ts +0 -21
  240. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +0 -74
  241. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +0 -47
  242. package/apps/storefront/src/pages/api/cache-purge.ts +0 -55
  243. package/apps/storefront/src/pages/api/compliance/tamper.ts +0 -88
  244. package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +0 -164
  245. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +0 -154
  246. package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +0 -120
  247. package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +0 -108
  248. package/apps/storefront/src/pages/api/publish/stage-bundle.ts +0 -70
  249. package/apps/storefront/src/pages/api/test/dev-session.ts +0 -133
  250. package/apps/storefront/src/pages/cockpit.astro +0 -1548
  251. package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +0 -119
  252. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +0 -105
  253. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +0 -177
  254. package/apps/storefront/src/pages/dashboard/index.astro +0 -160
@@ -1,32 +0,0 @@
1
- /**
2
- * `GET /api/apps/v1/health` — the D4 install delivery/telemetry health read
3
- * (PrivateApps epic; `docs/private-apps/contract/openapi.yaml`'s
4
- * `getInstallHealth`). Requires no scope beyond a valid token — only ever the
5
- * caller's OWN install health (the `sub` claim `authorizeAppRequest` already
6
- * resolved), never another install's.
7
- *
8
- * `WebhookDeliveryStore.getInstallHealth`'s `InstallHealth` return already IS
9
- * the contract's `InstallHealth` shape 1:1, incl. `p95DeliveryMs` (a
10
- * never-attempted install reads as healthy/zeroed) — no mapper needed, unlike
11
- * `Delivery` below.
12
- */
13
- import type { APIContext } from "astro";
14
- import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
15
- import { json, withRequestId } from "@/lib/apps/apiRoute.js";
16
- import { WebhookDeliveryStore } from "@/lib/webhooks/deliveryStore.js";
17
- import { fromD1 } from "@/lib/d1/catalog.js";
18
- import { readD1 } from "@/lib/env";
19
-
20
- export const prerender = false;
21
-
22
- export async function GET(context: APIContext): Promise<Response> {
23
- const auth = await authorizeAppRequest(context, null);
24
- if (!auth.ok) return withRequestId(auth.response, context);
25
-
26
- const d1 = await readD1("STOREFRONT_APPS_DB");
27
- if (!d1) return withRequestId(json({ error: "webhook store not configured", code: "unconfigured" }, 503), context);
28
- const store = new WebhookDeliveryStore(fromD1(d1));
29
-
30
- const health = await store.getInstallHealth(auth.install.installId);
31
- return withRequestId(json(health), context);
32
- }
@@ -1,59 +0,0 @@
1
- /**
2
- * `GET /api/apps/v1/inventory` — the D3 inventory-availability read
3
- * (PrivateApps epic; `docs/private-apps/contract/openapi.yaml`'s
4
- * `listInventory`). Requires scope `inventory:read`.
5
- *
6
- * No new table: this projects `{sku, available, quantity?, policy?}` off the
7
- * variants embedded in the existing catalog read model
8
- * (`../../../lib/d1/catalog.js`'s `listAllActiveProducts`) — active products
9
- * only. A variant with no `sku` is skipped; the contract's `InventoryList`
10
- * item requires one, and there's nothing to key it by otherwise.
11
- */
12
- import type { APIContext } from "astro";
13
- import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
14
- import { json, withRequestId } from "@/lib/apps/apiRoute.js";
15
- import { listAllActiveProducts, fromD1 } from "@/lib/d1/catalog.js";
16
- import { readD1 } from "@/lib/env";
17
-
18
- export const prerender = false;
19
-
20
- interface InventoryItem {
21
- sku: string;
22
- available: boolean;
23
- quantity?: number;
24
- policy?: "deny" | "continue";
25
- }
26
-
27
- function toInventoryItem(policy: string | undefined, quantity: number | undefined, sku: string, available: boolean): InventoryItem {
28
- return {
29
- sku,
30
- available,
31
- quantity,
32
- policy: policy === "deny" || policy === "continue" ? policy : undefined,
33
- };
34
- }
35
-
36
- export async function GET(context: APIContext): Promise<Response> {
37
- const auth = await authorizeAppRequest(context, "inventory:read");
38
- if (!auth.ok) return withRequestId(auth.response, context);
39
-
40
- const d1 = await readD1("CATALOG_DB");
41
- if (!d1) return withRequestId(json({ error: "catalog not configured", code: "unconfigured" }, 503), context);
42
- const db = fromD1(d1);
43
-
44
- const skuFilter = new URL(context.request.url).searchParams.get("sku");
45
- const products = await listAllActiveProducts(db);
46
-
47
- const items: InventoryItem[] = [];
48
- for (const product of products) {
49
- for (const variant of product.variants) {
50
- if (!variant.sku) continue;
51
- if (skuFilter && variant.sku !== skuFilter) continue;
52
- items.push(
53
- toInventoryItem(variant.inventory.policy, variant.inventory.quantity, variant.sku, variant.inventory.available),
54
- );
55
- }
56
- }
57
-
58
- return withRequestId(json({ items }), context);
59
- }
@@ -1,48 +0,0 @@
1
- /**
2
- * `GET /api/apps/v1/orders/{id}` — the D6 minimal single-order read
3
- * (PrivateApps epic; `docs/private-apps/contract/openapi.yaml`'s `getOrder`).
4
- * Requires scope `orders:read:minimal`.
5
- *
6
- * `{id}` is tot-foxycart's `externalId` — the same id `listOrders` echoes
7
- * back as `Order.id`. There's no cross-tenant oracle here: the Foxy partner
8
- * API key is itself tenant-scoped, so a call can only ever resolve THIS
9
- * tenant's own orders — an unknown id and a different tenant's id both come
10
- * back `null` from `FoxyOrderClient.getOrder` and are reported identically
11
- * as 404.
12
- */
13
- import type { APIContext } from "astro";
14
- import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
15
- import { json, withRequestId } from "@/lib/apps/apiRoute.js";
16
- import { toContractOrder } from "@/lib/apps/orders/orderMapper.js";
17
- import { getFoxyOrderClient } from "@/lib/apps/orders/foxyOrderClient.js";
18
-
19
- export const prerender = false;
20
-
21
- export async function GET(context: APIContext): Promise<Response> {
22
- const auth = await authorizeAppRequest(context, "orders:read:minimal");
23
- if (!auth.ok) return withRequestId(auth.response, context);
24
-
25
- const id = context.params.id;
26
- if (!id) return withRequestId(json({ error: "missing order id", code: "bad_request" }, 400), context);
27
-
28
- let client;
29
- try {
30
- client = await getFoxyOrderClient();
31
- } catch {
32
- return withRequestId(json({ error: "orders not configured", code: "unconfigured" }, 503), context);
33
- }
34
-
35
- let foxyOrder;
36
- try {
37
- foxyOrder = await client.getOrder(auth.install.tenantId, id);
38
- } catch {
39
- return withRequestId(json({ error: "failed to read order", code: "upstream_error" }, 502), context);
40
- }
41
- if (!foxyOrder) return withRequestId(json({ error: "order not found", code: "not_found" }, 404), context);
42
-
43
- const order = await toContractOrder(foxyOrder, {
44
- tenantId: auth.install.tenantId,
45
- installId: auth.install.installId,
46
- });
47
- return withRequestId(json(order), context);
48
- }
@@ -1,73 +0,0 @@
1
- /**
2
- * `GET /api/apps/v1/orders` — the D6 minimal order list (PrivateApps epic;
3
- * `docs/private-apps/contract/openapi.yaml`'s `listOrders`). Requires scope
4
- * `orders:read:minimal`.
5
- *
6
- * Reads live from tot-foxycart via `FoxyOrderClient` (never a raw fetch) and
7
- * maps every row through `orderMapper.ts`'s `toContractOrder` — nothing
8
- * beyond the contract's minimized `Order` shape (customer as opaque hash, no
9
- * checkout/tax/signed-cart internals) ever leaves this route. Fail-closed:
10
- * an unconfigured Foxy client is a 503, any upstream read failure is a clean
11
- * 502 — neither leaks Foxy's own error detail.
12
- */
13
- import type { APIContext } from "astro";
14
- import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
15
- import { json, withRequestId } from "@/lib/apps/apiRoute.js";
16
- import { toContractOrder, type ContractOrderStatus } from "@/lib/apps/orders/orderMapper.js";
17
- import { getFoxyOrderClient } from "@/lib/apps/orders/foxyOrderClient.js";
18
-
19
- export const prerender = false;
20
-
21
- const DEFAULT_PAGE_SIZE = 24;
22
- const MAX_PAGE_SIZE = 100;
23
- const ORDER_STATUSES: readonly ContractOrderStatus[] = ["created", "fulfilled", "cancelled"];
24
-
25
- function parsePage(raw: string | null, fallback: number): number {
26
- const n = raw != null ? Number(raw) : NaN;
27
- return Number.isFinite(n) && n >= 1 ? Math.floor(n) : fallback;
28
- }
29
-
30
- function parseStatus(raw: string | null): ContractOrderStatus | undefined {
31
- return raw != null && (ORDER_STATUSES as readonly string[]).includes(raw) ? (raw as ContractOrderStatus) : undefined;
32
- }
33
-
34
- export async function GET(context: APIContext): Promise<Response> {
35
- const auth = await authorizeAppRequest(context, "orders:read:minimal");
36
- if (!auth.ok) return withRequestId(auth.response, context);
37
-
38
- const url = new URL(context.request.url);
39
- const page = parsePage(url.searchParams.get("page"), 1);
40
- const pageSize = Math.min(
41
- MAX_PAGE_SIZE,
42
- Math.max(1, parsePage(url.searchParams.get("pageSize"), DEFAULT_PAGE_SIZE)),
43
- );
44
- const statusFilter = parseStatus(url.searchParams.get("status"));
45
-
46
- let client;
47
- try {
48
- client = await getFoxyOrderClient();
49
- } catch {
50
- return withRequestId(json({ error: "orders not configured", code: "unconfigured" }, 503), context);
51
- }
52
-
53
- let foxyPage;
54
- try {
55
- foxyPage = await client.listOrders(auth.install.tenantId, {
56
- limit: pageSize,
57
- offset: (page - 1) * pageSize,
58
- });
59
- } catch {
60
- return withRequestId(json({ error: "failed to read orders", code: "upstream_error" }, 502), context);
61
- }
62
-
63
- const mapCtx = { tenantId: auth.install.tenantId, installId: auth.install.installId };
64
- let orders = await Promise.all(foxyPage.orders.map((order) => toContractOrder(order, mapCtx)));
65
- // tot-foxycart's own `status` query param speaks Foxy's status vocabulary, not
66
- // the contract's created|fulfilled|cancelled enum — there's no confirmed
67
- // mapping to push this filter upstream, so it's applied post-map here. This
68
- // can leave a page short of `pageSize`, and `total` still reflects Foxy's
69
- // UNFILTERED count — a known gap, flagged rather than built around.
70
- if (statusFilter) orders = orders.filter((order) => order.status === statusFilter);
71
-
72
- return withRequestId(json({ orders, page, pageSize, total: foxyPage.total }), context);
73
- }
@@ -1,21 +0,0 @@
1
- /**
2
- * `GET /api/apps/v1/reports` — the D3 report-snapshot list (PrivateApps
3
- * epic; `docs/private-apps/contract/openapi.yaml`'s `listReports`). Requires
4
- * scope `reports:read`.
5
- *
6
- * Real auth + scope enforcement, empty data: this milestone ships the
7
- * endpoint's contract shape (a valid `ReportList`) with no report snapshots
8
- * behind it yet — there is no report-generation pipeline to read from.
9
- */
10
- import type { APIContext } from "astro";
11
- import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
12
- import { json, withRequestId } from "@/lib/apps/apiRoute.js";
13
-
14
- export const prerender = false;
15
-
16
- export async function GET(context: APIContext): Promise<Response> {
17
- const auth = await authorizeAppRequest(context, "reports:read");
18
- if (!auth.ok) return withRequestId(auth.response, context);
19
-
20
- return withRequestId(json({ reports: [] }), context);
21
- }
@@ -1,74 +0,0 @@
1
- /**
2
- * `POST /api/apps/v1/webhooks/deliveries/{deliveryId}/replay` — the D4
3
- * install-owned replay (PrivateApps epic; `docs/private-apps/contract/
4
- * openapi.yaml`'s `replayWebhookDelivery`). Authorized by install ownership,
5
- * not a scope (`x-requiredScope: null`, `x-authorization: install-owns-
6
- * delivery`): an app may replay only a delivery it owns.
7
- *
8
- * Preserves the SAME `deliveryId` (the CloudEvents wire id) throughout — a
9
- * receiver that already acked a `delivered` delivery still dedupes a
10
- * replayed resend of it — via `deliveryStore.ts`'s `requeueForReplay`, which
11
- * resets the row (incl. out of the `delivered`/`dead-lettered` terminal
12
- * states) to `pending` with a fresh attempt budget so the dispatcher's
13
- * `attemptDelivery` makes a real HTTP attempt instead of hitting its
14
- * terminal-state no-op branch. See `requeueForReplay`'s own doc comment for
15
- * the full rationale.
16
- *
17
- * No `Idempotency-Key` ledger exists yet (no table backs it), so this only
18
- * validates the header's presence, per the contract's `required: true` — it
19
- * does not yet detect/409 a reused key with a mismatched body (there is no
20
- * request body on this operation to hash in the first place).
21
- */
22
- import type { APIContext } from "astro";
23
- import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
24
- import { json, withRequestId } from "@/lib/apps/apiRoute.js";
25
- import { WebhookDeliveryStore } from "@/lib/webhooks/deliveryStore.js";
26
- import { toContractDelivery } from "@/lib/webhooks/deliveryMapper.js";
27
- import { getDispatcher } from "@/lib/webhooks/getDispatcher.js";
28
- import { fromD1 } from "@/lib/d1/catalog.js";
29
- import { readD1 } from "@/lib/env";
30
- import { recordActivity } from "@/lib/activity/recordActivity";
31
-
32
- export const prerender = false;
33
-
34
- export async function POST(context: APIContext): Promise<Response> {
35
- const auth = await authorizeAppRequest(context, null);
36
- if (!auth.ok) return withRequestId(auth.response, context);
37
-
38
- if (!context.request.headers.get("idempotency-key")) {
39
- return withRequestId(json({ error: "Idempotency-Key header is required" }, 400), context);
40
- }
41
-
42
- const deliveryId = context.params.deliveryId;
43
- if (!deliveryId) {
44
- return withRequestId(json({ error: "deliveryId is required", code: "not_found" }, 404), context);
45
- }
46
-
47
- const d1 = await readD1("STOREFRONT_APPS_DB");
48
- if (!d1) return withRequestId(json({ error: "webhook store not configured", code: "unconfigured" }, 503), context);
49
- const store = new WebhookDeliveryStore(fromD1(d1));
50
-
51
- const delivery = await store.getDelivery(deliveryId);
52
- if (!delivery || delivery.installId !== auth.install.installId) {
53
- return withRequestId(json({ error: "Delivery not found.", code: "not_found" }, 404), context);
54
- }
55
-
56
- const dispatcher = await getDispatcher();
57
- if (dispatcher instanceof Response) return withRequestId(dispatcher, context);
58
-
59
- const requeued = await store.requeueForReplay(deliveryId);
60
- await dispatcher.enqueue({ deliveryId });
61
-
62
- // D11: the app-facing (install-owned, app-JWT authed) replay — the `agent` cohort. Same
63
- // catalog key as the admin console call site; `surface: "v1"` disambiguates. installId
64
- // is an opaque per-install handle; deliveryId is the opaque CloudEvents wire id.
65
- recordActivity({
66
- action: "webhook.delivery.replayed",
67
- actor: { kind: "agent", id: auth.install.installId },
68
- source: "server",
69
- outcome: { status: "succeeded" },
70
- scope: { tenantId: auth.install.tenantId },
71
- payload: { args: { deliveryId, surface: "v1" } },
72
- });
73
- return withRequestId(json(toContractDelivery(requeued), 202), context);
74
- }
@@ -1,47 +0,0 @@
1
- /**
2
- * `GET /api/apps/v1/webhooks/deliveries` — the D4 install-owned delivery
3
- * list (PrivateApps epic; `docs/private-apps/contract/openapi.yaml`'s
4
- * `listWebhookDeliveries`). Authorized by install ownership, not a scope
5
- * (`x-requiredScope: null`, `x-authorization: install-owns-delivery`): any
6
- * installed app may list ITS OWN delivery records, never another install's —
7
- * `listDeliveriesByInstall` is scoped to `auth.install.installId` from the
8
- * JWT, never a caller-supplied id.
9
- *
10
- * Delivery rows never carry a payload body (see `deliveryStore.ts`'s header),
11
- * so there is nothing sensitive to redact in `toContractDelivery`'s output.
12
- */
13
- import type { APIContext } from "astro";
14
- import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
15
- import { json, withRequestId } from "@/lib/apps/apiRoute.js";
16
- import { WebhookDeliveryStore, type DeliveryState } from "@/lib/webhooks/deliveryStore.js";
17
- import { toContractDelivery } from "@/lib/webhooks/deliveryMapper.js";
18
- import { fromD1 } from "@/lib/d1/catalog.js";
19
- import { readD1 } from "@/lib/env";
20
-
21
- export const prerender = false;
22
-
23
- const DELIVERY_STATES: readonly DeliveryState[] = ["pending", "retrying", "delivered", "dead-lettered"];
24
-
25
- /** Absent = no filter (`undefined`); an unrecognized value is a 400, not a silent no-op. */
26
- function parseStateFilter(raw: string | null): DeliveryState | undefined | "invalid" {
27
- if (raw === null) return undefined;
28
- return (DELIVERY_STATES as readonly string[]).includes(raw) ? (raw as DeliveryState) : "invalid";
29
- }
30
-
31
- export async function GET(context: APIContext): Promise<Response> {
32
- const auth = await authorizeAppRequest(context, null);
33
- if (!auth.ok) return withRequestId(auth.response, context);
34
-
35
- const state = parseStateFilter(new URL(context.request.url).searchParams.get("state"));
36
- if (state === "invalid") {
37
- return withRequestId(json({ error: "invalid state filter", code: "invalid_state_filter" }, 400), context);
38
- }
39
-
40
- const d1 = await readD1("STOREFRONT_APPS_DB");
41
- if (!d1) return withRequestId(json({ error: "webhook store not configured", code: "unconfigured" }, 503), context);
42
- const store = new WebhookDeliveryStore(fromD1(d1));
43
-
44
- const deliveries = await store.listDeliveriesByInstall(auth.install.installId, { state });
45
-
46
- return withRequestId(json({ deliveries: deliveries.map(toContractDelivery) }), context);
47
- }
@@ -1,55 +0,0 @@
1
- /**
2
- * POST /api/cache-purge — bump a tenant's edge page-cache version.
3
- *
4
- * The ETL load step calls this per tenant after a (re)migration so freshly
5
- * migrated content is visible immediately, rather than waiting out the page
6
- * TTL. This closes the loop that HttpToTClient.purgeTenant() (catalog API cache)
7
- * only half-covered: that purges the data cache; this purges the rendered HTML.
8
- *
9
- * Auth: `Authorization: Bearer <CACHE_PURGE_SECRET>`. JSON bodies are exempt from
10
- * Astro's CSRF origin check, so the server-to-server ETL caller works.
11
- */
12
- import type { APIContext } from "astro";
13
- import { readEnv, readKv } from "@/lib/env";
14
- import { bumpCacheVersion } from "@/lib/edgeCache";
15
- import { recordActivity } from "@/lib/activity/recordActivity";
16
-
17
- export const prerender = false;
18
-
19
- export async function POST(context: APIContext): Promise<Response> {
20
- const secret = await readEnv("CACHE_PURGE_SECRET");
21
- const auth = context.request.headers.get("authorization");
22
- if (!secret || auth !== `Bearer ${secret}`) {
23
- return new Response("Unauthorized", { status: 401 });
24
- }
25
-
26
- const kv = await readKv("TENANT_CACHE");
27
- if (!kv) {
28
- return new Response("No cache store bound", { status: 503 });
29
- }
30
-
31
- let tenantId: string | undefined;
32
- try {
33
- tenantId = ((await context.request.json()) as { tenantId?: string })?.tenantId;
34
- } catch {
35
- /* fall through to 400 */
36
- }
37
- if (!tenantId) {
38
- return new Response("tenantId required", { status: 400 });
39
- }
40
-
41
- const version = await bumpCacheVersion(kv, tenantId);
42
- // D11: the caller is the Bearer-secret ETL load step (a machine, no session) — emit as
43
- // the `agent` cohort with an opaque handle. Best-effort; never affects the purge.
44
- recordActivity({
45
- action: "cache.purged",
46
- actor: { kind: "agent", id: "storefront" },
47
- source: "server",
48
- outcome: { status: "succeeded" },
49
- scope: { tenantId },
50
- payload: { args: { version } },
51
- });
52
- return new Response(JSON.stringify({ tenantId, version }), {
53
- headers: { "content-type": "application/json" },
54
- });
55
- }
@@ -1,88 +0,0 @@
1
- /**
2
- * POST /api/compliance/tamper — the server receiver for protected-surface tamper
3
- * beacons (D-R7, `integration-protected-tamper-recovery`).
4
- *
5
- * The browser sentinel restores safe state IN the document and posts a
6
- * `TamperEvidenceBeacon` here. This route validates it fail-closed, records the
7
- * durable evidence + operator alert, and — when the beacon is attributable to a
8
- * single active install — SUSPENDS that install. It is a DETECTIVE control: the
9
- * suspension is after-the-fact recovery, never prevention.
10
- *
11
- * TENANT SCOPE is server-owned: the install lookup and suspend are ALWAYS keyed on
12
- * `locals.tenant` (middleware-resolved from the request host), never on the
13
- * body-supplied `tenant` — a page cannot ask to suspend another tenant's app. The
14
- * beacon body carries no secret (the contract rejects one), and the response is a
15
- * write-path ack the emitter ignores, so a failed post never breaks the page.
16
- */
17
- import type { APIContext } from "astro";
18
- import { getRegistryService, json } from "@/lib/apps/adminService.js";
19
- import type { AppInstall } from "@/lib/apps/registryService.js";
20
- import { RegistryError } from "@/lib/apps/registryService.js";
21
- import { planTamperRecovery, type AttributableInstall } from "@tot/public-runtime";
22
-
23
- export const prerender = false;
24
-
25
- const SYSTEM_ACTOR = "system:tamper-sentinel";
26
-
27
- function toAttributable(install: AppInstall): AttributableInstall {
28
- return {
29
- installId: install.installId,
30
- appId: install.appId,
31
- appVersion: install.appVersion,
32
- status: install.status,
33
- };
34
- }
35
-
36
- export async function POST(context: APIContext): Promise<Response> {
37
- const tenantId = context.locals.tenant?.tenant_id;
38
- // No resolved tenant ⇒ nothing third-party could be scoped to; accept-and-drop so
39
- // the emitter never errors or retries.
40
- if (!tenantId) return json({ ok: true, accepted: false }, 202);
41
-
42
- let body: unknown;
43
- try {
44
- body = await context.request.json();
45
- } catch {
46
- return json({ ok: false, error: "invalid JSON body" }, 400);
47
- }
48
-
49
- // Resolve the tenant's installs (empty when the registry binding isn't configured
50
- // in this runtime — the beacon is still validated + recorded, just not actioned).
51
- const registry = await getRegistryService();
52
- const installs: AttributableInstall[] =
53
- registry instanceof Response
54
- ? []
55
- : (await registry.listByTenant(tenantId)).map(toAttributable);
56
-
57
- const plan = planTamperRecovery(body, installs, {
58
- recordId: crypto.randomUUID(),
59
- receivedAt: new Date().toISOString(),
60
- });
61
- if (!plan.ok) return json({ ok: false, error: "invalid tamper beacon" }, 400);
62
-
63
- const { record, suspend } = plan.plan;
64
- // Durable, secret-free evidence line for operations. The beacon itself is the record.
65
- // eslint-disable-next-line no-console
66
- console.warn(
67
- `[tamper] tenant=${tenantId} record=${record.recordId} surface=${record.beacon.surface} kind=${record.beacon.kind} recovered=${record.beacon.recovered} app=${record.beacon.attribution?.appId ?? "unattributable"}`,
68
- );
69
-
70
- let suspended = false;
71
- if (suspend && !(registry instanceof Response)) {
72
- try {
73
- await registry.suspend(suspend.installId, SYSTEM_ACTOR);
74
- suspended = true;
75
- } catch (err) {
76
- // An already-suspended/uninstalled install (invalid_state) is a benign
77
- // no-op under rapid re-mutation; anything else is swallowed so the write
78
- // path never errors the emitter. The audit row from a successful suspend is
79
- // the durable trail.
80
- if (!(err instanceof RegistryError)) {
81
- // eslint-disable-next-line no-console
82
- console.error(`[tamper] suspend failed for ${suspend.installId}`, err);
83
- }
84
- }
85
- }
86
-
87
- return json({ ok: true, accepted: true, recovered: record.beacon.recovered, suspended }, 202);
88
- }
@@ -1,164 +0,0 @@
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
- }