@app-vitals/agent-admin 0.0.0-stage → 0.1.0

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 (325) hide show
  1. package/Dockerfile +43 -0
  2. package/README.md +72 -2
  3. package/e2e/agents-page.e2e.ts +338 -0
  4. package/e2e/chat-degraded.e2e.ts +153 -0
  5. package/e2e/chat-page.e2e.ts +169 -0
  6. package/e2e/chat-progress.e2e.ts +206 -0
  7. package/e2e/chat-thread-page.e2e.ts +444 -0
  8. package/e2e/login-page.e2e.ts +148 -0
  9. package/e2e/test-server.ts +427 -0
  10. package/openapi.json +6098 -0
  11. package/openapi.platform.json +5553 -0
  12. package/package.json +64 -4
  13. package/playwright.config.ts +25 -0
  14. package/prisma/client/browser.ts +134 -0
  15. package/prisma/client/client.ts +158 -0
  16. package/prisma/client/commonInputTypes.ts +578 -0
  17. package/prisma/client/enums.ts +15 -0
  18. package/prisma/client/internal/class.ts +424 -0
  19. package/prisma/client/internal/prismaNamespace.ts +2898 -0
  20. package/prisma/client/internal/prismaNamespaceBrowser.ts +476 -0
  21. package/prisma/client/models/Account.ts +1774 -0
  22. package/prisma/client/models/AccountInvite.ts +1386 -0
  23. package/prisma/client/models/AccountMember.ts +1351 -0
  24. package/prisma/client/models/Agent.ts +4554 -0
  25. package/prisma/client/models/AgentAsk.ts +1373 -0
  26. package/prisma/client/models/AgentChatTokenUsageDailyByModel.ts +1638 -0
  27. package/prisma/client/models/AgentCronJob.ts +2416 -0
  28. package/prisma/client/models/AgentCronRun.ts +2858 -0
  29. package/prisma/client/models/AgentCronRunModelBreakdown.ts +1532 -0
  30. package/prisma/client/models/AgentCronRunSkillUsage.ts +1642 -0
  31. package/prisma/client/models/AgentEnv.ts +1421 -0
  32. package/prisma/client/models/AgentGitHubInstallationsSnapshot.ts +1289 -0
  33. package/prisma/client/models/AgentMember.ts +1316 -0
  34. package/prisma/client/models/AgentPhaseMethodology.ts +1355 -0
  35. package/prisma/client/models/AgentPlugin.ts +1421 -0
  36. package/prisma/client/models/AgentToken.ts +1380 -0
  37. package/prisma/client/models/AgentTool.ts +1351 -0
  38. package/prisma/client/models/AgentWorkQueueSnapshot.ts +1297 -0
  39. package/prisma/client/models/ChatThreadWatch.ts +1183 -0
  40. package/prisma/client/models/PushSubscription.ts +1233 -0
  41. package/prisma/client/models/SessionAlertState.ts +1187 -0
  42. package/prisma/client/models/SessionFollow.ts +1187 -0
  43. package/prisma/client/models/UserNotificationPrefs.ts +1221 -0
  44. package/prisma/client/models.ts +34 -0
  45. package/prisma/migrations/20260608000000_init/migration.sql +112 -0
  46. package/prisma/migrations/20260616000000_agent_members/migration.sql +18 -0
  47. package/prisma/migrations/20260624000000_add_agent_self_hosted/migration.sql +2 -0
  48. package/prisma/migrations/20260624120000_add_agent_repos/migration.sql +3 -0
  49. package/prisma/migrations/20260625000000_add_agent_cron_run/migration.sql +26 -0
  50. package/prisma/migrations/20260626000000_add_cron_run_token_fields/migration.sql +10 -0
  51. package/prisma/migrations/20260627000000_add_agent_chat_token_usage_daily/migration.sql +18 -0
  52. package/prisma/migrations/20260627010000_add_agent_chat_token_usage_fk/migration.sql +2 -0
  53. package/prisma/migrations/20260701000000_add_chat_threads_messages/migration.sql +37 -0
  54. package/prisma/migrations/20260701010000_drop_chat_threads_messages/migration.sql +17 -0
  55. package/prisma/migrations/20260701020000_add_cron_run_model_breakdown/migration.sql +23 -0
  56. package/prisma/migrations/20260701020000_chat_token_by_model/migration.sql +25 -0
  57. package/prisma/migrations/20260701030000_add_secret_flag_to_agent_env/migration.sql +2 -0
  58. package/prisma/migrations/20260701030000_drop_cron_run_legacy_columns/migration.sql +5 -0
  59. package/prisma/migrations/20260706000000_consolidate_cron_run_token_accounting/migration.sql +54 -0
  60. package/prisma/migrations/20260709000000_add_phase_to_agent_cron_run/migration.sql +8 -0
  61. package/prisma/migrations/20260717000000_add_item_tracking_to_agent_cron_run/migration.sql +10 -0
  62. package/prisma/migrations/20260717010000_add_agent_work_queue_snapshot/migration.sql +20 -0
  63. package/prisma/migrations/20260718000000_add_cron_phase_columns/migration.sql +25 -0
  64. package/prisma/migrations/20260718100000_drop_agent_cron_run_phase_column/migration.sql +8 -0
  65. package/prisma/migrations/20260724000000_add_agent_type_name/migration.sql +3 -0
  66. package/prisma/migrations/20260725232433_add_agent_author_allowlist/migration.sql +2 -0
  67. package/prisma/migrations/20260727120000_add_not_null_to_agent_author_allowlist/migration.sql +3 -0
  68. package/prisma/migrations/20260731000000_add_session_id_to_agent_cron_run/migration.sql +2 -0
  69. package/prisma/migrations/20260821000000_add_agent_restrict_slack_to_members/migration.sql +3 -0
  70. package/prisma/migrations/20260901000000_add_push_subscription_and_chat_thread_watch/migration.sql +40 -0
  71. package/prisma/migrations/20260903000000_add_agent_patch_author_allowlist/migration.sql +2 -0
  72. package/prisma/migrations/20260903120000_add_agent_review_author_allowlist/migration.sql +18 -0
  73. package/prisma/migrations/20260903150000_drop_agent_author_allowlist/migration.sql +14 -0
  74. package/prisma/migrations/20260905000000_add_started_at_index_to_agent_cron_run/migration.sql +11 -0
  75. package/prisma/migrations/20260906000000_add_session_follow_notification_prefs_alert_state/migration.sql +47 -0
  76. package/prisma/migrations/20260911000000_backfill_auto_follow_since/migration.sql +16 -0
  77. package/prisma/migrations/20260923115814_add_last_heartbeat_at_to_agent_cron_run/migration.sql +2 -0
  78. package/prisma/migrations/20260924120000_add_trial_expiry_fields_to_agent/migration.sql +6 -0
  79. package/prisma/migrations/20260929000000_add_agent_phase_methodology/migration.sql +21 -0
  80. package/prisma/migrations/20260929120000_add_agent_policy_fields/migration.sql +15 -0
  81. package/prisma/migrations/20261005000000_add_agent_github_installations_snapshot/migration.sql +21 -0
  82. package/prisma/migrations/20261008000000_add_cron_run_telemetry/migration.sql +50 -0
  83. package/prisma/migrations/20261008010000_add_account_models/migration.sql +48 -0
  84. package/prisma/migrations/20261008020000_add_cron_lockdown_disabled_at/migration.sql +3 -0
  85. package/prisma/migrations/20261009000000_chat_token_usage_bigint/migration.sql +7 -0
  86. package/prisma/migrations/20261010000000_add_cron_run_command_variant/migration.sql +2 -0
  87. package/prisma/migrations/20261019000000_add_agent_anthropic_identity/migration.sql +9 -0
  88. package/prisma/migrations/20261025000000_add_agent_ask/migration.sql +23 -0
  89. package/prisma/migrations/migration_lock.toml +3 -0
  90. package/prisma/schema/coding.prisma +85 -0
  91. package/prisma/schema/schema.prisma +506 -0
  92. package/prisma.config.ts +34 -0
  93. package/pwa-assets/icons/apple-touch-icon.png +0 -0
  94. package/pwa-assets/icons/favicon-32.png +0 -0
  95. package/pwa-assets/icons/icon-192.png +0 -0
  96. package/pwa-assets/icons/icon-512.png +0 -0
  97. package/pwa-assets/icons/icon-maskable-192.png +0 -0
  98. package/pwa-assets/icons/icon-maskable-512.png +0 -0
  99. package/src/account-agent-create.ts +42 -0
  100. package/src/account-agent-create.unit.test.ts +28 -0
  101. package/src/account-created-notifier.ts +71 -0
  102. package/src/account-created-notifier.unit.test.ts +70 -0
  103. package/src/account-email.ts +9 -0
  104. package/src/account-email.unit.test.ts +21 -0
  105. package/src/account-invites.ts +70 -0
  106. package/src/account-lifecycle.integration.test.ts +329 -0
  107. package/src/account-lifecycle.ts +206 -0
  108. package/src/account-lifecycle.unit.test.ts +108 -0
  109. package/src/account-members.ts +128 -0
  110. package/src/account-onboarding.integration.test.ts +101 -0
  111. package/src/account-onboarding.ts +95 -0
  112. package/src/account-trial-expiry-sweeper.ts +85 -0
  113. package/src/account-trial-warning-sweeper.integration.test.ts +228 -0
  114. package/src/account-trial-warning-sweeper.ts +216 -0
  115. package/src/accounts-api.integration.test.ts +196 -0
  116. package/src/accounts-api.smoke.test.ts +292 -0
  117. package/src/accounts-api.ts +289 -0
  118. package/src/accounts-api.unit.test.ts +93 -0
  119. package/src/accounts.integration.test.ts +178 -0
  120. package/src/accounts.ts +208 -0
  121. package/src/admin-boot-config.ts +574 -0
  122. package/src/admin-boot-config.unit.test.ts +28 -0
  123. package/src/admin-boot.boot-order.unit.test.ts +91 -0
  124. package/src/admin-boot.ts +690 -0
  125. package/src/admin-dev-login.smoke.test.ts +317 -0
  126. package/src/admin-extension.ts +432 -0
  127. package/src/admin-extension.unit.test.ts +532 -0
  128. package/src/admin-local-agent.smoke.test.ts +1643 -0
  129. package/src/admin-spec.smoke.test.ts +96 -0
  130. package/src/admin-spec.ts +186 -0
  131. package/src/admin-ui-account-nav.ts +20 -0
  132. package/src/admin-ui-account-pages.ts +131 -0
  133. package/src/admin-ui-account-pages.unit.test.ts +112 -0
  134. package/src/admin-ui-account.smoke.test.ts +286 -0
  135. package/src/admin-ui-account.ts +185 -0
  136. package/src/admin-ui-accounts-pages.ts +202 -0
  137. package/src/admin-ui-accounts-pages.unit.test.ts +138 -0
  138. package/src/admin-ui-accounts.smoke.test.ts +434 -0
  139. package/src/admin-ui-accounts.ts +266 -0
  140. package/src/admin-ui-layout.ts +60 -0
  141. package/src/admin-ui-layout.unit.test.ts +140 -0
  142. package/src/admin-ui-pages.ts +3489 -0
  143. package/src/admin-ui-pages.unit.test.ts +4355 -0
  144. package/src/admin-ui-push-test.smoke.test.ts +125 -0
  145. package/src/admin-ui-push-test.ts +102 -0
  146. package/src/admin-ui-styles.ts +656 -0
  147. package/src/admin-ui-styles.unit.test.ts +103 -0
  148. package/src/admin-ui.fixtures.ts +457 -0
  149. package/src/admin-ui.smoke.test.ts +7227 -0
  150. package/src/admin-ui.ts +3824 -0
  151. package/src/agent-account-assignment.ts +49 -0
  152. package/src/agent-account-assignment.unit.test.ts +99 -0
  153. package/src/agent-anthropic-identity.integration.test.ts +99 -0
  154. package/src/agent-anthropic-identity.ts +78 -0
  155. package/src/agent-anthropic-identity.unit.test.ts +77 -0
  156. package/src/agent-ask-config.ts +59 -0
  157. package/src/agent-ask-config.unit.test.ts +70 -0
  158. package/src/agent-ask.integration.test.ts +175 -0
  159. package/src/agent-ask.smoke.test.ts +457 -0
  160. package/src/agent-ask.ts +378 -0
  161. package/src/agent-chat-tokens.integration.test.ts +420 -0
  162. package/src/agent-chat-tokens.smoke.test.ts +551 -0
  163. package/src/agent-chat-tokens.ts +259 -0
  164. package/src/agent-cron-jobs.integration.test.ts +582 -0
  165. package/src/agent-cron-jobs.ts +615 -0
  166. package/src/agent-cron-jobs.unit.test.ts +420 -0
  167. package/src/agent-cron-run-stats.integration.test.ts +1551 -0
  168. package/src/agent-cron-run-stats.smoke.test.ts +953 -0
  169. package/src/agent-cron-run-stats.ts +752 -0
  170. package/src/agent-cron-runs.integration.test.ts +1491 -0
  171. package/src/agent-cron-runs.ts +489 -0
  172. package/src/agent-deletion-checklist.ts +128 -0
  173. package/src/agent-deletion-checklist.unit.test.ts +201 -0
  174. package/src/agent-deletion-per-agent.integration.test.ts +323 -0
  175. package/src/agent-deletion.integration.test.ts +323 -0
  176. package/src/agent-deletion.ts +329 -0
  177. package/src/agent-deletion.unit.test.ts +412 -0
  178. package/src/agent-envs.integration.test.ts +323 -0
  179. package/src/agent-envs.ts +244 -0
  180. package/src/agent-github-installations.integration.test.ts +115 -0
  181. package/src/agent-github-installations.ts +52 -0
  182. package/src/agent-manifest.ts +685 -0
  183. package/src/agent-manifest.unit.test.ts +870 -0
  184. package/src/agent-members.ts +69 -0
  185. package/src/agent-members.unit.test.ts +250 -0
  186. package/src/agent-patch-author-allowlist.integration.test.ts +218 -0
  187. package/src/agent-plugins.integration.test.ts +157 -0
  188. package/src/agent-plugins.ts +95 -0
  189. package/src/agent-plugins.unit.test.ts +43 -0
  190. package/src/agent-provisioner-per-agent.integration.test.ts +949 -0
  191. package/src/agent-provisioner.integration.test.ts +1245 -0
  192. package/src/agent-provisioner.ts +1650 -0
  193. package/src/agent-provisioner.unit.test.ts +1852 -0
  194. package/src/agent-tokens.integration.test.ts +150 -0
  195. package/src/agent-tokens.ts +112 -0
  196. package/src/agent-tools.integration.test.ts +129 -0
  197. package/src/agent-tools.ts +111 -0
  198. package/src/agents-api.integration.test.ts +705 -0
  199. package/src/agents-api.sentry.smoke.test.ts +478 -0
  200. package/src/agents-api.smoke.test.ts +5986 -0
  201. package/src/agents-api.ts +2826 -0
  202. package/src/agents.integration.test.ts +512 -0
  203. package/src/agents.ts +1097 -0
  204. package/src/agents.unit.test.ts +1736 -0
  205. package/src/anthropic-credential-guard.ts +44 -0
  206. package/src/anthropic-credential-guard.unit.test.ts +88 -0
  207. package/src/anthropic-federated-token-provider.integration.test.ts +248 -0
  208. package/src/anthropic-federated-token-provider.ts +249 -0
  209. package/src/anthropic-identity-client.fixtures.ts +237 -0
  210. package/src/anthropic-identity-client.integration.test.ts +342 -0
  211. package/src/anthropic-identity-client.ts +473 -0
  212. package/src/anthropic-identity-fixture-spec.integration.test.ts +169 -0
  213. package/src/anthropic-identity-name.integration.test.ts +83 -0
  214. package/src/anthropic-identity-name.ts +163 -0
  215. package/src/anthropic-identity-name.unit.test.ts +345 -0
  216. package/src/anthropic-openapi-contract.fixtures.ts +192 -0
  217. package/src/api-auth.ts +383 -0
  218. package/src/api-auth.unit.test.ts +837 -0
  219. package/src/api.smoke.test.ts +1225 -0
  220. package/src/api.ts +376 -0
  221. package/src/attachment-validation.ts +124 -0
  222. package/src/attachment-validation.unit.test.ts +218 -0
  223. package/src/caller-scope.ts +131 -0
  224. package/src/caller-scope.unit.test.ts +116 -0
  225. package/src/chat-credential-config.ts +22 -0
  226. package/src/chat-credentials-logging.unit.test.ts +92 -0
  227. package/src/chat-markers.ts +114 -0
  228. package/src/chat-markers.unit.test.ts +116 -0
  229. package/src/chat-service-provisioning-client.integration.test.ts +412 -0
  230. package/src/chat-service-provisioning-client.ts +221 -0
  231. package/src/chat.smoke.test.ts +1875 -0
  232. package/src/clock.ts +47 -0
  233. package/src/coding-task-store-provisioning-client.integration.test.ts +372 -0
  234. package/src/coding-task-store-provisioning-client.ts +156 -0
  235. package/src/crypto.ts +83 -0
  236. package/src/dev-auth-guard.ts +29 -0
  237. package/src/dev-auth-guard.unit.test.ts +51 -0
  238. package/src/errors.ts +77 -0
  239. package/src/errors.unit.test.ts +52 -0
  240. package/src/fixtures/anthropic-identity-recorded-fixtures.json +850 -0
  241. package/src/fixtures/anthropic-openapi/SDK_VERSION +1 -0
  242. package/src/fixtures/anthropic-openapi/mock-spec.json.gz +0 -0
  243. package/src/fixtures/chat-service-provisioning-cassette.json +156 -0
  244. package/src/fixtures/github-app-manifest-cassette.json +44 -0
  245. package/src/fixtures/google-auth-cassette.json +36 -0
  246. package/src/fixtures/http-chat-client-cassette.json +226 -0
  247. package/src/fixtures/k8s-cassette.json +470 -0
  248. package/src/fixtures/okta-auth-cassette.json +36 -0
  249. package/src/fixtures/slack-provision-cassette.json +12 -0
  250. package/src/fixtures/slack-provisioning-http-cassette.json +98 -0
  251. package/src/fixtures/task-store-provisioning-cassette.json +85 -0
  252. package/src/github-app-provisioning-client.integration.test.ts +149 -0
  253. package/src/github-app-provisioning-client.ts +161 -0
  254. package/src/github-app-provisioning-client.unit.test.ts +114 -0
  255. package/src/github-provisioning-service.ts +569 -0
  256. package/src/github-provisioning-service.unit.test.ts +123 -0
  257. package/src/google-auth-client.integration.test.ts +195 -0
  258. package/src/google-auth-client.ts +121 -0
  259. package/src/http-chat-client.integration.test.ts +554 -0
  260. package/src/http-chat-client.ts +533 -0
  261. package/src/http-chat-client.unit.test.ts +119 -0
  262. package/src/import-boundary.unit.test.ts +240 -0
  263. package/src/kubernetes-client.integration.test.ts +697 -0
  264. package/src/kubernetes-client.ts +1027 -0
  265. package/src/kubernetes-client.unit.test.ts +726 -0
  266. package/src/main.smoke.test.ts +91 -0
  267. package/src/main.ts +21 -0
  268. package/src/main.unit.test.ts +801 -0
  269. package/src/okta-auth-client.integration.test.ts +263 -0
  270. package/src/okta-auth-client.ts +152 -0
  271. package/src/openapi-schemas.ts +1310 -0
  272. package/src/openapi-schemas.unit.test.ts +348 -0
  273. package/src/per-agent-identity.fixtures.ts +305 -0
  274. package/src/platform-doc-route.ts +40 -0
  275. package/src/prisma-client.integration.test.ts +57 -0
  276. package/src/prisma-client.ts +187 -0
  277. package/src/prisma-client.unit.test.ts +178 -0
  278. package/src/prisma-tx.ts +23 -0
  279. package/src/push-content.ts +177 -0
  280. package/src/push-content.unit.test.ts +213 -0
  281. package/src/push-crypto.ts +135 -0
  282. package/src/push-crypto.unit.test.ts +66 -0
  283. package/src/push-sender.ts +169 -0
  284. package/src/push-sender.unit.test.ts +107 -0
  285. package/src/push-service.ts +332 -0
  286. package/src/push-service.unit.test.ts +679 -0
  287. package/src/push-toggle.ts +135 -0
  288. package/src/push-toggle.unit.test.ts +78 -0
  289. package/src/pwa-https-gating.smoke.test.ts +229 -0
  290. package/src/pwa-icons.unit.test.ts +41 -0
  291. package/src/pwa-routes.smoke.test.ts +276 -0
  292. package/src/pwa.ts +380 -0
  293. package/src/pwa.unit.test.ts +321 -0
  294. package/src/schema.unit.test.ts +122 -0
  295. package/src/self-serve-config.ts +52 -0
  296. package/src/self-serve-config.unit.test.ts +79 -0
  297. package/src/server.smoke.test.ts +273 -0
  298. package/src/server.ts +50 -0
  299. package/src/service-integration.ts +67 -0
  300. package/src/service-integration.unit.test.ts +129 -0
  301. package/src/slack-provisioning-client.integration.test.ts +308 -0
  302. package/src/slack-provisioning-client.ts +411 -0
  303. package/src/slack-provisioning-client.unit.test.ts +80 -0
  304. package/src/slack-provisioning-service.integration.test.ts +637 -0
  305. package/src/slack-provisioning-service.ts +451 -0
  306. package/src/slack-provisioning.integration.test.ts +395 -0
  307. package/src/specialty-manifest-loader.fixture.integration.test.ts +63 -0
  308. package/src/specialty-manifest-loader.ts +255 -0
  309. package/src/specialty-manifest-loader.unit.test.ts +301 -0
  310. package/src/specialty-registry.schema.unit.test.ts +35 -0
  311. package/src/specialty-registry.ts +236 -0
  312. package/src/specialty-registry.unit.test.ts +272 -0
  313. package/src/startup-policy-reconcile.ts +77 -0
  314. package/src/startup-policy-reconcile.unit.test.ts +251 -0
  315. package/src/theme-tokens.json +12 -0
  316. package/src/token-crypto.ts +60 -0
  317. package/src/token-crypto.unit.test.ts +182 -0
  318. package/src/trial-expiry-sweeper.integration.test.ts +318 -0
  319. package/src/trial-expiry-sweeper.ts +132 -0
  320. package/src/trial-expiry-sweeper.unit.test.ts +391 -0
  321. package/src/trial-expiry-warning-sweeper.integration.test.ts +468 -0
  322. package/src/trial-expiry-warning-sweeper.ts +447 -0
  323. package/src/trial-expiry-warning-sweeper.unit.test.ts +148 -0
  324. package/src/voice-contract.unit.test.ts +102 -0
  325. package/tsconfig.json +9 -0
