<?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: Hamed</title>
    <description>The latest articles on DEV Community by Hamed (@godofweb).</description>
    <link>https://dev.to/godofweb</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%2F4112847%2Fae60db95-65ca-4f5e-accd-77f060631c97.png</url>
      <title>DEV Community: Hamed</title>
      <link>https://dev.to/godofweb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/godofweb"/>
    <language>en</language>
    <item>
      <title>Your Nuxt layers are leaking, and your linter can't see it</title>
      <dc:creator>Hamed</dc:creator>
      <pubDate>Mon, 12 Oct 2026 16:07:32 +0000</pubDate>
      <link>https://dev.to/godofweb/your-nuxt-layers-are-leaking-and-your-linter-cant-see-it-57k4</link>
      <guid>https://dev.to/godofweb/your-nuxt-layers-are-leaking-and-your-linter-cant-see-it-57k4</guid>
      <description>&lt;p&gt;Here is a Vue component. Spot the architecture bug.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- lyears/admin/app/components/AdminPanel.vue --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;CartSummary&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks fine, right? No weird imports. Nothing red in the editor. ESLint is happy. The PR gets approved.&lt;/p&gt;

&lt;p&gt;The problem: &lt;code&gt;useCart&lt;/code&gt; and &lt;code&gt;&amp;lt;CartSummary&amp;gt;&lt;/code&gt; live in the &lt;code&gt;shop&lt;/code&gt; layer. The &lt;code&gt;admin&lt;/code&gt; layer was never supposed to touch &lt;code&gt;shop&lt;/code&gt;. You just tied two parts of your app together, and nobody noticed. Not you, not the reviewer, not a single tool in the pipeline.&lt;/p&gt;

&lt;p&gt;Do that thirty more times and your nice modular Nuxt app is one big ball of yarn with a &lt;code&gt;layers/&lt;/code&gt; folder on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nothing catches it
&lt;/h2&gt;

&lt;p&gt;Nuxt layers are a great way to split a big app: a &lt;code&gt;shared&lt;/code&gt; layer for the design system, an &lt;code&gt;auth&lt;/code&gt; layer, a &lt;code&gt;shop&lt;/code&gt; layer, an &lt;code&gt;admin&lt;/code&gt; layer. You get a modular monolith for free.&lt;/p&gt;

&lt;p&gt;But the modular part only holds if each layer stays in its lane. And the tools that guard boundaries (ESLint import rules, dependency-cruiser and friends) all read &lt;code&gt;import&lt;/code&gt; statements.&lt;/p&gt;

&lt;p&gt;In Nuxt, most dependencies &lt;strong&gt;have no import statement&lt;/strong&gt;. Composables, utils, components and Nitro server utils are auto-imported. The boundary tool looks at the file, sees zero imports, and gives it a gold star.&lt;/p&gt;

&lt;p&gt;I hit this on a real project. After a few months, "admin may only use shared and auth" was a rule that lived in a README and nowhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I built layerscope
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://layerscope.kitdev.space/" rel="noopener noreferrer"&gt;layerscope&lt;/a&gt; resolves auto-imports the same way Nuxt does, and fails CI when a layer uses something it is not allowed to.&lt;/p&gt;

&lt;p&gt;You write the rules once:&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="c1"&gt;// nuxt.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nuxt-layerscope&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;layerscope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shared&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="na"&gt;shop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shared&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth&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="na"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shared&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth&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="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx layerscope check &lt;span class="nt"&gt;--prepare&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the component from the top of this post finally gets caught:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;layers/admin/app/components/AdminPanel.vue
  2:14    error  Auto-import "useCart" crosses from layer "admin" into "shop"  layer-boundary
                 useCart → layers/shop/app/composables/useCart.ts
                 allowed for "admin": shared, auth
  8:5     error  Component &amp;lt;CartSummary&amp;gt; crosses from layer "admin" into "shop"  layer-boundary
                 CartSummary → layers/shop/app/components/CartSummary.vue
                 allowed for "admin": shared, auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It tells you what crossed, where it came from, and what the layer is allowed to use. No guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it knows
&lt;/h2&gt;

&lt;p&gt;layerscope does not guess from file names. A small Nuxt module records what Nuxt actually registered: every component, every auto-import, every server util, per layer. So layer priority, overridden components, &lt;code&gt;pathPrefix&lt;/code&gt;, custom component dirs and module auto-imports like &lt;code&gt;@vueuse/nuxt&lt;/code&gt; all come out the way Nuxt sees them.&lt;/p&gt;

&lt;p&gt;Then it parses your scripts with oxc and your templates with the real Vue compiler, tracks scope (a local &lt;code&gt;const useCart&lt;/code&gt; shadows the auto-import, same as at runtime), and checks every edge against your rules.&lt;/p&gt;

&lt;p&gt;If it can't resolve something, it says so. It never quietly calls an unknown reference "safe".&lt;/p&gt;

&lt;p&gt;On a test project with 3,000 source files in 10 layers, a full check takes about 770 ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  "But my project already has 200 violations"
&lt;/h2&gt;

&lt;p&gt;Same. That is why there is a baseline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx layerscope init &lt;span class="nt"&gt;--baseline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init&lt;/code&gt; looks at your layers, writes a config that allows exactly what exists today, and accepts the current findings. Your first CI run is green. From now on, only &lt;strong&gt;new&lt;/strong&gt; violations fail the build. Delete a line from the config when you want to fix that dependency for real.&lt;/p&gt;

