import type { SsrFPolicy } from "../infra/net/ssrf.js"; function normalizeHostnameSuffix(value: string): string { const trimmed = value.trim().toLowerCase(); if (!trimmed) { return ""; } if (trimmed === "*" || trimmed === "*.") { return "*"; } const withoutWildcard = trimmed.replace(/^\*\.?/, ""); const withoutLeadingDot = withoutWildcard.replace(/^\.+/, ""); return withoutLeadingDot.replace(/\.+$/, ""); } function isHostnameAllowedBySuffixAllowlist( hostname: string, allowlist: readonly string[], ): boolean { if (allowlist.includes("*")) { return true; } const normalized = hostname.toLowerCase(); return allowlist.some((entry) => normalized === entry || normalized.endsWith(`.${entry}`)); } export function normalizeHostnameSuffixAllowlist( input?: readonly string[], defaults?: readonly string[], ): string[] { const source = input && input.length > 0 ? input : defaults; if (!source || source.length === 0) { return []; } const normalized = source.map(normalizeHostnameSuffix).filter(Boolean); if (normalized.includes("*")) { return ["*"]; } return Array.from(new Set(normalized)); } export function isHttpsUrlAllowedByHostnameSuffixAllowlist( url: string, allowlist: readonly string[], ): boolean { try { const parsed = new URL(url); if (parsed.protocol !== "https:") { return false; } return isHostnameAllowedBySuffixAllowlist(parsed.hostname, allowlist); } catch { return false; } } /** * Converts suffix-style host allowlists (for example "example.com") into SSRF * hostname allowlist patterns used by the shared fetch guard. * * Suffix semantics: * - "example.com" allows "example.com" and "*.example.com" * - "*" disables hostname allowlist restrictions */ export function buildHostnameAllowlistPolicyFromSuffixAllowlist( allowHosts?: readonly string[], ): SsrFPolicy | undefined { const normalizedAllowHosts = normalizeHostnameSuffixAllowlist(allowHosts); if (normalizedAllowHosts.length === 0) { return undefined; } const patterns = new Set(); for (const normalized of normalizedAllowHosts) { if (normalized === "*") { return undefined; } patterns.add(normalized); patterns.add(`*.${normalized}`); } if (patterns.size === 0) { return undefined; } return { hostnameAllowlist: Array.from(patterns) }; }