From 268dc8c184ea9b29f8ca5a03555ee3f26a872df5 Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Thu, 23 Jul 2026 11:34:15 -0600 Subject: [PATCH] fix(ai-wandb): require clean api base urls --- packages/ai/wandb/src/index.test.ts | 31 ++++++++++++++++++++++++++--- packages/ai/wandb/src/index.ts | 19 +++++++++++++++++- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/packages/ai/wandb/src/index.test.ts b/packages/ai/wandb/src/index.test.ts index a4df0795..69c23500 100644 --- a/packages/ai/wandb/src/index.test.ts +++ b/packages/ai/wandb/src/index.test.ts @@ -85,26 +85,51 @@ describe('W&B Inference generation', () => { }); it('supports text-style choices from compatible responses', async () => { - vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ + const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ choices: [{ text: 'legacy text response' }], }), - })); + }); + vi.stubGlobal('fetch', fetchMock); const result = await adapter.generate( ctx(), 'hello', { model: 'deepseek-ai/DeepSeek-V3-0324' }, - { baseUrl: 'https://wandb.test/v1' }, + { baseUrl: 'https://wandb.test/v1/' }, ); + expect(fetchMock.mock.calls[0]?.[0]).toBe('https://wandb.test/v1/chat/completions'); expect(result).toEqual({ text: 'legacy text response', model: 'deepseek-ai/DeepSeek-V3-0324', }); }); + it('rejects invalid or unclean custom base URLs before network calls', async () => { + const fetchMock = vi.fn(); + vi.stubGlobal('fetch', fetchMock); + + await expect( + adapter.generate(ctx(), 'hello', {}, { baseUrl: 'wandb.test/v1' }), + ).rejects.toThrow('valid URL'); + await expect( + adapter.generate(ctx(), 'hello', {}, { baseUrl: 'ftp://wandb.test/v1' }), + ).rejects.toThrow('http or https'); + await expect( + adapter.generate(ctx(), 'hello', {}, { baseUrl: 'https://user:pass@wandb.test/v1' }), + ).rejects.toThrow('clean API base'); + await expect( + adapter.generate(ctx(), 'hello', {}, { baseUrl: 'https://wandb.test/v1?target=chat' }), + ).rejects.toThrow('clean API base'); + await expect( + adapter.generate(ctx(), 'hello', {}, { baseUrl: 'https://wandb.test/v1#chat' }), + ).rejects.toThrow('clean API base'); + + expect(fetchMock).not.toHaveBeenCalled(); + }); + it('includes status and response body excerpt on errors', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, diff --git a/packages/ai/wandb/src/index.ts b/packages/ai/wandb/src/index.ts index a301ab85..da455ca5 100644 --- a/packages/ai/wandb/src/index.ts +++ b/packages/ai/wandb/src/index.ts @@ -35,7 +35,8 @@ export default defineAi({ }; if (config.project) headers['OpenAI-Project'] = config.project; - const res = await fetch(`${config.baseUrl ?? DEFAULT_BASE}/chat/completions`, { + const baseUrl = cleanBaseUrl(config.baseUrl ?? DEFAULT_BASE); + const res = await fetch(`${baseUrl}/chat/completions`, { method: 'POST', headers, body: JSON.stringify({ @@ -72,6 +73,22 @@ export default defineAi({ }), }); +function cleanBaseUrl(value: string): string { + let url: URL; + try { + url = new URL(value); + } catch { + throw new Error('W&B Inference baseUrl must be a valid URL'); + } + if (url.protocol !== 'https:' && url.protocol !== 'http:') { + throw new Error('W&B Inference baseUrl must use http or https'); + } + if (url.username || url.password || url.search || url.hash) { + throw new Error('W&B Inference baseUrl must be a clean API base without credentials, query, or hash'); + } + return url.toString().replace(/\/+$/, ''); +} + type WandbRole = 'system' | 'user' | 'assistant' | 'tool'; interface WandbMessage {