Fixes switching and unlocking password-protected remote instances Stores SSH forwarded host client tokens from saved UI passwords Avoids unnecessary auth churn when no runtime headers are configured
124 lines
4.9 KiB
TypeScript
124 lines
4.9 KiB
TypeScript
import { refreshRuntimeUrlAuthToken, setRuntimeBearerToken, setRuntimeExtraHeaders } from '@/lib/runtime-auth';
|
|
import { configureRuntimeUrlResolver } from '@/lib/runtime-url';
|
|
|
|
export type RuntimeEndpointChangedDetail = {
|
|
apiBaseUrl: string;
|
|
previousApiBaseUrl: string;
|
|
runtimeKey: string;
|
|
previousRuntimeKey: string;
|
|
};
|
|
|
|
const RUNTIME_ENDPOINT_CHANGED_EVENT = 'openchamber:runtime-endpoint-changed';
|
|
|
|
let activeApiBaseUrl = '';
|
|
let activeRuntimeKey = '';
|
|
|
|
const setWindowRuntimeValue = <K extends '__OPENCHAMBER_API_BASE_URL__' | '__OPENCHAMBER_CLIENT_TOKEN__' | '__OPENCHAMBER_RUNTIME_HEADERS__'>(
|
|
runtimeWindow: typeof window & {
|
|
__OPENCHAMBER_API_BASE_URL__?: string;
|
|
__OPENCHAMBER_CLIENT_TOKEN__?: string;
|
|
__OPENCHAMBER_RUNTIME_HEADERS__?: Record<string, string>;
|
|
},
|
|
key: K,
|
|
value: (typeof runtimeWindow)[K],
|
|
): void => {
|
|
try {
|
|
runtimeWindow[key] = value;
|
|
} catch {
|
|
// Electron preload exposes some initial globals through contextBridge, which
|
|
// makes them read-only. Runtime switching must still update in-memory state.
|
|
}
|
|
};
|
|
|
|
const normalizeRuntimeUrlKey = (value: string): string => {
|
|
try {
|
|
const url = new URL(value);
|
|
url.hash = '';
|
|
url.search = '';
|
|
url.pathname = url.pathname.replace(/\/+$/, '') || '/';
|
|
return `url:${url.toString().replace(/\/+$/, '')}`;
|
|
} catch {
|
|
return `url:${value.trim().replace(/\/+$/, '') || 'default'}`;
|
|
}
|
|
};
|
|
|
|
const readInjectedApiBaseUrl = (): string => {
|
|
if (typeof window === 'undefined') return '';
|
|
const injected = (window as typeof window & { __OPENCHAMBER_API_BASE_URL__?: string }).__OPENCHAMBER_API_BASE_URL__;
|
|
return typeof injected === 'string' ? injected.trim() : '';
|
|
};
|
|
|
|
const readInjectedLocalOrigin = (): string => {
|
|
if (typeof window === 'undefined') return '';
|
|
const injected = (window as typeof window & { __OPENCHAMBER_LOCAL_ORIGIN__?: string }).__OPENCHAMBER_LOCAL_ORIGIN__;
|
|
return typeof injected === 'string' ? injected.trim() : '';
|
|
};
|
|
|
|
const sameOrigin = (left: string, right: string): boolean => {
|
|
if (!left || !right) return false;
|
|
try {
|
|
return new URL(left).origin === new URL(right).origin;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const getRuntimeApiBaseUrl = (): string => activeApiBaseUrl || readInjectedApiBaseUrl();
|
|
export const getRuntimeKey = (): string => {
|
|
if (activeRuntimeKey) return activeRuntimeKey;
|
|
const apiBaseUrl = getRuntimeApiBaseUrl();
|
|
if (sameOrigin(apiBaseUrl, readInjectedLocalOrigin())) return 'local';
|
|
return normalizeRuntimeUrlKey(apiBaseUrl);
|
|
};
|
|
|
|
export const initializeRuntimeEndpoint = (options: { apiBaseUrl?: string | null; runtimeKey?: string | null } = {}): void => {
|
|
if (activeApiBaseUrl || activeRuntimeKey) {
|
|
return;
|
|
}
|
|
|
|
const apiBaseUrl = options.apiBaseUrl?.trim() || readInjectedApiBaseUrl();
|
|
if (!apiBaseUrl) {
|
|
return;
|
|
}
|
|
|
|
activeApiBaseUrl = apiBaseUrl;
|
|
activeRuntimeKey = options.runtimeKey?.trim() || (sameOrigin(apiBaseUrl, readInjectedLocalOrigin()) ? 'local' : normalizeRuntimeUrlKey(apiBaseUrl));
|
|
};
|
|
|
|
export const switchRuntimeEndpoint = (options: { apiBaseUrl: string; clientToken?: string | null; runtimeKey?: string | null; requestHeaders?: Record<string, string> | null }): void => {
|
|
const apiBaseUrl = options.apiBaseUrl.trim();
|
|
const previousApiBaseUrl = getRuntimeApiBaseUrl();
|
|
const previousRuntimeKey = getRuntimeKey();
|
|
const runtimeKey = options.runtimeKey?.trim() || normalizeRuntimeUrlKey(apiBaseUrl);
|
|
activeApiBaseUrl = apiBaseUrl;
|
|
activeRuntimeKey = runtimeKey;
|
|
if (typeof window !== 'undefined') {
|
|
const runtimeWindow = window as typeof window & {
|
|
__OPENCHAMBER_API_BASE_URL__?: string;
|
|
__OPENCHAMBER_CLIENT_TOKEN__?: string;
|
|
__OPENCHAMBER_RUNTIME_HEADERS__?: Record<string, string>;
|
|
};
|
|
setWindowRuntimeValue(runtimeWindow, '__OPENCHAMBER_API_BASE_URL__', apiBaseUrl);
|
|
setWindowRuntimeValue(runtimeWindow, '__OPENCHAMBER_CLIENT_TOKEN__', options.clientToken || undefined);
|
|
setWindowRuntimeValue(runtimeWindow, '__OPENCHAMBER_RUNTIME_HEADERS__', options.requestHeaders || undefined);
|
|
}
|
|
configureRuntimeUrlResolver({ apiBaseUrl, realtimeBaseUrl: apiBaseUrl });
|
|
setRuntimeExtraHeaders(options.requestHeaders || null);
|
|
setRuntimeBearerToken(options.clientToken || null);
|
|
void refreshRuntimeUrlAuthToken(apiBaseUrl).catch(() => {});
|
|
if (typeof window !== 'undefined') {
|
|
window.dispatchEvent(new CustomEvent<RuntimeEndpointChangedDetail>(RUNTIME_ENDPOINT_CHANGED_EVENT, {
|
|
detail: { apiBaseUrl, previousApiBaseUrl, runtimeKey, previousRuntimeKey },
|
|
}));
|
|
}
|
|
};
|
|
|
|
export const subscribeRuntimeEndpointChanged = (callback: (detail: RuntimeEndpointChangedDetail) => void): (() => void) => {
|
|
if (typeof window === 'undefined') return () => {};
|
|
const listener = (event: Event) => {
|
|
callback((event as CustomEvent<RuntimeEndpointChangedDetail>).detail);
|
|
};
|
|
window.addEventListener(RUNTIME_ENDPOINT_CHANGED_EVENT, listener);
|
|
return () => window.removeEventListener(RUNTIME_ENDPOINT_CHANGED_EVENT, listener);
|
|
};
|