fix(cli): scope download credentials to registry API

Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
dongmucat 2026-11-03 18:10:37 +08:00
parent 04d7f81625
commit d384ef9860
2 changed files with 30 additions and 1 deletions

View file

@ -242,7 +242,12 @@ export class SkillHubClient {
// Keep the configured registry prefix, as direct download() does.
const relativeUrl = downloadUrl.startsWith('/api/') ? downloadUrl.slice(1) : downloadUrl
const registryBase = `${this.registry.replace(/\/+$/, '')}/`
response = await this.fetchImpl(new URL(relativeUrl, registryBase).toString(), { headers: this.headers() })
const url = new URL(relativeUrl, registryBase)
const registryApi = new URL('api/', registryBase)
const headers = url.origin === registryApi.origin && url.pathname.startsWith(registryApi.pathname)
? this.headers()
: {}
response = await this.fetchImpl(url.toString(), { headers })
} catch {
throw new CliError('registry unreachable', EXIT.network, { registry: this.registry, next: 'check network or pass --registry' })
}

View file

@ -23,6 +23,30 @@ describe('SkillHubClient', () => {
expect(requests).toEqual([expected])
})
test.each([
['https://registry.test', 'https://cdn.test/packages/skill.zip?signature=test'],
['https://registry.test', '//cdn.test/api/packages/skill.zip'],
['https://registry.test', 'https://registry.test.cdn.test/api/skill.zip'],
['https://registry.test', 'http://registry.test/api/skill.zip'],
['https://registry.test', 'https://registry.test:8443/api/skill.zip'],
['https://registry.test', 'https://registry.test/packages/skill.zip'],
['https://registry.test/skillhub', 'https://registry.test/api/skill.zip'],
['https://registry.test/skillhub', 'https://registry.test/skillhub/api-other/skill.zip']
])('omits registry credentials when %s downloads outside its API: %s', async (registry, downloadUrl) => {
const requests: Array<{ url: string; authorization: string | null }> = []
const fetchImpl = (async (input: URL | RequestInfo, init?: RequestInit) => {
const authorization = new Headers(init?.headers).get('Authorization')
requests.push({ url: String(input), authorization })
return authorization
? new Response('InvalidArgument', { status: 400 })
: new Response('package-bytes')
}) as unknown as typeof fetch
const client = new SkillHubClient(registry, 'test-token', fetchImpl)
expect(await (await client.downloadFromUrl(downloadUrl)).text()).toBe('package-bytes')
expect(requests).toEqual([{ url: new URL(downloadUrl, registry).toString(), authorization: null }])
})
test('uses the provided multipart file name when publishing', async () => {
const fetchImpl = (async (_input: URL | RequestInfo, init?: RequestInit) => {
const formData = init?.body as FormData