cc-haha/desktop/src/lib/providerSettingsJson.ts
程序员阿江(Relakkes) 67a64c7f80 Scrub ChatGPT OAuth env from provider settings JSON
The desktop provider settings editor has its own JSON scrubber for provider-managed env. It now removes the ChatGPT Official OAuth marker and token-file path along with the Anthropic provider env keys, so editing another provider cannot keep stale OpenAI OAuth runtime markers in previewed settings JSON.

Constraint: Desktop JSON preview cleanup is separate from server managed-env filtering.

Rejected: Rely only on server activation cleanup | the settings editor can preview and persist user-edited JSON before the server rebuilds provider env.

Confidence: high

Scope-risk: narrow

Tested: cd desktop && bun test src/lib/__tests__/providerSettingsJson.test.ts

Tested: bun test src/server/__tests__/providers.test.ts --test-name-pattern "ChatGPT Official|getProviderForProxy"

Tested: bun test src/server/__tests__/conversation-service.test.ts --test-name-pattern "ChatGPT Official|session-scoped provider"

Tested: git diff --check
2026-05-18 23:32:22 +08:00

106 lines
3.3 KiB
TypeScript

export const API_KEY_JSON_PLACEHOLDER = '••••••••'
const API_KEY_JSON_KEYS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'] as const
const PROVIDER_SETTINGS_JSON_ENV_KEYS = new Set([
'ANTHROPIC_BASE_URL',
'ANTHROPIC_API_KEY',
'ANTHROPIC_AUTH_TOKEN',
'ANTHROPIC_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION',
'ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME',
'ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_DEFAULT_SONNET_MODEL',
'ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION',
'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_DEFAULT_OPUS_MODEL',
'ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION',
'ANTHROPIC_DEFAULT_OPUS_MODEL_NAME',
'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_SMALL_FAST_MODEL',
'CC_HAHA_OPENAI_OAUTH_PROVIDER',
'CLAUDE_CODE_AUTO_COMPACT_WINDOW',
'CLAUDE_CODE_MODEL_CONTEXT_WINDOWS',
'CLAUDE_CODE_SUBAGENT_MODEL',
'OPENAI_CODEX_OAUTH_FILE',
])
function getEnvRecord(raw: string): Record<string, unknown> | null {
try {
const parsed = JSON.parse(raw) as { env?: unknown }
if (!parsed.env || typeof parsed.env !== 'object' || Array.isArray(parsed.env)) {
return null
}
return parsed.env as Record<string, unknown>
} catch {
return null
}
}
function isSecretDisplayValue(value: unknown): value is string {
return (
typeof value === 'string' &&
value.trim() !== '' &&
value !== API_KEY_JSON_PLACEHOLDER &&
value !== '(your API key)' &&
value !== 'proxy-managed'
)
}
export function maskSettingsJsonSecrets(raw: string): string {
try {
const parsed = JSON.parse(raw) as { env?: Record<string, unknown> }
if (!parsed.env || typeof parsed.env !== 'object' || Array.isArray(parsed.env)) return raw
let changed = false
for (const key of API_KEY_JSON_KEYS) {
if (isSecretDisplayValue(parsed.env[key])) {
parsed.env[key] = API_KEY_JSON_PLACEHOLDER
changed = true
}
}
return changed ? JSON.stringify(parsed, null, 2) : raw
} catch {
return raw
}
}
export function restoreSettingsJsonSecrets<T>(
settings: T,
previousRaw: string,
fallbackApiKey = '',
): T {
if (!settings || typeof settings !== 'object') return settings
const parsed = settings as { env?: Record<string, unknown> }
if (!parsed.env || typeof parsed.env !== 'object' || Array.isArray(parsed.env)) return settings
const previousEnv = getEnvRecord(previousRaw)
const fallback = fallbackApiKey.trim()
for (const key of API_KEY_JSON_KEYS) {
if (parsed.env[key] !== API_KEY_JSON_PLACEHOLDER) continue
const previousValue = previousEnv?.[key]
if (isSecretDisplayValue(previousValue)) {
parsed.env[key] = previousValue
} else if (fallback) {
parsed.env[key] = fallback
}
}
return settings
}
export function stripProviderSettingsJsonEnv(
env: Record<string, string>,
extraManagedKeys: Iterable<string> = [],
): Record<string, string> {
const extraKeys = new Set(Array.from(extraManagedKeys, key => key.toUpperCase()))
return Object.fromEntries(
Object.entries(env).filter(([key]) => {
const upperKey = key.toUpperCase()
return !PROVIDER_SETTINGS_JSON_ENV_KEYS.has(upperKey) && !extraKeys.has(upperKey)
}),
)
}