import { resolveAndCheck, validateWebhookUrl } from './webhook.js';
export class SsrfBlockedError extends Error {
    url;
    reason;
    constructor(url, reason) {
        super(`SSRF blocked: ${reason} (url=${url})`);
        this.name = 'SsrfBlockedError';
        this.url = url;
        this.reason = reason;
    }
}
async function preflightUrl(url, options) {
    const check = validateWebhookUrl(url);
    if (!check.valid) {
        throw new SsrfBlockedError(url, check.error ?? 'URL rejected by SSRF policy');
    }
    let parsed;
    try {
        parsed = new URL(url);
    }
    catch {
        throw new SsrfBlockedError(url, 'invalid URL');
    }
    // resolveAndCheck short-circuits when the hostname is already an IP literal
    // (which validateWebhookUrl just covered), so this only does real DNS work
    // for actual hostnames.
    let resolved;
    try {
        resolved = await options.resolver(parsed.hostname);
    }
    catch (err) {
        if (options.requireDnsCheck) {
            throw new SsrfBlockedError(url, `DNS resolution failed: ${err instanceof Error ? err.message : String(err)}`);
        }
        return; // best-effort mode: proceed with string check only
    }
    if (!resolved.safe) {
        throw new SsrfBlockedError(url, resolved.error ?? 'hostname resolves to a private IP');
    }
}
export async function safeFetch(url, options = {}) {
    const { timeoutMs = 5000, followRedirects = false, maxRedirects = 3, requireDnsCheck = false, _resolver = resolveAndCheck, ...init } = options;
    let currentUrl = url;
    let hops = 0;
    while (true) {
        await preflightUrl(currentUrl, { requireDnsCheck, resolver: _resolver });
        const response = await fetch(currentUrl, {
            ...init,
            redirect: 'manual',
            signal: init.signal ?? AbortSignal.timeout(timeoutMs),
        });
        const isRedirect = response.status >= 300 && response.status < 400;
        if (!isRedirect || !followRedirects) {
            return response;
        }
        if (hops >= maxRedirects) {
            throw new SsrfBlockedError(currentUrl, `exceeded ${maxRedirects} redirects`);
        }
        const location = response.headers.get('location');
        if (!location)
            return response;
        try {
            currentUrl = new URL(location, currentUrl).toString();
        }
        catch {
            throw new SsrfBlockedError(location, 'invalid Location header');
        }
        hops += 1;
    }
}
//# sourceMappingURL=safe-fetch.js.map