&lt;p&gt;The GitHub Action even adds a summary like &lt;code&gt;adds 3, fixes 5&lt;/code&gt; and &lt;code&gt;Baseline: 47 → 42&lt;/code&gt;, so you can watch the mess shrink one PR at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fun extras
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;layerscope why useCart&lt;/code&gt;&lt;/strong&gt; shows every place a symbol is used, and which uses break the rules. Great before you move a composable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;layerscope graph&lt;/code&gt;&lt;/strong&gt; draws the dependency graph between your layers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;layerscope unused&lt;/code&gt;&lt;/strong&gt; finds things nothing depends on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Nuxt DevTools tab&lt;/strong&gt; with findings, the layer graph and symbol traces. It updates when you save. &lt;a href="https://layerscope.netlify.app/__layerscope/" rel="noopener noreferrer"&gt;Try the live demo&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cycle detection, presets, an ESLint plugin, SARIF output&lt;/strong&gt; for the GitHub Security tab, and an &lt;strong&gt;MCP server&lt;/strong&gt; so your coding assistant can ask "may I use this here?" before it writes the code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Works with Nuxt 3 and 4, and with local, npm and git layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in five minutes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nuxi module add nuxt-layerscope &lt;span class="nt"&gt;--dev&lt;/span&gt;
npx layerscope init &lt;span class="nt"&gt;--baseline&lt;/span&gt;
npx layerscope check &lt;span class="nt"&gt;--prepare&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or poke at the &lt;a href="https://stackblitz.com/github/hamedniroomand/nuxt-layerscope/tree/main/examples/shop" rel="noopener noreferrer"&gt;example project on StackBlitz&lt;/a&gt; first. It has four layers and one violation for each rule.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docs: &lt;a href="https://layerscope.kitdev.space/" rel="noopener noreferrer"&gt;https://layerscope.kitdev.space/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/hamedniroomand/nuxt-layerscope" rel="noopener noreferrer"&gt;https://github.com/hamedniroomand/nuxt-layerscope&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/nuxt-layerscope" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/nuxt-layerscope&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is MIT and still young (0.x), so bug reports and weird edge cases are very welcome. If your layers have a secret you want to find out, run it and tell me how many violations you got. I promise not to judge. Much.&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>vue</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>CodePic: a private, offline code screenshot tool built with Svelte</title>
      <dc:creator>Hamed</dc:creator>
      <pubDate>Wed, 30 Sep 2026 15:27:55 +0000</pubDate>
      <link>https://dev.to/godofweb/codepic-a-private-offline-code-screenshot-tool-built-with-svelte-2011</link>
      <guid>https://dev.to/godofweb/codepic-a-private-offline-code-screenshot-tool-built-with-svelte-2011</guid>
      <description>&lt;p&gt;Most code screenshots are a crop of someone's editor. There's a stray sidebar, a blinking cursor, and colors that looked fine in dark mode but terrible on a white slide.&lt;/p&gt;

&lt;p&gt;I wanted something simpler: paste a snippet, pick a look, get a clean image. So I built &lt;strong&gt;CodePic&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://codepic.kitdev.space/" rel="noopener noreferrer"&gt;https://codepic.kitdev.space/&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/hamedniroomand/codepic" rel="noopener noreferrer"&gt;https://github.com/hamedniroomand/codepic&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;You paste your code. CodePic guesses the language and tells you it did, so you can change it in one click. If it isn't sure, it falls back to plain text instead of guessing wrong.&lt;/p&gt;

&lt;p&gt;From there you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pick from five themes (GitHub Light and Dark, Dracula, Nord, One Dark Pro)&lt;/li&gt;
&lt;li&gt;Choose a gradient, a solid color or a transparent background&lt;/li&gt;
&lt;li&gt;Adjust padding, width, font size, line height and the window style&lt;/li&gt;
&lt;li&gt;Download a PNG (at 1×, 2× or 3×) or an SVG&lt;/li&gt;
&lt;li&gt;Copy the image to your clipboard&lt;/li&gt;
&lt;li&gt;Copy a link that opens the exact same snapshot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SVG export keeps the text as text and embeds the code font, so it stays sharp at any size.&lt;/p&gt;
&lt;h2&gt;
  
  
  Highlighting the lines that matter
&lt;/h2&gt;

&lt;p&gt;Click a line number and that line is highlighted while the rest of the code fades back. Click it again to mark the line as added or removed. You get a diff view with &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;−&lt;/code&gt; in the gutter, which is handy for pull request descriptions and blog posts about a change.&lt;/p&gt;
&lt;h2&gt;
  
  
  Private by design
&lt;/h2&gt;

&lt;p&gt;There's no backend. Your code is never uploaded, stored or logged. Highlighting, layout and export all happen in your browser.&lt;/p&gt;

&lt;p&gt;The only thing saved between visits is how the card looks, such as your theme and presets. Never what's in it.&lt;/p&gt;

&lt;p&gt;It also works offline. Visit once and it loads and exports without a connection. You can install it like an app.&lt;/p&gt;
&lt;h2&gt;
  
  
  Share links without a server
&lt;/h2&gt;

&lt;p&gt;The share link packs your code and settings into the URL itself, compressed in the browser. That means long snippets fit, and there's still no server involved.&lt;/p&gt;

&lt;p&gt;A small meter appears next to the button as you get close to the size limit. If a snippet is too big for a link, the app says so and leaves the code out instead of failing quietly.&lt;/p&gt;

&lt;p&gt;One caveat, since anyone with the link can read the code in it: keep secrets out of snippets you share.&lt;/p&gt;
&lt;h2&gt;
  
  
  Other tools can drive it
