@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.
- package/dist/access-grant-rls.d.ts +8 -1
- package/dist/admission-trace.d.ts +8 -0
- package/dist/app.d.ts +50 -3
- package/dist/app.js +10 -1
- package/dist/artifact-catalog-cursor.d.ts +1 -0
- package/dist/attached-browser-viewer.d.ts +11 -0
- package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
- package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +9 -1
- package/dist/auth/managed-auth.d.ts +72 -5
- package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
- package/dist/auth/new-signups-gate.d.ts +23 -0
- package/dist/auth/signup-funnel-metrics.d.ts +44 -0
- package/dist/azure-live.d.ts +28 -0
- package/dist/browser-action-fences.d.ts +13 -0
- package/dist/browser-controller-authority.d.ts +1 -61
- package/dist/browser-ephemeral.d.ts +5 -0
- package/dist/browser-lost-runtime-end.d.ts +24 -0
- package/dist/browser-state-authority.d.ts +1 -27
- package/dist/{chunk-AJ62T5QN.js → chunk-GBUJ2DYZ.js} +80507 -39878
- package/dist/chunk-GBUJ2DYZ.js.map +1 -0
- package/dist/chunk-OGQDXVXY.js +18 -0
- package/dist/chunk-OGQDXVXY.js.map +1 -0
- package/dist/claude-default-models.d.ts +19 -0
- package/dist/claude-subscription-core.d.ts +50 -0
- package/dist/claude-subscription-oauth.d.ts +36 -0
- package/dist/claude-subscription-profile.d.ts +9 -0
- package/dist/claude-subscription-usage.d.ts +6 -0
- package/dist/claude-workspace-connection.d.ts +15 -0
- package/dist/codemode.d.ts +40 -5
- package/dist/codex-realtime.d.ts +29 -3
- package/dist/codex-session-pointers.d.ts +19 -0
- package/dist/connection-ownership.d.ts +6 -3
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/credit-promotion-snapshot.d.ts +6 -0
- package/dist/editable-artifact-source-session.d.ts +3 -0
- package/dist/editable-artifact-websocket.d.ts +5 -3
- package/dist/fatal-process-boundary.d.ts +9 -1
- package/dist/github-access.d.ts +7 -1
- package/dist/github-browser-flow.d.ts +1 -1
- package/dist/http/acting-person.d.ts +36 -0
- package/dist/http/api-contract-compat.d.ts +19 -0
- package/dist/http/api-error.d.ts +50 -1
- package/dist/http/auth.d.ts +8 -1
- package/dist/http/cors.d.ts +12 -0
- package/dist/http/deprecation.d.ts +42 -0
- package/dist/http/keyed-admission.d.ts +12 -0
- package/dist/http/local-browser-boundary.d.ts +72 -0
- package/dist/http/metrics-listener.d.ts +22 -0
- package/dist/http/path-identifier.d.ts +17 -0
- package/dist/http/registered-route-label.d.ts +16 -0
- package/dist/http/rejection-telemetry.d.ts +34 -0
- package/dist/http/request-body.d.ts +27 -0
- package/dist/http/request-source.d.ts +41 -3
- package/dist/http/sse.d.ts +12 -1
- package/dist/http/unmatched-route.d.ts +18 -0
- package/dist/http/user-content.d.ts +36 -0
- package/dist/http/variable-sets-removed.d.ts +41 -0
- package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
- package/dist/index.js +1279 -108
- package/dist/index.js.map +1 -1
- package/dist/integration-connect-metrics.d.ts +35 -0
- package/dist/integration-endpoint-test.d.ts +20 -0
- package/dist/integrations/api-integrations.d.ts +2 -2
- package/dist/integrations/atlassian.d.ts +8 -3
- package/dist/integrations/fiken.d.ts +3 -1
- package/dist/integrations/github-lens-connect.d.ts +37 -66
- package/dist/integrations/google-drive.d.ts +2 -0
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/marketplace-plugin.d.ts +43 -43
- package/dist/integrations/oauth-client.d.ts +171 -12
- package/dist/integrations/oauth-profiles.d.ts +42 -13
- package/dist/integrations/onepassword-vault-provider.d.ts +9 -0
- package/dist/integrations/slack-bot.d.ts +285 -7
- package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
- package/dist/integrations/slack-interactions.d.ts +89 -10
- package/dist/integrations/slack-mrkdwn.d.ts +41 -0
- package/dist/integrations/slack-routing.d.ts +1 -1
- package/dist/integrations/slack-session-defaults.d.ts +58 -0
- package/dist/integrations/slack-task-file-upload.d.ts +36 -0
- package/dist/integrations/slack-tool-review.d.ts +5 -0
- package/dist/interaction-agent-access.d.ts +13 -8
- package/dist/interaction-agent-version.d.ts +21 -0
- package/dist/interaction-holder-heartbeat.d.ts +5 -0
- package/dist/interaction-metrics.d.ts +27 -2
- package/dist/interaction-route-stages.d.ts +25 -0
- package/dist/interaction-secrets-vault.d.ts +20 -0
- package/dist/interaction-secrets.d.ts +103 -0
- package/dist/mcp/action-catalog-types.d.ts +14 -0
- package/dist/mcp/action-catalog.gen.d.ts +2 -0
- package/dist/mcp/action-descriptions.d.ts +4 -0
- package/dist/mcp/action-schema-imports.gen.d.ts +1 -0
- package/dist/mcp/capability-account-readiness.d.ts +7 -0
- package/dist/mcp/child-read-evidence.d.ts +9 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +12 -0
- package/dist/mcp/contract-input.d.ts +13 -0
- package/dist/mcp/documents.d.ts +2 -0
- package/dist/mcp/first-party-tool-permissions.d.ts +3 -606
- package/dist/mcp/notification-tools.d.ts +26 -0
- package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
- package/dist/mcp/scheduled-task-input.d.ts +1309 -0
- package/dist/mcp/scheduled-task-view.d.ts +217 -3
- package/dist/mcp/secret-tools.d.ts +14 -0
- package/dist/mcp/server.d.ts +108 -5
- package/dist/mcp/session-admin-access-tools.d.ts +33 -0
- package/dist/mcp/session-event-errors.d.ts +28 -0
- package/dist/mcp/session-event-view.d.ts +62 -0
- package/dist/mcp/session-view.d.ts +15 -0
- package/dist/mcp/session-wait.d.ts +18 -0
- package/dist/mcp/site-agent-clamp-tools.d.ts +38 -0
- package/dist/mcp/site-data-tools.d.ts +22 -0
- package/dist/mcp/site-publication-tools.d.ts +19 -0
- package/dist/mcp/site-sql-tools.d.ts +27 -0
- package/dist/mcp-oauth.d.ts +37 -4
- package/dist/model-calls/chat-completions.d.ts +87 -0
- package/dist/model-calls/pending-usage-settlements.d.ts +57 -0
- package/dist/model-calls/service.d.ts +36 -0
- package/dist/model-catalog.d.ts +9 -3
- package/dist/native-push-dispatch.d.ts +69 -0
- package/dist/observability.d.ts +3 -1
- package/dist/organization-mcp.d.ts +82 -0
- package/dist/prepared-mcp-actor.d.ts +77 -0
- package/dist/prepared-mcp-permissions.d.ts +8 -0
- package/dist/routes/analytics-consent.d.ts +26 -0
- package/dist/routes/api-integrations.d.ts +11 -0
- package/dist/routes/api-keys.d.ts +7 -7
- package/dist/routes/automations.d.ts +2 -0
- package/dist/routes/billing.d.ts +59 -0
- package/dist/routes/browser-sessions.d.ts +65 -2
- package/dist/routes/chat-completions.d.ts +11 -0
- package/dist/routes/claude-core-usage.d.ts +20 -0
- package/dist/routes/claude-subscription-accounts.d.ts +3 -0
- package/dist/routes/claude-subscription-oauth.d.ts +3 -0
- package/dist/routes/client-errors.d.ts +182 -0
- package/dist/routes/codex-core.d.ts +401 -0
- package/dist/routes/codex.d.ts +64 -2
- package/dist/routes/computer-sessions.d.ts +6 -1
- package/dist/routes/desktop-distribution.d.ts +32 -0
- package/dist/routes/github-browser-fonts.gen.d.ts +2 -0
- package/dist/routes/github-browser-pages.d.ts +27 -0
- package/dist/routes/inbox.d.ts +13 -0
- package/dist/routes/insights-response-cache.d.ts +10 -0
- package/dist/routes/insights-usage.d.ts +12 -0
- package/dist/routes/insights.d.ts +28 -0
- package/dist/routes/interaction-placement-message.d.ts +7 -0
- package/dist/routes/managed-sign-in-methods.d.ts +6 -0
- package/dist/routes/model-account-choice.d.ts +13 -0
- package/dist/routes/model-account-settings.d.ts +19 -0
- package/dist/routes/native-app-auth.d.ts +12 -0
- package/dist/routes/organization-integration-policy.d.ts +8 -0
- package/dist/routes/organization-integrations.d.ts +6 -0
- package/dist/routes/organization-mcp-connections.d.ts +3 -0
- package/dist/routes/organization-model-defaults.d.ts +9 -0
- package/dist/routes/organization-service-accounts.d.ts +3 -0
- package/dist/routes/personal-api-keys.d.ts +3 -0
- package/dist/routes/session-admin-access.d.ts +3 -0
- package/dist/routes/session-artifact-associations.d.ts +4 -0
- package/dist/routes/session-history-imports.d.ts +20 -0
- package/dist/routes/sessions.d.ts +38 -2
- package/dist/routes/site-data.d.ts +18 -0
- package/dist/routes/site-publishing.d.ts +5 -0
- package/dist/routes/site-runtime.d.ts +31 -0
- package/dist/routes/site-sql.d.ts +16 -0
- package/dist/routes/slack-bot-access.d.ts +3 -0
- package/dist/routes/subscription-account-pools.d.ts +21 -0
- package/dist/routes/subscription-core-http.d.ts +5 -0
- package/dist/routes/subscription-pool-access.d.ts +37 -0
- package/dist/routes/subscription-pool-core.d.ts +153 -0
- package/dist/routes/supergrok.d.ts +6 -6
- package/dist/routes/usage-allowances.d.ts +12 -0
- package/dist/routes/variable-sets-removed.d.ts +2 -0
- package/dist/routes/vault-providers.d.ts +34 -0
- package/dist/routes/vaults.d.ts +3 -0
- package/dist/routes/workspace-integrations.d.ts +35 -0
- package/dist/routes/workspace-model-providers.d.ts +4 -0
- package/dist/routes/workspaces.d.ts +11 -2
- package/dist/sandbox/agent-update.d.ts +64 -0
- package/dist/sandbox/auth-callout.d.ts +8 -0
- package/dist/sandbox/channel-a.d.ts +15 -1
- package/dist/sandbox/enrollment.d.ts +26 -4
- package/dist/sandbox/machine-audience.d.ts +29 -0
- package/dist/sandbox/machines.d.ts +15 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -2
- package/dist/sandbox/screen-control.d.ts +85 -0
- package/dist/sandbox/viewer.d.ts +5 -1
- package/dist/sandbox-file-artifacts.d.ts +17 -1
- package/dist/scheduled-task-blockers.d.ts +39 -0
- package/dist/scheduled-task-deletion.d.ts +3 -4
- package/dist/scheduled-task-lifecycle.d.ts +14 -0
- package/dist/secret-answers.d.ts +40 -0
- package/dist/session-create-observability.d.ts +5 -0
- package/dist/session-event-view-36GO3IL7.js +528 -0
- package/dist/session-event-view-36GO3IL7.js.map +1 -0
- package/dist/signed-state-secret.d.ts +18 -0
- package/dist/site-agent-guard.d.ts +53 -0
- package/dist/site-data/operations.d.ts +103 -0
- package/dist/site-data/service.d.ts +203 -0
- package/dist/site-frame-guard.d.ts +32 -0
- package/dist/site-publishing/agent-data-tools.d.ts +84 -0
- package/dist/site-publishing/agent-model.d.ts +58 -0
- package/dist/site-publishing/agent-session.d.ts +16 -0
- package/dist/site-publishing/content-scanner.d.ts +31 -0
- package/dist/site-publishing/edge-sync.d.ts +48 -0
- package/dist/site-publishing/gateway-signature.d.ts +56 -0
- package/dist/site-publishing/limits.d.ts +63 -0
- package/dist/site-publishing/page-tool-authority.d.ts +30 -0
- package/dist/site-publishing/platform-usage.d.ts +113 -0
- package/dist/site-publishing/platform.d.ts +21 -0
- package/dist/site-publishing/readiness.d.ts +62 -0
- package/dist/site-publishing/runtime.d.ts +179 -0
- package/dist/site-publishing/service.d.ts +264 -0
- package/dist/site-publishing/session-events.d.ts +22 -0
- package/dist/site-publishing/session-views.d.ts +36 -0
- package/dist/site-publishing/viewer.d.ts +40 -0
- package/dist/site-session-origin.d.ts +31 -6
- package/dist/site-sql/format.d.ts +47 -0
- package/dist/site-sql/gateway.d.ts +50 -0
- package/dist/site-sql/service.d.ts +213 -0
- package/dist/temporal-schedule-sync.d.ts +26 -0
- package/dist/transcription/billing-refusal.d.ts +8 -0
- package/dist/transcription/normalize.d.ts +23 -0
- package/dist/transcription/providers/azure-mai.d.ts +12 -0
- package/dist/transcription/providers/azure-openai.d.ts +4 -0
- package/dist/transcription/providers/openai.d.ts +16 -0
- package/dist/transcription/providers/subscription-core.d.ts +39 -0
- package/dist/transcription/service.d.ts +10 -1
- package/dist/transcription/wav-duration.d.ts +8 -0
- package/dist/workspace-tool-gateway-attestations.d.ts +37 -0
- package/dist/workspace-tool-gateway-observability.d.ts +16 -0
- package/dist/workspace-tool-gateway-secrets.d.ts +27 -0
- package/dist/workspace-tool-gateway.d.ts +102 -6
- package/dist/workspace-tool-target-fence.d.ts +116 -0
- package/dist/workspace-tool-target.d.ts +225 -0
- package/dist/workspace-webhook-dispatch.d.ts +26 -0
- package/dist/xai-subscription-core.d.ts +60 -0
- package/package.json +25 -20
- package/src/access-grant-rls.ts +64 -14
- package/src/admission-trace.ts +14 -0
- package/src/app.ts +1684 -252
- package/src/artifact-catalog-cursor.ts +1 -0
- package/src/attached-browser-viewer.ts +26 -0
- package/src/auth/managed-auth-attempt-context.ts +9 -0
- package/src/auth/managed-auth-rate-limits.ts +252 -0
- package/src/auth/managed-auth-session-adapter.ts +64 -29
- package/src/auth/managed-auth.ts +303 -35
- package/src/auth/managed-email.ts +18 -1
- package/src/auth/managed-sign-in-notifications.ts +81 -0
- package/src/auth/new-signups-gate.ts +49 -0
- package/src/auth/organization-user-setup.ts +3 -3
- package/src/auth/signup-funnel-metrics.ts +164 -0
- package/src/azure-live.ts +103 -0
- package/src/browser-action-fences.ts +45 -0
- package/src/browser-controller-authority.ts +1 -186
- package/src/browser-ephemeral.ts +39 -0
- package/src/browser-lost-runtime-end.ts +71 -0
- package/src/browser-state-authority.ts +1 -236
- package/src/claude-default-models.ts +37 -0
- package/src/claude-subscription-core.ts +154 -0
- package/src/claude-subscription-oauth.ts +352 -0
- package/src/claude-subscription-profile.ts +73 -0
- package/src/claude-subscription-usage.ts +51 -0
- package/src/claude-workspace-connection.ts +82 -0
- package/src/codemode.ts +236 -39
- package/src/codex-realtime.ts +181 -53
- package/src/codex-session-pointers.ts +80 -0
- package/src/company-brain-okf.ts +1 -1
- package/src/connected-machine-computer-access.ts +2 -1
- package/src/connection-ownership.ts +19 -13
- package/src/controller-data-plane.ts +34 -0
- package/src/credit-promotion-snapshot.ts +36 -0
- package/src/editable-artifact-production.ts +10 -0
- package/src/editable-artifact-source-session.ts +123 -0
- package/src/editable-artifact-websocket.ts +16 -3
- package/src/fatal-process-boundary.ts +67 -3
- package/src/gateway-realtime.ts +12 -8
- package/src/github-access.ts +31 -5
- package/src/github-browser-flow.ts +1 -1
- package/src/http/acting-person.ts +105 -0
- package/src/http/api-contract-compat.ts +49 -0
- package/src/http/api-error.ts +210 -2
- package/src/http/auth.ts +68 -4
- package/src/http/cors.ts +37 -5
- package/src/http/deprecation.ts +118 -0
- package/src/http/keyed-admission.ts +29 -0
- package/src/http/local-browser-boundary.ts +343 -0
- package/src/http/metrics-listener.ts +57 -0
- package/src/http/path-identifier.ts +54 -0
- package/src/http/registered-route-label.ts +34 -0
- package/src/http/rejection-telemetry.ts +120 -0
- package/src/http/request-body.ts +127 -0
- package/src/http/request-source.ts +154 -13
- package/src/http/sse.ts +88 -28
- package/src/http/unmatched-route.ts +60 -0
- package/src/http/user-content.ts +158 -0
- package/src/http/variable-sets-removed.ts +122 -0
- package/src/http/websocket-upgrade-dispatch.ts +29 -0
- package/src/index.ts +107 -57
- package/src/integration-connect-metrics.ts +228 -0
- package/src/integration-endpoint-test.ts +291 -0
- package/src/integrations/api-integrations.ts +81 -7
- package/src/integrations/atlassian.ts +202 -83
- package/src/integrations/fiken.ts +66 -38
- package/src/integrations/github-app-connect.ts +64 -18
- package/src/integrations/github-lens-connect.ts +8 -29
- package/src/integrations/google-drive.ts +448 -152
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/marketplace-plugin.ts +1 -1
- package/src/integrations/oauth-client.ts +733 -193
- package/src/integrations/oauth-profiles.ts +116 -45
- package/src/integrations/onepassword-vault-provider.ts +16 -0
- package/src/integrations/personal-github-repositories.ts +1 -1
- package/src/integrations/personal-github.ts +61 -35
- package/src/integrations/provider-oauth.ts +67 -85
- package/src/integrations/slack-app-home.ts +5 -5
- package/src/integrations/slack-bot.ts +893 -201
- package/src/integrations/slack-file-upload-flow.ts +147 -0
- package/src/integrations/slack-install.ts +7 -7
- package/src/integrations/slack-interactions.ts +1589 -299
- package/src/integrations/slack-mrkdwn.ts +193 -0
- package/src/integrations/slack-routing.ts +2 -2
- package/src/integrations/slack-session-defaults.ts +224 -0
- package/src/integrations/slack-task-file-upload.ts +323 -0
- package/src/integrations/slack-tool-review.ts +65 -0
- package/src/integrations/social-oauth.ts +90 -58
- package/src/interaction-agent-access.ts +40 -20
- package/src/interaction-agent-version.ts +85 -0
- package/src/interaction-holder-heartbeat.ts +24 -8
- package/src/interaction-metrics.ts +164 -2
- package/src/interaction-route-stages.ts +94 -0
- package/src/interaction-secrets-vault.ts +213 -0
- package/src/interaction-secrets.ts +426 -0
- package/src/mcp/action-catalog-types.ts +14 -0
- package/src/mcp/action-catalog.gen.ts +8014 -0
- package/src/mcp/action-descriptions.ts +299 -0
- package/src/mcp/action-schema-imports.gen.ts +304 -0
- package/src/mcp/capability-account-readiness.ts +17 -0
- package/src/mcp/child-read-evidence.ts +19 -0
- package/src/mcp/company-profile-agent-admin.ts +13 -0
- package/src/mcp/contract-input.ts +165 -0
- package/src/mcp/documents.ts +9 -2
- package/src/mcp/editable-artifacts.ts +29 -11
- package/src/mcp/files.ts +5 -1
- package/src/mcp/first-party-tool-permissions.ts +7 -285
- package/src/mcp/knowledge-entries.ts +108 -34
- package/src/mcp/notification-tools.ts +480 -0
- package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
- package/src/mcp/scheduled-task-input.ts +89 -0
- package/src/mcp/scheduled-task-view.ts +63 -3
- package/src/mcp/secret-tools.ts +185 -0
- package/src/mcp/server.ts +2213 -1116
- package/src/mcp/session-admin-access-tools.ts +163 -0
- package/src/mcp/session-event-errors.ts +123 -0
- package/src/mcp/session-event-view.ts +395 -13
- package/src/mcp/session-view.ts +3 -0
- package/src/mcp/session-wait.ts +67 -7
- package/src/mcp/site-agent-clamp-tools.ts +140 -0
- package/src/mcp/site-data-tools.ts +339 -0
- package/src/mcp/site-publication-tools.ts +201 -0
- package/src/mcp/site-sql-tools.ts +427 -0
- package/src/mcp-oauth.ts +635 -45
- package/src/model-calls/chat-completions.ts +352 -0
- package/src/model-calls/pending-usage-settlements.ts +125 -0
- package/src/model-calls/service.ts +694 -0
- package/src/model-catalog.ts +68 -17
- package/src/native-push-dispatch.ts +457 -0
- package/src/observability.ts +15 -1
- package/src/organization-mcp.ts +485 -0
- package/src/prepared-mcp-actor.ts +154 -0
- package/src/prepared-mcp-permissions.ts +38 -0
- package/src/routes/analytics-consent.ts +123 -0
- package/src/routes/api-integrations.ts +268 -9
- package/src/routes/api-keys.ts +164 -25
- package/src/routes/artifact-catalog.ts +111 -0
- package/src/routes/automations.ts +163 -90
- package/src/routes/billing.ts +535 -59
- package/src/routes/browser-sessions.ts +966 -342
- package/src/routes/canonical-human-identities.ts +61 -0
- package/src/routes/capabilities.ts +112 -5
- package/src/routes/channels.ts +4 -3
- package/src/routes/chat-completions.ts +235 -0
- package/src/routes/claude-core-usage.ts +147 -0
- package/src/routes/claude-subscription-accounts.ts +233 -0
- package/src/routes/claude-subscription-oauth.ts +175 -0
- package/src/routes/client-errors.ts +394 -0
- package/src/routes/codex-core.ts +687 -0
- package/src/routes/codex.ts +1277 -1119
- package/src/routes/company-brain.ts +4 -0
- package/src/routes/computer-sessions.ts +319 -85
- package/src/routes/connect.ts +277 -175
- package/src/routes/connections.ts +690 -363
- package/src/routes/desktop-distribution.ts +217 -0
- package/src/routes/documents.ts +13 -7
- package/src/routes/editable-artifacts.ts +74 -4
- package/src/routes/enrollments.ts +110 -16
- package/src/routes/external-identity-links.ts +13 -12
- package/src/routes/files.ts +41 -16
- package/src/routes/github-browser-fonts.gen.ts +12 -0
- package/src/routes/github-browser-pages.ts +258 -0
- package/src/routes/github.ts +200 -75
- package/src/routes/inbox.ts +353 -0
- package/src/routes/insights-response-cache.ts +83 -0
- package/src/routes/insights-usage.ts +345 -0
- package/src/routes/insights.ts +103 -8
- package/src/routes/install.ts +51 -10
- package/src/routes/integration-facets.ts +70 -28
- package/src/routes/interaction-placement-message.ts +12 -0
- package/src/routes/knowledge.ts +136 -27
- package/src/routes/machines.ts +189 -135
- package/src/routes/managed-auth-session-sets.ts +43 -10
- package/src/routes/managed-onboarding.ts +39 -19
- package/src/routes/managed-sign-in-methods.ts +433 -0
- package/src/routes/memory-slack-publications.ts +3 -2
- package/src/routes/model-account-choice.ts +194 -0
- package/src/routes/model-account-settings.ts +235 -0
- package/src/routes/model-connection-access.ts +210 -48
- package/src/routes/native-app-auth.ts +296 -0
- package/src/routes/organization-integration-policy.ts +116 -0
- package/src/routes/organization-integrations.ts +326 -0
- package/src/routes/organization-mcp-connections.ts +183 -0
- package/src/routes/organization-memberships.ts +205 -19
- package/src/routes/organization-model-defaults.ts +110 -0
- package/src/routes/organization-model-providers.ts +74 -3
- package/src/routes/organization-service-accounts.ts +128 -0
- package/src/routes/organization-sessions.ts +21 -5
- package/src/routes/personal-api-keys.ts +184 -0
- package/src/routes/personal-github-git-broker.ts +2 -2
- package/src/routes/personal-github.ts +9 -4
- package/src/routes/plugins.ts +46 -7
- package/src/routes/pr-review-github.ts +105 -118
- package/src/routes/pr-review.ts +88 -82
- package/src/routes/preference-registry.ts +18 -0
- package/src/routes/rigs.ts +48 -13
- package/src/routes/scheduled-tasks.ts +347 -27
- package/src/routes/session-admin-access.ts +271 -0
- package/src/routes/session-artifact-associations.ts +164 -0
- package/src/routes/session-history-imports.ts +305 -0
- package/src/routes/sessions.ts +1682 -363
- package/src/routes/site-data.ts +340 -0
- package/src/routes/site-publishing.ts +584 -0
- package/src/routes/site-runtime.ts +1944 -0
- package/src/routes/site-sql.ts +263 -0
- package/src/routes/skill-content.ts +72 -7
- package/src/routes/skills.ts +5 -4
- package/src/routes/slack-bot-access.ts +108 -0
- package/src/routes/social.ts +27 -15
- package/src/routes/subscription-account-pools.ts +209 -0
- package/src/routes/subscription-core-http.ts +18 -0
- package/src/routes/subscription-pool-access.ts +180 -0
- package/src/routes/subscription-pool-core.ts +552 -0
- package/src/routes/supergrok.ts +174 -655
- package/src/routes/transcription-recordings.ts +71 -4
- package/src/routes/transcriptions.ts +26 -2
- package/src/routes/usage-allowances.ts +381 -0
- package/src/routes/variable-sets-removed.ts +19 -0
- package/src/routes/vault-providers.ts +782 -0
- package/src/routes/vaults.ts +421 -0
- package/src/routes/video-generation.ts +30 -12
- package/src/routes/workspace-artifacts.ts +165 -53
- package/src/routes/workspace-integrations.ts +576 -0
- package/src/routes/workspace-model-providers.ts +148 -0
- package/src/routes/workspace-state.ts +4 -0
- package/src/routes/workspaces.ts +625 -117
- package/src/sandbox/agent-update.ts +315 -0
- package/src/sandbox/auth-callout.ts +104 -21
- package/src/sandbox/channel-a.ts +150 -49
- package/src/sandbox/enrollment.ts +131 -22
- package/src/sandbox/machine-audience.ts +192 -0
- package/src/sandbox/machines.ts +75 -31
- package/src/sandbox/metrics-ingestion.ts +133 -2
- package/src/sandbox/rematerialize.ts +51 -21
- package/src/sandbox/screen-control.ts +289 -0
- package/src/sandbox/viewer.ts +33 -58
- package/src/sandbox-file-artifacts.ts +53 -27
- package/src/scheduled-task-blockers.ts +293 -0
- package/src/scheduled-task-deletion.ts +13 -3
- package/src/scheduled-task-lifecycle.ts +84 -0
- package/src/secret-answers.ts +119 -0
- package/src/session-create-observability.ts +46 -0
- package/src/session-realtime-context.ts +26 -4
- package/src/signed-state-secret.ts +58 -0
- package/src/site-agent-guard.ts +261 -0
- package/src/site-data/operations.ts +777 -0
- package/src/site-data/service.ts +1043 -0
- package/src/site-frame-guard.ts +107 -0
- package/src/site-publishing/agent-data-tools.ts +250 -0
- package/src/site-publishing/agent-model.ts +127 -0
- package/src/site-publishing/agent-session.ts +148 -0
- package/src/site-publishing/content-scanner.ts +93 -0
- package/src/site-publishing/edge-sync.ts +148 -0
- package/src/site-publishing/gateway-signature.ts +198 -0
- package/src/site-publishing/limits.ts +315 -0
- package/src/site-publishing/page-tool-authority.ts +111 -0
- package/src/site-publishing/platform-usage.ts +479 -0
- package/src/site-publishing/platform.ts +98 -0
- package/src/site-publishing/readiness.ts +330 -0
- package/src/site-publishing/runtime.ts +831 -0
- package/src/site-publishing/service.ts +1282 -0
- package/src/site-publishing/session-events.ts +262 -0
- package/src/site-publishing/session-views.ts +262 -0
- package/src/site-publishing/viewer.ts +75 -0
- package/src/site-session-origin.ts +91 -6
- package/src/site-sql/format.ts +134 -0
- package/src/site-sql/gateway.ts +168 -0
- package/src/site-sql/service.ts +1636 -0
- package/src/site-uploads.ts +27 -5
- package/src/temporal-schedule-sync.ts +74 -0
- package/src/transcription/billing-refusal.ts +14 -0
- package/src/transcription/normalize.ts +77 -0
- package/src/transcription/providers/azure-mai.ts +98 -0
- package/src/transcription/providers/azure-openai.ts +16 -7
- package/src/transcription/providers/codex-subscription.ts +190 -67
- package/src/transcription/providers/openai.ts +79 -5
- package/src/transcription/providers/subscription-core.ts +125 -0
- package/src/transcription/providers/xai-subscription.ts +81 -83
- package/src/transcription/segmenter.ts +7 -1
- package/src/transcription/service.ts +239 -10
- package/src/transcription/wav-duration.ts +39 -0
- package/src/workspace-deletion.ts +25 -1
- package/src/workspace-tool-gateway-attestations.ts +133 -0
- package/src/workspace-tool-gateway-observability.ts +64 -0
- package/src/workspace-tool-gateway-secrets.ts +104 -0
- package/src/workspace-tool-gateway.ts +921 -103
- package/src/workspace-tool-target-fence.ts +380 -0
- package/src/workspace-tool-target.ts +554 -0
- package/src/workspace-webhook-dispatch.ts +225 -0
- package/src/xai-realtime.ts +85 -81
- package/src/xai-subscription-core.ts +172 -0
- package/dist/browser-auth-broker.d.ts +0 -20
- package/dist/chunk-AJ62T5QN.js.map +0 -1
- package/dist/connection-authority-owner.d.ts +0 -59
- package/dist/routes/connection-authorities.d.ts +0 -3
- package/dist/routes/environments.d.ts +0 -5
- package/dist/routes/host-mcp-bindings.d.ts +0 -5
- package/dist/routes/host-mcp-resolvers.d.ts +0 -3
- package/dist/routes/packs.d.ts +0 -3
- package/dist/session-event-view-MU3HMNV3.js +0 -264
- package/dist/session-event-view-MU3HMNV3.js.map +0 -1
- package/dist/xai-subscription-auth.d.ts +0 -23
- package/src/browser-auth-broker.ts +0 -155
- package/src/connection-authority-owner.ts +0 -61
- package/src/routes/connection-authorities.ts +0 -139
- package/src/routes/environments.ts +0 -388
- package/src/routes/host-mcp-bindings.ts +0 -122
- package/src/routes/host-mcp-resolvers.ts +0 -41
- package/src/routes/packs.ts +0 -784
- 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
|
-
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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("
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
717
|
-
if (
|
|
718
|
-
|
|
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:
|
|
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
|
-
*
|
|
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("
|
|
1741
|
+
throw new Error("Opengeni Slack bot connection no longer exists");
|
|
1075
1742
|
}
|
|
1076
1743
|
if (claim.kind === "conflict") {
|
|
1077
|
-
throw new
|
|
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
|
-
...(
|
|
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("
|
|
1883
|
+
throw new Error("Opengeni Slack bot connection no longer exists");
|
|
1216
1884
|
}
|
|
1217
1885
|
if (claim.kind === "conflict") {
|
|
1218
|
-
throw new
|
|
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
|
-
...(
|
|
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("
|
|
1977
|
+
throw new Error("Opengeni Slack bot connection no longer exists");
|
|
1310
1978
|
}
|
|
1311
1979
|
if (claim.kind === "conflict") {
|
|
1312
|
-
throw new
|
|
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 <
|
|
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(
|
|
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
|
|
1590
|
-
|
|
1591
|
-
|
|
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: [
|
|
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("
|
|
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("
|
|
2361
|
+
throw new Error("Opengeni Slack bot connection authority changed");
|
|
1660
2362
|
}
|
|
1661
|
-
//
|
|
1662
|
-
//
|
|
1663
|
-
if (
|
|
1664
|
-
|
|
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 (
|
|
1667
|
-
|
|
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:
|
|
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("
|
|
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("
|
|
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:
|
|
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
|
|
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 {
|