<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: mktbsh</title>
    <description>The latest articles on DEV Community by mktbsh (@hsb).</description>
    <link>https://dev.to/hsb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3769829%2F66012e20-1e28-4336-904c-39adf7cc3cf9.jpeg</url>
      <title>DEV Community: mktbsh</title>
      <link>https://dev.to/hsb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hsb"/>
    <language>en</language>
    <item>
      <title>I built Header Relay: a Chrome extension that captures response headers and replays them on later requests</title>
      <dc:creator>mktbsh</dc:creator>
      <pubDate>Fri, 17 Jul 2026 17:02:07 +0000</pubDate>
      <link>https://dev.to/hsb/i-built-header-relay-a-chrome-extension-that-captures-response-headers-and-replays-them-on-later-6h5</link>
      <guid>https://dev.to/hsb/i-built-header-relay-a-chrome-extension-that-captures-response-headers-and-replays-them-on-later-6h5</guid>
      <description>&lt;p&gt;If you develop against APIs that hand you a session token, trace ID, or gateway header in a &lt;strong&gt;response&lt;/strong&gt;, and expect it back on the &lt;strong&gt;next request&lt;/strong&gt;, you know the dance: copy the value from DevTools, paste it into ModHeader or curl, repeat every time it rotates.&lt;/p&gt;

&lt;p&gt;The fix seems obvious: let the browser itself relay the header. But the API you would reach for — a blocking &lt;code&gt;webRequest&lt;/code&gt; listener that rewrites request headers — is exactly what Manifest V3 removed. That constraint shaped most of the design below.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://chromewebstore.google.com/detail/header-relay/olmfkdclaloaacojbgaabokehlbphilf?utm_source=devto&amp;amp;amp%3Butm_medium=social&amp;amp;amp%3Butm_campaign=1st_promotion&amp;amp;amp%3Butm_id=20260718" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh3.googleusercontent.com%2FXA5n2KlYW0KEcZ-m7Xd2blm5AdPNTfuBbHZu05TV2Jl1cVQ-dUf-Qcn-YNKPsc0GQuHfTvZ-wFtr8g2py-8ooWEVbA%3Ds128-rj-sc0x00ffffff" height="128" class="m-0" width="128"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://chromewebstore.google.com/detail/header-relay/olmfkdclaloaacojbgaabokehlbphilf?utm_source=devto&amp;amp;amp%3Butm_medium=social&amp;amp;amp%3Butm_campaign=1st_promotion&amp;amp;amp%3Butm_id=20260718" rel="noopener noreferrer" class="c-link"&gt;
            Header Relay - Chrome Web Store
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Capture HTTP response headers and relay fixed or captured headers to matching browser requests.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fssl.gstatic.com%2Fchrome%2Fwebstore%2Fimages%2Ficon_48px.png" width="48" height="48"&gt;
          chromewebstore.google.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;a href="https://chromewebstore.google.com/detail/header-relay/olmfkdclaloaacojbgaabokehlbphilf?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=1st_promotion&amp;amp;utm_id=20260718" rel="noopener noreferrer"&gt;Header Relay&lt;/a&gt; automates the loop anyway, entirely inside the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Header Relay is a Manifest V3 Chrome extension for API development and testing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Captured headers&lt;/strong&gt; — watch responses from a target origin, keep a configured header's value (e.g. &lt;code&gt;x-session-token&lt;/code&gt;) in session memory, and attach it to later matching requests automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixed headers&lt;/strong&gt; — always attach static headers like &lt;code&gt;x-client-id: my-app&lt;/code&gt; to a target origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Origin-scoped profiles&lt;/strong&gt; — each profile targets specific origins (localhost, staging, internal envs), and multiple profiles can run at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Excluded paths&lt;/strong&gt; — glob patterns to keep relay-managed headers off assets or specific endpoints, with a built-in tester.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL Probe&lt;/strong&gt; — paste a URL and see whether it matches, which profiles apply, and exactly which headers would be attached — before saving.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why not a proxy or a static header extension?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Static header extensions (ModHeader-style) can't follow &lt;strong&gt;dynamic&lt;/strong&gt; values that the server rotates per session.&lt;/li&gt;
