From e276dc8a5d2113de3522c77cb6b09cd5a87d5ad6 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Mon, 15 Jun 2026 11:45:31 +0300 Subject: [PATCH] fix: resolve runtime URLs from injected desktop API base Uses the injected desktop API base URL at call time Fixes packaged desktop WebSocket URL generation Adds regression coverage for injected runtime URLs --- packages/ui/src/lib/runtime-url.test.ts | 36 +++++++++++++++++++++++++ packages/ui/src/lib/runtime-url.ts | 17 +++++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) 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,