rezo 1.0.42 → 1.0.44

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 (89) hide show
  1. package/dist/adapters/curl.cjs +131 -29
  2. package/dist/adapters/curl.js +131 -29
  3. package/dist/adapters/entries/curl.d.ts +65 -0
  4. package/dist/adapters/entries/fetch.d.ts +65 -0
  5. package/dist/adapters/entries/http.d.ts +65 -0
  6. package/dist/adapters/entries/http2.d.ts +65 -0
  7. package/dist/adapters/entries/react-native.d.ts +65 -0
  8. package/dist/adapters/entries/xhr.d.ts +65 -0
  9. package/dist/adapters/http2.cjs +209 -22
  10. package/dist/adapters/http2.js +209 -22
  11. package/dist/adapters/index.cjs +6 -6
  12. package/dist/cache/index.cjs +9 -13
  13. package/dist/cache/index.js +0 -2
  14. package/dist/core/rezo.cjs +7 -0
  15. package/dist/core/rezo.js +7 -0
  16. package/dist/crawler/addon/decodo/index.cjs +1 -0
  17. package/dist/crawler/addon/decodo/index.js +1 -0
  18. package/dist/crawler/crawler-options.cjs +1 -0
  19. package/dist/crawler/crawler-options.js +1 -0
  20. package/dist/crawler/crawler.cjs +1070 -0
  21. package/dist/crawler/crawler.js +1068 -0
  22. package/dist/crawler/index.cjs +40 -0
  23. package/dist/{plugin → crawler}/index.js +4 -2
  24. package/dist/crawler/plugin/file-cacher.cjs +19 -0
  25. package/dist/crawler/plugin/file-cacher.js +19 -0
  26. package/dist/crawler/plugin/index.cjs +1 -0
  27. package/dist/crawler/plugin/index.js +1 -0
  28. package/dist/crawler/plugin/navigation-history.cjs +43 -0
  29. package/dist/crawler/plugin/navigation-history.js +43 -0
  30. package/dist/crawler/plugin/robots-txt.cjs +2 -0
  31. package/dist/crawler/plugin/robots-txt.js +2 -0
  32. package/dist/crawler/plugin/url-store.cjs +18 -0
  33. package/dist/crawler/plugin/url-store.js +18 -0
  34. package/dist/crawler.d.ts +511 -183
  35. package/dist/entries/crawler.cjs +5 -5
  36. package/dist/entries/crawler.js +2 -2
  37. package/dist/index.cjs +27 -24
  38. package/dist/index.d.ts +73 -0
  39. package/dist/index.js +1 -0
  40. package/dist/internal/agents/base.cjs +113 -0
  41. package/dist/internal/agents/base.js +110 -0
  42. package/dist/internal/agents/http-proxy.cjs +89 -0
  43. package/dist/internal/agents/http-proxy.js +86 -0
  44. package/dist/internal/agents/https-proxy.cjs +176 -0
  45. package/dist/internal/agents/https-proxy.js +173 -0
  46. package/dist/internal/agents/index.cjs +10 -0
  47. package/dist/internal/agents/index.js +5 -0
  48. package/dist/internal/agents/socks-client.cjs +571 -0
  49. package/dist/internal/agents/socks-client.js +567 -0
  50. package/dist/internal/agents/socks-proxy.cjs +75 -0
  51. package/dist/internal/agents/socks-proxy.js +72 -0
  52. package/dist/platform/browser.d.ts +65 -0
  53. package/dist/platform/bun.d.ts +65 -0
  54. package/dist/platform/deno.d.ts +65 -0
  55. package/dist/platform/node.d.ts +65 -0
  56. package/dist/platform/react-native.d.ts +65 -0
  57. package/dist/platform/worker.d.ts +65 -0
  58. package/dist/proxy/index.cjs +18 -16
  59. package/dist/proxy/index.js +17 -12
  60. package/dist/queue/index.cjs +8 -8
  61. package/dist/responses/buildError.cjs +11 -2
  62. package/dist/responses/buildError.js +11 -2
  63. package/dist/responses/universal/index.cjs +11 -11
  64. package/dist/utils/curl.cjs +317 -0
  65. package/dist/utils/curl.js +314 -0
  66. package/package.json +2 -6
  67. package/dist/cache/file-cacher.cjs +0 -264
  68. package/dist/cache/file-cacher.js +0 -261
  69. package/dist/cache/url-store.cjs +0 -288
  70. package/dist/cache/url-store.js +0 -285
  71. package/dist/plugin/addon/decodo/index.cjs +0 -1
  72. package/dist/plugin/addon/decodo/index.js +0 -1
  73. package/dist/plugin/crawler-options.cjs +0 -1
  74. package/dist/plugin/crawler-options.js +0 -1
  75. package/dist/plugin/crawler.cjs +0 -519
  76. package/dist/plugin/crawler.js +0 -517
  77. package/dist/plugin/index.cjs +0 -36
  78. /package/dist/{plugin → crawler}/addon/decodo/options.cjs +0 -0
  79. /package/dist/{plugin → crawler}/addon/decodo/options.js +0 -0
  80. /package/dist/{plugin → crawler}/addon/decodo/types.cjs +0 -0
  81. /package/dist/{plugin → crawler}/addon/decodo/types.js +0 -0
  82. /package/dist/{plugin → crawler}/addon/oxylabs/index.cjs +0 -0
  83. /package/dist/{plugin → crawler}/addon/oxylabs/index.js +0 -0
  84. /package/dist/{plugin → crawler}/addon/oxylabs/options.cjs +0 -0
  85. /package/dist/{plugin → crawler}/addon/oxylabs/options.js +0 -0
  86. /package/dist/{plugin → crawler}/addon/oxylabs/types.cjs +0 -0
  87. /package/dist/{plugin → crawler}/addon/oxylabs/types.js +0 -0
  88. /package/dist/{plugin → crawler}/scraper.cjs +0 -0
  89. /package/dist/{plugin → crawler}/scraper.js +0 -0
