Posts in Javascript (20 found)
devansh Yesterday

HonoJS JWT/JWKS Algorithm Confusion

After spending some time looking for security issues in JS/TS frameworks , I moved on to Hono - fast, clean, and popular enough that small auth footguns can become "big internet problems". This post is about two issues I found in Hono's JWT/JWKS verification path: Both were fixed in hono 4.11.4 , and GitHub Security Advisories were published on January 13, 2026 . If you already have experience with JWT stuff, you can skip this: The key point here is that, algorithm choice must not be attacker-controlled. Hono's JWT helper documents that is optional - and defaults to HS256. That sounds harmless until you combine it with a very common real-world setup: In that case, the verification path defaults to HS256, treating that public key string as an HMAC secret, and that becomes forgeable because public keys are, well… public. If an attacker can generate a token that passes verification, they can mint whatever claims the application trusts ( , , , etc.) and walk straight into protected routes. This is the "algorithm confusion" class of bugs, where you think you're doing asymmetric verification, but you're actually doing symmetric verification with a key the attacker knows. This is configuration-dependent. The dangerous case is: The core issue is, Hono defaults to , so a public key string can accidentally be used as an HMAC secret, allowing forged tokens and auth bypass. Advisory: GHSA-f67f-6cw9-8mq4 This was classified as High (CVSS 8.2) and maps it to CWE-347 (Improper Verification of Cryptographic Signature) . Affected versions: Patched version: 4.11.4 In the JWK/JWKS verification middleware, Hono could pick the verification algorithm like this: GitHub's advisory spells it out, when the selected JWK doesn't explicitly define an algorithm, the middleware falls back to using the from the unverified JWT header - and since in JWK is optional and commonly omitted, this becomes a real-world issue. If the matching JWKS key lacks , falls back to token-controlled , enabling algorithm confusion / downgrade attacks. "Trusting " is basically letting the attacker influence how you verify the signature. Depending on surrounding constraints (allowed algorithms, how keys are selected, and how the app uses claims), this can lead to forged tokens being accepted and authz/authn bypass . Advisory: GHSA-3vhc-576x-3qv4 This was classified as High (CVSS 8.2) , also CWE-347 , with affected versions and patched in 4.11.4 . Both advisories took the same philosophical stance i.e. Make explicit. Don't infer it from attacker-controlled input. The JWT middleware now requires an explicit option — a breaking change that forces callers to pin the algorithm instead of relying on defaults. Before (vulnerable): After (patched): (Example configuration shown in the advisory.) The JWK/JWKS middleware now requires an explicit allowlist of asymmetric algorithms, and it no longer derives the algorithm from untrusted JWT header values. It also explicitly rejects symmetric HS* algorithms in this context. Before (vulnerable): After (patched): (Example configuration shown in the advisory.) JWT / JWK / JWKS Primer Vulnerabilities [CVE-2026-22817] - JWT middleware "unsafe default" (HS256) Why this becomes an auth bypass Who is affected? Advisory / severity [CVE-2026-22817] - JWK/JWKS middleware fallback Why it matters Advisory / severity The Fix Fix for #1 (JWT middleware) Fix for #2 (JWK/JWKS middleware) Disclosure Timeline a default algorithm footgun in the JWT middleware that can lead to forged tokens if an app is misconfigured a JWK/JWKS algorithm selection bug where verification could fall back to an untrusted value JWT is . The header includes (the signing algorithm). JWK is a JSON representation of a key (e.g. an RSA public key). JWKS is a set of JWKs, usually hosted at something like . The app expects RS256 (asymmetric) The developer passes an RSA public key string But they don't explicitly set you use the JWT middleware with an asymmetric public key and you don't pin Use if present Otherwise, fall back to from the JWT (unverified input) Discovery: 09th Dec, 2025 First Response: 09th Dec, 2025 Patched in: hono 4.11.4 Advisories published: 13 Jan, 2026 Advisory: GHSA-f67f-6cw9-8mq4 Advisory: GHSA-3vhc-576x-3qv4

0 views
Simon Willison 3 days ago

My answers to the questions I posed about porting open source code with LLMs

Last month I wrote about porting JustHTML from Python to JavaScript using Codex CLI and GPT-5.2 in a few hours while also buying a Christmas tree and watching Knives Out 3. I ended that post with a series of open questions about the ethics and legality of this style of work. Alexander Petros on lobste.rs just challenged me to answer them , which is fair enough! Here's my attempt at that. You can read the original post for background, but the short version is that it's now possible to point a coding agent at some other open source project and effectively tell it "port this to language X and make sure the tests still pass" and have it do exactly that. Here are the questions I posed along with my answers based on my current thinking. Extra context is that I've since tried variations on a similar theme a few more times using Claude Code and Opus 4.5 and found it to be astonishingly effective. I decided that the right thing to do here was to keep the open source license and copyright statement from the Python library author and treat what I had built as a derivative work, which is the entire point of open source. After sitting on this for a while I've come down on yes, provided full credit is given and the license is carefully considered. Open source allows and encourages further derivative works! I never got upset at some university student forking one of my projects on GitHub and hacking in a new feature that they used. I don't think this is materially different, although a port to another language entirely does feel like a slightly different shape. Now this one is complicated! It definitely hurts some projects because there are open source maintainers out there who say things like "I'm not going to release any open source code any more because I don't want it used for training" - I expect some of those would be equally angered by LLM-driven derived works as well. I don't know how serious this problem is - I've seen angry comments from anonymous usernames, but do they represent genuine open source contributions or are they just angry anonymous usernames? If we assume this is real, does the loss of those individuals get balanced out by the increase in individuals who CAN contribute to open source because they can now get work done in a few hours that might previously have taken them a few days that they didn't have to spare? I'll be brutally honest about that question: I think that if "they might train on my code / build a derived version with an LLM" is enough to drive you away from open source, your open source values are distinct enough from mine that I'm not ready to invest significantly in keeping you. I'll put that effort into welcoming the newcomers instead. The much bigger concern for me is the impact of generative AI on demand for open source. The recent Tailwind story is a visible example of this - while Tailwind blamed LLMs for reduced traffic to their documentation resulting in fewer conversions to their paid component library, I'm suspicious that the reduced demand there is because LLMs make building good-enough versions of those components for free easy enough that people do that instead. I've found myself affected by this for open source dependencies too. The other day I wanted to parse a cron expression in some Go code. Usually I'd go looking for an existing library for cron expression parsing - but this time I hardly thought about that for a second before prompting one (complete with extensive tests) into existence instead. I expect that this is going to quite radically impact the shape of the open source library world over the next few years. Is that "harmful to open source"? It may well be. I'm hoping that whatever new shape comes out of this has its own merits, but I don't know what those would be. I'm not a lawyer so I don't feel credible to comment on this one. My loose hunch is that I'm still putting enough creative control in through the way I direct the models for that to count as enough human intervention, at least under US law, but I have no idea. I've come down on "yes" here, again because I never thought it was irresponsible for some random university student to slap an Apache license on some bad code they just coughed up on GitHub. What's important here is making it very clear to potential users what they should expect from that software. I've started publishing my AI-generated and not 100% reviewed libraries as alphas, which I'm tentatively thinking of as "alpha slop" . I'll take the alpha label off once I've used them in production to the point that I'm willing to stake my reputation on them being decent implementations, and I'll ship a 1.0 version when I'm confident that they are a solid bet for other people to depend on. I think that's the responsible way to handle this. That one was a deliberately provocative question, because for a new HTML5 parsing library that passes 9,200 tests you would need a very good reason to hire an expert team for two months (at a cost of hundreds of thousands of dollars) to write such a thing. And honestly, thanks to the existing conformance suites this kind of library is simple enough that you may find their results weren't notably better than the one written by the coding agent. You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options .

1 views

The frustration of a perfect setup

No matter how I look at the list of apps I currently use , whether first-party or third-party, I can’t find anything to change, not a program to replace, not a service to swap for another. I think I am happy with my setup. It feels strange to admit, but somehow, I can’t quite believe it; I must be missing something, something surely can be tweaked. What happens after peak setup? This frustration comes from the fact that looking at new apps, digging into settings, trying new online services, working on how each of these things operates with the others, is one of my favourite hobbies. I mean, a quick glance at the archive of this site will tell you that, not only do I love writing about apps and digital tools, but I love playing with their configurations; I’m like a kid with Lego bricks, building things, taking them apart, and building them again, with a huge smile, in a slightly different and improved way. Now that my application setup appears to be “final”, it feels as though all my toys and Lego bricks are neatly stored away in their respective drawers, sorted by colour, by type, and by size. It’s perfect, and seeing my beautiful collection all nice and tidy like that is a very satisfying sensation, except I’m looking at it seated on the empty floor of my childhood bedroom, alone and bored. What is there to do when nothing needs to be improved? I recently wrote about my HTML and CSS “explorations” with this blog. Satisfied with the results, I think this job is done. The same goes for how Eleventy works on my machine: everything has been optimised , refined, future-proofed (especially Node.js ): nothing to see here! Even the hosting is something I’m very happy with. My only gripe with xmit is that there is no possibility for me to pay for it. The other apps on my Mac — the ones that don’t live in the Terminal like Eleventy, Node.js & npm, and xmit — are also perfect at what they do, and I can’t think of anything better to explore, let alone to use. If this is not your first visit, you already know how I feel about BBEdit . Well, I feel just about the same about NetNewsWire , which is as close to perfection an app can get as far as I’m concerned. It feels part of the OS (even more so than current system apps if I’m being honest), it is stable, it is simple to use, and it runs smoothly on my soon-to-be six-year-old MacBook Air. Being happy with Safari is by far the strongest proof that my setup is final. Using StopTheScript to block JavaScript on most media sites, along with the performance and privacy benefits of using a DNS resolver like Quad9 , has proven to be an efficient way to keep Safari light and responsive, even if my web experience is getting a little more interrupted than I would like, due to all the crap websites throw at first-time visitors these days. Yesterday, I had a look at apps like Yoink , Karabiner Elements , Hazel , and also got a taste of Mullvad Browser , and News Explorer . Some of these apps were tried purely out of curiosity, to see if they would fit right in my “workflow”, others were basically reassurance that my current system and choices were the best I could have made. * 1 Among all the parties involved in this setup, the obvious candidate for a replacement is my Intel-powered MacBook Air. Yet, this old computer is currently in great shape: the recent factory-settings reset I had to do surely helped. But its best feature is not being able to run MacOS Tahoe: stuck to MacOS Sequoia, it’s protecting me from Liquid Glass on the Mac and the “icons in menus everywhere” experience. My personal laptop is a breath of fresh air after spending hours on my work computer running Tahoe. * 2 So, what will be able to make that itch go away? When nothing is broken, don’t fix it, as they say. But surely, there must be something that I’m missing, surely there is a program, somewhere, that would delight me, that would put a smile on my face. I want a new box of Lego bricks, I want to empty my drawers on the floor and see if I can do better. In case you’re wondering, all of these apps are excellent, but not enough to replace what I already use, or to justify adding a new item to my list. For example, Mullvad Browser, like Firefox, isn’t scriptable; News Explorer has more features than NetNewsWire, but is not as polished; Yoink looks incredibly useful, but I prefer my own ways for now, &c. ^ Its replacement will have to wait until the new generation comes out, probably in March; then I can decide on whether I want to stick to the Air family, keep mine a bit longer, or upgrade for a far nicer screen and go with the Pro. ^ In case you’re wondering, all of these apps are excellent, but not enough to replace what I already use, or to justify adding a new item to my list. For example, Mullvad Browser, like Firefox, isn’t scriptable; News Explorer has more features than NetNewsWire, but is not as polished; Yoink looks incredibly useful, but I prefer my own ways for now, &c. ^ Its replacement will have to wait until the new generation comes out, probably in March; then I can decide on whether I want to stick to the Air family, keep mine a bit longer, or upgrade for a far nicer screen and go with the Pro. ^

0 views
Karboosx 3 days ago

Google Apps Script is amazing for automation

Ever heard of Google Apps Script? You're missing out! This free Google tool lets you automate tasks across all Google apps with custom JS scripts, no servers needed. I'll show you how I use it to magically update my nerdiflix site every time I add a video to a YouTube playlist. It's like having your own digital assistant for all the boring stuff!

0 views

Most Code is Just Cache

