@opengeni/api-router 0.30.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (436) hide show
  1. package/dist/access-grant-rls.d.ts +3 -0
  2. package/dist/admission-trace.d.ts +8 -0
  3. package/dist/api-websocket.d.ts +18 -0
  4. package/dist/app.d.ts +50 -2
  5. package/dist/app.js +12 -1
  6. package/dist/artifact-catalog-cursor.d.ts +27 -0
  7. package/dist/auth/managed-auth-attempt-context.d.ts +10 -0
  8. package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
  9. package/dist/auth/managed-auth-session-adapter.d.ts +6 -0
  10. package/dist/auth/managed-auth.d.ts +102 -3
  11. package/dist/auth/managed-email.d.ts +29 -0
  12. package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
  13. package/dist/auth/new-signups-gate.d.ts +23 -0
  14. package/dist/auth/organization-user-setup.d.ts +80 -0
  15. package/dist/auth/signup-funnel-metrics.d.ts +44 -0
  16. package/dist/browser-controller-authority.d.ts +1 -61
  17. package/dist/browser-ephemeral.d.ts +5 -0
  18. package/dist/browser-state-authority.d.ts +1 -27
  19. package/dist/chunk-6TZ5LG3O.js +123948 -0
  20. package/dist/chunk-6TZ5LG3O.js.map +1 -0
  21. package/dist/chunk-OGQDXVXY.js +18 -0
  22. package/dist/chunk-OGQDXVXY.js.map +1 -0
  23. package/dist/claude-subscription-account-conflicts.d.ts +1 -0
  24. package/dist/claude-subscription-account-usage.d.ts +39 -0
  25. package/dist/claude-subscription-oauth.d.ts +25 -0
  26. package/dist/claude-subscription-profile.d.ts +9 -0
  27. package/dist/claude-subscription-usage.d.ts +44 -0
  28. package/dist/claude-workspace-connection.d.ts +15 -0
  29. package/dist/codemode.d.ts +32 -1
  30. package/dist/company-brain-okf.d.ts +24 -0
  31. package/dist/connected-machine-computer-access.d.ts +11 -0
  32. package/dist/connection-ownership.d.ts +54 -0
  33. package/dist/controller-data-plane.d.ts +32 -0
  34. package/dist/editable-artifact-office-import.d.ts +7 -4
  35. package/dist/editable-artifact-source-session.d.ts +3 -0
  36. package/dist/editable-artifact-websocket.d.ts +10 -18
  37. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  38. package/dist/fatal-process-boundary.d.ts +25 -0
  39. package/dist/github-access.d.ts +31 -2
  40. package/dist/http/acting-person.d.ts +24 -0
  41. package/dist/http/api-contract-compat.d.ts +19 -0
  42. package/dist/http/api-error.d.ts +27 -1
  43. package/dist/http/cors.d.ts +17 -0
  44. package/dist/http/deprecation.d.ts +42 -0
  45. package/dist/http/interaction-control-error.d.ts +13 -0
  46. package/dist/http/keyed-admission.d.ts +12 -0
  47. package/dist/http/local-browser-boundary.d.ts +72 -0
  48. package/dist/http/metrics-listener.d.ts +22 -0
  49. package/dist/http/path-identifier.d.ts +17 -0
  50. package/dist/http/registered-route-label.d.ts +16 -0
  51. package/dist/http/request-body.d.ts +27 -0
  52. package/dist/http/request-source.d.ts +52 -0
  53. package/dist/http/sse.d.ts +17 -6
  54. package/dist/http/unmatched-route.d.ts +18 -0
  55. package/dist/http/user-content.d.ts +36 -0
  56. package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
  57. package/dist/index.d.ts +10 -3
  58. package/dist/index.js +1129 -166
  59. package/dist/index.js.map +1 -1
  60. package/dist/integration-endpoint-test.d.ts +20 -0
  61. package/dist/integrations/api-integrations.d.ts +2 -2
  62. package/dist/integrations/atlassian.d.ts +29 -3
  63. package/dist/integrations/connect-authority.d.ts +1 -0
  64. package/dist/integrations/connect-callback-return.d.ts +6 -0
  65. package/dist/integrations/fiken.d.ts +16 -2
  66. package/dist/integrations/github-app-connect.d.ts +47 -0
  67. package/dist/integrations/github-installation-proof.d.ts +3 -0
  68. package/dist/integrations/github-lens-connect.d.ts +116 -0
  69. package/dist/integrations/github-skill-source.d.ts +1 -5
  70. package/dist/integrations/google-drive.d.ts +25 -0
  71. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  72. package/dist/integrations/marketplace-plugin.d.ts +2691 -0
  73. package/dist/integrations/oauth-client.d.ts +178 -18
  74. package/dist/integrations/oauth-profiles.d.ts +222 -0
  75. package/dist/integrations/oauth-return-path.d.ts +1 -0
  76. package/dist/integrations/personal-github-repositories.d.ts +84 -0
  77. package/dist/integrations/personal-github.d.ts +42 -0
  78. package/dist/integrations/pr-review-provider.d.ts +22 -0
  79. package/dist/integrations/provider-oauth.d.ts +19 -1
  80. package/dist/integrations/slack-app-home.d.ts +24 -0
  81. package/dist/integrations/slack-bot.d.ts +367 -12
  82. package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
  83. package/dist/integrations/slack-install.d.ts +20 -0
  84. package/dist/integrations/slack-interactions.d.ts +113 -10
  85. package/dist/integrations/slack-mrkdwn.d.ts +41 -0
  86. package/dist/integrations/slack-routing.d.ts +120 -0
  87. package/dist/integrations/slack-session-defaults.d.ts +58 -0
  88. package/dist/integrations/slack-task-file-upload.d.ts +36 -0
  89. package/dist/integrations/social-oauth.d.ts +7 -0
  90. package/dist/interaction-agent-access.d.ts +15 -0
  91. package/dist/interaction-frame-proxy.d.ts +68 -0
  92. package/dist/interaction-metrics.d.ts +2 -0
  93. package/dist/mcp/action-catalog-types.d.ts +9 -0
  94. package/dist/mcp/action-catalog.gen.d.ts +2 -0
  95. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  96. package/dist/mcp/child-read-evidence.d.ts +9 -0
  97. package/dist/mcp/command-read.d.ts +24 -0
  98. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  99. package/dist/mcp/contract-input.d.ts +11 -0
  100. package/dist/mcp/documents.d.ts +7 -5
  101. package/dist/mcp/files.d.ts +2 -2
  102. package/dist/mcp/first-party-tool-permissions.d.ts +650 -0
  103. package/dist/mcp/knowledge-entries.d.ts +5 -0
  104. package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
  105. package/dist/mcp/receipts.d.ts +9 -0
  106. package/dist/mcp/request-abort.d.ts +19 -0
  107. package/dist/mcp/scheduled-task-input.d.ts +1189 -0
  108. package/dist/mcp/scheduled-task-view.d.ts +22 -4
  109. package/dist/mcp/server.d.ts +255 -3
  110. package/dist/mcp/session-event-view.d.ts +94 -0
  111. package/dist/mcp/session-view.d.ts +66 -2
  112. package/dist/mcp/session-wait.d.ts +174 -0
  113. package/dist/mcp-oauth.d.ts +52 -0
  114. package/dist/model-catalog.d.ts +11 -31
  115. package/dist/observability.d.ts +3 -1
  116. package/dist/organization-mcp.d.ts +52 -0
  117. package/dist/organization-recovery-notifications.d.ts +41 -0
  118. package/dist/routes/analytics-consent.d.ts +26 -0
  119. package/dist/routes/api-integrations.d.ts +25 -0
  120. package/dist/routes/api-keys.d.ts +17 -0
  121. package/dist/routes/artifact-catalog.d.ts +6 -0
  122. package/dist/routes/automations.d.ts +19 -0
  123. package/dist/routes/billing.d.ts +49 -0
  124. package/dist/routes/browser-sessions.d.ts +71 -3
  125. package/dist/routes/claude-subscription-accounts.d.ts +3 -0
  126. package/dist/routes/claude-subscription-oauth.d.ts +3 -0
  127. package/dist/routes/client-errors.d.ts +146 -0
  128. package/dist/routes/codex.d.ts +76 -2
  129. package/dist/routes/company-brain.d.ts +3 -0
  130. package/dist/routes/company-profile.d.ts +2 -1
  131. package/dist/routes/computer-sessions.d.ts +14 -0
  132. package/dist/routes/connect.d.ts +5 -0
  133. package/dist/routes/editable-artifacts.d.ts +2 -0
  134. package/dist/routes/external-identity-links.d.ts +6 -0
  135. package/dist/routes/feedback.d.ts +3 -0
  136. package/dist/routes/files.d.ts +4 -0
  137. package/dist/routes/github-browser-pages.d.ts +12 -0
  138. package/dist/routes/github.d.ts +2 -0
  139. package/dist/routes/insights-response-cache.d.ts +10 -0
  140. package/dist/routes/insights-usage.d.ts +12 -0
  141. package/dist/routes/insights.d.ts +30 -1
  142. package/dist/routes/knowledge.d.ts +3 -0
  143. package/dist/routes/managed-auth-session-sets.d.ts +26 -0
  144. package/dist/routes/managed-onboarding.d.ts +29 -0
  145. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  146. package/dist/routes/model-connection-access.d.ts +3 -0
  147. package/dist/routes/organization-integration-policy.d.ts +3 -0
  148. package/dist/routes/organization-integrations.d.ts +6 -0
  149. package/dist/routes/organization-mcp-connections.d.ts +3 -0
  150. package/dist/routes/organization-model-providers.d.ts +3 -0
  151. package/dist/routes/organization-recovery.d.ts +19 -0
  152. package/dist/routes/organization-service-accounts.d.ts +3 -0
  153. package/dist/routes/organization-sessions.d.ts +26 -0
  154. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  155. package/dist/routes/personal-github.d.ts +3 -0
  156. package/dist/routes/pr-review-github.d.ts +3 -0
  157. package/dist/routes/pr-review.d.ts +3 -0
  158. package/dist/routes/session-artifact-associations.d.ts +4 -0
  159. package/dist/routes/session-history-imports.d.ts +9 -0
  160. package/dist/routes/sessions.d.ts +44 -6
  161. package/dist/routes/skill-content.d.ts +4 -0
  162. package/dist/routes/skill-install-authority.d.ts +6 -0
  163. package/dist/routes/subscription-account-pools.d.ts +34 -0
  164. package/dist/routes/subscription-pool-access.d.ts +16 -0
  165. package/dist/routes/supergrok.d.ts +6 -1
  166. package/dist/routes/usage-allowances.d.ts +12 -0
  167. package/dist/routes/user-resource-authorities.d.ts +3 -0
  168. package/dist/routes/workspace-artifacts.d.ts +2 -1
  169. package/dist/routes/workspace-capture.d.ts +10 -2
  170. package/dist/routes/workspace-integrations.d.ts +35 -0
  171. package/dist/routes/workspace-learning.d.ts +3 -0
  172. package/dist/routes/workspace-model-providers.d.ts +4 -0
  173. package/dist/routes/workspaces.d.ts +29 -0
  174. package/dist/sandbox/auth-callout.d.ts +8 -0
  175. package/dist/sandbox/channel-a.d.ts +19 -1
  176. package/dist/sandbox/connection-authority.d.ts +3 -0
  177. package/dist/sandbox/enrollment.d.ts +9 -1
  178. package/dist/sandbox/machines.d.ts +5 -2
  179. package/dist/sandbox/metrics-ingestion.d.ts +28 -20
  180. package/dist/sandbox/rematerialize.d.ts +2 -0
  181. package/dist/sandbox/session-attach-variable-sets.d.ts +30 -0
  182. package/dist/sandbox/viewer.d.ts +26 -1
  183. package/dist/sandbox-file-artifacts.d.ts +50 -0
  184. package/dist/scheduled-task-deletion.d.ts +13 -0
  185. package/dist/session-create-observability.d.ts +5 -0
  186. package/dist/session-event-view-MPS2OZD3.js +292 -0
  187. package/dist/session-event-view-MPS2OZD3.js.map +1 -0
  188. package/dist/site-session-origin.d.ts +13 -0
  189. package/dist/site-uploads.d.ts +41 -0
  190. package/dist/slack-reaction-files.d.ts +1 -1
  191. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  192. package/dist/temporal-schedule-sync.d.ts +26 -0
  193. package/dist/transcription/providers/xai-subscription.d.ts +1 -0
  194. package/dist/transcription/service.d.ts +2 -1
  195. package/dist/work-discovery-observability.d.ts +33 -0
  196. package/dist/workspace-artifact-content.d.ts +28 -0
  197. package/dist/workspace-artifact-provenance.d.ts +7 -0
  198. package/dist/workspace-delete-observability.d.ts +10 -0
  199. package/dist/workspace-deletion.d.ts +6 -0
  200. package/dist/workspace-state-projection.d.ts +3 -5
  201. package/dist/workspace-tool-gateway-observability.d.ts +17 -0
  202. package/dist/workspace-tool-gateway.d.ts +123 -0
  203. package/dist/workspace-webhook-dispatch.d.ts +26 -0
  204. package/package.json +23 -19
  205. package/src/access-grant-rls.ts +75 -0
  206. package/src/admission-trace.ts +14 -0
  207. package/src/api-websocket.ts +23 -0
  208. package/src/app.ts +1974 -256
  209. package/src/artifact-catalog-cursor.ts +60 -0
  210. package/src/auth/managed-auth-attempt-context.ts +70 -0
  211. package/src/auth/managed-auth-rate-limits.ts +252 -0
  212. package/src/auth/managed-auth-session-adapter.ts +222 -0
  213. package/src/auth/managed-auth.ts +536 -60
  214. package/src/auth/managed-email.ts +174 -0
  215. package/src/auth/managed-sign-in-notifications.ts +81 -0
  216. package/src/auth/new-signups-gate.ts +49 -0
  217. package/src/auth/organization-user-setup.ts +307 -0
  218. package/src/auth/signup-funnel-metrics.ts +164 -0
  219. package/src/browser-controller-authority.ts +1 -186
  220. package/src/browser-ephemeral.ts +39 -0
  221. package/src/browser-state-authority.ts +1 -236
  222. package/src/claude-subscription-account-conflicts.ts +12 -0
  223. package/src/claude-subscription-account-usage.ts +48 -0
  224. package/src/claude-subscription-oauth.ts +362 -0
  225. package/src/claude-subscription-profile.ts +73 -0
  226. package/src/claude-subscription-usage.ts +97 -0
  227. package/src/claude-workspace-connection.ts +82 -0
  228. package/src/codemode.ts +194 -30
  229. package/src/codex-realtime.ts +8 -2
  230. package/src/company-brain-okf.ts +340 -0
  231. package/src/connected-machine-computer-access.ts +33 -0
  232. package/src/connection-ownership.ts +199 -0
  233. package/src/controller-data-plane.ts +81 -0
  234. package/src/editable-artifact-native-kernel.ts +26 -15
  235. package/src/editable-artifact-office-import.ts +77 -9
  236. package/src/editable-artifact-production.ts +23 -7
  237. package/src/editable-artifact-source-session.ts +123 -0
  238. package/src/editable-artifact-websocket.ts +27 -21
  239. package/src/editable-artifact-workspace-files.ts +31 -6
  240. package/src/fatal-process-boundary.ts +246 -0
  241. package/src/gateway-realtime.ts +1 -1
  242. package/src/github-access.ts +147 -5
  243. package/src/http/acting-person.ts +71 -0
  244. package/src/http/api-contract-compat.ts +49 -0
  245. package/src/http/api-error.ts +115 -1
  246. package/src/http/auth.ts +48 -0
  247. package/src/http/cors.ts +67 -0
  248. package/src/http/deprecation.ts +118 -0
  249. package/src/http/interaction-control-error.ts +164 -0
  250. package/src/http/keyed-admission.ts +29 -0
  251. package/src/http/local-browser-boundary.ts +343 -0
  252. package/src/http/metrics-listener.ts +57 -0
  253. package/src/http/path-identifier.ts +54 -0
  254. package/src/http/registered-route-label.ts +34 -0
  255. package/src/http/request-body.ts +127 -0
  256. package/src/http/request-source.ts +178 -0
  257. package/src/http/sse.ts +486 -132
  258. package/src/http/unmatched-route.ts +60 -0
  259. package/src/http/user-content.ts +158 -0
  260. package/src/http/websocket-upgrade-dispatch.ts +29 -0
  261. package/src/index.ts +157 -52
  262. package/src/integration-endpoint-test.ts +291 -0
  263. package/src/integrations/api-integrations.ts +98 -11
  264. package/src/integrations/atlassian.ts +474 -136
  265. package/src/integrations/connect-authority.ts +2 -0
  266. package/src/integrations/connect-callback-return.ts +86 -0
  267. package/src/integrations/fiken.ts +354 -64
  268. package/src/integrations/github-app-connect.ts +435 -0
  269. package/src/integrations/github-installation-proof.ts +60 -0
  270. package/src/integrations/github-lens-connect.ts +76 -0
  271. package/src/integrations/github-skill-source.ts +2 -142
  272. package/src/integrations/google-drive.ts +820 -222
  273. package/src/integrations/integration-commit-authority.ts +69 -0
  274. package/src/integrations/marketplace-plugin.ts +48 -0
  275. package/src/integrations/oauth-client.ts +1356 -459
  276. package/src/integrations/oauth-profiles.ts +537 -0
  277. package/src/integrations/oauth-return-path.ts +18 -0
  278. package/src/integrations/personal-github-repositories.ts +674 -0
  279. package/src/integrations/personal-github.ts +825 -0
  280. package/src/integrations/pr-review-provider.ts +246 -0
  281. package/src/integrations/provider-oauth.ts +250 -22
  282. package/src/integrations/slack-app-home.ts +300 -0
  283. package/src/integrations/slack-bot.ts +963 -239
  284. package/src/integrations/slack-file-upload-flow.ts +147 -0
  285. package/src/integrations/slack-install.ts +83 -0
  286. package/src/integrations/slack-interactions.ts +3170 -474
  287. package/src/integrations/slack-mrkdwn.ts +193 -0
  288. package/src/integrations/slack-routing.ts +337 -0
  289. package/src/integrations/slack-session-defaults.ts +224 -0
  290. package/src/integrations/slack-task-file-upload.ts +323 -0
  291. package/src/integrations/social-oauth.ts +213 -15
  292. package/src/interaction-agent-access.ts +48 -0
  293. package/src/interaction-frame-proxy.ts +409 -0
  294. package/src/interaction-metrics.ts +30 -0
  295. package/src/mcp/action-catalog-types.ts +9 -0
  296. package/src/mcp/action-catalog.gen.ts +6696 -0
  297. package/src/mcp/capability-account-readiness.ts +17 -0
  298. package/src/mcp/child-read-evidence.ts +19 -0
  299. package/src/mcp/command-read.ts +109 -0
  300. package/src/mcp/company-profile-agent-admin.ts +198 -0
  301. package/src/mcp/contract-input.ts +141 -0
  302. package/src/mcp/documents.ts +65 -377
  303. package/src/mcp/editable-artifacts.ts +12 -6
  304. package/src/mcp/files.ts +80 -38
  305. package/src/mcp/first-party-tool-permissions.ts +305 -0
  306. package/src/mcp/knowledge-entries.ts +322 -0
  307. package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
  308. package/src/mcp/receipts.ts +34 -0
  309. package/src/mcp/request-abort.ts +44 -0
  310. package/src/mcp/scheduled-task-input.ts +76 -0
  311. package/src/mcp/scheduled-task-view.ts +44 -3
  312. package/src/mcp/server.ts +3030 -1267
  313. package/src/mcp/session-event-view.ts +383 -0
  314. package/src/mcp/session-view.ts +84 -3
  315. package/src/mcp/session-wait.ts +665 -0
  316. package/src/mcp-oauth.ts +1123 -0
  317. package/src/model-catalog.ts +91 -333
  318. package/src/observability.ts +15 -1
  319. package/src/organization-mcp.ts +397 -0
  320. package/src/organization-recovery-notifications.ts +103 -0
  321. package/src/routes/analytics-consent.ts +123 -0
  322. package/src/routes/api-integrations.ts +364 -74
  323. package/src/routes/api-keys.ts +336 -10
  324. package/src/routes/artifact-catalog.ts +279 -0
  325. package/src/routes/automations.ts +768 -0
  326. package/src/routes/billing.ts +461 -46
  327. package/src/routes/browser-sessions.ts +839 -131
  328. package/src/routes/canonical-human-identities.ts +90 -14
  329. package/src/routes/capabilities.ts +107 -6
  330. package/src/routes/channels.ts +20 -3
  331. package/src/routes/claude-subscription-accounts.ts +334 -0
  332. package/src/routes/claude-subscription-oauth.ts +142 -0
  333. package/src/routes/client-errors.ts +362 -0
  334. package/src/routes/codex.ts +833 -116
  335. package/src/routes/company-brain.ts +322 -0
  336. package/src/routes/company-profile.ts +65 -1
  337. package/src/routes/computer-sessions.ts +557 -106
  338. package/src/routes/connect.ts +1938 -0
  339. package/src/routes/connections.ts +1040 -349
  340. package/src/routes/documents.ts +488 -287
  341. package/src/routes/editable-artifacts.ts +79 -9
  342. package/src/routes/enrollments.ts +135 -39
  343. package/src/routes/environments.ts +154 -58
  344. package/src/routes/external-identity-links.ts +201 -0
  345. package/src/routes/feedback.ts +113 -0
  346. package/src/routes/files.ts +337 -39
  347. package/src/routes/github-browser-pages.ts +162 -0
  348. package/src/routes/github.ts +446 -144
  349. package/src/routes/insights-response-cache.ts +83 -0
  350. package/src/routes/insights-usage.ts +345 -0
  351. package/src/routes/insights.ts +153 -19
  352. package/src/routes/install.ts +133 -38
  353. package/src/routes/integration-facets.ts +107 -33
  354. package/src/routes/interaction-resources.ts +30 -3
  355. package/src/routes/knowledge.ts +488 -0
  356. package/src/routes/machines.ts +313 -15
  357. package/src/routes/managed-auth-session-sets.ts +1218 -0
  358. package/src/routes/managed-onboarding.ts +337 -0
  359. package/src/routes/managed-sign-in-methods.ts +433 -0
  360. package/src/routes/memory-slack-publications.ts +3 -2
  361. package/src/routes/model-connection-access.ts +208 -0
  362. package/src/routes/organization-integration-policy.ts +116 -0
  363. package/src/routes/organization-integrations.ts +326 -0
  364. package/src/routes/organization-mcp-connections.ts +178 -0
  365. package/src/routes/organization-memberships.ts +1019 -27
  366. package/src/routes/organization-model-providers.ts +293 -0
  367. package/src/routes/organization-recovery.ts +325 -0
  368. package/src/routes/organization-service-accounts.ts +128 -0
  369. package/src/routes/organization-sessions.ts +350 -0
  370. package/src/routes/personal-github-git-broker.ts +785 -0
  371. package/src/routes/personal-github.ts +370 -0
  372. package/src/routes/plugins.ts +251 -22
  373. package/src/routes/pr-review-github.ts +881 -0
  374. package/src/routes/pr-review.ts +622 -0
  375. package/src/routes/preference-registry.ts +12 -93
  376. package/src/routes/rigs.ts +167 -50
  377. package/src/routes/scheduled-tasks.ts +346 -41
  378. package/src/routes/session-artifact-associations.ts +87 -0
  379. package/src/routes/session-history-imports.ts +280 -0
  380. package/src/routes/sessions.ts +2306 -264
  381. package/src/routes/skill-content.ts +297 -0
  382. package/src/routes/skill-install-authority.ts +64 -0
  383. package/src/routes/skills.ts +62 -9
  384. package/src/routes/social.ts +46 -18
  385. package/src/routes/subscription-account-pools.ts +436 -0
  386. package/src/routes/subscription-pool-access.ts +126 -0
  387. package/src/routes/supergrok.ts +219 -303
  388. package/src/routes/transcription-recordings.ts +38 -3
  389. package/src/routes/transcriptions.ts +4 -0
  390. package/src/routes/usage-allowances.ts +378 -0
  391. package/src/routes/user-resource-authorities.ts +138 -0
  392. package/src/routes/video-generation.ts +14 -10
  393. package/src/routes/workspace-artifacts.ts +275 -69
  394. package/src/routes/workspace-capture.ts +21 -0
  395. package/src/routes/workspace-integrations.ts +576 -0
  396. package/src/routes/workspace-learning.ts +159 -0
  397. package/src/routes/workspace-model-providers.ts +148 -0
  398. package/src/routes/workspace-state.ts +38 -25
  399. package/src/routes/workspaces.ts +1004 -127
  400. package/src/sandbox/auth-callout.ts +96 -19
  401. package/src/sandbox/channel-a.ts +323 -24
  402. package/src/sandbox/connection-authority.ts +3 -0
  403. package/src/sandbox/enrollment.ts +81 -6
  404. package/src/sandbox/machines.ts +218 -76
  405. package/src/sandbox/metrics-ingestion.ts +411 -62
  406. package/src/sandbox/rematerialize.ts +121 -19
  407. package/src/sandbox/session-attach-variable-sets.ts +131 -0
  408. package/src/sandbox/viewer.ts +280 -25
  409. package/src/sandbox-file-artifacts.ts +413 -0
  410. package/src/scheduled-task-deletion.ts +138 -0
  411. package/src/session-create-observability.ts +46 -0
  412. package/src/session-realtime-context.ts +6 -3
  413. package/src/site-session-origin.ts +48 -0
  414. package/src/site-uploads.ts +224 -0
  415. package/src/slack-reaction-files.ts +16 -5
  416. package/src/temporal-schedule-cleanup.ts +13 -0
  417. package/src/temporal-schedule-sync.ts +74 -0
  418. package/src/transcription/providers/codex-subscription.ts +2 -0
  419. package/src/transcription/providers/openai.ts +1 -0
  420. package/src/transcription/providers/xai-subscription.ts +49 -3
  421. package/src/transcription/segmenter.ts +8 -2
  422. package/src/transcription/service.ts +43 -6
  423. package/src/work-discovery-observability.ts +121 -0
  424. package/src/workspace-artifact-content.ts +144 -0
  425. package/src/workspace-artifact-provenance.ts +104 -0
  426. package/src/workspace-delete-observability.ts +75 -0
  427. package/src/workspace-deletion.ts +73 -0
  428. package/src/workspace-state-projection.ts +18 -148
  429. package/src/workspace-tool-gateway-observability.ts +100 -0
  430. package/src/workspace-tool-gateway.ts +870 -0
  431. package/src/workspace-webhook-dispatch.ts +190 -0
  432. package/src/xai-subscription-auth.ts +9 -2
  433. package/dist/chunk-2PQMSRCB.js +0 -49759
  434. package/dist/chunk-2PQMSRCB.js.map +0 -1
  435. package/dist/routes/packs.d.ts +0 -3
  436. package/src/routes/packs.ts +0 -729
