/** * IT Glue redaction. * * SECURITY-CRITICAL. This module recursively walks an arbitrary value * (object | array | scalar) and replaces any value whose KEY name looks like * a credential with the string "[REDACTED]". The original input is not * mutated — a deep-cloned copy is returned. * * Why it exists: IT Glue documents and configurations frequently embed plain- * text passwords, API keys, and tokens. None of those values may EVER reach * the LLM context, log lines, or any database row — only doc IDs and names. * See docs/wulf-pulse-ticket-analyzer-prompt.md → "Critical correctness notes". * * The match pattern is intentionally broad. False positives (a benign field * happens to be named "key") are acceptable; false negatives are not. */ const SENSITIVE_KEY_PATTERN = /password|secret|key|token|credential|api[_-]?key/i; export const REDACTED_VALUE = '[REDACTED]'; /** * Recursively redact values whose keys match the sensitive-key pattern. * Returns a new value; does not mutate the input. */ export function redact(value: T): T { return redactInternal(value, new WeakSet()) as T; } function redactInternal(value: unknown, seen: WeakSet): unknown { if (value === null || value === undefined) return value; // Primitives — return as-is. if (typeof value !== 'object') return value; // Cycle guard — if we've already seen this object on this branch, return a // marker rather than recursing infinitely. (IT Glue payloads shouldn't have // cycles in practice, but defensive against malformed input.) if (seen.has(value as object)) return '[CIRCULAR]'; seen.add(value as object); if (Array.isArray(value)) { return value.map((item) => redactInternal(item, seen)); } const out: Record = {}; for (const [key, nested] of Object.entries(value as Record)) { if (isSensitiveKey(key)) { // Even if the value is an object/array, replace the whole subtree — // a credentials block named "auth" should not leak its leaves. out[key] = nested === null || nested === undefined ? nested : REDACTED_VALUE; } else { out[key] = redactInternal(nested, seen); } } return out; } /** * Exported for tests. True if the key name looks like a credential field. */ export function isSensitiveKey(key: string): boolean { return SENSITIVE_KEY_PATTERN.test(key); }