<?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>Forem: V G P</title>
    <description>The latest articles on Forem by V G P (@mrtinkz).</description>
    <link>https://forem.com/mrtinkz</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3926115%2Fa2f0ea8b-e027-4057-bd8a-40b32977e263.png</url>
      <title>Forem: V G P</title>
      <link>https://forem.com/mrtinkz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/mrtinkz"/>
    <language>en</language>
    <item>
      <title>We Keep Building Bigger Walls Around the Wrong Thing</title>
      <dc:creator>V G P</dc:creator>
      <pubDate>Fri, 15 May 2026 22:34:14 +0000</pubDate>
      <link>https://forem.com/mrtinkz/we-keep-building-bigger-walls-around-the-wrong-thing-545b</link>
      <guid>https://forem.com/mrtinkz/we-keep-building-bigger-walls-around-the-wrong-thing-545b</guid>
      <description>&lt;p&gt;There's a species of tree in dense forests that grows toward whatever gap in the canopy lets light through. It doesn't ask permission. It doesn't register with the other trees. It just moves toward what it needs, and the forest works because of that — not in spite of it.&lt;/p&gt;

&lt;p&gt;We don't build software that way anymore.&lt;/p&gt;




&lt;p&gt;At some point we decided that before a user can do anything — read a note, save a preference, pick up where they left off — they need to announce themselves to a server, wait for a response, get issued a token, and carry that token everywhere like a passport in a country that checks papers at every door.&lt;/p&gt;

&lt;p&gt;And when that system gets too heavy, we don't question the system. We add another layer to it.&lt;/p&gt;

&lt;p&gt;There are entire job functions dedicated to managing this. Entire platforms — some charging by the login — whose whole purpose is to sit between your user and your app and say: &lt;em&gt;prove yourself first&lt;/em&gt;. The infrastructure behind a simple "remember my theme preference" can span multiple availability zones, three third-party vendors, and a compliance audit.&lt;/p&gt;

&lt;p&gt;Nobody stops to ask whether this was the only way.&lt;/p&gt;




&lt;p&gt;I'm not arguing against security. I'm asking about proportion.&lt;/p&gt;

&lt;p&gt;A river doesn't move water by building a bigger pump. It follows the path of least resistance, deposits what it carries at the delta, and keeps moving. The water arrives. Nothing registers. Nothing authenticates. The delta doesn't care where the water came from — it cares what the water does when it gets there.&lt;/p&gt;

&lt;p&gt;The session token model is the opposite of this. It says: water cannot flow until we verify it's water, issue it a credential, log that it arrived, and stand ready to revoke its water-ness at any time. Then we wonder why everything feels sluggish.&lt;/p&gt;




&lt;p&gt;What I've been working on with &lt;a href="https://www.npmjs.com/package/@mrtinkz/tessera" rel="noopener noreferrer"&gt;tessera&lt;/a&gt; starts from a different assumption.&lt;/p&gt;

&lt;p&gt;The user already has the device. The device already has the browser. The browser already has a crypto engine sitting idle — AES-256-GCM, PBKDF2, Web Crypto — powerful enough to handle what most apps actually need to protect. The passcode stays local. The key never leaves the device. The server never enters the picture.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Tessera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abc123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preferences&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prefs&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No round-trip. No token. No registration. The user's data is encrypted with their own passcode, in their own browser, and only they can read it.&lt;/p&gt;

&lt;p&gt;That's not a compromise. For a significant class of problems, that's strictly better — because there's no central store to breach, no credential database to leak, no session to hijack in transit.&lt;/p&gt;




&lt;p&gt;There's a concept in structural design called the minimum viable structure — the least amount of material you can use while still holding the load. Bridges built this way are elegant. They carry weight precisely because they don't carry anything extra. Every beam earns its place.&lt;/p&gt;

&lt;p&gt;Most auth infrastructure fails this test badly. It carries weight that exists to justify itself: the logging layer that feeds the dashboard nobody checks, the token refresh cycle that prevents a session timeout nobody would have noticed, the password complexity rules enforced by a system that hashes the password anyway.&lt;/p&gt;

