66 lines
2.9 KiB
TypeScript
66 lines
2.9 KiB
TypeScript
'use strict';
|
|
|
|
// Leading import so Node's type-stripping activates for `node --check`.
|
|
import type { Db } from '../types/db';
|
|
|
|
// Structured, redacted application logging. Console-compatible call signature so
|
|
// migrating from `console.*` is mechanical, with two things console can't do:
|
|
// 1. Level gating (LOG_LEVEL, or debug in dev / info in prod by default).
|
|
// 2. Redaction — secrets/PII (tokens, passwords, encrypted values, bearer/JWT)
|
|
// never reach the logs. Critical for a financial app.
|
|
// The backend is abstracted (currently console) so it can be swapped for pino
|
|
// later without touching call sites.
|
|
|
|
const LEVELS: Record<string, number> = { debug: 10, info: 20, warn: 30, error: 40, silent: 100 };
|
|
|
|
function resolveThreshold(): number {
|
|
const env = (process.env.LOG_LEVEL || '').toLowerCase();
|
|
if (env && Object.prototype.hasOwnProperty.call(LEVELS, env)) return LEVELS[env];
|
|
return process.env.NODE_ENV === 'production' ? LEVELS.info : LEVELS.debug;
|
|
}
|
|
let threshold = resolveThreshold();
|
|
|
|
// Object keys whose values are sensitive and must be masked wherever they appear.
|
|
const SENSITIVE_KEY =
|
|
/^(.*_)?(pass(word)?|secret|token|authorization|cookie|api[_-]?key|totp|totp_secret|totp_recovery_codes|encrypted|encrypted_secret|private[_-]?key|session|push_url|push_token|access_token|refresh_token|id_token|client_secret)(_.*)?$/i;
|
|
// Unambiguous secret-shaped strings (bearer tokens + JWTs). Kept conservative to
|
|
// avoid redacting legitimate ids/hashes.
|
|
const SECRETY_STRING = /(bearer\s+[\w.\-]+|eyJ[A-Za-z0-9._\-]{20,})/gi;
|
|
|
|
const MASK = '«redacted»';
|
|
|
|
function redact(value: unknown, depth = 0): unknown {
|
|
if (value == null || depth > 5) return value;
|
|
if (typeof value === 'string') return value.replace(SECRETY_STRING, MASK);
|
|
if (typeof value === 'bigint' || typeof value === 'number' || typeof value === 'boolean')
|
|
return value;
|
|
if (value instanceof Error) return value.stack || value.message;
|
|
if (Array.isArray(value)) return value.map((v) => redact(v, depth + 1));
|
|
if (typeof value === 'object') {
|
|
const out: Record<string, unknown> = {};
|
|
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
out[k] = SENSITIVE_KEY.test(k) ? MASK : redact(v, depth + 1);
|
|
}
|
|
return out;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function emit(level: string, sink: (...a: any[]) => void, args: unknown[]): void {
|
|
if (LEVELS[level] < threshold) return;
|
|
sink(...args.map((a) => redact(a)));
|
|
}
|
|
|
|
const log = {
|
|
debug: (...a: unknown[]) => emit('debug', console.log, a),
|
|
info: (...a: unknown[]) => emit('info', console.log, a),
|
|
warn: (...a: unknown[]) => emit('warn', console.warn, a),
|
|
error: (...a: unknown[]) => emit('error', console.error, a),
|
|
/** Override the active level at runtime (e.g. tests). */
|
|
setLevel(name: string): void {
|
|
if (Object.prototype.hasOwnProperty.call(LEVELS, name)) threshold = LEVELS[name];
|
|
},
|
|
};
|
|
|
|
module.exports = { log, redact };
|