@opengeni/api-router 0.30.1 → 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 (436) 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-office-import.d.ts +7 -4
  35. package/dist/editable-artifact-source-session.d.ts +3 -0
  36. package/dist/editable-artifact-websocket.d.ts +10 -18
  37. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  38. package/dist/fatal-process-boundary.d.ts +25 -0
  39. package/dist/github-access.d.ts +31 -2
  40. package/dist/http/acting-person.d.ts +24 -0
  41. package/dist/http/api-contract-compat.d.ts +19 -0
  42. package/dist/http/api-error.d.ts +27 -1
  43. package/dist/http/cors.d.ts +17 -0
  44. package/dist/http/deprecation.d.ts +42 -0
  45. package/dist/http/interaction-control-error.d.ts +13 -0
  46. package/dist/http/keyed-admission.d.ts +12 -0
  47. package/dist/http/local-browser-boundary.d.ts +72 -0
  48. package/dist/http/metrics-listener.d.ts +22 -0
  49. package/dist/http/path-identifier.d.ts +17 -0
  50. package/dist/http/registered-route-label.d.ts +16 -0
  51. package/dist/http/request-body.d.ts +27 -0
  52. package/dist/http/request-source.d.ts +52 -0
  53. package/dist/http/sse.d.ts +17 -6
  54. package/dist/http/unmatched-route.d.ts +18 -0
  55. package/dist/http/user-content.d.ts +36 -0
  56. package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
  57. package/dist/index.d.ts +10 -3
  58. package/dist/index.js +1129 -166
  59. package/dist/index.js.map +1 -1
  60. package/dist/integration-endpoint-test.d.ts +20 -0
  61. package/dist/integrations/api-integrations.d.ts +2 -2
  62. package/dist/integrations/atlassian.d.ts +29 -3
  63. package/dist/integrations/connect-authority.d.ts +1 -0
  64. package/dist/integrations/connect-callback-return.d.ts +6 -0
  65. package/dist/integrations/fiken.d.ts +16 -2
  66. package/dist/integrations/github-app-connect.d.ts +47 -0
  67. package/dist/integrations/github-installation-proof.d.ts +3 -0
  68. package/dist/integrations/github-lens-connect.d.ts +116 -0
  69. package/dist/integrations/github-skill-source.d.ts +1 -5
  70. package/dist/integrations/google-drive.d.ts +25 -0
  71. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  72. package/dist/integrations/marketplace-plugin.d.ts +2691 -0
  73. package/dist/integrations/oauth-client.d.ts +178 -18
  74. package/dist/integrations/oauth-profiles.d.ts +222 -0
  75. package/dist/integrations/oauth-return-path.d.ts +1 -0
  76. package/dist/integrations/personal-github-repositories.d.ts +84 -0
  77. package/dist/integrations/personal-github.d.ts +42 -0
  78. package/dist/integrations/pr-review-provider.d.ts +22 -0
  79. package/dist/integrations/provider-oauth.d.ts +19 -1
  80. package/dist/integrations/slack-app-home.d.ts +24 -0
  81. package/dist/integrations/slack-bot.d.ts +367 -12
  82. package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
  83. package/dist/integrations/slack-install.d.ts +20 -0
  84. package/dist/integrations/slack-interactions.d.ts +113 -10
  85. package/dist/integrations/slack-mrkdwn.d.ts +41 -0
  86. package/dist/integrations/slack-routing.d.ts +120 -0
  87. package/dist/integrations/slack-session-defaults.d.ts +58 -0
  88. package/dist/integrations/slack-task-file-upload.d.ts +36 -0
  89. package/dist/integrations/social-oauth.d.ts +7 -0
  90. package/dist/interaction-agent-access.d.ts +15 -0
  91. package/dist/interaction-frame-proxy.d.ts +68 -0
  92. package/dist/interaction-metrics.d.ts +2 -0
  93. package/dist/mcp/action-catalog-types.d.ts +9 -0
  94. package/dist/mcp/action-catalog.gen.d.ts +2 -0
  95. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  96. package/dist/mcp/child-read-evidence.d.ts +9 -0
  97. package/dist/mcp/command-read.d.ts +24 -0
  98. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  99. package/dist/mcp/contract-input.d.ts +11 -0
  100. package/dist/mcp/documents.d.ts +7 -5
  101. package/dist/mcp/files.d.ts +2 -2
  102. package/dist/mcp/first-party-tool-permissions.d.ts +650 -0
  103. package/dist/mcp/knowledge-entries.d.ts +5 -0
  104. package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
  105. package/dist/mcp/receipts.d.ts +9 -0
  106. package/dist/mcp/request-abort.d.ts +19 -0
  107. package/dist/mcp/scheduled-task-input.d.ts +1189 -0
  108. package/dist/mcp/scheduled-task-view.d.ts +22 -4
  109. package/dist/mcp/server.d.ts +255 -3
  110. package/dist/mcp/session-event-view.d.ts +94 -0
  111. package/dist/mcp/session-view.d.ts +66 -2
  112. package/dist/mcp/session-wait.d.ts +174 -0
  113. package/dist/mcp-oauth.d.ts +52 -0
  114. package/dist/model-catalog.d.ts +11 -31
  115. package/dist/observability.d.ts +3 -1
  116. package/dist/organization-mcp.d.ts +52 -0
  117. package/dist/organization-recovery-notifications.d.ts +41 -0
  118. package/dist/routes/analytics-consent.d.ts +26 -0
  119. package/dist/routes/api-integrations.d.ts +25 -0
  120. package/dist/routes/api-keys.d.ts +17 -0
  121. package/dist/routes/artifact-catalog.d.ts +6 -0
  122. package/dist/routes/automations.d.ts +19 -0
  123. package/dist/routes/billing.d.ts +49 -0
  124. package/dist/routes/browser-sessions.d.ts +71 -3
  125. package/dist/routes/claude-subscription-accounts.d.ts +3 -0
  126. package/dist/routes/claude-subscription-oauth.d.ts +3 -0
  127. package/dist/routes/client-errors.d.ts +146 -0
  128. package/dist/routes/codex.d.ts +76 -2
  129. package/dist/routes/company-brain.d.ts +3 -0
  130. package/dist/routes/company-profile.d.ts +2 -1
  131. package/dist/routes/computer-sessions.d.ts +14 -0
  132. package/dist/routes/connect.d.ts +5 -0
  133. package/dist/routes/editable-artifacts.d.ts +2 -0
  134. package/dist/routes/external-identity-links.d.ts +6 -0
  135. package/dist/routes/feedback.d.ts +3 -0
  136. package/dist/routes/files.d.ts +4 -0
  137. package/dist/routes/github-browser-pages.d.ts +12 -0
  138. package/dist/routes/github.d.ts +2 -0
  139. package/dist/routes/insights-response-cache.d.ts +10 -0
  140. package/dist/routes/insights-usage.d.ts +12 -0
  141. package/dist/routes/insights.d.ts +30 -1
  142. package/dist/routes/knowledge.d.ts +3 -0
  143. package/dist/routes/managed-auth-session-sets.d.ts +26 -0
  144. package/dist/routes/managed-onboarding.d.ts +29 -0
  145. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  146. package/dist/routes/model-connection-access.d.ts +3 -0
  147. package/dist/routes/organization-integration-policy.d.ts +3 -0
  148. package/dist/routes/organization-integrations.d.ts +6 -0
  149. package/dist/routes/organization-mcp-connections.d.ts +3 -0
  150. package/dist/routes/organization-model-providers.d.ts +3 -0
  151. package/dist/routes/organization-recovery.d.ts +19 -0
  152. package/dist/routes/organization-service-accounts.d.ts +3 -0
  153. package/dist/routes/organization-sessions.d.ts +26 -0
  154. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  155. package/dist/routes/personal-github.d.ts +3 -0
  156. package/dist/routes/pr-review-github.d.ts +3 -0
  157. package/dist/routes/pr-review.d.ts +3 -0
  158. package/dist/routes/session-artifact-associations.d.ts +4 -0
  159. package/dist/routes/session-history-imports.d.ts +9 -0
  160. package/dist/routes/sessions.d.ts +44 -6
  161. package/dist/routes/skill-content.d.ts +4 -0
  162. package/dist/routes/skill-install-authority.d.ts +6 -0
  163. package/dist/routes/subscription-account-pools.d.ts +34 -0
  164. package/dist/routes/subscription-pool-access.d.ts +16 -0
  165. package/dist/routes/supergrok.d.ts +6 -1
  166. package/dist/routes/usage-allowances.d.ts +12 -0
  167. package/dist/routes/user-resource-authorities.d.ts +3 -0
  168. package/dist/routes/workspace-artifacts.d.ts +2 -1
  169. package/dist/routes/workspace-capture.d.ts +10 -2
  170. package/dist/routes/workspace-integrations.d.ts +35 -0
  171. package/dist/routes/workspace-learning.d.ts +3 -0
  172. package/dist/routes/workspace-model-providers.d.ts +4 -0
  173. package/dist/routes/workspaces.d.ts +29 -0
  174. package/dist/sandbox/auth-callout.d.ts +8 -0
  175. package/dist/sandbox/channel-a.d.ts +19 -1
  176. package/dist/sandbox/connection-authority.d.ts +3 -0
  177. package/dist/sandbox/enrollment.d.ts +9 -1
  178. package/dist/sandbox/machines.d.ts +5 -2
  179. package/dist/sandbox/metrics-ingestion.d.ts +28 -20
  180. package/dist/sandbox/rematerialize.d.ts +2 -0
  181. package/dist/sandbox/session-attach-variable-sets.d.ts +30 -0
  182. package/dist/sandbox/viewer.d.ts +26 -1
  183. package/dist/sandbox-file-artifacts.d.ts +50 -0
  184. package/dist/scheduled-task-deletion.d.ts +13 -0
  185. package/dist/session-create-observability.d.ts +5 -0
  186. package/dist/session-event-view-MPS2OZD3.js +292 -0
  187. package/dist/session-event-view-MPS2OZD3.js.map +1 -0
  188. package/dist/site-session-origin.d.ts +13 -0
  189. package/dist/site-uploads.d.ts +41 -0
  190. package/dist/slack-reaction-files.d.ts +1 -1
  191. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  192. package/dist/temporal-schedule-sync.d.ts +26 -0
  193. package/dist/transcription/providers/xai-subscription.d.ts +1 -0
  194. package/dist/transcription/service.d.ts +2 -1
  195. package/dist/work-discovery-observability.d.ts +33 -0
  196. package/dist/workspace-artifact-content.d.ts +28 -0
  197. package/dist/workspace-artifact-provenance.d.ts +7 -0
  198. package/dist/workspace-delete-observability.d.ts +10 -0
  199. package/dist/workspace-deletion.d.ts +6 -0
  200. package/dist/workspace-state-projection.d.ts +3 -5
  201. package/dist/workspace-tool-gateway-observability.d.ts +17 -0
  202. package/dist/workspace-tool-gateway.d.ts +123 -0
  203. package/dist/workspace-webhook-dispatch.d.ts +26 -0
  204. package/package.json +23 -19
  205. package/src/access-grant-rls.ts +75 -0
  206. package/src/admission-trace.ts +14 -0
  207. package/src/api-websocket.ts +23 -0
  208. package/src/app.ts +1974 -256
  209. package/src/artifact-catalog-cursor.ts +60 -0
  210. package/src/auth/managed-auth-attempt-context.ts +70 -0
  211. package/src/auth/managed-auth-rate-limits.ts +252 -0
  212. package/src/auth/managed-auth-session-adapter.ts +222 -0
  213. package/src/auth/managed-auth.ts +536 -60
  214. package/src/auth/managed-email.ts +174 -0
  215. package/src/auth/managed-sign-in-notifications.ts +81 -0
  216. package/src/auth/new-signups-gate.ts +49 -0
  217. package/src/auth/organization-user-setup.ts +307 -0
  218. package/src/auth/signup-funnel-metrics.ts +164 -0
  219. package/src/browser-controller-authority.ts +1 -186
  220. package/src/browser-ephemeral.ts +39 -0
  221. package/src/browser-state-authority.ts +1 -236
  222. package/src/claude-subscription-account-conflicts.ts +12 -0
  223. package/src/claude-subscription-account-usage.ts +48 -0
  224. package/src/claude-subscription-oauth.ts +362 -0
  225. package/src/claude-subscription-profile.ts +73 -0
  226. package/src/claude-subscription-usage.ts +97 -0
  227. package/src/claude-workspace-connection.ts +82 -0
  228. package/src/codemode.ts +194 -30
  229. package/src/codex-realtime.ts +8 -2
  230. package/src/company-brain-okf.ts +340 -0
  231. package/src/connected-machine-computer-access.ts +33 -0
  232. package/src/connection-ownership.ts +199 -0
  233. package/src/controller-data-plane.ts +81 -0
  234. package/src/editable-artifact-native-kernel.ts +26 -15
  235. package/src/editable-artifact-office-import.ts +77 -9
  236. package/src/editable-artifact-production.ts +23 -7
  237. package/src/editable-artifact-source-session.ts +123 -0
  238. package/src/editable-artifact-websocket.ts +27 -21
  239. package/src/editable-artifact-workspace-files.ts +31 -6
  240. package/src/fatal-process-boundary.ts +246 -0
  241. package/src/gateway-realtime.ts +1 -1
  242. package/src/github-access.ts +147 -5
  243. package/src/http/acting-person.ts +71 -0
  244. package/src/http/api-contract-compat.ts +49 -0
  245. package/src/http/api-error.ts +115 -1
  246. package/src/http/auth.ts +48 -0
  247. package/src/http/cors.ts +67 -0
  248. package/src/http/deprecation.ts +118 -0
  249. package/src/http/interaction-control-error.ts +164 -0
  250. package/src/http/keyed-admission.ts +29 -0
  251. package/src/http/local-browser-boundary.ts +343 -0
  252. package/src/http/metrics-listener.ts +57 -0
  253. package/src/http/path-identifier.ts +54 -0
  254. package/src/http/registered-route-label.ts +34 -0
  255. package/src/http/request-body.ts +127 -0
  256. package/src/http/request-source.ts +178 -0
  257. package/src/http/sse.ts +486 -132
  258. package/src/http/unmatched-route.ts +60 -0
  259. package/src/http/user-content.ts +158 -0
  260. package/src/http/websocket-upgrade-dispatch.ts +29 -0
  261. package/src/index.ts +157 -52
  262. package/src/integration-endpoint-test.ts +291 -0
  263. package/src/integrations/api-integrations.ts +98 -11
  264. package/src/integrations/atlassian.ts +474 -136
  265. package/src/integrations/connect-authority.ts +2 -0
  266. package/src/integrations/connect-callback-return.ts +86 -0
  267. package/src/integrations/fiken.ts +354 -64
  268. package/src/integrations/github-app-connect.ts +435 -0
  269. package/src/integrations/github-installation-proof.ts +60 -0
  270. package/src/integrations/github-lens-connect.ts +76 -0
  271. package/src/integrations/github-skill-source.ts +2 -142
  272. package/src/integrations/google-drive.ts +820 -222
  273. package/src/integrations/integration-commit-authority.ts +69 -0
  274. package/src/integrations/marketplace-plugin.ts +48 -0
  275. package/src/integrations/oauth-client.ts +1356 -459
  276. package/src/integrations/oauth-profiles.ts +537 -0
  277. package/src/integrations/oauth-return-path.ts +18 -0
  278. package/src/integrations/personal-github-repositories.ts +674 -0
  279. package/src/integrations/personal-github.ts +825 -0
  280. package/src/integrations/pr-review-provider.ts +246 -0
  281. package/src/integrations/provider-oauth.ts +250 -22
  282. package/src/integrations/slack-app-home.ts +300 -0
  283. package/src/integrations/slack-bot.ts +963 -239
  284. package/src/integrations/slack-file-upload-flow.ts +147 -0
  285. package/src/integrations/slack-install.ts +83 -0
  286. package/src/integrations/slack-interactions.ts +3170 -474
  287. package/src/integrations/slack-mrkdwn.ts +193 -0
  288. package/src/integrations/slack-routing.ts +337 -0
  289. package/src/integrations/slack-session-defaults.ts +224 -0
  290. package/src/integrations/slack-task-file-upload.ts +323 -0
  291. package/src/integrations/social-oauth.ts +213 -15
  292. package/src/interaction-agent-access.ts +48 -0
  293. package/src/interaction-frame-proxy.ts +409 -0
  294. package/src/interaction-metrics.ts +30 -0
  295. package/src/mcp/action-catalog-types.ts +9 -0
  296. package/src/mcp/action-catalog.gen.ts +6696 -0
  297. package/src/mcp/capability-account-readiness.ts +17 -0
  298. package/src/mcp/child-read-evidence.ts +19 -0
  299. package/src/mcp/command-read.ts +109 -0
  300. package/src/mcp/company-profile-agent-admin.ts +198 -0
  301. package/src/mcp/contract-input.ts +141 -0
  302. package/src/mcp/documents.ts +65 -377
  303. package/src/mcp/editable-artifacts.ts +12 -6
  304. package/src/mcp/files.ts +80 -38
  305. package/src/mcp/first-party-tool-permissions.ts +305 -0
  306. package/src/mcp/knowledge-entries.ts +322 -0
  307. package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
  308. package/src/mcp/receipts.ts +34 -0
  309. package/src/mcp/request-abort.ts +44 -0
  310. package/src/mcp/scheduled-task-input.ts +76 -0
  311. package/src/mcp/scheduled-task-view.ts +44 -3
  312. package/src/mcp/server.ts +3030 -1267
  313. package/src/mcp/session-event-view.ts +383 -0
  314. package/src/mcp/session-view.ts +84 -3
  315. package/src/mcp/session-wait.ts +665 -0
  316. package/src/mcp-oauth.ts +1123 -0
  317. package/src/model-catalog.ts +91 -333
  318. package/src/observability.ts +15 -1
  319. package/src/organization-mcp.ts +397 -0
  320. package/src/organization-recovery-notifications.ts +103 -0
  321. package/src/routes/analytics-consent.ts +123 -0
  322. package/src/routes/api-integrations.ts +364 -74
  323. package/src/routes/api-keys.ts +336 -10
  324. package/src/routes/artifact-catalog.ts +279 -0
  325. package/src/routes/automations.ts +768 -0
  326. package/src/routes/billing.ts +461 -46
  327. package/src/routes/browser-sessions.ts +839 -131
  328. package/src/routes/canonical-human-identities.ts +90 -14
  329. package/src/routes/capabilities.ts +107 -6
  330. package/src/routes/channels.ts +20 -3
  331. package/src/routes/claude-subscription-accounts.ts +334 -0
  332. package/src/routes/claude-subscription-oauth.ts +142 -0
  333. package/src/routes/client-errors.ts +362 -0
  334. package/src/routes/codex.ts +833 -116
  335. package/src/routes/company-brain.ts +322 -0
  336. package/src/routes/company-profile.ts +65 -1
  337. package/src/routes/computer-sessions.ts +557 -106
  338. package/src/routes/connect.ts +1938 -0
  339. package/src/routes/connections.ts +1040 -349
  340. package/src/routes/documents.ts +488 -287
  341. package/src/routes/editable-artifacts.ts +79 -9
  342. package/src/routes/enrollments.ts +135 -39
  343. package/src/routes/environments.ts +154 -58
  344. package/src/routes/external-identity-links.ts +201 -0
  345. package/src/routes/feedback.ts +113 -0
  346. package/src/routes/files.ts +337 -39
  347. package/src/routes/github-browser-pages.ts +162 -0
  348. package/src/routes/github.ts +446 -144
  349. package/src/routes/insights-response-cache.ts +83 -0
  350. package/src/routes/insights-usage.ts +345 -0
  351. package/src/routes/insights.ts +153 -19
  352. package/src/routes/install.ts +133 -38
  353. package/src/routes/integration-facets.ts +107 -33
  354. package/src/routes/interaction-resources.ts +30 -3
  355. package/src/routes/knowledge.ts +488 -0
  356. package/src/routes/machines.ts +313 -15
  357. package/src/routes/managed-auth-session-sets.ts +1218 -0
  358. package/src/routes/managed-onboarding.ts +337 -0
  359. package/src/routes/managed-sign-in-methods.ts +433 -0
  360. package/src/routes/memory-slack-publications.ts +3 -2
  361. package/src/routes/model-connection-access.ts +208 -0
  362. package/src/routes/organization-integration-policy.ts +116 -0
  363. package/src/routes/organization-integrations.ts +326 -0
  364. package/src/routes/organization-mcp-connections.ts +178 -0
  365. package/src/routes/organization-memberships.ts +1019 -27
  366. package/src/routes/organization-model-providers.ts +293 -0
  367. package/src/routes/organization-recovery.ts +325 -0
  368. package/src/routes/organization-service-accounts.ts +128 -0
  369. package/src/routes/organization-sessions.ts +350 -0
  370. package/src/routes/personal-github-git-broker.ts +785 -0
  371. package/src/routes/personal-github.ts +370 -0
  372. package/src/routes/plugins.ts +251 -22
  373. package/src/routes/pr-review-github.ts +881 -0
  374. package/src/routes/pr-review.ts +622 -0
  375. package/src/routes/preference-registry.ts +12 -93
  376. package/src/routes/rigs.ts +167 -50
  377. package/src/routes/scheduled-tasks.ts +346 -41
  378. package/src/routes/session-artifact-associations.ts +87 -0
  379. package/src/routes/session-history-imports.ts +280 -0
  380. package/src/routes/sessions.ts +2306 -264
  381. package/src/routes/skill-content.ts +297 -0
  382. package/src/routes/skill-install-authority.ts +64 -0
  383. package/src/routes/skills.ts +62 -9
  384. package/src/routes/social.ts +46 -18
  385. package/src/routes/subscription-account-pools.ts +436 -0
  386. package/src/routes/subscription-pool-access.ts +126 -0
  387. package/src/routes/supergrok.ts +219 -303
  388. package/src/routes/transcription-recordings.ts +38 -3
  389. package/src/routes/transcriptions.ts +4 -0
  390. package/src/routes/usage-allowances.ts +378 -0
  391. package/src/routes/user-resource-authorities.ts +138 -0
  392. package/src/routes/video-generation.ts +14 -10
  393. package/src/routes/workspace-artifacts.ts +275 -69
  394. package/src/routes/workspace-capture.ts +21 -0
  395. package/src/routes/workspace-integrations.ts +576 -0
  396. package/src/routes/workspace-learning.ts +159 -0
  397. package/src/routes/workspace-model-providers.ts +148 -0
  398. package/src/routes/workspace-state.ts +38 -25
  399. package/src/routes/workspaces.ts +1004 -127
  400. package/src/sandbox/auth-callout.ts +96 -19
  401. package/src/sandbox/channel-a.ts +323 -24
  402. package/src/sandbox/connection-authority.ts +3 -0
  403. package/src/sandbox/enrollment.ts +81 -6
  404. package/src/sandbox/machines.ts +218 -76
  405. package/src/sandbox/metrics-ingestion.ts +411 -62
  406. package/src/sandbox/rematerialize.ts +121 -19
  407. package/src/sandbox/session-attach-variable-sets.ts +131 -0
  408. package/src/sandbox/viewer.ts +280 -25
  409. package/src/sandbox-file-artifacts.ts +413 -0
  410. package/src/scheduled-task-deletion.ts +138 -0
  411. package/src/session-create-observability.ts +46 -0
  412. package/src/session-realtime-context.ts +6 -3
  413. package/src/site-session-origin.ts +48 -0
  414. package/src/site-uploads.ts +224 -0
  415. package/src/slack-reaction-files.ts +16 -5
  416. package/src/temporal-schedule-cleanup.ts +13 -0
  417. package/src/temporal-schedule-sync.ts +74 -0
  418. package/src/transcription/providers/codex-subscription.ts +2 -0
  419. package/src/transcription/providers/openai.ts +1 -0
  420. package/src/transcription/providers/xai-subscription.ts +49 -3
  421. package/src/transcription/segmenter.ts +8 -2
  422. package/src/transcription/service.ts +43 -6
  423. package/src/work-discovery-observability.ts +121 -0
  424. package/src/workspace-artifact-content.ts +144 -0
  425. package/src/workspace-artifact-provenance.ts +104 -0
  426. package/src/workspace-delete-observability.ts +75 -0
  427. package/src/workspace-deletion.ts +73 -0
  428. package/src/workspace-state-projection.ts +18 -148
  429. package/src/workspace-tool-gateway-observability.ts +100 -0
  430. package/src/workspace-tool-gateway.ts +870 -0
  431. package/src/workspace-webhook-dispatch.ts +190 -0
  432. package/src/xai-subscription-auth.ts +9 -2
  433. package/dist/chunk-2PQMSRCB.js +0 -49759
  434. package/dist/chunk-2PQMSRCB.js.map +0 -1
  435. package/dist/routes/packs.d.ts +0 -3
  436. package/src/routes/packs.ts +0 -729
