@opengeni/api-router 3.0.1 → 3.0.2-canary.38894290271001

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 (546) hide show
  1. package/dist/access-grant-rls.d.ts +8 -1
  2. package/dist/admission-trace.d.ts +8 -0
  3. package/dist/app.d.ts +50 -3
  4. package/dist/app.js +10 -1
  5. package/dist/artifact-catalog-cursor.d.ts +1 -0
  6. package/dist/attached-browser-viewer.d.ts +11 -0
  7. package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
  8. package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
  9. package/dist/auth/managed-auth-session-adapter.d.ts +9 -1
  10. package/dist/auth/managed-auth.d.ts +72 -5
  11. package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
  12. package/dist/auth/new-signups-gate.d.ts +23 -0
  13. package/dist/auth/signup-funnel-metrics.d.ts +44 -0
  14. package/dist/azure-live.d.ts +28 -0
  15. package/dist/browser-action-fences.d.ts +13 -0
  16. package/dist/browser-controller-authority.d.ts +1 -61
  17. package/dist/browser-ephemeral.d.ts +5 -0
  18. package/dist/browser-lost-runtime-end.d.ts +24 -0
  19. package/dist/browser-state-authority.d.ts +1 -27
  20. package/dist/{chunk-AJ62T5QN.js → chunk-GBUJ2DYZ.js} +80507 -39878
  21. package/dist/chunk-GBUJ2DYZ.js.map +1 -0
  22. package/dist/chunk-OGQDXVXY.js +18 -0
  23. package/dist/chunk-OGQDXVXY.js.map +1 -0
  24. package/dist/claude-default-models.d.ts +19 -0
  25. package/dist/claude-subscription-core.d.ts +50 -0
  26. package/dist/claude-subscription-oauth.d.ts +36 -0
  27. package/dist/claude-subscription-profile.d.ts +9 -0
  28. package/dist/claude-subscription-usage.d.ts +6 -0
  29. package/dist/claude-workspace-connection.d.ts +15 -0
  30. package/dist/codemode.d.ts +40 -5
  31. package/dist/codex-realtime.d.ts +29 -3
  32. package/dist/codex-session-pointers.d.ts +19 -0
  33. package/dist/connection-ownership.d.ts +6 -3
  34. package/dist/controller-data-plane.d.ts +16 -0
  35. package/dist/credit-promotion-snapshot.d.ts +6 -0
  36. package/dist/editable-artifact-source-session.d.ts +3 -0
  37. package/dist/editable-artifact-websocket.d.ts +5 -3
  38. package/dist/fatal-process-boundary.d.ts +9 -1
  39. package/dist/github-access.d.ts +7 -1
  40. package/dist/github-browser-flow.d.ts +1 -1
  41. package/dist/http/acting-person.d.ts +36 -0
  42. package/dist/http/api-contract-compat.d.ts +19 -0
  43. package/dist/http/api-error.d.ts +50 -1
  44. package/dist/http/auth.d.ts +8 -1
  45. package/dist/http/cors.d.ts +12 -0
  46. package/dist/http/deprecation.d.ts +42 -0
  47. package/dist/http/keyed-admission.d.ts +12 -0
  48. package/dist/http/local-browser-boundary.d.ts +72 -0
  49. package/dist/http/metrics-listener.d.ts +22 -0
  50. package/dist/http/path-identifier.d.ts +17 -0
  51. package/dist/http/registered-route-label.d.ts +16 -0
  52. package/dist/http/rejection-telemetry.d.ts +34 -0
  53. package/dist/http/request-body.d.ts +27 -0
  54. package/dist/http/request-source.d.ts +41 -3
  55. package/dist/http/sse.d.ts +12 -1
  56. package/dist/http/unmatched-route.d.ts +18 -0
  57. package/dist/http/user-content.d.ts +36 -0
  58. package/dist/http/variable-sets-removed.d.ts +41 -0
  59. package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
  60. package/dist/index.js +1279 -108
  61. package/dist/index.js.map +1 -1
  62. package/dist/integration-connect-metrics.d.ts +35 -0
  63. package/dist/integration-endpoint-test.d.ts +20 -0
  64. package/dist/integrations/api-integrations.d.ts +2 -2
  65. package/dist/integrations/atlassian.d.ts +8 -3
  66. package/dist/integrations/fiken.d.ts +3 -1
  67. package/dist/integrations/github-lens-connect.d.ts +37 -66
  68. package/dist/integrations/google-drive.d.ts +2 -0
  69. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  70. package/dist/integrations/marketplace-plugin.d.ts +43 -43
  71. package/dist/integrations/oauth-client.d.ts +171 -12
  72. package/dist/integrations/oauth-profiles.d.ts +42 -13
  73. package/dist/integrations/onepassword-vault-provider.d.ts +9 -0
  74. package/dist/integrations/slack-bot.d.ts +285 -7
  75. package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
  76. package/dist/integrations/slack-interactions.d.ts +89 -10
  77. package/dist/integrations/slack-mrkdwn.d.ts +41 -0
  78. package/dist/integrations/slack-routing.d.ts +1 -1
  79. package/dist/integrations/slack-session-defaults.d.ts +58 -0
  80. package/dist/integrations/slack-task-file-upload.d.ts +36 -0
  81. package/dist/integrations/slack-tool-review.d.ts +5 -0
  82. package/dist/interaction-agent-access.d.ts +13 -8
  83. package/dist/interaction-agent-version.d.ts +21 -0
  84. package/dist/interaction-holder-heartbeat.d.ts +5 -0
  85. package/dist/interaction-metrics.d.ts +27 -2
  86. package/dist/interaction-route-stages.d.ts +25 -0
  87. package/dist/interaction-secrets-vault.d.ts +20 -0
  88. package/dist/interaction-secrets.d.ts +103 -0
  89. package/dist/mcp/action-catalog-types.d.ts +14 -0
  90. package/dist/mcp/action-catalog.gen.d.ts +2 -0
  91. package/dist/mcp/action-descriptions.d.ts +4 -0
  92. package/dist/mcp/action-schema-imports.gen.d.ts +1 -0
  93. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  94. package/dist/mcp/child-read-evidence.d.ts +9 -0
  95. package/dist/mcp/company-profile-agent-admin.d.ts +12 -0
  96. package/dist/mcp/contract-input.d.ts +13 -0
  97. package/dist/mcp/documents.d.ts +2 -0
  98. package/dist/mcp/first-party-tool-permissions.d.ts +3 -606
  99. package/dist/mcp/notification-tools.d.ts +26 -0
  100. package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
  101. package/dist/mcp/scheduled-task-input.d.ts +1309 -0
  102. package/dist/mcp/scheduled-task-view.d.ts +217 -3
  103. package/dist/mcp/secret-tools.d.ts +14 -0
  104. package/dist/mcp/server.d.ts +108 -5
  105. package/dist/mcp/session-admin-access-tools.d.ts +33 -0
  106. package/dist/mcp/session-event-errors.d.ts +28 -0
  107. package/dist/mcp/session-event-view.d.ts +62 -0
  108. package/dist/mcp/session-view.d.ts +15 -0
  109. package/dist/mcp/session-wait.d.ts +18 -0
  110. package/dist/mcp/site-agent-clamp-tools.d.ts +38 -0
  111. package/dist/mcp/site-data-tools.d.ts +22 -0
  112. package/dist/mcp/site-publication-tools.d.ts +19 -0
  113. package/dist/mcp/site-sql-tools.d.ts +27 -0
  114. package/dist/mcp-oauth.d.ts +37 -4
  115. package/dist/model-calls/chat-completions.d.ts +87 -0
  116. package/dist/model-calls/pending-usage-settlements.d.ts +57 -0
  117. package/dist/model-calls/service.d.ts +36 -0
  118. package/dist/model-catalog.d.ts +9 -3
  119. package/dist/native-push-dispatch.d.ts +69 -0
  120. package/dist/observability.d.ts +3 -1
  121. package/dist/organization-mcp.d.ts +82 -0
  122. package/dist/prepared-mcp-actor.d.ts +77 -0
  123. package/dist/prepared-mcp-permissions.d.ts +8 -0
  124. package/dist/routes/analytics-consent.d.ts +26 -0
  125. package/dist/routes/api-integrations.d.ts +11 -0
  126. package/dist/routes/api-keys.d.ts +7 -7
  127. package/dist/routes/automations.d.ts +2 -0
  128. package/dist/routes/billing.d.ts +59 -0
  129. package/dist/routes/browser-sessions.d.ts +65 -2
  130. package/dist/routes/chat-completions.d.ts +11 -0
  131. package/dist/routes/claude-core-usage.d.ts +20 -0
  132. package/dist/routes/claude-subscription-accounts.d.ts +3 -0
  133. package/dist/routes/claude-subscription-oauth.d.ts +3 -0
  134. package/dist/routes/client-errors.d.ts +182 -0
  135. package/dist/routes/codex-core.d.ts +401 -0
  136. package/dist/routes/codex.d.ts +64 -2
  137. package/dist/routes/computer-sessions.d.ts +6 -1
  138. package/dist/routes/desktop-distribution.d.ts +32 -0
  139. package/dist/routes/github-browser-fonts.gen.d.ts +2 -0
  140. package/dist/routes/github-browser-pages.d.ts +27 -0
  141. package/dist/routes/inbox.d.ts +13 -0
  142. package/dist/routes/insights-response-cache.d.ts +10 -0
  143. package/dist/routes/insights-usage.d.ts +12 -0
  144. package/dist/routes/insights.d.ts +28 -0
  145. package/dist/routes/interaction-placement-message.d.ts +7 -0
  146. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  147. package/dist/routes/model-account-choice.d.ts +13 -0
  148. package/dist/routes/model-account-settings.d.ts +19 -0
  149. package/dist/routes/native-app-auth.d.ts +12 -0
  150. package/dist/routes/organization-integration-policy.d.ts +8 -0
  151. package/dist/routes/organization-integrations.d.ts +6 -0
  152. package/dist/routes/organization-mcp-connections.d.ts +3 -0
  153. package/dist/routes/organization-model-defaults.d.ts +9 -0
  154. package/dist/routes/organization-service-accounts.d.ts +3 -0
  155. package/dist/routes/personal-api-keys.d.ts +3 -0
  156. package/dist/routes/session-admin-access.d.ts +3 -0
  157. package/dist/routes/session-artifact-associations.d.ts +4 -0
  158. package/dist/routes/session-history-imports.d.ts +20 -0
  159. package/dist/routes/sessions.d.ts +38 -2
  160. package/dist/routes/site-data.d.ts +18 -0
  161. package/dist/routes/site-publishing.d.ts +5 -0
  162. package/dist/routes/site-runtime.d.ts +31 -0
  163. package/dist/routes/site-sql.d.ts +16 -0
  164. package/dist/routes/slack-bot-access.d.ts +3 -0
  165. package/dist/routes/subscription-account-pools.d.ts +21 -0
  166. package/dist/routes/subscription-core-http.d.ts +5 -0
  167. package/dist/routes/subscription-pool-access.d.ts +37 -0
  168. package/dist/routes/subscription-pool-core.d.ts +153 -0
  169. package/dist/routes/supergrok.d.ts +6 -6
  170. package/dist/routes/usage-allowances.d.ts +12 -0
  171. package/dist/routes/variable-sets-removed.d.ts +2 -0
  172. package/dist/routes/vault-providers.d.ts +34 -0
  173. package/dist/routes/vaults.d.ts +3 -0
  174. package/dist/routes/workspace-integrations.d.ts +35 -0
  175. package/dist/routes/workspace-model-providers.d.ts +4 -0
  176. package/dist/routes/workspaces.d.ts +11 -2
  177. package/dist/sandbox/agent-update.d.ts +64 -0
  178. package/dist/sandbox/auth-callout.d.ts +8 -0
  179. package/dist/sandbox/channel-a.d.ts +15 -1
  180. package/dist/sandbox/enrollment.d.ts +26 -4
  181. package/dist/sandbox/machine-audience.d.ts +29 -0
  182. package/dist/sandbox/machines.d.ts +15 -2
  183. package/dist/sandbox/metrics-ingestion.d.ts +6 -2
  184. package/dist/sandbox/screen-control.d.ts +85 -0
  185. package/dist/sandbox/viewer.d.ts +5 -1
  186. package/dist/sandbox-file-artifacts.d.ts +17 -1
  187. package/dist/scheduled-task-blockers.d.ts +39 -0
  188. package/dist/scheduled-task-deletion.d.ts +3 -4
  189. package/dist/scheduled-task-lifecycle.d.ts +14 -0
  190. package/dist/secret-answers.d.ts +40 -0
  191. package/dist/session-create-observability.d.ts +5 -0
  192. package/dist/session-event-view-36GO3IL7.js +528 -0
  193. package/dist/session-event-view-36GO3IL7.js.map +1 -0
  194. package/dist/signed-state-secret.d.ts +18 -0
  195. package/dist/site-agent-guard.d.ts +53 -0
  196. package/dist/site-data/operations.d.ts +103 -0
  197. package/dist/site-data/service.d.ts +203 -0
  198. package/dist/site-frame-guard.d.ts +32 -0
  199. package/dist/site-publishing/agent-data-tools.d.ts +84 -0
  200. package/dist/site-publishing/agent-model.d.ts +58 -0
  201. package/dist/site-publishing/agent-session.d.ts +16 -0
  202. package/dist/site-publishing/content-scanner.d.ts +31 -0
  203. package/dist/site-publishing/edge-sync.d.ts +48 -0
  204. package/dist/site-publishing/gateway-signature.d.ts +56 -0
  205. package/dist/site-publishing/limits.d.ts +63 -0
  206. package/dist/site-publishing/page-tool-authority.d.ts +30 -0
  207. package/dist/site-publishing/platform-usage.d.ts +113 -0
  208. package/dist/site-publishing/platform.d.ts +21 -0
  209. package/dist/site-publishing/readiness.d.ts +62 -0
  210. package/dist/site-publishing/runtime.d.ts +179 -0
  211. package/dist/site-publishing/service.d.ts +264 -0
  212. package/dist/site-publishing/session-events.d.ts +22 -0
  213. package/dist/site-publishing/session-views.d.ts +36 -0
  214. package/dist/site-publishing/viewer.d.ts +40 -0
  215. package/dist/site-session-origin.d.ts +31 -6
  216. package/dist/site-sql/format.d.ts +47 -0
  217. package/dist/site-sql/gateway.d.ts +50 -0
  218. package/dist/site-sql/service.d.ts +213 -0
  219. package/dist/temporal-schedule-sync.d.ts +26 -0
  220. package/dist/transcription/billing-refusal.d.ts +8 -0
  221. package/dist/transcription/normalize.d.ts +23 -0
  222. package/dist/transcription/providers/azure-mai.d.ts +12 -0
  223. package/dist/transcription/providers/azure-openai.d.ts +4 -0
  224. package/dist/transcription/providers/openai.d.ts +16 -0
  225. package/dist/transcription/providers/subscription-core.d.ts +39 -0
  226. package/dist/transcription/service.d.ts +10 -1
  227. package/dist/transcription/wav-duration.d.ts +8 -0
  228. package/dist/workspace-tool-gateway-attestations.d.ts +37 -0
  229. package/dist/workspace-tool-gateway-observability.d.ts +16 -0
  230. package/dist/workspace-tool-gateway-secrets.d.ts +27 -0
  231. package/dist/workspace-tool-gateway.d.ts +102 -6
  232. package/dist/workspace-tool-target-fence.d.ts +116 -0
  233. package/dist/workspace-tool-target.d.ts +225 -0
  234. package/dist/workspace-webhook-dispatch.d.ts +26 -0
  235. package/dist/xai-subscription-core.d.ts +60 -0
  236. package/package.json +25 -20
  237. package/src/access-grant-rls.ts +64 -14
  238. package/src/admission-trace.ts +14 -0
  239. package/src/app.ts +1684 -252
  240. package/src/artifact-catalog-cursor.ts +1 -0
  241. package/src/attached-browser-viewer.ts +26 -0
  242. package/src/auth/managed-auth-attempt-context.ts +9 -0
  243. package/src/auth/managed-auth-rate-limits.ts +252 -0
  244. package/src/auth/managed-auth-session-adapter.ts +64 -29
  245. package/src/auth/managed-auth.ts +303 -35
  246. package/src/auth/managed-email.ts +18 -1
  247. package/src/auth/managed-sign-in-notifications.ts +81 -0
  248. package/src/auth/new-signups-gate.ts +49 -0
  249. package/src/auth/organization-user-setup.ts +3 -3
  250. package/src/auth/signup-funnel-metrics.ts +164 -0
  251. package/src/azure-live.ts +103 -0
  252. package/src/browser-action-fences.ts +45 -0
  253. package/src/browser-controller-authority.ts +1 -186
  254. package/src/browser-ephemeral.ts +39 -0
  255. package/src/browser-lost-runtime-end.ts +71 -0
  256. package/src/browser-state-authority.ts +1 -236
  257. package/src/claude-default-models.ts +37 -0
  258. package/src/claude-subscription-core.ts +154 -0
  259. package/src/claude-subscription-oauth.ts +352 -0
  260. package/src/claude-subscription-profile.ts +73 -0
  261. package/src/claude-subscription-usage.ts +51 -0
  262. package/src/claude-workspace-connection.ts +82 -0
  263. package/src/codemode.ts +236 -39
  264. package/src/codex-realtime.ts +181 -53
  265. package/src/codex-session-pointers.ts +80 -0
  266. package/src/company-brain-okf.ts +1 -1
  267. package/src/connected-machine-computer-access.ts +2 -1
  268. package/src/connection-ownership.ts +19 -13
  269. package/src/controller-data-plane.ts +34 -0
  270. package/src/credit-promotion-snapshot.ts +36 -0
  271. package/src/editable-artifact-production.ts +10 -0
  272. package/src/editable-artifact-source-session.ts +123 -0
  273. package/src/editable-artifact-websocket.ts +16 -3
  274. package/src/fatal-process-boundary.ts +67 -3
  275. package/src/gateway-realtime.ts +12 -8
  276. package/src/github-access.ts +31 -5
  277. package/src/github-browser-flow.ts +1 -1
  278. package/src/http/acting-person.ts +105 -0
  279. package/src/http/api-contract-compat.ts +49 -0
  280. package/src/http/api-error.ts +210 -2
  281. package/src/http/auth.ts +68 -4
  282. package/src/http/cors.ts +37 -5
  283. package/src/http/deprecation.ts +118 -0
  284. package/src/http/keyed-admission.ts +29 -0
  285. package/src/http/local-browser-boundary.ts +343 -0
  286. package/src/http/metrics-listener.ts +57 -0
  287. package/src/http/path-identifier.ts +54 -0
  288. package/src/http/registered-route-label.ts +34 -0
  289. package/src/http/rejection-telemetry.ts +120 -0
  290. package/src/http/request-body.ts +127 -0
  291. package/src/http/request-source.ts +154 -13
  292. package/src/http/sse.ts +88 -28
  293. package/src/http/unmatched-route.ts +60 -0
  294. package/src/http/user-content.ts +158 -0
  295. package/src/http/variable-sets-removed.ts +122 -0
  296. package/src/http/websocket-upgrade-dispatch.ts +29 -0
  297. package/src/index.ts +107 -57
  298. package/src/integration-connect-metrics.ts +228 -0
  299. package/src/integration-endpoint-test.ts +291 -0
  300. package/src/integrations/api-integrations.ts +81 -7
  301. package/src/integrations/atlassian.ts +202 -83
  302. package/src/integrations/fiken.ts +66 -38
  303. package/src/integrations/github-app-connect.ts +64 -18
  304. package/src/integrations/github-lens-connect.ts +8 -29
  305. package/src/integrations/google-drive.ts +448 -152
  306. package/src/integrations/integration-commit-authority.ts +69 -0
  307. package/src/integrations/marketplace-plugin.ts +1 -1
  308. package/src/integrations/oauth-client.ts +733 -193
  309. package/src/integrations/oauth-profiles.ts +116 -45
  310. package/src/integrations/onepassword-vault-provider.ts +16 -0
  311. package/src/integrations/personal-github-repositories.ts +1 -1
  312. package/src/integrations/personal-github.ts +61 -35
  313. package/src/integrations/provider-oauth.ts +67 -85
  314. package/src/integrations/slack-app-home.ts +5 -5
  315. package/src/integrations/slack-bot.ts +893 -201
  316. package/src/integrations/slack-file-upload-flow.ts +147 -0
  317. package/src/integrations/slack-install.ts +7 -7
  318. package/src/integrations/slack-interactions.ts +1589 -299
  319. package/src/integrations/slack-mrkdwn.ts +193 -0
  320. package/src/integrations/slack-routing.ts +2 -2
  321. package/src/integrations/slack-session-defaults.ts +224 -0
  322. package/src/integrations/slack-task-file-upload.ts +323 -0
  323. package/src/integrations/slack-tool-review.ts +65 -0
  324. package/src/integrations/social-oauth.ts +90 -58
  325. package/src/interaction-agent-access.ts +40 -20
  326. package/src/interaction-agent-version.ts +85 -0
  327. package/src/interaction-holder-heartbeat.ts +24 -8
  328. package/src/interaction-metrics.ts +164 -2
  329. package/src/interaction-route-stages.ts +94 -0
  330. package/src/interaction-secrets-vault.ts +213 -0
  331. package/src/interaction-secrets.ts +426 -0
  332. package/src/mcp/action-catalog-types.ts +14 -0
  333. package/src/mcp/action-catalog.gen.ts +8014 -0
  334. package/src/mcp/action-descriptions.ts +299 -0
  335. package/src/mcp/action-schema-imports.gen.ts +304 -0
  336. package/src/mcp/capability-account-readiness.ts +17 -0
  337. package/src/mcp/child-read-evidence.ts +19 -0
  338. package/src/mcp/company-profile-agent-admin.ts +13 -0
  339. package/src/mcp/contract-input.ts +165 -0
  340. package/src/mcp/documents.ts +9 -2
  341. package/src/mcp/editable-artifacts.ts +29 -11
  342. package/src/mcp/files.ts +5 -1
  343. package/src/mcp/first-party-tool-permissions.ts +7 -285
  344. package/src/mcp/knowledge-entries.ts +108 -34
  345. package/src/mcp/notification-tools.ts +480 -0
  346. package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
  347. package/src/mcp/scheduled-task-input.ts +89 -0
  348. package/src/mcp/scheduled-task-view.ts +63 -3
  349. package/src/mcp/secret-tools.ts +185 -0
  350. package/src/mcp/server.ts +2213 -1116
  351. package/src/mcp/session-admin-access-tools.ts +163 -0
  352. package/src/mcp/session-event-errors.ts +123 -0
  353. package/src/mcp/session-event-view.ts +395 -13
  354. package/src/mcp/session-view.ts +3 -0
  355. package/src/mcp/session-wait.ts +67 -7
  356. package/src/mcp/site-agent-clamp-tools.ts +140 -0
  357. package/src/mcp/site-data-tools.ts +339 -0
  358. package/src/mcp/site-publication-tools.ts +201 -0
  359. package/src/mcp/site-sql-tools.ts +427 -0
  360. package/src/mcp-oauth.ts +635 -45
  361. package/src/model-calls/chat-completions.ts +352 -0
  362. package/src/model-calls/pending-usage-settlements.ts +125 -0
  363. package/src/model-calls/service.ts +694 -0
  364. package/src/model-catalog.ts +68 -17
  365. package/src/native-push-dispatch.ts +457 -0
  366. package/src/observability.ts +15 -1
  367. package/src/organization-mcp.ts +485 -0
  368. package/src/prepared-mcp-actor.ts +154 -0
  369. package/src/prepared-mcp-permissions.ts +38 -0
  370. package/src/routes/analytics-consent.ts +123 -0
  371. package/src/routes/api-integrations.ts +268 -9
  372. package/src/routes/api-keys.ts +164 -25
  373. package/src/routes/artifact-catalog.ts +111 -0
  374. package/src/routes/automations.ts +163 -90
  375. package/src/routes/billing.ts +535 -59
  376. package/src/routes/browser-sessions.ts +966 -342
  377. package/src/routes/canonical-human-identities.ts +61 -0
  378. package/src/routes/capabilities.ts +112 -5
  379. package/src/routes/channels.ts +4 -3
  380. package/src/routes/chat-completions.ts +235 -0
  381. package/src/routes/claude-core-usage.ts +147 -0
  382. package/src/routes/claude-subscription-accounts.ts +233 -0
  383. package/src/routes/claude-subscription-oauth.ts +175 -0
  384. package/src/routes/client-errors.ts +394 -0
  385. package/src/routes/codex-core.ts +687 -0
  386. package/src/routes/codex.ts +1277 -1119
  387. package/src/routes/company-brain.ts +4 -0
  388. package/src/routes/computer-sessions.ts +319 -85
  389. package/src/routes/connect.ts +277 -175
  390. package/src/routes/connections.ts +690 -363
  391. package/src/routes/desktop-distribution.ts +217 -0
  392. package/src/routes/documents.ts +13 -7
  393. package/src/routes/editable-artifacts.ts +74 -4
  394. package/src/routes/enrollments.ts +110 -16
  395. package/src/routes/external-identity-links.ts +13 -12
  396. package/src/routes/files.ts +41 -16
  397. package/src/routes/github-browser-fonts.gen.ts +12 -0
  398. package/src/routes/github-browser-pages.ts +258 -0
  399. package/src/routes/github.ts +200 -75
  400. package/src/routes/inbox.ts +353 -0
  401. package/src/routes/insights-response-cache.ts +83 -0
  402. package/src/routes/insights-usage.ts +345 -0
  403. package/src/routes/insights.ts +103 -8
  404. package/src/routes/install.ts +51 -10
  405. package/src/routes/integration-facets.ts +70 -28
  406. package/src/routes/interaction-placement-message.ts +12 -0
  407. package/src/routes/knowledge.ts +136 -27
  408. package/src/routes/machines.ts +189 -135
  409. package/src/routes/managed-auth-session-sets.ts +43 -10
  410. package/src/routes/managed-onboarding.ts +39 -19
  411. package/src/routes/managed-sign-in-methods.ts +433 -0
  412. package/src/routes/memory-slack-publications.ts +3 -2
  413. package/src/routes/model-account-choice.ts +194 -0
  414. package/src/routes/model-account-settings.ts +235 -0
  415. package/src/routes/model-connection-access.ts +210 -48
  416. package/src/routes/native-app-auth.ts +296 -0
  417. package/src/routes/organization-integration-policy.ts +116 -0
  418. package/src/routes/organization-integrations.ts +326 -0
  419. package/src/routes/organization-mcp-connections.ts +183 -0
  420. package/src/routes/organization-memberships.ts +205 -19
  421. package/src/routes/organization-model-defaults.ts +110 -0
  422. package/src/routes/organization-model-providers.ts +74 -3
  423. package/src/routes/organization-service-accounts.ts +128 -0
  424. package/src/routes/organization-sessions.ts +21 -5
  425. package/src/routes/personal-api-keys.ts +184 -0
  426. package/src/routes/personal-github-git-broker.ts +2 -2
  427. package/src/routes/personal-github.ts +9 -4
  428. package/src/routes/plugins.ts +46 -7
  429. package/src/routes/pr-review-github.ts +105 -118
  430. package/src/routes/pr-review.ts +88 -82
  431. package/src/routes/preference-registry.ts +18 -0
  432. package/src/routes/rigs.ts +48 -13
  433. package/src/routes/scheduled-tasks.ts +347 -27
  434. package/src/routes/session-admin-access.ts +271 -0
  435. package/src/routes/session-artifact-associations.ts +164 -0
  436. package/src/routes/session-history-imports.ts +305 -0
  437. package/src/routes/sessions.ts +1682 -363
  438. package/src/routes/site-data.ts +340 -0
  439. package/src/routes/site-publishing.ts +584 -0
  440. package/src/routes/site-runtime.ts +1944 -0
  441. package/src/routes/site-sql.ts +263 -0
  442. package/src/routes/skill-content.ts +72 -7
  443. package/src/routes/skills.ts +5 -4
  444. package/src/routes/slack-bot-access.ts +108 -0
  445. package/src/routes/social.ts +27 -15
  446. package/src/routes/subscription-account-pools.ts +209 -0
  447. package/src/routes/subscription-core-http.ts +18 -0
  448. package/src/routes/subscription-pool-access.ts +180 -0
  449. package/src/routes/subscription-pool-core.ts +552 -0
  450. package/src/routes/supergrok.ts +174 -655
  451. package/src/routes/transcription-recordings.ts +71 -4
  452. package/src/routes/transcriptions.ts +26 -2
  453. package/src/routes/usage-allowances.ts +381 -0
  454. package/src/routes/variable-sets-removed.ts +19 -0
  455. package/src/routes/vault-providers.ts +782 -0
  456. package/src/routes/vaults.ts +421 -0
  457. package/src/routes/video-generation.ts +30 -12
  458. package/src/routes/workspace-artifacts.ts +165 -53
  459. package/src/routes/workspace-integrations.ts +576 -0
  460. package/src/routes/workspace-model-providers.ts +148 -0
  461. package/src/routes/workspace-state.ts +4 -0
  462. package/src/routes/workspaces.ts +625 -117
  463. package/src/sandbox/agent-update.ts +315 -0
  464. package/src/sandbox/auth-callout.ts +104 -21
  465. package/src/sandbox/channel-a.ts +150 -49
  466. package/src/sandbox/enrollment.ts +131 -22
  467. package/src/sandbox/machine-audience.ts +192 -0
  468. package/src/sandbox/machines.ts +75 -31
  469. package/src/sandbox/metrics-ingestion.ts +133 -2
  470. package/src/sandbox/rematerialize.ts +51 -21
  471. package/src/sandbox/screen-control.ts +289 -0
  472. package/src/sandbox/viewer.ts +33 -58
  473. package/src/sandbox-file-artifacts.ts +53 -27
  474. package/src/scheduled-task-blockers.ts +293 -0
  475. package/src/scheduled-task-deletion.ts +13 -3
  476. package/src/scheduled-task-lifecycle.ts +84 -0
  477. package/src/secret-answers.ts +119 -0
  478. package/src/session-create-observability.ts +46 -0
  479. package/src/session-realtime-context.ts +26 -4
  480. package/src/signed-state-secret.ts +58 -0
  481. package/src/site-agent-guard.ts +261 -0
  482. package/src/site-data/operations.ts +777 -0
  483. package/src/site-data/service.ts +1043 -0
  484. package/src/site-frame-guard.ts +107 -0
  485. package/src/site-publishing/agent-data-tools.ts +250 -0
  486. package/src/site-publishing/agent-model.ts +127 -0
  487. package/src/site-publishing/agent-session.ts +148 -0
  488. package/src/site-publishing/content-scanner.ts +93 -0
  489. package/src/site-publishing/edge-sync.ts +148 -0
  490. package/src/site-publishing/gateway-signature.ts +198 -0
  491. package/src/site-publishing/limits.ts +315 -0
  492. package/src/site-publishing/page-tool-authority.ts +111 -0
  493. package/src/site-publishing/platform-usage.ts +479 -0
  494. package/src/site-publishing/platform.ts +98 -0
  495. package/src/site-publishing/readiness.ts +330 -0
  496. package/src/site-publishing/runtime.ts +831 -0
  497. package/src/site-publishing/service.ts +1282 -0
  498. package/src/site-publishing/session-events.ts +262 -0
  499. package/src/site-publishing/session-views.ts +262 -0
  500. package/src/site-publishing/viewer.ts +75 -0
  501. package/src/site-session-origin.ts +91 -6
  502. package/src/site-sql/format.ts +134 -0
  503. package/src/site-sql/gateway.ts +168 -0
  504. package/src/site-sql/service.ts +1636 -0
  505. package/src/site-uploads.ts +27 -5
  506. package/src/temporal-schedule-sync.ts +74 -0
  507. package/src/transcription/billing-refusal.ts +14 -0
  508. package/src/transcription/normalize.ts +77 -0
  509. package/src/transcription/providers/azure-mai.ts +98 -0
  510. package/src/transcription/providers/azure-openai.ts +16 -7
  511. package/src/transcription/providers/codex-subscription.ts +190 -67
  512. package/src/transcription/providers/openai.ts +79 -5
  513. package/src/transcription/providers/subscription-core.ts +125 -0
  514. package/src/transcription/providers/xai-subscription.ts +81 -83
  515. package/src/transcription/segmenter.ts +7 -1
  516. package/src/transcription/service.ts +239 -10
  517. package/src/transcription/wav-duration.ts +39 -0
  518. package/src/workspace-deletion.ts +25 -1
  519. package/src/workspace-tool-gateway-attestations.ts +133 -0
  520. package/src/workspace-tool-gateway-observability.ts +64 -0
  521. package/src/workspace-tool-gateway-secrets.ts +104 -0
  522. package/src/workspace-tool-gateway.ts +921 -103
  523. package/src/workspace-tool-target-fence.ts +380 -0
  524. package/src/workspace-tool-target.ts +554 -0
  525. package/src/workspace-webhook-dispatch.ts +225 -0
  526. package/src/xai-realtime.ts +85 -81
  527. package/src/xai-subscription-core.ts +172 -0
  528. package/dist/browser-auth-broker.d.ts +0 -20
  529. package/dist/chunk-AJ62T5QN.js.map +0 -1
  530. package/dist/connection-authority-owner.d.ts +0 -59
  531. package/dist/routes/connection-authorities.d.ts +0 -3
  532. package/dist/routes/environments.d.ts +0 -5
  533. package/dist/routes/host-mcp-bindings.d.ts +0 -5
  534. package/dist/routes/host-mcp-resolvers.d.ts +0 -3
  535. package/dist/routes/packs.d.ts +0 -3
  536. package/dist/session-event-view-MU3HMNV3.js +0 -264
  537. package/dist/session-event-view-MU3HMNV3.js.map +0 -1
  538. package/dist/xai-subscription-auth.d.ts +0 -23
  539. package/src/browser-auth-broker.ts +0 -155
  540. package/src/connection-authority-owner.ts +0 -61
  541. package/src/routes/connection-authorities.ts +0 -139
  542. package/src/routes/environments.ts +0 -388
  543. package/src/routes/host-mcp-bindings.ts +0 -122
  544. package/src/routes/host-mcp-resolvers.ts +0 -41
  545. package/src/routes/packs.ts +0 -784
  546. package/src/xai-subscription-auth.ts +0 -139