Claude Code has systematically begun to consume many of the SaaS apps I used to (or plan to) pay for. Why pay a subscription when I can "vibe code" a personal MVP in twenty minutes? I don’t worry about maintenance or vendor lock-in because, frankly, the code is disposable. If I need a new feature tomorrow, I don’t refactor—I just rebuild it. 1 Code is becoming just an ephemeral cache of my intent. Cartoon via Nano Banana. In this model, the ‘Source Code’ is the prompt and the context; the actual Python or Javascript that executes is just the binary. We still run the code because it’s thermodynamically efficient and deterministic, but we treat it as disposable. If the behavior needs to change, we don’t refactor the binary; we re-compile the intent. This shift has made me intolerant of static interfaces. I have stopped caring about software that doesn’t let me dump massive amounts of context into Gemini or Claude to just do the thing . If a product forces me to click buttons to execute a process that an LLM could intuit from a prompt, that product is already legacy. It forces us to question the permanence of the current model. We often make the mistake of assuming software—as we know it today—is a permanent fixture of human productivity. But if you zoom out, the era of SaaS is a blink of an eye in modern history. It is easy to overestimate how core it is to the future. In this post, I want to extrapolate these thoughts a bit and write out what could be the final stages of software. Thanks for reading Shrivu’s Substack! Subscribe for free to receive new posts and support my work. The stages here might not necessarily be chronological or mutually exclusive. Instead, they are ordered from static to dynamic code generation — where more and more the intent of a customer is the software they use. This is the baseline where software is a static artifact sold as a service, built on the assumption that user problems are repetitive and predictable enough to be solved by rigid workflows. To the consumer, this looks like dashboards, CRUD forms, and hardcoded automations. The intelligence here is sourced mainly from the SaaS founder and hired domain experts, hard-coded into business logic years before the user ever logs in. When: We recognized that distributing software via the cloud was more efficient than on-premise installations. Value Loop: Customer Problem → Product Manager writes PRD → Engineers write Static Code → Deploy → Customer adapts their workflow to the tool. (Time: Months to Years | Fit: Generic / One-size-fits-none) We are seeing this now with companies adopting the Forward Deployed Engineering (FDE) . In this stage, the SaaS company hires humans to manually use AI to build bespoke solutions for the client. For the consumer, this feels like a concierge service; they don’t get a login to a generic tool, they get a custom-built outcome delivered by a human who used AI to write the glue code. The intelligence is hybrid: the human provides the architecture, the AI writes the implementation code in weeks to days. When: Companies realize AI allows their employees to build custom apps for clients faster than the clients can learn or adapt a generic tool. Value Loop: Customer Problem → SaaS Employee (FDE) Prompts AI → AI generates Custom Script/App → Employee Deploys for Customer. (Time: Days | Fit: High / Tailored to specific customer edge cases) This is the current “safe space” for most tech companies, where they bolt an LLM onto an existing application to handle unstructured data. Consumers experience this as a “Draft Email” button in their CRM or a “Chat” sidebar in their UI—the platform is still the main product, but AI is a feature that (hopefully) reduces friction and/or provides some extra functionality customization 2 . The intelligence comes from a constrained model of product design and LLM scaffolding, providing content within a structure still strictly dictated by the SaaS platform’s code. When: People start to see AI is good at summarizing, generating content, or taking actions within existing workflows. Value Loop: Customer Problem → Static SaaS Interface AI Feature Text Box → Stochastic Result → Human Review. (Time: Minutes | Fit: Medium / Constrained by the platform’s UI) This is the tipping point where the software interface starts to disappear because the “interface” was just a way to collect context that the model can now ingest directly. Consumers move to a “Do this for me” interface where intent maps directly to an outcome rather than a button click, often realized as an agent calling a database or MCP servers 3 . The intelligence is the model and it’s engineered input context, relegating the SaaS role to in some sense providing clean proprietary data via an agent friendly interface. Software as a Service for Agents . When: People start to see AI is good at orchestrating complex decisions and using tools—across SaaS platforms—autonomously. Value Loop: Customer Problem (Prompt as ~PRD) → Runtime Code Generation → Dynamic Outcome. (Time: Real-time | Fit: Very High / Dynamically generated for the specific context) Critically, this doesn't mean the LLM acts as the CPU for every single user interaction (which would be latency-poor and energy-inefficient). Instead, the model almost acts as a Just-In-Time compiler. It generates the necessary code to execute the user’s intent, runs that code for the session, and then potentially discards it This is the end game in some cases. If code is just a cache for intent, eventually we bypass the cache and bake the intuition directly into the model. To the consumer, the “tool” is invisible; the expert system simply exists and provides answers or actions without a login or workflow. The intelligence is in the model itself; the software platform exists solely as a distillation mechanism—a gym to train the vertical AI—and once the model learns the domain, the software is no longer needed. A company in this stage is not really even SaaS anymore, maybe more so a AI-gyms-aaS company. When: People start to see AI is good at absorbing the entire vertical’s intuition. Value Loop: Raw Domain Data → Reinforcement Learning / Fine-Tuning → Model Weights. (Time: Instant / Pre-computed | Fit: Very High / Intuitive domain mastery) This might feel unintuitive as a stage — like how could you bake some proprietary data lake into a model? How can our juicy data not be the moat? My conclusion is that most (but not all) data is a transformation of rawer upstream inputs and that these transformation (data pipelines, cross-tenant analysis, human research, etc.) are all “cache” that can be distilled into a more general model that operates on its intuition and upstream platform inputs. “But can agents run a bank?” Reliability and safety comes down to distinguishing between guardrails (deterministic interfaces and scaffolding) and runtime execution (LLM code). For now, you don’t let the LLM invent the concept of a transaction ledger or rewrite the core banking loop on the fly. In XX years, maybe we do trust AI to write core transaction logic after all fail-able humans wrote the code for most mission critical software that exists today. The line between human-defined determinism and agent symbolic interfaces will gradually move of time. “But enterprise SaaS is actually super complex.” Yes, but that complexity is mostly just unresolved ambiguity. Your “deep enterprise understanding” is often a collection of thousands of edge cases—permissions, policy exceptions, region-specific rules—that humans had to manually hard-code into IF/ELSE statements over a decade. Distilled to the core, this complexity collapses. The model doesn’t need 500 hard-coded features; it needs the raw data and the intent. An app built for one can also make a lot of simplifications compared to one that acts as a platform. “Customers don’t want to prompt features.” I agree. I don’t think the future looks like a chatbot. “Chat” is a skeuomorphic bridge we use because we haven’t figured out the consistent native interface yet. It might be a UI that pre-emptively changes based on your role, or it might feel like hiring a really competent employee who just “takes care of it” without you needing to specify the . Or, as we see in Stage 2, the user never prompts at all—an FDE does it for them, and the user just gets a bespoke app that works perfectly. Stage 1, where most companies are stuck today, definitely is. Why? Because the sheer overhead of traditional SaaS—the learning curve, the rigid workflows, the "click tax" to get work done—is becoming unacceptable in a world where intent can be executed directly. It feels increasingly archaic when flexible solutions can be generated on demand. The value is moving away from the workflow logic itself and toward two specific layers that sandwich it: The Data Layer: Proprietary data, trust, and the “agentic scaffolding” that allows models to act safely within your domain. The Presentation Layer: Brand and UI. While I suspect trying to control the presentation layer long-term is futile (as users will eventually bring their own “interface agents” to interact with your data), for now, it remains a differentiator. We are going to see companies move through these tiers. The winners IMO will be the ones who realize that the "Service" part of SaaS is being replaced by model intelligence. The SaaS that remains will be the infrastructure of truth and the engine of agency. We are transitioning from a world of static artifacts (code that persists for years) to dynamic generations (code that exists for milliseconds or for a single answer). Of course, I could be wrong. Maybe AI capability plateaus before it can fully integrate into complex verticals. Maybe traditional SaaS holds the line at Stage 2 or 3, protecting its moat through sheer inertia. Maybe the world ends up more decentralized. Some of my open questions: Which stage should you work on today? Is there alpha in skipping straight to Stage 4, or do you need to build the Stage 2 “vibe coding” service to bootstrap for now? What are the interfaces of the future? Is it MCP, curated compute sandboxes, or a yet-to-be-defined agent-to-agent-to-human protocol? What interface wins out or does each company or consumer bring their own agentic worker? How fast does this happen? Are we looking at a multi-decade-long transition, or do companies today rapidly start dropping lower stage SaaS tools? Does AI have a similar impact beyond software? does medicine move from “static protocols” to “on-demand, patient-specific treatments”? Thanks for reading Shrivu’s Substack! Subscribe for free to receive new posts and support my work. Even more so than me you can see Geoffrey Huntley’s ralph-powered rampage of GitHub and many other tools . I liked this tweet by Harj Taggar, “moved away from the FDE playbook that’s become the default for fast growing AI startups. Instead they’ve built AI to covert plain English from the customer into Python code to make the product work for their use cases” . Similar to Karpathy’s “LLMs not as a chatbot, but the kernel process of a new Operating System” (2023) Cartoon via Nano Banana. In this model, the ‘Source Code’ is the prompt and the context; the actual Python or Javascript that executes is just the binary. We still run the code because it’s thermodynamically efficient and deterministic, but we treat it as disposable. If the behavior needs to change, we don’t refactor the binary; we re-compile the intent. This shift has made me intolerant of static interfaces. I have stopped caring about software that doesn’t let me dump massive amounts of context into Gemini or Claude to just do the thing . If a product forces me to click buttons to execute a process that an LLM could intuit from a prompt, that product is already legacy. It forces us to question the permanence of the current model. We often make the mistake of assuming software—as we know it today—is a permanent fixture of human productivity. But if you zoom out, the era of SaaS is a blink of an eye in modern history. It is easy to overestimate how core it is to the future. In this post, I want to extrapolate these thoughts a bit and write out what could be the final stages of software. Thanks for reading Shrivu’s Substack! Subscribe for free to receive new posts and support my work. Software Evolution The stages here might not necessarily be chronological or mutually exclusive. Instead, they are ordered from static to dynamic code generation — where more and more the intent of a customer is the software they use. Stage 1. Traditional SaaS This is the baseline where software is a static artifact sold as a service, built on the assumption that user problems are repetitive and predictable enough to be solved by rigid workflows. To the consumer, this looks like dashboards, CRUD forms, and hardcoded automations. The intelligence here is sourced mainly from the SaaS founder and hired domain experts, hard-coded into business logic years before the user ever logs in. When: We recognized that distributing software via the cloud was more efficient than on-premise installations. Value Loop: Customer Problem → Product Manager writes PRD → Engineers write Static Code → Deploy → Customer adapts their workflow to the tool. (Time: Months to Years | Fit: Generic / One-size-fits-none) When: Companies realize AI allows their employees to build custom apps for clients faster than the clients can learn or adapt a generic tool. Value Loop: Customer Problem → SaaS Employee (FDE) Prompts AI → AI generates Custom Script/App → Employee Deploys for Customer. (Time: Days | Fit: High / Tailored to specific customer edge cases) When: People start to see AI is good at summarizing, generating content, or taking actions within existing workflows. Value Loop: Customer Problem → Static SaaS Interface AI Feature Text Box → Stochastic Result → Human Review. (Time: Minutes | Fit: Medium / Constrained by the platform’s UI) When: People start to see AI is good at orchestrating complex decisions and using tools—across SaaS platforms—autonomously. Value Loop: Customer Problem (Prompt as ~PRD) → Runtime Code Generation → Dynamic Outcome. (Time: Real-time | Fit: Very High / Dynamically generated for the specific context) When: People start to see AI is good at absorbing the entire vertical’s intuition. Value Loop: Raw Domain Data → Reinforcement Learning / Fine-Tuning → Model Weights. (Time: Instant / Pre-computed | Fit: Very High / Intuitive domain mastery) The Data Layer: Proprietary data, trust, and the “agentic scaffolding” that allows models to act safely within your domain. The Presentation Layer: Brand and UI. While I suspect trying to control the presentation layer long-term is futile (as users will eventually bring their own “interface agents” to interact with your data), for now, it remains a differentiator. Which stage should you work on today? Is there alpha in skipping straight to Stage 4, or do you need to build the Stage 2 “vibe coding” service to bootstrap for now? What are the interfaces of the future? Is it MCP, curated compute sandboxes, or a yet-to-be-defined agent-to-agent-to-human protocol? What interface wins out or does each company or consumer bring their own agentic worker? How fast does this happen? Are we looking at a multi-decade-long transition, or do companies today rapidly start dropping lower stage SaaS tools? Does AI have a similar impact beyond software? does medicine move from “static protocols” to “on-demand, patient-specific treatments”?

12 views
devansh 4 days ago

ElysiaJS Cookie Signature Validation Bypass

The recent React CVE(s) made quite a buzz in the industry. It was a pretty powerful vulnerability, which directly leads to Pre-auth RCE (one of the most impactful vuln classes). The React CVE inspired me to investigate vulnerabilities in other JS/TS frameworks. I selected Elysia as my target for several reasons: active maintenance, ~16K GitHub stars, clear documentation, and clean codebase - all factors that make for productive security research. While scrolling through the codebase, one specific codeblock looked interesting: It took me less than a minute to identify the "anti-pattern" here. Can you see what's wrong here? We'll get to it in a bit, but first, a little primer on ElysiaJS Cookie Signing. Elysia treats cookies as reactive signals, meaning they're mutable objects you can read and update directly in your route handlers without getters/setters. Cookie signing adds a cryptographic layer to prevent clients from modifying cookie values (e.g., escalating privileges in a session token). Elysia uses a signature appended to the cookie value, tied to a secret key. This ensures integrity (data wasn't altered) and authenticity (it came from your server). On a higher level, it works something like this: Rotating secrets is essential for security hygiene (e.g., after a potential breach or periodic refresh). Elysia handles this natively with multi-secret support . This code is responsible for handling cookie related logic (signing, unsigning, secrets rotation). Now, going back to the vulnerability, can you spot the vulnerability in the below screenshot? No worries if you couldn't. I will walk you through. The guard check at the end ( ) becomes completely useless because can never be . This is dead code. You see now? Basically if you are using the vulnerable version of Elysia and using secrets array (secrets rotation); Complete auth bypass is possible because error never gets thrown. This seemed like a pretty serious issue, so I dropped a DM to Elysia's creator SaltyAom . SaltyAom quickly confirmed the issue At this point, we know that this is a valid issue, but we still need to create a PoC for it to showcase what it can do, so a security advisory could be created. Given my limited experience with Tyscript. I looked into the docs of Elysia and looked into sample snippets. After getting a decent understanding of syntax Elysia uses, it was time to create the PoC app using Elysia. I had the basic idea in my mind of how my PoC app would look like, It will have a protected resource only admin can access, and by exploiting this vulnerability I should be able to reach the protected resource without authenticating as admin or without even having admin cookies. Eventually, I came up with the following PoC for demonstrating impact: Without signing up as admin, or login, issue the following cURL command: We got access to protected content; without using an signed admin cookie. Pretty slick, no? The developer likely meant to write: Instead, they wrote: The attacker only needs to: That's literally it. This vulnerability was fixed in v1.4.19 With this fix in place, the verification logic now works correctly. Affected Versions : Elysia ≤ v1.4.18 ( confirmed ), potentially earlier versions Fixed Versions : v1.4.19 Elysia and Cookie Signing Secrets Rotation Vulnerability Proof of Concept What It Does Let's Break It Disclosure Timeline cookies.ts#L413-L426 Signing : When you set a cookie (e.g., profile.value = data), Elysia hashes the serialized value + secret, appends sig to the cookie. Unsigning/Verification : On read, Elysia checks the signature against the secret. If invalid (tampered or wrong secret), it throws an error or rejects the cookie. How It Works: Provide secrets as an array: [oldestDeprecated, ..., currentActive]. Tries the latest secret first for signing new cookies. For reading, it falls back sequentially through the array until a match (or fails). Sets ( assumes the cookie is valid before checking anything! ) Loops through each secret Calls for each secret If any secret successfully verifies, sets ( wait, it's already - this does nothing ), stores the unsigned value, and breaks If no secrets verify , the loop completes naturally without ever modifying Checks if is ... but it's still from step 1 No error is thrown - the tampered cookie is accepted as valid Allows one-time signup of an admin account only Allows an existing admin to log in . Issues a signed session cookie once logged in. Protects a secret route so only logged-in admin can access it. Capture or observe one valid cookie ( even their own ) Edit the cookie value to some other users' identify in their browser or with curl; and remove the signature Send it back to the server Discovery : 9th December 2025 Vendor Contact : 9th December 2025 Vendor Response : 9th December 2025 Patch Release : 13th December 2025 CVE Assignment : Pending Vulnerable Code: src/cookies.ts#L413-L426 Elysia Documentation: elysiajs.com Elysia Cookie Documentation: elysiajs.com/patterns/cookie

