<?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: Yash Kumar Saini</title>
    <description>The latest articles on DEV Community by Yash Kumar Saini (@yashksaini).</description>
    <link>https://dev.to/yashksaini</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%2F1242333%2F27bdd497-8c93-4290-a901-94eec3759056.jpeg</url>
      <title>DEV Community: Yash Kumar Saini</title>
      <link>https://dev.to/yashksaini</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yashksaini"/>
    <language>en</language>
    <item>
      <title>Dev log #12 Hardening WebRTC and Polishing the UI: A Week of Networking and Refinement</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Mon, 13 Jul 2026 03:13:05 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-12-hardening-webrtc-and-polishing-the-ui-a-week-of-networking-and-refinement-29m2</link>
      <guid>https://dev.to/yashksaini/dev-log-12-hardening-webrtc-and-polishing-the-ui-a-week-of-networking-and-refinement-29m2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Spent the week balancing deep p2p networking work in Python with some much-needed UI polish on my personal site. 11 commits and 6 PRs later, I hit a perfect 7-day streak and made the codebase a bit more secure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;This week was all about the "invisible" work that makes software feel solid. I spent a good chunk of time in the weeds of p2p networking, specifically hardening WebRTC implementations, while also carving out time to refine the typography and feel of my personal portfolio. With 11 commits across 5 repos and 6 PRs in flight, I managed to keep the momentum going every single day of the week.&lt;/p&gt;

&lt;h2&gt;
  
  
  WHAT I BUILT
&lt;/h2&gt;

&lt;p&gt;Most of my direct commit activity this week was split between keeping my dev environment sharp and making my portfolio feel a bit more "me."&lt;/p&gt;

&lt;h3&gt;
  
  
  Portfolio &amp;amp; Personal Branding
&lt;/h3&gt;

&lt;p&gt;I spent some quality time in &lt;code&gt;yashksaini-coder/portfolio&lt;/code&gt;. If you're like me, you can't leave your personal site alone for more than a month. I pushed a few updates to the blog content, but the real fun was in the UI/UX tweaks. I swapped out the primary typography for &lt;strong&gt;JetBrains Mono&lt;/strong&gt;—there’s just something about a good monospace font that makes a dev portfolio feel right. &lt;/p&gt;

&lt;p&gt;I also went through a "make-interfaces-feel-better" phase. I refactored the &lt;code&gt;selectedwork&lt;/code&gt; section, specifically dropping a cursor-follow preview tile that felt a bit too "heavy" and replaced it with something more streamlined. I also polished the index rows to make the transitions feel snappier. It’s about +452/-279 lines of code, which is a healthy amount of churn for a week that was supposed to be about "minor" updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Maintenance Grind
&lt;/h3&gt;

&lt;p&gt;My &lt;code&gt;nvim&lt;/code&gt; config is basically a living organism at this point. I have CI set up to automatically track plugin updates, and this week was particularly noisy with 6 commits just keeping the toolchain current. It’s [skip ci] territory, but it ensures that when I sit down to actually write code, my editor isn't lagging behind the latest Lua API changes.&lt;/p&gt;

&lt;p&gt;I also did a quick version bump for &lt;code&gt;trx&lt;/code&gt; (v0.1.13) and merged a branch in &lt;code&gt;openslate&lt;/code&gt; to fix some styling and search logic in the command palette. It’s the kind of small-scale maintenance that prevents technical debt from snowballing.&lt;/p&gt;

&lt;h2&gt;
  
  
  PULL REQUESTS
&lt;/h2&gt;

&lt;p&gt;The PR list is where the "real" engineering happened this week. I’ve got five open PRs and one merged, mostly focused on the libp2p ecosystem and some documentation DX.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deep Diving into WebRTC (py-libp2p)
&lt;/h3&gt;

&lt;p&gt;The bulk of my brainpower went into &lt;code&gt;libp2p/py-libp2p&lt;/code&gt;. I’ve been working on a few critical pieces for the WebRTC-Direct transport:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Shared-port dispatching&lt;/strong&gt;: I opened a PR to add &lt;code&gt;UdpMux&lt;/code&gt;. This is a big one for anyone running nodes behind restrictive NATs. It allows for shared-port inbound dispatching, which is essential for making WebRTC-Direct viable in the real world. That PR alone is +541 lines of new logic.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Security &amp;amp; Hardening&lt;/strong&gt;: I submitted a fix to harden the &lt;code&gt;/sdp&lt;/code&gt; HTTP server against memory-amplification DoS attacks. Networking code is always a target, and making sure we aren't vulnerable to simple memory exhaustion is a priority.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Refactoring &amp;amp; Cleanup&lt;/strong&gt;: I also managed to delete some code! I deduplicated &lt;code&gt;_varint.py&lt;/code&gt; in favor of the centralized &lt;code&gt;libp2p.utils.varint&lt;/code&gt;. It’s only a net loss of about 60 lines, but every line of code removed is a line I don't have to debug later.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Testing&lt;/strong&gt;: Re-enabled some stale IPv6 integration tests for the TCP transport. Green CI is the only CI I trust.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Documentation DX
&lt;/h3&gt;

&lt;p&gt;Over at Hugging Face, I opened a PR for &lt;code&gt;tau&lt;/code&gt; to add a "copy" button to code blocks in the docs. It’s a small thing, but as a dev, there is nothing more annoying than trying to manually select a multi-line command and accidentally grabbing the line numbers. I opened an issue for it first to track the requirement and then followed up with the implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  CODE REVIEWS
&lt;/h2&gt;

&lt;p&gt;I spent some time on the other side of the fence this week, too. I reviewed and approved a PR for &lt;code&gt;trx&lt;/code&gt; that was updating the main branch with new contributor changes. It was a relatively light review week, but I always try to make sure I’m unblocking others, especially in smaller OSS projects where contributor momentum is everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  TECH STACK
&lt;/h2&gt;

&lt;p&gt;This week was a polyglot's dream. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Python&lt;/strong&gt;: My primary tool for the p2p networking work. Between &lt;code&gt;UdpMux&lt;/code&gt; and DoS protection, I was deep in the Python standard library and libp2p internals.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;TypeScript &amp;amp; MDX&lt;/strong&gt;: Used these for the portfolio updates. The JetBrains Mono swap and the UI refactor kept me busy in the frontend.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Rust&lt;/strong&gt;: Handled the version bumping and chore work for &lt;code&gt;trx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Lua&lt;/strong&gt;: Mostly automated, but still the backbone of my editing environment.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Svelte&lt;/strong&gt;: Touched this briefly while helping out with the &lt;code&gt;openslate&lt;/code&gt; command palette fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a &lt;strong&gt;7-day streak&lt;/strong&gt;, I’ve stayed consistent. The line delta (+609 / -306) shows that while I'm building new features (like the WebRTC muxing), I'm still making a conscious effort to refactor and delete the old cruft.&lt;/p&gt;

&lt;h2&gt;
  
  
  WHAT'S NEXT
&lt;/h2&gt;

&lt;p&gt;The WebRTC PRs in &lt;code&gt;py-libp2p&lt;/code&gt; are the priority for next week. I need to shepherd those through the review process and address any feedback on the &lt;code&gt;UdpMux&lt;/code&gt; implementation. I’m also looking forward to seeing the copy button land in the &lt;code&gt;tau&lt;/code&gt; docs—it’s a small win for developer experience. &lt;/p&gt;

&lt;p&gt;On the personal side, now that the typography on the portfolio is settled, I might start looking into some more interactive elements for the blog section. But for now, I’m just happy to have a week of green squares and solid networking code under my belt. See you in the next push!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Yash K Saini&lt;/strong&gt; — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Generated by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;

</description>
      <category>portfolio</category>
      <category>opensource</category>
      <category>weekendchallenge</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Adding WebRTC to py-libp2p: Two Runtimes, Three Specs, and One Unforgiving Private Slot</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Mon, 13 Jul 2026 03:07:12 +0000</pubDate>
      <link>https://dev.to/yashksaini/adding-webrtc-to-py-libp2p-two-runtimes-three-specs-and-one-unforgiving-private-slot-3n22</link>
      <guid>https://dev.to/yashksaini/adding-webrtc-to-py-libp2p-two-runtimes-three-specs-and-one-unforgiving-private-slot-3n22</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — py-libp2p &lt;a href="https://github.com/libp2p/py-libp2p/pull/1309" rel="noopener noreferrer"&gt;PR #1309&lt;/a&gt; adds an &lt;em&gt;experimental&lt;/em&gt; WebRTC transport (&lt;code&gt;/webrtc-direct&lt;/code&gt; and &lt;code&gt;/webrtc&lt;/code&gt;) as a v1 node-to-node foundation. It's ~6,700 lines across 41 files, gated by &lt;code&gt;libp2p[webrtc]&lt;/code&gt; + &lt;code&gt;enable_webrtc&lt;/code&gt;, and merged after four rounds of maintainer review. This is the story of what's inside, why the seams are where they are, and the three bugs that would have shipped silently if the loopback test hadn't been written.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  A quick honest note before we start
&lt;/h2&gt;

&lt;p&gt;I want to be upfront about how this post came together. This is a &lt;em&gt;reflective&lt;/em&gt; engineering write-up on a PR I authored, with the goal of turning the review conversation, spec cross-references, and multi-round fixes into something a future reader can learn from without having to scroll 400 GitHub comments.&lt;/p&gt;

&lt;p&gt;Where I quote maintainers, those are their words. Where I explain "why we did it this way," I've tried to preserve the trade-offs as they actually came up in review — not the polished, past-tense version. This isn't a marketing post. WebRTC in py-libp2p is &lt;em&gt;experimental v1&lt;/em&gt;; browser interop is not supported; there are follow-ups filed. I'll say that plainly throughout.&lt;/p&gt;

&lt;p&gt;If you're new to libp2p, WebRTC, or trio, there's enough background woven in to keep you oriented. If you're deep in this ecosystem already, the interesting sections are the framing, the certificate pin, and the two-runtime bridge — feel free to jump.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem: why a Python libp2p wants WebRTC at all
&lt;/h2&gt;

&lt;p&gt;libp2p is a modular network stack: identity, transports, security, multiplexing, routing, and pub/sub, each swappable. Its Go and JS implementations have had WebRTC for a while. Python did not — &lt;a href="https://github.com/libp2p/py-libp2p/issues/546" rel="noopener noreferrer"&gt;issue #546&lt;/a&gt; tracks the gap.&lt;/p&gt;

&lt;p&gt;Why care? Three reasons the ecosystem keeps coming back to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;NAT traversal for real users.&lt;/strong&gt; Most consumer machines sit behind at least one NAT, often more. WebRTC's ICE + STUN + TURN dance is the only widely-deployed answer that browsers speak natively. If your Python peer wants to talk to a browser peer — or two NATed Python peers want to talk directly with only a light-touch relay — WebRTC is the door.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native multiplexing.&lt;/strong&gt; WebRTC data channels give you real per-stream framing over SCTP. That means the transport itself carries a "muxer," so you can skip the Yamux / Mplex upgrade step that a TCP-based libp2p connection needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-to-browser without an HTTP server.&lt;/strong&gt; With &lt;code&gt;/webrtc-direct&lt;/code&gt;, a Python node can accept an inbound connection from a browser peer &lt;em&gt;purely&lt;/em&gt; via a UDP socket + a certificate hash embedded in the multiaddr. No signaling server, no HTTPS termination, no TURN unless traversal forces you there.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the "why." Now for the "how" — and specifically, why "how" turned into 6,700 lines of Python that took four review rounds to get right.&lt;/p&gt;




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

&lt;p&gt;The PR body says it, but here it is in one paragraph so you know the surface area:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;WebRTCDirectTransport&lt;/code&gt; (&lt;code&gt;/webrtc-direct&lt;/code&gt;) and &lt;code&gt;WebRTCPrivateTransport&lt;/code&gt; (&lt;code&gt;/webrtc&lt;/code&gt;, relay-signaled).&lt;/li&gt;
&lt;li&gt;Spec-compliant data-channel framing — uvarint length-prefixed protobuf, with the full FIN / FIN_ACK / STOP_SENDING / RESET state machine.&lt;/li&gt;
&lt;li&gt;In-band data channels for application streams; the Noise handshake channel (&lt;code&gt;id=0&lt;/code&gt;) stays pre-negotiated per spec.&lt;/li&gt;
&lt;li&gt;Signaling with the bilateral &lt;code&gt;ICE_DONE&lt;/code&gt; fix from &lt;a href="https://github.com/libp2p/specs/pull/585" rel="noopener noreferrer"&gt;libp2p/specs#585&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Noise XX with a prologue that binds the handshake to the DTLS certificate fingerprints.&lt;/li&gt;
&lt;li&gt;ECDSA P-256 cert generation with multihash / multibase fingerprint encoding.&lt;/li&gt;
&lt;li&gt;A DTLS cert pin that actually pins the cert. More on that shortly.&lt;/li&gt;
&lt;li&gt;A trio ↔ asyncio bridge, because py-libp2p is trio and aiortc is asyncio.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final tests count: &lt;strong&gt;153 / 153 passing&lt;/strong&gt;, including a real &lt;code&gt;RTCPeerConnection&lt;/code&gt; ↔ &lt;code&gt;RTCPeerConnection&lt;/code&gt; loopback test that exercises framing, chunked writes over the 16 KiB SCTP ceiling, and the SDP-fingerprint canary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's out of scope&lt;/strong&gt;, and this matters: browser interop is &lt;em&gt;not&lt;/em&gt; supported here; go / js &lt;code&gt;/webrtc-direct&lt;/code&gt; interop is &lt;em&gt;not&lt;/em&gt; wired (they reconstruct the SDP from STUN &lt;code&gt;USERNAME&lt;/code&gt;, we don't have that hook yet); the &lt;code&gt;/webrtc&lt;/code&gt; dial path is a listener + signaling skeleton only. All of that is deliberate. This is v1. v2 (&lt;a href="https://github.com/libp2p/specs/pull/715" rel="noopener noreferrer"&gt;specs#715&lt;/a&gt;) is where browser dial actually happens, and we wanted the v1 skeleton in the tree so it stops being vaporware.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture in one picture
&lt;/h2&gt;

&lt;p&gt;Two flavors of WebRTC show up in py-libp2p, and it took me a while to keep them straight. Here they are, side by side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  subgraph Direct["/webrtc-direct (server-reachable)"]
    A[Client] -- SDP offer built&amp;lt;br/&amp;gt;from server multiaddr --&amp;gt; B[Server&amp;lt;br/&amp;gt;publishes cert hash]
    A &amp;lt;-. DTLS + data channels .-&amp;gt; B
  end

  subgraph Private["/webrtc (both peers NATed)"]
    C[Peer A] --- R[Relay v2]
    D[Peer B] --- R
    C -. signaling over relay:&amp;lt;br/&amp;gt;SDP + ICE + ICE_DONE .-&amp;gt; D
    C &amp;lt;-. direct DTLS + data channels .-&amp;gt; D
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two transports, one shared substrate: certs, SDP, Noise binding, stream framing, and the trio↔asyncio bridge. Only signaling differs.&lt;/p&gt;

&lt;p&gt;Inside a single peer, the runtime layout looks 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;flowchart TB
  subgraph Trio["Trio world (py-libp2p)"]
    T1[Host / Swarm]
    T2[WebRTCConnection]
    T3[WebRTCStream × N]
    T4[Application handlers]
  end

  subgraph Bridge["AsyncioBridge&amp;lt;br/&amp;gt;(background daemon thread)"]
    BR[asyncio event loop]
  end

  subgraph Asyncio["Asyncio world (aiortc)"]
    A1[RTCPeerConnection]
    A2[RTCDataChannel × N]
    A3[DTLS / ICE / SCTP]
  end

  T1 --&amp;gt; T2 --&amp;gt; T3
  T2 -- run_coro --&amp;gt; BR
  BR -- runs on --&amp;gt; A1
  A1 --&amp;gt; A2 --&amp;gt; A3
  A2 -. on_data via trio_token .-&amp;gt; T3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting seam is the arrow labelled &lt;em&gt;trio_token&lt;/em&gt;: aiortc calls into us from an asyncio thread, and we need those callbacks to hand data to trio memory channels &lt;em&gt;safely&lt;/em&gt;. If we skip the token, we get &lt;code&gt;RunFinishedError&lt;/code&gt; and worse. That code lives in &lt;code&gt;stream.py:382-410&lt;/code&gt; — I'll come back to it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deep dive #1 — framing, or "why bare protobuf was the wrong wire"
&lt;/h2&gt;

&lt;p&gt;If you read the first draft of this PR, application-level writes were serialized as bare protobuf &lt;code&gt;Message&lt;/code&gt; bytes and shoved straight into &lt;code&gt;data_channel.send(...)&lt;/code&gt;. That "works" on the happy path for small payloads. It's also wrong for two independent reasons — and once you understand &lt;em&gt;both&lt;/em&gt;, you understand why the spec is written the way it is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reason 1: SCTP has a 16 KiB message ceiling
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/libp2p/specs/blob/master/webrtc/webrtc.md" rel="noopener noreferrer"&gt;libp2p WebRTC spec&lt;/a&gt; caps each data-channel message at 16 384 bytes. Below that, you're fine. Above, aiortc will happily hand SCTP an oversized message that gets fragmented in ways your peer can't reassemble, or your peer will drop it, or SCTP itself will complain — the failure modes vary by implementation, which is worse than a clean rejection.&lt;/p&gt;

&lt;p&gt;The fix is to chunk on the sender's side and let the read side see one wire message per SCTP message.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reason 2: bare protobuf isn't self-delimiting inside a stream frame
&lt;/h3&gt;

&lt;p&gt;Even inside the 16 KiB limit, a Go or JS peer decoding your message needs a length prefix. The spec is explicit: &lt;strong&gt;each SCTP message is one uvarint length prefix + one protobuf &lt;code&gt;Message&lt;/code&gt;&lt;/strong&gt;. Not "raw protobuf." Not "concatenated protobufs." Exactly one framed pair per SCTP datagram.&lt;/p&gt;

&lt;p&gt;Here's the on-wire shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────┬──────────────────────────────────────────┐
│  uvarint length   │  protobuf Message (flag? + message? bytes)│
└───────────────────┴──────────────────────────────────────────┘
                    ← up to 16 KiB total, hard ceiling ──────→
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The uvarint itself is straightforward — 7 bits of payload per byte, top bit for continuation, capped at 10 bytes so it fits &lt;code&gt;uint64&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# libp2p/transport/webrtc/_varint.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;encode_uvarint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uvarint cannot encode negative values&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bytearray&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mh"&gt;0x7F&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mh"&gt;0x80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sender side of a stream then becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# libp2p/transport/webrtc/stream.py
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;StreamState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RESET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;WebRTCStreamError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stream was reset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_write_closed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;WebRTCStreamError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write side is closed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;MAX_PAYLOAD_SIZE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&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;MAX_PAYLOAD_SIZE&lt;/code&gt; is chosen so that even with the largest reasonable varint length prefix plus protobuf tag bytes, the full framed wire message stays under 16 KiB. The chunk boundary is on the sender; the reader just decodes one frame per SCTP message.&lt;/p&gt;

&lt;p&gt;And this is worth calling out — there's one spec detail that's easy to miss:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The spec allows a message to carry both data and FIN, and the data must be delivered to the reader before the read channel is closed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the receive path enqueues payload &lt;em&gt;first&lt;/em&gt;, then processes the flag. Getting that order wrong gives you a data loss bug that only shows up on the last chunk of a stream close. From &lt;code&gt;stream.py:352-380&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# libp2p/transport/webrtc/stream.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_apply_on_trio_thread&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Enqueue payload BEFORE processing flags — the spec allows a
&lt;/span&gt;    &lt;span class="c1"&gt;# message to carry both data and FIN, and the data must be
&lt;/span&gt;    &lt;span class="c1"&gt;# delivered to the reader before the read channel is closed.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_read_send&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_nowait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;trio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WouldBlock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_flag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FIN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_read_closed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_enqueue_eof_sentinel_locked&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_schedule_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FIN_ACK&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FIN_ACK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_fin_ack_received&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comment isn't decoration; it's a spec citation for a future reader who wonders why the ordering matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deep dive #2 — the certificate pin that actually pins
&lt;/h2&gt;

&lt;p&gt;This is my favourite bug in the PR. It's the kind of bug that survives green tests because the failure is silent, the API looks correct, and the wrong value is only observable if you cross-check two places that most people cross-check exactly once — at spec-reading time, then never again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting the scene
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;/webrtc-direct&lt;/code&gt; works by embedding the server's DTLS certificate hash &lt;em&gt;in the multiaddr&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/ip4/127.0.0.1/udp/9090/webrtc-direct/certhash/uEiA…/p2p/12D3K…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client dials that address, constructs an SDP offer locally (there's no signaling server), and expects the server's DTLS handshake to present a cert whose SHA-256 matches the &lt;code&gt;certhash&lt;/code&gt; in the multiaddr. If they don't match, the client bails — that's the whole security story of &lt;code&gt;/webrtc-direct&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Which means: whatever cert the server &lt;em&gt;advertises&lt;/em&gt;, it must actually &lt;em&gt;use&lt;/em&gt; in the DTLS handshake. If those two paths diverge, you either get a broken connection or, worse, a working connection that isn't actually pinned to anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  The naïve pin
&lt;/h3&gt;

