@openclaw/proxyline 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/runtime.ts ADDED
@@ -0,0 +1,906 @@
1
+ import http from "node:http";
2
+ import https from "node:https";
3
+ import { AsyncLocalStorage } from "node:async_hooks";
4
+ import {
5
+ Agent as UndiciAgent,
6
+ Dispatcher,
7
+ FormData as UndiciFormData,
8
+ Headers as UndiciHeaders,
9
+ Request as UndiciRequest,
10
+ Response as UndiciResponse,
11
+ errors as undiciErrors,
12
+ fetch as undiciFetch,
13
+ getGlobalDispatcher,
14
+ ProxyAgent as UndiciProxyAgent,
15
+ setGlobalDispatcher,
16
+ } from "undici";
17
+ import {
18
+ createAmbientProxyResolver,
19
+ EMPTY_PROXY_ENV,
20
+ resolveAmbientProxyForUrl,
21
+ readProxyEnv,
22
+ type ProxyEnvSnapshot,
23
+ } from "./env.js";
24
+ import {
25
+ bindNodeHttpMethod,
26
+ createDirectNodeAgent,
27
+ createNodeProxyAgent,
28
+ type NodeHttpStackSnapshot,
29
+ type ProxylineNodeProxyAgent,
30
+ } from "./node-http.js";
31
+ import {
32
+ formatUrl,
33
+ ProxylineError,
34
+ redactProxyUrl,
35
+ resolveProxyTlsCa,
36
+ } from "./shared.js";
37
+ import type {
38
+ ProxylineBypassRegistration,
39
+ ProxylineBypassPolicy,
40
+ ProxylineEvent,
41
+ ProxylineHandle,
42
+ ProxylineOptions,
43
+ ProxyResolver,
44
+ ProxylineSurface,
45
+ ProxylineUndiciOptions,
46
+ } from "./types.js";
47
+ import { PROXYLINE_DISPATCHER_BRAND } from "./dispatcher-brand.js";
48
+
49
+ type RuntimeInstall = {
50
+ ambientEnv: ProxyEnvSnapshot | undefined;
51
+ bypassPolicy: ProxylineBypassPolicy | undefined;
52
+ installedDispatcher: Dispatcher;
53
+ mode: ProxylineOptions["mode"];
54
+ nodeHttpAgent: ProxylineNodeProxyAgent;
55
+ nodeHttpsAgent: ProxylineNodeProxyAgent;
56
+ originalDispatcher: Dispatcher;
57
+ originalFetch: typeof globalThis.fetch;
58
+ originalFormData: typeof globalThis.FormData;
59
+ originalHeaders: typeof globalThis.Headers;
60
+ originalRequest: typeof globalThis.Request;
61
+ originalResponse: typeof globalThis.Response;
62
+ proxyCa: string | undefined;
63
+ proxyUrl: string | undefined;
64
+ snapshot: NodeHttpStackSnapshot;
65
+ undiciOptions: ProxylineUndiciOptions | undefined;
66
+ };
67
+
68
+ let activeRuntime: RuntimeInstall | undefined;
69
+ let activeHandle: ProxylineHandle | undefined;
70
+
71
+ type ProxylineDispatcher = Dispatcher & {
72
+ [PROXYLINE_DISPATCHER_BRAND]?: true;
73
+ };
74
+
75
+ // Node's global fetch types come from bundled undici-types, while the runtime
76
+ // implementation intentionally delegates to this package's undici dependency.
77
+ const proxylineHeaders = UndiciHeaders as unknown as typeof globalThis.Headers;
78
+ const proxylineRequest = UndiciRequest as unknown as typeof globalThis.Request;
79
+ const proxylineResponse = UndiciResponse as unknown as typeof globalThis.Response;
80
+ const proxylineFormData = UndiciFormData as unknown as typeof globalThis.FormData;
81
+
82
+ type ProxylineRequestInit = {
83
+ body?: unknown;
84
+ cache?: unknown;
85
+ credentials?: unknown;
86
+ dispatcher?: unknown;
87
+ duplex?: "half";
88
+ headers?: unknown;
89
+ integrity?: unknown;
90
+ keepalive?: unknown;
91
+ method?: string;
92
+ mode?: unknown;
93
+ redirect?: unknown;
94
+ referrer?: unknown;
95
+ referrerPolicy?: unknown;
96
+ signal?: unknown;
97
+ };
98
+
99
+ type FetchRequestLike = Readonly<{
100
+ arrayBuffer: () => Promise<ArrayBuffer>;
101
+ body: ReadableStream<Uint8Array> | null;
102
+ cache?: unknown;
103
+ credentials?: unknown;
104
+ headers: InstanceType<typeof globalThis.Headers>;
105
+ integrity?: unknown;
106
+ keepalive?: unknown;
107
+ method: string;
108
+ mode?: unknown;
109
+ redirect?: unknown;
110
+ referrer?: unknown;
111
+ referrerPolicy?: unknown;
112
+ signal?: unknown;
113
+ url: string;
114
+ }>;
115
+
116
+ function getRequestDispatcher(request: FetchRequestLike): unknown {
117
+ for (const symbol of Object.getOwnPropertySymbols(request)) {
118
+ if (symbol.description !== "dispatcher") {
119
+ continue;
120
+ }
121
+ return Reflect.get(request, symbol);
122
+ }
123
+ return undefined;
124
+ }
125
+
126
+ function isFetchRequestLike(value: unknown): value is FetchRequestLike {
127
+ if (typeof value !== "object" || value === null) {
128
+ return false;
129
+ }
130
+ const record = value as Readonly<Record<string, unknown>>;
131
+ return (
132
+ typeof record.url === "string" &&
133
+ typeof record.method === "string" &&
134
+ typeof record.arrayBuffer === "function" &&
135
+ record.headers !== undefined
136
+ );
137
+ }
138
+
139
+ async function createProxylineRequestFromRequestLike(
140
+ request: FetchRequestLike,
141
+ options: { includeBody: boolean; preserveDispatcher: boolean },
142
+ ): Promise<globalThis.Request> {
143
+ const init: ProxylineRequestInit = {
144
+ headers: request.headers,
145
+ method: request.method,
146
+ };
147
+ if (request.cache !== undefined) {
148
+ init.cache = request.cache;
149
+ }
150
+ if (request.credentials !== undefined) {
151
+ init.credentials = request.credentials;
152
+ }
153
+ if (request.integrity !== undefined) {
154
+ init.integrity = request.integrity;
155
+ }
156
+ if (request.keepalive !== undefined) {
157
+ init.keepalive = request.keepalive;
158
+ }
159
+ if (request.mode !== undefined) {
160
+ init.mode = request.mode;
161
+ }
162
+ if (request.redirect !== undefined) {
163
+ init.redirect = request.redirect;
164
+ }
165
+ if (request.referrer !== undefined) {
166
+ init.referrer = request.referrer;
167
+ }
168
+ if (request.referrerPolicy !== undefined) {
169
+ init.referrerPolicy = request.referrerPolicy;
170
+ }
171
+ if (options.preserveDispatcher) {
172
+ const dispatcher = getRequestDispatcher(request);
173
+ if (dispatcher !== undefined) {
174
+ Reflect.set(init, "dispatcher", dispatcher);
175
+ }
176
+ }
177
+ if (request.signal !== undefined) {
178
+ init.signal = request.signal;
179
+ }
180
+ if (
181
+ options.includeBody &&
182
+ request.body !== null &&
183
+ request.method !== "GET" &&
184
+ request.method !== "HEAD"
185
+ ) {
186
+ init.body = request.body;
187
+ init.duplex = "half";
188
+ }
189
+ const requestUnknown: unknown = Reflect.construct(proxylineRequest, [request.url, init]);
190
+ if (!(requestUnknown instanceof proxylineRequest)) {
191
+ throw new TypeError("Proxyline failed to normalize a fetch Request.");
192
+ }
193
+ return requestUnknown;
194
+ }
195
+
196
+ function requestInitOverridesBody(init: Parameters<typeof globalThis.fetch>[1]): boolean {
197
+ if (typeof init !== "object" || init === null) {
198
+ return false;
199
+ }
200
+ return "body" in init;
201
+ }
202
+
203
+ async function normalizeFetchInput(
204
+ input: Parameters<typeof globalThis.fetch>[0],
205
+ init: Parameters<typeof globalThis.fetch>[1],
206
+ options: { preserveDispatcher: boolean },
207
+ ): Promise<Parameters<typeof globalThis.fetch>[0]> {
208
+ if ((input instanceof proxylineRequest && options.preserveDispatcher) || !isFetchRequestLike(input)) {
209
+ return input;
210
+ }
211
+ return await createProxylineRequestFromRequestLike(input, {
212
+ includeBody: !requestInitOverridesBody(init),
213
+ preserveDispatcher: options.preserveDispatcher,
214
+ });
215
+ }
216
+
217
+ function stripFetchDispatcher(
218
+ init: Parameters<typeof globalThis.fetch>[1],
219
+ ): Parameters<typeof globalThis.fetch>[1] {
220
+ if (typeof init !== "object" || init === null) {
221
+ return init;
222
+ }
223
+ const sanitized = Object.create(init);
224
+ Reflect.defineProperty(sanitized, "dispatcher", {
225
+ configurable: true,
226
+ enumerable: true,
227
+ value: undefined,
228
+ writable: true,
229
+ });
230
+ return sanitized;
231
+ }
232
+
233
+ const proxylineFetch: typeof globalThis.fetch = async (input, init) => {
234
+ const managedMode = activeRuntime?.mode === "managed";
235
+ const normalizedInput = await normalizeFetchInput(input, init, {
236
+ preserveDispatcher: !managedMode,
237
+ });
238
+ const normalizedInit = managedMode ? stripFetchDispatcher(init) : init;
239
+ const response: unknown = await Reflect.apply(
240
+ undiciFetch,
241
+ undefined,
242
+ normalizedInit === undefined ? [normalizedInput] : [normalizedInput, normalizedInit],
243
+ );
244
+ if (!(response instanceof proxylineResponse)) {
245
+ throw new TypeError("Proxyline fetch returned a non-Response value.");
246
+ }
247
+ return response;
248
+ };
249
+
250
+ function normalizeProxyUrl(value: string | URL | undefined): URL | undefined {
251
+ if (value === undefined) {
252
+ return undefined;
253
+ }
254
+ const url = value instanceof URL ? new URL(value.href) : new URL(value);
255
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
256
+ throw new ProxylineError(
257
+ "UNSUPPORTED_PROXY_PROTOCOL",
258
+ `Proxyline only supports http:// and https:// proxy endpoints in this slice: ${url.protocol}`,
259
+ );
260
+ }
261
+ return url;
262
+ }
263
+
264
+ function emit(onEvent: ProxylineOptions["onEvent"], event: ProxylineEvent): void {
265
+ onEvent?.(event);
266
+ }
267
+
268
+ function isProxyableUrlProtocol(protocol: string): boolean {
269
+ return protocol === "http:" ||
270
+ protocol === "https:" ||
271
+ protocol === "ws:" ||
272
+ protocol === "wss:";
273
+ }
274
+
275
+ function shouldBypassManagedProxy(
276
+ bypassPolicy: ProxylineBypassPolicy | undefined,
277
+ bypasses: DynamicBypassRegistry,
278
+ url: string | URL,
279
+ surface: ProxylineSurface,
280
+ ): boolean {
281
+ if (bypasses.has(url, surface)) {
282
+ return true;
283
+ }
284
+ if (bypassPolicy === undefined) {
285
+ return false;
286
+ }
287
+ return bypassPolicy({ surface, url: formatUrl(url) });
288
+ }
289
+
290
+ type DynamicBypassRegistry = {
291
+ add: (registration: ProxylineBypassRegistration) => () => void;
292
+ has: (url: string | URL, surface: ProxylineSurface) => boolean;
293
+ runScoped: <T>(registration: ProxylineBypassRegistration, run: () => T) => T;
294
+ };
295
+
296
+ function bypassKey(url: string | URL, surface: ProxylineSurface | undefined): string {
297
+ return `${surface ?? "*"}\n${formatUrl(url)}`;
298
+ }
299
+
300
+ function createDynamicBypassRegistry(): DynamicBypassRegistry {
301
+ const counts = new Map<string, number>();
302
+ const scopedBypasses = new AsyncLocalStorage<ReadonlySet<string>>();
303
+ const hasScopedBypass = (url: string | URL, surface: ProxylineSurface): boolean => {
304
+ const scoped = scopedBypasses.getStore();
305
+ return scoped !== undefined &&
306
+ (scoped.has(bypassKey(url, surface)) || scoped.has(bypassKey(url, undefined)));
307
+ };
308
+ return {
309
+ add: (registration) => {
310
+ const key = bypassKey(registration.url, registration.surface);
311
+ counts.set(key, (counts.get(key) ?? 0) + 1);
312
+ let stopped = false;
313
+ return () => {
314
+ if (stopped) {
315
+ return;
316
+ }
317
+ stopped = true;
318
+ const next = (counts.get(key) ?? 1) - 1;
319
+ if (next <= 0) {
320
+ counts.delete(key);
321
+ } else {
322
+ counts.set(key, next);
323
+ }
324
+ };
325
+ },
326
+ has: (url, surface) =>
327
+ hasScopedBypass(url, surface) ||
328
+ (counts.get(bypassKey(url, surface)) ?? 0) > 0 ||
329
+ (counts.get(bypassKey(url, undefined)) ?? 0) > 0,
330
+ runScoped: (registration, run) => {
331
+ const inherited = scopedBypasses.getStore();
332
+ const scoped = new Set(inherited);
333
+ scoped.add(bypassKey(registration.url, registration.surface));
334
+ return scopedBypasses.run(scoped, run);
335
+ },
336
+ };
337
+ }
338
+
339
+ function proxyEnvSnapshotKey(env: ProxyEnvSnapshot | undefined): string {
340
+ return JSON.stringify(env ?? EMPTY_PROXY_ENV);
341
+ }
342
+
343
+ function createManagedProxyResolver(
344
+ proxyUrl: URL,
345
+ bypassPolicy: ProxylineBypassPolicy | undefined,
346
+ bypasses: DynamicBypassRegistry,
347
+ ): ProxyResolver {
348
+ const redactedProxyUrl = redactProxyUrl(proxyUrl);
349
+ return {
350
+ active: true,
351
+ describeProxy: () => redactedProxyUrl,
352
+ explain: (url, surface) => {
353
+ const formattedUrl = formatUrl(url);
354
+ if (!isProxyableUrlProtocol(new URL(url).protocol)) {
355
+ return {
356
+ kind: "direct",
357
+ reason: "managed-proxy-unsupported-url-scheme",
358
+ surface,
359
+ url: formattedUrl,
360
+ };
361
+ }
362
+ if (shouldBypassManagedProxy(bypassPolicy, bypasses, url, surface)) {
363
+ return {
364
+ kind: "direct",
365
+ reason: "managed-proxy-bypass-policy",
366
+ surface,
367
+ url: formattedUrl,
368
+ };
369
+ }
370
+ return {
371
+ kind: "proxied",
372
+ reason: "managed-proxy-active",
373
+ surface,
374
+ url: formattedUrl,
375
+ proxyUrl: redactedProxyUrl,
376
+ };
377
+ },
378
+ getProxyForUrl: (url, surface = "unknown") => {
379
+ const protocol = new URL(url).protocol;
380
+ return isProxyableUrlProtocol(protocol) &&
381
+ !shouldBypassManagedProxy(bypassPolicy, bypasses, url, surface)
382
+ ? proxyUrl.href
383
+ : "";
384
+ },
385
+ };
386
+ }
387
+
388
+ type UndiciDispatcherOptions = Readonly<{
389
+ proxyCa: string | undefined;
390
+ undici: ProxylineUndiciOptions | undefined;
391
+ }>;
392
+
393
+ function finiteNonNegativeInteger(value: number | undefined): number | undefined {
394
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
395
+ ? Math.floor(value)
396
+ : undefined;
397
+ }
398
+
399
+ function finitePositiveInteger(value: number | undefined): number | undefined {
400
+ return typeof value === "number" && Number.isFinite(value) && value > 0
401
+ ? Math.floor(value)
402
+ : undefined;
403
+ }
404
+
405
+ function resolveUndiciBaseOptions(
406
+ options: ProxylineUndiciOptions | undefined,
407
+ ): Record<string, unknown> {
408
+ const bodyTimeout = finiteNonNegativeInteger(options?.bodyTimeout);
409
+ const headersTimeout = finiteNonNegativeInteger(options?.headersTimeout);
410
+ return {
411
+ ...(options?.allowH2 !== undefined ? { allowH2: options.allowH2 } : {}),
412
+ ...(bodyTimeout !== undefined ? { bodyTimeout } : {}),
413
+ ...(headersTimeout !== undefined ? { headersTimeout } : {}),
414
+ ...(options?.connect !== undefined
415
+ ? {
416
+ connect: {
417
+ ...(options.connect.autoSelectFamily !== undefined
418
+ ? { autoSelectFamily: options.connect.autoSelectFamily }
419
+ : {}),
420
+ ...(finitePositiveInteger(options.connect.autoSelectFamilyAttemptTimeout) !== undefined
421
+ ? {
422
+ autoSelectFamilyAttemptTimeout: finitePositiveInteger(
423
+ options.connect.autoSelectFamilyAttemptTimeout,
424
+ ),
425
+ }
426
+ : {}),
427
+ },
428
+ }
429
+ : {}),
430
+ };
431
+ }
432
+
433
+ function createUndiciAgent(options: ProxylineUndiciOptions | undefined): UndiciAgent {
434
+ return new UndiciAgent(resolveUndiciBaseOptions(options));
435
+ }
436
+
437
+ function createUndiciProxyAgent(
438
+ proxyUrl: string,
439
+ options: UndiciDispatcherOptions,
440
+ ): UndiciProxyAgent {
441
+ return new UndiciProxyAgent({
442
+ ...resolveUndiciBaseOptions(options.undici),
443
+ uri: proxyUrl,
444
+ ...(options.proxyCa !== undefined ? { proxyTls: { ca: options.proxyCa } } : {}),
445
+ } as ConstructorParameters<typeof UndiciProxyAgent>[0]);
446
+ }
447
+
448
+ function createUndiciProxyDispatcher(
449
+ options:
450
+ | { mode: "managed"; resolver: ProxyResolver }
451
+ | { mode: "ambient"; env: ProxyEnvSnapshot; active: boolean },
452
+ dispatcherOptions: UndiciDispatcherOptions,
453
+ ): Dispatcher {
454
+ if (options.mode === "ambient") {
455
+ if (!options.active) {
456
+ return createUndiciAgent(dispatcherOptions.undici);
457
+ }
458
+ return new AmbientUndiciDispatcher(options.env, dispatcherOptions);
459
+ }
460
+ return new ManagedUndiciDispatcher(options.resolver, dispatcherOptions);
461
+ }
462
+
463
+ class ManagedUndiciDispatcher extends Dispatcher {
464
+ public readonly [PROXYLINE_DISPATCHER_BRAND] = true;
465
+ readonly #directDispatcher: UndiciAgent;
466
+ readonly #dispatcherOptions: UndiciDispatcherOptions;
467
+ readonly #proxyDispatchers = new Map<string, UndiciProxyAgent>();
468
+ readonly #resolver: ProxyResolver;
469
+ #closedError: Error | undefined;
470
+
471
+ public constructor(resolver: ProxyResolver, dispatcherOptions: UndiciDispatcherOptions) {
472
+ super();
473
+ this.#resolver = resolver;
474
+ this.#dispatcherOptions = dispatcherOptions;
475
+ this.#directDispatcher = createUndiciAgent(dispatcherOptions.undici);
476
+ }
477
+
478
+ public override dispatch(
479
+ options: Dispatcher.DispatchOptions,
480
+ handler: Dispatcher.DispatchHandler,
481
+ ): boolean {
482
+ if (this.#closedError !== undefined) {
483
+ if (handler.onError === undefined) {
484
+ throw this.#closedError;
485
+ }
486
+ handler.onError(this.#closedError);
487
+ return false;
488
+ }
489
+ const url = resolveUndiciDispatchUrl(options);
490
+ const proxyUrl = url === undefined ? "" : this.#resolver.getProxyForUrl(url, "undici");
491
+ const dispatcher = proxyUrl === "" ? this.#directDispatcher : this.#proxyDispatcher(proxyUrl);
492
+ return dispatcher.dispatch(options, handler);
493
+ }
494
+
495
+ public override close(callback: () => void): void;
496
+ public override close(): Promise<void>;
497
+ public override close(callback?: () => void): Promise<void> | void {
498
+ const closing = this.#closeAll();
499
+ if (callback === undefined) {
500
+ return closing;
501
+ }
502
+ closing.then(callback, callback);
503
+ }
504
+
505
+ public override destroy(): Promise<void>;
506
+ public override destroy(error: Error | null): Promise<void>;
507
+ public override destroy(callback: () => void): void;
508
+ public override destroy(error: Error | null, callback: () => void): void;
509
+ public override destroy(
510
+ errorOrCallback?: Error | null | (() => void),
511
+ callback?: () => void,
512
+ ): Promise<void> | void {
513
+ const error = typeof errorOrCallback === "function" ? null : errorOrCallback ?? null;
514
+ const destroyCallback = typeof errorOrCallback === "function" ? errorOrCallback : callback;
515
+ const destroying = this.#destroyAll(error);
516
+ if (destroyCallback === undefined) {
517
+ return destroying;
518
+ }
519
+ destroying.then(destroyCallback, destroyCallback);
520
+ }
521
+
522
+ #proxyDispatcher(proxyUrl: string): UndiciProxyAgent {
523
+ const existing = this.#proxyDispatchers.get(proxyUrl);
524
+ if (existing !== undefined) {
525
+ return existing;
526
+ }
527
+ const dispatcher = createUndiciProxyAgent(proxyUrl, this.#dispatcherOptions);
528
+ this.#proxyDispatchers.set(proxyUrl, dispatcher);
529
+ return dispatcher;
530
+ }
531
+
532
+ async #closeAll(): Promise<void> {
533
+ this.#closedError ??= new undiciErrors.ClientClosedError();
534
+ const proxyDispatchers = [...this.#proxyDispatchers.values()];
535
+ this.#proxyDispatchers.clear();
536
+ await Promise.all([
537
+ this.#directDispatcher.close(),
538
+ ...proxyDispatchers.map((dispatcher) => dispatcher.close()),
539
+ ]);
540
+ }
541
+
542
+ async #destroyAll(error: Error | null): Promise<void> {
543
+ this.#closedError ??= error ?? new undiciErrors.ClientDestroyedError();
544
+ const proxyDispatchers = [...this.#proxyDispatchers.values()];
545
+ this.#proxyDispatchers.clear();
546
+ await Promise.all([
547
+ this.#directDispatcher.destroy(error),
548
+ ...proxyDispatchers.map((dispatcher) => dispatcher.destroy(error)),
549
+ ]);
550
+ }
551
+ }
552
+
553
+ class AmbientUndiciDispatcher extends Dispatcher {
554
+ public readonly [PROXYLINE_DISPATCHER_BRAND] = true;
555
+ readonly #directDispatcher: UndiciAgent;
556
+ readonly #dispatcherOptions: UndiciDispatcherOptions;
557
+ readonly #env: ProxyEnvSnapshot;
558
+ readonly #proxyDispatchers = new Map<string, UndiciProxyAgent>();
559
+ #closedError: Error | undefined;
560
+
561
+ public constructor(env: ProxyEnvSnapshot, dispatcherOptions: UndiciDispatcherOptions) {
562
+ super();
563
+ this.#env = env;
564
+ this.#dispatcherOptions = dispatcherOptions;
565
+ this.#directDispatcher = createUndiciAgent(dispatcherOptions.undici);
566
+ }
567
+
568
+ public override dispatch(
569
+ options: Dispatcher.DispatchOptions,
570
+ handler: Dispatcher.DispatchHandler,
571
+ ): boolean {
572
+ if (this.#closedError !== undefined) {
573
+ if (handler.onError === undefined) {
574
+ throw this.#closedError;
575
+ }
576
+ handler.onError(this.#closedError);
577
+ return false;
578
+ }
579
+ const url = resolveUndiciDispatchUrl(options);
580
+ const proxyUrl = url === undefined ? undefined : resolveAmbientProxyForUrl(url, this.#env);
581
+ const dispatcher = proxyUrl === undefined ? this.#directDispatcher : this.#proxyDispatcher(proxyUrl);
582
+ return dispatcher.dispatch(options, handler);
583
+ }
584
+
585
+ public override close(callback: () => void): void;
586
+ public override close(): Promise<void>;
587
+ public override close(callback?: () => void): Promise<void> | void {
588
+ const closing = this.#closeAll();
589
+ if (callback === undefined) {
590
+ return closing;
591
+ }
592
+ closing.then(callback, callback);
593
+ }
594
+
595
+ public override destroy(): Promise<void>;
596
+ public override destroy(error: Error | null): Promise<void>;
597
+ public override destroy(callback: () => void): void;
598
+ public override destroy(error: Error | null, callback: () => void): void;
599
+ public override destroy(
600
+ errorOrCallback?: Error | null | (() => void),
601
+ callback?: () => void,
602
+ ): Promise<void> | void {
603
+ const error = typeof errorOrCallback === "function" ? null : errorOrCallback ?? null;
604
+ const destroyCallback = typeof errorOrCallback === "function" ? errorOrCallback : callback;
605
+ const destroying = this.#destroyAll(error);
606
+ if (destroyCallback === undefined) {
607
+ return destroying;
608
+ }
609
+ destroying.then(destroyCallback, destroyCallback);
610
+ }
611
+
612
+ #proxyDispatcher(proxyUrl: string): UndiciProxyAgent {
613
+ const existing = this.#proxyDispatchers.get(proxyUrl);
614
+ if (existing !== undefined) {
615
+ return existing;
616
+ }
617
+ const dispatcher = createUndiciProxyAgent(proxyUrl, this.#dispatcherOptions);
618
+ this.#proxyDispatchers.set(proxyUrl, dispatcher);
619
+ return dispatcher;
620
+ }
621
+
622
+ async #closeAll(): Promise<void> {
623
+ this.#closedError ??= new undiciErrors.ClientClosedError();
624
+ const proxyDispatchers = [...this.#proxyDispatchers.values()];
625
+ this.#proxyDispatchers.clear();
626
+ await Promise.all([
627
+ this.#directDispatcher.close(),
628
+ ...proxyDispatchers.map((dispatcher) => dispatcher.close()),
629
+ ]);
630
+ }
631
+
632
+ async #destroyAll(error: Error | null): Promise<void> {
633
+ this.#closedError ??= error ?? new undiciErrors.ClientDestroyedError();
634
+ const proxyDispatchers = [...this.#proxyDispatchers.values()];
635
+ this.#proxyDispatchers.clear();
636
+ await Promise.all([
637
+ this.#directDispatcher.destroy(error),
638
+ ...proxyDispatchers.map((dispatcher) => dispatcher.destroy(error)),
639
+ ]);
640
+ }
641
+ }
642
+
643
+ function resolveUndiciDispatchUrl(options: Dispatcher.DispatchOptions): string | undefined {
644
+ if (options.origin !== undefined) {
645
+ const origin = options.origin.toString().replace(/\/$/, "");
646
+ const path = options.path.startsWith("/") ? options.path : `/${options.path}`;
647
+ return new URL(`${origin}${path}`).href;
648
+ }
649
+ try {
650
+ return new URL(options.path).href;
651
+ } catch {
652
+ return undefined;
653
+ }
654
+ }
655
+
656
+ function restoreNodeHttpSnapshot(snapshot: NodeHttpStackSnapshot): void {
657
+ http.request = snapshot.httpRequest;
658
+ http.get = snapshot.httpGet;
659
+ http.globalAgent = snapshot.httpGlobalAgent;
660
+ https.request = snapshot.httpsRequest;
661
+ https.get = snapshot.httpsGet;
662
+ https.globalAgent = snapshot.httpsGlobalAgent;
663
+ }
664
+
665
+ function installRuntime(
666
+ resolver: ProxyResolver,
667
+ dispatcherOptions:
668
+ | { mode: "managed"; resolver: ProxyResolver }
669
+ | { mode: "ambient"; env: ProxyEnvSnapshot; active: boolean },
670
+ proxyCa: string | undefined,
671
+ options: {
672
+ ambientEnv: ProxyEnvSnapshot | undefined;
673
+ bypassPolicy: ProxylineBypassPolicy | undefined;
674
+ proxyUrl: URL | undefined;
675
+ undici: ProxylineUndiciOptions | undefined;
676
+ },
677
+ ): RuntimeInstall {
678
+ if (activeRuntime !== undefined) {
679
+ throw new ProxylineError("RUNTIME_ALREADY_ACTIVE", "Proxyline already has an active runtime.");
680
+ }
681
+ const snapshot: NodeHttpStackSnapshot = {
682
+ httpRequest: http.request,
683
+ httpGet: http.get,
684
+ httpGlobalAgent: http.globalAgent,
685
+ httpsRequest: https.request,
686
+ httpsGet: https.get,
687
+ httpsGlobalAgent: https.globalAgent,
688
+ };
689
+ const nodeHttpAgent = createNodeProxyAgent(resolver, proxyCa, "http");
690
+ const nodeHttpsAgent = createNodeProxyAgent(resolver, proxyCa, "https");
691
+ const originalDispatcher = getGlobalDispatcher();
692
+ const originalFetch = globalThis.fetch;
693
+ const originalFormData = globalThis.FormData;
694
+ const originalHeaders = globalThis.Headers;
695
+ const originalRequest = globalThis.Request;
696
+ const originalResponse = globalThis.Response;
697
+ const installedDispatcher = createUndiciProxyDispatcher(dispatcherOptions, {
698
+ proxyCa,
699
+ undici: options.undici,
700
+ });
701
+ const runtime: RuntimeInstall = {
702
+ ambientEnv: options.ambientEnv,
703
+ bypassPolicy: options.bypassPolicy,
704
+ installedDispatcher,
705
+ mode: dispatcherOptions.mode,
706
+ nodeHttpAgent,
707
+ nodeHttpsAgent,
708
+ originalDispatcher,
709
+ originalFetch,
710
+ originalFormData,
711
+ originalHeaders,
712
+ originalRequest,
713
+ originalResponse,
714
+ proxyCa,
715
+ proxyUrl: options.proxyUrl?.href,
716
+ snapshot,
717
+ undiciOptions: options.undici,
718
+ };
719
+ activeRuntime = runtime;
720
+ try {
721
+ http.globalAgent = nodeHttpAgent;
722
+ https.globalAgent = nodeHttpsAgent as unknown as typeof https.globalAgent;
723
+ http.request = bindNodeHttpMethod(snapshot.httpRequest, () =>
724
+ createNodeProxyAgent(resolver, proxyCa, "http"),
725
+ );
726
+ http.get = bindNodeHttpMethod(snapshot.httpGet, () =>
727
+ createNodeProxyAgent(resolver, proxyCa, "http"),
728
+ );
729
+ https.request = bindNodeHttpMethod(snapshot.httpsRequest, () =>
730
+ createNodeProxyAgent(resolver, proxyCa, "https"),
731
+ );
732
+ https.get = bindNodeHttpMethod(snapshot.httpsGet, () =>
733
+ createNodeProxyAgent(resolver, proxyCa, "https"),
734
+ );
735
+ setGlobalDispatcher(installedDispatcher);
736
+ globalThis.fetch = proxylineFetch;
737
+ globalThis.FormData = proxylineFormData;
738
+ globalThis.Headers = proxylineHeaders;
739
+ globalThis.Request = proxylineRequest;
740
+ globalThis.Response = proxylineResponse;
741
+ } catch (error) {
742
+ restoreNodeHttpSnapshot(snapshot);
743
+ setGlobalDispatcher(originalDispatcher);
744
+ globalThis.fetch = originalFetch;
745
+ globalThis.FormData = originalFormData;
746
+ globalThis.Headers = originalHeaders;
747
+ globalThis.Request = originalRequest;
748
+ globalThis.Response = originalResponse;
749
+ activeRuntime = undefined;
750
+ void installedDispatcher.destroy();
751
+ nodeHttpAgent.destroy();
752
+ nodeHttpsAgent.destroy();
753
+ throw error;
754
+ }
755
+ return runtime;
756
+ }
757
+
758
+ function stopRuntime(runtime: RuntimeInstall): void {
759
+ if (activeRuntime !== runtime) {
760
+ return;
761
+ }
762
+ restoreNodeHttpSnapshot(runtime.snapshot);
763
+ setGlobalDispatcher(runtime.originalDispatcher);
764
+ globalThis.fetch = runtime.originalFetch;
765
+ globalThis.FormData = runtime.originalFormData;
766
+ globalThis.Headers = runtime.originalHeaders;
767
+ globalThis.Request = runtime.originalRequest;
768
+ globalThis.Response = runtime.originalResponse;
769
+ void runtime.installedDispatcher.destroy();
770
+ runtime.nodeHttpAgent.destroy();
771
+ runtime.nodeHttpsAgent.destroy();
772
+ activeRuntime = undefined;
773
+ activeHandle = undefined;
774
+ }
775
+
776
+ export function installProxyline(options: ProxylineOptions): ProxylineHandle {
777
+ const proxyUrl = options.mode === "managed" ? normalizeProxyUrl(options.proxyUrl) : undefined;
778
+ const ambientEnv = proxyUrl === undefined ? readProxyEnv() : undefined;
779
+ if (options.mode === "managed" && proxyUrl === undefined) {
780
+ throw new ProxylineError(
781
+ "MANAGED_PROXY_URL_REQUIRED",
782
+ "Proxyline managed mode requires an explicit proxyUrl.",
783
+ );
784
+ }
785
+
786
+ const activePolicy = options.ifActive ?? "error";
787
+ if (activeRuntime !== undefined) {
788
+ if (activePolicy === "replace") {
789
+ activeHandle?.stop();
790
+ } else if (
791
+ activePolicy === "reuse-compatible" &&
792
+ activeHandle !== undefined &&
793
+ activeRuntime.mode === options.mode &&
794
+ activeRuntime.proxyUrl === proxyUrl?.href &&
795
+ proxyEnvSnapshotKey(activeRuntime.ambientEnv) === proxyEnvSnapshotKey(ambientEnv) &&
796
+ activeRuntime.proxyCa === resolveProxyTlsCa(options.proxyTls) &&
797
+ activeRuntime.bypassPolicy === options.bypassPolicy &&
798
+ JSON.stringify(activeRuntime.undiciOptions ?? {}) === JSON.stringify(options.undici ?? {})
799
+ ) {
800
+ return activeHandle;
801
+ } else {
802
+ throw new ProxylineError(
803
+ "RUNTIME_ALREADY_ACTIVE",
804
+ "Proxyline already has an active runtime.",
805
+ );
806
+ }
807
+ }
808
+
809
+ let stopped = false;
810
+ const proxyCa = resolveProxyTlsCa(options.proxyTls);
811
+ const dynamicBypasses = createDynamicBypassRegistry();
812
+ const resolver =
813
+ proxyUrl !== undefined
814
+ ? createManagedProxyResolver(proxyUrl, options.bypassPolicy, dynamicBypasses)
815
+ : createAmbientProxyResolver(ambientEnv ?? EMPTY_PROXY_ENV);
816
+ const redactedProxyUrl = resolver.describeProxy();
817
+ const hasActiveProxy = resolver.active;
818
+ const runtime = hasActiveProxy
819
+ ? installRuntime(
820
+ resolver,
821
+ proxyUrl !== undefined
822
+ ? { mode: "managed", resolver }
823
+ : { mode: "ambient", env: ambientEnv ?? EMPTY_PROXY_ENV, active: hasActiveProxy },
824
+ proxyCa,
825
+ {
826
+ ambientEnv,
827
+ bypassPolicy: options.bypassPolicy,
828
+ proxyUrl,
829
+ undici: options.undici,
830
+ },
831
+ )
832
+ : undefined;
833
+ emit(options.onEvent, {
834
+ type: "runtime.installed",
835
+ mode: options.mode,
836
+ active: hasActiveProxy,
837
+ ...(redactedProxyUrl ? { proxyUrl: redactedProxyUrl } : {}),
838
+ });
839
+
840
+ const handle: ProxylineHandle = {
841
+ mode: options.mode,
842
+ active: hasActiveProxy,
843
+ ...(redactedProxyUrl ? { proxyUrl: redactedProxyUrl } : {}),
844
+ createNodeAgent: () => {
845
+ if (!hasActiveProxy || stopped) {
846
+ return createDirectNodeAgent();
847
+ }
848
+ return createNodeProxyAgent(resolver, proxyCa);
849
+ },
850
+ createUndiciDispatcher: () =>
851
+ stopped
852
+ ? createUndiciAgent(options.undici)
853
+ : createUndiciProxyDispatcher(
854
+ proxyUrl !== undefined
855
+ ? { mode: "managed", resolver }
856
+ : { mode: "ambient", env: ambientEnv ?? EMPTY_PROXY_ENV, active: hasActiveProxy },
857
+ { proxyCa, undici: options.undici },
858
+ ),
859
+ createWebSocketAgent: () => {
860
+ if (!hasActiveProxy || stopped) {
861
+ return createDirectNodeAgent();
862
+ }
863
+ return createNodeProxyAgent(resolver, proxyCa);
864
+ },
865
+ explain: (url, explainOptions) => {
866
+ const decision =
867
+ stopped
868
+ ? {
869
+ kind: "direct" as const,
870
+ reason: "runtime-stopped",
871
+ surface: explainOptions?.surface ?? "unknown",
872
+ url: formatUrl(url),
873
+ }
874
+ : resolver.explain(url, explainOptions?.surface ?? "unknown");
875
+ emit(options.onEvent, { type: "decision", decision });
876
+ return decision;
877
+ },
878
+ registerBypass: (registration) => {
879
+ if (stopped || proxyUrl === undefined) {
880
+ return () => {};
881
+ }
882
+ return dynamicBypasses.add(registration);
883
+ },
884
+ stop: () => {
885
+ if (stopped) {
886
+ return;
887
+ }
888
+ stopped = true;
889
+ if (runtime !== undefined) {
890
+ stopRuntime(runtime);
891
+ }
892
+ emit(options.onEvent, { type: "runtime.stopped", mode: options.mode });
893
+ },
894
+ withBypass: (registration, run) => {
895
+ if (stopped || proxyUrl === undefined) {
896
+ return run();
897
+ }
898
+ return dynamicBypasses.runScoped(registration, run);
899
+ },
900
+ };
901
+
902
+ activeHandle = hasActiveProxy ? handle : activeHandle;
903
+ return handle;
904
+ }
905
+
906
+ export const installGlobalProxy = installProxyline;