import { lookup } from "node:dns/promises"; import { isIP, BlockList } from "node:net"; import { request as httpsRequest } from "node:https"; import { request as httpRequest } from "node:http"; const blocked = new BlockList(); for (const [network, prefix] of [["0.0.0.0", 8], ["10.0.0.0", 8], ["100.64.0.0", 10], ["127.0.0.0", 8], ["169.254.0.0", 16], ["172.16.0.0", 12], ["192.168.0.0", 16], ["192.0.0.0", 24], ["192.0.2.0", 24], ["198.18.0.0", 15], ["198.51.100.0", 24], ["203.0.113.0", 24], ["224.0.0.0", 3]] as const) blocked.addSubnet(network, prefix); blocked.addSubnet("2001:db8::", 32, "ipv6"); blocked.addSubnet("2001::", 32, "ipv6"); blocked.addSubnet("2001:10::", 28, "ipv6"); blocked.addSubnet("2001:20::", 28, "ipv6"); blocked.addSubnet("2002::", 16, "ipv6"); export function publicAddress(address: string): boolean { const family = isIP(address); if (family === 4) return !blocked.check(address); // Reject mapped IPv4, local, multicast, transition and documentation ranges. return family === 6 && /^[23][0-9a-f]{3}:/i.test(address) && !blocked.check(address, "ipv6"); } /** * Serialize a request body for the pinned transport. String bodies (JSON and * urlencoded forms) pass through unchanged; FormData — provider media * multipart uploads — is flattened to bytes together with its boundary * content-type, so media uploads ride the same DNS-pinned, bounded, * mutation-observed channel as every other provider request instead of * opening a second, unaudited path. */ async function serializeBody( body: NonNullable, ): Promise<{ bytes: Buffer; contentType?: string }> { if (typeof body === "string") return { bytes: Buffer.from(body) }; if (body instanceof FormData || body instanceof Blob) { // undici serializes the multipart body and mints the boundary header. const request = new Request("http://localhost.internal/", { method: "POST", body }); const contentType = request.headers.get("content-type") ?? undefined; return { bytes: Buffer.from(await request.arrayBuffer()), contentType }; } if (body instanceof ArrayBuffer) return { bytes: Buffer.from(body) }; if (ArrayBuffer.isView(body)) return { bytes: Buffer.from(body.buffer, body.byteOffset, body.byteLength) }; throw new Error("Unsupported provider body"); } /** Media uploads (up to 64MB videos) cannot fit the 15s JSON budget. */ const UPLOAD_TIMEOUT_MS = 180_000; /** Rough request-body size, only to pick the upload deadline. */ function bodyBudgetOf(init: RequestInit): number { const body = init.body; if (body === undefined || body === null) return 0; if (typeof body === "string") return body.length; if (body instanceof ArrayBuffer) return body.byteLength; if (ArrayBuffer.isView(body)) return body.byteLength; if (body instanceof Blob) return body.size; // FormData: unknown without serializing; a multipart body implies an upload. if (body instanceof FormData) return Number.MAX_SAFE_INTEGER; return Number.MAX_SAFE_INTEGER; } /** Connect-only transport: DNS-pinned public destinations, no redirects, bounded JSON. */ export const providerFetch: typeof fetch = async (input, init = {}) => { const url = new URL(input instanceof Request ? input.url : String(input)); const hostname = url.hostname.replace(/^\[|\]$/g, ""); const local = process.env.NODE_ENV !== "production" && (hostname === "localhost" || hostname === "127.0.0.1"); if (url.username || url.password || (url.protocol !== "https:" && !(local && url.protocol === "http:"))) throw new Error("Invalid provider URL"); const uploadBytes = bodyBudgetOf(init); const signal = AbortSignal.any([AbortSignal.timeout(uploadBytes > 1_048_576 ? UPLOAD_TIMEOUT_MS : 15_000), ...(init.signal ? [init.signal] : [])]); signal.throwIfAborted(); const addresses = await Promise.race([lookup(hostname, { all: true }), new Promise((_resolve, reject) => { signal.addEventListener("abort", () => reject(new Error("PROVIDER_TIMEOUT")), { once: true }); })]); signal.throwIfAborted(); if (!addresses.length || (!local && addresses.some(({ address }) => !publicAddress(address)))) throw new Error("Provider must resolve to public addresses"); const pinned = addresses[0]; if (!pinned) throw new Error("Provider DNS unavailable"); const headers = Object.fromEntries(new Headers(init.headers).entries()); let body: Buffer | undefined; if (init.body !== undefined && init.body !== null) { const serialized = await serializeBody(init.body); body = serialized.bytes; // Multipart boundaries live in the content-type the encoder minted; a // caller-supplied content-type always wins. if (serialized.contentType !== undefined && headers["content-type"] === undefined) { headers["content-type"] = serialized.contentType; } } return new Promise((resolve, reject) => { const request = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, { method: init.method ?? "GET", headers, family: pinned.family, signal, lookup: (_name, _options, callback) => callback(null, pinned.address, pinned.family), }, (response) => { const chunks: Buffer[] = []; let size = 0; response.on("data", (chunk: Buffer) => { size += chunk.length; if (size > 1_048_576) request.destroy(new Error("Provider response too large")); else chunks.push(chunk); }); response.on("error", reject); response.on("aborted", () => reject(new Error("Provider response aborted"))); response.on("end", () => { const status = response.statusCode ?? 502; if (status >= 300 && status < 400) { reject(new Error("Provider redirect refused")); return; } const responseHeaders = new Headers(); for (const [key, value] of Object.entries(response.headers)) { if (value !== undefined) responseHeaders.set(key, Array.isArray(value) ? value.join(", ") : value); } try { resolve(new Response(status === 204 || status === 205 ? null : Buffer.concat(chunks), { status, headers: responseHeaders })); } catch { reject(new Error("Invalid provider response")); } }); }); request.setTimeout(uploadBytes > 1_048_576 ? UPLOAD_TIMEOUT_MS : 15_000, () => request.destroy(new Error("Provider timeout"))); request.on("error", reject); if (body !== undefined) request.write(body); request.end(); }); };