'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 = { 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 = {}; for (const [k, v] of Object.entries(value as Record)) { 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 };