@opengeni/api-router 0.30.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-grant-rls.d.ts +3 -0
- package/dist/admission-trace.d.ts +8 -0
- package/dist/api-websocket.d.ts +18 -0
- package/dist/app.d.ts +50 -2
- package/dist/app.js +12 -1
- package/dist/artifact-catalog-cursor.d.ts +27 -0
- package/dist/auth/managed-auth-attempt-context.d.ts +10 -0
- package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +6 -0
- package/dist/auth/managed-auth.d.ts +102 -3
- package/dist/auth/managed-email.d.ts +29 -0
- package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
- package/dist/auth/new-signups-gate.d.ts +23 -0
- package/dist/auth/organization-user-setup.d.ts +80 -0
- package/dist/auth/signup-funnel-metrics.d.ts +44 -0
- package/dist/browser-controller-authority.d.ts +1 -61
- package/dist/browser-ephemeral.d.ts +5 -0
- package/dist/browser-state-authority.d.ts +1 -27
- package/dist/chunk-6TZ5LG3O.js +123948 -0
- package/dist/chunk-6TZ5LG3O.js.map +1 -0
- package/dist/chunk-OGQDXVXY.js +18 -0
- package/dist/chunk-OGQDXVXY.js.map +1 -0
- package/dist/claude-subscription-account-conflicts.d.ts +1 -0
- package/dist/claude-subscription-account-usage.d.ts +39 -0
- package/dist/claude-subscription-oauth.d.ts +25 -0
- package/dist/claude-subscription-profile.d.ts +9 -0
- package/dist/claude-subscription-usage.d.ts +44 -0
- package/dist/claude-workspace-connection.d.ts +15 -0
- package/dist/codemode.d.ts +32 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-ownership.d.ts +54 -0
- package/dist/controller-data-plane.d.ts +32 -0
- package/dist/editable-artifact-source-session.d.ts +3 -0
- package/dist/editable-artifact-websocket.d.ts +10 -18
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/fatal-process-boundary.d.ts +25 -0
- package/dist/github-access.d.ts +31 -2
- package/dist/http/acting-person.d.ts +24 -0
- package/dist/http/api-contract-compat.d.ts +19 -0
- package/dist/http/api-error.d.ts +27 -1
- package/dist/http/cors.d.ts +17 -0
- package/dist/http/deprecation.d.ts +42 -0
- package/dist/http/interaction-control-error.d.ts +13 -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/request-body.d.ts +27 -0
- package/dist/http/request-source.d.ts +52 -0
- package/dist/http/sse.d.ts +17 -6
- package/dist/http/unmatched-route.d.ts +18 -0
- package/dist/http/user-content.d.ts +36 -0
- package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.js +1091 -162
- package/dist/index.js.map +1 -1
- package/dist/integration-endpoint-test.d.ts +20 -0
- package/dist/integrations/api-integrations.d.ts +2 -2
- package/dist/integrations/atlassian.d.ts +29 -3
- package/dist/integrations/connect-authority.d.ts +1 -0
- package/dist/integrations/connect-callback-return.d.ts +6 -0
- package/dist/integrations/fiken.d.ts +16 -2
- package/dist/integrations/github-app-connect.d.ts +47 -0
- package/dist/integrations/github-installation-proof.d.ts +3 -0
- package/dist/integrations/github-lens-connect.d.ts +116 -0
- package/dist/integrations/github-skill-source.d.ts +1 -5
- package/dist/integrations/google-drive.d.ts +24 -0
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/marketplace-plugin.d.ts +2691 -0
- package/dist/integrations/oauth-client.d.ts +178 -18
- package/dist/integrations/oauth-profiles.d.ts +222 -0
- package/dist/integrations/oauth-return-path.d.ts +1 -0
- package/dist/integrations/personal-github-repositories.d.ts +84 -0
- package/dist/integrations/personal-github.d.ts +42 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +19 -1
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +367 -12
- package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
- package/dist/integrations/slack-install.d.ts +20 -0
- package/dist/integrations/slack-interactions.d.ts +107 -9
- package/dist/integrations/slack-mrkdwn.d.ts +41 -0
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/slack-session-defaults.d.ts +58 -0
- package/dist/integrations/slack-task-file-upload.d.ts +36 -0
- package/dist/integrations/social-oauth.d.ts +7 -0
- package/dist/interaction-agent-access.d.ts +15 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/interaction-metrics.d.ts +2 -0
- package/dist/mcp/action-catalog-types.d.ts +9 -0
- package/dist/mcp/action-catalog.gen.d.ts +2 -0
- package/dist/mcp/capability-account-readiness.d.ts +7 -0
- package/dist/mcp/child-read-evidence.d.ts +9 -0
- package/dist/mcp/command-read.d.ts +24 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/contract-input.d.ts +11 -0
- package/dist/mcp/documents.d.ts +7 -5
- package/dist/mcp/files.d.ts +2 -2
- package/dist/mcp/first-party-tool-permissions.d.ts +650 -0
- package/dist/mcp/knowledge-entries.d.ts +5 -0
- package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
- package/dist/mcp/receipts.d.ts +9 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-input.d.ts +1189 -0
- package/dist/mcp/scheduled-task-view.d.ts +22 -4
- package/dist/mcp/server.d.ts +255 -3
- package/dist/mcp/session-event-view.d.ts +94 -0
- package/dist/mcp/session-view.d.ts +66 -2
- package/dist/mcp/session-wait.d.ts +174 -0
- package/dist/mcp-oauth.d.ts +52 -0
- package/dist/model-catalog.d.ts +11 -31
- package/dist/observability.d.ts +3 -1
- package/dist/organization-mcp.d.ts +52 -0
- package/dist/organization-recovery-notifications.d.ts +41 -0
- package/dist/routes/analytics-consent.d.ts +26 -0
- package/dist/routes/api-integrations.d.ts +25 -0
- package/dist/routes/api-keys.d.ts +17 -0
- package/dist/routes/artifact-catalog.d.ts +6 -0
- package/dist/routes/automations.d.ts +19 -0
- package/dist/routes/billing.d.ts +49 -0
- package/dist/routes/browser-sessions.d.ts +59 -2
- 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 +146 -0
- package/dist/routes/codex.d.ts +76 -2
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/computer-sessions.d.ts +14 -0
- package/dist/routes/connect.d.ts +5 -0
- package/dist/routes/editable-artifacts.d.ts +2 -0
- package/dist/routes/external-identity-links.d.ts +6 -0
- package/dist/routes/feedback.d.ts +3 -0
- package/dist/routes/github-browser-pages.d.ts +12 -0
- package/dist/routes/github.d.ts +2 -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 +30 -1
- package/dist/routes/knowledge.d.ts +3 -0
- package/dist/routes/managed-auth-session-sets.d.ts +26 -0
- package/dist/routes/managed-onboarding.d.ts +29 -0
- package/dist/routes/managed-sign-in-methods.d.ts +6 -0
- package/dist/routes/model-connection-access.d.ts +3 -0
- package/dist/routes/organization-integration-policy.d.ts +3 -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-providers.d.ts +3 -0
- package/dist/routes/organization-recovery.d.ts +19 -0
- package/dist/routes/organization-service-accounts.d.ts +3 -0
- package/dist/routes/organization-sessions.d.ts +26 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/session-artifact-associations.d.ts +4 -0
- package/dist/routes/session-history-imports.d.ts +9 -0
- package/dist/routes/sessions.d.ts +44 -6
- package/dist/routes/skill-content.d.ts +4 -0
- package/dist/routes/skill-install-authority.d.ts +6 -0
- package/dist/routes/subscription-account-pools.d.ts +34 -0
- package/dist/routes/subscription-pool-access.d.ts +16 -0
- package/dist/routes/supergrok.d.ts +6 -1
- package/dist/routes/usage-allowances.d.ts +12 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-artifacts.d.ts +2 -1
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-integrations.d.ts +35 -0
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspace-model-providers.d.ts +4 -0
- package/dist/routes/workspaces.d.ts +29 -0
- package/dist/sandbox/auth-callout.d.ts +8 -0
- package/dist/sandbox/channel-a.d.ts +19 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +9 -1
- package/dist/sandbox/machines.d.ts +5 -2
- package/dist/sandbox/metrics-ingestion.d.ts +28 -20
- package/dist/sandbox/rematerialize.d.ts +2 -0
- package/dist/sandbox/session-attach-variable-sets.d.ts +30 -0
- package/dist/sandbox/viewer.d.ts +26 -1
- package/dist/sandbox-file-artifacts.d.ts +50 -0
- package/dist/scheduled-task-deletion.d.ts +13 -0
- package/dist/session-create-observability.d.ts +5 -0
- package/dist/session-event-view-MPS2OZD3.js +292 -0
- package/dist/session-event-view-MPS2OZD3.js.map +1 -0
- package/dist/site-session-origin.d.ts +13 -0
- package/dist/site-uploads.d.ts +41 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/dist/temporal-schedule-sync.d.ts +26 -0
- package/dist/transcription/providers/xai-subscription.d.ts +1 -0
- package/dist/transcription/service.d.ts +2 -1
- package/dist/work-discovery-observability.d.ts +33 -0
- package/dist/workspace-artifact-content.d.ts +28 -0
- package/dist/workspace-artifact-provenance.d.ts +7 -0
- package/dist/workspace-delete-observability.d.ts +10 -0
- package/dist/workspace-deletion.d.ts +6 -0
- package/dist/workspace-state-projection.d.ts +3 -5
- package/dist/workspace-tool-gateway-observability.d.ts +17 -0
- package/dist/workspace-tool-gateway.d.ts +123 -0
- package/dist/workspace-webhook-dispatch.d.ts +26 -0
- package/package.json +23 -19
- package/src/access-grant-rls.ts +75 -0
- package/src/admission-trace.ts +14 -0
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +1967 -252
- package/src/artifact-catalog-cursor.ts +60 -0
- package/src/auth/managed-auth-attempt-context.ts +70 -0
- package/src/auth/managed-auth-rate-limits.ts +252 -0
- package/src/auth/managed-auth-session-adapter.ts +222 -0
- package/src/auth/managed-auth.ts +536 -60
- package/src/auth/managed-email.ts +174 -0
- 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 +307 -0
- package/src/auth/signup-funnel-metrics.ts +164 -0
- package/src/browser-controller-authority.ts +1 -186
- package/src/browser-ephemeral.ts +39 -0
- package/src/browser-state-authority.ts +1 -236
- package/src/claude-subscription-account-conflicts.ts +12 -0
- package/src/claude-subscription-account-usage.ts +48 -0
- package/src/claude-subscription-oauth.ts +362 -0
- package/src/claude-subscription-profile.ts +73 -0
- package/src/claude-subscription-usage.ts +97 -0
- package/src/claude-workspace-connection.ts +82 -0
- package/src/codemode.ts +194 -30
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-ownership.ts +199 -0
- package/src/controller-data-plane.ts +81 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +28 -3
- package/src/editable-artifact-production.ts +23 -7
- package/src/editable-artifact-source-session.ts +123 -0
- package/src/editable-artifact-websocket.ts +27 -21
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/fatal-process-boundary.ts +246 -0
- package/src/gateway-realtime.ts +1 -1
- package/src/github-access.ts +147 -5
- package/src/http/acting-person.ts +71 -0
- package/src/http/api-contract-compat.ts +49 -0
- package/src/http/api-error.ts +115 -1
- package/src/http/auth.ts +48 -0
- package/src/http/cors.ts +67 -0
- package/src/http/deprecation.ts +118 -0
- package/src/http/interaction-control-error.ts +164 -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/request-body.ts +127 -0
- package/src/http/request-source.ts +178 -0
- package/src/http/sse.ts +486 -132
- package/src/http/unmatched-route.ts +60 -0
- package/src/http/user-content.ts +158 -0
- package/src/http/websocket-upgrade-dispatch.ts +29 -0
- package/src/index.ts +157 -52
- package/src/integration-endpoint-test.ts +291 -0
- package/src/integrations/api-integrations.ts +98 -11
- package/src/integrations/atlassian.ts +474 -136
- package/src/integrations/connect-authority.ts +2 -0
- package/src/integrations/connect-callback-return.ts +86 -0
- package/src/integrations/fiken.ts +354 -64
- package/src/integrations/github-app-connect.ts +435 -0
- package/src/integrations/github-installation-proof.ts +60 -0
- package/src/integrations/github-lens-connect.ts +76 -0
- package/src/integrations/github-skill-source.ts +2 -142
- package/src/integrations/google-drive.ts +717 -225
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/marketplace-plugin.ts +48 -0
- package/src/integrations/oauth-client.ts +1356 -459
- package/src/integrations/oauth-profiles.ts +537 -0
- package/src/integrations/oauth-return-path.ts +18 -0
- package/src/integrations/personal-github-repositories.ts +674 -0
- package/src/integrations/personal-github.ts +825 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +250 -22
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +963 -239
- package/src/integrations/slack-file-upload-flow.ts +147 -0
- package/src/integrations/slack-install.ts +83 -0
- package/src/integrations/slack-interactions.ts +3167 -474
- package/src/integrations/slack-mrkdwn.ts +193 -0
- package/src/integrations/slack-routing.ts +337 -0
- package/src/integrations/slack-session-defaults.ts +224 -0
- package/src/integrations/slack-task-file-upload.ts +323 -0
- package/src/integrations/social-oauth.ts +213 -15
- package/src/interaction-agent-access.ts +48 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/interaction-metrics.ts +30 -0
- package/src/mcp/action-catalog-types.ts +9 -0
- package/src/mcp/action-catalog.gen.ts +6696 -0
- package/src/mcp/capability-account-readiness.ts +17 -0
- package/src/mcp/child-read-evidence.ts +19 -0
- package/src/mcp/command-read.ts +109 -0
- package/src/mcp/company-profile-agent-admin.ts +198 -0
- package/src/mcp/contract-input.ts +141 -0
- package/src/mcp/documents.ts +65 -377
- package/src/mcp/editable-artifacts.ts +12 -6
- package/src/mcp/files.ts +80 -43
- package/src/mcp/first-party-tool-permissions.ts +305 -0
- package/src/mcp/knowledge-entries.ts +322 -0
- package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
- package/src/mcp/receipts.ts +34 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-input.ts +76 -0
- package/src/mcp/scheduled-task-view.ts +44 -3
- package/src/mcp/server.ts +3030 -1267
- package/src/mcp/session-event-view.ts +383 -0
- package/src/mcp/session-view.ts +84 -3
- package/src/mcp/session-wait.ts +665 -0
- package/src/mcp-oauth.ts +1123 -0
- package/src/model-catalog.ts +91 -333
- package/src/observability.ts +15 -1
- package/src/organization-mcp.ts +397 -0
- package/src/organization-recovery-notifications.ts +103 -0
- package/src/routes/analytics-consent.ts +123 -0
- package/src/routes/api-integrations.ts +364 -74
- package/src/routes/api-keys.ts +336 -10
- package/src/routes/artifact-catalog.ts +279 -0
- package/src/routes/automations.ts +768 -0
- package/src/routes/billing.ts +461 -46
- package/src/routes/browser-sessions.ts +780 -112
- package/src/routes/canonical-human-identities.ts +90 -14
- package/src/routes/capabilities.ts +107 -6
- package/src/routes/channels.ts +20 -3
- package/src/routes/claude-subscription-accounts.ts +334 -0
- package/src/routes/claude-subscription-oauth.ts +142 -0
- package/src/routes/client-errors.ts +362 -0
- package/src/routes/codex.ts +833 -116
- package/src/routes/company-brain.ts +322 -0
- package/src/routes/company-profile.ts +65 -1
- package/src/routes/computer-sessions.ts +557 -106
- package/src/routes/connect.ts +1938 -0
- package/src/routes/connections.ts +1039 -349
- package/src/routes/documents.ts +488 -287
- package/src/routes/editable-artifacts.ts +79 -9
- package/src/routes/enrollments.ts +135 -39
- package/src/routes/environments.ts +154 -58
- package/src/routes/external-identity-links.ts +201 -0
- package/src/routes/feedback.ts +113 -0
- package/src/routes/files.ts +267 -33
- package/src/routes/github-browser-pages.ts +162 -0
- package/src/routes/github.ts +446 -144
- package/src/routes/insights-response-cache.ts +83 -0
- package/src/routes/insights-usage.ts +345 -0
- package/src/routes/insights.ts +153 -19
- package/src/routes/install.ts +133 -38
- package/src/routes/integration-facets.ts +107 -33
- package/src/routes/interaction-resources.ts +30 -3
- package/src/routes/knowledge.ts +488 -0
- package/src/routes/machines.ts +313 -15
- package/src/routes/managed-auth-session-sets.ts +1218 -0
- package/src/routes/managed-onboarding.ts +337 -0
- package/src/routes/managed-sign-in-methods.ts +433 -0
- package/src/routes/memory-slack-publications.ts +3 -2
- package/src/routes/model-connection-access.ts +208 -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 +178 -0
- package/src/routes/organization-memberships.ts +1019 -27
- package/src/routes/organization-model-providers.ts +293 -0
- package/src/routes/organization-recovery.ts +325 -0
- package/src/routes/organization-service-accounts.ts +128 -0
- package/src/routes/organization-sessions.ts +350 -0
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +370 -0
- package/src/routes/plugins.ts +251 -22
- package/src/routes/pr-review-github.ts +881 -0
- package/src/routes/pr-review.ts +622 -0
- package/src/routes/preference-registry.ts +12 -93
- package/src/routes/rigs.ts +167 -50
- package/src/routes/scheduled-tasks.ts +346 -41
- package/src/routes/session-artifact-associations.ts +87 -0
- package/src/routes/session-history-imports.ts +280 -0
- package/src/routes/sessions.ts +2306 -264
- package/src/routes/skill-content.ts +297 -0
- package/src/routes/skill-install-authority.ts +64 -0
- package/src/routes/skills.ts +62 -9
- package/src/routes/social.ts +46 -18
- package/src/routes/subscription-account-pools.ts +436 -0
- package/src/routes/subscription-pool-access.ts +126 -0
- package/src/routes/supergrok.ts +219 -303
- package/src/routes/transcription-recordings.ts +38 -3
- package/src/routes/transcriptions.ts +4 -0
- package/src/routes/usage-allowances.ts +378 -0
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/video-generation.ts +14 -10
- package/src/routes/workspace-artifacts.ts +275 -69
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-integrations.ts +576 -0
- package/src/routes/workspace-learning.ts +159 -0
- package/src/routes/workspace-model-providers.ts +148 -0
- package/src/routes/workspace-state.ts +38 -25
- package/src/routes/workspaces.ts +1004 -127
- package/src/sandbox/auth-callout.ts +96 -19
- package/src/sandbox/channel-a.ts +323 -24
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +81 -6
- package/src/sandbox/machines.ts +218 -76
- package/src/sandbox/metrics-ingestion.ts +411 -62
- package/src/sandbox/rematerialize.ts +121 -19
- package/src/sandbox/session-attach-variable-sets.ts +131 -0
- package/src/sandbox/viewer.ts +280 -25
- package/src/sandbox-file-artifacts.ts +413 -0
- package/src/scheduled-task-deletion.ts +138 -0
- package/src/session-create-observability.ts +46 -0
- package/src/session-realtime-context.ts +6 -3
- package/src/site-session-origin.ts +48 -0
- package/src/site-uploads.ts +224 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- package/src/temporal-schedule-sync.ts +74 -0
- package/src/transcription/providers/codex-subscription.ts +2 -0
- package/src/transcription/providers/openai.ts +1 -0
- package/src/transcription/providers/xai-subscription.ts +49 -3
- package/src/transcription/segmenter.ts +8 -2
- package/src/transcription/service.ts +43 -6
- package/src/work-discovery-observability.ts +121 -0
- package/src/workspace-artifact-content.ts +144 -0
- package/src/workspace-artifact-provenance.ts +104 -0
- package/src/workspace-delete-observability.ts +75 -0
- package/src/workspace-deletion.ts +73 -0
- package/src/workspace-state-projection.ts +18 -148
- package/src/workspace-tool-gateway-observability.ts +100 -0
- package/src/workspace-tool-gateway.ts +870 -0
- package/src/workspace-webhook-dispatch.ts +190 -0
- package/src/xai-subscription-auth.ts +9 -2
- package/dist/chunk-WCXHI3FF.js +0 -49920
- package/dist/chunk-WCXHI3FF.js.map +0 -1
- package/dist/routes/packs.d.ts +0 -3
- package/src/routes/packs.ts +0 -729
|
@@ -1,4 +1,36 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, createHmac } from "node:crypto";
|
|
2
|
+
import { verifyDirectModelAccess } from "@opengeni/core";
|
|
3
|
+
import {
|
|
4
|
+
assertClaudeWorkspaceCredential,
|
|
5
|
+
prepareClaudeWorkspaceCredential,
|
|
6
|
+
} from "../claude-workspace-connection";
|
|
7
|
+
import {
|
|
8
|
+
createConnectionIdempotently,
|
|
9
|
+
upsertWorkspaceProviderApiKeyConnection,
|
|
10
|
+
rotateWorkspaceProviderApiKeyConnection,
|
|
11
|
+
revokeWorkspaceProviderApiKeyConnections,
|
|
12
|
+
workspaceProviderApiKeyConnectionSpec,
|
|
13
|
+
type WorkspaceProviderApiKeyConnectionKind,
|
|
14
|
+
getConnectionCreationResult,
|
|
15
|
+
ConnectionCreateIdempotencyError,
|
|
16
|
+
} from "@opengeni/db";
|
|
17
|
+
import { sql } from "drizzle-orm";
|
|
18
|
+
import { assertOrganizationIntegrationAllowed } from "@opengeni/contracts";
|
|
19
|
+
import {
|
|
20
|
+
withOrganizationIntegrationAcquisition,
|
|
21
|
+
withOrganizationIntegrationPolicyFence,
|
|
22
|
+
} from "@opengeni/db/organization-integration-policy";
|
|
23
|
+
import { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
|
|
24
|
+
import {
|
|
25
|
+
startSlackBotInstall,
|
|
26
|
+
requireOpenGeniSlackOAuthSettings,
|
|
27
|
+
} from "../integrations/slack-install";
|
|
28
|
+
import {
|
|
29
|
+
WORKSPACE_OPENROUTER_CONNECTION_DOMAIN,
|
|
30
|
+
WORKSPACE_OPENROUTER_CONNECTION_ROLE,
|
|
31
|
+
VERCEL_AI_GATEWAY_CONNECTION_DOMAIN,
|
|
32
|
+
VERCEL_AI_GATEWAY_CONNECTION_ROLE,
|
|
33
|
+
} from "@opengeni/config";
|
|
2
34
|
import {
|
|
3
35
|
ATLASSIAN_PROVIDER_DOMAIN,
|
|
4
36
|
AtlassianConnectionMetadata,
|
|
@@ -10,22 +42,24 @@ import {
|
|
|
10
42
|
import {
|
|
11
43
|
API_INTEGRATION_OAUTH_CREDENTIAL_ROLE,
|
|
12
44
|
ApiIntegrationOAuthStartRequest,
|
|
45
|
+
DirectModelProviderMetadata,
|
|
13
46
|
ConnectionResponse,
|
|
14
47
|
CreateConnectionRequest,
|
|
15
|
-
FIKEN_CREDENTIAL_LABEL,
|
|
16
|
-
FIKEN_CREDENTIAL_ROLE,
|
|
17
|
-
FIKEN_PROVIDER_DOMAIN,
|
|
18
48
|
FikenInstallRequest,
|
|
19
49
|
FikenOAuthStartRequest,
|
|
20
50
|
FikenOAuthStartResponse,
|
|
21
51
|
IntegrationClientMetadata,
|
|
22
52
|
ListSlackInstallationBindingsResponse,
|
|
23
53
|
ListConnectionsResponse,
|
|
54
|
+
OPENROUTER_CREDENTIAL_OPERATION_DIGEST_METADATA_KEY,
|
|
55
|
+
OPENROUTER_CREDENTIAL_OPERATION_ID_METADATA_KEY,
|
|
24
56
|
OpenGeniSlackBotInstallRequest,
|
|
25
|
-
OpenGeniSlackBotInstallStart,
|
|
26
57
|
OAuthStartRequest,
|
|
27
58
|
OAuthStartResponse,
|
|
28
59
|
UpdateConnectionRequest,
|
|
60
|
+
VERCEL_AI_GATEWAY_CREDENTIAL_OPERATION_DIGEST_METADATA_KEY,
|
|
61
|
+
VERCEL_AI_GATEWAY_CREDENTIAL_OPERATION_ID_METADATA_KEY,
|
|
62
|
+
stableJson,
|
|
29
63
|
} from "@opengeni/contracts";
|
|
30
64
|
import {
|
|
31
65
|
bindConnectorDocumentDestination,
|
|
@@ -40,24 +74,34 @@ import {
|
|
|
40
74
|
GoogleDriveOAuthStartResponse,
|
|
41
75
|
} from "@opengeni/contracts/google-drive";
|
|
42
76
|
import {
|
|
43
|
-
|
|
77
|
+
PERSONAL_GITHUB_PROVIDER_DOMAIN,
|
|
78
|
+
PersonalGitHubDisconnectRequest,
|
|
79
|
+
hasReservedPersonalGitHubMetadata,
|
|
80
|
+
isPersonalGitHubConnection,
|
|
81
|
+
} from "@opengeni/contracts/personal-github";
|
|
82
|
+
import {
|
|
44
83
|
hasPermission,
|
|
84
|
+
listOwnConnectionAccountsForGrant,
|
|
45
85
|
hasReservedFikenMetadata,
|
|
46
86
|
hasReservedOpenGeniSlackBotMetadata,
|
|
47
|
-
isFikenConnection,
|
|
48
87
|
isOpenGeniSlackBotConnection,
|
|
49
88
|
openGeniSlackBotMetadata,
|
|
50
89
|
requireAccessGrant,
|
|
51
90
|
requireAccessGrantAuthorization,
|
|
52
91
|
requireEnvironmentEncryption,
|
|
53
|
-
|
|
92
|
+
externalContinuationCommitAuthorizer,
|
|
54
93
|
} from "@opengeni/core";
|
|
55
94
|
import {
|
|
56
95
|
consumeIntegrationOAuthStateNonce,
|
|
57
|
-
|
|
96
|
+
claimConnectOperation,
|
|
97
|
+
finishConnectOperation,
|
|
98
|
+
getConnectAttempt,
|
|
99
|
+
decryptEnvironmentValue,
|
|
100
|
+
withWorkspaceSubjectRls,
|
|
101
|
+
type Database,
|
|
58
102
|
encryptEnvironmentValue,
|
|
103
|
+
brokeredCredentialBundleProblem,
|
|
59
104
|
getConnectionMetadata,
|
|
60
|
-
getWorkspaceGrant,
|
|
61
105
|
listConnectionsMetadata,
|
|
62
106
|
listSlackInstallationBindings,
|
|
63
107
|
persistSlackBotInstallationWithSuccessAudit,
|
|
@@ -84,8 +128,10 @@ import {
|
|
|
84
128
|
} from "../integrations/google-drive";
|
|
85
129
|
import {
|
|
86
130
|
completeApiIntegrationProviderOAuth,
|
|
131
|
+
isApiIntegrationProviderOAuthState,
|
|
87
132
|
startApiIntegrationProviderOAuth,
|
|
88
133
|
} from "../integrations/provider-oauth";
|
|
134
|
+
import { disconnectPersonalGitHub } from "../integrations/personal-github";
|
|
89
135
|
import {
|
|
90
136
|
browseAtlassianSources,
|
|
91
137
|
completeAtlassianOAuthCallback,
|
|
@@ -97,9 +143,17 @@ import {
|
|
|
97
143
|
import {
|
|
98
144
|
completeMcpOAuthCallback,
|
|
99
145
|
integrationBaseUrl,
|
|
146
|
+
oauthStateFailureReturn,
|
|
100
147
|
startMcpOAuth,
|
|
101
148
|
} from "../integrations/oauth-client";
|
|
149
|
+
import {
|
|
150
|
+
assertPersonalConnectionOwnerPrincipal,
|
|
151
|
+
isPersonalConnectionOwnerPrincipal,
|
|
152
|
+
requireLegacyOAuthActor,
|
|
153
|
+
requireProviderConsentInBrowser,
|
|
154
|
+
} from "../connection-ownership";
|
|
102
155
|
import { canonicalProviderDomain } from "../integrations/provider-domain";
|
|
156
|
+
import { externalActorContinuationForAuthorization } from "@opengeni/core";
|
|
103
157
|
import {
|
|
104
158
|
exchangeOpenGeniSlackAuthorizationCode,
|
|
105
159
|
SlackBotCredentialVerificationError,
|
|
@@ -107,19 +161,21 @@ import {
|
|
|
107
161
|
} from "../integrations/slack-bot";
|
|
108
162
|
import {
|
|
109
163
|
completeFikenOAuthCallback,
|
|
110
|
-
fikenCredentialBundle,
|
|
111
164
|
startFikenOAuth,
|
|
112
|
-
|
|
165
|
+
prepareFikenTokenInstall,
|
|
113
166
|
} from "../integrations/fiken";
|
|
167
|
+
import { requireConnectOwnerAuthority } from "../integrations/connect-authority";
|
|
114
168
|
import {
|
|
115
169
|
OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
|
|
116
170
|
OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
|
|
117
|
-
OPENGENI_SLACK_BOT_REQUESTED_SCOPES,
|
|
118
171
|
} from "@opengeni/contracts";
|
|
119
|
-
import {
|
|
172
|
+
import { readSignedState } from "@opengeni/github";
|
|
120
173
|
import { oauthStateTtlMs, requireIntegrationsStateSecret } from "../integrations/oauth-client";
|
|
174
|
+
import { parseRequestJson } from "../http/request-body";
|
|
121
175
|
|
|
122
176
|
type OpenGeniSlackInstallState = {
|
|
177
|
+
connectAttemptId?: string;
|
|
178
|
+
externalContinuation?: ExternalActorContinuation;
|
|
123
179
|
accountId: string;
|
|
124
180
|
workspaceId: string;
|
|
125
181
|
subjectId: string;
|
|
@@ -149,6 +205,26 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
149
205
|
);
|
|
150
206
|
});
|
|
151
207
|
|
|
208
|
+
app.get("/v1/workspaces/:workspaceId/connections/accounts", async (c) => {
|
|
209
|
+
const workspaceId = c.req.param("workspaceId");
|
|
210
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
|
|
211
|
+
const includeInactive = c.req.query("includeInactive");
|
|
212
|
+
if (
|
|
213
|
+
includeInactive !== undefined &&
|
|
214
|
+
includeInactive !== "true" &&
|
|
215
|
+
includeInactive !== "false"
|
|
216
|
+
) {
|
|
217
|
+
throw new HTTPException(400, { message: "includeInactive must be true or false" });
|
|
218
|
+
}
|
|
219
|
+
return c.json(
|
|
220
|
+
ListConnectionsResponse.parse({
|
|
221
|
+
connections: await listOwnConnectionAccountsForGrant(db, grant, {
|
|
222
|
+
includeInactive: includeInactive === "true",
|
|
223
|
+
}),
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
152
228
|
app.get("/v1/workspaces/:workspaceId/connections/slack-bot/bindings", async (c) => {
|
|
153
229
|
const workspaceId = c.req.param("workspaceId");
|
|
154
230
|
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
|
|
@@ -162,79 +238,281 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
162
238
|
);
|
|
163
239
|
});
|
|
164
240
|
|
|
165
|
-
app.
|
|
241
|
+
app.get("/v1/workspaces/:workspaceId/connections/operations/:operationId", async (c) => {
|
|
166
242
|
const workspaceId = c.req.param("workspaceId");
|
|
167
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const key = requireEnvironmentEncryption(settings);
|
|
173
|
-
const subjectId = createConnectionSubjectId(payload, grant.subjectId);
|
|
174
|
-
const providerDomain = canonicalProviderDomain(payload.providerDomain);
|
|
175
|
-
assertNotDirectPersonalSlackOAuth(providerDomain, payload.kind);
|
|
176
|
-
assertNotDirectGoogleDriveOAuth(providerDomain, payload.kind, payload.metadata);
|
|
177
|
-
assertNotDirectAtlassianOAuth(providerDomain, payload.kind, payload.metadata);
|
|
178
|
-
const connection = await createConnection(db, {
|
|
243
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
|
|
244
|
+
const operationId = CreateConnectionRequest.shape.operationId
|
|
245
|
+
.unwrap()
|
|
246
|
+
.parse(c.req.param("operationId"));
|
|
247
|
+
const connection = await getConnectionCreationResult(db, {
|
|
179
248
|
accountId: grant.accountId,
|
|
180
249
|
workspaceId,
|
|
181
|
-
subjectId,
|
|
182
|
-
|
|
183
|
-
kind: payload.kind,
|
|
184
|
-
credentialEncrypted: encryptCredentialBundle(key, payload.credential),
|
|
185
|
-
grantedScopes: payload.grantedScopes,
|
|
186
|
-
expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null,
|
|
187
|
-
metadata: payload.metadata,
|
|
188
|
-
createdBySubjectId: grant.subjectId,
|
|
250
|
+
subjectId: grant.subjectId,
|
|
251
|
+
operationId,
|
|
189
252
|
});
|
|
190
|
-
|
|
253
|
+
if (!connection) throw new HTTPException(404, { message: "Connection operation not found" });
|
|
254
|
+
return c.json(ConnectionResponse.parse({ connection }));
|
|
191
255
|
});
|
|
192
256
|
|
|
193
|
-
app.post("/v1/workspaces/:workspaceId/connections
|
|
257
|
+
app.post("/v1/workspaces/:workspaceId/connections", async (c) => {
|
|
194
258
|
const workspaceId = c.req.param("workspaceId");
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
259
|
+
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
|
|
260
|
+
const grant = access.grant;
|
|
261
|
+
const beforeCommit = externalContinuationCommitAuthorizer(access);
|
|
262
|
+
const payload = await parseRequestJson(c, CreateConnectionRequest);
|
|
263
|
+
if (
|
|
264
|
+
payload.metadata?.directModelProvider !== undefined ||
|
|
265
|
+
String(payload.metadata?.credentialRole ?? "").startsWith("direct_")
|
|
266
|
+
) {
|
|
267
|
+
const config = DirectModelProviderMetadata.safeParse(payload.metadata?.directModelProvider);
|
|
268
|
+
const domain = config.success
|
|
269
|
+
? new URL(config.data.endpoint ?? "https://api.openai.com/v1").hostname
|
|
270
|
+
: null;
|
|
271
|
+
if (
|
|
272
|
+
!config.success ||
|
|
273
|
+
payload.subjectId !== null ||
|
|
274
|
+
payload.kind !== "api_key" ||
|
|
275
|
+
canonicalProviderDomain(payload.providerDomain) !== domain ||
|
|
276
|
+
payload.metadata?.credentialRole !== `direct_${config.data.provider}` ||
|
|
277
|
+
typeof payload.credential.apiKey !== "string" ||
|
|
278
|
+
!payload.credential.apiKey.trim() ||
|
|
279
|
+
!payload.operationId ||
|
|
280
|
+
payload.expiresAt
|
|
281
|
+
) {
|
|
282
|
+
throw new HTTPException(422, {
|
|
283
|
+
message:
|
|
284
|
+
"Invalid OpenAI or Azure OpenAI connection: provide a key, model, and official provider endpoint",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
payload.metadata = { ...payload.metadata, directModelProvider: config.data };
|
|
288
|
+
if (payload.verifyModelAccess) {
|
|
289
|
+
await verifyDirectModelAccess(
|
|
290
|
+
config.data,
|
|
291
|
+
payload.credential.apiKey as string,
|
|
292
|
+
deps.directModelFetch,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
} else if (payload.verifyModelAccess) {
|
|
205
296
|
throw new HTTPException(422, {
|
|
206
|
-
message: "
|
|
297
|
+
message: "Model checks require an OpenAI or Azure OpenAI connection",
|
|
207
298
|
});
|
|
208
299
|
}
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
300
|
+
const directModelKey = payload.metadata?.directModelProvider !== undefined;
|
|
301
|
+
// All writes in this closure must use the caller-owned scoped transaction.
|
|
302
|
+
// eslint-disable-next-line no-shadow
|
|
303
|
+
const persist = async (db: Database) => {
|
|
304
|
+
assertNotReservedSlackBotMetadata(payload.metadata);
|
|
305
|
+
assertNotReservedFikenMetadata(payload.metadata);
|
|
306
|
+
assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
|
|
307
|
+
assertNotReservedPersonalGitHubMetadata(payload.metadata);
|
|
308
|
+
const key = requireEnvironmentEncryption(settings);
|
|
309
|
+
const subjectId = createConnectionSubjectId(payload, grant.subjectId);
|
|
310
|
+
if (subjectId !== null) {
|
|
311
|
+
assertPersonalConnectionOwnerPrincipal(access);
|
|
312
|
+
}
|
|
313
|
+
const providerDomain = canonicalProviderDomain(payload.providerDomain);
|
|
314
|
+
payload.credential = prepareClaudeWorkspaceCredential(
|
|
315
|
+
settings,
|
|
316
|
+
workspaceId,
|
|
317
|
+
payload.metadata,
|
|
318
|
+
payload.credential,
|
|
319
|
+
);
|
|
320
|
+
assertClaudeWorkspaceCredential(settings, {
|
|
321
|
+
subjectId,
|
|
322
|
+
providerDomain,
|
|
323
|
+
kind: payload.kind,
|
|
324
|
+
metadata: payload.metadata,
|
|
325
|
+
credential: payload.credential,
|
|
326
|
+
});
|
|
327
|
+
assertNotDirectPersonalSlackOAuth(providerDomain, payload.kind);
|
|
328
|
+
assertNotDirectGoogleDriveOAuth(providerDomain, payload.kind, payload.metadata);
|
|
329
|
+
assertNotDirectAtlassianOAuth(providerDomain, payload.kind, payload.metadata);
|
|
330
|
+
assertNotDirectPersonalGitHubOAuth(providerDomain, payload.kind, payload.metadata);
|
|
331
|
+
const credentialEncrypted = encryptCredentialBundle(key, payload.credential);
|
|
332
|
+
const workspaceProviderKind = workspaceProviderApiKeyConnectionKind({
|
|
333
|
+
subjectId,
|
|
334
|
+
providerDomain,
|
|
335
|
+
kind: payload.kind,
|
|
336
|
+
metadata: payload.metadata,
|
|
337
|
+
});
|
|
338
|
+
// Validated model keys store `{ apiKey }` for model execution. Generic
|
|
339
|
+
// integration credentials still require an explicit broker destination.
|
|
340
|
+
if (!workspaceProviderKind && !directModelKey)
|
|
341
|
+
assertBrokeredApiKeyCredential(payload.kind, payload.credential);
|
|
342
|
+
const connection = workspaceProviderKind
|
|
343
|
+
? await (async () => {
|
|
344
|
+
const provider = workspaceProviderApiKeyConnectionSpec(workspaceProviderKind);
|
|
345
|
+
if (!payload.operationId) {
|
|
346
|
+
throw new HTTPException(400, {
|
|
347
|
+
message: `connecting ${provider.label} requires an operationId`,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
const expiresAt = payload.expiresAt ? new Date(payload.expiresAt) : null;
|
|
351
|
+
const metadata = workspaceProviderCredentialMetadata(
|
|
352
|
+
workspaceProviderKind,
|
|
353
|
+
payload.metadata,
|
|
354
|
+
);
|
|
355
|
+
const input = {
|
|
356
|
+
accountId: grant.accountId,
|
|
357
|
+
workspaceId,
|
|
358
|
+
operationId: payload.operationId,
|
|
359
|
+
requestDigest: workspaceProviderCredentialRequestDigest(key, {
|
|
360
|
+
action: "create",
|
|
361
|
+
providerDomain,
|
|
362
|
+
kind: payload.kind,
|
|
363
|
+
subjectId,
|
|
364
|
+
credential: payload.credential,
|
|
365
|
+
grantedScopes: payload.grantedScopes,
|
|
366
|
+
expiresAt: expiresAt?.toISOString() ?? null,
|
|
367
|
+
metadata,
|
|
368
|
+
}),
|
|
369
|
+
credentialEncrypted,
|
|
370
|
+
grantedScopes: payload.grantedScopes,
|
|
371
|
+
expiresAt,
|
|
372
|
+
metadata,
|
|
373
|
+
updatedBySubjectId: grant.subjectId,
|
|
374
|
+
};
|
|
375
|
+
const created = await upsertWorkspaceProviderApiKeyConnection(
|
|
376
|
+
db,
|
|
377
|
+
workspaceProviderKind,
|
|
378
|
+
input,
|
|
379
|
+
);
|
|
380
|
+
if (!created || created.status === "revoked") {
|
|
381
|
+
throw new HTTPException(409, {
|
|
382
|
+
message: `${provider.label} is already connected; reload before replacing its key`,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return created;
|
|
386
|
+
})()
|
|
387
|
+
: await createConnectionIdempotently(db, {
|
|
388
|
+
accountId: grant.accountId,
|
|
389
|
+
workspaceId,
|
|
390
|
+
subjectId,
|
|
391
|
+
providerDomain,
|
|
392
|
+
kind: payload.kind,
|
|
393
|
+
credentialEncrypted,
|
|
394
|
+
grantedScopes: payload.grantedScopes,
|
|
395
|
+
expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null,
|
|
396
|
+
metadata: payload.metadata,
|
|
397
|
+
createdBySubjectId: grant.subjectId,
|
|
398
|
+
...(payload.operationId
|
|
399
|
+
? {
|
|
400
|
+
operation: {
|
|
401
|
+
id: payload.operationId,
|
|
402
|
+
requestDigest: workspaceProviderCredentialRequestDigest(key, {
|
|
403
|
+
action: "create",
|
|
404
|
+
providerDomain,
|
|
405
|
+
kind: payload.kind,
|
|
406
|
+
subjectId,
|
|
407
|
+
credential: payload.credential,
|
|
408
|
+
grantedScopes: payload.grantedScopes,
|
|
409
|
+
expiresAt: payload.expiresAt
|
|
410
|
+
? new Date(payload.expiresAt).toISOString()
|
|
411
|
+
: null,
|
|
412
|
+
metadata: payload.metadata,
|
|
413
|
+
}),
|
|
414
|
+
},
|
|
415
|
+
}
|
|
416
|
+
: {}),
|
|
417
|
+
}).catch((error: unknown) => {
|
|
418
|
+
if (error instanceof ConnectionCreateIdempotencyError) {
|
|
419
|
+
throw new HTTPException(409, { message: error.message });
|
|
420
|
+
}
|
|
421
|
+
throw error;
|
|
422
|
+
});
|
|
423
|
+
return c.json(ConnectionResponse.parse({ connection }), 201);
|
|
424
|
+
};
|
|
425
|
+
const commit = (database: Database) =>
|
|
426
|
+
beforeCommit
|
|
427
|
+
? withWorkspaceSubjectRls(database, workspaceId, grant.subjectId, async (tx) => {
|
|
428
|
+
await beforeCommit(tx);
|
|
429
|
+
return persist(tx);
|
|
430
|
+
})
|
|
431
|
+
: persist(database);
|
|
432
|
+
// Only the canonical workspace model-key lane is outside integration policy.
|
|
433
|
+
// Generic credentials have no validated protocol/curated target in this contract.
|
|
434
|
+
const modelKey = workspaceProviderApiKeyConnectionKind({
|
|
435
|
+
subjectId: createConnectionSubjectId(payload, grant.subjectId),
|
|
436
|
+
providerDomain: canonicalProviderDomain(payload.providerDomain),
|
|
437
|
+
kind: payload.kind,
|
|
438
|
+
metadata: payload.metadata,
|
|
218
439
|
});
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
440
|
+
return modelKey || directModelKey
|
|
441
|
+
? commit(db)
|
|
442
|
+
: withOrganizationIntegrationAcquisition(
|
|
443
|
+
db,
|
|
444
|
+
{ accountId: grant.accountId, workspaceId },
|
|
445
|
+
[null],
|
|
446
|
+
commit,
|
|
447
|
+
);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
app.post("/v1/workspaces/:workspaceId/connections/slack-bot/install", async (c) => {
|
|
451
|
+
const workspaceId = c.req.param("workspaceId");
|
|
452
|
+
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
|
|
453
|
+
requireLegacyOAuthActor(access);
|
|
454
|
+
const grant = access.grant;
|
|
455
|
+
const payload = await parseRequestJson(c, OpenGeniSlackBotInstallRequest);
|
|
224
456
|
return c.json(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
457
|
+
await withOrganizationIntegrationAcquisition(
|
|
458
|
+
db,
|
|
459
|
+
{ accountId: grant.accountId, workspaceId },
|
|
460
|
+
["slack-bot"],
|
|
461
|
+
(tx) =>
|
|
462
|
+
startSlackBotInstall(
|
|
463
|
+
{ ...deps, db: tx },
|
|
464
|
+
{
|
|
465
|
+
accountId: grant.accountId,
|
|
466
|
+
workspaceId,
|
|
467
|
+
subjectId: grant.subjectId,
|
|
468
|
+
requestUrl: c.req.url,
|
|
469
|
+
...(payload.connectionId ? { connectionId: payload.connectionId } : {}),
|
|
470
|
+
},
|
|
471
|
+
),
|
|
472
|
+
),
|
|
229
473
|
);
|
|
230
474
|
});
|
|
231
475
|
|
|
232
476
|
app.get("/v1/integrations/slack/callback", async (c) => {
|
|
233
477
|
const baseUrl = integrationBaseUrl(settings.publicBaseUrl, c.req.url);
|
|
234
478
|
let state: OpenGeniSlackInstallState | null = null;
|
|
479
|
+
let exactReturnUrl: string | undefined;
|
|
480
|
+
let operation: { attemptId: string; operationId: string; inputDigest: string } | undefined;
|
|
235
481
|
let stage: SlackBotInstallCallbackFailureStage = "permission_check";
|
|
236
482
|
try {
|
|
237
483
|
state = readOpenGeniSlackInstallState(c.req.query("state"), settings);
|
|
484
|
+
if (state.connectAttemptId) {
|
|
485
|
+
const stored = await getConnectAttempt(db, state, state.connectAttemptId);
|
|
486
|
+
if (stored.attempt.providerId !== "slack-bot" || stored.attempt.ownership !== "workspace")
|
|
487
|
+
throw new HTTPException(403, { message: "Slack attempt mismatch" });
|
|
488
|
+
exactReturnUrl = stored.returnUrl;
|
|
489
|
+
operation = {
|
|
490
|
+
attemptId: state.connectAttemptId,
|
|
491
|
+
operationId: `oauth:${state.nonce}`,
|
|
492
|
+
inputDigest: createHash("sha256").update(c.req.query("state")!).digest("hex"),
|
|
493
|
+
};
|
|
494
|
+
const callbackState = state;
|
|
495
|
+
const callbackOperation = operation;
|
|
496
|
+
const claim = await withOrganizationIntegrationPolicyFence(
|
|
497
|
+
db,
|
|
498
|
+
callbackState,
|
|
499
|
+
(policyTx, policy) =>
|
|
500
|
+
claimConnectOperation(policyTx, callbackState, {
|
|
501
|
+
...callbackOperation,
|
|
502
|
+
expectedRevision: stored.attempt.revision,
|
|
503
|
+
authorize: (tx, _attempt, origin) =>
|
|
504
|
+
requireConnectOwnerAuthority(tx, callbackState, "connections:write", origin),
|
|
505
|
+
authorizeAcquisition: async () => {
|
|
506
|
+
// Provider denial/cancellation acquires nothing; completed exact
|
|
507
|
+
// receipts replay before this callback is invoked.
|
|
508
|
+
if (!c.req.query("error") && c.req.query("code"))
|
|
509
|
+
assertOrganizationIntegrationAllowed(policy, "slack-bot");
|
|
510
|
+
},
|
|
511
|
+
}),
|
|
512
|
+
);
|
|
513
|
+
if (claim.status === "replayed")
|
|
514
|
+
return new Response(null, { status: 302, headers: { Location: exactReturnUrl } });
|
|
515
|
+
}
|
|
238
516
|
await requireSlackInstallCallbackGrant(db, state);
|
|
239
517
|
stage = "nonce_consume";
|
|
240
518
|
const consumed = await consumeIntegrationOAuthStateNonce(db, {
|
|
@@ -252,6 +530,25 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
252
530
|
"Slack installation state has already been used",
|
|
253
531
|
);
|
|
254
532
|
}
|
|
533
|
+
if (operation && (c.req.query("error") || !c.req.query("code"))) {
|
|
534
|
+
await finishConnectOperation(db, state, {
|
|
535
|
+
...operation,
|
|
536
|
+
authorize: (tx, _attempt, origin) =>
|
|
537
|
+
requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
|
|
538
|
+
commit: async (_tx, current) => ({
|
|
539
|
+
...current,
|
|
540
|
+
revision: current.revision + 1,
|
|
541
|
+
state: c.req.query("error") === "access_denied" ? "cancelled" : "failed",
|
|
542
|
+
nextAction: { type: "none" },
|
|
543
|
+
error: {
|
|
544
|
+
code: c.req.query("error") ? "provider_denied" : "missing_code",
|
|
545
|
+
message: "Authorization was not completed. Start a new connection attempt.",
|
|
546
|
+
retryable: false,
|
|
547
|
+
},
|
|
548
|
+
}),
|
|
549
|
+
});
|
|
550
|
+
return new Response(null, { status: 302, headers: { Location: exactReturnUrl! } });
|
|
551
|
+
}
|
|
255
552
|
if (c.req.query("error")) {
|
|
256
553
|
stage = "provider_denial";
|
|
257
554
|
throw new SlackInstallCallbackError(
|
|
@@ -269,9 +566,10 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
269
566
|
"Slack installation callback is missing code",
|
|
270
567
|
);
|
|
271
568
|
}
|
|
569
|
+
await withOrganizationIntegrationAcquisition(db, state, ["slack-bot"], async () => {});
|
|
272
570
|
const slack = requireOpenGeniSlackOAuthSettings(settings);
|
|
273
571
|
const redirectUri = `${baseUrl}/v1/integrations/slack/callback`;
|
|
274
|
-
const
|
|
572
|
+
const authorization = await exchangeOpenGeniSlackAuthorizationCode(
|
|
275
573
|
{
|
|
276
574
|
code,
|
|
277
575
|
clientId: slack.clientId,
|
|
@@ -281,21 +579,72 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
281
579
|
deps.slackFetch ?? fetch,
|
|
282
580
|
);
|
|
283
581
|
stage = "credential_verification";
|
|
284
|
-
const verified = await verifyOpenGeniSlackBotCredential(
|
|
582
|
+
const verified = await verifyOpenGeniSlackBotCredential(
|
|
583
|
+
authorization.accessToken,
|
|
584
|
+
{
|
|
585
|
+
appId: authorization.appId,
|
|
586
|
+
displayName: settings.slackBotDisplayName,
|
|
587
|
+
},
|
|
588
|
+
deps.slackFetch ?? fetch,
|
|
589
|
+
new Date(),
|
|
590
|
+
);
|
|
285
591
|
stage = "permission_recheck";
|
|
286
592
|
await requireSlackInstallCallbackGrant(db, state);
|
|
287
593
|
stage = "persistence";
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
594
|
+
const acceptedState = state;
|
|
595
|
+
const persist = (tx: Database) =>
|
|
596
|
+
persistOpenGeniSlackBotConnection({
|
|
597
|
+
deps: { ...deps, db: tx },
|
|
598
|
+
state: acceptedState,
|
|
599
|
+
token: authorization.accessToken,
|
|
600
|
+
verified,
|
|
601
|
+
});
|
|
602
|
+
if (operation) {
|
|
603
|
+
const acceptedOperation = operation;
|
|
604
|
+
await withOrganizationIntegrationPolicyFence(db, acceptedState, (policyTx, policy) =>
|
|
605
|
+
finishConnectOperation(policyTx, acceptedState, {
|
|
606
|
+
...acceptedOperation,
|
|
607
|
+
authorize: (tx, _attempt, origin) =>
|
|
608
|
+
requireConnectOwnerAuthority(tx, acceptedState, "connections:write", origin),
|
|
609
|
+
commit: async (tx, current) => {
|
|
610
|
+
assertOrganizationIntegrationAllowed(policy, "slack-bot");
|
|
611
|
+
const connection = await persist(tx);
|
|
612
|
+
return {
|
|
613
|
+
...current,
|
|
614
|
+
revision: current.revision + 1,
|
|
615
|
+
state: "complete",
|
|
616
|
+
credentialsCommitted: true,
|
|
617
|
+
nextAction: { type: "none" },
|
|
618
|
+
account: {
|
|
619
|
+
id: connection.id,
|
|
620
|
+
version: connection.version,
|
|
621
|
+
providerId: "slack-bot",
|
|
622
|
+
label: "Slack workspace bot",
|
|
623
|
+
ownership: "workspace",
|
|
624
|
+
status: "connected",
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
},
|
|
628
|
+
}),
|
|
629
|
+
);
|
|
630
|
+
return new Response(null, { status: 302, headers: { Location: exactReturnUrl! } });
|
|
631
|
+
}
|
|
632
|
+
const connection = await withOrganizationIntegrationAcquisition(
|
|
633
|
+
db,
|
|
634
|
+
acceptedState,
|
|
635
|
+
["slack-bot"],
|
|
636
|
+
async (tx) => {
|
|
637
|
+
await requireSlackInstallCallbackGrant(tx, acceptedState);
|
|
638
|
+
return persist(tx);
|
|
639
|
+
},
|
|
640
|
+
);
|
|
294
641
|
return c.redirect(
|
|
295
642
|
slackInstallReturnUrl(baseUrl, state.returnPath, "connected", connection.id),
|
|
296
643
|
302,
|
|
297
644
|
);
|
|
298
645
|
} catch (error) {
|
|
646
|
+
if (exactReturnUrl)
|
|
647
|
+
return new Response(null, { status: 302, headers: { Location: exactReturnUrl } });
|
|
299
648
|
if (state) {
|
|
300
649
|
const failure = slackInstallCallbackFailure(stage, error);
|
|
301
650
|
try {
|
|
@@ -316,7 +665,12 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
316
665
|
}
|
|
317
666
|
const reason = slackInstallErrorReason(error);
|
|
318
667
|
return c.redirect(
|
|
319
|
-
slackInstallReturnUrl(
|
|
668
|
+
slackInstallReturnUrl(
|
|
669
|
+
baseUrl,
|
|
670
|
+
state?.returnPath ?? oauthStateFailureReturn(settings, c.req.query("state")).returnPath,
|
|
671
|
+
"error",
|
|
672
|
+
reason,
|
|
673
|
+
),
|
|
320
674
|
302,
|
|
321
675
|
);
|
|
322
676
|
}
|
|
@@ -328,80 +682,32 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
328
682
|
app.post("/v1/workspaces/:workspaceId/connections/fiken/install", async (c) => {
|
|
329
683
|
assertIntegrationsEnabled();
|
|
330
684
|
const workspaceId = c.req.param("workspaceId");
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const existing = payload.connectionId
|
|
335
|
-
? await getConnectionMetadata(db, workspaceId, payload.connectionId, null)
|
|
336
|
-
: null;
|
|
337
|
-
if (payload.connectionId && !existing) {
|
|
338
|
-
throw new HTTPException(404, { message: "connection not found" });
|
|
339
|
-
}
|
|
340
|
-
if (existing && !isFikenConnection(existing)) {
|
|
341
|
-
throw new HTTPException(422, { message: "connectionId is not a Fiken connection" });
|
|
342
|
-
}
|
|
343
|
-
const verified = await verifyFikenApiToken(payload.apiToken, deps.fikenFetch ?? fetch);
|
|
344
|
-
const previousDefault = existing
|
|
345
|
-
? (fikenConnectionMetadata(existing.metadata)?.defaultCompanySlug ?? null)
|
|
346
|
-
: null;
|
|
347
|
-
const defaultCompanySlug = resolveFikenDefaultCompanySlug({
|
|
348
|
-
requested: payload.defaultCompanySlug ?? null,
|
|
349
|
-
previous: previousDefault,
|
|
350
|
-
companies: verified.companies,
|
|
351
|
-
});
|
|
352
|
-
if (payload.defaultCompanySlug && defaultCompanySlug !== payload.defaultCompanySlug) {
|
|
353
|
-
throw new HTTPException(422, {
|
|
354
|
-
message: `defaultCompanySlug is not among the companies this token can access: ${verified.companies
|
|
355
|
-
.map((company) => company.slug)
|
|
356
|
-
.join(", ")}`,
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
const metadata = {
|
|
360
|
-
credentialRole: FIKEN_CREDENTIAL_ROLE,
|
|
361
|
-
credentialLabel: FIKEN_CREDENTIAL_LABEL,
|
|
362
|
-
companies: verified.companies,
|
|
363
|
-
defaultCompanySlug,
|
|
364
|
-
verifiedAt: new Date().toISOString(),
|
|
365
|
-
};
|
|
366
|
-
const credentialEncrypted = encryptCredentialBundle(
|
|
367
|
-
key,
|
|
368
|
-
fikenCredentialBundle(payload.apiToken),
|
|
369
|
-
);
|
|
370
|
-
if (existing) {
|
|
371
|
-
// Rewrites the whole credential identity: a token pasted over an OAuth
|
|
372
|
-
// row must also flip kind and clear the OAuth expiry, or the broker
|
|
373
|
-
// keeps treating the api_key bundle as a refreshable oauth2 credential.
|
|
374
|
-
const updated = await updateConnection(db, {
|
|
375
|
-
workspaceId,
|
|
376
|
-
connectionId: existing.id,
|
|
377
|
-
visibleToSubjectId: null,
|
|
378
|
-
expectedVersion: existing.version,
|
|
379
|
-
kind: "api_key",
|
|
380
|
-
status: "active",
|
|
381
|
-
credentialEncrypted,
|
|
382
|
-
grantedScopes: [],
|
|
383
|
-
expiresAt: null,
|
|
384
|
-
metadata,
|
|
385
|
-
updatedBySubjectId: grant.subjectId,
|
|
386
|
-
});
|
|
387
|
-
if (!updated) {
|
|
388
|
-
throw new HTTPException(409, { message: "the Fiken connection changed; retry" });
|
|
389
|
-
}
|
|
390
|
-
return c.json(ConnectionResponse.parse({ connection: updated }));
|
|
391
|
-
}
|
|
392
|
-
const connection = await createConnection(db, {
|
|
393
|
-
accountId: grant.accountId,
|
|
685
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
686
|
+
c,
|
|
687
|
+
deps,
|
|
394
688
|
workspaceId,
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
689
|
+
"connections:write",
|
|
690
|
+
);
|
|
691
|
+
const grant = authorization.grant;
|
|
692
|
+
const payload = await parseRequestJson(c, FikenInstallRequest);
|
|
693
|
+
const persist = await prepareFikenTokenInstall(deps, grant, payload);
|
|
694
|
+
const continuation = externalActorContinuationForAuthorization(authorization);
|
|
695
|
+
const connection = await withOrganizationIntegrationAcquisition(
|
|
696
|
+
db,
|
|
697
|
+
{ accountId: grant.accountId, workspaceId },
|
|
698
|
+
["fiken"],
|
|
699
|
+
async (tx) => {
|
|
700
|
+
await requireConnectOwnerAuthority(tx, {
|
|
701
|
+
accountId: grant.accountId,
|
|
702
|
+
workspaceId,
|
|
703
|
+
subjectId: grant.subjectId,
|
|
704
|
+
personalOwnerVerified: isPersonalConnectionOwnerPrincipal(authorization),
|
|
705
|
+
...(continuation ? { externalContinuation: continuation } : {}),
|
|
706
|
+
});
|
|
707
|
+
return persist(tx);
|
|
708
|
+
},
|
|
709
|
+
);
|
|
710
|
+
return c.json(ConnectionResponse.parse({ connection }), payload.connectionId ? 200 : 201);
|
|
405
711
|
});
|
|
406
712
|
|
|
407
713
|
// Fiken OAuth (registered app) start. Both Fiken lanes produce the same
|
|
@@ -409,8 +715,10 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
409
715
|
app.post("/v1/workspaces/:workspaceId/connections/fiken/oauth/start", async (c) => {
|
|
410
716
|
assertIntegrationsEnabled();
|
|
411
717
|
const workspaceId = c.req.param("workspaceId");
|
|
412
|
-
const
|
|
413
|
-
|
|
718
|
+
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
|
|
719
|
+
requireLegacyOAuthActor(access);
|
|
720
|
+
const grant = access.grant;
|
|
721
|
+
const payload = await parseRequestJson(c, FikenOAuthStartRequest);
|
|
414
722
|
requireEnvironmentEncryption(settings);
|
|
415
723
|
return c.json(
|
|
416
724
|
FikenOAuthStartResponse.parse(
|
|
@@ -433,13 +741,20 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
433
741
|
...(c.req.query("error") ? { error: c.req.query("error") } : {}),
|
|
434
742
|
requestUrl: c.req.url,
|
|
435
743
|
});
|
|
744
|
+
if (result.exactReturn)
|
|
745
|
+
return new Response(null, { status: 302, headers: { Location: result.redirectTo } });
|
|
436
746
|
return c.redirect(result.redirectTo, 302);
|
|
437
747
|
});
|
|
438
748
|
|
|
439
749
|
app.post("/v1/workspaces/:workspaceId/connections/google-drive/install", async (c) => {
|
|
440
750
|
assertIntegrationsEnabled();
|
|
441
751
|
const workspaceId = c.req.param("workspaceId");
|
|
442
|
-
|
|
752
|
+
// The Drive connector writes only a personal Connection, so only a managed
|
|
753
|
+
// human may start it.
|
|
754
|
+
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
|
|
755
|
+
assertPersonalConnectionOwnerPrincipal(access, "Google Drive");
|
|
756
|
+
requireLegacyOAuthActor(access);
|
|
757
|
+
const grant = access.grant;
|
|
443
758
|
const parsed = GoogleDriveOAuthStartRequest.safeParse(await c.req.json());
|
|
444
759
|
if (!parsed.success) {
|
|
445
760
|
throw new HTTPException(400, { message: "invalid Google Drive install request" });
|
|
@@ -460,7 +775,12 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
460
775
|
app.post("/v1/workspaces/:workspaceId/connections/atlassian/install", async (c) => {
|
|
461
776
|
assertIntegrationsEnabled();
|
|
462
777
|
const workspaceId = c.req.param("workspaceId");
|
|
463
|
-
|
|
778
|
+
// The Atlassian connector writes only a personal Connection, so only a
|
|
779
|
+
// managed human may start it.
|
|
780
|
+
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
|
|
781
|
+
assertPersonalConnectionOwnerPrincipal(access, "Atlassian");
|
|
782
|
+
requireLegacyOAuthActor(access);
|
|
783
|
+
const grant = access.grant;
|
|
464
784
|
const parsed = AtlassianOAuthStartRequest.safeParse(await c.req.json());
|
|
465
785
|
if (!parsed.success) {
|
|
466
786
|
throw new HTTPException(400, { message: "invalid Atlassian install request" });
|
|
@@ -486,6 +806,8 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
486
806
|
...(c.req.query("error") ? { error: c.req.query("error") } : {}),
|
|
487
807
|
requestUrl: c.req.url,
|
|
488
808
|
});
|
|
809
|
+
if (result.exactReturn)
|
|
810
|
+
return new Response(null, { status: 302, headers: { Location: result.redirectTo } });
|
|
489
811
|
return c.redirect(result.redirectTo, 302);
|
|
490
812
|
});
|
|
491
813
|
|
|
@@ -561,6 +883,8 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
561
883
|
requestUrl: c.req.url,
|
|
562
884
|
};
|
|
563
885
|
const result = await completeGoogleDriveOAuthCallback(deps, input);
|
|
886
|
+
if (result.exactReturn)
|
|
887
|
+
return new Response(null, { status: 302, headers: { Location: result.redirectTo } });
|
|
564
888
|
return c.redirect(result.redirectTo, 302);
|
|
565
889
|
});
|
|
566
890
|
|
|
@@ -659,150 +983,351 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
659
983
|
"connections:write",
|
|
660
984
|
);
|
|
661
985
|
const { grant } = authorization;
|
|
662
|
-
const payload =
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
986
|
+
const payload = await parseRequestJson(c, UpdateConnectionRequest);
|
|
987
|
+
// Policy first, then the existing tenancy prefix and exact credential row.
|
|
988
|
+
// The locked live identity makes unchanged/reducing updates safe against a
|
|
989
|
+
// concurrent replacement; all final writes below use this same transaction.
|
|
990
|
+
return withOrganizationIntegrationPolicyFence(
|
|
667
991
|
db,
|
|
668
|
-
workspaceId,
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
992
|
+
{ accountId: grant.accountId, workspaceId },
|
|
993
|
+
(policyTx, policy) =>
|
|
994
|
+
// eslint-disable-next-line no-shadow -- every existing final writer must use this exact scoped transaction
|
|
995
|
+
withWorkspaceSubjectRls(policyTx, workspaceId, grant.subjectId, async (db) => {
|
|
996
|
+
const initial = await getConnectionMetadata(
|
|
997
|
+
db,
|
|
998
|
+
workspaceId,
|
|
999
|
+
c.req.param("connectionId"),
|
|
1000
|
+
grant.subjectId,
|
|
1001
|
+
);
|
|
1002
|
+
if (
|
|
1003
|
+
initial?.metadata.directModelProvider !== undefined ||
|
|
1004
|
+
payload.metadata?.directModelProvider !== undefined ||
|
|
1005
|
+
String(payload.metadata?.credentialRole ?? "").startsWith("direct_")
|
|
1006
|
+
) {
|
|
1007
|
+
throw new HTTPException(422, {
|
|
1008
|
+
message:
|
|
1009
|
+
"Disconnect and reconnect OpenAI or Azure OpenAI to replace its key or deployment",
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
// Model-key rotation owns an advisory -> credential-row prefix. Do
|
|
1013
|
+
// not take its row first or change that excluded lane's lock order.
|
|
1014
|
+
if (!initial || !workspaceProviderApiKeyConnectionKind(initial)) {
|
|
1015
|
+
await db.execute(
|
|
1016
|
+
sql`select id from connections where workspace_id = ${workspaceId}::uuid and id = ${c.req.param("connectionId")}::uuid and (subject_id is null or subject_id = ${grant.subjectId}) for update`,
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
assertNotReservedSlackBotMetadata(payload.metadata);
|
|
1020
|
+
assertNotReservedFikenMetadata(payload.metadata);
|
|
1021
|
+
assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
|
|
1022
|
+
assertNotReservedPersonalGitHubMetadata(payload.metadata);
|
|
1023
|
+
const existing = await getConnectionMetadata(
|
|
1024
|
+
db,
|
|
1025
|
+
workspaceId,
|
|
1026
|
+
c.req.param("connectionId"),
|
|
1027
|
+
grant.subjectId,
|
|
1028
|
+
);
|
|
1029
|
+
if (existing && isOpenGeniSlackBotConnection(existing)) {
|
|
1030
|
+
const destination = ConnectorDocumentDestinationSelection.safeParse(
|
|
1031
|
+
payload.metadata?.documentDestination,
|
|
1032
|
+
);
|
|
1033
|
+
const destinationOnlyUpdate =
|
|
1034
|
+
payload.metadata !== undefined &&
|
|
1035
|
+
Object.keys(payload.metadata).length === 1 &&
|
|
1036
|
+
payload.providerDomain === undefined &&
|
|
1037
|
+
payload.subjectId === undefined &&
|
|
1038
|
+
payload.kind === undefined &&
|
|
1039
|
+
payload.status === undefined &&
|
|
1040
|
+
payload.credential === undefined &&
|
|
1041
|
+
payload.grantedScopes === undefined &&
|
|
1042
|
+
payload.expiresAt === undefined;
|
|
1043
|
+
if (!destination.success || !destinationOnlyUpdate) {
|
|
1044
|
+
throw new HTTPException(422, {
|
|
1045
|
+
message:
|
|
1046
|
+
"use the dedicated Opengeni Slack bot reinstall flow to update this connection",
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
const destinationSelection = destination.data;
|
|
1050
|
+
if (
|
|
1051
|
+
destinationSelection.authorityKind === "organization" &&
|
|
1052
|
+
authorization.accountGrant?.permissions.includes("account:admin") !== true
|
|
1053
|
+
) {
|
|
1054
|
+
throw new HTTPException(403, { message: "missing permission: account:admin" });
|
|
1055
|
+
}
|
|
1056
|
+
if (
|
|
1057
|
+
destinationSelection.authorityKind === "workspace" &&
|
|
1058
|
+
!hasPermission(grant.permissions, "workspace:admin")
|
|
1059
|
+
) {
|
|
1060
|
+
throw new HTTPException(403, { message: "missing permission: workspace:admin" });
|
|
1061
|
+
}
|
|
1062
|
+
if (
|
|
1063
|
+
destinationSelection.authorityKind === "personal" &&
|
|
1064
|
+
(!authorization.contextIntegrity ||
|
|
1065
|
+
authorization.authenticatedSubjectId !== grant.subjectId)
|
|
1066
|
+
) {
|
|
1067
|
+
throw new HTTPException(403, {
|
|
1068
|
+
message: "personal destination requires the exact actor",
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
const metadata = {
|
|
1072
|
+
...existing.metadata,
|
|
1073
|
+
documentDestination: bindConnectorDocumentDestination(destinationSelection, {
|
|
1074
|
+
accountId: grant.accountId,
|
|
1075
|
+
workspaceId,
|
|
1076
|
+
initiatingSubjectId: grant.subjectId,
|
|
1077
|
+
}),
|
|
1078
|
+
};
|
|
1079
|
+
if (
|
|
1080
|
+
stableJson(metadata.documentDestination) !==
|
|
1081
|
+
stableJson(existing.metadata.documentDestination)
|
|
1082
|
+
) {
|
|
1083
|
+
assertOrganizationIntegrationAllowed(policy, "slack-bot");
|
|
1084
|
+
}
|
|
1085
|
+
const connection = await updateSlackBotDocumentDestination(db, {
|
|
1086
|
+
accountId: grant.accountId,
|
|
1087
|
+
workspaceId,
|
|
1088
|
+
connectionId: existing.id,
|
|
1089
|
+
visibleToSubjectId: grant.subjectId,
|
|
1090
|
+
expectedVersion: existing.version,
|
|
1091
|
+
metadata,
|
|
1092
|
+
updatedBySubjectId: grant.subjectId,
|
|
1093
|
+
});
|
|
1094
|
+
if (!connection) {
|
|
1095
|
+
throw new HTTPException(409, {
|
|
1096
|
+
message: "Slack bot connection changed; reload before saving its destination",
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
return c.json(ConnectionResponse.parse({ connection }));
|
|
1100
|
+
}
|
|
1101
|
+
if (existing && GoogleDriveConnectionMetadata.safeParse(existing.metadata).success) {
|
|
1102
|
+
throw new HTTPException(422, {
|
|
1103
|
+
message: "use the dedicated Google Drive reconnect or source-selection flow",
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
if (existing?.metadata.credentialRole === API_INTEGRATION_OAUTH_CREDENTIAL_ROLE) {
|
|
1107
|
+
throw new HTTPException(422, {
|
|
1108
|
+
message:
|
|
1109
|
+
"use the dedicated Integration provider OAuth flow to update this connection",
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
if (existing && AtlassianConnectionMetadata.safeParse(existing.metadata).success) {
|
|
1113
|
+
throw new HTTPException(422, {
|
|
1114
|
+
message: "use the dedicated Atlassian reconnect or source-selection flow",
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
if (existing && isPersonalGitHubConnection(existing)) {
|
|
1118
|
+
throw new HTTPException(422, {
|
|
1119
|
+
message: "use the dedicated personal GitHub reconnect flow to update this connection",
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
if (existing) {
|
|
1123
|
+
const providerDomain = canonicalProviderDomain(
|
|
1124
|
+
payload.providerDomain ?? existing.providerDomain,
|
|
1125
|
+
);
|
|
1126
|
+
const kind = payload.kind ?? existing.kind;
|
|
1127
|
+
const subjectId =
|
|
1128
|
+
payload.subjectId === undefined
|
|
1129
|
+
? existing.subjectId
|
|
1130
|
+
: writableSubjectId(payload.subjectId, grant.subjectId);
|
|
1131
|
+
const targetWorkspaceProviderKind = workspaceProviderApiKeyConnectionKind({
|
|
1132
|
+
subjectId,
|
|
1133
|
+
providerDomain,
|
|
1134
|
+
kind,
|
|
1135
|
+
metadata: payload.metadata ?? existing.metadata,
|
|
1136
|
+
});
|
|
1137
|
+
if (!workspaceProviderApiKeyConnectionKind(existing) && targetWorkspaceProviderKind) {
|
|
1138
|
+
throw new HTTPException(422, {
|
|
1139
|
+
message:
|
|
1140
|
+
targetWorkspaceProviderKind === "vercel_gateway"
|
|
1141
|
+
? "use the Vercel AI Gateway connect flow to create this connection"
|
|
1142
|
+
: "use the OpenRouter connect flow to create this connection",
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
assertNotDirectPersonalSlackOAuth(providerDomain, kind);
|
|
1146
|
+
assertNotDirectGoogleDriveOAuth(
|
|
1147
|
+
providerDomain,
|
|
1148
|
+
kind,
|
|
1149
|
+
payload.metadata ?? existing.metadata,
|
|
1150
|
+
);
|
|
1151
|
+
assertNotDirectAtlassianOAuth(
|
|
1152
|
+
providerDomain,
|
|
1153
|
+
kind,
|
|
1154
|
+
payload.metadata ?? existing.metadata,
|
|
1155
|
+
);
|
|
1156
|
+
assertNotDirectPersonalGitHubOAuth(
|
|
1157
|
+
providerDomain,
|
|
1158
|
+
kind,
|
|
1159
|
+
payload.metadata ?? existing.metadata,
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
// Status is not a free-form field: revocation goes through DELETE, and the
|
|
1163
|
+
// broker owns needs_reauth/error. Reactivating a connection is only
|
|
1164
|
+
// meaningful together with a fresh credential bundle — otherwise a PATCH
|
|
1165
|
+
// could clear the broker's re-auth signal while stale tokens stay in place.
|
|
1166
|
+
if (payload.status !== undefined) {
|
|
1167
|
+
if (payload.status !== "active") {
|
|
1168
|
+
throw new HTTPException(400, {
|
|
1169
|
+
message: 'status can only be set to "active"; use DELETE to revoke',
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
if (payload.credential === undefined) {
|
|
1173
|
+
throw new HTTPException(400, {
|
|
1174
|
+
message: "reactivating a connection requires a new credential",
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
const existingWorkspaceProviderKind = existing
|
|
1179
|
+
? workspaceProviderApiKeyConnectionKind(existing)
|
|
1180
|
+
: null;
|
|
1181
|
+
if (existing && existingWorkspaceProviderKind) {
|
|
1182
|
+
const provider = workspaceProviderApiKeyConnectionSpec(existingWorkspaceProviderKind);
|
|
1183
|
+
const providerDomain = canonicalProviderDomain(
|
|
1184
|
+
payload.providerDomain ?? existing.providerDomain,
|
|
1185
|
+
);
|
|
1186
|
+
const subjectId =
|
|
1187
|
+
payload.subjectId === undefined
|
|
1188
|
+
? existing.subjectId
|
|
1189
|
+
: writableSubjectId(payload.subjectId, grant.subjectId);
|
|
1190
|
+
const kind = payload.kind ?? existing.kind;
|
|
1191
|
+
if (
|
|
1192
|
+
subjectId !== null ||
|
|
1193
|
+
providerDomain !== provider.providerDomain ||
|
|
1194
|
+
kind !== "api_key" ||
|
|
1195
|
+
(payload.metadata?.credentialRole !== undefined &&
|
|
1196
|
+
payload.metadata.credentialRole !== provider.credentialRole)
|
|
1197
|
+
) {
|
|
1198
|
+
throw new HTTPException(422, {
|
|
1199
|
+
message: `${provider.label} connection identity cannot be changed`,
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
if (payload.credential === undefined) {
|
|
1203
|
+
throw new HTTPException(400, {
|
|
1204
|
+
message: `updating a ${provider.label} connection requires a new credential`,
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
if (payload.expectedVersion === undefined || payload.operationId === undefined) {
|
|
1208
|
+
throw new HTTPException(400, {
|
|
1209
|
+
message: `updating a ${provider.label} connection requires expectedVersion and operationId`,
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
payload.credential = prepareClaudeWorkspaceCredential(
|
|
1213
|
+
settings,
|
|
1214
|
+
workspaceId,
|
|
1215
|
+
existing.metadata,
|
|
1216
|
+
payload.credential,
|
|
1217
|
+
);
|
|
1218
|
+
assertClaudeWorkspaceCredential(settings, {
|
|
1219
|
+
subjectId,
|
|
1220
|
+
providerDomain,
|
|
1221
|
+
kind,
|
|
1222
|
+
metadata: existing.metadata,
|
|
1223
|
+
credential: payload.credential,
|
|
1224
|
+
});
|
|
1225
|
+
const key = requireEnvironmentEncryption(settings);
|
|
1226
|
+
const grantedScopes = payload.grantedScopes ?? existing.grantedScopes;
|
|
1227
|
+
const expiresAt =
|
|
1228
|
+
payload.expiresAt !== undefined
|
|
1229
|
+
? payload.expiresAt
|
|
1230
|
+
? new Date(payload.expiresAt)
|
|
1231
|
+
: null
|
|
1232
|
+
: existing.expiresAt
|
|
1233
|
+
? new Date(existing.expiresAt)
|
|
1234
|
+
: null;
|
|
1235
|
+
const metadata = workspaceProviderCredentialMetadata(existingWorkspaceProviderKind, {
|
|
1236
|
+
...existing.metadata,
|
|
1237
|
+
...(payload.metadata ?? {}),
|
|
1238
|
+
});
|
|
1239
|
+
const input = {
|
|
1240
|
+
accountId: grant.accountId,
|
|
1241
|
+
workspaceId,
|
|
1242
|
+
connectionId: existing.id,
|
|
1243
|
+
expectedVersion: payload.expectedVersion,
|
|
1244
|
+
operationId: payload.operationId,
|
|
1245
|
+
requestDigest: workspaceProviderCredentialRequestDigest(key, {
|
|
1246
|
+
action: "rotate",
|
|
1247
|
+
connectionId: existing.id,
|
|
1248
|
+
expectedVersion: payload.expectedVersion,
|
|
1249
|
+
credential: payload.credential,
|
|
1250
|
+
grantedScopes,
|
|
1251
|
+
expiresAt: expiresAt?.toISOString() ?? null,
|
|
1252
|
+
metadata,
|
|
1253
|
+
}),
|
|
1254
|
+
credentialEncrypted: encryptCredentialBundle(key, payload.credential),
|
|
1255
|
+
grantedScopes,
|
|
1256
|
+
expiresAt,
|
|
1257
|
+
metadata,
|
|
1258
|
+
updatedBySubjectId: grant.subjectId,
|
|
1259
|
+
};
|
|
1260
|
+
const connection = await rotateWorkspaceProviderApiKeyConnection(
|
|
1261
|
+
db,
|
|
1262
|
+
existingWorkspaceProviderKind,
|
|
1263
|
+
input,
|
|
1264
|
+
);
|
|
1265
|
+
if (!connection) {
|
|
1266
|
+
throw new HTTPException(409, {
|
|
1267
|
+
message: `${provider.label} connection changed; reload before replacing its key`,
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
return c.json(ConnectionResponse.parse({ connection }));
|
|
1271
|
+
}
|
|
1272
|
+
const key =
|
|
1273
|
+
payload.credential === undefined ? null : requireEnvironmentEncryption(settings);
|
|
1274
|
+
const subjectId =
|
|
1275
|
+
payload.subjectId === undefined
|
|
1276
|
+
? undefined
|
|
1277
|
+
: writableSubjectId(payload.subjectId, grant.subjectId);
|
|
1278
|
+
if (
|
|
1279
|
+
existing &&
|
|
1280
|
+
(payload.credential !== undefined ||
|
|
1281
|
+
(payload.providerDomain !== undefined &&
|
|
1282
|
+
canonicalProviderDomain(payload.providerDomain) !== existing.providerDomain) ||
|
|
1283
|
+
(payload.kind !== undefined && payload.kind !== existing.kind) ||
|
|
1284
|
+
(subjectId !== undefined && subjectId !== existing.subjectId) ||
|
|
1285
|
+
payload.grantedScopes?.some((scope) => !existing.grantedScopes.includes(scope)) ||
|
|
1286
|
+
(payload.metadata !== undefined &&
|
|
1287
|
+
["mcpUrl", "resource", "credentialRole"].some(
|
|
1288
|
+
(field) =>
|
|
1289
|
+
stableJson(payload.metadata?.[field] ?? null) !==
|
|
1290
|
+
stableJson(existing.metadata[field] ?? null),
|
|
1291
|
+
)) ||
|
|
1292
|
+
(payload.expiresAt !== undefined &&
|
|
1293
|
+
existing.expiresAt !== null &&
|
|
1294
|
+
(payload.expiresAt === null ||
|
|
1295
|
+
Date.parse(payload.expiresAt) > Date.parse(existing.expiresAt))) ||
|
|
1296
|
+
(payload.status === "active" && existing.status !== "active"))
|
|
1297
|
+
) {
|
|
1298
|
+
assertOrganizationIntegrationAllowed(policy, null);
|
|
1299
|
+
}
|
|
1300
|
+
if (payload.credential !== undefined) {
|
|
1301
|
+
assertBrokeredApiKeyCredential(payload.kind ?? existing?.kind, payload.credential);
|
|
1302
|
+
}
|
|
1303
|
+
const connection = await updateConnection(db, {
|
|
1304
|
+
workspaceId,
|
|
1305
|
+
connectionId: c.req.param("connectionId"),
|
|
1306
|
+
visibleToSubjectId: grant.subjectId,
|
|
1307
|
+
updatedBySubjectId: grant.subjectId,
|
|
1308
|
+
...(payload.providerDomain !== undefined
|
|
1309
|
+
? { providerDomain: canonicalProviderDomain(payload.providerDomain) }
|
|
1310
|
+
: {}),
|
|
1311
|
+
...(subjectId !== undefined ? { subjectId } : {}),
|
|
1312
|
+
...(payload.kind !== undefined ? { kind: payload.kind } : {}),
|
|
1313
|
+
...(payload.status !== undefined ? { status: payload.status } : {}),
|
|
1314
|
+
...(payload.credential !== undefined && key
|
|
1315
|
+
? { credentialEncrypted: encryptCredentialBundle(key, payload.credential) }
|
|
1316
|
+
: {}),
|
|
1317
|
+
...(payload.grantedScopes !== undefined
|
|
1318
|
+
? { grantedScopes: payload.grantedScopes }
|
|
1319
|
+
: {}),
|
|
1320
|
+
...(payload.expiresAt !== undefined
|
|
1321
|
+
? { expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null }
|
|
1322
|
+
: {}),
|
|
1323
|
+
...(payload.metadata !== undefined ? { metadata: payload.metadata } : {}),
|
|
1324
|
+
});
|
|
1325
|
+
if (!connection) {
|
|
1326
|
+
throw new HTTPException(404, { message: "connection not found" });
|
|
1327
|
+
}
|
|
1328
|
+
return c.json(ConnectionResponse.parse({ connection }));
|
|
719
1329
|
}),
|
|
720
|
-
|
|
721
|
-
const connection = await updateSlackBotDocumentDestination(db, {
|
|
722
|
-
accountId: grant.accountId,
|
|
723
|
-
workspaceId,
|
|
724
|
-
connectionId: existing.id,
|
|
725
|
-
visibleToSubjectId: grant.subjectId,
|
|
726
|
-
expectedVersion: existing.version,
|
|
727
|
-
metadata,
|
|
728
|
-
updatedBySubjectId: grant.subjectId,
|
|
729
|
-
});
|
|
730
|
-
if (!connection) {
|
|
731
|
-
throw new HTTPException(409, {
|
|
732
|
-
message: "Slack bot connection changed; reload before saving its destination",
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
return c.json(ConnectionResponse.parse({ connection }));
|
|
736
|
-
}
|
|
737
|
-
if (existing && GoogleDriveConnectionMetadata.safeParse(existing.metadata).success) {
|
|
738
|
-
throw new HTTPException(422, {
|
|
739
|
-
message: "use the dedicated Google Drive reconnect or source-selection flow",
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
if (existing?.metadata.credentialRole === API_INTEGRATION_OAUTH_CREDENTIAL_ROLE) {
|
|
743
|
-
throw new HTTPException(422, {
|
|
744
|
-
message: "use the dedicated Integration provider OAuth flow to update this connection",
|
|
745
|
-
});
|
|
746
|
-
}
|
|
747
|
-
if (existing && AtlassianConnectionMetadata.safeParse(existing.metadata).success) {
|
|
748
|
-
throw new HTTPException(422, {
|
|
749
|
-
message: "use the dedicated Atlassian reconnect or source-selection flow",
|
|
750
|
-
});
|
|
751
|
-
}
|
|
752
|
-
if (existing) {
|
|
753
|
-
const providerDomain = canonicalProviderDomain(
|
|
754
|
-
payload.providerDomain ?? existing.providerDomain,
|
|
755
|
-
);
|
|
756
|
-
const kind = payload.kind ?? existing.kind;
|
|
757
|
-
assertNotDirectPersonalSlackOAuth(providerDomain, kind);
|
|
758
|
-
assertNotDirectGoogleDriveOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
|
|
759
|
-
assertNotDirectAtlassianOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
|
|
760
|
-
}
|
|
761
|
-
// Status is not a free-form field: revocation goes through DELETE, and the
|
|
762
|
-
// broker owns needs_reauth/error. Reactivating a connection is only
|
|
763
|
-
// meaningful together with a fresh credential bundle — otherwise a PATCH
|
|
764
|
-
// could clear the broker's re-auth signal while stale tokens stay in place.
|
|
765
|
-
if (payload.status !== undefined) {
|
|
766
|
-
if (payload.status !== "active") {
|
|
767
|
-
throw new HTTPException(400, {
|
|
768
|
-
message: 'status can only be set to "active"; use DELETE to revoke',
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
if (payload.credential === undefined) {
|
|
772
|
-
throw new HTTPException(400, {
|
|
773
|
-
message: "reactivating a connection requires a new credential",
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
const key = payload.credential === undefined ? null : requireEnvironmentEncryption(settings);
|
|
778
|
-
const subjectId =
|
|
779
|
-
payload.subjectId === undefined
|
|
780
|
-
? undefined
|
|
781
|
-
: writableSubjectId(payload.subjectId, grant.subjectId);
|
|
782
|
-
const connection = await updateConnection(db, {
|
|
783
|
-
workspaceId,
|
|
784
|
-
connectionId: c.req.param("connectionId"),
|
|
785
|
-
visibleToSubjectId: grant.subjectId,
|
|
786
|
-
updatedBySubjectId: grant.subjectId,
|
|
787
|
-
...(payload.providerDomain !== undefined
|
|
788
|
-
? { providerDomain: canonicalProviderDomain(payload.providerDomain) }
|
|
789
|
-
: {}),
|
|
790
|
-
...(subjectId !== undefined ? { subjectId } : {}),
|
|
791
|
-
...(payload.kind !== undefined ? { kind: payload.kind } : {}),
|
|
792
|
-
...(payload.status !== undefined ? { status: payload.status } : {}),
|
|
793
|
-
...(payload.credential !== undefined && key
|
|
794
|
-
? { credentialEncrypted: encryptCredentialBundle(key, payload.credential) }
|
|
795
|
-
: {}),
|
|
796
|
-
...(payload.grantedScopes !== undefined ? { grantedScopes: payload.grantedScopes } : {}),
|
|
797
|
-
...(payload.expiresAt !== undefined
|
|
798
|
-
? { expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null }
|
|
799
|
-
: {}),
|
|
800
|
-
...(payload.metadata !== undefined ? { metadata: payload.metadata } : {}),
|
|
801
|
-
});
|
|
802
|
-
if (!connection) {
|
|
803
|
-
throw new HTTPException(404, { message: "connection not found" });
|
|
804
|
-
}
|
|
805
|
-
return c.json(ConnectionResponse.parse({ connection }));
|
|
1330
|
+
);
|
|
806
1331
|
});
|
|
807
1332
|
|
|
808
1333
|
app.delete("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
|
|
@@ -813,6 +1338,26 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
813
1338
|
if (!existing) {
|
|
814
1339
|
throw new HTTPException(404, { message: "connection not found" });
|
|
815
1340
|
}
|
|
1341
|
+
const expectedVersion = c.req.query("expectedVersion");
|
|
1342
|
+
if (expectedVersion !== undefined) {
|
|
1343
|
+
if (!/^[1-9]\d*$/.test(expectedVersion) || !Number.isSafeInteger(Number(expectedVersion))) {
|
|
1344
|
+
throw new HTTPException(400, { message: "invalid expected connection version" });
|
|
1345
|
+
}
|
|
1346
|
+
if (Number(expectedVersion) !== existing.version) {
|
|
1347
|
+
throw new HTTPException(409, { message: "connection changed; refresh before disconnect" });
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
const isPersonalGitHub =
|
|
1351
|
+
existing.subjectId === grant.subjectId && isPersonalGitHubConnection(existing);
|
|
1352
|
+
if (isPersonalGitHub) {
|
|
1353
|
+
const access = await requireAccessGrantAuthorization(
|
|
1354
|
+
c,
|
|
1355
|
+
deps,
|
|
1356
|
+
workspaceId,
|
|
1357
|
+
"connections:write",
|
|
1358
|
+
);
|
|
1359
|
+
assertPersonalConnectionOwnerPrincipal(access, "My GitHub account");
|
|
1360
|
+
}
|
|
816
1361
|
const isGoogleDrive =
|
|
817
1362
|
existing.subjectId === grant.subjectId &&
|
|
818
1363
|
existing.providerDomain === GOOGLE_DRIVE_PROVIDER_DOMAIN &&
|
|
@@ -823,6 +1368,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
823
1368
|
existing.providerDomain === ATLASSIAN_PROVIDER_DOMAIN &&
|
|
824
1369
|
existing.kind === "oauth2" &&
|
|
825
1370
|
AtlassianConnectionMetadata.safeParse(existing.metadata).success;
|
|
1371
|
+
const workspaceProviderKind = workspaceProviderApiKeyConnectionKind(existing);
|
|
826
1372
|
const disconnectPayload = await c.req.json().catch(() => null);
|
|
827
1373
|
const googleDriveDisconnect = isGoogleDrive
|
|
828
1374
|
? GoogleDriveDisconnectRequest.safeParse(disconnectPayload)
|
|
@@ -843,7 +1389,23 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
843
1389
|
atlassianDisconnect.error.issues[0]?.message ?? "invalid Atlassian disconnect request",
|
|
844
1390
|
});
|
|
845
1391
|
}
|
|
846
|
-
|
|
1392
|
+
const personalGitHubDisconnect = isPersonalGitHub
|
|
1393
|
+
? PersonalGitHubDisconnectRequest.safeParse(disconnectPayload)
|
|
1394
|
+
: null;
|
|
1395
|
+
if (personalGitHubDisconnect && !personalGitHubDisconnect.success) {
|
|
1396
|
+
throw new HTTPException(400, {
|
|
1397
|
+
message:
|
|
1398
|
+
personalGitHubDisconnect.error.issues[0]?.message ??
|
|
1399
|
+
"invalid personal GitHub disconnect request",
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
if (
|
|
1403
|
+
existing.status === "revoked" &&
|
|
1404
|
+
!isGoogleDrive &&
|
|
1405
|
+
!isAtlassian &&
|
|
1406
|
+
!isPersonalGitHub &&
|
|
1407
|
+
!workspaceProviderKind
|
|
1408
|
+
) {
|
|
847
1409
|
return c.json(ConnectionResponse.parse({ connection: existing }));
|
|
848
1410
|
}
|
|
849
1411
|
const connection = isGoogleDrive
|
|
@@ -860,24 +1422,39 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
860
1422
|
connection: existing,
|
|
861
1423
|
payload: atlassianDisconnect!.data,
|
|
862
1424
|
})
|
|
863
|
-
:
|
|
864
|
-
? await
|
|
865
|
-
accountId: grant.accountId,
|
|
1425
|
+
: isPersonalGitHub
|
|
1426
|
+
? await disconnectPersonalGitHub(deps, {
|
|
866
1427
|
workspaceId,
|
|
867
1428
|
subjectId: grant.subjectId,
|
|
868
1429
|
connectionId,
|
|
869
|
-
|
|
870
|
-
credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
|
|
871
|
-
credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
|
|
872
|
-
slackTeamId: openGeniSlackBotMetadata(existing.metadata)!.slackTeamId,
|
|
1430
|
+
payload: personalGitHubDisconnect!.data,
|
|
873
1431
|
})
|
|
874
|
-
:
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1432
|
+
: isOpenGeniSlackBotConnection(existing)
|
|
1433
|
+
? await revokeConnectionWithSlackBotSuccessAudit(db, {
|
|
1434
|
+
accountId: grant.accountId,
|
|
1435
|
+
workspaceId,
|
|
1436
|
+
subjectId: grant.subjectId,
|
|
1437
|
+
connectionId,
|
|
1438
|
+
expectedVersion: existing.version,
|
|
1439
|
+
credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
|
|
1440
|
+
credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
|
|
1441
|
+
slackTeamId: openGeniSlackBotMetadata(existing.metadata)!.slackTeamId,
|
|
1442
|
+
})
|
|
1443
|
+
: workspaceProviderKind
|
|
1444
|
+
? await revokeWorkspaceProviderApiKeyConnections(db, workspaceProviderKind, {
|
|
1445
|
+
accountId: grant.accountId,
|
|
1446
|
+
workspaceId,
|
|
1447
|
+
connectionId,
|
|
1448
|
+
expectedVersion: existing.version,
|
|
1449
|
+
updatedBySubjectId: grant.subjectId,
|
|
1450
|
+
})
|
|
1451
|
+
: await revokeConnection(
|
|
1452
|
+
db,
|
|
1453
|
+
workspaceId,
|
|
1454
|
+
connectionId,
|
|
1455
|
+
grant.subjectId,
|
|
1456
|
+
existing.version,
|
|
1457
|
+
);
|
|
881
1458
|
if (!connection) {
|
|
882
1459
|
throw new HTTPException(409, { message: "connection changed during disconnect; try again" });
|
|
883
1460
|
}
|
|
@@ -887,7 +1464,9 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
887
1464
|
app.post("/v1/workspaces/:workspaceId/connections/oauth/start", async (c) => {
|
|
888
1465
|
assertIntegrationsEnabled();
|
|
889
1466
|
const workspaceId = c.req.param("workspaceId");
|
|
890
|
-
const
|
|
1467
|
+
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
|
|
1468
|
+
requireProviderConsentInBrowser(access);
|
|
1469
|
+
const grant = access.grant;
|
|
891
1470
|
const parsed = OAuthStartRequest.safeParse(await c.req.json());
|
|
892
1471
|
if (!parsed.success) {
|
|
893
1472
|
throw new HTTPException(400, {
|
|
@@ -898,9 +1477,13 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
898
1477
|
const result = await startMcpOAuth(
|
|
899
1478
|
{ db, settings, observability, oauthStartDeadlineMs: deps.oauthStartDeadlineMs },
|
|
900
1479
|
{
|
|
1480
|
+
...(externalActorContinuationForAuthorization(access)
|
|
1481
|
+
? { externalContinuation: externalActorContinuationForAuthorization(access)! }
|
|
1482
|
+
: {}),
|
|
901
1483
|
accountId: grant.accountId,
|
|
902
1484
|
workspaceId,
|
|
903
1485
|
subjectId: grant.subjectId,
|
|
1486
|
+
personalOwnershipAllowed: isPersonalConnectionOwnerPrincipal(access),
|
|
904
1487
|
requestUrl: c.req.url,
|
|
905
1488
|
payload,
|
|
906
1489
|
},
|
|
@@ -911,7 +1494,9 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
911
1494
|
app.post("/v1/workspaces/:workspaceId/integrations/oauth/start", async (c) => {
|
|
912
1495
|
assertIntegrationsEnabled();
|
|
913
1496
|
const workspaceId = c.req.param("workspaceId");
|
|
914
|
-
const
|
|
1497
|
+
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
|
|
1498
|
+
requireProviderConsentInBrowser(access);
|
|
1499
|
+
const grant = access.grant;
|
|
915
1500
|
const parsed = ApiIntegrationOAuthStartRequest.safeParse(await c.req.json());
|
|
916
1501
|
if (!parsed.success) {
|
|
917
1502
|
throw new HTTPException(400, {
|
|
@@ -921,9 +1506,13 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
921
1506
|
return c.json(
|
|
922
1507
|
OAuthStartResponse.parse(
|
|
923
1508
|
await startApiIntegrationProviderOAuth(deps, {
|
|
1509
|
+
...(externalActorContinuationForAuthorization(access)
|
|
1510
|
+
? { externalContinuation: externalActorContinuationForAuthorization(access)! }
|
|
1511
|
+
: {}),
|
|
924
1512
|
accountId: grant.accountId,
|
|
925
1513
|
workspaceId,
|
|
926
1514
|
subjectId: grant.subjectId,
|
|
1515
|
+
personalOwnershipAllowed: isPersonalConnectionOwnerPrincipal(access),
|
|
927
1516
|
requestUrl: c.req.url,
|
|
928
1517
|
payload: parsed.data,
|
|
929
1518
|
}),
|
|
@@ -933,15 +1522,24 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
933
1522
|
|
|
934
1523
|
app.get("/v1/integrations/oauth/callback", async (c) => {
|
|
935
1524
|
assertIntegrationsEnabled();
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
{
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1525
|
+
const input = {
|
|
1526
|
+
...(c.req.query("code") ? { code: c.req.query("code") } : {}),
|
|
1527
|
+
...(c.req.query("state") ? { state: c.req.query("state") } : {}),
|
|
1528
|
+
...(c.req.query("error") ? { error: c.req.query("error") } : {}),
|
|
1529
|
+
requestUrl: c.req.url,
|
|
1530
|
+
};
|
|
1531
|
+
const result = isApiIntegrationProviderOAuthState(input.state, deps.settings)
|
|
1532
|
+
? await completeApiIntegrationProviderOAuth(deps, input)
|
|
1533
|
+
: await completeMcpOAuthCallback(
|
|
1534
|
+
{ db, settings, observability, oauthCallbackDeadlineMs: deps.oauthCallbackDeadlineMs },
|
|
1535
|
+
input,
|
|
1536
|
+
);
|
|
1537
|
+
if ("exactReturn" in result && result.exactReturn)
|
|
1538
|
+
return new Response(null, { status: 302, headers: { Location: result.redirectTo } });
|
|
1539
|
+
const redirectTo = settings.webBaseUrl
|
|
1540
|
+
? new URL(result.redirectTo, settings.webBaseUrl).toString()
|
|
1541
|
+
: result.redirectTo;
|
|
1542
|
+
return c.redirect(redirectTo, 302);
|
|
945
1543
|
});
|
|
946
1544
|
|
|
947
1545
|
app.get("/v1/integrations/provider-oauth/callback", async (c) => {
|
|
@@ -952,6 +1550,8 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
952
1550
|
...(c.req.query("error") ? { error: c.req.query("error") } : {}),
|
|
953
1551
|
requestUrl: c.req.url,
|
|
954
1552
|
});
|
|
1553
|
+
if (result.exactReturn)
|
|
1554
|
+
return new Response(null, { status: 302, headers: { Location: result.redirectTo } });
|
|
955
1555
|
return c.redirect(result.redirectTo, 302);
|
|
956
1556
|
});
|
|
957
1557
|
|
|
@@ -1000,7 +1600,7 @@ async function persistOpenGeniSlackBotConnection(input: {
|
|
|
1000
1600
|
throw new SlackInstallCallbackError(
|
|
1001
1601
|
422,
|
|
1002
1602
|
"connection_conflict",
|
|
1003
|
-
"connectionId is not an
|
|
1603
|
+
"connectionId is not an Opengeni Slack bot connection",
|
|
1004
1604
|
"principal_validation",
|
|
1005
1605
|
);
|
|
1006
1606
|
}
|
|
@@ -1068,21 +1668,6 @@ async function persistOpenGeniSlackBotConnection(input: {
|
|
|
1068
1668
|
}
|
|
1069
1669
|
}
|
|
1070
1670
|
|
|
1071
|
-
function requireOpenGeniSlackOAuthSettings(settings: ApiRouteDeps["settings"]): {
|
|
1072
|
-
clientId: string;
|
|
1073
|
-
clientSecret: string;
|
|
1074
|
-
} {
|
|
1075
|
-
const clientId = settings.slackClientId?.trim();
|
|
1076
|
-
const clientSecret = settings.slackClientSecret?.trim();
|
|
1077
|
-
if (!clientId || !clientSecret) {
|
|
1078
|
-
throw new HTTPException(503, {
|
|
1079
|
-
message:
|
|
1080
|
-
"OpenGeni Slack installation requires OPENGENI_SLACK_CLIENT_ID and OPENGENI_SLACK_CLIENT_SECRET",
|
|
1081
|
-
});
|
|
1082
|
-
}
|
|
1083
|
-
return { clientId, clientSecret };
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
1671
|
function readOpenGeniSlackInstallState(
|
|
1087
1672
|
rawState: string | undefined,
|
|
1088
1673
|
settings: ApiRouteDeps["settings"],
|
|
@@ -1097,6 +1682,11 @@ function readOpenGeniSlackInstallState(
|
|
|
1097
1682
|
if (!payload) {
|
|
1098
1683
|
throw new HTTPException(400, { message: "invalid or expired Slack installation state" });
|
|
1099
1684
|
}
|
|
1685
|
+
if (
|
|
1686
|
+
(payload.kind !== undefined && payload.kind !== "slack_bot_install") ||
|
|
1687
|
+
(payload.connectAttemptId && payload.kind !== "slack_bot_install")
|
|
1688
|
+
)
|
|
1689
|
+
throw new HTTPException(400, { message: "invalid Slack installation state kind" });
|
|
1100
1690
|
const requiredString = (value: unknown, label: string): string => {
|
|
1101
1691
|
if (typeof value !== "string" || value.length === 0) {
|
|
1102
1692
|
throw new HTTPException(400, { message: `invalid Slack installation ${label}` });
|
|
@@ -1131,6 +1721,21 @@ function readOpenGeniSlackInstallState(
|
|
|
1131
1721
|
workspaceId,
|
|
1132
1722
|
subjectId,
|
|
1133
1723
|
returnPath,
|
|
1724
|
+
...(typeof payload.connectAttemptId === "string"
|
|
1725
|
+
? { connectAttemptId: payload.connectAttemptId }
|
|
1726
|
+
: {}),
|
|
1727
|
+
...(typeof payload.encryptedExternalContinuation === "string"
|
|
1728
|
+
? {
|
|
1729
|
+
externalContinuation: ExternalActorContinuation.parse(
|
|
1730
|
+
JSON.parse(
|
|
1731
|
+
decryptEnvironmentValue(
|
|
1732
|
+
requireEnvironmentEncryption(settings),
|
|
1733
|
+
payload.encryptedExternalContinuation,
|
|
1734
|
+
),
|
|
1735
|
+
),
|
|
1736
|
+
),
|
|
1737
|
+
}
|
|
1738
|
+
: {}),
|
|
1134
1739
|
...(connectionId ? { connectionId, connectionVersion: connectionVersion! } : {}),
|
|
1135
1740
|
nonce: requiredString(payload.nonce, "nonce"),
|
|
1136
1741
|
iat:
|
|
@@ -1182,6 +1787,9 @@ function slackInstallCallbackFailure(
|
|
|
1182
1787
|
if (error instanceof SlackBotLifecycleSuccessAuditError) {
|
|
1183
1788
|
return { stage: "persistence", reason: "success_audit_failed" };
|
|
1184
1789
|
}
|
|
1790
|
+
if (stage === "permission_check" && error instanceof HTTPException && error.status === 403) {
|
|
1791
|
+
return { stage, reason: "permission_lost" };
|
|
1792
|
+
}
|
|
1185
1793
|
if (stage === "code_exchange") {
|
|
1186
1794
|
return { stage, reason: "exchange_failed" };
|
|
1187
1795
|
}
|
|
@@ -1205,18 +1813,7 @@ async function requireSlackInstallCallbackGrant(
|
|
|
1205
1813
|
db: ApiRouteDeps["db"],
|
|
1206
1814
|
state: OpenGeniSlackInstallState,
|
|
1207
1815
|
): Promise<void> {
|
|
1208
|
-
|
|
1209
|
-
if (
|
|
1210
|
-
!grant ||
|
|
1211
|
-
grant.accountId !== state.accountId ||
|
|
1212
|
-
!hasPermission(grant.permissions, "connections:write")
|
|
1213
|
-
) {
|
|
1214
|
-
throw new SlackInstallCallbackError(
|
|
1215
|
-
403,
|
|
1216
|
-
"permission_lost",
|
|
1217
|
-
"Slack installation subject no longer has permission for this workspace",
|
|
1218
|
-
);
|
|
1219
|
-
}
|
|
1816
|
+
await requireConnectOwnerAuthority(db, state, "connections:write");
|
|
1220
1817
|
}
|
|
1221
1818
|
|
|
1222
1819
|
function assertNotDirectPersonalSlackOAuth(providerDomain: string, kind: string): void {
|
|
@@ -1257,10 +1854,78 @@ function assertNotDirectAtlassianOAuth(
|
|
|
1257
1854
|
}
|
|
1258
1855
|
}
|
|
1259
1856
|
|
|
1857
|
+
function assertNotDirectPersonalGitHubOAuth(
|
|
1858
|
+
providerDomain: string,
|
|
1859
|
+
kind: string,
|
|
1860
|
+
metadata: Record<string, unknown> | undefined,
|
|
1861
|
+
): void {
|
|
1862
|
+
if (
|
|
1863
|
+
(providerDomain === PERSONAL_GITHUB_PROVIDER_DOMAIN && kind === "oauth2") ||
|
|
1864
|
+
hasReservedPersonalGitHubMetadata(metadata)
|
|
1865
|
+
) {
|
|
1866
|
+
throw new HTTPException(422, {
|
|
1867
|
+
message: "personal GitHub credentials must use the dedicated OAuth connection flow",
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
function assertNotReservedPersonalGitHubMetadata(
|
|
1873
|
+
metadata: Record<string, unknown> | undefined,
|
|
1874
|
+
): void {
|
|
1875
|
+
if (hasReservedPersonalGitHubMetadata(metadata)) {
|
|
1876
|
+
throw new HTTPException(422, {
|
|
1877
|
+
message: "personal GitHub metadata is reserved for the dedicated OAuth connection flow",
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
function workspaceProviderApiKeyConnectionKind(input: {
|
|
1883
|
+
subjectId?: string | null;
|
|
1884
|
+
providerDomain: string;
|
|
1885
|
+
kind: string;
|
|
1886
|
+
metadata?: Record<string, unknown>;
|
|
1887
|
+
}): WorkspaceProviderApiKeyConnectionKind | null {
|
|
1888
|
+
if (input.subjectId != null || input.kind !== "api_key") return null;
|
|
1889
|
+
const providerDomain = input.providerDomain.toLowerCase();
|
|
1890
|
+
if (
|
|
1891
|
+
providerDomain === "api.anthropic.com" &&
|
|
1892
|
+
(input.metadata?.credentialRole === "anthropic" ||
|
|
1893
|
+
input.metadata?.credentialRole === "claude_subscription")
|
|
1894
|
+
)
|
|
1895
|
+
return input.metadata.credentialRole;
|
|
1896
|
+
if (
|
|
1897
|
+
providerDomain === VERCEL_AI_GATEWAY_CONNECTION_DOMAIN &&
|
|
1898
|
+
input.metadata?.credentialRole === VERCEL_AI_GATEWAY_CONNECTION_ROLE
|
|
1899
|
+
) {
|
|
1900
|
+
return "vercel_gateway";
|
|
1901
|
+
}
|
|
1902
|
+
if (
|
|
1903
|
+
providerDomain === WORKSPACE_OPENROUTER_CONNECTION_DOMAIN &&
|
|
1904
|
+
input.metadata?.credentialRole === WORKSPACE_OPENROUTER_CONNECTION_ROLE
|
|
1905
|
+
) {
|
|
1906
|
+
return "openrouter";
|
|
1907
|
+
}
|
|
1908
|
+
return null;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
/**
|
|
1912
|
+
* A brokered api_key Connection is usable only through `headers` or
|
|
1913
|
+
* `placements`; anything else was previously accepted and then failed every
|
|
1914
|
+
* tool call with a misleading "connect an account" auth-needed notice.
|
|
1915
|
+
*/
|
|
1916
|
+
function assertBrokeredApiKeyCredential(
|
|
1917
|
+
kind: string | undefined,
|
|
1918
|
+
credential: Record<string, unknown>,
|
|
1919
|
+
): void {
|
|
1920
|
+
if (kind !== "api_key") return;
|
|
1921
|
+
const problem = brokeredCredentialBundleProblem(credential);
|
|
1922
|
+
if (problem) throw new HTTPException(422, { message: problem });
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1260
1925
|
function assertNotReservedSlackBotMetadata(metadata: Record<string, unknown> | undefined): void {
|
|
1261
1926
|
if (hasReservedOpenGeniSlackBotMetadata(metadata)) {
|
|
1262
1927
|
throw new HTTPException(422, {
|
|
1263
|
-
message: "
|
|
1928
|
+
message: "Opengeni Slack bot metadata is reserved for the dedicated connection flow",
|
|
1264
1929
|
});
|
|
1265
1930
|
}
|
|
1266
1931
|
}
|
|
@@ -1316,6 +1981,31 @@ function encryptCredentialBundle(key: Uint8Array, credential: Record<string, unk
|
|
|
1316
1981
|
return encryptEnvironmentValue(key, JSON.stringify(credential));
|
|
1317
1982
|
}
|
|
1318
1983
|
|
|
1984
|
+
function workspaceProviderCredentialMetadata(
|
|
1985
|
+
providerKind: WorkspaceProviderApiKeyConnectionKind,
|
|
1986
|
+
metadata: Record<string, unknown> | undefined,
|
|
1987
|
+
): Record<string, unknown> {
|
|
1988
|
+
const {
|
|
1989
|
+
[OPENROUTER_CREDENTIAL_OPERATION_ID_METADATA_KEY]: _openRouterOperationId,
|
|
1990
|
+
[OPENROUTER_CREDENTIAL_OPERATION_DIGEST_METADATA_KEY]: _openRouterOperationDigest,
|
|
1991
|
+
[VERCEL_AI_GATEWAY_CREDENTIAL_OPERATION_ID_METADATA_KEY]: _operationId,
|
|
1992
|
+
[VERCEL_AI_GATEWAY_CREDENTIAL_OPERATION_DIGEST_METADATA_KEY]: _operationDigest,
|
|
1993
|
+
...effectiveMetadata
|
|
1994
|
+
} = metadata ?? {};
|
|
1995
|
+
for (const role of ["anthropic", "claude_subscription"]) {
|
|
1996
|
+
delete effectiveMetadata[`${role}CredentialOperationId`];
|
|
1997
|
+
delete effectiveMetadata[`${role}CredentialOperationDigest`];
|
|
1998
|
+
}
|
|
1999
|
+
return {
|
|
2000
|
+
...effectiveMetadata,
|
|
2001
|
+
credentialRole: workspaceProviderApiKeyConnectionSpec(providerKind).credentialRole,
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
function workspaceProviderCredentialRequestDigest(key: Uint8Array, value: unknown): string {
|
|
2006
|
+
return createHmac("sha256", key).update(stableJson(value), "utf8").digest("hex");
|
|
2007
|
+
}
|
|
2008
|
+
|
|
1319
2009
|
function slackBotCredentialBundle(token: string): Record<string, unknown> {
|
|
1320
2010
|
const headerName = ["author", "ization"].join("");
|
|
1321
2011
|
const scheme = ["Bear", "er"].join("");
|