&lt;li&gt;A local proxy (mitmproxy, Charles) can, but it means TLS interception on your machine — heavyweight for "I just want the browser to keep sending this token back".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Header Relay sits in that gap: no proxy, no DevTools babysitting. And if ModHeader's recent removal from the stores left you shopping for a replacement, the static-header use case is covered too.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;p&gt;So how do you rewrite request headers on MV3 without blocking &lt;code&gt;webRequest&lt;/code&gt;? By splitting observation from rewriting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;webRequest&lt;/code&gt; (non-blocking) observes response headers for enabled target origins and stores captured values in &lt;code&gt;storage.session&lt;/code&gt; (memory only).&lt;/li&gt;
&lt;li&gt;Every enabled profile + its session state is compiled into a single intermediate representation, which is synced to &lt;strong&gt;one declarativeNetRequest session ruleset&lt;/strong&gt;. Attach rules add fixed/captured headers; remove rules strip them on excluded paths.&lt;/li&gt;
&lt;li&gt;On the next matching request, Chrome's DNR engine attaches the headers natively — no request-time JavaScript involved.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  One compile path
&lt;/h3&gt;

&lt;p&gt;Origin matching and path exclusion are needed in three places: DNR rule generation, the URL Probe, and the runtime status view. Implement them three times and they will drift apart eventually — that's where bugs like "the Probe says the header attaches, but the real request never gets it" come from. So all three consume the output of a single &lt;code&gt;compileConfig()&lt;/code&gt; step, and the matching logic exists in exactly one place.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conflicts are errors
&lt;/h3&gt;

&lt;p&gt;What happens when two enabled profiles set the same header on the same origin? DNR rules carry a priority field, so silent auto-resolution would be the easy answer. But in a testing tool, "which value actually got attached?" is worse than attaching nothing at all. So a conflict is a compile error: all owned rules are removed, the UI shows what conflicts with what, and disabling either profile recovers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy and permissions
&lt;/h2&gt;

&lt;p&gt;This category of extension deserves suspicion, so the design is deliberately conservative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Captured values live &lt;strong&gt;only in in-memory session storage&lt;/strong&gt; — cleared on browser restart, extension reload, profile disable, or permission revocation. Masked by default in the UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No external transmission.&lt;/strong&gt; Nothing is sent to me or any analytics backend. Configured headers go only to the target origins you configured.&lt;/li&gt;
&lt;li&gt;Host permissions are &lt;strong&gt;optional and per-origin&lt;/strong&gt;: adding a target origin triggers a &lt;code&gt;permissions.request()&lt;/code&gt; from a user gesture. No grant → no rules, fail-closed. Only &lt;code&gt;http://localhost/*&lt;/code&gt; is bundled for the default local workflow.&lt;/li&gt;
&lt;li&gt;Response-only / transport headers (&lt;code&gt;Set-Cookie&lt;/code&gt;, &lt;code&gt;Host&lt;/code&gt;, &lt;code&gt;Content-Length&lt;/code&gt;, &lt;code&gt;Transfer-Encoding&lt;/code&gt;, …) are rejected outright; sensitive names like &lt;code&gt;Authorization&lt;/code&gt; are allowed with an explicit warning.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;p&gt;WXT + TypeScript + SolidJS + Tailwind CSS. The runtime touches the browser through three ports only (storage / DNR / audit), so the whole extension runs headless in Vitest with in-memory adapters — config in, fake responses, assert on the resulting ruleset, no module mocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://chromewebstore.google.com/detail/header-relay/olmfkdclaloaacojbgaabokehlbphilf?utm_source=devto&amp;amp;amp%3Butm_medium=social&amp;amp;amp%3Butm_campaign=1st_promotion&amp;amp;amp%3Butm_id=20260718" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh3.googleusercontent.com%2FXA5n2KlYW0KEcZ-m7Xd2blm5AdPNTfuBbHZu05TV2Jl1cVQ-dUf-Qcn-YNKPsc0GQuHfTvZ-wFtr8g2py-8ooWEVbA%3Ds128-rj-sc0x00ffffff" height="128" class="m-0" width="128"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://chromewebstore.google.com/detail/header-relay/olmfkdclaloaacojbgaabokehlbphilf?utm_source=devto&amp;amp;amp%3Butm_medium=social&amp;amp;amp%3Butm_campaign=1st_promotion&amp;amp;amp%3Butm_id=20260718" rel="noopener noreferrer" class="c-link"&gt;
            Header Relay - Chrome Web Store
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Capture HTTP response headers and relay fixed or captured headers to matching browser requests.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fssl.gstatic.com%2Fchrome%2Fwebstore%2Fimages%2Ficon_48px.png" width="48" height="48"&gt;
          chromewebstore.google.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://hsblabs.github.io/header-relay/" rel="noopener noreferrer"&gt;https://hsblabs.github.io/header-relay/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Issues &amp;amp; feedback: &lt;a href="https://github.com/hsblabs/header-relay/issues" rel="noopener noreferrer"&gt;https://github.com/hsblabs/header-relay/issues&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Blog:

