Overlay: fuenf Browser-Sources fuer OBS
Einblendungen, Chat, Statusleiste, Effekte, Pausenbild. Kein Build, kein Framework - OBS laedt die Dateien direkt, und jede Zwischenstufe waere ein Ding, das mitten im Stream kaputtgehen kann. Server-Sent Events statt WebSocket, entgegen der Skizze in chat-control.md: Node bringt einen WebSocket-*Client* mit, aber keinen Server, `EventSource` verbindet sich von allein neu, und die Richtung stimmt - das Overlay hoert nur zu. Gegen die tote Browser-Source nach einem Neustart stehen drei Dinge: der eingebaute Reconnect, ein Herzschlag als echtes Ereignis (ein SSE-Kommentar haelt die Verbindung offen, ist im Browser aber unsichtbar), und ein Zustands-Schnappschuss beim Verbinden. Abos, Raids, Bits und Loeschungen kommen ueber IRC-USERNOTICE und CLEARCHAT herein; das wurde bisher weggeworfen. Follows fehlen weiter, die gibt es nur ueber EventSub. GIFs im Chat: Twitch schickt dafuer einen Tag `gifs`, den wir nicht kannten. gifs=0-22|Pnd7qLFuGCOntZcNKB|https://media0.giphy.com/media/.../giphy.gif PRIVMSG #abdulive_ :[Idk GIF by Respective] Der Nachrichtentext ist nur ein Platzhalter, der Bereich markiert ihn. Ausgewertet wird er jetzt; ohne das stand woertlich "[Idk GIF by Respective]" im Bild. Gefunden wurde der Tag durch einen Rohmitschnitt - in 3094 Nachrichten aus acht grossen Kanaelen war kein einziges GIF, weil die Adresse eben nicht im Text steht. Zwei Fehler, die beim Durchklicken im Browser aufgefallen sind: - Die Aufraeumfunktion im Chat zaehlte Zeilen, die erst nach der Ausblend-Animation aus dem DOM verschwinden, und lief in eine Endlosschleife. In OBS waere die Quelle beim vierzehnten Chatbeitrag eingefroren. - Der Bereichsparser der GIFs war zu nachsichtig: `-1-4` zerfaellt beim Trennen in ['', '1', '4'], und `Number('')` ist 0 - der Bereich saehe gueltig aus und wuerde den falschen Textteil ersetzen. Auf der Uebersichtsseite steht fuer jede Einblendung, jede Chat-Variante und jeden Effekt ein Knopf. Ohne den liesse sich eine Raid-Einblendung nur pruefen, indem man auf einen echten Raid wartet. Was einen Zustand setzt, ist waehrend eines Streams gesperrt; "Stand zuruecksetzen" ausdruecklich nicht, sonst kaeme man aus einem erfundenen Zustand nicht mehr heraus. build-package.ts nimmt jetzt auch html/css/js mit - sonst liefert das Paket 404 statt Overlay. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
3b606e36fb
commit
bc2c8f6cbe
24 files changed
+3221
-8
No files matched your search
@@ -0,0 +1,68 @@
|
||||
// Das Sitzungs-Cookie ist der einzige Schutz vor der Einrichtungsseite, auf
|
||||
// der Twitch-Zugangsdaten eingetragen werden. Hier wird geprueft, dass es
|
||||
// haelt, was es soll - und dass es einen Neustart übersteht.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import crypto from 'node:crypto';
|
||||
import { sitzungGueltig, sitzungToken } from './auth.ts';
|
||||
|
||||
const KEY = crypto.randomBytes(32);
|
||||
const HASH = 'ein-passwort-hash';
|
||||
const JETZT = Date.parse('2026-09-17T18:00:00Z');
|
||||
const IN_30_TAGEN = JETZT + 30 * 24 * 3_600_000;
|
||||
|
||||
test('ein frisch ausgestelltes Cookie gilt', () => {
|
||||
const t = sitzungToken(KEY, HASH, IN_30_TAGEN);
|
||||
assert.equal(sitzungGueltig(t, KEY, HASH, JETZT), true);
|
||||
});
|
||||
|
||||
test('ein Neustart meldet niemanden ab', () => {
|
||||
// Der eigentliche Grund für diese Bauform: der Dienst startet bei jedem
|
||||
// Ausrollen neu. Lag die Sitzungsliste im Arbeitsspeicher, war man danach
|
||||
// jedes Mal draußen. Derselbe Schlüssel - dasselbe Ergebnis, ohne dass
|
||||
// irgendwo etwas gemerkt wurde.
|
||||
const t = sitzungToken(KEY, HASH, IN_30_TAGEN);
|
||||
const nachNeustart = Buffer.from(KEY); // derselbe Wert, neues Objekt
|
||||
assert.equal(sitzungGueltig(t, nachNeustart, HASH, JETZT), true);
|
||||
});
|
||||
|
||||
test('abgelaufen ist abgelaufen', () => {
|
||||
const t = sitzungToken(KEY, HASH, JETZT - 1);
|
||||
assert.equal(sitzungGueltig(t, KEY, HASH, JETZT), false);
|
||||
});
|
||||
|
||||
test('das Ablaufdatum laesst sich nicht verlaengern', () => {
|
||||
// Ohne Signatur ueber das Datum koennte jeder das Cookie umschreiben und
|
||||
// waere damit dauerhaft angemeldet.
|
||||
const t = sitzungToken(KEY, HASH, JETZT + 1000);
|
||||
const [, sig] = t.split('.');
|
||||
const gefaelscht = `${Buffer.from(String(IN_30_TAGEN)).toString('base64url')}.${sig}`;
|
||||
assert.equal(sitzungGueltig(gefaelscht, KEY, HASH, JETZT), false);
|
||||
});
|
||||
|
||||
test('ein fremder Schluessel oeffnet nichts', () => {
|
||||
const t = sitzungToken(KEY, HASH, IN_30_TAGEN);
|
||||
assert.equal(sitzungGueltig(t, crypto.randomBytes(32), HASH, JETZT), false);
|
||||
});
|
||||
|
||||
test('ein neues Passwort macht alle Cookies ungueltig', () => {
|
||||
// Das ist der Ersatz für eine Abmeldeliste: wer sein Passwort wechselt,
|
||||
// wirft damit alle offenen Sitzungen raus.
|
||||
const t = sitzungToken(KEY, HASH, IN_30_TAGEN);
|
||||
assert.equal(sitzungGueltig(t, KEY, 'neuer-hash', JETZT), false);
|
||||
});
|
||||
|
||||
test('Unsinn im Cookie wird abgewiesen, nicht geglaubt', () => {
|
||||
for (const müll of [undefined, '', 'abc', 'abc.def', '.', 'x.y.z',
|
||||
`${Buffer.from('keine-zahl').toString('base64url')}.xx`]) {
|
||||
assert.equal(sitzungGueltig(müll, KEY, HASH, JETZT), false, `"${müll}" haette abgewiesen gehoert`);
|
||||
}
|
||||
});
|
||||
|
||||
test('zwei Anmeldungen mit demselben Ablauf ergeben dasselbe Cookie', () => {
|
||||
// Bewusst so: ohne Zufallsanteil gibt es nichts, was der Server sich merken
|
||||
// müsste. Der Preis ist, dass sich eine einzelne Sitzung nicht gezielt
|
||||
// beenden lässt - dafür gibt es den Passwortwechsel.
|
||||
assert.equal(sitzungToken(KEY, HASH, IN_30_TAGEN), sitzungToken(KEY, HASH, IN_30_TAGEN));
|
||||
});
|
||||
Reference in new issue
Block a user