<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>sigh.dev - Scott Cooper&apos;s dev blog - All posts and notes</title><description>sigh.dev is Scott Cooper&apos;s dev blog about TypeScript, React, San Francisco, and the web. - All posts and notes</description><link>https://sigh.dev/</link><item><title>You can just port things to Cloudflare Workers</title><link>https://sigh.dev/posts/you-can-just-port-things-to-cloudflare-workers/</link><guid isPermaLink="true">https://sigh.dev/posts/you-can-just-port-things-to-cloudflare-workers/</guid><description>Vibecoding, Vibeporting?</description><pubDate>Sun, 25 Jan 2026 08:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This January I decided to double down on using up my &lt;a href=&quot;/posts/addicted-to-free-ai-credits/&quot;&gt;free ai credits&lt;/a&gt; by building a few projects on Cloudflare Workers. I’ve always really liked the idea of this platform that has cheap/free resources, but every time I build larger things on it I run into limits that make me frustrated and move to cheap vps hosting like fly.io or DigitalOcean.&lt;/p&gt;
&lt;p&gt;Is vibeporting a word? Maybe it should be?&lt;/p&gt;
&lt;h1&gt;&lt;a href=&quot;#datasette-ts&quot;&gt;Datasette-ts&lt;/a&gt;&lt;/h1&gt;
&lt;img alt=&quot;Datasette running on Cloudflare Workers | A port of Datasette to Cloudflare Workers&quot; loading=&quot;lazy&quot; width=&quot;1964&quot; height=&quot;1272&quot; src=&quot;/_astro/datasette.lR6-YtED_1F21hq.webp&quot; srcset=&quot;/_astro/datasette.lR6-YtED_Hekhy.webp 640w, /_astro/datasette.lR6-YtED_1pnBvh.webp 750w, /_astro/datasette.lR6-YtED_ZUWc2M.webp 828w, /_astro/datasette.lR6-YtED_Z6La66.webp 1080w, /_astro/datasette.lR6-YtED_Za7zSe.webp 1280w, /_astro/datasette.lR6-YtED_IWvsb.webp 1668w, /_astro/datasette.lR6-YtED_1F21hq.webp 1964w&quot; /&gt;
&lt;p&gt;Over holiday break, I was reading some of &lt;a href=&quot;https://simonwillison.net/&quot; target=&quot;_blank&quot;&gt;Simon Willison’s posts about AI&lt;/a&gt; and I checked where it’s currently possible to deploy it. You can deploy it pretty much everywhere but NOT on Cloudflare Workers, due to workers not really supporting much of Python’s ecosystem. I pointed Codex with GPT-5.2 Codex high/medium at the Datasette repo and had it break it down into README tasks and slowly do them one at a time. I’m not yet convinced by the crazy subagent stuff or that worktrees are worth it.&lt;/p&gt;
&lt;p&gt;As I got into it, it was clear I needed to narrow down the scope. Mr. Willison has built a very mature project that has a plugin system and a ton of features built-in and it depends on subdependencies that he also has published like sqlite-utils. Obviously, what I wanted is something that feels similar and runs on Cloudflare Workers. I picked up Drizzle, Hono, and Alchemy to handle Cloudflare deployments. I chose not to rebuild the frontend as a React SPA instead rendering something similar to the original Jinja templates using Hono’s JSX.&lt;/p&gt;
&lt;p&gt;Anyway, it ends up working pretty well. You can see a live demo at &lt;a href=&quot;https://datasette-legislators.ep.workers.dev/&quot; target=&quot;_blank&quot;&gt;datasette-legislators.ep.workers.dev&lt;/a&gt; and the source is available below. I can’t say the code is in a good state, but it lives here &lt;a href=&quot;https://github.com/scttcper/datasette-ts&quot; target=&quot;_blank&quot;&gt;https://github.com/scttcper/datasette-ts&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;&lt;a href=&quot;#sesnoop&quot;&gt;SESnoop&lt;/a&gt;&lt;/h1&gt;
&lt;img alt=&quot;SESnoop running on Cloudflare Workers | A port of Sessy to Cloudflare Workers&quot; loading=&quot;lazy&quot; width=&quot;2712&quot; height=&quot;2030&quot; src=&quot;/_astro/sesnoop.C6_5umH9_afh2n.webp&quot; srcset=&quot;/_astro/sesnoop.C6_5umH9_1bwH9m.webp 640w, /_astro/sesnoop.C6_5umH9_ZK8kLT.webp 750w, /_astro/sesnoop.C6_5umH9_Z2esmPH.webp 828w, /_astro/sesnoop.C6_5umH9_Z8e6xk.webp 1080w, /_astro/sesnoop.C6_5umH9_ZhzWBc.webp 1280w, /_astro/sesnoop.C6_5umH9_Z1C2Alj.webp 1668w, /_astro/sesnoop.C6_5umH9_xSMW3.webp 2048w, /_astro/sesnoop.C6_5umH9_Z17efrU.webp 2560w, /_astro/sesnoop.C6_5umH9_afh2n.webp 2712w&quot; /&gt;
&lt;p&gt;Another Cloudflare Worker port, this time of a Rails app called &lt;a href=&quot;https://sessy.do&quot; target=&quot;_blank&quot;&gt;Sessy&lt;/a&gt;. I decided to rename it because I was going for a different vibe. I send a bunch of emails via SES for xmplaylist.com and I was looking into running Sessy to handle the bounces and complaints, however I can’t get myself to think about Ruby for more than 1 minute. So again I pointed Codex at the Sessy repo and a few other Cloudflare/Hono example repos and had it build out a monorepo where the worker handles the api and Cloudflare assets serves a React SPA frontend.&lt;/p&gt;
&lt;p&gt;I think what was hard to reel in was how much frontend it was willing to build before I could stop it and start bringing in shadcn components. Like it made a bunch of select components and things that were generally pretty ugly. So then you have to tell it to replace all the ugly things with premade shadcn + baseui components.&lt;/p&gt;
&lt;p&gt;This project took a bit more work to get working outside of pointing AI at the code. It made a number of Cloudflare mistakes, tests were difficult to get working 100% correctly. One example was that it put the webhook at &lt;code&gt;/webhook&lt;/code&gt; which Cloudflare would attempt to load as an asset since the worker is bound to &lt;code&gt;/api&lt;/code&gt; which is always a bit of a headscratcher until you remember how this all works. It was really cool getting to know how SNS works with SES and watching them flow into D1 is fun.&lt;/p&gt;
&lt;p&gt;I tried publishing this package to npm and it kinda works to deploy to cloudflare if alchemy is setup. Or you can run it locally with &lt;code&gt;npx datasette-ts@latest serve ./legislators.db&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I don’t have a live demo, but you can check out the source code at &lt;a href=&quot;https://github.com/scttcper/sesnoop&quot; target=&quot;_blank&quot;&gt;scttcper/sesnoop&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;&lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Pretty happy with how both of these turned out. I didn’t exclusively use Codex on either of them, however Codex was the main driver 95% of the time. I think Codex w/ GPT-5.2 Codex on high or medium is a great model and through the team plan you can basically run medium as long as you want. I exhausted about 1 week worth of the plan I’m on for Codex for each project.&lt;/p&gt;</content:encoded><category>vibecoding</category><category>cloudflare</category><category>typescript</category><category>ai</category></item><item><title>xmplaylist in the wild</title><link>https://sigh.dev/posts/xmplaylist-in-the-wild/</link><guid isPermaLink="true">https://sigh.dev/posts/xmplaylist-in-the-wild/</guid><description>Random snapshots of projects that use the xmplaylist.com API.</description><pubDate>Sat, 17 Jan 2026 08:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve been collecting places where the xmplaylist.com API has shown up in the wild. I always think these are pretty interesting, so here’s a quick roundup of projects I’ve found that are scraping my data or using the API. As always, please use the xmplaylist feed endpoint instead of making 300 requests for all channels. &lt;a href=&quot;https://xmplaylist.com/api/feed&quot; target=&quot;_blank&quot;&gt;/api/feed&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#siriusxm-plugin-for-lyrion-media-server&quot;&gt;SiriusXM plugin for Lyrion Media Server&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/paul-1/plugin-SiriusXM&quot; target=&quot;_blank&quot;&gt;GitHub&lt;/a&gt; • &lt;a href=&quot;https://forums.lyrion.org/forum/user-forums/3rd-party-software/1782066-announce-siriusxm-plugin&quot; target=&quot;_blank&quot;&gt;Forum&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This plugin uses the xmplaylist.com API to fetch recent tracks metadata for SiriusXM channels because SiriusXM itself doesn’t expose that track metadata easily. The audio side is completely separate. It uses FFmpeg to demux the HLS streams and feed FLAC to the media server. It started as a Python proxy and was eventually ported to Perl to fit the plugin footprint.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#easylist&quot;&gt;EasyList&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Mattlk13/easylist/commit/818e4e10467c2ee585304411cc268160e7f8a934&quot; target=&quot;_blank&quot;&gt;GitHub Commit&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I finally made it. xmplaylist.com was added to EasyList so adblockers can more aggressively block my ads. Honestly, I’m just happy to be included.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#powershell-module-wrapper&quot;&gt;PowerShell module wrapper&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/dmaccormac/xmplaylist&quot; target=&quot;_blank&quot;&gt;GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This &lt;code&gt;XmPlaylist&lt;/code&gt; module wraps the API for all you Windows power users. It exposes station lookups and recently played queries directly in your shell. It even includes helpers to format items or play tracks via &lt;code&gt;yt-dlp&lt;/code&gt; and &lt;code&gt;ffplay&lt;/code&gt; which honestly isn’t a great way to get music, but I’m here for it.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#xm--spotify-archiver&quot;&gt;XM → Spotify archiver&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/angelam118/xm-to-spotify-1stwave&quot; target=&quot;_blank&quot;&gt;GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is a serverless bot that scrapes recent tracks and archives them into Spotify playlists, running entirely on GitHub Actions via cron. It uses &lt;code&gt;cloudscraper&lt;/code&gt; to bypass Cloudflare challenges because I assume the github actions ip addresses are trash or they’ve forgotten to include a user agent. It dedupes tracks locally and even auto-rotates playlists when they near Spotify’s 10,000 track cap. It’s built to self-heal if the local JSON state gets corrupted, which is probably better error handling than I have in my own app.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#xm-spotify-sync&quot;&gt;XM Spotify Sync&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/santiagobermudezparra/xmplaylist&quot; target=&quot;_blank&quot;&gt;GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This Python-based tool bridges the gap between satellite radio and streaming by automatically syncing tracks from XM Radio stations directly to your Spotify playlists. Built with a modern architecture featuring a FastAPI backend and Flask frontend, it handles the heavy lifting of polling station data and updating your playlists in the background. The project is container-ready with Docker Compose and Kubernetes support, making it a robust solution for archiving your favorite radio curation.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#android-playlist-hijacker&quot;&gt;Android “playlist hijacker”&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ghost-admiral/XMPLAYLISTHIJACKERSTANDALONE/blob/main/standaloneapptest1&quot; target=&quot;_blank&quot;&gt;GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This one is a standalone Android app scaffold built with Kotlin and Compose. It scrapes XM channel history and mirrors it into private YouTube playlists using the Google Sign-In and YouTube Data APIs. It has some maintenance tools to shuffle items or trim the playlist down to an 80-item cap. The code is a bit rough—there are some TODOs left in key places like the playlist ID mapping—but it’s a cool proof of concept for a mobile-first approach. I appreciate the repo name XMPLAYLISTHIJACKERSTANDALONE, wonderful vibe.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#siriusxm-to-usb&quot;&gt;SiriusXM to USB&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/mcoliver/siriusxm2usb&quot; target=&quot;_blank&quot;&gt;GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This Python CLI retrieves station data from the API and then scours YouTube Music for matches. The author built it to demonstrate creating a powerful CLI in under a day, complete with multiprocessing and colored logging. It even includes a &lt;code&gt;--download&lt;/code&gt; flag.&lt;/p&gt;</content:encoded><category>xmplaylist</category><category>siriusxm</category></item><item><title>I’m addicted to free AI credits</title><link>https://sigh.dev/posts/addicted-to-free-ai-credits/</link><guid isPermaLink="true">https://sigh.dev/posts/addicted-to-free-ai-credits/</guid><description>Free models, free credits, free VS Code forks. I’m using all of it while it lasts.</description><pubDate>Sun, 14 Dec 2025 08:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You might be too late to enjoy the era of $5 Ubers in SF, but you can still get a ride for free with AI. I’m absolutely hooked on using any available free AI models. Cursor will randomly announce that a new model is free for a week or whatever and I’m fucking there. Remember that time SOTA models were available for free? That was last week, and it’s hard to say how long this sort of spending will continue.&lt;/p&gt;
&lt;p&gt;Opus 4.5 was free for a while, and it did get me hooked on the model. I had tried Sonnet 4.5 a number of times but found it annoyingly cheerful and not as good as GPT-5.1 (granted GPT-5.1 came out way later). Now that I’m faced with having to pay for Opus 4.5, I am certainly tempted and I can see why so many are willing to jump on Claude Code.&lt;/p&gt;
&lt;p&gt;GitHub’s Copilot is giving me their monthly free open source subscription to a limited Copilot plan. I like how they’re still using the old Cursor pricing billed by requests rather than tokens. You can get quite a few things done with that. Google’s antigravity is interesting, but so far it seems to expire before it gets anywhere. I think the one takeaway from chasing free credits is that all the VS Code forks are now basically the same, assuming they have a plan feature. The furthest behind might be Gemini’s VS Code extension, which is a fairly shit experience. I’ve tried some free models and bounced off because they produce more slop than anything useful. Grok’s models are all shit so far.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;#what-did-i-build&quot;&gt;What did I build?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I launched a paid plan for &lt;a href=&quot;https://xmplaylist.com/pricing&quot; target=&quot;_blank&quot;&gt;xmplaylist.com&lt;/a&gt; and improved a number of my open source projects. I’d say the best use of these free credits has been nice little admin pages that I never would have spent time building. I can point the agent at enough examples that it needs hardly any direction to get it done. I only really do TypeScript in my free time and I’m sometimes jealous of the Django admins in other languages. Slop together a similar admin with TypeScript and &lt;a href=&quot;https://orm.drizzle.team/&quot; target=&quot;_blank&quot;&gt;drizzle&lt;/a&gt; and you’ve likely got a better dev experience than any Django app.&lt;/p&gt;
&lt;p&gt;I’m sure the free tokens will be locked down soon. I’m riding around as much as I can.&lt;/p&gt;
&lt;h4&gt;&lt;a href=&quot;#some-random-predictions-for-2027&quot;&gt;Some random predictions for 2027:&lt;/a&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;People will use smaller models to save money and because they’ve caught up in performance. I’m hopeful for Gemini 3 Flash and not hopeful for SOTA getting cheaper.&lt;/li&gt;
&lt;li&gt;Cursor will have to drop the price on their composer-1 model (which works great)&lt;/li&gt;
&lt;li&gt;At least 2 more VS Code forks will launch&lt;/li&gt;
&lt;li&gt;I’ll still be chasing free SOTA models&lt;/li&gt;
&lt;li&gt;Grok will continue to be 2nd rate&lt;/li&gt;
&lt;li&gt;I’ll actually pay for a personal plan from some provider&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TODO:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;try windsurf&lt;/li&gt;
&lt;/ul&gt;
&lt;img src=&quot;/_astro/uber-2017.Dy9Ys4ZO_Z1C9b0V.webp&quot; srcset=&quot;/_astro/uber-2017.Dy9Ys4ZO_1wpgy7.webp 640w, /_astro/uber-2017.Dy9Ys4ZO_8EmDr.webp 750w, /_astro/uber-2017.Dy9Ys4ZO_ZRKMDR.webp 828w, /_astro/uber-2017.Dy9Ys4ZO_4CE6s.webp 1080w, /_astro/uber-2017.Dy9Ys4ZO_Z1C9b0V.webp 1272w&quot; alt=&quot;5 dollar uber | five dollar uber in sf circa 2017. Currently $17 for the same trip&quot; loading=&quot;lazy&quot; width=&quot;1272&quot; height=&quot;1026&quot; /&gt;</content:encoded><category>ai</category><category>typescript</category><category>vibecoding</category></item><item><title>How I Use Hono and Tanstack Start</title><link>https://sigh.dev/posts/how-i-use-hono-and-tanstack-start/</link><guid isPermaLink="true">https://sigh.dev/posts/how-i-use-hono-and-tanstack-start/</guid><description>How I use Hono and Tanstack Start to build my side projects.</description><pubDate>Thu, 20 Nov 2025 08:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve never liked using any of the fullstack web frameworks for apis. They lack clear middleware and routing, they never quite support openapi and the types are often more difficult to work with. I see lots of people use tRPC or oRPC to try to smooth out the rough edges, but most of my side projects rely on heavy caching and RPC is inheirtly shit at caching at the CDN lavel because it makes post requests. Nextjs is the worst offender here, api middleware is a complete nightmare to understand.&lt;/p&gt;
&lt;p&gt;The two options for using hono as I see it are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use vite to start a hono server that loads tanstack start. This was popular how most remix+hono projects tend to be structured.&lt;/li&gt;
&lt;li&gt;Use tanstack start to route api requests to a hono server. This is the pattern I’ve been using for a while now and it’s working well for me.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;a href=&quot;#hono-up-front&quot;&gt;Hono up front&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I see the appeal of using hono up front to do all routing, you can inject everything you want and have the last word on the request/response via middleware. &lt;a href=&quot;https://github.com/bskimball/tanstack-hono&quot; target=&quot;_blank&quot;&gt;https://github.com/bskimball/tanstack-hono&lt;/a&gt; shows this off really well. I think the main tradeoffs here are that you have a way more custom vite config and a bit more of a custom build process which might be difficult to maintain especially as tanstack continues to iterate on the vite plugin leading up to the stable release. You’ll notice it runs &lt;code&gt;npm run build:client &amp;amp;&amp;amp; npm run build:server &amp;amp;&amp;amp; npm run build:types&lt;/code&gt; to build which creates different bundles via vite.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#hono-in-the-back&quot;&gt;Hono in the back&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I like to have hono sit in the back during development and then bring it to the front in production, which is a little funky but you only need to set up the production side once. The advantage here is that you don’t need to customize the vite config. Tanstack start can pass requests to hono via an api wildcard route.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;// src/routes/api/$.ts wildcard api route&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { createFileRoute } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &apos;&lt;/span&gt;&lt;span&gt;@tanstack/react-router&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { app } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &apos;&lt;/span&gt;&lt;span&gt;../../../server/app&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; serve&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; async&lt;/span&gt;&lt;span&gt; ({ &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; { request&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Request&lt;/span&gt;&lt;span&gt; }) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; app.&lt;/span&gt;&lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(request);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// Route all api requests to the hono server&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; const&lt;/span&gt;&lt;span&gt; Route &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; createFileRoute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;/api/$&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;)({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  server&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    handlers&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      GET&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; serve,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      POST&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; serve,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      PUT&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; serve,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      DELETE&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; serve,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      PATCH&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; serve,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      OPTIONS&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; serve,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      HEAD&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; serve,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;</content:encoded><category>typescript</category><category>hono</category><category>tanstack-start</category></item><item><title>My Favorite NPM Command</title><link>https://sigh.dev/posts/favorite-npm-command/</link><guid isPermaLink="true">https://sigh.dev/posts/favorite-npm-command/</guid><description>An ode to `npm repo`, the best npm command.</description><pubDate>Sat, 11 Oct 2025 08:00:00 GMT</pubDate><content:encoded> 
&lt;p&gt;Let’s talk about my favorite npm command, &lt;code&gt;npm repo&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Open package repository page in the browser - &lt;a href=&quot;https://docs.npmjs.com/cli/v11/commands/npm-repo&quot; target=&quot;_blank&quot;&gt;npm docs&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This command is dead simple. Open the repository page for the given package in the browser. If you omit the package name, it will look at the local package.json and open that repository.&lt;/p&gt;
&lt;p&gt;I use this command several times a day—often hourly. What’s great is that it works for published npm packages and any repo with a &lt;code&gt;package.json&lt;/code&gt;. I’ll use it just to open the repo I’m working in, too. I don’t need a bunch of bookmarks or to hope that my current repo shows up on the GitHub homepage. I’ll just run &lt;code&gt;npm repo&lt;/code&gt; and dig into any package, and I’ve yet to be rickrolled. Googling certain packages is annoying because you often have to add “GitHub repo” to the search.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#history&quot;&gt;History&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The npm repo command was added in 2013 in commit &lt;a href=&quot;https://github.com/npm/npm/commit/0223389130dfd220d2a18dfe7058c4f7f0b14808&quot; target=&quot;_blank&quot;&gt;0223389&lt;/a&gt; by &lt;a href=&quot;https://github.com/tj&quot; target=&quot;_blank&quot;&gt;TJ Holowaychuk&lt;/a&gt;, creator of Express, Mocha, and other popular npm packages. Ignoring various issues opened on the CLI, this is the only contribution he made directly.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#other-package-managers&quot;&gt;Other package managers&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The yarn cli does not have a repo command. Pnpm does not have a repo command, instead &lt;a href=&quot;https://github.com/pnpm/pnpm/blob/1b15e45ae949485ba9b9bfd4e079b551ab7d8819/pnpm/src/pnpm.ts#L30&quot; target=&quot;_blank&quot;&gt;they forward the command to the npm cli&lt;/a&gt; among a list of other commands they forward. &lt;code&gt;pnpm home&lt;/code&gt; is another good one they forward to npm, I just don’t usually reach for that one.&lt;/p&gt;
&lt;p&gt;As I move further away from using Koa, Express, and TJ’s other legacy projects, I’m positive I’ll keep using &lt;code&gt;npm repo&lt;/code&gt;. Praise TJ.&lt;/p&gt;</content:encoded><category>typescript</category><category>npm</category><category>tj</category></item><item><title>@ctrl/tinycolor Supply Chain Attack Post-mortem</title><link>https://sigh.dev/posts/ctrl-tinycolor-post-mortem/</link><guid isPermaLink="true">https://sigh.dev/posts/ctrl-tinycolor-post-mortem/</guid><description>Lessons learned from becoming the unexpected face of a npm supply-chain attack.</description><pubDate>Tue, 16 Sep 2025 08:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;a href=&quot;#tldr&quot;&gt;TL;DR&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A malicious GitHub Actions workflow was pushed to a shared repo and exfiltrated a npm token with broad publish rights. The attacker then used that token to publish malicious versions of 20 packages, including &lt;code&gt;@ctrl/tinycolor&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My GitHub account, the @ctrl/tinycolor repository were not directly compromised. There was no phishing involved, and no malicious packages were installed on my machine and I already use pnpm to avoid unapproved postinstall scripts. There was no pull request involved because a repo admin does not need a pull request to add new github actions.&lt;/p&gt;
&lt;p&gt;GitHub/npm security responded quickly, unpublishing the malicious versions. I followed by releasing clean versions to flush caches, as advised.&lt;/p&gt;
&lt;p&gt;For broader context, see &lt;a href=&quot;https://socket.dev/blog/tinycolor-supply-chain-attack-affects-40-packages&quot; target=&quot;_blank&quot;&gt;Socket’s write-up&lt;/a&gt; or &lt;a href=&quot;https://www.stepsecurity.io/blog/ctrl-tinycolor-and-40-npm-packages-compromised&quot; target=&quot;_blank&quot;&gt;StepSecurity’s analysis&lt;/a&gt;. For community discussion, see this &lt;a href=&quot;https://news.ycombinator.com/item?id=45260741&quot; target=&quot;_blank&quot;&gt;Hacker News post&lt;/a&gt;, which spent 24 hours on the front page. I’m also finding this &lt;a href=&quot;https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack&quot; target=&quot;_blank&quot;&gt;wiz.io&lt;/a&gt; post helpful.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#how-i-found-out&quot;&gt;How I Found Out&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;On September 15 around 4:30
 PM PT, &lt;a href=&quot;https://bsky.app/profile/notwes.bsky.social&quot; target=&quot;_blank&quot;&gt;Wes Todd&lt;/a&gt; DM’d me on Bluesky and looped me into the OpenJS Foundation Slack. By that point, Wes had already alerted GitHub/npm security, who were compiling lists of affected packages and rapidly unpublishing compromised versions.&lt;/p&gt;
