@opengeni/api-router 0.30.3 → 1.0.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 (434) hide show
  1. package/dist/access-grant-rls.d.ts +3 -0
  2. package/dist/admission-trace.d.ts +8 -0
  3. package/dist/api-websocket.d.ts +18 -0
  4. package/dist/app.d.ts +50 -2
  5. package/dist/app.js +12 -1
  6. package/dist/artifact-catalog-cursor.d.ts +27 -0
  7. package/dist/auth/managed-auth-attempt-context.d.ts +10 -0
  8. package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
  9. package/dist/auth/managed-auth-session-adapter.d.ts +6 -0
  10. package/dist/auth/managed-auth.d.ts +102 -3
  11. package/dist/auth/managed-email.d.ts +29 -0
  12. package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
  13. package/dist/auth/new-signups-gate.d.ts +23 -0
  14. package/dist/auth/organization-user-setup.d.ts +80 -0
  15. package/dist/auth/signup-funnel-metrics.d.ts +44 -0
  16. package/dist/browser-controller-authority.d.ts +1 -61
  17. package/dist/browser-ephemeral.d.ts +5 -0
  18. package/dist/browser-state-authority.d.ts +1 -27
  19. package/dist/chunk-6TZ5LG3O.js +123948 -0
  20. package/dist/chunk-6TZ5LG3O.js.map +1 -0
  21. package/dist/chunk-OGQDXVXY.js +18 -0
  22. package/dist/chunk-OGQDXVXY.js.map +1 -0
  23. package/dist/claude-subscription-account-conflicts.d.ts +1 -0
  24. package/dist/claude-subscription-account-usage.d.ts +39 -0
  25. package/dist/claude-subscription-oauth.d.ts +25 -0
  26. package/dist/claude-subscription-profile.d.ts +9 -0
  27. package/dist/claude-subscription-usage.d.ts +44 -0
  28. package/dist/claude-workspace-connection.d.ts +15 -0
  29. package/dist/codemode.d.ts +32 -1
  30. package/dist/company-brain-okf.d.ts +24 -0
  31. package/dist/connected-machine-computer-access.d.ts +11 -0
  32. package/dist/connection-ownership.d.ts +54 -0
  33. package/dist/controller-data-plane.d.ts +32 -0
  34. package/dist/editable-artifact-source-session.d.ts +3 -0
  35. package/dist/editable-artifact-websocket.d.ts +10 -18
  36. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  37. package/dist/fatal-process-boundary.d.ts +25 -0
  38. package/dist/github-access.d.ts +31 -2
  39. package/dist/http/acting-person.d.ts +24 -0
  40. package/dist/http/api-contract-compat.d.ts +19 -0
  41. package/dist/http/api-error.d.ts +27 -1
  42. package/dist/http/cors.d.ts +17 -0
  43. package/dist/http/deprecation.d.ts +42 -0
  44. package/dist/http/interaction-control-error.d.ts +13 -0
  45. package/dist/http/keyed-admission.d.ts +12 -0
  46. package/dist/http/local-browser-boundary.d.ts +72 -0
  47. package/dist/http/metrics-listener.d.ts +22 -0
  48. package/dist/http/path-identifier.d.ts +17 -0
  49. package/dist/http/registered-route-label.d.ts +16 -0
  50. package/dist/http/request-body.d.ts +27 -0
  51. package/dist/http/request-source.d.ts +52 -0
  52. package/dist/http/sse.d.ts +17 -6
  53. package/dist/http/unmatched-route.d.ts +18 -0
  54. package/dist/http/user-content.d.ts +36 -0
  55. package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
  56. package/dist/index.d.ts +10 -3
  57. package/dist/index.js +1091 -162
  58. package/dist/index.js.map +1 -1
  59. package/dist/integration-endpoint-test.d.ts +20 -0
  60. package/dist/integrations/api-integrations.d.ts +2 -2
  61. package/dist/integrations/atlassian.d.ts +29 -3
  62. package/dist/integrations/connect-authority.d.ts +1 -0
  63. package/dist/integrations/connect-callback-return.d.ts +6 -0
  64. package/dist/integrations/fiken.d.ts +16 -2
  65. package/dist/integrations/github-app-connect.d.ts +47 -0
  66. package/dist/integrations/github-installation-proof.d.ts +3 -0
  67. package/dist/integrations/github-lens-connect.d.ts +116 -0
  68. package/dist/integrations/github-skill-source.d.ts +1 -5
  69. package/dist/integrations/google-drive.d.ts +24 -0
  70. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  71. package/dist/integrations/marketplace-plugin.d.ts +2691 -0
  72. package/dist/integrations/oauth-client.d.ts +178 -18
  73. package/dist/integrations/oauth-profiles.d.ts +222 -0
  74. package/dist/integrations/oauth-return-path.d.ts +1 -0
  75. package/dist/integrations/personal-github-repositories.d.ts +84 -0
  76. package/dist/integrations/personal-github.d.ts +42 -0
  77. package/dist/integrations/pr-review-provider.d.ts +22 -0
  78. package/dist/integrations/provider-oauth.d.ts +19 -1
  79. package/dist/integrations/slack-app-home.d.ts +24 -0
  80. package/dist/integrations/slack-bot.d.ts +367 -12
  81. package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
  82. package/dist/integrations/slack-install.d.ts +20 -0
  83. package/dist/integrations/slack-interactions.d.ts +107 -9
  84. package/dist/integrations/slack-mrkdwn.d.ts +41 -0
  85. package/dist/integrations/slack-routing.d.ts +120 -0
  86. package/dist/integrations/slack-session-defaults.d.ts +58 -0
  87. package/dist/integrations/slack-task-file-upload.d.ts +36 -0
  88. package/dist/integrations/social-oauth.d.ts +7 -0
  89. package/dist/interaction-agent-access.d.ts +15 -0
  90. package/dist/interaction-frame-proxy.d.ts +68 -0
  91. package/dist/interaction-metrics.d.ts +2 -0
  92. package/dist/mcp/action-catalog-types.d.ts +9 -0
  93. package/dist/mcp/action-catalog.gen.d.ts +2 -0
  94. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  95. package/dist/mcp/child-read-evidence.d.ts +9 -0
  96. package/dist/mcp/command-read.d.ts +24 -0
  97. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  98. package/dist/mcp/contract-input.d.ts +11 -0
  99. package/dist/mcp/documents.d.ts +7 -5
  100. package/dist/mcp/files.d.ts +2 -2
  101. package/dist/mcp/first-party-tool-permissions.d.ts +650 -0
  102. package/dist/mcp/knowledge-entries.d.ts +5 -0
  103. package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
  104. package/dist/mcp/receipts.d.ts +9 -0
  105. package/dist/mcp/request-abort.d.ts +19 -0
  106. package/dist/mcp/scheduled-task-input.d.ts +1189 -0
  107. package/dist/mcp/scheduled-task-view.d.ts +22 -4
  108. package/dist/mcp/server.d.ts +255 -3
  109. package/dist/mcp/session-event-view.d.ts +94 -0
  110. package/dist/mcp/session-view.d.ts +66 -2
  111. package/dist/mcp/session-wait.d.ts +174 -0
  112. package/dist/mcp-oauth.d.ts +52 -0
  113. package/dist/model-catalog.d.ts +11 -31
  114. package/dist/observability.d.ts +3 -1
  115. package/dist/organization-mcp.d.ts +52 -0
  116. package/dist/organization-recovery-notifications.d.ts +41 -0
  117. package/dist/routes/analytics-consent.d.ts +26 -0
  118. package/dist/routes/api-integrations.d.ts +25 -0
  119. package/dist/routes/api-keys.d.ts +17 -0
  120. package/dist/routes/artifact-catalog.d.ts +6 -0
  121. package/dist/routes/automations.d.ts +19 -0
  122. package/dist/routes/billing.d.ts +49 -0
  123. package/dist/routes/browser-sessions.d.ts +59 -2
  124. package/dist/routes/claude-subscription-accounts.d.ts +3 -0
  125. package/dist/routes/claude-subscription-oauth.d.ts +3 -0
  126. package/dist/routes/client-errors.d.ts +146 -0
  127. package/dist/routes/codex.d.ts +76 -2
  128. package/dist/routes/company-brain.d.ts +3 -0
  129. package/dist/routes/company-profile.d.ts +2 -1
  130. package/dist/routes/computer-sessions.d.ts +14 -0
  131. package/dist/routes/connect.d.ts +5 -0
  132. package/dist/routes/editable-artifacts.d.ts +2 -0
  133. package/dist/routes/external-identity-links.d.ts +6 -0
  134. package/dist/routes/feedback.d.ts +3 -0
  135. package/dist/routes/github-browser-pages.d.ts +12 -0
  136. package/dist/routes/github.d.ts +2 -0
  137. package/dist/routes/insights-response-cache.d.ts +10 -0
  138. package/dist/routes/insights-usage.d.ts +12 -0
  139. package/dist/routes/insights.d.ts +30 -1
  140. package/dist/routes/knowledge.d.ts +3 -0
  141. package/dist/routes/managed-auth-session-sets.d.ts +26 -0
  142. package/dist/routes/managed-onboarding.d.ts +29 -0
  143. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  144. package/dist/routes/model-connection-access.d.ts +3 -0
  145. package/dist/routes/organization-integration-policy.d.ts +3 -0
  146. package/dist/routes/organization-integrations.d.ts +6 -0
  147. package/dist/routes/organization-mcp-connections.d.ts +3 -0
  148. package/dist/routes/organization-model-providers.d.ts +3 -0
  149. package/dist/routes/organization-recovery.d.ts +19 -0
  150. package/dist/routes/organization-service-accounts.d.ts +3 -0
  151. package/dist/routes/organization-sessions.d.ts +26 -0
  152. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  153. package/dist/routes/personal-github.d.ts +3 -0
  154. package/dist/routes/pr-review-github.d.ts +3 -0
  155. package/dist/routes/pr-review.d.ts +3 -0
  156. package/dist/routes/session-artifact-associations.d.ts +4 -0
  157. package/dist/routes/session-history-imports.d.ts +9 -0
  158. package/dist/routes/sessions.d.ts +44 -6
  159. package/dist/routes/skill-content.d.ts +4 -0
  160. package/dist/routes/skill-install-authority.d.ts +6 -0
  161. package/dist/routes/subscription-account-pools.d.ts +34 -0
  162. package/dist/routes/subscription-pool-access.d.ts +16 -0
  163. package/dist/routes/supergrok.d.ts +6 -1
  164. package/dist/routes/usage-allowances.d.ts +12 -0
  165. package/dist/routes/user-resource-authorities.d.ts +3 -0
  166. package/dist/routes/workspace-artifacts.d.ts +2 -1
  167. package/dist/routes/workspace-capture.d.ts +10 -2
  168. package/dist/routes/workspace-integrations.d.ts +35 -0
  169. package/dist/routes/workspace-learning.d.ts +3 -0
  170. package/dist/routes/workspace-model-providers.d.ts +4 -0
  171. package/dist/routes/workspaces.d.ts +29 -0
  172. package/dist/sandbox/auth-callout.d.ts +8 -0
  173. package/dist/sandbox/channel-a.d.ts +19 -1
  174. package/dist/sandbox/connection-authority.d.ts +3 -0
  175. package/dist/sandbox/enrollment.d.ts +9 -1
  176. package/dist/sandbox/machines.d.ts +5 -2
  177. package/dist/sandbox/metrics-ingestion.d.ts +28 -20
  178. package/dist/sandbox/rematerialize.d.ts +2 -0
  179. package/dist/sandbox/session-attach-variable-sets.d.ts +30 -0
  180. package/dist/sandbox/viewer.d.ts +26 -1
  181. package/dist/sandbox-file-artifacts.d.ts +50 -0
  182. package/dist/scheduled-task-deletion.d.ts +13 -0
  183. package/dist/session-create-observability.d.ts +5 -0
  184. package/dist/session-event-view-MPS2OZD3.js +292 -0
  185. package/dist/session-event-view-MPS2OZD3.js.map +1 -0
  186. package/dist/site-session-origin.d.ts +13 -0
  187. package/dist/site-uploads.d.ts +41 -0
  188. package/dist/slack-reaction-files.d.ts +1 -1
  189. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  190. package/dist/temporal-schedule-sync.d.ts +26 -0
  191. package/dist/transcription/providers/xai-subscription.d.ts +1 -0
  192. package/dist/transcription/service.d.ts +2 -1
  193. package/dist/work-discovery-observability.d.ts +33 -0
  194. package/dist/workspace-artifact-content.d.ts +28 -0
  195. package/dist/workspace-artifact-provenance.d.ts +7 -0
  196. package/dist/workspace-delete-observability.d.ts +10 -0
  197. package/dist/workspace-deletion.d.ts +6 -0
  198. package/dist/workspace-state-projection.d.ts +3 -5
  199. package/dist/workspace-tool-gateway-observability.d.ts +17 -0
  200. package/dist/workspace-tool-gateway.d.ts +123 -0
  201. package/dist/workspace-webhook-dispatch.d.ts +26 -0
  202. package/package.json +23 -19
  203. package/src/access-grant-rls.ts +75 -0
  204. package/src/admission-trace.ts +14 -0
  205. package/src/api-websocket.ts +23 -0
  206. package/src/app.ts +1967 -252
  207. package/src/artifact-catalog-cursor.ts +60 -0
  208. package/src/auth/managed-auth-attempt-context.ts +70 -0
  209. package/src/auth/managed-auth-rate-limits.ts +252 -0
  210. package/src/auth/managed-auth-session-adapter.ts +222 -0
  211. package/src/auth/managed-auth.ts +536 -60
  212. package/src/auth/managed-email.ts +174 -0
  213. package/src/auth/managed-sign-in-notifications.ts +81 -0
  214. package/src/auth/new-signups-gate.ts +49 -0
  215. package/src/auth/organization-user-setup.ts +307 -0
  216. package/src/auth/signup-funnel-metrics.ts +164 -0
  217. package/src/browser-controller-authority.ts +1 -186
  218. package/src/browser-ephemeral.ts +39 -0
  219. package/src/browser-state-authority.ts +1 -236
  220. package/src/claude-subscription-account-conflicts.ts +12 -0
  221. package/src/claude-subscription-account-usage.ts +48 -0
  222. package/src/claude-subscription-oauth.ts +362 -0
  223. package/src/claude-subscription-profile.ts +73 -0
  224. package/src/claude-subscription-usage.ts +97 -0
  225. package/src/claude-workspace-connection.ts +82 -0
  226. package/src/codemode.ts +194 -30
  227. package/src/codex-realtime.ts +8 -2
  228. package/src/company-brain-okf.ts +340 -0
  229. package/src/connected-machine-computer-access.ts +33 -0
  230. package/src/connection-ownership.ts +199 -0
  231. package/src/controller-data-plane.ts +81 -0
  232. package/src/editable-artifact-native-kernel.ts +26 -15
  233. package/src/editable-artifact-office-import.ts +28 -3
  234. package/src/editable-artifact-production.ts +23 -7
  235. package/src/editable-artifact-source-session.ts +123 -0
  236. package/src/editable-artifact-websocket.ts +27 -21
  237. package/src/editable-artifact-workspace-files.ts +31 -6
  238. package/src/fatal-process-boundary.ts +246 -0
  239. package/src/gateway-realtime.ts +1 -1
  240. package/src/github-access.ts +147 -5
  241. package/src/http/acting-person.ts +71 -0
  242. package/src/http/api-contract-compat.ts +49 -0
  243. package/src/http/api-error.ts +115 -1
  244. package/src/http/auth.ts +48 -0
  245. package/src/http/cors.ts +67 -0
  246. package/src/http/deprecation.ts +118 -0
  247. package/src/http/interaction-control-error.ts +164 -0
  248. package/src/http/keyed-admission.ts +29 -0
  249. package/src/http/local-browser-boundary.ts +343 -0
  250. package/src/http/metrics-listener.ts +57 -0
  251. package/src/http/path-identifier.ts +54 -0
  252. package/src/http/registered-route-label.ts +34 -0
  253. package/src/http/request-body.ts +127 -0
  254. package/src/http/request-source.ts +178 -0
  255. package/src/http/sse.ts +486 -132
  256. package/src/http/unmatched-route.ts +60 -0
  257. package/src/http/user-content.ts +158 -0
  258. package/src/http/websocket-upgrade-dispatch.ts +29 -0
  259. package/src/index.ts +157 -52
  260. package/src/integration-endpoint-test.ts +291 -0
  261. package/src/integrations/api-integrations.ts +98 -11
  262. package/src/integrations/atlassian.ts +474 -136
  263. package/src/integrations/connect-authority.ts +2 -0
  264. package/src/integrations/connect-callback-return.ts +86 -0
  265. package/src/integrations/fiken.ts +354 -64
  266. package/src/integrations/github-app-connect.ts +435 -0
  267. package/src/integrations/github-installation-proof.ts +60 -0
  268. package/src/integrations/github-lens-connect.ts +76 -0
  269. package/src/integrations/github-skill-source.ts +2 -142
  270. package/src/integrations/google-drive.ts +717 -225
  271. package/src/integrations/integration-commit-authority.ts +69 -0
  272. package/src/integrations/marketplace-plugin.ts +48 -0
  273. package/src/integrations/oauth-client.ts +1356 -459
  274. package/src/integrations/oauth-profiles.ts +537 -0
  275. package/src/integrations/oauth-return-path.ts +18 -0
  276. package/src/integrations/personal-github-repositories.ts +674 -0
  277. package/src/integrations/personal-github.ts +825 -0
  278. package/src/integrations/pr-review-provider.ts +246 -0
  279. package/src/integrations/provider-oauth.ts +250 -22
  280. package/src/integrations/slack-app-home.ts +300 -0
  281. package/src/integrations/slack-bot.ts +963 -239
  282. package/src/integrations/slack-file-upload-flow.ts +147 -0
  283. package/src/integrations/slack-install.ts +83 -0
  284. package/src/integrations/slack-interactions.ts +3167 -474
  285. package/src/integrations/slack-mrkdwn.ts +193 -0
  286. package/src/integrations/slack-routing.ts +337 -0
  287. package/src/integrations/slack-session-defaults.ts +224 -0
  288. package/src/integrations/slack-task-file-upload.ts +323 -0
  289. package/src/integrations/social-oauth.ts +213 -15
  290. package/src/interaction-agent-access.ts +48 -0
  291. package/src/interaction-frame-proxy.ts +409 -0
  292. package/src/interaction-metrics.ts +30 -0
  293. package/src/mcp/action-catalog-types.ts +9 -0
  294. package/src/mcp/action-catalog.gen.ts +6696 -0
  295. package/src/mcp/capability-account-readiness.ts +17 -0
  296. package/src/mcp/child-read-evidence.ts +19 -0
  297. package/src/mcp/command-read.ts +109 -0
  298. package/src/mcp/company-profile-agent-admin.ts +198 -0
  299. package/src/mcp/contract-input.ts +141 -0
  300. package/src/mcp/documents.ts +65 -377
  301. package/src/mcp/editable-artifacts.ts +12 -6
  302. package/src/mcp/files.ts +80 -43
  303. package/src/mcp/first-party-tool-permissions.ts +305 -0
  304. package/src/mcp/knowledge-entries.ts +322 -0
  305. package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
  306. package/src/mcp/receipts.ts +34 -0
  307. package/src/mcp/request-abort.ts +44 -0
  308. package/src/mcp/scheduled-task-input.ts +76 -0
  309. package/src/mcp/scheduled-task-view.ts +44 -3
  310. package/src/mcp/server.ts +3030 -1267
  311. package/src/mcp/session-event-view.ts +383 -0
  312. package/src/mcp/session-view.ts +84 -3
  313. package/src/mcp/session-wait.ts +665 -0
  314. package/src/mcp-oauth.ts +1123 -0
  315. package/src/model-catalog.ts +91 -333
  316. package/src/observability.ts +15 -1
  317. package/src/organization-mcp.ts +397 -0
  318. package/src/organization-recovery-notifications.ts +103 -0
  319. package/src/routes/analytics-consent.ts +123 -0
  320. package/src/routes/api-integrations.ts +364 -74
  321. package/src/routes/api-keys.ts +336 -10
  322. package/src/routes/artifact-catalog.ts +279 -0
  323. package/src/routes/automations.ts +768 -0
  324. package/src/routes/billing.ts +461 -46
  325. package/src/routes/browser-sessions.ts +780 -112
  326. package/src/routes/canonical-human-identities.ts +90 -14
  327. package/src/routes/capabilities.ts +107 -6
  328. package/src/routes/channels.ts +20 -3
  329. package/src/routes/claude-subscription-accounts.ts +334 -0
  330. package/src/routes/claude-subscription-oauth.ts +142 -0
  331. package/src/routes/client-errors.ts +362 -0
  332. package/src/routes/codex.ts +833 -116
  333. package/src/routes/company-brain.ts +322 -0
  334. package/src/routes/company-profile.ts +65 -1
  335. package/src/routes/computer-sessions.ts +557 -106
  336. package/src/routes/connect.ts +1938 -0
  337. package/src/routes/connections.ts +1039 -349
  338. package/src/routes/documents.ts +488 -287
  339. package/src/routes/editable-artifacts.ts +79 -9
  340. package/src/routes/enrollments.ts +135 -39
  341. package/src/routes/environments.ts +154 -58
  342. package/src/routes/external-identity-links.ts +201 -0
  343. package/src/routes/feedback.ts +113 -0
  344. package/src/routes/files.ts +267 -33
  345. package/src/routes/github-browser-pages.ts +162 -0
  346. package/src/routes/github.ts +446 -144
  347. package/src/routes/insights-response-cache.ts +83 -0
  348. package/src/routes/insights-usage.ts +345 -0
  349. package/src/routes/insights.ts +153 -19
  350. package/src/routes/install.ts +133 -38
  351. package/src/routes/integration-facets.ts +107 -33
  352. package/src/routes/interaction-resources.ts +30 -3
  353. package/src/routes/knowledge.ts +488 -0
  354. package/src/routes/machines.ts +313 -15
  355. package/src/routes/managed-auth-session-sets.ts +1218 -0
  356. package/src/routes/managed-onboarding.ts +337 -0
  357. package/src/routes/managed-sign-in-methods.ts +433 -0
  358. package/src/routes/memory-slack-publications.ts +3 -2
  359. package/src/routes/model-connection-access.ts +208 -0
  360. package/src/routes/organization-integration-policy.ts +116 -0
  361. package/src/routes/organization-integrations.ts +326 -0
  362. package/src/routes/organization-mcp-connections.ts +178 -0
  363. package/src/routes/organization-memberships.ts +1019 -27
  364. package/src/routes/organization-model-providers.ts +293 -0
  365. package/src/routes/organization-recovery.ts +325 -0
  366. package/src/routes/organization-service-accounts.ts +128 -0
  367. package/src/routes/organization-sessions.ts +350 -0
  368. package/src/routes/personal-github-git-broker.ts +785 -0
  369. package/src/routes/personal-github.ts +370 -0
  370. package/src/routes/plugins.ts +251 -22
  371. package/src/routes/pr-review-github.ts +881 -0
  372. package/src/routes/pr-review.ts +622 -0
  373. package/src/routes/preference-registry.ts +12 -93
  374. package/src/routes/rigs.ts +167 -50
  375. package/src/routes/scheduled-tasks.ts +346 -41
  376. package/src/routes/session-artifact-associations.ts +87 -0
  377. package/src/routes/session-history-imports.ts +280 -0
  378. package/src/routes/sessions.ts +2306 -264
  379. package/src/routes/skill-content.ts +297 -0
  380. package/src/routes/skill-install-authority.ts +64 -0
  381. package/src/routes/skills.ts +62 -9
  382. package/src/routes/social.ts +46 -18
  383. package/src/routes/subscription-account-pools.ts +436 -0
  384. package/src/routes/subscription-pool-access.ts +126 -0
  385. package/src/routes/supergrok.ts +219 -303
  386. package/src/routes/transcription-recordings.ts +38 -3
  387. package/src/routes/transcriptions.ts +4 -0
  388. package/src/routes/usage-allowances.ts +378 -0
  389. package/src/routes/user-resource-authorities.ts +138 -0
  390. package/src/routes/video-generation.ts +14 -10
  391. package/src/routes/workspace-artifacts.ts +275 -69
  392. package/src/routes/workspace-capture.ts +21 -0
  393. package/src/routes/workspace-integrations.ts +576 -0
  394. package/src/routes/workspace-learning.ts +159 -0
  395. package/src/routes/workspace-model-providers.ts +148 -0
  396. package/src/routes/workspace-state.ts +38 -25
  397. package/src/routes/workspaces.ts +1004 -127
  398. package/src/sandbox/auth-callout.ts +96 -19
  399. package/src/sandbox/channel-a.ts +323 -24
  400. package/src/sandbox/connection-authority.ts +3 -0
  401. package/src/sandbox/enrollment.ts +81 -6
  402. package/src/sandbox/machines.ts +218 -76
  403. package/src/sandbox/metrics-ingestion.ts +411 -62
  404. package/src/sandbox/rematerialize.ts +121 -19
  405. package/src/sandbox/session-attach-variable-sets.ts +131 -0
  406. package/src/sandbox/viewer.ts +280 -25
  407. package/src/sandbox-file-artifacts.ts +413 -0
  408. package/src/scheduled-task-deletion.ts +138 -0
  409. package/src/session-create-observability.ts +46 -0
  410. package/src/session-realtime-context.ts +6 -3
  411. package/src/site-session-origin.ts +48 -0
  412. package/src/site-uploads.ts +224 -0
  413. package/src/slack-reaction-files.ts +16 -5
  414. package/src/temporal-schedule-cleanup.ts +13 -0
  415. package/src/temporal-schedule-sync.ts +74 -0
  416. package/src/transcription/providers/codex-subscription.ts +2 -0
  417. package/src/transcription/providers/openai.ts +1 -0
  418. package/src/transcription/providers/xai-subscription.ts +49 -3
  419. package/src/transcription/segmenter.ts +8 -2
  420. package/src/transcription/service.ts +43 -6
  421. package/src/work-discovery-observability.ts +121 -0
  422. package/src/workspace-artifact-content.ts +144 -0
  423. package/src/workspace-artifact-provenance.ts +104 -0
  424. package/src/workspace-delete-observability.ts +75 -0
  425. package/src/workspace-deletion.ts +73 -0
  426. package/src/workspace-state-projection.ts +18 -148
  427. package/src/workspace-tool-gateway-observability.ts +100 -0
  428. package/src/workspace-tool-gateway.ts +870 -0
  429. package/src/workspace-webhook-dispatch.ts +190 -0
  430. package/src/xai-subscription-auth.ts +9 -2
  431. package/dist/chunk-WCXHI3FF.js +0 -49920
  432. package/dist/chunk-WCXHI3FF.js.map +0 -1
  433. package/dist/routes/packs.d.ts +0 -3
  434. package/src/routes/packs.ts +0 -729
