Irgendwann braucht man ein ReadableStream<Uint8Array>. Also öffnet man eine leere Datei und fängt wieder an, dasselbe Controller-Grundgerüst zu schreiben. Dann muss man am Ende die Bytes einsammeln. Noch ein Utility. Danach soll das Ergebnis irgendwohin, wo statt eines Uint8Array ein ArrayBuffer erwartet wird. Noch eine Konvertierung.
Nichts davon ist schwierig. Alles davon ist Lärm.
Deshalb habe ich diese Teile in @hsblabs/web-stream-extras zusammengefasst.
Die drei Muster, die ich ständig neu geschrieben habe
Einen ReadableStream aus einem Chunk-Array bauen
// beforeconst stream = new ReadableStream({ start(controller) { for (const chunk of chunks) controller.enqueue(chunk); controller.close(); },});Einen Stream in ein einzelnes Uint8Array einsammeln
// beforeconst chunks: Uint8Array[] = [];const reader = stream.getReader();while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value);}const result = concatU8Arrays(chunks); // auch das musste ich selbst schreibenZwischen Uint8Array, ArrayBuffer und Strings konvertieren
Diese kleinen Unterschiede, bei denen man wieder nach der richtigen View oder Copy-Variante suchen muss.
After
import { readableFromChunks, readAllBytes, stringToBinary, binaryToString,} from "@hsblabs/web-stream-extras";
const stream = readableFromChunks([ stringToBinary("hello"), stringToBinary(" world"),]);
const result = await readAllBytes(stream);console.log(binaryToString(result)); // "hello world"Installation:
npm install @hsblabs/web-stream-extrasLäuft unter Node.js ≥22 und in modernen Browsern. Keine Runtime-Abhängigkeiten.
Binäre Transform-Pipelines aufbauen
Wenn eine benutzerdefinierte Transform-Stufe nötig ist, liefert ByteTransformStream eine typisierte Basisklasse, mit der sich TransformStream erweitern lässt, ohne die komplette Infrastruktur neu zu schreiben.
import { ByteTransformStream } from "@hsblabs/web-stream-extras";
class UpperCaseStream extends ByteTransformStream { transform(chunk: Uint8Array) { // Bytes verarbeiten und mit this.push() ausgeben }}ByteQueue übernimmt das interne Buffering, wenn teilweise Byte-Lesevorgänge chunkübergreifend verfolgt werden müssen. encodeBase64Url und decodeBase64Url sitzen an den Grenzen zwischen Byte-Daten und Text.
Stream-Verschlüsselung
Verschlüsselung liegt in einem separaten Subpath. Wenn sie nicht gebraucht wird, wird sie auch nicht mit ausgeliefert.
import { encryptStream, decryptStream } from "@hsblabs/web-stream-extras/encryption";
const key = crypto.getRandomValues(new Uint8Array(32));
const plaintext = readableFromChunks([stringToBinary("secret payload")]);const encrypted = encryptStream(key, plaintext);const decrypted = decryptStream(key, encrypted);
console.log(binaryToString(await readAllBytes(decrypted))); // "secret payload"Beide Funktionen nehmen ein ReadableStream<Uint8Array> entgegen und geben denselben Typ zurück, wodurch sie sich sauber in den Rest der Pipeline einfügen. Die internen EncryptionStream- und DecryptionStream-Bausteine sind außerdem als TransformStream-Wrapper exportiert, falls man sie direkt braucht.
Das Format nutzt AES-GCM mit per Record via HKDF abgeleiteten Schlüsseln und Nonces. Es ist für Streams ausgelegt, nicht als allgemeines Dateiverschlüsselungsformat.
Muster mit Master-Key
Wenn die Anwendung bereits einen separaten AES-GCM-Master-Key verwaltet und pro Stream verschlüsselte Metadaten plus Stream-Key speichern möchte, kapselt webCryptoStream genau dieses Muster.
import { webCryptoStream } from "@hsblabs/web-stream-extras/encryption";
const masterKey = await crypto.subtle.generateKey( { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"],);
const crypto$ = webCryptoStream(masterKey);
const streamKey = await crypto$.createStreamKey();
const encrypted = await crypto$.encrypt(streamKey, plaintext);const decrypted = await crypto$.decrypt(streamKey, encrypted);Wenn bereits ein roher 32-Byte-Schlüssel vorhanden ist, reichen encryptStream / decryptStream allein.
Was das Paket nicht tut
Das sollte man explizit sagen:
- Keine Authentifizierung — es geht um Vertraulichkeit, nicht um Identitätsprüfung
- Keine Schlüsselspeicherung — wo Stream-Keys liegen, entscheidet die Anwendung
- Keine passwortbasierte Schlüsselableitung — PBKDF2 / Argon2 müssen separat eingebracht werden
- Keine Nutzerverwaltung — das hier ist ein Low-Level-Stream-Utility, kein Auth-Framework
Wenn etwas fehlt, ist ein Issue der schnellste Weg.
hsb.horse