This is in response to “I’m not a fan of Deno’s security model”.
I spent most of the last four years working on Val Town. When I joined, we were running JavaScript code in vm2, hoping that it was a sufficient sandbox. It was not. I advocated for Deno and we switched to Deno, in large part because it provided a stronger sandbox.
This decision wasn’t perfect: most of Deno’s core ideas lost in the end.
But: Deno’s security model. In short: it worked. We bet the company on the security of Deno’s runtime flags, and there were absolutely zero cases in which they were the culprit in a security sense. Now we were using it in a different way than the linked blog post describes: from day one, with --no-prompt. Using a program and interactively approving or denying permissions is not what I imagine as a core usecase for the permissions system: the prompts are really there for you to learn what permissions are necessary and then bake them into configuration going forward. We also didn’t enable --allow-run for anything, and yes, it’s confusing. Same with --allow-env, code in Val Town always ran with an artificial environment. The linked post sort of supposes a different use-case - running new code interactively and wanting to loosen the sandbox a bit. If you run the code non-interactively with a strict sandbox, it’s pretty good?
Now: bwrap. cgroups. Linux namespaces. The correct way to implement this system. Yes, I knew about all of that. And the decision of whether to switch from Deno’s process-based perhaps-faux-isolation to Linux’s strong, generic isolation was a constant topic of conversation.
The suggestion is always to just bubblewrap it. Or to just throw some cgroups on it. It’s always easy in abstract. On your local computer, sure, why not! Mostly we were aiming for cgroups, and it turns out that our hosting provider, as well as nearly every PaaS provider, runs processes inside of Docker, and unless you bless the Docker startup with --cgroupns=host to grant it privileges to mess with the host’s cgroups settings, cgroups will not work. That rules out hosts like Render, Railway, etc. The same goes for linux namespaces: doing them on an unprivileged host is hard or impossible. And sure, you can control everything from top to bottom if you run your own servers on metal, but that requires a lot of devops commitment.
Deno, on the other hand, works anywhere. It works the same on macOS and Windows, unlike bubblewrap, so you can reliably test it on developer machines. It’s clean and simple to deploy and scale.
And personally, I think that language VMs implementing controls like this is cool. For example, Starlark calls its execution ‘hermetic’ by default - unable to access the surrounding environment. Lua supports sandboxing too. Probably the ultimate would be an in-language effect implementation in which a bootstrap script could declare which resources an import is able to use - Effect in TypeScript can’t do that, but I believe Koka can.
In this era of untrusted code and exploits everywhere, wouldn’t it be cool if you could import some NPM module for doing date math and say up front that it can’t access the filesystem or environment variables? OS-level capabilities controls are powerful and reliable but coarse. Language-level controls could add an extra level trust & compartmentalization for applications.
Deno’s security model isn’t perfect. But it was there when we needed it, its guarantees held, and it solved a real problem, and for that I appreciate it.
I’m working through some basic computational geometry and today’s is the convex hull, which I implemented on Observable. Check it out. It’s nothing special, the main thing that I learned is the application of cross products to quickly determining whether a line is making a left or right turn. That was pretty neat and I think it’ll come in handy soon.
I didn’t expect it, but some things last a long time, and a handful of my oldest open source projects just keep going, and I’m still having fun with them.
commit 0b049220e18e9c01cce618e19d71dd7353a09a7f
Author: Tom MacWright <tom@macwright.org> Date: Wed Feb 8 13:18:12 2012 -0500
First commit
I started simple-statistics over fourteen years ago. Crazy. It’s still going. Now has TypeScript types, lots of functionality, good tests, good documentation.
I just tagged the 8th major version of it: a breaking change that makes a few functions throw on empty inputs and stops downleveling code to be compatible with IE11. I’ve been following semver, so “version 8” can be a little arbitrary. I tagged version 6 when I removed bower and component compatibility - remember those things? Ancient technology.
The project has gotten a lot of contributions lately too. Everyone’s definitely using LLMs, but the quality is mostly high enough to merge after a little back & forth. I’m still reading the code and trying to keep it consistent. Depending on how you measure, the module has really taken off in terms of downloads:
Probably some LLM scraping in there, maybe some LLM preference, maybe some big upstream consumers that I haven’t identified, but still in terms of vanity numbers, hitting peak weekly downloads of 1.6M is a real accomplishment for me. I’m trying my hardest to keep it stable and reliable. The release flow is well-tuned at this point and I just reworked how the documentation happens.
To another 14 years, I guess!
I was reading Feross Aboukhadijeh’s course notes for CS 253 Web Security, specifically bcrypt’s slides about real-world security, and an unexpected face popped up (mine). I filed a bug in the uglify project in 2015 and didn’t think much more about it, but turns out, there’s a blog post building on it as an exploit, a whole CVE in the national database and a critical assignment on GitHub.
Fun! I don’t take any credit because I didn’t even come up with a good reduced test case, Titus did more work on that, and Mihai actually made the fix. But it’s cool to come across an old thread that had an unusually big impact.
Observable just released a brand new version. There’s a lot to like about it:
viewof, and different syntax, like anonymous blocks. The tradeoffs for a custom language are pretty bad with few exceptions, especially when it so closely resembles another language that people assume everything that works in one should work in another. Trust me - I’ve worked on CartoCSS, Observable JavaScript, and the dialect of JavaScript that Val Town initially ran on. Embracing standard TypeScript is even more important now that lots of people are producing code with LLMs, and the training set for TypeScript is much, much bigger than for OJS.Updating my table from my last Observable update:
| Product | Launched | Local/hosted | Languages | OSS | Layout | Collaboration |
|---|---|---|---|---|---|---|
| Notebooks | ~2018 | Hosted | ojs, md, html, sql | no* | notebook | live |
| Framework | 2024 | Hybrid | js, md, sql, many others | yes | mixed | none/github |
| Canvas | 2025 | Hosted | js, sql | no | canvas | live |
| Notebooks 2.0 | 2025 | Local* | ojs, js, md, html, tex, dot | yes* | mixed | none/github |
| New Observable | 2026 | Hosted | ts, js, md, HTML, SQL, TeX, DOT, ojs | no | notebook | live |
Backstory/full disclosure: I was the first hire at Observable and hold equity in the company.
My dad once bought a lawnmower off of Craigslist, because my family needed a new lawnmower. It was listed as being broken, but after a few minutes of wrenching, it worked perfectly. He was noticeably disappointed.
I’ve been riding unnecessarily long distances on my bike. It’s a dirty, sweaty, occasionally painful sport. A fellow programmer asked why I don’t get an electric bicycle so I could ride faster and with less effort.
Good news from the folks at Mapbox - they’ve revamped geojson.io using some of the bones of Placemark, which I open-sourced in 2024. It’s pretty poetic for things to come full circle like that - Placemark was in many ways an attempt to fix some of the things that I thought were deficient about geojson.io, and now geojson.io has those things fixed because of the Placemark codebase.
Chris and Andrew were super nice about it and I think it’s a fantastic outcome. See also epanet-js, which is also based on the Placemark “stack”! It’s not perfect, but it’s really nice to see the open source code help people get to square 1 with new geospatial applications.
I’m feeling more repelled from software subscriptions recently, primarily for products that have their own proprietary data storage or formats. Partly it’s the lock-in effect, and also the platform decay that is accelerating as all companies (besides major AI labs) get nervous about their margins and sustainability.
Anyway, so far:
Still, my most expensive subscription is the hardest to cut: Capture One has been my go-to for organizing and light retouching of photos and drawings. It’s great, but expensive and likely to get more so as it gets revenue-maximized and sold from one private equity firm to another. I’ve tried digiKam but it’s ugly and the learning curve is steep. Aphera looks amazing but is much more focused on raw editing than organization and lacks any kind of retouching tools. I could return to Lightroom but I dread Adobe products. DaVinci Resolve Photo could be the answer, but there’s no support for Olympus raw yet. I could even use Apple Photos, but I don’t think Olympus raw is supported there either. What I’m really looking for is more of a smooth, capable ‘digital asset manager’ that’s good for managing images. The hunt continues. Maybe buying a perpetual license to Capture One is the answer, but the break-even with my pricing would be at two years.
If you’re encoding some binary data as text, you have a lot of options. My cheatsheet:
| encoding | transport-safe | length | standard | case-sensitive |
|---|---|---|---|---|
| hex | safe | long | yes | no |
| base32 | safe | medium | meh | no |
| base58 | safe | short | no | yes |
| base64 | meh | short | yes | yes |
| base64url | safe | short | yes | yes |
+ and - which necessitates base64url.My current defaults:
openssl rand -hex.I feel a pull to consolidate these further and ask whether I’m being too fancy using different encodings. In particular, whether all encryption keys should be base64 encoded, and whether we really need base32 for user-facing content. Though base32 subjectively looks nice and is short, it’s pretty nonstandard: no support in Node, and the main place where I see base32 in use, in the AT Protocol, they use a different character set than usual.
An update since the last Effect devlog:
I finally vanquished the last bits of neverthrow from the codebase and replaced them with Effect abstractions. neverthrow was great for what we used it for - I’d still use it on a simpler project, but Effect is a better fit for Val Town.
The LLMs are getting incrementally better at writing Effect code: they now do a decent job of annotating functions with explicit type parameters, but they still leave some refactoring debt where they call Effect.runPromise repeatedly instead of combining calls with Effect.gen.
Effect v4… I’m not using it yet. The beta has been the focus of the Effect team since February. Old-fashioned Effect v3 is only getting backported fixes. A lot of the community is using v4 and living on the edge. I’m not using v4 for the simple reasons: I don’t want to use beta modules with lots of churn in a production application, and v4 is not documented.
The documentation problem is still pretty bad. The JSDoc issue got closed. The SQL-is-undocumented issue got closed in anticipation of the v4 release and the upcoming focus on documentation. This team is producing incredible code and I hope that they can produce incredible docs to match, but the fear that documentation simply isn’t highly valued or prioritized persists because I’ve seen so little improvement.
I’ve written some more ast-grep rules for Effect. We have a big codebase and there are lots of ways to screw up. Some of the ways that I’ve recently automatically prevented are:
Throwing in Effect.gen! This is going to be refactoring debt one way or another. If you run into an issue, you yield an error, not throw:
Here’s my rule:
id: dont-throw-in-effect
language: Tsx
rule:
pattern: Effect.gen($$$A)
has:
pattern: throw $A
stopBy: endUsing Effect.promise. Early on, I used this method a bunch because I was being lazy. The LLMs probably used it too. I’ve learned that it’s a footgun: if the promise it wraps ever rejects, that produces a defect, not a failure, and that goes through a totally different error handling path. We should always use Effect.tryPromise.
Here’s the rule:
id: no-effect-promise
language: Tsx
rule:
pattern: Effect.promise($A)
fix: 'Effect.tryPromise({ try: $A, catch(error) { return new UnexpectedError({message: "Unexpectedly failed", cause: error }) } })'And then update the “Unexpectedly failed” message with something more descriptive.
Data.TaggedEnum is the new toy. I use a lot of discriminated unions. If there’s a x in y type check, that’s a code smell to me and we should use a type or kind field to differentiate the different object shapes instead. TaggedEnum cuts some of the boilerplate for that and adds some in another area. I like it but I’m not 100% sure the abstraction pays for itself yet.
At this stage, Effect is pretty productive, though a lot of our initial issues remain. The application is far from being a ‘pure’ Effect app, but most important database-touching, authentication-enforcing, or business logic-producting methods are using Effect’s abstractions. I have no regrets about the switch.