palaryn 0.1.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +243 -588
- package/dist/sdk/typescript/src/client.js +2 -2
- package/dist/sdk/typescript/src/client.js.map +1 -1
- package/dist/src/anomaly/detector.d.ts +7 -4
- package/dist/src/anomaly/detector.d.ts.map +1 -1
- package/dist/src/anomaly/detector.js +22 -12
- package/dist/src/anomaly/detector.js.map +1 -1
- package/dist/src/audit/logger.d.ts +10 -0
- package/dist/src/audit/logger.d.ts.map +1 -1
- package/dist/src/audit/logger.js +52 -38
- package/dist/src/audit/logger.js.map +1 -1
- package/dist/src/auth/routes.d.ts.map +1 -1
- package/dist/src/auth/routes.js +35 -0
- package/dist/src/auth/routes.js.map +1 -1
- package/dist/src/budget/manager.d.ts +5 -0
- package/dist/src/budget/manager.d.ts.map +1 -1
- package/dist/src/budget/manager.js +32 -0
- package/dist/src/budget/manager.js.map +1 -1
- package/dist/src/budget/model-pricing.d.ts +20 -0
- package/dist/src/budget/model-pricing.d.ts.map +1 -0
- package/dist/src/budget/model-pricing.js +107 -0
- package/dist/src/budget/model-pricing.js.map +1 -0
- package/dist/src/budget/usage-extractor.d.ts +3 -1
- package/dist/src/budget/usage-extractor.d.ts.map +1 -1
- package/dist/src/budget/usage-extractor.js +47 -3
- package/dist/src/budget/usage-extractor.js.map +1 -1
- package/dist/src/config/defaults.d.ts.map +1 -1
- package/dist/src/config/defaults.js +65 -13
- package/dist/src/config/defaults.js.map +1 -1
- package/dist/src/dlp/tool-patterns.d.ts +7 -0
- package/dist/src/dlp/tool-patterns.d.ts.map +1 -0
- package/dist/src/dlp/tool-patterns.js +34 -0
- package/dist/src/dlp/tool-patterns.js.map +1 -0
- package/dist/src/executor/filesystem-executor.d.ts +28 -0
- package/dist/src/executor/filesystem-executor.d.ts.map +1 -0
- package/dist/src/executor/filesystem-executor.js +192 -0
- package/dist/src/executor/filesystem-executor.js.map +1 -0
- package/dist/src/executor/http-executor.d.ts.map +1 -1
- package/dist/src/executor/http-executor.js +22 -2
- package/dist/src/executor/http-executor.js.map +1 -1
- package/dist/src/executor/index.d.ts +4 -0
- package/dist/src/executor/index.d.ts.map +1 -1
- package/dist/src/executor/index.js +9 -1
- package/dist/src/executor/index.js.map +1 -1
- package/dist/src/executor/shell-executor.d.ts +22 -0
- package/dist/src/executor/shell-executor.d.ts.map +1 -0
- package/dist/src/executor/shell-executor.js +119 -0
- package/dist/src/executor/shell-executor.js.map +1 -0
- package/dist/src/executor/sql-executor.d.ts +29 -0
- package/dist/src/executor/sql-executor.d.ts.map +1 -0
- package/dist/src/executor/sql-executor.js +114 -0
- package/dist/src/executor/sql-executor.js.map +1 -0
- package/dist/src/executor/websocket-executor.d.ts +26 -0
- package/dist/src/executor/websocket-executor.d.ts.map +1 -0
- package/dist/src/executor/websocket-executor.js +205 -0
- package/dist/src/executor/websocket-executor.js.map +1 -0
- package/dist/src/interceptor/index.d.ts +2 -0
- package/dist/src/interceptor/index.d.ts.map +1 -0
- package/dist/src/interceptor/index.js +6 -0
- package/dist/src/interceptor/index.js.map +1 -0
- package/dist/src/interceptor/provider-interceptor.d.ts +36 -0
- package/dist/src/interceptor/provider-interceptor.d.ts.map +1 -0
- package/dist/src/interceptor/provider-interceptor.js +302 -0
- package/dist/src/interceptor/provider-interceptor.js.map +1 -0
- package/dist/src/mcp/auth-verifier.d.ts.map +1 -1
- package/dist/src/mcp/auth-verifier.js +3 -2
- package/dist/src/mcp/auth-verifier.js.map +1 -1
- package/dist/src/mcp/bridge.d.ts +14 -10
- package/dist/src/mcp/bridge.d.ts.map +1 -1
- package/dist/src/mcp/bridge.js +51 -227
- package/dist/src/mcp/bridge.js.map +1 -1
- package/dist/src/mcp/http-transport.d.ts +2 -0
- package/dist/src/mcp/http-transport.d.ts.map +1 -1
- package/dist/src/mcp/http-transport.js +117 -66
- package/dist/src/mcp/http-transport.js.map +1 -1
- package/dist/src/mcp/internal-auth.d.ts +13 -0
- package/dist/src/mcp/internal-auth.d.ts.map +1 -0
- package/dist/src/mcp/internal-auth.js +12 -0
- package/dist/src/mcp/internal-auth.js.map +1 -0
- package/dist/src/mcp/tool-definitions.d.ts +41 -0
- package/dist/src/mcp/tool-definitions.d.ts.map +1 -0
- package/dist/src/mcp/tool-definitions.js +491 -0
- package/dist/src/mcp/tool-definitions.js.map +1 -0
- package/dist/src/middleware/auth.js.map +1 -1
- package/dist/src/middleware/session.js.map +1 -1
- package/dist/src/middleware/validate.d.ts +8 -0
- package/dist/src/middleware/validate.d.ts.map +1 -1
- package/dist/src/middleware/validate.js +45 -0
- package/dist/src/middleware/validate.js.map +1 -1
- package/dist/src/policy/engine.d.ts +4 -0
- package/dist/src/policy/engine.d.ts.map +1 -1
- package/dist/src/policy/engine.js +117 -0
- package/dist/src/policy/engine.js.map +1 -1
- package/dist/src/saas/routes.d.ts.map +1 -1
- package/dist/src/saas/routes.js +355 -22
- package/dist/src/saas/routes.js.map +1 -1
- package/dist/src/server/app.d.ts.map +1 -1
- package/dist/src/server/app.js +24 -3
- package/dist/src/server/app.js.map +1 -1
- package/dist/src/server/gateway.d.ts.map +1 -1
- package/dist/src/server/gateway.js +17 -0
- package/dist/src/server/gateway.js.map +1 -1
- package/dist/src/server/index.d.ts.map +1 -1
- package/dist/src/server/index.js +18 -0
- package/dist/src/server/index.js.map +1 -1
- package/dist/src/storage/interfaces.d.ts +14 -3
- package/dist/src/storage/interfaces.d.ts.map +1 -1
- package/dist/src/storage/memory.d.ts +2 -0
- package/dist/src/storage/memory.d.ts.map +1 -1
- package/dist/src/storage/memory.js +6 -0
- package/dist/src/storage/memory.js.map +1 -1
- package/dist/src/storage/postgres.d.ts +5 -0
- package/dist/src/storage/postgres.d.ts.map +1 -1
- package/dist/src/storage/postgres.js +16 -0
- package/dist/src/storage/postgres.js.map +1 -1
- package/dist/src/storage/redis.d.ts +10 -0
- package/dist/src/storage/redis.d.ts.map +1 -1
- package/dist/src/storage/redis.js +65 -0
- package/dist/src/storage/redis.js.map +1 -1
- package/dist/src/types/budget.d.ts +4 -0
- package/dist/src/types/budget.d.ts.map +1 -1
- package/dist/src/types/config.d.ts +58 -0
- package/dist/src/types/config.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +1 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/policy.d.ts +11 -1
- package/dist/src/types/policy.d.ts.map +1 -1
- package/dist/src/types/tool-result.d.ts +11 -0
- package/dist/src/types/tool-result.d.ts.map +1 -1
- package/dist/tests/unit/app-routes.test.d.ts +2 -0
- package/dist/tests/unit/app-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/app-routes.test.js +715 -0
- package/dist/tests/unit/app-routes.test.js.map +1 -0
- package/dist/tests/unit/audit-logger.test.js +105 -0
- package/dist/tests/unit/audit-logger.test.js.map +1 -1
- package/dist/tests/unit/auth-providers.test.d.ts +2 -0
- package/dist/tests/unit/auth-providers.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-providers.test.js +279 -0
- package/dist/tests/unit/auth-providers.test.js.map +1 -0
- package/dist/tests/unit/auth-routes-extended.test.d.ts +2 -0
- package/dist/tests/unit/auth-routes-extended.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-routes-extended.test.js +993 -0
- package/dist/tests/unit/auth-routes-extended.test.js.map +1 -0
- package/dist/tests/unit/auth-verifier.test.d.ts +2 -0
- package/dist/tests/unit/auth-verifier.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-verifier.test.js +505 -0
- package/dist/tests/unit/auth-verifier.test.js.map +1 -0
- package/dist/tests/unit/billing-routes.test.d.ts +2 -0
- package/dist/tests/unit/billing-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/billing-routes.test.js +432 -0
- package/dist/tests/unit/billing-routes.test.js.map +1 -0
- package/dist/tests/unit/config-defaults.test.d.ts +2 -0
- package/dist/tests/unit/config-defaults.test.d.ts.map +1 -0
- package/dist/tests/unit/config-defaults.test.js +119 -0
- package/dist/tests/unit/config-defaults.test.js.map +1 -0
- package/dist/tests/unit/defaults.test.js +0 -10
- package/dist/tests/unit/defaults.test.js.map +1 -1
- package/dist/tests/unit/filesystem-executor.test.d.ts +2 -0
- package/dist/tests/unit/filesystem-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/filesystem-executor.test.js +280 -0
- package/dist/tests/unit/filesystem-executor.test.js.map +1 -0
- package/dist/tests/unit/gateway-branches.test.d.ts +2 -0
- package/dist/tests/unit/gateway-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/gateway-branches.test.js +1039 -0
- package/dist/tests/unit/gateway-branches.test.js.map +1 -0
- package/dist/tests/unit/http-executor-branches.test.d.ts +2 -0
- package/dist/tests/unit/http-executor-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/http-executor-branches.test.js +495 -0
- package/dist/tests/unit/http-executor-branches.test.js.map +1 -0
- package/dist/tests/unit/logger.test.d.ts +2 -0
- package/dist/tests/unit/logger.test.d.ts.map +1 -0
- package/dist/tests/unit/logger.test.js +97 -0
- package/dist/tests/unit/logger.test.js.map +1 -0
- package/dist/tests/unit/mcp-internal-auth.test.d.ts +2 -0
- package/dist/tests/unit/mcp-internal-auth.test.d.ts.map +1 -0
- package/dist/tests/unit/mcp-internal-auth.test.js +445 -0
- package/dist/tests/unit/mcp-internal-auth.test.js.map +1 -0
- package/dist/tests/unit/metrics.test.js +102 -0
- package/dist/tests/unit/metrics.test.js.map +1 -1
- package/dist/tests/unit/model-pricing.test.d.ts +2 -0
- package/dist/tests/unit/model-pricing.test.d.ts.map +1 -0
- package/dist/tests/unit/model-pricing.test.js +87 -0
- package/dist/tests/unit/model-pricing.test.js.map +1 -0
- package/dist/tests/unit/oauth-stores.test.d.ts +2 -0
- package/dist/tests/unit/oauth-stores.test.d.ts.map +1 -0
- package/dist/tests/unit/oauth-stores.test.js +260 -0
- package/dist/tests/unit/oauth-stores.test.js.map +1 -0
- package/dist/tests/unit/policy-engine.test.js +466 -0
- package/dist/tests/unit/policy-engine.test.js.map +1 -1
- package/dist/tests/unit/provider-interceptor.test.d.ts +2 -0
- package/dist/tests/unit/provider-interceptor.test.d.ts.map +1 -0
- package/dist/tests/unit/provider-interceptor.test.js +472 -0
- package/dist/tests/unit/provider-interceptor.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-branches.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-branches.test.js +2165 -0
- package/dist/tests/unit/saas-routes-branches.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-crud.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-crud.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-crud.test.js +332 -0
- package/dist/tests/unit/saas-routes-crud.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-data.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-data.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-data.test.js +405 -0
- package/dist/tests/unit/saas-routes-data.test.js.map +1 -0
- package/dist/tests/unit/saas-routes.test.js +3 -3
- package/dist/tests/unit/saas-routes.test.js.map +1 -1
- package/dist/tests/unit/shell-executor.test.d.ts +2 -0
- package/dist/tests/unit/shell-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/shell-executor.test.js +145 -0
- package/dist/tests/unit/shell-executor.test.js.map +1 -0
- package/dist/tests/unit/sql-executor.test.d.ts +2 -0
- package/dist/tests/unit/sql-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/sql-executor.test.js +177 -0
- package/dist/tests/unit/sql-executor.test.js.map +1 -0
- package/dist/tests/unit/stream-proxy.test.d.ts +2 -0
- package/dist/tests/unit/stream-proxy.test.d.ts.map +1 -0
- package/dist/tests/unit/stream-proxy.test.js +147 -0
- package/dist/tests/unit/stream-proxy.test.js.map +1 -0
- package/dist/tests/unit/tool-definitions.test.d.ts +2 -0
- package/dist/tests/unit/tool-definitions.test.d.ts.map +1 -0
- package/dist/tests/unit/tool-definitions.test.js +184 -0
- package/dist/tests/unit/tool-definitions.test.js.map +1 -0
- package/dist/tests/unit/usage-extractor.test.js +140 -0
- package/dist/tests/unit/usage-extractor.test.js.map +1 -1
- package/dist/tests/unit/webhook-handler.test.d.ts +2 -0
- package/dist/tests/unit/webhook-handler.test.d.ts.map +1 -0
- package/dist/tests/unit/webhook-handler.test.js +453 -0
- package/dist/tests/unit/webhook-handler.test.js.map +1 -0
- package/dist/tests/unit/webhook-routes.test.d.ts +2 -0
- package/dist/tests/unit/webhook-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/webhook-routes.test.js +69 -0
- package/dist/tests/unit/webhook-routes.test.js.map +1 -0
- package/dist/tests/unit/websocket-executor.test.d.ts +2 -0
- package/dist/tests/unit/websocket-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/websocket-executor.test.js +121 -0
- package/dist/tests/unit/websocket-executor.test.js.map +1 -0
- package/package.json +8 -2
- package/policy-packs/demo_fail.yaml +41 -0
- package/policy-packs/full_tools.yaml +136 -0
- package/src/admin/index.ts +1 -0
- package/src/admin/routes.ts +509 -0
- package/src/admin/templates.ts +572 -0
- package/src/anomaly/detector.ts +730 -0
- package/src/anomaly/index.ts +1 -0
- package/src/approval/manager.ts +569 -0
- package/src/approval/webhook.ts +133 -0
- package/src/audit/logger.ts +490 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/password.ts +21 -0
- package/src/auth/pkce.ts +22 -0
- package/src/auth/providers.ts +208 -0
- package/src/auth/routes.ts +561 -0
- package/src/auth/session.ts +84 -0
- package/src/billing/index.ts +6 -0
- package/src/billing/plan-enforcer.ts +135 -0
- package/src/billing/routes.ts +229 -0
- package/src/billing/stripe-client.ts +58 -0
- package/src/billing/webhook-handler.ts +182 -0
- package/src/billing/webhook-routes.ts +28 -0
- package/src/budget/manager.ts +679 -0
- package/src/budget/model-pricing.ts +119 -0
- package/src/budget/usage-extractor.ts +214 -0
- package/src/cli.ts +91 -0
- package/src/config/defaults.ts +261 -0
- package/src/config/validate.ts +88 -0
- package/src/dlp/composite-scanner.ts +213 -0
- package/src/dlp/index.ts +9 -0
- package/src/dlp/interfaces.ts +34 -0
- package/src/dlp/patterns.ts +30 -0
- package/src/dlp/prompt-injection-backend.ts +181 -0
- package/src/dlp/prompt-injection-patterns.ts +302 -0
- package/src/dlp/regex-backend.ts +181 -0
- package/src/dlp/scanner.ts +502 -0
- package/src/dlp/text-normalizer.ts +225 -0
- package/src/dlp/tool-patterns.ts +35 -0
- package/src/dlp/trufflehog-backend.ts +190 -0
- package/src/executor/filesystem-executor.ts +196 -0
- package/src/executor/http-executor.ts +349 -0
- package/src/executor/index.ts +9 -0
- package/src/executor/interfaces.ts +11 -0
- package/src/executor/noop-executor.ts +23 -0
- package/src/executor/registry.ts +64 -0
- package/src/executor/shell-executor.ts +148 -0
- package/src/executor/slack-executor.ts +176 -0
- package/src/executor/sql-executor.ts +146 -0
- package/src/executor/websocket-executor.ts +211 -0
- package/src/index.ts +24 -0
- package/src/interceptor/index.ts +1 -0
- package/src/interceptor/provider-interceptor.ts +315 -0
- package/src/mcp/auth-verifier.ts +152 -0
- package/src/mcp/bridge.ts +703 -0
- package/src/mcp/http-transport.ts +698 -0
- package/src/mcp/index.ts +9 -0
- package/src/mcp/internal-auth.ts +14 -0
- package/src/mcp/oauth-pages.ts +139 -0
- package/src/mcp/oauth-postgres-stores.ts +278 -0
- package/src/mcp/oauth-provider.ts +536 -0
- package/src/mcp/oauth-stores.ts +202 -0
- package/src/mcp/server.ts +55 -0
- package/src/mcp/tool-definitions.ts +562 -0
- package/src/metrics/collector.ts +357 -0
- package/src/metrics/index.ts +1 -0
- package/src/middleware/auth.ts +814 -0
- package/src/middleware/session.ts +85 -0
- package/src/middleware/validate.ts +130 -0
- package/src/policy/engine.ts +815 -0
- package/src/policy/index.ts +2 -0
- package/src/policy/opa-engine.ts +829 -0
- package/src/proxy/forward-proxy.ts +649 -0
- package/src/proxy/index.ts +1 -0
- package/src/ratelimit/limiter.ts +196 -0
- package/src/replay/engine.ts +142 -0
- package/src/replay/index.ts +1 -0
- package/src/saas/index.ts +1 -0
- package/src/saas/routes.ts +2178 -0
- package/src/server/app.ts +985 -0
- package/src/server/errors.ts +49 -0
- package/src/server/gateway.ts +1130 -0
- package/src/server/index.ts +307 -0
- package/src/server/logger.ts +255 -0
- package/src/server/stream-proxy.ts +202 -0
- package/src/storage/file-persistence.ts +315 -0
- package/src/storage/index.ts +4 -0
- package/src/storage/interfaces.ts +287 -0
- package/src/storage/memory.ts +686 -0
- package/src/storage/postgres.ts +1831 -0
- package/src/storage/redis.ts +835 -0
- package/src/tracing/index.ts +1 -0
- package/src/tracing/provider.ts +100 -0
- package/src/trust/calculator.ts +141 -0
- package/src/trust/index.ts +7 -0
- package/src/types/budget.ts +36 -0
- package/src/types/config.ts +278 -0
- package/src/types/events.ts +41 -0
- package/src/types/express.d.ts +14 -0
- package/src/types/index.ts +7 -0
- package/src/types/policy.ts +83 -0
- package/src/types/stripe-config.ts +11 -0
- package/src/types/subscription.ts +59 -0
- package/src/types/tool-call.ts +47 -0
- package/src/types/tool-result.ts +82 -0
- package/src/types/user.ts +125 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP OAuth 2.0 Server Provider.
|
|
3
|
+
*
|
|
4
|
+
* Implements OAuthServerProvider from the MCP SDK. Handles:
|
|
5
|
+
* - Authorization (consent page with workspace picker)
|
|
6
|
+
* - Auth code exchange (code → access + refresh tokens)
|
|
7
|
+
* - Refresh token rotation
|
|
8
|
+
* - Access token verification (returns AuthInfo with Palaryn context)
|
|
9
|
+
* - Token revocation
|
|
10
|
+
*
|
|
11
|
+
* Reuses existing Palaryn session (pn_session cookie) for user identity.
|
|
12
|
+
*/
|
|
13
|
+
import { Response } from 'express';
|
|
14
|
+
import { randomBytes } from 'crypto';
|
|
15
|
+
import { OAuthServerProvider, AuthorizationParams } from '@modelcontextprotocol/sdk/server/auth/provider.js';
|
|
16
|
+
import { OAuthRegisteredClientsStore } from '@modelcontextprotocol/sdk/server/auth/clients.js';
|
|
17
|
+
import {
|
|
18
|
+
OAuthClientInformationFull,
|
|
19
|
+
OAuthTokens,
|
|
20
|
+
OAuthTokenRevocationRequest,
|
|
21
|
+
} from '@modelcontextprotocol/sdk/shared/auth.js';
|
|
22
|
+
import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
|
|
23
|
+
import {
|
|
24
|
+
UserStore,
|
|
25
|
+
WorkspaceStore,
|
|
26
|
+
WorkspaceMemberStore,
|
|
27
|
+
SessionStore,
|
|
28
|
+
} from '../storage/interfaces';
|
|
29
|
+
import { AuthCodeStore, StoredToken } from './oauth-stores';
|
|
30
|
+
import { renderConsentPage, renderErrorPage } from './oauth-pages';
|
|
31
|
+
import { SESSION_COOKIE_NAME } from '../auth/session';
|
|
32
|
+
import { resolvePermissions } from '../middleware/auth';
|
|
33
|
+
import { RBACConfig } from '../types/config';
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Constants
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/** Maximum lifetime for authorization codes (in milliseconds). Default: 60 seconds. */
|
|
40
|
+
export const AUTH_CODE_LIFETIME_MS = 60 * 1000;
|
|
41
|
+
|
|
42
|
+
/** Maximum lifetime for CSRF tokens (in milliseconds). Default: 10 minutes. */
|
|
43
|
+
const CSRF_TOKEN_TTL_MS = 10 * 60 * 1000;
|
|
44
|
+
|
|
45
|
+
/** Maximum number of CSRF tokens stored concurrently. */
|
|
46
|
+
const CSRF_TOKEN_MAX_SIZE = 10_000;
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Types
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
interface CsrfEntry {
|
|
53
|
+
createdAt: number;
|
|
54
|
+
expiresAt: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Structural interface for token stores (supports both in-memory and Postgres-backed) */
|
|
58
|
+
export interface OAuthTokenStoreInterface {
|
|
59
|
+
saveAccessToken(entry: StoredToken): void;
|
|
60
|
+
getAccessToken(token: string): StoredToken | undefined;
|
|
61
|
+
revokeAccessToken(token: string): void;
|
|
62
|
+
saveRefreshToken(entry: StoredToken): void;
|
|
63
|
+
getRefreshToken(token: string): StoredToken | undefined;
|
|
64
|
+
revokeRefreshToken(token: string): void;
|
|
65
|
+
readonly accessTtlSeconds: number;
|
|
66
|
+
readonly refreshTtlSeconds: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Structural interface for client stores (supports both in-memory and Postgres-backed) */
|
|
70
|
+
export interface OAuthClientsStoreInterface extends OAuthRegisteredClientsStore {
|
|
71
|
+
getClient(clientId: string): OAuthClientInformationFull | undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface PalarynOAuthProviderDeps {
|
|
75
|
+
clientsStore: OAuthClientsStoreInterface;
|
|
76
|
+
authCodeStore: AuthCodeStore;
|
|
77
|
+
tokenStore: OAuthTokenStoreInterface;
|
|
78
|
+
userStore: UserStore;
|
|
79
|
+
workspaceStore: WorkspaceStore;
|
|
80
|
+
workspaceMemberStore: WorkspaceMemberStore;
|
|
81
|
+
sessionStore: SessionStore;
|
|
82
|
+
rbacConfig?: RBACConfig;
|
|
83
|
+
/** URL to redirect to for login (default: /auth/login) */
|
|
84
|
+
loginUrl?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Provider implementation
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
export class PalarynOAuthProvider implements OAuthServerProvider {
|
|
92
|
+
private _clientsStore: OAuthClientsStoreInterface;
|
|
93
|
+
private authCodes: AuthCodeStore;
|
|
94
|
+
private tokens: OAuthTokenStoreInterface;
|
|
95
|
+
private users: UserStore;
|
|
96
|
+
private workspaces: WorkspaceStore;
|
|
97
|
+
private members: WorkspaceMemberStore;
|
|
98
|
+
private sessions: SessionStore;
|
|
99
|
+
private rbacConfig?: RBACConfig;
|
|
100
|
+
private loginUrl: string;
|
|
101
|
+
|
|
102
|
+
/** CSRF tokens: token → { createdAt, expiresAt } */
|
|
103
|
+
private csrfTokens = new Map<string, CsrfEntry>();
|
|
104
|
+
private csrfCleanupInterval: ReturnType<typeof setInterval>;
|
|
105
|
+
|
|
106
|
+
constructor(deps: PalarynOAuthProviderDeps) {
|
|
107
|
+
this._clientsStore = deps.clientsStore;
|
|
108
|
+
this.authCodes = deps.authCodeStore;
|
|
109
|
+
this.tokens = deps.tokenStore;
|
|
110
|
+
this.users = deps.userStore;
|
|
111
|
+
this.workspaces = deps.workspaceStore;
|
|
112
|
+
this.members = deps.workspaceMemberStore;
|
|
113
|
+
this.sessions = deps.sessionStore;
|
|
114
|
+
this.rbacConfig = deps.rbacConfig;
|
|
115
|
+
this.loginUrl = deps.loginUrl || '/login';
|
|
116
|
+
|
|
117
|
+
// Periodically clean up expired CSRF tokens
|
|
118
|
+
this.csrfCleanupInterval = setInterval(() => this.cleanupCsrfTokens(), 60_000);
|
|
119
|
+
this.csrfCleanupInterval.unref();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Stop the periodic CSRF cleanup interval. */
|
|
123
|
+
close(): void {
|
|
124
|
+
clearInterval(this.csrfCleanupInterval);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
get clientsStore(): OAuthRegisteredClientsStore {
|
|
128
|
+
return this._clientsStore;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// -----------------------------------------------------------------------
|
|
132
|
+
// authorize — show consent page or redirect to login
|
|
133
|
+
// -----------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
async authorize(
|
|
136
|
+
client: OAuthClientInformationFull,
|
|
137
|
+
params: AuthorizationParams,
|
|
138
|
+
res: Response,
|
|
139
|
+
): Promise<void> {
|
|
140
|
+
// Validate redirect URI
|
|
141
|
+
const uriCheck = PalarynOAuthProvider.validateRedirectUri(params.redirectUri);
|
|
142
|
+
if (!uriCheck.valid) {
|
|
143
|
+
res.status(400).send(renderErrorPage('Invalid redirect URI', uriCheck.reason || 'The redirect URI is not allowed.'));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Verify redirect_uri matches one of the client's registered redirect URIs
|
|
148
|
+
const registeredUris = (client.redirect_uris || []).map((u: string | URL) => u.toString());
|
|
149
|
+
if (registeredUris.length > 0 && !registeredUris.includes(params.redirectUri)) {
|
|
150
|
+
res.status(400).send(renderErrorPage('Redirect URI mismatch', 'The redirect URI does not match any registered URIs for this client.'));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Read the session cookie from the request (available via res.req)
|
|
155
|
+
const req = res.req;
|
|
156
|
+
const sessionId = (req as any).cookies?.[SESSION_COOKIE_NAME];
|
|
157
|
+
|
|
158
|
+
if (!sessionId) {
|
|
159
|
+
// Not logged in → redirect to login with return_to
|
|
160
|
+
const returnTo = this.buildAuthorizeReturnUrl(client, params);
|
|
161
|
+
res.redirect(`${this.loginUrl}?return_to=${encodeURIComponent(returnTo)}`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const session = this.sessions.getById(sessionId);
|
|
166
|
+
if (!session) {
|
|
167
|
+
const returnTo = this.buildAuthorizeReturnUrl(client, params);
|
|
168
|
+
res.redirect(`${this.loginUrl}?return_to=${encodeURIComponent(returnTo)}`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const user = this.users.getById(session.user_id);
|
|
173
|
+
if (!user || user.status !== 'active') {
|
|
174
|
+
res.status(403).send(renderErrorPage('Account unavailable', 'Your account is not active.'));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Get workspaces the user belongs to
|
|
179
|
+
const memberships = this.members.getByUser(user.id);
|
|
180
|
+
const userWorkspaces = memberships
|
|
181
|
+
.map((m) => {
|
|
182
|
+
const ws = this.workspaces.getById(m.workspace_id);
|
|
183
|
+
return ws ? { id: ws.id, name: ws.name, slug: ws.slug } : null;
|
|
184
|
+
})
|
|
185
|
+
.filter(Boolean) as { id: string; name: string; slug: string }[];
|
|
186
|
+
|
|
187
|
+
if (userWorkspaces.length === 0) {
|
|
188
|
+
res.status(403).send(renderErrorPage('No workspaces', 'You need at least one workspace to authorize MCP access.'));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Generate CSRF token (with cleanup of expired/overflow)
|
|
193
|
+
this.cleanupCsrfTokens();
|
|
194
|
+
const now = Date.now();
|
|
195
|
+
const csrfToken = randomBytes(32).toString('hex');
|
|
196
|
+
this.csrfTokens.set(csrfToken, { createdAt: now, expiresAt: now + CSRF_TOKEN_TTL_MS });
|
|
197
|
+
|
|
198
|
+
// Render consent page
|
|
199
|
+
const html = renderConsentPage({
|
|
200
|
+
clientName: client.client_name || client.client_id,
|
|
201
|
+
scopes: params.scopes || [],
|
|
202
|
+
workspaces: userWorkspaces,
|
|
203
|
+
clientId: client.client_id,
|
|
204
|
+
redirectUri: params.redirectUri,
|
|
205
|
+
state: params.state,
|
|
206
|
+
codeChallenge: params.codeChallenge,
|
|
207
|
+
csrfToken,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Override Helmet's CSP to allow form-action redirect to Claude Code's localhost callback
|
|
211
|
+
res.setHeader('Content-Security-Policy',
|
|
212
|
+
"default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; form-action 'self' http://localhost:* http://127.0.0.1:*");
|
|
213
|
+
res.status(200).type('html').send(html);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// -----------------------------------------------------------------------
|
|
217
|
+
// handleConsentDecision — called from POST /authorize/decision
|
|
218
|
+
// -----------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
async handleConsentDecision(
|
|
221
|
+
body: Record<string, string>,
|
|
222
|
+
sessionUserId: string,
|
|
223
|
+
): Promise<{ redirectUrl: string } | { error: string; status: number }> {
|
|
224
|
+
const {
|
|
225
|
+
client_id,
|
|
226
|
+
redirect_uri,
|
|
227
|
+
state,
|
|
228
|
+
code_challenge,
|
|
229
|
+
scopes,
|
|
230
|
+
csrf_token,
|
|
231
|
+
workspace_id,
|
|
232
|
+
decision,
|
|
233
|
+
} = body;
|
|
234
|
+
|
|
235
|
+
// Validate CSRF
|
|
236
|
+
const csrfEntry = this.csrfTokens.get(csrf_token);
|
|
237
|
+
if (!csrfEntry || Date.now() > csrfEntry.expiresAt) {
|
|
238
|
+
return { error: 'Invalid or expired CSRF token', status: 403 };
|
|
239
|
+
}
|
|
240
|
+
this.csrfTokens.delete(csrf_token);
|
|
241
|
+
|
|
242
|
+
// Validate redirect URI
|
|
243
|
+
const uriCheck = PalarynOAuthProvider.validateRedirectUri(redirect_uri);
|
|
244
|
+
if (!uriCheck.valid) {
|
|
245
|
+
return { error: uriCheck.reason || 'Invalid redirect URI', status: 400 };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Validate client
|
|
249
|
+
const client = this._clientsStore.getClient(client_id);
|
|
250
|
+
if (!client) {
|
|
251
|
+
return { error: 'Unknown client', status: 400 };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Verify redirect_uri matches registered URIs
|
|
255
|
+
const registeredUris = (client.redirect_uris || []).map((u: string | URL) => u.toString());
|
|
256
|
+
if (registeredUris.length > 0 && !registeredUris.includes(redirect_uri)) {
|
|
257
|
+
return { error: 'Redirect URI does not match registered URIs', status: 400 };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const redirectUrl = new URL(redirect_uri);
|
|
261
|
+
|
|
262
|
+
// Denied
|
|
263
|
+
if (decision !== 'approve') {
|
|
264
|
+
redirectUrl.searchParams.set('error', 'access_denied');
|
|
265
|
+
redirectUrl.searchParams.set('error_description', 'User denied the request');
|
|
266
|
+
if (state) redirectUrl.searchParams.set('state', state);
|
|
267
|
+
return { redirectUrl: redirectUrl.toString() };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Verify workspace membership
|
|
271
|
+
const membership = this.members.getByUser(sessionUserId)
|
|
272
|
+
.find((m) => m.workspace_id === workspace_id);
|
|
273
|
+
if (!membership) {
|
|
274
|
+
return { error: 'Invalid workspace selection', status: 403 };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Generate authorization code
|
|
278
|
+
const code = randomBytes(32).toString('hex');
|
|
279
|
+
this.authCodes.save({
|
|
280
|
+
code,
|
|
281
|
+
clientId: client_id,
|
|
282
|
+
codeChallenge: code_challenge,
|
|
283
|
+
redirectUri: redirect_uri,
|
|
284
|
+
scopes: scopes ? scopes.split(' ') : [],
|
|
285
|
+
userId: sessionUserId,
|
|
286
|
+
workspaceId: workspace_id,
|
|
287
|
+
state,
|
|
288
|
+
expiresAt: Date.now() + AUTH_CODE_LIFETIME_MS,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
redirectUrl.searchParams.set('code', code);
|
|
292
|
+
if (state) redirectUrl.searchParams.set('state', state);
|
|
293
|
+
return { redirectUrl: redirectUrl.toString() };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// -----------------------------------------------------------------------
|
|
297
|
+
// challengeForAuthorizationCode
|
|
298
|
+
// -----------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
async challengeForAuthorizationCode(
|
|
301
|
+
_client: OAuthClientInformationFull,
|
|
302
|
+
authorizationCode: string,
|
|
303
|
+
): Promise<string> {
|
|
304
|
+
const entry = this.authCodes.get(authorizationCode);
|
|
305
|
+
if (!entry) {
|
|
306
|
+
throw new Error('Authorization code not found or expired');
|
|
307
|
+
}
|
|
308
|
+
return entry.codeChallenge;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// -----------------------------------------------------------------------
|
|
312
|
+
// exchangeAuthorizationCode
|
|
313
|
+
// -----------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
async exchangeAuthorizationCode(
|
|
316
|
+
client: OAuthClientInformationFull,
|
|
317
|
+
authorizationCode: string,
|
|
318
|
+
_codeVerifier?: string,
|
|
319
|
+
_redirectUri?: string,
|
|
320
|
+
_resource?: URL,
|
|
321
|
+
): Promise<OAuthTokens> {
|
|
322
|
+
const entry = this.authCodes.get(authorizationCode);
|
|
323
|
+
if (!entry) {
|
|
324
|
+
throw new Error('Authorization code not found or expired');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (entry.clientId !== client.client_id) {
|
|
328
|
+
throw new Error('Client ID mismatch');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Consume (delete) the auth code FIRST to guarantee single-use.
|
|
332
|
+
// If token issuance fails after this, the client must request a new code.
|
|
333
|
+
this.authCodes.consume(authorizationCode);
|
|
334
|
+
|
|
335
|
+
return this.issueTokens(client.client_id, entry.userId, entry.workspaceId, entry.scopes);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// -----------------------------------------------------------------------
|
|
339
|
+
// exchangeRefreshToken
|
|
340
|
+
// -----------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
async exchangeRefreshToken(
|
|
343
|
+
client: OAuthClientInformationFull,
|
|
344
|
+
refreshToken: string,
|
|
345
|
+
scopes?: string[],
|
|
346
|
+
_resource?: URL,
|
|
347
|
+
): Promise<OAuthTokens> {
|
|
348
|
+
const entry = this.tokens.getRefreshToken(refreshToken);
|
|
349
|
+
if (!entry) {
|
|
350
|
+
throw new Error('Invalid refresh token');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (entry.clientId !== client.client_id) {
|
|
354
|
+
throw new Error('Client ID mismatch');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Revoke old refresh token (rotation)
|
|
358
|
+
this.tokens.revokeRefreshToken(refreshToken);
|
|
359
|
+
|
|
360
|
+
const effectiveScopes = scopes && scopes.length > 0 ? scopes : entry.scopes;
|
|
361
|
+
return this.issueTokens(client.client_id, entry.userId, entry.workspaceId, effectiveScopes);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// -----------------------------------------------------------------------
|
|
365
|
+
// verifyAccessToken
|
|
366
|
+
// -----------------------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
async verifyAccessToken(token: string): Promise<AuthInfo> {
|
|
369
|
+
const entry = this.tokens.getAccessToken(token);
|
|
370
|
+
if (!entry) {
|
|
371
|
+
throw new Error('Invalid or expired access token');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Resolve user's workspace role → RBAC permissions
|
|
375
|
+
const memberships = this.members.getByUser(entry.userId);
|
|
376
|
+
const membership = memberships.find((m) => m.workspace_id === entry.workspaceId);
|
|
377
|
+
|
|
378
|
+
let roles: string[] = [];
|
|
379
|
+
if (membership) {
|
|
380
|
+
if (membership.role === 'owner' || membership.role === 'admin') {
|
|
381
|
+
roles = ['admin'];
|
|
382
|
+
} else if (membership.role === 'member') {
|
|
383
|
+
roles = ['operator'];
|
|
384
|
+
} else {
|
|
385
|
+
roles = ['readonly'];
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const permissions = resolvePermissions(roles, this.rbacConfig);
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
token,
|
|
393
|
+
clientId: entry.clientId,
|
|
394
|
+
scopes: entry.scopes,
|
|
395
|
+
expiresAt: entry.expiresAt,
|
|
396
|
+
extra: {
|
|
397
|
+
workspace_id: entry.workspaceId,
|
|
398
|
+
actor_id: `user:${entry.userId}`,
|
|
399
|
+
user_id: entry.userId,
|
|
400
|
+
roles,
|
|
401
|
+
permissions,
|
|
402
|
+
auth_method: 'oauth',
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// -----------------------------------------------------------------------
|
|
408
|
+
// revokeToken
|
|
409
|
+
// -----------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
async revokeToken(
|
|
412
|
+
_client: OAuthClientInformationFull,
|
|
413
|
+
request: OAuthTokenRevocationRequest,
|
|
414
|
+
): Promise<void> {
|
|
415
|
+
// Try both token types
|
|
416
|
+
this.tokens.revokeAccessToken(request.token);
|
|
417
|
+
this.tokens.revokeRefreshToken(request.token);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// -----------------------------------------------------------------------
|
|
421
|
+
// Helpers
|
|
422
|
+
// -----------------------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
private issueTokens(
|
|
425
|
+
clientId: string,
|
|
426
|
+
userId: string,
|
|
427
|
+
workspaceId: string,
|
|
428
|
+
scopes: string[],
|
|
429
|
+
): OAuthTokens {
|
|
430
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
431
|
+
const accessToken = randomBytes(32).toString('hex');
|
|
432
|
+
const refreshToken = randomBytes(32).toString('hex');
|
|
433
|
+
|
|
434
|
+
this.tokens.saveAccessToken({
|
|
435
|
+
token: accessToken,
|
|
436
|
+
clientId,
|
|
437
|
+
userId,
|
|
438
|
+
workspaceId,
|
|
439
|
+
scopes,
|
|
440
|
+
expiresAt: nowSec + this.tokens.accessTtlSeconds,
|
|
441
|
+
createdAt: nowSec,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
this.tokens.saveRefreshToken({
|
|
445
|
+
token: refreshToken,
|
|
446
|
+
clientId,
|
|
447
|
+
userId,
|
|
448
|
+
workspaceId,
|
|
449
|
+
scopes,
|
|
450
|
+
expiresAt: nowSec + this.tokens.refreshTtlSeconds,
|
|
451
|
+
createdAt: nowSec,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
access_token: accessToken,
|
|
456
|
+
token_type: 'Bearer',
|
|
457
|
+
expires_in: this.tokens.accessTtlSeconds,
|
|
458
|
+
refresh_token: refreshToken,
|
|
459
|
+
scope: scopes.join(' '),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Remove expired CSRF tokens and evict oldest when map exceeds max size.
|
|
465
|
+
*/
|
|
466
|
+
private cleanupCsrfTokens(): void {
|
|
467
|
+
const now = Date.now();
|
|
468
|
+
|
|
469
|
+
// Remove expired tokens
|
|
470
|
+
for (const [token, entry] of this.csrfTokens) {
|
|
471
|
+
if (now > entry.expiresAt) {
|
|
472
|
+
this.csrfTokens.delete(token);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Evict oldest if still over max size
|
|
477
|
+
if (this.csrfTokens.size >= CSRF_TOKEN_MAX_SIZE) {
|
|
478
|
+
const sorted = [...this.csrfTokens.entries()]
|
|
479
|
+
.sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
480
|
+
const toEvict = sorted.length - CSRF_TOKEN_MAX_SIZE + 1; // make room for the new one
|
|
481
|
+
for (let i = 0; i < toEvict; i++) {
|
|
482
|
+
this.csrfTokens.delete(sorted[i][0]);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Validate that a redirect URI is safe for dynamically registered clients.
|
|
489
|
+
* Only allows localhost-based redirect URIs (127.0.0.1, ::1, localhost).
|
|
490
|
+
* Blocks javascript:, data:, and other dangerous schemes.
|
|
491
|
+
*/
|
|
492
|
+
static validateRedirectUri(uri: string): { valid: boolean; reason?: string } {
|
|
493
|
+
let parsed: URL;
|
|
494
|
+
try {
|
|
495
|
+
parsed = new URL(uri);
|
|
496
|
+
} catch {
|
|
497
|
+
return { valid: false, reason: 'Invalid URL format' };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Block dangerous schemes
|
|
501
|
+
const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'blob:'];
|
|
502
|
+
if (dangerousSchemes.includes(parsed.protocol)) {
|
|
503
|
+
return { valid: false, reason: `Dangerous scheme: ${parsed.protocol}` };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Only allow http/https
|
|
507
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
508
|
+
return { valid: false, reason: `Unsupported scheme: ${parsed.protocol}` };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// For dynamically registered MCP clients, only allow localhost
|
|
512
|
+
const hostname = parsed.hostname;
|
|
513
|
+
const localhostHosts = ['localhost', '127.0.0.1', '::1', '[::1]'];
|
|
514
|
+
if (!localhostHosts.includes(hostname)) {
|
|
515
|
+
return { valid: false, reason: 'Only localhost redirect URIs are allowed for dynamic clients' };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return { valid: true };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private buildAuthorizeReturnUrl(
|
|
522
|
+
client: OAuthClientInformationFull,
|
|
523
|
+
params: AuthorizationParams,
|
|
524
|
+
): string {
|
|
525
|
+
const url = new URL('/authorize', 'http://placeholder');
|
|
526
|
+
url.searchParams.set('client_id', client.client_id);
|
|
527
|
+
url.searchParams.set('redirect_uri', params.redirectUri);
|
|
528
|
+
url.searchParams.set('code_challenge', params.codeChallenge);
|
|
529
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
530
|
+
if (params.state) url.searchParams.set('state', params.state);
|
|
531
|
+
if (params.scopes?.length) url.searchParams.set('scope', params.scopes.join(' '));
|
|
532
|
+
url.searchParams.set('response_type', 'code');
|
|
533
|
+
// Return only path + query (no host)
|
|
534
|
+
return url.pathname + url.search;
|
|
535
|
+
}
|
|
536
|
+
}
|