@@ -1,4 +1,36 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, createHmac } from "node:crypto";
2
+ import { verifyDirectModelAccess } from "@opengeni/core";
3
+ import {
4
+ assertClaudeWorkspaceCredential,
5
+ prepareClaudeWorkspaceCredential,
6
+ } from "../claude-workspace-connection";
7
+ import {
8
+ createConnectionIdempotently,
9
+ upsertWorkspaceProviderApiKeyConnection,
10
+ rotateWorkspaceProviderApiKeyConnection,
11
+ revokeWorkspaceProviderApiKeyConnections,
12
+ workspaceProviderApiKeyConnectionSpec,
13
+ type WorkspaceProviderApiKeyConnectionKind,
14
+ getConnectionCreationResult,
15
+ ConnectionCreateIdempotencyError,
16
+ } from "@opengeni/db";
17
+ import { sql } from "drizzle-orm";
18
+ import { assertOrganizationIntegrationAllowed } from "@opengeni/contracts";
19
+ import {
20
+ withOrganizationIntegrationAcquisition,
21
+ withOrganizationIntegrationPolicyFence,
22
+ } from "@opengeni/db/organization-integration-policy";
23
+ import { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
24
+ import {
25
+ startSlackBotInstall,
26
+ requireOpenGeniSlackOAuthSettings,
27
+ } from "../integrations/slack-install";
28
+ import {
29
+ WORKSPACE_OPENROUTER_CONNECTION_DOMAIN,
30
+ WORKSPACE_OPENROUTER_CONNECTION_ROLE,
31
+ VERCEL_AI_GATEWAY_CONNECTION_DOMAIN,
32
+ VERCEL_AI_GATEWAY_CONNECTION_ROLE,
33
+ } from "@opengeni/config";
2
34
  import {
3
35
  ATLASSIAN_PROVIDER_DOMAIN,
4
36
  AtlassianConnectionMetadata,
@@ -10,22 +42,24 @@ import {
10
42
  import {
11
43
  API_INTEGRATION_OAUTH_CREDENTIAL_ROLE,
12
44
  ApiIntegrationOAuthStartRequest,
45
+ DirectModelProviderMetadata,
13
46
  ConnectionResponse,
14
47
  CreateConnectionRequest,
15
- FIKEN_CREDENTIAL_LABEL,
16
- FIKEN_CREDENTIAL_ROLE,
17
- FIKEN_PROVIDER_DOMAIN,
18
48
  FikenInstallRequest,
19
49
  FikenOAuthStartRequest,
20
50
  FikenOAuthStartResponse,
21
51
  IntegrationClientMetadata,
22
52
  ListSlackInstallationBindingsResponse,
23
53
  ListConnectionsResponse,
54
+ OPENROUTER_CREDENTIAL_OPERATION_DIGEST_METADATA_KEY,
55
+ OPENROUTER_CREDENTIAL_OPERATION_ID_METADATA_KEY,
24
56
  OpenGeniSlackBotInstallRequest,
25
- OpenGeniSlackBotInstallStart,
26
57
  OAuthStartRequest,
27
58
  OAuthStartResponse,
28
59
  UpdateConnectionRequest,
60
+ VERCEL_AI_GATEWAY_CREDENTIAL_OPERATION_DIGEST_METADATA_KEY,
61
+ VERCEL_AI_GATEWAY_CREDENTIAL_OPERATION_ID_METADATA_KEY,
62
+ stableJson,
29
63
  } from "@opengeni/contracts";
30
64
  import {
31
65
  bindConnectorDocumentDestination,
@@ -40,24 +74,34 @@ import {
40
74
  GoogleDriveOAuthStartResponse,
41
75
  } from "@opengeni/contracts/google-drive";
42
76
  import {
43
- fikenConnectionMetadata,
77
+ PERSONAL_GITHUB_PROVIDER_DOMAIN,
78
+ PersonalGitHubDisconnectRequest,
79
+ hasReservedPersonalGitHubMetadata,
80
+ isPersonalGitHubConnection,
81
+ } from "@opengeni/contracts/personal-github";
82
+ import {
44
83
  hasPermission,
84
+ listOwnConnectionAccountsForGrant,
45
85
  hasReservedFikenMetadata,
46
86
  hasReservedOpenGeniSlackBotMetadata,
47
- isFikenConnection,
48
87
  isOpenGeniSlackBotConnection,
49
88
  openGeniSlackBotMetadata,
50
89
  requireAccessGrant,
51
90
  requireAccessGrantAuthorization,
52
91
  requireEnvironmentEncryption,
53
- resolveFikenDefaultCompanySlug,
92
+ externalContinuationCommitAuthorizer,
54
93
  } from "@opengeni/core";
55
94
  import {
56
95
  consumeIntegrationOAuthStateNonce,
57
- createConnection,
96
+ claimConnectOperation,
97
+ finishConnectOperation,
98
+ getConnectAttempt,
99
+ decryptEnvironmentValue,
100
+ withWorkspaceSubjectRls,
101
+ type Database,
58
102
  encryptEnvironmentValue,
103
+ brokeredCredentialBundleProblem,
59
104
  getConnectionMetadata,
60
- getWorkspaceGrant,
61
105
  listConnectionsMetadata,
62
106
  listSlackInstallationBindings,
63
107
  persistSlackBotInstallationWithSuccessAudit,
@@ -84,8 +128,10 @@ import {
84
128
  } from "../integrations/google-drive";
85
129
  import {
86
130
  completeApiIntegrationProviderOAuth,
131
+ isApiIntegrationProviderOAuthState,
87
132
  startApiIntegrationProviderOAuth,
88
133
  } from "../integrations/provider-oauth";
134
+ import { disconnectPersonalGitHub } from "../integrations/personal-github";
89
135
  import {
90
136
  browseAtlassianSources,
91
137
  completeAtlassianOAuthCallback,
@@ -97,9 +143,17 @@ import {
97
143
  import {
98
144
  completeMcpOAuthCallback,
99
145
  integrationBaseUrl,
146
+ oauthStateFailureReturn,
100
147
  startMcpOAuth,
101
148
  } from "../integrations/oauth-client";
149
+ import {
150
+ assertPersonalConnectionOwnerPrincipal,
151
+ isPersonalConnectionOwnerPrincipal,
152
+ requireLegacyOAuthActor,
153
+ requireProviderConsentInBrowser,
154
+ } from "../connection-ownership";
102
155
  import { canonicalProviderDomain } from "../integrations/provider-domain";
156
+ import { externalActorContinuationForAuthorization } from "@opengeni/core";
103
157
  import {
104
158
  exchangeOpenGeniSlackAuthorizationCode,
105
159
  SlackBotCredentialVerificationError,
@@ -107,19 +161,21 @@ import {
107
161
  } from "../integrations/slack-bot";
108
162
  import {
109
163
  completeFikenOAuthCallback,
110
- fikenCredentialBundle,
111
164
  startFikenOAuth,
112
- verifyFikenApiToken,
165
+ prepareFikenTokenInstall,
113
166
  } from "../integrations/fiken";
167
+ import { requireConnectOwnerAuthority } from "../integrations/connect-authority";
114
168
  import {
115
169
  OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
116
170
  OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
117
- OPENGENI_SLACK_BOT_REQUESTED_SCOPES,
118
171
  } from "@opengeni/contracts";
119
- import { createSignedState, readSignedState } from "@opengeni/github";
172
+ import { readSignedState } from "@opengeni/github";
120
173
  import { oauthStateTtlMs, requireIntegrationsStateSecret } from "../integrations/oauth-client";
174
+ import { parseRequestJson } from "../http/request-body";
121
175
 
122
176
  type OpenGeniSlackInstallState = {
177
+ connectAttemptId?: string;
178
+ externalContinuation?: ExternalActorContinuation;
123
179
  accountId: string;
124
180
  workspaceId: string;
125
181
  subjectId: string;
@@ -149,6 +205,26 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
149
205
  );
150
206
  });
151
207
 
208
+ app.get("/v1/workspaces/:workspaceId/connections/accounts", async (c) => {
209
+ const workspaceId = c.req.param("workspaceId");
210
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
211
+ const includeInactive = c.req.query("includeInactive");
212
+ if (
213
+ includeInactive !== undefined &&
214
+ includeInactive !== "true" &&
215
+ includeInactive !== "false"
216
+ ) {
217
+ throw new HTTPException(400, { message: "includeInactive must be true or false" });
218
+ }
219
+ return c.json(
220
+ ListConnectionsResponse.parse({
221
+ connections: await listOwnConnectionAccountsForGrant(db, grant, {
222
+ includeInactive: includeInactive === "true",
223
+ }),
224
+ }),
225
+ );
226
+ });
227
+
152
228
  app.get("/v1/workspaces/:workspaceId/connections/slack-bot/bindings", async (c) => {
153
229
  const workspaceId = c.req.param("workspaceId");
154
230
  const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
@@ -162,79 +238,281 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
162
238
  );
163
239
  });
164
240
 
165
- app.post("/v1/workspaces/:workspaceId/connections", async (c) => {
241
+ app.get("/v1/workspaces/:workspaceId/connections/operations/:operationId", async (c) => {
166
242
  const workspaceId = c.req.param("workspaceId");
167
- const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
168
- const payload = CreateConnectionRequest.parse(await c.req.json());
169
- assertNotReservedSlackBotMetadata(payload.metadata);
170
- assertNotReservedFikenMetadata(payload.metadata);
171
- assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
172
- const key = requireEnvironmentEncryption(settings);
173
- const subjectId = createConnectionSubjectId(payload, grant.subjectId);
174
- const providerDomain = canonicalProviderDomain(payload.providerDomain);
175
- assertNotDirectPersonalSlackOAuth(providerDomain, payload.kind);
176
- assertNotDirectGoogleDriveOAuth(providerDomain, payload.kind, payload.metadata);
177
- assertNotDirectAtlassianOAuth(providerDomain, payload.kind, payload.metadata);
178
- const connection = await createConnection(db, {
243
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
244
+ const operationId = CreateConnectionRequest.shape.operationId
245
+ .unwrap()
246
+ .parse(c.req.param("operationId"));
247
+ const connection = await getConnectionCreationResult(db, {
179
248
  accountId: grant.accountId,
180
249
  workspaceId,
181
- subjectId,
182
- providerDomain,
183
- kind: payload.kind,
184
- credentialEncrypted: encryptCredentialBundle(key, payload.credential),
185
- grantedScopes: payload.grantedScopes,
186
- expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null,
187
- metadata: payload.metadata,
188
- createdBySubjectId: grant.subjectId,
250
+ subjectId: grant.subjectId,
251
+ operationId,
189
252
  });
190
- return c.json(ConnectionResponse.parse({ connection }), 201);
253
+ if (!connection) throw new HTTPException(404, { message: "Connection operation not found" });
254
+ return c.json(ConnectionResponse.parse({ connection }));
191
255
  });
192
256
 
193
- app.post("/v1/workspaces/:workspaceId/connections/slack-bot/install", async (c) => {
257
+ app.post("/v1/workspaces/:workspaceId/connections", async (c) => {
194
258
  const workspaceId = c.req.param("workspaceId");
195
- const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
196
- const payload = OpenGeniSlackBotInstallRequest.parse(await c.req.json());
197
- const slack = requireOpenGeniSlackOAuthSettings(settings);
198
- const existing = payload.connectionId
199
- ? await getConnectionMetadata(db, workspaceId, payload.connectionId, grant.subjectId)
200
- : null;
201
- if (payload.connectionId && !existing) {
202
- throw new HTTPException(404, { message: "connection not found" });
203
- }
204
- if (existing && !isOpenGeniSlackBotConnection(existing)) {
259
+ const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
260
+ const grant = access.grant;
261
+ const beforeCommit = externalContinuationCommitAuthorizer(access);
262
+ const payload = await parseRequestJson(c, CreateConnectionRequest);
263
+ if (
264
+ payload.metadata?.directModelProvider !== undefined ||
265
+ String(payload.metadata?.credentialRole ?? "").startsWith("direct_")
266
+ ) {
267
+ const config = DirectModelProviderMetadata.safeParse(payload.metadata?.directModelProvider);
268
+ const domain = config.success
269
+ ? new URL(config.data.endpoint ?? "https://api.openai.com/v1").hostname
270
+ : null;
271
+ if (
272
+ !config.success ||
273
+ payload.subjectId !== null ||
274
+ payload.kind !== "api_key" ||
275
+ canonicalProviderDomain(payload.providerDomain) !== domain ||
276
+ payload.metadata?.credentialRole !== `direct_${config.data.provider}` ||
277
+ typeof payload.credential.apiKey !== "string" ||
278
+ !payload.credential.apiKey.trim() ||
279
+ !payload.operationId ||
280
+ payload.expiresAt
281
+ ) {
282
+ throw new HTTPException(422, {
283
+ message:
284
+ "Invalid OpenAI or Azure OpenAI connection: provide a key, model, and official provider endpoint",
285
+ });
286
+ }
287
+ payload.metadata = { ...payload.metadata, directModelProvider: config.data };
288
+ if (payload.verifyModelAccess) {
289
+ await verifyDirectModelAccess(
290
+ config.data,
291
+ payload.credential.apiKey as string,
292
+ deps.directModelFetch,
293
+ );
294
+ }
295
+ } else if (payload.verifyModelAccess) {
205
296
  throw new HTTPException(422, {
206
- message: "connectionId is not an OpenGeni Slack bot connection",
297
+ message: "Model checks require an OpenAI or Azure OpenAI connection",
207
298
  });
208
299
  }
209
- const baseUrl = integrationBaseUrl(settings.publicBaseUrl, c.req.url);
210
- const redirectUri = `${baseUrl}/v1/integrations/slack/callback`;
211
- const returnPath = `/workspaces/${workspaceId}/capabilities`;
212
- const state = createSignedState(requireIntegrationsStateSecret(settings), {
213
- accountId: grant.accountId,
214
- workspaceId,
215
- subjectId: grant.subjectId,
216
- returnPath,
217
- ...(existing ? { connectionId: existing.id, connectionVersion: existing.version } : {}),
300
+ const directModelKey = payload.metadata?.directModelProvider !== undefined;
301
+ // All writes in this closure must use the caller-owned scoped transaction.
302
+ // eslint-disable-next-line no-shadow
303
+ const persist = async (db: Database) => {
304
+ assertNotReservedSlackBotMetadata(payload.metadata);
305
+ assertNotReservedFikenMetadata(payload.metadata);
306
+ assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
307
+ assertNotReservedPersonalGitHubMetadata(payload.metadata);
308
+ const key = requireEnvironmentEncryption(settings);
309
+ const subjectId = createConnectionSubjectId(payload, grant.subjectId);
310
+ if (subjectId !== null) {
311
+ assertPersonalConnectionOwnerPrincipal(access);
312
+ }
313
+ const providerDomain = canonicalProviderDomain(payload.providerDomain);
314
+ payload.credential = prepareClaudeWorkspaceCredential(
315
+ settings,
316
+ workspaceId,
317
+ payload.metadata,
318
+ payload.credential,
319
+ );
320
+ assertClaudeWorkspaceCredential(settings, {
321
+ subjectId,
322
+ providerDomain,
323
+ kind: payload.kind,
324
+ metadata: payload.metadata,
325
+ credential: payload.credential,
326
+ });
327
+ assertNotDirectPersonalSlackOAuth(providerDomain, payload.kind);
328
+ assertNotDirectGoogleDriveOAuth(providerDomain, payload.kind, payload.metadata);
329
+ assertNotDirectAtlassianOAuth(providerDomain, payload.kind, payload.metadata);
330
+ assertNotDirectPersonalGitHubOAuth(providerDomain, payload.kind, payload.metadata);
331
+ const credentialEncrypted = encryptCredentialBundle(key, payload.credential);
332
+ const workspaceProviderKind = workspaceProviderApiKeyConnectionKind({
333
+ subjectId,
334
+ providerDomain,
335
+ kind: payload.kind,
336
+ metadata: payload.metadata,
337
+ });
338
+ // Validated model keys store `{ apiKey }` for model execution. Generic
339
+ // integration credentials still require an explicit broker destination.
340
+ if (!workspaceProviderKind && !directModelKey)
341
+ assertBrokeredApiKeyCredential(payload.kind, payload.credential);
342
+ const connection = workspaceProviderKind
343
+ ? await (async () => {
344
+ const provider = workspaceProviderApiKeyConnectionSpec(workspaceProviderKind);
345
+ if (!payload.operationId) {
346
+ throw new HTTPException(400, {
347
+ message: `connecting ${provider.label} requires an operationId`,
348
+ });
349
+ }
350
+ const expiresAt = payload.expiresAt ? new Date(payload.expiresAt) : null;
351
+ const metadata = workspaceProviderCredentialMetadata(
352
+ workspaceProviderKind,
353
+ payload.metadata,
354
+ );
355
+ const input = {
356
+ accountId: grant.accountId,
357
+ workspaceId,
358
+ operationId: payload.operationId,
359
+ requestDigest: workspaceProviderCredentialRequestDigest(key, {
360
+ action: "create",
361
+ providerDomain,
362
+ kind: payload.kind,
363
+ subjectId,
364
+ credential: payload.credential,
365
+ grantedScopes: payload.grantedScopes,
366
+ expiresAt: expiresAt?.toISOString() ?? null,
367
+ metadata,
368
+ }),
369
+ credentialEncrypted,
370
+ grantedScopes: payload.grantedScopes,
371
+ expiresAt,
372
+ metadata,
373
+ updatedBySubjectId: grant.subjectId,
374
+ };
375
+ const created = await upsertWorkspaceProviderApiKeyConnection(
376
+ db,
377
+ workspaceProviderKind,
378
+ input,
379
+ );
380
+ if (!created || created.status === "revoked") {
381
+ throw new HTTPException(409, {
382
+ message: `${provider.label} is already connected; reload before replacing its key`,
383
+ });
384
+ }
385
+ return created;
386
+ })()
387
+ : await createConnectionIdempotently(db, {
388
+ accountId: grant.accountId,
389
+ workspaceId,
390
+ subjectId,
391
+ providerDomain,
392
+ kind: payload.kind,
393
+ credentialEncrypted,
394
+ grantedScopes: payload.grantedScopes,
395
+ expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null,
396
+ metadata: payload.metadata,
397
+ createdBySubjectId: grant.subjectId,
398
+ ...(payload.operationId
399
+ ? {
400
+ operation: {
401
+ id: payload.operationId,
402
+ requestDigest: workspaceProviderCredentialRequestDigest(key, {
403
+ action: "create",
404
+ providerDomain,
405
+ kind: payload.kind,
406
+ subjectId,
407
+ credential: payload.credential,
408
+ grantedScopes: payload.grantedScopes,
409
+ expiresAt: payload.expiresAt
410
+ ? new Date(payload.expiresAt).toISOString()
411
+ : null,
412
+ metadata: payload.metadata,
413
+ }),
414
+ },
415
+ }
416
+ : {}),
417
+ }).catch((error: unknown) => {
418
+ if (error instanceof ConnectionCreateIdempotencyError) {
419
+ throw new HTTPException(409, { message: error.message });
420
+ }
421
+ throw error;
422
+ });
423
+ return c.json(ConnectionResponse.parse({ connection }), 201);
424
+ };
425
+ const commit = (database: Database) =>
426
+ beforeCommit
427
+ ? withWorkspaceSubjectRls(database, workspaceId, grant.subjectId, async (tx) => {
428
+ await beforeCommit(tx);
429
+ return persist(tx);
430
+ })
431
+ : persist(database);
432
+ // Only the canonical workspace model-key lane is outside integration policy.
433
+ // Generic credentials have no validated protocol/curated target in this contract.
434
+ const modelKey = workspaceProviderApiKeyConnectionKind({
435
+ subjectId: createConnectionSubjectId(payload, grant.subjectId),
436
+ providerDomain: canonicalProviderDomain(payload.providerDomain),
437
+ kind: payload.kind,
438
+ metadata: payload.metadata,
218
439
  });
219
- const authorizationUrl = new URL("https://slack.com/oauth/v2/authorize");
220
- authorizationUrl.searchParams.set("client_id", slack.clientId);
221
- authorizationUrl.searchParams.set("scope", OPENGENI_SLACK_BOT_REQUESTED_SCOPES.join(","));
222
- authorizationUrl.searchParams.set("redirect_uri", redirectUri);
223
- authorizationUrl.searchParams.set("state", state);
440
+ return modelKey || directModelKey
441
+ ? commit(db)
442
+ : withOrganizationIntegrationAcquisition(
443
+ db,
444
+ { accountId: grant.accountId, workspaceId },
445
+ [null],
446
+ commit,
447
+ );
448
+ });
449
+
450
+ app.post("/v1/workspaces/:workspaceId/connections/slack-bot/install", async (c) => {
451
+ const workspaceId = c.req.param("workspaceId");
452
+ const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
453
+ requireLegacyOAuthActor(access);
454
+ const grant = access.grant;
455
+ const payload = await parseRequestJson(c, OpenGeniSlackBotInstallRequest);
224
456
  return c.json(
225
- OpenGeniSlackBotInstallStart.parse({
226
- authorizationUrl: authorizationUrl.toString(),
227
- expiresAt: new Date(Date.now() + oauthStateTtlMs).toISOString(),
228
- }),
457
+ await withOrganizationIntegrationAcquisition(
458
+ db,
459
+ { accountId: grant.accountId, workspaceId },
460
+ ["slack-bot"],
461
+ (tx) =>
462
+ startSlackBotInstall(
463
+ { ...deps, db: tx },
464
+ {
465
+ accountId: grant.accountId,
466
+ workspaceId,
467
+ subjectId: grant.subjectId,
468
+ requestUrl: c.req.url,
469
+ ...(payload.connectionId ? { connectionId: payload.connectionId } : {}),
470
+ },
471
+ ),
472
+ ),
229
473
  );
230
474
  });
231
475
 
232
476
  app.get("/v1/integrations/slack/callback", async (c) => {
233
477
  const baseUrl = integrationBaseUrl(settings.publicBaseUrl, c.req.url);
234
478
  let state: OpenGeniSlackInstallState | null = null;
479
+ let exactReturnUrl: string | undefined;
480
+ let operation: { attemptId: string; operationId: string; inputDigest: string } | undefined;
235
481
  let stage: SlackBotInstallCallbackFailureStage = "permission_check";
236
482
  try {
237
483
  state = readOpenGeniSlackInstallState(c.req.query("state"), settings);
484
+ if (state.connectAttemptId) {
485
+ const stored = await getConnectAttempt(db, state, state.connectAttemptId);
486
+ if (stored.attempt.providerId !== "slack-bot" || stored.attempt.ownership !== "workspace")
487
+ throw new HTTPException(403, { message: "Slack attempt mismatch" });
488
+ exactReturnUrl = stored.returnUrl;
489
+ operation = {
490
+ attemptId: state.connectAttemptId,
491
+ operationId: `oauth:${state.nonce}`,
492
+ inputDigest: createHash("sha256").update(c.req.query("state")!).digest("hex"),
493
+ };
494
+ const callbackState = state;
495
+ const callbackOperation = operation;
496
+ const claim = await withOrganizationIntegrationPolicyFence(
497
+ db,
498
+ callbackState,
499
+ (policyTx, policy) =>
500
+ claimConnectOperation(policyTx, callbackState, {
501
+ ...callbackOperation,
502
+ expectedRevision: stored.attempt.revision,
503
+ authorize: (tx, _attempt, origin) =>
504
+ requireConnectOwnerAuthority(tx, callbackState, "connections:write", origin),
505
+ authorizeAcquisition: async () => {
506
+ // Provider denial/cancellation acquires nothing; completed exact
507
+ // receipts replay before this callback is invoked.
508
+ if (!c.req.query("error") && c.req.query("code"))
509
+ assertOrganizationIntegrationAllowed(policy, "slack-bot");
510
+ },
511
+ }),
512
+ );
513
+ if (claim.status === "replayed")
514
+ return new Response(null, { status: 302, headers: { Location: exactReturnUrl } });
515
+ }
238
516
  await requireSlackInstallCallbackGrant(db, state);
239
517
  stage = "nonce_consume";
240
518
  const consumed = await consumeIntegrationOAuthStateNonce(db, {
@@ -252,6 +530,25 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
252
530
  "Slack installation state has already been used",
253
531
  );
254
532
  }
533
+ if (operation && (c.req.query("error") || !c.req.query("code"))) {
534
+ await finishConnectOperation(db, state, {
535
+ ...operation,
536
+ authorize: (tx, _attempt, origin) =>
537
+ requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
538
+ commit: async (_tx, current) => ({
539
+ ...current,
540
+ revision: current.revision + 1,
541
+ state: c.req.query("error") === "access_denied" ? "cancelled" : "failed",
542
+ nextAction: { type: "none" },
543
+ error: {
544
+ code: c.req.query("error") ? "provider_denied" : "missing_code",
545
+ message: "Authorization was not completed. Start a new connection attempt.",
546
+ retryable: false,
547
+ },
548
+ }),
549
+ });
550
+ return new Response(null, { status: 302, headers: { Location: exactReturnUrl! } });
551
+ }
255
552
  if (c.req.query("error")) {
256
553
  stage = "provider_denial";
257
554
  throw new SlackInstallCallbackError(
@@ -269,9 +566,10 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
269
566
  "Slack installation callback is missing code",
270
567
  );
271
568
  }
569
+ await withOrganizationIntegrationAcquisition(db, state, ["slack-bot"], async () => {});
272
570
  const slack = requireOpenGeniSlackOAuthSettings(settings);
273
571
  const redirectUri = `${baseUrl}/v1/integrations/slack/callback`;
274
- const token = await exchangeOpenGeniSlackAuthorizationCode(
572
+ const authorization = await exchangeOpenGeniSlackAuthorizationCode(
275
573
  {
276
574
  code,
277
575
  clientId: slack.clientId,
@@ -281,21 +579,72 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
281
579
  deps.slackFetch ?? fetch,
282
580
  );
283
581
  stage = "credential_verification";
284
- const verified = await verifyOpenGeniSlackBotCredential(token, deps.slackFetch ?? fetch);
582
+ const verified = await verifyOpenGeniSlackBotCredential(
583
+ authorization.accessToken,
584
+ {
585
+ appId: authorization.appId,
586
+ displayName: settings.slackBotDisplayName,
587
+ },
588
+ deps.slackFetch ?? fetch,
589
+ new Date(),
590
+ );
285
591
  stage = "permission_recheck";
286
592
  await requireSlackInstallCallbackGrant(db, state);
287
593
  stage = "persistence";
288
- const connection = await persistOpenGeniSlackBotConnection({
289
- deps,
290
- state,
291
- token,
292
- verified,
293
- });
594
+ const acceptedState = state;
595
+ const persist = (tx: Database) =>
596
+ persistOpenGeniSlackBotConnection({
597
+ deps: { ...deps, db: tx },
598
+ state: acceptedState,
599
+ token: authorization.accessToken,
600
+ verified,
601
+ });
602
+ if (operation) {
603
+ const acceptedOperation = operation;
604
+ await withOrganizationIntegrationPolicyFence(db, acceptedState, (policyTx, policy) =>
605
+ finishConnectOperation(policyTx, acceptedState, {
606
+ ...acceptedOperation,
607
+ authorize: (tx, _attempt, origin) =>
608
+ requireConnectOwnerAuthority(tx, acceptedState, "connections:write", origin),
609
+ commit: async (tx, current) => {
610
+ assertOrganizationIntegrationAllowed(policy, "slack-bot");
611
+ const connection = await persist(tx);
612
+ return {
613
+ ...current,
614
+ revision: current.revision + 1,
615
+ state: "complete",
616
+ credentialsCommitted: true,
617
+ nextAction: { type: "none" },
618
+ account: {
619
+ id: connection.id,
620
+ version: connection.version,
621
+ providerId: "slack-bot",
622
+ label: "Slack workspace bot",
623
+ ownership: "workspace",
624
+ status: "connected",
625
+ },
626
+ };
627
+ },
628
+ }),
629
+ );
630
+ return new Response(null, { status: 302, headers: { Location: exactReturnUrl! } });
631
+ }
632
+ const connection = await withOrganizationIntegrationAcquisition(
633
+ db,
634
+ acceptedState,
635
+ ["slack-bot"],
636
+ async (tx) => {
637
+ await requireSlackInstallCallbackGrant(tx, acceptedState);
638
+ return persist(tx);
639
+ },
640
+ );
294
641
  return c.redirect(
295
642
  slackInstallReturnUrl(baseUrl, state.returnPath, "connected", connection.id),
296
643
  302,
297
644
  );
298
645
  } catch (error) {
646
+ if (exactReturnUrl)
647
+ return new Response(null, { status: 302, headers: { Location: exactReturnUrl } });
299
648
  if (state) {
300
649
  const failure = slackInstallCallbackFailure(stage, error);
301
650
  try {
@@ -316,7 +665,12 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
316
665
  }
317
666
  const reason = slackInstallErrorReason(error);
318
667
  return c.redirect(
319
- slackInstallReturnUrl(baseUrl, state?.returnPath ?? "/integrations", "error", reason),
668
+ slackInstallReturnUrl(
669
+ baseUrl,
670
+ state?.returnPath ?? oauthStateFailureReturn(settings, c.req.query("state")).returnPath,
671
+ "error",
672
+ reason,
673
+ ),
320
674
  302,
321
675
  );
322
676
  }
@@ -328,80 +682,32 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
328
682
  app.post("/v1/workspaces/:workspaceId/connections/fiken/install", async (c) => {
329
683
  assertIntegrationsEnabled();
330
684
  const workspaceId = c.req.param("workspaceId");
331
- const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
332
- const payload = FikenInstallRequest.parse(await c.req.json());
333
- const key = requireEnvironmentEncryption(settings);
334
- const existing = payload.connectionId
335
- ? await getConnectionMetadata(db, workspaceId, payload.connectionId, null)
336
- : null;
337
- if (payload.connectionId && !existing) {
338
- throw new HTTPException(404, { message: "connection not found" });
339
- }
340
- if (existing && !isFikenConnection(existing)) {
341
- throw new HTTPException(422, { message: "connectionId is not a Fiken connection" });
342
- }
343
- const verified = await verifyFikenApiToken(payload.apiToken, deps.fikenFetch ?? fetch);
344
- const previousDefault = existing
345
- ? (fikenConnectionMetadata(existing.metadata)?.defaultCompanySlug ?? null)
346
- : null;
347
- const defaultCompanySlug = resolveFikenDefaultCompanySlug({
348
- requested: payload.defaultCompanySlug ?? null,
349
- previous: previousDefault,
350
- companies: verified.companies,
351
- });
352
- if (payload.defaultCompanySlug && defaultCompanySlug !== payload.defaultCompanySlug) {
353
- throw new HTTPException(422, {
354
- message: `defaultCompanySlug is not among the companies this token can access: ${verified.companies
355
- .map((company) => company.slug)
356
- .join(", ")}`,
357
- });
358
- }
359
- const metadata = {
360
- credentialRole: FIKEN_CREDENTIAL_ROLE,
361
- credentialLabel: FIKEN_CREDENTIAL_LABEL,
362
- companies: verified.companies,
363
- defaultCompanySlug,
364
- verifiedAt: new Date().toISOString(),
365
- };
366
- const credentialEncrypted = encryptCredentialBundle(
367
- key,
368
- fikenCredentialBundle(payload.apiToken),
369
- );
370
- if (existing) {
371
- // Rewrites the whole credential identity: a token pasted over an OAuth
372
- // row must also flip kind and clear the OAuth expiry, or the broker
373
- // keeps treating the api_key bundle as a refreshable oauth2 credential.
374
- const updated = await updateConnection(db, {
375
- workspaceId,
376
- connectionId: existing.id,
377
- visibleToSubjectId: null,
378
- expectedVersion: existing.version,
379
- kind: "api_key",
380
- status: "active",
381
- credentialEncrypted,
382
- grantedScopes: [],
383
- expiresAt: null,
384
- metadata,
385
- updatedBySubjectId: grant.subjectId,
386
- });
387
- if (!updated) {
388
- throw new HTTPException(409, { message: "the Fiken connection changed; retry" });
389
- }
390
- return c.json(ConnectionResponse.parse({ connection: updated }));
391
- }
392
- const connection = await createConnection(db, {
393
- accountId: grant.accountId,
685
+ const authorization = await requireAccessGrantAuthorization(
686
+ c,
687
+ deps,
394
688
  workspaceId,
395
- subjectId: null,
396
- providerDomain: FIKEN_PROVIDER_DOMAIN,
397
- kind: "api_key",
398
- credentialEncrypted,
399
- grantedScopes: [],
400
- expiresAt: null,
401
- metadata,
402
- createdBySubjectId: grant.subjectId,
403
- });
404
- return c.json(ConnectionResponse.parse({ connection }), 201);
689
+ "connections:write",
690
+ );
691
+ const grant = authorization.grant;
692
+ const payload = await parseRequestJson(c, FikenInstallRequest);
693
+ const persist = await prepareFikenTokenInstall(deps, grant, payload);
694
+ const continuation = externalActorContinuationForAuthorization(authorization);
695
+ const connection = await withOrganizationIntegrationAcquisition(
696
+ db,
697
+ { accountId: grant.accountId, workspaceId },
698
+ ["fiken"],
699
+ async (tx) => {
700
+ await requireConnectOwnerAuthority(tx, {
701
+ accountId: grant.accountId,
702
+ workspaceId,
703
+ subjectId: grant.subjectId,
704
+ personalOwnerVerified: isPersonalConnectionOwnerPrincipal(authorization),
705
+ ...(continuation ? { externalContinuation: continuation } : {}),
706
+ });
707
+ return persist(tx);
708
+ },
709
+ );
710
+ return c.json(ConnectionResponse.parse({ connection }), payload.connectionId ? 200 : 201);
405
711
  });
406
712
 
407
713
  // Fiken OAuth (registered app) start. Both Fiken lanes produce the same
@@ -409,8 +715,10 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
409
715
  app.post("/v1/workspaces/:workspaceId/connections/fiken/oauth/start", async (c) => {
410
716
  assertIntegrationsEnabled();
411
717
  const workspaceId = c.req.param("workspaceId");
412
- const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
413
- const payload = FikenOAuthStartRequest.parse(await c.req.json());
718
+ const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
719
+ requireLegacyOAuthActor(access);
720
+ const grant = access.grant;
721
+ const payload = await parseRequestJson(c, FikenOAuthStartRequest);
414
722
  requireEnvironmentEncryption(settings);
415
723
  return c.json(
416
724
  FikenOAuthStartResponse.parse(
@@ -433,13 +741,20 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
433
741
  ...(c.req.query("error") ? { error: c.req.query("error") } : {}),
434
742
  requestUrl: c.req.url,
435
743
  });
744
+ if (result.exactReturn)
745
+ return new Response(null, { status: 302, headers: { Location: result.redirectTo } });
436
746
  return c.redirect(result.redirectTo, 302);
437
747
  });
438
748
 
439
749
  app.post("/v1/workspaces/:workspaceId/connections/google-drive/install", async (c) => {
440
750
  assertIntegrationsEnabled();
441
751
  const workspaceId = c.req.param("workspaceId");
442
- const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
752
+ // The Drive connector writes only a personal Connection, so only a managed
753
+ // human may start it.
754
+ const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
755
+ assertPersonalConnectionOwnerPrincipal(access, "Google Drive");
756
+ requireLegacyOAuthActor(access);
757
+ const grant = access.grant;
443
758
  const parsed = GoogleDriveOAuthStartRequest.safeParse(await c.req.json());
444
759
  if (!parsed.success) {
445
760
  throw new HTTPException(400, { message: "invalid Google Drive install request" });
@@ -460,7 +775,12 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
460
775
  app.post("/v1/workspaces/:workspaceId/connections/atlassian/install", async (c) => {
461
776
  assertIntegrationsEnabled();
462
777
  const workspaceId = c.req.param("workspaceId");
463
- const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
778
+ // The Atlassian connector writes only a personal Connection, so only a
779
+ // managed human may start it.
780
+ const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
781
+ assertPersonalConnectionOwnerPrincipal(access, "Atlassian");
782
+ requireLegacyOAuthActor(access);
783
+ const grant = access.grant;
464
784
  const parsed = AtlassianOAuthStartRequest.safeParse(await c.req.json());
465
785
  if (!parsed.success) {
466
786
  throw new HTTPException(400, { message: "invalid Atlassian install request" });
@@ -486,6 +806,8 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
486
806
  ...(c.req.query("error") ? { error: c.req.query("error") } : {}),
487
807
  requestUrl: c.req.url,
488
808
  });
809
+ if (result.exactReturn)
810
+ return new Response(null, { status: 302, headers: { Location: result.redirectTo } });
489
811
  return c.redirect(result.redirectTo, 302);
490
812
  });
491
813
 
@@ -557,9 +879,12 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
557
879
  ...(c.req.query("code") ? { code: c.req.query("code") } : {}),
558
880
  ...(c.req.query("state") ? { state: c.req.query("state") } : {}),
559
881
  ...(c.req.query("error") ? { error: c.req.query("error") } : {}),
882
+ ...(c.req.query("picked_file_ids") ? { pickedFileIds: c.req.query("picked_file_ids") } : {}),
560
883
  requestUrl: c.req.url,
561
884
  };
562
885
  const result = await completeGoogleDriveOAuthCallback(deps, input);
886
+ if (result.exactReturn)
887
+ return new Response(null, { status: 302, headers: { Location: result.redirectTo } });
563
888
  return c.redirect(result.redirectTo, 302);
564
889
  });
565
890
 
@@ -658,150 +983,351 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
658
983
  "connections:write",
659
984
  );
660
985
  const { grant } = authorization;
661
- const payload = UpdateConnectionRequest.parse(await c.req.json());
662
- assertNotReservedSlackBotMetadata(payload.metadata);
663
- assertNotReservedFikenMetadata(payload.metadata);
664
- assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
665
- const existing = await getConnectionMetadata(
986
+ const payload = await parseRequestJson(c, UpdateConnectionRequest);
987
+ // Policy first, then the existing tenancy prefix and exact credential row.
988
+ // The locked live identity makes unchanged/reducing updates safe against a
989
+ // concurrent replacement; all final writes below use this same transaction.
990
+ return withOrganizationIntegrationPolicyFence(
666
991
  db,
667
- workspaceId,
668
- c.req.param("connectionId"),
669
- grant.subjectId,
670
- );
671
- if (existing && isOpenGeniSlackBotConnection(existing)) {
672
- const destination = ConnectorDocumentDestinationSelection.safeParse(
673
- payload.metadata?.documentDestination,
674
- );
675
- const destinationOnlyUpdate =
676
- payload.metadata !== undefined &&
677
- Object.keys(payload.metadata).length === 1 &&
678
- payload.providerDomain === undefined &&
679
- payload.subjectId === undefined &&
680
- payload.kind === undefined &&
681
- payload.status === undefined &&
682
- payload.credential === undefined &&
683
- payload.grantedScopes === undefined &&
684
- payload.expiresAt === undefined;
685
- if (!destination.success || !destinationOnlyUpdate) {
686
- throw new HTTPException(422, {
687
- message: "use the dedicated OpenGeni Slack bot reinstall flow to update this connection",
688
- });
689
- }
690
- const destinationSelection = destination.data;
691
- if (
692
- destinationSelection.authorityKind === "organization" &&
693
- authorization.accountGrant?.permissions.includes("account:admin") !== true
694
- ) {
695
- throw new HTTPException(403, { message: "missing permission: account:admin" });
696
- }
697
- if (
698
- destinationSelection.authorityKind === "workspace" &&
699
- !hasPermission(grant.permissions, "workspace:admin")
700
- ) {
701
- throw new HTTPException(403, { message: "missing permission: workspace:admin" });
702
- }
703
- if (
704
- destinationSelection.authorityKind === "personal" &&
705
- (!authorization.contextIntegrity ||
706
- authorization.authenticatedSubjectId !== grant.subjectId)
707
- ) {
708
- throw new HTTPException(403, {
709
- message: "personal destination requires the exact actor",
710
- });
711
- }
712
- const metadata = {
713
- ...existing.metadata,
714
- documentDestination: bindConnectorDocumentDestination(destinationSelection, {
715
- accountId: grant.accountId,
716
- workspaceId,
717
- initiatingSubjectId: grant.subjectId,
992
+ { accountId: grant.accountId, workspaceId },
993
+ (policyTx, policy) =>
994
+ // eslint-disable-next-line no-shadow -- every existing final writer must use this exact scoped transaction
995
+ withWorkspaceSubjectRls(policyTx, workspaceId, grant.subjectId, async (db) => {
996
+ const initial = await getConnectionMetadata(
997
+ db,
998
+ workspaceId,
999
+ c.req.param("connectionId"),
1000
+ grant.subjectId,
1001
+ );
1002
+ if (
1003
+ initial?.metadata.directModelProvider !== undefined ||
1004
+ payload.metadata?.directModelProvider !== undefined ||
1005
+ String(payload.metadata?.credentialRole ?? "").startsWith("direct_")
1006
+ ) {
1007
+ throw new HTTPException(422, {
1008
+ message:
1009
+ "Disconnect and reconnect OpenAI or Azure OpenAI to replace its key or deployment",
1010
+ });
1011
+ }
1012
+ // Model-key rotation owns an advisory -> credential-row prefix. Do
1013
+ // not take its row first or change that excluded lane's lock order.
1014
+ if (!initial || !workspaceProviderApiKeyConnectionKind(initial)) {
1015
+ await db.execute(
1016
+ sql`select id from connections where workspace_id = ${workspaceId}::uuid and id = ${c.req.param("connectionId")}::uuid and (subject_id is null or subject_id = ${grant.subjectId}) for update`,
1017
+ );
1018
+ }
1019
+ assertNotReservedSlackBotMetadata(payload.metadata);
1020
+ assertNotReservedFikenMetadata(payload.metadata);
1021
+ assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
1022
+ assertNotReservedPersonalGitHubMetadata(payload.metadata);
1023
+ const existing = await getConnectionMetadata(
1024
+ db,
1025
+ workspaceId,
1026
+ c.req.param("connectionId"),
1027
+ grant.subjectId,
1028
+ );
1029
+ if (existing && isOpenGeniSlackBotConnection(existing)) {
1030
+ const destination = ConnectorDocumentDestinationSelection.safeParse(
1031
+ payload.metadata?.documentDestination,
1032
+ );
1033
+ const destinationOnlyUpdate =
1034
+ payload.metadata !== undefined &&
1035
+ Object.keys(payload.metadata).length === 1 &&
1036
+ payload.providerDomain === undefined &&
1037
+ payload.subjectId === undefined &&
1038
+ payload.kind === undefined &&
1039
+ payload.status === undefined &&
1040
+ payload.credential === undefined &&
1041
+ payload.grantedScopes === undefined &&
1042
+ payload.expiresAt === undefined;
1043
+ if (!destination.success || !destinationOnlyUpdate) {
1044
+ throw new HTTPException(422, {
1045
+ message:
1046
+ "use the dedicated Opengeni Slack bot reinstall flow to update this connection",
1047
+ });
1048
+ }
1049
+ const destinationSelection = destination.data;
1050
+ if (
1051
+ destinationSelection.authorityKind === "organization" &&
1052
+ authorization.accountGrant?.permissions.includes("account:admin") !== true
1053
+ ) {
1054
+ throw new HTTPException(403, { message: "missing permission: account:admin" });
1055
+ }
1056
+ if (
1057
+ destinationSelection.authorityKind === "workspace" &&
1058
+ !hasPermission(grant.permissions, "workspace:admin")
1059
+ ) {
1060
+ throw new HTTPException(403, { message: "missing permission: workspace:admin" });
1061
+ }
1062
+ if (
1063
+ destinationSelection.authorityKind === "personal" &&
1064
+ (!authorization.contextIntegrity ||
1065
+ authorization.authenticatedSubjectId !== grant.subjectId)
1066
+ ) {
1067
+ throw new HTTPException(403, {
1068
+ message: "personal destination requires the exact actor",
1069
+ });
1070
+ }
1071
+ const metadata = {
1072
+ ...existing.metadata,
1073
+ documentDestination: bindConnectorDocumentDestination(destinationSelection, {
1074
+ accountId: grant.accountId,
1075
+ workspaceId,
1076
+ initiatingSubjectId: grant.subjectId,
1077
+ }),
1078
+ };
1079
+ if (
1080
+ stableJson(metadata.documentDestination) !==
1081
+ stableJson(existing.metadata.documentDestination)
1082
+ ) {
1083
+ assertOrganizationIntegrationAllowed(policy, "slack-bot");
1084
+ }
1085
+ const connection = await updateSlackBotDocumentDestination(db, {
1086
+ accountId: grant.accountId,
1087
+ workspaceId,
1088
+ connectionId: existing.id,
1089
+ visibleToSubjectId: grant.subjectId,
1090
+ expectedVersion: existing.version,
1091
+ metadata,
1092
+ updatedBySubjectId: grant.subjectId,
1093
+ });
1094
+ if (!connection) {
1095
+ throw new HTTPException(409, {
1096
+ message: "Slack bot connection changed; reload before saving its destination",
1097
+ });
1098
+ }
1099
+ return c.json(ConnectionResponse.parse({ connection }));
1100
+ }
1101
+ if (existing && GoogleDriveConnectionMetadata.safeParse(existing.metadata).success) {
1102
+ throw new HTTPException(422, {
1103
+ message: "use the dedicated Google Drive reconnect or source-selection flow",
1104
+ });
1105
+ }
1106
+ if (existing?.metadata.credentialRole === API_INTEGRATION_OAUTH_CREDENTIAL_ROLE) {
1107
+ throw new HTTPException(422, {
1108
+ message:
1109
+ "use the dedicated Integration provider OAuth flow to update this connection",
1110
+ });
1111
+ }
1112
+ if (existing && AtlassianConnectionMetadata.safeParse(existing.metadata).success) {
1113
+ throw new HTTPException(422, {
1114
+ message: "use the dedicated Atlassian reconnect or source-selection flow",
1115
+ });
1116
+ }
1117
+ if (existing && isPersonalGitHubConnection(existing)) {
1118
+ throw new HTTPException(422, {
1119
+ message: "use the dedicated personal GitHub reconnect flow to update this connection",
1120
+ });
1121
+ }
1122
+ if (existing) {
1123
+ const providerDomain = canonicalProviderDomain(
1124
+ payload.providerDomain ?? existing.providerDomain,
1125
+ );
1126
+ const kind = payload.kind ?? existing.kind;
1127
+ const subjectId =
1128
+ payload.subjectId === undefined
1129
+ ? existing.subjectId
1130
+ : writableSubjectId(payload.subjectId, grant.subjectId);
1131
+ const targetWorkspaceProviderKind = workspaceProviderApiKeyConnectionKind({
1132
+ subjectId,
1133
+ providerDomain,
1134
+ kind,
1135
+ metadata: payload.metadata ?? existing.metadata,
1136
+ });
1137
+ if (!workspaceProviderApiKeyConnectionKind(existing) && targetWorkspaceProviderKind) {
1138
+ throw new HTTPException(422, {
1139
+ message:
1140
+ targetWorkspaceProviderKind === "vercel_gateway"
1141
+ ? "use the Vercel AI Gateway connect flow to create this connection"
1142
+ : "use the OpenRouter connect flow to create this connection",
1143
+ });
1144
+ }
1145
+ assertNotDirectPersonalSlackOAuth(providerDomain, kind);
1146
+ assertNotDirectGoogleDriveOAuth(
1147
+ providerDomain,
1148
+ kind,
1149
+ payload.metadata ?? existing.metadata,
1150
+ );
1151
+ assertNotDirectAtlassianOAuth(
1152
+ providerDomain,
1153
+ kind,
1154
+ payload.metadata ?? existing.metadata,
1155
+ );
1156
+ assertNotDirectPersonalGitHubOAuth(
1157
+ providerDomain,
1158
+ kind,
1159
+ payload.metadata ?? existing.metadata,
1160
+ );
1161
+ }
1162
+ // Status is not a free-form field: revocation goes through DELETE, and the
1163
+ // broker owns needs_reauth/error. Reactivating a connection is only
1164
+ // meaningful together with a fresh credential bundle — otherwise a PATCH
1165
+ // could clear the broker's re-auth signal while stale tokens stay in place.
1166
+ if (payload.status !== undefined) {
1167
+ if (payload.status !== "active") {
1168
+ throw new HTTPException(400, {
1169
+ message: 'status can only be set to "active"; use DELETE to revoke',
1170
+ });
1171
+ }
1172
+ if (payload.credential === undefined) {
1173
+ throw new HTTPException(400, {
1174
+ message: "reactivating a connection requires a new credential",
1175
+ });
1176
+ }
1177
+ }
1178
+ const existingWorkspaceProviderKind = existing
1179
+ ? workspaceProviderApiKeyConnectionKind(existing)
1180
+ : null;
1181
+ if (existing && existingWorkspaceProviderKind) {
1182
+ const provider = workspaceProviderApiKeyConnectionSpec(existingWorkspaceProviderKind);
1183
+ const providerDomain = canonicalProviderDomain(
1184
+ payload.providerDomain ?? existing.providerDomain,
1185
+ );
1186
+ const subjectId =
1187
+ payload.subjectId === undefined
1188
+ ? existing.subjectId
1189
+ : writableSubjectId(payload.subjectId, grant.subjectId);
1190
+ const kind = payload.kind ?? existing.kind;
1191
+ if (
1192
+ subjectId !== null ||
1193
+ providerDomain !== provider.providerDomain ||
1194
+ kind !== "api_key" ||
1195
+ (payload.metadata?.credentialRole !== undefined &&
1196
+ payload.metadata.credentialRole !== provider.credentialRole)
1197
+ ) {
1198
+ throw new HTTPException(422, {
1199
+ message: `${provider.label} connection identity cannot be changed`,
1200
+ });
1201
+ }
1202
+ if (payload.credential === undefined) {
1203
+ throw new HTTPException(400, {
1204
+ message: `updating a ${provider.label} connection requires a new credential`,
1205
+ });
1206
+ }
1207
+ if (payload.expectedVersion === undefined || payload.operationId === undefined) {
1208
+ throw new HTTPException(400, {
1209
+ message: `updating a ${provider.label} connection requires expectedVersion and operationId`,
1210
+ });
1211
+ }
1212
+ payload.credential = prepareClaudeWorkspaceCredential(
1213
+ settings,
1214
+ workspaceId,
1215
+ existing.metadata,
1216
+ payload.credential,
1217
+ );
1218
+ assertClaudeWorkspaceCredential(settings, {
1219
+ subjectId,
1220
+ providerDomain,
1221
+ kind,
1222
+ metadata: existing.metadata,
1223
+ credential: payload.credential,
1224
+ });
1225
+ const key = requireEnvironmentEncryption(settings);
1226
+ const grantedScopes = payload.grantedScopes ?? existing.grantedScopes;
1227
+ const expiresAt =
1228
+ payload.expiresAt !== undefined
1229
+ ? payload.expiresAt
1230
+ ? new Date(payload.expiresAt)
1231
+ : null
1232
+ : existing.expiresAt
1233
+ ? new Date(existing.expiresAt)
1234
+ : null;
1235
+ const metadata = workspaceProviderCredentialMetadata(existingWorkspaceProviderKind, {
1236
+ ...existing.metadata,
1237
+ ...(payload.metadata ?? {}),
1238
+ });
1239
+ const input = {
1240
+ accountId: grant.accountId,
1241
+ workspaceId,
1242
+ connectionId: existing.id,
1243
+ expectedVersion: payload.expectedVersion,
1244
+ operationId: payload.operationId,
1245
+ requestDigest: workspaceProviderCredentialRequestDigest(key, {
1246
+ action: "rotate",
1247
+ connectionId: existing.id,
1248
+ expectedVersion: payload.expectedVersion,
1249
+ credential: payload.credential,
1250
+ grantedScopes,
1251
+ expiresAt: expiresAt?.toISOString() ?? null,
1252
+ metadata,
1253
+ }),
1254
+ credentialEncrypted: encryptCredentialBundle(key, payload.credential),
1255
+ grantedScopes,
1256
+ expiresAt,
1257
+ metadata,
1258
+ updatedBySubjectId: grant.subjectId,
1259
+ };
1260
+ const connection = await rotateWorkspaceProviderApiKeyConnection(
1261
+ db,
1262
+ existingWorkspaceProviderKind,
1263
+ input,
1264
+ );
1265
+ if (!connection) {
1266
+ throw new HTTPException(409, {
1267
+ message: `${provider.label} connection changed; reload before replacing its key`,
1268
+ });
1269
+ }
1270
+ return c.json(ConnectionResponse.parse({ connection }));
1271
+ }
1272
+ const key =
1273
+ payload.credential === undefined ? null : requireEnvironmentEncryption(settings);
1274
+ const subjectId =
1275
+ payload.subjectId === undefined
1276
+ ? undefined
1277
+ : writableSubjectId(payload.subjectId, grant.subjectId);
1278
+ if (
1279
+ existing &&
1280
+ (payload.credential !== undefined ||
1281
+ (payload.providerDomain !== undefined &&
1282
+ canonicalProviderDomain(payload.providerDomain) !== existing.providerDomain) ||
1283
+ (payload.kind !== undefined && payload.kind !== existing.kind) ||
1284
+ (subjectId !== undefined && subjectId !== existing.subjectId) ||
1285
+ payload.grantedScopes?.some((scope) => !existing.grantedScopes.includes(scope)) ||
1286
+ (payload.metadata !== undefined &&
1287
+ ["mcpUrl", "resource", "credentialRole"].some(
1288
+ (field) =>
1289
+ stableJson(payload.metadata?.[field] ?? null) !==
1290
+ stableJson(existing.metadata[field] ?? null),
1291
+ )) ||
1292
+ (payload.expiresAt !== undefined &&
1293
+ existing.expiresAt !== null &&
1294
+ (payload.expiresAt === null ||
1295
+ Date.parse(payload.expiresAt) > Date.parse(existing.expiresAt))) ||
1296
+ (payload.status === "active" && existing.status !== "active"))
1297
+ ) {
1298
+ assertOrganizationIntegrationAllowed(policy, null);
1299
+ }
1300
+ if (payload.credential !== undefined) {
1301
+ assertBrokeredApiKeyCredential(payload.kind ?? existing?.kind, payload.credential);
1302
+ }
1303
+ const connection = await updateConnection(db, {
1304
+ workspaceId,
1305
+ connectionId: c.req.param("connectionId"),
1306
+ visibleToSubjectId: grant.subjectId,
1307
+ updatedBySubjectId: grant.subjectId,
1308
+ ...(payload.providerDomain !== undefined
1309
+ ? { providerDomain: canonicalProviderDomain(payload.providerDomain) }
1310
+ : {}),
1311
+ ...(subjectId !== undefined ? { subjectId } : {}),
1312
+ ...(payload.kind !== undefined ? { kind: payload.kind } : {}),
1313
+ ...(payload.status !== undefined ? { status: payload.status } : {}),
1314
+ ...(payload.credential !== undefined && key
1315
+ ? { credentialEncrypted: encryptCredentialBundle(key, payload.credential) }
1316
+ : {}),
1317
+ ...(payload.grantedScopes !== undefined
1318
+ ? { grantedScopes: payload.grantedScopes }
1319
+ : {}),
1320
+ ...(payload.expiresAt !== undefined
1321
+ ? { expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null }
1322
+ : {}),
1323
+ ...(payload.metadata !== undefined ? { metadata: payload.metadata } : {}),
1324
+ });
1325
+ if (!connection) {
1326
+ throw new HTTPException(404, { message: "connection not found" });
1327
+ }
1328
+ return c.json(ConnectionResponse.parse({ connection }));
718
1329
  }),
719
- };
720
- const connection = await updateSlackBotDocumentDestination(db, {
721
- accountId: grant.accountId,
722
- workspaceId,
723
- connectionId: existing.id,
724
- visibleToSubjectId: grant.subjectId,
725
- expectedVersion: existing.version,
726
- metadata,
727
- updatedBySubjectId: grant.subjectId,
728
- });
729
- if (!connection) {
730
- throw new HTTPException(409, {
731
- message: "Slack bot connection changed; reload before saving its destination",
732
- });
733
- }
734
- return c.json(ConnectionResponse.parse({ connection }));
735
- }
736
- if (existing && GoogleDriveConnectionMetadata.safeParse(existing.metadata).success) {
737
- throw new HTTPException(422, {
738
- message: "use the dedicated Google Drive reconnect or source-selection flow",
739
- });
740
- }
741
- if (existing?.metadata.credentialRole === API_INTEGRATION_OAUTH_CREDENTIAL_ROLE) {
742
- throw new HTTPException(422, {
743
- message: "use the dedicated Integration provider OAuth flow to update this connection",
744
- });
745
- }
746
- if (existing && AtlassianConnectionMetadata.safeParse(existing.metadata).success) {
747
- throw new HTTPException(422, {
748
- message: "use the dedicated Atlassian reconnect or source-selection flow",
749
- });
750
- }
751
- if (existing) {
752
- const providerDomain = canonicalProviderDomain(
753
- payload.providerDomain ?? existing.providerDomain,
754
- );
755
- const kind = payload.kind ?? existing.kind;
756
- assertNotDirectPersonalSlackOAuth(providerDomain, kind);
757
- assertNotDirectGoogleDriveOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
758
- assertNotDirectAtlassianOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
759
- }
760
- // Status is not a free-form field: revocation goes through DELETE, and the
761
- // broker owns needs_reauth/error. Reactivating a connection is only
762
- // meaningful together with a fresh credential bundle — otherwise a PATCH
763
- // could clear the broker's re-auth signal while stale tokens stay in place.
764
- if (payload.status !== undefined) {
765
- if (payload.status !== "active") {
766
- throw new HTTPException(400, {
767
- message: 'status can only be set to "active"; use DELETE to revoke',
768
- });
769
- }
770
- if (payload.credential === undefined) {
771
- throw new HTTPException(400, {
772
- message: "reactivating a connection requires a new credential",
773
- });
774
- }
775
- }
776
- const key = payload.credential === undefined ? null : requireEnvironmentEncryption(settings);
777
- const subjectId =
778
- payload.subjectId === undefined
779
- ? undefined
780
- : writableSubjectId(payload.subjectId, grant.subjectId);
781
- const connection = await updateConnection(db, {
782
- workspaceId,
783
- connectionId: c.req.param("connectionId"),
784
- visibleToSubjectId: grant.subjectId,
785
- updatedBySubjectId: grant.subjectId,
786
- ...(payload.providerDomain !== undefined
787
- ? { providerDomain: canonicalProviderDomain(payload.providerDomain) }
788
- : {}),
789
- ...(subjectId !== undefined ? { subjectId } : {}),
790
- ...(payload.kind !== undefined ? { kind: payload.kind } : {}),
791
- ...(payload.status !== undefined ? { status: payload.status } : {}),
792
- ...(payload.credential !== undefined && key
793
- ? { credentialEncrypted: encryptCredentialBundle(key, payload.credential) }
794
- : {}),
795
- ...(payload.grantedScopes !== undefined ? { grantedScopes: payload.grantedScopes } : {}),
796
- ...(payload.expiresAt !== undefined
797
- ? { expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null }
798
- : {}),
799
- ...(payload.metadata !== undefined ? { metadata: payload.metadata } : {}),
800
- });
801
- if (!connection) {
802
- throw new HTTPException(404, { message: "connection not found" });
803
- }
804
- return c.json(ConnectionResponse.parse({ connection }));
1330
+ );
805
1331
  });
806
1332
 
807
1333
  app.delete("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
@@ -812,6 +1338,26 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
812
1338
  if (!existing) {
813
1339
  throw new HTTPException(404, { message: "connection not found" });
814
1340
  }
1341
+ const expectedVersion = c.req.query("expectedVersion");
1342
+ if (expectedVersion !== undefined) {
1343
+ if (!/^[1-9]\d*$/.test(expectedVersion) || !Number.isSafeInteger(Number(expectedVersion))) {
1344
+ throw new HTTPException(400, { message: "invalid expected connection version" });
1345
+ }
1346
+ if (Number(expectedVersion) !== existing.version) {
1347
+ throw new HTTPException(409, { message: "connection changed; refresh before disconnect" });
1348
+ }
1349
+ }
1350
+ const isPersonalGitHub =
1351
+ existing.subjectId === grant.subjectId && isPersonalGitHubConnection(existing);
1352
+ if (isPersonalGitHub) {
1353
+ const access = await requireAccessGrantAuthorization(
1354
+ c,
1355
+ deps,
1356
+ workspaceId,
1357
+ "connections:write",
1358
+ );
1359
+ assertPersonalConnectionOwnerPrincipal(access, "My GitHub account");
1360
+ }
815
1361
  const isGoogleDrive =
816
1362
  existing.subjectId === grant.subjectId &&
817
1363
  existing.providerDomain === GOOGLE_DRIVE_PROVIDER_DOMAIN &&
@@ -822,6 +1368,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
822
1368
  existing.providerDomain === ATLASSIAN_PROVIDER_DOMAIN &&
823
1369
  existing.kind === "oauth2" &&
824
1370
  AtlassianConnectionMetadata.safeParse(existing.metadata).success;
1371
+ const workspaceProviderKind = workspaceProviderApiKeyConnectionKind(existing);
825
1372
  const disconnectPayload = await c.req.json().catch(() => null);
826
1373
  const googleDriveDisconnect = isGoogleDrive
827
1374
  ? GoogleDriveDisconnectRequest.safeParse(disconnectPayload)
@@ -842,7 +1389,23 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
842
1389
  atlassianDisconnect.error.issues[0]?.message ?? "invalid Atlassian disconnect request",
843
1390
  });
844
1391
  }
845
- if (existing.status === "revoked" && !isGoogleDrive && !isAtlassian) {
1392
+ const personalGitHubDisconnect = isPersonalGitHub
1393
+ ? PersonalGitHubDisconnectRequest.safeParse(disconnectPayload)
1394
+ : null;
1395
+ if (personalGitHubDisconnect && !personalGitHubDisconnect.success) {
1396
+ throw new HTTPException(400, {
1397
+ message:
1398
+ personalGitHubDisconnect.error.issues[0]?.message ??
1399
+ "invalid personal GitHub disconnect request",
1400
+ });
1401
+ }
1402
+ if (
1403
+ existing.status === "revoked" &&
1404
+ !isGoogleDrive &&
1405
+ !isAtlassian &&
1406
+ !isPersonalGitHub &&
1407
+ !workspaceProviderKind
1408
+ ) {
846
1409
  return c.json(ConnectionResponse.parse({ connection: existing }));
847
1410
  }
848
1411
  const connection = isGoogleDrive
@@ -859,24 +1422,39 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
859
1422
  connection: existing,
860
1423
  payload: atlassianDisconnect!.data,
861
1424
  })
862
- : isOpenGeniSlackBotConnection(existing)
863
- ? await revokeConnectionWithSlackBotSuccessAudit(db, {
864
- accountId: grant.accountId,
1425
+ : isPersonalGitHub
1426
+ ? await disconnectPersonalGitHub(deps, {
865
1427
  workspaceId,
866
1428
  subjectId: grant.subjectId,
867
1429
  connectionId,
868
- expectedVersion: existing.version,
869
- credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
870
- credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
871
- slackTeamId: openGeniSlackBotMetadata(existing.metadata)!.slackTeamId,
1430
+ payload: personalGitHubDisconnect!.data,
872
1431
  })
873
- : await revokeConnection(
874
- db,
875
- workspaceId,
876
- connectionId,
877
- grant.subjectId,
878
- existing.version,
879
- );
1432
+ : isOpenGeniSlackBotConnection(existing)
1433
+ ? await revokeConnectionWithSlackBotSuccessAudit(db, {
1434
+ accountId: grant.accountId,
1435
+ workspaceId,
1436
+ subjectId: grant.subjectId,
1437
+ connectionId,
1438
+ expectedVersion: existing.version,
1439
+ credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
1440
+ credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
1441
+ slackTeamId: openGeniSlackBotMetadata(existing.metadata)!.slackTeamId,
1442
+ })
1443
+ : workspaceProviderKind
1444
+ ? await revokeWorkspaceProviderApiKeyConnections(db, workspaceProviderKind, {
1445
+ accountId: grant.accountId,
1446
+ workspaceId,
1447
+ connectionId,
1448
+ expectedVersion: existing.version,
1449
+ updatedBySubjectId: grant.subjectId,
1450
+ })
1451
+ : await revokeConnection(
1452
+ db,
1453
+ workspaceId,
1454
+ connectionId,
1455
+ grant.subjectId,
1456
+ existing.version,
1457
+ );
880
1458
  if (!connection) {
881
1459
  throw new HTTPException(409, { message: "connection changed during disconnect; try again" });
882
1460
  }
@@ -886,7 +1464,9 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
886
1464
  app.post("/v1/workspaces/:workspaceId/connections/oauth/start", async (c) => {
887
1465
  assertIntegrationsEnabled();
888
1466
  const workspaceId = c.req.param("workspaceId");
889
- const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
1467
+ const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
1468
+ requireProviderConsentInBrowser(access);
1469
+ const grant = access.grant;
890
1470
  const parsed = OAuthStartRequest.safeParse(await c.req.json());
891
1471
  if (!parsed.success) {
892
1472
  throw new HTTPException(400, {
@@ -897,9 +1477,13 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
897
1477
  const result = await startMcpOAuth(
898
1478
  { db, settings, observability, oauthStartDeadlineMs: deps.oauthStartDeadlineMs },
899
1479
  {
1480
+ ...(externalActorContinuationForAuthorization(access)
1481
+ ? { externalContinuation: externalActorContinuationForAuthorization(access)! }
1482
+ : {}),
900
1483
  accountId: grant.accountId,
901
1484
  workspaceId,
902
1485
  subjectId: grant.subjectId,
1486
+ personalOwnershipAllowed: isPersonalConnectionOwnerPrincipal(access),
903
1487
  requestUrl: c.req.url,
904
1488
  payload,
905
1489
  },
@@ -910,7 +1494,9 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
910
1494
  app.post("/v1/workspaces/:workspaceId/integrations/oauth/start", async (c) => {
911
1495
  assertIntegrationsEnabled();
912
1496
  const workspaceId = c.req.param("workspaceId");
913
- const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
1497
+ const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
1498
+ requireProviderConsentInBrowser(access);
1499
+ const grant = access.grant;
914
1500
  const parsed = ApiIntegrationOAuthStartRequest.safeParse(await c.req.json());
915
1501
  if (!parsed.success) {
916
1502
  throw new HTTPException(400, {
@@ -920,9 +1506,13 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
920
1506
  return c.json(
921
1507
  OAuthStartResponse.parse(
922
1508
  await startApiIntegrationProviderOAuth(deps, {
1509
+ ...(externalActorContinuationForAuthorization(access)
1510
+ ? { externalContinuation: externalActorContinuationForAuthorization(access)! }
1511
+ : {}),
923
1512
  accountId: grant.accountId,
924
1513
  workspaceId,
925
1514
  subjectId: grant.subjectId,
1515
+ personalOwnershipAllowed: isPersonalConnectionOwnerPrincipal(access),
926
1516
  requestUrl: c.req.url,
927
1517
  payload: parsed.data,
928
1518
  }),
@@ -932,15 +1522,24 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
932
1522
 
933
1523
  app.get("/v1/integrations/oauth/callback", async (c) => {
934
1524
  assertIntegrationsEnabled();
935
- const result = await completeMcpOAuthCallback(
936
- { db, settings, observability, oauthCallbackDeadlineMs: deps.oauthCallbackDeadlineMs },
937
- {
938
- code: c.req.query("code"),
939
- state: c.req.query("state"),
940
- requestUrl: c.req.url,
941
- },
942
- );
943
- return c.redirect(result.redirectTo, 302);
1525
+ const input = {
1526
+ ...(c.req.query("code") ? { code: c.req.query("code") } : {}),
1527
+ ...(c.req.query("state") ? { state: c.req.query("state") } : {}),
1528
+ ...(c.req.query("error") ? { error: c.req.query("error") } : {}),
1529
+ requestUrl: c.req.url,
1530
+ };
1531
+ const result = isApiIntegrationProviderOAuthState(input.state, deps.settings)
1532
+ ? await completeApiIntegrationProviderOAuth(deps, input)
1533
+ : await completeMcpOAuthCallback(
1534
+ { db, settings, observability, oauthCallbackDeadlineMs: deps.oauthCallbackDeadlineMs },
1535
+ input,
1536
+ );
1537
+ if ("exactReturn" in result && result.exactReturn)
1538
+ return new Response(null, { status: 302, headers: { Location: result.redirectTo } });
1539
+ const redirectTo = settings.webBaseUrl
1540
+ ? new URL(result.redirectTo, settings.webBaseUrl).toString()
1541
+ : result.redirectTo;
1542
+ return c.redirect(redirectTo, 302);
944
1543
  });
945
1544
 
946
1545
  app.get("/v1/integrations/provider-oauth/callback", async (c) => {
@@ -951,6 +1550,8 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
951
1550
  ...(c.req.query("error") ? { error: c.req.query("error") } : {}),
952
1551
  requestUrl: c.req.url,
953
1552
  });
1553
+ if (result.exactReturn)
1554
+ return new Response(null, { status: 302, headers: { Location: result.redirectTo } });
954
1555
  return c.redirect(result.redirectTo, 302);
955
1556
  });
956
1557
 
@@ -999,7 +1600,7 @@ async function persistOpenGeniSlackBotConnection(input: {
999
1600
  throw new SlackInstallCallbackError(
1000
1601
  422,
1001
1602
  "connection_conflict",
1002
- "connectionId is not an OpenGeni Slack bot connection",
1603
+ "connectionId is not an Opengeni Slack bot connection",
1003
1604
  "principal_validation",
1004
1605
  );
1005
1606
  }
@@ -1067,21 +1668,6 @@ async function persistOpenGeniSlackBotConnection(input: {
1067
1668
  }
1068
1669
  }
1069
1670
 
1070
- function requireOpenGeniSlackOAuthSettings(settings: ApiRouteDeps["settings"]): {
1071
- clientId: string;
1072
- clientSecret: string;
1073
- } {
1074
- const clientId = settings.slackClientId?.trim();
1075
- const clientSecret = settings.slackClientSecret?.trim();
1076
- if (!clientId || !clientSecret) {
1077
- throw new HTTPException(503, {
1078
- message:
1079
- "OpenGeni Slack installation requires OPENGENI_SLACK_CLIENT_ID and OPENGENI_SLACK_CLIENT_SECRET",
1080
- });
1081
- }
1082
- return { clientId, clientSecret };
1083
- }
1084
-
1085
1671
  function readOpenGeniSlackInstallState(
1086
1672
  rawState: string | undefined,
1087
1673
  settings: ApiRouteDeps["settings"],
@@ -1096,6 +1682,11 @@ function readOpenGeniSlackInstallState(
1096
1682
  if (!payload) {
1097
1683
  throw new HTTPException(400, { message: "invalid or expired Slack installation state" });
1098
1684
  }
1685
+ if (
1686
+ (payload.kind !== undefined && payload.kind !== "slack_bot_install") ||
1687
+ (payload.connectAttemptId && payload.kind !== "slack_bot_install")
1688
+ )
1689
+ throw new HTTPException(400, { message: "invalid Slack installation state kind" });
1099
1690
  const requiredString = (value: unknown, label: string): string => {
1100
1691
  if (typeof value !== "string" || value.length === 0) {
1101
1692
  throw new HTTPException(400, { message: `invalid Slack installation ${label}` });
@@ -1130,6 +1721,21 @@ function readOpenGeniSlackInstallState(
1130
1721
  workspaceId,
1131
1722
  subjectId,
1132
1723
  returnPath,
1724
+ ...(typeof payload.connectAttemptId === "string"
1725
+ ? { connectAttemptId: payload.connectAttemptId }
1726
+ : {}),
1727
+ ...(typeof payload.encryptedExternalContinuation === "string"
1728
+ ? {
1729
+ externalContinuation: ExternalActorContinuation.parse(
1730
+ JSON.parse(
1731
+ decryptEnvironmentValue(
1732
+ requireEnvironmentEncryption(settings),
1733
+ payload.encryptedExternalContinuation,
1734
+ ),
1735
+ ),
1736
+ ),
1737
+ }
1738
+ : {}),
1133
1739
  ...(connectionId ? { connectionId, connectionVersion: connectionVersion! } : {}),
1134
1740
  nonce: requiredString(payload.nonce, "nonce"),
1135
1741
  iat:
@@ -1181,6 +1787,9 @@ function slackInstallCallbackFailure(
1181
1787
  if (error instanceof SlackBotLifecycleSuccessAuditError) {
1182
1788
  return { stage: "persistence", reason: "success_audit_failed" };
1183
1789
  }
1790
+ if (stage === "permission_check" && error instanceof HTTPException && error.status === 403) {
1791
+ return { stage, reason: "permission_lost" };
1792
+ }
1184
1793
  if (stage === "code_exchange") {
1185
1794
  return { stage, reason: "exchange_failed" };
1186
1795
  }
@@ -1204,18 +1813,7 @@ async function requireSlackInstallCallbackGrant(
1204
1813
  db: ApiRouteDeps["db"],
1205
1814
  state: OpenGeniSlackInstallState,
1206
1815
  ): Promise<void> {
1207
- const grant = await getWorkspaceGrant(db, state.subjectId, state.workspaceId);
1208
- if (
1209
- !grant ||
1210
- grant.accountId !== state.accountId ||
1211
- !hasPermission(grant.permissions, "connections:write")
1212
- ) {
1213
- throw new SlackInstallCallbackError(
1214
- 403,
1215
- "permission_lost",
1216
- "Slack installation subject no longer has permission for this workspace",
1217
- );
1218
- }
1816
+ await requireConnectOwnerAuthority(db, state, "connections:write");
1219
1817
  }
1220
1818
 
1221
1819
  function assertNotDirectPersonalSlackOAuth(providerDomain: string, kind: string): void {
@@ -1256,10 +1854,78 @@ function assertNotDirectAtlassianOAuth(
1256
1854
  }
1257
1855
  }
1258
1856
 
1857
+ function assertNotDirectPersonalGitHubOAuth(
1858
+ providerDomain: string,
1859
+ kind: string,
1860
+ metadata: Record<string, unknown> | undefined,
1861
+ ): void {
1862
+ if (
1863
+ (providerDomain === PERSONAL_GITHUB_PROVIDER_DOMAIN && kind === "oauth2") ||
1864
+ hasReservedPersonalGitHubMetadata(metadata)
1865
+ ) {
1866
+ throw new HTTPException(422, {
1867
+ message: "personal GitHub credentials must use the dedicated OAuth connection flow",
1868
+ });
1869
+ }
1870
+ }
1871
+
1872
+ function assertNotReservedPersonalGitHubMetadata(
1873
+ metadata: Record<string, unknown> | undefined,
1874
+ ): void {
1875
+ if (hasReservedPersonalGitHubMetadata(metadata)) {
1876
+ throw new HTTPException(422, {
1877
+ message: "personal GitHub metadata is reserved for the dedicated OAuth connection flow",
1878
+ });
1879
+ }
1880
+ }
1881
+
1882
+ function workspaceProviderApiKeyConnectionKind(input: {
1883
+ subjectId?: string | null;
1884
+ providerDomain: string;
1885
+ kind: string;
1886
+ metadata?: Record<string, unknown>;
1887
+ }): WorkspaceProviderApiKeyConnectionKind | null {
1888
+ if (input.subjectId != null || input.kind !== "api_key") return null;
1889
+ const providerDomain = input.providerDomain.toLowerCase();
1890
+ if (
1891
+ providerDomain === "api.anthropic.com" &&
1892
+ (input.metadata?.credentialRole === "anthropic" ||
1893
+ input.metadata?.credentialRole === "claude_subscription")
1894
+ )
1895
+ return input.metadata.credentialRole;
1896
+ if (
1897
+ providerDomain === VERCEL_AI_GATEWAY_CONNECTION_DOMAIN &&
1898
+ input.metadata?.credentialRole === VERCEL_AI_GATEWAY_CONNECTION_ROLE
1899
+ ) {
1900
+ return "vercel_gateway";
1901
+ }
1902
+ if (
1903
+ providerDomain === WORKSPACE_OPENROUTER_CONNECTION_DOMAIN &&
1904
+ input.metadata?.credentialRole === WORKSPACE_OPENROUTER_CONNECTION_ROLE
1905
+ ) {
1906
+ return "openrouter";
1907
+ }
1908
+ return null;
1909
+ }
1910
+
1911
+ /**
1912
+ * A brokered api_key Connection is usable only through `headers` or
1913
+ * `placements`; anything else was previously accepted and then failed every
1914
+ * tool call with a misleading "connect an account" auth-needed notice.
1915
+ */
1916
+ function assertBrokeredApiKeyCredential(
1917
+ kind: string | undefined,
1918
+ credential: Record<string, unknown>,
1919
+ ): void {
1920
+ if (kind !== "api_key") return;
1921
+ const problem = brokeredCredentialBundleProblem(credential);
1922
+ if (problem) throw new HTTPException(422, { message: problem });
1923
+ }
1924
+
1259
1925
  function assertNotReservedSlackBotMetadata(metadata: Record<string, unknown> | undefined): void {
1260
1926
  if (hasReservedOpenGeniSlackBotMetadata(metadata)) {
1261
1927
  throw new HTTPException(422, {
1262
- message: "OpenGeni Slack bot metadata is reserved for the dedicated connection flow",
1928
+ message: "Opengeni Slack bot metadata is reserved for the dedicated connection flow",
1263
1929
  });
1264
1930
  }
1265
1931
  }
@@ -1315,6 +1981,31 @@ function encryptCredentialBundle(key: Uint8Array, credential: Record<string, unk
1315
1981
  return encryptEnvironmentValue(key, JSON.stringify(credential));
1316
1982
  }
1317
1983
 
1984
+ function workspaceProviderCredentialMetadata(
1985
+ providerKind: WorkspaceProviderApiKeyConnectionKind,
1986
+ metadata: Record<string, unknown> | undefined,
1987
+ ): Record<string, unknown> {
1988
+ const {
1989
+ [OPENROUTER_CREDENTIAL_OPERATION_ID_METADATA_KEY]: _openRouterOperationId,
1990
+ [OPENROUTER_CREDENTIAL_OPERATION_DIGEST_METADATA_KEY]: _openRouterOperationDigest,
1991
+ [VERCEL_AI_GATEWAY_CREDENTIAL_OPERATION_ID_METADATA_KEY]: _operationId,
1992
+ [VERCEL_AI_GATEWAY_CREDENTIAL_OPERATION_DIGEST_METADATA_KEY]: _operationDigest,
1993
+ ...effectiveMetadata
1994
+ } = metadata ?? {};
1995
+ for (const role of ["anthropic", "claude_subscription"]) {
1996
+ delete effectiveMetadata[`${role}CredentialOperationId`];
1997
+ delete effectiveMetadata[`${role}CredentialOperationDigest`];
1998
+ }
1999
+ return {
2000
+ ...effectiveMetadata,
2001
+ credentialRole: workspaceProviderApiKeyConnectionSpec(providerKind).credentialRole,
2002
+ };
2003
+ }
2004
+
2005
+ function workspaceProviderCredentialRequestDigest(key: Uint8Array, value: unknown): string {
2006
+ return createHmac("sha256", key).update(stableJson(value), "utf8").digest("hex");
2007
+ }
2008
+
1318
2009
  function slackBotCredentialBundle(token: string): Record<string, unknown> {
1319
2010
  const headerName = ["author", "ization"].join("");
1320
2011
  const scheme = ["Bear", "er"].join("");