&lt;p&gt;tessera tries to be the minimum viable structure for what it actually does. Encrypt the data. Derive the key locally. Let the value expire when it should. Notice when something looks wrong.&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="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;one_time_code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxReads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&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;That's it. The code is gone after one read. No server involved in that transaction. No revocation request. It just ceases to exist.&lt;/p&gt;




&lt;p&gt;The immune system doesn't keep a registry of every pathogen it's ever seen stored on a central server somewhere. It carries the memory in the cells themselves — distributed, local, present at the point of contact. When something unfamiliar shows up, the response comes from within, not from a round-trip to headquarters.&lt;/p&gt;

&lt;p&gt;tessera does something similar with honey keys — decoys planted inside storage that look identical to real entries. Real code never touches them. Anything enumerating storage and guessing will.&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="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;honey-hit&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="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// something is probing your storage&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detection lives where the data lives. No external service needed.&lt;/p&gt;




&lt;p&gt;I'm not trying to kill authentication. There are things that genuinely require a server, a verified identity, a shared secret negotiated between two parties. I'm not pretending otherwise.&lt;/p&gt;

&lt;p&gt;But I think we've spent so long inside one way of thinking about identity and access that we've stopped noticing the assumptions embedded in it. That every interaction needs a server to bless it. That local state is inherently less trustworthy than remote state. That the right response to a security problem is always more infrastructure.&lt;/p&gt;

&lt;p&gt;Some problems don't need a bigger wall. They need a different shape entirely.&lt;/p&gt;




&lt;p&gt;tessera is small. It's a start. But it's built on the premise that the device in your user's hand is already powerful enough to protect their data — if you give it the right tools and get out of the way.&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; @mrtinkz/tessera
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/mrtinkz/tessera" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — I'd genuinely like to know where you think this line of thinking breaks down.&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>@mrtinkz/tessera v0.1.2 — I Wasn't Done Yet</title>
      <dc:creator>V G P</dc:creator>
      <pubDate>Fri, 15 May 2026 22:19:49 +0000</pubDate>
      <link>https://forem.com/mrtinkz/tessera-v011-i-wasnt-done-yet-47he</link>
      <guid>https://forem.com/mrtinkz/tessera-v011-i-wasnt-done-yet-47he</guid>
      <description>&lt;p&gt;After shipping v0.1.0 I did what most developers do after a release — I opened my own app and started poking around.&lt;/p&gt;

&lt;p&gt;The values were ciphertext. Good. But the keys were sitting right there in plain English. &lt;code&gt;auth_state&lt;/code&gt;. &lt;code&gt;cart_items&lt;/code&gt;. &lt;code&gt;pending_payment&lt;/code&gt;. Anyone who opened DevTools knew exactly what I was keeping track of, even if they couldn't read the contents. That shouldn't have bothered me as much as it did. But I couldn't let it go.&lt;/p&gt;

&lt;p&gt;So I kept going.&lt;/p&gt;




&lt;h2&gt;
  
  
  Your keys now mean nothing to anyone but you
&lt;/h2&gt;

&lt;p&gt;tessera now runs every key name through HMAC-SHA-256 before it touches storage. What you call &lt;code&gt;cart_items&lt;/code&gt;, tessera stores as &lt;code&gt;t_3a9f7c2e&lt;/code&gt;. Close DevTools, reopen it, and all you see is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t_3a9f7c2e  →  &amp;lt;ciphertext&amp;gt;
t_b2d4f110  →  &amp;lt;ciphertext&amp;gt;
t_03e8a5cc  →  &amp;lt;ciphertext&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mapping only exists in memory, derived from your passcode. Lock the vault — it's gone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Some of those entries are fake
&lt;/h2&gt;

&lt;p&gt;Here's the thing I'm most pleased with: not all of those entries are real. tessera automatically plants &lt;strong&gt;honey keys&lt;/strong&gt; — decoys that look exactly like real values. Same key format, same ciphertext format. Completely indistinguishable.&lt;/p&gt;

&lt;p&gt;Real code never touches them. Only something enumerating your storage and guessing would. That's the tripwire.&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="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;honey-hit&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="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// something is probing your storage&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Values that delete themselves
&lt;/h2&gt;

