<?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: Bright Asare Bediako</title>
    <description>The latest articles on DEV Community by Bright Asare Bediako (@bright_asarebediako_a70d).</description>
    <link>https://dev.to/bright_asarebediako_a70d</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%2F4070217%2Fca6f79d4-d531-43e9-912c-7f8bb404f7fa.png</url>
      <title>DEV Community: Bright Asare Bediako</title>
      <link>https://dev.to/bright_asarebediako_a70d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bright_asarebediako_a70d"/>
    <language>en</language>
    <item>
      <title>I Built an AI That Spins Up a Full Dropshipping Store in 60 Seconds. Here's the Architecture.</title>
      <dc:creator>Bright Asare Bediako</dc:creator>
      <pubDate>Mon, 24 Aug 2026 19:01:39 +0000</pubDate>
      <link>https://dev.to/bright_asarebediako_a70d/i-built-an-ai-that-spins-up-a-full-dropshipping-store-in-60-seconds-heres-the-architecture-37o8</link>
      <guid>https://dev.to/bright_asarebediako_a70d/i-built-an-ai-that-spins-up-a-full-dropshipping-store-in-60-seconds-heres-the-architecture-37o8</guid>
      <description>&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%2Fa2bv57iqbskwx3wnzq2n.jpeg" 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%2Fa2bv57iqbskwx3wnzq2n.jpeg" alt=" " width="800" height="348"&gt;&lt;/a&gt;Most dropshipping "tools" import products. That's about 5% of the actual work. The other 95%, writing descriptions, setting prices, wiring suppliers, answering "where's my order," making ads, is the boring part nobody talks about, and it's exactly the part a machine should do.&lt;/p&gt;

&lt;p&gt;So I built one that does. You type one sentence ("a store for minimalist desk gadgets"), and about 60 seconds later you have a live storefront with products, prices, copy, a logo, and a working checkout. This post is the honest engineering breakdown, including the parts that broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint that shaped everything: zero budget
&lt;/h2&gt;

&lt;p&gt;I built this solo with no runway, which forced a rule: every dependency has to have a real free tier or it doesn't ship. That single constraint dictated the whole architecture.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI inference:&lt;/strong&gt; Cloudflare Workers AI (Llama 3.3 70B) as the primary, with a fallback chain (Gemini, then others) behind it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting:&lt;/strong&gt; a single small VM behind Cloudflare, nginx to Node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data:&lt;/strong&gt; Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments:&lt;/strong&gt; Stripe (only costs money when you make money).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interesting problems weren't "how do I call an LLM." They were "how do I make an LLM-driven store reliable enough to take real payments on a free tier."&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: free AI providers rot
&lt;/h2&gt;

&lt;p&gt;Free model endpoints change constantly. My fallback chain quietly decayed until one day the logs said &lt;code&gt;All AI providers exhausted&lt;/code&gt;. Groq's model id had been retired, Gemini's &lt;code&gt;gemini-2.0-flash&lt;/code&gt; was gone, and OpenRouter had killed its free tier, all at once.&lt;/p&gt;

&lt;p&gt;Two lessons that now shape the code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never hardcode a single model.&lt;/strong&gt; Every provider is a function behind a common interface, and the id is env-configurable so a rotation is a config change, not a redeploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The response shape drifts too.&lt;/strong&gt; One provider moved from &lt;code&gt;result.response&lt;/code&gt; to OpenAI-style &lt;code&gt;result.choices[0].message.content&lt;/code&gt; overnight. The parser now accepts both. One &lt;code&gt;??&lt;/code&gt; saved the entire engine.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Problem 2: the store has to run itself
&lt;/h2&gt;

