<?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: Dogwind</title>
    <description>The latest articles on DEV Community by Dogwind (@dogwind221).</description>
    <link>https://dev.to/dogwind221</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%2F4208790%2Fd2c5669e-c0b2-432a-8fe3-7c961b63fedb.jpg</url>
      <title>DEV Community: Dogwind</title>
      <link>https://dev.to/dogwind221</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dogwind221"/>
    <language>en</language>
    <item>
      <title>One plugin, two surfaces: putting an image editor inside an agent's composer</title>
      <dc:creator>Dogwind</dc:creator>
      <pubDate>Sat, 31 Oct 2026 09:57:30 +0000</pubDate>
      <link>https://dev.to/dogwind221/one-plugin-two-surfaces-putting-an-image-editor-inside-an-agents-composer-3jea</link>
      <guid>https://dev.to/dogwind221/one-plugin-two-surfaces-putting-an-image-editor-inside-an-agents-composer-3jea</guid>
      <description>&lt;p&gt;Most "chat with images" flows assume the image is already finished when you send it. Mine usually isn't: I want to circle a face and say "this expression, but warmer", or mask a product and say "re-shoot it on a wooden table". Typing that description is slower than pointing at it.&lt;/p&gt;

&lt;p&gt;So the plugin I built for DeepSeek Harness, &lt;a href="https://www.npmjs.com/package/dsh-img2img-config" rel="noopener noreferrer"&gt;&lt;code&gt;dsh-img2img-config&lt;/code&gt;&lt;/a&gt;, adds a full-screen editor that opens on the images already staged in the composer. The editing itself was the easy part — canvas, markers, brush mask, resize. The interesting problem was the hand-off: &lt;strong&gt;how do the edits reach the model without turning the composer into a JSON dump?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post is about that hand-off, plus the two surfaces the plugin ships and the host half that wires it to real provider credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two surfaces, one bundle
&lt;/h2&gt;