&lt;/h2&gt;

&lt;p&gt;Every setting can come from a URL parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;codepic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kitdev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;space&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="n"&gt;theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;nord&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you write docs or a blog, a link like that lets readers open your snippet and tweak it themselves. Unknown parameters are ignored, invalid values are skipped with a message, and nothing in a link ever runs as code.&lt;/p&gt;

&lt;p&gt;There's also a command line tool that draws the same image without opening a browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx codepic &lt;span class="nt"&gt;--file&lt;/span&gt; app.ts &lt;span class="nt"&gt;--theme&lt;/span&gt; nord &lt;span class="nt"&gt;-o&lt;/span&gt; out.png
&lt;span class="nb"&gt;cat &lt;/span&gt;app.ts | npx codepic &lt;span class="nt"&gt;--lang&lt;/span&gt; typescript &lt;span class="nt"&gt;-o&lt;/span&gt; out.svg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It uses the same highlighter, themes and layout numbers as the app, so the images match closely. The drop shadow is an approximation, and text wraps by character count, so wide characters like CJK or emoji can overrun a row.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few notes on how it's built
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One config object.&lt;/strong&gt; A single &lt;code&gt;AppearanceConfig&lt;/code&gt; drives the whole snapshot. Code is kept apart from it, so saving and sharing can treat the two differently. Presets and saved settings never contain your code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What you see is what you get.&lt;/strong&gt; The preview and the exported image render from the same &lt;code&gt;CodeWindow&lt;/code&gt; component. The export copy sits off-screen at the exact configured width, so the image matches your settings and not whatever size the preview is scaled to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A plain textarea.&lt;/strong&gt; Editing is a &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; layered over the highlighted lines. It keeps your text exactly as you typed it, and it's deliberately not an IDE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The stack.&lt;/strong&gt; Svelte 5, TypeScript, Shiki for highlighting, modern-screenshot for PNG and SVG, Bits UI for the controls and Vite+ for tooling.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Keyboard shortcuts
&lt;/h2&gt;

&lt;p&gt;Every shortcut uses Ctrl (⌘ on macOS), so nothing fires while you're typing. For example, &lt;code&gt;Ctrl/⌘ + Enter&lt;/code&gt; downloads the image and &lt;code&gt;Ctrl/⌘ + Shift + Enter&lt;/code&gt; copies it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Very long snippets take noticeably longer to highlight and export.&lt;/li&gt;
&lt;li&gt;Copying to the clipboard depends on browser support and permission. When it's blocked, the app says so, and downloading still works.&lt;/li&gt;
&lt;li&gt;Some design tools don't support the SVG export, because it embeds the card as HTML inside the SVG.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;CodePic started as a way to learn Svelte 5, and it was inspired by &lt;a href="https://ray.so" rel="noopener noreferrer"&gt;ray.so&lt;/a&gt;. It turned into something I actually use, so I kept polishing it.&lt;/p&gt;

&lt;p&gt;It's free and open source under the MIT license. Bug reports, ideas and pull requests are welcome. If you try it and something feels off, or you're missing a feature, please open an issue.&lt;/p&gt;

</description>
      <category>svelte</category>
      <category>typescript</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Will your dependencies run on Cloudflare Workers? Check before you deploy</title>
      <dc:creator>Hamed</dc:creator>
      <pubDate>Tue, 29 Sep 2026 20:23:25 +0000</pubDate>
      <link>https://dev.to/godofweb/will-your-dependencies-run-on-cloudflare-workers-check-before-you-deploy-3eb4</link>
      <guid>https://dev.to/godofweb/will-your-dependencies-run-on-cloudflare-workers-check-before-you-deploy-3eb4</guid>
      <description>&lt;p&gt;A Worker that builds and passes its tests can still fail the moment it runs. The usual cause is not your code. It is a dependency, sometimes three levels down, that calls an API the runtime does not have.&lt;/p&gt;

&lt;p&gt;I kept hitting this, so I built &lt;a href="https://github.com/hamedniroomand/edgefit" rel="noopener noreferrer"&gt;edgefit&lt;/a&gt;: a static check that tells you, before you deploy, which APIs your project and its dependencies reach and whether Cloudflare Workers, Bun or Deno support them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fatol3wyppcm05qeszq2u.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fatol3wyppcm05qeszq2u.gif" alt="edgefit check reporting five errors, each with the package, the file, the import chain and a link to the runtime source" width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx edgefit check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads the entry point and settings from your &lt;code&gt;wrangler.jsonc&lt;/code&gt;. For Bun or Deno, pass &lt;code&gt;--target bun --entry src/index.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is what it says about a small app that pulls in a file watcher and a process spawner by accident:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error    unsupported  node:fs.watch  (workerd)
       file watching is not implemented; throws ERR_UNSUPPORTED_OPERATION
       chokidar@4.0.3  node_modules/chokidar/esm/handler.js:1:34
       via src/index.js &amp;gt; chokidar
       see https://github.com/cloudflare/workerd/tree/v1.20260929.1/src/node/internal/internal_fs_callback.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every finding gives the API, the package and version, the file and line, the import chain from your entry, and a link to the runtime source the answer came from. You can check the answer yourself instead of trusting a tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a check works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Resolution.&lt;/strong&gt; edgefit resolves imports the way the target's bundler does, with its export conditions. For Workers that means &lt;code&gt;workerd&lt;/code&gt;, &lt;code&gt;worker&lt;/code&gt; and &lt;code&gt;browser&lt;/code&gt;, so a library's Workers build is checked, not its Node build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extraction.&lt;/strong&gt; It parses each reachable file and collects the Node and Web APIs it uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lookup.&lt;/strong&gt; Each API is checked against pinned data: the &lt;a href="https://github.com/cloudflare/workers-nodejs-compat-matrix" rel="noopener noreferrer"&gt;workers-nodejs-compat-matrix&lt;/a&gt; for what exists, plus a small set of curated overrides for APIs that exist but throw or do nothing, such as &lt;code&gt;fs.watch&lt;/code&gt; or &lt;code&gt;child_process&lt;/code&gt; on Workers. The overrides are read from the runtime source and link to it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your &lt;code&gt;compatibility_date&lt;/code&gt; and flags matter too. Without &lt;code&gt;nodejs_compat&lt;/code&gt;, Node built-ins are unavailable, so &lt;code&gt;import process from 'node:process'&lt;/code&gt; is an error. edgefit reads your wrangler config and reports that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Precision matters more than coverage
