BillTracker/utils/logger.cts

66 lines
2.9 KiB
TypeScript
Raw Normal View History

'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 };