kinetex 1.2.0 → 1.4.0

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.
Files changed (126) hide show
  1. package/README.md +1164 -453
  2. package/dist/browser/kinetex.esm.js +38 -22
  3. package/dist/browser/kinetex.js +3127 -715
  4. package/dist/browser/kinetex.min.js +38 -22
  5. package/dist/cjs/aws-sigv4.js +137 -20
  6. package/dist/cjs/cache.js +101 -21
  7. package/dist/cjs/circuit-breaker.js +69 -7
  8. package/dist/cjs/client.js +838 -191
  9. package/dist/cjs/cookie-parser.js +110 -9
  10. package/dist/cjs/cookie-store.js +141 -36
  11. package/dist/cjs/core.js +501 -63
  12. package/dist/cjs/dedup.js +58 -18
  13. package/dist/cjs/digest.js +185 -23
  14. package/dist/cjs/graphql.js +164 -24
  15. package/dist/cjs/headers.js +362 -48
  16. package/dist/cjs/interceptors.js +285 -29
  17. package/dist/cjs/lifecycle.js +89 -40
  18. package/dist/cjs/logging.js +169 -16
  19. package/dist/cjs/mod.js +3 -2
  20. package/dist/cjs/pagination.js +261 -28
  21. package/dist/cjs/progress.js +282 -52
  22. package/dist/cjs/proxy.js +412 -0
  23. package/dist/cjs/response.js +316 -47
  24. package/dist/cjs/socks5.js +167 -36
  25. package/dist/cjs/sse.js +201 -34
  26. package/dist/cjs/url.js +191 -45
  27. package/dist/cjs/utils.js +222 -48
  28. package/dist/cjs/worker.js +6 -6
  29. package/dist/cjs/ws.js +32 -16
  30. package/dist/esm/aws-sigv4.js +137 -20
  31. package/dist/esm/aws-sigv4.js.map +1 -1
  32. package/dist/esm/cache.js +101 -21
  33. package/dist/esm/cache.js.map +1 -1
  34. package/dist/esm/circuit-breaker.js +69 -7
  35. package/dist/esm/circuit-breaker.js.map +1 -1
  36. package/dist/esm/client.js +838 -191
  37. package/dist/esm/client.js.map +1 -1
  38. package/dist/esm/cookie-parser.js +110 -9
  39. package/dist/esm/cookie-parser.js.map +1 -1
  40. package/dist/esm/cookie-store.js +141 -36
  41. package/dist/esm/cookie-store.js.map +1 -1
  42. package/dist/esm/core.js +501 -63
  43. package/dist/esm/core.js.map +1 -1
  44. package/dist/esm/dedup.js +58 -18
  45. package/dist/esm/dedup.js.map +1 -1
  46. package/dist/esm/digest.js +185 -23
  47. package/dist/esm/digest.js.map +1 -1
  48. package/dist/esm/graphql.js +164 -24
  49. package/dist/esm/graphql.js.map +1 -1
  50. package/dist/esm/headers.js +362 -48
  51. package/dist/esm/headers.js.map +1 -1
  52. package/dist/esm/interceptors.js +285 -29
  53. package/dist/esm/interceptors.js.map +1 -1
  54. package/dist/esm/lifecycle.js +89 -40
  55. package/dist/esm/lifecycle.js.map +1 -1
  56. package/dist/esm/logging.js +169 -16
  57. package/dist/esm/logging.js.map +1 -1
  58. package/dist/esm/mod.js +3 -2
  59. package/dist/esm/mod.js.map +1 -1
  60. package/dist/esm/pagination.js +261 -28
  61. package/dist/esm/pagination.js.map +1 -1
  62. package/dist/esm/progress.js +282 -52
  63. package/dist/esm/progress.js.map +1 -1
  64. package/dist/esm/proxy.js +413 -0
  65. package/dist/esm/proxy.js.map +1 -0
  66. package/dist/esm/response.js +316 -47
  67. package/dist/esm/response.js.map +1 -1
  68. package/dist/esm/socks5.js +167 -36
  69. package/dist/esm/socks5.js.map +1 -1
  70. package/dist/esm/sse.js +201 -34
  71. package/dist/esm/sse.js.map +1 -1
  72. package/dist/esm/types.js.map +1 -1
  73. package/dist/esm/url.js +191 -45
  74. package/dist/esm/url.js.map +1 -1
  75. package/dist/esm/utils.js +222 -48
  76. package/dist/esm/utils.js.map +1 -1
  77. package/dist/esm/worker.js +6 -6
  78. package/dist/esm/worker.js.map +1 -1
  79. package/dist/esm/ws.js +32 -16
  80. package/dist/esm/ws.js.map +1 -1
  81. package/dist/types/aws-sigv4.d.ts.map +1 -1
  82. package/dist/types/cache.d.ts +27 -2
  83. package/dist/types/cache.d.ts.map +1 -1
  84. package/dist/types/circuit-breaker.d.ts +14 -1
  85. package/dist/types/circuit-breaker.d.ts.map +1 -1
  86. package/dist/types/client.d.ts +98 -23
  87. package/dist/types/client.d.ts.map +1 -1
  88. package/dist/types/cookie-parser.d.ts +0 -17
  89. package/dist/types/cookie-parser.d.ts.map +1 -1
  90. package/dist/types/cookie-store.d.ts.map +1 -1
  91. package/dist/types/core.d.ts +109 -25
  92. package/dist/types/core.d.ts.map +1 -1
  93. package/dist/types/dedup.d.ts +0 -7
  94. package/dist/types/dedup.d.ts.map +1 -1
  95. package/dist/types/digest.d.ts +31 -37
  96. package/dist/types/digest.d.ts.map +1 -1
  97. package/dist/types/graphql.d.ts.map +1 -1
  98. package/dist/types/headers.d.ts +62 -29
  99. package/dist/types/headers.d.ts.map +1 -1
  100. package/dist/types/interceptors.d.ts +102 -0
  101. package/dist/types/interceptors.d.ts.map +1 -1
  102. package/dist/types/lifecycle.d.ts +19 -2
  103. package/dist/types/lifecycle.d.ts.map +1 -1
  104. package/dist/types/logging.d.ts +23 -4
  105. package/dist/types/logging.d.ts.map +1 -1
  106. package/dist/types/mod.d.ts +5 -3
  107. package/dist/types/mod.d.ts.map +1 -1
  108. package/dist/types/pagination.d.ts +0 -25
  109. package/dist/types/pagination.d.ts.map +1 -1
  110. package/dist/types/progress.d.ts +1 -1
  111. package/dist/types/progress.d.ts.map +1 -1
  112. package/dist/types/proxy.d.ts +50 -0
  113. package/dist/types/proxy.d.ts.map +1 -0
  114. package/dist/types/response.d.ts +7 -1
  115. package/dist/types/response.d.ts.map +1 -1
  116. package/dist/types/socks5.d.ts.map +1 -1
  117. package/dist/types/sse.d.ts.map +1 -1
  118. package/dist/types/types.d.ts +139 -5
  119. package/dist/types/types.d.ts.map +1 -1
  120. package/dist/types/url.d.ts +0 -14
  121. package/dist/types/url.d.ts.map +1 -1
  122. package/dist/types/utils.d.ts.map +1 -1
  123. package/dist/types/worker.d.ts +6 -6
  124. package/dist/types/worker.d.ts.map +1 -1
  125. package/dist/types/ws.d.ts.map +1 -1
  126. package/package.json +2 -2
