redact.ts1.9 KBView on GitHub

185 error spans on 0 routes in the last 7 days.

const REDACTED_KEYS = new Set([
  'password',
  'token',
  'accessToken',
  'refreshToken',
  'authorization',
  'cookie',
  'apiKey',
  'secret',
  'sessionToken',
]);

const MAX_STRING_LEN = 2000;
const MAX_DEPTH = 8;
const MAX_ARRAY = 50;

export interface RedactOptions {
  captureBodies?: boolean;
}

export function redact(value: unknown, opts: RedactOptions = {}): unknown {
  const bodyKeys = opts.captureBodies
    ? new Set<string>()
    : new Set(['body', 'htmlBody', 'rawBody', 'textBody']);
  return walk(value, 0, bodyKeys);
}

function walk(v: unknown, depth: number, bodyKeys: Set<string>): unknown {
  if (depth > MAX_DEPTH) return '[max-depth]';
  if (v == null) return v;
  if (typeof v === 'string') {
    return v.length > MAX_STRING_LEN ? `${v.slice(0, MAX_STRING_LEN)}…[+${v.length - MAX_STRING_LEN}]` : v;
  }
  if (typeof v === 'number' || typeof v === 'boolean') return v;
  if (typeof v === 'bigint') return v.toString();
  if (typeof v === 'function') return '[function]';
  if (v instanceof Date) return v.toISOString();
  if (v instanceof Error) return { name: v.name, message: v.message, stack: v.stack };
  if (v instanceof Map) return walk(Object.fromEntries(v), depth + 1, bodyKeys);
  if (v instanceof Set) return walk(Array.from(v), depth + 1, bodyKeys);
  if (Array.isArray(v)) {
    const out = v.slice(0, MAX_ARRAY).map((x) => walk(x, depth + 1, bodyKeys));
    if (v.length > MAX_ARRAY) out.push(`…[+${v.length - MAX_ARRAY}]` as unknown);
    return out;
  }
  if (typeof v === 'object') {
    const out: Record<string, unknown> = {};
    for (const [k, val] of Object.entries(v)) {
      if (REDACTED_KEYS.has(k)) {
        out[k] = '[redacted]';
      } else if (bodyKeys.has(k)) {
        const s = typeof val === 'string' ? val : '';
        out[k] = `[body redacted, ${s.length} chars]`;
      } else {
        out[k] = walk(val, depth + 1, bodyKeys);
      }
    }
    return out;
  }
  return String(v);
}