/** * @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} held * @param {Record} 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} 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`); } }; /** * 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} a * @param {Record} 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} mount * @param {Record} request */ export const carries = (mount, request) => Object.entries(mount).every(([k, v]) => request[k] === v);