@@ -0,0 +1,1938 @@
1
+ import { createHash, createHmac, randomUUID } from "node:crypto";
2
+ import type { Hono } from "hono";
3
+ import { HTTPException } from "hono/http-exception";
4
+ import { getConnectBeginReplay } from "@opengeni/db/connect-attempts";
5
+ import {
6
+ AdvanceConnectRequest,
7
+ BeginConnectRequest,
8
+ ConnectOperationRequest,
9
+ ConnectProvider,
10
+ ConnectAccount,
11
+ } from "@opengeni/contracts/connect";
12
+ import {
13
+ API_INTEGRATION_OAUTH_CREDENTIAL_ROLE,
14
+ InstallApiIntegrationRequest,
15
+ stableJson,
16
+ assertOrganizationIntegrationAllowed,
17
+ OPENGENI_PERSONAL_SLACK_MCP_URL,
18
+ } from "@opengeni/contracts";
19
+ import {
20
+ CORE_INTEGRATION_DEFINITIONS,
21
+ createPinnedIntegrationTransport,
22
+ } from "@opengeni/capabilities";
23
+ import {
24
+ beginConnectAttempt as persistConnectAttempt,
25
+ getConnectAttempt,
26
+ listPendingConnectAttempts,
27
+ withWorkspaceSubjectRls,
28
+ installApiIntegration,
29
+ listConnectionsMetadata,
30
+ getConnectionMetadata,
31
+ createConnection,
32
+ updateConnection,
33
+ encryptEnvironmentValue,
34
+ normalizedCredentialHeaders,
35
+ listGitHubInstallationAccessForWorkspace,
36
+ listPrReviewAppRegistrations,
37
+ listSocialConnections,
38
+ getSocialConnection,
39
+ } from "@opengeni/db";
40
+ import {
41
+ externalActorContinuationForAuthorization,
42
+ requireAccessGrantAuthorization,
43
+ hasPermission,
44
+ requireEnvironmentEncryption,
45
+ isFikenConnection,
46
+ isOpenGeniSlackBotConnection,
47
+ prepareCapabilityEnable,
48
+ buildCapabilityCatalog,
49
+ integrationKeyForConnectProvider,
50
+ type ApiRouteDeps,
51
+ } from "@opengeni/core";
52
+ import { isPersonalConnectionOwnerPrincipal } from "../connection-ownership";
53
+ import { requireConnectOwnerAuthority } from "../integrations/connect-authority";
54
+ import { prepareFikenTokenInstall, startFikenOAuth } from "../integrations/fiken";
55
+ import { startAtlassianOAuth } from "../integrations/atlassian";
56
+ import { startSlackBotInstall } from "../integrations/slack-install";
57
+ import { startPersonalGitHubOAuth } from "../integrations/personal-github";
58
+ import { socialOAuthClientFor, startSocialOAuth } from "../integrations/social-oauth";
59
+ import {
60
+ githubAppConnectNavigation,
61
+ prepareGitHubAppConnectAction,
62
+ } from "../integrations/github-app-connect";
63
+ import { githubAppMissingSettings, prReviewGitHubAppMissingSettings } from "@opengeni/github";
64
+ import { requireGitHubLensConnect } from "../integrations/github-lens-connect";
65
+ import { isPersonalGitHubConnection } from "@opengeni/contracts/personal-github";
66
+ import { startGoogleDriveOAuth } from "../integrations/google-drive";
67
+ import {
68
+ GOOGLE_DRIVE_CREDENTIAL_ROLE,
69
+ googleDriveScopesAllowCapability,
70
+ } from "@opengeni/contracts/google-drive";
71
+ import { ATLASSIAN_NATIVE_RETIRED_MESSAGE } from "@opengeni/contracts/atlassian-native-retirement";
72
+ import {
73
+ curatedOAuthReadiness,
74
+ startApiIntegrationProviderOAuth,
75
+ } from "../integrations/provider-oauth";
76
+ import { resolveForRoute, validatedIntegrationInstallInput } from "./api-integrations";
77
+ import { executeConnectOperation } from "@opengeni/core";
78
+ import { withOrganizationIntegrationPolicyFence } from "@opengeni/db/organization-integration-policy";
79
+ import {
80
+ startMcpOAuth,
81
+ requireIntegrationsStateSecret,
82
+ gmailOAuthClientConfigured,
83
+ } from "../integrations/oauth-client";
84
+ import { OFFICIAL_GMAIL_MCP_URL } from "../integrations/oauth-profiles";
85
+ import { z } from "zod";
86
+ import {
87
+ WORKSPACE_OPENROUTER_CONNECTION_DOMAIN,
88
+ VERCEL_AI_GATEWAY_CONNECTION_DOMAIN,
89
+ } from "@opengeni/config";
90
+ import { parseRequestJson } from "../http/request-body";
91
+
92
+ /** Shared durable setup entry. Provider completion is distinct from
93
+ * subsequent preview/install; never report an OAuth token as a ready integration. */
94
+ export function registerConnectRoutes(app: Hono, deps: ApiRouteDeps): void {
95
+ const canReadAttempt = (permissions: Parameters<typeof hasPermission>[0], providerId: string) =>
96
+ ["x", "reddit", "mcp-install"].includes(providerId)
97
+ ? hasPermission(permissions, "workspace:read")
98
+ : providerId === "github-app"
99
+ ? hasPermission(permissions, "github:use") || hasPermission(permissions, "github:manage")
100
+ : providerId === "github-lens"
101
+ ? hasPermission(permissions, "workspace:read")
102
+ : hasPermission(permissions, "connections:read");
103
+ app.get("/v1/workspaces/:workspaceId/connect/catalog", async (c) => {
104
+ const authorization = await requireAccessGrantAuthorization(
105
+ c,
106
+ deps,
107
+ c.req.param("workspaceId"),
108
+ "workspace:read",
109
+ );
110
+ const external = authorization.contextIntegrity;
111
+ const policy = await withOrganizationIntegrationPolicyFence(
112
+ deps.db,
113
+ authorization.grant,
114
+ async (_tx, current) => current,
115
+ );
116
+ const canWrite = hasPermission(authorization.grant.permissions, "connections:write");
117
+ const personal = isPersonalConnectionOwnerPrincipal(authorization);
118
+ let mcpConfigured = false;
119
+ let credentialConfigured = false;
120
+ try {
121
+ requireEnvironmentEncryption(deps.settings);
122
+ credentialConfigured = deps.settings.integrationsEnabled;
123
+ requireIntegrationsStateSecret(deps.settings);
124
+ mcpConfigured = deps.settings.integrationsEnabled;
125
+ } catch {
126
+ /* Missing operator configuration is catalog readiness, not actor authority. */
127
+ }
128
+ const providers = [
129
+ ...(["x", "reddit"] as const).map((providerId) => {
130
+ let configured = mcpConfigured;
131
+ try {
132
+ socialOAuthClientFor(deps.settings, providerId);
133
+ } catch {
134
+ configured = false;
135
+ }
136
+ const ownership = [
137
+ ...(hasPermission(authorization.grant.permissions, "workspace:admin")
138
+ ? ["workspace" as const]
139
+ : []),
140
+ ...(personal ? ["personal" as const] : []),
141
+ ];
142
+ return ConnectProvider.parse({
143
+ id: providerId,
144
+ label: providerId === "x" ? "X" : "Reddit",
145
+ family: "social",
146
+ readiness:
147
+ !external || !ownership.length
148
+ ? "unsupported"
149
+ : configured
150
+ ? "available"
151
+ : "needs_configuration",
152
+ ownership,
153
+ setup: ["oauth"],
154
+ });
155
+ }),
156
+ ConnectProvider.parse({
157
+ id: "mcp-install",
158
+ label: "Install MCP capability",
159
+ family: "mcp",
160
+ readiness:
161
+ !external || !hasPermission(authorization.grant.permissions, "capabilities:manage")
162
+ ? "unsupported"
163
+ : deps.settings.integrationsEnabled
164
+ ? "available"
165
+ : "needs_configuration",
166
+ ownership: ["workspace"],
167
+ setup: ["installation"],
168
+ }),
169
+ ...CORE_INTEGRATION_DEFINITIONS.map((definition) => {
170
+ const readiness = curatedOAuthReadiness(deps.settings, definition);
171
+ return ConnectProvider.parse({
172
+ id: definition.id,
173
+ label: definition.name,
174
+ family: definition.provider.id,
175
+ readiness:
176
+ !external || !canWrite
177
+ ? "unsupported"
178
+ : readiness.configured
179
+ ? "available"
180
+ : "needs_configuration",
181
+ ...(!external
182
+ ? { reason: "A verified user is required by this Connect adapter" }
183
+ : !canWrite
184
+ ? { reason: "connections:write permission required" }
185
+ : !readiness.configured
186
+ ? { reason: "Deployment OAuth or encryption configuration is incomplete" }
187
+ : {}),
188
+ ownership: readiness.ownership.filter((owner) => owner !== "personal" || personal),
189
+ setup: ["oauth"],
190
+ });
191
+ }),
192
+ ...(["openapi", "graphql"] as const).map((protocol) =>
193
+ ConnectProvider.parse({
194
+ id: protocol,
195
+ label: protocol === "openapi" ? "OpenAPI service" : "GraphQL service",
196
+ family: protocol,
197
+ readiness:
198
+ !external || !canWrite
199
+ ? "unsupported"
200
+ : deps.settings.integrationsEnabled
201
+ ? "available"
202
+ : "needs_configuration",
203
+ ownership: personal ? ["workspace", "personal"] : ["workspace"],
204
+ setup: [protocol, "installation"],
205
+ }),
206
+ ),
207
+ ConnectProvider.parse({
208
+ id: "slack-bot",
209
+ label: "Slack workspace bot",
210
+ family: "slack",
211
+ readiness:
212
+ !external || !canWrite
213
+ ? "unsupported"
214
+ : mcpConfigured &&
215
+ deps.settings.slackClientId?.trim() &&
216
+ deps.settings.slackClientSecret?.trim() &&
217
+ deps.settings.slackSigningSecret?.trim()
218
+ ? "available"
219
+ : "needs_configuration",
220
+ ownership: ["workspace"],
221
+ setup: ["oauth"],
222
+ }),
223
+ ConnectProvider.parse({
224
+ id: "gmail",
225
+ label: "Gmail",
226
+ family: "google",
227
+ readiness:
228
+ !external || !canWrite || !personal
229
+ ? "unsupported"
230
+ : mcpConfigured && gmailOAuthClientConfigured(deps.settings)
231
+ ? "available"
232
+ : "needs_configuration",
233
+ ownership: personal ? ["personal"] : [],
234
+ setup: ["oauth"],
235
+ }),
236
+ ConnectProvider.parse({
237
+ id: "slack-personal",
238
+ label: "Slack account",
239
+ family: "slack",
240
+ readiness:
241
+ !external || !canWrite
242
+ ? "unsupported"
243
+ : mcpConfigured &&
244
+ deps.settings.slackClientId?.trim() &&
245
+ deps.settings.slackClientSecret?.trim()
246
+ ? "available"
247
+ : "needs_configuration",
248
+ ownership: personal ? ["workspace", "personal"] : ["workspace"],
249
+ setup: ["oauth"],
250
+ }),
251
+ ConnectProvider.parse({
252
+ id: "github-personal",
253
+ label: "My GitHub account",
254
+ family: "github",
255
+ readiness:
256
+ !external || !canWrite || !personal
257
+ ? "unsupported"
258
+ : mcpConfigured &&
259
+ deps.settings.githubPersonalOauthEnabled &&
260
+ deps.settings.githubPersonalOauthClientId?.trim() &&
261
+ deps.settings.githubPersonalOauthClientSecret?.trim()
262
+ ? "available"
263
+ : "needs_configuration",
264
+ ownership: personal ? ["personal"] : [],
265
+ setup: ["oauth"],
266
+ }),
267
+ ConnectProvider.parse({
268
+ id: "github-app",
269
+ label: "GitHub App installation",
270
+ family: "github",
271
+ readiness:
272
+ !external || !hasPermission(authorization.grant.permissions, "github:manage")
273
+ ? "unsupported"
274
+ : githubAppMissingSettings(deps.settings).length === 0 &&
275
+ deps.settings.githubAppSlug?.trim()
276
+ ? "available"
277
+ : "needs_configuration",
278
+ ownership: ["workspace"],
279
+ setup: ["oauth", "installation"],
280
+ }),
281
+ ConnectProvider.parse({
282
+ id: "github-lens",
283
+ label: "Opengeni Lens PR review",
284
+ family: "github-lens",
285
+ readiness:
286
+ !external ||
287
+ !hasPermission(authorization.grant.permissions, "workspace:admin") ||
288
+ !hasPermission(authorization.grant.permissions, "secrets:write")
289
+ ? "unsupported"
290
+ : credentialConfigured &&
291
+ deps.settings.sandboxBackend !== "selfhosted" &&
292
+ prReviewGitHubAppMissingSettings(deps.settings).length === 0
293
+ ? "available"
294
+ : "needs_configuration",
295
+ ownership: ["workspace"],
296
+ setup: ["oauth", "installation"],
297
+ }),
298
+ ...(["google-drive-knowledge", "google-drive-publish"] as const).map((id) =>
299
+ ConnectProvider.parse({
300
+ id,
301
+ label:
302
+ id === "google-drive-publish"
303
+ ? "Google Drive publishing"
304
+ : "Google Drive knowledge sources",
305
+ family: "google-drive",
306
+ readiness:
307
+ !external || !canWrite || !personal
308
+ ? "unsupported"
309
+ : mcpConfigured &&
310
+ deps.settings.googleDriveClientId?.trim() &&
311
+ deps.settings.googleDriveClientSecret?.trim()
312
+ ? "available"
313
+ : "needs_configuration",
314
+ ownership: personal ? ["personal"] : [],
315
+ setup: ["oauth"],
316
+ }),
317
+ ),
318
+ ConnectProvider.parse({
319
+ id: "fiken-token",
320
+ label: "Fiken",
321
+ family: "fiken",
322
+ readiness:
323
+ !external || !canWrite
324
+ ? "unsupported"
325
+ : credentialConfigured
326
+ ? "available"
327
+ : "needs_configuration",
328
+ ownership: ["workspace"],
329
+ setup: ["credentials"],
330
+ }),
331
+ ConnectProvider.parse({
332
+ id: "fiken-oauth",
333
+ label: "Fiken (OAuth)",
334
+ family: "fiken",
335
+ readiness:
336
+ !external || !canWrite
337
+ ? "unsupported"
338
+ : mcpConfigured &&
339
+ deps.settings.fikenClientId?.trim() &&
340
+ deps.settings.fikenClientSecret?.trim()
341
+ ? "available"
342
+ : "needs_configuration",
343
+ ownership: ["workspace"],
344
+ setup: ["oauth"],
345
+ }),
346
+ ConnectProvider.parse({
347
+ id: "mcp-oauth",
348
+ label: "MCP server (OAuth)",
349
+ family: "mcp",
350
+ readiness:
351
+ !external || !canWrite
352
+ ? "unsupported"
353
+ : mcpConfigured
354
+ ? "available"
355
+ : "needs_configuration",
356
+ ownership: personal ? ["workspace", "personal"] : ["workspace"],
357
+ setup: ["credentials", "oauth"],
358
+ }),
359
+ ConnectProvider.parse({
360
+ id: "mcp-headers",
361
+ label: "MCP server (custom headers)",
362
+ family: "mcp",
363
+ readiness:
364
+ !external || !canWrite
365
+ ? "unsupported"
366
+ : credentialConfigured
367
+ ? "available"
368
+ : "needs_configuration",
369
+ ownership: personal ? ["workspace", "personal"] : ["workspace"],
370
+ setup: ["credentials"],
371
+ }),
372
+ ConnectProvider.parse({
373
+ id: "mcp-bearer",
374
+ label: "MCP server (bearer credential)",
375
+ family: "mcp",
376
+ readiness:
377
+ !external || !canWrite
378
+ ? "unsupported"
379
+ : credentialConfigured
380
+ ? "available"
381
+ : "needs_configuration",
382
+ ownership: personal ? ["workspace", "personal"] : ["workspace"],
383
+ setup: ["credentials"],
384
+ }),
385
+ ];
386
+ return c.json(
387
+ providers.filter(
388
+ (provider) =>
389
+ policy.mode === "unrestricted" ||
390
+ policy.allowedIntegrationKeys.includes(
391
+ integrationKeyForConnectProvider(provider.id) ?? "",
392
+ ),
393
+ ),
394
+ );
395
+ });
396
+ app.get("/v1/workspaces/:workspaceId/connect/accounts", async (c) => {
397
+ const workspaceId = c.req.param("workspaceId");
398
+ const { grant } = await requireAccessGrantAuthorization(c, deps, workspaceId, "workspace:read");
399
+ const connections = hasPermission(grant.permissions, "connections:read")
400
+ ? await listConnectionsMetadata(deps.db, workspaceId, grant.subjectId)
401
+ : [];
402
+ const githubInstallations =
403
+ hasPermission(grant.permissions, "github:use") ||
404
+ hasPermission(grant.permissions, "github:manage")
405
+ ? await listGitHubInstallationAccessForWorkspace(deps.db, workspaceId)
406
+ : [];
407
+ const lensRegistrations = await listPrReviewAppRegistrations(
408
+ deps.db,
409
+ grant.accountId,
410
+ workspaceId,
411
+ );
412
+ const socialConnections = await listSocialConnections(
413
+ deps.db,
414
+ workspaceId,
415
+ 200,
416
+ grant.subjectId,
417
+ );
418
+ return c.json([
419
+ ...socialConnections
420
+ .filter((connection) => ["x", "reddit"].includes(connection.provider))
421
+ .map((connection) =>
422
+ ConnectAccount.parse({
423
+ id: `social:${connection.id}`,
424
+ version: connection.version,
425
+ providerId: connection.provider,
426
+ label: connection.accountHandle,
427
+ ownership: connection.ownership,
428
+ status:
429
+ connection.status === "connected"
430
+ ? "connected"
431
+ : connection.status === "disabled"
432
+ ? "disabled"
433
+ : "auth_needed",
434
+ }),
435
+ ),
436
+ ...lensRegistrations
437
+ .filter((registration) => registration.credentialKind === "managed_github_app")
438
+ .map((registration) =>
439
+ ConnectAccount.parse({
440
+ id: `lens-registration:${registration.id}`,
441
+ providerId: "github-lens",
442
+ label: registration.providerAccountLogin ?? "Lens installation",
443
+ ownership: "workspace",
444
+ status: registration.status === "active" ? "connected" : "disabled",
445
+ }),
446
+ ),
447
+ ...githubInstallations.map((installation) =>
448
+ ConnectAccount.parse({
449
+ id: `github-installation:${installation.installationId}`,
450
+ providerId: "github-app",
451
+ label: installation.accountLogin ?? `Installation ${installation.installationId}`,
452
+ ownership: "workspace",
453
+ status: "connected",
454
+ }),
455
+ ),
456
+ ...connections.flatMap((connection) => {
457
+ if (isPersonalGitHubConnection(connection))
458
+ return [
459
+ ConnectAccount.parse({
460
+ id: connection.id,
461
+ version: connection.version,
462
+ providerId: "github-personal",
463
+ label: String(connection.metadata.githubLogin ?? "GitHub"),
464
+ ownership: "personal",
465
+ status:
466
+ connection.status === "revoked"
467
+ ? "disabled"
468
+ : connection.status === "active"
469
+ ? "connected"
470
+ : "auth_needed",
471
+ }),
472
+ ];
473
+ if (isOpenGeniSlackBotConnection(connection))
474
+ return [
475
+ ConnectAccount.parse({
476
+ id: connection.id,
477
+ version: connection.version,
478
+ providerId: "slack-bot",
479
+ label: String(connection.metadata.slackTeamName ?? "Slack workspace bot"),
480
+ ownership: "workspace",
481
+ status:
482
+ connection.status === "revoked"
483
+ ? "disabled"
484
+ : connection.status === "active"
485
+ ? "connected"
486
+ : "auth_needed",
487
+ }),
488
+ ];
489
+ if (connection.metadata.credentialRole === GOOGLE_DRIVE_CREDENTIAL_ROLE)
490
+ return [
491
+ ConnectAccount.parse({
492
+ id: connection.id,
493
+ version: connection.version,
494
+ providerId: googleDriveScopesAllowCapability(connection.grantedScopes, "publish_file")
495
+ ? "google-drive-publish"
496
+ : "google-drive-knowledge",
497
+ label: String(
498
+ connection.metadata.googleEmail ??
499
+ connection.metadata.googleDisplayName ??
500
+ "Google Drive",
501
+ ),
502
+ ownership: "personal",
503
+ status:
504
+ connection.status === "revoked"
505
+ ? "disabled"
506
+ : connection.status === "active"
507
+ ? "connected"
508
+ : "auth_needed",
509
+ }),
510
+ ];
511
+ if (isFikenConnection(connection))
512
+ return [
513
+ ConnectAccount.parse({
514
+ id: connection.id,
515
+ version: connection.version,
516
+ providerId: connection.kind === "oauth2" ? "fiken-oauth" : "fiken-token",
517
+ label: "Fiken",
518
+ ownership: "workspace",
519
+ status:
520
+ connection.status === "revoked"
521
+ ? "disabled"
522
+ : connection.status === "active"
523
+ ? "connected"
524
+ : "auth_needed",
525
+ }),
526
+ ];
527
+ if (
528
+ connection.kind === "api_key" &&
529
+ ["mcp-bearer", "mcp-headers"].includes(String(connection.metadata.connectAdapter))
530
+ )
531
+ return [
532
+ ConnectAccount.parse({
533
+ id: connection.id,
534
+ version: connection.version,
535
+ providerId: connection.metadata.connectAdapter,
536
+ label: connection.providerDomain,
537
+ ownership: connection.subjectId === null ? "workspace" : "personal",
538
+ status:
539
+ connection.status === "revoked"
540
+ ? "disabled"
541
+ : connection.status === "active"
542
+ ? "connected"
543
+ : "auth_needed",
544
+ }),
545
+ ];
546
+ if (
547
+ connection.kind === "oauth2" &&
548
+ connection.metadata.oauthDiscovery &&
549
+ typeof connection.metadata.mcpUrl === "string"
550
+ )
551
+ return [
552
+ ConnectAccount.parse({
553
+ id: connection.id,
554
+ version: connection.version,
555
+ providerId:
556
+ connection.metadata.mcpUrl === OPENGENI_PERSONAL_SLACK_MCP_URL
557
+ ? "slack-personal"
558
+ : connection.metadata.mcpUrl === OFFICIAL_GMAIL_MCP_URL
559
+ ? "gmail"
560
+ : "mcp-oauth",
561
+ label: connection.providerDomain,
562
+ ownership: connection.subjectId === null ? "workspace" : "personal",
563
+ status:
564
+ connection.status === "revoked"
565
+ ? "disabled"
566
+ : connection.metadata.mcpUrl === OFFICIAL_GMAIL_MCP_URL &&
567
+ connection.subjectId === null
568
+ ? "auth_needed"
569
+ : connection.status === "active"
570
+ ? "connected"
571
+ : "auth_needed",
572
+ }),
573
+ ];
574
+ if (connection.metadata.credentialRole !== API_INTEGRATION_OAUTH_CREDENTIAL_ROLE) return [];
575
+ const ids = connection.metadata.authorizedDefinitionIds;
576
+ if (!Array.isArray(ids)) return [];
577
+ return CORE_INTEGRATION_DEFINITIONS.filter(
578
+ (definition) =>
579
+ ids.includes(definition.id) &&
580
+ definition.provider.id === connection.metadata.providerFamily,
581
+ ).map((definition) =>
582
+ ConnectAccount.parse({
583
+ id: connection.id,
584
+ version: connection.version,
585
+ providerId: definition.id,
586
+ label: String(
587
+ connection.metadata.providerDisplayName ??
588
+ connection.metadata.providerEmail ??
589
+ connection.metadata.providerPrincipalId ??
590
+ connection.id,
591
+ ).slice(0, 256),
592
+ ownership: connection.subjectId === null ? "workspace" : "personal",
593
+ status:
594
+ connection.status === "revoked"
595
+ ? "disabled"
596
+ : connection.status !== "active"
597
+ ? "auth_needed"
598
+ : "connected",
599
+ }),
600
+ );
601
+ }),
602
+ ]);
603
+ });
604
+ const transport = createPinnedIntegrationTransport({
605
+ network: deps.settings,
606
+ ...(deps.apiIntegrationSourceFetch ? { fetchImpl: deps.apiIntegrationSourceFetch } : {}),
607
+ });
608
+ app.post("/v1/workspaces/:workspaceId/connect/attempts/:attemptId/advance", async (c) => {
609
+ const input = await parseRequestJson(c, AdvanceConnectRequest);
610
+ const action = input.action;
611
+ const workspaceId = c.req.param("workspaceId");
612
+ const permission =
613
+ action.type === "install"
614
+ ? "capabilities:manage"
615
+ : action.type === "credentials"
616
+ ? "connections:write"
617
+ : "workspace:read";
618
+ // Resolve the authenticated workspace actor first, then enforce the exact
619
+ // stored provider/action permission below. Setup managers need not also
620
+ // carry the unrelated workspace metadata read permission.
621
+ const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
622
+ const continuation = externalActorContinuationForAuthorization(authorization);
623
+ if (!authorization.contextIntegrity)
624
+ throw new HTTPException(403, { message: "Verified Connect authority required" });
625
+ const scope = {
626
+ accountId: authorization.grant.accountId,
627
+ workspaceId,
628
+ subjectId: authorization.grant.subjectId,
629
+ personalOwnerVerified: isPersonalConnectionOwnerPrincipal(authorization),
630
+ ...(continuation ? { externalContinuation: continuation } : {}),
631
+ };
632
+ // Reject invalid caller selections before taking a durable operation claim.
633
+ // Older revisions still go through the receipt-aware claim path for replay.
634
+ const stored = await getConnectAttempt(deps.db, scope, c.req.param("attemptId"));
635
+ const before = stored.attempt;
636
+ const advancePermission =
637
+ before.providerId === "mcp-install" ? "capabilities:manage" : permission;
638
+ if (!hasPermission(authorization.grant.permissions, advancePermission))
639
+ throw new HTTPException(403, { message: `${advancePermission} required` });
640
+ if (before.providerId === "mcp-install") {
641
+ if (action.type !== "credentials")
642
+ throw new HTTPException(422, { message: "Choose an MCP capability" });
643
+ const values = z
644
+ .object({ capabilityId: z.string().min(1).max(512) })
645
+ .strict()
646
+ .parse(action.values);
647
+ return c.json(
648
+ await executeConnectOperation({
649
+ db: deps.db,
650
+ scope,
651
+ attemptId: before.id,
652
+ expectedRevision: input.expectedRevision,
653
+ operationId: input.idempotencyKey,
654
+ inputDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
655
+ authorize: (tx, _attempt, origin) =>
656
+ requireConnectOwnerAuthority(tx, scope, "capabilities:manage", origin),
657
+ execute: async (attempt) => {
658
+ if (attempt.state !== "credential_input")
659
+ throw new HTTPException(409, { message: "Reload MCP setup" });
660
+ const prepared = await prepareCapabilityEnable({
661
+ db: deps.db,
662
+ settings: deps.settings,
663
+ grant: authorization.grant,
664
+ accountId: scope.accountId,
665
+ workspaceId,
666
+ capabilityId: values.capabilityId,
667
+ payload: { config: {}, metadata: {}, headers: {} },
668
+ });
669
+ return {
670
+ commit: async (tx, current) => {
671
+ await prepared.commit(tx);
672
+ return {
673
+ ...current,
674
+ revision: current.revision + 1,
675
+ state: "complete",
676
+ completionRequirement: "provider_setup",
677
+ integrationInstalled: true,
678
+ nextAction: { type: "none" },
679
+ };
680
+ },
681
+ };
682
+ },
683
+ }),
684
+ );
685
+ }
686
+ if (["github-app", "github-lens"].includes(before.providerId)) {
687
+ const installationPermission =
688
+ before.providerId === "github-lens" ? "workspace:admin" : "github:manage";
689
+ if (
690
+ !hasPermission(authorization.grant.permissions, installationPermission) ||
691
+ (before.providerId === "github-lens" &&
692
+ !hasPermission(authorization.grant.permissions, "secrets:write"))
693
+ )
694
+ throw new HTTPException(403, { message: "Installation management permission required" });
695
+ return c.json(
696
+ await executeConnectOperation({
697
+ db: deps.db,
698
+ scope,
699
+ attemptId: before.id,
700
+ expectedRevision: input.expectedRevision,
701
+ operationId: input.idempotencyKey,
702
+ inputDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
703
+ authorize: async (tx, _attempt, origin) => {
704
+ await requireConnectOwnerAuthority(tx, scope, installationPermission, origin);
705
+ if (before.providerId === "github-lens") {
706
+ await requireConnectOwnerAuthority(tx, scope, "secrets:write", origin);
707
+ await requireGitHubLensConnect({ ...deps, db: tx });
708
+ }
709
+ },
710
+ execute: async (attempt) =>
711
+ prepareGitHubAppConnectAction(deps, scope, c.req.url, attempt, action),
712
+ }),
713
+ );
714
+ }
715
+ if (["openapi", "graphql"].includes(before.providerId) && action.type === "credentials") {
716
+ const values = z
717
+ .object({
718
+ url: z.string().url().max(2048),
719
+ connectionId: z.string().uuid().or(z.literal("")).optional(),
720
+ })
721
+ .strict()
722
+ .parse(action.values);
723
+ const source =
724
+ before.providerId === "openapi"
725
+ ? { kind: "openapi" as const, url: values.url }
726
+ : { kind: "graphql" as const, endpoint: values.url };
727
+ const connectionId = values.connectionId || undefined;
728
+ return c.json(
729
+ await executeConnectOperation({
730
+ db: deps.db,
731
+ scope,
732
+ attemptId: before.id,
733
+ expectedRevision: input.expectedRevision,
734
+ operationId: input.idempotencyKey,
735
+ inputDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
736
+ authorize: (tx, _attempt, origin) =>
737
+ requireConnectOwnerAuthority(tx, scope, "connections:write", origin),
738
+ execute: async (attempt) => {
739
+ if (attempt.state !== "credential_input")
740
+ throw new HTTPException(409, { message: "Reload setup before changing the source" });
741
+ const resolved = await resolveForRoute({
742
+ deps,
743
+ transport,
744
+ accountId: scope.accountId,
745
+ workspaceId,
746
+ subjectId: scope.subjectId,
747
+ payload: {
748
+ source,
749
+ ownership: attempt.ownership,
750
+ ...(connectionId ? { connectionId } : {}),
751
+ },
752
+ });
753
+ const account = connectionId
754
+ ? await getConnectionMetadata(deps.db, workspaceId, connectionId, scope.subjectId)
755
+ : null;
756
+ if (
757
+ connectionId &&
758
+ (!account ||
759
+ (account.subjectId === null ? "workspace" : "personal") !== attempt.ownership)
760
+ )
761
+ throw new HTTPException(404, {
762
+ message: "Connection unavailable for the requested ownership",
763
+ });
764
+ if (!account && attempt.ownership === "personal")
765
+ throw new HTTPException(422, {
766
+ message: "Personal installation requires a personal connection",
767
+ });
768
+ if (!account && resolved.preview.auth.kind !== "none")
769
+ throw new HTTPException(422, {
770
+ message: "Select a connection for this authenticated service",
771
+ });
772
+ return {
773
+ commit: async (_tx, current) => ({
774
+ ...current,
775
+ source,
776
+ revision: current.revision + 1,
777
+ state: "preview",
778
+ credentialsCommitted: !!account,
779
+ ...(account
780
+ ? {
781
+ account: {
782
+ id: account.id,
783
+ version: account.version,
784
+ providerId: attempt.providerId,
785
+ label: account.providerDomain,
786
+ ownership: attempt.ownership,
787
+ status: "connected" as const,
788
+ },
789
+ }
790
+ : {}),
791
+ nextAction: {
792
+ type: "preview",
793
+ previewId: resolved.preview.revisionId,
794
+ contentHash: resolved.preview.contentSha256,
795
+ operations: resolved.preview.tools.map((tool) => ({
796
+ id: tool.id,
797
+ label: tool.name,
798
+ kind: tool.safety,
799
+ })),
800
+ },
801
+ }),
802
+ };
803
+ },
804
+ }),
805
+ );
806
+ }
807
+ if (before.providerId === "fiken-token") {
808
+ if (action.type !== "credentials" || before.ownership !== "workspace")
809
+ throw new HTTPException(422, { message: "Fiken requires workspace-owned token setup" });
810
+ const values = z
811
+ .object({
812
+ apiToken: z.string().min(1).max(16_384),
813
+ defaultCompanySlug: z.string().max(128).optional(),
814
+ })
815
+ .strict()
816
+ .parse(action.values);
817
+ return c.json(
818
+ await executeConnectOperation({
819
+ db: deps.db,
820
+ scope,
821
+ attemptId: before.id,
822
+ expectedRevision: input.expectedRevision,
823
+ operationId: input.idempotencyKey,
824
+ inputDigest: createHmac("sha256", requireEnvironmentEncryption(deps.settings))
825
+ .update(stableJson(input))
826
+ .digest("hex"),
827
+ authorize: (tx, _attempt, origin) =>
828
+ requireConnectOwnerAuthority(tx, scope, "connections:write", origin),
829
+ execute: async (attempt) => {
830
+ if (attempt.state !== "credential_input")
831
+ throw new HTTPException(409, { message: "Reload Fiken setup" });
832
+ const persist = await prepareFikenTokenInstall(
833
+ deps,
834
+ scope,
835
+ {
836
+ apiToken: values.apiToken,
837
+ ...(values.defaultCompanySlug
838
+ ? { defaultCompanySlug: values.defaultCompanySlug }
839
+ : {}),
840
+ ...(attempt.account ? { connectionId: attempt.account.id } : {}),
841
+ },
842
+ attempt.account?.version,
843
+ );
844
+ return {
845
+ commit: async (tx, current) => {
846
+ const connection = await persist(tx);
847
+ return {
848
+ ...current,
849
+ revision: current.revision + 1,
850
+ state: "complete",
851
+ credentialsCommitted: true,
852
+ nextAction: { type: "none" },
853
+ account: {
854
+ id: connection.id,
855
+ version: connection.version,
856
+ providerId: "fiken-token",
857
+ label: "Fiken",
858
+ ownership: "workspace",
859
+ status: "connected",
860
+ },
861
+ };
862
+ },
863
+ };
864
+ },
865
+ }),
866
+ );
867
+ }
868
+ if (["mcp-bearer", "mcp-headers"].includes(before.providerId)) {
869
+ if (action.type !== "credentials")
870
+ throw new HTTPException(422, { message: "MCP credentials required" });
871
+ const values =
872
+ before.providerId === "mcp-headers"
873
+ ? z
874
+ .object({
875
+ mcpUrl: z.string().url().max(4096),
876
+ headers: z.string().min(2).max(65_536),
877
+ })
878
+ .strict()
879
+ .parse(action.values)
880
+ : z
881
+ .object({
882
+ mcpUrl: z.string().url().max(4096),
883
+ token: z
884
+ .string()
885
+ .min(1)
886
+ .max(16_384)
887
+ .regex(/^[^\u0000-\u0020\u007f]+$/),
888
+ })
889
+ .strict()
890
+ .parse(action.values);
891
+ let credentialHeaders: Record<string, string>;
892
+ try {
893
+ credentialHeaders =
894
+ "headers" in values
895
+ ? normalizedCredentialHeaders(
896
+ z.record(z.string(), z.string()).parse(JSON.parse(values.headers)),
897
+ )
898
+ : { authorization: `Bearer ${values.token}` };
899
+ } catch {
900
+ throw new HTTPException(422, {
901
+ message: "Supply a JSON object of valid credential headers",
902
+ });
903
+ }
904
+ const destination = new URL(values.mcpUrl);
905
+ if (
906
+ [WORKSPACE_OPENROUTER_CONNECTION_DOMAIN, VERCEL_AI_GATEWAY_CONNECTION_DOMAIN].some(
907
+ (domain) => domain === destination.hostname,
908
+ )
909
+ )
910
+ throw new HTTPException(422, {
911
+ message: "Use the dedicated workspace provider credential flow",
912
+ });
913
+ if (
914
+ destination.protocol !== "https:" ||
915
+ destination.username ||
916
+ destination.password ||
917
+ destination.hash
918
+ )
919
+ throw new HTTPException(422, {
920
+ message: "MCP credential destination requires HTTPS without userinfo or fragment",
921
+ });
922
+ const encryptionKey = requireEnvironmentEncryption(deps.settings);
923
+ const mcpUrl = destination.toString();
924
+ return c.json(
925
+ await executeConnectOperation({
926
+ db: deps.db,
927
+ scope,
928
+ attemptId: before.id,
929
+ expectedRevision: input.expectedRevision,
930
+ operationId: input.idempotencyKey,
931
+ inputDigest: createHmac("sha256", encryptionKey).update(stableJson(input)).digest("hex"),
932
+ authorize: (tx, _attempt, origin) =>
933
+ requireConnectOwnerAuthority(tx, scope, "connections:write", origin),
934
+ execute: async (attempt) => {
935
+ if (attempt.state !== "credential_input")
936
+ throw new HTTPException(409, { message: "Reload the current credential setup" });
937
+ const credentialEncrypted = encryptEnvironmentValue(
938
+ encryptionKey,
939
+ JSON.stringify({ headers: credentialHeaders }),
940
+ );
941
+ return {
942
+ commit: async (tx, current) => {
943
+ const existing = current.account
944
+ ? await getConnectionMetadata(
945
+ tx,
946
+ workspaceId,
947
+ current.account.id,
948
+ scope.subjectId,
949
+ )
950
+ : null;
951
+ if (
952
+ current.account &&
953
+ (!existing ||
954
+ existing.metadata.connectAdapter !== current.providerId ||
955
+ existing.metadata.mcpUrl !== mcpUrl ||
956
+ existing.version !== current.account.version ||
957
+ (existing.subjectId === null ? "workspace" : "personal") !== current.ownership)
958
+ )
959
+ throw new HTTPException(409, {
960
+ message: "MCP account changed; reload before replacing credentials",
961
+ });
962
+ const metadata = { connectAdapter: current.providerId, mcpUrl, resource: mcpUrl };
963
+ const subjectId = current.ownership === "personal" ? scope.subjectId : null;
964
+ const connection = existing
965
+ ? await updateConnection(tx, {
966
+ workspaceId,
967
+ connectionId: existing.id,
968
+ expectedVersion: existing.version,
969
+ visibleToSubjectId: scope.subjectId,
970
+ subjectId,
971
+ providerDomain: destination.hostname,
972
+ kind: "api_key",
973
+ status: "active",
974
+ credentialEncrypted,
975
+ grantedScopes: [],
976
+ expiresAt: null,
977
+ metadata,
978
+ updatedBySubjectId: scope.subjectId,
979
+ })
980
+ : await createConnection(tx, {
981
+ accountId: scope.accountId,
982
+ workspaceId,
983
+ subjectId,
984
+ providerDomain: destination.hostname,
985
+ kind: "api_key",
986
+ credentialEncrypted,
987
+ grantedScopes: [],
988
+ expiresAt: null,
989
+ metadata,
990
+ createdBySubjectId: scope.subjectId,
991
+ });
992
+ if (!connection) throw new HTTPException(409, { message: "MCP account changed" });
993
+ return {
994
+ ...current,
995
+ revision: current.revision + 1,
996
+ state: "complete",
997
+ credentialsCommitted: true,
998
+ nextAction: { type: "none" },
999
+ account: {
1000
+ id: connection.id,
1001
+ version: connection.version,
1002
+ providerId: current.providerId,
1003
+ label: connection.providerDomain,
1004
+ ownership: current.ownership,
1005
+ status: "connected",
1006
+ },
1007
+ };
1008
+ },
1009
+ };
1010
+ },
1011
+ }),
1012
+ );
1013
+ }
1014
+ if (["mcp-oauth", "slack-personal", "gmail"].includes(before.providerId)) {
1015
+ if (action.type !== "credentials")
1016
+ throw new HTTPException(422, { message: "MCP OAuth requires a server URL" });
1017
+ const values =
1018
+ before.providerId === "slack-personal"
1019
+ ? { mcpUrl: OPENGENI_PERSONAL_SLACK_MCP_URL }
1020
+ : before.providerId === "gmail"
1021
+ ? { mcpUrl: OFFICIAL_GMAIL_MCP_URL }
1022
+ : z
1023
+ .object({ mcpUrl: z.string().url().max(4096) })
1024
+ .strict()
1025
+ .parse(action.values);
1026
+ return c.json(
1027
+ await executeConnectOperation({
1028
+ db: deps.db,
1029
+ scope,
1030
+ attemptId: before.id,
1031
+ expectedRevision: input.expectedRevision,
1032
+ operationId: input.idempotencyKey,
1033
+ inputDigest: createHmac("sha256", requireIntegrationsStateSecret(deps.settings))
1034
+ .update(stableJson(input))
1035
+ .digest("hex"),
1036
+ authorize: (tx, _attempt, origin) =>
1037
+ requireConnectOwnerAuthority(tx, scope, "connections:write", origin),
1038
+ execute: async (attempt) => {
1039
+ if (attempt.state !== "credential_input")
1040
+ throw new HTTPException(409, { message: "Reload the current connection setup" });
1041
+ const started = await startMcpOAuth(deps, {
1042
+ ...scope,
1043
+ ...(before.providerId === "gmail" || before.providerId === "slack-personal"
1044
+ ? { integrationKey: before.providerId }
1045
+ : {}),
1046
+ ...(continuation ? { externalContinuation: continuation } : {}),
1047
+ connectAttemptId: attempt.id,
1048
+ personalOwnershipAllowed: isPersonalConnectionOwnerPrincipal(authorization),
1049
+ requestUrl: c.req.url,
1050
+ payload: {
1051
+ requestedScopes: [],
1052
+ mcpUrl: values.mcpUrl,
1053
+ ownership: attempt.ownership,
1054
+ returnUrl: stored.returnUrl,
1055
+ ...(attempt.account ? { connectionId: attempt.account.id } : {}),
1056
+ },
1057
+ });
1058
+ const authorizationUrl = started.authorizationUrl;
1059
+ if (!authorizationUrl)
1060
+ throw new HTTPException(502, {
1061
+ message: "provider did not return an authorization URL",
1062
+ });
1063
+ return {
1064
+ commit: async (_tx, current) => ({
1065
+ ...current,
1066
+ revision: current.revision + 1,
1067
+ state: "requires_user_action",
1068
+ nextAction: { type: "authorize", url: authorizationUrl },
1069
+ }),
1070
+ };
1071
+ },
1072
+ }),
1073
+ );
1074
+ }
1075
+ if (action.type !== "retry" && action.type !== "install")
1076
+ throw new HTTPException(422, {
1077
+ message: "this provider action is not supported by curated OAuth",
1078
+ });
1079
+ if (before.revision === input.expectedRevision) {
1080
+ if (
1081
+ (!before.source && (!before.credentialsCommitted || !before.account)) ||
1082
+ !["connected_but_incomplete", "preview"].includes(before.state)
1083
+ )
1084
+ throw new HTTPException(409, { message: "connection is not ready for preview or install" });
1085
+ if (action.type === "install") {
1086
+ const preview = before.nextAction;
1087
+ if (
1088
+ preview.type !== "preview" ||
1089
+ preview.previewId !== action.previewId ||
1090
+ preview.contentHash !== action.contentHash ||
1091
+ action.operationIds.some(
1092
+ (id) => !preview.operations.some((operation) => operation.id === id),
1093
+ )
1094
+ )
1095
+ throw new HTTPException(409, {
1096
+ message: "review the current preview before installation",
1097
+ });
1098
+ }
1099
+ }
1100
+ return c.json(
1101
+ await executeConnectOperation({
1102
+ db: deps.db,
1103
+ scope,
1104
+ attemptId: c.req.param("attemptId"),
1105
+ expectedRevision: input.expectedRevision,
1106
+ operationId: input.idempotencyKey,
1107
+ inputDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
1108
+ authorize: (tx, _attempt, origin) =>
1109
+ requireConnectOwnerAuthority(tx, scope, permission, origin),
1110
+ execute: async (attempt) => {
1111
+ if (
1112
+ (!attempt.source && (!attempt.credentialsCommitted || !attempt.account)) ||
1113
+ !["connected_but_incomplete", "preview"].includes(attempt.state)
1114
+ )
1115
+ throw new HTTPException(409, {
1116
+ message: "connection is not ready for preview or install",
1117
+ });
1118
+ if (
1119
+ action.type === "install" &&
1120
+ (attempt.nextAction.type !== "preview" ||
1121
+ attempt.nextAction.previewId !== action.previewId ||
1122
+ attempt.nextAction.contentHash !== action.contentHash)
1123
+ )
1124
+ throw new HTTPException(409, {
1125
+ message: "review the current preview before installation",
1126
+ });
1127
+ const previewRequest = {
1128
+ source: attempt.source ?? {
1129
+ kind: "definition" as const,
1130
+ definitionId: attempt.providerId,
1131
+ },
1132
+ ...(attempt.account ? { connectionId: attempt.account.id } : {}),
1133
+ ownership: attempt.ownership,
1134
+ };
1135
+ const resolved = await resolveForRoute({
1136
+ deps,
1137
+ transport,
1138
+ accountId: scope.accountId,
1139
+ workspaceId,
1140
+ subjectId: scope.subjectId,
1141
+ payload: previewRequest,
1142
+ });
1143
+ const preparedPreview = (changed = false) => ({
1144
+ commit: async () => ({
1145
+ ...attempt,
1146
+ error: changed
1147
+ ? {
1148
+ code: "source_changed",
1149
+ message: "The source changed; review the new operations before installing.",
1150
+ retryable: true,
1151
+ }
1152
+ : undefined,
1153
+ revision: attempt.revision + 1,
1154
+ state: "preview" as const,
1155
+ nextAction: {
1156
+ type: "preview" as const,
1157
+ previewId: resolved.preview.revisionId,
1158
+ contentHash: resolved.preview.contentSha256,
1159
+ operations: resolved.preview.tools.map((tool) => ({
1160
+ id: tool.id,
1161
+ label: tool.name,
1162
+ kind: tool.safety,
1163
+ })),
1164
+ },
1165
+ }),
1166
+ });
1167
+ if (action.type === "retry") return preparedPreview();
1168
+ // Always pass the explicit selection; omission must never select all tools.
1169
+ const payload = InstallApiIntegrationRequest.parse({
1170
+ ...previewRequest,
1171
+ expectedRevisionId: action.previewId,
1172
+ expectedContentSha256: action.contentHash,
1173
+ allowedTools: action.operationIds,
1174
+ ...attempt.installationTarget,
1175
+ });
1176
+ let installation: ReturnType<typeof validatedIntegrationInstallInput>;
1177
+ try {
1178
+ installation = validatedIntegrationInstallInput(
1179
+ authorization.grant,
1180
+ workspaceId,
1181
+ payload,
1182
+ resolved,
1183
+ );
1184
+ } catch (error) {
1185
+ // The shared validator's 409 means the reviewed source changed.
1186
+ // No installation write has run; persist a fresh, unselected preview.
1187
+ if (error instanceof HTTPException && error.status === 409)
1188
+ return preparedPreview(true);
1189
+ throw error;
1190
+ }
1191
+ return {
1192
+ commit: async (tx) => {
1193
+ await installApiIntegration(tx, installation);
1194
+ return {
1195
+ ...attempt,
1196
+ error: undefined,
1197
+ revision: attempt.revision + 1,
1198
+ state: "complete" as const,
1199
+ integrationInstalled: true,
1200
+ nextAction: { type: "none" as const },
1201
+ };
1202
+ },
1203
+ };
1204
+ },
1205
+ }),
1206
+ );
1207
+ });
1208
+ app.post("/v1/workspaces/:workspaceId/connect/attempts/:attemptId/cancel", async (c) => {
1209
+ const input = await parseRequestJson(c, ConnectOperationRequest);
1210
+ const workspaceId = c.req.param("workspaceId");
1211
+ const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
1212
+ const continuation = externalActorContinuationForAuthorization(authorization);
1213
+ if (!authorization.contextIntegrity)
1214
+ throw new HTTPException(403, { message: "Verified Connect authority required" });
1215
+ const scope = {
1216
+ accountId: authorization.grant.accountId,
1217
+ workspaceId,
1218
+ subjectId: authorization.grant.subjectId,
1219
+ personalOwnerVerified: isPersonalConnectionOwnerPrincipal(authorization),
1220
+ ...(continuation ? { externalContinuation: continuation } : {}),
1221
+ };
1222
+ return c.json(
1223
+ await executeConnectOperation({
1224
+ db: deps.db,
1225
+ scope,
1226
+ attemptId: c.req.param("attemptId"),
1227
+ expectedRevision: input.expectedRevision,
1228
+ operationId: input.idempotencyKey,
1229
+ inputDigest: createHash("sha256")
1230
+ .update(stableJson({ ...input, action: "cancel" }))
1231
+ .digest("hex"),
1232
+ purpose: "cancellation",
1233
+ authorize: async (tx, attempt, origin) => {
1234
+ const permission =
1235
+ attempt.providerId === "mcp-install"
1236
+ ? "capabilities:manage"
1237
+ : ["x", "reddit"].includes(attempt.providerId)
1238
+ ? attempt.ownership === "workspace"
1239
+ ? "workspace:admin"
1240
+ : "workspace:read"
1241
+ : attempt.providerId === "github-app"
1242
+ ? "github:manage"
1243
+ : attempt.providerId === "github-lens"
1244
+ ? "workspace:admin"
1245
+ : "connections:write";
1246
+ if (
1247
+ !hasPermission(authorization.grant.permissions, permission) ||
1248
+ (attempt.providerId === "github-lens" &&
1249
+ !hasPermission(authorization.grant.permissions, "secrets:write"))
1250
+ )
1251
+ throw new HTTPException(403, { message: "Connect management permission required" });
1252
+ await requireConnectOwnerAuthority(tx, scope, permission, origin);
1253
+ if (attempt.providerId === "github-lens")
1254
+ await requireConnectOwnerAuthority(tx, scope, "secrets:write", origin);
1255
+ },
1256
+ execute: async () => ({
1257
+ commit: async (_tx, current) => ({
1258
+ ...current,
1259
+ revision: current.revision + 1,
1260
+ state: "cancelled",
1261
+ nextAction: { type: "none" },
1262
+ }),
1263
+ }),
1264
+ }),
1265
+ );
1266
+ });
1267
+ app.post("/v1/workspaces/:workspaceId/connect/attempts", async (c) => {
1268
+ if (!deps.settings.integrationsEnabled)
1269
+ throw new HTTPException(403, { message: "integrations disabled" });
1270
+ const workspaceId = c.req.param("workspaceId");
1271
+ const input = await parseRequestJson(c, BeginConnectRequest);
1272
+ const setupPermission =
1273
+ input.providerId === "mcp-install"
1274
+ ? "capabilities:manage"
1275
+ : ["x", "reddit"].includes(input.providerId)
1276
+ ? input.ownership === "workspace"
1277
+ ? "workspace:admin"
1278
+ : "workspace:read"
1279
+ : input.providerId === "github-app"
1280
+ ? "github:manage"
1281
+ : input.providerId === "github-lens"
1282
+ ? "workspace:admin"
1283
+ : "connections:write";
1284
+ const authorization = await requireAccessGrantAuthorization(
1285
+ c,
1286
+ deps,
1287
+ workspaceId,
1288
+ setupPermission,
1289
+ );
1290
+ const continuation = externalActorContinuationForAuthorization(authorization);
1291
+ if (!authorization.contextIntegrity)
1292
+ throw new HTTPException(403, {
1293
+ message: "Verified Connect authority required",
1294
+ });
1295
+ if (input.providerId === "atlassian")
1296
+ throw new HTTPException(410, { message: ATLASSIAN_NATIVE_RETIRED_MESSAGE });
1297
+ if (input.providerId === "gmail" && input.ownership !== "personal")
1298
+ throw new HTTPException(422, { message: "Gmail connections must be personal-owned" });
1299
+ let target: URL;
1300
+ try {
1301
+ target = new URL(input.returnUrl);
1302
+ } catch {
1303
+ throw new HTTPException(400, { message: "invalid return URL" });
1304
+ }
1305
+ if (
1306
+ !["https:", "http:"].includes(target.protocol) ||
1307
+ target.username ||
1308
+ target.password ||
1309
+ /[\u0000-\u0020\u007f]/.test(input.returnUrl)
1310
+ )
1311
+ throw new HTTPException(400, { message: "invalid return URL" });
1312
+ const scope = {
1313
+ accountId: authorization.grant.accountId,
1314
+ workspaceId,
1315
+ subjectId: authorization.grant.subjectId,
1316
+ personalOwnerVerified: isPersonalConnectionOwnerPrincipal(authorization),
1317
+ ...(continuation ? { externalContinuation: continuation } : {}),
1318
+ };
1319
+ return c.json(
1320
+ await (async () => {
1321
+ // Remote setup preparation stays outside the policy and actor transactions.
1322
+ // Repeat both admissions when persisting the resulting attempt.
1323
+ const tx = deps.db;
1324
+ const admit = () =>
1325
+ withOrganizationIntegrationPolicyFence(deps.db, scope, async (policyTx, policy) => {
1326
+ return withWorkspaceSubjectRls(
1327
+ policyTx,
1328
+ workspaceId,
1329
+ scope.subjectId,
1330
+ async (actorTx) => {
1331
+ await requireConnectOwnerAuthority(actorTx, scope, setupPermission);
1332
+ const replay = await getConnectBeginReplay(actorTx, scope, {
1333
+ idempotencyKey: input.idempotencyKey,
1334
+ requestDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
1335
+ returnUrl: input.returnUrl,
1336
+ ...(continuation ? { externalContinuation: continuation } : {}),
1337
+ });
1338
+ if (replay) return replay;
1339
+ assertOrganizationIntegrationAllowed(
1340
+ policy,
1341
+ integrationKeyForConnectProvider(input.providerId),
1342
+ );
1343
+ return null;
1344
+ },
1345
+ );
1346
+ });
1347
+ const replay = await admit();
1348
+ if (replay) return replay;
1349
+ const beginConnectAttempt = async (
1350
+ _db: Parameters<typeof persistConnectAttempt>[0],
1351
+ _scope: Parameters<typeof persistConnectAttempt>[1],
1352
+ attemptInput: Parameters<typeof persistConnectAttempt>[2],
1353
+ ) =>
1354
+ withOrganizationIntegrationPolicyFence(deps.db, scope, async (policyTx, policy) => {
1355
+ return withWorkspaceSubjectRls(
1356
+ policyTx,
1357
+ workspaceId,
1358
+ scope.subjectId,
1359
+ async (actorTx) => {
1360
+ await requireConnectOwnerAuthority(actorTx, scope, setupPermission);
1361
+ return persistConnectAttempt(actorTx, scope, {
1362
+ ...attemptInput,
1363
+ authorizeAcquisition: async () => {
1364
+ assertOrganizationIntegrationAllowed(
1365
+ policy,
1366
+ integrationKeyForConnectProvider(input.providerId),
1367
+ );
1368
+ },
1369
+ });
1370
+ },
1371
+ );
1372
+ });
1373
+ const id = randomUUID();
1374
+ if (input.providerId === "mcp-install") {
1375
+ if (
1376
+ input.ownership !== "workspace" ||
1377
+ input.reconnectAccountId ||
1378
+ input.installationTarget
1379
+ )
1380
+ throw new HTTPException(422, {
1381
+ message: "MCP capabilities are workspace installations, not credential accounts",
1382
+ });
1383
+ const catalog = await buildCapabilityCatalog({
1384
+ db: tx,
1385
+ workspaceId,
1386
+ settings: deps.settings,
1387
+ subjectId: scope.subjectId,
1388
+ });
1389
+ const options = catalog.items
1390
+ .filter(
1391
+ (item) =>
1392
+ item.kind === "mcp" &&
1393
+ item.surfaceType !== "codex_apps" &&
1394
+ item.runtime.available &&
1395
+ item.authKind === "none",
1396
+ )
1397
+ .map((item) => ({ value: item.id, label: item.name }));
1398
+ if (options.length > 1000)
1399
+ throw new HTTPException(422, {
1400
+ message: "MCP catalog exceeds the setup chooser limit",
1401
+ });
1402
+ return beginConnectAttempt(tx, scope, {
1403
+ ...(continuation ? { externalContinuation: continuation } : {}),
1404
+ idempotencyKey: input.idempotencyKey,
1405
+ requestDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
1406
+ returnUrl: input.returnUrl,
1407
+ attempt: {
1408
+ id,
1409
+ workspaceId,
1410
+ providerId: input.providerId,
1411
+ ownership: "workspace",
1412
+ revision: 1,
1413
+ state: "credential_input",
1414
+ credentialsCommitted: false,
1415
+ integrationInstalled: false,
1416
+ completionRequirement: "provider_setup",
1417
+ nextAction: {
1418
+ type: "credentials",
1419
+ fields: [
1420
+ {
1421
+ name: "capabilityId",
1422
+ label: "MCP capability",
1423
+ required: true,
1424
+ secret: false,
1425
+ options,
1426
+ },
1427
+ ],
1428
+ },
1429
+ expiresAt: new Date(Date.now() + 30 * 60_000).toISOString(),
1430
+ },
1431
+ });
1432
+ }
1433
+ if (input.providerId === "x" || input.providerId === "reddit") {
1434
+ if (input.installationTarget)
1435
+ throw new HTTPException(422, {
1436
+ message: "Social setup does not create an API installation",
1437
+ });
1438
+ if (input.ownership === "personal" && !scope.personalOwnerVerified)
1439
+ throw new HTTPException(403, { message: "Personal ownership unavailable" });
1440
+ const reconnectId = input.reconnectAccountId?.startsWith("social:")
1441
+ ? input.reconnectAccountId.slice("social:".length)
1442
+ : null;
1443
+ const existing =
1444
+ reconnectId && z.string().uuid().safeParse(reconnectId).success
1445
+ ? await getSocialConnection(tx, workspaceId, reconnectId, scope.subjectId)
1446
+ : null;
1447
+ if (
1448
+ input.reconnectAccountId &&
1449
+ (!existing ||
1450
+ existing.provider !== input.providerId ||
1451
+ existing.ownership !== input.ownership ||
1452
+ !existing.version ||
1453
+ !existing.externalAccountId)
1454
+ )
1455
+ throw new HTTPException(409, {
1456
+ message: "Social account changed or cannot be safely reconnected",
1457
+ });
1458
+ const started = await startSocialOAuth(
1459
+ { db: tx, settings: deps.settings, observability: deps.observability },
1460
+ {
1461
+ accountId: scope.accountId,
1462
+ workspaceId,
1463
+ subjectId: scope.subjectId,
1464
+ personalOwnershipAllowed: scope.personalOwnerVerified,
1465
+ connectAttemptId: id,
1466
+ requestUrl: c.req.url,
1467
+ payload: { provider: input.providerId, ownership: input.ownership },
1468
+ },
1469
+ );
1470
+ if (!started.authorizationUrl)
1471
+ throw new HTTPException(503, { message: "Social authorization is unavailable" });
1472
+ return beginConnectAttempt(tx, scope, {
1473
+ ...(continuation ? { externalContinuation: continuation } : {}),
1474
+ idempotencyKey: input.idempotencyKey,
1475
+ requestDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
1476
+ returnUrl: input.returnUrl,
1477
+ attempt: {
1478
+ id,
1479
+ workspaceId,
1480
+ providerId: input.providerId,
1481
+ ownership: input.ownership,
1482
+ revision: 1,
1483
+ state: "requires_user_action",
1484
+ credentialsCommitted: false,
1485
+ integrationInstalled: false,
1486
+ completionRequirement: "connection",
1487
+ ...(existing
1488
+ ? {
1489
+ account: {
1490
+ id: `social:${existing.id}`,
1491
+ version: existing.version!,
1492
+ providerId: input.providerId,
1493
+ label: existing.accountHandle,
1494
+ ownership: input.ownership,
1495
+ status: "auth_needed" as const,
1496
+ },
1497
+ }
1498
+ : {}),
1499
+ nextAction: { type: "authorize", url: started.authorizationUrl },
1500
+ expiresAt: started.expiresAt,
1501
+ },
1502
+ });
1503
+ }
1504
+ if (["github-app", "github-lens"].includes(input.providerId)) {
1505
+ if (
1506
+ input.ownership !== "workspace" ||
1507
+ input.installationTarget ||
1508
+ input.reconnectAccountId
1509
+ )
1510
+ throw new HTTPException(422, {
1511
+ message:
1512
+ "GitHub App setup requires workspace ownership and explicit installation selection",
1513
+ });
1514
+ if (input.providerId === "github-lens") {
1515
+ if (!hasPermission(authorization.grant.permissions, "secrets:write"))
1516
+ throw new HTTPException(403, { message: "secrets:write required" });
1517
+ await requireConnectOwnerAuthority(tx, scope, "secrets:write");
1518
+ await requireGitHubLensConnect({ ...deps, db: tx });
1519
+ }
1520
+ const navigation = githubAppConnectNavigation(
1521
+ deps,
1522
+ scope,
1523
+ id,
1524
+ c.req.url,
1525
+ "discover",
1526
+ undefined,
1527
+ input.providerId === "github-lens" ? "github-lens" : "github-app",
1528
+ );
1529
+ return beginConnectAttempt(tx, scope, {
1530
+ ...(continuation ? { externalContinuation: continuation } : {}),
1531
+ idempotencyKey: input.idempotencyKey,
1532
+ requestDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
1533
+ returnUrl: input.returnUrl,
1534
+ attempt: {
1535
+ id,
1536
+ workspaceId,
1537
+ providerId: input.providerId,
1538
+ ownership: "workspace",
1539
+ revision: 1,
1540
+ state: "requires_user_action",
1541
+ credentialsCommitted: false,
1542
+ integrationInstalled: false,
1543
+ completionRequirement: "provider_setup",
1544
+ nextAction: { type: "authorize", url: navigation.authorizationUrl },
1545
+ expiresAt: navigation.expiresAt,
1546
+ },
1547
+ });
1548
+ }
1549
+ if (["openapi", "graphql"].includes(input.providerId)) {
1550
+ if (input.reconnectAccountId)
1551
+ throw new HTTPException(422, { message: "Select the service connection during setup" });
1552
+ if (input.ownership === "personal" && !isPersonalConnectionOwnerPrincipal(authorization))
1553
+ throw new HTTPException(403, { message: "personal ownership unavailable" });
1554
+ const choices = hasPermission(authorization.grant.permissions, "connections:read")
1555
+ ? (await listConnectionsMetadata(tx, workspaceId, scope.subjectId))
1556
+ .filter(
1557
+ (connection) =>
1558
+ connection.status === "active" &&
1559
+ (input.ownership === "workspace"
1560
+ ? connection.subjectId === null
1561
+ : connection.subjectId === scope.subjectId),
1562
+ )
1563
+ .map((connection) => ({
1564
+ value: connection.id,
1565
+ label: `${connection.providerDomain} · ${connection.id}`,
1566
+ }))
1567
+ : [];
1568
+ if (choices.length > 1000)
1569
+ throw new HTTPException(422, {
1570
+ message: "Account inventory exceeds the setup chooser limit",
1571
+ });
1572
+ return beginConnectAttempt(tx, scope, {
1573
+ ...(continuation ? { externalContinuation: continuation } : {}),
1574
+ idempotencyKey: input.idempotencyKey,
1575
+ requestDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
1576
+ returnUrl: input.returnUrl,
1577
+ attempt: {
1578
+ id,
1579
+ workspaceId,
1580
+ providerId: input.providerId,
1581
+ ownership: input.ownership,
1582
+ revision: 1,
1583
+ state: "credential_input",
1584
+ credentialsCommitted: false,
1585
+ integrationInstalled: false,
1586
+ completionRequirement: "integration",
1587
+ installationTarget: input.installationTarget ?? {
1588
+ instanceKey: `account-${id}`,
1589
+ displayName: input.providerId === "openapi" ? "OpenAPI service" : "GraphQL service",
1590
+ },
1591
+ nextAction: {
1592
+ type: "credentials",
1593
+ fields: [
1594
+ {
1595
+ name: "url",
1596
+ label:
1597
+ input.providerId === "openapi"
1598
+ ? "OpenAPI document URL"
1599
+ : "GraphQL endpoint URL",
1600
+ required: true,
1601
+ secret: false,
1602
+ },
1603
+ {
1604
+ name: "connectionId",
1605
+ label: "Service account (optional for public services)",
1606
+ required: false,
1607
+ secret: false,
1608
+ options: choices,
1609
+ },
1610
+ ],
1611
+ },
1612
+ expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(),
1613
+ },
1614
+ });
1615
+ }
1616
+ if (
1617
+ input.installationTarget &&
1618
+ [
1619
+ "mcp-oauth",
1620
+ "slack-personal",
1621
+ "gmail",
1622
+ "mcp-bearer",
1623
+ "mcp-headers",
1624
+ "fiken-token",
1625
+ "fiken-oauth",
1626
+ "atlassian",
1627
+ "google-drive-knowledge",
1628
+ "google-drive-publish",
1629
+ "slack-bot",
1630
+ "github-personal",
1631
+ ].includes(input.providerId)
1632
+ )
1633
+ throw new HTTPException(422, {
1634
+ message: "MCP credential setup does not install an API integration instance",
1635
+ });
1636
+ if (
1637
+ [
1638
+ "mcp-oauth",
1639
+ "slack-personal",
1640
+ "gmail",
1641
+ "mcp-bearer",
1642
+ "mcp-headers",
1643
+ "fiken-token",
1644
+ ].includes(input.providerId)
1645
+ ) {
1646
+ if (input.providerId === "fiken-token" && input.ownership !== "workspace")
1647
+ throw new HTTPException(422, { message: "Fiken is workspace-owned" });
1648
+ requireEnvironmentEncryption(deps.settings);
1649
+ if (["mcp-oauth", "slack-personal", "gmail"].includes(input.providerId))
1650
+ requireIntegrationsStateSecret(deps.settings);
1651
+ const existing = input.reconnectAccountId
1652
+ ? await getConnectionMetadata(
1653
+ tx,
1654
+ workspaceId,
1655
+ input.reconnectAccountId,
1656
+ scope.subjectId,
1657
+ )
1658
+ : null;
1659
+ if (
1660
+ input.reconnectAccountId &&
1661
+ (!existing ||
1662
+ (input.providerId === "fiken-token"
1663
+ ? !isFikenConnection(existing)
1664
+ : ["mcp-oauth", "slack-personal", "gmail"].includes(input.providerId)
1665
+ ? existing.kind !== "oauth2" ||
1666
+ !existing.metadata.oauthDiscovery ||
1667
+ (input.providerId === "slack-personal" &&
1668
+ existing.metadata.mcpUrl !== OPENGENI_PERSONAL_SLACK_MCP_URL) ||
1669
+ (input.providerId === "gmail" &&
1670
+ existing.metadata.mcpUrl !== OFFICIAL_GMAIL_MCP_URL)
1671
+ : existing.kind !== "api_key" ||
1672
+ existing.metadata.connectAdapter !== input.providerId) ||
1673
+ (existing.subjectId === null ? "workspace" : "personal") !== input.ownership)
1674
+ )
1675
+ throw new HTTPException(404, { message: "MCP account not found for this ownership" });
1676
+ if (input.ownership === "personal" && !isPersonalConnectionOwnerPrincipal(authorization))
1677
+ throw new HTTPException(403, { message: "personal ownership unavailable" });
1678
+ return beginConnectAttempt(tx, scope, {
1679
+ ...(continuation ? { externalContinuation: continuation } : {}),
1680
+ idempotencyKey: input.idempotencyKey,
1681
+ requestDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
1682
+ returnUrl: input.returnUrl,
1683
+ attempt: {
1684
+ id,
1685
+ workspaceId,
1686
+ providerId: input.providerId,
1687
+ ownership: input.ownership,
1688
+ revision: 1,
1689
+ state: "credential_input",
1690
+ credentialsCommitted: false,
1691
+ integrationInstalled: false,
1692
+ completionRequirement: "connection",
1693
+ ...(existing
1694
+ ? {
1695
+ account: {
1696
+ id: existing.id,
1697
+ version: existing.version,
1698
+ providerId: input.providerId,
1699
+ label: existing.providerDomain,
1700
+ ownership: input.ownership,
1701
+ status: "auth_needed" as const,
1702
+ },
1703
+ }
1704
+ : {}),
1705
+ nextAction: {
1706
+ type: "credentials",
1707
+ fields: ["slack-personal", "gmail"].includes(input.providerId)
1708
+ ? []
1709
+ : input.providerId === "fiken-token"
1710
+ ? [
1711
+ {
1712
+ name: "apiToken",
1713
+ label: "Fiken API token",
1714
+ required: true,
1715
+ secret: true,
1716
+ },
1717
+ {
1718
+ name: "defaultCompanySlug",
1719
+ label: "Default company slug (optional)",
1720
+ required: false,
1721
+ secret: false,
1722
+ },
1723
+ ]
1724
+ : [
1725
+ {
1726
+ name: "mcpUrl",
1727
+ label: "MCP server URL",
1728
+ required: true,
1729
+ secret: false,
1730
+ },
1731
+ ...(input.providerId === "mcp-bearer"
1732
+ ? [
1733
+ {
1734
+ name: "token",
1735
+ label: "Bearer credential",
1736
+ required: true,
1737
+ secret: true,
1738
+ },
1739
+ ]
1740
+ : input.providerId === "mcp-headers"
1741
+ ? [
1742
+ {
1743
+ name: "headers",
1744
+ label: "Credential headers (JSON)",
1745
+ required: true,
1746
+ secret: true,
1747
+ },
1748
+ ]
1749
+ : []),
1750
+ ],
1751
+ },
1752
+ expiresAt: new Date(Date.now() + 15 * 60_000).toISOString(),
1753
+ },
1754
+ });
1755
+ }
1756
+ if (
1757
+ ["fiken-oauth", "slack-bot"].includes(input.providerId) &&
1758
+ input.ownership !== "workspace"
1759
+ )
1760
+ throw new HTTPException(422, { message: "This connector is workspace-owned" });
1761
+ if (
1762
+ [
1763
+ "atlassian",
1764
+ "google-drive-knowledge",
1765
+ "google-drive-publish",
1766
+ "github-personal",
1767
+ ].includes(input.providerId) &&
1768
+ (input.ownership !== "personal" || !isPersonalConnectionOwnerPrincipal(authorization))
1769
+ )
1770
+ throw new HTTPException(422, { message: "This connector requires personal ownership" });
1771
+ const started =
1772
+ input.providerId === "github-personal"
1773
+ ? await startPersonalGitHubOAuth(
1774
+ { ...deps, db: tx },
1775
+ {
1776
+ access: authorization,
1777
+ workspaceId,
1778
+ connectAttemptId: id,
1779
+ ...(input.reconnectAccountId ? { connectionId: input.reconnectAccountId } : {}),
1780
+ },
1781
+ )
1782
+ : input.providerId === "slack-bot"
1783
+ ? await startSlackBotInstall(
1784
+ { ...deps, db: tx },
1785
+ {
1786
+ ...scope,
1787
+ requestUrl: c.req.url,
1788
+ connectAttemptId: id,
1789
+ ...(input.reconnectAccountId ? { connectionId: input.reconnectAccountId } : {}),
1790
+ },
1791
+ )
1792
+ : ["google-drive-knowledge", "google-drive-publish"].includes(input.providerId)
1793
+ ? await startGoogleDriveOAuth(
1794
+ { ...deps, db: tx },
1795
+ {
1796
+ ...scope,
1797
+ requestUrl: c.req.url,
1798
+ connectAttemptId: id,
1799
+ payload: {
1800
+ capability:
1801
+ input.providerId === "google-drive-publish" ? "publish" : "source_read",
1802
+ ...(input.reconnectAccountId
1803
+ ? { connectionId: input.reconnectAccountId }
1804
+ : {}),
1805
+ },
1806
+ },
1807
+ )
1808
+ : input.providerId === "atlassian"
1809
+ ? await startAtlassianOAuth(
1810
+ { ...deps, db: tx },
1811
+ {
1812
+ ...scope,
1813
+ requestUrl: c.req.url,
1814
+ connectAttemptId: id,
1815
+ payload: input.reconnectAccountId
1816
+ ? { connectionId: input.reconnectAccountId }
1817
+ : {},
1818
+ },
1819
+ )
1820
+ : input.providerId === "fiken-oauth"
1821
+ ? await startFikenOAuth(
1822
+ { ...deps, db: tx },
1823
+ {
1824
+ ...scope,
1825
+ requestUrl: c.req.url,
1826
+ connectAttemptId: id,
1827
+ payload: input.reconnectAccountId
1828
+ ? { connectionId: input.reconnectAccountId }
1829
+ : {},
1830
+ },
1831
+ )
1832
+ : await startApiIntegrationProviderOAuth(
1833
+ { ...deps, db: tx },
1834
+ {
1835
+ ...scope,
1836
+ personalOwnershipAllowed:
1837
+ isPersonalConnectionOwnerPrincipal(authorization),
1838
+ ...(continuation ? { externalContinuation: continuation } : {}),
1839
+ connectAttemptId: id,
1840
+ requestUrl: c.req.url,
1841
+ payload: {
1842
+ definitionId: input.providerId,
1843
+ ownership: input.ownership,
1844
+ ...(input.reconnectAccountId
1845
+ ? { connectionId: input.reconnectAccountId }
1846
+ : {}),
1847
+ },
1848
+ },
1849
+ );
1850
+ if (!started.authorizationUrl)
1851
+ throw new HTTPException(502, { message: "provider did not return an authorization URL" });
1852
+ const reconnect = input.reconnectAccountId
1853
+ ? await getConnectionMetadata(tx, workspaceId, input.reconnectAccountId, scope.subjectId)
1854
+ : null;
1855
+ return beginConnectAttempt(tx, scope, {
1856
+ ...(continuation ? { externalContinuation: continuation } : {}),
1857
+ idempotencyKey: input.idempotencyKey,
1858
+ requestDigest: createHash("sha256").update(stableJson(input)).digest("hex"),
1859
+ returnUrl: input.returnUrl,
1860
+ attempt: {
1861
+ id,
1862
+ workspaceId,
1863
+ providerId: input.providerId,
1864
+ ownership: input.ownership,
1865
+ revision: 1,
1866
+ state: "requires_user_action",
1867
+ credentialsCommitted: false,
1868
+ integrationInstalled: false,
1869
+ completionRequirement: [
1870
+ "github-personal",
1871
+ "slack-bot",
1872
+ "fiken-oauth",
1873
+ "atlassian",
1874
+ "google-drive-knowledge",
1875
+ "google-drive-publish",
1876
+ ].includes(input.providerId)
1877
+ ? "connection"
1878
+ : "integration",
1879
+ ...([
1880
+ "github-personal",
1881
+ "slack-bot",
1882
+ "fiken-oauth",
1883
+ "atlassian",
1884
+ "google-drive-knowledge",
1885
+ "google-drive-publish",
1886
+ ].includes(input.providerId)
1887
+ ? {}
1888
+ : {
1889
+ installationTarget: input.installationTarget ?? {
1890
+ instanceKey: `account-${id}`,
1891
+ displayName: input.providerId,
1892
+ },
1893
+ }),
1894
+ ...(reconnect
1895
+ ? {
1896
+ account: {
1897
+ id: reconnect.id,
1898
+ version: reconnect.version,
1899
+ providerId: input.providerId,
1900
+ label: reconnect.providerDomain,
1901
+ ownership: input.ownership,
1902
+ status: "auth_needed" as const,
1903
+ },
1904
+ }
1905
+ : {}),
1906
+ nextAction: { type: "authorize", url: started.authorizationUrl },
1907
+ expiresAt: started.expiresAt,
1908
+ },
1909
+ });
1910
+ })(),
1911
+ );
1912
+ });
1913
+ app.get("/v1/workspaces/:workspaceId/connect/attempts", async (c) => {
1914
+ const workspaceId = c.req.param("workspaceId");
1915
+ const { grant } = await requireAccessGrantAuthorization(c, deps, workspaceId);
1916
+ return c.json(
1917
+ (
1918
+ await listPendingConnectAttempts(deps.db, {
1919
+ accountId: grant.accountId,
1920
+ workspaceId,
1921
+ subjectId: grant.subjectId,
1922
+ })
1923
+ ).filter((attempt) => canReadAttempt(grant.permissions, attempt.providerId)),
1924
+ );
1925
+ });
1926
+ app.get("/v1/workspaces/:workspaceId/connect/attempts/:attemptId", async (c) => {
1927
+ const workspaceId = c.req.param("workspaceId");
1928
+ const { grant } = await requireAccessGrantAuthorization(c, deps, workspaceId);
1929
+ const { attempt } = await getConnectAttempt(
1930
+ deps.db,
1931
+ { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
1932
+ c.req.param("attemptId"),
1933
+ );
1934
+ if (!canReadAttempt(grant.permissions, attempt.providerId))
1935
+ throw new HTTPException(403, { message: "Connect read permission required" });
1936
+ return c.json(attempt);
1937
+ });
1938
+ }