@agent-native/core 0.128.3 → 0.129.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +91 -0
- package/corpus/core/docs/content/template-design-collaboration-and-full-apps.mdx +3 -1
- package/corpus/core/package.json +6 -3
- package/corpus/core/src/a2a/client.ts +18 -4
- package/corpus/core/src/a2a/server.ts +19 -3
- package/corpus/core/src/a2a/types.ts +7 -0
- package/corpus/core/src/action.ts +43 -16
- package/corpus/core/src/agent/engine/ai-sdk-engine.ts +14 -10
- package/corpus/core/src/agent/production-agent.ts +41 -3
- package/corpus/core/src/agent/thread-data-builder.ts +5 -0
- package/corpus/core/src/chat-threads/migrations.ts +53 -0
- package/corpus/core/src/cli/create.ts +4 -0
- package/corpus/core/src/cli/skills-content/design-visual-edit-skill.ts +36 -16
- package/corpus/core/src/cli/skills.ts +16 -0
- package/corpus/core/src/client/AgentPanel.tsx +9 -5
- package/corpus/core/src/client/AssistantChat.tsx +138 -46
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +16 -10
- package/corpus/core/src/client/agent-chat-adapter.ts +21 -3
- package/corpus/core/src/client/analytics.ts +159 -102
- package/corpus/core/src/client/chat/legacy-chart-shorthand.tsx +479 -0
- package/corpus/core/src/client/chat/markdown-renderer.tsx +23 -3
- package/corpus/core/src/client/chat/message-components.tsx +12 -5
- package/corpus/core/src/client/chat/runtime.ts +2 -1
- package/corpus/core/src/client/chat/tool-call-display.tsx +25 -2
- package/corpus/core/src/client/chat-model-groups.ts +3 -0
- package/corpus/core/src/client/client-status-requests.ts +161 -0
- package/corpus/core/src/client/settings/VoiceTranscriptionSection.tsx +2 -2
- package/corpus/core/src/client/sse-event-processor.ts +15 -3
- package/corpus/core/src/client/use-agent-engine-configured.ts +54 -50
- package/corpus/core/src/client/use-chat-models.ts +16 -11
- package/corpus/core/src/client/use-chat-threads.ts +32 -9
- package/corpus/core/src/client/use-session.ts +4 -5
- package/corpus/core/src/db/client.ts +83 -8
- package/corpus/core/src/db/ensure-additive-columns.ts +31 -20
- package/corpus/core/src/db/migrations.ts +22 -2
- package/corpus/core/src/extensions/actions.ts +1 -1
- package/corpus/core/src/mcp/approval-store.ts +99 -0
- package/corpus/core/src/mcp/build-server.ts +579 -303
- package/corpus/core/src/mcp/embed-app.ts +1 -1
- package/corpus/core/src/mcp/oauth-client-metadata.ts +380 -0
- package/corpus/core/src/mcp/oauth-route.ts +57 -52
- package/corpus/core/src/mcp/screen-memory-stdio.ts +400 -378
- package/corpus/core/src/mcp/server.ts +29 -145
- package/corpus/core/src/mcp/stdio.ts +72 -80
- package/corpus/core/src/mcp-client/manager.ts +6 -10
- package/corpus/core/src/mcp-client/oauth-client.ts +127 -36
- package/corpus/core/src/mcp-client/oauth-routes.ts +16 -6
- package/corpus/core/src/mcp-client/routes.ts +6 -5
- package/corpus/core/src/server/agent-capabilities.ts +20 -0
- package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +27 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +18 -0
- package/corpus/core/src/server/transcribe-voice.ts +14 -5
- package/corpus/core/src/templates/default/.agents/skills/security/SKILL.md +13 -2
- package/corpus/core/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +5 -2
- package/corpus/core/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +18 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/security/SKILL.md +13 -2
- package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +12 -0
- package/corpus/templates/analytics/.agents/skills/security/SKILL.md +13 -2
- package/corpus/templates/analytics/actions/generate-chart.ts +2 -2
- package/corpus/templates/analytics/app/components/dashboard/DashboardPanelSkeleton.tsx +1 -10
- package/corpus/templates/analytics/app/global.css +0 -31
- package/corpus/templates/analytics/app/lib/sql-query.ts +46 -20
- package/corpus/templates/analytics/changelog/2026-07-28-analytics-dashboard-loading-states-now-use-a-simpler-standar.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-28-analytics-dashboards-now-load-first-party-charts-reliably-in.md +6 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-cache.ts +42 -16
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +4 -2
- package/corpus/templates/analytics/shared/sql-query-limits.ts +1 -0
- package/corpus/templates/assets/.agents/skills/security/SKILL.md +13 -2
- package/corpus/templates/brain/.agents/skills/security/SKILL.md +13 -2
- package/corpus/templates/calendar/.agents/skills/security/SKILL.md +13 -2
- package/corpus/templates/chat/.agents/skills/security/SKILL.md +13 -2
- package/corpus/templates/clips/.agents/skills/security/SKILL.md +13 -2
- package/corpus/templates/clips/app/components/player/player-controls.tsx +3 -6
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +6 -2
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +3 -1
- package/corpus/templates/clips/changelog/2026-07-28-copied-share-links-no-longer-leave-an-unnecessary-confirmati.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-28-playback-skip-controls-no-longer-show-overlapping-numbers.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-28-public-clips-share-buttons-now-use-a-clean-text-only-label.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +14 -17
- package/corpus/templates/clips/desktop/src/overlays/live-audio-bars.tsx +3 -1
- package/corpus/templates/clips/desktop/src/styles.css +13 -10
- package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +2 -2
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +36 -0
- package/corpus/templates/content/.agents/skills/security/SKILL.md +13 -2
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +155 -34
- package/corpus/templates/content/actions/_builder-docs-client.ts +151 -30
- package/corpus/templates/content/actions/stage-builder-source-bulk-update.ts +5 -2
- package/corpus/templates/content/app/root.tsx +35 -14
- package/corpus/templates/content/changelog/2026-07-28-builder-backed-files-media-fields-now-accept-direct-file-url.md +6 -0
- package/corpus/templates/content/changelog/2026-07-28-builder-content-reads-now-negotiate-the-newest-mcp-protocol-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-28-public-content-pages-load-faster-before-the-optional-ask-sid.md +6 -0
- package/corpus/templates/design/.agents/skills/security/SKILL.md +13 -2
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +36 -16
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +42 -3
- package/corpus/templates/design/actions/open-visual-edit.ts +2 -1
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +2 -2
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +14 -15
- package/corpus/templates/design/app/components/design/ResponsiveInteractBar.tsx +314 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +88 -3
- package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +1 -1
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +7 -6
- package/corpus/templates/design/app/i18n-data.ts +56 -11
- package/corpus/templates/design/app/i18n-responsive-interact.ts +183 -0
- package/corpus/templates/design/app/lib/agent-chat.ts +60 -0
- package/corpus/templates/design/app/pages/design-editor/responsive-interact.ts +89 -0
- package/corpus/templates/design/changelog/2026-07-28-design-edits-can-be-applied-through-the-host-coding-agent-fr.md +6 -0
- package/corpus/templates/design/changelog/2026-07-28-interact-mode-can-preview-designs-in-responsive-device-sizes.md +6 -0
- package/corpus/templates/design/changelog/2026-07-28-interact-mode-now-opens-a-full-screen-responsive-preview-wit.md +6 -0
- package/corpus/templates/design/changelog/2026-07-28-show-or-hide-design-editing-chrome-with-cmd-on-apple-devices.md +2 -2
- package/corpus/templates/design/changelog/2026-07-28-visual-edit-now-always-shows-the-live-running-app-in-the-can.md +6 -0
- package/corpus/templates/dispatch/.agents/skills/security/SKILL.md +13 -2
- package/corpus/templates/forms/.agents/skills/security/SKILL.md +13 -2
- package/corpus/templates/macros/.agents/skills/security/SKILL.md +13 -2
- package/corpus/templates/mail/.agents/skills/security/SKILL.md +13 -2
- package/corpus/templates/plan/.agents/skills/security/SKILL.md +13 -2
- package/corpus/templates/slides/.agents/skills/security/SKILL.md +13 -2
- package/corpus/templates/tasks/.agents/skills/security/SKILL.md +13 -2
- package/corpus/toolkit/CHANGELOG.md +13 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/chat-history.css +1 -2
- package/corpus/toolkit/src/composer/TiptapComposer.tsx +45 -0
- package/dist/a2a/client.d.ts +7 -0
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +8 -3
- package/dist/a2a/client.js.map +1 -1
- package/dist/a2a/server.d.ts.map +1 -1
- package/dist/a2a/server.js +18 -3
- package/dist/a2a/server.js.map +1 -1
- package/dist/a2a/types.d.ts +7 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/action.d.ts +15 -1
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +39 -16
- package/dist/action.js.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.js +14 -10
- package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +35 -7
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/thread-data-builder.d.ts +2 -0
- package/dist/agent/thread-data-builder.d.ts.map +1 -1
- package/dist/agent/thread-data-builder.js +3 -0
- package/dist/agent/thread-data-builder.js.map +1 -1
- package/dist/chat-threads/migrations.d.ts +21 -0
- package/dist/chat-threads/migrations.d.ts.map +1 -0
- package/dist/chat-threads/migrations.js +45 -0
- package/dist/chat-threads/migrations.js.map +1 -0
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +4 -0
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/skills-content/design-visual-edit-skill.d.ts +1 -1
- package/dist/cli/skills-content/design-visual-edit-skill.d.ts.map +1 -1
- package/dist/cli/skills-content/design-visual-edit-skill.js +36 -16
- package/dist/cli/skills-content/design-visual-edit-skill.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +15 -0
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +6 -5
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +37 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +41 -8
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +11 -10
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +19 -3
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +150 -101
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/chat/legacy-chart-shorthand.d.ts +29 -0
- package/dist/client/chat/legacy-chart-shorthand.d.ts.map +1 -0
- package/dist/client/chat/legacy-chart-shorthand.js +330 -0
- package/dist/client/chat/legacy-chart-shorthand.js.map +1 -0
- package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
- package/dist/client/chat/markdown-renderer.js +8 -2
- package/dist/client/chat/markdown-renderer.js.map +1 -1
- package/dist/client/chat/message-components.d.ts +2 -1
- package/dist/client/chat/message-components.d.ts.map +1 -1
- package/dist/client/chat/message-components.js +11 -4
- package/dist/client/chat/message-components.js.map +1 -1
- package/dist/client/chat/runtime.d.ts.map +1 -1
- package/dist/client/chat/runtime.js +2 -1
- package/dist/client/chat/runtime.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts +4 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +14 -8
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/chat-model-groups.d.ts.map +1 -1
- package/dist/client/chat-model-groups.js +3 -0
- package/dist/client/chat-model-groups.js.map +1 -1
- package/dist/client/client-status-requests.d.ts +14 -0
- package/dist/client/client-status-requests.d.ts.map +1 -0
- package/dist/client/client-status-requests.js +126 -0
- package/dist/client/client-status-requests.js.map +1 -0
- package/dist/client/settings/VoiceTranscriptionSection.js +1 -1
- package/dist/client/settings/VoiceTranscriptionSection.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +7 -0
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +8 -3
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
- package/dist/client/use-agent-engine-configured.js +29 -43
- package/dist/client/use-agent-engine-configured.js.map +1 -1
- package/dist/client/use-chat-models.d.ts.map +1 -1
- package/dist/client/use-chat-models.js +11 -11
- package/dist/client/use-chat-models.js.map +1 -1
- package/dist/client/use-chat-threads.d.ts.map +1 -1
- package/dist/client/use-chat-threads.js +23 -7
- package/dist/client/use-chat-threads.js.map +1 -1
- package/dist/client/use-session.d.ts.map +1 -1
- package/dist/client/use-session.js +4 -4
- package/dist/client/use-session.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +48 -4
- package/dist/db/client.js.map +1 -1
- package/dist/db/ensure-additive-columns.d.ts.map +1 -1
- package/dist/db/ensure-additive-columns.js +32 -21
- package/dist/db/ensure-additive-columns.js.map +1 -1
- package/dist/db/migrations.d.ts +9 -0
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +14 -3
- package/dist/db/migrations.js.map +1 -1
- package/dist/extensions/actions.js +1 -1
- package/dist/extensions/actions.js.map +1 -1
- package/dist/mcp/approval-store.d.ts +16 -0
- package/dist/mcp/approval-store.d.ts.map +1 -0
- package/dist/mcp/approval-store.js +85 -0
- package/dist/mcp/approval-store.js.map +1 -0
- package/dist/mcp/build-server.d.ts +1 -34
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +220 -23
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.js +1 -1
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/mcp/oauth-client-metadata.d.ts +15 -0
- package/dist/mcp/oauth-client-metadata.d.ts.map +1 -0
- package/dist/mcp/oauth-client-metadata.js +297 -0
- package/dist/mcp/oauth-client-metadata.js.map +1 -0
- package/dist/mcp/oauth-route.d.ts.map +1 -1
- package/dist/mcp/oauth-route.js +39 -53
- package/dist/mcp/oauth-route.js.map +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
- package/dist/mcp/screen-memory-stdio.js +304 -296
- package/dist/mcp/screen-memory-stdio.js.map +1 -1
- package/dist/mcp/server.d.ts +12 -25
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +24 -133
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/stdio.d.ts.map +1 -1
- package/dist/mcp/stdio.js +36 -35
- package/dist/mcp/stdio.js.map +1 -1
- package/dist/mcp-client/manager.d.ts.map +1 -1
- package/dist/mcp-client/manager.js +6 -6
- package/dist/mcp-client/manager.js.map +1 -1
- package/dist/mcp-client/oauth-client.d.ts +13 -17
- package/dist/mcp-client/oauth-client.d.ts.map +1 -1
- package/dist/mcp-client/oauth-client.js +74 -16
- package/dist/mcp-client/oauth-client.js.map +1 -1
- package/dist/mcp-client/oauth-routes.d.ts +2 -2
- package/dist/mcp-client/oauth-routes.d.ts.map +1 -1
- package/dist/mcp-client/oauth-routes.js +16 -6
- package/dist/mcp-client/oauth-routes.js.map +1 -1
- package/dist/mcp-client/routes.js +5 -5
- package/dist/mcp-client/routes.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-capabilities.d.ts.map +1 -1
- package/dist/server/agent-capabilities.js +21 -0
- package/dist/server/agent-capabilities.js.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.d.ts +14 -0
- package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.js +16 -0
- package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +11 -1
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/transcribe-voice.js +12 -6
- package/dist/server/transcribe-voice.js.map +1 -1
- package/dist/templates/chat/.agents/skills/security/SKILL.md +13 -2
- package/dist/templates/default/.agents/skills/security/SKILL.md +13 -2
- package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +5 -2
- package/dist/templates/workspace-core/.agents/skills/external-agents/SKILL.md +18 -0
- package/dist/templates/workspace-core/.agents/skills/security/SKILL.md +13 -2
- package/docs/content/template-design-collaboration-and-full-apps.mdx +3 -1
- package/package.json +7 -4
- package/src/a2a/client.ts +18 -4
- package/src/a2a/server.ts +19 -3
- package/src/a2a/types.ts +7 -0
- package/src/action.ts +43 -16
- package/src/agent/engine/ai-sdk-engine.ts +14 -10
- package/src/agent/production-agent.ts +41 -3
- package/src/agent/thread-data-builder.ts +5 -0
- package/src/chat-threads/migrations.ts +53 -0
- package/src/cli/create.ts +4 -0
- package/src/cli/skills-content/design-visual-edit-skill.ts +36 -16
- package/src/cli/skills.ts +16 -0
- package/src/client/AgentPanel.tsx +9 -5
- package/src/client/AssistantChat.tsx +138 -46
- package/src/client/MultiTabAssistantChat.tsx +16 -10
- package/src/client/agent-chat-adapter.ts +21 -3
- package/src/client/analytics.ts +159 -102
- package/src/client/chat/legacy-chart-shorthand.tsx +479 -0
- package/src/client/chat/markdown-renderer.tsx +23 -3
- package/src/client/chat/message-components.tsx +12 -5
- package/src/client/chat/runtime.ts +2 -1
- package/src/client/chat/tool-call-display.tsx +25 -2
- package/src/client/chat-model-groups.ts +3 -0
- package/src/client/client-status-requests.ts +161 -0
- package/src/client/settings/VoiceTranscriptionSection.tsx +2 -2
- package/src/client/sse-event-processor.ts +15 -3
- package/src/client/use-agent-engine-configured.ts +54 -50
- package/src/client/use-chat-models.ts +16 -11
- package/src/client/use-chat-threads.ts +32 -9
- package/src/client/use-session.ts +4 -5
- package/src/db/client.ts +83 -8
- package/src/db/ensure-additive-columns.ts +31 -20
- package/src/db/migrations.ts +22 -2
- package/src/extensions/actions.ts +1 -1
- package/src/mcp/approval-store.ts +99 -0
- package/src/mcp/build-server.ts +579 -303
- package/src/mcp/embed-app.ts +1 -1
- package/src/mcp/oauth-client-metadata.ts +380 -0
- package/src/mcp/oauth-route.ts +57 -52
- package/src/mcp/screen-memory-stdio.ts +400 -378
- package/src/mcp/server.ts +29 -145
- package/src/mcp/stdio.ts +72 -80
- package/src/mcp-client/manager.ts +6 -10
- package/src/mcp-client/oauth-client.ts +127 -36
- package/src/mcp-client/oauth-routes.ts +16 -6
- package/src/mcp-client/routes.ts +6 -5
- package/src/server/agent-capabilities.ts +20 -0
- package/src/server/agent-chat/action-filters-a2a.ts +27 -0
- package/src/server/agent-chat-plugin.ts +18 -0
- package/src/server/transcribe-voice.ts +14 -5
- package/src/templates/chat/.agents/skills/security/SKILL.md +13 -2
- package/src/templates/default/.agents/skills/security/SKILL.md +13 -2
- package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +5 -2
- package/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +18 -0
- package/src/templates/workspace-core/.agents/skills/security/SKILL.md +13 -2
|
@@ -2,7 +2,7 @@ import type { ActionMcpAppResourceConfig } from "../action.js";
|
|
|
2
2
|
import { MCP_APP_CHAT_BRIDGE_QUERY_PARAM } from "../shared/embed-auth.js";
|
|
3
3
|
|
|
4
4
|
const MCP_APP_IMPORT =
|
|
5
|
-
"https://esm.sh/@modelcontextprotocol/ext-apps@1.7.
|
|
5
|
+
"https://esm.sh/@modelcontextprotocol/ext-apps@1.7.5/app-with-deps";
|
|
6
6
|
|
|
7
7
|
export const MCP_APP_REQUEST_ORIGIN_CSP_SOURCE = "$requestOrigin";
|
|
8
8
|
const MCP_APP_WRAPPER_CHROME_HEIGHT = 44;
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { ssrfSafeFetch } from "../extensions/url-safety.js";
|
|
2
|
+
|
|
3
|
+
const CLIENT_METADATA_MAX_BYTES = 64 * 1024;
|
|
4
|
+
const CLIENT_METADATA_TIMEOUT_MS = 5_000;
|
|
5
|
+
const CLIENT_METADATA_MAX_REDIRECTS = 2;
|
|
6
|
+
const CLIENT_METADATA_MAX_CACHE_ENTRIES = 128;
|
|
7
|
+
const CLIENT_METADATA_MAX_CACHE_TTL_MS = 5 * 60_000;
|
|
8
|
+
|
|
9
|
+
const DISALLOWED_REDIRECT_SCHEMES = new Set([
|
|
10
|
+
"javascript:",
|
|
11
|
+
"data:",
|
|
12
|
+
"vbscript:",
|
|
13
|
+
"file:",
|
|
14
|
+
"blob:",
|
|
15
|
+
"about:",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export interface OAuthClientMetadataDocument {
|
|
19
|
+
clientId: string;
|
|
20
|
+
clientName: string;
|
|
21
|
+
redirectUris: string[];
|
|
22
|
+
grantTypes: string[];
|
|
23
|
+
responseTypes: string[];
|
|
24
|
+
tokenEndpointAuthMethod: "none";
|
|
25
|
+
applicationType: "native" | "web";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface CachedClientMetadata {
|
|
29
|
+
expiresAt: number;
|
|
30
|
+
metadata: OAuthClientMetadataDocument;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const clientMetadataCache = new Map<string, CachedClientMetadata>();
|
|
34
|
+
|
|
35
|
+
export function isUrlBasedOAuthClientId(clientId: string): boolean {
|
|
36
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(clientId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function validateOAuthClientMetadataUrl(clientId: string): URL {
|
|
40
|
+
if (!clientId || clientId !== clientId.trim() || clientId.length > 2048) {
|
|
41
|
+
throw new Error("Client metadata URL is invalid");
|
|
42
|
+
}
|
|
43
|
+
let url: URL;
|
|
44
|
+
try {
|
|
45
|
+
url = new URL(clientId);
|
|
46
|
+
} catch {
|
|
47
|
+
throw new Error("Client metadata URL is invalid");
|
|
48
|
+
}
|
|
49
|
+
if (
|
|
50
|
+
url.protocol !== "https:" ||
|
|
51
|
+
url.pathname === "/" ||
|
|
52
|
+
!url.pathname ||
|
|
53
|
+
url.hash ||
|
|
54
|
+
url.username ||
|
|
55
|
+
url.password
|
|
56
|
+
) {
|
|
57
|
+
throw new Error("Client metadata URL must be HTTPS with a non-root path");
|
|
58
|
+
}
|
|
59
|
+
const authorityEnd = clientId.indexOf("/", "https://".length);
|
|
60
|
+
const queryStart = clientId.indexOf("?", authorityEnd);
|
|
61
|
+
const rawPath = clientId.slice(
|
|
62
|
+
authorityEnd,
|
|
63
|
+
queryStart === -1 ? undefined : queryStart,
|
|
64
|
+
);
|
|
65
|
+
if (
|
|
66
|
+
rawPath.split("/").some((segment) => {
|
|
67
|
+
try {
|
|
68
|
+
const decoded = decodeURIComponent(segment);
|
|
69
|
+
return decoded === "." || decoded === "..";
|
|
70
|
+
} catch {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
) {
|
|
75
|
+
throw new Error("Client metadata URL must not contain dot path segments");
|
|
76
|
+
}
|
|
77
|
+
return url;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function isAllowedOAuthRedirectUri(value: unknown): value is string {
|
|
81
|
+
if (typeof value !== "string" || value.length > 2048) return false;
|
|
82
|
+
try {
|
|
83
|
+
const url = new URL(value);
|
|
84
|
+
if (url.hash || url.username || url.password) return false;
|
|
85
|
+
if (url.protocol === "https:") return true;
|
|
86
|
+
if (url.protocol === "http:") {
|
|
87
|
+
return (
|
|
88
|
+
url.hostname === "localhost" ||
|
|
89
|
+
url.hostname === "127.0.0.1" ||
|
|
90
|
+
url.hostname === "::1" ||
|
|
91
|
+
url.hostname === "[::1]"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return (
|
|
95
|
+
!DISALLOWED_REDIRECT_SCHEMES.has(url.protocol) &&
|
|
96
|
+
/^[a-z][a-z0-9+.-]*:$/.test(url.protocol)
|
|
97
|
+
);
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function applicationTypeForRedirectUris(
|
|
104
|
+
redirectUris: string[],
|
|
105
|
+
): "native" | "web" {
|
|
106
|
+
return redirectUris.some((value) => {
|
|
107
|
+
const url = new URL(value);
|
|
108
|
+
return (
|
|
109
|
+
url.protocol !== "https:" ||
|
|
110
|
+
url.hostname === "localhost" ||
|
|
111
|
+
url.hostname === "127.0.0.1" ||
|
|
112
|
+
url.hostname === "::1" ||
|
|
113
|
+
url.hostname === "[::1]"
|
|
114
|
+
);
|
|
115
|
+
})
|
|
116
|
+
? "native"
|
|
117
|
+
: "web";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseStringArray(value: unknown): string[] {
|
|
121
|
+
return Array.isArray(value)
|
|
122
|
+
? value.filter((item): item is string => typeof item === "string")
|
|
123
|
+
: [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function validateClientMetadataDocument(
|
|
127
|
+
clientId: string,
|
|
128
|
+
value: unknown,
|
|
129
|
+
): OAuthClientMetadataDocument {
|
|
130
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
131
|
+
throw new Error("Client metadata document must be a JSON object");
|
|
132
|
+
}
|
|
133
|
+
const document = value as Record<string, unknown>;
|
|
134
|
+
if (document.client_id !== clientId) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
"Client metadata document client_id does not match its URL",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const clientName =
|
|
140
|
+
typeof document.client_name === "string" ? document.client_name.trim() : "";
|
|
141
|
+
if (
|
|
142
|
+
!clientName ||
|
|
143
|
+
clientName.length > 120 ||
|
|
144
|
+
/[\u0000-\u001f\u007f]/.test(clientName)
|
|
145
|
+
) {
|
|
146
|
+
throw new Error("Client metadata document client_name is invalid");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const rawRedirectUris = document.redirect_uris;
|
|
150
|
+
const redirectUris = parseStringArray(rawRedirectUris);
|
|
151
|
+
if (
|
|
152
|
+
!Array.isArray(rawRedirectUris) ||
|
|
153
|
+
redirectUris.length === 0 ||
|
|
154
|
+
redirectUris.length > 20 ||
|
|
155
|
+
redirectUris.length !== rawRedirectUris.length ||
|
|
156
|
+
!redirectUris.every(isAllowedOAuthRedirectUri)
|
|
157
|
+
) {
|
|
158
|
+
throw new Error("Client metadata document redirect_uris are invalid");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const tokenEndpointAuthMethod =
|
|
162
|
+
typeof document.token_endpoint_auth_method === "string"
|
|
163
|
+
? document.token_endpoint_auth_method
|
|
164
|
+
: "none";
|
|
165
|
+
if (
|
|
166
|
+
document.token_endpoint_auth_method !== undefined &&
|
|
167
|
+
typeof document.token_endpoint_auth_method !== "string"
|
|
168
|
+
) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
"Client metadata document token_endpoint_auth_method is invalid",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (tokenEndpointAuthMethod !== "none") {
|
|
174
|
+
throw new Error("Only public Client ID Metadata clients are supported");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const grantTypes = parseStringArray(document.grant_types);
|
|
178
|
+
if (
|
|
179
|
+
(document.grant_types !== undefined &&
|
|
180
|
+
(!Array.isArray(document.grant_types) ||
|
|
181
|
+
grantTypes.length !== document.grant_types.length)) ||
|
|
182
|
+
grantTypes.length > 10 ||
|
|
183
|
+
!grantTypes.every(
|
|
184
|
+
(grant) => grant === "authorization_code" || grant === "refresh_token",
|
|
185
|
+
)
|
|
186
|
+
) {
|
|
187
|
+
throw new Error("Client metadata document grant_types are unsupported");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const responseTypes = parseStringArray(document.response_types);
|
|
191
|
+
if (
|
|
192
|
+
(document.response_types !== undefined &&
|
|
193
|
+
(!Array.isArray(document.response_types) ||
|
|
194
|
+
responseTypes.length !== document.response_types.length)) ||
|
|
195
|
+
responseTypes.length > 10 ||
|
|
196
|
+
!responseTypes.every((responseType) => responseType === "code")
|
|
197
|
+
) {
|
|
198
|
+
throw new Error("Client metadata document response_types are unsupported");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const requestedApplicationType =
|
|
202
|
+
typeof document.application_type === "string"
|
|
203
|
+
? document.application_type
|
|
204
|
+
: undefined;
|
|
205
|
+
if (
|
|
206
|
+
(document.application_type !== undefined &&
|
|
207
|
+
typeof document.application_type !== "string") ||
|
|
208
|
+
(requestedApplicationType &&
|
|
209
|
+
requestedApplicationType !== "native" &&
|
|
210
|
+
requestedApplicationType !== "web")
|
|
211
|
+
) {
|
|
212
|
+
throw new Error("Client metadata document application_type is unsupported");
|
|
213
|
+
}
|
|
214
|
+
const applicationType: "native" | "web" =
|
|
215
|
+
requestedApplicationType === "native" || requestedApplicationType === "web"
|
|
216
|
+
? requestedApplicationType
|
|
217
|
+
: applicationTypeForRedirectUris(redirectUris);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
clientId,
|
|
221
|
+
clientName,
|
|
222
|
+
redirectUris: [...new Set(redirectUris)],
|
|
223
|
+
grantTypes: grantTypes.length
|
|
224
|
+
? [...new Set(grantTypes)]
|
|
225
|
+
: ["authorization_code", "refresh_token"],
|
|
226
|
+
responseTypes: responseTypes.length
|
|
227
|
+
? [...new Set(responseTypes)]
|
|
228
|
+
: ["code"],
|
|
229
|
+
tokenEndpointAuthMethod: "none",
|
|
230
|
+
applicationType,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function readBoundedResponseBody(response: Response): Promise<string> {
|
|
235
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
236
|
+
if (
|
|
237
|
+
Number.isFinite(contentLength) &&
|
|
238
|
+
contentLength > CLIENT_METADATA_MAX_BYTES
|
|
239
|
+
) {
|
|
240
|
+
await response.body?.cancel().catch(() => undefined);
|
|
241
|
+
throw new Error("Client metadata document is too large");
|
|
242
|
+
}
|
|
243
|
+
if (!response.body) return "";
|
|
244
|
+
|
|
245
|
+
const reader = response.body.getReader();
|
|
246
|
+
const decoder = new TextDecoder();
|
|
247
|
+
let bytesRead = 0;
|
|
248
|
+
let text = "";
|
|
249
|
+
try {
|
|
250
|
+
for (;;) {
|
|
251
|
+
const { done, value } = await reader.read();
|
|
252
|
+
if (done) break;
|
|
253
|
+
bytesRead += value.byteLength;
|
|
254
|
+
if (bytesRead > CLIENT_METADATA_MAX_BYTES) {
|
|
255
|
+
await reader.cancel().catch(() => undefined);
|
|
256
|
+
throw new Error("Client metadata document is too large");
|
|
257
|
+
}
|
|
258
|
+
text += decoder.decode(value, { stream: true });
|
|
259
|
+
}
|
|
260
|
+
text += decoder.decode();
|
|
261
|
+
return text;
|
|
262
|
+
} finally {
|
|
263
|
+
reader.releaseLock();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function cacheTtlMs(headers: Headers, now: number): number {
|
|
268
|
+
const cacheControl = headers.get("cache-control")?.toLowerCase() ?? "";
|
|
269
|
+
if (/(?:^|,)\s*(?:no-store|no-cache|private)(?:\s|,|$)/.test(cacheControl)) {
|
|
270
|
+
return 0;
|
|
271
|
+
}
|
|
272
|
+
const maxAge = cacheControl.match(/(?:^|,)\s*max-age=(\d+)/)?.[1];
|
|
273
|
+
if (maxAge) {
|
|
274
|
+
return Math.min(Number(maxAge) * 1_000, CLIENT_METADATA_MAX_CACHE_TTL_MS);
|
|
275
|
+
}
|
|
276
|
+
const expiresAt = Date.parse(headers.get("expires") ?? "");
|
|
277
|
+
if (Number.isFinite(expiresAt) && expiresAt > now) {
|
|
278
|
+
return Math.min(expiresAt - now, CLIENT_METADATA_MAX_CACHE_TTL_MS);
|
|
279
|
+
}
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function cacheClientMetadata(
|
|
284
|
+
clientId: string,
|
|
285
|
+
metadata: OAuthClientMetadataDocument,
|
|
286
|
+
response: Response,
|
|
287
|
+
now: number,
|
|
288
|
+
): void {
|
|
289
|
+
const ttlMs = cacheTtlMs(response.headers, now);
|
|
290
|
+
if (ttlMs <= 0) return;
|
|
291
|
+
clientMetadataCache.delete(clientId);
|
|
292
|
+
clientMetadataCache.set(clientId, {
|
|
293
|
+
expiresAt: now + ttlMs,
|
|
294
|
+
metadata,
|
|
295
|
+
});
|
|
296
|
+
while (clientMetadataCache.size > CLIENT_METADATA_MAX_CACHE_ENTRIES) {
|
|
297
|
+
const oldest = clientMetadataCache.keys().next().value;
|
|
298
|
+
if (typeof oldest !== "string") break;
|
|
299
|
+
clientMetadataCache.delete(oldest);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function fetchOAuthClientMetadataDocument(
|
|
304
|
+
clientId: string,
|
|
305
|
+
signal: AbortSignal,
|
|
306
|
+
): Promise<{ metadata: OAuthClientMetadataDocument; response: Response }> {
|
|
307
|
+
const response = await ssrfSafeFetch(
|
|
308
|
+
clientId,
|
|
309
|
+
{
|
|
310
|
+
headers: {
|
|
311
|
+
Accept: "application/json, application/client-metadata+json",
|
|
312
|
+
},
|
|
313
|
+
signal,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
maxRedirects: CLIENT_METADATA_MAX_REDIRECTS,
|
|
317
|
+
httpsOnly: true,
|
|
318
|
+
},
|
|
319
|
+
);
|
|
320
|
+
if (!response.ok) {
|
|
321
|
+
await response.body?.cancel().catch(() => undefined);
|
|
322
|
+
throw new Error(
|
|
323
|
+
`Client metadata document request failed with ${response.status}`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
const contentType =
|
|
327
|
+
response.headers.get("content-type")?.split(";", 1)[0]?.trim() ?? "";
|
|
328
|
+
if (
|
|
329
|
+
contentType !== "application/json" &&
|
|
330
|
+
contentType !== "application/client-metadata+json"
|
|
331
|
+
) {
|
|
332
|
+
await response.body?.cancel().catch(() => undefined);
|
|
333
|
+
throw new Error("Client metadata document content type is invalid");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const body = await readBoundedResponseBody(response);
|
|
337
|
+
let parsed: unknown;
|
|
338
|
+
try {
|
|
339
|
+
parsed = JSON.parse(body);
|
|
340
|
+
} catch {
|
|
341
|
+
throw new Error("Client metadata document is not valid JSON");
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
metadata: validateClientMetadataDocument(clientId, parsed),
|
|
345
|
+
response,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export async function resolveOAuthClientMetadataDocument(
|
|
350
|
+
clientId: string,
|
|
351
|
+
): Promise<OAuthClientMetadataDocument> {
|
|
352
|
+
validateOAuthClientMetadataUrl(clientId);
|
|
353
|
+
const now = Date.now();
|
|
354
|
+
const cached = clientMetadataCache.get(clientId);
|
|
355
|
+
if (cached && cached.expiresAt > now) {
|
|
356
|
+
clientMetadataCache.delete(clientId);
|
|
357
|
+
clientMetadataCache.set(clientId, cached);
|
|
358
|
+
return cached.metadata;
|
|
359
|
+
}
|
|
360
|
+
if (cached) clientMetadataCache.delete(clientId);
|
|
361
|
+
|
|
362
|
+
const abort = new AbortController();
|
|
363
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
364
|
+
try {
|
|
365
|
+
const timedOut = new Promise<never>((_, reject) => {
|
|
366
|
+
timeout = setTimeout(() => {
|
|
367
|
+
abort.abort();
|
|
368
|
+
reject(new Error("Client metadata document request timed out"));
|
|
369
|
+
}, CLIENT_METADATA_TIMEOUT_MS);
|
|
370
|
+
});
|
|
371
|
+
const { metadata, response } = await Promise.race([
|
|
372
|
+
fetchOAuthClientMetadataDocument(clientId, abort.signal),
|
|
373
|
+
timedOut,
|
|
374
|
+
]);
|
|
375
|
+
cacheClientMetadata(clientId, metadata, response, now);
|
|
376
|
+
return metadata;
|
|
377
|
+
} finally {
|
|
378
|
+
if (timeout) clearTimeout(timeout);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
@@ -17,6 +17,12 @@ import { getOrgDomain } from "../org/context.js";
|
|
|
17
17
|
import { getConfiguredLoginHtml, getSession } from "../server/auth.js";
|
|
18
18
|
import { getAuthSecret } from "../server/better-auth-instance.js";
|
|
19
19
|
import { readBody } from "../server/h3-helpers.js";
|
|
20
|
+
import {
|
|
21
|
+
applicationTypeForRedirectUris,
|
|
22
|
+
isAllowedOAuthRedirectUri,
|
|
23
|
+
isUrlBasedOAuthClientId,
|
|
24
|
+
resolveOAuthClientMetadataDocument,
|
|
25
|
+
} from "./oauth-client-metadata.js";
|
|
20
26
|
import {
|
|
21
27
|
createOAuthCode,
|
|
22
28
|
createOAuthRefreshToken,
|
|
@@ -30,7 +36,6 @@ import {
|
|
|
30
36
|
} from "./oauth-store.js";
|
|
31
37
|
import {
|
|
32
38
|
MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS,
|
|
33
|
-
MCP_OAUTH_DEFAULT_SCOPE,
|
|
34
39
|
MCP_OAUTH_SCOPES,
|
|
35
40
|
normalizeOAuthScope,
|
|
36
41
|
signMcpOAuthAccessToken,
|
|
@@ -42,6 +47,11 @@ export interface McpOAuthRouteOptions {
|
|
|
42
47
|
appName?: string;
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
const MCP_OAUTH_RESOURCE_SCOPES = MCP_OAUTH_SCOPES.filter(
|
|
51
|
+
(scope) => scope !== "offline_access",
|
|
52
|
+
);
|
|
53
|
+
const MCP_OAUTH_RESOURCE_SCOPE = MCP_OAUTH_RESOURCE_SCOPES.join(" ");
|
|
54
|
+
|
|
45
55
|
function json(body: unknown, status = 200): Response {
|
|
46
56
|
return new Response(JSON.stringify(body), {
|
|
47
57
|
status,
|
|
@@ -229,7 +239,7 @@ export function getMcpOAuthProtectedResourceMetadataUrl(
|
|
|
229
239
|
|
|
230
240
|
export function buildMcpOAuthChallenge(event: H3Event): string {
|
|
231
241
|
const metadata = getMcpOAuthProtectedResourceMetadataUrl(event);
|
|
232
|
-
const scope =
|
|
242
|
+
const scope = MCP_OAUTH_RESOURCE_SCOPE;
|
|
233
243
|
return metadata
|
|
234
244
|
? `Bearer resource_metadata="${metadata}", scope="${scope}"`
|
|
235
245
|
: `Bearer scope="${scope}"`;
|
|
@@ -268,7 +278,7 @@ export function handleMcpOAuthProtectedResourceMetadata(
|
|
|
268
278
|
return json({
|
|
269
279
|
resource,
|
|
270
280
|
authorization_servers: [issuer],
|
|
271
|
-
scopes_supported:
|
|
281
|
+
scopes_supported: MCP_OAUTH_RESOURCE_SCOPES,
|
|
272
282
|
resource_documentation: issuer,
|
|
273
283
|
});
|
|
274
284
|
}
|
|
@@ -296,54 +306,11 @@ export function handleMcpOAuthAuthorizationServerMetadata(
|
|
|
296
306
|
code_challenge_methods_supported: ["S256"],
|
|
297
307
|
token_endpoint_auth_methods_supported: ["none"],
|
|
298
308
|
scopes_supported: MCP_OAUTH_SCOPES,
|
|
309
|
+
authorization_response_iss_parameter_supported: true,
|
|
310
|
+
client_id_metadata_document_supported: true,
|
|
299
311
|
});
|
|
300
312
|
}
|
|
301
313
|
|
|
302
|
-
// Schemes that must never be accepted as a redirect target: they can execute
|
|
303
|
-
// script or read local resources if a redirect is ever rendered in a browser
|
|
304
|
-
// or webview context.
|
|
305
|
-
const DISALLOWED_REDIRECT_SCHEMES = new Set([
|
|
306
|
-
"javascript:",
|
|
307
|
-
"data:",
|
|
308
|
-
"vbscript:",
|
|
309
|
-
"file:",
|
|
310
|
-
"blob:",
|
|
311
|
-
"about:",
|
|
312
|
-
]);
|
|
313
|
-
|
|
314
|
-
// Native/desktop IDE clients (Cursor, VS Code, …) register a private-use URI
|
|
315
|
-
// scheme callback such as `cursor://` or `vscode://` (RFC 8252 §7.1) instead of
|
|
316
|
-
// an https/loopback URL. The authorization code is bound by PKCE (S256), so
|
|
317
|
-
// delivering it through a client-registered app scheme is safe.
|
|
318
|
-
function isPrivateUseRedirectScheme(protocol: string): boolean {
|
|
319
|
-
if (DISALLOWED_REDIRECT_SCHEMES.has(protocol)) return false;
|
|
320
|
-
// RFC 3986 scheme grammar: ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ), with a
|
|
321
|
-
// trailing ":" from URL.protocol. Require a non-http(s) custom scheme here;
|
|
322
|
-
// http/https are handled explicitly above.
|
|
323
|
-
return /^[a-z][a-z0-9+.-]*:$/.test(protocol);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function isAllowedRedirectUri(value: unknown): value is string {
|
|
327
|
-
if (typeof value !== "string" || value.length > 2048) return false;
|
|
328
|
-
try {
|
|
329
|
-
const url = new URL(value);
|
|
330
|
-
if (url.hash) return false;
|
|
331
|
-
if (url.username || url.password) return false;
|
|
332
|
-
if (url.protocol === "https:") return true;
|
|
333
|
-
if (url.protocol === "http:") {
|
|
334
|
-
return (
|
|
335
|
-
url.hostname === "localhost" ||
|
|
336
|
-
url.hostname === "127.0.0.1" ||
|
|
337
|
-
url.hostname === "::1" ||
|
|
338
|
-
url.hostname === "[::1]"
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
return isPrivateUseRedirectScheme(url.protocol);
|
|
342
|
-
} catch {
|
|
343
|
-
return false;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
314
|
function parseStringArray(value: unknown): string[] {
|
|
348
315
|
return Array.isArray(value)
|
|
349
316
|
? value.filter((item): item is string => typeof item === "string")
|
|
@@ -362,7 +329,7 @@ async function handleRegister(event: H3Event): Promise<Response> {
|
|
|
362
329
|
if (
|
|
363
330
|
redirectUris.length === 0 ||
|
|
364
331
|
redirectUris.length > 20 ||
|
|
365
|
-
!redirectUris.every(
|
|
332
|
+
!redirectUris.every(isAllowedOAuthRedirectUri)
|
|
366
333
|
) {
|
|
367
334
|
return oauthError(
|
|
368
335
|
"invalid_client_metadata",
|
|
@@ -393,6 +360,22 @@ async function handleRegister(event: H3Event): Promise<Response> {
|
|
|
393
360
|
"Only public OAuth clients are supported",
|
|
394
361
|
);
|
|
395
362
|
}
|
|
363
|
+
const requestedApplicationType =
|
|
364
|
+
typeof body.application_type === "string"
|
|
365
|
+
? body.application_type
|
|
366
|
+
: undefined;
|
|
367
|
+
if (
|
|
368
|
+
requestedApplicationType &&
|
|
369
|
+
requestedApplicationType !== "native" &&
|
|
370
|
+
requestedApplicationType !== "web"
|
|
371
|
+
) {
|
|
372
|
+
return oauthError(
|
|
373
|
+
"invalid_client_metadata",
|
|
374
|
+
"application_type must be native or web",
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
const applicationType =
|
|
378
|
+
requestedApplicationType ?? applicationTypeForRedirectUris(redirectUris);
|
|
396
379
|
|
|
397
380
|
const clientName =
|
|
398
381
|
typeof body.client_name === "string"
|
|
@@ -422,6 +405,7 @@ async function handleRegister(event: H3Event): Promise<Response> {
|
|
|
422
405
|
grant_types: client.grantTypes,
|
|
423
406
|
response_types: client.responseTypes,
|
|
424
407
|
token_endpoint_auth_method: client.tokenEndpointAuthMethod,
|
|
408
|
+
application_type: applicationType,
|
|
425
409
|
},
|
|
426
410
|
201,
|
|
427
411
|
);
|
|
@@ -429,12 +413,14 @@ async function handleRegister(event: H3Event): Promise<Response> {
|
|
|
429
413
|
|
|
430
414
|
function redirectWithOAuthError(params: {
|
|
431
415
|
redirectUri: string;
|
|
416
|
+
issuer: string;
|
|
432
417
|
state?: string;
|
|
433
418
|
error: string;
|
|
434
419
|
description?: string;
|
|
435
420
|
}): Response {
|
|
436
421
|
const url = new URL(params.redirectUri);
|
|
437
422
|
url.searchParams.set("error", params.error);
|
|
423
|
+
url.searchParams.set("iss", params.issuer);
|
|
438
424
|
if (params.description) {
|
|
439
425
|
url.searchParams.set("error_description", params.description);
|
|
440
426
|
}
|
|
@@ -444,17 +430,20 @@ function redirectWithOAuthError(params: {
|
|
|
444
430
|
|
|
445
431
|
function buildCodeRedirectUrl(params: {
|
|
446
432
|
redirectUri: string;
|
|
433
|
+
issuer: string;
|
|
447
434
|
code: string;
|
|
448
435
|
state?: string;
|
|
449
436
|
}): string {
|
|
450
437
|
const url = new URL(params.redirectUri);
|
|
451
438
|
url.searchParams.set("code", params.code);
|
|
439
|
+
url.searchParams.set("iss", params.issuer);
|
|
452
440
|
if (params.state) url.searchParams.set("state", params.state);
|
|
453
441
|
return url.toString();
|
|
454
442
|
}
|
|
455
443
|
|
|
456
444
|
function redirectWithCode(params: {
|
|
457
445
|
redirectUri: string;
|
|
446
|
+
issuer: string;
|
|
458
447
|
code: string;
|
|
459
448
|
state?: string;
|
|
460
449
|
}): Response {
|
|
@@ -574,6 +563,7 @@ function renderConsentPage(params: {
|
|
|
574
563
|
appName: string;
|
|
575
564
|
email: string;
|
|
576
565
|
clientName: string;
|
|
566
|
+
redirectUri: string;
|
|
577
567
|
scopes: string[];
|
|
578
568
|
fields: Record<string, string>;
|
|
579
569
|
}): string {
|
|
@@ -602,6 +592,7 @@ function renderConsentPage(params: {
|
|
|
602
592
|
<h1>Authorize ${escapeHtml(params.clientName)}</h1>
|
|
603
593
|
<p>${escapeHtml(params.appName)} will let this MCP client act as ${escapeHtml(params.email)} for these scopes:</p>
|
|
604
594
|
<ul>${scopes}</ul>
|
|
595
|
+
<p>After authorization, your browser will return to <code>${escapeHtml(params.redirectUri)}</code>.</p>
|
|
605
596
|
<form method="post">
|
|
606
597
|
${hidden}
|
|
607
598
|
<div class="actions">
|
|
@@ -721,6 +712,7 @@ async function handleAuthorize(
|
|
|
721
712
|
);
|
|
722
713
|
}
|
|
723
714
|
const params = await readOAuthParams(event);
|
|
715
|
+
const issuer = getMcpOAuthIssuer(event);
|
|
724
716
|
const state = params.state;
|
|
725
717
|
const clientId = params.client_id;
|
|
726
718
|
const redirectUri = params.redirect_uri;
|
|
@@ -739,6 +731,7 @@ async function handleAuthorize(
|
|
|
739
731
|
if (
|
|
740
732
|
!clientId ||
|
|
741
733
|
!redirectUri ||
|
|
734
|
+
!issuer ||
|
|
742
735
|
!resource ||
|
|
743
736
|
!expectedResources.includes(resource)
|
|
744
737
|
) {
|
|
@@ -748,7 +741,9 @@ async function handleAuthorize(
|
|
|
748
741
|
return oauthError("invalid_request", "PKCE S256 is required");
|
|
749
742
|
}
|
|
750
743
|
|
|
751
|
-
const client =
|
|
744
|
+
const client = isUrlBasedOAuthClientId(clientId)
|
|
745
|
+
? await resolveOAuthClientMetadataDocument(clientId).catch(() => null)
|
|
746
|
+
: await getOAuthClient(clientId);
|
|
752
747
|
if (!client || !client.redirectUris.includes(redirectUri)) {
|
|
753
748
|
return oauthError("invalid_client", "Unknown client or redirect_uri");
|
|
754
749
|
}
|
|
@@ -758,6 +753,7 @@ async function handleAuthorize(
|
|
|
758
753
|
if (params.prompt === "none") {
|
|
759
754
|
return redirectWithOAuthError({
|
|
760
755
|
redirectUri,
|
|
756
|
+
issuer,
|
|
761
757
|
state,
|
|
762
758
|
error: "login_required",
|
|
763
759
|
});
|
|
@@ -772,6 +768,7 @@ async function handleAuthorize(
|
|
|
772
768
|
if (!scope) {
|
|
773
769
|
return redirectWithOAuthError({
|
|
774
770
|
redirectUri,
|
|
771
|
+
issuer,
|
|
775
772
|
state,
|
|
776
773
|
error: "invalid_scope",
|
|
777
774
|
});
|
|
@@ -782,6 +779,7 @@ async function handleAuthorize(
|
|
|
782
779
|
appName: options.appName || options.appId || "Agent Native",
|
|
783
780
|
email: session.email,
|
|
784
781
|
clientName: client.clientName || client.clientId,
|
|
782
|
+
redirectUri,
|
|
785
783
|
scopes: scope.split(/\s+/),
|
|
786
784
|
fields: {
|
|
787
785
|
response_type: "code",
|
|
@@ -821,6 +819,7 @@ async function handleAuthorize(
|
|
|
821
819
|
if (params.decision !== "approve") {
|
|
822
820
|
return redirectWithOAuthError({
|
|
823
821
|
redirectUri,
|
|
822
|
+
issuer,
|
|
824
823
|
state,
|
|
825
824
|
error: "access_denied",
|
|
826
825
|
});
|
|
@@ -850,7 +849,7 @@ async function handleAuthorize(
|
|
|
850
849
|
isDeepLinkRedirect =
|
|
851
850
|
protocol !== "http:" &&
|
|
852
851
|
protocol !== "https:" &&
|
|
853
|
-
|
|
852
|
+
/^[a-z][a-z0-9+.-]*:$/.test(protocol);
|
|
854
853
|
} catch {
|
|
855
854
|
isDeepLinkRedirect = false;
|
|
856
855
|
}
|
|
@@ -861,13 +860,19 @@ async function handleAuthorize(
|
|
|
861
860
|
clientName: client.clientName ?? null,
|
|
862
861
|
redirectUrl: buildCodeRedirectUrl({
|
|
863
862
|
redirectUri,
|
|
863
|
+
issuer,
|
|
864
864
|
state,
|
|
865
865
|
code: code.code,
|
|
866
866
|
}),
|
|
867
867
|
}),
|
|
868
868
|
);
|
|
869
869
|
}
|
|
870
|
-
return redirectWithCode({
|
|
870
|
+
return redirectWithCode({
|
|
871
|
+
redirectUri,
|
|
872
|
+
issuer,
|
|
873
|
+
state,
|
|
874
|
+
code: code.code,
|
|
875
|
+
});
|
|
871
876
|
}
|
|
872
877
|
|
|
873
878
|
async function issueTokenSet(params: {
|