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.
- package/dist/browser/browser/mini.d.ts +2 -2
- package/dist/browser/browser/recker-mini.d.ts +8 -8
- package/dist/browser/browser/recker.d.ts +11 -8
- package/dist/browser/browser/recker.js +54 -6
- package/dist/browser/core/client.d.ts +15 -10
- package/dist/browser/core/client.js +54 -38
- package/dist/browser/index.iife.min.js +129 -130
- package/dist/browser/index.min.js +129 -130
- package/dist/browser/index.mini.iife.js +5697 -636
- package/dist/browser/index.mini.iife.min.js +47 -48
- package/dist/browser/index.mini.min.js +47 -48
- package/dist/browser/index.mini.umd.js +5697 -636
- package/dist/browser/index.mini.umd.min.js +47 -48
- package/dist/browser/index.umd.min.js +129 -130
- package/dist/browser/mini.d.ts +2 -2
- package/dist/browser/plugins/proxy-rotator.d.ts +2 -2
- package/dist/browser/plugins/proxy-rotator.js +6 -28
- package/dist/browser/recker-mini.d.ts +8 -8
- package/dist/browser/recker.d.ts +11 -8
- package/dist/browser/recker.js +54 -6
- package/dist/browser/scrape/document.js +2 -2
- package/dist/browser/scrape/element.js +7 -1
- package/dist/browser/scrape/parser/nodes/html.js +1 -1
- package/dist/browser/scrape/spider.d.ts +52 -0
- package/dist/browser/scrape/spider.js +620 -38
- package/dist/browser/scrape/types.d.ts +2 -0
- package/dist/browser/search/google.d.ts +26 -1
- package/dist/browser/search/google.js +427 -45
- package/dist/browser/seo/analyzer.d.ts +1 -0
- package/dist/browser/seo/analyzer.js +144 -1
- package/dist/browser/seo/index.d.ts +1 -1
- package/dist/browser/seo/keyword-campaign-analyzer.d.ts +2 -0
- package/dist/browser/seo/keyword-campaign-analyzer.js +538 -0
- package/dist/browser/seo/keyword-campaign-seed-advanced.d.ts +17 -0
- package/dist/browser/seo/keyword-campaign-seed-advanced.js +269 -0
- package/dist/browser/seo/keyword-campaign-seed-core.d.ts +29 -0
- package/dist/browser/seo/keyword-campaign-seed-core.js +525 -0
- package/dist/browser/seo/keyword-campaign-shared.d.ts +165 -0
- package/dist/browser/seo/keyword-campaign-shared.js +59 -0
- package/dist/browser/seo/keyword-campaign.d.ts +4 -107
- package/dist/browser/seo/keyword-campaign.js +2 -380
- package/dist/browser/seo/keywords.js +5 -22
- package/dist/browser/seo/types.d.ts +19 -0
- package/dist/browser/transport/curl.d.ts +5 -1
- package/dist/browser/transport/curl.js +207 -50
- package/dist/browser/transport/undici.d.ts +4 -3
- package/dist/browser/transport/undici.js +123 -49
- package/dist/browser/types/index.d.ts +9 -3
- package/dist/browser/utils/binary-manager.js +26 -3
- package/dist/browser/utils/block-detector.d.ts +8 -0
- package/dist/browser/utils/block-detector.js +542 -7
- package/dist/cli/commands/hls-runner.js +5 -4
- package/dist/cli/commands/live-runner.js +5 -4
- package/dist/cli/commands/loadtest-runner.js +3 -2
- package/dist/cli/commands/search.d.ts +2 -0
- package/dist/cli/commands/search.js +105 -0
- package/dist/cli/commands/seo-runner.js +9 -7
- package/dist/cli/commands/seo.js +140 -1
- package/dist/cli/commands/serve.js +75 -131
- package/dist/cli/commands/server-runner.js +59 -82
- package/dist/cli/commands/spider-runner.d.ts +37 -1
- package/dist/cli/commands/spider-runner.js +134 -10
- package/dist/cli/commands/spider.d.ts +18 -1
- package/dist/cli/commands/spider.js +457 -27
- package/dist/cli/events/handlers/cli.js +30 -1
- package/dist/cli/events/handlers/tui.js +26 -0
- package/dist/cli/events/types.d.ts +27 -0
- package/dist/cli/handler.d.ts +2 -12
- package/dist/cli/handler.js +20 -15
- package/dist/cli/handlers/protocols.js +39 -12
- package/dist/cli/handlers/search.d.ts +2 -0
- package/dist/cli/handlers/search.js +171 -0
- package/dist/cli/handlers/seo-analyze.d.ts +1 -0
- package/dist/cli/handlers/seo-analyze.js +666 -0
- package/dist/cli/handlers/seo-robots.d.ts +1 -0
- package/dist/cli/handlers/seo-robots.js +76 -0
- package/dist/cli/handlers/seo-serp.d.ts +54 -0
- package/dist/cli/handlers/seo-serp.js +243 -0
- package/dist/cli/handlers/seo-sitemap.d.ts +1 -0
- package/dist/cli/handlers/seo-sitemap.js +55 -0
- package/dist/cli/handlers/seo-spider.d.ts +1 -0
- package/dist/cli/handlers/seo-spider.js +334 -0
- package/dist/cli/handlers/seo.d.ts +8 -4
- package/dist/cli/handlers/seo.js +294 -442
- package/dist/cli/handlers/spider.js +94 -17
- package/dist/cli/handlers/streaming.js +5 -1
- package/dist/cli/index.js +11 -2
- package/dist/cli/presets.d.ts +1 -1
- package/dist/cli/presets.js +15 -4
- package/dist/cli/tui/app.js +6 -1
- package/dist/cli/tui/components/rich-response.d.ts +72 -0
- package/dist/cli/tui/components/rich-response.js +117 -0
- package/dist/cli/tui/executor-commands/background.js +30 -24
- package/dist/cli/tui/executor-commands/testing.js +3 -2
- package/dist/cli/tui/hooks/useDomains.d.ts +17 -0
- package/dist/cli/tui/hooks/useHelp.js +15 -2
- package/dist/cli/tui/job-manager.d.ts +4 -4
- package/dist/cli/tui/job-manager.js +5 -1
- package/dist/cli/tui/spider-tui.d.ts +63 -0
- package/dist/cli/tui/spider-tui.js +120 -2
- package/dist/cli/types.d.ts +12 -0
- package/dist/cli/types.js +1 -0
- package/dist/cli/utils/option-helpers.d.ts +10 -0
- package/dist/cli/utils/option-helpers.js +63 -0
- package/dist/cli/utils/score-color.d.ts +11 -0
- package/dist/cli/utils/score-color.js +11 -0
- package/dist/cli/utils/serp-campaign.d.ts +53 -0
- package/dist/cli/utils/serp-campaign.js +53 -0
- package/dist/cli/utils/serp-config.d.ts +26 -0
- package/dist/cli/utils/serp-config.js +125 -0
- package/dist/core/client.d.ts +15 -10
- package/dist/core/client.js +54 -38
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/cli.js +35 -34
- package/dist/mcp/client.d.ts +2 -2
- package/dist/mcp/client.js +20 -2
- package/dist/mcp/contract.d.ts +1 -1
- package/dist/mcp/profiles.js +5 -1
- package/dist/mcp/prompts/index.js +8 -4
- package/dist/mcp/resources/index.js +46 -23
- package/dist/mcp/server.js +9 -6
- package/dist/mcp/tools/protocols.js +9 -2
- package/dist/mcp/tools/registry.js +13 -3
- package/dist/mcp/tools/seo.js +427 -2
- package/dist/mcp/types.d.ts +5 -1
- package/dist/plugins/proxy-rotator.d.ts +2 -2
- package/dist/plugins/proxy-rotator.js +6 -28
- package/dist/raffel/client.d.ts +38 -0
- package/dist/raffel/client.js +282 -0
- package/dist/raffel/index.d.ts +2 -0
- package/dist/raffel/index.js +2 -0
- package/dist/raffel/types.d.ts +40 -0
- package/dist/raffel/types.js +14 -0
- package/dist/recker.d.ts +13 -7
- package/dist/recker.js +58 -6
- package/dist/scrape/document.js +2 -2
- package/dist/scrape/element.js +7 -1
- package/dist/scrape/parser/nodes/html.js +1 -1
- package/dist/scrape/spider.d.ts +52 -0
- package/dist/scrape/spider.js +620 -38
- package/dist/scrape/types.d.ts +2 -0
- package/dist/search/google.d.ts +26 -1
- package/dist/search/google.js +427 -45
- package/dist/search/index.d.ts +1 -1
- package/dist/seo/analyzer.d.ts +1 -0
- package/dist/seo/analyzer.js +144 -1
- package/dist/seo/index.d.ts +1 -1
- package/dist/seo/keyword-campaign-analyzer.d.ts +2 -0
- package/dist/seo/keyword-campaign-analyzer.js +538 -0
- package/dist/seo/keyword-campaign-seed-advanced.d.ts +17 -0
- package/dist/seo/keyword-campaign-seed-advanced.js +269 -0
- package/dist/seo/keyword-campaign-seed-core.d.ts +29 -0
- package/dist/seo/keyword-campaign-seed-core.js +525 -0
- package/dist/seo/keyword-campaign-shared.d.ts +165 -0
- package/dist/seo/keyword-campaign-shared.js +59 -0
- package/dist/seo/keyword-campaign.d.ts +4 -107
- package/dist/seo/keyword-campaign.js +2 -380
- package/dist/seo/keywords.js +5 -22
- package/dist/seo/types.d.ts +19 -0
- package/dist/template/index.d.ts +1 -1
- package/dist/template/types.d.ts +0 -2
- package/dist/testing/index.d.ts +1 -22
- package/dist/testing/index.js +1 -11
- package/dist/transport/curl.d.ts +5 -1
- package/dist/transport/curl.js +207 -50
- package/dist/transport/undici.d.ts +4 -3
- package/dist/transport/undici.js +123 -49
- package/dist/types/index.d.ts +9 -3
- package/dist/utils/binary-manager.js +26 -3
- package/dist/utils/block-detector.d.ts +8 -0
- package/dist/utils/block-detector.js +542 -7
- package/dist/version.js +1 -1
- package/package.json +12 -1
- package/dist/testing/mock-dns-server.d.ts +0 -69
- package/dist/testing/mock-dns-server.js +0 -269
- package/dist/testing/mock-ftp-server.d.ts +0 -89
- package/dist/testing/mock-ftp-server.js +0 -562
- package/dist/testing/mock-hls-server.d.ts +0 -80
- package/dist/testing/mock-hls-server.js +0 -381
- package/dist/testing/mock-http-server.d.ts +0 -124
- package/dist/testing/mock-http-server.js +0 -343
- package/dist/testing/mock-proxy-server.d.ts +0 -108
- package/dist/testing/mock-proxy-server.js +0 -615
- package/dist/testing/mock-sse-server.d.ts +0 -76
- package/dist/testing/mock-sse-server.js +0 -291
- package/dist/testing/mock-telnet-server.d.ts +0 -59
- package/dist/testing/mock-telnet-server.js +0 -274
- package/dist/testing/mock-udp-server.d.ts +0 -43
- package/dist/testing/mock-udp-server.js +0 -188
- package/dist/testing/mock-websocket-server.d.ts +0 -76
- package/dist/testing/mock-websocket-server.js +0 -334
- package/dist/testing/mock-whois-server.d.ts +0 -56
- package/dist/testing/mock-whois-server.js +0 -234
- package/dist/testing/proxy-certs.d.ts +0 -19
- package/dist/testing/proxy-certs.js +0 -208
package/dist/testing/index.js
CHANGED
|
@@ -1,12 +1,2 @@
|
|
|
1
1
|
export { MockClient, MockTransport, createMockClient, installGlobalMock, uninstallGlobalMock, MockAgent, } from './mock.js';
|
|
2
|
-
export
|
|
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';
|
package/dist/transport/curl.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/transport/curl.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
69
|
-
const
|
|
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 (
|
|
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
|
|
90
|
-
|
|
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
|
|
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
|
}
|
package/dist/transport/undici.js
CHANGED
|
@@ -214,10 +214,10 @@ function mapTimeoutOptions(requestTimeout, transportDefaults) {
|
|
|
214
214
|
export class UndiciTransport {
|
|
215
215
|
baseUrl;
|
|
216
216
|
options;
|
|
217
|
-
|
|
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
|
|
238
|
-
|
|
239
|
-
:
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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();
|