@apifuse/provider-sdk 2.2.0-beta.11 → 2.2.0-beta.12
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 +37 -0
- package/CHANGELOG.md +4 -0
- package/README.md +18 -0
- package/bin/apifuse-pack-smoke.ts +14 -0
- package/bin/apifuse-pack-types.ts +11 -1
- package/dist/config/loader.d.ts +9 -1
- package/dist/config/loader.js +9 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +15 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/runtime/stealth.js +113 -47
- package/dist/types.d.ts +39 -7
- package/package.json +2 -1
- package/src/config/loader.ts +22 -1
- package/src/errors.ts +15 -0
- package/src/index.ts +8 -1
- package/src/runtime/stealth.ts +127 -48
- package/src/types.ts +41 -7
package/AUTHORING.md
CHANGED
|
@@ -517,6 +517,43 @@ const credentialsAuth = defineCredentialsAuth({
|
|
|
517
517
|
`bunx playwright install chromium`, or set
|
|
518
518
|
`APIFUSE__CDP_POOL__URL` for remote browser debugging.
|
|
519
519
|
|
|
520
|
+
### Persisting stealth session cookies
|
|
521
|
+
|
|
522
|
+
Persist `session.cookies.serialize()` as JSON when an authenticated session must
|
|
523
|
+
survive a restart or move to another replica. The returned
|
|
524
|
+
`StealthCookieStoreV1` has an explicit version and retains every cookie together
|
|
525
|
+
with its Domain, Path, Secure, expiry, host-only, and other cookie attributes.
|
|
526
|
+
Restore it with `session.cookies.deserialize()`. Unsupported future versions
|
|
527
|
+
fail explicitly instead of being accepted as a partial cookie jar.
|
|
528
|
+
|
|
529
|
+
Credential values are strings, so stringify the store at the credential
|
|
530
|
+
boundary and parse it when rebuilding the session:
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
// After login (including any redirects across sibling hosts):
|
|
534
|
+
const result = await session.redirects.run({ url: loginUrl });
|
|
535
|
+
return {
|
|
536
|
+
credential: {
|
|
537
|
+
cookieStore: JSON.stringify(result.cookieStore),
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
// In a later operation or replica:
|
|
542
|
+
const persisted = ctx.credential.get("cookieStore");
|
|
543
|
+
if (persisted) {
|
|
544
|
+
session.cookies.deserialize(JSON.parse(persisted));
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
`snapshot()` and `restore()` remain only for backward compatibility with flat
|
|
549
|
+
`Record<string, string>` credentials. `snapshot()` enumerates cookies across all
|
|
550
|
+
hosts and paths, but the flat shape is inherently lossy: duplicate names
|
|
551
|
+
collapse and Domain, Path, Secure, expiry, and host-only attributes cannot be
|
|
552
|
+
represented. `restore()` therefore recreates host-only `Path=/` cookies on the
|
|
553
|
+
session base origin. Do not use the flat form for new persistence code. Cookie
|
|
554
|
+
headers remain origin-filtered: use `toHeader(url)` for a particular request and
|
|
555
|
+
never build a request header from serialized or snapshotted persistence data.
|
|
556
|
+
|
|
520
557
|
### Running the pre-submission report
|
|
521
558
|
|
|
522
559
|
```bash
|
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -146,6 +146,24 @@ the bad request path; provider/runtime failures include `code`, `message`, and
|
|
|
146
146
|
`profile` such as `chrome-146`; do not tune JA3, HTTP/2 SETTINGS, or
|
|
147
147
|
pseudo-header order in provider code. Chrome/Firefox-style profiles are
|
|
148
148
|
supported; use `ctx.browser` when Safari-specific behavior is required.
|
|
149
|
+
- **Proxy URLs for non-stealth consumers**: use `resolveProxy()` when a
|
|
150
|
+
provider-owned client outside `ctx.stealth` needs the provider's proxy, such
|
|
151
|
+
as a CAPTCHA solver that must use matching egress. Pass the provider proxy
|
|
152
|
+
policy used by the provider and consume the returned `url`; the SDK owns
|
|
153
|
+
vendor selection, allocation, failover, and URL formats. Never call proxy
|
|
154
|
+
allocator APIs or hardcode proxy vendor hostnames in provider code.
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { resolveProxy } from "@apifuse/provider-sdk"
|
|
158
|
+
|
|
159
|
+
const resolvedProxy = await resolveProxy({
|
|
160
|
+
proxyPolicy,
|
|
161
|
+
affinityKey: connectionId,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
if (!resolvedProxy.url) throw new Error("This login requires proxy egress")
|
|
165
|
+
const captchaTask = { proxy: resolvedProxy.url }
|
|
166
|
+
```
|
|
149
167
|
- **Browser providers**: for TypeScript Providers use `runtime: "browser"` plus
|
|
150
168
|
`browser.engine: "playwright-stealth"`; `nodriver` is a Python-runtime path.
|
|
151
169
|
Install local browser assets with `bunx playwright install chromium` when
|
|
@@ -59,6 +59,20 @@ try {
|
|
|
59
59
|
);
|
|
60
60
|
|
|
61
61
|
run("bun", ["install"], consumerDir);
|
|
62
|
+
run(
|
|
63
|
+
"bun",
|
|
64
|
+
[
|
|
65
|
+
"--eval",
|
|
66
|
+
[
|
|
67
|
+
'import { resolveProxy } from "@apifuse/provider-sdk";',
|
|
68
|
+
'if (typeof resolveProxy !== "function") throw new Error("resolveProxy is not exported");',
|
|
69
|
+
'const resolved = await resolveProxy({ proxy: "http://127.0.0.1:8080" });',
|
|
70
|
+
'if (resolved.url !== "http://127.0.0.1:8080") throw new Error("resolveProxy returned the wrong URL");',
|
|
71
|
+
'console.log("packed root resolveProxy export OK");',
|
|
72
|
+
].join("\n"),
|
|
73
|
+
],
|
|
74
|
+
consumerDir,
|
|
75
|
+
);
|
|
62
76
|
|
|
63
77
|
const cliBin = join(consumerDir, "node_modules", ".bin", "apifuse");
|
|
64
78
|
if (!existsSync(cliBin)) {
|
|
@@ -123,7 +123,8 @@ function setUpFixtureConsumer(consumerDir: string, tarballPath: string): void {
|
|
|
123
123
|
writeFileSync(
|
|
124
124
|
join(consumerDir, "consumer.ts"),
|
|
125
125
|
[
|
|
126
|
-
'import { ProviderError, SessionExpiredError, z } from "@apifuse/provider-sdk";',
|
|
126
|
+
'import { ProviderError, resolveProxy, SessionExpiredError, z } from "@apifuse/provider-sdk";',
|
|
127
|
+
'import type { ProxyProtocol, ProxyResolutionOptions, ProxyResolutionSource, ProxyVendorName, ResolvedProxyConfig } from "@apifuse/provider-sdk";',
|
|
127
128
|
'import { defineCredentialsAuth } from "@apifuse/provider-sdk/provider";',
|
|
128
129
|
'import { extractProviderContract } from "@apifuse/provider-sdk/contract";',
|
|
129
130
|
'import { AUTH_TURN_SCHEMA } from "@apifuse/provider-sdk/auth-turn";',
|
|
@@ -143,6 +144,11 @@ function setUpFixtureConsumer(consumerDir: string, tarballPath: string): void {
|
|
|
143
144
|
"// Re-exported zod must keep .refine() callback parameter inference.",
|
|
144
145
|
"const refined = z.object({ shopId: z.string() }).refine((value) => value.shopId.length > 0);",
|
|
145
146
|
'const refinedString = z.string().refine((value) => value.startsWith("tabelog:"));',
|
|
147
|
+
'const proxyOptions: ProxyResolutionOptions = { proxyPolicy: { mode: "disabled" } };',
|
|
148
|
+
'const proxyProtocol: ProxyProtocol = "http";',
|
|
149
|
+
"const proxyResult: Promise<ResolvedProxyConfig> = resolveProxy(proxyOptions);",
|
|
150
|
+
'const proxySource: ProxyResolutionSource = "smartproxy-allocator";',
|
|
151
|
+
'const proxyVendor: ProxyVendorName = "smartproxy";',
|
|
146
152
|
"",
|
|
147
153
|
"export const witnesses = {",
|
|
148
154
|
" inheritedName,",
|
|
@@ -152,6 +158,10 @@ function setUpFixtureConsumer(consumerDir: string, tarballPath: string): void {
|
|
|
152
158
|
" sessionExpired,",
|
|
153
159
|
" refined,",
|
|
154
160
|
" refinedString,",
|
|
161
|
+
" proxyResult,",
|
|
162
|
+
" proxyProtocol,",
|
|
163
|
+
" proxySource,",
|
|
164
|
+
" proxyVendor,",
|
|
155
165
|
" defineCredentialsAuth,",
|
|
156
166
|
" extractProviderContract,",
|
|
157
167
|
" AUTH_TURN_SCHEMA,",
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -112,10 +112,13 @@ export type ProxyTelemetrySink = {
|
|
|
112
112
|
recordProxyAttempt?(event: ProxyAttemptTelemetryEvent): void;
|
|
113
113
|
recordProxyVendorFailover?(event: ProxyVendorFailoverTelemetryEvent): void;
|
|
114
114
|
};
|
|
115
|
+
export type ProxyResolutionSource = "explicit" | "env" | "config" | "smartproxy-allocator" | "nodemaven-gateway";
|
|
115
116
|
export type ResolvedProxyConfig = {
|
|
116
117
|
shouldWarn: boolean;
|
|
117
118
|
url?: string;
|
|
118
|
-
|
|
119
|
+
/** SDK-native vendor that supplied the URL, when applicable. */
|
|
120
|
+
vendor?: ProxyVendorName;
|
|
121
|
+
source?: ProxyResolutionSource;
|
|
119
122
|
protocol?: ProxyProtocol;
|
|
120
123
|
diagnostics?: Record<string, string | number | boolean>;
|
|
121
124
|
};
|
|
@@ -142,6 +145,11 @@ export declare function __setProxyRedisForTests(redis: ProxyRedisClient | undefi
|
|
|
142
145
|
export declare function __setSmartproxyAllocatorDeadlineMsForTests(deadlineMs: number | undefined): void;
|
|
143
146
|
export declare function resolveProxyConfig(options?: ProxyResolutionOptions): ResolvedProxyConfig;
|
|
144
147
|
export declare function resolveProxyConfigAsync(options?: ProxyResolutionOptions): Promise<ResolvedProxyConfig>;
|
|
148
|
+
/**
|
|
149
|
+
* Resolve the proxy URL for a provider-owned consumer such as a CAPTCHA solver.
|
|
150
|
+
* Vendor allocation and failover remain owned by the SDK.
|
|
151
|
+
*/
|
|
152
|
+
export declare function resolveProxy(options?: ProxyResolutionOptions): Promise<ResolvedProxyConfig>;
|
|
145
153
|
/**
|
|
146
154
|
* Guard the No-MITM invariant: a resolved proxy URL must use a tunnelling scheme
|
|
147
155
|
* (http CONNECT or socks5) so the client TLS handshake reaches the origin
|
package/dist/config/loader.js
CHANGED
|
@@ -400,6 +400,15 @@ export async function resolveProxyConfigAsync(options = {}) {
|
|
|
400
400
|
}
|
|
401
401
|
return { shouldWarn: true };
|
|
402
402
|
}
|
|
403
|
+
/**
|
|
404
|
+
* Resolve the proxy URL for a provider-owned consumer such as a CAPTCHA solver.
|
|
405
|
+
* Vendor allocation and failover remain owned by the SDK.
|
|
406
|
+
*/
|
|
407
|
+
export async function resolveProxy(options = {}) {
|
|
408
|
+
const resolved = await resolveProxyConfigAsync(options);
|
|
409
|
+
const vendor = vendorFromResolvedSource(resolved.source);
|
|
410
|
+
return vendor ? { ...resolved, vendor } : resolved;
|
|
411
|
+
}
|
|
403
412
|
/**
|
|
404
413
|
* Each vendor's default egress protocol, chosen from live KR benchmarks. HTTP
|
|
405
414
|
* CONNECT wins for nodemaven (socks5 adds ~500ms through the gateway) and ties
|
package/dist/errors.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export declare class ProviderError extends Error {
|
|
|
17
17
|
export declare class SDKError extends ProviderError {
|
|
18
18
|
constructor(message: string, options?: ProviderErrorOptions);
|
|
19
19
|
}
|
|
20
|
+
/** Raised when persisted stealth cookies use a store version this SDK cannot read. */
|
|
21
|
+
export declare class StealthCookieStoreVersionError extends SDKError {
|
|
22
|
+
readonly version: unknown;
|
|
23
|
+
constructor(version: unknown);
|
|
24
|
+
}
|
|
20
25
|
export declare class AuthError extends ProviderError {
|
|
21
26
|
constructor(message: string, options?: ProviderErrorOptions);
|
|
22
27
|
}
|
package/dist/errors.js
CHANGED
|
@@ -57,6 +57,21 @@ export class SDKError extends ProviderError {
|
|
|
57
57
|
this.name = "SDKError";
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
+
/** Raised when persisted stealth cookies use a store version this SDK cannot read. */
|
|
61
|
+
export class StealthCookieStoreVersionError extends SDKError {
|
|
62
|
+
version;
|
|
63
|
+
constructor(version) {
|
|
64
|
+
const displayedVersion = typeof version === "string" || typeof version === "number"
|
|
65
|
+
? String(version)
|
|
66
|
+
: "missing or invalid";
|
|
67
|
+
super(`Unsupported stealth cookie store version: ${displayedVersion}`, {
|
|
68
|
+
code: "unsupported_stealth_cookie_store_version",
|
|
69
|
+
details: { receivedVersion: version, supportedVersions: [1] },
|
|
70
|
+
});
|
|
71
|
+
this.version = version;
|
|
72
|
+
this.name = "StealthCookieStoreVersionError";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
60
75
|
export class AuthError extends ProviderError {
|
|
61
76
|
constructor(message, options) {
|
|
62
77
|
super(message, options);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export * from "./auth.js";
|
|
2
2
|
export * from "./ceremonies/index.js";
|
|
3
3
|
export * from "./choice-token.js";
|
|
4
|
-
export type { ApiFuseConfig, BrowserConfig, ProxyConfig, SessionConfig, } from "./config/loader.js";
|
|
5
|
-
export { defineConfig, loadApiFuseConfig } from "./config/loader.js";
|
|
4
|
+
export type { ApiFuseConfig, BrowserConfig, ProxyConfig, ProxyProtocol, ProxyResolutionOptions, ProxyResolutionSource, ProxyVendorName, ResolvedProxyConfig, SessionConfig, } from "./config/loader.js";
|
|
5
|
+
export { defineConfig, loadApiFuseConfig, resolveProxy } from "./config/loader.js";
|
|
6
6
|
export { canonicalJson, digestProviderContract, extractProviderContract, type JsonPrimitive, type JsonValue, PROVIDER_CONTRACT_SCHEMA_VERSION, type ProviderContractOperation, type ProviderContractSnapshot, } from "./contract.js";
|
|
7
7
|
export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, defineStreamOperation, every, type ProviderConfig, } from "./define.js";
|
|
8
8
|
export type { DevServerOptions } from "./dev.js";
|
|
@@ -36,7 +36,7 @@ export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SEN
|
|
|
36
36
|
export { createServerApp, type ServeOptions, serve } from "./server/index.js";
|
|
37
37
|
export { getStealthProfile, listStealthProfiles } from "./stealth/profiles.js";
|
|
38
38
|
export * from "./stream.js";
|
|
39
|
-
export type { ApiFuseResponse, AuthConfig, AuthContext, AuthFlowDefinition, AuthFlowInputHandler, AuthFlowStartHandler, AuthMode, AuthTurn, Bcp47Locale, BrowserEngine, BrowserOptions, BrowserResourceBody, BrowserResourceDecision, BrowserResourceMethod, BrowserResourcePolicy, BrowserResourceRequest, BrowserResourceRoute, ConnectionMode, ContextDeclaration, CookieJar, ContextScratchpad, CredentialContext, CredentialDeclaration, E164PhoneNumber, EnvContext, FlowContext, FlowContextStore, HealthCheckAssertionContext, HealthCheckCase, HealthCheckCaseResult, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyGatewayContext, HealthJourneyJournalContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthJourneySchedule, HealthScheduleRandomization, HealthJourneySmsContext, HealthJourneyStep, HttpClient, HttpMethod, HttpResponse, HttpRetryOptions, HttpRetrySummary, HttpStreamResponse, IanaTimeZone, InferSchemaOutput, Iso3166Alpha2CountryCode, Iso4217CurrencyCode, Iso8601Duration, OperationAnnotations, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDeprecationMetadata, OperationDocMeta, OperationErrorCode, OperationHandlerResult, OperationInputExample, OperationLifecycle, OperationObservabilityConfig, OperationObservabilitySensitiveConfig, OperationRelationships, OperationRiskClass, OperationSensitivePath, OperationToolRouterMetadata, OperationTransport, OperationTransportKind, ProbeInterval, ProviderAccessConfig, ProviderAccessVisibility, ProviderCache, ProviderCacheGetOrSetOptions, ProviderCacheKeyOptions, ProviderCacheLookupMeta, ProviderCacheResponseMeta, ProviderCacheResult, ProviderChoiceBindingOptions, ProviderChoiceContext, ProviderChoiceIssueOptions, ProviderChoiceParseOptions, ProviderContext, ProviderDefinition, ProviderDeploymentOverrides, ProviderHealthMonitorConfig, ProviderHealthProbeConfig, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderLogoSource, ProviderMeta, ProviderProxyConfig, ProviderProxyMode, ProviderProxyPolicy, ProviderProxyProvider, ProviderProxySessionAffinity, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderReviewed, ProviderRuntimeState, ProviderSecretDeclaration, ProviderStateDurationString, ProviderStateNamespace, ProviderStreamEvent, ProviderSttConfig, ProviderSttMode, ProviderSupportLevel, RequestOptions, Rfc3339Instant, SchemaLike, SmsOrigin, SmsOtpExtractionPattern, SmsOtpMatcherDefinition, SseMessage, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateValue, StateWriteOptions, StealthClient, StealthFetchOptions, StealthPlatform, StealthProfile, StealthRedirectHop, StealthRedirectRunOptions, StealthRedirectRunResult, StealthResponse, StealthSession, StealthSessionCookies, SttAudioInput, SttContext, SttPromptPolicy, SttSegment, SttTranscribeMode, SttTranscribeRequest, SttTranscript, SttUnsupportedOptionPolicy, SttUsage, SttVerificationCodeOptions, SttWarning, TraceConfig, TraceSpan, VerificationCodeCandidate, VerificationCodeCandidateSource, VerificationCodeExtractionResult, } from "./types.js";
|
|
39
|
+
export type { ApiFuseResponse, AuthConfig, AuthContext, AuthFlowDefinition, AuthFlowInputHandler, AuthFlowStartHandler, AuthMode, AuthTurn, Bcp47Locale, BrowserEngine, BrowserOptions, BrowserResourceBody, BrowserResourceDecision, BrowserResourceMethod, BrowserResourcePolicy, BrowserResourceRequest, BrowserResourceRoute, ConnectionMode, ContextDeclaration, CookieJar, ContextScratchpad, CredentialContext, CredentialDeclaration, E164PhoneNumber, EnvContext, FlowContext, FlowContextStore, HealthCheckAssertionContext, HealthCheckCase, HealthCheckCaseResult, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyGatewayContext, HealthJourneyJournalContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthJourneySchedule, HealthScheduleRandomization, HealthJourneySmsContext, HealthJourneyStep, HttpClient, HttpMethod, HttpResponse, HttpRetryOptions, HttpRetrySummary, HttpStreamResponse, IanaTimeZone, InferSchemaOutput, Iso3166Alpha2CountryCode, Iso4217CurrencyCode, Iso8601Duration, OperationAnnotations, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDeprecationMetadata, OperationDocMeta, OperationErrorCode, OperationHandlerResult, OperationInputExample, OperationLifecycle, OperationObservabilityConfig, OperationObservabilitySensitiveConfig, OperationRelationships, OperationRiskClass, OperationSensitivePath, OperationToolRouterMetadata, OperationTransport, OperationTransportKind, ProbeInterval, ProviderAccessConfig, ProviderAccessVisibility, ProviderCache, ProviderCacheGetOrSetOptions, ProviderCacheKeyOptions, ProviderCacheLookupMeta, ProviderCacheResponseMeta, ProviderCacheResult, ProviderChoiceBindingOptions, ProviderChoiceContext, ProviderChoiceIssueOptions, ProviderChoiceParseOptions, ProviderContext, ProviderDefinition, ProviderDeploymentOverrides, ProviderHealthMonitorConfig, ProviderHealthProbeConfig, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderLogoSource, ProviderMeta, ProviderProxyConfig, ProviderProxyMode, ProviderProxyPolicy, ProviderProxyProvider, ProviderProxySessionAffinity, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderReviewed, ProviderRuntimeState, ProviderSecretDeclaration, ProviderStateDurationString, ProviderStateNamespace, ProviderStreamEvent, ProviderSttConfig, ProviderSttMode, ProviderSupportLevel, RequestOptions, Rfc3339Instant, SchemaLike, SmsOrigin, SmsOtpExtractionPattern, SmsOtpMatcherDefinition, SseMessage, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateValue, StateWriteOptions, StealthClient, StealthCookieStore, StealthCookieStoreV1, StealthFetchOptions, StealthPlatform, StealthProfile, StealthRedirectHop, StealthRedirectRunOptions, StealthRedirectRunResult, StealthResponse, StealthSession, StealthSessionCookies, SttAudioInput, SttContext, SttPromptPolicy, SttSegment, SttTranscribeMode, SttTranscribeRequest, SttTranscript, SttUnsupportedOptionPolicy, SttUsage, SttVerificationCodeOptions, SttWarning, TraceConfig, TraceSpan, VerificationCodeCandidate, VerificationCodeCandidateSource, VerificationCodeExtractionResult, } from "./types.js";
|
|
40
40
|
export { DEFAULT_OPERATION_TRANSPORT, HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, PROBE_INTERVALS, STREAM_CHUNK_BYTES_MAX, STREAM_CHUNK_BYTES_MIN, STREAM_HEARTBEAT_MS_MAX, STREAM_HEARTBEAT_MS_MIN, STREAM_IDLE_TIMEOUT_MS_MAX, STREAM_IDLE_TIMEOUT_MS_MIN, STREAM_MAX_DURATION_MS_MAX, STREAM_MAX_DURATION_MS_MIN, } from "./types.js";
|
|
41
41
|
export * from "./utils/date.js";
|
|
42
42
|
export * from "./utils/parse.js";
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export * from "./auth.js";
|
|
3
3
|
export * from "./ceremonies/index.js";
|
|
4
4
|
export * from "./choice-token.js";
|
|
5
|
-
export { defineConfig, loadApiFuseConfig } from "./config/loader.js";
|
|
5
|
+
export { defineConfig, loadApiFuseConfig, resolveProxy } from "./config/loader.js";
|
|
6
6
|
export { canonicalJson, digestProviderContract, extractProviderContract, PROVIDER_CONTRACT_SCHEMA_VERSION, } from "./contract.js";
|
|
7
7
|
export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, defineStreamOperation, every, } from "./define.js";
|
|
8
8
|
export { createDevServer, startDevServer } from "./dev.js";
|
package/dist/runtime/stealth.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { Impit } from "impit";
|
|
3
|
-
import {
|
|
4
|
-
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";
|
|
5
6
|
import { getStealthProfile } from "../stealth/profiles.js";
|
|
6
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";
|
|
7
8
|
import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, normalizeProxyTransportRetryOptions, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy.js";
|
|
@@ -49,69 +50,128 @@ const FIREFOX_IMPIT_BY_MAJOR = {
|
|
|
49
50
|
function isRecord(value) {
|
|
50
51
|
return typeof value === "object" && value !== null;
|
|
51
52
|
}
|
|
53
|
+
const LEGACY_COOKIE_ORIGIN = "https://legacy-cookie.invalid/";
|
|
52
54
|
class CookieJarImpl {
|
|
53
55
|
cookies;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
defaultUrl;
|
|
57
|
+
constructor(cookieStrings, defaultUrl = LEGACY_COOKIE_ORIGIN) {
|
|
58
|
+
this.cookies = new ToughCookieJar(undefined, {
|
|
59
|
+
allowSecureOnLocal: false,
|
|
60
|
+
rejectPublicSuffixes: true,
|
|
61
|
+
});
|
|
62
|
+
this.defaultUrl = this.normalizeUrl(defaultUrl) ?? LEGACY_COOKIE_ORIGIN;
|
|
56
63
|
this.setFromCookieStrings(cookieStrings);
|
|
57
64
|
}
|
|
58
|
-
|
|
65
|
+
/**
|
|
66
|
+
* URL-less legacy operations are scoped to this jar's default URL. Session
|
|
67
|
+
* jars use the client's base URL and response jars use the response URL. A
|
|
68
|
+
* flat restore has no attributes to recover, so it creates host-only Path=/
|
|
69
|
+
* cookies for that default URL instead of making them visible to every host.
|
|
70
|
+
*/
|
|
71
|
+
setFromCookieStrings(cookieStrings, url = this.defaultUrl) {
|
|
72
|
+
const cookieUrl = this.normalizeUrl(url);
|
|
73
|
+
if (!cookieUrl)
|
|
74
|
+
return;
|
|
59
75
|
for (const cookieString of cookieStrings) {
|
|
60
|
-
|
|
61
|
-
if (!nameValue) {
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
const separatorIndex = nameValue.indexOf("=");
|
|
65
|
-
if (separatorIndex === -1) {
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
const name = nameValue.slice(0, separatorIndex).trim();
|
|
69
|
-
const value = nameValue.slice(separatorIndex + 1).trim();
|
|
70
|
-
if (name)
|
|
71
|
-
this.cookies[name] = value;
|
|
76
|
+
this.cookies.setCookieSync(cookieString, cookieUrl, { ignoreError: true });
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
|
-
get(name) {
|
|
75
|
-
return this.
|
|
79
|
+
get(name, url) {
|
|
80
|
+
return this.getAll(url)[name];
|
|
76
81
|
}
|
|
77
|
-
getAll() {
|
|
78
|
-
return
|
|
82
|
+
getAll(url) {
|
|
83
|
+
return Object.fromEntries(this.getUniqueCookies(url ?? this.defaultUrl).map((cookie) => [cookie.key, cookie.value]));
|
|
79
84
|
}
|
|
80
|
-
has(name) {
|
|
81
|
-
return Object.hasOwn(this.
|
|
85
|
+
has(name, url) {
|
|
86
|
+
return Object.hasOwn(this.getAll(url), name);
|
|
82
87
|
}
|
|
83
|
-
toString() {
|
|
84
|
-
return
|
|
85
|
-
.map((
|
|
88
|
+
toString(url) {
|
|
89
|
+
return this.getUniqueCookies(url ?? this.defaultUrl)
|
|
90
|
+
.map((cookie) => cookie.cookieString())
|
|
86
91
|
.join("; ");
|
|
87
92
|
}
|
|
88
|
-
toHeader() {
|
|
89
|
-
return this.toString();
|
|
93
|
+
toHeader(url) {
|
|
94
|
+
return this.toString(url);
|
|
90
95
|
}
|
|
91
96
|
snapshot() {
|
|
92
|
-
|
|
97
|
+
// This compatibility view deliberately enumerates the serialized store,
|
|
98
|
+
// not getAll(defaultUrl): persistence must include sibling hosts and paths.
|
|
99
|
+
// Duplicate names still collapse because a flat map cannot represent them.
|
|
100
|
+
const entries = [];
|
|
101
|
+
for (const cookie of this.serialize().jar.cookies) {
|
|
102
|
+
if (typeof cookie.key === "string" && typeof cookie.value === "string" && cookie.key) {
|
|
103
|
+
entries.push([cookie.key, cookie.value]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return Object.fromEntries(entries);
|
|
93
107
|
}
|
|
94
108
|
restore(cookies) {
|
|
95
109
|
this.clear();
|
|
96
110
|
for (const [name, value] of Object.entries(cookies)) {
|
|
97
|
-
if (name)
|
|
98
|
-
|
|
111
|
+
if (!name)
|
|
112
|
+
continue;
|
|
113
|
+
this.cookies.setCookieSync(new Cookie({ key: name, path: "/", value }), this.defaultUrl, {
|
|
114
|
+
ignoreError: true,
|
|
115
|
+
});
|
|
99
116
|
}
|
|
100
117
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
118
|
+
serialize() {
|
|
119
|
+
const jar = this.cookies.serializeSync();
|
|
120
|
+
if (!jar) {
|
|
121
|
+
throw new SDKError("Stealth cookie store could not be serialized", {
|
|
122
|
+
code: "stealth_cookie_store_serialize_failed",
|
|
123
|
+
});
|
|
104
124
|
}
|
|
125
|
+
return { version: 1, jar };
|
|
105
126
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
127
|
+
deserialize(state) {
|
|
128
|
+
const version = isRecord(state) ? state.version : undefined;
|
|
129
|
+
if (version !== 1) {
|
|
130
|
+
throw new StealthCookieStoreVersionError(version);
|
|
131
|
+
}
|
|
132
|
+
// Deserialize into a new jar first so invalid state cannot partially clear
|
|
133
|
+
// or replace a live session. tough-cookie restores the cookie attributes and
|
|
134
|
+
// matching semantics represented in its own serialized format.
|
|
135
|
+
const restored = ToughCookieJar.deserializeSync(state.jar);
|
|
136
|
+
// tough-cookie 6 does not include this option in serializeSync(). Preserve
|
|
137
|
+
// the SDK's stricter setting across restoration.
|
|
138
|
+
Reflect.set(restored, "allowSecureOnLocal", false);
|
|
139
|
+
this.cookies = restored;
|
|
140
|
+
}
|
|
141
|
+
clear() {
|
|
142
|
+
this.cookies.removeAllCookiesSync();
|
|
143
|
+
}
|
|
144
|
+
find(predicate, url) {
|
|
145
|
+
for (const cookie of this.getUniqueCookies(url ?? this.defaultUrl)) {
|
|
146
|
+
const cookieString = cookie.cookieString();
|
|
147
|
+
if (predicate(cookieString)) {
|
|
148
|
+
return cookieString;
|
|
111
149
|
}
|
|
112
150
|
}
|
|
113
151
|
return undefined;
|
|
114
152
|
}
|
|
153
|
+
normalizeUrl(url) {
|
|
154
|
+
try {
|
|
155
|
+
return new URL(url).toString();
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
getUniqueCookies(url) {
|
|
162
|
+
const cookieUrl = this.normalizeUrl(url);
|
|
163
|
+
if (!cookieUrl)
|
|
164
|
+
return [];
|
|
165
|
+
// tough-cookie returns longer (more-specific) paths first. Keeping the
|
|
166
|
+
// first cookie for each name prevents ambiguous duplicate-name headers.
|
|
167
|
+
const names = new Set();
|
|
168
|
+
return this.cookies.getCookiesSync(cookieUrl).filter((cookie) => {
|
|
169
|
+
if (names.has(cookie.key))
|
|
170
|
+
return false;
|
|
171
|
+
names.add(cookie.key);
|
|
172
|
+
return true;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
115
175
|
}
|
|
116
176
|
function closestImpitBrowser(major, candidates) {
|
|
117
177
|
let closestMajor;
|
|
@@ -175,13 +235,13 @@ function hasOwn(object, key) {
|
|
|
175
235
|
}
|
|
176
236
|
function toImpitCookieJar(cookieJar) {
|
|
177
237
|
return {
|
|
178
|
-
setCookie(cookie,
|
|
179
|
-
cookieJar.setFromCookieStrings([cookie]);
|
|
238
|
+
setCookie(cookie, url, cb) {
|
|
239
|
+
cookieJar.setFromCookieStrings([cookie], url);
|
|
180
240
|
if (typeof cb === "function")
|
|
181
241
|
cb();
|
|
182
242
|
},
|
|
183
|
-
getCookieString(
|
|
184
|
-
return cookieJar.
|
|
243
|
+
getCookieString(url) {
|
|
244
|
+
return cookieJar.toHeader(url);
|
|
185
245
|
},
|
|
186
246
|
};
|
|
187
247
|
}
|
|
@@ -235,7 +295,7 @@ function splitCombinedSetCookieHeader(headerValue) {
|
|
|
235
295
|
}
|
|
236
296
|
export async function normalizeResponse(response, requestUrl) {
|
|
237
297
|
const headers = Object.fromEntries(response.headers.entries());
|
|
238
|
-
const cookies = new CookieJarImpl(setCookieHeadersFromResponse(response.headers));
|
|
298
|
+
const cookies = new CookieJarImpl(setCookieHeadersFromResponse(response.headers), response.url ?? requestUrl);
|
|
239
299
|
const bodyBytes = await response.arrayBuffer();
|
|
240
300
|
const body = new TextDecoder().decode(bodyBytes);
|
|
241
301
|
return {
|
|
@@ -441,7 +501,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
441
501
|
let closed = false;
|
|
442
502
|
let hasWarnedMissingProxy = false;
|
|
443
503
|
const warn = clientOptions.warn ?? console.warn;
|
|
444
|
-
const cookieJar = new CookieJarImpl([]);
|
|
504
|
+
const cookieJar = new CookieJarImpl([], baseUrl);
|
|
445
505
|
const impitCookieJar = toImpitCookieJar(cookieJar);
|
|
446
506
|
function getClient(profileName, proxyUrl, ignoreTlsErrors) {
|
|
447
507
|
if (closed) {
|
|
@@ -580,7 +640,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
580
640
|
const requestUrl = appendQueryParams(resolveUrl(baseUrl, url), options.params);
|
|
581
641
|
const headers = { ...(options.headers ?? {}) };
|
|
582
642
|
if (!hasHeader(headers, "Cookie")) {
|
|
583
|
-
const cookieHeader = cookieJar.
|
|
643
|
+
const cookieHeader = cookieJar.toHeader(requestUrl);
|
|
584
644
|
if (cookieHeader)
|
|
585
645
|
headers.Cookie = cookieHeader;
|
|
586
646
|
}
|
|
@@ -595,7 +655,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
595
655
|
}
|
|
596
656
|
const response = await getClient(profileName, proxy, ignoreTlsErrors).fetch(requestUrl, requestInit);
|
|
597
657
|
const normalized = await normalizeResponse(response, requestUrl);
|
|
598
|
-
cookieJar.setFromCookieStrings(setCookieHeadersFromResponse(response.headers));
|
|
658
|
+
cookieJar.setFromCookieStrings(setCookieHeadersFromResponse(response.headers), response.url ?? requestUrl);
|
|
599
659
|
if (proxy && isProxyConnectFailureResponse(response, normalized.body)) {
|
|
600
660
|
throw createProxyConnectFailureError(normalized.body);
|
|
601
661
|
}
|
|
@@ -735,6 +795,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
735
795
|
hops,
|
|
736
796
|
reason: "completed",
|
|
737
797
|
cookies: cookieJar.snapshot(),
|
|
798
|
+
cookieStore: cookieJar.serialize(),
|
|
738
799
|
};
|
|
739
800
|
}
|
|
740
801
|
const location = locationHeader(response.headers);
|
|
@@ -755,6 +816,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
755
816
|
hops,
|
|
756
817
|
reason: "stopped",
|
|
757
818
|
cookies: cookieJar.snapshot(),
|
|
819
|
+
cookieStore: cookieJar.serialize(),
|
|
758
820
|
};
|
|
759
821
|
}
|
|
760
822
|
if (!nextUrl) {
|
|
@@ -763,6 +825,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
763
825
|
hops,
|
|
764
826
|
reason: "missing_location",
|
|
765
827
|
cookies: cookieJar.snapshot(),
|
|
828
|
+
cookieStore: cookieJar.serialize(),
|
|
766
829
|
};
|
|
767
830
|
}
|
|
768
831
|
if (hops.length > maxHops) {
|
|
@@ -771,6 +834,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
771
834
|
hops,
|
|
772
835
|
reason: "max_hops",
|
|
773
836
|
cookies: cookieJar.snapshot(),
|
|
837
|
+
cookieStore: cookieJar.serialize(),
|
|
774
838
|
};
|
|
775
839
|
}
|
|
776
840
|
const nextMethod = nextRedirectMethod(response.status, method);
|
|
@@ -783,6 +847,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
783
847
|
hops,
|
|
784
848
|
reason: "loop",
|
|
785
849
|
cookies: cookieJar.snapshot(),
|
|
850
|
+
cookieStore: cookieJar.serialize(),
|
|
786
851
|
};
|
|
787
852
|
}
|
|
788
853
|
method = nextMethod;
|
|
@@ -803,6 +868,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
803
868
|
hops,
|
|
804
869
|
reason: "max_hops",
|
|
805
870
|
cookies: cookieJar.snapshot(),
|
|
871
|
+
cookieStore: cookieJar.serialize(),
|
|
806
872
|
};
|
|
807
873
|
},
|
|
808
874
|
},
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type ms from "ms";
|
|
2
|
+
import type { SerializedCookieJar } from "tough-cookie";
|
|
2
3
|
import type { infer as ZodInfer, ZodType } from "zod";
|
|
3
4
|
/** Minimal Standard Schema v1 shape accepted by provider operations. */
|
|
4
5
|
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
@@ -872,17 +873,42 @@ export interface StealthFetchOptions extends RequestOptions {
|
|
|
872
873
|
};
|
|
873
874
|
}
|
|
874
875
|
export interface CookieJar {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
876
|
+
/** URL-less reads use the jar's response URL or session base URL. */
|
|
877
|
+
get(name: string, url?: string): string | undefined;
|
|
878
|
+
getAll(url?: string): Record<string, string>;
|
|
879
|
+
toString(url?: string): string;
|
|
880
|
+
find?(predicate: (cookie: string) => boolean, url?: string): string | undefined;
|
|
879
881
|
}
|
|
882
|
+
/**
|
|
883
|
+
* Version 1 of the JSON-safe, attribute-preserving stealth cookie store.
|
|
884
|
+
* The nested jar is tough-cookie's serialized form and retains cookie origin,
|
|
885
|
+
* Path, Secure, expiry, host-only, and other RFC attributes.
|
|
886
|
+
*/
|
|
887
|
+
export interface StealthCookieStoreV1 {
|
|
888
|
+
readonly version: 1;
|
|
889
|
+
readonly jar: SerializedCookieJar;
|
|
890
|
+
}
|
|
891
|
+
/** Cookie persistence formats understood by this SDK version. */
|
|
892
|
+
export type StealthCookieStore = StealthCookieStoreV1;
|
|
880
893
|
export interface StealthSessionCookies extends CookieJar {
|
|
881
|
-
has(name: string): boolean;
|
|
882
|
-
|
|
883
|
-
|
|
894
|
+
has(name: string, url?: string): boolean;
|
|
895
|
+
/** URL-less writes are scoped to the session base URL. */
|
|
896
|
+
setFromCookieStrings(cookieStrings: readonly string[], url?: string): void;
|
|
897
|
+
toHeader(url?: string): string;
|
|
898
|
+
/**
|
|
899
|
+
* Returns every cookie as a flat name/value map, collapsing duplicate names.
|
|
900
|
+
* @deprecated Use serialize() for lossless, attribute-preserving persistence.
|
|
901
|
+
*/
|
|
884
902
|
snapshot(): Record<string, string>;
|
|
903
|
+
/**
|
|
904
|
+
* Restores flat values as host-only, Path=/ cookies on the session base URL.
|
|
905
|
+
* @deprecated Use deserialize() with state produced by serialize().
|
|
906
|
+
*/
|
|
885
907
|
restore(cookies: Record<string, string>): void;
|
|
908
|
+
/** Returns a versioned, JSON-safe, attribute-preserving representation of every cookie. */
|
|
909
|
+
serialize(): StealthCookieStoreV1;
|
|
910
|
+
/** Replaces the jar with a previously serialized, attribute-preserving cookie store. */
|
|
911
|
+
deserialize(state: StealthCookieStore): void;
|
|
886
912
|
clear(): void;
|
|
887
913
|
}
|
|
888
914
|
export interface DeclarativeStealthResponse {
|
|
@@ -925,7 +951,13 @@ export interface StealthRedirectRunResult {
|
|
|
925
951
|
final: StealthResponse;
|
|
926
952
|
hops: StealthRedirectHop[];
|
|
927
953
|
reason: "completed" | "stopped" | "max_hops" | "missing_location" | "loop";
|
|
954
|
+
/**
|
|
955
|
+
* Complete flat view across all redirect hosts. Attributes and duplicate names are lost.
|
|
956
|
+
* @deprecated Use cookieStore for lossless persistence.
|
|
957
|
+
*/
|
|
928
958
|
cookies: Record<string, string>;
|
|
959
|
+
/** Versioned, attribute-preserving cookie state accumulated across the redirect chain. */
|
|
960
|
+
cookieStore: StealthCookieStoreV1;
|
|
929
961
|
}
|
|
930
962
|
export interface StealthSession {
|
|
931
963
|
fetch(url: string, options?: StealthFetchOptions): Promise<StealthResponse>;
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.2.0-beta.
|
|
2
|
+
"version": "2.2.0-beta.12",
|
|
3
3
|
"name": "@apifuse/provider-sdk",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
@@ -114,6 +114,7 @@
|
|
|
114
114
|
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
|
115
115
|
"re2-wasm": "^1.0",
|
|
116
116
|
"safe-regex": "^2.1",
|
|
117
|
+
"tough-cookie": "^6.0.2",
|
|
117
118
|
"zod": "^4.4.3"
|
|
118
119
|
},
|
|
119
120
|
"repository": {
|
package/src/config/loader.ts
CHANGED
|
@@ -156,10 +156,19 @@ export type ProxyTelemetrySink = {
|
|
|
156
156
|
recordProxyVendorFailover?(event: ProxyVendorFailoverTelemetryEvent): void;
|
|
157
157
|
};
|
|
158
158
|
|
|
159
|
+
export type ProxyResolutionSource =
|
|
160
|
+
| "explicit"
|
|
161
|
+
| "env"
|
|
162
|
+
| "config"
|
|
163
|
+
| "smartproxy-allocator"
|
|
164
|
+
| "nodemaven-gateway";
|
|
165
|
+
|
|
159
166
|
export type ResolvedProxyConfig = {
|
|
160
167
|
shouldWarn: boolean;
|
|
161
168
|
url?: string;
|
|
162
|
-
|
|
169
|
+
/** SDK-native vendor that supplied the URL, when applicable. */
|
|
170
|
+
vendor?: ProxyVendorName;
|
|
171
|
+
source?: ProxyResolutionSource;
|
|
163
172
|
protocol?: ProxyProtocol;
|
|
164
173
|
diagnostics?: Record<string, string | number | boolean>;
|
|
165
174
|
};
|
|
@@ -656,6 +665,18 @@ export async function resolveProxyConfigAsync(
|
|
|
656
665
|
return { shouldWarn: true };
|
|
657
666
|
}
|
|
658
667
|
|
|
668
|
+
/**
|
|
669
|
+
* Resolve the proxy URL for a provider-owned consumer such as a CAPTCHA solver.
|
|
670
|
+
* Vendor allocation and failover remain owned by the SDK.
|
|
671
|
+
*/
|
|
672
|
+
export async function resolveProxy(
|
|
673
|
+
options: ProxyResolutionOptions = {},
|
|
674
|
+
): Promise<ResolvedProxyConfig> {
|
|
675
|
+
const resolved = await resolveProxyConfigAsync(options);
|
|
676
|
+
const vendor = vendorFromResolvedSource(resolved.source);
|
|
677
|
+
return vendor ? { ...resolved, vendor } : resolved;
|
|
678
|
+
}
|
|
679
|
+
|
|
659
680
|
/**
|
|
660
681
|
* Each vendor's default egress protocol, chosen from live KR benchmarks. HTTP
|
|
661
682
|
* CONNECT wins for nodemaven (socks5 adds ~500ms through the gateway) and ties
|
package/src/errors.ts
CHANGED
|
@@ -79,6 +79,21 @@ export class SDKError extends ProviderError {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** Raised when persisted stealth cookies use a store version this SDK cannot read. */
|
|
83
|
+
export class StealthCookieStoreVersionError extends SDKError {
|
|
84
|
+
constructor(public readonly version: unknown) {
|
|
85
|
+
const displayedVersion =
|
|
86
|
+
typeof version === "string" || typeof version === "number"
|
|
87
|
+
? String(version)
|
|
88
|
+
: "missing or invalid";
|
|
89
|
+
super(`Unsupported stealth cookie store version: ${displayedVersion}`, {
|
|
90
|
+
code: "unsupported_stealth_cookie_store_version",
|
|
91
|
+
details: { receivedVersion: version, supportedVersions: [1] },
|
|
92
|
+
});
|
|
93
|
+
this.name = "StealthCookieStoreVersionError";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
82
97
|
export class AuthError extends ProviderError {
|
|
83
98
|
constructor(message: string, options?: ProviderErrorOptions) {
|
|
84
99
|
super(message, options);
|
package/src/index.ts
CHANGED
|
@@ -7,9 +7,14 @@ export type {
|
|
|
7
7
|
ApiFuseConfig,
|
|
8
8
|
BrowserConfig,
|
|
9
9
|
ProxyConfig,
|
|
10
|
+
ProxyProtocol,
|
|
11
|
+
ProxyResolutionOptions,
|
|
12
|
+
ProxyResolutionSource,
|
|
13
|
+
ProxyVendorName,
|
|
14
|
+
ResolvedProxyConfig,
|
|
10
15
|
SessionConfig,
|
|
11
16
|
} from "./config/loader.js";
|
|
12
|
-
export { defineConfig, loadApiFuseConfig } from "./config/loader.js";
|
|
17
|
+
export { defineConfig, loadApiFuseConfig, resolveProxy } from "./config/loader.js";
|
|
13
18
|
export {
|
|
14
19
|
canonicalJson,
|
|
15
20
|
digestProviderContract,
|
|
@@ -245,6 +250,8 @@ export type {
|
|
|
245
250
|
StateValue,
|
|
246
251
|
StateWriteOptions,
|
|
247
252
|
StealthClient,
|
|
253
|
+
StealthCookieStore,
|
|
254
|
+
StealthCookieStoreV1,
|
|
248
255
|
StealthFetchOptions,
|
|
249
256
|
StealthPlatform,
|
|
250
257
|
StealthProfile,
|
package/src/runtime/stealth.ts
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
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
6
|
import type { ProxyResolutionOptions, ProxyVendorName } from "../config/loader.js";
|
|
6
7
|
import {
|
|
7
8
|
DEFAULT_SMARTPROXY_POOL_SIZE,
|
|
8
9
|
invalidateProxyResolutionCacheAsync,
|
|
9
|
-
policyResolvesRegistryVendorChain,
|
|
10
10
|
ProxyResolutionError,
|
|
11
|
+
policyResolvesRegistryVendorChain,
|
|
11
12
|
resolvePolicyProxyPoolSpan,
|
|
12
13
|
resolvePolicyTransportAttemptCap,
|
|
13
14
|
resolveProxyConfigAsync,
|
|
14
15
|
vendorFromResolvedSource,
|
|
15
16
|
} from "../config/loader.js";
|
|
16
|
-
import { SDKError, TransportError } from "../errors.js";
|
|
17
|
+
import { SDKError, StealthCookieStoreVersionError, TransportError } from "../errors.js";
|
|
17
18
|
import { getStealthProfile } from "../stealth/profiles.js";
|
|
18
19
|
import type {
|
|
19
20
|
CookieJar,
|
|
20
21
|
HttpMethod,
|
|
21
22
|
StealthClient,
|
|
23
|
+
StealthCookieStore,
|
|
24
|
+
StealthCookieStoreV1,
|
|
22
25
|
StealthFetchOptions,
|
|
23
26
|
StealthRedirectHop,
|
|
24
27
|
StealthResponse,
|
|
@@ -127,81 +130,145 @@ function isRecord(value: unknown): value is Record<PropertyKey, unknown> {
|
|
|
127
130
|
return typeof value === "object" && value !== null;
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
const LEGACY_COOKIE_ORIGIN = "https://legacy-cookie.invalid/";
|
|
134
|
+
|
|
130
135
|
class CookieJarImpl implements CookieJar {
|
|
131
|
-
private
|
|
136
|
+
private cookies: ToughCookieJar;
|
|
137
|
+
private readonly defaultUrl: string;
|
|
132
138
|
|
|
133
|
-
constructor(cookieStrings: string[]) {
|
|
134
|
-
this.cookies = {
|
|
139
|
+
constructor(cookieStrings: readonly string[], defaultUrl = LEGACY_COOKIE_ORIGIN) {
|
|
140
|
+
this.cookies = new ToughCookieJar(undefined, {
|
|
141
|
+
allowSecureOnLocal: false,
|
|
142
|
+
rejectPublicSuffixes: true,
|
|
143
|
+
});
|
|
144
|
+
this.defaultUrl = this.normalizeUrl(defaultUrl) ?? LEGACY_COOKIE_ORIGIN;
|
|
135
145
|
this.setFromCookieStrings(cookieStrings);
|
|
136
146
|
}
|
|
137
147
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
148
|
+
/**
|
|
149
|
+
* URL-less legacy operations are scoped to this jar's default URL. Session
|
|
150
|
+
* jars use the client's base URL and response jars use the response URL. A
|
|
151
|
+
* flat restore has no attributes to recover, so it creates host-only Path=/
|
|
152
|
+
* cookies for that default URL instead of making them visible to every host.
|
|
153
|
+
*/
|
|
154
|
+
setFromCookieStrings(cookieStrings: readonly string[], url = this.defaultUrl): void {
|
|
155
|
+
const cookieUrl = this.normalizeUrl(url);
|
|
156
|
+
if (!cookieUrl) return;
|
|
149
157
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (name) this.cookies[name] = value;
|
|
158
|
+
for (const cookieString of cookieStrings) {
|
|
159
|
+
this.cookies.setCookieSync(cookieString, cookieUrl, { ignoreError: true });
|
|
153
160
|
}
|
|
154
161
|
}
|
|
155
162
|
|
|
156
|
-
get(name: string): string | undefined {
|
|
157
|
-
return this.
|
|
163
|
+
get(name: string, url?: string): string | undefined {
|
|
164
|
+
return this.getAll(url)[name];
|
|
158
165
|
}
|
|
159
166
|
|
|
160
|
-
getAll(): Record<string, string> {
|
|
161
|
-
return
|
|
167
|
+
getAll(url?: string): Record<string, string> {
|
|
168
|
+
return Object.fromEntries(
|
|
169
|
+
this.getUniqueCookies(url ?? this.defaultUrl).map((cookie) => [cookie.key, cookie.value]),
|
|
170
|
+
);
|
|
162
171
|
}
|
|
163
172
|
|
|
164
|
-
has(name: string): boolean {
|
|
165
|
-
return Object.hasOwn(this.
|
|
173
|
+
has(name: string, url?: string): boolean {
|
|
174
|
+
return Object.hasOwn(this.getAll(url), name);
|
|
166
175
|
}
|
|
167
176
|
|
|
168
|
-
toString(): string {
|
|
169
|
-
return
|
|
170
|
-
.map((
|
|
177
|
+
toString(url?: string): string {
|
|
178
|
+
return this.getUniqueCookies(url ?? this.defaultUrl)
|
|
179
|
+
.map((cookie) => cookie.cookieString())
|
|
171
180
|
.join("; ");
|
|
172
181
|
}
|
|
173
182
|
|
|
174
|
-
toHeader(): string {
|
|
175
|
-
return this.toString();
|
|
183
|
+
toHeader(url?: string): string {
|
|
184
|
+
return this.toString(url);
|
|
176
185
|
}
|
|
177
186
|
|
|
178
187
|
snapshot(): Record<string, string> {
|
|
179
|
-
|
|
188
|
+
// This compatibility view deliberately enumerates the serialized store,
|
|
189
|
+
// not getAll(defaultUrl): persistence must include sibling hosts and paths.
|
|
190
|
+
// Duplicate names still collapse because a flat map cannot represent them.
|
|
191
|
+
const entries: [string, string][] = [];
|
|
192
|
+
for (const cookie of this.serialize().jar.cookies) {
|
|
193
|
+
if (typeof cookie.key === "string" && typeof cookie.value === "string" && cookie.key) {
|
|
194
|
+
entries.push([cookie.key, cookie.value]);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return Object.fromEntries(entries);
|
|
180
198
|
}
|
|
181
199
|
|
|
182
200
|
restore(cookies: Record<string, string>): void {
|
|
183
201
|
this.clear();
|
|
184
202
|
for (const [name, value] of Object.entries(cookies)) {
|
|
185
|
-
if (name)
|
|
203
|
+
if (!name) continue;
|
|
204
|
+
this.cookies.setCookieSync(new Cookie({ key: name, path: "/", value }), this.defaultUrl, {
|
|
205
|
+
ignoreError: true,
|
|
206
|
+
});
|
|
186
207
|
}
|
|
187
208
|
}
|
|
188
209
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
210
|
+
serialize(): StealthCookieStoreV1 {
|
|
211
|
+
const jar = this.cookies.serializeSync();
|
|
212
|
+
if (!jar) {
|
|
213
|
+
throw new SDKError("Stealth cookie store could not be serialized", {
|
|
214
|
+
code: "stealth_cookie_store_serialize_failed",
|
|
215
|
+
});
|
|
192
216
|
}
|
|
217
|
+
return { version: 1, jar };
|
|
193
218
|
}
|
|
194
219
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
220
|
+
deserialize(state: StealthCookieStore): void {
|
|
221
|
+
const version = isRecord(state) ? state.version : undefined;
|
|
222
|
+
if (version !== 1) {
|
|
223
|
+
throw new StealthCookieStoreVersionError(version);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Deserialize into a new jar first so invalid state cannot partially clear
|
|
227
|
+
// or replace a live session. tough-cookie restores the cookie attributes and
|
|
228
|
+
// matching semantics represented in its own serialized format.
|
|
229
|
+
const restored = ToughCookieJar.deserializeSync(state.jar);
|
|
230
|
+
// tough-cookie 6 does not include this option in serializeSync(). Preserve
|
|
231
|
+
// the SDK's stricter setting across restoration.
|
|
232
|
+
Reflect.set(restored, "allowSecureOnLocal", false);
|
|
233
|
+
this.cookies = restored;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
clear(): void {
|
|
237
|
+
this.cookies.removeAllCookiesSync();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
find(predicate: (cookie: string) => boolean, url?: string): string | undefined {
|
|
241
|
+
for (const cookie of this.getUniqueCookies(url ?? this.defaultUrl)) {
|
|
242
|
+
const cookieString = cookie.cookieString();
|
|
243
|
+
if (predicate(cookieString)) {
|
|
244
|
+
return cookieString;
|
|
200
245
|
}
|
|
201
246
|
}
|
|
202
247
|
|
|
203
248
|
return undefined;
|
|
204
249
|
}
|
|
250
|
+
|
|
251
|
+
private normalizeUrl(url: string): string | undefined {
|
|
252
|
+
try {
|
|
253
|
+
return new URL(url).toString();
|
|
254
|
+
} catch {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private getUniqueCookies(url: string): Cookie[] {
|
|
260
|
+
const cookieUrl = this.normalizeUrl(url);
|
|
261
|
+
if (!cookieUrl) return [];
|
|
262
|
+
|
|
263
|
+
// tough-cookie returns longer (more-specific) paths first. Keeping the
|
|
264
|
+
// first cookie for each name prevents ambiguous duplicate-name headers.
|
|
265
|
+
const names = new Set<string>();
|
|
266
|
+
return this.cookies.getCookiesSync(cookieUrl).filter((cookie) => {
|
|
267
|
+
if (names.has(cookie.key)) return false;
|
|
268
|
+
names.add(cookie.key);
|
|
269
|
+
return true;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
205
272
|
}
|
|
206
273
|
|
|
207
274
|
function closestImpitBrowser(
|
|
@@ -282,12 +349,12 @@ function hasOwn(object: object, key: string): boolean {
|
|
|
282
349
|
}
|
|
283
350
|
function toImpitCookieJar(cookieJar: CookieJarImpl): NonNullable<ImpitOptions["cookieJar"]> {
|
|
284
351
|
return {
|
|
285
|
-
setCookie(cookie: string,
|
|
286
|
-
cookieJar.setFromCookieStrings([cookie]);
|
|
352
|
+
setCookie(cookie: string, url: string, cb?: (error?: unknown) => void) {
|
|
353
|
+
cookieJar.setFromCookieStrings([cookie], url);
|
|
287
354
|
if (typeof cb === "function") cb();
|
|
288
355
|
},
|
|
289
|
-
getCookieString(
|
|
290
|
-
return cookieJar.
|
|
356
|
+
getCookieString(url: string) {
|
|
357
|
+
return cookieJar.toHeader(url);
|
|
291
358
|
},
|
|
292
359
|
};
|
|
293
360
|
}
|
|
@@ -341,7 +408,10 @@ export async function normalizeResponse(
|
|
|
341
408
|
requestUrl?: string,
|
|
342
409
|
): Promise<StealthResponse> {
|
|
343
410
|
const headers = Object.fromEntries(response.headers.entries());
|
|
344
|
-
const cookies = new CookieJarImpl(
|
|
411
|
+
const cookies = new CookieJarImpl(
|
|
412
|
+
setCookieHeadersFromResponse(response.headers),
|
|
413
|
+
response.url ?? requestUrl,
|
|
414
|
+
);
|
|
345
415
|
const bodyBytes = await response.arrayBuffer();
|
|
346
416
|
const body = new TextDecoder().decode(bodyBytes);
|
|
347
417
|
|
|
@@ -588,7 +658,7 @@ function createSessionFetcher(
|
|
|
588
658
|
let closed = false;
|
|
589
659
|
let hasWarnedMissingProxy = false;
|
|
590
660
|
const warn = clientOptions.warn ?? console.warn;
|
|
591
|
-
const cookieJar = new CookieJarImpl([]);
|
|
661
|
+
const cookieJar = new CookieJarImpl([], baseUrl);
|
|
592
662
|
const impitCookieJar = toImpitCookieJar(cookieJar);
|
|
593
663
|
|
|
594
664
|
function getClient(
|
|
@@ -757,7 +827,7 @@ function createSessionFetcher(
|
|
|
757
827
|
const requestUrl = appendQueryParams(resolveUrl(baseUrl, url), options.params);
|
|
758
828
|
const headers = { ...(options.headers ?? {}) };
|
|
759
829
|
if (!hasHeader(headers, "Cookie")) {
|
|
760
|
-
const cookieHeader = cookieJar.
|
|
830
|
+
const cookieHeader = cookieJar.toHeader(requestUrl);
|
|
761
831
|
if (cookieHeader) headers.Cookie = cookieHeader;
|
|
762
832
|
}
|
|
763
833
|
const requestInit: StealthRequestInit = {
|
|
@@ -774,7 +844,10 @@ function createSessionFetcher(
|
|
|
774
844
|
requestInit,
|
|
775
845
|
);
|
|
776
846
|
const normalized = await normalizeResponse(response, requestUrl);
|
|
777
|
-
cookieJar.setFromCookieStrings(
|
|
847
|
+
cookieJar.setFromCookieStrings(
|
|
848
|
+
setCookieHeadersFromResponse(response.headers),
|
|
849
|
+
response.url ?? requestUrl,
|
|
850
|
+
);
|
|
778
851
|
|
|
779
852
|
if (proxy && isProxyConnectFailureResponse(response, normalized.body)) {
|
|
780
853
|
throw createProxyConnectFailureError(normalized.body);
|
|
@@ -954,6 +1027,7 @@ function createSessionFetcher(
|
|
|
954
1027
|
hops,
|
|
955
1028
|
reason: "completed",
|
|
956
1029
|
cookies: cookieJar.snapshot(),
|
|
1030
|
+
cookieStore: cookieJar.serialize(),
|
|
957
1031
|
};
|
|
958
1032
|
}
|
|
959
1033
|
|
|
@@ -976,6 +1050,7 @@ function createSessionFetcher(
|
|
|
976
1050
|
hops,
|
|
977
1051
|
reason: "stopped",
|
|
978
1052
|
cookies: cookieJar.snapshot(),
|
|
1053
|
+
cookieStore: cookieJar.serialize(),
|
|
979
1054
|
};
|
|
980
1055
|
}
|
|
981
1056
|
|
|
@@ -985,6 +1060,7 @@ function createSessionFetcher(
|
|
|
985
1060
|
hops,
|
|
986
1061
|
reason: "missing_location",
|
|
987
1062
|
cookies: cookieJar.snapshot(),
|
|
1063
|
+
cookieStore: cookieJar.serialize(),
|
|
988
1064
|
};
|
|
989
1065
|
}
|
|
990
1066
|
|
|
@@ -994,6 +1070,7 @@ function createSessionFetcher(
|
|
|
994
1070
|
hops,
|
|
995
1071
|
reason: "max_hops",
|
|
996
1072
|
cookies: cookieJar.snapshot(),
|
|
1073
|
+
cookieStore: cookieJar.serialize(),
|
|
997
1074
|
};
|
|
998
1075
|
}
|
|
999
1076
|
|
|
@@ -1007,6 +1084,7 @@ function createSessionFetcher(
|
|
|
1007
1084
|
hops,
|
|
1008
1085
|
reason: "loop",
|
|
1009
1086
|
cookies: cookieJar.snapshot(),
|
|
1087
|
+
cookieStore: cookieJar.serialize(),
|
|
1010
1088
|
};
|
|
1011
1089
|
}
|
|
1012
1090
|
method = nextMethod;
|
|
@@ -1028,6 +1106,7 @@ function createSessionFetcher(
|
|
|
1028
1106
|
hops,
|
|
1029
1107
|
reason: "max_hops",
|
|
1030
1108
|
cookies: cookieJar.snapshot(),
|
|
1109
|
+
cookieStore: cookieJar.serialize(),
|
|
1031
1110
|
};
|
|
1032
1111
|
},
|
|
1033
1112
|
},
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type ms from "ms";
|
|
2
|
+
import type { SerializedCookieJar } from "tough-cookie";
|
|
2
3
|
|
|
3
4
|
import type { infer as ZodInfer, ZodType } from "zod";
|
|
4
5
|
|
|
@@ -1051,18 +1052,45 @@ export interface StealthFetchOptions extends RequestOptions {
|
|
|
1051
1052
|
}
|
|
1052
1053
|
|
|
1053
1054
|
export interface CookieJar {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1055
|
+
/** URL-less reads use the jar's response URL or session base URL. */
|
|
1056
|
+
get(name: string, url?: string): string | undefined;
|
|
1057
|
+
getAll(url?: string): Record<string, string>;
|
|
1058
|
+
toString(url?: string): string;
|
|
1059
|
+
find?(predicate: (cookie: string) => boolean, url?: string): string | undefined;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Version 1 of the JSON-safe, attribute-preserving stealth cookie store.
|
|
1064
|
+
* The nested jar is tough-cookie's serialized form and retains cookie origin,
|
|
1065
|
+
* Path, Secure, expiry, host-only, and other RFC attributes.
|
|
1066
|
+
*/
|
|
1067
|
+
export interface StealthCookieStoreV1 {
|
|
1068
|
+
readonly version: 1;
|
|
1069
|
+
readonly jar: SerializedCookieJar;
|
|
1058
1070
|
}
|
|
1059
1071
|
|
|
1072
|
+
/** Cookie persistence formats understood by this SDK version. */
|
|
1073
|
+
export type StealthCookieStore = StealthCookieStoreV1;
|
|
1074
|
+
|
|
1060
1075
|
export interface StealthSessionCookies extends CookieJar {
|
|
1061
|
-
has(name: string): boolean;
|
|
1062
|
-
|
|
1063
|
-
|
|
1076
|
+
has(name: string, url?: string): boolean;
|
|
1077
|
+
/** URL-less writes are scoped to the session base URL. */
|
|
1078
|
+
setFromCookieStrings(cookieStrings: readonly string[], url?: string): void;
|
|
1079
|
+
toHeader(url?: string): string;
|
|
1080
|
+
/**
|
|
1081
|
+
* Returns every cookie as a flat name/value map, collapsing duplicate names.
|
|
1082
|
+
* @deprecated Use serialize() for lossless, attribute-preserving persistence.
|
|
1083
|
+
*/
|
|
1064
1084
|
snapshot(): Record<string, string>;
|
|
1085
|
+
/**
|
|
1086
|
+
* Restores flat values as host-only, Path=/ cookies on the session base URL.
|
|
1087
|
+
* @deprecated Use deserialize() with state produced by serialize().
|
|
1088
|
+
*/
|
|
1065
1089
|
restore(cookies: Record<string, string>): void;
|
|
1090
|
+
/** Returns a versioned, JSON-safe, attribute-preserving representation of every cookie. */
|
|
1091
|
+
serialize(): StealthCookieStoreV1;
|
|
1092
|
+
/** Replaces the jar with a previously serialized, attribute-preserving cookie store. */
|
|
1093
|
+
deserialize(state: StealthCookieStore): void;
|
|
1066
1094
|
clear(): void;
|
|
1067
1095
|
}
|
|
1068
1096
|
|
|
@@ -1108,7 +1136,13 @@ export interface StealthRedirectRunResult {
|
|
|
1108
1136
|
final: StealthResponse;
|
|
1109
1137
|
hops: StealthRedirectHop[];
|
|
1110
1138
|
reason: "completed" | "stopped" | "max_hops" | "missing_location" | "loop";
|
|
1139
|
+
/**
|
|
1140
|
+
* Complete flat view across all redirect hosts. Attributes and duplicate names are lost.
|
|
1141
|
+
* @deprecated Use cookieStore for lossless persistence.
|
|
1142
|
+
*/
|
|
1111
1143
|
cookies: Record<string, string>;
|
|
1144
|
+
/** Versioned, attribute-preserving cookie state accumulated across the redirect chain. */
|
|
1145
|
+
cookieStore: StealthCookieStoreV1;
|
|
1112
1146
|
}
|
|
1113
1147
|
|
|
1114
1148
|
export interface StealthSession {
|