// Logger mit Maskierung. Zugangsdaten duerfen nie in einem Log landen - // Logs werden weitergegeben, in Issues gepastet und im Zweifel im Stream // gezeigt. Siehe ADR-0005. export type Level = 'debug' | 'info' | 'warn' | 'error'; const ORDER: Record = { debug: 10, info: 20, warn: 30, error: 40 }; const MIN = ORDER[(process.env.LOG_LEVEL as Level) || 'info'] ?? ORDER.info; // Feldnamen, deren Wert nie ausgegeben wird const SECRET_KEYS = /(secret|token|password|passwort|authorization|client_?id|refresh|api_?key|webhook)/i; // Muster, die auch in freiem Text maskiert werden const SECRET_PATTERNS: Array<[RegExp, string]> = [ [/oauth:[A-Za-z0-9]+/g, 'oauth:•••'], [/\b[a-z0-9]{30}\b/g, '•••'], // Twitch Client-ID / Token [/\bBearer\s+\S+/gi, 'Bearer •••'], ]; export function mask(value: unknown): unknown { if (typeof value === 'string') { let out = value; for (const [re, replacement] of SECRET_PATTERNS) out = out.replace(re, replacement); return out; } if (Array.isArray(value)) return value.map(mask); if (value && typeof value === 'object') { const out: Record = {}; for (const [k, v] of Object.entries(value)) { out[k] = SECRET_KEYS.test(k) ? (v == null ? v : '•••') : mask(v); } return out; } return value; } function emit(level: Level, scope: string, msg: string, extra?: unknown) { if (ORDER[level] < MIN) return; const ts = new Date().toISOString().slice(11, 23); const tag = level.toUpperCase().padEnd(5); let line = `${ts} ${tag} [${scope}] ${String(mask(msg))}`; if (extra !== undefined) line += ` ${JSON.stringify(mask(extra))}`; (level === 'error' || level === 'warn' ? console.error : console.log)(line); } export type Logger = { debug(msg: string, extra?: unknown): void; info(msg: string, extra?: unknown): void; warn(msg: string, extra?: unknown): void; error(msg: string, extra?: unknown): void; child(sub: string): Logger; }; export function logger(scope: string): Logger { return { debug: (m, e) => emit('debug', scope, m, e), info: (m, e) => emit('info', scope, m, e), warn: (m, e) => emit('warn', scope, m, e), error: (m, e) => emit('error', scope, m, e), child: (sub) => logger(`${scope}:${sub}`), }; }