@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,599 +0,0 @@
1
- /**
2
- * Managed analytics API v1 facade over the canonical app registry authorities.
3
- * Installs live in `app_installs`; live versions in `app_version_snapshots`;
4
- * candidate history in `app_audit_log`; inert drafts in their narrow store;
5
- * retries in the existing `idempotency_keys` ledger.
6
- */
7
- import {
8
- MANAGED_ANALYTICS_API_VERSION, MANAGED_ANALYTICS_SCHEMA_VERSION,
9
- getPlatformAdapter, platformAdapterCatalog, sha256Hex,
10
- buildPlatformAdapterDeclaration, validateIntegrationManifest,
11
- resolveEffectivePlan, compileIntegrationCspFromPlan,
12
- collectDeclaredCspProfiles, ENVIRONMENT_SCHEMA_VERSION,
13
- validateManagedAnalyticsSafePayload,
14
- validateApiErrorEnvelope, validateIntegrationInstall,
15
- validateAdapterDefinition, validateIntegrationRevisionV1, validateMigrationDraftV1,
16
- validateCreateMigrationDraftRequest, validateUpdateMigrationDraftRequest,
17
- validateDeleteMigrationDraftRequest, validateCreateIntegrationInstallRequest,
18
- validateReviseIntegrationInstallRequest, validateSetIntegrationInstallStatusRequest,
19
- validateDeleteIntegrationInstallRequest,
20
- snapshotManagedAnalyticsDeclarativeInput,
21
- type AdapterConfigValue, type AdapterDefinition, type ApiErrorEnvelope,
22
- type ApiResult, type CreateIntegrationInstallRequest,
23
- type CreateMigrationDraftRequest, type DeleteIntegrationInstallRequest,
24
- type DeleteMigrationDraftRequest, type IntegrationInstall,
25
- type IntegrationRevisionV1, type ListAdapterDefinitionsRequest,
26
- type ListIntegrationInstallsRequest, type MigrationDraftV1,
27
- type MutationResult, type ReviseIntegrationInstallRequest,
28
- type SetIntegrationInstallStatusRequest, type UpdateMigrationDraftRequest,
29
- type IntegrationManifest,
30
- } from "@tot/public-runtime";
31
- import type { Queryable, QueryStatement } from "../d1/catalog.js";
32
- import { AppVersionStore, type AppVersionFacets } from "./appVersionStore.js";
33
-
34
- export type ManagedAnalyticsOperation =
35
- | "listAdapterDefinitions" | "getAdapterDefinition"
36
- | "listIntegrationInstalls" | "getIntegrationInstall"
37
- | "listIntegrationRevisions" | "getIntegrationRevision"
38
- | "getMigrationDraft" | "createMigrationDraft" | "updateMigrationDraft"
39
- | "deleteMigrationDraft" | "createIntegrationInstall"
40
- | "reviseIntegrationInstall" | "setIntegrationInstallStatus"
41
- | "deleteIntegrationInstall";
42
-
43
- export const MANAGED_ANALYTICS_READ_OPERATIONS: ReadonlySet<ManagedAnalyticsOperation> = new Set([
44
- "listAdapterDefinitions", "getAdapterDefinition", "listIntegrationInstalls",
45
- "getIntegrationInstall", "listIntegrationRevisions", "getIntegrationRevision",
46
- "getMigrationDraft",
47
- ]);
48
-
49
- export interface ManagedAnalyticsActorContext { tenantId: string; actor: string }
50
- interface Effects { digest: string; cspOrigins: string[]; subscriptions: string[]; cacheIdentity: string }
51
- interface CandidateRecord { revision: IntegrationRevisionV1; digest: string; effects: Effects; approvedAt: string }
52
- interface StoredCandidate extends CandidateRecord {
53
- manifest: IntegrationManifest;
54
- facets: AppVersionFacets;
55
- /** Immutable activated snapshot selected by a revise-to-prior-config rollback. */
56
- rollbackFromSeq?: number;
57
- }
58
- interface InstallRow {
59
- install_id: string; app_id: string; app_version: string; tenant_id: string;
60
- env: string; status: string; scopes: string; manifest: string;
61
- webhook_endpoint: string | null; widget_placements: string; install_actor: string;
62
- managed_kind: string | null; managed_revision: number; managed_active_revision_id: string | null;
63
- managed_candidate: string | null;
64
- created_at: string; updated_at: string;
65
- }
66
- interface AuditRow { id: string; event: string; detail: string; at: string }
67
- interface DraftRow { draft_id: string; tenant_id: string; revision: number; state: string; record: string; created_at: string; updated_at: string }
68
- interface IdempotencyRow { body_hash: string; response_body: string }
69
- type CandidateValidation =
70
- | { ok: true; effects: Effects; facets: AppVersionFacets; manifest: IntegrationManifest }
71
- | { ok: false; details: Array<{ path: string; code: string; message: string }> };
72
- interface MutationPlan<T> {
73
- response: MutationResult<T>;
74
- statements: QueryStatement[];
75
- casStillCurrent?: () => Promise<boolean>;
76
- }
77
-
78
- const KIND = "managed-analytics-v1";
79
- /**
80
- * Preserve UUID entropy while preventing any contract-visible identifier from
81
- * accidentally resembling a phone/card number to the frozen value canary.
82
- * Underscores are PUBLIC_ID-safe and split every hexadecimal run at four chars.
83
- */
84
- export function managedAnalyticsOpaqueId(prefix: string, uuid = crypto.randomUUID()): string {
85
- const hex = uuid.replaceAll("-", "");
86
- if (!/^[0-9a-f]{32}$/i.test(hex)) throw new TypeError("Managed analytics entropy must be a UUID.");
87
- return `${prefix}_${hex.match(/.{1,4}/g)!.join("_")}`;
88
- }
89
- function requestId(): string { return managedAnalyticsOpaqueId("req"); }
90
- function publicId(prefix: string): string { return managedAnalyticsOpaqueId(prefix); }
91
- function stableValue(value: unknown): unknown {
92
- if (Array.isArray(value)) return value.map(stableValue);
93
- if (value && typeof value === "object") return Object.fromEntries(
94
- Object.entries(value as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b))
95
- .map(([key, child]) => [key, stableValue(child)]),
96
- );
97
- return value;
98
- }
99
- function canonicalJson(value: unknown): string { return JSON.stringify(stableValue(value)); }
100
- function record(value: unknown): Record<string, unknown> | null {
101
- return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
102
- }
103
- function exactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
104
- return canonicalJson(Object.keys(value).sort()) === canonicalJson([...keys].sort());
105
- }
106
- function validatesReplayData(operation: ManagedAnalyticsOperation, data: unknown): boolean {
107
- if (operation === "createMigrationDraft" || operation === "updateMigrationDraft") {
108
- return validateMigrationDraftV1(data).ok;
109
- }
110
- if (operation === "setIntegrationInstallStatus") return validateIntegrationInstall(data).ok;
111
- if (operation === "createIntegrationInstall" || operation === "reviseIntegrationInstall") {
112
- const value = record(data);
113
- return !!value && exactKeys(value, ["install", "revision"]) &&
114
- validateIntegrationInstall(value["install"]).ok && validateIntegrationRevisionV1(value["revision"]).ok;
115
- }
116
- if (operation === "deleteMigrationDraft" || operation === "deleteIntegrationInstall") {
117
- const value = record(data);
118
- return !!value && exactKeys(value, ["deleted"]) && value["deleted"] === true;
119
- }
120
- return false;
121
- }
122
- function validatesPage(data: unknown, itemValidator: (item: unknown) => { ok: boolean }): boolean {
123
- const value = record(data);
124
- if (!value || !Array.isArray(value["items"]) || !exactKeys(value, value["nextCursor"] === undefined ? ["items"] : ["items", "nextCursor"])) return false;
125
- if (value["nextCursor"] !== undefined && !validOpaqueId(value["nextCursor"])) return false;
126
- return value["items"].every((item) => itemValidator(item).ok);
127
- }
128
- function validOpaqueId(value: unknown): boolean {
129
- return validateApiErrorEnvelope({ apiVersion: "v1", requestId: value, error: { code: "internal_error", message: "Internal error." } }).ok;
130
- }
131
- function validSafeOpaqueId(value: unknown): value is string {
132
- return typeof value === "string" && validOpaqueId(value) && validateManagedAnalyticsSafePayload(value).ok;
133
- }
134
- function validIdempotencyKey(value: unknown): value is string {
135
- return validSafeOpaqueId(value) && value.length >= 8 && value.length <= 128;
136
- }
137
- function validatesOutboundData(operation: ManagedAnalyticsOperation, data: unknown): boolean {
138
- switch (operation) {
139
- case "listAdapterDefinitions": return validatesPage(data, validateAdapterDefinition);
140
- case "getAdapterDefinition": return validateAdapterDefinition(data).ok;
141
- case "listIntegrationInstalls": return validatesPage(data, validateIntegrationInstall);
142
- case "getIntegrationInstall": return validateIntegrationInstall(data).ok;
143
- case "listIntegrationRevisions": return validatesPage(data, validateIntegrationRevisionV1);
144
- case "getIntegrationRevision": return validateIntegrationRevisionV1(data).ok;
145
- case "getMigrationDraft": return validateMigrationDraftV1(data).ok;
146
- default: return validatesReplayData(operation, data);
147
- }
148
- }
149
-
150
- /** Final frozen-wire guard and detached value used by persistence/routes. */
151
- export function sanitizeManagedAnalyticsOutbound(operation: ManagedAnalyticsOperation, input: unknown): unknown | null {
152
- const safe = validateManagedAnalyticsSafePayload(input);
153
- if (!safe.ok) return null;
154
- const envelope = record(safe.value);
155
- if (!envelope) return null;
156
- if (Object.prototype.hasOwnProperty.call(envelope, "error")) return validateApiErrorEnvelope(envelope).ok ? safe.value : null;
157
- const mutation = !MANAGED_ANALYTICS_READ_OPERATIONS.has(operation);
158
- const keys = mutation ? ["apiVersion", "requestId", "data", "idempotencyReplayed"] : ["apiVersion", "requestId", "data"];
159
- if (!exactKeys(envelope, keys) || envelope["apiVersion"] !== "v1" || !validOpaqueId(envelope["requestId"])) return null;
160
- if (mutation && typeof envelope["idempotencyReplayed"] !== "boolean") return null;
161
- return validatesOutboundData(operation, envelope["data"]) ? safe.value : null;
162
- }
163
- export function validateManagedAnalyticsOutbound(operation: ManagedAnalyticsOperation, input: unknown): boolean {
164
- return sanitizeManagedAnalyticsOutbound(operation, input) !== null;
165
- }
166
- function success<T>(data: T): ApiResult<T> {
167
- return { apiVersion: MANAGED_ANALYTICS_API_VERSION, requestId: requestId(), data };
168
- }
169
- function mutationSuccess<T>(data: T): MutationResult<T> {
170
- return { apiVersion: MANAGED_ANALYTICS_API_VERSION, requestId: requestId(), data, idempotencyReplayed: false };
171
- }
172
- function error(code: ApiErrorEnvelope["error"]["code"], message: string, details?: ApiErrorEnvelope["error"]["details"]): ApiErrorEnvelope {
173
- return { apiVersion: MANAGED_ANALYTICS_API_VERSION, requestId: requestId(), error: { code, message, ...(details?.length ? { details } : {}) } };
174
- }
175
- function notFound(): ApiErrorEnvelope { return error("not_found", "The requested resource was not found."); }
176
- function stale(): ApiErrorEnvelope { return error("stale_revision", "The resource changed; refresh and retry."); }
177
- function internalError(): ApiErrorEnvelope { return error("internal_error", "The integration service could not complete the request."); }
178
- function parseCandidate(row: InstallRow): StoredCandidate | null {
179
- return row.managed_candidate ? JSON.parse(row.managed_candidate) as StoredCandidate : null;
180
- }
181
- function parseLive(row: InstallRow): IntegrationManifest | null {
182
- const value = JSON.parse(row.manifest) as unknown;
183
- const valid = validateIntegrationManifest(value);
184
- return valid.ok ? valid.value : null;
185
- }
186
- function publicInstall(row: InstallRow): IntegrationInstall {
187
- const live = parseLive(row);
188
- const value: IntegrationInstall = {
189
- schemaVersion: MANAGED_ANALYTICS_SCHEMA_VERSION, installId: row.install_id,
190
- adapterId: row.app_id, status: row.status === "active" && live ? "enabled" : "disabled",
191
- activeRevisionId: row.managed_active_revision_id, revision: row.managed_revision,
192
- createdAt: row.created_at, updatedAt: row.updated_at,
193
- };
194
- const validated = validateIntegrationInstall(value);
195
- if (!validated.ok) throw new Error("Managed analytics install failed persisted-data validation.");
196
- return validated.value;
197
- }
198
- /**
199
- * Build the public managed-analytics adapter catalog from the canonical platform
200
- * adapter cards. Exported for read-only/offline contract proofs; callers receive
201
- * detached data and no tenant or persistence authority.
202
- */
203
- export function managedAnalyticsAdapterDefinitions(): AdapterDefinition[] {
204
- return platformAdapterCatalog().map((card) => ({
205
- schemaVersion: MANAGED_ANALYTICS_SCHEMA_VERSION, adapterId: card.adapterId,
206
- adapterVersion: "1.0.0", displayName: card.name, description: card.purpose,
207
- availability: "available",
208
- configuration: [{ key: "destinationId", label: card.idLabel, description: "Public destination identifier used by this reviewed adapter.", kind: "string", required: true }],
209
- eventTypes: [...card.eventTypes], consentPurposes: [...card.consentPurposes],
210
- collectionOrigins: [...card.cspOrigins].sort(),
211
- }));
212
- }
213
- const CATALOG = managedAnalyticsAdapterDefinitions();
214
- const CATALOG_BY_ID: Readonly<Record<string, AdapterDefinition>> = Object.assign(
215
- Object.create(null) as Record<string, AdapterDefinition>,
216
- Object.fromEntries(CATALOG.map((item) => [item.adapterId, item])),
217
- );
218
- function page<T extends Record<I, string>, I extends string>(items: T[], id: I, query: { cursor?: string; limit?: number }) {
219
- const found = query.cursor ? items.findIndex((item) => item[id] === query.cursor) : -1;
220
- const start = found < 0 ? 0 : found + 1;
221
- const limit = Math.min(query.limit ?? 50, 100);
222
- const selected = items.slice(start, start + limit);
223
- return { items: selected, ...(start + limit < items.length && selected.length ? { nextCursor: selected.at(-1)![id] } : {}) };
224
- }
225
-
226
- /** Pure validation and declarative effect compilation; performs no I/O. */
227
- export async function validateManagedAnalyticsCandidate(input: {
228
- adapterId: string; adapterVersion: string; configuration: Record<string, AdapterConfigValue>;
229
- }, tenantPublicId = "managed-validation.invalid"): Promise<CandidateValidation> {
230
- const definition = Object.prototype.hasOwnProperty.call(CATALOG_BY_ID, input.adapterId)
231
- ? CATALOG_BY_ID[input.adapterId] : undefined;
232
- if (!definition) return { ok: false, details: [{ path: "body.adapterId", code: "unknown_adapter", message: "Unknown adapter." }] };
233
- if (definition.adapterVersion !== input.adapterVersion) return { ok: false, details: [{ path: "body.adapterVersion", code: "unknown_version", message: "Unknown adapter version." }] };
234
- const keys = Object.keys(input.configuration);
235
- if (keys.length !== 1 || keys[0] !== "destinationId") return { ok: false, details: [{ path: "body.configuration", code: "configuration_shape", message: "Configuration does not match the adapter definition." }] };
236
- const destinationId = input.configuration.destinationId;
237
- const platform = getPlatformAdapter(input.adapterId);
238
- if (typeof destinationId !== "string" || !platform?.validateId(destinationId)) return { ok: false, details: [{ path: "body.configuration.destinationId", code: "format", message: "Destination identifier has an invalid format." }] };
239
- const safe = validateManagedAnalyticsSafePayload({ adapterId: input.adapterId, adapterVersion: input.adapterVersion, configuration: input.configuration });
240
- if (!safe.ok) return { ok: false, details: safe.violations };
241
- // The frozen public candidate body is the only value scanned by the safety
242
- // validator above. The auth-derived tenant scope is a separate materializer
243
- // argument and can therefore never become an unpublished request field.
244
- const tenantId = tenantPublicId;
245
- const built = buildPlatformAdapterDeclaration({
246
- adapterId: input.adapterId,
247
- ids: { live: destinationId },
248
- tenantPublicId: tenantId,
249
- environment: {
250
- schemaVersion: ENVIRONMENT_SCHEMA_VERSION,
251
- siteSurface: "admin",
252
- environment: "live",
253
- canonicalHost: tenantId,
254
- revision: "managed-analytics-v1",
255
- },
256
- });
257
- if (!built.ok) return { ok: false, details: built.violations.map(({ path, code, message }) => ({ path: path ? `body.${path}` : "body", code, message })) };
258
- const manifest = validateIntegrationManifest(built.declaration.manifest);
259
- if (!manifest.ok) return { ok: false, details: manifest.violations };
260
- const resolved = resolveEffectivePlan({ tenantPublicId: tenantId, environment: {
261
- schemaVersion: ENVIRONMENT_SCHEMA_VERSION, siteSurface: "admin", environment: "live",
262
- canonicalHost: tenantId, revision: "managed-analytics-v1",
263
- }, declarations: [built.declaration] });
264
- if (!resolved.ok) return { ok: false, details: resolved.violations };
265
- const compiled = compileIntegrationCspFromPlan(resolved.value, collectDeclaredCspProfiles([built.declaration]));
266
- if (!compiled.ok) return { ok: false, details: compiled.violations };
267
- const cspOrigins = [...new Set(Object.values(compiled.value.additions).flatMap((origins) => origins ?? []))].sort();
268
- const integration = resolved.value.integrations.find((item) => item.appId === input.adapterId)!;
269
- const digest = manifest.value.app.digest;
270
- const effects: Effects = { digest, cspOrigins, subscriptions: [...built.card.eventTypes], cacheIdentity: `managed-analytics:${digest}` };
271
- return { ok: true, effects, manifest: manifest.value, facets: {
272
- appVersion: integration.version, digest, executionTier: integration.executionTier,
273
- entry: integration.entry, publicConfig: { ...input.configuration },
274
- contributions: [...resolved.value.contributions], scopes: [], consent: integration.consent,
275
- cspOrigins,
276
- } };
277
- }
278
-
279
- /** Thin v1 facade; canonical registry/version/audit/idempotency tables are authority. */
280
- export class ManagedAnalyticsApiV1Facade {
281
- private readonly versions: AppVersionStore;
282
- constructor(private readonly db: Queryable, private readonly now: () => string = () => new Date().toISOString()) {
283
- this.versions = new AppVersionStore(db, now);
284
- }
285
- listAdapterDefinitions(request: ListAdapterDefinitionsRequest) { return success(page(CATALOG, "adapterId", request.query)); }
286
- getAdapterDefinition(adapterId: string): ApiResult<AdapterDefinition> {
287
- const found = Object.prototype.hasOwnProperty.call(CATALOG_BY_ID, adapterId) ? CATALOG_BY_ID[adapterId] : undefined;
288
- return found ? success(found) : notFound();
289
- }
290
- async listIntegrationInstalls(ctx: ManagedAnalyticsActorContext, request: ListIntegrationInstallsRequest) {
291
- try {
292
- const rows = await this.db.all<InstallRow>(`SELECT * FROM app_installs WHERE tenant_id = ? AND managed_kind = ? AND status != 'uninstalled' ORDER BY created_at, install_id`, ctx.tenantId, KIND);
293
- return success(page(rows.map(publicInstall), "installId", request.query));
294
- } catch { return internalError(); }
295
- }
296
- async getIntegrationInstall(ctx: ManagedAnalyticsActorContext, installId: string) {
297
- try {
298
- const row = await this.ownedInstall(ctx.tenantId, installId);
299
- return row ? success(publicInstall(row)) : notFound();
300
- } catch { return internalError(); }
301
- }
302
- async listIntegrationRevisions(ctx: ManagedAnalyticsActorContext, installId: string, query: { cursor?: string; limit?: number }) {
303
- try {
304
- const row = await this.ownedInstall(ctx.tenantId, installId);
305
- if (!row) return notFound();
306
- return success(page(await this.revisions(row), "revisionId", query));
307
- } catch { return internalError(); }
308
- }
309
- async getIntegrationRevision(ctx: ManagedAnalyticsActorContext, installId: string, revisionId: string) {
310
- try {
311
- const row = await this.ownedInstall(ctx.tenantId, installId);
312
- if (!row) return notFound();
313
- const found = (await this.revisions(row)).find((item) => item.revisionId === revisionId);
314
- return found ? success(found) : notFound();
315
- } catch { return internalError(); }
316
- }
317
- async getMigrationDraft(ctx: ManagedAnalyticsActorContext, draftId: string) {
318
- try {
319
- const draft = await this.loadDraft(ctx.tenantId, draftId);
320
- return draft?.open ? success(draft.draft) : notFound();
321
- } catch { return internalError(); }
322
- }
323
-
324
- async createMigrationDraft(ctx: ManagedAnalyticsActorContext, request: CreateMigrationDraftRequest) {
325
- const snapshot = snapshotManagedAnalyticsDeclarativeInput<CreateMigrationDraftRequest>(request);
326
- if (!snapshot.ok) return error("validation_failed", "The request failed validation.", snapshot.violations);
327
- const prepared = validateCreateMigrationDraftRequest(snapshot.value);
328
- if (!prepared.ok) return this.rejectInvalidMutation<MigrationDraftV1>(ctx, "createMigrationDraft", snapshot.value, prepared.violations);
329
- request = prepared.value;
330
- return this.idempotent(ctx, "createMigrationDraft", request, this.tenantScope(ctx), async () => {
331
- if (request.expectedRevision !== 0) return { response: stale(), statements: [] };
332
- const now = this.now();
333
- const draft: MigrationDraftV1 = { schemaVersion: 1, draftId: publicId("migration"), revision: 1, source: "approved-migration", status: "draft", enabled: false, publishes: false, proposedAdapterId: request.body.proposedAdapterId, proposedConfiguration: { ...request.body.proposedConfiguration }, findings: [...request.body.findings], createdAt: now, updatedAt: now };
334
- const safe = validateManagedAnalyticsSafePayload(draft);
335
- if (!safe.ok) return { response: error("validation_failed", "The migration draft failed validation.", safe.violations), statements: [] };
336
- return { response: mutationSuccess(draft), statements: [
337
- { sql: `INSERT INTO integration_migration_drafts (draft_id, tenant_id, revision, state, record, created_at, updated_at) VALUES (?,?,?,'open',?,?,?)`, params: [draft.draftId, ctx.tenantId, draft.revision, JSON.stringify(draft), now, now] },
338
- this.auditStatement(draft.draftId, ctx, "managed_analytics.migration_draft_created", { revision: draft.revision }),
339
- ] };
340
- });
341
- }
342
- async updateMigrationDraft(ctx: ManagedAnalyticsActorContext, request: UpdateMigrationDraftRequest) {
343
- const snapshot = snapshotManagedAnalyticsDeclarativeInput<UpdateMigrationDraftRequest>(request);
344
- if (!snapshot.ok) return error("validation_failed", "The request failed validation.", snapshot.violations);
345
- const prepared = validateUpdateMigrationDraftRequest(snapshot.value);
346
- if (!prepared.ok) return this.rejectInvalidMutation<MigrationDraftV1>(ctx, "updateMigrationDraft", snapshot.value, prepared.violations, "draftId");
347
- request = prepared.value;
348
- const scope = request.body.draftId;
349
- return this.idempotent(ctx, "updateMigrationDraft", request, scope, async () => {
350
- const current = await this.loadDraft(ctx.tenantId, scope);
351
- if (!current?.open) return { response: notFound(), statements: [] };
352
- if (current.draft.revision !== request.expectedRevision) return { response: stale(), statements: [] };
353
- const draft: MigrationDraftV1 = { ...current.draft, proposedAdapterId: request.body.proposedAdapterId, proposedConfiguration: { ...request.body.proposedConfiguration }, findings: [...request.body.findings], revision: current.draft.revision + 1, updatedAt: this.now() };
354
- const safe = validateManagedAnalyticsSafePayload(draft);
355
- if (!safe.ok) return { response: error("validation_failed", "The migration draft failed validation.", safe.violations), statements: [] };
356
- const update: QueryStatement = { sql: `UPDATE integration_migration_drafts SET revision = ?, record = ?, updated_at = ? WHERE draft_id = ? AND tenant_id = ? AND state = 'open' AND revision = ?`, params: [draft.revision, JSON.stringify(draft), draft.updatedAt, scope, ctx.tenantId, request.expectedRevision] };
357
- return { response: mutationSuccess(draft), statements: [update, this.auditStatement(scope, ctx, "managed_analytics.migration_draft_updated", { revision: draft.revision }, true)], casStillCurrent: async () => { const latest = await this.loadDraft(ctx.tenantId, scope); return latest?.open === true && latest.draft.revision === request.expectedRevision; } };
358
- });
359
- }
360
- async deleteMigrationDraft(ctx: ManagedAnalyticsActorContext, request: DeleteMigrationDraftRequest) {
361
- const snapshot = snapshotManagedAnalyticsDeclarativeInput<DeleteMigrationDraftRequest>(request);
362
- if (!snapshot.ok) return error("validation_failed", "The request failed validation.", snapshot.violations);
363
- const prepared = validateDeleteMigrationDraftRequest(snapshot.value);
364
- if (!prepared.ok) return this.rejectInvalidMutation<{ deleted: true }>(ctx, "deleteMigrationDraft", snapshot.value, prepared.violations, "draftId");
365
- request = prepared.value;
366
- const scope = request.body.draftId;
367
- return this.idempotent(ctx, "deleteMigrationDraft", request, scope, async () => {
368
- const current = await this.loadDraft(ctx.tenantId, scope);
369
- if (!current?.open) return { response: notFound(), statements: [] };
370
- if (current.draft.revision !== request.expectedRevision) return { response: stale(), statements: [] };
371
- const revision = current.draft.revision + 1;
372
- const update: QueryStatement = { sql: `UPDATE integration_migration_drafts SET revision = ?, state = 'rejected', updated_at = ? WHERE draft_id = ? AND tenant_id = ? AND state = 'open' AND revision = ?`, params: [revision, this.now(), scope, ctx.tenantId, request.expectedRevision] };
373
- return { response: mutationSuccess({ deleted: true as const }), statements: [update, this.auditStatement(scope, ctx, "managed_analytics.migration_draft_rejected", { revision }, true)], casStillCurrent: async () => { const latest = await this.loadDraft(ctx.tenantId, scope); return latest?.open === true && latest.draft.revision === request.expectedRevision; } };
374
- });
375
- }
376
-
377
- async createIntegrationInstall(ctx: ManagedAnalyticsActorContext, request: CreateIntegrationInstallRequest) {
378
- const snapshot = snapshotManagedAnalyticsDeclarativeInput<CreateIntegrationInstallRequest>(request);
379
- if (!snapshot.ok) return error("validation_failed", "The request failed validation.", snapshot.violations);
380
- const prepared = validateCreateIntegrationInstallRequest(snapshot.value);
381
- if (!prepared.ok) return this.rejectInvalidMutation<{ install: IntegrationInstall; revision: IntegrationRevisionV1 }>(ctx, "createIntegrationInstall", snapshot.value, prepared.violations);
382
- request = prepared.value;
383
- return this.idempotent(ctx, "createIntegrationInstall", request, this.tenantScope(ctx), async () => {
384
- if (request.expectedRevision !== 0) return { response: stale(), statements: [] };
385
- const validated = await validateManagedAnalyticsCandidate(request.body, ctx.tenantId);
386
- if (!validated.ok) return { response: error("validation_failed", "The integration revision failed validation.", validated.details), statements: [] };
387
- const existing = await this.adapterSlot(ctx.tenantId, request.body.adapterId);
388
- if (existing && (existing.managed_kind !== KIND || existing.status !== "uninstalled")) {
389
- return { response: error("conflict", "This adapter is already configured."), statements: [] };
390
- }
391
- const now = this.now();
392
- const installId = existing?.install_id ?? publicId("integration");
393
- const nextRevision = existing ? existing.managed_revision + 1 : 1;
394
- const revision: IntegrationRevisionV1 = { schemaVersion: 1, revisionId: publicId("revision"), installId, adapterId: request.body.adapterId, adapterVersion: request.body.adapterVersion, sequence: nextRevision, status: "draft", configuration: { ...request.body.configuration }, createdAt: now };
395
- const candidate: StoredCandidate = { revision, digest: validated.effects.digest, effects: validated.effects, manifest: validated.manifest, facets: validated.facets, approvedAt: now };
396
- const install: IntegrationInstall = { schemaVersion: 1, installId, adapterId: request.body.adapterId, status: "disabled", activeRevisionId: null, revision: nextRevision, createdAt: existing?.created_at ?? now, updatedAt: now };
397
- const statements: QueryStatement[] = [
398
- existing
399
- ? { sql: `UPDATE app_installs SET app_version = ?, status = 'suspended', scopes = '[]', manifest = '{}', webhook_endpoint = NULL, widget_placements = '[]', install_actor = ?, managed_active_revision_id = NULL, managed_candidate = ?, managed_revision = ?, updated_at = ? WHERE install_id = ? AND app_id = ? AND tenant_id = ? AND env = 'production' AND managed_kind = ? AND status = 'uninstalled' AND managed_revision = ?`, params: [request.body.adapterVersion, ctx.actor, JSON.stringify(candidate), nextRevision, now, installId, request.body.adapterId, ctx.tenantId, KIND, existing.managed_revision] }
400
- : { sql: `INSERT INTO app_installs (install_id, app_id, app_version, tenant_id, env, status, scopes, manifest, webhook_endpoint, widget_placements, install_actor, managed_kind, managed_revision, managed_candidate, created_at, updated_at) VALUES (?,?,?,?,?,'suspended','[]','{}',NULL,'[]',?,?,?,?,?,?)`, params: [installId, request.body.adapterId, request.body.adapterVersion, ctx.tenantId, "production", ctx.actor, KIND, nextRevision, JSON.stringify(candidate), now, now] },
401
- this.auditStatement(installId, ctx, "managed_analytics.candidate_staged", { revision, digest: candidate.digest }, existing !== null),
402
- ];
403
- return {
404
- response: mutationSuccess({ install, revision }),
405
- statements,
406
- ...(existing ? { casStillCurrent: () => this.installStillTombstoned(ctx.tenantId, installId, existing.managed_revision) } : {}),
407
- };
408
- });
409
- }
410
- async reviseIntegrationInstall(ctx: ManagedAnalyticsActorContext, request: ReviseIntegrationInstallRequest) {
411
- const snapshot = snapshotManagedAnalyticsDeclarativeInput<ReviseIntegrationInstallRequest>(request);
412
- if (!snapshot.ok) return error("validation_failed", "The request failed validation.", snapshot.violations);
413
- const prepared = validateReviseIntegrationInstallRequest(snapshot.value);
414
- if (!prepared.ok) return this.rejectInvalidMutation<{ install: IntegrationInstall; revision: IntegrationRevisionV1 }>(ctx, "reviseIntegrationInstall", snapshot.value, prepared.violations, "installId");
415
- request = prepared.value;
416
- return this.idempotent(ctx, "reviseIntegrationInstall", request, request.body.installId, async () => {
417
- const row = await this.ownedInstall(ctx.tenantId, request.body.installId);
418
- if (!row) return { response: notFound(), statements: [] };
419
- if (row.managed_revision !== request.expectedRevision) return { response: stale(), statements: [] };
420
- const validated = await validateManagedAnalyticsCandidate({ adapterId: row.app_id, adapterVersion: request.body.adapterVersion, configuration: request.body.configuration }, ctx.tenantId);
421
- if (!validated.ok) return { response: error("validation_failed", "The integration revision failed validation.", validated.details), statements: [] };
422
- const now = this.now();
423
- const revision: IntegrationRevisionV1 = { schemaVersion: 1, revisionId: publicId("revision"), installId: row.install_id, adapterId: row.app_id, adapterVersion: request.body.adapterVersion, sequence: row.managed_revision + 1, status: "draft", configuration: { ...request.body.configuration }, createdAt: now };
424
- const priorApproved = (await this.versions.listVersions(row.install_id)).find((snapshot) =>
425
- snapshot.appVersion === request.body.adapterVersion &&
426
- canonicalJson(snapshot.facets.publicConfig) === canonicalJson(request.body.configuration) &&
427
- snapshot.facets.digest === validated.effects.digest);
428
- const candidate: StoredCandidate = { revision, digest: validated.effects.digest, effects: validated.effects, manifest: validated.manifest, facets: validated.facets, approvedAt: now, ...(priorApproved ? { rollbackFromSeq: priorApproved.seq } : {}) };
429
- const install = { ...publicInstall(row), revision: row.managed_revision + 1, updatedAt: now };
430
- const update: QueryStatement = { sql: `UPDATE app_installs SET managed_candidate = ?, managed_revision = ?, updated_at = ? WHERE install_id = ? AND tenant_id = ? AND managed_kind = ? AND managed_revision = ? AND status != 'uninstalled'`, params: [JSON.stringify(candidate), install.revision, now, row.install_id, ctx.tenantId, KIND, request.expectedRevision] };
431
- return { response: mutationSuccess({ install, revision }), statements: [update, this.auditStatement(row.install_id, ctx, "managed_analytics.candidate_staged", { revision, digest: candidate.digest }, true)], casStillCurrent: () => this.installStillCurrent(ctx.tenantId, row.install_id, request.expectedRevision) };
432
- });
433
- }
434
-
435
- async setIntegrationInstallStatus(ctx: ManagedAnalyticsActorContext, request: SetIntegrationInstallStatusRequest) {
436
- const snapshot = snapshotManagedAnalyticsDeclarativeInput<SetIntegrationInstallStatusRequest>(request);
437
- if (!snapshot.ok) return error("validation_failed", "The request failed validation.", snapshot.violations);
438
- const prepared = validateSetIntegrationInstallStatusRequest(snapshot.value);
439
- if (!prepared.ok) return this.rejectInvalidMutation<IntegrationInstall>(ctx, "setIntegrationInstallStatus", snapshot.value, prepared.violations, "installId");
440
- request = prepared.value;
441
- return this.idempotent(ctx, "setIntegrationInstallStatus", request, request.body.installId, async () => {
442
- const row = await this.ownedInstall(ctx.tenantId, request.body.installId);
443
- if (!row) return { response: notFound(), statements: [] };
444
- if (row.managed_revision !== request.expectedRevision) return { response: stale(), statements: [] };
445
- const now = this.now(); const nextRevision = row.managed_revision + 1;
446
- if (request.body.status === "disabled") {
447
- if (row.status === "suspended") {
448
- return { response: mutationSuccess(publicInstall(row)), statements: [] };
449
- }
450
- const update: QueryStatement = { sql: `UPDATE app_installs SET status = 'suspended', managed_revision = ?, updated_at = ? WHERE install_id = ? AND tenant_id = ? AND managed_kind = ? AND managed_revision = ? AND status != 'uninstalled'`, params: [nextRevision, now, row.install_id, ctx.tenantId, KIND, request.expectedRevision] };
451
- const install = { ...publicInstall(row), status: "disabled" as const, revision: nextRevision, updatedAt: now };
452
- return { response: mutationSuccess(install), statements: [update, this.auditStatement(row.install_id, ctx, "managed_analytics.suspended", {}, true)], casStillCurrent: () => this.installStillCurrent(ctx.tenantId, row.install_id, request.expectedRevision) };
453
- }
454
- const candidate = parseCandidate(row); const existingLive = parseLive(row);
455
- if (!candidate && !existingLive) return { response: error("conflict", "No approved candidate is available to publish."), statements: [] };
456
- if (!candidate && existingLive) {
457
- const current = await this.versions.currentVersion(row.install_id);
458
- const validated = current ? await validateManagedAnalyticsCandidate({ adapterId: row.app_id, adapterVersion: current.appVersion, configuration: current.facets.publicConfig as Record<string, AdapterConfigValue> }, ctx.tenantId) : null;
459
- if (!validated?.ok || validated.effects.digest !== current?.facets.digest || validated.manifest.app.digest !== existingLive.app.digest || !row.managed_active_revision_id) return { response: error("conflict", "The approved live digest no longer matches."), statements: [] };
460
- if (row.status === "active") {
461
- return { response: mutationSuccess(publicInstall(row)), statements: [] };
462
- }
463
- const update: QueryStatement = { sql: `UPDATE app_installs SET status = 'active', managed_revision = ?, updated_at = ? WHERE install_id = ? AND tenant_id = ? AND managed_kind = ? AND managed_revision = ? AND status = 'suspended'`, params: [nextRevision, now, row.install_id, ctx.tenantId, KIND, request.expectedRevision] };
464
- const install = { ...publicInstall(row), status: "enabled" as const, revision: nextRevision, updatedAt: now };
465
- return { response: mutationSuccess(install), statements: [update, this.auditStatement(row.install_id, ctx, "managed_analytics.resumed", { revisionId: row.managed_active_revision_id, digest: current.facets.digest }, true)], casStillCurrent: () => this.installStillCurrent(ctx.tenantId, row.install_id, request.expectedRevision) };
466
- }
467
- const approved = await validateManagedAnalyticsCandidate({ adapterId: candidate!.revision.adapterId, adapterVersion: candidate!.revision.adapterVersion, configuration: candidate!.revision.configuration }, ctx.tenantId);
468
- if (!approved.ok || approved.effects.digest !== candidate!.digest || canonicalJson(approved.manifest) !== canonicalJson(candidate!.manifest)) return { response: error("conflict", "The approved candidate digest no longer matches."), statements: [] };
469
- const planned = candidate!.rollbackFromSeq !== undefined
470
- ? await this.versions.planRollbackTo({ installId: row.install_id, toSeq: candidate!.rollbackFromSeq, actor: ctx.actor, note: "managed-analytics rollback publish" })
471
- : await this.versions.planRetainVersion({ installId: row.install_id, tenantId: ctx.tenantId, facets: candidate!.facets, actor: ctx.actor, note: "managed-analytics publish" });
472
- const update: QueryStatement = { sql: `UPDATE app_installs SET status = 'active', app_version = ?, manifest = ?, managed_active_revision_id = ?, managed_candidate = NULL, managed_revision = ?, updated_at = ? WHERE install_id = ? AND tenant_id = ? AND managed_kind = ? AND managed_revision = ? AND status != 'uninstalled'`, params: [candidate!.revision.adapterVersion, JSON.stringify(candidate!.manifest), candidate!.revision.revisionId, nextRevision, now, row.install_id, ctx.tenantId, KIND, request.expectedRevision] };
473
- const install: IntegrationInstall = { ...publicInstall(row), status: "enabled", activeRevisionId: candidate!.revision.revisionId, revision: nextRevision, updatedAt: now };
474
- return { response: mutationSuccess(install), statements: [update, this.auditStatement(row.install_id, ctx, "managed_analytics.published", { revisionId: candidate!.revision.revisionId, digest: candidate!.digest }, true), planned.statement], casStillCurrent: () => this.installStillCurrent(ctx.tenantId, row.install_id, request.expectedRevision) };
475
- });
476
- }
477
- async deleteIntegrationInstall(ctx: ManagedAnalyticsActorContext, request: DeleteIntegrationInstallRequest) {
478
- const snapshot = snapshotManagedAnalyticsDeclarativeInput<DeleteIntegrationInstallRequest>(request);
479
- if (!snapshot.ok) return error("validation_failed", "The request failed validation.", snapshot.violations);
480
- const prepared = validateDeleteIntegrationInstallRequest(snapshot.value);
481
- if (!prepared.ok) return this.rejectInvalidMutation<{ deleted: true }>(ctx, "deleteIntegrationInstall", snapshot.value, prepared.violations, "installId");
482
- request = prepared.value;
483
- return this.idempotent(ctx, "deleteIntegrationInstall", request, request.body.installId, async () => {
484
- const row = await this.ownedInstall(ctx.tenantId, request.body.installId);
485
- if (!row) return { response: notFound(), statements: [] };
486
- if (row.managed_revision !== request.expectedRevision) return { response: stale(), statements: [] };
487
- const now = this.now();
488
- const update: QueryStatement = { sql: `UPDATE app_installs SET status = 'uninstalled', app_version = 'removed', scopes = '[]', manifest = '{}', webhook_endpoint = NULL, widget_placements = '[]', managed_active_revision_id = NULL, managed_candidate = NULL, managed_revision = ?, updated_at = ? WHERE install_id = ? AND tenant_id = ? AND managed_kind = ? AND managed_revision = ? AND status != 'uninstalled'`, params: [row.managed_revision + 1, now, row.install_id, ctx.tenantId, KIND, request.expectedRevision] };
489
- return { response: mutationSuccess({ deleted: true as const }), statements: [update, this.auditStatement(row.install_id, ctx, "managed_analytics.removed", {}, true)], casStillCurrent: () => this.installStillCurrent(ctx.tenantId, row.install_id, request.expectedRevision) };
490
- });
491
- }
492
-
493
- private async ownedInstall(tenantId: string, installId: string): Promise<InstallRow | null> {
494
- return this.db.first<InstallRow>(`SELECT * FROM app_installs WHERE install_id = ? AND tenant_id = ? AND managed_kind = ? AND status != 'uninstalled'`, installId, tenantId, KIND);
495
- }
496
- private async adapterSlot(tenantId: string, adapterId: string): Promise<InstallRow | null> {
497
- return this.db.first<InstallRow>(`SELECT * FROM app_installs WHERE app_id = ? AND tenant_id = ? AND env = 'production'`, adapterId, tenantId);
498
- }
499
- private async installStillTombstoned(tenantId: string, installId: string, revision: number): Promise<boolean> {
500
- const row = await this.db.first<Pick<InstallRow, "status" | "managed_kind" | "managed_revision">>(
501
- `SELECT status, managed_kind, managed_revision FROM app_installs WHERE install_id = ? AND tenant_id = ?`,
502
- installId,
503
- tenantId,
504
- );
505
- return row?.status === "uninstalled" && row.managed_kind === KIND && row.managed_revision === revision;
506
- }
507
- private async installStillCurrent(tenantId: string, installId: string, revision: number): Promise<boolean> {
508
- return (await this.ownedInstall(tenantId, installId))?.managed_revision === revision;
509
- }
510
- private async revisions(row: InstallRow): Promise<IntegrationRevisionV1[]> {
511
- const audits = await this.db.all<AuditRow>(`SELECT id, event, detail, at FROM app_audit_log WHERE install_id = ? AND tenant_id = ? AND event = 'managed_analytics.candidate_staged' ORDER BY at, id`, row.install_id, row.tenant_id);
512
- const liveId = row.managed_active_revision_id; const candidateId = parseCandidate(row)?.revision.revisionId ?? null;
513
- return audits.map((audit) => {
514
- const detail = JSON.parse(audit.detail) as unknown;
515
- const safe = validateManagedAnalyticsSafePayload(detail);
516
- const revision = detail && typeof detail === "object" && !Array.isArray(detail)
517
- ? validateIntegrationRevisionV1((detail as Record<string, unknown>)["revision"])
518
- : { ok: false as const, violations: [] };
519
- if (!safe.ok || !revision.ok || revision.value.installId !== row.install_id) {
520
- throw new Error("Managed analytics revision failed persisted-data validation.");
521
- }
522
- return revision.value;
523
- })
524
- .map((revision) => ({ ...revision, status: revision.revisionId === liveId ? "active" : revision.revisionId === candidateId ? "draft" : "superseded" }));
525
- }
526
- private async loadDraft(tenantId: string, draftId: string): Promise<{ draft: MigrationDraftV1; open: boolean } | null> {
527
- const row = await this.db.first<DraftRow>(`SELECT * FROM integration_migration_drafts WHERE draft_id = ? AND tenant_id = ? AND state = 'open'`, draftId, tenantId);
528
- if (!row) return null;
529
- const draft = validateMigrationDraftV1(JSON.parse(row.record) as unknown);
530
- if (!draft.ok || draft.value.draftId !== row.draft_id || draft.value.revision !== row.revision) {
531
- throw new Error("Managed analytics draft failed persisted-data validation.");
532
- }
533
- return { draft: draft.value, open: true };
534
- }
535
- private tenantScope(ctx: ManagedAnalyticsActorContext): string { return `managed-analytics-tenant:${ctx.tenantId}`; }
536
- private async rejectInvalidMutation<T>(
537
- ctx: ManagedAnalyticsActorContext,
538
- operation: ManagedAnalyticsOperation,
539
- snapshot: unknown,
540
- violations: NonNullable<ApiErrorEnvelope["error"]["details"]>,
541
- resourceKey?: "draftId" | "installId",
542
- ): Promise<MutationResult<T>> {
543
- const request = record(snapshot);
544
- const rejected = error("validation_failed", "The request failed validation.", violations);
545
- if (!request || !validIdempotencyKey(request["idempotencyKey"])) {
546
- return error("validation_failed", "The request failed validation.");
547
- }
548
- const body = record(request["body"]);
549
- const resourceId = resourceKey && body ? body[resourceKey] : undefined;
550
- const scope = resourceKey && validSafeOpaqueId(resourceId) ? resourceId : this.tenantScope(ctx);
551
- return this.idempotent<T>(ctx, operation, request as { idempotencyKey: string }, scope, async () => ({ response: rejected, statements: [] }));
552
- }
553
- private auditStatement(installId: string, ctx: ManagedAnalyticsActorContext, event: string, detail: Record<string, unknown>, assertPreviousChange = false, id = publicId("audit")): QueryStatement {
554
- return { sql: `INSERT INTO app_audit_log (id, install_id, tenant_id, event, actor, trace_id, detail, at) VALUES (${assertPreviousChange ? "CASE WHEN changes() = 1 THEN ? ELSE NULL END" : "?"},?,?,?,?,NULL,?,?)`, params: [id, installId, ctx.tenantId, event, ctx.actor, JSON.stringify(detail), this.now()] };
555
- }
556
- private async idempotent<T>(ctx: ManagedAnalyticsActorContext, operation: ManagedAnalyticsOperation, request: { idempotencyKey: string }, scope: string, planMutation: () => Promise<MutationPlan<T>>): Promise<MutationResult<T>> {
557
- const key = `${operation}:${request.idempotencyKey}`; const digest = await sha256Hex(canonicalJson(request));
558
- // `idempotency_keys.install_id` is a generic resource-scope column, not a
559
- // foreign key. Hash the authenticated tenant together with the public
560
- // resource scope so replay is impossible before (or without) an ownership
561
- // read, even when another tenant knows both an id and a retry key.
562
- const ledgerScope = `managed-analytics:${await sha256Hex(canonicalJson([ctx.tenantId, scope]))}`;
563
- const prior = await this.db.first<IdempotencyRow>(`SELECT body_hash, response_body FROM idempotency_keys WHERE install_id = ? AND idempotency_key = ?`, ledgerScope, key);
564
- if (prior) return this.replay<T>(prior, digest, operation);
565
- const plan = await planMutation();
566
- // The ledger is itself a persisted outbound-response authority. Refuse to
567
- // store anything that the frozen v1 response contract would reject, even
568
- // if a future mutation planner accidentally constructs an unsafe value.
569
- const outbound = sanitizeManagedAnalyticsOutbound(operation, plan.response);
570
- if (!outbound) return internalError();
571
- const persistedResponse = outbound as MutationResult<T>;
572
- if (!this.db.batch) throw new Error("Atomic D1 batch support is required.");
573
- const ledger: QueryStatement = { sql: `INSERT INTO idempotency_keys (install_id, idempotency_key, body_hash, response_body, created_at) VALUES (?,?,?,?,?)`, params: [ledgerScope, key, digest, JSON.stringify(persistedResponse), this.now()] };
574
- try {
575
- await this.db.batch([ledger, ...plan.statements]);
576
- return persistedResponse;
577
- } catch (cause) {
578
- const raced = await this.db.first<IdempotencyRow>(`SELECT body_hash, response_body FROM idempotency_keys WHERE install_id = ? AND idempotency_key = ?`, ledgerScope, key);
579
- if (raced) return this.replay<T>(raced, digest, operation);
580
- if (plan.casStillCurrent && !(await plan.casStillCurrent())) return stale();
581
- throw cause;
582
- }
583
- }
584
- private replay<T>(row: IdempotencyRow, digest: string, operation: ManagedAnalyticsOperation): MutationResult<T> {
585
- if (row.body_hash !== digest) return error("idempotency_conflict", "The idempotency key was already used for a different request.");
586
- try {
587
- const parsed = JSON.parse(row.response_body) as unknown;
588
- const response = record(parsed);
589
- if (!response) return internalError();
590
- if (Object.prototype.hasOwnProperty.call(response, "error")) {
591
- const validated = validateApiErrorEnvelope(response);
592
- return validated.ok ? validated.value : internalError();
593
- }
594
- const outbound = sanitizeManagedAnalyticsOutbound(operation, response);
595
- if (!outbound) return internalError();
596
- return { ...(outbound as MutationResult<T>), idempotencyReplayed: true };
597
- } catch { return internalError(); }
598
- }
599
- }