<?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: SPOE</title>
    <description>The latest articles on DEV Community by SPOE (@spoe).</description>
    <link>https://dev.to/spoe</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%2F4174412%2Fb4ea6155-9191-478a-9a96-08f2992bb44a.png</url>
      <title>DEV Community: SPOE</title>
      <link>https://dev.to/spoe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/spoe"/>
    <language>en</language>
    <item>
      <title>Is AI-generated code safe to ship? What to check, and what a second AI catches</title>
      <dc:creator>SPOE</dc:creator>
      <pubDate>Wed, 14 Oct 2026 22:18:13 +0000</pubDate>
      <link>https://dev.to/spoe/is-ai-generated-code-safe-to-ship-what-to-check-and-what-a-second-ai-catches-2e6k</link>
      <guid>https://dev.to/spoe/is-ai-generated-code-safe-to-ship-what-to-check-and-what-a-second-ai-catches-2e6k</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://spoe.ai/blog/is-ai-generated-code-safe" rel="noopener noreferrer"&gt;the SPOE blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AI-written code usually works. Whether it is safe is a different question, and the answer depends less on the model than on what checks the code before it reaches your users.&lt;/p&gt;

&lt;p&gt;The good news: the mistakes are predictable. The same handful turn up again and again, and most can be caught without you reading a line of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistakes that matter
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A secret in the code.&lt;/strong&gt; An API key, a database password or a payment key pasted straight into a file. Once that file is in a public repository or shipped to a browser, anyone can use the key. This is the most common and the most expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database queries built from text.&lt;/strong&gt; When the code glues what a user typed into a database query instead of passing it separately, a crafted input can read or delete data that is not theirs. The fix is boring and standard: parameterised queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Routes that change data without asking who you are.&lt;/strong&gt; A page or endpoint that deletes a record or changes a price, open to anyone who finds the address.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Raw HTML from users.&lt;/strong&gt; Showing something a visitor typed as HTML lets them run their own script in other people's browsers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cookies accepted from any site.&lt;/strong&gt; A loose setting that lets another website act as your signed-in user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pages search engines cannot read.&lt;/strong&gt; Not a security hole, but an AI-built site often ships without a title, a description or a sitemap, and then nobody finds it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the same AI should not check its own work
&lt;/h2&gt;

&lt;p&gt;Ask the model that wrote the code whether it is safe, and it tends to agree with itself: it shares the blind spots that produced the mistake. A reviewer from a different model family reads the change fresh, with different habits and different training. It is the same reason a writer does not proofread their own book.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;A second AI reviews every change.&lt;/strong&gt; Every change the builder makes is reviewed by a model from a different family before it reaches your project. If the review finds something serious, the change waits for you in a side-by-side diff instead of going in, and one click sends the findings back to be fixed. A review that times out or fails is never treated as a pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check before you ship, free.&lt;/strong&gt; The Check button runs a scan with no AI and no credits: keys in the code, SQL built from text, raw HTML, cookies accepted from any site, routes that change data without sign-in, and what search engines will miss. A critical finding holds a publish until you decide, and that decision is recorded. The same check ships inside every export as &lt;code&gt;check.mjs&lt;/code&gt;, so you can run it on your own machine, or in your own CI, for as long as the app lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets stay out of the code.&lt;/strong&gt; Keys you add go into the project's vault, sealed, and reach your app's server as environment variables. The builder is told the names of your keys, never their values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every change is a version.&lt;/strong&gt; If something slips through, you roll back in one click.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is still your job
&lt;/h2&gt;

&lt;p&gt;No checker finds everything. Before you put real customers' data in an app, built by AI or by people:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click through every page signed out, and try to reach what you should not.&lt;/li&gt;
&lt;li&gt;Try the forms with nonsense, very long text and odd characters.&lt;/li&gt;
&lt;li&gt;Look at what the Check finds and fix the critical ones.&lt;/li&gt;
&lt;li&gt;Keep keys in the vault, never in a message to the builder.&lt;/li&gt;
&lt;li&gt;Have someone technical read the parts that handle money or personal data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;AI makes building fast. Checks are what make it safe to ship. &lt;a href="https://spoe.ai/register" rel="noopener noreferrer"&gt;Start free&lt;/a&gt; and run the Check on your first build, or read &lt;a href="https://spoe.ai/compare" rel="noopener noreferrer"&gt;how SPOE compares&lt;/a&gt; with other builders.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The one test that proves no user can see another user's data</title>
      <dc:creator>SPOE</dc:creator>
      <pubDate>Tue, 13 Oct 2026 04:46:08 +0000</pubDate>
      <link>https://dev.to/spoe/the-one-test-that-proves-no-user-can-see-another-users-data-26fp</link>
      <guid>https://dev.to/spoe/the-one-test-that-proves-no-user-can-see-another-users-data-26fp</guid>
      <description>&lt;p&gt;Every multi-tenant app has the same nightmare bug: a route that forgets to check who owns the thing it returns. One missing &lt;code&gt;where owner_id = ?&lt;/code&gt; and user B can read user A's projects by changing an id in the URL.&lt;/p&gt;