@@ -0,0 +1,412 @@
1
+ /**
2
+ * §14 HTTP(S) CONNECT PROXY TUNNELING
3
+ *
4
+ * Establishes a TCP tunnel to a target host through an HTTP/HTTPS proxy using
5
+ * the `CONNECT` method (RFC 9110 §9.3.6), then optionally wraps that socket in
6
+ * TLS to the target.
7
+ *
8
+ * The tunnel is how an HTTP proxy must be used for HTTPS: the client sends
9
+ * only the CONNECT request line to the proxy, and from there the proxy is a
10
+ * blind byte relay. The target's certificate is therefore validated end to
11
+ * end against the target's own name — the proxy never terminates TLS, so it
12
+ * cannot impersonate the target without the CA store rejecting it.
13
+ *
14
+ * This module is Node-only by construction: it imports `node:net` and
15
+ * `node:tls`. Other runtimes route through their own `fetch` and must use a
16
+ * proxy-capable dispatcher instead.
17
+ *
18
+ * @module
19
+ */
20
+ import { KinetexError } from "./types.js";
21
+ const DEFAULT_CONNECT_TIMEOUT_MS = 30_000;
22
+ /** Maximum CONNECT response head we will buffer before giving up. */
23
+ const MAX_HEAD_BYTES = 64 * 1024;
24
+ /**
25
+ * True when the proxy URL names a SOCKS proxy, which needs a different
26
+ * mechanism entirely (`createSocks5Tunnel` in `kinetex/socks5`).
27
+ */
28
+ function isSocksProxy(url) {
29
+ return /^socks5h?:/i.test(url);
30
+ }
31
+ /**
32
+ * Build the `Proxy-Authorization` header value for Basic proxy auth.
33
+ *
34
+ * Uses the same encoding as the client's HTTP Basic auth. Returns `undefined`
35
+ * when no username was supplied — an empty username is still a deliberate
36
+ * (if unusual) credential, so it is encoded rather than skipped.
37
+ */
38
+ function proxyAuthHeader(proxy) {
39
+ if (proxy.username === undefined)
40
+ return undefined;
41
+ const raw = `${proxy.username}:${proxy.password ?? ""}`;
42
+ const bytes = new TextEncoder().encode(raw);
43
+ let binary = "";
44
+ for (const b of bytes)
45
+ binary += String.fromCharCode(b);
46
+ const b64 = typeof globalThis.btoa === "function"
47
+ ? globalThis.btoa(binary)
48
+ : // Node without btoa (or a non-Latin1 username) — encode explicitly.
49
+ Buffer.from(bytes).toString("base64");
50
+ return `Basic ${b64}`;
51
+ }
52
+ /**
53
+ * Parse a `CONNECT` response head.
54
+ *
55
+ * @returns The numeric status code, or `null` when the head is incomplete.
56
+ */
57
+ function parseConnectStatus(head) {
58
+ const statusLine = head.split("\r\n", 1)[0] ?? "";
59
+ const match = /^HTTP\/1\.[01] (\d{3})(?: (.*))?$/.exec(statusLine);
60
+ if (!match)
61
+ return null;
62
+ return { status: Number(match[1]), reason: match[2] ?? "" };
63
+ }
64
+ /**
65
+ * Open a socket to a target host through an HTTP(S) proxy via `CONNECT`.
66
+ *
67
+ * Returns a socket connected to the **target** — already TLS-wrapped when the
68
+ * target is `https:`. The caller owns it and decides whether to pool it.
69
+ *
70
+ * @param proxy - Proxy URL and optional Basic credentials.
71
+ * @param target - Fully-resolved target URL.
72
+ * @param options - CA, timeout, and abort configuration.
73
+ * @returns A socket tunneled to the target.
74
+ * @throws {KinetexError} `EVALIDATION` for a SOCKS proxy or a malformed proxy
75
+ * URL, `EPROXYAUTH` when the proxy demands credentials, `EPROXY` for any
76
+ * other non-2xx CONNECT response, `ETIMEOUT` if the proxy never answers,
77
+ * `EABORT` if `signal` fires, and `ENETWORK` for socket/TLS failures.
78
+ */
79
+ export async function connectThroughProxy(proxy, target, options = {}) {
80
+ // Built conditionally: `exactOptionalPropertyTypes` forbids passing an
81
+ // explicit `undefined` for an optional property.
82
+ const errMeta = options.request !== undefined ? { request: options.request } : {};
83
+ if (isSocksProxy(proxy.url)) {
84
+ throw new KinetexError(`proxy url "${proxy.url}" is a SOCKS proxy, which needs a different mechanism. ` +
85
+ "Use createSocks5Tunnel() from kinetex/socks5 and supply your own fetch over the tunnel.", "EVALIDATION", errMeta);
86
+ }
87
+ let proxyUrl;
88
+ try {
89
+ proxyUrl = new URL(proxy.url);
90
+ }
91
+ catch (err) {
92
+ throw new KinetexError(`proxy url "${proxy.url}" is not a valid URL`, "EVALIDATION", {
93
+ ...errMeta,
94
+ cause: err,
95
+ });
96
+ }
97
+ if (proxyUrl.protocol !== "http:" && proxyUrl.protocol !== "https:") {
98
+ throw new KinetexError(`proxy url scheme "${proxyUrl.protocol}" is not supported; use http:, https:, socks5: or socks5h:`, "EVALIDATION", errMeta);
99
+ }
100
+ const net = await import("node:net");
101
+ const tls = await import("node:tls");
102
+ const proxyPort = proxyUrl.port
103
+ ? Number(proxyUrl.port)
104
+ : proxyUrl.protocol === "https:"
105
+ ? 443
106
+ : 80;
107
+ const targetPort = target.port ? Number(target.port) : target.protocol === "https:" ? 443 : 80;
108
+ const authority = `${target.hostname}:${targetPort}`;
109
+ const timeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
110
+ // ── 1. TCP (or TLS-to-proxy) connection to the proxy itself ───────────────
111
+ const baseSocket = proxyUrl.protocol === "https:"
112
+ ? await openTlsSocket(tls, {
113
+ host: proxyUrl.hostname,
114
+ port: proxyPort,
115
+ servername: proxyUrl.hostname,
116
+ ...(options.ca !== undefined ? { ca: options.ca } : {}),
117
+ }, timeoutMs, options.signal, errMeta, `proxy ${proxyUrl.host}`)
118
+ : await openTcpSocket(net, { host: proxyUrl.hostname, port: proxyPort }, timeoutMs, options.signal, errMeta, `proxy ${proxyUrl.host}`);
119
+ // ── 2. CONNECT handshake ─────────────────────────────────────────────────
120
+ const auth = proxyAuthHeader(proxy);
121
+ const connectLines = [
122
+ `CONNECT ${authority} HTTP/1.1`,
123
+ `Host: ${authority}`,
124
+ // RFC 9110 §9.3.6: proxies that support persistent tunnels are expected to
125
+ // treat a tunnel as still-open even without this, but sending it is what
126
+ // every real client does and some proxies misbehave without it.
127
+ "Proxy-Connection: Keep-Alive",
128
+ ...(auth !== undefined ? [`Proxy-Authorization: ${auth}`] : []),
129
+ "",
130
+ "",
131
+ ];
132
+ baseSocket.write(connectLines.join("\r\n"));
133
+ let head = "";
134
+ // TCP does not preserve write boundaries. A proxy that splits the 200 head
135
+ // across two writes and puts the target's first bytes in the second
136
+ // delivers the head terminator and the start of the tunnelled stream in a
137
+ // single read, so the tail is kept and put back rather than discarded --
138
+ // dropping it silently truncates the message the caller is about to read.
139
+ //
140
+ // The head is therefore read in PAUSED mode -- `on("readable")` plus
141
+ // `read()` -- and never with `on("data")`. `on("data")` switches the stream
142
+ // to flowing mode, and removing the last `data` listener does NOT switch it
143
+ // back, so a flowing socket with nothing attached silently DISCARDS the
144
+ // bytes after the head terminator. `unshift()` alone does not recover them
145
+ // either: a later `on("data")` sets `state.flowing = true` before calling
146
+ // `resume()`, and `resume()` only restarts the flow when flowing was
147
+ // previously false, so the socket stalls with the tail still buffered and
148
+ // the caller waits forever. Reading paused leaves the flow state unset, so
149
+ // whichever consumer comes next -- `on("data")`, `pipe()`, or
150
+ // `tls.connect({ socket })` -- starts the flow and sees the tail.
151
+ const headResult = await new Promise((resolve) => {
152
+ // Pull every buffered chunk into `head`; a null read means the buffer is
153
+ // drained and `readable` will fire again when more arrives.
154
+ const drain = () => {
155
+ for (;;) {
156
+ const chunk = baseSocket.read();
157
+ if (chunk === null)
158
+ return;
159
+ head += chunk.toString("latin1");
160
+ }
161
+ };
162
+ const onReadable = () => {
163
+ drain();
164
+ const end = head.indexOf("\r\n\r\n");
165
+ if (end === -1) {
166
+ if (head.length > MAX_HEAD_BYTES) {
167
+ cleanup();
168
+ resolve({
169
+ ok: false,
170
+ err: new KinetexError("Proxy sent an oversized CONNECT response", "EPROXY", errMeta),
171
+ });
172
+ }
173
+ return;
174
+ }
175
+ cleanup();
176
+ const parsed = parseConnectStatus(head.slice(0, end));
177
+ if (!parsed) {
178
+ resolve({
179
+ ok: false,
180
+ err: new KinetexError(`Proxy returned a malformed CONNECT response: ${JSON.stringify(head.slice(0, end).split("\r\n", 1)[0] ?? "")}`, "EPROXY", errMeta),
181
+ });
182
+ return;
183
+ }
184
+ // RFC 9110 section 9.3.6: any 2xx switches the proxy to tunnel mode.
185
+ // Only 200 was accepted, so a proxy answering 201 or 204 was reported
186
+ // as a refusal.
187
+ if (parsed.status >= 200 && parsed.status < 300) {
188
+ // Put the tunnel bytes back in front of the buffered stream before
189
+ // handing the socket on. latin1 round-trips the bytes exactly, which
190
+ // is how `head` was built.
191
+ const tail = head.slice(end + 4);
192
+ if (tail.length > 0)
193
+ baseSocket.unshift(Buffer.from(tail, "latin1"));
194
+ resolve({ ok: true });
195
+ return;
196
+ }
197
+ if (parsed.status === 407) {
198
+ // Deliberately does not echo credentials back to the caller.
199
+ resolve({
200
+ ok: false,
201
+ err: new KinetexError("Proxy requires authentication (407). Set `username`/`password` on the proxy config, " +
202
+ "or embed them in the proxy URL.", "EPROXY", errMeta),
203
+ });
204
+ return;
205
+ }
206
+ resolve({
207
+ ok: false,
208
+ err: new KinetexError(`Proxy refused CONNECT to ${authority}: ${parsed.status} ${parsed.reason}`.trim(), "EPROXY", errMeta),
209
+ });
210
+ };
211
+ const onError = (e) => {
212
+ cleanup();
213
+ resolve({
214
+ ok: false,
215
+ err: new KinetexError(`Proxy connection failed: ${e.message}`, "ENETWORK", {
216
+ ...errMeta,
217
+ cause: e,
218
+ }),
219
+ });
220
+ };
221
+ // A proxy that closes the connection mid-handshake delivers a clean FIN,
222
+ // not a reset — so no `error` event fires and the socket simply ends.
223
+ // Without this the request would sit here until the connect timeout
224
+ // expired: 30s by default, for a proxy that is already gone.
225
+ const onClosedEarly = () => {
226
+ cleanup();
227
+ resolve({
228
+ ok: false,
229
+ err: new KinetexError(`Proxy closed the connection before answering CONNECT for ${authority}`, "EPROXY", errMeta),
230
+ });
231
+ };
232
+ const onTimeout = () => {
233
+ cleanup();
234
+ baseSocket.destroy();
235
+ resolve({
236
+ ok: false,
237
+ err: new KinetexError(`Proxy did not respond to CONNECT for ${authority} within ${timeoutMs}ms`, "ETIMEOUT", errMeta),
238
+ });
239
+ };
240
+ const onAbort = () => {
241
+ cleanup();
242
+ baseSocket.destroy();
243
+ resolve({
244
+ ok: false,
245
+ err: new KinetexError("Proxy connection aborted", "EABORT", errMeta),
246
+ });
247
+ };
248
+ const timer = setTimeout(onTimeout, timeoutMs);
249
+ if (typeof timer.unref === "function") {
250
+ timer.unref();
251
+ }
252
+ function cleanup() {
253
+ clearTimeout(timer);
254
+ baseSocket.off("readable", onReadable);
255
+ baseSocket.off("error", onError);
256
+ baseSocket.off("end", onClosedEarly);
257
+ baseSocket.off("close", onClosedEarly);
258
+ baseSocket.off("timeout", onTimeout);
259
+ options.signal?.removeEventListener("abort", onAbort);
260
+ }
261
+ baseSocket.on("readable", onReadable);
262
+ baseSocket.once("error", onError);
263
+ baseSocket.once("end", onClosedEarly);
264
+ baseSocket.once("close", onClosedEarly);
265
+ baseSocket.setTimeout(timeoutMs, onTimeout);
266
+ options.signal?.addEventListener("abort", onAbort, { once: true });
267
+ // Data may already be buffered (the request write above and the reply
268
+ // can cross), and `readable` does not fire for bytes that arrived before
269
+ // the listener was attached.
270
+ drain();
271
+ });
272
+ if (!headResult.ok) {
273
+ baseSocket.destroy();
274
+ throw headResult.err;
275
+ }
276
+ // ── 3. TLS to the target through the tunnel ───────────────────────────────
277
+ if (target.protocol !== "https:") {
278
+ // Clear the handshake listeners' timeout; the socket now belongs to the
279
+ // caller and its own timeout policy.
280
+ baseSocket.setTimeout(0);
281
+ return baseSocket;
282
+ }
283
+ return await new Promise((resolve, reject) => {
284
+ // The CONNECT handshake is over and its timeout/abort listeners are torn
285
+ // down, but the TLS handshake to the target has not happened yet. A peer
286
+ // that accepts the tunnel and then says nothing -- a black-holed target,
287
+ // or a proxy that relays to nothing -- would otherwise wait forever, so
288
+ // the same timeout and abort cover this step too.
289
+ const onTunnelTimeout = () => {
290
+ done();
291
+ baseSocket.destroy();
292
+ reject(new KinetexError(`TLS handshake through proxy to ${target.hostname} did not complete within ${timeoutMs}ms`, "ETIMEOUT", errMeta));
293
+ };
294
+ const onTunnelAbort = () => {
295
+ done();
296
+ baseSocket.destroy();
297
+ reject(new KinetexError("Proxy connection aborted", "EABORT", errMeta));
298
+ };
299
+ const tunnelTimer = setTimeout(onTunnelTimeout, timeoutMs);
300
+ if (typeof tunnelTimer.unref === "function") {
301
+ tunnelTimer.unref();
302
+ }
303
+ options.signal?.addEventListener("abort", onTunnelAbort, { once: true });
304
+ function done() {
305
+ clearTimeout(tunnelTimer);
306
+ options.signal?.removeEventListener("abort", onTunnelAbort);
307
+ }
308
+ const secure = tls.connect({
309
+ socket: baseSocket,
310
+ servername: options.servername ?? target.hostname,
311
+ ...(options.ca !== undefined ? { ca: options.ca } : {}),
312
+ },
313
+ // `rejectUnauthorized` is left at its default (true), so a peer that
314
+ // fails verification emits `error` rather than reaching this callback.
315
+ // Verification therefore cannot be bypassed here, and the callback
316
+ // only ever runs for a verified handshake.
317
+ () => {
318
+ done();
319
+ baseSocket.setTimeout(0);
320
+ resolve(secure);
321
+ });
322
+ secure.once("error", (e) => {
323
+ done();
324
+ baseSocket.destroy();
325
+ reject(new KinetexError(`TLS handshake through proxy failed for ${target.hostname}: ${e.message}`, "ENETWORK", { ...errMeta, cause: e }));
326
+ });
327
+ });
328
+ }
329
+ /** Open a plain TCP socket, rejecting on error/timeout/abort. */
330
+ async function openTcpSocket(net, opts, timeoutMs, signal, errMeta, what) {
331
+ return await new Promise((resolve, reject) => {
332
+ const socket = net.connect({ host: opts.host, port: opts.port });
333
+ const onError = (e) => {
334
+ cleanup();
335
+ reject(new KinetexError(`Cannot reach ${what}: ${e.message}`, "ENETWORK", {
336
+ ...errMeta,
337
+ cause: e,
338
+ }));
339
+ };
340
+ const timer = setTimeout(() => {
341
+ cleanup();
342
+ socket.destroy();
343
+ reject(new KinetexError(`Connecting to ${what} timed out`, "ETIMEOUT", errMeta));
344
+ }, timeoutMs);
345
+ if (typeof timer.unref === "function") {
346
+ timer.unref();
347
+ }
348
+ const onAbort = () => {
349
+ cleanup();
350
+ socket.destroy();
351
+ reject(new KinetexError("Proxy connection aborted", "EABORT", errMeta));
352
+ };
353
+ function cleanup() {
354
+ clearTimeout(timer);
355
+ socket.off("error", onError);
356
+ signal?.removeEventListener("abort", onAbort);
357
+ }
358
+ socket.once("error", onError);
359
+ socket.once("connect", () => {
360
+ cleanup();
361
+ resolve(socket);
362
+ });
363
+ if (signal?.aborted)
364
+ onAbort();
365
+ else
366
+ signal?.addEventListener("abort", onAbort, { once: true });
367
+ });
368
+ }
369
+ /** Open a TLS socket to an HTTPS proxy itself. */
370
+ async function openTlsSocket(tls, opts, timeoutMs, signal, errMeta, what) {
371
+ return await new Promise((resolve, reject) => {
372
+ const socket = tls.connect({
373
+ host: opts.host,
374
+ port: opts.port,
375
+ servername: opts.servername,
376
+ ...(opts.ca !== undefined ? { ca: opts.ca } : {}),
377
+ }, () => {
378
+ cleanup();
379
+ resolve(socket);
380
+ });
381
+ const onError = (e) => {
382
+ cleanup();
383
+ reject(new KinetexError(`Cannot reach ${what}: ${e.message}`, "ENETWORK", {
384
+ ...errMeta,
385
+ cause: e,
386
+ }));
387
+ };
388
+ const timer = setTimeout(() => {
389
+ cleanup();
390
+ socket.destroy();
391
+ reject(new KinetexError(`Connecting to ${what} timed out`, "ETIMEOUT", errMeta));
392
+ }, timeoutMs);
393
+ if (typeof timer.unref === "function") {
394
+ timer.unref();
395
+ }
396
+ const onAbort = () => {
397
+ cleanup();
398
+ socket.destroy();
399
+ reject(new KinetexError("Proxy connection aborted", "EABORT", errMeta));
400
+ };
401
+ function cleanup() {
402
+ clearTimeout(timer);
403
+ socket.off("error", onError);
404
+ signal?.removeEventListener("abort", onAbort);
405
+ }
406
+ socket.once("error", onError);
407
+ if (signal?.aborted)
408
+ onAbort();
409
+ else
410
+ signal?.addEventListener("abort", onAbort, { once: true });
411
+ });
412
+ }