0 views
Simon Willison 5 days ago

Fly's new Sprites.dev addresses both developer sandboxes and API sandboxes at the same time

New from Fly.io today: Sprites.dev . Here's their blog post and YouTube demo . It's an interesting new product that's quite difficult to explain - Fly call it "Stateful sandbox environments with checkpoint & restore" but I see it as hitting two of my current favorite problems: a safe development environment for running coding agents and an API for running untrusted code in a secure sandbox. Disclosure: Fly sponsor some of my work. They did not ask me to write about Sprites and I didn't get preview access prior to the launch. My enthusiasm here is genuine. I predicted earlier this week that "we’re due a Challenger disaster with respect to coding agent security" due to the terrifying way most of us are using coding agents like Claude Code and Codex CLI. Running them in mode (aka YOLO mode, where the agent acts without constantly seeking approval first) unlocks so much more power, but also means that a mistake or a malicious prompt injection can cause all sorts of damage to your system and data. The safe way to run YOLO mode is in a robust sandbox, where the worst thing that can happen is the sandbox gets messed up and you have to throw it away and get another one. That's the first problem Sprites solves: That's all it takes to get SSH connected to a fresh environment, running in an ~8GB RAM, 8 CPU server. And... Claude Code and Codex and Gemini CLI and Python 3.13 and Node.js 22.20 and a bunch of other tools are already installed. The first time you run it neatly signs you in to your existing account with Anthropic. The Sprites VM is persistent so future runs of will get you back to where you were before. ... and it automatically sets up port forwarding, so you can run a localhost server on your Sprite and access it from on your machine. There's also a command you can run to assign a public URL to your Sprite, so anyone else can access it if they know the secret URL. In the blog post Kurt Mackey argues that ephemeral, disposable sandboxes are not the best fit for coding agents: The state of the art in agent isolation is a read-only sandbox. At Fly.io, we’ve been selling that story for years, and we’re calling it: ephemeral sandboxes are obsolete. Stop killing your sandboxes every time you use them. [...] If you force an agent to, it’ll work around containerization and do work . But you’re not helping the agent in any way by doing that. They don’t want containers. They don’t want “sandboxes”. They want computers. [...] with an actual computer, Claude doesn’t have to rebuild my entire development environment every time I pick up a PR. Each Sprite gets a proper filesystem which persists in between sessions, even while the Sprite itself shuts down after inactivity. It sounds like they're doing some clever filesystem tricks here, I'm looking forward to learning more about those in the future. There are some clues on the homepage : You read and write to fast, directly attached NVMe storage. Your data then gets written to durable, external object storage. [...] You don't pay for allocated filesystem space, just the blocks you write. And it's all TRIM friendly, so your bill goes down when you delete things. The really clever feature is checkpoints. You (or your coding agent) can trigger a checkpoint which takes around 300ms. This captures the entire disk state and can then be rolled back to later. For more on how that works, run this in a Sprite: Here's the relevant section: Or run this to see the for the command used to manage them: Which looks like this: I'm a big fan of Skills , the mechanism whereby Claude Code (and increasingly other agents too) can be given additional capabilities by describing them in Markdown files in a specific directory structure. In a smart piece of design, Sprites uses pre-installed skills to teach Claude how Sprites itself works. This means you can ask Claude on the machine how to do things like open up ports and it will talk you through the process. There's all sorts of interesting stuff in the folder on that machine - digging in there is a great way to learn more about how Sprites works. Also from my predictions post earlier this week: "We’re finally going to solve sandboxing" . I am obsessed with this problem: I want to be able to run untrusted code safely, both on my personal devices and in the context of web services I'm building for other people to use. I have so many things I want to build that depend on being able to take untrusted code - from users or from LLMs or from LLMs-driven-by-users - and run that code in a sandbox where I can be confident that the blast radius if something goes wrong is tightly contained. Sprites offers a clean JSON API for doing exactly that, plus client libraries in Go and TypeScript and coming-soon Python and Elixir . From their quick start: You can also checkpoint and rollback via the API, so you can get your environment exactly how you like it, checkpoint it, run a bunch of untrusted code, then roll back to the clean checkpoint when you're done. Managing network access is an important part of maintaining a good sandbox. The Sprites API lets you configure network access policies using a DNS-based allow/deny list like this: Sprites have scale-to-zero baked into the architecture. They go to sleep after 30 seconds of inactivity, wake up quickly when needed and bill you for just the CPU hours, RAM hours and GB-hours of storage you use while the Sprite is awake. Fly estimate a 4 hour intensive coding session as costing around 46 cents, and a low traffic web app with 30 hours of wake time per month at ~$4. (I calculate that a web app that consumes all 8 CPUs and all 8GBs of RAM 24/7 for a month would cost ((7 cents * 8 * 24 * 30) + (4.375 cents * 8 * 24 * 30)) / 100 = $655.2 per month, so don't necessarily use these as your primary web hosting solution for an app that soaks up all available CPU and RAM!) I was hopeful that Fly would enter the developer-friendly sandbox API market, especially given other entrants from companies like Cloudflare and Modal and E2B . I did not expect that they'd tackle the developer sandbox problem at the same time, and with the same product! My one concern here is that it makes the product itself a little harder to explain. I'm already spinning up some prototypes of sandbox-adjacent things I've always wanted to build, and early signs are very promising. I'll write more about these as they turn into useful projects. Update : Here's some additional colour from Thomas Ptacek on Hacker News: This has been in the works for quite awhile here. We put a long bet on "slow create fast start/stop" --- which is a really interesting and useful shape for execution environments --- but it didn't make sense to sandboxers, so "fast create" has been the White Whale at Fly.io for over a year. You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options . Developer sandboxes Storage and checkpoints Really clever use of Claude Skills A sandbox API Scale-to-zero billing Two of my favorite problems at once

1 views
Den Odell 1 weeks ago

The Main Thread Is Not Yours

When a user visits your site or app, their browser dedicates a single thread to running your JavaScript, handling their interactions, and painting what they see on the screen. This is the main thread , and it’s the direct link between your code and the person using it. As developers, we often use it without considering the end user and their device, which could be anything from a mid-range phone to a high-end gaming rig. We don’t think about the fact that the main thread doesn’t belong to us; it belongs to them. I’ve watched this mistake get repeated for years: we burn through the user’s main thread budget as if it were free, and then act surprised when the interface feels broken. Every millisecond you spend executing JavaScript is a millisecond the browser can’t spend responding to a click, updating a scroll position, or acknowledging that the user did just try to type something. When your code runs long, you’re not causing "jank" in some abstract technical sense; you’re ignoring someone who’s trying to talk to you. Because the main thread can only do one thing at a time, everything else waits while your JavaScript executes: clicks queue up, scrolls freeze, and keystrokes pile up hoping you’ll finish soon. If your code takes 50ms to respond nobody notices, but at 500ms the interface starts feeling sluggish, and after several seconds the browser may offer to kill your page entirely. Users don’t know why the interface isn’t responding. They don’t see your code executing; they just see a broken experience and blame themselves, then the browser, then you, in that order. Browser vendors have spent years studying how humans perceive responsiveness, and the research converged on a threshold: respond to user input within 100ms and the interaction feels instant, push past 200ms and users notice the delay. The industry formalized this as the Interaction to Next Paint (INP) metric , where anything over 200ms is considered poor and now affects your search rankings. But that 200ms budget isn’t just for your JavaScript. The browser needs time for style calculations, layout, and painting, so your code gets what’s left: maybe 50ms per interaction before things start feeling wrong. That’s your allocation from a resource you don’t own. The web platform has evolved specifically to help you be a better guest on the main thread, and many of these APIs exist because browser engineers got tired of watching developers block the thread unnecessarily. Web Workers let you run JavaScript in a completely separate thread. Heavy computation, whether parsing large datasets, image processing, or complex calculations, can happen in a Worker without blocking the main thread at all: Workers can’t touch the DOM, but that constraint is deliberate since it forces a clean separation between "work" and "interaction." requestIdleCallback lets you run code only when the browser has nothing better to do. (Due to a WebKit bug , Safari support is still pending at time of writing.) When the user is actively interacting, your callback waits; when things are quiet, your code gets a turn: This is ideal for non-urgent work like analytics, pre-fetching, or background updates. isInputPending (Chromium-only for now) is perhaps the most user-respecting API of the lot, because it lets you check mid-task whether someone is waiting for you: You’re explicitly asking "is someone trying to get my attention?" and if the answer is yes, you stop and let them. The obvious main thread crimes like infinite loops or rendering 100,000 table rows are easy to spot, but the subtle ones look harmless. Calling , for example, on a large API response blocks the main thread until parsing completes, and while this feels instant on a developer’s machine, a mid-range phone with a throttled CPU and competing browser tabs might take 300ms to finish the same operation, ignoring the user’s interactions the whole time. The main thread doesn’t degrade gracefully; it’s either responsive or it isn’t, and your users are running your code in conditions you’ve probably never tested. You can’t manage what you can’t measure, and Chrome DevTools’ Performance panel shows exactly where your main thread time goes if you know where to look. Find the "Main" track and watch for long yellow blocks of JavaScript execution. Tasks exceeding 50ms get flagged with red shading to mark the overtime portion. Use the Insights pane to surface these automatically if you prefer a guided approach. For more precise instrumentation, the API lets you time specific operations in your own code: The Web Vitals library can capture INP scores from real users across all major browsers in production, and when you see spikes you’ll know where to start investigating. Before your application code runs a single line, your framework has already spent some of the user’s main thread budget on initialization, hydration, and virtual DOM reconciliation. This isn’t an argument against frameworks so much as an argument for understanding what you’re spending. A framework that costs 200ms to hydrate has consumed four times your per-interaction budget before you’ve done anything, and that needs to be a conscious choice you’re making, rather than an accident. Some frameworks have started taking this seriously: Qwik’s "resumability" avoids hydration entirely, while React’s concurrent features let rendering yield to user input. These are all responses to the same fundamental constraint, which is that the main thread is finite and we’ve been spending it carelessly. The technical solutions matter, but they follow from a shift in perspective, and when I finally internalized that the main thread belongs to the user, not to me, my own decisions started to change. Performance stops being about how fast your code executes and starts being about how responsive the interface stays while your code executes. Blocking the main thread stops being an implementation detail and starts feeling like taking something that isn’t yours. The browser gave us a single thread of execution, and it gave our users that same thread for interacting with what we built. The least we can do is share it fairly.

0 views
JSLegendDev 1 weeks ago

The Phaser Game Framework in 5 Minutes

Phaser is the most popular JavaScript/TypeScript framework for making 2D games. It’s performant, and popular games like Vampire Survivors and PokéRogue were made with it. Because it’s a web-native framework, games made with Phaser are lightweight and generally load and run better on the web than the web exports produced by major game engines. For that reason, if you’re looking to make 2D web games, Phaser is a great addition to your toolbelt. In this post, I’ll explain the framework’s core concepts in around 5 minutes. — SPONSORED SEGMENT — In case you want to bring your web game to desktop platforms, today’s sponsor GemShell , allows you to build executables for Windows/Mac/Linux in what amounts to a click. It also makes Steam integration easy. For more info, visit 👉 https://l0om.itch.io/gemshell You have a tool/product you want featured in a sponsored segment? Contact me at [email protected] A Phaser project starts with defining a config to describe how the game’s canvas should be initialized. To make the game scale according to the window’s size, we can set the scale property in our config. The mode property is set to FIT, so the canvas scales while preserving its own aspect ratio. As for keeping the canvas centered on the page, the autoCenter property is used with the CENTER_BOTH value. Most games are composed of multiple scenes and switching between them is expected during the course of gameplay. Since Phaser uses the object oriented paradigm, a scene is created by defining a class that inherits from the Phaser.Scene class. To be able to reference the scene elsewhere in our code, it’s important to give it a key. For this purpose and for being able to use the methods and properties of the parent class we need to call the super constructor and pass to it the key we want to use. The two most important methods of a Phaser scene are the and methods. The first is used for, among other things, creating game objects like text and sprites and setting things like scores. It runs once, every time the scene becomes active. The latter which runs once per frame is, for example, used to handle movement logic. Once a scene is created, we still need to add it to our game. This is done in the Phaser config under a property called scenes, which expects an array. The order of scenes in this array is important. The first element will be used as the default scene of the game. To switch scenes, we can call the method of the scene manager. Before we can render sprites, we need to load them. For this purpose, a Phaser scene has access to the method where asset loading logic should be placed. To load an image, we can use the image method of the loader plugin. Then, in the method, we can render a sprite by calling the method of the Game Object Factory plugin. The first two params are for specifying the X and Y coordinates while the third param is for providing the key of the sprite to render. Because we created our sprite game object in the method, we don’t have access to it in our method, that’s why you’ll often see the pattern of assigning a game object to an instance field so it becomes accessible to other methods of the scene. Finally, movement logic code is placed in the method which runs every frame. Rendering text is similar to sprites. Rather than the using the method, we use the method. If you want to hold data or define custom methods for a sprite game object, a better approach is to define a class that inherits from the Phaser.GameObject.Sprite class. Once the class is defined, we can use it in our scene’s code. While asset loading can be done in any Phaser scene, a better approach is to create a scene dedicated to loading assets, which then switches to the main game scene once loading is complete. This can be achieved like shown below : Another important aspect of any game is the ability to play animations. Usually for 2D games, we have spritesheets containing all the needed frames to animate a character in a single image. An example of a spritesheet In Phaser, we first specify the dimensions of a frame in the loading logic of the spritesheet so that the framework knows how to slice the image into individual frames. Then, we can create an animation by defining its starting and ending frames. To provide the needed frames we call the method of Phaser’s animation manager. Finally once the animation is created, it can be played by using the method of the sprite game object. If you want the animation to loop back indefinitely, add the repeat property and set it to -1. A game needs to be interactive to be called a “game”. One way to handle input is by using event listeners provided by Phaser. For keyboard input, we can use : And for handling mouse and touch input we can use . At one point, you might need to share data between scenes. For this purpose, you can use Phaser’s registry. Here is an example of its usage. To play sounds (assuming you have already loaded the sound first) you can use the method of the sound manager. You can specify the sound’s volume in the second param of that method. If you need to be able to stop, pause or play the same sound at a later time, you can add it to the sound manager rather than playing it immediately. This comes in handy when you transition from one scene to another and you have a sound that loops indefinitely. In that case, you need to stop the sound before switching over otherwise the sound will keep playing in the next scene. By default, Phaser offers an Arcade physics system which is not meant for complex physics simulations. However, it’s well suited for most types of games. To enable it, you can add the following to your Phaser config. You can add an existing game object to the physics system the same way you add one to a scene. This will create a physics body for that game object which is accessible with the body instance field. You can view this body as a hitbox around your sprite if you turn on the debug mode in your project’s config. Example of a Phaser game with debug set to true To create bodies that aren’t affected by gravity, like platforms, you can create a static group and then create and add static bodies to that group. Here’s an example : You can also add already existing physics bodies to a group. Now, you might be wondering what groups are useful for? They shine in collision handling logic. Let’s assume you have multiple enemies attacking the player. To determine when a collision occurs between any enemy and the player, you can set up the following collision handler : There are many concepts I did not have time to cover. If you want to delve further into Phaser, I have a project based course you can purchase where I guide you through the process of building a Sonic themed infinite runner game. This is a great opportunity to put in practice what you’ve learned here. If you’re interested, here’s the link to the course : https://www.patreon.com/posts/learn-phaser-4-147473030 . That said, you can freely play the game being built in the course as well as have access to the final source code. Original Phaser game live demo : https://jslegend.itch.io/sonic-ring-run-phaser-4 Demo of the version built in the course : https://jslegend.itch.io/sonic-runner-tutorial-build Final source code : https://github.com/JSLegendDev/sonic-runner-phaser-tutorial If you enjoy technical posts like this one, I recommend subscribing to not miss out on future releases. Subscribe now In the meantime, you can read the following : Phaser is the most popular JavaScript/TypeScript framework for making 2D games. It’s performant, and popular games like Vampire Survivors and PokéRogue were made with it. Because it’s a web-native framework, games made with Phaser are lightweight and generally load and run better on the web than the web exports produced by major game engines. For that reason, if you’re looking to make 2D web games, Phaser is a great addition to your toolbelt. In this post, I’ll explain the framework’s core concepts in around 5 minutes. — SPONSORED SEGMENT — In case you want to bring your web game to desktop platforms, today’s sponsor GemShell , allows you to build executables for Windows/Mac/Linux in what amounts to a click. It also makes Steam integration easy. For more info, visit 👉 https://l0om.itch.io/gemshell You have a tool/product you want featured in a sponsored segment? Contact me at [email protected] The Phaser Config A Phaser project starts with defining a config to describe how the game’s canvas should be initialized. To make the game scale according to the window’s size, we can set the scale property in our config. The mode property is set to FIT, so the canvas scales while preserving its own aspect ratio. As for keeping the canvas centered on the page, the autoCenter property is used with the CENTER_BOTH value. Scene Creation Most games are composed of multiple scenes and switching between them is expected during the course of gameplay. Since Phaser uses the object oriented paradigm, a scene is created by defining a class that inherits from the Phaser.Scene class. To be able to reference the scene elsewhere in our code, it’s important to give it a key. For this purpose and for being able to use the methods and properties of the parent class we need to call the super constructor and pass to it the key we want to use. The two most important methods of a Phaser scene are the and methods. The first is used for, among other things, creating game objects like text and sprites and setting things like scores. It runs once, every time the scene becomes active. The latter which runs once per frame is, for example, used to handle movement logic. Hooking Up a Scene to Our Game Once a scene is created, we still need to add it to our game. This is done in the Phaser config under a property called scenes, which expects an array. The order of scenes in this array is important. The first element will be used as the default scene of the game. Switching Scenes To switch scenes, we can call the method of the scene manager. Rendering Sprites Before we can render sprites, we need to load them. For this purpose, a Phaser scene has access to the method where asset loading logic should be placed. To load an image, we can use the image method of the loader plugin. Then, in the method, we can render a sprite by calling the method of the Game Object Factory plugin. The first two params are for specifying the X and Y coordinates while the third param is for providing the key of the sprite to render. Because we created our sprite game object in the method, we don’t have access to it in our method, that’s why you’ll often see the pattern of assigning a game object to an instance field so it becomes accessible to other methods of the scene. Finally, movement logic code is placed in the method which runs every frame. Rendering Text Rendering text is similar to sprites. Rather than the using the method, we use the method. Entity Creation If you want to hold data or define custom methods for a sprite game object, a better approach is to define a class that inherits from the Phaser.GameObject.Sprite class. Once the class is defined, we can use it in our scene’s code. Asset Loading While asset loading can be done in any Phaser scene, a better approach is to create a scene dedicated to loading assets, which then switches to the main game scene once loading is complete. This can be achieved like shown below : Animation API Another important aspect of any game is the ability to play animations. Usually for 2D games, we have spritesheets containing all the needed frames to animate a character in a single image. An example of a spritesheet In Phaser, we first specify the dimensions of a frame in the loading logic of the spritesheet so that the framework knows how to slice the image into individual frames. Then, we can create an animation by defining its starting and ending frames. To provide the needed frames we call the method of Phaser’s animation manager. Finally once the animation is created, it can be played by using the method of the sprite game object. If you want the animation to loop back indefinitely, add the repeat property and set it to -1. Input Handling A game needs to be interactive to be called a “game”. One way to handle input is by using event listeners provided by Phaser. For keyboard input, we can use : And for handling mouse and touch input we can use . Sharing Data Between Scenes At one point, you might need to share data between scenes. For this purpose, you can use Phaser’s registry. Here is an example of its usage. Playing Sound To play sounds (assuming you have already loaded the sound first) you can use the method of the sound manager. You can specify the sound’s volume in the second param of that method. If you need to be able to stop, pause or play the same sound at a later time, you can add it to the sound manager rather than playing it immediately. This comes in handy when you transition from one scene to another and you have a sound that loops indefinitely. In that case, you need to stop the sound before switching over otherwise the sound will keep playing in the next scene. Physics, Debug Mode, Physics Bodies and Collision Logic By default, Phaser offers an Arcade physics system which is not meant for complex physics simulations. However, it’s well suited for most types of games. To enable it, you can add the following to your Phaser config. You can add an existing game object to the physics system the same way you add one to a scene. This will create a physics body for that game object which is accessible with the body instance field. You can view this body as a hitbox around your sprite if you turn on the debug mode in your project’s config. Example of a Phaser game with debug set to true To create bodies that aren’t affected by gravity, like platforms, you can create a static group and then create and add static bodies to that group. Here’s an example : You can also add already existing physics bodies to a group. Now, you might be wondering what groups are useful for? They shine in collision handling logic. Let’s assume you have multiple enemies attacking the player. To determine when a collision occurs between any enemy and the player, you can set up the following collision handler : Project Based Tutorial There are many concepts I did not have time to cover. If you want to delve further into Phaser, I have a project based course you can purchase where I guide you through the process of building a Sonic themed infinite runner game. This is a great opportunity to put in practice what you’ve learned here. If you’re interested, here’s the link to the course : https://www.patreon.com/posts/learn-phaser-4-147473030 . That said, you can freely play the game being built in the course as well as have access to the final source code. Original Phaser game live demo : https://jslegend.itch.io/sonic-ring-run-phaser-4 Demo of the version built in the course : https://jslegend.itch.io/sonic-runner-tutorial-build Final source code : https://github.com/JSLegendDev/sonic-runner-phaser-tutorial

0 views
Lea Verou 1 weeks ago

Web dependencies are broken. Can we fix them?

Abstraction is the cornerstone of modern software engineering. Reusing logic and building higher-level solutions from lower-level building blocks is what makes all the technological wonders around us possible. Imagine if every time anyone wrote a calculator they also had to reinvent floating-point arithmetic and string encoding! In healthy ecosystems dependencies are normal, cheap, and first-class. “Dependency-free” is not a badge of honor. And yet, the web platform has outsourced this fundamental functionality to third-party tooling . As a result, code reuse has become a balancing of tradeoffs that should not have existed in the first place. In NodeJS, you just and reference specifiers straight away in your code. Same in Python, with . Same in Rust with . In healthy ecosystems you don’t ponder how or whether to use dependencies. The ecosystem assumes dependencies are normal, cheap, and first-class . You just install them, use them, and move on. “Dependency-free” is not a badge of honor. Instead, dependency management in the web platform consists of bits and bobs of scattered primitives, with no coherent end-to-end solution . Naturally, bundlers such as Webpack , rollup , and esbuild have picked up the slack, with browserify being the one that started it all, in 2012. There is nothing wrong with bundlers when used as a performance optimization to minimize waterfall effects and overhead from too many HTTP requests. You know, what a bundler is supposed to do. It is okay to require advanced tools for advanced needs , and performance optimization is generally an advanced use case. Same for most other things bundlers and build tools are used for, such as strong typing, linting, or transpiling. All of these are needs that come much later than dependency management, both in a programmer’s learning journey, as well as in a project’s development lifecycle. Dependency management is such a basic and ubiquitous need, it should be a part of the platform, decoupled from bundling. Requiring advanced tools for basic needs is a textbook usability cliff . In other ecosystems, optimizations happen (and are learned) after dependency resolution. On the web, optimization is the price of admission! This is not normal. Bundlers have become so ubiquitous that most JS developers cannot even imagine deploying code without them. READMEs are written assuming a bundler, without even mentioning the assumption. It’s just how JS is consumed. My heart breaks for the newbie trying to use a drag and drop library, only to get mysterious errors about specifiers that failed to resolve. However, bundling is not technically a necessary step of dependency management. Importing files through URLs is natively supported in every browser, via ESM imports. HTTP/2 makes importing multiple small files far more reasonable than it used to be — at least from a connection overhead perspective. You can totally get by without bundlers in a project that doesn’t use any libraries. But the moment you add that first dependency, everything changes. You are suddenly faced with a huge usability cliff : which bundler to use, how to configure it, how to deploy with it, a mountain of decisions standing between you and your goal of using that one dependency. That one drag and drop library. For newcomers, this often comes very early in their introduction to the web platform, and it can be downright overwhelming. It is technically possible to use dependencies without bundlers, today. There are a few different approaches, and — I will not sugarcoat it — they all suck . There are three questions here: There is currently no good answer to any of them, only fragile workarounds held together by duct tape. Using a dependency should not need any additional song and dance besides “install this package” + “now import it here”. That’s it. That’s the minimum necessary to declare intent . And that’s precisely how it works in NodeJS and other JS runtimes. Anything beyond that is reducing signal-to-noise ratio , especially if it needs to be done separately for every project or worse, for every dependency. You may need to have something to bite hard on while reading the next few sections. It’s going to be bad . Typically, package managers like take care of deduplicating compatible package versions and may use a directory like to install packages. In theory, one could deploy as part of their website and directly reference files in client-side JS. For example, to use Vue : It works out of the box, and is a very natural thing to try the first time you install a package and you notice . Great, right? No. Not great. First, deploying your entire directory is both wasteful , and a security risk . In fact, most serverless hosts (e.g. Netlify or Vercel ) automatically remove it from the publicly deployed files after the build is finished. Additionally, it violates encapsulation : paths within a package are generally seen as an implementation detail of the package itself, and packages expose specifier exports like or that they map to internal paths. If you decide to circumvent this and link to files directly, you now need to update your import paths whenever you update the package. It is also fragile, as not every module is installed directly in — though those explicitly marked as app dependencies are. Another common path is importing from CDNs like Unpkg and JSDelivr . For Vue, it would look like this: It’s quick and easy. Nothing to install or configure! Great, right? No. Not great. It is always a bad idea to introduce a dependency on a whole other domain you do not control , and an even worse one when linking to executable code. First, there is the obvious security risk. Unless you link to a specific version, down to the patch number and/or use SRI , the resource could turn malicious overnight under your nose if the package is compromised. And even if you link to a specific version, there is always the risk that the CDN itself could get compromised. Who remembers polyfill.io ? But even supply-chain attacks aside, any third-party domain is an unnecessary additional point of failure . I still remember scrambling to change JSDelivr URLs to Unpkg during an outage right before one of my talks, or having to hunt down all my repos that used RawGit URLs when it sunset, including many libraries. The DX is also suboptimal. You lose the immediacy and resilience of local, relative paths. Without additional tooling ( Requestly , file edits, etc.), you now need to wait for CDN roundtrips even during local development. Wanted to code on a flight? Good luck. Needed to show a live demo during a talk, over clogged conference wifi? Maybe sacrifice a goat to the gods first. And while they maintain encapsulation slightly better than raw file imports, as they let you reference a package by its name for its default export, additional specifiers (e.g. ) typically still require importing by file path. “But with public CDNs, I benefit from the resource having already been cached by another website the user visited!” Oh my sweet summer child. I hate to be the one to break it to you, but no, you don’t, and that has been the case since about 2020 . Double keyed caching obliterated this advantage . In case you were not aware, yes, your browser will redownload every single resource anew for every single website (origin) that requests it. Yes, even if it’s exactly the same. This changed to prevent cross-site leaks : malicious websites could exfiltrate information about your past network activity by measuring how long a resource took to download, and thus infer whether it was cached. Those who have looked into this problem claim that there is no other way to prevent these timing attacks other than to actually redownload the resource. No way for the browser to even fake a download by simply delaying the response. Even requiring resources to opt-in (e.g. via CORS) was ruled out, the concern being that websites could then use it as a third-party tracking mechanism. I personally have trouble accepting that such wasteful bandwidth usage was the best balance of tradeoffs for all Web users , including those in emerging economies and different locales [1] . It’s not that I don’t see the risks — it’s that I am acutely aware of the cost, a cost that is disproportionately borne by those not in the Wealthy Western Web . How likely is it that a Web user in Zimbabwe, where 1 GB of bandwidth costs 17% of the median monthly income , would choose to download React or nine weights of Roboto thousands of times to avoid seeing personalized ads? And how patronizing is it for people in California to be making this decision for them? A quick and dirty way to get local URLs for local development and CDN URLs for the remote site is to link to relative URLs, and add a URL rewrite to a CDN if that is not found. E.g. with Netlify rewrites this looks like this: Since is not deployed, this will always redirect on the remote URL, while still allowing for local URLs during development. Great, right? No. Not great. Like the mythical hydra, it solves one problem and creates two new ones. First, it still carries many of the same issues of the approaches it combines: Additionally, it introduces a new problem: the two files need to match, but the naïve approach above would always just link to the latest version. Sure, one could alleviate this by building the file with tooling, to link to specific versions, read from . But the point is not that it’s insurmountable, but that it should not be this hard . Another solution is a lightweight build script that copies either entire packages or specific exports into a directory that will actually get deployed. When dependencies are few, this can be as simple as an npm script: So now we have our own nice subset of and we don’t depend on any third-party domains. Great, right? No. Not great. Just like most other solution, this still breaks encapsulation, forcing us to maintain a separate, ad-hoc index of specifiers to file paths. Additionally, it has no awareness of the dependency graph. Dependencies of dependencies need to be copied separately. But wait a second. Did I say dependencies of dependencies? How would that even work? In addition to their individual flaws, all of the solutions above share a major flaw: they can only handle importing dependency-free packages . But what happens if the package you’re importing also uses dependencies? It gets unimaginably worse my friend, that’s what happens. There is no reasonable way for a library author to link to dependencies without excluding certain consumer workflows. There is no local URL a library author can use to reliably link to dependencies, and CDN URLs are highly problematic. Specifiers are the only way here. So the moment you include a dependency that uses dependencies, you’re forced into specifier-based dependency management workflows , whether these are bundlers, or import map flavored JSON vomit in every single HTML page (discussed later). As a fig leaf, libraries will often provide a “browser” bundle that consumers can import instead of their normal , which does not use specifiers. This combines all their dependencies into a single dependency-free file that you can import from a browser. This means they can use whatever dependencies they want, and you can still import that bundle using regular ESM imports in a browser, sans bundler. Great, right? No. Not great. It’s called a bundle for a reason. It bundles all their dependencies too, and now they cannot be shared with any other dependency in your tree, even if it’s exactly the same version of exactly the same package. You’re not avoiding bundling, you’re outsourcing it , and multiplying the size of your JS code in the process. And if the library author has not done that, you’re stuck with little to do, besides a CDN that rewrites specifiers on the fly like esm.sh , with all CDN downsides described above. As someone who regularly releases open source packages ( some with billions of npm installs ), I find this incredibly frustrating. I want to write packages that can be consumed by people using or not using bundlers, without penalizing either group , but the only way to do that today is to basically not use any dependencies. I cannot even modularize my own packages without running into this! This doesn’t scale. Browsers can import specifiers, as long as the mapping to a URL is explicitly provided through an import map . Import maps look like this: Did you notice something? Yes, this is an HTML block. No, I cannot link to an import map that lives in a separate file. Instead, I have to include the darn thing in. Every. Single. Page. The moment you decide to use JS dependencies, you now need an HTML templating tool as well. 🙃 “💡 Oh I know, I’ll generate this from my library via DOM methods! ” I hear you say. No, my sweet summer child. It needs to be present at parse time. So unless you’re willing to it (please don’t), the answer is a big flat NOPE. “💡 Ok, at least I’ll keep it short by routing everything through a CDN or the same local folder ” No, my sweet summer child. Go to sleep and dream of globs and URLPatterns . Then wake up and get to work, because you actually need to specify. Every. Single. Mapping. Yes, transitive dependencies too. You wanted to use dependencies? You will pay with your blood, sweat, and tears. Or, well, another build tool. So now I need a build tool to manage the import map , like JSPM . It also needs to talk to my HTML templating tool, which I now had to add so it can spit out these import maps on. Every. Single. HTML. Page. There are three invariants that import maps violate: Plus, you still have all of the issues discussed above, because you still need URLs to link to. By trying to solve your problem with import maps, you now got multiple problems. To sum up, in their current form, import maps don’t eliminate bundlers — they recreate them in JSON form, while adding an HTML dependency and worse latency. Given the current state of the ecosystem, not using bundlers in any nontrivial application does seem like an exercise in masochism. Indeed, per State of JS 2024 , bundlers were extremely popular, with Webpack having been used by 9 in 10 developers and having close to 100% awareness! But sorting by sentiment paints a different picture, with satisfaction, interest, and positivity dropping year after year. Even those who never question the status quo can feel it in their gut that this is not okay. This is not a reasonable way to manage dependencies. This is not a healthy ecosystem. Out of curiosity, I also ran two polls on my own social media. Obviously, this suffers from selection bias , due to the snowball sampling nature of social media, but I was still surprised to see such a high percentage of bundle-less JS workflows: I’m very curious how these folks manage the problems discussed here. Oftentimes when discussing these issues, I get the question “but other languages are completely compiled, why is it a problem here?”. Yes, but their compiler is official and always there. You literally can’t use the language without it. The problem is not compilation, it’s fragmentation. It’s the experience of linking to a package via a browser import only to see errors about specifiers. It’s adding mountains of config and complexity to use a utility function. It’s having no clear path to write a package that uses another package, even if both are yours. Abstraction itself is not something to outsource to third-party tools. This is the programming equivalent of privatizing fundamental infrastructure — roads, law enforcement, healthcare — systems that work precisely because everyone can rely on them being there. Like boiling frogs , JS developers have resigned themselves to immense levels of complexity and gruntwork as simply how things are . The rise of AI introduced swaths of less technical folks to web development and their overwhelm and confusion is forcing us to take a long hard look at the current shape of the ecosystem — and it’s not pretty. Few things must always be part of a language’s standard library, but dependency management is absolutely one of them. Any cognitive overhead should be going into deciding which library to use, not whether to include it and how . This is also actively harming web platform architecture . Because bundlers are so ubiquitous, we have ended up designing the platform around them, when it should be the opposite. For example, because is unreliable when bundlers are used, components have no robust way to link to other resources (styles, images, icons, etc.) relative to themselves, unless these resources can be part of the module tree. So now we are adding features to the web platform that break any reasonable assumption about what HTML, CSS, and JS are, like JS imports for CSS and HTML, which could have been a simple if web platform features could be relied on. And because using dependencies is nontrivial, we are adding features to the standard library that could have been userland or even browser-provided dependencies. To reiterate, the problem isn’t that bundlers exist — it’s that they are the only viable way to get first-class dependency management on the web. JS developers deserve better. The web platform deserves better. As a web standards person, my first thought when spotting such a lacking is “how can the web platform improve?”. And after four years in the TAG , I cannot shake the holistic architectural perspective of “which part of the Web stack is best suited for this?” Before we can fix this, we need to understand why it is the way it is. What is the fundamental reason the JS ecosystem overwhelmingly prefers specifiers over URLs? On the surface, people often quote syntax, but that seems to be a red herring. There is little DX advantage of (a specifier) over (a URL), or even (which can be configured to have a JS MIME type). Another oft-cited reason is immutability: Remote URLs can change, whereas specifiers cannot. This also appears to be a red herring: local URLs can be just as immutable as specifiers. Digging deeper, it seems that the more fundamental reason has to do with purview . A URL is largely the same everywhere, whereas can resolve to different things depending on context. A specifier is app-controlled whereas a URL is not. There needs to be a standard location for a dependency to be located and referenced from, and that needs to be app-controlled. Additionally, specifiers are universal . Once a package is installed, it can be imported from anywhere, without having to work out paths. The closest HTTP URLs can get to this is root-relative URLs, and that’s still not quite the same. Specifiers are clearly the path of least resistance here, so the low hanging fruit would be to make it easier to map specifiers to URLs, starting by improving import maps. An area with huge room for improvement here is import maps . Both making it easier to generate and include import maps, and making the import maps themselves smaller, leaner, and easier to maintain. The biggest need here is external import maps , even if it’s only via . This would eliminate the dependency on HTML templating and opens the way for generating them with a simple build tool. This was actually part of the original import map work , and was removed from the spec due to lack of implementer interest, despite overwhelming demand. In 2022, external import maps were prototyped in WebKit (Safari), which prompted a new WHATWG issue . Unfortunately, it appears that progress has since stalled once more. External import maps do alleviate some of the core pain points, but are still globally managed in HTML, which hinders composability and requires heavier tooling. What if import maps could be imported into JS code? If JS could import import maps, (e.g. via ), this would eliminate the dependency on HTML altogether, allowing for scripts to localize their own import info, and for the graph to be progressively composed instead of globally managed. Going further, import maps via an HTTP header (e.g. ) would even allow webhosts to generate them for you and send them down the wire completely transparently. This could be the final missing piece for making dependencies truly first-class. Imagine a future where you just install packages and use specifiers without setting anything up, without compiling any files into other files, with the server transparently handling the mapping ! However, import maps need URLs to map specifiers to, so we also need some way to deploy the relevant subset of to public-facing URLs, as deploying the entire directory is not a viable option. One solution might be a way to explicitly mark dependencies as client side , possibly even specific exports. This would decouple detection from processing app files: in complex apps it can be managed via tooling, and in simple apps it could even be authored manually, since it would only include top-level dependencies. Even if we had better ways to mark which dependencies are client-side and map specifiers to URLs, these are still pieces of the puzzle, not the entire puzzle. Without a way to figure out what depends on what, transitive dependencies will still need to be managed globally at the top level, defeating any hope of a tooling-light workflow. The current system relies on reading and parsing thousands of files to build the dependency graph. This is reasonable for a JS runtime where the cost of file reads is negligible, but not for a browser where HTTP roundtrips are costly. And even if it were, this does not account for any tree-shaking. Think of how this works when using URLs: modules simply link to other URLs and the graph is progressively composed through these requests. What if specifiers could work the same way? What if we could look up and route specifiers when they are actually imported? Here’s a radical idea: What if specifiers were just another type of URL , and specifier resolution could be handled by the server in the same way a URL is resolved when it is requested? They could use a protocol, that can be omitted in certain contexts, such as ESM imports. How would these URLs be different than regular local URLs? Architecturally, this has several advantages: Obviously, this is just a loose strawman at this point, and would need a lot of work to turn into an actual proposal (which I’d be happy to help out with, with funding ), but I suspect we need some way to bridge the gap between these two fundamentally different ways to import modules. Too radical? Quite likely. But abstraction is foundational, and you often need radical solutions to fix foundational problems. Even if this is not the right path, I doubt incremental improvements can get us out of this mess for good. But in the end, this is about the problem . I’m much more confident that the problem needs solving, than I am of any particular solution. Hopefully, after reading this, so are you. So this is a call to action for the community. To browser vendors, to standards groups, to individual developers. Let’s fix this! 💪🏼 Thanks to Jordan Harband , Wes Todd , and Anne van Kesteren for reviewing earlier versions of this draft. In fact, when I was in the TAG, Sangwhan Moon and I drafted a Finding on the topic, but the TAG never reached consensus on it. ↩︎ Use specifiers or URLs? How to resolve specifiers to URLs? Which URL do my dependencies live at? Linking to CDNs is inherently insecure It breaks encapsulation of the dependencies Locality : Dependency declarations live in HTML, not JS. Libraries cannot declare their own dependencies. Composability : Import maps do not compose across dependencies and require global coordination Scalability : Mapping every transitive dependency is not viable without tooling Twitter/X poll : 17.6% of respondents Mastodon poll : 40% (!) of respondents Their protocol would be implied in certain contexts — that would be how we can import bare specifiers in ESM Their resolution would be customizable (e.g. through import maps, or even regular URL rewrites) Despite looking like absolute URLs, their resolution would depend on the request’s header (thus allowing different modules to use different versions of the same dependency). A request to a URL without an header would fail. HTTP caching would work differently; basically in a way that emulates the current behavior of the JS module cache. It bridges the gap between specifiers and URLs . Rather than having two entirely separate primitives for linking to a resource, it makes specifiers a high-level primitive and URLs the low-level primitive that explains it. It allows retrofitting specifiers into parts of the platform that were not designed for them, such as CSS . This is not theoretical: I was at a session at TPAC where bringing specifiers to CSS was discussed. With this, every part of the platform that takes URLs can now utilize specifiers, it would just need to specify the protocol explicitly. In fact, when I was in the TAG, Sangwhan Moon and I drafted a Finding on the topic, but the TAG never reached consensus on it. ↩︎

0 views
JSLegendDev 1 weeks ago

Learn Phaser 4 by Building a Sonic Themed Infinite Runner Game in JavaScript

Phaser is the most popular JavaScript/TypeScript framework for making 2D games. It is performant and popular games like Vampire Survivors and Pokérogue were made with it. Because it’s a web-native framework, games built with it are lightweight and generally load and run better on the web than web exports produced by major game engines. For this reason, if you’re a web developer looking to make 2D web games, Phaser is a great addition to your toolbelt. To make the process of learning Phaser easier, I have released a course that takes you through the process of building a Sonic themed infinite runner game with Phaser 4 and JavaScript. You can purchase the course here : h ttps://www.patreon.com/posts/learn-phaser-4-147473030 . Total length of the course is 1h 43min. More details regarding content and prerequisites are included in the link. That said, you can freely play the game being built in the course as well as have access to the final source code. Original Phaser game live demo : https://jslegend.itch.io/sonic-ring-run-phaser-4 Demo of the version built in the course : https://jslegend.itch.io/sonic-runner-tutorial-build Final source code : https://github.com/JSLegendDev/sonic-runner-phaser-tutorial Phaser is the most popular JavaScript/TypeScript framework for making 2D games. It is performant and popular games like Vampire Survivors and Pokérogue were made with it. Because it’s a web-native framework, games built with it are lightweight and generally load and run better on the web than web exports produced by major game engines. For this reason, if you’re a web developer looking to make 2D web games, Phaser is a great addition to your toolbelt. To make the process of learning Phaser easier, I have released a course that takes you through the process of building a Sonic themed infinite runner game with Phaser 4 and JavaScript. You can purchase the course here : h ttps://www.patreon.com/posts/learn-phaser-4-147473030 . Total length of the course is 1h 43min. More details regarding content and prerequisites are included in the link. That said, you can freely play the game being built in the course as well as have access to the final source code. Original Phaser game live demo : https://jslegend.itch.io/sonic-ring-run-phaser-4 Demo of the version built in the course : https://jslegend.itch.io/sonic-runner-tutorial-build Final source code : https://github.com/JSLegendDev/sonic-runner-phaser-tutorial

0 views

Prolog Basics Explained with Pokémon

The project that inspired this post is a little silly—I am about to describe the mechanics of a children’s video game in great detail—but this particular problem is what finally made Prolog click for me, an epiphany I’ve been hunting for ever since reading Bruce Tate’s “Seven Languages in Seven Weeks.” This exercise has taught me a lot about the kinds of interfaces I’m trying to build in somewhat more practical domains . For certain kinds of relationships, logic programming is by far the most concise and expressive programming system I’ve ever used. To understand why, let’s talk about Pokémon. Pokémon is a video game series/multimedia franchise/lifestyle brand set in a world where humans live alongside a menagerie of colorful animal characters. “Pokémon” is both the name of the franchise and the generic term for the animal characters themselves, which all have their own individual species names. There are over a thousand distinct species of Pokémon, from Bulbasaur ( #1 ) to Pecharunt ( #1025 ). There are all sorts of Pokémon games now, but the main series has always been about catching and battling them. During a battle, your team of six Pokémon faces off against another team. Each Pokémon is equipped with four moves that it can choose to (usually) do damage to their opponent. You need to reduce the HP (Hit Points) of all your opponent’s Pokémon to zero before they are able to do so to you. Each Pokémon has unique traits that affects how it battles. They have a set of base stats, a large pool of possible moves, a handful of abilities, and a typing. As you will see in a moment, the immense number of combinations here is the motivation for trying to track this with software. Typing is especially important. Moves have a type, like Fire or Rock, and Pokémon can have up to two types. A move with a type that is Super Effective against the opposing Pokémon will do double damage; a move that is Not Very Effective will do half damage. It’s a little more intuitive with examples. The Fire-type move Flamethrower will do 2x to Grass-type Pokémon, because Grass is weak to Fire, but the Water-type move Surf will only do ½ damage to them, because Grass resists Water. Type modifiers can stack. Scizor is a Bug/Steel type, and both Bug and Steel are weak to Fire, so Fire moves will do 4x damage to Scizor. Electric is weak to Water, but Ground is immune, so if you use an Electric type move against Water/Ground Swampert , you’ll do zero damage, since 0×2 is still 0. Naturally, there is a chart to help you keep track. Those are effectively the mechanics of the Pokémon video games as I understood them when I was 8. Click moves to do damage, try to click moves with good type matchups. These games are for children and, at the surface level, they’re not very hard. Before I explain how wonky the Pokémon mechanics can get under the hood, I first need to explain how logic programming works. Pokémon is a great fit for logic programming because Pokémon battles are essentially an extremely intricate rules engine. Let’s start by creating a file with a bunch of facts. In Prolog, we declare “predicates.” Predicates define relationships: is a , is a , and so on. We refer to this predicate as , because the name of the predicate is and it has one argument. These facts are loaded into an interactive prompt called the “top-level.” You query the top-level by typing a statement into the prompt; Prolog tries to find all the ways to make that statement true. When there’s more than one possible solution, the top-level displays the first solution and then awaits user input. You can then have it display one more solution, all the solutions, or stop entirely. In this first example, we type and hit Enter. The top-level replies Squirtle is, in fact, a Pokémon. Not all things are Pokémon. Let’s add Pokémon types in there, as the predicate . Recall that some Pokémon have just one type while others have two. In the latter case, that’s modeled with two facts. Bulbasaur is a Grass type, and Bulbasaur is a Poison type; both are true. The paradigm is similar to a One-To-Many relation in a SQL database. Interactively, we can confirm whether Squirtle is a water type. Can we state that Squirtle is a Grass type? No, because Squirtle is a Water type. Suppose we didn’t know what type Squirtle was. We can ask! In Prolog, names that start with an upper-case letter are variables. Prolog tries to “unify” the predicate with all possible matches for the variable. There’s only one way to make this particular predicate true though: has to be , because Squirtle’s only type is Water. For Pokémon with two types, the predicate unifies twice. Semantically, that leading semicolon on the third line means “or.” is true when or when . Any of the terms can be be a variable, which means we can ask questions in any direction. What are all the Grass types? Just make the first argument the variable, and set the second argument to . I cut it off, but the prompt would happily would list all 164 of them. Commas can be used to list multiple predicates—Prolog will unify the variables such that all of them are true. Listing all the Water/Ice types is just a matter of asking what Pokémon exist that unify with both the Water and Ice types. Even though is a variable, in the context of the query, both instances of it have to be the same (just like in algebra). The query only unifies for values of where both those predicates hold. For instance, the Water/Ice type Dewgong is a solution because our program contains the following two facts: Therefore, subbing in for the variable satisfies the query. Squirtle, by contrast, is just a Water type: exists, but not . The query requires both to unify, so is not a possible value for . Pokémon have lots of data that you can play around with. Iron Bundle is a strong Water/Ice-type Pokémon with high Special Attack. How high exactly? With Special Attack that high, we want to make use of strong Special moves. What Special moves does Iron Bundle know? Freeze-Dry is a particularly good Special move. Here’s a query for all Ice-type Pokémon with Special Attack greater than 120 that learn Freeze-Dry . One last concept before we move on: Rules. Rules have a head and a body, and they unify if the body is true. A move is considered a damaging move if it’s either a Physical Move or a Special Move. The predicate defines all the moves that do direct damage. This will unify with any moves that do direct damage. Nothing I’ve shown so far is, logically speaking, very ambitious—just “and” and “or” statements about various facts. It’s essentially a glorified lookup table. Still, take a moment to appreciate how much nicer it is to query this database than a plausible alternative, like SQL. For the facts we’ve seen so far, I would probably set up SQL tables like this: Then query it like so: For comparison, here’s the equivalent Prolog query again: I’m not ripping on SQL—I love SQL—but that’s the best declarative query language most people interact with. It’s amazing to me how much simpler and more flexible the Prolog version is. The SQL query would become unmanageably complex if we continued to add clauses, while the Prolog query remains easy to read and edit (once you get the hang of how variables work). With the basics established, here’s some context on the project I’m working on. Pokémon battles have an outrageous number of number of mechanics that all interact in complex and probabilistic ways. Part of the appeal of these games is the futile attempt to keep them all in your head better than your opponent, using that information to out-predict and out-maneuver their plans. It’s a sort of like very silly Poker. The challenge, if you want to build software for this game, is to model all that complexity without losing your mind. Prolog is stunningly good at this, for two main reasons: To illustrate that, here’s how I implemented priority moves for my Pokémon draft league. Pokémon draft is pretty much what it sounds like. Pokémon are given a point value based on how good they are, each player is given a certain amount of points to spend, and you draft until every player has spent their points. Your team ends up with about 8-11 Pokémon and each week you go head to head against another person in the league. My friend and WMI collaborator Morry invited me to his a couple years ago and I’ve been hooked on the format ever since. The games are 6v6, so a big part of the battle is preparing for all the possible combinations of six your opponent could bring, and putting together six of your own that can handle all of them. Naturally, you can only build teams with the Pokémon you drafted. I just made that predicate my name: . What Pokémon do I have that learn Freeze-Dry ? None. Rats. One very important type of move is priority moves. Earlier I mentioned that the Speed stat controls which Pokémon moves first. Some nuance: the Pokémon that used the move with the highest priority goes first, and if they both selected a move of the same priority, then the one with the higher Speed goes first. Most moves have a priority of zero. Ah, but not all! Accelerock has a priority of 1. A Pokémon that uses Accelerock will move before any Pokémon that uses a move with priority 0 (or less), even if the latter Pokémon has a higher Speed stat. I define a predicate that unifies with a Pokémon, the priority move it learns, and what priority that move is. A simple query that asks “what priority moves does my team learn” returns a lot of answers. Although this is technically correct (the best kind), most of these answers are not actually useful. Helping Hand and Ally Switch have very high priority, but they only have a purpose in Double Battles, which isn’t the format I’m playing. To fix this, I define all the Double Battle moves and exclude them. I’m going to exclude the move Bide too, which is functionally useless. The predicate means “true if this goal fails”, and means “these two terms are different.” We get the following results: Much better, but there’s a handful of moves in there that go first because they protect the user from damage or status, like Detect . That’s not really what I mean by priority move—I’m interested in moves that will surprise my opponent with damage or an adverse side effect, like Quick Attack and Sucker Punch . With those rules in place, we arrive at a very useful answer! It’s even more useful to look up what priority moves my opponent for the week has. At this point, I showed the program to Morry and he hit me with a challenge. Pokémon with the Prankster ability get an additional +1 priority on their status moves. Could the rule be extended to note that? I happen to have one such Pokémon on my team. This took me 3 minutes, using Prolog’s if/then construct, . Now the same query includes all of Tornadus’ status moves, with their increased priority. At the top, I said that this experience had taught me about the kinds of interfaces I want to build. One of those lessons is fairly obvious: Prolog can be a little clunky, but it’s an elegant language for expressing and querying relations like the ones described here. That has implications if you, like me, are interested in the judicious use of declarative DSLs for programming. The other lesson is what kinds of tools work for non -programmers. I’m not the first person to think “it would be nice to know what priority moves my opponent’s team has.” The Pokémon community has resources like this, built in the best programming interface of all time: the humble spreadsheet. I use a copy of “Techno’s Prep Doc” , which is one of those spectacularly-advanced Google Sheets you come across in the wild sometimes. You put in the teams and it generates tons of useful information about the matchup. It has a great interface, support for a variety of formats, scannable visuals, and even auto-complete. I was curious about the formula for finding priority moves. It’s gnarly. With a little bit of clicking around, I was basically able to figure out what this does. There’s a “Backend” sheet that lists all the moves. It’s effectively a hard-coded version of my Prolog query. The lookup formula does some filtering, VLOOKUP-ing, and kinda-metaprogramming (INDIRECT returns a cell reference ) to find all the Pokémon on your team that are in that Backend list, and display them. There are a number of reasons that I, personally, would prefer to work on a version of this database implemented in Prolog instead of one implemented with spreadsheet VLOOKUPs. I plan to build webapps with this that do things the existing suite of Pokémon tooling can’t. (If I can ever get scryer-prolog to compile to WASM , that is.) Furthermore, the Prolog paradigm is clearly more extensible. The spreadsheet backend is a hard-coded list of notable moves; my database can look up any move. I still can’t really believe this query, which finds all the Special moves that Tornadus learns which are super-effective against any member of Justin’s team. Nothing like that exists in any tool that I know of—it’s the kind of thing I normally try to figure out by endlessly switching tabs. With the grammar established by my program, I put this together in like 30 seconds. I’m not interested in how structured programming is more extensible than spreadsheets, though. I already know why I don’t do all my programming in spreadsheets. A question I find very important is: What is it about this particular problem, and the kinds of people who were motivated to solve it, where the most well-maintained solution available is a spreadsheet? I believe there are a great many problems like that in the world, and a lot of improvements on that programming paradigm yet to be properly realized. Thanks to Morry Kolman for reading a draft of this blog . Some moves miss a certain percentage of the time, doing no damage. Some moves raise or lower a Pokémon's stats. Pokémon can hold items that have various effects. Damage calculations aren't constant; moves do normally-distributed damage within the calculated range. Pokémon can get frozen, burned, paralyzed, poisoned, or fall asleep; these all have various adverse effects. There are a variety of field effects (like weather, terrain, Trick Room) which alter move damage, turn order, and other things. Pokémon each have an ability that has various effects i.e Levitate makes you immune to ground moves, Drizzle turns the weather to Rain when the Pokemon switches in, Sheer Force disables a move's side effects but multiplies its damage by 1.3x. Players have points they (invisibly) allocate to each Pokémon before the game, to boost chosen stats. Depending on they built the team, each Pokemon might do more damage or take hits better than you were expecting. The challenge, if you want to build software for this game, is to model all that complexity without losing your mind. Prolog is stunningly good at this, for two main reasons: Take a look at the damage calculator to get an idea of what I mean. The query model excels at describing ad-hoc combinations. The data model is perfectly suited to layering rules in a consistent way. I joined the draft league in Season 3, lost in finals, then won Seasons 4 and 5. We just started Season 6. If you want it, you can have the crown . There are a number of coders in this draft league and I have gotten precisely zero of them to try out my Prolog program. That’s kind of the point though! It needs to be a website… The Prolog implementation I’m using is Scryer Prolog , a modern Prolog implementation that emphasizes standards and formal correctness. The creator, Markus Triska, has a terrific online book, “The Power of Prolog,” and accompanying YouTube channel that has soundtracked my breakfast for weeks. Scryer Prolog is also designed to encourage more constructs that preserve logical completeness and monotonicity , which means I’m not really supposed to use the or predicates. I couldn’t really figure out how to express what I wanted with the replacements offered, though. Happy to edit if anyone wants to help. Also, on Markus’ website : “My goal is to provide programs that work as intended, reliably and conveniently, with zero surprises. Programs that you can run for multiple decades without any issues such as crashes, resource leaks or other unexpected behaviour.” This guy and I have some similar interests! I did some fun metaprogrogramming to get all the data into Prolog predicates using the Pokémon Showdown NodeJS API. Yes, putting the accent on the “e” everywhere but the code blocks was very annoying.

0 views
Jim Nielsen 1 weeks ago

Creating “Edit” Links That Open Plain-Text Source Files in a Native App

The setup for my notes blog looks like this : I try to catch spelling issues and what not before I publish, but I’m not perfect. I can proofread a draft as much as I want, but nothing helps me catch errors better than hitting publish and re-reading what I just published on my website. If that fails, kind readers will often reach out and say “Hey, I found a typo in your post [link].” To fix these errors, I will: However, the “Open iA Writer” and “Find the post” are points of friction I’ve wanted to optimize. I’ve found myself thinking: “When I’m reading a post on and I spot a mistake, I wish I could just click an ‘Edit’ link right there and be editing my file.” You might be thinking, “Yeah that’s what a hosted CMS does.” But I like my plain-text files. And I love my native writing app. What’s one to do? Well, turns out iA Writer supports opening files via links with this protocol: So, in my case, I can create a link for each post on my website that will open the corresponding plain-text file in iA Writer, e.g. And voilà, my OS is now my CMS! It’s not a link to open the post in a hosted CMS somewhere. It’s a link to open a file on the device I’m using — cool! My new workflow looks like this: It works great. Here’s an example of opening a post from the browser on my laptop: And another on my phone: Granted, these “Edit” links are only useful to me. So I don’t put them in the source markup. Instead, I generate them with JavaScript when it’s just me browsing. How do I know it’s just me? I wrote a little script that watches for the presence of a search param . If that is present, my site generates an “Edit” link on every post with the correct and stores that piece of state in localstorage so every time I revisit the site, the “Edit” links are rendered for me (but nobody else sees them). Well, not nobody. Now that I revealed my secret I know you can go get the “Edit” links to appear. But they won’t work for you because A) you don’t have iA Writer installed, or B) you don’t have my files on your device. So here’s a little tip if you tried rendering the “Edit” links: do to turn them back off :) Reply via: Email · Mastodon · Bluesky Content is plain-text markdown files (synced via Dropbox, editable in iA Writer on my Mac, iPad, or iPhone) Codebase is on GitHub Builds are triggered in Netlify by a Shortcut Open iA Writer Find the post Fire Shortcut to trigger a build Refresh my website and see the updated post Read a post in the browser Click “Edit” hyperlink to open plain-text file in native app Make changes Fire Shortcut to trigger a build

