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:
ZeasyAlexandClaude Opus 5 committed 2026-09-09 09:56:55 +02:00
1 parent 4ceb61f095
commit d07c18441b
82 files changed
+2775 -838

No files matched your search

-12
View File
@@ -1,12 +0,0 @@
{
"name": "@abdulive/shared",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "node --test"
}
}
+66
View File
@@ -0,0 +1,66 @@
// Einheitliches Ereignisformat. Alles, was aus IRC, EventSub, Helix oder OBS
// hereinkommt, wird darauf normalisiert und in `events` geschrieben.
// Siehe docs/03-data-model.md.
export type EventType =
| 'stream.online' | 'stream.offline' | 'stream.segment_changed'
| 'chat.message' | 'chat.first_message' | 'chat.spike'
| 'user.arrived' | 'user.returning'
| 'follow' | 'subscribe' | 'subgift' | 'cheer'
| 'raid.in' | 'raid.out'
| 'points.redeemed' | 'shop.redeemed'
| 'poll.begin' | 'poll.end'
| 'obs.scene_changed' | 'obs.replay_saved'
| 'clip.requested' | 'clip.created' | 'clip.rendered';
export type StreamEvent = {
id: string;
ts: string; // ISO-8601, UTC
type: EventType;
streamId: string | null;
userId: string | null;
payload: unknown;
};
export type ChatMessage = {
id: string; // msg-id aus IRC
ts: string;
userId: string;
login: string;
displayName: string;
text: string;
emotes: Array<{ id: string; name: string; count: number }>;
isFirst: boolean; // first-msg Tag
isReturning: boolean; // returning-chatter Tag
isCommand: boolean;
isMod: boolean;
isSub: boolean;
isBroadcaster: boolean;
bits: number;
color: string | null;
};
export type SpikeEvent = {
score: number;
rate: number;
baseline: number;
chatters: number;
emoteHits: number;
clipMentions: number;
windowFrom: string;
windowTo: string;
overThreshold: boolean;
};
/** ULID-artige, zeitlich sortierbare ID ohne Abhaengigkeit. */
const B32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
export function ulid(now = Date.now()): string {
let time = '';
let t = now;
for (let i = 0; i < 10; i++) { time = B32[t % 32]! + time; t = Math.floor(t / 32); }
const rnd = new Uint8Array(16);
crypto.getRandomValues(rnd);
let tail = '';
for (const b of rnd) tail += B32[b % 32]!;
return time + tail;
}
-2
View File
@@ -1,2 +0,0 @@
// @abdulive/shared — siehe docs/01-architecture.md
export {};
-5
View File
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "outDir": "dist", "rootDir": "src" },
"include": ["src"]
}