import { sanitizeText } from "../../shared/index.js";
export const SEED_UNTRUSTED_CONTENT_BEGIN = "BEGIN SEED UNTRUSTED CONTENT";
export const SEED_UNTRUSTED_CONTENT_END = "END SEED UNTRUSTED CONTENT";
const MAX_FINDINGS = 20;
const MAX_WRAPPED_EXCERPT_CHARS = 4000;
const ARIA_INJECTION_PATTERNS = [
    /\bignore\s+(?:previous|prior|above|all)\s+instructions?\b/i,
    /\bdisregard\s+(?:previous|prior|above|all)\s+instructions?\b/i,
    /\breveal\s+(?:the\s+)?system\s+prompt\b/i,
    /\bdeveloper\s+message\b/i,
    /\bnew\s+instructions?\s*:/i,
    /\bdo\s+not\s+(?:follow|obey|listen)\b/i,
    /\bforget\s+(?:everything|all|your)\b/i,
];
const TAG_WITH_ATTRS = /<([a-zA-Z][\w:-]*)(\s[^>]*?)\/?>/gi;
const NOISE_TAGS = /<(script|style|noscript|svg)\b[\s\S]*?<\/\1>/gi;
export function analyzeSeedUntrustedHtml(html) {
    const { cleanedHtml, findings: hiddenElements } = stripHiddenElements(html);
    const ariaInjections = detectAriaInjections(html);
    const unsafeUrls = detectUnsafeUrls(html);
    return {
        visible_text: htmlToText(cleanedHtml.replace(NOISE_TAGS, " ")),
        hidden_elements: hiddenElements.slice(0, MAX_FINDINGS),
        aria_injections: ariaInjections.slice(0, MAX_FINDINGS),
        unsafe_urls: unsafeUrls.slice(0, MAX_FINDINGS),
    };
}
export function extractVisibleTextFromHtml(html) {
    return analyzeSeedUntrustedHtml(html).visible_text;
}
export function wrapSeedUntrustedContent(content, warnings = []) {
    const escaped = escapeSeedTrustBoundaryMarkers(content);
    const warningLines = warnings.length ? [`CONTENT WARNINGS: ${warnings.join("; ")}`] : [];
    const wrapped = [
        ...warningLines,
        SEED_UNTRUSTED_CONTENT_BEGIN,
        escaped.content,
        SEED_UNTRUSTED_CONTENT_END,
    ].join("\n");
    return {
        schema_version: 1,
        type: "seed.trust_boundary",
        label: "untrusted_content",
        begin_marker: SEED_UNTRUSTED_CONTENT_BEGIN,
        end_marker: SEED_UNTRUSTED_CONTENT_END,
        escaped_marker_count: escaped.escaped_marker_count,
        warning_count: warnings.length,
        wrapped_excerpt: sanitizeText(wrapped).slice(0, MAX_WRAPPED_EXCERPT_CHARS),
    };
}
export function escapeSeedTrustBoundaryMarkers(content) {
    let escapedMarkerCount = 0;
    const replaceMarker = (value, marker) => {
        const count = value.split(marker).length - 1;
        if (count <= 0)
            return value;
        escapedMarkerCount += count;
        return value.replaceAll(marker, marker.replace("CONTENT", "C[escaped]ONTENT"));
    };
    let contentWithEscapedMarkers = content;
    contentWithEscapedMarkers = replaceMarker(contentWithEscapedMarkers, SEED_UNTRUSTED_CONTENT_BEGIN);
    contentWithEscapedMarkers = replaceMarker(contentWithEscapedMarkers, SEED_UNTRUSTED_CONTENT_END);
    return {
        content: contentWithEscapedMarkers,
        escaped_marker_count: escapedMarkerCount,
    };
}
function stripHiddenElements(html) {
    const findings = [];
    let cleanedHtml = html;
    for (const match of html.matchAll(TAG_WITH_ATTRS)) {
        const tag = match[1]?.toLowerCase() ?? "element";
        const attrs = match[2] ?? "";
        const reason = hiddenReason(attrs);
        const fragment = tagFragment(html, match.index ?? 0, tag, match[0]);
        const text = htmlToText(fragment);
        if (!reason || !text)
            continue;
        findings.push({
            kind: "hidden_element",
            reason: `${tag} ${reason}`,
            sample: text.slice(0, 160),
        });
        cleanedHtml = cleanedHtml.replace(fragment, " ");
        if (findings.length >= MAX_FINDINGS)
            break;
    }
    return { cleanedHtml, findings };
}
function tagFragment(html, start, tag, openingTag) {
    if (/\/\s*>$/.test(openingTag))
        return openingTag;
    const closeTag = `</${tag}>`;
    const closeIndex = html.toLowerCase().indexOf(closeTag, start + openingTag.length);
    if (closeIndex < 0)
        return openingTag;
    return html.slice(start, closeIndex + closeTag.length);
}
function detectAriaInjections(html) {
    const findings = [];
    for (const match of html.matchAll(TAG_WITH_ATTRS)) {
        const tag = match[1]?.toLowerCase() ?? "element";
        const attrs = match[2] ?? "";
        const label = [
            attributeValue(attrs, "aria-label"),
            attributeValue(attrs, "aria-description"),
            attributeValue(attrs, "title"),
        ]
            .filter((value) => Boolean(value))
            .join(" ");
        if (!label)
            continue;
        if (!ARIA_INJECTION_PATTERNS.some((pattern) => pattern.test(label)))
            continue;
        findings.push({
            kind: "aria_injection",
            reason: `${tag} ARIA/title text contains prompt-injection-like instructions`,
            sample: sanitizeText(label).slice(0, 160),
        });
        if (findings.length >= MAX_FINDINGS)
            break;
    }
    return findings;
}
function detectUnsafeUrls(html) {
    const findings = [];
    const seen = new Set();
    for (const match of html.matchAll(/\b(?:href|src|action|formaction)\s*=\s*["']?([^"'\s>]+)/gi)) {
        const raw = decodeHtmlEntity(match[1] ?? "");
        const finding = classifyUnsafeUrl(raw);
        if (!finding)
            continue;
        const key = `${finding.reason}:${finding.sample}`;
        if (seen.has(key))
            continue;
        seen.add(key);
        findings.push(finding);
        if (findings.length >= MAX_FINDINGS)
            break;
    }
    return findings;
}
export function classifyUnsafeUrl(rawUrl) {
    const value = rawUrl.trim();
    const lower = value.toLowerCase();
    if (!value)
        return undefined;
    if (lower.startsWith("javascript:")) {
        return unsafeUrlFinding("active javascript URL scheme", value);
    }
    if (lower.startsWith("file:")) {
        return unsafeUrlFinding("local file URL reference", value);
    }
    if (lower.startsWith("data:text/html")) {
        return unsafeUrlFinding("active HTML data URL", value);
    }
    let parsed;
    try {
        parsed = new URL(value, "https://seed.invalid");
    }
    catch {
        return undefined;
    }
    if (hasSensitiveQueryParam(parsed)) {
        return unsafeUrlFinding("sensitive URL query parameter", redactSensitiveUrl(parsed));
    }
    if (isLocalOrPrivateHost(parsed.hostname)) {
        return unsafeUrlFinding("local or private network URL reference", parsed.toString());
    }
    return undefined;
}
function unsafeUrlFinding(reason, sample) {
    return {
        kind: "unsafe_url",
        reason,
        sample: sanitizeText(sample).slice(0, 200),
    };
}
function hiddenReason(attrs) {
    if (/\s(?:hidden)\b/i.test(attrs))
        return "hidden attribute";
    if (/\saria-hidden\s*=\s*["']?true\b/i.test(attrs))
        return "aria-hidden=true";
    const style = attributeValue(attrs, "style")?.toLowerCase();
    if (!style)
        return undefined;
    if (/\bdisplay\s*:\s*none\b/.test(style))
        return "display:none";
    if (/\bvisibility\s*:\s*hidden\b/.test(style))
        return "visibility:hidden";
    if (/\bopacity\s*:\s*(?:0(?:\.0+)?|0?\.0?\d+)\b/.test(style))
        return "opacity below visible threshold";
    if (/\bfont-size\s*:\s*(?:0(?:\.0+)?|0?\.\d+)\s*px\b/.test(style))
        return "near-zero font size";
    if (/\b(?:left|right|top|bottom)\s*:\s*-\d{3,}(?:px|rem|em)?\b/.test(style))
        return "off-screen positioning";
    if (/\bclip(?:-path)?\s*:\s*(?:rect\(0|inset\(100%)/.test(style))
        return "clip hiding";
    if (sameForegroundBackground(style))
        return "same foreground/background color";
    return undefined;
}
function sameForegroundBackground(style) {
    const color = cssPropertyValue(style, "color");
    const background = cssPropertyValue(style, "background-color") ?? cssPropertyValue(style, "background");
    return Boolean(color && background && color === background);
}
function cssPropertyValue(style, property) {
    const escaped = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    const match = style.match(new RegExp(`(?:^|;)\\s*${escaped}\\s*:\\s*([^;]+)`, "i"));
    return match?.[1]?.trim();
}
function attributeValue(attrs, name) {
    const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    const match = attrs.match(new RegExp(`\\s${escaped}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i"));
    return decodeHtmlEntity(match?.[1] ?? match?.[2] ?? match?.[3] ?? "");
}
function htmlToText(html) {
    return sanitizeText(decodeHtmlEntity(html)
        .replace(/<[^>]+>/g, " ")
        .replace(/\s+/g, " ")
        .trim());
}
function decodeHtmlEntity(value) {
    return value
        .replace(/&quot;/gi, "\"")
        .replace(/&#39;/g, "'")
        .replace(/&apos;/gi, "'")
        .replace(/&amp;/gi, "&")
        .replace(/&lt;/gi, "<")
        .replace(/&gt;/gi, ">");
}
function hasSensitiveQueryParam(url) {
    const sensitive = /^(?:api[_-]?key|token|secret|password|authorization|cookie|credential|refresh[_-]?token|access[_-]?token|auth)$/i;
    return [...url.searchParams.keys()].some((key) => sensitive.test(key));
}
function redactSensitiveUrl(url) {
    const redacted = new URL(url.toString());
    for (const key of [...redacted.searchParams.keys()]) {
        if (/^(?:api[_-]?key|token|secret|password|authorization|cookie|credential|refresh[_-]?token|access[_-]?token|auth)$/i.test(key)) {
            redacted.searchParams.set(key, "[REDACTED]");
        }
    }
    return redacted.toString().replaceAll("%5BREDACTED%5D", "[REDACTED]");
}
function isLocalOrPrivateHost(hostname) {
    const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
    if (host === "localhost" || host === "::1" || host.endsWith(".local"))
        return true;
    if (/^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host) || /^169\.254\./.test(host))
        return true;
    const match = host.match(/^172\.(\d{1,2})\./);
    if (match) {
        const second = Number(match[1]);
        if (second >= 16 && second <= 31)
            return true;
    }
    return false;
}
//# sourceMappingURL=index.js.map