@apifuse/provider-sdk 2.2.0-beta.31 → 2.2.0-beta.32
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 +4 -0
- package/dist/runtime/resolver-vendors/capsolver.d.ts +24 -0
- package/dist/runtime/resolver-vendors/capsolver.js +402 -0
- package/dist/runtime/resolver.js +8 -1
- package/package.json +1 -1
- package/src/runtime/resolver-vendors/capsolver.ts +563 -0
- package/src/runtime/resolver.ts +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ChallengeSolution, ProviderChallenge } from "../../types.js";
|
|
2
|
+
import type { TraceRecorder } from "../trace.js";
|
|
3
|
+
import { type ResolverIdentity, type ResolverVendorAdapter } from "./types.js";
|
|
4
|
+
type Delay = (ms: number, signal: AbortSignal) => Promise<void>;
|
|
5
|
+
export interface CapsolverResolverVendorOptions {
|
|
6
|
+
readonly apiKey?: string;
|
|
7
|
+
readonly timeoutMs?: number;
|
|
8
|
+
readonly pollIntervalMs?: number;
|
|
9
|
+
readonly allowedHosts: readonly string[];
|
|
10
|
+
readonly fetchImpl?: typeof fetch;
|
|
11
|
+
readonly baseUrl?: string;
|
|
12
|
+
/** Test-only clock override; supplying it disables the real-time deadline timer. */
|
|
13
|
+
readonly now?: () => number;
|
|
14
|
+
/** Test-only delay override used with `now` to exercise polling without sleeping. */
|
|
15
|
+
readonly delay?: Delay;
|
|
16
|
+
}
|
|
17
|
+
export interface CapsolverResolverVendorAdapter extends ResolverVendorAdapter {
|
|
18
|
+
readonly id: "capsolver";
|
|
19
|
+
solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder): Promise<Extract<ChallengeSolution, {
|
|
20
|
+
readonly form: "token";
|
|
21
|
+
}>>;
|
|
22
|
+
}
|
|
23
|
+
export declare function createCapsolverResolverVendorAdapter(options: CapsolverResolverVendorOptions): CapsolverResolverVendorAdapter;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { redactSensitiveText } from "../request-options.js";
|
|
2
|
+
import { assertResolverHostAllowed } from "./hosts.js";
|
|
3
|
+
import { ResolverChallengeVerdictError, ResolverVendorUnavailableError, resolverVendorSupports, } from "./types.js";
|
|
4
|
+
const CAPSOLVER_VENDOR_ID = "capsolver";
|
|
5
|
+
const DEFAULT_CAPSOLVER_BASE_URL = "https://api.capsolver.com";
|
|
6
|
+
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
8
|
+
class CapsolverSolveTimeoutError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super("Capsolver resolver solve budget elapsed");
|
|
11
|
+
this.name = "CapsolverSolveTimeoutError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
class CapsolverUnavailableError extends ResolverVendorUnavailableError {
|
|
15
|
+
errorCode;
|
|
16
|
+
errorDescription;
|
|
17
|
+
responseStatus;
|
|
18
|
+
responseContentType;
|
|
19
|
+
responseLength;
|
|
20
|
+
constructor(reason, options) {
|
|
21
|
+
super(CAPSOLVER_VENDOR_ID, reason, options);
|
|
22
|
+
this.errorCode = options.errorCode;
|
|
23
|
+
this.errorDescription = options.errorDescription;
|
|
24
|
+
this.responseStatus = options.responseStatus;
|
|
25
|
+
this.responseContentType = options.responseContentType;
|
|
26
|
+
this.responseLength = options.responseLength;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
class CapsolverVerdictError extends ResolverChallengeVerdictError {
|
|
30
|
+
phase;
|
|
31
|
+
errorCode;
|
|
32
|
+
errorDescription;
|
|
33
|
+
constructor(options) {
|
|
34
|
+
super(CAPSOLVER_VENDOR_ID, "solve_failed", options);
|
|
35
|
+
this.phase = options.phase;
|
|
36
|
+
this.errorCode = options.errorCode;
|
|
37
|
+
this.errorDescription = options.errorDescription;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function isJsonRecord(value) {
|
|
41
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
function abortReason(signal) {
|
|
44
|
+
return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
45
|
+
}
|
|
46
|
+
function containsSensitiveValue(value, sensitiveValues) {
|
|
47
|
+
const secrets = sensitiveValues.filter((secret) => secret.length > 0);
|
|
48
|
+
if (secrets.length === 0)
|
|
49
|
+
return false;
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
const inspect = (candidate) => {
|
|
52
|
+
if (typeof candidate === "string") {
|
|
53
|
+
return secrets.some((secret) => candidate.includes(secret));
|
|
54
|
+
}
|
|
55
|
+
if (candidate === null || (typeof candidate !== "object" && typeof candidate !== "function")) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (seen.has(candidate))
|
|
59
|
+
return false;
|
|
60
|
+
seen.add(candidate);
|
|
61
|
+
try {
|
|
62
|
+
for (const property of Reflect.ownKeys(candidate)) {
|
|
63
|
+
if (typeof property === "string" && inspect(property))
|
|
64
|
+
return true;
|
|
65
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, property);
|
|
66
|
+
if (!descriptor)
|
|
67
|
+
return true;
|
|
68
|
+
if ("value" in descriptor && inspect(descriptor.value))
|
|
69
|
+
return true;
|
|
70
|
+
if (descriptor.get !== undefined || descriptor.set !== undefined)
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
};
|
|
79
|
+
return inspect(value);
|
|
80
|
+
}
|
|
81
|
+
function safeCauseOptions(error, sensitiveValues) {
|
|
82
|
+
return containsSensitiveValue(error, sensitiveValues) ? {} : { cause: error };
|
|
83
|
+
}
|
|
84
|
+
function sanitizedJsonParseCause() {
|
|
85
|
+
return new SyntaxError("Upstream response failed");
|
|
86
|
+
}
|
|
87
|
+
function raceWithAbort(operation, signal, phase, sensitiveValues = []) {
|
|
88
|
+
if (signal.aborted)
|
|
89
|
+
return Promise.reject(abortReason(signal));
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
92
|
+
const onAbort = () => {
|
|
93
|
+
cleanup();
|
|
94
|
+
reject(abortReason(signal));
|
|
95
|
+
};
|
|
96
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
97
|
+
operation().then((value) => {
|
|
98
|
+
cleanup();
|
|
99
|
+
resolve(value);
|
|
100
|
+
}, (error) => {
|
|
101
|
+
cleanup();
|
|
102
|
+
if (phase === undefined) {
|
|
103
|
+
reject(error);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
reject(new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
107
|
+
...safeCauseOptions(error, sensitiveValues),
|
|
108
|
+
phase,
|
|
109
|
+
}));
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async function abortableDelay(ms, signal) {
|
|
114
|
+
let timer;
|
|
115
|
+
try {
|
|
116
|
+
await raceWithAbort(() => new Promise((resolve) => {
|
|
117
|
+
timer = setTimeout(resolve, ms);
|
|
118
|
+
}), signal);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
if (timer !== undefined)
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function responseErrorFields(payload) {
|
|
126
|
+
return {
|
|
127
|
+
errorId: typeof payload.errorId === "number" ? payload.errorId : undefined,
|
|
128
|
+
errorCode: typeof payload.errorCode === "string" ? payload.errorCode : undefined,
|
|
129
|
+
errorDescription: typeof payload.errorDescription === "string" ? payload.errorDescription : undefined,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function parseCreateTaskResponse(payload) {
|
|
133
|
+
const taskId = payload.taskId;
|
|
134
|
+
return {
|
|
135
|
+
...responseErrorFields(payload),
|
|
136
|
+
taskId: typeof taskId === "string" || typeof taskId === "number" ? taskId : undefined,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function parsePollResultResponse(payload) {
|
|
140
|
+
const solution = isJsonRecord(payload.solution) ? payload.solution : undefined;
|
|
141
|
+
return {
|
|
142
|
+
...responseErrorFields(payload),
|
|
143
|
+
status: typeof payload.status === "string" ? payload.status : undefined,
|
|
144
|
+
solution: solution
|
|
145
|
+
? { token: typeof solution.token === "string" ? solution.token : undefined }
|
|
146
|
+
: undefined,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function isAllocationExhausted(payload) {
|
|
150
|
+
const code = payload.errorCode?.toLowerCase() ?? "";
|
|
151
|
+
const description = payload.errorDescription?.toLowerCase() ?? "";
|
|
152
|
+
return (code === "error_zero_balance" ||
|
|
153
|
+
/(?:insufficient|zero|no|not enough)\s+(?:balance|funds|credit)/u.test(`${code} ${description}`));
|
|
154
|
+
}
|
|
155
|
+
function isNegativeVerdict(payload) {
|
|
156
|
+
return payload.errorCode?.toLowerCase() === "error_captcha_unsolvable";
|
|
157
|
+
}
|
|
158
|
+
function safeVendorDetail(value, sensitiveValues) {
|
|
159
|
+
const redacted = redactSensitiveText(value, sensitiveValues);
|
|
160
|
+
return containsSensitiveValue(redacted, sensitiveValues) ? "[REDACTED]" : redacted;
|
|
161
|
+
}
|
|
162
|
+
function vendorErrorDetails(payload, sensitiveValues) {
|
|
163
|
+
const errorCode = payload.errorCode?.trim();
|
|
164
|
+
const errorDescription = payload.errorDescription?.trim();
|
|
165
|
+
return {
|
|
166
|
+
...(errorCode ? { errorCode: safeVendorDetail(errorCode, sensitiveValues) } : {}),
|
|
167
|
+
...(errorDescription
|
|
168
|
+
? { errorDescription: safeVendorDetail(payload.errorDescription ?? "", sensitiveValues) }
|
|
169
|
+
: {}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function unavailableForPayload(payload, phase, sensitiveValues) {
|
|
173
|
+
const details = vendorErrorDetails(payload, sensitiveValues);
|
|
174
|
+
if (isNegativeVerdict(payload)) {
|
|
175
|
+
return new CapsolverVerdictError({ phase, ...details });
|
|
176
|
+
}
|
|
177
|
+
return new CapsolverUnavailableError(isAllocationExhausted(payload) ? "allocation_exhausted" : "transport_failure", { phase, ...details });
|
|
178
|
+
}
|
|
179
|
+
async function postJson(fetchImpl, url, body, signal, phase, sensitiveValues, parseResponse) {
|
|
180
|
+
const response = await raceWithAbort(() => fetchImpl(url, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: { "content-type": "application/json" },
|
|
183
|
+
body: JSON.stringify(body),
|
|
184
|
+
signal,
|
|
185
|
+
redirect: "error",
|
|
186
|
+
}), signal, phase, sensitiveValues);
|
|
187
|
+
let responseText;
|
|
188
|
+
try {
|
|
189
|
+
responseText = await raceWithAbort(() => response.text(), signal, phase, sensitiveValues);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (error instanceof ResolverVendorUnavailableError)
|
|
193
|
+
throw error;
|
|
194
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
195
|
+
cause: error,
|
|
196
|
+
phase,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
let payload;
|
|
200
|
+
try {
|
|
201
|
+
payload = JSON.parse(responseText);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
const contentType = response.headers.get("content-type");
|
|
205
|
+
throw new CapsolverUnavailableError("transport_failure", {
|
|
206
|
+
cause: sanitizedJsonParseCause(),
|
|
207
|
+
phase,
|
|
208
|
+
responseStatus: response.status,
|
|
209
|
+
responseContentType: contentType
|
|
210
|
+
? safeVendorDetail(contentType.slice(0, 128), sensitiveValues)
|
|
211
|
+
: undefined,
|
|
212
|
+
responseLength: responseText.length,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (!isJsonRecord(payload)) {
|
|
216
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
217
|
+
phase,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return { ok: response.ok, payload: parseResponse(payload) };
|
|
221
|
+
}
|
|
222
|
+
function endpoint(baseUrl, path) {
|
|
223
|
+
return `${baseUrl.replace(/\/+$/u, "")}/${path}`;
|
|
224
|
+
}
|
|
225
|
+
function spanErrorAttributes(error, phase) {
|
|
226
|
+
if (error instanceof ResolverVendorUnavailableError) {
|
|
227
|
+
return {
|
|
228
|
+
unavailability_reason: error.reason,
|
|
229
|
+
transport_phase: error.phase,
|
|
230
|
+
...(error instanceof CapsolverUnavailableError
|
|
231
|
+
? {
|
|
232
|
+
vendor_error_code: error.errorCode,
|
|
233
|
+
vendor_error_description: error.errorDescription,
|
|
234
|
+
response_status: error.responseStatus,
|
|
235
|
+
response_content_type: error.responseContentType,
|
|
236
|
+
response_length: error.responseLength,
|
|
237
|
+
}
|
|
238
|
+
: {}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (error instanceof CapsolverVerdictError) {
|
|
242
|
+
return {
|
|
243
|
+
verdict_reason: error.reason,
|
|
244
|
+
transport_phase: error.phase,
|
|
245
|
+
vendor_error_code: error.errorCode,
|
|
246
|
+
vendor_error_description: error.errorDescription,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (error instanceof CapsolverSolveTimeoutError) {
|
|
250
|
+
return {
|
|
251
|
+
unavailability_reason: "timeout",
|
|
252
|
+
transport_phase: phase,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
export function createCapsolverResolverVendorAdapter(options) {
|
|
258
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
259
|
+
const baseUrl = options.baseUrl ?? DEFAULT_CAPSOLVER_BASE_URL;
|
|
260
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
261
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
262
|
+
const now = options.now ?? Date.now;
|
|
263
|
+
const delay = options.delay ?? abortableDelay;
|
|
264
|
+
return {
|
|
265
|
+
id: CAPSOLVER_VENDOR_ID,
|
|
266
|
+
supports(kind) {
|
|
267
|
+
return resolverVendorSupports(CAPSOLVER_VENDOR_ID, kind);
|
|
268
|
+
},
|
|
269
|
+
async solve(challenge, _identity, callerSignal, traceRecorder) {
|
|
270
|
+
const apiKey = options.apiKey?.trim();
|
|
271
|
+
if (!apiKey) {
|
|
272
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "missing_credentials", {
|
|
273
|
+
phase: "create_task",
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (!resolverVendorSupports(CAPSOLVER_VENDOR_ID, challenge.kind)) {
|
|
277
|
+
throw new TypeError(`Capsolver resolver does not support ${challenge.kind}`);
|
|
278
|
+
}
|
|
279
|
+
if (challenge.kind !== "turnstile") {
|
|
280
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "not_implemented", {
|
|
281
|
+
phase: "create_task",
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const sensitiveValues = [
|
|
285
|
+
apiKey,
|
|
286
|
+
challenge.pageUrl,
|
|
287
|
+
challenge.siteKey,
|
|
288
|
+
...(challenge.action !== undefined ? [challenge.action] : []),
|
|
289
|
+
...(challenge.cdata !== undefined ? [challenge.cdata] : []),
|
|
290
|
+
];
|
|
291
|
+
assertResolverHostAllowed(challenge.pageUrl, options.allowedHosts);
|
|
292
|
+
callerSignal.throwIfAborted();
|
|
293
|
+
const solveController = new AbortController();
|
|
294
|
+
const onCallerAbort = () => solveController.abort(abortReason(callerSignal));
|
|
295
|
+
callerSignal.addEventListener("abort", onCallerAbort, { once: true });
|
|
296
|
+
const timeout = options.now
|
|
297
|
+
? undefined
|
|
298
|
+
: setTimeout(() => solveController.abort(new CapsolverSolveTimeoutError()), timeoutMs);
|
|
299
|
+
const startedAt = now();
|
|
300
|
+
let phase = "create_task";
|
|
301
|
+
try {
|
|
302
|
+
const createTask = async () => {
|
|
303
|
+
const metadata = {
|
|
304
|
+
...(challenge.action !== undefined ? { action: challenge.action } : {}),
|
|
305
|
+
...(challenge.cdata !== undefined ? { cdata: challenge.cdata } : {}),
|
|
306
|
+
};
|
|
307
|
+
const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), {
|
|
308
|
+
clientKey: apiKey,
|
|
309
|
+
task: {
|
|
310
|
+
type: "AntiTurnstileTaskProxyLess",
|
|
311
|
+
websiteURL: challenge.pageUrl,
|
|
312
|
+
websiteKey: challenge.siteKey,
|
|
313
|
+
...(challenge.action !== undefined || challenge.cdata !== undefined
|
|
314
|
+
? { metadata }
|
|
315
|
+
: {}),
|
|
316
|
+
},
|
|
317
|
+
}, solveController.signal, phase, sensitiveValues, parseCreateTaskResponse);
|
|
318
|
+
const taskId = createResult.payload.taskId;
|
|
319
|
+
if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
|
|
320
|
+
throw unavailableForPayload(createResult.payload, phase, sensitiveValues);
|
|
321
|
+
}
|
|
322
|
+
return taskId;
|
|
323
|
+
};
|
|
324
|
+
const taskId = traceRecorder
|
|
325
|
+
? await traceRecorder.runSpan("resolver.vendor.create_task", createTask, {
|
|
326
|
+
attributes: {
|
|
327
|
+
vendor: CAPSOLVER_VENDOR_ID,
|
|
328
|
+
challenge_kind: challenge.kind,
|
|
329
|
+
},
|
|
330
|
+
onError: (error) => spanErrorAttributes(error, "create_task"),
|
|
331
|
+
})
|
|
332
|
+
: await createTask();
|
|
333
|
+
phase = "poll_result";
|
|
334
|
+
const pollResult = async () => {
|
|
335
|
+
while (true) {
|
|
336
|
+
callerSignal.throwIfAborted();
|
|
337
|
+
const remainingMs = timeoutMs - (now() - startedAt);
|
|
338
|
+
if (remainingMs <= 0)
|
|
339
|
+
throw new CapsolverSolveTimeoutError();
|
|
340
|
+
await delay(Math.min(pollIntervalMs, remainingMs), solveController.signal);
|
|
341
|
+
callerSignal.throwIfAborted();
|
|
342
|
+
if (now() - startedAt >= timeoutMs)
|
|
343
|
+
throw new CapsolverSolveTimeoutError();
|
|
344
|
+
const result = await postJson(fetchImpl, endpoint(baseUrl, "getTaskResult"), { clientKey: apiKey, taskId }, solveController.signal, phase, sensitiveValues, parsePollResultResponse);
|
|
345
|
+
if (!result.ok || result.payload.errorId !== 0) {
|
|
346
|
+
throw unavailableForPayload(result.payload, phase, sensitiveValues);
|
|
347
|
+
}
|
|
348
|
+
switch (result.payload.status) {
|
|
349
|
+
case "idle":
|
|
350
|
+
case "processing":
|
|
351
|
+
continue;
|
|
352
|
+
case "ready":
|
|
353
|
+
break;
|
|
354
|
+
default:
|
|
355
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", { phase });
|
|
356
|
+
}
|
|
357
|
+
const token = result.payload.solution?.token;
|
|
358
|
+
if (!token?.trim()) {
|
|
359
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
360
|
+
phase,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
return { form: "token", token };
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
return traceRecorder
|
|
367
|
+
? await traceRecorder.runSpan("resolver.vendor.poll_result", pollResult, {
|
|
368
|
+
attributes: {
|
|
369
|
+
vendor: CAPSOLVER_VENDOR_ID,
|
|
370
|
+
challenge_kind: challenge.kind,
|
|
371
|
+
},
|
|
372
|
+
onError: (error) => spanErrorAttributes(error, "poll_result"),
|
|
373
|
+
})
|
|
374
|
+
: await pollResult();
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
if (callerSignal.aborted)
|
|
378
|
+
throw abortReason(callerSignal);
|
|
379
|
+
if (error instanceof CapsolverSolveTimeoutError ||
|
|
380
|
+
solveController.signal.reason instanceof CapsolverSolveTimeoutError) {
|
|
381
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "timeout", {
|
|
382
|
+
cause: error,
|
|
383
|
+
phase,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
if (error instanceof ResolverVendorUnavailableError ||
|
|
387
|
+
error instanceof ResolverChallengeVerdictError) {
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
391
|
+
...safeCauseOptions(error, sensitiveValues),
|
|
392
|
+
phase,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
finally {
|
|
396
|
+
if (timeout !== undefined)
|
|
397
|
+
clearTimeout(timeout);
|
|
398
|
+
callerSignal.removeEventListener("abort", onCallerAbort);
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
package/dist/runtime/resolver.js
CHANGED
|
@@ -4,6 +4,7 @@ import { ProviderError } from "../errors.js";
|
|
|
4
4
|
import { getStealthProfile } from "../stealth/profiles.js";
|
|
5
5
|
import { resolverChallengeAllowsDirectCache, resolverChallengeIsCacheable, resolverChallengeIsIdentityScoped, resolverChallengeIssuingIdentity, } from "./resolver-vendors/bindings.js";
|
|
6
6
|
import { createBrowserResolverVendorAdapter } from "./resolver-vendors/browser.js";
|
|
7
|
+
import { createCapsolverResolverVendorAdapter } from "./resolver-vendors/capsolver.js";
|
|
7
8
|
import { assertResolverHostAllowed } from "./resolver-vendors/hosts.js";
|
|
8
9
|
import { createTwoCaptchaResolverVendorAdapter } from "./resolver-vendors/twocaptcha.js";
|
|
9
10
|
import { RESOLVER_VENDOR_CAPABILITIES, ResolverVendorUnavailableError, resolverVendorSupports, } from "./resolver-vendors/types.js";
|
|
@@ -61,6 +62,13 @@ const resolverAdapterRegistry = {
|
|
|
61
62
|
timeoutMs,
|
|
62
63
|
});
|
|
63
64
|
},
|
|
65
|
+
capsolver(configuration, timeoutMs, allowedHosts) {
|
|
66
|
+
return createCapsolverResolverVendorAdapter({
|
|
67
|
+
allowedHosts,
|
|
68
|
+
apiKey: configuration,
|
|
69
|
+
timeoutMs,
|
|
70
|
+
});
|
|
71
|
+
},
|
|
64
72
|
browser(configuration, timeoutMs, allowedHosts) {
|
|
65
73
|
return createBrowserResolverVendorAdapter({
|
|
66
74
|
allowedHosts,
|
|
@@ -104,7 +112,6 @@ export function swapResolverDefaultUserAgentForTests(resolver) {
|
|
|
104
112
|
// This is the sole allowlist for declared vendors whose registry entry may be absent.
|
|
105
113
|
// Remove a vendor here when its adapter is registered.
|
|
106
114
|
const KNOWN_UNIMPLEMENTED_RESOLVER_VENDORS = new Set([
|
|
107
|
-
"capsolver",
|
|
108
115
|
"capmonster",
|
|
109
116
|
"custom",
|
|
110
117
|
]);
|
package/package.json
CHANGED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
import type { ChallengeSolution, ProviderChallenge } from "../../types.js";
|
|
2
|
+
import { redactSensitiveText } from "../request-options.js";
|
|
3
|
+
import type { TraceRecorder } from "../trace.js";
|
|
4
|
+
import { assertResolverHostAllowed } from "./hosts.js";
|
|
5
|
+
import {
|
|
6
|
+
ResolverChallengeVerdictError,
|
|
7
|
+
type ResolverIdentity,
|
|
8
|
+
type ResolverVendorAdapter,
|
|
9
|
+
ResolverVendorUnavailableError,
|
|
10
|
+
type ResolverVendorUnavailableReason,
|
|
11
|
+
resolverVendorSupports,
|
|
12
|
+
} from "./types.js";
|
|
13
|
+
|
|
14
|
+
const CAPSOLVER_VENDOR_ID = "capsolver" as const;
|
|
15
|
+
const DEFAULT_CAPSOLVER_BASE_URL = "https://api.capsolver.com";
|
|
16
|
+
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
17
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
18
|
+
|
|
19
|
+
type Delay = (ms: number, signal: AbortSignal) => Promise<void>;
|
|
20
|
+
type CapsolverOperationPhase = "create_task" | "poll_result";
|
|
21
|
+
|
|
22
|
+
export interface CapsolverResolverVendorOptions {
|
|
23
|
+
readonly apiKey?: string;
|
|
24
|
+
readonly timeoutMs?: number;
|
|
25
|
+
readonly pollIntervalMs?: number;
|
|
26
|
+
readonly allowedHosts: readonly string[];
|
|
27
|
+
readonly fetchImpl?: typeof fetch;
|
|
28
|
+
readonly baseUrl?: string;
|
|
29
|
+
/** Test-only clock override; supplying it disables the real-time deadline timer. */
|
|
30
|
+
readonly now?: () => number;
|
|
31
|
+
/** Test-only delay override used with `now` to exercise polling without sleeping. */
|
|
32
|
+
readonly delay?: Delay;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CapsolverResolverVendorAdapter extends ResolverVendorAdapter {
|
|
36
|
+
readonly id: "capsolver";
|
|
37
|
+
solve(
|
|
38
|
+
challenge: ProviderChallenge,
|
|
39
|
+
identity: ResolverIdentity | undefined,
|
|
40
|
+
signal: AbortSignal,
|
|
41
|
+
traceRecorder?: TraceRecorder,
|
|
42
|
+
): Promise<Extract<ChallengeSolution, { readonly form: "token" }>>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class CapsolverSolveTimeoutError extends Error {
|
|
46
|
+
constructor() {
|
|
47
|
+
super("Capsolver resolver solve budget elapsed");
|
|
48
|
+
this.name = "CapsolverSolveTimeoutError";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class CapsolverUnavailableError extends ResolverVendorUnavailableError {
|
|
53
|
+
readonly errorCode: string | undefined;
|
|
54
|
+
readonly errorDescription: string | undefined;
|
|
55
|
+
readonly responseStatus: number | undefined;
|
|
56
|
+
readonly responseContentType: string | undefined;
|
|
57
|
+
readonly responseLength: number | undefined;
|
|
58
|
+
|
|
59
|
+
constructor(
|
|
60
|
+
reason: ResolverVendorUnavailableReason,
|
|
61
|
+
options: {
|
|
62
|
+
readonly phase: CapsolverOperationPhase;
|
|
63
|
+
readonly cause?: unknown;
|
|
64
|
+
readonly errorCode?: string;
|
|
65
|
+
readonly errorDescription?: string;
|
|
66
|
+
readonly responseStatus?: number;
|
|
67
|
+
readonly responseContentType?: string;
|
|
68
|
+
readonly responseLength?: number;
|
|
69
|
+
},
|
|
70
|
+
) {
|
|
71
|
+
super(CAPSOLVER_VENDOR_ID, reason, options);
|
|
72
|
+
this.errorCode = options.errorCode;
|
|
73
|
+
this.errorDescription = options.errorDescription;
|
|
74
|
+
this.responseStatus = options.responseStatus;
|
|
75
|
+
this.responseContentType = options.responseContentType;
|
|
76
|
+
this.responseLength = options.responseLength;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type JsonRecord = Record<string, unknown>;
|
|
81
|
+
|
|
82
|
+
interface CapsolverResponseErrorFields {
|
|
83
|
+
readonly errorId: number | undefined;
|
|
84
|
+
readonly errorCode: string | undefined;
|
|
85
|
+
readonly errorDescription: string | undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface CapsolverCreateTaskResponse extends CapsolverResponseErrorFields {
|
|
89
|
+
readonly taskId: string | number | undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface CapsolverPollResultResponse extends CapsolverResponseErrorFields {
|
|
93
|
+
readonly status: string | undefined;
|
|
94
|
+
readonly solution: { readonly token: string | undefined } | undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
class CapsolverVerdictError extends ResolverChallengeVerdictError {
|
|
98
|
+
readonly phase: CapsolverOperationPhase;
|
|
99
|
+
readonly errorCode: string | undefined;
|
|
100
|
+
readonly errorDescription: string | undefined;
|
|
101
|
+
|
|
102
|
+
constructor(options: {
|
|
103
|
+
readonly phase: CapsolverOperationPhase;
|
|
104
|
+
readonly errorCode?: string;
|
|
105
|
+
readonly errorDescription?: string;
|
|
106
|
+
}) {
|
|
107
|
+
super(CAPSOLVER_VENDOR_ID, "solve_failed", options);
|
|
108
|
+
this.phase = options.phase;
|
|
109
|
+
this.errorCode = options.errorCode;
|
|
110
|
+
this.errorDescription = options.errorDescription;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isJsonRecord(value: unknown): value is JsonRecord {
|
|
115
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function abortReason(signal: AbortSignal): unknown {
|
|
119
|
+
return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function containsSensitiveValue(value: unknown, sensitiveValues: readonly string[]): boolean {
|
|
123
|
+
const secrets = sensitiveValues.filter((secret) => secret.length > 0);
|
|
124
|
+
if (secrets.length === 0) return false;
|
|
125
|
+
const seen = new Set<object>();
|
|
126
|
+
|
|
127
|
+
const inspect = (candidate: unknown): boolean => {
|
|
128
|
+
if (typeof candidate === "string") {
|
|
129
|
+
return secrets.some((secret) => candidate.includes(secret));
|
|
130
|
+
}
|
|
131
|
+
if (candidate === null || (typeof candidate !== "object" && typeof candidate !== "function")) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (seen.has(candidate)) return false;
|
|
135
|
+
seen.add(candidate);
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
for (const property of Reflect.ownKeys(candidate)) {
|
|
139
|
+
if (typeof property === "string" && inspect(property)) return true;
|
|
140
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, property);
|
|
141
|
+
if (!descriptor) return true;
|
|
142
|
+
if ("value" in descriptor && inspect(descriptor.value)) return true;
|
|
143
|
+
if (descriptor.get !== undefined || descriptor.set !== undefined) return true;
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return inspect(value);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function safeCauseOptions(
|
|
155
|
+
error: unknown,
|
|
156
|
+
sensitiveValues: readonly string[],
|
|
157
|
+
): { readonly cause?: unknown } {
|
|
158
|
+
return containsSensitiveValue(error, sensitiveValues) ? {} : { cause: error };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function sanitizedJsonParseCause(): SyntaxError {
|
|
162
|
+
return new SyntaxError("Upstream response failed");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function raceWithAbort<T>(
|
|
166
|
+
operation: () => Promise<T>,
|
|
167
|
+
signal: AbortSignal,
|
|
168
|
+
phase?: CapsolverOperationPhase,
|
|
169
|
+
sensitiveValues: readonly string[] = [],
|
|
170
|
+
): Promise<T> {
|
|
171
|
+
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
172
|
+
|
|
173
|
+
return new Promise<T>((resolve, reject) => {
|
|
174
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
175
|
+
const onAbort = () => {
|
|
176
|
+
cleanup();
|
|
177
|
+
reject(abortReason(signal));
|
|
178
|
+
};
|
|
179
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
180
|
+
operation().then(
|
|
181
|
+
(value) => {
|
|
182
|
+
cleanup();
|
|
183
|
+
resolve(value);
|
|
184
|
+
},
|
|
185
|
+
(error: unknown) => {
|
|
186
|
+
cleanup();
|
|
187
|
+
if (phase === undefined) {
|
|
188
|
+
reject(error);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
reject(
|
|
192
|
+
new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
193
|
+
...safeCauseOptions(error, sensitiveValues),
|
|
194
|
+
phase,
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function abortableDelay(ms: number, signal: AbortSignal): Promise<void> {
|
|
203
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
204
|
+
try {
|
|
205
|
+
await raceWithAbort(
|
|
206
|
+
() =>
|
|
207
|
+
new Promise<void>((resolve) => {
|
|
208
|
+
timer = setTimeout(resolve, ms);
|
|
209
|
+
}),
|
|
210
|
+
signal,
|
|
211
|
+
);
|
|
212
|
+
} finally {
|
|
213
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function responseErrorFields(payload: JsonRecord): CapsolverResponseErrorFields {
|
|
218
|
+
return {
|
|
219
|
+
errorId: typeof payload.errorId === "number" ? payload.errorId : undefined,
|
|
220
|
+
errorCode: typeof payload.errorCode === "string" ? payload.errorCode : undefined,
|
|
221
|
+
errorDescription:
|
|
222
|
+
typeof payload.errorDescription === "string" ? payload.errorDescription : undefined,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseCreateTaskResponse(payload: JsonRecord): CapsolverCreateTaskResponse {
|
|
227
|
+
const taskId = payload.taskId;
|
|
228
|
+
return {
|
|
229
|
+
...responseErrorFields(payload),
|
|
230
|
+
taskId: typeof taskId === "string" || typeof taskId === "number" ? taskId : undefined,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function parsePollResultResponse(payload: JsonRecord): CapsolverPollResultResponse {
|
|
235
|
+
const solution = isJsonRecord(payload.solution) ? payload.solution : undefined;
|
|
236
|
+
return {
|
|
237
|
+
...responseErrorFields(payload),
|
|
238
|
+
status: typeof payload.status === "string" ? payload.status : undefined,
|
|
239
|
+
solution: solution
|
|
240
|
+
? { token: typeof solution.token === "string" ? solution.token : undefined }
|
|
241
|
+
: undefined,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isAllocationExhausted(payload: CapsolverResponseErrorFields): boolean {
|
|
246
|
+
const code = payload.errorCode?.toLowerCase() ?? "";
|
|
247
|
+
const description = payload.errorDescription?.toLowerCase() ?? "";
|
|
248
|
+
return (
|
|
249
|
+
code === "error_zero_balance" ||
|
|
250
|
+
/(?:insufficient|zero|no|not enough)\s+(?:balance|funds|credit)/u.test(`${code} ${description}`)
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function isNegativeVerdict(payload: CapsolverResponseErrorFields): boolean {
|
|
255
|
+
return payload.errorCode?.toLowerCase() === "error_captcha_unsolvable";
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function safeVendorDetail(value: string, sensitiveValues: readonly string[]): string {
|
|
259
|
+
const redacted = redactSensitiveText(value, sensitiveValues);
|
|
260
|
+
return containsSensitiveValue(redacted, sensitiveValues) ? "[REDACTED]" : redacted;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function vendorErrorDetails(
|
|
264
|
+
payload: CapsolverResponseErrorFields,
|
|
265
|
+
sensitiveValues: readonly string[],
|
|
266
|
+
): { readonly errorCode?: string; readonly errorDescription?: string } {
|
|
267
|
+
const errorCode = payload.errorCode?.trim();
|
|
268
|
+
const errorDescription = payload.errorDescription?.trim();
|
|
269
|
+
return {
|
|
270
|
+
...(errorCode ? { errorCode: safeVendorDetail(errorCode, sensitiveValues) } : {}),
|
|
271
|
+
...(errorDescription
|
|
272
|
+
? { errorDescription: safeVendorDetail(payload.errorDescription ?? "", sensitiveValues) }
|
|
273
|
+
: {}),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function unavailableForPayload(
|
|
278
|
+
payload: CapsolverResponseErrorFields,
|
|
279
|
+
phase: CapsolverOperationPhase,
|
|
280
|
+
sensitiveValues: readonly string[],
|
|
281
|
+
): ResolverVendorUnavailableError | ResolverChallengeVerdictError {
|
|
282
|
+
const details = vendorErrorDetails(payload, sensitiveValues);
|
|
283
|
+
if (isNegativeVerdict(payload)) {
|
|
284
|
+
return new CapsolverVerdictError({ phase, ...details });
|
|
285
|
+
}
|
|
286
|
+
return new CapsolverUnavailableError(
|
|
287
|
+
isAllocationExhausted(payload) ? "allocation_exhausted" : "transport_failure",
|
|
288
|
+
{ phase, ...details },
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function postJson<TResponse>(
|
|
293
|
+
fetchImpl: typeof fetch,
|
|
294
|
+
url: string,
|
|
295
|
+
body: JsonRecord,
|
|
296
|
+
signal: AbortSignal,
|
|
297
|
+
phase: CapsolverOperationPhase,
|
|
298
|
+
sensitiveValues: readonly string[],
|
|
299
|
+
parseResponse: (payload: JsonRecord) => TResponse,
|
|
300
|
+
): Promise<{ readonly ok: boolean; readonly payload: TResponse }> {
|
|
301
|
+
const response = await raceWithAbort(
|
|
302
|
+
() =>
|
|
303
|
+
fetchImpl(url, {
|
|
304
|
+
method: "POST",
|
|
305
|
+
headers: { "content-type": "application/json" },
|
|
306
|
+
body: JSON.stringify(body),
|
|
307
|
+
signal,
|
|
308
|
+
redirect: "error",
|
|
309
|
+
}),
|
|
310
|
+
signal,
|
|
311
|
+
phase,
|
|
312
|
+
sensitiveValues,
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
let responseText: string;
|
|
316
|
+
try {
|
|
317
|
+
responseText = await raceWithAbort(() => response.text(), signal, phase, sensitiveValues);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if (error instanceof ResolverVendorUnavailableError) throw error;
|
|
320
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
321
|
+
cause: error,
|
|
322
|
+
phase,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let payload: unknown;
|
|
327
|
+
try {
|
|
328
|
+
payload = JSON.parse(responseText);
|
|
329
|
+
} catch {
|
|
330
|
+
const contentType = response.headers.get("content-type");
|
|
331
|
+
throw new CapsolverUnavailableError("transport_failure", {
|
|
332
|
+
cause: sanitizedJsonParseCause(),
|
|
333
|
+
phase,
|
|
334
|
+
responseStatus: response.status,
|
|
335
|
+
responseContentType: contentType
|
|
336
|
+
? safeVendorDetail(contentType.slice(0, 128), sensitiveValues)
|
|
337
|
+
: undefined,
|
|
338
|
+
responseLength: responseText.length,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (!isJsonRecord(payload)) {
|
|
342
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
343
|
+
phase,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return { ok: response.ok, payload: parseResponse(payload) };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function endpoint(baseUrl: string, path: string): string {
|
|
350
|
+
return `${baseUrl.replace(/\/+$/u, "")}/${path}`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function spanErrorAttributes(
|
|
354
|
+
error: unknown,
|
|
355
|
+
phase: CapsolverOperationPhase,
|
|
356
|
+
): Record<string, unknown> | undefined {
|
|
357
|
+
if (error instanceof ResolverVendorUnavailableError) {
|
|
358
|
+
return {
|
|
359
|
+
unavailability_reason: error.reason,
|
|
360
|
+
transport_phase: error.phase,
|
|
361
|
+
...(error instanceof CapsolverUnavailableError
|
|
362
|
+
? {
|
|
363
|
+
vendor_error_code: error.errorCode,
|
|
364
|
+
vendor_error_description: error.errorDescription,
|
|
365
|
+
response_status: error.responseStatus,
|
|
366
|
+
response_content_type: error.responseContentType,
|
|
367
|
+
response_length: error.responseLength,
|
|
368
|
+
}
|
|
369
|
+
: {}),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (error instanceof CapsolverVerdictError) {
|
|
373
|
+
return {
|
|
374
|
+
verdict_reason: error.reason,
|
|
375
|
+
transport_phase: error.phase,
|
|
376
|
+
vendor_error_code: error.errorCode,
|
|
377
|
+
vendor_error_description: error.errorDescription,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
if (error instanceof CapsolverSolveTimeoutError) {
|
|
381
|
+
return {
|
|
382
|
+
unavailability_reason: "timeout",
|
|
383
|
+
transport_phase: phase,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function createCapsolverResolverVendorAdapter(
|
|
390
|
+
options: CapsolverResolverVendorOptions,
|
|
391
|
+
): CapsolverResolverVendorAdapter {
|
|
392
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
393
|
+
const baseUrl = options.baseUrl ?? DEFAULT_CAPSOLVER_BASE_URL;
|
|
394
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
395
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
396
|
+
const now = options.now ?? Date.now;
|
|
397
|
+
const delay = options.delay ?? abortableDelay;
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
id: CAPSOLVER_VENDOR_ID,
|
|
401
|
+
|
|
402
|
+
supports(kind) {
|
|
403
|
+
return resolverVendorSupports(CAPSOLVER_VENDOR_ID, kind);
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
async solve(challenge, _identity, callerSignal, traceRecorder) {
|
|
407
|
+
const apiKey = options.apiKey?.trim();
|
|
408
|
+
if (!apiKey) {
|
|
409
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "missing_credentials", {
|
|
410
|
+
phase: "create_task",
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
if (!resolverVendorSupports(CAPSOLVER_VENDOR_ID, challenge.kind)) {
|
|
414
|
+
throw new TypeError(`Capsolver resolver does not support ${challenge.kind}`);
|
|
415
|
+
}
|
|
416
|
+
if (challenge.kind !== "turnstile") {
|
|
417
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "not_implemented", {
|
|
418
|
+
phase: "create_task",
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
const sensitiveValues = [
|
|
422
|
+
apiKey,
|
|
423
|
+
challenge.pageUrl,
|
|
424
|
+
challenge.siteKey,
|
|
425
|
+
...(challenge.action !== undefined ? [challenge.action] : []),
|
|
426
|
+
...(challenge.cdata !== undefined ? [challenge.cdata] : []),
|
|
427
|
+
];
|
|
428
|
+
|
|
429
|
+
assertResolverHostAllowed(challenge.pageUrl, options.allowedHosts);
|
|
430
|
+
callerSignal.throwIfAborted();
|
|
431
|
+
|
|
432
|
+
const solveController = new AbortController();
|
|
433
|
+
const onCallerAbort = () => solveController.abort(abortReason(callerSignal));
|
|
434
|
+
callerSignal.addEventListener("abort", onCallerAbort, { once: true });
|
|
435
|
+
const timeout = options.now
|
|
436
|
+
? undefined
|
|
437
|
+
: setTimeout(() => solveController.abort(new CapsolverSolveTimeoutError()), timeoutMs);
|
|
438
|
+
const startedAt = now();
|
|
439
|
+
let phase: CapsolverOperationPhase = "create_task";
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
const createTask = async () => {
|
|
443
|
+
const metadata = {
|
|
444
|
+
...(challenge.action !== undefined ? { action: challenge.action } : {}),
|
|
445
|
+
...(challenge.cdata !== undefined ? { cdata: challenge.cdata } : {}),
|
|
446
|
+
};
|
|
447
|
+
const createResult = await postJson(
|
|
448
|
+
fetchImpl,
|
|
449
|
+
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
|
+
},
|
|
461
|
+
solveController.signal,
|
|
462
|
+
phase,
|
|
463
|
+
sensitiveValues,
|
|
464
|
+
parseCreateTaskResponse,
|
|
465
|
+
);
|
|
466
|
+
const taskId = createResult.payload.taskId;
|
|
467
|
+
if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
|
|
468
|
+
throw unavailableForPayload(createResult.payload, phase, sensitiveValues);
|
|
469
|
+
}
|
|
470
|
+
return taskId;
|
|
471
|
+
};
|
|
472
|
+
const taskId = traceRecorder
|
|
473
|
+
? await traceRecorder.runSpan("resolver.vendor.create_task", createTask, {
|
|
474
|
+
attributes: {
|
|
475
|
+
vendor: CAPSOLVER_VENDOR_ID,
|
|
476
|
+
challenge_kind: challenge.kind,
|
|
477
|
+
},
|
|
478
|
+
onError: (error) => spanErrorAttributes(error, "create_task"),
|
|
479
|
+
})
|
|
480
|
+
: await createTask();
|
|
481
|
+
|
|
482
|
+
phase = "poll_result";
|
|
483
|
+
const pollResult = async () => {
|
|
484
|
+
while (true) {
|
|
485
|
+
callerSignal.throwIfAborted();
|
|
486
|
+
const remainingMs = timeoutMs - (now() - startedAt);
|
|
487
|
+
if (remainingMs <= 0) throw new CapsolverSolveTimeoutError();
|
|
488
|
+
await delay(Math.min(pollIntervalMs, remainingMs), solveController.signal);
|
|
489
|
+
callerSignal.throwIfAborted();
|
|
490
|
+
if (now() - startedAt >= timeoutMs) throw new CapsolverSolveTimeoutError();
|
|
491
|
+
|
|
492
|
+
const result = await postJson(
|
|
493
|
+
fetchImpl,
|
|
494
|
+
endpoint(baseUrl, "getTaskResult"),
|
|
495
|
+
{ clientKey: apiKey, taskId },
|
|
496
|
+
solveController.signal,
|
|
497
|
+
phase,
|
|
498
|
+
sensitiveValues,
|
|
499
|
+
parsePollResultResponse,
|
|
500
|
+
);
|
|
501
|
+
if (!result.ok || result.payload.errorId !== 0) {
|
|
502
|
+
throw unavailableForPayload(result.payload, phase, sensitiveValues);
|
|
503
|
+
}
|
|
504
|
+
switch (result.payload.status) {
|
|
505
|
+
case "idle":
|
|
506
|
+
case "processing":
|
|
507
|
+
continue;
|
|
508
|
+
case "ready":
|
|
509
|
+
break;
|
|
510
|
+
default:
|
|
511
|
+
throw new ResolverVendorUnavailableError(
|
|
512
|
+
CAPSOLVER_VENDOR_ID,
|
|
513
|
+
"transport_failure",
|
|
514
|
+
{ phase },
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const token = result.payload.solution?.token;
|
|
519
|
+
if (!token?.trim()) {
|
|
520
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
521
|
+
phase,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
return { form: "token" as const, token };
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
return traceRecorder
|
|
528
|
+
? await traceRecorder.runSpan("resolver.vendor.poll_result", pollResult, {
|
|
529
|
+
attributes: {
|
|
530
|
+
vendor: CAPSOLVER_VENDOR_ID,
|
|
531
|
+
challenge_kind: challenge.kind,
|
|
532
|
+
},
|
|
533
|
+
onError: (error) => spanErrorAttributes(error, "poll_result"),
|
|
534
|
+
})
|
|
535
|
+
: await pollResult();
|
|
536
|
+
} catch (error) {
|
|
537
|
+
if (callerSignal.aborted) throw abortReason(callerSignal);
|
|
538
|
+
if (
|
|
539
|
+
error instanceof CapsolverSolveTimeoutError ||
|
|
540
|
+
solveController.signal.reason instanceof CapsolverSolveTimeoutError
|
|
541
|
+
) {
|
|
542
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "timeout", {
|
|
543
|
+
cause: error,
|
|
544
|
+
phase,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
if (
|
|
548
|
+
error instanceof ResolverVendorUnavailableError ||
|
|
549
|
+
error instanceof ResolverChallengeVerdictError
|
|
550
|
+
) {
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
553
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
554
|
+
...safeCauseOptions(error, sensitiveValues),
|
|
555
|
+
phase,
|
|
556
|
+
});
|
|
557
|
+
} finally {
|
|
558
|
+
if (timeout !== undefined) clearTimeout(timeout);
|
|
559
|
+
callerSignal.removeEventListener("abort", onCallerAbort);
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
}
|
package/src/runtime/resolver.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
resolverChallengeIssuingIdentity,
|
|
25
25
|
} from "./resolver-vendors/bindings.js";
|
|
26
26
|
import { createBrowserResolverVendorAdapter } from "./resolver-vendors/browser.js";
|
|
27
|
+
import { createCapsolverResolverVendorAdapter } from "./resolver-vendors/capsolver.js";
|
|
27
28
|
import { assertResolverHostAllowed } from "./resolver-vendors/hosts.js";
|
|
28
29
|
import { createTwoCaptchaResolverVendorAdapter } from "./resolver-vendors/twocaptcha.js";
|
|
29
30
|
import {
|
|
@@ -196,6 +197,13 @@ const resolverAdapterRegistry: Partial<Record<ProviderResolverVendor, ResolverAd
|
|
|
196
197
|
timeoutMs,
|
|
197
198
|
});
|
|
198
199
|
},
|
|
200
|
+
capsolver(configuration, timeoutMs, allowedHosts) {
|
|
201
|
+
return createCapsolverResolverVendorAdapter({
|
|
202
|
+
allowedHosts,
|
|
203
|
+
apiKey: configuration,
|
|
204
|
+
timeoutMs,
|
|
205
|
+
});
|
|
206
|
+
},
|
|
199
207
|
browser(configuration, timeoutMs, allowedHosts) {
|
|
200
208
|
return createBrowserResolverVendorAdapter({
|
|
201
209
|
allowedHosts,
|
|
@@ -246,7 +254,6 @@ export function swapResolverDefaultUserAgentForTests(
|
|
|
246
254
|
// This is the sole allowlist for declared vendors whose registry entry may be absent.
|
|
247
255
|
// Remove a vendor here when its adapter is registered.
|
|
248
256
|
const KNOWN_UNIMPLEMENTED_RESOLVER_VENDORS: ReadonlySet<ProviderResolverVendor> = new Set([
|
|
249
|
-
"capsolver",
|
|
250
257
|
"capmonster",
|
|
251
258
|
"custom",
|
|
252
259
|
]);
|