diff --git a/packages/web/server/lib/tts/base-url.js b/packages/web/server/lib/tts/base-url.js index 493d18e4..855a5365 100644 --- a/packages/web/server/lib/tts/base-url.js +++ b/packages/web/server/lib/tts/base-url.js @@ -47,7 +47,10 @@ export const normalizeCustomOpenAIBaseURL = (value) => { return { error: 'Custom server URL must not include credentials' }; } - const allowRemote = isEnvFlagEnabled(process.env.OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS); + const isDesktop = (process.env.OPENCHAMBER_RUNTIME || '').trim().toLowerCase() === 'desktop'; + const envFlagRaw = process.env.OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS; + const hasExplicitFlag = typeof envFlagRaw === 'string' && envFlagRaw.trim().length > 0; + const allowRemote = hasExplicitFlag ? isEnvFlagEnabled(envFlagRaw) : isDesktop; if (!allowRemote && !isAllowedLocalHost(parsed.hostname)) { return { error: 'Remote custom server URLs are disabled. Set OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS=true to allow this host.', diff --git a/packages/web/server/lib/tts/routes.test.js b/packages/web/server/lib/tts/routes.test.js index c2d61396..ec30ea55 100644 --- a/packages/web/server/lib/tts/routes.test.js +++ b/packages/web/server/lib/tts/routes.test.js @@ -1,8 +1,9 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, afterEach } from 'vitest'; import express from 'express'; import request from 'supertest'; import { registerTtsRoutes } from './routes.js'; +import { normalizeCustomOpenAIBaseURL } from './base-url.js'; const createApp = () => { const app = express(); @@ -51,3 +52,74 @@ describe('tts routes', () => { }); }); }); + +describe('normalizeCustomOpenAIBaseURL', () => { + const originalRuntime = process.env.OPENCHAMBER_RUNTIME; + const originalAllowRemote = process.env.OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS; + + afterEach(() => { + // Restore env vars after each test + if (originalRuntime === undefined) { + delete process.env.OPENCHAMBER_RUNTIME; + } else { + process.env.OPENCHAMBER_RUNTIME = originalRuntime; + } + if (originalAllowRemote === undefined) { + delete process.env.OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS; + } else { + process.env.OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS = originalAllowRemote; + } + }); + + it('rejects remote URLs when OPENCHAMBER_RUNTIME is not set (web)', () => { + delete process.env.OPENCHAMBER_RUNTIME; + delete process.env.OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS; + + const result = normalizeCustomOpenAIBaseURL('https://my-tts-server.example.com/v1'); + expect(result.error).toMatch(/Remote custom server URLs are disabled/); + expect(result.value).toBeUndefined(); + }); + + it('allows remote URLs when OPENCHAMBER_RUNTIME is desktop', () => { + process.env.OPENCHAMBER_RUNTIME = 'desktop'; + delete process.env.OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS; + + const result = normalizeCustomOpenAIBaseURL('https://my-tts-server.example.com/v1'); + expect(result.error).toBeUndefined(); + expect(result.value).toBe('https://my-tts-server.example.com/v1'); + }); + + it('allows remote URLs when OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS is true', () => { + delete process.env.OPENCHAMBER_RUNTIME; + process.env.OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS = 'true'; + + const result = normalizeCustomOpenAIBaseURL('https://my-tts-server.example.com/v1'); + expect(result.error).toBeUndefined(); + expect(result.value).toBe('https://my-tts-server.example.com/v1'); + }); + + it('allows localhost URLs regardless of runtime', () => { + delete process.env.OPENCHAMBER_RUNTIME; + delete process.env.OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS; + + const result = normalizeCustomOpenAIBaseURL('http://localhost:8880/v1'); + expect(result.error).toBeUndefined(); + expect(result.value).toBe('http://localhost:8880/v1'); + }); + + it('strips query strings and trailing slashes', () => { + process.env.OPENCHAMBER_RUNTIME = 'desktop'; + + const result = normalizeCustomOpenAIBaseURL('https://my-server.com/v1/?key=123'); + expect(result.value).toBe('https://my-server.com/v1'); + }); + + it('denies remote URLs on desktop when env var is explicitly false', () => { + process.env.OPENCHAMBER_RUNTIME = 'desktop'; + process.env.OPENCHAMBER_ALLOW_REMOTE_OPENAI_COMPAT_URLS = 'false'; + + const result = normalizeCustomOpenAIBaseURL('https://my-tts-server.example.com/v1'); + expect(result.error).toMatch(/Remote custom server URLs are disabled/); + expect(result.value).toBeUndefined(); + }); +});