@agent-native/core 0.128.4 → 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 +84 -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/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/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/security/SKILL.md +13 -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/chat/README.md +0 -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/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/collab/struct-routes.d.ts +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/file-upload/actions/upload-image.d.ts +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/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- 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/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +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/chat/README.md +0 -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/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/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/chat/README.md +0 -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
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { ssrfSafeFetch } from "../extensions/url-safety.js";
|
|
2
|
+
const CLIENT_METADATA_MAX_BYTES = 64 * 1024;
|
|
3
|
+
const CLIENT_METADATA_TIMEOUT_MS = 5_000;
|
|
4
|
+
const CLIENT_METADATA_MAX_REDIRECTS = 2;
|
|
5
|
+
const CLIENT_METADATA_MAX_CACHE_ENTRIES = 128;
|
|
6
|
+
const CLIENT_METADATA_MAX_CACHE_TTL_MS = 5 * 60_000;
|
|
7
|
+
const DISALLOWED_REDIRECT_SCHEMES = new Set([
|
|
8
|
+
"javascript:",
|
|
9
|
+
"data:",
|
|
10
|
+
"vbscript:",
|
|
11
|
+
"file:",
|
|
12
|
+
"blob:",
|
|
13
|
+
"about:",
|
|
14
|
+
]);
|
|
15
|
+
const clientMetadataCache = new Map();
|
|
16
|
+
export function isUrlBasedOAuthClientId(clientId) {
|
|
17
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(clientId);
|
|
18
|
+
}
|
|
19
|
+
export function validateOAuthClientMetadataUrl(clientId) {
|
|
20
|
+
if (!clientId || clientId !== clientId.trim() || clientId.length > 2048) {
|
|
21
|
+
throw new Error("Client metadata URL is invalid");
|
|
22
|
+
}
|
|
23
|
+
let url;
|
|
24
|
+
try {
|
|
25
|
+
url = new URL(clientId);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error("Client metadata URL is invalid");
|
|
29
|
+
}
|
|
30
|
+
if (url.protocol !== "https:" ||
|
|
31
|
+
url.pathname === "/" ||
|
|
32
|
+
!url.pathname ||
|
|
33
|
+
url.hash ||
|
|
34
|
+
url.username ||
|
|
35
|
+
url.password) {
|
|
36
|
+
throw new Error("Client metadata URL must be HTTPS with a non-root path");
|
|
37
|
+
}
|
|
38
|
+
const authorityEnd = clientId.indexOf("/", "https://".length);
|
|
39
|
+
const queryStart = clientId.indexOf("?", authorityEnd);
|
|
40
|
+
const rawPath = clientId.slice(authorityEnd, queryStart === -1 ? undefined : queryStart);
|
|
41
|
+
if (rawPath.split("/").some((segment) => {
|
|
42
|
+
try {
|
|
43
|
+
const decoded = decodeURIComponent(segment);
|
|
44
|
+
return decoded === "." || decoded === "..";
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
})) {
|
|
50
|
+
throw new Error("Client metadata URL must not contain dot path segments");
|
|
51
|
+
}
|
|
52
|
+
return url;
|
|
53
|
+
}
|
|
54
|
+
export function isAllowedOAuthRedirectUri(value) {
|
|
55
|
+
if (typeof value !== "string" || value.length > 2048)
|
|
56
|
+
return false;
|
|
57
|
+
try {
|
|
58
|
+
const url = new URL(value);
|
|
59
|
+
if (url.hash || url.username || url.password)
|
|
60
|
+
return false;
|
|
61
|
+
if (url.protocol === "https:")
|
|
62
|
+
return true;
|
|
63
|
+
if (url.protocol === "http:") {
|
|
64
|
+
return (url.hostname === "localhost" ||
|
|
65
|
+
url.hostname === "127.0.0.1" ||
|
|
66
|
+
url.hostname === "::1" ||
|
|
67
|
+
url.hostname === "[::1]");
|
|
68
|
+
}
|
|
69
|
+
return (!DISALLOWED_REDIRECT_SCHEMES.has(url.protocol) &&
|
|
70
|
+
/^[a-z][a-z0-9+.-]*:$/.test(url.protocol));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function applicationTypeForRedirectUris(redirectUris) {
|
|
77
|
+
return redirectUris.some((value) => {
|
|
78
|
+
const url = new URL(value);
|
|
79
|
+
return (url.protocol !== "https:" ||
|
|
80
|
+
url.hostname === "localhost" ||
|
|
81
|
+
url.hostname === "127.0.0.1" ||
|
|
82
|
+
url.hostname === "::1" ||
|
|
83
|
+
url.hostname === "[::1]");
|
|
84
|
+
})
|
|
85
|
+
? "native"
|
|
86
|
+
: "web";
|
|
87
|
+
}
|
|
88
|
+
function parseStringArray(value) {
|
|
89
|
+
return Array.isArray(value)
|
|
90
|
+
? value.filter((item) => typeof item === "string")
|
|
91
|
+
: [];
|
|
92
|
+
}
|
|
93
|
+
function validateClientMetadataDocument(clientId, value) {
|
|
94
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
95
|
+
throw new Error("Client metadata document must be a JSON object");
|
|
96
|
+
}
|
|
97
|
+
const document = value;
|
|
98
|
+
if (document.client_id !== clientId) {
|
|
99
|
+
throw new Error("Client metadata document client_id does not match its URL");
|
|
100
|
+
}
|
|
101
|
+
const clientName = typeof document.client_name === "string" ? document.client_name.trim() : "";
|
|
102
|
+
if (!clientName ||
|
|
103
|
+
clientName.length > 120 ||
|
|
104
|
+
/[\u0000-\u001f\u007f]/.test(clientName)) {
|
|
105
|
+
throw new Error("Client metadata document client_name is invalid");
|
|
106
|
+
}
|
|
107
|
+
const rawRedirectUris = document.redirect_uris;
|
|
108
|
+
const redirectUris = parseStringArray(rawRedirectUris);
|
|
109
|
+
if (!Array.isArray(rawRedirectUris) ||
|
|
110
|
+
redirectUris.length === 0 ||
|
|
111
|
+
redirectUris.length > 20 ||
|
|
112
|
+
redirectUris.length !== rawRedirectUris.length ||
|
|
113
|
+
!redirectUris.every(isAllowedOAuthRedirectUri)) {
|
|
114
|
+
throw new Error("Client metadata document redirect_uris are invalid");
|
|
115
|
+
}
|
|
116
|
+
const tokenEndpointAuthMethod = typeof document.token_endpoint_auth_method === "string"
|
|
117
|
+
? document.token_endpoint_auth_method
|
|
118
|
+
: "none";
|
|
119
|
+
if (document.token_endpoint_auth_method !== undefined &&
|
|
120
|
+
typeof document.token_endpoint_auth_method !== "string") {
|
|
121
|
+
throw new Error("Client metadata document token_endpoint_auth_method is invalid");
|
|
122
|
+
}
|
|
123
|
+
if (tokenEndpointAuthMethod !== "none") {
|
|
124
|
+
throw new Error("Only public Client ID Metadata clients are supported");
|
|
125
|
+
}
|
|
126
|
+
const grantTypes = parseStringArray(document.grant_types);
|
|
127
|
+
if ((document.grant_types !== undefined &&
|
|
128
|
+
(!Array.isArray(document.grant_types) ||
|
|
129
|
+
grantTypes.length !== document.grant_types.length)) ||
|
|
130
|
+
grantTypes.length > 10 ||
|
|
131
|
+
!grantTypes.every((grant) => grant === "authorization_code" || grant === "refresh_token")) {
|
|
132
|
+
throw new Error("Client metadata document grant_types are unsupported");
|
|
133
|
+
}
|
|
134
|
+
const responseTypes = parseStringArray(document.response_types);
|
|
135
|
+
if ((document.response_types !== undefined &&
|
|
136
|
+
(!Array.isArray(document.response_types) ||
|
|
137
|
+
responseTypes.length !== document.response_types.length)) ||
|
|
138
|
+
responseTypes.length > 10 ||
|
|
139
|
+
!responseTypes.every((responseType) => responseType === "code")) {
|
|
140
|
+
throw new Error("Client metadata document response_types are unsupported");
|
|
141
|
+
}
|
|
142
|
+
const requestedApplicationType = typeof document.application_type === "string"
|
|
143
|
+
? document.application_type
|
|
144
|
+
: undefined;
|
|
145
|
+
if ((document.application_type !== undefined &&
|
|
146
|
+
typeof document.application_type !== "string") ||
|
|
147
|
+
(requestedApplicationType &&
|
|
148
|
+
requestedApplicationType !== "native" &&
|
|
149
|
+
requestedApplicationType !== "web")) {
|
|
150
|
+
throw new Error("Client metadata document application_type is unsupported");
|
|
151
|
+
}
|
|
152
|
+
const applicationType = requestedApplicationType === "native" || requestedApplicationType === "web"
|
|
153
|
+
? requestedApplicationType
|
|
154
|
+
: applicationTypeForRedirectUris(redirectUris);
|
|
155
|
+
return {
|
|
156
|
+
clientId,
|
|
157
|
+
clientName,
|
|
158
|
+
redirectUris: [...new Set(redirectUris)],
|
|
159
|
+
grantTypes: grantTypes.length
|
|
160
|
+
? [...new Set(grantTypes)]
|
|
161
|
+
: ["authorization_code", "refresh_token"],
|
|
162
|
+
responseTypes: responseTypes.length
|
|
163
|
+
? [...new Set(responseTypes)]
|
|
164
|
+
: ["code"],
|
|
165
|
+
tokenEndpointAuthMethod: "none",
|
|
166
|
+
applicationType,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async function readBoundedResponseBody(response) {
|
|
170
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
171
|
+
if (Number.isFinite(contentLength) &&
|
|
172
|
+
contentLength > CLIENT_METADATA_MAX_BYTES) {
|
|
173
|
+
await response.body?.cancel().catch(() => undefined);
|
|
174
|
+
throw new Error("Client metadata document is too large");
|
|
175
|
+
}
|
|
176
|
+
if (!response.body)
|
|
177
|
+
return "";
|
|
178
|
+
const reader = response.body.getReader();
|
|
179
|
+
const decoder = new TextDecoder();
|
|
180
|
+
let bytesRead = 0;
|
|
181
|
+
let text = "";
|
|
182
|
+
try {
|
|
183
|
+
for (;;) {
|
|
184
|
+
const { done, value } = await reader.read();
|
|
185
|
+
if (done)
|
|
186
|
+
break;
|
|
187
|
+
bytesRead += value.byteLength;
|
|
188
|
+
if (bytesRead > CLIENT_METADATA_MAX_BYTES) {
|
|
189
|
+
await reader.cancel().catch(() => undefined);
|
|
190
|
+
throw new Error("Client metadata document is too large");
|
|
191
|
+
}
|
|
192
|
+
text += decoder.decode(value, { stream: true });
|
|
193
|
+
}
|
|
194
|
+
text += decoder.decode();
|
|
195
|
+
return text;
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
reader.releaseLock();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function cacheTtlMs(headers, now) {
|
|
202
|
+
const cacheControl = headers.get("cache-control")?.toLowerCase() ?? "";
|
|
203
|
+
if (/(?:^|,)\s*(?:no-store|no-cache|private)(?:\s|,|$)/.test(cacheControl)) {
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
const maxAge = cacheControl.match(/(?:^|,)\s*max-age=(\d+)/)?.[1];
|
|
207
|
+
if (maxAge) {
|
|
208
|
+
return Math.min(Number(maxAge) * 1_000, CLIENT_METADATA_MAX_CACHE_TTL_MS);
|
|
209
|
+
}
|
|
210
|
+
const expiresAt = Date.parse(headers.get("expires") ?? "");
|
|
211
|
+
if (Number.isFinite(expiresAt) && expiresAt > now) {
|
|
212
|
+
return Math.min(expiresAt - now, CLIENT_METADATA_MAX_CACHE_TTL_MS);
|
|
213
|
+
}
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
function cacheClientMetadata(clientId, metadata, response, now) {
|
|
217
|
+
const ttlMs = cacheTtlMs(response.headers, now);
|
|
218
|
+
if (ttlMs <= 0)
|
|
219
|
+
return;
|
|
220
|
+
clientMetadataCache.delete(clientId);
|
|
221
|
+
clientMetadataCache.set(clientId, {
|
|
222
|
+
expiresAt: now + ttlMs,
|
|
223
|
+
metadata,
|
|
224
|
+
});
|
|
225
|
+
while (clientMetadataCache.size > CLIENT_METADATA_MAX_CACHE_ENTRIES) {
|
|
226
|
+
const oldest = clientMetadataCache.keys().next().value;
|
|
227
|
+
if (typeof oldest !== "string")
|
|
228
|
+
break;
|
|
229
|
+
clientMetadataCache.delete(oldest);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async function fetchOAuthClientMetadataDocument(clientId, signal) {
|
|
233
|
+
const response = await ssrfSafeFetch(clientId, {
|
|
234
|
+
headers: {
|
|
235
|
+
Accept: "application/json, application/client-metadata+json",
|
|
236
|
+
},
|
|
237
|
+
signal,
|
|
238
|
+
}, {
|
|
239
|
+
maxRedirects: CLIENT_METADATA_MAX_REDIRECTS,
|
|
240
|
+
httpsOnly: true,
|
|
241
|
+
});
|
|
242
|
+
if (!response.ok) {
|
|
243
|
+
await response.body?.cancel().catch(() => undefined);
|
|
244
|
+
throw new Error(`Client metadata document request failed with ${response.status}`);
|
|
245
|
+
}
|
|
246
|
+
const contentType = response.headers.get("content-type")?.split(";", 1)[0]?.trim() ?? "";
|
|
247
|
+
if (contentType !== "application/json" &&
|
|
248
|
+
contentType !== "application/client-metadata+json") {
|
|
249
|
+
await response.body?.cancel().catch(() => undefined);
|
|
250
|
+
throw new Error("Client metadata document content type is invalid");
|
|
251
|
+
}
|
|
252
|
+
const body = await readBoundedResponseBody(response);
|
|
253
|
+
let parsed;
|
|
254
|
+
try {
|
|
255
|
+
parsed = JSON.parse(body);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
throw new Error("Client metadata document is not valid JSON");
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
metadata: validateClientMetadataDocument(clientId, parsed),
|
|
262
|
+
response,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
export async function resolveOAuthClientMetadataDocument(clientId) {
|
|
266
|
+
validateOAuthClientMetadataUrl(clientId);
|
|
267
|
+
const now = Date.now();
|
|
268
|
+
const cached = clientMetadataCache.get(clientId);
|
|
269
|
+
if (cached && cached.expiresAt > now) {
|
|
270
|
+
clientMetadataCache.delete(clientId);
|
|
271
|
+
clientMetadataCache.set(clientId, cached);
|
|
272
|
+
return cached.metadata;
|
|
273
|
+
}
|
|
274
|
+
if (cached)
|
|
275
|
+
clientMetadataCache.delete(clientId);
|
|
276
|
+
const abort = new AbortController();
|
|
277
|
+
let timeout;
|
|
278
|
+
try {
|
|
279
|
+
const timedOut = new Promise((_, reject) => {
|
|
280
|
+
timeout = setTimeout(() => {
|
|
281
|
+
abort.abort();
|
|
282
|
+
reject(new Error("Client metadata document request timed out"));
|
|
283
|
+
}, CLIENT_METADATA_TIMEOUT_MS);
|
|
284
|
+
});
|
|
285
|
+
const { metadata, response } = await Promise.race([
|
|
286
|
+
fetchOAuthClientMetadataDocument(clientId, abort.signal),
|
|
287
|
+
timedOut,
|
|
288
|
+
]);
|
|
289
|
+
cacheClientMetadata(clientId, metadata, response, now);
|
|
290
|
+
return metadata;
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
if (timeout)
|
|
294
|
+
clearTimeout(timeout);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
//# sourceMappingURL=oauth-client-metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-client-metadata.js","sourceRoot":"","sources":["../../src/mcp/oauth-client-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAE5D,MAAM,yBAAyB,GAAG,EAAE,GAAG,IAAI,CAAC;AAC5C,MAAM,0BAA0B,GAAG,KAAK,CAAC;AACzC,MAAM,6BAA6B,GAAG,CAAC,CAAC;AACxC,MAAM,iCAAiC,GAAG,GAAG,CAAC;AAC9C,MAAM,gCAAgC,GAAG,CAAC,GAAG,MAAM,CAAC;AAEpD,MAAM,2BAA2B,GAAG,IAAI,GAAG,CAAC;IAC1C,aAAa;IACb,OAAO;IACP,WAAW;IACX,OAAO;IACP,OAAO;IACP,QAAQ;CACT,CAAC,CAAC;AAiBH,MAAM,mBAAmB,GAAG,IAAI,GAAG,EAAgC,CAAC;AAEpE,MAAM,UAAU,uBAAuB,CAAC,QAAgB;IACtD,OAAO,sBAAsB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,8BAA8B,CAAC,QAAgB;IAC7D,IAAI,CAAC,QAAQ,IAAI,QAAQ,KAAK,QAAQ,CAAC,IAAI,EAAE,IAAI,QAAQ,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;QACxE,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACpD,CAAC;IACD,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACpD,CAAC;IACD,IACE,GAAG,CAAC,QAAQ,KAAK,QAAQ;QACzB,GAAG,CAAC,QAAQ,KAAK,GAAG;QACpB,CAAC,GAAG,CAAC,QAAQ;QACb,GAAG,CAAC,IAAI;QACR,GAAG,CAAC,QAAQ;QACZ,GAAG,CAAC,QAAQ,EACZ,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;IAC5E,CAAC;IACD,MAAM,YAAY,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;IAC9D,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IACvD,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAC5B,YAAY,EACZ,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAC3C,CAAC;IACF,IACE,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE;QAClC,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;YAC5C,OAAO,OAAO,KAAK,GAAG,IAAI,OAAO,KAAK,IAAI,CAAC;QAC7C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC,CAAC,EACF,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,KAAc;IACtD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,IAAI;QAAE,OAAO,KAAK,CAAC;IACnE,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;QAC3B,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC3D,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC3C,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YAC7B,OAAO,CACL,GAAG,CAAC,QAAQ,KAAK,WAAW;gBAC5B,GAAG,CAAC,QAAQ,KAAK,WAAW;gBAC5B,GAAG,CAAC,QAAQ,KAAK,KAAK;gBACtB,GAAG,CAAC,QAAQ,KAAK,OAAO,CACzB,CAAC;QACJ,CAAC;QACD,OAAO,CACL,CAAC,2BAA2B,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC;YAC9C,sBAAsB,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAC1C,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,UAAU,8BAA8B,CAC5C,YAAsB;IAEtB,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;QACjC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;QAC3B,OAAO,CACL,GAAG,CAAC,QAAQ,KAAK,QAAQ;YACzB,GAAG,CAAC,QAAQ,KAAK,WAAW;YAC5B,GAAG,CAAC,QAAQ,KAAK,WAAW;YAC5B,GAAG,CAAC,QAAQ,KAAK,KAAK;YACtB,GAAG,CAAC,QAAQ,KAAK,OAAO,CACzB,CAAC;IACJ,CAAC,CAAC;QACA,CAAC,CAAC,QAAQ;QACV,CAAC,CAAC,KAAK,CAAC;AACZ,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAc;IACtC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACzB,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC;QAClE,CAAC,CAAC,EAAE,CAAC;AACT,CAAC;AAED,SAAS,8BAA8B,CACrC,QAAgB,EAChB,KAAc;IAEd,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IACD,MAAM,QAAQ,GAAG,KAAgC,CAAC;IAClD,IAAI,QAAQ,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CACb,2DAA2D,CAC5D,CAAC;IACJ,CAAC;IACD,MAAM,UAAU,GACd,OAAO,QAAQ,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9E,IACE,CAAC,UAAU;QACX,UAAU,CAAC,MAAM,GAAG,GAAG;QACvB,uBAAuB,CAAC,IAAI,CAAC,UAAU,CAAC,EACxC,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;IAED,MAAM,eAAe,GAAG,QAAQ,CAAC,aAAa,CAAC;IAC/C,MAAM,YAAY,GAAG,gBAAgB,CAAC,eAAe,CAAC,CAAC;IACvD,IACE,CAAC,KAAK,CAAC,OAAO,CAAC,eAAe,CAAC;QAC/B,YAAY,CAAC,MAAM,KAAK,CAAC;QACzB,YAAY,CAAC,MAAM,GAAG,EAAE;QACxB,YAAY,CAAC,MAAM,KAAK,eAAe,CAAC,MAAM;QAC9C,CAAC,YAAY,CAAC,KAAK,CAAC,yBAAyB,CAAC,EAC9C,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,uBAAuB,GAC3B,OAAO,QAAQ,CAAC,0BAA0B,KAAK,QAAQ;QACrD,CAAC,CAAC,QAAQ,CAAC,0BAA0B;QACrC,CAAC,CAAC,MAAM,CAAC;IACb,IACE,QAAQ,CAAC,0BAA0B,KAAK,SAAS;QACjD,OAAO,QAAQ,CAAC,0BAA0B,KAAK,QAAQ,EACvD,CAAC;QACD,MAAM,IAAI,KAAK,CACb,gEAAgE,CACjE,CAAC;IACJ,CAAC;IACD,IAAI,uBAAuB,KAAK,MAAM,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAC1E,CAAC;IAED,MAAM,UAAU,GAAG,gBAAgB,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC1D,IACE,CAAC,QAAQ,CAAC,WAAW,KAAK,SAAS;QACjC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;YACnC,UAAU,CAAC,MAAM,KAAK,QAAQ,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QACvD,UAAU,CAAC,MAAM,GAAG,EAAE;QACtB,CAAC,UAAU,CAAC,KAAK,CACf,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,oBAAoB,IAAI,KAAK,KAAK,eAAe,CACvE,EACD,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAC1E,CAAC;IAED,MAAM,aAAa,GAAG,gBAAgB,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;IAChE,IACE,CAAC,QAAQ,CAAC,cAAc,KAAK,SAAS;QACpC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;YACtC,aAAa,CAAC,MAAM,KAAK,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QAC7D,aAAa,CAAC,MAAM,GAAG,EAAE;QACzB,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,KAAK,MAAM,CAAC,EAC/D,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;IAC7E,CAAC;IAED,MAAM,wBAAwB,GAC5B,OAAO,QAAQ,CAAC,gBAAgB,KAAK,QAAQ;QAC3C,CAAC,CAAC,QAAQ,CAAC,gBAAgB;QAC3B,CAAC,CAAC,SAAS,CAAC;IAChB,IACE,CAAC,QAAQ,CAAC,gBAAgB,KAAK,SAAS;QACtC,OAAO,QAAQ,CAAC,gBAAgB,KAAK,QAAQ,CAAC;QAChD,CAAC,wBAAwB;YACvB,wBAAwB,KAAK,QAAQ;YACrC,wBAAwB,KAAK,KAAK,CAAC,EACrC,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAC;IAC9E,CAAC;IACD,MAAM,eAAe,GACnB,wBAAwB,KAAK,QAAQ,IAAI,wBAAwB,KAAK,KAAK;QACzE,CAAC,CAAC,wBAAwB;QAC1B,CAAC,CAAC,8BAA8B,CAAC,YAAY,CAAC,CAAC;IAEnD,OAAO;QACL,QAAQ;QACR,UAAU;QACV,YAAY,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,CAAC;QACxC,UAAU,EAAE,UAAU,CAAC,MAAM;YAC3B,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;YAC1B,CAAC,CAAC,CAAC,oBAAoB,EAAE,eAAe,CAAC;QAC3C,aAAa,EAAE,aAAa,CAAC,MAAM;YACjC,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC;YAC7B,CAAC,CAAC,CAAC,MAAM,CAAC;QACZ,uBAAuB,EAAE,MAAM;QAC/B,eAAe;KAChB,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,uBAAuB,CAAC,QAAkB;IACvD,MAAM,aAAa,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACrE,IACE,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC;QAC9B,aAAa,GAAG,yBAAyB,EACzC,CAAC;QACD,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACrD,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IAE9B,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;IACzC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,CAAC;QACH,SAAS,CAAC;YACR,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,IAAI;gBAAE,MAAM;YAChB,SAAS,IAAI,KAAK,CAAC,UAAU,CAAC;YAC9B,IAAI,SAAS,GAAG,yBAAyB,EAAE,CAAC;gBAC1C,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;gBAC7C,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;YAC3D,CAAC;YACD,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,CAAC;QACD,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC;IACd,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,WAAW,EAAE,CAAC;IACvB,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,OAAgB,EAAE,GAAW;IAC/C,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;IACvE,IAAI,mDAAmD,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;QAC3E,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,yBAAyB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAClE,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,KAAK,EAAE,gCAAgC,CAAC,CAAC;IAC5E,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;IAC3D,IAAI,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,GAAG,GAAG,EAAE,CAAC;QAClD,OAAO,IAAI,CAAC,GAAG,CAAC,SAAS,GAAG,GAAG,EAAE,gCAAgC,CAAC,CAAC;IACrE,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAS,mBAAmB,CAC1B,QAAgB,EAChB,QAAqC,EACrC,QAAkB,EAClB,GAAW;IAEX,MAAM,KAAK,GAAG,UAAU,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAChD,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO;IACvB,mBAAmB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACrC,mBAAmB,CAAC,GAAG,CAAC,QAAQ,EAAE;QAChC,SAAS,EAAE,GAAG,GAAG,KAAK;QACtB,QAAQ;KACT,CAAC,CAAC;IACH,OAAO,mBAAmB,CAAC,IAAI,GAAG,iCAAiC,EAAE,CAAC;QACpE,MAAM,MAAM,GAAG,mBAAmB,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;QACvD,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,MAAM;QACtC,mBAAmB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACrC,CAAC;AACH,CAAC;AAED,KAAK,UAAU,gCAAgC,CAC7C,QAAgB,EAChB,MAAmB;IAEnB,MAAM,QAAQ,GAAG,MAAM,aAAa,CAClC,QAAQ,EACR;QACE,OAAO,EAAE;YACP,MAAM,EAAE,oDAAoD;SAC7D;QACD,MAAM;KACP,EACD;QACE,YAAY,EAAE,6BAA6B;QAC3C,SAAS,EAAE,IAAI;KAChB,CACF,CAAC;IACF,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACrD,MAAM,IAAI,KAAK,CACb,gDAAgD,QAAQ,CAAC,MAAM,EAAE,CAClE,CAAC;IACJ,CAAC;IACD,MAAM,WAAW,GACf,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACvE,IACE,WAAW,KAAK,kBAAkB;QAClC,WAAW,KAAK,kCAAkC,EAClD,CAAC;QACD,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACrD,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IACtE,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IACrD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAChE,CAAC;IACD,OAAO;QACL,QAAQ,EAAE,8BAA8B,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1D,QAAQ;KACT,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kCAAkC,CACtD,QAAgB;IAEhB,8BAA8B,CAAC,QAAQ,CAAC,CAAC;IACzC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,MAAM,GAAG,mBAAmB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACjD,IAAI,MAAM,IAAI,MAAM,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC;QACrC,mBAAmB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACrC,mBAAmB,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC1C,OAAO,MAAM,CAAC,QAAQ,CAAC;IACzB,CAAC;IACD,IAAI,MAAM;QAAE,mBAAmB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAEjD,MAAM,KAAK,GAAG,IAAI,eAAe,EAAE,CAAC;IACpC,IAAI,OAAkD,CAAC;IACvD,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;YAChD,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;gBACxB,KAAK,CAAC,KAAK,EAAE,CAAC;gBACd,MAAM,CAAC,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC,CAAC;YAClE,CAAC,EAAE,0BAA0B,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QACH,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;YAChD,gCAAgC,CAAC,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC;YACxD,QAAQ;SACT,CAAC,CAAC;QACH,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC;QACvD,OAAO,QAAQ,CAAC;IAClB,CAAC;YAAS,CAAC;QACT,IAAI,OAAO;YAAE,YAAY,CAAC,OAAO,CAAC,CAAC;IACrC,CAAC;AACH,CAAC","sourcesContent":["import { ssrfSafeFetch } from \"../extensions/url-safety.js\";\n\nconst CLIENT_METADATA_MAX_BYTES = 64 * 1024;\nconst CLIENT_METADATA_TIMEOUT_MS = 5_000;\nconst CLIENT_METADATA_MAX_REDIRECTS = 2;\nconst CLIENT_METADATA_MAX_CACHE_ENTRIES = 128;\nconst CLIENT_METADATA_MAX_CACHE_TTL_MS = 5 * 60_000;\n\nconst DISALLOWED_REDIRECT_SCHEMES = new Set([\n \"javascript:\",\n \"data:\",\n \"vbscript:\",\n \"file:\",\n \"blob:\",\n \"about:\",\n]);\n\nexport interface OAuthClientMetadataDocument {\n clientId: string;\n clientName: string;\n redirectUris: string[];\n grantTypes: string[];\n responseTypes: string[];\n tokenEndpointAuthMethod: \"none\";\n applicationType: \"native\" | \"web\";\n}\n\ninterface CachedClientMetadata {\n expiresAt: number;\n metadata: OAuthClientMetadataDocument;\n}\n\nconst clientMetadataCache = new Map<string, CachedClientMetadata>();\n\nexport function isUrlBasedOAuthClientId(clientId: string): boolean {\n return /^[a-z][a-z0-9+.-]*:/i.test(clientId);\n}\n\nexport function validateOAuthClientMetadataUrl(clientId: string): URL {\n if (!clientId || clientId !== clientId.trim() || clientId.length > 2048) {\n throw new Error(\"Client metadata URL is invalid\");\n }\n let url: URL;\n try {\n url = new URL(clientId);\n } catch {\n throw new Error(\"Client metadata URL is invalid\");\n }\n if (\n url.protocol !== \"https:\" ||\n url.pathname === \"/\" ||\n !url.pathname ||\n url.hash ||\n url.username ||\n url.password\n ) {\n throw new Error(\"Client metadata URL must be HTTPS with a non-root path\");\n }\n const authorityEnd = clientId.indexOf(\"/\", \"https://\".length);\n const queryStart = clientId.indexOf(\"?\", authorityEnd);\n const rawPath = clientId.slice(\n authorityEnd,\n queryStart === -1 ? undefined : queryStart,\n );\n if (\n rawPath.split(\"/\").some((segment) => {\n try {\n const decoded = decodeURIComponent(segment);\n return decoded === \".\" || decoded === \"..\";\n } catch {\n return true;\n }\n })\n ) {\n throw new Error(\"Client metadata URL must not contain dot path segments\");\n }\n return url;\n}\n\nexport function isAllowedOAuthRedirectUri(value: unknown): value is string {\n if (typeof value !== \"string\" || value.length > 2048) return false;\n try {\n const url = new URL(value);\n if (url.hash || url.username || url.password) return false;\n if (url.protocol === \"https:\") return true;\n if (url.protocol === \"http:\") {\n return (\n url.hostname === \"localhost\" ||\n url.hostname === \"127.0.0.1\" ||\n url.hostname === \"::1\" ||\n url.hostname === \"[::1]\"\n );\n }\n return (\n !DISALLOWED_REDIRECT_SCHEMES.has(url.protocol) &&\n /^[a-z][a-z0-9+.-]*:$/.test(url.protocol)\n );\n } catch {\n return false;\n }\n}\n\nexport function applicationTypeForRedirectUris(\n redirectUris: string[],\n): \"native\" | \"web\" {\n return redirectUris.some((value) => {\n const url = new URL(value);\n return (\n url.protocol !== \"https:\" ||\n url.hostname === \"localhost\" ||\n url.hostname === \"127.0.0.1\" ||\n url.hostname === \"::1\" ||\n url.hostname === \"[::1]\"\n );\n })\n ? \"native\"\n : \"web\";\n}\n\nfunction parseStringArray(value: unknown): string[] {\n return Array.isArray(value)\n ? value.filter((item): item is string => typeof item === \"string\")\n : [];\n}\n\nfunction validateClientMetadataDocument(\n clientId: string,\n value: unknown,\n): OAuthClientMetadataDocument {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new Error(\"Client metadata document must be a JSON object\");\n }\n const document = value as Record<string, unknown>;\n if (document.client_id !== clientId) {\n throw new Error(\n \"Client metadata document client_id does not match its URL\",\n );\n }\n const clientName =\n typeof document.client_name === \"string\" ? document.client_name.trim() : \"\";\n if (\n !clientName ||\n clientName.length > 120 ||\n /[\\u0000-\\u001f\\u007f]/.test(clientName)\n ) {\n throw new Error(\"Client metadata document client_name is invalid\");\n }\n\n const rawRedirectUris = document.redirect_uris;\n const redirectUris = parseStringArray(rawRedirectUris);\n if (\n !Array.isArray(rawRedirectUris) ||\n redirectUris.length === 0 ||\n redirectUris.length > 20 ||\n redirectUris.length !== rawRedirectUris.length ||\n !redirectUris.every(isAllowedOAuthRedirectUri)\n ) {\n throw new Error(\"Client metadata document redirect_uris are invalid\");\n }\n\n const tokenEndpointAuthMethod =\n typeof document.token_endpoint_auth_method === \"string\"\n ? document.token_endpoint_auth_method\n : \"none\";\n if (\n document.token_endpoint_auth_method !== undefined &&\n typeof document.token_endpoint_auth_method !== \"string\"\n ) {\n throw new Error(\n \"Client metadata document token_endpoint_auth_method is invalid\",\n );\n }\n if (tokenEndpointAuthMethod !== \"none\") {\n throw new Error(\"Only public Client ID Metadata clients are supported\");\n }\n\n const grantTypes = parseStringArray(document.grant_types);\n if (\n (document.grant_types !== undefined &&\n (!Array.isArray(document.grant_types) ||\n grantTypes.length !== document.grant_types.length)) ||\n grantTypes.length > 10 ||\n !grantTypes.every(\n (grant) => grant === \"authorization_code\" || grant === \"refresh_token\",\n )\n ) {\n throw new Error(\"Client metadata document grant_types are unsupported\");\n }\n\n const responseTypes = parseStringArray(document.response_types);\n if (\n (document.response_types !== undefined &&\n (!Array.isArray(document.response_types) ||\n responseTypes.length !== document.response_types.length)) ||\n responseTypes.length > 10 ||\n !responseTypes.every((responseType) => responseType === \"code\")\n ) {\n throw new Error(\"Client metadata document response_types are unsupported\");\n }\n\n const requestedApplicationType =\n typeof document.application_type === \"string\"\n ? document.application_type\n : undefined;\n if (\n (document.application_type !== undefined &&\n typeof document.application_type !== \"string\") ||\n (requestedApplicationType &&\n requestedApplicationType !== \"native\" &&\n requestedApplicationType !== \"web\")\n ) {\n throw new Error(\"Client metadata document application_type is unsupported\");\n }\n const applicationType: \"native\" | \"web\" =\n requestedApplicationType === \"native\" || requestedApplicationType === \"web\"\n ? requestedApplicationType\n : applicationTypeForRedirectUris(redirectUris);\n\n return {\n clientId,\n clientName,\n redirectUris: [...new Set(redirectUris)],\n grantTypes: grantTypes.length\n ? [...new Set(grantTypes)]\n : [\"authorization_code\", \"refresh_token\"],\n responseTypes: responseTypes.length\n ? [...new Set(responseTypes)]\n : [\"code\"],\n tokenEndpointAuthMethod: \"none\",\n applicationType,\n };\n}\n\nasync function readBoundedResponseBody(response: Response): Promise<string> {\n const contentLength = Number(response.headers.get(\"content-length\"));\n if (\n Number.isFinite(contentLength) &&\n contentLength > CLIENT_METADATA_MAX_BYTES\n ) {\n await response.body?.cancel().catch(() => undefined);\n throw new Error(\"Client metadata document is too large\");\n }\n if (!response.body) return \"\";\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let bytesRead = 0;\n let text = \"\";\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n bytesRead += value.byteLength;\n if (bytesRead > CLIENT_METADATA_MAX_BYTES) {\n await reader.cancel().catch(() => undefined);\n throw new Error(\"Client metadata document is too large\");\n }\n text += decoder.decode(value, { stream: true });\n }\n text += decoder.decode();\n return text;\n } finally {\n reader.releaseLock();\n }\n}\n\nfunction cacheTtlMs(headers: Headers, now: number): number {\n const cacheControl = headers.get(\"cache-control\")?.toLowerCase() ?? \"\";\n if (/(?:^|,)\\s*(?:no-store|no-cache|private)(?:\\s|,|$)/.test(cacheControl)) {\n return 0;\n }\n const maxAge = cacheControl.match(/(?:^|,)\\s*max-age=(\\d+)/)?.[1];\n if (maxAge) {\n return Math.min(Number(maxAge) * 1_000, CLIENT_METADATA_MAX_CACHE_TTL_MS);\n }\n const expiresAt = Date.parse(headers.get(\"expires\") ?? \"\");\n if (Number.isFinite(expiresAt) && expiresAt > now) {\n return Math.min(expiresAt - now, CLIENT_METADATA_MAX_CACHE_TTL_MS);\n }\n return 0;\n}\n\nfunction cacheClientMetadata(\n clientId: string,\n metadata: OAuthClientMetadataDocument,\n response: Response,\n now: number,\n): void {\n const ttlMs = cacheTtlMs(response.headers, now);\n if (ttlMs <= 0) return;\n clientMetadataCache.delete(clientId);\n clientMetadataCache.set(clientId, {\n expiresAt: now + ttlMs,\n metadata,\n });\n while (clientMetadataCache.size > CLIENT_METADATA_MAX_CACHE_ENTRIES) {\n const oldest = clientMetadataCache.keys().next().value;\n if (typeof oldest !== \"string\") break;\n clientMetadataCache.delete(oldest);\n }\n}\n\nasync function fetchOAuthClientMetadataDocument(\n clientId: string,\n signal: AbortSignal,\n): Promise<{ metadata: OAuthClientMetadataDocument; response: Response }> {\n const response = await ssrfSafeFetch(\n clientId,\n {\n headers: {\n Accept: \"application/json, application/client-metadata+json\",\n },\n signal,\n },\n {\n maxRedirects: CLIENT_METADATA_MAX_REDIRECTS,\n httpsOnly: true,\n },\n );\n if (!response.ok) {\n await response.body?.cancel().catch(() => undefined);\n throw new Error(\n `Client metadata document request failed with ${response.status}`,\n );\n }\n const contentType =\n response.headers.get(\"content-type\")?.split(\";\", 1)[0]?.trim() ?? \"\";\n if (\n contentType !== \"application/json\" &&\n contentType !== \"application/client-metadata+json\"\n ) {\n await response.body?.cancel().catch(() => undefined);\n throw new Error(\"Client metadata document content type is invalid\");\n }\n\n const body = await readBoundedResponseBody(response);\n let parsed: unknown;\n try {\n parsed = JSON.parse(body);\n } catch {\n throw new Error(\"Client metadata document is not valid JSON\");\n }\n return {\n metadata: validateClientMetadataDocument(clientId, parsed),\n response,\n };\n}\n\nexport async function resolveOAuthClientMetadataDocument(\n clientId: string,\n): Promise<OAuthClientMetadataDocument> {\n validateOAuthClientMetadataUrl(clientId);\n const now = Date.now();\n const cached = clientMetadataCache.get(clientId);\n if (cached && cached.expiresAt > now) {\n clientMetadataCache.delete(clientId);\n clientMetadataCache.set(clientId, cached);\n return cached.metadata;\n }\n if (cached) clientMetadataCache.delete(clientId);\n\n const abort = new AbortController();\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n const timedOut = new Promise<never>((_, reject) => {\n timeout = setTimeout(() => {\n abort.abort();\n reject(new Error(\"Client metadata document request timed out\"));\n }, CLIENT_METADATA_TIMEOUT_MS);\n });\n const { metadata, response } = await Promise.race([\n fetchOAuthClientMetadataDocument(clientId, abort.signal),\n timedOut,\n ]);\n cacheClientMetadata(clientId, metadata, response, now);\n return metadata;\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth-route.d.ts","sourceRoot":"","sources":["../../src/mcp/oauth-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"oauth-route.d.ts","sourceRoot":"","sources":["../../src/mcp/oauth-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAgClC,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiID,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAIpE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAItE;AAWD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAuB7D;AAED,wBAAgB,uCAAuC,CACrD,KAAK,EAAE,OAAO,GACb,MAAM,GAAG,SAAS,CAIpB;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAM7D;AAqBD,wBAAgB,uCAAuC,CACrD,KAAK,EAAE,OAAO,GACb,QAAQ,CAeV;AAED,wBAAgB,yCAAyC,CACvD,KAAK,EAAE,OAAO,GACb,QAAQ,CAwBV;AAkrBD,wBAAsB,cAAc,CAClC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,QAAQ,CAAC,CAenB"}
|
package/dist/mcp/oauth-route.js
CHANGED
|
@@ -13,9 +13,12 @@ import { getOrgDomain } from "../org/context.js";
|
|
|
13
13
|
import { getConfiguredLoginHtml, getSession } from "../server/auth.js";
|
|
14
14
|
import { getAuthSecret } from "../server/better-auth-instance.js";
|
|
15
15
|
import { readBody } from "../server/h3-helpers.js";
|
|
16
|
+
import { applicationTypeForRedirectUris, isAllowedOAuthRedirectUri, isUrlBasedOAuthClientId, resolveOAuthClientMetadataDocument, } from "./oauth-client-metadata.js";
|
|
16
17
|
import { createOAuthCode, createOAuthRefreshToken, consumeOAuthCode, generateOpaqueToken, getOAuthClient, getOAuthCode, getOAuthRefreshToken, registerOAuthClient, touchOAuthRefreshToken, } from "./oauth-store.js";
|
|
17
|
-
import { MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS,
|
|
18
|
+
import { MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS, MCP_OAUTH_SCOPES, normalizeOAuthScope, signMcpOAuthAccessToken, } from "./oauth-token.js";
|
|
18
19
|
import { MCP_PUBLIC_ROUTE_PREFIX, MCP_ROUTE_PREFIXES } from "./route-paths.js";
|
|
20
|
+
const MCP_OAUTH_RESOURCE_SCOPES = MCP_OAUTH_SCOPES.filter((scope) => scope !== "offline_access");
|
|
21
|
+
const MCP_OAUTH_RESOURCE_SCOPE = MCP_OAUTH_RESOURCE_SCOPES.join(" ");
|
|
19
22
|
function json(body, status = 200) {
|
|
20
23
|
return new Response(JSON.stringify(body), {
|
|
21
24
|
status,
|
|
@@ -187,7 +190,7 @@ export function getMcpOAuthProtectedResourceMetadataUrl(event) {
|
|
|
187
190
|
}
|
|
188
191
|
export function buildMcpOAuthChallenge(event) {
|
|
189
192
|
const metadata = getMcpOAuthProtectedResourceMetadataUrl(event);
|
|
190
|
-
const scope =
|
|
193
|
+
const scope = MCP_OAUTH_RESOURCE_SCOPE;
|
|
191
194
|
return metadata
|
|
192
195
|
? `Bearer resource_metadata="${metadata}", scope="${scope}"`
|
|
193
196
|
: `Bearer scope="${scope}"`;
|
|
@@ -220,7 +223,7 @@ export function handleMcpOAuthProtectedResourceMetadata(event) {
|
|
|
220
223
|
return json({
|
|
221
224
|
resource,
|
|
222
225
|
authorization_servers: [issuer],
|
|
223
|
-
scopes_supported:
|
|
226
|
+
scopes_supported: MCP_OAUTH_RESOURCE_SCOPES,
|
|
224
227
|
resource_documentation: issuer,
|
|
225
228
|
});
|
|
226
229
|
}
|
|
@@ -245,54 +248,10 @@ export function handleMcpOAuthAuthorizationServerMetadata(event) {
|
|
|
245
248
|
code_challenge_methods_supported: ["S256"],
|
|
246
249
|
token_endpoint_auth_methods_supported: ["none"],
|
|
247
250
|
scopes_supported: MCP_OAUTH_SCOPES,
|
|
251
|
+
authorization_response_iss_parameter_supported: true,
|
|
252
|
+
client_id_metadata_document_supported: true,
|
|
248
253
|
});
|
|
249
254
|
}
|
|
250
|
-
// Schemes that must never be accepted as a redirect target: they can execute
|
|
251
|
-
// script or read local resources if a redirect is ever rendered in a browser
|
|
252
|
-
// or webview context.
|
|
253
|
-
const DISALLOWED_REDIRECT_SCHEMES = new Set([
|
|
254
|
-
"javascript:",
|
|
255
|
-
"data:",
|
|
256
|
-
"vbscript:",
|
|
257
|
-
"file:",
|
|
258
|
-
"blob:",
|
|
259
|
-
"about:",
|
|
260
|
-
]);
|
|
261
|
-
// Native/desktop IDE clients (Cursor, VS Code, …) register a private-use URI
|
|
262
|
-
// scheme callback such as `cursor://` or `vscode://` (RFC 8252 §7.1) instead of
|
|
263
|
-
// an https/loopback URL. The authorization code is bound by PKCE (S256), so
|
|
264
|
-
// delivering it through a client-registered app scheme is safe.
|
|
265
|
-
function isPrivateUseRedirectScheme(protocol) {
|
|
266
|
-
if (DISALLOWED_REDIRECT_SCHEMES.has(protocol))
|
|
267
|
-
return false;
|
|
268
|
-
// RFC 3986 scheme grammar: ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ), with a
|
|
269
|
-
// trailing ":" from URL.protocol. Require a non-http(s) custom scheme here;
|
|
270
|
-
// http/https are handled explicitly above.
|
|
271
|
-
return /^[a-z][a-z0-9+.-]*:$/.test(protocol);
|
|
272
|
-
}
|
|
273
|
-
function isAllowedRedirectUri(value) {
|
|
274
|
-
if (typeof value !== "string" || value.length > 2048)
|
|
275
|
-
return false;
|
|
276
|
-
try {
|
|
277
|
-
const url = new URL(value);
|
|
278
|
-
if (url.hash)
|
|
279
|
-
return false;
|
|
280
|
-
if (url.username || url.password)
|
|
281
|
-
return false;
|
|
282
|
-
if (url.protocol === "https:")
|
|
283
|
-
return true;
|
|
284
|
-
if (url.protocol === "http:") {
|
|
285
|
-
return (url.hostname === "localhost" ||
|
|
286
|
-
url.hostname === "127.0.0.1" ||
|
|
287
|
-
url.hostname === "::1" ||
|
|
288
|
-
url.hostname === "[::1]");
|
|
289
|
-
}
|
|
290
|
-
return isPrivateUseRedirectScheme(url.protocol);
|
|
291
|
-
}
|
|
292
|
-
catch {
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
255
|
function parseStringArray(value) {
|
|
297
256
|
return Array.isArray(value)
|
|
298
257
|
? value.filter((item) => typeof item === "string")
|
|
@@ -306,7 +265,7 @@ async function handleRegister(event) {
|
|
|
306
265
|
const redirectUris = parseStringArray(body.redirect_uris);
|
|
307
266
|
if (redirectUris.length === 0 ||
|
|
308
267
|
redirectUris.length > 20 ||
|
|
309
|
-
!redirectUris.every(
|
|
268
|
+
!redirectUris.every(isAllowedOAuthRedirectUri)) {
|
|
310
269
|
return oauthError("invalid_client_metadata", "redirect_uris must contain valid HTTPS or localhost callback URLs");
|
|
311
270
|
}
|
|
312
271
|
const grantTypes = parseStringArray(body.grant_types);
|
|
@@ -324,6 +283,15 @@ async function handleRegister(event) {
|
|
|
324
283
|
if (method !== "none") {
|
|
325
284
|
return oauthError("invalid_client_metadata", "Only public OAuth clients are supported");
|
|
326
285
|
}
|
|
286
|
+
const requestedApplicationType = typeof body.application_type === "string"
|
|
287
|
+
? body.application_type
|
|
288
|
+
: undefined;
|
|
289
|
+
if (requestedApplicationType &&
|
|
290
|
+
requestedApplicationType !== "native" &&
|
|
291
|
+
requestedApplicationType !== "web") {
|
|
292
|
+
return oauthError("invalid_client_metadata", "application_type must be native or web");
|
|
293
|
+
}
|
|
294
|
+
const applicationType = requestedApplicationType ?? applicationTypeForRedirectUris(redirectUris);
|
|
327
295
|
const clientName = typeof body.client_name === "string"
|
|
328
296
|
? body.client_name.trim().slice(0, 120)
|
|
329
297
|
: null;
|
|
@@ -351,11 +319,13 @@ async function handleRegister(event) {
|
|
|
351
319
|
grant_types: client.grantTypes,
|
|
352
320
|
response_types: client.responseTypes,
|
|
353
321
|
token_endpoint_auth_method: client.tokenEndpointAuthMethod,
|
|
322
|
+
application_type: applicationType,
|
|
354
323
|
}, 201);
|
|
355
324
|
}
|
|
356
325
|
function redirectWithOAuthError(params) {
|
|
357
326
|
const url = new URL(params.redirectUri);
|
|
358
327
|
url.searchParams.set("error", params.error);
|
|
328
|
+
url.searchParams.set("iss", params.issuer);
|
|
359
329
|
if (params.description) {
|
|
360
330
|
url.searchParams.set("error_description", params.description);
|
|
361
331
|
}
|
|
@@ -366,6 +336,7 @@ function redirectWithOAuthError(params) {
|
|
|
366
336
|
function buildCodeRedirectUrl(params) {
|
|
367
337
|
const url = new URL(params.redirectUri);
|
|
368
338
|
url.searchParams.set("code", params.code);
|
|
339
|
+
url.searchParams.set("iss", params.issuer);
|
|
369
340
|
if (params.state)
|
|
370
341
|
url.searchParams.set("state", params.state);
|
|
371
342
|
return url.toString();
|
|
@@ -467,6 +438,7 @@ function renderConsentPage(params) {
|
|
|
467
438
|
<h1>Authorize ${escapeHtml(params.clientName)}</h1>
|
|
468
439
|
<p>${escapeHtml(params.appName)} will let this MCP client act as ${escapeHtml(params.email)} for these scopes:</p>
|
|
469
440
|
<ul>${scopes}</ul>
|
|
441
|
+
<p>After authorization, your browser will return to <code>${escapeHtml(params.redirectUri)}</code>.</p>
|
|
470
442
|
<form method="post">
|
|
471
443
|
${hidden}
|
|
472
444
|
<div class="actions">
|
|
@@ -561,6 +533,7 @@ async function handleAuthorize(event, options) {
|
|
|
561
533
|
return oauthError("invalid_request", "Cross-origin authorize POST rejected", 403);
|
|
562
534
|
}
|
|
563
535
|
const params = await readOAuthParams(event);
|
|
536
|
+
const issuer = getMcpOAuthIssuer(event);
|
|
564
537
|
const state = params.state;
|
|
565
538
|
const clientId = params.client_id;
|
|
566
539
|
const redirectUri = params.redirect_uri;
|
|
@@ -571,6 +544,7 @@ async function handleAuthorize(event, options) {
|
|
|
571
544
|
}
|
|
572
545
|
if (!clientId ||
|
|
573
546
|
!redirectUri ||
|
|
547
|
+
!issuer ||
|
|
574
548
|
!resource ||
|
|
575
549
|
!expectedResources.includes(resource)) {
|
|
576
550
|
return oauthError("invalid_request", "Invalid OAuth authorization request");
|
|
@@ -578,7 +552,9 @@ async function handleAuthorize(event, options) {
|
|
|
578
552
|
if (params.code_challenge_method !== "S256" || !params.code_challenge) {
|
|
579
553
|
return oauthError("invalid_request", "PKCE S256 is required");
|
|
580
554
|
}
|
|
581
|
-
const client =
|
|
555
|
+
const client = isUrlBasedOAuthClientId(clientId)
|
|
556
|
+
? await resolveOAuthClientMetadataDocument(clientId).catch(() => null)
|
|
557
|
+
: await getOAuthClient(clientId);
|
|
582
558
|
if (!client || !client.redirectUris.includes(redirectUri)) {
|
|
583
559
|
return oauthError("invalid_client", "Unknown client or redirect_uri");
|
|
584
560
|
}
|
|
@@ -587,6 +563,7 @@ async function handleAuthorize(event, options) {
|
|
|
587
563
|
if (params.prompt === "none") {
|
|
588
564
|
return redirectWithOAuthError({
|
|
589
565
|
redirectUri,
|
|
566
|
+
issuer,
|
|
590
567
|
state,
|
|
591
568
|
error: "login_required",
|
|
592
569
|
});
|
|
@@ -600,6 +577,7 @@ async function handleAuthorize(event, options) {
|
|
|
600
577
|
if (!scope) {
|
|
601
578
|
return redirectWithOAuthError({
|
|
602
579
|
redirectUri,
|
|
580
|
+
issuer,
|
|
603
581
|
state,
|
|
604
582
|
error: "invalid_scope",
|
|
605
583
|
});
|
|
@@ -609,6 +587,7 @@ async function handleAuthorize(event, options) {
|
|
|
609
587
|
appName: options.appName || options.appId || "Agent Native",
|
|
610
588
|
email: session.email,
|
|
611
589
|
clientName: client.clientName || client.clientId,
|
|
590
|
+
redirectUri,
|
|
612
591
|
scopes: scope.split(/\s+/),
|
|
613
592
|
fields: {
|
|
614
593
|
response_type: "code",
|
|
@@ -643,6 +622,7 @@ async function handleAuthorize(event, options) {
|
|
|
643
622
|
if (params.decision !== "approve") {
|
|
644
623
|
return redirectWithOAuthError({
|
|
645
624
|
redirectUri,
|
|
625
|
+
issuer,
|
|
646
626
|
state,
|
|
647
627
|
error: "access_denied",
|
|
648
628
|
});
|
|
@@ -670,7 +650,7 @@ async function handleAuthorize(event, options) {
|
|
|
670
650
|
isDeepLinkRedirect =
|
|
671
651
|
protocol !== "http:" &&
|
|
672
652
|
protocol !== "https:" &&
|
|
673
|
-
|
|
653
|
+
/^[a-z][a-z0-9+.-]*:$/.test(protocol);
|
|
674
654
|
}
|
|
675
655
|
catch {
|
|
676
656
|
isDeepLinkRedirect = false;
|
|
@@ -681,12 +661,18 @@ async function handleAuthorize(event, options) {
|
|
|
681
661
|
clientName: client.clientName ?? null,
|
|
682
662
|
redirectUrl: buildCodeRedirectUrl({
|
|
683
663
|
redirectUri,
|
|
664
|
+
issuer,
|
|
684
665
|
state,
|
|
685
666
|
code: code.code,
|
|
686
667
|
}),
|
|
687
668
|
}));
|
|
688
669
|
}
|
|
689
|
-
return redirectWithCode({
|
|
670
|
+
return redirectWithCode({
|
|
671
|
+
redirectUri,
|
|
672
|
+
issuer,
|
|
673
|
+
state,
|
|
674
|
+
code: code.code,
|
|
675
|
+
});
|
|
690
676
|
}
|
|
691
677
|
async function issueTokenSet(params) {
|
|
692
678
|
const refreshToken = generateOpaqueToken();
|