Skip to content

Commit b9a5030

Browse files
authored
fix(status): show active provider route instead of legacy provider bucket (Gitlawb#1673)
* fix(status): show active provider route instead of legacy bucket The /status command collapsed many concrete providers (OpenRouter, Groq, Ollama, Fireworks AI, etc.) into a single "OpenAI-compatible" label, making multi-provider setups hard to verify and debug. When apiProvider resolves to the generic "openai" bucket, /status now uses route metadata to surface the real active route: Provider route: OpenRouter Transport: OpenAI-compatible API OpenAI base URL: https://openrouter.ai/api/v1 Model: anthropic/claude-sonnet-4.5 Credential: OPENROUTER_API_KEY configured The legacy "OpenAI-compatible" label and fallback are preserved for unknown custom base URLs. Dedicated provider buckets (nvidia-nim, minimax, codex, github, xai, gemini, bedrock, vertex, foundry, firstParty, mistral) already have accurate labels and are left untouched. Credential display uses env-var names only (never values). Transport kind and route label come from the existing descriptor-driven route metadata; no new hardcoded provider maps or network calls are introduced. * fix(status): include route status defaults * fix(status): address route status review findings * fix(status): cover route secret redaction review * fix(status): avoid duplicate route resolution * fix(status): redact base URL query credentials * fix(status): harden status URL secret redaction * fix(status): redact route secrets in status text * test(status): cover fallback URL fragment redaction * test(status): isolate route status provider imports * fix(status): redact encoded route secrets * fix(status): redact encoded query secrets safely * fix(status): redact nested encoded query secrets * fix(status): redact encoded secret substrings * fix(status): redact strict encoded secret variants
1 parent 02ee7c6 commit b9a5030

8 files changed

Lines changed: 901 additions & 17 deletions

src/utils/providerProfile.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1588,6 +1588,7 @@ test('maskSecretForDisplay preserves only a short prefix and suffix', () => {
15881588
test('redactSecretValueForDisplay masks poisoned display fields that equal configured secrets', () => {
15891589
const apiKey = 'sk-secret-12345678'
15901590
const authHeaderValue = 'hicap-header-secret'
1591+
const routeApiKey = 'gsk-route-secret-value'
15911592

15921593
assert.equal(
15931594
redactSecretValueForDisplay(apiKey, { OPENAI_API_KEY: apiKey }),
@@ -1599,10 +1600,49 @@ test('redactSecretValueForDisplay masks poisoned display fields that equal confi
15991600
}),
16001601
'hic...ret',
16011602
)
1603+
assert.equal(
1604+
redactSecretValueForDisplay(routeApiKey, { GROQ_API_KEY: routeApiKey }),
1605+
'gsk...lue',
1606+
)
16021607
assert.equal(
16031608
redactSecretValueForDisplay('gpt-4o', { OPENAI_API_KEY: apiKey }),
16041609
'gpt-4o',
16051610
)
1611+
assert.equal(
1612+
redactSecretValueForDisplay('gpt-4o', { OPENAI_MODEL: 'gpt-4o' }),
1613+
'gpt-4o',
1614+
)
1615+
})
1616+
1617+
test('redactSecretValueForDisplay collects common secret env suffixes', () => {
1618+
const secretEnvCases = [
1619+
['ROUTE_API_KEY', 'route-api-secret-value'],
1620+
['ROUTE_AUTH_HEADER_VALUE', 'route-auth-header-secret'],
1621+
['SERVICE_PASSWORD', 'database-password-secret'],
1622+
['SERVICE_SECRET', 'service-secret-value'],
1623+
['AWS_SECRET_ACCESS_KEY', 'aws-secret-access-value'],
1624+
['OAUTH_SECRET_KEY', 'oauth-secret-key-value'],
1625+
['GITHUB_TOKEN', 'github-token-secret'],
1626+
] as const
1627+
1628+
for (const [key, value] of secretEnvCases) {
1629+
const source = { [key]: value }
1630+
1631+
assert.equal(
1632+
redactSecretValueForDisplay(value, source),
1633+
maskSecretForDisplay(value),
1634+
)
1635+
assert.equal(sanitizeProviderConfigValue(value, source), undefined)
1636+
}
1637+
1638+
assert.equal(
1639+
redactSecretValueForDisplay('gpt-4o', { OPENAI_MODEL: 'gpt-4o' }),
1640+
'gpt-4o',
1641+
)
1642+
assert.equal(
1643+
sanitizeProviderConfigValue('gpt-4o', { OPENAI_MODEL: 'gpt-4o' }),
1644+
'gpt-4o',
1645+
)
16061646
})
16071647

16081648
test('sanitizeProviderConfigValue drops secret-like poisoned values', () => {

src/utils/providerSecrets.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
const FAKE_OPENAI_KEY = 'sk-fake-openai-1234567890abcdef'
2121
const FAKE_GEMINI_KEY = 'AIzaSyFAKEGEMINIkey1234567890abcdefghijklmnopqr'
2222
const FAKE_GITHUB_PAT = 'ghp_FAKEgithubPat0123456789abcdefghij'
23+
const FAKE_GITHUB_USER_TOKEN = 'ghu_1234567890abcdef1234567890abcdef1234'
2324
const FAKE_LONG_OPAQUE = 'live-pr-1234567890abcdefABCDEF1234567890abcdef'
2425
const FAKE_JWT_TOKEN = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
2526

@@ -179,6 +180,7 @@ describe('redactSecretValueForDisplay', () => {
179180
expect(redactSecretValueForDisplay('sk-ant-fakeAnthropicToken-1234567890')).toBe('sk-...890')
180181
expect(redactSecretValueForDisplay('AIzaSyFAKEGEMINIkey1234567890abcdefghijklmnopqr')).toBe('AIz...pqr')
181182
expect(redactSecretValueForDisplay(FAKE_GITHUB_PAT)).toBe('ghp...hij')
183+
expect(redactSecretValueForDisplay(FAKE_GITHUB_USER_TOKEN)).toBe('ghu...234')
182184
expect(redactSecretValueForDisplay(FAKE_LONG_OPAQUE)).toBe('liv...def')
183185
expect(redactSecretValueForDisplay(FAKE_JWT_TOKEN)).toBe('eyJ...w5c')
184186
})
@@ -242,11 +244,12 @@ describe('redactSecretSubstringsForDisplay', () => {
242244
const leakedKey = 'sk-liveLeakToken1234567890ABCdef'
243245

244246
const redacted = redactSecretSubstringsForDisplay(
245-
`Invalid API key: ${leakedKey}`,
247+
`Invalid API key: ${leakedKey}; GitHub token: ${FAKE_GITHUB_USER_TOKEN}`,
246248
)
247249

248-
expect(redacted).toBe('Invalid API key: sk-...def')
250+
expect(redacted).toBe('Invalid API key: sk-...def; GitHub token: ghu...234')
249251
expect(redacted).not.toContain(leakedKey)
252+
expect(redacted).not.toContain(FAKE_GITHUB_USER_TOKEN)
250253
})
251254

252255
test('redacts JWT-shaped values embedded in longer messages', () => {
@@ -285,6 +288,7 @@ describe('sanitizeProviderConfigValue', () => {
285288
expect(sanitizeProviderConfigValue('sk-ant-looks-like-a-key-1234567890')).toBeUndefined()
286289
expect(sanitizeProviderConfigValue('AIzaSySomeGeminiKey1234567890abcdefghijklmnopqr')).toBeUndefined()
287290
expect(sanitizeProviderConfigValue(FAKE_GITHUB_PAT)).toBeUndefined()
291+
expect(sanitizeProviderConfigValue(FAKE_GITHUB_USER_TOKEN)).toBeUndefined()
288292
expect(sanitizeProviderConfigValue(FAKE_JWT_TOKEN)).toBeUndefined()
289293
})
290294

src/utils/providerSecrets.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,14 @@ const SECRET_PREFIX_PATTERNS = [
9090
/^AIza/,
9191
/^ghp_/,
9292
/^gho_/,
93+
/^ghu_/,
9394
/^ghs_/,
9495
/^ghr_/,
9596
/^github_pat_/,
9697
]
9798

9899
const SECRET_PREFIX_SUBSTRING_PATTERN =
99-
/(?:sk-ant-|sk-|AIza|ghp_|gho_|ghs_|ghr_|github_pat_)[A-Za-z0-9._-]{8,}/g
100+
/(?:sk-ant-|sk-|AIza|ghp_|gho_|ghu_|ghs_|ghr_|github_pat_)[A-Za-z0-9._-]{8,}/g
100101
const JWT_SUBSTRING_PATTERN =
101102
/\b[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g
102103

@@ -152,16 +153,38 @@ function hasLowerUpperDigit(value: string): boolean {
152153
return hasLower && hasUpper && hasDigit
153154
}
154155

156+
// Redaction sources may be full process env objects, so also collect values
157+
// from generic credential-bearing suffixes. The descriptor registry covers
158+
// known providers; this defensive path covers custom routes and cloud/database
159+
// auth variables that can still be surfaced through status/config displays.
160+
function isSecretEnvKey(
161+
key: string,
162+
knownKeys: ReadonlySet<string>,
163+
): boolean {
164+
return (
165+
knownKeys.has(key) ||
166+
key.endsWith('_API_KEY') ||
167+
key.endsWith('_AUTH_HEADER_VALUE') ||
168+
key.endsWith('_PASSWORD') ||
169+
key.endsWith('_SECRET') ||
170+
key.endsWith('_SECRET_ACCESS_KEY') ||
171+
key.endsWith('_SECRET_KEY') ||
172+
key.endsWith('_TOKEN')
173+
)
174+
}
175+
155176
function collectSecretValues(
156177
sources: Array<SecretValueSource | null | undefined>,
157178
): string[] {
158-
const knownKeys = getKnownProviderSecretEnvKeys()
179+
const knownKeys = new Set(getKnownProviderSecretEnvKeys())
159180
const values = new Set<string>()
160181

161182
for (const source of sources) {
162183
if (!source) continue
163184

164-
for (const key of knownKeys) {
185+
for (const key of Object.keys(source)) {
186+
if (!isSecretEnvKey(key, knownKeys)) continue
187+
165188
const value = sanitizeApiKey(source[key])?.trim()
166189
if (value) {
167190
values.add(value)

0 commit comments

Comments
 (0)