@apifuse/provider-sdk 2.1.0-beta.10 → 2.1.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/CHANGELOG.md +8 -0
- package/README.md +10 -6
- package/dist/cli/create.js +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/runtime/stealth.d.ts +5 -2
- package/dist/runtime/stealth.js +149 -3
- package/dist/types.d.ts +33 -0
- package/package.json +1 -1
- package/src/cli/create.ts +3 -3
- package/src/index.ts +5 -0
- package/src/runtime/stealth.ts +182 -5
- package/src/types.ts +38 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @apifuse/provider-sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.0-beta.12
|
|
4
|
+
|
|
5
|
+
- Release candidate for main commit 2bc1061c6a68facaa2efde08bee31bf5cd96945e.
|
|
6
|
+
|
|
7
|
+
## 2.1.0-beta.11
|
|
8
|
+
|
|
9
|
+
- Release candidate for main commit b98ddc5024698f8c79e05e3295f7c8c8e8fe5a8a.
|
|
10
|
+
|
|
3
11
|
## 2.1.0-beta.9
|
|
4
12
|
|
|
5
13
|
- Preserve raw stealth response bytes through the public SDK response wrapper.
|
package/README.md
CHANGED
|
@@ -124,12 +124,16 @@ the bad request path; provider/runtime failures include `code`, `message`, and
|
|
|
124
124
|
`flowId`; preserve any returned `contextPatch` in the next local request's
|
|
125
125
|
`context` object.
|
|
126
126
|
- **Stealth-sensitive providers**: use `ctx.http` for normal JSON/REST calls and
|
|
127
|
-
`ctx.stealth.fetch()` when you need browser-like session or cookie control.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
`
|
|
131
|
-
|
|
132
|
-
|
|
127
|
+
`ctx.stealth.fetch()` when you need browser-like session or cookie control.
|
|
128
|
+
`ctx.stealth.fetch()` uses the impit-backed browser stealth transport and
|
|
129
|
+
accepts request controls for `params`, `proxy`, `timeout`, `profile`,
|
|
130
|
+
`redirect`, `throwOnHttpError`, and `stealth.insecureSkipVerify`. For login
|
|
131
|
+
flows that must inspect intermediate `Location`/`Set-Cookie` headers, create
|
|
132
|
+
a session with `ctx.stealth.createSession()` and use `session.redirects.run()`;
|
|
133
|
+
inspect accumulated cookies through `session.cookies`. Select an SDK stealth
|
|
134
|
+
`profile` such as `chrome-146`; do not tune JA3, HTTP/2 SETTINGS, or
|
|
135
|
+
pseudo-header order in provider code. Chrome/Firefox-style profiles are
|
|
136
|
+
supported; use `ctx.browser` when Safari-specific behavior is required.
|
|
133
137
|
- **Browser providers**: for TypeScript Providers use `runtime: "browser"` plus
|
|
134
138
|
`browser.engine: "playwright-stealth"`; `nodriver` is a Python-runtime path.
|
|
135
139
|
Install local browser assets with `bunx playwright install chromium` when
|
package/dist/cli/create.js
CHANGED
|
@@ -544,8 +544,8 @@ function renderAuthBlock(authMode) {
|
|
|
544
544
|
return `{
|
|
545
545
|
mode: "credentials",
|
|
546
546
|
flow: {
|
|
547
|
-
start: async () => ({
|
|
548
|
-
kind: "
|
|
547
|
+
start: async (_ctx) => ({
|
|
548
|
+
kind: "form",
|
|
549
549
|
turnId: crypto.randomUUID(),
|
|
550
550
|
expectedInput: {
|
|
551
551
|
schema: {
|
|
@@ -588,7 +588,7 @@ function renderAuthBlock(authMode) {
|
|
|
588
588
|
return `{
|
|
589
589
|
mode: "oauth2",
|
|
590
590
|
flow: {
|
|
591
|
-
start: async () => ({
|
|
591
|
+
start: async (_ctx) => ({
|
|
592
592
|
kind: "redirect",
|
|
593
593
|
turnId: crypto.randomUUID(),
|
|
594
594
|
data: {
|
package/dist/index.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SEN
|
|
|
33
33
|
export { createServerApp, type ServeOptions, serve } from "./server";
|
|
34
34
|
export { getStealthProfile, listStealthProfiles } from "./stealth/profiles";
|
|
35
35
|
export * from "./stream";
|
|
36
|
-
export type { ApiFuseResponse, AuthConfig, AuthContext, AuthFlowDefinition, AuthFlowInputHandler, AuthFlowStartHandler, AuthMode, AuthTurn, Bcp47Locale, BrowserEngine, BrowserOptions, ConnectionMode, ContextDeclaration, ContextScratchpad, CredentialContext, CredentialDeclaration, E164PhoneNumber, EnvContext, FlowContext, FlowContextStore, HealthCheckAssertionContext, HealthCheckCase, HealthCheckCaseResult, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyGatewayContext, HealthJourneyJournalContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthJourneySchedule, 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, ProviderHealthMonitorConfig, 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, StealthResponse, StealthSession, SttAudioInput, SttContext, SttPromptPolicy, SttSegment, SttTranscribeMode, SttTranscribeRequest, SttTranscript, SttUnsupportedOptionPolicy, SttUsage, SttVerificationCodeOptions, SttWarning, TraceConfig, TraceSpan, VerificationCodeCandidate, VerificationCodeCandidateSource, VerificationCodeExtractionResult, } from "./types";
|
|
36
|
+
export type { ApiFuseResponse, AuthConfig, AuthContext, AuthFlowDefinition, AuthFlowInputHandler, AuthFlowStartHandler, AuthMode, AuthTurn, Bcp47Locale, BrowserEngine, BrowserOptions, ConnectionMode, ContextDeclaration, CookieJar, ContextScratchpad, CredentialContext, CredentialDeclaration, E164PhoneNumber, EnvContext, FlowContext, FlowContextStore, HealthCheckAssertionContext, HealthCheckCase, HealthCheckCaseResult, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyGatewayContext, HealthJourneyJournalContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthJourneySchedule, 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, ProviderHealthMonitorConfig, 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";
|
|
37
37
|
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";
|
|
38
38
|
export * from "./utils/date";
|
|
39
39
|
export * from "./utils/parse";
|
|
@@ -12,7 +12,10 @@ export type StealthClientOptions = ProxyResolutionOptions & {
|
|
|
12
12
|
insecureSkipVerify?: boolean;
|
|
13
13
|
};
|
|
14
14
|
};
|
|
15
|
-
type StealthTransportResponse = Pick<ImpitResponse, "arrayBuffer" | "headers" | "json" | "ok" | "status" | "text"
|
|
16
|
-
|
|
15
|
+
type StealthTransportResponse = Pick<ImpitResponse, "arrayBuffer" | "headers" | "json" | "ok" | "status" | "text"> & {
|
|
16
|
+
url?: string;
|
|
17
|
+
redirected?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare function normalizeResponse(response: StealthTransportResponse, requestUrl?: string): Promise<StealthResponse>;
|
|
17
20
|
export declare function createStealthClient(baseUrl: string, defaultProfileOrOptions?: string | StealthClientOptions, clientOptions?: StealthClientOptions): StealthClient;
|
|
18
21
|
export {};
|
package/dist/runtime/stealth.js
CHANGED
|
@@ -102,11 +102,32 @@ class CookieJarImpl {
|
|
|
102
102
|
getAll() {
|
|
103
103
|
return { ...this.cookies };
|
|
104
104
|
}
|
|
105
|
+
has(name) {
|
|
106
|
+
return Object.hasOwn(this.cookies, name);
|
|
107
|
+
}
|
|
105
108
|
toString() {
|
|
106
109
|
return Object.entries(this.cookies)
|
|
107
110
|
.map(([name, value]) => `${name}=${value}`)
|
|
108
111
|
.join("; ");
|
|
109
112
|
}
|
|
113
|
+
toHeader() {
|
|
114
|
+
return this.toString();
|
|
115
|
+
}
|
|
116
|
+
snapshot() {
|
|
117
|
+
return this.getAll();
|
|
118
|
+
}
|
|
119
|
+
restore(cookies) {
|
|
120
|
+
this.clear();
|
|
121
|
+
for (const [name, value] of Object.entries(cookies)) {
|
|
122
|
+
if (name)
|
|
123
|
+
this.cookies[name] = value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
clear() {
|
|
127
|
+
for (const name of Object.keys(this.cookies)) {
|
|
128
|
+
delete this.cookies[name];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
110
131
|
find(predicate) {
|
|
111
132
|
for (const [name, value] of Object.entries(this.cookies)) {
|
|
112
133
|
const cookie = `${name}=${value}`;
|
|
@@ -237,7 +258,7 @@ function splitCombinedSetCookieHeader(headerValue) {
|
|
|
237
258
|
cookieStrings.push(finalCookie);
|
|
238
259
|
return cookieStrings;
|
|
239
260
|
}
|
|
240
|
-
export async function normalizeResponse(response) {
|
|
261
|
+
export async function normalizeResponse(response, requestUrl) {
|
|
241
262
|
const headers = Object.fromEntries(response.headers.entries());
|
|
242
263
|
const cookies = new CookieJarImpl(setCookieHeadersFromResponse(response.headers));
|
|
243
264
|
const bodyBytes = await response.arrayBuffer();
|
|
@@ -245,6 +266,12 @@ export async function normalizeResponse(response) {
|
|
|
245
266
|
return {
|
|
246
267
|
status: response.status,
|
|
247
268
|
ok: response.status >= 200 && response.status < 300,
|
|
269
|
+
...(response.url ? { url: response.url } : {}),
|
|
270
|
+
...(response.redirected !== undefined
|
|
271
|
+
? { redirected: response.redirected }
|
|
272
|
+
: requestUrl && response.url
|
|
273
|
+
? { redirected: response.url !== requestUrl }
|
|
274
|
+
: {}),
|
|
248
275
|
headers,
|
|
249
276
|
rawHeaders: headerEntriesFromHeaders(response.headers),
|
|
250
277
|
body,
|
|
@@ -537,6 +564,23 @@ function normalizeMethod(method) {
|
|
|
537
564
|
throw new SDKError(`Unsupported stealth method: ${method}`);
|
|
538
565
|
}
|
|
539
566
|
}
|
|
567
|
+
function isRedirectStatus(status) {
|
|
568
|
+
return [301, 302, 303, 307, 308].includes(status);
|
|
569
|
+
}
|
|
570
|
+
function nextRedirectMethod(status, method) {
|
|
571
|
+
if (status === 303 && method !== "HEAD")
|
|
572
|
+
return "GET";
|
|
573
|
+
if ((status === 301 || status === 302) && method === "POST")
|
|
574
|
+
return "GET";
|
|
575
|
+
return method;
|
|
576
|
+
}
|
|
577
|
+
function locationHeader(headers) {
|
|
578
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
579
|
+
if (name.toLowerCase() === "location")
|
|
580
|
+
return value;
|
|
581
|
+
}
|
|
582
|
+
return undefined;
|
|
583
|
+
}
|
|
540
584
|
function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
541
585
|
const clients = new Map();
|
|
542
586
|
let closed = false;
|
|
@@ -588,7 +632,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
588
632
|
proxyHash: proxyEndpointHash(resolvedProxy.url),
|
|
589
633
|
};
|
|
590
634
|
}
|
|
591
|
-
|
|
635
|
+
const session = {
|
|
592
636
|
async fetch(url, options = {}) {
|
|
593
637
|
const method = normalizeMethod(options.method ?? "GET");
|
|
594
638
|
const hasExplicitRetryPolicy = options.retry !== undefined;
|
|
@@ -656,13 +700,14 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
656
700
|
const requestInit = {
|
|
657
701
|
headers: normalizeHeaders(headers),
|
|
658
702
|
method,
|
|
703
|
+
...(options.redirect ? { redirect: options.redirect } : {}),
|
|
659
704
|
...(options.timeout ? { timeout: options.timeout } : {}),
|
|
660
705
|
};
|
|
661
706
|
if (options.body !== undefined) {
|
|
662
707
|
requestInit.body = normalizeBody(options.body);
|
|
663
708
|
}
|
|
664
709
|
const response = await getClient(profileName, proxy, ignoreTlsErrors).fetch(requestUrl, requestInit);
|
|
665
|
-
const normalized = await normalizeResponse(response);
|
|
710
|
+
const normalized = await normalizeResponse(response, requestUrl);
|
|
666
711
|
cookieJar.setFromCookieStrings(setCookieHeadersFromResponse(response.headers));
|
|
667
712
|
if (proxy &&
|
|
668
713
|
isProxyConnectFailureResponse(response, normalized.body)) {
|
|
@@ -758,11 +803,112 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
758
803
|
}
|
|
759
804
|
throw normalizeStealthTransportError(lastError);
|
|
760
805
|
},
|
|
806
|
+
cookies: cookieJar,
|
|
807
|
+
redirects: {
|
|
808
|
+
async run(options) {
|
|
809
|
+
const maxHops = options.maxHops === undefined || !Number.isFinite(options.maxHops)
|
|
810
|
+
? 10
|
|
811
|
+
: Math.max(0, Math.floor(options.maxHops));
|
|
812
|
+
const hops = [];
|
|
813
|
+
let currentUrl = resolveUrl(baseUrl, options.url);
|
|
814
|
+
let method = normalizeMethod(options.method ?? "GET");
|
|
815
|
+
let body = options.body;
|
|
816
|
+
let response;
|
|
817
|
+
const visitedRequests = new Set();
|
|
818
|
+
const { url: _url, maxHops: _maxHops, stopWhen, params, ...fetchOptions } = options;
|
|
819
|
+
for (let hopIndex = 0; hopIndex <= maxHops; hopIndex += 1) {
|
|
820
|
+
visitedRequests.add(`${method} ${currentUrl}`);
|
|
821
|
+
response = await session.fetch(currentUrl, {
|
|
822
|
+
...fetchOptions,
|
|
823
|
+
body,
|
|
824
|
+
method,
|
|
825
|
+
...(hopIndex === 0 && params ? { params } : {}),
|
|
826
|
+
redirect: "manual",
|
|
827
|
+
throwOnHttpError: false,
|
|
828
|
+
});
|
|
829
|
+
if (!isRedirectStatus(response.status)) {
|
|
830
|
+
return {
|
|
831
|
+
final: response,
|
|
832
|
+
hops,
|
|
833
|
+
reason: "completed",
|
|
834
|
+
cookies: cookieJar.snapshot(),
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
const location = locationHeader(response.headers);
|
|
838
|
+
const nextUrl = location
|
|
839
|
+
? new URL(location, response.url ?? currentUrl).toString()
|
|
840
|
+
: undefined;
|
|
841
|
+
const hop = {
|
|
842
|
+
url: response.url ?? currentUrl,
|
|
843
|
+
status: response.status,
|
|
844
|
+
method,
|
|
845
|
+
...(location ? { location } : {}),
|
|
846
|
+
...(nextUrl ? { nextUrl } : {}),
|
|
847
|
+
};
|
|
848
|
+
hops.push(hop);
|
|
849
|
+
if (stopWhen && (await stopWhen(hop))) {
|
|
850
|
+
return {
|
|
851
|
+
final: response,
|
|
852
|
+
hops,
|
|
853
|
+
reason: "stopped",
|
|
854
|
+
cookies: cookieJar.snapshot(),
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
if (!nextUrl) {
|
|
858
|
+
return {
|
|
859
|
+
final: response,
|
|
860
|
+
hops,
|
|
861
|
+
reason: "missing_location",
|
|
862
|
+
cookies: cookieJar.snapshot(),
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
if (hops.length > maxHops) {
|
|
866
|
+
return {
|
|
867
|
+
final: response,
|
|
868
|
+
hops,
|
|
869
|
+
reason: "max_hops",
|
|
870
|
+
cookies: cookieJar.snapshot(),
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
const nextMethod = nextRedirectMethod(response.status, method);
|
|
874
|
+
if (nextMethod !== method) {
|
|
875
|
+
body = undefined;
|
|
876
|
+
}
|
|
877
|
+
if (visitedRequests.has(`${nextMethod} ${nextUrl}`)) {
|
|
878
|
+
return {
|
|
879
|
+
final: response,
|
|
880
|
+
hops,
|
|
881
|
+
reason: "loop",
|
|
882
|
+
cookies: cookieJar.snapshot(),
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
method = nextMethod;
|
|
886
|
+
currentUrl = nextUrl;
|
|
887
|
+
}
|
|
888
|
+
if (!response) {
|
|
889
|
+
response = await session.fetch(currentUrl, {
|
|
890
|
+
...fetchOptions,
|
|
891
|
+
body,
|
|
892
|
+
method,
|
|
893
|
+
...(params ? { params } : {}),
|
|
894
|
+
redirect: "manual",
|
|
895
|
+
throwOnHttpError: false,
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
return {
|
|
899
|
+
final: response,
|
|
900
|
+
hops,
|
|
901
|
+
reason: "max_hops",
|
|
902
|
+
cookies: cookieJar.snapshot(),
|
|
903
|
+
};
|
|
904
|
+
},
|
|
905
|
+
},
|
|
761
906
|
close() {
|
|
762
907
|
closed = true;
|
|
763
908
|
clients.clear();
|
|
764
909
|
},
|
|
765
910
|
};
|
|
911
|
+
return session;
|
|
766
912
|
async function classifyProxyAuthDiagnostic(profileName, proxy) {
|
|
767
913
|
try {
|
|
768
914
|
const response = await getClient(profileName, proxy, false).fetch(PROXY_AUTH_DIAGNOSTIC_URL, {
|
package/dist/types.d.ts
CHANGED
|
@@ -805,6 +805,7 @@ export type HttpMethod = "HEAD" | "head" | "GET" | "get" | "POST" | "post" | "PU
|
|
|
805
805
|
export interface StealthFetchOptions extends RequestOptions {
|
|
806
806
|
method?: HttpMethod;
|
|
807
807
|
body?: string | Buffer;
|
|
808
|
+
redirect?: "follow" | "manual" | "error";
|
|
808
809
|
/**
|
|
809
810
|
* Offsets policy-managed proxy pool selection for caller-managed retries.
|
|
810
811
|
* Use when a request receives an upstream challenge page rather than a
|
|
@@ -829,9 +830,19 @@ export interface CookieJar {
|
|
|
829
830
|
toString(): string;
|
|
830
831
|
find?(predicate: (cookie: string) => boolean): string | undefined;
|
|
831
832
|
}
|
|
833
|
+
export interface StealthSessionCookies extends CookieJar {
|
|
834
|
+
has(name: string): boolean;
|
|
835
|
+
setFromCookieStrings(cookieStrings: readonly string[]): void;
|
|
836
|
+
toHeader(): string;
|
|
837
|
+
snapshot(): Record<string, string>;
|
|
838
|
+
restore(cookies: Record<string, string>): void;
|
|
839
|
+
clear(): void;
|
|
840
|
+
}
|
|
832
841
|
export interface DeclarativeStealthResponse {
|
|
833
842
|
status: number;
|
|
834
843
|
ok: boolean;
|
|
844
|
+
url?: string;
|
|
845
|
+
redirected?: boolean;
|
|
835
846
|
headers: Record<string, string>;
|
|
836
847
|
rawHeaders: [string, string][];
|
|
837
848
|
body: string;
|
|
@@ -851,8 +862,30 @@ export type RequestWithMethodOptions = RequestOptions & {
|
|
|
851
862
|
method?: string;
|
|
852
863
|
body?: unknown;
|
|
853
864
|
};
|
|
865
|
+
export interface StealthRedirectHop {
|
|
866
|
+
url: string;
|
|
867
|
+
status: number;
|
|
868
|
+
method: string;
|
|
869
|
+
location?: string;
|
|
870
|
+
nextUrl?: string;
|
|
871
|
+
}
|
|
872
|
+
export interface StealthRedirectRunOptions extends Omit<StealthFetchOptions, "redirect"> {
|
|
873
|
+
url: string;
|
|
874
|
+
maxHops?: number;
|
|
875
|
+
stopWhen?: (hop: StealthRedirectHop) => boolean | Promise<boolean>;
|
|
876
|
+
}
|
|
877
|
+
export interface StealthRedirectRunResult {
|
|
878
|
+
final: StealthResponse;
|
|
879
|
+
hops: StealthRedirectHop[];
|
|
880
|
+
reason: "completed" | "stopped" | "max_hops" | "missing_location" | "loop";
|
|
881
|
+
cookies: Record<string, string>;
|
|
882
|
+
}
|
|
854
883
|
export interface StealthSession {
|
|
855
884
|
fetch(url: string, options?: StealthFetchOptions): Promise<StealthResponse>;
|
|
885
|
+
cookies: StealthSessionCookies;
|
|
886
|
+
redirects: {
|
|
887
|
+
run(options: StealthRedirectRunOptions): Promise<StealthRedirectRunResult>;
|
|
888
|
+
};
|
|
856
889
|
close(): void;
|
|
857
890
|
}
|
|
858
891
|
export interface ApiFuseResponse<T> {
|
package/package.json
CHANGED
package/src/cli/create.ts
CHANGED
|
@@ -734,8 +734,8 @@ function renderAuthBlock(authMode: CreateAuthMode): string {
|
|
|
734
734
|
return `{
|
|
735
735
|
mode: "credentials",
|
|
736
736
|
flow: {
|
|
737
|
-
start: async () => ({
|
|
738
|
-
kind: "
|
|
737
|
+
start: async (_ctx) => ({
|
|
738
|
+
kind: "form",
|
|
739
739
|
turnId: crypto.randomUUID(),
|
|
740
740
|
expectedInput: {
|
|
741
741
|
schema: {
|
|
@@ -778,7 +778,7 @@ function renderAuthBlock(authMode: CreateAuthMode): string {
|
|
|
778
778
|
return `{
|
|
779
779
|
mode: "oauth2",
|
|
780
780
|
flow: {
|
|
781
|
-
start: async () => ({
|
|
781
|
+
start: async (_ctx) => ({
|
|
782
782
|
kind: "redirect",
|
|
783
783
|
turnId: crypto.randomUUID(),
|
|
784
784
|
data: {
|
package/src/index.ts
CHANGED
|
@@ -124,6 +124,7 @@ export type {
|
|
|
124
124
|
BrowserOptions,
|
|
125
125
|
ConnectionMode,
|
|
126
126
|
ContextDeclaration,
|
|
127
|
+
CookieJar,
|
|
127
128
|
ContextScratchpad,
|
|
128
129
|
CredentialContext,
|
|
129
130
|
CredentialDeclaration,
|
|
@@ -229,8 +230,12 @@ export type {
|
|
|
229
230
|
StealthFetchOptions,
|
|
230
231
|
StealthPlatform,
|
|
231
232
|
StealthProfile,
|
|
233
|
+
StealthRedirectHop,
|
|
234
|
+
StealthRedirectRunOptions,
|
|
235
|
+
StealthRedirectRunResult,
|
|
232
236
|
StealthResponse,
|
|
233
237
|
StealthSession,
|
|
238
|
+
StealthSessionCookies,
|
|
234
239
|
SttAudioInput,
|
|
235
240
|
SttContext,
|
|
236
241
|
SttPromptPolicy,
|
package/src/runtime/stealth.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
HttpRetryOptions,
|
|
19
19
|
StealthClient,
|
|
20
20
|
StealthFetchOptions,
|
|
21
|
+
StealthRedirectHop,
|
|
21
22
|
StealthResponse,
|
|
22
23
|
StealthSession,
|
|
23
24
|
} from "../types";
|
|
@@ -128,10 +129,16 @@ const FIREFOX_IMPIT_BY_MAJOR: Record<number, ImpitBrowser> = {
|
|
|
128
129
|
|
|
129
130
|
type StealthTransportResponse = Pick<
|
|
130
131
|
ImpitResponse,
|
|
131
|
-
"arrayBuffer" | "headers" | "json" | "ok" | "status" | "text"
|
|
132
|
-
|
|
132
|
+
"arrayBuffer" | "headers" | "json" | "ok" | "status" | "text"
|
|
133
|
+
> & {
|
|
134
|
+
url?: string;
|
|
135
|
+
redirected?: boolean;
|
|
136
|
+
};
|
|
133
137
|
|
|
134
138
|
type StealthMethod = NonNullable<ImpitRequestInit["method"]>;
|
|
139
|
+
type StealthRequestInit = ImpitRequestInit & {
|
|
140
|
+
redirect?: NonNullable<StealthFetchOptions["redirect"]>;
|
|
141
|
+
};
|
|
135
142
|
type NormalizedStealthRetryOptions = {
|
|
136
143
|
attempts: number;
|
|
137
144
|
methods: readonly string[];
|
|
@@ -177,12 +184,37 @@ class CookieJarImpl implements CookieJar {
|
|
|
177
184
|
return { ...this.cookies };
|
|
178
185
|
}
|
|
179
186
|
|
|
187
|
+
has(name: string): boolean {
|
|
188
|
+
return Object.hasOwn(this.cookies, name);
|
|
189
|
+
}
|
|
190
|
+
|
|
180
191
|
toString(): string {
|
|
181
192
|
return Object.entries(this.cookies)
|
|
182
193
|
.map(([name, value]) => `${name}=${value}`)
|
|
183
194
|
.join("; ");
|
|
184
195
|
}
|
|
185
196
|
|
|
197
|
+
toHeader(): string {
|
|
198
|
+
return this.toString();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
snapshot(): Record<string, string> {
|
|
202
|
+
return this.getAll();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
restore(cookies: Record<string, string>): void {
|
|
206
|
+
this.clear();
|
|
207
|
+
for (const [name, value] of Object.entries(cookies)) {
|
|
208
|
+
if (name) this.cookies[name] = value;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
clear(): void {
|
|
213
|
+
for (const name of Object.keys(this.cookies)) {
|
|
214
|
+
delete this.cookies[name];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
186
218
|
find(predicate: (cookie: string) => boolean): string | undefined {
|
|
187
219
|
for (const [name, value] of Object.entries(this.cookies)) {
|
|
188
220
|
const cookie = `${name}=${value}`;
|
|
@@ -335,6 +367,7 @@ function splitCombinedSetCookieHeader(headerValue: string): string[] {
|
|
|
335
367
|
|
|
336
368
|
export async function normalizeResponse(
|
|
337
369
|
response: StealthTransportResponse,
|
|
370
|
+
requestUrl?: string,
|
|
338
371
|
): Promise<StealthResponse> {
|
|
339
372
|
const headers = Object.fromEntries(response.headers.entries());
|
|
340
373
|
const cookies = new CookieJarImpl(
|
|
@@ -346,6 +379,12 @@ export async function normalizeResponse(
|
|
|
346
379
|
return {
|
|
347
380
|
status: response.status,
|
|
348
381
|
ok: response.status >= 200 && response.status < 300,
|
|
382
|
+
...(response.url ? { url: response.url } : {}),
|
|
383
|
+
...(response.redirected !== undefined
|
|
384
|
+
? { redirected: response.redirected }
|
|
385
|
+
: requestUrl && response.url
|
|
386
|
+
? { redirected: response.url !== requestUrl }
|
|
387
|
+
: {}),
|
|
349
388
|
headers,
|
|
350
389
|
rawHeaders: headerEntriesFromHeaders(response.headers),
|
|
351
390
|
body,
|
|
@@ -743,6 +782,26 @@ function normalizeMethod(method: HttpMethod | string): StealthMethod {
|
|
|
743
782
|
}
|
|
744
783
|
}
|
|
745
784
|
|
|
785
|
+
function isRedirectStatus(status: number): boolean {
|
|
786
|
+
return [301, 302, 303, 307, 308].includes(status);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function nextRedirectMethod(
|
|
790
|
+
status: number,
|
|
791
|
+
method: StealthMethod,
|
|
792
|
+
): StealthMethod {
|
|
793
|
+
if (status === 303 && method !== "HEAD") return "GET";
|
|
794
|
+
if ((status === 301 || status === 302) && method === "POST") return "GET";
|
|
795
|
+
return method;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function locationHeader(headers: Record<string, string>): string | undefined {
|
|
799
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
800
|
+
if (name.toLowerCase() === "location") return value;
|
|
801
|
+
}
|
|
802
|
+
return undefined;
|
|
803
|
+
}
|
|
804
|
+
|
|
746
805
|
function createSessionFetcher(
|
|
747
806
|
baseUrl: string,
|
|
748
807
|
defaultProfile: string,
|
|
@@ -811,7 +870,7 @@ function createSessionFetcher(
|
|
|
811
870
|
};
|
|
812
871
|
}
|
|
813
872
|
|
|
814
|
-
|
|
873
|
+
const session: StealthSession = {
|
|
815
874
|
async fetch(url, options: StealthFetchOptions = {}) {
|
|
816
875
|
const method = normalizeMethod(options.method ?? "GET");
|
|
817
876
|
const hasExplicitRetryPolicy = options.retry !== undefined;
|
|
@@ -896,9 +955,10 @@ function createSessionFetcher(
|
|
|
896
955
|
const cookieHeader = cookieJar.toString();
|
|
897
956
|
if (cookieHeader) headers.Cookie = cookieHeader;
|
|
898
957
|
}
|
|
899
|
-
const requestInit:
|
|
958
|
+
const requestInit: StealthRequestInit = {
|
|
900
959
|
headers: normalizeHeaders(headers),
|
|
901
960
|
method,
|
|
961
|
+
...(options.redirect ? { redirect: options.redirect } : {}),
|
|
902
962
|
...(options.timeout ? { timeout: options.timeout } : {}),
|
|
903
963
|
};
|
|
904
964
|
if (options.body !== undefined) {
|
|
@@ -909,7 +969,7 @@ function createSessionFetcher(
|
|
|
909
969
|
proxy,
|
|
910
970
|
ignoreTlsErrors,
|
|
911
971
|
).fetch(requestUrl, requestInit);
|
|
912
|
-
const normalized = await normalizeResponse(response);
|
|
972
|
+
const normalized = await normalizeResponse(response, requestUrl);
|
|
913
973
|
cookieJar.setFromCookieStrings(
|
|
914
974
|
setCookieHeadersFromResponse(response.headers),
|
|
915
975
|
);
|
|
@@ -1052,11 +1112,128 @@ function createSessionFetcher(
|
|
|
1052
1112
|
|
|
1053
1113
|
throw normalizeStealthTransportError(lastError);
|
|
1054
1114
|
},
|
|
1115
|
+
cookies: cookieJar,
|
|
1116
|
+
redirects: {
|
|
1117
|
+
async run(options) {
|
|
1118
|
+
const maxHops =
|
|
1119
|
+
options.maxHops === undefined || !Number.isFinite(options.maxHops)
|
|
1120
|
+
? 10
|
|
1121
|
+
: Math.max(0, Math.floor(options.maxHops));
|
|
1122
|
+
const hops: StealthRedirectHop[] = [];
|
|
1123
|
+
let currentUrl = resolveUrl(baseUrl, options.url);
|
|
1124
|
+
let method = normalizeMethod(options.method ?? "GET");
|
|
1125
|
+
let body = options.body;
|
|
1126
|
+
let response: StealthResponse | undefined;
|
|
1127
|
+
const visitedRequests = new Set<string>();
|
|
1128
|
+
|
|
1129
|
+
const {
|
|
1130
|
+
url: _url,
|
|
1131
|
+
maxHops: _maxHops,
|
|
1132
|
+
stopWhen,
|
|
1133
|
+
params,
|
|
1134
|
+
...fetchOptions
|
|
1135
|
+
} = options;
|
|
1136
|
+
|
|
1137
|
+
for (let hopIndex = 0; hopIndex <= maxHops; hopIndex += 1) {
|
|
1138
|
+
visitedRequests.add(`${method} ${currentUrl}`);
|
|
1139
|
+
response = await session.fetch(currentUrl, {
|
|
1140
|
+
...fetchOptions,
|
|
1141
|
+
body,
|
|
1142
|
+
method,
|
|
1143
|
+
...(hopIndex === 0 && params ? { params } : {}),
|
|
1144
|
+
redirect: "manual",
|
|
1145
|
+
throwOnHttpError: false,
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
if (!isRedirectStatus(response.status)) {
|
|
1149
|
+
return {
|
|
1150
|
+
final: response,
|
|
1151
|
+
hops,
|
|
1152
|
+
reason: "completed",
|
|
1153
|
+
cookies: cookieJar.snapshot(),
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const location = locationHeader(response.headers);
|
|
1158
|
+
const nextUrl = location
|
|
1159
|
+
? new URL(location, response.url ?? currentUrl).toString()
|
|
1160
|
+
: undefined;
|
|
1161
|
+
const hop: StealthRedirectHop = {
|
|
1162
|
+
url: response.url ?? currentUrl,
|
|
1163
|
+
status: response.status,
|
|
1164
|
+
method,
|
|
1165
|
+
...(location ? { location } : {}),
|
|
1166
|
+
...(nextUrl ? { nextUrl } : {}),
|
|
1167
|
+
};
|
|
1168
|
+
hops.push(hop);
|
|
1169
|
+
|
|
1170
|
+
if (stopWhen && (await stopWhen(hop))) {
|
|
1171
|
+
return {
|
|
1172
|
+
final: response,
|
|
1173
|
+
hops,
|
|
1174
|
+
reason: "stopped",
|
|
1175
|
+
cookies: cookieJar.snapshot(),
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (!nextUrl) {
|
|
1180
|
+
return {
|
|
1181
|
+
final: response,
|
|
1182
|
+
hops,
|
|
1183
|
+
reason: "missing_location",
|
|
1184
|
+
cookies: cookieJar.snapshot(),
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
if (hops.length > maxHops) {
|
|
1189
|
+
return {
|
|
1190
|
+
final: response,
|
|
1191
|
+
hops,
|
|
1192
|
+
reason: "max_hops",
|
|
1193
|
+
cookies: cookieJar.snapshot(),
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const nextMethod = nextRedirectMethod(response.status, method);
|
|
1198
|
+
if (nextMethod !== method) {
|
|
1199
|
+
body = undefined;
|
|
1200
|
+
}
|
|
1201
|
+
if (visitedRequests.has(`${nextMethod} ${nextUrl}`)) {
|
|
1202
|
+
return {
|
|
1203
|
+
final: response,
|
|
1204
|
+
hops,
|
|
1205
|
+
reason: "loop",
|
|
1206
|
+
cookies: cookieJar.snapshot(),
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
method = nextMethod;
|
|
1210
|
+
currentUrl = nextUrl;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
if (!response) {
|
|
1214
|
+
response = await session.fetch(currentUrl, {
|
|
1215
|
+
...fetchOptions,
|
|
1216
|
+
body,
|
|
1217
|
+
method,
|
|
1218
|
+
...(params ? { params } : {}),
|
|
1219
|
+
redirect: "manual",
|
|
1220
|
+
throwOnHttpError: false,
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
return {
|
|
1224
|
+
final: response,
|
|
1225
|
+
hops,
|
|
1226
|
+
reason: "max_hops",
|
|
1227
|
+
cookies: cookieJar.snapshot(),
|
|
1228
|
+
};
|
|
1229
|
+
},
|
|
1230
|
+
},
|
|
1055
1231
|
close() {
|
|
1056
1232
|
closed = true;
|
|
1057
1233
|
clients.clear();
|
|
1058
1234
|
},
|
|
1059
1235
|
};
|
|
1236
|
+
return session;
|
|
1060
1237
|
|
|
1061
1238
|
async function classifyProxyAuthDiagnostic(
|
|
1062
1239
|
profileName: string,
|
package/src/types.ts
CHANGED
|
@@ -979,6 +979,7 @@ export type HttpMethod =
|
|
|
979
979
|
export interface StealthFetchOptions extends RequestOptions {
|
|
980
980
|
method?: HttpMethod;
|
|
981
981
|
body?: string | Buffer;
|
|
982
|
+
redirect?: "follow" | "manual" | "error";
|
|
982
983
|
/**
|
|
983
984
|
* Offsets policy-managed proxy pool selection for caller-managed retries.
|
|
984
985
|
* Use when a request receives an upstream challenge page rather than a
|
|
@@ -1005,9 +1006,20 @@ export interface CookieJar {
|
|
|
1005
1006
|
find?(predicate: (cookie: string) => boolean): string | undefined;
|
|
1006
1007
|
}
|
|
1007
1008
|
|
|
1009
|
+
export interface StealthSessionCookies extends CookieJar {
|
|
1010
|
+
has(name: string): boolean;
|
|
1011
|
+
setFromCookieStrings(cookieStrings: readonly string[]): void;
|
|
1012
|
+
toHeader(): string;
|
|
1013
|
+
snapshot(): Record<string, string>;
|
|
1014
|
+
restore(cookies: Record<string, string>): void;
|
|
1015
|
+
clear(): void;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1008
1018
|
export interface DeclarativeStealthResponse {
|
|
1009
1019
|
status: number;
|
|
1010
1020
|
ok: boolean;
|
|
1021
|
+
url?: string;
|
|
1022
|
+
redirected?: boolean;
|
|
1011
1023
|
headers: Record<string, string>;
|
|
1012
1024
|
rawHeaders: [string, string][];
|
|
1013
1025
|
body: string;
|
|
@@ -1026,8 +1038,34 @@ export type RequestWithMethodOptions = RequestOptions & {
|
|
|
1026
1038
|
body?: unknown;
|
|
1027
1039
|
};
|
|
1028
1040
|
|
|
1041
|
+
export interface StealthRedirectHop {
|
|
1042
|
+
url: string;
|
|
1043
|
+
status: number;
|
|
1044
|
+
method: string;
|
|
1045
|
+
location?: string;
|
|
1046
|
+
nextUrl?: string;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
export interface StealthRedirectRunOptions
|
|
1050
|
+
extends Omit<StealthFetchOptions, "redirect"> {
|
|
1051
|
+
url: string;
|
|
1052
|
+
maxHops?: number;
|
|
1053
|
+
stopWhen?: (hop: StealthRedirectHop) => boolean | Promise<boolean>;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
export interface StealthRedirectRunResult {
|
|
1057
|
+
final: StealthResponse;
|
|
1058
|
+
hops: StealthRedirectHop[];
|
|
1059
|
+
reason: "completed" | "stopped" | "max_hops" | "missing_location" | "loop";
|
|
1060
|
+
cookies: Record<string, string>;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1029
1063
|
export interface StealthSession {
|
|
1030
1064
|
fetch(url: string, options?: StealthFetchOptions): Promise<StealthResponse>;
|
|
1065
|
+
cookies: StealthSessionCookies;
|
|
1066
|
+
redirects: {
|
|
1067
|
+
run(options: StealthRedirectRunOptions): Promise<StealthRedirectRunResult>;
|
|
1068
|
+
};
|
|
1031
1069
|
close(): void;
|
|
1032
1070
|
}
|
|
1033
1071
|
|