&lt;p&gt;A "store builder" that stops the moment you close the tab isn't automation, it's a form. The features that make it actually autonomous were the fun ones to build:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A self-optimising storefront.&lt;/strong&gt; The store A/B-tests its own product titles and prices, and &lt;em&gt;keeps the winner automatically&lt;/em&gt;. The brain is a plain two-proportion z-test over impressions and conversions: at 95% confidence (or after a two-week ceiling), it applies the winning variant to the live product and messages the owner "Variant B won, +23% conversion, applied for you." No dashboard babysitting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;twoPropZ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c2&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n2&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;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;c2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;n2&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;se&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n2&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;se&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;se&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;strong&gt;Order-aware AI support.&lt;/strong&gt; Every storefront has a chat widget, but the trick is grounding. When a shopper asks "where's my order," the model gets that customer's real order + tracking injected into the prompt, so it answers with the actual status instead of "a team member will follow up." Grounding beats a bigger model almost every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-generated ad kits.&lt;/strong&gt; Every product a merchant adds gets a ready-to-post ad: hook, caption, hashtags, a voiceover script, and a 9:16 card rendered server-side as an SVG (no ffmpeg, no render farm, no cost).&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: taking money on autopilot without leaking it
&lt;/h2&gt;

&lt;p&gt;This is where "let AI handle it" gets dangerous. Discounts, gift cards, and loyalty points were all being trusted from the browser, which means a technical customer could POST any amount and get free product. The fix is boring and non-negotiable: &lt;strong&gt;the server re-derives every discount from its own data at checkout&lt;/strong&gt;, and never trusts a number the client sends. Flash sales, bundles, and geo-pricing are all resolved server-side too. If you build commerce with an LLM in the loop, assume the client is hostile, because eventually one is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone starting this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A free tier is an architecture, not a discount.&lt;/strong&gt; Design for provider rot and you get resilience for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grounding &amp;gt; model size.&lt;/strong&gt; Feed the model real data and a small free model is genuinely useful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 95% you automate is the boring 95%.&lt;/strong&gt; That's the whole value.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to poke at the thing itself, it's live and free to start at &lt;a href="https://nivroo.com" rel="noopener noreferrer"&gt;Nivroo&lt;/a&gt; — describe a store and watch it build one. And if you're building AI products on a shoestring, I'm documenting the whole thing in public, so say hi.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What would you have architected differently? I'd genuinely like to know.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>sass</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>I wrote 111 tests for my security extension. They caught none of the real bugs.</title>
      <dc:creator>Bright Asare Bediako</dc:creator>
      <pubDate>Wed, 19 Aug 2026 19:17:46 +0000</pubDate>
      <link>https://dev.to/bright_asarebediako_a70d/i-wrote-111-tests-for-my-security-extension-they-caught-none-of-the-real-bugs-4ei9</link>
      <guid>https://dev.to/bright_asarebediako_a70d/i-wrote-111-tests-for-my-security-extension-they-caught-none-of-the-real-bugs-4ei9</guid>
      <description>&lt;p&gt;I built a browser extension called QuickAudit. It runs ten OWASP-style checks against whatever page you're on (HSTS, CSP quality, cookie flags, mixed content, exposed &lt;code&gt;.env&lt;/code&gt; files, JavaScript libraries with known CVEs) and gives you a pass/fail sheet in about 200ms.&lt;/p&gt;

&lt;p&gt;By the time I thought it was finished it had 111 passing tests: unit tests over the check functions, integration tests against local fixture servers, cross-browser compatibility tests, and extension-wiring tests run twice under two different API namespaces.&lt;/p&gt;

&lt;p&gt;Then I started actually &lt;em&gt;looking&lt;/em&gt; at it, and found four real bugs.&lt;/p&gt;

&lt;p&gt;The test suite caught none of them. Not one. And the interesting part isn't that I under-tested. It's that each bug needed a completely different kind of looking to find, and no amount of the previous kind would have surfaced the next.&lt;/p&gt;

&lt;p&gt;Here they are, worst first.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. I audited Cloudflare's challenge page and called it your website
&lt;/h2&gt;

&lt;p&gt;I pointed the tool at twenty real sites as a validation exercise. It reported that &lt;strong&gt;sourceforge.net has no HSTS header&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Sourceforge has HSTS. Here's what an independent request returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two clues gave it away. The response came back in 44ms, far too fast for a real page load. And the header list contained this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cf-mitigated: challenge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I hadn't been served sourceforge. I'd been served a Cloudflare bot-protection interstitial, and my scanner faithfully described &lt;em&gt;the challenge page's&lt;/em&gt; headers as though they were the site's.&lt;/p&gt;

