Val Town Town
Val Town Town is Val Town implemented on Val Town. Fork it, extend it, build stuff on it, or read on if you’d like to learn more.
Val Town lets you build all sorts of tools with vals: HTTP handlers, crons, email endpoints, frontend applications, the list goes on. But can you build Val Town with Val Town? With a few caveats, the answer is yes: let’s dive in.
Handling a Request with import
So we want to accept user code, parse and execute it, and use it to handle a web request.
The most naïve and solution is to use JavaScript’s dynamic import functionality to load their code.
Supposing the user-provided HTTP handler looks like this:
We wrap it in our runtime by creating a data URL including the code, calling import, and running the default export (source):
This works, but is very dangerous:
there’s almost no separation between the system code and the user code. The user
code has access to the same memory and permissions that we do. It could read our
secrets in Deno.env
or mess with objects on globalThis
. This is barely
better than the much-feared
eval()
method. (Fun side note: Val Town was originally called “Eval Town” but was
shortened when we started calling the blocks of code “vals”.)
How can we lock things down?
Using Web Workers
I recently published a Val that demonstrates how to execute untrusted code safely, using Deno’s Web Worker and granular permissions. We can build all sorts of things with this, like secure playgrounds for writing code, or scripting interfaces for existing applications.
With a little cleverness, we can use Web Workers as a sandbox for user-defined HTTP handlers!
By wrapping things in a Deno Web Worker, we can isolate user code from the
parent process. This’ll give us much better security guarantees than the
import()
method did.
Here’s a very minimal example:
Notice that we’ve added the permission {net: false}
to lock things down even
further. Now the running code also doesn’t get to make HTTP requests.
Web Workers make us work a little harder to call that user-provided function: we’re passing messages here instead of calling functions directly. That’s the cost of better security.
We’re passing messages back and forth using postMessage, which has some limitations on the kinds of objects it can transmit – passing numbers back and forth like this is fine:
But we can’t just pass a complex object like a Request
.
Only structured-cloneable types are supported.
That’s a bummer - how are we going to implement an HTTP handler if we can’t send requests and responses between the user code and the system?
Put a Server in the Worker
If we can’t send Request
and Response
using postMessage
, then let’s set up
a web server in the Worker and communicate directly with that, instead of using
postMessage
. Here’s how it works with
reqEvaltown the library I’ve created
to handle HTTP requests within a Worker.
Within this function, we:
- Find an available port.
- Spawn a Worker using our script. We’re running the Worker with limited permissions to allow the server to run and allow some https imports to work, but otherwise block network activity.
- Send a message to the Worker with the port and importUrl.
- Wait for the post to be available.
- Proxy our request to the Worker and return the response.
Phew, certainly a little more complicated than before. Over in our Worker code, we handle the rest of the exchange.
We start a server, queue up requests that arrive while we’re loading, and then serve any pending requests with the loaded module.
There we have it, now we’re ready to handle requests with a user-provided handler.
UI
Now for the fun part: we head over to Townie to create the UI. I asked for a simple site with minimal styling that accepted user code in a text box and stored it in a SQLite database. From there I made a few manual tweaks, hooked up the code in the database to the request handler endpoint, added some stylistic touches, and tweaked a few things with Townie’s help.
Try it out 👉 https://maxm-valtowntown.web.val.run
The functionality is quite limited compared to Val Town, but you can still do lots of things. You can host TLDraw, build a React Playground, and even stream Server Sent Events.
And there you have it: you can implement a subset of Val Town’s functionality on Val Town itself!
Workers provide a viable security sandbox, but it’s important to note that it is not as isolated or safe as the runtime strategy that we use today, which uses process isolation as well. Be careful when implementing this and any other security-sensitive code on Val Town in your accounts!
There are many more features that would be possible to implement:
- We currently spawn one Worker per request, would it be possible to share a Worker across many requests?
- We use
data:text/tsx
for import urls, could you implement your own module host so that you can use https imports? - How could you capture request logs and request metadata for users to view?
- Could you provide user accounts? Environment variables? Database access?
- Val Town Town cannot run itself, can you fix that so we can make Val Town Town Town?!
Fork the code and see what you can make!
Edit this page