Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions packages/core/src/code_assist/oauth2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { UserAccountManager } from '../utils/userAccountManager.js';
import * as fs from 'node:fs';
import * as path from 'node:path';
import http from 'node:http';
import * as https from 'node:https';
import open from 'open';
import crypto from 'node:crypto';
import * as os from 'node:os';
Expand Down Expand Up @@ -410,6 +411,78 @@ describe('oauth2', () => {
expect(mockOAuth2Client.setCredentials).toHaveBeenCalledWith(mockTokens);
});

describe('transporter keep-alive (nodejs/node#63989 workaround)', () => {
const setupUserCodeClient = () => {
const mockOAuth2Client = {
generateAuthUrl: vi.fn().mockReturnValue('https://example.com/auth'),
getToken: vi.fn().mockResolvedValue({
tokens: {
access_token: 'test-access-token',
refresh_token: 'test-refresh-token',
},
}),
generateCodeVerifierAsync: vi.fn().mockResolvedValue({
codeChallenge: 'test-challenge',
codeVerifier: 'test-verifier',
}),
getAccessToken: vi.fn().mockResolvedValue({ token: 'test-token' }),
setCredentials: vi.fn(),
on: vi.fn(),
credentials: {},
} as unknown as OAuth2Client;
vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);
(readline.createInterface as Mock).mockReturnValue({
question: vi.fn((_query, callback) => callback('test-user-code')),
close: vi.fn(),
on: vi.fn(),
});
};

it('disables HTTP keep-alive when no proxy is configured', async () => {
setupUserCodeClient();
const config = {
getNoBrowser: () => true,
getProxy: () => undefined,
isBrowserLaunchSuppressed: () => true,
isInteractive: () => true,
} as unknown as Config;

await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, config);

const transporterOptions = (
vi.mocked(OAuth2Client).mock.calls[0]?.[0] as unknown as {
transporterOptions?: {
agent?: { keepAlive?: boolean };
proxy?: string;
};
}
)?.transporterOptions;
expect(transporterOptions?.agent).toBeInstanceOf(https.Agent);
expect(transporterOptions?.agent?.keepAlive).toBe(false);
expect(transporterOptions?.proxy).toBeUndefined();
});

it('keeps the proxy and no custom agent when a proxy is configured', async () => {
setupUserCodeClient();
const config = {
getNoBrowser: () => true,
getProxy: () => 'http://test.proxy.com:8080',
isBrowserLaunchSuppressed: () => true,
isInteractive: () => true,
} as unknown as Config;

await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, config);

const transporterOptions = (
vi.mocked(OAuth2Client).mock.calls[0]?.[0] as unknown as {
transporterOptions?: { agent?: unknown; proxy?: string };
}
)?.transporterOptions;
expect(transporterOptions?.proxy).toBe('http://test.proxy.com:8080');
expect(transporterOptions?.agent).toBeUndefined();
});
});

it('should cache Google Account when logging in with user code', async () => {
const mockConfigWithNoBrowser = {
getNoBrowser: () => true,
Expand Down
14 changes: 11 additions & 3 deletions packages/core/src/code_assist/oauth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type JWTInput,
} from 'google-auth-library';
import * as http from 'node:http';
import * as https from 'node:https';
import url from 'node:url';
import crypto from 'node:crypto';
import * as net from 'node:net';
Expand Down Expand Up @@ -140,12 +141,19 @@ async function initOauthClient(
}
}

// The CVE-2026-48931 fix regressed http.Agent keep-alive socket reuse in
// Node 24.17.0 / 22.23.0 / 26.3.1 (fixed in 24.18.0 / 22.23.1 / 26.x),
// making node-fetch (via gaxios) throw a spurious ERR_STREAM_PREMATURE_CLOSE
// during the OAuth token exchange. Disable keep-alive to avoid it; with a
// proxy we keep `proxy` instead, since our own agent would make gaxios
// ignore it. See https://github.com/nodejs/node/issues/63989
const proxy = config.getProxy();
const client = new OAuth2Client({
clientId: OAUTH_CLIENT_ID,
clientSecret: OAUTH_CLIENT_SECRET,
transporterOptions: {
proxy: config.getProxy(),
},
transporterOptions: proxy
? { proxy }
: { agent: new https.Agent({ keepAlive: false }) },
});
const useEncryptedStorage = getUseEncryptedStorageFlag();

Expand Down
Loading