import crypto from "node:crypto"; import { estimateBase64DecodedBytes } from "../media/base64.js"; export const REDACTED_IMAGE_DATA = ""; const NON_CREDENTIAL_FIELD_NAMES = new Set([ "passwordfile", "tokenbudget", "tokencount", "tokenfield", "tokenlimit", "tokens", ]); function toLowerTrimmed(value: unknown): string { return typeof value === "string" ? value.trim().toLowerCase() : ""; } function normalizeFieldName(value: string): string { return value.replaceAll(/[^a-z0-9]/gi, "").toLowerCase(); } function isCredentialFieldName(key: string): boolean { const normalized = normalizeFieldName(key); if (!normalized || NON_CREDENTIAL_FIELD_NAMES.has(normalized)) { return false; } if (normalized === "authorization" || normalized === "proxyauthorization") { return true; } return ( normalized.endsWith("apikey") || normalized.endsWith("password") || normalized.endsWith("passwd") || normalized.endsWith("passphrase") || normalized.endsWith("secret") || normalized.endsWith("secretkey") || normalized.endsWith("token") ); } function hasImageMime(record: Record): boolean { const candidates = [ toLowerTrimmed(record.mimeType), toLowerTrimmed(record.media_type), toLowerTrimmed(record.mime_type), ]; return candidates.some((value) => value.startsWith("image/")); } function shouldRedactImageData(record: Record): record is Record { if (typeof record.data !== "string") { return false; } const type = toLowerTrimmed(record.type); return type === "image" || hasImageMime(record); } function digestBase64Payload(data: string): string { return crypto.createHash("sha256").update(data).digest("hex"); } function visitDiagnosticPayload( value: unknown, opts?: { omitField?: (key: string) => boolean }, ): unknown { const seen = new WeakSet(); const visit = (input: unknown): unknown => { if (Array.isArray(input)) { return input.map((entry) => visit(entry)); } if (!input || typeof input !== "object") { return input; } if (seen.has(input)) { return "[Circular]"; } seen.add(input); const record = input as Record; const out: Record = {}; for (const [key, val] of Object.entries(record)) { if (opts?.omitField?.(key)) { continue; } out[key] = visit(val); } if (shouldRedactImageData(record)) { out.data = REDACTED_IMAGE_DATA; out.bytes = estimateBase64DecodedBytes(record.data); out.sha256 = digestBase64Payload(record.data); } return out; }; return visit(value); } /** * Redacts image/base64 payload data from diagnostic objects before persistence. */ export function redactImageDataForDiagnostics(value: unknown): unknown { return visitDiagnosticPayload(value); } /** * Removes credential-like fields and image/base64 payload data from diagnostic * objects before persistence. */ export function sanitizeDiagnosticPayload(value: unknown): unknown { return visitDiagnosticPayload(value, { omitField: isCredentialFieldName }); }