@@ -4355,6 +4355,71 @@ export declare class Rezo {
4355
4355
  * @see {@link cookieJar} - Access the underlying RezoCookieJar for more control
4356
4356
  */
4357
4357
  clearCookies(): void;
4358
+ /**
4359
+ * Convert a Rezo request configuration to a cURL command string.
4360
+ *
4361
+ * Generates a valid cURL command that can be executed in a terminal to
4362
+ * reproduce the same HTTP request. Useful for:
4363
+ * - Debugging and sharing requests
4364
+ * - Documentation and examples
4365
+ * - Testing requests outside of Node.js
4366
+ * - Exporting requests to other tools
4367
+ *
4368
+ * @param config - Request configuration object
4369
+ * @returns A cURL command string
4370
+ *
4371
+ * @example
4372
+ * ```typescript
4373
+ * const curl = Rezo.toCurl({
4374
+ * url: 'https://api.example.com/users',
4375
+ * method: 'POST',
4376
+ * headers: { 'Content-Type': 'application/json' },
4377
+ * body: { name: 'John', email: 'john@example.com' }
4378
+ * });
4379
+ * // Output: curl -X POST -H 'content-type: application/json' --data-raw '{"name":"John","email":"john@example.com"}' -L --compressed 'https://api.example.com/users'
4380
+ * ```
4381
+ */
4382
+ static toCurl(config: RezoRequestConfig | RezoRequestOptions): string;
4383
+ /**
4384
+ * Parse a cURL command string into a Rezo request configuration.
4385
+ *
4386
+ * Converts a cURL command into a configuration object that can be
4387
+ * passed directly to Rezo request methods. Useful for:
4388
+ * - Importing requests from browser DevTools
4389
+ * - Converting curl examples from API documentation
4390
+ * - Migrating scripts from curl to Rezo
4391
+ *
4392
+ * Supports common cURL options:
4393
+ * - `-X, --request` - HTTP method
4394
+ * - `-H, --header` - Request headers
4395
+ * - `-d, --data, --data-raw, --data-binary` - Request body
4396
+ * - `-u, --user` - Basic authentication
4397
+ * - `-x, --proxy` - Proxy configuration
4398
+ * - `--socks5, --socks4` - SOCKS proxy
4399
+ * - `-L, --location` - Follow redirects
4400
+ * - `--max-redirs` - Maximum redirects
4401
+ * - `--max-time` - Request timeout
4402
+ * - `-k, --insecure` - Skip TLS verification
4403
+ * - `-A, --user-agent` - User agent header
4404
+ *
4405
+ * @param curlCommand - A cURL command string
4406
+ * @returns A request configuration object
4407
+ *
4408
+ * @example
4409
+ * ```typescript
4410
+ * // From browser DevTools "Copy as cURL"
4411
+ * const config = Rezo.fromCurl(`
4412
+ * curl 'https://api.example.com/data' \\
4413
+ * -H 'Authorization: Bearer token123' \\
4414
+ * -H 'Content-Type: application/json'
4415
+ * `);
4416
+ *
4417
+ * // Use with Rezo
4418
+ * const rezo = new Rezo();
4419
+ * const response = await rezo.request(config);
4420
+ * ```
4421
+ */
4422
+ static fromCurl(curlCommand: string): RezoRequestOptions;
4358
4423
  }
