Any value passed to a logger
Objects already visited on this path, used to cut cycles
Current nesting depth
A scrubbed, serializable copy
scrubValue({ title: 'Aspirin', headers: { Authorization: 'Bearer abcdefgh1234' }, email: 'a@b.co' });
// { title: 'Aspirin', headers: { Authorization: '[redacted]' }, email: '[redacted]' }
export function scrubValue(value: unknown, seen = new WeakSet<object>(), depth = 0): unknown {
if (typeof value === 'string') {
return scrubText(value);
}
if (typeof value === 'function') {
return '[function]';
}
if (typeof value === 'symbol') {
return scrubText(value.toString());
}
if (typeof value === 'bigint') {
return value.toString();
}
if (typeof value !== 'object' || value === null) {
return value;
}
if (value instanceof Error) {
return { name: value.name, message: scrubText(value.message) };
}
if (seen.has(value)) {
return '[circular]';
}
if (depth >= MAX_DEPTH) {
return Array.isArray(value) ? '[array]' : '[object]';
}
seen.add(value);
try {
if (Array.isArray(value)) {
const items = value.slice(0, MAX_ITEMS).map((item) => scrubValue(item, seen, depth + 1));
return value.length > MAX_ITEMS ? [...items, `[${value.length - MAX_ITEMS} more]`] : items;
}
if (value instanceof Map || value instanceof Set) {
return scrubValue([...value], seen, depth);
}
const result: Record<string, unknown> = {};
for (const [key, item] of Object.entries(value).slice(0, MAX_ITEMS)) {
result[key] = isSensitiveKey(key) ? REDACTED : scrubValue(item, seen, depth + 1);
}
return result;
} finally {
seen.delete(value);
}
}
Copies a value into a JSON-safe form with sensitive data removed: strings are scrubbed, values under sensitive keys are replaced, and size, depth, and cycles are bounded.