&lt;p&gt;Some data shouldn't outlive its purpose. A one-time code. A recovery token. A payment session. v0.1.2 lets values carry their own expiry:&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="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;one_time_code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// gone after 30 seconds&lt;/span&gt;
  &lt;span class="na"&gt;maxReads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// gone after first read&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no background timer running. The check happens at read time — the moment something requests the value, tessera looks at the write timestamp and acts. If it's expired, it wipes before returning anything. A timer can be cleared by an attacker. A check on read cannot.&lt;/p&gt;

&lt;p&gt;Don't want to configure every key manually? Sensitivity presets have you covered:&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="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recovery_code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sensitivity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// 5 minute TTL, 3 max reads, wiped at first sign of trouble&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  It notices things that shouldn't be happening
&lt;/h2&gt;

&lt;p&gt;The last thing I added was a suspicion engine. If reads start coming in faster than any human could trigger them, tessera notices. If an HMAC check fails on a read — meaning the value was touched outside the API — tessera notices that too.&lt;/p&gt;

&lt;p&gt;You decide what happens:&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="nx"&gt;Tessera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;123456&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;suspicion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;callsPerSecond&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;onSuspicion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lock&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lock&lt;/code&gt;, &lt;code&gt;wipe&lt;/code&gt;, or &lt;code&gt;throw&lt;/code&gt;. tessera just makes sure &lt;em&gt;something&lt;/em&gt; happens.&lt;/p&gt;




&lt;p&gt;The encryption in v0.1.0 was the obvious part. v0.1.1 is all the stuff I couldn't stop thinking about after — the layers that make it hard to learn anything useful from your storage even when someone already has full read access.&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; @mrtinkz/tessera
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/mrtinkz/tessera" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — feedback always welcome.&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>I Built a Zero-Dependency Browser Storage Encryption Library — Here's Why</title>
      <dc:creator>V G P</dc:creator>
      <pubDate>Thu, 14 May 2026 05:21:55 +0000</pubDate>
      <link>https://forem.com/mrtinkz/i-built-a-zero-dependency-browser-storage-encryption-library-heres-why-444l</link>
      <guid>https://forem.com/mrtinkz/i-built-a-zero-dependency-browser-storage-encryption-library-heres-why-444l</guid>
      <description>&lt;p&gt;A few months ago I found myself auditing a side project and noticed something uncomfortable: I was storing sensitive user preferences, cart data, and session tokens in &lt;code&gt;localStorage&lt;/code&gt; — completely in plaintext. Anyone with DevTools open could read it in two seconds.&lt;/p&gt;

&lt;p&gt;The obvious fix is "just encrypt it." But when I went looking for a library that actually did this well, I kept running into the same problems: heavy dependencies, weak key derivation, or APIs that felt bolted on as an afterthought. So I built &lt;a href="https://www.npmjs.com/package/@mrtinkz/tessera" rel="noopener noreferrer"&gt;tessera&lt;/a&gt;.&lt;/p&gt;




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

&lt;p&gt;One passcode. All your browser storage — &lt;code&gt;localStorage&lt;/code&gt;, &lt;code&gt;sessionStorage&lt;/code&gt;, &lt;code&gt;IndexedDB&lt;/code&gt;, and &lt;code&gt;cookies&lt;/code&gt; — encrypted with AES-256-GCM. The key is derived from PBKDF2-SHA-256 at ≥ 310,000 iterations (the OWASP 2024 minimum), and it never leaves the Web Crypto engine as raw bytes.&lt;/p&gt;

&lt;p&gt;The API is a drop-in replacement for the storage APIs you already use:&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;Tessera&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;@mrtinkz/tessera&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;vault&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Tessera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abc123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cartData&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;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// plaintext back&lt;/span&gt;
&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// zeroes the in-memory key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No server round-trips. No cloud keys. No dependencies.&lt;/p&gt;




&lt;h2&gt;
  
  
  The threat model I was actually designing against
&lt;/h2&gt;