&lt;p&gt;In aiortc, you'd expect to set the cert on the &lt;code&gt;RTCPeerConnection&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RTCPeerConnection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_certificates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;my_cert&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# ← looks fine, right?
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a silent no-op. &lt;code&gt;RTCPeerConnection&lt;/code&gt; reads only its own name-mangled private attribute — &lt;code&gt;self.__certificates&lt;/code&gt; inside the class, which mangles to &lt;code&gt;self._RTCPeerConnection__certificates&lt;/code&gt; from the outside. The &lt;code&gt;pc._certificates&lt;/code&gt; attribute lives on the object, sure, but aiortc never looks at it. It happily generates its &lt;em&gt;own&lt;/em&gt; certificate at construction time and uses that in DTLS. Your advertised &lt;code&gt;/certhash/&lt;/code&gt; is a completely different digest.&lt;/p&gt;

&lt;p&gt;The maintainer review flagged this indirectly — the loopback test caught it directly. Every test that ran without an actual peer connection passed. The moment we ran an SDP round-trip and grepped the &lt;code&gt;a=fingerprint&lt;/code&gt; line, the pinned cert was nowhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# libp2p/transport/webrtc/_aiortc_helpers.py
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_peer_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;rtc_cert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RTCCertificate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ice_servers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&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="n"&gt;RTCPeerConnection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Create an RTCPeerConnection pinned to the given certificate.

    aiortc &amp;gt;= 1.5 no longer accepts certificates= in RTCConfiguration —
    passing it raises TypeError.  We build the peer connection with ICE
    servers only, then replace the auto-generated DTLS certificate before
    any SDP operation triggers the DTLS handshake.  The fingerprint and
    the actual handshake cert are read from a name-mangled private
    attribute (self.__certificates inside RTCPeerConnection →
    self._RTCPeerConnection__certificates from the outside).  Setting
    the un-mangled pc._certificates is a no-op that aiortc never reads.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RTCConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iceServers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ice_servers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ice_servers&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="n"&gt;pc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RTCPeerConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Must use the mangled name — aiortc reads only self.__certificates.
&lt;/span&gt;    &lt;span class="n"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_RTCPeerConnection__certificates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rtc_cert&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# type: ignore[attr-defined]
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two characters (&lt;code&gt;__&lt;/code&gt;) that would look like a typo in a code review, guarding a subtle security property.&lt;/p&gt;

&lt;h3&gt;
  
  
  The canary
&lt;/h3&gt;

&lt;p&gt;Fixing the bug is one thing; making sure it stays fixed is another. The loopback test now includes a &lt;em&gt;canary&lt;/em&gt; — after the SDP exchange, it parses the &lt;code&gt;a=fingerprint:sha-256&lt;/code&gt; line out of the offer and asserts it equals the SHA-256 of the pinned cert:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/core/transport/webrtc/test_multiplexing_loopback.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_sdp_fingerprint_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WebRTCCertificate&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Render *cert*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s SHA-256 fingerprint in the colon-separated upper-hex
    form aiortc writes into SDP a=fingerprint:sha-256 ... lines.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If somebody in the future refactors the pin — or aiortc renames &lt;code&gt;__certificates&lt;/code&gt; — this test fails immediately, with a message that points straight at the divergence. That's the whole point of a canary: it wouldn't have been added if the bug hadn't happened, and it stays there so the bug can't come back.&lt;/p&gt;

&lt;p&gt;The subplot here isn't "aiortc is buggy" — it's that libraries with public APIs sometimes leak private-attribute contracts into their integrators, and when they do, the only defense is a test that observes the on-wire outcome, not the API you &lt;em&gt;think&lt;/em&gt; you called.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deep dive #3 — bridging two runtimes without blowing up
&lt;/h2&gt;

&lt;p&gt;py-libp2p is trio. aiortc is asyncio. Both are event loops. Neither will &lt;code&gt;await&lt;/code&gt; the other's coroutines. This isn't a limitation of Python; it's a design decision by trio (structured concurrency, no thread-safety-by-default) that leans hard against interop.&lt;/p&gt;

&lt;p&gt;The options are: pick one runtime, run both loops in one thread with a bridge library, or run the second loop in its own thread. Option one wasn't available — swapping either dependency was a non-starter. Option two (&lt;code&gt;anyio&lt;/code&gt;, &lt;code&gt;trio-asyncio&lt;/code&gt;) has enough corner cases with aiortc's assumptions that we didn't want to chase them under a review deadline. Option three is boring and explicit, which is what we wanted.&lt;/p&gt;

&lt;p&gt;The bridge is a small class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# libp2p/transport/webrtc/_asyncio_bridge.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AsyncioBridge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Manages a background asyncio event loop for running aiortc coroutines.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_loop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AbstractEventLoop&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_thread&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Thread&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="c1"&gt;# Protects start/stop against concurrent trio calls
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;# Protects _loop/_started/_stopped reads from run_coro against
&lt;/span&gt;        &lt;span class="c1"&gt;# concurrent stop() — needed because run_coro_sync is called
&lt;/span&gt;        &lt;span class="c1"&gt;# from non-trio threads (asyncio callbacks).
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_state_lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two locks — a &lt;code&gt;trio.Lock&lt;/code&gt; for the trio side of start/stop, and a &lt;code&gt;threading.Lock&lt;/code&gt; because &lt;em&gt;asyncio callbacks&lt;/em&gt; can hit the bridge from a foreign thread. If you only have the trio lock, you race with those callbacks and it manifests as intermittent &lt;code&gt;AsyncioBridgeError: not running&lt;/code&gt; right at shutdown.&lt;/p&gt;

&lt;p&gt;The heart of it is &lt;code&gt;run_coro&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# libp2p/transport/webrtc/_asyncio_bridge.py
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_coro&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coro&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Coroutine&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_state_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_loop&lt;/span&gt;
        &lt;span class="n"&gt;running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_started&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_stopped&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;running&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;coro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AsyncioBridgeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AsyncioBridge is not running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_coroutine_threadsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coro&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_wait_for_result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# abandon_on_cancel=True lets trio cancel the scope immediately.
&lt;/span&gt;        &lt;span class="c1"&gt;# The background thread is abandoned but we cancel the asyncio
&lt;/span&gt;        &lt;span class="c1"&gt;# future below so it doesn't leak.
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;trio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;_wait_for_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abandon_on_cancel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;trio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cancelled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two subtle things worth pausing on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;abandon_on_cancel=True&lt;/code&gt;&lt;/strong&gt; — without it, trio's cancellation waits for the thread to unwind before actually cancelling. With aiortc's coroutines that can mean seconds, or forever. With &lt;code&gt;abandon_on_cancel=True&lt;/code&gt;, trio releases the scope immediately, we forward the cancellation to the asyncio future, and the background loop cleans up on its own timeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;_state_lock&lt;/code&gt; snapshot&lt;/strong&gt; — we read &lt;code&gt;loop&lt;/code&gt; and &lt;code&gt;running&lt;/code&gt; under the lock, then use the local &lt;code&gt;loop&lt;/code&gt; variable. If &lt;code&gt;stop()&lt;/code&gt; runs concurrently, we either dispatched to a valid loop (which will handle the cancel) or we raise cleanly. Never a race where we scheduled on a partially-torn-down loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The reverse direction — aiortc calling &lt;em&gt;us&lt;/em&gt; — needs a different trick. When a data channel fires an &lt;code&gt;on_message&lt;/code&gt; event, we're on the asyncio thread, and we need to push bytes into a &lt;code&gt;trio.MemorySendChannel&lt;/code&gt;. Direct calls into trio primitives from a foreign thread are undefined behavior. The trick is &lt;code&gt;trio.lowlevel.current_trio_token()&lt;/code&gt;, captured at construction time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# libp2p/transport/webrtc/stream.py
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_trio_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lowlevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_trio_token&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_trio_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_run_on_trio_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[],&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_trio_token&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;trio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lowlevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_task&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;in_trio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;in_trio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;in_trio&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;trio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trio_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;trio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RunFinishedError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we're already on a trio task (tests), we call inline. If we're on an asyncio callback thread, we go through &lt;code&gt;trio.from_thread.run_sync&lt;/code&gt; with the token. If trio has already shut down, we log and drop — not raise, because the callback is fire-and-forget from aiortc's point of view.&lt;/p&gt;

&lt;p&gt;This is the code that makes the whole transport not-flaky. It's ~30 lines. It took two review cycles to converge on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deep dive #4 — Noise XX, but bound to DTLS
&lt;/h2&gt;

&lt;p&gt;WebRTC already gives you an encrypted DTLS channel between two peers. Why do we still run Noise on top of it?&lt;/p&gt;

&lt;p&gt;Because DTLS authenticates the &lt;em&gt;certificate&lt;/em&gt;, not the &lt;em&gt;libp2p peer&lt;/em&gt;. Anyone with a valid ECDSA P-256 cert can complete the DTLS handshake. But libp2p peers are identified by their libp2p identity key, which is a completely different keypair. So we need a step where each side proves control of a libp2p identity — and the natural way to do that is Noise XX, which is already how libp2p peers authenticate on TCP.&lt;/p&gt;

&lt;p&gt;But there's a subtle attack here: what if an attacker sits between the two peers, has their own valid DTLS cert, and forwards both DTLS sessions? Both peers see a valid DTLS handshake, then run Noise on top — and if Noise doesn't know anything about DTLS, it authenticates fine end-to-end &lt;em&gt;through&lt;/em&gt; the attacker.&lt;/p&gt;