&lt;p&gt;Code review doesn't reliably catch it. Neither do the tests most of us write, because we test the routes we remember, and the leaky one is always the route we forgot.&lt;/p&gt;

&lt;p&gt;So I stopped writing per-route authorisation tests and wrote one test that walks every route the API has.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Collect every route the server registers, automatically.&lt;/li&gt;
&lt;li&gt;Make two accounts. Alice creates real data: a project, a file, a secret, an API key.&lt;/li&gt;
&lt;li&gt;For every route that takes an id, call it as Mallory using Alice's ids.&lt;/li&gt;
&lt;li&gt;Every single one must answer 404 (not 403, which confirms the thing exists). With no session at all, every one must answer 401.&lt;/li&gt;
&lt;li&gt;The only exceptions are routes that show something an owner chose to publish, and each exception is written down with its reason.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A route added next month that forgets the owner check fails this test on day one, without anyone remembering to test it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Collecting the routes
&lt;/h2&gt;

&lt;p&gt;In Fastify there is a hook for exactly this. Register it before your routes:&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;routeList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onRoute&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;r&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="k"&gt;for &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;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;flat&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;m&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HEAD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OPTIONS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;routeList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decorate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;routeList&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;routeList&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Express 4 you can walk &lt;code&gt;app._router.stack&lt;/code&gt;; most frameworks have an equivalent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&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;SHARED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&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;GET /showcase/:id&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;an app its owner shared publicly&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;GET /shared-chats/:slug&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;a chat its owner published with a link&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;withIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/:|&lt;/span&gt;&lt;span class="se"&gt;\*&lt;/span&gt;&lt;span class="sr"&gt;/&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Alice's real ids wherever a route names one of hers.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fill&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;projects&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;:id/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`/projects/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;project&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:gid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;generation&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:name&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;ALICE_KEY&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/:id/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&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;web/secret.ts&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;leaks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="k"&gt;for &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;r&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;withIds&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;SHARED&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="k"&gt;continue&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&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;res&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject&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="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mallory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;plausibleBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;leaks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&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="s2"&gt; as another account: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alice only&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;leaks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&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="s2"&gt; showed Alice's data`&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;anon&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject&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="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;plausibleBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&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;anon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;leaks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&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="s2"&gt; with no session: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;anon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;leaks&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter more than they look.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Send a plausible body.&lt;/strong&gt; If a route validates the body before it checks ownership, an empty body gets a 400 and the test learns nothing about the owner check. Give every request a body that would pass validation, so the 404 you get back is the owner check speaking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collect leaks, then assert once.&lt;/strong&gt; A list of every failing route is far more useful than the first failure. On my codebase the list was empty, which brings me to the part most people skip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove the test can fail
&lt;/h2&gt;

&lt;p&gt;An empty list is only good news if the test can actually catch a leak. So I broke one route on purpose: I removed the owner check from the route that lists a project's secrets, and ran the test again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- "GET /projects/:id/secrets as another account: 200 {"secrets":[{"name":"ALICE_KEY"...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It failed, naming the route and showing the leaked secret names. I put the check back. Now I trust the green.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lists of "mine"
&lt;/h2&gt;

&lt;p&gt;Routes without an id leak too: &lt;code&gt;/me/projects&lt;/code&gt;, &lt;code&gt;/me/exports&lt;/code&gt;, &lt;code&gt;/me/api-keys&lt;/code&gt;. A second test signs in as Mallory and checks that none of those responses contain Alice's ids, email or names anywhere in the JSON.&lt;/p&gt;

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

&lt;p&gt;76 routes with ids, two requests each, about 400 ms with Fastify's &lt;code&gt;inject&lt;/code&gt;. It runs with the rest of the test suite before every commit.&lt;/p&gt;

&lt;p&gt;I built this for &lt;a href="https://spoe.ai" rel="noopener noreferrer"&gt;spoe.ai&lt;/a&gt;, an AI app builder where people prompt full-stack apps and export the code. When an AI writes a lot of your routes, "we'll remember to test authorisation" stops being a plan. A test that walks every route does not need anyone to remember.&lt;/p&gt;

&lt;p&gt;If your app has more than one user, steal the idea. It's an afternoon of work and it closes the most common serious bug in multi-tenant software.&lt;/p&gt;

</description>
      <category>security</category>
      <category>testing</category>
      <category>node</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Every change our AI app builder makes gets a second model’s review. A timeout is never a pass.</title>
      <dc:creator>SPOE</dc:creator>
      <pubDate>Sat, 10 Oct 2026 10:13:58 +0000</pubDate>
      <link>https://dev.to/spoe/every-change-our-ai-app-builder-makes-gets-a-second-models-review-a-timeout-is-never-a-pass-17na</link>
      <guid>https://dev.to/spoe/every-change-our-ai-app-builder-makes-gets-a-second-models-review-a-timeout-is-never-a-pass-17na</guid>
      <description>&lt;p&gt;I'm building SPOE, an AI app builder. You describe an app, a model writes it into a real Next.js, Fastify and Postgres project, and you can export the whole thing. &lt;a href="https://dev.to/spoe/we-sign-every-export-our-ai-app-builder-produces-heres-how-it-works-33ae"&gt;Last time&lt;/a&gt; I wrote about how every export is signed. This post is about the other half: nothing the builder writes reaches your project until a second model has read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a second model, and why a different family
&lt;/h2&gt;

&lt;p&gt;A model reviewing its own output tends to like it. Two sizes of the same model family share training data, so they tend to share blind spots too. So the rule is that the reviewer must come from a different model family than the writer.&lt;/p&gt;

&lt;p&gt;That rule lives in code, not in a doc. The server reads the family off each model's id (Qwen, GLM, Llama, DeepSeek, Mistral and so on). If it can't tell, the operator has to state it. If the writer and the reviewer turn out to be the same family, the server refuses to start:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`Reviewer and generator are both "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" family. The security review must use a different model family.`&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 pick your own model for building, the reviewer is chosen from a different family than that one too. Both run on Venice's private, zero-retention inference.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the reviewer sees
&lt;/h2&gt;

&lt;p&gt;It gets two things: the original request and the proposed diff. Its instructions open with "You are a security reviewer. You did not write this code."&lt;/p&gt;

&lt;p&gt;It looks for, roughly in this order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Critical:&lt;/strong&gt; injection, auth that can be bypassed, secrets in code, arbitrary file access, one user reaching another user's data. Also any import of an SPOE package, because the export has to run without us.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High:&lt;/strong&gt; missing validation at the edges, weak crypto or password storage, SSRF, path traversal, XSS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medium:&lt;/strong&gt; missing rate limits on auth, error messages that leak, insecure defaults.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It answers in JSON at temperature 0: a verdict, and findings that each name a file, a line, the problem and the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't trust the model's own summary
&lt;/h2&gt;

&lt;p&gt;A model will happily list a critical finding and then write &lt;code&gt;"verdict": "pass"&lt;/code&gt; underneath it. So the verdict is worked out again on our side, from the findings:&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;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&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;critical&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pass&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&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;medium&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;sev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&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;warn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A critical finding blocks the change, whatever the reviewer wrote. A blocked change can't be approved at all. You regenerate, and the findings go back to the writer word for word as a checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  A timeout is never a pass
&lt;/h2&gt;

&lt;p&gt;This is the rule I care about most. When a review errors, times out or returns something that doesn't parse, the easy move is to let the change through. SPOE doesn't. A network or provider hiccup gets one retry. After that, the change comes back marked as not reviewed, with a note in plain words saying exactly that. Approving it takes an explicit "I know this is unreviewed", and that approval goes into the audit log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fast, without skipping the check
&lt;/h2&gt;

&lt;p&gt;At first, every change waited for a human in a diff view. Safe, and slow. Now, with auto-apply on (the default), a change the review passes goes in by itself, as a version you can undo in one click.&lt;/p&gt;

&lt;p&gt;Four kinds of change never apply themselves: a blocked one, an unreviewed one, one with a high or critical finding, and one whose files you edited by hand since it was written. Those wait in the diff view for you. A manual edit is never silently overwritten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it falls short
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A second model is not a security audit. It catches common, obvious mistakes. It will miss subtle logic bugs, and it can be wrong in both directions.&lt;/li&gt;
&lt;li&gt;It reads the diff and the request, not the whole running system.&lt;/li&gt;
&lt;li&gt;Calibration never ends. A reviewer that calls everything critical gets ignored, so its instructions spell out what critical means: a human must not merge this, full stop. A missing rate limit is not that.&lt;/li&gt;
&lt;li&gt;Two families can still share blind spots. Different beats same. It is not a guarantee.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it, and tell me where it breaks
&lt;/h2&gt;

&lt;p&gt;SPOE is at &lt;a href="https://spoe.ai/?ref=devto" rel="noopener noreferrer"&gt;spoe.ai&lt;/a&gt;. You get 50 free credits to start, a build costs roughly 10 to 30, and your first payment of any size unlocks export for good.&lt;/p&gt;

&lt;p&gt;I'd like to hear where you think this review would fail. What would you want it to catch that it doesn't?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>We sign every export our AI app builder produces. Here's how it works</title>
      <dc:creator>SPOE</dc:creator>
      <pubDate>Sat, 10 Oct 2026 01:49:26 +0000</pubDate>
      <link>https://dev.to/spoe/we-sign-every-export-our-ai-app-builder-produces-heres-how-it-works-33ae</link>
      <guid>https://dev.to/spoe/we-sign-every-export-our-ai-app-builder-produces-heres-how-it-works-33ae</guid>
      <description>&lt;p&gt;I'm building SPOE, an AI app builder. Every tool in this space says "you own your code". I wanted to be able to prove it, so every export SPOE produces is signed. This is how that works, in enough detail that you can poke holes in it.&lt;/p&gt;

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

&lt;p&gt;When an AI writes your app, two questions follow the code around. Is this exactly what I was shown? And does it quietly depend on the tool that made it? A ZIP answers neither.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in an export
&lt;/h2&gt;

&lt;p&gt;Three files ride along with every project: &lt;code&gt;PROVENANCE.json&lt;/code&gt;, &lt;code&gt;PROVENANCE.sig&lt;/code&gt; and &lt;code&gt;verify-provenance.mjs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PROVENANCE.json&lt;/code&gt; lists every file with its SHA-256 and size, a hash over the whole tree, which models wrote and reviewed the changes (never the prompts), and the signer's public key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spoe_provenance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"project"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"slug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tint-booker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"stack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"frontend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nextjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"backend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"database"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgres"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web/app/page.tsx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sha256"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9f2c…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1834&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tree_sha256"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"41ad…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"generations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"generator"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reviewer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ed25519"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"c85280961cc26721"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"public_key_pem"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-----BEGIN PUBLIC KEY-----…"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signature in &lt;code&gt;PROVENANCE.sig&lt;/code&gt; is Ed25519 over a canonical form of that JSON: keys sorted recursively, no whitespace. The tree hash is SHA-256 over the sorted lines &lt;code&gt;path\0sha256\n&lt;/code&gt;, so the order files happen to be zipped in never matters. The key id is the first 16 hex characters of the SHA-256 of the public key's DER encoding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying it
&lt;/h2&gt;

&lt;p&gt;The verifier is about 40 lines of plain Node 18+, with no packages and no network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ node verify-provenance.mjs
OK   signature valid (key_id c85280961cc26721)
OK   30 files match manifest
VERIFIED - exported 2026-10-09T01:24:13Z from project tint-booker v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It checks that the key id matches the embedded key, that the signature is valid, that every listed file is on disk with the right hash, and the tree hash. It exits 1 if anything is off.&lt;/p&gt;

&lt;p&gt;A valid signature only tells you the files match a key. To know who signed it, compare the key id with the keys spoe.ai publishes at &lt;a href="https://spoe.ai/api/signing-keys" rel="noopener noreferrer"&gt;spoe.ai/api/signing-keys&lt;/a&gt;. The page at &lt;a href="https://spoe.ai/verify?ref=devto" rel="noopener noreferrer"&gt;spoe.ai/verify&lt;/a&gt; does all of this in your browser without uploading the ZIP, and also lists any file in the ZIP that the manifest doesn't know about. The offline script now does that same check too.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Runs without us" is a test, not a promise
&lt;/h2&gt;

&lt;p&gt;Before an export is signed, a lint refuses it if anything ties it back to SPOE: an import of an SPOE package, a dependency on one, code reading &lt;code&gt;SPOE_*&lt;/code&gt; environment variables, a call to SPOE's API, or one of our internal folders. No warnings, no override.&lt;/p&gt;

&lt;p&gt;And CI does the whole thing on every commit: export the scaffold, unzip it into an empty directory, verify the signature, &lt;code&gt;docker compose up&lt;/code&gt;, hit the health check. If that fails, nothing ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other half: a second model
&lt;/h2&gt;

&lt;p&gt;Every change the builder makes is reviewed by a second model from a different model family before it lands. A critical finding blocks the change whatever the reviewer's own verdict says, and a review that times out or errors is never treated as a pass: it waits for a human.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't prove
&lt;/h2&gt;

&lt;p&gt;A signature proves integrity and origin, not quality. It can't tell you the code is free of bugs or safe to run; the review and your own eyes are for that. It also says nothing about the prompts, which are deliberately left out of the manifest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it, and tell me what's wrong with it
&lt;/h2&gt;

&lt;p&gt;SPOE is at &lt;a href="https://spoe.ai/?ref=devto" rel="noopener noreferrer"&gt;spoe.ai&lt;/a&gt;. It's free to start (50 credits on sign-up and a daily top-up), runs on Venice's private models, and your first payment of any size unlocks export for good.&lt;/p&gt;

&lt;p&gt;I'd especially like critique of the manifest format and the verifier. What would you need to see before you trusted an export from a tool like this?&lt;/p&gt;

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