export const OUTBOUND_PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"] as const; export const PROXY_ENV_KEYS = [...OUTBOUND_PROXY_ENV_KEYS, "NO_PROXY"] as const; export type ProxyEnvKey = typeof PROXY_ENV_KEYS[number]; export type ProxyEnvMap = Record; export type ProxyRoute = | { kind: "direct" } | { kind: "proxy"; proxy: string } | { kind: "fallback" }; export function normalizeProxyHostname(hostname: string): string { const normalized = hostname.trim().toLowerCase().replace(/\.+$/, ""); return normalized.startsWith("[") && normalized.endsWith("]") ? normalized.slice(1, -1) : normalized; } export function noProxyMatches( url: URL, env: ProxyEnvMap = process.env, ): boolean { const raw = env.NO_PROXY ?? env.no_proxy ?? ""; const hostname = normalizeProxyHostname(url.hostname); const port = url.port || (url.protocol === "https:" || url.protocol === "wss:" ? "443" : "80"); for (const rawEntry of raw.split(",")) { let entry = rawEntry.trim().toLowerCase(); if (!entry) continue; if (entry === "*") return true; entry = entry.replace(/^(?:https?|wss?):\/\//, "").split("/", 1)[0]!; let entryHost = entry; let entryPort = ""; const bracketed = /^\[([^\]]+)](?::(\d+))?$/.exec(entry); if (bracketed) { entryHost = bracketed[1]!; entryPort = bracketed[2] ?? ""; } else if ((entry.match(/:/g)?.length ?? 0) === 1) { const separator = entry.lastIndexOf(":"); const possiblePort = entry.slice(separator + 1); if (/^\d+$/.test(possiblePort)) { entryHost = entry.slice(0, separator); entryPort = possiblePort; } } if (entryPort && entryPort !== port) continue; entryHost = normalizeProxyHostname(entryHost.replace(/^\*?\./, "")); if (entryHost && (hostname === entryHost || hostname.endsWith(`.${entryHost}`))) return true; } return false; } export function resolveProxyRoute( url: URL, env: ProxyEnvMap = process.env, ): ProxyRoute { if (noProxyMatches(url, env)) return { kind: "direct" }; const key = url.protocol === "https:" || url.protocol === "wss:" ? "HTTPS_PROXY" : "HTTP_PROXY"; const proxy = [key, key.toLowerCase(), "ALL_PROXY", "all_proxy"] .map(candidate => env[candidate]?.trim()) .find(Boolean); if (!proxy) return { kind: "direct" }; try { const protocol = new URL(proxy).protocol; return protocol === "http:" || protocol === "https:" ? { kind: "proxy", proxy } : { kind: "fallback" }; } catch { return { kind: "fallback" }; } } export function proxyEnvPresent( key: ProxyEnvKey, env: ProxyEnvMap = process.env, ): boolean { return Boolean(env[key]?.trim() || env[key.toLowerCase()]?.trim()); } export function outboundProxyConfigured( env: ProxyEnvMap = process.env, ): boolean { return OUTBOUND_PROXY_ENV_KEYS.some(key => proxyEnvPresent(key, env)); } /** * The proxy URL that Bun's fetch will actually use for `url`, or null when none applies. * * Bun selects by scheme: `HTTPS_PROXY` for `https:` targets, `HTTP_PROXY` for `http:`. * `ALL_PROXY` is deliberately not consulted here — fetch does not honour it, so a caller * that needs "this request will ride the proxy" as a precondition must not count it. * Presence of *some* proxy variable (`outboundProxyConfigured`) is not that guarantee. */ export function effectiveProxyFor( url: URL, env: ProxyEnvMap = process.env, ): string | null { const key: ProxyEnvKey | null = url.protocol === "https:" ? "HTTPS_PROXY" : url.protocol === "http:" ? "HTTP_PROXY" : null; if (!key) return null; const value = env[key]?.trim() || env[key.toLowerCase()]?.trim(); return value ? value : null; } function proxyValue(key: ProxyEnvKey, env: ProxyEnvMap): string | undefined { const value = env[key]?.trim() || env[key.toLowerCase()]?.trim(); return value || undefined; } export function proxyForUrl(url: string | URL, env: ProxyEnvMap = process.env): string | undefined { let parsed: URL; try { parsed = new URL(url); } catch { return undefined; } if (noProxyMatches(parsed, env)) return undefined; if (parsed.protocol === "https:") return proxyValue("HTTPS_PROXY", env) ?? proxyValue("ALL_PROXY", env); if (parsed.protocol === "http:") return proxyValue("HTTP_PROXY", env) ?? proxyValue("ALL_PROXY", env); return undefined; }