199 lines
7.0 KiB
TypeScript
199 lines
7.0 KiB
TypeScript
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<Headers> => {
|
||
|
|
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<Response> => {
|
||
|
|
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<Response> => {
|
||
|
|
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);
|
||
|
|
};
|
||
|
|
};
|