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
This commit is contained in:
Bohdan Triapitsyn
2026-06-15 11:45:31 +03:00
parent 71bae089a7
commit e276dc8a5d
2 changed files with 49 additions and 4 deletions
+36
View File
@@ -8,6 +8,19 @@ import {
import { setRuntimeBearerToken, setRuntimeUrlAuthToken } from './runtime-auth';
describe('createRuntimeUrlResolver', () => {
const withWindow = <T>(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 {
+13 -4
View File
@@ -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,