Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270// node:net. Address predicates, BlockList and SocketAddress are real (pure JS, no// socket involved); sockets and servers refuse, because guests reach the network only// through http over the SW bridge.import { invalidArgType } from "./errors";import { refused, refusedClass } from "./refuse";
const v4Seg = "(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])";const v4Str = `(?:${v4Seg}\\.){3}${v4Seg}`;const IPv4Reg = new RegExp(`^${v4Str}$`);const v6Seg = "(?:[0-9a-fA-F]{1,4})";const IPv6Reg = new RegExp("^(?:" + `(?:${v6Seg}:){7}(?:${v6Seg}|:)|` + `(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|` + `(?:${v6Seg}:){5}(?::${v4Str}|(?::${v6Seg}){1,2}|:)|` + `(?:${v6Seg}:){4}(?:(?::${v6Seg}){0,1}:${v4Str}|(?::${v6Seg}){1,3}|:)|` + `(?:${v6Seg}:){3}(?:(?::${v6Seg}){0,2}:${v4Str}|(?::${v6Seg}){1,4}|:)|` + `(?:${v6Seg}:){2}(?:(?::${v6Seg}){0,3}:${v4Str}|(?::${v6Seg}){1,5}|:)|` + `(?:${v6Seg}:){1}(?:(?::${v6Seg}){0,4}:${v4Str}|(?::${v6Seg}){1,6}|:)|` + `(?::(?:(?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:))` + ")(?:%[0-9a-zA-Z-.:]{1,})?$");
const isIPv4 = (s: unknown): boolean => IPv4Reg.test(String(s));const isIPv6 = (s: unknown): boolean => IPv6Reg.test(String(s));export const isIP = (s: unknown): 0 | 4 | 6 => (isIPv4(s) ? 4 : isIPv6(s) ? 6 : 0);
function outOfRange(name: string, range: string, received: unknown): RangeError { return Object.assign(new RangeError(`The value of "${name}" is out of range. It must be ${range}. Received ${received}`), { code: "ERR_OUT_OF_RANGE" });}
function validateInt(value: unknown, name: string, min: number, max: number): number { if (typeof value !== "number") throw invalidArgType(name, "of type number", value); if (!Number.isInteger(value)) throw outOfRange(name, "an integer", value); if (value < min || value > max) throw outOfRange(name, `>= ${min} && <= ${max}`, value); return value;}
/** A dotted-quad as a 32-bit int, or null when `address` is not one. */function ipv4ToInt(address: string): number | null { if (!isIPv4(address)) return null; const parts = address.split(".").map(Number); return ((parts[0]! << 24) | (parts[1]! << 16) | (parts[2]! << 8) | parts[3]!) >>> 0;}
/** An IPv6 address as a 128-bit value, expanding "::" and any embedded IPv4 tail. */function ipv6ToBigInt(address: string): bigint | null { const bare = address.split("%")[0]!; if (!isIPv6(bare)) return null; const expand = (part: string): string[] => { if (!part.includes(".")) return [part]; const [a, b, c, d] = part.split(".").map(Number); return [(((a! << 8) | b!) & 0xffff).toString(16), (((c! << 8) | d!) & 0xffff).toString(16)]; }; const [head, tail] = bare.includes("::") ? bare.split("::") : [bare, undefined]; const headParts = (head ? head.split(":") : []).flatMap(expand); const tailParts = tail === undefined ? [] : (tail ? tail.split(":") : []).flatMap(expand); const missing = 8 - headParts.length - tailParts.length; const groups = [...headParts, ...Array(Math.max(0, missing)).fill("0"), ...tailParts]; let value = 0n; for (const g of groups) value = (value << 16n) | BigInt(Number.parseInt(g || "0", 16)); return value;}
function invalidAddress(): Error { return Object.assign(new Error("Invalid socket address"), { code: "ERR_INVALID_ADDRESS" });}
type Family = "ipv4" | "ipv6";
function normalizeFamily(family: unknown): Family { const f = String(family ?? "ipv4").toLowerCase(); if (f !== "ipv4" && f !== "ipv6") { throw Object.assign(new TypeError(`The property 'options.family' is invalid. Received '${String(family)}'`), { code: "ERR_INVALID_ARG_VALUE" }); } return f;}
function addressToValue(address: string, family: Family): bigint { const v = family === "ipv4" ? ipv4ToInt(address) : ipv6ToBigInt(address); if (v === null) throw invalidAddress(); return BigInt(v);}
type Rule = | { kind: "address"; family: Family; address: string; value: bigint } | { kind: "range"; family: Family; start: string; end: string; low: bigint; high: bigint } | { kind: "subnet"; family: Family; net: string; prefix: number; base: bigint; bits: number };
/** net.BlockList: pure address-set arithmetic, needing no socket to evaluate. Node's * own is backed by a native list; this one is a plain array checked in insertion * order (reported newest-first, matching Node's `rules`). */export class BlockList { readonly #rules: Rule[] = [];
addAddress(address: string | { address: string }, family?: string): void { const addr = typeof address === "string" ? address : address.address; if (typeof addr !== "string") throw invalidArgType("address", "of type string", address); const fam = normalizeFamily(family); const value = addressToValue(addr, fam); this.#rules.push({ kind: "address", family: fam, address: addr, value }); }
addRange(start: string, end: string, family?: string): void { if (typeof start !== "string") throw invalidArgType("start", "of type string", start); if (typeof end !== "string") throw invalidArgType("end", "of type string", end); const fam = normalizeFamily(family); const low = addressToValue(start, fam); const high = addressToValue(end, fam); if (low > high) throw Object.assign(new TypeError(`The argument 'start' must come before 'end'. Received '${start}'`), { code: "ERR_INVALID_ARG_VALUE" }); this.#rules.push({ kind: "range", family: fam, start, end, low, high }); }
addSubnet(net: string, prefix: number, family?: string): void { if (typeof net !== "string") throw invalidArgType("net", "of type string", net); const fam = normalizeFamily(family); const bits = fam === "ipv4" ? 32 : 128; validateInt(prefix, "prefix", 0, bits); const base = addressToValue(net, fam); this.#rules.push({ kind: "subnet", family: fam, net, prefix, base, bits }); }
check(address: string | { address: string }, family?: string): boolean { const addr = typeof address === "string" ? address : address.address; let fam: Family; let value: bigint; try { fam = normalizeFamily(family); value = addressToValue(String(addr), fam); } catch { return false; } for (const rule of this.#rules) { if (rule.family !== fam) continue; if (rule.kind === "address" && rule.value === value) return true; if (rule.kind === "range" && value >= rule.low && value <= rule.high) return true; if (rule.kind === "subnet") { const shift = BigInt(rule.bits - rule.prefix); if (rule.prefix === rule.bits ? value === rule.base : (value >> shift) === (rule.base >> shift)) return true; } } return false; }
get rules(): string[] { return this.#rules.slice().reverse().map((rule) => { const fam = rule.family === "ipv4" ? "IPv4" : "IPv6"; if (rule.kind === "address") return `Address: ${fam} ${rule.address}`; if (rule.kind === "range") return `Range: ${fam} ${rule.start}-${rule.end}`; return `Subnet: ${fam} ${rule.net}/${rule.prefix}`; }); }
toJSON(): string[] { return this.rules; }
/** Reconstructs rules from `rules`'s own text, added in the given order (as Node's * own does) — which reverses the set again, since `rules` itself reads newest-first. */ fromJSON(data: unknown): void { const list = typeof data === "string" ? JSON.parse(data) : data; if (!Array.isArray(list) || list.some((n) => typeof n !== "string")) { throw invalidArgType("data", "of type string or string[]", data); } for (const rule of list as string[]) { const address = /^Address: (IPv4|IPv6) (.+)$/.exec(rule); const range = /^Range: (IPv4|IPv6) (.+)-(.+)$/.exec(rule); const subnet = /^Subnet: (IPv4|IPv6) (.+)\/(\d+)$/.exec(rule); const fam = (f: string): string => (f === "IPv4" ? "ipv4" : "ipv6"); if (address) this.addAddress(address[2]!, fam(address[1]!)); else if (range) this.addRange(range[2]!, range[3]!, fam(range[1]!)); else if (subnet) this.addSubnet(subnet[2]!, Number(subnet[3]), fam(subnet[1]!)); } }
static isBlockList(value: unknown): value is BlockList { return value instanceof BlockList; }}
interface SocketAddressInit { address?: string; port?: number; family?: string; flowlabel?: number;}
function validatePort(port: unknown): number { if (typeof port !== "number" || !Number.isInteger(port) || port < 0 || port >= 65536) { throw Object.assign(new RangeError(`options.port should be >= 0 and < 65536. Received type ${typeof port} (${String(port)}).`), { code: "ERR_SOCKET_BAD_PORT" }); } return port;}
/** net.SocketAddress: an address/port/family/flowlabel tuple, no socket attached. */export class SocketAddress { readonly #address: string; readonly #port: number; readonly #family: Family; readonly #flowlabel: number;
constructor(init: SocketAddressInit = {}) { this.#family = normalizeFamily(init.family); this.#address = init.address ?? (this.#family === "ipv4" ? "127.0.0.1" : "::"); addressToValue(this.#address, this.#family); // validates; throws ERR_INVALID_ADDRESS this.#port = init.port === undefined ? 0 : validatePort(init.port); this.#flowlabel = init.flowlabel ?? 0; }
get address(): string { return this.#address; } get port(): number { return this.#port; } get family(): Family { return this.#family; } get flowlabel(): number { return this.#flowlabel; }
toJSON(): { address: string; port: number; family: Family; flowlabel: number } { return { address: this.#address, port: this.#port, family: this.#family, flowlabel: this.#flowlabel }; }
static isSocketAddress(value: unknown): value is SocketAddress { return value instanceof SocketAddress; }
/** Node's own trick: borrow the URL parser (an "http://" wrapper) to split * host/port, including its quirk of reporting an empty port for scheme 80 since * that's http's default port and the URL spec drops it. */ static parse(input: string): SocketAddress | undefined { if (typeof input !== "string") throw invalidArgType("input", "of type string", input); try { const url = new URL(`http://${input}`); const port = Number(url.port) | 0; if (url.hostname.startsWith("[") && url.hostname.endsWith("]")) { return new SocketAddress({ address: url.hostname.slice(1, -1), port, family: "ipv6" }); } return new SocketAddress({ address: url.hostname, port }); } catch { return undefined; } }}
let autoSelectFamilyAttemptTimeout = 250;
const WHY = "guests reach the network only through node:http over the Service Worker bridge";
export const net = { isIP, isIPv4, isIPv6, createServer: refused("net", "createServer", WHY), createConnection: refused("net", "createConnection", WHY), connect: refused("net", "connect", WHY), Socket: refusedClass("net", "Socket", WHY), Server: refusedClass("net", "Server", WHY), Stream: refusedClass("net", "Stream", WHY), BlockList, SocketAddress, getDefaultAutoSelectFamily: () => true, setDefaultAutoSelectFamily: refused("net", "setDefaultAutoSelectFamily", WHY), getDefaultAutoSelectFamilyAttemptTimeout: () => autoSelectFamilyAttemptTimeout, setDefaultAutoSelectFamilyAttemptTimeout: (value: unknown) => { autoSelectFamilyAttemptTimeout = validateInt(value, "value", 1, 2_147_483_647); },};