&lt;ul&gt;
&lt;li&gt;en: &lt;a href="https://hsb.horse/en/blog/header-relay-published/" rel="noopener noreferrer"&gt;https://hsb.horse/en/blog/header-relay-published/&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;ja-JP: &lt;a href="https://hsb.horse/blog/header-relay-published/" rel="noopener noreferrer"&gt;https://hsb.horse/blog/header-relay-published/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It ships with a localhost profile out of the box (&lt;code&gt;x-session-token&lt;/code&gt; capture + a fixed &lt;code&gt;x-client-id&lt;/code&gt;), so &lt;code&gt;pnpm dev&lt;/code&gt; on port 3000 works immediately.&lt;/p&gt;

&lt;p&gt;The source itself isn't public, but bug reports and feature requests are tracked openly on &lt;a href="https://github.com/hsblabs/header-relay/issues" rel="noopener noreferrer"&gt;GitHub issues&lt;/a&gt;. Feedback very welcome — especially edge cases in your header workflows that it doesn't cover yet.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>showdev</category>
    </item>
    <item>
      <title>ModHeader was flagged as malware and pulled from Edge — here's how to audit any header-editing extension before you trust it</title>
      <dc:creator>mktbsh</dc:creator>
      <pubDate>Mon, 13 Jul 2026 04:32:25 +0000</pubDate>
      <link>https://dev.to/hsb/modheader-was-flagged-as-malware-and-pulled-from-edge-heres-how-to-audit-any-header-editing-3ofl</link>
      <guid>https://dev.to/hsb/modheader-was-flagged-as-malware-and-pulled-from-edge-heres-how-to-audit-any-header-editing-3ofl</guid>
      <description>&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;On July 3, 2026, Microsoft removed &lt;strong&gt;ModHeader&lt;/strong&gt; — a header-editing extension with roughly 1.6M combined installs across Chrome and Edge — from the Edge Add-ons store after Google flagged it as malware.&lt;/p&gt;

&lt;p&gt;Independent security researchers who analyzed build 7.0.18 found a hidden module disguised as a date-formatting library. It was capable of collecting visited domains and preparing a daily encrypted upload to an external endpoint. To be precise about what was and wasn't confirmed: the researchers reported the collector was &lt;strong&gt;dormant&lt;/strong&gt; in the build they tested, and they found no confirmed evidence of data actually leaving a test profile. Still, the capability being present at all — undisclosed, in a widely-installed dev tool — is the story.&lt;/p&gt;

