@opengeni/api-router 0.30.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (434) hide show
  1. package/dist/access-grant-rls.d.ts +3 -0
  2. package/dist/admission-trace.d.ts +8 -0
  3. package/dist/api-websocket.d.ts +18 -0
  4. package/dist/app.d.ts +50 -2
  5. package/dist/app.js +12 -1
  6. package/dist/artifact-catalog-cursor.d.ts +27 -0
  7. package/dist/auth/managed-auth-attempt-context.d.ts +10 -0
  8. package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
  9. package/dist/auth/managed-auth-session-adapter.d.ts +6 -0
  10. package/dist/auth/managed-auth.d.ts +102 -3
  11. package/dist/auth/managed-email.d.ts +29 -0
  12. package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
  13. package/dist/auth/new-signups-gate.d.ts +23 -0
  14. package/dist/auth/organization-user-setup.d.ts +80 -0
  15. package/dist/auth/signup-funnel-metrics.d.ts +44 -0
  16. package/dist/browser-controller-authority.d.ts +1 -61
  17. package/dist/browser-ephemeral.d.ts +5 -0
  18. package/dist/browser-state-authority.d.ts +1 -27
  19. package/dist/chunk-6TZ5LG3O.js +123948 -0
  20. package/dist/chunk-6TZ5LG3O.js.map +1 -0
  21. package/dist/chunk-OGQDXVXY.js +18 -0
  22. package/dist/chunk-OGQDXVXY.js.map +1 -0
  23. package/dist/claude-subscription-account-conflicts.d.ts +1 -0
  24. package/dist/claude-subscription-account-usage.d.ts +39 -0
  25. package/dist/claude-subscription-oauth.d.ts +25 -0
  26. package/dist/claude-subscription-profile.d.ts +9 -0
  27. package/dist/claude-subscription-usage.d.ts +44 -0
  28. package/dist/claude-workspace-connection.d.ts +15 -0
  29. package/dist/codemode.d.ts +32 -1
  30. package/dist/company-brain-okf.d.ts +24 -0
  31. package/dist/connected-machine-computer-access.d.ts +11 -0
  32. package/dist/connection-ownership.d.ts +54 -0
  33. package/dist/controller-data-plane.d.ts +32 -0
  34. package/dist/editable-artifact-source-session.d.ts +3 -0
  35. package/dist/editable-artifact-websocket.d.ts +10 -18
  36. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  37. package/dist/fatal-process-boundary.d.ts +25 -0
  38. package/dist/github-access.d.ts +31 -2
  39. package/dist/http/acting-person.d.ts +24 -0
  40. package/dist/http/api-contract-compat.d.ts +19 -0
  41. package/dist/http/api-error.d.ts +27 -1
  42. package/dist/http/cors.d.ts +17 -0
  43. package/dist/http/deprecation.d.ts +42 -0
  44. package/dist/http/interaction-control-error.d.ts +13 -0
  45. package/dist/http/keyed-admission.d.ts +12 -0
  46. package/dist/http/local-browser-boundary.d.ts +72 -0
  47. package/dist/http/metrics-listener.d.ts +22 -0
  48. package/dist/http/path-identifier.d.ts +17 -0
  49. package/dist/http/registered-route-label.d.ts +16 -0
  50. package/dist/http/request-body.d.ts +27 -0
  51. package/dist/http/request-source.d.ts +52 -0
  52. package/dist/http/sse.d.ts +17 -6
  53. package/dist/http/unmatched-route.d.ts +18 -0
  54. package/dist/http/user-content.d.ts +36 -0
  55. package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
  56. package/dist/index.d.ts +10 -3
  57. package/dist/index.js +1091 -162
  58. package/dist/index.js.map +1 -1
  59. package/dist/integration-endpoint-test.d.ts +20 -0
  60. package/dist/integrations/api-integrations.d.ts +2 -2
  61. package/dist/integrations/atlassian.d.ts +29 -3
  62. package/dist/integrations/connect-authority.d.ts +1 -0
  63. package/dist/integrations/connect-callback-return.d.ts +6 -0
  64. package/dist/integrations/fiken.d.ts +16 -2
  65. package/dist/integrations/github-app-connect.d.ts +47 -0
  66. package/dist/integrations/github-installation-proof.d.ts +3 -0
  67. package/dist/integrations/github-lens-connect.d.ts +116 -0
  68. package/dist/integrations/github-skill-source.d.ts +1 -5
  69. package/dist/integrations/google-drive.d.ts +24 -0
  70. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  71. package/dist/integrations/marketplace-plugin.d.ts +2691 -0
  72. package/dist/integrations/oauth-client.d.ts +178 -18
  73. package/dist/integrations/oauth-profiles.d.ts +222 -0
  74. package/dist/integrations/oauth-return-path.d.ts +1 -0
  75. package/dist/integrations/personal-github-repositories.d.ts +84 -0
  76. package/dist/integrations/personal-github.d.ts +42 -0
  77. package/dist/integrations/pr-review-provider.d.ts +22 -0
  78. package/dist/integrations/provider-oauth.d.ts +19 -1
  79. package/dist/integrations/slack-app-home.d.ts +24 -0
  80. package/dist/integrations/slack-bot.d.ts +367 -12
  81. package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
  82. package/dist/integrations/slack-install.d.ts +20 -0
  83. package/dist/integrations/slack-interactions.d.ts +107 -9
  84. package/dist/integrations/slack-mrkdwn.d.ts +41 -0
  85. package/dist/integrations/slack-routing.d.ts +120 -0
  86. package/dist/integrations/slack-session-defaults.d.ts +58 -0
  87. package/dist/integrations/slack-task-file-upload.d.ts +36 -0
  88. package/dist/integrations/social-oauth.d.ts +7 -0
  89. package/dist/interaction-agent-access.d.ts +15 -0
  90. package/dist/interaction-frame-proxy.d.ts +68 -0
  91. package/dist/interaction-metrics.d.ts +2 -0
  92. package/dist/mcp/action-catalog-types.d.ts +9 -0
  93. package/dist/mcp/action-catalog.gen.d.ts +2 -0
  94. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  95. package/dist/mcp/child-read-evidence.d.ts +9 -0
  96. package/dist/mcp/command-read.d.ts +24 -0
  97. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  98. package/dist/mcp/contract-input.d.ts +11 -0
  99. package/dist/mcp/documents.d.ts +7 -5
  100. package/dist/mcp/files.d.ts +2 -2
  101. package/dist/mcp/first-party-tool-permissions.d.ts +650 -0
  102. package/dist/mcp/knowledge-entries.d.ts +5 -0
  103. package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
  104. package/dist/mcp/receipts.d.ts +9 -0
  105. package/dist/mcp/request-abort.d.ts +19 -0
  106. package/dist/mcp/scheduled-task-input.d.ts +1189 -0
  107. package/dist/mcp/scheduled-task-view.d.ts +22 -4
  108. package/dist/mcp/server.d.ts +255 -3
  109. package/dist/mcp/session-event-view.d.ts +94 -0
  110. package/dist/mcp/session-view.d.ts +66 -2
  111. package/dist/mcp/session-wait.d.ts +174 -0
  112. package/dist/mcp-oauth.d.ts +52 -0
  113. package/dist/model-catalog.d.ts +11 -31
  114. package/dist/observability.d.ts +3 -1
  115. package/dist/organization-mcp.d.ts +52 -0
  116. package/dist/organization-recovery-notifications.d.ts +41 -0
  117. package/dist/routes/analytics-consent.d.ts +26 -0
  118. package/dist/routes/api-integrations.d.ts +25 -0
  119. package/dist/routes/api-keys.d.ts +17 -0
  120. package/dist/routes/artifact-catalog.d.ts +6 -0
  121. package/dist/routes/automations.d.ts +19 -0
  122. package/dist/routes/billing.d.ts +49 -0
  123. package/dist/routes/browser-sessions.d.ts +59 -2
  124. package/dist/routes/claude-subscription-accounts.d.ts +3 -0
  125. package/dist/routes/claude-subscription-oauth.d.ts +3 -0
  126. package/dist/routes/client-errors.d.ts +146 -0
  127. package/dist/routes/codex.d.ts +76 -2
  128. package/dist/routes/company-brain.d.ts +3 -0
  129. package/dist/routes/company-profile.d.ts +2 -1
  130. package/dist/routes/computer-sessions.d.ts +14 -0
  131. package/dist/routes/connect.d.ts +5 -0
  132. package/dist/routes/editable-artifacts.d.ts +2 -0
  133. package/dist/routes/external-identity-links.d.ts +6 -0
  134. package/dist/routes/feedback.d.ts +3 -0
  135. package/dist/routes/github-browser-pages.d.ts +12 -0
  136. package/dist/routes/github.d.ts +2 -0
  137. package/dist/routes/insights-response-cache.d.ts +10 -0
  138. package/dist/routes/insights-usage.d.ts +12 -0
  139. package/dist/routes/insights.d.ts +30 -1
  140. package/dist/routes/knowledge.d.ts +3 -0
  141. package/dist/routes/managed-auth-session-sets.d.ts +26 -0
  142. package/dist/routes/managed-onboarding.d.ts +29 -0
  143. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  144. package/dist/routes/model-connection-access.d.ts +3 -0
  145. package/dist/routes/organization-integration-policy.d.ts +3 -0
  146. package/dist/routes/organization-integrations.d.ts +6 -0
  147. package/dist/routes/organization-mcp-connections.d.ts +3 -0
  148. package/dist/routes/organization-model-providers.d.ts +3 -0
  149. package/dist/routes/organization-recovery.d.ts +19 -0
  150. package/dist/routes/organization-service-accounts.d.ts +3 -0
  151. package/dist/routes/organization-sessions.d.ts +26 -0
  152. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  153. package/dist/routes/personal-github.d.ts +3 -0
  154. package/dist/routes/pr-review-github.d.ts +3 -0
  155. package/dist/routes/pr-review.d.ts +3 -0
  156. package/dist/routes/session-artifact-associations.d.ts +4 -0
  157. package/dist/routes/session-history-imports.d.ts +9 -0
  158. package/dist/routes/sessions.d.ts +44 -6
  159. package/dist/routes/skill-content.d.ts +4 -0
  160. package/dist/routes/skill-install-authority.d.ts +6 -0
  161. package/dist/routes/subscription-account-pools.d.ts +34 -0
  162. package/dist/routes/subscription-pool-access.d.ts +16 -0
  163. package/dist/routes/supergrok.d.ts +6 -1
  164. package/dist/routes/usage-allowances.d.ts +12 -0
  165. package/dist/routes/user-resource-authorities.d.ts +3 -0
  166. package/dist/routes/workspace-artifacts.d.ts +2 -1
  167. package/dist/routes/workspace-capture.d.ts +10 -2
  168. package/dist/routes/workspace-integrations.d.ts +35 -0
  169. package/dist/routes/workspace-learning.d.ts +3 -0
  170. package/dist/routes/workspace-model-providers.d.ts +4 -0
  171. package/dist/routes/workspaces.d.ts +29 -0
  172. package/dist/sandbox/auth-callout.d.ts +8 -0
  173. package/dist/sandbox/channel-a.d.ts +19 -1
  174. package/dist/sandbox/connection-authority.d.ts +3 -0
  175. package/dist/sandbox/enrollment.d.ts +9 -1
  176. package/dist/sandbox/machines.d.ts +5 -2
  177. package/dist/sandbox/metrics-ingestion.d.ts +28 -20
  178. package/dist/sandbox/rematerialize.d.ts +2 -0
  179. package/dist/sandbox/session-attach-variable-sets.d.ts +30 -0
  180. package/dist/sandbox/viewer.d.ts +26 -1
  181. package/dist/sandbox-file-artifacts.d.ts +50 -0
  182. package/dist/scheduled-task-deletion.d.ts +13 -0
  183. package/dist/session-create-observability.d.ts +5 -0
  184. package/dist/session-event-view-MPS2OZD3.js +292 -0
  185. package/dist/session-event-view-MPS2OZD3.js.map +1 -0
  186. package/dist/site-session-origin.d.ts +13 -0
  187. package/dist/site-uploads.d.ts +41 -0
  188. package/dist/slack-reaction-files.d.ts +1 -1
  189. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  190. package/dist/temporal-schedule-sync.d.ts +26 -0
  191. package/dist/transcription/providers/xai-subscription.d.ts +1 -0
  192. package/dist/transcription/service.d.ts +2 -1
  193. package/dist/work-discovery-observability.d.ts +33 -0
  194. package/dist/workspace-artifact-content.d.ts +28 -0
  195. package/dist/workspace-artifact-provenance.d.ts +7 -0
  196. package/dist/workspace-delete-observability.d.ts +10 -0
  197. package/dist/workspace-deletion.d.ts +6 -0
  198. package/dist/workspace-state-projection.d.ts +3 -5
  199. package/dist/workspace-tool-gateway-observability.d.ts +17 -0
  200. package/dist/workspace-tool-gateway.d.ts +123 -0
  201. package/dist/workspace-webhook-dispatch.d.ts +26 -0
  202. package/package.json +23 -19
  203. package/src/access-grant-rls.ts +75 -0
  204. package/src/admission-trace.ts +14 -0
  205. package/src/api-websocket.ts +23 -0
  206. package/src/app.ts +1967 -252
  207. package/src/artifact-catalog-cursor.ts +60 -0
  208. package/src/auth/managed-auth-attempt-context.ts +70 -0
  209. package/src/auth/managed-auth-rate-limits.ts +252 -0
  210. package/src/auth/managed-auth-session-adapter.ts +222 -0
  211. package/src/auth/managed-auth.ts +536 -60
  212. package/src/auth/managed-email.ts +174 -0
  213. package/src/auth/managed-sign-in-notifications.ts +81 -0
  214. package/src/auth/new-signups-gate.ts +49 -0
  215. package/src/auth/organization-user-setup.ts +307 -0
  216. package/src/auth/signup-funnel-metrics.ts +164 -0
  217. package/src/browser-controller-authority.ts +1 -186
  218. package/src/browser-ephemeral.ts +39 -0
  219. package/src/browser-state-authority.ts +1 -236
  220. package/src/claude-subscription-account-conflicts.ts +12 -0
  221. package/src/claude-subscription-account-usage.ts +48 -0
  222. package/src/claude-subscription-oauth.ts +362 -0
  223. package/src/claude-subscription-profile.ts +73 -0
  224. package/src/claude-subscription-usage.ts +97 -0
  225. package/src/claude-workspace-connection.ts +82 -0
  226. package/src/codemode.ts +194 -30
  227. package/src/codex-realtime.ts +8 -2
  228. package/src/company-brain-okf.ts +340 -0
  229. package/src/connected-machine-computer-access.ts +33 -0
  230. package/src/connection-ownership.ts +199 -0
  231. package/src/controller-data-plane.ts +81 -0
  232. package/src/editable-artifact-native-kernel.ts +26 -15
  233. package/src/editable-artifact-office-import.ts +28 -3
  234. package/src/editable-artifact-production.ts +23 -7
  235. package/src/editable-artifact-source-session.ts +123 -0
  236. package/src/editable-artifact-websocket.ts +27 -21
  237. package/src/editable-artifact-workspace-files.ts +31 -6
  238. package/src/fatal-process-boundary.ts +246 -0
  239. package/src/gateway-realtime.ts +1 -1
  240. package/src/github-access.ts +147 -5
  241. package/src/http/acting-person.ts +71 -0
  242. package/src/http/api-contract-compat.ts +49 -0
  243. package/src/http/api-error.ts +115 -1
  244. package/src/http/auth.ts +48 -0
  245. package/src/http/cors.ts +67 -0
  246. package/src/http/deprecation.ts +118 -0
  247. package/src/http/interaction-control-error.ts +164 -0
  248. package/src/http/keyed-admission.ts +29 -0
  249. package/src/http/local-browser-boundary.ts +343 -0
  250. package/src/http/metrics-listener.ts +57 -0
  251. package/src/http/path-identifier.ts +54 -0
  252. package/src/http/registered-route-label.ts +34 -0
  253. package/src/http/request-body.ts +127 -0
  254. package/src/http/request-source.ts +178 -0
  255. package/src/http/sse.ts +486 -132
  256. package/src/http/unmatched-route.ts +60 -0
  257. package/src/http/user-content.ts +158 -0
  258. package/src/http/websocket-upgrade-dispatch.ts +29 -0
  259. package/src/index.ts +157 -52
  260. package/src/integration-endpoint-test.ts +291 -0
  261. package/src/integrations/api-integrations.ts +98 -11
  262. package/src/integrations/atlassian.ts +474 -136
  263. package/src/integrations/connect-authority.ts +2 -0
  264. package/src/integrations/connect-callback-return.ts +86 -0
  265. package/src/integrations/fiken.ts +354 -64
  266. package/src/integrations/github-app-connect.ts +435 -0
  267. package/src/integrations/github-installation-proof.ts +60 -0
  268. package/src/integrations/github-lens-connect.ts +76 -0
  269. package/src/integrations/github-skill-source.ts +2 -142
  270. package/src/integrations/google-drive.ts +717 -225
  271. package/src/integrations/integration-commit-authority.ts +69 -0
  272. package/src/integrations/marketplace-plugin.ts +48 -0
  273. package/src/integrations/oauth-client.ts +1356 -459
  274. package/src/integrations/oauth-profiles.ts +537 -0
  275. package/src/integrations/oauth-return-path.ts +18 -0
  276. package/src/integrations/personal-github-repositories.ts +674 -0
  277. package/src/integrations/personal-github.ts +825 -0
  278. package/src/integrations/pr-review-provider.ts +246 -0
  279. package/src/integrations/provider-oauth.ts +250 -22
  280. package/src/integrations/slack-app-home.ts +300 -0
  281. package/src/integrations/slack-bot.ts +963 -239
  282. package/src/integrations/slack-file-upload-flow.ts +147 -0
  283. package/src/integrations/slack-install.ts +83 -0
  284. package/src/integrations/slack-interactions.ts +3167 -474
  285. package/src/integrations/slack-mrkdwn.ts +193 -0
  286. package/src/integrations/slack-routing.ts +337 -0
  287. package/src/integrations/slack-session-defaults.ts +224 -0
  288. package/src/integrations/slack-task-file-upload.ts +323 -0
  289. package/src/integrations/social-oauth.ts +213 -15
  290. package/src/interaction-agent-access.ts +48 -0
  291. package/src/interaction-frame-proxy.ts +409 -0
  292. package/src/interaction-metrics.ts +30 -0
  293. package/src/mcp/action-catalog-types.ts +9 -0
  294. package/src/mcp/action-catalog.gen.ts +6696 -0
  295. package/src/mcp/capability-account-readiness.ts +17 -0
  296. package/src/mcp/child-read-evidence.ts +19 -0
  297. package/src/mcp/command-read.ts +109 -0
  298. package/src/mcp/company-profile-agent-admin.ts +198 -0
  299. package/src/mcp/contract-input.ts +141 -0
  300. package/src/mcp/documents.ts +65 -377
  301. package/src/mcp/editable-artifacts.ts +12 -6
  302. package/src/mcp/files.ts +80 -43
  303. package/src/mcp/first-party-tool-permissions.ts +305 -0
  304. package/src/mcp/knowledge-entries.ts +322 -0
  305. package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
  306. package/src/mcp/receipts.ts +34 -0
  307. package/src/mcp/request-abort.ts +44 -0
  308. package/src/mcp/scheduled-task-input.ts +76 -0
  309. package/src/mcp/scheduled-task-view.ts +44 -3
  310. package/src/mcp/server.ts +3030 -1267
  311. package/src/mcp/session-event-view.ts +383 -0
  312. package/src/mcp/session-view.ts +84 -3
  313. package/src/mcp/session-wait.ts +665 -0
  314. package/src/mcp-oauth.ts +1123 -0
  315. package/src/model-catalog.ts +91 -333
  316. package/src/observability.ts +15 -1
  317. package/src/organization-mcp.ts +397 -0
  318. package/src/organization-recovery-notifications.ts +103 -0
  319. package/src/routes/analytics-consent.ts +123 -0
  320. package/src/routes/api-integrations.ts +364 -74
  321. package/src/routes/api-keys.ts +336 -10
  322. package/src/routes/artifact-catalog.ts +279 -0
  323. package/src/routes/automations.ts +768 -0
  324. package/src/routes/billing.ts +461 -46
  325. package/src/routes/browser-sessions.ts +780 -112
  326. package/src/routes/canonical-human-identities.ts +90 -14
  327. package/src/routes/capabilities.ts +107 -6
  328. package/src/routes/channels.ts +20 -3
  329. package/src/routes/claude-subscription-accounts.ts +334 -0
  330. package/src/routes/claude-subscription-oauth.ts +142 -0
  331. package/src/routes/client-errors.ts +362 -0
  332. package/src/routes/codex.ts +833 -116
  333. package/src/routes/company-brain.ts +322 -0
  334. package/src/routes/company-profile.ts +65 -1
  335. package/src/routes/computer-sessions.ts +557 -106
  336. package/src/routes/connect.ts +1938 -0
  337. package/src/routes/connections.ts +1039 -349
  338. package/src/routes/documents.ts +488 -287
  339. package/src/routes/editable-artifacts.ts +79 -9
  340. package/src/routes/enrollments.ts +135 -39
  341. package/src/routes/environments.ts +154 -58
  342. package/src/routes/external-identity-links.ts +201 -0
  343. package/src/routes/feedback.ts +113 -0
  344. package/src/routes/files.ts +267 -33
  345. package/src/routes/github-browser-pages.ts +162 -0
  346. package/src/routes/github.ts +446 -144
  347. package/src/routes/insights-response-cache.ts +83 -0
  348. package/src/routes/insights-usage.ts +345 -0
  349. package/src/routes/insights.ts +153 -19
  350. package/src/routes/install.ts +133 -38
  351. package/src/routes/integration-facets.ts +107 -33
  352. package/src/routes/interaction-resources.ts +30 -3
  353. package/src/routes/knowledge.ts +488 -0
  354. package/src/routes/machines.ts +313 -15
  355. package/src/routes/managed-auth-session-sets.ts +1218 -0
  356. package/src/routes/managed-onboarding.ts +337 -0
  357. package/src/routes/managed-sign-in-methods.ts +433 -0
  358. package/src/routes/memory-slack-publications.ts +3 -2
  359. package/src/routes/model-connection-access.ts +208 -0
  360. package/src/routes/organization-integration-policy.ts +116 -0
  361. package/src/routes/organization-integrations.ts +326 -0
  362. package/src/routes/organization-mcp-connections.ts +178 -0
  363. package/src/routes/organization-memberships.ts +1019 -27
  364. package/src/routes/organization-model-providers.ts +293 -0
  365. package/src/routes/organization-recovery.ts +325 -0
  366. package/src/routes/organization-service-accounts.ts +128 -0
  367. package/src/routes/organization-sessions.ts +350 -0
  368. package/src/routes/personal-github-git-broker.ts +785 -0
  369. package/src/routes/personal-github.ts +370 -0
  370. package/src/routes/plugins.ts +251 -22
  371. package/src/routes/pr-review-github.ts +881 -0
  372. package/src/routes/pr-review.ts +622 -0
  373. package/src/routes/preference-registry.ts +12 -93
  374. package/src/routes/rigs.ts +167 -50
  375. package/src/routes/scheduled-tasks.ts +346 -41
  376. package/src/routes/session-artifact-associations.ts +87 -0
  377. package/src/routes/session-history-imports.ts +280 -0
  378. package/src/routes/sessions.ts +2306 -264
  379. package/src/routes/skill-content.ts +297 -0
  380. package/src/routes/skill-install-authority.ts +64 -0
  381. package/src/routes/skills.ts +62 -9
  382. package/src/routes/social.ts +46 -18
  383. package/src/routes/subscription-account-pools.ts +436 -0
  384. package/src/routes/subscription-pool-access.ts +126 -0
  385. package/src/routes/supergrok.ts +219 -303
  386. package/src/routes/transcription-recordings.ts +38 -3
  387. package/src/routes/transcriptions.ts +4 -0
  388. package/src/routes/usage-allowances.ts +378 -0
  389. package/src/routes/user-resource-authorities.ts +138 -0
  390. package/src/routes/video-generation.ts +14 -10
  391. package/src/routes/workspace-artifacts.ts +275 -69
  392. package/src/routes/workspace-capture.ts +21 -0
  393. package/src/routes/workspace-integrations.ts +576 -0
  394. package/src/routes/workspace-learning.ts +159 -0
  395. package/src/routes/workspace-model-providers.ts +148 -0
  396. package/src/routes/workspace-state.ts +38 -25
  397. package/src/routes/workspaces.ts +1004 -127
  398. package/src/sandbox/auth-callout.ts +96 -19
  399. package/src/sandbox/channel-a.ts +323 -24
  400. package/src/sandbox/connection-authority.ts +3 -0
  401. package/src/sandbox/enrollment.ts +81 -6
  402. package/src/sandbox/machines.ts +218 -76
  403. package/src/sandbox/metrics-ingestion.ts +411 -62
  404. package/src/sandbox/rematerialize.ts +121 -19
  405. package/src/sandbox/session-attach-variable-sets.ts +131 -0
  406. package/src/sandbox/viewer.ts +280 -25
  407. package/src/sandbox-file-artifacts.ts +413 -0
  408. package/src/scheduled-task-deletion.ts +138 -0
  409. package/src/session-create-observability.ts +46 -0
  410. package/src/session-realtime-context.ts +6 -3
  411. package/src/site-session-origin.ts +48 -0
  412. package/src/site-uploads.ts +224 -0
  413. package/src/slack-reaction-files.ts +16 -5
  414. package/src/temporal-schedule-cleanup.ts +13 -0
  415. package/src/temporal-schedule-sync.ts +74 -0
  416. package/src/transcription/providers/codex-subscription.ts +2 -0
  417. package/src/transcription/providers/openai.ts +1 -0
  418. package/src/transcription/providers/xai-subscription.ts +49 -3
  419. package/src/transcription/segmenter.ts +8 -2
  420. package/src/transcription/service.ts +43 -6
  421. package/src/work-discovery-observability.ts +121 -0
  422. package/src/workspace-artifact-content.ts +144 -0
  423. package/src/workspace-artifact-provenance.ts +104 -0
  424. package/src/workspace-delete-observability.ts +75 -0
  425. package/src/workspace-deletion.ts +73 -0
  426. package/src/workspace-state-projection.ts +18 -148
  427. package/src/workspace-tool-gateway-observability.ts +100 -0
  428. package/src/workspace-tool-gateway.ts +870 -0
  429. package/src/workspace-webhook-dispatch.ts +190 -0
  430. package/src/xai-subscription-auth.ts +9 -2
  431. package/dist/chunk-WCXHI3FF.js +0 -49920
  432. package/dist/chunk-WCXHI3FF.js.map +0 -1
  433. package/dist/routes/packs.d.ts +0 -3
  434. package/src/routes/packs.ts +0 -729
