import http from "node:http"; import https from "node:https"; import { AsyncLocalStorage } from "node:async_hooks"; import { X509Certificate } from "node:crypto"; import net from "node:net"; import tls from "node:tls"; import { Agent as UndiciAgent, Dispatcher, FormData as UndiciFormData, Headers as UndiciHeaders, Pool as UndiciPool, Request as UndiciRequest, Response as UndiciResponse, errors as undiciErrors, fetch as undiciFetch, getGlobalDispatcher, ProxyAgent as UndiciProxyAgent, setGlobalDispatcher, } from "undici"; import { createAmbientProxyResolver, EMPTY_PROXY_ENV, resolveAmbientProxyForUrl, readProxyEnv, type ProxyEnvSnapshot, } from "./env.js"; import { bindNodeHttpMethod, createDirectNodeAgent, createNodeProxyAgent, type NodeHttpStackSnapshot, type ProxylineNodeProxyAgent, } from "./node-http.js"; import { formatUrl, ProxylineError, redactProxyUrl, resolveProxyTlsCa, } from "./shared.js"; import type { ProxylineBypassRegistration, ProxylineBypassPolicy, ProxylineEvent, ProxylineHandle, ProxylineOptions, ProxyResolver, ProxylineSurface, ProxylineUndiciOptions, } from "./types.js"; import { PROXYLINE_DISPATCHER_BRAND } from "./dispatcher-brand.js"; type RuntimeInstall = { ambientEnv: ProxyEnvSnapshot | undefined; bypassPolicy: ProxylineBypassPolicy | undefined; installedDispatcher: Dispatcher; mode: ProxylineOptions["mode"]; nodeHttpAgent: ProxylineNodeProxyAgent; nodeHttpsAgent: ProxylineNodeProxyAgent; originalDispatcher: Dispatcher; originalFetch: typeof globalThis.fetch; originalFormData: typeof globalThis.FormData; originalHeaders: typeof globalThis.Headers; originalRequest: typeof globalThis.Request; originalResponse: typeof globalThis.Response; proxyCa: string | undefined; proxyUrl: string | undefined; snapshot: NodeHttpStackSnapshot; undiciOptions: ProxylineUndiciOptions | undefined; }; let activeRuntime: RuntimeInstall | undefined; let activeHandle: ProxylineHandle | undefined; type ProxylineDispatcher = Dispatcher & { [PROXYLINE_DISPATCHER_BRAND]?: true; }; type CompatibleDispatchHandler = Dispatcher.DispatchHandler & { onError?: (error: Error) => void; onResponseError?: (controller: Dispatcher.DispatchController | null, error: Error) => void; }; // Node's global fetch types come from bundled undici-types, while the runtime // implementation intentionally delegates to this package's undici dependency. const proxylineHeaders = UndiciHeaders as unknown as typeof globalThis.Headers; const proxylineRequest = UndiciRequest as unknown as typeof globalThis.Request; const proxylineResponse = UndiciResponse as unknown as typeof globalThis.Response; const proxylineFormData = UndiciFormData as unknown as typeof globalThis.FormData; type ProxylineRequestInit = { body?: unknown; cache?: unknown; credentials?: unknown; dispatcher?: unknown; duplex?: "half"; headers?: unknown; integrity?: unknown; keepalive?: unknown; method?: string; mode?: unknown; redirect?: unknown; referrer?: unknown; referrerPolicy?: unknown; signal?: unknown; }; type FetchRequestLike = Readonly<{ arrayBuffer: () => Promise; body: ReadableStream | null; cache?: unknown; credentials?: unknown; headers: InstanceType; integrity?: unknown; keepalive?: unknown; method: string; mode?: unknown; redirect?: unknown; referrer?: unknown; referrerPolicy?: unknown; signal?: unknown; url: string; }>; function getRequestDispatcher(request: FetchRequestLike): unknown { for (const symbol of Object.getOwnPropertySymbols(request)) { if (symbol.description !== "dispatcher") { continue; } return Reflect.get(request, symbol); } return undefined; } function isFetchRequestLike(value: unknown): value is FetchRequestLike { if (typeof value !== "object" || value === null) { return false; } const record = value as Readonly>; return ( typeof record.url === "string" && typeof record.method === "string" && typeof record.arrayBuffer === "function" && record.headers !== undefined ); } async function createProxylineRequestFromRequestLike( request: FetchRequestLike, options: { includeBody: boolean; preserveDispatcher: boolean }, ): Promise { const init: ProxylineRequestInit = { headers: request.headers, method: request.method, }; if (request.cache !== undefined) { init.cache = request.cache; } if (request.credentials !== undefined) { init.credentials = request.credentials; } if (request.integrity !== undefined) { init.integrity = request.integrity; } if (request.keepalive !== undefined) { init.keepalive = request.keepalive; } if (request.mode !== undefined) { init.mode = request.mode; } if (request.redirect !== undefined) { init.redirect = request.redirect; } if (request.referrer !== undefined) { init.referrer = request.referrer; } if (request.referrerPolicy !== undefined) { init.referrerPolicy = request.referrerPolicy; } if (options.preserveDispatcher) { const dispatcher = getRequestDispatcher(request); if (dispatcher !== undefined) { Reflect.set(init, "dispatcher", dispatcher); } } if (request.signal !== undefined) { init.signal = request.signal; } if ( options.includeBody && request.body !== null && request.method !== "GET" && request.method !== "HEAD" ) { init.body = request.body; init.duplex = "half"; } const requestUnknown: unknown = Reflect.construct(proxylineRequest, [request.url, init]); if (!(requestUnknown instanceof proxylineRequest)) { throw new TypeError("Proxyline failed to normalize a fetch Request."); } return requestUnknown; } function requestInitOverridesBody(init: Parameters[1]): boolean { if (typeof init !== "object" || init === null) { return false; } return "body" in init; } async function normalizeFetchInput( input: Parameters[0], init: Parameters[1], options: { preserveDispatcher: boolean }, ): Promise[0]> { if ((input instanceof proxylineRequest && options.preserveDispatcher) || !isFetchRequestLike(input)) { return input; } return await createProxylineRequestFromRequestLike(input, { includeBody: !requestInitOverridesBody(init), preserveDispatcher: options.preserveDispatcher, }); } function withManagedFetchDispatcher( init: Parameters[1], dispatcher: Dispatcher, ): Parameters[1] { if ( init !== undefined && init !== null && typeof init !== "object" && typeof init !== "function" ) { throw new TypeError( `Request constructor: Expected ${String(init)} to be one of: Null, Undefined, Object.`, ); } const sanitized = init === undefined || init === null ? {} : Object.create(init); Reflect.defineProperty(sanitized, "dispatcher", { configurable: true, enumerable: true, value: dispatcher, writable: true, }); return sanitized; } const proxylineFetch: typeof globalThis.fetch = async (input, init) => { const managedDispatcher = activeRuntime?.mode === "managed" ? activeRuntime.installedDispatcher : undefined; const normalizedInput = await normalizeFetchInput(input, init, { preserveDispatcher: managedDispatcher === undefined, }); const normalizedInit = managedDispatcher === undefined ? init : withManagedFetchDispatcher(init, managedDispatcher); const response: unknown = await Reflect.apply( undiciFetch, undefined, normalizedInit === undefined ? [normalizedInput] : [normalizedInput, normalizedInit], ); if (!(response instanceof proxylineResponse)) { throw new TypeError("Proxyline fetch returned a non-Response value."); } return response; }; function normalizeProxyUrl(value: string | URL | undefined): URL | undefined { if (value === undefined) { return undefined; } const url = value instanceof URL ? new URL(value.href) : new URL(value); if (url.protocol !== "http:" && url.protocol !== "https:") { throw new ProxylineError( "UNSUPPORTED_PROXY_PROTOCOL", `Proxyline only supports http:// and https:// proxy endpoints in this slice: ${url.protocol}`, ); } return url; } function emit(onEvent: ProxylineOptions["onEvent"], event: ProxylineEvent): void { onEvent?.(event); } function isProxyableUrlProtocol(protocol: string): boolean { return protocol === "http:" || protocol === "https:" || protocol === "ws:" || protocol === "wss:"; } function shouldBypassManagedProxy( bypassPolicy: ProxylineBypassPolicy | undefined, bypasses: DynamicBypassRegistry, url: string | URL, surface: ProxylineSurface, ): boolean { if (bypasses.has(url, surface)) { return true; } if (bypassPolicy === undefined) { return false; } return bypassPolicy({ surface, url: formatUrl(url) }); } type DynamicBypassRegistry = { add: (registration: ProxylineBypassRegistration) => () => void; has: (url: string | URL, surface: ProxylineSurface) => boolean; runScoped: (registration: ProxylineBypassRegistration, run: () => T) => T; }; function bypassKey(url: string | URL, surface: ProxylineSurface | undefined): string { return `${surface ?? "*"}\n${formatUrl(url)}`; } function createDynamicBypassRegistry(): DynamicBypassRegistry { const counts = new Map(); const scopedBypasses = new AsyncLocalStorage>(); const hasScopedBypass = (url: string | URL, surface: ProxylineSurface): boolean => { const scoped = scopedBypasses.getStore(); return scoped !== undefined && (scoped.has(bypassKey(url, surface)) || scoped.has(bypassKey(url, undefined))); }; return { add: (registration) => { const key = bypassKey(registration.url, registration.surface); counts.set(key, (counts.get(key) ?? 0) + 1); let stopped = false; return () => { if (stopped) { return; } stopped = true; const next = (counts.get(key) ?? 1) - 1; if (next <= 0) { counts.delete(key); } else { counts.set(key, next); } }; }, has: (url, surface) => hasScopedBypass(url, surface) || (counts.get(bypassKey(url, surface)) ?? 0) > 0 || (counts.get(bypassKey(url, undefined)) ?? 0) > 0, runScoped: (registration, run) => { const inherited = scopedBypasses.getStore(); const scoped = new Set(inherited); scoped.add(bypassKey(registration.url, registration.surface)); return scopedBypasses.run(scoped, run); }, }; } function proxyEnvSnapshotKey(env: ProxyEnvSnapshot | undefined): string { return JSON.stringify(env ?? EMPTY_PROXY_ENV); } function createManagedProxyResolver( proxyUrl: URL, bypassPolicy: ProxylineBypassPolicy | undefined, bypasses: DynamicBypassRegistry, ): ProxyResolver { const redactedProxyUrl = redactProxyUrl(proxyUrl); return { active: true, describeProxy: () => redactedProxyUrl, explain: (url, surface) => { const formattedUrl = formatUrl(url); if (!isProxyableUrlProtocol(new URL(url).protocol)) { return { kind: "direct", reason: "managed-proxy-unsupported-url-scheme", surface, url: formattedUrl, }; } if (shouldBypassManagedProxy(bypassPolicy, bypasses, url, surface)) { return { kind: "direct", reason: "managed-proxy-bypass-policy", surface, url: formattedUrl, }; } return { kind: "proxied", reason: "managed-proxy-active", surface, url: formattedUrl, proxyUrl: redactedProxyUrl, }; }, getProxyForUrl: (url, surface = "unknown") => { const protocol = new URL(url).protocol; return isProxyableUrlProtocol(protocol) && !shouldBypassManagedProxy(bypassPolicy, bypasses, url, surface) ? proxyUrl.href : ""; }, }; } type UndiciDispatcherOptions = Readonly<{ proxyCa: string | undefined; undici: ProxylineUndiciOptions | undefined; }>; type UndiciProxyAgentOptions = Exclude[0], string | URL>; type UndiciProxyClientFactory = NonNullable< UndiciProxyAgentOptions["clientFactory"] >; type UnknownFunction = (...args: unknown[]) => unknown; function finiteNonNegativeInteger(value: number | undefined): number | undefined { return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : undefined; } function finitePositiveInteger(value: number | undefined): number | undefined { return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : undefined; } function resolveUndiciBaseOptions( options: ProxylineUndiciOptions | undefined, ): Record { const bodyTimeout = finiteNonNegativeInteger(options?.bodyTimeout); const headersTimeout = finiteNonNegativeInteger(options?.headersTimeout); return { ...(options?.allowH2 !== undefined ? { allowH2: options.allowH2 } : {}), ...(bodyTimeout !== undefined ? { bodyTimeout } : {}), ...(headersTimeout !== undefined ? { headersTimeout } : {}), ...(options?.connect !== undefined ? { connect: { ...(options.connect.autoSelectFamily !== undefined ? { autoSelectFamily: options.connect.autoSelectFamily } : {}), ...(finitePositiveInteger(options.connect.autoSelectFamilyAttemptTimeout) !== undefined ? { autoSelectFamilyAttemptTimeout: finitePositiveInteger( options.connect.autoSelectFamilyAttemptTimeout, ), } : {}), }, } : {}), }; } function createUndiciAgent(options: ProxylineUndiciOptions | undefined): UndiciAgent { return new UndiciAgent(resolveUndiciBaseOptions(options)); } function isObjectRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function stripIpServernameFromConnectOptions(options: unknown): unknown { if (!isObjectRecord(options) || typeof options.servername !== "string") { return options; } const servername = options.servername.replace(/^\[|\]$/g, ""); if (net.isIP(servername) === 0) { return options; } const next = { ...options }; delete next.servername; return next; } function stripIpServernameFromConnect(connect: unknown): unknown { if (typeof connect !== "function") { return connect; } return (options: unknown, callback: unknown): unknown => (connect as UnknownFunction)(stripIpServernameFromConnectOptions(options), callback); } const checkProxyServerIdentity: typeof tls.checkServerIdentity = (hostname, certificate) => { const normalizedHostname = hostname.replace(/^\[|\]$/g, ""); if (net.isIP(normalizedHostname) === 6) { try { // Compare binary addresses so compressed and expanded IPv6 SAN forms remain equivalent. if (new X509Certificate(certificate.raw).checkIP(normalizedHostname) !== undefined) { return undefined; } } catch { // Let Node return its canonical certificate validation error below. } } return tls.checkServerIdentity(normalizedHostname, certificate); }; function createProxyClientFactory(): UndiciProxyClientFactory { return (origin: URL, options: object): Dispatcher => { const clientOptions = isObjectRecord(options) ? { ...options, connect: stripIpServernameFromConnect(options.connect) } : options; return new UndiciPool( origin, clientOptions as ConstructorParameters[1], ); }; } function createUndiciProxyAgent( proxyUrl: string, options: UndiciDispatcherOptions, ): UndiciProxyAgent { const proxyHostname = new URL(proxyUrl).hostname.replace(/^\[|\]$/g, ""); const proxyTls = options.proxyCa !== undefined || net.isIP(proxyHostname) === 6 ? { ...(options.proxyCa !== undefined ? { ca: options.proxyCa } : {}), ...(net.isIP(proxyHostname) === 6 ? { checkServerIdentity: checkProxyServerIdentity } : {}), } : undefined; const dispatcherFactory = createProxyClientFactory(); return new UndiciProxyAgent({ ...resolveUndiciBaseOptions(options.undici), uri: proxyUrl, clientFactory: dispatcherFactory, factory: dispatcherFactory, ...(proxyTls !== undefined ? { proxyTls } : {}), } as ConstructorParameters[0]); } function normalizeUndiciDispatchOptions( options: Dispatcher.DispatchOptions, ): Dispatcher.DispatchOptions { if (options.origin === undefined || !/^https?:\/\//i.test(options.path)) { return options; } const pathUrl = new URL(options.path); return { ...options, path: `${pathUrl.pathname}${pathUrl.search}`, }; } function createUndiciProxyDispatcher( options: | { mode: "managed"; resolver: ProxyResolver } | { mode: "ambient"; env: ProxyEnvSnapshot; active: boolean }, dispatcherOptions: UndiciDispatcherOptions, ): Dispatcher { if (options.mode === "ambient") { if (!options.active) { return createUndiciAgent(dispatcherOptions.undici); } return new AmbientUndiciDispatcher(options.env, dispatcherOptions); } return new ManagedUndiciDispatcher(options.resolver, dispatcherOptions); } function reportClosedDispatchError( handler: Dispatcher.DispatchHandler, error: Error, ): boolean { const compatibleHandler = handler as CompatibleDispatchHandler; if (compatibleHandler.onResponseError !== undefined) { compatibleHandler.onResponseError(null, error); return false; } if (compatibleHandler.onError !== undefined) { compatibleHandler.onError(error); return false; } throw error; } class ManagedUndiciDispatcher extends Dispatcher { public readonly [PROXYLINE_DISPATCHER_BRAND] = true; readonly #directDispatcher: UndiciAgent; readonly #dispatcherOptions: UndiciDispatcherOptions; readonly #proxyDispatchers = new Map(); readonly #resolver: ProxyResolver; #closedError: Error | undefined; public constructor(resolver: ProxyResolver, dispatcherOptions: UndiciDispatcherOptions) { super(); this.#resolver = resolver; this.#dispatcherOptions = dispatcherOptions; this.#directDispatcher = createUndiciAgent(dispatcherOptions.undici); } public override dispatch( options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler, ): boolean { if (this.#closedError !== undefined) { return reportClosedDispatchError(handler, this.#closedError); } const normalizedOptions = normalizeUndiciDispatchOptions(options); const url = resolveUndiciDispatchUrl(normalizedOptions); const proxyUrl = url === undefined ? "" : this.#resolver.getProxyForUrl(url, "undici"); const dispatcher = proxyUrl === "" ? this.#directDispatcher : this.#proxyDispatcher(proxyUrl); return dispatcher.dispatch(normalizedOptions, handler); } public override close(callback: () => void): void; public override close(): Promise; public override close(callback?: () => void): Promise | void { const closing = this.#closeAll(); if (callback === undefined) { return closing; } closing.then(callback, callback); } public override destroy(): Promise; public override destroy(error: Error | null): Promise; public override destroy(callback: () => void): void; public override destroy(error: Error | null, callback: () => void): void; public override destroy( errorOrCallback?: Error | null | (() => void), callback?: () => void, ): Promise | void { const error = typeof errorOrCallback === "function" ? null : errorOrCallback ?? null; const destroyCallback = typeof errorOrCallback === "function" ? errorOrCallback : callback; const destroying = this.#destroyAll(error); if (destroyCallback === undefined) { return destroying; } destroying.then(destroyCallback, destroyCallback); } #proxyDispatcher(proxyUrl: string): UndiciProxyAgent { const existing = this.#proxyDispatchers.get(proxyUrl); if (existing !== undefined) { return existing; } const dispatcher = createUndiciProxyAgent(proxyUrl, this.#dispatcherOptions); this.#proxyDispatchers.set(proxyUrl, dispatcher); return dispatcher; } async #closeAll(): Promise { this.#closedError ??= new undiciErrors.ClientClosedError(); const proxyDispatchers = [...this.#proxyDispatchers.values()]; this.#proxyDispatchers.clear(); await Promise.all([ this.#directDispatcher.close(), ...proxyDispatchers.map((dispatcher) => dispatcher.close()), ]); } async #destroyAll(error: Error | null): Promise { this.#closedError ??= error ?? new undiciErrors.ClientDestroyedError(); const proxyDispatchers = [...this.#proxyDispatchers.values()]; this.#proxyDispatchers.clear(); await Promise.all([ this.#directDispatcher.destroy(error), ...proxyDispatchers.map((dispatcher) => dispatcher.destroy(error)), ]); } } class AmbientUndiciDispatcher extends Dispatcher { public readonly [PROXYLINE_DISPATCHER_BRAND] = true; readonly #directDispatcher: UndiciAgent; readonly #dispatcherOptions: UndiciDispatcherOptions; readonly #env: ProxyEnvSnapshot; readonly #proxyDispatchers = new Map(); #closedError: Error | undefined; public constructor(env: ProxyEnvSnapshot, dispatcherOptions: UndiciDispatcherOptions) { super(); this.#env = env; this.#dispatcherOptions = dispatcherOptions; this.#directDispatcher = createUndiciAgent(dispatcherOptions.undici); } public override dispatch( options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler, ): boolean { if (this.#closedError !== undefined) { return reportClosedDispatchError(handler, this.#closedError); } const normalizedOptions = normalizeUndiciDispatchOptions(options); const url = resolveUndiciDispatchUrl(normalizedOptions); const proxyUrl = url === undefined ? undefined : resolveAmbientProxyForUrl(url, this.#env); const dispatcher = proxyUrl === undefined ? this.#directDispatcher : this.#proxyDispatcher(proxyUrl); return dispatcher.dispatch(normalizedOptions, handler); } public override close(callback: () => void): void; public override close(): Promise; public override close(callback?: () => void): Promise | void { const closing = this.#closeAll(); if (callback === undefined) { return closing; } closing.then(callback, callback); } public override destroy(): Promise; public override destroy(error: Error | null): Promise; public override destroy(callback: () => void): void; public override destroy(error: Error | null, callback: () => void): void; public override destroy( errorOrCallback?: Error | null | (() => void), callback?: () => void, ): Promise | void { const error = typeof errorOrCallback === "function" ? null : errorOrCallback ?? null; const destroyCallback = typeof errorOrCallback === "function" ? errorOrCallback : callback; const destroying = this.#destroyAll(error); if (destroyCallback === undefined) { return destroying; } destroying.then(destroyCallback, destroyCallback); } #proxyDispatcher(proxyUrl: string): UndiciProxyAgent { const existing = this.#proxyDispatchers.get(proxyUrl); if (existing !== undefined) { return existing; } const dispatcher = createUndiciProxyAgent(proxyUrl, this.#dispatcherOptions); this.#proxyDispatchers.set(proxyUrl, dispatcher); return dispatcher; } async #closeAll(): Promise { this.#closedError ??= new undiciErrors.ClientClosedError(); const proxyDispatchers = [...this.#proxyDispatchers.values()]; this.#proxyDispatchers.clear(); await Promise.all([ this.#directDispatcher.close(), ...proxyDispatchers.map((dispatcher) => dispatcher.close()), ]); } async #destroyAll(error: Error | null): Promise { this.#closedError ??= error ?? new undiciErrors.ClientDestroyedError(); const proxyDispatchers = [...this.#proxyDispatchers.values()]; this.#proxyDispatchers.clear(); await Promise.all([ this.#directDispatcher.destroy(error), ...proxyDispatchers.map((dispatcher) => dispatcher.destroy(error)), ]); } } function resolveUndiciDispatchUrl(options: Dispatcher.DispatchOptions): string | undefined { if (options.origin !== undefined) { const origin = options.origin.toString().replace(/\/$/, ""); const path = options.path.startsWith("/") ? options.path : `/${options.path}`; return new URL(`${origin}${path}`).href; } try { return new URL(options.path).href; } catch { return undefined; } } function restoreNodeHttpSnapshot(snapshot: NodeHttpStackSnapshot): void { http.request = snapshot.httpRequest; http.get = snapshot.httpGet; http.globalAgent = snapshot.httpGlobalAgent; https.request = snapshot.httpsRequest; https.get = snapshot.httpsGet; https.globalAgent = snapshot.httpsGlobalAgent; } function installRuntime( resolver: ProxyResolver, dispatcherOptions: | { mode: "managed"; resolver: ProxyResolver } | { mode: "ambient"; env: ProxyEnvSnapshot; active: boolean }, proxyCa: string | undefined, options: { ambientEnv: ProxyEnvSnapshot | undefined; bypassPolicy: ProxylineBypassPolicy | undefined; proxyUrl: URL | undefined; undici: ProxylineUndiciOptions | undefined; }, ): RuntimeInstall { if (activeRuntime !== undefined) { throw new ProxylineError("RUNTIME_ALREADY_ACTIVE", "Proxyline already has an active runtime."); } const snapshot: NodeHttpStackSnapshot = { httpRequest: http.request, httpGet: http.get, httpGlobalAgent: http.globalAgent, httpsRequest: https.request, httpsGet: https.get, httpsGlobalAgent: https.globalAgent, }; const nodeHttpAgent = createNodeProxyAgent(resolver, proxyCa, "http"); const nodeHttpsAgent = createNodeProxyAgent(resolver, proxyCa, "https"); const originalDispatcher = getGlobalDispatcher(); const originalFetch = globalThis.fetch; const originalFormData = globalThis.FormData; const originalHeaders = globalThis.Headers; const originalRequest = globalThis.Request; const originalResponse = globalThis.Response; const installedDispatcher = createUndiciProxyDispatcher(dispatcherOptions, { proxyCa, undici: options.undici, }); const runtime: RuntimeInstall = { ambientEnv: options.ambientEnv, bypassPolicy: options.bypassPolicy, installedDispatcher, mode: dispatcherOptions.mode, nodeHttpAgent, nodeHttpsAgent, originalDispatcher, originalFetch, originalFormData, originalHeaders, originalRequest, originalResponse, proxyCa, proxyUrl: options.proxyUrl?.href, snapshot, undiciOptions: options.undici, }; activeRuntime = runtime; try { http.globalAgent = nodeHttpAgent; https.globalAgent = nodeHttpsAgent as unknown as typeof https.globalAgent; http.request = bindNodeHttpMethod(snapshot.httpRequest, () => createNodeProxyAgent(resolver, proxyCa, "http"), ); http.get = bindNodeHttpMethod(snapshot.httpGet, () => createNodeProxyAgent(resolver, proxyCa, "http"), ); https.request = bindNodeHttpMethod(snapshot.httpsRequest, () => createNodeProxyAgent(resolver, proxyCa, "https"), ); https.get = bindNodeHttpMethod(snapshot.httpsGet, () => createNodeProxyAgent(resolver, proxyCa, "https"), ); setGlobalDispatcher(installedDispatcher); globalThis.fetch = proxylineFetch; globalThis.FormData = proxylineFormData; globalThis.Headers = proxylineHeaders; globalThis.Request = proxylineRequest; globalThis.Response = proxylineResponse; } catch (error) { restoreNodeHttpSnapshot(snapshot); setGlobalDispatcher(originalDispatcher); globalThis.fetch = originalFetch; globalThis.FormData = originalFormData; globalThis.Headers = originalHeaders; globalThis.Request = originalRequest; globalThis.Response = originalResponse; activeRuntime = undefined; void installedDispatcher.destroy(); nodeHttpAgent.destroy(); nodeHttpsAgent.destroy(); throw error; } return runtime; } function stopRuntime(runtime: RuntimeInstall): void { if (activeRuntime !== runtime) { return; } restoreNodeHttpSnapshot(runtime.snapshot); setGlobalDispatcher(runtime.originalDispatcher); globalThis.fetch = runtime.originalFetch; globalThis.FormData = runtime.originalFormData; globalThis.Headers = runtime.originalHeaders; globalThis.Request = runtime.originalRequest; globalThis.Response = runtime.originalResponse; void runtime.installedDispatcher.destroy(); runtime.nodeHttpAgent.destroy(); runtime.nodeHttpsAgent.destroy(); activeRuntime = undefined; activeHandle = undefined; } export function installProxyline(options: ProxylineOptions): ProxylineHandle { const proxyUrl = options.mode === "managed" ? normalizeProxyUrl(options.proxyUrl) : undefined; const ambientEnv = proxyUrl === undefined ? readProxyEnv() : undefined; if (options.mode === "managed" && proxyUrl === undefined) { throw new ProxylineError( "MANAGED_PROXY_URL_REQUIRED", "Proxyline managed mode requires an explicit proxyUrl.", ); } const activePolicy = options.ifActive ?? "error"; if (activeRuntime !== undefined) { if (activePolicy === "replace") { activeHandle?.stop(); } else if ( activePolicy === "reuse-compatible" && activeHandle !== undefined && activeRuntime.mode === options.mode && activeRuntime.proxyUrl === proxyUrl?.href && proxyEnvSnapshotKey(activeRuntime.ambientEnv) === proxyEnvSnapshotKey(ambientEnv) && activeRuntime.proxyCa === resolveProxyTlsCa(options.proxyTls) && activeRuntime.bypassPolicy === options.bypassPolicy && JSON.stringify(activeRuntime.undiciOptions ?? {}) === JSON.stringify(options.undici ?? {}) ) { return activeHandle; } else { throw new ProxylineError( "RUNTIME_ALREADY_ACTIVE", "Proxyline already has an active runtime.", ); } } let stopped = false; const proxyCa = resolveProxyTlsCa(options.proxyTls); const dynamicBypasses = createDynamicBypassRegistry(); const resolver = proxyUrl !== undefined ? createManagedProxyResolver(proxyUrl, options.bypassPolicy, dynamicBypasses) : createAmbientProxyResolver(ambientEnv ?? EMPTY_PROXY_ENV); const redactedProxyUrl = resolver.describeProxy(); const hasActiveProxy = resolver.active; const runtime = hasActiveProxy ? installRuntime( resolver, proxyUrl !== undefined ? { mode: "managed", resolver } : { mode: "ambient", env: ambientEnv ?? EMPTY_PROXY_ENV, active: hasActiveProxy }, proxyCa, { ambientEnv, bypassPolicy: options.bypassPolicy, proxyUrl, undici: options.undici, }, ) : undefined; emit(options.onEvent, { type: "runtime.installed", mode: options.mode, active: hasActiveProxy, ...(redactedProxyUrl ? { proxyUrl: redactedProxyUrl } : {}), }); const handle: ProxylineHandle = { mode: options.mode, active: hasActiveProxy, ...(redactedProxyUrl ? { proxyUrl: redactedProxyUrl } : {}), createNodeAgent: () => { if (!hasActiveProxy || stopped) { return createDirectNodeAgent(); } return createNodeProxyAgent(resolver, proxyCa); }, createUndiciDispatcher: () => stopped ? createUndiciAgent(options.undici) : createUndiciProxyDispatcher( proxyUrl !== undefined ? { mode: "managed", resolver } : { mode: "ambient", env: ambientEnv ?? EMPTY_PROXY_ENV, active: hasActiveProxy }, { proxyCa, undici: options.undici }, ), createWebSocketAgent: () => { if (!hasActiveProxy || stopped) { return createDirectNodeAgent(); } return createNodeProxyAgent(resolver, proxyCa); }, explain: (url, explainOptions) => { const decision = stopped ? { kind: "direct" as const, reason: "runtime-stopped", surface: explainOptions?.surface ?? "unknown", url: formatUrl(url), } : resolver.explain(url, explainOptions?.surface ?? "unknown"); emit(options.onEvent, { type: "decision", decision }); return decision; }, registerBypass: (registration) => { if (stopped || proxyUrl === undefined) { return () => {}; } return dynamicBypasses.add(registration); }, stop: () => { if (stopped) { return; } stopped = true; if (runtime !== undefined) { stopRuntime(runtime); } emit(options.onEvent, { type: "runtime.stopped", mode: options.mode }); }, withBypass: (registration, run) => { if (stopped || proxyUrl === undefined) { return run(); } return dynamicBypasses.runScoped(registration, run); }, }; activeHandle = hasActiveProxy ? handle : activeHandle; return handle; } export const installGlobalProxy = installProxyline;