@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/dist/runtime/stealth.js
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { Impit } from "impit";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
3
|
+
import { Cookie, CookieJar as ToughCookieJar } from "tough-cookie";
|
|
4
|
+
import { DEFAULT_SMARTPROXY_POOL_SIZE, invalidateProxyResolutionCacheAsync, ProxyResolutionError, policyResolvesRegistryVendorChain, resolvePolicyProxyPoolSpan, resolvePolicyTransportAttemptCap, resolveProxyConfigAsync, vendorFromResolvedSource, } from "../config/loader.js";
|
|
5
|
+
import { SDKError, StealthCookieStoreVersionError, TransportError } from "../errors.js";
|
|
6
|
+
import { getStealthProfile } from "../stealth/profiles.js";
|
|
7
|
+
import { createProxyAuthIpDeniedError, createProxyEdgeAuthRejectedError, createProxyEdgeTlsRejectedError, createProxyPoolExhaustedError, createProxyPoolStaleError, isProxyAuthIpDeniedMessage, isProxyEdgeAuthRejectedMessage, isProxyEdgeTlsRejectedResponse, isProxyPoolRefreshableError, isProxyPoolStaleMessage, isProxyPoolStaleStatus, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_STALE_CODE, } from "./proxy-errors.js";
|
|
8
|
+
import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, normalizeProxyTransportRetryOptions, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy.js";
|
|
9
|
+
import { evaluateRedirectHop, isRedirectStatus, resolveRedirectUrl, } from "./redirects.js";
|
|
10
|
+
import { isSensitiveKey, redactSensitiveError, redactSensitiveRequestError, redactSensitiveText, redactUrlQueryParams, normalizeSensitiveParams, serializeRequestUrl, } from "./request-options.js";
|
|
9
11
|
const DEFAULT_PROFILE = "chrome-146";
|
|
10
12
|
const MISSING_PROXY_WARNING = "[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
|
|
11
|
-
const MAX_POLICY_PROXY_RETRY_ATTEMPTS = SMARTPROXY_MAX_POOL_SIZE;
|
|
12
13
|
const MAX_POLICY_PROXY_POOL_REFRESHES = 1;
|
|
13
14
|
const PROXY_CONNECT_FAILURE_CODE = "proxy_connect_failed";
|
|
14
15
|
const PROXY_CONNECT_FAILURE_BODY_PATTERN = /\bproxy\b.*\b(non[\s-]?200|connect|tunnel)|\bconnect\b.*\bproxy\b|\btunnel\b/i;
|
|
15
16
|
const PROXY_AUTH_DIAGNOSTIC_URL = "http://example.com/";
|
|
16
17
|
const PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS = 5_000;
|
|
17
18
|
const STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES = [PROXY_CONNECT_FAILURE_CODE];
|
|
19
|
+
function sensitiveQueryParamNames(url) {
|
|
20
|
+
const queryStart = url.indexOf("?");
|
|
21
|
+
if (queryStart === -1)
|
|
22
|
+
return [];
|
|
23
|
+
const fragmentStart = url.indexOf("#", queryStart);
|
|
24
|
+
const query = url.slice(queryStart + 1, fragmentStart === -1 ? undefined : fragmentStart);
|
|
25
|
+
return [...new URLSearchParams(query).keys()].filter(isSensitiveKey);
|
|
26
|
+
}
|
|
18
27
|
const REMOVED_CHROME_PROFILE_NAMES = new Set([
|
|
19
28
|
"chrome-120",
|
|
20
29
|
"chrome-124",
|
|
@@ -50,69 +59,128 @@ const FIREFOX_IMPIT_BY_MAJOR = {
|
|
|
50
59
|
function isRecord(value) {
|
|
51
60
|
return typeof value === "object" && value !== null;
|
|
52
61
|
}
|
|
62
|
+
const LEGACY_COOKIE_ORIGIN = "https://legacy-cookie.invalid/";
|
|
53
63
|
class CookieJarImpl {
|
|
54
64
|
cookies;
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
defaultUrl;
|
|
66
|
+
constructor(cookieStrings, defaultUrl = LEGACY_COOKIE_ORIGIN) {
|
|
67
|
+
this.cookies = new ToughCookieJar(undefined, {
|
|
68
|
+
allowSecureOnLocal: false,
|
|
69
|
+
rejectPublicSuffixes: true,
|
|
70
|
+
});
|
|
71
|
+
this.defaultUrl = this.normalizeUrl(defaultUrl) ?? LEGACY_COOKIE_ORIGIN;
|
|
57
72
|
this.setFromCookieStrings(cookieStrings);
|
|
58
73
|
}
|
|
59
|
-
|
|
74
|
+
/**
|
|
75
|
+
* URL-less legacy operations are scoped to this jar's default URL. Session
|
|
76
|
+
* jars use the client's base URL and response jars use the response URL. A
|
|
77
|
+
* flat restore has no attributes to recover, so it creates host-only Path=/
|
|
78
|
+
* cookies for that default URL instead of making them visible to every host.
|
|
79
|
+
*/
|
|
80
|
+
setFromCookieStrings(cookieStrings, url = this.defaultUrl) {
|
|
81
|
+
const cookieUrl = this.normalizeUrl(url);
|
|
82
|
+
if (!cookieUrl)
|
|
83
|
+
return;
|
|
60
84
|
for (const cookieString of cookieStrings) {
|
|
61
|
-
|
|
62
|
-
if (!nameValue) {
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
const separatorIndex = nameValue.indexOf("=");
|
|
66
|
-
if (separatorIndex === -1) {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
const name = nameValue.slice(0, separatorIndex).trim();
|
|
70
|
-
const value = nameValue.slice(separatorIndex + 1).trim();
|
|
71
|
-
if (name)
|
|
72
|
-
this.cookies[name] = value;
|
|
85
|
+
this.cookies.setCookieSync(cookieString, cookieUrl, { ignoreError: true });
|
|
73
86
|
}
|
|
74
87
|
}
|
|
75
|
-
get(name) {
|
|
76
|
-
return this.
|
|
88
|
+
get(name, url) {
|
|
89
|
+
return this.getAll(url)[name];
|
|
77
90
|
}
|
|
78
|
-
getAll() {
|
|
79
|
-
return
|
|
91
|
+
getAll(url) {
|
|
92
|
+
return Object.fromEntries(this.getUniqueCookies(url ?? this.defaultUrl).map((cookie) => [cookie.key, cookie.value]));
|
|
80
93
|
}
|
|
81
|
-
has(name) {
|
|
82
|
-
return Object.hasOwn(this.
|
|
94
|
+
has(name, url) {
|
|
95
|
+
return Object.hasOwn(this.getAll(url), name);
|
|
83
96
|
}
|
|
84
|
-
toString() {
|
|
85
|
-
return
|
|
86
|
-
.map((
|
|
97
|
+
toString(url) {
|
|
98
|
+
return this.getUniqueCookies(url ?? this.defaultUrl)
|
|
99
|
+
.map((cookie) => cookie.cookieString())
|
|
87
100
|
.join("; ");
|
|
88
101
|
}
|
|
89
|
-
toHeader() {
|
|
90
|
-
return this.toString();
|
|
102
|
+
toHeader(url) {
|
|
103
|
+
return this.toString(url);
|
|
91
104
|
}
|
|
92
105
|
snapshot() {
|
|
93
|
-
|
|
106
|
+
// This compatibility view deliberately enumerates the serialized store,
|
|
107
|
+
// not getAll(defaultUrl): persistence must include sibling hosts and paths.
|
|
108
|
+
// Duplicate names still collapse because a flat map cannot represent them.
|
|
109
|
+
const entries = [];
|
|
110
|
+
for (const cookie of this.serialize().jar.cookies) {
|
|
111
|
+
if (typeof cookie.key === "string" && typeof cookie.value === "string" && cookie.key) {
|
|
112
|
+
entries.push([cookie.key, cookie.value]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return Object.fromEntries(entries);
|
|
94
116
|
}
|
|
95
117
|
restore(cookies) {
|
|
96
118
|
this.clear();
|
|
97
119
|
for (const [name, value] of Object.entries(cookies)) {
|
|
98
|
-
if (name)
|
|
99
|
-
|
|
120
|
+
if (!name)
|
|
121
|
+
continue;
|
|
122
|
+
this.cookies.setCookieSync(new Cookie({ key: name, path: "/", value }), this.defaultUrl, {
|
|
123
|
+
ignoreError: true,
|
|
124
|
+
});
|
|
100
125
|
}
|
|
101
126
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
127
|
+
serialize() {
|
|
128
|
+
const jar = this.cookies.serializeSync();
|
|
129
|
+
if (!jar) {
|
|
130
|
+
throw new SDKError("Stealth cookie store could not be serialized", {
|
|
131
|
+
code: "stealth_cookie_store_serialize_failed",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return { version: 1, jar };
|
|
135
|
+
}
|
|
136
|
+
deserialize(state) {
|
|
137
|
+
const version = isRecord(state) ? state.version : undefined;
|
|
138
|
+
if (version !== 1) {
|
|
139
|
+
throw new StealthCookieStoreVersionError(version);
|
|
105
140
|
}
|
|
141
|
+
// Deserialize into a new jar first so invalid state cannot partially clear
|
|
142
|
+
// or replace a live session. tough-cookie restores the cookie attributes and
|
|
143
|
+
// matching semantics represented in its own serialized format.
|
|
144
|
+
const restored = ToughCookieJar.deserializeSync(state.jar);
|
|
145
|
+
// tough-cookie 6 does not include this option in serializeSync(). Preserve
|
|
146
|
+
// the SDK's stricter setting across restoration.
|
|
147
|
+
Reflect.set(restored, "allowSecureOnLocal", false);
|
|
148
|
+
this.cookies = restored;
|
|
106
149
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
150
|
+
clear() {
|
|
151
|
+
this.cookies.removeAllCookiesSync();
|
|
152
|
+
}
|
|
153
|
+
find(predicate, url) {
|
|
154
|
+
for (const cookie of this.getUniqueCookies(url ?? this.defaultUrl)) {
|
|
155
|
+
const cookieString = cookie.cookieString();
|
|
156
|
+
if (predicate(cookieString)) {
|
|
157
|
+
return cookieString;
|
|
112
158
|
}
|
|
113
159
|
}
|
|
114
160
|
return undefined;
|
|
115
161
|
}
|
|
162
|
+
normalizeUrl(url) {
|
|
163
|
+
try {
|
|
164
|
+
return new URL(url).toString();
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
getUniqueCookies(url) {
|
|
171
|
+
const cookieUrl = this.normalizeUrl(url);
|
|
172
|
+
if (!cookieUrl)
|
|
173
|
+
return [];
|
|
174
|
+
// tough-cookie returns longer (more-specific) paths first. Keeping the
|
|
175
|
+
// first cookie for each name prevents ambiguous duplicate-name headers.
|
|
176
|
+
const names = new Set();
|
|
177
|
+
return this.cookies.getCookiesSync(cookieUrl).filter((cookie) => {
|
|
178
|
+
if (names.has(cookie.key))
|
|
179
|
+
return false;
|
|
180
|
+
names.add(cookie.key);
|
|
181
|
+
return true;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
116
184
|
}
|
|
117
185
|
function closestImpitBrowser(major, candidates) {
|
|
118
186
|
let closestMajor;
|
|
@@ -176,13 +244,13 @@ function hasOwn(object, key) {
|
|
|
176
244
|
}
|
|
177
245
|
function toImpitCookieJar(cookieJar) {
|
|
178
246
|
return {
|
|
179
|
-
setCookie(cookie,
|
|
180
|
-
cookieJar.setFromCookieStrings([cookie]);
|
|
247
|
+
setCookie(cookie, url, cb) {
|
|
248
|
+
cookieJar.setFromCookieStrings([cookie], url);
|
|
181
249
|
if (typeof cb === "function")
|
|
182
250
|
cb();
|
|
183
251
|
},
|
|
184
|
-
getCookieString(
|
|
185
|
-
return cookieJar.
|
|
252
|
+
getCookieString(url) {
|
|
253
|
+
return cookieJar.toHeader(url);
|
|
186
254
|
},
|
|
187
255
|
};
|
|
188
256
|
}
|
|
@@ -234,10 +302,12 @@ function splitCombinedSetCookieHeader(headerValue) {
|
|
|
234
302
|
cookieStrings.push(finalCookie);
|
|
235
303
|
return cookieStrings;
|
|
236
304
|
}
|
|
237
|
-
export async function normalizeResponse(response, requestUrl) {
|
|
305
|
+
export async function normalizeResponse(response, requestUrl, maxBodyBytes) {
|
|
238
306
|
const headers = Object.fromEntries(response.headers.entries());
|
|
239
|
-
const cookies = new CookieJarImpl(setCookieHeadersFromResponse(response.headers));
|
|
240
|
-
const bodyBytes =
|
|
307
|
+
const cookies = new CookieJarImpl(setCookieHeadersFromResponse(response.headers), response.url ?? requestUrl);
|
|
308
|
+
const bodyBytes = maxBodyBytes === undefined
|
|
309
|
+
? await response.arrayBuffer()
|
|
310
|
+
: await readResponseBodyWithLimit(response, maxBodyBytes);
|
|
241
311
|
const body = new TextDecoder().decode(bodyBytes);
|
|
242
312
|
return {
|
|
243
313
|
status: response.status,
|
|
@@ -263,6 +333,75 @@ export async function normalizeResponse(response, requestUrl) {
|
|
|
263
333
|
},
|
|
264
334
|
};
|
|
265
335
|
}
|
|
336
|
+
function responseTooLargeError(maxBodyBytes, observedBytes) {
|
|
337
|
+
return new TransportError(`Response body exceeded maxBodyBytes limit of ${maxBodyBytes} bytes (observed ${observedBytes} bytes)`, {
|
|
338
|
+
code: "response_too_large",
|
|
339
|
+
category: "upstream_http",
|
|
340
|
+
retryable: false,
|
|
341
|
+
status: 0,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
function declaredContentLength(headers) {
|
|
345
|
+
const contentLength = headers.get("content-length")?.trim();
|
|
346
|
+
if (!contentLength || !/^\d+$/.test(contentLength))
|
|
347
|
+
return undefined;
|
|
348
|
+
const parsed = Number(contentLength);
|
|
349
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
350
|
+
}
|
|
351
|
+
function abortTransportResponse(response) {
|
|
352
|
+
if (!response.abort)
|
|
353
|
+
return false;
|
|
354
|
+
try {
|
|
355
|
+
response.abort();
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
// The size error remains the primary failure if impit has already closed the response.
|
|
359
|
+
}
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
async function readResponseBodyWithLimit(response, maxBodyBytes) {
|
|
363
|
+
const contentLength = declaredContentLength(response.headers);
|
|
364
|
+
if (contentLength !== undefined && contentLength > maxBodyBytes) {
|
|
365
|
+
if (!abortTransportResponse(response)) {
|
|
366
|
+
await response.body?.cancel().catch(() => undefined);
|
|
367
|
+
}
|
|
368
|
+
throw responseTooLargeError(maxBodyBytes, contentLength);
|
|
369
|
+
}
|
|
370
|
+
if (!response.body) {
|
|
371
|
+
throw new TransportError("Response body stream is unavailable", {
|
|
372
|
+
code: "transport_stream_unavailable",
|
|
373
|
+
category: "upstream_http",
|
|
374
|
+
status: 0,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
const reader = response.body.getReader();
|
|
378
|
+
const chunks = [];
|
|
379
|
+
let receivedBytes = 0;
|
|
380
|
+
try {
|
|
381
|
+
while (true) {
|
|
382
|
+
const { done, value } = await reader.read();
|
|
383
|
+
if (done)
|
|
384
|
+
break;
|
|
385
|
+
receivedBytes += value.byteLength;
|
|
386
|
+
if (receivedBytes > maxBodyBytes) {
|
|
387
|
+
await reader.cancel().catch(() => undefined);
|
|
388
|
+
abortTransportResponse(response);
|
|
389
|
+
throw responseTooLargeError(maxBodyBytes, receivedBytes);
|
|
390
|
+
}
|
|
391
|
+
chunks.push(value);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
reader.releaseLock();
|
|
396
|
+
}
|
|
397
|
+
const bodyBytes = new Uint8Array(receivedBytes);
|
|
398
|
+
let offset = 0;
|
|
399
|
+
for (const chunk of chunks) {
|
|
400
|
+
bodyBytes.set(chunk, offset);
|
|
401
|
+
offset += chunk.byteLength;
|
|
402
|
+
}
|
|
403
|
+
return bodyBytes.buffer;
|
|
404
|
+
}
|
|
266
405
|
function normalizeBody(body) {
|
|
267
406
|
if (body === undefined) {
|
|
268
407
|
return "";
|
|
@@ -420,16 +559,6 @@ function normalizeMethod(method) {
|
|
|
420
559
|
throw new SDKError(`Unsupported stealth method: ${method}`);
|
|
421
560
|
}
|
|
422
561
|
}
|
|
423
|
-
function isRedirectStatus(status) {
|
|
424
|
-
return [301, 302, 303, 307, 308].includes(status);
|
|
425
|
-
}
|
|
426
|
-
function nextRedirectMethod(status, method) {
|
|
427
|
-
if (status === 303 && method !== "HEAD")
|
|
428
|
-
return "GET";
|
|
429
|
-
if ((status === 301 || status === 302) && method === "POST")
|
|
430
|
-
return "GET";
|
|
431
|
-
return method;
|
|
432
|
-
}
|
|
433
562
|
function locationHeader(headers) {
|
|
434
563
|
for (const [name, value] of Object.entries(headers)) {
|
|
435
564
|
if (name.toLowerCase() === "location")
|
|
@@ -442,7 +571,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
442
571
|
let closed = false;
|
|
443
572
|
let hasWarnedMissingProxy = false;
|
|
444
573
|
const warn = clientOptions.warn ?? console.warn;
|
|
445
|
-
const cookieJar = new CookieJarImpl([]);
|
|
574
|
+
const cookieJar = new CookieJarImpl([], baseUrl);
|
|
446
575
|
const impitCookieJar = toImpitCookieJar(cookieJar);
|
|
447
576
|
function getClient(profileName, proxyUrl, ignoreTlsErrors) {
|
|
448
577
|
if (closed) {
|
|
@@ -463,7 +592,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
463
592
|
}
|
|
464
593
|
return client;
|
|
465
594
|
}
|
|
466
|
-
async function resolveRequestProxy(options, proxyAttempt) {
|
|
595
|
+
async function resolveRequestProxy(options, proxyAttempt, refreshEpoch) {
|
|
467
596
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
468
597
|
proxy: options?.proxy ?? clientOptions.proxy,
|
|
469
598
|
upstream: clientOptions.upstream,
|
|
@@ -474,6 +603,10 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
474
603
|
proxyAttemptOffset: options?.proxyAttemptOffset,
|
|
475
604
|
retryAttemptOffset: proxyAttempt,
|
|
476
605
|
}),
|
|
606
|
+
// The impit stealth transport tunnels both HTTP CONNECT and SOCKS5,
|
|
607
|
+
// preserving the client TLS fingerprint end-to-end.
|
|
608
|
+
transportProtocols: ["http", "socks5"],
|
|
609
|
+
...(refreshEpoch === undefined ? {} : { proxyRefreshEpoch: refreshEpoch }),
|
|
477
610
|
telemetry: clientOptions.telemetry,
|
|
478
611
|
});
|
|
479
612
|
if (resolvedProxy.shouldWarn && !hasWarnedMissingProxy) {
|
|
@@ -484,34 +617,59 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
484
617
|
url: resolvedProxy.url,
|
|
485
618
|
poolIndex: proxyPoolIndexFromDiagnostics(resolvedProxy.diagnostics),
|
|
486
619
|
proxyHash: proxyEndpointHash(resolvedProxy.url),
|
|
620
|
+
vendor: vendorFromResolvedSource(resolvedProxy.source),
|
|
487
621
|
};
|
|
488
622
|
}
|
|
489
623
|
const session = {
|
|
490
624
|
async fetch(url, options = {}) {
|
|
491
|
-
const method =
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}) ??
|
|
497
|
-
(hasExplicitRetryPolicy
|
|
498
|
-
? undefined
|
|
499
|
-
: createDefaultProxyTransportRetryOptions({
|
|
625
|
+
const { hasExplicitRetryPolicy, method, stealthRetryOptions } = (() => {
|
|
626
|
+
try {
|
|
627
|
+
const method = normalizeMethod(options.method ?? "GET");
|
|
628
|
+
const hasExplicitRetryPolicy = options.retry !== undefined;
|
|
629
|
+
const stealthRetryOptions = normalizeProxyTransportRetryOptions(options.retry, {
|
|
500
630
|
extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
|
|
501
631
|
label: "Stealth",
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
632
|
+
}) ??
|
|
633
|
+
(hasExplicitRetryPolicy
|
|
634
|
+
? undefined
|
|
635
|
+
: createDefaultProxyTransportRetryOptions({
|
|
636
|
+
extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
|
|
637
|
+
label: "Stealth",
|
|
638
|
+
}));
|
|
639
|
+
if (stealthRetryOptions) {
|
|
640
|
+
validateUnsafeProxyTransportRetryMethods(stealthRetryOptions, "Stealth");
|
|
641
|
+
}
|
|
642
|
+
return { hasExplicitRetryPolicy, method, stealthRetryOptions };
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
646
|
+
}
|
|
647
|
+
})();
|
|
506
648
|
const hasPolicyProxy = isPolicyManagedProxy(clientOptions);
|
|
507
649
|
const usesPolicyAllocator = hasPolicyProxy && !options.proxy && !clientOptions.proxy;
|
|
508
650
|
const retryAttemptCap = Math.max(1, stealthRetryOptions?.attempts ?? 1);
|
|
509
|
-
|
|
651
|
+
// Span the whole vendor chain: successive attempts rotate one vendor's
|
|
652
|
+
// pool, then fail over to the next vendor via the flat attempt index.
|
|
653
|
+
const policyProxy = clientOptions.proxyPolicy ??
|
|
510
654
|
(typeof clientOptions.upstream?.proxy === "object"
|
|
511
|
-
? clientOptions.upstream.proxy
|
|
512
|
-
: undefined)
|
|
513
|
-
|
|
514
|
-
|
|
655
|
+
? clientOptions.upstream.proxy
|
|
656
|
+
: undefined);
|
|
657
|
+
// The pool span is already bounded by each vendor's max pool size
|
|
658
|
+
// (smartproxy ≤20, nodemaven ≤50), so the configured span never exceeds
|
|
659
|
+
// the chain's true maximum — a large NodeMaven pool stays fully
|
|
660
|
+
// reachable rather than being truncated at an arbitrary ceiling.
|
|
661
|
+
const policyProxyAttemptCap = Math.max(1, policyProxy ? resolvePolicyProxyPoolSpan(policyProxy) : DEFAULT_SMARTPROXY_POOL_SIZE);
|
|
662
|
+
// A registry vendor chain (smartproxy/nodemaven) is the only policy whose
|
|
663
|
+
// successive attempts resolve a *different* endpoint, so it is the only one
|
|
664
|
+
// that may widen the attempt cap to the pool span, de-duplicate endpoints,
|
|
665
|
+
// and drive allocator stale-pool refresh. A static custom/decodo policy
|
|
666
|
+
// resolves the same URL every attempt: widening/refreshing it would resend
|
|
667
|
+
// the request dozens of times (up to maxAttempts × refreshes) and bypass
|
|
668
|
+
// retry:false and unsafe-method controls. Static policies therefore follow
|
|
669
|
+
// the ordinary transport-retry budget instead.
|
|
670
|
+
const rotatesRegistryChain = usesPolicyAllocator && policyResolvesRegistryVendorChain(policyProxy);
|
|
671
|
+
const maxAttempts = rotatesRegistryChain ? policyProxyAttemptCap : retryAttemptCap;
|
|
672
|
+
const dedupeAllocatorEndpoints = rotatesRegistryChain;
|
|
515
673
|
let lastError;
|
|
516
674
|
for (let refreshAttempt = 0; refreshAttempt <= MAX_POLICY_PROXY_POOL_REFRESHES; refreshAttempt += 1) {
|
|
517
675
|
let stalePoolError;
|
|
@@ -520,6 +678,11 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
520
678
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
521
679
|
let proxy;
|
|
522
680
|
let attemptProxy;
|
|
681
|
+
// Reuse the exact serialization used by this outbound attempt in its catch path.
|
|
682
|
+
let serializedUrl;
|
|
683
|
+
let fallbackSensitiveValues = [];
|
|
684
|
+
let fallbackRequestUrl;
|
|
685
|
+
let fallbackRedactedUrl;
|
|
523
686
|
const attemptStartedAt = Date.now();
|
|
524
687
|
let attemptRecorded = false;
|
|
525
688
|
const recordProxyAttempt = (outcome, errorCode, status) => {
|
|
@@ -527,7 +690,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
527
690
|
return;
|
|
528
691
|
attemptRecorded = true;
|
|
529
692
|
clientOptions.telemetry?.recordProxyAttempt?.({
|
|
530
|
-
provider: "smartproxy",
|
|
693
|
+
provider: attemptProxy?.vendor ?? "smartproxy",
|
|
531
694
|
attempt: attempt + 1,
|
|
532
695
|
...(attemptProxy?.poolIndex === undefined
|
|
533
696
|
? {}
|
|
@@ -540,22 +703,38 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
540
703
|
});
|
|
541
704
|
};
|
|
542
705
|
try {
|
|
706
|
+
const sensitiveParams = normalizeSensitiveParams(options.sensitiveParams);
|
|
707
|
+
const structural = redactUrlQueryParams(url, Object.keys(sensitiveParams ?? {}));
|
|
708
|
+
fallbackSensitiveValues = [
|
|
709
|
+
...new Set([
|
|
710
|
+
...Object.values(sensitiveParams ?? {}).map(String),
|
|
711
|
+
...structural.sensitiveValues,
|
|
712
|
+
]),
|
|
713
|
+
].filter((value) => value !== "");
|
|
714
|
+
fallbackRequestUrl = url;
|
|
715
|
+
fallbackRedactedUrl = structural.redactedUrl;
|
|
543
716
|
assertNoUnsupportedFingerprintOverrides(options);
|
|
544
|
-
attemptProxy = await resolveRequestProxy(options, attempt);
|
|
717
|
+
attemptProxy = await resolveRequestProxy(options, attempt, refreshAttempt);
|
|
545
718
|
proxy = attemptProxy.url;
|
|
546
|
-
if (proxy &&
|
|
719
|
+
if (proxy && dedupeAllocatorEndpoints) {
|
|
720
|
+
// An under-filled allocation repeats endpoints (via the modulo
|
|
721
|
+
// pool mapping) before the flat offset crosses into the next
|
|
722
|
+
// vendor. Skip an already-tried endpoint and advance the offset
|
|
723
|
+
// rather than breaking — breaking here would strand the request on
|
|
724
|
+
// the primary vendor and never reach the fallback leg.
|
|
547
725
|
if (attemptedProxies.has(proxy)) {
|
|
548
|
-
|
|
726
|
+
continue;
|
|
549
727
|
}
|
|
550
728
|
attemptedProxies.add(proxy);
|
|
551
729
|
}
|
|
552
730
|
const ignoreTlsErrors = Boolean(options.stealth?.insecureSkipVerify ??
|
|
553
731
|
(!hasPolicyProxy && proxy && clientOptions.proxyStealth?.insecureSkipVerify));
|
|
554
732
|
const profileName = options.profile ?? defaultProfile;
|
|
555
|
-
|
|
733
|
+
serializedUrl = serializeRequestUrl(resolveUrl(baseUrl, url), options.params, sensitiveParams);
|
|
734
|
+
const { requestUrl } = serializedUrl;
|
|
556
735
|
const headers = { ...(options.headers ?? {}) };
|
|
557
736
|
if (!hasHeader(headers, "Cookie")) {
|
|
558
|
-
const cookieHeader = cookieJar.
|
|
737
|
+
const cookieHeader = cookieJar.toHeader(requestUrl);
|
|
559
738
|
if (cookieHeader)
|
|
560
739
|
headers.Cookie = cookieHeader;
|
|
561
740
|
}
|
|
@@ -569,8 +748,8 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
569
748
|
requestInit.body = normalizeBody(options.body);
|
|
570
749
|
}
|
|
571
750
|
const response = await getClient(profileName, proxy, ignoreTlsErrors).fetch(requestUrl, requestInit);
|
|
572
|
-
const normalized = await normalizeResponse(response, requestUrl);
|
|
573
|
-
cookieJar.setFromCookieStrings(setCookieHeadersFromResponse(response.headers));
|
|
751
|
+
const normalized = await normalizeResponse(response, requestUrl, options.maxBodyBytes);
|
|
752
|
+
cookieJar.setFromCookieStrings(setCookieHeadersFromResponse(response.headers), response.url ?? requestUrl);
|
|
574
753
|
if (proxy && isProxyConnectFailureResponse(response, normalized.body)) {
|
|
575
754
|
throw createProxyConnectFailureError(normalized.body);
|
|
576
755
|
}
|
|
@@ -602,12 +781,23 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
602
781
|
return normalized;
|
|
603
782
|
}
|
|
604
783
|
catch (error) {
|
|
605
|
-
const
|
|
784
|
+
const sensitiveValues = serializedUrl?.sensitiveValues ?? fallbackSensitiveValues;
|
|
785
|
+
let normalizedError;
|
|
786
|
+
try {
|
|
787
|
+
normalizedError = normalizeStealthTransportError(error);
|
|
788
|
+
}
|
|
789
|
+
catch (normalizationError) {
|
|
790
|
+
throw redactSensitiveError(normalizationError, sensitiveValues, serializedUrl?.requestUrl ?? fallbackRequestUrl, serializedUrl?.redactedUrl ?? fallbackRedactedUrl);
|
|
791
|
+
}
|
|
792
|
+
const retryErrorCode = proxyAttemptErrorCode(normalizedError);
|
|
793
|
+
const refreshableProxyError = isProxyPoolRefreshableError(normalizedError);
|
|
794
|
+
const runProxyAuthDiagnostic = shouldRunProxyAuthDiagnostic(normalizedError);
|
|
795
|
+
normalizedError = redactSensitiveError(normalizedError, sensitiveValues, serializedUrl?.requestUrl ?? fallbackRequestUrl, serializedUrl?.redactedUrl ?? fallbackRedactedUrl);
|
|
606
796
|
recordProxyAttempt("error", proxyAttemptErrorCode(normalizedError), proxyAttemptStatus(normalizedError));
|
|
607
797
|
lastError = normalizedError;
|
|
608
|
-
if (proxy &&
|
|
798
|
+
if (proxy && rotatesRegistryChain && refreshableProxyError) {
|
|
609
799
|
stalePoolError = normalizedError;
|
|
610
|
-
if (
|
|
800
|
+
if (runProxyAuthDiagnostic) {
|
|
611
801
|
stalePoolDiagnosticProxy = proxy;
|
|
612
802
|
}
|
|
613
803
|
if (attempt + 1 < maxAttempts) {
|
|
@@ -615,12 +805,29 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
615
805
|
}
|
|
616
806
|
break;
|
|
617
807
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
808
|
+
// Cap the number of transport retries. For a policy-allocator chain,
|
|
809
|
+
// every attempt resolves a *different* endpoint/vendor (poolIndex
|
|
810
|
+
// rotates across the concatenated vendor pool spans), so a transport
|
|
811
|
+
// failure is a signal to advance to the next endpoint — potentially
|
|
812
|
+
// crossing into the fallback vendor — not to retry the same endpoint.
|
|
813
|
+
// Truncating that rotation at the per-endpoint retry budget would
|
|
814
|
+
// strand the request on the primary vendor and never reach the
|
|
815
|
+
// fallback, since the crossover only happens once the flat attempt
|
|
816
|
+
// index exceeds the primary vendor's pool size (~10-20).
|
|
817
|
+
// resolvePolicyTransportAttemptCap widens to the full chain span only
|
|
818
|
+
// for implicit, safe-method allocator requests; explicit retry
|
|
819
|
+
// policies (their documented `attempts` ceiling), unsafe methods, and
|
|
820
|
+
// static/non-registry vendors keep the per-endpoint retry budget.
|
|
821
|
+
const transportRetryCap = resolvePolicyTransportAttemptCap({
|
|
822
|
+
policy: policyProxy,
|
|
823
|
+
usesPolicyAllocator,
|
|
824
|
+
retryAttempts: stealthRetryOptions?.attempts ?? 1,
|
|
825
|
+
explicitRetry: hasExplicitRetryPolicy,
|
|
826
|
+
method,
|
|
827
|
+
});
|
|
828
|
+
if (attempt + 1 < transportRetryCap &&
|
|
622
829
|
shouldRetryProxyTransportAttempt({
|
|
623
|
-
error:
|
|
830
|
+
error: { code: retryErrorCode },
|
|
624
831
|
explicitRetry: hasExplicitRetryPolicy,
|
|
625
832
|
method,
|
|
626
833
|
options: stealthRetryOptions,
|
|
@@ -634,7 +841,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
634
841
|
throw normalizedError;
|
|
635
842
|
}
|
|
636
843
|
}
|
|
637
|
-
if (
|
|
844
|
+
if (rotatesRegistryChain &&
|
|
638
845
|
stalePoolError &&
|
|
639
846
|
refreshAttempt < MAX_POLICY_PROXY_POOL_REFRESHES) {
|
|
640
847
|
await invalidateProxyResolutionCacheAsync({
|
|
@@ -670,81 +877,153 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
670
877
|
const maxHops = options.maxHops === undefined || !Number.isFinite(options.maxHops)
|
|
671
878
|
? 10
|
|
672
879
|
: Math.max(0, Math.floor(options.maxHops));
|
|
880
|
+
const { url: _url, maxHops: _maxHops, stopWhen, params, sensitiveParams, ...fetchOptions } = options;
|
|
673
881
|
const hops = [];
|
|
674
|
-
let currentUrl = resolveUrl(baseUrl, options.url);
|
|
675
882
|
let method = normalizeMethod(options.method ?? "GET");
|
|
676
883
|
let body = options.body;
|
|
677
884
|
let response;
|
|
678
885
|
const visitedRequests = new Set();
|
|
679
|
-
const
|
|
886
|
+
const initialParams = params
|
|
887
|
+
? Object.fromEntries(Object.entries(params).map(([key, value]) => [
|
|
888
|
+
key,
|
|
889
|
+
Array.isArray(value) ? [...value] : value,
|
|
890
|
+
]))
|
|
891
|
+
: undefined;
|
|
892
|
+
const normalizedSensitiveParams = normalizeSensitiveParams(sensitiveParams);
|
|
893
|
+
const initialSensitiveParams = normalizedSensitiveParams
|
|
894
|
+
? { ...normalizedSensitiveParams }
|
|
895
|
+
: undefined;
|
|
896
|
+
const sensitiveParamNames = initialSensitiveParams
|
|
897
|
+
? Object.keys(initialSensitiveParams)
|
|
898
|
+
: [];
|
|
899
|
+
const callerStructural = redactUrlQueryParams(options.url, sensitiveParamNames);
|
|
900
|
+
const sensitiveValues = new Set([
|
|
901
|
+
...Object.values(initialSensitiveParams ?? {}),
|
|
902
|
+
...callerStructural.sensitiveValues,
|
|
903
|
+
].filter((value) => value !== ""));
|
|
904
|
+
const redactRedirectUrl = (value) => {
|
|
905
|
+
const structural = redactUrlQueryParams(value, [
|
|
906
|
+
...new Set([...sensitiveParamNames, ...sensitiveQueryParamNames(value)]),
|
|
907
|
+
]);
|
|
908
|
+
for (const sensitiveValue of structural.sensitiveValues) {
|
|
909
|
+
sensitiveValues.add(sensitiveValue);
|
|
910
|
+
}
|
|
911
|
+
return redactSensitiveText(structural.redactedUrl, [...sensitiveValues]);
|
|
912
|
+
};
|
|
913
|
+
let currentUrl;
|
|
914
|
+
let initialUrl;
|
|
915
|
+
try {
|
|
916
|
+
currentUrl = resolveUrl(baseUrl, options.url);
|
|
917
|
+
redactRedirectUrl(currentUrl);
|
|
918
|
+
initialUrl = serializeRequestUrl(currentUrl, initialParams, initialSensitiveParams);
|
|
919
|
+
for (const value of initialUrl.sensitiveValues) {
|
|
920
|
+
if (value !== "")
|
|
921
|
+
sensitiveValues.add(value);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
catch (error) {
|
|
925
|
+
throw redactSensitiveError(error, [...sensitiveValues], options.url, redactRedirectUrl(options.url));
|
|
926
|
+
}
|
|
680
927
|
for (let hopIndex = 0; hopIndex <= maxHops; hopIndex += 1) {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
928
|
+
const outboundUrl = hopIndex === 0 ? initialUrl.requestUrl : serializeRequestUrl(currentUrl).requestUrl;
|
|
929
|
+
// Preserve params-only loop bookkeeping from before sensitiveParams:
|
|
930
|
+
// the first visited key is the caller's resolved URL, not its expanded query.
|
|
931
|
+
const visitedUrl = hopIndex === 0 && !initialSensitiveParams ? currentUrl : outboundUrl;
|
|
932
|
+
visitedRequests.add(`${method} ${visitedUrl}`);
|
|
933
|
+
try {
|
|
934
|
+
response = await session.fetch(currentUrl, {
|
|
935
|
+
...fetchOptions,
|
|
936
|
+
body,
|
|
937
|
+
method,
|
|
938
|
+
...(hopIndex === 0 && initialParams ? { params: initialParams } : {}),
|
|
939
|
+
...(hopIndex === 0 && initialSensitiveParams
|
|
940
|
+
? { sensitiveParams: initialSensitiveParams }
|
|
941
|
+
: {}),
|
|
942
|
+
redirect: "manual",
|
|
943
|
+
throwOnHttpError: false,
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
catch (error) {
|
|
947
|
+
throw redactSensitiveError(error, [...sensitiveValues], outboundUrl, redactRedirectUrl(outboundUrl));
|
|
948
|
+
}
|
|
949
|
+
// StealthResponse.url is programmatic metadata and remains raw. Only the
|
|
950
|
+
// redirect hop emitted below is a diagnostic surface.
|
|
951
|
+
const responseUrl = response.url ?? (hopIndex === 0 && initialSensitiveParams ? outboundUrl : currentUrl);
|
|
690
952
|
if (!isRedirectStatus(response.status)) {
|
|
691
953
|
return {
|
|
692
954
|
final: response,
|
|
693
955
|
hops,
|
|
694
956
|
reason: "completed",
|
|
695
957
|
cookies: cookieJar.snapshot(),
|
|
958
|
+
cookieStore: cookieJar.serialize(),
|
|
696
959
|
};
|
|
697
960
|
}
|
|
698
961
|
const location = locationHeader(response.headers);
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
962
|
+
const redactedResponseUrl = redactRedirectUrl(responseUrl);
|
|
963
|
+
const redactedLocation = location ? redactRedirectUrl(location) : undefined;
|
|
964
|
+
let nextUrl;
|
|
965
|
+
try {
|
|
966
|
+
nextUrl = resolveRedirectUrl(location, responseUrl);
|
|
967
|
+
}
|
|
968
|
+
catch (error) {
|
|
969
|
+
throw redactSensitiveError(error, [...sensitiveValues], location, redactedLocation);
|
|
970
|
+
}
|
|
971
|
+
const realHop = {
|
|
972
|
+
url: responseUrl,
|
|
704
973
|
status: response.status,
|
|
705
974
|
method,
|
|
706
975
|
...(location ? { location } : {}),
|
|
707
976
|
...(nextUrl ? { nextUrl } : {}),
|
|
708
977
|
};
|
|
978
|
+
const hop = {
|
|
979
|
+
...realHop,
|
|
980
|
+
url: redactedResponseUrl,
|
|
981
|
+
...(redactedLocation ? { location: redactedLocation } : {}),
|
|
982
|
+
...(nextUrl ? { nextUrl: redactRedirectUrl(nextUrl) } : {}),
|
|
983
|
+
};
|
|
709
984
|
hops.push(hop);
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
985
|
+
let shouldStop = false;
|
|
986
|
+
if (stopWhen) {
|
|
987
|
+
try {
|
|
988
|
+
shouldStop = await stopWhen(realHop);
|
|
989
|
+
}
|
|
990
|
+
catch (error) {
|
|
991
|
+
let sanitizedError = error;
|
|
992
|
+
for (const [rawUrl, safeUrl] of [
|
|
993
|
+
[responseUrl, redactedResponseUrl],
|
|
994
|
+
[location, redactedLocation],
|
|
995
|
+
[nextUrl, nextUrl ? redactRedirectUrl(nextUrl) : undefined],
|
|
996
|
+
]) {
|
|
997
|
+
if (!rawUrl || !safeUrl)
|
|
998
|
+
continue;
|
|
999
|
+
sanitizedError = redactSensitiveError(sanitizedError, [...sensitiveValues], rawUrl, safeUrl);
|
|
1000
|
+
}
|
|
1001
|
+
throw sanitizedError;
|
|
1002
|
+
}
|
|
725
1003
|
}
|
|
726
|
-
|
|
1004
|
+
const decision = evaluateRedirectHop({
|
|
1005
|
+
status: response.status,
|
|
1006
|
+
method,
|
|
1007
|
+
nextUrl,
|
|
1008
|
+
shouldStop,
|
|
1009
|
+
redirectCount: hops.length,
|
|
1010
|
+
maxHops,
|
|
1011
|
+
visitedRequests,
|
|
1012
|
+
});
|
|
1013
|
+
if (decision.kind === "stop") {
|
|
727
1014
|
return {
|
|
728
1015
|
final: response,
|
|
729
1016
|
hops,
|
|
730
|
-
reason:
|
|
1017
|
+
reason: decision.reason,
|
|
731
1018
|
cookies: cookieJar.snapshot(),
|
|
1019
|
+
cookieStore: cookieJar.serialize(),
|
|
732
1020
|
};
|
|
733
1021
|
}
|
|
734
|
-
|
|
735
|
-
if (nextMethod !== method) {
|
|
1022
|
+
if (decision.nextMethod !== method) {
|
|
736
1023
|
body = undefined;
|
|
737
1024
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
final: response,
|
|
741
|
-
hops,
|
|
742
|
-
reason: "loop",
|
|
743
|
-
cookies: cookieJar.snapshot(),
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
method = nextMethod;
|
|
747
|
-
currentUrl = nextUrl;
|
|
1025
|
+
method = decision.nextMethod;
|
|
1026
|
+
currentUrl = decision.nextUrl;
|
|
748
1027
|
}
|
|
749
1028
|
if (!response) {
|
|
750
1029
|
response = await session.fetch(currentUrl, {
|
|
@@ -761,6 +1040,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
761
1040
|
hops,
|
|
762
1041
|
reason: "max_hops",
|
|
763
1042
|
cookies: cookieJar.snapshot(),
|
|
1043
|
+
cookieStore: cookieJar.serialize(),
|
|
764
1044
|
};
|
|
765
1045
|
},
|
|
766
1046
|
},
|