@@ -1,15 +1,24 @@
1
1
  import { safeReturnPath } from "./oauth-return-path";
2
+ import { assertOrganizationIntegrationAllowed, stableJson } from "@opengeni/contracts";
3
+ import {
4
+ withOrganizationIntegrationAcquisition,
5
+ withOrganizationIntegrationPolicyFence,
6
+ } from "@opengeni/db/organization-integration-policy";
2
7
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
8
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
9
  import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
5
- import { parseIntegrationsOauthClientsJson, type Settings } from "@opengeni/config";
10
+ import {
11
+ findIntegrationsOauthClient,
12
+ parseIntegrationsOauthClientsJson,
13
+ type Settings,
14
+ } from "@opengeni/config";
6
15
  import {
7
16
  OAuthStartResponse,
8
17
  selectCanonicalPersonalSlackConnection,
9
18
  type ConnectionOwnership,
10
19
  type OAuthStartRequest,
11
20
  } from "@opengeni/contracts";
12
- import { requireEnvironmentEncryption } from "@opengeni/core";
21
+ import { requireEnvironmentEncryption, integrationKeyForConnectProvider } from "@opengeni/core";
13
22
  import { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
14
23
  import type { Observability } from "@opengeni/observability";
15
24
  import {
@@ -24,14 +33,16 @@ import {
24
33
  getGlobalCatalogOAuthProfile,
25
34
  listConnectionsMetadata,
26
35
  loadIntegrationOAuthClient,
36
+ loadIntegrationOAuthPendingState,
27
37
  normalizeBearerScheme,
28
38
  replaceIntegrationOAuthClientIfCurrent,
29
39
  storeIntegrationOAuthClient,
40
+ storeIntegrationOAuthPendingState,
30
41
  updateConnection,
31
42
  withDatabaseStatementTimeout,
32
43
  type Database,
33
44
  } from "@opengeni/db";
34
- import { createSignedState, readSignedState } from "@opengeni/github";
45
+ import { createSignedState, inspectSignedState, readSignedState } from "@opengeni/github";
35
46
  import {
36
47
  DestinationPolicyError,
37
48
  McpOAuthDiscoveryError,
@@ -50,8 +61,9 @@ import {
50
61
  type McpProtectedResourceMetadata,
51
62
  } from "@opengeni/network";
52
63
  import { Buffer } from "node:buffer";
53
- import { createHash, randomBytes } from "node:crypto";
64
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
54
65
  import { HTTPException } from "hono/http-exception";
66
+ import { observeOAuthStart } from "../integration-connect-metrics";
55
67
  import {
56
68
  assertConnectionOwnershipAllowedForPrincipal,
57
69
  personalOwnerStateAccepted,
@@ -63,11 +75,11 @@ import { ApiHttpError } from "../http/api-error";
63
75
  import { requireConnectOwnerAuthority } from "./connect-authority";
64
76
  import {
65
77
  DEFAULT_OAUTH_PROFILE,
78
+ OFFICIAL_GMAIL_MCP_URL,
66
79
  DEPLOYMENT_MANAGED_CLIENTS,
67
80
  builtInOAuthProfileByKey,
68
81
  assertAuthorizationServerNotReserved,
69
82
  assertAuthorizationServerPins,
70
- assertOwnershipAllowed,
71
83
  builtInOAuthProfileFor,
72
84
  catalogMcpUrlKey,
73
85
  defaultOwnershipFor,
@@ -79,6 +91,64 @@ import {
79
91
  import { canonicalProviderDomain } from "./provider-domain";
80
92
 
81
93
  export const oauthStateTtlMs = 10 * 60 * 1000;
94
+
95
+ /**
96
+ * Workspace-free landing for a callback whose state names no trustworthy
97
+ * workspace. The web app resolves it to the viewer's current workspace
98
+ * integrations page and keeps the callback outcome parameters.
99
+ */
100
+ export const INTEGRATIONS_FALLBACK_PATH = "/integrations";
101
+
102
+ const WORKSPACE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
103
+
104
+ /** The web integrations page for one workspace (the Plugins surface). */
105
+ export function workspaceIntegrationsPath(workspaceId: string): string {
106
+ return `/workspaces/${encodeURIComponent(workspaceId)}/plugins`;
107
+ }
108
+
109
+ /**
110
+ * The integrations page for a workspace id read from untrusted input (a URL
111
+ * parameter or an unverified-age state), or null unless it is a workspace UUID.
112
+ * A link target only: it grants nothing.
113
+ */
114
+ export function workspaceIntegrationsPathForUntrusted(candidate: unknown): string | null {
115
+ return typeof candidate === "string" && WORKSPACE_ID_PATTERN.test(candidate)
116
+ ? workspaceIntegrationsPath(candidate)
117
+ : null;
118
+ }
119
+
120
+ /**
121
+ * Where a callback returns, and why, when its own signed state is unusable.
122
+ *
123
+ * A correctly signed state still names its workspace, even when it is too old
124
+ * or its flow rejected it, so the browser returns to that workspace's
125
+ * integrations page; an aged one is reported as expired. Anything unsigned,
126
+ * tampered, or signed with another secret returns to
127
+ * {@link INTEGRATIONS_FALLBACK_PATH}. Display routing only: nothing here
128
+ * authorizes, resumes, or replays the callback.
129
+ */
130
+ export function oauthStateFailureReturn(
131
+ settings: Settings,
132
+ rawState: string | undefined,
133
+ nowMs = Date.now(),
134
+ ): { returnPath: string; reason: "state_expired" | "state_invalid" } {
135
+ let payload: ReturnType<typeof inspectSignedState> = null;
136
+ try {
137
+ payload = rawState
138
+ ? inspectSignedState(rawState, requireIntegrationsStateSecret(settings))
139
+ : null;
140
+ } catch {
141
+ payload = null;
142
+ }
143
+ return {
144
+ returnPath:
145
+ workspaceIntegrationsPathForUntrusted(payload?.workspaceId) ?? INTEGRATIONS_FALLBACK_PATH,
146
+ reason:
147
+ payload !== null && nowMs - payload.iat * 1000 > oauthStateTtlMs
148
+ ? "state_expired"
149
+ : "state_invalid",
150
+ };
151
+ }
82
152
  export {
83
153
  OFFICIAL_GMAIL_MCP_SCOPES,
84
154
  OFFICIAL_GMAIL_MCP_URL,
@@ -95,6 +165,8 @@ type OAuthClientDeps = {
95
165
  };
96
166
 
97
167
  export type OAuthStartContext = {
168
+ /** Server-selected dedicated adapter identity; never copied from request metadata. */
169
+ integrationKey?: "gmail" | "slack-personal";
98
170
  connectAttemptId?: string;
99
171
  externalContinuation?: ExternalActorContinuation;
100
172
  accountId: string;
@@ -118,6 +190,7 @@ export type OAuthCallbackResult = {
118
190
  type WwwAuthenticateChallenge = McpOAuthChallenge;
119
191
  type ProtectedResourceMetadata = McpProtectedResourceMetadata;
120
192
  type AuthorizationServerMetadata = McpAuthorizationServerMetadata;
193
+ type OAuthDiscoveryMode = McpOAuthDiscoveryMode | "provider_oauth_metadata";
121
194
 
122
195
  type OAuthClientRegistration = {
123
196
  method: "operator" | "manual" | "cimd" | "dcr";
@@ -129,6 +202,7 @@ type OAuthClientRegistration = {
129
202
  };
130
203
 
131
204
  type OAuthStatePayload = {
205
+ integrationKey?: "gmail" | "slack-personal";
132
206
  connectAttemptId?: string;
133
207
  externalContinuation?: ExternalActorContinuation;
134
208
  returnUrl?: string;
@@ -148,7 +222,7 @@ type OAuthStatePayload = {
148
222
  tokenEndpoint: string;
149
223
  authorizationServer: string;
150
224
  issuer: string;
151
- discoveryMode: McpOAuthDiscoveryMode;
225
+ discoveryMode: OAuthDiscoveryMode;
152
226
  discoveryMetadataSha256?: string;
153
227
  protectedResourceMetadataUrl?: string;
154
228
  authorizationServerMetadataUrl?: string;
@@ -188,7 +262,8 @@ export type OAuthStartStage =
188
262
  | "mcp_challenge"
189
263
  | "protected_resource_metadata"
190
264
  | "authorization_server_metadata"
191
- | "client_registration";
265
+ | "client_registration"
266
+ | "state_persist";
192
267
 
193
268
  class OAuthStartStageError extends Error {
194
269
  constructor(
@@ -312,19 +387,107 @@ class OAuthCallbackDeadline {
312
387
  }
313
388
  }
314
389
 
390
+ /** Policy is the outermost lock. Receipt replay still runs normal authorization,
391
+ * but the DB calls authorizeAcquisition only for uncommitted progress. Provider
392
+ * requests must happen after this transaction, never inside either wrapper. */
393
+ export async function claimOAuthAcquisition(
394
+ db: Database,
395
+ scope: Parameters<typeof claimConnectOperation>[1],
396
+ input: Parameters<typeof claimConnectOperation>[2],
397
+ integrationKey: string | null,
398
+ acquiring = true,
399
+ ) {
400
+ if (!acquiring) return claimConnectOperation(db, scope, input);
401
+ return withOrganizationIntegrationPolicyFence(db, scope, (tx, policy) =>
402
+ claimConnectOperation(tx, scope, {
403
+ ...input,
404
+ authorizeAcquisition: async (locked, attempt) => {
405
+ assertOrganizationIntegrationAllowed(policy, integrationKey);
406
+ await input.authorizeAcquisition?.(locked, attempt);
407
+ },
408
+ }),
409
+ );
410
+ }
411
+
412
+ type IntegrationSourceSelection = Record<string, unknown> & {
413
+ id: string;
414
+ syncEnabled: boolean;
415
+ readPolicy: "allow" | "ask" | "block";
416
+ };
417
+
418
+ /** Compare server-bound selections, not provider classification. Removing a
419
+ * source, disabling sync, or narrowing its read permission acquires no authority.
420
+ * Generation/timestamp bookkeeping does not turn an unchanged save into setup. */
421
+ export function integrationSourceSelectionRequiresAcquisition(
422
+ previous: readonly IntegrationSourceSelection[],
423
+ requested: readonly IntegrationSourceSelection[],
424
+ ): boolean {
425
+ const rank = { block: 0, ask: 1, allow: 2 };
426
+ const config = (source: IntegrationSourceSelection) => {
427
+ const {
428
+ selectedAt: _selectedAt,
429
+ configGeneration: _generation,
430
+ syncEnabled: _enabled,
431
+ readPolicy: _readPolicy,
432
+ ...binding
433
+ } = source;
434
+ return stableJson(binding);
435
+ };
436
+ return requested.some((source) => {
437
+ const existing = previous.find((candidate) => candidate.id === source.id);
438
+ return (
439
+ !existing ||
440
+ (!existing.syncEnabled && source.syncEnabled) ||
441
+ rank[source.readPolicy] > rank[existing.readPolicy] ||
442
+ config(existing) !== config(source)
443
+ );
444
+ });
445
+ }
446
+
447
+ export async function finishOAuthAcquisition(
448
+ db: Database,
449
+ scope: Parameters<typeof finishConnectOperation>[1],
450
+ input: Parameters<typeof finishConnectOperation>[2],
451
+ integrationKey: string | null,
452
+ ) {
453
+ return withOrganizationIntegrationPolicyFence(db, scope, (tx, policy) =>
454
+ finishConnectOperation(tx, scope, {
455
+ ...input,
456
+ authorizeAcquisition: async (locked, attempt) => {
457
+ assertOrganizationIntegrationAllowed(policy, integrationKey);
458
+ await input.authorizeAcquisition?.(locked, attempt);
459
+ },
460
+ }),
461
+ );
462
+ }
463
+
315
464
  export async function startMcpOAuth(
316
465
  deps: OAuthClientDeps,
317
466
  context: OAuthStartContext,
318
467
  ): Promise<OAuthStartResponse> {
468
+ await withOrganizationIntegrationAcquisition(
469
+ deps.db,
470
+ context,
471
+ [context.integrationKey ?? "custom:mcp"],
472
+ async () => {},
473
+ );
319
474
  const deadline = new OAuthStartDeadline(deps.oauthStartDeadlineMs ?? OAUTH_START_DEADLINE_MS);
320
475
  try {
321
- return await startMcpOAuthWithinDeadline(deps, context, deadline);
476
+ const started = await startMcpOAuthWithinDeadline(deps, context, deadline);
477
+ observeOAuthStart(deps.observability, { flow: "mcp_oauth", outcome: "success" });
478
+ return started;
322
479
  } catch (error) {
323
480
  const staged =
324
481
  error instanceof OAuthStartStageError
325
482
  ? error
326
483
  : new OAuthStartStageError("connection_lookup", oauthStartFailureReason(error), error);
327
484
  logOAuthStartFailure(deps.observability, staged);
485
+ observeOAuthStart(deps.observability, {
486
+ flow: "mcp_oauth",
487
+ outcome: "failure",
488
+ stage: staged.stage,
489
+ reason: staged.reason,
490
+ });
328
491
  throw oauthStartApiError(staged);
329
492
  } finally {
330
493
  deadline.dispose();
@@ -375,13 +538,13 @@ async function startMcpOAuthWithinDeadline(
375
538
  })) ??
376
539
  DEFAULT_OAUTH_PROFILE;
377
540
  assertOAuthStartProfile(settings, context.payload, mcpUrl, profile);
378
- // Personal-only resources default an omitted ownership to personal, matching
379
- // the fence that existed before #1240; an explicit non-personal value is the
380
- // only thing rejected. Everything else defaults to workspace as before.
541
+ // Catalog defaults are setup preferences; an explicit ownership always wins.
381
542
  const requestedOwnership: ConnectionOwnership =
382
543
  context.payload.ownership ?? defaultOwnershipFor(profile);
383
- assertOwnershipAllowed(profile, requestedOwnership);
384
- const returnPath = safeReturnPath(context.payload.returnPath ?? "/integrations");
544
+ assertProfileOwnership(profile, requestedOwnership);
545
+ const returnPath = safeReturnPath(
546
+ context.payload.returnPath ?? workspaceIntegrationsPath(context.workspaceId),
547
+ );
385
548
  const baseUrl = integrationBaseUrl(settings.publicBaseUrl, context.requestUrl);
386
549
  const redirectUri = `${baseUrl}/v1/integrations/oauth/callback`;
387
550
  const metadataUrl = `${baseUrl}/v1/integrations/oauth/client-metadata.json`;
@@ -394,6 +557,7 @@ async function startMcpOAuthWithinDeadline(
394
557
  connectionSelection: profile.connectionSelection,
395
558
  exactMcpBinding: profile.exactMcpBinding,
396
559
  connectionId: context.payload.connectionId,
560
+ newAccount: context.payload.newAccount === true,
397
561
  requestedOwnership: context.payload.ownership,
398
562
  newConnectionOwnership: requestedOwnership,
399
563
  }),
@@ -404,18 +568,17 @@ async function startMcpOAuthWithinDeadline(
404
568
  const ownership = existing
405
569
  ? ownershipForConnection(existing.subjectId, context.subjectId)
406
570
  : requestedOwnership;
407
- // A reconnect of a legacy row must not renew an ownership the profile no
408
- // longer allows (e.g. shared authority over one human's hosted-Slack grant).
409
- assertOwnershipAllowed(profile, ownership);
571
+ assertProfileOwnership(profile, ownership);
410
572
  // Only a managed human can own a personal Connection: personal-authority
411
573
  // execution resolves through a delegation snapshot frozen on a human's causal
412
574
  // turn, and migration 0256 can mint the `user` authority scope only for a
413
- // subject that holds an active organization membership. For a personal-only
414
- // profile (Gmail, hosted Slack MCP) this refuses the whole flow rather than
415
- // silently downgrading to the workspace ownership those profiles forbid.
575
+ // subject that holds an active organization membership. An explicit personal
576
+ // choice is never silently downgraded to workspace ownership.
416
577
  assertConnectionOwnershipAllowedForPrincipal(ownership, context.personalOwnershipAllowed);
417
578
 
418
- const discovery = await discoverMcpOAuth(mcpUrl, settings, deadline);
579
+ const discovery = profile.providerOAuthDiscovery
580
+ ? await discoverProviderOAuth(mcpUrl, settings, deadline, profile)
581
+ : await discoverMcpOAuth(mcpUrl, settings, deadline);
419
582
  assertDiscoveredAuthorizationServer(settings, discovery.as, profile, providerDomain);
420
583
  const resourceParameterSupported =
421
584
  discovery.mode === "rfc9728_protected_resource" && profile.sendResourceParameter;
@@ -427,6 +590,7 @@ async function startMcpOAuthWithinDeadline(
427
590
  requested: context.payload.requestedScopes,
428
591
  challenged: discovery.challenge.scope,
429
592
  supported: discovery.prm.scopesSupported,
593
+ authorizationServerScopesSupported: stringArray(discovery.as.raw.scopes_supported),
430
594
  });
431
595
  const client = await deadline.run("client_registration", (signal) =>
432
596
  registerOAuthClient(
@@ -439,10 +603,26 @@ async function startMcpOAuthWithinDeadline(
439
603
  context.payload.oauthClient,
440
604
  profile,
441
605
  signal,
606
+ context,
442
607
  ),
443
608
  );
609
+ if (
610
+ profile.confidentialClientRequired &&
611
+ (!isConfidentialOAuthClient(client) ||
612
+ !discovery.as.tokenEndpointAuthMethodsSupported.includes(client.tokenEndpointAuthMethod))
613
+ ) {
614
+ throw new OAuthStartStageError(
615
+ "client_registration",
616
+ "configuration_missing",
617
+ new HTTPException(503, {
618
+ message:
619
+ "Gmail OAuth requires a registered Google Web application client with a secret and supported token authentication",
620
+ }),
621
+ );
622
+ }
444
623
  const key = requireEnvironmentEncryption(settings);
445
- const state = createSignedState(requireIntegrationsStateSecret(settings), {
624
+ const fullState = createSignedState(requireIntegrationsStateSecret(settings), {
625
+ ...(context.integrationKey ? { integrationKey: context.integrationKey } : {}),
446
626
  ...(context.connectAttemptId ? { connectAttemptId: context.connectAttemptId } : {}),
447
627
  accountId: context.accountId,
448
628
  workspaceId: context.workspaceId,
@@ -491,6 +671,28 @@ async function startMcpOAuthWithinDeadline(
491
671
  returnPath,
492
672
  ...(existing ? { connectionId: existing.id, connectionVersion: existing.version } : {}),
493
673
  });
674
+ const expiresAt = new Date(Date.now() + oauthStateTtlMs);
675
+ // Providers cap `state` length (Stripe at 1024 characters), so only a short
676
+ // signed reference leaves; the full context waits encrypted at rest.
677
+ const stateId = randomUUID();
678
+ const state = createSignedState(requireIntegrationsStateSecret(settings), {
679
+ kind: "mcp_oauth_reference",
680
+ id: stateId,
681
+ accountId: context.accountId,
682
+ workspaceId: context.workspaceId,
683
+ });
684
+ if (state.length > 512) {
685
+ throw new Error("MCP OAuth reference state exceeded its provider-safe length");
686
+ }
687
+ await deadline.run("state_persist", async () =>
688
+ storeIntegrationOAuthPendingState(db, {
689
+ id: stateId,
690
+ accountId: context.accountId,
691
+ workspaceId: context.workspaceId,
692
+ stateEncrypted: encryptEnvironmentValue(key, fullState),
693
+ expiresAt,
694
+ }),
695
+ );
494
696
  const authorizationUrl = buildAuthorizationUrl({
495
697
  endpoint: discovery.as.authorizationEndpoint,
496
698
  settings,
@@ -506,36 +708,10 @@ async function startMcpOAuthWithinDeadline(
506
708
  return OAuthStartResponse.parse({
507
709
  state,
508
710
  authorizationUrl,
509
- expiresAt: new Date(Date.now() + oauthStateTtlMs).toISOString(),
711
+ expiresAt: expiresAt.toISOString(),
510
712
  });
511
713
  }
512
714
 
513
- /** Ownership fence for the official Gmail profile, keyed by exact MCP URL. */
514
- export function assertOfficialGmailPersonalOwnership(
515
- mcpUrl: string,
516
- ownership: ConnectionOwnership,
517
- ): void {
518
- const profile = builtInOAuthProfileFor({ mcpUrl });
519
- if (profile?.key === "official-gmail") {
520
- assertOwnershipAllowed(profile, ownership);
521
- }
522
- }
523
-
524
- /**
525
- * Slack's hosted MCP issues user tokens only. Sharing one human's grant as
526
- * workspace authority made every shared agent act as a named employee; the
527
- * OpenGeni workspace bot owns shared Slack access instead (bot-token search
528
- * covers public channels, files, and users).
529
- */
530
- export function assertHostedSlackMcpPersonalOwnership(
531
- hostedSlackMcp: boolean,
532
- ownership: ConnectionOwnership,
533
- ): void {
534
- if (hostedSlackMcp) {
535
- assertOwnershipAllowed(builtInOAuthProfileByKey("hosted-slack-mcp"), ownership);
536
- }
537
- }
538
-
539
715
  export function isHostedSlackMcpTarget(providerDomain: string, mcpUrl: string): boolean {
540
716
  return builtInOAuthProfileFor({ mcpUrl, providerDomain })?.key === "hosted-slack-mcp";
541
717
  }
@@ -603,6 +779,8 @@ export async function completeMcpOAuthCallback(
603
779
  input: {
604
780
  code?: string | undefined;
605
781
  state?: string | undefined;
782
+ /** The provider's `error` parameter, for example `access_denied` on Cancel. */
783
+ error?: string | undefined;
606
784
  requestUrl: string;
607
785
  },
608
786
  ): Promise<OAuthCallbackResult> {
@@ -621,12 +799,14 @@ async function completeMcpOAuthCallbackWithinDeadline(
621
799
  input: {
622
800
  code?: string | undefined;
623
801
  state?: string | undefined;
802
+ error?: string | undefined;
624
803
  requestUrl: string;
625
804
  },
626
805
  deadline: OAuthCallbackDeadline,
627
806
  ): Promise<OAuthCallbackResult> {
628
807
  const { db, settings, observability } = deps;
629
808
  let state: OAuthStatePayload | null = null;
809
+ let integrationKey: string | null = "custom:mcp";
630
810
  let connectOperation: { attemptId: string; operationId: string; inputDigest: string } | undefined;
631
811
  if (!input.state) {
632
812
  const error = new OAuthCallbackStageError(
@@ -636,18 +816,29 @@ async function completeMcpOAuthCallbackWithinDeadline(
636
816
  );
637
817
  logOAuthCallbackFailure(observability, error, state);
638
818
  return {
639
- redirectTo: callbackReturnPath("/integrations", "error", {
819
+ redirectTo: callbackReturnPath(INTEGRATIONS_FALLBACK_PATH, "error", {
640
820
  stage: error.stage,
641
821
  reason: error.reason,
642
822
  }),
643
823
  };
644
824
  }
645
825
  try {
646
- state = readOAuthState(input.state, settings);
826
+ state = await resolveMcpOAuthState(db, settings, input.state, deadline);
827
+ integrationKey = state.integrationKey ?? "custom:mcp";
647
828
  if (state.connectAttemptId) {
648
829
  const stored = await getConnectAttempt(db, state, state.connectAttemptId);
830
+ // Native Gmail setup uses its own provider id, but shares this callback.
831
+ // Bind that id to the exact reviewed Gmail destination before
832
+ // claiming an operation (including receipt replay), not to arbitrary MCP.
833
+ const gmailAttemptMatches =
834
+ stored.attempt.providerId === "gmail" &&
835
+ state.mcpUrl === OFFICIAL_GMAIL_MCP_URL &&
836
+ state.providerDomain === "gmailmcp.googleapis.com" &&
837
+ builtInOAuthProfileFor(state)?.key === "official-gmail" &&
838
+ personalOwnerStateAccepted(state);
649
839
  if (
650
- !["mcp-oauth", "slack-personal"].includes(stored.attempt.providerId) ||
840
+ (!["mcp-oauth", "slack-personal"].includes(stored.attempt.providerId) &&
841
+ !gmailAttemptMatches) ||
651
842
  stored.attempt.ownership !== state.ownership ||
652
843
  stored.returnUrl !== state.returnUrl
653
844
  )
@@ -657,29 +848,23 @@ async function completeMcpOAuthCallbackWithinDeadline(
657
848
  operationId: `oauth:${state.nonce}`,
658
849
  inputDigest: createHash("sha256").update(input.state).digest("hex"),
659
850
  };
660
- const claim = await claimConnectOperation(db, state, {
661
- ...connectOperation,
662
- expectedRevision: stored.attempt.revision,
663
- authorize: (tx, _attempt, origin) =>
664
- requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
665
- });
851
+ integrationKey = integrationKeyForConnectProvider(stored.attempt.providerId);
852
+ const claim = await claimOAuthAcquisition(
853
+ db,
854
+ state,
855
+ {
856
+ ...connectOperation,
857
+ expectedRevision: stored.attempt.revision,
858
+ authorize: (tx, _attempt, origin) =>
859
+ requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
860
+ },
861
+ integrationKey,
862
+ Boolean(input.code),
863
+ );
666
864
  if (claim.status === "replayed") return { redirectTo: stored.returnUrl, exactReturn: true };
667
865
  }
668
- // Fence state minted by an older deployment too: a rolling update must not
669
- // let a still-valid callback persist an ownership the target's profile no
670
- // longer allows (workspace-owned Gmail or hosted Slack, for example).
671
- const builtInCallbackProfile = builtInOAuthProfileFor({
672
- mcpUrl: state.mcpUrl,
673
- providerDomain: state.providerDomain,
674
- });
675
- if (builtInCallbackProfile) {
676
- assertOwnershipAllowed(builtInCallbackProfile, state.ownership);
677
- }
678
- // Only a managed human may own a personal Connection. This callback has no
679
- // live principal, so it enforces the signed start-time decision; a state
680
- // minted before that fence existed carries no claim and is refused, which
681
- // closes the in-flight window across a rolling deploy. The legacy
682
- // `?? "personal"` decode above therefore cannot land a machine-owned row.
866
+ // The callback enforces the signed start-time principal decision.
867
+ // Workspace ownership requires no personal-owner claim.
683
868
  if (!personalOwnerStateAccepted(state)) {
684
869
  throw new OAuthCallbackStageError(
685
870
  "state_verify",
@@ -687,7 +872,11 @@ async function completeMcpOAuthCallbackWithinDeadline(
687
872
  new HTTPException(422, { message: PERSONAL_CONNECTION_PRINCIPAL_MESSAGE }),
688
873
  );
689
874
  }
690
- if (!input.code) {
875
+ if (input.error || !input.code) {
876
+ // The provider sends `error` (for example `access_denied` when the user
877
+ // clicks Cancel) instead of a code. Report that as a refusal, not as an
878
+ // expired attempt, and never echo the provider's own text.
879
+ const cancelled = input.error === "access_denied";
691
880
  if (connectOperation) {
692
881
  await finishConnectOperation(db, state, {
693
882
  ...connectOperation,
@@ -696,39 +885,31 @@ async function completeMcpOAuthCallbackWithinDeadline(
696
885
  commit: async (_tx, current) => ({
697
886
  ...current,
698
887
  revision: current.revision + 1,
699
- state: "failed",
888
+ state: cancelled ? "cancelled" : "failed",
700
889
  nextAction: { type: "none" },
701
890
  error: {
702
- code: "missing_code",
891
+ code: input.error ? "provider_denied" : "missing_code",
703
892
  message: "Authorization was not completed. Start a new connection attempt.",
704
893
  retryable: false,
705
894
  },
706
895
  }),
707
896
  });
708
897
  }
709
- return callbackStateResult(state, "error", {
710
- stage: "state_verify",
711
- reason: "missing_code",
712
- });
898
+ return callbackStateResult(
899
+ state,
900
+ "error",
901
+ input.error
902
+ ? { stage: "authorize", reason: cancelled ? "access_denied" : "provider_error" }
903
+ : { stage: "state_verify", reason: "missing_code" },
904
+ );
713
905
  }
906
+ await withOrganizationIntegrationAcquisition(db, state, [integrationKey], async () => {});
714
907
  const consumed = await runCallbackDatabaseStage(
715
908
  deadline,
716
909
  "state_verify",
717
910
  db,
718
911
  async (scopedDb) => {
719
912
  await requireOAuthCallbackGrant(scopedDb, state!);
720
- if (!builtInCallbackProfile) {
721
- // Catalog-declared ownership fences get the same rolling-update
722
- // treatment as the built-in ones.
723
- const raw = await getGlobalCatalogOAuthProfile(
724
- scopedDb,
725
- state!.workspaceId,
726
- catalogMcpUrlKey(state!.mcpUrl),
727
- );
728
- if (raw !== null) {
729
- assertOwnershipAllowed(oauthProfileFromCatalog(state!.mcpUrl, raw), state!.ownership);
730
- }
731
- }
732
913
  return await consumeIntegrationOAuthStateNonce(scopedDb, {
733
914
  accountId: state!.accountId,
734
915
  workspaceId: state!.workspaceId,
@@ -750,6 +931,17 @@ async function completeMcpOAuthCallbackWithinDeadline(
750
931
  ? error
751
932
  : new OAuthCallbackStageError("state_verify", "state_invalid", error);
752
933
  logOAuthCallbackFailure(observability, staged, state);
934
+ if (!state) {
935
+ // The state itself could not be resolved, so it carries no return path.
936
+ // An authentic but aged state still names its workspace and "expired".
937
+ const failure = oauthStateFailureReturn(settings, input.state);
938
+ return {
939
+ redirectTo: callbackReturnPath(failure.returnPath, "error", {
940
+ stage: staged.stage,
941
+ reason: staged.reason === "state_invalid" ? failure.reason : staged.reason,
942
+ }),
943
+ };
944
+ }
753
945
  return callbackStateResult(state, "error", {
754
946
  stage: staged.stage,
755
947
  reason: staged.reason,
@@ -765,6 +957,16 @@ async function completeMcpOAuthCallbackWithinDeadline(
765
957
  const client = await runCallbackDatabaseStage(deadline, "client_lookup", db, (scopedDb) =>
766
958
  clientForState(scopedDb, settings, state),
767
959
  );
960
+ if (
961
+ builtInOAuthProfileFor(state)?.confidentialClientRequired &&
962
+ !isConfidentialOAuthClient(client)
963
+ ) {
964
+ throw new OAuthCallbackStageError(
965
+ "client_lookup",
966
+ "invalid_client",
967
+ new Error("Gmail OAuth client is unavailable"),
968
+ );
969
+ }
768
970
  const token = await deadline.run("token_exchange", (signal) =>
769
971
  exchangeAuthorizationCode(settings, {
770
972
  code: input.code!,
@@ -777,6 +979,27 @@ async function completeMcpOAuthCallbackWithinDeadline(
777
979
  signal,
778
980
  }),
779
981
  );
982
+ const profile = builtInOAuthProfileFor(state);
983
+ if (profile?.freshRefreshTokenRequired && !token.refreshToken) {
984
+ // Never splice a previous account's refresh token into a new grant.
985
+ // Google can omit refresh_token even after another consent prompt.
986
+ throw new OAuthCallbackStageError(
987
+ "token_exchange",
988
+ "offline_access_unavailable",
989
+ new Error("Google did not grant offline access. Start a new Gmail connection attempt."),
990
+ );
991
+ }
992
+ const scopes = grantedScopes(token.scopeText, state.authorizeScopes, profile);
993
+ if (
994
+ profile?.fullRequestedScopesRequired &&
995
+ profile.requestedScopes?.some((scope) => !scopes.includes(scope))
996
+ ) {
997
+ throw new OAuthCallbackStageError(
998
+ "token_exchange",
999
+ "insufficient_scope",
1000
+ new Error("Google did not grant the reviewed Gmail permissions."),
1001
+ );
1002
+ }
780
1003
  const verification = await verifyMcpToolsListNonFatal(
781
1004
  observability,
782
1005
  settings,
@@ -784,7 +1007,6 @@ async function completeMcpOAuthCallbackWithinDeadline(
784
1007
  token,
785
1008
  deadline,
786
1009
  );
787
- const scopes = grantedScopes(token.scopeText, state.authorizeScopes);
788
1010
  const credential = credentialBundle(token, state, client);
789
1011
  const metadata = {
790
1012
  resource: state.resource,
@@ -807,6 +1029,7 @@ async function completeMcpOAuthCallbackWithinDeadline(
807
1029
  : {}),
808
1030
  },
809
1031
  mcpToolsVerification: verification.metadata,
1032
+ ...(verification.providerIdentity ?? {}),
810
1033
  ...(verification.tools ? { mcpTools: verification.tools } : {}),
811
1034
  };
812
1035
  const credentialEncrypted = encryptEnvironmentValue(key, JSON.stringify(credential));
@@ -821,7 +1044,7 @@ async function completeMcpOAuthCallbackWithinDeadline(
821
1044
  subjectId: ownerSubjectId,
822
1045
  providerDomain: state.providerDomain,
823
1046
  kind: "oauth2",
824
- status: "active",
1047
+ status: verification.connectionStatus ?? "active",
825
1048
  credentialEncrypted,
826
1049
  grantedScopes: scopes,
827
1050
  expiresAt: token.expiresAt,
@@ -834,6 +1057,7 @@ async function completeMcpOAuthCallbackWithinDeadline(
834
1057
  subjectId: ownerSubjectId,
835
1058
  providerDomain: state.providerDomain,
836
1059
  kind: "oauth2",
1060
+ status: verification.connectionStatus ?? "active",
837
1061
  credentialEncrypted,
838
1062
  grantedScopes: scopes,
839
1063
  expiresAt: token.expiresAt,
@@ -843,38 +1067,45 @@ async function completeMcpOAuthCallbackWithinDeadline(
843
1067
  };
844
1068
  if (connectOperation) {
845
1069
  await runCallbackDatabaseStage(deadline, "persist", db, (scopedDb) =>
846
- finishConnectOperation(scopedDb, state!, {
847
- ...connectOperation!,
848
- authorize: (tx, _attempt, origin) =>
849
- requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
850
- commit: async (tx, current) => {
851
- const connection = await persist(tx);
852
- if (!connection)
853
- throw new HTTPException(409, {
854
- message: "connection changed during OAuth reconnect",
855
- });
856
- return {
857
- ...current,
858
- revision: current.revision + 1,
859
- state: "complete",
860
- completionRequirement: "connection",
861
- credentialsCommitted: true,
862
- nextAction: { type: "none" },
863
- account: {
864
- id: connection.id,
865
- version: connection.version,
866
- providerId: current.providerId,
867
- label: state!.providerDomain,
868
- ownership: current.ownership,
869
- status: "connected",
870
- },
871
- };
1070
+ finishOAuthAcquisition(
1071
+ scopedDb,
1072
+ state!,
1073
+ {
1074
+ ...connectOperation!,
1075
+ authorize: (tx, _attempt, origin) =>
1076
+ requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
1077
+ commit: async (tx, current) => {
1078
+ const connection = await persist(tx);
1079
+ if (!connection)
1080
+ throw new HTTPException(409, {
1081
+ message: "connection changed during OAuth reconnect",
1082
+ });
1083
+ return {
1084
+ ...current,
1085
+ revision: current.revision + 1,
1086
+ state: "complete",
1087
+ completionRequirement: "connection",
1088
+ credentialsCommitted: true,
1089
+ nextAction: { type: "none" },
1090
+ account: {
1091
+ id: connection.id,
1092
+ version: connection.version,
1093
+ providerId: current.providerId,
1094
+ label: state!.providerDomain,
1095
+ ownership: current.ownership,
1096
+ status: connection.status === "needs_reauth" ? "auth_needed" : "connected",
1097
+ },
1098
+ };
1099
+ },
872
1100
  },
873
- }),
1101
+ integrationKey,
1102
+ ),
874
1103
  );
875
1104
  return { redirectTo: state.returnUrl!, exactReturn: true };
876
1105
  }
877
- const connection = await runCallbackDatabaseStage(deadline, "persist", db, persist);
1106
+ const connection = await runCallbackDatabaseStage(deadline, "persist", db, (scopedDb) =>
1107
+ withOrganizationIntegrationAcquisition(scopedDb, state!, [integrationKey], persist),
1108
+ );
878
1109
  if (!connection) {
879
1110
  throw new HTTPException(409, {
880
1111
  message: "connection changed during OAuth reconnect; start again",
@@ -896,6 +1127,41 @@ async function completeMcpOAuthCallbackWithinDeadline(
896
1127
  ? error
897
1128
  : new OAuthCallbackStageError("persist", "persist_failed", error);
898
1129
  logOAuthCallbackFailure(observability, staged, state);
1130
+ if (connectOperation && builtInOAuthProfileFor(state)?.key === "official-gmail") {
1131
+ // The external return URL stays exact. Its caller reads the durable
1132
+ // attempt result, which must not remain in flight after a rejected grant.
1133
+ try {
1134
+ await withDatabaseStatementTimeout(db, OAUTH_CALLBACK_DB_STATEMENT_TIMEOUT_MS, (tx) =>
1135
+ finishConnectOperation(tx, state!, {
1136
+ ...connectOperation!,
1137
+ authorize: (locked, _attempt, origin) =>
1138
+ requireConnectOwnerAuthority(locked, state!, "connections:write", origin),
1139
+ commit: async (_locked, current) => ({
1140
+ ...current,
1141
+ revision: current.revision + 1,
1142
+ state: "failed",
1143
+ nextAction: { type: "none" },
1144
+ error: {
1145
+ code: staged.reason,
1146
+ message:
1147
+ staged.reason === "offline_access_unavailable"
1148
+ ? "Google did not grant offline access. Your previous connection is unchanged. Start a new Gmail connection attempt."
1149
+ : staged.reason === "insufficient_scope"
1150
+ ? "Google did not grant all required Gmail permissions. Your previous connection is unchanged. Start a new connection attempt and approve the requested permissions."
1151
+ : "Gmail authorization could not be verified. Your previous connection is unchanged. Start a new connection attempt.",
1152
+ retryable: true,
1153
+ },
1154
+ }),
1155
+ }),
1156
+ );
1157
+ } catch (settlementError) {
1158
+ logOAuthCallbackFailure(
1159
+ observability,
1160
+ new OAuthCallbackStageError("persist", "persist_failed", settlementError),
1161
+ state,
1162
+ );
1163
+ }
1164
+ }
899
1165
  return callbackStateResult(state, "error", {
900
1166
  stage: staged.stage,
901
1167
  reason: staged.reason,
@@ -924,7 +1190,13 @@ function callbackStateResult(
924
1190
  ): OAuthCallbackResult {
925
1191
  if ((state?.externalContinuation || state?.connectAttemptId) && state.returnUrl)
926
1192
  return { redirectTo: state.returnUrl, exactReturn: true };
927
- return { redirectTo: callbackReturnPath(state?.returnPath ?? "/integrations", status, details) };
1193
+ return {
1194
+ redirectTo: callbackReturnPath(
1195
+ state?.returnPath ?? INTEGRATIONS_FALLBACK_PATH,
1196
+ status,
1197
+ details,
1198
+ ),
1199
+ };
928
1200
  }
929
1201
 
930
1202
  export function integrationBaseUrl(publicBaseUrl: string | undefined, requestUrl: string): string {
@@ -985,7 +1257,7 @@ export async function inspectMcpAuthentication(
985
1257
  params: {
986
1258
  protocolVersion: "2025-03-26",
987
1259
  capabilities: {},
988
- clientInfo: { name: "OpenGeni", version: "1.0" },
1260
+ clientInfo: { name: "Opengeni", version: "1.0" },
989
1261
  },
990
1262
  }),
991
1263
  signal,
@@ -1042,10 +1314,13 @@ export async function inspectMcpAuthentication(
1042
1314
  await resolveMcpOAuthDiscovery({
1043
1315
  resourceUrl: resource,
1044
1316
  challenge,
1045
- fetchMetadata: async ({ url }) => {
1317
+ fetchMetadata: async ({ kind, url }) => {
1046
1318
  try {
1047
- const result = await deadline.run("protected_resource_metadata", (signal) =>
1048
- fetchOAuthMetadata(url, settings, signal),
1319
+ const result = await deadline.run(
1320
+ kind === "protected_resource"
1321
+ ? "protected_resource_metadata"
1322
+ : "authorization_server_metadata",
1323
+ (signal) => fetchOAuthMetadata(url, settings, signal),
1049
1324
  );
1050
1325
  metadataRead = true;
1051
1326
  if (result.status !== "absent") metadataAbsent = false;
@@ -1059,7 +1334,7 @@ export async function inspectMcpAuthentication(
1059
1334
  canonicalizeResource: canonicalOAuthResource,
1060
1335
  });
1061
1336
  return { kind: "oauth2" };
1062
- } catch {
1337
+ } catch (failure) {
1063
1338
  // Public initialization does not exclude optional or tool-level OAuth.
1064
1339
  // Only explicit absence of metadata permits the unauthenticated path.
1065
1340
  if (
@@ -1072,20 +1347,31 @@ export async function inspectMcpAuthentication(
1072
1347
  return { kind: "none" };
1073
1348
  return {
1074
1349
  kind: "unknown",
1075
- message:
1076
- "This server's sign-in requirements could not be determined. Check its setup instructions.",
1350
+ message: mcpAuthInspectionFailureMessage(failure),
1077
1351
  };
1078
1352
  }
1079
- } catch {
1353
+ } catch (failure) {
1080
1354
  return {
1081
1355
  kind: "unknown",
1082
- message: "Could not check this server. Retry or consult its setup instructions.",
1356
+ message: mcpAuthInspectionFailureMessage(failure),
1083
1357
  };
1084
1358
  } finally {
1085
1359
  deadline.dispose();
1086
1360
  }
1087
1361
  }
1088
1362
 
1363
+ function mcpAuthInspectionFailureMessage(failure: unknown): string {
1364
+ if (failure instanceof OAuthStartStageError) {
1365
+ if (failure.reason === "timeout") {
1366
+ return "Checking how to sign in timed out. Try again.";
1367
+ }
1368
+ if (failure.cause instanceof OAuthMetadataUpstreamError) {
1369
+ return `The provider returned HTTP ${failure.cause.upstreamStatus} while checking how to sign in. Retry or check the provider's setup instructions.`;
1370
+ }
1371
+ }
1372
+ return "Could not determine how to sign in. Retry or check the provider's setup instructions.";
1373
+ }
1374
+
1089
1375
  async function discoverMcpOAuth(
1090
1376
  resource: string,
1091
1377
  settings: Settings,
@@ -1143,6 +1429,70 @@ async function discoverMcpOAuth(
1143
1429
  }
1144
1430
  }
1145
1431
 
1432
+ /** Exact built-in API bridge discovery never contacts its historical MCP identity. */
1433
+ async function discoverProviderOAuth(
1434
+ resource: string,
1435
+ settings: Settings,
1436
+ deadline: OAuthStartDeadline,
1437
+ profile: OAuthProviderProfile,
1438
+ ) {
1439
+ const pinned = profile.providerOAuthDiscovery;
1440
+ if (!pinned || resource !== profile.requireExactMcpUrl?.url)
1441
+ throw new HTTPException(422, { message: "invalid provider OAuth resource" });
1442
+ const fetched = await deadline.run("authorization_server_metadata", (signal) =>
1443
+ fetchOAuthMetadata(pinned.metadataUrl, settings, signal),
1444
+ );
1445
+ if (fetched.status !== "present")
1446
+ throw new HTTPException(422, { message: "provider OAuth metadata is unavailable" });
1447
+ const raw = fetched.document;
1448
+ const issuer = normalizedIssuerKey(
1449
+ oauthEndpointUrl(requiredString(raw.issuer, "OAuth issuer"), settings, "OAuth issuer"),
1450
+ );
1451
+ if (issuer !== pinned.issuer)
1452
+ throw new HTTPException(422, { message: "provider OAuth issuer did not match its profile" });
1453
+ const as: AuthorizationServerMetadata = {
1454
+ issuer,
1455
+ authorizationServer: pinned.issuer,
1456
+ authorizationEndpoint: oauthEndpointUrl(
1457
+ requiredString(raw.authorization_endpoint, "OAuth authorization endpoint"),
1458
+ settings,
1459
+ "OAuth authorization endpoint",
1460
+ ),
1461
+ tokenEndpoint: oauthEndpointUrl(
1462
+ requiredString(raw.token_endpoint, "OAuth token endpoint"),
1463
+ settings,
1464
+ "OAuth token endpoint",
1465
+ ),
1466
+ clientIdMetadataDocumentSupported: false,
1467
+ tokenEndpointAuthMethodsSupported: stringArray(raw.token_endpoint_auth_methods_supported),
1468
+ codeChallengeMethodsSupported: stringArray(raw.code_challenge_methods_supported),
1469
+ raw,
1470
+ metadataUrl: fetched.url,
1471
+ };
1472
+ if (!as.codeChallengeMethodsSupported.includes("S256"))
1473
+ throw new HTTPException(422, { message: "provider OAuth requires PKCE S256" });
1474
+ return {
1475
+ challenge: { scheme: null, scope: [] } as WwwAuthenticateChallenge,
1476
+ prm: {
1477
+ resource,
1478
+ authorizationServers: [pinned.issuer],
1479
+ scopesSupported: [...(profile.requestedScopes ?? [])],
1480
+ raw: {},
1481
+ metadataUrl: "",
1482
+ } as ProtectedResourceMetadata,
1483
+ as,
1484
+ mode: "provider_oauth_metadata" as const,
1485
+ resource,
1486
+ provenance: {
1487
+ protectedResourceMetadataUrl: null,
1488
+ authorizationServerMetadataUrl: fetched.url,
1489
+ metadataSha256: createHash("sha256")
1490
+ .update(stableJson({ profile: profile.key, resource, metadataUrl: fetched.url, raw }))
1491
+ .digest("hex"),
1492
+ },
1493
+ };
1494
+ }
1495
+
1146
1496
  async function probeMcpChallenge(
1147
1497
  resource: string,
1148
1498
  settings: Settings,
@@ -1206,6 +1556,7 @@ async function registerOAuthClient(
1206
1556
  manual: OAuthStartRequest["oauthClient"],
1207
1557
  profile: OAuthProviderProfile,
1208
1558
  signal: AbortSignal,
1559
+ acquisition: Pick<OAuthStartContext, "accountId" | "workspaceId" | "integrationKey">,
1209
1560
  ): Promise<OAuthClientRegistration> {
1210
1561
  const operator = operatorClientForAs(settings, as);
1211
1562
  if (operator) {
@@ -1220,6 +1571,7 @@ async function registerOAuthClient(
1220
1571
  redirectUri,
1221
1572
  scopes,
1222
1573
  signal,
1574
+ acquisition,
1223
1575
  );
1224
1576
  }
1225
1577
  if (selfRegistration === "cimd") {
@@ -1244,7 +1596,15 @@ async function registerOAuthClient(
1244
1596
  ),
1245
1597
  };
1246
1598
  }
1247
- return await getOrCreateDynamicClientRegistration(db, settings, as, redirectUri, scopes, signal);
1599
+ return await getOrCreateDynamicClientRegistration(
1600
+ db,
1601
+ settings,
1602
+ as,
1603
+ redirectUri,
1604
+ scopes,
1605
+ signal,
1606
+ acquisition,
1607
+ );
1248
1608
  }
1249
1609
 
1250
1610
  export function preferredOAuthSelfRegistration(
@@ -1272,6 +1632,7 @@ async function getOrCreateDynamicClientRegistration(
1272
1632
  redirectUri: string,
1273
1633
  scopes: string[],
1274
1634
  signal: AbortSignal,
1635
+ acquisition: Pick<OAuthStartContext, "accountId" | "workspaceId" | "integrationKey">,
1275
1636
  ): Promise<OAuthClientRegistration> {
1276
1637
  const storedClient = await loadIntegrationOAuthClient(db, settings, as.issuer);
1277
1638
  if (storedClient && storedDcrClientSatisfiesPolicy(storedClient, as, redirectUri, scopes)) {
@@ -1292,6 +1653,12 @@ async function getOrCreateDynamicClientRegistration(
1292
1653
  message: "manual OAuth client credentials are required for this authorization server",
1293
1654
  });
1294
1655
  }
1656
+ await withOrganizationIntegrationAcquisition(
1657
+ db,
1658
+ acquisition,
1659
+ [acquisition.integrationKey ?? "custom:mcp"],
1660
+ async () => {},
1661
+ );
1295
1662
  const dcr = await dynamicClientRegistration(settings, as, redirectUri, scopes, signal);
1296
1663
  const key = dcr.clientSecret ? requireEnvironmentEncryption(settings) : null;
1297
1664
  const storeInput = {
@@ -1303,34 +1670,43 @@ async function getOrCreateDynamicClientRegistration(
1303
1670
  tokenEndpointAuthMethod: dcr.tokenEndpointAuthMethod,
1304
1671
  metadata: registrationMetadata(as, redirectUri, scopes),
1305
1672
  };
1306
- if (storedClient) {
1307
- const replaced = await replaceIntegrationOAuthClientIfCurrent(db, {
1308
- ...storeInput,
1309
- expectedClientId: storedClient.clientId,
1310
- });
1311
- if (replaced?.clientId === dcr.clientId) {
1312
- return dcr;
1313
- }
1314
- return await loadCompatibleDcrWinner(db, settings, as, redirectUri, scopes);
1315
- }
1316
- const storedWinner = await storeIntegrationOAuthClient(db, storeInput);
1317
- if (storedWinner.clientId === dcr.clientId) {
1318
- return dcr;
1319
- }
1320
- const winner = await loadIntegrationOAuthClient(db, settings, as.issuer);
1321
- if (winner && storedDcrClientSatisfiesPolicy(winner, as, redirectUri, scopes)) {
1322
- return dcrRegistrationFromStored(winner);
1323
- }
1324
- if (winner) {
1325
- const replaced = await replaceIntegrationOAuthClientIfCurrent(db, {
1326
- ...storeInput,
1327
- expectedClientId: winner.clientId,
1328
- });
1329
- if (replaced?.clientId === dcr.clientId) {
1330
- return dcr;
1331
- }
1332
- }
1333
- return await loadCompatibleDcrWinner(db, settings, as, redirectUri, scopes);
1673
+ // Registration has already happened outside this transaction. Fence only the
1674
+ // durable client credential; a restriction racing DCR must not persist it.
1675
+ return withOrganizationIntegrationAcquisition(
1676
+ db,
1677
+ acquisition,
1678
+ [acquisition.integrationKey ?? "custom:mcp"],
1679
+ async (tx) => {
1680
+ if (storedClient) {
1681
+ const replaced = await replaceIntegrationOAuthClientIfCurrent(tx, {
1682
+ ...storeInput,
1683
+ expectedClientId: storedClient.clientId,
1684
+ });
1685
+ if (replaced?.clientId === dcr.clientId) {
1686
+ return dcr;
1687
+ }
1688
+ return await loadCompatibleDcrWinner(tx, settings, as, redirectUri, scopes);
1689
+ }
1690
+ const storedWinner = await storeIntegrationOAuthClient(tx, storeInput);
1691
+ if (storedWinner.clientId === dcr.clientId) {
1692
+ return dcr;
1693
+ }
1694
+ const winner = await loadIntegrationOAuthClient(tx, settings, as.issuer);
1695
+ if (winner && storedDcrClientSatisfiesPolicy(winner, as, redirectUri, scopes)) {
1696
+ return dcrRegistrationFromStored(winner);
1697
+ }
1698
+ if (winner) {
1699
+ const replaced = await replaceIntegrationOAuthClientIfCurrent(tx, {
1700
+ ...storeInput,
1701
+ expectedClientId: winner.clientId,
1702
+ });
1703
+ if (replaced?.clientId === dcr.clientId) {
1704
+ return dcr;
1705
+ }
1706
+ }
1707
+ return await loadCompatibleDcrWinner(tx, settings, as, redirectUri, scopes);
1708
+ },
1709
+ );
1334
1710
  }
1335
1711
 
1336
1712
  function storedDcrClientSatisfiesPolicy(
@@ -1436,7 +1812,6 @@ function operatorClientEntryFor(
1436
1812
  settings: Settings,
1437
1813
  candidates: string[],
1438
1814
  ): ReturnType<typeof parseIntegrationsOauthClientsJson>[string] | null {
1439
- const normalizedCandidates = new Set(candidates.map(normalizedIssuerKey));
1440
1815
  const candidateOrigins = candidates.flatMap((candidate) => {
1441
1816
  try {
1442
1817
  return [new URL(candidate).origin];
@@ -1453,22 +1828,37 @@ function operatorClientEntryFor(
1453
1828
  return resolved;
1454
1829
  }
1455
1830
  }
1456
- const configured = parseIntegrationsOauthClientsJson(settings.integrationsOauthClientsJson);
1457
- const exactKeys = uniqueStrings(
1458
- candidates.flatMap((candidate) => [candidate, normalizedIssuerKey(candidate)]),
1831
+ return findIntegrationsOauthClient(
1832
+ parseIntegrationsOauthClientsJson(settings.integrationsOauthClientsJson),
1833
+ candidates,
1834
+ );
1835
+ }
1836
+
1837
+ /** Secret-free readiness for the exact registered client used by Gmail setup. */
1838
+ export function gmailOAuthClientConfigured(settings: Settings): boolean {
1839
+ const entry = operatorClientEntryFor(settings, ["https://accounts.google.com"]);
1840
+ return Boolean(
1841
+ entry &&
1842
+ isConfidentialOAuthClient({
1843
+ ...entry,
1844
+ tokenEndpointAuthMethod: tokenAuthMethod(
1845
+ entry.tokenEndpointAuthMethod,
1846
+ Boolean(entry.clientSecret),
1847
+ ),
1848
+ }),
1849
+ );
1850
+ }
1851
+
1852
+ function isConfidentialOAuthClient(client: {
1853
+ clientId: string;
1854
+ clientSecret?: string | undefined;
1855
+ tokenEndpointAuthMethod: string;
1856
+ }): boolean {
1857
+ return Boolean(
1858
+ client.clientId.trim() &&
1859
+ client.clientSecret?.trim() &&
1860
+ ["client_secret_post", "client_secret_basic"].includes(client.tokenEndpointAuthMethod),
1459
1861
  );
1460
- for (const key of exactKeys) {
1461
- const entry = configured[key];
1462
- if (entry) {
1463
- return entry;
1464
- }
1465
- }
1466
- for (const [key, entry] of Object.entries(configured)) {
1467
- if (normalizedCandidates.has(normalizedIssuerKey(key))) {
1468
- return entry;
1469
- }
1470
- }
1471
- return null;
1472
1862
  }
1473
1863
 
1474
1864
  function normalizedIssuerKey(value: string): string {
@@ -1491,7 +1881,7 @@ async function dynamicClientRegistration(
1491
1881
  method: "POST",
1492
1882
  headers: { "content-type": "application/json", accept: "application/json" },
1493
1883
  body: JSON.stringify({
1494
- client_name: "OpenGeni",
1884
+ client_name: "Opengeni",
1495
1885
  redirect_uris: [redirectUri],
1496
1886
  token_endpoint_auth_method: "none",
1497
1887
  grant_types: ["authorization_code", "refresh_token"],
@@ -1542,10 +1932,21 @@ async function existingOAuthConnectionForStart(
1542
1932
  connectionSelection: OAuthProviderProfile["connectionSelection"];
1543
1933
  exactMcpBinding: boolean;
1544
1934
  connectionId?: string | undefined;
1935
+ newAccount?: boolean;
1545
1936
  requestedOwnership?: ConnectionOwnership | undefined;
1546
1937
  newConnectionOwnership: ConnectionOwnership;
1547
1938
  },
1548
1939
  ) {
1940
+ if (input.newAccount) {
1941
+ // A further account always mints its own row; it never refreshes (and so
1942
+ // never replaces) an account the caller already holds for this provider.
1943
+ if (input.connectionSelection === "canonical_personal") {
1944
+ throw new HTTPException(409, {
1945
+ message: "this connector supports one account per person; reconnect it instead",
1946
+ });
1947
+ }
1948
+ return null;
1949
+ }
1549
1950
  if (input.connectionId) {
1550
1951
  const connection = await getConnectionMetadata(
1551
1952
  db,
@@ -1633,6 +2034,41 @@ export function buildAuthorizationUrl(input: {
1633
2034
  return url.toString();
1634
2035
  }
1635
2036
 
2037
+ async function resolveMcpOAuthState(
2038
+ db: Database,
2039
+ settings: Settings,
2040
+ state: string,
2041
+ deadline: OAuthCallbackDeadline,
2042
+ ): Promise<OAuthStatePayload> {
2043
+ const reference = readSignedState(state, requireIntegrationsStateSecret(settings)) as Record<
2044
+ string,
2045
+ unknown
2046
+ > | null;
2047
+ if (reference?.kind !== "mcp_oauth_reference") {
2048
+ // In-flight grants from the previous release remain valid for their
2049
+ // normal ten-minute lifetime during a rolling deployment.
2050
+ return readOAuthState(state, settings);
2051
+ }
2052
+ const accountId = requiredString(reference.accountId, "state.accountId");
2053
+ const workspaceId = requiredString(reference.workspaceId, "state.workspaceId");
2054
+ const id = requiredString(reference.id, "state.id");
2055
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id)) {
2056
+ throw new HTTPException(400, { message: "invalid OAuth state reference" });
2057
+ }
2058
+ const encrypted = await runCallbackDatabaseStage(deadline, "state_verify", db, (scopedDb) =>
2059
+ loadIntegrationOAuthPendingState(scopedDb, { accountId, workspaceId, id }),
2060
+ );
2061
+ if (!encrypted) {
2062
+ throw new HTTPException(400, { message: "invalid or expired OAuth state" });
2063
+ }
2064
+ const fullState = decryptEnvironmentValue(requireEnvironmentEncryption(settings), encrypted);
2065
+ const resolved = readOAuthState(fullState, settings);
2066
+ if (resolved.accountId !== accountId || resolved.workspaceId !== workspaceId) {
2067
+ throw new HTTPException(400, { message: "OAuth state reference mismatch" });
2068
+ }
2069
+ return resolved;
2070
+ }
2071
+
1636
2072
  function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
1637
2073
  const payload = readSignedState(state, requireIntegrationsStateSecret(settings)) as Record<
1638
2074
  string,
@@ -1641,6 +2077,13 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
1641
2077
  if (!payload) {
1642
2078
  throw new HTTPException(400, { message: "invalid or expired OAuth state" });
1643
2079
  }
2080
+ if (
2081
+ payload.integrationKey !== undefined &&
2082
+ payload.integrationKey !== "gmail" &&
2083
+ payload.integrationKey !== "slack-personal"
2084
+ ) {
2085
+ throw new HTTPException(400, { message: "invalid OAuth integration identity" });
2086
+ }
1644
2087
  const nowSeconds = Math.floor(Date.now() / 1000);
1645
2088
  const iat = numberValue(payload.iat);
1646
2089
  if (iat === undefined || nowSeconds - iat > oauthStateTtlMs / 1000 || nowSeconds < iat) {
@@ -1662,7 +2105,7 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
1662
2105
  encodedDiscoveryMode &&
1663
2106
  (!authorizationServerMetadataUrl ||
1664
2107
  (discoveryMode === "rfc9728_protected_resource" && !protectedResourceMetadataUrl) ||
1665
- (discoveryMode === "legacy_2025_03_26_metadata" && protectedResourceMetadataUrl))
2108
+ (discoveryMode !== "rfc9728_protected_resource" && protectedResourceMetadataUrl))
1666
2109
  ) {
1667
2110
  throw new HTTPException(400, { message: "invalid OAuth discovery provenance state" });
1668
2111
  }
@@ -1670,6 +2113,12 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
1670
2113
  if (discoveryMode === "legacy_2025_03_26_metadata" && resourceParameterSupported) {
1671
2114
  throw new HTTPException(400, { message: "invalid legacy OAuth resource parameter state" });
1672
2115
  }
2116
+ const ownership = connectionOwnership(payload.ownership);
2117
+ if (!ownership) {
2118
+ throw new HTTPException(400, {
2119
+ message: "missing or invalid OAuth ownership; restart connection setup",
2120
+ });
2121
+ }
1673
2122
  const parsed = {
1674
2123
  accountId: requiredString(payload.accountId, "state.accountId"),
1675
2124
  workspaceId: requiredString(payload.workspaceId, "state.workspaceId"),
@@ -1692,11 +2141,12 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
1692
2141
  ...(stringValue(payload.connectAttemptId)
1693
2142
  ? { connectAttemptId: requiredString(payload.connectAttemptId, "state.connectAttemptId") }
1694
2143
  : {}),
1695
- // OAuth states minted before ownership was explicit were always personal.
1696
- // Preserve that meaning for in-flight reconnects during a rolling deploy.
1697
- ownership: connectionOwnership(payload.ownership) ?? "personal",
2144
+ ownership,
1698
2145
  // Absent on a legacy state, which therefore cannot land a personal owner.
1699
2146
  personalOwnerVerified: personalOwnerVerifiedInState(payload),
2147
+ ...(payload.integrationKey
2148
+ ? { integrationKey: payload.integrationKey as "gmail" | "slack-personal" }
2149
+ : {}),
1700
2150
  providerDomain: requiredString(payload.providerDomain, "state.providerDomain"),
1701
2151
  mcpUrl: stringValue(payload.mcpUrl) ?? resource,
1702
2152
  resource,
@@ -1756,6 +2206,7 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
1756
2206
  };
1757
2207
  const connectionId = stringValue(payload.connectionId);
1758
2208
  const connectionVersion = numberValue(payload.connectionVersion);
2209
+ assertProfileOwnership(builtInOAuthProfileFor(parsed), parsed.ownership);
1759
2210
  if (Boolean(connectionId) !== Boolean(connectionVersion)) {
1760
2211
  throw new HTTPException(400, { message: "invalid OAuth reconnect state" });
1761
2212
  }
@@ -1766,24 +2217,61 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
1766
2217
  ) {
1767
2218
  throw new HTTPException(400, { message: "invalid OAuth discovery binding state" });
1768
2219
  }
2220
+ if (parsed.discoveryMode === "provider_oauth_metadata") {
2221
+ const profile = builtInOAuthProfileFor(parsed);
2222
+ const discovery = profile?.providerOAuthDiscovery;
2223
+ if (
2224
+ !discovery ||
2225
+ parsed.mcpUrl !== profile?.requireExactMcpUrl?.url ||
2226
+ parsed.resource !== parsed.mcpUrl ||
2227
+ parsed.providerDomain !== profile?.postDiscoveryProviderDomain?.domain ||
2228
+ normalizedIssuerKey(parsed.issuer) !== discovery.issuer ||
2229
+ parsed.authorizationServer !== discovery.issuer ||
2230
+ parsed.authorizationServerMetadataUrl !== discovery.metadataUrl ||
2231
+ parsed.resourceParameterSupported
2232
+ ) {
2233
+ throw new HTTPException(400, { message: "invalid provider OAuth discovery binding state" });
2234
+ }
2235
+ assertDiscoveredAuthorizationServer(
2236
+ settings,
2237
+ { ...parsed, authorizationEndpoint: discovery.issuer },
2238
+ profile!,
2239
+ parsed.providerDomain,
2240
+ );
2241
+ }
1769
2242
  return {
1770
2243
  ...parsed,
2244
+ ...(parsed.discoveryMode === "provider_oauth_metadata"
2245
+ ? { issuer: normalizedIssuerKey(parsed.issuer) }
2246
+ : {}),
1771
2247
  ...(connectionId ? { connectionId } : {}),
1772
2248
  ...(connectionVersion !== undefined ? { connectionVersion } : {}),
1773
2249
  };
1774
2250
  }
1775
2251
 
1776
- function discoveryModeValue(value: string | undefined): McpOAuthDiscoveryMode {
2252
+ function discoveryModeValue(value: string | undefined): OAuthDiscoveryMode {
1777
2253
  if (!value) {
1778
2254
  // Every state minted before discovery modes existed used RFC 9728 PRM.
1779
2255
  return "rfc9728_protected_resource";
1780
2256
  }
1781
- if (value === "rfc9728_protected_resource" || value === "legacy_2025_03_26_metadata") {
2257
+ if (
2258
+ value === "rfc9728_protected_resource" ||
2259
+ value === "legacy_2025_03_26_metadata" ||
2260
+ value === "provider_oauth_metadata"
2261
+ ) {
1782
2262
  return value;
1783
2263
  }
1784
2264
  throw new HTTPException(400, { message: "invalid OAuth discovery mode state" });
1785
2265
  }
1786
2266
 
2267
+ function assertProfileOwnership(
2268
+ profile: OAuthProviderProfile | null,
2269
+ ownership: ConnectionOwnership,
2270
+ ): void {
2271
+ if (profile?.requiredOwnership && ownership !== profile.requiredOwnership.ownership)
2272
+ throw new HTTPException(422, { message: profile.requiredOwnership.message });
2273
+ }
2274
+
1787
2275
  function connectionOwnership(value: unknown): ConnectionOwnership | undefined {
1788
2276
  return value === "workspace" || value === "personal" ? value : undefined;
1789
2277
  }
@@ -2071,6 +2559,8 @@ function oauthStartStageLabel(stage: OAuthStartStage): string {
2071
2559
  return "authorization-server discovery";
2072
2560
  case "client_registration":
2073
2561
  return "OAuth client registration";
2562
+ case "state_persist":
2563
+ return "OAuth state persistence";
2074
2564
  }
2075
2565
  }
2076
2566
 
@@ -2196,11 +2686,45 @@ async function verifyMcpToolsListNonFatal(
2196
2686
  | { status: "ok"; checkedAt: string; toolCount: number }
2197
2687
  | { status: "failed"; checkedAt: string; reason: string };
2198
2688
  tools?: Array<{ name: string; description?: string }>;
2689
+ providerIdentity?: Record<string, string>;
2690
+ connectionStatus?: "needs_reauth";
2199
2691
  }> {
2692
+ let grantRejected = false;
2200
2693
  try {
2201
- const tools = await deadline.run("tools_list", (signal) =>
2202
- verifyMcpToolsList(settings, state.mcpUrl, token, signal),
2203
- );
2694
+ const profile = builtInOAuthProfileFor(state);
2695
+ const local = profile?.localToolVerification;
2696
+ let providerIdentity: Record<string, string> | undefined;
2697
+ const tools = await deadline.run("tools_list", async (signal) => {
2698
+ if (!local || state.mcpUrl !== profile?.requireExactMcpUrl?.url) {
2699
+ return verifyMcpToolsList(settings, state.mcpUrl, token, signal);
2700
+ }
2701
+ const response = await fetchOAuth(local.url, settings, {
2702
+ method: local.method ?? "POST",
2703
+ signal,
2704
+ headers: {
2705
+ authorization: `${normalizeBearerScheme(token.tokenType)} ${token.accessToken}`,
2706
+ },
2707
+ });
2708
+ if (!response.ok) {
2709
+ await cancelResponseBody(response);
2710
+ throw new Error("Connector token verification failed");
2711
+ }
2712
+ const payload = await readResponseJsonBounded<Record<string, unknown>>(
2713
+ response,
2714
+ OAUTH_MAX_RESPONSE_BYTES,
2715
+ "Connector token verification",
2716
+ { signal },
2717
+ );
2718
+ grantRejected = local.isRejectedGrant?.(payload) ?? false;
2719
+ providerIdentity = local.validateIdentity(payload);
2720
+ const verifiedTools = local.toolsForScopes(
2721
+ grantedScopes(token.scopeText, state.authorizeScopes, profile),
2722
+ settings,
2723
+ );
2724
+ if (local.required && verifiedTools.length === 0)
2725
+ throw new Error("Connector verification did not report an authorized tool scope");
2726
+ return verifiedTools;
2727
+ });
2204
2728
  return {
2205
2729
  metadata: {
2206
2730
  status: "ok",
@@ -2208,6 +2732,7 @@ async function verifyMcpToolsListNonFatal(
2208
2732
  toolCount: tools.length,
2209
2733
  },
2210
2734
  tools,
2735
+ ...(providerIdentity ? { providerIdentity } : {}),
2211
2736
  };
2212
2737
  } catch (error) {
2213
2738
  const staged =
@@ -2217,6 +2742,7 @@ async function verifyMcpToolsListNonFatal(
2217
2742
  if (staged.reason === "timeout" && deadline.signal.aborted) {
2218
2743
  throw staged;
2219
2744
  }
2745
+ if (builtInOAuthProfileFor(state)?.localToolVerification?.required) throw staged;
2220
2746
  logOAuthVerificationWarning(observability, staged, state);
2221
2747
  return {
2222
2748
  metadata: {
@@ -2224,6 +2750,7 @@ async function verifyMcpToolsListNonFatal(
2224
2750
  checkedAt: new Date().toISOString(),
2225
2751
  reason: staged.reason,
2226
2752
  },
2753
+ ...(grantRejected ? { connectionStatus: "needs_reauth" as const } : {}),
2227
2754
  };
2228
2755
  }
2229
2756
  }
@@ -2270,7 +2797,7 @@ function callbackReturnPath(
2270
2797
  // Defense in depth: a `//host` pathname becomes a protocol-relative absolute
2271
2798
  // Location — an open redirect from the unauthenticated callback.
2272
2799
  if (url.pathname.startsWith("//")) {
2273
- const fallback = new URL("/integrations", "https://opengeni.local");
2800
+ const fallback = new URL(INTEGRATIONS_FALLBACK_PATH, "https://opengeni.local");
2274
2801
  fallback.search = url.search;
2275
2802
  return `${fallback.pathname}${fallback.search}`;
2276
2803
  }
@@ -2417,6 +2944,7 @@ export function chooseMcpAuthorizeScopes(input: {
2417
2944
  requested: string[] | undefined;
2418
2945
  challenged: string[] | undefined;
2419
2946
  supported: string[];
2947
+ authorizationServerScopesSupported?: string[];
2420
2948
  }): string[] {
2421
2949
  const profile = builtInOAuthProfileFor({ mcpUrl: input.mcpUrl }) ?? DEFAULT_OAUTH_PROFILE;
2422
2950
  return chooseProfileAuthorizeScopes(profile, input);
@@ -2428,14 +2956,26 @@ function chooseProfileAuthorizeScopes(
2428
2956
  requested: string[] | undefined;
2429
2957
  challenged: string[] | undefined;
2430
2958
  supported: string[];
2959
+ authorizationServerScopesSupported?: string[];
2431
2960
  },
2432
2961
  ): string[] {
2433
- return profile.requestedScopes
2434
- ? [...profile.requestedScopes]
2435
- : chooseAuthorizeScopes(input.requested, input.challenged, input.supported);
2436
- }
2437
-
2438
- function grantedScopes(scopeText: string | undefined, fallback: string[]): string[] {
2962
+ // Reviewed profiles keep their exact scope contract. A generic long-running
2963
+ // connector also needs the AS-advertised refresh grant: the MCP resource's
2964
+ // challenge often advertises only its resource permission (e.g. mcp:use).
2965
+ if (profile.requestedScopes) return [...profile.requestedScopes];
2966
+ const scopes = chooseAuthorizeScopes(input.requested, input.challenged, input.supported);
2967
+ return input.authorizationServerScopesSupported?.includes("offline_access")
2968
+ ? uniqueStrings([...scopes, "offline_access"])
2969
+ : scopes;
2970
+ }
2971
+
2972
+ function grantedScopes(
2973
+ scopeText: string | undefined,
2974
+ fallback: string[],
2975
+ profile?: OAuthProviderProfile | null,
2976
+ ): string[] {
2977
+ if (profile?.reportedScopesRequired && !scopeText) return [];
2978
+ if (profile?.normalizeGrantedScopes) return profile.normalizeGrantedScopes(scopeText ?? fallback);
2439
2979
  if (scopeText) {
2440
2980
  return uniqueStrings(scopeText.split(/\s+/).filter(Boolean));
2441
2981
  }