0 views
Bill Mill 1 weeks ago

my tools in 2026

Here's a brief survey of the tools I'm currently using I use a 14-inch MacBook Pro M1 Max that I bought in 2021. It is, on balance, the best computer I have ever owned. The keyboard is usable (not the best macbook keyboard ever, but... fine), the screen is lovely, it sleeps when it's supposed to sleep and the battery still lasts a long time. The right shift key is broken, but using the left one hasn't proved to be much of a challenge. I usually replace my computers every 5 years, but I don't see any reason why I'd need a new one next year. The most important piece of software on my computer is neovim . I've been using vim-ish software since 2003 or so, and on balance I think the neovim team has done a great job shepherding the software into the modern age. I get a bit irritated that I need to edit my configuration more often than I used to with Vim, but coding tools are changing so fast right now that it doesn't seem possible to go back to the world where I edited my config file once every other year. My vim config is available here . I won't list all the plugins; there aren't that many , and most of them are trivial, rarely-used or both. The ones I need are: Now that vim has native LSP support, I could honestly probably get away with just those plugins. Here's a screenshot of what nvim looks like in a terminal window for me, with a telescope grep open: I use kitty and my config is here . I'd probably switch to ghostty if it supported opening hyperlinks in a terminal application , but it doesn't. I use this feature constantly so I want to explain a bit about why I find it so valuable. A common workflow looks like this: Here's a video demonstrating how it works: The kitty actions are configured in this file - the idea is that you connect a mime type or file extension to an action; in this case it's for a filename without a line number, and if it has a line number. I switched from Firefox to Orion recently, mostly because I get the urge to switch browsers every six months or so when each of their annoyances accumulate. Orion definitely has bugs, and is slow in places, and Safari's inspector isn't nearly as nice as Firefox or Chrome. I wouldn't recommend other people switch, even though I'm currently enjoying it. That's it, I don't use any more. Browser plugins have a terrible security story and should be avoided as much as possible. I use Obsidian , which I publish to the web with the code here (see generating HTML )) It's not clear to me why I like using obsidian rather than just editing markdown files in vim, but I'm very happy with it. I use Apple's Mail.app. It's... fine enough I guess. I also occasionally use the fastmail web app, and it's alright too. I am very happy with Fastmail as an email host, and glad I switched from gmail a decade or so ago. I use Slack for work chat and several friend group chats. I hate it even though it's the best chat app I've ever used. I desperately want a chat app that doesn't suck and isn't beholden to Salesforce, but I hate Discord and IRC. I've made some minor attempts at replacing it with no success. It's the piece of software I use day to day that I would most love to replace. I also use Messages.app for SMS and texts I switched in October from Spotify to Apple Music. I dislike Apple Music, but I also disliked Spotify ever since I switched from Rdio when it died. I'm still on the lookout for a good music listening app. Maybe I'll try Qobuz or something? I don't know. I've also used yt-dlp to download a whole bunch of concerts and DJ sets from youtube (see youtube concerts for a list of some of them) and I often listen to those. I have an old iPhone mounted to my desk, and use Reincubate Camo to connect it to video apps. I occasionally use OBS to record a video or add a goofy overlay to video calls, but not that often. I use Adobe Lightroom to import photos from my Fuji X-T30 and Apple Photos to manage photos. With the demise of flickr, I really have no place to post my photos and I've considered adding something to my website but haven't gotten it done. I use vlc and IINA for playing videos, and ffmpeg for chopping them up from the command line Software editor neovim plugins terminal software Browser browser plugins Note Taking telescope.nvim I have "open by filename search" bound to and "open by grep search" bound to , and those are probably the two most common tools I use in vim. Telescope looks great and works fast (I use telescope-fzf-native for grep) codecompanion I added this in January last year, and it's become the default way I interact with LLMs. It has generally very nice features for working with buffers in vim and handles communications with LLMs cleanly and simply. You don't need Cursor et al to work with LLMs in your editor! I do use claude code for agentic missions, as I have not had much success with agentic mode in codecompanion - I use it more for smaller tasks while I'm coding, and it's low-friction enough to make that pretty painless sonokai colorscheme I use a customized version , and it makes me happy. I to a project directory I either remember a file name or a string I can search for to find where I want to work In the former case, I do In the latter, I do Then I click on the filename or line number to open that file or jump to that line number in a file I love mise for managing versions of programming environments. I use it for node, terraform, go, python, etc etc I have files in most of my projects which set important environment variables, so it has replaced direnv for me as well fd for finding files, a better find ripgrep for grepping atuin for recording my command line history GNU Make and occasionally Just for running tasks is broken and annoying in old, predictable ways; but I know how it works and it's available everywhere here's an example makefile I made for a modern js project is modern in important ways but also doesn't support output file targets, which is a feature I commonly use in ; see the above file for an example gh for interacting with github. I particularly use my alias for quite a lot to open pull requests jq for manipulating json llm for interacting with LLMs from the command line; see An AI tool I find useful for an example Bitwarden - I dunno, it's fine, it mostly doesn't annoy me uBlock origin - I'm very happy with it as an ad blocker

