/**
 * Redaction pipeline — strips sensitive data from traces before storage.
 *
 * Applied before ANY storage (both blob and Content Lake). Configurable
 * patterns handle Bearer tokens, API keys, Sanity tokens, and other
 * common secret formats.
 *
 * Principles:
 * 1. Redact before store — sensitive data never reaches storage
 * 2. Configurable patterns — teams can add project-specific rules
 * 3. Truncation for cost — large outputs truncated to max bytes
 * 4. No PII by default — tasks shouldn't contain PII, this is a safety net
 *
 * @see docs/design-docs/architecture-overhaul/observability-telemetry.md
 */
// ---------------------------------------------------------------------------
// Default rules
// ---------------------------------------------------------------------------
/** Built-in redaction rules for common secret patterns */
export const DEFAULT_REDACTION_RULES = [
    {
        name: "bearer_tokens",
        pattern: /Bearer\s+[A-Za-z0-9._~+/=-]{10,}/g,
        replacement: "Bearer [REDACTED]",
    },
    {
        name: "sanity_tokens",
        pattern: /sk[A-Za-z0-9]{30,}/g,
        replacement: "[REDACTED_SANITY_TOKEN]",
    },
    {
        name: "openai_keys",
        pattern: /sk-[A-Za-z0-9_-]{20,}/g,
        replacement: "[REDACTED_OPENAI_KEY]",
    },
    {
        name: "api_key_values",
        pattern: /((?:api[_-]?key|token|secret|password|authorization)\s*[:=]\s*)(["']?)(?!\[REDACTED)[^\s"']{8,}\2/gi,
        replacement: "$1$2[REDACTED]$2",
    },
    {
        name: "slack_tokens",
        pattern: /xoxb-[A-Za-z0-9-]{20,}/g,
        replacement: "[REDACTED_SLACK_TOKEN]",
    },
    {
        name: "github_tokens",
        pattern: /gh[ps]_[A-Za-z0-9]{30,}/g,
        replacement: "[REDACTED_GITHUB_TOKEN]",
    },
    {
        name: "anthropic_keys",
        pattern: /sk-ant-[A-Za-z0-9_-]{20,}/g,
        replacement: "[REDACTED_ANTHROPIC_KEY]",
    },
    {
        name: "base64_credentials",
        pattern: /Basic\s+[A-Za-z0-9+/=]{20,}/g,
        replacement: "Basic [REDACTED]",
    },
];
/** Default fields to omit entirely */
const DEFAULT_OMIT_FIELDS = [
    "toolCalls[*].input.headers.Authorization",
    "toolCalls[*].input.headers.Cookie",
    "toolCalls[*].input.headers.Set-Cookie",
];
const DEFAULT_MAX_OUTPUT_BYTES = 10_240;
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Create a default redaction config.
 *
 * @param overrides - Custom rules or settings to merge
 */
export function createRedactionConfig(overrides) {
    return {
        rules: overrides?.rules
            ? [...DEFAULT_REDACTION_RULES, ...overrides.rules]
            : DEFAULT_REDACTION_RULES,
        omitFields: overrides?.omitFields
            ? [...DEFAULT_OMIT_FIELDS, ...overrides.omitFields]
            : DEFAULT_OMIT_FIELDS,
        maxOutputBytes: overrides?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES,
    };
}
/**
 * Apply redaction to an evaluation trace.
 *
 * Processes tool call inputs and outputs, event data, and search terms.
 * Returns a new trace (does not mutate the original).
 */
export function redactTrace(trace, config) {
    const cfg = config ?? createRedactionConfig();
    let redactionCount = 0;
    const rulesApplied = new Set();
    // Deep clone to avoid mutation
    const redacted = JSON.parse(JSON.stringify(trace));
    // Redact tool calls
    redacted.toolCalls = redacted.toolCalls.map((call) => {
        const result = redactToolCall(call, cfg);
        redactionCount += result.count;
        for (const rule of result.rules)
            rulesApplied.add(rule);
        return result.call;
    });
    // Redact events
    redacted.events = redacted.events.map((event) => {
        const dataStr = JSON.stringify(event.data);
        const { text, count, rules } = applyRules(dataStr, cfg.rules);
        redactionCount += count;
        for (const rule of rules)
            rulesApplied.add(rule);
        return { ...event, data: JSON.parse(text) };
    });
    // Redact search terms (may contain embedded secrets)
    redacted.searchTerms = redacted.searchTerms.map((term) => {
        const { text, count, rules } = applyRules(term, cfg.rules);
        redactionCount += count;
        for (const rule of rules)
            rulesApplied.add(rule);
        return text;
    });
    return {
        trace: redacted,
        redactionCount,
        rulesApplied: [...rulesApplied],
    };
}
// ---------------------------------------------------------------------------
// Tool call redaction
// ---------------------------------------------------------------------------
function redactToolCall(call, config) {
    let count = 0;
    const rules = [];
    // Redact input
    const inputStr = JSON.stringify(call.input);
    const inputResult = applyRules(inputStr, config.rules);
    count += inputResult.count;
    rules.push(...inputResult.rules);
    // Redact output
    let outputStr = JSON.stringify(call.output);
    // Truncate output if too large
    if (outputStr.length > config.maxOutputBytes) {
        outputStr = outputStr.slice(0, config.maxOutputBytes) + "... [truncated]";
    }
    const outputResult = applyRules(outputStr, config.rules);
    count += outputResult.count;
    rules.push(...outputResult.rules);
    // Redact the error message. Adapters can fold a failed call's (possibly
    // secret-bearing) output into this field — e.g. the agent-harness adapter
    // derives `error` from the `claude-agent-sdk` tool output — so it needs the
    // same scrubbing as `output`, which the `...call` spread alone would skip.
    let redactedError = call.error;
    if (call.error !== undefined) {
        const errorResult = applyRules(call.error, config.rules);
        count += errorResult.count;
        rules.push(...errorResult.rules);
        redactedError = errorResult.text;
    }
    // Omit specific fields from input
    let parsedInput = JSON.parse(inputResult.text);
    parsedInput = omitFields(parsedInput, config.omitFields, "input");
    return {
        call: {
            ...call,
            input: parsedInput,
            output: parseJsonSafe(outputResult.text),
            ...(redactedError !== undefined ? { error: redactedError } : {}),
        },
        count,
        rules,
    };
}
// ---------------------------------------------------------------------------
// Rule application
// ---------------------------------------------------------------------------
function applyRules(text, rules) {
    let result = text;
    let count = 0;
    const appliedRules = [];
    for (const rule of rules) {
        // Reset lastIndex before match() — global regexes are stateful
        rule.pattern.lastIndex = 0;
        const matches = result.match(rule.pattern);
        if (matches && matches.length > 0) {
            count += matches.length;
            appliedRules.push(rule.name);
            // Reset again before replace() — match() may leave lastIndex dirty
            rule.pattern.lastIndex = 0;
            result = result.replace(rule.pattern, rule.replacement);
        }
    }
    return { text: result, count, rules: appliedRules };
}
// ---------------------------------------------------------------------------
// Field omission
// ---------------------------------------------------------------------------
function omitFields(obj, patterns, context) {
    for (const pattern of patterns) {
        // Simple field path handling (not full JSONPath)
        // Handles: "toolCalls[*].input.headers.Authorization" when context is "input"
        if (pattern.includes(context)) {
            const parts = pattern.split(".");
            const fieldIndex = parts.indexOf(context);
            if (fieldIndex >= 0) {
                const remainingPath = parts.slice(fieldIndex + 1);
                deleteNestedField(obj, remainingPath);
            }
        }
    }
    return obj;
}
function deleteNestedField(obj, path) {
    if (path.length === 0)
        return;
    if (path.length === 1) {
        delete obj[path[0]];
        return;
    }
    const child = obj[path[0]];
    if (child && typeof child === "object") {
        deleteNestedField(child, path.slice(1));
    }
}
function parseJsonSafe(text) {
    try {
        return JSON.parse(text);
    }
    catch {
        return text;
    }
}
