import { getDomainRoutingConfig, loadRequiredWebConfig, type DomainRoutingEnv, type EnvRecord } from "@toolctrl/config"; import { getCanonicalHostRedirectUrl, getHostRoutingContextFromRequest, getRequestRoutingHost } from "./app/HostRouting.server"; import { isAbsoluteWebHostPath, pathMatchesRoutePrefix, protectedWebRouteExactPaths, protectedWebRoutePrefixes } from "./app/PublicRouteSegments"; type UrlServerOptions = { https?: boolean; env?: EnvRecord & DomainRoutingEnv; }; export const getBaseURL = (request: Request, options?: UrlServerOptions) => { const config = loadRequiredWebConfig(options?.env); const url = new URL(request.url); const host = getRequestRoutingHost(request, url); const forwardedProtocol = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim(); if (config.nodeEnv === "development") { return `http://${host}`; } if (options?.https || config.nodeEnv === "production") { return `https://${host}`; } else if (forwardedProtocol) { return `${forwardedProtocol}://${host}`; } else { return `${url.protocol}//${host}`; } }; export const getDomainName = (request: Request) => { const url = new URL(request.url); return getRequestRoutingHost(request, url); }; export const getAbsoluteUrl = (request: Request, path: string, options?: UrlServerOptions) => { const config = getDomainRoutingConfig(options?.env); const context = getHostRoutingContextFromRequest(request, options?.env); const targetPath = normalizeAbsoluteUrlInputToPath(path, config); const host = getTargetHostForPath(targetPath, context, config); const targetUrl = new URL(targetPath, `${config.protocol}://${host}`); const canonicalRedirectUrl = getCanonicalHostRedirectUrl(targetUrl, config); if (canonicalRedirectUrl) { return canonicalRedirectUrl; } return targetUrl.toString(); }; function normalizeAbsoluteUrlInputToPath(path: string, config: ReturnType) { const parsedUrl = new URL(path, `${config.protocol}://${config.appBaseDomain}`); return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`; } function getTargetHostForPath(path: string, context: ReturnType, config: ReturnType) { const targetPath = new URL(path, `${config.protocol}://${config.appBaseDomain}`).pathname; if (targetPath === "/v1" || targetPath.startsWith("/v1/")) { return config.apiHost; } if (isAbsoluteWebHostPath(targetPath) || isProtectedWebHostPath(targetPath)) { return config.webAppHost; } return context === "app" ? config.webAppHost : context === "admin" ? config.adminAppHost : context === "api" ? config.apiHost : config.appBaseDomain; } function isProtectedWebHostPath(pathname: string) { return ( (protectedWebRouteExactPaths as readonly string[]).includes(pathname) || protectedWebRoutePrefixes.some((prefix) => pathMatchesRoutePrefix(pathname, prefix)) ); } export const getAbsoluteCurrentUrl = (request: Request, options?: UrlServerOptions) => { const config = getDomainRoutingConfig(options?.env); const url = new URL(request.url); const canonicalRedirectUrl = getCanonicalHostRedirectUrl(request, config); if (canonicalRedirectUrl) { return canonicalRedirectUrl; } const context = getHostRoutingContextFromRequest(request, options?.env); if (context === "app") { return `${config.protocol}://${config.webAppHost}${url.pathname}${url.search}${url.hash}`; } if (context === "admin") { return `${config.protocol}://${config.adminAppHost}${url.pathname}${url.search}${url.hash}`; } if (context === "api") { return `${config.protocol}://${config.apiHost}${url.pathname}${url.search}${url.hash}`; } return getAbsoluteUrl(request, `${url.pathname}${url.search}${url.hash}`, options); }; export const getAbsoluteApiUrl = (request: Request, path: string, options?: UrlServerOptions) => { const config = getDomainRoutingConfig(options?.env); const normalizedPath = path.startsWith("/") ? path : `/${path}`; const targetUrl = new URL(normalizedPath, `${config.protocol}://${config.apiHost}`); const strippedPath = targetUrl.pathname.replace(/^\/api\/v1(?=\/|$)/, "").replace(/^\/v1(?=\/|$)/, ""); const versionedPath = `/v1${strippedPath === "/" ? "" : strippedPath}`; return new URL(`${versionedPath}${targetUrl.search}${targetUrl.hash}`, `${config.protocol}://${config.apiHost}`).toString(); }; export const getWebhooksUrl = (request: Request, options?: UrlServerOptions) => { const config = loadRequiredWebConfig(options?.env); return config.webhooksUrl ?? getBaseURL(request, options); };