0 views
The Jolly Teapot 1 weeks ago

New year, new me, new web browsing setup?

Since we’re at the start of a new year, I will stop fine-tuning everything on this blog and let it live as the receptacle it’s supposed to be. With my mind cleared of HTML and CSS concerns, I now have energy to waste on new optimisations of my digital environment, and this time with an old favourite of mine: content blockers. * 1 In 2022, I experimented with blocking JavaScript on a per-site basis , which, at the time, allowed me to feel better about my behaviour on the web. You see, I thought that I was not actively refusing adverts. I was just disabling a specific technology on my web browser; not my fault if most ads are enabled via JS after all. True, ads couldn’t reach my house, but not because I actively refused their delivery; simply because the trucks used for their delivery weren’t allowed to drive on my pedestrian-only street. Ethically, I preferred this approach to the one blocking all ads blindly on every site, even if the consequences, from the publishers’ perspective, were the same. I know it was very hypocritical of me, and I know I was still technically blocking the ads. Nevertheless, I felt less guilty blocking the technology used for ads, and not the ads directly. This setup was fine, until it wasn’t. My web experience was not great. Blocking JavaScript by default breaks too many non-media sites, and letting it on made me realise how awful browsing the web without a content blocker can be. The only way for this system to work was to have patience and discipline on the per-site settings. Eventually, I gave up and reinstalled the excellent Wipr Safari extension on all my devices a few weeks later. Last year, on top of Wipr , I also tried services like NextDNS and Mullvad DNS . With these, the browser ad blocker becomes almost superfluous, as all it has to do is remove empty boxes that were supposed to be ads before being blocked by the DNS. It was an efficient setup, but I was still blocking ads, which kept on bothering me. While I happily support financially a few publications, I can’t do the same for all the sites I visit. For the ones I am not paying, seeing ads seems like a fair deal; blocking ads was making me feel increasingly guilty. * 2 Like I wrote in the other post on the topic : Somehow, I always feel a little bit of shame and guilt when talking about content blockers, especially ad blockers. Obviously ads are too often the only way many publishers manage to make decent money on the internet: every newspaper can’t be financially successful with subscriptions, and every media company can’t survive only on contributions and grants. That’s why recently, I stopped using Mullvad as my DNS resolver, and switched to Quad9 , which focuses on privacy-protection and not ad-blocking. I also uninstalled Wipr. Today, I rely solely on StopTheScript . What’s new this time around is that I will try to be more disciplined than I was three years ago, and do the work to make this system last. What I do is set the default StopTheScript setting on “Ask”. When a site aggressively welcomes me with three or four banners masking the article I came to read, I click on the StopTheScript icon and allow it to block JavaScript on the website, and refresh the page. Two clicks, one keyboard shortcut. In most cases, these steps are easier and faster than what is the usual series of events. You know, the one where you need to reload the page with ad blockers disabled, just so you can close the modal window that was blocking scrolling on the page, and then reload the page once again, this time with ad blockers enabled. With JavaScript turned off, visiting most websites is a breeze: my computer feels like it uses an M4 chip and not an Intel Core i5, the page is clean, the article is there, it works. There are a few media sites that refuse to display anything with JS turned off, but I’d say that 95% of the time it’s fine, and I can live my life without a proper ad blocker. * 3 For websites where ads are tolerable, I don’t bother blocking JavaScript, I let it pass. In my mind, this is how my first interaction with a website goes if it were a department store: [opens page at URL] Website: “ Hi dear visitor, I see you’re looking at this product, but may I interest you in a free newsletter? Or would you like to share your Google account with us so next time you come back we’ll know? Also, could you sign this agreement real quick? Oh, and by the way, have you seen that we have a special offer currently? Would you like a cookie? ” Me: “ Hello, yes, oh wow, hum… wait a second… ” [blocks JavaScript] Me: “ Sorry, I don’t speak your language and don’t understand anything you say .” [Salesperson goes away instantly] Me: “ Ah, this is nice and quiet. ” Maybe I’m wrong, but to me, this is a more “polite” default behaviour than using an ad blocker from the get-go, which, in this analogy, would be something like this: [opens page at URL] Ad blocker: “ Alright, well done team, great job. We arrested all sales people, handcuffed them, and brought them all to in the basement. All clear. The boss can come in. ” Me: “ Ah, this is nice and quiet. ” If you have a better analogy, I’m all ears: I really struggled with this one. I’m not sure how long this JS blocking setup will last this time. I’m not sure if it feels that much better to block JS permanently on some websites rather than blocking ads. All I know is that most websites are much quicker to load without JavaScript, much easier to handle by my machine, and just for those reasons, StopTheScript may be the best content blocker for Safari. I guess this is not surprising that all the cool new web browsers include a JavaScript toggle natively. Why are they called content blockers and not ad blockers? Pretty sure it’s some sort of diplomatic lingo used to avoid hurting the feelings of ad companies. I don’t like the word content , but calling ads and trackers content is just weird. ^ I know I could use an ad blocker and disable it on some websites, or only activate it on the most annoying sites, but ad blockers tend to disappear in the background, don’t they? ^ I mention media sites because obviously ecommerce sites, video sites, and interactive sites require JavaScript. Interestingly, Mastodon doesn’t need it to display posts, whereas Bluesky does. ^ Why are they called content blockers and not ad blockers? Pretty sure it’s some sort of diplomatic lingo used to avoid hurting the feelings of ad companies. I don’t like the word content , but calling ads and trackers content is just weird. ^ I know I could use an ad blocker and disable it on some websites, or only activate it on the most annoying sites, but ad blockers tend to disappear in the background, don’t they? ^ I mention media sites because obviously ecommerce sites, video sites, and interactive sites require JavaScript. Interestingly, Mastodon doesn’t need it to display posts, whereas Bluesky does. ^