&lt;/h2&gt;

&lt;p&gt;A tool that cries wolf gets ignored, so false errors cost more than missed ones. Two things keep edgefit honest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Guards.&lt;/strong&gt; When code checks that an API exists before using it, the finding is reported as guarded and does not fail the check. drizzle-orm's blob columns, for example, use &lt;code&gt;Buffer&lt;/code&gt; without a check, and edgefit reports that only when &lt;code&gt;nodejs_compat&lt;/code&gt; is off. The guarded uses in the same file stay out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real apps.&lt;/strong&gt; It is tested against small apps built on real, pinned packages: the Hono starter, Hono with zod, drizzle-orm on D1, a Postgres client and a Nitro build with oauth4webapi and jose. They must report nothing that nobody can act on. A known-bad app must report exactly what Workers lacks, so a change that makes edgefit miss an API fails too.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it will not tell you
&lt;/h2&gt;

&lt;p&gt;edgefit is static, so a clean run is not a guarantee. Code it cannot analyze, such as a computed &lt;code&gt;require&lt;/code&gt;, is reported as &lt;code&gt;unknown&lt;/code&gt; instead of passing silently. It does not run your code and does not see what happens at runtime. The &lt;a href="https://hamedniroomand.github.io/edgefit/guide/limitations" rel="noopener noreferrer"&gt;limitations page&lt;/a&gt; lists the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the data current
&lt;/h2&gt;

&lt;p&gt;The data is vendored and pinned (workerd, Bun and Deno versions are on the &lt;a href="https://hamedniroomand.github.io/edgefit/guide/status" rel="noopener noreferrer"&gt;status page&lt;/a&gt;), so a result only changes when your project or the data changes. A weekly job runs the real runtimes, compares them with the data and opens an issue when it falls behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  In CI
&lt;/h2&gt;

&lt;p&gt;The GitHub Action checks the merge base and the head of a pull request and comments only on what the pull request adds or fixes, so findings you already accepted do not drown out new ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hamedniroomand/edgefit@v0&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;workerd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It installs the published package and verifies its signature and provenance. JSON output is available for your own tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I would like help
&lt;/h2&gt;

&lt;p&gt;It is a 0.x release, and I would like to know where it is wrong: an API reported that works, or one that should have been reported. Open an issue with the API and the target. Those are the most useful reports, and each one becomes a test.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/hamedniroomand/edgefit" rel="noopener noreferrer"&gt;https://github.com/hamedniroomand/edgefit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://hamedniroomand.github.io/edgefit/" rel="noopener noreferrer"&gt;https://hamedniroomand.github.io/edgefit/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cloudflare</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>WaveRune: hiding a 32-bit ID inside a WAV file, and reading it back without the original</title>
      <dc:creator>Hamed</dc:creator>
      <pubDate>Fri, 11 Sep 2026 16:04:36 +0000</pubDate>
      <link>https://dev.to/godofweb/waverune-hiding-a-32-bit-id-inside-a-wav-file-and-reading-it-back-without-the-original-3mkk</link>
      <guid>https://dev.to/godofweb/waverune-hiding-a-32-bit-id-inside-a-wav-file-and-reading-it-back-without-the-original-3mkk</guid>
      <description>&lt;p&gt;A while ago I wanted to answer a small question about an audio file: which copy is this? Not who owns it, not whether it was tampered with, only which of several exported versions ended up in front of me. The obvious answers all had a catch. Metadata tags disappear the moment someone re-exports the file. Fingerprinting tells you &lt;em&gt;what&lt;/em&gt; a recording is, not which copy. Comparing against the original needs the original, and the whole point was that I did not have it at the time I was asking.&lt;/p&gt;

&lt;p&gt;So I built WaveRune. It embeds a 32-bit identifier inside the audio itself, and it reads the identifier back using only a key. The original recording is not needed for detection. It is a TypeScript library with zero runtime dependencies, a command-line tool, and a browser demo that never uploads your file.&lt;/p&gt;

&lt;p&gt;This post is a tour of what it does, how the signal processing works in plain terms, and what I measured. That last part matters more than the rest. Watermarking libraries tend to promise a lot, and I would rather show you the failure cases up front.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/hamedniroomand/waverune" rel="noopener noreferrer"&gt;https://github.com/hamedniroomand/waverune&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Live demo: &lt;a href="https://hamedniroomand.github.io/waverune/" rel="noopener noreferrer"&gt;https://hamedniroomand.github.io/waverune/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Package: &lt;code&gt;npm install waverune&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Thirty seconds with the CLI
&lt;/h2&gt;

