import { redactSecretText } from "../utils/redactSecrets.js";
const DEFAULT_MAX_DEPTH = 24;
const DEFAULT_MAX_ARRAY_ITEMS = 100;
const DEFAULT_MAX_FINDINGS = 100;
const MAX_DIAGNOSTIC_TEXT_LENGTH = 1_000;
const MAX_SURFACE_LABEL_LENGTH = 120;
const MAX_PATH_KEY_LENGTH = 80;
const MAX_GUARD_FAILURE_FINDINGS = 12;
const MAX_GUARD_FAILURE_TEXT_LENGTH = 120;
const FORBIDDEN_FIELD_NAMES = [
    "accessToken",
    "apiKey",
    "auditId",
    "authorization",
    "capabilityToken",
    "command",
    "commandLine",
    "commandMatcher",
    "commandMatchKey",
    "cookie",
    "credential",
    "credentials",
    "credentialReference",
    "credentialScope",
    "credentialScopeKey",
    "credentialValue",
    "decisionAuditId",
    "decisionId",
    "env",
    "extraEnv",
    "headers",
    "idToken",
    "matchKey",
    "matcher",
    "matcherKey",
    "messageId",
    "modelId",
    "modelID",
    "password",
    "pattern",
    "permissionId",
    "privateKey",
    "processId",
    "prompt",
    "provider",
    "providerId",
    "providerID",
    "providerPermissionId",
    "providerRequestId",
    "providerRuntimeId",
    "providerSessionId",
    "raw",
    "rawCommand",
    "rawPattern",
    "refreshToken",
    "requestId",
    "revealSessionId",
    "revokeAuditId",
    "runId",
    "runtimeEventId",
    "runtimeId",
    "runtimeProcessId",
    "runtimeSessionId",
    "secret",
    "sessionId",
    "sessionToken",
    "setCookie",
    "shellCommand",
    "sourcePermissionRequestId",
    "sourceRuntimeEventId",
    "stack",
    "token",
    "toolCallId",
    "trace",
    "userId",
    "workspaceId",
];
const FORBIDDEN_FIELD_NAME_SET = new Set(FORBIDDEN_FIELD_NAMES.map(normalizePublicPayloadFieldName));
const FORBIDDEN_FIELD_ASSIGNMENT_NAMES = Array.from(new Set(FORBIDDEN_FIELD_NAMES.flatMap((name) => [
    name,
    toSeparatedFieldName(name, "_"),
    toSeparatedFieldName(name, "-"),
]))).sort((left, right) => right.length - left.length);
const FORBIDDEN_FIELD_ASSIGNMENT_PATTERN = new RegExp(`\\b(${FORBIDDEN_FIELD_ASSIGNMENT_NAMES.map(escapeRegExp).join("|")})\\s*[:=]\\s*(\"[^\"]*\"|'[^']*'|[^\\s,}]+)`, "gi");
const REDACTED_FORBIDDEN_FIELD_ASSIGNMENT_PATTERN = new RegExp(`\\b(?:${FORBIDDEN_FIELD_ASSIGNMENT_NAMES.map(escapeRegExp).join("|")})\\s*=\\[redacted\\]`, "gi");
const REDACTED_HANDOFF_FRAGMENT_PATTERN = /patchTrailLocalHandoff=\[redacted\]/giu;
const REDACTED_TOKEN_CLI_ARGUMENT_PATTERN = /--?(?:token|password|api-key|apikey|key|secret)(?:\s+|=)\[redacted\]/giu;
const REDACTED_SECRET_ENV_ASSIGNMENT_PATTERN = /\b[A-Za-z0-9_]*(?:TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL)[A-Za-z0-9_]*=\[redacted\]/giu;
const LOCAL_PATH_PATTERN = /(?:^|[\s"'(=])(?:file:\/\/)?(?:(?:\/Users|\/home|\/private\/var|\/var\/folders|\/tmp|\/Volumes)\/[^\s"',)]+|[A-Za-z]:\\(?:Users|Documents and Settings|Temp|tmp)\\[^\s"',)]+)/u;
const LOCAL_PATH_REDACTION_PATTERN = /(^|[\s"'(=])(?:file:\/\/)?((?:\/Users|\/home|\/private\/var|\/var\/folders|\/tmp|\/Volumes)\/[^\s"',)]+|[A-Za-z]:\\(?:Users|Documents and Settings|Temp|tmp)\\[^\s"',)]+)/gu;
const SECRET_STRING_RULES = [
    {
        id: "private-key-block",
        label: "private key block",
        regex: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/u,
    },
    {
        id: "local-ui-handoff-fragment",
        label: "local UI handoff fragment",
        regex: /patchTrailLocalHandoff=[^\s"'&#)]+/u,
    },
    {
        id: "local-ui-session-token",
        label: "local UI session token",
        regex: /\bpt_lui_sess_[A-Za-z0-9_-]{4,}\b/u,
    },
    {
        id: "local-ui-capability-token",
        label: "local UI capability token",
        regex: /\bpt_lui_(?!sess_)[A-Za-z0-9_-]{4,}\b/u,
    },
    {
        id: "bearer-token",
        label: "bearer token",
        regex: /\b(?:authorization:\s*)?bearer\s+[A-Za-z0-9._~+\/-]{8,}=*/iu,
    },
    {
        id: "provider-api-key",
        label: "provider API key",
        regex: /\b(?:sk-(?:proj-|ant-api03-|or-v1-)?[A-Za-z0-9_-]{8,}|gsk_[A-Za-z0-9]{8,}|AIza[0-9A-Za-z_-]{16,})\b/u,
    },
    {
        id: "provider-token",
        label: "provider token",
        regex: /\b(?:ghp_[A-Za-z0-9_]{8,}|github_pat_[A-Za-z0-9_]{8,}|glpat-[A-Za-z0-9_-]{8,}|xox[baprs]-[A-Za-z0-9-]{8,})\b/u,
    },
    {
        id: "url-credentials",
        label: "URL credentials",
        regex: /https?:\/\/[^\s:@/]+:[^\s@/]+@/iu,
    },
    {
        id: "token-cli-argument",
        label: "token CLI argument",
        regex: /--?(?:token|password|api-key|apikey|key|secret)(?:\s+|=)[^\s"']+/iu,
    },
    {
        id: "secret-env-assignment",
        label: "secret environment assignment",
        regex: /\b[A-Za-z0-9_]*(?:TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL)[A-Za-z0-9_]*=[^\s"']+/iu,
    },
    {
        id: "forbidden-field-assignment",
        label: "forbidden field assignment",
        regex: FORBIDDEN_FIELD_ASSIGNMENT_PATTERN,
    },
    {
        id: "local-path-diagnostic",
        label: "local path diagnostic",
        regex: LOCAL_PATH_PATTERN,
    },
];
const MASKED_CREDENTIAL_LABEL_RULE = {
    id: "masked-credential-label",
    label: "masked credential label",
    regex: /^(?:[A-Za-z0-9_-]{2,16}-)?[•●*]{2,}[A-Za-z0-9_-]{2,16}$|^(?:sk|gsk|AIza)[A-Za-z0-9_-]*-[•●*]{2,}[A-Za-z0-9_-]{2,16}$/u,
};
const SURFACE_POLICIES = {
    "local-ui.capability-token-issued": {
        allowedPaths: [
            "$.token",
            "$.capabilityId",
            "$.workspaceId",
            "$.deviceId",
            "$.userSessionId",
        ],
        allowedFieldNames: [
            "token",
            "capabilityId",
            "workspaceId",
            "deviceId",
            "userSessionId",
        ],
    },
    "local-ui.capability.mint": {
        allowedPaths: [
            "$.token",
            "$.capabilityId",
            "$.workspaceId",
            "$.deviceId",
            "$.userSessionId",
        ],
        allowedFieldNames: [
            "token",
            "capabilityId",
            "workspaceId",
            "deviceId",
            "userSessionId",
        ],
    },
    "local-ui.session-token-issued": {
        allowedPaths: [
            "$.sessionToken",
            "$.session.id",
            "$.session.workspaceId",
            "$.session.deviceId",
            "$.session.userSessionId",
        ],
        allowedFieldNames: ["sessionToken"],
    },
    "local-ui.session.exchange": {
        allowedPaths: [
            "$.sessionToken",
            "$.session.id",
            "$.session.workspaceId",
            "$.session.deviceId",
            "$.session.userSessionId",
        ],
        allowedFieldNames: ["sessionToken"],
    },
    "provider-profile.credential-status": {
        allowedPaths: [
            "$.credential",
            "$.maskedLabel",
            "$.credential.credential",
            "$.credential.maskedLabel",
        ],
        allowedPathPatterns: [
            "$.profiles[*].credential",
            "$.profiles[*].maskedLabel",
            "$.profiles[*].credentialStatus.credential",
            "$.profiles[*].credentialStatus.maskedLabel",
        ],
        allowedFindingLabels: [MASKED_CREDENTIAL_LABEL_RULE.label],
    },
    "workspace.provider-profiles.credential": {
        allowedPaths: [
            "$.credential",
            "$.maskedLabel",
            "$.credential.credential",
            "$.credential.maskedLabel",
        ],
        allowedPathPatterns: [
            "$.profiles[*].credential",
            "$.profiles[*].maskedLabel",
            "$.profiles[*].credentialStatus.credential",
            "$.profiles[*].credentialStatus.maskedLabel",
        ],
        allowedFindingLabels: [MASKED_CREDENTIAL_LABEL_RULE.label],
    },
};
export class PublicPayloadLeakError extends Error {
    result;
    findings;
    constructor(result) {
        super(formatLeakErrorMessage(result));
        this.name = "PublicPayloadLeakError";
        this.result = result;
        this.findings = result.findings;
    }
}
export function normalizePublicPayloadFieldName(value) {
    return value
        .normalize("NFKC")
        .replace(/[^A-Za-z0-9]/gu, "")
        .toLowerCase();
}
function toSeparatedFieldName(value, separator) {
    return value
        .normalize("NFKC")
        .replace(/([A-Z]+)([A-Z][a-z])/gu, `$1${separator}$2`)
        .replace(/([a-z0-9])([A-Z])/gu, `$1${separator}$2`)
        .replace(/[^A-Za-z0-9]+/gu, separator)
        .toLowerCase()
        .split(separator)
        .filter(Boolean)
        .join(separator);
}
export const normalizeForbiddenFieldName = normalizePublicPayloadFieldName;
export function isForbiddenPublicPayloadFieldName(value) {
    return FORBIDDEN_FIELD_NAME_SET.has(normalizePublicPayloadFieldName(value));
}
export function redactDiagnosticText(value) {
    const raw = diagnosticTextFromUnknown(value);
    const redacted = redactSecretText(raw)
        .replace(FORBIDDEN_FIELD_ASSIGNMENT_PATTERN, (_match, key) => `${key}=[redacted]`)
        .replace(LOCAL_PATH_REDACTION_PATTERN, (_match, prefix) => `${prefix}[redacted-local-path]`);
    return redacted.slice(0, MAX_DIAGNOSTIC_TEXT_LENGTH);
}
export function redactPublicDiagnosticText(value) {
    return redactDiagnosticText(value)
        .replace(REDACTED_FORBIDDEN_FIELD_ASSIGNMENT_PATTERN, "[redacted-internal-field]")
        .replace(REDACTED_HANDOFF_FRAGMENT_PATTERN, "[redacted-local-ui-handoff]")
        .replace(REDACTED_TOKEN_CLI_ARGUMENT_PATTERN, "[redacted-cli-secret-argument]")
        .replace(REDACTED_SECRET_ENV_ASSIGNMENT_PATTERN, "[redacted-secret-assignment]");
}
export function scanPublicPayload(surface, payload, options = {}) {
    const safeSurface = sanitizeSurfaceName(surface);
    const policy = resolvePolicy(safeSurface, options);
    const findings = [];
    const seen = new WeakSet();
    const addFinding = (input) => {
        if (findings.length >= policy.maxFindings)
            return;
        const finding = {
            surface: safeSurface,
            path: input.path,
            kind: input.kind,
            label: input.label,
            reason: input.reason,
        };
        if (input.field !== undefined)
            finding.field = input.field;
        if (input.patternId !== undefined)
            finding.patternId = input.patternId;
        findings.push(finding);
    };
    const scanString = (text, path) => {
        for (const rule of stringRulesFor(text)) {
            if (isAllowedPattern(path, rule, policy))
                continue;
            addFinding({
                path,
                kind: "secret-pattern",
                label: rule.label,
                patternId: rule.id,
                reason: `String matched ${rule.label}.`,
            });
        }
    };
    const scanFieldName = (key, path) => {
        const normalized = normalizePublicPayloadFieldName(key);
        if (FORBIDDEN_FIELD_NAME_SET.has(normalized) &&
            !isAllowedField(path, normalized, policy)) {
            addFinding({
                path,
                kind: "forbidden-field",
                label: "forbidden public field",
                field: safeFieldLabel(key),
                reason: "Field is not allowed on public payload surfaces.",
            });
        }
        for (const rule of stringRulesFor(key)) {
            if (isAllowedPattern(path, rule, policy))
                continue;
            addFinding({
                path,
                kind: "secret-pattern",
                label: `${rule.label} in field name`,
                patternId: rule.id,
                reason: "Field name matched a forbidden secret-like pattern.",
            });
        }
    };
    const scanNode = (value, path, depth) => {
        if (findings.length >= policy.maxFindings)
            return;
        if (depth > policy.maxDepth) {
            addFinding({
                path,
                kind: "depth-exceeded",
                label: "payload depth exceeded",
                reason: "Payload exceeded the public payload scan depth limit.",
            });
            return;
        }
        if (typeof value === "string") {
            scanString(value, path);
            return;
        }
        if (value === null ||
            typeof value === "number" ||
            typeof value === "boolean") {
            return;
        }
        if (typeof value === "undefined") {
            addFinding({
                path,
                kind: "unsupported-value",
                label: "undefined value",
                reason: "Public payloads must be JSON-serializable.",
            });
            return;
        }
        if (typeof value === "bigint" ||
            typeof value === "function" ||
            typeof value === "symbol") {
            addFinding({
                path,
                kind: "unsupported-value",
                label: `${typeof value} value`,
                reason: "Public payloads must be JSON-serializable.",
            });
            return;
        }
        if (seen.has(value)) {
            addFinding({
                path,
                kind: "circular-reference",
                label: "circular payload reference",
                reason: "Circular payloads cannot be safely serialized for public surfaces.",
            });
            return;
        }
        seen.add(value);
        if (Array.isArray(value)) {
            const limit = Math.min(value.length, policy.maxArrayItems);
            for (let index = 0; index < limit; index += 1) {
                scanNode(value[index], `${path}[${index}]`, depth + 1);
            }
            if (value.length > policy.maxArrayItems) {
                addFinding({
                    path,
                    kind: "array-truncated",
                    label: "array scan limit exceeded",
                    reason: "Payload array exceeded the public payload scan item limit.",
                });
            }
            return;
        }
        if (value instanceof Date) {
            scanString(value.toISOString(), path);
            return;
        }
        if (value instanceof Error) {
            scanString(`${value.name}: ${value.message}`, `${path}.message`);
            if (value.stack)
                scanString(value.stack, `${path}.stack`);
            return;
        }
        if (value instanceof URL) {
            scanString(value.href, path);
            return;
        }
        if (!isPlainRecord(value)) {
            addFinding({
                path,
                kind: "unsupported-value",
                label: "unsupported object",
                reason: "Only plain JSON objects are allowed on public payload surfaces.",
            });
        }
        for (const [key, entry] of Object.entries(value)) {
            const childPath = childPropertyPath(path, key);
            scanFieldName(key, childPath);
            scanNode(entry, childPath, depth + 1);
        }
    };
    scanNode(payload, "$", 0);
    return {
        surface: safeSurface,
        ok: findings.length === 0,
        findings,
    };
}
export function assertNoPublicPayloadLeaks(surface, payload, options = {}) {
    const result = scanPublicPayload(surface, payload, options);
    if (!result.ok)
        throw new PublicPayloadLeakError(result);
}
export function formatPublicPayloadGuardFailureBody(rejection) {
    const result = rejection instanceof PublicPayloadLeakError ? rejection.result : rejection;
    const surface = sanitizeGuardFailureText(result.surface, "http.response");
    const findings = result.findings
        .slice(0, MAX_GUARD_FAILURE_FINDINGS)
        .map(formatPublicPayloadGuardFailureFinding);
    return {
        error: "public_payload_guard_rejected",
        reason: "public_payload_leak_detected",
        message: "Public response payload was rejected by the no-leak guard.",
        surface,
        findingCount: Math.max(0, result.findings.length),
        findingsTruncated: result.findings.length > findings.length,
        findings,
    };
}
function formatPublicPayloadGuardFailureFinding(finding) {
    const output = {
        kind: isPublicPayloadLeakKind(finding.kind) ? finding.kind : "unknown",
        label: sanitizeGuardFailureText(finding.label, "public payload finding"),
        path: sanitizeGuardFailurePath(finding.path),
    };
    if (finding.field !== undefined) {
        output.field = isForbiddenPublicPayloadFieldName(finding.field)
            ? "restricted-field"
            : sanitizeGuardFailureText(finding.field, "field");
    }
    if (finding.patternId !== undefined) {
        output.patternId = sanitizeGuardFailureText(finding.patternId, "pattern");
    }
    return output;
}
function isPublicPayloadLeakKind(value) {
    return (value === "forbidden-field" ||
        value === "secret-pattern" ||
        value === "unsupported-value" ||
        value === "circular-reference" ||
        value === "depth-exceeded" ||
        value === "array-truncated");
}
function sanitizeGuardFailurePath(value) {
    const sanitized = sanitizeGuardFailureText(value, "$", 200).replace(/[A-Za-z_$][A-Za-z0-9_$]*/gu, (segment) => isForbiddenPublicPayloadFieldName(segment) ? "restricted" : segment);
    return sanitized.startsWith("$") ? sanitized : "$";
}
function sanitizeGuardFailureText(value, fallback, maxLength = MAX_GUARD_FAILURE_TEXT_LENGTH) {
    const redacted = redactPublicDiagnosticText(value)
        .replace(/[`"'\\]/gu, "")
        .replace(/[\u0000-\u001F\u007F]/gu, " ")
        .trim()
        .slice(0, maxLength);
    return redacted.length > 0 ? redacted : fallback;
}
function resolvePolicy(surface, options) {
    const surfacePolicy = SURFACE_POLICIES[surface] ?? {};
    const allowedPaths = new Set([
        ...(surfacePolicy.allowedPaths ?? []),
        ...(options.allowedPaths ?? []),
    ]);
    const allowedPathPatterns = [
        ...(surfacePolicy.allowedPathPatterns ?? []),
        ...(options.allowedPathPatterns ?? []),
    ].map(pathPatternToRegExp);
    const allowedFieldNames = new Set([
        ...(surfacePolicy.allowedFieldNames ?? []),
        ...(options.allowedFieldNames ?? []),
        ...(options.allowedKeys ?? []),
    ].map(normalizePublicPayloadFieldName));
    const allowedFindingLabels = new Set([
        ...(surfacePolicy.allowedFindingLabels ?? []),
        ...(options.allowedFindingLabels ?? []),
        ...(options.allowedLabels ?? []),
    ]);
    const allowedPatternIds = new Set([
        ...(surfacePolicy.allowedPatternIds ?? []),
        ...(options.allowedPatternIds ?? []),
    ]);
    return {
        allowedPaths,
        allowedPathPatterns,
        allowedFieldNames,
        allowedFindingLabels,
        allowedPatternIds,
        maxDepth: options.maxDepth ?? DEFAULT_MAX_DEPTH,
        maxArrayItems: options.maxArrayItems ?? DEFAULT_MAX_ARRAY_ITEMS,
        maxFindings: options.maxFindings ?? DEFAULT_MAX_FINDINGS,
    };
}
function stringRulesFor(value) {
    const rules = SECRET_STRING_RULES.filter((rule) => ruleMatches(rule, value));
    if (ruleMatches(MASKED_CREDENTIAL_LABEL_RULE, value)) {
        rules.push(MASKED_CREDENTIAL_LABEL_RULE);
    }
    return rules;
}
function ruleMatches(rule, value) {
    rule.regex.lastIndex = 0;
    const matches = rule.regex.test(value);
    rule.regex.lastIndex = 0;
    return matches;
}
function isAllowedField(path, normalizedField, policy) {
    return (isAllowedPath(path, policy) || policy.allowedFieldNames.has(normalizedField));
}
function isAllowedPattern(path, rule, policy) {
    return (isAllowedPath(path, policy) ||
        policy.allowedFindingLabels.has(rule.label) ||
        policy.allowedPatternIds.has(rule.id));
}
function isAllowedPath(path, policy) {
    return (policy.allowedPaths.has(path) ||
        policy.allowedPathPatterns.some((pattern) => pattern.test(path)));
}
function pathPatternToRegExp(pattern) {
    return new RegExp(`^${pattern.split("*").map(escapeRegExp).join("[^.\\[\\]]+")}$`, "u");
}
function diagnosticTextFromUnknown(value) {
    if (typeof value === "string")
        return value;
    if (value instanceof Error)
        return `${value.name}: ${value.message}`;
    if (value === null ||
        typeof value === "number" ||
        typeof value === "boolean" ||
        typeof value === "bigint") {
        return String(value);
    }
    return "[redacted-diagnostic-payload]";
}
function sanitizeSurfaceName(surface) {
    const redacted = redactDiagnosticText(surface)
        .replace(/[\u0000-\u001F\u007F]/gu, " ")
        .trim()
        .slice(0, MAX_SURFACE_LABEL_LENGTH);
    return redacted.length > 0 ? redacted : "public-payload";
}
function formatLeakErrorMessage(result) {
    const preview = result.findings
        .slice(0, 8)
        .map((finding) => {
        const field = finding.field ? ` field=${finding.field}` : "";
        return `${finding.kind}:${finding.label} at ${finding.path}${field}`;
    })
        .join("; ");
    const more = result.findings.length > 8 ? "; additional findings omitted" : "";
    return `Public payload leak guard rejected ${result.surface}: ${result.findings.length} finding(s). ${preview}${more}`;
}
function childPropertyPath(parent, key) {
    const safeKey = safePathKey(key);
    if (/^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(safeKey)) {
        return `${parent}.${safeKey}`;
    }
    return `${parent}[${JSON.stringify(safeKey)}]`;
}
function safePathKey(key) {
    if (stringRulesFor(key).length > 0)
        return "[redacted-key]";
    const withoutControls = key.replace(/[\u0000-\u001F\u007F]/gu, " ").trim();
    if (withoutControls.length === 0)
        return "[empty-key]";
    if (withoutControls.length <= MAX_PATH_KEY_LENGTH)
        return withoutControls;
    return `${withoutControls.slice(0, MAX_PATH_KEY_LENGTH - 1)}…`;
}
function safeFieldLabel(key) {
    if (stringRulesFor(key).length > 0)
        return "[redacted-field]";
    return key
        .replace(/[\u0000-\u001F\u007F]/gu, " ")
        .slice(0, MAX_PATH_KEY_LENGTH);
}
function isPlainRecord(value) {
    const prototype = Object.getPrototypeOf(value);
    return prototype === Object.prototype || prototype === null;
}
function escapeRegExp(value) {
    return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
}
