@vellumai/assistant 0.11.3 → 0.11.4-staging.2
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/ARCHITECTURE.md +11 -6
- package/docs/architecture/memory.md +11 -0
- package/docs/architecture/turn-actor.md +70 -0
- package/docs/flux-turn-detection-spike.md +243 -0
- package/docs/stt-provider-onboarding.md +3 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/openapi.yaml +139 -37
- package/package.json +1 -1
- package/scripts/voice-ttft-spike.ts +3 -3
- package/src/__tests__/app-compiler.test.ts +38 -3
- package/src/__tests__/attachments-store.test.ts +21 -12
- package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
- package/src/__tests__/call-site-routing-provider.test.ts +1 -1
- package/src/__tests__/channel-availability-routes.test.ts +14 -1
- package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
- package/src/__tests__/channel-delivery-store.test.ts +14 -14
- package/src/__tests__/config-loader-backfill.test.ts +3 -3
- package/src/__tests__/config-schema.test.ts +25 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop.test.ts +28 -20
- package/src/__tests__/conversation-attention-store.test.ts +63 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
- package/src/__tests__/conversation-fork-crud.test.ts +69 -0
- package/src/__tests__/conversation-fork-referential.test.ts +67 -0
- package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
- package/src/__tests__/conversation-queue.test.ts +177 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
- package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
- package/src/__tests__/credential-prompt-route.test.ts +7 -10
- package/src/__tests__/custom-profile-ensure.test.ts +5 -1
- package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
- package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
- package/src/__tests__/document-append-idempotency.test.ts +233 -0
- package/src/__tests__/edit-propagation.test.ts +0 -7
- package/src/__tests__/helpers/mock-actor-context.ts +49 -0
- package/src/__tests__/helpers/mock-conversation.ts +13 -1
- package/src/__tests__/injector-chain.test.ts +63 -41
- package/src/__tests__/injector-disk-pressure.test.ts +11 -23
- package/src/__tests__/llm-context-resolution.test.ts +73 -1
- package/src/__tests__/llm-schema.test.ts +5 -2
- package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
- package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
- package/src/__tests__/mtime-cache.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -20
- package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
- package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
- package/src/__tests__/scoped-approval-grants.test.ts +11 -6
- package/src/__tests__/secret-ingress-channel.test.ts +0 -1
- package/src/__tests__/skills.test.ts +32 -0
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
- package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
- package/src/__tests__/turn-events-store.test.ts +43 -0
- package/src/__tests__/ui-shape-teaching.test.ts +33 -0
- package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/voice-config-update.test.ts +40 -0
- package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
- package/src/__tests__/worker-plugin-surface.test.ts +77 -0
- package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
- package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
- package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
- package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
- package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
- package/src/acp/__tests__/auth-required.test.ts +161 -0
- package/src/acp/acp-claude-oauth.ts +19 -2
- package/src/acp/agent-process.test.ts +100 -0
- package/src/acp/agent-process.ts +29 -26
- package/src/acp/auth-required.ts +102 -0
- package/src/acp/session-manager.test.ts +119 -0
- package/src/acp/session-manager.ts +68 -2
- package/src/api/events/acp-auth-required.ts +55 -0
- package/src/api/index.ts +7 -0
- package/src/api/surfaces.ts +7 -3
- package/src/apps/app-store.ts +3 -0
- package/src/bundler/package-resolver.ts +2 -30
- package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
- package/src/calls/call-controller.ts +19 -3
- package/src/calls/call-setup-flow.ts +0 -1
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/calls/voice-session-bridge.ts +71 -16
- package/src/calls/voice-triage-escalate.ts +104 -2
- package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
- package/src/channels/config.ts +13 -0
- package/src/channels/plugin-channel-declarations.ts +108 -0
- package/src/channels/types.ts +30 -0
- package/src/cli/AGENTS.md +5 -2
- package/src/cli/commands/credentials.help.ts +2 -2
- package/src/cli/commands/inference-providers.ts +1 -1
- package/src/cli/commands/mcp.help.ts +13 -4
- package/src/cli/commands/mcp.ts +9 -0
- package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
- package/src/cli/commands/memory/index.help.ts +43 -1
- package/src/cli/commands/memory/memory-v3.ts +64 -0
- package/src/cli/commands/stt.help.ts +27 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/upgrade-plugin.ts +42 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
- package/src/config/__tests__/default-provider.test.ts +6 -1
- package/src/config/__tests__/profile-materialization.test.ts +75 -19
- package/src/config/bundled-skills/acp/SKILL.md +6 -7
- package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
- package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
- package/src/config/call-site-defaults.ts +4 -5
- package/src/config/default-profile-catalog.ts +83 -12
- package/src/config/default-profile-names.ts +4 -1
- package/src/config/default-provider-resolution.ts +4 -0
- package/src/config/llm-context-resolution.ts +11 -3
- package/src/config/llm-resolver.ts +28 -1
- package/src/config/profile-materialization.ts +70 -22
- package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
- package/src/config/schemas/call-site-catalog.ts +4 -4
- package/src/config/schemas/live-voice.ts +57 -23
- package/src/config/schemas/llm.ts +59 -32
- package/src/config/schemas/mcp.ts +23 -0
- package/src/config/schemas/plugin-updates.ts +6 -2
- package/src/config/schemas/stt.ts +1 -0
- package/src/context/outbound-sanitize.ts +96 -1
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
- package/src/daemon/conversation-agent-loop.ts +17 -6
- package/src/daemon/conversation-messaging.ts +5 -1
- package/src/daemon/conversation-notifiers.ts +9 -1
- package/src/daemon/conversation-process.ts +36 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-surfaces.ts +27 -5
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/interactive-turn-sender.ts +59 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +24 -24
- package/src/daemon/providers-setup.ts +6 -3
- package/src/daemon/trust-context-types.ts +29 -0
- package/src/daemon/wake-conversation-ops.ts +3 -2
- package/src/documents/document-store.ts +138 -5
- package/src/hooks/hook-loader.ts +3 -3
- package/src/hooks/registry.ts +50 -6
- package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
- package/src/inbound/oauth-callback-url.ts +61 -0
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
- package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
- package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
- package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
- package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
- package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
- package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
- package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
- package/src/live-voice/live-voice-archive.ts +2 -0
- package/src/live-voice/live-voice-metrics.ts +57 -32
- package/src/live-voice/live-voice-photo.ts +1 -2
- package/src/live-voice/live-voice-session.ts +535 -314
- package/src/live-voice/progress-narration.ts +277 -0
- package/src/live-voice/protocol.ts +21 -1
- package/src/mcp/__tests__/effective-config.test.ts +238 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
- package/src/mcp/client.ts +16 -11
- package/src/mcp/effective-config.ts +113 -0
- package/src/mcp/manager.ts +11 -6
- package/src/mcp/mcp-auth-orchestrator.ts +13 -22
- package/src/mcp/mcp-oauth-provider.ts +205 -240
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
- package/src/monitoring/plugin-auto-update.ts +128 -24
- package/src/notifications/signal.ts +1 -0
- package/src/permissions/confirmation-guardian-request.test.ts +15 -11
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/question-guardian-request.test.ts +14 -6
- package/src/permissions/question-guardian-request.ts +1 -2
- package/src/persistence/attachments-store.ts +8 -1
- package/src/persistence/bookmark-crud.ts +3 -7
- package/src/persistence/conversation-attention-store.ts +16 -45
- package/src/persistence/conversation-crud.ts +33 -4
- package/src/persistence/conversation-lineage.ts +9 -0
- package/src/persistence/conversation-queries.ts +108 -41
- package/src/persistence/delivery-crud.ts +38 -29
- package/src/persistence/external-conversation-store.ts +32 -4
- package/src/persistence/llm-request-log-store.ts +4 -10
- package/src/persistence/llm-usage-store.ts +8 -3
- package/src/persistence/message-reads.test.ts +197 -0
- package/src/persistence/message-reads.ts +211 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
- package/src/persistence/real-user-turn-filter.ts +27 -3
- package/src/persistence/steps.ts +9 -0
- package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
- package/src/plugin-api/conversation-turn.ts +168 -5
- package/src/plugin-api/index.ts +21 -5
- package/src/plugin-api/vision-support.test.ts +1 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
- package/src/plugins/defaults/memory/AGENTS.md +4 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
- package/src/plugins/defaults/memory/buffer-format.ts +165 -0
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
- package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
- package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
- package/src/plugins/defaults/memory/indexer.ts +3 -1
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
- package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
- package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
- package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
- package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
- package/src/plugins/defaults/memory/v3/prune.ts +2 -0
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
- package/src/plugins/defaults/memory/worker.ts +6 -3
- package/src/plugins/external-plugin-loader.ts +47 -0
- package/src/plugins/mcp-servers.ts +361 -0
- package/src/plugins/mtime-cache.ts +23 -49
- package/src/plugins/worker-plugin-surface.ts +33 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
- package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
- package/src/providers/__tests__/retry-callsite.test.ts +5 -2
- package/src/providers/call-site-routing.ts +30 -3
- package/src/providers/connection-resolution.ts +194 -11
- package/src/providers/inference/auth.ts +6 -0
- package/src/providers/inference/connection-availability.ts +24 -2
- package/src/providers/inference/connections.ts +2 -0
- package/src/providers/model-catalog.ts +3 -3
- package/src/providers/model-intents.ts +28 -8
- package/src/providers/openai/chat-completions-provider.ts +5 -6
- package/src/providers/openai/codex-models.ts +2 -1
- package/src/providers/provider-send-message.ts +32 -3
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
- package/src/providers/speech-to-text/provider-catalog.ts +99 -8
- package/src/providers/speech-to-text/resolve.ts +25 -2
- package/src/routes/worker.ts +17 -5
- package/src/runtime/access-request-helper.ts +9 -12
- package/src/runtime/agent-wake.ts +3 -3
- package/src/runtime/pre-first-message-gate.ts +4 -0
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
- package/src/runtime/routes/channel-availability-routes.ts +32 -14
- package/src/runtime/routes/channel-route-shared.ts +0 -6
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
- package/src/runtime/routes/conversation-list-routes.ts +112 -1
- package/src/runtime/routes/conversation-query-routes.ts +40 -27
- package/src/runtime/routes/credential-prompt-routes.ts +4 -7
- package/src/runtime/routes/default-provider-routes.ts +15 -0
- package/src/runtime/routes/inbound-message-handler.ts +17 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
- package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
- package/src/runtime/routes/inference-profiles-routes.ts +20 -11
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/mcp-auth-routes.ts +148 -57
- package/src/runtime/routes/plugins-routes.ts +21 -3
- package/src/runtime/routes/stt-routes.ts +31 -25
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
- package/src/runtime/routes/user-route-dispatcher.ts +39 -14
- package/src/runtime/routes/user-route-import.ts +108 -0
- package/src/schedule/worker.ts +6 -0
- package/src/security/oauth2.ts +6 -22
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
- package/src/stt/__tests__/types.test.ts +94 -0
- package/src/stt/daemon-batch-transcriber.ts +10 -0
- package/src/stt/stt-stream-session.ts +8 -4
- package/src/stt/types.ts +103 -0
- package/src/subagent/manager.ts +1 -3
- package/src/subagent/types.ts +7 -6
- package/src/tools/acp/spawn.test.ts +97 -0
- package/src/tools/acp/spawn.ts +32 -0
- package/src/tools/document/document-tool.ts +12 -3
- package/src/tools/registry.ts +2 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +11 -0
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
- package/src/tts/reasoning-tag-filter.ts +89 -0
- package/src/util/think-tag-stream.ts +95 -0
- package/src/workspace/byok-default-profile-ensure.ts +76 -24
- package/src/workspace/custom-profile-ensure.ts +4 -24
- package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
- package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
- package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
- package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
- package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/provider-commit-message-generator.ts +7 -5
- package/src/live-voice/__tests__/front-decision.test.ts +0 -645
- package/src/live-voice/front-decision.ts +0 -476
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OAuthClientProvider implementation for MCP servers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Credentials persist through secure-keys (the credential store). The
|
|
5
|
+
* browser callback arrives on the assistant's shared OAuth callback route,
|
|
6
|
+
* which the gateway already serves and forwards to
|
|
7
|
+
* `POST /v1/internal/oauth/callback`; the runtime matches the arriving
|
|
8
|
+
* OAuth `state` against `security/oauth-callback-registry.ts` and resolves
|
|
9
|
+
* the waiting flow.
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* URL and the in-memory oauth-callback-registry. Used when the daemon
|
|
15
|
-
* runs inside Docker/platform where localhost is unreachable from the
|
|
16
|
-
* user's browser.
|
|
11
|
+
* The redirect URI comes from `resolveOauthCallbackUrl`, so it is the same
|
|
12
|
+
* stable URL for every server and every attempt. That stability is what
|
|
13
|
+
* makes dynamic client registration and Client ID Metadata Documents
|
|
14
|
+
* usable: both pin `redirect_uris` at registration time, and the
|
|
15
|
+
* authorization server matches the value exactly on each authorization
|
|
16
|
+
* request.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { randomBytes } from "node:crypto";
|
|
20
|
-
import { createServer, type Server } from "node:http";
|
|
21
20
|
|
|
22
21
|
import type {
|
|
23
22
|
OAuthClientProvider,
|
|
@@ -29,6 +28,8 @@ import type {
|
|
|
29
28
|
OAuthTokens,
|
|
30
29
|
} from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
31
30
|
|
|
31
|
+
import { getPlatformAssistantId } from "../config/env.js";
|
|
32
|
+
import { getAssistantName } from "../daemon/identity-helpers.js";
|
|
32
33
|
import {
|
|
33
34
|
deleteSecureKeyAsync,
|
|
34
35
|
getSecureKeyAsync,
|
|
@@ -36,12 +37,10 @@ import {
|
|
|
36
37
|
} from "../security/secure-keys.js";
|
|
37
38
|
import { openInHostBrowser } from "../util/browser.js";
|
|
38
39
|
import { getLogger } from "../util/logger.js";
|
|
40
|
+
import { APP_VERSION } from "../version.js";
|
|
39
41
|
|
|
40
42
|
const log = getLogger("mcp-oauth");
|
|
41
43
|
|
|
42
|
-
const CALLBACK_PATH = "/oauth/callback";
|
|
43
|
-
const CALLBACK_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
|
|
44
|
-
|
|
45
44
|
// Credential store key helpers
|
|
46
45
|
function tokensKey(serverId: string): string {
|
|
47
46
|
return `mcp:${serverId}:tokens`;
|
|
@@ -52,15 +51,38 @@ function clientInfoKey(serverId: string): string {
|
|
|
52
51
|
function discoveryKey(serverId: string): string {
|
|
53
52
|
return `mcp:${serverId}:discovery`;
|
|
54
53
|
}
|
|
54
|
+
function clientBindingKey(serverId: string): string {
|
|
55
|
+
return `mcp:${serverId}:client_binding`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Logo shown on an authorization server's consent screen.
|
|
60
|
+
*
|
|
61
|
+
* Anonymously fetchable, which is the requirement: the server loads it
|
|
62
|
+
* without credentials. It identifies Vellum rather than the individual
|
|
63
|
+
* assistant, so every assistant a person runs presents the same mark.
|
|
64
|
+
*/
|
|
65
|
+
const CLIENT_LOGO_URI = "https://www.vellum.ai/favicon.svg";
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* What a stored client registration was made against.
|
|
69
|
+
*
|
|
70
|
+
* A client identifier belongs to the authorization server that issued it and
|
|
71
|
+
* is registered for one set of redirect URIs, so reusing a registration after
|
|
72
|
+
* either changes is invalid. Recording both is what lets a registration be
|
|
73
|
+
* reused when they still hold, which is the difference between registering
|
|
74
|
+
* once per assistant and registering once per attempt.
|
|
75
|
+
*/
|
|
76
|
+
interface ClientRegistrationBinding {
|
|
77
|
+
issuer: string | null;
|
|
78
|
+
redirectUri: string;
|
|
79
|
+
}
|
|
55
80
|
|
|
56
81
|
export interface McpOAuthCallbackResult {
|
|
57
82
|
/** Resolves with the authorization code when the callback is received. */
|
|
58
83
|
codePromise: Promise<string>;
|
|
59
84
|
}
|
|
60
85
|
|
|
61
|
-
/** Which callback transport to use for receiving the OAuth redirect. */
|
|
62
|
-
export type McpOAuthCallbackTransport = "loopback" | "gateway";
|
|
63
|
-
|
|
64
86
|
export interface McpOAuthProviderOptions {
|
|
65
87
|
/**
|
|
66
88
|
* If provided, called with the authorization URL during
|
|
@@ -75,37 +97,29 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
75
97
|
private readonly serverId: string;
|
|
76
98
|
private readonly serverUrl: string;
|
|
77
99
|
private readonly interactive: boolean;
|
|
78
|
-
private readonly callbackTransport: McpOAuthCallbackTransport;
|
|
79
100
|
private _codeVerifier: string | undefined;
|
|
80
101
|
private _state: string | undefined;
|
|
81
102
|
private _redirectUrl: string | undefined;
|
|
82
103
|
private _codePromise: Promise<string> | null = null;
|
|
83
|
-
|
|
84
|
-
private
|
|
85
|
-
|
|
86
|
-
private _gatewayCodeResolve: ((code: string) => void) | undefined;
|
|
87
|
-
private _gatewayCodeReject: ((err: Error) => void) | undefined;
|
|
104
|
+
/** Deferred resolver/rejector for the callback code promise. */
|
|
105
|
+
private _codeResolve: ((code: string) => void) | undefined;
|
|
106
|
+
private _codeReject: ((err: Error) => void) | undefined;
|
|
88
107
|
private readonly _onAuthorizationUrl: ((url: string) => void) | undefined;
|
|
89
108
|
|
|
90
109
|
/**
|
|
91
110
|
* @param interactive When true (e.g. `mcp auth` CLI), opens browser for OAuth.
|
|
92
111
|
* When false (daemon), logs a message instead.
|
|
93
|
-
* @param callbackTransport Which transport to use for the OAuth redirect.
|
|
94
|
-
* - `"loopback"` (default): localhost HTTP server — for desktop clients.
|
|
95
|
-
* - `"gateway"`: platform ingress + callback registry — for Docker/platform.
|
|
96
112
|
* @param options Additional options for the provider.
|
|
97
113
|
*/
|
|
98
114
|
constructor(
|
|
99
115
|
serverId: string,
|
|
100
116
|
serverUrl: string,
|
|
101
117
|
interactive = false,
|
|
102
|
-
callbackTransport: McpOAuthCallbackTransport = "loopback",
|
|
103
118
|
options: McpOAuthProviderOptions = {},
|
|
104
119
|
) {
|
|
105
120
|
this.serverId = serverId;
|
|
106
121
|
this.serverUrl = serverUrl;
|
|
107
122
|
this.interactive = interactive;
|
|
108
|
-
this.callbackTransport = callbackTransport;
|
|
109
123
|
this._onAuthorizationUrl = options.onAuthorizationUrl;
|
|
110
124
|
}
|
|
111
125
|
|
|
@@ -117,13 +131,36 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
117
131
|
|
|
118
132
|
// --- clientMetadata ---
|
|
119
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Identity presented at dynamic registration, and on the consent screen
|
|
136
|
+
* the user reads before approving.
|
|
137
|
+
*
|
|
138
|
+
* The client is this assistant, not the Vellum product and not the plugin
|
|
139
|
+
* that declared the server: each assistant registers separately, with its
|
|
140
|
+
* own redirect URI, so its own name is what makes the consent screen
|
|
141
|
+
* meaningful when a person runs several. `software_id` carries the
|
|
142
|
+
* assistant id so a server can correlate an assistant's registrations
|
|
143
|
+
* across servers without treating every assistant as the same client.
|
|
144
|
+
*
|
|
145
|
+
* `logo_uri` is the Vellum mark rather than the assistant's own avatar.
|
|
146
|
+
* An authorization server fetching a logo is anonymous, and the avatar is
|
|
147
|
+
* served only behind authentication, so there is no per-assistant URL to
|
|
148
|
+
* give it. A stable public identity per assistant is what would supply
|
|
149
|
+
* one, and the same prerequisite would let this move to Client ID
|
|
150
|
+
* Metadata Documents and drop registration altogether.
|
|
151
|
+
*/
|
|
120
152
|
get clientMetadata(): OAuthClientMetadata {
|
|
153
|
+
const assistantName = getAssistantName();
|
|
154
|
+
const assistantId = getPlatformAssistantId().trim();
|
|
121
155
|
return {
|
|
122
|
-
client_name: "Vellum Assistant",
|
|
156
|
+
client_name: assistantName ?? "Vellum Assistant",
|
|
157
|
+
logo_uri: CLIENT_LOGO_URI,
|
|
123
158
|
redirect_uris: this._redirectUrl ? [this._redirectUrl] : [],
|
|
124
159
|
token_endpoint_auth_method: "none",
|
|
125
160
|
grant_types: ["authorization_code", "refresh_token"],
|
|
126
161
|
response_types: ["code"],
|
|
162
|
+
...(assistantId.length > 0 && { software_id: assistantId }),
|
|
163
|
+
software_version: APP_VERSION,
|
|
127
164
|
};
|
|
128
165
|
}
|
|
129
166
|
|
|
@@ -214,13 +251,26 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
214
251
|
|
|
215
252
|
// --- Client Information ---
|
|
216
253
|
|
|
254
|
+
/**
|
|
255
|
+
* The stored registration, or `undefined` to make the SDK register a new
|
|
256
|
+
* one.
|
|
257
|
+
*
|
|
258
|
+
* Returning the stored value is the normal case: dynamic registration
|
|
259
|
+
* writes a record on the authorization server, so re-registering per
|
|
260
|
+
* attempt accumulates records nobody cleans up. It is withheld only when
|
|
261
|
+
* the registration provably no longer applies, because the redirect URI
|
|
262
|
+
* it was made for changed or because a different authorization server now
|
|
263
|
+
* fronts the resource.
|
|
264
|
+
*/
|
|
217
265
|
async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {
|
|
218
266
|
const raw = await getSecureKeyAsync(clientInfoKey(this.serverId));
|
|
219
267
|
if (!raw) {
|
|
220
268
|
return undefined;
|
|
221
269
|
}
|
|
270
|
+
|
|
271
|
+
let info: OAuthClientInformationMixed;
|
|
222
272
|
try {
|
|
223
|
-
|
|
273
|
+
info = JSON.parse(raw) as OAuthClientInformationMixed;
|
|
224
274
|
} catch {
|
|
225
275
|
log.warn(
|
|
226
276
|
{ serverId: this.serverId },
|
|
@@ -228,6 +278,16 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
228
278
|
);
|
|
229
279
|
return undefined;
|
|
230
280
|
}
|
|
281
|
+
|
|
282
|
+
const stale = await this.describeStaleBinding();
|
|
283
|
+
if (stale) {
|
|
284
|
+
log.info(
|
|
285
|
+
{ serverId: this.serverId, reason: stale },
|
|
286
|
+
"Stored client registration no longer applies; registering again",
|
|
287
|
+
);
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
return info;
|
|
231
291
|
}
|
|
232
292
|
|
|
233
293
|
async saveClientInformation(
|
|
@@ -244,9 +304,76 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
244
304
|
);
|
|
245
305
|
return;
|
|
246
306
|
}
|
|
307
|
+
|
|
308
|
+
// Record what the registration was made against, so a later run can tell
|
|
309
|
+
// whether reusing it is still valid.
|
|
310
|
+
if (this._redirectUrl) {
|
|
311
|
+
const binding: ClientRegistrationBinding = {
|
|
312
|
+
issuer: await this.currentIssuer(),
|
|
313
|
+
redirectUri: this._redirectUrl,
|
|
314
|
+
};
|
|
315
|
+
const boundOk = await setSecureKeyAsync(
|
|
316
|
+
clientBindingKey(this.serverId),
|
|
317
|
+
JSON.stringify(binding),
|
|
318
|
+
);
|
|
319
|
+
if (!boundOk) {
|
|
320
|
+
log.warn(
|
|
321
|
+
{ serverId: this.serverId },
|
|
322
|
+
"Failed to persist OAuth client binding; the registration will be remade on the next flow",
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
247
327
|
log.info({ serverId: this.serverId }, "OAuth client information saved");
|
|
248
328
|
}
|
|
249
329
|
|
|
330
|
+
/** Issuer of the authorization server currently in play, when known. */
|
|
331
|
+
private async currentIssuer(): Promise<string | null> {
|
|
332
|
+
const state = await this.discoveryState();
|
|
333
|
+
return (
|
|
334
|
+
state?.authorizationServerMetadata?.issuer ??
|
|
335
|
+
state?.authorizationServerUrl ??
|
|
336
|
+
null
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Why the stored registration cannot be reused, or null when it can.
|
|
342
|
+
*
|
|
343
|
+
* The redirect URI is only checked while a flow is being prepared:
|
|
344
|
+
* outside one there is no redirect URI to compare against, and a silent
|
|
345
|
+
* reconnect must not be turned into a registration.
|
|
346
|
+
*
|
|
347
|
+
* The issuer is only checked when discovery has run. An unverifiable
|
|
348
|
+
* issuer keeps the registration rather than discarding it, since the
|
|
349
|
+
* redirect check already covers the case this plugin actually changes.
|
|
350
|
+
*/
|
|
351
|
+
private async describeStaleBinding(): Promise<string | null> {
|
|
352
|
+
const raw = await getSecureKeyAsync(clientBindingKey(this.serverId));
|
|
353
|
+
if (!raw) {
|
|
354
|
+
// Registered before the binding was recorded. Keep it: discarding
|
|
355
|
+
// every pre-existing registration would remake all of them at once.
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
let binding: ClientRegistrationBinding;
|
|
360
|
+
try {
|
|
361
|
+
binding = JSON.parse(raw) as ClientRegistrationBinding;
|
|
362
|
+
} catch {
|
|
363
|
+
return "stored binding is unreadable";
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (this._redirectUrl && binding.redirectUri !== this._redirectUrl) {
|
|
367
|
+
return "redirect URI changed";
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const issuer = await this.currentIssuer();
|
|
371
|
+
if (issuer && binding.issuer && binding.issuer !== issuer) {
|
|
372
|
+
return "authorization server changed";
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
|
|
250
377
|
// --- Code Verifier (in-memory, ephemeral) ---
|
|
251
378
|
|
|
252
379
|
async saveCodeVerifier(verifier: string): Promise<void> {
|
|
@@ -312,31 +439,24 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
312
439
|
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
|
|
313
440
|
const url = authorizationUrl.toString();
|
|
314
441
|
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
this._gatewayCodeResolve &&
|
|
320
|
-
this._gatewayCodeReject
|
|
321
|
-
) {
|
|
442
|
+
// The callback is demultiplexed by OAuth `state`, and the SDK mints it
|
|
443
|
+
// while building this URL, so this is the earliest point the pending
|
|
444
|
+
// callback can be registered.
|
|
445
|
+
if (this._codeResolve && this._codeReject) {
|
|
322
446
|
const sdkState = authorizationUrl.searchParams.get("state");
|
|
323
447
|
if (sdkState) {
|
|
324
448
|
// Dynamic import to avoid circular deps
|
|
325
449
|
const { registerPendingCallback } =
|
|
326
450
|
await import("../security/oauth-callback-registry.js");
|
|
327
|
-
registerPendingCallback(
|
|
328
|
-
sdkState,
|
|
329
|
-
this._gatewayCodeResolve,
|
|
330
|
-
this._gatewayCodeReject,
|
|
331
|
-
);
|
|
451
|
+
registerPendingCallback(sdkState, this._codeResolve, this._codeReject);
|
|
332
452
|
log.info(
|
|
333
453
|
{ serverId: this.serverId, state: sdkState },
|
|
334
|
-
"MCP OAuth
|
|
454
|
+
"MCP OAuth callback registered with SDK state",
|
|
335
455
|
);
|
|
336
456
|
} else {
|
|
337
457
|
log.warn(
|
|
338
458
|
{ serverId: this.serverId },
|
|
339
|
-
"Authorization URL missing state parameter
|
|
459
|
+
"Authorization URL missing state parameter, callback may not resolve",
|
|
340
460
|
);
|
|
341
461
|
}
|
|
342
462
|
}
|
|
@@ -404,6 +524,10 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
404
524
|
"OAuth client information key not found in secure storage (already removed)",
|
|
405
525
|
);
|
|
406
526
|
}
|
|
527
|
+
// The binding describes the registration being dropped, so it goes
|
|
528
|
+
// with it. Leaving it would let a later registration inherit the
|
|
529
|
+
// previous one's issuer and redirect URI.
|
|
530
|
+
await deleteSecureKeyAsync(clientBindingKey(this.serverId));
|
|
407
531
|
}
|
|
408
532
|
if (scope === "all" || scope === "verifier") {
|
|
409
533
|
this._codeVerifier = undefined;
|
|
@@ -425,52 +549,29 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
425
549
|
}
|
|
426
550
|
}
|
|
427
551
|
|
|
428
|
-
// --- Callback
|
|
552
|
+
// --- Callback ---
|
|
429
553
|
|
|
430
554
|
/**
|
|
431
|
-
*
|
|
555
|
+
* Prepare to receive the OAuth callback.
|
|
432
556
|
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
557
|
+
* Resolves the shared redirect URI the gateway serves and creates a
|
|
558
|
+
* deferred code promise. There is no listener to start: the gateway
|
|
559
|
+
* already accepts `webhooks/oauth/callback` and forwards it to
|
|
560
|
+
* `POST /v1/internal/oauth/callback`, which resolves the waiting flow
|
|
561
|
+
* by matching the arriving OAuth `state`.
|
|
437
562
|
*
|
|
438
|
-
*
|
|
563
|
+
* `registerPendingCallback` is deferred to `redirectToAuthorization`,
|
|
564
|
+
* the first point at which the SDK-generated `state` is known.
|
|
439
565
|
*/
|
|
440
|
-
startCallbackServer(): Promise<McpOAuthCallbackResult> {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
444
|
-
return this.startLoopbackServer();
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Gateway transport: resolve the public redirect URL and create a
|
|
449
|
-
* deferred code promise. The actual `registerPendingCallback` call
|
|
450
|
-
* is deferred until `redirectToAuthorization` where we can extract
|
|
451
|
-
* the SDK-generated `state` parameter from the authorization URL.
|
|
452
|
-
*/
|
|
453
|
-
private async startGatewayCallback(): Promise<McpOAuthCallbackResult> {
|
|
454
|
-
const { resolveCallbackUrl } =
|
|
455
|
-
await import("../inbound/platform-callback-registration.js");
|
|
456
|
-
const { getOAuthCallbackUrl } =
|
|
457
|
-
await import("../inbound/public-ingress-urls.js");
|
|
458
|
-
const { loadConfig } = await import("../config/loader.js");
|
|
459
|
-
|
|
460
|
-
const appConfig = loadConfig();
|
|
461
|
-
const redirectUrl = await resolveCallbackUrl(
|
|
462
|
-
() => getOAuthCallbackUrl(appConfig),
|
|
463
|
-
"webhooks/oauth/callback",
|
|
464
|
-
"mcp_oauth",
|
|
465
|
-
);
|
|
566
|
+
async startCallbackServer(): Promise<McpOAuthCallbackResult> {
|
|
567
|
+
const { resolveOauthCallbackUrl } =
|
|
568
|
+
await import("../inbound/oauth-callback-url.js");
|
|
466
569
|
|
|
467
|
-
this._redirectUrl =
|
|
570
|
+
this._redirectUrl = await resolveOauthCallbackUrl();
|
|
468
571
|
|
|
469
|
-
// Create a deferred promise — it will be wired to the callback
|
|
470
|
-
// registry in redirectToAuthorization() once we know the SDK's state.
|
|
471
572
|
const codePromise = new Promise<string>((resolve, reject) => {
|
|
472
|
-
this.
|
|
473
|
-
this.
|
|
573
|
+
this._codeResolve = resolve;
|
|
574
|
+
this._codeReject = reject;
|
|
474
575
|
});
|
|
475
576
|
this._codePromise = codePromise;
|
|
476
577
|
|
|
@@ -481,154 +582,32 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
481
582
|
void codePromise.catch(() => {});
|
|
482
583
|
|
|
483
584
|
log.info(
|
|
484
|
-
{ serverId: this.serverId, redirectUrl },
|
|
485
|
-
"MCP OAuth
|
|
585
|
+
{ serverId: this.serverId, redirectUrl: this._redirectUrl },
|
|
586
|
+
"MCP OAuth callback prepared (awaiting state from auth URL)",
|
|
486
587
|
);
|
|
487
588
|
|
|
488
589
|
return { codePromise };
|
|
489
590
|
}
|
|
490
591
|
|
|
491
|
-
/**
|
|
492
|
-
* Loopback transport: start a temporary HTTP server on localhost.
|
|
493
|
-
*/
|
|
494
|
-
private startLoopbackServer(): Promise<McpOAuthCallbackResult> {
|
|
495
|
-
return new Promise((resolveSetup, rejectSetup) => {
|
|
496
|
-
let settled = false;
|
|
497
|
-
let listening = false;
|
|
498
|
-
let codeResolve: (code: string) => void;
|
|
499
|
-
let codeReject: (err: Error) => void;
|
|
500
|
-
|
|
501
|
-
const codePromise = new Promise<string>((resolve, reject) => {
|
|
502
|
-
codeResolve = resolve;
|
|
503
|
-
codeReject = reject;
|
|
504
|
-
});
|
|
505
|
-
this._codePromise = codePromise;
|
|
506
|
-
|
|
507
|
-
const server = createServer((req, res) => {
|
|
508
|
-
if (settled) {
|
|
509
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
510
|
-
res.end(renderPage("Authorization already completed", false));
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
515
|
-
if (url.pathname !== CALLBACK_PATH) {
|
|
516
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
517
|
-
res.end("Not found");
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const code = url.searchParams.get("code");
|
|
522
|
-
const error = url.searchParams.get("error");
|
|
523
|
-
|
|
524
|
-
settled = true;
|
|
525
|
-
|
|
526
|
-
if (error) {
|
|
527
|
-
const errorDesc = url.searchParams.get("error_description") ?? error;
|
|
528
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
529
|
-
res.end(renderPage(`Authorization failed: ${errorDesc}`, false));
|
|
530
|
-
cleanup();
|
|
531
|
-
codeReject(new Error(`MCP OAuth authorization denied: ${error}`));
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
if (!code) {
|
|
536
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
537
|
-
res.end(renderPage("Missing authorization code", false));
|
|
538
|
-
cleanup();
|
|
539
|
-
codeReject(
|
|
540
|
-
new Error("MCP OAuth callback missing authorization code"),
|
|
541
|
-
);
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
546
|
-
res.end(
|
|
547
|
-
renderPage("Authorization successful! You can close this tab.", true),
|
|
548
|
-
);
|
|
549
|
-
cleanup();
|
|
550
|
-
codeResolve(code);
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
this.callbackServer = server;
|
|
554
|
-
|
|
555
|
-
const timeout = setTimeout(() => {
|
|
556
|
-
if (!settled) {
|
|
557
|
-
settled = true;
|
|
558
|
-
cleanup();
|
|
559
|
-
codeReject(new Error("MCP OAuth callback timed out"));
|
|
560
|
-
}
|
|
561
|
-
}, CALLBACK_TIMEOUT_MS);
|
|
562
|
-
if (typeof timeout === "object" && "unref" in timeout) {
|
|
563
|
-
timeout.unref();
|
|
564
|
-
}
|
|
565
|
-
this.callbackTimeout = timeout;
|
|
566
|
-
|
|
567
|
-
const cleanup = () => {
|
|
568
|
-
if (this.callbackTimeout) {
|
|
569
|
-
clearTimeout(this.callbackTimeout);
|
|
570
|
-
this.callbackTimeout = null;
|
|
571
|
-
}
|
|
572
|
-
if (this.callbackServer) {
|
|
573
|
-
this.callbackServer.close();
|
|
574
|
-
this.callbackServer = null;
|
|
575
|
-
}
|
|
576
|
-
};
|
|
577
|
-
|
|
578
|
-
server.listen(0, "127.0.0.1", () => {
|
|
579
|
-
const addr = server.address() as { port: number };
|
|
580
|
-
this._redirectUrl = `http://127.0.0.1:${addr.port}${CALLBACK_PATH}`;
|
|
581
|
-
listening = true;
|
|
582
|
-
log.info(
|
|
583
|
-
{ serverId: this.serverId, redirectUrl: this._redirectUrl },
|
|
584
|
-
"OAuth callback server started",
|
|
585
|
-
);
|
|
586
|
-
resolveSetup({ codePromise });
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
server.on("error", (err) => {
|
|
590
|
-
const message = `MCP OAuth callback server error: ${err.message}`;
|
|
591
|
-
if (!listening) {
|
|
592
|
-
settled = true;
|
|
593
|
-
cleanup();
|
|
594
|
-
rejectSetup(new Error(message));
|
|
595
|
-
} else if (!settled) {
|
|
596
|
-
settled = true;
|
|
597
|
-
cleanup();
|
|
598
|
-
codeReject(new Error(message));
|
|
599
|
-
}
|
|
600
|
-
});
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
/** Returns the code promise from the running callback server. */
|
|
592
|
+
/** Returns the code promise created by {@link startCallbackServer}. */
|
|
605
593
|
waitForCode(): Promise<string> {
|
|
606
594
|
if (!this._codePromise) {
|
|
607
595
|
throw new Error(
|
|
608
|
-
"Callback
|
|
596
|
+
"Callback not prepared, call startCallbackServer() first",
|
|
609
597
|
);
|
|
610
598
|
}
|
|
611
599
|
return this._codePromise;
|
|
612
600
|
}
|
|
613
601
|
|
|
614
|
-
/**
|
|
602
|
+
/**
|
|
603
|
+
* Abandon a prepared callback. Rejects the deferred promise so callers
|
|
604
|
+
* awaiting the code do not hang until the registry's own TTL fires.
|
|
605
|
+
*/
|
|
615
606
|
stopCallbackServer(): void {
|
|
616
|
-
if (this.
|
|
617
|
-
|
|
618
|
-
this.
|
|
619
|
-
|
|
620
|
-
if (this.callbackServer) {
|
|
621
|
-
this.callbackServer.close();
|
|
622
|
-
this.callbackServer = null;
|
|
623
|
-
}
|
|
624
|
-
// Gateway transport cleanup — reject the deferred promise so callers
|
|
625
|
-
// awaiting codePromise don't hang indefinitely.
|
|
626
|
-
if (this._gatewayCodeReject) {
|
|
627
|
-
this._gatewayCodeReject(
|
|
628
|
-
new Error("MCP OAuth gateway callback cancelled"),
|
|
629
|
-
);
|
|
630
|
-
this._gatewayCodeResolve = undefined;
|
|
631
|
-
this._gatewayCodeReject = undefined;
|
|
607
|
+
if (this._codeReject) {
|
|
608
|
+
this._codeReject(new Error("MCP OAuth callback cancelled"));
|
|
609
|
+
this._codeResolve = undefined;
|
|
610
|
+
this._codeReject = undefined;
|
|
632
611
|
}
|
|
633
612
|
}
|
|
634
613
|
}
|
|
@@ -650,14 +629,17 @@ export async function hasMcpOAuthTokens(serverId: string): Promise<boolean> {
|
|
|
650
629
|
export async function deleteMcpOAuthCredentials(
|
|
651
630
|
serverId: string,
|
|
652
631
|
): Promise<{ ok: boolean; failedKeys: string[] }> {
|
|
653
|
-
const [tokensResult, clientResult, discoveryResult] =
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
632
|
+
const [tokensResult, clientResult, bindingResult, discoveryResult] =
|
|
633
|
+
await Promise.all([
|
|
634
|
+
deleteSecureKeyAsync(tokensKey(serverId)),
|
|
635
|
+
deleteSecureKeyAsync(clientInfoKey(serverId)),
|
|
636
|
+
deleteSecureKeyAsync(clientBindingKey(serverId)),
|
|
637
|
+
deleteSecureKeyAsync(discoveryKey(serverId)),
|
|
638
|
+
]);
|
|
658
639
|
const results = [
|
|
659
640
|
{ key: "tokens", result: tokensResult },
|
|
660
641
|
{ key: "client_info", result: clientResult },
|
|
642
|
+
{ key: "client_binding", result: bindingResult },
|
|
661
643
|
{ key: "discovery", result: discoveryResult },
|
|
662
644
|
];
|
|
663
645
|
const failedKeys = results
|
|
@@ -678,20 +660,3 @@ export async function deleteMcpOAuthCredentials(
|
|
|
678
660
|
);
|
|
679
661
|
return { ok, failedKeys };
|
|
680
662
|
}
|
|
681
|
-
|
|
682
|
-
// --- HTML rendering ---
|
|
683
|
-
|
|
684
|
-
function escapeHtml(s: string): string {
|
|
685
|
-
return s
|
|
686
|
-
.replace(/&/g, "&")
|
|
687
|
-
.replace(/</g, "<")
|
|
688
|
-
.replace(/>/g, ">")
|
|
689
|
-
.replace(/"/g, """)
|
|
690
|
-
.replace(/'/g, "'");
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function renderPage(message: string, success: boolean): string {
|
|
694
|
-
const title = success ? "Authorization Successful" : "Authorization Failed";
|
|
695
|
-
const color = success ? "#4CAF50" : "#f44336";
|
|
696
|
-
return `<!DOCTYPE html><html><head><title>${escapeHtml(title)}</title><style>body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#f5f5f5}div{text-align:center;padding:2rem;background:white;border-radius:8px;box-shadow:0 2px 4px rgba(0,0,0,0.1)}h1{color:${color}}</style></head><body><div><h1>${escapeHtml(title)}</h1><p>${escapeHtml(message)}</p></div></body></html>`;
|
|
697
|
-
}
|