From b10571e95a9addb670b5f82bdec594c0ebfdd762 Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Thu, 23 Jul 2026 13:12:29 -0600 Subject: [PATCH] fix(ai-phala): require clean api base urls --- packages/ai/phala/src/index.test.ts | 14 ++++++++++++++ packages/ai/phala/src/index.ts | 25 ++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/ai/phala/src/index.test.ts b/packages/ai/phala/src/index.test.ts index 68be7b5b..82ed27f9 100644 --- a/packages/ai/phala/src/index.test.ts +++ b/packages/ai/phala/src/index.test.ts @@ -81,6 +81,20 @@ describe('Phala OpenAI-compatible generation', () => { expect(url).toBe('https://proxy.example.com/v1/chat/completions'); }); + it.each([ + ['not-a-url', 'valid URL'], + ['ftp://proxy.example.com', 'http or https'], + ['https://user:pass@proxy.example.com', 'must not include credentials'], + ['https://proxy.example.com?region=us', 'must not include query strings or fragments'], + ['https://proxy.example.com#v1', 'must not include query strings or fragments'], + ])('rejects unsafe configured base URL %s', async (baseUrl, message) => { + const fetchMock = vi.fn(); + vi.stubGlobal('fetch', fetchMock); + + await expect(adapter.generate(ctx(), 'hello', {}, { baseUrl })).rejects.toThrow(message); + expect(fetchMock).not.toHaveBeenCalled(); + }); + it('includes status and redacted response body excerpt on errors', async () => { const apiKey = 'test-key-crossing-truncation-boundary'; const prefix = 'x'.repeat(190); diff --git a/packages/ai/phala/src/index.ts b/packages/ai/phala/src/index.ts index 94bcc9b0..f0c761a5 100644 --- a/packages/ai/phala/src/index.ts +++ b/packages/ai/phala/src/index.ts @@ -6,8 +6,31 @@ interface Config { const DEFAULT_BASE = 'https://inference.phala.com'; +function cleanBaseUrl(baseUrl?: string): string { + if (!baseUrl) return DEFAULT_BASE; + + let parsed: URL; + try { + parsed = new URL(baseUrl); + } catch { + throw new Error('Phala baseUrl must be a valid URL'); + } + + if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') { + throw new Error('Phala baseUrl must use http or https'); + } + if (parsed.username || parsed.password) { + throw new Error('Phala baseUrl must not include credentials'); + } + if (parsed.search || parsed.hash) { + throw new Error('Phala baseUrl must not include query strings or fragments'); + } + + return parsed.toString().replace(/\/+$/, ''); +} + function chatCompletionsUrl(baseUrl?: string): string { - return `${(baseUrl ?? DEFAULT_BASE).replace(/\/+$/, '')}/v1/chat/completions`; + return `${cleanBaseUrl(baseUrl)}/v1/chat/completions`; } function redact(value: string, apiKey: string): string {