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
This commit is contained in:
1 parent
4ceb61f095
commit
d07c18441b
82 files changed
+2775
-838
No files matched your search
@@ -0,0 +1,63 @@
|
||||
// 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}`),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user