mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
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
This commit is contained in:
parent
a31b3045fd
commit
587e6809fc
87
src/services/api/client.test.ts
Normal file
87
src/services/api/client.test.ts
Normal file
@ -0,0 +1,87 @@
|
||||
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
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -91,6 +91,24 @@ function createStderrLogger(): ClientOptions['logger'] {
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveAnthropicClientApiKey({
|
||||
explicitApiKey,
|
||||
envAuthToken = process.env.ANTHROPIC_AUTH_TOKEN,
|
||||
envApiKey = process.env.ANTHROPIC_API_KEY,
|
||||
getFallbackApiKey = getAnthropicApiKey,
|
||||
}: {
|
||||
explicitApiKey?: string
|
||||
envAuthToken?: string
|
||||
envApiKey?: string
|
||||
getFallbackApiKey?: () => string | null
|
||||
}): string | null {
|
||||
if (envAuthToken && !explicitApiKey && !envApiKey) {
|
||||
return null
|
||||
}
|
||||
|
||||
return explicitApiKey || getFallbackApiKey()
|
||||
}
|
||||
|
||||
export async function getAnthropicClient({
|
||||
apiKey,
|
||||
maxRetries,
|
||||
@ -319,7 +337,7 @@ export async function getAnthropicClient({
|
||||
? null
|
||||
: usingOpenAICodex
|
||||
? OPENAI_OAUTH_DUMMY_KEY
|
||||
: apiKey || getAnthropicApiKey(),
|
||||
: resolveAnthropicClientApiKey({ explicitApiKey: apiKey }),
|
||||
authToken: isClaudeAISubscriber()
|
||||
? getClaudeAIOAuthTokens()?.accessToken
|
||||
: undefined,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user