&lt;p&gt;Most encryption libraries stop at "we encrypt the data." tessera is built against the &lt;a href="https://owasp.org/www-community/threats/" rel="noopener noreferrer"&gt;OWASP browser storage threat model&lt;/a&gt;, so let me be specific about what it protects and what it doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it protects against:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Passive observer in DevTools&lt;/strong&gt; — storage values are ciphertext. Useless without the key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XSS reading storage&lt;/strong&gt; — same deal. The attacker gets ciphertext.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline brute force&lt;/strong&gt; — PBKDF2 at 310k iterations costs roughly 1 second per attempt on modern hardware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key exfiltration via heap dump&lt;/strong&gt; — &lt;code&gt;extractable: false&lt;/code&gt; means the raw key bytes never exist in JavaScript memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-device brute force&lt;/strong&gt; — configurable lockout: wipe all storage, apply exponential backoff, or throw immediately after N failed attempts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What it doesn't protect against:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;tessera protects your data at rest — when the vault is locked, everything in storage is ciphertext. Unlocking doesn't decrypt everything at once; it just derives the key and holds it in memory. Individual values only decrypt on demand when you call &lt;code&gt;getItem&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The one scenario where this breaks down is if your page already has an XSS vulnerability. An attacker running code on your page has the same access you do — they can call &lt;code&gt;vault.local.getItem()&lt;/code&gt; while the vault is unlocked and get plaintext back, one value at a time. They can't steal the raw key bytes (&lt;code&gt;extractable: false&lt;/code&gt; blocks that), but they don't need to. Fix XSS first; tessera handles the rest. The &lt;a href="https://github.com/mrtinkz/tessera/blob/main/docs/threat-model.md" rel="noopener noreferrer"&gt;threat model docs&lt;/a&gt; go deeper on this.&lt;/p&gt;




&lt;h2&gt;
  
  
  The crypto internals
&lt;/h2&gt;

&lt;p&gt;Each stored value gets its own salt and IV. The stored format is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;salt(16) ‖ iv(12) ‖ ciphertext ‖ tag(16)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The vault salt lives in &lt;code&gt;localStorage&lt;/code&gt; so the same passcode re-derives the same key across sessions — you unlock once per session, not once per page load.&lt;/p&gt;

&lt;p&gt;Key derivation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PBKDF2(passcode, vaultSalt, 310_000 iterations, SHA-256) → AES-256-GCM key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;CryptoKey&lt;/code&gt; is created with &lt;code&gt;extractable: false&lt;/code&gt;. The Web Crypto engine holds the key material; your JavaScript never sees the raw bytes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The PIN pad problem
&lt;/h2&gt;

&lt;p&gt;I wanted to mitigate keyloggers and click-sequence recording. The naive approach — an HTML grid of buttons with digit labels — fails because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keyloggers read &lt;code&gt;keydown&lt;/code&gt; events&lt;/li&gt;
&lt;li&gt;Click-sequence recording reads which DOM element was clicked&lt;/li&gt;
&lt;li&gt;The digit labels on buttons reveal the sequence&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;tessera ships a Canvas-based PIN pad. Digit positions are randomised on every render. No DOM element carries a digit value. A click recorder sees coordinates, not digits.&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;renderPinPad&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;@mrtinkz/tessera&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;renderPinPad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pin&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;onUnlock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;passcode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;vault&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Tessera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;passcode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;randomize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&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;You can style it with CSS custom properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.tessera-pin-pad&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--tessera-pad-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1a1a2e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--tessera-btn-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#16213e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--tessera-btn-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e2e8f0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--tessera-btn-hover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0f3460&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--tessera-btn-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;64px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--tessera-indicator-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#4ade80&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;h2&gt;
  
  
  Framework support
&lt;/h2&gt;

&lt;p&gt;tessera ships ESM, CJS, and IIFE builds. There are native adapters for React, Vue 3, Svelte, and Angular so you get a hook/store/service rather than managing vault state yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;React:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useTessera&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;@mrtinkz/tessera/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SecureApp&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLocked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTessera&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;idleTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="nx"&gt;_000&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;isLocked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PinPad&lt;/span&gt; &lt;span class="na"&gt;onUnlock&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;unlock&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Dashboard&lt;/span&gt; &lt;span class="na"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onLock&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lock&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Vue 3:&lt;/strong&gt;&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="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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useTessera&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;@mrtinkz/tessera/vue&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLocked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTessera&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;idleTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="nx"&gt;_000&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Svelte:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tesseraStore&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;@mrtinkz/tessera/svelte&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLocked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tesseraStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;idleTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also an Angular &lt;code&gt;TesseraModule&lt;/code&gt; and &lt;code&gt;TesseraService&lt;/code&gt; if that's your stack.&lt;/p&gt;




