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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (546) hide show
  1. package/dist/access-grant-rls.d.ts +8 -1
  2. package/dist/admission-trace.d.ts +8 -0
  3. package/dist/app.d.ts +50 -3
  4. package/dist/app.js +10 -1
  5. package/dist/artifact-catalog-cursor.d.ts +1 -0
  6. package/dist/attached-browser-viewer.d.ts +11 -0
  7. package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
  8. package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
  9. package/dist/auth/managed-auth-session-adapter.d.ts +9 -1
  10. package/dist/auth/managed-auth.d.ts +72 -5
  11. package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
  12. package/dist/auth/new-signups-gate.d.ts +23 -0
  13. package/dist/auth/signup-funnel-metrics.d.ts +44 -0
  14. package/dist/azure-live.d.ts +28 -0
  15. package/dist/browser-action-fences.d.ts +13 -0
  16. package/dist/browser-controller-authority.d.ts +1 -61
  17. package/dist/browser-ephemeral.d.ts +5 -0
  18. package/dist/browser-lost-runtime-end.d.ts +24 -0
  19. package/dist/browser-state-authority.d.ts +1 -27
  20. package/dist/{chunk-AJ62T5QN.js → chunk-GBUJ2DYZ.js} +80507 -39878
  21. package/dist/chunk-GBUJ2DYZ.js.map +1 -0
  22. package/dist/chunk-OGQDXVXY.js +18 -0
  23. package/dist/chunk-OGQDXVXY.js.map +1 -0
  24. package/dist/claude-default-models.d.ts +19 -0
  25. package/dist/claude-subscription-core.d.ts +50 -0
  26. package/dist/claude-subscription-oauth.d.ts +36 -0
  27. package/dist/claude-subscription-profile.d.ts +9 -0
  28. package/dist/claude-subscription-usage.d.ts +6 -0
  29. package/dist/claude-workspace-connection.d.ts +15 -0
  30. package/dist/codemode.d.ts +40 -5
  31. package/dist/codex-realtime.d.ts +29 -3
  32. package/dist/codex-session-pointers.d.ts +19 -0
  33. package/dist/connection-ownership.d.ts +6 -3
  34. package/dist/controller-data-plane.d.ts +16 -0
  35. package/dist/credit-promotion-snapshot.d.ts +6 -0
  36. package/dist/editable-artifact-source-session.d.ts +3 -0
  37. package/dist/editable-artifact-websocket.d.ts +5 -3
  38. package/dist/fatal-process-boundary.d.ts +9 -1
  39. package/dist/github-access.d.ts +7 -1
  40. package/dist/github-browser-flow.d.ts +1 -1
  41. package/dist/http/acting-person.d.ts +36 -0
  42. package/dist/http/api-contract-compat.d.ts +19 -0
  43. package/dist/http/api-error.d.ts +50 -1
  44. package/dist/http/auth.d.ts +8 -1
  45. package/dist/http/cors.d.ts +12 -0
  46. package/dist/http/deprecation.d.ts +42 -0
  47. package/dist/http/keyed-admission.d.ts +12 -0
  48. package/dist/http/local-browser-boundary.d.ts +72 -0
  49. package/dist/http/metrics-listener.d.ts +22 -0
  50. package/dist/http/path-identifier.d.ts +17 -0
  51. package/dist/http/registered-route-label.d.ts +16 -0
  52. package/dist/http/rejection-telemetry.d.ts +34 -0
  53. package/dist/http/request-body.d.ts +27 -0
  54. package/dist/http/request-source.d.ts +41 -3
  55. package/dist/http/sse.d.ts +12 -1
  56. package/dist/http/unmatched-route.d.ts +18 -0
  57. package/dist/http/user-content.d.ts +36 -0
  58. package/dist/http/variable-sets-removed.d.ts +41 -0
  59. package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
  60. package/dist/index.js +1279 -108
  61. package/dist/index.js.map +1 -1
  62. package/dist/integration-connect-metrics.d.ts +35 -0
  63. package/dist/integration-endpoint-test.d.ts +20 -0
  64. package/dist/integrations/api-integrations.d.ts +2 -2
  65. package/dist/integrations/atlassian.d.ts +8 -3
  66. package/dist/integrations/fiken.d.ts +3 -1
  67. package/dist/integrations/github-lens-connect.d.ts +37 -66
  68. package/dist/integrations/google-drive.d.ts +2 -0
  69. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  70. package/dist/integrations/marketplace-plugin.d.ts +43 -43
  71. package/dist/integrations/oauth-client.d.ts +171 -12
  72. package/dist/integrations/oauth-profiles.d.ts +42 -13
  73. package/dist/integrations/onepassword-vault-provider.d.ts +9 -0
  74. package/dist/integrations/slack-bot.d.ts +285 -7
  75. package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
  76. package/dist/integrations/slack-interactions.d.ts +89 -10
  77. package/dist/integrations/slack-mrkdwn.d.ts +41 -0
  78. package/dist/integrations/slack-routing.d.ts +1 -1
  79. package/dist/integrations/slack-session-defaults.d.ts +58 -0
  80. package/dist/integrations/slack-task-file-upload.d.ts +36 -0
  81. package/dist/integrations/slack-tool-review.d.ts +5 -0
  82. package/dist/interaction-agent-access.d.ts +13 -8
  83. package/dist/interaction-agent-version.d.ts +21 -0
  84. package/dist/interaction-holder-heartbeat.d.ts +5 -0
  85. package/dist/interaction-metrics.d.ts +27 -2
  86. package/dist/interaction-route-stages.d.ts +25 -0
  87. package/dist/interaction-secrets-vault.d.ts +20 -0
  88. package/dist/interaction-secrets.d.ts +103 -0
  89. package/dist/mcp/action-catalog-types.d.ts +14 -0
  90. package/dist/mcp/action-catalog.gen.d.ts +2 -0
  91. package/dist/mcp/action-descriptions.d.ts +4 -0
  92. package/dist/mcp/action-schema-imports.gen.d.ts +1 -0
  93. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  94. package/dist/mcp/child-read-evidence.d.ts +9 -0
  95. package/dist/mcp/company-profile-agent-admin.d.ts +12 -0
  96. package/dist/mcp/contract-input.d.ts +13 -0
  97. package/dist/mcp/documents.d.ts +2 -0
  98. package/dist/mcp/first-party-tool-permissions.d.ts +3 -606
  99. package/dist/mcp/notification-tools.d.ts +26 -0
  100. package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
  101. package/dist/mcp/scheduled-task-input.d.ts +1309 -0
  102. package/dist/mcp/scheduled-task-view.d.ts +217 -3
  103. package/dist/mcp/secret-tools.d.ts +14 -0
  104. package/dist/mcp/server.d.ts +108 -5
  105. package/dist/mcp/session-admin-access-tools.d.ts +33 -0
  106. package/dist/mcp/session-event-errors.d.ts +28 -0
  107. package/dist/mcp/session-event-view.d.ts +62 -0
  108. package/dist/mcp/session-view.d.ts +15 -0
  109. package/dist/mcp/session-wait.d.ts +18 -0
  110. package/dist/mcp/site-agent-clamp-tools.d.ts +38 -0
  111. package/dist/mcp/site-data-tools.d.ts +22 -0
  112. package/dist/mcp/site-publication-tools.d.ts +19 -0
  113. package/dist/mcp/site-sql-tools.d.ts +27 -0
  114. package/dist/mcp-oauth.d.ts +37 -4
  115. package/dist/model-calls/chat-completions.d.ts +87 -0
  116. package/dist/model-calls/pending-usage-settlements.d.ts +57 -0
  117. package/dist/model-calls/service.d.ts +36 -0
  118. package/dist/model-catalog.d.ts +9 -3
  119. package/dist/native-push-dispatch.d.ts +69 -0
  120. package/dist/observability.d.ts +3 -1
  121. package/dist/organization-mcp.d.ts +82 -0
  122. package/dist/prepared-mcp-actor.d.ts +77 -0
  123. package/dist/prepared-mcp-permissions.d.ts +8 -0
  124. package/dist/routes/analytics-consent.d.ts +26 -0
  125. package/dist/routes/api-integrations.d.ts +11 -0
  126. package/dist/routes/api-keys.d.ts +7 -7
  127. package/dist/routes/automations.d.ts +2 -0
  128. package/dist/routes/billing.d.ts +59 -0
  129. package/dist/routes/browser-sessions.d.ts +65 -2
  130. package/dist/routes/chat-completions.d.ts +11 -0
  131. package/dist/routes/claude-core-usage.d.ts +20 -0
  132. package/dist/routes/claude-subscription-accounts.d.ts +3 -0
  133. package/dist/routes/claude-subscription-oauth.d.ts +3 -0
  134. package/dist/routes/client-errors.d.ts +182 -0
  135. package/dist/routes/codex-core.d.ts +401 -0
  136. package/dist/routes/codex.d.ts +64 -2
  137. package/dist/routes/computer-sessions.d.ts +6 -1
  138. package/dist/routes/desktop-distribution.d.ts +32 -0
  139. package/dist/routes/github-browser-fonts.gen.d.ts +2 -0
  140. package/dist/routes/github-browser-pages.d.ts +27 -0
  141. package/dist/routes/inbox.d.ts +13 -0
  142. package/dist/routes/insights-response-cache.d.ts +10 -0
  143. package/dist/routes/insights-usage.d.ts +12 -0
  144. package/dist/routes/insights.d.ts +28 -0
  145. package/dist/routes/interaction-placement-message.d.ts +7 -0
  146. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  147. package/dist/routes/model-account-choice.d.ts +13 -0
  148. package/dist/routes/model-account-settings.d.ts +19 -0
  149. package/dist/routes/native-app-auth.d.ts +12 -0
  150. package/dist/routes/organization-integration-policy.d.ts +8 -0
  151. package/dist/routes/organization-integrations.d.ts +6 -0
  152. package/dist/routes/organization-mcp-connections.d.ts +3 -0
  153. package/dist/routes/organization-model-defaults.d.ts +9 -0
  154. package/dist/routes/organization-service-accounts.d.ts +3 -0
  155. package/dist/routes/personal-api-keys.d.ts +3 -0
  156. package/dist/routes/session-admin-access.d.ts +3 -0
  157. package/dist/routes/session-artifact-associations.d.ts +4 -0
  158. package/dist/routes/session-history-imports.d.ts +20 -0
  159. package/dist/routes/sessions.d.ts +38 -2
  160. package/dist/routes/site-data.d.ts +18 -0
  161. package/dist/routes/site-publishing.d.ts +5 -0
  162. package/dist/routes/site-runtime.d.ts +31 -0
  163. package/dist/routes/site-sql.d.ts +16 -0
  164. package/dist/routes/slack-bot-access.d.ts +3 -0
  165. package/dist/routes/subscription-account-pools.d.ts +21 -0
  166. package/dist/routes/subscription-core-http.d.ts +5 -0
  167. package/dist/routes/subscription-pool-access.d.ts +37 -0
  168. package/dist/routes/subscription-pool-core.d.ts +153 -0
  169. package/dist/routes/supergrok.d.ts +6 -6
  170. package/dist/routes/usage-allowances.d.ts +12 -0
  171. package/dist/routes/variable-sets-removed.d.ts +2 -0
  172. package/dist/routes/vault-providers.d.ts +34 -0
  173. package/dist/routes/vaults.d.ts +3 -0
  174. package/dist/routes/workspace-integrations.d.ts +35 -0
  175. package/dist/routes/workspace-model-providers.d.ts +4 -0
  176. package/dist/routes/workspaces.d.ts +11 -2
  177. package/dist/sandbox/agent-update.d.ts +64 -0
  178. package/dist/sandbox/auth-callout.d.ts +8 -0
  179. package/dist/sandbox/channel-a.d.ts +15 -1
  180. package/dist/sandbox/enrollment.d.ts +26 -4
  181. package/dist/sandbox/machine-audience.d.ts +29 -0
  182. package/dist/sandbox/machines.d.ts +15 -2
  183. package/dist/sandbox/metrics-ingestion.d.ts +6 -2
  184. package/dist/sandbox/screen-control.d.ts +85 -0
  185. package/dist/sandbox/viewer.d.ts +5 -1
  186. package/dist/sandbox-file-artifacts.d.ts +17 -1
  187. package/dist/scheduled-task-blockers.d.ts +39 -0
  188. package/dist/scheduled-task-deletion.d.ts +3 -4
  189. package/dist/scheduled-task-lifecycle.d.ts +14 -0
  190. package/dist/secret-answers.d.ts +40 -0
  191. package/dist/session-create-observability.d.ts +5 -0
  192. package/dist/session-event-view-36GO3IL7.js +528 -0
  193. package/dist/session-event-view-36GO3IL7.js.map +1 -0
  194. package/dist/signed-state-secret.d.ts +18 -0
  195. package/dist/site-agent-guard.d.ts +53 -0
  196. package/dist/site-data/operations.d.ts +103 -0
  197. package/dist/site-data/service.d.ts +203 -0
  198. package/dist/site-frame-guard.d.ts +32 -0
  199. package/dist/site-publishing/agent-data-tools.d.ts +84 -0
  200. package/dist/site-publishing/agent-model.d.ts +58 -0
  201. package/dist/site-publishing/agent-session.d.ts +16 -0
  202. package/dist/site-publishing/content-scanner.d.ts +31 -0
  203. package/dist/site-publishing/edge-sync.d.ts +48 -0
  204. package/dist/site-publishing/gateway-signature.d.ts +56 -0
  205. package/dist/site-publishing/limits.d.ts +63 -0
  206. package/dist/site-publishing/page-tool-authority.d.ts +30 -0
  207. package/dist/site-publishing/platform-usage.d.ts +113 -0
  208. package/dist/site-publishing/platform.d.ts +21 -0
  209. package/dist/site-publishing/readiness.d.ts +62 -0
  210. package/dist/site-publishing/runtime.d.ts +179 -0
  211. package/dist/site-publishing/service.d.ts +264 -0
  212. package/dist/site-publishing/session-events.d.ts +22 -0
  213. package/dist/site-publishing/session-views.d.ts +36 -0
  214. package/dist/site-publishing/viewer.d.ts +40 -0
  215. package/dist/site-session-origin.d.ts +31 -6
  216. package/dist/site-sql/format.d.ts +47 -0
  217. package/dist/site-sql/gateway.d.ts +50 -0
  218. package/dist/site-sql/service.d.ts +213 -0
  219. package/dist/temporal-schedule-sync.d.ts +26 -0
  220. package/dist/transcription/billing-refusal.d.ts +8 -0
  221. package/dist/transcription/normalize.d.ts +23 -0
  222. package/dist/transcription/providers/azure-mai.d.ts +12 -0
  223. package/dist/transcription/providers/azure-openai.d.ts +4 -0
  224. package/dist/transcription/providers/openai.d.ts +16 -0
  225. package/dist/transcription/providers/subscription-core.d.ts +39 -0
  226. package/dist/transcription/service.d.ts +10 -1
  227. package/dist/transcription/wav-duration.d.ts +8 -0
  228. package/dist/workspace-tool-gateway-attestations.d.ts +37 -0
  229. package/dist/workspace-tool-gateway-observability.d.ts +16 -0
  230. package/dist/workspace-tool-gateway-secrets.d.ts +27 -0
  231. package/dist/workspace-tool-gateway.d.ts +102 -6
  232. package/dist/workspace-tool-target-fence.d.ts +116 -0
  233. package/dist/workspace-tool-target.d.ts +225 -0
  234. package/dist/workspace-webhook-dispatch.d.ts +26 -0
  235. package/dist/xai-subscription-core.d.ts +60 -0
  236. package/package.json +25 -20
  237. package/src/access-grant-rls.ts +64 -14
  238. package/src/admission-trace.ts +14 -0
  239. package/src/app.ts +1684 -252
  240. package/src/artifact-catalog-cursor.ts +1 -0
  241. package/src/attached-browser-viewer.ts +26 -0
  242. package/src/auth/managed-auth-attempt-context.ts +9 -0
  243. package/src/auth/managed-auth-rate-limits.ts +252 -0
  244. package/src/auth/managed-auth-session-adapter.ts +64 -29
  245. package/src/auth/managed-auth.ts +303 -35
  246. package/src/auth/managed-email.ts +18 -1
  247. package/src/auth/managed-sign-in-notifications.ts +81 -0
  248. package/src/auth/new-signups-gate.ts +49 -0
  249. package/src/auth/organization-user-setup.ts +3 -3
  250. package/src/auth/signup-funnel-metrics.ts +164 -0
  251. package/src/azure-live.ts +103 -0
  252. package/src/browser-action-fences.ts +45 -0
  253. package/src/browser-controller-authority.ts +1 -186
  254. package/src/browser-ephemeral.ts +39 -0
  255. package/src/browser-lost-runtime-end.ts +71 -0
  256. package/src/browser-state-authority.ts +1 -236
  257. package/src/claude-default-models.ts +37 -0
  258. package/src/claude-subscription-core.ts +154 -0
  259. package/src/claude-subscription-oauth.ts +352 -0
  260. package/src/claude-subscription-profile.ts +73 -0
  261. package/src/claude-subscription-usage.ts +51 -0
  262. package/src/claude-workspace-connection.ts +82 -0
  263. package/src/codemode.ts +236 -39
  264. package/src/codex-realtime.ts +181 -53
  265. package/src/codex-session-pointers.ts +80 -0
  266. package/src/company-brain-okf.ts +1 -1
  267. package/src/connected-machine-computer-access.ts +2 -1
  268. package/src/connection-ownership.ts +19 -13
  269. package/src/controller-data-plane.ts +34 -0
  270. package/src/credit-promotion-snapshot.ts +36 -0
  271. package/src/editable-artifact-production.ts +10 -0
  272. package/src/editable-artifact-source-session.ts +123 -0
  273. package/src/editable-artifact-websocket.ts +16 -3
  274. package/src/fatal-process-boundary.ts +67 -3
  275. package/src/gateway-realtime.ts +12 -8
  276. package/src/github-access.ts +31 -5
  277. package/src/github-browser-flow.ts +1 -1
  278. package/src/http/acting-person.ts +105 -0
  279. package/src/http/api-contract-compat.ts +49 -0
  280. package/src/http/api-error.ts +210 -2
  281. package/src/http/auth.ts +68 -4
  282. package/src/http/cors.ts +37 -5
  283. package/src/http/deprecation.ts +118 -0
  284. package/src/http/keyed-admission.ts +29 -0
  285. package/src/http/local-browser-boundary.ts +343 -0
  286. package/src/http/metrics-listener.ts +57 -0
  287. package/src/http/path-identifier.ts +54 -0
  288. package/src/http/registered-route-label.ts +34 -0
  289. package/src/http/rejection-telemetry.ts +120 -0
  290. package/src/http/request-body.ts +127 -0
  291. package/src/http/request-source.ts +154 -13
  292. package/src/http/sse.ts +88 -28
  293. package/src/http/unmatched-route.ts +60 -0
  294. package/src/http/user-content.ts +158 -0
  295. package/src/http/variable-sets-removed.ts +122 -0
  296. package/src/http/websocket-upgrade-dispatch.ts +29 -0
  297. package/src/index.ts +107 -57
  298. package/src/integration-connect-metrics.ts +228 -0
  299. package/src/integration-endpoint-test.ts +291 -0
  300. package/src/integrations/api-integrations.ts +81 -7
  301. package/src/integrations/atlassian.ts +202 -83
  302. package/src/integrations/fiken.ts +66 -38
  303. package/src/integrations/github-app-connect.ts +64 -18
  304. package/src/integrations/github-lens-connect.ts +8 -29
  305. package/src/integrations/google-drive.ts +448 -152
  306. package/src/integrations/integration-commit-authority.ts +69 -0
  307. package/src/integrations/marketplace-plugin.ts +1 -1
  308. package/src/integrations/oauth-client.ts +733 -193
  309. package/src/integrations/oauth-profiles.ts +116 -45
  310. package/src/integrations/onepassword-vault-provider.ts +16 -0
  311. package/src/integrations/personal-github-repositories.ts +1 -1
  312. package/src/integrations/personal-github.ts +61 -35
  313. package/src/integrations/provider-oauth.ts +67 -85
  314. package/src/integrations/slack-app-home.ts +5 -5
  315. package/src/integrations/slack-bot.ts +893 -201
  316. package/src/integrations/slack-file-upload-flow.ts +147 -0
  317. package/src/integrations/slack-install.ts +7 -7
  318. package/src/integrations/slack-interactions.ts +1589 -299
  319. package/src/integrations/slack-mrkdwn.ts +193 -0
  320. package/src/integrations/slack-routing.ts +2 -2
  321. package/src/integrations/slack-session-defaults.ts +224 -0
  322. package/src/integrations/slack-task-file-upload.ts +323 -0
  323. package/src/integrations/slack-tool-review.ts +65 -0
  324. package/src/integrations/social-oauth.ts +90 -58
  325. package/src/interaction-agent-access.ts +40 -20
  326. package/src/interaction-agent-version.ts +85 -0
  327. package/src/interaction-holder-heartbeat.ts +24 -8
  328. package/src/interaction-metrics.ts +164 -2
  329. package/src/interaction-route-stages.ts +94 -0
  330. package/src/interaction-secrets-vault.ts +213 -0
  331. package/src/interaction-secrets.ts +426 -0
  332. package/src/mcp/action-catalog-types.ts +14 -0
  333. package/src/mcp/action-catalog.gen.ts +8014 -0
  334. package/src/mcp/action-descriptions.ts +299 -0
  335. package/src/mcp/action-schema-imports.gen.ts +304 -0
  336. package/src/mcp/capability-account-readiness.ts +17 -0
  337. package/src/mcp/child-read-evidence.ts +19 -0
  338. package/src/mcp/company-profile-agent-admin.ts +13 -0
  339. package/src/mcp/contract-input.ts +165 -0
  340. package/src/mcp/documents.ts +9 -2
  341. package/src/mcp/editable-artifacts.ts +29 -11
  342. package/src/mcp/files.ts +5 -1
  343. package/src/mcp/first-party-tool-permissions.ts +7 -285
  344. package/src/mcp/knowledge-entries.ts +108 -34
  345. package/src/mcp/notification-tools.ts +480 -0
  346. package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
  347. package/src/mcp/scheduled-task-input.ts +89 -0
  348. package/src/mcp/scheduled-task-view.ts +63 -3
  349. package/src/mcp/secret-tools.ts +185 -0
  350. package/src/mcp/server.ts +2213 -1116
  351. package/src/mcp/session-admin-access-tools.ts +163 -0
  352. package/src/mcp/session-event-errors.ts +123 -0
  353. package/src/mcp/session-event-view.ts +395 -13
  354. package/src/mcp/session-view.ts +3 -0
  355. package/src/mcp/session-wait.ts +67 -7
  356. package/src/mcp/site-agent-clamp-tools.ts +140 -0
  357. package/src/mcp/site-data-tools.ts +339 -0
  358. package/src/mcp/site-publication-tools.ts +201 -0
  359. package/src/mcp/site-sql-tools.ts +427 -0
  360. package/src/mcp-oauth.ts +635 -45
  361. package/src/model-calls/chat-completions.ts +352 -0
  362. package/src/model-calls/pending-usage-settlements.ts +125 -0
  363. package/src/model-calls/service.ts +694 -0
  364. package/src/model-catalog.ts +68 -17
  365. package/src/native-push-dispatch.ts +457 -0
  366. package/src/observability.ts +15 -1
  367. package/src/organization-mcp.ts +485 -0
  368. package/src/prepared-mcp-actor.ts +154 -0
  369. package/src/prepared-mcp-permissions.ts +38 -0
  370. package/src/routes/analytics-consent.ts +123 -0
  371. package/src/routes/api-integrations.ts +268 -9
  372. package/src/routes/api-keys.ts +164 -25
  373. package/src/routes/artifact-catalog.ts +111 -0
  374. package/src/routes/automations.ts +163 -90
  375. package/src/routes/billing.ts +535 -59
  376. package/src/routes/browser-sessions.ts +966 -342
  377. package/src/routes/canonical-human-identities.ts +61 -0
  378. package/src/routes/capabilities.ts +112 -5
  379. package/src/routes/channels.ts +4 -3
  380. package/src/routes/chat-completions.ts +235 -0
  381. package/src/routes/claude-core-usage.ts +147 -0
  382. package/src/routes/claude-subscription-accounts.ts +233 -0
  383. package/src/routes/claude-subscription-oauth.ts +175 -0
  384. package/src/routes/client-errors.ts +394 -0
  385. package/src/routes/codex-core.ts +687 -0
  386. package/src/routes/codex.ts +1277 -1119
  387. package/src/routes/company-brain.ts +4 -0
  388. package/src/routes/computer-sessions.ts +319 -85
  389. package/src/routes/connect.ts +277 -175
  390. package/src/routes/connections.ts +690 -363
  391. package/src/routes/desktop-distribution.ts +217 -0
  392. package/src/routes/documents.ts +13 -7
  393. package/src/routes/editable-artifacts.ts +74 -4
  394. package/src/routes/enrollments.ts +110 -16
  395. package/src/routes/external-identity-links.ts +13 -12
  396. package/src/routes/files.ts +41 -16
  397. package/src/routes/github-browser-fonts.gen.ts +12 -0
  398. package/src/routes/github-browser-pages.ts +258 -0
  399. package/src/routes/github.ts +200 -75
  400. package/src/routes/inbox.ts +353 -0
  401. package/src/routes/insights-response-cache.ts +83 -0
  402. package/src/routes/insights-usage.ts +345 -0
  403. package/src/routes/insights.ts +103 -8
  404. package/src/routes/install.ts +51 -10
  405. package/src/routes/integration-facets.ts +70 -28
  406. package/src/routes/interaction-placement-message.ts +12 -0
  407. package/src/routes/knowledge.ts +136 -27
  408. package/src/routes/machines.ts +189 -135
  409. package/src/routes/managed-auth-session-sets.ts +43 -10
  410. package/src/routes/managed-onboarding.ts +39 -19
  411. package/src/routes/managed-sign-in-methods.ts +433 -0
  412. package/src/routes/memory-slack-publications.ts +3 -2
  413. package/src/routes/model-account-choice.ts +194 -0
  414. package/src/routes/model-account-settings.ts +235 -0
  415. package/src/routes/model-connection-access.ts +210 -48
  416. package/src/routes/native-app-auth.ts +296 -0
  417. package/src/routes/organization-integration-policy.ts +116 -0
  418. package/src/routes/organization-integrations.ts +326 -0
  419. package/src/routes/organization-mcp-connections.ts +183 -0
  420. package/src/routes/organization-memberships.ts +205 -19
  421. package/src/routes/organization-model-defaults.ts +110 -0
  422. package/src/routes/organization-model-providers.ts +74 -3
  423. package/src/routes/organization-service-accounts.ts +128 -0
  424. package/src/routes/organization-sessions.ts +21 -5
  425. package/src/routes/personal-api-keys.ts +184 -0
  426. package/src/routes/personal-github-git-broker.ts +2 -2
  427. package/src/routes/personal-github.ts +9 -4
  428. package/src/routes/plugins.ts +46 -7
  429. package/src/routes/pr-review-github.ts +105 -118
  430. package/src/routes/pr-review.ts +88 -82
  431. package/src/routes/preference-registry.ts +18 -0
  432. package/src/routes/rigs.ts +48 -13
  433. package/src/routes/scheduled-tasks.ts +347 -27
  434. package/src/routes/session-admin-access.ts +271 -0
  435. package/src/routes/session-artifact-associations.ts +164 -0
  436. package/src/routes/session-history-imports.ts +305 -0
  437. package/src/routes/sessions.ts +1682 -363
  438. package/src/routes/site-data.ts +340 -0
  439. package/src/routes/site-publishing.ts +584 -0
  440. package/src/routes/site-runtime.ts +1944 -0
  441. package/src/routes/site-sql.ts +263 -0
  442. package/src/routes/skill-content.ts +72 -7
  443. package/src/routes/skills.ts +5 -4
  444. package/src/routes/slack-bot-access.ts +108 -0
  445. package/src/routes/social.ts +27 -15
  446. package/src/routes/subscription-account-pools.ts +209 -0
  447. package/src/routes/subscription-core-http.ts +18 -0
  448. package/src/routes/subscription-pool-access.ts +180 -0
  449. package/src/routes/subscription-pool-core.ts +552 -0
  450. package/src/routes/supergrok.ts +174 -655
  451. package/src/routes/transcription-recordings.ts +71 -4
  452. package/src/routes/transcriptions.ts +26 -2
  453. package/src/routes/usage-allowances.ts +381 -0
  454. package/src/routes/variable-sets-removed.ts +19 -0
  455. package/src/routes/vault-providers.ts +782 -0
  456. package/src/routes/vaults.ts +421 -0
  457. package/src/routes/video-generation.ts +30 -12
  458. package/src/routes/workspace-artifacts.ts +165 -53
  459. package/src/routes/workspace-integrations.ts +576 -0
  460. package/src/routes/workspace-model-providers.ts +148 -0
  461. package/src/routes/workspace-state.ts +4 -0
  462. package/src/routes/workspaces.ts +625 -117
  463. package/src/sandbox/agent-update.ts +315 -0
  464. package/src/sandbox/auth-callout.ts +104 -21
  465. package/src/sandbox/channel-a.ts +150 -49
  466. package/src/sandbox/enrollment.ts +131 -22
  467. package/src/sandbox/machine-audience.ts +192 -0
  468. package/src/sandbox/machines.ts +75 -31
  469. package/src/sandbox/metrics-ingestion.ts +133 -2
  470. package/src/sandbox/rematerialize.ts +51 -21
  471. package/src/sandbox/screen-control.ts +289 -0
  472. package/src/sandbox/viewer.ts +33 -58
  473. package/src/sandbox-file-artifacts.ts +53 -27
  474. package/src/scheduled-task-blockers.ts +293 -0
  475. package/src/scheduled-task-deletion.ts +13 -3
  476. package/src/scheduled-task-lifecycle.ts +84 -0
  477. package/src/secret-answers.ts +119 -0
  478. package/src/session-create-observability.ts +46 -0
  479. package/src/session-realtime-context.ts +26 -4
  480. package/src/signed-state-secret.ts +58 -0
  481. package/src/site-agent-guard.ts +261 -0
  482. package/src/site-data/operations.ts +777 -0
  483. package/src/site-data/service.ts +1043 -0
  484. package/src/site-frame-guard.ts +107 -0
  485. package/src/site-publishing/agent-data-tools.ts +250 -0
  486. package/src/site-publishing/agent-model.ts +127 -0
  487. package/src/site-publishing/agent-session.ts +148 -0
  488. package/src/site-publishing/content-scanner.ts +93 -0
  489. package/src/site-publishing/edge-sync.ts +148 -0
  490. package/src/site-publishing/gateway-signature.ts +198 -0
  491. package/src/site-publishing/limits.ts +315 -0
  492. package/src/site-publishing/page-tool-authority.ts +111 -0
  493. package/src/site-publishing/platform-usage.ts +479 -0
  494. package/src/site-publishing/platform.ts +98 -0
  495. package/src/site-publishing/readiness.ts +330 -0
  496. package/src/site-publishing/runtime.ts +831 -0
  497. package/src/site-publishing/service.ts +1282 -0
  498. package/src/site-publishing/session-events.ts +262 -0
  499. package/src/site-publishing/session-views.ts +262 -0
  500. package/src/site-publishing/viewer.ts +75 -0
  501. package/src/site-session-origin.ts +91 -6
  502. package/src/site-sql/format.ts +134 -0
  503. package/src/site-sql/gateway.ts +168 -0
  504. package/src/site-sql/service.ts +1636 -0
  505. package/src/site-uploads.ts +27 -5
  506. package/src/temporal-schedule-sync.ts +74 -0
  507. package/src/transcription/billing-refusal.ts +14 -0
  508. package/src/transcription/normalize.ts +77 -0
  509. package/src/transcription/providers/azure-mai.ts +98 -0
  510. package/src/transcription/providers/azure-openai.ts +16 -7
  511. package/src/transcription/providers/codex-subscription.ts +190 -67
  512. package/src/transcription/providers/openai.ts +79 -5
  513. package/src/transcription/providers/subscription-core.ts +125 -0
  514. package/src/transcription/providers/xai-subscription.ts +81 -83
  515. package/src/transcription/segmenter.ts +7 -1
  516. package/src/transcription/service.ts +239 -10
  517. package/src/transcription/wav-duration.ts +39 -0
  518. package/src/workspace-deletion.ts +25 -1
  519. package/src/workspace-tool-gateway-attestations.ts +133 -0
  520. package/src/workspace-tool-gateway-observability.ts +64 -0
  521. package/src/workspace-tool-gateway-secrets.ts +104 -0
  522. package/src/workspace-tool-gateway.ts +921 -103
  523. package/src/workspace-tool-target-fence.ts +380 -0
  524. package/src/workspace-tool-target.ts +554 -0
  525. package/src/workspace-webhook-dispatch.ts +225 -0
  526. package/src/xai-realtime.ts +85 -81
  527. package/src/xai-subscription-core.ts +172 -0
  528. package/dist/browser-auth-broker.d.ts +0 -20
  529. package/dist/chunk-AJ62T5QN.js.map +0 -1
  530. package/dist/connection-authority-owner.d.ts +0 -59
  531. package/dist/routes/connection-authorities.d.ts +0 -3
  532. package/dist/routes/environments.d.ts +0 -5
  533. package/dist/routes/host-mcp-bindings.d.ts +0 -5
  534. package/dist/routes/host-mcp-resolvers.d.ts +0 -3
  535. package/dist/routes/packs.d.ts +0 -3
  536. package/dist/session-event-view-MU3HMNV3.js +0 -264
  537. package/dist/session-event-view-MU3HMNV3.js.map +0 -1
  538. package/dist/xai-subscription-auth.d.ts +0 -23
  539. package/src/browser-auth-broker.ts +0 -155
  540. package/src/connection-authority-owner.ts +0 -61
  541. package/src/routes/connection-authorities.ts +0 -139
  542. package/src/routes/environments.ts +0 -388
  543. package/src/routes/host-mcp-bindings.ts +0 -122
  544. package/src/routes/host-mcp-resolvers.ts +0 -41
  545. package/src/routes/packs.ts +0 -784
  546. package/src/xai-subscription-auth.ts +0 -139
