recker 1.0.79 → 1.0.80-next.70ea84d

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 (196) hide show
  1. package/dist/browser/browser/mini.d.ts +2 -2
  2. package/dist/browser/browser/recker-mini.d.ts +8 -8
  3. package/dist/browser/browser/recker.d.ts +11 -8
  4. package/dist/browser/browser/recker.js +54 -6
  5. package/dist/browser/core/client.d.ts +15 -10
  6. package/dist/browser/core/client.js +54 -38
  7. package/dist/browser/index.iife.min.js +129 -130
  8. package/dist/browser/index.min.js +129 -130
  9. package/dist/browser/index.mini.iife.js +5697 -636
  10. package/dist/browser/index.mini.iife.min.js +47 -48
  11. package/dist/browser/index.mini.min.js +47 -48
  12. package/dist/browser/index.mini.umd.js +5697 -636
  13. package/dist/browser/index.mini.umd.min.js +47 -48
  14. package/dist/browser/index.umd.min.js +129 -130
  15. package/dist/browser/mini.d.ts +2 -2
  16. package/dist/browser/plugins/proxy-rotator.d.ts +2 -2
  17. package/dist/browser/plugins/proxy-rotator.js +6 -28
  18. package/dist/browser/recker-mini.d.ts +8 -8
  19. package/dist/browser/recker.d.ts +11 -8
  20. package/dist/browser/recker.js +54 -6
  21. package/dist/browser/scrape/document.js +2 -2
  22. package/dist/browser/scrape/element.js +7 -1
  23. package/dist/browser/scrape/parser/nodes/html.js +1 -1
  24. package/dist/browser/scrape/spider.d.ts +52 -0
  25. package/dist/browser/scrape/spider.js +620 -38
  26. package/dist/browser/scrape/types.d.ts +2 -0
  27. package/dist/browser/search/google.d.ts +26 -1
  28. package/dist/browser/search/google.js +427 -45
  29. package/dist/browser/seo/analyzer.d.ts +1 -0
  30. package/dist/browser/seo/analyzer.js +144 -1
  31. package/dist/browser/seo/index.d.ts +1 -1
  32. package/dist/browser/seo/keyword-campaign-analyzer.d.ts +2 -0
  33. package/dist/browser/seo/keyword-campaign-analyzer.js +538 -0
  34. package/dist/browser/seo/keyword-campaign-seed-advanced.d.ts +17 -0
  35. package/dist/browser/seo/keyword-campaign-seed-advanced.js +269 -0
  36. package/dist/browser/seo/keyword-campaign-seed-core.d.ts +29 -0
  37. package/dist/browser/seo/keyword-campaign-seed-core.js +525 -0
  38. package/dist/browser/seo/keyword-campaign-shared.d.ts +165 -0
  39. package/dist/browser/seo/keyword-campaign-shared.js +59 -0
  40. package/dist/browser/seo/keyword-campaign.d.ts +4 -107
  41. package/dist/browser/seo/keyword-campaign.js +2 -380
  42. package/dist/browser/seo/keywords.js +5 -22
  43. package/dist/browser/seo/types.d.ts +19 -0
  44. package/dist/browser/transport/curl.d.ts +5 -1
  45. package/dist/browser/transport/curl.js +207 -50
  46. package/dist/browser/transport/undici.d.ts +4 -3
  47. package/dist/browser/transport/undici.js +123 -49
  48. package/dist/browser/types/index.d.ts +9 -3
  49. package/dist/browser/utils/binary-manager.js +26 -3
  50. package/dist/browser/utils/block-detector.d.ts +8 -0
  51. package/dist/browser/utils/block-detector.js +542 -7
  52. package/dist/cli/commands/hls-runner.js +5 -4
  53. package/dist/cli/commands/live-runner.js +5 -4
  54. package/dist/cli/commands/loadtest-runner.js +3 -2
  55. package/dist/cli/commands/search.d.ts +2 -0
  56. package/dist/cli/commands/search.js +105 -0
  57. package/dist/cli/commands/seo-runner.js +9 -7
  58. package/dist/cli/commands/seo.js +140 -1
  59. package/dist/cli/commands/serve.js +75 -131
  60. package/dist/cli/commands/server-runner.js +59 -82
  61. package/dist/cli/commands/spider-runner.d.ts +37 -1
  62. package/dist/cli/commands/spider-runner.js +134 -10
  63. package/dist/cli/commands/spider.d.ts +18 -1
  64. package/dist/cli/commands/spider.js +457 -27
  65. package/dist/cli/events/handlers/cli.js +30 -1
  66. package/dist/cli/events/handlers/tui.js +26 -0
  67. package/dist/cli/events/types.d.ts +27 -0
  68. package/dist/cli/handler.d.ts +2 -12
  69. package/dist/cli/handler.js +20 -15
  70. package/dist/cli/handlers/protocols.js +39 -12
  71. package/dist/cli/handlers/search.d.ts +2 -0
  72. package/dist/cli/handlers/search.js +171 -0
  73. package/dist/cli/handlers/seo-analyze.d.ts +1 -0
  74. package/dist/cli/handlers/seo-analyze.js +666 -0
  75. package/dist/cli/handlers/seo-robots.d.ts +1 -0
  76. package/dist/cli/handlers/seo-robots.js +76 -0
  77. package/dist/cli/handlers/seo-serp.d.ts +54 -0
  78. package/dist/cli/handlers/seo-serp.js +243 -0
  79. package/dist/cli/handlers/seo-sitemap.d.ts +1 -0
  80. package/dist/cli/handlers/seo-sitemap.js +55 -0
  81. package/dist/cli/handlers/seo-spider.d.ts +1 -0
  82. package/dist/cli/handlers/seo-spider.js +334 -0
  83. package/dist/cli/handlers/seo.d.ts +8 -4
  84. package/dist/cli/handlers/seo.js +294 -442
  85. package/dist/cli/handlers/spider.js +94 -17
  86. package/dist/cli/handlers/streaming.js +5 -1
  87. package/dist/cli/index.js +11 -2
  88. package/dist/cli/presets.d.ts +1 -1
  89. package/dist/cli/presets.js +15 -4
  90. package/dist/cli/tui/app.js +6 -1
  91. package/dist/cli/tui/components/rich-response.d.ts +72 -0
  92. package/dist/cli/tui/components/rich-response.js +117 -0
  93. package/dist/cli/tui/executor-commands/background.js +30 -24
  94. package/dist/cli/tui/executor-commands/testing.js +3 -2
  95. package/dist/cli/tui/hooks/useDomains.d.ts +17 -0
  96. package/dist/cli/tui/hooks/useHelp.js +15 -2
  97. package/dist/cli/tui/job-manager.d.ts +4 -4
  98. package/dist/cli/tui/job-manager.js +5 -1
  99. package/dist/cli/tui/spider-tui.d.ts +63 -0
  100. package/dist/cli/tui/spider-tui.js +120 -2
  101. package/dist/cli/types.d.ts +12 -0
  102. package/dist/cli/types.js +1 -0
  103. package/dist/cli/utils/option-helpers.d.ts +10 -0
  104. package/dist/cli/utils/option-helpers.js +63 -0
  105. package/dist/cli/utils/score-color.d.ts +11 -0
  106. package/dist/cli/utils/score-color.js +11 -0
  107. package/dist/cli/utils/serp-campaign.d.ts +53 -0
  108. package/dist/cli/utils/serp-campaign.js +53 -0
  109. package/dist/cli/utils/serp-config.d.ts +26 -0
  110. package/dist/cli/utils/serp-config.js +125 -0
  111. package/dist/core/client.d.ts +15 -10
  112. package/dist/core/client.js +54 -38
  113. package/dist/index.d.ts +1 -0
  114. package/dist/index.js +1 -0
  115. package/dist/mcp/cli.js +35 -34
  116. package/dist/mcp/client.d.ts +2 -2
  117. package/dist/mcp/client.js +20 -2
  118. package/dist/mcp/contract.d.ts +1 -1
  119. package/dist/mcp/profiles.js +5 -1
  120. package/dist/mcp/prompts/index.js +8 -4
  121. package/dist/mcp/resources/index.js +46 -23
  122. package/dist/mcp/server.js +9 -6
  123. package/dist/mcp/tools/protocols.js +9 -2
  124. package/dist/mcp/tools/registry.js +13 -3
  125. package/dist/mcp/tools/seo.js +427 -2
  126. package/dist/mcp/types.d.ts +5 -1
  127. package/dist/plugins/proxy-rotator.d.ts +2 -2
  128. package/dist/plugins/proxy-rotator.js +6 -28
  129. package/dist/raffel/client.d.ts +38 -0
  130. package/dist/raffel/client.js +282 -0
  131. package/dist/raffel/index.d.ts +2 -0
  132. package/dist/raffel/index.js +2 -0
  133. package/dist/raffel/types.d.ts +40 -0
  134. package/dist/raffel/types.js +14 -0
  135. package/dist/recker.d.ts +13 -7
  136. package/dist/recker.js +58 -6
  137. package/dist/scrape/document.js +2 -2
  138. package/dist/scrape/element.js +7 -1
  139. package/dist/scrape/parser/nodes/html.js +1 -1
  140. package/dist/scrape/spider.d.ts +52 -0
  141. package/dist/scrape/spider.js +620 -38
  142. package/dist/scrape/types.d.ts +2 -0
  143. package/dist/search/google.d.ts +26 -1
  144. package/dist/search/google.js +427 -45
  145. package/dist/search/index.d.ts +1 -1
  146. package/dist/seo/analyzer.d.ts +1 -0
  147. package/dist/seo/analyzer.js +144 -1
  148. package/dist/seo/index.d.ts +1 -1
  149. package/dist/seo/keyword-campaign-analyzer.d.ts +2 -0
  150. package/dist/seo/keyword-campaign-analyzer.js +538 -0
  151. package/dist/seo/keyword-campaign-seed-advanced.d.ts +17 -0
  152. package/dist/seo/keyword-campaign-seed-advanced.js +269 -0
  153. package/dist/seo/keyword-campaign-seed-core.d.ts +29 -0
  154. package/dist/seo/keyword-campaign-seed-core.js +525 -0
  155. package/dist/seo/keyword-campaign-shared.d.ts +165 -0
  156. package/dist/seo/keyword-campaign-shared.js +59 -0
  157. package/dist/seo/keyword-campaign.d.ts +4 -107
  158. package/dist/seo/keyword-campaign.js +2 -380
  159. package/dist/seo/keywords.js +5 -22
  160. package/dist/seo/types.d.ts +19 -0
  161. package/dist/template/index.d.ts +1 -1
  162. package/dist/template/types.d.ts +0 -2
  163. package/dist/testing/index.d.ts +1 -22
  164. package/dist/testing/index.js +1 -11
  165. package/dist/transport/curl.d.ts +5 -1
  166. package/dist/transport/curl.js +207 -50
  167. package/dist/transport/undici.d.ts +4 -3
  168. package/dist/transport/undici.js +123 -49
  169. package/dist/types/index.d.ts +9 -3
  170. package/dist/utils/binary-manager.js +26 -3
  171. package/dist/utils/block-detector.d.ts +8 -0
  172. package/dist/utils/block-detector.js +542 -7
  173. package/dist/version.js +1 -1
  174. package/package.json +12 -1
  175. package/dist/testing/mock-dns-server.d.ts +0 -69
  176. package/dist/testing/mock-dns-server.js +0 -269
  177. package/dist/testing/mock-ftp-server.d.ts +0 -89
  178. package/dist/testing/mock-ftp-server.js +0 -562
  179. package/dist/testing/mock-hls-server.d.ts +0 -80
  180. package/dist/testing/mock-hls-server.js +0 -381
  181. package/dist/testing/mock-http-server.d.ts +0 -124
  182. package/dist/testing/mock-http-server.js +0 -343
  183. package/dist/testing/mock-proxy-server.d.ts +0 -108
  184. package/dist/testing/mock-proxy-server.js +0 -615
  185. package/dist/testing/mock-sse-server.d.ts +0 -76
  186. package/dist/testing/mock-sse-server.js +0 -291
  187. package/dist/testing/mock-telnet-server.d.ts +0 -59
  188. package/dist/testing/mock-telnet-server.js +0 -274
  189. package/dist/testing/mock-udp-server.d.ts +0 -43
  190. package/dist/testing/mock-udp-server.js +0 -188
  191. package/dist/testing/mock-websocket-server.d.ts +0 -76
  192. package/dist/testing/mock-websocket-server.js +0 -334
  193. package/dist/testing/mock-whois-server.d.ts +0 -56
  194. package/dist/testing/mock-whois-server.js +0 -234
  195. package/dist/testing/proxy-certs.d.ts +0 -19
  196. package/dist/testing/proxy-certs.js +0 -208