&lt;p&gt;Sources (do your own reading, don't just take my word for it):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HackIndex — "ModHeader Malware: Inside the Chrome Spyware Google Removed"&lt;/li&gt;
&lt;li&gt;GridinSoft — "ModHeader Malware Warning: Remove Extension 7.0.18"&lt;/li&gt;
&lt;li&gt;OffSeq Threat Radar — "ModHeader removed for malware"&lt;/li&gt;
&lt;li&gt;Koi Security — "Is ModHeader safe?"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this matters beyond ModHeader
&lt;/h2&gt;

&lt;p&gt;Header-editing extensions are a uniquely sensitive category. To do their job they need broad host permissions (&lt;code&gt;&amp;lt;all_urls&amp;gt;&lt;/code&gt; or close to it) and they sit in the request/response path of literally everything you browse — including whatever internal APIs, staging environments, and authenticated sessions you're debugging. A malicious or compromised one is a bigger blast radius than almost any other extension class.&lt;/p&gt;

&lt;p&gt;If you rely on one of these for API development, this is a good moment to actually check what yours does, not just assume it's fine because it has a lot of installs.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick checklist for auditing your header extension
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read the permissions it actually requests.&lt;/strong&gt; Does it need &lt;code&gt;&amp;lt;all_urls&amp;gt;&lt;/code&gt;, or could it be scoped to specific origins?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check what network requests the extension itself makes&lt;/strong&gt; — not the pages you're on, the extension's own background/service worker. &lt;code&gt;chrome://extensions&lt;/code&gt; → enable Developer mode → inspect the service worker → Network tab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look for bundled dependencies that don't match what a header tool needs.&lt;/strong&gt; A hidden analytics/upload module dressed up as a date library is exactly the kind of thing that hides in plain sight in a large bundle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer extensions where captured/stored values are visible, not hidden.&lt;/strong&gt; If a tool captures a session token or API key from a response header, you should be able to see the actual value it captured, not just a black box confirming "captured."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer declarativeNetRequest over raw webRequest blocking&lt;/strong&gt; where possible — it's a narrower, more sandboxed API for attaching headers, and easier to audit.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where I'm coming from
&lt;/h2&gt;

&lt;p&gt;I maintain &lt;strong&gt;Header Relay&lt;/strong&gt;, a Chrome extension in the same category (attach fixed or captured headers to requests for local/staging/internal API development). I'm not neutral here, so take the following with that in mind — but the checklist above applies to Header Relay too, and you should hold it to the same bar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No bundled code that talks to any external endpoint — header relay logic lives in one reviewable module, and requests are attached entirely via Chrome's &lt;code&gt;declarativeNetRequest&lt;/code&gt; + local storage.&lt;/li&gt;
&lt;li&gt;Captured and fixed header values are shown in plain text in the popup and options page.&lt;/li&gt;
&lt;li&gt;Runtime behavior is scoped to the origins enabled in your active profile — nothing else gets touched.&lt;/li&gt;
&lt;li&gt;Full source and privacy policy are linked from the project site if you want to verify any of this yourself rather than trust the bullet points.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're migrating off ModHeader (or auditing whatever extension you currently use, header-related or not), migration is 3 steps: create a profile with your target origins, re-add any fixed headers (API keys, client IDs) as Fixed Headers, and set up Captured Headers for anything you were previously copy-pasting from responses (session tokens, trace IDs).&lt;/p&gt;

&lt;p&gt;Full writeup with sources: &lt;a href="https://hsblabs.github.io/header-relay/modheader-alternative" rel="noopener noreferrer"&gt;https://hsblabs.github.io/header-relay/modheader-alternative&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not affiliated with ModHeader or its developers — everything above is based on the third-party research linked, published July 2026.&lt;/p&gt;

</description>
      <category>security</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>browser</category>
    </item>
    <item>
      <title>Releasing @hsblabs/web-stream-extras: small utilities for WHATWG byte stream pipelines</title>
      <dc:creator>mktbsh</dc:creator>
      <pubDate>Wed, 04 Mar 2026 18:30:00 +0000</pubDate>
      <link>https://dev.to/hsb/releasing-hsblabsweb-stream-extras-small-utilities-for-whatwg-byte-stream-pipelines-5fil</link>
      <guid>https://dev.to/hsb/releasing-hsblabsweb-stream-extras-small-utilities-for-whatwg-byte-stream-pipelines-5fil</guid>
      <description>&lt;p&gt;I published &lt;a href="https://www.npmjs.com/package/@hsblabs/web-stream-extras" rel="noopener noreferrer"&gt;@hsblabs/web-stream-extras&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It started as internal plumbing I kept copy-pasting across projects. Enough utility collected around &lt;code&gt;ReadableStream&amp;lt;Uint8Array&amp;gt;&lt;/code&gt; that it made sense to package it properly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @hsblabs/web-stream-extras
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node.js ≥22 and modern browsers. No runtime dependencies.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's in the root package
&lt;/h2&gt;

&lt;p&gt;The root export handles the everyday byte stream operations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;readableFromChunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;readAllBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;stringToBinary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;binaryToString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hsblabs/web-stream-extras&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readableFromChunks&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nf"&gt;stringToBinary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;stringToBinary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; world&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readAllBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;binaryToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// "hello world"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;readableFromChunks&lt;/code&gt; wraps an array of &lt;code&gt;Uint8Array&lt;/code&gt; chunks into a proper &lt;code&gt;ReadableStream&lt;/code&gt;. &lt;code&gt;readAllBytes&lt;/code&gt; collects everything from a stream into a single &lt;code&gt;Uint8Array&lt;/code&gt;. The string and buffer conversion helpers — &lt;code&gt;stringToBinary&lt;/code&gt;, &lt;code&gt;binaryToString&lt;/code&gt;, &lt;code&gt;toU8Array&lt;/code&gt;, &lt;code&gt;toArrayBuffer&lt;/code&gt;, &lt;code&gt;concatU8Arrays&lt;/code&gt; — exist because converting between &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;Uint8Array&lt;/code&gt;, and &lt;code&gt;ArrayBuffer&lt;/code&gt; is repetitive and easy to get subtly wrong.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ByteTransformStream&lt;/code&gt; is an abstract base for building typed binary transform pipelines. &lt;code&gt;ByteQueue&lt;/code&gt; handles internal buffering with a clean read/write interface — useful when you need to track partial byte reads across chunks.&lt;/p&gt;

&lt;p&gt;Base64url encoding is included too: &lt;code&gt;encodeBase64Url&lt;/code&gt; and &lt;code&gt;decodeBase64Url&lt;/code&gt;, without padding.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stream encryption
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;encryption&lt;/code&gt; subpath adds encryption built on the Web Crypto API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;encryptStream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;decryptStream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hsblabs/web-stream-extras/encryption&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encryptStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decryptStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;encrypted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;encryptStream&lt;/code&gt; and &lt;code&gt;decryptStream&lt;/code&gt; take a &lt;code&gt;ReadableStream&lt;/code&gt; and return a &lt;code&gt;ReadableStream&lt;/code&gt;. The underlying &lt;code&gt;EncryptionStream&lt;/code&gt; and &lt;code&gt;DecryptionStream&lt;/code&gt; are exposed as &lt;code&gt;TransformStream&lt;/code&gt; wrappers if you need to compose them differently.&lt;/p&gt;

&lt;p&gt;For cases where you need key management, &lt;code&gt;webCryptoStream(masterKey)&lt;/code&gt; handles deriving and encrypting per-stream keys with an &lt;code&gt;AES-GCM&lt;/code&gt; master key.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Web Streams
&lt;/h2&gt;

&lt;p&gt;The WHATWG Streams API is available natively in Node.js ≥18 and all modern browsers. Using it directly avoids Node.js-specific stream abstractions and keeps the code portable. If you're targeting environments where both sides — browser and server — need to handle the same byte pipeline, the WHATWG API is the right base layer.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@hsblabs/web-stream-extras" rel="noopener noreferrer"&gt;npm&lt;/a&gt; · &lt;a href="https://github.com/hsblabs/web-stream-extras" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Issues and feedback welcome.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>node</category>
    </item>
  </channel>
</rss>
