logo hsb.horse
← Retour au blog

Blog

J'en avais assez de réécrire le même boilerplate Web Streams

Pourquoi j'ai fini par regrouper des utilitaires ReadableStream<Uint8Array> dans @hsblabs/web-stream-extras. Les trois motifs qui revenaient sans cesse, la classe de base ByteTransformStream et le chiffrement de flux avec Web Crypto.

Publié:

Un jour, vous avez besoin d’un ReadableStream<Uint8Array>. Vous ouvrez un fichier vide et recommencez à écrire la même ossature de contrôleur. Ensuite, il faut récupérer les octets à la fin du flux. Encore un utilitaire. Puis il faut passer ce résultat à quelque chose qui attend un ArrayBuffer plutôt qu’un Uint8Array. Encore une conversion.

Rien de tout cela n’est difficile. Tout cela fait du bruit.

J’ai donc regroupé ces morceaux dans @hsblabs/web-stream-extras.


Les trois motifs que je réécrivais sans cesse

Créer un ReadableStream à partir d’un tableau de chunks

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

Collecter un flux dans un unique Uint8Array

// 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); // celui-là aussi, il fallait l'écrire soi-même

Convertir entre Uint8Array, ArrayBuffer et chaînes

Ces petits décalages qui vous obligent à fouiller encore pour trouver la bonne vue ou le bon schéma de copie.


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

Compatible Node.js ≥22 et navigateurs modernes. Aucune dépendance runtime.


Construire des pipelines de transformation binaire

Quand il faut une étape de transformation personnalisée, ByteTransformStream fournit une classe de base typée qui permet d’étendre TransformStream sans réécrire toute la plomberie.

import { ByteTransformStream } from "@hsblabs/web-stream-extras";
class UpperCaseStream extends ByteTransformStream {
transform(chunk: Uint8Array) {
// traiter les octets et pousser la sortie avec this.push()
}
}

ByteQueue gère le buffering interne lorsqu’il faut lire des octets partiels à travers plusieurs chunks. encodeBase64Url et decodeBase64Url servent aux frontières entre données binaires et texte.


Chiffrement de flux

Le chiffrement est rangé dans un sous-chemin séparé. Si vous n’en avez pas besoin, il n’est pas embarqué.

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"

Les deux fonctions prennent et renvoient un ReadableStream<Uint8Array>, ce qui les rend naturelles à brancher au reste d’un pipeline. Les EncryptionStream et DecryptionStream internes sont aussi exposés comme wrappers TransformStream si vous devez les utiliser directement.

Le format repose sur AES-GCM avec des clés et des nonce dérivés par enregistrement via HKDF. Il est conçu pour les flux, pas comme format de chiffrement de fichiers générique.

Variante avec clé maître

Si l’application gère déjà une clé maître AES-GCM distincte et veut stocker des clés par flux avec les métadonnées chiffrées, webCryptoStream encapsule ce modèle.

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);

Si vous avez déjà une clé brute de 32 octets, encryptStream / decryptStream suffisent.


Ce que le package ne fait pas

Cela vaut la peine d’être explicite :

  • Pas d’authentification — il fournit de la confidentialité, pas de la vérification d’identité
  • Pas de stockage de clés — l’application décide où garder les clés de flux
  • Pas de dérivation de clé à partir d’un mot de passe — PBKDF2 / Argon2 restent à votre charge
  • Pas de gestion d’utilisateurs — c’est un utilitaire bas niveau pour les flux, pas un framework d’auth

npm · GitHub

Si quelque chose manque, ouvrir une issue reste le moyen le plus rapide.