Each request parsed every mount's labels and searched them in JS. Labels now sit one per row in mount_label, indexed on (key, value): a lookup joins the request's labels (json_each) on the index and keeps the vat whose every label matched. The overlap check at registration is SQL too. mount.labels keeps the whole set as JSON; mounts stored before mount_label get their rows filled in when the schema runs. The SQL lives in mounts-sql.js, tested on node:sqlite, old-shape state included. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
54 lines
1.6 KiB
JavaScript
54 lines
1.6 KiB
JavaScript
/**
|
|
* @file Egress labels, after [Flower](https://github.com/NuxiNL/flower)'s
|
|
* switchboard: string key-value pairs that only accumulate. A missing key
|
|
* matches anything; a key present on both sides must agree.
|
|
*
|
|
* Keys that constrain a URL: `scheme` (`http`, `https`), `host` (name or
|
|
* address, no port), `port` (explicit or the scheme's default). The
|
|
* switchboard sets `network` per socket; it constrains no URL.
|
|
*/
|
|
|
|
const DEFAULT_PORT = { http: "80", https: "443" };
|
|
|
|
/**
|
|
* Union of two label sets; throws on a key they give different values,
|
|
* naming the key but not the values (they may end up in logs).
|
|
*
|
|
* @param {Record<string, string>} held
|
|
* @param {Record<string, string>} more
|
|
*/
|
|
export const mergeLabels = (held, more) => {
|
|
const merged = { ...held };
|
|
for (const [k, v] of Object.entries(more)) {
|
|
if (typeof v !== "string") throw new TypeError(`label ${k} is not a string`);
|
|
if (k in merged && merged[k] !== v) throw new Error(`label ${k} is already set to a different value`);
|
|
merged[k] = v;
|
|
}
|
|
return Object.freeze(merged);
|
|
};
|
|
|
|
/**
|
|
* The labels a URL carries.
|
|
*
|
|
* @param {string} url
|
|
*/
|
|
export const urlLabels = (url) => {
|
|
const u = new URL(url);
|
|
const scheme = u.protocol.replace(/:$/, "");
|
|
return { scheme, host: u.hostname, port: u.port || DEFAULT_PORT[scheme] || "" };
|
|
};
|
|
|
|
/**
|
|
* Throws unless `url` is within `labels`.
|
|
*
|
|
* @param {Record<string, string>} labels
|
|
* @param {string} url
|
|
*/
|
|
export const checkUrl = (labels, url) => {
|
|
const actual = urlLabels(url);
|
|
for (const [k, v] of Object.entries(actual)) {
|
|
if (k in labels && labels[k] !== v) throw new Error(`egress to ${actual.host} not granted`);
|
|
}
|
|
};
|
|
|