&lt;p&gt;The package is one npm artifact with a browser half and a host half:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browser half&lt;/strong&gt; (&lt;code&gt;./client&lt;/code&gt;): the composer dock + editor, and a Settings panel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host half&lt;/strong&gt; (&lt;code&gt;./index.js&lt;/code&gt;): the provider store and HTTP routes behind &lt;code&gt;/dsh-img2img-config/*&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Installation is a bundle patch — the profile gets a row pointing at the package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# cordis.patch.yml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;insert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dsh-img2img-config&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dsh-img2img-config'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and &lt;code&gt;package.json&lt;/code&gt; declares both halves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&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;"dsh"&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;"bundle"&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;"patch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./cordis.patch.yml"&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;"client"&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;"inject"&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="s2"&gt;"@deepseek-ai/dsh-client-locale"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"@deepseek-ai/dsh-client-ui-slots"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"@deepseek-ai/dsh-client-ui-conversation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"@deepseek-ai/dsh-client-ui-session"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"@deepseek-ai/dsh-client-ui-settings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"@deepseek-ai/dsh-client-ui-theme"&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;"platform"&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"&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="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;That &lt;code&gt;dsh.bundle&lt;/code&gt; field is the part people forget. A plugin that only declares &lt;code&gt;dsh.client&lt;/code&gt; is not installable — the client half has nowhere to attach. (I learned this the hard way; more in the last section.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The dock renders nothing most of the time
&lt;/h2&gt;

&lt;p&gt;The editor is a dock entry on &lt;code&gt;conversation.input.dock&lt;/code&gt;. The rule that matters for a crowded composer: &lt;strong&gt;the strip renders nothing while the session holds no image drafts&lt;/strong&gt;, so it never competes with the queue / todo / goal docks that may already be there. When there &lt;em&gt;are&lt;/em&gt; image drafts, each one gets a chip; clicking a chip opens the editor.&lt;/p&gt;

&lt;p&gt;Confirming the editor does three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;exports the edited files (&lt;code&gt;edited.png&lt;/code&gt;, plus &lt;code&gt;marked.png&lt;/code&gt; when markers exist, plus a binary brush mask when strokes exist),&lt;/li&gt;
&lt;li&gt;stages them as &lt;strong&gt;draft attachments&lt;/strong&gt;, replacing the edited source,&lt;/li&gt;
&lt;li&gt;hands the brief to the composer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 3 is where the design questions are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hand-off, three options
&lt;/h2&gt;

&lt;p&gt;If you read the image plus the instructions as "one message to the model", there are three shapes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write the instructions into the draft as text.&lt;/strong&gt; Simple, but users hate it: a wall of coordinates in the input box they have to scroll past.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attach the images and say nothing.&lt;/strong&gt; The model gets pixels, not intent. "Warmer" doesn't survive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Carry the instructions out-of-band and attach them as a file.&lt;/strong&gt; The model still reads them; the human still gets a clean composer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I went with a variant of 3 plus a reference chip. The composer shows &lt;strong&gt;one chip&lt;/strong&gt; labelled with the edit summary. The chip owns a payload — the full brief text — and the model never sees the chip: at submit, the chip's codec expands it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/client/brief-source.ts (trimmed)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;briefSource&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;TriggerSourceFace&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;{&lt;/span&gt;
    &lt;span class="na"&gt;trigger&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;img2img-brief&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;showGroupTitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;([]),&lt;/span&gt;   &lt;span class="c1"&gt;// no menu rows&lt;/span&gt;
    &lt;span class="na"&gt;onPick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;codec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Projection used for copy, draft persistence, and "flatten to text"&lt;/span&gt;
      &lt;span class="na"&gt;clipboardText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&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;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&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="mi"&gt;0&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;first&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;first&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="nx"&gt;BRIEF_MARK&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="c1"&gt;// The payload already IS the model text&lt;/span&gt;
      &lt;span class="na"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details make this work in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The first line is the marker.&lt;/strong&gt; Every brief starts with &lt;code&gt;[img2img] 图片编辑请求 · img2img-studio&lt;/code&gt;. That line is short, language-neutral as an identifier, and is exactly what the chip flattens to when the draft is persisted or copied. The rest of the brief never enters the draft.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-editing replaces, it doesn't stack.&lt;/strong&gt; &lt;code&gt;mergeDraft()&lt;/code&gt; removes the previous &lt;code&gt;[img2img] … [/img2img]&lt;/code&gt; block (and the legacy &lt;code&gt;&amp;lt;!-- img2img-editor:begin --&amp;gt;&lt;/code&gt; block from an older version), then appends the new brief. Your own typed text is left alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same facts are also attached as a machine-readable file, &lt;code&gt;img2img-manifest.json&lt;/code&gt;, next to the exported PNGs. So the model can read pixels &lt;em&gt;and&lt;/em&gt; fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&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;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&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;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-27T09:12:44.183Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"table.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1200&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;"size"&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;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cover"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"changed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;"bgRemove"&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;"requested"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"localPreview"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tolerance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&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;"erase"&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;"strokes"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"replace the price tag with the same wood grain"&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;"markers"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&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;"x"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.412&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"y"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.238&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"keep the glaze highlights here"&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;"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;"edited"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"edited.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"marked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"marked.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mask"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mask.png"&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;Marker coordinates are normalized percentages with the origin at the top-left, so a model that works in relative terms doesn't need the source resolution. And the brief closes with an explicit instruction to the generation provider:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;按标记与掩码做局部重绘，再做整体 i2i；标记与掩码只用于定位，不要出现在成图里。&lt;br&gt;
(Local redraw per mark and mask first, then a whole-image i2i. Marks and masks are for localisation only — never let them into the output.)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last sentence is doing real work. Without it, models happily draw the numbered pins into the final image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Degrading instead of crashing
&lt;/h2&gt;

&lt;p&gt;The chip path depends on services owned by the harness (&lt;code&gt;inputTriggers&lt;/code&gt;, &lt;code&gt;conversation&lt;/code&gt;, &lt;code&gt;session&lt;/code&gt;). Those are not guaranteed to be present, or present &lt;em&gt;at the moment this plugin applies&lt;/em&gt; — a batched client boot can materialise a service after your &lt;code&gt;apply&lt;/code&gt;. So the client half resolves them lazily and falls back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No trigger registry&lt;/strong&gt; (older client build, moved service): the same brief is written into the draft as text between &lt;code&gt;[img2img]&lt;/code&gt; and &lt;code&gt;[/img2img]&lt;/code&gt;. Re-editing replaces that block instead of stacking another one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service appears late&lt;/strong&gt;: register the codec source in &lt;code&gt;ctx.inject(['inputTriggers'], …)&lt;/code&gt; rather than reading it once during &lt;code&gt;apply&lt;/code&gt;. A one-shot read on such a boot finds nothing and silently leaves an unexpandable chip behind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draft attachments unavailable&lt;/strong&gt;: keep the dock, but the confirm path drops to the draft-line carrier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these paths crash; they get less pretty. That's the trade I wanted: the editor is a convenience layer on top of the composer, not a new failure mode for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The settings surface: from a panel to real credentials
&lt;/h2&gt;

&lt;p&gt;The second surface is a Settings panel that configures &lt;strong&gt;both halves of the toolchain&lt;/strong&gt;: vision (&lt;code&gt;dsh-vision-skill&lt;/code&gt;) and generation (this skill). The panel talks to the host half through the plugin's own routes, and the host owns a provider store at &lt;code&gt;~/.dsh/dsh-img2img-config/providers.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What the panel actually manages, briefly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Three priority tiers&lt;/strong&gt; (main / backup / fallback) with drag ordering — so "try ChatGPT web, then DashScope, then a local fallback" is a configuration, not code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-channel model fallback chains&lt;/strong&gt; — a provider can list several models; the script walks them on failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arrears-aware probing&lt;/strong&gt; — providers that report insufficient balance can be auto-disabled instead of retried into a wall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A ChatGPT-web account store&lt;/strong&gt; — the channel that drives a real browser session, with per-account quota state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Saving writes each provider &lt;strong&gt;back into the matching skill's &lt;code&gt;scripts/.env&lt;/code&gt;&lt;/strong&gt;, so the skill's own scripts pick the settings up with no extra step. The mapping — which keys to write, which to delete, which to leave alone — lives in one &lt;code&gt;planSync()&lt;/code&gt; function, which is also exported to an offline regression script (&lt;code&gt;scripts/verify-catalog.mjs&lt;/code&gt;). That last part matters: the panel can be tested without a browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipping it actually took
&lt;/h2&gt;

&lt;p&gt;The code was the fun part. Distribution was the humbling part:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;dsh.client&lt;/code&gt; alone is not installable.&lt;/strong&gt; The first version declared only the client half; the plugin resolved, but had nowhere to attach. &lt;code&gt;dsh.bundle&lt;/code&gt; + &lt;code&gt;cordis.patch.yml&lt;/code&gt; is what makes it a bundle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guessed peer upper bounds are a silent killer.&lt;/strong&gt; I had &lt;code&gt;&amp;gt;=4.0.0-rc &amp;lt;5&lt;/code&gt; on the host package. That's a promise to be refused on the next major release — with no log line, because the harness refuses before importing your module. I dropped the upper bounds and pinned "no &lt;code&gt;&amp;lt;&lt;/code&gt; in peer ranges" with a CI check instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publishing needed three gates.&lt;/strong&gt; Security-key 2FA, provenance (only from CI, via OIDC), and staged publishing for a brand-new package name. I wrote that one up separately: &lt;em&gt;&lt;a href="https://dev.to/dogwind221/three-gates-before-your-first-npm-publish-2fa-provenance-and-staged-releases-4jcb"&gt;Three gates before your first npm publish&lt;/a&gt;.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storefront metadata lives in your repo.&lt;/strong&gt; A &lt;code&gt;screenshots.json&lt;/code&gt; next to &lt;code&gt;package.json&lt;/code&gt; (1–8 images, relative paths or GitHub-hosted URLs) is what gives the plugin page App Store-style screenshots; without it, the storefront falls back to scraping your README.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would change next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Make the brief locale-aware.&lt;/strong&gt; The brief is written in Chinese today because the primary user is; a model-facing text probably should follow the session language, not the author's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version the manifest schema explicitly.&lt;/strong&gt; &lt;code&gt;version: 1&lt;/code&gt; is there, but there's no migration path yet for when the editor grows a new operation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two carriers, one truth.&lt;/strong&gt; The chip and the manifest file carry the same facts; I'd like to derive one from the other in exactly one place rather than keeping both builders in sync by hand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point of the plugin was to make "point at the image and describe the change" feel like part of writing, not a separate app. The dock and the chip get most of the way there; the degradation paths are what make it survive contact with a host that is still moving.&lt;/p&gt;

&lt;p&gt;Package: &lt;a href="https://www.npmjs.com/package/dsh-img2img-config" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/dsh-img2img-config&lt;/a&gt;&lt;br&gt;
Repository: &lt;a href="https://github.com/Dogwind221/img2img-studio/tree/main/plugins/dsh-img2img-config" rel="noopener noreferrer"&gt;https://github.com/Dogwind221/img2img-studio/tree/main/plugins/dsh-img2img-config&lt;/a&gt;&lt;br&gt;
Market entry: &lt;a href="https://awesome-dsh-plugin.com/p/Dogwind221/img2img-studio--plugins-dsh-img2img-config/" rel="noopener noreferrer"&gt;https://awesome-dsh-plugin.com/p/Dogwind221/img2img-studio--plugins-dsh-img2img-config/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>typescript</category>
      <category>ux</category>
    </item>
    <item>
      <title>Three gates before your first npm publish: 2FA, provenance, and staged releases</title>
      <dc:creator>Dogwind</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:25:37 +0000</pubDate>
      <link>https://dev.to/dogwind221/three-gates-before-your-first-npm-publish-2fa-provenance-and-staged-releases-4jcb</link>
      <guid>https://dev.to/dogwind221/three-gates-before-your-first-npm-publish-2fa-provenance-and-staged-releases-4jcb</guid>
      <description>&lt;p&gt;I published a small plugin to npm for the first time last week. It is a DeepSeek Harness plugin called &lt;a href="https://www.npmjs.com/package/dsh-img2img-config" rel="noopener noreferrer"&gt;&lt;code&gt;dsh-img2img-config&lt;/code&gt;&lt;/a&gt; — a composer image editor plus a provider settings panel. The code was done in a day; getting it onto the registry took three separate attempts, each blocked by a different gate.&lt;/p&gt;

&lt;p&gt;None of the errors said &lt;em&gt;what the gate was for&lt;/em&gt;. They said things like &lt;code&gt;EUSAGE&lt;/code&gt;, &lt;code&gt;E403&lt;/code&gt;, and &lt;code&gt;404 Not Found&lt;/code&gt; — the last one while publishing a package that did not exist yet. This post is the write-up I wish I had found first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 1 — 2FA is a browser flow, not a code
&lt;/h2&gt;

&lt;p&gt;The account had 2FA set up with a &lt;strong&gt;security key&lt;/strong&gt;, not a TOTP app. &lt;code&gt;npm publish&lt;/code&gt; does not ask for a six-digit code in that case, so the first attempt looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm error code EOTP
npm error This operation requires a one-time password.
npm error Open this URL in your browser to authenticate:
npm error   https://www.npmjs.com/auth/cli/***
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The URL is the actual gate. Open it in a browser that is already logged in to npm, confirm with the security key, and the waiting CLI continues. Two things make this confusing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The URL is masked in non-interactive output.&lt;/strong&gt; Run &lt;code&gt;npm publish&lt;/code&gt; from a TTY and the prompt is obvious. Run it from a script, a background job, or an agent, and you get &lt;code&gt;***&lt;/code&gt; and no way to complete the flow. If you are automating, you cannot be the thing that authorizes a new package by hand — that is what trusted publishing is for (Gate 2).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An expired session token fails with 404, not 401.&lt;/strong&gt; After the token in &lt;code&gt;~/.npmrc&lt;/code&gt; had been revoked, &lt;code&gt;npm whoami&lt;/code&gt; returned &lt;code&gt;E401 Unauthorized&lt;/code&gt;, but &lt;code&gt;npm publish&lt;/code&gt; returned:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm error 404 Not Found - PUT https://registry.npmjs.org/my-package - Not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a brand-new package name, the registry does not tell you whether the name is taken or whether you are simply not authenticated. It returns 404 either way. If a publish fails with 404 on a name you know is free, check &lt;code&gt;npm whoami&lt;/code&gt; before you start debugging the package.&lt;/p&gt;

&lt;p&gt;One more detail worth knowing early: npm is tightening bypass-2FA tokens — account changes first, direct publishing later. The deprecation notice points at &lt;a href="https://gh.io/npm-gat-bypass2fa-deprecation" rel="noopener noreferrer"&gt;gh.io/npm-gat-bypass2fa-deprecation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 2 — &lt;code&gt;provenance: true&lt;/code&gt; cannot run on your laptop
&lt;/h2&gt;

&lt;p&gt;The package had this in &lt;code&gt;package.json&lt;/code&gt;:&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;"publishConfig"&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;"access"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"provenance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;Locally that fails immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm error code EUSAGE
npm error Automatic provenance generation not supported for provider: null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;null&lt;/code&gt; here means "I am not running inside a supported CI provider, so I have no OIDC identity to sign a build statement with". Provenance is a &lt;a href="https://slsa.dev/" rel="noopener noreferrer"&gt;SLSA&lt;/a&gt; attestation: it says &lt;em&gt;this tarball was built by this workflow, from this commit, in this repository&lt;/em&gt;. A laptop has no such identity to vouch for.&lt;/p&gt;

&lt;p&gt;So you have two options, and they are not interchangeable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Publish locally&lt;/strong&gt; with &lt;code&gt;--no-provenance&lt;/code&gt;. The package goes up without an attestation. Fine for a first version if you just need the name and a working tarball.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish from CI&lt;/strong&gt; with &lt;code&gt;--provenance&lt;/code&gt; and &lt;code&gt;permissions: id-token: write&lt;/code&gt;. This is the one that produces the "Built and signed on GitHub Actions" badge on the package page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CI route is not much more work. A minimal workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;publish&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;          &lt;span class="c1"&gt;# required for provenance / trusted publishing&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v7&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;22&lt;/span&gt;
          &lt;span class="na"&gt;registry-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://registry.npmjs.org&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install -g npm@latest&lt;/span&gt;   &lt;span class="c1"&gt;# OIDC publishing needs a recent npm&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm publish --access public --provenance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that workflow present in the repository, configure the trusted publisher &lt;strong&gt;once&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm trust github my-package &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--file&lt;/span&gt; publish.yml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--repo&lt;/span&gt; my-org/my-repo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allow-publish&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;npm trust list my-package&lt;/code&gt; then shows what you granted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type: github
file: publish.yml
repository: my-org/my-repo
permissions: publish, stage publish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From that point on, a &lt;code&gt;workflow_dispatch&lt;/code&gt; run publishes with a signed attestation and &lt;strong&gt;no long-lived token&lt;/strong&gt;. That is the whole point: the gate moves from "a human proves presence with a security key" to "a workflow proves identity with OIDC".&lt;/p&gt;

&lt;p&gt;You can check the result from the registry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://registry.npmjs.org/-/npm/v1/attestations/my-package@0.1.0 | jq &lt;span class="s1"&gt;'.attestations[].predicateType'&lt;/span&gt;
&lt;span class="c"&gt;# "https://github.com/npm/attestation/tree/main/specs/publish/v0.1"&lt;/span&gt;
&lt;span class="c"&gt;# "https://slsa.dev/provenance/v1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Gate 3 — stage-only tokens and the &lt;code&gt;0.0.0-stage&lt;/code&gt; placeholder
&lt;/h2&gt;

&lt;p&gt;The first API token I created was allowed to publish, but only &lt;strong&gt;to the staging area&lt;/strong&gt;. For an existing package that is fine — you stage a version, review it, approve it. For a package that has never been published, npm refuses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm error 403 Forbidden - PUT https://registry.npmjs.org/my-package
npm error Cannot publish "my-package": this token can only publish to a staging area,
npm error and "my-package" does not exist yet. Create it first with a direct-capable
npm error token, then use `npm stage publish`. (E_STAGE_REQUIRED)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The message is oddly specific and worth reading twice: &lt;strong&gt;the first publish of a name needs a direct publish capability.&lt;/strong&gt; Staged publishing is a mechanism for &lt;em&gt;subsequent&lt;/em&gt; versions.&lt;/p&gt;

&lt;p&gt;What you get when a staged path does run is a placeholder version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;versions: ["0.0.0-stage", "0.1.0"]
_dist-tags: { latest: "0.1.0" }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;0.0.0-stage&lt;/code&gt; is npm's own placeholder for staged publishing — its description literally reads "Temporary package placeholder for staged publishing". Do not unpublish it; it is the name reservation that lets the staged flow exist. Make sure you set &lt;code&gt;latest&lt;/code&gt; to your real version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm dist-tag add my-package@0.1.0 latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful commands from this family: &lt;code&gt;npm stage publish&lt;/code&gt;, &lt;code&gt;npm stage list&lt;/code&gt;, &lt;code&gt;npm stage view &amp;lt;id&amp;gt;&lt;/code&gt;, &lt;code&gt;npm stage approve &amp;lt;id&amp;gt;&lt;/code&gt;, &lt;code&gt;npm stage reject &amp;lt;id&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody warns you about: propagation is not instant
&lt;/h2&gt;

&lt;p&gt;The CI run printed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm notice Publishing to https://registry.npmjs.org/ with tag latest and public access
npm notice publish Signed provenance statement with source and build information from GitHub Actions
+ my-package@0.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Green checkmarks, &lt;code&gt;+ my-package@0.1.0&lt;/code&gt;, done — except the version was not on the registry yet. For a few minutes, &lt;code&gt;GET /my-package/0.1.0&lt;/code&gt; returned 404, &lt;code&gt;latest&lt;/code&gt; still pointed at the previous version, and the tarball URL 404'd. npm says as much, one line earlier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm notice Your package is being processed and may take a few minutes to become available.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So if a "successful" publish does not show up immediately, wait and re-check the registry directly instead of re-running the publish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt; https://registry.npmjs.org/my-package/0.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Checklist I now run before publishing anything
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;npm whoami&lt;/code&gt; — is the token still alive?&lt;/li&gt;
&lt;li&gt;Is this the &lt;strong&gt;first&lt;/strong&gt; publish of the name? Then plan for a direct/interactive publish, not a staged one.&lt;/li&gt;
&lt;li&gt;Does &lt;code&gt;package.json&lt;/code&gt; carry a &lt;code&gt;dsh.bundle&lt;/code&gt; / framework manifest that the host can actually load? (For DeepSeek Harness plugins, &lt;code&gt;dsh.client&lt;/code&gt; alone is not installable — the most common rejection reason on the community list.)&lt;/li&gt;
&lt;li&gt;Are there guessed &lt;strong&gt;upper bounds&lt;/strong&gt; in &lt;code&gt;peerDependencies&lt;/code&gt;? A &lt;code&gt;&amp;gt;=1.0.0 &amp;lt;2.0.0&lt;/code&gt; on a host package becomes a silent refusal on the next major release. See &lt;a href="https://github.com/deepseek-ai/deepseek-harness/discussions/10776" rel="noopener noreferrer"&gt;deepseek-ai/deepseek-harness#10776&lt;/a&gt; for the failure mode: the module resolves, &lt;code&gt;apply()&lt;/code&gt; never runs, and the host logs nothing. Pin "no upper bounds" with a test rather than a comment.&lt;/li&gt;
&lt;li&gt;Prefer the trusted-publisher path from day one: workflow + OIDC + &lt;code&gt;--provenance&lt;/code&gt;. No stored token, and the supply-chain evidence is a side effect.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What actually shipped
&lt;/h2&gt;

&lt;p&gt;Three attempts, one evening:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0.1.0&lt;/code&gt; — published by hand, with browser authorization and no provenance.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0.1.1&lt;/code&gt; — published from GitHub Actions through the trusted publisher, with a &lt;a href="https://registry.npmjs.org/-/npm/v1/attestations/dsh-img2img-config@0.1.1" rel="noopener noreferrer"&gt;SLSA provenance attestation&lt;/a&gt; and a fixed peer range.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gates are not arbitrary. 2FA asks "is a human here?"; provenance asks "which build produced this artifact, and can anyone check?"; staged publishing asks "is this name already owned, and by whom?". Each one is answered by a different mechanism, which is why the first publish feels like three unrelated failures instead of one publishing model.&lt;/p&gt;

&lt;p&gt;The package: &lt;a href="https://www.npmjs.com/package/dsh-img2img-config" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/dsh-img2img-config&lt;/a&gt;&lt;br&gt;
The repository: &lt;a href="https://github.com/Dogwind221/img2img-studio" rel="noopener noreferrer"&gt;https://github.com/Dogwind221/img2img-studio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>npm</category>
      <category>githubactions</category>
      <category>opensource</category>
      <category>supplychain</category>
    </item>
  </channel>
</rss>
