"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildSourceContextMarkdown = exports.normalizeSourceContextArtifact = exports.normalizeSourceArtifacts = exports.resolveSourceUrls = exports.extractPromptUrls = exports.normalizeUrl = void 0;
const errors_1 = require("./errors");
// NOTE: This creates an ES module cycle with inspirationDesignContext (which
// imports `extractPromptUrls` from here). The cycle is safe because
// `isLocalLiteralHostname` is only invoked from inside `normalizeUrl`'s
// function body, not at module-eval time. Without this guard,
// `resolveSourceUrls` would happily pass `http://169.254.169.254/...` or
// `http://localhost:.../` URLs from prompts / `sourceContext.sourceUrls`
// straight into the `source_plan` work item, where the agent's browser
// extraction step would dial them — bypassing the SSRF guard that already
// covers the server-side inspiration fetcher.
const inspirationDesignContext_1 = require("./inspirationDesignContext");
const MAX_PROMPT_URLS = 6;
const MAX_SOURCE_URLS = 24;
const MAX_ARTIFACTS = 24;
const MAX_ARTIFACT_CONTENT_LENGTH = 8_000;
const MAX_FIELD_LENGTH = 400;
const MAX_TOTAL_MARKDOWN_LENGTH = 60_000;
const TRAILING_URL_PUNCTUATION = /[),.;:!?]+$/;
const EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
const PHONE_PATTERN = /\b(?:\+?\d{1,3}[ -]?)?(?:\(?\d{2,4}\)?[ -]?)?\d{3,4}[ -]?\d{4}\b/g;
const TOKEN_PATTERN = /\b(?:api[_-]?key|token|secret|password)\s*[:=]\s*[^\s,;]+/gi;
const sanitizeText = (value, maxLength) => {
    const redacted = value
        .replace(EMAIL_PATTERN, '[redacted-email]')
        .replace(PHONE_PATTERN, '[redacted-phone]')
        .replace(TOKEN_PATTERN, '[redacted-secret]');
    return redacted.replace(/\s+/g, ' ').trim().slice(0, maxLength);
};
const normalizeUrl = (rawUrl) => {
    const trimmed = rawUrl.trim().replace(TRAILING_URL_PUNCTUATION, '');
    if (!trimmed)
        return null;
    let parsed;
    try {
        parsed = new URL(trimmed);
    }
    catch {
        return null;
    }
    if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
        return null;
    }
    // SSRF gate: reject literal localhost / loopback / link-local /
    // RFC1918 / cloud-metadata hosts here, before the URL is handed to
    // `source_plan` and the agent's browser extraction step. The
    // server-side inspiration fetcher applies the same check (plus a
    // resolved-DNS check) — without this, prompt-injected URLs like
    // `http://169.254.169.254/...` or `http://localhost:.../` would
    // sneak through `resolveSourceUrls` and drive the agent at internal
    // services. The DNS-resolved layer is intentionally not run here
    // because `normalizeUrl` must stay sync; the literal check covers
    // every cheap-to-stuff target without needing IO.
    const hostname = parsed.hostname.toLowerCase();
    if (!hostname || (0, inspirationDesignContext_1.isLocalLiteralHostname)(hostname)) {
        return null;
    }
    parsed.hash = '';
    return parsed.toString();
};
exports.normalizeUrl = normalizeUrl;
const extractPromptUrls = (prompt) => {
    const matches = prompt.match(/\bhttps?:\/\/[^\s)]+/g) ?? [];
    const deduped = [];
    const seen = new Set();
    for (const raw of matches) {
        const normalized = (0, exports.normalizeUrl)(raw);
        if (!normalized || seen.has(normalized))
            continue;
        seen.add(normalized);
        deduped.push(normalized);
        if (deduped.length >= MAX_PROMPT_URLS)
            break;
    }
    return deduped;
};
exports.extractPromptUrls = extractPromptUrls;
const resolveSourceUrls = (prompt, sourceContext) => {
    const merged = [...(0, exports.extractPromptUrls)(prompt), ...(sourceContext?.sourceUrls ?? [])];
    const deduped = [];
    const seen = new Set();
    for (const candidate of merged) {
        const normalized = (0, exports.normalizeUrl)(candidate);
        if (!normalized || seen.has(normalized))
            continue;
        seen.add(normalized);
        deduped.push(normalized);
        if (deduped.length >= MAX_SOURCE_URLS)
            break;
    }
    return deduped;
};
exports.resolveSourceUrls = resolveSourceUrls;
const normalizeSourceArtifacts = (sourceArtifacts) => {
    if (!sourceArtifacts || sourceArtifacts.length === 0)
        return [];
    return sourceArtifacts.slice(0, MAX_ARTIFACTS).map((artifact) => ({
        kind: artifact.kind,
        label: sanitizeText(artifact.label, 80),
        content: sanitizeText(artifact.content, MAX_ARTIFACT_CONTENT_LENGTH),
    }));
};
exports.normalizeSourceArtifacts = normalizeSourceArtifacts;
const normalizeSourceContextArtifact = (artifact) => {
    const normalizeList = (items) => items.map((item) => sanitizeText(item, MAX_FIELD_LENGTH)).filter(Boolean);
    const normalized = {
        sourceFindings: normalizeList(artifact.sourceFindings),
        sectionInventory: normalizeList(artifact.sectionInventory),
        visualObservations: normalizeList(artifact.visualObservations),
        sourceRoles: artifact.sourceRoles.map((entry) => ({
            url: sanitizeText(entry.url, 2_048),
            role: entry.role,
            ...(entry.rationale
                ? { rationale: sanitizeText(entry.rationale, MAX_FIELD_LENGTH) }
                : {}),
        })),
        qualityBar: normalizeList(artifact.qualityBar),
        ...(artifact.screenshotReferences
            ? {
                screenshotReferences: normalizeList(artifact.screenshotReferences).slice(0, 16),
            }
            : {}),
        ...(artifact.risks
            ? { risks: normalizeList(artifact.risks).slice(0, 24) }
            : {}),
    };
    const estimatedSize = JSON.stringify(normalized).length;
    if (estimatedSize > MAX_TOTAL_MARKDOWN_LENGTH) {
        throw new errors_1.AgentVariantsError('SOURCE_CONTEXT_TOO_LARGE', `Source context exceeds size limit (${estimatedSize} > ${MAX_TOTAL_MARKDOWN_LENGTH})`);
    }
    return normalized;
};
exports.normalizeSourceContextArtifact = normalizeSourceContextArtifact;
const redactUrlForDisplay = (url) => {
    try {
        const parsed = new URL(url);
        parsed.search = '';
        parsed.hash = '';
        return parsed.toString();
    }
    catch {
        return url;
    }
};
const buildSourceContextMarkdown = (artifact) => {
    const lines = [
        '# SOURCE_CONTEXT',
        '',
        '## Source Findings',
        ...artifact.sourceFindings.map((item) => `- ${item}`),
        '',
        '## Section Inventory',
        ...artifact.sectionInventory.map((item) => `- ${item}`),
        '',
        '## Visual Observations',
        ...artifact.visualObservations.map((item) => `- ${item}`),
        '',
        '## Source Roles',
        ...artifact.sourceRoles.map((entry) => entry.rationale
            ? `- ${entry.role}: ${redactUrlForDisplay(entry.url)} (${entry.rationale})`
            : `- ${entry.role}: ${redactUrlForDisplay(entry.url)}`),
        '',
        '## Quality Bar',
        ...artifact.qualityBar.map((item) => `- ${item}`),
    ];
    if (artifact.screenshotReferences && artifact.screenshotReferences.length > 0) {
        lines.push('', '## Screenshot References');
        lines.push(...artifact.screenshotReferences.map((item) => `- ${item}`));
    }
    if (artifact.risks && artifact.risks.length > 0) {
        lines.push('', '## Risks');
        lines.push(...artifact.risks.map((item) => `- ${item}`));
    }
    const markdown = lines.join('\n').trim();
    if (markdown.length > MAX_TOTAL_MARKDOWN_LENGTH) {
        throw new errors_1.AgentVariantsError('SOURCE_CONTEXT_TOO_LARGE', `SOURCE_CONTEXT.md exceeds size limit (${markdown.length} > ${MAX_TOTAL_MARKDOWN_LENGTH})`);
    }
    return `${markdown}\n`;
};
exports.buildSourceContextMarkdown = buildSourceContextMarkdown;
//# sourceMappingURL=sourceContext.js.map