0 views
Rob Zolkos 1 weeks ago

A Month Exploring Fizzy

In their book Getting Real , 37signals talk about Open Doors — the idea that you should give customers access to their data through RSS feeds and APIs. Let them get their information when they want it, how they want it. Open up and good things happen. Fizzy takes that seriously. When 37signals released Fizzy with its full git history available , they didn’t just open-source the code — they shipped a complete API and webhook system too. The doors were wide open baby! So I dove in — reading the source, building tools, and sharing what I found. Every time curiosity kicked in, there was a direct path from “I wonder if…” to something I could actually try and execute. This post is a catch-all for my very bubbly month of December. Fizzy Webhooks: What You Need to Know — I set up a local webhook receiver to capture and document every event type Fizzy sends. The post covers the payload structures, signature verification, and ideas for what you could build on top of the webhook system. The Making of Fizzy, Told by Git — I prompted Claude Code to analyze the entire git history and write a documentary about the development. Vanilla CSS is all you need — Diving into the no-build CSS architecture across Campfire, Writebook, and Fizzy. Fizzy Design Evolution: A Flipbook from Git — I went through each day of commits, got the application to a bootable state, seeded the database, and took a screenshot. Then I stitched those screenshots into a flipbook video with a soundtrack made from Fizzy’s own audio files. Fizzy’s Pull Requests: Who Built What and How — An analysis of who owned which domains in the Fizzy codebase. The post maps contributors to their expertise areas and curates learning paths through the PRs for topics like Turbo/Hotwire, caching, AI integration, multi-tenancy, and webhooks. The open API invited experimentation. I spotted gaps that would make integration easier for other developers, so I filled them: fizzy-api-client — Ruby client for the Fizzy API. fizzy-client-python — Python client for the Fizzy API. fizzy-cli — Command-line interface for the Fizzy API, built first in Ruby and then migrated to Go for portability. fizzy-skill — An AI agent skill for interacting with Fizzy. n8n-nodes-fizzy — An n8n community node that brings Fizzy into your automation workflows. Create cards, manage assignments, and react to real-time events through webhook triggers. Migration tools — I built these to make it easier to try Fizzy without starting from scratch. Migrating your existing issues and boards gives you an immediate sense of how it could work for you, without having to manually create test cards. You can see your real data running in Fizzy from day one, which I think makes it easier to evaluate and decide if its useful for you. I also contributed a few small fixes back to the main repository: Fizzy is released under the O’Saasy License , which is similar in spirit to MIT but includes a restriction on offering the software as a competing hosted or SaaS product. You can modify and self-host it, but you can’t repackage it and sell it as your own hosted service. I built O’Saasy Directory to make it easy to find applications released under this license. Beyond Fizzy, the directory includes other submitted projects where the source is available to read and modify. If you have built something under the O’Saasy License, visit the submission page to add yours. Having built the Fizzy CLI and fizzy-api-client Rubygem, I saw some fun opportunities to build little lab experiments to show how Fizzy could be integrated with - both to power up some functionality that isn’t there yet, but also creating boards in some interesting ways (eg Movie Quiz). I got the idea for this on a flight to Australia with no internet. Just a pad of paper and a pen. I should probably do that more often as a bunch of ideas for all sorts of products came out. CarbonationLabs is not a product per se. It’s an open source Rails application designed to be run locally where you can interact with the hosted or self-hosted versions of Fizzy. If anything I hope it inspires creation of little problem solving workflows for Fizzy that wouldn’t be built into the main product (the problem is too niche). The API and webhook system is really flexible and most of your bespoke problems could be solved with some creative thinking. Introducing Carbonation Labs - fun ways to add experiments to and extend Fizzy (repo link and demo videos below)🧵 I built carbonation.dev to bring together all the tools, libraries, and integrations that I and others in the community have created for Fizzy. It’s a directory covering API clients (Ruby, Python, JavaScript), CLI tools with packages for macOS, Arch Linux, Debian, Fedora, and Windows, integrations for Claude Code and other AI agents, n8n, Raycast, Telegram, and MCP servers, plus migration tools for GitHub, Linear, Asana, and Jira. If you’ve built something for Fizzy, I’d love to feature it. You can submit a pull request to add your tool to the directory. Building the Fizzy CLI pushed me into some new territory. I created an AUR package for Arch Linux users, set up a Homebrew tap for macOS, published my first Python package to PyPI, and made an n8n plugin — all firsts for me. While I already knew Go, rewriting the CLI in it was a fun exercise, and building TUIs for the setup and skill commands introduced me to terminal UI libraries I hadn’t used before. Gosh it was fun! If you want to get better at Rails, Fizzy is a great place to study real-world code. And in my view if you want to work at 37signals as a Rails programmer, digging into Fizzy — along with Campfire and Writebook — is a solid way to learn how they approach Rails architecture and design decisions. Submitting PRs is also a good way to contribute back while learning — just be respectful of the contribution policy . The review discussions give you a window into how to reason about problems, spot opportunities, and make trade-offs. This month pushed parts of my creative thinking that weren’t gone, but definitely weren’t being stressed. Like any muscle, use it or lose it. The direction of what to explore came from my own curiosity and a habit of poking around under the hood, and AI helped me move a lot faster once I knew where I wanted to go. Most of this information already exists somewhere — Google, Stack Overflow, documentation — but having AI right there alongside me as a partner was thrilling. All of this was made possible because a team left the doors open. No one asked me to step inside; I decided to invest the time and do the work to see what I could build, learn and share. I do this at work too—when I can—looking for opportunities I can shape, experiment with, and get genuinely excited about. Most importantly I had fun and I hope you enjoyed following along. linear2fizzy — Migrate Linear issues jira2fizzy — Migrate JIRA issues asana2fizzy — Migrate Asana tasks gh2fizzy — Migrate GitHub Issues prd2fizzy — Convert PRDs to Fizzy cards #2114 — Remove unused install.svg and its CSS class #2111 — Remove unpaired view-transition-name #2095 — Fix typo: minues → minutes #2094 — Fix duplicate word: use use → use #2093 — Add QrCodesController test #2088 — Fix view-transition-name typo in public card show

