fix(openrouter): use the loopback's bound port in the OAuth callback_url (#3904)

Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
This commit is contained in:
ly-wang19
2026-06-22 01:43:53 -07:00
committed by GitHub
co-authored by ly-wang19
parent 9d37f1f6c1
commit 60350f0041
2 changed files with 40 additions and 2 deletions
@@ -31,8 +31,10 @@ describe('connectOpenRouterViaOAuth', () => {
expect(openExternalUrl).toHaveBeenCalledTimes(1);
const authUrl = new URL(openExternalUrl.mock.calls[0][0]);
expect(authUrl.origin + authUrl.pathname).toBe('https://openrouter.ai/auth');
// callback_url must use the port the listener actually bound to (carried in
// redirectUri), not the requested OPENROUTER_LOOPBACK_PORT constant.
expect(authUrl.searchParams.get('callback_url')).toBe(
'http://localhost:3000/auth?state=expected-state'
'http://localhost:53824/auth?state=expected-state'
);
expect(authUrl.searchParams.get('code_challenge_method')).toBe('S256');
expect(authUrl.searchParams.get('code_challenge')).toBeTruthy();
@@ -43,6 +45,37 @@ describe('connectOpenRouterViaOAuth', () => {
expect(cancel).toHaveBeenCalledTimes(1);
});
it('uses the listener bound port for callback_url when the requested port was busy', async () => {
// Port 3000 busy -> the Tauri listener falls back to an OS-assigned port and
// returns it in redirectUri. callback_url must point at that bound port, or
// OpenRouter redirects to a port nothing is listening on and OAuth fails.
const openExternalUrl = vi.fn().mockResolvedValue(undefined);
const startLoopbackListener = vi
.fn()
.mockResolvedValue({
redirectUri: 'http://127.0.0.1:54321/auth?state=expected-state',
state: 'expected-state',
awaitCallback: vi
.fn()
.mockResolvedValue('http://127.0.0.1:54321/auth?state=expected-state&code=abc123'),
cancel: vi.fn().mockResolvedValue(undefined),
});
const fetchImpl = vi
.fn()
.mockResolvedValue({ ok: true, json: async () => ({ key: 'sk-or-via-oauth' }) });
await connectOpenRouterViaOAuth({
startLoopbackListener,
openExternalUrl,
fetchImpl: fetchImpl as unknown as typeof fetch,
});
const authUrl = new URL(openExternalUrl.mock.calls[0][0]);
expect(authUrl.searchParams.get('callback_url')).toBe(
'http://localhost:54321/auth?state=expected-state'
);
});
it('rejects when the loopback listener is unavailable', async () => {
await expect(
connectOpenRouterViaOAuth({ startLoopbackListener: vi.fn().mockResolvedValue(null) })
+6 -1
View File
@@ -105,8 +105,13 @@ function toOpenRouterCallbackUrl(redirectUri: string): string {
throw new Error('OpenRouter OAuth listener returned an invalid redirect URL.');
}
// Preserve the port the loopback listener actually bound to (carried in
// redirectUri): when the requested port is busy, the Tauri command falls back
// to an OS-assigned ephemeral port, so hardcoding OPENROUTER_LOOPBACK_PORT here
// sent OpenRouter a callback_url pointing at the wrong port. The PKCE
// callback_url is per-request, so the dynamic port is valid (this matches the
// sibling OAuthProviderButton flow, which trusts the bound port).
parsed.hostname = 'localhost';
parsed.port = String(OPENROUTER_LOOPBACK_PORT);
return parsed.toString();
}