Files
abdulive/packages/core/src/logger.ts
T
ZeasyAlexandClaude Opus 5 d07c18441b S1: Einrichtungs-Assistent, Twitch-Auth, Datenbank und Chat-Aufnahme
Zugangsdaten kommen jetzt aus der App statt aus einer .env: ein
Einrichtungs-Assistent auf http://localhost:4792/setup nimmt Client-ID,
Secret und OBS-Passwort entgegen, der OAuth-Rueckruf laeuft auf denselben
Server. Gespeichert wird verschlueselt (AES-256-GCM) in einem Volume,
bewusst nicht in der Datenbank - die wird auf die Windows-Seite gesichert.
Siehe ADR-0005.

Technikbasis vereinfacht: Node 24 bringt node:sqlite, WebSocket, fetch und
die Ausfuehrung von TypeScript mit. Das Projekt hat damit null
Laufzeitabhaengigkeiten und keinen Build-Schritt; pnpm-Workspaces und die
Paketmanifeste sind entfallen, Importe laufen ueber die imports-Map der
Wurzel-package.json. erasableSyntaxOnly verhindert Syntax, die das Type
Stripping stillschweigend entfernen wuerde. Siehe Nachtrag 2 in ADR-0001.

Enthalten:
- packages/core: Config, Layout, Logger mit Secret-Maskierung, Retry, YAML
- packages/db: Migrationen, Repositories, alle SQL-Anweisungen an einem Ort
- packages/twitch: Secret-Speicher, OAuth, Helix mit Token-Bucket, IRC
- apps/ingest: Daemon, Einrichtungsseite, Spike-Erkennung
- 20 Tests (IRC-Parser, Spike-Detektor, Datenbankschicht)

Der Chat verbindet sich anonym, solange nichts hinterlegt ist - damit laesst
sich die Datenaufnahme pruefen, bevor die Twitch-Apps existieren.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013VTGjd92LsDDWF3frd9FTB
2026-09-09 09:56:55 +02:00

64 lines
2.3 KiB
TypeScript

// 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<Level, number> = { 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<string, unknown> = {};
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}`),
};
}