@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,72 +0,0 @@
1
- /**
2
- * r05 — persist each Publish-tab collapsible section's open/closed state per user,
3
- * scoped per store so a power user's layout survives reloads without bleeding across
4
- * stores. Pure key/read/write logic, extracted from the DOM wiring in
5
- * `adminPublishTab.ts` so it is unit-testable and storage-agnostic.
6
- *
7
- * The persisted value is the human-legible "open"/"closed" (not a boolean string).
8
- * `readPersistedOpen` returns `null` when nothing is stored (or storage is
9
- * unreadable) — the caller then leaves the section at its SSR default, so the
10
- * default open/closed state is preserved when a user has never toggled a section.
11
- */
12
-
13
- /** The slice of the Web Storage API this helper needs (localStorage satisfies it). */
14
- export interface CollapseStorage {
15
- getItem(key: string): string | null;
16
- setItem(key: string, value: string): void;
17
- }
18
-
19
- /** Stable identifiers for the collapsible sections on the Publish tab. */
20
- export type CollapseSectionId =
21
- | "review-queue"
22
- | "closed-changes"
23
- | "next-release"
24
- | "live-site"
25
- | "rollback";
26
-
27
- const KEY_PREFIX = "tot.publish.collapse";
28
- const OPEN = "open";
29
- const CLOSED = "closed";
30
-
31
- /**
32
- * Per-store, per-section storage key. The store scope (appDomain) keeps one store's
33
- * layout from bleeding into another; an empty tenant collapses to a shared "_" scope
34
- * rather than producing a dangling key.
35
- */
36
- export function collapseStorageKey(tenant: string, sectionId: CollapseSectionId): string {
37
- return `${KEY_PREFIX}.${tenant || "_"}.${sectionId}`;
38
- }
39
-
40
- /**
41
- * The stored open/closed state, or `null` when nothing is stored, the value is
42
- * unrecognized, or storage throws (private mode / disabled). Never throws.
43
- */
44
- export function readPersistedOpen(
45
- storage: CollapseStorage,
46
- tenant: string,
47
- sectionId: CollapseSectionId,
48
- ): boolean | null {
49
- let raw: string | null;
50
- try {
51
- raw = storage.getItem(collapseStorageKey(tenant, sectionId));
52
- } catch {
53
- return null;
54
- }
55
- if (raw === OPEN) return true;
56
- if (raw === CLOSED) return false;
57
- return null;
58
- }
59
-
60
- /** Persist a section's open/closed state. Swallows storage errors (never throws). */
61
- export function writePersistedOpen(
62
- storage: CollapseStorage,
63
- tenant: string,
64
- sectionId: CollapseSectionId,
65
- open: boolean,
66
- ): void {
67
- try {
68
- storage.setItem(collapseStorageKey(tenant, sectionId), open ? OPEN : CLOSED);
69
- } catch {
70
- /* storage unavailable (private mode / disabled) — persistence is best-effort */
71
- }
72
- }
@@ -1,267 +0,0 @@
1
- /**
2
- * Dispatch-health assessment — is the static-publish `repository_dispatch`
3
- * path actually WIRED and HEALTHY, or would a "go-live" silently hand the publish
4
- * to a dead-end? The Worker fires a `repository_dispatch` and gets a 204 back
5
- * having verified NOTHING about the target (see `createRepositoryDispatchSink` —
6
- * it is fire-and-forget by construction). This module is the HONEST pre-flight
7
- * surface that answers whether that path can carry a publish at all — reporting each
8
- * concrete signal pass/fail/unknown with a reason, never a silent assumption.
9
- *
10
- * FOUR SIGNALS:
11
- * - `dispatch-cred-present` — is WWW_PUBLISH_DISPATCH_TOKEN set?
12
- * - `target-config-present` — does this tenant declare a static
13
- * target AND does the dispatch repo resolve?
14
- * - `workflow-registered-on-default-branch` — is the publish workflow visible to
15
- * the Actions API? `repository_dispatch`
16
- * ONLY triggers workflows on the repo's
17
- * DEFAULT branch (main), so a workflow
18
- * that lives only on the deploy branch is
19
- * NOT yet reachable. A 404 here is the
20
- * honest "not yet promoted to main" state
21
- * (the HELD main-promotion gate) —
22
- * surfaced, not papered.
23
- * - `target-reachable` — does the published `live/manifest.json`
24
- * answer? DIAGNOSTIC ONLY: a pre-first-
25
- * publish target legitimately has none, so
26
- * this never blocks a ship (the post-merge
27
- * VERIFY in the orchestrator is the real
28
- * no-false-live gate).
29
- *
30
- * FAIL-CLOSED SEMANTICS:
31
- * - `healthy` is true ONLY when every SHIP-CRITICAL signal (cred, target-config,
32
- * workflow-registration) is `pass`. An `unknown` is NOT healthy.
33
- * - `blockShip` is true only for a DEFINITIVELY-bad critical precondition (cred
34
- * missing, target-config missing, or a workflow 404). An `unknown` (e.g. the
35
- * Actions API is unreachable, or the token lacks read scope) does NOT set
36
- * `blockShip` — so GitHub reachability is never a hard dependency of every
37
- * accept; the orchestrator's post-merge verify still fails closed regardless.
38
- */
39
-
40
- /** A single signal's honest state. `unknown` = we genuinely could not determine it. */
41
- export type DispatchHealthStatus = "pass" | "fail" | "unknown";
42
-
43
- export type DispatchHealthSignalId =
44
- | "dispatch-cred-present"
45
- | "target-config-present"
46
- | "workflow-registered-on-default-branch"
47
- | "target-reachable";
48
-
49
- export interface DispatchHealthSignal {
50
- id: DispatchHealthSignalId;
51
- status: DispatchHealthStatus;
52
- /** Human/audit-legible reason for the status. */
53
- detail: string;
54
- /** Whether this signal gates a ship (false = diagnostic only, e.g. target-reachable). */
55
- critical: boolean;
56
- }
57
-
58
- export interface DispatchHealthReport {
59
- /** True ONLY when every ship-critical signal is `pass` (fail-closed: unknown ⇒ not healthy). */
60
- healthy: boolean;
61
- /**
62
- * True when a ship should be REFUSED before merge because a critical precondition
63
- * is DEFINITIVELY bad (never for an `unknown`). Consumed by the ship path so a
64
- * static go-live never merges into a dead-end that could report a false live.
65
- */
66
- blockShip: boolean;
67
- /** The one-line honest reason `blockShip` is set, else null. */
68
- blockReason: string | null;
69
- signals: DispatchHealthSignal[];
70
- }
71
-
72
- export interface AssessDispatchHealthInput {
73
- /** WWW_PUBLISH_DISPATCH_TOKEN (presence only; the value never leaves env). */
74
- dispatchToken: string | null;
75
- /** `owner/repo` hosting the publish workflow (WWW_PUBLISH_DISPATCH_REPO). */
76
- repoSlug: string | null;
77
- /** The `repository_dispatch` event type (informational — surfaced for audit). */
78
- eventType: string;
79
- /** GitHub API base (default https://api.github.com); set for Enterprise. */
80
- apiBase?: string;
81
- /** The workflow file name the Actions API is probed for (e.g. "www-publish.yml"). */
82
- workflowFile: string;
83
- /** The tenant's static-publish target label when enabled, else null. */
84
- staticPublishTarget: string | null;
85
- /** Published-manifest URL for the `target-reachable` probe, or null when unresolved. */
86
- manifestUrl: string | null;
87
- /** Injectable fetch (Worker/Node global by default) — so this is unit-tested with no network. */
88
- fetchImpl?: typeof fetch;
89
- }
90
-
91
- /** Parse `owner/repo`, returning null when it isn't a clean two-part slug. */
92
- export function parseRepoSlug(slug: string | null): { owner: string; repo: string } | null {
93
- if (!slug) return null;
94
- const [owner, repo] = slug.split("/", 2);
95
- if (!owner || !repo) return null;
96
- return { owner, repo };
97
- }
98
-
99
- /**
100
- * Probe the Actions API for the publish workflow. A 200 means GitHub has the
101
- * workflow REGISTERED — which only happens once it is on the repo's DEFAULT
102
- * branch, so `repository_dispatch` can trigger it. A 404 is the honest "not on the
103
- * default branch yet" state. A 401/403/other/network error is `unknown` (we can't
104
- * tell — often the token has write-only scope), NEVER a false pass or a hard block.
105
- */
106
- async function probeWorkflowRegistered(
107
- input: AssessDispatchHealthInput,
108
- repo: { owner: string; repo: string },
109
- ): Promise<DispatchHealthSignal> {
110
- const base = {
111
- id: "workflow-registered-on-default-branch" as const,
112
- critical: true,
113
- };
114
- if (!input.dispatchToken) {
115
- return { ...base, status: "unknown", detail: "cannot probe: dispatch credential absent" };
116
- }
117
- const apiBase = (input.apiBase ?? "https://api.github.com").replace(/\/+$/, "");
118
- const url = `${apiBase}/repos/${repo.owner}/${repo.repo}/actions/workflows/${encodeURIComponent(input.workflowFile)}`;
119
- const doFetch = input.fetchImpl ?? fetch;
120
- let res: Response;
121
- try {
122
- res = await doFetch(url, {
123
- headers: {
124
- authorization: `Bearer ${input.dispatchToken}`,
125
- accept: "application/vnd.github+json",
126
- "user-agent": "tot-storefront-dispatch-health",
127
- },
128
- });
129
- } catch (e) {
130
- return { ...base, status: "unknown", detail: `Actions API unreachable: ${(e as Error).message}` };
131
- }
132
- if (res.status === 200) {
133
- return {
134
- ...base,
135
- status: "pass",
136
- detail: `${input.workflowFile} is registered on the default branch (repository_dispatch can trigger it)`,
137
- };
138
- }
139
- if (res.status === 404) {
140
- return {
141
- ...base,
142
- status: "fail",
143
- detail:
144
- `${input.workflowFile} is NOT registered on ${repo.owner}/${repo.repo}'s default branch ` +
145
- `(main) — repository_dispatch cannot trigger it until it is promoted to main ` +
146
- `(pending main-promotion gate).`,
147
- };
148
- }
149
- if (res.status === 401 || res.status === 403) {
150
- return {
151
- ...base,
152
- status: "unknown",
153
- detail: `Actions API returned HTTP ${res.status} — dispatch token lacks read scope; registration cannot be confirmed`,
154
- };
155
- }
156
- return { ...base, status: "unknown", detail: `Actions API returned HTTP ${res.status}` };
157
- }
158
-
159
- /**
160
- * Probe the published-manifest URL. DIAGNOSTIC ONLY (`critical: false`): a
161
- * pre-first-publish target has no manifest yet, which is a legitimate bootstrap
162
- * state, so this never gates a ship.
163
- */
164
- async function probeTargetReachable(input: AssessDispatchHealthInput): Promise<DispatchHealthSignal> {
165
- const base = { id: "target-reachable" as const, critical: false };
166
- if (!input.manifestUrl) {
167
- return { ...base, status: "unknown", detail: "no published-manifest URL resolved for this target" };
168
- }
169
- const doFetch = input.fetchImpl ?? fetch;
170
- let res: Response;
171
- try {
172
- res = await doFetch(input.manifestUrl, { headers: { accept: "application/json" } });
173
- } catch (e) {
174
- return { ...base, status: "unknown", detail: `manifest fetch failed: ${(e as Error).message}` };
175
- }
176
- if (!res.ok) {
177
- return {
178
- ...base,
179
- status: res.status === 404 ? "fail" : "unknown",
180
- detail: `manifest HTTP ${res.status}${res.status === 404 ? " (target not yet published — bootstrap)" : ""}`,
181
- };
182
- }
183
- let hasDigest = false;
184
- try {
185
- const manifest = (await res.json()) as { treeDigest?: unknown };
186
- hasDigest = typeof manifest.treeDigest === "string" && manifest.treeDigest.length > 0;
187
- } catch {
188
- return { ...base, status: "fail", detail: "manifest was not valid JSON" };
189
- }
190
- return {
191
- ...base,
192
- status: "pass",
193
- detail: hasDigest ? "target serves a published manifest with a treeDigest" : "target reachable (manifest carries no treeDigest yet)",
194
- };
195
- }
196
-
197
- /**
198
- * Assess the dispatch path's health. Runs the two network probes concurrently.
199
- * Pure w.r.t. its inputs — the caller resolves env/config and injects `fetchImpl`.
200
- */
201
- export async function assessDispatchHealth(
202
- input: AssessDispatchHealthInput,
203
- ): Promise<DispatchHealthReport> {
204
- const repo = parseRepoSlug(input.repoSlug);
205
-
206
- const credSignal: DispatchHealthSignal = input.dispatchToken
207
- ? {
208
- id: "dispatch-cred-present",
209
- status: "pass",
210
- detail: "WWW_PUBLISH_DISPATCH_TOKEN is configured",
211
- critical: true,
212
- }
213
- : {
214
- id: "dispatch-cred-present",
215
- status: "fail",
216
- detail: "WWW_PUBLISH_DISPATCH_TOKEN is not set — the Worker cannot fire repository_dispatch",
217
- critical: true,
218
- };
219
-
220
- const targetConfigSignal: DispatchHealthSignal =
221
- input.staticPublishTarget && repo
222
- ? {
223
- id: "target-config-present",
224
- status: "pass",
225
- detail: `static target "${input.staticPublishTarget}" → dispatch repo ${repo.owner}/${repo.repo} (event ${input.eventType})`,
226
- critical: true,
227
- }
228
- : {
229
- id: "target-config-present",
230
- status: "fail",
231
- detail: !input.staticPublishTarget
232
- ? "tenant declares no enabled static-publish target"
233
- : "dispatch repo (WWW_PUBLISH_DISPATCH_REPO) does not resolve to owner/repo",
234
- critical: true,
235
- };
236
-
237
- // The workflow probe needs a resolved repo; without one it can't be evaluated.
238
- const workflowSignal: DispatchHealthSignal = repo
239
- ? await probeWorkflowRegistered(input, repo)
240
- : {
241
- id: "workflow-registered-on-default-branch",
242
- status: "unknown",
243
- detail: "cannot probe: dispatch repo does not resolve",
244
- critical: true,
245
- };
246
-
247
- const reachableSignal = await probeTargetReachable(input);
248
-
249
- const signals: DispatchHealthSignal[] = [
250
- credSignal,
251
- targetConfigSignal,
252
- workflowSignal,
253
- reachableSignal,
254
- ];
255
-
256
- const critical = signals.filter((s) => s.critical);
257
- const healthy = critical.every((s) => s.status === "pass");
258
- // Block a ship only for a DEFINITIVELY-bad critical precondition — never an unknown.
259
- const blocker = critical.find((s) => s.status === "fail") ?? null;
260
-
261
- return {
262
- healthy,
263
- blockShip: blocker !== null,
264
- blockReason: blocker ? `${blocker.id}: ${blocker.detail}` : null,
265
- signals,
266
- };
267
- }
@@ -1,360 +0,0 @@
1
- /**
2
- * Apex domain-connect state + dispatch.
3
- *
4
- * The admin Publish tab's "Domain" section drives the apex DNS cutover
5
- * (tokenoftrust.com → CloudFront) and its rollback as PRODUCT actions, not a
6
- * terminal runbook. The Worker holds no AWS credentials: it fires the SAME
7
- * `repository_dispatch` seam www-publish uses (`event_type=www-domain`,
8
- * `client_payload.action: connect | rollback`) and the CI workflow (OIDC role)
9
- * runs the existing scripts. This module owns:
10
- *
11
- * - the per-tenant domain-cutover STATE record (KV) — current state
12
- * (`pantheon` | `cloudfront`), prior-DNS-records capture pointer, and the
13
- * last/in-flight run;
14
- * - the PURE state transitions (`beginDomainRun` / `completeDomainRun`) so
15
- * the refusal + completion policy is unit-testable without I/O;
16
- * - the dispatch transport (`createDomainDispatch`) mirroring
17
- * `createRepositoryDispatchSink`'s contract (throw on non-2xx).
18
- *
19
- * STATUS-BACK CHOICE (handoff asks to pick one and say why): CI posts a
20
- * completion CALLBACK to the Worker (`POST /api/domain/callback`, HMAC over the
21
- * raw body — the same webhook trust shape as `/api/preview/reconcile`), rather
22
- * than the Worker polling GitHub run status. Why: the dispatch token is a
23
- * write-only fine-grained credential (actions:write) and the Worker should not
24
- * grow a GitHub READ credential + polling loop when the reconcile webhook trust
25
- * model already exists and CI knows exactly when it finished.
26
- */
27
-
28
- /** Where the apex DNS points right now. `pantheon` is the pre-cutover host. */
29
- export type DomainPlatformState = "pantheon" | "cloudfront";
30
-
31
- export type DomainAction = "connect" | "rollback";
32
-
33
- /** CI callbacks are terminal-only (succeeded|failed); "dispatched" is the in-flight state. */
34
- export type DomainRunStatus = "dispatched" | "succeeded" | "failed";
35
-
36
- /** One connect/rollback run — audit fields included (who/when, accept.ts-style). */
37
- export interface DomainRun {
38
- /** The dispatch correlation id — `dispatchId` on the wire (both directions). */
39
- runId: string;
40
- action: DomainAction;
41
- /** Dry-run rehearsal (cutover-dns.mjs --dry-run in CI) — never moves state. */
42
- rehearsal: boolean;
43
- status: DomainRunStatus;
44
- /** Audit: the acting operator (viewer email / owner), never from the body. */
45
- actor: string;
46
- startedAt: string;
47
- finishedAt: string | null;
48
- /** CI run link once known (from the completion callback). */
49
- runUrl: string | null;
50
- /** Legible failure detail, or null. */
51
- error: string | null;
52
- }
53
-
54
- /** The per-tenant domain-cutover state record (KV, `www-domain:state:<tenantId>`). */
55
- export interface DomainState {
56
- state: DomainPlatformState;
57
- /** When the current `state` took effect (ISO), or null if never cutover. */
58
- since: string | null;
59
- /**
60
- * S3 key of the captured prior DNS records powering rollback
61
- * (`meta/dns-prior-records.<ts>.json`) + when they were captured. Written by
62
- * CI (which does the capture) via the completion callback.
63
- */
64
- priorRecordsKey: string | null;
65
- priorCapturedAt: string | null;
66
- lastRun: DomainRun | null;
67
- }
68
-
69
- /** KV key for a tenant's domain-cutover state. */
70
- export function domainStateKey(tenantId: string): string {
71
- return `www-domain:state:${tenantId}`;
72
- }
73
-
74
- /** The starting state for a tenant that has never been cutover. */
75
- export function initialDomainState(): DomainState {
76
- return {
77
- state: "pantheon",
78
- since: null,
79
- priorRecordsKey: null,
80
- priorCapturedAt: null,
81
- lastRun: null,
82
- };
83
- }
84
-
85
- /** Minimal KV surface (matches the `CandidateKv` shape used across the repo). */
86
- export interface DomainStateKv {
87
- get(key: string): Promise<string | null>;
88
- put(key: string, value: string): Promise<void>;
89
- }
90
-
91
- /** Read a tenant's domain state; a missing/corrupt record is the initial state. */
92
- export async function readDomainState(
93
- kv: DomainStateKv,
94
- tenantId: string,
95
- ): Promise<DomainState> {
96
- const raw = await kv.get(domainStateKey(tenantId));
97
- if (!raw) return initialDomainState();
98
- try {
99
- const parsed = JSON.parse(raw) as DomainState;
100
- if (parsed.state !== "pantheon" && parsed.state !== "cloudfront") {
101
- return initialDomainState();
102
- }
103
- return parsed;
104
- } catch {
105
- return initialDomainState();
106
- }
107
- }
108
-
109
- export async function writeDomainState(
110
- kv: DomainStateKv,
111
- tenantId: string,
112
- state: DomainState,
113
- ): Promise<void> {
114
- await kv.put(domainStateKey(tenantId), JSON.stringify(state));
115
- }
116
-
117
- export type DomainTransitionResult =
118
- | { ok: true; state: DomainState }
119
- | { ok: false; status: number; error: string };
120
-
121
- /** True when the state's last run is still in flight (awaiting the CI callback). */
122
- export function runInFlight(state: DomainState): boolean {
123
- return state.lastRun?.status === "dispatched";
124
- }
125
-
126
- /**
127
- * PURE: begin a connect/rollback run. Refuses (409) while a run is in flight —
128
- * two concurrent DNS mutations is exactly the race this state exists to prevent.
129
- * Also refuses (409) a non-rehearsal connect when already on `cloudfront`, and a
130
- * rollback when there is no captured prior-records pointer to restore from
131
- * (nothing to roll back TO — CI would have nothing to apply).
132
- */
133
- export function beginDomainRun(
134
- state: DomainState,
135
- run: {
136
- runId: string;
137
- action: DomainAction;
138
- rehearsal: boolean;
139
- actor: string;
140
- at: string;
141
- },
142
- ): DomainTransitionResult {
143
- if (runInFlight(state)) {
144
- return {
145
- ok: false,
146
- status: 409,
147
- error: `a ${state.lastRun?.action} run is already in flight (${state.lastRun?.runId})`,
148
- };
149
- }
150
- if (run.action === "connect" && !run.rehearsal && state.state === "cloudfront") {
151
- return {
152
- ok: false,
153
- status: 409,
154
- error: "domain is already connected (state: cloudfront)",
155
- };
156
- }
157
- if (run.action === "rollback" && !run.rehearsal && !state.priorRecordsKey) {
158
- return {
159
- ok: false,
160
- status: 409,
161
- error: "no captured prior DNS records to roll back to",
162
- };
163
- }
164
- return {
165
- ok: true,
166
- state: {
167
- ...state,
168
- lastRun: {
169
- runId: run.runId,
170
- action: run.action,
171
- rehearsal: run.rehearsal,
172
- status: "dispatched",
173
- actor: run.actor,
174
- startedAt: run.at,
175
- finishedAt: null,
176
- runUrl: null,
177
- error: null,
178
- },
179
- },
180
- };
181
- }
182
-
183
- /**
184
- * The CI completion callback (wire contract with the www-domain workflow,
185
- * branch issue/www-publish-auto-path). Terminal-only: succeeded | failed.
186
- * Correlated on `dispatchId` — the id WE minted at dispatch time and sent in
187
- * `client_payload.dispatchId`; the workflow's `runId`/`runUrl` identify the GH
188
- * Actions run and are recorded/surfaced, never used for correlation.
189
- */
190
- export interface DomainRunCallback {
191
- /** Round-trips from `client_payload.dispatchId` — the correlation key. */
192
- dispatchId: string;
193
- status: "succeeded" | "failed";
194
- /** CI-reported resulting platform state ("unknown" → infer from the action). */
195
- state?: "cloudfront" | "pantheon" | "unknown" | null;
196
- /** GH Actions run link — surfaced in the UI status. */
197
- runUrl?: string | null;
198
- error?: string | null;
199
- /** On a successful connect: where CI persisted the prior-records capture. */
200
- priorKey?: string | null;
201
- completedAt?: string | null;
202
- }
203
-
204
- /**
205
- * PURE: apply a CI completion callback. A callback for an unknown `dispatchId`
206
- * is refused (409) — it belongs to a run this state never dispatched (or one
207
- * that was superseded), and applying it would corrupt the record.
208
- *
209
- * On a SUCCESSFUL non-rehearsal (non-dryRun) run the platform state flips to
210
- * the CI-reported `state` when concrete, else inferred from the action:
211
- * connect → `cloudfront` (+ prior-records pointer from `priorKey`; never
212
- * overwrite an existing pointer with null — the FIRST capture is
213
- * the true pre-platform state)
214
- * rollback → `pantheon`
215
- * Rehearsal (wire: dryRun) runs never move `state` — but a rehearsal CONNECT
216
- * that reports a `priorKey` DOES record the revert target (prior-records pointer),
217
- * because persisting a metadata capture mutates no live DNS, and it's what lets
218
- * the apex-cutover readiness gate's "revert captured" precondition go green BEFORE
219
- * the real cutover. Capturing the revert target is decoupled from moving state.
220
- */
221
- export function completeDomainRun(
222
- state: DomainState,
223
- cb: DomainRunCallback,
224
- at: string,
225
- ): DomainTransitionResult {
226
- const run = state.lastRun;
227
- if (!run || run.runId !== cb.dispatchId) {
228
- return {
229
- ok: false,
230
- status: 409,
231
- error: `callback for unknown dispatch "${cb.dispatchId}"`,
232
- };
233
- }
234
- if (run.status === "succeeded" || run.status === "failed") {
235
- return {
236
- ok: false,
237
- status: 409,
238
- error: `dispatch "${cb.dispatchId}" already completed (${run.status})`,
239
- };
240
- }
241
-
242
- const updatedRun: DomainRun = {
243
- ...run,
244
- status: cb.status,
245
- runUrl: cb.runUrl ?? run.runUrl,
246
- error: cb.status === "failed" ? (cb.error ?? "run failed") : null,
247
- finishedAt: cb.completedAt ?? at,
248
- };
249
-
250
- let next: DomainState = { ...state, lastRun: updatedRun };
251
- if (cb.status === "succeeded") {
252
- // Platform state flips only on a REAL (non-rehearsal) run — a rehearsal
253
- // changes no DNS, so it must never claim the apex moved.
254
- if (!run.rehearsal) {
255
- const reported =
256
- cb.state === "cloudfront" || cb.state === "pantheon" ? cb.state : null;
257
- next = {
258
- ...next,
259
- state: reported ?? (run.action === "connect" ? "cloudfront" : "pantheon"),
260
- since: updatedRun.finishedAt,
261
- };
262
- }
263
- // Capturing the revert target is DECOUPLED from moving state: any successful
264
- // connect that carries a priorKey records it — a rehearsal included — so the
265
- // readiness gate can confirm a tested rollback before the real cutover. Never
266
- // regress an existing pointer to null (the FIRST capture is the true origin).
267
- if (run.action === "connect") {
268
- next = {
269
- ...next,
270
- priorRecordsKey: cb.priorKey ?? state.priorRecordsKey,
271
- priorCapturedAt: cb.priorKey
272
- ? (cb.completedAt ?? at)
273
- : state.priorCapturedAt,
274
- };
275
- }
276
- }
277
- return { ok: true, state: next };
278
- }
279
-
280
- /** Config for the `www-domain` repository_dispatch (env-sourced, never tenant config). */
281
- export interface DomainDispatchConfig {
282
- apiBase?: string;
283
- owner: string;
284
- repo: string;
285
- /** `repository_dispatch` event type; the workflow shim routes on it. */
286
- eventType: string;
287
- /** Bearer token with actions:write — never logged. */
288
- token: string;
289
- fetchImpl?: typeof fetch;
290
- }
291
-
292
- /**
293
- * The `client_payload` wire contract shared with the www-domain CI workflow
294
- * (branch issue/www-publish-auto-path). The CI shim GATES on the presence/shape
295
- * of `tenantId`, `action`, `dispatchId`, `callbackUrl`, and `dryRun`; the
296
- * remaining fields are ADDITIONAL keys (audit-only, tolerated by the shim).
297
- */
298
- export interface DomainDispatchPayload {
299
- tenantId: string;
300
- action: DomainAction;
301
- /** Correlation id — round-trips back as the callback's `dispatchId`. */
302
- dispatchId: string;
303
- /** Absolute plain-https URL of /api/domain/callback (CI gates: https, no query/userinfo). */
304
- callbackUrl: string;
305
- /** The rehearsal flag on the wire. */
306
- dryRun: boolean;
307
- // --- additional (non-gated) audit keys ---
308
- appDomain: string;
309
- /** Audit only — CI echoes it back; authorization happened Worker-side. */
310
- actor: string;
311
- dispatchedAt: string;
312
- }
313
-
314
- /**
315
- * Fire the `www-domain` repository_dispatch. Mirrors
316
- * `createRepositoryDispatchSink` (publish-sink.ts): a non-2xx response or a
317
- * transport error THROWS, so the caller never records a dispatched run that was
318
- * never actually dispatched.
319
- */
320
- export function createDomainDispatch(config: DomainDispatchConfig) {
321
- const apiBase = (config.apiBase ?? "https://api.github.com").replace(/\/+$/, "");
322
- const doFetch = config.fetchImpl ?? fetch;
323
- return {
324
- async dispatch(payload: DomainDispatchPayload): Promise<void> {
325
- const url = `${apiBase}/repos/${config.owner}/${config.repo}/dispatches`;
326
- let res: Response;
327
- try {
328
- res = await doFetch(url, {
329
- method: "POST",
330
- headers: {
331
- authorization: `Bearer ${config.token}`,
332
- accept: "application/vnd.github+json",
333
- "content-type": "application/json",
334
- "user-agent": "tot-storefront-domain-dispatch",
335
- },
336
- body: JSON.stringify({
337
- event_type: config.eventType,
338
- client_payload: payload,
339
- }),
340
- });
341
- } catch (e) {
342
- throw new Error(
343
- `domain dispatch to ${config.owner}/${config.repo} failed: ${(e as Error).message}`,
344
- );
345
- }
346
- if (!res.ok) {
347
- let detail = "";
348
- try {
349
- detail = (await res.text()).slice(0, 500);
350
- } catch {
351
- /* keep the HTTP status */
352
- }
353
- throw new Error(
354
- `domain dispatch to ${config.owner}/${config.repo} returned HTTP ${res.status}` +
355
- (detail ? `: ${detail}` : ""),
356
- );
357
- }
358
- },
359
- };
360
- }