@@ -1,12 +1,2 @@
1
1
  export { MockClient, MockTransport, createMockClient, installGlobalMock, uninstallGlobalMock, MockAgent, } from './mock.js';
2
- export { MockUDPServer, createMockUDPServer, } from './mock-udp-server.js';
3
- export { MockHlsServer, createMockHlsVod, createMockHlsLive, createMockHlsMultiQuality, } from './mock-hls-server.js';
4
- export { MockWebSocketServer, createMockWebSocketServer, } from './mock-websocket-server.js';
5
- export { MockSSEServer, createMockSSEServer, } from './mock-sse-server.js';
6
- export { MockHttpServer, createMockHttpServer, createWebhookServer, } from './mock-http-server.js';
7
- export { MockDnsServer, } from './mock-dns-server.js';
8
- export { MockWhoisServer, } from './mock-whois-server.js';
9
- export { MockTelnetServer, } from './mock-telnet-server.js';
10
- export { MockFtpServer, } from './mock-ftp-server.js';
11
- export { MockProxyServer, createForwardProxy, createInterceptProxy, } from './mock-proxy-server.js';
12
- export { generateCertificate, generateCA, getDefaultCA, } from './proxy-certs.js';
2
+ export * from 'raffel/testing';
@@ -1,4 +1,8 @@
1
- import { Transport, ReckerRequest, ReckerResponse } from '../types/index.js';
1
+ import { Transport, ReckerRequest, ReckerResponse, ProxyOptions } from '../types/index.js';
2
2
  export declare class CurlTransport implements Transport {
3
+ private proxyList;
4
+ private proxyIndex;
5
+ constructor(proxy?: ProxyOptions | string | (ProxyOptions | string)[]);
6
+ private getNextProxy;
3
7
  dispatch(req: ReckerRequest): Promise<ReckerResponse>;
4
8
  }
