@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
package/src/mcp-oauth.ts
CHANGED
|
@@ -9,7 +9,11 @@ import {
|
|
|
9
9
|
McpOAuthClientRegistrationRequest,
|
|
10
10
|
McpOAuthClientRegistrationResponse,
|
|
11
11
|
McpOAuthProtectedResourceMetadata,
|
|
12
|
+
McpConnectionDecision,
|
|
12
13
|
McpOAuthTokenResponse,
|
|
14
|
+
OrganizationAccessPolicy,
|
|
15
|
+
organizationAccessPresetPermissions,
|
|
16
|
+
type AccessContext,
|
|
13
17
|
type AccessGrant,
|
|
14
18
|
type ToolGatewayCatalog,
|
|
15
19
|
type ToolGatewayIdentity,
|
|
@@ -19,18 +23,32 @@ import {
|
|
|
19
23
|
createMcpOAuthAuthorizationRequest,
|
|
20
24
|
deleteMcpOAuthAuthorizationRequest,
|
|
21
25
|
exchangeMcpOAuthAuthorizationCode,
|
|
26
|
+
getManagedAccount,
|
|
22
27
|
getMcpOAuthAuthorizationRequest,
|
|
23
28
|
getMcpOAuthClient,
|
|
29
|
+
getWorkspace,
|
|
30
|
+
listSharedWorkspacesForAccount,
|
|
31
|
+
listWorkspacesForSubject,
|
|
24
32
|
McpOAuthClientRegistrationRateLimitError,
|
|
33
|
+
rebindMcpOAuthAuthorizationRequest,
|
|
25
34
|
registerMcpOAuthClient,
|
|
26
35
|
resolveLiveMcpOAuthGrant,
|
|
27
36
|
resolveMcpOAuthAccessToken,
|
|
28
37
|
rotateMcpOAuthRefreshToken,
|
|
38
|
+
setMcpOAuthRequestOrganizationAccess,
|
|
39
|
+
type McpOAuthAccess,
|
|
29
40
|
} from "@opengeni/db";
|
|
30
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
hasPermission,
|
|
43
|
+
requireAccessContext,
|
|
44
|
+
requireAccessGrantAuthorization,
|
|
45
|
+
type ApiRouteDeps,
|
|
46
|
+
} from "@opengeni/core";
|
|
31
47
|
import { Hono, type Context } from "hono";
|
|
32
48
|
import { HTTPException } from "hono/http-exception";
|
|
33
|
-
import {
|
|
49
|
+
import { trustedRequestSourceRateLimitKey, type RequestSourceTrust } from "./http/request-source";
|
|
50
|
+
import { isAgentActingAsPerson, requireFullAccessIfAgent } from "./http/acting-person";
|
|
51
|
+
import { requireSameOriginBrowserMutation } from "./routes/codex";
|
|
34
52
|
import {
|
|
35
53
|
prepareWorkspaceToolGateway,
|
|
36
54
|
requireWorkspaceToolGatewayAuthorization,
|
|
@@ -45,10 +63,14 @@ const PKCE_VERIFIER = /^[A-Za-z0-9._~-]{43,128}$/u;
|
|
|
45
63
|
const PKCE_CHALLENGE = /^[A-Za-z0-9_-]{43}$/u;
|
|
46
64
|
const WORKSPACE_MCP_PATH = /^\/v1\/workspaces\/([0-9a-f-]{36})\/mcp(?:\/(docs|files))?$/u;
|
|
47
65
|
|
|
66
|
+
/** One organization MCP server for everyone; the sign-in chooses the organization. */
|
|
67
|
+
export const ORGANIZATION_MCP_PATH = "/v1/mcp";
|
|
68
|
+
|
|
48
69
|
export type McpOAuthResource = {
|
|
49
70
|
resource: string;
|
|
50
|
-
|
|
51
|
-
|
|
71
|
+
/** Null for the organization server. */
|
|
72
|
+
workspaceId: string | null;
|
|
73
|
+
kind: "all" | "docs" | "files" | "organization";
|
|
52
74
|
};
|
|
53
75
|
|
|
54
76
|
export type McpOAuthRouteAccess = {
|
|
@@ -57,7 +79,10 @@ export type McpOAuthRouteAccess = {
|
|
|
57
79
|
};
|
|
58
80
|
|
|
59
81
|
export function registerMcpOAuthRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
60
|
-
|
|
82
|
+
// RFC 8414 path-aware discovery: MCP clients (Claude among them) ask for the
|
|
83
|
+
// metadata at /.well-known/oauth-authorization-server/<resource path> first
|
|
84
|
+
// and give up on anything but JSON. Serve the same document at both.
|
|
85
|
+
const authorizationServerMetadata = (c: Context) => {
|
|
61
86
|
requireMcpOAuthEnabled(deps);
|
|
62
87
|
const issuer = mcpOAuthIssuer(deps);
|
|
63
88
|
c.header("cache-control", "public, max-age=300");
|
|
@@ -75,7 +100,9 @@ export function registerMcpOAuthRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
75
100
|
authorization_response_iss_parameter_supported: true,
|
|
76
101
|
}),
|
|
77
102
|
);
|
|
78
|
-
}
|
|
103
|
+
};
|
|
104
|
+
app.get("/.well-known/oauth-authorization-server", authorizationServerMetadata);
|
|
105
|
+
app.get("/.well-known/oauth-authorization-server/*", authorizationServerMetadata);
|
|
79
106
|
|
|
80
107
|
app.get("/.well-known/oauth-protected-resource/*", (c) => {
|
|
81
108
|
requireMcpOAuthEnabled(deps);
|
|
@@ -115,9 +142,7 @@ export function registerMcpOAuthRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
115
142
|
clientName: parsed.data.client_name ?? null,
|
|
116
143
|
grantTypes: parsed.data.grant_types,
|
|
117
144
|
responseTypes: ["code"],
|
|
118
|
-
registrationScopeHash: tokenHash(
|
|
119
|
-
mcpOAuthRegistrationClientKey(c, deps.settings.mcpOauthTrustedProxyHops),
|
|
120
|
-
),
|
|
145
|
+
registrationScopeHash: tokenHash(mcpOAuthRegistrationClientKey(c, deps.settings)),
|
|
121
146
|
});
|
|
122
147
|
} catch (error) {
|
|
123
148
|
if (error instanceof McpOAuthClientRegistrationRateLimitError) {
|
|
@@ -148,10 +173,62 @@ export function registerMcpOAuthRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
148
173
|
const query = new URL(c.req.url).searchParams;
|
|
149
174
|
const client = await requireAuthorizationClient(deps, query);
|
|
150
175
|
const resource = requireAuthorizationResource(deps, query);
|
|
176
|
+
if (resource.kind === "organization") {
|
|
177
|
+
// The person picks the organization, what the agent can do and where on
|
|
178
|
+
// the web app's sign-in page, which reads and answers this request.
|
|
179
|
+
const web = new URL(deps.settings.webBaseUrl ?? mcpOAuthIssuer(deps));
|
|
180
|
+
let person: { context: AccessContext };
|
|
181
|
+
try {
|
|
182
|
+
person = await requireBrowserPerson(c, deps);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (!(error instanceof HTTPException) || error.status !== 401) throw error;
|
|
185
|
+
// Not signed in here yet: sign in on the web app, which returns to
|
|
186
|
+
// this exact authorization request (a same-origin path only).
|
|
187
|
+
const url = new URL(c.req.url);
|
|
188
|
+
c.header("cache-control", "no-store");
|
|
189
|
+
return c.redirect(
|
|
190
|
+
`${web.origin}/connect-agent?authorize=${encodeURIComponent(`${url.pathname}${url.search}`)}`,
|
|
191
|
+
302,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
const accountId =
|
|
195
|
+
person.context.defaultAccountId ?? person.context.accountGrants[0]?.accountId;
|
|
196
|
+
if (!accountId) {
|
|
197
|
+
throw new HTTPException(403, { message: "no organization is available for MCP OAuth" });
|
|
198
|
+
}
|
|
199
|
+
const requestToken = opaque(REQUEST_PREFIX);
|
|
200
|
+
await createMcpOAuthAuthorizationRequest(deps.db, {
|
|
201
|
+
requestHash: tokenHash(requestToken),
|
|
202
|
+
clientId: client.clientId,
|
|
203
|
+
accountId,
|
|
204
|
+
workspaceId: null,
|
|
205
|
+
subjectId: person.context.subjectId,
|
|
206
|
+
resource: resource.resource,
|
|
207
|
+
redirectUri: requireRedirectUri(client.redirectUris, query.get("redirect_uri")),
|
|
208
|
+
codeChallenge: requireCodeChallenge(query),
|
|
209
|
+
state: boundedState(query.get("state")),
|
|
210
|
+
permissions: [],
|
|
211
|
+
toolIdentities: [],
|
|
212
|
+
organizationAccess: DEFAULT_ORGANIZATION_ACCESS(),
|
|
213
|
+
expiresAt: expiresIn(MCP_OAUTH_CONSENT_TTL_SECONDS),
|
|
214
|
+
});
|
|
215
|
+
c.header("cache-control", "no-store");
|
|
216
|
+
return c.redirect(
|
|
217
|
+
`${web.origin}/connect-agent?request=${encodeURIComponent(requestToken)}`,
|
|
218
|
+
302,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const context = await requireAccessContext(c, deps);
|
|
222
|
+
const workspaces = await listConsentWorkspaces(deps, context);
|
|
223
|
+
if (workspaces.length === 0) {
|
|
224
|
+
throw new HTTPException(403, { message: "no workspace is available for MCP OAuth" });
|
|
225
|
+
}
|
|
226
|
+
const selectedWorkspace =
|
|
227
|
+
workspaces.find((workspace) => workspace.id === resource.workspaceId) ?? workspaces[0]!;
|
|
151
228
|
const authorization = await requireAccessGrantAuthorization(
|
|
152
229
|
c,
|
|
153
230
|
deps,
|
|
154
|
-
|
|
231
|
+
selectedWorkspace.id,
|
|
155
232
|
"workspace:read",
|
|
156
233
|
);
|
|
157
234
|
const grant = requireWorkspaceToolGatewayAuthorization(authorization);
|
|
@@ -170,47 +247,112 @@ export function registerMcpOAuthRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
170
247
|
state: boundedState(query.get("state")),
|
|
171
248
|
permissions: grant.permissions,
|
|
172
249
|
toolIdentities: mcpOAuthConsentToolIdentities(prepared.toolGatewayCatalog),
|
|
250
|
+
organizationAccess: null,
|
|
173
251
|
expiresAt: expiresIn(MCP_OAUTH_CONSENT_TTL_SECONDS),
|
|
174
252
|
});
|
|
175
253
|
} finally {
|
|
176
254
|
await prepared.close();
|
|
177
255
|
}
|
|
256
|
+
const accounts = await consentAccountsForWorkspaces(deps, workspaces);
|
|
178
257
|
c.header(
|
|
179
258
|
"content-security-policy",
|
|
180
|
-
"default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'",
|
|
259
|
+
"default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'",
|
|
181
260
|
);
|
|
182
261
|
c.header("cache-control", "no-store");
|
|
183
|
-
return c.html(
|
|
262
|
+
return c.html(
|
|
263
|
+
renderMcpOAuthConsentPage({
|
|
264
|
+
clientName: client.clientName ?? "MCP client",
|
|
265
|
+
requestToken,
|
|
266
|
+
accounts,
|
|
267
|
+
workspaces: workspaces.map((workspace) => ({
|
|
268
|
+
id: workspace.id,
|
|
269
|
+
accountId: workspace.accountId,
|
|
270
|
+
name: workspace.name,
|
|
271
|
+
kind: workspace.kind,
|
|
272
|
+
})),
|
|
273
|
+
selectedWorkspaceId: grant.workspaceId,
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
184
276
|
});
|
|
185
277
|
|
|
186
278
|
app.post("/oauth/authorize", async (c) => {
|
|
187
279
|
requireMcpOAuthEnabled(deps);
|
|
188
280
|
const form = new URLSearchParams(await c.req.text());
|
|
189
281
|
const requestToken = form.get("request") ?? "";
|
|
190
|
-
if (!requestToken.startsWith(REQUEST_PREFIX))
|
|
282
|
+
if (!requestToken.startsWith(REQUEST_PREFIX)) {
|
|
283
|
+
return oauthAuthorizeBrowserError(
|
|
284
|
+
c,
|
|
285
|
+
"This authorization request is missing or already used.",
|
|
286
|
+
);
|
|
287
|
+
}
|
|
191
288
|
const request = await getMcpOAuthAuthorizationRequest(deps.db, tokenHash(requestToken));
|
|
192
|
-
if (!request)
|
|
289
|
+
if (!request) {
|
|
290
|
+
return oauthAuthorizeBrowserError(
|
|
291
|
+
c,
|
|
292
|
+
"This authorization request expired or was already approved.",
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
// Organization sign-ins are answered on the Opengeni page they opened.
|
|
296
|
+
if (request.workspaceId === null) {
|
|
297
|
+
return oauthAuthorizeBrowserError(c, "Answer this sign-in on the Opengeni page it opened.");
|
|
298
|
+
}
|
|
299
|
+
const requestWorkspaceId = request.workspaceId;
|
|
300
|
+
if (form.get("decision") !== "approve") {
|
|
301
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
302
|
+
c,
|
|
303
|
+
deps,
|
|
304
|
+
requestWorkspaceId,
|
|
305
|
+
"workspace:read",
|
|
306
|
+
);
|
|
307
|
+
if (requireWorkspaceToolGatewayAuthorization(authorization).subjectId !== request.subjectId) {
|
|
308
|
+
throw new HTTPException(403, { message: "OAuth consent authority changed" });
|
|
309
|
+
}
|
|
310
|
+
await deleteMcpOAuthAuthorizationRequest(deps.db, request.requestHash);
|
|
311
|
+
return completeAuthorizationRedirect(
|
|
312
|
+
c,
|
|
313
|
+
authorizationRedirect(request.redirectUri, {
|
|
314
|
+
error: "access_denied",
|
|
315
|
+
iss: mcpOAuthIssuer(deps),
|
|
316
|
+
...(request.state ? { state: request.state } : {}),
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const selectedWorkspaceId = form.get("workspace_id") || requestWorkspaceId;
|
|
321
|
+
if (!isWorkspaceId(selectedWorkspaceId)) {
|
|
322
|
+
return oauthAuthorizeBrowserError(c, "Choose a workspace before authorizing this client.");
|
|
323
|
+
}
|
|
193
324
|
const authorization = await requireAccessGrantAuthorization(
|
|
194
325
|
c,
|
|
195
326
|
deps,
|
|
196
|
-
|
|
327
|
+
selectedWorkspaceId,
|
|
197
328
|
"workspace:read",
|
|
198
329
|
);
|
|
199
330
|
const grant = requireWorkspaceToolGatewayAuthorization(authorization);
|
|
200
|
-
if (grant.subjectId !== request.subjectId
|
|
331
|
+
if (grant.subjectId !== request.subjectId) {
|
|
201
332
|
throw new HTTPException(403, {
|
|
202
333
|
message: "OAuth consent authority changed",
|
|
203
334
|
});
|
|
204
335
|
}
|
|
205
|
-
if (
|
|
206
|
-
await
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
336
|
+
if (grant.workspaceId !== request.workspaceId || grant.accountId !== request.accountId) {
|
|
337
|
+
const prepared = await prepareWorkspaceToolGateway(deps, authorization);
|
|
338
|
+
try {
|
|
339
|
+
const rebound = await rebindMcpOAuthAuthorizationRequest(deps.db, {
|
|
340
|
+
requestHash: request.requestHash,
|
|
341
|
+
subjectId: grant.subjectId,
|
|
342
|
+
accountId: grant.accountId,
|
|
343
|
+
workspaceId: grant.workspaceId,
|
|
344
|
+
permissions: grant.permissions,
|
|
345
|
+
toolIdentities: mcpOAuthConsentToolIdentities(prepared.toolGatewayCatalog),
|
|
346
|
+
});
|
|
347
|
+
if (!rebound) {
|
|
348
|
+
return oauthAuthorizeBrowserError(
|
|
349
|
+
c,
|
|
350
|
+
"This authorization request expired or was already approved.",
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
} finally {
|
|
354
|
+
await prepared.close();
|
|
355
|
+
}
|
|
214
356
|
}
|
|
215
357
|
const code = opaque(CODE_PREFIX);
|
|
216
358
|
const consumed = await consumeMcpOAuthAuthorizationRequest(deps.db, {
|
|
@@ -219,8 +361,14 @@ export function registerMcpOAuthRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
219
361
|
codeHash: tokenHash(code),
|
|
220
362
|
codeExpiresAt: expiresIn(MCP_OAUTH_AUTHORIZATION_CODE_TTL_SECONDS),
|
|
221
363
|
});
|
|
222
|
-
if (!consumed)
|
|
223
|
-
|
|
364
|
+
if (!consumed) {
|
|
365
|
+
return oauthAuthorizeBrowserError(
|
|
366
|
+
c,
|
|
367
|
+
"This authorization request expired or was already approved.",
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
return completeAuthorizationRedirect(
|
|
371
|
+
c,
|
|
224
372
|
authorizationRedirect(consumed.redirectUri, {
|
|
225
373
|
code,
|
|
226
374
|
iss: mcpOAuthIssuer(deps),
|
|
@@ -229,6 +377,83 @@ export function registerMcpOAuthRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
229
377
|
);
|
|
230
378
|
});
|
|
231
379
|
|
|
380
|
+
// The web sign-in page for the organization server reads the pending request…
|
|
381
|
+
app.get("/v1/mcp-connections/requests/:request", async (c) => {
|
|
382
|
+
requireMcpOAuthEnabled(deps);
|
|
383
|
+
const person = await requireBrowserPerson(c, deps);
|
|
384
|
+
const request = await requirePendingOrganizationRequest(deps, c.req.param("request"), person);
|
|
385
|
+
const client = await getMcpOAuthClient(deps.db, request.clientId);
|
|
386
|
+
const organizations = await consentOrganizations(deps, person.context);
|
|
387
|
+
c.header("cache-control", "no-store");
|
|
388
|
+
return c.json({
|
|
389
|
+
client: {
|
|
390
|
+
name: client?.clientName ?? "MCP client",
|
|
391
|
+
host: redirectHost(request.redirectUri),
|
|
392
|
+
},
|
|
393
|
+
person: { name: person.context.subjectLabel ?? null },
|
|
394
|
+
organizations,
|
|
395
|
+
defaultOrganizationId: organizations.some((each) => each.id === request.accountId)
|
|
396
|
+
? request.accountId
|
|
397
|
+
: (organizations[0]?.id ?? null),
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// …and answers it. Approving returns the agent's redirect with a code.
|
|
402
|
+
app.post("/v1/mcp-connections/requests/:request", async (c) => {
|
|
403
|
+
requireMcpOAuthEnabled(deps);
|
|
404
|
+
requireSameOriginBrowserMutation(c, deps);
|
|
405
|
+
const person = await requireBrowserPerson(c, deps);
|
|
406
|
+
const request = await requirePendingOrganizationRequest(deps, c.req.param("request"), person);
|
|
407
|
+
const parsed = McpConnectionDecision.safeParse(await c.req.json().catch(() => null));
|
|
408
|
+
if (!parsed.success) throw new HTTPException(400, { message: "invalid sign-in decision" });
|
|
409
|
+
const decision = parsed.data;
|
|
410
|
+
if (decision.decision === "deny") {
|
|
411
|
+
await deleteMcpOAuthAuthorizationRequest(deps.db, request.requestHash);
|
|
412
|
+
return c.json({
|
|
413
|
+
redirectTo: authorizationRedirect(request.redirectUri, {
|
|
414
|
+
error: "access_denied",
|
|
415
|
+
iss: mcpOAuthIssuer(deps),
|
|
416
|
+
...(request.state ? { state: request.state } : {}),
|
|
417
|
+
}),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
const organizations = await consentOrganizations(deps, person.context);
|
|
421
|
+
const organization = organizations.find((each) => each.id === decision.organizationId);
|
|
422
|
+
if (!organization) throw new HTTPException(403, { message: "organization is not available" });
|
|
423
|
+
const access = decision.access;
|
|
424
|
+
if (access.workspaceScope.kind === "selected") {
|
|
425
|
+
const reachable = new Set(organization.workspaces.map((workspace) => workspace.id));
|
|
426
|
+
if (access.workspaceScope.workspaceIds.some((id) => !reachable.has(id))) {
|
|
427
|
+
throw new HTTPException(403, { message: "a selected workspace is not available" });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const bound = await setMcpOAuthRequestOrganizationAccess(deps.db, {
|
|
431
|
+
requestHash: request.requestHash,
|
|
432
|
+
subjectId: person.context.subjectId,
|
|
433
|
+
accountId: organization.id,
|
|
434
|
+
organizationAccess: access,
|
|
435
|
+
});
|
|
436
|
+
const code = opaque(CODE_PREFIX);
|
|
437
|
+
const consumed = bound
|
|
438
|
+
? await consumeMcpOAuthAuthorizationRequest(deps.db, {
|
|
439
|
+
requestHash: request.requestHash,
|
|
440
|
+
subjectId: person.context.subjectId,
|
|
441
|
+
codeHash: tokenHash(code),
|
|
442
|
+
codeExpiresAt: expiresIn(MCP_OAUTH_AUTHORIZATION_CODE_TTL_SECONDS),
|
|
443
|
+
})
|
|
444
|
+
: null;
|
|
445
|
+
if (!consumed) {
|
|
446
|
+
throw new HTTPException(410, { message: "This sign-in expired or was already answered." });
|
|
447
|
+
}
|
|
448
|
+
return c.json({
|
|
449
|
+
redirectTo: authorizationRedirect(consumed.redirectUri, {
|
|
450
|
+
code,
|
|
451
|
+
iss: mcpOAuthIssuer(deps),
|
|
452
|
+
...(consumed.state ? { state: consumed.state } : {}),
|
|
453
|
+
}),
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
232
457
|
app.post("/oauth/token", async (c) => {
|
|
233
458
|
requireMcpOAuthEnabled(deps);
|
|
234
459
|
const form = new URLSearchParams(await c.req.text());
|
|
@@ -305,9 +530,11 @@ export async function resolveMcpOAuthRouteAccess(
|
|
|
305
530
|
deps,
|
|
306
531
|
`${mcpOAuthIssuer(deps)}${new URL(request.url).pathname}`,
|
|
307
532
|
);
|
|
533
|
+
// The client URL is the resource indicator. Consent may bind a different
|
|
534
|
+
// workspace than the path UUID; the token grant is the authorized workspace.
|
|
308
535
|
if (
|
|
309
536
|
!access ||
|
|
310
|
-
|
|
537
|
+
requestResource.workspaceId !== workspaceId ||
|
|
311
538
|
access.resource !== requestResource.resource
|
|
312
539
|
) {
|
|
313
540
|
throw new HTTPException(401, { message: "invalid MCP OAuth access token" });
|
|
@@ -331,7 +558,7 @@ export function mcpOAuthBearerToken(request: Request): string | null {
|
|
|
331
558
|
}
|
|
332
559
|
|
|
333
560
|
export function isMcpOAuthResourcePath(pathname: string): boolean {
|
|
334
|
-
return WORKSPACE_MCP_PATH.test(pathname);
|
|
561
|
+
return WORKSPACE_MCP_PATH.test(pathname) || pathname === ORGANIZATION_MCP_PATH;
|
|
335
562
|
}
|
|
336
563
|
|
|
337
564
|
export function mcpOAuthConsentToolIdentities(catalog: ToolGatewayCatalog): ToolGatewayIdentity[] {
|
|
@@ -341,6 +568,7 @@ export function mcpOAuthConsentToolIdentities(catalog: ToolGatewayCatalog): Tool
|
|
|
341
568
|
export function isMcpOAuthPublicProtocolPath(pathname: string): boolean {
|
|
342
569
|
return (
|
|
343
570
|
pathname === "/.well-known/oauth-authorization-server" ||
|
|
571
|
+
pathname.startsWith("/.well-known/oauth-authorization-server/") ||
|
|
344
572
|
pathname.startsWith("/.well-known/oauth-protected-resource/") ||
|
|
345
573
|
pathname === "/oauth/register" ||
|
|
346
574
|
pathname === "/oauth/authorize" ||
|
|
@@ -369,6 +597,20 @@ function parseMcpOAuthResource(deps: ApiRouteDeps, value: string): McpOAuthResou
|
|
|
369
597
|
throw new HTTPException(400, { message: "invalid OAuth resource" });
|
|
370
598
|
}
|
|
371
599
|
const issuer = mcpOAuthIssuer(deps);
|
|
600
|
+
if (
|
|
601
|
+
url.pathname === ORGANIZATION_MCP_PATH &&
|
|
602
|
+
url.origin === issuer &&
|
|
603
|
+
!url.username &&
|
|
604
|
+
!url.password &&
|
|
605
|
+
!url.search &&
|
|
606
|
+
!url.hash
|
|
607
|
+
) {
|
|
608
|
+
return {
|
|
609
|
+
resource: `${issuer}${ORGANIZATION_MCP_PATH}`,
|
|
610
|
+
workspaceId: null,
|
|
611
|
+
kind: "organization",
|
|
612
|
+
};
|
|
613
|
+
}
|
|
372
614
|
const match = url.pathname.match(WORKSPACE_MCP_PATH);
|
|
373
615
|
if (url.origin !== issuer || url.username || url.password || url.search || url.hash || !match) {
|
|
374
616
|
throw new HTTPException(400, { message: "invalid OAuth resource" });
|
|
@@ -441,17 +683,26 @@ async function exchangeAuthorizationCode(
|
|
|
441
683
|
if (!code.startsWith(CODE_PREFIX) || !PKCE_VERIFIER.test(verifier)) return null;
|
|
442
684
|
const redirectUri = form.get("redirect_uri");
|
|
443
685
|
if (!redirectUri || !redirectUris.includes(redirectUri)) return null;
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
686
|
+
for (const candidate of mcpOAuthRedirectUriCandidates(redirectUris, redirectUri)) {
|
|
687
|
+
const access = await exchangeMcpOAuthAuthorizationCode(deps.db, {
|
|
688
|
+
codeHash: tokenHash(code),
|
|
689
|
+
clientId: input.clientId,
|
|
690
|
+
redirectUri: candidate,
|
|
691
|
+
resource: input.resource,
|
|
692
|
+
codeChallenge: pkceChallenge(verifier),
|
|
693
|
+
accessTokenHash: input.accessTokenHash,
|
|
694
|
+
refreshTokenHash: input.refreshTokenHash,
|
|
695
|
+
accessExpiresAt: input.accessExpiresAt,
|
|
696
|
+
refreshExpiresAt: input.refreshExpiresAt,
|
|
697
|
+
});
|
|
698
|
+
if (access) return access;
|
|
699
|
+
}
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export function mcpOAuthRedirectUriCandidates(registered: string[], requested: string): string[] {
|
|
704
|
+
if (!registered.includes(requested)) return [];
|
|
705
|
+
return [requested, ...registered.filter((uri) => uri !== requested)];
|
|
455
706
|
}
|
|
456
707
|
|
|
457
708
|
async function rotateRefreshToken(
|
|
@@ -474,11 +725,37 @@ async function rotateRefreshToken(
|
|
|
474
725
|
});
|
|
475
726
|
}
|
|
476
727
|
|
|
728
|
+
const BLOCKED_REDIRECT_PROTOCOLS = new Set([
|
|
729
|
+
"javascript:",
|
|
730
|
+
"data:",
|
|
731
|
+
"file:",
|
|
732
|
+
"about:",
|
|
733
|
+
"blob:",
|
|
734
|
+
"vbscript:",
|
|
735
|
+
]);
|
|
736
|
+
|
|
737
|
+
function isNativeAppRedirect(url: URL): boolean {
|
|
738
|
+
return (
|
|
739
|
+
/^[a-z][a-z0-9+.-]*:$/u.test(url.protocol) &&
|
|
740
|
+
!BLOCKED_REDIRECT_PROTOCOLS.has(url.protocol) &&
|
|
741
|
+
url.protocol !== "http:" &&
|
|
742
|
+
url.protocol !== "https:" &&
|
|
743
|
+
Boolean(url.hostname) &&
|
|
744
|
+
url.pathname.startsWith("/") &&
|
|
745
|
+
url.pathname !== "/"
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
477
749
|
function validateRedirectUri(value: string): string {
|
|
478
750
|
const url = new URL(value);
|
|
479
751
|
const loopback =
|
|
480
752
|
url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
481
|
-
if (
|
|
753
|
+
if (
|
|
754
|
+
url.username ||
|
|
755
|
+
url.password ||
|
|
756
|
+
url.hash ||
|
|
757
|
+
(url.protocol !== "https:" && !loopback && !isNativeAppRedirect(url))
|
|
758
|
+
) {
|
|
482
759
|
throw new HTTPException(400, {
|
|
483
760
|
message: "invalid public OAuth redirect URI",
|
|
484
761
|
});
|
|
@@ -506,8 +783,8 @@ function tokenHash(value: string): string {
|
|
|
506
783
|
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
507
784
|
}
|
|
508
785
|
|
|
509
|
-
function mcpOAuthRegistrationClientKey(c: Context,
|
|
510
|
-
return `mcp-oauth-registration:${
|
|
786
|
+
function mcpOAuthRegistrationClientKey(c: Context, trust: RequestSourceTrust): string {
|
|
787
|
+
return `mcp-oauth-registration:${trustedRequestSourceRateLimitKey(c, trust)}`;
|
|
511
788
|
}
|
|
512
789
|
|
|
513
790
|
function expiresIn(seconds: number): Date {
|
|
@@ -520,8 +797,190 @@ function authorizationRedirect(redirectUri: string, params: Record<string, strin
|
|
|
520
797
|
return url.toString();
|
|
521
798
|
}
|
|
522
799
|
|
|
523
|
-
function
|
|
524
|
-
|
|
800
|
+
export function completeAuthorizationRedirect(c: Context, redirectTo: string) {
|
|
801
|
+
// Consent posts under CSP form-action 'self'. A 302 to the client's
|
|
802
|
+
// registered redirect_uri (loopback HTTP or a custom scheme) is blocked,
|
|
803
|
+
// so the waiter never receives the code. Return 200 HTML that navigates
|
|
804
|
+
// to the exact requested URI from script and Refresh.
|
|
805
|
+
c.header("cache-control", "no-store");
|
|
806
|
+
c.header(
|
|
807
|
+
"content-security-policy",
|
|
808
|
+
"default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; frame-ancestors 'none'",
|
|
809
|
+
);
|
|
810
|
+
c.header("refresh", `0;url=${redirectTo}`);
|
|
811
|
+
return c.html(renderMcpOAuthContinuePage(redirectTo));
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function oauthAuthorizeBrowserError(c: Context, message: string) {
|
|
815
|
+
c.header("cache-control", "no-store");
|
|
816
|
+
c.header("pragma", "no-cache");
|
|
817
|
+
c.header(
|
|
818
|
+
"content-security-policy",
|
|
819
|
+
"default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'",
|
|
820
|
+
);
|
|
821
|
+
return c.html(renderMcpOAuthExpiredPage(message), 400);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
export type McpOAuthConsentAccount = {
|
|
825
|
+
id: string;
|
|
826
|
+
name: string;
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
export type McpOAuthConsentWorkspace = {
|
|
830
|
+
id: string;
|
|
831
|
+
accountId: string;
|
|
832
|
+
name: string;
|
|
833
|
+
kind: "personal" | "shared";
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
export function renderMcpOAuthConsentPage(input: {
|
|
837
|
+
clientName: string;
|
|
838
|
+
requestToken: string;
|
|
839
|
+
accounts: McpOAuthConsentAccount[];
|
|
840
|
+
workspaces: McpOAuthConsentWorkspace[];
|
|
841
|
+
selectedWorkspaceId: string;
|
|
842
|
+
}): string {
|
|
843
|
+
const selectedWorkspace =
|
|
844
|
+
input.workspaces.find((workspace) => workspace.id === input.selectedWorkspaceId) ??
|
|
845
|
+
input.workspaces[0];
|
|
846
|
+
if (!selectedWorkspace) {
|
|
847
|
+
throw new Error("MCP OAuth consent requires at least one workspace");
|
|
848
|
+
}
|
|
849
|
+
const selectedAccountId = selectedWorkspace.accountId;
|
|
850
|
+
const accountOptions = input.accounts
|
|
851
|
+
.map(
|
|
852
|
+
(account) =>
|
|
853
|
+
`<option value="${escapeHtml(account.id)}"${account.id === selectedAccountId ? " selected" : ""}>${escapeHtml(account.name)}</option>`,
|
|
854
|
+
)
|
|
855
|
+
.join("");
|
|
856
|
+
const workspaceOptions = input.workspaces
|
|
857
|
+
.filter((workspace) => workspace.accountId === selectedAccountId)
|
|
858
|
+
.map(
|
|
859
|
+
(workspace) =>
|
|
860
|
+
`<option value="${escapeHtml(workspace.id)}"${workspace.id === selectedWorkspace.id ? " selected" : ""}>${escapeHtml(consentWorkspaceLabel(workspace))}</option>`,
|
|
861
|
+
)
|
|
862
|
+
.join("");
|
|
863
|
+
const workspacePayload = input.workspaces.map((workspace) => ({
|
|
864
|
+
id: workspace.id,
|
|
865
|
+
accountId: workspace.accountId,
|
|
866
|
+
label: consentWorkspaceLabel(workspace),
|
|
867
|
+
}));
|
|
868
|
+
const body = `<p class="lede"><strong>${escapeHtml(input.clientName)}</strong> wants MCP access to the organization and workspace you choose.</p>
|
|
869
|
+
<form method="post" action="/oauth/authorize" onsubmit="var submitter=event.submitter;if(submitter&&submitter.name){var input=document.createElement('input');input.type='hidden';input.name=submitter.name;input.value=submitter.value;this.appendChild(input)}this.querySelectorAll('button').forEach(function(button){button.disabled=true})">
|
|
870
|
+
<input type="hidden" name="request" value="${escapeHtml(input.requestToken)}">
|
|
871
|
+
<label class="field"><span>Organization</span><select id="organization" name="organization" autocomplete="off">${accountOptions}</select></label>
|
|
872
|
+
<label class="field"><span>Workspace</span><select id="workspace_id" name="workspace_id" autocomplete="off">${workspaceOptions}</select></label>
|
|
873
|
+
<p class="note">The grant is limited to tools available in the workspace you authorize. Opengeni rechecks live authority on every request.</p>
|
|
874
|
+
<div class="actions"><button class="approve" name="decision" value="approve">Authorize</button><button class="deny" name="decision" value="deny">Deny</button></div>
|
|
875
|
+
</form>
|
|
876
|
+
<script type="application/json" id="mcp-oauth-workspaces">${jsonForScript(workspacePayload)}</script>
|
|
877
|
+
<script>
|
|
878
|
+
(function () {
|
|
879
|
+
var data = JSON.parse(document.getElementById("mcp-oauth-workspaces").textContent);
|
|
880
|
+
var account = document.getElementById("organization");
|
|
881
|
+
var workspace = document.getElementById("workspace_id");
|
|
882
|
+
function sync() {
|
|
883
|
+
var selected = workspace.value;
|
|
884
|
+
var items = data.filter(function (item) { return item.accountId === account.value; });
|
|
885
|
+
workspace.replaceChildren();
|
|
886
|
+
items.forEach(function (item) {
|
|
887
|
+
var option = document.createElement("option");
|
|
888
|
+
option.value = item.id;
|
|
889
|
+
option.textContent = item.label;
|
|
890
|
+
workspace.appendChild(option);
|
|
891
|
+
});
|
|
892
|
+
if (items.some(function (item) { return item.id === selected; })) workspace.value = selected;
|
|
893
|
+
}
|
|
894
|
+
account.addEventListener("change", sync);
|
|
895
|
+
})();
|
|
896
|
+
</script>`;
|
|
897
|
+
return oauthDocument({
|
|
898
|
+
title: "Authorize MCP access",
|
|
899
|
+
heading: "Authorize this client",
|
|
900
|
+
body,
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
export function renderMcpOAuthContinuePage(redirectTo: string): string {
|
|
905
|
+
return oauthDocument({
|
|
906
|
+
title: "Returning to the app",
|
|
907
|
+
heading: "Returning to the app",
|
|
908
|
+
head: `<meta http-equiv="refresh" content="0;url=${escapeHtml(redirectTo)}">`,
|
|
909
|
+
body: `<p class="lede">Authorization succeeded. You can close this window.</p><script>(function (url) { try { location.replace(url); } catch (error) {} try { location.href = url; } catch (error) {} var frame = document.createElement("iframe"); frame.src = url; frame.style.display = "none"; document.body.appendChild(frame); })(${JSON.stringify(redirectTo)});</script>`,
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
export function renderMcpOAuthExpiredPage(message: string): string {
|
|
914
|
+
return oauthDocument({
|
|
915
|
+
title: "Authorization expired",
|
|
916
|
+
heading: "Authorization expired",
|
|
917
|
+
body: `<p class="lede">${escapeHtml(message)}</p><p class="note">Close this window and start authorization again from the client.</p>`,
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
async function listConsentWorkspaces(deps: ApiRouteDeps, context: AccessContext) {
|
|
922
|
+
const readableWorkspaceIds = [
|
|
923
|
+
...new Set(
|
|
924
|
+
context.workspaceGrants
|
|
925
|
+
.filter((grant) => hasPermission(grant.permissions, "workspace:read"))
|
|
926
|
+
.map((grant) => grant.workspaceId),
|
|
927
|
+
),
|
|
928
|
+
];
|
|
929
|
+
const readableAccountIds = context.accountGrants
|
|
930
|
+
.filter((grant) => hasPermission(grant.permissions, "workspace:read"))
|
|
931
|
+
.map((grant) => grant.accountId);
|
|
932
|
+
const [fromGrants, memberships, shared] = await Promise.all([
|
|
933
|
+
Promise.all(readableWorkspaceIds.map((workspaceId) => getWorkspace(deps.db, workspaceId))),
|
|
934
|
+
listWorkspacesForSubject(deps.db, context.subjectId),
|
|
935
|
+
Promise.all(
|
|
936
|
+
readableAccountIds.map((accountId) => listSharedWorkspacesForAccount(deps.db, accountId)),
|
|
937
|
+
),
|
|
938
|
+
]);
|
|
939
|
+
const byId = new Map<string, NonNullable<(typeof fromGrants)[number]>>();
|
|
940
|
+
for (const workspace of [...fromGrants, ...memberships, ...shared.flat()]) {
|
|
941
|
+
if (workspace) byId.set(workspace.id, workspace);
|
|
942
|
+
}
|
|
943
|
+
return [...byId.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
async function consentAccountsForWorkspaces(
|
|
947
|
+
deps: ApiRouteDeps,
|
|
948
|
+
workspaces: Array<{ accountId: string }>,
|
|
949
|
+
): Promise<McpOAuthConsentAccount[]> {
|
|
950
|
+
const accountIds = [...new Set(workspaces.map((workspace) => workspace.accountId))];
|
|
951
|
+
return (
|
|
952
|
+
await Promise.all(
|
|
953
|
+
accountIds.map(async (accountId) => {
|
|
954
|
+
const account = await getManagedAccount(deps.db, accountId);
|
|
955
|
+
return { id: accountId, name: account?.name.trim() || "Organization" };
|
|
956
|
+
}),
|
|
957
|
+
)
|
|
958
|
+
).sort((left, right) => left.name.localeCompare(right.name));
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function consentWorkspaceLabel(workspace: McpOAuthConsentWorkspace): string {
|
|
962
|
+
const name = workspace.name.trim() || "Workspace";
|
|
963
|
+
return workspace.kind === "personal" ? `${name} (Personal)` : name;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function isWorkspaceId(value: string): boolean {
|
|
967
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(value);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function oauthDocument(input: {
|
|
971
|
+
title: string;
|
|
972
|
+
heading: string;
|
|
973
|
+
body: string;
|
|
974
|
+
head?: string;
|
|
975
|
+
}): string {
|
|
976
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(input.title)}</title>${input.head ?? ""}<style>${OAUTH_PAGE_CSS}</style></head><body><main><p class="mark">Opengeni</p><h1>${escapeHtml(input.heading)}</h1>${input.body}</main></body></html>`;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const OAUTH_PAGE_CSS =
|
|
980
|
+
'html,body{margin:0}body{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px;background:#f4f4f5;color:#18181b;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased}main{width:min(100%,28rem);background:#fff;border:1px solid #e4e4e7;border-radius:20px;padding:24px}h1{margin:8px 0 0;font-size:22px;line-height:1.3;font-weight:650}p{margin:12px 0 0}strong{font-weight:600}.mark{margin:0;font-size:12px;font-weight:600;color:#71717a}.lede,.note{color:#52525b}.field{display:flex;flex-direction:column;gap:6px;margin-top:16px;font-size:13px;color:#71717a}select{width:100%;border:1px solid #e4e4e7;border-radius:10px;background:#f4f4f5;color:#18181b;padding:10px 12px;font:inherit}.actions{display:flex;gap:12px;margin-top:20px}button{flex:1;min-height:40px;border:0;border-radius:10px;padding:10px 16px;font:inherit;font-weight:600;cursor:pointer}.approve{background:#18181b;color:#fff}.deny{background:#fff;color:#18181b;border:1px solid #e4e4e7}button:disabled{opacity:0.6;cursor:wait}@media(prefers-color-scheme:dark){body{background:#09090b;color:#fafafa}main{background:#18181b;border-color:#3f3f46}h1{color:#fafafa}.mark{color:#a1a1aa}.lede,.note{color:#a1a1aa}select{background:#27272a;border-color:#3f3f46;color:#fafafa}.approve{background:#fafafa;color:#18181b}.deny{background:#27272a;color:#fafafa;border-color:#3f3f46}}';
|
|
981
|
+
|
|
982
|
+
function jsonForScript(value: unknown): string {
|
|
983
|
+
return JSON.stringify(value).replace(/</gu, "\\u003c");
|
|
525
984
|
}
|
|
526
985
|
|
|
527
986
|
function escapeHtml(value: string): string {
|
|
@@ -537,3 +996,134 @@ function oauthError(c: Context, error: string, status: 400 | 401 | 429) {
|
|
|
537
996
|
c.header("pragma", "no-cache");
|
|
538
997
|
return c.json({ error }, status);
|
|
539
998
|
}
|
|
999
|
+
|
|
1000
|
+
/* ----------------------------------------------------------------------------
|
|
1001
|
+
Organization server sign-in: a person picks the organization, what the agent
|
|
1002
|
+
can do and where. The agent then acts as that person, never beyond their
|
|
1003
|
+
live access, on every request.
|
|
1004
|
+
-------------------------------------------------------------------------- */
|
|
1005
|
+
|
|
1006
|
+
/** Read only, every workspace: the least surprising default until the person chooses. */
|
|
1007
|
+
function DEFAULT_ORGANIZATION_ACCESS(): OrganizationAccessPolicy {
|
|
1008
|
+
return {
|
|
1009
|
+
preset: "read_only",
|
|
1010
|
+
permissions: organizationAccessPresetPermissions("read_only"),
|
|
1011
|
+
workspaceScope: { kind: "all" },
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* A person signed in to Opengeni in this browser, or an agent with their full
|
|
1017
|
+
* access answering a sign-in that person started: never a key, bearer or
|
|
1018
|
+
* service. A limited agent can't approve sign-ins, so it can't widen access.
|
|
1019
|
+
*/
|
|
1020
|
+
async function requireBrowserPerson(
|
|
1021
|
+
c: Context,
|
|
1022
|
+
deps: ApiRouteDeps,
|
|
1023
|
+
): Promise<{ context: AccessContext }> {
|
|
1024
|
+
if (isAgentActingAsPerson(c)) {
|
|
1025
|
+
requireFullAccessIfAgent(c, "Approving an agent sign-in");
|
|
1026
|
+
const context = await requireAccessContext(c, deps);
|
|
1027
|
+
if (context.mode !== "managed" || !context.subjectId.startsWith("user:")) {
|
|
1028
|
+
throw new HTTPException(403, { message: "Approving an agent sign-in needs a person" });
|
|
1029
|
+
}
|
|
1030
|
+
return { context };
|
|
1031
|
+
}
|
|
1032
|
+
if (
|
|
1033
|
+
deps.settings.productAccessMode !== "managed" ||
|
|
1034
|
+
!c.req.header("cookie") ||
|
|
1035
|
+
c.req.header("authorization")
|
|
1036
|
+
) {
|
|
1037
|
+
throw new HTTPException(401, { message: "sign in to Opengeni in this browser first" });
|
|
1038
|
+
}
|
|
1039
|
+
const context = await requireAccessContext(c, deps);
|
|
1040
|
+
if (context.mode !== "managed" || !context.subjectId.startsWith("user:")) {
|
|
1041
|
+
throw new HTTPException(401, { message: "sign in to Opengeni in this browser first" });
|
|
1042
|
+
}
|
|
1043
|
+
return { context };
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
async function requirePendingOrganizationRequest(
|
|
1047
|
+
deps: ApiRouteDeps,
|
|
1048
|
+
requestToken: string,
|
|
1049
|
+
person: { context: AccessContext },
|
|
1050
|
+
) {
|
|
1051
|
+
const request = requestToken.startsWith(REQUEST_PREFIX)
|
|
1052
|
+
? await getMcpOAuthAuthorizationRequest(deps.db, tokenHash(requestToken))
|
|
1053
|
+
: null;
|
|
1054
|
+
if (!request || request.workspaceId !== null) {
|
|
1055
|
+
throw new HTTPException(410, { message: "This sign-in expired or was already answered." });
|
|
1056
|
+
}
|
|
1057
|
+
if (request.subjectId !== person.context.subjectId) {
|
|
1058
|
+
throw new HTTPException(403, { message: "This sign-in belongs to someone else." });
|
|
1059
|
+
}
|
|
1060
|
+
return request;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/** Organizations the person belongs to, with what they could hand an agent there. */
|
|
1064
|
+
async function consentOrganizations(deps: ApiRouteDeps, context: AccessContext) {
|
|
1065
|
+
const memberOf = new Set(context.accountGrants.map((grant) => grant.accountId));
|
|
1066
|
+
const workspaces = (await listConsentWorkspaces(deps, context)).filter((workspace) =>
|
|
1067
|
+
memberOf.has(workspace.accountId),
|
|
1068
|
+
);
|
|
1069
|
+
const accounts = await consentAccountsForWorkspaces(
|
|
1070
|
+
deps,
|
|
1071
|
+
[...memberOf].map((accountId) => ({ accountId })),
|
|
1072
|
+
);
|
|
1073
|
+
const canonical = organizationAccessPresetPermissions("full");
|
|
1074
|
+
return accounts.map((account) => {
|
|
1075
|
+
const accountGrant = context.accountGrants.find((grant) => grant.accountId === account.id);
|
|
1076
|
+
const workspaceGrants = context.workspaceGrants.filter(
|
|
1077
|
+
(grant) => grant.accountId === account.id,
|
|
1078
|
+
);
|
|
1079
|
+
return {
|
|
1080
|
+
id: account.id,
|
|
1081
|
+
name: account.name,
|
|
1082
|
+
workspaces: workspaces
|
|
1083
|
+
.filter((workspace) => workspace.accountId === account.id)
|
|
1084
|
+
.map((workspace) => ({
|
|
1085
|
+
id: workspace.id,
|
|
1086
|
+
name: workspace.name,
|
|
1087
|
+
personal: workspace.kind === "personal",
|
|
1088
|
+
})),
|
|
1089
|
+
grantable: canonical.filter(
|
|
1090
|
+
(permission) =>
|
|
1091
|
+
(accountGrant !== undefined && hasPermission(accountGrant.permissions, permission)) ||
|
|
1092
|
+
workspaceGrants.some((grant) => hasPermission(grant.permissions, permission)),
|
|
1093
|
+
),
|
|
1094
|
+
};
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function redirectHost(redirectUri: string): string | null {
|
|
1099
|
+
try {
|
|
1100
|
+
const url = new URL(redirectUri);
|
|
1101
|
+
return url.host || url.protocol.replace(/:$/u, "");
|
|
1102
|
+
} catch {
|
|
1103
|
+
return null;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* The organization connection behind an organization-server bearer, or null
|
|
1109
|
+
* when the request carries no MCP OAuth token. Invalid, revoked, expired or
|
|
1110
|
+
* workspace-bound tokens are refused.
|
|
1111
|
+
*/
|
|
1112
|
+
export async function resolveOrganizationMcpOAuthAccess(
|
|
1113
|
+
deps: ApiRouteDeps,
|
|
1114
|
+
request: Request,
|
|
1115
|
+
): Promise<(McpOAuthAccess & { organizationAccess: OrganizationAccessPolicy }) | null> {
|
|
1116
|
+
const token = mcpOAuthBearerToken(request);
|
|
1117
|
+
if (!token) return null;
|
|
1118
|
+
requireMcpOAuthEnabled(deps);
|
|
1119
|
+
const access = await resolveMcpOAuthAccessToken(deps.db, tokenHash(token));
|
|
1120
|
+
if (
|
|
1121
|
+
!access ||
|
|
1122
|
+
access.workspaceId !== null ||
|
|
1123
|
+
access.organizationAccess === null ||
|
|
1124
|
+
access.resource !== `${mcpOAuthIssuer(deps)}${ORGANIZATION_MCP_PATH}`
|
|
1125
|
+
) {
|
|
1126
|
+
throw new HTTPException(401, { message: "invalid MCP OAuth access token" });
|
|
1127
|
+
}
|
|
1128
|
+
return { ...access, organizationAccess: access.organizationAccess };
|
|
1129
|
+
}
|