0 views
Xe Iaso 1 weeks ago

2026 will be my year of the Linux desktop

TL;DR: 2026 is going to be The Year of The Linux Desktop for me. I haven't booted into Windows in over 3 months on my tower and I'm starting to realize that it's not worth wasting the space for. I plan to unify my three SSDs and turn them all into btrfs drives on Fedora. I've been merely tolerating Windows 11 for a while but recently it's gotten to the point where it's just absolutely intolerable. Somehow Linux on the desktop has gotten so much better by not even doing anything differently. Microsoft has managed to actively sabotage the desktop experience through years of active disregard and spite against their users. They've managed to take some of their most revolutionary technological innovations (the NT kernel's hybrid design allowing it to restart drivers, NTFS, ReFS, WSL, Hyper-V, etc.) then just shat all over them with start menus made with React Native, control-alt-delete menus that are actually just webviews, and forcing Copilot down everyone's throats to the point that I've accidentally gotten stuck in Copilot in a handheld gaming PC and had to hard reboot the device to get out of it. It's as if the internal teams at Microsoft have had decades of lead time in shooting each other in the head with predictable results. To be honest, I've had enough. I'm going to go with Fedora on my tower and Bazzite (or SteamOS) on my handhelds. I think that Linux on the desktop is ready for the masses now, not because it's advanced in a huge leap/bound. It's ready for the masses to use because Windows has gotten so much actively worse that continuing to use it is an active detriment to user experience and stability. Not to mention with the price of ram lately, you need every gigabyte you can get and desktop Linux lets you waste less of it on superfluous bullshit that very few people actually want. Oh, and if I want a large language model integrated into my tower, I'm going to write the integration myself with the model running on hardware I can look at . At the very least, when something goes wrong on Linux you have log messages that can let you know what went wrong so you can search for it.

