oauthPort.ts
1 /** 2 * OAuth redirect port helpers — extracted from auth.ts to break the 3 * auth.ts ↔ xaaIdpLogin.ts circular dependency. 4 */ 5 import { createServer } from 'http' 6 import { getPlatform } from '../../utils/platform.js' 7 8 // Windows dynamic port range 49152-65535 is reserved 9 const REDIRECT_PORT_RANGE = 10 getPlatform() === 'windows' 11 ? { min: 39152, max: 49151 } 12 : { min: 49152, max: 65535 } 13 const REDIRECT_PORT_FALLBACK = 3118 14 15 /** 16 * Builds a redirect URI on localhost with the given port and a fixed `/callback` path. 17 * 18 * RFC 8252 Section 7.3 (OAuth for Native Apps): loopback redirect URIs match any 19 * port as long as the path matches. 20 */ 21 export function buildRedirectUri( 22 port: number = REDIRECT_PORT_FALLBACK, 23 ): string { 24 return `http://localhost:${port}/callback` 25 } 26 27 function getMcpOAuthCallbackPort(): number | undefined { 28 const port = parseInt(process.env.MCP_OAUTH_CALLBACK_PORT || '', 10) 29 return port > 0 ? port : undefined 30 } 31 32 /** 33 * Finds an available port in the specified range for OAuth redirect 34 * Uses random selection for better security 35 */ 36 export async function findAvailablePort(): Promise<number> { 37 // First, try the configured port if specified 38 const configuredPort = getMcpOAuthCallbackPort() 39 if (configuredPort) { 40 return configuredPort 41 } 42 43 const { min, max } = REDIRECT_PORT_RANGE 44 const range = max - min + 1 45 const maxAttempts = Math.min(range, 100) // Don't try forever 46 47 for (let attempt = 0; attempt < maxAttempts; attempt++) { 48 const port = min + Math.floor(Math.random() * range) 49 50 try { 51 await new Promise<void>((resolve, reject) => { 52 const testServer = createServer() 53 testServer.once('error', reject) 54 testServer.listen(port, () => { 55 testServer.close(() => resolve()) 56 }) 57 }) 58 return port 59 } catch { 60 // Port in use, try another random port 61 continue 62 } 63 } 64 65 // If random selection failed, try the fallback port 66 try { 67 await new Promise<void>((resolve, reject) => { 68 const testServer = createServer() 69 testServer.once('error', reject) 70 testServer.listen(REDIRECT_PORT_FALLBACK, () => { 71 testServer.close(() => resolve()) 72 }) 73 }) 74 return REDIRECT_PORT_FALLBACK 75 } catch { 76 throw new Error(`No available ports for OAuth redirect`) 77 } 78 }