logo hsb.horse
← Zur Blog-Übersicht

Blog

Ich hatte es satt, denselben Web-Streams-Boilerplate immer wieder zu schreiben

Warum ich wiederkehrende ReadableStream<Uint8Array>-Utilities in @hsblabs/web-stream-extras gebündelt habe. Die drei Muster, die ständig wiederkamen, die ByteTransformStream-Basisklasse und Stream-Verschlüsselung mit Web Crypto.

Veröffentlicht:

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

// before
const stream = new ReadableStream({
start(controller) {
for (const chunk of chunks) controller.enqueue(chunk);
controller.close();
},
});

Einen Stream in ein einzelnes Uint8Array einsammeln

// before
const 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 schreiben

Zwischen 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:

Terminal window
npm install @hsblabs/web-stream-extras

Lä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

npm · GitHub

Wenn etwas fehlt, ist ein Issue der schnellste Weg.