Skip to content

Commit 6d73969

Browse files
committed
In the Google service, make the OAUTH_CALLBACK_PORT dynamic, based on availability. (vibe-kanban 3d2c8b20)
1 parent 78ca0bf commit 6d73969

1 file changed

Lines changed: 52 additions & 3 deletions

File tree

src/services/google.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import * as http from 'node:http';
1616
import * as url from 'node:url';
1717

1818
const DEFAULT_TIMEOUT_MS = 8000;
19-
const OAUTH_CALLBACK_PORT = 8080;
2019
const 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

Comments
 (0)