&lt;p&gt;This is the failure mode that scares me most, because it fails toward &lt;strong&gt;confident wrongness&lt;/strong&gt;. The tool didn't error. It didn't warn. It produced a clean, specific, professional-looking finding that was entirely false. Take that report to your team and you'd spend an afternoon "fixing" a header that was already there.&lt;/p&gt;

&lt;p&gt;It isn't a bug unique to me either. &lt;em&gt;Any&lt;/em&gt; tool that fetches a URL rather than reading the response your browser actually received inherits this. If you've built something that curls a site and reports on the headers, go and check what it does behind a WAF.&lt;/p&gt;

&lt;p&gt;The fix isn't to guess better. It's to notice you can't see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detectChallenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&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;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cf-mitigated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cloudflare served a bot-protection challenge&lt;/span&gt;&lt;span class="dl"&gt;'&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="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-amzn-waf-action&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;challenge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AWS WAF served a challenge&lt;/span&gt;&lt;span class="dl"&gt;'&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="sr"&gt;/&amp;lt;title&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;Just a moment|Attention Required!|Access denied&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Interstitial challenge page&lt;/span&gt;&lt;span class="dl"&gt;'&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="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/cloudflare|akamai|incapsula|imperva/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&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="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`WAF returned HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; rather than the page`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;When that fires, every header-dependent check now returns &lt;strong&gt;skip&lt;/strong&gt; with an explanation instead of a finding. Three of my twenty corpus sites turned out to be unassessable this way. Reporting "I couldn't see your site" is a result. Reporting a confident lie is not.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The main button locked up forever, and 111 tests were fine with it
&lt;/h2&gt;

&lt;p&gt;Here's the code. See if you spot it faster than I did:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;btn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&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;scan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;btn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;btn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Scanning...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&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;idle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;p&amp;gt;Running ten checks...&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;-- here&lt;/span&gt;

  &lt;span class="k"&gt;try&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;report&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;sendMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;btn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;btn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Rescan&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;#idle&lt;/code&gt; is a placeholder node inside the results container. And &lt;code&gt;render()&lt;/code&gt; clears that container.&lt;/p&gt;

&lt;p&gt;So a first scan works fine. But the actual user sequence is: open the popup, see the report from last time, click &lt;strong&gt;Rescan&lt;/strong&gt;. By then &lt;code&gt;#idle&lt;/code&gt; no longer exists, &lt;code&gt;$('idle')&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt;, and the dereference throws &lt;strong&gt;before&lt;/strong&gt; the &lt;code&gt;try&lt;/code&gt; block. The &lt;code&gt;finally&lt;/code&gt; never runs. The button stays disabled, reading "Scanning...", forever. The primary control of the extension, dead, in a completely ordinary flow.&lt;/p&gt;

&lt;p&gt;Every one of my tests stubbed the browser away. None of them rendered a DOM. So I built a small harness that serves the real popup with the extension APIs faked and a real scan report loaded, opened it in an actual browser, and clicked the button twice.&lt;/p&gt;

&lt;p&gt;That's the whole technique. Two clicks.&lt;/p&gt;

&lt;p&gt;The lesson: &lt;strong&gt;if your tests mock the environment, they cannot test the environment.&lt;/strong&gt; Something has to run the real thing, even crudely.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. A permission that could never work, hidden by a fallback that did
&lt;/h2&gt;

&lt;p&gt;The extension requested &lt;code&gt;webRequest&lt;/code&gt; to read response headers from the real navigation. Nothing appeared broken. It worked.&lt;/p&gt;

&lt;p&gt;But Chrome showed a red &lt;strong&gt;"Errors"&lt;/strong&gt; badge on the extension card. I attached a debugger to the live service worker and got this, verbatim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;You need to request host permissions in the manifest file in order to be notified about requests from the webRequest API.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Chrome only delivers &lt;code&gt;webRequest&lt;/code&gt; events to extensions holding host permissions &lt;strong&gt;at install time&lt;/strong&gt;. QuickAudit deliberately asks for one origin at a time, when you click Scan. That's the entire privacy pitch. The two are fundamentally incompatible.&lt;/p&gt;

&lt;p&gt;So the listener registered, never fired once, and logged a permanent error for every user.&lt;/p&gt;

