A vat's globalOutbound is now always null. A granted host arrives as env.egress, an Egress the app can hand to whichever of its parts should have it; as globalOutbound it was ambient to every module in the vat. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
103 lines
3.9 KiB
JavaScript
103 lines
3.9 KiB
JavaScript
/** @file toybox supervisor: one Vat per name; its app runs as a facet on installed code. */
|
|
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
|
|
|
|
const hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
|
|
/**
|
|
* Holds the vat's installed code and egress grant; the app facet holds the app's state.
|
|
*
|
|
* Code is stored by hash so the facet can be rebuilt after hibernation or a
|
|
* restart. The app's global `fetch` throws; a granted host arrives as
|
|
* `env.egress`, an {@link Egress} the app can pass along like any object.
|
|
*
|
|
* Each `start` begins a new incarnation, which names the loaded Worker: the
|
|
* same code with a different grant must not share a Worker.
|
|
*/
|
|
export class Vat extends DurableObject {
|
|
constructor(ctx, env) {
|
|
super(ctx, env);
|
|
ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS bundle (hash TEXT PRIMARY KEY, src TEXT)");
|
|
ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS current (k INTEGER PRIMARY KEY CHECK (k = 0), hash TEXT)");
|
|
ctx.storage.sql.exec(
|
|
"CREATE TABLE IF NOT EXISTS incarnation (k INTEGER PRIMARY KEY CHECK (k = 0), n INTEGER, egress TEXT)",
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Install `src` and run the app on it; the app's storage carries over.
|
|
*
|
|
* @param {string} src an ES module exporting `class App extends DurableObject`
|
|
* @param {string | null} egress the one host (`name[:port]`) the app may fetch, or none
|
|
* @returns {Promise<string>} the code's SHA-256, hex
|
|
*/
|
|
async start(src, egress) {
|
|
const hash = hex(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(src)));
|
|
const { sql } = this.ctx.storage;
|
|
sql.exec("INSERT OR IGNORE INTO bundle VALUES (?, ?)", hash, src);
|
|
sql.exec("INSERT OR REPLACE INTO current VALUES (0, ?)", hash);
|
|
const [prev] = sql.exec("SELECT n FROM incarnation").toArray();
|
|
sql.exec("INSERT OR REPLACE INTO incarnation VALUES (0, ?, ?)", (prev?.n ?? 0) + 1, egress);
|
|
this.ctx.facets.abort("app", new Error("upgraded"));
|
|
return hash;
|
|
}
|
|
|
|
#app({ hash, src, n, egress }) {
|
|
return this.ctx.facets.get("app", () => {
|
|
const worker = this.env.LOADER.get(`${this.ctx.id}:${n}`, () => ({
|
|
compatibilityDate: "2026-10-01",
|
|
mainModule: "app.js",
|
|
modules: { "app.js": src },
|
|
env: egress ? { egress: this.ctx.exports.Egress({ props: { host: egress } }) } : {},
|
|
globalOutbound: null,
|
|
}));
|
|
return { class: worker.getDurableObjectClass("App") };
|
|
});
|
|
}
|
|
|
|
fetch(request) {
|
|
const [row] = this.ctx.storage.sql
|
|
.exec(
|
|
`SELECT b.hash, b.src, coalesce(i.n, 0) AS n, i.egress
|
|
FROM current c JOIN bundle b USING (hash) LEFT JOIN incarnation i ON i.k = 0`,
|
|
)
|
|
.toArray();
|
|
if (!row) return new Response("no code installed\n", { status: 404 });
|
|
return this.#app(row).fetch(request);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A grant of one host: `fetch`, WebSocket upgrades included, reaches the
|
|
* switchboard only for the host in `ctx.props`, which no holder can change.
|
|
*/
|
|
export class Egress extends WorkerEntrypoint {
|
|
fetch(request) {
|
|
const { host } = new URL(request.url);
|
|
if (host !== this.ctx.props.host) return new Response(`egress to ${host} not granted\n`, { status: 403 });
|
|
return this.env.SWITCHBOARD.fetch(request);
|
|
}
|
|
}
|
|
|
|
const vatOf = (env, url) => env.VATS.getByName(new URL(url).pathname.split("/")[1]);
|
|
|
|
/** The http socket: `/<vat>/...` goes to that vat's app. */
|
|
export class Public extends WorkerEntrypoint {
|
|
fetch(request) {
|
|
return vatOf(this.env, request.url).fetch(request);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The admin socket: `PUT /<vat>` with an ES module body installs and starts
|
|
* it; `?egress=<host>` grants it that one host.
|
|
*/
|
|
export class Admin extends WorkerEntrypoint {
|
|
async fetch(request) {
|
|
if (request.method !== "PUT") return new Response("PUT code to /<vat>\n", { status: 405 });
|
|
const egress = new URL(request.url).searchParams.get("egress");
|
|
return new Response((await vatOf(this.env, request.url).start(await request.text(), egress)) + "\n");
|
|
}
|
|
}
|
|
|
|
export default Public;
|