4359
4424
  /**
4360
4425
  * Extended Rezo instance with Axios-compatible static helpers.
@@ -4355,6 +4355,71 @@ export declare class Rezo {
4355
4355
  * @see {@link cookieJar} - Access the underlying RezoCookieJar for more control
4356
4356
  */
4357
4357
  clearCookies(): void;
4358
+ /**
4359
+ * Convert a Rezo request configuration to a cURL command string.
4360
+ *
4361
+ * Generates a valid cURL command that can be executed in a terminal to
4362
+ * reproduce the same HTTP request. Useful for:
4363
+ * - Debugging and sharing requests
4364
+ * - Documentation and examples
4365
+ * - Testing requests outside of Node.js
4366
+ * - Exporting requests to other tools
4367
+ *
4368
+ * @param config - Request configuration object
4369
+ * @returns A cURL command string
4370
+ *
4371
+ * @example
4372
+ * ```typescript
4373
+ * const curl = Rezo.toCurl({
4374
+ * url: 'https://api.example.com/users',
4375
+ * method: 'POST',
4376
+ * headers: { 'Content-Type': 'application/json' },
4377
+ * body: { name: 'John', email: 'john@example.com' }
4378
+ * });
4379
+ * // Output: curl -X POST -H 'content-type: application/json' --data-raw '{"name":"John","email":"john@example.com"}' -L --compressed 'https://api.example.com/users'
4380
+ * ```
4381
+ */
4382
+ static toCurl(config: RezoRequestConfig | RezoRequestOptions): string;
4383
+ /**
4384
+ * Parse a cURL command string into a Rezo request configuration.
4385
+ *
4386
+ * Converts a cURL command into a configuration object that can be
4387
+ * passed directly to Rezo request methods. Useful for:
4388
+ * - Importing requests from browser DevTools
4389
+ * - Converting curl examples from API documentation
4390
+ * - Migrating scripts from curl to Rezo
4391
+ *
4392
+ * Supports common cURL options:
4393
+ * - `-X, --request` - HTTP method
4394
+ * - `-H, --header` - Request headers
4395
+ * - `-d, --data, --data-raw, --data-binary` - Request body
4396
+ * - `-u, --user` - Basic authentication
4397
+ * - `-x, --proxy` - Proxy configuration
4398
+ * - `--socks5, --socks4` - SOCKS proxy
4399
+ * - `-L, --location` - Follow redirects
4400
+ * - `--max-redirs` - Maximum redirects
4401
+ * - `--max-time` - Request timeout
4402
+ * - `-k, --insecure` - Skip TLS verification
4403
+ * - `-A, --user-agent` - User agent header
4404
+ *
4405
+ * @param curlCommand - A cURL command string
4406
+ * @returns A request configuration object
4407
+ *
4408
+ * @example
4409
+ * ```typescript
4410
+ * // From browser DevTools "Copy as cURL"
4411
+ * const config = Rezo.fromCurl(`
4412
+ * curl 'https://api.example.com/data' \\
4413
+ * -H 'Authorization: Bearer token123' \\
4414
+ * -H 'Content-Type: application/json'
4415
+ * `);
4416
+ *
4417
+ * // Use with Rezo
4418
+ * const rezo = new Rezo();
4419
+ * const response = await rezo.request(config);
4420
+ * ```
4421
+ */
4422
+ static fromCurl(curlCommand: string): RezoRequestOptions;
4358
4423
  }