@@ -1,17 +1,21 @@
1
- import { createHmac, timingSafeEqual } from "node:crypto";
1
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2
2
  import { environmentsEncryptionKeyBytes, type Settings } from "@opengeni/config";
3
3
  import {
4
4
  OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
5
5
  OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
6
6
  OPENGENI_SLACK_BOT_REQUIRED_SCOPES,
7
+ OPENGENI_SLACK_FILE_UPLOAD_REQUIRED_SCOPE,
7
8
  evaluateOpenGeniSlackBotScopes,
9
+ hasOpenGeniSlackFileUploadScope,
8
10
  hasOpenGeniSlackBotSearchScopes,
11
+ hasOpenGeniSlackReactionScope,
9
12
  type AccessGrant,
10
13
  type ConnectionMetadata,
11
14
  type OpenGeniSlackBotDisplayName,
12
15
  type OpenGeniSlackBotConnectionMetadata,
13
16
  } from "@opengeni/contracts";
14
17
  import {
18
+ grantHasAgentAttemptAuthority,
15
19
  isOpenGeniSlackBotConnection,
16
20
  isTrustedScheduledSlackBotSession,
17
21
  openGeniSlackBotMetadata,
@@ -20,14 +24,21 @@ import {
20
24
  } from "@opengeni/core";
21
25
  import {
22
26
  buildConnectionTokenResolver,
27
+ buildSlackApiRateLimiter,
23
28
  claimSlackBotDeleteOperation,
24
29
  claimSlackBotPostOperation,
25
30
  claimSlackBotUpdateOperation,
26
31
  completeSlackBotDeleteOperation,
27
32
  completeSlackBotPostOperation,
28
33
  completeSlackBotUpdateOperation,
34
+ getScheduledTask,
29
35
  getSession,
30
- listConnectionsMetadata,
36
+ getSlackBotPostOperation,
37
+ availableSlackBotConnectionMetadata,
38
+ sharedOrganizationSlackBots,
39
+ prepareBotMessage,
40
+ readPreparedBotMessage,
41
+ freezeAgentLearningPolicy,
31
42
  markSlackBotDeleteOperationProviderStarted,
32
43
  markSlackBotPostOperationProviderStarted,
33
44
  recordAuditEvent,
@@ -43,11 +54,14 @@ import {
43
54
  type FetchLike,
44
55
  } from "@opengeni/network";
45
56
  import { HTTPException } from "hono/http-exception";
57
+ import sharp from "sharp";
46
58
 
47
59
  const SLACK_API_BASE = "https://slack.com/api/";
48
60
  const SLACK_RESPONSE_MAX_BYTES = 2 * 1024 * 1024;
49
61
  const SLACK_FILE_RESPONSE_MAX_BYTES = 4 * 1024 * 1024;
50
62
  export const SLACK_REACTION_IMAGE_MAX_BYTES = 4 * 1024 * 1024;
63
+ // Base64 plus MCP metadata must stay below the runtime's 1 MiB model-result cap.
64
+ export const SLACK_MCP_IMAGE_MAX_BYTES = 640 * 1024;
51
65
  const SLACK_FILE_CONTENT_PAGE_CHARS = 50_000;
52
66
  const SLACK_TIMEOUT_MS = 10_000;
53
67
  const MAX_CHANNEL_PAGE = 200;
@@ -258,6 +272,7 @@ type SlackBotOperation =
258
272
  | "files.list"
259
273
  | "file.info"
260
274
  | "file.content.read"
275
+ | "file.upload"
261
276
  | "home.publish"
262
277
  | "message.post"
263
278
  | "message.update"
@@ -323,6 +338,66 @@ export class SlackBotProviderError extends Error {
323
338
  }
324
339
  }
325
340
 
341
+ export function slackHistoryRateLimited(error: unknown): error is SlackBotProviderError {
342
+ return (
343
+ error instanceof SlackBotProviderError &&
344
+ (error.code === "http_429" || error.code === "rate_limited" || error.code === "ratelimited")
345
+ );
346
+ }
347
+
348
+ /**
349
+ * The post or update ledger already binds this operation id to different
350
+ * request bytes. Raised from the durable claim, before any Slack write, so a
351
+ * caller that knows an earlier release wrote other bytes for the same operation
352
+ * may retry once with those bytes.
353
+ */
354
+ export class SlackBotOperationConflictError extends Error {
355
+ constructor(kind: "post" | "update" | "delete") {
356
+ super(`operationId is already bound to a different Slack ${kind} request`);
357
+ this.name = "SlackBotOperationConflictError";
358
+ }
359
+ }
360
+
361
+ /** Only Slack's one-time HTTPS upload endpoint may receive retained bytes. */
362
+ export function slackFileUploadUrl(value: string): URL {
363
+ let url: URL;
364
+ try {
365
+ url = new URL(value);
366
+ } catch {
367
+ throw new SlackBotProviderError("invalid_upload_url");
368
+ }
369
+ if (
370
+ value.length > 4096 ||
371
+ url.protocol !== "https:" ||
372
+ url.hostname !== "files.slack.com" ||
373
+ url.port !== "" ||
374
+ url.username ||
375
+ url.password ||
376
+ url.hash ||
377
+ !url.pathname.startsWith("/upload/v1/")
378
+ ) {
379
+ throw new SlackBotProviderError("invalid_upload_url");
380
+ }
381
+ return url;
382
+ }
383
+
384
+ /** A channel listing alone is not proof of delivery to the requested thread. */
385
+ export function slackFileSharedToThread(
386
+ value: unknown,
387
+ input: { fileId: string; channelId: string; threadTimestamp: string; botUserId: string },
388
+ ): boolean {
389
+ const file = slackRecord(value);
390
+ if (file?.id !== input.fileId || file.user !== input.botUserId) return false;
391
+ const shares = slackRecord(file.shares);
392
+ return ["public", "private"].some((kind) => {
393
+ const entries = slackRecord(shares?.[kind])?.[input.channelId];
394
+ return (
395
+ Array.isArray(entries) &&
396
+ entries.some((entry) => slackRecord(entry)?.thread_ts === input.threadTimestamp)
397
+ );
398
+ });
399
+ }
400
+
326
401
  export async function authorizeSlackSharedImageRead(
327
402
  channel: {
328
403
  isArchived: boolean;
@@ -443,6 +518,8 @@ export async function resolveSlackBotConnectionForTool(input: {
443
518
  connection: ConnectionMetadata;
444
519
  metadata: OpenGeniSlackBotConnectionMetadata;
445
520
  context: SlackBotContext;
521
+ sharingGeneration?: number;
522
+ authorizeOrganizationAccess?: SlackProviderAuthorization;
446
523
  }> {
447
524
  const session = input.sessionId
448
525
  ? await getSession(input.db, input.grant.workspaceId, input.sessionId)
@@ -452,27 +529,29 @@ export async function resolveSlackBotConnectionForTool(input: {
452
529
  }
453
530
  const boundConnectionId = scheduledSlackBotConnectionId(session?.metadata);
454
531
  if (boundConnectionId && (!session || !isTrustedScheduledSlackBotSession(session))) {
455
- throw new Error("OpenGeni Slack bot routing metadata is not scheduler-authorized");
532
+ throw new Error("Opengeni Slack bot routing metadata is not scheduler-authorized");
456
533
  }
457
534
  if (
458
535
  boundConnectionId &&
459
536
  input.requestedConnectionId &&
460
537
  input.requestedConnectionId !== boundConnectionId
461
538
  ) {
462
- throw new Error("this scheduled session is bound to a different OpenGeni Slack bot connection");
539
+ throw new Error("this scheduled session is bound to a different Opengeni Slack bot connection");
463
540
  }
464
541
  if (!boundConnectionId && !input.grant.permissions.includes("connections:read")) {
465
- throw new Error("connections:read is required to select an OpenGeni Slack bot connection");
542
+ throw new Error("connections:read is required to select an Opengeni Slack bot connection");
466
543
  }
467
544
  let connectionId = boundConnectionId ?? input.requestedConnectionId;
468
545
  if (!connectionId) {
469
546
  const activeConnections = (
470
- await listConnectionsMetadata(input.db, input.grant.workspaceId, null)
547
+ await availableSlackBotConnectionMetadata(input.db, input.grant)
471
548
  ).filter(
472
549
  (connection) => connection.status === "active" && isOpenGeniSlackBotConnection(connection),
473
550
  );
474
551
  if (activeConnections.length === 0) {
475
- throw new Error("no active OpenGeni Slack bot connection is installed in this workspace");
552
+ throw new Error(
553
+ "No active Opengeni bot is available. Connect it in Capabilities or ask an organization administrator to share the installed bot.",
554
+ );
476
555
  }
477
556
  if (activeConnections.length > 1) {
478
557
  const principals = new Set(
@@ -483,7 +562,10 @@ export async function resolveSlackBotConnectionForTool(input: {
483
562
  );
484
563
  if (principals.size > 1) {
485
564
  throw new Error(
486
- "connectionId is required because this workspace has multiple active OpenGeni Slack bot connections",
565
+ `Choose connectionId because multiple Opengeni bots are available: ${activeConnections
566
+ .slice(0, 20)
567
+ .map((bot) => `${openGeniSlackBotMetadataLabel(bot)} (${bot.id})`)
568
+ .join(", ")}`,
487
569
  );
488
570
  }
489
571
  }
@@ -496,14 +578,31 @@ export async function resolveSlackBotConnectionForTool(input: {
496
578
  );
497
579
  const metadata = openGeniSlackBotMetadata(connection.metadata);
498
580
  if (!metadata) {
499
- throw new Error("OpenGeni Slack bot connection metadata is invalid");
581
+ throw new Error("Opengeni Slack bot connection metadata is invalid");
500
582
  }
583
+ const sharingGeneration =
584
+ connection.workspaceId === input.grant.workspaceId
585
+ ? 0
586
+ : Number(connection.metadata.organizationSharingGeneration);
501
587
  return {
502
588
  connection,
503
589
  metadata,
590
+ sharingGeneration,
591
+ ...(connection.workspaceId !== input.grant.workspaceId
592
+ ? {
593
+ authorizeOrganizationAccess: async () =>
594
+ (await sharedOrganizationSlackBots(input.db, input.grant)).some(
595
+ (share) =>
596
+ share.connectionId === connection.id &&
597
+ share.homeWorkspaceId === connection.workspaceId &&
598
+ share.generation === sharingGeneration &&
599
+ share.connectionVersion === connection.version,
600
+ ),
601
+ }
602
+ : {}),
504
603
  context: {
505
604
  accountId: input.grant.accountId,
506
- workspaceId: input.grant.workspaceId,
605
+ workspaceId: connection.workspaceId,
507
606
  subjectId: input.grant.subjectId,
508
607
  sessionId: input.sessionId,
509
608
  scheduledTaskId:
@@ -514,8 +613,402 @@ export async function resolveSlackBotConnectionForTool(input: {
514
613
  };
515
614
  }
516
615
 
616
+ function openGeniSlackBotMetadataLabel(connection: ConnectionMetadata): string {
617
+ const metadata = openGeniSlackBotMetadata(connection.metadata);
618
+ return metadata?.slackTeamName ?? "Slack workspace";
619
+ }
620
+
621
+ /**
622
+ * The one destination a scheduled run may post to as the Opengeni bot: the
623
+ * channel a person chose on the task, through the bot connection frozen on the
624
+ * run's session. The agent never supplies a channel. Every call re-reads the
625
+ * task, so a person clearing or changing the channel takes effect at once.
626
+ */
627
+ export async function resolveScheduledSlackBotPostTarget(input: {
628
+ db: Database;
629
+ grant: AccessGrant;
630
+ sessionId: string | null;
631
+ }): Promise<
632
+ Awaited<ReturnType<typeof resolveSlackBotConnectionForTool>> & {
633
+ scheduledTaskId: string;
634
+ channelId: string;
635
+ }
636
+ > {
637
+ const refuse = (reason: string): never => {
638
+ throw new Error(`Posting to the task's Slack channel is unavailable: ${reason}`);
639
+ };
640
+ if (!input.sessionId) refuse("this is not a scheduled task run");
641
+ const session = await getSession(input.db, input.grant.workspaceId, input.sessionId!);
642
+ if (!session || !isTrustedScheduledSlackBotSession(session)) {
643
+ refuse("this is not a scheduled task run with an Opengeni Slack bot");
644
+ }
645
+ const connectionId = scheduledSlackBotConnectionId(session!.metadata)!;
646
+ const scheduledTaskId = String(session!.metadata.scheduledTaskId);
647
+ const task = await getScheduledTask(input.db, input.grant.workspaceId, scheduledTaskId);
648
+ if (!task) refuse("the scheduled task was deleted");
649
+ if (task!.runMode === "existing_session") refuse("the task continues an existing chat");
650
+ if (task!.agentConfig.slackBotConnectionId !== connectionId) {
651
+ refuse("the task no longer uses this Opengeni Slack bot");
652
+ }
653
+ const channelId = task!.agentConfig.slackBotChannelId;
654
+ if (!channelId) refuse("no one has chosen a Slack channel for this task");
655
+ const resolved = await resolveSlackBotConnectionForTool({
656
+ db: input.db,
657
+ grant: input.grant,
658
+ sessionId: input.sessionId,
659
+ requestedConnectionId: connectionId,
660
+ });
661
+ return { ...resolved, scheduledTaskId, channelId: channelId! };
662
+ }
663
+
664
+ /**
665
+ * Save one message for the task's channel without sending it. The returned id
666
+ * is server-owned and becomes the Slack post operation id, so retrying the
667
+ * send can never post the same message twice.
668
+ */
669
+ export async function prepareScheduledSlackBotPost(input: {
670
+ db: Database;
671
+ grant: AccessGrant;
672
+ sessionId: string | null;
673
+ text: string;
674
+ threadTimestamp?: string | undefined;
675
+ }) {
676
+ const target = await resolveScheduledSlackBotPostTarget(input);
677
+ const message = await prepareBotMessage(input.db, {
678
+ accountId: input.grant.accountId,
679
+ workspaceId: input.grant.workspaceId,
680
+ sessionId: input.sessionId!,
681
+ scheduledTaskId: target.scheduledTaskId,
682
+ connectionId: target.connection.id,
683
+ connectionVersion: target.connection.version,
684
+ homeWorkspaceId: target.connection.workspaceId,
685
+ sharingGeneration: target.sharingGeneration ?? 0,
686
+ channelId: target.channelId,
687
+ threadTimestamp: input.threadTimestamp ?? null,
688
+ text: input.text,
689
+ });
690
+ return {
691
+ messageId: message.id,
692
+ identity: "workspace_bot" as const,
693
+ channelId: message.channelId,
694
+ threadTimestamp: message.threadTimestamp,
695
+ text: message.text,
696
+ sent: false,
697
+ };
698
+ }
699
+
700
+ /** Ordinary chats choose an explicit channel; scheduled roots retain their
701
+ * person-chosen destination and cannot enter this path through a child. */
702
+ export async function prepareSlackBotPost(input: {
703
+ db: Database;
704
+ grant: AccessGrant;
705
+ sessionId: string | null;
706
+ text: string;
707
+ channelId?: string;
708
+ connectionId?: string;
709
+ threadTimestamp?: string;
710
+ }) {
711
+ if (!input.sessionId) throw new Error("Bot posting requires a chat");
712
+ const session = await getSession(input.db, input.grant.workspaceId, input.sessionId);
713
+ if (!session) throw new Error("Bot posting chat is unavailable");
714
+ if (isTrustedScheduledSlackBotSession(session)) {
715
+ if (input.channelId || input.connectionId)
716
+ throw new Error(
717
+ "A scheduled task uses only the bot and channel chosen by a person on the task",
718
+ );
719
+ return prepareScheduledSlackBotPost(input);
720
+ }
721
+ await authorizeOrdinaryBotPosting(input);
722
+ if (!input.grant.permissions.includes("connections:read"))
723
+ throw new Error("connections:read is required to post as the bot");
724
+ if (!input.channelId)
725
+ throw new Error(
726
+ "Choose the Slack channel for this bot message. For a schedule, choose Post to Slack in the schedule editor.",
727
+ );
728
+ const target = await resolveSlackBotConnectionForTool({
729
+ db: input.db,
730
+ grant: input.grant,
731
+ sessionId: input.sessionId,
732
+ ...(input.connectionId ? { requestedConnectionId: input.connectionId } : {}),
733
+ });
734
+ const message = await prepareBotMessage(input.db, {
735
+ accountId: input.grant.accountId,
736
+ workspaceId: input.grant.workspaceId,
737
+ sessionId: input.sessionId,
738
+ scheduledTaskId: null,
739
+ connectionId: target.connection.id,
740
+ connectionVersion: target.connection.version,
741
+ homeWorkspaceId: target.connection.workspaceId,
742
+ sharingGeneration: target.sharingGeneration ?? 0,
743
+ channelId: input.channelId,
744
+ threadTimestamp: input.threadTimestamp ?? null,
745
+ text: input.text,
746
+ });
747
+ return {
748
+ messageId: message.id,
749
+ identity: "organization_bot" as const,
750
+ channelId: message.channelId,
751
+ threadTimestamp: message.threadTimestamp,
752
+ text: message.text,
753
+ sent: false,
754
+ };
755
+ }
756
+
757
+ /** The saved server-owned identity, destination, text and sharing generation
758
+ * are immutable. Neither a reconnect nor a revoked/re-enabled share redirects it. */
759
+ export async function sendSlackBotPost(input: {
760
+ db: Database;
761
+ settings: Settings;
762
+ grant: AccessGrant;
763
+ sessionId: string | null;
764
+ messageId: string;
765
+ slackFetch?: typeof fetch;
766
+ authorizeProviderRequest?: SlackProviderAuthorization;
767
+ }) {
768
+ if (!input.sessionId) throw new Error("Bot posting requires a chat");
769
+ const message = await readPreparedBotMessage(input.db, {
770
+ accountId: input.grant.accountId,
771
+ workspaceId: input.grant.workspaceId,
772
+ sessionId: input.sessionId,
773
+ id: input.messageId,
774
+ });
775
+ if (!message) throw new Error("This prepared bot message does not exist in this chat");
776
+ if (message.scheduledTaskId) return sendScheduledSlackBotPost(input);
777
+ await authorizeOrdinaryBotPosting(input);
778
+ const earlier = await getSlackBotPostOperation(
779
+ input.db,
780
+ message.homeWorkspaceId,
781
+ message.connectionId,
782
+ message.id,
783
+ );
784
+ if (earlier?.status === "completed") return completedPreparedBotPost(message, earlier);
785
+ if (!input.grant.permissions.includes("connections:read"))
786
+ throw new Error("connections:read is required to post as the bot");
787
+ const target = await resolveSlackBotConnectionForTool({
788
+ db: input.db,
789
+ grant: input.grant,
790
+ sessionId: input.sessionId,
791
+ requestedConnectionId: message.connectionId,
792
+ });
793
+ if (
794
+ message.connectionVersion !== target.connection.version ||
795
+ message.homeWorkspaceId !== target.connection.workspaceId ||
796
+ message.sharingGeneration !== (target.sharingGeneration ?? 0)
797
+ ) {
798
+ throw new Error(
799
+ "Bot access changed after this message was prepared. It was not sent again; an earlier interrupted send may already have posted it.",
800
+ );
801
+ }
802
+ const client = createOpenGeniSlackBotClient(input, target);
803
+ return client.postMessage({
804
+ operationId: message.id,
805
+ channelId: message.channelId,
806
+ ...(message.threadTimestamp ? { threadTimestamp: message.threadTimestamp } : {}),
807
+ text: message.text,
808
+ requireActiveNonSharedChannel: true,
809
+ });
810
+ }
811
+
812
+ async function authorizeOrdinaryBotPosting(input: {
813
+ db: Database;
814
+ grant: AccessGrant;
815
+ sessionId: string | null;
816
+ }) {
817
+ if (!grantHasAgentAttemptAuthority(input.grant)) return;
818
+ const { sessionId, turnId, attemptId, executionGeneration } = input.grant.metadata ?? {};
819
+ if (
820
+ typeof sessionId !== "string" ||
821
+ sessionId !== input.sessionId ||
822
+ typeof turnId !== "string" ||
823
+ typeof attemptId !== "string" ||
824
+ typeof executionGeneration !== "number" ||
825
+ !Number.isSafeInteger(executionGeneration) ||
826
+ executionGeneration < 1
827
+ )
828
+ throw new Error("Exact live agent attempt required for bot posting");
829
+ const policy = await freezeAgentLearningPolicy(input.db, {
830
+ accountId: input.grant.accountId,
831
+ workspaceId: input.grant.workspaceId,
832
+ actor: { kind: "agent", sessionId, turnId, attemptId, executionGeneration },
833
+ });
834
+ if (typeof policy.scheduledTaskRunId === "string") {
835
+ throw new Error(
836
+ "A scheduled occurrence can post as the bot only to its person-chosen Post to Slack destination. Tasks that continue an existing chat cannot post as the bot.",
837
+ );
838
+ }
839
+ }
840
+
841
+ function completedPreparedBotPost(
842
+ message: Awaited<ReturnType<typeof readPreparedBotMessage>> & {},
843
+ operation: { slackChannelId: string | null; slackMessageTimestamp: string | null },
844
+ ) {
845
+ return {
846
+ channelId: operation.slackChannelId,
847
+ timestamp: operation.slackMessageTimestamp,
848
+ threadTimestamp: message.threadTimestamp,
849
+ alreadySent: true,
850
+ receipt: {
851
+ credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
852
+ credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
853
+ connectionId: message.connectionId,
854
+ operation: "message.post",
855
+ operationId: message.id,
856
+ clientMessageId: message.id,
857
+ },
858
+ };
859
+ }
860
+
861
+ /**
862
+ * Send a message this run prepared, exactly as saved. The destination must
863
+ * still be the task's channel and the bot connection unchanged; otherwise the
864
+ * send is refused rather than redirected.
865
+ */
866
+ export async function sendScheduledSlackBotPost(input: {
867
+ db: Database;
868
+ settings: Settings;
869
+ grant: AccessGrant;
870
+ sessionId: string | null;
871
+ messageId: string;
872
+ slackFetch?: typeof fetch;
873
+ authorizeProviderRequest?: SlackProviderAuthorization;
874
+ }) {
875
+ if (!input.sessionId) throw new Error("Bot posting requires a chat");
876
+ const saved = await readPreparedBotMessage(input.db, {
877
+ accountId: input.grant.accountId,
878
+ workspaceId: input.grant.workspaceId,
879
+ sessionId: input.sessionId,
880
+ id: input.messageId,
881
+ });
882
+ if (saved) {
883
+ const earlier = await getSlackBotPostOperation(
884
+ input.db,
885
+ saved.homeWorkspaceId,
886
+ saved.connectionId,
887
+ saved.id,
888
+ );
889
+ if (earlier?.status === "completed") return completedPreparedBotPost(saved, earlier);
890
+ }
891
+ const target = await resolveScheduledSlackBotPostTarget(input);
892
+ const message = await readPreparedBotMessage(input.db, {
893
+ accountId: input.grant.accountId,
894
+ workspaceId: input.grant.workspaceId,
895
+ sessionId: input.sessionId!,
896
+ id: input.messageId,
897
+ });
898
+ if (!message) {
899
+ throw new Error("This prepared Slack message does not exist in this chat");
900
+ }
901
+ const channelChanged =
902
+ message.channelId !== target.channelId || message.scheduledTaskId !== target.scheduledTaskId;
903
+ const botChanged =
904
+ message.connectionId !== target.connection.id ||
905
+ message.connectionVersion !== target.connection.version ||
906
+ message.homeWorkspaceId !== target.connection.workspaceId ||
907
+ message.sharingGeneration !== (target.sharingGeneration ?? 0);
908
+ const client = createOpenGeniSlackBotClient(
909
+ {
910
+ db: input.db,
911
+ settings: input.settings,
912
+ ...(input.slackFetch ? { slackFetch: input.slackFetch } : {}),
913
+ ...(input.authorizeProviderRequest
914
+ ? { authorizeProviderRequest: input.authorizeProviderRequest }
915
+ : {}),
916
+ authorizeBotMutation: async () => {
917
+ const current = await resolveScheduledSlackBotPostTarget(input);
918
+ if (
919
+ current.channelId !== message.channelId ||
920
+ current.connection.id !== message.connectionId ||
921
+ current.connection.version !== message.connectionVersion ||
922
+ (current.sharingGeneration ?? 0) !== message.sharingGeneration
923
+ )
924
+ throw new Error(
925
+ "The task's bot destination changed before sending; the message was not sent again",
926
+ );
927
+ },
928
+ },
929
+ target,
930
+ );
931
+ const post = () =>
932
+ client.postMessage({
933
+ operationId: message.id,
934
+ channelId: message.channelId,
935
+ ...(message.threadTimestamp ? { threadTimestamp: message.threadTimestamp } : {}),
936
+ text: message.text,
937
+ requireActiveNonSharedChannel: true,
938
+ });
939
+ if (!channelChanged && !botChanged) return await post();
940
+ // The destination moved after this message was prepared, so it is never sent
941
+ // now. Say truthfully whether an earlier send already reached Slack, so the
942
+ // agent does not post the same content again believing nothing was sent.
943
+ const earlier = await getSlackBotPostOperation(
944
+ input.db,
945
+ message.homeWorkspaceId,
946
+ message.connectionId,
947
+ message.id,
948
+ );
949
+ if (earlier?.status === "completed" && message.connectionId === target.connection.id) {
950
+ // Replays the recorded result from the post ledger; no Slack call is made.
951
+ return await post();
952
+ }
953
+ const reason = channelChanged
954
+ ? "The task's Slack channel changed after this message was prepared"
955
+ : "The Opengeni Slack bot changed after this message was prepared";
956
+ if (earlier?.status === "completed") {
957
+ throw new Error(`${reason}. It had already been posted, so it was not sent again.`);
958
+ }
959
+ if (earlier && earlier.status !== "pending") {
960
+ throw new Error(
961
+ `${reason}. An earlier send was interrupted, so it may already have been posted to the previous channel; it was not sent again.`,
962
+ );
963
+ }
964
+ throw new Error(`${reason}, so it was not sent. Prepare a new message for the current channel.`);
965
+ }
966
+
967
+ /**
968
+ * Check, with the bot's token, that a person's chosen task channel is one the
969
+ * bot is a member of, active, and not shared with another organization.
970
+ */
971
+ export async function verifyScheduledTaskSlackChannel(
972
+ deps: { db: Database; settings: Settings; slackFetch?: typeof fetch },
973
+ input: {
974
+ accountId: string;
975
+ workspaceId: string;
976
+ subjectId: string;
977
+ connectionId: string;
978
+ channelId: string;
979
+ },
980
+ ): Promise<void> {
981
+ const client = await createOpenGeniSlackBotInteractionClient(deps, {
982
+ accountId: input.accountId,
983
+ workspaceId: input.workspaceId,
984
+ connectionId: input.connectionId,
985
+ subjectId: input.subjectId,
986
+ });
987
+ let channel: Awaited<ReturnType<OpenGeniSlackBotClient["verifyChannelAccess"]>>;
988
+ try {
989
+ channel = await client.verifyChannelAccess(input.channelId);
990
+ } catch (error) {
991
+ throw new HTTPException(422, {
992
+ message: `The Opengeni bot cannot post in that Slack channel. Invite it to the channel first. (${safeFailureCode(error)})`,
993
+ });
994
+ }
995
+ if (
996
+ channel.isDirectMessage ||
997
+ channel.isArchived ||
998
+ channel.isShared ||
999
+ channel.isExternallyShared ||
1000
+ channel.isOrgShared
1001
+ ) {
1002
+ throw new HTTPException(422, {
1003
+ message:
1004
+ "Scheduled posts need an active Slack channel that is not shared with another organization",
1005
+ });
1006
+ }
1007
+ }
1008
+
517
1009
  export class OpenGeniSlackBotClient {
518
1010
  private readonly resolveCredential: ReturnType<typeof buildConnectionTokenResolver>;
1011
+ private readonly rateLimit: ReturnType<typeof buildSlackApiRateLimiter>;
519
1012
 
520
1013
  constructor(
521
1014
  private readonly db: Database,
@@ -525,8 +1018,20 @@ export class OpenGeniSlackBotClient {
525
1018
  private readonly context: SlackBotContext,
526
1019
  private readonly fetchImpl: FetchLike = fetch,
527
1020
  private readonly authorizeProviderRequest?: SlackProviderAuthorization,
1021
+ private readonly authorizeBotAccess?: (method: string) => Promise<boolean | void>,
528
1022
  ) {
529
1023
  this.resolveCredential = buildConnectionTokenResolver(db, settings);
1024
+ this.rateLimit = buildSlackApiRateLimiter(db, settings);
1025
+ }
1026
+
1027
+ private historyPageLimit(approvedLimit: number): number {
1028
+ return this.settings.slackAccessMode === "full" ? approvedLimit : 15;
1029
+ }
1030
+
1031
+ private postReconciliationPageLimit(): number {
1032
+ // Ambiguous writes stay unknown when the one bounded pilot page cannot
1033
+ // prove the provider identity. Never crawl or resend an unknown message.
1034
+ return this.settings.slackAccessMode === "full" ? MAX_SLACK_POST_RECONCILIATION_PAGES : 1;
530
1035
  }
531
1036
 
532
1037
  async listChannels(input: { limit?: number; cursor?: string } = {}) {
@@ -551,6 +1056,138 @@ export class OpenGeniSlackBotClient {
551
1056
  return await this.requireMemberChannel(headers, channelId);
552
1057
  }
553
1058
 
1059
+ /** Server-only upload preparation; the temporary URL never enters a tool result. */
1060
+ async allocateFileUpload(input: {
1061
+ channelId: string;
1062
+ privateRecipientSlackUserId?: string;
1063
+ filename: string;
1064
+ sizeBytes: number;
1065
+ }) {
1066
+ this.requireFileUploadScope();
1067
+ return this.withAudit("file.upload", async (authority) => {
1068
+ await this.requireActiveNonSharedMemberChannel(
1069
+ authority,
1070
+ input.channelId,
1071
+ input.privateRecipientSlackUserId,
1072
+ );
1073
+ const payload = await this.call(authority, "files.getUploadURLExternal", {
1074
+ filename: input.filename,
1075
+ length: String(input.sizeBytes),
1076
+ });
1077
+ const fileId = slackString(payload.file_id);
1078
+ const uploadUrl = slackString(payload.upload_url);
1079
+ if (!fileId || !/^F[A-Z0-9]{1,63}$/.test(fileId) || !uploadUrl) {
1080
+ throw new SlackBotProviderError("invalid_upload_response");
1081
+ }
1082
+ return { fileId, uploadUrl: slackFileUploadUrl(uploadUrl) };
1083
+ });
1084
+ }
1085
+
1086
+ async transferFileUpload(input: {
1087
+ channelId: string;
1088
+ privateRecipientSlackUserId?: string;
1089
+ uploadUrl: URL;
1090
+ bytes: Uint8Array;
1091
+ }) {
1092
+ this.requireFileUploadScope();
1093
+ const uploadUrl = slackFileUploadUrl(input.uploadUrl.toString());
1094
+ return this.withAudit("file.upload", async (authority) => {
1095
+ await this.requireActiveNonSharedMemberChannel(
1096
+ authority,
1097
+ input.channelId,
1098
+ input.privateRecipientSlackUserId,
1099
+ );
1100
+ // Reauthorize this exact destination and live attempt immediately before
1101
+ // I/O, but do NOT forward the bot token to Slack's temporary upload URL.
1102
+ await this.headersForDestination("file.upload", uploadUrl.toString());
1103
+ let response: Response;
1104
+ try {
1105
+ response = await this.fetchImpl(uploadUrl, {
1106
+ method: "POST",
1107
+ headers: { "content-type": "application/octet-stream" },
1108
+ body: Buffer.from(input.bytes),
1109
+ redirect: "error",
1110
+ signal: AbortSignal.timeout(30_000),
1111
+ });
1112
+ } catch {
1113
+ throw new SlackBotProviderError("upload_transport_error");
1114
+ }
1115
+ await response.body?.cancel().catch(() => undefined);
1116
+ if (!response.ok) {
1117
+ throw new SlackBotProviderError(
1118
+ `upload_http_${response.status}`,
1119
+ slackRetryAfterMs(response),
1120
+ );
1121
+ }
1122
+ return { uploaded: true };
1123
+ });
1124
+ }
1125
+
1126
+ async completeFileUpload(input: {
1127
+ channelId: string;
1128
+ privateRecipientSlackUserId?: string;
1129
+ threadTimestamp: string;
1130
+ fileId: string;
1131
+ title: string;
1132
+ }) {
1133
+ this.requireFileUploadScope();
1134
+ return this.withAudit("file.upload", async (authority) => {
1135
+ await this.requireActiveNonSharedMemberChannel(
1136
+ authority,
1137
+ input.channelId,
1138
+ input.privateRecipientSlackUserId,
1139
+ );
1140
+ const payload = await this.call(authority, "files.completeUploadExternal", {
1141
+ files: JSON.stringify([{ id: input.fileId, title: input.title }]),
1142
+ channel_id: input.channelId,
1143
+ thread_ts: input.threadTimestamp,
1144
+ });
1145
+ if (
1146
+ !Array.isArray(payload.files) ||
1147
+ !payload.files.some((file) => slackRecord(file)?.id === input.fileId)
1148
+ ) {
1149
+ throw new SlackBotProviderError("invalid_upload_completion_response");
1150
+ }
1151
+ return { fileId: input.fileId };
1152
+ });
1153
+ }
1154
+
1155
+ async reconcileFileUpload(input: {
1156
+ channelId: string;
1157
+ privateRecipientSlackUserId?: string;
1158
+ threadTimestamp: string;
1159
+ fileId: string;
1160
+ }) {
1161
+ this.requireFileUploadScope();
1162
+ return this.withAudit("file.upload", async (authority) => {
1163
+ await this.requireActiveNonSharedMemberChannel(
1164
+ authority,
1165
+ input.channelId,
1166
+ input.privateRecipientSlackUserId,
1167
+ );
1168
+ let payload: SlackPayload;
1169
+ try {
1170
+ payload = await this.call(authority, "files.info", { file: input.fileId });
1171
+ } catch (error) {
1172
+ if (error instanceof SlackBotProviderError && error.code === "file_not_found")
1173
+ return { shared: false };
1174
+ throw error;
1175
+ }
1176
+ return {
1177
+ shared: slackFileSharedToThread(payload.file, {
1178
+ ...input,
1179
+ botUserId: this.metadata.botUserId,
1180
+ }),
1181
+ };
1182
+ });
1183
+ }
1184
+
1185
+ private requireFileUploadScope() {
1186
+ if (!hasOpenGeniSlackFileUploadScope(this.connection.grantedScopes)) {
1187
+ throw new SlackBotProviderError("slack_bot_file_upload_scope_missing");
1188
+ }
1189
+ }
1190
+
554
1191
  /**
555
1192
  * Replace one Slack user's private App Home view. `views.publish` is a
556
1193
  * naturally convergent replace operation: Slack event retries may safely
@@ -604,7 +1241,13 @@ export class OpenGeniSlackBotClient {
604
1241
  await input.authorizeRead?.();
605
1242
  const payload = await this.call(headers, "conversations.history", {
606
1243
  channel: input.channelId,
607
- limit: String(boundedInt(input.limit, MAX_HISTORY_PAGE, 50)),
1244
+ limit: String(
1245
+ boundedInt(
1246
+ input.limit,
1247
+ this.historyPageLimit(MAX_HISTORY_PAGE),
1248
+ this.historyPageLimit(50),
1249
+ ),
1250
+ ),
608
1251
  ...(input.cursor ? { cursor: input.cursor } : {}),
609
1252
  ...(input.latest ? { latest: input.latest } : {}),
610
1253
  ...(input.latest && input.inclusive ? { inclusive: "true" } : {}),
@@ -633,7 +1276,13 @@ export class OpenGeniSlackBotClient {
633
1276
  const payload = await this.call(headers, "conversations.replies", {
634
1277
  channel: input.channelId,
635
1278
  ts: input.threadTimestamp,
636
- limit: String(boundedInt(input.limit, MAX_THREAD_PAGE, 50)),
1279
+ limit: String(
1280
+ boundedInt(
1281
+ input.limit,
1282
+ this.historyPageLimit(MAX_THREAD_PAGE),
1283
+ this.historyPageLimit(50),
1284
+ ),
1285
+ ),
637
1286
  ...(input.cursor ? { cursor: input.cursor } : {}),
638
1287
  ...(input.oldest ? { oldest: input.oldest } : {}),
639
1288
  ...(input.latest ? { latest: input.latest } : {}),
@@ -653,7 +1302,6 @@ export class OpenGeniSlackBotClient {
653
1302
  messageTimestamp: string;
654
1303
  checkpoint: unknown | null;
655
1304
  checkpointBinding: SlackReactionContextCheckpointBinding;
656
- saveCheckpoint: (checkpoint: SlackReactionContextCheckpoint) => Promise<void>;
657
1305
  }) {
658
1306
  return await this.withAudit("thread_replies.read", async (headers) => {
659
1307
  const checkpointKey = environmentsEncryptionKeyBytes(this.settings);
@@ -666,84 +1314,61 @@ export class OpenGeniSlackBotClient {
666
1314
  input.channelId,
667
1315
  input.messageTimestamp,
668
1316
  );
669
- const restored = input.checkpoint
670
- ? parseSlackReactionContextCheckpoint(
671
- input.checkpoint,
672
- input.checkpointBinding,
673
- checkpointKey,
674
- )
675
- : null;
1317
+ // Validate historical inbox checkpoints, but never use retained context as
1318
+ // current provider truth or continue the old full-thread scan.
1319
+ if (input.checkpoint) {
1320
+ parseSlackReactionContextCheckpoint(
1321
+ input.checkpoint,
1322
+ input.checkpointBinding,
1323
+ checkpointKey,
1324
+ );
1325
+ }
676
1326
  const info = await this.requireMemberChannel(headers, input.channelId);
677
1327
  if (info.isShared || info.isExternallyShared || info.isOrgShared) {
678
1328
  throw new SlackBotProviderError("slack_connect_unsupported");
679
1329
  }
680
- const messages: ReturnType<typeof projectMessage>[] = restored
681
- ? restored.state.messages.map(projectSlackReactionCheckpointMessage)
682
- : [];
683
- const seenMessageTimestamps = new Set(restored?.state.seenMessageTimestamps ?? []);
684
- const seenCursors = new Set(restored?.state.seenCursors ?? []);
685
- let cursor: string | null = restored?.state.nextCursor ?? null;
686
- let nextCursor: string | null = cursor;
687
- let threadTimestamp: string | null = restored?.state.threadTimestamp ?? null;
688
- let reactedMessage: ReturnType<typeof projectMessage> | null = null;
689
- const checkpointCreatedAtMs = restored?.state.createdAtMs ?? Date.now();
690
- const firstPage = restored?.state.pageCount ?? 0;
691
-
692
- for (let page = firstPage; page < MAX_REACTION_CONTEXT_PAGES; page += 1) {
1330
+ if (!hasOpenGeniSlackReactionScope(this.connection.grantedScopes)) {
1331
+ throw new SlackBotProviderError("slack_bot_reaction_scope_missing");
1332
+ }
1333
+ // This method is used only for a validated reaction summon. reactions.get
1334
+ // supplies that exact reacted message even when it is far into a thread;
1335
+ // it is not an alternate transport for arbitrary conversation history.
1336
+ const reacted = await this.call(headers, "reactions.get", {
1337
+ channel: input.channelId,
1338
+ timestamp: input.messageTimestamp,
1339
+ full: "true",
1340
+ });
1341
+ if (reacted.type !== "message" || reacted.channel !== input.channelId) {
1342
+ throw new SlackBotProviderError("message_not_found");
1343
+ }
1344
+ const reactedMessage = projectMessage(reacted.message);
1345
+ if (reactedMessage.timestamp !== input.messageTimestamp) {
1346
+ throw new SlackBotProviderError("message_not_found");
1347
+ }
1348
+ const threadTimestamp = reactedMessage.threadTimestamp || reactedMessage.timestamp;
1349
+ let messages = [reactedMessage];
1350
+ let nextCursor: string | null = null;
1351
+ let contextUnavailable = false;
1352
+ try {
693
1353
  const payload = await this.call(headers, "conversations.replies", {
694
1354
  channel: input.channelId,
695
- // Slack accepts either the parent timestamp or a message timestamp from
696
- // inside the thread and returns the containing thread.
697
- ts: input.messageTimestamp,
1355
+ ts: threadTimestamp,
1356
+ latest: reactedMessage.timestamp,
1357
+ inclusive: "true",
698
1358
  limit: String(MAX_REACTION_CONTEXT_MESSAGES),
699
- ...(cursor ? { cursor } : {}),
700
1359
  });
701
1360
  const pageMessages = slackArray(payload.messages)
702
1361
  .map(projectMessage)
703
1362
  .filter((message) => message.timestamp.length > 0);
704
- const first = pageMessages[0];
705
- threadTimestamp ??= first?.threadTimestamp || first?.timestamp || null;
706
- for (const message of pageMessages) {
707
- if (seenMessageTimestamps.has(message.timestamp)) continue;
708
- seenMessageTimestamps.add(message.timestamp);
709
- messages.push(message);
710
- }
711
- reactedMessage =
712
- reactedMessage ??
713
- pageMessages.find((message) => message.timestamp === input.messageTimestamp) ??
714
- null;
1363
+ // Fresh exact message content wins over any duplicate context entry.
1364
+ messages = [
1365
+ ...pageMessages.filter((message) => message.timestamp !== reactedMessage.timestamp),
1366
+ reactedMessage,
1367
+ ];
715
1368
  nextCursor = responseCursor(payload);
716
- if (reactedMessage || !nextCursor) break;
717
- if (seenCursors.has(nextCursor)) {
718
- throw new SlackBotProviderError("reaction_pagination_invalid");
719
- }
720
- seenCursors.add(nextCursor);
721
- const pageCount = page + 1;
722
- if (pageCount >= MAX_REACTION_CONTEXT_PAGES) {
723
- throw new SlackBotProviderError("reaction_pagination_exhausted");
724
- }
725
- const retainedMessages = selectSlackReactionCheckpointMessages(messages);
726
- messages.splice(0, messages.length, ...retainedMessages);
727
- await input.saveCheckpoint(
728
- createSlackReactionContextCheckpoint(
729
- input.checkpointBinding,
730
- {
731
- createdAtMs: checkpointCreatedAtMs,
732
- pageCount,
733
- nextCursor,
734
- seenCursors: [...seenCursors],
735
- seenMessageTimestamps: [...seenMessageTimestamps],
736
- threadTimestamp,
737
- messages: retainedMessages.map(slackReactionCheckpointMessage),
738
- },
739
- checkpointKey,
740
- ),
741
- );
742
- cursor = nextCursor;
743
- }
744
-
745
- if (!reactedMessage || !threadTimestamp) {
746
- throw new SlackBotProviderError("message_not_found");
1369
+ } catch (error) {
1370
+ if (!slackHistoryRateLimited(error)) throw error;
1371
+ contextUnavailable = true;
747
1372
  }
748
1373
  const boundedMessages = selectSlackReactionContextMessages(
749
1374
  messages,
@@ -754,7 +1379,9 @@ export class OpenGeniSlackBotClient {
754
1379
  threadTimestamp,
755
1380
  reactedMessage,
756
1381
  messages: boundedMessages,
757
- truncated: nextCursor !== null || seenMessageTimestamps.size > boundedMessages.length,
1382
+ truncated:
1383
+ contextUnavailable || nextCursor !== null || messages.length > boundedMessages.length,
1384
+ ...(contextUnavailable ? { contextUnavailable: true } : {}),
758
1385
  };
759
1386
  });
760
1387
  }
@@ -880,8 +1507,9 @@ export class OpenGeniSlackBotClient {
880
1507
  }
881
1508
 
882
1509
  /**
883
- * Workspace-wide public search through Slack's Real-time Search API
884
- * (`assistant.search.context`) under the bot identity.
1510
+ * Public search in a trusted Slack interaction through Real-time Search
1511
+ * (`assistant.search.context`) under the bot identity. Unlisted deployments
1512
+ * cannot use this API; approved deployments also need Slack's action token.
885
1513
  *
886
1514
  * The bot never carries private-search authority: `channel_types` is pinned
887
1515
  * to `public_channel` server-side regardless of caller input, so private
@@ -891,6 +1519,8 @@ export class OpenGeniSlackBotClient {
891
1519
  */
892
1520
  async searchContext(input: {
893
1521
  query: string;
1522
+ /** Server-owned Slack interaction token, never an agent tool argument. */
1523
+ actionToken?: string;
894
1524
  contentTypes?: readonly ("messages" | "files" | "channels")[];
895
1525
  includeBots?: boolean;
896
1526
  /** UNIX seconds bounds on message timestamps. */
@@ -909,6 +1539,12 @@ export class OpenGeniSlackBotClient {
909
1539
  ? [...new Set(input.contentTypes)]
910
1540
  : ["messages"];
911
1541
  return await this.withAudit("search.context", async (headers) => {
1542
+ if (this.settings.slackAccessMode !== "full") {
1543
+ throw new SlackBotProviderError("slack_bot_search_unavailable");
1544
+ }
1545
+ if (!input.actionToken) {
1546
+ throw new SlackBotProviderError("slack_bot_search_action_required");
1547
+ }
912
1548
  // Inside the audit boundary so a fail-closed denial on a legacy install
913
1549
  // leaves the same `failed` evidence as a provider rejection.
914
1550
  if (!hasOpenGeniSlackBotSearchScopes(this.connection.grantedScopes)) {
@@ -916,6 +1552,7 @@ export class OpenGeniSlackBotClient {
916
1552
  }
917
1553
  const payload = await this.call(headers, "assistant.search.context", {
918
1554
  query,
1555
+ action_token: input.actionToken,
919
1556
  channel_types: "public_channel",
920
1557
  content_types: contentTypes.join(","),
921
1558
  ...(input.includeBots ? { include_bots: "true" } : {}),
@@ -991,6 +1628,35 @@ export class OpenGeniSlackBotClient {
991
1628
  "file.content.read",
992
1629
  input,
993
1630
  );
1631
+ if (SLACK_REACTION_IMAGE_MIME_TYPES.has(normalizedContentType(file.mimetype))) {
1632
+ if (input.offset !== undefined && input.offset !== 0) {
1633
+ throw new SlackBotProviderError("invalid_file_offset");
1634
+ }
1635
+ // A context file is read only on explicit request. Keep the same
1636
+ // non-shared channel boundary and byte validation as invocation images.
1637
+ await this.requireActiveNonSharedMemberChannel(headers, input.channelId);
1638
+ if (!fileIsSharedToChannel(fileRecord, input.channelId)) {
1639
+ throw new SlackBotProviderError("file_not_shared_to_channel");
1640
+ }
1641
+ if (file.size !== null && (file.size < 1 || file.size > SLACK_REACTION_IMAGE_MAX_BYTES)) {
1642
+ throw new SlackBotProviderError("invalid_file_size");
1643
+ }
1644
+ if (file.size !== null && file.size > SLACK_MCP_IMAGE_MAX_BYTES) {
1645
+ throw new SlackBotProviderError("image_result_too_large");
1646
+ }
1647
+ const image = await this.downloadReactionImage({
1648
+ fileId: file.id,
1649
+ filename: file.name || file.title || file.id,
1650
+ declaredMimeType: normalizedContentType(file.mimetype),
1651
+ declaredSizeBytes: file.size,
1652
+ downloadUrl: privateSlackFileUrl(fileRecord),
1653
+ });
1654
+ if (image.bytes.byteLength > SLACK_MCP_IMAGE_MAX_BYTES) {
1655
+ throw new SlackBotProviderError("image_result_too_large");
1656
+ }
1657
+ await validateSlackMcpImage(image.bytes);
1658
+ return { channel: info, file, image };
1659
+ }
994
1660
  const embeddedTranscript = embeddedHuddleTranscription(fileRecord, parentFileRecord);
995
1661
  const { contentType, content } =
996
1662
  embeddedTranscript ?? (await this.readPrivateFileText(fileRecord, "file.content.read"));
@@ -1051,6 +1717,7 @@ export class OpenGeniSlackBotClient {
1051
1717
  const targetKind = input.userId ? "user" : "channel";
1052
1718
  const targetId = input.userId ?? input.channelId!;
1053
1719
  const blocks = validateSlackMessageBlocks(input.blocks);
1720
+ const wireBlocks = slackMessageWireBlocks(blocks);
1054
1721
  const requestDigest = this.postRequestDigest({
1055
1722
  operationId: input.operationId,
1056
1723
  targetKind,
@@ -1071,10 +1738,10 @@ export class OpenGeniSlackBotClient {
1071
1738
  claimLeaseMs: SLACK_POST_CLAIM_LEASE_MS,
1072
1739
  });
1073
1740
  if (claim.kind === "connection_not_found") {
1074
- throw new Error("OpenGeni Slack bot connection no longer exists");
1741
+ throw new Error("Opengeni Slack bot connection no longer exists");
1075
1742
  }
1076
1743
  if (claim.kind === "conflict") {
1077
- throw new Error("operationId is already bound to a different Slack post request");
1744
+ throw new SlackBotOperationConflictError("post");
1078
1745
  }
1079
1746
  if (claim.kind === "in_progress") {
1080
1747
  throw new Error("Slack post operation is already in progress; retry the same operationId");
@@ -1132,7 +1799,7 @@ export class OpenGeniSlackBotClient {
1132
1799
  const posted = await this.call(headers, "chat.postMessage", {
1133
1800
  channel: channelId,
1134
1801
  text: input.text,
1135
- ...(blocks ? { blocks: JSON.stringify(blocks) } : {}),
1802
+ ...(wireBlocks ? { blocks: JSON.stringify(wireBlocks) } : {}),
1136
1803
  client_msg_id: input.operationId,
1137
1804
  unfurl_links: "false",
1138
1805
  unfurl_media: "false",
@@ -1197,6 +1864,7 @@ export class OpenGeniSlackBotClient {
1197
1864
  let providerCallStarted = false;
1198
1865
  const blocks = validateSlackMessageBlocks(input.blocks);
1199
1866
  try {
1867
+ const wireBlocks = slackMessageWireBlocks(blocks);
1200
1868
  const headers = await this.headersFor(operation);
1201
1869
  await this.requireMemberChannel(headers, input.channelId);
1202
1870
  const requestDigest = this.updateRequestDigest({ ...input, ...(blocks ? { blocks } : {}) });
@@ -1212,10 +1880,10 @@ export class OpenGeniSlackBotClient {
1212
1880
  claimLeaseMs: SLACK_UPDATE_CLAIM_LEASE_MS,
1213
1881
  });
1214
1882
  if (claim.kind === "connection_not_found") {
1215
- throw new Error("OpenGeni Slack bot connection no longer exists");
1883
+ throw new Error("Opengeni Slack bot connection no longer exists");
1216
1884
  }
1217
1885
  if (claim.kind === "conflict") {
1218
- throw new Error("operationId is already bound to a different Slack update request");
1886
+ throw new SlackBotOperationConflictError("update");
1219
1887
  }
1220
1888
  if (claim.kind === "in_progress") {
1221
1889
  throw new Error(
@@ -1235,7 +1903,7 @@ export class OpenGeniSlackBotClient {
1235
1903
  channel: input.channelId,
1236
1904
  ts: input.timestamp,
1237
1905
  text: input.text,
1238
- ...(blocks ? { blocks: JSON.stringify(blocks) } : {}),
1906
+ ...(wireBlocks ? { blocks: JSON.stringify(wireBlocks) } : {}),
1239
1907
  });
1240
1908
  const slackChannelId = requiredSlackString(updated.channel, "channel");
1241
1909
  const slackMessageTimestamp = requiredSlackString(updated.ts, "ts");
@@ -1306,10 +1974,10 @@ export class OpenGeniSlackBotClient {
1306
1974
  claimLeaseMs: SLACK_DELETE_CLAIM_LEASE_MS,
1307
1975
  });
1308
1976
  if (claim.kind === "connection_not_found") {
1309
- throw new Error("OpenGeni Slack bot connection no longer exists");
1977
+ throw new Error("Opengeni Slack bot connection no longer exists");
1310
1978
  }
1311
1979
  if (claim.kind === "conflict") {
1312
- throw new Error("operationId is already bound to a different Slack delete request");
1980
+ throw new SlackBotOperationConflictError("delete");
1313
1981
  }
1314
1982
  if (claim.kind === "in_progress") {
1315
1983
  throw new Error(
@@ -1432,10 +2100,12 @@ export class OpenGeniSlackBotClient {
1432
2100
  let cursor: string | null = null;
1433
2101
  let matchedTimestamp: string | null = null;
1434
2102
  let exhausted = false;
1435
- for (let page = 0; page < MAX_SLACK_POST_RECONCILIATION_PAGES; page += 1) {
2103
+ for (let page = 0; page < this.postReconciliationPageLimit(); page += 1) {
1436
2104
  const payload = await this.call(headers, method, {
1437
2105
  channel: input.channelId,
1438
- limit: String(input.threadTimestamp ? MAX_THREAD_PAGE : MAX_HISTORY_PAGE),
2106
+ limit: String(
2107
+ this.historyPageLimit(input.threadTimestamp ? MAX_THREAD_PAGE : MAX_HISTORY_PAGE),
2108
+ ),
1439
2109
  ...(input.threadTimestamp ? { ts: input.threadTimestamp } : {}),
1440
2110
  ...(cursor ? { cursor } : {}),
1441
2111
  });
@@ -1492,6 +2162,7 @@ export class OpenGeniSlackBotClient {
1492
2162
  private async requireActiveNonSharedMemberChannel(
1493
2163
  headers: SlackCallAuthority,
1494
2164
  channelId: string,
2165
+ privateRecipientSlackUserId?: string,
1495
2166
  ) {
1496
2167
  const projected = await this.requireMemberChannel(headers, channelId);
1497
2168
  if (projected.isArchived) {
@@ -1500,6 +2171,9 @@ export class OpenGeniSlackBotClient {
1500
2171
  if (projected.isShared || projected.isExternallyShared || projected.isOrgShared) {
1501
2172
  throw new SlackBotProviderError("slack_connect_unsupported");
1502
2173
  }
2174
+ if (privateRecipientSlackUserId) {
2175
+ assertSlackPrivateTaskRecipient(projected, privateRecipientSlackUserId);
2176
+ }
1503
2177
  return projected;
1504
2178
  }
1505
2179
 
@@ -1585,13 +2259,28 @@ export class OpenGeniSlackBotClient {
1585
2259
  method: string,
1586
2260
  params: Record<string, string>,
1587
2261
  ): Promise<SlackPayload> {
2262
+ let providerRequestStarted = false;
1588
2263
  try {
1589
- const headers = await this.headersForDestination(
1590
- authority.operation,
1591
- `${SLACK_API_BASE}${method}`,
1592
- );
2264
+ const destination = `${SLACK_API_BASE}${method}`;
2265
+ // Refused or stale authority cannot consume another installation's quota.
2266
+ await this.headersForDestination(authority.operation, destination, "preflight");
2267
+ const retryAfterSeconds = await this.rateLimit(this.metadata.slackTeamId, method);
2268
+ if (retryAfterSeconds > 0) {
2269
+ throw new SlackBotProviderError("rate_limited", retryAfterSeconds * 1_000);
2270
+ }
2271
+ // Quota reservation is an await: recheck live authority after it, as the
2272
+ // final await before each physical provider request.
2273
+ const headers = await this.headersForDestination(authority.operation, destination);
2274
+ providerRequestStarted = true;
1593
2275
  return (await slackApiFetchWithHeaders(this.fetchImpl, method, headers, params)).payload;
1594
2276
  } catch (error) {
2277
+ if (providerRequestStarted && slackHistoryRateLimited(error)) {
2278
+ await this.rateLimit(
2279
+ this.metadata.slackTeamId,
2280
+ method,
2281
+ Math.max(1, Math.ceil((error.retryAfterMs ?? 60_000) / 1_000)),
2282
+ );
2283
+ }
1595
2284
  if (slackCredentialRejected(error)) {
1596
2285
  await setConnectionStatus(this.db, this.context.workspaceId, "needs_reauth", error.code, {
1597
2286
  id: this.connection.id,
@@ -1624,6 +2313,7 @@ export class OpenGeniSlackBotClient {
1624
2313
  private async headersForDestination(
1625
2314
  operation: SlackBotOperation,
1626
2315
  destinationUrl: string,
2316
+ credentialResolutionMode: "execution" | "preflight" = "execution",
1627
2317
  ): Promise<Record<string, string>> {
1628
2318
  const result = await this.resolveCredential({
1629
2319
  workspaceId: this.context.workspaceId,
@@ -1633,13 +2323,22 @@ export class OpenGeniSlackBotClient {
1633
2323
  connectionId: this.connection.id,
1634
2324
  providerDomain: "slack.com",
1635
2325
  kind: "app_install",
1636
- scopes: [...OPENGENI_SLACK_BOT_REQUIRED_SCOPES],
2326
+ scopes: [
2327
+ ...OPENGENI_SLACK_BOT_REQUIRED_SCOPES,
2328
+ ...(operation === "file.upload" ? [OPENGENI_SLACK_FILE_UPLOAD_REQUIRED_SCOPE] : []),
2329
+ ],
1637
2330
  subjectScope: "workspace",
1638
2331
  },
1639
2332
  destinationUrl,
2333
+ credentialResolutionMode,
1640
2334
  });
1641
2335
  if (result.status !== "ok" || result.connectionId !== this.connection.id) {
1642
- throw new Error("OpenGeni Slack bot connection needs to be reinstalled");
2336
+ throw new Error("Opengeni Slack bot connection needs to be reinstalled");
2337
+ }
2338
+ // Caller revalidation may await arbitrary adapter work. Verify bot authority
2339
+ // only after it, immediately before the final provider access fence.
2340
+ if (this.authorizeProviderRequest && (await this.authorizeProviderRequest()) === false) {
2341
+ throw new Error("Opengeni Slack bot provider request is no longer authorized");
1643
2342
  }
1644
2343
  const current = await requireOpenGeniSlackBotConnection(
1645
2344
  this.db,
@@ -1647,6 +2346,9 @@ export class OpenGeniSlackBotClient {
1647
2346
  this.connection.id,
1648
2347
  );
1649
2348
  const currentMetadata = openGeniSlackBotMetadata(current.metadata);
2349
+ if (operation === "file.upload" && !hasOpenGeniSlackFileUploadScope(current.grantedScopes)) {
2350
+ throw new SlackBotProviderError("slack_bot_file_upload_scope_missing");
2351
+ }
1650
2352
  if (
1651
2353
  current.accountId !== this.context.accountId ||
1652
2354
  current.version !== this.connection.version ||
@@ -1656,15 +2358,23 @@ export class OpenGeniSlackBotClient {
1656
2358
  currentMetadata.botId !== this.metadata.botId ||
1657
2359
  currentMetadata.botUserId !== this.metadata.botUserId
1658
2360
  ) {
1659
- throw new Error("OpenGeni Slack bot connection authority changed");
2361
+ throw new Error("Opengeni Slack bot connection authority changed");
1660
2362
  }
1661
- // The adapter callback is a fallible preflight. The canonical credential
1662
- // callback must remain the final await before the physical fetch.
1663
- if (this.authorizeProviderRequest && (await this.authorizeProviderRequest()) === false) {
1664
- throw new Error("OpenGeni Slack bot provider request is no longer authorized");
2363
+ // Recheck the canonical credential after adapter work, then the exact bot
2364
+ // sharing/destination fence immediately before the physical fetch.
2365
+ if (
2366
+ credentialResolutionMode === "execution" &&
2367
+ result.authorizeProviderRequest &&
2368
+ !(await result.authorizeProviderRequest())
2369
+ ) {
2370
+ throw new Error("Opengeni Slack bot provider request is no longer authorized");
1665
2371
  }
1666
- if (result.authorizeProviderRequest && !(await result.authorizeProviderRequest())) {
1667
- throw new Error("OpenGeni Slack bot provider request is no longer authorized");
2372
+ if (
2373
+ this.authorizeBotAccess &&
2374
+ (await this.authorizeBotAccess(new URL(destinationUrl).pathname.split("/").at(-1) ?? "")) ===
2375
+ false
2376
+ ) {
2377
+ throw new Error("Opengeni Slack bot organization access is no longer authorized");
1668
2378
  }
1669
2379
  return result.headers;
1670
2380
  }
@@ -1716,7 +2426,8 @@ export class OpenGeniSlackBotClient {
1716
2426
  method: "GET",
1717
2427
  headers: {
1718
2428
  ...headers,
1719
- accept: "text/*, application/json, application/xml, application/xhtml+xml",
2429
+ accept:
2430
+ "image/png, image/jpeg, image/webp, text/*, application/json, application/xml, application/xhtml+xml",
1720
2431
  },
1721
2432
  redirect: "manual",
1722
2433
  signal: AbortSignal.timeout(SLACK_TIMEOUT_MS),
@@ -1919,12 +2630,22 @@ export class OpenGeniSlackBotClient {
1919
2630
  }
1920
2631
  }
1921
2632
 
2633
+ /** Full decode guards the model image block against header-only or corrupt files. */
2634
+ export async function validateSlackMcpImage(bytes: Uint8Array): Promise<void> {
2635
+ try {
2636
+ await sharp(bytes, { limitInputPixels: 16_000_000, failOn: "error" }).stats();
2637
+ } catch {
2638
+ throw new SlackBotProviderError("invalid_file_content");
2639
+ }
2640
+ }
2641
+
1922
2642
  export function createOpenGeniSlackBotClient(
1923
2643
  deps: {
1924
2644
  db: Database;
1925
2645
  settings: Settings;
1926
2646
  slackFetch?: typeof fetch;
1927
2647
  authorizeProviderRequest?: SlackProviderAuthorization;
2648
+ authorizeBotMutation?: () => Promise<void>;
1928
2649
  },
1929
2650
  resolved: Awaited<ReturnType<typeof resolveSlackBotConnectionForTool>>,
1930
2651
  ): OpenGeniSlackBotClient {
@@ -1936,6 +2657,15 @@ export function createOpenGeniSlackBotClient(
1936
2657
  resolved.context,
1937
2658
  deps.slackFetch,
1938
2659
  deps.authorizeProviderRequest,
2660
+ async (method) => {
2661
+ if (method === "chat.postMessage") await deps.authorizeBotMutation?.();
2662
+ if (
2663
+ resolved.authorizeOrganizationAccess &&
2664
+ (await resolved.authorizeOrganizationAccess()) === false
2665
+ )
2666
+ return false;
2667
+ return true;
2668
+ },
1939
2669
  );
1940
2670
  }
1941
2671
 
@@ -1960,10 +2690,10 @@ export async function createOpenGeniSlackBotInteractionClient(
1960
2690
  input.connectionId,
1961
2691
  );
1962
2692
  if (connection.accountId !== input.accountId) {
1963
- throw new Error("OpenGeni Slack bot connection tenant mismatch");
2693
+ throw new Error("Opengeni Slack bot connection tenant mismatch");
1964
2694
  }
1965
2695
  const metadata = openGeniSlackBotMetadata(connection.metadata);
1966
- if (!metadata) throw new Error("OpenGeni Slack bot connection metadata is invalid");
2696
+ if (!metadata) throw new Error("Opengeni Slack bot connection metadata is invalid");
1967
2697
  return new OpenGeniSlackBotClient(
1968
2698
  deps.db,
1969
2699
  deps.settings,
@@ -1971,13 +2701,27 @@ export async function createOpenGeniSlackBotInteractionClient(
1971
2701
  metadata,
1972
2702
  {
1973
2703
  accountId: input.accountId,
1974
- workspaceId: input.workspaceId,
2704
+ workspaceId: connection.workspaceId,
1975
2705
  subjectId: input.subjectId,
1976
2706
  sessionId: input.sessionId ?? null,
1977
2707
  scheduledTaskId: null,
1978
2708
  },
1979
2709
  deps.slackFetch,
1980
2710
  deps.authorizeProviderRequest,
2711
+ async () => {
2712
+ if (
2713
+ connection.workspaceId !== input.workspaceId &&
2714
+ !(await sharedOrganizationSlackBots(deps.db, input)).some(
2715
+ (share) =>
2716
+ share.connectionId === connection.id &&
2717
+ share.homeWorkspaceId === connection.workspaceId &&
2718
+ share.generation === Number(connection.metadata.organizationSharingGeneration) &&
2719
+ share.connectionVersion === connection.version,
2720
+ )
2721
+ )
2722
+ return false;
2723
+ return true;
2724
+ },
1981
2725
  );
1982
2726
  }
1983
2727
 
@@ -2016,6 +2760,7 @@ async function slackApiFetchWithHeaders(
2016
2760
  "content-type": "application/x-www-form-urlencoded",
2017
2761
  },
2018
2762
  body,
2763
+ redirect: "error",
2019
2764
  signal: AbortSignal.timeout(SLACK_TIMEOUT_MS),
2020
2765
  });
2021
2766
  } catch {
@@ -2060,7 +2805,7 @@ function assertOpenGeniSlackBotScopes(grantedScopes: string[]): void {
2060
2805
  ];
2061
2806
  throw new SlackBotCredentialVerificationError(
2062
2807
  "scope_mismatch",
2063
- `Slack bot scopes do not satisfy the OpenGeni manifest (${facts.join("; ")})`,
2808
+ `Slack bot scopes do not satisfy the Opengeni manifest (${facts.join("; ")})`,
2064
2809
  );
2065
2810
  }
2066
2811
  }
@@ -2093,6 +2838,7 @@ function projectChannel(value: unknown) {
2093
2838
  isMember: channel.is_member === true,
2094
2839
  isDirectMessage: channel.is_im === true,
2095
2840
  isMpim: channel.is_mpim === true,
2841
+ userId: nullableBoundedSlackString(channel.user, 128),
2096
2842
  isArchived: channel.is_archived === true,
2097
2843
  isShared: channel.is_shared === true,
2098
2844
  isExternallyShared: channel.is_ext_shared === true,
@@ -2110,6 +2856,16 @@ function projectChannel(value: unknown) {
2110
2856
  };
2111
2857
  }
2112
2858
 
2859
+ /** A private route needs live provider proof of the exact linked requester's bot IM. */
2860
+ export function assertSlackPrivateTaskRecipient(
2861
+ channel: { isDirectMessage: boolean; isMpim: boolean; userId: string | null },
2862
+ slackUserId: string,
2863
+ ): void {
2864
+ if (!channel.isDirectMessage || channel.isMpim || channel.userId !== slackUserId) {
2865
+ throw new SlackBotProviderError("private_task_recipient_changed");
2866
+ }
2867
+ }
2868
+
2113
2869
  function projectSlackTaskPolicyUser(value: unknown, installationTeamId: string) {
2114
2870
  const user = slackRecord(value);
2115
2871
  const id = slackString(user?.id);
@@ -2183,39 +2939,6 @@ function assertSlackReactionCheckpointBinding(
2183
2939
  }
2184
2940
  }
2185
2941
 
2186
- function createSlackReactionContextCheckpoint(
2187
- binding: SlackReactionContextCheckpointBinding,
2188
- state: SlackReactionContextCheckpointUnsigned["state"],
2189
- key: Uint8Array,
2190
- ): SlackReactionContextCheckpoint {
2191
- const unsigned: SlackReactionContextCheckpointUnsigned = {
2192
- version: SLACK_REACTION_CONTEXT_CHECKPOINT_VERSION,
2193
- binding: { ...binding },
2194
- state: {
2195
- createdAtMs: state.createdAtMs,
2196
- pageCount: state.pageCount,
2197
- nextCursor: state.nextCursor,
2198
- seenCursors: [...state.seenCursors],
2199
- seenMessageTimestamps: [...state.seenMessageTimestamps],
2200
- threadTimestamp: state.threadTimestamp,
2201
- messages: state.messages.map((message) => ({
2202
- ...message,
2203
- files: message.files.map((file) => ({ ...file })),
2204
- })),
2205
- },
2206
- };
2207
- const checkpoint: SlackReactionContextCheckpoint = {
2208
- ...unsigned,
2209
- signature: slackReactionContextCheckpointSignature(unsigned, key),
2210
- };
2211
- if (
2212
- Buffer.byteLength(JSON.stringify(checkpoint), "utf8") > MAX_REACTION_CONTEXT_CHECKPOINT_BYTES
2213
- ) {
2214
- throw new SlackBotProviderError("reaction_checkpoint_too_large");
2215
- }
2216
- return checkpoint;
2217
- }
2218
-
2219
2942
  function parseSlackReactionContextCheckpoint(
2220
2943
  value: unknown,
2221
2944
  expectedBinding: SlackReactionContextCheckpointBinding,
@@ -2434,64 +3157,6 @@ function parseSlackReactionCheckpointMessage(value: unknown): SlackReactionCheck
2434
3157
  };
2435
3158
  }
2436
3159
 
2437
- function slackReactionCheckpointMessage(
2438
- message: ReturnType<typeof projectMessage>,
2439
- ): SlackReactionCheckpointMessage {
2440
- const files: SlackReactionCheckpointMessage["files"] = [];
2441
- let fileLabelChars = 0;
2442
- for (const file of message.files) {
2443
- const label = file.title || file.name || file.id;
2444
- if (!label) continue;
2445
- const addedChars = label.length + (files.length > 0 ? 2 : 0);
2446
- if (
2447
- files.length >= MAX_REACTION_CONTEXT_CHECKPOINT_FILES ||
2448
- fileLabelChars + addedChars > MAX_REACTION_CONTEXT_CHECKPOINT_FILE_LABEL_CHARS
2449
- ) {
2450
- break;
2451
- }
2452
- files.push({ id: file.id, label });
2453
- fileLabelChars += addedChars;
2454
- }
2455
- return {
2456
- timestamp: message.timestamp,
2457
- userId: message.userId,
2458
- botId: message.botId,
2459
- threadTimestamp: message.threadTimestamp,
2460
- text: message.text,
2461
- files,
2462
- };
2463
- }
2464
-
2465
- function projectSlackReactionCheckpointMessage(
2466
- message: SlackReactionCheckpointMessage,
2467
- ): ReturnType<typeof projectMessage> {
2468
- return {
2469
- timestamp: message.timestamp,
2470
- userId: message.userId,
2471
- botId: message.botId,
2472
- threadTimestamp: message.threadTimestamp,
2473
- text: message.text,
2474
- files: message.files.map((file) => ({
2475
- id: file.id,
2476
- name: "",
2477
- title: file.label,
2478
- mimetype: "",
2479
- filetype: "",
2480
- mode: "",
2481
- size: null,
2482
- originatingHuddleId: "",
2483
- huddleTranscriptFileId: "",
2484
- })),
2485
- };
2486
- }
2487
-
2488
- function selectSlackReactionCheckpointMessages(
2489
- messages: ReturnType<typeof projectMessage>[],
2490
- ): ReturnType<typeof projectMessage>[] {
2491
- if (messages.length <= MAX_REACTION_CONTEXT_MESSAGES) return [...messages];
2492
- return [messages[0]!, ...messages.slice(-(MAX_REACTION_CONTEXT_MESSAGES - 1))];
2493
- }
2494
-
2495
3160
  function hasExactSlackCheckpointKeys(value: Record<string, unknown>, expected: string[]): boolean {
2496
3161
  return Object.keys(value).sort().join(",") === [...expected].sort().join(",");
2497
3162
  }
@@ -3000,6 +3665,33 @@ export function nextSlackFilesListPage(
3000
3665
  return nextPage;
3001
3666
  }
3002
3667
 
3668
+ /**
3669
+ * Slack action IDs are unique within a block. Keep the logical card untouched
3670
+ * for the durable request digest, and partition repeated actions only on the
3671
+ * provider wire. In particular, a legacy invalid_blocks receipt can retry with
3672
+ * its original operation ID and digest; completed/uncertain posts keep their
3673
+ * existing replay/reconciliation path. Values and action IDs remain exact.
3674
+ */
3675
+ export function slackMessageWireBlocks(
3676
+ blocks: SlackMessageBlock[] | undefined,
3677
+ ): SlackMessageBlock[] | undefined {
3678
+ if (!blocks) return undefined;
3679
+ const wire = blocks.flatMap((block, index): SlackMessageBlock[] => {
3680
+ if (
3681
+ block.type !== "actions" ||
3682
+ new Set(block.elements.map((e) => e.action_id)).size === block.elements.length
3683
+ )
3684
+ return [block];
3685
+ const id = createHash("sha256").update(`${index}:${block.block_id}`).digest("hex").slice(0, 24);
3686
+ return block.elements.map((element, position) => ({
3687
+ type: "actions",
3688
+ block_id: `opengeni_wire_${id}_${position}`,
3689
+ elements: [element],
3690
+ }));
3691
+ });
3692
+ return validateSlackMessageBlocks(wire);
3693
+ }
3694
+
3003
3695
  function validateSlackMessageBlocks(
3004
3696
  blocks: SlackMessageBlock[] | undefined,
3005
3697
  ): SlackMessageBlock[] | undefined {