&lt;p&gt;Install it globally, or run it through &lt;code&gt;npx&lt;/code&gt;:&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; &lt;span class="nt"&gt;-g&lt;/span&gt; waverune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Embed the identifier 42 into a WAV file with a key of your choice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;waverune embed input.wav &lt;span class="nt"&gt;-o&lt;/span&gt; marked.wav &lt;span class="nt"&gt;--id&lt;/span&gt; 42 &lt;span class="nt"&gt;--key&lt;/span&gt; my-secret-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Wrote the watermark to "marked.wav".
Requested id: 42
Recovered id: 42
Correlation score: 0.176
SNR: 23.69 dB
MSE: 5.3213e-5
PSNR: 34.33 dB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that &lt;code&gt;embed&lt;/code&gt; did not stop at writing the file. It read &lt;code&gt;marked.wav&lt;/code&gt; back from disk and ran detection on it, so the "Recovered id" line is a real check on the saved bytes. If that check fails, the command keeps the file for inspection and exits with code 3 instead of pretending it worked.&lt;/p&gt;

&lt;p&gt;Now detect, with only the file and the key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;waverune detect marked.wav &lt;span class="nt"&gt;--key&lt;/span&gt; my-secret-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Watermark found. Id: 42
Correlation score: 0.176
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with the wrong key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;waverune detect marked.wav &lt;span class="nt"&gt;--key&lt;/span&gt; wrong-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No watermark found.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That run exits with code 2, which is useful in scripts. Add &lt;code&gt;--json&lt;/code&gt; to any command to get the full result as one line of JSON, including the diagnostics I describe further down.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9dm0fdgb7yx63oe6fi0u.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9dm0fdgb7yx63oe6fi0u.gif" alt="Terminal recording of installing waverune, embedding id 42 and detecting it again" width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you do not want a JavaScript runtime at all, there are standalone executables for macOS, Linux and Windows on the releases page, with an install script that checks the SHA-256 before it puts anything on your PATH.&lt;/p&gt;

&lt;h2&gt;
  
  
  The library
&lt;/h2&gt;

&lt;p&gt;The functional API is four calls. Read a file, embed, write, detect:&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;detect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readWavFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeWavFile&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="s1"&gt;waverune&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-watermark-key&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;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="nx"&gt;n&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;original&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;readWavFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input.wav&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;marked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;original&lt;/span&gt;&lt;span class="p"&gt;,&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;payload&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeWavFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;marked.wav&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;marked&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;saved&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;readWavFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;marked.wav&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;detect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;,&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="k"&gt;if &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="nx"&gt;detected&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Verified:&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payload is a &lt;code&gt;bigint&lt;/code&gt; in the range 0 to 4294967295. &lt;code&gt;embed&lt;/code&gt; returns a new buffer and leaves the input alone. If you already have bytes in memory, &lt;code&gt;decodeWav&lt;/code&gt; and &lt;code&gt;encodeWav&lt;/code&gt; work on &lt;code&gt;Uint8Array&lt;/code&gt;, which is what the browser demo uses. The codec handles 16, 24 and 32-bit PCM and 32-bit float.&lt;/p&gt;

&lt;p&gt;Everything runs on Node.js 22 or later and on Bun. There is no native addon, no model download and no network call anywhere in the package.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the data gets into the sound
&lt;/h2&gt;

&lt;p&gt;I want to explain this properly, because "it hides data in the audio" is not an explanation. The approach is classical spread-spectrum watermarking in the frequency domain. Nothing here is new research. The work was in choosing parameters that hold up on real recordings and in writing an honest acceptance rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Framing.&lt;/strong&gt; The signal is analysed in overlapping windows of about 46 milliseconds, moving forward 10 milliseconds at a time. Both values are set in seconds rather than samples, so the frame grid is the same at 44.1 and 48 kHz.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The band.&lt;/strong&gt; The payload is spread across 500 to 5000 Hz, divided into 48 frequency slots of equal width. That band survives most things people do to audio, and it stays clear of the very low end where a small change is easy to hear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A masking threshold.&lt;/strong&gt; For every frame and every slot, the embedder computes how much it is allowed to change the magnitude. The rule is simple: take the energy of the strongest neighbouring slot, let it spread at 10 dB per slot, then drop 14 dB below that. Frames quieter than 5% of the loudest frame carry nothing at all. I want to be clear that this is a simplified spreading model, not a calibrated psychoacoustic one. Staying under it is not proof that the change is inaudible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Embedding.&lt;/strong&gt; A pseudo-random sequence derived from the key assigns each spectral cell a sign and a bit position. The embedder nudges each cell's magnitude up or down by a fraction of its threshold, according to the bit it carries. Because the analysis windows overlap, one analysis-and-synthesis pass only delivers part of the intended change, so the embedder runs eight passes, each one re-analysing the previous output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detection.&lt;/strong&gt; The detector analyses the candidate file the same way, then removes the host audio statistically. It whitens each frame against its own slots and each slot against its average over the file, and it weights every cell by the inverse of its local residual power so a drum hit or a plosive does not dominate the correlation. Then it correlates against the keyed sequence for every possible block position and for eight sub-hop sample shifts, and keeps the alignment that agrees best with a sync pattern. This is why the original is not needed: the key regenerates the sequence, and the whitening step does the job the original would have done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What "detected" means.&lt;/strong&gt; The payload lives in a 1.5 second block along with a 16-bit sync pattern and an 8-bit checksum, and the block repeats for the length of the file. Acceptance is exact and deterministic. All 16 sync bits must match, and the 8 checksum bits must match the checksum of the decoded payload bits, at the chosen alignment. There is no correlation threshold to tune. If the rule fails, &lt;code&gt;detected&lt;/code&gt; is false, &lt;code&gt;payload&lt;/code&gt; is null, and the raw decoded bits are exposed as a diagnostic so you can see what the detector was looking at.&lt;/p&gt;

