79 lines
2.3 KiB
TypeScript
79 lines
2.3 KiB
TypeScript
/**
|
|
* OAuth redirect port helpers — extracted from auth.ts to break the
|
|
* auth.ts ↔ xaaIdpLogin.ts circular dependency.
|
|
*/
|
|
import { createServer } from 'http'
|
|
import { getPlatform } from '../../utils/platform.js'
|
|
|
|
// Windows dynamic port range 49152-65535 is reserved
|
|
const REDIRECT_PORT_RANGE =
|
|
getPlatform() === 'windows'
|
|
? { min: 39152, max: 49151 }
|
|
: { min: 49152, max: 65535 }
|
|
const REDIRECT_PORT_FALLBACK = 3118
|
|
|
|
/**
|
|
* Builds a redirect URI on localhost with the given port and a fixed `/callback` path.
|
|
*
|
|
* RFC 8252 Section 7.3 (OAuth for Native Apps): loopback redirect URIs match any
|
|
* port as long as the path matches.
|
|
*/
|
|
export function buildRedirectUri(
|
|
port: number = REDIRECT_PORT_FALLBACK,
|
|
): string {
|
|
return `http://localhost:${port}/callback`
|
|
}
|
|
|
|
function getMcpOAuthCallbackPort(): number | undefined {
|
|
const port = parseInt(process.env.MCP_OAUTH_CALLBACK_PORT || '', 10)
|
|
return port > 0 ? port : undefined
|
|
}
|
|
|
|
/**
|
|
* Finds an available port in the specified range for OAuth redirect
|
|
* Uses random selection for better security
|
|
*/
|
|
export async function findAvailablePort(): Promise<number> {
|
|
// First, try the configured port if specified
|
|
const configuredPort = getMcpOAuthCallbackPort()
|
|
if (configuredPort) {
|
|
return configuredPort
|
|
}
|
|
|
|
const { min, max } = REDIRECT_PORT_RANGE
|
|
const range = max - min + 1
|
|
const maxAttempts = Math.min(range, 100) // Don't try forever
|
|
|
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
const port = min + Math.floor(Math.random() * range)
|
|
|
|
try {
|
|
await new Promise<void>((resolve, reject) => {
|
|
const testServer = createServer()
|
|
testServer.once('error', reject)
|
|
testServer.listen(port, () => {
|
|
testServer.close(() => resolve())
|
|
})
|
|
})
|
|
return port
|
|
} catch {
|
|
// Port in use, try another random port
|
|
continue
|
|
}
|
|
}
|
|
|
|
// If random selection failed, try the fallback port
|
|
try {
|
|
await new Promise<void>((resolve, reject) => {
|
|
const testServer = createServer()
|
|
testServer.once('error', reject)
|
|
testServer.listen(REDIRECT_PORT_FALLBACK, () => {
|
|
testServer.close(() => resolve())
|
|
})
|
|
})
|
|
return REDIRECT_PORT_FALLBACK
|
|
} catch {
|
|
throw new Error(`No available ports for OAuth redirect`)
|
|
}
|
|
}
|