@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
@@ -0,0 +1,397 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import {
3
+ CallToolRequestSchema,
4
+ ListToolsRequestSchema,
5
+ type CallToolResult,
6
+ } from "@modelcontextprotocol/sdk/types.js";
7
+ import * as contracts from "@opengeni/contracts";
8
+ import {
9
+ isReadOnlyPermissionSet,
10
+ OPENGENI_API_CONTRACT_HEADER,
11
+ OPENGENI_API_CONTRACT_REVISION,
12
+ type OrganizationAccessPolicy,
13
+ } from "@opengeni/contracts";
14
+ import { stampDelegatedHumanAuthorization } from "@opengeni/core";
15
+ import { z } from "zod";
16
+
17
+ import type { ActionCatalogEntry } from "./mcp/action-catalog-types";
18
+ import { ACTION_CATALOG } from "./mcp/action-catalog.gen";
19
+
20
+ /* ----------------------------------------------------------------------------
21
+ The organization MCP server (`/v1/mcp`).
22
+
23
+ Every public API action is reachable through three tools: find an action,
24
+ describe it, run it. A call runs the real route in this process as the
25
+ caller, so the route's own permission checks decide, exactly as for the
26
+ app:
27
+
28
+ - a person who signed in (MCP OAuth): the request carries the verified
29
+ person proof, re-resolving their live access and capping it by the
30
+ connection's access setting on every call;
31
+ - an organization API key: the request carries that key.
32
+
33
+ The proof lives on the exact Request object in process memory; nothing a
34
+ client sends can create it.
35
+ -------------------------------------------------------------------------- */
36
+
37
+ export type OrganizationMcpCaller =
38
+ | {
39
+ kind: "person";
40
+ accountId: string;
41
+ subjectId: string;
42
+ access: OrganizationAccessPolicy;
43
+ }
44
+ | { kind: "key"; authorization: string; accessKey: string | null };
45
+
46
+ /**
47
+ * Requests that only read but are sent as POST (a search query or a file path
48
+ * in the body). Read only connections may run them; each route still checks
49
+ * its own read permission.
50
+ */
51
+ export const READ_ONLY_POST_ACTIONS: ReadonlySet<string> = new Set([
52
+ "/v1/workspaces/:workspaceId/knowledge/search",
53
+ "/v1/workspaces/:workspaceId/knowledge/entries/search",
54
+ "/v1/workspaces/:workspaceId/document-bases/:baseId/search",
55
+ "/v1/workspaces/:workspaceId/sessions/:sessionId/fs/read",
56
+ "/v1/workspaces/:workspaceId/sessions/:sessionId/git/diff",
57
+ "/v1/workspaces/:workspaceId/integrations/preview",
58
+ "/v1/workspaces/:workspaceId/plugins/preview",
59
+ "/v1/workspaces/:workspaceId/skills/preview",
60
+ "/v1/organizations/:organizationId/external-identities/lookup",
61
+ ]);
62
+
63
+ const MAX_RESPONSE_BYTES = 256 * 1024;
64
+ const STREAM_READ_MS = 2_000;
65
+
66
+ const SearchInput = z
67
+ .object({
68
+ query: z.string().max(200).default(""),
69
+ limit: z.number().int().min(1).max(50).default(20),
70
+ offset: z.number().int().min(0).default(0),
71
+ })
72
+ .strict();
73
+ const DescribeInput = z.object({ id: z.string().min(1).max(300) }).strict();
74
+ const CallInput = z
75
+ .object({
76
+ id: z.string().min(1).max(300),
77
+ pathParameters: z.record(z.string(), z.string().min(1).max(1024)).default({}),
78
+ query: z
79
+ .record(
80
+ z.string(),
81
+ z.union([z.string(), z.number(), z.boolean(), z.array(z.union([z.string(), z.number()]))]),
82
+ )
83
+ .default({}),
84
+ body: z.unknown().optional(),
85
+ })
86
+ .strict();
87
+
88
+ const TOOLS = [
89
+ {
90
+ name: "opengeni_actions_search",
91
+ title: "Find Opengeni actions",
92
+ description:
93
+ "Search every action available in Opengeni: sessions and chats, workspaces, people, knowledge, files, schedules, connections, models, keys, billing and settings. Returns action ids to describe and call. An empty query lists everything, page by page.",
94
+ inputSchema: z.toJSONSchema(SearchInput, { io: "input" }),
95
+ annotations: { readOnlyHint: true },
96
+ },
97
+ {
98
+ name: "opengeni_action_describe",
99
+ title: "Describe an Opengeni action",
100
+ description:
101
+ "Show an action's method, path, path parameters and the JSON schema of its input, so it can be called correctly.",
102
+ inputSchema: z.toJSONSchema(DescribeInput, { io: "input" }),
103
+ annotations: { readOnlyHint: true },
104
+ },
105
+ {
106
+ name: "opengeni_action_call",
107
+ title: "Run an Opengeni action",
108
+ description:
109
+ "Run one Opengeni action as this connection, with exactly the access it was given. Pass path parameters (for example workspaceId), query parameters and a JSON body. Returns the HTTP status and response.",
110
+ inputSchema: z.toJSONSchema(CallInput, { io: "input" }),
111
+ annotations: { readOnlyHint: false, openWorldHint: false },
112
+ },
113
+ ] as const;
114
+
115
+ export function buildOrganizationMcpServer(input: {
116
+ caller: OrganizationMcpCaller;
117
+ /** This API's own origin; the call never leaves the process. */
118
+ origin: string;
119
+ dispatch: (request: Request) => Promise<Response>;
120
+ signal?: AbortSignal;
121
+ }): Server {
122
+ const server = new Server(
123
+ { name: "opengeni", version: "1.0.0" },
124
+ {
125
+ capabilities: { tools: {} },
126
+ instructions:
127
+ "Opengeni actions: use opengeni_actions_search to find what you need, opengeni_action_describe for its inputs, then opengeni_action_call. Most actions take a workspaceId path parameter; list workspaces first (search 'workspaces').",
128
+ },
129
+ );
130
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
131
+ tools: TOOLS.map((tool) => ({ ...tool, inputSchema: tool.inputSchema as never })),
132
+ }));
133
+ server.setRequestHandler(CallToolRequestSchema, async (request): Promise<CallToolResult> => {
134
+ const args = request.params.arguments ?? {};
135
+ switch (request.params.name) {
136
+ case "opengeni_actions_search": {
137
+ const parsed = SearchInput.safeParse(args);
138
+ if (!parsed.success) return invalid(parsed.error);
139
+ return json(searchActions(parsed.data));
140
+ }
141
+ case "opengeni_action_describe": {
142
+ const parsed = DescribeInput.safeParse(args);
143
+ if (!parsed.success) return invalid(parsed.error);
144
+ const entry = findAction(parsed.data.id);
145
+ if (!entry) return failure(`No action "${parsed.data.id}". Search for it first.`);
146
+ return json(describeAction(entry));
147
+ }
148
+ case "opengeni_action_call": {
149
+ const parsed = CallInput.safeParse(args);
150
+ if (!parsed.success) return invalid(parsed.error);
151
+ return await callAction(input, parsed.data);
152
+ }
153
+ default:
154
+ return failure(`Unknown tool ${request.params.name}.`);
155
+ }
156
+ });
157
+ return server;
158
+ }
159
+
160
+ export function findAction(id: string): ActionCatalogEntry | undefined {
161
+ const trimmed = id.trim();
162
+ return ACTION_CATALOG.find(
163
+ (entry) => entry.id === trimmed || `${entry.method} ${entry.path}` === trimmed,
164
+ );
165
+ }
166
+
167
+ function words(value: string): string[] {
168
+ return value
169
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
170
+ .toLowerCase()
171
+ .split(/[^a-z0-9]+/u)
172
+ .filter(Boolean);
173
+ }
174
+
175
+ export function searchActions(input: { query: string; limit: number; offset: number }) {
176
+ // Singular and plural match ("workspace" finds listWorkspaces).
177
+ const stem = (word: string) =>
178
+ word.endsWith("ies") ? `${word.slice(0, -3)}y` : word.replace(/(?<!s)s$/u, "");
179
+ const wanted = words(input.query).map(stem);
180
+ // "workspaces" asks for a list; prefer list actions on a plural query.
181
+ const plural = words(input.query).some((word) => /[^s]s$/u.test(word));
182
+ const scored = ACTION_CATALOG.map((entry) => {
183
+ // A word in the action's name matters far more than one in its path:
184
+ // nearly every path contains "workspaces".
185
+ const name = words(entry.id).map(stem);
186
+ const path = [...words(entry.path.replace(/:\w+/g, "")), entry.method.toLowerCase()].map(stem);
187
+ const score = wanted.reduce(
188
+ (total, word) =>
189
+ total +
190
+ (name.includes(word)
191
+ ? 4
192
+ : name.some((token) => token.startsWith(word))
193
+ ? 3
194
+ : path.includes(word)
195
+ ? 1
196
+ : path.some((token) => token.startsWith(word))
197
+ ? 0.5
198
+ : 0),
199
+ 0,
200
+ );
201
+ return {
202
+ entry,
203
+ score: score + (plural && score > 0 && name[0] === "list" ? 0.5 : 0),
204
+ size: name.length,
205
+ };
206
+ }).filter((candidate) => wanted.length === 0 || candidate.score > 0);
207
+ scored.sort(
208
+ (left, right) =>
209
+ right.score - left.score ||
210
+ left.size - right.size ||
211
+ // On a tie, reads come before writes.
212
+ Number(left.entry.method !== "GET") - Number(right.entry.method !== "GET") ||
213
+ left.entry.id.localeCompare(right.entry.id),
214
+ );
215
+ return {
216
+ total: scored.length,
217
+ offset: input.offset,
218
+ actions: scored.slice(input.offset, input.offset + input.limit).map(({ entry }) => ({
219
+ id: entry.id,
220
+ method: entry.method,
221
+ path: entry.path,
222
+ })),
223
+ };
224
+ }
225
+
226
+ function pathParameters(path: string): string[] {
227
+ return [...path.matchAll(/:(\w+)/g)].map((match) => match[1]!);
228
+ }
229
+
230
+ function schemaFor(name: string): unknown {
231
+ const candidate = (contracts as Record<string, unknown>)[name];
232
+ if (!(candidate instanceof z.ZodType)) return null;
233
+ try {
234
+ return z.toJSONSchema(candidate, { io: "input", unrepresentable: "any" });
235
+ } catch {
236
+ return null;
237
+ }
238
+ }
239
+
240
+ export function describeAction(entry: ActionCatalogEntry) {
241
+ const reads = entry.method === "GET" || entry.method === "HEAD";
242
+ return {
243
+ id: entry.id,
244
+ method: entry.method,
245
+ path: entry.path,
246
+ pathParameters: pathParameters(entry.path),
247
+ input: entry.request.map((name) => ({
248
+ name,
249
+ in: reads ? "query" : "body",
250
+ schema: schemaFor(name),
251
+ })),
252
+ response: entry.response,
253
+ };
254
+ }
255
+
256
+ async function callAction(
257
+ context: Parameters<typeof buildOrganizationMcpServer>[0],
258
+ input: z.infer<typeof CallInput>,
259
+ ): Promise<CallToolResult> {
260
+ const entry = findAction(input.id);
261
+ if (!entry) return failure(`No action "${input.id}". Search for it first.`);
262
+ const reads = entry.method === "GET" || entry.method === "HEAD";
263
+ if (
264
+ context.caller.kind === "person" &&
265
+ !reads &&
266
+ !READ_ONLY_POST_ACTIONS.has(entry.path) &&
267
+ isReadOnlyPermissionSet(context.caller.access.permissions)
268
+ ) {
269
+ return failure("This connection is read only, so it can't change anything.");
270
+ }
271
+ let path = entry.path;
272
+ for (const name of pathParameters(entry.path)) {
273
+ const value = input.pathParameters[name];
274
+ if (!value) return failure(`Missing path parameter ${name}.`);
275
+ // Encoding leaves "." alone, so a dot segment would reach another route.
276
+ if (value === "." || value === "..") return failure(`Invalid path parameter ${name}.`);
277
+ path = path.replace(`:${name}`, encodeURIComponent(value));
278
+ }
279
+ const url = new URL(path, context.origin);
280
+ if (url.pathname !== path) return failure("Invalid path parameters.");
281
+ for (const [key, value] of Object.entries(input.query)) {
282
+ for (const each of Array.isArray(value) ? value : [value])
283
+ url.searchParams.append(key, String(each));
284
+ }
285
+ const headers = new Headers({
286
+ accept: "application/json",
287
+ [OPENGENI_API_CONTRACT_HEADER]: OPENGENI_API_CONTRACT_REVISION,
288
+ });
289
+ if (context.caller.kind === "key") {
290
+ headers.set("authorization", context.caller.authorization);
291
+ if (context.caller.accessKey) headers.set("x-opengeni-access-key", context.caller.accessKey);
292
+ }
293
+ const body = reads ? undefined : JSON.stringify(input.body ?? {});
294
+ if (body !== undefined) {
295
+ headers.set("content-type", "application/json");
296
+ // Exact length: the body-limit middleware otherwise rebuilds the Request,
297
+ // and the person proof lives on this exact object.
298
+ headers.set("content-length", String(Buffer.byteLength(body)));
299
+ }
300
+ const request = new Request(url, {
301
+ method: entry.method,
302
+ headers,
303
+ ...(body !== undefined ? { body } : {}),
304
+ ...(context.signal ? { signal: context.signal } : {}),
305
+ redirect: "manual",
306
+ });
307
+ if (context.caller.kind === "person") {
308
+ stampDelegatedHumanAuthorization(request, {
309
+ organizationId: context.caller.accountId,
310
+ subjectId: context.caller.subjectId,
311
+ permissions: context.caller.access.permissions,
312
+ workspaceScope: context.caller.access.workspaceScope,
313
+ });
314
+ }
315
+ const response = await context.dispatch(request);
316
+ return await toolResult(response);
317
+ }
318
+
319
+ async function toolResult(response: Response): Promise<CallToolResult> {
320
+ const status = response.status;
321
+ const location = response.headers.get("location");
322
+ if (status >= 300 && status < 400 && location) {
323
+ await response.body?.cancel();
324
+ return json({
325
+ status,
326
+ openInBrowser: location,
327
+ hint: "Open this link in a browser to finish.",
328
+ });
329
+ }
330
+ const contentType = response.headers.get("content-type") ?? "";
331
+ const result: Record<string, unknown> = { status };
332
+ if (/text\/event-stream/i.test(contentType)) {
333
+ result.events = await readStream(response);
334
+ } else {
335
+ const bytes = new Uint8Array(await response.arrayBuffer());
336
+ const truncated = bytes.byteLength > MAX_RESPONSE_BYTES;
337
+ const kept = truncated ? bytes.slice(0, MAX_RESPONSE_BYTES) : bytes;
338
+ if (/json/i.test(contentType) && !truncated) {
339
+ result.body = JSON.parse(new TextDecoder().decode(kept) || "null");
340
+ } else if (/^text\/|json|xml|yaml|javascript/i.test(contentType) || contentType === "") {
341
+ result.body = new TextDecoder().decode(kept);
342
+ } else {
343
+ result.contentType = contentType;
344
+ result.base64 = Buffer.from(kept).toString("base64");
345
+ }
346
+ if (truncated)
347
+ result.truncated = { returnedBytes: MAX_RESPONSE_BYTES, totalBytes: bytes.byteLength };
348
+ }
349
+ if (
350
+ status === 403 &&
351
+ /human|browser|cookie|same-origin/i.test(JSON.stringify(result.body ?? ""))
352
+ ) {
353
+ result.hint = "This action has to be done by the person in the Opengeni app in a browser.";
354
+ }
355
+ return {
356
+ content: [{ type: "text", text: JSON.stringify(result) }],
357
+ ...(status >= 400 ? { isError: true } : {}),
358
+ };
359
+ }
360
+
361
+ async function readStream(response: Response): Promise<string> {
362
+ const reader = response.body?.getReader();
363
+ if (!reader) return "";
364
+ const decoder = new TextDecoder();
365
+ let text = "";
366
+ const deadline = Date.now() + STREAM_READ_MS;
367
+ try {
368
+ while (Date.now() < deadline && text.length < MAX_RESPONSE_BYTES) {
369
+ const next = await Promise.race([
370
+ reader.read(),
371
+ new Promise<{ done: true; value: undefined }>((resolve) =>
372
+ setTimeout(
373
+ () => resolve({ done: true, value: undefined }),
374
+ Math.max(0, deadline - Date.now()),
375
+ ),
376
+ ),
377
+ ]);
378
+ if (next.done) break;
379
+ text += decoder.decode(next.value, { stream: true });
380
+ }
381
+ } finally {
382
+ await reader.cancel().catch(() => undefined);
383
+ }
384
+ return text;
385
+ }
386
+
387
+ function json(value: unknown): CallToolResult {
388
+ return { content: [{ type: "text", text: JSON.stringify(value) }] };
389
+ }
390
+
391
+ function failure(message: string): CallToolResult {
392
+ return { content: [{ type: "text", text: message }], isError: true };
393
+ }
394
+
395
+ function invalid(error: z.ZodError): CallToolResult {
396
+ return failure(`Invalid input: ${z.prettifyError(error)}`);
397
+ }
@@ -0,0 +1,103 @@
1
+ import type { Database } from "@opengeni/db";
2
+ import {
3
+ prepareOrganizationRecoveryNotifications,
4
+ settleOrganizationRecoveryNotification,
5
+ type OrganizationRecoveryNotificationClaim,
6
+ type OrganizationRecoveryNotificationSettlement,
7
+ } from "@opengeni/db";
8
+
9
+ export type OrganizationRecoveryNotificationDeliveryResult =
10
+ | { status: "sent"; providerMessageId: string | null }
11
+ | { status: "failed"; errorClass: string }
12
+ | { status: "outcome_unknown" };
13
+
14
+ export interface OrganizationRecoveryNotificationTransport {
15
+ readonly provider: string;
16
+ send(
17
+ claim: OrganizationRecoveryNotificationClaim,
18
+ ): Promise<OrganizationRecoveryNotificationDeliveryResult>;
19
+ }
20
+
21
+ export type OrganizationRecoveryNotificationLifecycle = {
22
+ prepare: typeof prepareOrganizationRecoveryNotifications;
23
+ settle: typeof settleOrganizationRecoveryNotification;
24
+ };
25
+
26
+ const productionLifecycle: OrganizationRecoveryNotificationLifecycle = {
27
+ prepare: prepareOrganizationRecoveryNotifications,
28
+ settle: settleOrganizationRecoveryNotification,
29
+ };
30
+
31
+ export async function dispatchOrganizationRecoveryNotifications(input: {
32
+ db: Database;
33
+ transport: OrganizationRecoveryNotificationTransport;
34
+ claimOwner: string;
35
+ limit?: number;
36
+ leaseSeconds?: number;
37
+ lifecycle?: OrganizationRecoveryNotificationLifecycle;
38
+ }): Promise<OrganizationRecoveryNotificationSettlement[]> {
39
+ const lifecycle = input.lifecycle ?? productionLifecycle;
40
+ const claims = await lifecycle.prepare(input.db, {
41
+ provider: input.transport.provider,
42
+ claimOwner: input.claimOwner,
43
+ limit: input.limit ?? 25,
44
+ leaseSeconds: input.leaseSeconds ?? 60,
45
+ });
46
+ return await Promise.all(
47
+ claims.map(async (claim) => {
48
+ let result: OrganizationRecoveryNotificationDeliveryResult;
49
+ try {
50
+ result = await input.transport.send(claim);
51
+ } catch {
52
+ // The provider may have accepted the stable idempotency key before the
53
+ // transport failed. Preserve ambiguity for explicit reconciliation.
54
+ result = { status: "outcome_unknown" };
55
+ }
56
+ return await lifecycle.settle(input.db, {
57
+ outboxId: claim.outboxId,
58
+ deliveryId: claim.deliveryId,
59
+ claimOwner: claim.claimOwner,
60
+ phase: result.status,
61
+ ...(result.status === "sent" ? { providerMessageId: result.providerMessageId } : {}),
62
+ ...(result.status === "failed" ? { errorClass: result.errorClass } : {}),
63
+ });
64
+ }),
65
+ );
66
+ }
67
+
68
+ export class InMemoryOrganizationRecoveryNotificationTransport implements OrganizationRecoveryNotificationTransport {
69
+ readonly provider = "fake";
70
+ readonly attempts: Array<{
71
+ idempotencyKey: string;
72
+ payloadDigest: string;
73
+ recipientCanonicalIdentityId: string;
74
+ providerMessageId: string;
75
+ }> = [];
76
+ private readonly deliveries = new Map<string, string>();
77
+ private readonly scripted: OrganizationRecoveryNotificationDeliveryResult[] = [];
78
+
79
+ enqueue(...results: OrganizationRecoveryNotificationDeliveryResult[]): void {
80
+ this.scripted.push(...results);
81
+ }
82
+
83
+ logicalDeliveryCount(): number {
84
+ return this.deliveries.size;
85
+ }
86
+
87
+ async send(
88
+ claim: OrganizationRecoveryNotificationClaim,
89
+ ): Promise<OrganizationRecoveryNotificationDeliveryResult> {
90
+ const scripted = this.scripted.shift();
91
+ if (scripted) return scripted;
92
+ const providerMessageId =
93
+ this.deliveries.get(claim.idempotencyKey) ?? `fake:${crypto.randomUUID()}`;
94
+ this.deliveries.set(claim.idempotencyKey, providerMessageId);
95
+ this.attempts.push({
96
+ idempotencyKey: claim.idempotencyKey,
97
+ payloadDigest: claim.payloadDigest,
98
+ recipientCanonicalIdentityId: claim.recipientCanonicalIdentityId,
99
+ providerMessageId,
100
+ });
101
+ return { status: "sent", providerMessageId };
102
+ }
103
+ }
@@ -0,0 +1,123 @@
1
+ // Content-free analytics consent beacon.
2
+ //
3
+ // When a person answers the web console's optional-analytics banner, the
4
+ // browser reports only the closed decision (`granted` or `denied`). Each
5
+ // accepted report increments `opengeni_analytics_consent_total{decision}`, so
6
+ // product reports can state how much of the audience the consent-gated
7
+ // analytics providers never see. The body carries no identifier, URL, cookie,
8
+ // or user content, the browser sends it with `credentials: "omit"`, and the
9
+ // route writes no log line. It admits anonymous callers because the banner is
10
+ // answered before sign-in too. Any HTTP client that omits `Origin` can still
11
+ // send reports up to the admission ceiling, so read the counter as a ratio of
12
+ // decisions, not as a count of people.
13
+ import type { Settings } from "@opengeni/config";
14
+ import {
15
+ ANALYTICS_CONSENT_DECISIONS,
16
+ ANALYTICS_CONSENT_PATH,
17
+ ANALYTICS_CONSENT_REPORT_MAX_BYTES,
18
+ type AnalyticsConsentDecision,
19
+ } from "@opengeni/contracts/analytics-consent-report";
20
+ import type { Observability } from "@opengeni/observability";
21
+ import type { Context, Hono } from "hono";
22
+ import { bodyLimit } from "hono/body-limit";
23
+ import { z } from "zod";
24
+
25
+ import { isAllowedBrowserOrigin } from "../http/cors";
26
+ import { createKeyedAdmission, type KeyedAdmission } from "../http/keyed-admission";
27
+
28
+ export { ANALYTICS_CONSENT_DECISIONS, ANALYTICS_CONSENT_PATH, type AnalyticsConsentDecision };
29
+
30
+ const REJECTION_REASONS = ["invalid", "too_large", "origin", "rate_limited"] as const;
31
+ type RejectionReason = (typeof REJECTION_REASONS)[number];
32
+
33
+ export const AnalyticsConsentReport = z
34
+ .object({ decision: z.enum(ANALYTICS_CONSENT_DECISIONS) })
35
+ .strict();
36
+ export type AnalyticsConsentReport = z.infer<typeof AnalyticsConsentReport>;
37
+
38
+ const CONSENT_METRIC = {
39
+ name: "opengeni_analytics_consent_total",
40
+ help: "Answers to the web console's optional-analytics banner, by closed decision. Admission is bounded per API process, so this is a lower bound.",
41
+ } as const;
42
+ const REJECTIONS_METRIC = {
43
+ name: "opengeni_analytics_consent_reports_rejected_total",
44
+ help: "Analytics consent reports the API refused, by closed reason.",
45
+ } as const;
46
+
47
+ export type AnalyticsConsentAdmission = KeyedAdmission<AnalyticsConsentDecision>;
48
+
49
+ export function parseAnalyticsConsentReport(body: string | null): AnalyticsConsentReport | null {
50
+ if (
51
+ body === null ||
52
+ new TextEncoder().encode(body).byteLength > ANALYTICS_CONSENT_REPORT_MAX_BYTES
53
+ ) {
54
+ return null;
55
+ }
56
+ let value: unknown;
57
+ try {
58
+ value = JSON.parse(body);
59
+ } catch {
60
+ return null;
61
+ }
62
+ const parsed = AnalyticsConsentReport.safeParse(value);
63
+ return parsed.success ? parsed.data : null;
64
+ }
65
+
66
+ /**
67
+ * The exact beacon request. The app-wide request-body limit skips it because
68
+ * the route enforces its own small limit on the streamed body.
69
+ */
70
+ export function isAnalyticsConsentReportRequest(method: string, pathname: string): boolean {
71
+ return method === "POST" && pathname === ANALYTICS_CONSENT_PATH;
72
+ }
73
+
74
+ export function registerAnalyticsConsentRoutes(
75
+ app: Hono,
76
+ deps: {
77
+ observability: Observability;
78
+ settings: Pick<Settings, "corsAllowOriginRegex" | "publicBaseUrl" | "webBaseUrl">;
79
+ admission?: AnalyticsConsentAdmission;
80
+ },
81
+ ): void {
82
+ const { observability, settings } = deps;
83
+ // Consent answers are rare (once per browser), so a small burst and a slow
84
+ // refill still cover a launch-day spike on each API process.
85
+ const admission =
86
+ deps.admission ??
87
+ createKeyedAdmission<AnalyticsConsentDecision>({ capacity: 60, refillPerSecond: 1 });
88
+ // Publish every finite series at zero so the first answer after a deploy is
89
+ // an increase from a baseline rather than a series appearing from nothing.
90
+ for (const decision of ANALYTICS_CONSENT_DECISIONS) {
91
+ observability.incrementCounter({ ...CONSENT_METRIC, labels: { decision }, amount: 0 });
92
+ }
93
+ for (const reason of REJECTION_REASONS) {
94
+ observability.incrementCounter({ ...REJECTIONS_METRIC, labels: { reason }, amount: 0 });
95
+ }
96
+ const reject = (c: Context, reason: RejectionReason, status: 400 | 403 | 413 | 429) => {
97
+ observability.incrementCounter({ ...REJECTIONS_METRIC, labels: { reason } });
98
+ c.header("cache-control", "no-store");
99
+ return c.body(null, status);
100
+ };
101
+
102
+ app.post(
103
+ ANALYTICS_CONSENT_PATH,
104
+ bodyLimit({
105
+ maxSize: ANALYTICS_CONSENT_REPORT_MAX_BYTES,
106
+ onError: (c) => reject(c, "too_large", 413),
107
+ }),
108
+ async (c) => {
109
+ c.header("cache-control", "no-store");
110
+ // Browsers always send Origin on this POST, so a foreign page cannot
111
+ // inflate the counter through its visitors' browsers.
112
+ const origin = c.req.header("origin");
113
+ if (origin !== undefined && !isAllowedBrowserOrigin(origin, settings)) {
114
+ return reject(c, "origin", 403);
115
+ }
116
+ const report = parseAnalyticsConsentReport(await c.req.text().catch(() => null));
117
+ if (!report) return reject(c, "invalid", 400);
118
+ if (!admission.admit(report.decision)) return reject(c, "rate_limited", 429);
119
+ observability.incrementCounter({ ...CONSENT_METRIC, labels: { decision: report.decision } });
120
+ return c.body(null, 204);
121
+ },
122
+ );
123
+ }