@mcp-z/client 1.2.0 → 2.0.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/dist/cjs/auth/capability-discovery.js +5 -1
- package/dist/cjs/auth/capability-discovery.js.map +1 -1
- package/dist/cjs/auth/interactive-oauth-flow.d.cts +9 -2
- package/dist/cjs/auth/interactive-oauth-flow.d.ts +9 -2
- package/dist/cjs/auth/interactive-oauth-flow.js +29 -11
- package/dist/cjs/auth/interactive-oauth-flow.js.map +1 -1
- package/dist/cjs/auth/oauth-callback-listener.js +4 -0
- package/dist/cjs/auth/oauth-callback-listener.js.map +1 -1
- package/dist/cjs/auth/types.d.cts +18 -4
- package/dist/cjs/auth/types.d.ts +18 -4
- package/dist/cjs/auth/types.js.map +1 -1
- package/dist/cjs/dcr/dcr-authenticator.d.cts +6 -1
- package/dist/cjs/dcr/dcr-authenticator.d.ts +6 -1
- package/dist/cjs/dcr/dcr-authenticator.js +423 -40
- package/dist/cjs/dcr/dcr-authenticator.js.map +1 -1
- package/dist/cjs/dcr/dynamic-client-registrar.d.cts +2 -2
- package/dist/cjs/dcr/dynamic-client-registrar.d.ts +2 -2
- package/dist/cjs/dcr/dynamic-client-registrar.js +10 -11
- package/dist/cjs/dcr/dynamic-client-registrar.js.map +1 -1
- package/dist/cjs/lib/url-utils.js +2 -2
- package/dist/cjs/lib/url-utils.js.map +1 -1
- package/dist/esm/auth/capability-discovery.js +5 -1
- package/dist/esm/auth/capability-discovery.js.map +1 -1
- package/dist/esm/auth/interactive-oauth-flow.d.ts +9 -2
- package/dist/esm/auth/interactive-oauth-flow.js +28 -10
- package/dist/esm/auth/interactive-oauth-flow.js.map +1 -1
- package/dist/esm/auth/oauth-callback-listener.js +4 -0
- package/dist/esm/auth/oauth-callback-listener.js.map +1 -1
- package/dist/esm/auth/types.d.ts +18 -4
- package/dist/esm/auth/types.js.map +1 -1
- package/dist/esm/dcr/dcr-authenticator.d.ts +6 -1
- package/dist/esm/dcr/dcr-authenticator.js +79 -35
- package/dist/esm/dcr/dcr-authenticator.js.map +1 -1
- package/dist/esm/dcr/dynamic-client-registrar.d.ts +2 -2
- package/dist/esm/dcr/dynamic-client-registrar.js +7 -6
- package/dist/esm/dcr/dynamic-client-registrar.js.map +1 -1
- package/dist/esm/lib/url-utils.js +2 -2
- package/dist/esm/lib/url-utils.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/dcr/dcr-authenticator.ts"],"sourcesContent":["/**\n * DCR Authenticator\n * Consolidates DCR and OAuth flow logic for MCP HTTP servers\n */\n\nimport path from 'node:path';\nimport * as fs from 'fs';\nimport Keyv from 'keyv';\nimport { KeyvFile } from 'keyv-file';\nimport { isLoopbackUrl } from '../auth/discovery-fetch.ts';\nimport { InteractiveOAuthFlow } from '../auth/interactive-oauth-flow.ts';\nimport type { AuthCapabilities, TokenSet } from '../auth/types.ts';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport { DynamicClientRegistrar } from './dynamic-client-registrar.ts';\n\n/**\n * DcrAuthenticator configuration options\n */\nexport interface DcrAuthenticatorOptions {\n /** Custom Keyv store (for testing) - if not provided, uses default ~/.mcpeasy/tokens.json */\n tokenStore?: Keyv;\n /** Headless mode (don't open browser) */\n headless?: boolean;\n /** Required redirect URI for OAuth callback */\n redirectUri: string;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: Logger;\n}\n\n/**\n * Buffer time before token expiry to trigger proactive refresh (5 minutes)\n */\nconst REFRESH_BUFFER_MS = 5 * 60 * 1000;\n\n/**\n * DcrAuthenticator manages authentication for MCP HTTP servers\n * Handles DCR registration, OAuth flows, and token management\n */\nexport class DcrAuthenticator {\n private tokenStore: Keyv;\n private dcrClient: DynamicClientRegistrar;\n private oauthFlow: InteractiveOAuthFlow;\n private headless: boolean;\n private redirectUri: string;\n private logger: Logger;\n\n constructor(options: DcrAuthenticatorOptions) {\n if (options.tokenStore) {\n this.tokenStore = options.tokenStore;\n } else {\n // Default CLI store in .mcp-z directory (per-project)\n const storePath = path.join(process.cwd(), '.mcp-z', 'tokens.json');\n\n // Ensure directory exists before creating store\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n\n this.tokenStore = new Keyv({\n store: new KeyvFile({ filename: storePath }),\n });\n }\n this.dcrClient = new DynamicClientRegistrar();\n this.oauthFlow = new InteractiveOAuthFlow();\n this.headless = options.headless || false;\n this.redirectUri = options.redirectUri;\n this.logger = options.logger ?? defaultLogger;\n }\n\n /**\n * Detect if server is self-hosted DCR (vs external OAuth provider)\n * Self-hosted servers have their own OAuth endpoints and manage token storage\n */\n private async detectSelfHostedMode(baseUrl: string): Promise<boolean> {\n try {\n // Self-hosted DCR servers typically run their own OAuth server\n // Check if this is a self-hosted instance by testing OAuth metadata\n // For now, assume self-hosted if baseUrl matches common localhost patterns\n // TODO: Implement proper self-hosted detection logic\n return baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1');\n } catch (_error) {\n return false; // Assume external mode if detection fails\n }\n }\n\n /**\n * Ensure server is authenticated, performing DCR and OAuth if needed\n * Proactively refreshes tokens if they're within 5 minutes of expiry\n *\n * @param baseUrl - Base URL of the server (e.g., https://example.com)\n * @param capabilities - Auth capabilities from .well-known endpoint\n * @returns Valid token set ready to use\n *\n * @throws Error if authentication fails\n *\n * @example\n * const authenticator = new DcrAuthenticator({ redirectUri: 'http://localhost:3000/callback' });\n * const tokens = await authenticator.ensureAuthenticated(\n * 'https://example.com',\n * capabilities\n * );\n */\n async ensureAuthenticated(baseUrl: string, capabilities: AuthCapabilities): Promise<TokenSet> {\n // Auto-detect server mode\n const isSelfHosted = await this.detectSelfHostedMode(baseUrl);\n\n if (isSelfHosted) {\n return this.ensureAuthenticatedSelfHosted(baseUrl, capabilities);\n }\n return this.ensureAuthenticatedExternal(baseUrl, capabilities);\n }\n\n /**\n * Handle authentication for self-hosted DCR servers\n * Self-hosted servers manage their own token storage via /oauth/verify\n */\n private async ensureAuthenticatedSelfHosted(baseUrl: string, capabilities: AuthCapabilities): Promise<TokenSet> {\n // Loopback trust for every discovery-derived fetch below, computed from\n // the server we're talking to, never from capabilities' endpoints (see discovery-fetch.ts).\n const allowLoopback = isLoopbackUrl(baseUrl);\n const dcrTokenKey = `dcr-tokens:${baseUrl}`;\n\n // 1. Check for existing DCR tokens (different from external tokens)\n let tokens = (await this.tokenStore.get(dcrTokenKey)) as TokenSet | undefined;\n\n if (tokens) {\n // 2. Verify token is still valid by calling /oauth/verify\n try {\n const verifyUrl = `${baseUrl}/oauth/verify`;\n const verifyResponse = await fetch(verifyUrl, {\n headers: { Authorization: `Bearer ${tokens.accessToken}`, Connection: 'close' },\n });\n\n if (verifyResponse.ok) {\n const verifyData = (await verifyResponse.json()) as { token?: string };\n if (verifyData.token === tokens.accessToken) {\n // Token is still valid with the self-hosted server\n return tokens;\n }\n }\n } catch (_error) {\n // Token verification failed - need to re-authenticate\n }\n\n // Token is expired or invalid\n await this.tokenStore.delete(dcrTokenKey);\n tokens = undefined;\n }\n\n // 3. No valid tokens - perform full DCR + OAuth flow\n if (!capabilities.registrationEndpoint || !capabilities.authorizationEndpoint || !capabilities.tokenEndpoint) {\n throw new Error('Server does not provide required OAuth endpoints');\n }\n\n this.logger.debug('🔐 No valid tokens found, starting self-hosted DCR authentication...');\n\n // Extract port from pre-resolved redirectUri\n const port = parseInt(new URL(this.redirectUri).port, 10) || (this.redirectUri.startsWith('https:') ? 443 : 80);\n\n // Register OAuth client via DCR\n this.logger.debug('📝 Registering OAuth client with self-hosted server...');\n const client = await this.dcrClient.registerClient(capabilities.registrationEndpoint, {\n redirectUri: this.redirectUri,\n allowLoopback,\n });\n\n // Perform OAuth authorization flow with PKCE (RFC 7636)\n const flowOptions: { port: number; headless: boolean; scopes?: string[]; redirectUri: string; pkce: boolean; logger: Logger; allowLoopback: boolean } = {\n port,\n headless: this.headless,\n redirectUri: this.redirectUri,\n pkce: true,\n logger: this.logger,\n allowLoopback,\n };\n if (capabilities.scopes) {\n flowOptions.scopes = capabilities.scopes;\n }\n\n tokens = await this.oauthFlow.performAuthFlow(capabilities.authorizationEndpoint, capabilities.tokenEndpoint, client.clientId, client.clientSecret, flowOptions);\n\n // For self-hosted mode, verify the token works with /oauth/verify immediately\n try {\n const verifyUrl = `${baseUrl}/oauth/verify`;\n const verifyResponse = await fetch(verifyUrl, {\n headers: { Authorization: `Bearer ${tokens.accessToken}`, Connection: 'close' },\n });\n\n if (!verifyResponse.ok) {\n throw new Error(`DCR token verification failed after authentication: ${verifyResponse.status}`);\n }\n\n const verifyData = (await verifyResponse.json()) as { token?: string };\n if (verifyData.token !== tokens.accessToken) {\n throw new Error('DCR server returned different token in verification');\n }\n\n this.logger.debug('✅ DCR token verified with self-hosted server');\n } catch (error) {\n this.logger.error('❌ DCR token verification failed:', error instanceof Error ? error.message : String(error));\n throw new Error('Self-hosted DCR authentication completed but token verification failed');\n }\n\n // Save tokens for future use\n await this.tokenStore.set(dcrTokenKey, tokens);\n this.logger.debug('✅ Self-hosted DCR authentication successful, tokens saved');\n\n return tokens;\n }\n\n /** Handles authentication for external (non-self-hosted) OAuth providers. */\n private async ensureAuthenticatedExternal(baseUrl: string, capabilities: AuthCapabilities): Promise<TokenSet> {\n // See ensureAuthenticatedSelfHosted - same loopback trust rule.\n const allowLoopback = isLoopbackUrl(baseUrl);\n const tokenKey = `tokens:${baseUrl}`;\n\n // 1. Check for existing tokens\n let tokens = (await this.tokenStore.get(tokenKey)) as TokenSet | undefined;\n\n if (tokens) {\n // 2. Proactive refresh if token expires within 5 minutes\n if (tokens.expiresAt < Date.now() + REFRESH_BUFFER_MS) {\n this.logger.debug('🔄 Refreshing access token...');\n\n try {\n tokens = await this.refreshTokens(tokens, capabilities.tokenEndpoint, allowLoopback);\n await this.tokenStore.set(tokenKey, tokens);\n this.logger.debug('✅ Token refreshed successfully');\n } catch (_error) {\n // Refresh failed - clear tokens and re-authenticate\n this.logger.warn('⚠️ Token refresh failed, re-authenticating...');\n await this.tokenStore.delete(tokenKey);\n tokens = undefined;\n }\n }\n\n if (tokens) {\n return tokens;\n }\n }\n\n // 3. No valid tokens - perform DCR + OAuth flow\n if (!capabilities.registrationEndpoint || !capabilities.authorizationEndpoint || !capabilities.tokenEndpoint) {\n throw new Error('Server does not provide required OAuth endpoints');\n }\n\n this.logger.debug('🔐 No valid tokens found, starting external OAuth authentication...');\n\n // Extract port from pre-resolved redirectUri\n const port = parseInt(new URL(this.redirectUri).port, 10) || (this.redirectUri.startsWith('https:') ? 443 : 80);\n\n // Register OAuth client via DCR\n this.logger.debug('📝 Registering OAuth client...');\n const client = await this.dcrClient.registerClient(capabilities.registrationEndpoint, {\n redirectUri: this.redirectUri,\n allowLoopback,\n });\n\n // Perform OAuth authorization flow with PKCE (RFC 7636)\n const flowOptions: { port: number; headless: boolean; scopes?: string[]; redirectUri: string; pkce: boolean; logger: Logger; allowLoopback: boolean } = {\n port,\n headless: this.headless,\n redirectUri: this.redirectUri,\n pkce: true,\n logger: this.logger,\n allowLoopback,\n };\n if (capabilities.scopes) {\n flowOptions.scopes = capabilities.scopes;\n }\n\n tokens = await this.oauthFlow.performAuthFlow(capabilities.authorizationEndpoint, capabilities.tokenEndpoint, client.clientId, client.clientSecret, flowOptions);\n\n // Save tokens for future use\n await this.tokenStore.set(tokenKey, tokens);\n this.logger.debug('✅ Authentication successful, tokens saved');\n\n return tokens;\n }\n\n /**\n * Refreshes an access token using a refresh token.\n * @param allowLoopback - Loopback trust grant computed from the server actually being talked to (SSRF mitigation, see discovery-fetch.ts). Defaults to `false`.\n */\n private async refreshTokens(tokens: TokenSet, tokenEndpoint: string | undefined, allowLoopback = false): Promise<TokenSet> {\n if (!tokenEndpoint) {\n throw new Error('Token endpoint not available for refresh');\n }\n\n if (!tokens.refreshToken) {\n throw new Error('No refresh token available');\n }\n\n if (!tokens.clientId || !tokens.clientSecret) {\n throw new Error('Client credentials not available for refresh');\n }\n\n return await this.oauthFlow.refreshTokens(tokenEndpoint, tokens.refreshToken, tokens.clientId, tokens.clientSecret, allowLoopback);\n }\n\n /**\n * Delete stored tokens for a server\n */\n async deleteTokens(baseUrl: string): Promise<void> {\n const tokenKey = `tokens:${baseUrl}`;\n await this.tokenStore.delete(tokenKey);\n this.logger.debug(`🗑️ Deleted tokens for ${baseUrl}`);\n }\n}\n"],"names":["DcrAuthenticator","REFRESH_BUFFER_MS","options","tokenStore","storePath","path","join","process","cwd","fs","mkdirSync","dirname","recursive","Keyv","store","KeyvFile","filename","dcrClient","DynamicClientRegistrar","oauthFlow","InteractiveOAuthFlow","headless","redirectUri","logger","defaultLogger","detectSelfHostedMode","baseUrl","includes","_error","ensureAuthenticated","capabilities","isSelfHosted","ensureAuthenticatedSelfHosted","ensureAuthenticatedExternal","allowLoopback","dcrTokenKey","tokens","verifyUrl","verifyResponse","verifyData","port","client","flowOptions","error","isLoopbackUrl","get","fetch","headers","Authorization","accessToken","Connection","ok","json","token","delete","undefined","registrationEndpoint","authorizationEndpoint","tokenEndpoint","Error","debug","parseInt","URL","startsWith","registerClient","pkce","scopes","performAuthFlow","clientId","clientSecret","status","message","String","set","tokenKey","expiresAt","Date","now","refreshTokens","warn","refreshToken","deleteTokens"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/dcr/dcr-authenticator.ts"],"sourcesContent":["/**\n * DCR Authenticator\n * Consolidates DCR and OAuth flow logic for MCP HTTP servers\n */\n\nimport path from 'node:path';\nimport * as fs from 'fs';\nimport Keyv from 'keyv';\nimport { KeyvFile } from 'keyv-file';\nimport { isLoopbackUrl } from '../auth/discovery-fetch.ts';\nimport { InteractiveOAuthFlow } from '../auth/interactive-oauth-flow.ts';\nimport type { AuthCapabilities, OAuthFlowOptions, TokenSet } from '../auth/types.ts';\nimport { normalizeUrl } from '../lib/url-utils.ts';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport { DynamicClientRegistrar } from './dynamic-client-registrar.ts';\n\n/**\n * DcrAuthenticator configuration options\n */\nexport interface DcrAuthenticatorOptions {\n /** Custom Keyv store (for testing) - if not provided, uses default ~/.mcpeasy/tokens.json */\n tokenStore?: Keyv;\n /** Headless mode (don't open browser) */\n headless?: boolean;\n /** Required redirect URI for OAuth callback */\n redirectUri: string;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: Logger;\n}\n\n/**\n * Buffer time before token expiry to trigger proactive refresh (5 minutes)\n */\nconst REFRESH_BUFFER_MS = 5 * 60 * 1000;\n\n/** Raised when a credential cannot be bound to, or looked up by, an issuer. */\nclass CredentialBindingError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'CredentialBindingError';\n }\n}\n\n/**\n * The issuer credentials are keyed by, so they can never be presented to a\n * different authorization server even when the resource keeps its URL (SEP-2352).\n */\nfunction requireIssuer(capabilities: AuthCapabilities): string {\n if (!capabilities.issuer) {\n throw new CredentialBindingError('Authorization server metadata has no issuer - credentials cannot be bound to an authorization server (RFC 8414 requires issuer)');\n }\n return capabilities.issuer;\n}\n\n/**\n * DcrAuthenticator manages authentication for MCP HTTP servers\n * Handles DCR registration, OAuth flows, and token management\n */\nexport class DcrAuthenticator {\n private tokenStore: Keyv;\n private dcrClient: DynamicClientRegistrar;\n private oauthFlow: InteractiveOAuthFlow;\n private headless: boolean;\n private redirectUri: string;\n private logger: Logger;\n\n constructor(options: DcrAuthenticatorOptions) {\n if (options.tokenStore) {\n this.tokenStore = options.tokenStore;\n } else {\n // Default CLI store in .mcp-z directory (per-project)\n const storePath = path.join(process.cwd(), '.mcp-z', 'tokens.json');\n\n // Ensure directory exists before creating store\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n\n this.tokenStore = new Keyv({\n store: new KeyvFile({ filename: storePath }),\n });\n }\n this.dcrClient = new DynamicClientRegistrar();\n this.oauthFlow = new InteractiveOAuthFlow();\n this.headless = options.headless || false;\n this.redirectUri = options.redirectUri;\n this.logger = options.logger ?? defaultLogger;\n }\n\n /**\n * Detect if server is self-hosted DCR (vs external OAuth provider)\n * Self-hosted servers have their own OAuth endpoints and manage token storage\n */\n private async detectSelfHostedMode(baseUrl: string): Promise<boolean> {\n try {\n // Self-hosted DCR servers typically run their own OAuth server\n // Check if this is a self-hosted instance by testing OAuth metadata\n // For now, assume self-hosted if baseUrl matches common localhost patterns\n // TODO: Implement proper self-hosted detection logic\n return baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1');\n } catch (_error) {\n return false; // Assume external mode if detection fails\n }\n }\n\n /**\n * Ensure server is authenticated, performing DCR and OAuth if needed\n * Proactively refreshes tokens if they're within 5 minutes of expiry\n *\n * @param baseUrl - Base URL of the server (e.g., https://example.com)\n * @param capabilities - Auth capabilities from .well-known endpoint\n * @returns Valid token set ready to use\n *\n * @throws Error if authentication fails\n *\n * @example\n * const authenticator = new DcrAuthenticator({ redirectUri: 'http://localhost:3000/callback' });\n * const tokens = await authenticator.ensureAuthenticated(\n * 'https://example.com',\n * capabilities\n * );\n */\n async ensureAuthenticated(baseUrl: string, capabilities: AuthCapabilities): Promise<TokenSet> {\n // Auto-detect server mode\n const isSelfHosted = await this.detectSelfHostedMode(baseUrl);\n\n if (isSelfHosted) {\n return this.ensureAuthenticatedSelfHosted(baseUrl, capabilities);\n }\n return this.ensureAuthenticatedExternal(baseUrl, capabilities);\n }\n\n /**\n * Handle authentication for self-hosted DCR servers\n * Self-hosted servers manage their own token storage via /oauth/verify\n */\n private async ensureAuthenticatedSelfHosted(baseUrl: string, capabilities: AuthCapabilities): Promise<TokenSet> {\n // Loopback trust for every discovery-derived fetch below, computed from\n // the server we're talking to, never from capabilities' endpoints (see discovery-fetch.ts).\n const allowLoopback = isLoopbackUrl(baseUrl);\n const issuer = requireIssuer(capabilities);\n const resource = normalizeUrl(baseUrl);\n const dcrTokenKey = `dcr-tokens:${issuer}:${resource}`;\n\n // 1. Check for existing DCR tokens (different from external tokens)\n let tokens = await this.loadTokens(dcrTokenKey, issuer);\n\n if (tokens) {\n // 2. Verify token is still valid by calling /oauth/verify\n try {\n const verifyUrl = `${baseUrl}/oauth/verify`;\n const verifyResponse = await fetch(verifyUrl, {\n headers: { Authorization: `Bearer ${tokens.accessToken}`, Connection: 'close' },\n });\n\n if (verifyResponse.ok) {\n const verifyData = (await verifyResponse.json()) as { token?: string };\n if (verifyData.token === tokens.accessToken) {\n // Token is still valid with the self-hosted server\n return tokens;\n }\n }\n } catch (_error) {\n // Token verification failed - need to re-authenticate\n }\n\n // Token is expired or invalid\n await this.tokenStore.delete(dcrTokenKey);\n tokens = undefined;\n }\n\n // 3. No valid tokens - perform full DCR + OAuth flow\n if (!capabilities.registrationEndpoint || !capabilities.authorizationEndpoint || !capabilities.tokenEndpoint) {\n throw new Error('Server does not provide required OAuth endpoints');\n }\n\n this.logger.debug('🔐 No valid tokens found, starting self-hosted DCR authentication...');\n\n // Extract port from pre-resolved redirectUri\n const port = parseInt(new URL(this.redirectUri).port, 10) || (this.redirectUri.startsWith('https:') ? 443 : 80);\n\n // Register OAuth client via DCR\n this.logger.debug('📝 Registering OAuth client with self-hosted server...');\n const client = await this.dcrClient.registerClient(capabilities.registrationEndpoint, {\n redirectUri: this.redirectUri,\n allowLoopback,\n });\n\n // Perform OAuth authorization flow with PKCE (RFC 7636)\n const flowOptions = this.buildFlowOptions(port, capabilities, issuer, resource, allowLoopback);\n\n tokens = await this.oauthFlow.performAuthFlow(capabilities.authorizationEndpoint, capabilities.tokenEndpoint, client.clientId, client.clientSecret, flowOptions);\n\n // For self-hosted mode, verify the token works with /oauth/verify immediately\n try {\n const verifyUrl = `${baseUrl}/oauth/verify`;\n const verifyResponse = await fetch(verifyUrl, {\n headers: { Authorization: `Bearer ${tokens.accessToken}`, Connection: 'close' },\n });\n\n if (!verifyResponse.ok) {\n throw new Error(`DCR token verification failed after authentication: ${verifyResponse.status}`);\n }\n\n const verifyData = (await verifyResponse.json()) as { token?: string };\n if (verifyData.token !== tokens.accessToken) {\n throw new Error('DCR server returned different token in verification');\n }\n\n this.logger.debug('✅ DCR token verified with self-hosted server');\n } catch (error) {\n this.logger.error('❌ DCR token verification failed:', error instanceof Error ? error.message : String(error));\n throw new Error('Self-hosted DCR authentication completed but token verification failed');\n }\n\n // Save tokens for future use\n await this.tokenStore.set(dcrTokenKey, { ...tokens, issuer });\n this.logger.debug('✅ Self-hosted DCR authentication successful, tokens saved');\n\n return tokens;\n }\n\n /** Handles authentication for external (non-self-hosted) OAuth providers. */\n private async ensureAuthenticatedExternal(baseUrl: string, capabilities: AuthCapabilities): Promise<TokenSet> {\n // See ensureAuthenticatedSelfHosted - same loopback trust rule.\n const allowLoopback = isLoopbackUrl(baseUrl);\n const issuer = requireIssuer(capabilities);\n const resource = normalizeUrl(baseUrl);\n const tokenKey = `tokens:${issuer}:${resource}`;\n\n // 1. Check for existing tokens\n let tokens = await this.loadTokens(tokenKey, issuer);\n\n if (tokens) {\n // 2. Proactive refresh if token expires within 5 minutes\n if (tokens.expiresAt < Date.now() + REFRESH_BUFFER_MS) {\n this.logger.debug('🔄 Refreshing access token...');\n\n try {\n tokens = await this.refreshTokens(tokens, capabilities.tokenEndpoint, resource, allowLoopback);\n await this.tokenStore.set(tokenKey, { ...tokens, issuer });\n this.logger.debug('✅ Token refreshed successfully');\n } catch (_error) {\n // Refresh failed - clear tokens and re-authenticate\n this.logger.warn('⚠️ Token refresh failed, re-authenticating...');\n await this.tokenStore.delete(tokenKey);\n tokens = undefined;\n }\n }\n\n if (tokens) {\n return tokens;\n }\n }\n\n // 3. No valid tokens - perform DCR + OAuth flow\n if (!capabilities.registrationEndpoint || !capabilities.authorizationEndpoint || !capabilities.tokenEndpoint) {\n throw new Error('Server does not provide required OAuth endpoints');\n }\n\n this.logger.debug('🔐 No valid tokens found, starting external OAuth authentication...');\n\n // Extract port from pre-resolved redirectUri\n const port = parseInt(new URL(this.redirectUri).port, 10) || (this.redirectUri.startsWith('https:') ? 443 : 80);\n\n // Register OAuth client via DCR\n this.logger.debug('📝 Registering OAuth client...');\n const client = await this.dcrClient.registerClient(capabilities.registrationEndpoint, {\n redirectUri: this.redirectUri,\n allowLoopback,\n });\n\n // Perform OAuth authorization flow with PKCE (RFC 7636)\n const flowOptions = this.buildFlowOptions(port, capabilities, issuer, resource, allowLoopback);\n\n tokens = await this.oauthFlow.performAuthFlow(capabilities.authorizationEndpoint, capabilities.tokenEndpoint, client.clientId, client.clientSecret, flowOptions);\n\n // Save tokens for future use\n await this.tokenStore.set(tokenKey, { ...tokens, issuer });\n this.logger.debug('✅ Authentication successful, tokens saved');\n\n return tokens;\n }\n\n /**\n * Refreshes an access token using a refresh token.\n * @param resource - Canonical resource server URI, audience-binding the token (RFC 8707).\n * @param allowLoopback - Loopback trust grant computed from the server actually being talked to (SSRF mitigation, see discovery-fetch.ts). Defaults to `false`.\n */\n private async refreshTokens(tokens: TokenSet, tokenEndpoint: string | undefined, resource: string, allowLoopback = false): Promise<TokenSet> {\n if (!tokenEndpoint) {\n throw new Error('Token endpoint not available for refresh');\n }\n\n if (!tokens.refreshToken) {\n throw new Error('No refresh token available');\n }\n\n if (!tokens.clientId || !tokens.clientSecret) {\n throw new Error('Client credentials not available for refresh');\n }\n\n return await this.oauthFlow.refreshTokens(tokenEndpoint, tokens.refreshToken, tokens.clientId, tokens.clientSecret, resource, allowLoopback);\n }\n\n /**\n * Deletes both stored token families for a server, across every issuer they were bound to.\n * @throws CredentialBindingError if the configured store cannot enumerate keys.\n */\n async deleteTokens(baseUrl: string): Promise<void> {\n const suffix = `:${normalizeUrl(baseUrl)}`;\n if (!this.tokenStore.iterator) {\n throw new CredentialBindingError('Token store does not support key enumeration, which issuer-keyed credentials require');\n }\n\n for await (const [key] of this.tokenStore.iterator(this.tokenStore.namespace)) {\n if (typeof key === 'string' && (key.startsWith('tokens:') || key.startsWith('dcr-tokens:')) && key.endsWith(suffix)) {\n await this.tokenStore.delete(key);\n }\n }\n this.logger.debug(`🗑️ Deleted tokens for ${baseUrl}`);\n }\n\n /** Discards a stored credential that is not bound to `issuer` (SEP-2352), including one stored before issuer binding existed. */\n private async loadTokens(key: string, issuer: string): Promise<TokenSet | undefined> {\n const tokens = (await this.tokenStore.get(key)) as TokenSet | undefined;\n if (!tokens) return undefined;\n if (tokens.issuer === issuer) return tokens;\n\n this.logger.debug('🔑 Stored credential is not bound to the discovered issuer, re-authorizing');\n await this.tokenStore.delete(key);\n return undefined;\n }\n\n private buildFlowOptions(port: number, capabilities: AuthCapabilities, issuer: string, resource: string, allowLoopback: boolean): OAuthFlowOptions {\n const flowOptions: OAuthFlowOptions = {\n port,\n issuer,\n resource,\n headless: this.headless,\n redirectUri: this.redirectUri,\n pkce: true,\n logger: this.logger,\n allowLoopback,\n authorizationResponseIssSupported: capabilities.authorizationResponseIssSupported ?? false,\n };\n if (capabilities.scopes) {\n flowOptions.scopes = capabilities.scopes;\n }\n return flowOptions;\n }\n}\n"],"names":["DcrAuthenticator","REFRESH_BUFFER_MS","CredentialBindingError","message","name","Error","requireIssuer","capabilities","issuer","options","tokenStore","storePath","path","join","process","cwd","fs","mkdirSync","dirname","recursive","Keyv","store","KeyvFile","filename","dcrClient","DynamicClientRegistrar","oauthFlow","InteractiveOAuthFlow","headless","redirectUri","logger","defaultLogger","detectSelfHostedMode","baseUrl","includes","_error","ensureAuthenticated","isSelfHosted","ensureAuthenticatedSelfHosted","ensureAuthenticatedExternal","allowLoopback","resource","dcrTokenKey","tokens","verifyUrl","verifyResponse","verifyData","port","client","flowOptions","error","isLoopbackUrl","normalizeUrl","loadTokens","fetch","headers","Authorization","accessToken","Connection","ok","json","token","delete","undefined","registrationEndpoint","authorizationEndpoint","tokenEndpoint","debug","parseInt","URL","startsWith","registerClient","buildFlowOptions","performAuthFlow","clientId","clientSecret","status","String","set","tokenKey","expiresAt","Date","now","refreshTokens","warn","refreshToken","deleteTokens","suffix","key","iterator","namespace","endsWith","get","pkce","authorizationResponseIssSupported","scopes"],"mappings":"AAAA;;;CAGC;;;;+BAuDYA;;;eAAAA;;;+DArDI;0DACG;2DACH;wBACQ;gCACK;sCACO;0BAER;wBACwB;wCACd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgBvC;;CAEC,GACD,IAAMC,oBAAoB,IAAI,KAAK;AAEnC,6EAA6E,GAC7E,IAAA,AAAMC,uCAAN;;cAAMA;aAAAA,uBACQC,OAAe;gCADvBD;;gBAEF,kBAFEA;YAEIC;;QACN,MAAKC,IAAI,GAAG;;;WAHVF;qBAA+BG;AAOrC;;;CAGC,GACD,SAASC,cAAcC,YAA8B;IACnD,IAAI,CAACA,aAAaC,MAAM,EAAE;QACxB,MAAM,IAAIN,uBAAuB;IACnC;IACA,OAAOK,aAAaC,MAAM;AAC5B;AAMO,IAAA,AAAMR,iCAAN;;aAAMA,iBAQCS,OAAgC;gCARjCT;YA0BKS;QAjBd,IAAIA,QAAQC,UAAU,EAAE;YACtB,IAAI,CAACA,UAAU,GAAGD,QAAQC,UAAU;QACtC,OAAO;YACL,sDAAsD;YACtD,IAAMC,YAAYC,iBAAI,CAACC,IAAI,CAACC,QAAQC,GAAG,IAAI,UAAU;YAErD,gDAAgD;YAChDC,IAAGC,SAAS,CAACL,iBAAI,CAACM,OAAO,CAACP,YAAY;gBAAEQ,WAAW;YAAK;YAExD,IAAI,CAACT,UAAU,GAAG,IAAIU,aAAI,CAAC;gBACzBC,OAAO,IAAIC,kBAAQ,CAAC;oBAAEC,UAAUZ;gBAAU;YAC5C;QACF;QACA,IAAI,CAACa,SAAS,GAAG,IAAIC,gDAAsB;QAC3C,IAAI,CAACC,SAAS,GAAG,IAAIC,4CAAoB;QACzC,IAAI,CAACC,QAAQ,GAAGnB,QAAQmB,QAAQ,IAAI;QACpC,IAAI,CAACC,WAAW,GAAGpB,QAAQoB,WAAW;QACtC,IAAI,CAACC,MAAM,IAAGrB,kBAAAA,QAAQqB,MAAM,cAAdrB,6BAAAA,kBAAkBsB,gBAAa;;iBA1BpC/B;IA6BX;;;GAGC,GACD,OAAcgC,oBAUb,GAVD,SAAcA,qBAAqBC,OAAe;;;gBAChD,IAAI;oBACF,+DAA+D;oBAC/D,oEAAoE;oBACpE,2EAA2E;oBAC3E,qDAAqD;oBACrD;;wBAAOA,QAAQC,QAAQ,CAAC,gBAAgBD,QAAQC,QAAQ,CAAC;;gBAC3D,EAAE,OAAOC,QAAQ;oBACf;;wBAAO;uBAAO,0CAA0C;gBAC1D;;;;;QACF;;IAEA;;;;;;;;;;;;;;;;GAgBC,GACD,OAAMC,mBAQL,GARD,SAAMA,oBAAoBH,OAAe,EAAE1B,YAA8B;;gBAEjE8B;;;;wBAAe;;4BAAM,IAAI,CAACL,oBAAoB,CAACC;;;wBAA/CI,eAAe;wBAErB,IAAIA,cAAc;4BAChB;;gCAAO,IAAI,CAACC,6BAA6B,CAACL,SAAS1B;;wBACrD;wBACA;;4BAAO,IAAI,CAACgC,2BAA2B,CAACN,SAAS1B;;;;QACnD;;IAEA;;;GAGC,GACD,OAAc+B,6BAoFb,GApFD,SAAcA,8BAA8BL,OAAe,EAAE1B,YAA8B;;gBAGnFiC,eACAhC,QACAiC,UACAC,aAGFC,QAKMC,WACAC,gBAKEC,YAMDX,QAiBLY,MAIAC,QAMAC,aAMEL,YACAC,iBAQAC,aAMCI;;;;wBAzET,wEAAwE;wBACxE,4FAA4F;wBACtFV,gBAAgBW,IAAAA,+BAAa,EAAClB;wBAC9BzB,SAASF,cAAcC;wBACvBkC,WAAWW,IAAAA,wBAAY,EAACnB;wBACxBS,cAAc,AAAC,cAAuBD,OAAVjC,QAAO,KAAY,OAATiC;wBAG/B;;4BAAM,IAAI,CAACY,UAAU,CAACX,aAAalC;;;wBAA5CmC,SAAS;6BAETA,QAAAA;;;;;;;;;;;;wBAGMC,YAAY,AAAC,GAAU,OAARX,SAAQ;wBACN;;4BAAMqB,MAAMV,WAAW;gCAC5CW,SAAS;oCAAEC,eAAe,AAAC,UAA4B,OAAnBb,OAAOc,WAAW;oCAAIC,YAAY;gCAAQ;4BAChF;;;wBAFMb,iBAAiB;6BAInBA,eAAec,EAAE,EAAjBd;;;;wBACkB;;4BAAMA,eAAee,IAAI;;;wBAAvCd,aAAc;wBACpB,IAAIA,WAAWe,KAAK,KAAKlB,OAAOc,WAAW,EAAE;4BAC3C,mDAAmD;4BACnD;;gCAAOd;;wBACT;;;;;;;;wBAEKR;;;;;;wBAIT,8BAA8B;wBAC9B;;4BAAM,IAAI,CAACzB,UAAU,CAACoD,MAAM,CAACpB;;;wBAA7B;wBACAC,SAASoB;;;wBAGX,qDAAqD;wBACrD,IAAI,CAACxD,aAAayD,oBAAoB,IAAI,CAACzD,aAAa0D,qBAAqB,IAAI,CAAC1D,aAAa2D,aAAa,EAAE;4BAC5G,MAAM,IAAI7D,MAAM;wBAClB;wBAEA,IAAI,CAACyB,MAAM,CAACqC,KAAK,CAAC;wBAElB,6CAA6C;wBACvCpB,OAAOqB,SAAS,IAAIC,IAAI,IAAI,CAACxC,WAAW,EAAEkB,IAAI,EAAE,OAAQ,CAAA,IAAI,CAAClB,WAAW,CAACyC,UAAU,CAAC,YAAY,MAAM,EAAC;wBAE7G,gCAAgC;wBAChC,IAAI,CAACxC,MAAM,CAACqC,KAAK,CAAC;wBACH;;4BAAM,IAAI,CAAC3C,SAAS,CAAC+C,cAAc,CAAChE,aAAayD,oBAAoB,EAAE;gCACpFnC,aAAa,IAAI,CAACA,WAAW;gCAC7BW,eAAAA;4BACF;;;wBAHMQ,SAAS;wBAKf,wDAAwD;wBAClDC,cAAc,IAAI,CAACuB,gBAAgB,CAACzB,MAAMxC,cAAcC,QAAQiC,UAAUD;wBAEvE;;4BAAM,IAAI,CAACd,SAAS,CAAC+C,eAAe,CAAClE,aAAa0D,qBAAqB,EAAE1D,aAAa2D,aAAa,EAAElB,OAAO0B,QAAQ,EAAE1B,OAAO2B,YAAY,EAAE1B;;;wBAApJN,SAAS;;;;;;;;;wBAIDC,aAAY,AAAC,GAAU,OAARX,SAAQ;wBACN;;4BAAMqB,MAAMV,YAAW;gCAC5CW,SAAS;oCAAEC,eAAe,AAAC,UAA4B,OAAnBb,OAAOc,WAAW;oCAAIC,YAAY;gCAAQ;4BAChF;;;wBAFMb,kBAAiB;wBAIvB,IAAI,CAACA,gBAAec,EAAE,EAAE;4BACtB,MAAM,IAAItD,MAAM,AAAC,uDAA4E,OAAtBwC,gBAAe+B,MAAM;wBAC9F;wBAEoB;;4BAAM/B,gBAAee,IAAI;;;wBAAvCd,cAAc;wBACpB,IAAIA,YAAWe,KAAK,KAAKlB,OAAOc,WAAW,EAAE;4BAC3C,MAAM,IAAIpD,MAAM;wBAClB;wBAEA,IAAI,CAACyB,MAAM,CAACqC,KAAK,CAAC;;;;;;wBACXjB;wBACP,IAAI,CAACpB,MAAM,CAACoB,KAAK,CAAC,oCAAoCA,AAAK,YAALA,OAAiB7C,SAAQ6C,MAAM/C,OAAO,GAAG0E,OAAO3B;wBACtG,MAAM,IAAI7C,MAAM;;wBAGlB,6BAA6B;wBAC7B;;4BAAM,IAAI,CAACK,UAAU,CAACoE,GAAG,CAACpC,aAAa,wCAAKC;gCAAQnC,QAAAA;;;;wBAApD;wBACA,IAAI,CAACsB,MAAM,CAACqC,KAAK,CAAC;wBAElB;;4BAAOxB;;;;QACT;;IAEA,2EAA2E,GAC3E,OAAcJ,2BA2Db,GA3DD,SAAcA,4BAA4BN,OAAe,EAAE1B,YAA8B;;gBAEjFiC,eACAhC,QACAiC,UACAsC,UAGFpC,QAWSR,QAqBPY,MAIAC,QAMAC;;;;wBAjDN,gEAAgE;wBAC1DT,gBAAgBW,IAAAA,+BAAa,EAAClB;wBAC9BzB,SAASF,cAAcC;wBACvBkC,WAAWW,IAAAA,wBAAY,EAACnB;wBACxB8C,WAAW,AAAC,UAAmBtC,OAAVjC,QAAO,KAAY,OAATiC;wBAGxB;;4BAAM,IAAI,CAACY,UAAU,CAAC0B,UAAUvE;;;wBAAzCmC,SAAS;6BAETA,QAAAA;;;;6BAEEA,CAAAA,OAAOqC,SAAS,GAAGC,KAAKC,GAAG,KAAKjF,iBAAgB,GAAhD0C;;;;wBACF,IAAI,CAACb,MAAM,CAACqC,KAAK,CAAC;;;;;;;;;wBAGP;;4BAAM,IAAI,CAACgB,aAAa,CAACxC,QAAQpC,aAAa2D,aAAa,EAAEzB,UAAUD;;;wBAAhFG,SAAS;wBACT;;4BAAM,IAAI,CAACjC,UAAU,CAACoE,GAAG,CAACC,UAAU,wCAAKpC;gCAAQnC,QAAAA;;;;wBAAjD;wBACA,IAAI,CAACsB,MAAM,CAACqC,KAAK,CAAC;;;;;;wBACXhC;wBACP,oDAAoD;wBACpD,IAAI,CAACL,MAAM,CAACsD,IAAI,CAAC;wBACjB;;4BAAM,IAAI,CAAC1E,UAAU,CAACoD,MAAM,CAACiB;;;wBAA7B;wBACApC,SAASoB;;;;;;wBAIb,IAAIpB,QAAQ;4BACV;;gCAAOA;;wBACT;;;wBAGF,gDAAgD;wBAChD,IAAI,CAACpC,aAAayD,oBAAoB,IAAI,CAACzD,aAAa0D,qBAAqB,IAAI,CAAC1D,aAAa2D,aAAa,EAAE;4BAC5G,MAAM,IAAI7D,MAAM;wBAClB;wBAEA,IAAI,CAACyB,MAAM,CAACqC,KAAK,CAAC;wBAElB,6CAA6C;wBACvCpB,OAAOqB,SAAS,IAAIC,IAAI,IAAI,CAACxC,WAAW,EAAEkB,IAAI,EAAE,OAAQ,CAAA,IAAI,CAAClB,WAAW,CAACyC,UAAU,CAAC,YAAY,MAAM,EAAC;wBAE7G,gCAAgC;wBAChC,IAAI,CAACxC,MAAM,CAACqC,KAAK,CAAC;wBACH;;4BAAM,IAAI,CAAC3C,SAAS,CAAC+C,cAAc,CAAChE,aAAayD,oBAAoB,EAAE;gCACpFnC,aAAa,IAAI,CAACA,WAAW;gCAC7BW,eAAAA;4BACF;;;wBAHMQ,SAAS;wBAKf,wDAAwD;wBAClDC,cAAc,IAAI,CAACuB,gBAAgB,CAACzB,MAAMxC,cAAcC,QAAQiC,UAAUD;wBAEvE;;4BAAM,IAAI,CAACd,SAAS,CAAC+C,eAAe,CAAClE,aAAa0D,qBAAqB,EAAE1D,aAAa2D,aAAa,EAAElB,OAAO0B,QAAQ,EAAE1B,OAAO2B,YAAY,EAAE1B;;;wBAApJN,SAAS;wBAET,6BAA6B;wBAC7B;;4BAAM,IAAI,CAACjC,UAAU,CAACoE,GAAG,CAACC,UAAU,wCAAKpC;gCAAQnC,QAAAA;;;;wBAAjD;wBACA,IAAI,CAACsB,MAAM,CAACqC,KAAK,CAAC;wBAElB;;4BAAOxB;;;;QACT;;IAEA;;;;GAIC,GACD,OAAcwC,aAcb,GAdD,SAAcA,cAAcxC,MAAgB,EAAEuB,aAAiC,EAAEzB,QAAgB;YAAED,gBAAAA,iEAAgB;;;;;wBACjH,IAAI,CAAC0B,eAAe;4BAClB,MAAM,IAAI7D,MAAM;wBAClB;wBAEA,IAAI,CAACsC,OAAO0C,YAAY,EAAE;4BACxB,MAAM,IAAIhF,MAAM;wBAClB;wBAEA,IAAI,CAACsC,OAAO+B,QAAQ,IAAI,CAAC/B,OAAOgC,YAAY,EAAE;4BAC5C,MAAM,IAAItE,MAAM;wBAClB;wBAEO;;4BAAM,IAAI,CAACqB,SAAS,CAACyD,aAAa,CAACjB,eAAevB,OAAO0C,YAAY,EAAE1C,OAAO+B,QAAQ,EAAE/B,OAAOgC,YAAY,EAAElC,UAAUD;;;wBAA9H;;4BAAO;;;;QACT;;IAEA;;;GAGC,GACD,OAAM8C,YAYL,GAZD,SAAMA,aAAarD,OAAe;;gBAC1BsD,yGAKYC;;;;wBALZD,SAAS,AAAC,IAAyB,OAAtBnC,IAAAA,wBAAY,EAACnB;wBAChC,IAAI,CAAC,IAAI,CAACvB,UAAU,CAAC+E,QAAQ,EAAE;4BAC7B,MAAM,IAAIvF,uBAAuB;wBACnC;;;;;;;;;;oDAE0B,IAAI,CAACQ,UAAU,CAAC+E,QAAQ,CAAC,IAAI,CAAC/E,UAAU,CAACgF,SAAS;;;;;;;;;;;;;+DAA1DF;6BACZ,CAAA,OAAOA,QAAQ,YAAaA,CAAAA,IAAIlB,UAAU,CAAC,cAAckB,IAAIlB,UAAU,CAAC,cAAa,KAAMkB,IAAIG,QAAQ,CAACJ,OAAM,GAA9G;;;;wBACF;;4BAAM,IAAI,CAAC7E,UAAU,CAACoD,MAAM,CAAC0B;;;wBAA7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAGJ,IAAI,CAAC1D,MAAM,CAACqC,KAAK,CAAC,AAAC,qCAAkC,OAARlC;;;;;;QAC/C;;IAEA,+HAA+H,GAC/H,OAAcoB,UAQb,GARD,SAAcA,WAAWmC,GAAW,EAAEhF,MAAc;;gBAC5CmC;;;;wBAAU;;4BAAM,IAAI,CAACjC,UAAU,CAACkF,GAAG,CAACJ;;;wBAApC7C,SAAU;wBAChB,IAAI,CAACA,QAAQ;;4BAAOoB;;wBACpB,IAAIpB,OAAOnC,MAAM,KAAKA,QAAQ;;4BAAOmC;;wBAErC,IAAI,CAACb,MAAM,CAACqC,KAAK,CAAC;wBAClB;;4BAAM,IAAI,CAACzD,UAAU,CAACoD,MAAM,CAAC0B;;;wBAA7B;wBACA;;4BAAOzB;;;;QACT;;IAEA,OAAQS,gBAgBP,GAhBD,SAAQA,iBAAiBzB,IAAY,EAAExC,YAA8B,EAAEC,MAAc,EAAEiC,QAAgB,EAAED,aAAsB;YAUxFjC;QATrC,IAAM0C,cAAgC;YACpCF,MAAAA;YACAvC,QAAAA;YACAiC,UAAAA;YACAb,UAAU,IAAI,CAACA,QAAQ;YACvBC,aAAa,IAAI,CAACA,WAAW;YAC7BgE,MAAM;YACN/D,QAAQ,IAAI,CAACA,MAAM;YACnBU,eAAAA;YACAsD,iCAAiC,GAAEvF,kDAAAA,aAAauF,iCAAiC,cAA9CvF,6DAAAA,kDAAkD;QACvF;QACA,IAAIA,aAAawF,MAAM,EAAE;YACvB9C,YAAY8C,MAAM,GAAGxF,aAAawF,MAAM;QAC1C;QACA,OAAO9C;IACT;WAlSWjD"}
|
|
@@ -9,9 +9,9 @@ export declare class DynamicClientRegistrar {
|
|
|
9
9
|
* Registers a new OAuth client with the authorization server (RFC 7591).
|
|
10
10
|
*
|
|
11
11
|
* @param registrationEndpoint - Often sourced from remote-controlled AS metadata; pass `options.allowLoopback` explicitly. Defaults to `false`.
|
|
12
|
-
* @param options - Registration options (
|
|
12
|
+
* @param options - Registration options (redirect URI, client name, loopback trust).
|
|
13
13
|
* @returns Client credentials (client ID and secret).
|
|
14
14
|
* @throws Error if registration fails or the server returns an error.
|
|
15
15
|
*/
|
|
16
|
-
registerClient(registrationEndpoint: string, options
|
|
16
|
+
registerClient(registrationEndpoint: string, options: DcrRegistrationOptions): Promise<ClientCredentials>;
|
|
17
17
|
}
|
|
@@ -9,9 +9,9 @@ export declare class DynamicClientRegistrar {
|
|
|
9
9
|
* Registers a new OAuth client with the authorization server (RFC 7591).
|
|
10
10
|
*
|
|
11
11
|
* @param registrationEndpoint - Often sourced from remote-controlled AS metadata; pass `options.allowLoopback` explicitly. Defaults to `false`.
|
|
12
|
-
* @param options - Registration options (
|
|
12
|
+
* @param options - Registration options (redirect URI, client name, loopback trust).
|
|
13
13
|
* @returns Client credentials (client ID and secret).
|
|
14
14
|
* @throws Error if registration fails or the server returns an error.
|
|
15
15
|
*/
|
|
16
|
-
registerClient(registrationEndpoint: string, options
|
|
16
|
+
registerClient(registrationEndpoint: string, options: DcrRegistrationOptions): Promise<ClientCredentials>;
|
|
17
17
|
}
|
|
@@ -150,23 +150,19 @@ var DynamicClientRegistrar = /*#__PURE__*/ function() {
|
|
|
150
150
|
* Registers a new OAuth client with the authorization server (RFC 7591).
|
|
151
151
|
*
|
|
152
152
|
* @param registrationEndpoint - Often sourced from remote-controlled AS metadata; pass `options.allowLoopback` explicitly. Defaults to `false`.
|
|
153
|
-
* @param options - Registration options (
|
|
153
|
+
* @param options - Registration options (redirect URI, client name, loopback trust).
|
|
154
154
|
* @returns Client credentials (client ID and secret).
|
|
155
155
|
* @throws Error if registration fails or the server returns an error.
|
|
156
|
-
*/ _proto.registerClient = function registerClient(
|
|
157
|
-
return _async_to_generator(function(
|
|
158
|
-
var
|
|
159
|
-
var _arguments = arguments;
|
|
156
|
+
*/ _proto.registerClient = function registerClient(registrationEndpoint, options) {
|
|
157
|
+
return _async_to_generator(function() {
|
|
158
|
+
var _options_allowLoopback, requestBody, response, errorText, data, credentials;
|
|
160
159
|
return _ts_generator(this, function(_state) {
|
|
161
160
|
switch(_state.label){
|
|
162
161
|
case 0:
|
|
163
|
-
options = _arguments.length > 1 && _arguments[1] !== void 0 ? _arguments[1] : {};
|
|
164
162
|
requestBody = {
|
|
165
163
|
client_name: options.clientName || '@mcp-z/client',
|
|
166
|
-
redirect_uris:
|
|
164
|
+
redirect_uris: [
|
|
167
165
|
options.redirectUri
|
|
168
|
-
] : [
|
|
169
|
-
'http://localhost:3000/callback'
|
|
170
166
|
],
|
|
171
167
|
grant_types: [
|
|
172
168
|
'authorization_code',
|
|
@@ -175,7 +171,10 @@ var DynamicClientRegistrar = /*#__PURE__*/ function() {
|
|
|
175
171
|
response_types: [
|
|
176
172
|
'code'
|
|
177
173
|
],
|
|
178
|
-
token_endpoint_auth_method: 'client_secret_basic'
|
|
174
|
+
token_endpoint_auth_method: 'client_secret_basic',
|
|
175
|
+
// The callback listener always binds an OS-assigned loopback port, so the
|
|
176
|
+
// redirect URI is one an authorization server rejects for a web client (SEP-837, RFC 8252).
|
|
177
|
+
application_type: 'native'
|
|
179
178
|
};
|
|
180
179
|
return [
|
|
181
180
|
4,
|
|
@@ -227,7 +226,7 @@ var DynamicClientRegistrar = /*#__PURE__*/ function() {
|
|
|
227
226
|
];
|
|
228
227
|
}
|
|
229
228
|
});
|
|
230
|
-
})
|
|
229
|
+
})();
|
|
231
230
|
};
|
|
232
231
|
return DynamicClientRegistrar;
|
|
233
232
|
}();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/dcr/dynamic-client-registrar.ts"],"sourcesContent":["/**\n * Dynamic Client Registration (DCR) Client\n * Implements RFC 7591 for OAuth client registration\n */\n\nimport { discoveryFetch } from '../auth/discovery-fetch.ts';\nimport type { ClientCredentials, DcrRegistrationOptions } from '../auth/types.ts';\n\n/** DCR Registration Request (RFC 7591) */\ninterface DcrRegistrationRequest {\n client_name?: string;\n redirect_uris?: string[];\n grant_types?: string[];\n response_types?: string[];\n token_endpoint_auth_method?: string;\n}\n\n/** DCR Registration Response (RFC 7591) */\ninterface DcrRegistrationResponse {\n client_id: string;\n client_secret?: string;\n client_id_issued_at?: number;\n client_secret_expires_at?: number;\n}\n\n/** Handles Dynamic Client Registration with OAuth servers. */\nexport class DynamicClientRegistrar {\n /**\n * Registers a new OAuth client with the authorization server (RFC 7591).\n *\n * @param registrationEndpoint - Often sourced from remote-controlled AS metadata; pass `options.allowLoopback` explicitly. Defaults to `false`.\n * @param options - Registration options (
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/dcr/dynamic-client-registrar.ts"],"sourcesContent":["/**\n * Dynamic Client Registration (DCR) Client\n * Implements RFC 7591 for OAuth client registration\n */\n\nimport { discoveryFetch } from '../auth/discovery-fetch.ts';\nimport type { ClientCredentials, DcrRegistrationOptions } from '../auth/types.ts';\n\n/** DCR Registration Request (RFC 7591) */\ninterface DcrRegistrationRequest {\n client_name?: string;\n redirect_uris?: string[];\n grant_types?: string[];\n response_types?: string[];\n token_endpoint_auth_method?: string;\n application_type?: string;\n}\n\n/** DCR Registration Response (RFC 7591) */\ninterface DcrRegistrationResponse {\n client_id: string;\n client_secret?: string;\n client_id_issued_at?: number;\n client_secret_expires_at?: number;\n}\n\n/** Handles Dynamic Client Registration with OAuth servers. */\nexport class DynamicClientRegistrar {\n /**\n * Registers a new OAuth client with the authorization server (RFC 7591).\n *\n * @param registrationEndpoint - Often sourced from remote-controlled AS metadata; pass `options.allowLoopback` explicitly. Defaults to `false`.\n * @param options - Registration options (redirect URI, client name, loopback trust).\n * @returns Client credentials (client ID and secret).\n * @throws Error if registration fails or the server returns an error.\n */\n async registerClient(registrationEndpoint: string, options: DcrRegistrationOptions): Promise<ClientCredentials> {\n const requestBody: DcrRegistrationRequest = {\n client_name: options.clientName || '@mcp-z/client',\n redirect_uris: [options.redirectUri],\n grant_types: ['authorization_code', 'refresh_token'],\n response_types: ['code'],\n token_endpoint_auth_method: 'client_secret_basic',\n // The callback listener always binds an OS-assigned loopback port, so the\n // redirect URI is one an authorization server rejects for a web client (SEP-837, RFC 8252).\n application_type: 'native',\n };\n\n // registrationEndpoint is remote-controlled discovery data; allowLoopback\n // must come from the caller's own trust, never from registrationEndpoint.\n const response = await discoveryFetch(\n registrationEndpoint,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n Connection: 'close',\n },\n body: JSON.stringify(requestBody),\n },\n 'registration endpoint',\n { allowLoopback: options.allowLoopback ?? false }\n );\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`DCR registration failed (${response.status}): ${errorText}`);\n }\n\n const data = (await response.json()) as DcrRegistrationResponse;\n\n if (!data.client_id) {\n throw new Error('DCR registration response missing client_id');\n }\n\n const credentials: ClientCredentials = {\n clientId: data.client_id,\n clientSecret: data.client_secret || '',\n };\n\n if (data.client_id_issued_at) {\n credentials.issuedAt = data.client_id_issued_at;\n }\n\n return credentials;\n }\n}\n"],"names":["DynamicClientRegistrar","registerClient","registrationEndpoint","options","requestBody","response","errorText","data","credentials","client_name","clientName","redirect_uris","redirectUri","grant_types","response_types","token_endpoint_auth_method","application_type","discoveryFetch","method","headers","Accept","Connection","body","JSON","stringify","allowLoopback","ok","text","Error","status","json","client_id","clientId","clientSecret","client_secret","client_id_issued_at","issuedAt"],"mappings":"AAAA;;;CAGC;;;;+BAwBYA;;;eAAAA;;;gCAtBkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsBxB,IAAA,AAAMA,uCAAN;;aAAMA;gCAAAA;;iBAAAA;IACX;;;;;;;GAOC,GACD,OAAMC,cAkDL,GAlDD,SAAMA,eAAeC,oBAA4B,EAAEC,OAA+B;;gBA0B7DA,wBAzBbC,aAaAC,UAgBEC,WAIFC,MAMAC;;;;wBAvCAJ,cAAsC;4BAC1CK,aAAaN,QAAQO,UAAU,IAAI;4BACnCC,aAAa;gCAAGR,QAAQS,WAAW;;4BACnCC,WAAW;gCAAG;gCAAsB;;4BACpCC,cAAc;gCAAG;;4BACjBC,4BAA4B;4BAC5B,0EAA0E;4BAC1E,4FAA4F;4BAC5FC,kBAAkB;wBACpB;wBAIiB;;4BAAMC,IAAAA,gCAAc,EACnCf,sBACA;gCACEgB,QAAQ;gCACRC,SAAS;oCACP,gBAAgB;oCAChBC,QAAQ;oCACRC,YAAY;gCACd;gCACAC,MAAMC,KAAKC,SAAS,CAACpB;4BACvB,GACA,yBACA;gCAAEqB,aAAa,GAAEtB,yBAAAA,QAAQsB,aAAa,cAArBtB,oCAAAA,yBAAyB;4BAAM;;;wBAZ5CE,WAAW;6BAeb,CAACA,SAASqB,EAAE,EAAZ;;;;wBACgB;;4BAAMrB,SAASsB,IAAI;;;wBAA/BrB,YAAY;wBAClB,MAAM,IAAIsB,MAAM,AAAC,4BAAgDtB,OAArBD,SAASwB,MAAM,EAAC,OAAe,OAAVvB;;wBAGrD;;4BAAMD,SAASyB,IAAI;;;wBAA3BvB,OAAQ;wBAEd,IAAI,CAACA,KAAKwB,SAAS,EAAE;4BACnB,MAAM,IAAIH,MAAM;wBAClB;wBAEMpB,cAAiC;4BACrCwB,UAAUzB,KAAKwB,SAAS;4BACxBE,cAAc1B,KAAK2B,aAAa,IAAI;wBACtC;wBAEA,IAAI3B,KAAK4B,mBAAmB,EAAE;4BAC5B3B,YAAY4B,QAAQ,GAAG7B,KAAK4B,mBAAmB;wBACjD;wBAEA;;4BAAO3B;;;;QACT;;WA3DWR"}
|
|
@@ -21,8 +21,8 @@ function normalizeUrl(input) {
|
|
|
21
21
|
var url = new URL(input);
|
|
22
22
|
url.search = '';
|
|
23
23
|
url.hash = '';
|
|
24
|
-
|
|
25
|
-
return url.origin + url.pathname;
|
|
24
|
+
// Strip after joining: assigning an empty pathname puts the '/' straight back.
|
|
25
|
+
return (url.origin + url.pathname).replace(/\/+$/, '');
|
|
26
26
|
} catch (unused) {
|
|
27
27
|
return input.replace(/\/+$/, '');
|
|
28
28
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/lib/url-utils.ts"],"sourcesContent":["export function normalizeUrl(input: string): string {\n try {\n const url = new URL(input);\n url.search = '';\n url.hash = '';\n
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/lib/url-utils.ts"],"sourcesContent":["export function normalizeUrl(input: string): string {\n try {\n const url = new URL(input);\n url.search = '';\n url.hash = '';\n // Strip after joining: assigning an empty pathname puts the '/' straight back.\n return (url.origin + url.pathname).replace(/\\/+$/, '');\n } catch {\n return input.replace(/\\/+$/, '');\n }\n}\n\nexport function joinWellKnown(baseUrl: string, suffix: string): string {\n return `${normalizeUrl(baseUrl)}${suffix}`;\n}\n"],"names":["joinWellKnown","normalizeUrl","input","url","URL","search","hash","origin","pathname","replace","baseUrl","suffix"],"mappings":";;;;;;;;;;;QAYgBA;eAAAA;;QAZAC;eAAAA;;;AAAT,SAASA,aAAaC,KAAa;IACxC,IAAI;QACF,IAAMC,MAAM,IAAIC,IAAIF;QACpBC,IAAIE,MAAM,GAAG;QACbF,IAAIG,IAAI,GAAG;QACX,+EAA+E;QAC/E,OAAO,AAACH,CAAAA,IAAII,MAAM,GAAGJ,IAAIK,QAAQ,AAAD,EAAGC,OAAO,CAAC,QAAQ;IACrD,EAAE,eAAM;QACN,OAAOP,MAAMO,OAAO,CAAC,QAAQ;IAC/B;AACF;AAEO,SAAST,cAAcU,OAAe,EAAEC,MAAc;IAC3D,OAAO,AAAC,GAA0BA,OAAxBV,aAAaS,UAAkB,OAAPC;AACpC"}
|
|
@@ -41,8 +41,12 @@ import { discoverAuthorizationServerIssuer, discoverAuthorizationServerMetadata,
|
|
|
41
41
|
*/ function buildCapabilities(metadata, scopes) {
|
|
42
42
|
const supportsDcr = !!metadata.registration_endpoint;
|
|
43
43
|
const capabilities = {
|
|
44
|
-
supportsDcr
|
|
44
|
+
supportsDcr,
|
|
45
|
+
authorizationResponseIssSupported: metadata.authorization_response_iss_parameter_supported === true
|
|
45
46
|
};
|
|
47
|
+
if (metadata.issuer) {
|
|
48
|
+
capabilities.issuer = metadata.issuer;
|
|
49
|
+
}
|
|
46
50
|
if (metadata.registration_endpoint) {
|
|
47
51
|
capabilities.registrationEndpoint = metadata.registration_endpoint;
|
|
48
52
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/capability-discovery.ts"],"sourcesContent":["/**\n * OAuth Server Capability Discovery\n * Probes RFC 9728 (Protected Resource) and RFC 8414 (Authorization Server) metadata\n */\n\nimport { normalizeUrl } from '../lib/url-utils.ts';\nimport { isLoopbackUrl } from './discovery-fetch.ts';\nimport { discoverAuthorizationServerIssuer, discoverAuthorizationServerMetadata, discoverProtectedResourceMetadata } from './rfc9728-discovery.ts';\nimport type { AuthCapabilities, AuthorizationServerMetadata } from './types.ts';\n\n/**\n * Extract origin (protocol + host) from a URL\n * @param url - Full URL that may include a path\n * @returns Origin (e.g., \"https://example.com\") or original string if invalid URL\n *\n * @example\n * getOrigin('https://example.com/mcp') // → 'https://example.com'\n * getOrigin('http://localhost:9999/api/v1/mcp') // → 'http://localhost:9999'\n */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n // Invalid URL - return as-is for graceful degradation\n return url;\n }\n}\n\n/**\n * Probe OAuth server capabilities using RFC 9728 → RFC 8414 discovery chain\n * Returns capabilities including DCR support detection\n *\n * Discovery Strategy:\n * 1. Try RFC 9728 Protected Resource Metadata (supports cross-domain OAuth)\n * 2. If found, use first authorization_server to discover RFC 8414 Authorization Server Metadata\n * 3. Fall back to direct RFC 8414 discovery at resource origin\n *\n * @param baseUrl - Base URL of the protected resource (e.g., https://ai.todoist.net/mcp)\n * @returns AuthCapabilities object with discovered endpoints and features\n *\n * @example\n * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com\n * const caps = await probeAuthCapabilities('https://ai.todoist.net/mcp');\n * if (caps.supportsDcr) {\n * console.log('Registration endpoint:', caps.registrationEndpoint);\n * }\n */\nfunction buildCapabilities(metadata: AuthorizationServerMetadata, scopes?: string[]): AuthCapabilities {\n const supportsDcr = !!metadata.registration_endpoint;\n const capabilities: AuthCapabilities = { supportsDcr };\n\n if (metadata.registration_endpoint) {\n capabilities.registrationEndpoint = metadata.registration_endpoint;\n }\n if (metadata.authorization_endpoint) {\n capabilities.authorizationEndpoint = metadata.authorization_endpoint;\n }\n if (metadata.token_endpoint) capabilities.tokenEndpoint = metadata.token_endpoint;\n if (metadata.introspection_endpoint) {\n capabilities.introspectionEndpoint = metadata.introspection_endpoint;\n }\n\n if (scopes && scopes.length > 0) {\n capabilities.scopes = scopes;\n } else if (metadata.scopes_supported) {\n capabilities.scopes = metadata.scopes_supported;\n }\n\n return capabilities;\n}\n\nasync function resolveCapabilitiesFromAuthorizationServer(authServerUrl: string, scopes: string[] | undefined, allowLoopback: boolean): Promise<AuthCapabilities | null> {\n const metadata = await discoverAuthorizationServerMetadata(authServerUrl, { allowLoopback });\n if (!metadata) return null;\n return buildCapabilities(metadata, scopes);\n}\n\nexport async function probeAuthCapabilities(baseUrl: string): Promise<AuthCapabilities> {\n try {\n const normalizedBaseUrl = normalizeUrl(baseUrl);\n // Trust signal for every authorization-server fetch below: whether the\n // configured MCP server is itself loopback, never a remote-supplied URL.\n const allowLoopback = isLoopbackUrl(normalizedBaseUrl);\n // Strategy 1: Try RFC 9728 Protected Resource Metadata discovery\n // This handles cross-domain OAuth (e.g., Todoist: ai.todoist.net/mcp → todoist.com)\n const resourceMetadata = await discoverProtectedResourceMetadata(normalizedBaseUrl);\n\n if (resourceMetadata && resourceMetadata.authorization_servers.length > 0) {\n // Found protected resource metadata with authorization servers\n // Discover the authorization server's metadata (RFC 8414)\n const authServerUrl = resourceMetadata.authorization_servers[0];\n if (!authServerUrl) {\n // Array has length > 0 but first element is undefined/null - skip this path\n return { supportsDcr: false };\n }\n const capabilities = await resolveCapabilitiesFromAuthorizationServer(authServerUrl, resourceMetadata.scopes_supported, allowLoopback);\n if (capabilities) {\n return capabilities;\n }\n\n const issuer = await discoverAuthorizationServerIssuer(baseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer, resourceMetadata.scopes_supported, allowLoopback);\n if (issuerCapabilities) return issuerCapabilities;\n }\n }\n\n const issuer = await discoverAuthorizationServerIssuer(normalizedBaseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer, undefined, allowLoopback);\n if (issuerCapabilities) return issuerCapabilities;\n }\n\n // Strategy 2: Fall back to direct RFC 8414 discovery at resource origin\n // This handles same-domain OAuth (traditional setup)\n const origin = getOrigin(normalizedBaseUrl);\n const originCapabilities = await resolveCapabilitiesFromAuthorizationServer(origin, undefined, allowLoopback);\n if (originCapabilities) return originCapabilities;\n\n // No OAuth metadata found\n return { supportsDcr: false };\n } catch (_error) {\n // Network error, invalid JSON, or other fetch failure\n // Gracefully degrade - assume no DCR support\n return { supportsDcr: false };\n }\n}\n"],"names":["normalizeUrl","isLoopbackUrl","discoverAuthorizationServerIssuer","discoverAuthorizationServerMetadata","discoverProtectedResourceMetadata","getOrigin","url","URL","origin","buildCapabilities","metadata","scopes","supportsDcr","registration_endpoint","capabilities","registrationEndpoint","authorization_endpoint","authorizationEndpoint","token_endpoint","tokenEndpoint","introspection_endpoint","introspectionEndpoint","length","scopes_supported","resolveCapabilitiesFromAuthorizationServer","authServerUrl","allowLoopback","probeAuthCapabilities","baseUrl","normalizedBaseUrl","resourceMetadata","authorization_servers","
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/capability-discovery.ts"],"sourcesContent":["/**\n * OAuth Server Capability Discovery\n * Probes RFC 9728 (Protected Resource) and RFC 8414 (Authorization Server) metadata\n */\n\nimport { normalizeUrl } from '../lib/url-utils.ts';\nimport { isLoopbackUrl } from './discovery-fetch.ts';\nimport { discoverAuthorizationServerIssuer, discoverAuthorizationServerMetadata, discoverProtectedResourceMetadata } from './rfc9728-discovery.ts';\nimport type { AuthCapabilities, AuthorizationServerMetadata } from './types.ts';\n\n/**\n * Extract origin (protocol + host) from a URL\n * @param url - Full URL that may include a path\n * @returns Origin (e.g., \"https://example.com\") or original string if invalid URL\n *\n * @example\n * getOrigin('https://example.com/mcp') // → 'https://example.com'\n * getOrigin('http://localhost:9999/api/v1/mcp') // → 'http://localhost:9999'\n */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n // Invalid URL - return as-is for graceful degradation\n return url;\n }\n}\n\n/**\n * Probe OAuth server capabilities using RFC 9728 → RFC 8414 discovery chain\n * Returns capabilities including DCR support detection\n *\n * Discovery Strategy:\n * 1. Try RFC 9728 Protected Resource Metadata (supports cross-domain OAuth)\n * 2. If found, use first authorization_server to discover RFC 8414 Authorization Server Metadata\n * 3. Fall back to direct RFC 8414 discovery at resource origin\n *\n * @param baseUrl - Base URL of the protected resource (e.g., https://ai.todoist.net/mcp)\n * @returns AuthCapabilities object with discovered endpoints and features\n *\n * @example\n * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com\n * const caps = await probeAuthCapabilities('https://ai.todoist.net/mcp');\n * if (caps.supportsDcr) {\n * console.log('Registration endpoint:', caps.registrationEndpoint);\n * }\n */\nfunction buildCapabilities(metadata: AuthorizationServerMetadata, scopes?: string[]): AuthCapabilities {\n const supportsDcr = !!metadata.registration_endpoint;\n const capabilities: AuthCapabilities = { supportsDcr, authorizationResponseIssSupported: metadata.authorization_response_iss_parameter_supported === true };\n\n if (metadata.issuer) {\n capabilities.issuer = metadata.issuer;\n }\n\n if (metadata.registration_endpoint) {\n capabilities.registrationEndpoint = metadata.registration_endpoint;\n }\n if (metadata.authorization_endpoint) {\n capabilities.authorizationEndpoint = metadata.authorization_endpoint;\n }\n if (metadata.token_endpoint) capabilities.tokenEndpoint = metadata.token_endpoint;\n if (metadata.introspection_endpoint) {\n capabilities.introspectionEndpoint = metadata.introspection_endpoint;\n }\n\n if (scopes && scopes.length > 0) {\n capabilities.scopes = scopes;\n } else if (metadata.scopes_supported) {\n capabilities.scopes = metadata.scopes_supported;\n }\n\n return capabilities;\n}\n\nasync function resolveCapabilitiesFromAuthorizationServer(authServerUrl: string, scopes: string[] | undefined, allowLoopback: boolean): Promise<AuthCapabilities | null> {\n const metadata = await discoverAuthorizationServerMetadata(authServerUrl, { allowLoopback });\n if (!metadata) return null;\n return buildCapabilities(metadata, scopes);\n}\n\nexport async function probeAuthCapabilities(baseUrl: string): Promise<AuthCapabilities> {\n try {\n const normalizedBaseUrl = normalizeUrl(baseUrl);\n // Trust signal for every authorization-server fetch below: whether the\n // configured MCP server is itself loopback, never a remote-supplied URL.\n const allowLoopback = isLoopbackUrl(normalizedBaseUrl);\n // Strategy 1: Try RFC 9728 Protected Resource Metadata discovery\n // This handles cross-domain OAuth (e.g., Todoist: ai.todoist.net/mcp → todoist.com)\n const resourceMetadata = await discoverProtectedResourceMetadata(normalizedBaseUrl);\n\n if (resourceMetadata && resourceMetadata.authorization_servers.length > 0) {\n // Found protected resource metadata with authorization servers\n // Discover the authorization server's metadata (RFC 8414)\n const authServerUrl = resourceMetadata.authorization_servers[0];\n if (!authServerUrl) {\n // Array has length > 0 but first element is undefined/null - skip this path\n return { supportsDcr: false };\n }\n const capabilities = await resolveCapabilitiesFromAuthorizationServer(authServerUrl, resourceMetadata.scopes_supported, allowLoopback);\n if (capabilities) {\n return capabilities;\n }\n\n const issuer = await discoverAuthorizationServerIssuer(baseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer, resourceMetadata.scopes_supported, allowLoopback);\n if (issuerCapabilities) return issuerCapabilities;\n }\n }\n\n const issuer = await discoverAuthorizationServerIssuer(normalizedBaseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer, undefined, allowLoopback);\n if (issuerCapabilities) return issuerCapabilities;\n }\n\n // Strategy 2: Fall back to direct RFC 8414 discovery at resource origin\n // This handles same-domain OAuth (traditional setup)\n const origin = getOrigin(normalizedBaseUrl);\n const originCapabilities = await resolveCapabilitiesFromAuthorizationServer(origin, undefined, allowLoopback);\n if (originCapabilities) return originCapabilities;\n\n // No OAuth metadata found\n return { supportsDcr: false };\n } catch (_error) {\n // Network error, invalid JSON, or other fetch failure\n // Gracefully degrade - assume no DCR support\n return { supportsDcr: false };\n }\n}\n"],"names":["normalizeUrl","isLoopbackUrl","discoverAuthorizationServerIssuer","discoverAuthorizationServerMetadata","discoverProtectedResourceMetadata","getOrigin","url","URL","origin","buildCapabilities","metadata","scopes","supportsDcr","registration_endpoint","capabilities","authorizationResponseIssSupported","authorization_response_iss_parameter_supported","issuer","registrationEndpoint","authorization_endpoint","authorizationEndpoint","token_endpoint","tokenEndpoint","introspection_endpoint","introspectionEndpoint","length","scopes_supported","resolveCapabilitiesFromAuthorizationServer","authServerUrl","allowLoopback","probeAuthCapabilities","baseUrl","normalizedBaseUrl","resourceMetadata","authorization_servers","issuerCapabilities","undefined","originCapabilities","_error"],"mappings":"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"}
|
|
@@ -29,12 +29,18 @@ export declare class InteractiveOAuthFlow {
|
|
|
29
29
|
* 'https://example.com/oauth/token',
|
|
30
30
|
* 'client-id',
|
|
31
31
|
* 'client-secret',
|
|
32
|
-
* { port, scopes: ['read', 'write'] }
|
|
32
|
+
* { port, issuer: 'https://example.com', resource: 'https://example.com/mcp', scopes: ['read', 'write'] }
|
|
33
33
|
* );
|
|
34
34
|
*/
|
|
35
35
|
performAuthFlow(authorizationEndpoint: string, tokenEndpoint: string, clientId: string, clientSecret: string, options: OAuthFlowOptions): Promise<TokenSet>;
|
|
36
|
+
/**
|
|
37
|
+
* Rejects an authorization response that was not minted by the issuer
|
|
38
|
+
* discovered before the flow started (RFC 9207 authorization-server mix-up).
|
|
39
|
+
*/
|
|
40
|
+
private assertResponseIssuer;
|
|
36
41
|
/**
|
|
37
42
|
* Exchanges an authorization code for access and refresh tokens.
|
|
43
|
+
* @param resource - Canonical resource server URI, audience-binding the token (RFC 8707).
|
|
38
44
|
* @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`.
|
|
39
45
|
* @param codeVerifier - Optional PKCE code verifier (RFC 7636).
|
|
40
46
|
*/
|
|
@@ -45,11 +51,12 @@ export declare class InteractiveOAuthFlow {
|
|
|
45
51
|
* @param refreshToken - Refresh token from a previous token set.
|
|
46
52
|
* @param clientId - OAuth client ID.
|
|
47
53
|
* @param clientSecret - OAuth client secret.
|
|
54
|
+
* @param resource - Canonical resource server URI, audience-binding the token (RFC 8707).
|
|
48
55
|
* @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`. Defaults to `false`.
|
|
49
56
|
* @returns New token set with a refreshed access token.
|
|
50
57
|
* @throws Error if refresh fails.
|
|
51
58
|
*/
|
|
52
|
-
refreshTokens(tokenEndpoint: string, refreshToken: string, clientId: string, clientSecret: string, allowLoopback?: boolean): Promise<TokenSet>;
|
|
59
|
+
refreshTokens(tokenEndpoint: string, refreshToken: string, clientId: string, clientSecret: string, resource: string, allowLoopback?: boolean): Promise<TokenSet>;
|
|
53
60
|
/**
|
|
54
61
|
* Open browser to authorization URL
|
|
55
62
|
* Uses platform-specific command to open default browser
|
|
@@ -31,7 +31,7 @@ import { generatePkce } from './pkce.js';
|
|
|
31
31
|
* 'https://example.com/oauth/token',
|
|
32
32
|
* 'client-id',
|
|
33
33
|
* 'client-secret',
|
|
34
|
-
* { port, scopes: ['read', 'write'] }
|
|
34
|
+
* { port, issuer: 'https://example.com', resource: 'https://example.com/mcp', scopes: ['read', 'write'] }
|
|
35
35
|
* );
|
|
36
36
|
*/ async performAuthFlow(authorizationEndpoint, tokenEndpoint, clientId, clientSecret, options) {
|
|
37
37
|
var _options_logger;
|
|
@@ -60,10 +60,8 @@ import { generatePkce } from './pkce.js';
|
|
|
60
60
|
if (options.scopes && options.scopes.length > 0) {
|
|
61
61
|
authUrl.searchParams.set('scope', options.scopes.join(' '));
|
|
62
62
|
}
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
authUrl.searchParams.set('resource', options.resource);
|
|
66
|
-
}
|
|
63
|
+
// Audience-bind the request to the resource server (RFC 8707)
|
|
64
|
+
authUrl.searchParams.set('resource', options.resource);
|
|
67
65
|
// Add PKCE parameters if generated (RFC 7636)
|
|
68
66
|
if (pkce) {
|
|
69
67
|
authUrl.searchParams.set('code_challenge', pkce.codeChallenge);
|
|
@@ -82,8 +80,9 @@ import { generatePkce } from './pkce.js';
|
|
|
82
80
|
// Wait for callback with timeout
|
|
83
81
|
const timeout = options.timeout || (options.headless ? 60000 : 300000);
|
|
84
82
|
const result = await callbackListener.waitForCallback(timeout);
|
|
83
|
+
this.assertResponseIssuer(result.iss, options, logger);
|
|
85
84
|
// Exchange authorization code for tokens (with PKCE verifier if used)
|
|
86
|
-
const tokens = await this.exchangeCodeForTokens(tokenEndpoint, result.code, clientId, clientSecret, redirectUri, (_options_allowLoopback = options.allowLoopback) !== null && _options_allowLoopback !== void 0 ? _options_allowLoopback : false, pkce === null || pkce === void 0 ? void 0 : pkce.codeVerifier);
|
|
85
|
+
const tokens = await this.exchangeCodeForTokens(tokenEndpoint, result.code, clientId, clientSecret, redirectUri, options.resource, (_options_allowLoopback = options.allowLoopback) !== null && _options_allowLoopback !== void 0 ? _options_allowLoopback : false, pkce === null || pkce === void 0 ? void 0 : pkce.codeVerifier);
|
|
87
86
|
return tokens;
|
|
88
87
|
} catch (error) {
|
|
89
88
|
logger.error('❌ OAuth flow failed:', error instanceof Error ? error.message : String(error));
|
|
@@ -94,16 +93,33 @@ import { generatePkce } from './pkce.js';
|
|
|
94
93
|
}
|
|
95
94
|
}
|
|
96
95
|
/**
|
|
96
|
+
* Rejects an authorization response that was not minted by the issuer
|
|
97
|
+
* discovered before the flow started (RFC 9207 authorization-server mix-up).
|
|
98
|
+
*/ assertResponseIssuer(iss, options, logger) {
|
|
99
|
+
if (iss !== undefined) {
|
|
100
|
+
if (iss !== options.issuer) {
|
|
101
|
+
throw new Error(`Authorization response issuer mismatch: got '${iss}', expected '${options.issuer}' - refusing to redeem the authorization code`);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (options.authorizationResponseIssSupported) {
|
|
106
|
+
throw new Error(`Authorization server '${options.issuer}' advertises authorization_response_iss_parameter_supported but omitted 'iss' - refusing to redeem the authorization code`);
|
|
107
|
+
}
|
|
108
|
+
logger.debug(`⚠️ Authorization response carried no 'iss' and '${options.issuer}' does not advertise support for it (RFC 9207)`);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
97
111
|
* Exchanges an authorization code for access and refresh tokens.
|
|
112
|
+
* @param resource - Canonical resource server URI, audience-binding the token (RFC 8707).
|
|
98
113
|
* @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`.
|
|
99
114
|
* @param codeVerifier - Optional PKCE code verifier (RFC 7636).
|
|
100
|
-
*/ async exchangeCodeForTokens(tokenEndpoint, code, clientId, clientSecret, redirectUri, allowLoopback, codeVerifier) {
|
|
115
|
+
*/ async exchangeCodeForTokens(tokenEndpoint, code, clientId, clientSecret, redirectUri, resource, allowLoopback, codeVerifier) {
|
|
101
116
|
const params = new URLSearchParams({
|
|
102
117
|
grant_type: 'authorization_code',
|
|
103
118
|
code,
|
|
104
119
|
redirect_uri: redirectUri,
|
|
105
120
|
client_id: clientId,
|
|
106
|
-
client_secret: clientSecret
|
|
121
|
+
client_secret: clientSecret,
|
|
122
|
+
resource
|
|
107
123
|
});
|
|
108
124
|
// Add PKCE code verifier if provided (RFC 7636)
|
|
109
125
|
if (codeVerifier) {
|
|
@@ -148,10 +164,11 @@ import { generatePkce } from './pkce.js';
|
|
|
148
164
|
* @param refreshToken - Refresh token from a previous token set.
|
|
149
165
|
* @param clientId - OAuth client ID.
|
|
150
166
|
* @param clientSecret - OAuth client secret.
|
|
167
|
+
* @param resource - Canonical resource server URI, audience-binding the token (RFC 8707).
|
|
151
168
|
* @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`. Defaults to `false`.
|
|
152
169
|
* @returns New token set with a refreshed access token.
|
|
153
170
|
* @throws Error if refresh fails.
|
|
154
|
-
*/ async refreshTokens(tokenEndpoint, refreshToken, clientId, clientSecret, allowLoopback = false) {
|
|
171
|
+
*/ async refreshTokens(tokenEndpoint, refreshToken, clientId, clientSecret, resource, allowLoopback = false) {
|
|
155
172
|
// See exchangeCodeForTokens - tokenEndpoint is remote-controlled discovery data.
|
|
156
173
|
const response = await discoveryFetch(tokenEndpoint, {
|
|
157
174
|
method: 'POST',
|
|
@@ -164,7 +181,8 @@ import { generatePkce } from './pkce.js';
|
|
|
164
181
|
grant_type: 'refresh_token',
|
|
165
182
|
refresh_token: refreshToken,
|
|
166
183
|
client_id: clientId,
|
|
167
|
-
client_secret: clientSecret
|
|
184
|
+
client_secret: clientSecret,
|
|
185
|
+
resource
|
|
168
186
|
})
|
|
169
187
|
}, 'token endpoint', {
|
|
170
188
|
allowLoopback
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/interactive-oauth-flow.ts"],"sourcesContent":["/**\n * OAuth Authorization Flow Handler\n * Manages browser-based OAuth flows and token exchange with PKCE support\n */\n\nimport * as child_process from 'node:child_process';\nimport { logger as defaultLogger } from '../utils/logger.ts';\nimport { discoveryFetch } from './discovery-fetch.ts';\nimport { OAuthCallbackListener } from './oauth-callback-listener.ts';\nimport { generatePkce } from './pkce.ts';\nimport type { OAuthFlowOptions, PkceParams, TokenSet } from './types.ts';\n\n/**\n * OAuth token response from token endpoint\n */\ninterface TokenResponse {\n access_token: string;\n refresh_token?: string;\n expires_in: number;\n scope?: string;\n token_type?: string;\n}\n\n/**\n * InteractiveOAuthFlow manages the complete OAuth authorization code flow\n */\nexport class InteractiveOAuthFlow {\n /**\n * Perform OAuth authorization code flow\n *\n * @param authorizationEndpoint - OAuth authorization endpoint URL\n * @param tokenEndpoint - OAuth token endpoint URL\n * @param clientId - OAuth client ID\n * @param clientSecret - OAuth client secret\n * @param options - Flow options (port is required - use get-port to find available port)\n * @returns Token set with access and refresh tokens\n *\n * @throws Error if flow fails or times out\n *\n * @example\n * import getPort from 'get-port';\n *\n * const flow = new InteractiveOAuthFlow();\n * const port = await getPort();\n * const tokens = await flow.performAuthFlow(\n * 'https://example.com/oauth/authorize',\n * 'https://example.com/oauth/token',\n * 'client-id',\n * 'client-secret',\n * { port, scopes: ['read', 'write'] }\n * );\n */\n async performAuthFlow(authorizationEndpoint: string, tokenEndpoint: string, clientId: string, clientSecret: string, options: OAuthFlowOptions): Promise<TokenSet> {\n const logger = options.logger ?? defaultLogger;\n const callbackListener = new OAuthCallbackListener({ port: options.port, logger });\n\n // Generate PKCE parameters if requested (RFC 7636)\n let pkce: PkceParams | undefined;\n if (options.pkce) {\n logger.debug('🔐 Generating PKCE parameters...');\n pkce = await generatePkce();\n }\n\n try {\n // Start callback server\n await callbackListener.start();\n\n // Build redirect URI\n const redirectUri = options.redirectUri || `http://localhost:${options.port}/callback`;\n\n // Build authorization URL\n const authUrl = new URL(authorizationEndpoint);\n authUrl.searchParams.set('client_id', clientId);\n authUrl.searchParams.set('redirect_uri', redirectUri);\n authUrl.searchParams.set('response_type', 'code');\n\n if (options.scopes && options.scopes.length > 0) {\n authUrl.searchParams.set('scope', options.scopes.join(' '));\n }\n\n // Add resource parameter if specified (RFC 8707)\n if (options.resource) {\n authUrl.searchParams.set('resource', options.resource);\n }\n\n // Add PKCE parameters if generated (RFC 7636)\n if (pkce) {\n authUrl.searchParams.set('code_challenge', pkce.codeChallenge);\n authUrl.searchParams.set('code_challenge_method', pkce.codeChallengeMethod);\n }\n\n // Open browser or print URL for headless mode\n if (options.headless) {\n logger.info('🔗 Please visit this URL to authorize:');\n logger.info(authUrl.toString());\n logger.info('Waiting for callback...');\n } else {\n logger.debug('🌐 Opening browser for OAuth authorization...');\n // Try to open browser (requires 'open' package or native command)\n await this.openBrowser(authUrl.toString());\n }\n\n // Wait for callback with timeout\n const timeout = options.timeout || (options.headless ? 60000 : 300000);\n const result = await callbackListener.waitForCallback(timeout);\n\n // Exchange authorization code for tokens (with PKCE verifier if used)\n const tokens = await this.exchangeCodeForTokens(tokenEndpoint, result.code, clientId, clientSecret, redirectUri, options.allowLoopback ?? false, pkce?.codeVerifier);\n\n return tokens;\n } catch (error) {\n logger.error('❌ OAuth flow failed:', error instanceof Error ? error.message : String(error));\n throw error;\n } finally {\n // Always close callback server\n await callbackListener.stop();\n }\n }\n\n /**\n * Exchanges an authorization code for access and refresh tokens.\n * @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`.\n * @param codeVerifier - Optional PKCE code verifier (RFC 7636).\n */\n private async exchangeCodeForTokens(tokenEndpoint: string, code: string, clientId: string, clientSecret: string, redirectUri: string, allowLoopback: boolean, codeVerifier?: string): Promise<TokenSet> {\n const params = new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: redirectUri,\n client_id: clientId,\n client_secret: clientSecret,\n });\n\n // Add PKCE code verifier if provided (RFC 7636)\n if (codeVerifier) {\n params.set('code_verifier', codeVerifier);\n }\n\n // tokenEndpoint is remote-controlled discovery data; discoveryFetch blocks\n // a private/internal target before the client secret is sent to it.\n const response = await discoveryFetch(\n tokenEndpoint,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n Connection: 'close',\n },\n body: params,\n },\n 'token endpoint',\n { allowLoopback }\n );\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token exchange failed (${response.status}): ${errorText}`);\n }\n\n const data = (await response.json()) as TokenResponse;\n\n if (!data.access_token) {\n throw new Error('Token response missing access_token');\n }\n\n const tokenSet: TokenSet = {\n accessToken: data.access_token,\n refreshToken: data.refresh_token || '',\n expiresAt: Date.now() + data.expires_in * 1000,\n clientId,\n clientSecret,\n };\n\n if (data.scope) {\n tokenSet.scopes = data.scope.split(' ');\n }\n\n return tokenSet;\n }\n\n /**\n * Refreshes an access token using a refresh token.\n * @param tokenEndpoint - OAuth token endpoint URL.\n * @param refreshToken - Refresh token from a previous token set.\n * @param clientId - OAuth client ID.\n * @param clientSecret - OAuth client secret.\n * @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`. Defaults to `false`.\n * @returns New token set with a refreshed access token.\n * @throws Error if refresh fails.\n */\n async refreshTokens(tokenEndpoint: string, refreshToken: string, clientId: string, clientSecret: string, allowLoopback = false): Promise<TokenSet> {\n // See exchangeCodeForTokens - tokenEndpoint is remote-controlled discovery data.\n const response = await discoveryFetch(\n tokenEndpoint,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n Connection: 'close',\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: clientId,\n client_secret: clientSecret,\n }),\n },\n 'token endpoint',\n { allowLoopback }\n );\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token refresh failed (${response.status}): ${errorText}`);\n }\n\n const data = (await response.json()) as TokenResponse;\n\n if (!data.access_token) {\n throw new Error('Token refresh response missing access_token');\n }\n\n const tokenSet: TokenSet = {\n accessToken: data.access_token,\n refreshToken: data.refresh_token || refreshToken, // Reuse old refresh token if not provided\n expiresAt: Date.now() + data.expires_in * 1000,\n clientId,\n clientSecret,\n };\n\n if (data.scope) {\n tokenSet.scopes = data.scope.split(' ');\n }\n\n return tokenSet;\n }\n\n /**\n * Open browser to authorization URL\n * Uses platform-specific command to open default browser\n */\n private async openBrowser(url: string): Promise<void> {\n // Determine platform-specific command\n const platform = process.platform;\n let command: string;\n let args: string[];\n\n if (platform === 'darwin') {\n command = 'open';\n args = [url];\n } else if (platform === 'win32') {\n command = 'cmd';\n args = ['/c', 'start', url];\n } else {\n // Linux and others\n command = 'xdg-open';\n args = [url];\n }\n\n // Spawn browser process\n const child = child_process.spawn(command, args, {\n detached: true,\n stdio: 'ignore',\n });\n\n child.unref();\n }\n}\n"],"names":["child_process","logger","defaultLogger","discoveryFetch","OAuthCallbackListener","generatePkce","InteractiveOAuthFlow","performAuthFlow","authorizationEndpoint","tokenEndpoint","clientId","clientSecret","options","callbackListener","port","pkce","debug","start","redirectUri","authUrl","URL","searchParams","set","scopes","length","join","resource","codeChallenge","codeChallengeMethod","headless","info","toString","openBrowser","timeout","result","waitForCallback","tokens","exchangeCodeForTokens","code","allowLoopback","codeVerifier","error","Error","message","String","stop","params","URLSearchParams","grant_type","redirect_uri","client_id","client_secret","response","method","headers","Accept","Connection","body","ok","errorText","text","status","data","json","access_token","tokenSet","accessToken","refreshToken","refresh_token","expiresAt","Date","now","expires_in","scope","split","refreshTokens","url","platform","process","command","args","child","spawn","detached","stdio","unref"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/interactive-oauth-flow.ts"],"sourcesContent":["/**\n * OAuth Authorization Flow Handler\n * Manages browser-based OAuth flows and token exchange with PKCE support\n */\n\nimport * as child_process from 'node:child_process';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport { discoveryFetch } from './discovery-fetch.ts';\nimport { OAuthCallbackListener } from './oauth-callback-listener.ts';\nimport { generatePkce } from './pkce.ts';\nimport type { OAuthFlowOptions, PkceParams, TokenSet } from './types.ts';\n\n/**\n * OAuth token response from token endpoint\n */\ninterface TokenResponse {\n access_token: string;\n refresh_token?: string;\n expires_in: number;\n scope?: string;\n token_type?: string;\n}\n\n/**\n * InteractiveOAuthFlow manages the complete OAuth authorization code flow\n */\nexport class InteractiveOAuthFlow {\n /**\n * Perform OAuth authorization code flow\n *\n * @param authorizationEndpoint - OAuth authorization endpoint URL\n * @param tokenEndpoint - OAuth token endpoint URL\n * @param clientId - OAuth client ID\n * @param clientSecret - OAuth client secret\n * @param options - Flow options (port is required - use get-port to find available port)\n * @returns Token set with access and refresh tokens\n *\n * @throws Error if flow fails or times out\n *\n * @example\n * import getPort from 'get-port';\n *\n * const flow = new InteractiveOAuthFlow();\n * const port = await getPort();\n * const tokens = await flow.performAuthFlow(\n * 'https://example.com/oauth/authorize',\n * 'https://example.com/oauth/token',\n * 'client-id',\n * 'client-secret',\n * { port, issuer: 'https://example.com', resource: 'https://example.com/mcp', scopes: ['read', 'write'] }\n * );\n */\n async performAuthFlow(authorizationEndpoint: string, tokenEndpoint: string, clientId: string, clientSecret: string, options: OAuthFlowOptions): Promise<TokenSet> {\n const logger = options.logger ?? defaultLogger;\n const callbackListener = new OAuthCallbackListener({ port: options.port, logger });\n\n // Generate PKCE parameters if requested (RFC 7636)\n let pkce: PkceParams | undefined;\n if (options.pkce) {\n logger.debug('🔐 Generating PKCE parameters...');\n pkce = await generatePkce();\n }\n\n try {\n // Start callback server\n await callbackListener.start();\n\n // Build redirect URI\n const redirectUri = options.redirectUri || `http://localhost:${options.port}/callback`;\n\n // Build authorization URL\n const authUrl = new URL(authorizationEndpoint);\n authUrl.searchParams.set('client_id', clientId);\n authUrl.searchParams.set('redirect_uri', redirectUri);\n authUrl.searchParams.set('response_type', 'code');\n\n if (options.scopes && options.scopes.length > 0) {\n authUrl.searchParams.set('scope', options.scopes.join(' '));\n }\n\n // Audience-bind the request to the resource server (RFC 8707)\n authUrl.searchParams.set('resource', options.resource);\n\n // Add PKCE parameters if generated (RFC 7636)\n if (pkce) {\n authUrl.searchParams.set('code_challenge', pkce.codeChallenge);\n authUrl.searchParams.set('code_challenge_method', pkce.codeChallengeMethod);\n }\n\n // Open browser or print URL for headless mode\n if (options.headless) {\n logger.info('🔗 Please visit this URL to authorize:');\n logger.info(authUrl.toString());\n logger.info('Waiting for callback...');\n } else {\n logger.debug('🌐 Opening browser for OAuth authorization...');\n // Try to open browser (requires 'open' package or native command)\n await this.openBrowser(authUrl.toString());\n }\n\n // Wait for callback with timeout\n const timeout = options.timeout || (options.headless ? 60000 : 300000);\n const result = await callbackListener.waitForCallback(timeout);\n\n this.assertResponseIssuer(result.iss, options, logger);\n\n // Exchange authorization code for tokens (with PKCE verifier if used)\n const tokens = await this.exchangeCodeForTokens(tokenEndpoint, result.code, clientId, clientSecret, redirectUri, options.resource, options.allowLoopback ?? false, pkce?.codeVerifier);\n\n return tokens;\n } catch (error) {\n logger.error('❌ OAuth flow failed:', error instanceof Error ? error.message : String(error));\n throw error;\n } finally {\n // Always close callback server\n await callbackListener.stop();\n }\n }\n\n /**\n * Rejects an authorization response that was not minted by the issuer\n * discovered before the flow started (RFC 9207 authorization-server mix-up).\n */\n private assertResponseIssuer(iss: string | undefined, options: OAuthFlowOptions, logger: Logger): void {\n if (iss !== undefined) {\n if (iss !== options.issuer) {\n throw new Error(`Authorization response issuer mismatch: got '${iss}', expected '${options.issuer}' - refusing to redeem the authorization code`);\n }\n return;\n }\n\n if (options.authorizationResponseIssSupported) {\n throw new Error(`Authorization server '${options.issuer}' advertises authorization_response_iss_parameter_supported but omitted 'iss' - refusing to redeem the authorization code`);\n }\n\n logger.debug(`⚠️ Authorization response carried no 'iss' and '${options.issuer}' does not advertise support for it (RFC 9207)`);\n }\n\n /**\n * Exchanges an authorization code for access and refresh tokens.\n * @param resource - Canonical resource server URI, audience-binding the token (RFC 8707).\n * @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`.\n * @param codeVerifier - Optional PKCE code verifier (RFC 7636).\n */\n private async exchangeCodeForTokens(tokenEndpoint: string, code: string, clientId: string, clientSecret: string, redirectUri: string, resource: string, allowLoopback: boolean, codeVerifier?: string): Promise<TokenSet> {\n const params = new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: redirectUri,\n client_id: clientId,\n client_secret: clientSecret,\n resource,\n });\n\n // Add PKCE code verifier if provided (RFC 7636)\n if (codeVerifier) {\n params.set('code_verifier', codeVerifier);\n }\n\n // tokenEndpoint is remote-controlled discovery data; discoveryFetch blocks\n // a private/internal target before the client secret is sent to it.\n const response = await discoveryFetch(\n tokenEndpoint,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n Connection: 'close',\n },\n body: params,\n },\n 'token endpoint',\n { allowLoopback }\n );\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token exchange failed (${response.status}): ${errorText}`);\n }\n\n const data = (await response.json()) as TokenResponse;\n\n if (!data.access_token) {\n throw new Error('Token response missing access_token');\n }\n\n const tokenSet: TokenSet = {\n accessToken: data.access_token,\n refreshToken: data.refresh_token || '',\n expiresAt: Date.now() + data.expires_in * 1000,\n clientId,\n clientSecret,\n };\n\n if (data.scope) {\n tokenSet.scopes = data.scope.split(' ');\n }\n\n return tokenSet;\n }\n\n /**\n * Refreshes an access token using a refresh token.\n * @param tokenEndpoint - OAuth token endpoint URL.\n * @param refreshToken - Refresh token from a previous token set.\n * @param clientId - OAuth client ID.\n * @param clientSecret - OAuth client secret.\n * @param resource - Canonical resource server URI, audience-binding the token (RFC 8707).\n * @param allowLoopback - Loopback trust grant computed by the caller from the server it is actually talking to, never from `tokenEndpoint`. Defaults to `false`.\n * @returns New token set with a refreshed access token.\n * @throws Error if refresh fails.\n */\n async refreshTokens(tokenEndpoint: string, refreshToken: string, clientId: string, clientSecret: string, resource: string, allowLoopback = false): Promise<TokenSet> {\n // See exchangeCodeForTokens - tokenEndpoint is remote-controlled discovery data.\n const response = await discoveryFetch(\n tokenEndpoint,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n Connection: 'close',\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: clientId,\n client_secret: clientSecret,\n resource,\n }),\n },\n 'token endpoint',\n { allowLoopback }\n );\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token refresh failed (${response.status}): ${errorText}`);\n }\n\n const data = (await response.json()) as TokenResponse;\n\n if (!data.access_token) {\n throw new Error('Token refresh response missing access_token');\n }\n\n const tokenSet: TokenSet = {\n accessToken: data.access_token,\n refreshToken: data.refresh_token || refreshToken, // Reuse old refresh token if not provided\n expiresAt: Date.now() + data.expires_in * 1000,\n clientId,\n clientSecret,\n };\n\n if (data.scope) {\n tokenSet.scopes = data.scope.split(' ');\n }\n\n return tokenSet;\n }\n\n /**\n * Open browser to authorization URL\n * Uses platform-specific command to open default browser\n */\n private async openBrowser(url: string): Promise<void> {\n // Determine platform-specific command\n const platform = process.platform;\n let command: string;\n let args: string[];\n\n if (platform === 'darwin') {\n command = 'open';\n args = [url];\n } else if (platform === 'win32') {\n command = 'cmd';\n args = ['/c', 'start', url];\n } else {\n // Linux and others\n command = 'xdg-open';\n args = [url];\n }\n\n // Spawn browser process\n const child = child_process.spawn(command, args, {\n detached: true,\n stdio: 'ignore',\n });\n\n child.unref();\n }\n}\n"],"names":["child_process","logger","defaultLogger","discoveryFetch","OAuthCallbackListener","generatePkce","InteractiveOAuthFlow","performAuthFlow","authorizationEndpoint","tokenEndpoint","clientId","clientSecret","options","callbackListener","port","pkce","debug","start","redirectUri","authUrl","URL","searchParams","set","scopes","length","join","resource","codeChallenge","codeChallengeMethod","headless","info","toString","openBrowser","timeout","result","waitForCallback","assertResponseIssuer","iss","tokens","exchangeCodeForTokens","code","allowLoopback","codeVerifier","error","Error","message","String","stop","undefined","issuer","authorizationResponseIssSupported","params","URLSearchParams","grant_type","redirect_uri","client_id","client_secret","response","method","headers","Accept","Connection","body","ok","errorText","text","status","data","json","access_token","tokenSet","accessToken","refreshToken","refresh_token","expiresAt","Date","now","expires_in","scope","split","refreshTokens","url","platform","process","command","args","child","spawn","detached","stdio","unref"],"mappings":"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"}
|
|
@@ -49,6 +49,7 @@ import { logger as defaultLogger } from '../utils/logger.js';
|
|
|
49
49
|
*/ handleCallback(url, res) {
|
|
50
50
|
const code = url.searchParams.get('code');
|
|
51
51
|
const state = url.searchParams.get('state');
|
|
52
|
+
const iss = url.searchParams.get('iss');
|
|
52
53
|
const error = url.searchParams.get('error');
|
|
53
54
|
const errorDescription = url.searchParams.get('error_description');
|
|
54
55
|
// Handle OAuth errors
|
|
@@ -114,6 +115,9 @@ import { logger as defaultLogger } from '../utils/logger.js';
|
|
|
114
115
|
if (state) {
|
|
115
116
|
result.state = state;
|
|
116
117
|
}
|
|
118
|
+
if (iss) {
|
|
119
|
+
result.iss = iss;
|
|
120
|
+
}
|
|
117
121
|
this.resolveCallback(result);
|
|
118
122
|
}
|
|
119
123
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/oauth-callback-listener.ts"],"sourcesContent":["/**\n * OAuth Callback Server for CLI Authentication\n * Listens for OAuth authorization callbacks and captures authorization code\n */\n\nimport http from 'node:http';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport type { CallbackResult } from './types.ts';\n\nexport interface OAuthCallbackListenerOptions {\n /** Port to listen on (required - use get-port package to find available port) */\n port: number;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: Logger;\n}\n\n/**\n * OAuthCallbackListener handles OAuth redirect callbacks\n * Starts a temporary HTTP server to receive authorization code\n *\n * Note: Caller is responsible for finding an available port using get-port package\n */\nexport class OAuthCallbackListener {\n private server: http.Server | undefined;\n private resolveCallback?: (result: CallbackResult) => void;\n private rejectCallback?: (error: Error) => void;\n private timeout: NodeJS.Timeout | undefined;\n private port: number;\n private logger: Logger;\n\n constructor(options: OAuthCallbackListenerOptions) {\n this.port = options.port;\n this.logger = options.logger ?? defaultLogger;\n }\n\n /**\n * Start the callback server\n * Fails fast if port is already in use - caller should use get-port to find available port\n */\n async start(): Promise<void> {\n await this.listen(this.port);\n this.logger.debug(`✅ Callback server listening on http://localhost:${this.port}/callback`);\n }\n\n /**\n * Listen on a specific port\n */\n private listen(port: number): Promise<void> {\n return new Promise((resolve, reject) => {\n this.server = http.createServer((req, res) => {\n this.handleRequest(req, res);\n });\n\n this.server.on('error', (error) => {\n reject(error);\n });\n\n this.server.listen(port, () => {\n resolve();\n });\n });\n }\n\n /**\n * Handle incoming HTTP requests\n */\n private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {\n const url = new URL(req.url || '', `http://localhost:${this.port}`);\n\n if (url.pathname === '/callback') {\n this.handleCallback(url, res);\n } else {\n res.writeHead(404, { 'Content-Type': 'text/plain' });\n res.end('Not Found');\n }\n }\n\n /**\n * Handle OAuth callback\n */\n private handleCallback(url: URL, res: http.ServerResponse): void {\n const code = url.searchParams.get('code');\n const state = url.searchParams.get('state');\n const error = url.searchParams.get('error');\n const errorDescription = url.searchParams.get('error_description');\n\n // Handle OAuth errors\n if (error) {\n const errorMessage = errorDescription ? `${error}: ${errorDescription}` : error;\n\n res.writeHead(400, { 'Content-Type': 'text/html' });\n res.end(`\n <html>\n <body>\n <h1>Authorization Failed</h1>\n <p>${errorMessage}</p>\n <script>setTimeout(() => window.close(), 3000);</script>\n </body>\n </html>\n `);\n\n if (this.rejectCallback) {\n this.rejectCallback(new Error(errorMessage));\n }\n return;\n }\n\n // Validate code parameter\n if (!code) {\n res.writeHead(400, { 'Content-Type': 'text/html' });\n res.end(`\n <html>\n <body>\n <h1>Invalid Callback</h1>\n <p>Missing authorization code</p>\n <script>setTimeout(() => window.close(), 3000);</script>\n </body>\n </html>\n `);\n\n if (this.rejectCallback) {\n this.rejectCallback(new Error('Missing authorization code'));\n }\n return;\n }\n\n // Success - send confirmation page\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n res.end(`\n <html>\n <head>\n <meta charset=\"UTF-8\">\n </head>\n <body>\n <h1>Authorization Successful</h1>\n <p>You can close this window and return to the terminal.</p>\n <script>setTimeout(() => window.close(), 2000);</script>\n </body>\n </html>\n `);\n\n // Resolve the promise with authorization code\n if (this.resolveCallback) {\n const result: CallbackResult = { code };\n if (state) {\n result.state = state;\n }\n this.resolveCallback(result);\n }\n }\n\n /**\n * Wait for OAuth callback with timeout\n */\n async waitForCallback(timeoutMs = 300000): Promise<CallbackResult> {\n return new Promise((resolve, reject) => {\n this.resolveCallback = resolve;\n this.rejectCallback = reject;\n\n // Set timeout to prevent hanging forever\n this.timeout = setTimeout(() => {\n reject(new Error(`Authorization timeout - no callback received within ${timeoutMs / 1000} seconds`));\n this.stop();\n }, timeoutMs);\n });\n }\n\n /**\n * Stop the callback server and close\n */\n async stop(): Promise<void> {\n // Clear the timeout\n if (this.timeout) {\n clearTimeout(this.timeout);\n this.timeout = undefined;\n }\n\n // Close the server\n if (this.server) {\n await new Promise<void>((resolve) => {\n this.server?.close(() => {\n this.logger.debug('🔒 Callback server closed');\n resolve();\n });\n });\n this.server = undefined;\n }\n }\n\n /**\n * Get the callback URL for this server\n */\n getCallbackUrl(): string {\n if (!this.port) {\n throw new Error('Server not started - call start() first');\n }\n return `http://localhost:${this.port}/callback`;\n }\n}\n"],"names":["http","logger","defaultLogger","OAuthCallbackListener","start","listen","port","debug","Promise","resolve","reject","server","createServer","req","res","handleRequest","on","error","url","URL","pathname","handleCallback","writeHead","end","code","searchParams","get","state","errorDescription","errorMessage","rejectCallback","Error","resolveCallback","result","waitForCallback","timeoutMs","timeout","setTimeout","stop","clearTimeout","undefined","close","getCallbackUrl","options"],"mappings":"AAAA;;;CAGC,GAED,OAAOA,UAAU,YAAY;AAC7B,SAASC,UAAUC,aAAa,QAAqB,qBAAqB;AAU1E;;;;;CAKC,GACD,OAAO,MAAMC;IAaX;;;GAGC,GACD,MAAMC,QAAuB;QAC3B,MAAM,IAAI,CAACC,MAAM,CAAC,IAAI,CAACC,IAAI;QAC3B,IAAI,CAACL,MAAM,CAACM,KAAK,CAAC,CAAC,gDAAgD,EAAE,IAAI,CAACD,IAAI,CAAC,SAAS,CAAC;IAC3F;IAEA;;GAEC,GACD,AAAQD,OAAOC,IAAY,EAAiB;QAC1C,OAAO,IAAIE,QAAQ,CAACC,SAASC;YAC3B,IAAI,CAACC,MAAM,GAAGX,KAAKY,YAAY,CAAC,CAACC,KAAKC;gBACpC,IAAI,CAACC,aAAa,CAACF,KAAKC;YAC1B;YAEA,IAAI,CAACH,MAAM,CAACK,EAAE,CAAC,SAAS,CAACC;gBACvBP,OAAOO;YACT;YAEA,IAAI,CAACN,MAAM,CAACN,MAAM,CAACC,MAAM;gBACvBG;YACF;QACF;IACF;IAEA;;GAEC,GACD,AAAQM,cAAcF,GAAyB,EAAEC,GAAwB,EAAQ;QAC/E,MAAMI,MAAM,IAAIC,IAAIN,IAAIK,GAAG,IAAI,IAAI,CAAC,iBAAiB,EAAE,IAAI,CAACZ,IAAI,EAAE;QAElE,IAAIY,IAAIE,QAAQ,KAAK,aAAa;YAChC,IAAI,CAACC,cAAc,CAACH,KAAKJ;QAC3B,OAAO;YACLA,IAAIQ,SAAS,CAAC,KAAK;gBAAE,gBAAgB;YAAa;YAClDR,IAAIS,GAAG,CAAC;QACV;IACF;IAEA;;GAEC,GACD,AAAQF,eAAeH,GAAQ,EAAEJ,GAAwB,EAAQ;QAC/D,MAAMU,OAAON,IAAIO,YAAY,CAACC,GAAG,CAAC;QAClC,MAAMC,QAAQT,IAAIO,YAAY,CAACC,GAAG,CAAC;QACnC,MAAMT,QAAQC,IAAIO,YAAY,CAACC,GAAG,CAAC;QACnC,
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/oauth-callback-listener.ts"],"sourcesContent":["/**\n * OAuth Callback Server for CLI Authentication\n * Listens for OAuth authorization callbacks and captures authorization code\n */\n\nimport http from 'node:http';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport type { CallbackResult } from './types.ts';\n\nexport interface OAuthCallbackListenerOptions {\n /** Port to listen on (required - use get-port package to find available port) */\n port: number;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: Logger;\n}\n\n/**\n * OAuthCallbackListener handles OAuth redirect callbacks\n * Starts a temporary HTTP server to receive authorization code\n *\n * Note: Caller is responsible for finding an available port using get-port package\n */\nexport class OAuthCallbackListener {\n private server: http.Server | undefined;\n private resolveCallback?: (result: CallbackResult) => void;\n private rejectCallback?: (error: Error) => void;\n private timeout: NodeJS.Timeout | undefined;\n private port: number;\n private logger: Logger;\n\n constructor(options: OAuthCallbackListenerOptions) {\n this.port = options.port;\n this.logger = options.logger ?? defaultLogger;\n }\n\n /**\n * Start the callback server\n * Fails fast if port is already in use - caller should use get-port to find available port\n */\n async start(): Promise<void> {\n await this.listen(this.port);\n this.logger.debug(`✅ Callback server listening on http://localhost:${this.port}/callback`);\n }\n\n /**\n * Listen on a specific port\n */\n private listen(port: number): Promise<void> {\n return new Promise((resolve, reject) => {\n this.server = http.createServer((req, res) => {\n this.handleRequest(req, res);\n });\n\n this.server.on('error', (error) => {\n reject(error);\n });\n\n this.server.listen(port, () => {\n resolve();\n });\n });\n }\n\n /**\n * Handle incoming HTTP requests\n */\n private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {\n const url = new URL(req.url || '', `http://localhost:${this.port}`);\n\n if (url.pathname === '/callback') {\n this.handleCallback(url, res);\n } else {\n res.writeHead(404, { 'Content-Type': 'text/plain' });\n res.end('Not Found');\n }\n }\n\n /**\n * Handle OAuth callback\n */\n private handleCallback(url: URL, res: http.ServerResponse): void {\n const code = url.searchParams.get('code');\n const state = url.searchParams.get('state');\n const iss = url.searchParams.get('iss');\n const error = url.searchParams.get('error');\n const errorDescription = url.searchParams.get('error_description');\n\n // Handle OAuth errors\n if (error) {\n const errorMessage = errorDescription ? `${error}: ${errorDescription}` : error;\n\n res.writeHead(400, { 'Content-Type': 'text/html' });\n res.end(`\n <html>\n <body>\n <h1>Authorization Failed</h1>\n <p>${errorMessage}</p>\n <script>setTimeout(() => window.close(), 3000);</script>\n </body>\n </html>\n `);\n\n if (this.rejectCallback) {\n this.rejectCallback(new Error(errorMessage));\n }\n return;\n }\n\n // Validate code parameter\n if (!code) {\n res.writeHead(400, { 'Content-Type': 'text/html' });\n res.end(`\n <html>\n <body>\n <h1>Invalid Callback</h1>\n <p>Missing authorization code</p>\n <script>setTimeout(() => window.close(), 3000);</script>\n </body>\n </html>\n `);\n\n if (this.rejectCallback) {\n this.rejectCallback(new Error('Missing authorization code'));\n }\n return;\n }\n\n // Success - send confirmation page\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n res.end(`\n <html>\n <head>\n <meta charset=\"UTF-8\">\n </head>\n <body>\n <h1>Authorization Successful</h1>\n <p>You can close this window and return to the terminal.</p>\n <script>setTimeout(() => window.close(), 2000);</script>\n </body>\n </html>\n `);\n\n // Resolve the promise with authorization code\n if (this.resolveCallback) {\n const result: CallbackResult = { code };\n if (state) {\n result.state = state;\n }\n if (iss) {\n result.iss = iss;\n }\n this.resolveCallback(result);\n }\n }\n\n /**\n * Wait for OAuth callback with timeout\n */\n async waitForCallback(timeoutMs = 300000): Promise<CallbackResult> {\n return new Promise((resolve, reject) => {\n this.resolveCallback = resolve;\n this.rejectCallback = reject;\n\n // Set timeout to prevent hanging forever\n this.timeout = setTimeout(() => {\n reject(new Error(`Authorization timeout - no callback received within ${timeoutMs / 1000} seconds`));\n this.stop();\n }, timeoutMs);\n });\n }\n\n /**\n * Stop the callback server and close\n */\n async stop(): Promise<void> {\n // Clear the timeout\n if (this.timeout) {\n clearTimeout(this.timeout);\n this.timeout = undefined;\n }\n\n // Close the server\n if (this.server) {\n await new Promise<void>((resolve) => {\n this.server?.close(() => {\n this.logger.debug('🔒 Callback server closed');\n resolve();\n });\n });\n this.server = undefined;\n }\n }\n\n /**\n * Get the callback URL for this server\n */\n getCallbackUrl(): string {\n if (!this.port) {\n throw new Error('Server not started - call start() first');\n }\n return `http://localhost:${this.port}/callback`;\n }\n}\n"],"names":["http","logger","defaultLogger","OAuthCallbackListener","start","listen","port","debug","Promise","resolve","reject","server","createServer","req","res","handleRequest","on","error","url","URL","pathname","handleCallback","writeHead","end","code","searchParams","get","state","iss","errorDescription","errorMessage","rejectCallback","Error","resolveCallback","result","waitForCallback","timeoutMs","timeout","setTimeout","stop","clearTimeout","undefined","close","getCallbackUrl","options"],"mappings":"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"}
|