evOo-local/apps/api/src/web-origin.ts

36 lines
1.5 KiB
TypeScript

/** Hosts the API already trusts as browser origins: loopback and private networks. */
export function isPrivateOrLocalHost(host: string): boolean {
if (isLoopbackHost(host)) return true;
if (host.endsWith(".local")) return true;
if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(host)) return true;
if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host)) return true;
if (/^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/.test(host)) return true;
if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.\d{1,3}\.\d{1,3}$/.test(host)) return true;
return false;
}
export function isLoopbackHost(host: string): boolean {
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
}
/** The web origin that browser-facing URLs (OAuth returns, screen links) should
* point at for this request. A browser opened over a loopback, private-network,
* or explicitly trusted origin gets its own origin back so a phone on the LAN
* or Tailscale is not sent to 127.0.0.1; any other caller gets WEB_ORIGIN. */
export function requestWebOrigin(
origin: string | undefined,
webOrigin: string,
extraTrustedOrigins: readonly string[] = [],
): string {
if (!origin) return webOrigin;
try {
const url = new URL(origin);
if (url.protocol !== "http:" && url.protocol !== "https:") return webOrigin;
if (url.origin === new URL(webOrigin).origin) return url.origin;
if (extraTrustedOrigins.includes(url.origin)) return url.origin;
return isPrivateOrLocalHost(url.hostname) ? url.origin : webOrigin;
} catch {
return webOrigin;
}
}