// CASAN shared log helper (Node side). // Taxonomy (shared with packages/casan-harness/scripts/bash/casan-log.sh): // error(0) < warn(1) < info(2) < debug(3) < trace(4), default info. // All lines go to stderr so stdout stays reserved for existing outputs. export const LEVELS = { error: 0, warn: 1, info: 2, debug: 3, trace: 4 }; const raw = (process.env.CASAN_LOG_LEVEL ?? 'info').toLowerCase(); export const LOG_LEVEL = raw in LEVELS ? raw : 'info'; export const LOG_THRESHOLD = LEVELS[LOG_LEVEL]; export function enabled(level) { return (LEVELS[level] ?? LEVELS.info) <= LOG_THRESHOLD; } export function log(level, component, message) { if (!enabled(level)) return; process.stderr.write( `[${level.toUpperCase()}] ${new Date().toISOString()} [${component}] ${message}\n`, ); } // Redaction for trace-level payload excerpts. Mirrors the H4 masking families // (email/phone/id/credit-card/secret/private-key/conn-string/AWS key) so no // secret or PII ever reaches the terminal, even at trace. const REDACTIONS = [ [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(-----END [A-Z ]*PRIVATE KEY-----|$)/g, '[REDACTED_PRIVATE_KEY]'], [/(API[_-]?KEY|ACCESS[_-]?TOKEN|REFRESH[_-]?TOKEN|PASSWORD|JWT[_-]?SECRET|SECRET)\s*[:=]\s*\S+/gi, '$1=[REDACTED]'], [/(postgres|mysql|mongodb):\/\/[^@\s]+@/gi, '$1://[REDACTED]@'], [/AKIA[0-9A-Z]{16}/g, '[REDACTED_AWS_KEY]'], [/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '***MASKED_EMAIL***'], [/\b(?:[0-9]{4}[- ]?){3}[0-9]{4}\b/g, '***MASKED_CARD***'], [/\b[0-9]{9,12}\b/g, '***MASKED_ID***'], [/\+?[0-9][0-9 .-]{8,}[0-9]/g, '***MASKED_PHONE***'], ]; export function redact(text, maxLen = 200) { let out = String(text ?? ''); for (const [re, sub] of REDACTIONS) out = out.replace(re, sub); if (out.length > maxLen) out = `${out.slice(0, maxLen)}…(+${out.length - maxLen} chars)`; return out.replace(/\n/g, '\\n'); }