&lt;p&gt;It kept working because I'd written a &lt;code&gt;fetch&lt;/code&gt; fallback for the case where the header cache was empty, and the cache was &lt;em&gt;always&lt;/em&gt; empty. The fallback wasn't a fallback. It was the whole implementation, and it had been quietly carrying the feature the entire time.&lt;/p&gt;

&lt;p&gt;The tempting fix is to widen permissions to &lt;code&gt;&amp;lt;all_urls&amp;gt;&lt;/code&gt;. I deleted the permission instead. A tool whose selling point is "I don't ask for access to every site you visit" doesn't get to ask for access to every site you visit in exchange for marginally more faithful headers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A feature that works is not proof that its implementation works.&lt;/strong&gt; Sometimes it means your fallback is better than you knew.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The paid feature was invisible on half of all machines
&lt;/h2&gt;

&lt;p&gt;QuickAudit has a Pro tier whose main artifact is an exported HTML report you'd hand to a client. Its stylesheet looked like this:&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="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--fg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#16181d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--muted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#6b7280&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--fg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c"&gt;/* no background */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dark text. No background declared. So in any browser set to dark mode, the page painted the browser's dark default and rendered &lt;strong&gt;near-black text on near-black&lt;/strong&gt;. Every check title and every summary line: invisible. The one thing people would pay for, unreadable for anyone not on a light theme.&lt;/p&gt;

&lt;p&gt;I found it while composing a screenshot for the store listing.&lt;/p&gt;

&lt;p&gt;The fix is one line, but the reasoning matters more than the line. This is a document you print and send to someone, so it shouldn't follow the &lt;em&gt;reader's&lt;/em&gt; theme at all:&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="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;color-scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;light&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fff&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;
  
  
  What I'd actually take from this
&lt;/h2&gt;

&lt;p&gt;Four bugs, four different kinds of looking:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bug&lt;/th&gt;
&lt;th&gt;Found by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WAF challenge audited as the site&lt;/td&gt;
&lt;td&gt;running against 20 real sites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dead Rescan button&lt;/td&gt;
&lt;td&gt;rendering the real UI and clicking twice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Impossible &lt;code&gt;webRequest&lt;/code&gt; permission&lt;/td&gt;
&lt;td&gt;attaching a debugger to the live service worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invisible Pro report&lt;/td&gt;
&lt;td&gt;making a screenshot&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None by the test suite. And the suite isn't bad. It catches regressions in all four now. It structurally could not have found them the first time, because each one lived somewhere the tests had abstracted away.&lt;/p&gt;

&lt;p&gt;Oh, and one more, for humility: my &lt;em&gt;compatibility test&lt;/em&gt; had a comment-stripper that treated the &lt;code&gt;/*&lt;/code&gt; and &lt;code&gt;*/&lt;/code&gt; inside the string &lt;code&gt;'http://*/*'&lt;/code&gt; as a comment and ate the surrounding code. The tool that checks the tool needed checking too.&lt;/p&gt;

&lt;p&gt;If you're building anything that renders a verdict about someone else's system: the corpus isn't optional, and &lt;strong&gt;the cases you can't assess are a result worth reporting, not an error to swallow&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;QuickAudit is free and open source. Ten checks, no account, and the only thing that leaves your browser is library &lt;code&gt;name@version&lt;/code&gt; strings sent to OSV.dev to look up CVEs. No URLs, no page content, no cookie values.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source + the full 20-site accuracy write-up:&lt;/strong&gt; &lt;a href="https://github.com/BAB78/quickaudit" rel="noopener noreferrer"&gt;https://github.com/BAB78/quickaudit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chrome / Edge:&lt;/strong&gt; &lt;a href="https://chromewebstore.google.com/detail/kbhfdiianifmneefgfmlmloejffekjdm" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/kbhfdiianifmneefgfmlmloejffekjdm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firefox:&lt;/strong&gt; &lt;a href="https://addons.mozilla.org/en-US/firefox/addon/quickaudit-web-security/" rel="noopener noreferrer"&gt;https://addons.mozilla.org/en-US/firefox/addon/quickaudit-web-security/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy to have any of the ten checks torn apart. That's most of why I'm posting.&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%2Fzbvmziirq5hcq3dyvb6j.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%2Fzbvmziirq5hcq3dyvb6j.png" alt=" " width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