package/src/app.ts CHANGED
@@ -1,25 +1,52 @@
1
+ import { registerClaudeSubscriptionAccountRoutes } from "./routes/claude-subscription-accounts";
2
+ import { registerWorkspaceModelProviderRoutes } from "./routes/workspace-model-providers";
3
+ import { registerClaudeSubscriptionOAuthRoutes } from "./routes/claude-subscription-oauth";
4
+ import { registerConnectCallbackReturns } from "./integrations/connect-callback-return";
5
+ import { registerFeedbackRoutes } from "./routes/feedback";
6
+ import { registerWorkspaceIntegrationRoutes } from "./routes/workspace-integrations";
7
+ import { registerOrganizationIntegrationRoutes } from "./routes/organization-integrations";
1
8
  import {
2
- canonicalizeConfiguredModelId,
3
- configuredAllowedModels,
9
+ CLIENT_ERRORS_PATH,
10
+ isClientErrorReportRequest,
11
+ registerClientErrorRoutes,
12
+ } from "./routes/client-errors";
13
+ import {
14
+ ANALYTICS_CONSENT_PATH,
15
+ isAnalyticsConsentReportRequest,
16
+ registerAnalyticsConsentRoutes,
17
+ } from "./routes/analytics-consent";
18
+ import { codemodeSessionRequest } from "./codemode";
19
+ import { SiteSessionPathError, OrganizationIntegrationDeniedError } from "@opengeni/contracts";
20
+ import { registerModelConnectionAccessRoutes } from "./routes/model-connection-access";
21
+ import {
22
+ codeSearchDeploymentPolicy,
23
+ agentConfigDeploymentPolicy,
4
24
  configuredAllowedReasoningEfforts,
5
- configuredModels,
6
25
  resolveFirstPartyMcpToolPolicy,
7
- withCodexCatalogProvider,
8
- withXaiSubscriptionCatalogProvider,
26
+ resolveVoiceInputProviderRegistry,
27
+ UnsupportedLatencyModeError,
28
+ type Settings,
9
29
  } from "@opengeni/config";
10
30
  import {
31
+ AGENT_CAPABILITY_IDS,
11
32
  ClientConfig,
12
33
  CodemodeCallRequest,
13
34
  ErrorEnvelope,
14
35
  OPENGENI_API_CONTRACT_HEADER,
15
36
  OPENGENI_API_CONTRACT_REVISION,
16
37
  OPENGENI_CORRELATION_HEADER,
38
+ resolveAgentToolFamilies,
17
39
  resolveWorkspaceMemoryEnabled,
18
40
  resolveWorkspaceMemoryPromptMode,
19
41
  VOICE_INPUT_ACCEPTED_MIME_TYPES,
20
42
  TRANSCRIPTION_RECORDING_PROVIDER_SEGMENT_SECONDS,
43
+ ToolGatewayApprovalRequest,
44
+ ToolGatewayCallRequest,
21
45
  type AccessGrant,
46
+ type ClientAgentConfig,
22
47
  type ErrorCode,
48
+ type FirstPartyMcpToolName,
49
+ type Session,
23
50
  } from "@opengeni/contracts";