4359
4424
  /**
4360
4425
  * Extended Rezo instance with Axios-compatible static helpers.
@@ -4355,6 +4355,71 @@ export declare class Rezo {
4355
4355
  * @see {@link cookieJar} - Access the underlying RezoCookieJar for more control
4356
4356
  */
4357
4357
  clearCookies(): void;
4358
+ /**
4359
+ * Convert a Rezo request configuration to a cURL command string.
4360
+ *
4361
+ * Generates a valid cURL command that can be executed in a terminal to
4362
+ * reproduce the same HTTP request. Useful for:
4363
+ * - Debugging and sharing requests
4364
+ * - Documentation and examples
4365
+ * - Testing requests outside of Node.js
4366
+ * - Exporting requests to other tools
4367
+ *
4368
+ * @param config - Request configuration object
4369
+ * @returns A cURL command string
4370
+ *
4371
+ * @example
4372
+ * ```typescript
4373
+ * const curl = Rezo.toCurl({
4374
+ * url: 'https://api.example.com/users',
4375
+ * method: 'POST',
4376
+ * headers: { 'Content-Type': 'application/json' },
4377
+ * body: { name: 'John', email: 'john@example.com' }
4378
+ * });
4379
+ * // Output: curl -X POST -H 'content-type: application/json' --data-raw '{"name":"John","email":"john@example.com"}' -L --compressed 'https://api.example.com/users'
4380
+ * ```
4381
+ */
4382
+ static toCurl(config: RezoRequestConfig | RezoRequestOptions): string;
4383
+ /**
4384
+ * Parse a cURL command string into a Rezo request configuration.
4385
+ *
4386
+ * Converts a cURL command into a configuration object that can be
4387
+ * passed directly to Rezo request methods. Useful for:
4388
+ * - Importing requests from browser DevTools
4389
+ * - Converting curl examples from API documentation
4390
+ * - Migrating scripts from curl to Rezo
4391
+ *
4392
+ * Supports common cURL options:
4393
+ * - `-X, --request` - HTTP method
4394
+ * - `-H, --header` - Request headers
4395
+ * - `-d, --data, --data-raw, --data-binary` - Request body
4396
+ * - `-u, --user` - Basic authentication
4397
+ * - `-x, --proxy` - Proxy configuration
4398
+ * - `--socks5, --socks4` - SOCKS proxy
4399
+ * - `-L, --location` - Follow redirects
4400
+ * - `--max-redirs` - Maximum redirects
4401
+ * - `--max-time` - Request timeout
4402
+ * - `-k, --insecure` - Skip TLS verification
4403
+ * - `-A, --user-agent` - User agent header
4404
+ *
4405
+ * @param curlCommand - A cURL command string
4406
+ * @returns A request configuration object
4407
+ *
4408
+ * @example
4409
+ * ```typescript
4410
+ * // From browser DevTools "Copy as cURL"
4411
+ * const config = Rezo.fromCurl(`
4412
+ * curl 'https://api.example.com/data' \\
4413
+ * -H 'Authorization: Bearer token123' \\
4414
+ * -H 'Content-Type: application/json'
4415
+ * `);
4416
+ *
4417
+ * // Use with Rezo
4418
+ * const rezo = new Rezo();
4419
+ * const response = await rezo.request(config);
4420
+ * ```
4421
+ */
4422
+ static fromCurl(curlCommand: string): RezoRequestOptions;
4358
4423
  }