&lt;p&gt;The correlation score you saw in the CLI output is diagnostic only. Unmarked audio scores about 0.05, with 0.083 the highest value seen across 585 unmarked, wrong-key and silent trials. The clean embed above scored 0.176, and digital silence scores exactly 0. The score plays no part in the accept or reject decision. I made that choice after an earlier detector accepted a wrong payload on a real-audio excerpt, and I did not want a tunable number standing between the user and a clear answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser demo
&lt;/h2&gt;

&lt;p&gt;The demo is the same library bundled for the browser. Choose a WAV file, type a key, and either embed a new identifier or detect an existing one. Processing happens on your machine. The page has no backend, and there is nothing to upload to.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ymhrjy4x91m8nceygy7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ymhrjy4x91m8nceygy7.jpg" alt="WaveRune demo landing page in light theme" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After an embed, the results panel shows the original and the watermarked audio side by side for listening, the recovered identifier, the sync and checksum outcomes, how many frames were active, and a download button for the marked file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwoao42d0h70khcid3skx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwoao42d0h70khcid3skx.jpg" alt="Demo results panel after embedding identifier 42: watermark embedded and verified, 0 of 16 sync errors, checksum valid, 803 of 805 frames active" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demo caps files at 120 seconds because the work happens on the main thread and a long stereo file can pause the page for a while. The library and the CLI have no such cap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured, and where it fails
&lt;/h2&gt;

&lt;p&gt;Every number in the README comes from a reproducible benchmark run, and the reliability report in the repository lists the inputs, commands and per-file results. These are the results for the current detector on the test sets I have. They are not guarantees for your audio.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test set&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clean synthetic audio, 20 key and payload pairs on each of two fixtures&lt;/td&gt;
&lt;td&gt;40 of 40 exact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The same trials after a 16-bit WAV round trip&lt;/td&gt;
&lt;td&gt;40 of 40 exact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sample-rate conversions through macOS Core Audio&lt;/td&gt;
&lt;td&gt;50 of 50 exact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clean recorded audio, three pairs across nine files&lt;/td&gt;
&lt;td&gt;24 of 27 exact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefix removal on eligible recorded files&lt;/td&gt;
&lt;td&gt;119 of 119 exact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recorded excerpts of 5 seconds or less&lt;/td&gt;
&lt;td&gt;115 of 329 exact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rejection trials with unmarked audio, wrong keys and silence&lt;/td&gt;
&lt;td&gt;0 acceptances in 585&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The line I want you to read twice is the excerpt one. Short clips are where this breaks. The block is 1.5 seconds long and the detector wants to see it repeat, so a clip under about three seconds rarely recovers on recorded audio. On the recorded corpus the clean full-length recovery rate is 24 of 27, not 27 of 27, and the three misses happened on ordinary recordings, not on synthetic edge cases.&lt;/p&gt;

&lt;p&gt;A few more things it does not do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is WAV only.&lt;/strong&gt; MP3, AAC and Opus are out of scope. The transform coding in those formats does its own thing to the spectrum, and I have not measured survival through it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audibility has not been validated by listening tests.&lt;/strong&gt; The masking model is simplified. Some spectral cells exceed its threshold in the measurements. The SNR figures from &lt;code&gt;waverune metrics&lt;/code&gt; are a number, not an opinion about how it sounds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A watermark is not proof of ownership.&lt;/strong&gt; Anyone who has the key can embed one that passes. The checksum protects against decoding errors. It does not authenticate anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False acceptance is possible in principle.&lt;/strong&gt; No wrong payload was accepted in the 585 rejection trials for the current detector. A finite test set does not establish a rate, and the previous detector did accept one, which is the whole reason the acceptance rule is now exact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it on your own recordings and something fails, the most useful thing you can send me is a reproduction: duration, sample rate, bit depth, channel count, the command you ran, and what you did to the audio between embed and detect. Use a throwaway key. The contributing guide in the repo has the full list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I published it anyway
&lt;/h2&gt;

&lt;p&gt;There are commercial watermarking systems that do more than this, and there are research models that survive far harsher attacks. WaveRune is not competing with those. It is a small, readable implementation of the classical approach, with a test matrix that says exactly what it can and cannot do, and no dependency you have to audit.&lt;/p&gt;

&lt;p&gt;If you need to tell copies of a WAV file apart in a pipeline you control, it does that job today. If you want to learn how spread-spectrum audio watermarking works by reading code instead of papers, the source is short enough to read in an afternoon, and every non-obvious signal-processing step has a comment explaining why it is there.&lt;/p&gt;

&lt;p&gt;The repository is at &lt;a href="https://github.com/hamedniroomand/waverune" rel="noopener noreferrer"&gt;https://github.com/hamedniroomand/waverune&lt;/a&gt;, under the MIT licence. Try the demo, break it, and tell me how.&lt;/p&gt;

