@apifuse/provider-sdk 2.2.0-beta.31 → 2.2.0-beta.33
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 +9 -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/browser.d.ts +1 -0
- package/dist/runtime/browser.js +344 -27
- package/dist/runtime/choice.d.ts +0 -1
- package/dist/runtime/choice.js +1 -78
- 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 +22 -0
- package/dist/runtime/resolver-vendors/capsolver.js +526 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +46 -13
- package/dist/runtime/resolver-vendors/types.d.ts +1 -1
- package/dist/runtime/resolver-vendors/types.js +6 -3
- package/dist/runtime/resolver.d.ts +1 -1
- package/dist/runtime/resolver.js +19 -4
- package/dist/server/serve-implementation.js +1 -2
- package/dist/types.d.ts +9 -1
- package/package.json +1 -1
- package/src/error-resolution.ts +0 -1
- package/src/index.ts +0 -1
- package/src/provider.ts +0 -1
- package/src/runtime/browser.ts +430 -31
- package/src/runtime/choice.ts +1 -91
- package/src/runtime/resolver-vendors/browser.ts +82 -15
- package/src/runtime/resolver-vendors/capsolver.ts +700 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +43 -8
- package/src/runtime/resolver-vendors/types.ts +6 -3
- package/src/runtime/resolver.ts +21 -6
- package/src/server/serve-implementation.ts +0 -2
- package/src/types.ts +10 -1
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import { getStealthProfile } from "../../stealth/profiles.js";
|
|
2
|
+
import { redactSensitiveText } from "../request-options.js";
|
|
3
|
+
import { DEFAULT_PROFILE } from "../stealth.js";
|
|
4
|
+
import { assertResolverHostAllowed } from "./hosts.js";
|
|
5
|
+
import { ResolverChallengeVerdictError, ResolverVendorUnavailableError, resolverVendorSupports, } from "./types.js";
|
|
6
|
+
const CAPSOLVER_VENDOR_ID = "capsolver";
|
|
7
|
+
const DEFAULT_CAPSOLVER_BASE_URL = "https://api.capsolver.com";
|
|
8
|
+
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
10
|
+
class CapsolverSolveTimeoutError extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("Capsolver resolver solve budget elapsed");
|
|
13
|
+
this.name = "CapsolverSolveTimeoutError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
class CapsolverUnavailableError extends ResolverVendorUnavailableError {
|
|
17
|
+
errorCode;
|
|
18
|
+
errorDescription;
|
|
19
|
+
responseStatus;
|
|
20
|
+
responseContentType;
|
|
21
|
+
responseLength;
|
|
22
|
+
constructor(reason, options) {
|
|
23
|
+
super(CAPSOLVER_VENDOR_ID, reason, options);
|
|
24
|
+
this.errorCode = options.errorCode;
|
|
25
|
+
this.errorDescription = options.errorDescription;
|
|
26
|
+
this.responseStatus = options.responseStatus;
|
|
27
|
+
this.responseContentType = options.responseContentType;
|
|
28
|
+
this.responseLength = options.responseLength;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
class CapsolverVerdictError extends ResolverChallengeVerdictError {
|
|
32
|
+
phase;
|
|
33
|
+
errorCode;
|
|
34
|
+
errorDescription;
|
|
35
|
+
constructor(options) {
|
|
36
|
+
super(CAPSOLVER_VENDOR_ID, "solve_failed", options);
|
|
37
|
+
this.phase = options.phase;
|
|
38
|
+
this.errorCode = options.errorCode;
|
|
39
|
+
this.errorDescription = options.errorDescription;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function isJsonRecord(value) {
|
|
43
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
function abortReason(signal) {
|
|
46
|
+
return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
47
|
+
}
|
|
48
|
+
function containsSensitiveValue(value, sensitiveValues) {
|
|
49
|
+
const secrets = sensitiveValues.filter((secret) => secret.length > 0);
|
|
50
|
+
if (secrets.length === 0)
|
|
51
|
+
return false;
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
const inspect = (candidate) => {
|
|
54
|
+
if (typeof candidate === "string") {
|
|
55
|
+
return secrets.some((secret) => candidate.includes(secret));
|
|
56
|
+
}
|
|
57
|
+
if (candidate === null || (typeof candidate !== "object" && typeof candidate !== "function")) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (seen.has(candidate))
|
|
61
|
+
return false;
|
|
62
|
+
seen.add(candidate);
|
|
63
|
+
try {
|
|
64
|
+
for (const property of Reflect.ownKeys(candidate)) {
|
|
65
|
+
if (typeof property === "string" && inspect(property))
|
|
66
|
+
return true;
|
|
67
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, property);
|
|
68
|
+
if (!descriptor)
|
|
69
|
+
return true;
|
|
70
|
+
if ("value" in descriptor && inspect(descriptor.value))
|
|
71
|
+
return true;
|
|
72
|
+
if (descriptor.get !== undefined || descriptor.set !== undefined)
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
};
|
|
81
|
+
return inspect(value);
|
|
82
|
+
}
|
|
83
|
+
function safeCauseOptions(error, sensitiveValues) {
|
|
84
|
+
return containsSensitiveValue(error, sensitiveValues) ? {} : { cause: error };
|
|
85
|
+
}
|
|
86
|
+
function sanitizedJsonParseCause() {
|
|
87
|
+
return new SyntaxError("Upstream response failed");
|
|
88
|
+
}
|
|
89
|
+
function raceWithAbort(operation, signal, phase, sensitiveValues = []) {
|
|
90
|
+
if (signal.aborted)
|
|
91
|
+
return Promise.reject(abortReason(signal));
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
94
|
+
const onAbort = () => {
|
|
95
|
+
cleanup();
|
|
96
|
+
reject(abortReason(signal));
|
|
97
|
+
};
|
|
98
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
99
|
+
operation().then((value) => {
|
|
100
|
+
cleanup();
|
|
101
|
+
resolve(value);
|
|
102
|
+
}, (error) => {
|
|
103
|
+
cleanup();
|
|
104
|
+
if (phase === undefined) {
|
|
105
|
+
reject(error);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
reject(new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
109
|
+
...safeCauseOptions(error, sensitiveValues),
|
|
110
|
+
phase,
|
|
111
|
+
}));
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async function abortableDelay(ms, signal) {
|
|
116
|
+
let timer;
|
|
117
|
+
try {
|
|
118
|
+
await raceWithAbort(() => new Promise((resolve) => {
|
|
119
|
+
timer = setTimeout(resolve, ms);
|
|
120
|
+
}), signal);
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
if (timer !== undefined)
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function responseErrorFields(payload) {
|
|
128
|
+
return {
|
|
129
|
+
errorId: typeof payload.errorId === "number" ? payload.errorId : undefined,
|
|
130
|
+
errorCode: typeof payload.errorCode === "string" ? payload.errorCode : undefined,
|
|
131
|
+
errorDescription: typeof payload.errorDescription === "string" ? payload.errorDescription : undefined,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function parseCreateTaskResponse(payload) {
|
|
135
|
+
const taskId = payload.taskId;
|
|
136
|
+
return {
|
|
137
|
+
...responseErrorFields(payload),
|
|
138
|
+
taskId: typeof taskId === "string" || typeof taskId === "number" ? taskId : undefined,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function parsePollResultResponse(payload) {
|
|
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;
|
|
146
|
+
return {
|
|
147
|
+
...responseErrorFields(payload),
|
|
148
|
+
status: typeof payload.status === "string" ? payload.status : undefined,
|
|
149
|
+
solution: solution
|
|
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
|
+
}
|
|
159
|
+
: undefined,
|
|
160
|
+
};
|
|
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
|
+
}
|
|
182
|
+
function isAllocationExhausted(payload) {
|
|
183
|
+
const code = payload.errorCode?.toLowerCase() ?? "";
|
|
184
|
+
const description = payload.errorDescription?.toLowerCase() ?? "";
|
|
185
|
+
return (code === "error_zero_balance" ||
|
|
186
|
+
/(?:insufficient|zero|no|not enough)\s+(?:balance|funds|credit)/u.test(`${code} ${description}`));
|
|
187
|
+
}
|
|
188
|
+
function isNegativeVerdict(payload) {
|
|
189
|
+
return payload.errorCode?.toLowerCase() === "error_captcha_unsolvable";
|
|
190
|
+
}
|
|
191
|
+
function safeVendorDetail(value, sensitiveValues) {
|
|
192
|
+
const redacted = redactSensitiveText(value, sensitiveValues);
|
|
193
|
+
return containsSensitiveValue(redacted, sensitiveValues) ? "[REDACTED]" : redacted;
|
|
194
|
+
}
|
|
195
|
+
function vendorErrorDetails(payload, sensitiveValues) {
|
|
196
|
+
const errorCode = payload.errorCode?.trim();
|
|
197
|
+
const errorDescription = payload.errorDescription?.trim();
|
|
198
|
+
return {
|
|
199
|
+
...(errorCode ? { errorCode: safeVendorDetail(errorCode, sensitiveValues) } : {}),
|
|
200
|
+
...(errorDescription
|
|
201
|
+
? { errorDescription: safeVendorDetail(payload.errorDescription ?? "", sensitiveValues) }
|
|
202
|
+
: {}),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function unavailableForPayload(payload, phase, sensitiveValues) {
|
|
206
|
+
const details = vendorErrorDetails(payload, sensitiveValues);
|
|
207
|
+
if (isNegativeVerdict(payload)) {
|
|
208
|
+
return new CapsolverVerdictError({ phase, ...details });
|
|
209
|
+
}
|
|
210
|
+
return new CapsolverUnavailableError(isAllocationExhausted(payload) ? "allocation_exhausted" : "transport_failure", { phase, ...details });
|
|
211
|
+
}
|
|
212
|
+
async function postJson(fetchImpl, url, body, signal, phase, sensitiveValues, parseResponse) {
|
|
213
|
+
const response = await raceWithAbort(() => fetchImpl(url, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: { "content-type": "application/json" },
|
|
216
|
+
body: JSON.stringify(body),
|
|
217
|
+
signal,
|
|
218
|
+
redirect: "error",
|
|
219
|
+
}), signal, phase, sensitiveValues);
|
|
220
|
+
let responseText;
|
|
221
|
+
try {
|
|
222
|
+
responseText = await raceWithAbort(() => response.text(), signal, phase, sensitiveValues);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (error instanceof ResolverVendorUnavailableError)
|
|
226
|
+
throw error;
|
|
227
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
228
|
+
cause: error,
|
|
229
|
+
phase,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
let payload;
|
|
233
|
+
try {
|
|
234
|
+
payload = JSON.parse(responseText);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
const contentType = response.headers.get("content-type");
|
|
238
|
+
throw new CapsolverUnavailableError("transport_failure", {
|
|
239
|
+
cause: sanitizedJsonParseCause(),
|
|
240
|
+
phase,
|
|
241
|
+
responseStatus: response.status,
|
|
242
|
+
responseContentType: contentType
|
|
243
|
+
? safeVendorDetail(contentType.slice(0, 128), sensitiveValues)
|
|
244
|
+
: undefined,
|
|
245
|
+
responseLength: responseText.length,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
if (!isJsonRecord(payload)) {
|
|
249
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
250
|
+
phase,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return { ok: response.ok, payload: parseResponse(payload) };
|
|
254
|
+
}
|
|
255
|
+
function endpoint(baseUrl, path) {
|
|
256
|
+
return `${baseUrl.replace(/\/+$/u, "")}/${path}`;
|
|
257
|
+
}
|
|
258
|
+
function spanErrorAttributes(error, phase) {
|
|
259
|
+
if (error instanceof ResolverVendorUnavailableError) {
|
|
260
|
+
return {
|
|
261
|
+
unavailability_reason: error.reason,
|
|
262
|
+
transport_phase: error.phase,
|
|
263
|
+
...(error instanceof CapsolverUnavailableError
|
|
264
|
+
? {
|
|
265
|
+
vendor_error_code: error.errorCode,
|
|
266
|
+
vendor_error_description: error.errorDescription,
|
|
267
|
+
response_status: error.responseStatus,
|
|
268
|
+
response_content_type: error.responseContentType,
|
|
269
|
+
response_length: error.responseLength,
|
|
270
|
+
}
|
|
271
|
+
: {}),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (error instanceof CapsolverVerdictError) {
|
|
275
|
+
return {
|
|
276
|
+
verdict_reason: error.reason,
|
|
277
|
+
transport_phase: error.phase,
|
|
278
|
+
vendor_error_code: error.errorCode,
|
|
279
|
+
vendor_error_description: error.errorDescription,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (error instanceof CapsolverSolveTimeoutError) {
|
|
283
|
+
return {
|
|
284
|
+
unavailability_reason: "timeout",
|
|
285
|
+
transport_phase: phase,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
export function createCapsolverResolverVendorAdapter(options) {
|
|
291
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
292
|
+
const baseUrl = options.baseUrl ?? DEFAULT_CAPSOLVER_BASE_URL;
|
|
293
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
294
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
295
|
+
const now = options.now ?? Date.now;
|
|
296
|
+
const delay = options.delay ?? abortableDelay;
|
|
297
|
+
return {
|
|
298
|
+
id: CAPSOLVER_VENDOR_ID,
|
|
299
|
+
supports(kind) {
|
|
300
|
+
return resolverVendorSupports(CAPSOLVER_VENDOR_ID, kind);
|
|
301
|
+
},
|
|
302
|
+
async solve(challenge, identity, callerSignal, traceRecorder) {
|
|
303
|
+
const apiKey = options.apiKey?.trim();
|
|
304
|
+
if (!apiKey) {
|
|
305
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "missing_credentials", {
|
|
306
|
+
phase: "create_task",
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
if (!resolverVendorSupports(CAPSOLVER_VENDOR_ID, challenge.kind)) {
|
|
310
|
+
throw new TypeError(`Capsolver resolver does not support ${challenge.kind}`);
|
|
311
|
+
}
|
|
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", {
|
|
320
|
+
phase: "create_task",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (identity && !proxy) {
|
|
324
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
325
|
+
phase: "create_task",
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
const challengeFields = challenge;
|
|
329
|
+
const sensitiveValues = [
|
|
330
|
+
apiKey,
|
|
331
|
+
challenge.pageUrl,
|
|
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 ?? []),
|
|
340
|
+
];
|
|
341
|
+
assertResolverHostAllowed(challenge.pageUrl, options.allowedHosts);
|
|
342
|
+
callerSignal.throwIfAborted();
|
|
343
|
+
const solveController = new AbortController();
|
|
344
|
+
const onCallerAbort = () => solveController.abort(abortReason(callerSignal));
|
|
345
|
+
callerSignal.addEventListener("abort", onCallerAbort, { once: true });
|
|
346
|
+
const timeout = options.now
|
|
347
|
+
? undefined
|
|
348
|
+
: setTimeout(() => solveController.abort(new CapsolverSolveTimeoutError()), timeoutMs);
|
|
349
|
+
const startedAt = now();
|
|
350
|
+
let phase = "create_task";
|
|
351
|
+
try {
|
|
352
|
+
const createTask = async () => {
|
|
353
|
+
const task = challenge.kind === "aws_waf"
|
|
354
|
+
? {
|
|
355
|
+
type: proxy ? "AntiAwsWafTask" : "AntiAwsWafTaskProxyLess",
|
|
356
|
+
websiteURL: challenge.pageUrl,
|
|
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 }
|
|
362
|
+
: {}),
|
|
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);
|
|
416
|
+
const taskId = createResult.payload.taskId;
|
|
417
|
+
if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
|
|
418
|
+
throw unavailableForPayload(createResult.payload, phase, sensitiveValues);
|
|
419
|
+
}
|
|
420
|
+
return taskId;
|
|
421
|
+
};
|
|
422
|
+
const taskId = traceRecorder
|
|
423
|
+
? await traceRecorder.runSpan("resolver.vendor.create_task", createTask, {
|
|
424
|
+
attributes: {
|
|
425
|
+
vendor: CAPSOLVER_VENDOR_ID,
|
|
426
|
+
challenge_kind: challenge.kind,
|
|
427
|
+
},
|
|
428
|
+
onError: (error) => spanErrorAttributes(error, "create_task"),
|
|
429
|
+
})
|
|
430
|
+
: await createTask();
|
|
431
|
+
phase = "poll_result";
|
|
432
|
+
const pollResult = async () => {
|
|
433
|
+
while (true) {
|
|
434
|
+
callerSignal.throwIfAborted();
|
|
435
|
+
const remainingMs = timeoutMs - (now() - startedAt);
|
|
436
|
+
if (remainingMs <= 0)
|
|
437
|
+
throw new CapsolverSolveTimeoutError();
|
|
438
|
+
await delay(Math.min(pollIntervalMs, remainingMs), solveController.signal);
|
|
439
|
+
callerSignal.throwIfAborted();
|
|
440
|
+
if (now() - startedAt >= timeoutMs)
|
|
441
|
+
throw new CapsolverSolveTimeoutError();
|
|
442
|
+
const result = await postJson(fetchImpl, endpoint(baseUrl, "getTaskResult"), { clientKey: apiKey, taskId }, solveController.signal, phase, sensitiveValues, parsePollResultResponse);
|
|
443
|
+
if (!result.ok || result.payload.errorId !== 0) {
|
|
444
|
+
throw unavailableForPayload(result.payload, phase, sensitiveValues);
|
|
445
|
+
}
|
|
446
|
+
switch (result.payload.status) {
|
|
447
|
+
case "idle":
|
|
448
|
+
case "processing":
|
|
449
|
+
continue;
|
|
450
|
+
case "ready":
|
|
451
|
+
break;
|
|
452
|
+
default:
|
|
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
|
+
};
|
|
475
|
+
}
|
|
476
|
+
if (!solutionValue?.trim()) {
|
|
477
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
478
|
+
phase,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
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 };
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
return traceRecorder
|
|
491
|
+
? await traceRecorder.runSpan("resolver.vendor.poll_result", pollResult, {
|
|
492
|
+
attributes: {
|
|
493
|
+
vendor: CAPSOLVER_VENDOR_ID,
|
|
494
|
+
challenge_kind: challenge.kind,
|
|
495
|
+
},
|
|
496
|
+
onError: (error) => spanErrorAttributes(error, "poll_result"),
|
|
497
|
+
})
|
|
498
|
+
: await pollResult();
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
if (callerSignal.aborted)
|
|
502
|
+
throw abortReason(callerSignal);
|
|
503
|
+
if (error instanceof CapsolverSolveTimeoutError ||
|
|
504
|
+
solveController.signal.reason instanceof CapsolverSolveTimeoutError) {
|
|
505
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "timeout", {
|
|
506
|
+
cause: error,
|
|
507
|
+
phase,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
if (error instanceof ResolverVendorUnavailableError ||
|
|
511
|
+
error instanceof ResolverChallengeVerdictError) {
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
514
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
515
|
+
...safeCauseOptions(error, sensitiveValues),
|
|
516
|
+
phase,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
finally {
|
|
520
|
+
if (timeout !== undefined)
|
|
521
|
+
clearTimeout(timeout);
|
|
522
|
+
callerSignal.removeEventListener("abort", onCallerAbort);
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
}
|
|
@@ -235,11 +235,6 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
|
235
235
|
if (!resolverVendorSupports(TWOCAPTCHA_VENDOR_ID, challenge.kind)) {
|
|
236
236
|
throw new TypeError(`2captcha resolver does not support ${challenge.kind}`);
|
|
237
237
|
}
|
|
238
|
-
if (challenge.kind !== "recaptcha_v2" && challenge.kind !== "aws_waf") {
|
|
239
|
-
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
|
|
240
|
-
phase: "create_task",
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
238
|
if (challenge.kind === "aws_waf" &&
|
|
244
239
|
(!challenge.siteKey?.trim() ||
|
|
245
240
|
!challenge.captchaScript?.trim() ||
|
|
@@ -249,6 +244,11 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
|
249
244
|
phase: "create_task",
|
|
250
245
|
});
|
|
251
246
|
}
|
|
247
|
+
if (challenge.kind === "recaptcha_v3" && challenge.minScore === undefined) {
|
|
248
|
+
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
|
|
249
|
+
phase: "create_task",
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
252
|
assertResolverHostAllowed(challenge.pageUrl, options.allowedHosts);
|
|
253
253
|
callerSignal.throwIfAborted();
|
|
254
254
|
const proxy = identity ? parseProxyConfiguration(identity.proxyUrl) : undefined;
|
|
@@ -278,14 +278,47 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
|
278
278
|
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
279
279
|
...(proxy ?? {}),
|
|
280
280
|
}
|
|
281
|
-
:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
281
|
+
: challenge.kind === "recaptcha_v2"
|
|
282
|
+
? {
|
|
283
|
+
type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
|
|
284
|
+
websiteURL: challenge.pageUrl,
|
|
285
|
+
websiteKey: challenge.siteKey,
|
|
286
|
+
isInvisible: false,
|
|
287
|
+
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
288
|
+
...(proxy ?? {}),
|
|
289
|
+
}
|
|
290
|
+
: challenge.kind === "recaptcha_v3"
|
|
291
|
+
? {
|
|
292
|
+
type: "RecaptchaV3TaskProxyless",
|
|
293
|
+
websiteURL: challenge.pageUrl,
|
|
294
|
+
websiteKey: challenge.siteKey,
|
|
295
|
+
minScore: challenge.minScore,
|
|
296
|
+
pageAction: challenge.action,
|
|
297
|
+
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
298
|
+
}
|
|
299
|
+
: challenge.kind === "hcaptcha"
|
|
300
|
+
? {
|
|
301
|
+
type: proxy ? "HCaptchaTask" : "HCaptchaTaskProxyless",
|
|
302
|
+
websiteURL: challenge.pageUrl,
|
|
303
|
+
websiteKey: challenge.siteKey,
|
|
304
|
+
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
305
|
+
...(proxy ?? {}),
|
|
306
|
+
}
|
|
307
|
+
: challenge.kind === "turnstile"
|
|
308
|
+
? {
|
|
309
|
+
type: proxy ? "TurnstileTask" : "TurnstileTaskProxyless",
|
|
310
|
+
websiteURL: challenge.pageUrl,
|
|
311
|
+
websiteKey: challenge.siteKey,
|
|
312
|
+
...(challenge.action !== undefined ? { action: challenge.action } : {}),
|
|
313
|
+
...(challenge.cdata !== undefined ? { data: challenge.cdata } : {}),
|
|
314
|
+
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
315
|
+
...(proxy ?? {}),
|
|
316
|
+
}
|
|
317
|
+
: // `resolverVendorSupports` above already rejected every kind this
|
|
318
|
+
// adapter does not build a task for, so this branch is unreachable.
|
|
319
|
+
(() => {
|
|
320
|
+
throw new TypeError(`2captcha resolver does not support ${challenge.kind}`);
|
|
321
|
+
})();
|
|
289
322
|
const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), { clientKey: apiKey, task }, solveController.signal, phase, [apiKey]);
|
|
290
323
|
const taskId = taskIdFrom(createResult.payload);
|
|
291
324
|
if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
|
|
@@ -2,7 +2,7 @@ import type { ChallengeSolution, ProviderChallenge, ProviderChallengeKind, Provi
|
|
|
2
2
|
import type { TraceRecorder } from "../trace.js";
|
|
3
3
|
export declare const RESOLVER_VENDOR_CAPABILITIES: {
|
|
4
4
|
readonly browser: readonly ["aws_waf", "cloudflare_interstitial"];
|
|
5
|
-
readonly "2captcha": readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "
|
|
5
|
+
readonly "2captcha": readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "aws_waf"];
|
|
6
6
|
readonly capsolver: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf"];
|
|
7
7
|
readonly capmonster: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha"];
|
|
8
8
|
readonly custom: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf", "akamai_sec_cpt", "akamai_sensor"];
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
export const RESOLVER_VENDOR_CAPABILITIES = {
|
|
2
2
|
browser: ["aws_waf", "cloudflare_interstitial"],
|
|
3
|
+
// Every kind listed per vendor is implemented by that vendor's adapter; the
|
|
4
|
+
// per-adapter "agrees with every declared capability" tests iterate this
|
|
5
|
+
// table, so adding a kind here without an implementation fails the suite.
|
|
6
|
+
// 2captcha omits `cloudflare_interstitial`, `akamai_sec_cpt`, and
|
|
7
|
+
// `akamai_sensor`: their API offers no task type for them, so declaring them
|
|
8
|
+
// would route challenges to a vendor that can only refuse.
|
|
3
9
|
"2captcha": [
|
|
4
10
|
"turnstile",
|
|
5
11
|
"recaptcha_v2",
|
|
6
12
|
"recaptcha_v3",
|
|
7
13
|
"hcaptcha",
|
|
8
|
-
"cloudflare_interstitial",
|
|
9
14
|
"aws_waf",
|
|
10
|
-
"akamai_sec_cpt",
|
|
11
|
-
"akamai_sensor",
|
|
12
15
|
],
|
|
13
16
|
capsolver: [
|
|
14
17
|
"turnstile",
|
|
@@ -34,7 +34,7 @@ export type ResolverInstrumentationMetadata = {
|
|
|
34
34
|
readonly target: ResolverContext;
|
|
35
35
|
readonly traceRecorder: TraceRecorder;
|
|
36
36
|
};
|
|
37
|
-
export type ResolverAdapterFactory = (configuration: string, timeoutMs: number, allowedHosts: readonly string[]) => ResolverVendorAdapter;
|
|
37
|
+
export type ResolverAdapterFactory = (configuration: string | undefined, timeoutMs: number, allowedHosts: readonly string[]) => ResolverVendorAdapter;
|
|
38
38
|
export declare const RESOLVER_ADAPTER_REGISTRY: Partial<Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>>;
|
|
39
39
|
export declare function swapResolverAdapterFactoryForTests(vendor: ProviderResolverVendor, factory: ResolverAdapterFactory | undefined): () => void;
|
|
40
40
|
/** Internal test seam; deliberately not re-exported from the package root. */
|