import { buildRuntimeAuthHeaders } from './runtime-auth'; import { getRuntimeUrlResolver, type RuntimeUrlQuery } from './runtime-url'; export interface RuntimeFetchOptions extends RequestInit { query?: RuntimeUrlQuery; } const shouldResolveApiPath = (input: string): boolean => { return input.startsWith('/api/') || input === '/api' || input.startsWith('/auth/') || input === '/auth' || input === '/health'; }; const getCurrentOrigin = (): string => { if (typeof window === 'undefined') return ''; return window.location.origin || ''; }; const isCurrentWindowUrl = (url: URL): boolean => { if (typeof window === 'undefined') return false; const currentOrigin = getCurrentOrigin(); if (currentOrigin && url.origin === currentOrigin) return true; try { const current = new URL(window.location.href || currentOrigin); return url.protocol === current.protocol && url.host === current.host; } catch { return false; } }; const isAbsoluteUrl = (value: string): boolean => /^[a-z][a-z\d+.-]*:\/\//i.test(value); const appendRuntimeQuery = (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 isActiveRuntimeServiceUrl = (url: URL): boolean => { try { const apiBase = getRuntimeUrlResolver().api('/api'); if (!/^[a-z][a-z\d+.-]*:\/\//i.test(apiBase)) return false; const base = new URL(apiBase); if (url.origin !== base.origin) return false; return shouldResolveApiPath(url.pathname); } catch { return false; } }; const shouldResolveFetchInput = (input: string): boolean => { if (shouldResolveApiPath(input)) return true; if (!/^[a-z][a-z\d+.-]*:\/\//i.test(input)) return false; try { const url = new URL(input); return isCurrentWindowUrl(url) && shouldResolveApiPath(url.pathname); } catch { return false; } }; const buildRuntimeFetchUrlFromAbsolute = (input: string, query?: RuntimeUrlQuery): string => { try { const url = new URL(input); if (!isCurrentWindowUrl(url)) return input; const rewritten = buildRuntimeFetchUrl(`${url.pathname}${url.search}`, query); if (!isAbsoluteUrl(rewritten) && (url.protocol === 'http:' || url.protocol === 'https:')) { appendRuntimeQuery(url, query); return url.toString(); } return url.hash ? `${rewritten}${url.hash}` : rewritten; } catch { return input; } }; export const buildRuntimeFetchUrl = (input: string, query?: RuntimeUrlQuery): string => { if (input === '/health') return getRuntimeUrlResolver().health(query); if (input.startsWith('/auth/') || input === '/auth') return getRuntimeUrlResolver().auth(input, query); if (shouldResolveApiPath(input)) return getRuntimeUrlResolver().api(input, query); if (/^[a-z][a-z\d+.-]*:\/\//i.test(input)) return buildRuntimeFetchUrlFromAbsolute(input, query); return input; }; const shouldAttachRuntimeAuth = (input: string | URL | Request): boolean => { const raw = input instanceof Request ? input.url : input.toString(); if (!isAbsoluteUrl(raw)) { return shouldResolveApiPath(raw); } try { return isActiveRuntimeServiceUrl(new URL(raw)); } catch { return false; } }; const mergeHeaders = async (inputHeaders?: HeadersInit, initHeaders?: HeadersInit, attachAuth = true): Promise => { const headers = new Headers(inputHeaders); if (initHeaders) { new Headers(initHeaders).forEach((value, key) => headers.set(key, value)); } if (!attachAuth) { return headers; } return buildRuntimeAuthHeaders(headers); }; const resolveRuntimeFetchInput = (input: string | URL | Request, query?: RuntimeUrlQuery): string | URL | Request => { if (typeof input === 'string') { return buildRuntimeFetchUrl(input, query); } if (input instanceof URL) { return buildRuntimeFetchUrl(input.toString(), query); } const target = buildRuntimeFetchUrl(input.url, query); return target === input.url ? input : new Request(target, input); }; export const runtimeFetch = async (input: string | URL | Request, init: RuntimeFetchOptions = {}): Promise => { const { query, ...requestInit } = init; const resolvedInput = resolveRuntimeFetchInput(input, query); const inputHeaders = resolvedInput instanceof Request ? resolvedInput.headers : undefined; const headers = await mergeHeaders(inputHeaders, requestInit.headers, shouldAttachRuntimeAuth(resolvedInput)); if (resolvedInput instanceof Request) { return fetch(new Request(resolvedInput, { ...requestInit, headers })); } return fetch(resolvedInput, { ...requestInit, headers, }); }; let runtimeFetchBridgeInstalled = false; export const installRuntimeFetchBridge = (): void => { if (runtimeFetchBridgeInstalled || typeof window === 'undefined') return; runtimeFetchBridgeInstalled = true; const nativeFetch = window.fetch.bind(window); window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise => { if (typeof input === 'string') { if (!shouldResolveFetchInput(input)) { try { const url = new URL(input); if (isActiveRuntimeServiceUrl(url)) { const headers = await mergeHeaders(undefined, init?.headers); return nativeFetch(input, { ...init, headers }); } } catch { // Non-URL fetch inputs should fall through unchanged. } return nativeFetch(input, init); } const headers = await mergeHeaders(undefined, init?.headers); return nativeFetch(buildRuntimeFetchUrl(input), { ...init, headers }); } if (input instanceof URL) { const raw = input.toString(); if (!shouldResolveFetchInput(raw)) { if (isActiveRuntimeServiceUrl(input)) { const headers = await mergeHeaders(undefined, init?.headers); return nativeFetch(input, { ...init, headers }); } return nativeFetch(input, init); } const headers = await mergeHeaders(undefined, init?.headers); return nativeFetch(buildRuntimeFetchUrl(raw), { ...init, headers }); } if (input instanceof Request) { if (!shouldResolveFetchInput(input.url)) { try { const url = new URL(input.url); if (isActiveRuntimeServiceUrl(url)) { const headers = await mergeHeaders(input.headers, init?.headers); return nativeFetch(new Request(input, { ...init, headers })); } } catch { // Non-URL request inputs should fall through unchanged. } return nativeFetch(input, init); } const headers = await mergeHeaders(input.headers, init?.headers); const target = buildRuntimeFetchUrl(input.url); const request = target === input.url ? input : new Request(target, input); return nativeFetch(new Request(request, { ...init, headers })); } return nativeFetch(input, init); }; };