@apifuse/provider-sdk 2.1.0-beta.9 → 2.2.0-beta.10
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 +240 -0
- package/CHANGELOG.md +93 -0
- package/README.md +26 -10
- package/SUBMISSION.md +11 -12
- package/bin/apifuse-check.ts +44 -59
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +27 -52
- package/bin/apifuse-pack-check.ts +36 -0
- package/bin/apifuse-pack-smoke.ts +22 -81
- package/bin/apifuse-pack-types.ts +266 -0
- package/bin/apifuse-perf.ts +45 -127
- package/bin/apifuse-record.ts +53 -70
- package/bin/apifuse-submit-check.ts +2177 -353
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/apifuse.ts +1 -1
- package/bin/submit-check-delimited-text.ts +50 -0
- package/bin/submit-check-xml-semantics.ts +204 -0
- package/bin/submit-check-xml.ts +134 -0
- package/dist/auth-turn/auth-turn.v1.schema.json +89 -0
- package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
- package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
- package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
- package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
- package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
- package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
- package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
- package/dist/auth-turn/fixtures/valid/abort.json +8 -0
- package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
- package/dist/auth-turn/fixtures/valid/complete.json +13 -0
- package/dist/auth-turn/fixtures/valid/form.json +14 -0
- package/dist/auth-turn/fixtures/valid/message.json +13 -0
- package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
- package/dist/auth-turn/fixtures/valid/pending.json +5 -0
- package/dist/auth-turn/fixtures/valid/poll.json +9 -0
- package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
- package/dist/auth-turn/fixtures/valid/retry.json +8 -0
- package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
- package/dist/auth-turn/index.d.ts +195 -0
- package/dist/auth-turn/index.js +133 -0
- package/dist/auth.d.ts +76 -0
- package/dist/auth.js +427 -0
- package/dist/ceremonies/index.d.ts +1 -1
- package/dist/ceremonies/index.js +14 -48
- 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 +47 -33
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/dist/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/dist/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/dist/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/dist/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/dist/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl +13 -0
- package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
- package/dist/cli/templates/provider/.gitignore.tpl +22 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +96 -0
- package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
- package/dist/cli/templates/provider/README.md.tpl +163 -0
- package/dist/cli/templates/provider/dev.ts.tpl +5 -0
- package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
- package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
- package/dist/cli/templates/provider/index.ts.tpl +15 -0
- package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/dist/cli/templates/provider/meta.ts.tpl +7 -0
- package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/start.ts.tpl +5 -0
- package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/dist/config/loader.d.ts +149 -8
- package/dist/config/loader.js +378 -89
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +3 -6
- package/dist/contract-types.d.ts +2 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +5 -6
- package/dist/define.d.ts +13 -1
- package/dist/define.js +245 -178
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/errors.d.ts +4 -1
- package/dist/errors.js +48 -0
- package/dist/i18n/catalog.d.ts +2 -2
- package/dist/i18n/catalog.js +4 -10
- package/dist/i18n/index.d.ts +2 -2
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/keys.d.ts +2 -2
- package/dist/index.d.ts +44 -41
- package/dist/index.js +39 -36
- package/dist/lint.d.ts +1 -0
- package/dist/lint.js +35 -15
- package/dist/provider.d.ts +11 -9
- package/dist/provider.js +9 -8
- package/dist/public-schema-field-lint.d.ts +1 -1
- package/dist/recipes/gov-api.js +1 -1
- package/dist/runtime/auth-flow.d.ts +1 -1
- package/dist/runtime/auth-flow.js +4 -2
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +214 -25
- package/dist/runtime/cache.d.ts +1 -1
- package/dist/runtime/cache.js +4 -8
- package/dist/runtime/choice.d.ts +1 -1
- package/dist/runtime/choice.js +31 -35
- package/dist/runtime/credential.d.ts +1 -1
- package/dist/runtime/credential.js +1 -1
- package/dist/runtime/env.d.ts +1 -1
- package/dist/runtime/executor.d.ts +1 -1
- package/dist/runtime/executor.js +15 -3
- package/dist/runtime/http.d.ts +2 -2
- package/dist/runtime/http.js +160 -344
- package/dist/runtime/insights.d.ts +1 -1
- package/dist/runtime/insights.js +6 -13
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +56 -19
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- package/dist/runtime/otlp.d.ts +1 -1
- package/dist/runtime/perf.d.ts +1 -1
- package/dist/runtime/provider.d.ts +1 -1
- package/dist/runtime/provider.js +1 -2
- package/dist/runtime/proxy-errors.d.ts +1 -1
- package/dist/runtime/proxy-errors.js +9 -7
- package/dist/runtime/proxy-nodemaven.d.ts +35 -0
- package/dist/runtime/proxy-nodemaven.js +128 -0
- package/dist/runtime/proxy-retry-policy.d.ts +40 -0
- package/dist/runtime/proxy-retry-policy.js +326 -0
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +55 -52
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +2 -4
- package/dist/runtime/request-options.d.ts +1 -1
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +2 -2
- package/dist/runtime/state.js +15 -4
- package/dist/runtime/stealth.d.ts +7 -4
- package/dist/runtime/stealth.js +257 -215
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +11 -15
- package/dist/runtime/trace.d.ts +2 -2
- package/dist/runtime/trace.js +2 -4
- package/dist/runtime/waterfall.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +7 -15
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +7 -3
- package/dist/server/index.js +6 -2
- package/dist/server/self-test-input-tokens.d.ts +1 -0
- package/dist/server/self-test-input-tokens.js +37 -0
- package/dist/server/self-test-redaction.d.ts +20 -0
- package/dist/server/self-test-redaction.js +70 -0
- package/dist/server/self-test-token.d.ts +30 -0
- package/dist/server/self-test-token.js +50 -0
- package/dist/server/self-test.d.ts +199 -0
- package/dist/server/self-test.js +1113 -0
- package/dist/server/serve.d.ts +14 -3
- package/dist/server/serve.js +135 -64
- package/dist/server/types.d.ts +10 -9
- package/dist/server/types.js +3 -7
- package/dist/stealth/profiles.d.ts +1 -1
- package/dist/stealth/profiles.js +5 -14
- package/dist/stream.d.ts +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/testing/run.d.ts +1 -1
- package/dist/testing/run.js +12 -15
- package/dist/types.d.ts +237 -1
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +16 -5
- package/src/auth-turn/auth-turn.v1.schema.json +89 -0
- package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
- package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
- package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
- package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
- package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
- package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
- package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
- package/src/auth-turn/fixtures/valid/abort.json +8 -0
- package/src/auth-turn/fixtures/valid/challenge.json +17 -0
- package/src/auth-turn/fixtures/valid/complete.json +13 -0
- package/src/auth-turn/fixtures/valid/form.json +14 -0
- package/src/auth-turn/fixtures/valid/message.json +13 -0
- package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
- package/src/auth-turn/fixtures/valid/pending.json +5 -0
- package/src/auth-turn/fixtures/valid/poll.json +9 -0
- package/src/auth-turn/fixtures/valid/redirect.json +16 -0
- package/src/auth-turn/fixtures/valid/retry.json +8 -0
- package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
- package/src/auth-turn/index.ts +177 -0
- package/src/auth.ts +728 -0
- package/src/ceremonies/index.ts +33 -121
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +69 -99
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/src/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/src/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/src/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/src/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/src/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl +13 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +96 -0
- package/src/cli/templates/provider/README.md.tpl +7 -4
- package/src/config/loader.ts +543 -208
- package/src/contract-serialization.ts +5 -11
- package/src/contract-types.ts +2 -2
- package/src/contract.ts +13 -28
- package/src/define.ts +397 -528
- package/src/dev.ts +4 -9
- package/src/errors.ts +58 -5
- package/src/i18n/catalog.ts +10 -32
- package/src/i18n/index.ts +2 -2
- package/src/i18n/keys.ts +5 -11
- package/src/index.ts +64 -41
- package/src/lint.ts +122 -159
- package/src/provider.ts +40 -9
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +6 -6
- package/src/runtime/browser.ts +320 -151
- package/src/runtime/cache.ts +20 -67
- package/src/runtime/choice.ts +79 -132
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +23 -20
- package/src/runtime/http.ts +216 -539
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +78 -65
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- package/src/runtime/otlp.ts +12 -23
- package/src/runtime/perf.ts +1 -1
- package/src/runtime/provider.ts +4 -9
- package/src/runtime/proxy-errors.ts +29 -42
- package/src/runtime/proxy-nodemaven.ts +178 -0
- package/src/runtime/proxy-retry-policy.ts +469 -0
- package/src/runtime/proxy-telemetry.ts +79 -77
- package/src/runtime/redis.ts +4 -12
- package/src/runtime/request-options.ts +4 -13
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +41 -110
- package/src/runtime/stealth.ts +331 -369
- package/src/runtime/stt.ts +38 -94
- package/src/runtime/trace.ts +14 -44
- package/src/runtime/waterfall.ts +5 -18
- package/src/schema.ts +23 -84
- package/src/serve.ts +1 -1
- package/src/server/index.ts +44 -3
- package/src/server/self-test-input-tokens.ts +46 -0
- package/src/server/self-test-redaction.ts +97 -0
- package/src/server/self-test-token.ts +70 -0
- package/src/server/self-test.ts +1450 -0
- package/src/server/serve.ts +206 -216
- package/src/server/types.ts +7 -19
- package/src/stealth/profiles.ts +10 -26
- package/src/stream.ts +8 -19
- package/src/testing/index.ts +2 -2
- package/src/testing/run.ts +24 -64
- package/src/types.ts +274 -1
- package/src/user-input.ts +118 -0
package/src/runtime/stealth.ts
CHANGED
|
@@ -2,26 +2,28 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import type { Browser, ImpitOptions, ImpitResponse, RequestInit } from "impit";
|
|
3
3
|
import { Impit } from "impit";
|
|
4
4
|
|
|
5
|
-
import type { ProxyResolutionOptions } from "../config/loader";
|
|
5
|
+
import type { ProxyResolutionOptions, ProxyVendorName } from "../config/loader.js";
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_SMARTPROXY_POOL_SIZE,
|
|
8
8
|
invalidateProxyResolutionCacheAsync,
|
|
9
|
+
policyResolvesRegistryVendorChain,
|
|
9
10
|
ProxyResolutionError,
|
|
11
|
+
resolvePolicyProxyPoolSpan,
|
|
12
|
+
resolvePolicyTransportAttemptCap,
|
|
10
13
|
resolveProxyConfigAsync,
|
|
11
|
-
|
|
12
|
-
} from "../config/loader";
|
|
13
|
-
import {
|
|
14
|
-
import { getStealthProfile } from "../stealth/profiles";
|
|
14
|
+
vendorFromResolvedSource,
|
|
15
|
+
} from "../config/loader.js";
|
|
16
|
+
import { SDKError, TransportError } from "../errors.js";
|
|
17
|
+
import { getStealthProfile } from "../stealth/profiles.js";
|
|
15
18
|
import type {
|
|
16
19
|
CookieJar,
|
|
17
20
|
HttpMethod,
|
|
18
|
-
HttpRetryOptions,
|
|
19
21
|
StealthClient,
|
|
20
22
|
StealthFetchOptions,
|
|
23
|
+
StealthRedirectHop,
|
|
21
24
|
StealthResponse,
|
|
22
25
|
StealthSession,
|
|
23
|
-
} from "../types";
|
|
24
|
-
import { HttpRetryPreset, HttpRetryUnsafeMethodPolicy } from "../types";
|
|
26
|
+
} from "../types.js";
|
|
25
27
|
import {
|
|
26
28
|
createProxyAuthIpDeniedError,
|
|
27
29
|
createProxyEdgeAuthRejectedError,
|
|
@@ -34,49 +36,31 @@ import {
|
|
|
34
36
|
isProxyPoolRefreshableError,
|
|
35
37
|
isProxyPoolStaleMessage,
|
|
36
38
|
isProxyPoolStaleStatus,
|
|
37
|
-
PROXY_AUTH_IP_DENIED_CODE,
|
|
38
39
|
PROXY_EDGE_AUTH_REJECTED_CODE,
|
|
39
40
|
PROXY_POOL_STALE_CODE,
|
|
40
|
-
} from "./proxy-errors";
|
|
41
|
-
import {
|
|
41
|
+
} from "./proxy-errors.js";
|
|
42
|
+
import {
|
|
43
|
+
computeProxyAttemptIndex,
|
|
44
|
+
computeProxyTransportRetryDelayMs,
|
|
45
|
+
createDefaultProxyTransportRetryOptions,
|
|
46
|
+
normalizeProxyTransportRetryOptions,
|
|
47
|
+
shouldRetryProxyTransportAttempt,
|
|
48
|
+
validateUnsafeProxyTransportRetryMethods,
|
|
49
|
+
} from "./proxy-retry-policy.js";
|
|
50
|
+
import { appendQueryParams } from "./request-options.js";
|
|
42
51
|
|
|
43
52
|
const DEFAULT_PROFILE = "chrome-146";
|
|
44
53
|
|
|
45
54
|
const MISSING_PROXY_WARNING =
|
|
46
55
|
"[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
|
|
47
56
|
|
|
48
|
-
const MAX_POLICY_PROXY_RETRY_ATTEMPTS = SMARTPROXY_MAX_POOL_SIZE;
|
|
49
57
|
const MAX_POLICY_PROXY_POOL_REFRESHES = 1;
|
|
50
58
|
const PROXY_CONNECT_FAILURE_CODE = "proxy_connect_failed";
|
|
51
59
|
const PROXY_CONNECT_FAILURE_BODY_PATTERN =
|
|
52
60
|
/\bproxy\b.*\b(non[\s-]?200|connect|tunnel)|\bconnect\b.*\bproxy\b|\btunnel\b/i;
|
|
53
61
|
const PROXY_AUTH_DIAGNOSTIC_URL = "http://example.com/";
|
|
54
62
|
const PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS = 5_000;
|
|
55
|
-
const
|
|
56
|
-
const DEFAULT_STEALTH_RETRY_ERROR_CODES = [
|
|
57
|
-
PROXY_CONNECT_FAILURE_CODE,
|
|
58
|
-
"transport_network_error",
|
|
59
|
-
"transport_timeout",
|
|
60
|
-
] as const;
|
|
61
|
-
const RATE_LIMIT_STEALTH_RETRY_ERROR_CODES = ["transport_timeout"] as const;
|
|
62
|
-
const KNOWN_STEALTH_RETRY_METHODS = new Set([
|
|
63
|
-
"GET",
|
|
64
|
-
"HEAD",
|
|
65
|
-
"POST",
|
|
66
|
-
"PUT",
|
|
67
|
-
"DELETE",
|
|
68
|
-
"OPTIONS",
|
|
69
|
-
"TRACE",
|
|
70
|
-
"PATCH",
|
|
71
|
-
]);
|
|
72
|
-
const UNSAFE_STEALTH_RETRY_METHODS = new Set([
|
|
73
|
-
"POST",
|
|
74
|
-
"PUT",
|
|
75
|
-
"PATCH",
|
|
76
|
-
"DELETE",
|
|
77
|
-
"TRACE",
|
|
78
|
-
]);
|
|
79
|
-
const MAX_STEALTH_RETRY_ATTEMPTS = 8;
|
|
63
|
+
const STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES = [PROXY_CONNECT_FAILURE_CODE] as const;
|
|
80
64
|
|
|
81
65
|
export type StealthClientOptions = ProxyResolutionOptions & {
|
|
82
66
|
warn?: (message: string) => void;
|
|
@@ -128,15 +112,15 @@ const FIREFOX_IMPIT_BY_MAJOR: Record<number, ImpitBrowser> = {
|
|
|
128
112
|
|
|
129
113
|
type StealthTransportResponse = Pick<
|
|
130
114
|
ImpitResponse,
|
|
131
|
-
"arrayBuffer" | "headers" | "json" | "ok" | "status" | "text"
|
|
132
|
-
|
|
115
|
+
"arrayBuffer" | "headers" | "json" | "ok" | "status" | "text"
|
|
116
|
+
> & {
|
|
117
|
+
url?: string;
|
|
118
|
+
redirected?: boolean;
|
|
119
|
+
};
|
|
133
120
|
|
|
134
121
|
type StealthMethod = NonNullable<ImpitRequestInit["method"]>;
|
|
135
|
-
type
|
|
136
|
-
|
|
137
|
-
methods: readonly string[];
|
|
138
|
-
errorCodes: readonly string[];
|
|
139
|
-
unsafeMethodPolicy: HttpRetryOptions["unsafeMethodPolicy"];
|
|
122
|
+
type StealthRequestInit = ImpitRequestInit & {
|
|
123
|
+
redirect?: NonNullable<StealthFetchOptions["redirect"]>;
|
|
140
124
|
};
|
|
141
125
|
|
|
142
126
|
function isRecord(value: unknown): value is Record<PropertyKey, unknown> {
|
|
@@ -177,12 +161,37 @@ class CookieJarImpl implements CookieJar {
|
|
|
177
161
|
return { ...this.cookies };
|
|
178
162
|
}
|
|
179
163
|
|
|
164
|
+
has(name: string): boolean {
|
|
165
|
+
return Object.hasOwn(this.cookies, name);
|
|
166
|
+
}
|
|
167
|
+
|
|
180
168
|
toString(): string {
|
|
181
169
|
return Object.entries(this.cookies)
|
|
182
170
|
.map(([name, value]) => `${name}=${value}`)
|
|
183
171
|
.join("; ");
|
|
184
172
|
}
|
|
185
173
|
|
|
174
|
+
toHeader(): string {
|
|
175
|
+
return this.toString();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
snapshot(): Record<string, string> {
|
|
179
|
+
return this.getAll();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
restore(cookies: Record<string, string>): void {
|
|
183
|
+
this.clear();
|
|
184
|
+
for (const [name, value] of Object.entries(cookies)) {
|
|
185
|
+
if (name) this.cookies[name] = value;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
clear(): void {
|
|
190
|
+
for (const name of Object.keys(this.cookies)) {
|
|
191
|
+
delete this.cookies[name];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
186
195
|
find(predicate: (cookie: string) => boolean): string | undefined {
|
|
187
196
|
for (const [name, value] of Object.entries(this.cookies)) {
|
|
188
197
|
const cookie = `${name}=${value}`;
|
|
@@ -271,9 +280,7 @@ function normalizeHeaders(
|
|
|
271
280
|
function hasOwn(object: object, key: string): boolean {
|
|
272
281
|
return Object.hasOwn(object, key);
|
|
273
282
|
}
|
|
274
|
-
function toImpitCookieJar(
|
|
275
|
-
cookieJar: CookieJarImpl,
|
|
276
|
-
): NonNullable<ImpitOptions["cookieJar"]> {
|
|
283
|
+
function toImpitCookieJar(cookieJar: CookieJarImpl): NonNullable<ImpitOptions["cookieJar"]> {
|
|
277
284
|
return {
|
|
278
285
|
setCookie(cookie: string, _url: string, cb?: (error?: unknown) => void) {
|
|
279
286
|
cookieJar.setFromCookieStrings([cookie]);
|
|
@@ -290,10 +297,8 @@ function assertNoUnsupportedFingerprintOverrides(options: unknown): void {
|
|
|
290
297
|
const unsupported: string[] = [];
|
|
291
298
|
if (hasOwn(options, "headerOrder")) unsupported.push("headerOrder");
|
|
292
299
|
const stealth = options.stealth;
|
|
293
|
-
if (isRecord(stealth) && hasOwn(stealth, "ja3"))
|
|
294
|
-
|
|
295
|
-
if (isRecord(stealth) && hasOwn(stealth, "h2"))
|
|
296
|
-
unsupported.push("stealth.h2");
|
|
300
|
+
if (isRecord(stealth) && hasOwn(stealth, "ja3")) unsupported.push("stealth.ja3");
|
|
301
|
+
if (isRecord(stealth) && hasOwn(stealth, "h2")) unsupported.push("stealth.h2");
|
|
297
302
|
if (unsupported.length === 0) return;
|
|
298
303
|
|
|
299
304
|
throw new SDKError(
|
|
@@ -301,9 +306,7 @@ function assertNoUnsupportedFingerprintOverrides(options: unknown): void {
|
|
|
301
306
|
);
|
|
302
307
|
}
|
|
303
308
|
|
|
304
|
-
function responseHeadersToRecord(
|
|
305
|
-
headers: Headers,
|
|
306
|
-
): Record<string, string | string[] | undefined> {
|
|
309
|
+
function responseHeadersToRecord(headers: Headers): Record<string, string | string[] | undefined> {
|
|
307
310
|
const record: Record<string, string> = {};
|
|
308
311
|
for (const [name, value] of headers.entries()) record[name] = value;
|
|
309
312
|
return record;
|
|
@@ -335,17 +338,22 @@ function splitCombinedSetCookieHeader(headerValue: string): string[] {
|
|
|
335
338
|
|
|
336
339
|
export async function normalizeResponse(
|
|
337
340
|
response: StealthTransportResponse,
|
|
341
|
+
requestUrl?: string,
|
|
338
342
|
): Promise<StealthResponse> {
|
|
339
343
|
const headers = Object.fromEntries(response.headers.entries());
|
|
340
|
-
const cookies = new CookieJarImpl(
|
|
341
|
-
setCookieHeadersFromResponse(response.headers),
|
|
342
|
-
);
|
|
344
|
+
const cookies = new CookieJarImpl(setCookieHeadersFromResponse(response.headers));
|
|
343
345
|
const bodyBytes = await response.arrayBuffer();
|
|
344
346
|
const body = new TextDecoder().decode(bodyBytes);
|
|
345
347
|
|
|
346
348
|
return {
|
|
347
349
|
status: response.status,
|
|
348
350
|
ok: response.status >= 200 && response.status < 300,
|
|
351
|
+
...(response.url ? { url: response.url } : {}),
|
|
352
|
+
...(response.redirected !== undefined
|
|
353
|
+
? { redirected: response.redirected }
|
|
354
|
+
: requestUrl && response.url
|
|
355
|
+
? { redirected: response.url !== requestUrl }
|
|
356
|
+
: {}),
|
|
349
357
|
headers,
|
|
350
358
|
rawHeaders: headerEntriesFromHeaders(response.headers),
|
|
351
359
|
body,
|
|
@@ -383,189 +391,11 @@ function isPolicyManagedProxy(options: StealthClientOptions): boolean {
|
|
|
383
391
|
return Boolean(policy && typeof policy === "object");
|
|
384
392
|
}
|
|
385
393
|
|
|
386
|
-
function
|
|
387
|
-
return
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function createStealthRetryOptions(
|
|
391
|
-
preset: HttpRetryPreset,
|
|
392
|
-
): NormalizedStealthRetryOptions {
|
|
393
|
-
switch (preset) {
|
|
394
|
-
case HttpRetryPreset.Off:
|
|
395
|
-
return {
|
|
396
|
-
attempts: 1,
|
|
397
|
-
methods: DEFAULT_STEALTH_RETRY_METHODS,
|
|
398
|
-
errorCodes: DEFAULT_STEALTH_RETRY_ERROR_CODES,
|
|
399
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
400
|
-
};
|
|
401
|
-
case HttpRetryPreset.AggressiveRead:
|
|
402
|
-
return {
|
|
403
|
-
attempts: 4,
|
|
404
|
-
methods: DEFAULT_STEALTH_RETRY_METHODS,
|
|
405
|
-
errorCodes: DEFAULT_STEALTH_RETRY_ERROR_CODES,
|
|
406
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
407
|
-
};
|
|
408
|
-
case HttpRetryPreset.RateLimitAware:
|
|
409
|
-
return {
|
|
410
|
-
attempts: 3,
|
|
411
|
-
methods: DEFAULT_STEALTH_RETRY_METHODS,
|
|
412
|
-
errorCodes: RATE_LIMIT_STEALTH_RETRY_ERROR_CODES,
|
|
413
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
414
|
-
};
|
|
415
|
-
case HttpRetryPreset.SafeRead:
|
|
416
|
-
case HttpRetryPreset.TransportTransient:
|
|
417
|
-
return {
|
|
418
|
-
attempts: 3,
|
|
419
|
-
methods: DEFAULT_STEALTH_RETRY_METHODS,
|
|
420
|
-
errorCodes: DEFAULT_STEALTH_RETRY_ERROR_CODES,
|
|
421
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
throw new ProviderError(`Unknown stealth retry preset: ${preset}`, {
|
|
425
|
-
code: "retry_invalid_policy",
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function normalizeStealthRetryOptions(
|
|
430
|
-
retry: StealthFetchOptions["retry"],
|
|
431
|
-
): NormalizedStealthRetryOptions | undefined {
|
|
432
|
-
if (retry === undefined) return undefined;
|
|
433
|
-
if (retry === false) return createStealthRetryOptions(HttpRetryPreset.Off);
|
|
434
|
-
if (retry === true)
|
|
435
|
-
return createStealthRetryOptions(HttpRetryPreset.TransportTransient);
|
|
436
|
-
if (typeof retry === "string") {
|
|
437
|
-
if (!Object.values(HttpRetryPreset).includes(retry)) {
|
|
438
|
-
throw new ProviderError(`Unknown stealth retry preset: ${retry}`, {
|
|
439
|
-
code: "retry_invalid_policy",
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
return createStealthRetryOptions(retry);
|
|
443
|
-
}
|
|
444
|
-
if (typeof retry !== "object" || retry === null || Array.isArray(retry)) {
|
|
445
|
-
throw new ProviderError("Stealth retry policy must be a plain object", {
|
|
446
|
-
code: "retry_invalid_policy",
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
if (
|
|
450
|
-
retry.unsafeMethodPolicy !== undefined &&
|
|
451
|
-
!Object.values(HttpRetryUnsafeMethodPolicy).includes(
|
|
452
|
-
retry.unsafeMethodPolicy,
|
|
453
|
-
)
|
|
454
|
-
) {
|
|
455
|
-
throw new ProviderError(
|
|
456
|
-
`Unknown stealth retry unsafe method policy: ${String(retry.unsafeMethodPolicy)}`,
|
|
457
|
-
{ code: "retry_invalid_policy" },
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
if (retry.methods !== undefined) {
|
|
461
|
-
if (!Array.isArray(retry.methods)) {
|
|
462
|
-
throw new ProviderError("Stealth retry methods must be an array", {
|
|
463
|
-
code: "retry_invalid_policy",
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
const unknownMethods = retry.methods
|
|
467
|
-
.map((method) => (typeof method === "string" ? method.toUpperCase() : ""))
|
|
468
|
-
.filter((method) => !KNOWN_STEALTH_RETRY_METHODS.has(method));
|
|
469
|
-
if (unknownMethods.length > 0) {
|
|
470
|
-
throw new ProviderError(
|
|
471
|
-
`Unknown stealth retry method(s): ${unknownMethods.join(", ")}`,
|
|
472
|
-
{ code: "retry_invalid_policy" },
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
if (retry.errorCodes !== undefined) {
|
|
477
|
-
if (
|
|
478
|
-
!Array.isArray(retry.errorCodes) ||
|
|
479
|
-
retry.errorCodes.some((errorCode) => typeof errorCode !== "string")
|
|
480
|
-
) {
|
|
481
|
-
throw new ProviderError(
|
|
482
|
-
"Stealth retry errorCodes must contain only strings",
|
|
483
|
-
{ code: "retry_invalid_policy" },
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const base = createStealthRetryOptions(
|
|
489
|
-
retry.preset ?? HttpRetryPreset.TransportTransient,
|
|
490
|
-
);
|
|
491
|
-
const attempts =
|
|
492
|
-
retry.attempts === undefined || !Number.isFinite(retry.attempts)
|
|
493
|
-
? base.attempts
|
|
494
|
-
: Math.max(
|
|
495
|
-
1,
|
|
496
|
-
Math.min(MAX_STEALTH_RETRY_ATTEMPTS, Math.floor(retry.attempts)),
|
|
497
|
-
);
|
|
498
|
-
const normalized: NormalizedStealthRetryOptions = {
|
|
499
|
-
attempts,
|
|
500
|
-
methods:
|
|
501
|
-
retry.methods?.map((method) => method.toUpperCase()) ?? base.methods,
|
|
502
|
-
errorCodes: retry.errorCodes ?? base.errorCodes,
|
|
503
|
-
unsafeMethodPolicy: retry.unsafeMethodPolicy ?? base.unsafeMethodPolicy,
|
|
504
|
-
};
|
|
505
|
-
|
|
506
|
-
if (
|
|
507
|
-
normalized.unsafeMethodPolicy !==
|
|
508
|
-
HttpRetryUnsafeMethodPolicy.AllowExplicitUnsafe
|
|
509
|
-
) {
|
|
510
|
-
const unsafeMethods = normalized.methods.filter((method) =>
|
|
511
|
-
UNSAFE_STEALTH_RETRY_METHODS.has(method.toUpperCase()),
|
|
512
|
-
);
|
|
513
|
-
if (unsafeMethods.length > 0) {
|
|
514
|
-
throw new ProviderError(
|
|
515
|
-
`Stealth retry methods include unsafe method(s): ${unsafeMethods.join(", ")}`,
|
|
516
|
-
{ code: "retry_unsafe_method" },
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
return normalized;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function isExplicitStealthRetryAllowed(
|
|
525
|
-
method: StealthMethod,
|
|
526
|
-
error: TransportError,
|
|
527
|
-
retryOptions: NormalizedStealthRetryOptions | undefined,
|
|
528
|
-
): boolean {
|
|
529
|
-
if (!retryOptions || retryOptions.attempts <= 1) return false;
|
|
530
|
-
return (
|
|
531
|
-
retryOptions.methods.includes(method.toUpperCase()) &&
|
|
532
|
-
retryOptions.errorCodes.includes(proxyAttemptErrorCode(error))
|
|
533
|
-
);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function isRetryableProxyTransportError(error: unknown): boolean {
|
|
537
|
-
if (error instanceof TransportError) {
|
|
538
|
-
if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
|
|
539
|
-
return false;
|
|
540
|
-
}
|
|
541
|
-
return (
|
|
542
|
-
error.code === PROXY_CONNECT_FAILURE_CODE ||
|
|
543
|
-
error.code === "transport_network_error" ||
|
|
544
|
-
error.code === "transport_timeout"
|
|
545
|
-
);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
if (error instanceof SDKError) {
|
|
549
|
-
return false;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
553
|
-
return /\bproxy\b|\bnon[\s-]?200\b|\bconnect\b|\btunnel\b/i.test(message);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
function isProxyConnectFailureResponse(
|
|
557
|
-
response: StealthTransportResponse,
|
|
558
|
-
body: string,
|
|
559
|
-
): boolean {
|
|
560
|
-
return (
|
|
561
|
-
response.status === 0 && PROXY_CONNECT_FAILURE_BODY_PATTERN.test(body ?? "")
|
|
562
|
-
);
|
|
394
|
+
function isProxyConnectFailureResponse(response: StealthTransportResponse, body: string): boolean {
|
|
395
|
+
return response.status === 0 && PROXY_CONNECT_FAILURE_BODY_PATTERN.test(body ?? "");
|
|
563
396
|
}
|
|
564
397
|
|
|
565
|
-
function createProxyConnectFailureError(
|
|
566
|
-
body: string,
|
|
567
|
-
cause?: Error,
|
|
568
|
-
): TransportError {
|
|
398
|
+
function createProxyConnectFailureError(body: string, cause?: Error): TransportError {
|
|
569
399
|
const bodyExcerpt = (body ?? "").trim().slice(0, 1_000);
|
|
570
400
|
if (isProxyAuthIpDeniedMessage(bodyExcerpt)) {
|
|
571
401
|
return createProxyAuthIpDeniedError(cause);
|
|
@@ -574,10 +404,7 @@ function createProxyConnectFailureError(
|
|
|
574
404
|
return createProxyEdgeAuthRejectedError(cause);
|
|
575
405
|
}
|
|
576
406
|
if (isProxyPoolStaleMessage(bodyExcerpt)) {
|
|
577
|
-
return createProxyPoolStaleError(
|
|
578
|
-
bodyExcerpt.includes("512") ? 512 : 509,
|
|
579
|
-
cause,
|
|
580
|
-
);
|
|
407
|
+
return createProxyPoolStaleError(bodyExcerpt.includes("512") ? 512 : 509, cause);
|
|
581
408
|
}
|
|
582
409
|
return new TransportError(bodyExcerpt || "Proxy CONNECT failed", {
|
|
583
410
|
code: PROXY_CONNECT_FAILURE_CODE,
|
|
@@ -601,6 +428,7 @@ type ResolvedAttemptProxy = {
|
|
|
601
428
|
url?: string;
|
|
602
429
|
poolIndex?: number;
|
|
603
430
|
proxyHash?: string;
|
|
431
|
+
vendor?: ProxyVendorName;
|
|
604
432
|
};
|
|
605
433
|
|
|
606
434
|
function proxyPoolIndexFromDiagnostics(
|
|
@@ -684,33 +512,20 @@ function normalizeStealthTransportError(error: unknown): TransportError {
|
|
|
684
512
|
}
|
|
685
513
|
|
|
686
514
|
if (isProxyAuthIpDeniedMessage(message)) {
|
|
687
|
-
return createProxyAuthIpDeniedError(
|
|
688
|
-
error instanceof Error ? error : undefined,
|
|
689
|
-
);
|
|
515
|
+
return createProxyAuthIpDeniedError(error instanceof Error ? error : undefined);
|
|
690
516
|
}
|
|
691
517
|
|
|
692
518
|
if (isProxyEdgeAuthRejectedMessage(message)) {
|
|
693
|
-
return createProxyEdgeAuthRejectedError(
|
|
694
|
-
error instanceof Error ? error : undefined,
|
|
695
|
-
);
|
|
519
|
+
return createProxyEdgeAuthRejectedError(error instanceof Error ? error : undefined);
|
|
696
520
|
}
|
|
697
521
|
|
|
698
522
|
const proxyTunnelStatus = getProxyTunnelStatus(error);
|
|
699
|
-
if (
|
|
700
|
-
proxyTunnelStatus
|
|
701
|
-
isProxyPoolStaleStatus(proxyTunnelStatus)
|
|
702
|
-
) {
|
|
703
|
-
return createProxyPoolStaleError(
|
|
704
|
-
proxyTunnelStatus,
|
|
705
|
-
error instanceof Error ? error : undefined,
|
|
706
|
-
);
|
|
523
|
+
if (proxyTunnelStatus !== undefined && isProxyPoolStaleStatus(proxyTunnelStatus)) {
|
|
524
|
+
return createProxyPoolStaleError(proxyTunnelStatus, error instanceof Error ? error : undefined);
|
|
707
525
|
}
|
|
708
526
|
|
|
709
527
|
if (PROXY_CONNECT_FAILURE_BODY_PATTERN.test(message)) {
|
|
710
|
-
return createProxyConnectFailureError(
|
|
711
|
-
message,
|
|
712
|
-
error instanceof Error ? error : undefined,
|
|
713
|
-
);
|
|
528
|
+
return createProxyConnectFailureError(message, error instanceof Error ? error : undefined);
|
|
714
529
|
}
|
|
715
530
|
|
|
716
531
|
return new TransportError("Network error", {
|
|
@@ -720,6 +535,10 @@ function normalizeStealthTransportError(error: unknown): TransportError {
|
|
|
720
535
|
});
|
|
721
536
|
}
|
|
722
537
|
|
|
538
|
+
function sleep(ms: number): Promise<void> {
|
|
539
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
540
|
+
}
|
|
541
|
+
|
|
723
542
|
function normalizeMethod(method: HttpMethod | string): StealthMethod {
|
|
724
543
|
switch (method.toUpperCase()) {
|
|
725
544
|
case "HEAD":
|
|
@@ -743,6 +562,23 @@ function normalizeMethod(method: HttpMethod | string): StealthMethod {
|
|
|
743
562
|
}
|
|
744
563
|
}
|
|
745
564
|
|
|
565
|
+
function isRedirectStatus(status: number): boolean {
|
|
566
|
+
return [301, 302, 303, 307, 308].includes(status);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function nextRedirectMethod(status: number, method: StealthMethod): StealthMethod {
|
|
570
|
+
if (status === 303 && method !== "HEAD") return "GET";
|
|
571
|
+
if ((status === 301 || status === 302) && method === "POST") return "GET";
|
|
572
|
+
return method;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function locationHeader(headers: Record<string, string>): string | undefined {
|
|
576
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
577
|
+
if (name.toLowerCase() === "location") return value;
|
|
578
|
+
}
|
|
579
|
+
return undefined;
|
|
580
|
+
}
|
|
581
|
+
|
|
746
582
|
function createSessionFetcher(
|
|
747
583
|
baseUrl: string,
|
|
748
584
|
defaultProfile: string,
|
|
@@ -782,20 +618,22 @@ function createSessionFetcher(
|
|
|
782
618
|
async function resolveRequestProxy(
|
|
783
619
|
options?: StealthFetchOptions,
|
|
784
620
|
proxyAttempt?: number,
|
|
621
|
+
refreshEpoch?: number,
|
|
785
622
|
): Promise<ResolvedAttemptProxy> {
|
|
786
|
-
const rawProxyAttemptOffset = options?.proxyAttemptOffset ?? 0;
|
|
787
|
-
const proxyAttemptOffset = Number.isFinite(rawProxyAttemptOffset)
|
|
788
|
-
? Math.max(0, Math.floor(rawProxyAttemptOffset))
|
|
789
|
-
: 0;
|
|
790
623
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
791
624
|
proxy: options?.proxy ?? clientOptions.proxy,
|
|
792
625
|
upstream: clientOptions.upstream,
|
|
793
626
|
apifuseConfig: clientOptions.apifuseConfig,
|
|
794
627
|
affinityKey: clientOptions.affinityKey,
|
|
795
|
-
proxyAttempt:
|
|
796
|
-
proxyAttempt
|
|
797
|
-
|
|
798
|
-
|
|
628
|
+
proxyAttempt: computeProxyAttemptIndex({
|
|
629
|
+
baseProxyAttempt: clientOptions.proxyAttempt,
|
|
630
|
+
proxyAttemptOffset: options?.proxyAttemptOffset,
|
|
631
|
+
retryAttemptOffset: proxyAttempt,
|
|
632
|
+
}),
|
|
633
|
+
// The impit stealth transport tunnels both HTTP CONNECT and SOCKS5,
|
|
634
|
+
// preserving the client TLS fingerprint end-to-end.
|
|
635
|
+
transportProtocols: ["http", "socks5"],
|
|
636
|
+
...(refreshEpoch === undefined ? {} : { proxyRefreshEpoch: refreshEpoch }),
|
|
799
637
|
telemetry: clientOptions.telemetry,
|
|
800
638
|
});
|
|
801
639
|
|
|
@@ -808,30 +646,58 @@ function createSessionFetcher(
|
|
|
808
646
|
url: resolvedProxy.url,
|
|
809
647
|
poolIndex: proxyPoolIndexFromDiagnostics(resolvedProxy.diagnostics),
|
|
810
648
|
proxyHash: proxyEndpointHash(resolvedProxy.url),
|
|
649
|
+
vendor: vendorFromResolvedSource(resolvedProxy.source),
|
|
811
650
|
};
|
|
812
651
|
}
|
|
813
652
|
|
|
814
|
-
|
|
653
|
+
const session: StealthSession = {
|
|
815
654
|
async fetch(url, options: StealthFetchOptions = {}) {
|
|
816
655
|
const method = normalizeMethod(options.method ?? "GET");
|
|
817
656
|
const hasExplicitRetryPolicy = options.retry !== undefined;
|
|
818
|
-
const stealthRetryOptions =
|
|
657
|
+
const stealthRetryOptions =
|
|
658
|
+
normalizeProxyTransportRetryOptions(options.retry, {
|
|
659
|
+
extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
|
|
660
|
+
label: "Stealth",
|
|
661
|
+
}) ??
|
|
662
|
+
(hasExplicitRetryPolicy
|
|
663
|
+
? undefined
|
|
664
|
+
: createDefaultProxyTransportRetryOptions({
|
|
665
|
+
extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
|
|
666
|
+
label: "Stealth",
|
|
667
|
+
}));
|
|
668
|
+
if (stealthRetryOptions) {
|
|
669
|
+
validateUnsafeProxyTransportRetryMethods(stealthRetryOptions, "Stealth");
|
|
670
|
+
}
|
|
819
671
|
const hasPolicyProxy = isPolicyManagedProxy(clientOptions);
|
|
820
|
-
const usesPolicyAllocator =
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
672
|
+
const usesPolicyAllocator = hasPolicyProxy && !options.proxy && !clientOptions.proxy;
|
|
673
|
+
const retryAttemptCap = Math.max(1, stealthRetryOptions?.attempts ?? 1);
|
|
674
|
+
// Span the whole vendor chain: successive attempts rotate one vendor's
|
|
675
|
+
// pool, then fail over to the next vendor via the flat attempt index.
|
|
676
|
+
const policyProxy =
|
|
677
|
+
clientOptions.proxyPolicy ??
|
|
678
|
+
(typeof clientOptions.upstream?.proxy === "object"
|
|
679
|
+
? clientOptions.upstream.proxy
|
|
680
|
+
: undefined);
|
|
681
|
+
// The pool span is already bounded by each vendor's max pool size
|
|
682
|
+
// (smartproxy ≤20, nodemaven ≤50), so the configured span never exceeds
|
|
683
|
+
// the chain's true maximum — a large NodeMaven pool stays fully
|
|
684
|
+
// reachable rather than being truncated at an arbitrary ceiling.
|
|
685
|
+
const policyProxyAttemptCap = Math.max(
|
|
686
|
+
1,
|
|
687
|
+
policyProxy ? resolvePolicyProxyPoolSpan(policyProxy) : DEFAULT_SMARTPROXY_POOL_SIZE,
|
|
688
|
+
);
|
|
689
|
+
// A registry vendor chain (smartproxy/nodemaven) is the only policy whose
|
|
690
|
+
// successive attempts resolve a *different* endpoint, so it is the only one
|
|
691
|
+
// that may widen the attempt cap to the pool span, de-duplicate endpoints,
|
|
692
|
+
// and drive allocator stale-pool refresh. A static custom/decodo policy
|
|
693
|
+
// resolves the same URL every attempt: widening/refreshing it would resend
|
|
694
|
+
// the request dozens of times (up to maxAttempts × refreshes) and bypass
|
|
695
|
+
// retry:false and unsafe-method controls. Static policies therefore follow
|
|
696
|
+
// the ordinary transport-retry budget instead.
|
|
697
|
+
const rotatesRegistryChain =
|
|
698
|
+
usesPolicyAllocator && policyResolvesRegistryVendorChain(policyProxy);
|
|
699
|
+
const maxAttempts = rotatesRegistryChain ? policyProxyAttemptCap : retryAttemptCap;
|
|
700
|
+
const dedupeAllocatorEndpoints = rotatesRegistryChain;
|
|
835
701
|
let lastError: unknown;
|
|
836
702
|
|
|
837
703
|
for (
|
|
@@ -856,14 +722,12 @@ function createSessionFetcher(
|
|
|
856
722
|
if (attemptRecorded || !proxy) return;
|
|
857
723
|
attemptRecorded = true;
|
|
858
724
|
clientOptions.telemetry?.recordProxyAttempt?.({
|
|
859
|
-
provider: "smartproxy",
|
|
725
|
+
provider: attemptProxy?.vendor ?? "smartproxy",
|
|
860
726
|
attempt: attempt + 1,
|
|
861
727
|
...(attemptProxy?.poolIndex === undefined
|
|
862
728
|
? {}
|
|
863
729
|
: { poolIndex: attemptProxy.poolIndex }),
|
|
864
|
-
...(attemptProxy?.proxyHash
|
|
865
|
-
? { proxyHash: attemptProxy.proxyHash }
|
|
866
|
-
: {}),
|
|
730
|
+
...(attemptProxy?.proxyHash ? { proxyHash: attemptProxy.proxyHash } : {}),
|
|
867
731
|
outcome,
|
|
868
732
|
...(errorCode ? { errorCode } : {}),
|
|
869
733
|
...(status === undefined ? {} : { status }),
|
|
@@ -872,52 +736,47 @@ function createSessionFetcher(
|
|
|
872
736
|
};
|
|
873
737
|
try {
|
|
874
738
|
assertNoUnsupportedFingerprintOverrides(options);
|
|
875
|
-
attemptProxy = await resolveRequestProxy(options, attempt);
|
|
739
|
+
attemptProxy = await resolveRequestProxy(options, attempt, refreshAttempt);
|
|
876
740
|
proxy = attemptProxy.url;
|
|
877
|
-
if (proxy) {
|
|
741
|
+
if (proxy && dedupeAllocatorEndpoints) {
|
|
742
|
+
// An under-filled allocation repeats endpoints (via the modulo
|
|
743
|
+
// pool mapping) before the flat offset crosses into the next
|
|
744
|
+
// vendor. Skip an already-tried endpoint and advance the offset
|
|
745
|
+
// rather than breaking — breaking here would strand the request on
|
|
746
|
+
// the primary vendor and never reach the fallback leg.
|
|
878
747
|
if (attemptedProxies.has(proxy)) {
|
|
879
|
-
|
|
748
|
+
continue;
|
|
880
749
|
}
|
|
881
750
|
attemptedProxies.add(proxy);
|
|
882
751
|
}
|
|
883
752
|
const ignoreTlsErrors = Boolean(
|
|
884
753
|
options.stealth?.insecureSkipVerify ??
|
|
885
|
-
(!hasPolicyProxy &&
|
|
886
|
-
proxy &&
|
|
887
|
-
clientOptions.proxyStealth?.insecureSkipVerify),
|
|
754
|
+
(!hasPolicyProxy && proxy && clientOptions.proxyStealth?.insecureSkipVerify),
|
|
888
755
|
);
|
|
889
756
|
const profileName = options.profile ?? defaultProfile;
|
|
890
|
-
const requestUrl = appendQueryParams(
|
|
891
|
-
resolveUrl(baseUrl, url),
|
|
892
|
-
options.params,
|
|
893
|
-
);
|
|
757
|
+
const requestUrl = appendQueryParams(resolveUrl(baseUrl, url), options.params);
|
|
894
758
|
const headers = { ...(options.headers ?? {}) };
|
|
895
759
|
if (!hasHeader(headers, "Cookie")) {
|
|
896
760
|
const cookieHeader = cookieJar.toString();
|
|
897
761
|
if (cookieHeader) headers.Cookie = cookieHeader;
|
|
898
762
|
}
|
|
899
|
-
const requestInit:
|
|
763
|
+
const requestInit: StealthRequestInit = {
|
|
900
764
|
headers: normalizeHeaders(headers),
|
|
901
765
|
method,
|
|
766
|
+
...(options.redirect ? { redirect: options.redirect } : {}),
|
|
902
767
|
...(options.timeout ? { timeout: options.timeout } : {}),
|
|
903
768
|
};
|
|
904
769
|
if (options.body !== undefined) {
|
|
905
770
|
requestInit.body = normalizeBody(options.body);
|
|
906
771
|
}
|
|
907
|
-
const response = await getClient(
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
ignoreTlsErrors,
|
|
911
|
-
).fetch(requestUrl, requestInit);
|
|
912
|
-
const normalized = await normalizeResponse(response);
|
|
913
|
-
cookieJar.setFromCookieStrings(
|
|
914
|
-
setCookieHeadersFromResponse(response.headers),
|
|
772
|
+
const response = await getClient(profileName, proxy, ignoreTlsErrors).fetch(
|
|
773
|
+
requestUrl,
|
|
774
|
+
requestInit,
|
|
915
775
|
);
|
|
776
|
+
const normalized = await normalizeResponse(response, requestUrl);
|
|
777
|
+
cookieJar.setFromCookieStrings(setCookieHeadersFromResponse(response.headers));
|
|
916
778
|
|
|
917
|
-
if (
|
|
918
|
-
proxy &&
|
|
919
|
-
isProxyConnectFailureResponse(response, normalized.body)
|
|
920
|
-
) {
|
|
779
|
+
if (proxy && isProxyConnectFailureResponse(response, normalized.body)) {
|
|
921
780
|
throw createProxyConnectFailureError(normalized.body);
|
|
922
781
|
}
|
|
923
782
|
|
|
@@ -927,10 +786,9 @@ function createSessionFetcher(
|
|
|
927
786
|
usesPolicyAllocator &&
|
|
928
787
|
isProxyEdgeTlsRejectedResponse(
|
|
929
788
|
response.status,
|
|
930
|
-
[
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
].join("\n"),
|
|
789
|
+
[JSON.stringify(responseHeadersToRecord(response.headers)), normalized.body].join(
|
|
790
|
+
"\n",
|
|
791
|
+
),
|
|
934
792
|
)
|
|
935
793
|
) {
|
|
936
794
|
throw createProxyEdgeTlsRejectedError(response.status);
|
|
@@ -951,13 +809,10 @@ function createSessionFetcher(
|
|
|
951
809
|
}
|
|
952
810
|
|
|
953
811
|
if (response.status >= 400 && options.throwOnHttpError !== false) {
|
|
954
|
-
throw new TransportError(
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
status: response.status,
|
|
959
|
-
},
|
|
960
|
-
);
|
|
812
|
+
throw new TransportError(`Upstream request failed with status ${response.status}`, {
|
|
813
|
+
code: "upstream_http_error",
|
|
814
|
+
status: response.status,
|
|
815
|
+
});
|
|
961
816
|
}
|
|
962
817
|
|
|
963
818
|
recordProxyAttempt("ok", undefined, response.status);
|
|
@@ -970,11 +825,7 @@ function createSessionFetcher(
|
|
|
970
825
|
proxyAttemptStatus(normalizedError),
|
|
971
826
|
);
|
|
972
827
|
lastError = normalizedError;
|
|
973
|
-
if (
|
|
974
|
-
proxy &&
|
|
975
|
-
usesPolicyAllocator &&
|
|
976
|
-
isProxyPoolRefreshableError(normalizedError)
|
|
977
|
-
) {
|
|
828
|
+
if (proxy && rotatesRegistryChain && isProxyPoolRefreshableError(normalizedError)) {
|
|
978
829
|
stalePoolError = normalizedError;
|
|
979
830
|
if (shouldRunProxyAuthDiagnostic(normalizedError)) {
|
|
980
831
|
stalePoolDiagnosticProxy = proxy;
|
|
@@ -984,21 +835,39 @@ function createSessionFetcher(
|
|
|
984
835
|
}
|
|
985
836
|
break;
|
|
986
837
|
}
|
|
838
|
+
// Cap the number of transport retries. For a policy-allocator chain,
|
|
839
|
+
// every attempt resolves a *different* endpoint/vendor (poolIndex
|
|
840
|
+
// rotates across the concatenated vendor pool spans), so a transport
|
|
841
|
+
// failure is a signal to advance to the next endpoint — potentially
|
|
842
|
+
// crossing into the fallback vendor — not to retry the same endpoint.
|
|
843
|
+
// Truncating that rotation at the per-endpoint retry budget would
|
|
844
|
+
// strand the request on the primary vendor and never reach the
|
|
845
|
+
// fallback, since the crossover only happens once the flat attempt
|
|
846
|
+
// index exceeds the primary vendor's pool size (~10-20).
|
|
847
|
+
// resolvePolicyTransportAttemptCap widens to the full chain span only
|
|
848
|
+
// for implicit, safe-method allocator requests; explicit retry
|
|
849
|
+
// policies (their documented `attempts` ceiling), unsafe methods, and
|
|
850
|
+
// static/non-registry vendors keep the per-endpoint retry budget.
|
|
851
|
+
const transportRetryCap = resolvePolicyTransportAttemptCap({
|
|
852
|
+
policy: policyProxy,
|
|
853
|
+
usesPolicyAllocator,
|
|
854
|
+
retryAttempts: stealthRetryOptions?.attempts ?? 1,
|
|
855
|
+
explicitRetry: hasExplicitRetryPolicy,
|
|
856
|
+
method,
|
|
857
|
+
});
|
|
987
858
|
if (
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
method,
|
|
997
|
-
normalizedError,
|
|
998
|
-
stealthRetryOptions,
|
|
999
|
-
)) &&
|
|
1000
|
-
isRetryableProxyTransportError(normalizedError)
|
|
859
|
+
attempt + 1 < transportRetryCap &&
|
|
860
|
+
shouldRetryProxyTransportAttempt({
|
|
861
|
+
error: normalizedError,
|
|
862
|
+
explicitRetry: hasExplicitRetryPolicy,
|
|
863
|
+
method,
|
|
864
|
+
options: stealthRetryOptions,
|
|
865
|
+
proxyUsed: Boolean(proxy),
|
|
866
|
+
})
|
|
1001
867
|
) {
|
|
868
|
+
if (stealthRetryOptions) {
|
|
869
|
+
await sleep(computeProxyTransportRetryDelayMs(stealthRetryOptions!, attempt + 1));
|
|
870
|
+
}
|
|
1002
871
|
continue;
|
|
1003
872
|
}
|
|
1004
873
|
throw normalizedError;
|
|
@@ -1006,7 +875,7 @@ function createSessionFetcher(
|
|
|
1006
875
|
}
|
|
1007
876
|
|
|
1008
877
|
if (
|
|
1009
|
-
|
|
878
|
+
rotatesRegistryChain &&
|
|
1010
879
|
stalePoolError &&
|
|
1011
880
|
refreshAttempt < MAX_POLICY_PROXY_POOL_REFRESHES
|
|
1012
881
|
) {
|
|
@@ -1052,33 +921,138 @@ function createSessionFetcher(
|
|
|
1052
921
|
|
|
1053
922
|
throw normalizeStealthTransportError(lastError);
|
|
1054
923
|
},
|
|
924
|
+
cookies: cookieJar,
|
|
925
|
+
redirects: {
|
|
926
|
+
async run(options) {
|
|
927
|
+
const maxHops =
|
|
928
|
+
options.maxHops === undefined || !Number.isFinite(options.maxHops)
|
|
929
|
+
? 10
|
|
930
|
+
: Math.max(0, Math.floor(options.maxHops));
|
|
931
|
+
const hops: StealthRedirectHop[] = [];
|
|
932
|
+
let currentUrl = resolveUrl(baseUrl, options.url);
|
|
933
|
+
let method = normalizeMethod(options.method ?? "GET");
|
|
934
|
+
let body = options.body;
|
|
935
|
+
let response: StealthResponse | undefined;
|
|
936
|
+
const visitedRequests = new Set<string>();
|
|
937
|
+
|
|
938
|
+
const { url: _url, maxHops: _maxHops, stopWhen, params, ...fetchOptions } = options;
|
|
939
|
+
|
|
940
|
+
for (let hopIndex = 0; hopIndex <= maxHops; hopIndex += 1) {
|
|
941
|
+
visitedRequests.add(`${method} ${currentUrl}`);
|
|
942
|
+
response = await session.fetch(currentUrl, {
|
|
943
|
+
...fetchOptions,
|
|
944
|
+
body,
|
|
945
|
+
method,
|
|
946
|
+
...(hopIndex === 0 && params ? { params } : {}),
|
|
947
|
+
redirect: "manual",
|
|
948
|
+
throwOnHttpError: false,
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
if (!isRedirectStatus(response.status)) {
|
|
952
|
+
return {
|
|
953
|
+
final: response,
|
|
954
|
+
hops,
|
|
955
|
+
reason: "completed",
|
|
956
|
+
cookies: cookieJar.snapshot(),
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const location = locationHeader(response.headers);
|
|
961
|
+
const nextUrl = location
|
|
962
|
+
? new URL(location, response.url ?? currentUrl).toString()
|
|
963
|
+
: undefined;
|
|
964
|
+
const hop: StealthRedirectHop = {
|
|
965
|
+
url: response.url ?? currentUrl,
|
|
966
|
+
status: response.status,
|
|
967
|
+
method,
|
|
968
|
+
...(location ? { location } : {}),
|
|
969
|
+
...(nextUrl ? { nextUrl } : {}),
|
|
970
|
+
};
|
|
971
|
+
hops.push(hop);
|
|
972
|
+
|
|
973
|
+
if (stopWhen && (await stopWhen(hop))) {
|
|
974
|
+
return {
|
|
975
|
+
final: response,
|
|
976
|
+
hops,
|
|
977
|
+
reason: "stopped",
|
|
978
|
+
cookies: cookieJar.snapshot(),
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (!nextUrl) {
|
|
983
|
+
return {
|
|
984
|
+
final: response,
|
|
985
|
+
hops,
|
|
986
|
+
reason: "missing_location",
|
|
987
|
+
cookies: cookieJar.snapshot(),
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (hops.length > maxHops) {
|
|
992
|
+
return {
|
|
993
|
+
final: response,
|
|
994
|
+
hops,
|
|
995
|
+
reason: "max_hops",
|
|
996
|
+
cookies: cookieJar.snapshot(),
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const nextMethod = nextRedirectMethod(response.status, method);
|
|
1001
|
+
if (nextMethod !== method) {
|
|
1002
|
+
body = undefined;
|
|
1003
|
+
}
|
|
1004
|
+
if (visitedRequests.has(`${nextMethod} ${nextUrl}`)) {
|
|
1005
|
+
return {
|
|
1006
|
+
final: response,
|
|
1007
|
+
hops,
|
|
1008
|
+
reason: "loop",
|
|
1009
|
+
cookies: cookieJar.snapshot(),
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
method = nextMethod;
|
|
1013
|
+
currentUrl = nextUrl;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (!response) {
|
|
1017
|
+
response = await session.fetch(currentUrl, {
|
|
1018
|
+
...fetchOptions,
|
|
1019
|
+
body,
|
|
1020
|
+
method,
|
|
1021
|
+
...(params ? { params } : {}),
|
|
1022
|
+
redirect: "manual",
|
|
1023
|
+
throwOnHttpError: false,
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
return {
|
|
1027
|
+
final: response,
|
|
1028
|
+
hops,
|
|
1029
|
+
reason: "max_hops",
|
|
1030
|
+
cookies: cookieJar.snapshot(),
|
|
1031
|
+
};
|
|
1032
|
+
},
|
|
1033
|
+
},
|
|
1055
1034
|
close() {
|
|
1056
1035
|
closed = true;
|
|
1057
1036
|
clients.clear();
|
|
1058
1037
|
},
|
|
1059
1038
|
};
|
|
1039
|
+
return session;
|
|
1060
1040
|
|
|
1061
1041
|
async function classifyProxyAuthDiagnostic(
|
|
1062
1042
|
profileName: string,
|
|
1063
1043
|
proxy: string,
|
|
1064
1044
|
): Promise<"source_ip_denied" | "edge_auth_rejected" | undefined> {
|
|
1065
1045
|
try {
|
|
1066
|
-
const response = await getClient(profileName, proxy, false).fetch(
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
timeout: PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS,
|
|
1071
|
-
},
|
|
1072
|
-
);
|
|
1046
|
+
const response = await getClient(profileName, proxy, false).fetch(PROXY_AUTH_DIAGNOSTIC_URL, {
|
|
1047
|
+
method: "GET",
|
|
1048
|
+
timeout: PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS,
|
|
1049
|
+
});
|
|
1073
1050
|
const normalized = await normalizeResponse(response);
|
|
1074
1051
|
return classifyProxyAuthDiagnosticMessage(normalized.body);
|
|
1075
1052
|
} catch (error) {
|
|
1076
1053
|
const message =
|
|
1077
1054
|
error instanceof Error
|
|
1078
|
-
? [
|
|
1079
|
-
error.message,
|
|
1080
|
-
error.cause instanceof Error ? error.cause.message : "",
|
|
1081
|
-
]
|
|
1055
|
+
? [error.message, error.cause instanceof Error ? error.cause.message : ""]
|
|
1082
1056
|
.filter(Boolean)
|
|
1083
1057
|
.join(" ")
|
|
1084
1058
|
: String(error);
|
|
@@ -1118,22 +1092,14 @@ export function createStealthClient(
|
|
|
1118
1092
|
clientOptions: StealthClientOptions = {},
|
|
1119
1093
|
): StealthClient {
|
|
1120
1094
|
const defaultProfile =
|
|
1121
|
-
typeof defaultProfileOrOptions === "string"
|
|
1122
|
-
? defaultProfileOrOptions
|
|
1123
|
-
: DEFAULT_PROFILE;
|
|
1095
|
+
typeof defaultProfileOrOptions === "string" ? defaultProfileOrOptions : DEFAULT_PROFILE;
|
|
1124
1096
|
const resolvedClientOptions =
|
|
1125
|
-
typeof defaultProfileOrOptions === "string"
|
|
1126
|
-
? clientOptions
|
|
1127
|
-
: defaultProfileOrOptions;
|
|
1097
|
+
typeof defaultProfileOrOptions === "string" ? clientOptions : defaultProfileOrOptions;
|
|
1128
1098
|
let sharedSession: StealthSession | null = null;
|
|
1129
1099
|
|
|
1130
1100
|
function getSharedSession(): StealthSession {
|
|
1131
1101
|
if (!sharedSession) {
|
|
1132
|
-
sharedSession = createSessionFetcher(
|
|
1133
|
-
baseUrl,
|
|
1134
|
-
defaultProfile,
|
|
1135
|
-
resolvedClientOptions,
|
|
1136
|
-
);
|
|
1102
|
+
sharedSession = createSessionFetcher(baseUrl, defaultProfile, resolvedClientOptions);
|
|
1137
1103
|
}
|
|
1138
1104
|
|
|
1139
1105
|
return sharedSession;
|
|
@@ -1145,11 +1111,7 @@ export function createStealthClient(
|
|
|
1145
1111
|
},
|
|
1146
1112
|
createSession(opts?: { profile?: string }) {
|
|
1147
1113
|
const sessionProfile = opts?.profile ?? defaultProfile;
|
|
1148
|
-
return createSessionFetcher(
|
|
1149
|
-
baseUrl,
|
|
1150
|
-
sessionProfile,
|
|
1151
|
-
resolvedClientOptions,
|
|
1152
|
-
);
|
|
1114
|
+
return createSessionFetcher(baseUrl, sessionProfile, resolvedClientOptions);
|
|
1153
1115
|
},
|
|
1154
1116
|
close() {
|
|
1155
1117
|
sharedSession?.close();
|