@@ -0,0 +1,1123 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import {
3
+ MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS,
4
+ MCP_OAUTH_AUTHORIZATION_CODE_TTL_SECONDS,
5
+ MCP_OAUTH_CONSENT_TTL_SECONDS,
6
+ MCP_OAUTH_REFRESH_TOKEN_TTL_SECONDS,
7
+ MCP_OAUTH_SCOPE,
8
+ McpOAuthAuthorizationServerMetadata,
9
+ McpOAuthClientRegistrationRequest,
10
+ McpOAuthClientRegistrationResponse,
11
+ McpOAuthProtectedResourceMetadata,
12
+ McpOAuthTokenResponse,
13
+ OrganizationAccessPolicy,
14
+ organizationAccessPresetPermissions,
15
+ type AccessContext,
16
+ type AccessGrant,
17
+ type ToolGatewayCatalog,
18
+ type ToolGatewayIdentity,
19
+ } from "@opengeni/contracts";
20
+ import {
21
+ consumeMcpOAuthAuthorizationRequest,
22
+ createMcpOAuthAuthorizationRequest,
23
+ deleteMcpOAuthAuthorizationRequest,
24
+ exchangeMcpOAuthAuthorizationCode,
25
+ getManagedAccount,
26
+ getMcpOAuthAuthorizationRequest,
27
+ getMcpOAuthClient,
28
+ getWorkspace,
29
+ listSharedWorkspacesForAccount,
30
+ listWorkspacesForSubject,
31
+ McpOAuthClientRegistrationRateLimitError,
32
+ rebindMcpOAuthAuthorizationRequest,
33
+ registerMcpOAuthClient,
34
+ resolveLiveMcpOAuthGrant,
35
+ resolveMcpOAuthAccessToken,
36
+ rotateMcpOAuthRefreshToken,
37
+ setMcpOAuthRequestOrganizationAccess,
38
+ type McpOAuthAccess,
39
+ } from "@opengeni/db";
40
+ import {
41
+ hasPermission,
42
+ requireAccessContext,
43
+ requireAccessGrantAuthorization,
44
+ type ApiRouteDeps,
45
+ } from "@opengeni/core";
46
+ import { Hono, type Context } from "hono";
47
+ import { HTTPException } from "hono/http-exception";
48
+ import { z } from "zod";
49
+ import { trustedRequestSourceRateLimitKey, type RequestSourceTrust } from "./http/request-source";
50
+ import { requireNotAgent } from "./http/acting-person";
51
+ import { requireSameOriginBrowserMutation } from "./routes/codex";
52
+ import {
53
+ prepareWorkspaceToolGateway,
54
+ requireWorkspaceToolGatewayAuthorization,
55
+ } from "./workspace-tool-gateway";
56
+
57
+ const CLIENT_PREFIX = "ogmcp_client_";
58
+ const REQUEST_PREFIX = "ogmcp_req_";
59
+ const CODE_PREFIX = "ogmcp_code_";
60
+ const ACCESS_PREFIX = "ogmcp_at_";
61
+ const REFRESH_PREFIX = "ogmcp_rt_";
62
+ const PKCE_VERIFIER = /^[A-Za-z0-9._~-]{43,128}$/u;
63
+ const PKCE_CHALLENGE = /^[A-Za-z0-9_-]{43}$/u;
64
+ const WORKSPACE_MCP_PATH = /^\/v1\/workspaces\/([0-9a-f-]{36})\/mcp(?:\/(docs|files))?$/u;
65
+
66
+ /** One organization MCP server for everyone; the sign-in chooses the organization. */
67
+ export const ORGANIZATION_MCP_PATH = "/v1/mcp";
68
+
69
+ export type McpOAuthResource = {
70
+ resource: string;
71
+ /** Null for the organization server. */
72
+ workspaceId: string | null;
73
+ kind: "all" | "docs" | "files" | "organization";
74
+ };
75
+
76
+ export type McpOAuthRouteAccess = {
77
+ grant: AccessGrant;
78
+ allowedToolIdentities: ToolGatewayIdentity[];
79
+ };
80
+
81
+ export function registerMcpOAuthRoutes(app: Hono, deps: ApiRouteDeps): void {
82
+ app.get("/.well-known/oauth-authorization-server", (c) => {
83
+ requireMcpOAuthEnabled(deps);
84
+ const issuer = mcpOAuthIssuer(deps);
85
+ c.header("cache-control", "public, max-age=300");
86
+ return c.json(
87
+ McpOAuthAuthorizationServerMetadata.parse({
88
+ issuer,
89
+ authorization_endpoint: `${issuer}/oauth/authorize`,
90
+ token_endpoint: `${issuer}/oauth/token`,
91
+ registration_endpoint: `${issuer}/oauth/register`,
92
+ response_types_supported: ["code"],
93
+ grant_types_supported: ["authorization_code", "refresh_token"],
94
+ code_challenge_methods_supported: ["S256"],
95
+ token_endpoint_auth_methods_supported: ["none"],
96
+ scopes_supported: [MCP_OAUTH_SCOPE],
97
+ authorization_response_iss_parameter_supported: true,
98
+ }),
99
+ );
100
+ });
101
+
102
+ app.get("/.well-known/oauth-protected-resource/*", (c) => {
103
+ requireMcpOAuthEnabled(deps);
104
+ const pathname = new URL(c.req.url).pathname.replace(
105
+ "/.well-known/oauth-protected-resource",
106
+ "",
107
+ );
108
+ const resource = parseMcpOAuthResource(deps, `${mcpOAuthIssuer(deps)}${pathname}`);
109
+ c.header("cache-control", "public, max-age=300");
110
+ return c.json(
111
+ McpOAuthProtectedResourceMetadata.parse({
112
+ resource: resource.resource,
113
+ authorization_servers: [mcpOAuthIssuer(deps)],
114
+ scopes_supported: [MCP_OAUTH_SCOPE],
115
+ bearer_methods_supported: ["header"],
116
+ }),
117
+ );
118
+ });
119
+
120
+ app.post("/oauth/register", async (c) => {
121
+ requireMcpOAuthEnabled(deps);
122
+ const parsed = McpOAuthClientRegistrationRequest.safeParse(
123
+ await c.req.json().catch(() => null),
124
+ );
125
+ if (!parsed.success) return oauthError(c, "invalid_client_metadata", 400);
126
+ let redirectUris: string[];
127
+ try {
128
+ redirectUris = [...new Set(parsed.data.redirect_uris.map(validateRedirectUri))];
129
+ } catch {
130
+ return oauthError(c, "invalid_redirect_uri", 400);
131
+ }
132
+ let client;
133
+ try {
134
+ client = await registerMcpOAuthClient(deps.db, {
135
+ clientId: opaque(CLIENT_PREFIX),
136
+ redirectUris,
137
+ clientName: parsed.data.client_name ?? null,
138
+ grantTypes: parsed.data.grant_types,
139
+ responseTypes: ["code"],
140
+ registrationScopeHash: tokenHash(mcpOAuthRegistrationClientKey(c, deps.settings)),
141
+ });
142
+ } catch (error) {
143
+ if (error instanceof McpOAuthClientRegistrationRateLimitError) {
144
+ c.header("retry-after", "600");
145
+ return oauthError(c, "temporarily_unavailable", 429);
146
+ }
147
+ throw error;
148
+ }
149
+ c.header("cache-control", "no-store");
150
+ return c.json(
151
+ McpOAuthClientRegistrationResponse.parse({
152
+ client_id: client.clientId,
153
+ client_id_issued_at: Math.floor(client.createdAt.getTime() / 1_000),
154
+ redirect_uris: client.redirectUris,
155
+ ...(client.clientName ? { client_name: client.clientName } : {}),
156
+ ...(parsed.data.application_type ? { application_type: parsed.data.application_type } : {}),
157
+ ...(parsed.data.scope ? { scope: parsed.data.scope } : {}),
158
+ token_endpoint_auth_method: "none",
159
+ grant_types: client.grantTypes,
160
+ response_types: client.responseTypes,
161
+ }),
162
+ 201,
163
+ );
164
+ });
165
+
166
+ app.get("/oauth/authorize", async (c) => {
167
+ requireMcpOAuthEnabled(deps);
168
+ const query = new URL(c.req.url).searchParams;
169
+ const client = await requireAuthorizationClient(deps, query);
170
+ const resource = requireAuthorizationResource(deps, query);
171
+ if (resource.kind === "organization") {
172
+ // The person picks the organization, what the agent can do and where on
173
+ // the web app's sign-in page, which reads and answers this request.
174
+ const web = new URL(deps.settings.webBaseUrl ?? mcpOAuthIssuer(deps));
175
+ let person: { context: AccessContext };
176
+ try {
177
+ person = await requireBrowserPerson(c, deps);
178
+ } catch (error) {
179
+ if (!(error instanceof HTTPException) || error.status !== 401) throw error;
180
+ // Not signed in here yet: sign in on the web app, which returns to
181
+ // this exact authorization request (a same-origin path only).
182
+ const url = new URL(c.req.url);
183
+ c.header("cache-control", "no-store");
184
+ return c.redirect(
185
+ `${web.origin}/connect-agent?authorize=${encodeURIComponent(`${url.pathname}${url.search}`)}`,
186
+ 302,
187
+ );
188
+ }
189
+ const accountId =
190
+ person.context.defaultAccountId ?? person.context.accountGrants[0]?.accountId;
191
+ if (!accountId) {
192
+ throw new HTTPException(403, { message: "no organization is available for MCP OAuth" });
193
+ }
194
+ const requestToken = opaque(REQUEST_PREFIX);
195
+ await createMcpOAuthAuthorizationRequest(deps.db, {
196
+ requestHash: tokenHash(requestToken),
197
+ clientId: client.clientId,
198
+ accountId,
199
+ workspaceId: null,
200
+ subjectId: person.context.subjectId,
201
+ resource: resource.resource,
202
+ redirectUri: requireRedirectUri(client.redirectUris, query.get("redirect_uri")),
203
+ codeChallenge: requireCodeChallenge(query),
204
+ state: boundedState(query.get("state")),
205
+ permissions: [],
206
+ toolIdentities: [],
207
+ organizationAccess: DEFAULT_ORGANIZATION_ACCESS(),
208
+ expiresAt: expiresIn(MCP_OAUTH_CONSENT_TTL_SECONDS),
209
+ });
210
+ c.header("cache-control", "no-store");
211
+ return c.redirect(
212
+ `${web.origin}/connect-agent?request=${encodeURIComponent(requestToken)}`,
213
+ 302,
214
+ );
215
+ }
216
+ const context = await requireAccessContext(c, deps);
217
+ const workspaces = await listConsentWorkspaces(deps, context);
218
+ if (workspaces.length === 0) {
219
+ throw new HTTPException(403, { message: "no workspace is available for MCP OAuth" });
220
+ }
221
+ const selectedWorkspace =
222
+ workspaces.find((workspace) => workspace.id === resource.workspaceId) ?? workspaces[0]!;
223
+ const authorization = await requireAccessGrantAuthorization(
224
+ c,
225
+ deps,
226
+ selectedWorkspace.id,
227
+ "workspace:read",
228
+ );
229
+ const grant = requireWorkspaceToolGatewayAuthorization(authorization);
230
+ const prepared = await prepareWorkspaceToolGateway(deps, authorization);
231
+ const requestToken = opaque(REQUEST_PREFIX);
232
+ try {
233
+ await createMcpOAuthAuthorizationRequest(deps.db, {
234
+ requestHash: tokenHash(requestToken),
235
+ clientId: client.clientId,
236
+ accountId: grant.accountId,
237
+ workspaceId: grant.workspaceId,
238
+ subjectId: grant.subjectId,
239
+ resource: resource.resource,
240
+ redirectUri: requireRedirectUri(client.redirectUris, query.get("redirect_uri")),
241
+ codeChallenge: requireCodeChallenge(query),
242
+ state: boundedState(query.get("state")),
243
+ permissions: grant.permissions,
244
+ toolIdentities: mcpOAuthConsentToolIdentities(prepared.toolGatewayCatalog),
245
+ organizationAccess: null,
246
+ expiresAt: expiresIn(MCP_OAUTH_CONSENT_TTL_SECONDS),
247
+ });
248
+ } finally {
249
+ await prepared.close();
250
+ }
251
+ const accounts = await consentAccountsForWorkspaces(deps, workspaces);
252
+ c.header(
253
+ "content-security-policy",
254
+ "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'",
255
+ );
256
+ c.header("cache-control", "no-store");
257
+ return c.html(
258
+ renderMcpOAuthConsentPage({
259
+ clientName: client.clientName ?? "MCP client",
260
+ requestToken,
261
+ accounts,
262
+ workspaces: workspaces.map((workspace) => ({
263
+ id: workspace.id,
264
+ accountId: workspace.accountId,
265
+ name: workspace.name,
266
+ kind: workspace.kind,
267
+ })),
268
+ selectedWorkspaceId: grant.workspaceId,
269
+ }),
270
+ );
271
+ });
272
+
273
+ app.post("/oauth/authorize", async (c) => {
274
+ requireMcpOAuthEnabled(deps);
275
+ const form = new URLSearchParams(await c.req.text());
276
+ const requestToken = form.get("request") ?? "";
277
+ if (!requestToken.startsWith(REQUEST_PREFIX)) {
278
+ return oauthAuthorizeBrowserError(
279
+ c,
280
+ "This authorization request is missing or already used.",
281
+ );
282
+ }
283
+ const request = await getMcpOAuthAuthorizationRequest(deps.db, tokenHash(requestToken));
284
+ if (!request) {
285
+ return oauthAuthorizeBrowserError(
286
+ c,
287
+ "This authorization request expired or was already approved.",
288
+ );
289
+ }
290
+ // Organization sign-ins are answered on the Opengeni page they opened.
291
+ if (request.workspaceId === null) {
292
+ return oauthAuthorizeBrowserError(c, "Answer this sign-in on the Opengeni page it opened.");
293
+ }
294
+ const requestWorkspaceId = request.workspaceId;
295
+ if (form.get("decision") !== "approve") {
296
+ const authorization = await requireAccessGrantAuthorization(
297
+ c,
298
+ deps,
299
+ requestWorkspaceId,
300
+ "workspace:read",
301
+ );
302
+ if (requireWorkspaceToolGatewayAuthorization(authorization).subjectId !== request.subjectId) {
303
+ throw new HTTPException(403, { message: "OAuth consent authority changed" });
304
+ }
305
+ await deleteMcpOAuthAuthorizationRequest(deps.db, request.requestHash);
306
+ return completeAuthorizationRedirect(
307
+ c,
308
+ authorizationRedirect(request.redirectUri, {
309
+ error: "access_denied",
310
+ iss: mcpOAuthIssuer(deps),
311
+ ...(request.state ? { state: request.state } : {}),
312
+ }),
313
+ );
314
+ }
315
+ const selectedWorkspaceId = form.get("workspace_id") || requestWorkspaceId;
316
+ if (!isWorkspaceId(selectedWorkspaceId)) {
317
+ return oauthAuthorizeBrowserError(c, "Choose a workspace before authorizing this client.");
318
+ }
319
+ const authorization = await requireAccessGrantAuthorization(
320
+ c,
321
+ deps,
322
+ selectedWorkspaceId,
323
+ "workspace:read",
324
+ );
325
+ const grant = requireWorkspaceToolGatewayAuthorization(authorization);
326
+ if (grant.subjectId !== request.subjectId) {
327
+ throw new HTTPException(403, {
328
+ message: "OAuth consent authority changed",
329
+ });
330
+ }
331
+ if (grant.workspaceId !== request.workspaceId || grant.accountId !== request.accountId) {
332
+ const prepared = await prepareWorkspaceToolGateway(deps, authorization);
333
+ try {
334
+ const rebound = await rebindMcpOAuthAuthorizationRequest(deps.db, {
335
+ requestHash: request.requestHash,
336
+ subjectId: grant.subjectId,
337
+ accountId: grant.accountId,
338
+ workspaceId: grant.workspaceId,
339
+ permissions: grant.permissions,
340
+ toolIdentities: mcpOAuthConsentToolIdentities(prepared.toolGatewayCatalog),
341
+ });
342
+ if (!rebound) {
343
+ return oauthAuthorizeBrowserError(
344
+ c,
345
+ "This authorization request expired or was already approved.",
346
+ );
347
+ }
348
+ } finally {
349
+ await prepared.close();
350
+ }
351
+ }
352
+ const code = opaque(CODE_PREFIX);
353
+ const consumed = await consumeMcpOAuthAuthorizationRequest(deps.db, {
354
+ requestHash: request.requestHash,
355
+ subjectId: grant.subjectId,
356
+ codeHash: tokenHash(code),
357
+ codeExpiresAt: expiresIn(MCP_OAUTH_AUTHORIZATION_CODE_TTL_SECONDS),
358
+ });
359
+ if (!consumed) {
360
+ return oauthAuthorizeBrowserError(
361
+ c,
362
+ "This authorization request expired or was already approved.",
363
+ );
364
+ }
365
+ return completeAuthorizationRedirect(
366
+ c,
367
+ authorizationRedirect(consumed.redirectUri, {
368
+ code,
369
+ iss: mcpOAuthIssuer(deps),
370
+ ...(consumed.state ? { state: consumed.state } : {}),
371
+ }),
372
+ );
373
+ });
374
+
375
+ // The web sign-in page for the organization server reads the pending request…
376
+ app.get("/v1/mcp-connections/requests/:request", async (c) => {
377
+ requireMcpOAuthEnabled(deps);
378
+ const person = await requireBrowserPerson(c, deps);
379
+ const request = await requirePendingOrganizationRequest(deps, c.req.param("request"), person);
380
+ const client = await getMcpOAuthClient(deps.db, request.clientId);
381
+ const organizations = await consentOrganizations(deps, person.context);
382
+ c.header("cache-control", "no-store");
383
+ return c.json({
384
+ client: {
385
+ name: client?.clientName ?? "MCP client",
386
+ host: redirectHost(request.redirectUri),
387
+ },
388
+ person: { name: person.context.subjectLabel ?? null },
389
+ organizations,
390
+ defaultOrganizationId: organizations.some((each) => each.id === request.accountId)
391
+ ? request.accountId
392
+ : (organizations[0]?.id ?? null),
393
+ });
394
+ });
395
+
396
+ // …and answers it. Approving returns the agent's redirect with a code.
397
+ app.post("/v1/mcp-connections/requests/:request", async (c) => {
398
+ requireMcpOAuthEnabled(deps);
399
+ requireSameOriginBrowserMutation(c, deps);
400
+ const person = await requireBrowserPerson(c, deps);
401
+ const request = await requirePendingOrganizationRequest(deps, c.req.param("request"), person);
402
+ const parsed = OrganizationConsentDecision.safeParse(await c.req.json().catch(() => null));
403
+ if (!parsed.success) throw new HTTPException(400, { message: "invalid sign-in decision" });
404
+ const decision = parsed.data;
405
+ if (decision.decision === "deny") {
406
+ await deleteMcpOAuthAuthorizationRequest(deps.db, request.requestHash);
407
+ return c.json({
408
+ redirectTo: authorizationRedirect(request.redirectUri, {
409
+ error: "access_denied",
410
+ iss: mcpOAuthIssuer(deps),
411
+ ...(request.state ? { state: request.state } : {}),
412
+ }),
413
+ });
414
+ }
415
+ const organizations = await consentOrganizations(deps, person.context);
416
+ const organization = organizations.find((each) => each.id === decision.organizationId);
417
+ if (!organization) throw new HTTPException(403, { message: "organization is not available" });
418
+ const access = decision.access;
419
+ if (access.workspaceScope.kind === "selected") {
420
+ const reachable = new Set(organization.workspaces.map((workspace) => workspace.id));
421
+ if (access.workspaceScope.workspaceIds.some((id) => !reachable.has(id))) {
422
+ throw new HTTPException(403, { message: "a selected workspace is not available" });
423
+ }
424
+ }
425
+ const bound = await setMcpOAuthRequestOrganizationAccess(deps.db, {
426
+ requestHash: request.requestHash,
427
+ subjectId: person.context.subjectId,
428
+ accountId: organization.id,
429
+ organizationAccess: access,
430
+ });
431
+ const code = opaque(CODE_PREFIX);
432
+ const consumed = bound
433
+ ? await consumeMcpOAuthAuthorizationRequest(deps.db, {
434
+ requestHash: request.requestHash,
435
+ subjectId: person.context.subjectId,
436
+ codeHash: tokenHash(code),
437
+ codeExpiresAt: expiresIn(MCP_OAUTH_AUTHORIZATION_CODE_TTL_SECONDS),
438
+ })
439
+ : null;
440
+ if (!consumed) {
441
+ throw new HTTPException(410, { message: "This sign-in expired or was already answered." });
442
+ }
443
+ return c.json({
444
+ redirectTo: authorizationRedirect(consumed.redirectUri, {
445
+ code,
446
+ iss: mcpOAuthIssuer(deps),
447
+ ...(consumed.state ? { state: consumed.state } : {}),
448
+ }),
449
+ });
450
+ });
451
+
452
+ app.post("/oauth/token", async (c) => {
453
+ requireMcpOAuthEnabled(deps);
454
+ const form = new URLSearchParams(await c.req.text());
455
+ const grantType = form.get("grant_type");
456
+ const clientId = form.get("client_id") ?? "";
457
+ const resource = form.get("resource");
458
+ const client = await getMcpOAuthClient(deps.db, clientId);
459
+ if (!client) return oauthError(c, "invalid_client", 401);
460
+ if (grantType !== "authorization_code" && grantType !== "refresh_token") {
461
+ return oauthError(c, "unsupported_grant_type", 400);
462
+ }
463
+ if (!client.grantTypes.includes(grantType)) {
464
+ return oauthError(c, "unauthorized_client", 400);
465
+ }
466
+ const requestedScope = form.get("scope");
467
+ if (requestedScope !== null && requestedScope !== MCP_OAUTH_SCOPE) {
468
+ return oauthError(c, "invalid_scope", 400);
469
+ }
470
+ if (!resource) return oauthError(c, "invalid_target", 400);
471
+ let parsedResource: McpOAuthResource;
472
+ try {
473
+ parsedResource = parseMcpOAuthResource(deps, resource);
474
+ } catch {
475
+ return oauthError(c, "invalid_target", 400);
476
+ }
477
+ const accessToken = opaque(ACCESS_PREFIX);
478
+ const refreshToken = client.grantTypes.includes("refresh_token")
479
+ ? opaque(REFRESH_PREFIX)
480
+ : null;
481
+ const tokenInput = {
482
+ clientId,
483
+ resource: parsedResource.resource,
484
+ accessTokenHash: tokenHash(accessToken),
485
+ accessExpiresAt: expiresIn(MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS),
486
+ refreshExpiresAt: expiresIn(MCP_OAUTH_REFRESH_TOKEN_TTL_SECONDS),
487
+ };
488
+ const access =
489
+ grantType === "authorization_code"
490
+ ? await exchangeAuthorizationCode(deps, client.redirectUris, form, {
491
+ ...tokenInput,
492
+ refreshTokenHash: refreshToken ? tokenHash(refreshToken) : null,
493
+ })
494
+ : refreshToken
495
+ ? await rotateRefreshToken(deps, form, {
496
+ ...tokenInput,
497
+ nextRefreshTokenHash: tokenHash(refreshToken),
498
+ })
499
+ : null;
500
+ if (!access) return oauthError(c, "invalid_grant", 400);
501
+ c.header("cache-control", "no-store");
502
+ c.header("pragma", "no-cache");
503
+ return c.json(
504
+ McpOAuthTokenResponse.parse({
505
+ access_token: accessToken,
506
+ token_type: "Bearer",
507
+ expires_in: MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS,
508
+ ...(refreshToken ? { refresh_token: refreshToken } : {}),
509
+ scope: MCP_OAUTH_SCOPE,
510
+ }),
511
+ );
512
+ });
513
+ }
514
+
515
+ export async function resolveMcpOAuthRouteAccess(
516
+ deps: ApiRouteDeps,
517
+ request: Request,
518
+ workspaceId: string,
519
+ ): Promise<McpOAuthRouteAccess | null> {
520
+ const token = mcpOAuthBearerToken(request);
521
+ if (!token) return null;
522
+ requireMcpOAuthEnabled(deps);
523
+ const access = await resolveMcpOAuthAccessToken(deps.db, tokenHash(token));
524
+ const requestResource = parseMcpOAuthResource(
525
+ deps,
526
+ `${mcpOAuthIssuer(deps)}${new URL(request.url).pathname}`,
527
+ );
528
+ // The client URL is the resource indicator. Consent may bind a different
529
+ // workspace than the path UUID; the token grant is the authorized workspace.
530
+ if (
531
+ !access ||
532
+ requestResource.workspaceId !== workspaceId ||
533
+ access.resource !== requestResource.resource
534
+ ) {
535
+ throw new HTTPException(401, { message: "invalid MCP OAuth access token" });
536
+ }
537
+ const grant = await resolveLiveMcpOAuthGrant(deps.db, access);
538
+ if (!grant)
539
+ throw new HTTPException(401, {
540
+ message: "MCP OAuth authority is no longer active",
541
+ });
542
+ const allowedToolIdentities = access.toolIdentities.filter((identity) =>
543
+ requestResource.kind === "all" ? true : identity.serverId === requestResource.kind,
544
+ );
545
+ return { grant, allowedToolIdentities };
546
+ }
547
+
548
+ export function mcpOAuthBearerToken(request: Request): string | null {
549
+ const authorization = request.headers.get("authorization");
550
+ const token = authorization?.match(/^Bearer +(\S+)$/iu)?.[1];
551
+ if (!token) return null;
552
+ return token.startsWith(ACCESS_PREFIX) ? token : null;
553
+ }
554
+
555
+ export function isMcpOAuthResourcePath(pathname: string): boolean {
556
+ return WORKSPACE_MCP_PATH.test(pathname) || pathname === ORGANIZATION_MCP_PATH;
557
+ }
558
+
559
+ export function mcpOAuthConsentToolIdentities(catalog: ToolGatewayCatalog): ToolGatewayIdentity[] {
560
+ return catalog.entries.map((entry) => entry.identity);
561
+ }
562
+
563
+ export function isMcpOAuthPublicProtocolPath(pathname: string): boolean {
564
+ return (
565
+ pathname === "/.well-known/oauth-authorization-server" ||
566
+ pathname.startsWith("/.well-known/oauth-protected-resource/") ||
567
+ pathname === "/oauth/register" ||
568
+ pathname === "/oauth/authorize" ||
569
+ pathname === "/oauth/token"
570
+ );
571
+ }
572
+
573
+ export function mcpOAuthAuthenticateHeader(deps: ApiRouteDeps, pathname: string): string {
574
+ return `Bearer resource_metadata="${mcpOAuthIssuer(deps)}/.well-known/oauth-protected-resource${pathname}", scope="${MCP_OAUTH_SCOPE}"`;
575
+ }
576
+
577
+ function requireMcpOAuthEnabled(deps: ApiRouteDeps): void {
578
+ if (!deps.settings.mcpOauthEnabled) throw new HTTPException(404, { message: "not found" });
579
+ }
580
+
581
+ function mcpOAuthIssuer(deps: ApiRouteDeps): string {
582
+ if (!deps.settings.publicBaseUrl) throw new Error("MCP OAuth public base URL is not configured");
583
+ return new URL(deps.settings.publicBaseUrl).origin;
584
+ }
585
+
586
+ function parseMcpOAuthResource(deps: ApiRouteDeps, value: string): McpOAuthResource {
587
+ let url: URL;
588
+ try {
589
+ url = new URL(value);
590
+ } catch {
591
+ throw new HTTPException(400, { message: "invalid OAuth resource" });
592
+ }
593
+ const issuer = mcpOAuthIssuer(deps);
594
+ if (
595
+ url.pathname === ORGANIZATION_MCP_PATH &&
596
+ url.origin === issuer &&
597
+ !url.username &&
598
+ !url.password &&
599
+ !url.search &&
600
+ !url.hash
601
+ ) {
602
+ return {
603
+ resource: `${issuer}${ORGANIZATION_MCP_PATH}`,
604
+ workspaceId: null,
605
+ kind: "organization",
606
+ };
607
+ }
608
+ const match = url.pathname.match(WORKSPACE_MCP_PATH);
609
+ if (url.origin !== issuer || url.username || url.password || url.search || url.hash || !match) {
610
+ throw new HTTPException(400, { message: "invalid OAuth resource" });
611
+ }
612
+ return {
613
+ resource: `${issuer}${url.pathname}`,
614
+ workspaceId: match[1]!,
615
+ kind: match[2] === "docs" ? "docs" : match[2] === "files" ? "files" : "all",
616
+ };
617
+ }
618
+
619
+ async function requireAuthorizationClient(deps: ApiRouteDeps, query: URLSearchParams) {
620
+ if (query.get("response_type") !== "code" || query.get("scope") !== MCP_OAUTH_SCOPE) {
621
+ throw new HTTPException(400, {
622
+ message: "unsupported OAuth authorization request",
623
+ });
624
+ }
625
+ const clientId = query.get("client_id") ?? "";
626
+ const client = await getMcpOAuthClient(deps.db, clientId);
627
+ if (!client || !client.grantTypes.includes("authorization_code")) {
628
+ throw new HTTPException(400, { message: "invalid OAuth client" });
629
+ }
630
+ requireRedirectUri(client.redirectUris, query.get("redirect_uri"));
631
+ requireCodeChallenge(query);
632
+ return client;
633
+ }
634
+
635
+ function requireAuthorizationResource(
636
+ deps: ApiRouteDeps,
637
+ query: URLSearchParams,
638
+ ): McpOAuthResource {
639
+ const resources = query.getAll("resource");
640
+ if (resources.length !== 1)
641
+ throw new HTTPException(400, { message: "one OAuth resource is required" });
642
+ return parseMcpOAuthResource(deps, resources[0]!);
643
+ }
644
+
645
+ function requireRedirectUri(registered: string[], value: string | null): string {
646
+ if (!value || !registered.includes(value)) {
647
+ throw new HTTPException(400, {
648
+ message: "OAuth redirect_uri is not registered",
649
+ });
650
+ }
651
+ return value;
652
+ }
653
+
654
+ function requireCodeChallenge(query: URLSearchParams): string {
655
+ const challenge = query.get("code_challenge") ?? "";
656
+ if (query.get("code_challenge_method") !== "S256" || !PKCE_CHALLENGE.test(challenge)) {
657
+ throw new HTTPException(400, { message: "PKCE S256 is required" });
658
+ }
659
+ return challenge;
660
+ }
661
+
662
+ async function exchangeAuthorizationCode(
663
+ deps: ApiRouteDeps,
664
+ redirectUris: string[],
665
+ form: URLSearchParams,
666
+ input: {
667
+ clientId: string;
668
+ resource: string;
669
+ accessTokenHash: string;
670
+ refreshTokenHash: string | null;
671
+ accessExpiresAt: Date;
672
+ refreshExpiresAt: Date;
673
+ },
674
+ ) {
675
+ const code = form.get("code") ?? "";
676
+ const verifier = form.get("code_verifier") ?? "";
677
+ if (!code.startsWith(CODE_PREFIX) || !PKCE_VERIFIER.test(verifier)) return null;
678
+ const redirectUri = form.get("redirect_uri");
679
+ if (!redirectUri || !redirectUris.includes(redirectUri)) return null;
680
+ for (const candidate of mcpOAuthRedirectUriCandidates(redirectUris, redirectUri)) {
681
+ const access = await exchangeMcpOAuthAuthorizationCode(deps.db, {
682
+ codeHash: tokenHash(code),
683
+ clientId: input.clientId,
684
+ redirectUri: candidate,
685
+ resource: input.resource,
686
+ codeChallenge: pkceChallenge(verifier),
687
+ accessTokenHash: input.accessTokenHash,
688
+ refreshTokenHash: input.refreshTokenHash,
689
+ accessExpiresAt: input.accessExpiresAt,
690
+ refreshExpiresAt: input.refreshExpiresAt,
691
+ });
692
+ if (access) return access;
693
+ }
694
+ return null;
695
+ }
696
+
697
+ export function mcpOAuthRedirectUriCandidates(registered: string[], requested: string): string[] {
698
+ if (!registered.includes(requested)) return [];
699
+ return [requested, ...registered.filter((uri) => uri !== requested)];
700
+ }
701
+
702
+ async function rotateRefreshToken(
703
+ deps: ApiRouteDeps,
704
+ form: URLSearchParams,
705
+ input: {
706
+ clientId: string;
707
+ resource: string;
708
+ accessTokenHash: string;
709
+ nextRefreshTokenHash: string;
710
+ accessExpiresAt: Date;
711
+ refreshExpiresAt: Date;
712
+ },
713
+ ) {
714
+ const refreshToken = form.get("refresh_token") ?? "";
715
+ if (!refreshToken.startsWith(REFRESH_PREFIX)) return null;
716
+ return await rotateMcpOAuthRefreshToken(deps.db, {
717
+ refreshTokenHash: tokenHash(refreshToken),
718
+ ...input,
719
+ });
720
+ }
721
+
722
+ const BLOCKED_REDIRECT_PROTOCOLS = new Set([
723
+ "javascript:",
724
+ "data:",
725
+ "file:",
726
+ "about:",
727
+ "blob:",
728
+ "vbscript:",
729
+ ]);
730
+
731
+ function isNativeAppRedirect(url: URL): boolean {
732
+ return (
733
+ /^[a-z][a-z0-9+.-]*:$/u.test(url.protocol) &&
734
+ !BLOCKED_REDIRECT_PROTOCOLS.has(url.protocol) &&
735
+ url.protocol !== "http:" &&
736
+ url.protocol !== "https:" &&
737
+ Boolean(url.hostname) &&
738
+ url.pathname.startsWith("/") &&
739
+ url.pathname !== "/"
740
+ );
741
+ }
742
+
743
+ function validateRedirectUri(value: string): string {
744
+ const url = new URL(value);
745
+ const loopback =
746
+ url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
747
+ if (
748
+ url.username ||
749
+ url.password ||
750
+ url.hash ||
751
+ (url.protocol !== "https:" && !loopback && !isNativeAppRedirect(url))
752
+ ) {
753
+ throw new HTTPException(400, {
754
+ message: "invalid public OAuth redirect URI",
755
+ });
756
+ }
757
+ return url.toString();
758
+ }
759
+
760
+ function boundedState(value: string | null): string | null {
761
+ if (value === null) return null;
762
+ if (!value || new TextEncoder().encode(value).byteLength > 1_024) {
763
+ throw new HTTPException(400, { message: "invalid OAuth state" });
764
+ }
765
+ return value;
766
+ }
767
+
768
+ function pkceChallenge(verifier: string): string {
769
+ return createHash("sha256").update(verifier, "ascii").digest("base64url");
770
+ }
771
+
772
+ function opaque(prefix: string): string {
773
+ return `${prefix}${randomBytes(32).toString("base64url")}`;
774
+ }
775
+
776
+ function tokenHash(value: string): string {
777
+ return createHash("sha256").update(value, "utf8").digest("hex");
778
+ }
779
+
780
+ function mcpOAuthRegistrationClientKey(c: Context, trust: RequestSourceTrust): string {
781
+ return `mcp-oauth-registration:${trustedRequestSourceRateLimitKey(c, trust)}`;
782
+ }
783
+
784
+ function expiresIn(seconds: number): Date {
785
+ return new Date(Date.now() + seconds * 1_000);
786
+ }
787
+
788
+ function authorizationRedirect(redirectUri: string, params: Record<string, string>): string {
789
+ const url = new URL(redirectUri);
790
+ for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
791
+ return url.toString();
792
+ }
793
+
794
+ export function completeAuthorizationRedirect(c: Context, redirectTo: string) {
795
+ // Consent posts under CSP form-action 'self'. A 302 to the client's
796
+ // registered redirect_uri (loopback HTTP or a custom scheme) is blocked,
797
+ // so the waiter never receives the code. Return 200 HTML that navigates
798
+ // to the exact requested URI from script and Refresh.
799
+ c.header("cache-control", "no-store");
800
+ c.header(
801
+ "content-security-policy",
802
+ "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; frame-ancestors 'none'",
803
+ );
804
+ c.header("refresh", `0;url=${redirectTo}`);
805
+ return c.html(renderMcpOAuthContinuePage(redirectTo));
806
+ }
807
+
808
+ function oauthAuthorizeBrowserError(c: Context, message: string) {
809
+ c.header("cache-control", "no-store");
810
+ c.header("pragma", "no-cache");
811
+ c.header(
812
+ "content-security-policy",
813
+ "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'",
814
+ );
815
+ return c.html(renderMcpOAuthExpiredPage(message), 400);
816
+ }
817
+
818
+ export type McpOAuthConsentAccount = {
819
+ id: string;
820
+ name: string;
821
+ };
822
+
823
+ export type McpOAuthConsentWorkspace = {
824
+ id: string;
825
+ accountId: string;
826
+ name: string;
827
+ kind: "personal" | "shared";
828
+ };
829
+
830
+ export function renderMcpOAuthConsentPage(input: {
831
+ clientName: string;
832
+ requestToken: string;
833
+ accounts: McpOAuthConsentAccount[];
834
+ workspaces: McpOAuthConsentWorkspace[];
835
+ selectedWorkspaceId: string;
836
+ }): string {
837
+ const selectedWorkspace =
838
+ input.workspaces.find((workspace) => workspace.id === input.selectedWorkspaceId) ??
839
+ input.workspaces[0];
840
+ if (!selectedWorkspace) {
841
+ throw new Error("MCP OAuth consent requires at least one workspace");
842
+ }
843
+ const selectedAccountId = selectedWorkspace.accountId;
844
+ const accountOptions = input.accounts
845
+ .map(
846
+ (account) =>
847
+ `<option value="${escapeHtml(account.id)}"${account.id === selectedAccountId ? " selected" : ""}>${escapeHtml(account.name)}</option>`,
848
+ )
849
+ .join("");
850
+ const workspaceOptions = input.workspaces
851
+ .filter((workspace) => workspace.accountId === selectedAccountId)
852
+ .map(
853
+ (workspace) =>
854
+ `<option value="${escapeHtml(workspace.id)}"${workspace.id === selectedWorkspace.id ? " selected" : ""}>${escapeHtml(consentWorkspaceLabel(workspace))}</option>`,
855
+ )
856
+ .join("");
857
+ const workspacePayload = input.workspaces.map((workspace) => ({
858
+ id: workspace.id,
859
+ accountId: workspace.accountId,
860
+ label: consentWorkspaceLabel(workspace),
861
+ }));
862
+ const body = `<p class="lede"><strong>${escapeHtml(input.clientName)}</strong> wants MCP access to the organization and workspace you choose.</p>
863
+ <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})">
864
+ <input type="hidden" name="request" value="${escapeHtml(input.requestToken)}">
865
+ <label class="field"><span>Organization</span><select id="organization" name="organization" autocomplete="off">${accountOptions}</select></label>
866
+ <label class="field"><span>Workspace</span><select id="workspace_id" name="workspace_id" autocomplete="off">${workspaceOptions}</select></label>
867
+ <p class="note">The grant is limited to tools available in the workspace you authorize. Opengeni rechecks live authority on every request.</p>
868
+ <div class="actions"><button class="approve" name="decision" value="approve">Authorize</button><button class="deny" name="decision" value="deny">Deny</button></div>
869
+ </form>
870
+ <script type="application/json" id="mcp-oauth-workspaces">${jsonForScript(workspacePayload)}</script>
871
+ <script>
872
+ (function () {
873
+ var data = JSON.parse(document.getElementById("mcp-oauth-workspaces").textContent);
874
+ var account = document.getElementById("organization");
875
+ var workspace = document.getElementById("workspace_id");
876
+ function sync() {
877
+ var selected = workspace.value;
878
+ var items = data.filter(function (item) { return item.accountId === account.value; });
879
+ workspace.replaceChildren();
880
+ items.forEach(function (item) {
881
+ var option = document.createElement("option");
882
+ option.value = item.id;
883
+ option.textContent = item.label;
884
+ workspace.appendChild(option);
885
+ });
886
+ if (items.some(function (item) { return item.id === selected; })) workspace.value = selected;
887
+ }
888
+ account.addEventListener("change", sync);
889
+ })();
890
+ </script>`;
891
+ return oauthDocument({
892
+ title: "Authorize MCP access",
893
+ heading: "Authorize this client",
894
+ body,
895
+ });
896
+ }
897
+
898
+ export function renderMcpOAuthContinuePage(redirectTo: string): string {
899
+ return oauthDocument({
900
+ title: "Returning to the app",
901
+ heading: "Returning to the app",
902
+ head: `<meta http-equiv="refresh" content="0;url=${escapeHtml(redirectTo)}">`,
903
+ 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>`,
904
+ });
905
+ }
906
+
907
+ export function renderMcpOAuthExpiredPage(message: string): string {
908
+ return oauthDocument({
909
+ title: "Authorization expired",
910
+ heading: "Authorization expired",
911
+ body: `<p class="lede">${escapeHtml(message)}</p><p class="note">Close this window and start authorization again from the client.</p>`,
912
+ });
913
+ }
914
+
915
+ async function listConsentWorkspaces(deps: ApiRouteDeps, context: AccessContext) {
916
+ const readableWorkspaceIds = [
917
+ ...new Set(
918
+ context.workspaceGrants
919
+ .filter((grant) => hasPermission(grant.permissions, "workspace:read"))
920
+ .map((grant) => grant.workspaceId),
921
+ ),
922
+ ];
923
+ const readableAccountIds = context.accountGrants
924
+ .filter((grant) => hasPermission(grant.permissions, "workspace:read"))
925
+ .map((grant) => grant.accountId);
926
+ const [fromGrants, memberships, shared] = await Promise.all([
927
+ Promise.all(readableWorkspaceIds.map((workspaceId) => getWorkspace(deps.db, workspaceId))),
928
+ listWorkspacesForSubject(deps.db, context.subjectId),
929
+ Promise.all(
930
+ readableAccountIds.map((accountId) => listSharedWorkspacesForAccount(deps.db, accountId)),
931
+ ),
932
+ ]);
933
+ const byId = new Map<string, NonNullable<(typeof fromGrants)[number]>>();
934
+ for (const workspace of [...fromGrants, ...memberships, ...shared.flat()]) {
935
+ if (workspace) byId.set(workspace.id, workspace);
936
+ }
937
+ return [...byId.values()].sort((left, right) => left.name.localeCompare(right.name));
938
+ }
939
+
940
+ async function consentAccountsForWorkspaces(
941
+ deps: ApiRouteDeps,
942
+ workspaces: Array<{ accountId: string }>,
943
+ ): Promise<McpOAuthConsentAccount[]> {
944
+ const accountIds = [...new Set(workspaces.map((workspace) => workspace.accountId))];
945
+ return (
946
+ await Promise.all(
947
+ accountIds.map(async (accountId) => {
948
+ const account = await getManagedAccount(deps.db, accountId);
949
+ return { id: accountId, name: account?.name.trim() || "Organization" };
950
+ }),
951
+ )
952
+ ).sort((left, right) => left.name.localeCompare(right.name));
953
+ }
954
+
955
+ function consentWorkspaceLabel(workspace: McpOAuthConsentWorkspace): string {
956
+ const name = workspace.name.trim() || "Workspace";
957
+ return workspace.kind === "personal" ? `${name} (Personal)` : name;
958
+ }
959
+
960
+ function isWorkspaceId(value: string): boolean {
961
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(value);
962
+ }
963
+
964
+ function oauthDocument(input: {
965
+ title: string;
966
+ heading: string;
967
+ body: string;
968
+ head?: string;
969
+ }): string {
970
+ 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>`;
971
+ }
972
+
973
+ const OAUTH_PAGE_CSS =
974
+ '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}}';
975
+
976
+ function jsonForScript(value: unknown): string {
977
+ return JSON.stringify(value).replace(/</gu, "\\u003c");
978
+ }
979
+
980
+ function escapeHtml(value: string): string {
981
+ return value.replace(
982
+ /[&<>"']/gu,
983
+ (character) =>
984
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]!,
985
+ );
986
+ }
987
+
988
+ function oauthError(c: Context, error: string, status: 400 | 401 | 429) {
989
+ c.header("cache-control", "no-store");
990
+ c.header("pragma", "no-cache");
991
+ return c.json({ error }, status);
992
+ }
993
+
994
+ /* ----------------------------------------------------------------------------
995
+ Organization server sign-in: a person picks the organization, what the agent
996
+ can do and where. The agent then acts as that person, never beyond their
997
+ live access, on every request.
998
+ -------------------------------------------------------------------------- */
999
+
1000
+ /** Read only, every workspace: the least surprising default until the person chooses. */
1001
+ function DEFAULT_ORGANIZATION_ACCESS(): OrganizationAccessPolicy {
1002
+ return {
1003
+ preset: "read_only",
1004
+ permissions: organizationAccessPresetPermissions("read_only"),
1005
+ workspaceScope: { kind: "all" },
1006
+ };
1007
+ }
1008
+
1009
+ const OrganizationConsentDecision = z.discriminatedUnion("decision", [
1010
+ z.object({ decision: z.literal("deny") }).strict(),
1011
+ z
1012
+ .object({
1013
+ decision: z.literal("approve"),
1014
+ organizationId: z.string().uuid(),
1015
+ access: OrganizationAccessPolicy,
1016
+ })
1017
+ .strict(),
1018
+ ]);
1019
+
1020
+ /** A person signed in to Opengeni in this browser: never a key, bearer or service. */
1021
+ async function requireBrowserPerson(
1022
+ c: Context,
1023
+ deps: ApiRouteDeps,
1024
+ ): Promise<{ context: AccessContext }> {
1025
+ requireNotAgent(c, "Approving a connection");
1026
+ if (
1027
+ deps.settings.productAccessMode !== "managed" ||
1028
+ !c.req.header("cookie") ||
1029
+ c.req.header("authorization")
1030
+ ) {
1031
+ throw new HTTPException(401, { message: "sign in to Opengeni in this browser first" });
1032
+ }
1033
+ const context = await requireAccessContext(c, deps);
1034
+ if (context.mode !== "managed" || !context.subjectId.startsWith("user:")) {
1035
+ throw new HTTPException(401, { message: "sign in to Opengeni in this browser first" });
1036
+ }
1037
+ return { context };
1038
+ }
1039
+
1040
+ async function requirePendingOrganizationRequest(
1041
+ deps: ApiRouteDeps,
1042
+ requestToken: string,
1043
+ person: { context: AccessContext },
1044
+ ) {
1045
+ const request = requestToken.startsWith(REQUEST_PREFIX)
1046
+ ? await getMcpOAuthAuthorizationRequest(deps.db, tokenHash(requestToken))
1047
+ : null;
1048
+ if (!request || request.workspaceId !== null) {
1049
+ throw new HTTPException(410, { message: "This sign-in expired or was already answered." });
1050
+ }
1051
+ if (request.subjectId !== person.context.subjectId) {
1052
+ throw new HTTPException(403, { message: "This sign-in belongs to someone else." });
1053
+ }
1054
+ return request;
1055
+ }
1056
+
1057
+ /** Organizations the person belongs to, with what they could hand an agent there. */
1058
+ async function consentOrganizations(deps: ApiRouteDeps, context: AccessContext) {
1059
+ const memberOf = new Set(context.accountGrants.map((grant) => grant.accountId));
1060
+ const workspaces = (await listConsentWorkspaces(deps, context)).filter((workspace) =>
1061
+ memberOf.has(workspace.accountId),
1062
+ );
1063
+ const accounts = await consentAccountsForWorkspaces(
1064
+ deps,
1065
+ [...memberOf].map((accountId) => ({ accountId })),
1066
+ );
1067
+ const canonical = organizationAccessPresetPermissions("full");
1068
+ return accounts.map((account) => {
1069
+ const accountGrant = context.accountGrants.find((grant) => grant.accountId === account.id);
1070
+ const workspaceGrants = context.workspaceGrants.filter(
1071
+ (grant) => grant.accountId === account.id,
1072
+ );
1073
+ return {
1074
+ id: account.id,
1075
+ name: account.name,
1076
+ workspaces: workspaces
1077
+ .filter((workspace) => workspace.accountId === account.id)
1078
+ .map((workspace) => ({
1079
+ id: workspace.id,
1080
+ name: workspace.name,
1081
+ personal: workspace.kind === "personal",
1082
+ })),
1083
+ grantable: canonical.filter(
1084
+ (permission) =>
1085
+ (accountGrant !== undefined && hasPermission(accountGrant.permissions, permission)) ||
1086
+ workspaceGrants.some((grant) => hasPermission(grant.permissions, permission)),
1087
+ ),
1088
+ };
1089
+ });
1090
+ }
1091
+
1092
+ function redirectHost(redirectUri: string): string | null {
1093
+ try {
1094
+ const url = new URL(redirectUri);
1095
+ return url.host || url.protocol.replace(/:$/u, "");
1096
+ } catch {
1097
+ return null;
1098
+ }
1099
+ }
1100
+
1101
+ /**
1102
+ * The organization connection behind an organization-server bearer, or null
1103
+ * when the request carries no MCP OAuth token. Invalid, revoked, expired or
1104
+ * workspace-bound tokens are refused.
1105
+ */
1106
+ export async function resolveOrganizationMcpOAuthAccess(
1107
+ deps: ApiRouteDeps,
1108
+ request: Request,
1109
+ ): Promise<(McpOAuthAccess & { organizationAccess: OrganizationAccessPolicy }) | null> {
1110
+ const token = mcpOAuthBearerToken(request);
1111
+ if (!token) return null;
1112
+ requireMcpOAuthEnabled(deps);
1113
+ const access = await resolveMcpOAuthAccessToken(deps.db, tokenHash(token));
1114
+ if (
1115
+ !access ||
1116
+ access.workspaceId !== null ||
1117
+ access.organizationAccess === null ||
1118
+ access.resource !== `${mcpOAuthIssuer(deps)}${ORGANIZATION_MCP_PATH}`
1119
+ ) {
1120
+ throw new HTTPException(401, { message: "invalid MCP OAuth access token" });
1121
+ }
1122
+ return { ...access, organizationAccess: access.organizationAccess };
1123
+ }