{"version":3,"file":"url.mjs","names":[],"sources":["../../src/utils/url.ts"],"sourcesContent":["import type { BaseURLConfig, DynamicBaseURLConfig } from \"@better-auth/core\";\nimport { env } from \"@better-auth/core/env\";\nimport { BetterAuthError } from \"@better-auth/core/error\";\nimport { wildcardMatch } from \"./wildcard\";\n\nfunction checkHasPath(url: string): boolean {\n\ttry {\n\t\tconst parsedUrl = new URL(url);\n\t\tconst pathname = parsedUrl.pathname.replace(/\\/+$/, \"\") || \"/\";\n\t\treturn pathname !== \"/\";\n\t} catch {\n\t\tthrow new BetterAuthError(\n\t\t\t`Invalid base URL: ${url}. Please provide a valid base URL.`,\n\t\t);\n\t}\n}\n\nfunction assertHasProtocol(url: string): void {\n\ttry {\n\t\tconst parsedUrl = new URL(url);\n\t\tif (parsedUrl.protocol !== \"http:\" && parsedUrl.protocol !== \"https:\") {\n\t\t\tthrow new BetterAuthError(\n\t\t\t\t`Invalid base URL: ${url}. URL must include 'http://' or 'https://'`,\n\t\t\t);\n\t\t}\n\t} catch (error) {\n\t\tif (error instanceof BetterAuthError) {\n\t\t\tthrow error;\n\t\t}\n\t\tthrow new BetterAuthError(\n\t\t\t`Invalid base URL: ${url}. Please provide a valid base URL.`,\n\t\t\t{\n\t\t\t\tcause: error,\n\t\t\t},\n\t\t);\n\t}\n}\n\nfunction withPath(url: string, path = \"/api/auth\") {\n\tassertHasProtocol(url);\n\n\tconst hasPath = checkHasPath(url);\n\tif (hasPath) {\n\t\treturn url;\n\t}\n\n\tconst trimmedUrl = url.replace(/\\/+$/, \"\");\n\n\tif (!path || path === \"/\") {\n\t\treturn trimmedUrl;\n\t}\n\n\tpath = path.startsWith(\"/\") ? path : `/${path}`;\n\treturn `${trimmedUrl}${path}`;\n}\n\nfunction validateProxyHeader(header: string, type: \"host\" | \"proto\"): boolean {\n\tif (!header || header.trim() === \"\") {\n\t\treturn false;\n\t}\n\n\tif (type === \"proto\") {\n\t\t// Only allow http and https protocols\n\t\treturn header === \"http\" || header === \"https\";\n\t}\n\n\tif (type === \"host\") {\n\t\tconst suspiciousPatterns = [\n\t\t\t/\\.\\./, // Path traversal\n\t\t\t/\\0/, // Null bytes\n\t\t\t/[\\s]/, // Whitespace (except legitimate spaces that should be trimmed)\n\t\t\t/^[.]/, // Starting with dot\n\t\t\t/[<>'\"]/, // HTML/script injection characters\n\t\t\t/javascript:/i, // Protocol injection\n\t\t\t/file:/i, // File protocol\n\t\t\t/data:/i, // Data protocol\n\t\t];\n\n\t\tif (suspiciousPatterns.some((pattern) => pattern.test(header))) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Basic hostname validation (allows localhost, IPs, and domains with ports)\n\t\t// This is a simple check, not exhaustive RFC validation\n\t\tconst hostnameRegex =\n\t\t\t/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(:[0-9]{1,5})?$/;\n\n\t\t// Also allow IPv4 addresses\n\t\tconst ipv4Regex = /^(\\d{1,3}\\.){3}\\d{1,3}(:[0-9]{1,5})?$/;\n\n\t\t// Also allow IPv6 addresses in brackets\n\t\tconst ipv6Regex = /^\\[[0-9a-fA-F:]+\\](:[0-9]{1,5})?$/;\n\n\t\t// Allow localhost variations\n\t\tconst localhostRegex = /^localhost(:[0-9]{1,5})?$/i;\n\n\t\treturn (\n\t\t\thostnameRegex.test(header) ||\n\t\t\tipv4Regex.test(header) ||\n\t\t\tipv6Regex.test(header) ||\n\t\t\tlocalhostRegex.test(header)\n\t\t);\n\t}\n\n\treturn false;\n}\n\nexport function getBaseURL(\n\turl?: string,\n\tpath?: string,\n\trequest?: Request,\n\tloadEnv?: boolean,\n\ttrustedProxyHeaders?: boolean | undefined,\n) {\n\tif (url) {\n\t\treturn withPath(url, path);\n\t}\n\n\tif (loadEnv !== false) {\n\t\tconst fromEnv =\n\t\t\tenv.BETTER_AUTH_URL ||\n\t\t\tenv.NEXT_PUBLIC_BETTER_AUTH_URL ||\n\t\t\tenv.PUBLIC_BETTER_AUTH_URL ||\n\t\t\tenv.NUXT_PUBLIC_BETTER_AUTH_URL ||\n\t\t\tenv.NUXT_PUBLIC_AUTH_URL ||\n\t\t\t(env.BASE_URL !== \"/\" ? env.BASE_URL : undefined);\n\n\t\tif (fromEnv) {\n\t\t\treturn withPath(fromEnv, path);\n\t\t}\n\t}\n\n\tconst fromRequest = request?.headers.get(\"x-forwarded-host\");\n\tconst fromRequestProto = request?.headers.get(\"x-forwarded-proto\");\n\tif (fromRequest && fromRequestProto && trustedProxyHeaders) {\n\t\tif (\n\t\t\tvalidateProxyHeader(fromRequestProto, \"proto\") &&\n\t\t\tvalidateProxyHeader(fromRequest, \"host\")\n\t\t) {\n\t\t\ttry {\n\t\t\t\treturn withPath(`${fromRequestProto}://${fromRequest}`, path);\n\t\t\t} catch (_error) {}\n\t\t}\n\t}\n\n\tif (request) {\n\t\tconst url = getOrigin(request.url);\n\t\tif (!url) {\n\t\t\tthrow new BetterAuthError(\n\t\t\t\t\"Could not get origin from request. Please provide a valid base URL.\",\n\t\t\t);\n\t\t}\n\t\treturn withPath(url, path);\n\t}\n\n\tif (typeof window !== \"undefined\" && window.location) {\n\t\treturn withPath(window.location.origin, path);\n\t}\n\treturn undefined;\n}\n\nexport function getOrigin(url: string) {\n\ttry {\n\t\tconst parsedUrl = new URL(url);\n\t\t// For custom URL schemes (like exp://), the origin property returns the string \"null\"\n\t\t// instead of null. We need to handle this case and return null so the fallback logic works.\n\t\treturn parsedUrl.origin === \"null\" ? null : parsedUrl.origin;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function getProtocol(url: string) {\n\ttry {\n\t\tconst parsedUrl = new URL(url);\n\t\treturn parsedUrl.protocol;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function getHost(url: string) {\n\ttry {\n\t\tconst parsedUrl = new URL(url);\n\t\treturn parsedUrl.host;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Checks if the baseURL config is a dynamic config object\n */\nexport function isDynamicBaseURLConfig(\n\tconfig: BaseURLConfig | undefined,\n): config is DynamicBaseURLConfig {\n\treturn (\n\t\ttypeof config === \"object\" &&\n\t\tconfig !== null &&\n\t\t\"allowedHosts\" in config &&\n\t\tArray.isArray(config.allowedHosts)\n\t);\n}\n\n/**\n * Extracts the host from the request headers.\n * Tries x-forwarded-host first (for proxy setups), then falls back to host header.\n *\n * @param request The incoming request\n * @returns The host string or null if not found\n */\nexport function getHostFromRequest(request: Request): string | null {\n\tconst forwardedHost = request.headers.get(\"x-forwarded-host\");\n\tif (forwardedHost && validateProxyHeader(forwardedHost, \"host\")) {\n\t\treturn forwardedHost;\n\t}\n\n\tconst host = request.headers.get(\"host\");\n\tif (host && validateProxyHeader(host, \"host\")) {\n\t\treturn host;\n\t}\n\n\ttry {\n\t\tconst url = new URL(request.url);\n\t\treturn url.host;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Extracts the protocol from the request headers.\n * Tries x-forwarded-proto first (for proxy setups), then infers from request URL.\n *\n * @param request The incoming request\n * @param configProtocol Protocol override from config\n * @returns The protocol (\"http\" or \"https\")\n */\nexport function getProtocolFromRequest(\n\trequest: Request,\n\tconfigProtocol?: \"http\" | \"https\" | \"auto\" | undefined,\n): \"http\" | \"https\" {\n\tif (configProtocol === \"http\" || configProtocol === \"https\") {\n\t\treturn configProtocol;\n\t}\n\n\tconst forwardedProto = request.headers.get(\"x-forwarded-proto\");\n\tif (forwardedProto && validateProxyHeader(forwardedProto, \"proto\")) {\n\t\treturn forwardedProto as \"http\" | \"https\";\n\t}\n\n\ttry {\n\t\tconst url = new URL(request.url);\n\t\tif (url.protocol === \"http:\" || url.protocol === \"https:\") {\n\t\t\treturn url.protocol.slice(0, -1) as \"http\" | \"https\";\n\t\t}\n\t} catch {}\n\n\treturn \"https\";\n}\n\n/**\n * Matches a hostname against a host pattern.\n * Supports wildcard patterns like `*.vercel.app` or `preview-*.myapp.com`.\n *\n * @param host The hostname to test (e.g., \"myapp.com\", \"preview-123.vercel.app\")\n * @param pattern The host pattern (e.g., \"myapp.com\", \"*.vercel.app\")\n * @returns {boolean} true if the host matches the pattern, false otherwise.\n *\n * @example\n * ```ts\n * matchesHostPattern(\"myapp.com\", \"myapp.com\") // true\n * matchesHostPattern(\"preview-123.vercel.app\", \"*.vercel.app\") // true\n * matchesHostPattern(\"preview-123.myapp.com\", \"preview-*.myapp.com\") // true\n * matchesHostPattern(\"evil.com\", \"myapp.com\") // false\n * ```\n */\nexport const matchesHostPattern = (host: string, pattern: string): boolean => {\n\tif (!host || !pattern) {\n\t\treturn false;\n\t}\n\n\t// Normalize: remove protocol if accidentally included, lowercase for case-insensitive matching\n\tconst normalizedHost = host\n\t\t.replace(/^https?:\\/\\//, \"\")\n\t\t.split(\"/\")[0]!\n\t\t.toLowerCase();\n\tconst normalizedPattern = pattern\n\t\t.replace(/^https?:\\/\\//, \"\")\n\t\t.split(\"/\")[0]!\n\t\t.toLowerCase();\n\n\t// Check if pattern contains wildcard characters\n\tconst hasWildcard =\n\t\tnormalizedPattern.includes(\"*\") || normalizedPattern.includes(\"?\");\n\n\tif (hasWildcard) {\n\t\treturn wildcardMatch(normalizedPattern)(normalizedHost);\n\t}\n\n\t// Exact match (case-insensitive for hostnames)\n\treturn normalizedHost.toLowerCase() === normalizedPattern.toLowerCase();\n};\n\n/**\n * Resolves the base URL from a dynamic config based on the incoming request.\n * Validates the derived host against the allowedHosts allowlist.\n *\n * @param config The dynamic base URL config\n * @param request The incoming request\n * @param basePath The base path to append\n * @returns The resolved base URL with path\n * @throws BetterAuthError if host is not in allowedHosts and no fallback is set\n */\nexport function resolveDynamicBaseURL(\n\tconfig: DynamicBaseURLConfig,\n\trequest: Request,\n\tbasePath: string,\n): string {\n\tconst host = getHostFromRequest(request);\n\n\tif (!host) {\n\t\tif (config.fallback) {\n\t\t\treturn withPath(config.fallback, basePath);\n\t\t}\n\t\tthrow new BetterAuthError(\n\t\t\t\"Could not determine host from request headers. \" +\n\t\t\t\t\"Please provide a fallback URL in your baseURL config.\",\n\t\t);\n\t}\n\n\tconst isAllowed = config.allowedHosts.some((pattern) =>\n\t\tmatchesHostPattern(host, pattern),\n\t);\n\n\tif (isAllowed) {\n\t\tconst protocol = getProtocolFromRequest(request, config.protocol);\n\t\treturn withPath(`${protocol}://${host}`, basePath);\n\t}\n\n\tif (config.fallback) {\n\t\treturn withPath(config.fallback, basePath);\n\t}\n\n\tthrow new BetterAuthError(\n\t\t`Host \"${host}\" is not in the allowed hosts list. ` +\n\t\t\t`Allowed hosts: ${config.allowedHosts.join(\", \")}. ` +\n\t\t\t`Add this host to your allowedHosts config or provide a fallback URL.`,\n\t);\n}\n\n/**\n * Resolves the base URL from any config type (static string or dynamic object).\n * This is the main entry point for base URL resolution.\n *\n * @param config The base URL config (string or object)\n * @param basePath The base path to append\n * @param request Optional request for dynamic resolution\n * @param loadEnv Whether to load from environment variables\n * @param trustedProxyHeaders Whether to trust proxy headers (for legacy behavior)\n * @returns The resolved base URL with path\n */\nexport function resolveBaseURL(\n\tconfig: BaseURLConfig | undefined,\n\tbasePath: string,\n\trequest?: Request,\n\tloadEnv?: boolean,\n\ttrustedProxyHeaders?: boolean,\n): string | undefined {\n\tif (isDynamicBaseURLConfig(config)) {\n\t\tif (request) {\n\t\t\treturn resolveDynamicBaseURL(config, request, basePath);\n\t\t}\n\t\tif (config.fallback) {\n\t\t\treturn withPath(config.fallback, basePath);\n\t\t}\n\t\treturn getBaseURL(\n\t\t\tundefined,\n\t\t\tbasePath,\n\t\t\trequest,\n\t\t\tloadEnv,\n\t\t\ttrustedProxyHeaders,\n\t\t);\n\t}\n\n\tif (typeof config === \"string\") {\n\t\treturn getBaseURL(config, basePath, request, loadEnv, trustedProxyHeaders);\n\t}\n\n\treturn getBaseURL(undefined, basePath, request, loadEnv, trustedProxyHeaders);\n}\n"],"mappings":"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"}