&lt;h2&gt;
  
  
  Idle timeout and cross-tab sync
&lt;/h2&gt;

&lt;p&gt;The vault auto-locks after a configurable idle period. When it locks, it broadcasts over &lt;code&gt;BroadcastChannel&lt;/code&gt; so every open tab locks simultaneously. No stale unlocked tabs sitting in the background.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Tessera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abc123&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;idleTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// 15 minutes&lt;/span&gt;
  &lt;span class="na"&gt;lockoutAttempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lockoutAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wipe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// nuclear option&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Install
&lt;/h2&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; @mrtinkz/tessera
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CDN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/@mrtinkz/tessera/dist/index.global.global.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Tessera&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TesseraLib&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;Tessera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abc123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&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;dark&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Browser support: Chrome/Edge 89+, Firefox 86+, Safari 15+. Also works in Deno, Bun, and Cloudflare Workers.&lt;/p&gt;




&lt;p&gt;I'd love feedback — especially from anyone who's thought hard about browser storage security. What's missing? What would you do differently? Drop it in the comments or open an issue on &lt;a href="https://github.com/mrtinkz/tessera" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Docker Desktop Won't Start After a BIOS Update? Check This First</title>
      <dc:creator>V G P</dc:creator>
      <pubDate>Tue, 12 May 2026 02:36:29 +0000</pubDate>
      <link>https://forem.com/mrtinkz/docker-desktop-wont-start-after-a-bios-update-check-this-first-45nj</link>
      <guid>https://forem.com/mrtinkz/docker-desktop-wont-start-after-a-bios-update-check-this-first-45nj</guid>
      <description>&lt;p&gt;I spent way too long troubleshooting this. Reinstalled Docker Desktop multiple times, wiped config folders, checked WSL, verified Hyper-V — nothing worked. Turns out the fix was two PowerShell commands.&lt;/p&gt;

&lt;p&gt;Here's what happened and how to fix it fast if you run into the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Situation
&lt;/h2&gt;

&lt;p&gt;My ThinkPad T480 got a BIOS and firmware update. After rebooting, Docker Desktop stopped launching. No error dialog, no crash message — it just silently died every time. The whale icon would never show up in the system tray.&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;wsl --list&lt;/code&gt; showed no &lt;code&gt;docker-desktop&lt;/code&gt; distro. The logs had nothing useful — just Docker initializing and then abruptly stopping. Looked like a WSL problem, a virtualization problem, or a busted installation. It was none of those.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Was Actually Wrong
&lt;/h2&gt;

&lt;p&gt;The BIOS update disrupted Windows service configurations. The Docker Desktop backend service — &lt;code&gt;com.docker.service&lt;/code&gt; — got flipped to &lt;strong&gt;Manual&lt;/strong&gt; startup, so it was no longer running when Windows booted.&lt;/p&gt;

&lt;p&gt;Docker Desktop's UI process depends entirely on that backend service. If the service isn't running, the UI launches, finds nothing to connect to, and kills itself immediately. No warning, no useful error — just gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Open PowerShell as Administrator and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Start-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;com.docker.service&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;com.docker.service&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;Status: Running&lt;/code&gt;. Then lock it in so it survives future reboots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Set-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;com.docker.service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-StartupType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Automatic&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then launch Docker Desktop normally from the Start Menu (or via PowerShell):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Start-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\Program Files\Docker\Docker\Docker Desktop.exe"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Whale icon appears, engine starts, everything works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Save This for Next Time
&lt;/h2&gt;

&lt;p&gt;If Docker Desktop ever silently dies on you again, run this before doing anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;com.docker.service&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the status is &lt;code&gt;Stopped&lt;/code&gt;, just start it. You'll save yourself an hour of unnecessary reinstalls.&lt;/p&gt;

&lt;p&gt;And if the service is running but Docker still won't start, then check the actual log for a real error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;LOCALAPPDATA&lt;/span&gt;&lt;span class="s2"&gt;\Docker\log\host\com.docker.backend.exe.log"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Tail&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;50&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Pattern&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error|fatal|panic|fail"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-CaseSensitive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="bp"&gt;$false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Tested on ThinkPad T480, Windows 11, WSL2 with Debian. Hope this saves someone the hour I lost.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>windows</category>
      <category>wsl</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
