A Request/Response Logger That Does Not Leak Secrets

The redact-by-key logger I add to every Node service before it touches production logs. Catches headers, JWTs, card numbers, and Stripe keys without paying for a SIEM scrubber.

TypeScript
Frontend
3 snippets
logging
security
error-handling
code-template
nadiaali

By @nadiaali

March 6, 2026

·

Updated July 29, 2026

435 views

14

4.4 (15)

// Redact-by-key middleware for HTTP request/response logging.
// Tested with `npx tsx`. No deps.

type Json = string | number | boolean | null | Json[] | { [k: string]: Json };

const REDACT_KEYS = new Set([
    'authorization', 'cookie', 'set-cookie',
    'password', 'token', 'access_token', 'refresh_token',
    'api_key', 'apikey', 'x-api-key',
    'card', 'cvc', 'pan', 'ssn',
]);
const REDACT_PATTERNS: RegExp[] = [
    /\b\d{13,19}\b/,                    // card-like numbers
    /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/, // JWTs
    /sk_(?:test|live)_[A-Za-z0-9]{16,}/,                     // Stripe secret keys
];

function redact(value: Json, parentKey = ''): Json {
    if (typeof value === 'string') {
        if (REDACT_KEYS.has(parentKey.toLowerCase())) return '[REDACTED]';
        let s = value;
        for (const re of REDACT_PATTERNS) {
            s = s.replace(re, '[REDACTED]');
        }
        return s;
    }
    if (Array.isArray(value)) return value.map((v) => redact(v));
    if (value && typeof value === 'object') {
        const out: { [k: string]: Json } = {};
        for (const [k, v] of Object.entries(value)) {
            out[k] = REDACT_KEYS.has(k.toLowerCase()) ? '[REDACTED]' : redact(v, k);
        }
        return out;
    }
    return value;
}

const sample: Json = {
    headers: { authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.payload.sig', 'x-trace-id': 'abc' },
    body: { user: 'me', card: '4242424242424242', note: 'My key is sk_test_AbCdEfGhIjKlMnOpQrSt12' },
};
console.log(JSON.stringify(redact(sample), null, 2));

I have shipped this redactor in three companies because the failure mode of NOT having it is so loud: one accidental console.log(req.headers) and your bearer tokens land in CloudWatch forever. It does two passes per value. First, a key-name check against a denylist (authorization, password, token, common card-field names). Second, a regex sweep over string values for the patterns that look like secrets even when the key is benign, like a customer-supplied note containing sk_test_.... Keep both layers; key-only redaction misses the notes field every time.