import { isLoopbackHost } from "@/worker/services/url"; // Canonicalize a configured URL down to its origin. Rejects values that // would split discovery, ID-token `iss`, cookie domain, or invite URLs: // non-http(s) schemes, non-loopback http, embedded credentials, query, // hash, or any non-root pathname. Throw shape never echoes the offending // value because env vars may carry username/password components. const canonicalOrigin = (value: string, envName: string): string => { let url: URL; try { url = new URL(value.trim()); } catch { throw new Error(`${envName} is not a valid URL.`); } if (url.protocol !== "https:" && url.protocol !== "http:") { throw new Error(`${envName} must use http or https.`); } if (url.protocol === "http:" && !isLoopbackHost(url.hostname)) { throw new Error(`${envName} with http: is allowed only on loopback hosts.`); } if (url.username || url.password || url.search || url.hash) { throw new Error(`${envName} must not include path, query, hash, or credentials.`); } if (url.pathname !== "/" && url.pathname !== "") { throw new Error(`${envName} must not include path, query, hash, or credentials.`); } return url.origin; }; /** * Resolve the public base URL the worker should advertise (OIDC `iss`, * Better Auth `baseURL`, cookie domain). * * Resolution order: * 1. `BETTER_AUTH_URL` env var if set and non-empty. * 2. The request's own origin. * * Production always sets `BETTER_AUTH_URL` (wrangler.jsonc `vars`), so the * fallback only fires in dev/test where the worker should reflect whatever * hostname the request was made to. */ export const resolveBaseUrl = (env: Env, requestUrl: string): string => { const configured = env.BETTER_AUTH_URL?.trim(); if (configured) { return canonicalOrigin(configured, "BETTER_AUTH_URL"); } return new URL(requestUrl).origin; }; export const resolveIssuer = (env: Env, requestUrl: string): string => { const configured = env.OIDC_ISSUER?.trim(); if (configured) { return canonicalOrigin(configured, "OIDC_ISSUER"); } return resolveBaseUrl(env, requestUrl); };