@@ -0,0 +1,451 @@
1
+ /**
2
+ * admin/src/slack-provisioning-service.ts
3
+ * SlackProvisioningService — Slack app-manifest-creation/OAuth orchestration
4
+ * for connecting an already-existing agent to Slack.
5
+ *
6
+ * Extracted out of admin-ui.ts's inline /admin/provision/* handlers (UAP-1.1)
7
+ * so the same logic can be reused by both the legacy provisioning wizard
8
+ * (POST /admin/provision/start Slack branch, GET /admin/provision/complete,
9
+ * POST /admin/provision/xapp-token) and the new per-agent connect-slack
10
+ * routes (POST /admin/agents/:id/connect-slack, GET .../callback, POST
11
+ * .../app-token). Pure extraction — no behavior change.
12
+ *
13
+ * HTTP-specific mechanics (reading/writing the PROVISION_STATE_COOKIE,
14
+ * issuing redirects, rendering HTML) stay in the route handlers; this service
15
+ * owns the Slack API calls, the signed provision-state token payload, and the
16
+ * env/cron side effects.
17
+ */
18
+
19
+ import { sign, verify } from "hono/jwt";
20
+ import type { AdminUISlackClient } from "./admin-ui.ts";
21
+ import type { AgentCronJobService } from "./agent-cron-jobs.ts";
22
+ import type { AgentEnvService } from "./agent-envs.ts";
23
+ import type { AgentService } from "./agents.ts";
24
+ import { buildAgentManifest } from "./slack-provisioning-client.ts";
25
+
26
+ // ─── Constants ────────────────────────────────────────────────────────────────
27
+
28
+ /** Shared with admin-ui.ts — the signed cookie carrying provision state across the Slack OAuth redirect. */
29
+ export const PROVISION_STATE_COOKIE = "slack_provision_state";
30
+ export const PROVISION_STATE_TTL_SECONDS = 300; // 5 min
31
+
32
+ // ─── Types ────────────────────────────────────────────────────────────────────
33
+
34
+ interface ProvisionStatePayload {
35
+ agentId: string;
36
+ clientId: string;
37
+ clientSecret: string;
38
+ signingSecret: string;
39
+ appId: string;
40
+ /**
41
+ * The OAuth `redirect_uri` registered in the app manifest for this
42
+ * provisioning attempt. Persisted so completeConnect() replays the exact
43
+ * same value to `oauth.v2.access` (Slack requires the exchange's
44
+ * `redirect_uri` to match a registered one). Optional for backwards
45
+ * compatibility with any cookie signed before this field existed, in which
46
+ * case the caller-supplied fallback is used.
47
+ */
48
+ redirectUri?: string;
49
+ }
50
+
51
+ type StartConnectResult =
52
+ | {
53
+ ok: true;
54
+ /** Signed JWT to store in the PROVISION_STATE_COOKIE. */
55
+ provisionStateToken: string;
56
+ /** Slack OAuth v2 authorize URL to redirect the operator to. */
57
+ oauthRedirectUrl: string;
58
+ }
59
+ | { ok: false; error: string };
60
+
61
+ export type CompleteConnectResult =
62
+ | {
63
+ /** Provision-state cookie missing or invalid/expired — restart the flow. */
64
+ outcome: "invalid_state";
65
+ error: string;
66
+ }
67
+ | {
68
+ /** Valid state but no OAuth code param (e.g. user denied consent). Cookie must NOT be cleared by the caller in this case. */
69
+ outcome: "missing_code";
70
+ error: string;
71
+ }
72
+ | {
73
+ /** OAuth code exchange with Slack failed. */
74
+ outcome: "exchange_failed";
75
+ error: string;
76
+ }
77
+ | {
78
+ /** Success, and this is a reinstall (SLACK_APP_TOKEN was already set) — redirect straight to the agent page. */
79
+ outcome: "reinstalled";
80
+ agentId: string;
81
+ /**
82
+ * Set only for the combined create+connect flow (UAP-2.1): a non-fatal
83
+ * GitHub PAT storage failure that happened before this Slack flow began.
84
+ * The caller surfaces it as a warning on the landing page so the operator
85
+ * isn't misled into believing GH_TOKEN was stored.
86
+ */
87
+ ghConnectError?: string;
88
+ }
89
+ | {
90
+ /** Success, fresh provisioning — show the xapp-token page next. */
91
+ outcome: "needs_app_token";
92
+ agentId: string;
93
+ /** See `reinstalled.ghConnectError` — same non-fatal combined-flow warning. */
94
+ ghConnectError?: string;
95
+ };
96
+
97
+ type SaveAppTokenResult =
98
+ | { ok: true; agentId: string }
99
+ | { ok: false; agentId: string; error: string };
100
+
101
+ interface SlackAppManifestResult {
102
+ appId: string;
103
+ oauthRedirectUrl: string;
104
+ clientId: string;
105
+ clientSecret: string;
106
+ signingSecret: string;
107
+ }
108
+
109
+ interface SlackProvisioningServiceDeps {
110
+ slackClient: AdminUISlackClient;
111
+ agentService: Pick<AgentService, "getDetail" | "updateFields">;
112
+ agentEnvService: Pick<AgentEnvService, "patch" | "getConfigBundle">;
113
+ agentCronJobService: Pick<AgentCronJobService, "reconcileSystemCrons">;
114
+ sessionSecret: string;
115
+ appBaseUrl: string;
116
+ /** Env var keys that must be stored as secrets — mirrors SECRET_ENV_VARS. */
117
+ secretEnvVars: Set<string>;
118
+ }
119
+
120
+ // ─── Service ──────────────────────────────────────────────────────────────────
121
+
122
+ export class SlackProvisioningService {
123
+ constructor(private readonly deps: SlackProvisioningServiceDeps) {}
124
+
125
+ /**
126
+ * Calls apps.manifest.create for `agentName` and returns the raw Slack
127
+ * result (app id, OAuth authorize URL, and OAuth credentials). Low-level
128
+ * building block shared by startConnect() below and by the legacy POST
129
+ * /admin/provision/start wizard's Slack branch — the wizard needs the raw
130
+ * credentials to sign its own provision-state cookie (which additionally
131
+ * carries `githubOrg` for the GitHub-App-auto-provision sub-flow), so it
132
+ * calls this instead of startConnect().
133
+ *
134
+ * `redirectUri` is supplied by the caller and becomes the manifest's OAuth
135
+ * `redirect_urls` entry, so both the legacy wizard (passing
136
+ * `/admin/provision/complete`) and the new per-agent connect-slack route
137
+ * (passing `/admin/agents/:id/connect-slack/callback`) can reuse this same
138
+ * manifest-building logic while registering different OAuth callback URLs
139
+ * with Slack. Slack requires the `redirect_uri` sent to `oauth.v2.access`
140
+ * to match one of the app's registered `redirect_urls`, so the same value
141
+ * must be threaded through to completeConnect()'s exchange call below.
142
+ */
143
+ async createAppManifest(
144
+ agentName: string,
145
+ xoxpToken: string,
146
+ redirectUri: string,
147
+ ): Promise<SlackAppManifestResult> {
148
+ const manifest = buildAgentManifest(agentName, redirectUri);
149
+ return this.deps.slackClient.createAppManifest(xoxpToken, manifest);
150
+ }
151
+
152
+ /**
153
+ * Signs a provision-state token (for the PROVISION_STATE_COOKIE) carrying
154
+ * the given Slack OAuth credentials for `agentId`. Exposed so the legacy
155
+ * wizard can fold its own extra `githubOrg` field into the same signed
156
+ * payload shape that completeConnect() below verifies.
157
+ */
158
+ async signProvisionState(
159
+ agentId: string,
160
+ creds: {
161
+ clientId: string;
162
+ clientSecret: string;
163
+ signingSecret: string;
164
+ appId: string;
165
+ /**
166
+ * The manifest's registered OAuth `redirect_uri` for this attempt,
167
+ * replayed verbatim to `oauth.v2.access` by completeConnect().
168
+ */
169
+ redirectUri: string;
170
+ },
171
+ extra?: Record<string, unknown>,
172
+ ): Promise<string> {
173
+ const now = Math.floor(Date.now() / 1000);
174
+ return sign(
175
+ {
176
+ agentId,
177
+ ...creds,
178
+ iat: now,
179
+ exp: now + PROVISION_STATE_TTL_SECONDS,
180
+ ...extra,
181
+ },
182
+ this.deps.sessionSecret,
183
+ "HS256",
184
+ );
185
+ }
186
+
187
+ /**
188
+ * Creates the Slack app manifest for `agentId` via apps.manifest.create and
189
+ * returns a signed provision-state token (for the PROVISION_STATE_COOKIE)
190
+ * plus the Slack OAuth authorize URL to redirect to.
191
+ *
192
+ * Mirrors the Slack branch of the legacy POST /admin/provision/start
193
+ * handler (validation + apps.manifest.create + cookie payload), but for an
194
+ * agent that already exists — no agent-creation/rollback orchestration.
195
+ *
196
+ * `redirectUri` is threaded through to createAppManifest() (registered as
197
+ * the app's OAuth `redirect_urls`) and folded into the signed
198
+ * provision-state cookie so completeConnect() can replay the identical URL
199
+ * to `oauth.v2.access` — Slack rejects an exchange whose `redirect_uri`
200
+ * doesn't match a registered one. The per-agent connect-slack route passes
201
+ * its own `/admin/agents/:id/connect-slack/callback` here.
202
+ *
203
+ * `ghConnectError` (UAP-2.1) is an optional non-fatal GitHub PAT storage
204
+ * failure from the combined create+connect flow. When present it's folded
205
+ * into the signed provision-state cookie so it survives Slack's OAuth round
206
+ * trip and completeConnect() can hand it back to the caller to surface on the
207
+ * post-callback landing page — otherwise the failure would be silently
208
+ * swallowed and the operator misled into believing GH_TOKEN was stored.
209
+ */
210
+ async startConnect(
211
+ agentId: string,
212
+ xoxpToken: string | undefined,
213
+ redirectUri: string,
214
+ ghConnectError?: string,
215
+ ): Promise<StartConnectResult> {
216
+ if (!xoxpToken?.startsWith("xoxe.xoxp-")) {
217
+ return {
218
+ ok: false,
219
+ error: "Slack app configuration token must start with xoxe.xoxp-",
220
+ };
221
+ }
222
+
223
+ const agent = await this.deps.agentService.getDetail(agentId);
224
+ if (!agent) {
225
+ return { ok: false, error: "Agent not found." };
226
+ }
227
+
228
+ let slackResult: SlackAppManifestResult;
229
+ try {
230
+ slackResult = await this.createAppManifest(
231
+ agent.name,
232
+ xoxpToken,
233
+ redirectUri,
234
+ );
235
+ } catch (err) {
236
+ const msg =
237
+ err instanceof Error
238
+ ? err.message
239
+ : "Unknown error creating Slack app.";
240
+ return { ok: false, error: msg };
241
+ }
242
+
243
+ const { appId, oauthRedirectUrl, clientId, clientSecret, signingSecret } =
244
+ slackResult;
245
+
246
+ const provisionStateToken = await this.signProvisionState(
247
+ agentId,
248
+ {
249
+ clientId,
250
+ clientSecret,
251
+ signingSecret,
252
+ appId,
253
+ redirectUri,
254
+ },
255
+ ghConnectError ? { ghConnectError } : undefined,
256
+ );
257
+
258
+ return { ok: true, provisionStateToken, oauthRedirectUrl };
259
+ }
260
+
261
+ /**
262
+ * Verifies the provision-state token, exchanges the OAuth `code` for a bot
263
+ * token, and stores SLACK_BOT_TOKEN. Mirrors the legacy
264
+ * GET /admin/provision/complete handler exactly.
265
+ *
266
+ * The caller (route handler) is responsible for reading the raw cookie
267
+ * value and for clearing it — this method never has cookie access; it only
268
+ * decides whether the "invalid state" outcome should tell the caller to
269
+ * clear the cookie (yes, for invalid_state) or not (no, for missing_code,
270
+ * where the cookie must survive a retry).
271
+ *
272
+ * `fallbackRedirectUri` is used for the OAuth exchange only when the signed
273
+ * state cookie predates the `redirectUri` field (cookies signed before this
274
+ * fix). New cookies carry their own per-flow redirect URI, so callers pass
275
+ * their own flow's URL as the fallback to preserve behavior for in-flight
276
+ * legacy sessions.
277
+ */
278
+ async completeConnect(
279
+ rawStateCookie: string | undefined,
280
+ code: string | undefined,
281
+ fallbackRedirectUri: string,
282
+ ): Promise<CompleteConnectResult> {
283
+ if (!rawStateCookie) {
284
+ return {
285
+ outcome: "invalid_state",
286
+ error:
287
+ "Provision session expired or missing. Please start the provisioning flow again.",
288
+ };
289
+ }
290
+
291
+ let provisionState: ProvisionStatePayload;
292
+ // Non-fatal combined-flow warning (UAP-2.1), threaded through the signed
293
+ // cookie so it survives Slack's OAuth round trip. Declared out here so it
294
+ // escapes the verify() try block; kept separate from the
295
+ // ProvisionStatePayload shape since it's flow-specific, not Slack state.
296
+ let ghConnectError: string | undefined;
297
+ try {
298
+ const payload = (await verify(
299
+ rawStateCookie,
300
+ this.deps.sessionSecret,
301
+ "HS256",
302
+ )) as Record<string, unknown>;
303
+ if (
304
+ typeof payload.agentId !== "string" ||
305
+ typeof payload.clientId !== "string" ||
306
+ typeof payload.clientSecret !== "string" ||
307
+ typeof payload.signingSecret !== "string" ||
308
+ typeof payload.appId !== "string"
309
+ ) {
310
+ throw new Error("invalid payload shape");
311
+ }
312
+ provisionState = {
313
+ agentId: payload.agentId,
314
+ clientId: payload.clientId,
315
+ clientSecret: payload.clientSecret,
316
+ signingSecret: payload.signingSecret,
317
+ appId: payload.appId,
318
+ redirectUri:
319
+ typeof payload.redirectUri === "string"
320
+ ? payload.redirectUri
321
+ : undefined,
322
+ };
323
+ ghConnectError =
324
+ typeof payload.ghConnectError === "string"
325
+ ? payload.ghConnectError
326
+ : undefined;
327
+ } catch {
328
+ return {
329
+ outcome: "invalid_state",
330
+ error:
331
+ "Provision session expired or invalid. Please start the provisioning flow again.",
332
+ };
333
+ }
334
+
335
+ if (!code) {
336
+ return {
337
+ outcome: "missing_code",
338
+ error:
339
+ "Authorization was not completed (no OAuth code received). Please restart the provisioning flow from the beginning.",
340
+ };
341
+ }
342
+
343
+ let botToken: string;
344
+ let slackUserId: string;
345
+ try {
346
+ const result = await this.deps.slackClient.exchangeOAuthCode(
347
+ code,
348
+ provisionState.clientId,
349
+ provisionState.clientSecret,
350
+ provisionState.redirectUri ?? fallbackRedirectUri,
351
+ );
352
+ botToken = result.botToken;
353
+
354
+ // Resolve the bot's own Slack user id so it can be persisted to
355
+ // agent.slackId (UAP-1.3) — required for the Sync Manifest button to
356
+ // appear. Folded into the same try/catch as the exchange: an auth.test
357
+ // failure right after a successful exchange is treated the same as an
358
+ // exchange failure so the flow doesn't half-complete with a bot token
359
+ // stored but no resolved slackId.
360
+ const authTestResult = await this.deps.slackClient.authTest(botToken);
361
+ slackUserId = authTestResult.userId;
362
+ } catch (err) {
363
+ const msg =
364
+ err instanceof Error
365
+ ? err.message
366
+ : "Unknown error exchanging OAuth code.";
367
+ return {
368
+ outcome: "exchange_failed",
369
+ error: `OAuth exchange failed: ${msg}`,
370
+ };
371
+ }
372
+
373
+ const existingBundle = await this.deps.agentEnvService.getConfigBundle(
374
+ provisionState.agentId,
375
+ );
376
+ await this.deps.agentEnvService.patch(
377
+ provisionState.agentId,
378
+ { SLACK_BOT_TOKEN: botToken },
379
+ this.deps.secretEnvVars,
380
+ );
381
+
382
+ // Persist unconditionally on every successful completion (both
383
+ // "reinstalled" and "needs_app_token" outcomes) — a bot reinstall can
384
+ // also resolve a fresh/different user id.
385
+ await this.deps.agentService.updateFields(provisionState.agentId, {
386
+ slackId: slackUserId,
387
+ });
388
+
389
+ if (existingBundle?.env.SLACK_APP_TOKEN) {
390
+ return {
391
+ outcome: "reinstalled",
392
+ agentId: provisionState.agentId,
393
+ ghConnectError,
394
+ };
395
+ }
396
+
397
+ return {
398
+ outcome: "needs_app_token",
399
+ agentId: provisionState.agentId,
400
+ ghConnectError,
401
+ };
402
+ }
403
+
404
+ /**
405
+ * Stores SLACK_APP_TOKEN and reconciles system crons. Mirrors the legacy
406
+ * POST /admin/provision/xapp-token handler exactly.
407
+ */
408
+ async saveAppToken(
409
+ agentId: string | undefined,
410
+ xappToken: string | undefined,
411
+ ): Promise<SaveAppTokenResult> {
412
+ if (!agentId) {
413
+ return { ok: false, agentId: "", error: "Agent ID is required." };
414
+ }
415
+
416
+ if (!xappToken?.startsWith("xapp-")) {
417
+ return {
418
+ ok: false,
419
+ agentId,
420
+ error: "App-Level Token must start with xapp-",
421
+ };
422
+ }
423
+
424
+ try {
425
+ // SHIPWRIGHT_AGENT_API_KEY and SHIPWRIGHT_TASK_STORE_TOKEN are NOT minted
426
+ // here. K8s-managed agents already have both minted straight into the K8s
427
+ // Secret by provisioner.provision() (agent-provisioner.ts) — that Secret is
428
+ // the sole source of truth (via secretKeyRef in the Deployment manifest).
429
+ // Self-hosted agents never run the containerized entrypoint that reads
430
+ // SHIPWRIGHT_AGENT_API_KEY (agent/src/runtime/entrypoint.ts), and use the
431
+ // GitHub-backed task-store CLI config instead of a bearer token, so they
432
+ // don't need either key in AgentEnv. Minting them here previously orphaned
433
+ // a second, different, unused live credential per key on every provision.
434
+ await this.deps.agentEnvService.patch(
435
+ agentId,
436
+ { SLACK_APP_TOKEN: xappToken },
437
+ this.deps.secretEnvVars,
438
+ );
439
+
440
+ await this.deps.agentCronJobService.reconcileSystemCrons(agentId);
441
+
442
+ return { ok: true, agentId };
443
+ } catch (err) {
444
+ const msg =
445
+ err instanceof Error
446
+ ? err.message
447
+ : "Unknown error completing provisioning.";
448
+ return { ok: false, agentId, error: msg };
449
+ }
450
+ }
451
+ }