@@ -2,7 +2,136 @@ import { spawn } from 'node:child_process';
2
2
  import { HttpResponse } from '../core/response.js';
3
3
  import { NetworkError } from '../core/errors.js';
4
4
  import { getCurlPath, hasImpersonate } from '../utils/binary-manager.js';
5
+ const TIMING_MARKER = '__RECKER_TIMING__';
6
+ function parseCurlTimingValue(value) {
7
+ if (!value)
8
+ return undefined;
9
+ const parsed = Number.parseFloat(value);
10
+ if (Number.isNaN(parsed))
11
+ return undefined;
12
+ return Math.round(parsed * 1000);
13
+ }
14
+ function parseCurlTimings(payload) {
15
+ if (!payload)
16
+ return {};
17
+ const pairs = payload.split(/\s+/).filter(Boolean);
18
+ const values = {};
19
+ for (const pair of pairs) {
20
+ const idx = pair.indexOf('=');
21
+ if (idx <= 0)
22
+ continue;
23
+ const key = pair.slice(0, idx);
24
+ const value = pair.slice(idx + 1);
25
+ switch (key) {
26
+ case 'dns':
27
+ values.dns = parseCurlTimingValue(value);
28
+ break;
29
+ case 'tcp':
30
+ values.tcp = parseCurlTimingValue(value);
31
+ break;
32
+ case 'tls':
33
+ values.tls = parseCurlTimingValue(value);
34
+ break;
35
+ case 'ttfb':
36
+ values.ttfb = parseCurlTimingValue(value);
37
+ break;
38
+ case 'download':
39
+ values.download = parseCurlTimingValue(value);
40
+ break;
41
+ case 'total':
42
+ values.total = parseCurlTimingValue(value);
43
+ break;
44
+ default:
45
+ break;
46
+ }
47
+ }
48
+ if (values.download === undefined && values.total !== undefined && values.ttfb !== undefined) {
49
+ values.download = Math.max(0, values.total - values.ttfb);
50
+ }
51
+ return values;
52
+ }
53
+ function parseCurlOutput(responseText) {
54
+ const markerMatch = responseText.match(/(?:\r?\n)__RECKER_TIMING__(.*)$/);
55
+ let timingText;
56
+ let bodyAndHeaders = responseText;
57
+ if (markerMatch && markerMatch.index !== undefined) {
58
+ const index = markerMatch.index;
59
+ timingText = markerMatch[1]?.trim();
60
+ bodyAndHeaders = responseText.slice(0, index);
61
+ }
62
+ const splitModes = [
63
+ { sep: '\r\n\r\n', sepLen: 4 },
64
+ { sep: '\n\n', sepLen: 2 },
65
+ ];
66
+ let foundHeaderBlock;
67
+ let bodyText = '';
68
+ for (const mode of splitModes) {
69
+ let cursor = 0;
70
+ let lastBlockStart = -1;
71
+ let lastBlockEnd = -1;
72
+ while (cursor < bodyAndHeaders.length) {
73
+ const start = bodyAndHeaders.indexOf('HTTP/', cursor);
74
+ if (start < 0)
75
+ break;
76
+ const headerEnd = bodyAndHeaders.indexOf(mode.sep, start);
77
+ if (headerEnd < 0)
78
+ break;
79
+ lastBlockStart = start;
80
+ lastBlockEnd = headerEnd + mode.sepLen;
81
+ cursor = lastBlockEnd;
82
+ }
83
+ if (lastBlockStart >= 0 && lastBlockEnd >= 0) {
84
+ foundHeaderBlock = bodyAndHeaders.slice(lastBlockStart, lastBlockEnd - mode.sepLen);
85
+ bodyText = bodyAndHeaders.slice(lastBlockEnd);
86
+ break;
87
+ }
88
+ }
89
+ if (foundHeaderBlock === undefined) {
90
+ return {
91
+ statusLine: 'HTTP/1.1 200 OK',
92
+ headersText: '',
93
+ bodyText: bodyAndHeaders,
94
+ timings: timingText ? parseCurlTimings(timingText) : undefined,
95
+ };
96
+ }
97
+ const lines = foundHeaderBlock.split(/\r?\n/);
98
+ const statusLine = lines[0]?.trim() || 'HTTP/1.1 200 OK';
99
+ const headersText = lines.slice(1).filter((line) => line.trim().length > 0).join('\r\n');
100
+ return {
101
+ statusLine,
102
+ headersText,
103
+ bodyText,
104
+ timings: timingText ? parseCurlTimings(timingText) : undefined,
105
+ };
106
+ }
107
+ let resolvedCommand = null;
108
+ async function getCommand() {
109
+ if (resolvedCommand !== null)
110
+ return resolvedCommand;
111
+ if (process.env.RECKER_CURL_BIN) {
112
+ resolvedCommand = process.env.RECKER_CURL_BIN;
113
+ }
114
+ else if (await hasImpersonate()) {
115
+ resolvedCommand = getCurlPath();
116
+ }
117
+ else {
118
+ resolvedCommand = 'curl';
119
+ }
120
+ return resolvedCommand;
121
+ }
5
122
  export class CurlTransport {
123
+ proxyList;
124
+ proxyIndex = 0;
125
+ constructor(proxy) {
126
+ this.proxyList = proxy ? (Array.isArray(proxy) ? proxy : [proxy]) : [];
127
+ }
128
+ getNextProxy() {
129
+ if (this.proxyList.length === 0)
130
+ return undefined;
131
+ const slot = this.proxyList[this.proxyIndex % this.proxyList.length];
132
+ this.proxyIndex++;
133
+ return slot;
134
+ }
6
135
  async dispatch(req) {
7
136
  return new Promise(async (resolve, reject) => {
8
137
  const args = [
@@ -10,24 +139,44 @@ export class CurlTransport {
10
139
  req.url,
11
140
  '-i',
12
141
  '-s',
142
+ '-L',
13
143
  '--compressed',
14
- '--no-keepalive'
144
+ '--no-keepalive',
145
+ '--write-out',
146
+ `\\n${TIMING_MARKER} dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}`,
15
147
  ];
148
+ const proxy = this.getNextProxy();
149
+ if (proxy) {
150
+ const proxyUrl = typeof proxy === 'string' ? proxy : proxy.url;
151
+ const proxyType = typeof proxy === 'object' && proxy.type
152
+ ? proxy.type
153
+ : new URL(proxyUrl).protocol.replace(':', '');
154
+ if (proxyType === 'socks5h') {
155
+ args.push('--socks5-hostname', proxyUrl.replace(/^socks5h:\/\//, ''));
156
+ }
157
+ else if (proxyType === 'socks5' || proxyType === 'socks') {
158
+ args.push('--socks5', proxyUrl.replace(/^socks5?:\/\//, ''));
159
+ }
160
+ else if (proxyType === 'socks4a') {
161
+ args.push('--socks4a', proxyUrl.replace(/^socks4a:\/\//, ''));
162
+ }
163
+ else if (proxyType === 'socks4') {
164
+ args.push('--socks4', proxyUrl.replace(/^socks4:\/\//, ''));
165
+ }
166
+ else {
167
+ args.push('--proxy', proxyUrl);
168
+ if (typeof proxy === 'object' && proxy.auth) {
169
+ args.push('--proxy-user', `${proxy.auth.username}:${proxy.auth.password}`);
170
+ }
171
+ }
172
+ }
16
173
  req.headers.forEach((val, key) => {
17
174
  args.push('-H', `${key}: ${val}`);
18
175
  });
19
176
  if (req.body && typeof req.body === 'string') {
20
177
  args.push('-d', req.body);
21
178
  }
22
- let command = process.env.RECKER_CURL_BIN;
23
- if (!command) {
24
- if (await hasImpersonate()) {
25
- command = getCurlPath();
26
- }
27
- else {
28
- command = 'curl';
29
- }
30
- }
179
+ const command = await getCommand();
31
180
  const child = spawn(command, args);
32
181
  const stdoutChunks = [];
33
182
  const stderrChunks = [];
@@ -39,46 +188,43 @@ export class CurlTransport {
39
188
  child.on('close', (code) => {
40
189
  if (code !== 0) {
41
190
  const stderr = Buffer.concat(stderrChunks).toString();
42
- reject(new NetworkError(`Curl exited with code ${code}: ${stderr}`, 'ERR_CURL_EXIT', req));
43
- return;
44
- }
45
- const fullOutput = Buffer.concat(stdoutChunks);
46
- let headerEndIndex = fullOutput.indexOf('\r\n\r\n');
47
- let offset = 4;
48
- if (headerEndIndex === -1) {
49
- headerEndIndex = fullOutput.indexOf('\n\n');
50
- offset = 2;
51
- }
52
- if (headerEndIndex !== -1) {
53
- const firstLine = fullOutput.subarray(0, Math.min(20, headerEndIndex)).toString();
54
- if (firstLine.startsWith('HTTP/1.1 100') || firstLine.startsWith('HTTP/2 100')) {
55
- const nextStart = headerEndIndex + offset;
56
- const secondSplit = fullOutput.indexOf('\r\n\r\n', nextStart);
57
- if (secondSplit !== -1) {
58
- headerEndIndex = secondSplit;
59
- offset = 4;
191
+ const parsed = parseCurlOutput(Buffer.concat(stdoutChunks).toString());
192
+ if (parsed.bodyText.length > 0) {
193
+ try {
194
+ const parsedHeaders = new Headers();
195
+ for (const line of parsed.headersText.split(/\r?\n/)) {
196
+ const colon = line.indexOf(':');
197
+ if (colon > 0) {
198
+ const key = line.substring(0, colon).trim();
199
+ const val = line.substring(colon + 1).trim();
200
+ parsedHeaders.append(key, val);
201
+ }
202
+ }
203
+ const statusMatch = parsed.statusLine.match(/HTTP\/[^ ]+ (\d+)(?:\s+(.*))?/);
204
+ const status = statusMatch ? Number.parseInt(statusMatch[1], 10) : 0;
205
+ const statusText = statusMatch ? statusMatch[2] || '' : '';
206
+ const nativeResponse = new Response(parsed.bodyText, {
207
+ status: Number.isFinite(status) && status > 0 ? status : 0,
208
+ statusText,
209
+ headers: parsedHeaders,
210
+ });
211
+ resolve(new HttpResponse(nativeResponse, {
212
+ timings: parsed.timings,
213
+ connection: { protocol: 'curl' },
214
+ }));
215
+ }
216
+ catch {
217
+ reject(new NetworkError(`Curl exited with code ${code}: ${stderr}`, 'ERR_CURL_EXIT', req));
60
218
  }
219
+ return;
61
220
  }
62
- }
63
- if (headerEndIndex === -1) {
64
- const nativeResponse = new Response(fullOutput, { status: 200, statusText: 'OK' });
65
- resolve(new HttpResponse(nativeResponse, { connection: { protocol: 'curl' } }));
221
+ reject(new NetworkError(`Curl exited with code ${code}: ${stderr}`, 'ERR_CURL_EXIT', req));
66
222
  return;
67
223
  }
68
- const headerBlock = fullOutput.subarray(0, headerEndIndex).toString();
69
- const bodyBlock = fullOutput.subarray(headerEndIndex + offset);
70
- const headerLines = headerBlock.split(/\r?\n/);
71
- const statusLine = headerLines[0];
72
- let status = 200;
73
- let statusText = 'OK';
74
- const statusMatch = statusLine.match(/HTTP\/[\d\.]+ (\d+) ?(.*)/);
75
- if (statusMatch) {
76
- status = parseInt(statusMatch[1], 10);
77
- statusText = statusMatch[2] || '';
78
- }
224
+ const fullOutput = Buffer.concat(stdoutChunks);
225
+ const { statusLine, headersText, bodyText, timings } = parseCurlOutput(fullOutput.toString());
79
226
  const headers = new Headers();
80
- for (let i = 1; i < headerLines.length; i++) {
81
- const line = headerLines[i];
227
+ for (const line of headersText.split(/\r?\n/)) {
82
228
  const colon = line.indexOf(':');
83
229
  if (colon > 0) {
84
230
  const key = line.substring(0, colon).trim();
@@ -86,14 +232,25 @@ export class CurlTransport {
86
232
  headers.append(key, val);
87
233
  }
88
234
  }
89
- const nativeResponse = new Response(bodyBlock, {
90
- status,
235
+ const statusMatch = statusLine.match(/HTTP\/[^ ]+ (\d+)(?:\s+(.*))?/);
236
+ const status = statusMatch ? Number.parseInt(statusMatch[1], 10) : 200;
237
+ const statusText = statusMatch ? statusMatch[2] || '' : 'OK';
238
+ const nativeResponse = new Response(bodyText, {
239
+ status: Number.isFinite(status) && status >= 0 ? status : 200,
91
240
  statusText,
92
- headers
241
+ headers,
93
242
  });
243
+ const responseTimings = timings && Object.keys(timings).length > 0 ? {
244
+ dns: timings.dns,
245
+ tcp: timings.tcp,
246
+ tls: timings.tls ? (timings.ttfb !== undefined ? timings.tls : timings.tls) : undefined,
247
+ firstByte: timings.ttfb,
248
+ content: timings.download,
249
+ total: timings.total,
250
+ } : {};
94
251
  resolve(new HttpResponse(nativeResponse, {
95
- timings: {},
96
- connection: { protocol: 'curl' }
252
+ timings: responseTimings,
253
+ connection: { protocol: 'curl' },
97
254
  }));
98
255
  });
99
256
  });
@@ -6,7 +6,7 @@ interface UndiciTransportOptions {
6
6
  headersTimeout?: number;
7
7
  bodyTimeout?: number;
8
8
  maxRedirections?: number;
9
- proxy?: ProxyOptions | string;
9
+ proxy?: ProxyOptions | string | (ProxyOptions | string)[];
10
10
  http2?: HTTP2Options;
11
11
  dns?: DNSOptions;
12
12
  agent?: AgentManager;
@@ -26,10 +26,10 @@ interface UndiciTransportOptions {
26
26
  export declare class UndiciTransport implements Transport {
27
27
  private baseUrl;
28
28
  private options;
29
- private proxyAgent?;
29
+ private proxySlots;
30
+ private proxyIndex;
30
31
  private dnsAgent?;
31
32
  private agentManager?;
32
- private proxyBypassList?;
33
33
  private tlsOptions?;
34
34
  private socketClient?;
35
35
  private observability;
@@ -39,6 +39,7 @@ export declare class UndiciTransport implements Transport {
39
39
  private recordProtocolFailure;
40
40
  shouldUseHttp2(url: string): boolean;
41
41
  getProtocolCache(): ProtocolCache | undefined;
42
+ private resolveDispatcher;
42
43
  dispatch(req: ReckerRequest): Promise<ReckerResponse>;
43
44
  private dispatchFast;
44
45
  }
@@ -214,10 +214,10 @@ function mapTimeoutOptions(requestTimeout, transportDefaults) {
214
214
  export class UndiciTransport {
215
215
  baseUrl;
216
216
  options;
217
- proxyAgent;
217
+ proxySlots = [];
218
+ proxyIndex = 0;
218
219
  dnsAgent;
219
220
  agentManager;
220
- proxyBypassList;
221
221
  tlsOptions;
222
222
  socketClient;
223
223
  observability;
@@ -234,39 +234,19 @@ export class UndiciTransport {
234
234
  this.protocolCache = options.protocolCache;
235
235
  }
236
236
  if (options.proxy) {
237
- const proxyConfig = typeof options.proxy === 'string'
238
- ? { url: options.proxy }
239
- : options.proxy;
240
- const proxyUrl = new URL(proxyConfig.url);
241
- const proxyType = proxyConfig.type || detectProxyType(proxyUrl.protocol);
242
- if (proxyType?.startsWith('socks')) {
243
- throw new NetworkError(`SOCKS proxy (${proxyType}) is not supported. Use an HTTP/HTTPS proxy or a SOCKS-to-HTTP bridge.`, 'ERR_UNSUPPORTED_PROXY_TYPE');
244
- }
245
- const proxyAuth = proxyConfig.auth
246
- ? `${proxyConfig.auth.username}:${proxyConfig.auth.password}`
247
- : undefined;
248
- const finalProxyUrl = proxyAuth
249
- ? proxyConfig.url.replace('://', `://${proxyAuth}@`)
250
- : proxyConfig.url;
251
- this.proxyBypassList = proxyConfig.bypass;
252
- const proxyAgentOptions = {
253
- uri: finalProxyUrl,
254
- headers: proxyConfig.headers,
255
- token: proxyConfig.token,
256
- proxyTunnel: proxyConfig.tunnel,
257
- requestTls: mapTlsOptions(proxyConfig.requestTls ?? options.tls),
258
- proxyTls: mapTlsOptions(proxyConfig.proxyTls),
259
- };
260
- if (proxyConfig.connectTimeout) {
261
- proxyAgentOptions.connectTimeout = proxyConfig.connectTimeout;
262
- }
263
- if (proxyConfig.http2) {
264
- if (!proxyAgentOptions.requestTls) {
265
- proxyAgentOptions.requestTls = {};
237
+ const proxyList = Array.isArray(options.proxy) ? options.proxy : [options.proxy];
238
+ for (const proxyItem of proxyList) {
239
+ const config = typeof proxyItem === 'string' ? { url: proxyItem } : proxyItem;
240
+ const proxyUrl = new URL(config.url);
241
+ const proxyType = config.type || detectProxyType(proxyUrl.protocol) || 'http';
242
+ if (proxyType.startsWith('socks')) {
243
+ this.proxySlots.push({ config, proxyType, bypass: config.bypass });
244
+ }
245
+ else {
246
+ const agent = buildHttpProxyAgent(config, options.tls);
247
+ this.proxySlots.push({ config, proxyType, bypass: config.bypass, agent });
266
248
  }
267
- proxyAgentOptions.requestTls.ALPNProtocols = ['h2', 'http/1.1'];
268
249
  }
269
- this.proxyAgent = new ProxyAgent(proxyAgentOptions);
270
250
  }
271
251
  this.agentManager = options.agent;
272
252
  if (options.dns && !this.agentManager) {
@@ -320,6 +300,30 @@ export class UndiciTransport {
320
300
  getProtocolCache() {
321
301
  return this.protocolCache;
322
302
  }
303
+ async resolveDispatcher(url, explicit) {
304
+ if (explicit)
305
+ return explicit;
306
+ if (this.socketClient)
307
+ return this.socketClient;
308
+ if (this.proxySlots.length > 0) {
309
+ const slot = this.proxySlots[this.proxyIndex % this.proxySlots.length];
310
+ this.proxyIndex++;
311
+ if (!shouldBypassProxy(url, slot.bypass)) {
312
+ if (slot.agent)
313
+ return slot.agent;
314
+ if (!slot.socksAgentPromise) {
315
+ slot.socksAgentPromise = buildSocksAgent(slot.config);
316
+ }
317
+ slot.agent = await slot.socksAgentPromise;
318
+ return slot.agent;
319
+ }
320
+ }
321
+ if (this.agentManager)
322
+ return this.agentManager.getAgentForUrl(url);
323
+ if (this.dnsAgent)
324
+ return this.dnsAgent;
325
+ return undefined;
326
+ }
323
327
  async dispatch(req) {
324
328
  const headers = Object.fromEntries(req.headers);
325
329
  const contentLengthHeader = headers['content-length'];
@@ -406,14 +410,7 @@ export class UndiciTransport {
406
410
  }, timeouts.totalTimeout);
407
411
  }
408
412
  while (true) {
409
- const dispatcher = this.socketClient || determineDispatcher({
410
- explicit: req._dispatcher,
411
- proxyAgent: this.proxyAgent,
412
- agentManager: this.agentManager,
413
- dnsAgent: this.dnsAgent,
414
- url: currentUrl,
415
- bypass: this.proxyBypassList
416
- });
413
+ const dispatcher = await this.resolveDispatcher(currentUrl, req._dispatcher);
417
414
  const bodyWithProgress = redirectCount === 0
418
415
  ? wrapUploadBody(currentBody, req.onUploadProgress, uploadTotal)
419
416
  : currentBody;
@@ -708,14 +705,7 @@ export class UndiciTransport {
708
705
  }, timeouts.totalTimeout);
709
706
  }
710
707
  while (true) {
711
- const dispatcher = this.socketClient || determineDispatcher({
712
- explicit: req._dispatcher,
713
- proxyAgent: this.proxyAgent,
714
- agentManager: this.agentManager,
715
- dnsAgent: this.dnsAgent,
716
- url: currentUrl,
717
- bypass: this.proxyBypassList
718
- });
708
+ const dispatcher = await this.resolveDispatcher(currentUrl, req._dispatcher);
719
709
  const bodyWithProgress = redirectCount === 0
720
710
  ? wrapUploadBody(currentBody, req.onUploadProgress, uploadTotal)
721
711
  : currentBody;
@@ -941,6 +931,88 @@ function mapTlsOptions(options) {
941
931
  }
942
932
  return tls;
943
933
  }
934
+ function buildHttpProxyAgent(config, globalTls) {
935
+ const proxyAuth = config.auth
936
+ ? `${config.auth.username}:${config.auth.password}`
937
+ : undefined;
938
+ const finalProxyUrl = proxyAuth
939
+ ? config.url.replace('://', `://${proxyAuth}@`)
940
+ : config.url;
941
+ const proxyAgentOptions = {
942
+ uri: finalProxyUrl,
943
+ headers: config.headers,
944
+ token: config.token,
945
+ proxyTunnel: config.tunnel,
946
+ requestTls: mapTlsOptions(config.requestTls ?? globalTls),
947
+ proxyTls: mapTlsOptions(config.proxyTls),
948
+ };
949
+ if (config.connectTimeout) {
950
+ proxyAgentOptions.connectTimeout = config.connectTimeout;
951
+ }
952
+ if (config.http2) {
953
+ if (!proxyAgentOptions.requestTls)
954
+ proxyAgentOptions.requestTls = {};
955
+ proxyAgentOptions.requestTls.ALPNProtocols = ['h2', 'http/1.1'];
956
+ }
957
+ return new ProxyAgent(proxyAgentOptions);
958
+ }
959
+ async function buildSocksAgent(config) {
960
+ let SocksClient;
961
+ try {
962
+ ({ SocksClient } = await import('socks'));
963
+ }
964
+ catch {
965
+ throw new NetworkError('SOCKS proxy support requires the "socks" package. Install it with: npm install socks', 'ERR_SOCKS_NOT_INSTALLED');
966
+ }
967
+ const proxyUrl = new URL(config.url);
968
+ const proxyHost = proxyUrl.hostname;
969
+ const proxyPort = parseInt(proxyUrl.port) || 1080;
970
+ const socksType = (config.type === 'socks4' || config.type === 'socks4a') ? 4 : 5;
971
+ const username = proxyUrl.username ? decodeURIComponent(proxyUrl.username) : config.auth?.username;
972
+ const password = proxyUrl.password ? decodeURIComponent(proxyUrl.password) : config.auth?.password;
973
+ return new Agent({
974
+ connect: async (options, callback) => {
975
+ try {
976
+ const { connect: tlsConnect } = await import('tls');
977
+ const destHost = options.hostname;
978
+ const destPort = Number(options.port) || (options.servername ? 443 : 80);
979
+ const { socket } = await SocksClient.createConnection({
980
+ proxy: {
981
+ host: proxyHost,
982
+ port: proxyPort,
983
+ type: socksType,
984
+ ...(username && { userId: username }),
985
+ ...(password && { password }),
986
+ },
987
+ command: 'connect',
988
+ destination: { host: destHost, port: destPort },
989
+ });
990
+ if (options.protocol === 'https:' || destPort === 443) {
991
+ const tlsOpts = {
992
+ socket,
993
+ servername: options.servername || destHost,
994
+ rejectUnauthorized: options.rejectUnauthorized ?? true,
995
+ };
996
+ if (options.ca)
997
+ tlsOpts.ca = options.ca;
998
+ if (options.cert)
999
+ tlsOpts.cert = options.cert;
1000
+ if (options.key)
1001
+ tlsOpts.key = options.key;
1002
+ const tlsSocket = tlsConnect(tlsOpts);
1003
+ tlsSocket.once('secureConnect', () => callback(null, tlsSocket));
1004
+ tlsSocket.once('error', (err) => callback(err, null));
1005
+ }
1006
+ else {
1007
+ callback(null, socket);
1008
+ }
1009
+ }
1010
+ catch (err) {
1011
+ callback(err, null);
1012
+ }
1013
+ },
1014
+ });
1015
+ }
944
1016
  function determineDispatcher(params) {
945
1017
  if (params.explicit)
946
1018
  return params.explicit;
@@ -968,6 +1040,8 @@ function detectProxyType(protocol) {
968
1040
  case 'socks5':
969
1041
  case 'socks':
970
1042
  return 'socks5';
1043
+ case 'socks5h':
1044
+ return 'socks5h';
971
1045
  default:
972
1046
  return undefined;
973
1047
  }
@@ -214,7 +214,13 @@ export interface CacheStorage {
214
214
  delete(key: string): Promise<void>;
215
215
  }
216
216
  export type CacheStrategy = 'cache-first' | 'network-only' | 'network-first' | 'stale-while-revalidate' | 'revalidate' | 'rfc-compliant';
217
- export type Plugin = (client: any) => void;
217
+ export interface PluginClient {
218
+ beforeRequest(fn: (req: ReckerRequest) => ReckerRequest | void | Promise<ReckerRequest | void>): void;
219
+ afterResponse(fn: (req: ReckerRequest, res: ReckerResponse) => ReckerResponse | void | Promise<ReckerResponse | void>): void;
220
+ onError(fn: (error: Error, req: ReckerRequest) => ReckerResponse | void | Promise<ReckerResponse | void>): void;
221
+ use(middleware: Middleware): void;
222
+ }
223
+ export type Plugin = (client: PluginClient) => void;
218
224
  export type HookFunction<T = void> = (req: ReckerRequest, extra?: any) => T | Promise<T>;
219
225
  export interface Hooks {
220
226
  beforeRequest?: Array<(req: ReckerRequest) => ReckerRequest | void | Promise<ReckerRequest | void>>;
@@ -289,7 +295,7 @@ export interface Http2FlowControlInfo {
289
295
  }
290
296
  export interface ProxyOptions {
291
297
  url: string;
292
- type?: 'http' | 'https' | 'socks4' | 'socks4a' | 'socks5';
298
+ type?: 'http' | 'https' | 'socks4' | 'socks4a' | 'socks5' | 'socks5h';
293
299
  auth?: {
294
300
  username: string;
295
301
  password: string;
@@ -388,7 +394,7 @@ export interface ClientOptions {
388
394
  pagination?: PaginationConfig;
389
395
  debug?: boolean;
390
396
  logger?: import('./logger.js').Logger;
391
- proxy?: ProxyOptions | string;
397
+ proxy?: ProxyOptions | string | (ProxyOptions | string)[];
392
398
  tls?: TLSOptions;
393
399
  dns?: DNSOptions;
394
400
  xsrf?: boolean | XSRFOptions;
@@ -1,4 +1,4 @@
1
- import { promises as fs } from 'node:fs';
1
+ import { promises as fs, constants } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { homedir, platform, arch } from 'node:os';
4
4
  import { spawn } from 'node:child_process';
@@ -14,13 +14,36 @@ export function getCurlPath() {
14
14
  return join(BIN_DIR, getCurlBinName());
15
15
  }
16
16
  export async function hasImpersonate() {
17
+ const binaryPath = process.env.RECKER_CURL_BIN || getCurlPath();
17
18
  try {
18
- await fs.access(getCurlPath());
19
- return true;
19
+ await fs.access(binaryPath, constants.X_OK);
20
20
  }
21
21
  catch {
22
22
  return false;
23
23
  }
24
+ return new Promise((resolve) => {
25
+ let settled = false;
26
+ const child = spawn(binaryPath, ['--version']);
27
+ const settle = (ok) => {
28
+ if (!settled) {
29
+ settled = true;
30
+ resolve(ok);
31
+ }
32
+ };
33
+ child.on('error', () => settle(false));
34
+ child.on('close', (code) => {
35
+ settle(code === 0);
36
+ });
37
+ const timeout = setTimeout(() => {
38
+ if (child.exitCode === null) {
39
+ child.kill();
40
+ }
41
+ settle(false);
42
+ }, 2_000);
43
+ child.on('close', () => {
44
+ clearTimeout(timeout);
45
+ });
46
+ });
24
47
  }
25
48
  function getDownloadUrl() {
26
49
  const p = platform();