@@ -16,7 +16,6 @@ import * as http from 'node:http';
1616import * as url from 'node:url' ;
1717
1818const DEFAULT_TIMEOUT_MS = 8000 ;
19- const OAUTH_CALLBACK_PORT = 8080 ;
2019const OAUTH_SCOPES = [
2120 // User info (for credential validation)
2221 'https://www.googleapis.com/auth/userinfo.profile' ,
@@ -75,6 +74,54 @@ class OAuthTokenExchangeError extends Error {
7574 }
7675}
7776
77+ class PortUnavailableError extends Error {
78+ constructor ( message : string ) {
79+ super ( message ) ;
80+ this . name = 'PortUnavailableError' ;
81+ }
82+ }
83+
84+ /**
85+ * Find an available port starting from the specified port.
86+ * Tries ports sequentially until it finds one that's available.
87+ */
88+ async function findAvailablePort ( startPort : number , maxAttempts = 10 ) : Promise < number > {
89+ for ( let port = startPort ; port < startPort + maxAttempts ; port ++ ) {
90+ try {
91+ await new Promise < void > ( ( resolve , reject ) => {
92+ const testServer = http . createServer ( ) ;
93+
94+ testServer . once ( 'error' , ( error : NodeJS . ErrnoException ) => {
95+ if ( error . code === 'EADDRINUSE' ) {
96+ reject ( error ) ;
97+ } else {
98+ reject ( error ) ;
99+ }
100+ } ) ;
101+
102+ testServer . once ( 'listening' , ( ) => {
103+ testServer . close ( ( ) => {
104+ resolve ( ) ;
105+ } ) ;
106+ } ) ;
107+
108+ testServer . listen ( port , 'localhost' ) ;
109+ } ) ;
110+
111+ return port ;
112+ } catch ( error : unknown ) {
113+ if ( error instanceof Error && 'code' in error && error . code === 'EADDRINUSE' ) {
114+ continue ;
115+ }
116+ throw error ;
117+ }
118+ }
119+
120+ throw new PortUnavailableError (
121+ `Could not find an available port in range ${ startPort . toString ( ) } -${ ( startPort + maxAttempts - 1 ) . toString ( ) } `
122+ ) ;
123+ }
124+
78125/**
79126 * Start a temporary HTTP server to receive OAuth callback.
80127 * Returns a promise that resolves with the authorization code.
@@ -473,10 +520,12 @@ class GoogleServiceSession extends BrowserFollowupServiceSession {
473520 accessTokenExpiresAt ?: string ;
474521 refreshTokenExpiresAt ?: string ;
475522 } > {
476- const redirectUri = `http://localhost:${ OAUTH_CALLBACK_PORT . toString ( ) } /oauth2callback` ;
523+ // Find an available port starting from 8080
524+ const port = await findAvailablePort ( 8080 ) ;
525+ const redirectUri = `http://localhost:${ port . toString ( ) } /oauth2callback` ;
477526
478527 // Start the callback server
479- const serverPromise = startOAuthCallbackServer ( OAUTH_CALLBACK_PORT , 120000 ) ;
528+ const serverPromise = startOAuthCallbackServer ( port , 120000 ) ;
480529
481530 // Build OAuth authorization URL
482531 const authUrl = new URL ( 'https://accounts.google.com/o/oauth2/v2/auth' ) ;
0 commit comments