WIP: toybox: OCapN vats on workerd, with ingress and egress as capabilities #4
No files matched your search
@@ -51,23 +51,3 @@ export const checkUrl = (labels, url) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Do `a` and `b` give some key different values? Flower refuses to register
|
|
||||||
* a target that doesn't conflict with every other: lookups stay unambiguous.
|
|
||||||
*
|
|
||||||
* @param {Record<string, string>} a
|
|
||||||
* @param {Record<string, string>} b
|
|
||||||
*/
|
|
||||||
export const conflict = (a, b) => Object.entries(a).some(([k, v]) => k in b && b[k] !== v);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Does `request` carry every label of `mount`, with the same values?
|
|
||||||
*
|
|
||||||
* Stricter than Flower's match (no conflict): an ingress client holds no
|
|
||||||
* constrained handle, so a label it omits must not match, or a secret
|
|
||||||
* label would protect nothing.
|
|
||||||
*
|
|
||||||
* @param {Record<string, string>} mount
|
|
||||||
* @param {Record<string, string>} request
|
|
||||||
*/
|
|
||||||
export const carries = (mount, request) => Object.entries(mount).every(([k, v]) => request[k] === v);
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* @file The ingress's mounts, in SQL: which vat serves which label set.
|
||||||
|
*
|
||||||
|
* Labels sit one per row, so a lookup can use an index; `mount.labels`
|
||||||
|
* keeps the whole set as JSON. Request and mount labels arrive as JSON
|
||||||
|
* objects, split into rows by `json_each`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const MOUNT_SCHEMA = [
|
||||||
|
"CREATE TABLE IF NOT EXISTS mount (vat TEXT PRIMARY KEY, labels TEXT)",
|
||||||
|
"CREATE TABLE IF NOT EXISTS mount_label (vat TEXT, key TEXT, value TEXT, PRIMARY KEY (vat, key))",
|
||||||
|
"CREATE INDEX IF NOT EXISTS mount_label_kv ON mount_label (key, value)",
|
||||||
|
// mounts made before mount_label existed
|
||||||
|
`INSERT OR IGNORE INTO mount_label
|
||||||
|
SELECT m.vat, j.key, j.value FROM mount m, json_each(m.labels) j
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM mount_label l WHERE l.vat = m.vat)`,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Another mount (than ?2's) whose labels conflict with none of ?1's.
|
||||||
|
* Flower's registration rule: every mount must conflict with every other.
|
||||||
|
*/
|
||||||
|
export const OVERLAP = `
|
||||||
|
SELECT 1 FROM mount m WHERE m.vat != ?2 AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM mount_label l JOIN json_each(?1) r ON r.key = l.key
|
||||||
|
WHERE l.vat = m.vat AND l.value != r.value)
|
||||||
|
LIMIT 1`;
|
||||||
|
|
||||||
|
export const MOUNT = "INSERT OR REPLACE INTO mount VALUES (?1, ?2)";
|
||||||
|
export const MOUNT_LABELS = "INSERT INTO mount_label SELECT ?1, key, value FROM json_each(?2)";
|
||||||
|
export const UNMOUNT = "DELETE FROM mount WHERE vat = ?1";
|
||||||
|
export const UNMOUNT_LABELS = "DELETE FROM mount_label WHERE vat = ?1";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mount whose every label ?1 (the request's labels) carries, with the
|
||||||
|
* same value: the labels that match, counted per vat, against how many
|
||||||
|
* the vat has. A mount with no labels matches any request.
|
||||||
|
*
|
||||||
|
* Stricter than Flower's match (no conflict): an ingress client holds no
|
||||||
|
* constrained handle, so a label it omits must not match, or a secret
|
||||||
|
* label would protect nothing.
|
||||||
|
*/
|
||||||
|
export const LOOKUP = `
|
||||||
|
SELECT m.vat, m.labels FROM mount m JOIN (
|
||||||
|
SELECT l.vat, count(*) AS hits
|
||||||
|
FROM json_each(?1) r JOIN mount_label l ON l.key = r.key AND l.value = r.value
|
||||||
|
GROUP BY l.vat
|
||||||
|
) h ON h.vat = m.vat
|
||||||
|
WHERE h.hits = (SELECT count(*) FROM mount_label WHERE vat = m.vat)
|
||||||
|
UNION ALL
|
||||||
|
SELECT m.vat, m.labels FROM mount m
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM mount_label l WHERE l.vat = m.vat)
|
||||||
|
LIMIT 1`;
|
||||||
+17
-17
@@ -4,7 +4,8 @@ import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
|
|||||||
import { RpcTarget, newWebSocketRpcSession, newWorkersRpcResponse } from "capnweb";
|
import { RpcTarget, newWebSocketRpcSession, newWorkersRpcResponse } from "capnweb";
|
||||||
import { E, Far } from "@endo/far";
|
import { E, Far } from "@endo/far";
|
||||||
import { makeClient, swissnumFromBytes } from "@endo/ocapn";
|
import { makeClient, swissnumFromBytes } from "@endo/ocapn";
|
||||||
import { carries, conflict, mergeLabels, urlLabels } from "./labels.js";
|
import { mergeLabels, urlLabels } from "./labels.js";
|
||||||
|
import { LOOKUP, MOUNT, MOUNT_LABELS, MOUNT_SCHEMA, OVERLAP, UNMOUNT, UNMOUNT_LABELS } from "./mounts-sql.js";
|
||||||
import { locationOf, makeRpcNetLayer } from "./netlayer.js";
|
import { locationOf, makeRpcNetLayer } from "./netlayer.js";
|
||||||
import VAT_RUNTIME from "vat-runtime.txt";
|
import VAT_RUNTIME from "vat-runtime.txt";
|
||||||
|
|
||||||
@@ -282,14 +283,7 @@ export class Router extends WorkerEntrypoint {
|
|||||||
export class Directory extends DurableObject {
|
export class Directory extends DurableObject {
|
||||||
constructor(ctx, env) {
|
constructor(ctx, env) {
|
||||||
super(ctx, env);
|
super(ctx, env);
|
||||||
ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS mount (vat TEXT PRIMARY KEY, labels TEXT)");
|
for (const statement of MOUNT_SCHEMA) ctx.storage.sql.exec(statement);
|
||||||
}
|
|
||||||
|
|
||||||
#mounts() {
|
|
||||||
return this.ctx.storage.sql
|
|
||||||
.exec("SELECT vat, labels FROM mount")
|
|
||||||
.toArray()
|
|
||||||
.map(({ vat, labels }) => ({ vat, labels: JSON.parse(labels) }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -299,22 +293,28 @@ export class Directory extends DurableObject {
|
|||||||
* @param {Record<string, string> | null} labels
|
* @param {Record<string, string> | null} labels
|
||||||
*/
|
*/
|
||||||
mount(vat, labels) {
|
mount(vat, labels) {
|
||||||
if (labels) {
|
const { sql } = this.ctx.storage;
|
||||||
const overlap = this.#mounts().find((m) => m.vat !== vat && !conflict(m.labels, labels));
|
this.ctx.storage.transactionSync(() => {
|
||||||
if (overlap) throw new Error("mount overlaps another: add a label whose value tells them apart");
|
sql.exec(UNMOUNT_LABELS, vat);
|
||||||
this.ctx.storage.sql.exec("INSERT OR REPLACE INTO mount VALUES (?, ?)", vat, JSON.stringify(labels));
|
if (!labels) return void sql.exec(UNMOUNT, vat);
|
||||||
} else {
|
const json = JSON.stringify(labels);
|
||||||
this.ctx.storage.sql.exec("DELETE FROM mount WHERE vat = ?", vat);
|
if (sql.exec(OVERLAP, json, vat).toArray().length) {
|
||||||
|
throw new Error("mount overlaps another: add a label whose value tells them apart");
|
||||||
}
|
}
|
||||||
|
sql.exec(MOUNT, vat, json);
|
||||||
|
sql.exec(MOUNT_LABELS, vat, json);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The mount whose labels `request` carries, if any.
|
* The mount whose labels `request` carries, if any. Registration keeps
|
||||||
|
* mounts pairwise conflicting, so at most one can match.
|
||||||
*
|
*
|
||||||
* @param {Record<string, string>} request
|
* @param {Record<string, string>} request
|
||||||
*/
|
*/
|
||||||
lookup(request) {
|
lookup(request) {
|
||||||
return this.#mounts().find((m) => carries(m.labels, request)) ?? null;
|
const [row] = this.ctx.storage.sql.exec(LOOKUP, JSON.stringify(request)).toArray();
|
||||||
|
return row ? { vat: row.vat, labels: JSON.parse(row.labels) } : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/** @file The ingress's mount SQL, on node:sqlite: lookup, overlap, and mounts stored before mount_label. */
|
||||||
|
import { DatabaseSync } from "node:sqlite";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { LOOKUP, MOUNT, MOUNT_LABELS, MOUNT_SCHEMA, OVERLAP } from "../mounts-sql.js";
|
||||||
|
|
||||||
|
const makeDb = () => {
|
||||||
|
const db = new DatabaseSync(":memory:");
|
||||||
|
for (const statement of MOUNT_SCHEMA) db.exec(statement);
|
||||||
|
const mount = (vat, labels) => {
|
||||||
|
const json = JSON.stringify(labels);
|
||||||
|
db.prepare(MOUNT).run(vat, json);
|
||||||
|
db.prepare(MOUNT_LABELS).run(vat, json);
|
||||||
|
};
|
||||||
|
const lookup = (request) => db.prepare(LOOKUP).get(JSON.stringify(request))?.vat ?? null;
|
||||||
|
const overlaps = (vat, labels) => !!db.prepare(OVERLAP).get(JSON.stringify(labels), vat);
|
||||||
|
return { db, mount, lookup, overlaps };
|
||||||
|
};
|
||||||
|
|
||||||
|
test("a request reaches the mount whose every label it carries", () => {
|
||||||
|
const { mount, lookup } = makeDb();
|
||||||
|
mount("a", { vat: "alice", key: "s3cret" });
|
||||||
|
mount("b", { vat: "bob" });
|
||||||
|
assert.equal(lookup({ vat: "alice", key: "s3cret", page: "2" }), "a");
|
||||||
|
assert.equal(lookup({ vat: "alice" }), null);
|
||||||
|
assert.equal(lookup({ vat: "alice", key: "wrong" }), null);
|
||||||
|
assert.equal(lookup({ vat: "bob" }), "b");
|
||||||
|
assert.equal(lookup({}), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a mount with no labels matches any request", () => {
|
||||||
|
const { mount, lookup } = makeDb();
|
||||||
|
mount("only", {});
|
||||||
|
assert.equal(lookup({ anything: "x" }), "only");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("overlap: a mount must conflict with every other", () => {
|
||||||
|
const { mount, overlaps } = makeDb();
|
||||||
|
mount("a", { vat: "alice" });
|
||||||
|
assert.equal(overlaps("b", { vat: "alice", n: "2" }), true);
|
||||||
|
assert.equal(overlaps("b", { app: "x" }), true);
|
||||||
|
assert.equal(overlaps("b", { vat: "bob" }), false);
|
||||||
|
assert.equal(overlaps("a", { vat: "alice", n: "2" }), false, "a vat's own mount doesn't count");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mounts stored before mount_label are filled in when the schema runs", () => {
|
||||||
|
const db = new DatabaseSync(":memory:");
|
||||||
|
db.exec("CREATE TABLE mount (vat TEXT PRIMARY KEY, labels TEXT)");
|
||||||
|
db.prepare("INSERT INTO mount VALUES (?, ?)").run("old", JSON.stringify({ vat: "old" }));
|
||||||
|
for (const statement of MOUNT_SCHEMA) db.exec(statement);
|
||||||
|
assert.equal(db.prepare(LOOKUP).get(JSON.stringify({ vat: "old" }))?.vat, "old");
|
||||||
|
assert.equal(db.prepare(LOOKUP).get(JSON.stringify({ vat: "other" })), undefined);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user