@apifuse/provider-sdk 2.2.0-beta.2 → 2.2.0-beta.21
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 +487 -0
- package/CHANGELOG.md +90 -0
- package/README.md +49 -3
- package/SUBMISSION.md +1 -1
- 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 +14 -0
- package/bin/apifuse-pack-smoke.ts +36 -81
- package/bin/apifuse-pack-types.ts +305 -0
- package/bin/apifuse-perf.ts +45 -127
- package/bin/apifuse-record.ts +659 -111
- package/bin/apifuse-submit-check.ts +546 -48
- 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/index.d.ts +3 -3
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +9 -18
- package/dist/ceremonies/index.d.ts +9 -1
- package/dist/ceremonies/index.js +65 -18
- 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/config/loader.d.ts +176 -8
- package/dist/config/loader.js +424 -95
- 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 +4 -6
- package/dist/define.d.ts +9 -1
- package/dist/define.js +304 -119
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/error-resolution.d.ts +2 -0
- package/dist/error-resolution.js +90 -0
- package/dist/errors.d.ts +22 -1
- package/dist/errors.js +88 -0
- package/dist/fixture-sanitization.d.ts +26 -0
- package/dist/fixture-sanitization.js +216 -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 +46 -42
- package/dist/index.js +41 -37
- package/dist/lint.d.ts +1 -1
- package/dist/lint.js +8 -15
- 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 +12 -11
- package/dist/provider.js +10 -9
- 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 +2 -1
- package/dist/runtime/auth-flow.js +4 -3
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +15 -29
- 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 +31 -4
- package/dist/runtime/http.d.ts +2 -2
- package/dist/runtime/http.js +387 -47
- 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 +345 -22
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- package/dist/runtime/native-network.d.ts +127 -0
- package/dist/runtime/native-network.js +1298 -0
- 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 +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-retry-policy.d.ts +2 -2
- package/dist/runtime/proxy-retry-policy.js +2 -2
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +55 -52
- 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 +2 -4
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- 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 +238 -26
- package/dist/runtime/stealth.d.ts +5 -3
- package/dist/runtime/stealth.js +423 -143
- 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 -7
- package/dist/server/index.js +6 -6
- 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-redaction.d.ts +1 -1
- package/dist/server/self-test-redaction.js +1 -1
- package/dist/server/self-test.d.ts +104 -3
- package/dist/server/self-test.js +673 -115
- package/dist/server/serve.d.ts +116 -4
- package/dist/server/serve.js +799 -128
- package/dist/server/types.d.ts +34 -9
- package/dist/server/types.js +8 -1
- package/dist/stateful/errors.d.ts +14 -0
- package/dist/stateful/errors.js +14 -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 +71 -0
- package/dist/stateful/stateful-provider-session-routing.js +353 -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.d.ts +1 -1
- package/dist/stealth/profiles.js +5 -14
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- 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 +32 -2
- package/dist/testing/run.js +478 -28
- package/dist/types.d.ts +342 -14
- package/dist/types.js +1 -0
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +16 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +28 -86
- package/src/ceremonies/index.ts +103 -78
- 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/config/loader.ts +652 -204
- package/src/contract-serialization.ts +5 -11
- package/src/contract-types.ts +2 -2
- package/src/contract.ts +12 -28
- package/src/define.ts +499 -498
- package/src/dev.ts +4 -9
- package/src/error-resolution.ts +91 -0
- package/src/errors.ts +110 -5
- package/src/fixture-sanitization.ts +247 -0
- 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 +112 -42
- package/src/lint.ts +88 -152
- 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 +66 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +7 -7
- package/src/runtime/browser.ts +64 -187
- 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 +43 -20
- package/src/runtime/http.ts +494 -57
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +495 -66
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- package/src/runtime/native-network.ts +1686 -0
- 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 +221 -0
- package/src/runtime/proxy-retry-policy.ts +3 -3
- package/src/runtime/proxy-telemetry.ts +79 -77
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +4 -12
- package/src/runtime/request-options.ts +679 -9
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +353 -133
- package/src/runtime/stealth.ts +505 -154
- 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 +29 -7
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test-redaction.ts +2 -2
- package/src/server/self-test.ts +857 -132
- package/src/server/serve.ts +1151 -328
- package/src/server/types.ts +12 -13
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +23 -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 +555 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +10 -26
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -19
- package/src/testing/index.ts +10 -2
- package/src/testing/run.ts +653 -74
- package/src/types.ts +408 -28
- package/src/user-input.ts +118 -0
- 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/src/runtime/stealth.ts
CHANGED
|
@@ -1,26 +1,32 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import type { Browser, ImpitOptions, ImpitResponse, RequestInit } from "impit";
|
|
3
3
|
import { Impit } from "impit";
|
|
4
|
+
import { Cookie, CookieJar as ToughCookieJar } from "tough-cookie";
|
|
4
5
|
|
|
5
|
-
import type { ProxyResolutionOptions } from "../config/loader";
|
|
6
|
+
import type { ProxyResolutionOptions, ProxyVendorName } from "../config/loader.js";
|
|
6
7
|
import {
|
|
7
8
|
DEFAULT_SMARTPROXY_POOL_SIZE,
|
|
8
9
|
invalidateProxyResolutionCacheAsync,
|
|
9
10
|
ProxyResolutionError,
|
|
11
|
+
policyResolvesRegistryVendorChain,
|
|
12
|
+
resolvePolicyProxyPoolSpan,
|
|
13
|
+
resolvePolicyTransportAttemptCap,
|
|
10
14
|
resolveProxyConfigAsync,
|
|
11
|
-
|
|
12
|
-
} from "../config/loader";
|
|
13
|
-
import { SDKError, TransportError } from "../errors";
|
|
14
|
-
import { getStealthProfile } from "../stealth/profiles";
|
|
15
|
+
vendorFromResolvedSource,
|
|
16
|
+
} from "../config/loader.js";
|
|
17
|
+
import { SDKError, StealthCookieStoreVersionError, TransportError } from "../errors.js";
|
|
18
|
+
import { getStealthProfile } from "../stealth/profiles.js";
|
|
15
19
|
import type {
|
|
16
20
|
CookieJar,
|
|
17
21
|
HttpMethod,
|
|
18
22
|
StealthClient,
|
|
23
|
+
StealthCookieStore,
|
|
24
|
+
StealthCookieStoreV1,
|
|
19
25
|
StealthFetchOptions,
|
|
20
26
|
StealthRedirectHop,
|
|
21
27
|
StealthResponse,
|
|
22
28
|
StealthSession,
|
|
23
|
-
} from "../types";
|
|
29
|
+
} from "../types.js";
|
|
24
30
|
import {
|
|
25
31
|
createProxyAuthIpDeniedError,
|
|
26
32
|
createProxyEdgeAuthRejectedError,
|
|
@@ -35,7 +41,7 @@ import {
|
|
|
35
41
|
isProxyPoolStaleStatus,
|
|
36
42
|
PROXY_EDGE_AUTH_REJECTED_CODE,
|
|
37
43
|
PROXY_POOL_STALE_CODE,
|
|
38
|
-
} from "./proxy-errors";
|
|
44
|
+
} from "./proxy-errors.js";
|
|
39
45
|
import {
|
|
40
46
|
computeProxyAttemptIndex,
|
|
41
47
|
computeProxyTransportRetryDelayMs,
|
|
@@ -43,15 +49,27 @@ import {
|
|
|
43
49
|
normalizeProxyTransportRetryOptions,
|
|
44
50
|
shouldRetryProxyTransportAttempt,
|
|
45
51
|
validateUnsafeProxyTransportRetryMethods,
|
|
46
|
-
} from "./proxy-retry-policy";
|
|
47
|
-
import {
|
|
52
|
+
} from "./proxy-retry-policy.js";
|
|
53
|
+
import {
|
|
54
|
+
evaluateRedirectHop,
|
|
55
|
+
isRedirectStatus,
|
|
56
|
+
resolveRedirectUrl,
|
|
57
|
+
} from "./redirects.js";
|
|
58
|
+
import {
|
|
59
|
+
isSensitiveKey,
|
|
60
|
+
redactSensitiveError,
|
|
61
|
+
redactSensitiveRequestError,
|
|
62
|
+
redactSensitiveText,
|
|
63
|
+
redactUrlQueryParams,
|
|
64
|
+
normalizeSensitiveParams,
|
|
65
|
+
serializeRequestUrl,
|
|
66
|
+
} from "./request-options.js";
|
|
48
67
|
|
|
49
68
|
const DEFAULT_PROFILE = "chrome-146";
|
|
50
69
|
|
|
51
70
|
const MISSING_PROXY_WARNING =
|
|
52
71
|
"[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
|
|
53
72
|
|
|
54
|
-
const MAX_POLICY_PROXY_RETRY_ATTEMPTS = SMARTPROXY_MAX_POOL_SIZE;
|
|
55
73
|
const MAX_POLICY_PROXY_POOL_REFRESHES = 1;
|
|
56
74
|
const PROXY_CONNECT_FAILURE_CODE = "proxy_connect_failed";
|
|
57
75
|
const PROXY_CONNECT_FAILURE_BODY_PATTERN =
|
|
@@ -60,6 +78,14 @@ const PROXY_AUTH_DIAGNOSTIC_URL = "http://example.com/";
|
|
|
60
78
|
const PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS = 5_000;
|
|
61
79
|
const STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES = [PROXY_CONNECT_FAILURE_CODE] as const;
|
|
62
80
|
|
|
81
|
+
function sensitiveQueryParamNames(url: string): string[] {
|
|
82
|
+
const queryStart = url.indexOf("?");
|
|
83
|
+
if (queryStart === -1) return [];
|
|
84
|
+
const fragmentStart = url.indexOf("#", queryStart);
|
|
85
|
+
const query = url.slice(queryStart + 1, fragmentStart === -1 ? undefined : fragmentStart);
|
|
86
|
+
return [...new URLSearchParams(query).keys()].filter(isSensitiveKey);
|
|
87
|
+
}
|
|
88
|
+
|
|
63
89
|
export type StealthClientOptions = ProxyResolutionOptions & {
|
|
64
90
|
warn?: (message: string) => void;
|
|
65
91
|
/**
|
|
@@ -112,6 +138,8 @@ type StealthTransportResponse = Pick<
|
|
|
112
138
|
ImpitResponse,
|
|
113
139
|
"arrayBuffer" | "headers" | "json" | "ok" | "status" | "text"
|
|
114
140
|
> & {
|
|
141
|
+
body?: ReadableStream<Uint8Array>;
|
|
142
|
+
abort?: () => void;
|
|
115
143
|
url?: string;
|
|
116
144
|
redirected?: boolean;
|
|
117
145
|
};
|
|
@@ -125,81 +153,145 @@ function isRecord(value: unknown): value is Record<PropertyKey, unknown> {
|
|
|
125
153
|
return typeof value === "object" && value !== null;
|
|
126
154
|
}
|
|
127
155
|
|
|
156
|
+
const LEGACY_COOKIE_ORIGIN = "https://legacy-cookie.invalid/";
|
|
157
|
+
|
|
128
158
|
class CookieJarImpl implements CookieJar {
|
|
129
|
-
private
|
|
159
|
+
private cookies: ToughCookieJar;
|
|
160
|
+
private readonly defaultUrl: string;
|
|
130
161
|
|
|
131
|
-
constructor(cookieStrings: string[]) {
|
|
132
|
-
this.cookies = {
|
|
162
|
+
constructor(cookieStrings: readonly string[], defaultUrl = LEGACY_COOKIE_ORIGIN) {
|
|
163
|
+
this.cookies = new ToughCookieJar(undefined, {
|
|
164
|
+
allowSecureOnLocal: false,
|
|
165
|
+
rejectPublicSuffixes: true,
|
|
166
|
+
});
|
|
167
|
+
this.defaultUrl = this.normalizeUrl(defaultUrl) ?? LEGACY_COOKIE_ORIGIN;
|
|
133
168
|
this.setFromCookieStrings(cookieStrings);
|
|
134
169
|
}
|
|
135
170
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
171
|
+
/**
|
|
172
|
+
* URL-less legacy operations are scoped to this jar's default URL. Session
|
|
173
|
+
* jars use the client's base URL and response jars use the response URL. A
|
|
174
|
+
* flat restore has no attributes to recover, so it creates host-only Path=/
|
|
175
|
+
* cookies for that default URL instead of making them visible to every host.
|
|
176
|
+
*/
|
|
177
|
+
setFromCookieStrings(cookieStrings: readonly string[], url = this.defaultUrl): void {
|
|
178
|
+
const cookieUrl = this.normalizeUrl(url);
|
|
179
|
+
if (!cookieUrl) return;
|
|
147
180
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (name) this.cookies[name] = value;
|
|
181
|
+
for (const cookieString of cookieStrings) {
|
|
182
|
+
this.cookies.setCookieSync(cookieString, cookieUrl, { ignoreError: true });
|
|
151
183
|
}
|
|
152
184
|
}
|
|
153
185
|
|
|
154
|
-
get(name: string): string | undefined {
|
|
155
|
-
return this.
|
|
186
|
+
get(name: string, url?: string): string | undefined {
|
|
187
|
+
return this.getAll(url)[name];
|
|
156
188
|
}
|
|
157
189
|
|
|
158
|
-
getAll(): Record<string, string> {
|
|
159
|
-
return
|
|
190
|
+
getAll(url?: string): Record<string, string> {
|
|
191
|
+
return Object.fromEntries(
|
|
192
|
+
this.getUniqueCookies(url ?? this.defaultUrl).map((cookie) => [cookie.key, cookie.value]),
|
|
193
|
+
);
|
|
160
194
|
}
|
|
161
195
|
|
|
162
|
-
has(name: string): boolean {
|
|
163
|
-
return Object.hasOwn(this.
|
|
196
|
+
has(name: string, url?: string): boolean {
|
|
197
|
+
return Object.hasOwn(this.getAll(url), name);
|
|
164
198
|
}
|
|
165
199
|
|
|
166
|
-
toString(): string {
|
|
167
|
-
return
|
|
168
|
-
.map((
|
|
200
|
+
toString(url?: string): string {
|
|
201
|
+
return this.getUniqueCookies(url ?? this.defaultUrl)
|
|
202
|
+
.map((cookie) => cookie.cookieString())
|
|
169
203
|
.join("; ");
|
|
170
204
|
}
|
|
171
205
|
|
|
172
|
-
toHeader(): string {
|
|
173
|
-
return this.toString();
|
|
206
|
+
toHeader(url?: string): string {
|
|
207
|
+
return this.toString(url);
|
|
174
208
|
}
|
|
175
209
|
|
|
176
210
|
snapshot(): Record<string, string> {
|
|
177
|
-
|
|
211
|
+
// This compatibility view deliberately enumerates the serialized store,
|
|
212
|
+
// not getAll(defaultUrl): persistence must include sibling hosts and paths.
|
|
213
|
+
// Duplicate names still collapse because a flat map cannot represent them.
|
|
214
|
+
const entries: [string, string][] = [];
|
|
215
|
+
for (const cookie of this.serialize().jar.cookies) {
|
|
216
|
+
if (typeof cookie.key === "string" && typeof cookie.value === "string" && cookie.key) {
|
|
217
|
+
entries.push([cookie.key, cookie.value]);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return Object.fromEntries(entries);
|
|
178
221
|
}
|
|
179
222
|
|
|
180
223
|
restore(cookies: Record<string, string>): void {
|
|
181
224
|
this.clear();
|
|
182
225
|
for (const [name, value] of Object.entries(cookies)) {
|
|
183
|
-
if (name)
|
|
226
|
+
if (!name) continue;
|
|
227
|
+
this.cookies.setCookieSync(new Cookie({ key: name, path: "/", value }), this.defaultUrl, {
|
|
228
|
+
ignoreError: true,
|
|
229
|
+
});
|
|
184
230
|
}
|
|
185
231
|
}
|
|
186
232
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
233
|
+
serialize(): StealthCookieStoreV1 {
|
|
234
|
+
const jar = this.cookies.serializeSync();
|
|
235
|
+
if (!jar) {
|
|
236
|
+
throw new SDKError("Stealth cookie store could not be serialized", {
|
|
237
|
+
code: "stealth_cookie_store_serialize_failed",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return { version: 1, jar };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
deserialize(state: StealthCookieStore): void {
|
|
244
|
+
const version = isRecord(state) ? state.version : undefined;
|
|
245
|
+
if (version !== 1) {
|
|
246
|
+
throw new StealthCookieStoreVersionError(version);
|
|
190
247
|
}
|
|
248
|
+
|
|
249
|
+
// Deserialize into a new jar first so invalid state cannot partially clear
|
|
250
|
+
// or replace a live session. tough-cookie restores the cookie attributes and
|
|
251
|
+
// matching semantics represented in its own serialized format.
|
|
252
|
+
const restored = ToughCookieJar.deserializeSync(state.jar);
|
|
253
|
+
// tough-cookie 6 does not include this option in serializeSync(). Preserve
|
|
254
|
+
// the SDK's stricter setting across restoration.
|
|
255
|
+
Reflect.set(restored, "allowSecureOnLocal", false);
|
|
256
|
+
this.cookies = restored;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
clear(): void {
|
|
260
|
+
this.cookies.removeAllCookiesSync();
|
|
191
261
|
}
|
|
192
262
|
|
|
193
|
-
find(predicate: (cookie: string) => boolean): string | undefined {
|
|
194
|
-
for (const
|
|
195
|
-
const
|
|
196
|
-
if (predicate(
|
|
197
|
-
return
|
|
263
|
+
find(predicate: (cookie: string) => boolean, url?: string): string | undefined {
|
|
264
|
+
for (const cookie of this.getUniqueCookies(url ?? this.defaultUrl)) {
|
|
265
|
+
const cookieString = cookie.cookieString();
|
|
266
|
+
if (predicate(cookieString)) {
|
|
267
|
+
return cookieString;
|
|
198
268
|
}
|
|
199
269
|
}
|
|
200
270
|
|
|
201
271
|
return undefined;
|
|
202
272
|
}
|
|
273
|
+
|
|
274
|
+
private normalizeUrl(url: string): string | undefined {
|
|
275
|
+
try {
|
|
276
|
+
return new URL(url).toString();
|
|
277
|
+
} catch {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private getUniqueCookies(url: string): Cookie[] {
|
|
283
|
+
const cookieUrl = this.normalizeUrl(url);
|
|
284
|
+
if (!cookieUrl) return [];
|
|
285
|
+
|
|
286
|
+
// tough-cookie returns longer (more-specific) paths first. Keeping the
|
|
287
|
+
// first cookie for each name prevents ambiguous duplicate-name headers.
|
|
288
|
+
const names = new Set<string>();
|
|
289
|
+
return this.cookies.getCookiesSync(cookieUrl).filter((cookie) => {
|
|
290
|
+
if (names.has(cookie.key)) return false;
|
|
291
|
+
names.add(cookie.key);
|
|
292
|
+
return true;
|
|
293
|
+
});
|
|
294
|
+
}
|
|
203
295
|
}
|
|
204
296
|
|
|
205
297
|
function closestImpitBrowser(
|
|
@@ -280,12 +372,12 @@ function hasOwn(object: object, key: string): boolean {
|
|
|
280
372
|
}
|
|
281
373
|
function toImpitCookieJar(cookieJar: CookieJarImpl): NonNullable<ImpitOptions["cookieJar"]> {
|
|
282
374
|
return {
|
|
283
|
-
setCookie(cookie: string,
|
|
284
|
-
cookieJar.setFromCookieStrings([cookie]);
|
|
375
|
+
setCookie(cookie: string, url: string, cb?: (error?: unknown) => void) {
|
|
376
|
+
cookieJar.setFromCookieStrings([cookie], url);
|
|
285
377
|
if (typeof cb === "function") cb();
|
|
286
378
|
},
|
|
287
|
-
getCookieString(
|
|
288
|
-
return cookieJar.
|
|
379
|
+
getCookieString(url: string) {
|
|
380
|
+
return cookieJar.toHeader(url);
|
|
289
381
|
},
|
|
290
382
|
};
|
|
291
383
|
}
|
|
@@ -337,10 +429,17 @@ function splitCombinedSetCookieHeader(headerValue: string): string[] {
|
|
|
337
429
|
export async function normalizeResponse(
|
|
338
430
|
response: StealthTransportResponse,
|
|
339
431
|
requestUrl?: string,
|
|
432
|
+
maxBodyBytes?: number,
|
|
340
433
|
): Promise<StealthResponse> {
|
|
341
434
|
const headers = Object.fromEntries(response.headers.entries());
|
|
342
|
-
const cookies = new CookieJarImpl(
|
|
343
|
-
|
|
435
|
+
const cookies = new CookieJarImpl(
|
|
436
|
+
setCookieHeadersFromResponse(response.headers),
|
|
437
|
+
response.url ?? requestUrl,
|
|
438
|
+
);
|
|
439
|
+
const bodyBytes =
|
|
440
|
+
maxBodyBytes === undefined
|
|
441
|
+
? await response.arrayBuffer()
|
|
442
|
+
: await readResponseBodyWithLimit(response, maxBodyBytes);
|
|
344
443
|
const body = new TextDecoder().decode(bodyBytes);
|
|
345
444
|
|
|
346
445
|
return {
|
|
@@ -368,6 +467,83 @@ export async function normalizeResponse(
|
|
|
368
467
|
};
|
|
369
468
|
}
|
|
370
469
|
|
|
470
|
+
function responseTooLargeError(maxBodyBytes: number, observedBytes: number): TransportError {
|
|
471
|
+
return new TransportError(
|
|
472
|
+
`Response body exceeded maxBodyBytes limit of ${maxBodyBytes} bytes (observed ${observedBytes} bytes)`,
|
|
473
|
+
{
|
|
474
|
+
code: "response_too_large",
|
|
475
|
+
category: "upstream_http",
|
|
476
|
+
retryable: false,
|
|
477
|
+
status: 0,
|
|
478
|
+
},
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function declaredContentLength(headers: Headers): number | undefined {
|
|
483
|
+
const contentLength = headers.get("content-length")?.trim();
|
|
484
|
+
if (!contentLength || !/^\d+$/.test(contentLength)) return undefined;
|
|
485
|
+
const parsed = Number(contentLength);
|
|
486
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function abortTransportResponse(response: StealthTransportResponse): boolean {
|
|
490
|
+
if (!response.abort) return false;
|
|
491
|
+
try {
|
|
492
|
+
response.abort();
|
|
493
|
+
} catch {
|
|
494
|
+
// The size error remains the primary failure if impit has already closed the response.
|
|
495
|
+
}
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function readResponseBodyWithLimit(
|
|
500
|
+
response: StealthTransportResponse,
|
|
501
|
+
maxBodyBytes: number,
|
|
502
|
+
): Promise<ArrayBuffer> {
|
|
503
|
+
const contentLength = declaredContentLength(response.headers);
|
|
504
|
+
if (contentLength !== undefined && contentLength > maxBodyBytes) {
|
|
505
|
+
if (!abortTransportResponse(response)) {
|
|
506
|
+
await response.body?.cancel().catch(() => undefined);
|
|
507
|
+
}
|
|
508
|
+
throw responseTooLargeError(maxBodyBytes, contentLength);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (!response.body) {
|
|
512
|
+
throw new TransportError("Response body stream is unavailable", {
|
|
513
|
+
code: "transport_stream_unavailable",
|
|
514
|
+
category: "upstream_http",
|
|
515
|
+
status: 0,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const reader = response.body.getReader();
|
|
520
|
+
const chunks: Uint8Array[] = [];
|
|
521
|
+
let receivedBytes = 0;
|
|
522
|
+
try {
|
|
523
|
+
while (true) {
|
|
524
|
+
const { done, value } = await reader.read();
|
|
525
|
+
if (done) break;
|
|
526
|
+
receivedBytes += value.byteLength;
|
|
527
|
+
if (receivedBytes > maxBodyBytes) {
|
|
528
|
+
await reader.cancel().catch(() => undefined);
|
|
529
|
+
abortTransportResponse(response);
|
|
530
|
+
throw responseTooLargeError(maxBodyBytes, receivedBytes);
|
|
531
|
+
}
|
|
532
|
+
chunks.push(value);
|
|
533
|
+
}
|
|
534
|
+
} finally {
|
|
535
|
+
reader.releaseLock();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const bodyBytes = new Uint8Array(receivedBytes);
|
|
539
|
+
let offset = 0;
|
|
540
|
+
for (const chunk of chunks) {
|
|
541
|
+
bodyBytes.set(chunk, offset);
|
|
542
|
+
offset += chunk.byteLength;
|
|
543
|
+
}
|
|
544
|
+
return bodyBytes.buffer;
|
|
545
|
+
}
|
|
546
|
+
|
|
371
547
|
function normalizeBody(body: StealthFetchOptions["body"]): string {
|
|
372
548
|
if (body === undefined) {
|
|
373
549
|
return "";
|
|
@@ -426,6 +602,7 @@ type ResolvedAttemptProxy = {
|
|
|
426
602
|
url?: string;
|
|
427
603
|
poolIndex?: number;
|
|
428
604
|
proxyHash?: string;
|
|
605
|
+
vendor?: ProxyVendorName;
|
|
429
606
|
};
|
|
430
607
|
|
|
431
608
|
function proxyPoolIndexFromDiagnostics(
|
|
@@ -559,16 +736,6 @@ function normalizeMethod(method: HttpMethod | string): StealthMethod {
|
|
|
559
736
|
}
|
|
560
737
|
}
|
|
561
738
|
|
|
562
|
-
function isRedirectStatus(status: number): boolean {
|
|
563
|
-
return [301, 302, 303, 307, 308].includes(status);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function nextRedirectMethod(status: number, method: StealthMethod): StealthMethod {
|
|
567
|
-
if (status === 303 && method !== "HEAD") return "GET";
|
|
568
|
-
if ((status === 301 || status === 302) && method === "POST") return "GET";
|
|
569
|
-
return method;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
739
|
function locationHeader(headers: Record<string, string>): string | undefined {
|
|
573
740
|
for (const [name, value] of Object.entries(headers)) {
|
|
574
741
|
if (name.toLowerCase() === "location") return value;
|
|
@@ -585,7 +752,7 @@ function createSessionFetcher(
|
|
|
585
752
|
let closed = false;
|
|
586
753
|
let hasWarnedMissingProxy = false;
|
|
587
754
|
const warn = clientOptions.warn ?? console.warn;
|
|
588
|
-
const cookieJar = new CookieJarImpl([]);
|
|
755
|
+
const cookieJar = new CookieJarImpl([], baseUrl);
|
|
589
756
|
const impitCookieJar = toImpitCookieJar(cookieJar);
|
|
590
757
|
|
|
591
758
|
function getClient(
|
|
@@ -615,6 +782,7 @@ function createSessionFetcher(
|
|
|
615
782
|
async function resolveRequestProxy(
|
|
616
783
|
options?: StealthFetchOptions,
|
|
617
784
|
proxyAttempt?: number,
|
|
785
|
+
refreshEpoch?: number,
|
|
618
786
|
): Promise<ResolvedAttemptProxy> {
|
|
619
787
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
620
788
|
proxy: options?.proxy ?? clientOptions.proxy,
|
|
@@ -626,6 +794,10 @@ function createSessionFetcher(
|
|
|
626
794
|
proxyAttemptOffset: options?.proxyAttemptOffset,
|
|
627
795
|
retryAttemptOffset: proxyAttempt,
|
|
628
796
|
}),
|
|
797
|
+
// The impit stealth transport tunnels both HTTP CONNECT and SOCKS5,
|
|
798
|
+
// preserving the client TLS fingerprint end-to-end.
|
|
799
|
+
transportProtocols: ["http", "socks5"],
|
|
800
|
+
...(refreshEpoch === undefined ? {} : { proxyRefreshEpoch: refreshEpoch }),
|
|
629
801
|
telemetry: clientOptions.telemetry,
|
|
630
802
|
});
|
|
631
803
|
|
|
@@ -638,42 +810,65 @@ function createSessionFetcher(
|
|
|
638
810
|
url: resolvedProxy.url,
|
|
639
811
|
poolIndex: proxyPoolIndexFromDiagnostics(resolvedProxy.diagnostics),
|
|
640
812
|
proxyHash: proxyEndpointHash(resolvedProxy.url),
|
|
813
|
+
vendor: vendorFromResolvedSource(resolvedProxy.source),
|
|
641
814
|
};
|
|
642
815
|
}
|
|
643
816
|
|
|
644
817
|
const session: StealthSession = {
|
|
645
818
|
async fetch(url, options: StealthFetchOptions = {}) {
|
|
646
|
-
const method =
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}) ??
|
|
653
|
-
(hasExplicitRetryPolicy
|
|
654
|
-
? undefined
|
|
655
|
-
: createDefaultProxyTransportRetryOptions({
|
|
819
|
+
const { hasExplicitRetryPolicy, method, stealthRetryOptions } = (() => {
|
|
820
|
+
try {
|
|
821
|
+
const method = normalizeMethod(options.method ?? "GET");
|
|
822
|
+
const hasExplicitRetryPolicy = options.retry !== undefined;
|
|
823
|
+
const stealthRetryOptions =
|
|
824
|
+
normalizeProxyTransportRetryOptions(options.retry, {
|
|
656
825
|
extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
|
|
657
826
|
label: "Stealth",
|
|
658
|
-
})
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
827
|
+
}) ??
|
|
828
|
+
(hasExplicitRetryPolicy
|
|
829
|
+
? undefined
|
|
830
|
+
: createDefaultProxyTransportRetryOptions({
|
|
831
|
+
extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
|
|
832
|
+
label: "Stealth",
|
|
833
|
+
}));
|
|
834
|
+
if (stealthRetryOptions) {
|
|
835
|
+
validateUnsafeProxyTransportRetryMethods(stealthRetryOptions, "Stealth");
|
|
836
|
+
}
|
|
837
|
+
return { hasExplicitRetryPolicy, method, stealthRetryOptions };
|
|
838
|
+
} catch (error) {
|
|
839
|
+
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
840
|
+
}
|
|
841
|
+
})();
|
|
662
842
|
const hasPolicyProxy = isPolicyManagedProxy(clientOptions);
|
|
663
843
|
const usesPolicyAllocator = hasPolicyProxy && !options.proxy && !clientOptions.proxy;
|
|
664
844
|
const retryAttemptCap = Math.max(1, stealthRetryOptions?.attempts ?? 1);
|
|
845
|
+
// Span the whole vendor chain: successive attempts rotate one vendor's
|
|
846
|
+
// pool, then fail over to the next vendor via the flat attempt index.
|
|
847
|
+
const policyProxy =
|
|
848
|
+
clientOptions.proxyPolicy ??
|
|
849
|
+
(typeof clientOptions.upstream?.proxy === "object"
|
|
850
|
+
? clientOptions.upstream.proxy
|
|
851
|
+
: undefined);
|
|
852
|
+
// The pool span is already bounded by each vendor's max pool size
|
|
853
|
+
// (smartproxy ≤20, nodemaven ≤50), so the configured span never exceeds
|
|
854
|
+
// the chain's true maximum — a large NodeMaven pool stays fully
|
|
855
|
+
// reachable rather than being truncated at an arbitrary ceiling.
|
|
665
856
|
const policyProxyAttemptCap = Math.max(
|
|
666
857
|
1,
|
|
667
|
-
|
|
668
|
-
MAX_POLICY_PROXY_RETRY_ATTEMPTS,
|
|
669
|
-
clientOptions.proxyPolicy?.session?.poolSize ??
|
|
670
|
-
(typeof clientOptions.upstream?.proxy === "object"
|
|
671
|
-
? clientOptions.upstream.proxy.session?.poolSize
|
|
672
|
-
: undefined) ??
|
|
673
|
-
DEFAULT_SMARTPROXY_POOL_SIZE,
|
|
674
|
-
),
|
|
858
|
+
policyProxy ? resolvePolicyProxyPoolSpan(policyProxy) : DEFAULT_SMARTPROXY_POOL_SIZE,
|
|
675
859
|
);
|
|
676
|
-
|
|
860
|
+
// A registry vendor chain (smartproxy/nodemaven) is the only policy whose
|
|
861
|
+
// successive attempts resolve a *different* endpoint, so it is the only one
|
|
862
|
+
// that may widen the attempt cap to the pool span, de-duplicate endpoints,
|
|
863
|
+
// and drive allocator stale-pool refresh. A static custom/decodo policy
|
|
864
|
+
// resolves the same URL every attempt: widening/refreshing it would resend
|
|
865
|
+
// the request dozens of times (up to maxAttempts × refreshes) and bypass
|
|
866
|
+
// retry:false and unsafe-method controls. Static policies therefore follow
|
|
867
|
+
// the ordinary transport-retry budget instead.
|
|
868
|
+
const rotatesRegistryChain =
|
|
869
|
+
usesPolicyAllocator && policyResolvesRegistryVendorChain(policyProxy);
|
|
870
|
+
const maxAttempts = rotatesRegistryChain ? policyProxyAttemptCap : retryAttemptCap;
|
|
871
|
+
const dedupeAllocatorEndpoints = rotatesRegistryChain;
|
|
677
872
|
let lastError: unknown;
|
|
678
873
|
|
|
679
874
|
for (
|
|
@@ -688,6 +883,11 @@ function createSessionFetcher(
|
|
|
688
883
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
689
884
|
let proxy: string | undefined;
|
|
690
885
|
let attemptProxy: ResolvedAttemptProxy | undefined;
|
|
886
|
+
// Reuse the exact serialization used by this outbound attempt in its catch path.
|
|
887
|
+
let serializedUrl: ReturnType<typeof serializeRequestUrl> | undefined;
|
|
888
|
+
let fallbackSensitiveValues: readonly string[] = [];
|
|
889
|
+
let fallbackRequestUrl: string | undefined;
|
|
890
|
+
let fallbackRedactedUrl: string | undefined;
|
|
691
891
|
const attemptStartedAt = Date.now();
|
|
692
892
|
let attemptRecorded = false;
|
|
693
893
|
const recordProxyAttempt = (
|
|
@@ -698,7 +898,7 @@ function createSessionFetcher(
|
|
|
698
898
|
if (attemptRecorded || !proxy) return;
|
|
699
899
|
attemptRecorded = true;
|
|
700
900
|
clientOptions.telemetry?.recordProxyAttempt?.({
|
|
701
|
-
provider: "smartproxy",
|
|
901
|
+
provider: attemptProxy?.vendor ?? "smartproxy",
|
|
702
902
|
attempt: attempt + 1,
|
|
703
903
|
...(attemptProxy?.poolIndex === undefined
|
|
704
904
|
? {}
|
|
@@ -711,12 +911,27 @@ function createSessionFetcher(
|
|
|
711
911
|
});
|
|
712
912
|
};
|
|
713
913
|
try {
|
|
914
|
+
const sensitiveParams = normalizeSensitiveParams(options.sensitiveParams);
|
|
915
|
+
const structural = redactUrlQueryParams(url, Object.keys(sensitiveParams ?? {}));
|
|
916
|
+
fallbackSensitiveValues = [
|
|
917
|
+
...new Set([
|
|
918
|
+
...Object.values(sensitiveParams ?? {}).map(String),
|
|
919
|
+
...structural.sensitiveValues,
|
|
920
|
+
]),
|
|
921
|
+
].filter((value) => value !== "");
|
|
922
|
+
fallbackRequestUrl = url;
|
|
923
|
+
fallbackRedactedUrl = structural.redactedUrl;
|
|
714
924
|
assertNoUnsupportedFingerprintOverrides(options);
|
|
715
|
-
attemptProxy = await resolveRequestProxy(options, attempt);
|
|
925
|
+
attemptProxy = await resolveRequestProxy(options, attempt, refreshAttempt);
|
|
716
926
|
proxy = attemptProxy.url;
|
|
717
|
-
if (proxy &&
|
|
927
|
+
if (proxy && dedupeAllocatorEndpoints) {
|
|
928
|
+
// An under-filled allocation repeats endpoints (via the modulo
|
|
929
|
+
// pool mapping) before the flat offset crosses into the next
|
|
930
|
+
// vendor. Skip an already-tried endpoint and advance the offset
|
|
931
|
+
// rather than breaking — breaking here would strand the request on
|
|
932
|
+
// the primary vendor and never reach the fallback leg.
|
|
718
933
|
if (attemptedProxies.has(proxy)) {
|
|
719
|
-
|
|
934
|
+
continue;
|
|
720
935
|
}
|
|
721
936
|
attemptedProxies.add(proxy);
|
|
722
937
|
}
|
|
@@ -725,10 +940,15 @@ function createSessionFetcher(
|
|
|
725
940
|
(!hasPolicyProxy && proxy && clientOptions.proxyStealth?.insecureSkipVerify),
|
|
726
941
|
);
|
|
727
942
|
const profileName = options.profile ?? defaultProfile;
|
|
728
|
-
|
|
943
|
+
serializedUrl = serializeRequestUrl(
|
|
944
|
+
resolveUrl(baseUrl, url),
|
|
945
|
+
options.params,
|
|
946
|
+
sensitiveParams,
|
|
947
|
+
);
|
|
948
|
+
const { requestUrl } = serializedUrl;
|
|
729
949
|
const headers = { ...(options.headers ?? {}) };
|
|
730
950
|
if (!hasHeader(headers, "Cookie")) {
|
|
731
|
-
const cookieHeader = cookieJar.
|
|
951
|
+
const cookieHeader = cookieJar.toHeader(requestUrl);
|
|
732
952
|
if (cookieHeader) headers.Cookie = cookieHeader;
|
|
733
953
|
}
|
|
734
954
|
const requestInit: StealthRequestInit = {
|
|
@@ -744,8 +964,11 @@ function createSessionFetcher(
|
|
|
744
964
|
requestUrl,
|
|
745
965
|
requestInit,
|
|
746
966
|
);
|
|
747
|
-
const normalized = await normalizeResponse(response, requestUrl);
|
|
748
|
-
cookieJar.setFromCookieStrings(
|
|
967
|
+
const normalized = await normalizeResponse(response, requestUrl, options.maxBodyBytes);
|
|
968
|
+
cookieJar.setFromCookieStrings(
|
|
969
|
+
setCookieHeadersFromResponse(response.headers),
|
|
970
|
+
response.url ?? requestUrl,
|
|
971
|
+
);
|
|
749
972
|
|
|
750
973
|
if (proxy && isProxyConnectFailureResponse(response, normalized.body)) {
|
|
751
974
|
throw createProxyConnectFailureError(normalized.body);
|
|
@@ -789,16 +1012,36 @@ function createSessionFetcher(
|
|
|
789
1012
|
recordProxyAttempt("ok", undefined, response.status);
|
|
790
1013
|
return normalized;
|
|
791
1014
|
} catch (error) {
|
|
792
|
-
const
|
|
1015
|
+
const sensitiveValues = serializedUrl?.sensitiveValues ?? fallbackSensitiveValues;
|
|
1016
|
+
let normalizedError: TransportError;
|
|
1017
|
+
try {
|
|
1018
|
+
normalizedError = normalizeStealthTransportError(error);
|
|
1019
|
+
} catch (normalizationError) {
|
|
1020
|
+
throw redactSensitiveError(
|
|
1021
|
+
normalizationError,
|
|
1022
|
+
sensitiveValues,
|
|
1023
|
+
serializedUrl?.requestUrl ?? fallbackRequestUrl,
|
|
1024
|
+
serializedUrl?.redactedUrl ?? fallbackRedactedUrl,
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
const retryErrorCode = proxyAttemptErrorCode(normalizedError);
|
|
1028
|
+
const refreshableProxyError = isProxyPoolRefreshableError(normalizedError);
|
|
1029
|
+
const runProxyAuthDiagnostic = shouldRunProxyAuthDiagnostic(normalizedError);
|
|
1030
|
+
normalizedError = redactSensitiveError(
|
|
1031
|
+
normalizedError,
|
|
1032
|
+
sensitiveValues,
|
|
1033
|
+
serializedUrl?.requestUrl ?? fallbackRequestUrl,
|
|
1034
|
+
serializedUrl?.redactedUrl ?? fallbackRedactedUrl,
|
|
1035
|
+
);
|
|
793
1036
|
recordProxyAttempt(
|
|
794
1037
|
"error",
|
|
795
1038
|
proxyAttemptErrorCode(normalizedError),
|
|
796
1039
|
proxyAttemptStatus(normalizedError),
|
|
797
1040
|
);
|
|
798
1041
|
lastError = normalizedError;
|
|
799
|
-
if (proxy &&
|
|
1042
|
+
if (proxy && rotatesRegistryChain && refreshableProxyError) {
|
|
800
1043
|
stalePoolError = normalizedError;
|
|
801
|
-
if (
|
|
1044
|
+
if (runProxyAuthDiagnostic) {
|
|
802
1045
|
stalePoolDiagnosticProxy = proxy;
|
|
803
1046
|
}
|
|
804
1047
|
if (attempt + 1 < maxAttempts) {
|
|
@@ -806,13 +1049,30 @@ function createSessionFetcher(
|
|
|
806
1049
|
}
|
|
807
1050
|
break;
|
|
808
1051
|
}
|
|
1052
|
+
// Cap the number of transport retries. For a policy-allocator chain,
|
|
1053
|
+
// every attempt resolves a *different* endpoint/vendor (poolIndex
|
|
1054
|
+
// rotates across the concatenated vendor pool spans), so a transport
|
|
1055
|
+
// failure is a signal to advance to the next endpoint — potentially
|
|
1056
|
+
// crossing into the fallback vendor — not to retry the same endpoint.
|
|
1057
|
+
// Truncating that rotation at the per-endpoint retry budget would
|
|
1058
|
+
// strand the request on the primary vendor and never reach the
|
|
1059
|
+
// fallback, since the crossover only happens once the flat attempt
|
|
1060
|
+
// index exceeds the primary vendor's pool size (~10-20).
|
|
1061
|
+
// resolvePolicyTransportAttemptCap widens to the full chain span only
|
|
1062
|
+
// for implicit, safe-method allocator requests; explicit retry
|
|
1063
|
+
// policies (their documented `attempts` ceiling), unsafe methods, and
|
|
1064
|
+
// static/non-registry vendors keep the per-endpoint retry budget.
|
|
1065
|
+
const transportRetryCap = resolvePolicyTransportAttemptCap({
|
|
1066
|
+
policy: policyProxy,
|
|
1067
|
+
usesPolicyAllocator,
|
|
1068
|
+
retryAttempts: stealthRetryOptions?.attempts ?? 1,
|
|
1069
|
+
explicitRetry: hasExplicitRetryPolicy,
|
|
1070
|
+
method,
|
|
1071
|
+
});
|
|
809
1072
|
if (
|
|
810
|
-
attempt + 1 <
|
|
811
|
-
(stealthRetryOptions
|
|
812
|
-
? Math.min(maxAttempts, stealthRetryOptions.attempts)
|
|
813
|
-
: maxAttempts) &&
|
|
1073
|
+
attempt + 1 < transportRetryCap &&
|
|
814
1074
|
shouldRetryProxyTransportAttempt({
|
|
815
|
-
error:
|
|
1075
|
+
error: { code: retryErrorCode },
|
|
816
1076
|
explicitRetry: hasExplicitRetryPolicy,
|
|
817
1077
|
method,
|
|
818
1078
|
options: stealthRetryOptions,
|
|
@@ -829,7 +1089,7 @@ function createSessionFetcher(
|
|
|
829
1089
|
}
|
|
830
1090
|
|
|
831
1091
|
if (
|
|
832
|
-
|
|
1092
|
+
rotatesRegistryChain &&
|
|
833
1093
|
stalePoolError &&
|
|
834
1094
|
refreshAttempt < MAX_POLICY_PROXY_POOL_REFRESHES
|
|
835
1095
|
) {
|
|
@@ -882,25 +1142,99 @@ function createSessionFetcher(
|
|
|
882
1142
|
options.maxHops === undefined || !Number.isFinite(options.maxHops)
|
|
883
1143
|
? 10
|
|
884
1144
|
: Math.max(0, Math.floor(options.maxHops));
|
|
1145
|
+
const {
|
|
1146
|
+
url: _url,
|
|
1147
|
+
maxHops: _maxHops,
|
|
1148
|
+
stopWhen,
|
|
1149
|
+
params,
|
|
1150
|
+
sensitiveParams,
|
|
1151
|
+
...fetchOptions
|
|
1152
|
+
} = options;
|
|
885
1153
|
const hops: StealthRedirectHop[] = [];
|
|
886
|
-
let currentUrl = resolveUrl(baseUrl, options.url);
|
|
887
1154
|
let method = normalizeMethod(options.method ?? "GET");
|
|
888
1155
|
let body = options.body;
|
|
889
1156
|
let response: StealthResponse | undefined;
|
|
890
1157
|
const visitedRequests = new Set<string>();
|
|
891
|
-
|
|
892
|
-
|
|
1158
|
+
const initialParams = params
|
|
1159
|
+
? Object.fromEntries(
|
|
1160
|
+
Object.entries(params).map(([key, value]) => [
|
|
1161
|
+
key,
|
|
1162
|
+
Array.isArray(value) ? [...value] : value,
|
|
1163
|
+
]),
|
|
1164
|
+
)
|
|
1165
|
+
: undefined;
|
|
1166
|
+
const normalizedSensitiveParams = normalizeSensitiveParams(sensitiveParams);
|
|
1167
|
+
const initialSensitiveParams = normalizedSensitiveParams
|
|
1168
|
+
? { ...normalizedSensitiveParams }
|
|
1169
|
+
: undefined;
|
|
1170
|
+
const sensitiveParamNames = initialSensitiveParams
|
|
1171
|
+
? Object.keys(initialSensitiveParams)
|
|
1172
|
+
: [];
|
|
1173
|
+
const callerStructural = redactUrlQueryParams(options.url, sensitiveParamNames);
|
|
1174
|
+
const sensitiveValues = new Set(
|
|
1175
|
+
[
|
|
1176
|
+
...Object.values(initialSensitiveParams ?? {}),
|
|
1177
|
+
...callerStructural.sensitiveValues,
|
|
1178
|
+
].filter((value) => value !== ""),
|
|
1179
|
+
);
|
|
1180
|
+
const redactRedirectUrl = (value: string): string => {
|
|
1181
|
+
const structural = redactUrlQueryParams(value, [
|
|
1182
|
+
...new Set([...sensitiveParamNames, ...sensitiveQueryParamNames(value)]),
|
|
1183
|
+
]);
|
|
1184
|
+
for (const sensitiveValue of structural.sensitiveValues) {
|
|
1185
|
+
sensitiveValues.add(sensitiveValue);
|
|
1186
|
+
}
|
|
1187
|
+
return redactSensitiveText(structural.redactedUrl, [...sensitiveValues]);
|
|
1188
|
+
};
|
|
1189
|
+
let currentUrl: string;
|
|
1190
|
+
let initialUrl: ReturnType<typeof serializeRequestUrl>;
|
|
1191
|
+
try {
|
|
1192
|
+
currentUrl = resolveUrl(baseUrl, options.url);
|
|
1193
|
+
redactRedirectUrl(currentUrl);
|
|
1194
|
+
initialUrl = serializeRequestUrl(currentUrl, initialParams, initialSensitiveParams);
|
|
1195
|
+
for (const value of initialUrl.sensitiveValues) {
|
|
1196
|
+
if (value !== "") sensitiveValues.add(value);
|
|
1197
|
+
}
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
throw redactSensitiveError(
|
|
1200
|
+
error,
|
|
1201
|
+
[...sensitiveValues],
|
|
1202
|
+
options.url,
|
|
1203
|
+
redactRedirectUrl(options.url),
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
893
1206
|
|
|
894
1207
|
for (let hopIndex = 0; hopIndex <= maxHops; hopIndex += 1) {
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1208
|
+
const outboundUrl =
|
|
1209
|
+
hopIndex === 0 ? initialUrl.requestUrl : serializeRequestUrl(currentUrl).requestUrl;
|
|
1210
|
+
// Preserve params-only loop bookkeeping from before sensitiveParams:
|
|
1211
|
+
// the first visited key is the caller's resolved URL, not its expanded query.
|
|
1212
|
+
const visitedUrl = hopIndex === 0 && !initialSensitiveParams ? currentUrl : outboundUrl;
|
|
1213
|
+
visitedRequests.add(`${method} ${visitedUrl}`);
|
|
1214
|
+
try {
|
|
1215
|
+
response = await session.fetch(currentUrl, {
|
|
1216
|
+
...fetchOptions,
|
|
1217
|
+
body,
|
|
1218
|
+
method,
|
|
1219
|
+
...(hopIndex === 0 && initialParams ? { params: initialParams } : {}),
|
|
1220
|
+
...(hopIndex === 0 && initialSensitiveParams
|
|
1221
|
+
? { sensitiveParams: initialSensitiveParams }
|
|
1222
|
+
: {}),
|
|
1223
|
+
redirect: "manual",
|
|
1224
|
+
throwOnHttpError: false,
|
|
1225
|
+
});
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
throw redactSensitiveError(
|
|
1228
|
+
error,
|
|
1229
|
+
[...sensitiveValues],
|
|
1230
|
+
outboundUrl,
|
|
1231
|
+
redactRedirectUrl(outboundUrl),
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
// StealthResponse.url is programmatic metadata and remains raw. Only the
|
|
1235
|
+
// redirect hop emitted below is a diagnostic surface.
|
|
1236
|
+
const responseUrl =
|
|
1237
|
+
response.url ?? (hopIndex === 0 && initialSensitiveParams ? outboundUrl : currentUrl);
|
|
904
1238
|
|
|
905
1239
|
if (!isRedirectStatus(response.status)) {
|
|
906
1240
|
return {
|
|
@@ -908,63 +1242,79 @@ function createSessionFetcher(
|
|
|
908
1242
|
hops,
|
|
909
1243
|
reason: "completed",
|
|
910
1244
|
cookies: cookieJar.snapshot(),
|
|
1245
|
+
cookieStore: cookieJar.serialize(),
|
|
911
1246
|
};
|
|
912
1247
|
}
|
|
913
1248
|
|
|
914
1249
|
const location = locationHeader(response.headers);
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1250
|
+
const redactedResponseUrl = redactRedirectUrl(responseUrl);
|
|
1251
|
+
const redactedLocation = location ? redactRedirectUrl(location) : undefined;
|
|
1252
|
+
let nextUrl: string | undefined;
|
|
1253
|
+
try {
|
|
1254
|
+
nextUrl = resolveRedirectUrl(location, responseUrl);
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
throw redactSensitiveError(error, [...sensitiveValues], location, redactedLocation);
|
|
1257
|
+
}
|
|
1258
|
+
const realHop: StealthRedirectHop = {
|
|
1259
|
+
url: responseUrl,
|
|
920
1260
|
status: response.status,
|
|
921
1261
|
method,
|
|
922
1262
|
...(location ? { location } : {}),
|
|
923
1263
|
...(nextUrl ? { nextUrl } : {}),
|
|
924
1264
|
};
|
|
1265
|
+
const hop: StealthRedirectHop = {
|
|
1266
|
+
...realHop,
|
|
1267
|
+
url: redactedResponseUrl,
|
|
1268
|
+
...(redactedLocation ? { location: redactedLocation } : {}),
|
|
1269
|
+
...(nextUrl ? { nextUrl: redactRedirectUrl(nextUrl) } : {}),
|
|
1270
|
+
};
|
|
925
1271
|
hops.push(hop);
|
|
926
1272
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1273
|
+
let shouldStop = false;
|
|
1274
|
+
if (stopWhen) {
|
|
1275
|
+
try {
|
|
1276
|
+
shouldStop = await stopWhen(realHop);
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
let sanitizedError: unknown = error;
|
|
1279
|
+
for (const [rawUrl, safeUrl] of [
|
|
1280
|
+
[responseUrl, redactedResponseUrl],
|
|
1281
|
+
[location, redactedLocation],
|
|
1282
|
+
[nextUrl, nextUrl ? redactRedirectUrl(nextUrl) : undefined],
|
|
1283
|
+
] as const) {
|
|
1284
|
+
if (!rawUrl || !safeUrl) continue;
|
|
1285
|
+
sanitizedError = redactSensitiveError(
|
|
1286
|
+
sanitizedError,
|
|
1287
|
+
[...sensitiveValues],
|
|
1288
|
+
rawUrl,
|
|
1289
|
+
safeUrl,
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
throw sanitizedError;
|
|
1293
|
+
}
|
|
943
1294
|
}
|
|
944
|
-
|
|
945
|
-
|
|
1295
|
+
const decision = evaluateRedirectHop({
|
|
1296
|
+
status: response.status,
|
|
1297
|
+
method,
|
|
1298
|
+
nextUrl,
|
|
1299
|
+
shouldStop,
|
|
1300
|
+
redirectCount: hops.length,
|
|
1301
|
+
maxHops,
|
|
1302
|
+
visitedRequests,
|
|
1303
|
+
});
|
|
1304
|
+
if (decision.kind === "stop") {
|
|
946
1305
|
return {
|
|
947
1306
|
final: response,
|
|
948
1307
|
hops,
|
|
949
|
-
reason:
|
|
1308
|
+
reason: decision.reason,
|
|
950
1309
|
cookies: cookieJar.snapshot(),
|
|
1310
|
+
cookieStore: cookieJar.serialize(),
|
|
951
1311
|
};
|
|
952
1312
|
}
|
|
953
|
-
|
|
954
|
-
const nextMethod = nextRedirectMethod(response.status, method);
|
|
955
|
-
if (nextMethod !== method) {
|
|
1313
|
+
if (decision.nextMethod !== method) {
|
|
956
1314
|
body = undefined;
|
|
957
1315
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
final: response,
|
|
961
|
-
hops,
|
|
962
|
-
reason: "loop",
|
|
963
|
-
cookies: cookieJar.snapshot(),
|
|
964
|
-
};
|
|
965
|
-
}
|
|
966
|
-
method = nextMethod;
|
|
967
|
-
currentUrl = nextUrl;
|
|
1316
|
+
method = decision.nextMethod;
|
|
1317
|
+
currentUrl = decision.nextUrl;
|
|
968
1318
|
}
|
|
969
1319
|
|
|
970
1320
|
if (!response) {
|
|
@@ -982,6 +1332,7 @@ function createSessionFetcher(
|
|
|
982
1332
|
hops,
|
|
983
1333
|
reason: "max_hops",
|
|
984
1334
|
cookies: cookieJar.snapshot(),
|
|
1335
|
+
cookieStore: cookieJar.serialize(),
|
|
985
1336
|
};
|
|
986
1337
|
},
|
|
987
1338
|
},
|