&lt;p&gt;Early guidance (attributed to Daniel Pereira) was to look for suspicious &lt;code&gt;Shai-Hulud&lt;/code&gt; repos or branches. I wasn’t able to find any of these repos or branches on my own personal repos. The mystery was: how was I impacted at all?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Shai-Hulud was the Fremen term for the sandworm of Arrakis. - &lt;a href=&quot;https://dune.fandom.com/wiki/Shai-Hulud&quot; target=&quot;_blank&quot;&gt;dune wiki&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;&lt;a href=&quot;#what-actually-happened&quot;&gt;What Actually Happened&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A while ago, I collaborated on &lt;a href=&quot;https://github.com/angulartics/angulartics2&quot; target=&quot;_blank&quot;&gt;angulartics2&lt;/a&gt;, a shared repository where multiple people still had admin rights. That repo still contained a GitHub Actions secret — a npm token with broad publish rights. This collaborator had access to projects with other people which I believe explains some of the other 40 initial packages that were affected.&lt;/p&gt;
&lt;p&gt;A new Shai-Hulud branch was force pushed to angulartics2 with a malicious github action workflow by a collaborator. The workflow ran immediately on push (did not need review since the collaborator is an admin) and stole the npm token. With the stolen token, the attacker published malicious versions of 20 packages. Many of which are not widely used, however the @ctrl/tinycolor package is downloaded about 2 million times a week.&lt;/p&gt;
&lt;p&gt;GitHub and npm security teams moved quickly to unpublish the malicious versions. I then re-published fresh, verified versions of the packages I maintain to flush caches and restore trust.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#impact&quot;&gt;Impact&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Malicious versions of several packages — including @ctrl/tinycolor — were briefly available on npm before removal. Installing those compromised versions would have triggered a postinstall payload, which is documented in detail by &lt;a href=&quot;https://www.stepsecurity.io/blog/ctrl-tinycolor-and-40-npm-packages-compromised#attack-mechanism&quot; target=&quot;_blank&quot;&gt;StepSecurity&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;What should you do if you’ve installed a compromised version of a package? &lt;a href=&quot;https://www.stepsecurity.io/blog/ctrl-tinycolor-and-40-npm-packages-compromised#immediate-actions-required&quot; target=&quot;_blank&quot;&gt;see StepSecurity’s immediate actions&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#publishing-setup--interim-plan&quot;&gt;Publishing Setup &amp;amp; Interim Plan&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I currently use &lt;a href=&quot;https://github.com/semantic-release/semantic-release&quot; target=&quot;_blank&quot;&gt;semantic-release&lt;/a&gt; with GitHub Actions to handle publishing. The automation is convenient and predictable. I also have npm provenance enabled on many packages, which provides attestations of how they were built. Unfortunately, provenance didn’t prevent this attack because the attacker had a valid token.&lt;/p&gt;
&lt;p&gt;My goal is to move to npm’s &lt;strong&gt;Trusted Publishing (OIDC)&lt;/strong&gt; to eliminate static tokens altogether. However, semantic-release integration is still in progress: &lt;a href=&quot;https://github.com/npm/cli/issues/8525&quot; target=&quot;_blank&quot;&gt;npm/cli#8525&lt;/a&gt;.&lt;/p&gt;
&lt;img alt=&quot;npm Publishing access settings&quot; loading=&quot;lazy&quot; width=&quot;1618&quot; height=&quot;804&quot; src=&quot;/_astro/publishing-access.DTmYbTkJ_Z2may3Q.webp&quot; srcset=&quot;/_astro/publishing-access.DTmYbTkJ_1Fa49o.webp 640w, /_astro/publishing-access.DTmYbTkJ_Z22BseH.webp 750w, /_astro/publishing-access.DTmYbTkJ_Z27AVbY.webp 828w, /_astro/publishing-access.DTmYbTkJ_ZGtYiM.webp 1080w, /_astro/publishing-access.DTmYbTkJ_Z11X4ph.webp 1280w, /_astro/publishing-access.DTmYbTkJ_Z2may3Q.webp 1618w&quot; /&gt;
&lt;p&gt;For the forseeable future, @ctrl/tinycolor requires 2FA for publishing, and all tokens have been revoked. Not expecting to merge any new changes anytime soon.&lt;/p&gt;
&lt;p&gt;For smaller packages, I’ll continue using semantic-release but under stricter controls: no new contributors will be added, and each repo will use a granular npm token limited to publish-only rights for that specific package.&lt;/p&gt;
&lt;p&gt;Local 2FA based publishing isn’t sustainable, so I’m watching OIDC/Trusted Publishing closely and will adopt it as soon as it fits the workflow.&lt;/p&gt;
&lt;p&gt;I plan to continue using pnpm that prevents unapproved postinstall scripts from being run and I’ll look into adding pnpm’s new &lt;a href=&quot;https://pnpm.io/settings#minimumreleaseage&quot; target=&quot;_blank&quot;&gt;minimumReleaseAge&lt;/a&gt; setting.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#publishing-wishlist&quot;&gt;Publishing Wishlist&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If I could wave a magic wand and design my ideal setup, npm would allow me to require Trusted Publishing (OIDC) with a single toggle for all of my packages. That same toggle would block any release missing provenance, enforcing security at the account level. I’d also want first-class semantic-release support with OIDC and provenance so no static tokens are ever needed.&lt;/p&gt;
&lt;p&gt;On top of that, I’d like a secure, human-approved publishing option directly in the GitHub UI: a protected workflow_dispatch flow that uses github 2FA approval to satisfy 2FA, without requiring me to publish from my laptop.&lt;/p&gt;
&lt;p&gt;GitHub Environments — or equivalent workflow protections — should be available without a Pro subscription, or else integrated directly into Trusted Publishing so that security doesn’t depend on the pricing tier.&lt;/p&gt;
&lt;p&gt;It would be really nice if NPM also had a more visible mark on the package details page to indicate if the package had a postinstall script. Also, once the packages are pulled its not clear what versions were removed and why.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#thanks&quot;&gt;Thanks&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Thanks to Wes Todd, the OpenJS Foundation, and the GitHub/npm security teams for their rapid and coordinated response. Everyone was incredibly fast, helpful, and knowledgeable.&lt;/p&gt;
&lt;img alt=&quot;dune worm [wide] | dune worm via chatgpt&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; src=&quot;/_astro/dune-worm.QN2JLFkT_ZOy594.webp&quot; srcset=&quot;/_astro/dune-worm.QN2JLFkT_ZWljFi.webp 640w, /_astro/dune-worm.QN2JLFkT_Z24bQ0U.webp 750w, /_astro/dune-worm.QN2JLFkT_1CdkyI.webp 828w, /_astro/dune-worm.QN2JLFkT_1kRnJS.webp 1080w, /_astro/dune-worm.QN2JLFkT_Z1xwr04.webp 1280w, /_astro/dune-worm.QN2JLFkT_ZOy594.webp 1536w&quot; /&gt;</content:encoded><category>typescript</category><category>security</category></item><item><title>Starting a blog</title><link>https://sigh.dev/posts/starting-a-blog/</link><guid isPermaLink="true">https://sigh.dev/posts/starting-a-blog/</guid><description>Everyone says you should start a blog, so I&apos;m starting one.</description><pubDate>Mon, 01 Sep 2025 08:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;a href=&quot;#who&quot;&gt;Who?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I work at &lt;a href=&quot;https://sentry.io&quot; target=&quot;_blank&quot;&gt;Sentry.io&lt;/a&gt; where I add my cursor generated slop (more on that soon) to their React SPA. Outside of work, I’ve made &lt;a href=&quot;https://xmplaylist.com&quot; target=&quot;_blank&quot;&gt;xmplaylist.com&lt;/a&gt;, a music discovery and song tracking app for Sirius XM which runs on Tanstack Start + hono, my current favorite stack. This blog is built with Astro which I’ve never used before.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#why-sighdev&quot;&gt;Why sigh.dev?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;My name is Scott Cooper. If you google my name, you’ll find a movie director or a baseball player. My name is essentially annonymous on the internet which can be great. scottcooper.dev is literally owned by someone that seems to do the same thing as me. The sigh.dev domain name is short and sad which is great. The blog theme is currently inspired by soloraized blue theme, weirdly I don’t use that theme. My favorite theme is Dracula + Menlo font. You can actually buy &lt;a href=&quot;https://draculatheme.com/pro&quot; target=&quot;_blank&quot;&gt;Dracula pro&lt;/a&gt; now? But should you?&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#slightly-more-about-me&quot;&gt;Slightly more about me&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I’ve been a developer for 10+ years, mostly worked at startups that have run out of money. I once helped send over a billion emails in a 24 hour period and this was not an accident. I watch a lot of movies, especially horror movies, some even by director Scott Cooper. I’ve been trying to do a better job of leaving reviews on my &lt;a href=&quot;https://letterboxd.com/scottcooper/&quot; target=&quot;_blank&quot;&gt;letterboxd profile&lt;/a&gt; this year.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;#diving-in&quot;&gt;Diving in&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I’m an idiot in San Francisco, but I’ve figured out TypeScript and you’re going to hear about it. If you want updates, subscribe to the &lt;a href=&quot;/posts/rss.xml&quot;&gt;posts RSS&lt;/a&gt;.&lt;/p&gt;
&lt;img alt=&quot;a screenshot from the movie vertigo [wide] | Vertigo 1958&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; src=&quot;/_astro/vertigo.2Dc-qg3t_8M6TO.webp&quot; srcset=&quot;/_astro/vertigo.2Dc-qg3t_ZHIRLa.webp 640w, /_astro/vertigo.2Dc-qg3t_2gaEk.webp 750w, /_astro/vertigo.2Dc-qg3t_ZAhNIu.webp 828w, /_astro/vertigo.2Dc-qg3t_xWy9v.webp 1080w, /_astro/vertigo.2Dc-qg3t_2buAuJ.webp 1280w, /_astro/vertigo.2Dc-qg3t_2twDkL.webp 1668w, /_astro/vertigo.2Dc-qg3t_8M6TO.webp 1920w&quot; /&gt;</content:encoded><category>nothing</category></item><item><title>Test Note</title><link>https://sigh.dev/notes/welcome/</link><guid isPermaLink="true">https://sigh.dev/notes/welcome/</guid><description>First note</description><pubDate>Thu, 05 Jun 2025 03:29:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve not figured out how much I’ll use notes. They have their own RSS feed at &lt;code&gt;/notes/rss.xml&lt;/code&gt; separate from the blog.&lt;/p&gt;</content:encoded></item></channel></rss>