@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/dist/runtime/choice.js
CHANGED
|
@@ -3,7 +3,6 @@ import { assertFreshProviderChoiceIssuedAt, ProviderChoiceTokenError, } from "..
|
|
|
3
3
|
import { isProviderError, ProviderError } from "../errors.js";
|
|
4
4
|
import { CHOICE_WORDLIST_SIZE, choiceWordAt, HIGH_CHOICE_WORD_COUNT, isChoiceWord, STANDARD_CHOICE_WORD_COUNT, } from "./choice-wordlist.js";
|
|
5
5
|
export const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
|
|
6
|
-
export const PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_WORD_ISSUANCE";
|
|
7
6
|
const PRIMARY_CHOICE_TOKEN_KID = "v1";
|
|
8
7
|
const MANAGED_CHOICE_TOKEN_VERSION = 1;
|
|
9
8
|
const SERVER_STORED_CHOICE_RECORD_VERSION = 1;
|
|
@@ -16,7 +15,6 @@ export function createProviderChoiceContext(options) {
|
|
|
16
15
|
const issuedAtMs = issueOptions.nowMs ?? Date.now();
|
|
17
16
|
const resolvedStorage = resolveIssueStorage(issueOptions.storage, issueOptions.payload);
|
|
18
17
|
if (resolvedStorage.mode === "server") {
|
|
19
|
-
const issuance = resolveChoiceWordIssuance(options.env);
|
|
20
18
|
const keys = hasRequestedChoiceBinding(issueOptions.bind)
|
|
21
19
|
? deriveManagedChoiceKeys({
|
|
22
20
|
masterSecret: resolveMasterSecret(),
|
|
@@ -42,28 +40,8 @@ export function createProviderChoiceContext(options) {
|
|
|
42
40
|
ttl_ms: issueOptions.ttlMs,
|
|
43
41
|
binding,
|
|
44
42
|
};
|
|
45
|
-
if (issuance === "legacy") {
|
|
46
|
-
const legacyKeys = keys ??
|
|
47
|
-
deriveManagedChoiceKeys({
|
|
48
|
-
masterSecret: resolveMasterSecret(),
|
|
49
|
-
providerId: options.providerId,
|
|
50
|
-
purpose: issueOptions.purpose,
|
|
51
|
-
kid,
|
|
52
|
-
});
|
|
53
|
-
return issueLegacyServerStoredChoice({
|
|
54
|
-
baseEnvelope,
|
|
55
|
-
issueOptions,
|
|
56
|
-
storage: resolvedStorage.storage,
|
|
57
|
-
contextState: options.state,
|
|
58
|
-
kid,
|
|
59
|
-
keys: legacyKeys,
|
|
60
|
-
issuedAtMs,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
43
|
return issueWordServerStoredChoice({
|
|
64
|
-
baseEnvelope
|
|
65
|
-
...baseEnvelope,
|
|
66
|
-
},
|
|
44
|
+
baseEnvelope,
|
|
67
45
|
issueOptions,
|
|
68
46
|
storage: resolvedStorage.storage,
|
|
69
47
|
contextState: options.state,
|
|
@@ -139,10 +117,9 @@ export function createProviderChoiceContext(options) {
|
|
|
139
117
|
consumeMode,
|
|
140
118
|
});
|
|
141
119
|
}
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
// consumption, and binding failures can never enter this branch.
|
|
120
|
+
// Inline choices continue to use the encrypted envelope. A structurally
|
|
121
|
+
// valid word token returns above, so lookup, expiry, consumption, and
|
|
122
|
+
// binding failures can never enter this branch.
|
|
146
123
|
try {
|
|
147
124
|
const [actualPrefix, tokenKid, encodedIv, encryptedPayload, authTag, signature] = parseManagedChoiceTokenParts(parseOptions.token);
|
|
148
125
|
if (actualPrefix !== parseOptions.prefix ||
|
|
@@ -188,15 +165,12 @@ export function createProviderChoiceContext(options) {
|
|
|
188
165
|
required: true,
|
|
189
166
|
}),
|
|
190
167
|
});
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
contextState: options.state,
|
|
196
|
-
})
|
|
197
|
-
: envelope.payload;
|
|
168
|
+
if (isServerChoiceHandlePayload(envelope.payload)) {
|
|
169
|
+
throw wordChoiceNotFoundError();
|
|
170
|
+
}
|
|
171
|
+
const payload = envelope.payload;
|
|
198
172
|
const parsed = consumeMode === "explicit"
|
|
199
|
-
? Promise.resolve(payload).then((resolvedPayload) =>
|
|
173
|
+
? Promise.resolve(payload).then((resolvedPayload) => createInlineExplicitParseResult({
|
|
200
174
|
payload: resolvedPayload,
|
|
201
175
|
replayKey: digestChoiceReplayKey(parseOptions.token),
|
|
202
176
|
onConsume: () => emitChoiceTelemetry(options.onTelemetry, {
|
|
@@ -296,7 +270,7 @@ function emitChoiceTelemetry(onTelemetry, event) {
|
|
|
296
270
|
// Observability must never change provider token semantics.
|
|
297
271
|
}
|
|
298
272
|
}
|
|
299
|
-
function
|
|
273
|
+
function createInlineExplicitParseResult(options) {
|
|
300
274
|
return {
|
|
301
275
|
status: "active",
|
|
302
276
|
payload: options.payload,
|
|
@@ -307,20 +281,6 @@ function createLegacyExplicitParseResult(options) {
|
|
|
307
281
|
},
|
|
308
282
|
};
|
|
309
283
|
}
|
|
310
|
-
function resolveChoiceWordIssuance(env) {
|
|
311
|
-
const configured = env?.get(PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV);
|
|
312
|
-
const value = configured?.trim() ?? "";
|
|
313
|
-
if (value === "" || value === "legacy")
|
|
314
|
-
return "legacy";
|
|
315
|
-
if (value === "word")
|
|
316
|
-
return "word";
|
|
317
|
-
throw new ProviderError(`Unsupported provider choice word issuance mode "${value}". Expected "legacy" or "word".`, {
|
|
318
|
-
code: "CHOICE_WORD_ISSUANCE_INVALID",
|
|
319
|
-
category: "input_validation",
|
|
320
|
-
retryable: false,
|
|
321
|
-
details: { env: PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV },
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
284
|
function resolveChoiceMasterSecret(options) {
|
|
325
285
|
const configured = options.masterSecret ?? options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
|
|
326
286
|
const trimmed = configured?.trim();
|
|
@@ -420,47 +380,6 @@ async function issueWordServerStoredChoice(options) {
|
|
|
420
380
|
retryable: false,
|
|
421
381
|
});
|
|
422
382
|
}
|
|
423
|
-
/**
|
|
424
|
-
* Beta.28-compatible server handle issuance. The state stores the payload and
|
|
425
|
-
* the client receives the existing six-part encrypted envelope whose payload
|
|
426
|
-
* is a server-state handle.
|
|
427
|
-
*/
|
|
428
|
-
async function issueLegacyServerStoredChoice(options) {
|
|
429
|
-
const serializedPayload = serializeChoicePayload(options.issueOptions.payload);
|
|
430
|
-
const payloadBytes = Buffer.byteLength(serializedPayload, "utf8");
|
|
431
|
-
if (payloadBytes > options.storage.maxValueBytes) {
|
|
432
|
-
throw new ProviderError("Provider choice payload exceeds state storage policy.", {
|
|
433
|
-
code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
|
|
434
|
-
category: "input_validation",
|
|
435
|
-
retryable: false,
|
|
436
|
-
details: { maxValueBytes: options.storage.maxValueBytes, payloadBytes },
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
const stateId = `choice_${randomBytes(16).toString("base64url")}`;
|
|
440
|
-
const namespace = resolveChoiceStateNamespace({
|
|
441
|
-
storage: options.storage,
|
|
442
|
-
contextState: options.contextState,
|
|
443
|
-
ttlMs: options.issueOptions.ttlMs,
|
|
444
|
-
});
|
|
445
|
-
await namespace.set(optionsStateKey(stateId), options.issueOptions.payload, {
|
|
446
|
-
ttl: stateTtl(options.storage, options.issueOptions.ttlMs),
|
|
447
|
-
});
|
|
448
|
-
const envelope = {
|
|
449
|
-
...options.baseEnvelope,
|
|
450
|
-
payload: {
|
|
451
|
-
storage: "server",
|
|
452
|
-
state_id: stateId,
|
|
453
|
-
payload_digest: digestChoicePayload(serializedPayload),
|
|
454
|
-
created_at_ms: options.issuedAtMs,
|
|
455
|
-
},
|
|
456
|
-
};
|
|
457
|
-
return encryptManagedChoiceToken({
|
|
458
|
-
prefix: options.issueOptions.prefix,
|
|
459
|
-
kid: options.kid,
|
|
460
|
-
envelope,
|
|
461
|
-
keys: options.keys,
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
383
|
async function parseWordServerStoredChoice(options) {
|
|
465
384
|
const storage = resolveParseStorage(options.parseOptions.storage);
|
|
466
385
|
const namespace = resolveChoiceStateNamespace({
|
|
@@ -579,41 +498,6 @@ async function consumeWordServerStoredChoice(options) {
|
|
|
579
498
|
throw wordChoiceNotFoundError();
|
|
580
499
|
}
|
|
581
500
|
}
|
|
582
|
-
async function parseLegacyServerStoredChoice(options) {
|
|
583
|
-
const storage = resolveParseStorage(options.storage);
|
|
584
|
-
const namespace = resolveChoiceStateNamespace({
|
|
585
|
-
storage,
|
|
586
|
-
contextState: options.contextState,
|
|
587
|
-
});
|
|
588
|
-
// Reading a server-stored choice back deserializes a persisted value. A
|
|
589
|
-
// corrupt/undecodable value would otherwise surface as a raw JSON.parse
|
|
590
|
-
// SyntaxError (or another unexpected throwable) that escapes the choice error
|
|
591
|
-
// taxonomy, gets masked as internal_error 500, and is treated as retryable by
|
|
592
|
-
// the hub -> reservation restart loop (2026-07-22 catchtable RCA, candidate A).
|
|
593
|
-
// Convert any non-branded throwable into a branded invalid_payload so it maps
|
|
594
|
-
// to a clean, non-retryable 400. Branded ProviderChoiceTokenError and genuine
|
|
595
|
-
// ProviderError (e.g. Redis-unavailable / state-unavailable) pass through so
|
|
596
|
-
// their category/retryable semantics are preserved.
|
|
597
|
-
let record;
|
|
598
|
-
try {
|
|
599
|
-
record = await namespace.get(optionsStateKey(options.handle.state_id));
|
|
600
|
-
}
|
|
601
|
-
catch (error) {
|
|
602
|
-
if (error instanceof ProviderChoiceTokenError || isProviderError(error)) {
|
|
603
|
-
throw error;
|
|
604
|
-
}
|
|
605
|
-
throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token state payload could not be decoded.");
|
|
606
|
-
}
|
|
607
|
-
if (!record) {
|
|
608
|
-
throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token state payload is missing.");
|
|
609
|
-
}
|
|
610
|
-
const serializedPayload = serializeChoicePayload(record.value);
|
|
611
|
-
assertPayloadDigestMatches({
|
|
612
|
-
actual: digestChoicePayload(serializedPayload),
|
|
613
|
-
expected: options.handle.payload_digest,
|
|
614
|
-
});
|
|
615
|
-
return record.value;
|
|
616
|
-
}
|
|
617
501
|
function generateChoiceWordSequence(wordCount) {
|
|
618
502
|
return Array.from({ length: wordCount }, () => choiceWordAt(randomInt(CHOICE_WORDLIST_SIZE))).join("-");
|
|
619
503
|
}
|
|
@@ -3,6 +3,8 @@ import { type BrowserClientOptions } from "../browser.js";
|
|
|
3
3
|
import type { TraceRecorder } from "../trace.js";
|
|
4
4
|
import { type ResolverIdentity, type ResolverVendorAdapter } from "./types.js";
|
|
5
5
|
type BrowserClientFactory = (options: BrowserClientOptions) => BrowserClient;
|
|
6
|
+
/** Internal test seam; deliberately not re-exported from the package root. */
|
|
7
|
+
export declare function swapBrowserResolverClientFactoryForTests(factory: BrowserClientFactory | undefined): () => void;
|
|
6
8
|
export interface BrowserResolverVendorOptions {
|
|
7
9
|
readonly cdpUrl?: string;
|
|
8
10
|
readonly timeoutMs: number;
|
|
@@ -5,10 +5,25 @@ import { assertResolverHostAllowed, normalizedResolverHostname } from "./hosts.j
|
|
|
5
5
|
import { ResolverVendorUnavailableError, } from "./types.js";
|
|
6
6
|
const BROWSER_VENDOR_ID = "browser";
|
|
7
7
|
const DEFAULT_COOKIE_POLL_INTERVAL_MS = 100;
|
|
8
|
+
const AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX = ".awswaf.com";
|
|
9
|
+
const RESOLVER_DOCUMENT_CONTENT_SECURITY_POLICY = "connect-src http: https:; worker-src 'none'";
|
|
8
10
|
const SUCCESS_COOKIE_NAMES = {
|
|
9
11
|
aws_waf: "aws-waf-token",
|
|
10
12
|
cloudflare_interstitial: "cf_clearance",
|
|
11
13
|
};
|
|
14
|
+
let createResolverBrowserClient = createBrowserClient;
|
|
15
|
+
/** Internal test seam; deliberately not re-exported from the package root. */
|
|
16
|
+
export function swapBrowserResolverClientFactoryForTests(factory) {
|
|
17
|
+
const original = createResolverBrowserClient;
|
|
18
|
+
createResolverBrowserClient = factory ?? createBrowserClient;
|
|
19
|
+
let restored = false;
|
|
20
|
+
return () => {
|
|
21
|
+
if (restored)
|
|
22
|
+
return;
|
|
23
|
+
restored = true;
|
|
24
|
+
createResolverBrowserClient = original;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
12
27
|
class BrowserSolveTimeoutError extends Error {
|
|
13
28
|
constructor() {
|
|
14
29
|
super("Browser resolver solve budget elapsed");
|
|
@@ -126,22 +141,58 @@ function selectSuccessCookie(cookies, successCookieName, pageUrl) {
|
|
|
126
141
|
cookieDomainSpecificity(right) - cookieDomainSpecificity(left) ||
|
|
127
142
|
right.path.length - left.path.length)[0];
|
|
128
143
|
}
|
|
129
|
-
async function solveInPage(page, pageUrl, successCookieName, pollIntervalMs, signal) {
|
|
130
|
-
await
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
144
|
+
async function solveInPage(page, challengeKind, pageUrl, allowedHosts, successCookieName, pollIntervalMs, signal) {
|
|
145
|
+
return await page.withResourcePolicy({
|
|
146
|
+
allowedMethods: ["GET", "HEAD", "POST"],
|
|
147
|
+
documentContentSecurityPolicy: RESOLVER_DOCUMENT_CONTENT_SECURITY_POLICY,
|
|
148
|
+
routes: [
|
|
149
|
+
{
|
|
150
|
+
match: () => true,
|
|
151
|
+
handle: (request) => ({
|
|
152
|
+
action: isResolverBrowserRequestAllowed(request.url, challengeKind, allowedHosts)
|
|
153
|
+
? "continue"
|
|
154
|
+
: "block",
|
|
155
|
+
}),
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
}, async () => {
|
|
159
|
+
const userAgent = await raceWithAbort(() => page.userAgent(), signal);
|
|
160
|
+
await raceWithAbort(() => page.goto(pageUrl), signal);
|
|
161
|
+
while (true) {
|
|
162
|
+
const cookies = await raceWithAbort(() => page.cookies(), signal);
|
|
163
|
+
const successCookie = selectSuccessCookie(cookies, successCookieName, pageUrl);
|
|
164
|
+
if (successCookie) {
|
|
165
|
+
return {
|
|
166
|
+
form: "cookies",
|
|
167
|
+
cookies: { [successCookieName]: successCookie.value },
|
|
168
|
+
userAgent,
|
|
169
|
+
...(successCookie.expires === undefined ? {} : { expires: successCookie.expires }),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
await abortableDelay(pollIntervalMs, signal);
|
|
142
173
|
}
|
|
143
|
-
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function isResolverBrowserRequestAllowed(targetUrl, challengeKind, allowedHosts) {
|
|
177
|
+
try {
|
|
178
|
+
assertResolverHostAllowed(targetUrl, allowedHosts);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
if (challengeKind !== "aws_waf")
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
let target;
|
|
186
|
+
try {
|
|
187
|
+
target = new URL(targetUrl);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return false;
|
|
144
191
|
}
|
|
192
|
+
const hostname = normalizedResolverHostname(target.hostname);
|
|
193
|
+
return (target.protocol === "https:" &&
|
|
194
|
+
hostname.length > AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX.length &&
|
|
195
|
+
hostname.endsWith(AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX));
|
|
145
196
|
}
|
|
146
197
|
const POOL_ALLOCATION_EXHAUSTED_CODES = new Set([
|
|
147
198
|
-32_001, // queue full
|
|
@@ -188,7 +239,7 @@ async function closeBrowserClient(client, timeoutMs, challengeKind, traceRecorde
|
|
|
188
239
|
await runBoundedCleanup(() => close.call(client), timeoutMs, challengeKind, "client.close", traceRecorder);
|
|
189
240
|
}
|
|
190
241
|
export function createBrowserResolverVendorAdapter(options) {
|
|
191
|
-
const createClient = options.createClient ??
|
|
242
|
+
const createClient = options.createClient ?? createResolverBrowserClient;
|
|
192
243
|
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_COOKIE_POLL_INTERVAL_MS;
|
|
193
244
|
return {
|
|
194
245
|
id: BROWSER_VENDOR_ID,
|
|
@@ -235,10 +286,11 @@ export function createBrowserResolverVendorAdapter(options) {
|
|
|
235
286
|
cdpUrl: cdpUrl ?? "",
|
|
236
287
|
...(proxyUrl === undefined ? {} : { proxy: proxyUrl }),
|
|
237
288
|
requireCdpPool: cdpUrl !== undefined,
|
|
289
|
+
serviceWorkers: "block",
|
|
238
290
|
});
|
|
239
291
|
const contextOperation = client.withIsolatedContext(async (page) => {
|
|
240
292
|
handlerEntered = true;
|
|
241
|
-
return await solveInPage(page, challenge.pageUrl, SUCCESS_COOKIE_NAMES[challengeKind], pollIntervalMs, solveController.signal);
|
|
293
|
+
return await solveInPage(page, challengeKind, challenge.pageUrl, options.allowedHosts, SUCCESS_COOKIE_NAMES[challengeKind], pollIntervalMs, solveController.signal);
|
|
242
294
|
});
|
|
243
295
|
try {
|
|
244
296
|
return await raceWithAbort(() => contextOperation, solveController.signal);
|
|
@@ -16,9 +16,7 @@ export interface CapsolverResolverVendorOptions {
|
|
|
16
16
|
}
|
|
17
17
|
export interface CapsolverResolverVendorAdapter extends ResolverVendorAdapter {
|
|
18
18
|
readonly id: "capsolver";
|
|
19
|
-
solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder): Promise<
|
|
20
|
-
readonly form: "token";
|
|
21
|
-
}>>;
|
|
19
|
+
solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder): Promise<ChallengeSolution>;
|
|
22
20
|
}
|
|
23
21
|
export declare function createCapsolverResolverVendorAdapter(options: CapsolverResolverVendorOptions): CapsolverResolverVendorAdapter;
|
|
24
22
|
export {};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { getStealthProfile } from "../../stealth/profiles.js";
|
|
1
2
|
import { redactSensitiveText } from "../request-options.js";
|
|
3
|
+
import { DEFAULT_PROFILE } from "../stealth.js";
|
|
2
4
|
import { assertResolverHostAllowed } from "./hosts.js";
|
|
3
5
|
import { ResolverChallengeVerdictError, ResolverVendorUnavailableError, resolverVendorSupports, } from "./types.js";
|
|
4
6
|
const CAPSOLVER_VENDOR_ID = "capsolver";
|
|
@@ -138,14 +140,45 @@ function parseCreateTaskResponse(payload) {
|
|
|
138
140
|
}
|
|
139
141
|
function parsePollResultResponse(payload) {
|
|
140
142
|
const solution = isJsonRecord(payload.solution) ? payload.solution : undefined;
|
|
143
|
+
const cookies = solution && isJsonRecord(solution.cookies)
|
|
144
|
+
? Object.fromEntries(Object.entries(solution.cookies).filter((entry) => typeof entry[1] === "string"))
|
|
145
|
+
: undefined;
|
|
141
146
|
return {
|
|
142
147
|
...responseErrorFields(payload),
|
|
143
148
|
status: typeof payload.status === "string" ? payload.status : undefined,
|
|
144
149
|
solution: solution
|
|
145
|
-
? {
|
|
150
|
+
? {
|
|
151
|
+
token: typeof solution.token === "string" ? solution.token : undefined,
|
|
152
|
+
cookie: typeof solution.cookie === "string" ? solution.cookie : undefined,
|
|
153
|
+
gRecaptchaResponse: typeof solution.gRecaptchaResponse === "string"
|
|
154
|
+
? solution.gRecaptchaResponse
|
|
155
|
+
: undefined,
|
|
156
|
+
cookies,
|
|
157
|
+
userAgent: typeof solution.userAgent === "string" ? solution.userAgent : undefined,
|
|
158
|
+
}
|
|
146
159
|
: undefined,
|
|
147
160
|
};
|
|
148
161
|
}
|
|
162
|
+
function proxyForCapsolver(proxyUrl) {
|
|
163
|
+
try {
|
|
164
|
+
const url = new URL(proxyUrl);
|
|
165
|
+
const protocol = url.protocol.slice(0, -1).toLowerCase();
|
|
166
|
+
const scheme = protocol === "http" || protocol === "socks5" ? protocol : undefined;
|
|
167
|
+
const port = Number(url.port || (scheme === "socks5" ? 1080 : 80));
|
|
168
|
+
if (!scheme || !url.hostname || !Number.isInteger(port) || port <= 0 || port > 65_535) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
const username = url.username ? decodeURIComponent(url.username) : "";
|
|
172
|
+
const password = url.password ? decodeURIComponent(url.password) : "";
|
|
173
|
+
if (username.includes(":") || password.includes(":"))
|
|
174
|
+
return undefined;
|
|
175
|
+
const value = `${scheme}:${url.hostname}:${port}${username || password ? `:${username}:${password}` : ""}`;
|
|
176
|
+
return { value, sensitive: [proxyUrl, value, username, password].filter(Boolean) };
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
149
182
|
function isAllocationExhausted(payload) {
|
|
150
183
|
const code = payload.errorCode?.toLowerCase() ?? "";
|
|
151
184
|
const description = payload.errorDescription?.toLowerCase() ?? "";
|
|
@@ -266,7 +299,7 @@ export function createCapsolverResolverVendorAdapter(options) {
|
|
|
266
299
|
supports(kind) {
|
|
267
300
|
return resolverVendorSupports(CAPSOLVER_VENDOR_ID, kind);
|
|
268
301
|
},
|
|
269
|
-
async solve(challenge,
|
|
302
|
+
async solve(challenge, identity, callerSignal, traceRecorder) {
|
|
270
303
|
const apiKey = options.apiKey?.trim();
|
|
271
304
|
if (!apiKey) {
|
|
272
305
|
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "missing_credentials", {
|
|
@@ -276,17 +309,34 @@ export function createCapsolverResolverVendorAdapter(options) {
|
|
|
276
309
|
if (!resolverVendorSupports(CAPSOLVER_VENDOR_ID, challenge.kind)) {
|
|
277
310
|
throw new TypeError(`Capsolver resolver does not support ${challenge.kind}`);
|
|
278
311
|
}
|
|
279
|
-
|
|
280
|
-
|
|
312
|
+
const proxy = identity ? proxyForCapsolver(identity.proxyUrl) : undefined;
|
|
313
|
+
if (challenge.kind === "aws_waf" && identity && !proxy) {
|
|
314
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
315
|
+
phase: "create_task",
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
if (challenge.kind === "cloudflare_interstitial" && !identity) {
|
|
319
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "missing_proxy_identity", {
|
|
281
320
|
phase: "create_task",
|
|
282
321
|
});
|
|
283
322
|
}
|
|
323
|
+
if (identity && !proxy) {
|
|
324
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
325
|
+
phase: "create_task",
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
const challengeFields = challenge;
|
|
284
329
|
const sensitiveValues = [
|
|
285
330
|
apiKey,
|
|
286
331
|
challenge.pageUrl,
|
|
287
|
-
|
|
288
|
-
...(
|
|
289
|
-
...(
|
|
332
|
+
...(typeof challengeFields.siteKey === "string" ? [challengeFields.siteKey] : []),
|
|
333
|
+
...(typeof challengeFields.action === "string" ? [challengeFields.action] : []),
|
|
334
|
+
...(typeof challengeFields.cdata === "string" ? [challengeFields.cdata] : []),
|
|
335
|
+
...(typeof challengeFields.blockedHtml === "string" ? [challengeFields.blockedHtml] : []),
|
|
336
|
+
...(typeof challengeFields.captchaScript === "string" ? [challengeFields.captchaScript] : []),
|
|
337
|
+
...(typeof challengeFields.context === "string" ? [challengeFields.context] : []),
|
|
338
|
+
...(typeof challengeFields.iv === "string" ? [challengeFields.iv] : []),
|
|
339
|
+
...(proxy?.sensitive ?? []),
|
|
290
340
|
];
|
|
291
341
|
assertResolverHostAllowed(challenge.pageUrl, options.allowedHosts);
|
|
292
342
|
callerSignal.throwIfAborted();
|
|
@@ -300,21 +350,69 @@ export function createCapsolverResolverVendorAdapter(options) {
|
|
|
300
350
|
let phase = "create_task";
|
|
301
351
|
try {
|
|
302
352
|
const createTask = async () => {
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
};
|
|
307
|
-
const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), {
|
|
308
|
-
clientKey: apiKey,
|
|
309
|
-
task: {
|
|
310
|
-
type: "AntiTurnstileTaskProxyLess",
|
|
353
|
+
const task = challenge.kind === "aws_waf"
|
|
354
|
+
? {
|
|
355
|
+
type: proxy ? "AntiAwsWafTask" : "AntiAwsWafTaskProxyLess",
|
|
311
356
|
websiteURL: challenge.pageUrl,
|
|
312
|
-
|
|
313
|
-
...(challenge.
|
|
314
|
-
|
|
357
|
+
...(challenge.siteKey !== undefined ? { awsKey: challenge.siteKey } : {}),
|
|
358
|
+
...(challenge.iv !== undefined ? { awsIv: challenge.iv } : {}),
|
|
359
|
+
...(challenge.context !== undefined ? { awsContext: challenge.context } : {}),
|
|
360
|
+
...(challenge.captchaScript !== undefined
|
|
361
|
+
? { awsChallengeJS: challenge.captchaScript }
|
|
315
362
|
: {}),
|
|
316
|
-
|
|
317
|
-
|
|
363
|
+
...(proxy ? { proxy: proxy.value } : {}),
|
|
364
|
+
}
|
|
365
|
+
: challenge.kind === "turnstile"
|
|
366
|
+
? {
|
|
367
|
+
type: "AntiTurnstileTaskProxyLess",
|
|
368
|
+
websiteURL: challenge.pageUrl,
|
|
369
|
+
websiteKey: challenge.siteKey,
|
|
370
|
+
...(challenge.action !== undefined || challenge.cdata !== undefined
|
|
371
|
+
? {
|
|
372
|
+
metadata: {
|
|
373
|
+
...(challenge.action !== undefined ? { action: challenge.action } : {}),
|
|
374
|
+
...(challenge.cdata !== undefined ? { cdata: challenge.cdata } : {}),
|
|
375
|
+
},
|
|
376
|
+
}
|
|
377
|
+
: {}),
|
|
378
|
+
}
|
|
379
|
+
: challenge.kind === "recaptcha_v2"
|
|
380
|
+
? {
|
|
381
|
+
type: proxy ? "ReCaptchaV2Task" : "ReCaptchaV2TaskProxyLess",
|
|
382
|
+
websiteURL: challenge.pageUrl,
|
|
383
|
+
websiteKey: challenge.siteKey,
|
|
384
|
+
...(proxy ? { proxy: proxy.value } : {}),
|
|
385
|
+
}
|
|
386
|
+
: challenge.kind === "recaptcha_v3"
|
|
387
|
+
? {
|
|
388
|
+
type: proxy ? "ReCaptchaV3Task" : "ReCaptchaV3TaskProxyLess",
|
|
389
|
+
websiteURL: challenge.pageUrl,
|
|
390
|
+
websiteKey: challenge.siteKey,
|
|
391
|
+
pageAction: challenge.action,
|
|
392
|
+
...(challenge.minScore !== undefined ? { minScore: challenge.minScore } : {}),
|
|
393
|
+
...(proxy ? { proxy: proxy.value } : {}),
|
|
394
|
+
}
|
|
395
|
+
: challenge.kind === "hcaptcha"
|
|
396
|
+
? {
|
|
397
|
+
type: proxy ? "HCaptchaTask" : "HCaptchaTaskProxyLess",
|
|
398
|
+
websiteURL: challenge.pageUrl,
|
|
399
|
+
websiteKey: challenge.siteKey,
|
|
400
|
+
...(proxy ? { proxy: proxy.value } : {}),
|
|
401
|
+
}
|
|
402
|
+
: challenge.kind === "cloudflare_interstitial"
|
|
403
|
+
? {
|
|
404
|
+
type: "AntiCloudflareTask",
|
|
405
|
+
websiteURL: challenge.pageUrl,
|
|
406
|
+
proxy: proxy?.value,
|
|
407
|
+
...(identity?.userAgent ? { userAgent: identity.userAgent } : {}),
|
|
408
|
+
...(challenge.kind === "cloudflare_interstitial" && challenge.blockedHtml !== undefined
|
|
409
|
+
? { html: challenge.blockedHtml }
|
|
410
|
+
: {}),
|
|
411
|
+
}
|
|
412
|
+
: (() => {
|
|
413
|
+
throw new TypeError(`Capsolver resolver does not support ${challenge.kind}`);
|
|
414
|
+
})();
|
|
415
|
+
const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), { clientKey: apiKey, task }, solveController.signal, phase, sensitiveValues, parseCreateTaskResponse);
|
|
318
416
|
const taskId = createResult.payload.taskId;
|
|
319
417
|
if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
|
|
320
418
|
throw unavailableForPayload(createResult.payload, phase, sensitiveValues);
|
|
@@ -352,15 +450,41 @@ export function createCapsolverResolverVendorAdapter(options) {
|
|
|
352
450
|
case "ready":
|
|
353
451
|
break;
|
|
354
452
|
default:
|
|
355
|
-
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
453
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
454
|
+
phase,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
const solutionValue = challenge.kind === "aws_waf"
|
|
458
|
+
? result.payload.solution?.cookie
|
|
459
|
+
: result.payload.solution?.token ?? result.payload.solution?.gRecaptchaResponse;
|
|
460
|
+
if (challenge.kind === "cloudflare_interstitial") {
|
|
461
|
+
const cookies = result.payload.solution?.cookies;
|
|
462
|
+
const clearance = cookies?.cf_clearance ?? solutionValue;
|
|
463
|
+
if (!clearance?.trim()) {
|
|
464
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
465
|
+
phase,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
form: "cookies",
|
|
470
|
+
cookies: cookies && Object.keys(cookies).length > 0 ? cookies : { cf_clearance: clearance },
|
|
471
|
+
userAgent: result.payload.solution?.userAgent ??
|
|
472
|
+
identity?.userAgent ??
|
|
473
|
+
getStealthProfile(DEFAULT_PROFILE).userAgent,
|
|
474
|
+
};
|
|
356
475
|
}
|
|
357
|
-
|
|
358
|
-
if (!token?.trim()) {
|
|
476
|
+
if (!solutionValue?.trim()) {
|
|
359
477
|
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
360
478
|
phase,
|
|
361
479
|
});
|
|
362
480
|
}
|
|
363
|
-
return
|
|
481
|
+
return challenge.kind === "aws_waf"
|
|
482
|
+
? {
|
|
483
|
+
form: "cookies",
|
|
484
|
+
cookies: { "aws-waf-token": solutionValue },
|
|
485
|
+
userAgent: identity?.userAgent ?? getStealthProfile(DEFAULT_PROFILE).userAgent,
|
|
486
|
+
}
|
|
487
|
+
: { form: "token", token: solutionValue };
|
|
364
488
|
}
|
|
365
489
|
};
|
|
366
490
|
return traceRecorder
|