import { isSensitive } from "@suaveplan/error/redact"; import type { PathRedactionRule, PatternRedactionRule, RedactionOptions, RedactionRule, } from "./redaction.types.js"; const REDACTED = "[REDACTED]"; const DEFAULT_MAX_DEPTH = 10; function pathMatches(rulePath: string, keyPath: string): boolean { const ruleSegments = rulePath.split("."); const keySegments = keyPath.split("."); if (ruleSegments.length !== keySegments.length) return false; for (let i = 0; i < ruleSegments.length; i++) { if (ruleSegments[i] !== "*" && ruleSegments[i] !== keySegments[i]) { return false; } } return true; } function shouldRedactPath( rules: PathRedactionRule[], keyPath: string ): boolean { return rules.some((r) => pathMatches(r.path, keyPath)); } function shouldRedactPattern( rules: PatternRedactionRule[], key: string ): boolean { return rules.some((r) => r.pattern.test(key)); } function redactValue( value: unknown, pathRules: PathRedactionRule[], patternRules: PatternRedactionRule[], label: string, currentPath: string, depth: number, maxDepth: number ): unknown { if (isSensitive(value)) { return label; } if ( shouldRedactPath(pathRules, currentPath) || (currentPath !== "" && shouldRedactPattern(patternRules, currentPath.split(".").at(-1)!)) ) { return label; } if (depth >= maxDepth || value === null || typeof value !== "object") { return value; } if (Array.isArray(value)) { return value.map((item, i) => redactValue( item, pathRules, patternRules, label, `${currentPath}[${i}]`, depth + 1, maxDepth ) ); } const out: Record = {}; for (const [k, v] of Object.entries(value as Record)) { const childPath = currentPath === "" ? k : `${currentPath}.${k}`; out[k] = redactValue( v, pathRules, patternRules, label, childPath, depth + 1, maxDepth ); } return out; } export class RedactionEngine { private readonly pathRules: PathRedactionRule[]; private readonly patternRules: PatternRedactionRule[]; private readonly label: string; private readonly maxDepth: number; public constructor(options: RedactionOptions = {}) { const rules: RedactionRule[] = options.rules ?? []; this.pathRules = rules.filter( (r): r is PathRedactionRule => r.kind === "path" ); this.patternRules = rules.filter( (r): r is PatternRedactionRule => r.kind === "pattern" ); this.label = options.redactedLabel ?? REDACTED; this.maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH; } public redact(ctx: Record): Record { return redactValue( ctx, this.pathRules, this.patternRules, this.label, "", 0, this.maxDepth ) as Record; } } export function applyRedaction( ctx: Record, options: RedactionOptions = {} ): Record { return new RedactionEngine(options).redact(ctx); } export type { RedactionOptions, RedactionRule };