From 94e22e19d3ac1570abc267b24bf3ff165ab281f7 Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 06:22:19 -0700 Subject: [PATCH] build(deps): bump google-auth-library to 11.3.1 (#2387) --- bun.lock | 12 +- package.json | 4 +- .../api/googleAuth.integration.test.ts | 168 ++++++++++++++++++ 3 files changed, 176 insertions(+), 8 deletions(-) create mode 100644 src/services/api/googleAuth.integration.test.ts diff --git a/bun.lock b/bun.lock index 443906786..834ae4413 100644 --- a/bun.lock +++ b/bun.lock @@ -51,7 +51,7 @@ "figures": "6.1.0", "fuse.js": "7.5.0", "get-east-asian-width": "1.7.0", - "google-auth-library": "10.6.2", + "google-auth-library": "11.3.1", "graphology": "0.26.0", "graphology-metrics": "2.4.2", "https-proxy-agent": "9.1.0", @@ -105,7 +105,7 @@ }, }, "overrides": { - "google-auth-library": "10.6.2", + "google-auth-library": "11.3.1", "ip-address": "10.2.0", "lodash-es": "4.18.1", "node-domexception": "file:vendor/node-domexception-shim", @@ -819,9 +819,9 @@ "fuse.js": ["fuse.js@7.5.0", "", {}, "sha512-sQtrEfA+ez/3G0cCZecF70oqpCRttCexYUG4mUrtWL49ULUzUyxokt5kyqwtKzj1270RaKih+hcP3qLcumccow=="], - "gaxios": ["gaxios@7.1.5", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg=="], + "gaxios": ["gaxios@8.3.2", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-7VsaTExxjbDv2ZK1YuC4jbgfN33NI/6/7zdYWUHB6mcg51v4tCBZe+xzjqPzOpVzI+C5k1Kk9OQ80WVrXAHOqw=="], - "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + "gcp-metadata": ["gcp-metadata@9.2.2", "", { "dependencies": { "gaxios": "^8.3.0", "google-logging-utils": "^2.0.0", "json-bigint": "^1.0.0" } }, "sha512-hlaHpCBoNCzoUjMnBZq0LbUiYqPAhXFaAgyNwQqwWU44TZIYJK3RD3o2wkNdvPs8nnUXqJjTudnodLJkLMBjow=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], @@ -839,9 +839,9 @@ "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - "google-auth-library": ["google-auth-library@10.6.2", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="], + "google-auth-library": ["google-auth-library@11.3.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^8.3.0", "gcp-metadata": "^9.0.0", "google-logging-utils": "^2.0.0", "jws": "^4.0.0" } }, "sha512-mQS0R2Ev/DKkzuWSYtDeq15uhDeaWuT2qzLYqJ41LmooxVKHmn5ut3pc6dlHY6wfHW/D7FBmOYjAtkjaD1A7qA=="], - "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + "google-logging-utils": ["google-logging-utils@2.1.0", "", {}, "sha512-1R6uiq6XhXW2ZrcGtZpGgVDsYGzSmveVOn8Pp37qe1fbThHZ34e3t3eXj5mVu6PFSVx9fvB1KNVnYgVtq/BRcA=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], diff --git a/package.json b/package.json index 4c19f5c18..fa8fb73a2 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "figures": "6.1.0", "fuse.js": "7.5.0", "get-east-asian-width": "1.7.0", - "google-auth-library": "10.6.2", + "google-auth-library": "11.3.1", "graphology": "0.26.0", "graphology-metrics": "2.4.2", "https-proxy-agent": "9.1.0", @@ -213,7 +213,7 @@ }, "overrides": { "ip-address": "10.2.0", - "google-auth-library": "10.6.2", + "google-auth-library": "11.3.1", "lodash-es": "4.18.1", "node-domexception": "file:vendor/node-domexception-shim" }, diff --git a/src/services/api/googleAuth.integration.test.ts b/src/services/api/googleAuth.integration.test.ts new file mode 100644 index 000000000..70edde827 --- /dev/null +++ b/src/services/api/googleAuth.integration.test.ts @@ -0,0 +1,168 @@ +import { afterEach, beforeEach, expect, mock, spyOn, test } from 'bun:test' +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { GoogleAuth, UserRefreshClient } from 'google-auth-library' + +import { snapshotEnv } from '../../test/envSnapshot.js' +import { + acquireSharedMutationLock, + releaseSharedMutationLock, +} from '../../test/sharedMutationLock.js' +import { checkGcpCredentialsValid } from '../../utils/auth.js' +import { resolveGeminiCredential } from '../../utils/geminiAuth.js' +import { getAnthropicClient } from './client.js' + +const originalGetClient = GoogleAuth.prototype.getClient +const envKeys = [ + 'GOOGLE_APPLICATION_CREDENTIALS', 'GOOGLE_CLOUD_PROJECT', 'GCLOUD_PROJECT', + 'GOOGLE_CLOUD_QUOTA_PROJECT', 'GOOGLE_PROJECT_ID', + 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_SKIP_VERTEX_AUTH', + 'ANTHROPIC_VERTEX_PROJECT_ID', 'ANTHROPIC_VERTEX_BASE_URL', 'CLOUD_ML_REGION', + 'CLAUDE_CODE_USE_GEMINI', 'GEMINI_AUTH_MODE', 'GEMINI_API_KEY', 'GOOGLE_API_KEY', + 'GEMINI_ACCESS_TOKEN', 'GEMINI_BASE_URL', 'GEMINI_MODEL', + 'CLAUDE_CODE_USE_OPENAI', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_FOUNDRY', + 'CLAUDE_CODE_USE_GITHUB', 'CLAUDE_CODE_USE_MISTRAL', 'ANTHROPIC_CUSTOM_HEADERS', +] as const + +let restoreEnv: () => void +let fixtureDir: string +let originalMacro: PropertyDescriptor | undefined +let authInstances: GoogleAuth[] +let tokenRequests: URLSearchParams[] +let apiHeaders: Headers | undefined +let apiUrl: string | undefined + +const apiFetch = (async (input, init) => { + apiUrl = String(input) + apiHeaders = new Headers(init?.headers) + const vertex = process.env.CLAUDE_CODE_USE_VERTEX === '1' + return new Response(JSON.stringify(vertex ? { + id: 'msg_auth', type: 'message', role: 'assistant', + model: 'claude-sonnet-4-6', content: [{ type: 'text', text: 'ok' }], + stop_reason: 'end_turn', stop_sequence: null, + usage: { input_tokens: 1, output_tokens: 1 }, + } : { + id: 'chatcmpl_auth', model: 'gemini-2.5-flash', + choices: [{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }), { headers: { 'Content-Type': 'application/json' } }) +}) as typeof fetch + +beforeEach(async () => { + await acquireSharedMutationLock('services/api/googleAuth.integration.test.ts') + restoreEnv = snapshotEnv(envKeys, { clear: true }) + originalMacro = Object.getOwnPropertyDescriptor(globalThis, 'MACRO') + ;(globalThis as Record).MACRO = { VERSION: 'test-version' } + fixtureDir = mkdtempSync(join(tmpdir(), 'openclaude-google-auth-')) + const credentialsPath = join(fixtureDir, 'adc.json') + writeFileSync(credentialsPath, JSON.stringify({ + type: 'authorized_user', + client_id: 'fixture-client', + client_secret: 'fixture-secret', + refresh_token: 'fixture-refresh', + quota_project_id: 'fixture-quota', + })) + process.env.GOOGLE_APPLICATION_CREDENTIALS = credentialsPath + process.env.GOOGLE_CLOUD_PROJECT = 'fixture-project' + authInstances = [] + tokenRequests = [] + apiHeaders = undefined + apiUrl = undefined + + // Keep the production constructor, ADC file loading, getClient, and token/header + // methods real. Replace only the HTTP transport of the resulting auth client. + spyOn(GoogleAuth.prototype, 'getClient').mockImplementation(async function (this: GoogleAuth) { + const client = await originalGetClient.call(this) + expect(client).toBeInstanceOf(UserRefreshClient) + authInstances.push(this) + client.transporter.defaults.fetchImplementation = async (url, init) => { + expect(String(url)).toBe('https://oauth2.googleapis.com/token') + expect(init?.method).toBe('POST') + tokenRequests.push(new URLSearchParams(String(init?.body))) + return new Response(JSON.stringify({ + access_token: 'fixture-access-token', token_type: 'Bearer', expires_in: 3600, + }), { headers: { 'Content-Type': 'application/json' } }) + } + return client + }) +}) + +afterEach(() => { + try { + mock.restore() + restoreEnv() + if (originalMacro) { + Object.defineProperty(globalThis, 'MACRO', originalMacro) + } else { + delete (globalThis as Record).MACRO + } + rmSync(fixtureDir, { recursive: true, force: true }) + } finally { + releaseSharedMutationLock() + } +}) + +function expectRealAuthFlow(): void { + expect(authInstances).toHaveLength(1) + expect(tokenRequests).toHaveLength(1) + expect(Object.fromEntries(tokenRequests[0]!)).toEqual({ + client_id: 'fixture-client', + client_secret: 'fixture-secret', + refresh_token: 'fixture-refresh', + grant_type: 'refresh_token', + }) +} + +test('Vertex loads ADC and forwards real Google auth Headers to rawPredict', async () => { + process.env.CLAUDE_CODE_USE_VERTEX = '1' + process.env.CLOUD_ML_REGION = 'us-east5' + process.env.ANTHROPIC_VERTEX_PROJECT_ID = 'fixture-target' + const client = await getAnthropicClient({ + maxRetries: 0, model: 'claude-sonnet-4-6', fetchOverride: apiFetch, + }) + await client.messages.create({ + model: 'claude-sonnet-4-6', max_tokens: 16, + messages: [{ role: 'user', content: 'hello' }], + }) + + expectRealAuthFlow() + expect(apiUrl).toContain('/projects/fixture-target/locations/us-east5/') + expect(apiUrl).toEndWith(':rawPredict') + expect(apiHeaders?.get('authorization')).toBe('Bearer fixture-access-token') + expect(apiHeaders?.get('x-goog-user-project')).toBe('fixture-quota') +}) + +test('Gemini loads ADC and normalizes the real getAccessToken result for requests', async () => { + // The OpenAI shim uses the shared fetch transport rather than fetchOverride. + spyOn(globalThis, 'fetch').mockImplementation(apiFetch) + process.env.CLAUDE_CODE_USE_GEMINI = '1' + process.env.GEMINI_AUTH_MODE = 'adc' + process.env.GEMINI_MODEL = 'gemini-2.5-flash' + process.env.GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/openai' + const client = await getAnthropicClient({ + maxRetries: 0, model: 'gemini-2.5-flash', fetchOverride: apiFetch, + }) + await client.messages.create({ + model: 'gemini-2.5-flash', max_tokens: 16, + messages: [{ role: 'user', content: 'hello' }], + }) + + expectRealAuthFlow() + expect(apiUrl).toBe('https://generativelanguage.googleapis.com/v1beta/openai/chat/completions') + expect(apiHeaders?.get('authorization')).toBe('Bearer fixture-access-token') + expect(apiHeaders?.get('x-goog-user-project')).toBe('fixture-project') +}) + +test('Vertex credential validation loads ADC and retrieves a real access token', async () => { + await expect(checkGcpCredentialsValid()).resolves.toBe(true) + expectRealAuthFlow() +}) + +test('Vertex validation and Gemini ADC reject malformed credential files', async () => { + writeFileSync(process.env.GOOGLE_APPLICATION_CREDENTIALS!, '{invalid json') + process.env.GEMINI_AUTH_MODE = 'adc' + await expect(checkGcpCredentialsValid()).resolves.toBe(false) + await expect(resolveGeminiCredential()).resolves.toEqual({ kind: 'none' }) + expect(tokenRequests).toHaveLength(0) +})