4359
4424
  /**
4360
4425
  * Extended Rezo instance with Axios-compatible static helpers.
@@ -1,4 +1,5 @@
1
1
  const http2 = require("node:http2");
2
+ const tls = require("node:tls");
2
3
  const zlib = require("node:zlib");
3
4
  const { URL } = require("node:url");
4
5
  const { Readable } = require("node:stream");
@@ -14,6 +15,8 @@ const { DownloadResponse } = require('../responses/download.cjs');
14
15
  const { UploadResponse } = require('../responses/upload.cjs');
15
16
  const { CompressionUtil } = require('../utils/compression.cjs');
16
17
  const { isSameDomain, RezoPerformance } = require('../utils/tools.cjs');
18
+ const { SocksClient } = require('../internal/agents/socks-client.cjs');
19
+ const net = require("node:net");
17
20
  const { ResponseCache } = require('../cache/response-cache.cjs');
18
21
  let zstdDecompressSync = null;
19
22
  let zstdChecked = false;
@@ -194,8 +197,9 @@ class Http2SessionPool {
194
197
  this.cleanupInterval.unref();
195
198
  }
196
199
  }
197
- getSessionKey(url, options) {
198
- return `${url.protocol}//${url.host}`;
200
+ getSessionKey(url, options, proxy) {
201
+ const proxyKey = proxy ? typeof proxy === "string" ? proxy : `${proxy.protocol}://${proxy.host}:${proxy.port}` : "";
202
+ return `${url.protocol}//${url.host}${proxyKey ? `@${proxyKey}` : ""}`;
199
203
  }
200
204
  isSessionHealthy(session, entry) {
201
205
  if (session.closed || session.destroyed)
@@ -207,8 +211,8 @@ class Http2SessionPool {
207
211
  return false;
208
212
  return true;
209
213
  }
210
- async getSession(url, options, timeout, forceNew = false) {
211
- const key = this.getSessionKey(url, options);
214
+ async getSession(url, options, timeout, forceNew = false, proxy) {
215
+ const key = this.getSessionKey(url, options, proxy);
212
216
  const existing = this.sessions.get(key);
213
217
  if (!forceNew && existing && this.isSessionHealthy(existing.session, existing)) {
214
218
  existing.lastUsed = Date.now();
@@ -221,12 +225,13 @@ class Http2SessionPool {
221
225
  } catch {}
222
226
  this.sessions.delete(key);
223
227
  }
224
- const session = await this.createSession(url, options, timeout);
228
+ const session = await this.createSession(url, options, timeout, proxy);
225
229
  const entry = {
226
230
  session,
227
231
  lastUsed: Date.now(),
228
232
  refCount: 1,
229
- goawayReceived: false
233
+ goawayReceived: false,
234
+ proxy
230
235
  };
231
236
  this.sessions.set(key, entry);
232
237
  session.on("close", () => {
@@ -240,15 +245,19 @@ class Http2SessionPool {
240
245
  });
241
246
  return session;
242
247
  }
243
- createSession(url, options, timeout) {
248
+ async createSession(url, options, timeout, proxy) {
249
+ const authority = `${url.protocol}//${url.host}`;
250
+ const sessionOptions = {
251
+ ...options,
252
+ rejectUnauthorized: options?.rejectUnauthorized !== false,
253
+ ALPNProtocols: ["h2", "http/1.1"],
254
+ timeout
255
+ };
256
+ if (proxy) {
257
+ const tunnelSocket = await this.createProxyTunnel(url, proxy, timeout, options?.rejectUnauthorized);
258
+ sessionOptions.createConnection = () => tunnelSocket;
259
+ }
244
260
  return new Promise((resolve, reject) => {
245
- const authority = `${url.protocol}//${url.host}`;
246
- const sessionOptions = {
247
- ...options,
248
- rejectUnauthorized: options?.rejectUnauthorized !== false,
249
- ALPNProtocols: ["h2", "http/1.1"],
250
- timeout
251
- };
252
261
  const session = http2.connect(authority, sessionOptions);
253
262
  let settled = false;
254
263
  const timeoutId = timeout ? setTimeout(() => {
@@ -279,8 +288,186 @@ class Http2SessionPool {
279
288
  });
280
289
  });
281
290
  }
282
- releaseSession(url) {
283
- const key = this.getSessionKey(url);
291
+ async createProxyTunnel(url, proxy, timeout, rejectUnauthorized) {
292
+ return new Promise((resolve, reject) => {
293
+ let proxyUrl;
294
+ let proxyAuth;
295
+ if (typeof proxy === "string") {
296
+ proxyUrl = new URL(proxy);
297
+ if (proxyUrl.username || proxyUrl.password) {
298
+ proxyAuth = Buffer.from(`${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`).toString("base64");
299
+ }
300
+ } else {
301
+ const protocol = proxy.protocol || "http";
302
+ let proxyUrlStr = `${protocol}://${proxy.host}:${proxy.port}`;
303
+ if (proxy.auth) {
304
+ const encodedUser = encodeURIComponent(proxy.auth.username);
305
+ const encodedPass = encodeURIComponent(proxy.auth.password);
306
+ proxyUrlStr = `${protocol}://${encodedUser}:${encodedPass}@${proxy.host}:${proxy.port}`;
307
+ proxyAuth = Buffer.from(`${proxy.auth.username}:${proxy.auth.password}`).toString("base64");
308
+ }
309
+ proxyUrl = new URL(proxyUrlStr);
310
+ }
311
+ const targetHost = url.hostname;
312
+ const targetPort = url.port || (url.protocol === "https:" ? "443" : "80");
313
+ if (proxyUrl.protocol.startsWith("socks")) {
314
+ const socksType = proxyUrl.protocol === "socks5:" || proxyUrl.protocol === "socks5h:" ? 5 : 4;
315
+ const socksOpts = {
316
+ proxy: {
317
+ host: proxyUrl.hostname,
318
+ port: parseInt(proxyUrl.port || "1080", 10),
319
+ type: socksType,
320
+ userId: proxyUrl.username ? decodeURIComponent(proxyUrl.username) : undefined,
321
+ password: proxyUrl.password ? decodeURIComponent(proxyUrl.password) : undefined
322
+ },
323
+ destination: {
324
+ host: targetHost,
325
+ port: parseInt(targetPort, 10)
326
+ },
327
+ command: "connect",
328
+ timeout
329
+ };
330
+ SocksClient.createConnection(socksOpts).then(({ socket }) => {
331
+ if (url.protocol === "https:") {
332
+ const tlsSocket = tls.connect({
333
+ socket,
334
+ host: targetHost,
335
+ servername: targetHost,
336
+ rejectUnauthorized: rejectUnauthorized !== false,
337
+ ALPNProtocols: ["h2", "http/1.1"]
338
+ });
339
+ const tlsTimeoutId = timeout ? setTimeout(() => {
340
+ tlsSocket.destroy();
341
+ reject(new Error(`TLS handshake timeout after ${timeout}ms`));
342
+ }, timeout) : null;
343
+ tlsSocket.on("secureConnect", () => {
344
+ if (tlsTimeoutId)
345
+ clearTimeout(tlsTimeoutId);
346
+ const alpn = tlsSocket.alpnProtocol;
347
+ if (alpn && alpn !== "h2") {
348
+ tlsSocket.destroy();
349
+ reject(new Error(`Server does not support HTTP/2 (negotiated: ${alpn})`));
350
+ return;
351
+ }
352
+ resolve(tlsSocket);
353
+ });
354
+ tlsSocket.on("error", (err) => {
355
+ if (tlsTimeoutId)
356
+ clearTimeout(tlsTimeoutId);
357
+ reject(new Error(`TLS handshake failed: ${err.message}`));
358
+ });
359
+ } else {
360
+ resolve(socket);
361
+ }
362
+ }).catch((err) => {
363
+ reject(new Error(`SOCKS proxy connection failed: ${err.message}`));
364
+ });
365
+ return;
366
+ }
367
+ const proxyHost = proxyUrl.hostname;
368
+ const proxyPort = parseInt(proxyUrl.port || (proxyUrl.protocol === "https:" ? "443" : "80"), 10);
369
+ let proxySocket;
370
+ const connectToProxy = () => {
371
+ if (proxyUrl.protocol === "https:") {
372
+ proxySocket = tls.connect({
373
+ host: proxyHost,
374
+ port: proxyPort,
375
+ rejectUnauthorized: rejectUnauthorized !== false
376
+ });
377
+ } else {
378
+ proxySocket = net.connect({
379
+ host: proxyHost,
380
+ port: proxyPort
381
+ });
382
+ }
383
+ let settled = false;
384
+ const timeoutId = timeout ? setTimeout(() => {
385
+ if (!settled) {
386
+ settled = true;
387
+ proxySocket.destroy();
388
+ reject(new Error(`Proxy connection timeout after ${timeout}ms`));
389
+ }
390
+ }, timeout) : null;
391
+ proxySocket.on("error", (err) => {
392
+ if (!settled) {
393
+ settled = true;
394
+ if (timeoutId)
395
+ clearTimeout(timeoutId);
396
+ reject(new Error(`Proxy connection error: ${err.message}`));
397
+ }
398
+ });
399
+ proxySocket.on("connect", () => {
400
+ const connectRequest = [
401
+ `CONNECT ${targetHost}:${targetPort} HTTP/1.1`,
402
+ `Host: ${targetHost}:${targetPort}`,
403
+ proxyAuth ? `Proxy-Authorization: Basic ${proxyAuth}` : "",
404
+ "",
405
+ ""
406
+ ].filter(Boolean).join(`\r
407
+ `);
408
+ proxySocket.write(connectRequest);
409
+ });
410
+ let responseBuffer = "";
411
+ proxySocket.on("data", function onData(data) {
412
+ if (settled)
413
+ return;
414
+ responseBuffer += data.toString();
415
+ const headerEnd = responseBuffer.indexOf(`\r
416
+ \r
417
+ `);
418
+ if (headerEnd !== -1) {
419
+ settled = true;
420
+ if (timeoutId)
421
+ clearTimeout(timeoutId);
422
+ proxySocket.removeListener("data", onData);
423
+ const statusLine = responseBuffer.split(`\r
424
+ `)[0];
425
+ const statusMatch = statusLine.match(/HTTP\/\d\.\d (\d{3})/);
426
+ const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : 0;
427
+ if (statusCode === 200) {
428
+ if (url.protocol === "https:") {
429
+ const tlsSocket = tls.connect({
430
+ socket: proxySocket,
431
+ host: targetHost,
432
+ servername: targetHost,
433
+ rejectUnauthorized: rejectUnauthorized !== false,
434
+ ALPNProtocols: ["h2", "http/1.1"]
435
+ });
436
+ const tlsTimeoutId = timeout ? setTimeout(() => {
437
+ tlsSocket.destroy();
438
+ reject(new Error(`TLS handshake timeout after ${timeout}ms`));
439
+ }, timeout) : null;
440
+ tlsSocket.on("secureConnect", () => {
441
+ if (tlsTimeoutId)
442
+ clearTimeout(tlsTimeoutId);
443
+ const alpn = tlsSocket.alpnProtocol;
444
+ if (alpn && alpn !== "h2") {
445
+ tlsSocket.destroy();
446
+ reject(new Error(`Server does not support HTTP/2 (negotiated: ${alpn})`));
447
+ return;
448
+ }
449
+ resolve(tlsSocket);
450
+ });
451
+ tlsSocket.on("error", (err) => {
452
+ if (tlsTimeoutId)
453
+ clearTimeout(tlsTimeoutId);
454
+ reject(new Error(`TLS handshake failed: ${err.message}`));
455
+ });
456
+ } else {
457
+ resolve(proxySocket);
458
+ }
459
+ } else {
460
+ proxySocket.destroy();
461
+ reject(new Error(`Proxy CONNECT failed with status ${statusCode}: ${statusLine}`));
462
+ }
463
+ }
464
+ });
465
+ };
466
+ connectToProxy();
467
+ });
468
+ }
469
+ releaseSession(url, proxy) {
470
+ const key = this.getSessionKey(url, undefined, proxy);
284
471
  const entry = this.sessions.get(key);
285
472
  if (entry) {
286
473
  entry.refCount = Math.max(0, entry.refCount - 1);
@@ -293,8 +480,8 @@ class Http2SessionPool {
293
480
  }
294
481
  }
295
482
  }
296
- closeSession(url) {
297
- const key = this.getSessionKey(url);
483
+ closeSession(url, proxy) {
484
+ const key = this.getSessionKey(url, undefined, proxy);
298
485
  const entry = this.sessions.get(key);
299
486
  if (entry) {
300
487
  entry.session.close();
@@ -1008,10 +1195,10 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
1008
1195
  const forceNewSession = requestCount > 0;
1009
1196
  let session;
1010
1197
  if (config.debug) {
1011
- console.log(`[Rezo Debug] HTTP/2: Acquiring session for ${url.host}${forceNewSession ? " (forcing new for redirect)" : ""}...`);
1198
+ console.log(`[Rezo Debug] HTTP/2: Acquiring session for ${url.host}${forceNewSession ? " (forcing new for redirect)" : ""}${fetchOptions.proxy ? " (via proxy)" : ""}...`);
1012
1199
  }
1013
1200
  try {
1014
- session = await (sessionPool || Http2SessionPool.getInstance()).getSession(url, sessionOptions, config.timeout !== null ? config.timeout : undefined, forceNewSession);
1201
+ session = await (sessionPool || Http2SessionPool.getInstance()).getSession(url, sessionOptions, config.timeout !== null ? config.timeout : undefined, forceNewSession, fetchOptions.proxy);
1015
1202
  if (config.debug) {
1016
1203
  console.log(`[Rezo Debug] HTTP/2: Session acquired successfully`);
1017
1204
  }
@@ -1240,7 +1427,7 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
1240
1427
  config.transfer.requestSize = Buffer.byteLength(JSON.stringify(body), "utf8");
1241
1428
  }
1242
1429
  }
1243
- (sessionPool || Http2SessionPool.getInstance()).releaseSession(url);
1430
+ (sessionPool || Http2SessionPool.getInstance()).releaseSession(url, fetchOptions.proxy);
1244
1431
  if (isRedirect) {
1245
1432
  _stats.statusOnNext = "redirect";
1246
1433
  const partialResponse = {
@@ -1417,7 +1604,7 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
1417
1604
  if (config.debug) {
1418
1605
  console.log(`[Rezo Debug] HTTP/2: Error in 'end' handler:`, endError.message);
1419
1606
  }
1420
- (sessionPool || Http2SessionPool.getInstance()).releaseSession(url);
1607
+ (sessionPool || Http2SessionPool.getInstance()).releaseSession(url, fetchOptions.proxy);
1421
1608
  const error = buildSmartError(config, fetchOptions, endError);
1422
1609
  _stats.statusOnNext = "error";
1423
1610
  resolve(error);