mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
Provider settings JSON previously masked only values that matched the current form API key. Switching providers or editing stale settings could leave ANTHROPIC_API_KEY visible while ANTHROPIC_AUTH_TOKEN was masked, even though both are sensitive auth env vars. The settings preview now masks Anthropic API key and auth token fields by key name, restores placeholders from the previous JSON value per field, and strips provider-managed env vars before merging the active provider preview. This keeps historical provider auth values from leaking into the editable JSON surface. Constraint: Claude Code uses ANTHROPIC_API_KEY for x-api-key auth and ANTHROPIC_AUTH_TOKEN for Bearer auth, so the fix keeps runtime semantics separate from UI redaction. Rejected: Collapse all providers onto one env var | direct Anthropic and gateway auth paths have different documented headers. Confidence: high Scope-risk: narrow Directive: Treat provider auth env vars as secrets by field name in UI surfaces, not by comparing against the current form API key. Tested: cd desktop && bun run test -- src/lib/__tests__/providerSettingsJson.test.ts Tested: cd desktop && bun run lint Tested: bun run check:desktop Tested: agent-browser opened DeepSeek provider edit modal and verified Settings JSON shows ANTHROPIC_AUTH_TOKEN as placeholder with no ANTHROPIC_API_KEY entry.
104 lines
3.2 KiB
TypeScript
104 lines
3.2 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',
|
|
'CLAUDE_CODE_AUTO_COMPACT_WINDOW',
|
|
'CLAUDE_CODE_MODEL_CONTEXT_WINDOWS',
|
|
'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
])
|
|
|
|
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)
|
|
}),
|
|
)
|
|
}
|