24
51
  import {
25
52
  createDocumentServices,
@@ -28,44 +55,146 @@ import {
28
55
  type DocumentServices,
29
56
  } from "@opengeni/documents";
30
57
  import {
31
- CodemodeCallBudgetExceededError,
32
58
  CodemodeOperationConflictError,
33
59
  CodemodeOperationNotExecutableError,
34
60
  CodemodePayloadTooLargeError,
35
61
  CodemodeToolApprovalRequiredError,
36
62
  CodemodeToolNotInCatalogError,
63
+ ConnectAttemptConflictError,
64
+ ConnectAttemptNotFoundError,
65
+ configureChildLifecycleNotices,
66
+ configureCodeSearchDeploymentPolicy,
67
+ configureWorkspaceControlRequestLockTimeoutMs,
37
68
  dbSql,
69
+ getManagedAuthSessionSetSnapshot,
38
70
  getWorkspace,
71
+ reapManagedAuthIsolatedSessions,
72
+ reapExpiredManagedAuthSessionSets,
73
+ resolveSessionMemoryAgentScope,
39
74
  rlsContextForWorkspace,
40
- withSessionRlsActorContext,
41
75
  } from "@opengeni/db";
42
- import { createObservability } from "@opengeni/observability";
76
+ import { requireSessionEventDurableFanoutCapability } from "@opengeni/events";
77
+ import { githubAppBotIdentityWarnings } from "@opengeni/github";
78
+ import {
79
+ createObservability,
80
+ failureDiagnostic,
81
+ withTraceContext,
82
+ withMcpTelemetry,
83
+ } from "@opengeni/observability";
43
84
  import { createObjectStorage } from "@opengeni/storage";
44
85
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
45
- import { Hono } from "hono";
86
+ import { SUPPORTED_PROTOCOL_VERSIONS } from "@modelcontextprotocol/sdk/types.js";
87
+ import { handleMcpRequestWithClientAbort } from "./mcp/request-abort";
88
+ import { Hono, type Context } from "hono";
46
89
  import { bodyLimit } from "hono/body-limit";
47
90
  import { compress } from "hono/compress";
48
91
  import { cors } from "hono/cors";
92
+ import { getCookie } from "hono/cookie";
49
93
  import { HTTPException } from "hono/http-exception";
50
- import { ApiHttpError } from "./http/api-error";
94
+ import {
95
+ ApiHttpError,
96
+ agentConfigHttpError,
97
+ allowanceExhaustedHttpError,
98
+ modelUnavailableHttpError,
99
+ scheduledTaskTargetAccessHttpError,
100
+ workspaceControlBusyHttpError,
101
+ } from "./http/api-error";
102
+ import {
103
+ isRequestBodyValidationError,
104
+ requestBodyValidationHttpError,
105
+ tagRequestJsonParseErrors,
106
+ } from "./http/request-body";
107
+ import { invalidPathIdentifierHttpError } from "./http/path-identifier";
108
+ import { replaceTrustedClientAddressHeader } from "./http/request-source";
109
+ import { unmatchedRoute } from "./http/unmatched-route";
51
110
  import type { ContentfulStatusCode } from "hono/utils/http-status";
111
+ import { bearerApiContractHeaderCompatibility } from "./http/api-contract-compat";
112
+ import { deprecationHeadersMiddleware } from "./http/deprecation";
113
+ import {
114
+ boundedRegisteredRouteLabel,
115
+ registeredHandlerRoutePath,
116
+ } from "./http/registered-route-label";
52
117
  import type { ApiRouteDeps, AppDependencies } from "@opengeni/core";
53
118
  import {
54
119
  CodexCompactionV2ProviderLockedError,
120
+ ManagedAuthActorLeaseOutcomeUnknownError,
55
121
  hasPermission,
56
122
  requireAccessGrant,
57
- requireLiveAgentAttemptAuthorization,
123
+ requireAccessGrantAuthorization,
124
+ accountScopedApiKeyWorkspaceAuthority,
125
+ requireAccessContext,
58
126
  requirePermission,
127
+ releaseManagedAuthRequestActorLease,
128
+ resolveCatalogSettings,
129
+ resolveWorkspaceCatalogSettings,
130
+ resolveCallerWorkspaceModelSelections,
131
+ resolveDefaultSessionModelForSelections,
132
+ selectDefaultSessionModel,
133
+ isWorkspaceModelAdmissible,
134
+ resolveWorkspaceModelSelection,
135
+ validateManagedAuthRequestActorLease,
59
136
  requireSessionAuthorization,
60
137
  SessionAuthorizationDeniedError,
138
+ withSessionAuthorizationReadReuse,
61
139
  SessionAuthorizationUnavailableError,
140
+ createUserPresenceRecorder,
141
+ registerProductUsageMetricBaselines,
62
142
  } from "@opengeni/core";
63
- import { createManagedAuth } from "./auth/managed-auth";
143
+ import { createManagedAuthNewSignupsGate } from "./auth/new-signups-gate";
144
+ import {
145
+ createManagedAuth,
146
+ isolatedManagedAuthOAuthCallbackRequest,
147
+ managedAuthNewSignupsPausedResponse,
148
+ managedAuthOAuthReturnMatches,
149
+ resolveManagedAuthOAuthAttempt,
150
+ } from "./auth/managed-auth";
151
+ import {
152
+ adoptManagedAuthSession,
153
+ MANAGED_AUTH_SESSION_SET_COOKIE,
154
+ ManagedAuthActorChangeError,
155
+ managedAuthCsrfHash,
156
+ managedAuthDerivedUuid,
157
+ managedAuthSecretRequestDigest,
158
+ managedAuthSha256,
159
+ } from "@opengeni/core/managed-auth-session-sets";
160
+ import { createBetterAuthSessionAdapter } from "./auth/managed-auth-session-adapter";
161
+ import {
162
+ currentManagedAuthCreatedSessionId,
163
+ runManagedAuthAttempt,
164
+ runManagedAuthDiscardedProviderSession,
165
+ runManagedAuthProvider,
166
+ } from "./auth/managed-auth-attempt-context";
167
+ import { createManagedEmailTransport } from "./auth/managed-email";
168
+ import { startManagedSignInNotificationDelivery } from "./auth/managed-sign-in-notifications";
169
+ import {
170
+ assertManagedEmailTransportMetadata,
171
+ assertOrganizationUserSetupQueryTransportConfigured,
172
+ } from "./auth/organization-user-setup";
64
173
  import { createApiSandboxClient, makeResumeBoxById } from "./sandbox/access";
65
- import { requireLimit } from "@opengeni/core";
66
174
  import { buildOpenGeniMcpServer } from "./mcp/server";
175
+ import {
176
+ buildWorkspaceToolGatewayMcpServer,
177
+ approveWorkspaceToolGatewayCall,
178
+ callWorkspaceToolGateway,
179
+ grantUsesAttemptScopedMcp,
180
+ prepareMcpOAuthWorkspaceToolGateway,
181
+ prepareWorkspaceToolGateway,
182
+ workspaceToolGatewayDeclarations,
183
+ } from "./workspace-tool-gateway";
184
+ import {
185
+ isMcpOAuthResourcePath,
186
+ mcpOAuthAuthenticateHeader,
187
+ mcpOAuthBearerToken,
188
+ ORGANIZATION_MCP_PATH,
189
+ registerMcpOAuthRoutes,
190
+ resolveMcpOAuthRouteAccess,
191
+ resolveOrganizationMcpOAuthAccess,
192
+ } from "./mcp-oauth";
193
+ import { buildOrganizationMcpServer, type OrganizationMcpCaller } from "./organization-mcp";
67
194
  import {
68
195
  CodemodeAuthorityError,
196
+ CodemodeCatalogNotReadyError,
197
+ CodemodeCatalogStaleError,
69
198
  isCodemodeGrant,
70
199
  readCodemodeOperation,
71
200
  requireActiveCodemodeCatalog,
@@ -77,49 +206,97 @@ import {
77
206
  BrowserControlTransportError,
78
207
  } from "@opengeni/runtime/sandbox";
79
208
  import { requireAccessKey } from "./http/auth";
209
+ import {
210
+ publicListenerServesMetrics,
211
+ registerPrometheusMetricsRoute,
212
+ } from "./http/metrics-listener";
80
213
  import { allowedCorsOrigin } from "./http/cors";
214
+ import {
215
+ createLocalBrowserBoundary,
216
+ localBrowserRequestHost,
217
+ markLocalInternalDispatch,
218
+ } from "./http/local-browser-boundary";
219
+ import { withAccessGrantSessionRlsContext } from "./access-grant-rls";
81
220
  import { registerCapabilityRoutes } from "./routes/capabilities";
82
221
  import { registerCatalogAssetRoutes } from "./routes/catalog-assets";
83
222
  import { registerCodexRoutes } from "./routes/codex";
223
+ import { registerOrganizationModelProviderRoutes } from "./routes/organization-model-providers";
224
+ import { registerOrganizationIntegrationPolicyRoutes } from "./routes/organization-integration-policy";
84
225
  import { registerSuperGrokRoutes } from "./routes/supergrok";
85
226
  import { registerConnectionRoutes } from "./routes/connections";
227
+ import { registerConnectRoutes } from "./routes/connect";
228
+
229
+ import { registerExternalIdentityLinkRoutes } from "./routes/external-identity-links";
86
230
  import { registerDocumentRoutes } from "./routes/documents";
231
+ import { registerKnowledgeRoutes } from "./routes/knowledge";
87
232
  import { registerEnrollmentRoutes } from "./routes/enrollments";
88
233
  import { registerMachineRoutes } from "./routes/machines";
89
234
  import { registerMemorySlackPublicationRoutes } from "./routes/memory-slack-publications";
90
235
  import { registerEnvironmentRoutes } from "./routes/environments";
91
236
  import { registerFileRoutes } from "./routes/files";
92
237
  import { registerApiKeyRoutes } from "./routes/api-keys";
238
+ import { registerOrganizationMcpConnectionRoutes } from "./routes/organization-mcp-connections";
239
+ import { registerOrganizationServiceAccountRoutes } from "./routes/organization-service-accounts";
93
240
  import { registerBillingRoutes } from "./routes/billing";
94
241
  import { registerBrowserIdentityRoutes } from "./routes/browser-identities";
95
242
  import { registerBrowserSessionRoutes } from "./routes/browser-sessions";
96
243
  import { registerComputerSessionRoutes } from "./routes/computer-sessions";
97
244
  import { registerGitHubRoutes } from "./routes/github";
245
+ import { registerPersonalGitHubRoutes } from "./routes/personal-github";
246
+ import {
247
+ isPersonalGitHubGitBrokerRequest,
248
+ registerPersonalGitHubGitBrokerRoutes,
249
+ } from "./routes/personal-github-git-broker";
98
250
  import { registerInstallRoutes } from "./routes/install";
99
251
  import { registerApiIntegrationRoutes } from "./routes/api-integrations";
100
252
  import { registerIntegrationFacetRoutes } from "./routes/integration-facets";
101
253
  import { registerInteractionResourceRoutes } from "./routes/interaction-resources";
102
- import { registerPackRoutes } from "./routes/packs";
254
+ import { registerPrReviewRoutes } from "./routes/pr-review";
255
+ import { registerAutomationRoutes } from "./routes/automations";
103
256
  import { registerPluginRoutes } from "./routes/plugins";
104
257
  import { registerSkillRoutes } from "./routes/skills";
105
258
  import { registerChannelRoutes } from "./routes/channels";
106
259
  import { registerRigRoutes } from "./routes/rigs";
107
260
  import { registerScheduledTaskRoutes } from "./routes/scheduled-tasks";
108
261
  import { registerSessionRoutes } from "./routes/sessions";
262
+ import {
263
+ archivedSessionImportErrorResponse,
264
+ isSessionHistoryImportRequest,
265
+ registerSessionHistoryImportRoutes,
266
+ } from "./routes/session-history-imports";
109
267
  import { registerSocialRoutes } from "./routes/social";
110
268
  import { registerWorkspaceRoutes } from "./routes/workspaces";
269
+ import { registerUsageAllowanceRoutes } from "./routes/usage-allowances";
111
270
  import { registerWorkspaceInstructionPolicyRoutes } from "./routes/workspace-instruction-policies";
271
+ import { registerWorkspaceLearningRoutes } from "./routes/workspace-learning";
112
272
  import { registerCompanyProfileRoutes } from "./routes/company-profile";
273
+ import { registerCompanyBrainRoutes } from "./routes/company-brain";
113
274
  import { registerSlackTaskPolicyRoutes } from "./routes/slack-task-policy";
114
275
  import { registerWorkspaceStateRoutes } from "./routes/workspace-state";
115
276
  import { registerWorkspaceArtifactRoutes } from "./routes/workspace-artifacts";
277
+ import { registerArtifactCatalogRoutes } from "./routes/artifact-catalog";
116
278
  import { registerPreferenceRegistryRoutes } from "./routes/preference-registry";
117
279
  import { registerInsightsRoutes } from "./routes/insights";
280
+ import { registerInsightsUsageRoutes } from "./routes/insights-usage";
118
281
  import { registerTranscriptionRoutes } from "./routes/transcriptions";
119
282
  import { registerEditableArtifactRoutes } from "./routes/editable-artifacts";
283
+ import { registerSessionArtifactAssociationRoutes } from "./routes/session-artifact-associations";
120
284
  import { registerVideoGenerationRoutes } from "./routes/video-generation";
121
285
  import { registerCanonicalHumanIdentityRoutes } from "./routes/canonical-human-identities";
122
286
  import { registerOrganizationMembershipRoutes } from "./routes/organization-memberships";
287
+ import { registerOrganizationSessionRoutes } from "./routes/organization-sessions";
288
+ import { registerOrganizationRecoveryRoutes } from "./routes/organization-recovery";
289
+ import { registerManagedOnboardingRoutes } from "./routes/managed-onboarding";
290
+ import {
291
+ registerManagedSignInMethodRoutes,
292
+ handleManagedSignInConnectCallback,
293
+ } from "./routes/managed-sign-in-methods";
294
+ import {
295
+ registerManagedAuthSessionSetRoutes,
296
+ requireManagedAuthProviderRouteAllowed,
297
+ scrubManagedAuthProviderResponse,
298
+ } from "./routes/managed-auth-session-sets";
299
+ import { registerUserResourceAuthorityRoutes } from "./routes/user-resource-authorities";
123
300
  import { projectClientModel } from "./model-catalog";
124
301
  import { createTranscriptionService } from "./transcription/service";
125
302
  import { createFfmpegTranscriptionSegmenter } from "./transcription/segmenter";
@@ -149,6 +326,8 @@ export { workflowIdForSession } from "@opengeni/core";
149
326
  export { replaySessionEvents, sseSessionStream, sseWorkspaceControlStream } from "./http/sse";
150
327
 
151
328
  export const API_MAX_REQUEST_BODY_BYTES = 32 * 1024 * 1024;
329
+ const managedAuthReaperDatabases = new WeakSet<object>();
330
+ const MANAGED_AUTH_REAPER_INTERVAL_MS = 60_000;
152
331
 
153
332
  /** Effective Hono bodyLimit — API JSON ceiling or voice multipart + multipart overhead. */
154
333
  export function apiRequestBodyLimitBytes(settings: {
@@ -167,11 +346,58 @@ export function createApp(deps: AppDependencies): Hono {
167
346
  return createAppComposition(deps).app;
168
347
  }
169
348
 
349
+ export async function resolveWorkspaceMcpRouteDeps(
350
+ routeDeps: ApiRouteDeps,
351
+ grant: AccessGrant,
352
+ ): Promise<ApiRouteDeps> {
353
+ const catalogSourceSettings = routeDeps.catalogSourceSettings ?? routeDeps.settings;
354
+ const resolvedCatalog = await resolveWorkspaceCatalogSettings(
355
+ routeDeps.db,
356
+ catalogSourceSettings,
357
+ {
358
+ accountId: grant.accountId,
359
+ workspaceId: grant.workspaceId,
360
+ },
361
+ );
362
+ return { ...routeDeps, catalogSourceSettings, settings: resolvedCatalog.settings };
363
+ }
364
+
170
365
  export function createAppComposition(deps: AppDependencies): {
171
366
  app: Hono;
172
367
  routeDeps: ApiRouteDeps;
173
368
  } {
174
- const managedAuth = deps.managedAuth ?? createManagedAuth(deps.settings, deps.db);
369
+ assertOrganizationUserSetupQueryTransportConfigured(deps.settings);
370
+ // The request-scoped workspace control-prefix budget is validated once by
371
+ // @opengeni/config at boot; install it for every request-scoped db command
372
+ // (Send/Steer/control/queue/settings/delete) built by this app.
373
+ configureWorkspaceControlRequestLockTimeoutMs(deps.settings.workspaceControlLockTimeoutMs);
374
+ // Child lifecycle notice producers (human-input/approval resolutions, direct
375
+ // Pause) run inside API-originated db commands; install the boot-validated
376
+ // rollout flag once for this process.
377
+ configureChildLifecycleNotices({ enabled: deps.settings.childLifecycleNoticesEnabled });
378
+ // Sessions created by this process freeze their code_search decision from
379
+ // the boot-parsed deployment policy.
380
+ configureCodeSearchDeploymentPolicy(codeSearchDeploymentPolicy(deps.settings));
381
+ const managedEmailTransport =
382
+ deps.managedEmailTransport ?? createManagedEmailTransport(deps.settings);
383
+ assertManagedEmailTransportMetadata(managedEmailTransport);
384
+ const observability =
385
+ deps.observability ?? createObservability(deps.settings, { component: "api" });
386
+ const managedAuthNewSignupsGate = createManagedAuthNewSignupsGate({
387
+ db: deps.db,
388
+ settings: deps.settings,
389
+ observability,
390
+ });
391
+ const managedAuth =
392
+ deps.managedAuth ??
393
+ createManagedAuth(deps.settings, deps.db, managedEmailTransport, {
394
+ observability,
395
+ newSignupsGate: managedAuthNewSignupsGate,
396
+ });
397
+ const managedAuthSessionAdapter =
398
+ deps.managedAuthSessionAdapter ??
399
+ (managedAuth ? createBetterAuthSessionAdapter(managedAuth, deps.db) : null);
400
+ if (managedAuth) startManagedSignInNotificationDelivery(deps.db, managedEmailTransport);
175
401
  const objectStorage =
176
402
  deps.objectStorage === undefined ? createObjectStorage(deps.settings) : deps.objectStorage;
177
403
  let documentServices: DocumentServices | null = deps.documentServices ?? null;
@@ -224,16 +450,6 @@ export function createAppComposition(deps: AppDependencies): {
224
450
  workspaceId,
225
451
  documentId,
226
452
  getDocumentServices(),
227
- {
228
- beforeEmbed: async ({ chunkCount }) => {
229
- await requireLimit(routeDeps, {
230
- accountId,
231
- workspaceId,
232
- action: "document:index",
233
- quantity: chunkCount,
234
- });
235
- },
236
- },
237
453
  { viewerSubjectId: authoritySubjectId },
238
454
  );
239
455
  if (
@@ -252,8 +468,24 @@ export function createAppComposition(deps: AppDependencies): {
252
468
  // concrete for routes; it throws SandboxResumeError when sandboxBackend=none.
253
469
  const sandboxClient = deps.sandboxClient ?? createApiSandboxClient(deps.settings);
254
470
  const resumeBoxById = deps.resumeBoxById ?? makeResumeBoxById(sandboxClient);
255
- const observability =
256
- deps.observability ?? createObservability(deps.settings, { component: "api" });
471
+ if (
472
+ managedAuth &&
473
+ deps.settings.managedAuthSessionSetMode !== "legacy" &&
474
+ !managedAuthReaperDatabases.has(deps.db as object)
475
+ ) {
476
+ managedAuthReaperDatabases.add(deps.db as object);
477
+ const timer = setInterval(() => {
478
+ void Promise.all([
479
+ reapManagedAuthIsolatedSessions(deps.db, 100),
480
+ reapExpiredManagedAuthSessionSets(deps.db, 100),
481
+ ]).catch((error) => {
482
+ observability.error("Managed isolated-auth orphan reap failed", {
483
+ errorClass: error instanceof Error ? error.name : "UnknownError",
484
+ });
485
+ });
486
+ }, MANAGED_AUTH_REAPER_INTERVAL_MS);
487
+ (timer as ReturnType<typeof setInterval> & { unref?: () => void }).unref?.();
488
+ }
257
489
  const transcription =
258
490
  deps.transcription === undefined
259
491
  ? createTranscriptionService({
@@ -268,12 +500,26 @@ export function createAppComposition(deps: AppDependencies): {
268
500
  ffmpegPath: deps.settings.voiceInputFfmpegPath,
269
501
  })
270
502
  : deps.transcriptionSegmenter;
503
+ registerProductUsageMetricBaselines(observability);
504
+ // Server-side presence counts managed people, so it exists only where
505
+ // canonical managed browser sessions exist. Writes are batched off the
506
+ // request path; see packages/core/src/user-presence.ts.
507
+ const userPresence =
508
+ deps.userPresence !== undefined
509
+ ? deps.userPresence
510
+ : deps.settings.productAccessMode === "managed"
511
+ ? createUserPresenceRecorder({ db: deps.db, observability })
512
+ : null;
271
513
  const routeDeps: ApiRouteDeps = {
272
514
  ...deps,
515
+ resolveCatalogSettings: () => resolveCatalogSettings(deps.db, deps.settings),
273
516
  observability,
517
+ userPresence,
274
518
  githubStateSecret:
275
519
  deps.githubStateSecret ?? deps.settings.githubAppManifestStateSecret ?? crypto.randomUUID(),
276
520
  managedAuth,
521
+ managedAuthSessionAdapter,
522
+ managedEmailTransport,
277
523
  objectStorage,
278
524
  documentIndexer,
279
525
  getDocumentServices,
@@ -283,16 +529,107 @@ export function createAppComposition(deps: AppDependencies): {
283
529
  resumeBoxById,
284
530
  };
285
531
  const app = new Hono();
286
- const correlationIds = new WeakMap<Request, string>();
532
+ // Body/auth middleware may replace c.req.raw. The Hono context remains the
533
+ // same for the response, diagnostic and completion log of this request.
534
+ const correlationIds = new WeakMap<Context, string>();
535
+ const loggedHttpFailures = new WeakSet<Context>();
536
+ const logHttpFailure = (c: Context, status: number, code: ErrorCode, error?: unknown) => {
537
+ if (loggedHttpFailures.has(c)) return;
538
+ loggedHttpFailures.add(c);
539
+ const correlationId = correlationIds.get(c) ?? crypto.randomUUID();
540
+ const route = routeLabel(new URL(c.req.url).pathname, registeredHandlerRoutePath(c));
541
+ const input =
542
+ error === undefined
543
+ ? null
544
+ : {
545
+ code: "http_request_failed" as const,
546
+ stage: "http.request" as const,
547
+ error,
548
+ };
549
+ const diagnostic = input ? failureDiagnostic(input, deps.settings.deploymentRevision) : null;
550
+ // Diagnostics and logging are observers, never a new response failure.
551
+ try {
552
+ if (input && diagnostic) {
553
+ observability.recordFailureDiagnostic({ ...input, diagnosticId: diagnostic.diagnosticId });
554
+ }
555
+ } catch {
556
+ /* Preserve the original error response if the diagnostic sink fails. */
557
+ }
558
+ try {
559
+ observability.error("HTTP request failed", {
560
+ method: c.req.method,
561
+ route,
562
+ status,
563
+ correlationId,
564
+ errorCode: code,
565
+ errorClass: "HttpOperationError",
566
+ origin: "api",
567
+ reasonKind: diagnostic?.causes[0]?.kind ?? "Response",
568
+ ...(diagnostic ? { diagnosticId: diagnostic.diagnosticId } : {}),
569
+ });
570
+ observability.incrementCounter({
571
+ name: "opengeni_http_errors_total",
572
+ help: "Total OpenGeni HTTP request failures by bounded route, status, and stable code.",
573
+ labels: { route, status: String(status), code },
574
+ });
575
+ } catch {
576
+ /* Preserve the original error response if the logger fails. */
577
+ }
578
+ };
287
579
 
288
580
  app.use("*", async (c, next) => {
289
581
  const correlationId =
290
582
  boundedCorrelationId(c.req.header(OPENGENI_CORRELATION_HEADER)) ?? crypto.randomUUID();
291
- correlationIds.set(c.req.raw, correlationId);
583
+ correlationIds.set(c, correlationId);
292
584
  c.header(OPENGENI_CORRELATION_HEADER, correlationId);
293
585
  await next();
294
586
  });
295
587
 
588
+ // Unauthenticated local mode: admit only requests addressed to this computer
589
+ // and, when a browser sent them, from this stack's web app (see
590
+ // http/local-browser-boundary.ts). Runs before CORS so a refused preflight
591
+ // carries no CORS grant, and logs each distinct refused Host or Origin once
592
+ // because the browser shows only a generic CORS error. Null outside local
593
+ // development.
594
+ const localBrowserBoundary = createLocalBrowserBoundary(deps.settings, {
595
+ warn: (message, attributes) => observability.warn(message, attributes),
596
+ });
597
+ if (localBrowserBoundary) {
598
+ app.use("*", async (c, next) => {
599
+ const rejection = localBrowserBoundary.rejection(c.req.raw);
600
+ if (rejection) {
601
+ throw new ApiHttpError(rejection.status, {
602
+ code: "forbidden",
603
+ message: rejection.message,
604
+ retryable: false,
605
+ details: { code: rejection.code },
606
+ });
607
+ }
608
+ await next();
609
+ });
610
+ }
611
+
612
+ // Better Auth keys its rate limits and session addresses on a request
613
+ // header. Drop any caller-supplied copy everywhere and stamp the trusted
614
+ // source address on managed-auth routes before any route derives a Better
615
+ // Auth request from this one.
616
+ app.use("*", async (c, next) => {
617
+ replaceTrustedClientAddressHeader(c, deps.settings, c.req.path.startsWith("/v1/auth/"));
618
+ await next();
619
+ });
620
+
621
+ app.use("*", async (c, next) => {
622
+ const oauthToken = mcpOAuthBearerToken(c.req.raw);
623
+ if (
624
+ oauthToken &&
625
+ (!deps.settings.mcpOauthEnabled || !isMcpOAuthResourcePath(new URL(c.req.url).pathname))
626
+ ) {
627
+ c.header("www-authenticate", 'Bearer error="invalid_token"');
628
+ return c.json({ error: "invalid_token" }, 401);
629
+ }
630
+ await next();
631
+ });
632
+
296
633
  const corsHeaders = {
297
634
  allowHeaders: [
298
635
  "Accept",
@@ -301,14 +638,39 @@ export function createAppComposition(deps: AppDependencies): {
301
638
  "Range",
302
639
  "X-OpenGeni-Access-Key",
303
640
  "X-OpenGeni-Api-Contract",
641
+ "X-OpenGeni-Actor-Epoch",
304
642
  "X-OpenGeni-Correlation-Id",
643
+ "X-OpenGeni-Session-Csrf",
644
+ "X-OpenGeni-Site-Id",
645
+ "X-OpenGeni-Site-Version",
305
646
  "X-OpenGeni-Subject",
647
+ "X-OpenGeni-User-Activity",
306
648
  ],
307
649
  exposeHeaders: [
308
650
  "Accept-Ranges",
309
651
  "Content-Range",
652
+ "Deprecation",
653
+ "Link",
654
+ "Sunset",
310
655
  "X-OpenGeni-Api-Contract",
656
+ "X-OpenGeni-Actor-Epoch",
657
+ "X-OpenGeni-Actor-State",
311
658
  "X-OpenGeni-Correlation-Id",
659
+ "X-OpenGeni-Covered-First",
660
+ "X-OpenGeni-Covered-Last",
661
+ "X-OpenGeni-Event-Direction",
662
+ "X-OpenGeni-Event-Mode",
663
+ "X-OpenGeni-Event-Result",
664
+ "X-OpenGeni-Event-Result-Mode",
665
+ "X-OpenGeni-Forensic-Exact",
666
+ "X-OpenGeni-Has-More",
667
+ "X-OpenGeni-Next-After",
668
+ "X-OpenGeni-Next-Before",
669
+ "X-OpenGeni-Page-Bytes",
670
+ "X-OpenGeni-Page-Max-Bytes",
671
+ "X-OpenGeni-Page-Truncated",
672
+ "X-OpenGeni-Payload-Mode",
673
+ "X-OpenGeni-Truncated-By",
312
674
  ],
313
675
  };
314
676
  const publicApiCors = cors({
@@ -323,8 +685,25 @@ export function createAppComposition(deps: AppDependencies): {
323
685
  allowedCorsOrigin(deps.settings.corsAllowOriginRegex, origin) ? origin : null,
324
686
  });
325
687
 
688
+ const localCors = localBrowserBoundary
689
+ ? cors({
690
+ ...corsHeaders,
691
+ credentials: true,
692
+ origin: (origin, c) =>
693
+ localBrowserBoundary.originAllowed(origin, localBrowserRequestHost(c.req.raw))
694
+ ? origin
695
+ : null,
696
+ })
697
+ : null;
698
+
326
699
  app.use("*", (c, next) => {
327
700
  const origin = c.req.header("origin");
701
+ if (localCors) {
702
+ // The boundary above already refused every other origin. Local mode
703
+ // never answers with wildcard CORS: a request without credentials acts
704
+ // as the local user, so any site could otherwise read its responses.
705
+ return origin ? localCors(c, next) : next();
706
+ }
328
707
  const middleware =
329
708
  origin && allowedCorsOrigin(deps.settings.corsAllowOriginRegex, origin)
330
709
  ? credentialedCors
@@ -332,14 +711,32 @@ export function createAppComposition(deps: AppDependencies): {
332
711
  return middleware(c, next);
333
712
  });
334
713
 
335
- app.use(
336
- "*",
337
- bodyLimit({
338
- maxSize: apiRequestBodyLimitBytes(deps.settings),
339
- onError: (c) =>
340
- c.json({ code: "PAYLOAD_TOO_LARGE", message: "Request body is too large." }, 413),
341
- }),
342
- );
714
+ const standardRequestBodyLimit = bodyLimit({
715
+ maxSize: apiRequestBodyLimitBytes(deps.settings),
716
+ onError: (c) =>
717
+ c.json({ code: "PAYLOAD_TOO_LARGE", message: "Request body is too large." }, 413),
718
+ });
719
+ app.use("*", async (c, next) => {
720
+ // Git packfiles must stay streaming and can legitimately exceed the JSON
721
+ // request ceiling. The exact closed broker routes apply their own method,
722
+ // content-type, authority, and idle-deadline checks.
723
+ const pathname = new URL(c.req.url).pathname;
724
+ if (isPersonalGitHubGitBrokerRequest(c.req.method, pathname)) {
725
+ await next();
726
+ return;
727
+ }
728
+ // The anonymous web beacons enforce their own small limits on the
729
+ // streamed body; the generic ceiling would buffer far more first.
730
+ if (
731
+ isClientErrorReportRequest(c.req.method, pathname) ||
732
+ isAnalyticsConsentReportRequest(c.req.method, pathname) ||
733
+ isSessionHistoryImportRequest(c.req.method, pathname)
734
+ ) {
735
+ await next();
736
+ return;
737
+ }
738
+ return await standardRequestBodyLimit(c, next);
739
+ });
343
740
 
344
741
  // Large catalog, capture, and session-list responses are on the browser's
345
742
  // critical path. Compress JSON at the API boundary while leaving SSE and
@@ -355,90 +752,146 @@ export function createAppComposition(deps: AppDependencies): {
355
752
  }
356
753
  });
357
754
 
755
+ // Public-route deprecations (docs/design/api-compatibility-policy.md) are
756
+ // advertised on every response of the affected route, errors included.
757
+ app.use("/v1/*", deprecationHeadersMiddleware());
758
+ // A pinned SDK must not be told about a revision it is built to reject; see
759
+ // the policy's contract-header rule. Registered before the contract fence so
760
+ // it observes the header that fence sets.
761
+ app.use("/v1/*", bearerApiContractHeaderCompatibility());
762
+
358
763
  app.use("*", async (c, next) => {
359
764
  const url = new URL(c.req.url);
360
- const route = routeLabel(url.pathname);
361
- const correlationId = correlationIds.get(c.req.raw) ?? crypto.randomUUID();
765
+ const route = routeLabel(url.pathname, registeredHandlerRoutePath(c));
766
+ const correlationId = correlationIds.get(c) ?? crypto.randomUUID();
362
767
  const start = performance.now();
363
- const span = observability.startSpan(`HTTP ${c.req.method} ${route}`, {
364
- "http.request.method": c.req.method,
365
- "opengeni.route": route,
366
- });
367
- try {
768
+ const span = observability.startSpan(
769
+ `HTTP ${c.req.method} ${route}`,
770
+ {
771
+ "http.request.method": c.req.method,
772
+ "opengeni.route": route,
773
+ },
774
+ { parent: null },
775
+ );
776
+ return await withMcpTelemetry(observability, span.traceId, () =>
777
+ withTraceContext(span, async () => {
778
+ try {
779
+ await next();
780
+ const status = c.res.status || 200;
781
+ // Hono normally handles exceptions in onError before next() resolves.
782
+ // Also cover a handler returning a 5xx without throwing.
783
+ if (status >= 500) logHttpFailure(c, status, errorCodeForStatus(status), c.error);
784
+ const durationSeconds = (performance.now() - start) / 1000;
785
+ observability.recordHttpRequest({
786
+ method: c.req.method,
787
+ route,
788
+ status,
789
+ durationSeconds,
790
+ });
791
+ span.end({
792
+ attributes: {
793
+ "http.response.status_code": status,
794
+ "opengeni.duration_ms": Math.round(durationSeconds * 1000),
795
+ },
796
+ ...(c.error ? { error: c.error } : {}),
797
+ });
798
+ observability.info("HTTP request completed", {
799
+ method: c.req.method,
800
+ route,
801
+ status,
802
+ durationMs: Math.round(durationSeconds * 1000),
803
+ traceId: span.traceId,
804
+ spanId: span.spanId,
805
+ correlationId,
806
+ });
807
+ } catch (error) {
808
+ const status = httpStatusForError(error);
809
+ const errorCode = errorCodeForStatus(status);
810
+ const durationSeconds = (performance.now() - start) / 1000;
811
+ observability.recordHttpRequest({
812
+ method: c.req.method,
813
+ route,
814
+ status,
815
+ durationSeconds,
816
+ });
817
+ span.end({
818
+ attributes: {
819
+ "http.response.status_code": status,
820
+ "opengeni.duration_ms": Math.round(durationSeconds * 1000),
821
+ },
822
+ error,
823
+ });
824
+ logHttpFailure(c, status, errorCode, error);
825
+ throw error;
826
+ }
827
+ }),
828
+ );
829
+ });
830
+
831
+ const accessKeyBoundary = requireAccessKey(deps.settings);
832
+ app.use("*", async (c, next) => {
833
+ // Git's credential helper supplies the exact encrypted broker bearer in
834
+ // Authorization. It cannot also supply the deployment access key, and the
835
+ // route performs its own attempt-bound authorization before every use.
836
+ if (isPersonalGitHubGitBrokerRequest(c.req.method, new URL(c.req.url).pathname)) {
368
837
  await next();
369
- const status = c.res.status || 200;
370
- const durationSeconds = (performance.now() - start) / 1000;
371
- observability.recordHttpRequest({
372
- method: c.req.method,
373
- route,
374
- status,
375
- durationSeconds,
376
- });
377
- span.end({
378
- attributes: {
379
- "http.response.status_code": status,
380
- "opengeni.duration_ms": Math.round(durationSeconds * 1000),
381
- },
382
- });
383
- observability.info("HTTP request completed", {
384
- method: c.req.method,
385
- route,
386
- status,
387
- durationMs: Math.round(durationSeconds * 1000),
388
- traceId: span.traceId,
389
- spanId: span.spanId,
390
- correlationId,
391
- });
392
- } catch (error) {
393
- const status = httpStatusForError(error);
394
- const errorCode = errorCodeForStatus(status);
395
- const durationSeconds = (performance.now() - start) / 1000;
396
- observability.recordHttpRequest({
397
- method: c.req.method,
398
- route,
399
- status,
400
- durationSeconds,
401
- });
402
- observability.incrementCounter({
403
- name: "opengeni_http_errors_total",
404
- help: "Total OpenGeni HTTP request failures by bounded route, status, and stable code.",
405
- labels: { route, status: String(status), code: errorCode },
406
- });
407
- span.end({
408
- attributes: {
409
- "http.response.status_code": status,
410
- "opengeni.duration_ms": Math.round(durationSeconds * 1000),
411
- },
412
- error,
413
- });
414
- observability.error("HTTP request failed", {
415
- method: c.req.method,
416
- route,
417
- status,
418
- durationMs: Math.round(durationSeconds * 1000),
419
- traceId: span.traceId,
420
- spanId: span.spanId,
421
- correlationId,
422
- errorCode,
423
- errorClass: "HttpOperationError",
424
- });
425
- throw error;
838
+ return;
426
839
  }
840
+ return await accessKeyBoundary(c, next);
841
+ });
842
+
843
+ // Malformed JSON in a client request body is a 400 wherever a route reads it.
844
+ app.use("/v1/*", async (c, next) => {
845
+ tagRequestJsonParseErrors(c);
846
+ await next();
427
847
  });
428
848
 
429
- app.use("*", requireAccessKey(deps.settings));
849
+ // A request no registered handler answers is a 404 (or a 405 when the path
850
+ // exists for other methods) before any authentication, authorization, or
851
+ // contract middleware can turn it into a misleading 401/409/503. This runs
852
+ // after the deployment perimeter so an unauthenticated caller of a
853
+ // key-protected deployment learns nothing new.
854
+ app.use("/v1/*", async (c, next) => {
855
+ const unmatched = unmatchedRoute(app, c);
856
+ if (!unmatched) {
857
+ await next();
858
+ return;
859
+ }
860
+ const requestId = correlationIds.get(c) ?? crypto.randomUUID();
861
+ c.header(OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION);
862
+ if (unmatched.status === 405) c.header("allow", unmatched.allow.join(", "));
863
+ return c.json(
864
+ ErrorEnvelope.parse({
865
+ error: {
866
+ status: unmatched.status,
867
+ code: "not_found",
868
+ message:
869
+ unmatched.status === 405
870
+ ? `Method ${c.req.method} is not supported for this resource. Allowed: ${unmatched.allow.join(", ")}.`
871
+ : "Resource not found.",
872
+ retryable: false,
873
+ requestId,
874
+ },
875
+ }),
876
+ unmatched.status,
877
+ );
878
+ });
430
879
 
431
880
  app.use("/v1/*", async (c, next) => {
432
881
  c.header(OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION);
433
882
  if (
434
883
  deps.settings.environment !== "test" &&
435
- isApiContractProtectedMutation(c.req.method, new URL(c.req.url).pathname) &&
436
- c.req.header(OPENGENI_API_CONTRACT_HEADER) !== OPENGENI_API_CONTRACT_REVISION
884
+ apiContractAdmission({
885
+ method: c.req.method,
886
+ pathname: new URL(c.req.url).pathname,
887
+ authorization: c.req.header("authorization"),
888
+ claimedRevision: c.req.header(OPENGENI_API_CONTRACT_HEADER),
889
+ }) === "reject"
437
890
  ) {
438
891
  return c.json(
439
892
  {
440
893
  code: "API_CONTRACT_CHANGED",
441
- message: "OpenGeni updated. Reload this client before changing state.",
894
+ message: "Opengeni updated. Reload this client before changing state.",
442
895
  apiContractRevision: OPENGENI_API_CONTRACT_REVISION,
443
896
  },
444
897
  409,
@@ -447,19 +900,255 @@ export function createAppComposition(deps: AppDependencies): {
447
900
  await next();
448
901
  });
449
902
 
903
+ app.use("/v1/*", async (c, next) => {
904
+ try {
905
+ await next();
906
+ try {
907
+ await validateManagedAuthRequestActorLease(c.req.raw);
908
+ } catch (error) {
909
+ if (error instanceof ManagedAuthActorChangeError) {
910
+ c.header("x-opengeni-actor-state", "changed");
911
+ throw new HTTPException(409, { message: error.code, cause: error });
912
+ }
913
+ if (error instanceof ManagedAuthActorLeaseOutcomeUnknownError) {
914
+ throw new ApiHttpError(503, {
915
+ code: "upstream_unavailable",
916
+ message: error.code,
917
+ retryable: true,
918
+ outcomeUnknown: true,
919
+ details: { managedAuthCode: error.code },
920
+ });
921
+ }
922
+ throw error;
923
+ }
924
+ } finally {
925
+ await releaseManagedAuthRequestActorLease(c.req.raw).catch((error) => {
926
+ observability.error("Managed actor mutation lease release failed", {
927
+ errorClass: error instanceof Error ? error.name : "UnknownError",
928
+ });
929
+ });
930
+ }
931
+ });
932
+
933
+ // These product-owned auth routes must be registered before Better Auth's
934
+ // wildcard handler or the provider returns its own 404 first.
935
+ registerManagedOnboardingRoutes(app, routeDeps);
936
+ registerManagedAuthSessionSetRoutes(app, routeDeps);
937
+ registerManagedSignInMethodRoutes(app, routeDeps);
450
938
  if (managedAuth) {
451
- app.on(["GET", "POST"], "/v1/auth/*", (c) => managedAuth.handler(c.req.raw));
939
+ app.on(["GET", "POST"], "/v1/auth/*", async (c) => {
940
+ const pathname = new URL(c.req.url).pathname;
941
+ const oauthCallbackProvider = managedAuthOAuthCallbackProvider(pathname);
942
+ if (
943
+ pathname === "/v1/auth/sign-up/email" &&
944
+ c.req.method === "POST" &&
945
+ !(await managedAuthNewSignupsGate.signupsOpen())
946
+ ) {
947
+ // Launch-load safety switch: refuse before Better Auth hashes the
948
+ // password or sends mail. Better Auth's own disableSignUp flags and
949
+ // the user-create hook remain the backstop for every other path.
950
+ return managedAuthNewSignupsPausedResponse();
951
+ }
952
+ if (pathname === "/v1/auth/sign-in/social" && c.req.method === "POST") {
953
+ const body = await c.req.raw
954
+ .clone()
955
+ .json()
956
+ .catch(() => null);
957
+ if (
958
+ !body ||
959
+ !["google", "github"].includes(body.provider) ||
960
+ Object.prototype.hasOwnProperty.call(body, "idToken")
961
+ ) {
962
+ return c.json(
963
+ {
964
+ code: "SIGN_IN_METHOD_OAUTH_REDIRECT_REQUIRED",
965
+ message: "Use the browser OAuth sign-in redirect",
966
+ },
967
+ 403,
968
+ );
969
+ }
970
+ }
971
+ if (oauthCallbackProvider) {
972
+ const connectResponse = await handleManagedSignInConnectCallback(
973
+ c,
974
+ routeDeps,
975
+ oauthCallbackProvider,
976
+ );
977
+ if (connectResponse) return connectResponse;
978
+ }
979
+ if (
980
+ new Set([
981
+ "link-social",
982
+ "unlink-account",
983
+ "list-accounts",
984
+ "set-password",
985
+ "change-password",
986
+ "change-email",
987
+ "update-user",
988
+ "delete-user",
989
+ "get-access-token",
990
+ "refresh-token",
991
+ "account-info",
992
+ ]).has(pathname.slice("/v1/auth/".length))
993
+ ) {
994
+ return c.json(
995
+ {
996
+ code: "SIGN_IN_METHOD_PRODUCT_ROUTE_REQUIRED",
997
+ message: "Use personal sign-in method settings",
998
+ },
999
+ 403,
1000
+ );
1001
+ }
1002
+ if (deps.settings.managedAuthSessionSetMode === "legacy") {
1003
+ return oauthCallbackProvider
1004
+ ? await runManagedAuthProvider(
1005
+ oauthCallbackProvider,
1006
+ async () => await managedAuth.handler(c.req.raw),
1007
+ )
1008
+ : await managedAuth.handler(c.req.raw);
1009
+ }
1010
+ requireManagedAuthProviderRouteAllowed(c.req.method, pathname);
1011
+ // Provider authentication/recovery is isolated from whichever actor the
1012
+ // browser currently renders. Selected-session capabilities are all
1013
+ // product-owned above this wildcard and generation/epoch fenced.
1014
+ const authority = getCookie(c, MANAGED_AUTH_SESSION_SET_COOKIE);
1015
+ let providerResponse: Response;
1016
+ let preserveCookieNames: readonly string[] | undefined;
1017
+ if (oauthCallbackProvider) {
1018
+ const attempt = await resolveManagedAuthOAuthAttempt(
1019
+ managedAuth,
1020
+ c.req.raw,
1021
+ oauthCallbackProvider,
1022
+ deps.settings.publicBaseUrl!,
1023
+ );
1024
+ if (!attempt || !authority || attempt.authorityHash !== managedAuthSha256(authority)) {
1025
+ throw new HTTPException(409, { message: "provider_route_blocked" });
1026
+ }
1027
+ const isolated = await isolatedManagedAuthOAuthCallbackRequest(managedAuth, c.req.raw);
1028
+ preserveCookieNames = [isolated.stateCookieName];
1029
+ const handled = await runManagedAuthAttempt(
1030
+ attempt.transactionId,
1031
+ oauthCallbackProvider,
1032
+ async () => {
1033
+ const response = await managedAuth.handler(isolated.request);
1034
+ return {
1035
+ response,
1036
+ authSessionId: currentManagedAuthCreatedSessionId(),
1037
+ };
1038
+ },
1039
+ );
1040
+ providerResponse = handled.response;
1041
+ if (
1042
+ !handled.authSessionId &&
1043
+ managedAuthOAuthReturnMatches(
1044
+ providerResponse.headers.get("location") ?? "",
1045
+ new URL(deps.settings.publicBaseUrl!).origin,
1046
+ attempt.transactionId,
1047
+ "complete",
1048
+ )
1049
+ ) {
1050
+ const location = new URL("/account-auth", deps.settings.publicBaseUrl!);
1051
+ location.searchParams.set("transaction", attempt.transactionId);
1052
+ location.searchParams.set("social", "error");
1053
+ providerResponse = Response.redirect(location, 302);
1054
+ } else if (handled.authSessionId) {
1055
+ try {
1056
+ await adoptManagedAuthSession({
1057
+ db: deps.db,
1058
+ adapter: managedAuthSessionAdapter!,
1059
+ authority,
1060
+ authorityHash: attempt.authorityHash,
1061
+ csrfHash: managedAuthCsrfHash(authority),
1062
+ operationId: managedAuthDerivedUuid(
1063
+ "opengeni:managed-auth:social-completion",
1064
+ `${attempt.transactionId}:${attempt.provider}`,
1065
+ ),
1066
+ requestDigest: managedAuthSecretRequestDigest(deps.settings.betterAuthSecret!, {
1067
+ operation: "social_completion",
1068
+ transactionId: attempt.transactionId,
1069
+ provider: attempt.provider,
1070
+ expectedGeneration: attempt.expectedGeneration,
1071
+ expectedActorEpoch: attempt.expectedActorEpoch,
1072
+ }),
1073
+ expectedGeneration: attempt.expectedGeneration,
1074
+ expectedActorEpoch: attempt.expectedActorEpoch,
1075
+ transactionId: attempt.transactionId,
1076
+ transactionSecretHash: attempt.transactionSecretHash,
1077
+ authSessionId: handled.authSessionId,
1078
+ mode: deps.settings.managedAuthSessionSetMode,
1079
+ });
1080
+ } catch {
1081
+ // adoptManagedAuthSession revokes only when durable reconciliation
1082
+ // proves that completion did not commit. An uncertain outcome may
1083
+ // already point the selected slot at this session, so the callback
1084
+ // must not independently revoke it.
1085
+ const location = new URL("/account-auth", deps.settings.publicBaseUrl!);
1086
+ location.searchParams.set("transaction", attempt.transactionId);
1087
+ location.searchParams.set("social", "error");
1088
+ providerResponse = Response.redirect(location, 302);
1089
+ }
1090
+ }
1091
+ } else {
1092
+ const headers = new Headers(c.req.raw.headers);
1093
+ headers.delete("cookie");
1094
+ headers.delete("authorization");
1095
+ headers.delete("x-forwarded-user");
1096
+ const providerRequest = new Request(c.req.raw, { headers });
1097
+ const discardProviderSession =
1098
+ deps.settings.managedAuthSessionSetMode === "broker" || authority !== undefined;
1099
+ providerResponse = discardProviderSession
1100
+ ? await runManagedAuthDiscardedProviderSession(
1101
+ async () => await managedAuth.handler(providerRequest),
1102
+ )
1103
+ : await managedAuth.handler(providerRequest);
1104
+ }
1105
+ let replacementCookies: readonly string[] | undefined;
1106
+ if (deps.settings.managedAuthSessionSetMode === "broker") {
1107
+ replacementCookies = await managedAuthSessionAdapter!.createLegacySelectedSessionCookies(
1108
+ null,
1109
+ c.req.header("cookie") ?? null,
1110
+ );
1111
+ } else {
1112
+ if (authority !== undefined) {
1113
+ const snapshot = await getManagedAuthSessionSetSnapshot(deps.db, {
1114
+ authorityHash: managedAuthSha256(authority),
1115
+ mode: "dual",
1116
+ includeInternal: true,
1117
+ readOnly: true,
1118
+ });
1119
+ if (snapshot) {
1120
+ replacementCookies =
1121
+ await managedAuthSessionAdapter!.createLegacySelectedSessionCookies(
1122
+ snapshot.selected,
1123
+ c.req.header("cookie") ?? null,
1124
+ );
1125
+ } else {
1126
+ replacementCookies =
1127
+ await managedAuthSessionAdapter!.createLegacySelectedSessionCookies(
1128
+ null,
1129
+ c.req.header("cookie") ?? null,
1130
+ );
1131
+ }
1132
+ }
1133
+ }
1134
+ return await scrubManagedAuthProviderResponse(providerResponse, {
1135
+ replacementCookies,
1136
+ preserveCookieNames,
1137
+ });
1138
+ });
452
1139
  }
453
1140
 
454
- app.get("/healthz", (c) =>
455
- c.json({
1141
+ app.get("/healthz", (c) => {
1142
+ const warnings = githubAppBotIdentityWarnings(deps.settings);
1143
+ return c.json({
456
1144
  service: deps.settings.serviceName,
457
1145
  environment: deps.settings.environment,
458
1146
  deploymentRevision: deps.settings.deploymentRevision,
459
1147
  ...(deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {}),
1148
+ ...(warnings.length > 0 ? { warnings } : {}),
460
1149
  ok: true,
461
- }),
462
- );
1150
+ });
1151
+ });
463
1152
 
464
1153
  app.get("/readyz", async (c) => {
465
1154
  const result = await runReadinessChecks(readinessChecks(deps), 2_000);
@@ -472,44 +1161,146 @@ export function createAppComposition(deps: AppDependencies): {
472
1161
  return c.json(result, result.ok ? 200 : 503);
473
1162
  });
474
1163
 
475
- app.get("/metrics", async (c) =>
476
- c.text(await observability.prometheusMetrics(), 200, {
477
- "content-type": "text/plain; version=0.0.4; charset=utf-8",
478
- }),
479
- );
1164
+ if (publicListenerServesMetrics(deps.settings)) {
1165
+ registerPrometheusMetricsRoute(app, observability);
1166
+ }
1167
+
1168
+ registerMcpOAuthRoutes(app, routeDeps);
1169
+
1170
+ registerClientErrorRoutes(app, { observability, settings: deps.settings });
1171
+ registerAnalyticsConsentRoutes(app, { observability, settings: deps.settings });
480
1172
 
481
1173
  app.get("/v1/config/client", async (c) => {
482
1174
  c.header("cache-control", "no-store");
483
- const codexCatalogSettings = deps.settings.codexSubscriptionEnabled
484
- ? withCodexCatalogProvider(deps.settings)
485
- : deps.settings;
486
- const catalogSettings = deps.settings.supergrokSubscriptionEnabled
487
- ? withXaiSubscriptionCatalogProvider(codexCatalogSettings)
488
- : codexCatalogSettings;
1175
+ const resolvedCatalog = await resolveCatalogSettings(deps.db, deps.settings);
1176
+ let catalogSettings = resolvedCatalog.settings;
1177
+ let selections = resolveWorkspaceModelSelection({
1178
+ settings: catalogSettings,
1179
+ policy: null,
1180
+ codexSubscriptionActive: false,
1181
+ });
1182
+ let defaultSelection = selectDefaultSessionModel({
1183
+ settings: catalogSettings,
1184
+ selections,
1185
+ workspaceDefaults: null,
1186
+ creditsAvailable: false,
1187
+ });
1188
+ let modelSelectionForbidden = false;
1189
+ const requestedWorkspaceId = c.req.query("workspaceId");
1190
+ if (requestedWorkspaceId !== undefined && requestedWorkspaceId.trim() === "") {
1191
+ throw new HTTPException(422, { message: "workspaceId must not be empty" });
1192
+ }
1193
+ const callerScoped =
1194
+ requestedWorkspaceId !== undefined ||
1195
+ deps.settings.productAccessMode !== "managed" ||
1196
+ c.req.header("authorization") !== undefined ||
1197
+ // Browser bootstrap precedes session-set reconciliation. A cookie alone
1198
+ // cannot scope this public read (broker/changed actors need an epoch).
1199
+ c.req.header("x-opengeni-actor-epoch") !== undefined ||
1200
+ c.req.header("x-opengeni-external-actor") !== undefined;
1201
+ if (callerScoped) {
1202
+ let context: Awaited<ReturnType<typeof requireAccessContext>> | null = null;
1203
+ try {
1204
+ context = await requireAccessContext(c, routeDeps);
1205
+ } catch (error) {
1206
+ // A stale browser cookie must not break the signed-out bootstrap.
1207
+ // Explicit workspace/bearer/external-actor requests fail closed.
1208
+ if (
1209
+ !(error instanceof HTTPException && error.status === 401) ||
1210
+ requestedWorkspaceId !== undefined ||
1211
+ c.req.header("authorization") !== undefined ||
1212
+ c.req.header("x-opengeni-actor-epoch") !== undefined ||
1213
+ c.req.header("x-opengeni-external-actor") !== undefined
1214
+ ) {
1215
+ throw error;
1216
+ }
1217
+ }
1218
+ const workspaceId = requestedWorkspaceId ?? context?.defaultWorkspaceId;
1219
+ if (context && workspaceId) {
1220
+ const grant = await requireAccessGrant(c, routeDeps, workspaceId);
1221
+ await withAccessGrantSessionRlsContext(routeDeps, grant, async () => {
1222
+ catalogSettings = (
1223
+ await resolveWorkspaceCatalogSettings(deps.db, deps.settings, {
1224
+ accountId: grant.accountId,
1225
+ workspaceId,
1226
+ })
1227
+ ).settings;
1228
+ selections = await resolveCallerWorkspaceModelSelections(
1229
+ deps.db,
1230
+ catalogSettings,
1231
+ { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
1232
+ { observeAvailability: true },
1233
+ );
1234
+ if (!hasPermission(grant.permissions, "sessions:create")) {
1235
+ modelSelectionForbidden = true;
1236
+ selections = [];
1237
+ }
1238
+ const workspace = await getWorkspace(deps.db, workspaceId);
1239
+ defaultSelection = await resolveDefaultSessionModelForSelections(deps.db, {
1240
+ settings: catalogSettings,
1241
+ accountId: grant.accountId,
1242
+ workspaceSettings: workspace?.settings ?? {},
1243
+ selections,
1244
+ });
1245
+ });
1246
+ } else if (context) {
1247
+ // An organization key has no implicit workspace. Do not advertise
1248
+ // another workspace's authority; it can request an exact workspace.
1249
+ selections = [];
1250
+ modelSelectionForbidden = true;
1251
+ }
1252
+ }
1253
+ const models = selections.filter(isWorkspaceModelAdmissible);
1254
+ const defaultModel = defaultSelection.model;
1255
+ const fallbackDefinition = selections.find(
1256
+ ({ model }) => model.id === defaultModel || model.aliases.includes(defaultModel),
1257
+ );
1258
+ const legacyModelFallback =
1259
+ models.length === 0
1260
+ ? {
1261
+ id: defaultModel,
1262
+ availability: {
1263
+ status: "unavailable" as const,
1264
+ selectable: false as const,
1265
+ reason: modelSelectionForbidden
1266
+ ? ("policy_blocked" as const)
1267
+ : (fallbackDefinition?.availability.reason ?? ("unsupported" as const)),
1268
+ checkedAt: fallbackDefinition?.availability.checkedAt ?? null,
1269
+ },
1270
+ }
1271
+ : undefined;
489
1272
  return c.json(
490
1273
  ClientConfig.parse({
491
1274
  deploymentRevision: deps.settings.deploymentRevision,
1275
+ claudeSubscriptionEnabled: deps.settings.claudeSubscriptionEnabled,
492
1276
  apiContractRevision: OPENGENI_API_CONTRACT_REVISION,
493
1277
  ...(deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {}),
494
- defaultModel: canonicalizeConfiguredModelId(catalogSettings, catalogSettings.openaiModel),
495
- allowedModels: configuredAllowedModels(catalogSettings),
496
- // Provider-grouped model list for the picker. configuredModels() carries the
497
- // union of the built-in allow-list and every registry provider's models, in
498
- // selection order (default model first); project each to the client-safe
499
- // provider-blind ClientModel shape (execution topology remains server-side).
500
- models: configuredModels(catalogSettings).map(projectClientModel),
501
- defaultReasoningEffort: deps.settings.openaiReasoningEffort,
1278
+ defaultModel,
1279
+ allowedModels: models.length > 0 ? models.map(({ model }) => model.id) : [defaultModel],
1280
+ ...(legacyModelFallback ? { legacyModelFallback } : {}),
1281
+ // Availability remains an observation hint, not the admission predicate.
1282
+ models: models.map(({ model, availability }) => ({
1283
+ ...projectClientModel(model),
1284
+ availability,
1285
+ })),
1286
+ defaultReasoningEffort: defaultSelection.reasoningEffort,
502
1287
  allowedReasoningEfforts: configuredAllowedReasoningEfforts(deps.settings),
1288
+ defaultSandboxBackend: deps.settings.sandboxBackend,
503
1289
  mcpServers: deps.settings.mcpServers.map((server) => ({
504
1290
  id: server.id,
505
1291
  name: server.name ?? server.id,
506
1292
  })),
507
1293
  firstPartyMcpTools: resolveFirstPartyMcpToolPolicy(deps.settings),
1294
+ codeSearch: codeSearchDeploymentPolicy(deps.settings),
1295
+ agentConfig: clientAgentConfig(deps.settings),
508
1296
  fileUploads: {
509
1297
  enabled: objectStorage !== null,
510
1298
  maxSizeBytes: objectStorage?.maxSinglePutSizeBytes ?? 5_000_000_000,
511
1299
  },
512
1300
  voiceInput: {
1301
+ providers: resolveVoiceInputProviderRegistry(deps.settings).map(
1302
+ (provider) => provider.id,
1303
+ ),
513
1304
  available: (await transcription?.available()) ?? false,
514
1305
  maxDurationSeconds: deps.settings.voiceInputMaxDurationSeconds,
515
1306
  maxSizeBytes: deps.settings.voiceInputMaxSizeBytes,
@@ -534,8 +1325,18 @@ export function createAppComposition(deps: AppDependencies): {
534
1325
  : {}),
535
1326
  },
536
1327
  productAccessMode: deps.settings.productAccessMode,
537
- auth: clientAuthConfig(deps.settings),
1328
+ billingMode: deps.settings.billingMode,
1329
+ managedAuthSessionSetMode: deps.settings.managedAuthSessionSetMode,
1330
+ auth: clientAuthConfig(
1331
+ deps.settings,
1332
+ deps.settings.productAccessMode === "managed"
1333
+ ? await managedAuthNewSignupsGate.signupsOpen()
1334
+ : true,
1335
+ ),
1336
+ documentationUrl: deps.settings.documentationUrl,
538
1337
  analytics: clientAnalyticsConfig(deps.settings),
1338
+ legal: clientLegalConfig(deps.settings),
1339
+ ...(deps.settings.supportEmail ? { supportEmail: deps.settings.supportEmail } : {}),
539
1340
  // Channel-A structured services (P4.4) ride exec/readFile/createEditor,
540
1341
  // available on every real backend; `none` has no box so they are all off.
541
1342
  // Per-session availability is still negotiated on /stream-capabilities.
@@ -544,6 +1345,67 @@ export function createAppComposition(deps: AppDependencies): {
544
1345
  );
545
1346
  });
546
1347
 
1348
+ // The organization MCP server: every public action, as a person who signed
1349
+ // in (capped by their connection's access setting) or as an organization
1350
+ // API key. Each action runs the real route in this process.
1351
+ app.all(ORGANIZATION_MCP_PATH, async (c) => {
1352
+ let boundedRequest: Request;
1353
+ try {
1354
+ boundedRequest = await boundedMcpRequest(c.req.raw);
1355
+ } catch (error) {
1356
+ if (error instanceof McpPayloadTooLargeError) {
1357
+ throw new HTTPException(413, { message: "MCP request body exceeds the safety limit" });
1358
+ }
1359
+ throw error;
1360
+ }
1361
+ const challenge = () => {
1362
+ if (deps.settings.mcpOauthEnabled) {
1363
+ c.header("www-authenticate", mcpOAuthAuthenticateHeader(routeDeps, ORGANIZATION_MCP_PATH));
1364
+ }
1365
+ };
1366
+ let caller: OrganizationMcpCaller;
1367
+ try {
1368
+ const connection = await resolveOrganizationMcpOAuthAccess(routeDeps, c.req.raw);
1369
+ if (connection) {
1370
+ caller = {
1371
+ kind: "person",
1372
+ accountId: connection.accountId,
1373
+ subjectId: connection.subjectId,
1374
+ access: connection.organizationAccess,
1375
+ };
1376
+ } else {
1377
+ const context = await requireAccessContext(c, routeDeps);
1378
+ const authorization = c.req.header("authorization");
1379
+ if (!accountScopedApiKeyWorkspaceAuthority(context) || !authorization) {
1380
+ throw new HTTPException(403, {
1381
+ message: "Connect with Opengeni sign-in or an organization API key.",
1382
+ });
1383
+ }
1384
+ caller = {
1385
+ kind: "key",
1386
+ authorization,
1387
+ accessKey: c.req.header("x-opengeni-access-key") ?? null,
1388
+ };
1389
+ }
1390
+ } catch (error) {
1391
+ if (error instanceof HTTPException && error.status === 401) challenge();
1392
+ throw error;
1393
+ }
1394
+ const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
1395
+ const mcp = buildOrganizationMcpServer({
1396
+ caller,
1397
+ origin: new URL(c.req.url).origin,
1398
+ dispatch: async (request) => await app.fetch(request, c.env),
1399
+ signal: c.req.raw.signal,
1400
+ });
1401
+ try {
1402
+ await mcp.connect(transport);
1403
+ return await handleMcpRequestWithClientAbort(transport, boundedRequest, c.req.raw.signal);
1404
+ } finally {
1405
+ await mcp.close().catch(() => undefined);
1406
+ }
1407
+ });
1408
+
547
1409
  app.use("/v1/workspaces/:workspaceId/*", async (c, next) => {
548
1410
  const workspaceId = c.req.param("workspaceId");
549
1411
  if (workspaceRequestRequiresCodexAccountPrevalidation(c.req.raw)) {
@@ -562,6 +1424,27 @@ export function createAppComposition(deps: AppDependencies): {
562
1424
 
563
1425
  app.all("/v1/workspaces/:workspaceId/mcp", async (c) => {
564
1426
  const workspaceId = c.req.param("workspaceId");
1427
+ // This endpoint uses a fresh stateless JSON-response transport per POST;
1428
+ // there is no persistent server-to-client stream. Refuse GET only after
1429
+ // the existing OAuth/workspace/session authorization, before tool setup.
1430
+ const unsupportedGet = () => {
1431
+ const version = c.req.header("mcp-protocol-version");
1432
+ if (version && !SUPPORTED_PROTOCOL_VERSIONS.includes(version)) {
1433
+ return c.json(
1434
+ {
1435
+ jsonrpc: "2.0",
1436
+ id: null,
1437
+ error: { code: -32000, message: "Unsupported protocol version." },
1438
+ },
1439
+ 400,
1440
+ );
1441
+ }
1442
+ return c.json(
1443
+ { jsonrpc: "2.0", id: null, error: { code: -32000, message: "Method not allowed." } },
1444
+ 405,
1445
+ { allow: "POST" },
1446
+ );
1447
+ };
565
1448
  let boundedRequest: Request;
566
1449
  try {
567
1450
  boundedRequest = await boundedMcpRequest(c.req.raw);
@@ -573,42 +1456,253 @@ export function createAppComposition(deps: AppDependencies): {
573
1456
  }
574
1457
  throw error;
575
1458
  }
576
- const grant = await requireMcpAccessGrant(c, routeDeps, workspaceId);
577
- return await withAccessGrantSessionRlsContext(routeDeps, grant, async () => {
578
- const boundSessionId = grant.metadata?.sessionId;
579
- if (typeof boundSessionId === "string") {
1459
+ let oauthAccess: Awaited<ReturnType<typeof resolveMcpOAuthRouteAccess>>;
1460
+ try {
1461
+ oauthAccess = await resolveMcpOAuthRouteAccess(routeDeps, c.req.raw, workspaceId);
1462
+ } catch (error) {
1463
+ if (error instanceof HTTPException && error.status === 401) {
1464
+ c.header(
1465
+ "www-authenticate",
1466
+ mcpOAuthAuthenticateHeader(routeDeps, new URL(c.req.url).pathname),
1467
+ );
1468
+ }
1469
+ throw error;
1470
+ }
1471
+ if (oauthAccess) {
1472
+ return await withAccessGrantSessionRlsContext(routeDeps, oauthAccess.grant, async () => {
1473
+ if (c.req.method === "GET") return unsupportedGet();
1474
+ const prepared = await prepareMcpOAuthWorkspaceToolGateway(
1475
+ routeDeps,
1476
+ oauthAccess.grant,
1477
+ oauthAccess.allowedToolIdentities,
1478
+ );
1479
+ const transport = new WebStandardStreamableHTTPServerTransport({
1480
+ enableJsonResponse: true,
1481
+ });
1482
+ const mcp = buildWorkspaceToolGatewayMcpServer(
1483
+ prepared,
1484
+ oauthAccess.grant,
1485
+ routeDeps.observability,
1486
+ );
580
1487
  try {
581
- await requireSessionAuthorization(routeDeps, grant, {
582
- sessionId: boundSessionId,
583
- operation: "session.first_party_mcp.call",
584
- surface: "first_party_mcp",
585
- });
586
- } catch (error) {
587
- if (error instanceof SessionAuthorizationDeniedError) {
588
- throw new HTTPException(404, { message: "session not found" });
589
- }
590
- if (error instanceof SessionAuthorizationUnavailableError) {
591
- throw new HTTPException(503, {
592
- message: "session authorization is unavailable",
1488
+ await mcp.connect(transport);
1489
+ return await handleMcpRequestWithClientAbort(transport, boundedRequest, c.req.raw.signal);
1490
+ } finally {
1491
+ await Promise.allSettled([mcp.close(), prepared.close()]);
1492
+ }
1493
+ });
1494
+ }
1495
+ let authorization: Awaited<ReturnType<typeof requireMcpAccessGrantAuthorization>>;
1496
+ try {
1497
+ authorization = await requireMcpAccessGrantAuthorization(c, routeDeps, workspaceId);
1498
+ } catch (error) {
1499
+ if (deps.settings.mcpOauthEnabled && error instanceof HTTPException && error.status === 401) {
1500
+ c.header(
1501
+ "www-authenticate",
1502
+ mcpOAuthAuthenticateHeader(routeDeps, new URL(c.req.url).pathname),
1503
+ );
1504
+ }
1505
+ throw error;
1506
+ }
1507
+ const grant = authorization.grant;
1508
+ // The agent-attempt context, this route check, and a tool's own entry check
1509
+ // re-read the same caller session and attempt; share those reads.
1510
+ return await withSessionAuthorizationReadReuse((reads) =>
1511
+ withAccessGrantSessionRlsContext(routeDeps, grant, async () => {
1512
+ const boundSessionId = grant.metadata?.sessionId;
1513
+ if (typeof boundSessionId === "string") {
1514
+ try {
1515
+ await requireSessionAuthorization(routeDeps, grant, {
1516
+ sessionId: boundSessionId,
1517
+ operation: "session.first_party_mcp.call",
1518
+ surface: "first_party_mcp",
593
1519
  });
1520
+ } catch (error) {
1521
+ if (error instanceof SessionAuthorizationDeniedError) {
1522
+ throw new HTTPException(404, { message: "session not found" });
1523
+ }
1524
+ if (error instanceof SessionAuthorizationUnavailableError) {
1525
+ throw new HTTPException(503, {
1526
+ message: "session authorization is unavailable",
1527
+ });
1528
+ }
1529
+ throw error;
594
1530
  }
595
- throw error;
596
1531
  }
597
- }
598
- const workspace = await getWorkspace(routeDeps.db, workspaceId);
599
- const workspaceMemoryEnabled = resolveWorkspaceMemoryEnabled(workspace?.settings);
600
- const workspaceMemoryPromptMode = resolveWorkspaceMemoryPromptMode(workspace?.settings);
601
- const transport = new WebStandardStreamableHTTPServerTransport({
602
- enableJsonResponse: true,
603
- });
604
- const mcp = buildOpenGeniMcpServer(routeDeps, grant, {
605
- requestOrigin: new URL(c.req.url).origin,
606
- workspaceMemoryEnabled,
607
- workspaceMemoryPromptMode,
1532
+ if (c.req.method === "GET") return unsupportedGet();
1533
+ reads.handOffToToolDispatch();
1534
+ const workspace = await getWorkspace(routeDeps.db, workspaceId);
1535
+ const workspaceMemoryEnabled = resolveWorkspaceMemoryEnabled(workspace?.settings);
1536
+ const workspaceMemoryPromptMode = resolveWorkspaceMemoryPromptMode();
1537
+ const transport = new WebStandardStreamableHTTPServerTransport({
1538
+ enableJsonResponse: true,
1539
+ });
1540
+ if (!grantUsesAttemptScopedMcp(grant)) {
1541
+ const prepared = await prepareWorkspaceToolGateway(routeDeps, authorization);
1542
+ const mcp = buildWorkspaceToolGatewayMcpServer(prepared, grant, routeDeps.observability);
1543
+ try {
1544
+ await mcp.connect(transport);
1545
+ return await handleMcpRequestWithClientAbort(
1546
+ transport,
1547
+ boundedRequest,
1548
+ c.req.raw.signal,
1549
+ );
1550
+ } finally {
1551
+ await Promise.allSettled([mcp.close(), prepared.close()]);
1552
+ }
1553
+ }
1554
+ const mcpDeps = await resolveWorkspaceMcpRouteDeps(routeDeps, grant);
1555
+ // The bound session's frozen Memory selector (migration 0427) decides
1556
+ // which Memory tools the attempt receives and which typed layers they
1557
+ // read and write. A missing row resolves to no Memory tools.
1558
+ const sessionMemory =
1559
+ typeof boundSessionId === "string"
1560
+ ? ((await resolveSessionMemoryAgentScope(
1561
+ routeDeps.db,
1562
+ workspaceId,
1563
+ boundSessionId,
1564
+ grant.metadata,
1565
+ )) ?? {
1566
+ mode: "off" as const,
1567
+ userSubjectId: null,
1568
+ rootSessionId: null,
1569
+ })
1570
+ : null;
1571
+ const mcp = buildOpenGeniMcpServer(mcpDeps, grant, {
1572
+ requestOrigin: new URL(c.req.url).origin,
1573
+ workspaceMemoryEnabled,
1574
+ workspaceMemoryPromptMode,
1575
+ sessionMemory,
1576
+ });
1577
+ // Bind tool handlers' `extra.signal` to the HTTP client's connection: a
1578
+ // worker that drops the call (Steer/Pause) aborts a blocking tool here.
1579
+ // Each POST builds a fresh server; close it once the JSON response is
1580
+ // ready, like the gateway paths above, so it can't outlive the request.
1581
+ try {
1582
+ await mcp.connect(transport);
1583
+ return await handleMcpRequestWithClientAbort(transport, boundedRequest, c.req.raw.signal);
1584
+ } finally {
1585
+ await mcp.close().catch(() => undefined);
1586
+ }
1587
+ }),
1588
+ );
1589
+ });
1590
+
1591
+ app.get("/v1/workspaces/:workspaceId/tools/catalog", async (c) => {
1592
+ const workspaceId = c.req.param("workspaceId");
1593
+ const authorization = await requireAccessGrantAuthorization(
1594
+ c,
1595
+ routeDeps,
1596
+ workspaceId,
1597
+ "workspace:read",
1598
+ );
1599
+ const prepared = await prepareWorkspaceToolGateway(routeDeps, authorization);
1600
+ try {
1601
+ c.header("cache-control", "no-store");
1602
+ return c.json(prepared.toolGatewayCatalog);
1603
+ } finally {
1604
+ await prepared.close();
1605
+ }
1606
+ });
1607
+
1608
+ app.post("/v1/workspaces/:workspaceId/tools/calls", async (c) => {
1609
+ const workspaceId = c.req.param("workspaceId");
1610
+ const authorization = await requireAccessGrantAuthorization(
1611
+ c,
1612
+ routeDeps,
1613
+ workspaceId,
1614
+ "workspace:read",
1615
+ );
1616
+ const grant = authorization.grant;
1617
+ const parsed = ToolGatewayCallRequest.safeParse(await c.req.json().catch(() => null));
1618
+ if (!parsed.success) {
1619
+ throw new HTTPException(400, { message: "Invalid tool gateway call" });
1620
+ }
1621
+ const prepared = await prepareWorkspaceToolGateway(routeDeps, authorization);
1622
+ try {
1623
+ return c.json(
1624
+ await callWorkspaceToolGateway(
1625
+ prepared,
1626
+ grant,
1627
+ parsed.data,
1628
+ routeDeps.db,
1629
+ undefined,
1630
+ routeDeps.observability,
1631
+ ),
1632
+ );
1633
+ } finally {
1634
+ await prepared.close();
1635
+ }
1636
+ });
1637
+
1638
+ app.post("/v1/workspaces/:workspaceId/tools/approvals", async (c) => {
1639
+ const workspaceId = c.req.param("workspaceId");
1640
+ const authorization = await requireAccessGrantAuthorization(
1641
+ c,
1642
+ routeDeps,
1643
+ workspaceId,
1644
+ "workspace:read",
1645
+ );
1646
+ const parsed = ToolGatewayApprovalRequest.safeParse(await c.req.json().catch(() => null));
1647
+ if (!parsed.success) {
1648
+ throw new HTTPException(400, { message: "Invalid tool gateway approval" });
1649
+ }
1650
+ const prepared = await prepareWorkspaceToolGateway(routeDeps, authorization);
1651
+ try {
1652
+ return c.json(
1653
+ await approveWorkspaceToolGatewayCall(
1654
+ prepared,
1655
+ authorization.grant,
1656
+ routeDeps.db,
1657
+ parsed.data,
1658
+ undefined,
1659
+ routeDeps.observability,
1660
+ ),
1661
+ 201,
1662
+ );
1663
+ } finally {
1664
+ await prepared.close();
1665
+ }
1666
+ });
1667
+
1668
+ app.get("/v1/workspaces/:workspaceId/tools/declarations", async (c) => {
1669
+ const workspaceId = c.req.param("workspaceId");
1670
+ const authorization = await requireAccessGrantAuthorization(
1671
+ c,
1672
+ routeDeps,
1673
+ workspaceId,
1674
+ "workspace:read",
1675
+ );
1676
+ const prepared = await prepareWorkspaceToolGateway(routeDeps, authorization);
1677
+ try {
1678
+ c.header("cache-control", "no-store");
1679
+ return c.json(workspaceToolGatewayDeclarations(prepared));
1680
+ } finally {
1681
+ await prepared.close();
1682
+ }
1683
+ });
1684
+
1685
+ app.all("/v1/workspaces/:workspaceId/codemode/sdk/*", async (c) => {
1686
+ const workspaceId = c.req.param("workspaceId");
1687
+ const grant = await requireAccessGrant(c, routeDeps, workspaceId);
1688
+ const url = new URL(c.req.url);
1689
+ const prefix = `/v1/workspaces/${workspaceId}/codemode/sdk`;
1690
+ let forwarded: Request;
1691
+ try {
1692
+ const path = url.pathname.slice(prefix.length) + url.search;
1693
+ forwarded = await codemodeSessionRequest(routeDeps, grant, c.req.raw, path, (session) => {
1694
+ assertConfiguredCodemodeSessionProxyPath(session, path, c.req.method);
1695
+ const proxyTools = configuredCodemodeSessionProxyTools(routeDeps.settings, session);
1696
+ return proxyTools === null
1697
+ ? routeDeps.settings
1698
+ : { ...routeDeps.settings, allowedFirstPartyMcpTools: proxyTools };
608
1699
  });
609
- await mcp.connect(transport);
610
- return await transport.handleRequest(boundedRequest);
611
- });
1700
+ } catch (error) {
1701
+ throw codemodeHttpError(error);
1702
+ }
1703
+ // The forwarded request drops the caller's Host and names a non-sandbox
1704
+ // path; the request it was built from already passed the local boundary.
1705
+ return app.fetch(markLocalInternalDispatch(forwarded));
612
1706
  });
613
1707
 
614
1708
  app.get("/v1/workspaces/:workspaceId/codemode/catalog", async (c) => {
@@ -664,27 +1758,42 @@ export function createAppComposition(deps: AppDependencies): {
664
1758
  }
665
1759
  });
666
1760
 
1761
+ registerConnectCallbackReturns(app, routeDeps);
667
1762
  registerFileRoutes(app, routeDeps);
1763
+ registerSessionArtifactAssociationRoutes(app, routeDeps);
668
1764
  registerApiKeyRoutes(app, routeDeps);
1765
+ registerOrganizationMcpConnectionRoutes(app, routeDeps);
1766
+ registerOrganizationServiceAccountRoutes(app, routeDeps);
669
1767
  registerBillingRoutes(app, routeDeps);
670
1768
  registerBrowserIdentityRoutes(app, routeDeps);
671
1769
  registerBrowserSessionRoutes(app, routeDeps);
672
1770
  registerComputerSessionRoutes(app, routeDeps);
673
1771
  registerDocumentRoutes(app, routeDeps);
1772
+ registerKnowledgeRoutes(app, routeDeps);
674
1773
  registerGitHubRoutes(app, routeDeps);
675
1774
  registerInstallRoutes(app, routeDeps);
676
1775
  registerInteractionResourceRoutes(app, routeDeps);
677
1776
  registerWorkspaceRoutes(app, routeDeps);
1777
+ registerUsageAllowanceRoutes(app, routeDeps);
678
1778
  registerInsightsRoutes(app, routeDeps);
1779
+ registerInsightsUsageRoutes(app, routeDeps);
679
1780
  registerWorkspaceInstructionPolicyRoutes(app, routeDeps);
1781
+ registerWorkspaceLearningRoutes(app, routeDeps);
680
1782
  registerCompanyProfileRoutes(app, routeDeps);
1783
+ registerCompanyBrainRoutes(app, routeDeps);
681
1784
  registerSlackTaskPolicyRoutes(app, routeDeps);
682
1785
  registerWorkspaceStateRoutes(app, routeDeps);
683
1786
  registerMemorySlackPublicationRoutes(app, routeDeps);
684
1787
  registerWorkspaceArtifactRoutes(app, routeDeps);
1788
+ registerArtifactCatalogRoutes(app, routeDeps);
685
1789
  registerPreferenceRegistryRoutes(app, routeDeps);
686
1790
  registerSocialRoutes(app, routeDeps);
1791
+ registerPersonalGitHubRoutes(app, routeDeps);
1792
+ registerPersonalGitHubGitBrokerRoutes(app, routeDeps);
687
1793
  registerConnectionRoutes(app, routeDeps);
1794
+ registerConnectRoutes(app, routeDeps);
1795
+
1796
+ registerExternalIdentityLinkRoutes(app, routeDeps);
688
1797
  registerCapabilityRoutes(app, routeDeps);
689
1798
  registerApiIntegrationRoutes(app, routeDeps);
690
1799
  registerIntegrationFacetRoutes(app, routeDeps);
@@ -694,23 +1803,37 @@ export function createAppComposition(deps: AppDependencies): {
694
1803
  registerEnvironmentRoutes(app, routeDeps);
695
1804
  registerChannelRoutes(app, routeDeps);
696
1805
  registerRigRoutes(app, routeDeps);
697
- registerPackRoutes(app, routeDeps);
1806
+ registerAutomationRoutes(app, routeDeps);
1807
+ registerPrReviewRoutes(app, routeDeps);
698
1808
  registerPluginRoutes(app, routeDeps);
699
1809
  registerSkillRoutes(app, routeDeps);
700
1810
  registerSessionRoutes(app, routeDeps);
1811
+ registerSessionHistoryImportRoutes(app, routeDeps);
1812
+ registerFeedbackRoutes(app, routeDeps);
1813
+ registerWorkspaceIntegrationRoutes(app, routeDeps);
1814
+ registerOrganizationIntegrationRoutes(app, routeDeps);
701
1815
  registerScheduledTaskRoutes(app, routeDeps);
702
1816
  registerCodexRoutes(app, routeDeps);
1817
+ registerOrganizationModelProviderRoutes(app, routeDeps);
1818
+ registerWorkspaceModelProviderRoutes(app, routeDeps);
1819
+ registerClaudeSubscriptionOAuthRoutes(app, routeDeps);
1820
+ registerOrganizationIntegrationPolicyRoutes(app, routeDeps);
1821
+ registerModelConnectionAccessRoutes(app, routeDeps);
703
1822
  registerSuperGrokRoutes(app, routeDeps);
1823
+ registerClaudeSubscriptionAccountRoutes(app, routeDeps);
704
1824
  registerTranscriptionRoutes(app, routeDeps);
705
1825
  registerEditableArtifactRoutes(app, routeDeps);
706
1826
  registerVideoGenerationRoutes(app, routeDeps);
707
1827
  registerCanonicalHumanIdentityRoutes(app, routeDeps);
708
1828
  registerOrganizationMembershipRoutes(app, routeDeps);
1829
+ registerOrganizationSessionRoutes(app, routeDeps);
1830
+ registerOrganizationRecoveryRoutes(app, routeDeps);
1831
+ registerUserResourceAuthorityRoutes(app, routeDeps);
709
1832
  registerSlackInteractionRoutes(app, routeDeps);
710
1833
 
711
1834
  app.notFound((c) => {
712
1835
  if (!new URL(c.req.url).pathname.startsWith("/v1/")) return c.text("Not Found", 404);
713
- const requestId = correlationIds.get(c.req.raw) ?? crypto.randomUUID();
1836
+ const requestId = correlationIds.get(c) ?? crypto.randomUUID();
714
1837
  return c.json(
715
1838
  ErrorEnvelope.parse({
716
1839
  error: {
@@ -725,18 +1848,49 @@ export function createAppComposition(deps: AppDependencies): {
725
1848
  );
726
1849
  });
727
1850
 
728
- app.onError((error, c) => {
1851
+ app.onError((rawError, c) => {
1852
+ const requestId = correlationIds.get(c) ?? crypto.randomUUID();
1853
+ c.header(OPENGENI_CORRELATION_HEADER, requestId);
1854
+ if (new URL(c.req.url).pathname.startsWith("/v1/")) {
1855
+ c.header(OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION);
1856
+ }
1857
+ // Archives remain read-only through the existing Send/Steer/control paths.
1858
+ const importErrorResponse = archivedSessionImportErrorResponse(c, rawError);
1859
+ if (importErrorResponse) return importErrorResponse;
1860
+ // One central mapping for every Send/Steer/control route: a bounded
1861
+ // control-prefix wait that expired is a known, retryable, not-applied 503.
1862
+ const error =
1863
+ rawError instanceof OrganizationIntegrationDeniedError
1864
+ ? new HTTPException(403, { message: rawError.message })
1865
+ : (allowanceExhaustedHttpError(rawError) ??
1866
+ workspaceControlBusyHttpError(rawError) ??
1867
+ scheduledTaskTargetAccessHttpError(rawError) ??
1868
+ agentConfigHttpError(rawError) ??
1869
+ modelUnavailableHttpError(rawError) ??
1870
+ (rawError instanceof UnsupportedLatencyModeError
1871
+ ? new ApiHttpError(422, {
1872
+ code: "validation_failed",
1873
+ message: rawError.message,
1874
+ retryable: false,
1875
+ outcomeUnknown: false,
1876
+ details: {
1877
+ code: rawError.code,
1878
+ modelId: rawError.modelId,
1879
+ latencyMode: rawError.latencyMode,
1880
+ allowedLatencyModes: [...rawError.allowedLatencyModes],
1881
+ },
1882
+ })
1883
+ : null) ??
1884
+ requestBodyValidationHttpError(rawError) ??
1885
+ invalidPathIdentifierHttpError(rawError, new URL(c.req.url).pathname) ??
1886
+ rawError);
729
1887
  const compactionLock = codexCompactionV2ProviderLockedError(error);
730
1888
  const apiError = error instanceof ApiHttpError ? error : null;
731
1889
  const status = compactionLock ? 422 : httpStatusForError(error);
732
1890
  const code: ErrorCode = compactionLock
733
1891
  ? compactionLock.code
734
1892
  : (apiError?.code ?? errorCodeForStatus(status));
735
- const requestId = correlationIds.get(c.req.raw) ?? crypto.randomUUID();
736
- c.header(OPENGENI_CORRELATION_HEADER, requestId);
737
- if (new URL(c.req.url).pathname.startsWith("/v1/")) {
738
- c.header(OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION);
739
- }
1893
+ if (status >= 500) logHttpFailure(c, status, code, rawError);
740
1894
  const envelope = ErrorEnvelope.parse({
741
1895
  error: {
742
1896
  status,
@@ -747,7 +1901,9 @@ export function createAppComposition(deps: AppDependencies): {
747
1901
  ? (boundedPublicMessage(apiError.message) ?? "Request failed.")
748
1902
  : publicErrorMessage(error, status),
749
1903
  retryable: apiError?.retryable ?? retryableHttpStatus(status),
750
- ...(mutationOutcomeUnknown(error, c.req.method) ? { outcomeUnknown: true } : {}),
1904
+ ...((apiError?.outcomeUnknown ?? mutationOutcomeUnknown(error, c.req.method))
1905
+ ? { outcomeUnknown: true }
1906
+ : {}),
751
1907
  requestId,
752
1908
  ...(apiError?.details ? { details: apiError.details } : {}),
753
1909
  },
@@ -758,6 +1914,11 @@ export function createAppComposition(deps: AppDependencies): {
758
1914
  return { app, routeDeps };
759
1915
  }
760
1916
 
1917
+ function managedAuthOAuthCallbackProvider(pathname: string): "google" | "github" | null {
1918
+ const match = pathname.match(/^\/v1\/auth\/callback\/(google|github)$/u);
1919
+ return match?.[1] === "google" || match?.[1] === "github" ? match[1] : null;
1920
+ }
1921
+
761
1922
  function mutationOutcomeUnknown(error: unknown, method: string): boolean {
762
1923
  if (method === "GET" || method === "HEAD" || method === "OPTIONS") return false;
763
1924
  const cause = error instanceof HTTPException ? error.cause : error;
@@ -784,7 +1945,41 @@ const publicWorkspaceBrowserRoutePatterns = [
784
1945
  ] as const;
785
1946
 
786
1947
  export function workspaceActorContextExempt(method: string, pathname: string): boolean {
787
- if (/^\/v1\/workspaces\/[^/]+\/mcp$/.test(pathname)) return true;
1948
+ // The account-scoped provisioning route shares the workspace collection prefix.
1949
+ // Hono's trailing wildcard also matches it, but "external" is not a workspace UUID;
1950
+ // the route performs its own organization API-key authorization.
1951
+ if (method === "PUT" && pathname === "/v1/workspaces/external") return true;
1952
+ // Import adapters apply their own exact create/control gates and request-local
1953
+ // RLS in core. Tenant mirrors first resolve the organization-local mapping;
1954
+ // "external" itself is not a native workspace id.
1955
+ if (isSessionHistoryImportRequest(method, pathname)) return true;
1956
+ // Organization budget authority is independent of ordinary workspace
1957
+ // membership; these routes apply their own exact account/workspace gate.
1958
+ if (
1959
+ (["GET", "PUT", "DELETE"].includes(method) &&
1960
+ /^\/v1\/workspaces\/[^/]+\/allowance$/.test(pathname)) ||
1961
+ (method === "GET" && /^\/v1\/workspaces\/[^/]+\/allowance\/state$/.test(pathname)) ||
1962
+ (method === "POST" && /^\/v1\/workspaces\/[^/]+\/allowance\/grants$/.test(pathname))
1963
+ )
1964
+ return true;
1965
+ // Allowance tenant mirrors resolve the workspace by exact organization-local
1966
+ // external identity before applying the ordinary workspace authorization.
1967
+ if (
1968
+ (["GET", "PUT", "DELETE"].includes(method) &&
1969
+ /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/allowance$/.test(pathname)) ||
1970
+ (method === "GET" &&
1971
+ /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/allowance\/state$/.test(pathname)) ||
1972
+ (method === "POST" &&
1973
+ /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/allowance\/grants$/.test(pathname)) ||
1974
+ (method === "GET" &&
1975
+ /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/usage(?:\/me)?$/.test(pathname)) ||
1976
+ (method === "PUT" &&
1977
+ /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/members\/(?:[^/]+|external\/[^/]+\/[^/]+)\/allowance$/.test(
1978
+ pathname,
1979
+ ))
1980
+ )
1981
+ return true;
1982
+ if (/^\/v1\/workspaces\/[^/]+\/mcp(?:\/(?:docs|files))?$/.test(pathname)) return true;
788
1983
  if (
789
1984
  method === "GET" &&
790
1985
  publicWorkspaceBrowserRoutePatterns.some((route) => route.test(pathname))
@@ -813,46 +2008,15 @@ async function validateCodexAccountTarget(request: Request): Promise<void> {
813
2008
  }
814
2009
  }
815
2010
 
816
- async function withAccessGrantSessionRlsContext<T>(
817
- deps: ApiRouteDeps,
818
- grant: AccessGrant,
819
- fn: () => Promise<T>,
820
- ): Promise<T> {
821
- if (grant.principalKind !== "agent_attempt") {
822
- return await withSessionRlsActorContext({ subjectId: grant.subjectId }, fn);
823
- }
824
- const callerSessionId = grant.metadata?.sessionId;
825
- if (typeof callerSessionId !== "string") {
826
- throw new HTTPException(403, { message: "agent attempt authority is invalid" });
827
- }
828
- try {
829
- const actor = await requireLiveAgentAttemptAuthorization(deps.db, grant, callerSessionId);
830
- return await withSessionRlsActorContext(
831
- {
832
- subjectId: actor.subjectId,
833
- initiatingHumanSubjectId: actor.initiatingHumanSubjectId,
834
- },
835
- fn,
836
- );
837
- } catch (error) {
838
- if (error instanceof SessionAuthorizationDeniedError) {
839
- throw new HTTPException(403, {
840
- message: "agent attempt authority is invalid",
841
- cause: error,
842
- });
843
- }
844
- throw error;
845
- }
846
- }
847
-
848
- async function requireMcpAccessGrant(
2011
+ async function requireMcpAccessGrantAuthorization(
849
2012
  c: Parameters<typeof requireAccessGrant>[0],
850
2013
  deps: ApiRouteDeps,
851
2014
  workspaceId: string,
852
- ): Promise<AccessGrant> {
853
- const grant = await requireAccessGrant(c, deps, workspaceId);
2015
+ ): Promise<Awaited<ReturnType<typeof requireAccessGrantAuthorization>>> {
2016
+ const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
2017
+ const grant = authorization.grant;
854
2018
  if (hasPermission(grant.permissions, "workspace:read")) {
855
- return grant;
2019
+ return authorization;
856
2020
  }
857
2021
  if (isCodemodeGrant(grant)) {
858
2022
  requirePermission(grant, "workspace:read");
@@ -862,15 +2026,28 @@ async function requireMcpAccessGrant(
862
2026
  // authorization seam runs immediately after this gate, and tool registration
863
2027
  // still exposes only capabilities permitted by the delegated grant.
864
2028
  if (grant.metadata?.delegated === true && typeof grant.metadata.sessionId === "string") {
865
- return grant;
2029
+ return authorization;
866
2030
  }
867
2031
  requirePermission(grant, "workspace:read");
868
- return grant;
2032
+ return authorization;
869
2033
  }
870
2034
 
871
- function clientAuthConfig(settings: AppDependencies["settings"]) {
2035
+ function clientAuthConfig(settings: AppDependencies["settings"], newSignupsEnabled: boolean) {
872
2036
  if (settings.productAccessMode === "managed") {
873
- return { mode: "managedSession" as const, session: "cookie" as const };
2037
+ return {
2038
+ mode: "managedSession" as const,
2039
+ session: "cookie" as const,
2040
+ emailVerificationRequired: settings.environment !== "local",
2041
+ newSignupsEnabled,
2042
+ socialProviders: [
2043
+ ...(settings.managedAuthGoogleClientId && settings.managedAuthGoogleClientSecret
2044
+ ? (["google"] as const)
2045
+ : []),
2046
+ ...(settings.managedAuthGithubClientId && settings.managedAuthGithubClientSecret
2047
+ ? (["github"] as const)
2048
+ : []),
2049
+ ],
2050
+ };
874
2051
  }
875
2052
  if (settings.productAccessMode === "configured") {
876
2053
  return {
@@ -888,7 +2065,143 @@ function clientAuthConfig(settings: AppDependencies["settings"]) {
888
2065
  return { mode: "none" as const };
889
2066
  }
890
2067
 
2068
+ /** Configured SDK credentials cannot regain tools from widened legacy columns. */
2069
+ export function configuredCodemodeSessionProxyTools(
2070
+ settings: Pick<Settings, "defaultFirstPartyMcpTools" | "allowedFirstPartyMcpTools">,
2071
+ session: Pick<Session, "agent">,
2072
+ ): FirstPartyMcpToolName[] | null {
2073
+ if (!session.agent) return null;
2074
+ const families = resolveAgentToolFamilies(session.agent);
2075
+ return resolveFirstPartyMcpToolPolicy(settings).allowed.filter((tool) =>
2076
+ families.allowsFirstPartyTool(tool),
2077
+ );
2078
+ }
2079
+
2080
+ /**
2081
+ * Permissions span capabilities: project_create grants sessions:create,
2082
+ * and runtime titling grants sessions:control. Fence the endpoint family
2083
+ * too, without changing null-config routing or ordinary REST authorization.
2084
+ */
2085
+ export function assertConfiguredCodemodeSessionProxyPath(
2086
+ session: Pick<Session, "id" | "agent">,
2087
+ path: string,
2088
+ method: string,
2089
+ ): void {
2090
+ if (!session.agent) return;
2091
+ const families = resolveAgentToolFamilies(session.agent);
2092
+ const pathname = new URL(path, "http://codemode.invalid").pathname;
2093
+ const segments = pathname.split("/").filter(Boolean);
2094
+ const verb = method.toUpperCase();
2095
+ const read = verb === "GET" || verb === "HEAD";
2096
+ const allows = (tool: FirstPartyMcpToolName) => families.allowsFirstPartyTool(tool);
2097
+ let allowed = false;
2098
+ if (pathname === "/v1/config/client") {
2099
+ allowed = read;
2100
+ } else if (segments[0] === "v1" && segments[1] === "workspaces" && segments[2] === "site-host") {
2101
+ const [surface, targetSessionId, operation] = segments.slice(3);
2102
+ if (!surface) {
2103
+ allowed = read;
2104
+ } else if (surface === "sessions") {
2105
+ const ownSession = targetSessionId === session.id;
2106
+ const targetAllowed = ownSession || families.subagents;
2107
+ if (!targetSessionId) {
2108
+ allowed = families.subagents;
2109
+ } else if (ownSession && !operation && read) {
2110
+ allowed = true;
2111
+ } else if (ownSession && !operation && verb === "PATCH") {
2112
+ allowed = allows("set_session_title");
2113
+ } else if (ownSession && operation === "background-commands") {
2114
+ allowed = read && allows("command_read");
2115
+ } else if (operation === "goal") {
2116
+ allowed = allows("goal_set") && targetAllowed;
2117
+ } else if (operation === "browser" || operation === "computer") {
2118
+ allowed = allows("browser_open") && targetAllowed;
2119
+ } else if (operation === "human-input-requests") {
2120
+ allowed = families.humanInput && targetAllowed;
2121
+ } else if (operation === "artifacts") {
2122
+ allowed = allows("artifacts_list") && targetAllowed;
2123
+ } else if (ownSession && ["fs", "git", "terminal", "workspace"].includes(operation ?? "")) {
2124
+ // Resource-derived sandbox mechanics are not platform capabilities.
2125
+ allowed = true;
2126
+ } else if (
2127
+ [
2128
+ "agent",
2129
+ "tool-policy",
2130
+ "mcp-servers",
2131
+ "mcp-credentials",
2132
+ "variable-sets",
2133
+ "project",
2134
+ ].includes(operation ?? "")
2135
+ ) {
2136
+ allowed = allows("project_get") && targetAllowed;
2137
+ } else {
2138
+ allowed = families.subagents;
2139
+ }
2140
+ } else if (surface === "files") {
2141
+ allowed = families.allowsMcpServer("files");
2142
+ } else if (surface === "skills") {
2143
+ allowed = families.skills === "manage" || (families.skills === "read" && read);
2144
+ } else if (surface === "human-input-requests") {
2145
+ allowed = families.humanInput;
2146
+ } else {
2147
+ const owners: Record<string, FirstPartyMcpToolName> = {
2148
+ "model-catalog": "sessions_list",
2149
+ "realtime-model-catalog": "sessions_list",
2150
+ "model-policy": "sessions_list",
2151
+ "gateway-custom-models": "sessions_list",
2152
+ "openrouter-custom-models": "sessions_list",
2153
+ "new-session-draft": "session_create",
2154
+ "session-tenancy": "session_create",
2155
+ "session-message-search": "session_events",
2156
+ "agent-topology": "sessions_list",
2157
+ projects: "project_get",
2158
+ sandboxes: "sandboxes_list",
2159
+ machines: "sandboxes_list",
2160
+ rigs: "rig_list",
2161
+ environments: "environment_list",
2162
+ "variable-sets": "variable_set_list",
2163
+ capabilities: "capability_catalog_search",
2164
+ catalog: "capability_catalog_search",
2165
+ members: "project_get",
2166
+ "member-candidates": "project_get",
2167
+ "control-events": "project_get",
2168
+ "browser-identities": "browser_identity",
2169
+ "browser-sessions": "browser_open",
2170
+ "computer-sessions": "computer_open",
2171
+ "attached-browsers": "browser_open",
2172
+ documents: "knowledge_search",
2173
+ knowledge: "knowledge_search",
2174
+ memory: "knowledge_search",
2175
+ "instruction-policy": "instruction_policy_get",
2176
+ preferences: "preference_registry_get",
2177
+ "company-profile": "company_profile_propose",
2178
+ artifacts: "artifacts_list",
2179
+ "scheduled-tasks": "scheduled_tasks_list",
2180
+ automations: "scheduled_tasks_list",
2181
+ connections: "github_connect_link",
2182
+ integrations: "github_connect_link",
2183
+ github: "github_connect_link",
2184
+ social: "social_connections_list",
2185
+ mcp: "github_connect_link",
2186
+ };
2187
+ const tool = owners[surface];
2188
+ allowed = tool !== undefined && allows(tool);
2189
+ }
2190
+ }
2191
+ if (!allowed) {
2192
+ throw new HTTPException(403, {
2193
+ message: "Agent configuration does not allow this Codemode SDK operation",
2194
+ });
2195
+ }
2196
+ }
2197
+
891
2198
  function codemodeHttpError(error: unknown): HTTPException {
2199
+ if (error instanceof SiteSessionPathError) {
2200
+ // The proxied Site/SDK surface is an explicit allowlist; a route outside
2201
+ // it (tool policy, visibility, forks, Steer, control, ...) does not exist
2202
+ // for this caller rather than being a server fault.
2203
+ return new HTTPException(404, { message: error.message, cause: error });
2204
+ }
892
2205
  if (error instanceof SessionAuthorizationDeniedError) {
893
2206
  return new HTTPException(404, {
894
2207
  message: "session not found",
@@ -901,18 +2214,39 @@ function codemodeHttpError(error: unknown): HTTPException {
901
2214
  cause: error,
902
2215
  });
903
2216
  }
904
- if (
905
- error instanceof CodemodeAuthorityError ||
906
- error instanceof CodemodeOperationNotExecutableError
907
- ) {
2217
+ if (error instanceof CodemodeCatalogNotReadyError) {
2218
+ return new ApiHttpError(409, {
2219
+ code: "conflict",
2220
+ message: error.message,
2221
+ retryable: true,
2222
+ outcomeUnknown: false,
2223
+ details: { code: error.code },
2224
+ });
2225
+ }
2226
+ if (error instanceof CodemodeCatalogStaleError) {
2227
+ return new ApiHttpError(409, {
2228
+ code: "conflict",
2229
+ message: error.message,
2230
+ retryable: true,
2231
+ outcomeUnknown: false,
2232
+ details: { code: error.code },
2233
+ });
2234
+ }
2235
+ if (error instanceof CodemodeAuthorityError) {
2236
+ return new ApiHttpError(error.reason === "invalid_grant" ? 403 : 409, {
2237
+ code: error.reason === "invalid_grant" ? "forbidden" : "conflict",
2238
+ message: error.message,
2239
+ retryable: false,
2240
+ outcomeUnknown: false,
2241
+ details: { code: error.code },
2242
+ });
2243
+ }
2244
+ if (error instanceof CodemodeOperationNotExecutableError) {
908
2245
  return new HTTPException(409, { message: error.message, cause: error });
909
2246
  }
910
2247
  if (error instanceof CodemodeOperationConflictError) {
911
2248
  return new HTTPException(409, { message: error.message, cause: error });
912
2249
  }
913
- if (error instanceof CodemodeCallBudgetExceededError) {
914
- return new HTTPException(429, { message: error.message, cause: error });
915
- }
916
2250
  if (error instanceof CodemodeToolNotInCatalogError) {
917
2251
  return new HTTPException(404, { message: error.message, cause: error });
918
2252
  }
@@ -955,6 +2289,15 @@ function clientAnalyticsConfig(settings: AppDependencies["settings"]) {
955
2289
  };
956
2290
  }
957
2291
 
2292
+ function clientLegalConfig(settings: AppDependencies["settings"]) {
2293
+ return {
2294
+ ...(settings.legalPrivacyPolicyUrl ? { privacyPolicyUrl: settings.legalPrivacyPolicyUrl } : {}),
2295
+ ...(settings.legalTermsOfServiceUrl
2296
+ ? { termsOfServiceUrl: settings.legalTermsOfServiceUrl }
2297
+ : {}),
2298
+ };
2299
+ }
2300
+
958
2301
  function structuredServicesHint(backend: string): {
959
2302
  fileSystem: boolean;
960
2303
  git: boolean;
@@ -978,9 +2321,17 @@ function codexCompactionV2ProviderLockedError(
978
2321
  }
979
2322
 
980
2323
  export function httpStatusForError(error: unknown): number {
2324
+ if (error instanceof ConnectAttemptConflictError) return 409;
2325
+ if (error instanceof ConnectAttemptNotFoundError) return 404;
981
2326
  if (codexCompactionV2ProviderLockedError(error)) {
982
2327
  return 422;
983
2328
  }
2329
+ if (workspaceControlBusyHttpError(error)) {
2330
+ return 503;
2331
+ }
2332
+ if (isRequestBodyValidationError(error)) {
2333
+ return 400;
2334
+ }
984
2335
  if (error instanceof HTTPException) {
985
2336
  return error.status;
986
2337
  }
@@ -994,7 +2345,7 @@ export function errorCodeForStatus(status: number): ErrorCode {
994
2345
  if (status === 401) return "unauthenticated";
995
2346
  if (status === 402) return "payment_required";
996
2347
  if (status === 403) return "forbidden";
997
- if (status === 404) return "not_found";
2348
+ if (status === 404 || status === 405) return "not_found";
998
2349
  if (status === 409) return "conflict";
999
2350
  if (status === 413 || status === 422 || status === 400) return "validation_failed";
1000
2351
  if (status === 429) return "limit_exceeded";
@@ -1007,11 +2358,14 @@ function retryableHttpStatus(status: number): boolean {
1007
2358
  }
1008
2359
 
1009
2360
  function publicErrorMessage(error: unknown, status: number): string {
2361
+ if (error instanceof ConnectAttemptConflictError)
2362
+ return "Connection setup changed or is still in progress. Reload its current status before retrying.";
2363
+ if (error instanceof ConnectAttemptNotFoundError) return "Connection setup not found.";
1010
2364
  if (status === 502 || status === 503 || status === 504) {
1011
- return "OpenGeni is temporarily unavailable — retry.";
2365
+ return "Opengeni is temporarily unavailable — retry.";
1012
2366
  }
1013
2367
  if (status >= 500) {
1014
- return "OpenGeni could not complete the request.";
2368
+ return "Opengeni could not complete the request.";
1015
2369
  }
1016
2370
  if (error instanceof HTTPException) {
1017
2371
  return boundedPublicMessage(error.message) ?? "Request failed.";
@@ -1041,19 +2395,26 @@ type ReadinessChecks = Record<ReadinessCheckName, ReadinessCheck>;
1041
2395
  type ReadinessCheckResult = { ok: boolean; error?: string };
1042
2396
 
1043
2397
  function readinessChecks(deps: AppDependencies): ReadinessChecks {
2398
+ const configuredNatsCheck = deps.readinessChecks?.nats;
1044
2399
  return {
1045
- db:
1046
- deps.readinessChecks?.db ??
1047
- (async () => {
2400
+ db: async () => {
2401
+ if (deps.readinessChecks?.db) {
2402
+ await deps.readinessChecks.db();
2403
+ } else {
1048
2404
  await deps.db.execute(dbSql`select 1`);
1049
- }),
1050
- nats:
1051
- deps.readinessChecks?.nats ??
1052
- (() => {
2405
+ }
2406
+ await resolveCatalogSettings(deps.db, deps.settings);
2407
+ },
2408
+ nats: async () => {
2409
+ requireSessionEventDurableFanoutCapability(deps.bus);
2410
+ if (configuredNatsCheck) {
2411
+ await configuredNatsCheck();
2412
+ } else {
1053
2413
  if (deps.bus.isConnected && !deps.bus.isConnected()) {
1054
2414
  throw new Error("NATS is not connected");
1055
2415
  }
1056
- }),
2416
+ }
2417
+ },
1057
2418
  temporal:
1058
2419
  deps.readinessChecks?.temporal ??
1059
2420
  deps.workflowClient.check ??
@@ -1118,8 +2479,37 @@ const routeLabelPatterns: Array<{
1118
2479
  pattern: RegExp;
1119
2480
  label: string | ((match: RegExpMatchArray) => string);
1120
2481
  }> = [
2482
+ {
2483
+ pattern: /^\/\.well-known\/oauth-authorization-server$/,
2484
+ label: "/.well-known/oauth-authorization-server",
2485
+ },
2486
+ {
2487
+ pattern:
2488
+ /^\/\.well-known\/oauth-protected-resource\/v1\/workspaces\/[^/]+\/mcp(?:\/(docs|files))?$/,
2489
+ label: (match) =>
2490
+ `/.well-known/oauth-protected-resource/v1/workspaces/:workspaceId/mcp${match[1] ? `/${match[1]}` : ""}`,
2491
+ },
2492
+ {
2493
+ pattern: /^\/oauth\/(register|authorize|token)$/,
2494
+ label: (match) => `/oauth/${match[1]}`,
2495
+ },
1121
2496
  { pattern: /^\/healthz$/, label: "/healthz" },
1122
2497
  { pattern: /^\/readyz$/, label: "/readyz" },
2498
+ // Better Auth answers these behind one `/v1/auth/*` registration, so the
2499
+ // provider endpoints need an explicit closed set to stay distinguishable.
2500
+ {
2501
+ pattern:
2502
+ /^\/v1\/auth\/(sign-up\/email|sign-in\/email|sign-in\/social|sign-out|send-verification-email|verify-email|request-password-reset|reset-password|error|ok)$/,
2503
+ label: (match) => `/v1/auth/${match[1]}`,
2504
+ },
2505
+ { pattern: /^\/v1\/auth\/reset-password\/[^/]+$/, label: "/v1/auth/reset-password/:token" },
2506
+ {
2507
+ pattern: /^\/v1\/auth\/callback\/([^/]+)$/,
2508
+ label: (match) =>
2509
+ match[1] === "google" || match[1] === "github"
2510
+ ? `/v1/auth/callback/${match[1]}`
2511
+ : "/v1/auth/callback/:providerId",
2512
+ },
1123
2513
  { pattern: /^\/traffic-readyz$/, label: "/traffic-readyz" },
1124
2514
  {
1125
2515
  pattern: /^\/v1\/workspaces\/[^/]+\/codex\/connect\/start$/,
@@ -1133,6 +2523,22 @@ const routeLabelPatterns: Array<{
1133
2523
  pattern: /^\/v1\/workspaces\/[^/]+\/codex\/status$/,
1134
2524
  label: "/v1/workspaces/:workspaceId/codex/status",
1135
2525
  },
2526
+ {
2527
+ pattern: /^\/v1\/workspaces\/[^/]+\/codex\/source$/,
2528
+ label: "/v1/workspaces/:workspaceId/codex/source",
2529
+ },
2530
+ {
2531
+ pattern: /^\/v1\/organizations\/[^/]+\/codex\/(accounts|settings)$/,
2532
+ label: (match) => `/v1/organizations/:organizationId/codex/${match[1]}`,
2533
+ },
2534
+ {
2535
+ pattern: /^\/v1\/organizations\/[^/]+\/codex\/connect\/(start|poll)$/,
2536
+ label: (match) => `/v1/organizations/:organizationId/codex/connect/${match[1]}`,
2537
+ },
2538
+ {
2539
+ pattern: /^\/v1\/organizations\/[^/]+\/codex\/accounts\/[^/]+(?:\/activate)?$/,
2540
+ label: "/v1/organizations/:organizationId/codex/accounts/:accountId",
2541
+ },
1136
2542
  {
1137
2543
  pattern: /^\/v1\/workspaces\/[^/]+\/supergrok\/connect\/(start|poll)$/,
1138
2544
  label: (match) => `/v1/workspaces/:workspaceId/supergrok/connect/${match[1]}`,
@@ -1159,6 +2565,8 @@ const routeLabelPatterns: Array<{
1159
2565
  },
1160
2566
  { pattern: /^\/metrics$/, label: "/metrics" },
1161
2567
  { pattern: /^\/v1\/config\/client$/, label: "/v1/config/client" },
2568
+ { pattern: /^\/v1\/client-errors$/, label: "/v1/client-errors" },
2569
+ { pattern: /^\/v1\/analytics-consent$/, label: "/v1/analytics-consent" },
1162
2570
  { pattern: /^\/v1\/billing$/, label: "/v1/billing" },
1163
2571
  { pattern: /^\/v1\/billing\/checkout$/, label: "/v1/billing/checkout" },
1164
2572
  { pattern: /^\/v1\/billing\/usage$/, label: "/v1/billing/usage" },
@@ -1166,11 +2574,31 @@ const routeLabelPatterns: Array<{
1166
2574
  pattern: /^\/v1\/workspaces\/[^/]+\/insights$/,
1167
2575
  label: "/v1/workspaces/:workspaceId/insights",
1168
2576
  },
2577
+ {
2578
+ pattern: /^\/v1\/workspaces\/[^/]+\/insights\/usage$/,
2579
+ label: "/v1/workspaces/:workspaceId/insights/usage",
2580
+ },
2581
+ {
2582
+ pattern: /^\/v1\/workspaces\/[^/]+\/insights\/calls$/,
2583
+ label: "/v1/workspaces/:workspaceId/insights/calls",
2584
+ },
2585
+ {
2586
+ pattern: /^\/v1\/organizations\/[^/]+\/insights\/usage$/,
2587
+ label: "/v1/organizations/:accountId/insights/usage",
2588
+ },
2589
+ {
2590
+ pattern: /^\/v1\/organizations\/[^/]+\/insights\/calls$/,
2591
+ label: "/v1/organizations/:accountId/insights/calls",
2592
+ },
1169
2593
  {
1170
2594
  pattern: /^\/v1\/billing\/entitlements$/,
1171
2595
  label: "/v1/billing/entitlements",
1172
2596
  },
1173
2597
  { pattern: /^\/v1\/webhooks\/stripe$/, label: "/v1/webhooks/stripe" },
2598
+ {
2599
+ pattern: /^\/v1\/workspaces\/[^/]+\/pr-review\/(registrations|repositories)(?:\/[^/]+)?$/,
2600
+ label: (match) => `/v1/workspaces/:workspaceId/pr-review/${match[1]}`,
2601
+ },
1174
2602
  {
1175
2603
  pattern: /^\/v1\/workspaces\/[^/]+\/mcp$/,
1176
2604
  label: "/v1/workspaces/:workspaceId/mcp",
@@ -1183,6 +2611,22 @@ const routeLabelPatterns: Array<{
1183
2611
  pattern: /^\/v1\/workspaces\/[^/]+\/mcp\/files$/,
1184
2612
  label: "/v1/workspaces/:workspaceId/mcp/files",
1185
2613
  },
2614
+ {
2615
+ pattern: /^\/v1\/workspaces\/[^/]+\/tools\/catalog$/,
2616
+ label: "/v1/workspaces/:workspaceId/tools/catalog",
2617
+ },
2618
+ {
2619
+ pattern: /^\/v1\/workspaces\/[^/]+\/tools\/calls$/,
2620
+ label: "/v1/workspaces/:workspaceId/tools/calls",
2621
+ },
2622
+ {
2623
+ pattern: /^\/v1\/workspaces\/[^/]+\/tools\/approvals$/,
2624
+ label: "/v1/workspaces/:workspaceId/tools/approvals",
2625
+ },
2626
+ {
2627
+ pattern: /^\/v1\/workspaces\/[^/]+\/tools\/declarations$/,
2628
+ label: "/v1/workspaces/:workspaceId/tools/declarations",
2629
+ },
1186
2630
  {
1187
2631
  pattern: /^\/v1\/workspaces\/[^/]+\/default-rig$/,
1188
2632
  label: "/v1/workspaces/:workspaceId/default-rig",
@@ -1191,6 +2635,18 @@ const routeLabelPatterns: Array<{
1191
2635
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions$/,
1192
2636
  label: "/v1/workspaces/:workspaceId/sessions",
1193
2637
  },
2638
+ {
2639
+ pattern: /^\/v1\/workspaces\/[^/]+\/session-imports$/,
2640
+ label: "/v1/workspaces/:workspaceId/session-imports",
2641
+ },
2642
+ {
2643
+ pattern: /^\/v1\/workspaces\/[^/]+\/session-imports\/[^/]+\/events$/,
2644
+ label: "/v1/workspaces/:workspaceId/session-imports/:importId/events",
2645
+ },
2646
+ {
2647
+ pattern: /^\/v1\/workspaces\/[^/]+\/session-message-search$/,
2648
+ label: "/v1/workspaces/:workspaceId/session-message-search",
2649
+ },
1194
2650
  {
1195
2651
  pattern: /^\/v1\/workspaces\/[^/]+\/control-events\/stream$/,
1196
2652
  label: "/v1/workspaces/:workspaceId/control-events/stream",
@@ -1288,6 +2744,11 @@ const routeLabelPatterns: Array<{
1288
2744
  label:
1289
2745
  "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/targets/:targetId/observation",
1290
2746
  },
2747
+ {
2748
+ pattern: /^\/v1\/workspaces\/[^/]+\/computer-sessions\/[^/]+\/targets\/[^/]+\/screenshot$/,
2749
+ label:
2750
+ "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/targets/:targetId/screenshot",
2751
+ },
1291
2752
  {
1292
2753
  pattern: /^\/v1\/workspaces\/[^/]+\/computer-sessions\/[^/]+\/operations\/[^/]+$/,
1293
2754
  label:
@@ -1351,6 +2812,10 @@ const routeLabelPatterns: Array<{
1351
2812
  pattern: /^\/v1\/workspaces\/[^/]+\/inference-control$/,
1352
2813
  label: "/v1/workspaces/:workspaceId/inference-control",
1353
2814
  },
2815
+ {
2816
+ pattern: /^\/v1\/workspaces\/[^/]+\/pause-timer$/,
2817
+ label: "/v1/workspaces/:workspaceId/pause-timer",
2818
+ },
1354
2819
  {
1355
2820
  pattern:
1356
2821
  /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/fs\/(list|list-batch|read|write|delete|move|mkdir)$/,
@@ -1372,6 +2837,10 @@ const routeLabelPatterns: Array<{
1372
2837
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/events\/stream$/,
1373
2838
  label: "/v1/workspaces/:workspaceId/sessions/:id/events/stream",
1374
2839
  },
2840
+ {
2841
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/(visibility|forks)$/,
2842
+ label: (match) => `/v1/workspaces/:workspaceId/sessions/:id/${match[1]}`,
2843
+ },
1375
2844
  {
1376
2845
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/lineage$/,
1377
2846
  label: "/v1/workspaces/:workspaceId/sessions/:id/lineage",
@@ -1380,6 +2849,10 @@ const routeLabelPatterns: Array<{
1380
2849
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/events$/,
1381
2850
  label: "/v1/workspaces/:workspaceId/sessions/:id/events",
1382
2851
  },
2852
+ {
2853
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/turns$/,
2854
+ label: "/v1/workspaces/:workspaceId/sessions/:id/turns",
2855
+ },
1383
2856
  {
1384
2857
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/queue\/[^/]+\/(move|edit|steer|delete)$/,
1385
2858
  label: "/v1/workspaces/:workspaceId/sessions/:id/queue/:turnId/:action",
@@ -1388,6 +2861,10 @@ const routeLabelPatterns: Array<{
1388
2861
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/queue$/,
1389
2862
  label: "/v1/workspaces/:workspaceId/sessions/:id/queue",
1390
2863
  },
2864
+ {
2865
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/composer-draft\/submit$/,
2866
+ label: "/v1/workspaces/:workspaceId/sessions/:id/composer-draft/submit",
2867
+ },
1391
2868
  {
1392
2869
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/composer-draft$/,
1393
2870
  label: "/v1/workspaces/:workspaceId/sessions/:id/composer-draft",
@@ -1452,6 +2929,84 @@ const routeLabelPatterns: Array<{
1452
2929
  pattern: /^\/v1\/workspaces\/[^/]+\/api-keys\/[^/]+$/,
1453
2930
  label: "/v1/workspaces/:workspaceId/api-keys/:id",
1454
2931
  },
2932
+ {
2933
+ pattern: /^\/v1\/organizations\/[^/]+\/api-keys$/,
2934
+ label: "/v1/organizations/:organizationId/api-keys",
2935
+ },
2936
+ {
2937
+ pattern: /^\/v1\/organizations\/[^/]+\/api-keys\/[^/]+$/,
2938
+ label: "/v1/organizations/:organizationId/api-keys/:id",
2939
+ },
2940
+ {
2941
+ pattern: /^\/v1\/workspaces\/external$/,
2942
+ label: "/v1/workspaces/external",
2943
+ },
2944
+ {
2945
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/session-imports$/,
2946
+ label: "/v1/workspaces/external/:source/:externalId/session-imports",
2947
+ },
2948
+ {
2949
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/session-imports\/[^/]+\/events$/,
2950
+ label: "/v1/workspaces/external/:source/:externalId/session-imports/:importId/events",
2951
+ },
2952
+ {
2953
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/allowance\/grants$/,
2954
+ label: "/v1/workspaces/external/:source/:externalId/allowance/grants",
2955
+ },
2956
+ {
2957
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/allowance$/,
2958
+ label: "/v1/workspaces/external/:source/:externalId/allowance",
2959
+ },
2960
+ {
2961
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/allowance\/state$/,
2962
+ label: "/v1/workspaces/external/:source/:externalId/allowance/state",
2963
+ },
2964
+ {
2965
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/usage\/me$/,
2966
+ label: "/v1/workspaces/external/:source/:externalId/usage/me",
2967
+ },
2968
+ {
2969
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/usage$/,
2970
+ label: "/v1/workspaces/external/:source/:externalId/usage",
2971
+ },
2972
+ {
2973
+ pattern:
2974
+ /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/members\/external\/[^/]+\/[^/]+\/allowance$/,
2975
+ label:
2976
+ "/v1/workspaces/external/:source/:externalId/members/external/:memberSource/:memberExternalId/allowance",
2977
+ },
2978
+ {
2979
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/members\/[^/]+\/allowance$/,
2980
+ label: "/v1/workspaces/external/:source/:externalId/members/:subjectId/allowance",
2981
+ },
2982
+ {
2983
+ pattern: /^\/v1\/workspaces\/[^/]+\/allowance\/grants$/,
2984
+ label: "/v1/workspaces/:workspaceId/allowance/grants",
2985
+ },
2986
+ {
2987
+ pattern: /^\/v1\/workspaces\/[^/]+\/allowance$/,
2988
+ label: "/v1/workspaces/:workspaceId/allowance",
2989
+ },
2990
+ {
2991
+ pattern: /^\/v1\/workspaces\/[^/]+\/allowance\/state$/,
2992
+ label: "/v1/workspaces/:workspaceId/allowance/state",
2993
+ },
2994
+ {
2995
+ pattern: /^\/v1\/workspaces\/[^/]+\/usage\/me$/,
2996
+ label: "/v1/workspaces/:workspaceId/usage/me",
2997
+ },
2998
+ {
2999
+ pattern: /^\/v1\/workspaces\/[^/]+\/usage$/,
3000
+ label: "/v1/workspaces/:workspaceId/usage",
3001
+ },
3002
+ {
3003
+ pattern: /^\/v1\/workspaces\/[^/]+\/members\/external\/[^/]+\/[^/]+\/allowance$/,
3004
+ label: "/v1/workspaces/:workspaceId/members/external/:source/:externalId/allowance",
3005
+ },
3006
+ {
3007
+ pattern: /^\/v1\/workspaces\/[^/]+\/members\/[^/]+\/allowance$/,
3008
+ label: "/v1/workspaces/:workspaceId/members/:subjectId/allowance",
3009
+ },
1455
3010
  {
1456
3011
  pattern: /^\/v1\/workspaces\/[^/]+\/scheduled-tasks$/,
1457
3012
  label: "/v1/workspaces/:workspaceId/scheduled-tasks",
@@ -1516,6 +3071,10 @@ const routeLabelPatterns: Array<{
1516
3071
  pattern: /^\/v1\/workspaces\/[^/]+\/github\/app$/,
1517
3072
  label: "/v1/workspaces/:workspaceId/github/app",
1518
3073
  },
3074
+ {
3075
+ pattern: /^\/v1\/workspaces\/[^/]+\/github\/action-policies$/,
3076
+ label: "/v1/workspaces/:workspaceId/github/action-policies",
3077
+ },
1519
3078
  {
1520
3079
  pattern: /^\/v1\/workspaces\/[^/]+\/github\/repositories$/,
1521
3080
  label: "/v1/workspaces/:workspaceId/github/repositories",
@@ -1548,6 +3107,22 @@ const routeLabelPatterns: Array<{
1548
3107
  pattern: /^\/v1\/workspaces\/[^/]+\/github\/app-manifest$/,
1549
3108
  label: "/v1/workspaces/:workspaceId/github/app-manifest",
1550
3109
  },
3110
+ {
3111
+ pattern: /^\/v1\/workspaces\/[^/]+\/pr-review\/github$/,
3112
+ label: "/v1/workspaces/:workspaceId/pr-review/github",
3113
+ },
3114
+ {
3115
+ pattern: /^\/v1\/workspaces\/[^/]+\/pr-review\/github\/connect$/,
3116
+ label: "/v1/workspaces/:workspaceId/pr-review/github/connect",
3117
+ },
3118
+ {
3119
+ pattern: /^\/v1\/workspaces\/[^/]+\/pr-review\/github\/installations\/select$/,
3120
+ label: "/v1/workspaces/:workspaceId/pr-review/github/installations/select",
3121
+ },
3122
+ {
3123
+ pattern: /^\/v1\/workspaces\/[^/]+\/pr-review\/github\/installations\/[^/]+\/configure$/,
3124
+ label: "/v1/workspaces/:workspaceId/pr-review/github/installations/:installationId/configure",
3125
+ },
1551
3126
  {
1552
3127
  pattern: /^\/v1\/workspaces\/[^/]+\/capabilities$/,
1553
3128
  label: "/v1/workspaces/:workspaceId/capabilities",
@@ -1650,26 +3225,7 @@ const routeLabelPatterns: Array<{
1650
3225
  pattern: /^\/v1\/workspaces\/[^/]+\/environments\/[^/]+$/,
1651
3226
  label: "/v1/workspaces/:workspaceId/environments/:id",
1652
3227
  },
1653
- {
1654
- pattern: /^\/v1\/workspaces\/[^/]+\/packs$/,
1655
- label: "/v1/workspaces/:workspaceId/packs",
1656
- },
1657
- {
1658
- pattern: /^\/v1\/workspaces\/[^/]+\/packs\/installations$/,
1659
- label: "/v1/workspaces/:workspaceId/packs/installations",
1660
- },
1661
- {
1662
- pattern: /^\/v1\/workspaces\/[^/]+\/packs\/marketing-social-daily-analysis\/scheduled-tasks$/,
1663
- label: "/v1/workspaces/:workspaceId/packs/marketing-social-daily-analysis/scheduled-tasks",
1664
- },
1665
- {
1666
- pattern: /^\/v1\/workspaces\/[^/]+\/packs\/[^/]+\/enable$/,
1667
- label: "/v1/workspaces/:workspaceId/packs/:id/enable",
1668
- },
1669
- {
1670
- pattern: /^\/v1\/workspaces\/[^/]+\/packs\/[^/]+$/,
1671
- label: "/v1/workspaces/:workspaceId/packs/:id",
1672
- },
3228
+
1673
3229
  {
1674
3230
  pattern: /^\/v1\/workspaces\/[^/]+\/plugins\/preview$/,
1675
3231
  label: "/v1/workspaces/:workspaceId/plugins/preview",
@@ -1706,6 +3262,26 @@ const routeLabelPatterns: Array<{
1706
3262
  pattern: /^\/v1\/workspaces\/[^/]+\/connections\/oauth\/start$/,
1707
3263
  label: "/v1/workspaces/:workspaceId/connections/oauth/start",
1708
3264
  },
3265
+ {
3266
+ pattern: /^\/v1\/workspaces\/[^/]+\/connections\/github$/,
3267
+ label: "/v1/workspaces/:workspaceId/connections/github",
3268
+ },
3269
+ {
3270
+ pattern: /^\/v1\/workspaces\/[^/]+\/connections\/github\/oauth\/start$/,
3271
+ label: "/v1/workspaces/:workspaceId/connections/github/oauth/start",
3272
+ },
3273
+ {
3274
+ pattern: /^\/v1\/workspaces\/[^/]+\/connections\/[^/]+\/github\/reconnect$/,
3275
+ label: "/v1/workspaces/:workspaceId/connections/:connectionId/github/reconnect",
3276
+ },
3277
+ {
3278
+ pattern: /^\/v1\/workspaces\/[^/]+\/connections\/[^/]+\/github\/repositories\/verify$/,
3279
+ label: "/v1/workspaces/:workspaceId/connections/:connectionId/github/repositories/verify",
3280
+ },
3281
+ {
3282
+ pattern: /^\/v1\/workspaces\/[^/]+\/connections\/[^/]+\/github\/repositories$/,
3283
+ label: "/v1/workspaces/:workspaceId/connections/:connectionId/github/repositories",
3284
+ },
1709
3285
  {
1710
3286
  pattern: /^\/v1\/workspaces\/[^/]+\/integrations\/oauth\/start$/,
1711
3287
  label: "/v1/workspaces/:workspaceId/integrations/oauth/start",
@@ -1730,6 +3306,18 @@ const routeLabelPatterns: Array<{
1730
3306
  pattern: /^\/v1\/workspaces\/[^/]+\/connections\/[^/]+$/,
1731
3307
  label: "/v1/workspaces/:workspaceId/connections/:connectionId",
1732
3308
  },
3309
+ {
3310
+ pattern: /^\/v1\/git\/personal\/[^/]+\/info\/refs$/,
3311
+ label: "/v1/git/personal/:routeId/info/refs",
3312
+ },
3313
+ {
3314
+ pattern: /^\/v1\/git\/personal\/[^/]+\/git-upload-pack$/,
3315
+ label: "/v1/git/personal/:routeId/git-upload-pack",
3316
+ },
3317
+ {
3318
+ pattern: /^\/v1\/git\/personal\/[^/]+\/git-receive-pack$/,
3319
+ label: "/v1/git/personal/:routeId/git-receive-pack",
3320
+ },
1733
3321
  { pattern: /^\/v1\/catalog-assets\/.+$/, label: "/v1/catalog-assets/*" },
1734
3322
  {
1735
3323
  pattern: /^\/v1\/integrations\/oauth\/callback$/,
@@ -1739,6 +3327,10 @@ const routeLabelPatterns: Array<{
1739
3327
  pattern: /^\/v1\/integrations\/provider-oauth\/callback$/,
1740
3328
  label: "/v1/integrations/provider-oauth/callback",
1741
3329
  },
3330
+ {
3331
+ pattern: /^\/v1\/integrations\/github-personal\/oauth\/callback$/,
3332
+ label: "/v1/integrations/github-personal/oauth/callback",
3333
+ },
1742
3334
  {
1743
3335
  pattern: /^\/v1\/integrations\/google-drive\/callback$/,
1744
3336
  label: "/v1/integrations/google-drive/callback",
@@ -1779,6 +3371,14 @@ const routeLabelPatterns: Array<{
1779
3371
  pattern: /^\/v1\/workspaces\/[^/]+\/machines\/[^/]+\/metrics\/series$/,
1780
3372
  label: "/v1/workspaces/:workspaceId/machines/:enrollmentId/metrics/series",
1781
3373
  },
3374
+ {
3375
+ pattern: /^\/v1\/workspaces\/[^/]+\/machines\/[^/]+\/operation-policy$/,
3376
+ label: "/v1/workspaces/:workspaceId/machines/:enrollmentId/operation-policy",
3377
+ },
3378
+ {
3379
+ pattern: /^\/v1\/workspaces\/[^/]+\/machines\/[^/]+\/update$/,
3380
+ label: "/v1/workspaces/:workspaceId/machines/:enrollmentId/update",
3381
+ },
1782
3382
  {
1783
3383
  pattern: /^\/v1\/workspaces\/[^/]+\/machines$/,
1784
3384
  label: "/v1/workspaces/:workspaceId/machines",
@@ -1796,18 +3396,105 @@ const routeLabelPatterns: Array<{
1796
3396
  pattern: /^\/v1\/github\/oauth\/callback$/,
1797
3397
  label: "/v1/github/oauth/callback",
1798
3398
  },
3399
+ { pattern: /^\/v1\/pr-review\/github\/setup$/, label: "/v1/pr-review/github/setup" },
3400
+ {
3401
+ pattern: /^\/v1\/pr-review\/github\/install\/callback$/,
3402
+ label: "/v1/pr-review/github/install/callback",
3403
+ },
3404
+ {
3405
+ pattern: /^\/v1\/pr-review\/github\/oauth\/callback$/,
3406
+ label: "/v1/pr-review/github/oauth/callback",
3407
+ },
3408
+ {
3409
+ pattern: /^\/v1\/webhooks\/pr-review\/github$/,
3410
+ label: "/v1/webhooks/pr-review/github",
3411
+ },
3412
+ {
3413
+ pattern: /^\/v1\/workspaces\/[^/]+$/,
3414
+ label: "/v1/workspaces/:workspaceId",
3415
+ },
1799
3416
  ];
1800
3417
 
1801
- export function routeLabel(pathname: string): string {
3418
+ /**
3419
+ * Bounded route label for metrics, spans, and request logs. Explicit patterns
3420
+ * keep established label spellings stable; any other request answered by a
3421
+ * registered handler uses that handler's code-owned path template (see
3422
+ * `registeredHandlerRoutePath`). Only unregistered paths fall into `unknown`.
3423
+ */
3424
+ export function routeLabel(pathname: string, registeredRoutePath?: string | null): string {
3425
+ if (/^\/v1\/workspaces\/[^/]+\/transcriptions$/.test(pathname))
3426
+ return "/v1/workspaces/:workspaceId/transcriptions";
3427
+ const transcription = pathname.match(
3428
+ /^\/v1\/workspaces\/[^/]+\/transcription-recordings(?:\/[^/]+(\/(?:finalize|process-next)|\/chunks\/\d+)?)?$/,
3429
+ );
3430
+ if (transcription) {
3431
+ const base = "/v1/workspaces/:workspaceId/transcription-recordings";
3432
+ if (pathname.split("/").length === 5) return base;
3433
+ return (
3434
+ base +
3435
+ "/:recordingId" +
3436
+ (transcription[1]?.startsWith("/chunks/") ? "/chunks/:chunkNumber" : (transcription[1] ?? ""))
3437
+ );
3438
+ }
1802
3439
  for (const candidate of routeLabelPatterns) {
1803
3440
  const match = pathname.match(candidate.pattern);
1804
3441
  if (match) {
1805
3442
  return typeof candidate.label === "string" ? candidate.label : candidate.label(match);
1806
3443
  }
1807
3444
  }
3445
+ const registered = boundedRegisteredRouteLabel(registeredRoutePath);
3446
+ if (registered) return registered;
1808
3447
  return pathname.startsWith("/v1/") ? "/v1/unknown" : "/unknown";
1809
3448
  }
1810
3449
 
3450
+ /**
3451
+ * API contract revisions no longer admitted from ANY caller, including bearer
3452
+ * integrations that would otherwise be accepted across revisions. Add a
3453
+ * revision here only for a truly breaking wire change (which also requires a
3454
+ * major release-train change); ordinary revision bumps stay additive.
3455
+ */
3456
+ export const REFUSED_API_CONTRACT_REVISIONS: ReadonlySet<string> = new Set<string>([]);
3457
+
3458
+ const BEARER_AUTHORIZATION = /^bearer\s+\S/i;
3459
+
3460
+ /**
3461
+ * The rollout fence for state-changing product calls.
3462
+ *
3463
+ * The exact-revision check exists to stop a stale first-party browser tab
3464
+ * (cookie session, or unauthenticated local mode) from writing with an old
3465
+ * request shape after a deployment; it answers 409 so the page reloads onto
3466
+ * the matching bundle. That tab never sends `Authorization`, so this is not a
3467
+ * header a stale bundle can choose to bypass it with.
3468
+ *
3469
+ * Bearer-authenticated callers (organization/workspace API keys, delegated
3470
+ * tokens, the deployment key sent as a bearer) are integrations pinned to an
3471
+ * SDK version: reloading cannot upgrade them, and the API is additive within a
3472
+ * major release train. They are admitted with an older revision or with no
3473
+ * revision claim at all. Only a revision listed in
3474
+ * `REFUSED_API_CONTRACT_REVISIONS` is refused for them.
3475
+ *
3476
+ * This is a compatibility fence, not an authorization boundary: every route
3477
+ * still authenticates and authorizes the request independently.
3478
+ */
3479
+ export function apiContractAdmission(
3480
+ input: {
3481
+ method: string;
3482
+ pathname: string;
3483
+ authorization: string | undefined;
3484
+ claimedRevision: string | undefined;
3485
+ },
3486
+ refusedRevisions: ReadonlySet<string> = REFUSED_API_CONTRACT_REVISIONS,
3487
+ ): "admit" | "reject" {
3488
+ if (!isApiContractProtectedMutation(input.method, input.pathname)) return "admit";
3489
+ if (input.claimedRevision === OPENGENI_API_CONTRACT_REVISION) return "admit";
3490
+ if (input.claimedRevision !== undefined && refusedRevisions.has(input.claimedRevision)) {
3491
+ return "reject";
3492
+ }
3493
+ return input.authorization !== undefined && BEARER_AUTHORIZATION.test(input.authorization)
3494
+ ? "admit"
3495
+ : "reject";
3496
+ }
3497
+
1811
3498
  /**
1812
3499
  * State-changing OpenGeni HTTP calls must never cross an incompatible rollout
1813
3500
  * boundary. Standard third-party protocols and externally initiated callbacks
@@ -1820,19 +3507,47 @@ export function isApiContractProtectedMutation(method: string, pathname: string)
1820
3507
  if (!pathname.startsWith("/v1/")) {
1821
3508
  return false;
1822
3509
  }
3510
+ if (
3511
+ pathname === "/v1/auth/organization-onboarding" ||
3512
+ pathname === "/v1/auth/organization-setup"
3513
+ ) {
3514
+ return true;
3515
+ }
1823
3516
  if (
1824
3517
  pathname.startsWith("/v1/auth/") ||
1825
3518
  pathname.startsWith("/v1/webhooks/") ||
1826
3519
  pathname.startsWith("/v1/integrations/oauth/") ||
3520
+ isPersonalGitHubGitBrokerRequest(method, pathname) ||
1827
3521
  pathname === "/v1/integrations/slack/events" ||
1828
3522
  pathname === "/v1/integrations/slack/commands" ||
1829
3523
  pathname === "/v1/integrations/slack/interactions" ||
1830
3524
  pathname.startsWith("/v1/github/") ||
3525
+ // A stale tab must still report the error that follows a rollout.
3526
+ pathname === CLIENT_ERRORS_PATH ||
3527
+ pathname === ANALYTICS_CONSENT_PATH ||
1831
3528
  pathname === "/v1/enrollments/device/start" ||
1832
3529
  pathname === "/v1/enrollments/device/poll" ||
1833
- pathname === "/v1/enrollments/token/exchange"
3530
+ pathname === "/v1/enrollments/token/exchange" ||
3531
+ // The runner authenticates this protocol with its install-key proof, not
3532
+ // a browser cookie or the product-client revision header.
3533
+ pathname === "/v1/enrollments/renew"
1834
3534
  ) {
1835
3535
  return false;
1836
3536
  }
1837
- return !pathname.split("/").includes("mcp");
3537
+ const segments = pathname.split("/");
3538
+ return !segments.includes("mcp") && !segments.includes("codemode");
3539
+ }
3540
+
3541
+ /** Client-safe agent-configuration projection. */
3542
+ function clientAgentConfig(settings: Settings): ClientAgentConfig {
3543
+ const policy = agentConfigDeploymentPolicy(settings);
3544
+ return {
3545
+ // Deprecated: agent configuration is always on. Kept for client compatibility.
3546
+ enabled: true,
3547
+ defaultForNewSessions: true,
3548
+ capabilities: AGENT_CAPABILITY_IDS.map((id) => {
3549
+ const reason = policy.unavailable[id];
3550
+ return reason === undefined ? { id, available: true } : { id, available: false, reason };
3551
+ }),
3552
+ };
1838
3553
  }