</description>
      <category>audio</category>
      <category>typescript</category>
      <category>opensource</category>
      <category>dsp</category>
    </item>
    <item>
      <title>Browser first: the one rule behind every tool on KitDev Space</title>
      <dc:creator>Hamed</dc:creator>
      <pubDate>Sun, 06 Sep 2026 22:30:42 +0000</pubDate>
      <link>https://dev.to/godofweb/browser-first-the-one-rule-behind-every-tool-on-kitdev-space-19id</link>
      <guid>https://dev.to/godofweb/browser-first-the-one-rule-behind-every-tool-on-kitdev-space-19id</guid>
      <description>&lt;p&gt;A while ago I caught myself pasting a JWT from a production API into the first "JWT decoder" Google gave me. I wanted to read the claims. I didn't stop to think about where the token went, and honestly, neither does anyone else. We do the same with &lt;code&gt;.env&lt;/code&gt; files in JSON converters and with photos in EXIF strippers. The site has a privacy policy that says the server keeps nothing, and we take its word for it.&lt;/p&gt;

&lt;p&gt;That moment is roughly why &lt;a href="https://kitdev.space" rel="noopener noreferrer"&gt;KitDev Space&lt;/a&gt; exists. I wanted a set of everyday developer tools where I didn't have to take anyone's word for it, including my own. The way I got there was less about the tools and more about one rule I wrote down early and then refused to bend. This post is about that rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;Run the work in the browser. Use a server only when the browser genuinely can't do the job.&lt;/p&gt;

&lt;p&gt;The first half is easy to agree with. The second half is where it gets interesting, because "can't" has to mean something concrete or every tool ends up on the server for convenience. So I wrote down the situations that actually qualify, and the list ended up shorter than I expected:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The work needs a native library. Encoding AVIF or rasterizing an SVG needs a real image codec. Opening a TLS handshake needs a socket.&lt;/li&gt;
&lt;li&gt;The work needs to talk to another host. DNS records, HTTP headers, RDAP, OpenGraph previews. The browser can't make those requests because of CORS, and it shouldn't be able to.&lt;/li&gt;
&lt;li&gt;The work needs a runtime API with no browser equivalent, like &lt;code&gt;Bun.dns&lt;/code&gt; or &lt;code&gt;Bun.Archive&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The input is too large for a browser tab to hold comfortably.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a tool doesn't hit one of those, it stays in the browser. I've been tempted to make exceptions a few times, usually when the server version would have been twenty lines and the browser version two hundred. I haven't yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser does more than I gave it credit for
&lt;/h2&gt;

&lt;p&gt;Going in, I assumed I'd be writing a lot of server code. I was wrong about that. Most of what a developer tool site does is already covered by platform APIs or by a small dependency that runs fine in a tab.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Work&lt;/th&gt;
&lt;th&gt;What handles it in the browser&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SHA-1, SHA-256, SHA-384, SHA-512&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.subtle.digest&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Random bytes, UUIDs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.getRandomValues&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-256-GCM, PBKDF2, HMAC&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.subtle&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resize, rotate, crop&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;OffscreenCanvas&lt;/code&gt;, &lt;code&gt;createImageBitmap&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG, PNG, WebP encode&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OffscreenCanvas.convertToBlob&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reading EXIF and GPS metadata&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;DataView&lt;/code&gt; over the file bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gzip, deflate, tar&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fflate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YAML, TOML, JSONC, JSON5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;confbox&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sql.js&lt;/code&gt; (WebAssembly)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the JWT debugger verifies HS256 signatures with Web Crypto and never sees a server. The AES tool derives its key with PBKDF2 in your tab. The tar explorer opens an archive without uploading it. The SQLite studio runs an actual database in memory. None of that required anything clever, just a willingness to check what the platform already had before reaching for a route.&lt;/p&gt;

&lt;p&gt;Web Crypto does have gaps. There's no MD5, no CRC32, no xxHash. I could have pulled in a JavaScript MD5 library and kept everything client side, but the hash tool also offers xxhash64 and wyhash, and shipping all of that to the browser for algorithms most people rarely use felt wrong. So the hash generator keeps a server path for those, and the page says so in plain words when you pick one. I'd rather tell you which option leaves your machine than pretend nothing does.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc2bu1tppttnsjq5wl4ii.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc2bu1tppttnsjq5wl4ii.png" alt="Hash Generator with SHA-256 selected. A green notice reads " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One module, two paths
&lt;/h2&gt;

&lt;p&gt;A tool with both a browser path and a server path is a maintenance trap waiting to happen. Two implementations, one of them quietly drifts, and now the same input gives different output depending on where it ran. I got bitten by exactly this on the data converter early on.&lt;/p&gt;

&lt;p&gt;The fix I settled on: each shared module exports the function that does the work plus a small guard that answers "can this run here?". The page checks the guard. The server route imports the same module. The code that actually hashes a file exists once.&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="c1"&gt;// shared/utils/crypto/hash.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SUBTLE_NAMES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;sha1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sha384&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-384&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sha512&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-512&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canHashInBrowser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HashAlgorithm&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;algorithm&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;SUBTLE_NAMES&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// simplified page logic&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="nf"&gt;canHashInBrowser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;hashInBrowser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;$fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/hash&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;algorithm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hashing, data format conversion, code formatting and semver all use this shape now. When a new tool needs both paths, it gets the same treatment, and I don't have to think about it anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the server is actually for
&lt;/h2&gt;

