@sellable/install
Version:
One-command installer for Sellable MCP in Claude Code, Codex, and Hermes
242 lines (228 loc) • 10.1 kB
JavaScript
import { createHash } from "node:crypto";
import { promises as dns } from "node:dns";
import { createServer as createHttpServer } from "node:http";
import { isIP } from "node:net";
import { createConnection } from "node:net";
import { domainToASCII } from "node:url";
import { lstatSync, readFileSync, readlinkSync } from "node:fs";
import { isAbsolute, resolve } from "node:path";
import { RUNTIME_EGRESS_HOSTS } from "./runtime-boundary.mjs";
export const RUNTIME_EGRESS_PROXY_VERSION = "sellable-agent-egress-proxy/v1";
const CONFIG_KEYS = new Set(["version", "listenHost", "listenPort", "uid", "gid", "allowedHosts"]);
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
function stable(value) {
if (Array.isArray(value)) return value.map(stable);
if (value && typeof value === "object") {
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
}
return value;
}
export function validateRuntimeEgressProxyConfig(value) {
if (
!value ||
typeof value !== "object" ||
Array.isArray(value) ||
Object.keys(value).length !== CONFIG_KEYS.size ||
Object.keys(value).some((key) => !CONFIG_KEYS.has(key)) ||
value.version !== RUNTIME_EGRESS_PROXY_VERSION ||
value.listenHost !== "127.0.0.1" ||
!Number.isSafeInteger(value.listenPort) ||
value.listenPort < 1024 ||
value.listenPort > 65535 ||
!Number.isSafeInteger(value.uid) ||
value.uid < 1 ||
!Number.isSafeInteger(value.gid) ||
value.gid < 1 ||
JSON.stringify(value.allowedHosts) !== JSON.stringify([...RUNTIME_EGRESS_HOSTS])
) return { ok: false, code: "egress_proxy_config_rejected" };
const config = Object.freeze({ ...value, allowedHosts: Object.freeze([...value.allowedHosts]) });
return { ok: true, config, configDigest: sha256(JSON.stringify(stable(config))) };
}
function normalizedHost(value) {
if (typeof value !== "string" || !value || value.length > 253 || /[\0\s/@?#]/.test(value)) return null;
const ascii = domainToASCII(value).toLowerCase();
if (!ascii || ascii.endsWith(".") || isIP(ascii) || !/^[a-z0-9.-]+$/.test(ascii)) return null;
if (ascii.split(".").some((label) => !label || label.length > 63 || label.startsWith("-") || label.endsWith("-"))) {
return null;
}
return ascii;
}
function hostAllowed(host, allowedHosts) {
return allowedHosts.some((entry) =>
entry.startsWith(".") ? host.endsWith(entry) && host.length > entry.length : host === entry
);
}
export function authorizeRuntimeEgressTarget(config, authority) {
const validated = validateRuntimeEgressProxyConfig(config);
if (!validated.ok || typeof authority !== "string" || authority.length > 300) {
return { ok: false, code: "egress_target_rejected" };
}
const match = authority.match(/^([^:\[\]]+):(\d{1,5})$/);
if (!match) return { ok: false, code: "egress_target_rejected" };
const host = normalizedHost(match[1]);
const port = Number(match[2]);
if (!host || port !== 443 || !hostAllowed(host, validated.config.allowedHosts)) {
return { ok: false, code: "egress_target_rejected" };
}
return { ok: true, host, port };
}
function publicIpv4(address) {
const parts = address.split(".").map(Number);
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
const [a, b, c] = parts;
if (
a === 0 || a === 10 || a === 127 || a >= 224 ||
(a === 100 && b >= 64 && b <= 127) ||
(a === 169 && b === 254) ||
(a === 172 && b >= 16 && b <= 31) ||
(a === 192 && b === 0 && c === 0) ||
(a === 192 && b === 0 && c === 2) ||
(a === 192 && b === 168) ||
(a === 198 && (b === 18 || b === 19)) ||
(a === 198 && b === 51 && c === 100) ||
(a === 203 && b === 0 && c === 113)
) return false;
return true;
}
function publicIpv6(address) {
const normalized = address.toLowerCase().split("%")[0];
return !(
normalized === "::" ||
normalized === "::1" ||
normalized.startsWith("fc") ||
normalized.startsWith("fd") ||
/^fe[89ab]/.test(normalized) ||
normalized.startsWith("ff") ||
normalized.startsWith("2001:db8:") ||
normalized.startsWith("::ffff:")
);
}
export function isPublicRuntimeEgressAddress(address) {
const family = isIP(address);
return family === 4 ? publicIpv4(address) : family === 6 ? publicIpv6(address) : false;
}
function rejectConnect(socket, status = "403 Forbidden") {
if (!socket.destroyed) socket.end(`HTTP/1.1 ${status}\r\nConnection: close\r\n\r\n`);
}
export function createRuntimeEgressProxy(config, dependencies = {}) {
const validated = validateRuntimeEgressProxyConfig(config);
if (!validated.ok) throw new Error("egress proxy config rejected");
const lookup = dependencies.lookup ?? ((host) => dns.lookup(host, { all: true, verbatim: true, family: 4 }));
const dial = dependencies.dial ?? ((options) => createConnection(options));
const maxConnections = 256;
let connections = 0;
const server = createHttpServer({ maxHeaderSize: 8 * 1024 }, (_request, response) => {
response.writeHead(405, { Connection: "close", "Content-Length": "0" });
response.end();
});
server.on("connect", async (request, client, head) => {
const target = authorizeRuntimeEgressTarget(validated.config, request.url ?? "");
if (!target.ok || connections >= maxConnections) {
rejectConnect(client, connections >= maxConnections ? "429 Too Many Requests" : "403 Forbidden");
return;
}
connections += 1;
let upstream;
const release = () => { connections = Math.max(0, connections - 1); };
try {
const answers = await lookup(target.host);
const selected = Array.isArray(answers) ? answers[0] : null;
if (
!Array.isArray(answers) ||
!selected ||
answers.some((answer) => answer.family !== 4 || !publicIpv4(answer.address))
) {
throw new Error("egress DNS answer rejected");
}
upstream = dial({ host: selected.address, port: target.port, family: selected.family });
upstream.setTimeout?.(300_000, () => upstream.destroy());
upstream.once("connect", () => {
client.write("HTTP/1.1 200 Connection Established\r\nProxy-Agent: sellable-agent\r\n\r\n");
if (head?.length) upstream.write(head);
client.pipe(upstream);
upstream.pipe(client);
});
upstream.once("error", () => rejectConnect(client, "502 Bad Gateway"));
upstream.once("close", release);
client.once("error", () => upstream?.destroy());
client.once("close", () => upstream?.destroy());
} catch {
release();
rejectConnect(client, "502 Bad Gateway");
}
});
server.on("clientError", (_error, socket) => rejectConnect(socket, "400 Bad Request"));
server.maxConnections = maxConnections;
return {
server,
listen() {
return new Promise((resolveListen, rejectListen) => {
const onError = (error) => rejectListen(error);
server.once("error", onError);
server.listen(validated.config.listenPort, validated.config.listenHost, () => {
server.off("error", onError);
resolveListen({
ok: true,
version: RUNTIME_EGRESS_PROXY_VERSION,
host: validated.config.listenHost,
port: validated.config.listenPort,
allowedHosts: [...validated.config.allowedHosts],
configDigest: validated.configDigest,
});
});
});
},
close() {
return new Promise((resolveClose, rejectClose) => server.close((error) => error ? rejectClose(error) : resolveClose()));
},
};
}
function parseStatus(source) {
return Object.fromEntries(source.split(/\r?\n/).filter(Boolean).map((line) => {
const index = line.indexOf(":");
return [line.slice(0, index), line.slice(index + 1).trim()];
}));
}
export function observeRuntimeEgressProxy({ configPath, executablePath, pid, procRoot = "/proc", listenObserved }) {
try {
if (
!isAbsolute(configPath) || resolve(configPath) !== configPath ||
!isAbsolute(executablePath) || resolve(executablePath) !== executablePath ||
!Number.isSafeInteger(pid) || pid < 2 || listenObserved !== true
) throw new Error("proxy observation input rejected");
const configLink = lstatSync(configPath);
const executableLink = lstatSync(executablePath);
if (
configLink.isSymbolicLink() || !configLink.isFile() || (configLink.mode & 0o022) !== 0 ||
executableLink.isSymbolicLink() || !executableLink.isFile() || (executableLink.mode & 0o022) !== 0
) throw new Error("proxy observation file rejected");
const configBytes = readFileSync(configPath);
const validated = validateRuntimeEgressProxyConfig(JSON.parse(configBytes.toString("utf8")));
if (!validated.ok) throw new Error("proxy config rejected");
const processRoot = resolve(procRoot, String(pid));
const status = parseStatus(readFileSync(resolve(processRoot, "status"), "utf8"));
const uid = Number(status.Uid?.split(/\s+/)[0]);
const gid = Number(status.Gid?.split(/\s+/)[0]);
const cmdline = readFileSync(resolve(processRoot, "cmdline")).toString("utf8").split("\0").filter(Boolean);
if (
uid !== validated.config.uid || gid !== validated.config.gid ||
status.NoNewPrivs !== "1" || !/^0+$/.test(status.CapEff ?? "") ||
cmdline.length !== 2 || cmdline[0] !== "/usr/bin/node" || cmdline[1] !== executablePath
) throw new Error("proxy process identity rejected");
const nodeExecutable = readlinkSync(resolve(processRoot, "exe"));
if (!isAbsolute(nodeExecutable)) throw new Error("proxy executable rejected");
return {
ok: true,
pid,
uid,
gid,
host: validated.config.listenHost,
port: validated.config.listenPort,
executableDigest: sha256(readFileSync(executablePath)),
configDigest: sha256(configBytes),
allowedHosts: [...validated.config.allowedHosts],
};
} catch {
return { ok: false, code: "egress_proxy_observation_rejected" };
}
}