&lt;p&gt;The fix is the &lt;em&gt;Noise prologue&lt;/em&gt; — a byte string that's mixed into the Noise handshake state at the start. The libp2p spec binds Noise to DTLS by putting both certificate fingerprints into the prologue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# libp2p/transport/webrtc/noise_handshake.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_noise_prologue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;local_fingerprint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;remote_fingerprint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&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;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Build the Noise prologue that binds the handshake to the DTLS session.

    Prologue format:
        b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;libp2p-webrtc-noise:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; + encode(local_fp) + encode(remote_fp)
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;local_mh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_MH_SHA256_HEADER&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;local_fingerprint&lt;/span&gt;
    &lt;span class="n"&gt;remote_mh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_MH_SHA256_HEADER&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;remote_fingerprint&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;NOISE_PROLOGUE_PREFIX&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;local_mh&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;remote_mh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an attacker sits in the middle, each side sees a &lt;em&gt;different&lt;/em&gt; remote fingerprint (the attacker's), so the prologues mismatch, and Noise fails. Clean. Nothing needs to change in the Noise state machine itself — the prologue is a parameter it already accepts.&lt;/p&gt;

&lt;p&gt;The rest of the handshake reuses py-libp2p's existing &lt;code&gt;PatternXX&lt;/code&gt; implementation. That's what "in-band data channels" is buying us — the Noise handshake rides over data channel 0, which is pre-negotiated (&lt;code&gt;negotiated=True, id=0&lt;/code&gt;) so both peers agree on its identity without SDP renegotiation. Application streams get their own in-band channels, so the &lt;code&gt;on('datachannel')&lt;/code&gt; event fires on the peer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; in the first draft of the PR, application streams used &lt;code&gt;negotiated=True&lt;/code&gt; as well. The consequence was that on the remote peer, the &lt;code&gt;datachannel&lt;/code&gt; event &lt;em&gt;never fired&lt;/em&gt;, because pre-negotiated channels bypass it entirely. &lt;code&gt;accept_stream()&lt;/code&gt; blocked forever. It was a classic "the spec is clear, we read it too quickly" fix — Noise uses &lt;code&gt;negotiated=True&lt;/code&gt; because it's channel 0 and both sides know it's there; application streams use &lt;code&gt;negotiated=False&lt;/code&gt; because they need to be &lt;em&gt;announced&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deep dive #5 — signaling, ICE_DONE, and a spec fix in flight
&lt;/h2&gt;

&lt;p&gt;For the &lt;code&gt;/webrtc&lt;/code&gt; (relay-signaled) case, two NATed peers need to exchange SDP over some other channel to get started. That channel is a Circuit Relay v2 stream, and the protocol is &lt;code&gt;/webrtc-signaling/0.0.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The interesting part isn't the SDP exchange — it's how you know it's &lt;em&gt;done&lt;/em&gt;. ICE candidates trickle over the signaling stream as each peer's ICE agent discovers them. At some point, both sides are done trickling, and the stream can close. But if one side closes prematurely, the other side loses candidates it hadn't received yet.&lt;/p&gt;

&lt;p&gt;The old spec left this timing ambiguous. &lt;a href="https://github.com/libp2p/specs/pull/585" rel="noopener noreferrer"&gt;libp2p/specs#585&lt;/a&gt; fixed it by adding a &lt;em&gt;bilateral&lt;/em&gt; &lt;code&gt;ICE_DONE&lt;/code&gt; message — each side signals it's done, and neither closes until both &lt;code&gt;ICE_DONE&lt;/code&gt;s are seen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initiator                              Responder (via relay)
  ──── SDP_OFFER ─────────────────────────&amp;gt;
  &amp;lt;─── SDP_ANSWER ─────────────────────────
  &amp;lt;──&amp;gt; ICE_CANDIDATE (trickle, both ways) &amp;lt;&amp;gt;
  ──── ICE_DONE ───────────────────────────&amp;gt;
  &amp;lt;─── ICE_DONE ───────────────────────────
  (both sides close signaling stream)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's what &lt;code&gt;signaling.py&lt;/code&gt; implements. It's not the most complicated code in the PR, but it's the piece that &lt;em&gt;cannot&lt;/em&gt; be handwaved with "we'll add ICE_DONE later" — it changes the state machine of when both peers agree the signaling phase has ended.&lt;/p&gt;




&lt;h2&gt;
  
  
  The seam that will save us a rewrite: &lt;code&gt;_apply_ice_credentials()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;There's one more piece of "spec is in flight" that we chose to handle &lt;em&gt;architecturally&lt;/em&gt; rather than by writing a TODO.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/libp2p/specs/issues/672" rel="noopener noreferrer"&gt;libp2p/specs#672&lt;/a&gt; discussion is about Chrome removing SDP munging for ICE credentials — a change that will eventually break the v1 way of injecting the peer's ICE ufrag into the SDP. When that lands, code that has ICE credential handling &lt;em&gt;scattered&lt;/em&gt; across the SDP builder is in for a bad time. Code that has all of it in &lt;em&gt;one function&lt;/em&gt; just changes that function.&lt;/p&gt;

&lt;p&gt;So we put all of it in one function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# libp2p/transport/webrtc/sdp.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_apply_ice_credentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sdp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ufrag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;fingerprint_hex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;remote_ufrag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;remote_pwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Apply ICE credentials to the SDP.

    **This is the single seam for libp2p/specs#672.**  When Chrome drops
    ICE credential munging support, only this function needs to change.
    Currently a no-op passthrough — local credentials are already in the
    SDP template.  The remote credentials are accepted but unused until
    the spec changes require injecting them via a separate mechanism.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sdp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Today it's a passthrough. Tomorrow, when v2 dispatches by &lt;code&gt;libp2p+webrtc+v1/&lt;/code&gt; / &lt;code&gt;libp2p+webrtc+v2/&lt;/code&gt; ufrag prefixes, this function grows a small branch on the ufrag prefix and everything else in the builder stays intact.&lt;/p&gt;

&lt;p&gt;The maintainer's response, verbatim, was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The v1 foundation here (framing, Noise prologue, certhash, &lt;code&gt;_apply_ice_credentials()&lt;/code&gt; as a seam) is not throwaway — it feeds v2, but &lt;code&gt;_apply_ice_credentials()&lt;/code&gt; will likely evolve into a version-dispatch layer rather than a single swap.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the value of a seam. It reserves the space for future evolution without pre-committing to which evolution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing: 153 tests, but only one of them mattered
&lt;/h2&gt;

&lt;p&gt;Unit tests are cheap; they run in ~10 seconds and give you fast feedback on individual modules. Integration tests are expensive; the two-&lt;code&gt;RTCPeerConnection&lt;/code&gt; loopback test spins up an ICE agent, waits for connectivity, and pushes data through the SCTP stack. It takes a few seconds and is annoyingly flaky under heavy CI load.&lt;/p&gt;

&lt;p&gt;But that expensive test caught three bugs the cheap ones missed:&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;Symptom in unit tests&lt;/th&gt;
&lt;th&gt;Symptom in loopback&lt;/th&gt;
&lt;th&gt;Caught by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pc._certificates = [...]&lt;/code&gt; no-ops&lt;/td&gt;
&lt;td&gt;✅ Green&lt;/td&gt;
&lt;td&gt;Advertised certhash ≠ SDP fingerprint&lt;/td&gt;
&lt;td&gt;SDP-fingerprint canary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application channels used &lt;code&gt;negotiated=True&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅ Green&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;accept_stream()&lt;/code&gt; hangs forever&lt;/td&gt;
&lt;td&gt;Real accept-stream round trip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bare protobuf, no length prefix&lt;/td&gt;
&lt;td&gt;✅ Green&lt;/td&gt;
&lt;td&gt;16 KiB payload gets fragmented, reader dumps garbage&lt;/td&gt;
&lt;td&gt;&amp;gt; 16 KiB echo round-trip&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each of these was &lt;em&gt;reachable&lt;/em&gt; by unit tests only if you knew to look for them, which no one did. That's the lesson of integration testing WebRTC: unit tests are a floor, not a ceiling, and the ceiling has real hardware in it.&lt;/p&gt;

&lt;p&gt;The loopback test's docstring calls this out explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Integration test: two real RTCPeerConnections, end-to-end loopback.

Validates the WebRTC data-channel layer at the wire level — the three
ecosystem-compatibility fixes documented in PR #1309 review notes:

  - In-band data channels (negotiated=False): the responder&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s
    &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;datachannel&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; event must fire so WebRTCConnection.accept_stream()
    unblocks.
  - uvarint length-prefixed framing: every send must stay within the
    16 KiB spec ceiling; &amp;gt; 16 KiB payload round-trips through chunking
    and reassembly.
  - DTLS certificate pinning: aiortc &amp;gt;= 1.5 dropped &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;certificates=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; in
    RTCConfiguration; the cert override must take effect before
    createOffer/createAnswer so the local DTLS fingerprint in the SDP
    matches the one our peer expects.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If somebody reads this file six months from now, they know exactly &lt;em&gt;why&lt;/em&gt; those three assertions exist. That's the value of leaving the review context in the code, not just in the PR comments.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the review looked like from the inside
&lt;/h2&gt;

&lt;p&gt;Four review pushes on top of the initial scaffolding, roughly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Correctness pass.&lt;/strong&gt; Copilot flagged 22 inline issues, mostly F401 unused imports and one substantial one — that &lt;code&gt;dial()&lt;/code&gt; was returning connections without actually running ICE/DTLS/Noise. We raised &lt;code&gt;NotImplementedError&lt;/code&gt; on the paths that weren't wired yet, so a caller would fail loudly instead of getting a phantom connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wire the transport into aiortc.&lt;/strong&gt; This is where the cert-pin bug lived, silent, until we started running real handshakes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framing + in-band channels + cert pin.&lt;/strong&gt; The three bugs above, all in one series of commits. The commit messages are worth reading — each one names exactly what was wrong and why the fix is right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-PC loopback test + newsfragment reframe.&lt;/strong&gt; Landing the integration test that guards the three fixes, and rewriting the newsfragment so "experimental v1 node-to-node scaffolding" is the first thing a changelog reader sees.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The maintainer's approving comment, in full:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Merging this as &lt;strong&gt;experimental WebRTC&lt;/strong&gt; scaffolding behind &lt;code&gt;libp2p[webrtc]&lt;/code&gt; + &lt;code&gt;enable_webrtc&lt;/code&gt;. The critical-path fixes from the re-review (uvarint framing, in-band channels + DCEP wait, cert pinning, loopback tests, docstring/newsfragment accuracy) are in place, and CI is green.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;#546 stays open&lt;/strong&gt; (&lt;code&gt;Refs #546&lt;/code&gt; — do not auto-close on merge). This PR advances the parent issue but does not satisfy it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last line matters. Issue #546 is "WebRTC support in py-libp2p." This PR is not that. This PR is &lt;em&gt;scaffolding for&lt;/em&gt; that. The follow-ups — browser interop, go/js interop, full private dial — are separate issues that get their own PRs on this foundation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it goes next
&lt;/h2&gt;

&lt;p&gt;The concrete follow-ups, in the order they'll probably ship:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;STUN &lt;code&gt;USERNAME&lt;/code&gt; exposure spike in aiortc.&lt;/strong&gt; Go and JS &lt;code&gt;/webrtc-direct&lt;/code&gt; listeners reconstruct the SDP offer from the STUN username on the inbound packet. Our listener uses an HTTP &lt;code&gt;POST /sdp&lt;/code&gt; exchange as a py-to-py harness. If aiortc exposes the inbound STUN username via a hook, our listener can switch and interop lights up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private &lt;code&gt;/webrtc&lt;/code&gt; dial.&lt;/strong&gt; The listener is here, the signaling protocol is here, the &lt;code&gt;WebRTCPrivateTransport&lt;/code&gt; is here. The dial path currently constructs and returns a connection without running relay signaling — that's the next PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version dispatch in &lt;code&gt;_apply_ice_credentials()&lt;/code&gt;.&lt;/strong&gt; When &lt;code&gt;libp2p+webrtc+v1/&lt;/code&gt; and &lt;code&gt;libp2p+webrtc+v2/&lt;/code&gt; ufrag prefixes are standardized, that function grows a branch and &lt;code&gt;WebRTCDirectListener&lt;/code&gt; learns to look at the prefix. v2 is the browser-compatible path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interop tests against go-libp2p and js-libp2p.&lt;/strong&gt; These will fail today, deliberately. When the STUN username hook lands, they start passing, and we know we're on the ecosystem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are architecturally scary. The scary architecture is what this PR did: two runtimes, one wire format, one lifecycle, three specs. That's done.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one takeaway I want you to leave with
&lt;/h2&gt;

&lt;p&gt;If you're building anything that crosses two runtimes, two libraries, or two specs, &lt;strong&gt;the seams are the API&lt;/strong&gt;. Not the surface API — the &lt;em&gt;internal&lt;/em&gt; seams. Where does ICE credential handling live? One function. Where does the DTLS cert pin land? One helper. Where does the trio ↔ asyncio boundary get crossed? One class.&lt;/p&gt;

&lt;p&gt;The maintainer review didn't just catch bugs. It repeatedly pushed us toward &lt;em&gt;fewer&lt;/em&gt; places where the interesting decisions were expressed. Every review round pulled scattered logic into a named function, so that when the next spec revision lands (v1 → v2, &lt;code&gt;specs#672&lt;/code&gt;, &lt;code&gt;specs#715&lt;/code&gt;), the change is localized.&lt;/p&gt;

&lt;p&gt;Scaffolding isn't just "the parts that don't work yet." Scaffolding is &lt;em&gt;the shape of the seams&lt;/em&gt;, so the parts you fill in later fit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it, and honest expectations
&lt;/h2&gt;

&lt;p&gt;You can install and inspect it today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s1"&gt;'libp2p[webrtc]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;libp2p&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;new_host&lt;/span&gt;

&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;new_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enable_webrtc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# ...multiaddr: /ip4/…/udp/…/webrtc-direct/certhash/u…/p2p/…
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Realistic expectations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Two Python peers talking &lt;code&gt;/webrtc-direct&lt;/code&gt; on a loopback interface will work.&lt;/li&gt;
&lt;li&gt;⚠️ Python peer ↔ Go peer or Python peer ↔ JS peer over &lt;code&gt;/webrtc-direct&lt;/code&gt; will &lt;em&gt;not&lt;/em&gt; work yet (the STUN username reconstruction gap).&lt;/li&gt;
&lt;li&gt;❌ Browser peer dialing a Python peer will not work. That's v2's job.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to help, the follow-up sub-issues under &lt;a href="https://github.com/libp2p/py-libp2p/issues/546" rel="noopener noreferrer"&gt;#546&lt;/a&gt; are the map. The seams are ready. The wires need connecting.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/libp2p/specs/tree/master/webrtc" rel="noopener noreferrer"&gt;libp2p WebRTC spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libp2p/specs/blob/master/webrtc/webrtc-direct.md" rel="noopener noreferrer"&gt;libp2p WebRTC Direct spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libp2p/specs/pull/585" rel="noopener noreferrer"&gt;libp2p/specs#585 — bilateral ICE_DONE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libp2p/specs/issues/672" rel="noopener noreferrer"&gt;libp2p/specs#672 — ICE credential munging&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libp2p/specs/pull/715" rel="noopener noreferrer"&gt;libp2p/specs#715 — WebRTC v2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libp2p/js-libp2p/pull/3480" rel="noopener noreferrer"&gt;js-libp2p#3480 — reference v2 implementation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libp2p/py-libp2p/pull/1309" rel="noopener noreferrer"&gt;py-libp2p#1309 — this PR&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libp2p/py-libp2p/issues/546" rel="noopener noreferrer"&gt;py-libp2p#546 — parent issue, still open&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/aiortc/aiortc" rel="noopener noreferrer"&gt;aiortc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://trio.readthedocs.io/" rel="noopener noreferrer"&gt;trio&lt;/a&gt; — structured concurrency&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;Yash Kumar Saini&lt;/a&gt; — reflections on a merged py-libp2p PR. Corrections and pushback welcome; I'll update this post in place if I got anything wrong. The PR itself is the source of truth.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>libp2p</category>
      <category>webrtc</category>
      <category>python</category>
      <category>networking</category>
    </item>
    <item>
      <title>Hardening Kademlia DHT: The Eclipse Attack That Record Signing Doesn't Stop</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Thu, 09 Jul 2026 05:34:38 +0000</pubDate>
      <link>https://dev.to/yashksaini/hardening-kademlia-dht-the-eclipse-attack-that-record-signing-doesnt-stop-2k79</link>
      <guid>https://dev.to/yashksaini/hardening-kademlia-dht-the-eclipse-attack-that-record-signing-doesnt-stop-2k79</guid>
      <description>&lt;h1&gt;
  
  
  Hardening Kademlia DHT: The Eclipse Attack That Record Signing Doesn't Stop
&lt;/h1&gt;

&lt;p&gt;A few days ago I published &lt;a href="https://dev.to/yashksaini/dev-log-9-hardening-kademlia-dht-and-automating-the-neovim-grind-43f3"&gt;Dev Log #9&lt;/a&gt;, which covered a security fix I landed in py-libp2p: binding a signed PeerRecord to its signer's identity so an attacker can't relay another peer's record as their own.&lt;/p&gt;

&lt;p&gt;Then this comment came in from Valentyn Kit, a systems engineer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Binding the record to its signer kills the forged-record path, nice. The attack that tends to show up right after in Kademlia is eclipse/Sybil, where nobody forges anything: libp2p IDs are already pubkey-derived, but nothing makes it costly to mint a batch of valid IDs that land closest to a target key and quietly own that slice of the routing table. Curious whether py-libp2p has appetite for the S/Kademlia counters (crypto-puzzle IDs + disjoint lookup paths), or if that's considered out of scope for the DHT."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is one of those comments where someone hands you a full threat model in two sentences. I spent the next few days digging into it — reading the &lt;a href="https://doi.org/10.1109/NCA.2007.1" rel="noopener noreferrer"&gt;S/Kademlia paper&lt;/a&gt;, tracing through py-libp2p's routing code, and checking what rust-libp2p has already shipped. Here's what I found.&lt;/p&gt;




&lt;h2&gt;
  
  
  What my fix actually closed
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/libp2p/py-libp2p/pull/1338" rel="noopener noreferrer"&gt;PR #1338&lt;/a&gt; closed the &lt;strong&gt;record-forgery path&lt;/strong&gt;. Before the fix, an attacker could inject a signed PeerRecord into the DHT claiming to represent peer X — even without X's private key. The fix ties the envelope's signer identity to the record payload, so &lt;code&gt;maybe_consume_signed_record&lt;/code&gt; now rejects any record where those don't match.&lt;/p&gt;

&lt;p&gt;That's a real fix. But Valentyn is pointing at something it doesn't touch at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  The attack that signing doesn't stop
&lt;/h2&gt;

&lt;p&gt;Here's the thing: the eclipse/Sybil attack doesn't need to forge anything.&lt;/p&gt;

&lt;p&gt;A libp2p peer ID is &lt;code&gt;multihash(pubkey)&lt;/code&gt;. Generating a keypair takes microseconds. There is nothing preventing anyone from generating 10,000 valid, spec-compliant peer IDs in a few seconds.&lt;/p&gt;

&lt;p&gt;Now here's why that matters for Kademlia. Kademlia routes by XOR distance — every node's routing table stores the peers closest to it in XOR space. For any target key &lt;code&gt;T&lt;/code&gt;, I can generate keypairs in bulk, compute each one's XOR distance to &lt;code&gt;T&lt;/code&gt;, and keep the ones that land closest. With enough trials I get 20 IDs that cluster tighter around &lt;code&gt;T&lt;/code&gt; than any honest node. I connect them to the network, let them bootstrap normally, and now every honest node's routing table has my attacker nodes occupying the "closest to &lt;code&gt;T&lt;/code&gt;" slots.&lt;/p&gt;

&lt;p&gt;From that point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any &lt;code&gt;FIND_NODE(T)&lt;/code&gt; returns my nodes as the nearest&lt;/li&gt;
&lt;li&gt;Any &lt;code&gt;GET_VALUE(T)&lt;/code&gt; routes through my nodes first&lt;/li&gt;
&lt;li&gt;My nodes return each other as "even closer" — it's a closed loop&lt;/li&gt;
&lt;li&gt;The honest node has no way to detect this&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And critically: &lt;strong&gt;every message is signed by a valid keypair&lt;/strong&gt;. The fix I landed passes all my attacker messages just fine, because they're all authentic. The attack lives in the routing layer, not the record layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  What S/Kademlia proposes
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://doi.org/10.1109/NCA.2007.1" rel="noopener noreferrer"&gt;S/Kademlia paper (Baumgart &amp;amp; Mies, 2007)&lt;/a&gt; defines two specific counters for this class of attack.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Crypto-puzzle IDs
&lt;/h3&gt;

&lt;p&gt;Instead of just &lt;code&gt;PeerID = H(pubkey)&lt;/code&gt;, require that the ID also satisfies a proof-of-work constraint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PeerID = H(pubkey)
H(H(pubkey), nonce) &amp;lt; D     # PoW puzzle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To generate an ID that lands near a specific target in XOR space, an attacker now has to solve &lt;code&gt;2^d&lt;/code&gt; extra puzzles per attempt. Minting thousands of targeted IDs goes from milliseconds to hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem: this is a dead end for py-libp2p.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;libp2p peer IDs are specified uniformly across all implementations — &lt;a href="https://github.com/libp2p/go-libp2p" rel="noopener noreferrer"&gt;go-libp2p&lt;/a&gt;, &lt;a href="https://github.com/libp2p/rust-libp2p" rel="noopener noreferrer"&gt;rust-libp2p&lt;/a&gt;, &lt;a href="https://github.com/libp2p/js-libp2p" rel="noopener noreferrer"&gt;js-libp2p&lt;/a&gt;, nim-libp2p. They're all &lt;code&gt;multihash(pubkey)&lt;/code&gt; with no additional constraints. If py-libp2p started rejecting peers without PoW IDs it would disconnect from every other node in the ecosystem. This needs a spec-level change with buy-in from every implementation. Not something one library can ship unilaterally.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Disjoint lookup paths
&lt;/h3&gt;

&lt;p&gt;This one is different — and it's in scope.&lt;/p&gt;

&lt;p&gt;Instead of one iterative lookup, run &lt;code&gt;d ≥ 2&lt;/code&gt; &lt;strong&gt;independent&lt;/strong&gt; lookups, each starting from a disjoint partition of your initial closest-peer set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Path 1 starts from peers &lt;code&gt;[p1, p2, ..., pk/2]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Path 2 starts from peers &lt;code&gt;[pk/2+1, ..., pk]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Peers discovered on path 1 are never introduced to path 2's candidate set, and vice versa&lt;/li&gt;
&lt;li&gt;Results merge only at the end&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To eclipse this, an attacker now needs to dominate &lt;em&gt;both&lt;/em&gt; paths simultaneously. With &lt;code&gt;d = 2&lt;/code&gt; and bucket size &lt;code&gt;k = 20&lt;/code&gt;, that's 40 targeted IDs instead of 20. With &lt;code&gt;d = 3&lt;/code&gt; it's 60. Each path multiplies the cost linearly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The important thing: this requires zero protocol changes.&lt;/strong&gt; It uses the existing &lt;code&gt;FIND_NODE&lt;/code&gt; wire format. Other nodes don't need to implement it. It's a pure algorithmic change to how the lookup query is structured — which means it's entirely within the scope of a single-library contribution to py-libp2p.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where py-libp2p stands today
&lt;/h2&gt;

&lt;p&gt;I went through the routing code carefully. Here's the honest state:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single-path iterative lookup&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/libp2p/py-libp2p/blob/main/libp2p/kad_dht/peer_routing.py" rel="noopener noreferrer"&gt;&lt;code&gt;kad_dht/peer_routing.py&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Implemented — one candidate set, converges on a single path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;k-bucket with liveness eviction&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/libp2p/py-libp2p/blob/main/libp2p/kad_dht/routing_table.py" rel="noopener noreferrer"&gt;&lt;code&gt;kad_dht/routing_table.py&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Implemented — pings oldest peer before evicting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sliding-window ALPHA concurrency&lt;/td&gt;
&lt;td&gt;&lt;code&gt;peer_routing.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Implemented — I shipped this in &lt;a href="https://github.com/libp2p/py-libp2p/pull/1274" rel="noopener noreferrer"&gt;PR #1274&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eclipse attack simulation&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/libp2p/py-libp2p/tree/main/tests/examples/attack_simulation/eclipse_attack" rel="noopener noreferrer"&gt;&lt;code&gt;tests/examples/attack_simulation/eclipse_attack/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Simulation only — fake string IDs, never touches the real routing table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sybil attack simulation&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/libp2p/py-libp2p/tree/main/tests/examples/attack_simulation/sybil_attack" rel="noopener noreferrer"&gt;&lt;code&gt;tests/examples/attack_simulation/sybil_attack/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Simulation only — IDs are Python strings like &lt;code&gt;"peer_sybil_0"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IP/subnet diversity in k-buckets&lt;/td&gt;
&lt;td&gt;&lt;code&gt;routing_table.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Missing&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disjoint lookup paths&lt;/td&gt;
&lt;td&gt;&lt;code&gt;peer_routing.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Missing&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any actual Eclipse/Sybil defense&lt;/td&gt;
&lt;td&gt;anywhere&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Missing&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the one that matters. The simulation code — merged under &lt;a href="https://github.com/libp2p/py-libp2p/issues/57" rel="noopener noreferrer"&gt;issue #57&lt;/a&gt; via &lt;a href="https://github.com/libp2p/py-libp2p/pull/950" rel="noopener noreferrer"&gt;PR #950&lt;/a&gt; — shows what an attack looks like but doesn't implement any counter-measure. The &lt;code&gt;MaliciousPeer&lt;/code&gt; class in &lt;code&gt;malicious_peer.py&lt;/code&gt; creates fake IDs as Python strings; it never touches a real &lt;code&gt;KBucket&lt;/code&gt; or &lt;code&gt;RoutingTable&lt;/code&gt;. And issue #57 itself is still open — PR #950 never formally closed it.&lt;/p&gt;

&lt;p&gt;The gap between "we have simulations" and "we have defenses" is exactly where the contribution lives.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I want to build
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For py-libp2p: disjoint lookup paths
&lt;/h3&gt;

&lt;p&gt;The target is &lt;code&gt;find_closest_peers_network&lt;/code&gt; in &lt;a href="https://github.com/libp2p/py-libp2p/blob/main/libp2p/kad_dht/peer_routing.py" rel="noopener noreferrer"&gt;&lt;code&gt;peer_routing.py&lt;/code&gt;&lt;/a&gt;. Right now it maintains one shared &lt;code&gt;closest_peers&lt;/code&gt; list and one shared &lt;code&gt;queried_peers&lt;/code&gt; set. All ALPHA-concurrent queries feed into the same pool. If an attacker owns the closest-k slots for a target, every round of the lookup converges deeper into the eclipse.&lt;/p&gt;

&lt;p&gt;The change I'm planning:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Partition the initial closest-peer set into &lt;code&gt;d&lt;/code&gt; disjoint subsets (configurable, default &lt;code&gt;d = 1&lt;/code&gt; to preserve current behaviour)&lt;/li&gt;
&lt;li&gt;Spawn &lt;code&gt;d&lt;/code&gt; parallel but isolated iterative lookups — each with its own candidate list and queried-peers tracker&lt;/li&gt;
&lt;li&gt;No peer crosses from one path to another mid-lookup&lt;/li&gt;
&lt;li&gt;Merge results by XOR distance at the end&lt;/li&gt;
&lt;li&gt;Expose &lt;code&gt;disjoint_paths: int = 1&lt;/code&gt; on &lt;code&gt;KadDHT.__init__&lt;/code&gt; so users opt in to &lt;code&gt;d = 2&lt;/code&gt; or &lt;code&gt;d = 3&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Files involved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/libp2p/py-libp2p/blob/main/libp2p/kad_dht/peer_routing.py" rel="noopener noreferrer"&gt;&lt;code&gt;libp2p/kad_dht/peer_routing.py&lt;/code&gt;&lt;/a&gt; — the lookup refactor&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/libp2p/py-libp2p/blob/main/libp2p/kad_dht/kad_dht.py" rel="noopener noreferrer"&gt;&lt;code&gt;libp2p/kad_dht/kad_dht.py&lt;/code&gt;&lt;/a&gt; — expose the parameter&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/libp2p/py-libp2p/blob/main/libp2p/kad_dht/common.py" rel="noopener noreferrer"&gt;&lt;code&gt;libp2p/kad_dht/common.py&lt;/code&gt;&lt;/a&gt; — add &lt;code&gt;DISJOINT_LOOKUP_PATHS = 1&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This would be the first actual eclipse mitigation (not simulation) in py-libp2p.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secondary: IP/subnet diversity in k-buckets
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;KBucket.add_peer&lt;/code&gt; in &lt;a href="https://github.com/libp2p/py-libp2p/blob/main/libp2p/kad_dht/routing_table.py" rel="noopener noreferrer"&gt;&lt;code&gt;routing_table.py&lt;/code&gt;&lt;/a&gt;, before accepting a new peer, check whether its &lt;code&gt;/24&lt;/code&gt; subnet already has &lt;code&gt;MAX_PEERS_PER_SUBNET&lt;/code&gt; entries. If yes, reject rather than evict an existing peer. This forces an eclipse attacker to control IPs across many different subnets — not just many keypairs from a single VPS.&lt;/p&gt;

&lt;p&gt;go-libp2p enforces &lt;code&gt;/16&lt;/code&gt; subnet diversity. rust-libp2p currently has nothing at this level (more on that below). ~30–50 lines of Python.&lt;/p&gt;




&lt;h2&gt;
  
  
  What rust-libp2p has — and what it's missing
&lt;/h2&gt;

&lt;p&gt;I checked &lt;a href="https://github.com/libp2p/rust-libp2p/tree/master/protocols/kad/src" rel="noopener noreferrer"&gt;rust-libp2p's Kademlia implementation&lt;/a&gt; while researching this, because it's the most production-hardened libp2p implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The good news:&lt;/strong&gt; rust-libp2p has had disjoint lookup paths since &lt;a href="https://github.com/libp2p/rust-libp2p/blob/master/protocols/kad/CHANGELOG.md" rel="noopener noreferrer"&gt;v0.20.0 (2020)&lt;/a&gt;. The config flag is &lt;a href="https://docs.rs/libp2p-kad/latest/libp2p_kad/struct.Config.html#method.disjoint_query_paths" rel="noopener noreferrer"&gt;&lt;code&gt;disjoint_query_paths(true)&lt;/code&gt;&lt;/a&gt; on &lt;code&gt;libp2p_kad::Config&lt;/code&gt;. The number of paths equals the &lt;code&gt;set_parallelism()&lt;/code&gt; value (ALPHA).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bad news:&lt;/strong&gt; it's opt-in, and almost no application enables it explicitly. More importantly, rust-libp2p has &lt;strong&gt;no IP/subnet diversity in &lt;a href="https://github.com/libp2p/rust-libp2p/blob/master/protocols/kad/src/kbucket.rs" rel="noopener noreferrer"&gt;&lt;code&gt;kbucket.rs&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; at all. The &lt;code&gt;kbucket_inserts&lt;/code&gt; strategy only controls whether connected or disconnected peers are preferred — not IP locality. An attacker with a /24 full of targeted IDs faces zero additional resistance.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;py-libp2p&lt;/th&gt;
&lt;th&gt;rust-libp2p&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Disjoint lookup paths&lt;/td&gt;
&lt;td&gt;Missing&lt;/td&gt;
&lt;td&gt;Present, opt-in, off by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IP/subnet diversity&lt;/td&gt;
&lt;td&gt;Missing&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Missing&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eclipse attack testbed (real nodes)&lt;/td&gt;
&lt;td&gt;Simulation only&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Rust project: &lt;code&gt;kad-eclipse-guard&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Because rust-libp2p is missing the subnet diversity piece, and because I want empirical data before writing the py-libp2p PR, I'm planning a standalone Rust crate that uses &lt;a href="https://github.com/libp2p/rust-libp2p" rel="noopener noreferrer"&gt;rust-libp2p&lt;/a&gt; as a dependency.&lt;/p&gt;

&lt;p&gt;The idea: build a real testbed that proves the attack works, shows what each defense does to the success rate, and ships a &lt;code&gt;SubnetDiversityFilter&lt;/code&gt; behaviour that can eventually be proposed upstream.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[package]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"kad-eclipse-guard"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;edition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2021"&lt;/span&gt;

&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;libp2p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.55"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"kad"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"tcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"noise"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"yamux"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"identify"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"macros"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;tokio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"full"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;tracing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1"&lt;/span&gt;
&lt;span class="py"&gt;tracing-subscriber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"env-filter"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;clap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"derive"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;serde&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"derive"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;serde_json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;
&lt;span class="py"&gt;ipnet&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2"&lt;/span&gt;
&lt;span class="py"&gt;rand&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.8"&lt;/span&gt;
&lt;span class="py"&gt;sha2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.10"&lt;/span&gt;
&lt;span class="py"&gt;libp2p-identity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What it builds
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Targeted ID generator&lt;/strong&gt; (&lt;code&gt;src/attack/targeted_id_gen.rs&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;Generates Ed25519 keypairs whose &lt;code&gt;PeerId&lt;/code&gt; lands XOR-closest to a target key. This is the core of the attack proof — 100k keypair trials in ~200ms, take the 20 closest to target T, done.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;TargetedIdGenerator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;target_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PeerId&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;best_n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;TargetedIdGenerator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Keypair&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Subnet diversity filter&lt;/strong&gt; (&lt;code&gt;src/hardening/subnet_diversity.rs&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;NetworkBehaviour&lt;/code&gt; wrapper around &lt;code&gt;libp2p_kad::Behaviour&lt;/code&gt; that enforces per-subnet caps at the &lt;code&gt;NetworkBehaviour&lt;/code&gt; composition layer — no changes needed to upstream kad internals.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;SubnetDiversityFilter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TStore&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;libp2p_kad&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Behaviour&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TStore&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;subnet_counts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IpNet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_per_subnet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// default: 2 per /24&lt;/span&gt;
    &lt;span class="n"&gt;prefix_len_v4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// default: 24&lt;/span&gt;
    &lt;span class="n"&gt;prefix_len_v6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// default: 64&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;libp2p_kad&lt;/code&gt; fires &lt;code&gt;RoutingUpdated&lt;/code&gt; for a new peer, the filter checks the peer's IP subnet against &lt;code&gt;subnet_counts&lt;/code&gt;. If the subnet is saturated, it emits &lt;code&gt;RemovePeer&lt;/code&gt; back to the inner behaviour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testbed&lt;/strong&gt; (&lt;code&gt;src/testbed/network.rs&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;Runs 4 configurations with real rust-libp2p nodes over TCP:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;&lt;code&gt;disjoint_query_paths&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;SubnetDiversityFilter&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disjoint only&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diversity only&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardened&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Metrics collected per config: routing table contamination ratio, lookup success rate, time-to-eclipse (TTFE).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;USAGE:
    kad-eclipse-guard [OPTIONS]

OPTIONS:
&lt;/span&gt;&lt;span class="gp"&gt;    --honest-peers &amp;lt;N&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;default: 50]
&lt;span class="gp"&gt;    --attacker-peers &amp;lt;M&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;default: 30]
&lt;span class="gp"&gt;    --trials &amp;lt;T&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;keypair generation trials &lt;span class="o"&gt;[&lt;/span&gt;default: 100000]
&lt;span class="gp"&gt;    --config &amp;lt;C&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;baseline|disjoint|diversity|hardened|all
&lt;span class="gp"&gt;    --output &amp;lt;FILE&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;JSON metrics output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What this proves
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The attack is cheap.&lt;/strong&gt; 100k keypair trials → 20 targeted IDs in ~200ms. No special hardware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disjoint paths help but multiply rather than block.&lt;/strong&gt; With &lt;code&gt;parallelism=3&lt;/code&gt; and disjoint enabled, the attacker needs 3× the IDs — still microseconds per ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subnet diversity raises the cost meaningfully.&lt;/strong&gt; Filling a bucket of 20 across /24 caps of 2 requires IPs in at least 10 different subnets — that's a real operational barrier, not a CPU barrier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combined is strongest.&lt;/strong&gt; Attacker must simultaneously control many subnets AND eclipse all &lt;code&gt;d&lt;/code&gt; independent paths.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What's off the table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Idea&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Crypto-puzzle peer IDs&lt;/td&gt;
&lt;td&gt;Requires spec change + buy-in from all libp2p implementations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reputation / trust scoring&lt;/td&gt;
&lt;td&gt;No spec, breaks with churn, too complex to get right&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proof-of-work ID validation&lt;/td&gt;
&lt;td&gt;Would reject every non-py-libp2p peer on the network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DHT access control&lt;/td&gt;
&lt;td&gt;Contradicts libp2p's open-participation design&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The contribution path
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Build and benchmark &lt;code&gt;kad-eclipse-guard&lt;/code&gt; with real rust-libp2p nodes — get hard numbers.&lt;/li&gt;
&lt;li&gt;Open a &lt;a href="https://github.com/libp2p/rust-libp2p" rel="noopener noreferrer"&gt;rust-libp2p&lt;/a&gt; discussion proposing &lt;code&gt;SubnetDiversityFilter&lt;/code&gt; as a companion behaviour or eventual built-in, backed by the testbed metrics.&lt;/li&gt;
&lt;li&gt;Open a &lt;a href="https://github.com/libp2p/py-libp2p" rel="noopener noreferrer"&gt;py-libp2p&lt;/a&gt; issue for disjoint lookup paths, referencing &lt;a href="https://github.com/libp2p/py-libp2p/issues/57" rel="noopener noreferrer"&gt;issue #57&lt;/a&gt; — gauge maintainer appetite before writing code.&lt;/li&gt;
&lt;li&gt;Once there's signal, open the PR. Disjoint paths first (higher impact, no protocol change), subnet diversity second (smaller, easier to review standalone).&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Valentyn's comment identified something real: the fix I shipped in &lt;a href="https://github.com/libp2p/py-libp2p/pull/1338" rel="noopener noreferrer"&gt;PR #1338&lt;/a&gt; closes one attack surface but leaves the routing layer undefended. Record authentication and routing table hardening are separate problems, and py-libp2p currently has zero actual defenses for the second one — only simulations that never touch the real &lt;code&gt;KBucket&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The path forward is concrete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disjoint lookup paths in &lt;code&gt;peer_routing.py&lt;/code&gt; — in scope, no protocol changes, the biggest impact&lt;/li&gt;
&lt;li&gt;Subnet diversity in &lt;code&gt;routing_table.py&lt;/code&gt; — smaller, modeled on go-libp2p's approach&lt;/li&gt;
&lt;li&gt;A Rust testbed to build the empirical case first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/libp2p/py-libp2p/issues/57" rel="noopener noreferrer"&gt;Issue #57&lt;/a&gt; has been open since 2018. It asked for attack simulation (done, via &lt;a href="https://github.com/libp2p/py-libp2p/pull/950" rel="noopener noreferrer"&gt;PR #950&lt;/a&gt;) and security assessment (not done). The actual hardening is still waiting.&lt;/p&gt;

</description>
      <category>kademlia</category>
      <category>libp2p</category>
      <category>rust</category>
      <category>security</category>
    </item>
    <item>
      <title>Shipping Docs, Rust Tooling, and a Week of Deep Reviews</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Tue, 07 Jul 2026 15:01:14 +0000</pubDate>
      <link>https://dev.to/yashksaini/shipping-docs-rust-tooling-and-a-week-of-deep-reviews-9i6</link>
      <guid>https://dev.to/yashksaini/shipping-docs-rust-tooling-and-a-week-of-deep-reviews-9i6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;14 commits, 2 PRs, 1 issue, and 5 reviews across 3 repos. This week was all about hardening my Rust CLI 'trx' with mdBook and format checks, while doing some heavy lifting on Python backend reviews for the community.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;It’s been a solid, high-energy week. I managed to hit a 7-day commit streak, which always feels great when the flow state actually sticks. The stats tell a story of expansion: 14 commits, 2 merged PRs, and 5 deep-dive reviews. I spent most of my "writing" time in Rust, specifically getting the documentation and CI for &lt;code&gt;trx&lt;/code&gt; into a professional state, but my "thinking" time was heavily invested in Python backend architecture through some gnarly code reviews.&lt;/p&gt;

&lt;h2&gt;
  
  
  WHAT I BUILT
&lt;/h2&gt;

&lt;p&gt;The star of the show this week was &lt;strong&gt;trx&lt;/strong&gt;, my Rust-based project. I’ve been pushing this one forward for a while, but this week I realized that code without documentation is just a riddle for my future self. I spent a significant chunk of time setting up a proper documentation site using mdBook. &lt;/p&gt;

&lt;p&gt;If you look at the numbers, I added over 1,500 lines and only deleted about 160. Usually, I’m a fan of the "less is more" refactoring approach, but this was a pure growth week. Most of those additions were the &lt;a href="https://github.com/pie-314/trx/pull/22" rel="noopener noreferrer"&gt;mdBook documentation site&lt;/a&gt;. I wanted a place where users (and I) could actually see the vision for the tool, and mdBook is just so snappy for Rust projects. It feels right.&lt;/p&gt;

&lt;p&gt;I also spent some time on the "boring but important" stuff for &lt;code&gt;trx&lt;/code&gt;. I &lt;a href="https://github.com/pie-314/trx/pull/13" rel="noopener noreferrer"&gt;integrated format checks&lt;/a&gt; into the workflow. There’s nothing that kills my momentum faster than a PR that fails because of a stray space or a missing newline, so getting &lt;code&gt;cargo fmt&lt;/code&gt; enforced in CI was a priority. It’s one of those things you do once and thank yourself for every day after.&lt;/p&gt;

&lt;p&gt;Outside of the Rust world, I did some maintenance on my &lt;strong&gt;nvim&lt;/strong&gt; config and &lt;strong&gt;GitBanner&lt;/strong&gt;. For &lt;code&gt;nvim&lt;/code&gt;, it was mostly Lua-based tweaks to keep my environment feeling fast. I’m a firm believer that your editor should evolve with your workflow, so I’m constantly pruning and grafting my config. &lt;code&gt;GitBanner&lt;/code&gt; saw a couple of TypeScript commits as well—just keeping the lights on and ensuring everything is running smoothly.&lt;/p&gt;

&lt;h2&gt;
  
  
  PULL REQUESTS
&lt;/h2&gt;

&lt;p&gt;I kept the PR count low but the impact high this week. Both of my PRs were in the &lt;code&gt;trx&lt;/code&gt; repo and both are now merged. &lt;/p&gt;

&lt;p&gt;The first was the &lt;a href="https://github.com/pie-314/trx/pull/22" rel="noopener noreferrer"&gt;documentation overhaul&lt;/a&gt;. Adding 1,281 lines of documentation might sound like a slog, but it actually helped me find a few inconsistencies in how I was thinking about the CLI's API. Writing about your code is the best way to realize where your code is confusing.&lt;/p&gt;

&lt;p&gt;The second was the &lt;a href="https://github.com/pie-314/trx/pull/13" rel="noopener noreferrer"&gt;format check PR&lt;/a&gt;. It was a mix of 292 additions and 161 deletions. This wasn't just adding a YAML file for GitHub Actions; it was also about running the formatter across the whole codebase and cleaning up the technical debt that had accumulated while I was "just hacking." It feels much cleaner now.&lt;/p&gt;

&lt;h2&gt;
  
  
  ISSUES &amp;amp; DISCUSSIONS
&lt;/h2&gt;

&lt;p&gt;I opened one new issue this week in &lt;code&gt;trx&lt;/code&gt;: &lt;a href="https://github.com/pie-314/trx/issues/14" rel="noopener noreferrer"&gt;Feature: Add a update keymap option&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;As I was writing the documentation, I realized that the current way I handle keybindings is a bit too rigid. I want users to be able to rebind things without having to recompile the whole world. It’s currently marked as OPEN, and it’s likely going to be my main focus for the upcoming weekend. It’s one of those features that sounds simple until you start thinking about conflict resolution and nested maps.&lt;/p&gt;

&lt;h2&gt;
  
  
  CODE REVIEWS
&lt;/h2&gt;

&lt;p&gt;This was a heavy review week, and honestly, I love that. I gave 5 reviews, which is more than double the number of PRs I opened myself. Most of this work happened over at &lt;strong&gt;opensre&lt;/strong&gt;, a project I’ve been following closely.&lt;/p&gt;

&lt;p&gt;I spent a lot of time looking at Python code this week. One of the more interesting reviews was for a fix in the &lt;a href="https://github.com/Tracer-Cloud/opensre/pull/2047" rel="noopener noreferrer"&gt;Anthropic SDK client&lt;/a&gt;. We were seeing some weird &lt;code&gt;TypeError&lt;/code&gt; issues when an API key was missing, and the PR introduced a "fail fast" mechanism. I’m a huge advocate for failing fast—there’s nothing worse than a silent failure that manifests as a cryptic error ten layers deep in your stack.&lt;/p&gt;

&lt;p&gt;I also reviewed some &lt;a href="https://github.com/Tracer-Cloud/opensre/pull/1424" rel="noopener noreferrer"&gt;AWS session caching logic&lt;/a&gt; and a &lt;a href="https://github.com/Tracer-Cloud/opensre/pull/1905" rel="noopener noreferrer"&gt;psutil probe integration&lt;/a&gt;. The &lt;code&gt;psutil&lt;/code&gt; one was particularly cool—it’s wiring a background sampler into a REPL. It’s the kind of low-level systems work that makes Python feel a lot more powerful than people give it credit for.&lt;/p&gt;

&lt;p&gt;Reviewing other people's code is such a great way to stay sharp. You see patterns you wouldn't have thought of, and you catch bugs that you’ve definitely made yourself in the past. (We’ve all forgotten to validate a host boundary at least once, right? Looking at you, &lt;a href="https://github.com/Tracer-Cloud/opensre/pull/2009" rel="noopener noreferrer"&gt;PR #2009&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  TECH STACK
&lt;/h2&gt;

&lt;p&gt;This week was a polyglot's dream. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Rust&lt;/strong&gt; was my primary language for building, especially with the &lt;code&gt;trx&lt;/code&gt; work. The type safety and the tooling (like &lt;code&gt;cargo fmt&lt;/code&gt; and &lt;code&gt;mdBook&lt;/code&gt;) just make the developer experience so top-tier.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Python&lt;/strong&gt; dominated my review time. Between LLM clients and AWS SDKs, I was deep in the weeds of asynchronous Python and cloud infrastructure.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;TypeScript&lt;/strong&gt; and &lt;strong&gt;Lua&lt;/strong&gt; provided the glue for my personal tools and editor setup. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My add/delete ratio was heavily skewed toward additions (1573 vs 161), which makes sense given the documentation push. It wasn't a "cleanup" week; it was a "foundation" week. And hitting that 7-day streak? That’s the cherry on top. It’s not about the grind; it’s about the rhythm.&lt;/p&gt;

&lt;h2&gt;
  
  
  WHAT'S NEXT
&lt;/h2&gt;

&lt;p&gt;Next week is going to be all about that keymap feature for &lt;code&gt;trx&lt;/code&gt;. I want to get the configuration logic sorted so I can finally move it out of the "experimental" phase. I’m also planning to dive back into some of the open issues in the &lt;code&gt;opensre&lt;/code&gt; repo—there’s some interesting work happening around agent reliability that I want to be a part of.&lt;/p&gt;

&lt;p&gt;Until then, keep shipping. (And maybe run &lt;code&gt;cargo fmt&lt;/code&gt; before you push—your reviewers will thank you).&lt;/p&gt;




&lt;p&gt;4 min read · Generated 2026-05-17 by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>python</category>
      <category>documentation</category>
      <category>consistency</category>
    </item>
    <item>
      <title>Dev log #6 Networking Deep Dives and Scaling Docs: My 30k Line Week in OSS</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Tue, 07 Jul 2026 14:59:01 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-6-networking-deep-dives-and-scaling-docs-my-30k-line-week-in-oss-3pbb</link>
      <guid>https://dev.to/yashksaini/dev-log-6-networking-deep-dives-and-scaling-docs-my-30k-line-week-in-oss-3pbb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;16 commits, 13 PRs, 4 issues, and 13 reviews across 7 repos. This week was a massive balancing act between deep p2p networking fixes in Python, scaling documentation with mdBook, and a heavy rotation of SRE code reviews.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;It’s been one of those weeks where the line count looks absolutely ridiculous on paper—nearly 30,000 additions—but the reality was a mix of high-level architectural setup and low-level bug hunting. I pushed 16 commits, opened 13 PRs, and stayed in the flow with a perfect 7-day streak. The bulk of the volume came from launching a new documentation site, but the real "brain power" went into debugging IPv6 4-tuple handling in &lt;code&gt;py-libp2p&lt;/code&gt; and refining AI-ready structured outputs for &lt;code&gt;screenpipe&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Big Push: The Rust Book Simplified
&lt;/h2&gt;

&lt;p&gt;If you look at my stats and see +29,945 lines, don't worry—I didn't suddenly become a superhuman typing machine. Most of that was the heavy lifting involved in setting up &lt;a href="https://github.com/yashksaini-coder/the-rust-book-simplified" rel="noopener noreferrer"&gt;the-rust-book-simplified&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;I’ve been wanting to make the Rust book more accessible, and this week I finally sat down to build the infrastructure for it. I started by setting up &lt;code&gt;mdBook&lt;/code&gt; with a custom theme and a landing page that doesn't just look like a standard doc site. In &lt;a href="https://github.com/yashksaini-coder/the-rust-book-simplified/pull/1" rel="noopener noreferrer"&gt;PR #1&lt;/a&gt; and &lt;a href="https://github.com/yashksaini-coder/the-rust-book-simplified/pull/2" rel="noopener noreferrer"&gt;PR #2&lt;/a&gt;, I focused on the themed landing and playground integration. &lt;/p&gt;

&lt;p&gt;There was a bit of a learning curve with the GitHub Pages deployment—I actually had to revert and drop a test step in &lt;a href="https://github.com/yashksaini-coder/the-rust-book-simplified/pull/4" rel="noopener noreferrer"&gt;PR #4&lt;/a&gt; because the CI was being finicky with the &lt;code&gt;mdbook&lt;/code&gt; binary. By &lt;a href="https://github.com/yashksaini-coder/the-rust-book-simplified/pull/5" rel="noopener noreferrer"&gt;PR #5&lt;/a&gt;, I had the core content merged and the site live. It’s a net-positive week for the Rust community, and honestly, seeing those green checkmarks on a fresh docs site is incredibly satisfying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deep in the Networking Weeds: py-libp2p
&lt;/h2&gt;

&lt;p&gt;When I wasn't playing with CSS and Markdown, I was back in my natural habitat: p2p networking. I’ve been contributing to &lt;code&gt;py-libp2p&lt;/code&gt; for a while, and this week I hit a particularly gnarly issue with IPv6.&lt;/p&gt;

&lt;p&gt;I opened &lt;a href="https://github.com/libp2p/py-libp2p/issues/1316" rel="noopener noreferrer"&gt;Issue #1316&lt;/a&gt; because the websocket listener was failing to bind on IPv6. The culprit? A classic &lt;code&gt;getsockname()&lt;/code&gt; unpack error. When you're dealing with IPv4, you expect a 2-tuple (address, port). But IPv6 gives you a 4-tuple (address, port, flow info, scope id). If your code isn't expecting those extra two values, everything blows up. I submitted the fix in &lt;a href="https://github.com/libp2p/py-libp2p/pull/1317" rel="noopener noreferrer"&gt;PR #1317&lt;/a&gt;, ensuring we handle the 4-tuple correctly.&lt;/p&gt;

&lt;p&gt;I also spent some time refactoring the test suite. In &lt;a href="https://github.com/libp2p/py-libp2p/pull/1315" rel="noopener noreferrer"&gt;PR #1315&lt;/a&gt;, I moved the &lt;code&gt;subscribed_mesh&lt;/code&gt; fixture to a predicate-based readiness check. If you've ever worked on p2p tests, you know how flaky they can be—waiting for peers to discover each other and settle into a mesh is a recipe for race conditions. Moving to a predicate-based approach makes the tests much more resilient. (And yes, I also closed &lt;a href="https://github.com/libp2p/py-libp2p/issues/1314" rel="noopener noreferrer"&gt;Issue #1314&lt;/a&gt; to remove the &lt;code&gt;nursery&lt;/code&gt; parameter from &lt;code&gt;IListener.listen()&lt;/code&gt;, which was a long-overdue breaking change to clean up the API).&lt;/p&gt;

&lt;h2&gt;
  
  
  SRE Tooling and AI Contexts
&lt;/h2&gt;

&lt;p&gt;Over at &lt;code&gt;opensre&lt;/code&gt;, I was tackling a bug where Azure SQL integrations weren't being populated correctly. It’s one of those silent failures where the tools report as "unavailable" even when the config is perfect. I tracked it down to &lt;code&gt;detect_sources.py&lt;/code&gt; and pushed a fix in &lt;a href="https://github.com/Tracer-Cloud/opensre/pull/722" rel="noopener noreferrer"&gt;PR #722&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;I also had a "developer moment" with Grafana. I opened &lt;a href="https://github.com/Tracer-Cloud/opensre/pull/786" rel="noopener noreferrer"&gt;PR #786&lt;/a&gt; to use relative provisioning paths in the local compose setup, realized I’d missed a side effect, and immediately had to revert it in &lt;a href="https://github.com/Tracer-Cloud/opensre/pull/787" rel="noopener noreferrer"&gt;PR #787&lt;/a&gt;. We've all been there—the "fix" that breaks the dev environment for everyone else. Better to revert and rethink than to leave the team stranded.&lt;/p&gt;

&lt;p&gt;On the AI front, I’ve been working on &lt;a href="https://github.com/screenpipe/screenpipe" rel="noopener noreferrer"&gt;screenpipe&lt;/a&gt;. I’m really excited about the Model Context Protocol (MCP) work I’m doing there. In &lt;a href="https://github.com/screenpipe/screenpipe/pull/3072" rel="noopener noreferrer"&gt;PR #3072&lt;/a&gt;, I’m building out a structured output format specifically for AI consumption. The goal is to make the data coming out of screenpipe easily "digestible" for LLMs without a ton of brittle parsing logic. It’s still open, but the foundation is solid.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reviewer's Hat
&lt;/h2&gt;

&lt;p&gt;This week was heavy on the mentorship and review side. I gave 13 reviews, which is almost a 1:1 ratio with my own PRs. Most of my time was spent in the &lt;code&gt;opensre&lt;/code&gt; repo, helping the team move faster.&lt;/p&gt;

&lt;p&gt;I reviewed everything from Victoria Logs integrations (&lt;a href="https://github.com/Tracer-Cloud/opensre/pull/663" rel="noopener noreferrer"&gt;PR #663&lt;/a&gt;) to Apache Airflow evidence gathering (&lt;a href="https://github.com/Tracer-Cloud/opensre/pull/570" rel="noopener noreferrer"&gt;PR #570&lt;/a&gt;). One of the more interesting reviews was &lt;a href="https://github.com/Tracer-Cloud/opensre/pull/780" rel="noopener noreferrer"&gt;PR #780&lt;/a&gt;, which dealt with redacting overlapping spans in guardrails. It’s a tricky logic problem—ensuring you don't leak sensitive "bookends" of data when two redacted sections overlap. &lt;/p&gt;

&lt;p&gt;I also pushed for some better project structure, requesting changes on &lt;a href="https://github.com/Tracer-Cloud/opensre/pull/945" rel="noopener noreferrer"&gt;PR #945&lt;/a&gt; to move the Notion client into a dedicated services directory. Keeping the architecture clean now saves us a world of hurt six months down the line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack &amp;amp; Vibe
&lt;/h2&gt;

&lt;p&gt;This was a very Python-heavy week, but the "lines changed" tell a story of two different worlds. On one hand, I was doing high-volume work in Rust and TypeScript for the docs and AI layers. On the other, I was doing surgical, low-line-count fixes in Python and C (shoutout to the 1 commit in &lt;code&gt;PiEngine&lt;/code&gt;—sometimes you just need to tweak one thing in the core).&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;nvim&lt;/code&gt; config also got some love with 7 commits. No major additions there, just constant sharpening of the saw. If my editor isn't feeling right, my code doesn't feel right.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Language Mix:&lt;/strong&gt; Python (89M bytes processed), TypeScript, and Rust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add/Delete Ratio:&lt;/strong&gt; Massive additions due to the &lt;code&gt;mdBook&lt;/code&gt; launch. It’s a "building" phase, not a "cleaning" phase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streak:&lt;/strong&gt; 7 days. The momentum is real.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;Next week, I’m focusing on getting that MCP structured output merged in &lt;code&gt;screenpipe&lt;/code&gt;. I also want to circle back to &lt;code&gt;py-libp2p&lt;/code&gt; and see if there are more IPv6 edge cases hiding in the other transport layers. If this week was about breadth (docs, networking, SRE, AI), next week is going to be about depth. &lt;/p&gt;

&lt;p&gt;Catch you on the next push!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Total Commits: 16 | PRs: 13 | Issues: 4 | Reviews: 13&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Streak: 7 days of shipping.&lt;/em&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  python #rust #open-source #backend #consistency · 4 min read · Generated 2026-04-26 by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;
&lt;/h1&gt;

</description>
      <category>python</category>
      <category>rust</category>
      <category>opensource</category>
      <category>backend</category>
    </item>
    <item>
      <title>Dev log #11 Kanagawa Dreams and Test Suites: A Week of Refactoring my Neovim and Hardening the Backend</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Tue, 07 Jul 2026 04:30:43 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-11-kanagawa-dreams-and-test-suites-a-week-of-refactoring-my-neovim-and-hardening-the-nb3</link>
      <guid>https://dev.to/yashksaini/dev-log-11-kanagawa-dreams-and-test-suites-a-week-of-refactoring-my-neovim-and-hardening-the-nb3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Hit a perfect 7-day streak this week, splitting my time between a massive aesthetic pivot in my Neovim config and a relentless testing marathon for openslate. With 22 commits and 10 PRs, I managed to delete over 2,000 lines of code while shipping a more stable integration layer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;There is something deeply satisfying about a week that ends with a net-positive refactor and a perfect 7-day streak. I spent the last seven days oscillating between the purely aesthetic—tearing down and rebuilding my Neovim environment—and the purely functional—writing enough integration tests for &lt;code&gt;openslate&lt;/code&gt; to sleep soundly at night. Between 22 commits and 10 PRs, I touched everything from Lua config files to Svelte components and network retry logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Neovim Pivot (&lt;code&gt;nvim&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;If you’re like me, your editor is a living organism. This week, I decided it was time for a major evolution. I pushed 21 commits to my &lt;code&gt;nvim&lt;/code&gt; repo, which resulted in about 3,200 additions and 2,000 deletions. That’s a lot of churn for a config, but the result was worth it: I’ve fully pivoted to a "Kanagawa modern-minimal" aesthetic. &lt;/p&gt;

&lt;p&gt;I spent a significant chunk of time in &lt;code&gt;lua/plugins&lt;/code&gt; and &lt;code&gt;lua/config&lt;/code&gt;, cleaning up my &lt;code&gt;&amp;lt;leader&amp;gt;&lt;/code&gt; notation and ensuring the UI felt snappy. I even updated the title logo and added a site HTML file to the repo. Most of the heavy lifting was actually maintenance—I have a CI job that updates all plugins to the latest versions, and I spent a few sessions manually resolving the fallout from those updates. There’s a specific kind of peace that comes from applying whitespace and lint fixes across 42 changed files. It’s the digital equivalent of vacuuming your office; you don’t realize how much the clutter was bothering you until it’s gone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hardening the Core (&lt;code&gt;openslate&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;While the Neovim work was about "feel," my work on &lt;code&gt;openslate&lt;/code&gt; was all about "fact." I’m currently in the middle of a massive testing push. Even though the commit count looks low on the main branch, the PR activity tells the real story. I’m building out a robust integration suite to cover the entire application lifecycle.&lt;/p&gt;

&lt;p&gt;I spent a lot of time in &lt;code&gt;api/src&lt;/code&gt;, specifically focusing on authentication. I merged a critical PR that asserts the auth middleware returns a 401 when no cookie is present—it’s a small thing, but it’s the foundation of the whole security model. I also have several open PRs right now that cover the signup and signin flows, ensuring that the first user created actually gets the correct cookie back. &lt;/p&gt;

&lt;p&gt;One of the trickier parts of the week was working on the &lt;code&gt;build_fts_query&lt;/code&gt; logic. Full-text search (FTS) is always a bit of a nightmare when it comes to wildcard and quoting rules. I opened a PR specifically to test those edge cases because I noticed some weird behavior when users tried to search with special characters. Better to catch that in a test suite than in a bug report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull Requests &amp;amp; Pipeline Fixes
&lt;/h2&gt;

&lt;p&gt;This was a heavy PR week. I opened 10 in total, and they really represent the "breadth" of my current projects.&lt;/p&gt;

&lt;p&gt;Over at &lt;code&gt;opensre&lt;/code&gt;, I tackled a frustrating UI bug. I noticed that the interactive shell loading bar wasn't spinning while the LLM was doing its investigation. There’s nothing worse than staring at a static screen wondering if the backend has died or if it’s just thinking. I submitted a fix with about 150 lines of changes to ensure the progress state is correctly reflected in the terminal. Even though that PR is currently closed, the investigation into why the state wasn't bubbling up was a great deep dive into the shell's event loop.&lt;/p&gt;

&lt;p&gt;I also spent some time on &lt;code&gt;DevNotion&lt;/code&gt; (the very tool I'm using to track this!). I opened a PR to handle transient network errors during the "harvest" phase. When you're pulling data from multiple APIs, something is bound to flake out. Adding a retry strategy and a secure deployment plan (about 800 lines of new code) makes the whole system feel much more "production-ready" and less like a weekend hack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tech Stack &amp;amp; The Streak
&lt;/h2&gt;

&lt;p&gt;Looking at the language breakdown for the week, it’s a bit of a polyglot explosion. While Lua dominated my commit count because of the Neovim refactor, the actual logic was spread across TypeScript, Svelte, and a bit of Python. &lt;/p&gt;

&lt;p&gt;The additions-to-deletions ratio (3226 vs 2063) is exactly where I like it to be during a refactor week. It shows that I’m not just piling on new features; I’m actively removing the old, brittle parts of the codebase to make room for the new stuff. &lt;/p&gt;

&lt;p&gt;And then there’s the streak. Seven straight days of commits. It wasn't a forced grind; it was just one of those weeks where every time I sat down, there was something clear and actionable to do. Whether it was a 2am tweak to a Kanagawa color highlight or a mid-afternoon integration test for a media MIME filter, the momentum stayed high.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Next week is all about closing the loop. I have a mountain of open PRs in &lt;code&gt;openslate&lt;/code&gt;—everything from the command palette styling to the media DB filters. My goal is to get the integration test suite merged so I can start building out the next set of features on top of a "green" CI pipeline.&lt;/p&gt;

&lt;p&gt;I’m also planning to dive deeper into the &lt;code&gt;DevNotion&lt;/code&gt; harvest logic. Now that the retry strategy is in place, I want to see if I can optimize the data fetching to be a bit more parallel. &lt;/p&gt;

&lt;p&gt;Until then, I’ll be enjoying the new, minimal look of my editor. It’s amazing how a fresh coat of paint can make you want to write another thousand lines of code. See you all next week!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Yash K Saini&lt;/strong&gt; — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Generated by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;

</description>
      <category>neovim</category>
      <category>productivity</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Dev log #10 The Power of the Zero-Commit Week: Stepping Back to Move Faster</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Fri, 03 Jul 2026 04:49:00 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-10-the-power-of-the-zero-commit-week-stepping-back-to-move-faster-1e5h</link>
      <guid>https://dev.to/yashksaini/dev-log-10-the-power-of-the-zero-commit-week-stepping-back-to-move-faster-1e5h</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A rare week of total radio silence on GitHub—no commits, no PRs, and zero green squares. Sometimes the best thing for a codebase is for the developer to step away from the keyboard and focus on the bigger picture.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;If you looked at my GitHub contribution graph for this past week, you’d see a whole lot of nothing. Zero commits, zero pull requests, and a broken streak. It wasn't a failure of productivity, but a conscious choice to trade the keyboard for a whiteboard and some much-needed headspace; I finished the week with 0 lines added and 0 lines deleted, but a much clearer roadmap for what's coming next.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sound of Silence
&lt;/h2&gt;

&lt;p&gt;I’ve always been a bit of a "green square" addict. As a freelancer and active open-source contributor, there’s a certain dopamine hit that comes with shipping a PR or seeing a complex CI pipeline finally turn green. But this week, for the first time in a long while, I didn't touch a single repository. &lt;/p&gt;

&lt;p&gt;There’s a specific kind of pressure that comes with being deep into p2p networking and backend tooling. When you're contributing to projects like libp2p or maintaining your own CLI tools, the "to-do" list never actually ends. There is always a dependency to bump, a bug to triage, or a refactor that's been nagging at the back of your brain. However, I’ve learned the hard way that if you don't intentionally schedule a "zero week," your brain will eventually schedule one for you in the form of burnout.&lt;/p&gt;

&lt;p&gt;This week was about breaking the cycle of reactive coding. Instead of jumping into the &lt;code&gt;src&lt;/code&gt; directory of my latest Python API or wrestling with borrow checker errors in my Rust crates, I spent my time reading, planning, and—dare I say it—resting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reflecting on the Stack
&lt;/h2&gt;

&lt;p&gt;Even though I didn't push code, looking at my language distribution gives a pretty clear picture of where my head is usually at. My local environment is a graveyard of half-finished experiments across a massive range of technologies. &lt;/p&gt;

&lt;p&gt;Python remains my heavy hitter, sitting at over 100MB of source code across my various projects. It’s my go-to for data pipelines and AI backend work because of the sheer speed of iteration. But when I’m looking for safety and performance, especially in the p2p space, I’m almost always reaching for Rust or TypeScript. Seeing nearly 12MB of Rust and 16MB of TypeScript in my stats reminds me of the shift I've made lately toward more memory-safe, robust systems.&lt;/p&gt;

&lt;p&gt;I also noticed a significant amount of Nix, Lua, and even some OCaml and Nim in my historical data. It’s a reminder that being a developer isn't just about the code you write today; it's about the cumulative knowledge of all the paradigms you've explored. This week, instead of writing more Python, I spent time reading through some OCaml documentation and exploring how different languages handle concurrency. It’s the kind of "invisible work" that doesn't show up on a contribution graph but fundamentally changes how I'll approach my next PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Invisible" Work
&lt;/h2&gt;

&lt;p&gt;We often talk about "shipping" as the only metric that matters. But what about the work that happens before the first line of code is written? &lt;/p&gt;

&lt;p&gt;I spent a good chunk of this week thinking about architecture. I have a few open ideas for some CLI tools that help with developer experience (DX), and I realized that if I had started coding them on Monday, I would have built them the wrong way. By stepping back, I realized I was over-complicating the state management. I didn't need a complex database; I needed a simple, flat-file structure that stayed out of the user's way. &lt;/p&gt;

&lt;p&gt;This is why I’m not sweating the 0% merge rate or the lack of reviews this week. As a developer, your value isn't just in your ability to type; it's in your ability to solve problems. Sometimes, the best solution is the code you decide &lt;em&gt;not&lt;/em&gt; to write.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack &amp;amp; Consistency
&lt;/h2&gt;

&lt;p&gt;Usually, I’m bragging about a 7-day streak or a high review-to-PR ratio. This week, the streak is at zero. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Language Mix:&lt;/strong&gt; My history is polyglot by nature. While Python, Rust, and TypeScript dominate the byte count, the presence of things like Solidity, Go, and even some C++ shows the breadth of the projects I’ve touched. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add/Delete Ratio:&lt;/strong&gt; 0/0. It’s a perfect balance, if a bit boring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review Balance:&lt;/strong&gt; No reviews given, no reviews received. This was a week of total internal focus.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It feels strange to see "0 commits" at the top of a dev log, but I think it’s important to be honest about it. We aren't machines. We don't just produce code at a constant rate until we're decommissioned. &lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The "rest" phase is officially over. My brain is already starting to itch for a terminal window. &lt;/p&gt;

&lt;p&gt;Next week, I’m planning to dive back into the TypeScript and Rust side of my portfolio. I have some ideas for hardening a few SDKs I've been working on, and the architectural clarity I gained this week is going to make that process much smoother. I’m also looking at some of the issues I left hanging in my Python repos—now that I’ve had a break, the solutions to those "impossible" bugs are starting to feel a lot more obvious.&lt;/p&gt;

&lt;p&gt;To anyone else feeling the pressure of the empty contribution graph: it’s okay to have a quiet week. The code will still be there on Monday. The goal isn't to have the most green squares; the goal is to build things that actually matter, and sometimes that requires stepping away from the screen.&lt;/p&gt;

&lt;p&gt;See you all in the commit logs next week.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Yash K Saini&lt;/strong&gt; — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Generated by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;

</description>
      <category>devjournal</category>
      <category>travel</category>
    </item>
    <item>
      <title>Dev log #9 Hardening Kademlia DHT and automating the Neovim grind</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Fri, 03 Jul 2026 03:48:03 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-9-hardening-kademlia-dht-and-automating-the-neovim-grind-43f3</link>
      <guid>https://dev.to/yashksaini/dev-log-9-hardening-kademlia-dht-and-automating-the-neovim-grind-43f3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Seven days of flow. Fixed a peer identity binding issue in py-libp2p, automated my Neovim lockfile merges, and added a massive batch of notes on xv6 and Category Theory. 10 commits and a solid PR in the works.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I managed to hit a perfect seven-day streak this week, balancing some deep-dive p2p networking work with necessary maintenance on my local environment. The highlight was opening a PR in &lt;code&gt;py-libp2p&lt;/code&gt; to tighten up how PeerRecords are handled in the Kademlia DHT. On the side, I spent time automating the annoying parts of my Neovim config and dumping a fresh batch of notes into my knowledge base. 10 commits, 204 lines added, and a much cleaner workflow to show for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Neovim Configuration &amp;amp; CI
&lt;/h3&gt;

&lt;p&gt;I’m a firm believer that your editor should work for you, not the other way around. My &lt;code&gt;nvim&lt;/code&gt; repo saw a lot of action this week—9 commits in total—but most of it was under-the-hood maintenance. I’ve been leaning on Lua to keep things snappy, and this week was about ensuring my plugin ecosystem doesn't rot.&lt;/p&gt;

&lt;p&gt;I pushed several updates to keep plugins at their latest versions, but the real "quality of life" improvement was adding a chore to auto-resolve &lt;code&gt;lazy-lock.json&lt;/code&gt; merge conflicts. If you’ve ever worked on your Neovim config across multiple machines, you know the headache of the lockfile drifting. I set up a flow to prioritize incoming changes, which saves me from manually triaging JSON diffs every time I pull from &lt;code&gt;dev&lt;/code&gt;. It’s a small tweak, but it removes a recurring friction point in my daily flow. I also spent time cleaning up the root of the config, with about 37 additions and 33 deletions—refactoring is a constant process when you live in your terminal.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Knowledge Base
&lt;/h3&gt;

&lt;p&gt;I also put some serious time into &lt;code&gt;main-notes&lt;/code&gt;. I’m currently going deep on a few different subjects, and I use this repo as my "second brain." I added 167 lines of new material across 13 files, covering a pretty diverse range of topics: xv6 (the re-implementation of Unix V6), Category Theory, HQSE, and the Rust Handbook. &lt;/p&gt;

&lt;p&gt;Writing notes for xv6 is always a trip because it forces you to think about the C-level reality of operating systems. It’s a nice contrast to the high-level Python and TypeScript work I do during the day. Most of these additions were in the &lt;code&gt;Books&lt;/code&gt; and &lt;code&gt;Books/Covers&lt;/code&gt; directories, effectively building out a digital library that I can reference whenever I’m stuck on a systems design problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull Requests
&lt;/h2&gt;

&lt;p&gt;The most significant piece of code I wrote this week is currently sitting as an open PR in &lt;code&gt;py-libp2p&lt;/code&gt;. I've been looking at how we handle the Kademlia Distributed Hash Table (DHT), specifically regarding security and record integrity.&lt;/p&gt;

&lt;p&gt;The PR, titled &lt;code&gt;fix(kad-dht): bind signed PeerRecord to its signer identity&lt;/code&gt;, addresses a subtle but important issue in how peer records are validated. In a p2p network, you can’t just trust data because it exists; you have to verify the source. By explicitly binding the signed PeerRecord to the identity of the signer, we’re making the DHT more resilient against certain types of spoofing or stale data propagation. It was 103 lines of Python that touched the core logic of the DHT implementation. It’s still open for review, and I’m looking forward to the feedback from the other libp2p contributors. There’s something uniquely satisfying about contributing to the foundational layers of the decentralized web.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;This was a polyglot week, as is usually the case when you're jumping between open-source contributions and personal tooling.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Python:&lt;/strong&gt; This was the heavy lifter for the libp2p work. Python remains my go-to for quick prototyping in the p2p space, even if the eventual goal is often a Rust or Go implementation.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Lua:&lt;/strong&gt; Purely for the Neovim environment. Lua’s integration into the Nvim ecosystem has made it so much easier to build custom logic for things like that lockfile auto-resolution I mentioned earlier.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;C:&lt;/strong&gt; This showed up in the context of my xv6 notes. Even when I’m not writing production C code, reading and documenting it is essential for keeping my systems knowledge sharp.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The Numbers:&lt;/strong&gt; I added 204 lines and deleted 33. It wasn't a massive "tear it all down" refactoring week, but rather one of steady growth and precise fixes. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 7-day streak is what I’m most proud of, though. It wasn’t about grinding out 12-hour days; it was about showing up every single day to move the needle, whether that was a complex p2p fix or just documenting a chapter of a book I’m reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Next week, I’m hoping to get that &lt;code&gt;py-libp2p&lt;/code&gt; PR merged. Once that’s landed, I want to look at more edge cases in the Python DHT implementation—there’s always more to harden. I’m also planning to dive deeper into the Category Theory notes. It’s abstract stuff, but the more I read, the more I see how those patterns emerge in functional programming and API design. &lt;/p&gt;

&lt;p&gt;On the tooling side, my Neovim config feels stable for now, but I might look into optimizing my CI pipeline further to handle plugin updates more gracefully without cluttering my commit history. Until then, I'll be in the terminal. See you on GitHub!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Yash K Saini&lt;/strong&gt; — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Generated by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;

</description>
      <category>neovim</category>
      <category>linux</category>
      <category>network</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Dev log #8 Hardening the Orchestrator: A Week of Making dev-publish Resilient</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Fri, 03 Jul 2026 03:47:12 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-8-hardening-the-orchestrator-a-week-of-making-dev-publish-resilient-14lh</link>
      <guid>https://dev.to/yashksaini/dev-log-8-hardening-the-orchestrator-a-week-of-making-dev-publish-resilient-14lh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Spent the week deep-diving into my dev-publish tool, focusing on durability and orchestrator resilience. 21 commits across two repos, with a massive cleanup of the publishing logic and some much-needed architecture documentation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;There is a specific kind of satisfaction that comes from taking a tool you use every day and finally giving it the "production-grade" treatment it deserves. This week was exactly that. I spent most of my time in the guts of &lt;code&gt;dev-publish&lt;/code&gt;, moving past the "it works on my machine" phase and into "it works even if the world is on fire" territory. With 21 commits and over 11,000 lines of code churn, I focused on making the publishing orchestrator resilient and the state durable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;The star of the show this week was &lt;a href="https://github.com/yashksaini-coder/dev-publish" rel="noopener noreferrer"&gt;dev-publish&lt;/a&gt;. If you’ve ever tried to automate cross-platform technical writing, you know that the edge cases are where the real pain lives. I pushed 16 commits here, touching about 45 files. The diff was pretty wild: +6,926 additions and -4,289 deletions. That net positive tells part of the story, but the deletions represent me ripping out brittle logic that just wasn't cutting it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hardening the Orchestrator
&lt;/h3&gt;

&lt;p&gt;The biggest win was a massive fix to make the publish state durable and the orchestrator resilient. In the previous iteration, if a network request to an API (like Dev.to) failed halfway through a multi-platform push, the state was... let's just say "vague." I spent a lot of time in &lt;code&gt;src&lt;/code&gt; ensuring that the orchestrator can now pick up where it left off. I also documented the published-flag semantics and re-run resilience in the README. It sounds like a small thing, but knowing that a re-run won't accidentally double-post your article is a huge weight off my mind.&lt;/p&gt;

&lt;p&gt;I also spent some time on the "boring but important" stuff. I normalized how tags are handled to make them safer across different platforms and implemented a much stricter resolution for cover images. If a local image is required but missing, the tool now yells at you immediately rather than failing silently mid-upload.&lt;/p&gt;

&lt;p&gt;One of my favorite commits this week was &lt;code&gt;refactor: drop unused assetsDir config&lt;/code&gt;. There is nothing quite like the feeling of deleting configuration options that you realized were over-engineered and unnecessary. It keeps the surface area small and the DX (developer experience) clean.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture and Docs
&lt;/h3&gt;

&lt;p&gt;I’m a firm believer that if you can't draw it, you don't understand it. I spent a chunk of time in &lt;code&gt;docs/diagrams&lt;/code&gt; and actually embedded an architecture diagram directly into the README. It helps me visualize the flow between the local markdown files, the image resolver, and the final API calls. &lt;/p&gt;

&lt;p&gt;I also did a specific dive into the Dev.to integration. I added better validation for their API responses and exposed a properly typed error system. No more guessing why a request failed—now I get a clear, typed reason that the orchestrator can actually act upon.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keeping the Saw Sharp
&lt;/h3&gt;

&lt;p&gt;Outside of the heavy lifting on &lt;code&gt;dev-publish&lt;/code&gt;, I spent a bit of time on my &lt;a href="https://github.com/yashksaini-coder/nvim" rel="noopener noreferrer"&gt;nvim&lt;/a&gt; config. This wasn't anything groundbreaking—mostly just keeping the environment healthy. I have a set of CI workflows that update all my plugins to the latest versions. It’s five quick commits that keep my editor snappy and ensure I’m not missing out on any new features or bug fixes in the Lua ecosystem. It’s the digital equivalent of cleaning your desk at the end of the day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull Requests &amp;amp; Issues
&lt;/h2&gt;

&lt;p&gt;You might notice there are zero PRs this week. Since I’m the primary driver on these specific projects right now, I’ve been pushing directly to the main branches to keep the velocity high. It’s a bit of a "move fast and fix things" approach, but since I’m also the one writing the tests and the docs, the quality bar stays where it needs to be. No issues opened or discussions started either—just pure, unadulterated flow state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;This was a very TypeScript-heavy week. When you're building CLI tools and orchestrators, the type safety of TS is basically a superpower. Being able to define exactly what a "PublishState" looks like and having the compiler yell at me when I forget an edge case is the only way I stay sane.&lt;/p&gt;

&lt;p&gt;The line count this week was pretty high (+6,935 / -4,298), which usually signals a mix of new feature work and heavy refactoring. I managed to maintain a 3-day streak, which felt good. It wasn't a "grind" week, but rather a few days of deep, focused work where I could really get into the zone.&lt;/p&gt;

&lt;p&gt;Even though my total language profile shows a massive amount of Python and Rust from past projects, this week was all about the TypeScript and Lua. It’s a reminder that as a dev, your "current" stack is often just whatever tool is best for the problem you're solving right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Now that the orchestrator in &lt;code&gt;dev-publish&lt;/code&gt; is stable and the state is durable, I want to start looking at expanding the platform support. The foundation is there, the architecture is documented, and the error handling is typed. &lt;/p&gt;

&lt;p&gt;I’m also keeping an eye on my Neovim setup. Those automated plugin updates sometimes introduce small breaking changes in the Lua APIs, so I might need to spend a morning next week tweaking my &lt;code&gt;init.lua&lt;/code&gt; to accommodate any upstream shifts.&lt;/p&gt;

&lt;p&gt;It was a solid week of building. Nothing beats the feeling of seeing a tool you built yourself get faster, stronger, and more reliable. Catch you in the next update!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Yash K Saini&lt;/strong&gt; — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Generated by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Dev log #7 Reviving DevNotion: 10,000 Lines, Multi-LLM Support, and the Road to v2.1</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Tue, 23 Jun 2026 15:14:06 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-1-reviving-devnotion-10000-lines-multi-llm-support-and-the-road-to-v21-47hl</link>
      <guid>https://dev.to/yashksaini/dev-log-1-reviving-devnotion-10000-lines-multi-llm-support-and-the-road-to-v21-47hl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Spent the week breathing new life into DevNotion—59 commits and over 10,000 lines of code later, v2.1 is officially alive. It was a massive push toward multi-LLM support and public-facing dashboards, keeping a steady 6-day streak in the process.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I spent the week breathing new life into DevNotion—59 commits and over 10,000 lines of code later, v2.1 is officially alive. It was a massive push toward multi-LLM support and public-facing dashboards, keeping a steady 6-day streak in the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Most of my energy this week went into a massive revival of &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;. I’ve been sitting on some ideas for a while, but this week I finally sat down and tore into the codebase to bring v2 and v2.1 to life. We’re talking 150 files changed and a delta of +10,199/-3,768 lines. That’s a lot of TypeScript, but the result feels like a completely different beast.&lt;/p&gt;

&lt;p&gt;The core of the work was moving toward a more robust, multi-LLM architecture. I wanted the ability to flip between models and handle the preview-to-publish flow more gracefully. I spent a significant chunk of time in &lt;code&gt;src/server&lt;/code&gt; rebuilding how we handle sessions and public access. One of the more satisfying bits was implementing a global brute-force backstop for the auth layer—it’s one of those "peace of mind" features that you don't realize you need until you're actually looking at a public deploy.&lt;/p&gt;

&lt;p&gt;Speaking of public deploys, I spent some time in the root directory getting the project ready for the real world. I wrote a proper Dockerfile and set up a Render blueprint. I also had to make sure the app honors the platform’s &lt;code&gt;$PORT&lt;/code&gt; environment variable (classic "it works on my machine but fails in the cloud" moment). &lt;/p&gt;

&lt;p&gt;The dashboard also got a major overhaul. I added a public view and implemented token-gated actions for publishing, running, and deleting content. It makes the whole "dashboard as a service" feel much more real. I even took a moment to "un-mangle" the &lt;code&gt;.gitignore&lt;/code&gt; because my patterns for &lt;code&gt;.remember/&lt;/code&gt; and the masterplan were getting a bit messy. &lt;/p&gt;

&lt;p&gt;On a much smaller scale, I kept my &lt;a href="https://github.com/yashksaini-coder/nvim" rel="noopener noreferrer"&gt;nvim&lt;/a&gt; config fresh. Nothing ground-breaking there—just five quick commits to keep all my plugins updated. It’s a small ritual, but keeping the tools sharp is half the battle when you're doing 10k-line weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull Requests
&lt;/h2&gt;

&lt;p&gt;The centerpiece of the week was definitely &lt;a href="https://github.com/yashksaini-coder/DevNotion/pull/1" rel="noopener noreferrer"&gt;PR #1 in DevNotion&lt;/a&gt;: "DevNotion v2 + v2.1 — multi-LLM, preview→approve publishing, images, dashboard &amp;amp; landing." &lt;/p&gt;

&lt;p&gt;Merging this felt like a massive weight off my shoulders. It wasn't just a feature update; it was a total revival. It introduced the multi-LLM logic, the new image handling, and the entire landing page. With over 8,000 additions in this single PR, it was basically the foundation for everything I’m planning next. Merging it on Saturday was the perfect way to cap off the week's momentum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;This was a TypeScript-heavy week, no doubt about it. When you're building out complex dashboard logic and server-side auth, TS is just non-negotiable for me. I also spent some time with Lua for my editor config and a bit of Dockerfile/Shell work to get the deployment pipeline smoothed out.&lt;/p&gt;

&lt;p&gt;The line count this week is pretty wild—over 10,000 additions. Usually, I'm a big fan of "deletion weeks" where I refactor and prune, but this was a week of pure creation. I was building out net-new features and expanding the scope of the project, so the heavy addition-to-deletion ratio feels right for where the project is at.&lt;/p&gt;

&lt;p&gt;I also managed a 6-day streak. I missed the perfect 7-day run by a hair, but honestly, the flow was so good that it didn't feel like a grind. When you’re deep in the "revival" phase of a project you care about, the commits just kind of happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Now that the v2.1 foundation is merged and the Docker/Render setup is live, I’m looking at the docs. I’ve already started updating the README with new architecture diagrams (swapped out some old Mermaid flowcharts for something a bit cleaner) and fresh dashboard images. &lt;/p&gt;

&lt;p&gt;Next week, I want to battle-test the public view logic. Now that the token-gated actions are in place, I want to see how it holds up under some actual usage. There’s also some more work to do on the "masterplan" (now that I’ve fixed the gitignore for it!). I'm feeling really good about the direction DevNotion is heading—it’s finally starting to match the vision I had in my head months ago. Catch you in the next update!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Yash K Saini&lt;/strong&gt; — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Generated by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;

</description>
      <category>devjournal</category>
      <category>llm</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Revived DevNotion (multi agent blog workflow) — And Finally Perfected it</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Sat, 06 Jun 2026 13:58:54 +0000</pubDate>
      <link>https://dev.to/yashksaini/i-revived-devnotion-multi-agent-blog-workflow-and-finally-perfected-it-411p</link>
      <guid>https://dev.to/yashksaini/i-revived-devnotion-multi-agent-blog-workflow-and-finally-perfected-it-411p</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-05-21"&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Every Monday standup, someone asks: &lt;em&gt;"What'd you ship last week?"&lt;/em&gt; And every Monday I freeze. Did I merge that PR Tuesday or Wednesday? Was the gnarly part in the harvest code or the publisher? I'd sit there scrolling my own commit history like it belonged to a stranger.&lt;/p&gt;

&lt;p&gt;So back in March I built something to answer the question for me. &lt;strong&gt;&lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;&lt;/strong&gt; — a &lt;a href="https://mastra.ai" rel="noopener noreferrer"&gt;Mastra&lt;/a&gt; pipeline that reads a week of my GitHub activity, writes it up as a first-person blog post, and ships it to Notion and DEV.to. It won &lt;strong&gt;$500&lt;/strong&gt; in the Notion MCP Challenge. Then I did what everyone does with a hackathon project: nothing, for two months.&lt;/p&gt;

&lt;p&gt;Here's the thing, though. It worked, but I never fully trusted it. One LLM, no say in it. It published the instant it finished writing — no preview, no edit, no "hang on, let me read that first." And when I opened the repo back up for the Finish-Up-A-Thon, the very first run failed in the most embarrassing way I can think of. Honestly? Best thing that could've happened. It showed me exactly what "finish this" was supposed to mean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DevNotion v2&lt;/strong&gt; is the version I should've shipped the first time. It writes a draft, shows it to me, lets me edit it, and &lt;em&gt;only&lt;/em&gt; publishes when I say go. Multiple LLM providers. Three publishing targets. A deterministic cover image, much better data, a real test suite. Seven phases, 51 tests, a one-command setup wizard — and, most of all, a failure mode that protects my name instead of betting it.&lt;/p&gt;

&lt;p&gt;One idea ran under the whole rebuild: a tool that writes in your voice and posts under your name has to be trustworthy before it gets to be convenient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;github.com/yashksaini-coder/DevNotion&lt;/a&gt;&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;
        yashksaini-coder
      &lt;/a&gt; / &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;
        DevNotion
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      DevNotion is an agentic server built using 3 mastra agents to work as a pipeline to publish weekly blogs on Notion Pages &amp;amp; DEV.to 
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;DevNotion&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Turn your weekly GitHub activity into a polished, illustrated blog post — narrated by an LLM, reviewed by you, and published to Notion, DEV.to, and Hashnode.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DevNotion is a &lt;a href="https://mastra.ai" rel="nofollow noopener noreferrer"&gt;Mastra&lt;/a&gt; pipeline of specialist agents: it harvests a week of your GitHub work, narrates it into a first-person post, generates images, and lets you preview/edit before it publishes anywhere. Originally built for the DEV.to × Notion MCP Challenge (where it won $500); &lt;strong&gt;v2&lt;/strong&gt; was rebuilt for the &lt;a href="https://dev.to/challenges/github-2026-05-21" rel="nofollow"&gt;GitHub Finish-Up-A-Thon&lt;/a&gt; with multi-LLM support, a review dashboard, image generation, richer harvesting, and a fail-safe publishing flow.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What's new in v2&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;v1&lt;/th&gt;
&lt;th&gt;v2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LLM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gemini only&lt;/td&gt;
&lt;td&gt;Gemini · OpenAI · Anthropic (provider abstraction; default &lt;code&gt;gemini-3-flash-preview&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Publish targets&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Notion + DEV.to&lt;/td&gt;
&lt;td&gt;Notion + DEV.to + &lt;strong&gt;Hashnode&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flow&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;generate → publish immediately&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;generate → preview → edit → approve → publish&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;none (CLI/cron)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;web dashboard&lt;/strong&gt; (trigger, preview, edit, history)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Images&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;deterministic stats&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&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.amazonaws.com%2Fuploads%2Farticles%2Ff2zds547d4aqf68cgzx6.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.amazonaws.com%2Fuploads%2Farticles%2Ff2zds547d4aqf68cgzx6.png" alt="Dashboard run history — dark UI, status badges (Preview Ready / Published), Review &amp;amp; Publish actions" width="800" height="377"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fblikw1h9n5nivwqqpz7n.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.amazonaws.com%2Fuploads%2Farticles%2Fblikw1h9n5nivwqqpz7n.png" alt="Preview &amp;amp; edit screen — the generated post in an editable markdown box, an Approve &amp;amp; Publish button, and a stats grid" width="799" height="398"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fguc8z5bi9duc5g3u6su0.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.amazonaws.com%2Fuploads%2Farticles%2Fguc8z5bi9duc5g3u6su0.png" alt="Weekly stats card used as the cover — " width="800" height="420"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2F9vs4ctqrse1cbs8dkuc9.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.amazonaws.com%2Fuploads%2Farticles%2F9vs4ctqrse1cbs8dkuc9.png" alt="Public landing page" width="799" height="391"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2F2p5edyiq3kkch9awd7rp.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.amazonaws.com%2Fuploads%2Farticles%2F2p5edyiq3kkch9awd7rp.png" alt="Architecture — three agents, generation split from publishing by a human approval gate" width="799" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three specialist agents, with generation and publishing deliberately split by a human approval gate:&lt;/p&gt;

&lt;p&gt;Each agent does one thing and only one thing. &lt;strong&gt;Harvest&lt;/strong&gt; is pure data — two quick GraphQL/REST passes over the week, no LLM anywhere near it. &lt;strong&gt;Narrate&lt;/strong&gt; is the single model call: raw data goes in, a first-person post comes out, in whatever tone you picked. &lt;strong&gt;Publish&lt;/strong&gt; is plumbing — a Notion page, a DEV.to draft, a Hashnode draft if you've set it up, plus the cover and the footer. And right in the middle, between writing and shipping, there's a human. That gate is the whole reason v2 exists.&lt;/p&gt;

&lt;p&gt;The rule I kept from v1: an LLM only shows up where it earns its spot. Harvesting the data, drawing the stats card, pushing to each platform — that's all plain deterministic code. No tokens, no rate limits, no chance of the thing inventing a number. The model writes the prose. Everything around it is code I can unit-test. That's the only reason I'm comfortable letting it post under my name without re-reading every figure first.&lt;/p&gt;

&lt;p&gt;In practice it's quick. I kick off a run (scheduled, or by hand from the dashboard) and a few seconds later there's a draft sitting there marked &lt;strong&gt;Preview Ready&lt;/strong&gt;. Not published. I read it, fix a sentence or two right in the browser, hit &lt;strong&gt;Approve &amp;amp; Publish&lt;/strong&gt;. &lt;em&gt;Then&lt;/em&gt; it goes out — and even then it goes out as a draft on each platform, not a live post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Comeback Story
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Where v1 actually was
&lt;/h3&gt;

&lt;p&gt;I genuinely thought I'd left v1 in decent shape. The first run set me straight in about four seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Narrate step: LLM call failed: You exceeded your current quota …
  limit: 0, model: gemini-2.0-flash
Publish: Created Notion page: https://app.notion.com/…
Publish: Published DEV.to article: https://dev.to/…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Read it twice. The narration &lt;strong&gt;failed&lt;/strong&gt; — and the thing &lt;strong&gt;published anyway&lt;/strong&gt;. v1 had this well-meaning "always produce a blog" fallback, so a quota error quietly shipped a bare stats stub to my real DEV.to account. For a tool whose entire job is to represent my work? That's about the worst thing it could possibly do.&lt;/p&gt;

&lt;p&gt;That one log was hiding three separate bugs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wrong model, wired to nothing.&lt;/strong&gt; The live workflow read a different config key than the one I'd been carefully setting. So it quietly fell back to a &lt;em&gt;retired&lt;/em&gt; &lt;code&gt;gemini-2.0-flash&lt;/code&gt;, whose free quota is now &lt;code&gt;0&lt;/code&gt;. I'd been turning a knob connected to thin air.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The failure was silent.&lt;/strong&gt; A &lt;code&gt;try/catch&lt;/code&gt; swapped in a deterministic stub and marched straight on to publishing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing ever let me look.&lt;/strong&gt; No preview, no gate, no pause before it went live.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And a sneakier one underneath all that: a week with 13 direct commits reported &lt;strong&gt;&lt;code&gt;+0/-0&lt;/code&gt; lines changed&lt;/strong&gt;, because line stats were summed from pull requests only. The posts came out technically accurate and completely dead.&lt;/p&gt;

&lt;p&gt;What actually bothered me wasn't any single bug. It was that the &lt;em&gt;worst&lt;/em&gt; one was the helpful one. That &lt;code&gt;try/catch&lt;/code&gt;-into-a-stub got added in v1 on purpose, to guarantee a post always shipped. And that good intention is exactly what turned a recoverable quota error into a published embarrassment. Turns out "always succeed" is the wrong goal for something that talks for you. "Never lie" is the right one.&lt;/p&gt;

&lt;p&gt;So I rebuilt it in seven phases. Each one got a quick spec, a plan, the actual work, a review, and a green test run before I let myself start the next. Not ceremony — it's the fail-loud rule pointed at my own process. Every phase had to prove itself (types clean, tests green) before anything got stacked on top, so nothing could quietly rot three phases deep. Here's how it went.&lt;/p&gt;
&lt;h3&gt;
  
  
  Phase 1 — Make narration trustworthy
&lt;/h3&gt;

&lt;p&gt;First, one place to choose the model, defaulting to &lt;code&gt;gemini-3-flash-preview&lt;/code&gt; (which, unlike the thing it replaced, actually has a free tier). Then the part that matters most — narration that &lt;strong&gt;fails loud&lt;/strong&gt;:&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/llm/narrate.ts&lt;/span&gt;
&lt;span class="c1"&gt;// FAIL-LOUD: throws on provider error or unparseable output. NEVER returns a&lt;/span&gt;
&lt;span class="c1"&gt;// deterministic fallback — the caller decides what to do with a failure.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;narrateBlog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt; &lt;span class="o"&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;system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildNarratorSystemPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tone&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;casual&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;focusAreas&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;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Generate a blog post from this GitHub contribution data:\n\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&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;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxTokens&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Gemini 3 is a thinking model — small budgets starve the answer&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;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseFrontmatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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="o"&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;success&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;`Narration failed: &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;error&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;return&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blog&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;If narration throws, the step throws, the run is marked &lt;strong&gt;failed&lt;/strong&gt;, and the publish step never runs. A quota error can't reach a single reader anymore. (The old deterministic builder is still in there — but now it's a manual escape hatch you reach for on purpose, never something that fires on its own.)&lt;/p&gt;

&lt;p&gt;Trustworthy also meant not living and dying by one vendor's quota. v1 was Gemini or nothing. v2 runs every call through a tiny provider interface, so an outage or a quota wall is a config change instead of a rewrite:&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/llm/provider.ts — one interface, three interchangeable backends&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;createProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;LLMProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LLM_PROVIDER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openai&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="nf"&gt;createOpenAIProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anthropic&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="nf"&gt;createAnthropicProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createGeminiProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&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;The Gemini side also rotates through a comma-separated list of keys (&lt;code&gt;keys[keyIndex++ % keys.length]&lt;/code&gt;). Three free keys gets you somewhere around 1,500 requests a day between them, which is plenty for a weekly run plus a day of messing around. Swapping the whole brain of the pipeline is one env var now.&lt;/p&gt;
&lt;h3&gt;
  
  
  Phase 2 — Split generate from publish
&lt;/h3&gt;

&lt;p&gt;This was the keystone, and it's the reason v1 never had a preview to begin with. The old workflow welded harvest → narrate → publish into one chain, and that chain's output schema &lt;strong&gt;dropped the blog body on the floor&lt;/strong&gt;. There was nothing to preview because the content never survived the handoff between steps. The smoking gun was the workflow's own output type: it carried URLs and counts and no &lt;code&gt;content&lt;/code&gt; field at all. You can't bolt a preview screen onto something that throws the draft away. So splitting it wasn't a nice-to-have stacked on v1. It was the thing that had to happen before anything else could.&lt;/p&gt;

&lt;p&gt;v2 cuts the pipeline in half. A &lt;code&gt;generate&lt;/code&gt; phase makes a full, previewable draft and &lt;strong&gt;stops&lt;/strong&gt;. A separate &lt;code&gt;publish&lt;/code&gt; step runs only when you approve, edits and all:&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/server/routes/run.ts&lt;/span&gt;
&lt;span class="c1"&gt;// POST /run  → generates a draft, stores it, sets status "preview". Publishes NOTHING.&lt;/span&gt;
&lt;span class="c1"&gt;// POST /publish/:jobId → applies your edited markdown, then calls publishBlog().&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That one cut bought three things at once: a safe failure mode, an editable preview in the browser, and run history that finally shows what actually got written. And the publisher turned into a single function that both the dashboard and the cron job call:&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/publish/publish-content.ts — one publisher, used by cron AND the dashboard&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;publishBlog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;blog&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;weeklyData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;publishMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&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;draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;images&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;statsCardPath&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="p"&gt;}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PublishResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* notion → devto → hashnode → write planner + footer */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Tying it together is a little JSON-backed &lt;strong&gt;run store&lt;/strong&gt; that's basically the pipeline's state machine. Every run walks a fixed path — &lt;code&gt;running → preview → publishing → published&lt;/code&gt;, or &lt;code&gt;failed&lt;/code&gt; — and that one record is what the dashboard lists, what the preview renders from, and what publish reads back when you hit approve. It's also where the &lt;code&gt;Dev log #n&lt;/code&gt; counter lives, so the numbers stay in order whether a post came from the dashboard or the cron job. One ledger, two doors in.&lt;/p&gt;
&lt;h3&gt;
  
  
  Phase 3 — Harvest the real diff
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;+0/-0&lt;/code&gt; bug is dead. A second, bounded GraphQL pass walks each active repo's commits — real additions and deletions, changed-file counts, and the &lt;strong&gt;directories you actually touched&lt;/strong&gt; — so the narrator can talk about specifics instead of waving its hands. That same week that used to report nothing now says:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;week 2026-05-27 → totals +2,252/-293
- DevNotion:        +2,220/-282, files=25, areas=[src/server, src/tools, bin]
- yashksaini-coder: +24/-3,      files=5,  areas=[assets, .github/workflows]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It's quota-safe on purpose: hard caps on how many repos and commits it'll look at, and every commit wrapped in its own &lt;code&gt;try/catch&lt;/code&gt; so one bad fetch can't sink the whole week:&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/tools/github-commits.ts — bounded, best-effort changed-file harvest&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;sha&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;shas&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&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;res&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.github.com/repos/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;owner&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;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/commits/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sha&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="nx"&gt;headers&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="o"&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;ok&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="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;f&lt;/span&gt; &lt;span class="k"&gt;of &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="nx"&gt;paths&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="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// skip this commit on any error — never sink the whole week&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 passes, on purpose. GraphQL is great for breadth — a whole week across a pile of repos in one query. The per-commit file lists come from a few targeted REST calls instead, each one cheap and each one isolated. This was the single thing v1's readers kept telling me: commit titles are cryptic, the diff is where the story actually is. Now it's in there.&lt;/p&gt;
&lt;h3&gt;
  
  
  Phase 4 — Sharper writing + an author footer
&lt;/h3&gt;

&lt;p&gt;I tightened the narrator's instructions. Lead with a character or a moment, not a stat line. A hard &lt;em&gt;no stat-dumping&lt;/em&gt; rule — the numbers live on the card, the prose doesn't need to recite them. And lean on the new touched-areas data, so a post can say "a week deep in &lt;code&gt;src/server&lt;/code&gt;" instead of "made several changes." You still get to pick the tone — &lt;code&gt;casual&lt;/code&gt;, &lt;code&gt;professional&lt;/code&gt;, &lt;code&gt;technical&lt;/code&gt;, &lt;code&gt;storytelling&lt;/code&gt; — but those rules hold no matter which one you choose. Then I gave every post, on every platform, the same author footer, built from one config file:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
**Yash K Saini** — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.

[GitHub] · [X] · [LinkedIn] · [Portfolio]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It gets stamped on at publish time, not written by the narrator. So you can't accidentally delete it in the preview, and Hashnode gets it for free.&lt;/p&gt;

&lt;p&gt;You can see the difference in the titles. One real run opened with &lt;em&gt;"Bridging the Gap: TLS Interop, p2p Hardening, and Neovim Refinement"&lt;/em&gt; — pulled straight out of the directories I'd been living in that week. Not "This week I made 35 commits." That's what the touched-areas data buys you.&lt;/p&gt;
&lt;h3&gt;
  
  
  Phase 5 — The cover image
&lt;/h3&gt;

&lt;p&gt;My first instinct was the obvious one: an AI cover. Nano Banana (&lt;code&gt;gemini-2.5-flash-image&lt;/code&gt;), prompted off the week's headline. I wired the whole thing up… and found out the free tier gives image generation a quota of &lt;code&gt;limit: 0&lt;/code&gt;. It never produced a single cover. So I was carrying a flaky, rate-limited dependency that did, in practice, nothing.&lt;/p&gt;

&lt;p&gt;So I ripped it out and used something I already had. The &lt;strong&gt;stats card&lt;/strong&gt; — a hand-built SVG rendered to PNG with &lt;code&gt;@resvg/resvg-js&lt;/code&gt;, showing the week's &lt;em&gt;exact&lt;/em&gt; numbers — happens to already be 1200×630, which is cover/OG size. So I just made it the cover:&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/images/stats-card.ts — the numbers come from code, never from a model&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt; &lt;span class="o"&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;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalCommits&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;commits&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="s2"&gt;`+&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalAdditions&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;added&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="s2"&gt;`-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalDeletions&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;removed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="c1"&gt;// …&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It leads with the post title and the headline numbers (commits, PRs, reviews, lines, repos) and becomes the cover everywhere: DEV.to's &lt;code&gt;main_image&lt;/code&gt;, the Hashnode cover, the Notion page cover. The &lt;code&gt;Dev log #n&lt;/code&gt; bit stays on the article title, not the card, so the banner is all title and stats, and a long title wraps instead of running off the edge. Because the card is built from code, no model can ever round &lt;code&gt;2,252&lt;/code&gt; down to "about 2,000." And here's the bit I liked: ripping out the AI cover made the feature &lt;em&gt;more&lt;/em&gt; reliable, not less. It went from never working to always working in the same commit that deleted code. No API, no quota, no fallback path. It just renders.&lt;/p&gt;
&lt;h3&gt;
  
  
  Phase 6 — A dashboard worth screenshotting
&lt;/h3&gt;

&lt;p&gt;My three dashboard routes had quietly drifted into three slightly-different stylesheets. I pulled them back into one design system — shared tokens, one shell, shared components — and gave it a deliberate &lt;strong&gt;Swiss / International Typographic&lt;/strong&gt; feel: tight type scale, one orange accent, lots of breathing room. It's all server-rendered HTML, no build step, straight through &lt;code&gt;tsx&lt;/code&gt;, which keeps the footprint tiny and the dependency list short. Most of the effort went into the preview/edit screen, because honestly that screen &lt;em&gt;is&lt;/em&gt; the product. It's where you read the draft, tweak a line, and decide to ship.&lt;/p&gt;
&lt;h3&gt;
  
  
  Phase 7 — Tests
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;51 tests across 18 suites&lt;/strong&gt; now cover the frontmatter parser, the fail-loud path, the deterministic fallback, tag normalization, provider and model selection, publish-target selection, the diff aggregation, and the stats-card builder (cover title-strip and wrap included). They run in under two seconds and they're deliberately boring. The point isn't cleverness — it's a regression net under the deterministic spine, so something like swapping out the cover implementation can't quietly break publishing.&lt;/p&gt;
&lt;h3&gt;
  
  
  Setup in one command
&lt;/h3&gt;

&lt;p&gt;v1's setup was a &lt;code&gt;.env&lt;/code&gt; scavenger hunt: copy the example, guess which keys you actually need, find out you missed one when a run blows up halfway through. &lt;code&gt;npx devnotion init&lt;/code&gt; is a wizard instead. It asks for each credential, &lt;strong&gt;checks it with a live API call&lt;/strong&gt; (green check or red error, right there in the terminal), and writes a clean &lt;code&gt;.env.local&lt;/code&gt;. Small thing. But something that calls itself &lt;em&gt;finished&lt;/em&gt; shouldn't make the first five minutes the worst five.&lt;/p&gt;
&lt;h3&gt;
  
  
  Before vs after
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Before (v1)&lt;/th&gt;
&lt;th&gt;After (v2)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Failure mode&lt;/td&gt;
&lt;td&gt;silently published a stub&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;fail-loud&lt;/strong&gt; — publishes nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review&lt;/td&gt;
&lt;td&gt;none — instant publish&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;preview, edit, approve&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLMs&lt;/td&gt;
&lt;td&gt;Gemini only&lt;/td&gt;
&lt;td&gt;Gemini / OpenAI / Anthropic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Publish targets&lt;/td&gt;
&lt;td&gt;Notion + DEV.to&lt;/td&gt;
&lt;td&gt;+ Hashnode (Subscription)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Line stats&lt;/td&gt;
&lt;td&gt;PR-only (&lt;code&gt;+0/-0&lt;/code&gt; on commit weeks)&lt;/td&gt;
&lt;td&gt;real per-commit deltas + touched areas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cover image&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;Deterministic stats card, used as the cover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup&lt;/td&gt;
&lt;td&gt;hand-edit &lt;code&gt;.env&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npx devnotion init&lt;/code&gt; wizard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;a handful&lt;/td&gt;
&lt;td&gt;51 across 18 suites&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; "It works, but you have to trust it blindly."&lt;br&gt;
&lt;strong&gt;After:&lt;/strong&gt; "It works, and it gets out of your way — safely."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Publishing responsibly (the research that turned into a feature)
&lt;/h2&gt;

&lt;p&gt;Around phase five I almost bolted on auto-publishing to a couple more platforms. Then I did the boring thing first and actually read the rules. The question I typed into the search bar was blunt: &lt;em&gt;"is automated/AI publishing even allowed here, and can it get my account restricted?"&lt;/em&gt; The answers ended up reshaping the design.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Medium&lt;/strong&gt; is a dead end for automation, on two counts. Its &lt;a href="https://github.com/Medium/medium-api-docs" rel="noopener noreferrer"&gt;publishing API was archived in 2023&lt;/a&gt; (no new integration tokens), and its &lt;a href="https://help.medium.com/hc/en-us/articles/22576852947223-Artificial-Intelligence-AI-content-policy" rel="noopener noreferrer"&gt;AI content policy&lt;/a&gt; gives undisclosed AI writing "Network Only" distribution and can pull it outright. An auto-posted AI dev-log is exactly what that policy is aimed at — so Medium's off the roadmap by design, not by accident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hashnode&lt;/strong&gt; has a real &lt;a href="https://apidocs.hashnode.com/" rel="noopener noreferrer"&gt;publishing API&lt;/a&gt;, but its &lt;a href="https://hashnode.com/code-of-conduct" rel="noopener noreferrer"&gt;Code of Conduct&lt;/a&gt; bans "automated or bulk posting" and self-promotion without contributing, and its &lt;a href="https://hashnode.com/terms" rel="noopener noreferrer"&gt;terms&lt;/a&gt; let them suspend accounts at their discretion. A genuine weekly post is fine. Blind, scheduled, multi-account auto-posting is the pattern they're watching for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DEV.to&lt;/strong&gt; is the friendliest to drafts and review, which is exactly the workflow I landed on anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix wasn't to publish &lt;em&gt;less&lt;/em&gt;. It was to publish honestly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Draft by default, human approval required.&lt;/strong&gt; Every platform gets a &lt;em&gt;draft&lt;/em&gt;; I review and edit it in the dashboard, then publish on-platform myself. Nothing ships unattended.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A disclosure footer&lt;/strong&gt; on every post — small, bold, &lt;strong&gt;Generated by DevNotion&lt;/strong&gt;. No hiding the tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;Dev log #n&lt;/code&gt; title prefix&lt;/strong&gt;, so the series is honest about what it is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The thread through all of it is disclosure over cleverness. A reader — and a platform's moderation team — can tell exactly what this is and whose name is on it. That's cheaper than building some workaround, and a lot cheaper than getting an account suspended.&lt;/p&gt;

&lt;p&gt;That one search turned a feature I'd have rushed into a constraint that made the whole thing more defensible. "Finished" doesn't just mean it runs. It means it won't get your accounts pulled.&lt;/p&gt;
&lt;h2&gt;
  
  
  My Experience with GitHub Copilot
&lt;/h2&gt;

&lt;p&gt;Reopening a codebase you haven't touched in months is mostly just remembering. You spend the first few hours working out why past-you did what they did before you can change any of it. That's where pairing with AI actually paid off, and it was almost never "write this function." It was "help me understand this again." A few moments stuck with me more than any autocomplete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Diagnosing the silent bug.&lt;/strong&gt; The most useful assist wasn't generated code — it was tracing &lt;em&gt;why&lt;/em&gt; setting the model config changed nothing. Walking the two divergent code paths with an AI partner surfaced the split-brain config in minutes, instead of an afternoon of &lt;code&gt;console.log&lt;/code&gt; archaeology.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The provider abstraction.&lt;/strong&gt; Describing "one interface, swap Gemini / OpenAI / Anthropic" in plain English got me a clean factory I refined, instead of writing it from a blank file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relearning an unfamiliar API.&lt;/strong&gt; Mastra's workflow primitives had shifted since I first used them. Rather than spelunking changelogs, I described the harvest → narrate → publish shape I wanted, let the assistant scaffold the &lt;code&gt;createStep&lt;/code&gt;/&lt;code&gt;createWorkflow&lt;/code&gt; calls, then fixed the schema wiring against the real types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knowing when to delete.&lt;/strong&gt; I started wiring an AI cover via the Nano Banana API and paired to figure out its quirks — Gemini image models return bytes via &lt;code&gt;result.files&lt;/code&gt;, and you call &lt;code&gt;generateText&lt;/code&gt;, not some dedicated image function. The genuinely useful assist came right after: confirming the free tier's image quota was &lt;code&gt;limit: 0&lt;/code&gt;, which made dropping the whole thing the obvious call instead of a stubborn one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests.&lt;/strong&gt; Generating the first pass of each unit test from the module's signature, then tightening the assertions by hand, is what made a 51-test suite cheap enough to actually write.&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2Frq3urpk266z9yw0acckw.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.amazonaws.com%2Fuploads%2Farticles%2Frq3urpk266z9yw0acckw.png" alt="GitHub Copilot in the workflow — e.g. Copilot Chat generating the provider interface, or inline completion filling a provider class" width="800" height="561"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The thing that changed most wasn't speed. It was confidence coming back into a cold codebase. It didn't just finish my lines — it helped me remember what the code did and figure out where to take it next. Nobody warns you that's the hard part of "finishing" something. That's the part pairing helped with most.&lt;/p&gt;
&lt;h2&gt;
  
  
  Results &amp;amp; Validation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;7 phases, shipped incrementally&lt;/strong&gt; — each verified against &lt;code&gt;tsc&lt;/code&gt; and the test suite before the next.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;51 tests / 18 suites, green.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The narration path was confirmed live on the free tier (&lt;code&gt;gemini-3-flash-preview&lt;/code&gt;), the harvest fix verified on a real week (&lt;code&gt;+0/-0&lt;/code&gt; → &lt;code&gt;+2,252/-293&lt;/code&gt;), and the stats card rendered to a real 1200×630 PNG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proven end-to-end on real weeks.&lt;/strong&gt; I ran v2 against three separate January 2026 weeks; each produced a Notion page and a DEV.to draft with the deterministic stats card attached as the cover — verified straight from the DEV.to API, where &lt;code&gt;cover_image&lt;/code&gt; resolved to the generated card for every post.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is theoretical. The rebuild got verified the same way it got built — incrementally. Every phase ended with a clean &lt;code&gt;tsc&lt;/code&gt; and a green suite before the next one started. The failing first run became a passing one I watched run end to end. And the final proof was deliberately mundane: three real weeks, published as reviewable drafts, covers and all, with nothing shipped that I hadn't read first.&lt;/p&gt;
&lt;h2&gt;
  
  
  What "finishing" actually meant
&lt;/h2&gt;

&lt;p&gt;I came into this assuming "finishing" meant features. More platforms, nicer output, an AI cover. Almost none of it was that. The phases that actually mattered were the ones that made the tool &lt;strong&gt;honest&lt;/strong&gt; — failing loud instead of shipping junk, showing me the draft before it went live, putting real diffs where guesses used to be, reading each platform's rules before automating against them.&lt;/p&gt;

&lt;p&gt;And the clearest tell? How often "finishing" turned out to mean &lt;em&gt;deleting&lt;/em&gt;. The AI cover, gone. The silent fallback, out of the hot path. A whole config key wired to nothing, gone. The version that finally felt done wasn't the one with the most code. It was the one I'd trust to post under my name while I wasn't looking.&lt;/p&gt;



&lt;p&gt;The repo is &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;yashksaini-coder/DevNotion&lt;/a&gt;: &lt;code&gt;npx devnotion init&lt;/code&gt;, point it at a week, and watch the draft land in the dashboard &lt;em&gt;before&lt;/em&gt; anything ships. If you try it, open an issue — I read them now, and "I read them now" is its own small proof the thing is finished. ⭐ it if the rebuild resonates.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;
        yashksaini-coder
      &lt;/a&gt; / &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;
        DevNotion
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      DevNotion is an agentic server built using 3 mastra agents to work as a pipeline to publish weekly blogs on Notion Pages &amp;amp; DEV.to 
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;DevNotion&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Turn your weekly GitHub activity into a polished, illustrated blog post — narrated by an LLM, reviewed by you, and published to Notion, DEV.to, and Hashnode.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DevNotion is a &lt;a href="https://mastra.ai" rel="nofollow noopener noreferrer"&gt;Mastra&lt;/a&gt; pipeline of specialist agents: it harvests a week of your GitHub work, narrates it into a first-person post, generates images, and lets you preview/edit before it publishes anywhere. Originally built for the DEV.to × Notion MCP Challenge (where it won $500); &lt;strong&gt;v2&lt;/strong&gt; was rebuilt for the &lt;a href="https://dev.to/challenges/github-2026-05-21" rel="nofollow"&gt;GitHub Finish-Up-A-Thon&lt;/a&gt; with multi-LLM support, a review dashboard, image generation, richer harvesting, and a fail-safe publishing flow.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What's new in v2&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;&lt;/p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;table&gt;

&lt;thead&gt;

&lt;tr&gt;

&lt;th&gt;v1&lt;/th&gt;

&lt;th&gt;v2&lt;/th&gt;

&lt;/tr&gt;

&lt;/thead&gt;

&lt;tbody&gt;

&lt;tr&gt;

&lt;td&gt;&lt;strong&gt;LLM&lt;/strong&gt;&lt;/td&gt;

&lt;td&gt;Gemini only&lt;/td&gt;

&lt;td&gt;Gemini · OpenAI · Anthropic (provider abstraction; default &lt;code&gt;gemini-3-flash-preview&lt;/code&gt;)&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;&lt;strong&gt;Publish targets&lt;/strong&gt;&lt;/td&gt;

&lt;td&gt;Notion + DEV.to&lt;/td&gt;

&lt;td&gt;Notion + DEV.to + &lt;strong&gt;Hashnode&lt;/strong&gt;&lt;br&gt;
&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;&lt;strong&gt;Flow&lt;/strong&gt;&lt;/td&gt;

&lt;td&gt;generate → publish immediately&lt;/td&gt;

&lt;td&gt;&lt;strong&gt;generate → preview → edit → approve → publish&lt;/strong&gt;&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;&lt;strong&gt;UI&lt;/strong&gt;&lt;/td&gt;

&lt;td&gt;none (CLI/cron)&lt;/td&gt;

&lt;td&gt;
&lt;br&gt;
&lt;strong&gt;web dashboard&lt;/strong&gt; (trigger, preview, edit, history)&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;&lt;strong&gt;Images&lt;/strong&gt;&lt;/td&gt;

&lt;td&gt;none&lt;/td&gt;

&lt;td&gt;&lt;strong&gt;deterministic stats&lt;/strong&gt;&lt;/td&gt;

&lt;/tr&gt;

&lt;/tbody&gt;

&lt;/table&gt;&lt;/div&gt;…&lt;p&gt;&lt;/p&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;strong&gt;Connect:&lt;/strong&gt; &lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with &lt;a href="https://mastra.ai" rel="noopener noreferrer"&gt;Mastra&lt;/a&gt;, the &lt;a href="https://ai-sdk.dev" rel="noopener noreferrer"&gt;Vercel AI SDK&lt;/a&gt;, Gemini 3 Flash for narration, &lt;code&gt;@resvg/resvg-js&lt;/code&gt; for the stats-card cover, and the &lt;a href="https://developers.notion.com" rel="noopener noreferrer"&gt;Notion&lt;/a&gt; / &lt;a href="https://developers.forem.com/api" rel="noopener noreferrer"&gt;DEV.to&lt;/a&gt; / &lt;a href="https://apidocs.hashnode.com/" rel="noopener noreferrer"&gt;Hashnode&lt;/a&gt; APIs. By &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Yash K Saini&lt;/a&gt; — &lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;. Star it if it made you smile.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
    </item>
    <item>
      <title>From Rust CLI bug-hunting to a massive Portfolio overhaul</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Sat, 06 Jun 2026 08:34:48 +0000</pubDate>
      <link>https://dev.to/yashksaini/from-rust-cli-bug-hunting-to-a-massive-portfolio-overhaul-7nn</link>
      <guid>https://dev.to/yashksaini/from-rust-cli-bug-hunting-to-a-massive-portfolio-overhaul-7nn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;23 commits, 3 PRs, and a 7-day streak. This week was a balance of deep-diving into Rust CLI logic, refining my Neovim setup, and a massive +4,500 line overhaul of my portfolio.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;It’s been a while since I’ve had a "perfect week," but I finally hit that 7-day commit streak. 23 commits, 3 PRs, and 1 closed issue across 4 different repos—this week was the definition of a polyglot grind. I spent my time bouncing between surgical Rust fixes in a CLI tool, deep-diving into Python backend logic, and completely ripping apart my portfolio for a modern redesign.&lt;/p&gt;

&lt;h2&gt;
  
  
  WHAT I BUILT
&lt;/h2&gt;

&lt;p&gt;The week was split between high-level visual work and low-level logic. I managed to touch four different repositories, each serving a very different purpose in my workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  DeepRead
&lt;/h3&gt;

&lt;p&gt;I spent a good chunk of the week in &lt;a href="https://github.com/yashksaini-coder/DeepRead" rel="noopener noreferrer"&gt;DeepRead&lt;/a&gt;. With 8 commits, this was my most active repo in terms of frequency. It’s a Python-heavy project, and I was focused on refining the core processing logic. Python is usually my go-to for anything involving heavy data or AI-adjacent backend work, and this week was no exception. I didn't open a PR here yet, but the 8 commits represent a lot of iterative testing and refining of the internal modules.&lt;/p&gt;

&lt;h3&gt;
  
  
  nvim
&lt;/h3&gt;

&lt;p&gt;You know it’s a productive week when you’re also tweaking your tools. I pushed 7 commits to my &lt;a href="https://github.com/yashksaini-coder/nvim" rel="noopener noreferrer"&gt;nvim&lt;/a&gt; config. Most of this was Lua-based (obviously), and I was mainly focused on tightening up my LSP settings and UI. There’s something incredibly satisfying about spending 20 minutes fixing a minor annoyance in your editor and then seeing it pay off for the rest of the week. If my environment isn't perfect, I can't get into that deep flow state.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitBanner
&lt;/h3&gt;

&lt;p&gt;I’ve been working on &lt;a href="https://github.com/yashksaini-coder/GitBanner" rel="noopener noreferrer"&gt;GitBanner&lt;/a&gt;, a TypeScript tool for generating GitHub profile banners. I pushed 6 commits and opened a new feature PR: &lt;a href="https://github.com/yashksaini-coder/GitBanner/pull/2" rel="noopener noreferrer"&gt;feat: add ignore-languages input to filter generated/vendored files&lt;/a&gt;. The goal here was to give users more control over what shows up in their stats. Nobody wants their "top languages" to be skewed by a massive vendored library or a bunch of generated CSS files. It’s a small DX improvement, but those are the ones that make a tool actually usable.&lt;/p&gt;

&lt;h3&gt;
  
  
  trx
&lt;/h3&gt;

&lt;p&gt;Finally, I did some surgical work on &lt;a href="https://github.com/pie-314/trx" rel="noopener noreferrer"&gt;trx&lt;/a&gt;, a Rust-based CLI tool. This was a classic "scratch your own itch" situation. I noticed a bug where the tool would repeatedly prompt for an update even after I’d already upgraded. I opened an issue for it—&lt;a href="https://github.com/pie-314/trx/issues/24" rel="noopener noreferrer"&gt;bug: TRX repeatedly prompts for update even after successfully updating&lt;/a&gt;—and then immediately went in to fix it. Rust is great for this kind of thing because once you get the logic right and the compiler is happy, you know it’s solid.&lt;/p&gt;

&lt;h2&gt;
  
  
  PULL REQUESTS
&lt;/h2&gt;

&lt;p&gt;I opened three PRs this week, and they tell the story of where my head was at.&lt;/p&gt;

&lt;p&gt;The most significant one in terms of sheer volume was for my &lt;a href="https://github.com/yashksaini-coder/portfolio/pull/13" rel="noopener noreferrer"&gt;portfolio&lt;/a&gt;. The PR, &lt;a href="https://github.com/yashksaini-coder/portfolio/pull/13" rel="noopener noreferrer"&gt;feat: portfolio modern redesign + scrolling GitHub PR sidebar&lt;/a&gt;, clocked in at a massive +4,509 lines. I decided it was time to move away from my old layout and go for something much more modern. The highlight of this redesign is a new scrolling sidebar that pulls in my latest GitHub PRs. It’s a bit meta—building a tool to show the tools I’m building—but I love how it looks. It’s still open as I’m fine-tuning some of the CSS transitions, but the bulk of the work is done.&lt;/p&gt;

&lt;p&gt;On the Rust side, I got &lt;a href="https://github.com/pie-314/trx/pull/25" rel="noopener noreferrer"&gt;fix: stop re-prompting update after user skips or upgrades&lt;/a&gt; merged into &lt;code&gt;trx&lt;/code&gt;. This was the fix for the bug I mentioned earlier. It involved tightening up the version check logic to ensure the CLI respects the user's choice to skip an update or correctly identifies when the local version matches the remote.&lt;/p&gt;

&lt;p&gt;I also have the &lt;a href="https://github.com/yashksaini-coder/GitBanner/pull/2" rel="noopener noreferrer"&gt;GitBanner&lt;/a&gt; PR open, which adds the &lt;code&gt;ignore-languages&lt;/code&gt; input. This was a fun bit of TypeScript logic, essentially filtering the language data before it hits the rendering engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  ISSUES &amp;amp; DISCUSSIONS
&lt;/h2&gt;

&lt;p&gt;I only opened one issue this week, but it was a productive one. I reported the update prompt bug in &lt;a href="https://github.com/pie-314/trx/issues/24" rel="noopener noreferrer"&gt;trx&lt;/a&gt; and then closed it myself with the PR I mentioned above. I’m a big believer in documenting bugs even if you’re the one fixing them five minutes later. It creates a paper trail for anyone else who might run into the same weird behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  TECH STACK
&lt;/h2&gt;

&lt;p&gt;This was a true polyglot week. My language breakdown is all over the place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt; for the heavy lifting in DeepRead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; for the GitBanner logic and the massive Portfolio redesign.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rust&lt;/strong&gt; for the CLI stability in trx.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lua&lt;/strong&gt; for the Neovim configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The numbers tell an interesting story: +4,614 additions and -1,081 deletions. That’s a huge net positive, mostly driven by the portfolio redesign. Usually, I like to see more deletions (refactoring is a gift), but when you’re shipping a whole new UI, the additions are going to win every time.&lt;/p&gt;

&lt;p&gt;And of course, the 7-day streak. It wasn't a forced grind; I just happened to be in a really good rhythm where I had something I wanted to ship every single day.&lt;/p&gt;

&lt;h2&gt;
  
  
  WHAT'S NEXT
&lt;/h2&gt;

&lt;p&gt;Next week is all about landing those open PRs. I want to get the portfolio redesign merged and live—there are still a few responsive design bugs I need to squash in that new scrolling sidebar. I also want to see the GitBanner feature through to completion. Beyond that, I’m planning to dive back into the Python side of things with DeepRead. There’s some performance optimization I’ve been putting off, and now that my environment is dialed in and my portfolio is looking fresh, I’ve got no excuses left.&lt;/p&gt;

&lt;p&gt;Catch you all next week!&lt;/p&gt;




&lt;h1&gt;
  
  
  python #typescript #rust #frontend #dx #consistency · 4 min read · Generated 2026-05-24 by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;
&lt;/h1&gt;

</description>
      <category>python</category>
      <category>typescript</category>
      <category>rust</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
