cc-haha/src/services/api/client.test.ts
程序员阿江(Relakkes) 587e6809fc Stop bearer providers from inheriting local API keys
Custom Anthropic-compatible providers that authenticate with ANTHROPIC_AUTH_TOKEN must not also inherit the user's local login or keychain API key as x-api-key. The Provider Test path uses direct fetch and already sent only Authorization, while real desktop sessions use the Anthropic SDK and could attach both headers in trusted project cwd sessions.

Constraint: Provider Test uses direct fetch while real sessions use Anthropic SDK headers
Rejected: Disable prompt, git, or context loading | 401 reproduced with a minimal prompt and no tools while the extra x-api-key persisted
Confidence: high
Scope-risk: narrow
Directive: Do not fallback to local Anthropic API keys when ANTHROPIC_AUTH_TOKEN is explicit unless ANTHROPIC_API_KEY or an explicit apiKey is also provided
Tested: bun test src/services/api/client.test.ts
Tested: bun run check:server
Tested: source CLI proxy capture verified Authorization=true and x-api-key=false in a real repository cwd
Tested: live parallel source CLI calls returned ok for PackyCodex2ccCustom and 胜算云
Not-tested: Rebuilt macOS app artifact after this patch
2026-05-08 14:59:35 +08:00

88 lines
3.1 KiB
TypeScript

import { describe, expect, mock, test } from 'bun:test'
mock.module('src/utils/http.js', () => ({
getAuthHeaders: mock(() => ({})),
getMCPUserAgent: mock(() => 'client-test-agent'),
getUserAgent: mock(() => 'client-test-agent'),
getWebFetchUserAgent: mock(() => 'client-test-agent'),
withOAuth401Retry: mock(async <T>(fn: () => Promise<T>) => fn()),
}))
describe('resolveAnthropicClientApiKey', () => {
test('does not inherit a local api key when a provider auth token is explicit', async () => {
const { resolveAnthropicClientApiKey } = await import('./client.js')
const getFallbackApiKey = mock(() => 'sk-keychain-fallback')
const apiKey = resolveAnthropicClientApiKey({
envAuthToken: 'provider-bearer-token',
envApiKey: undefined,
getFallbackApiKey,
})
expect(apiKey).toBeNull()
expect(getFallbackApiKey).not.toHaveBeenCalled()
})
test('preserves an explicit api key when the caller opts into dual auth', async () => {
const { resolveAnthropicClientApiKey } = await import('./client.js')
const getFallbackApiKey = mock(() => 'sk-keychain-fallback')
const apiKey = resolveAnthropicClientApiKey({
explicitApiKey: 'sk-explicit-api-key',
envAuthToken: 'provider-bearer-token',
getFallbackApiKey,
})
expect(apiKey).toBe('sk-explicit-api-key')
expect(getFallbackApiKey).not.toHaveBeenCalled()
})
test('falls back to the local api key when no provider auth token is present', async () => {
const { resolveAnthropicClientApiKey } = await import('./client.js')
const getFallbackApiKey = mock(() => 'sk-keychain-fallback')
const apiKey = resolveAnthropicClientApiKey({
envAuthToken: undefined,
envApiKey: undefined,
getFallbackApiKey,
})
expect(apiKey).toBe('sk-keychain-fallback')
expect(getFallbackApiKey).toHaveBeenCalled()
})
})
describe('getAnthropicClient', () => {
test('passes bearer-token provider auth without an SDK api key', async () => {
const { getAnthropicClient } = await import('./client.js')
const originalAuthToken = process.env.ANTHROPIC_AUTH_TOKEN
const originalApiKey = process.env.ANTHROPIC_API_KEY
const originalSimple = process.env.CLAUDE_CODE_SIMPLE
process.env.ANTHROPIC_AUTH_TOKEN = 'provider-bearer-token'
process.env.CLAUDE_CODE_SIMPLE = '1'
delete process.env.ANTHROPIC_API_KEY
try {
const client = await getAnthropicClient({
maxRetries: 0,
model: 'claude-sonnet-4-6',
})
expect(client.apiKey).toBeNull()
expect(client._options.defaultHeaders).toMatchObject({
Authorization: 'Bearer provider-bearer-token',
})
} finally {
if (originalAuthToken === undefined) delete process.env.ANTHROPIC_AUTH_TOKEN
else process.env.ANTHROPIC_AUTH_TOKEN = originalAuthToken
if (originalApiKey === undefined) delete process.env.ANTHROPIC_API_KEY
else process.env.ANTHROPIC_API_KEY = originalApiKey
if (originalSimple === undefined) delete process.env.CLAUDE_CODE_SIMPLE
else process.env.CLAUDE_CODE_SIMPLE = originalSimple
}
})
})