@apifuse/provider-sdk 2.2.0-beta.4 → 2.2.0-beta.40
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/AUTHORING.md +501 -13
- package/CHANGELOG.md +165 -1
- package/README.md +72 -14
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +88 -4
- package/bin/apifuse-dev.ts +38 -5
- package/bin/apifuse-pack-check.ts +22 -2
- package/bin/apifuse-pack-smoke.ts +57 -2
- package/bin/apifuse-pack-types.ts +356 -38
- package/bin/apifuse-perf.ts +14 -13
- package/bin/apifuse-record.ts +691 -68
- package/bin/apifuse-submit-check.ts +518 -37
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/submit-check-delimited-text.ts +50 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth-turn/index.js +1 -1
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +67 -0
- package/dist/ceremonies/index.d.ts +16 -0
- package/dist/ceremonies/index.js +141 -36
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.d.ts +3 -0
- package/dist/cli/create.js +34 -35
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
- package/dist/cli/templates/provider/README.md.tpl +4 -4
- package/dist/cli/templates/provider/index.ts.tpl +6 -3
- package/dist/cli/templates/provider/operations/ping.ts.tpl +2 -1
- package/dist/config/loader.d.ts +177 -16
- package/dist/config/loader.js +424 -127
- package/dist/contract-serialization.js +4 -8
- package/dist/contract-types.d.ts +1 -0
- package/dist/contract.js +2 -0
- package/dist/declaration-validation.d.ts +32 -0
- package/dist/declaration-validation.js +207 -0
- package/dist/define.d.ts +51 -25
- package/dist/define.js +752 -38
- package/dist/error-resolution.d.ts +4 -0
- package/dist/error-resolution.js +122 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +40 -0
- package/dist/fixture-sanitization.d.ts +28 -0
- package/dist/fixture-sanitization.js +217 -0
- package/dist/health-scenario.d.ts +1842 -0
- package/dist/health-scenario.js +624 -0
- package/dist/index.d.ts +19 -9
- package/dist/index.js +10 -6
- package/dist/lint.d.ts +6 -1
- package/dist/lint.js +362 -3
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +8 -2
- package/dist/provider.js +3 -1
- package/dist/runtime/auth-flow.d.ts +5 -1
- package/dist/runtime/auth-flow.js +6 -0
- package/dist/runtime/browser.d.ts +1 -0
- package/dist/runtime/browser.js +492 -49
- package/dist/runtime/cache.d.ts +1 -0
- package/dist/runtime/cache.js +169 -15
- package/dist/runtime/choice-wordlist.d.ts +9 -0
- package/dist/runtime/choice-wordlist.js +138 -0
- package/dist/runtime/choice.d.ts +13 -1
- package/dist/runtime/choice.js +490 -102
- package/dist/runtime/executor.d.ts +2 -2
- package/dist/runtime/executor.js +26 -2
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.js +515 -53
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +366 -8
- package/dist/runtime/native-network-errors.d.ts +33 -0
- package/dist/runtime/native-network-errors.js +69 -0
- package/dist/runtime/native-network.d.ts +96 -0
- package/dist/runtime/native-network.js +1232 -0
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +440 -0
- package/dist/runtime/proxy-errors.js +6 -2
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-telemetry.d.ts +80 -1
- package/dist/runtime/proxy-telemetry.js +154 -47
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +4 -2
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/resolver-config.d.ts +6 -0
- package/dist/runtime/resolver-config.js +6 -0
- package/dist/runtime/resolver-public.d.ts +1 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-shared.d.ts +3 -0
- package/dist/runtime/resolver-shared.js +12 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
- package/dist/runtime/resolver-vendors/bindings.js +40 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
- package/dist/runtime/resolver-vendors/browser.js +377 -0
- package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
- package/dist/runtime/resolver-vendors/capsolver.js +526 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
- package/dist/runtime/resolver-vendors/hosts.js +33 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +407 -0
- package/dist/runtime/resolver-vendors/types.d.ts +94 -0
- package/dist/runtime/resolver-vendors/types.js +96 -0
- package/dist/runtime/resolver.d.ts +60 -0
- package/dist/runtime/resolver.js +737 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +277 -71
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.d.ts +28 -3
- package/dist/runtime/stealth.js +519 -255
- package/dist/runtime/stt.js +1 -12
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/runtime/trace-config.d.ts +12 -0
- package/dist/runtime/trace-config.js +61 -0
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +5 -3
- package/dist/server/index.js +3 -3
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test.d.ts +114 -0
- package/dist/server/self-test.js +784 -148
- package/dist/server/serve-implementation.d.ts +213 -0
- package/dist/server/serve-implementation.js +2173 -0
- package/dist/server/serve.d.ts +1 -70
- package/dist/server/serve.js +1 -1130
- package/dist/server/trace-output.d.ts +4 -0
- package/dist/server/trace-output.js +20 -0
- package/dist/server/types.d.ts +30 -5
- package/dist/server/types.js +13 -1
- package/dist/stateful/errors.d.ts +19 -0
- package/dist/stateful/errors.js +24 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
- package/dist/stateful/stateful-provider-session-routing.js +345 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.js +16 -7
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.js +7 -1
- package/dist/testing/index.d.ts +2 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +489 -21
- package/dist/trace-sanitization.d.ts +5 -0
- package/dist/trace-sanitization.js +45 -0
- package/dist/types.d.ts +545 -23
- package/dist/types.js +1 -0
- package/package.json +44 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +118 -0
- package/src/ceremonies/index.ts +189 -46
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +42 -35
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/cli/templates/provider/Dockerfile.tpl +1 -1
- package/src/cli/templates/provider/README.md.tpl +4 -4
- package/src/cli/templates/provider/index.ts.tpl +6 -3
- package/src/cli/templates/provider/operations/ping.ts.tpl +2 -1
- package/src/config/loader.ts +665 -163
- package/src/contract-serialization.ts +5 -7
- package/src/contract-types.ts +1 -0
- package/src/contract.ts +2 -0
- package/src/declaration-validation.ts +266 -0
- package/src/define.ts +970 -87
- package/src/error-resolution.ts +127 -0
- package/src/errors.ts +52 -0
- package/src/fixture-sanitization.ts +248 -0
- package/src/health-scenario.ts +875 -0
- package/src/index.ts +204 -8
- package/src/lint.ts +408 -4
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +133 -0
- package/src/runtime/auth-flow.ts +12 -0
- package/src/runtime/browser.ts +661 -63
- package/src/runtime/cache.ts +189 -14
- package/src/runtime/choice-wordlist.ts +145 -0
- package/src/runtime/choice.ts +631 -120
- package/src/runtime/executor.ts +40 -7
- package/src/runtime/http.ts +641 -61
- package/src/runtime/instrumentation.ts +520 -15
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +1605 -0
- package/src/runtime/ocr.ts +523 -0
- package/src/runtime/proxy-errors.ts +12 -4
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-telemetry.ts +244 -75
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +7 -2
- package/src/runtime/request-options.ts +680 -1
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-public.ts +20 -0
- package/src/runtime/resolver-shared.ts +17 -0
- package/src/runtime/resolver-vendors/bindings.ts +56 -0
- package/src/runtime/resolver-vendors/browser.ts +533 -0
- package/src/runtime/resolver-vendors/capsolver.ts +700 -0
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +539 -0
- package/src/runtime/resolver-vendors/types.ts +212 -0
- package/src/runtime/resolver.ts +1103 -0
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +394 -77
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +675 -289
- package/src/runtime/stt.ts +1 -19
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace-config.ts +77 -0
- package/src/serve.ts +6 -1
- package/src/server/index.ts +37 -2
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test.ts +1025 -175
- package/src/server/serve-implementation.ts +3250 -0
- package/src/server/serve.ts +1 -1626
- package/src/server/trace-output.ts +32 -0
- package/src/server/types.ts +13 -1
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +35 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +546 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +17 -7
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -1
- package/src/testing/index.ts +10 -1
- package/src/testing/run.ts +658 -15
- package/src/trace-sanitization.ts +63 -0
- package/src/types.ts +660 -38
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/dist/runtime/stealth.js
CHANGED
|
@@ -1,20 +1,38 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { DEFAULT_SMARTPROXY_POOL_SIZE, invalidateProxyResolutionCacheAsync, ProxyResolutionError, resolveProxyConfigAsync, SMARTPROXY_MAX_POOL_SIZE, } from "../config/loader.js";
|
|
2
|
+
import { DEFAULT_SMARTPROXY_POOL_SIZE, invalidateProxyResolutionCacheAsync, ProxyResolutionError, policyResolvesRegistryVendorChain, resolvePolicyProxyPoolSpan, resolvePolicyTransportAttemptCap, resolveProxyConfigAsync, vendorFromResolvedSource, } from "../config/loader.js";
|
|
4
3
|
import { SDKError, TransportError } from "../errors.js";
|
|
5
4
|
import { getStealthProfile } from "../stealth/profiles.js";
|
|
5
|
+
import { StealthCookieJar } from "./stealth-cookies.js";
|
|
6
6
|
import { createProxyAuthIpDeniedError, createProxyEdgeAuthRejectedError, createProxyEdgeTlsRejectedError, createProxyPoolExhaustedError, createProxyPoolStaleError, isProxyAuthIpDeniedMessage, isProxyEdgeAuthRejectedMessage, isProxyEdgeTlsRejectedResponse, isProxyPoolRefreshableError, isProxyPoolStaleMessage, isProxyPoolStaleStatus, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_STALE_CODE, } from "./proxy-errors.js";
|
|
7
7
|
import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, normalizeProxyTransportRetryOptions, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy.js";
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import { evaluateRedirectHop, isRedirectStatus, nextRedirectMethod, resolveRedirectUrl, } from "./redirects.js";
|
|
9
|
+
import { isSensitiveKey, normalizeSensitiveParams, redactSensitiveError, redactSensitiveRequestError, redactSensitiveText, redactUrlQueryParams, serializeRequestUrl, } from "./request-options.js";
|
|
10
|
+
export const DEFAULT_PROFILE = "chrome-146";
|
|
10
11
|
const MISSING_PROXY_WARNING = "[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
|
|
11
|
-
const MAX_POLICY_PROXY_RETRY_ATTEMPTS = SMARTPROXY_MAX_POOL_SIZE;
|
|
12
12
|
const MAX_POLICY_PROXY_POOL_REFRESHES = 1;
|
|
13
13
|
const PROXY_CONNECT_FAILURE_CODE = "proxy_connect_failed";
|
|
14
14
|
const PROXY_CONNECT_FAILURE_BODY_PATTERN = /\bproxy\b.*\b(non[\s-]?200|connect|tunnel)|\bconnect\b.*\bproxy\b|\btunnel\b/i;
|
|
15
15
|
const PROXY_AUTH_DIAGNOSTIC_URL = "http://example.com/";
|
|
16
16
|
const PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS = 5_000;
|
|
17
17
|
const STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES = [PROXY_CONNECT_FAILURE_CODE];
|
|
18
|
+
const MAX_STEALTH_REDIRECT_HOPS = 10;
|
|
19
|
+
const REDIRECT_BODY_HEADERS = new Set([
|
|
20
|
+
"content-encoding",
|
|
21
|
+
"content-language",
|
|
22
|
+
"content-location",
|
|
23
|
+
"content-type",
|
|
24
|
+
]);
|
|
25
|
+
function isRecord(value) {
|
|
26
|
+
return typeof value === "object" && value !== null;
|
|
27
|
+
}
|
|
28
|
+
function sensitiveQueryParamNames(url) {
|
|
29
|
+
const queryStart = url.indexOf("?");
|
|
30
|
+
if (queryStart === -1)
|
|
31
|
+
return [];
|
|
32
|
+
const fragmentStart = url.indexOf("#", queryStart);
|
|
33
|
+
const query = url.slice(queryStart + 1, fragmentStart === -1 ? undefined : fragmentStart);
|
|
34
|
+
return [...new URLSearchParams(query).keys()].filter(isSensitiveKey);
|
|
35
|
+
}
|
|
18
36
|
const REMOVED_CHROME_PROFILE_NAMES = new Set([
|
|
19
37
|
"chrome-120",
|
|
20
38
|
"chrome-124",
|
|
@@ -28,133 +46,93 @@ const REMOVED_CHROME_PROFILE_NAMES = new Set([
|
|
|
28
46
|
"chrome-130-psk",
|
|
29
47
|
"edge-131",
|
|
30
48
|
]);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
131: "chrome131",
|
|
41
|
-
136: "chrome136",
|
|
42
|
-
142: "chrome142",
|
|
43
|
-
};
|
|
44
|
-
const FIREFOX_IMPIT_BY_MAJOR = {
|
|
45
|
-
128: "firefox128",
|
|
46
|
-
133: "firefox133",
|
|
47
|
-
135: "firefox135",
|
|
48
|
-
144: "firefox144",
|
|
49
|
-
};
|
|
50
|
-
function isRecord(value) {
|
|
51
|
-
return typeof value === "object" && value !== null;
|
|
52
|
-
}
|
|
53
|
-
class CookieJarImpl {
|
|
54
|
-
cookies;
|
|
55
|
-
constructor(cookieStrings) {
|
|
56
|
-
this.cookies = {};
|
|
57
|
-
this.setFromCookieStrings(cookieStrings);
|
|
58
|
-
}
|
|
59
|
-
setFromCookieStrings(cookieStrings) {
|
|
60
|
-
for (const cookieString of cookieStrings) {
|
|
61
|
-
const [nameValue] = cookieString.split(";");
|
|
62
|
-
if (!nameValue) {
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
const separatorIndex = nameValue.indexOf("=");
|
|
66
|
-
if (separatorIndex === -1) {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
const name = nameValue.slice(0, separatorIndex).trim();
|
|
70
|
-
const value = nameValue.slice(separatorIndex + 1).trim();
|
|
71
|
-
if (name)
|
|
72
|
-
this.cookies[name] = value;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
get(name) {
|
|
76
|
-
return this.cookies[name];
|
|
77
|
-
}
|
|
78
|
-
getAll() {
|
|
79
|
-
return { ...this.cookies };
|
|
80
|
-
}
|
|
81
|
-
has(name) {
|
|
82
|
-
return Object.hasOwn(this.cookies, name);
|
|
83
|
-
}
|
|
84
|
-
toString() {
|
|
85
|
-
return Object.entries(this.cookies)
|
|
86
|
-
.map(([name, value]) => `${name}=${value}`)
|
|
87
|
-
.join("; ");
|
|
88
|
-
}
|
|
89
|
-
toHeader() {
|
|
90
|
-
return this.toString();
|
|
49
|
+
let wreqModulePromise;
|
|
50
|
+
function getWreqModule() {
|
|
51
|
+
if (!wreqModulePromise) {
|
|
52
|
+
wreqModulePromise = import("wreq-js").catch((error) => {
|
|
53
|
+
throw new SDKError(`Stealth transport is unavailable on ${process.platform}-${process.arch}: the wreq-js native binary could not be loaded.`, {
|
|
54
|
+
code: "stealth_transport_unavailable",
|
|
55
|
+
cause: error instanceof Error ? error : undefined,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
91
58
|
}
|
|
92
|
-
|
|
93
|
-
|
|
59
|
+
return wreqModulePromise;
|
|
60
|
+
}
|
|
61
|
+
function parseProfileIdentifier(identifier) {
|
|
62
|
+
const match = /^(safari_ios|safari_ipad|firefox_android|firefox_private|chrome|edge|firefox|opera|safari|okhttp)_(\d+(?:[._]\d+)*)$/.exec(identifier.toLowerCase());
|
|
63
|
+
if (!match?.[1] || !match[2])
|
|
64
|
+
return null;
|
|
65
|
+
return {
|
|
66
|
+
family: match[1],
|
|
67
|
+
version: match[2].split(/[._]/).map(Number),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function compareVersionDistance(target, left, right) {
|
|
71
|
+
const width = Math.max(target.length, left.length, right.length);
|
|
72
|
+
for (let index = 0; index < width; index += 1) {
|
|
73
|
+
const targetPart = target[index] ?? 0;
|
|
74
|
+
const leftDistance = Math.abs((left[index] ?? 0) - targetPart);
|
|
75
|
+
const rightDistance = Math.abs((right[index] ?? 0) - targetPart);
|
|
76
|
+
if (leftDistance !== rightDistance)
|
|
77
|
+
return leftDistance - rightDistance;
|
|
94
78
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
function closestWreqProfile(identifier, wreqProfiles) {
|
|
82
|
+
const requested = parseProfileIdentifier(identifier);
|
|
83
|
+
if (!requested)
|
|
84
|
+
return undefined;
|
|
85
|
+
let closest;
|
|
86
|
+
for (const candidateName of wreqProfiles) {
|
|
87
|
+
const candidate = parseProfileIdentifier(candidateName);
|
|
88
|
+
if (!candidate || candidate.family !== requested.family)
|
|
89
|
+
continue;
|
|
90
|
+
if (!closest ||
|
|
91
|
+
compareVersionDistance(requested.version, candidate.version, closest.version) < 0) {
|
|
92
|
+
closest = { name: candidateName, version: candidate.version };
|
|
100
93
|
}
|
|
101
94
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
95
|
+
return closest?.name;
|
|
96
|
+
}
|
|
97
|
+
function resolveDefaultWreqProfileMapping() {
|
|
98
|
+
let profile;
|
|
99
|
+
try {
|
|
100
|
+
profile = getStealthProfile(DEFAULT_PROFILE);
|
|
106
101
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const cookie = `${name}=${value}`;
|
|
110
|
-
if (predicate(cookie)) {
|
|
111
|
-
return cookie;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return undefined;
|
|
102
|
+
catch (error) {
|
|
103
|
+
throw new SDKError(`Default stealth profile "${DEFAULT_PROFILE}" cannot be mapped to a wreq-js browser profile.`, { cause: error instanceof Error ? error : undefined });
|
|
115
104
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
let closestBrowser;
|
|
120
|
-
for (const [candidateMajorText, browser] of Object.entries(candidates)) {
|
|
121
|
-
const candidateMajor = Number(candidateMajorText);
|
|
122
|
-
if (closestMajor === undefined ||
|
|
123
|
-
Math.abs(candidateMajor - major) < Math.abs(closestMajor - major)) {
|
|
124
|
-
closestMajor = candidateMajor;
|
|
125
|
-
closestBrowser = browser;
|
|
126
|
-
}
|
|
105
|
+
const identifier = profile.tlsClientIdentifier?.toLowerCase() ?? "";
|
|
106
|
+
if (!parseProfileIdentifier(identifier)) {
|
|
107
|
+
throw new SDKError(`Default stealth profile "${DEFAULT_PROFILE}" cannot be mapped to a wreq-js browser profile.`);
|
|
127
108
|
}
|
|
128
|
-
return
|
|
109
|
+
return { identifier, os: profile.platform };
|
|
129
110
|
}
|
|
130
|
-
|
|
111
|
+
const DEFAULT_WREQ_PROFILE_MAPPING = resolveDefaultWreqProfileMapping();
|
|
112
|
+
export function resolveWreqProfile(profileName, wreqProfiles) {
|
|
131
113
|
if (REMOVED_CHROME_PROFILE_NAMES.has(profileName)) {
|
|
132
114
|
throw new SDKError(`Unknown stealth profile: ${profileName}`);
|
|
133
115
|
}
|
|
134
|
-
let
|
|
116
|
+
let identifier;
|
|
117
|
+
let os;
|
|
135
118
|
try {
|
|
136
|
-
profile = getStealthProfile(profileName);
|
|
119
|
+
const profile = getStealthProfile(profileName);
|
|
120
|
+
identifier = profile.tlsClientIdentifier?.toLowerCase() ?? "";
|
|
121
|
+
os = profile.platform;
|
|
137
122
|
}
|
|
138
123
|
catch {
|
|
139
124
|
// Preserve the previous ctx.stealth.fetch() compatibility behavior: unknown
|
|
140
125
|
// profile strings still run with the transport default instead of failing
|
|
141
126
|
// before the request starts. Removed built-in profile aliases above remain
|
|
142
127
|
// explicit errors so callers do not accidentally pin retired fingerprints.
|
|
143
|
-
|
|
128
|
+
identifier = DEFAULT_WREQ_PROFILE_MAPPING.identifier;
|
|
129
|
+
os = DEFAULT_WREQ_PROFILE_MAPPING.os;
|
|
144
130
|
}
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return closestImpitBrowser(Number(chromeMatch[1]), CHROME_IMPIT_BY_MAJOR);
|
|
131
|
+
const browser = closestWreqProfile(identifier, wreqProfiles);
|
|
132
|
+
if (!browser) {
|
|
133
|
+
throw new SDKError(`Stealth profile "${profileName}" cannot be mapped to a wreq-js browser profile.`);
|
|
149
134
|
}
|
|
150
|
-
|
|
151
|
-
if (firefoxMatch?.[1]) {
|
|
152
|
-
return closestImpitBrowser(Number(firefoxMatch[1]), FIREFOX_IMPIT_BY_MAJOR);
|
|
153
|
-
}
|
|
154
|
-
if (identifier.startsWith("safari_")) {
|
|
155
|
-
throw new SDKError(`Stealth profile "${profileName}" uses a Safari stealth fingerprint, but TypeScript ctx.stealth uses impit which currently supports Chrome, Firefox, and OkHttp profiles only. Use a Chrome/Firefox stealth profile for ctx.stealth or ctx.browser for Safari-specific behavior.`);
|
|
156
|
-
}
|
|
157
|
-
throw new SDKError(`Stealth profile "${profileName}" cannot be mapped to an impit browser profile.`);
|
|
135
|
+
return { browser, os };
|
|
158
136
|
}
|
|
159
137
|
function resolveUrl(baseUrl, url) {
|
|
160
138
|
return new URL(url, baseUrl).toString();
|
|
@@ -174,18 +152,6 @@ function normalizeHeaders(headers) {
|
|
|
174
152
|
function hasOwn(object, key) {
|
|
175
153
|
return Object.hasOwn(object, key);
|
|
176
154
|
}
|
|
177
|
-
function toImpitCookieJar(cookieJar) {
|
|
178
|
-
return {
|
|
179
|
-
setCookie(cookie, _url, cb) {
|
|
180
|
-
cookieJar.setFromCookieStrings([cookie]);
|
|
181
|
-
if (typeof cb === "function")
|
|
182
|
-
cb();
|
|
183
|
-
},
|
|
184
|
-
getCookieString(_url) {
|
|
185
|
-
return cookieJar.toString();
|
|
186
|
-
},
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
155
|
function assertNoUnsupportedFingerprintOverrides(options) {
|
|
190
156
|
if (!isRecord(options))
|
|
191
157
|
return;
|
|
@@ -199,7 +165,7 @@ function assertNoUnsupportedFingerprintOverrides(options) {
|
|
|
199
165
|
unsupported.push("stealth.h2");
|
|
200
166
|
if (unsupported.length === 0)
|
|
201
167
|
return;
|
|
202
|
-
throw new SDKError(`ctx.stealth.fetch uses
|
|
168
|
+
throw new SDKError(`ctx.stealth.fetch uses transport-managed browser fingerprints and no longer accepts low-level stealth overrides: ${unsupported.join(", ")}. Use the profile option instead.`);
|
|
203
169
|
}
|
|
204
170
|
function responseHeadersToRecord(headers) {
|
|
205
171
|
const record = {};
|
|
@@ -234,10 +200,12 @@ function splitCombinedSetCookieHeader(headerValue) {
|
|
|
234
200
|
cookieStrings.push(finalCookie);
|
|
235
201
|
return cookieStrings;
|
|
236
202
|
}
|
|
237
|
-
export async function normalizeResponse(response, requestUrl) {
|
|
203
|
+
export async function normalizeResponse(response, requestUrl, maxBodyBytes) {
|
|
238
204
|
const headers = Object.fromEntries(response.headers.entries());
|
|
239
|
-
const cookies = new
|
|
240
|
-
const bodyBytes =
|
|
205
|
+
const cookies = new StealthCookieJar(setCookieHeadersFromResponse(response.headers), response.url ?? requestUrl);
|
|
206
|
+
const bodyBytes = maxBodyBytes === undefined
|
|
207
|
+
? await response.arrayBuffer()
|
|
208
|
+
: await readResponseBodyWithLimit(response, maxBodyBytes);
|
|
241
209
|
const body = new TextDecoder().decode(bodyBytes);
|
|
242
210
|
return {
|
|
243
211
|
status: response.status,
|
|
@@ -263,6 +231,63 @@ export async function normalizeResponse(response, requestUrl) {
|
|
|
263
231
|
},
|
|
264
232
|
};
|
|
265
233
|
}
|
|
234
|
+
function responseTooLargeError(maxBodyBytes, observedBytes) {
|
|
235
|
+
return new TransportError(`Response body exceeded maxBodyBytes limit of ${maxBodyBytes} bytes (observed ${observedBytes} bytes)`, {
|
|
236
|
+
code: "response_too_large",
|
|
237
|
+
category: "upstream_http",
|
|
238
|
+
retryable: false,
|
|
239
|
+
status: 0,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function declaredContentLength(headers) {
|
|
243
|
+
const contentLength = headers.get("content-length")?.trim();
|
|
244
|
+
if (!contentLength || !/^\d+$/.test(contentLength))
|
|
245
|
+
return undefined;
|
|
246
|
+
const parsed = Number(contentLength);
|
|
247
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
248
|
+
}
|
|
249
|
+
async function readResponseBodyWithLimit(response, maxBodyBytes) {
|
|
250
|
+
const contentLength = declaredContentLength(response.headers);
|
|
251
|
+
if (contentLength !== undefined && contentLength > maxBodyBytes) {
|
|
252
|
+
await response.body?.cancel().catch(() => undefined);
|
|
253
|
+
throw responseTooLargeError(maxBodyBytes, contentLength);
|
|
254
|
+
}
|
|
255
|
+
if (!response.body) {
|
|
256
|
+
throw new TransportError("Response body stream is unavailable", {
|
|
257
|
+
code: "transport_stream_unavailable",
|
|
258
|
+
category: "upstream_http",
|
|
259
|
+
status: 0,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
const reader = response.body.getReader();
|
|
263
|
+
const chunks = [];
|
|
264
|
+
let receivedBytes = 0;
|
|
265
|
+
try {
|
|
266
|
+
while (true) {
|
|
267
|
+
const { done, value } = await reader.read();
|
|
268
|
+
if (done)
|
|
269
|
+
break;
|
|
270
|
+
if (!value)
|
|
271
|
+
continue;
|
|
272
|
+
receivedBytes += value.byteLength;
|
|
273
|
+
if (receivedBytes > maxBodyBytes) {
|
|
274
|
+
await reader.cancel().catch(() => undefined);
|
|
275
|
+
throw responseTooLargeError(maxBodyBytes, receivedBytes);
|
|
276
|
+
}
|
|
277
|
+
chunks.push(value);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
reader.releaseLock();
|
|
282
|
+
}
|
|
283
|
+
const bodyBytes = new Uint8Array(receivedBytes);
|
|
284
|
+
let offset = 0;
|
|
285
|
+
for (const chunk of chunks) {
|
|
286
|
+
bodyBytes.set(chunk, offset);
|
|
287
|
+
offset += chunk.byteLength;
|
|
288
|
+
}
|
|
289
|
+
return bodyBytes.buffer;
|
|
290
|
+
}
|
|
266
291
|
function normalizeBody(body) {
|
|
267
292
|
if (body === undefined) {
|
|
268
293
|
return "";
|
|
@@ -420,16 +445,6 @@ function normalizeMethod(method) {
|
|
|
420
445
|
throw new SDKError(`Unsupported stealth method: ${method}`);
|
|
421
446
|
}
|
|
422
447
|
}
|
|
423
|
-
function isRedirectStatus(status) {
|
|
424
|
-
return [301, 302, 303, 307, 308].includes(status);
|
|
425
|
-
}
|
|
426
|
-
function nextRedirectMethod(status, method) {
|
|
427
|
-
if (status === 303 && method !== "HEAD")
|
|
428
|
-
return "GET";
|
|
429
|
-
if ((status === 301 || status === 302) && method === "POST")
|
|
430
|
-
return "GET";
|
|
431
|
-
return method;
|
|
432
|
-
}
|
|
433
448
|
function locationHeader(headers) {
|
|
434
449
|
for (const [name, value] of Object.entries(headers)) {
|
|
435
450
|
if (name.toLowerCase() === "location")
|
|
@@ -437,43 +452,153 @@ function locationHeader(headers) {
|
|
|
437
452
|
}
|
|
438
453
|
return undefined;
|
|
439
454
|
}
|
|
455
|
+
function withoutRedirectBodyHeaders(headers) {
|
|
456
|
+
return Object.fromEntries(Object.entries(headers).filter(([name]) => !REDIRECT_BODY_HEADERS.has(name.toLowerCase())));
|
|
457
|
+
}
|
|
458
|
+
function assertStealthRedirectUrl(url) {
|
|
459
|
+
const protocol = new URL(url).protocol;
|
|
460
|
+
if (protocol !== "http:" && protocol !== "https:") {
|
|
461
|
+
throw new TransportError(`Stealth redirect target scheme "${protocol}" is not allowed`, {
|
|
462
|
+
code: "transport_invalid_url",
|
|
463
|
+
status: 0,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function discardStealthRedirectBody(response) {
|
|
468
|
+
try {
|
|
469
|
+
const cancellation = response.body?.cancel();
|
|
470
|
+
if (cancellation)
|
|
471
|
+
void cancellation.catch(() => undefined);
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
// Redirect handling is decided from status and headers. A cancellation
|
|
475
|
+
// failure must not replace or delay that decision.
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async function fetchStealthRedirectChain(transport, cookieJar, requestUrl, method, options) {
|
|
479
|
+
let currentUrl = requestUrl;
|
|
480
|
+
let currentMethod = method;
|
|
481
|
+
let currentBody = options.body === undefined ? undefined : normalizeBody(options.body);
|
|
482
|
+
let currentHeaders = { ...(options.headers ?? {}) };
|
|
483
|
+
let followedHops = 0;
|
|
484
|
+
let response;
|
|
485
|
+
const deadline = options.timeout ? performance.now() + options.timeout : undefined;
|
|
486
|
+
while (true) {
|
|
487
|
+
const headers = { ...currentHeaders };
|
|
488
|
+
if (!hasHeader(headers, "Cookie")) {
|
|
489
|
+
const cookieHeader = cookieJar.toHeader(currentUrl);
|
|
490
|
+
if (cookieHeader)
|
|
491
|
+
headers.Cookie = cookieHeader;
|
|
492
|
+
}
|
|
493
|
+
const requestInit = {
|
|
494
|
+
headers: normalizeHeaders(headers),
|
|
495
|
+
method: currentMethod,
|
|
496
|
+
redirect: "manual",
|
|
497
|
+
};
|
|
498
|
+
if (currentBody !== undefined)
|
|
499
|
+
requestInit.body = currentBody;
|
|
500
|
+
await transport.clearCookies();
|
|
501
|
+
const remainingTimeout = deadline === undefined ? undefined : Math.ceil(deadline - performance.now());
|
|
502
|
+
if (remainingTimeout !== undefined && remainingTimeout <= 0) {
|
|
503
|
+
throw new TransportError("Request timed out", {
|
|
504
|
+
code: "transport_timeout",
|
|
505
|
+
status: 0,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
if (remainingTimeout !== undefined)
|
|
509
|
+
requestInit.timeout = remainingTimeout;
|
|
510
|
+
response = await transport.fetch(currentUrl, requestInit);
|
|
511
|
+
cookieJar.setFromCookieStrings(setCookieHeadersFromResponse(response.headers), response.url ?? currentUrl);
|
|
512
|
+
if (!isRedirectStatus(response.status) || options.redirect === "manual")
|
|
513
|
+
break;
|
|
514
|
+
if (options.redirect === "error") {
|
|
515
|
+
discardStealthRedirectBody(response);
|
|
516
|
+
throw new TransportError("Stealth request encountered a redirect", {
|
|
517
|
+
code: "transport_network_error",
|
|
518
|
+
status: 0,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
const nextUrl = resolveRedirectUrl(response.headers.get("location") ?? undefined, response.url ?? currentUrl);
|
|
522
|
+
if (!nextUrl)
|
|
523
|
+
break;
|
|
524
|
+
if (followedHops >= MAX_STEALTH_REDIRECT_HOPS) {
|
|
525
|
+
discardStealthRedirectBody(response);
|
|
526
|
+
throw new TransportError(`Stealth request exceeded the ${MAX_STEALTH_REDIRECT_HOPS}-redirect limit`, { code: "transport_network_error", status: 0 });
|
|
527
|
+
}
|
|
528
|
+
assertStealthRedirectUrl(nextUrl);
|
|
529
|
+
discardStealthRedirectBody(response);
|
|
530
|
+
const nextMethod = nextRedirectMethod(response.status, currentMethod);
|
|
531
|
+
if (nextMethod !== currentMethod) {
|
|
532
|
+
currentBody = undefined;
|
|
533
|
+
currentHeaders = withoutRedirectBodyHeaders(currentHeaders);
|
|
534
|
+
}
|
|
535
|
+
currentMethod = nextMethod;
|
|
536
|
+
currentUrl = nextUrl;
|
|
537
|
+
followedHops += 1;
|
|
538
|
+
}
|
|
539
|
+
const normalized = await normalizeResponse(response, currentUrl, options.maxBodyBytes);
|
|
540
|
+
if (followedHops > 0)
|
|
541
|
+
normalized.redirected = true;
|
|
542
|
+
return { normalized, response };
|
|
543
|
+
}
|
|
440
544
|
function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
441
545
|
const clients = new Map();
|
|
442
546
|
let closed = false;
|
|
443
547
|
let hasWarnedMissingProxy = false;
|
|
444
548
|
const warn = clientOptions.warn ?? console.warn;
|
|
445
|
-
const cookieJar = new
|
|
446
|
-
|
|
447
|
-
function getClient(profileName, proxyUrl, ignoreTlsErrors) {
|
|
549
|
+
const cookieJar = new StealthCookieJar([], baseUrl);
|
|
550
|
+
async function getClientEntry(profileName, proxyUrl, ignoreTlsErrors) {
|
|
448
551
|
if (closed) {
|
|
449
552
|
throw new TransportError("Stealth session is closed", { status: 0 });
|
|
450
553
|
}
|
|
451
|
-
const
|
|
554
|
+
const wreq = await getWreqModule();
|
|
555
|
+
const { browser, os } = resolveWreqProfile(profileName, wreq.getProfiles());
|
|
452
556
|
const cacheKey = JSON.stringify({ browser, proxyUrl, ignoreTlsErrors });
|
|
453
|
-
let
|
|
454
|
-
if (!
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
557
|
+
let entry = clients.get(cacheKey);
|
|
558
|
+
if (!entry) {
|
|
559
|
+
entry = {
|
|
560
|
+
session: wreq.createSession({
|
|
561
|
+
browser,
|
|
562
|
+
os,
|
|
563
|
+
...(proxyUrl ? { proxy: proxyUrl } : {}),
|
|
564
|
+
...(ignoreTlsErrors ? { insecure: true } : {}),
|
|
565
|
+
timeout: 30_000,
|
|
566
|
+
}),
|
|
567
|
+
tail: Promise.resolve(),
|
|
568
|
+
};
|
|
569
|
+
clients.set(cacheKey, entry);
|
|
570
|
+
}
|
|
571
|
+
return entry;
|
|
572
|
+
}
|
|
573
|
+
async function withClient(profileName, proxyUrl, ignoreTlsErrors, operation) {
|
|
574
|
+
const entry = await getClientEntry(profileName, proxyUrl, ignoreTlsErrors);
|
|
575
|
+
const previous = entry.tail;
|
|
576
|
+
let release;
|
|
577
|
+
entry.tail = new Promise((resolve) => {
|
|
578
|
+
release = resolve;
|
|
579
|
+
});
|
|
580
|
+
await previous;
|
|
581
|
+
try {
|
|
582
|
+
return await operation(await entry.session);
|
|
583
|
+
}
|
|
584
|
+
finally {
|
|
585
|
+
release();
|
|
463
586
|
}
|
|
464
|
-
return client;
|
|
465
587
|
}
|
|
466
|
-
async function resolveRequestProxy(options, proxyAttempt) {
|
|
588
|
+
async function resolveRequestProxy(options, proxyAttempt, refreshEpoch) {
|
|
467
589
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
468
590
|
proxy: options?.proxy ?? clientOptions.proxy,
|
|
469
591
|
upstream: clientOptions.upstream,
|
|
470
|
-
apifuseConfig: clientOptions.apifuseConfig,
|
|
471
592
|
affinityKey: clientOptions.affinityKey,
|
|
472
593
|
proxyAttempt: computeProxyAttemptIndex({
|
|
473
594
|
baseProxyAttempt: clientOptions.proxyAttempt,
|
|
474
595
|
proxyAttemptOffset: options?.proxyAttemptOffset,
|
|
475
596
|
retryAttemptOffset: proxyAttempt,
|
|
476
597
|
}),
|
|
598
|
+
// The stealth transport tunnels both HTTP CONNECT and SOCKS5,
|
|
599
|
+
// preserving the client TLS fingerprint end-to-end.
|
|
600
|
+
transportProtocols: ["http", "socks5"],
|
|
601
|
+
...(refreshEpoch === undefined ? {} : { proxyRefreshEpoch: refreshEpoch }),
|
|
477
602
|
telemetry: clientOptions.telemetry,
|
|
478
603
|
});
|
|
479
604
|
if (resolvedProxy.shouldWarn && !hasWarnedMissingProxy) {
|
|
@@ -484,34 +609,57 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
484
609
|
url: resolvedProxy.url,
|
|
485
610
|
poolIndex: proxyPoolIndexFromDiagnostics(resolvedProxy.diagnostics),
|
|
486
611
|
proxyHash: proxyEndpointHash(resolvedProxy.url),
|
|
612
|
+
vendor: vendorFromResolvedSource(resolvedProxy.source),
|
|
487
613
|
};
|
|
488
614
|
}
|
|
489
615
|
const session = {
|
|
490
616
|
async fetch(url, options = {}) {
|
|
491
|
-
const method =
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}) ??
|
|
497
|
-
(hasExplicitRetryPolicy
|
|
498
|
-
? undefined
|
|
499
|
-
: createDefaultProxyTransportRetryOptions({
|
|
617
|
+
const { hasExplicitRetryPolicy, method, stealthRetryOptions } = (() => {
|
|
618
|
+
try {
|
|
619
|
+
const method = normalizeMethod(options.method ?? "GET");
|
|
620
|
+
const hasExplicitRetryPolicy = options.retry !== undefined;
|
|
621
|
+
const stealthRetryOptions = normalizeProxyTransportRetryOptions(options.retry, {
|
|
500
622
|
extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
|
|
501
623
|
label: "Stealth",
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
624
|
+
}) ??
|
|
625
|
+
(hasExplicitRetryPolicy
|
|
626
|
+
? undefined
|
|
627
|
+
: createDefaultProxyTransportRetryOptions({
|
|
628
|
+
extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
|
|
629
|
+
label: "Stealth",
|
|
630
|
+
}));
|
|
631
|
+
if (stealthRetryOptions) {
|
|
632
|
+
validateUnsafeProxyTransportRetryMethods(stealthRetryOptions, "Stealth");
|
|
633
|
+
}
|
|
634
|
+
return { hasExplicitRetryPolicy, method, stealthRetryOptions };
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
638
|
+
}
|
|
639
|
+
})();
|
|
506
640
|
const hasPolicyProxy = isPolicyManagedProxy(clientOptions);
|
|
507
641
|
const usesPolicyAllocator = hasPolicyProxy && !options.proxy && !clientOptions.proxy;
|
|
508
642
|
const retryAttemptCap = Math.max(1, stealthRetryOptions?.attempts ?? 1);
|
|
509
|
-
|
|
643
|
+
// Span the whole vendor chain: successive attempts rotate one vendor's
|
|
644
|
+
// pool, then fail over to the next vendor via the flat attempt index.
|
|
645
|
+
const policyProxy = clientOptions.proxyPolicy ??
|
|
510
646
|
(typeof clientOptions.upstream?.proxy === "object"
|
|
511
|
-
? clientOptions.upstream.proxy
|
|
512
|
-
: undefined)
|
|
513
|
-
|
|
514
|
-
|
|
647
|
+
? clientOptions.upstream.proxy
|
|
648
|
+
: undefined);
|
|
649
|
+
// The pool span is already bounded by each vendor's max pool size
|
|
650
|
+
// (smartproxy ≤20, nodemaven ≤50), so the configured span never exceeds
|
|
651
|
+
// the chain's true maximum — a large NodeMaven pool stays fully
|
|
652
|
+
// reachable rather than being truncated at an arbitrary ceiling.
|
|
653
|
+
const policyProxyAttemptCap = Math.max(1, policyProxy ? resolvePolicyProxyPoolSpan(policyProxy) : DEFAULT_SMARTPROXY_POOL_SIZE);
|
|
654
|
+
// A registry vendor chain (smartproxy/nodemaven) is the only policy whose
|
|
655
|
+
// successive attempts resolve a *different* endpoint, so it is the only one
|
|
656
|
+
// that may widen the attempt cap to the pool span, de-duplicate endpoints,
|
|
657
|
+
// and drive allocator stale-pool refresh. Deprecated custom/decodo policies
|
|
658
|
+
// have no managed endpoint to rotate or refresh, so they follow the ordinary
|
|
659
|
+
// transport-retry budget instead.
|
|
660
|
+
const rotatesRegistryChain = usesPolicyAllocator && policyResolvesRegistryVendorChain(policyProxy);
|
|
661
|
+
const maxAttempts = rotatesRegistryChain ? policyProxyAttemptCap : retryAttemptCap;
|
|
662
|
+
const dedupeAllocatorEndpoints = rotatesRegistryChain;
|
|
515
663
|
let lastError;
|
|
516
664
|
for (let refreshAttempt = 0; refreshAttempt <= MAX_POLICY_PROXY_POOL_REFRESHES; refreshAttempt += 1) {
|
|
517
665
|
let stalePoolError;
|
|
@@ -520,6 +668,11 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
520
668
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
521
669
|
let proxy;
|
|
522
670
|
let attemptProxy;
|
|
671
|
+
// Reuse the exact serialization used by this outbound attempt in its catch path.
|
|
672
|
+
let serializedUrl;
|
|
673
|
+
let fallbackSensitiveValues = [];
|
|
674
|
+
let fallbackRequestUrl;
|
|
675
|
+
let fallbackRedactedUrl;
|
|
523
676
|
const attemptStartedAt = Date.now();
|
|
524
677
|
let attemptRecorded = false;
|
|
525
678
|
const recordProxyAttempt = (outcome, errorCode, status) => {
|
|
@@ -527,7 +680,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
527
680
|
return;
|
|
528
681
|
attemptRecorded = true;
|
|
529
682
|
clientOptions.telemetry?.recordProxyAttempt?.({
|
|
530
|
-
provider: "smartproxy",
|
|
683
|
+
provider: attemptProxy?.vendor ?? "smartproxy",
|
|
531
684
|
attempt: attempt + 1,
|
|
532
685
|
...(attemptProxy?.poolIndex === undefined
|
|
533
686
|
? {}
|
|
@@ -540,37 +693,36 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
540
693
|
});
|
|
541
694
|
};
|
|
542
695
|
try {
|
|
696
|
+
const sensitiveParams = normalizeSensitiveParams(options.sensitiveParams);
|
|
697
|
+
const structural = redactUrlQueryParams(url, Object.keys(sensitiveParams ?? {}));
|
|
698
|
+
fallbackSensitiveValues = [
|
|
699
|
+
...new Set([
|
|
700
|
+
...Object.values(sensitiveParams ?? {}).map(String),
|
|
701
|
+
...structural.sensitiveValues,
|
|
702
|
+
]),
|
|
703
|
+
].filter((value) => value !== "");
|
|
704
|
+
fallbackRequestUrl = url;
|
|
705
|
+
fallbackRedactedUrl = structural.redactedUrl;
|
|
543
706
|
assertNoUnsupportedFingerprintOverrides(options);
|
|
544
|
-
attemptProxy = await resolveRequestProxy(options, attempt);
|
|
707
|
+
attemptProxy = await resolveRequestProxy(options, attempt, refreshAttempt);
|
|
545
708
|
proxy = attemptProxy.url;
|
|
546
|
-
if (proxy &&
|
|
709
|
+
if (proxy && dedupeAllocatorEndpoints) {
|
|
710
|
+
// An under-filled allocation repeats endpoints (via the modulo
|
|
711
|
+
// pool mapping) before the flat offset crosses into the next
|
|
712
|
+
// vendor. Skip an already-tried endpoint and advance the offset
|
|
713
|
+
// rather than breaking — breaking here would strand the request on
|
|
714
|
+
// the primary vendor and never reach the fallback leg.
|
|
547
715
|
if (attemptedProxies.has(proxy)) {
|
|
548
|
-
|
|
716
|
+
continue;
|
|
549
717
|
}
|
|
550
718
|
attemptedProxies.add(proxy);
|
|
551
719
|
}
|
|
552
720
|
const ignoreTlsErrors = Boolean(options.stealth?.insecureSkipVerify ??
|
|
553
721
|
(!hasPolicyProxy && proxy && clientOptions.proxyStealth?.insecureSkipVerify));
|
|
554
722
|
const profileName = options.profile ?? defaultProfile;
|
|
555
|
-
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
const cookieHeader = cookieJar.toString();
|
|
559
|
-
if (cookieHeader)
|
|
560
|
-
headers.Cookie = cookieHeader;
|
|
561
|
-
}
|
|
562
|
-
const requestInit = {
|
|
563
|
-
headers: normalizeHeaders(headers),
|
|
564
|
-
method,
|
|
565
|
-
...(options.redirect ? { redirect: options.redirect } : {}),
|
|
566
|
-
...(options.timeout ? { timeout: options.timeout } : {}),
|
|
567
|
-
};
|
|
568
|
-
if (options.body !== undefined) {
|
|
569
|
-
requestInit.body = normalizeBody(options.body);
|
|
570
|
-
}
|
|
571
|
-
const response = await getClient(profileName, proxy, ignoreTlsErrors).fetch(requestUrl, requestInit);
|
|
572
|
-
const normalized = await normalizeResponse(response, requestUrl);
|
|
573
|
-
cookieJar.setFromCookieStrings(setCookieHeadersFromResponse(response.headers));
|
|
723
|
+
serializedUrl = serializeRequestUrl(resolveUrl(baseUrl, url), options.params, sensitiveParams);
|
|
724
|
+
const { requestUrl } = serializedUrl;
|
|
725
|
+
const { normalized, response } = await withClient(profileName, proxy, ignoreTlsErrors, (transport) => fetchStealthRedirectChain(transport, cookieJar, requestUrl, method, options));
|
|
574
726
|
if (proxy && isProxyConnectFailureResponse(response, normalized.body)) {
|
|
575
727
|
throw createProxyConnectFailureError(normalized.body);
|
|
576
728
|
}
|
|
@@ -602,12 +754,23 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
602
754
|
return normalized;
|
|
603
755
|
}
|
|
604
756
|
catch (error) {
|
|
605
|
-
const
|
|
757
|
+
const sensitiveValues = serializedUrl?.sensitiveValues ?? fallbackSensitiveValues;
|
|
758
|
+
let normalizedError;
|
|
759
|
+
try {
|
|
760
|
+
normalizedError = normalizeStealthTransportError(error);
|
|
761
|
+
}
|
|
762
|
+
catch (normalizationError) {
|
|
763
|
+
throw redactSensitiveError(normalizationError, sensitiveValues, serializedUrl?.requestUrl ?? fallbackRequestUrl, serializedUrl?.redactedUrl ?? fallbackRedactedUrl);
|
|
764
|
+
}
|
|
765
|
+
const retryErrorCode = proxyAttemptErrorCode(normalizedError);
|
|
766
|
+
const refreshableProxyError = isProxyPoolRefreshableError(normalizedError);
|
|
767
|
+
const runProxyAuthDiagnostic = shouldRunProxyAuthDiagnostic(normalizedError);
|
|
768
|
+
normalizedError = redactSensitiveError(normalizedError, sensitiveValues, serializedUrl?.requestUrl ?? fallbackRequestUrl, serializedUrl?.redactedUrl ?? fallbackRedactedUrl);
|
|
606
769
|
recordProxyAttempt("error", proxyAttemptErrorCode(normalizedError), proxyAttemptStatus(normalizedError));
|
|
607
770
|
lastError = normalizedError;
|
|
608
|
-
if (proxy &&
|
|
771
|
+
if (proxy && rotatesRegistryChain && refreshableProxyError) {
|
|
609
772
|
stalePoolError = normalizedError;
|
|
610
|
-
if (
|
|
773
|
+
if (runProxyAuthDiagnostic) {
|
|
611
774
|
stalePoolDiagnosticProxy = proxy;
|
|
612
775
|
}
|
|
613
776
|
if (attempt + 1 < maxAttempts) {
|
|
@@ -615,12 +778,29 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
615
778
|
}
|
|
616
779
|
break;
|
|
617
780
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
781
|
+
// Cap the number of transport retries. For a policy-allocator chain,
|
|
782
|
+
// every attempt resolves a *different* endpoint/vendor (poolIndex
|
|
783
|
+
// rotates across the concatenated vendor pool spans), so a transport
|
|
784
|
+
// failure is a signal to advance to the next endpoint — potentially
|
|
785
|
+
// crossing into the fallback vendor — not to retry the same endpoint.
|
|
786
|
+
// Truncating that rotation at the per-endpoint retry budget would
|
|
787
|
+
// strand the request on the primary vendor and never reach the
|
|
788
|
+
// fallback, since the crossover only happens once the flat attempt
|
|
789
|
+
// index exceeds the primary vendor's pool size (~10-20).
|
|
790
|
+
// resolvePolicyTransportAttemptCap widens to the full chain span only
|
|
791
|
+
// for implicit, safe-method allocator requests; explicit retry
|
|
792
|
+
// policies (their documented `attempts` ceiling), unsafe methods, and
|
|
793
|
+
// static/non-registry vendors keep the per-endpoint retry budget.
|
|
794
|
+
const transportRetryCap = resolvePolicyTransportAttemptCap({
|
|
795
|
+
policy: policyProxy,
|
|
796
|
+
usesPolicyAllocator,
|
|
797
|
+
retryAttempts: stealthRetryOptions?.attempts ?? 1,
|
|
798
|
+
explicitRetry: hasExplicitRetryPolicy,
|
|
799
|
+
method,
|
|
800
|
+
});
|
|
801
|
+
if (attempt + 1 < transportRetryCap &&
|
|
622
802
|
shouldRetryProxyTransportAttempt({
|
|
623
|
-
error:
|
|
803
|
+
error: { code: retryErrorCode },
|
|
624
804
|
explicitRetry: hasExplicitRetryPolicy,
|
|
625
805
|
method,
|
|
626
806
|
options: stealthRetryOptions,
|
|
@@ -634,7 +814,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
634
814
|
throw normalizedError;
|
|
635
815
|
}
|
|
636
816
|
}
|
|
637
|
-
if (
|
|
817
|
+
if (rotatesRegistryChain &&
|
|
638
818
|
stalePoolError &&
|
|
639
819
|
refreshAttempt < MAX_POLICY_PROXY_POOL_REFRESHES) {
|
|
640
820
|
await invalidateProxyResolutionCacheAsync({
|
|
@@ -670,81 +850,153 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
670
850
|
const maxHops = options.maxHops === undefined || !Number.isFinite(options.maxHops)
|
|
671
851
|
? 10
|
|
672
852
|
: Math.max(0, Math.floor(options.maxHops));
|
|
853
|
+
const { url: _url, maxHops: _maxHops, stopWhen, params, sensitiveParams, ...fetchOptions } = options;
|
|
673
854
|
const hops = [];
|
|
674
|
-
let currentUrl = resolveUrl(baseUrl, options.url);
|
|
675
855
|
let method = normalizeMethod(options.method ?? "GET");
|
|
676
856
|
let body = options.body;
|
|
677
857
|
let response;
|
|
678
858
|
const visitedRequests = new Set();
|
|
679
|
-
const
|
|
859
|
+
const initialParams = params
|
|
860
|
+
? Object.fromEntries(Object.entries(params).map(([key, value]) => [
|
|
861
|
+
key,
|
|
862
|
+
Array.isArray(value) ? [...value] : value,
|
|
863
|
+
]))
|
|
864
|
+
: undefined;
|
|
865
|
+
const normalizedSensitiveParams = normalizeSensitiveParams(sensitiveParams);
|
|
866
|
+
const initialSensitiveParams = normalizedSensitiveParams
|
|
867
|
+
? { ...normalizedSensitiveParams }
|
|
868
|
+
: undefined;
|
|
869
|
+
const sensitiveParamNames = initialSensitiveParams
|
|
870
|
+
? Object.keys(initialSensitiveParams)
|
|
871
|
+
: [];
|
|
872
|
+
const callerStructural = redactUrlQueryParams(options.url, sensitiveParamNames);
|
|
873
|
+
const sensitiveValues = new Set([
|
|
874
|
+
...Object.values(initialSensitiveParams ?? {}),
|
|
875
|
+
...callerStructural.sensitiveValues,
|
|
876
|
+
].filter((value) => value !== ""));
|
|
877
|
+
const redactRedirectUrl = (value) => {
|
|
878
|
+
const structural = redactUrlQueryParams(value, [
|
|
879
|
+
...new Set([...sensitiveParamNames, ...sensitiveQueryParamNames(value)]),
|
|
880
|
+
]);
|
|
881
|
+
for (const sensitiveValue of structural.sensitiveValues) {
|
|
882
|
+
sensitiveValues.add(sensitiveValue);
|
|
883
|
+
}
|
|
884
|
+
return redactSensitiveText(structural.redactedUrl, [...sensitiveValues]);
|
|
885
|
+
};
|
|
886
|
+
let currentUrl;
|
|
887
|
+
let initialUrl;
|
|
888
|
+
try {
|
|
889
|
+
currentUrl = resolveUrl(baseUrl, options.url);
|
|
890
|
+
redactRedirectUrl(currentUrl);
|
|
891
|
+
initialUrl = serializeRequestUrl(currentUrl, initialParams, initialSensitiveParams);
|
|
892
|
+
for (const value of initialUrl.sensitiveValues) {
|
|
893
|
+
if (value !== "")
|
|
894
|
+
sensitiveValues.add(value);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
catch (error) {
|
|
898
|
+
throw redactSensitiveError(error, [...sensitiveValues], options.url, redactRedirectUrl(options.url));
|
|
899
|
+
}
|
|
680
900
|
for (let hopIndex = 0; hopIndex <= maxHops; hopIndex += 1) {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
901
|
+
const outboundUrl = hopIndex === 0 ? initialUrl.requestUrl : serializeRequestUrl(currentUrl).requestUrl;
|
|
902
|
+
// Preserve params-only loop bookkeeping from before sensitiveParams:
|
|
903
|
+
// the first visited key is the caller's resolved URL, not its expanded query.
|
|
904
|
+
const visitedUrl = hopIndex === 0 && !initialSensitiveParams ? currentUrl : outboundUrl;
|
|
905
|
+
visitedRequests.add(`${method} ${visitedUrl}`);
|
|
906
|
+
try {
|
|
907
|
+
response = await session.fetch(currentUrl, {
|
|
908
|
+
...fetchOptions,
|
|
909
|
+
body,
|
|
910
|
+
method,
|
|
911
|
+
...(hopIndex === 0 && initialParams ? { params: initialParams } : {}),
|
|
912
|
+
...(hopIndex === 0 && initialSensitiveParams
|
|
913
|
+
? { sensitiveParams: initialSensitiveParams }
|
|
914
|
+
: {}),
|
|
915
|
+
redirect: "manual",
|
|
916
|
+
throwOnHttpError: false,
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
throw redactSensitiveError(error, [...sensitiveValues], outboundUrl, redactRedirectUrl(outboundUrl));
|
|
921
|
+
}
|
|
922
|
+
// StealthResponse.url is programmatic metadata and remains raw. Only the
|
|
923
|
+
// redirect hop emitted below is a diagnostic surface.
|
|
924
|
+
const responseUrl = response.url ?? (hopIndex === 0 && initialSensitiveParams ? outboundUrl : currentUrl);
|
|
690
925
|
if (!isRedirectStatus(response.status)) {
|
|
691
926
|
return {
|
|
692
927
|
final: response,
|
|
693
928
|
hops,
|
|
694
929
|
reason: "completed",
|
|
695
930
|
cookies: cookieJar.snapshot(),
|
|
931
|
+
cookieStore: cookieJar.serialize(),
|
|
696
932
|
};
|
|
697
933
|
}
|
|
698
934
|
const location = locationHeader(response.headers);
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
935
|
+
const redactedResponseUrl = redactRedirectUrl(responseUrl);
|
|
936
|
+
const redactedLocation = location ? redactRedirectUrl(location) : undefined;
|
|
937
|
+
let nextUrl;
|
|
938
|
+
try {
|
|
939
|
+
nextUrl = resolveRedirectUrl(location, responseUrl);
|
|
940
|
+
}
|
|
941
|
+
catch (error) {
|
|
942
|
+
throw redactSensitiveError(error, [...sensitiveValues], location, redactedLocation);
|
|
943
|
+
}
|
|
944
|
+
const realHop = {
|
|
945
|
+
url: responseUrl,
|
|
704
946
|
status: response.status,
|
|
705
947
|
method,
|
|
706
948
|
...(location ? { location } : {}),
|
|
707
949
|
...(nextUrl ? { nextUrl } : {}),
|
|
708
950
|
};
|
|
951
|
+
const hop = {
|
|
952
|
+
...realHop,
|
|
953
|
+
url: redactedResponseUrl,
|
|
954
|
+
...(redactedLocation ? { location: redactedLocation } : {}),
|
|
955
|
+
...(nextUrl ? { nextUrl: redactRedirectUrl(nextUrl) } : {}),
|
|
956
|
+
};
|
|
709
957
|
hops.push(hop);
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
958
|
+
let shouldStop = false;
|
|
959
|
+
if (stopWhen) {
|
|
960
|
+
try {
|
|
961
|
+
shouldStop = await stopWhen(realHop);
|
|
962
|
+
}
|
|
963
|
+
catch (error) {
|
|
964
|
+
let sanitizedError = error;
|
|
965
|
+
for (const [rawUrl, safeUrl] of [
|
|
966
|
+
[responseUrl, redactedResponseUrl],
|
|
967
|
+
[location, redactedLocation],
|
|
968
|
+
[nextUrl, nextUrl ? redactRedirectUrl(nextUrl) : undefined],
|
|
969
|
+
]) {
|
|
970
|
+
if (!rawUrl || !safeUrl)
|
|
971
|
+
continue;
|
|
972
|
+
sanitizedError = redactSensitiveError(sanitizedError, [...sensitiveValues], rawUrl, safeUrl);
|
|
973
|
+
}
|
|
974
|
+
throw sanitizedError;
|
|
975
|
+
}
|
|
725
976
|
}
|
|
726
|
-
|
|
977
|
+
const decision = evaluateRedirectHop({
|
|
978
|
+
status: response.status,
|
|
979
|
+
method,
|
|
980
|
+
nextUrl,
|
|
981
|
+
shouldStop,
|
|
982
|
+
redirectCount: hops.length,
|
|
983
|
+
maxHops,
|
|
984
|
+
visitedRequests,
|
|
985
|
+
});
|
|
986
|
+
if (decision.kind === "stop") {
|
|
727
987
|
return {
|
|
728
988
|
final: response,
|
|
729
989
|
hops,
|
|
730
|
-
reason:
|
|
990
|
+
reason: decision.reason,
|
|
731
991
|
cookies: cookieJar.snapshot(),
|
|
992
|
+
cookieStore: cookieJar.serialize(),
|
|
732
993
|
};
|
|
733
994
|
}
|
|
734
|
-
|
|
735
|
-
if (nextMethod !== method) {
|
|
995
|
+
if (decision.nextMethod !== method) {
|
|
736
996
|
body = undefined;
|
|
737
997
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
final: response,
|
|
741
|
-
hops,
|
|
742
|
-
reason: "loop",
|
|
743
|
-
cookies: cookieJar.snapshot(),
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
method = nextMethod;
|
|
747
|
-
currentUrl = nextUrl;
|
|
998
|
+
method = decision.nextMethod;
|
|
999
|
+
currentUrl = decision.nextUrl;
|
|
748
1000
|
}
|
|
749
1001
|
if (!response) {
|
|
750
1002
|
response = await session.fetch(currentUrl, {
|
|
@@ -761,23 +1013,35 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
761
1013
|
hops,
|
|
762
1014
|
reason: "max_hops",
|
|
763
1015
|
cookies: cookieJar.snapshot(),
|
|
1016
|
+
cookieStore: cookieJar.serialize(),
|
|
764
1017
|
};
|
|
765
1018
|
},
|
|
766
1019
|
},
|
|
767
1020
|
close() {
|
|
768
1021
|
closed = true;
|
|
1022
|
+
for (const client of clients.values()) {
|
|
1023
|
+
void client.session
|
|
1024
|
+
.then((session) => session.close())
|
|
1025
|
+
.catch((error) => {
|
|
1026
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1027
|
+
warn(`[provider-sdk] Failed to close stealth transport session: ${message}`);
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
769
1030
|
clients.clear();
|
|
770
1031
|
},
|
|
771
1032
|
};
|
|
772
1033
|
return session;
|
|
773
1034
|
async function classifyProxyAuthDiagnostic(profileName, proxy) {
|
|
774
1035
|
try {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1036
|
+
return await withClient(profileName, proxy, false, async (client) => {
|
|
1037
|
+
await client.clearCookies();
|
|
1038
|
+
const response = await client.fetch(PROXY_AUTH_DIAGNOSTIC_URL, {
|
|
1039
|
+
method: "GET",
|
|
1040
|
+
timeout: PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS,
|
|
1041
|
+
});
|
|
1042
|
+
const normalized = await normalizeResponse(response);
|
|
1043
|
+
return classifyProxyAuthDiagnosticMessage(normalized.body);
|
|
778
1044
|
});
|
|
779
|
-
const normalized = await normalizeResponse(response);
|
|
780
|
-
return classifyProxyAuthDiagnosticMessage(normalized.body);
|
|
781
1045
|
}
|
|
782
1046
|
catch (error) {
|
|
783
1047
|
const message = error instanceof Error
|