Files
openchamber/packages/ui/src/lib/runtime-url.ts
T

141 lines
4.7 KiB
TypeScript
Raw Normal View History

import { getRuntimeUrlAuthTokenSync } from '@/lib/runtime-auth';
type QueryValue = string | number | boolean | null | undefined;
export type RuntimeUrlQuery = Record<string, QueryValue> | URLSearchParams;
export interface RuntimeUrlConfig {
apiBaseUrl?: string | null;
realtimeBaseUrl?: string | null;
currentHref?: () => string;
}
export interface RuntimeUrlResolver {
api(path: string, query?: RuntimeUrlQuery): string;
authenticatedAsset(path: string, query?: RuntimeUrlQuery): string;
auth(path: string, query?: RuntimeUrlQuery): string;
health(query?: RuntimeUrlQuery): string;
rawFile(path: string, options?: { download?: boolean }): string;
sse(path: string, query?: RuntimeUrlQuery): string;
websocket(path: string, query?: RuntimeUrlQuery): string;
}
const ABSOLUTE_URL_PATTERN = /^[a-z][a-z\d+.-]*:\/\//i;
const normalizePath = (path: string): string => {
const trimmed = path.trim();
if (!trimmed) return '/';
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
};
const normalizeBaseUrl = (value: string | null | undefined): string => {
if (typeof value !== 'string') return '';
return value.trim().replace(/\/+$/, '');
};
const currentHref = (config: RuntimeUrlConfig): string => {
const configured = config.currentHref?.();
if (configured) return configured;
if (typeof window !== 'undefined') {
return window.location.href || window.location.origin;
}
return '';
};
const appendQuery = (url: URL, query?: RuntimeUrlQuery): void => {
if (!query) return;
const entries = query instanceof URLSearchParams
? Array.from(query.entries())
: Object.entries(query);
for (const [key, value] of entries) {
if (value === null || value === undefined) continue;
url.searchParams.set(key, String(value));
}
};
const appendRelativeQuery = (path: string, query?: RuntimeUrlQuery): string => {
if (!query) return path;
const params = new URLSearchParams();
appendQuery({ searchParams: params } as URL, query);
const serialized = params.toString();
if (!serialized) return path;
return path.includes('?') ? `${path}&${serialized}` : `${path}?${serialized}`;
};
const buildHttpUrl = (baseUrl: string, path: string, query?: RuntimeUrlQuery): string => {
if (ABSOLUTE_URL_PATTERN.test(path)) {
const url = new URL(path);
appendQuery(url, query);
return url.toString();
}
const normalizedPath = normalizePath(path);
if (!baseUrl) {
return appendRelativeQuery(normalizedPath, query);
}
const url = new URL(normalizedPath, `${baseUrl}/`);
appendQuery(url, query);
return url.toString();
};
const withUrlAuth = (urlValue: string): string => {
const token = getRuntimeUrlAuthTokenSync();
if (!token) return urlValue;
if (ABSOLUTE_URL_PATTERN.test(urlValue)) {
const url = new URL(urlValue);
url.searchParams.set('oc_url_token', token);
return url.toString();
}
const separator = urlValue.includes('?') ? '&' : '?';
return `${urlValue}${separator}oc_url_token=${encodeURIComponent(token)}`;
};
const toWebSocketUrl = (candidate: string, config: RuntimeUrlConfig): string => {
const url = ABSOLUTE_URL_PATTERN.test(candidate)
? new URL(candidate)
: new URL(candidate, currentHref(config));
if (url.protocol === 'ws:' || url.protocol === 'wss:') {
return url.toString();
}
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
return url.toString();
};
export const createRuntimeUrlResolver = (config: RuntimeUrlConfig = {}): RuntimeUrlResolver => {
const apiBaseUrl = normalizeBaseUrl(config.apiBaseUrl);
const realtimeBaseUrl = normalizeBaseUrl(config.realtimeBaseUrl) || 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,
authenticatedAsset: (path, query) => withUrlAuth(http(path, query)),
auth: http,
health: (query) => http('/health', query),
rawFile: (path, options) => http('/api/fs/raw', { path, download: options?.download === true ? true : undefined }),
sse: (path, query) => withUrlAuth(realtime(path, query)),
websocket: (path, query) => toWebSocketUrl(withUrlAuth(realtime(path, query)), config),
};
};
let activeRuntimeUrlResolver = createRuntimeUrlResolver();
export const getRuntimeUrlResolver = (): RuntimeUrlResolver => activeRuntimeUrlResolver;
export const setRuntimeUrlResolver = (resolver: RuntimeUrlResolver): void => {
activeRuntimeUrlResolver = resolver;
};
export const configureRuntimeUrlResolver = (config: RuntimeUrlConfig): RuntimeUrlResolver => {
activeRuntimeUrlResolver = createRuntimeUrlResolver(config);
return activeRuntimeUrlResolver;
};
export const runtimeUrl = activeRuntimeUrlResolver;