0 views
Simon Willison 1 weeks ago

Introducing gisthost.github.io

I am a huge fan of gistpreview.github.io , the site by Leon Huang that lets you append to see a browser-rendered version of an HTML page that you have saved to a Gist. The last commit was ten years ago and I needed a couple of small changes so I've forked it and deployed an updated version at gisthost.github.io . The genius thing about is that it's a core piece of GitHub infrastructure, hosted and cost-covered entirely by GitHub, that wasn't built with any involvement from GitHub at all. To understand how it works we need to first talk about Gists. Any file hosted in a GitHub Gist can be accessed via a direct URL that looks like this: That URL is served with a few key HTTP headers: These ensure that every file is treated by browsers as plan text, so HTML file will not be rendered even by older browsers that attempt to guess the content type based on the content. These confirm that the file is sever via GitHub's caching CDN, which means I don't feel guilty about linking to them for potentially high traffic scenarios. This is my favorite HTTP header! It means I can hit these files with a call from any domain on the internet, which is fantastic for building HTML tools that do useful things with content hosted in a Gist. The one big catch is that Content-Type header. It means you can't use a Gist to serve HTML files that people can view. That's where comes in. The site belongs to the dedicated gistpreview GitHub organization, and is served out of the github.com/gistpreview/gistpreview.github.io repository by GitHub Pages. It's not much code. The key functionality is this snippet of JavaScript from main.js : This chain of promises fetches the Gist content from the GitHub API, finds the section of that JSON corresponding to the requested file name and then outputs it to the page like this: This is smart. Injecting the content using would fail to execute inline scripts. Using causes the browser to treat the HTML as if it was directly part of the parent page. That's pretty much the whole trick! Read the Gist ID from the query string, fetch the content via the JSON API and it into the page. Here's a demo: https://gistpreview.github.io/?d168778e8e62f65886000f3f314d63e3 I forked to add two new features: I also removed some dependencies (jQuery and Bootstrap and an old polyfill) and inlined the JavaScript into a single index.html file . The Substack issue was small but frustrating. If you email out a link to a page via Substack it modifies the URL to look like this: https://gistpreview.github.io/?f40971b693024fbe984a68b73cc283d2=&utm_source=substack&utm_medium=email This breaks because it treats as the Gist ID. The fix is to read everything up to that equals sign. I submitted a PR for that back in November. The second issue around truncated files was reported against my claude-code-transcripts project a few days ago. That project provides a CLI tool for exporting HTML rendered versions of Claude Code sessions. It includes a option which uses the CLI tool to publish the resulting HTML to a Gist and returns a gistpreview URL that the user can share. These exports can get pretty big, and some of the resulting HTML was past the size limit of what comes back from the Gist API. As of claude-code-transcripts 0.5 the option now publishes to gisthost.github.io instead, fixing both bugs. Here's the Claude Code transcript that refactored Gist Host to remove those dependencies, which I published to Gist Host using the following command: You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options . A workaround for Substack mangling the URLs The ability to serve larger files that get truncated in the JSON API

0 views
Bill Mill 2 weeks ago

2025 in review

I didn't do that much work in the open this year, unfortunately. I made a few small tools and polished my workflow scripts quite a bit though: I use my review script to get feedback from an LLM on a pull request before I submit it, or to review other people's PRs. I like this a lot more than tools that annotate a PR in github, because it lets me read it locally and privately, and decide for myself which of its suggestions are valuable. As I wrote when I released the tool , I find that most of its suggestions are not useful, but having a list of suggestions to evaluate has proved a very valuable resource for me and has prevented me from making quite a few obvious errors. My worktree script helps me use git worktrees in a large node.js monorepo at work without so much pain. This year I: I did play with a bit this year , but I'm so used to git that it's not a pain point for me and it seems like a waste of time, though I remain interested in it. My favorite tiny script of the year is my script, which I wrote because I often have to compare times in different timezones with different time formats. For example, sometimes I'm reading timestamps from a log message in milliseconds since the epoch, and I need to know when that was both in my local time and in UTC. It has two functions: I have the rights to push to the main branch of a bunch of git repositories, and I found myself occasionally pushing directly to main by accident instead of creating a branch. To solve that problem, I replaced my alias with a script that checks if I'm pushing to a main branch by accident, and displays a message to the console with the ability to do the push if it was indeed intentional. I still spend most of my life in neovim in the terminal, and have no desire to change either of those. I'd like to switch to ghostty , but it doesn't support opening hyperlinks in terminal applications ( github discussion ). I find this workflow incredibly useful and don't want to live without it, so I'm still using kitty . I've been using Obsidian to make this website, and remain very happy with it. I've updated the script that builds the website in minor ways throughout the year. It's slow and I dislike the markdown library it uses , but it also gets the job done and I don't have to think about it. I added hacky callout support But it was hacky and painful to do, so it goes I switched from Firefox to Orion browser, which is working well enough though it definitely has some bugs. I tend to switch browsers every six months as I accumulate dissatisfactions with all of them. For a long time, I've vaguely wanted to play games that are Windows-only, but I also refuse to install Windows and I don't have a Linux gaming computer set up. A couple weeks ago, I finally bit the bullet and purchased CrossOver and have started playing a few games that are Windows-only. Some games I or my kids enjoyed this year: Working for a distributed company means traveling for meetups, and we had them in particularly fun locations this year. I was fortunate to travel to Colorado, Montana, and Mexico for work this year. My family and I spent two weeks in Lecco, Italy, which was gorgeous. I'd never been to Italy before and it was great I continued toying around with different visualizations of NBA data this year. My son asked me the other day if it was work, and I told him that I make graphs in the same way that other guys build models or go fishing visualization added submodule support added support for github fork notation ( ) added the ability to store its config in git global config when called without arguments, prints out the current time in a variety of formats, useful for comparing time between timezones or formats: if passed an argument, will do its best to parse the time and display it in those same formats: We all enjoyed N++ , a platformer stripped down to the absolute basics. I remember playing n as a flash game so many years ago, and its older sibling is still a blast. I enjoyed playing Animal Well Because Silksong came out this year, I went back and tried to finish Hollow Knight . I managed to get to the last boss, but couldn't beat him, and didn't end up buying Siksong. Maybe next year My older son really enjoyed Ball x Pit My younger son got old enough to play Tears of the Kingdom for himself, and enjoys coming up with silly weapon combinations more than actually playing the story, which is fun We had fun playing some FTL runs together We all had fun with Dead Cells I kept up my nba_data archive, and added a few data sources to it. I particularly enjoy Dean Oliver's analytics data , and have found some good uses for it Lately I've been playing around with tables and trying to make them especially expressive; this one has a bar graph background that changes with the column you've sorted by, and has interactive filtering: Due to a particularly irritating github pages bug , I moved my observable notebook off of github pages and over to https://billmill.org/nba I post about this work on my bluesky account , and occasionally post a graph or progress on other work

0 views

1Password Dependency Breaks Syntax Highlighting

Earlier today I noticed the syntax highlighting on this website was broken. But not fully: on reload I’d see a flash of highlighted text, that then turned monochrome. The raw HTML from showed rouge tags, but the web inspector showed raw text inside the elements. This didn’t happen in Chromium. My first thought was: there’s malformed HTML, and Firefox is recovering in a way that loses the DOM inside tags. Then I noticed it doesn’t happen in incognito. Turning my extensions off one by one, I found that 1Password is responsible. Others ( 1 , 2 ) have reported this also. If you extract the latest XPI , unzip it, and dig around, you’ll find they’re using Prism.js , a JavaScript syntax highlighter. I don’t know why a password manager needs a syntax highlighter. I imagine it has to do with the app feature where, if you have an SSH key, you can open a modal that tells you how to configure Git commit signing using. Maybe they want to highlight the SSH configuration code block (which is unnecessary anyways, since you could write that HTML by hand). But I can’t know for sure. Why write about this? Because 1Password is a security critical product, and they are apparently pulling random JavaScript dependencies and unwittingly running them in the tab context , where the code has access to everything. This is no good. I don’t need to explain how bad a supply-chain attack on the 1Password browser extension would be. I like 1Password and I was sad when Apple Sherlocked them with the Passwords app, but this is a bad sign about their security practices.

0 views