import { lookup as dnsLookup } from "node:dns/promises";
import { createRequire } from "node:module";
import { fetchWithDnsPinnedAddresses } from "./dns-pinned-fetch.js";
import { attachDnsPinnedAddresses, isNativeOrBoundGlobalFetch, markFetchAsDnsPinned, resolveDnsPinnedFetch, supportsDnsPinnedFetch, } from "./fetch-capabilities.js";
import { getNetworkAddressFamily, isBlockedNetworkAddress, isBlockedNetworkHostname, normalizeNetworkHostname, } from "./network-safety.js";
const DEFAULT_MAX_REDIRECTS = 10;
const require = createRequire(import.meta.url);
async function defaultLookup(hostname) {
    return await dnsLookup(hostname, { all: true, verbatim: true });
}
function loadUndici() {
    return require("undici");
}
function createPinnedDispatcher(addresses) {
    const { Agent } = loadUndici();
    const pinnedAddresses = addresses.map((entry) => ({
        address: entry.address,
        family: entry.family || getNetworkAddressFamily(entry.address) || 4,
    }));
    return new Agent({
        autoSelectFamily: true,
        autoSelectFamilyAttemptTimeout: 250,
        connect: {
            lookup: (_hostname, options, callback) => {
                if (options?.all) {
                    callback(null, pinnedAddresses);
                    return;
                }
                const first = pinnedAddresses[0];
                callback(null, first?.address ?? "0.0.0.0", first?.family ?? 4);
            },
        },
    });
}
function isBunRuntime() {
    return typeof process.versions.bun === "string";
}
function getInputUrl(input) {
    if (typeof input === "string")
        return input;
    if (input instanceof URL)
        return input.href;
    return input.url;
}
function getMethod(input, init) {
    return (init?.method ?? (typeof input !== "string" && !(input instanceof URL) ? input.method : "GET")).toUpperCase();
}
function getRedirectMode(input, init) {
    return (init?.redirect ??
        (typeof input !== "string" && !(input instanceof URL) ? input.redirect : "follow") ??
        "follow");
}
async function resolveNetworkTarget(rawUrl, { lookup = defaultLookup, targetLabel, }) {
    let url;
    try {
        url = new URL(rawUrl);
    }
    catch {
        throw new Error(`${targetLabel} is invalid`);
    }
    if (url.protocol !== "http:" && url.protocol !== "https:") {
        throw new Error(`${targetLabel} must use http or https`);
    }
    const hostname = normalizeNetworkHostname(url.hostname);
    if (isBlockedNetworkHostname(hostname)) {
        throw new Error(`${targetLabel} resolves to a blocked local network host`);
    }
    if (getNetworkAddressFamily(hostname) !== 0) {
        if (isBlockedNetworkAddress(hostname)) {
            throw new Error(`${targetLabel} resolves to a blocked local network address`);
        }
        return { url, addresses: [] };
    }
    const addresses = await lookup(hostname);
    if (addresses.length === 0 || addresses.some((entry) => isBlockedNetworkAddress(entry.address))) {
        throw new Error(`${targetLabel} resolves to a blocked local network address`);
    }
    return { url, addresses };
}
export async function assertNetworkTargetAllowed(rawUrl, options) {
    await resolveNetworkTarget(rawUrl, options);
}
export function createNetworkGuardedFetch(fetchImpl, { targetLabel, lookup = defaultLookup, maxRedirects = DEFAULT_MAX_REDIRECTS, pinnedFetchImpl, }) {
    const guardedFetch = async (input, init, redirectCount = 0) => {
        const rawUrl = getInputUrl(input);
        const target = await resolveNetworkTarget(rawUrl, { lookup, targetLabel });
        const redirectMode = getRedirectMode(input, init);
        const requiresPinnedDns = target.addresses.length > 0;
        const isNativeFetch = isNativeOrBoundGlobalFetch(fetchImpl);
        if (requiresPinnedDns && !isNativeFetch && !supportsDnsPinnedFetch(fetchImpl)) {
            throw new Error(`${targetLabel} requires native fetch for DNS pinning`);
        }
        const pinnedInit = requiresPinnedDns
            ? attachDnsPinnedAddresses({
                ...init,
                dispatcher: createPinnedDispatcher(target.addresses),
            }, target.addresses)
            : init;
        const fetchForPinnedDns = requiresPinnedDns
            ? isNativeFetch
                ? (pinnedFetchImpl ?? (isBunRuntime() ? fetchWithDnsPinnedAddresses : loadUndici().fetch))
                : (resolveDnsPinnedFetch(fetchImpl) ?? fetchImpl)
            : fetchImpl;
        if (redirectMode !== "follow") {
            return await fetchForPinnedDns(input, pinnedInit);
        }
        const response = await fetchForPinnedDns(input, { ...pinnedInit, redirect: "manual" });
        if (![301, 302, 303, 307, 308].includes(response.status)) {
            return response;
        }
        const location = response.headers.get("location");
        if (!location)
            return response;
        if (redirectCount >= maxRedirects) {
            throw new Error(`${targetLabel} redirected too many times`);
        }
        const method = getMethod(input, init);
        if (method !== "GET" && method !== "HEAD") {
            throw new Error(`${targetLabel} redirected a non-GET request`);
        }
        const nextUrl = new URL(location, response.url || target.url.href).href;
        return await guardedFetch(nextUrl, { ...init, body: null, method }, redirectCount + 1);
    };
    return markFetchAsDnsPinned(guardedFetch);
}
//# sourceMappingURL=network-guard.js.map