@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
package/src/mcp-oauth.ts CHANGED
@@ -9,7 +9,11 @@ import {
9
9
  McpOAuthClientRegistrationRequest,
10
10
  McpOAuthClientRegistrationResponse,
11
11
  McpOAuthProtectedResourceMetadata,
12
+ McpConnectionDecision,
12
13
  McpOAuthTokenResponse,
14
+ OrganizationAccessPolicy,
15
+ organizationAccessPresetPermissions,
16
+ type AccessContext,
13
17
  type AccessGrant,
14
18
  type ToolGatewayCatalog,
15
19
  type ToolGatewayIdentity,
@@ -19,18 +23,32 @@ import {
19
23
  createMcpOAuthAuthorizationRequest,
20
24
  deleteMcpOAuthAuthorizationRequest,
21
25
  exchangeMcpOAuthAuthorizationCode,
26
+ getManagedAccount,
22
27
  getMcpOAuthAuthorizationRequest,
23
28
  getMcpOAuthClient,
29
+ getWorkspace,
30
+ listSharedWorkspacesForAccount,
31
+ listWorkspacesForSubject,
24
32
  McpOAuthClientRegistrationRateLimitError,
33
+ rebindMcpOAuthAuthorizationRequest,
25
34
  registerMcpOAuthClient,
26
35
  resolveLiveMcpOAuthGrant,
27
36
  resolveMcpOAuthAccessToken,
28
37
  rotateMcpOAuthRefreshToken,
38
+ setMcpOAuthRequestOrganizationAccess,
39
+ type McpOAuthAccess,
29
40
  } from "@opengeni/db";
30
- import { requireAccessGrantAuthorization, type ApiRouteDeps } from "@opengeni/core";
41
+ import {
42
+ hasPermission,
43
+ requireAccessContext,
44
+ requireAccessGrantAuthorization,
45
+ type ApiRouteDeps,
46
+ } from "@opengeni/core";
31
47
  import { Hono, type Context } from "hono";
32
48
  import { HTTPException } from "hono/http-exception";
33
- import { trustedRequestSourceAddress } from "./http/request-source";
49
+ import { trustedRequestSourceRateLimitKey, type RequestSourceTrust } from "./http/request-source";
50
+ import { isAgentActingAsPerson, requireFullAccessIfAgent } from "./http/acting-person";
51
+ import { requireSameOriginBrowserMutation } from "./routes/codex";
34
52
  import {
35
53
  prepareWorkspaceToolGateway,
36
54
  requireWorkspaceToolGatewayAuthorization,
@@ -45,10 +63,14 @@ const PKCE_VERIFIER = /^[A-Za-z0-9._~-]{43,128}$/u;
45
63
  const PKCE_CHALLENGE = /^[A-Za-z0-9_-]{43}$/u;
46
64
  const WORKSPACE_MCP_PATH = /^\/v1\/workspaces\/([0-9a-f-]{36})\/mcp(?:\/(docs|files))?$/u;
47
65
 
66
+ /** One organization MCP server for everyone; the sign-in chooses the organization. */
67
+ export const ORGANIZATION_MCP_PATH = "/v1/mcp";
68
+
48
69
  export type McpOAuthResource = {
49
70
  resource: string;
50
- workspaceId: string;
51
- kind: "all" | "docs" | "files";
71
+ /** Null for the organization server. */
72
+ workspaceId: string | null;
73
+ kind: "all" | "docs" | "files" | "organization";
52
74
  };
53
75
 
54
76
  export type McpOAuthRouteAccess = {
@@ -57,7 +79,10 @@ export type McpOAuthRouteAccess = {
57
79
  };
58
80
 
59
81
  export function registerMcpOAuthRoutes(app: Hono, deps: ApiRouteDeps): void {
60
- app.get("/.well-known/oauth-authorization-server", (c) => {
82
+ // RFC 8414 path-aware discovery: MCP clients (Claude among them) ask for the
83
+ // metadata at /.well-known/oauth-authorization-server/<resource path> first
84
+ // and give up on anything but JSON. Serve the same document at both.
85
+ const authorizationServerMetadata = (c: Context) => {
61
86
  requireMcpOAuthEnabled(deps);
62
87
  const issuer = mcpOAuthIssuer(deps);
63
88
  c.header("cache-control", "public, max-age=300");
@@ -75,7 +100,9 @@ export function registerMcpOAuthRoutes(app: Hono, deps: ApiRouteDeps): void {
75
100
  authorization_response_iss_parameter_supported: true,
76
101
  }),
77
102
  );
78
- });
103
+ };
104
+ app.get("/.well-known/oauth-authorization-server", authorizationServerMetadata);
105
+ app.get("/.well-known/oauth-authorization-server/*", authorizationServerMetadata);
79
106
 
80
107
  app.get("/.well-known/oauth-protected-resource/*", (c) => {
81
108
  requireMcpOAuthEnabled(deps);
@@ -115,9 +142,7 @@ export function registerMcpOAuthRoutes(app: Hono, deps: ApiRouteDeps): void {
115
142
  clientName: parsed.data.client_name ?? null,
116
143
  grantTypes: parsed.data.grant_types,
117
144
  responseTypes: ["code"],
118
- registrationScopeHash: tokenHash(
119
- mcpOAuthRegistrationClientKey(c, deps.settings.mcpOauthTrustedProxyHops),
120
- ),
145
+ registrationScopeHash: tokenHash(mcpOAuthRegistrationClientKey(c, deps.settings)),
121
146
  });
122
147
  } catch (error) {
123
148
  if (error instanceof McpOAuthClientRegistrationRateLimitError) {
@@ -148,10 +173,62 @@ export function registerMcpOAuthRoutes(app: Hono, deps: ApiRouteDeps): void {
148
173
  const query = new URL(c.req.url).searchParams;
149
174
  const client = await requireAuthorizationClient(deps, query);
150
175
  const resource = requireAuthorizationResource(deps, query);
176
+ if (resource.kind === "organization") {
177
+ // The person picks the organization, what the agent can do and where on
178
+ // the web app's sign-in page, which reads and answers this request.
179
+ const web = new URL(deps.settings.webBaseUrl ?? mcpOAuthIssuer(deps));
180
+ let person: { context: AccessContext };
181
+ try {
182
+ person = await requireBrowserPerson(c, deps);
183
+ } catch (error) {
184
+ if (!(error instanceof HTTPException) || error.status !== 401) throw error;
185
+ // Not signed in here yet: sign in on the web app, which returns to
186
+ // this exact authorization request (a same-origin path only).
187
+ const url = new URL(c.req.url);
188
+ c.header("cache-control", "no-store");
189
+ return c.redirect(
190
+ `${web.origin}/connect-agent?authorize=${encodeURIComponent(`${url.pathname}${url.search}`)}`,
191
+ 302,
192
+ );
193
+ }
194
+ const accountId =
195
+ person.context.defaultAccountId ?? person.context.accountGrants[0]?.accountId;
196
+ if (!accountId) {
197
+ throw new HTTPException(403, { message: "no organization is available for MCP OAuth" });
198
+ }
199
+ const requestToken = opaque(REQUEST_PREFIX);
200
+ await createMcpOAuthAuthorizationRequest(deps.db, {
201
+ requestHash: tokenHash(requestToken),
202
+ clientId: client.clientId,
203
+ accountId,
204
+ workspaceId: null,
205
+ subjectId: person.context.subjectId,
206
+ resource: resource.resource,
207
+ redirectUri: requireRedirectUri(client.redirectUris, query.get("redirect_uri")),
208
+ codeChallenge: requireCodeChallenge(query),
209
+ state: boundedState(query.get("state")),
210
+ permissions: [],
211
+ toolIdentities: [],
212
+ organizationAccess: DEFAULT_ORGANIZATION_ACCESS(),
213
+ expiresAt: expiresIn(MCP_OAUTH_CONSENT_TTL_SECONDS),
214
+ });
215
+ c.header("cache-control", "no-store");
216
+ return c.redirect(
217
+ `${web.origin}/connect-agent?request=${encodeURIComponent(requestToken)}`,
218
+ 302,
219
+ );
220
+ }
221
+ const context = await requireAccessContext(c, deps);
222
+ const workspaces = await listConsentWorkspaces(deps, context);
223
+ if (workspaces.length === 0) {
224
+ throw new HTTPException(403, { message: "no workspace is available for MCP OAuth" });
225
+ }
226
+ const selectedWorkspace =
227
+ workspaces.find((workspace) => workspace.id === resource.workspaceId) ?? workspaces[0]!;
151
228
  const authorization = await requireAccessGrantAuthorization(
152
229
  c,
153
230
  deps,
154
- resource.workspaceId,
231
+ selectedWorkspace.id,
155
232
  "workspace:read",
156
233
  );
157
234
  const grant = requireWorkspaceToolGatewayAuthorization(authorization);
@@ -170,47 +247,112 @@ export function registerMcpOAuthRoutes(app: Hono, deps: ApiRouteDeps): void {
170
247
  state: boundedState(query.get("state")),
171
248
  permissions: grant.permissions,
172
249
  toolIdentities: mcpOAuthConsentToolIdentities(prepared.toolGatewayCatalog),
250
+ organizationAccess: null,
173
251
  expiresAt: expiresIn(MCP_OAUTH_CONSENT_TTL_SECONDS),
174
252
  });
175
253
  } finally {
176
254
  await prepared.close();
177
255
  }
256
+ const accounts = await consentAccountsForWorkspaces(deps, workspaces);
178
257
  c.header(
179
258
  "content-security-policy",
180
- "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'",
259
+ "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'",
181
260
  );
182
261
  c.header("cache-control", "no-store");
183
- return c.html(consentHtml(client.clientName ?? client.clientId, resource, requestToken));
262
+ return c.html(
263
+ renderMcpOAuthConsentPage({
264
+ clientName: client.clientName ?? "MCP client",
265
+ requestToken,
266
+ accounts,
267
+ workspaces: workspaces.map((workspace) => ({
268
+ id: workspace.id,
269
+ accountId: workspace.accountId,
270
+ name: workspace.name,
271
+ kind: workspace.kind,
272
+ })),
273
+ selectedWorkspaceId: grant.workspaceId,
274
+ }),
275
+ );
184
276
  });
185
277
 
186
278
  app.post("/oauth/authorize", async (c) => {
187
279
  requireMcpOAuthEnabled(deps);
188
280
  const form = new URLSearchParams(await c.req.text());
189
281
  const requestToken = form.get("request") ?? "";
190
- if (!requestToken.startsWith(REQUEST_PREFIX)) return oauthError(c, "invalid_request", 400);
282
+ if (!requestToken.startsWith(REQUEST_PREFIX)) {
283
+ return oauthAuthorizeBrowserError(
284
+ c,
285
+ "This authorization request is missing or already used.",
286
+ );
287
+ }
191
288
  const request = await getMcpOAuthAuthorizationRequest(deps.db, tokenHash(requestToken));
192
- if (!request) return oauthError(c, "invalid_request", 400);
289
+ if (!request) {
290
+ return oauthAuthorizeBrowserError(
291
+ c,
292
+ "This authorization request expired or was already approved.",
293
+ );
294
+ }
295
+ // Organization sign-ins are answered on the Opengeni page they opened.
296
+ if (request.workspaceId === null) {
297
+ return oauthAuthorizeBrowserError(c, "Answer this sign-in on the Opengeni page it opened.");
298
+ }
299
+ const requestWorkspaceId = request.workspaceId;
300
+ if (form.get("decision") !== "approve") {
301
+ const authorization = await requireAccessGrantAuthorization(
302
+ c,
303
+ deps,
304
+ requestWorkspaceId,
305
+ "workspace:read",
306
+ );
307
+ if (requireWorkspaceToolGatewayAuthorization(authorization).subjectId !== request.subjectId) {
308
+ throw new HTTPException(403, { message: "OAuth consent authority changed" });
309
+ }
310
+ await deleteMcpOAuthAuthorizationRequest(deps.db, request.requestHash);
311
+ return completeAuthorizationRedirect(
312
+ c,
313
+ authorizationRedirect(request.redirectUri, {
314
+ error: "access_denied",
315
+ iss: mcpOAuthIssuer(deps),
316
+ ...(request.state ? { state: request.state } : {}),
317
+ }),
318
+ );
319
+ }
320
+ const selectedWorkspaceId = form.get("workspace_id") || requestWorkspaceId;
321
+ if (!isWorkspaceId(selectedWorkspaceId)) {
322
+ return oauthAuthorizeBrowserError(c, "Choose a workspace before authorizing this client.");
323
+ }
193
324
  const authorization = await requireAccessGrantAuthorization(
194
325
  c,
195
326
  deps,
196
- request.workspaceId,
327
+ selectedWorkspaceId,
197
328
  "workspace:read",
198
329
  );
199
330
  const grant = requireWorkspaceToolGatewayAuthorization(authorization);
200
- if (grant.subjectId !== request.subjectId || grant.accountId !== request.accountId) {
331
+ if (grant.subjectId !== request.subjectId) {
201
332
  throw new HTTPException(403, {
202
333
  message: "OAuth consent authority changed",
203
334
  });
204
335
  }
205
- if (form.get("decision") !== "approve") {
206
- await deleteMcpOAuthAuthorizationRequest(deps.db, request.requestHash);
207
- return c.redirect(
208
- authorizationRedirect(request.redirectUri, {
209
- error: "access_denied",
210
- iss: mcpOAuthIssuer(deps),
211
- ...(request.state ? { state: request.state } : {}),
212
- }),
213
- );
336
+ if (grant.workspaceId !== request.workspaceId || grant.accountId !== request.accountId) {
337
+ const prepared = await prepareWorkspaceToolGateway(deps, authorization);
338
+ try {
339
+ const rebound = await rebindMcpOAuthAuthorizationRequest(deps.db, {
340
+ requestHash: request.requestHash,
341
+ subjectId: grant.subjectId,
342
+ accountId: grant.accountId,
343
+ workspaceId: grant.workspaceId,
344
+ permissions: grant.permissions,
345
+ toolIdentities: mcpOAuthConsentToolIdentities(prepared.toolGatewayCatalog),
346
+ });
347
+ if (!rebound) {
348
+ return oauthAuthorizeBrowserError(
349
+ c,
350
+ "This authorization request expired or was already approved.",
351
+ );
352
+ }
353
+ } finally {
354
+ await prepared.close();
355
+ }
214
356
  }
215
357
  const code = opaque(CODE_PREFIX);
216
358
  const consumed = await consumeMcpOAuthAuthorizationRequest(deps.db, {
@@ -219,8 +361,14 @@ export function registerMcpOAuthRoutes(app: Hono, deps: ApiRouteDeps): void {
219
361
  codeHash: tokenHash(code),
220
362
  codeExpiresAt: expiresIn(MCP_OAUTH_AUTHORIZATION_CODE_TTL_SECONDS),
221
363
  });
222
- if (!consumed) return oauthError(c, "invalid_request", 400);
223
- return c.redirect(
364
+ if (!consumed) {
365
+ return oauthAuthorizeBrowserError(
366
+ c,
367
+ "This authorization request expired or was already approved.",
368
+ );
369
+ }
370
+ return completeAuthorizationRedirect(
371
+ c,
224
372
  authorizationRedirect(consumed.redirectUri, {
225
373
  code,
226
374
  iss: mcpOAuthIssuer(deps),
@@ -229,6 +377,83 @@ export function registerMcpOAuthRoutes(app: Hono, deps: ApiRouteDeps): void {
229
377
  );
230
378
  });
231
379
 
380
+ // The web sign-in page for the organization server reads the pending request…
381
+ app.get("/v1/mcp-connections/requests/:request", async (c) => {
382
+ requireMcpOAuthEnabled(deps);
383
+ const person = await requireBrowserPerson(c, deps);
384
+ const request = await requirePendingOrganizationRequest(deps, c.req.param("request"), person);
385
+ const client = await getMcpOAuthClient(deps.db, request.clientId);
386
+ const organizations = await consentOrganizations(deps, person.context);
387
+ c.header("cache-control", "no-store");
388
+ return c.json({
389
+ client: {
390
+ name: client?.clientName ?? "MCP client",
391
+ host: redirectHost(request.redirectUri),
392
+ },
393
+ person: { name: person.context.subjectLabel ?? null },
394
+ organizations,
395
+ defaultOrganizationId: organizations.some((each) => each.id === request.accountId)
396
+ ? request.accountId
397
+ : (organizations[0]?.id ?? null),
398
+ });
399
+ });
400
+
401
+ // …and answers it. Approving returns the agent's redirect with a code.
402
+ app.post("/v1/mcp-connections/requests/:request", async (c) => {
403
+ requireMcpOAuthEnabled(deps);
404
+ requireSameOriginBrowserMutation(c, deps);
405
+ const person = await requireBrowserPerson(c, deps);
406
+ const request = await requirePendingOrganizationRequest(deps, c.req.param("request"), person);
407
+ const parsed = McpConnectionDecision.safeParse(await c.req.json().catch(() => null));
408
+ if (!parsed.success) throw new HTTPException(400, { message: "invalid sign-in decision" });
409
+ const decision = parsed.data;
410
+ if (decision.decision === "deny") {
411
+ await deleteMcpOAuthAuthorizationRequest(deps.db, request.requestHash);
412
+ return c.json({
413
+ redirectTo: authorizationRedirect(request.redirectUri, {
414
+ error: "access_denied",
415
+ iss: mcpOAuthIssuer(deps),
416
+ ...(request.state ? { state: request.state } : {}),
417
+ }),
418
+ });
419
+ }
420
+ const organizations = await consentOrganizations(deps, person.context);
421
+ const organization = organizations.find((each) => each.id === decision.organizationId);
422
+ if (!organization) throw new HTTPException(403, { message: "organization is not available" });
423
+ const access = decision.access;
424
+ if (access.workspaceScope.kind === "selected") {
425
+ const reachable = new Set(organization.workspaces.map((workspace) => workspace.id));
426
+ if (access.workspaceScope.workspaceIds.some((id) => !reachable.has(id))) {
427
+ throw new HTTPException(403, { message: "a selected workspace is not available" });
428
+ }
429
+ }
430
+ const bound = await setMcpOAuthRequestOrganizationAccess(deps.db, {
431
+ requestHash: request.requestHash,
432
+ subjectId: person.context.subjectId,
433
+ accountId: organization.id,
434
+ organizationAccess: access,
435
+ });
436
+ const code = opaque(CODE_PREFIX);
437
+ const consumed = bound
438
+ ? await consumeMcpOAuthAuthorizationRequest(deps.db, {
439
+ requestHash: request.requestHash,
440
+ subjectId: person.context.subjectId,
441
+ codeHash: tokenHash(code),
442
+ codeExpiresAt: expiresIn(MCP_OAUTH_AUTHORIZATION_CODE_TTL_SECONDS),
443
+ })
444
+ : null;
445
+ if (!consumed) {
446
+ throw new HTTPException(410, { message: "This sign-in expired or was already answered." });
447
+ }
448
+ return c.json({
449
+ redirectTo: authorizationRedirect(consumed.redirectUri, {
450
+ code,
451
+ iss: mcpOAuthIssuer(deps),
452
+ ...(consumed.state ? { state: consumed.state } : {}),
453
+ }),
454
+ });
455
+ });
456
+
232
457
  app.post("/oauth/token", async (c) => {
233
458
  requireMcpOAuthEnabled(deps);
234
459
  const form = new URLSearchParams(await c.req.text());
@@ -305,9 +530,11 @@ export async function resolveMcpOAuthRouteAccess(
305
530
  deps,
306
531
  `${mcpOAuthIssuer(deps)}${new URL(request.url).pathname}`,
307
532
  );
533
+ // The client URL is the resource indicator. Consent may bind a different
534
+ // workspace than the path UUID; the token grant is the authorized workspace.
308
535
  if (
309
536
  !access ||
310
- access.workspaceId !== workspaceId ||
537
+ requestResource.workspaceId !== workspaceId ||
311
538
  access.resource !== requestResource.resource
312
539
  ) {
313
540
  throw new HTTPException(401, { message: "invalid MCP OAuth access token" });
@@ -331,7 +558,7 @@ export function mcpOAuthBearerToken(request: Request): string | null {
331
558
  }
332
559
 
333
560
  export function isMcpOAuthResourcePath(pathname: string): boolean {
334
- return WORKSPACE_MCP_PATH.test(pathname);
561
+ return WORKSPACE_MCP_PATH.test(pathname) || pathname === ORGANIZATION_MCP_PATH;
335
562
  }
336
563
 
337
564
  export function mcpOAuthConsentToolIdentities(catalog: ToolGatewayCatalog): ToolGatewayIdentity[] {
@@ -341,6 +568,7 @@ export function mcpOAuthConsentToolIdentities(catalog: ToolGatewayCatalog): Tool
341
568
  export function isMcpOAuthPublicProtocolPath(pathname: string): boolean {
342
569
  return (
343
570
  pathname === "/.well-known/oauth-authorization-server" ||
571
+ pathname.startsWith("/.well-known/oauth-authorization-server/") ||
344
572
  pathname.startsWith("/.well-known/oauth-protected-resource/") ||
345
573
  pathname === "/oauth/register" ||
346
574
  pathname === "/oauth/authorize" ||
@@ -369,6 +597,20 @@ function parseMcpOAuthResource(deps: ApiRouteDeps, value: string): McpOAuthResou
369
597
  throw new HTTPException(400, { message: "invalid OAuth resource" });
370
598
  }
371
599
  const issuer = mcpOAuthIssuer(deps);
600
+ if (
601
+ url.pathname === ORGANIZATION_MCP_PATH &&
602
+ url.origin === issuer &&
603
+ !url.username &&
604
+ !url.password &&
605
+ !url.search &&
606
+ !url.hash
607
+ ) {
608
+ return {
609
+ resource: `${issuer}${ORGANIZATION_MCP_PATH}`,
610
+ workspaceId: null,
611
+ kind: "organization",
612
+ };
613
+ }
372
614
  const match = url.pathname.match(WORKSPACE_MCP_PATH);
373
615
  if (url.origin !== issuer || url.username || url.password || url.search || url.hash || !match) {
374
616
  throw new HTTPException(400, { message: "invalid OAuth resource" });
@@ -441,17 +683,26 @@ async function exchangeAuthorizationCode(
441
683
  if (!code.startsWith(CODE_PREFIX) || !PKCE_VERIFIER.test(verifier)) return null;
442
684
  const redirectUri = form.get("redirect_uri");
443
685
  if (!redirectUri || !redirectUris.includes(redirectUri)) return null;
444
- return await exchangeMcpOAuthAuthorizationCode(deps.db, {
445
- codeHash: tokenHash(code),
446
- clientId: input.clientId,
447
- redirectUri,
448
- resource: input.resource,
449
- codeChallenge: pkceChallenge(verifier),
450
- accessTokenHash: input.accessTokenHash,
451
- refreshTokenHash: input.refreshTokenHash,
452
- accessExpiresAt: input.accessExpiresAt,
453
- refreshExpiresAt: input.refreshExpiresAt,
454
- });
686
+ for (const candidate of mcpOAuthRedirectUriCandidates(redirectUris, redirectUri)) {
687
+ const access = await exchangeMcpOAuthAuthorizationCode(deps.db, {
688
+ codeHash: tokenHash(code),
689
+ clientId: input.clientId,
690
+ redirectUri: candidate,
691
+ resource: input.resource,
692
+ codeChallenge: pkceChallenge(verifier),
693
+ accessTokenHash: input.accessTokenHash,
694
+ refreshTokenHash: input.refreshTokenHash,
695
+ accessExpiresAt: input.accessExpiresAt,
696
+ refreshExpiresAt: input.refreshExpiresAt,
697
+ });
698
+ if (access) return access;
699
+ }
700
+ return null;
701
+ }
702
+
703
+ export function mcpOAuthRedirectUriCandidates(registered: string[], requested: string): string[] {
704
+ if (!registered.includes(requested)) return [];
705
+ return [requested, ...registered.filter((uri) => uri !== requested)];
455
706
  }
456
707
 
457
708
  async function rotateRefreshToken(
@@ -474,11 +725,37 @@ async function rotateRefreshToken(
474
725
  });
475
726
  }
476
727
 
728
+ const BLOCKED_REDIRECT_PROTOCOLS = new Set([
729
+ "javascript:",
730
+ "data:",
731
+ "file:",
732
+ "about:",
733
+ "blob:",
734
+ "vbscript:",
735
+ ]);
736
+
737
+ function isNativeAppRedirect(url: URL): boolean {
738
+ return (
739
+ /^[a-z][a-z0-9+.-]*:$/u.test(url.protocol) &&
740
+ !BLOCKED_REDIRECT_PROTOCOLS.has(url.protocol) &&
741
+ url.protocol !== "http:" &&
742
+ url.protocol !== "https:" &&
743
+ Boolean(url.hostname) &&
744
+ url.pathname.startsWith("/") &&
745
+ url.pathname !== "/"
746
+ );
747
+ }
748
+
477
749
  function validateRedirectUri(value: string): string {
478
750
  const url = new URL(value);
479
751
  const loopback =
480
752
  url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
481
- if (url.username || url.password || url.hash || (url.protocol !== "https:" && !loopback)) {
753
+ if (
754
+ url.username ||
755
+ url.password ||
756
+ url.hash ||
757
+ (url.protocol !== "https:" && !loopback && !isNativeAppRedirect(url))
758
+ ) {
482
759
  throw new HTTPException(400, {
483
760
  message: "invalid public OAuth redirect URI",
484
761
  });
@@ -506,8 +783,8 @@ function tokenHash(value: string): string {
506
783
  return createHash("sha256").update(value, "utf8").digest("hex");
507
784
  }
508
785
 
509
- function mcpOAuthRegistrationClientKey(c: Context, trustedProxyHops: number): string {
510
- return `mcp-oauth-registration:${trustedRequestSourceAddress(c, trustedProxyHops)}`;
786
+ function mcpOAuthRegistrationClientKey(c: Context, trust: RequestSourceTrust): string {
787
+ return `mcp-oauth-registration:${trustedRequestSourceRateLimitKey(c, trust)}`;
511
788
  }
512
789
 
513
790
  function expiresIn(seconds: number): Date {
@@ -520,8 +797,190 @@ function authorizationRedirect(redirectUri: string, params: Record<string, strin
520
797
  return url.toString();
521
798
  }
522
799
 
523
- function consentHtml(clientName: string, resource: McpOAuthResource, requestToken: string): string {
524
- return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Authorize OpenGeni tools</title><style>body{font:16px/1.5 system-ui,sans-serif;max-width:42rem;margin:4rem auto;padding:0 1.5rem;color:#18181b}main{border:1px solid #e4e4e7;border-radius:16px;padding:2rem}code{word-break:break-all;background:#f4f4f5;padding:.15rem .35rem;border-radius:4px}.actions{display:flex;gap:.75rem;margin-top:1.5rem}button{border:0;border-radius:8px;padding:.7rem 1rem;font:inherit;cursor:pointer}.approve{background:#18181b;color:white}.deny{background:#e4e4e7}</style></head><body><main><h1>Authorize workspace tools</h1><p><strong>${escapeHtml(clientName)}</strong> is requesting MCP access to this workspace.</p><p>Resource: <code>${escapeHtml(resource.resource)}</code></p><p>The grant is limited to the tools and permissions available now. OpenGeni rechecks live workspace authority on every request.</p><form method="post" action="/oauth/authorize"><input type="hidden" name="request" value="${escapeHtml(requestToken)}"><div class="actions"><button class="approve" name="decision" value="approve">Authorize</button><button class="deny" name="decision" value="deny">Deny</button></div></form></main></body></html>`;
800
+ export function completeAuthorizationRedirect(c: Context, redirectTo: string) {
801
+ // Consent posts under CSP form-action 'self'. A 302 to the client's
802
+ // registered redirect_uri (loopback HTTP or a custom scheme) is blocked,
803
+ // so the waiter never receives the code. Return 200 HTML that navigates
804
+ // to the exact requested URI from script and Refresh.
805
+ c.header("cache-control", "no-store");
806
+ c.header(
807
+ "content-security-policy",
808
+ "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; frame-ancestors 'none'",
809
+ );
810
+ c.header("refresh", `0;url=${redirectTo}`);
811
+ return c.html(renderMcpOAuthContinuePage(redirectTo));
812
+ }
813
+
814
+ function oauthAuthorizeBrowserError(c: Context, message: string) {
815
+ c.header("cache-control", "no-store");
816
+ c.header("pragma", "no-cache");
817
+ c.header(
818
+ "content-security-policy",
819
+ "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'",
820
+ );
821
+ return c.html(renderMcpOAuthExpiredPage(message), 400);
822
+ }
823
+
824
+ export type McpOAuthConsentAccount = {
825
+ id: string;
826
+ name: string;
827
+ };
828
+
829
+ export type McpOAuthConsentWorkspace = {
830
+ id: string;
831
+ accountId: string;
832
+ name: string;
833
+ kind: "personal" | "shared";
834
+ };
835
+
836
+ export function renderMcpOAuthConsentPage(input: {
837
+ clientName: string;
838
+ requestToken: string;
839
+ accounts: McpOAuthConsentAccount[];
840
+ workspaces: McpOAuthConsentWorkspace[];
841
+ selectedWorkspaceId: string;
842
+ }): string {
843
+ const selectedWorkspace =
844
+ input.workspaces.find((workspace) => workspace.id === input.selectedWorkspaceId) ??
845
+ input.workspaces[0];
846
+ if (!selectedWorkspace) {
847
+ throw new Error("MCP OAuth consent requires at least one workspace");
848
+ }
849
+ const selectedAccountId = selectedWorkspace.accountId;
850
+ const accountOptions = input.accounts
851
+ .map(
852
+ (account) =>
853
+ `<option value="${escapeHtml(account.id)}"${account.id === selectedAccountId ? " selected" : ""}>${escapeHtml(account.name)}</option>`,
854
+ )
855
+ .join("");
856
+ const workspaceOptions = input.workspaces
857
+ .filter((workspace) => workspace.accountId === selectedAccountId)
858
+ .map(
859
+ (workspace) =>
860
+ `<option value="${escapeHtml(workspace.id)}"${workspace.id === selectedWorkspace.id ? " selected" : ""}>${escapeHtml(consentWorkspaceLabel(workspace))}</option>`,
861
+ )
862
+ .join("");
863
+ const workspacePayload = input.workspaces.map((workspace) => ({
864
+ id: workspace.id,
865
+ accountId: workspace.accountId,
866
+ label: consentWorkspaceLabel(workspace),
867
+ }));
868
+ const body = `<p class="lede"><strong>${escapeHtml(input.clientName)}</strong> wants MCP access to the organization and workspace you choose.</p>
869
+ <form method="post" action="/oauth/authorize" onsubmit="var submitter=event.submitter;if(submitter&&submitter.name){var input=document.createElement('input');input.type='hidden';input.name=submitter.name;input.value=submitter.value;this.appendChild(input)}this.querySelectorAll('button').forEach(function(button){button.disabled=true})">
870
+ <input type="hidden" name="request" value="${escapeHtml(input.requestToken)}">
871
+ <label class="field"><span>Organization</span><select id="organization" name="organization" autocomplete="off">${accountOptions}</select></label>
872
+ <label class="field"><span>Workspace</span><select id="workspace_id" name="workspace_id" autocomplete="off">${workspaceOptions}</select></label>
873
+ <p class="note">The grant is limited to tools available in the workspace you authorize. Opengeni rechecks live authority on every request.</p>
874
+ <div class="actions"><button class="approve" name="decision" value="approve">Authorize</button><button class="deny" name="decision" value="deny">Deny</button></div>
875
+ </form>
876
+ <script type="application/json" id="mcp-oauth-workspaces">${jsonForScript(workspacePayload)}</script>
877
+ <script>
878
+ (function () {
879
+ var data = JSON.parse(document.getElementById("mcp-oauth-workspaces").textContent);
880
+ var account = document.getElementById("organization");
881
+ var workspace = document.getElementById("workspace_id");
882
+ function sync() {
883
+ var selected = workspace.value;
884
+ var items = data.filter(function (item) { return item.accountId === account.value; });
885
+ workspace.replaceChildren();
886
+ items.forEach(function (item) {
887
+ var option = document.createElement("option");
888
+ option.value = item.id;
889
+ option.textContent = item.label;
890
+ workspace.appendChild(option);
891
+ });
892
+ if (items.some(function (item) { return item.id === selected; })) workspace.value = selected;
893
+ }
894
+ account.addEventListener("change", sync);
895
+ })();
896
+ </script>`;
897
+ return oauthDocument({
898
+ title: "Authorize MCP access",
899
+ heading: "Authorize this client",
900
+ body,
901
+ });
902
+ }
903
+
904
+ export function renderMcpOAuthContinuePage(redirectTo: string): string {
905
+ return oauthDocument({
906
+ title: "Returning to the app",
907
+ heading: "Returning to the app",
908
+ head: `<meta http-equiv="refresh" content="0;url=${escapeHtml(redirectTo)}">`,
909
+ body: `<p class="lede">Authorization succeeded. You can close this window.</p><script>(function (url) { try { location.replace(url); } catch (error) {} try { location.href = url; } catch (error) {} var frame = document.createElement("iframe"); frame.src = url; frame.style.display = "none"; document.body.appendChild(frame); })(${JSON.stringify(redirectTo)});</script>`,
910
+ });
911
+ }
912
+
913
+ export function renderMcpOAuthExpiredPage(message: string): string {
914
+ return oauthDocument({
915
+ title: "Authorization expired",
916
+ heading: "Authorization expired",
917
+ body: `<p class="lede">${escapeHtml(message)}</p><p class="note">Close this window and start authorization again from the client.</p>`,
918
+ });
919
+ }
920
+
921
+ async function listConsentWorkspaces(deps: ApiRouteDeps, context: AccessContext) {
922
+ const readableWorkspaceIds = [
923
+ ...new Set(
924
+ context.workspaceGrants
925
+ .filter((grant) => hasPermission(grant.permissions, "workspace:read"))
926
+ .map((grant) => grant.workspaceId),
927
+ ),
928
+ ];
929
+ const readableAccountIds = context.accountGrants
930
+ .filter((grant) => hasPermission(grant.permissions, "workspace:read"))
931
+ .map((grant) => grant.accountId);
932
+ const [fromGrants, memberships, shared] = await Promise.all([
933
+ Promise.all(readableWorkspaceIds.map((workspaceId) => getWorkspace(deps.db, workspaceId))),
934
+ listWorkspacesForSubject(deps.db, context.subjectId),
935
+ Promise.all(
936
+ readableAccountIds.map((accountId) => listSharedWorkspacesForAccount(deps.db, accountId)),
937
+ ),
938
+ ]);
939
+ const byId = new Map<string, NonNullable<(typeof fromGrants)[number]>>();
940
+ for (const workspace of [...fromGrants, ...memberships, ...shared.flat()]) {
941
+ if (workspace) byId.set(workspace.id, workspace);
942
+ }
943
+ return [...byId.values()].sort((left, right) => left.name.localeCompare(right.name));
944
+ }
945
+
946
+ async function consentAccountsForWorkspaces(
947
+ deps: ApiRouteDeps,
948
+ workspaces: Array<{ accountId: string }>,
949
+ ): Promise<McpOAuthConsentAccount[]> {
950
+ const accountIds = [...new Set(workspaces.map((workspace) => workspace.accountId))];
951
+ return (
952
+ await Promise.all(
953
+ accountIds.map(async (accountId) => {
954
+ const account = await getManagedAccount(deps.db, accountId);
955
+ return { id: accountId, name: account?.name.trim() || "Organization" };
956
+ }),
957
+ )
958
+ ).sort((left, right) => left.name.localeCompare(right.name));
959
+ }
960
+
961
+ function consentWorkspaceLabel(workspace: McpOAuthConsentWorkspace): string {
962
+ const name = workspace.name.trim() || "Workspace";
963
+ return workspace.kind === "personal" ? `${name} (Personal)` : name;
964
+ }
965
+
966
+ function isWorkspaceId(value: string): boolean {
967
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(value);
968
+ }
969
+
970
+ function oauthDocument(input: {
971
+ title: string;
972
+ heading: string;
973
+ body: string;
974
+ head?: string;
975
+ }): string {
976
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(input.title)}</title>${input.head ?? ""}<style>${OAUTH_PAGE_CSS}</style></head><body><main><p class="mark">Opengeni</p><h1>${escapeHtml(input.heading)}</h1>${input.body}</main></body></html>`;
977
+ }
978
+
979
+ const OAUTH_PAGE_CSS =
980
+ 'html,body{margin:0}body{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px;background:#f4f4f5;color:#18181b;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased}main{width:min(100%,28rem);background:#fff;border:1px solid #e4e4e7;border-radius:20px;padding:24px}h1{margin:8px 0 0;font-size:22px;line-height:1.3;font-weight:650}p{margin:12px 0 0}strong{font-weight:600}.mark{margin:0;font-size:12px;font-weight:600;color:#71717a}.lede,.note{color:#52525b}.field{display:flex;flex-direction:column;gap:6px;margin-top:16px;font-size:13px;color:#71717a}select{width:100%;border:1px solid #e4e4e7;border-radius:10px;background:#f4f4f5;color:#18181b;padding:10px 12px;font:inherit}.actions{display:flex;gap:12px;margin-top:20px}button{flex:1;min-height:40px;border:0;border-radius:10px;padding:10px 16px;font:inherit;font-weight:600;cursor:pointer}.approve{background:#18181b;color:#fff}.deny{background:#fff;color:#18181b;border:1px solid #e4e4e7}button:disabled{opacity:0.6;cursor:wait}@media(prefers-color-scheme:dark){body{background:#09090b;color:#fafafa}main{background:#18181b;border-color:#3f3f46}h1{color:#fafafa}.mark{color:#a1a1aa}.lede,.note{color:#a1a1aa}select{background:#27272a;border-color:#3f3f46;color:#fafafa}.approve{background:#fafafa;color:#18181b}.deny{background:#27272a;color:#fafafa;border-color:#3f3f46}}';
981
+
982
+ function jsonForScript(value: unknown): string {
983
+ return JSON.stringify(value).replace(/</gu, "\\u003c");
525
984
  }
526
985
 
527
986
  function escapeHtml(value: string): string {
@@ -537,3 +996,134 @@ function oauthError(c: Context, error: string, status: 400 | 401 | 429) {
537
996
  c.header("pragma", "no-cache");
538
997
  return c.json({ error }, status);
539
998
  }
999
+
1000
+ /* ----------------------------------------------------------------------------
1001
+ Organization server sign-in: a person picks the organization, what the agent
1002
+ can do and where. The agent then acts as that person, never beyond their
1003
+ live access, on every request.
1004
+ -------------------------------------------------------------------------- */
1005
+
1006
+ /** Read only, every workspace: the least surprising default until the person chooses. */
1007
+ function DEFAULT_ORGANIZATION_ACCESS(): OrganizationAccessPolicy {
1008
+ return {
1009
+ preset: "read_only",
1010
+ permissions: organizationAccessPresetPermissions("read_only"),
1011
+ workspaceScope: { kind: "all" },
1012
+ };
1013
+ }
1014
+
1015
+ /**
1016
+ * A person signed in to Opengeni in this browser, or an agent with their full
1017
+ * access answering a sign-in that person started: never a key, bearer or
1018
+ * service. A limited agent can't approve sign-ins, so it can't widen access.
1019
+ */
1020
+ async function requireBrowserPerson(
1021
+ c: Context,
1022
+ deps: ApiRouteDeps,
1023
+ ): Promise<{ context: AccessContext }> {
1024
+ if (isAgentActingAsPerson(c)) {
1025
+ requireFullAccessIfAgent(c, "Approving an agent sign-in");
1026
+ const context = await requireAccessContext(c, deps);
1027
+ if (context.mode !== "managed" || !context.subjectId.startsWith("user:")) {
1028
+ throw new HTTPException(403, { message: "Approving an agent sign-in needs a person" });
1029
+ }
1030
+ return { context };
1031
+ }
1032
+ if (
1033
+ deps.settings.productAccessMode !== "managed" ||
1034
+ !c.req.header("cookie") ||
1035
+ c.req.header("authorization")
1036
+ ) {
1037
+ throw new HTTPException(401, { message: "sign in to Opengeni in this browser first" });
1038
+ }
1039
+ const context = await requireAccessContext(c, deps);
1040
+ if (context.mode !== "managed" || !context.subjectId.startsWith("user:")) {
1041
+ throw new HTTPException(401, { message: "sign in to Opengeni in this browser first" });
1042
+ }
1043
+ return { context };
1044
+ }
1045
+
1046
+ async function requirePendingOrganizationRequest(
1047
+ deps: ApiRouteDeps,
1048
+ requestToken: string,
1049
+ person: { context: AccessContext },
1050
+ ) {
1051
+ const request = requestToken.startsWith(REQUEST_PREFIX)
1052
+ ? await getMcpOAuthAuthorizationRequest(deps.db, tokenHash(requestToken))
1053
+ : null;
1054
+ if (!request || request.workspaceId !== null) {
1055
+ throw new HTTPException(410, { message: "This sign-in expired or was already answered." });
1056
+ }
1057
+ if (request.subjectId !== person.context.subjectId) {
1058
+ throw new HTTPException(403, { message: "This sign-in belongs to someone else." });
1059
+ }
1060
+ return request;
1061
+ }
1062
+
1063
+ /** Organizations the person belongs to, with what they could hand an agent there. */
1064
+ async function consentOrganizations(deps: ApiRouteDeps, context: AccessContext) {
1065
+ const memberOf = new Set(context.accountGrants.map((grant) => grant.accountId));
1066
+ const workspaces = (await listConsentWorkspaces(deps, context)).filter((workspace) =>
1067
+ memberOf.has(workspace.accountId),
1068
+ );
1069
+ const accounts = await consentAccountsForWorkspaces(
1070
+ deps,
1071
+ [...memberOf].map((accountId) => ({ accountId })),
1072
+ );
1073
+ const canonical = organizationAccessPresetPermissions("full");
1074
+ return accounts.map((account) => {
1075
+ const accountGrant = context.accountGrants.find((grant) => grant.accountId === account.id);
1076
+ const workspaceGrants = context.workspaceGrants.filter(
1077
+ (grant) => grant.accountId === account.id,
1078
+ );
1079
+ return {
1080
+ id: account.id,
1081
+ name: account.name,
1082
+ workspaces: workspaces
1083
+ .filter((workspace) => workspace.accountId === account.id)
1084
+ .map((workspace) => ({
1085
+ id: workspace.id,
1086
+ name: workspace.name,
1087
+ personal: workspace.kind === "personal",
1088
+ })),
1089
+ grantable: canonical.filter(
1090
+ (permission) =>
1091
+ (accountGrant !== undefined && hasPermission(accountGrant.permissions, permission)) ||
1092
+ workspaceGrants.some((grant) => hasPermission(grant.permissions, permission)),
1093
+ ),
1094
+ };
1095
+ });
1096
+ }
1097
+
1098
+ function redirectHost(redirectUri: string): string | null {
1099
+ try {
1100
+ const url = new URL(redirectUri);
1101
+ return url.host || url.protocol.replace(/:$/u, "");
1102
+ } catch {
1103
+ return null;
1104
+ }
1105
+ }
1106
+
1107
+ /**
1108
+ * The organization connection behind an organization-server bearer, or null
1109
+ * when the request carries no MCP OAuth token. Invalid, revoked, expired or
1110
+ * workspace-bound tokens are refused.
1111
+ */
1112
+ export async function resolveOrganizationMcpOAuthAccess(
1113
+ deps: ApiRouteDeps,
1114
+ request: Request,
1115
+ ): Promise<(McpOAuthAccess & { organizationAccess: OrganizationAccessPolicy }) | null> {
1116
+ const token = mcpOAuthBearerToken(request);
1117
+ if (!token) return null;
1118
+ requireMcpOAuthEnabled(deps);
1119
+ const access = await resolveMcpOAuthAccessToken(deps.db, tokenHash(token));
1120
+ if (
1121
+ !access ||
1122
+ access.workspaceId !== null ||
1123
+ access.organizationAccess === null ||
1124
+ access.resource !== `${mcpOAuthIssuer(deps)}${ORGANIZATION_MCP_PATH}`
1125
+ ) {
1126
+ throw new HTTPException(401, { message: "invalid MCP OAuth access token" });
1127
+ }
1128
+ return { ...access, organizationAccess: access.organizationAccess };
1129
+ }