"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isArgusWebFetchEnabled = isArgusWebFetchEnabled;
exports.fetchPublicUrlForArgus = fetchPublicUrlForArgus;
exports.getArgusWebFetchTools = getArgusWebFetchTools;
const promises_1 = __importDefault(require("node:dns/promises"));
const logger_1 = __importDefault(require("../../../../logger"));
const TOOL_NAME = 'fetch_public_url';
const DEFAULT_TIMEOUT_MS = 15000;
const DEFAULT_MAX_BYTES = 512 * 1024;
const DEFAULT_MAX_REDIRECTS = 4;
const DEFAULT_MAX_TEXT_CHARS = 48000;
function parseEnvInt(key, fallback) {
    const raw = process.env[key]?.trim();
    if (!raw) {
        return fallback;
    }
    const n = Number.parseInt(raw, 10);
    return Number.isFinite(n) && n > 0 ? n : fallback;
}
/** When false/0/off, the tool is not registered. Default: enabled. */
function isArgusWebFetchEnabled() {
    const v = process.env.ARGUS_WEB_FETCH_ENABLED?.trim().toLowerCase();
    if (v === '0' || v === 'false' || v === 'off' || v === 'no') {
        return false;
    }
    return true;
}
function parseIpv4(host) {
    const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
    if (!m) {
        return null;
    }
    const parts = m.slice(1).map((x) => Number.parseInt(x, 10));
    if (parts.some((n) => !Number.isFinite(n) || n < 0 || n > 255)) {
        return null;
    }
    return parts;
}
function isPrivateOrReservedIpv4(parts) {
    const [a, b] = parts;
    if (a === 10) {
        return true;
    }
    if (a === 127) {
        return true;
    }
    if (a === 0) {
        return true;
    }
    if (a === 169 && b === 254) {
        return true;
    }
    if (a === 172 && b >= 16 && b <= 31) {
        return true;
    }
    if (a === 192 && b === 168) {
        return true;
    }
    if (a === 100 && b >= 64 && b <= 127) {
        return true;
    }
    if (a >= 224) {
        return true;
    }
    return false;
}
function isPrivateOrReservedIpv6(addr) {
    const a = addr.toLowerCase().trim();
    if (a === '::1') {
        return true;
    }
    if (a.startsWith('fe80:') || a.startsWith('fec0:')) {
        return true;
    }
    if (/^f[cd][0-9a-f]{2}:/i.test(a)) {
        return true;
    }
    const mapped = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i.exec(a);
    if (mapped) {
        const p = parseIpv4(mapped[1]);
        return p ? isPrivateOrReservedIpv4(p) : true;
    }
    return false;
}
function isBlockedHostname(hostname) {
    const h = hostname.toLowerCase();
    if (h === 'localhost' || h.endsWith('.localhost')) {
        return true;
    }
    if (h === 'metadata.google.internal' || h.endsWith('.metadata.google.internal')) {
        return true;
    }
    if (h.endsWith('.local')) {
        return true;
    }
    return false;
}
function hostMatchesAllowlist(hostname) {
    const raw = process.env.ARGUS_WEB_FETCH_ALLOWED_HOSTS?.trim();
    if (!raw) {
        return true;
    }
    const entries = raw
        .split(',')
        .map((s) => s.trim().toLowerCase())
        .filter(Boolean);
    if (entries.length === 0) {
        return true;
    }
    const h = hostname.toLowerCase();
    for (const e of entries) {
        if (h === e || h.endsWith(`.${e}`)) {
            return true;
        }
    }
    return false;
}
async function assertTargetSafe(hostname) {
    if (isBlockedHostname(hostname)) {
        throw new Error(`Hostname is not allowed: ${hostname}`);
    }
    if (!hostMatchesAllowlist(hostname)) {
        throw new Error('URL host is not on ARGUS_WEB_FETCH_ALLOWED_HOSTS; ask the user to allow this domain or paste content instead.');
    }
    const v4 = parseIpv4(hostname);
    if (v4) {
        if (isPrivateOrReservedIpv4(v4)) {
            throw new Error('URL resolves to a blocked address range');
        }
        return;
    }
    // URL.hostname may be an IPv6 literal without brackets (e.g. ::1).
    if (hostname.includes(':') && parseIpv4(hostname) === null) {
        if (isPrivateOrReservedIpv6(hostname)) {
            throw new Error('URL resolves to a blocked address range');
        }
        return;
    }
    if (hostname.startsWith('[') && hostname.endsWith(']')) {
        const inner = hostname.slice(1, -1);
        if (isPrivateOrReservedIpv6(inner)) {
            throw new Error('URL resolves to a blocked address range');
        }
        return;
    }
    let records;
    try {
        records = await promises_1.default.lookup(hostname, { all: true, verbatim: true });
    }
    catch (e) {
        throw new Error(`DNS lookup failed: ${e instanceof Error ? e.message : String(e)}`);
    }
    if (!Array.isArray(records) || records.length === 0) {
        throw new Error('DNS lookup returned no addresses');
    }
    for (const r of records) {
        const addr = String(r.address);
        if (r.family === 4) {
            const p = parseIpv4(addr);
            if (!p || !isPrivateOrReservedIpv4(p)) {
                continue;
            }
            throw new Error('URL resolves to a blocked IPv4 range');
        }
        if (r.family === 6 && isPrivateOrReservedIpv6(addr)) {
            throw new Error('URL resolves to a blocked IPv6 range');
        }
    }
}
function stripHtmlToText(html) {
    let s = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ' ');
    s = s.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, ' ');
    s = s.replace(/<[^>]+>/g, ' ');
    s = s.replace(/\s+/g, ' ').trim();
    return s;
}
function decodeResponseBody(buffer, contentType) {
    const ct = contentType.toLowerCase();
    const u8 = new Uint8Array(buffer);
    const dec = new TextDecoder('utf-8', { fatal: false });
    const text = dec.decode(u8);
    if (ct.includes('application/json') || ct.includes('+json')) {
        return { text, mode: 'json' };
    }
    if (ct.includes('text/html') || ct.includes('application/xhtml')) {
        return { text, mode: 'html' };
    }
    return { text, mode: 'text' };
}
async function fetchPublicUrlForArgus(input) {
    const rawUrl = typeof input?.url === 'string' ? input.url.trim() : '';
    if (!rawUrl) {
        return JSON.stringify({ error: 'Missing required parameter: url' });
    }
    let current;
    try {
        current = new URL(rawUrl);
    }
    catch {
        return JSON.stringify({ error: 'Invalid URL' });
    }
    if (current.username || current.password) {
        return JSON.stringify({ error: 'URLs with embedded credentials are not allowed' });
    }
    if (current.protocol !== 'http:' && current.protocol !== 'https:') {
        return JSON.stringify({ error: 'Only http and https URLs are allowed' });
    }
    const timeoutMs = parseEnvInt('ARGUS_WEB_FETCH_TIMEOUT_MS', DEFAULT_TIMEOUT_MS);
    const maxBytes = parseEnvInt('ARGUS_WEB_FETCH_MAX_BYTES', DEFAULT_MAX_BYTES);
    const maxRedirects = parseEnvInt('ARGUS_WEB_FETCH_MAX_REDIRECTS', DEFAULT_MAX_REDIRECTS);
    const maxTextChars = parseEnvInt('ARGUS_WEB_FETCH_MAX_TEXT_CHARS', DEFAULT_MAX_TEXT_CHARS);
    const stripHtml = input.strip_html !== false;
    const hopLimit = Math.min(Math.max(maxRedirects, 0), 10);
    let urlToFetch = current;
    for (let hop = 0; hop <= hopLimit; hop += 1) {
        await assertTargetSafe(urlToFetch.hostname);
        const controller = new AbortController();
        const t = setTimeout(() => controller.abort(), timeoutMs);
        try {
            const res = await fetch(urlToFetch.toString(), {
                method: 'GET',
                redirect: 'manual',
                signal: controller.signal,
                headers: {
                    Accept: 'text/html, text/plain, application/json, application/xhtml+xml;q=0.9, */*;q=0.1',
                    'User-Agent': 'Trusys-Argus/1.0 (+https://trusys.ai; read-only URL preview)',
                },
            });
            if (res.status >= 300 && res.status < 400) {
                const loc = res.headers.get('location');
                if (!loc || hop >= hopLimit) {
                    return JSON.stringify({
                        error: `Redirect response (${res.status}) without a valid Location or redirect limit exceeded`,
                        finalUrl: urlToFetch.toString(),
                    });
                }
                let next;
                try {
                    next = new URL(loc, urlToFetch);
                }
                catch {
                    return JSON.stringify({ error: 'Invalid redirect Location URL', finalUrl: urlToFetch.toString() });
                }
                if (next.protocol !== 'http:' && next.protocol !== 'https:') {
                    return JSON.stringify({ error: 'Redirect to non-http(s) URL blocked', finalUrl: urlToFetch.toString() });
                }
                if (next.username || next.password) {
                    return JSON.stringify({ error: 'Redirect target with credentials blocked' });
                }
                urlToFetch = next;
                continue;
            }
            if (!res.ok) {
                return JSON.stringify({
                    error: `HTTP ${res.status} ${res.statusText || ''}`.trim(),
                    finalUrl: urlToFetch.toString(),
                });
            }
            const contentType = res.headers.get('content-type') || '';
            const ab = await res.arrayBuffer();
            if (ab.byteLength > maxBytes) {
                return JSON.stringify({
                    error: `Response body exceeds ARGUS_WEB_FETCH_MAX_BYTES (${maxBytes})`,
                    finalUrl: urlToFetch.toString(),
                    contentType,
                });
            }
            const decoded = decodeResponseBody(ab, contentType);
            let bodyText = decoded.text;
            if (decoded.mode === 'html' && stripHtml) {
                bodyText = stripHtmlToText(bodyText);
            }
            if (bodyText.length > maxTextChars) {
                bodyText = `${bodyText.slice(0, maxTextChars)}\n\n... [truncated: ARGUS_WEB_FETCH_MAX_TEXT_CHARS=${maxTextChars}]`;
            }
            return JSON.stringify({
                finalUrl: urlToFetch.toString(),
                contentType,
                mode: decoded.mode,
                text: bodyText,
            });
        }
        catch (e) {
            const msg = e instanceof Error ? e.message : String(e);
            logger_1.default.warn(`Argus ${TOOL_NAME}: ${msg}`);
            return JSON.stringify({
                error: msg.includes('aborted') ? `Request timed out after ${timeoutMs}ms` : msg,
                finalUrl: urlToFetch.toString(),
            });
        }
        finally {
            clearTimeout(t);
        }
    }
    return JSON.stringify({ error: 'Too many redirects', finalUrl: urlToFetch.toString() });
}
function createTool(name, description, schema, execute) {
    return { name, description, schema, execute };
}
/**
 * Read-only GET of public http(s) URLs for Argus (SSRF-hardened). Disabled when ARGUS_WEB_FETCH_ENABLED=false.
 */
function getArgusWebFetchTools() {
    if (!isArgusWebFetchEnabled()) {
        return [];
    }
    return [
        createTool(TOOL_NAME, 'Fetch and return **public** web page or API text from an **http(s)** URL (GET only). Use when the user asks about **external** documentation, standards, or public pages and telemetry tools cannot answer. In **security config** Argus, also use when the user pastes **only** a URL—that means fetch the site to derive applicationDefinition context for the draft. **Do not** use for authenticated, internal, or private endpoints; **do not** pass secrets in URLs. Prefer telemetry tools for project data. Returns JSON with `text`, `finalUrl`, `contentType`, and `mode` (text|html|json), or `error`.', {
            type: 'object',
            properties: {
                url: {
                    type: 'string',
                    description: 'Absolute http or https URL to fetch (no credentials in the URL).',
                },
                strip_html: {
                    type: 'boolean',
                    description: 'If true (default), reduce HTML to plain text. Set false to keep raw markup (still size-capped).',
                },
            },
            required: ['url'],
        }, fetchPublicUrlForArgus),
    ];
}
//# sourceMappingURL=argusWebFetchTool.js.map