@opengeni/api-router 3.0.1 → 3.0.2-canary.38894290271001
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-grant-rls.d.ts +8 -1
- package/dist/admission-trace.d.ts +8 -0
- package/dist/app.d.ts +50 -3
- package/dist/app.js +10 -1
- package/dist/artifact-catalog-cursor.d.ts +1 -0
- package/dist/attached-browser-viewer.d.ts +11 -0
- package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
- package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +9 -1
- package/dist/auth/managed-auth.d.ts +72 -5
- package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
- package/dist/auth/new-signups-gate.d.ts +23 -0
- package/dist/auth/signup-funnel-metrics.d.ts +44 -0
- package/dist/azure-live.d.ts +28 -0
- package/dist/browser-action-fences.d.ts +13 -0
- package/dist/browser-controller-authority.d.ts +1 -61
- package/dist/browser-ephemeral.d.ts +5 -0
- package/dist/browser-lost-runtime-end.d.ts +24 -0
- package/dist/browser-state-authority.d.ts +1 -27
- package/dist/{chunk-AJ62T5QN.js → chunk-GBUJ2DYZ.js} +80507 -39878
- package/dist/chunk-GBUJ2DYZ.js.map +1 -0
- package/dist/chunk-OGQDXVXY.js +18 -0
- package/dist/chunk-OGQDXVXY.js.map +1 -0
- package/dist/claude-default-models.d.ts +19 -0
- package/dist/claude-subscription-core.d.ts +50 -0
- package/dist/claude-subscription-oauth.d.ts +36 -0
- package/dist/claude-subscription-profile.d.ts +9 -0
- package/dist/claude-subscription-usage.d.ts +6 -0
- package/dist/claude-workspace-connection.d.ts +15 -0
- package/dist/codemode.d.ts +40 -5
- package/dist/codex-realtime.d.ts +29 -3
- package/dist/codex-session-pointers.d.ts +19 -0
- package/dist/connection-ownership.d.ts +6 -3
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/credit-promotion-snapshot.d.ts +6 -0
- package/dist/editable-artifact-source-session.d.ts +3 -0
- package/dist/editable-artifact-websocket.d.ts +5 -3
- package/dist/fatal-process-boundary.d.ts +9 -1
- package/dist/github-access.d.ts +7 -1
- package/dist/github-browser-flow.d.ts +1 -1
- package/dist/http/acting-person.d.ts +36 -0
- package/dist/http/api-contract-compat.d.ts +19 -0
- package/dist/http/api-error.d.ts +50 -1
- package/dist/http/auth.d.ts +8 -1
- package/dist/http/cors.d.ts +12 -0
- package/dist/http/deprecation.d.ts +42 -0
- package/dist/http/keyed-admission.d.ts +12 -0
- package/dist/http/local-browser-boundary.d.ts +72 -0
- package/dist/http/metrics-listener.d.ts +22 -0
- package/dist/http/path-identifier.d.ts +17 -0
- package/dist/http/registered-route-label.d.ts +16 -0
- package/dist/http/rejection-telemetry.d.ts +34 -0
- package/dist/http/request-body.d.ts +27 -0
- package/dist/http/request-source.d.ts +41 -3
- package/dist/http/sse.d.ts +12 -1
- package/dist/http/unmatched-route.d.ts +18 -0
- package/dist/http/user-content.d.ts +36 -0
- package/dist/http/variable-sets-removed.d.ts +41 -0
- package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
- package/dist/index.js +1279 -108
- package/dist/index.js.map +1 -1
- package/dist/integration-connect-metrics.d.ts +35 -0
- package/dist/integration-endpoint-test.d.ts +20 -0
- package/dist/integrations/api-integrations.d.ts +2 -2
- package/dist/integrations/atlassian.d.ts +8 -3
- package/dist/integrations/fiken.d.ts +3 -1
- package/dist/integrations/github-lens-connect.d.ts +37 -66
- package/dist/integrations/google-drive.d.ts +2 -0
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/marketplace-plugin.d.ts +43 -43
- package/dist/integrations/oauth-client.d.ts +171 -12
- package/dist/integrations/oauth-profiles.d.ts +42 -13
- package/dist/integrations/onepassword-vault-provider.d.ts +9 -0
- package/dist/integrations/slack-bot.d.ts +285 -7
- package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
- package/dist/integrations/slack-interactions.d.ts +89 -10
- package/dist/integrations/slack-mrkdwn.d.ts +41 -0
- package/dist/integrations/slack-routing.d.ts +1 -1
- package/dist/integrations/slack-session-defaults.d.ts +58 -0
- package/dist/integrations/slack-task-file-upload.d.ts +36 -0
- package/dist/integrations/slack-tool-review.d.ts +5 -0
- package/dist/interaction-agent-access.d.ts +13 -8
- package/dist/interaction-agent-version.d.ts +21 -0
- package/dist/interaction-holder-heartbeat.d.ts +5 -0
- package/dist/interaction-metrics.d.ts +27 -2
- package/dist/interaction-route-stages.d.ts +25 -0
- package/dist/interaction-secrets-vault.d.ts +20 -0
- package/dist/interaction-secrets.d.ts +103 -0
- package/dist/mcp/action-catalog-types.d.ts +14 -0
- package/dist/mcp/action-catalog.gen.d.ts +2 -0
- package/dist/mcp/action-descriptions.d.ts +4 -0
- package/dist/mcp/action-schema-imports.gen.d.ts +1 -0
- package/dist/mcp/capability-account-readiness.d.ts +7 -0
- package/dist/mcp/child-read-evidence.d.ts +9 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +12 -0
- package/dist/mcp/contract-input.d.ts +13 -0
- package/dist/mcp/documents.d.ts +2 -0
- package/dist/mcp/first-party-tool-permissions.d.ts +3 -606
- package/dist/mcp/notification-tools.d.ts +26 -0
- package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
- package/dist/mcp/scheduled-task-input.d.ts +1309 -0
- package/dist/mcp/scheduled-task-view.d.ts +217 -3
- package/dist/mcp/secret-tools.d.ts +14 -0
- package/dist/mcp/server.d.ts +108 -5
- package/dist/mcp/session-admin-access-tools.d.ts +33 -0
- package/dist/mcp/session-event-errors.d.ts +28 -0
- package/dist/mcp/session-event-view.d.ts +62 -0
- package/dist/mcp/session-view.d.ts +15 -0
- package/dist/mcp/session-wait.d.ts +18 -0
- package/dist/mcp/site-agent-clamp-tools.d.ts +38 -0
- package/dist/mcp/site-data-tools.d.ts +22 -0
- package/dist/mcp/site-publication-tools.d.ts +19 -0
- package/dist/mcp/site-sql-tools.d.ts +27 -0
- package/dist/mcp-oauth.d.ts +37 -4
- package/dist/model-calls/chat-completions.d.ts +87 -0
- package/dist/model-calls/pending-usage-settlements.d.ts +57 -0
- package/dist/model-calls/service.d.ts +36 -0
- package/dist/model-catalog.d.ts +9 -3
- package/dist/native-push-dispatch.d.ts +69 -0
- package/dist/observability.d.ts +3 -1
- package/dist/organization-mcp.d.ts +82 -0
- package/dist/prepared-mcp-actor.d.ts +77 -0
- package/dist/prepared-mcp-permissions.d.ts +8 -0
- package/dist/routes/analytics-consent.d.ts +26 -0
- package/dist/routes/api-integrations.d.ts +11 -0
- package/dist/routes/api-keys.d.ts +7 -7
- package/dist/routes/automations.d.ts +2 -0
- package/dist/routes/billing.d.ts +59 -0
- package/dist/routes/browser-sessions.d.ts +65 -2
- package/dist/routes/chat-completions.d.ts +11 -0
- package/dist/routes/claude-core-usage.d.ts +20 -0
- package/dist/routes/claude-subscription-accounts.d.ts +3 -0
- package/dist/routes/claude-subscription-oauth.d.ts +3 -0
- package/dist/routes/client-errors.d.ts +182 -0
- package/dist/routes/codex-core.d.ts +401 -0
- package/dist/routes/codex.d.ts +64 -2
- package/dist/routes/computer-sessions.d.ts +6 -1
- package/dist/routes/desktop-distribution.d.ts +32 -0
- package/dist/routes/github-browser-fonts.gen.d.ts +2 -0
- package/dist/routes/github-browser-pages.d.ts +27 -0
- package/dist/routes/inbox.d.ts +13 -0
- package/dist/routes/insights-response-cache.d.ts +10 -0
- package/dist/routes/insights-usage.d.ts +12 -0
- package/dist/routes/insights.d.ts +28 -0
- package/dist/routes/interaction-placement-message.d.ts +7 -0
- package/dist/routes/managed-sign-in-methods.d.ts +6 -0
- package/dist/routes/model-account-choice.d.ts +13 -0
- package/dist/routes/model-account-settings.d.ts +19 -0
- package/dist/routes/native-app-auth.d.ts +12 -0
- package/dist/routes/organization-integration-policy.d.ts +8 -0
- package/dist/routes/organization-integrations.d.ts +6 -0
- package/dist/routes/organization-mcp-connections.d.ts +3 -0
- package/dist/routes/organization-model-defaults.d.ts +9 -0
- package/dist/routes/organization-service-accounts.d.ts +3 -0
- package/dist/routes/personal-api-keys.d.ts +3 -0
- package/dist/routes/session-admin-access.d.ts +3 -0
- package/dist/routes/session-artifact-associations.d.ts +4 -0
- package/dist/routes/session-history-imports.d.ts +20 -0
- package/dist/routes/sessions.d.ts +38 -2
- package/dist/routes/site-data.d.ts +18 -0
- package/dist/routes/site-publishing.d.ts +5 -0
- package/dist/routes/site-runtime.d.ts +31 -0
- package/dist/routes/site-sql.d.ts +16 -0
- package/dist/routes/slack-bot-access.d.ts +3 -0
- package/dist/routes/subscription-account-pools.d.ts +21 -0
- package/dist/routes/subscription-core-http.d.ts +5 -0
- package/dist/routes/subscription-pool-access.d.ts +37 -0
- package/dist/routes/subscription-pool-core.d.ts +153 -0
- package/dist/routes/supergrok.d.ts +6 -6
- package/dist/routes/usage-allowances.d.ts +12 -0
- package/dist/routes/variable-sets-removed.d.ts +2 -0
- package/dist/routes/vault-providers.d.ts +34 -0
- package/dist/routes/vaults.d.ts +3 -0
- package/dist/routes/workspace-integrations.d.ts +35 -0
- package/dist/routes/workspace-model-providers.d.ts +4 -0
- package/dist/routes/workspaces.d.ts +11 -2
- package/dist/sandbox/agent-update.d.ts +64 -0
- package/dist/sandbox/auth-callout.d.ts +8 -0
- package/dist/sandbox/channel-a.d.ts +15 -1
- package/dist/sandbox/enrollment.d.ts +26 -4
- package/dist/sandbox/machine-audience.d.ts +29 -0
- package/dist/sandbox/machines.d.ts +15 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -2
- package/dist/sandbox/screen-control.d.ts +85 -0
- package/dist/sandbox/viewer.d.ts +5 -1
- package/dist/sandbox-file-artifacts.d.ts +17 -1
- package/dist/scheduled-task-blockers.d.ts +39 -0
- package/dist/scheduled-task-deletion.d.ts +3 -4
- package/dist/scheduled-task-lifecycle.d.ts +14 -0
- package/dist/secret-answers.d.ts +40 -0
- package/dist/session-create-observability.d.ts +5 -0
- package/dist/session-event-view-36GO3IL7.js +528 -0
- package/dist/session-event-view-36GO3IL7.js.map +1 -0
- package/dist/signed-state-secret.d.ts +18 -0
- package/dist/site-agent-guard.d.ts +53 -0
- package/dist/site-data/operations.d.ts +103 -0
- package/dist/site-data/service.d.ts +203 -0
- package/dist/site-frame-guard.d.ts +32 -0
- package/dist/site-publishing/agent-data-tools.d.ts +84 -0
- package/dist/site-publishing/agent-model.d.ts +58 -0
- package/dist/site-publishing/agent-session.d.ts +16 -0
- package/dist/site-publishing/content-scanner.d.ts +31 -0
- package/dist/site-publishing/edge-sync.d.ts +48 -0
- package/dist/site-publishing/gateway-signature.d.ts +56 -0
- package/dist/site-publishing/limits.d.ts +63 -0
- package/dist/site-publishing/page-tool-authority.d.ts +30 -0
- package/dist/site-publishing/platform-usage.d.ts +113 -0
- package/dist/site-publishing/platform.d.ts +21 -0
- package/dist/site-publishing/readiness.d.ts +62 -0
- package/dist/site-publishing/runtime.d.ts +179 -0
- package/dist/site-publishing/service.d.ts +264 -0
- package/dist/site-publishing/session-events.d.ts +22 -0
- package/dist/site-publishing/session-views.d.ts +36 -0
- package/dist/site-publishing/viewer.d.ts +40 -0
- package/dist/site-session-origin.d.ts +31 -6
- package/dist/site-sql/format.d.ts +47 -0
- package/dist/site-sql/gateway.d.ts +50 -0
- package/dist/site-sql/service.d.ts +213 -0
- package/dist/temporal-schedule-sync.d.ts +26 -0
- package/dist/transcription/billing-refusal.d.ts +8 -0
- package/dist/transcription/normalize.d.ts +23 -0
- package/dist/transcription/providers/azure-mai.d.ts +12 -0
- package/dist/transcription/providers/azure-openai.d.ts +4 -0
- package/dist/transcription/providers/openai.d.ts +16 -0
- package/dist/transcription/providers/subscription-core.d.ts +39 -0
- package/dist/transcription/service.d.ts +10 -1
- package/dist/transcription/wav-duration.d.ts +8 -0
- package/dist/workspace-tool-gateway-attestations.d.ts +37 -0
- package/dist/workspace-tool-gateway-observability.d.ts +16 -0
- package/dist/workspace-tool-gateway-secrets.d.ts +27 -0
- package/dist/workspace-tool-gateway.d.ts +102 -6
- package/dist/workspace-tool-target-fence.d.ts +116 -0
- package/dist/workspace-tool-target.d.ts +225 -0
- package/dist/workspace-webhook-dispatch.d.ts +26 -0
- package/dist/xai-subscription-core.d.ts +60 -0
- package/package.json +25 -20
- package/src/access-grant-rls.ts +64 -14
- package/src/admission-trace.ts +14 -0
- package/src/app.ts +1684 -252
- package/src/artifact-catalog-cursor.ts +1 -0
- package/src/attached-browser-viewer.ts +26 -0
- package/src/auth/managed-auth-attempt-context.ts +9 -0
- package/src/auth/managed-auth-rate-limits.ts +252 -0
- package/src/auth/managed-auth-session-adapter.ts +64 -29
- package/src/auth/managed-auth.ts +303 -35
- package/src/auth/managed-email.ts +18 -1
- package/src/auth/managed-sign-in-notifications.ts +81 -0
- package/src/auth/new-signups-gate.ts +49 -0
- package/src/auth/organization-user-setup.ts +3 -3
- package/src/auth/signup-funnel-metrics.ts +164 -0
- package/src/azure-live.ts +103 -0
- package/src/browser-action-fences.ts +45 -0
- package/src/browser-controller-authority.ts +1 -186
- package/src/browser-ephemeral.ts +39 -0
- package/src/browser-lost-runtime-end.ts +71 -0
- package/src/browser-state-authority.ts +1 -236
- package/src/claude-default-models.ts +37 -0
- package/src/claude-subscription-core.ts +154 -0
- package/src/claude-subscription-oauth.ts +352 -0
- package/src/claude-subscription-profile.ts +73 -0
- package/src/claude-subscription-usage.ts +51 -0
- package/src/claude-workspace-connection.ts +82 -0
- package/src/codemode.ts +236 -39
- package/src/codex-realtime.ts +181 -53
- package/src/codex-session-pointers.ts +80 -0
- package/src/company-brain-okf.ts +1 -1
- package/src/connected-machine-computer-access.ts +2 -1
- package/src/connection-ownership.ts +19 -13
- package/src/controller-data-plane.ts +34 -0
- package/src/credit-promotion-snapshot.ts +36 -0
- package/src/editable-artifact-production.ts +10 -0
- package/src/editable-artifact-source-session.ts +123 -0
- package/src/editable-artifact-websocket.ts +16 -3
- package/src/fatal-process-boundary.ts +67 -3
- package/src/gateway-realtime.ts +12 -8
- package/src/github-access.ts +31 -5
- package/src/github-browser-flow.ts +1 -1
- package/src/http/acting-person.ts +105 -0
- package/src/http/api-contract-compat.ts +49 -0
- package/src/http/api-error.ts +210 -2
- package/src/http/auth.ts +68 -4
- package/src/http/cors.ts +37 -5
- package/src/http/deprecation.ts +118 -0
- package/src/http/keyed-admission.ts +29 -0
- package/src/http/local-browser-boundary.ts +343 -0
- package/src/http/metrics-listener.ts +57 -0
- package/src/http/path-identifier.ts +54 -0
- package/src/http/registered-route-label.ts +34 -0
- package/src/http/rejection-telemetry.ts +120 -0
- package/src/http/request-body.ts +127 -0
- package/src/http/request-source.ts +154 -13
- package/src/http/sse.ts +88 -28
- package/src/http/unmatched-route.ts +60 -0
- package/src/http/user-content.ts +158 -0
- package/src/http/variable-sets-removed.ts +122 -0
- package/src/http/websocket-upgrade-dispatch.ts +29 -0
- package/src/index.ts +107 -57
- package/src/integration-connect-metrics.ts +228 -0
- package/src/integration-endpoint-test.ts +291 -0
- package/src/integrations/api-integrations.ts +81 -7
- package/src/integrations/atlassian.ts +202 -83
- package/src/integrations/fiken.ts +66 -38
- package/src/integrations/github-app-connect.ts +64 -18
- package/src/integrations/github-lens-connect.ts +8 -29
- package/src/integrations/google-drive.ts +448 -152
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/marketplace-plugin.ts +1 -1
- package/src/integrations/oauth-client.ts +733 -193
- package/src/integrations/oauth-profiles.ts +116 -45
- package/src/integrations/onepassword-vault-provider.ts +16 -0
- package/src/integrations/personal-github-repositories.ts +1 -1
- package/src/integrations/personal-github.ts +61 -35
- package/src/integrations/provider-oauth.ts +67 -85
- package/src/integrations/slack-app-home.ts +5 -5
- package/src/integrations/slack-bot.ts +893 -201
- package/src/integrations/slack-file-upload-flow.ts +147 -0
- package/src/integrations/slack-install.ts +7 -7
- package/src/integrations/slack-interactions.ts +1589 -299
- package/src/integrations/slack-mrkdwn.ts +193 -0
- package/src/integrations/slack-routing.ts +2 -2
- package/src/integrations/slack-session-defaults.ts +224 -0
- package/src/integrations/slack-task-file-upload.ts +323 -0
- package/src/integrations/slack-tool-review.ts +65 -0
- package/src/integrations/social-oauth.ts +90 -58
- package/src/interaction-agent-access.ts +40 -20
- package/src/interaction-agent-version.ts +85 -0
- package/src/interaction-holder-heartbeat.ts +24 -8
- package/src/interaction-metrics.ts +164 -2
- package/src/interaction-route-stages.ts +94 -0
- package/src/interaction-secrets-vault.ts +213 -0
- package/src/interaction-secrets.ts +426 -0
- package/src/mcp/action-catalog-types.ts +14 -0
- package/src/mcp/action-catalog.gen.ts +8014 -0
- package/src/mcp/action-descriptions.ts +299 -0
- package/src/mcp/action-schema-imports.gen.ts +304 -0
- package/src/mcp/capability-account-readiness.ts +17 -0
- package/src/mcp/child-read-evidence.ts +19 -0
- package/src/mcp/company-profile-agent-admin.ts +13 -0
- package/src/mcp/contract-input.ts +165 -0
- package/src/mcp/documents.ts +9 -2
- package/src/mcp/editable-artifacts.ts +29 -11
- package/src/mcp/files.ts +5 -1
- package/src/mcp/first-party-tool-permissions.ts +7 -285
- package/src/mcp/knowledge-entries.ts +108 -34
- package/src/mcp/notification-tools.ts +480 -0
- package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
- package/src/mcp/scheduled-task-input.ts +89 -0
- package/src/mcp/scheduled-task-view.ts +63 -3
- package/src/mcp/secret-tools.ts +185 -0
- package/src/mcp/server.ts +2213 -1116
- package/src/mcp/session-admin-access-tools.ts +163 -0
- package/src/mcp/session-event-errors.ts +123 -0
- package/src/mcp/session-event-view.ts +395 -13
- package/src/mcp/session-view.ts +3 -0
- package/src/mcp/session-wait.ts +67 -7
- package/src/mcp/site-agent-clamp-tools.ts +140 -0
- package/src/mcp/site-data-tools.ts +339 -0
- package/src/mcp/site-publication-tools.ts +201 -0
- package/src/mcp/site-sql-tools.ts +427 -0
- package/src/mcp-oauth.ts +635 -45
- package/src/model-calls/chat-completions.ts +352 -0
- package/src/model-calls/pending-usage-settlements.ts +125 -0
- package/src/model-calls/service.ts +694 -0
- package/src/model-catalog.ts +68 -17
- package/src/native-push-dispatch.ts +457 -0
- package/src/observability.ts +15 -1
- package/src/organization-mcp.ts +485 -0
- package/src/prepared-mcp-actor.ts +154 -0
- package/src/prepared-mcp-permissions.ts +38 -0
- package/src/routes/analytics-consent.ts +123 -0
- package/src/routes/api-integrations.ts +268 -9
- package/src/routes/api-keys.ts +164 -25
- package/src/routes/artifact-catalog.ts +111 -0
- package/src/routes/automations.ts +163 -90
- package/src/routes/billing.ts +535 -59
- package/src/routes/browser-sessions.ts +966 -342
- package/src/routes/canonical-human-identities.ts +61 -0
- package/src/routes/capabilities.ts +112 -5
- package/src/routes/channels.ts +4 -3
- package/src/routes/chat-completions.ts +235 -0
- package/src/routes/claude-core-usage.ts +147 -0
- package/src/routes/claude-subscription-accounts.ts +233 -0
- package/src/routes/claude-subscription-oauth.ts +175 -0
- package/src/routes/client-errors.ts +394 -0
- package/src/routes/codex-core.ts +687 -0
- package/src/routes/codex.ts +1277 -1119
- package/src/routes/company-brain.ts +4 -0
- package/src/routes/computer-sessions.ts +319 -85
- package/src/routes/connect.ts +277 -175
- package/src/routes/connections.ts +690 -363
- package/src/routes/desktop-distribution.ts +217 -0
- package/src/routes/documents.ts +13 -7
- package/src/routes/editable-artifacts.ts +74 -4
- package/src/routes/enrollments.ts +110 -16
- package/src/routes/external-identity-links.ts +13 -12
- package/src/routes/files.ts +41 -16
- package/src/routes/github-browser-fonts.gen.ts +12 -0
- package/src/routes/github-browser-pages.ts +258 -0
- package/src/routes/github.ts +200 -75
- package/src/routes/inbox.ts +353 -0
- package/src/routes/insights-response-cache.ts +83 -0
- package/src/routes/insights-usage.ts +345 -0
- package/src/routes/insights.ts +103 -8
- package/src/routes/install.ts +51 -10
- package/src/routes/integration-facets.ts +70 -28
- package/src/routes/interaction-placement-message.ts +12 -0
- package/src/routes/knowledge.ts +136 -27
- package/src/routes/machines.ts +189 -135
- package/src/routes/managed-auth-session-sets.ts +43 -10
- package/src/routes/managed-onboarding.ts +39 -19
- package/src/routes/managed-sign-in-methods.ts +433 -0
- package/src/routes/memory-slack-publications.ts +3 -2
- package/src/routes/model-account-choice.ts +194 -0
- package/src/routes/model-account-settings.ts +235 -0
- package/src/routes/model-connection-access.ts +210 -48
- package/src/routes/native-app-auth.ts +296 -0
- package/src/routes/organization-integration-policy.ts +116 -0
- package/src/routes/organization-integrations.ts +326 -0
- package/src/routes/organization-mcp-connections.ts +183 -0
- package/src/routes/organization-memberships.ts +205 -19
- package/src/routes/organization-model-defaults.ts +110 -0
- package/src/routes/organization-model-providers.ts +74 -3
- package/src/routes/organization-service-accounts.ts +128 -0
- package/src/routes/organization-sessions.ts +21 -5
- package/src/routes/personal-api-keys.ts +184 -0
- package/src/routes/personal-github-git-broker.ts +2 -2
- package/src/routes/personal-github.ts +9 -4
- package/src/routes/plugins.ts +46 -7
- package/src/routes/pr-review-github.ts +105 -118
- package/src/routes/pr-review.ts +88 -82
- package/src/routes/preference-registry.ts +18 -0
- package/src/routes/rigs.ts +48 -13
- package/src/routes/scheduled-tasks.ts +347 -27
- package/src/routes/session-admin-access.ts +271 -0
- package/src/routes/session-artifact-associations.ts +164 -0
- package/src/routes/session-history-imports.ts +305 -0
- package/src/routes/sessions.ts +1682 -363
- package/src/routes/site-data.ts +340 -0
- package/src/routes/site-publishing.ts +584 -0
- package/src/routes/site-runtime.ts +1944 -0
- package/src/routes/site-sql.ts +263 -0
- package/src/routes/skill-content.ts +72 -7
- package/src/routes/skills.ts +5 -4
- package/src/routes/slack-bot-access.ts +108 -0
- package/src/routes/social.ts +27 -15
- package/src/routes/subscription-account-pools.ts +209 -0
- package/src/routes/subscription-core-http.ts +18 -0
- package/src/routes/subscription-pool-access.ts +180 -0
- package/src/routes/subscription-pool-core.ts +552 -0
- package/src/routes/supergrok.ts +174 -655
- package/src/routes/transcription-recordings.ts +71 -4
- package/src/routes/transcriptions.ts +26 -2
- package/src/routes/usage-allowances.ts +381 -0
- package/src/routes/variable-sets-removed.ts +19 -0
- package/src/routes/vault-providers.ts +782 -0
- package/src/routes/vaults.ts +421 -0
- package/src/routes/video-generation.ts +30 -12
- package/src/routes/workspace-artifacts.ts +165 -53
- package/src/routes/workspace-integrations.ts +576 -0
- package/src/routes/workspace-model-providers.ts +148 -0
- package/src/routes/workspace-state.ts +4 -0
- package/src/routes/workspaces.ts +625 -117
- package/src/sandbox/agent-update.ts +315 -0
- package/src/sandbox/auth-callout.ts +104 -21
- package/src/sandbox/channel-a.ts +150 -49
- package/src/sandbox/enrollment.ts +131 -22
- package/src/sandbox/machine-audience.ts +192 -0
- package/src/sandbox/machines.ts +75 -31
- package/src/sandbox/metrics-ingestion.ts +133 -2
- package/src/sandbox/rematerialize.ts +51 -21
- package/src/sandbox/screen-control.ts +289 -0
- package/src/sandbox/viewer.ts +33 -58
- package/src/sandbox-file-artifacts.ts +53 -27
- package/src/scheduled-task-blockers.ts +293 -0
- package/src/scheduled-task-deletion.ts +13 -3
- package/src/scheduled-task-lifecycle.ts +84 -0
- package/src/secret-answers.ts +119 -0
- package/src/session-create-observability.ts +46 -0
- package/src/session-realtime-context.ts +26 -4
- package/src/signed-state-secret.ts +58 -0
- package/src/site-agent-guard.ts +261 -0
- package/src/site-data/operations.ts +777 -0
- package/src/site-data/service.ts +1043 -0
- package/src/site-frame-guard.ts +107 -0
- package/src/site-publishing/agent-data-tools.ts +250 -0
- package/src/site-publishing/agent-model.ts +127 -0
- package/src/site-publishing/agent-session.ts +148 -0
- package/src/site-publishing/content-scanner.ts +93 -0
- package/src/site-publishing/edge-sync.ts +148 -0
- package/src/site-publishing/gateway-signature.ts +198 -0
- package/src/site-publishing/limits.ts +315 -0
- package/src/site-publishing/page-tool-authority.ts +111 -0
- package/src/site-publishing/platform-usage.ts +479 -0
- package/src/site-publishing/platform.ts +98 -0
- package/src/site-publishing/readiness.ts +330 -0
- package/src/site-publishing/runtime.ts +831 -0
- package/src/site-publishing/service.ts +1282 -0
- package/src/site-publishing/session-events.ts +262 -0
- package/src/site-publishing/session-views.ts +262 -0
- package/src/site-publishing/viewer.ts +75 -0
- package/src/site-session-origin.ts +91 -6
- package/src/site-sql/format.ts +134 -0
- package/src/site-sql/gateway.ts +168 -0
- package/src/site-sql/service.ts +1636 -0
- package/src/site-uploads.ts +27 -5
- package/src/temporal-schedule-sync.ts +74 -0
- package/src/transcription/billing-refusal.ts +14 -0
- package/src/transcription/normalize.ts +77 -0
- package/src/transcription/providers/azure-mai.ts +98 -0
- package/src/transcription/providers/azure-openai.ts +16 -7
- package/src/transcription/providers/codex-subscription.ts +190 -67
- package/src/transcription/providers/openai.ts +79 -5
- package/src/transcription/providers/subscription-core.ts +125 -0
- package/src/transcription/providers/xai-subscription.ts +81 -83
- package/src/transcription/segmenter.ts +7 -1
- package/src/transcription/service.ts +239 -10
- package/src/transcription/wav-duration.ts +39 -0
- package/src/workspace-deletion.ts +25 -1
- package/src/workspace-tool-gateway-attestations.ts +133 -0
- package/src/workspace-tool-gateway-observability.ts +64 -0
- package/src/workspace-tool-gateway-secrets.ts +104 -0
- package/src/workspace-tool-gateway.ts +921 -103
- package/src/workspace-tool-target-fence.ts +380 -0
- package/src/workspace-tool-target.ts +554 -0
- package/src/workspace-webhook-dispatch.ts +225 -0
- package/src/xai-realtime.ts +85 -81
- package/src/xai-subscription-core.ts +172 -0
- package/dist/browser-auth-broker.d.ts +0 -20
- package/dist/chunk-AJ62T5QN.js.map +0 -1
- package/dist/connection-authority-owner.d.ts +0 -59
- package/dist/routes/connection-authorities.d.ts +0 -3
- package/dist/routes/environments.d.ts +0 -5
- package/dist/routes/host-mcp-bindings.d.ts +0 -5
- package/dist/routes/host-mcp-resolvers.d.ts +0 -3
- package/dist/routes/packs.d.ts +0 -3
- package/dist/session-event-view-MU3HMNV3.js +0 -264
- package/dist/session-event-view-MU3HMNV3.js.map +0 -1
- package/dist/xai-subscription-auth.d.ts +0 -23
- package/src/browser-auth-broker.ts +0 -155
- package/src/connection-authority-owner.ts +0 -61
- package/src/routes/connection-authorities.ts +0 -139
- package/src/routes/environments.ts +0 -388
- package/src/routes/host-mcp-bindings.ts +0 -122
- package/src/routes/host-mcp-resolvers.ts +0 -41
- package/src/routes/packs.ts +0 -784
- package/src/xai-subscription-auth.ts +0 -139
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { safeReturnPath } from "./oauth-return-path";
|
|
2
|
+
import { assertOrganizationIntegrationAllowed, stableJson } from "@opengeni/contracts";
|
|
3
|
+
import {
|
|
4
|
+
withOrganizationIntegrationAcquisition,
|
|
5
|
+
withOrganizationIntegrationPolicyFence,
|
|
6
|
+
} from "@opengeni/db/organization-integration-policy";
|
|
2
7
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
8
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
9
|
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
5
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
findIntegrationsOauthClient,
|
|
12
|
+
parseIntegrationsOauthClientsJson,
|
|
13
|
+
type Settings,
|
|
14
|
+
} from "@opengeni/config";
|
|
6
15
|
import {
|
|
7
16
|
OAuthStartResponse,
|
|
8
17
|
selectCanonicalPersonalSlackConnection,
|
|
9
18
|
type ConnectionOwnership,
|
|
10
19
|
type OAuthStartRequest,
|
|
11
20
|
} from "@opengeni/contracts";
|
|
12
|
-
import { requireEnvironmentEncryption } from "@opengeni/core";
|
|
21
|
+
import { requireEnvironmentEncryption, integrationKeyForConnectProvider } from "@opengeni/core";
|
|
13
22
|
import { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
|
|
14
23
|
import type { Observability } from "@opengeni/observability";
|
|
15
24
|
import {
|
|
@@ -24,14 +33,16 @@ import {
|
|
|
24
33
|
getGlobalCatalogOAuthProfile,
|
|
25
34
|
listConnectionsMetadata,
|
|
26
35
|
loadIntegrationOAuthClient,
|
|
36
|
+
loadIntegrationOAuthPendingState,
|
|
27
37
|
normalizeBearerScheme,
|
|
28
38
|
replaceIntegrationOAuthClientIfCurrent,
|
|
29
39
|
storeIntegrationOAuthClient,
|
|
40
|
+
storeIntegrationOAuthPendingState,
|
|
30
41
|
updateConnection,
|
|
31
42
|
withDatabaseStatementTimeout,
|
|
32
43
|
type Database,
|
|
33
44
|
} from "@opengeni/db";
|
|
34
|
-
import { createSignedState, readSignedState } from "@opengeni/github";
|
|
45
|
+
import { createSignedState, inspectSignedState, readSignedState } from "@opengeni/github";
|
|
35
46
|
import {
|
|
36
47
|
DestinationPolicyError,
|
|
37
48
|
McpOAuthDiscoveryError,
|
|
@@ -50,8 +61,9 @@ import {
|
|
|
50
61
|
type McpProtectedResourceMetadata,
|
|
51
62
|
} from "@opengeni/network";
|
|
52
63
|
import { Buffer } from "node:buffer";
|
|
53
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
64
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
54
65
|
import { HTTPException } from "hono/http-exception";
|
|
66
|
+
import { observeOAuthStart } from "../integration-connect-metrics";
|
|
55
67
|
import {
|
|
56
68
|
assertConnectionOwnershipAllowedForPrincipal,
|
|
57
69
|
personalOwnerStateAccepted,
|
|
@@ -63,11 +75,11 @@ import { ApiHttpError } from "../http/api-error";
|
|
|
63
75
|
import { requireConnectOwnerAuthority } from "./connect-authority";
|
|
64
76
|
import {
|
|
65
77
|
DEFAULT_OAUTH_PROFILE,
|
|
78
|
+
OFFICIAL_GMAIL_MCP_URL,
|
|
66
79
|
DEPLOYMENT_MANAGED_CLIENTS,
|
|
67
80
|
builtInOAuthProfileByKey,
|
|
68
81
|
assertAuthorizationServerNotReserved,
|
|
69
82
|
assertAuthorizationServerPins,
|
|
70
|
-
assertOwnershipAllowed,
|
|
71
83
|
builtInOAuthProfileFor,
|
|
72
84
|
catalogMcpUrlKey,
|
|
73
85
|
defaultOwnershipFor,
|
|
@@ -79,6 +91,64 @@ import {
|
|
|
79
91
|
import { canonicalProviderDomain } from "./provider-domain";
|
|
80
92
|
|
|
81
93
|
export const oauthStateTtlMs = 10 * 60 * 1000;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Workspace-free landing for a callback whose state names no trustworthy
|
|
97
|
+
* workspace. The web app resolves it to the viewer's current workspace
|
|
98
|
+
* integrations page and keeps the callback outcome parameters.
|
|
99
|
+
*/
|
|
100
|
+
export const INTEGRATIONS_FALLBACK_PATH = "/integrations";
|
|
101
|
+
|
|
102
|
+
const WORKSPACE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
103
|
+
|
|
104
|
+
/** The web integrations page for one workspace (the Plugins surface). */
|
|
105
|
+
export function workspaceIntegrationsPath(workspaceId: string): string {
|
|
106
|
+
return `/workspaces/${encodeURIComponent(workspaceId)}/plugins`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The integrations page for a workspace id read from untrusted input (a URL
|
|
111
|
+
* parameter or an unverified-age state), or null unless it is a workspace UUID.
|
|
112
|
+
* A link target only: it grants nothing.
|
|
113
|
+
*/
|
|
114
|
+
export function workspaceIntegrationsPathForUntrusted(candidate: unknown): string | null {
|
|
115
|
+
return typeof candidate === "string" && WORKSPACE_ID_PATTERN.test(candidate)
|
|
116
|
+
? workspaceIntegrationsPath(candidate)
|
|
117
|
+
: null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Where a callback returns, and why, when its own signed state is unusable.
|
|
122
|
+
*
|
|
123
|
+
* A correctly signed state still names its workspace, even when it is too old
|
|
124
|
+
* or its flow rejected it, so the browser returns to that workspace's
|
|
125
|
+
* integrations page; an aged one is reported as expired. Anything unsigned,
|
|
126
|
+
* tampered, or signed with another secret returns to
|
|
127
|
+
* {@link INTEGRATIONS_FALLBACK_PATH}. Display routing only: nothing here
|
|
128
|
+
* authorizes, resumes, or replays the callback.
|
|
129
|
+
*/
|
|
130
|
+
export function oauthStateFailureReturn(
|
|
131
|
+
settings: Settings,
|
|
132
|
+
rawState: string | undefined,
|
|
133
|
+
nowMs = Date.now(),
|
|
134
|
+
): { returnPath: string; reason: "state_expired" | "state_invalid" } {
|
|
135
|
+
let payload: ReturnType<typeof inspectSignedState> = null;
|
|
136
|
+
try {
|
|
137
|
+
payload = rawState
|
|
138
|
+
? inspectSignedState(rawState, requireIntegrationsStateSecret(settings))
|
|
139
|
+
: null;
|
|
140
|
+
} catch {
|
|
141
|
+
payload = null;
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
returnPath:
|
|
145
|
+
workspaceIntegrationsPathForUntrusted(payload?.workspaceId) ?? INTEGRATIONS_FALLBACK_PATH,
|
|
146
|
+
reason:
|
|
147
|
+
payload !== null && nowMs - payload.iat * 1000 > oauthStateTtlMs
|
|
148
|
+
? "state_expired"
|
|
149
|
+
: "state_invalid",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
82
152
|
export {
|
|
83
153
|
OFFICIAL_GMAIL_MCP_SCOPES,
|
|
84
154
|
OFFICIAL_GMAIL_MCP_URL,
|
|
@@ -95,6 +165,8 @@ type OAuthClientDeps = {
|
|
|
95
165
|
};
|
|
96
166
|
|
|
97
167
|
export type OAuthStartContext = {
|
|
168
|
+
/** Server-selected dedicated adapter identity; never copied from request metadata. */
|
|
169
|
+
integrationKey?: "gmail" | "slack-personal";
|
|
98
170
|
connectAttemptId?: string;
|
|
99
171
|
externalContinuation?: ExternalActorContinuation;
|
|
100
172
|
accountId: string;
|
|
@@ -118,6 +190,7 @@ export type OAuthCallbackResult = {
|
|
|
118
190
|
type WwwAuthenticateChallenge = McpOAuthChallenge;
|
|
119
191
|
type ProtectedResourceMetadata = McpProtectedResourceMetadata;
|
|
120
192
|
type AuthorizationServerMetadata = McpAuthorizationServerMetadata;
|
|
193
|
+
type OAuthDiscoveryMode = McpOAuthDiscoveryMode | "provider_oauth_metadata";
|
|
121
194
|
|
|
122
195
|
type OAuthClientRegistration = {
|
|
123
196
|
method: "operator" | "manual" | "cimd" | "dcr";
|
|
@@ -129,6 +202,7 @@ type OAuthClientRegistration = {
|
|
|
129
202
|
};
|
|
130
203
|
|
|
131
204
|
type OAuthStatePayload = {
|
|
205
|
+
integrationKey?: "gmail" | "slack-personal";
|
|
132
206
|
connectAttemptId?: string;
|
|
133
207
|
externalContinuation?: ExternalActorContinuation;
|
|
134
208
|
returnUrl?: string;
|
|
@@ -148,7 +222,7 @@ type OAuthStatePayload = {
|
|
|
148
222
|
tokenEndpoint: string;
|
|
149
223
|
authorizationServer: string;
|
|
150
224
|
issuer: string;
|
|
151
|
-
discoveryMode:
|
|
225
|
+
discoveryMode: OAuthDiscoveryMode;
|
|
152
226
|
discoveryMetadataSha256?: string;
|
|
153
227
|
protectedResourceMetadataUrl?: string;
|
|
154
228
|
authorizationServerMetadataUrl?: string;
|
|
@@ -188,7 +262,8 @@ export type OAuthStartStage =
|
|
|
188
262
|
| "mcp_challenge"
|
|
189
263
|
| "protected_resource_metadata"
|
|
190
264
|
| "authorization_server_metadata"
|
|
191
|
-
| "client_registration"
|
|
265
|
+
| "client_registration"
|
|
266
|
+
| "state_persist";
|
|
192
267
|
|
|
193
268
|
class OAuthStartStageError extends Error {
|
|
194
269
|
constructor(
|
|
@@ -312,19 +387,107 @@ class OAuthCallbackDeadline {
|
|
|
312
387
|
}
|
|
313
388
|
}
|
|
314
389
|
|
|
390
|
+
/** Policy is the outermost lock. Receipt replay still runs normal authorization,
|
|
391
|
+
* but the DB calls authorizeAcquisition only for uncommitted progress. Provider
|
|
392
|
+
* requests must happen after this transaction, never inside either wrapper. */
|
|
393
|
+
export async function claimOAuthAcquisition(
|
|
394
|
+
db: Database,
|
|
395
|
+
scope: Parameters<typeof claimConnectOperation>[1],
|
|
396
|
+
input: Parameters<typeof claimConnectOperation>[2],
|
|
397
|
+
integrationKey: string | null,
|
|
398
|
+
acquiring = true,
|
|
399
|
+
) {
|
|
400
|
+
if (!acquiring) return claimConnectOperation(db, scope, input);
|
|
401
|
+
return withOrganizationIntegrationPolicyFence(db, scope, (tx, policy) =>
|
|
402
|
+
claimConnectOperation(tx, scope, {
|
|
403
|
+
...input,
|
|
404
|
+
authorizeAcquisition: async (locked, attempt) => {
|
|
405
|
+
assertOrganizationIntegrationAllowed(policy, integrationKey);
|
|
406
|
+
await input.authorizeAcquisition?.(locked, attempt);
|
|
407
|
+
},
|
|
408
|
+
}),
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
type IntegrationSourceSelection = Record<string, unknown> & {
|
|
413
|
+
id: string;
|
|
414
|
+
syncEnabled: boolean;
|
|
415
|
+
readPolicy: "allow" | "ask" | "block";
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
/** Compare server-bound selections, not provider classification. Removing a
|
|
419
|
+
* source, disabling sync, or narrowing its read permission acquires no authority.
|
|
420
|
+
* Generation/timestamp bookkeeping does not turn an unchanged save into setup. */
|
|
421
|
+
export function integrationSourceSelectionRequiresAcquisition(
|
|
422
|
+
previous: readonly IntegrationSourceSelection[],
|
|
423
|
+
requested: readonly IntegrationSourceSelection[],
|
|
424
|
+
): boolean {
|
|
425
|
+
const rank = { block: 0, ask: 1, allow: 2 };
|
|
426
|
+
const config = (source: IntegrationSourceSelection) => {
|
|
427
|
+
const {
|
|
428
|
+
selectedAt: _selectedAt,
|
|
429
|
+
configGeneration: _generation,
|
|
430
|
+
syncEnabled: _enabled,
|
|
431
|
+
readPolicy: _readPolicy,
|
|
432
|
+
...binding
|
|
433
|
+
} = source;
|
|
434
|
+
return stableJson(binding);
|
|
435
|
+
};
|
|
436
|
+
return requested.some((source) => {
|
|
437
|
+
const existing = previous.find((candidate) => candidate.id === source.id);
|
|
438
|
+
return (
|
|
439
|
+
!existing ||
|
|
440
|
+
(!existing.syncEnabled && source.syncEnabled) ||
|
|
441
|
+
rank[source.readPolicy] > rank[existing.readPolicy] ||
|
|
442
|
+
config(existing) !== config(source)
|
|
443
|
+
);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export async function finishOAuthAcquisition(
|
|
448
|
+
db: Database,
|
|
449
|
+
scope: Parameters<typeof finishConnectOperation>[1],
|
|
450
|
+
input: Parameters<typeof finishConnectOperation>[2],
|
|
451
|
+
integrationKey: string | null,
|
|
452
|
+
) {
|
|
453
|
+
return withOrganizationIntegrationPolicyFence(db, scope, (tx, policy) =>
|
|
454
|
+
finishConnectOperation(tx, scope, {
|
|
455
|
+
...input,
|
|
456
|
+
authorizeAcquisition: async (locked, attempt) => {
|
|
457
|
+
assertOrganizationIntegrationAllowed(policy, integrationKey);
|
|
458
|
+
await input.authorizeAcquisition?.(locked, attempt);
|
|
459
|
+
},
|
|
460
|
+
}),
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
315
464
|
export async function startMcpOAuth(
|
|
316
465
|
deps: OAuthClientDeps,
|
|
317
466
|
context: OAuthStartContext,
|
|
318
467
|
): Promise<OAuthStartResponse> {
|
|
468
|
+
await withOrganizationIntegrationAcquisition(
|
|
469
|
+
deps.db,
|
|
470
|
+
context,
|
|
471
|
+
[context.integrationKey ?? "custom:mcp"],
|
|
472
|
+
async () => {},
|
|
473
|
+
);
|
|
319
474
|
const deadline = new OAuthStartDeadline(deps.oauthStartDeadlineMs ?? OAUTH_START_DEADLINE_MS);
|
|
320
475
|
try {
|
|
321
|
-
|
|
476
|
+
const started = await startMcpOAuthWithinDeadline(deps, context, deadline);
|
|
477
|
+
observeOAuthStart(deps.observability, { flow: "mcp_oauth", outcome: "success" });
|
|
478
|
+
return started;
|
|
322
479
|
} catch (error) {
|
|
323
480
|
const staged =
|
|
324
481
|
error instanceof OAuthStartStageError
|
|
325
482
|
? error
|
|
326
483
|
: new OAuthStartStageError("connection_lookup", oauthStartFailureReason(error), error);
|
|
327
484
|
logOAuthStartFailure(deps.observability, staged);
|
|
485
|
+
observeOAuthStart(deps.observability, {
|
|
486
|
+
flow: "mcp_oauth",
|
|
487
|
+
outcome: "failure",
|
|
488
|
+
stage: staged.stage,
|
|
489
|
+
reason: staged.reason,
|
|
490
|
+
});
|
|
328
491
|
throw oauthStartApiError(staged);
|
|
329
492
|
} finally {
|
|
330
493
|
deadline.dispose();
|
|
@@ -375,13 +538,13 @@ async function startMcpOAuthWithinDeadline(
|
|
|
375
538
|
})) ??
|
|
376
539
|
DEFAULT_OAUTH_PROFILE;
|
|
377
540
|
assertOAuthStartProfile(settings, context.payload, mcpUrl, profile);
|
|
378
|
-
//
|
|
379
|
-
// the fence that existed before #1240; an explicit non-personal value is the
|
|
380
|
-
// only thing rejected. Everything else defaults to workspace as before.
|
|
541
|
+
// Catalog defaults are setup preferences; an explicit ownership always wins.
|
|
381
542
|
const requestedOwnership: ConnectionOwnership =
|
|
382
543
|
context.payload.ownership ?? defaultOwnershipFor(profile);
|
|
383
|
-
|
|
384
|
-
const returnPath = safeReturnPath(
|
|
544
|
+
assertProfileOwnership(profile, requestedOwnership);
|
|
545
|
+
const returnPath = safeReturnPath(
|
|
546
|
+
context.payload.returnPath ?? workspaceIntegrationsPath(context.workspaceId),
|
|
547
|
+
);
|
|
385
548
|
const baseUrl = integrationBaseUrl(settings.publicBaseUrl, context.requestUrl);
|
|
386
549
|
const redirectUri = `${baseUrl}/v1/integrations/oauth/callback`;
|
|
387
550
|
const metadataUrl = `${baseUrl}/v1/integrations/oauth/client-metadata.json`;
|
|
@@ -394,6 +557,7 @@ async function startMcpOAuthWithinDeadline(
|
|
|
394
557
|
connectionSelection: profile.connectionSelection,
|
|
395
558
|
exactMcpBinding: profile.exactMcpBinding,
|
|
396
559
|
connectionId: context.payload.connectionId,
|
|
560
|
+
newAccount: context.payload.newAccount === true,
|
|
397
561
|
requestedOwnership: context.payload.ownership,
|
|
398
562
|
newConnectionOwnership: requestedOwnership,
|
|
399
563
|
}),
|
|
@@ -404,18 +568,17 @@ async function startMcpOAuthWithinDeadline(
|
|
|
404
568
|
const ownership = existing
|
|
405
569
|
? ownershipForConnection(existing.subjectId, context.subjectId)
|
|
406
570
|
: requestedOwnership;
|
|
407
|
-
|
|
408
|
-
// longer allows (e.g. shared authority over one human's hosted-Slack grant).
|
|
409
|
-
assertOwnershipAllowed(profile, ownership);
|
|
571
|
+
assertProfileOwnership(profile, ownership);
|
|
410
572
|
// Only a managed human can own a personal Connection: personal-authority
|
|
411
573
|
// execution resolves through a delegation snapshot frozen on a human's causal
|
|
412
574
|
// turn, and migration 0256 can mint the `user` authority scope only for a
|
|
413
|
-
// subject that holds an active organization membership.
|
|
414
|
-
//
|
|
415
|
-
// silently downgrading to the workspace ownership those profiles forbid.
|
|
575
|
+
// subject that holds an active organization membership. An explicit personal
|
|
576
|
+
// choice is never silently downgraded to workspace ownership.
|
|
416
577
|
assertConnectionOwnershipAllowedForPrincipal(ownership, context.personalOwnershipAllowed);
|
|
417
578
|
|
|
418
|
-
const discovery =
|
|
579
|
+
const discovery = profile.providerOAuthDiscovery
|
|
580
|
+
? await discoverProviderOAuth(mcpUrl, settings, deadline, profile)
|
|
581
|
+
: await discoverMcpOAuth(mcpUrl, settings, deadline);
|
|
419
582
|
assertDiscoveredAuthorizationServer(settings, discovery.as, profile, providerDomain);
|
|
420
583
|
const resourceParameterSupported =
|
|
421
584
|
discovery.mode === "rfc9728_protected_resource" && profile.sendResourceParameter;
|
|
@@ -427,6 +590,7 @@ async function startMcpOAuthWithinDeadline(
|
|
|
427
590
|
requested: context.payload.requestedScopes,
|
|
428
591
|
challenged: discovery.challenge.scope,
|
|
429
592
|
supported: discovery.prm.scopesSupported,
|
|
593
|
+
authorizationServerScopesSupported: stringArray(discovery.as.raw.scopes_supported),
|
|
430
594
|
});
|
|
431
595
|
const client = await deadline.run("client_registration", (signal) =>
|
|
432
596
|
registerOAuthClient(
|
|
@@ -439,10 +603,26 @@ async function startMcpOAuthWithinDeadline(
|
|
|
439
603
|
context.payload.oauthClient,
|
|
440
604
|
profile,
|
|
441
605
|
signal,
|
|
606
|
+
context,
|
|
442
607
|
),
|
|
443
608
|
);
|
|
609
|
+
if (
|
|
610
|
+
profile.confidentialClientRequired &&
|
|
611
|
+
(!isConfidentialOAuthClient(client) ||
|
|
612
|
+
!discovery.as.tokenEndpointAuthMethodsSupported.includes(client.tokenEndpointAuthMethod))
|
|
613
|
+
) {
|
|
614
|
+
throw new OAuthStartStageError(
|
|
615
|
+
"client_registration",
|
|
616
|
+
"configuration_missing",
|
|
617
|
+
new HTTPException(503, {
|
|
618
|
+
message:
|
|
619
|
+
"Gmail OAuth requires a registered Google Web application client with a secret and supported token authentication",
|
|
620
|
+
}),
|
|
621
|
+
);
|
|
622
|
+
}
|
|
444
623
|
const key = requireEnvironmentEncryption(settings);
|
|
445
|
-
const
|
|
624
|
+
const fullState = createSignedState(requireIntegrationsStateSecret(settings), {
|
|
625
|
+
...(context.integrationKey ? { integrationKey: context.integrationKey } : {}),
|
|
446
626
|
...(context.connectAttemptId ? { connectAttemptId: context.connectAttemptId } : {}),
|
|
447
627
|
accountId: context.accountId,
|
|
448
628
|
workspaceId: context.workspaceId,
|
|
@@ -491,6 +671,28 @@ async function startMcpOAuthWithinDeadline(
|
|
|
491
671
|
returnPath,
|
|
492
672
|
...(existing ? { connectionId: existing.id, connectionVersion: existing.version } : {}),
|
|
493
673
|
});
|
|
674
|
+
const expiresAt = new Date(Date.now() + oauthStateTtlMs);
|
|
675
|
+
// Providers cap `state` length (Stripe at 1024 characters), so only a short
|
|
676
|
+
// signed reference leaves; the full context waits encrypted at rest.
|
|
677
|
+
const stateId = randomUUID();
|
|
678
|
+
const state = createSignedState(requireIntegrationsStateSecret(settings), {
|
|
679
|
+
kind: "mcp_oauth_reference",
|
|
680
|
+
id: stateId,
|
|
681
|
+
accountId: context.accountId,
|
|
682
|
+
workspaceId: context.workspaceId,
|
|
683
|
+
});
|
|
684
|
+
if (state.length > 512) {
|
|
685
|
+
throw new Error("MCP OAuth reference state exceeded its provider-safe length");
|
|
686
|
+
}
|
|
687
|
+
await deadline.run("state_persist", async () =>
|
|
688
|
+
storeIntegrationOAuthPendingState(db, {
|
|
689
|
+
id: stateId,
|
|
690
|
+
accountId: context.accountId,
|
|
691
|
+
workspaceId: context.workspaceId,
|
|
692
|
+
stateEncrypted: encryptEnvironmentValue(key, fullState),
|
|
693
|
+
expiresAt,
|
|
694
|
+
}),
|
|
695
|
+
);
|
|
494
696
|
const authorizationUrl = buildAuthorizationUrl({
|
|
495
697
|
endpoint: discovery.as.authorizationEndpoint,
|
|
496
698
|
settings,
|
|
@@ -506,36 +708,10 @@ async function startMcpOAuthWithinDeadline(
|
|
|
506
708
|
return OAuthStartResponse.parse({
|
|
507
709
|
state,
|
|
508
710
|
authorizationUrl,
|
|
509
|
-
expiresAt:
|
|
711
|
+
expiresAt: expiresAt.toISOString(),
|
|
510
712
|
});
|
|
511
713
|
}
|
|
512
714
|
|
|
513
|
-
/** Ownership fence for the official Gmail profile, keyed by exact MCP URL. */
|
|
514
|
-
export function assertOfficialGmailPersonalOwnership(
|
|
515
|
-
mcpUrl: string,
|
|
516
|
-
ownership: ConnectionOwnership,
|
|
517
|
-
): void {
|
|
518
|
-
const profile = builtInOAuthProfileFor({ mcpUrl });
|
|
519
|
-
if (profile?.key === "official-gmail") {
|
|
520
|
-
assertOwnershipAllowed(profile, ownership);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* Slack's hosted MCP issues user tokens only. Sharing one human's grant as
|
|
526
|
-
* workspace authority made every shared agent act as a named employee; the
|
|
527
|
-
* OpenGeni workspace bot owns shared Slack access instead (bot-token search
|
|
528
|
-
* covers public channels, files, and users).
|
|
529
|
-
*/
|
|
530
|
-
export function assertHostedSlackMcpPersonalOwnership(
|
|
531
|
-
hostedSlackMcp: boolean,
|
|
532
|
-
ownership: ConnectionOwnership,
|
|
533
|
-
): void {
|
|
534
|
-
if (hostedSlackMcp) {
|
|
535
|
-
assertOwnershipAllowed(builtInOAuthProfileByKey("hosted-slack-mcp"), ownership);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
715
|
export function isHostedSlackMcpTarget(providerDomain: string, mcpUrl: string): boolean {
|
|
540
716
|
return builtInOAuthProfileFor({ mcpUrl, providerDomain })?.key === "hosted-slack-mcp";
|
|
541
717
|
}
|
|
@@ -603,6 +779,8 @@ export async function completeMcpOAuthCallback(
|
|
|
603
779
|
input: {
|
|
604
780
|
code?: string | undefined;
|
|
605
781
|
state?: string | undefined;
|
|
782
|
+
/** The provider's `error` parameter, for example `access_denied` on Cancel. */
|
|
783
|
+
error?: string | undefined;
|
|
606
784
|
requestUrl: string;
|
|
607
785
|
},
|
|
608
786
|
): Promise<OAuthCallbackResult> {
|
|
@@ -621,12 +799,14 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
621
799
|
input: {
|
|
622
800
|
code?: string | undefined;
|
|
623
801
|
state?: string | undefined;
|
|
802
|
+
error?: string | undefined;
|
|
624
803
|
requestUrl: string;
|
|
625
804
|
},
|
|
626
805
|
deadline: OAuthCallbackDeadline,
|
|
627
806
|
): Promise<OAuthCallbackResult> {
|
|
628
807
|
const { db, settings, observability } = deps;
|
|
629
808
|
let state: OAuthStatePayload | null = null;
|
|
809
|
+
let integrationKey: string | null = "custom:mcp";
|
|
630
810
|
let connectOperation: { attemptId: string; operationId: string; inputDigest: string } | undefined;
|
|
631
811
|
if (!input.state) {
|
|
632
812
|
const error = new OAuthCallbackStageError(
|
|
@@ -636,18 +816,29 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
636
816
|
);
|
|
637
817
|
logOAuthCallbackFailure(observability, error, state);
|
|
638
818
|
return {
|
|
639
|
-
redirectTo: callbackReturnPath(
|
|
819
|
+
redirectTo: callbackReturnPath(INTEGRATIONS_FALLBACK_PATH, "error", {
|
|
640
820
|
stage: error.stage,
|
|
641
821
|
reason: error.reason,
|
|
642
822
|
}),
|
|
643
823
|
};
|
|
644
824
|
}
|
|
645
825
|
try {
|
|
646
|
-
state =
|
|
826
|
+
state = await resolveMcpOAuthState(db, settings, input.state, deadline);
|
|
827
|
+
integrationKey = state.integrationKey ?? "custom:mcp";
|
|
647
828
|
if (state.connectAttemptId) {
|
|
648
829
|
const stored = await getConnectAttempt(db, state, state.connectAttemptId);
|
|
830
|
+
// Native Gmail setup uses its own provider id, but shares this callback.
|
|
831
|
+
// Bind that id to the exact reviewed Gmail destination before
|
|
832
|
+
// claiming an operation (including receipt replay), not to arbitrary MCP.
|
|
833
|
+
const gmailAttemptMatches =
|
|
834
|
+
stored.attempt.providerId === "gmail" &&
|
|
835
|
+
state.mcpUrl === OFFICIAL_GMAIL_MCP_URL &&
|
|
836
|
+
state.providerDomain === "gmailmcp.googleapis.com" &&
|
|
837
|
+
builtInOAuthProfileFor(state)?.key === "official-gmail" &&
|
|
838
|
+
personalOwnerStateAccepted(state);
|
|
649
839
|
if (
|
|
650
|
-
!["mcp-oauth", "slack-personal"].includes(stored.attempt.providerId)
|
|
840
|
+
(!["mcp-oauth", "slack-personal"].includes(stored.attempt.providerId) &&
|
|
841
|
+
!gmailAttemptMatches) ||
|
|
651
842
|
stored.attempt.ownership !== state.ownership ||
|
|
652
843
|
stored.returnUrl !== state.returnUrl
|
|
653
844
|
)
|
|
@@ -657,29 +848,23 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
657
848
|
operationId: `oauth:${state.nonce}`,
|
|
658
849
|
inputDigest: createHash("sha256").update(input.state).digest("hex"),
|
|
659
850
|
};
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
851
|
+
integrationKey = integrationKeyForConnectProvider(stored.attempt.providerId);
|
|
852
|
+
const claim = await claimOAuthAcquisition(
|
|
853
|
+
db,
|
|
854
|
+
state,
|
|
855
|
+
{
|
|
856
|
+
...connectOperation,
|
|
857
|
+
expectedRevision: stored.attempt.revision,
|
|
858
|
+
authorize: (tx, _attempt, origin) =>
|
|
859
|
+
requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
|
|
860
|
+
},
|
|
861
|
+
integrationKey,
|
|
862
|
+
Boolean(input.code),
|
|
863
|
+
);
|
|
666
864
|
if (claim.status === "replayed") return { redirectTo: stored.returnUrl, exactReturn: true };
|
|
667
865
|
}
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
// longer allows (workspace-owned Gmail or hosted Slack, for example).
|
|
671
|
-
const builtInCallbackProfile = builtInOAuthProfileFor({
|
|
672
|
-
mcpUrl: state.mcpUrl,
|
|
673
|
-
providerDomain: state.providerDomain,
|
|
674
|
-
});
|
|
675
|
-
if (builtInCallbackProfile) {
|
|
676
|
-
assertOwnershipAllowed(builtInCallbackProfile, state.ownership);
|
|
677
|
-
}
|
|
678
|
-
// Only a managed human may own a personal Connection. This callback has no
|
|
679
|
-
// live principal, so it enforces the signed start-time decision; a state
|
|
680
|
-
// minted before that fence existed carries no claim and is refused, which
|
|
681
|
-
// closes the in-flight window across a rolling deploy. The legacy
|
|
682
|
-
// `?? "personal"` decode above therefore cannot land a machine-owned row.
|
|
866
|
+
// The callback enforces the signed start-time principal decision.
|
|
867
|
+
// Workspace ownership requires no personal-owner claim.
|
|
683
868
|
if (!personalOwnerStateAccepted(state)) {
|
|
684
869
|
throw new OAuthCallbackStageError(
|
|
685
870
|
"state_verify",
|
|
@@ -687,7 +872,11 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
687
872
|
new HTTPException(422, { message: PERSONAL_CONNECTION_PRINCIPAL_MESSAGE }),
|
|
688
873
|
);
|
|
689
874
|
}
|
|
690
|
-
if (!input.code) {
|
|
875
|
+
if (input.error || !input.code) {
|
|
876
|
+
// The provider sends `error` (for example `access_denied` when the user
|
|
877
|
+
// clicks Cancel) instead of a code. Report that as a refusal, not as an
|
|
878
|
+
// expired attempt, and never echo the provider's own text.
|
|
879
|
+
const cancelled = input.error === "access_denied";
|
|
691
880
|
if (connectOperation) {
|
|
692
881
|
await finishConnectOperation(db, state, {
|
|
693
882
|
...connectOperation,
|
|
@@ -696,39 +885,31 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
696
885
|
commit: async (_tx, current) => ({
|
|
697
886
|
...current,
|
|
698
887
|
revision: current.revision + 1,
|
|
699
|
-
state: "failed",
|
|
888
|
+
state: cancelled ? "cancelled" : "failed",
|
|
700
889
|
nextAction: { type: "none" },
|
|
701
890
|
error: {
|
|
702
|
-
code: "missing_code",
|
|
891
|
+
code: input.error ? "provider_denied" : "missing_code",
|
|
703
892
|
message: "Authorization was not completed. Start a new connection attempt.",
|
|
704
893
|
retryable: false,
|
|
705
894
|
},
|
|
706
895
|
}),
|
|
707
896
|
});
|
|
708
897
|
}
|
|
709
|
-
return callbackStateResult(
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
898
|
+
return callbackStateResult(
|
|
899
|
+
state,
|
|
900
|
+
"error",
|
|
901
|
+
input.error
|
|
902
|
+
? { stage: "authorize", reason: cancelled ? "access_denied" : "provider_error" }
|
|
903
|
+
: { stage: "state_verify", reason: "missing_code" },
|
|
904
|
+
);
|
|
713
905
|
}
|
|
906
|
+
await withOrganizationIntegrationAcquisition(db, state, [integrationKey], async () => {});
|
|
714
907
|
const consumed = await runCallbackDatabaseStage(
|
|
715
908
|
deadline,
|
|
716
909
|
"state_verify",
|
|
717
910
|
db,
|
|
718
911
|
async (scopedDb) => {
|
|
719
912
|
await requireOAuthCallbackGrant(scopedDb, state!);
|
|
720
|
-
if (!builtInCallbackProfile) {
|
|
721
|
-
// Catalog-declared ownership fences get the same rolling-update
|
|
722
|
-
// treatment as the built-in ones.
|
|
723
|
-
const raw = await getGlobalCatalogOAuthProfile(
|
|
724
|
-
scopedDb,
|
|
725
|
-
state!.workspaceId,
|
|
726
|
-
catalogMcpUrlKey(state!.mcpUrl),
|
|
727
|
-
);
|
|
728
|
-
if (raw !== null) {
|
|
729
|
-
assertOwnershipAllowed(oauthProfileFromCatalog(state!.mcpUrl, raw), state!.ownership);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
913
|
return await consumeIntegrationOAuthStateNonce(scopedDb, {
|
|
733
914
|
accountId: state!.accountId,
|
|
734
915
|
workspaceId: state!.workspaceId,
|
|
@@ -750,6 +931,17 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
750
931
|
? error
|
|
751
932
|
: new OAuthCallbackStageError("state_verify", "state_invalid", error);
|
|
752
933
|
logOAuthCallbackFailure(observability, staged, state);
|
|
934
|
+
if (!state) {
|
|
935
|
+
// The state itself could not be resolved, so it carries no return path.
|
|
936
|
+
// An authentic but aged state still names its workspace and "expired".
|
|
937
|
+
const failure = oauthStateFailureReturn(settings, input.state);
|
|
938
|
+
return {
|
|
939
|
+
redirectTo: callbackReturnPath(failure.returnPath, "error", {
|
|
940
|
+
stage: staged.stage,
|
|
941
|
+
reason: staged.reason === "state_invalid" ? failure.reason : staged.reason,
|
|
942
|
+
}),
|
|
943
|
+
};
|
|
944
|
+
}
|
|
753
945
|
return callbackStateResult(state, "error", {
|
|
754
946
|
stage: staged.stage,
|
|
755
947
|
reason: staged.reason,
|
|
@@ -765,6 +957,16 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
765
957
|
const client = await runCallbackDatabaseStage(deadline, "client_lookup", db, (scopedDb) =>
|
|
766
958
|
clientForState(scopedDb, settings, state),
|
|
767
959
|
);
|
|
960
|
+
if (
|
|
961
|
+
builtInOAuthProfileFor(state)?.confidentialClientRequired &&
|
|
962
|
+
!isConfidentialOAuthClient(client)
|
|
963
|
+
) {
|
|
964
|
+
throw new OAuthCallbackStageError(
|
|
965
|
+
"client_lookup",
|
|
966
|
+
"invalid_client",
|
|
967
|
+
new Error("Gmail OAuth client is unavailable"),
|
|
968
|
+
);
|
|
969
|
+
}
|
|
768
970
|
const token = await deadline.run("token_exchange", (signal) =>
|
|
769
971
|
exchangeAuthorizationCode(settings, {
|
|
770
972
|
code: input.code!,
|
|
@@ -777,6 +979,27 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
777
979
|
signal,
|
|
778
980
|
}),
|
|
779
981
|
);
|
|
982
|
+
const profile = builtInOAuthProfileFor(state);
|
|
983
|
+
if (profile?.freshRefreshTokenRequired && !token.refreshToken) {
|
|
984
|
+
// Never splice a previous account's refresh token into a new grant.
|
|
985
|
+
// Google can omit refresh_token even after another consent prompt.
|
|
986
|
+
throw new OAuthCallbackStageError(
|
|
987
|
+
"token_exchange",
|
|
988
|
+
"offline_access_unavailable",
|
|
989
|
+
new Error("Google did not grant offline access. Start a new Gmail connection attempt."),
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
const scopes = grantedScopes(token.scopeText, state.authorizeScopes, profile);
|
|
993
|
+
if (
|
|
994
|
+
profile?.fullRequestedScopesRequired &&
|
|
995
|
+
profile.requestedScopes?.some((scope) => !scopes.includes(scope))
|
|
996
|
+
) {
|
|
997
|
+
throw new OAuthCallbackStageError(
|
|
998
|
+
"token_exchange",
|
|
999
|
+
"insufficient_scope",
|
|
1000
|
+
new Error("Google did not grant the reviewed Gmail permissions."),
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
780
1003
|
const verification = await verifyMcpToolsListNonFatal(
|
|
781
1004
|
observability,
|
|
782
1005
|
settings,
|
|
@@ -784,7 +1007,6 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
784
1007
|
token,
|
|
785
1008
|
deadline,
|
|
786
1009
|
);
|
|
787
|
-
const scopes = grantedScopes(token.scopeText, state.authorizeScopes);
|
|
788
1010
|
const credential = credentialBundle(token, state, client);
|
|
789
1011
|
const metadata = {
|
|
790
1012
|
resource: state.resource,
|
|
@@ -807,6 +1029,7 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
807
1029
|
: {}),
|
|
808
1030
|
},
|
|
809
1031
|
mcpToolsVerification: verification.metadata,
|
|
1032
|
+
...(verification.providerIdentity ?? {}),
|
|
810
1033
|
...(verification.tools ? { mcpTools: verification.tools } : {}),
|
|
811
1034
|
};
|
|
812
1035
|
const credentialEncrypted = encryptEnvironmentValue(key, JSON.stringify(credential));
|
|
@@ -821,7 +1044,7 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
821
1044
|
subjectId: ownerSubjectId,
|
|
822
1045
|
providerDomain: state.providerDomain,
|
|
823
1046
|
kind: "oauth2",
|
|
824
|
-
status: "active",
|
|
1047
|
+
status: verification.connectionStatus ?? "active",
|
|
825
1048
|
credentialEncrypted,
|
|
826
1049
|
grantedScopes: scopes,
|
|
827
1050
|
expiresAt: token.expiresAt,
|
|
@@ -834,6 +1057,7 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
834
1057
|
subjectId: ownerSubjectId,
|
|
835
1058
|
providerDomain: state.providerDomain,
|
|
836
1059
|
kind: "oauth2",
|
|
1060
|
+
status: verification.connectionStatus ?? "active",
|
|
837
1061
|
credentialEncrypted,
|
|
838
1062
|
grantedScopes: scopes,
|
|
839
1063
|
expiresAt: token.expiresAt,
|
|
@@ -843,38 +1067,45 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
843
1067
|
};
|
|
844
1068
|
if (connectOperation) {
|
|
845
1069
|
await runCallbackDatabaseStage(deadline, "persist", db, (scopedDb) =>
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1070
|
+
finishOAuthAcquisition(
|
|
1071
|
+
scopedDb,
|
|
1072
|
+
state!,
|
|
1073
|
+
{
|
|
1074
|
+
...connectOperation!,
|
|
1075
|
+
authorize: (tx, _attempt, origin) =>
|
|
1076
|
+
requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
|
|
1077
|
+
commit: async (tx, current) => {
|
|
1078
|
+
const connection = await persist(tx);
|
|
1079
|
+
if (!connection)
|
|
1080
|
+
throw new HTTPException(409, {
|
|
1081
|
+
message: "connection changed during OAuth reconnect",
|
|
1082
|
+
});
|
|
1083
|
+
return {
|
|
1084
|
+
...current,
|
|
1085
|
+
revision: current.revision + 1,
|
|
1086
|
+
state: "complete",
|
|
1087
|
+
completionRequirement: "connection",
|
|
1088
|
+
credentialsCommitted: true,
|
|
1089
|
+
nextAction: { type: "none" },
|
|
1090
|
+
account: {
|
|
1091
|
+
id: connection.id,
|
|
1092
|
+
version: connection.version,
|
|
1093
|
+
providerId: current.providerId,
|
|
1094
|
+
label: state!.providerDomain,
|
|
1095
|
+
ownership: current.ownership,
|
|
1096
|
+
status: connection.status === "needs_reauth" ? "auth_needed" : "connected",
|
|
1097
|
+
},
|
|
1098
|
+
};
|
|
1099
|
+
},
|
|
872
1100
|
},
|
|
873
|
-
|
|
1101
|
+
integrationKey,
|
|
1102
|
+
),
|
|
874
1103
|
);
|
|
875
1104
|
return { redirectTo: state.returnUrl!, exactReturn: true };
|
|
876
1105
|
}
|
|
877
|
-
const connection = await runCallbackDatabaseStage(deadline, "persist", db,
|
|
1106
|
+
const connection = await runCallbackDatabaseStage(deadline, "persist", db, (scopedDb) =>
|
|
1107
|
+
withOrganizationIntegrationAcquisition(scopedDb, state!, [integrationKey], persist),
|
|
1108
|
+
);
|
|
878
1109
|
if (!connection) {
|
|
879
1110
|
throw new HTTPException(409, {
|
|
880
1111
|
message: "connection changed during OAuth reconnect; start again",
|
|
@@ -896,6 +1127,41 @@ async function completeMcpOAuthCallbackWithinDeadline(
|
|
|
896
1127
|
? error
|
|
897
1128
|
: new OAuthCallbackStageError("persist", "persist_failed", error);
|
|
898
1129
|
logOAuthCallbackFailure(observability, staged, state);
|
|
1130
|
+
if (connectOperation && builtInOAuthProfileFor(state)?.key === "official-gmail") {
|
|
1131
|
+
// The external return URL stays exact. Its caller reads the durable
|
|
1132
|
+
// attempt result, which must not remain in flight after a rejected grant.
|
|
1133
|
+
try {
|
|
1134
|
+
await withDatabaseStatementTimeout(db, OAUTH_CALLBACK_DB_STATEMENT_TIMEOUT_MS, (tx) =>
|
|
1135
|
+
finishConnectOperation(tx, state!, {
|
|
1136
|
+
...connectOperation!,
|
|
1137
|
+
authorize: (locked, _attempt, origin) =>
|
|
1138
|
+
requireConnectOwnerAuthority(locked, state!, "connections:write", origin),
|
|
1139
|
+
commit: async (_locked, current) => ({
|
|
1140
|
+
...current,
|
|
1141
|
+
revision: current.revision + 1,
|
|
1142
|
+
state: "failed",
|
|
1143
|
+
nextAction: { type: "none" },
|
|
1144
|
+
error: {
|
|
1145
|
+
code: staged.reason,
|
|
1146
|
+
message:
|
|
1147
|
+
staged.reason === "offline_access_unavailable"
|
|
1148
|
+
? "Google did not grant offline access. Your previous connection is unchanged. Start a new Gmail connection attempt."
|
|
1149
|
+
: staged.reason === "insufficient_scope"
|
|
1150
|
+
? "Google did not grant all required Gmail permissions. Your previous connection is unchanged. Start a new connection attempt and approve the requested permissions."
|
|
1151
|
+
: "Gmail authorization could not be verified. Your previous connection is unchanged. Start a new connection attempt.",
|
|
1152
|
+
retryable: true,
|
|
1153
|
+
},
|
|
1154
|
+
}),
|
|
1155
|
+
}),
|
|
1156
|
+
);
|
|
1157
|
+
} catch (settlementError) {
|
|
1158
|
+
logOAuthCallbackFailure(
|
|
1159
|
+
observability,
|
|
1160
|
+
new OAuthCallbackStageError("persist", "persist_failed", settlementError),
|
|
1161
|
+
state,
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
899
1165
|
return callbackStateResult(state, "error", {
|
|
900
1166
|
stage: staged.stage,
|
|
901
1167
|
reason: staged.reason,
|
|
@@ -924,7 +1190,13 @@ function callbackStateResult(
|
|
|
924
1190
|
): OAuthCallbackResult {
|
|
925
1191
|
if ((state?.externalContinuation || state?.connectAttemptId) && state.returnUrl)
|
|
926
1192
|
return { redirectTo: state.returnUrl, exactReturn: true };
|
|
927
|
-
return {
|
|
1193
|
+
return {
|
|
1194
|
+
redirectTo: callbackReturnPath(
|
|
1195
|
+
state?.returnPath ?? INTEGRATIONS_FALLBACK_PATH,
|
|
1196
|
+
status,
|
|
1197
|
+
details,
|
|
1198
|
+
),
|
|
1199
|
+
};
|
|
928
1200
|
}
|
|
929
1201
|
|
|
930
1202
|
export function integrationBaseUrl(publicBaseUrl: string | undefined, requestUrl: string): string {
|
|
@@ -985,7 +1257,7 @@ export async function inspectMcpAuthentication(
|
|
|
985
1257
|
params: {
|
|
986
1258
|
protocolVersion: "2025-03-26",
|
|
987
1259
|
capabilities: {},
|
|
988
|
-
clientInfo: { name: "
|
|
1260
|
+
clientInfo: { name: "Opengeni", version: "1.0" },
|
|
989
1261
|
},
|
|
990
1262
|
}),
|
|
991
1263
|
signal,
|
|
@@ -1042,10 +1314,13 @@ export async function inspectMcpAuthentication(
|
|
|
1042
1314
|
await resolveMcpOAuthDiscovery({
|
|
1043
1315
|
resourceUrl: resource,
|
|
1044
1316
|
challenge,
|
|
1045
|
-
fetchMetadata: async ({ url }) => {
|
|
1317
|
+
fetchMetadata: async ({ kind, url }) => {
|
|
1046
1318
|
try {
|
|
1047
|
-
const result = await deadline.run(
|
|
1048
|
-
|
|
1319
|
+
const result = await deadline.run(
|
|
1320
|
+
kind === "protected_resource"
|
|
1321
|
+
? "protected_resource_metadata"
|
|
1322
|
+
: "authorization_server_metadata",
|
|
1323
|
+
(signal) => fetchOAuthMetadata(url, settings, signal),
|
|
1049
1324
|
);
|
|
1050
1325
|
metadataRead = true;
|
|
1051
1326
|
if (result.status !== "absent") metadataAbsent = false;
|
|
@@ -1059,7 +1334,7 @@ export async function inspectMcpAuthentication(
|
|
|
1059
1334
|
canonicalizeResource: canonicalOAuthResource,
|
|
1060
1335
|
});
|
|
1061
1336
|
return { kind: "oauth2" };
|
|
1062
|
-
} catch {
|
|
1337
|
+
} catch (failure) {
|
|
1063
1338
|
// Public initialization does not exclude optional or tool-level OAuth.
|
|
1064
1339
|
// Only explicit absence of metadata permits the unauthenticated path.
|
|
1065
1340
|
if (
|
|
@@ -1072,20 +1347,31 @@ export async function inspectMcpAuthentication(
|
|
|
1072
1347
|
return { kind: "none" };
|
|
1073
1348
|
return {
|
|
1074
1349
|
kind: "unknown",
|
|
1075
|
-
message:
|
|
1076
|
-
"This server's sign-in requirements could not be determined. Check its setup instructions.",
|
|
1350
|
+
message: mcpAuthInspectionFailureMessage(failure),
|
|
1077
1351
|
};
|
|
1078
1352
|
}
|
|
1079
|
-
} catch {
|
|
1353
|
+
} catch (failure) {
|
|
1080
1354
|
return {
|
|
1081
1355
|
kind: "unknown",
|
|
1082
|
-
message:
|
|
1356
|
+
message: mcpAuthInspectionFailureMessage(failure),
|
|
1083
1357
|
};
|
|
1084
1358
|
} finally {
|
|
1085
1359
|
deadline.dispose();
|
|
1086
1360
|
}
|
|
1087
1361
|
}
|
|
1088
1362
|
|
|
1363
|
+
function mcpAuthInspectionFailureMessage(failure: unknown): string {
|
|
1364
|
+
if (failure instanceof OAuthStartStageError) {
|
|
1365
|
+
if (failure.reason === "timeout") {
|
|
1366
|
+
return "Checking how to sign in timed out. Try again.";
|
|
1367
|
+
}
|
|
1368
|
+
if (failure.cause instanceof OAuthMetadataUpstreamError) {
|
|
1369
|
+
return `The provider returned HTTP ${failure.cause.upstreamStatus} while checking how to sign in. Retry or check the provider's setup instructions.`;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return "Could not determine how to sign in. Retry or check the provider's setup instructions.";
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1089
1375
|
async function discoverMcpOAuth(
|
|
1090
1376
|
resource: string,
|
|
1091
1377
|
settings: Settings,
|
|
@@ -1143,6 +1429,70 @@ async function discoverMcpOAuth(
|
|
|
1143
1429
|
}
|
|
1144
1430
|
}
|
|
1145
1431
|
|
|
1432
|
+
/** Exact built-in API bridge discovery never contacts its historical MCP identity. */
|
|
1433
|
+
async function discoverProviderOAuth(
|
|
1434
|
+
resource: string,
|
|
1435
|
+
settings: Settings,
|
|
1436
|
+
deadline: OAuthStartDeadline,
|
|
1437
|
+
profile: OAuthProviderProfile,
|
|
1438
|
+
) {
|
|
1439
|
+
const pinned = profile.providerOAuthDiscovery;
|
|
1440
|
+
if (!pinned || resource !== profile.requireExactMcpUrl?.url)
|
|
1441
|
+
throw new HTTPException(422, { message: "invalid provider OAuth resource" });
|
|
1442
|
+
const fetched = await deadline.run("authorization_server_metadata", (signal) =>
|
|
1443
|
+
fetchOAuthMetadata(pinned.metadataUrl, settings, signal),
|
|
1444
|
+
);
|
|
1445
|
+
if (fetched.status !== "present")
|
|
1446
|
+
throw new HTTPException(422, { message: "provider OAuth metadata is unavailable" });
|
|
1447
|
+
const raw = fetched.document;
|
|
1448
|
+
const issuer = normalizedIssuerKey(
|
|
1449
|
+
oauthEndpointUrl(requiredString(raw.issuer, "OAuth issuer"), settings, "OAuth issuer"),
|
|
1450
|
+
);
|
|
1451
|
+
if (issuer !== pinned.issuer)
|
|
1452
|
+
throw new HTTPException(422, { message: "provider OAuth issuer did not match its profile" });
|
|
1453
|
+
const as: AuthorizationServerMetadata = {
|
|
1454
|
+
issuer,
|
|
1455
|
+
authorizationServer: pinned.issuer,
|
|
1456
|
+
authorizationEndpoint: oauthEndpointUrl(
|
|
1457
|
+
requiredString(raw.authorization_endpoint, "OAuth authorization endpoint"),
|
|
1458
|
+
settings,
|
|
1459
|
+
"OAuth authorization endpoint",
|
|
1460
|
+
),
|
|
1461
|
+
tokenEndpoint: oauthEndpointUrl(
|
|
1462
|
+
requiredString(raw.token_endpoint, "OAuth token endpoint"),
|
|
1463
|
+
settings,
|
|
1464
|
+
"OAuth token endpoint",
|
|
1465
|
+
),
|
|
1466
|
+
clientIdMetadataDocumentSupported: false,
|
|
1467
|
+
tokenEndpointAuthMethodsSupported: stringArray(raw.token_endpoint_auth_methods_supported),
|
|
1468
|
+
codeChallengeMethodsSupported: stringArray(raw.code_challenge_methods_supported),
|
|
1469
|
+
raw,
|
|
1470
|
+
metadataUrl: fetched.url,
|
|
1471
|
+
};
|
|
1472
|
+
if (!as.codeChallengeMethodsSupported.includes("S256"))
|
|
1473
|
+
throw new HTTPException(422, { message: "provider OAuth requires PKCE S256" });
|
|
1474
|
+
return {
|
|
1475
|
+
challenge: { scheme: null, scope: [] } as WwwAuthenticateChallenge,
|
|
1476
|
+
prm: {
|
|
1477
|
+
resource,
|
|
1478
|
+
authorizationServers: [pinned.issuer],
|
|
1479
|
+
scopesSupported: [...(profile.requestedScopes ?? [])],
|
|
1480
|
+
raw: {},
|
|
1481
|
+
metadataUrl: "",
|
|
1482
|
+
} as ProtectedResourceMetadata,
|
|
1483
|
+
as,
|
|
1484
|
+
mode: "provider_oauth_metadata" as const,
|
|
1485
|
+
resource,
|
|
1486
|
+
provenance: {
|
|
1487
|
+
protectedResourceMetadataUrl: null,
|
|
1488
|
+
authorizationServerMetadataUrl: fetched.url,
|
|
1489
|
+
metadataSha256: createHash("sha256")
|
|
1490
|
+
.update(stableJson({ profile: profile.key, resource, metadataUrl: fetched.url, raw }))
|
|
1491
|
+
.digest("hex"),
|
|
1492
|
+
},
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1146
1496
|
async function probeMcpChallenge(
|
|
1147
1497
|
resource: string,
|
|
1148
1498
|
settings: Settings,
|
|
@@ -1206,6 +1556,7 @@ async function registerOAuthClient(
|
|
|
1206
1556
|
manual: OAuthStartRequest["oauthClient"],
|
|
1207
1557
|
profile: OAuthProviderProfile,
|
|
1208
1558
|
signal: AbortSignal,
|
|
1559
|
+
acquisition: Pick<OAuthStartContext, "accountId" | "workspaceId" | "integrationKey">,
|
|
1209
1560
|
): Promise<OAuthClientRegistration> {
|
|
1210
1561
|
const operator = operatorClientForAs(settings, as);
|
|
1211
1562
|
if (operator) {
|
|
@@ -1220,6 +1571,7 @@ async function registerOAuthClient(
|
|
|
1220
1571
|
redirectUri,
|
|
1221
1572
|
scopes,
|
|
1222
1573
|
signal,
|
|
1574
|
+
acquisition,
|
|
1223
1575
|
);
|
|
1224
1576
|
}
|
|
1225
1577
|
if (selfRegistration === "cimd") {
|
|
@@ -1244,7 +1596,15 @@ async function registerOAuthClient(
|
|
|
1244
1596
|
),
|
|
1245
1597
|
};
|
|
1246
1598
|
}
|
|
1247
|
-
return await getOrCreateDynamicClientRegistration(
|
|
1599
|
+
return await getOrCreateDynamicClientRegistration(
|
|
1600
|
+
db,
|
|
1601
|
+
settings,
|
|
1602
|
+
as,
|
|
1603
|
+
redirectUri,
|
|
1604
|
+
scopes,
|
|
1605
|
+
signal,
|
|
1606
|
+
acquisition,
|
|
1607
|
+
);
|
|
1248
1608
|
}
|
|
1249
1609
|
|
|
1250
1610
|
export function preferredOAuthSelfRegistration(
|
|
@@ -1272,6 +1632,7 @@ async function getOrCreateDynamicClientRegistration(
|
|
|
1272
1632
|
redirectUri: string,
|
|
1273
1633
|
scopes: string[],
|
|
1274
1634
|
signal: AbortSignal,
|
|
1635
|
+
acquisition: Pick<OAuthStartContext, "accountId" | "workspaceId" | "integrationKey">,
|
|
1275
1636
|
): Promise<OAuthClientRegistration> {
|
|
1276
1637
|
const storedClient = await loadIntegrationOAuthClient(db, settings, as.issuer);
|
|
1277
1638
|
if (storedClient && storedDcrClientSatisfiesPolicy(storedClient, as, redirectUri, scopes)) {
|
|
@@ -1292,6 +1653,12 @@ async function getOrCreateDynamicClientRegistration(
|
|
|
1292
1653
|
message: "manual OAuth client credentials are required for this authorization server",
|
|
1293
1654
|
});
|
|
1294
1655
|
}
|
|
1656
|
+
await withOrganizationIntegrationAcquisition(
|
|
1657
|
+
db,
|
|
1658
|
+
acquisition,
|
|
1659
|
+
[acquisition.integrationKey ?? "custom:mcp"],
|
|
1660
|
+
async () => {},
|
|
1661
|
+
);
|
|
1295
1662
|
const dcr = await dynamicClientRegistration(settings, as, redirectUri, scopes, signal);
|
|
1296
1663
|
const key = dcr.clientSecret ? requireEnvironmentEncryption(settings) : null;
|
|
1297
1664
|
const storeInput = {
|
|
@@ -1303,34 +1670,43 @@ async function getOrCreateDynamicClientRegistration(
|
|
|
1303
1670
|
tokenEndpointAuthMethod: dcr.tokenEndpointAuthMethod,
|
|
1304
1671
|
metadata: registrationMetadata(as, redirectUri, scopes),
|
|
1305
1672
|
};
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1673
|
+
// Registration has already happened outside this transaction. Fence only the
|
|
1674
|
+
// durable client credential; a restriction racing DCR must not persist it.
|
|
1675
|
+
return withOrganizationIntegrationAcquisition(
|
|
1676
|
+
db,
|
|
1677
|
+
acquisition,
|
|
1678
|
+
[acquisition.integrationKey ?? "custom:mcp"],
|
|
1679
|
+
async (tx) => {
|
|
1680
|
+
if (storedClient) {
|
|
1681
|
+
const replaced = await replaceIntegrationOAuthClientIfCurrent(tx, {
|
|
1682
|
+
...storeInput,
|
|
1683
|
+
expectedClientId: storedClient.clientId,
|
|
1684
|
+
});
|
|
1685
|
+
if (replaced?.clientId === dcr.clientId) {
|
|
1686
|
+
return dcr;
|
|
1687
|
+
}
|
|
1688
|
+
return await loadCompatibleDcrWinner(tx, settings, as, redirectUri, scopes);
|
|
1689
|
+
}
|
|
1690
|
+
const storedWinner = await storeIntegrationOAuthClient(tx, storeInput);
|
|
1691
|
+
if (storedWinner.clientId === dcr.clientId) {
|
|
1692
|
+
return dcr;
|
|
1693
|
+
}
|
|
1694
|
+
const winner = await loadIntegrationOAuthClient(tx, settings, as.issuer);
|
|
1695
|
+
if (winner && storedDcrClientSatisfiesPolicy(winner, as, redirectUri, scopes)) {
|
|
1696
|
+
return dcrRegistrationFromStored(winner);
|
|
1697
|
+
}
|
|
1698
|
+
if (winner) {
|
|
1699
|
+
const replaced = await replaceIntegrationOAuthClientIfCurrent(tx, {
|
|
1700
|
+
...storeInput,
|
|
1701
|
+
expectedClientId: winner.clientId,
|
|
1702
|
+
});
|
|
1703
|
+
if (replaced?.clientId === dcr.clientId) {
|
|
1704
|
+
return dcr;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
return await loadCompatibleDcrWinner(tx, settings, as, redirectUri, scopes);
|
|
1708
|
+
},
|
|
1709
|
+
);
|
|
1334
1710
|
}
|
|
1335
1711
|
|
|
1336
1712
|
function storedDcrClientSatisfiesPolicy(
|
|
@@ -1436,7 +1812,6 @@ function operatorClientEntryFor(
|
|
|
1436
1812
|
settings: Settings,
|
|
1437
1813
|
candidates: string[],
|
|
1438
1814
|
): ReturnType<typeof parseIntegrationsOauthClientsJson>[string] | null {
|
|
1439
|
-
const normalizedCandidates = new Set(candidates.map(normalizedIssuerKey));
|
|
1440
1815
|
const candidateOrigins = candidates.flatMap((candidate) => {
|
|
1441
1816
|
try {
|
|
1442
1817
|
return [new URL(candidate).origin];
|
|
@@ -1453,22 +1828,37 @@ function operatorClientEntryFor(
|
|
|
1453
1828
|
return resolved;
|
|
1454
1829
|
}
|
|
1455
1830
|
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
candidates
|
|
1831
|
+
return findIntegrationsOauthClient(
|
|
1832
|
+
parseIntegrationsOauthClientsJson(settings.integrationsOauthClientsJson),
|
|
1833
|
+
candidates,
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
/** Secret-free readiness for the exact registered client used by Gmail setup. */
|
|
1838
|
+
export function gmailOAuthClientConfigured(settings: Settings): boolean {
|
|
1839
|
+
const entry = operatorClientEntryFor(settings, ["https://accounts.google.com"]);
|
|
1840
|
+
return Boolean(
|
|
1841
|
+
entry &&
|
|
1842
|
+
isConfidentialOAuthClient({
|
|
1843
|
+
...entry,
|
|
1844
|
+
tokenEndpointAuthMethod: tokenAuthMethod(
|
|
1845
|
+
entry.tokenEndpointAuthMethod,
|
|
1846
|
+
Boolean(entry.clientSecret),
|
|
1847
|
+
),
|
|
1848
|
+
}),
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
function isConfidentialOAuthClient(client: {
|
|
1853
|
+
clientId: string;
|
|
1854
|
+
clientSecret?: string | undefined;
|
|
1855
|
+
tokenEndpointAuthMethod: string;
|
|
1856
|
+
}): boolean {
|
|
1857
|
+
return Boolean(
|
|
1858
|
+
client.clientId.trim() &&
|
|
1859
|
+
client.clientSecret?.trim() &&
|
|
1860
|
+
["client_secret_post", "client_secret_basic"].includes(client.tokenEndpointAuthMethod),
|
|
1459
1861
|
);
|
|
1460
|
-
for (const key of exactKeys) {
|
|
1461
|
-
const entry = configured[key];
|
|
1462
|
-
if (entry) {
|
|
1463
|
-
return entry;
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
for (const [key, entry] of Object.entries(configured)) {
|
|
1467
|
-
if (normalizedCandidates.has(normalizedIssuerKey(key))) {
|
|
1468
|
-
return entry;
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
return null;
|
|
1472
1862
|
}
|
|
1473
1863
|
|
|
1474
1864
|
function normalizedIssuerKey(value: string): string {
|
|
@@ -1491,7 +1881,7 @@ async function dynamicClientRegistration(
|
|
|
1491
1881
|
method: "POST",
|
|
1492
1882
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
1493
1883
|
body: JSON.stringify({
|
|
1494
|
-
client_name: "
|
|
1884
|
+
client_name: "Opengeni",
|
|
1495
1885
|
redirect_uris: [redirectUri],
|
|
1496
1886
|
token_endpoint_auth_method: "none",
|
|
1497
1887
|
grant_types: ["authorization_code", "refresh_token"],
|
|
@@ -1542,10 +1932,21 @@ async function existingOAuthConnectionForStart(
|
|
|
1542
1932
|
connectionSelection: OAuthProviderProfile["connectionSelection"];
|
|
1543
1933
|
exactMcpBinding: boolean;
|
|
1544
1934
|
connectionId?: string | undefined;
|
|
1935
|
+
newAccount?: boolean;
|
|
1545
1936
|
requestedOwnership?: ConnectionOwnership | undefined;
|
|
1546
1937
|
newConnectionOwnership: ConnectionOwnership;
|
|
1547
1938
|
},
|
|
1548
1939
|
) {
|
|
1940
|
+
if (input.newAccount) {
|
|
1941
|
+
// A further account always mints its own row; it never refreshes (and so
|
|
1942
|
+
// never replaces) an account the caller already holds for this provider.
|
|
1943
|
+
if (input.connectionSelection === "canonical_personal") {
|
|
1944
|
+
throw new HTTPException(409, {
|
|
1945
|
+
message: "this connector supports one account per person; reconnect it instead",
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
return null;
|
|
1949
|
+
}
|
|
1549
1950
|
if (input.connectionId) {
|
|
1550
1951
|
const connection = await getConnectionMetadata(
|
|
1551
1952
|
db,
|
|
@@ -1633,6 +2034,41 @@ export function buildAuthorizationUrl(input: {
|
|
|
1633
2034
|
return url.toString();
|
|
1634
2035
|
}
|
|
1635
2036
|
|
|
2037
|
+
async function resolveMcpOAuthState(
|
|
2038
|
+
db: Database,
|
|
2039
|
+
settings: Settings,
|
|
2040
|
+
state: string,
|
|
2041
|
+
deadline: OAuthCallbackDeadline,
|
|
2042
|
+
): Promise<OAuthStatePayload> {
|
|
2043
|
+
const reference = readSignedState(state, requireIntegrationsStateSecret(settings)) as Record<
|
|
2044
|
+
string,
|
|
2045
|
+
unknown
|
|
2046
|
+
> | null;
|
|
2047
|
+
if (reference?.kind !== "mcp_oauth_reference") {
|
|
2048
|
+
// In-flight grants from the previous release remain valid for their
|
|
2049
|
+
// normal ten-minute lifetime during a rolling deployment.
|
|
2050
|
+
return readOAuthState(state, settings);
|
|
2051
|
+
}
|
|
2052
|
+
const accountId = requiredString(reference.accountId, "state.accountId");
|
|
2053
|
+
const workspaceId = requiredString(reference.workspaceId, "state.workspaceId");
|
|
2054
|
+
const id = requiredString(reference.id, "state.id");
|
|
2055
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id)) {
|
|
2056
|
+
throw new HTTPException(400, { message: "invalid OAuth state reference" });
|
|
2057
|
+
}
|
|
2058
|
+
const encrypted = await runCallbackDatabaseStage(deadline, "state_verify", db, (scopedDb) =>
|
|
2059
|
+
loadIntegrationOAuthPendingState(scopedDb, { accountId, workspaceId, id }),
|
|
2060
|
+
);
|
|
2061
|
+
if (!encrypted) {
|
|
2062
|
+
throw new HTTPException(400, { message: "invalid or expired OAuth state" });
|
|
2063
|
+
}
|
|
2064
|
+
const fullState = decryptEnvironmentValue(requireEnvironmentEncryption(settings), encrypted);
|
|
2065
|
+
const resolved = readOAuthState(fullState, settings);
|
|
2066
|
+
if (resolved.accountId !== accountId || resolved.workspaceId !== workspaceId) {
|
|
2067
|
+
throw new HTTPException(400, { message: "OAuth state reference mismatch" });
|
|
2068
|
+
}
|
|
2069
|
+
return resolved;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
1636
2072
|
function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
|
|
1637
2073
|
const payload = readSignedState(state, requireIntegrationsStateSecret(settings)) as Record<
|
|
1638
2074
|
string,
|
|
@@ -1641,6 +2077,13 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
|
|
|
1641
2077
|
if (!payload) {
|
|
1642
2078
|
throw new HTTPException(400, { message: "invalid or expired OAuth state" });
|
|
1643
2079
|
}
|
|
2080
|
+
if (
|
|
2081
|
+
payload.integrationKey !== undefined &&
|
|
2082
|
+
payload.integrationKey !== "gmail" &&
|
|
2083
|
+
payload.integrationKey !== "slack-personal"
|
|
2084
|
+
) {
|
|
2085
|
+
throw new HTTPException(400, { message: "invalid OAuth integration identity" });
|
|
2086
|
+
}
|
|
1644
2087
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
1645
2088
|
const iat = numberValue(payload.iat);
|
|
1646
2089
|
if (iat === undefined || nowSeconds - iat > oauthStateTtlMs / 1000 || nowSeconds < iat) {
|
|
@@ -1662,7 +2105,7 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
|
|
|
1662
2105
|
encodedDiscoveryMode &&
|
|
1663
2106
|
(!authorizationServerMetadataUrl ||
|
|
1664
2107
|
(discoveryMode === "rfc9728_protected_resource" && !protectedResourceMetadataUrl) ||
|
|
1665
|
-
(discoveryMode
|
|
2108
|
+
(discoveryMode !== "rfc9728_protected_resource" && protectedResourceMetadataUrl))
|
|
1666
2109
|
) {
|
|
1667
2110
|
throw new HTTPException(400, { message: "invalid OAuth discovery provenance state" });
|
|
1668
2111
|
}
|
|
@@ -1670,6 +2113,12 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
|
|
|
1670
2113
|
if (discoveryMode === "legacy_2025_03_26_metadata" && resourceParameterSupported) {
|
|
1671
2114
|
throw new HTTPException(400, { message: "invalid legacy OAuth resource parameter state" });
|
|
1672
2115
|
}
|
|
2116
|
+
const ownership = connectionOwnership(payload.ownership);
|
|
2117
|
+
if (!ownership) {
|
|
2118
|
+
throw new HTTPException(400, {
|
|
2119
|
+
message: "missing or invalid OAuth ownership; restart connection setup",
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
1673
2122
|
const parsed = {
|
|
1674
2123
|
accountId: requiredString(payload.accountId, "state.accountId"),
|
|
1675
2124
|
workspaceId: requiredString(payload.workspaceId, "state.workspaceId"),
|
|
@@ -1692,11 +2141,12 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
|
|
|
1692
2141
|
...(stringValue(payload.connectAttemptId)
|
|
1693
2142
|
? { connectAttemptId: requiredString(payload.connectAttemptId, "state.connectAttemptId") }
|
|
1694
2143
|
: {}),
|
|
1695
|
-
|
|
1696
|
-
// Preserve that meaning for in-flight reconnects during a rolling deploy.
|
|
1697
|
-
ownership: connectionOwnership(payload.ownership) ?? "personal",
|
|
2144
|
+
ownership,
|
|
1698
2145
|
// Absent on a legacy state, which therefore cannot land a personal owner.
|
|
1699
2146
|
personalOwnerVerified: personalOwnerVerifiedInState(payload),
|
|
2147
|
+
...(payload.integrationKey
|
|
2148
|
+
? { integrationKey: payload.integrationKey as "gmail" | "slack-personal" }
|
|
2149
|
+
: {}),
|
|
1700
2150
|
providerDomain: requiredString(payload.providerDomain, "state.providerDomain"),
|
|
1701
2151
|
mcpUrl: stringValue(payload.mcpUrl) ?? resource,
|
|
1702
2152
|
resource,
|
|
@@ -1756,6 +2206,7 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
|
|
|
1756
2206
|
};
|
|
1757
2207
|
const connectionId = stringValue(payload.connectionId);
|
|
1758
2208
|
const connectionVersion = numberValue(payload.connectionVersion);
|
|
2209
|
+
assertProfileOwnership(builtInOAuthProfileFor(parsed), parsed.ownership);
|
|
1759
2210
|
if (Boolean(connectionId) !== Boolean(connectionVersion)) {
|
|
1760
2211
|
throw new HTTPException(400, { message: "invalid OAuth reconnect state" });
|
|
1761
2212
|
}
|
|
@@ -1766,24 +2217,61 @@ function readOAuthState(state: string, settings: Settings): OAuthStatePayload {
|
|
|
1766
2217
|
) {
|
|
1767
2218
|
throw new HTTPException(400, { message: "invalid OAuth discovery binding state" });
|
|
1768
2219
|
}
|
|
2220
|
+
if (parsed.discoveryMode === "provider_oauth_metadata") {
|
|
2221
|
+
const profile = builtInOAuthProfileFor(parsed);
|
|
2222
|
+
const discovery = profile?.providerOAuthDiscovery;
|
|
2223
|
+
if (
|
|
2224
|
+
!discovery ||
|
|
2225
|
+
parsed.mcpUrl !== profile?.requireExactMcpUrl?.url ||
|
|
2226
|
+
parsed.resource !== parsed.mcpUrl ||
|
|
2227
|
+
parsed.providerDomain !== profile?.postDiscoveryProviderDomain?.domain ||
|
|
2228
|
+
normalizedIssuerKey(parsed.issuer) !== discovery.issuer ||
|
|
2229
|
+
parsed.authorizationServer !== discovery.issuer ||
|
|
2230
|
+
parsed.authorizationServerMetadataUrl !== discovery.metadataUrl ||
|
|
2231
|
+
parsed.resourceParameterSupported
|
|
2232
|
+
) {
|
|
2233
|
+
throw new HTTPException(400, { message: "invalid provider OAuth discovery binding state" });
|
|
2234
|
+
}
|
|
2235
|
+
assertDiscoveredAuthorizationServer(
|
|
2236
|
+
settings,
|
|
2237
|
+
{ ...parsed, authorizationEndpoint: discovery.issuer },
|
|
2238
|
+
profile!,
|
|
2239
|
+
parsed.providerDomain,
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
1769
2242
|
return {
|
|
1770
2243
|
...parsed,
|
|
2244
|
+
...(parsed.discoveryMode === "provider_oauth_metadata"
|
|
2245
|
+
? { issuer: normalizedIssuerKey(parsed.issuer) }
|
|
2246
|
+
: {}),
|
|
1771
2247
|
...(connectionId ? { connectionId } : {}),
|
|
1772
2248
|
...(connectionVersion !== undefined ? { connectionVersion } : {}),
|
|
1773
2249
|
};
|
|
1774
2250
|
}
|
|
1775
2251
|
|
|
1776
|
-
function discoveryModeValue(value: string | undefined):
|
|
2252
|
+
function discoveryModeValue(value: string | undefined): OAuthDiscoveryMode {
|
|
1777
2253
|
if (!value) {
|
|
1778
2254
|
// Every state minted before discovery modes existed used RFC 9728 PRM.
|
|
1779
2255
|
return "rfc9728_protected_resource";
|
|
1780
2256
|
}
|
|
1781
|
-
if (
|
|
2257
|
+
if (
|
|
2258
|
+
value === "rfc9728_protected_resource" ||
|
|
2259
|
+
value === "legacy_2025_03_26_metadata" ||
|
|
2260
|
+
value === "provider_oauth_metadata"
|
|
2261
|
+
) {
|
|
1782
2262
|
return value;
|
|
1783
2263
|
}
|
|
1784
2264
|
throw new HTTPException(400, { message: "invalid OAuth discovery mode state" });
|
|
1785
2265
|
}
|
|
1786
2266
|
|
|
2267
|
+
function assertProfileOwnership(
|
|
2268
|
+
profile: OAuthProviderProfile | null,
|
|
2269
|
+
ownership: ConnectionOwnership,
|
|
2270
|
+
): void {
|
|
2271
|
+
if (profile?.requiredOwnership && ownership !== profile.requiredOwnership.ownership)
|
|
2272
|
+
throw new HTTPException(422, { message: profile.requiredOwnership.message });
|
|
2273
|
+
}
|
|
2274
|
+
|
|
1787
2275
|
function connectionOwnership(value: unknown): ConnectionOwnership | undefined {
|
|
1788
2276
|
return value === "workspace" || value === "personal" ? value : undefined;
|
|
1789
2277
|
}
|
|
@@ -2071,6 +2559,8 @@ function oauthStartStageLabel(stage: OAuthStartStage): string {
|
|
|
2071
2559
|
return "authorization-server discovery";
|
|
2072
2560
|
case "client_registration":
|
|
2073
2561
|
return "OAuth client registration";
|
|
2562
|
+
case "state_persist":
|
|
2563
|
+
return "OAuth state persistence";
|
|
2074
2564
|
}
|
|
2075
2565
|
}
|
|
2076
2566
|
|
|
@@ -2196,11 +2686,45 @@ async function verifyMcpToolsListNonFatal(
|
|
|
2196
2686
|
| { status: "ok"; checkedAt: string; toolCount: number }
|
|
2197
2687
|
| { status: "failed"; checkedAt: string; reason: string };
|
|
2198
2688
|
tools?: Array<{ name: string; description?: string }>;
|
|
2689
|
+
providerIdentity?: Record<string, string>;
|
|
2690
|
+
connectionStatus?: "needs_reauth";
|
|
2199
2691
|
}> {
|
|
2692
|
+
let grantRejected = false;
|
|
2200
2693
|
try {
|
|
2201
|
-
const
|
|
2202
|
-
|
|
2203
|
-
|
|
2694
|
+
const profile = builtInOAuthProfileFor(state);
|
|
2695
|
+
const local = profile?.localToolVerification;
|
|
2696
|
+
let providerIdentity: Record<string, string> | undefined;
|
|
2697
|
+
const tools = await deadline.run("tools_list", async (signal) => {
|
|
2698
|
+
if (!local || state.mcpUrl !== profile?.requireExactMcpUrl?.url) {
|
|
2699
|
+
return verifyMcpToolsList(settings, state.mcpUrl, token, signal);
|
|
2700
|
+
}
|
|
2701
|
+
const response = await fetchOAuth(local.url, settings, {
|
|
2702
|
+
method: local.method ?? "POST",
|
|
2703
|
+
signal,
|
|
2704
|
+
headers: {
|
|
2705
|
+
authorization: `${normalizeBearerScheme(token.tokenType)} ${token.accessToken}`,
|
|
2706
|
+
},
|
|
2707
|
+
});
|
|
2708
|
+
if (!response.ok) {
|
|
2709
|
+
await cancelResponseBody(response);
|
|
2710
|
+
throw new Error("Connector token verification failed");
|
|
2711
|
+
}
|
|
2712
|
+
const payload = await readResponseJsonBounded<Record<string, unknown>>(
|
|
2713
|
+
response,
|
|
2714
|
+
OAUTH_MAX_RESPONSE_BYTES,
|
|
2715
|
+
"Connector token verification",
|
|
2716
|
+
{ signal },
|
|
2717
|
+
);
|
|
2718
|
+
grantRejected = local.isRejectedGrant?.(payload) ?? false;
|
|
2719
|
+
providerIdentity = local.validateIdentity(payload);
|
|
2720
|
+
const verifiedTools = local.toolsForScopes(
|
|
2721
|
+
grantedScopes(token.scopeText, state.authorizeScopes, profile),
|
|
2722
|
+
settings,
|
|
2723
|
+
);
|
|
2724
|
+
if (local.required && verifiedTools.length === 0)
|
|
2725
|
+
throw new Error("Connector verification did not report an authorized tool scope");
|
|
2726
|
+
return verifiedTools;
|
|
2727
|
+
});
|
|
2204
2728
|
return {
|
|
2205
2729
|
metadata: {
|
|
2206
2730
|
status: "ok",
|
|
@@ -2208,6 +2732,7 @@ async function verifyMcpToolsListNonFatal(
|
|
|
2208
2732
|
toolCount: tools.length,
|
|
2209
2733
|
},
|
|
2210
2734
|
tools,
|
|
2735
|
+
...(providerIdentity ? { providerIdentity } : {}),
|
|
2211
2736
|
};
|
|
2212
2737
|
} catch (error) {
|
|
2213
2738
|
const staged =
|
|
@@ -2217,6 +2742,7 @@ async function verifyMcpToolsListNonFatal(
|
|
|
2217
2742
|
if (staged.reason === "timeout" && deadline.signal.aborted) {
|
|
2218
2743
|
throw staged;
|
|
2219
2744
|
}
|
|
2745
|
+
if (builtInOAuthProfileFor(state)?.localToolVerification?.required) throw staged;
|
|
2220
2746
|
logOAuthVerificationWarning(observability, staged, state);
|
|
2221
2747
|
return {
|
|
2222
2748
|
metadata: {
|
|
@@ -2224,6 +2750,7 @@ async function verifyMcpToolsListNonFatal(
|
|
|
2224
2750
|
checkedAt: new Date().toISOString(),
|
|
2225
2751
|
reason: staged.reason,
|
|
2226
2752
|
},
|
|
2753
|
+
...(grantRejected ? { connectionStatus: "needs_reauth" as const } : {}),
|
|
2227
2754
|
};
|
|
2228
2755
|
}
|
|
2229
2756
|
}
|
|
@@ -2270,7 +2797,7 @@ function callbackReturnPath(
|
|
|
2270
2797
|
// Defense in depth: a `//host` pathname becomes a protocol-relative absolute
|
|
2271
2798
|
// Location — an open redirect from the unauthenticated callback.
|
|
2272
2799
|
if (url.pathname.startsWith("//")) {
|
|
2273
|
-
const fallback = new URL(
|
|
2800
|
+
const fallback = new URL(INTEGRATIONS_FALLBACK_PATH, "https://opengeni.local");
|
|
2274
2801
|
fallback.search = url.search;
|
|
2275
2802
|
return `${fallback.pathname}${fallback.search}`;
|
|
2276
2803
|
}
|
|
@@ -2417,6 +2944,7 @@ export function chooseMcpAuthorizeScopes(input: {
|
|
|
2417
2944
|
requested: string[] | undefined;
|
|
2418
2945
|
challenged: string[] | undefined;
|
|
2419
2946
|
supported: string[];
|
|
2947
|
+
authorizationServerScopesSupported?: string[];
|
|
2420
2948
|
}): string[] {
|
|
2421
2949
|
const profile = builtInOAuthProfileFor({ mcpUrl: input.mcpUrl }) ?? DEFAULT_OAUTH_PROFILE;
|
|
2422
2950
|
return chooseProfileAuthorizeScopes(profile, input);
|
|
@@ -2428,14 +2956,26 @@ function chooseProfileAuthorizeScopes(
|
|
|
2428
2956
|
requested: string[] | undefined;
|
|
2429
2957
|
challenged: string[] | undefined;
|
|
2430
2958
|
supported: string[];
|
|
2959
|
+
authorizationServerScopesSupported?: string[];
|
|
2431
2960
|
},
|
|
2432
2961
|
): string[] {
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2962
|
+
// Reviewed profiles keep their exact scope contract. A generic long-running
|
|
2963
|
+
// connector also needs the AS-advertised refresh grant: the MCP resource's
|
|
2964
|
+
// challenge often advertises only its resource permission (e.g. mcp:use).
|
|
2965
|
+
if (profile.requestedScopes) return [...profile.requestedScopes];
|
|
2966
|
+
const scopes = chooseAuthorizeScopes(input.requested, input.challenged, input.supported);
|
|
2967
|
+
return input.authorizationServerScopesSupported?.includes("offline_access")
|
|
2968
|
+
? uniqueStrings([...scopes, "offline_access"])
|
|
2969
|
+
: scopes;
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
function grantedScopes(
|
|
2973
|
+
scopeText: string | undefined,
|
|
2974
|
+
fallback: string[],
|
|
2975
|
+
profile?: OAuthProviderProfile | null,
|
|
2976
|
+
): string[] {
|
|
2977
|
+
if (profile?.reportedScopesRequired && !scopeText) return [];
|
|
2978
|
+
if (profile?.normalizeGrantedScopes) return profile.normalizeGrantedScopes(scopeText ?? fallback);
|
|
2439
2979
|
if (scopeText) {
|
|
2440
2980
|
return uniqueStrings(scopeText.split(/\s+/).filter(Boolean));
|
|
2441
2981
|
}
|