diff --git a/packages/ui/src/lib/runtime-url.test.ts b/packages/ui/src/lib/runtime-url.test.ts index ba38e790..631076cf 100644 --- a/packages/ui/src/lib/runtime-url.test.ts +++ b/packages/ui/src/lib/runtime-url.test.ts @@ -8,6 +8,19 @@ import { import { setRuntimeBearerToken, setRuntimeUrlAuthToken } from './runtime-auth'; describe('createRuntimeUrlResolver', () => { + const withWindow = (value: unknown, callback: () => T): T => { + const originalWindow = globalThis.window; + try { + Object.defineProperty(globalThis, 'window', { + configurable: true, + value, + }); + return callback(); + } finally { + Object.defineProperty(globalThis, 'window', { configurable: true, value: originalWindow }); + } + }; + test('preserves relative same-origin URLs by default', () => { const urls = createRuntimeUrlResolver({ currentHref: () => 'http://127.0.0.1:3000/app' }); @@ -52,6 +65,29 @@ describe('createRuntimeUrlResolver', () => { expect(urls.websocket('/api/terminal/ws')).toBe('ws://localhost:5173/api/terminal/ws'); }); + test('uses injected desktop API base URL for packaged WebSocket URLs', () => { + withWindow({ + location: { origin: 'openchamber-ui://app', href: 'openchamber-ui://app/index.html' }, + __OPENCHAMBER_API_BASE_URL__: 'http://127.0.0.1:57123', + }, () => { + const urls = createRuntimeUrlResolver({}); + + expect(urls.websocket('/api/global/event/ws')).toBe('ws://127.0.0.1:57123/api/global/event/ws'); + }); + }); + + test('reads injected desktop API base URL at call time', () => { + withWindow({ + location: { origin: 'openchamber-ui://app', href: 'openchamber-ui://app/index.html' }, + }, () => { + const urls = createRuntimeUrlResolver({}); + (window as typeof window & { __OPENCHAMBER_API_BASE_URL__?: string }).__OPENCHAMBER_API_BASE_URL__ = 'http://127.0.0.1:57123'; + + expect(urls.api('/api/config/settings')).toBe('http://127.0.0.1:57123/api/config/settings'); + expect(urls.websocket('/api/global/event/ws')).toBe('ws://127.0.0.1:57123/api/global/event/ws'); + }); + }); + test('allows runtime-wide resolver configuration', () => { const previous = getRuntimeUrlResolver(); try { diff --git a/packages/ui/src/lib/runtime-url.ts b/packages/ui/src/lib/runtime-url.ts index 6eafc9c9..d3449113 100644 --- a/packages/ui/src/lib/runtime-url.ts +++ b/packages/ui/src/lib/runtime-url.ts @@ -33,6 +33,12 @@ const normalizeBaseUrl = (value: string | null | undefined): string => { return value.trim().replace(/\/+$/, ''); }; +const readInjectedApiBaseUrl = (): string => { + if (typeof window === 'undefined') return ''; + const injected = (window as typeof window & { __OPENCHAMBER_API_BASE_URL__?: string }).__OPENCHAMBER_API_BASE_URL__; + return normalizeBaseUrl(injected); +}; + const currentHref = (config: RuntimeUrlConfig): string => { const configured = config.currentHref?.(); if (configured) return configured; @@ -107,11 +113,14 @@ const toWebSocketUrl = (candidate: string, config: RuntimeUrlConfig): string => }; export const createRuntimeUrlResolver = (config: RuntimeUrlConfig = {}): RuntimeUrlResolver => { - const apiBaseUrl = normalizeBaseUrl(config.apiBaseUrl); - const realtimeBaseUrl = normalizeBaseUrl(config.realtimeBaseUrl) || apiBaseUrl; + const configuredApiBaseUrl = normalizeBaseUrl(config.apiBaseUrl); + const configuredRealtimeBaseUrl = normalizeBaseUrl(config.realtimeBaseUrl); - const http = (path: string, query?: RuntimeUrlQuery): string => buildHttpUrl(apiBaseUrl, path, query); - const realtime = (path: string, query?: RuntimeUrlQuery): string => buildHttpUrl(realtimeBaseUrl, path, query); + const apiBaseUrl = (): string => configuredApiBaseUrl || readInjectedApiBaseUrl(); + const realtimeBaseUrl = (): string => configuredRealtimeBaseUrl || apiBaseUrl(); + + const http = (path: string, query?: RuntimeUrlQuery): string => buildHttpUrl(apiBaseUrl(), path, query); + const realtime = (path: string, query?: RuntimeUrlQuery): string => buildHttpUrl(realtimeBaseUrl(), path, query); return { api: http,