fix(llm-deepseek): refuse an API key no header can carry

This commit is contained in:
Yichen Jiang
2026-08-06 21:39:57 +08:00
parent b6b57ceda3
commit 5514dd2bd6
4 changed files with 77 additions and 7 deletions

View File

@@ -991,3 +991,33 @@ describe('plugin registration and config', () => {
expect(ctx.llm.listProviders()).toEqual([])
})
})
describe('API key format', () => {
it('trims a padded literal apiKey', () => {
expect(resolveAdapterOptions({ apiKey: ' sk-abc ' }).apiKey).toBe('sk-abc')
})
it('leaves an omitted apiKey absent so apiKeyEnv still resolves it', () => {
expect(resolveAdapterOptions({}).apiKey).toBeUndefined()
})
it('rejects a literal apiKey of whitespace only', () => {
expect(() => resolveAdapterOptions({ apiKey: ' ' }))
.toThrow(/apiKey is empty; omit it/)
})
it('rejects a literal apiKey no header can carry', () => {
expect(() => resolveAdapterOptions({ apiKey: 'sk-\u{1F600}' }))
.toThrow(/no HTTP header can carry/)
})
it('never echoes the key in the rejection', () => {
const secret = 'sk-\u{1F600}supersecret'
expect(() => resolveAdapterOptions({ apiKey: secret })).toThrow()
try {
resolveAdapterOptions({ apiKey: secret })
} catch (error) {
expect((error as Error).message).not.toContain('supersecret')
}
})
})

View File

@@ -3,7 +3,7 @@ import { Context } from 'cordis'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import LlmService from '@deepseek-ai/dsh-llm'
import LlmService, { INVALID_CREDENTIAL_CODE } from '@deepseek-ai/dsh-llm'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
import { CredentialsLocal } from '@deepseek-ai/dsh-credentials-local'
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
@@ -103,6 +103,25 @@ describe('request-level dynamic configuration', () => {
expect(server.headers[0]?.authorization).toBe('Bearer sk-arrived')
})
it('rejects a stored credential no header can carry, never echoing it in the failure', async () => {
vi.stubEnv('DEEPSEEK_API_KEY', '')
const dir = await home()
const { ctx } = await boot(dir, { baseURL: 'http://127.0.0.1:1' })
const secret = 'sk-\u{1F600}supersecret'
// The real credentials seam (the path the web Models page writes through),
// not a hand-built stub: this package's own dynamic-config harness already
// boots one, and round-tripping the value through its actual store/read
// path is stronger evidence than a canned in-memory return would be.
await ctx.credentials.set(KEY_REF, secret)
const result = await prompt(ctx)
expect(result.finish).toMatchObject({ kind: 'error', failure: { code: INVALID_CREDENTIAL_CODE } })
if (result.finish.kind !== 'error') throw new Error('expected an error finish')
expect(result.finish.failure.message).not.toContain(secret)
expect(result.finish.failure.message).not.toContain('supersecret')
expect(result.finish.failure.message).not.toContain('ByteString')
})
it('advertises a live settings catalog without re-registration', async () => {
const dir = await home()
const { ctx } = await boot(dir, { apiKey: 'k', baseURL: 'http://127.0.0.1:1' })