@apifuse/provider-sdk 2.2.0-beta.32 → 2.2.0-beta.35
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 +14 -0
- package/dist/error-resolution.js +0 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +1 -1
- package/dist/runtime/auth-flow.d.ts +3 -1
- package/dist/runtime/auth-flow.js +1 -0
- package/dist/runtime/browser.d.ts +1 -0
- package/dist/runtime/browser.js +350 -27
- package/dist/runtime/choice.d.ts +0 -1
- package/dist/runtime/choice.js +10 -126
- package/dist/runtime/resolver-vendors/browser.d.ts +2 -0
- package/dist/runtime/resolver-vendors/browser.js +68 -16
- package/dist/runtime/resolver-vendors/capsolver.d.ts +1 -3
- package/dist/runtime/resolver-vendors/capsolver.js +148 -24
- package/dist/runtime/resolver-vendors/twocaptcha.js +57 -18
- package/dist/runtime/resolver-vendors/types.d.ts +5 -2
- package/dist/runtime/resolver-vendors/types.js +16 -4
- package/dist/runtime/resolver.d.ts +1 -1
- package/dist/runtime/resolver.js +24 -5
- package/dist/server/serve-implementation.d.ts +2 -1
- package/dist/server/serve-implementation.js +27 -17
- package/dist/testing/run.js +1 -0
- package/dist/types.d.ts +25 -3
- package/package.json +3 -2
- package/src/error-resolution.ts +0 -1
- package/src/index.ts +0 -1
- package/src/provider.ts +0 -1
- package/src/runtime/auth-flow.ts +4 -0
- package/src/runtime/browser.ts +438 -31
- package/src/runtime/choice.ts +10 -151
- package/src/runtime/resolver-vendors/browser.ts +83 -16
- package/src/runtime/resolver-vendors/capsolver.ts +170 -33
- package/src/runtime/resolver-vendors/twocaptcha.ts +54 -15
- package/src/runtime/resolver-vendors/types.ts +22 -4
- package/src/runtime/resolver.ts +31 -7
- package/src/server/serve-implementation.ts +74 -17
- package/src/testing/run.ts +1 -0
- package/src/types.ts +26 -3
package/src/runtime/choice.ts
CHANGED
|
@@ -39,8 +39,6 @@ import type {
|
|
|
39
39
|
|
|
40
40
|
export const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV =
|
|
41
41
|
"APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
|
|
42
|
-
export const PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV =
|
|
43
|
-
"APIFUSE__PROVIDER_RUNTIME__CHOICE_WORD_ISSUANCE";
|
|
44
42
|
|
|
45
43
|
const PRIMARY_CHOICE_TOKEN_KID = "v1";
|
|
46
44
|
const MANAGED_CHOICE_TOKEN_VERSION = 1;
|
|
@@ -133,7 +131,6 @@ export function createProviderChoiceContext(
|
|
|
133
131
|
const issuedAtMs = issueOptions.nowMs ?? Date.now();
|
|
134
132
|
const resolvedStorage = resolveIssueStorage(issueOptions.storage, issueOptions.payload);
|
|
135
133
|
if (resolvedStorage.mode === "server") {
|
|
136
|
-
const issuance = resolveChoiceWordIssuance(options.env);
|
|
137
134
|
const keys = hasRequestedChoiceBinding(issueOptions.bind)
|
|
138
135
|
? deriveManagedChoiceKeys({
|
|
139
136
|
masterSecret: resolveMasterSecret(),
|
|
@@ -159,29 +156,8 @@ export function createProviderChoiceContext(
|
|
|
159
156
|
ttl_ms: issueOptions.ttlMs,
|
|
160
157
|
binding,
|
|
161
158
|
} satisfies Omit<ManagedChoiceEnvelope, "payload">;
|
|
162
|
-
if (issuance === "legacy") {
|
|
163
|
-
const legacyKeys =
|
|
164
|
-
keys ??
|
|
165
|
-
deriveManagedChoiceKeys({
|
|
166
|
-
masterSecret: resolveMasterSecret(),
|
|
167
|
-
providerId: options.providerId,
|
|
168
|
-
purpose: issueOptions.purpose,
|
|
169
|
-
kid,
|
|
170
|
-
});
|
|
171
|
-
return issueLegacyServerStoredChoice({
|
|
172
|
-
baseEnvelope,
|
|
173
|
-
issueOptions,
|
|
174
|
-
storage: resolvedStorage.storage,
|
|
175
|
-
contextState: options.state,
|
|
176
|
-
kid,
|
|
177
|
-
keys: legacyKeys,
|
|
178
|
-
issuedAtMs,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
159
|
return issueWordServerStoredChoice({
|
|
182
|
-
baseEnvelope
|
|
183
|
-
...baseEnvelope,
|
|
184
|
-
},
|
|
160
|
+
baseEnvelope,
|
|
185
161
|
issueOptions,
|
|
186
162
|
storage: resolvedStorage.storage,
|
|
187
163
|
contextState: options.state,
|
|
@@ -287,10 +263,9 @@ export function createProviderChoiceContext(
|
|
|
287
263
|
);
|
|
288
264
|
}
|
|
289
265
|
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
// consumption, and binding failures can never enter this branch.
|
|
266
|
+
// Inline choices continue to use the encrypted envelope. A structurally
|
|
267
|
+
// valid word token returns above, so lookup, expiry, consumption, and
|
|
268
|
+
// binding failures can never enter this branch.
|
|
294
269
|
try {
|
|
295
270
|
const [actualPrefix, tokenKid, encodedIv, encryptedPayload, authTag, signature] =
|
|
296
271
|
parseManagedChoiceTokenParts(parseOptions.token);
|
|
@@ -345,17 +320,14 @@ export function createProviderChoiceContext(
|
|
|
345
320
|
required: true,
|
|
346
321
|
}),
|
|
347
322
|
});
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
contextState: options.state,
|
|
353
|
-
})
|
|
354
|
-
: envelope.payload;
|
|
323
|
+
if (isServerChoiceHandlePayload(envelope.payload)) {
|
|
324
|
+
throw wordChoiceNotFoundError();
|
|
325
|
+
}
|
|
326
|
+
const payload = envelope.payload;
|
|
355
327
|
const parsed =
|
|
356
328
|
consumeMode === "explicit"
|
|
357
329
|
? Promise.resolve(payload).then((resolvedPayload) =>
|
|
358
|
-
|
|
330
|
+
createInlineExplicitParseResult({
|
|
359
331
|
payload: resolvedPayload,
|
|
360
332
|
replayKey: digestChoiceReplayKey(parseOptions.token),
|
|
361
333
|
onConsume: () =>
|
|
@@ -495,7 +467,7 @@ function emitChoiceTelemetry(
|
|
|
495
467
|
}
|
|
496
468
|
}
|
|
497
469
|
|
|
498
|
-
function
|
|
470
|
+
function createInlineExplicitParseResult(options: {
|
|
499
471
|
readonly payload: ProviderChoiceTokenPayload;
|
|
500
472
|
readonly replayKey: string;
|
|
501
473
|
readonly onConsume: () => void;
|
|
@@ -511,22 +483,6 @@ function createLegacyExplicitParseResult(options: {
|
|
|
511
483
|
};
|
|
512
484
|
}
|
|
513
485
|
|
|
514
|
-
function resolveChoiceWordIssuance(env?: EnvContext): "legacy" | "word" {
|
|
515
|
-
const configured = env?.get(PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV);
|
|
516
|
-
const value = configured?.trim() ?? "";
|
|
517
|
-
if (value === "" || value === "legacy") return "legacy";
|
|
518
|
-
if (value === "word") return "word";
|
|
519
|
-
throw new ProviderError(
|
|
520
|
-
`Unsupported provider choice word issuance mode "${value}". Expected "legacy" or "word".`,
|
|
521
|
-
{
|
|
522
|
-
code: "CHOICE_WORD_ISSUANCE_INVALID",
|
|
523
|
-
category: "input_validation",
|
|
524
|
-
retryable: false,
|
|
525
|
-
details: { env: PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV },
|
|
526
|
-
},
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
486
|
function resolveChoiceMasterSecret(options: CreateProviderChoiceContextOptions): string {
|
|
531
487
|
const configured =
|
|
532
488
|
options.masterSecret ?? options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
|
|
@@ -657,56 +613,6 @@ async function issueWordServerStoredChoice<TPayload extends ProviderChoiceTokenP
|
|
|
657
613
|
});
|
|
658
614
|
}
|
|
659
615
|
|
|
660
|
-
/**
|
|
661
|
-
* Beta.28-compatible server handle issuance. The state stores the payload and
|
|
662
|
-
* the client receives the existing six-part encrypted envelope whose payload
|
|
663
|
-
* is a server-state handle.
|
|
664
|
-
*/
|
|
665
|
-
async function issueLegacyServerStoredChoice<TPayload extends ProviderChoiceTokenPayload>(options: {
|
|
666
|
-
readonly baseEnvelope: Omit<ManagedChoiceEnvelope, "payload">;
|
|
667
|
-
readonly issueOptions: ProviderChoiceIssueOptions<TPayload>;
|
|
668
|
-
readonly storage: ServerProviderChoiceStorageOptions;
|
|
669
|
-
readonly contextState?: ProviderRuntimeState;
|
|
670
|
-
readonly kid: string;
|
|
671
|
-
readonly keys: ManagedChoiceKeys;
|
|
672
|
-
readonly issuedAtMs: number;
|
|
673
|
-
}): Promise<string> {
|
|
674
|
-
const serializedPayload = serializeChoicePayload(options.issueOptions.payload);
|
|
675
|
-
const payloadBytes = Buffer.byteLength(serializedPayload, "utf8");
|
|
676
|
-
if (payloadBytes > options.storage.maxValueBytes) {
|
|
677
|
-
throw new ProviderError("Provider choice payload exceeds state storage policy.", {
|
|
678
|
-
code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
|
|
679
|
-
category: "input_validation",
|
|
680
|
-
retryable: false,
|
|
681
|
-
details: { maxValueBytes: options.storage.maxValueBytes, payloadBytes },
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
const stateId = `choice_${randomBytes(16).toString("base64url")}`;
|
|
685
|
-
const namespace = resolveChoiceStateNamespace({
|
|
686
|
-
storage: options.storage,
|
|
687
|
-
contextState: options.contextState,
|
|
688
|
-
ttlMs: options.issueOptions.ttlMs,
|
|
689
|
-
});
|
|
690
|
-
await namespace.set(optionsStateKey(stateId), options.issueOptions.payload, {
|
|
691
|
-
ttl: stateTtl(options.storage, options.issueOptions.ttlMs),
|
|
692
|
-
});
|
|
693
|
-
const envelope: ManagedChoiceEnvelope = {
|
|
694
|
-
...options.baseEnvelope,
|
|
695
|
-
payload: {
|
|
696
|
-
storage: "server",
|
|
697
|
-
state_id: stateId,
|
|
698
|
-
payload_digest: digestChoicePayload(serializedPayload),
|
|
699
|
-
created_at_ms: options.issuedAtMs,
|
|
700
|
-
},
|
|
701
|
-
};
|
|
702
|
-
return encryptManagedChoiceToken({
|
|
703
|
-
prefix: options.issueOptions.prefix,
|
|
704
|
-
kid: options.kid,
|
|
705
|
-
envelope,
|
|
706
|
-
keys: options.keys,
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
|
|
710
616
|
async function parseWordServerStoredChoice(options: {
|
|
711
617
|
readonly stateKey: string;
|
|
712
618
|
readonly parseOptions: ProviderChoiceParseOptions;
|
|
@@ -849,53 +755,6 @@ async function consumeWordServerStoredChoice(options: {
|
|
|
849
755
|
}
|
|
850
756
|
}
|
|
851
757
|
|
|
852
|
-
async function parseLegacyServerStoredChoice(options: {
|
|
853
|
-
readonly handle: ServerChoiceHandlePayload;
|
|
854
|
-
readonly storage?: ProviderChoiceStorageOptions;
|
|
855
|
-
readonly contextState?: ProviderRuntimeState;
|
|
856
|
-
}): Promise<ProviderChoiceTokenPayload> {
|
|
857
|
-
const storage = resolveParseStorage(options.storage);
|
|
858
|
-
const namespace = resolveChoiceStateNamespace({
|
|
859
|
-
storage,
|
|
860
|
-
contextState: options.contextState,
|
|
861
|
-
});
|
|
862
|
-
// Reading a server-stored choice back deserializes a persisted value. A
|
|
863
|
-
// corrupt/undecodable value would otherwise surface as a raw JSON.parse
|
|
864
|
-
// SyntaxError (or another unexpected throwable) that escapes the choice error
|
|
865
|
-
// taxonomy, gets masked as internal_error 500, and is treated as retryable by
|
|
866
|
-
// the hub -> reservation restart loop (2026-07-22 catchtable RCA, candidate A).
|
|
867
|
-
// Convert any non-branded throwable into a branded invalid_payload so it maps
|
|
868
|
-
// to a clean, non-retryable 400. Branded ProviderChoiceTokenError and genuine
|
|
869
|
-
// ProviderError (e.g. Redis-unavailable / state-unavailable) pass through so
|
|
870
|
-
// their category/retryable semantics are preserved.
|
|
871
|
-
let record: StateValue<ProviderChoiceTokenPayload> | null;
|
|
872
|
-
try {
|
|
873
|
-
record = await namespace.get<ProviderChoiceTokenPayload>(
|
|
874
|
-
optionsStateKey(options.handle.state_id),
|
|
875
|
-
);
|
|
876
|
-
} catch (error) {
|
|
877
|
-
if (error instanceof ProviderChoiceTokenError || isProviderError(error)) {
|
|
878
|
-
throw error;
|
|
879
|
-
}
|
|
880
|
-
throw new ProviderChoiceTokenError(
|
|
881
|
-
"invalid_payload",
|
|
882
|
-
"Provider choice token state payload could not be decoded.",
|
|
883
|
-
);
|
|
884
|
-
}
|
|
885
|
-
if (!record) {
|
|
886
|
-
throw new ProviderChoiceTokenError(
|
|
887
|
-
"invalid_payload",
|
|
888
|
-
"Provider choice token state payload is missing.",
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
const serializedPayload = serializeChoicePayload(record.value);
|
|
892
|
-
assertPayloadDigestMatches({
|
|
893
|
-
actual: digestChoicePayload(serializedPayload),
|
|
894
|
-
expected: options.handle.payload_digest,
|
|
895
|
-
});
|
|
896
|
-
return record.value;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
758
|
function generateChoiceWordSequence(wordCount: number): string {
|
|
900
759
|
return Array.from({ length: wordCount }, () =>
|
|
901
760
|
choiceWordAt(randomInt(CHOICE_WORDLIST_SIZE)),
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
|
|
19
19
|
const BROWSER_VENDOR_ID = "browser" as const;
|
|
20
20
|
const DEFAULT_COOKIE_POLL_INTERVAL_MS = 100;
|
|
21
|
+
const AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX = ".awswaf.com";
|
|
22
|
+
const RESOLVER_DOCUMENT_CONTENT_SECURITY_POLICY = "connect-src http: https:; worker-src 'none'";
|
|
21
23
|
|
|
22
24
|
const SUCCESS_COOKIE_NAMES = {
|
|
23
25
|
aws_waf: "aws-waf-token",
|
|
@@ -28,6 +30,22 @@ type SupportedBrowserChallengeKind = keyof typeof SUCCESS_COOKIE_NAMES;
|
|
|
28
30
|
|
|
29
31
|
type BrowserClientFactory = (options: BrowserClientOptions) => BrowserClient;
|
|
30
32
|
|
|
33
|
+
let createResolverBrowserClient: BrowserClientFactory = createBrowserClient;
|
|
34
|
+
|
|
35
|
+
/** Internal test seam; deliberately not re-exported from the package root. */
|
|
36
|
+
export function swapBrowserResolverClientFactoryForTests(
|
|
37
|
+
factory: BrowserClientFactory | undefined,
|
|
38
|
+
): () => void {
|
|
39
|
+
const original = createResolverBrowserClient;
|
|
40
|
+
createResolverBrowserClient = factory ?? createBrowserClient;
|
|
41
|
+
let restored = false;
|
|
42
|
+
return () => {
|
|
43
|
+
if (restored) return;
|
|
44
|
+
restored = true;
|
|
45
|
+
createResolverBrowserClient = original;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
export interface BrowserResolverVendorOptions {
|
|
32
50
|
readonly cdpUrl?: string;
|
|
33
51
|
readonly timeoutMs: number;
|
|
@@ -200,31 +218,77 @@ function selectSuccessCookie(
|
|
|
200
218
|
|
|
201
219
|
async function solveInPage(
|
|
202
220
|
page: BrowserPage,
|
|
221
|
+
challengeKind: SupportedBrowserChallengeKind,
|
|
203
222
|
pageUrl: string,
|
|
223
|
+
allowedHosts: readonly string[],
|
|
204
224
|
successCookieName: string,
|
|
205
225
|
pollIntervalMs: number,
|
|
206
226
|
signal: AbortSignal,
|
|
207
227
|
): Promise<Extract<ChallengeSolution, { readonly form: "cookies" }>> {
|
|
208
|
-
await
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
228
|
+
return await page.withResourcePolicy(
|
|
229
|
+
{
|
|
230
|
+
allowedMethods: ["GET", "HEAD", "POST"],
|
|
231
|
+
documentContentSecurityPolicy: RESOLVER_DOCUMENT_CONTENT_SECURITY_POLICY,
|
|
232
|
+
routes: [
|
|
233
|
+
{
|
|
234
|
+
match: () => true,
|
|
235
|
+
handle: (request) => ({
|
|
236
|
+
action: isResolverBrowserRequestAllowed(request.url, challengeKind, allowedHosts)
|
|
237
|
+
? "continue"
|
|
238
|
+
: "block",
|
|
239
|
+
}),
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
},
|
|
243
|
+
async () => {
|
|
214
244
|
const userAgent = await raceWithAbort(
|
|
215
|
-
() => page.
|
|
245
|
+
() => page.userAgent(),
|
|
216
246
|
signal,
|
|
217
247
|
);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
248
|
+
await raceWithAbort(() => page.goto(pageUrl), signal);
|
|
249
|
+
|
|
250
|
+
while (true) {
|
|
251
|
+
const cookies = await raceWithAbort(() => page.cookies(), signal);
|
|
252
|
+
const successCookie = selectSuccessCookie(cookies, successCookieName, pageUrl);
|
|
253
|
+
if (successCookie) {
|
|
254
|
+
return {
|
|
255
|
+
form: "cookies",
|
|
256
|
+
cookies: { [successCookieName]: successCookie.value },
|
|
257
|
+
userAgent,
|
|
258
|
+
...(successCookie.expires === undefined ? {} : { expires: successCookie.expires }),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
await abortableDelay(pollIntervalMs, signal);
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
}
|
|
225
267
|
|
|
226
|
-
|
|
268
|
+
function isResolverBrowserRequestAllowed(
|
|
269
|
+
targetUrl: string,
|
|
270
|
+
challengeKind: SupportedBrowserChallengeKind,
|
|
271
|
+
allowedHosts: readonly string[],
|
|
272
|
+
): boolean {
|
|
273
|
+
try {
|
|
274
|
+
assertResolverHostAllowed(targetUrl, allowedHosts);
|
|
275
|
+
return true;
|
|
276
|
+
} catch {
|
|
277
|
+
if (challengeKind !== "aws_waf") return false;
|
|
227
278
|
}
|
|
279
|
+
|
|
280
|
+
let target: URL;
|
|
281
|
+
try {
|
|
282
|
+
target = new URL(targetUrl);
|
|
283
|
+
} catch {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
const hostname = normalizedResolverHostname(target.hostname);
|
|
287
|
+
return (
|
|
288
|
+
target.protocol === "https:" &&
|
|
289
|
+
hostname.length > AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX.length &&
|
|
290
|
+
hostname.endsWith(AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX)
|
|
291
|
+
);
|
|
228
292
|
}
|
|
229
293
|
|
|
230
294
|
const POOL_ALLOCATION_EXHAUSTED_CODES = new Set([
|
|
@@ -297,7 +361,7 @@ async function closeBrowserClient(
|
|
|
297
361
|
export function createBrowserResolverVendorAdapter(
|
|
298
362
|
options: BrowserResolverVendorOptions,
|
|
299
363
|
): BrowserResolverVendorAdapter {
|
|
300
|
-
const createClient = options.createClient ??
|
|
364
|
+
const createClient = options.createClient ?? createResolverBrowserClient;
|
|
301
365
|
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_COOKIE_POLL_INTERVAL_MS;
|
|
302
366
|
|
|
303
367
|
return {
|
|
@@ -352,12 +416,15 @@ export function createBrowserResolverVendorAdapter(
|
|
|
352
416
|
cdpUrl: cdpUrl ?? "",
|
|
353
417
|
...(proxyUrl === undefined ? {} : { proxy: proxyUrl }),
|
|
354
418
|
requireCdpPool: cdpUrl !== undefined,
|
|
419
|
+
serviceWorkers: "block",
|
|
355
420
|
});
|
|
356
421
|
const contextOperation = client.withIsolatedContext(async (page) => {
|
|
357
422
|
handlerEntered = true;
|
|
358
423
|
return await solveInPage(
|
|
359
424
|
page,
|
|
425
|
+
challengeKind,
|
|
360
426
|
challenge.pageUrl,
|
|
427
|
+
options.allowedHosts,
|
|
361
428
|
SUCCESS_COOKIE_NAMES[challengeKind],
|
|
362
429
|
pollIntervalMs,
|
|
363
430
|
solveController.signal,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { getStealthProfile } from "../../stealth/profiles.js";
|
|
1
2
|
import type { ChallengeSolution, ProviderChallenge } from "../../types.js";
|
|
2
3
|
import { redactSensitiveText } from "../request-options.js";
|
|
4
|
+
import { DEFAULT_PROFILE } from "../stealth.js";
|
|
3
5
|
import type { TraceRecorder } from "../trace.js";
|
|
4
6
|
import { assertResolverHostAllowed } from "./hosts.js";
|
|
5
7
|
import {
|
|
@@ -39,7 +41,7 @@ export interface CapsolverResolverVendorAdapter extends ResolverVendorAdapter {
|
|
|
39
41
|
identity: ResolverIdentity | undefined,
|
|
40
42
|
signal: AbortSignal,
|
|
41
43
|
traceRecorder?: TraceRecorder,
|
|
42
|
-
): Promise<
|
|
44
|
+
): Promise<ChallengeSolution>;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
class CapsolverSolveTimeoutError extends Error {
|
|
@@ -91,7 +93,15 @@ interface CapsolverCreateTaskResponse extends CapsolverResponseErrorFields {
|
|
|
91
93
|
|
|
92
94
|
interface CapsolverPollResultResponse extends CapsolverResponseErrorFields {
|
|
93
95
|
readonly status: string | undefined;
|
|
94
|
-
readonly solution:
|
|
96
|
+
readonly solution:
|
|
97
|
+
| {
|
|
98
|
+
readonly token: string | undefined;
|
|
99
|
+
readonly cookie: string | undefined;
|
|
100
|
+
readonly gRecaptchaResponse: string | undefined;
|
|
101
|
+
readonly cookies: Readonly<Record<string, string>> | undefined;
|
|
102
|
+
readonly userAgent: string | undefined;
|
|
103
|
+
}
|
|
104
|
+
| undefined;
|
|
95
105
|
}
|
|
96
106
|
|
|
97
107
|
class CapsolverVerdictError extends ResolverChallengeVerdictError {
|
|
@@ -233,15 +243,52 @@ function parseCreateTaskResponse(payload: JsonRecord): CapsolverCreateTaskRespon
|
|
|
233
243
|
|
|
234
244
|
function parsePollResultResponse(payload: JsonRecord): CapsolverPollResultResponse {
|
|
235
245
|
const solution = isJsonRecord(payload.solution) ? payload.solution : undefined;
|
|
246
|
+
const cookies = solution && isJsonRecord(solution.cookies)
|
|
247
|
+
? Object.fromEntries(
|
|
248
|
+
Object.entries(solution.cookies).filter(
|
|
249
|
+
(entry): entry is [string, string] => typeof entry[1] === "string",
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
: undefined;
|
|
236
253
|
return {
|
|
237
254
|
...responseErrorFields(payload),
|
|
238
255
|
status: typeof payload.status === "string" ? payload.status : undefined,
|
|
239
|
-
|
|
240
|
-
? {
|
|
256
|
+
solution: solution
|
|
257
|
+
? {
|
|
258
|
+
token: typeof solution.token === "string" ? solution.token : undefined,
|
|
259
|
+
cookie: typeof solution.cookie === "string" ? solution.cookie : undefined,
|
|
260
|
+
gRecaptchaResponse:
|
|
261
|
+
typeof solution.gRecaptchaResponse === "string"
|
|
262
|
+
? solution.gRecaptchaResponse
|
|
263
|
+
: undefined,
|
|
264
|
+
cookies,
|
|
265
|
+
userAgent: typeof solution.userAgent === "string" ? solution.userAgent : undefined,
|
|
266
|
+
}
|
|
241
267
|
: undefined,
|
|
242
268
|
};
|
|
243
269
|
}
|
|
244
270
|
|
|
271
|
+
function proxyForCapsolver(
|
|
272
|
+
proxyUrl: string,
|
|
273
|
+
): { readonly value: string; readonly sensitive: readonly string[] } | undefined {
|
|
274
|
+
try {
|
|
275
|
+
const url = new URL(proxyUrl);
|
|
276
|
+
const protocol = url.protocol.slice(0, -1).toLowerCase();
|
|
277
|
+
const scheme = protocol === "http" || protocol === "socks5" ? protocol : undefined;
|
|
278
|
+
const port = Number(url.port || (scheme === "socks5" ? 1080 : 80));
|
|
279
|
+
if (!scheme || !url.hostname || !Number.isInteger(port) || port <= 0 || port > 65_535) {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
const username = url.username ? decodeURIComponent(url.username) : "";
|
|
283
|
+
const password = url.password ? decodeURIComponent(url.password) : "";
|
|
284
|
+
if (username.includes(":") || password.includes(":")) return undefined;
|
|
285
|
+
const value = `${scheme}:${url.hostname}:${port}${username || password ? `:${username}:${password}` : ""}`;
|
|
286
|
+
return { value, sensitive: [proxyUrl, value, username, password].filter(Boolean) };
|
|
287
|
+
} catch {
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
245
292
|
function isAllocationExhausted(payload: CapsolverResponseErrorFields): boolean {
|
|
246
293
|
const code = payload.errorCode?.toLowerCase() ?? "";
|
|
247
294
|
const description = payload.errorDescription?.toLowerCase() ?? "";
|
|
@@ -403,7 +450,7 @@ export function createCapsolverResolverVendorAdapter(
|
|
|
403
450
|
return resolverVendorSupports(CAPSOLVER_VENDOR_ID, kind);
|
|
404
451
|
},
|
|
405
452
|
|
|
406
|
-
async solve(challenge,
|
|
453
|
+
async solve(challenge, identity, callerSignal, traceRecorder) {
|
|
407
454
|
const apiKey = options.apiKey?.trim();
|
|
408
455
|
if (!apiKey) {
|
|
409
456
|
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "missing_credentials", {
|
|
@@ -413,17 +460,34 @@ export function createCapsolverResolverVendorAdapter(
|
|
|
413
460
|
if (!resolverVendorSupports(CAPSOLVER_VENDOR_ID, challenge.kind)) {
|
|
414
461
|
throw new TypeError(`Capsolver resolver does not support ${challenge.kind}`);
|
|
415
462
|
}
|
|
416
|
-
|
|
417
|
-
|
|
463
|
+
const proxy = identity ? proxyForCapsolver(identity.proxyUrl) : undefined;
|
|
464
|
+
if (challenge.kind === "aws_waf" && identity && !proxy) {
|
|
465
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
418
466
|
phase: "create_task",
|
|
419
467
|
});
|
|
420
468
|
}
|
|
469
|
+
if (challenge.kind === "cloudflare_interstitial" && !identity) {
|
|
470
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "missing_proxy_identity", {
|
|
471
|
+
phase: "create_task",
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
if (identity && !proxy) {
|
|
475
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
476
|
+
phase: "create_task",
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
const challengeFields = challenge as unknown as Record<string, unknown>;
|
|
421
480
|
const sensitiveValues = [
|
|
422
481
|
apiKey,
|
|
423
482
|
challenge.pageUrl,
|
|
424
|
-
|
|
425
|
-
...(
|
|
426
|
-
...(
|
|
483
|
+
...(typeof challengeFields.siteKey === "string" ? [challengeFields.siteKey] : []),
|
|
484
|
+
...(typeof challengeFields.action === "string" ? [challengeFields.action] : []),
|
|
485
|
+
...(typeof challengeFields.cdata === "string" ? [challengeFields.cdata] : []),
|
|
486
|
+
...(typeof challengeFields.blockedHtml === "string" ? [challengeFields.blockedHtml] : []),
|
|
487
|
+
...(typeof challengeFields.captchaScript === "string" ? [challengeFields.captchaScript] : []),
|
|
488
|
+
...(typeof challengeFields.context === "string" ? [challengeFields.context] : []),
|
|
489
|
+
...(typeof challengeFields.iv === "string" ? [challengeFields.iv] : []),
|
|
490
|
+
...(proxy?.sensitive ?? []),
|
|
427
491
|
];
|
|
428
492
|
|
|
429
493
|
assertResolverHostAllowed(challenge.pageUrl, options.allowedHosts);
|
|
@@ -440,24 +504,73 @@ export function createCapsolverResolverVendorAdapter(
|
|
|
440
504
|
|
|
441
505
|
try {
|
|
442
506
|
const createTask = async () => {
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
507
|
+
const task =
|
|
508
|
+
challenge.kind === "aws_waf"
|
|
509
|
+
? {
|
|
510
|
+
type: proxy ? "AntiAwsWafTask" : "AntiAwsWafTaskProxyLess",
|
|
511
|
+
websiteURL: challenge.pageUrl,
|
|
512
|
+
...(challenge.siteKey !== undefined ? { awsKey: challenge.siteKey } : {}),
|
|
513
|
+
...(challenge.iv !== undefined ? { awsIv: challenge.iv } : {}),
|
|
514
|
+
...(challenge.context !== undefined ? { awsContext: challenge.context } : {}),
|
|
515
|
+
...(challenge.captchaScript !== undefined
|
|
516
|
+
? { awsChallengeJS: challenge.captchaScript }
|
|
517
|
+
: {}),
|
|
518
|
+
...(proxy ? { proxy: proxy.value } : {}),
|
|
519
|
+
}
|
|
520
|
+
: challenge.kind === "turnstile"
|
|
521
|
+
? {
|
|
522
|
+
type: "AntiTurnstileTaskProxyLess",
|
|
523
|
+
websiteURL: challenge.pageUrl,
|
|
524
|
+
websiteKey: challenge.siteKey,
|
|
525
|
+
...(challenge.action !== undefined || challenge.cdata !== undefined
|
|
526
|
+
? {
|
|
527
|
+
metadata: {
|
|
528
|
+
...(challenge.action !== undefined ? { action: challenge.action } : {}),
|
|
529
|
+
...(challenge.cdata !== undefined ? { cdata: challenge.cdata } : {}),
|
|
530
|
+
},
|
|
531
|
+
}
|
|
532
|
+
: {}),
|
|
533
|
+
}
|
|
534
|
+
: challenge.kind === "recaptcha_v2"
|
|
535
|
+
? {
|
|
536
|
+
type: proxy ? "ReCaptchaV2Task" : "ReCaptchaV2TaskProxyLess",
|
|
537
|
+
websiteURL: challenge.pageUrl,
|
|
538
|
+
websiteKey: challenge.siteKey,
|
|
539
|
+
...(proxy ? { proxy: proxy.value } : {}),
|
|
540
|
+
}
|
|
541
|
+
: challenge.kind === "recaptcha_v3"
|
|
542
|
+
? {
|
|
543
|
+
type: proxy ? "ReCaptchaV3Task" : "ReCaptchaV3TaskProxyLess",
|
|
544
|
+
websiteURL: challenge.pageUrl,
|
|
545
|
+
websiteKey: challenge.siteKey,
|
|
546
|
+
pageAction: challenge.action,
|
|
547
|
+
...(challenge.minScore !== undefined ? { minScore: challenge.minScore } : {}),
|
|
548
|
+
...(proxy ? { proxy: proxy.value } : {}),
|
|
549
|
+
}
|
|
550
|
+
: challenge.kind === "hcaptcha"
|
|
551
|
+
? {
|
|
552
|
+
type: proxy ? "HCaptchaTask" : "HCaptchaTaskProxyLess",
|
|
553
|
+
websiteURL: challenge.pageUrl,
|
|
554
|
+
websiteKey: challenge.siteKey,
|
|
555
|
+
...(proxy ? { proxy: proxy.value } : {}),
|
|
556
|
+
}
|
|
557
|
+
: challenge.kind === "cloudflare_interstitial"
|
|
558
|
+
? {
|
|
559
|
+
type: "AntiCloudflareTask",
|
|
560
|
+
websiteURL: challenge.pageUrl,
|
|
561
|
+
proxy: proxy?.value,
|
|
562
|
+
...(identity?.userAgent ? { userAgent: identity.userAgent } : {}),
|
|
563
|
+
...(challenge.kind === "cloudflare_interstitial" && challenge.blockedHtml !== undefined
|
|
564
|
+
? { html: challenge.blockedHtml }
|
|
565
|
+
: {}),
|
|
566
|
+
}
|
|
567
|
+
: (() => {
|
|
568
|
+
throw new TypeError(`Capsolver resolver does not support ${challenge.kind}`);
|
|
569
|
+
})();
|
|
447
570
|
const createResult = await postJson(
|
|
448
571
|
fetchImpl,
|
|
449
572
|
endpoint(baseUrl, "createTask"),
|
|
450
|
-
{
|
|
451
|
-
clientKey: apiKey,
|
|
452
|
-
task: {
|
|
453
|
-
type: "AntiTurnstileTaskProxyLess",
|
|
454
|
-
websiteURL: challenge.pageUrl,
|
|
455
|
-
websiteKey: challenge.siteKey,
|
|
456
|
-
...(challenge.action !== undefined || challenge.cdata !== undefined
|
|
457
|
-
? { metadata }
|
|
458
|
-
: {}),
|
|
459
|
-
},
|
|
460
|
-
},
|
|
573
|
+
{ clientKey: apiKey, task },
|
|
461
574
|
solveController.signal,
|
|
462
575
|
phase,
|
|
463
576
|
sensitiveValues,
|
|
@@ -508,20 +621,44 @@ export function createCapsolverResolverVendorAdapter(
|
|
|
508
621
|
case "ready":
|
|
509
622
|
break;
|
|
510
623
|
default:
|
|
511
|
-
throw new ResolverVendorUnavailableError(
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
{ phase },
|
|
515
|
-
);
|
|
624
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
625
|
+
phase,
|
|
626
|
+
});
|
|
516
627
|
}
|
|
517
628
|
|
|
518
|
-
const
|
|
519
|
-
|
|
629
|
+
const solutionValue =
|
|
630
|
+
challenge.kind === "aws_waf"
|
|
631
|
+
? result.payload.solution?.cookie
|
|
632
|
+
: result.payload.solution?.token ?? result.payload.solution?.gRecaptchaResponse;
|
|
633
|
+
if (challenge.kind === "cloudflare_interstitial") {
|
|
634
|
+
const cookies = result.payload.solution?.cookies;
|
|
635
|
+
const clearance = cookies?.cf_clearance ?? solutionValue;
|
|
636
|
+
if (!clearance?.trim()) {
|
|
637
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
638
|
+
phase,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
form: "cookies" as const,
|
|
643
|
+
cookies: cookies && Object.keys(cookies).length > 0 ? cookies : { cf_clearance: clearance },
|
|
644
|
+
userAgent:
|
|
645
|
+
result.payload.solution?.userAgent ??
|
|
646
|
+
identity?.userAgent ??
|
|
647
|
+
getStealthProfile(DEFAULT_PROFILE).userAgent,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
if (!solutionValue?.trim()) {
|
|
520
651
|
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
521
652
|
phase,
|
|
522
653
|
});
|
|
523
654
|
}
|
|
524
|
-
return
|
|
655
|
+
return challenge.kind === "aws_waf"
|
|
656
|
+
? {
|
|
657
|
+
form: "cookies" as const,
|
|
658
|
+
cookies: { "aws-waf-token": solutionValue },
|
|
659
|
+
userAgent: identity?.userAgent ?? getStealthProfile(DEFAULT_PROFILE).userAgent,
|
|
660
|
+
}
|
|
661
|
+
: { form: "token" as const, token: solutionValue };
|
|
525
662
|
}
|
|
526
663
|
};
|
|
527
664
|
return traceRecorder
|