&lt;p&gt;The tools that do need a server run on &lt;a href="https://bun.sh" rel="noopener noreferrer"&gt;Bun&lt;/a&gt;. Each one is there because of one of the four reasons above, and I can tell you which:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The TLS Certificate Inspector opens a socket and reads the chain, the SANs and the cipher suite.&lt;/li&gt;
&lt;li&gt;DNS Lookup and the Email Health Inspector query A, MX, TXT, SPF, DKIM and DMARC records.&lt;/li&gt;
&lt;li&gt;The HTTP Inspector, OpenGraph Previewer and RDAP Lookup all fetch from a third-party host.&lt;/li&gt;
&lt;li&gt;Image Studio and the Favicon Set Generator use &lt;code&gt;Bun.Image&lt;/code&gt; for AVIF and ICO output.&lt;/li&gt;
&lt;li&gt;The TS/JSX Transpiler and the AST Playground run OXC, which is native code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because every server route is a liability, they all go through the same checklist before they ship. Validate the URL and allow only &lt;code&gt;http&lt;/code&gt; and &lt;code&gt;https&lt;/code&gt;. Block localhost, private ranges, link-local, cloud metadata hosts, and the IPv6 transition ranges that smuggle an IPv4 address inside (I learned about NAT64 and Teredo the hard way while writing that blocklist). Restrict the destination port, so the TLS inspector can't be turned into a port scanner. Check &lt;code&gt;content-length&lt;/code&gt;, then stream the body and stop at a cap instead of buffering it. Rate-limit by a client key that isn't derived from &lt;code&gt;x-forwarded-for&lt;/code&gt;, since anyone can set that header. Keep uploads in memory and never touch the disk. And store no input and no output, period.&lt;/p&gt;

&lt;p&gt;Analytics follows the same idea. An event carries the tool id and nothing else. I can see that someone opened the cron visualizer today. I have no idea what expression they typed, and I like it that way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff7st5wgc37gtusfgltfv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff7st5wgc37gtusfgltfv.png" alt="SQLite Studio with a 7.5 MB database of 55,991 companies open in the tab. The query ran in 4 ms, and the file never left the browser." width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the rule visible
&lt;/h2&gt;

&lt;p&gt;A rule that only I know about isn't a privacy feature. It's a promise. So the sidebar shows where each tool runs.&lt;/p&gt;

&lt;p&gt;Every tool has a small badge next to its name. 🔒 Client means nothing on that page calls the server. ⚡ Bun means every action does. A tool with both paths shows no badge, and the page itself explains which action leaves the browser. The badge isn't something I type by hand, either. It comes from two flags in the tool registry, the same registry that generates the page title and the search index, so it can't drift out of sync with the code.&lt;/p&gt;

&lt;p&gt;And the code is public. KitDev Space is open source under the MIT license, and the repository is linked from the site footer. If a badge says Client, you don't have to trust the badge. You can open the tool's module and see for yourself that nothing in it calls a server. That was the last piece of the "don't take my word for it" idea, and honestly the part I should have done first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdd1bszl2vy1pqhs1pd0i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdd1bszl2vy1pqhs1pd0i.png" alt=" " width="576" height="1688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in there
&lt;/h2&gt;

&lt;p&gt;New tools land most weeks, so I won't give you a count. Here are the ones I open most often, by category.&lt;/p&gt;

&lt;p&gt;Data: a JSON formatter that accepts JSON5 and JSONC, JSON to TypeScript, converters between YAML, TOML, XML and CSV (including CSV to SQL inserts), a table viewer for large CSVs, and the SQLite studio.&lt;/p&gt;

&lt;p&gt;Dev: a regex tester that explains each token, a cron visualizer that shows the next runs, cURL to fetch/Axios/Python/Go, HTML and SVG to JSX or Vue, a glob tester, and the tar explorer.&lt;/p&gt;

&lt;p&gt;Network: TLS inspector, DNS lookup, email health check, HTTP header inspector, CIDR calculator, cookie inspector, RDAP lookup.&lt;/p&gt;

&lt;p&gt;Crypto: hash generator, HMAC, JWT debugger, TOTP generator, AES encrypt and decrypt, UUID and passphrase generators.&lt;/p&gt;

&lt;p&gt;Color: an OKLCH converter, a WCAG contrast checker, a Tailwind shade generator, a CSS gradient studio, and palette extraction from an image.&lt;/p&gt;

&lt;p&gt;Image: crop, resize, rotate and convert in one pass, an EXIF inspector and remover, SVG to PNG or WebP at 1x/2x/4x, and a favicon set generator that hands you a ZIP.&lt;/p&gt;

&lt;p&gt;There's no account, no paywall, and the whole thing is open source. The site follows your system color scheme, so you won't have to hunt for a toggle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;p&gt;Nuxt 4 and Nuxt UI 4 on the front end, VueUse for composables, CodeMirror for the editors, Bun for the server routes, Vitest and Playwright for tests, deployed on Vercel. Nothing exotic.&lt;/p&gt;

&lt;h2&gt;
  
  
  I'd like your help
&lt;/h2&gt;

&lt;p&gt;If you try &lt;a href="https://kitdev.space" rel="noopener noreferrer"&gt;kitdev.space&lt;/a&gt;, I have two requests. Tell me which tool you wanted and couldn't find. And if you ever catch a tool sending something to the server that should have stayed in your browser, tell me that first. It would mean the rule has a hole, and I'd want to know.&lt;/p&gt;

&lt;p&gt;The repository is one click away from the footer. Issues and pull requests are welcome, whether it's a new tool, a fix, or just a note that a badge is wrong. A tool hub like this gets better with more hands on it, which is the main reason I opened the source.&lt;/p&gt;

&lt;p&gt;If you build tools of your own, take the rule with you. Write down the handful of cases where the browser truly can't do the work. Everything else belongs on the user's machine.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>privacy</category>
      <category>bunjs</category>
    </item>
  </channel>
</rss>
