@apifuse/provider-sdk 2.2.0-beta.27 → 2.2.0-beta.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/bin/apifuse-dev.ts +34 -5
- package/bin/apifuse-pack-smoke.ts +1 -1
- package/bin/apifuse-pack-types.ts +26 -2
- package/bin/apifuse-perf.ts +2 -4
- package/bin/apifuse-record.ts +39 -6
- package/dist/auth-turn/index.d.ts +1 -1
- package/dist/auth-turn/index.js +1 -1
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +38 -0
- package/dist/ceremonies/index.js +52 -11
- package/dist/config/loader.d.ts +3 -1
- package/dist/config/loader.js +4 -2
- package/dist/index.d.ts +8 -7
- package/dist/index.js +5 -7
- package/dist/provider.d.ts +2 -1
- package/dist/provider.js +1 -1
- package/dist/runtime/auth-flow.js +1 -1
- package/dist/runtime/browser.js +45 -2
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.js +135 -12
- package/dist/runtime/instrumentation.js +1 -1
- package/dist/runtime/native-network-errors.d.ts +33 -0
- package/dist/runtime/native-network-errors.js +69 -0
- package/dist/runtime/native-network.d.ts +2 -33
- package/dist/runtime/native-network.js +2 -68
- package/dist/runtime/proxy-telemetry.js +3 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +4 -2
- package/dist/runtime/resolver-config.d.ts +6 -0
- package/dist/runtime/resolver-config.js +6 -0
- package/dist/runtime/resolver-public.d.ts +1 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-shared.d.ts +3 -0
- package/dist/runtime/resolver-shared.js +12 -0
- package/dist/runtime/resolver-vendors/browser.js +14 -4
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -1
- package/dist/runtime/resolver-vendors/twocaptcha.js +157 -53
- package/dist/runtime/resolver-vendors/types.d.ts +2 -2
- package/dist/runtime/resolver-vendors/types.js +3 -1
- package/dist/runtime/resolver.d.ts +15 -10
- package/dist/runtime/resolver.js +92 -23
- package/dist/runtime/state.js +5 -115
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.d.ts +1 -0
- package/dist/runtime/stealth.js +8 -131
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/self-test.d.ts +13 -0
- package/dist/server/self-test.js +124 -46
- package/dist/server/serve-implementation.d.ts +199 -0
- package/dist/server/serve-implementation.js +2072 -0
- package/dist/server/serve.d.ts +1 -187
- package/dist/server/serve.js +1 -1827
- package/dist/stateful/errors.d.ts +5 -0
- package/dist/stateful/errors.js +10 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +1 -5
- package/dist/stateful/stateful-provider-session-routing.js +2 -10
- package/dist/stream.js +7 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/package.json +27 -2
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +78 -0
- package/src/ceremonies/index.ts +68 -18
- package/src/config/loader.ts +8 -2
- package/src/index.ts +18 -24
- package/src/provider.ts +12 -14
- package/src/runtime/auth-flow.ts +1 -1
- package/src/runtime/browser.ts +50 -2
- package/src/runtime/http.ts +155 -11
- package/src/runtime/instrumentation.ts +1 -1
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +16 -97
- package/src/runtime/proxy-telemetry.ts +5 -0
- package/src/runtime/redis.ts +7 -2
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-public.ts +18 -0
- package/src/runtime/resolver-shared.ts +17 -0
- package/src/runtime/resolver-vendors/browser.ts +14 -4
- package/src/runtime/resolver-vendors/twocaptcha.ts +190 -56
- package/src/runtime/resolver-vendors/types.ts +8 -2
- package/src/runtime/resolver.ts +140 -28
- package/src/runtime/state.ts +5 -144
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +15 -158
- package/src/serve.ts +6 -1
- package/src/server/index.ts +1 -0
- package/src/server/self-test.ts +184 -59
- package/src/server/serve-implementation.ts +3042 -0
- package/src/server/serve.ts +1 -2661
- package/src/stateful/errors.ts +12 -0
- package/src/stateful/stateful-provider-session-routing.ts +2 -11
- package/src/stream.ts +8 -1
- package/src/testing/index.ts +1 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { ChallengeSolution, ProviderChallenge } from "../../types.js";
|
|
2
|
+
import type { TraceRecorder } from "../trace.js";
|
|
2
3
|
import { assertResolverHostAllowed } from "./hosts.js";
|
|
3
4
|
import {
|
|
4
5
|
type ResolverIdentity,
|
|
5
6
|
type ResolverVendorAdapter,
|
|
7
|
+
ResolverChallengeVerdictError,
|
|
6
8
|
ResolverVendorUnavailableError,
|
|
7
9
|
resolverVendorSupports,
|
|
8
10
|
} from "./types.js";
|
|
@@ -34,6 +36,7 @@ export interface TwoCaptchaResolverVendorAdapter extends ResolverVendorAdapter {
|
|
|
34
36
|
challenge: ProviderChallenge,
|
|
35
37
|
identity: ResolverIdentity | undefined,
|
|
36
38
|
signal: AbortSignal,
|
|
39
|
+
traceRecorder?: TraceRecorder,
|
|
37
40
|
): Promise<Extract<ChallengeSolution, { readonly form: "token" }>>;
|
|
38
41
|
}
|
|
39
42
|
|
|
@@ -62,10 +65,53 @@ function abortReason(signal: AbortSignal): unknown {
|
|
|
62
65
|
return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
function containsSensitiveValue(value: unknown, sensitiveValues: readonly string[]): boolean {
|
|
69
|
+
const secrets = sensitiveValues.filter((secret) => secret.length > 0);
|
|
70
|
+
if (secrets.length === 0) return false;
|
|
71
|
+
const seen = new Set<object>();
|
|
72
|
+
|
|
73
|
+
const inspect = (candidate: unknown): boolean => {
|
|
74
|
+
if (typeof candidate === "string") {
|
|
75
|
+
return secrets.some((secret) => candidate.includes(secret));
|
|
76
|
+
}
|
|
77
|
+
if (
|
|
78
|
+
candidate === null ||
|
|
79
|
+
(typeof candidate !== "object" && typeof candidate !== "function")
|
|
80
|
+
) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (seen.has(candidate)) return false;
|
|
84
|
+
seen.add(candidate);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
for (const property of Reflect.ownKeys(candidate)) {
|
|
88
|
+
if (typeof property === "string" && inspect(property)) return true;
|
|
89
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, property);
|
|
90
|
+
if (!descriptor) return true;
|
|
91
|
+
if ("value" in descriptor && inspect(descriptor.value)) return true;
|
|
92
|
+
if (descriptor.get !== undefined || descriptor.set !== undefined) return true;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return inspect(value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function safeCauseOptions(
|
|
104
|
+
error: unknown,
|
|
105
|
+
sensitiveValues: readonly string[],
|
|
106
|
+
): { readonly cause?: unknown } {
|
|
107
|
+
return containsSensitiveValue(error, sensitiveValues) ? {} : { cause: error };
|
|
108
|
+
}
|
|
109
|
+
|
|
65
110
|
function raceWithAbort<T>(
|
|
66
111
|
operation: () => Promise<T>,
|
|
67
112
|
signal: AbortSignal,
|
|
68
113
|
phase?: TwoCaptchaOperationPhase,
|
|
114
|
+
sensitiveValues: readonly string[] = [],
|
|
69
115
|
): Promise<T> {
|
|
70
116
|
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
71
117
|
|
|
@@ -89,7 +135,7 @@ function raceWithAbort<T>(
|
|
|
89
135
|
}
|
|
90
136
|
reject(
|
|
91
137
|
new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
|
|
92
|
-
|
|
138
|
+
...safeCauseOptions(error, sensitiveValues),
|
|
93
139
|
phase,
|
|
94
140
|
}),
|
|
95
141
|
);
|
|
@@ -156,10 +202,17 @@ function isAllocationExhausted(payload: JsonRecord): boolean {
|
|
|
156
202
|
);
|
|
157
203
|
}
|
|
158
204
|
|
|
205
|
+
function isNegativeVerdict(payload: JsonRecord): boolean {
|
|
206
|
+
return errorText(payload, "errorCode").toLowerCase() === "error_captcha_unsolvable";
|
|
207
|
+
}
|
|
208
|
+
|
|
159
209
|
function unavailableForPayload(
|
|
160
210
|
payload: JsonRecord,
|
|
161
211
|
phase: TwoCaptchaOperationPhase,
|
|
162
|
-
): ResolverVendorUnavailableError {
|
|
212
|
+
): ResolverVendorUnavailableError | ResolverChallengeVerdictError {
|
|
213
|
+
if (isNegativeVerdict(payload)) {
|
|
214
|
+
return new ResolverChallengeVerdictError(TWOCAPTCHA_VENDOR_ID, "solve_failed");
|
|
215
|
+
}
|
|
163
216
|
return new ResolverVendorUnavailableError(
|
|
164
217
|
TWOCAPTCHA_VENDOR_ID,
|
|
165
218
|
isAllocationExhausted(payload) ? "allocation_exhausted" : "transport_failure",
|
|
@@ -173,6 +226,7 @@ async function postJson(
|
|
|
173
226
|
body: JsonRecord,
|
|
174
227
|
signal: AbortSignal,
|
|
175
228
|
phase: TwoCaptchaOperationPhase,
|
|
229
|
+
sensitiveValues: readonly string[],
|
|
176
230
|
): Promise<{ readonly ok: boolean; readonly payload: JsonRecord }> {
|
|
177
231
|
const response = await raceWithAbort(
|
|
178
232
|
() =>
|
|
@@ -185,6 +239,7 @@ async function postJson(
|
|
|
185
239
|
}),
|
|
186
240
|
signal,
|
|
187
241
|
phase,
|
|
242
|
+
sensitiveValues,
|
|
188
243
|
);
|
|
189
244
|
|
|
190
245
|
let responseText: string;
|
|
@@ -220,9 +275,12 @@ function taskIdFrom(payload: JsonRecord): string | number | undefined {
|
|
|
220
275
|
return typeof taskId === "string" || typeof taskId === "number" ? taskId : undefined;
|
|
221
276
|
}
|
|
222
277
|
|
|
223
|
-
function tokenFrom(payload: JsonRecord): string | undefined {
|
|
278
|
+
function tokenFrom(payload: JsonRecord, challenge: ProviderChallenge): string | undefined {
|
|
224
279
|
const solution = payload.solution;
|
|
225
280
|
if (!isJsonRecord(solution)) return undefined;
|
|
281
|
+
if (challenge.kind === "aws_waf") {
|
|
282
|
+
return typeof solution.existing_token === "string" ? solution.existing_token : undefined;
|
|
283
|
+
}
|
|
226
284
|
if (typeof solution.gRecaptchaResponse === "string") return solution.gRecaptchaResponse;
|
|
227
285
|
return typeof solution.token === "string" ? solution.token : undefined;
|
|
228
286
|
}
|
|
@@ -231,6 +289,28 @@ function endpoint(baseUrl: string, path: string): string {
|
|
|
231
289
|
return `${baseUrl.replace(/\/+$/u, "")}/${path}`;
|
|
232
290
|
}
|
|
233
291
|
|
|
292
|
+
function spanErrorAttributes(
|
|
293
|
+
error: unknown,
|
|
294
|
+
phase: TwoCaptchaOperationPhase,
|
|
295
|
+
): Record<string, unknown> | undefined {
|
|
296
|
+
if (error instanceof ResolverVendorUnavailableError) {
|
|
297
|
+
return {
|
|
298
|
+
unavailability_reason: error.reason,
|
|
299
|
+
transport_phase: error.phase,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
// The solve budget raises TwoCaptchaSolveTimeoutError inside the spanned closure and
|
|
303
|
+
// the outer catch converts it only after the span has been finalized, so the span has
|
|
304
|
+
// to recognize it here or slow solves lose exactly the attribution these spans add.
|
|
305
|
+
if (error instanceof TwoCaptchaSolveTimeoutError) {
|
|
306
|
+
return {
|
|
307
|
+
unavailability_reason: "timeout",
|
|
308
|
+
transport_phase: phase,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
|
|
234
314
|
export function createTwoCaptchaResolverVendorAdapter(
|
|
235
315
|
options: TwoCaptchaResolverVendorOptions,
|
|
236
316
|
): TwoCaptchaResolverVendorAdapter {
|
|
@@ -248,7 +328,7 @@ export function createTwoCaptchaResolverVendorAdapter(
|
|
|
248
328
|
return resolverVendorSupports(TWOCAPTCHA_VENDOR_ID, kind);
|
|
249
329
|
},
|
|
250
330
|
|
|
251
|
-
async solve(challenge, identity, callerSignal) {
|
|
331
|
+
async solve(challenge, identity, callerSignal, traceRecorder) {
|
|
252
332
|
const apiKey = options.apiKey?.trim();
|
|
253
333
|
if (!apiKey) {
|
|
254
334
|
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "missing_credentials", {
|
|
@@ -258,8 +338,18 @@ export function createTwoCaptchaResolverVendorAdapter(
|
|
|
258
338
|
if (!resolverVendorSupports(TWOCAPTCHA_VENDOR_ID, challenge.kind)) {
|
|
259
339
|
throw new TypeError(`2captcha resolver does not support ${challenge.kind}`);
|
|
260
340
|
}
|
|
261
|
-
if (challenge.kind !== "recaptcha_v2") {
|
|
262
|
-
|
|
341
|
+
if (challenge.kind !== "recaptcha_v2" && challenge.kind !== "aws_waf") {
|
|
342
|
+
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
|
|
343
|
+
phase: "create_task",
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
if (
|
|
347
|
+
challenge.kind === "aws_waf" &&
|
|
348
|
+
(!challenge.siteKey?.trim() ||
|
|
349
|
+
!challenge.captchaScript?.trim() ||
|
|
350
|
+
!challenge.context?.trim() ||
|
|
351
|
+
!challenge.iv?.trim())
|
|
352
|
+
) {
|
|
263
353
|
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
|
|
264
354
|
phase: "create_task",
|
|
265
355
|
});
|
|
@@ -285,62 +375,101 @@ export function createTwoCaptchaResolverVendorAdapter(
|
|
|
285
375
|
let phase: TwoCaptchaOperationPhase = "create_task";
|
|
286
376
|
|
|
287
377
|
try {
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
phase = "poll_result";
|
|
311
|
-
while (true) {
|
|
312
|
-
callerSignal.throwIfAborted();
|
|
313
|
-
const remainingMs = timeoutMs - (now() - startedAt);
|
|
314
|
-
if (remainingMs <= 0) throw new TwoCaptchaSolveTimeoutError();
|
|
315
|
-
await delay(Math.min(pollIntervalMs, remainingMs), solveController.signal);
|
|
316
|
-
callerSignal.throwIfAborted();
|
|
317
|
-
if (now() - startedAt >= timeoutMs) throw new TwoCaptchaSolveTimeoutError();
|
|
318
|
-
|
|
319
|
-
const pollResult = await postJson(
|
|
378
|
+
const createTask = async () => {
|
|
379
|
+
const task =
|
|
380
|
+
challenge.kind === "aws_waf"
|
|
381
|
+
? {
|
|
382
|
+
type: proxy ? "AmazonTask" : "AmazonTaskProxyless",
|
|
383
|
+
websiteURL: challenge.pageUrl,
|
|
384
|
+
websiteKey: challenge.siteKey,
|
|
385
|
+
captchaScript: challenge.captchaScript,
|
|
386
|
+
context: challenge.context,
|
|
387
|
+
iv: challenge.iv,
|
|
388
|
+
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
389
|
+
...(proxy ?? {}),
|
|
390
|
+
}
|
|
391
|
+
: {
|
|
392
|
+
type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
|
|
393
|
+
websiteURL: challenge.pageUrl,
|
|
394
|
+
websiteKey: challenge.siteKey,
|
|
395
|
+
isInvisible: false,
|
|
396
|
+
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
397
|
+
...(proxy ?? {}),
|
|
398
|
+
};
|
|
399
|
+
const createResult = await postJson(
|
|
320
400
|
fetchImpl,
|
|
321
|
-
endpoint(baseUrl, "
|
|
322
|
-
{ clientKey: apiKey,
|
|
401
|
+
endpoint(baseUrl, "createTask"),
|
|
402
|
+
{ clientKey: apiKey, task },
|
|
323
403
|
solveController.signal,
|
|
324
404
|
phase,
|
|
405
|
+
[apiKey],
|
|
325
406
|
);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (pollResult.payload.status === "processing") continue;
|
|
330
|
-
if (pollResult.payload.status !== "ready") {
|
|
331
|
-
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
|
|
332
|
-
phase,
|
|
333
|
-
});
|
|
407
|
+
const taskId = taskIdFrom(createResult.payload);
|
|
408
|
+
if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
|
|
409
|
+
throw unavailableForPayload(createResult.payload, phase);
|
|
334
410
|
}
|
|
411
|
+
return taskId;
|
|
412
|
+
};
|
|
413
|
+
const taskId = traceRecorder
|
|
414
|
+
? await traceRecorder.runSpan("resolver.vendor.create_task", createTask, {
|
|
415
|
+
attributes: {
|
|
416
|
+
vendor: TWOCAPTCHA_VENDOR_ID,
|
|
417
|
+
challenge_kind: challenge.kind,
|
|
418
|
+
},
|
|
419
|
+
onError: (error) => spanErrorAttributes(error, "create_task"),
|
|
420
|
+
})
|
|
421
|
+
: await createTask();
|
|
335
422
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
423
|
+
phase = "poll_result";
|
|
424
|
+
const pollResult = async () => {
|
|
425
|
+
while (true) {
|
|
426
|
+
callerSignal.throwIfAborted();
|
|
427
|
+
const remainingMs = timeoutMs - (now() - startedAt);
|
|
428
|
+
if (remainingMs <= 0) throw new TwoCaptchaSolveTimeoutError();
|
|
429
|
+
await delay(Math.min(pollIntervalMs, remainingMs), solveController.signal);
|
|
430
|
+
callerSignal.throwIfAborted();
|
|
431
|
+
if (now() - startedAt >= timeoutMs) throw new TwoCaptchaSolveTimeoutError();
|
|
432
|
+
|
|
433
|
+
const pollResult = await postJson(
|
|
434
|
+
fetchImpl,
|
|
435
|
+
endpoint(baseUrl, "getTaskResult"),
|
|
436
|
+
{ clientKey: apiKey, taskId },
|
|
437
|
+
solveController.signal,
|
|
339
438
|
phase,
|
|
340
|
-
|
|
439
|
+
[apiKey],
|
|
440
|
+
);
|
|
441
|
+
if (!pollResult.ok || pollResult.payload.errorId !== 0) {
|
|
442
|
+
throw unavailableForPayload(pollResult.payload, phase);
|
|
443
|
+
}
|
|
444
|
+
if (pollResult.payload.status === "processing") continue;
|
|
445
|
+
if (pollResult.payload.status !== "ready") {
|
|
446
|
+
throw new ResolverVendorUnavailableError(
|
|
447
|
+
TWOCAPTCHA_VENDOR_ID,
|
|
448
|
+
"transport_failure",
|
|
449
|
+
{ phase },
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const token = tokenFrom(pollResult.payload, challenge);
|
|
454
|
+
if (!token?.trim()) {
|
|
455
|
+
throw new ResolverVendorUnavailableError(
|
|
456
|
+
TWOCAPTCHA_VENDOR_ID,
|
|
457
|
+
"transport_failure",
|
|
458
|
+
{ phase },
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
return { form: "token" as const, token };
|
|
341
462
|
}
|
|
342
|
-
|
|
343
|
-
|
|
463
|
+
};
|
|
464
|
+
return traceRecorder
|
|
465
|
+
? await traceRecorder.runSpan("resolver.vendor.poll_result", pollResult, {
|
|
466
|
+
attributes: {
|
|
467
|
+
vendor: TWOCAPTCHA_VENDOR_ID,
|
|
468
|
+
challenge_kind: challenge.kind,
|
|
469
|
+
},
|
|
470
|
+
onError: (error) => spanErrorAttributes(error, "poll_result"),
|
|
471
|
+
})
|
|
472
|
+
: await pollResult();
|
|
344
473
|
} catch (error) {
|
|
345
474
|
if (callerSignal.aborted) throw abortReason(callerSignal);
|
|
346
475
|
if (
|
|
@@ -352,9 +481,14 @@ export function createTwoCaptchaResolverVendorAdapter(
|
|
|
352
481
|
phase,
|
|
353
482
|
});
|
|
354
483
|
}
|
|
355
|
-
if (
|
|
484
|
+
if (
|
|
485
|
+
error instanceof ResolverVendorUnavailableError ||
|
|
486
|
+
error instanceof ResolverChallengeVerdictError
|
|
487
|
+
) {
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
356
490
|
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
|
|
357
|
-
|
|
491
|
+
...safeCauseOptions(error, [apiKey]),
|
|
358
492
|
phase,
|
|
359
493
|
});
|
|
360
494
|
} finally {
|
|
@@ -112,13 +112,15 @@ export interface ResolverVendorAdapter {
|
|
|
112
112
|
|
|
113
113
|
export type ResolverVendorUnavailableReason =
|
|
114
114
|
| "missing_credentials"
|
|
115
|
+
| "missing_proxy_identity"
|
|
116
|
+
| "missing_client_profile"
|
|
115
117
|
| "missing_transport"
|
|
116
118
|
| "allocation_exhausted"
|
|
117
119
|
| "transport_failure"
|
|
118
120
|
| "timeout"
|
|
119
121
|
| "not_implemented";
|
|
120
122
|
|
|
121
|
-
export type ResolverChallengeVerdictReason = "human_puzzle";
|
|
123
|
+
export type ResolverChallengeVerdictReason = "human_puzzle" | "solve_failed";
|
|
122
124
|
|
|
123
125
|
type ResolverErrorOptions = {
|
|
124
126
|
/** Raw cause; adapters must not place bodies, cookies, headers, credentials, or proxy URLs here. */
|
|
@@ -158,7 +160,11 @@ export class ResolverChallengeVerdictError extends Error {
|
|
|
158
160
|
readonly reason: ResolverChallengeVerdictReason,
|
|
159
161
|
options: ResolverErrorOptions = {},
|
|
160
162
|
) {
|
|
161
|
-
super(
|
|
163
|
+
super(
|
|
164
|
+
reason === "solve_failed"
|
|
165
|
+
? `Resolver vendor ${vendor} attempted the challenge but did not solve it`
|
|
166
|
+
: `Resolver vendor ${vendor} returned a challenge verdict: ${reason}`,
|
|
167
|
+
);
|
|
162
168
|
this.name = "ResolverChallengeVerdictError";
|
|
163
169
|
if (options.cause !== undefined) {
|
|
164
170
|
this.cause = options.cause;
|
package/src/runtime/resolver.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
resolveProxyConfigAsync,
|
|
5
|
+
type ProxyResolutionOptions,
|
|
6
|
+
type ProxyUserAgentSource,
|
|
7
|
+
} from "../config/loader.js";
|
|
3
8
|
import { ProviderError } from "../errors.js";
|
|
9
|
+
import { getStealthProfile } from "../stealth/profiles.js";
|
|
4
10
|
import type {
|
|
5
11
|
ChallengeSolution,
|
|
6
12
|
ProviderCache,
|
|
7
13
|
ProviderChallenge,
|
|
8
14
|
ProviderChallengeKind,
|
|
15
|
+
ProviderProxyMode,
|
|
9
16
|
ProviderResolverConfig,
|
|
10
17
|
ProviderResolverVendor,
|
|
11
18
|
ResolverContext,
|
|
@@ -29,14 +36,33 @@ import {
|
|
|
29
36
|
type ResolverVendorUnavailableReason,
|
|
30
37
|
resolverVendorSupports,
|
|
31
38
|
} from "./resolver-vendors/types.js";
|
|
39
|
+
import {
|
|
40
|
+
createUnsupportedResolverClient,
|
|
41
|
+
RESOLVER_INSTRUMENTATION_METADATA,
|
|
42
|
+
} from "./resolver-shared.js";
|
|
43
|
+
import {
|
|
44
|
+
APIFUSE__CDP_POOL__URL,
|
|
45
|
+
APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
|
|
46
|
+
APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
|
|
47
|
+
APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
|
|
48
|
+
APIFUSE__RESOLVER__TIMEOUT_MS,
|
|
49
|
+
DEFAULT_RESOLVER_TIMEOUT_MS,
|
|
50
|
+
} from "./resolver-config.js";
|
|
51
|
+
import { DEFAULT_PROFILE } from "./stealth.js";
|
|
32
52
|
import type { TraceRecorder } from "./trace.js";
|
|
33
53
|
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export
|
|
39
|
-
|
|
54
|
+
export {
|
|
55
|
+
createUnsupportedResolverClient,
|
|
56
|
+
RESOLVER_INSTRUMENTATION_METADATA,
|
|
57
|
+
} from "./resolver-shared.js";
|
|
58
|
+
export {
|
|
59
|
+
APIFUSE__CDP_POOL__URL,
|
|
60
|
+
APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
|
|
61
|
+
APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
|
|
62
|
+
APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
|
|
63
|
+
APIFUSE__RESOLVER__TIMEOUT_MS,
|
|
64
|
+
DEFAULT_RESOLVER_TIMEOUT_MS,
|
|
65
|
+
} from "./resolver-config.js";
|
|
40
66
|
|
|
41
67
|
type EnvLike = Record<string, string | undefined>;
|
|
42
68
|
|
|
@@ -75,6 +101,14 @@ type ResolverChainClient = ResolverContext & {
|
|
|
75
101
|
export interface ResolverRuntimeOptions {
|
|
76
102
|
readonly allowedHosts?: readonly string[];
|
|
77
103
|
readonly cache?: ProviderCache;
|
|
104
|
+
/** Inputs for SDK-owned lazy proxy resolution. The SDK never accepts a caller-built identity. */
|
|
105
|
+
readonly proxyIntent?: {
|
|
106
|
+
readonly mode: ProviderProxyMode;
|
|
107
|
+
readonly upstream: NonNullable<ProxyResolutionOptions["upstream"]>;
|
|
108
|
+
readonly affinityKey?: ProxyResolutionOptions["affinityKey"];
|
|
109
|
+
readonly telemetry?: ProxyResolutionOptions["telemetry"];
|
|
110
|
+
readonly userAgent?: string;
|
|
111
|
+
};
|
|
78
112
|
/** Server-owned context/proxy scope used only for identity-bound cache entries. */
|
|
79
113
|
readonly identityScope?: string;
|
|
80
114
|
/** SDK-owned transport already bound to the resolved proxy lease and client profile. */
|
|
@@ -143,10 +177,6 @@ const SAFE_CAUSE_MESSAGE_WORDS: ReadonlySet<string> = new Set([
|
|
|
143
177
|
"writing",
|
|
144
178
|
]);
|
|
145
179
|
|
|
146
|
-
export const RESOLVER_INSTRUMENTATION_METADATA = Symbol.for(
|
|
147
|
-
"@apifuse/provider-sdk/runtime/resolver-instrumentation-metadata",
|
|
148
|
-
);
|
|
149
|
-
|
|
150
180
|
export type ResolverInstrumentationMetadata = {
|
|
151
181
|
readonly target: ResolverContext;
|
|
152
182
|
readonly traceRecorder: TraceRecorder;
|
|
@@ -195,6 +225,24 @@ export function swapResolverAdapterFactoryForTests(
|
|
|
195
225
|
};
|
|
196
226
|
}
|
|
197
227
|
|
|
228
|
+
let resolveDefaultResolverUserAgent: () => string | undefined = () =>
|
|
229
|
+
getStealthProfile(DEFAULT_PROFILE).userAgent;
|
|
230
|
+
|
|
231
|
+
/** Internal test seam; deliberately not re-exported from the package root. */
|
|
232
|
+
export function swapResolverDefaultUserAgentForTests(
|
|
233
|
+
resolver: (() => string | undefined) | undefined,
|
|
234
|
+
): () => void {
|
|
235
|
+
const original = resolveDefaultResolverUserAgent;
|
|
236
|
+
resolveDefaultResolverUserAgent =
|
|
237
|
+
resolver ?? (() => getStealthProfile(DEFAULT_PROFILE).userAgent);
|
|
238
|
+
let restored = false;
|
|
239
|
+
return () => {
|
|
240
|
+
if (restored) return;
|
|
241
|
+
restored = true;
|
|
242
|
+
resolveDefaultResolverUserAgent = original;
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
198
246
|
// This is the sole allowlist for declared vendors whose registry entry may be absent.
|
|
199
247
|
// Remove a vendor here when its adapter is registered.
|
|
200
248
|
const KNOWN_UNIMPLEMENTED_RESOLVER_VENDORS: ReadonlySet<ProviderResolverVendor> = new Set([
|
|
@@ -323,6 +371,7 @@ function adapterRequiresTransport(
|
|
|
323
371
|
function sanitizeDiagnosticUrl(rawUrl: string): string {
|
|
324
372
|
try {
|
|
325
373
|
const parsed = new URL(rawUrl);
|
|
374
|
+
if (parsed.username || parsed.password) return "[REDACTED_PROXY_URL]";
|
|
326
375
|
return `${parsed.protocol}//${parsed.host}`;
|
|
327
376
|
} catch {
|
|
328
377
|
return "[REDACTED_URL]";
|
|
@@ -338,9 +387,16 @@ function sanitizeCauseMessage(message: string): string {
|
|
|
338
387
|
.split(/\s+/)
|
|
339
388
|
.filter(Boolean)
|
|
340
389
|
.map((token) => {
|
|
341
|
-
if (
|
|
390
|
+
if (
|
|
391
|
+
token === "[REDACTED]" ||
|
|
392
|
+
token === "[REDACTED_PROXY_URL]" ||
|
|
393
|
+
/^[a-z][a-z\d+.-]*:\/\/[^\s]+$/i.test(token)
|
|
394
|
+
)
|
|
395
|
+
return token;
|
|
342
396
|
const word = token.replace(/^[^a-z\d]+|[^a-z\d]+$/gi, "");
|
|
343
|
-
return word.length > 0 &&
|
|
397
|
+
return word.length > 0 &&
|
|
398
|
+
word.length <= 32 &&
|
|
399
|
+
SAFE_CAUSE_MESSAGE_WORDS.has(word.toLowerCase())
|
|
344
400
|
? token
|
|
345
401
|
: "[REDACTED]";
|
|
346
402
|
});
|
|
@@ -685,12 +741,58 @@ export async function invalidateResolverSolution(
|
|
|
685
741
|
});
|
|
686
742
|
}
|
|
687
743
|
|
|
744
|
+
async function resolveResolverIdentity(
|
|
745
|
+
proxyIntent: NonNullable<ResolverRuntimeOptions["proxyIntent"]>,
|
|
746
|
+
): Promise<{
|
|
747
|
+
readonly identity?: ResolverIdentity;
|
|
748
|
+
readonly unavailableReason?: ResolverVendorUnavailableReason;
|
|
749
|
+
readonly userAgentSource?: ProxyUserAgentSource;
|
|
750
|
+
}> {
|
|
751
|
+
const userAgentSource: ProxyUserAgentSource = proxyIntent.userAgent ? "declared" : "defaulted";
|
|
752
|
+
let proxyUrl: string | undefined;
|
|
753
|
+
try {
|
|
754
|
+
const resolved = await resolveProxyConfigAsync({
|
|
755
|
+
upstream: proxyIntent.upstream,
|
|
756
|
+
affinityKey: proxyIntent.affinityKey,
|
|
757
|
+
telemetry: proxyIntent.telemetry
|
|
758
|
+
? {
|
|
759
|
+
...proxyIntent.telemetry,
|
|
760
|
+
recordProxyResolution(event) {
|
|
761
|
+
proxyIntent.telemetry?.recordProxyResolution({
|
|
762
|
+
...event,
|
|
763
|
+
userAgentSource,
|
|
764
|
+
});
|
|
765
|
+
},
|
|
766
|
+
}
|
|
767
|
+
: undefined,
|
|
768
|
+
});
|
|
769
|
+
proxyUrl = resolved.url;
|
|
770
|
+
if (!proxyUrl) return { unavailableReason: "missing_proxy_identity", userAgentSource };
|
|
771
|
+
} catch {
|
|
772
|
+
// Lease failures contain infrastructure detail that must not cross the resolver
|
|
773
|
+
// boundary. A required policy is classified by the existing fail-closed guard.
|
|
774
|
+
return { unavailableReason: "missing_proxy_identity", userAgentSource };
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
try {
|
|
778
|
+
const userAgent = proxyIntent.userAgent || resolveDefaultResolverUserAgent();
|
|
779
|
+
if (!userAgent) return { unavailableReason: "missing_client_profile", userAgentSource };
|
|
780
|
+
return {
|
|
781
|
+
identity: { proxyUrl, userAgent },
|
|
782
|
+
userAgentSource,
|
|
783
|
+
};
|
|
784
|
+
} catch {
|
|
785
|
+
return { unavailableReason: "missing_client_profile", userAgentSource };
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
688
789
|
function createResolverChainClient(options: {
|
|
689
790
|
readonly kinds: readonly ProviderChallengeKind[];
|
|
690
791
|
readonly entries: readonly ResolverChainEntry[];
|
|
691
792
|
readonly unavailableReason?: string;
|
|
692
793
|
readonly cache?: ProviderCache;
|
|
693
794
|
readonly identity?: ResolverIdentity;
|
|
795
|
+
readonly proxyIntent?: ResolverRuntimeOptions["proxyIntent"];
|
|
694
796
|
readonly identityScope?: string;
|
|
695
797
|
readonly transport?: ResolverVendorTransport;
|
|
696
798
|
readonly createTransport?: ResolverRuntimeOptions["createTransport"];
|
|
@@ -715,16 +817,33 @@ function createResolverChainClient(options: {
|
|
|
715
817
|
const supportingEntries = options.entries.filter((entry) => entry.supports(challenge.kind));
|
|
716
818
|
if (supportingEntries.length === 0) throwUnsupportedKind(challenge.kind);
|
|
717
819
|
signal.throwIfAborted();
|
|
820
|
+
const identityResolution = options.proxyIntent
|
|
821
|
+
? await resolveResolverIdentity(options.proxyIntent)
|
|
822
|
+
: { identity: options.identity };
|
|
823
|
+
const identity = identityResolution.identity;
|
|
824
|
+
signal.throwIfAborted();
|
|
825
|
+
// Resolve a required proxy lease before consulting the cache. Solutions minted
|
|
826
|
+
// under a previous release are shared and long-lived, but a portable cached token
|
|
827
|
+
// must not bypass the upstream admission policy when no lease can be resolved.
|
|
828
|
+
const requiredProxyIdentityMissing =
|
|
829
|
+
options.proxyIntent?.mode === "required" && identity === undefined;
|
|
830
|
+
if (requiredProxyIdentityMissing) {
|
|
831
|
+
throwExhausted(
|
|
832
|
+
supportingEntries.map((entry) => ({
|
|
833
|
+
vendor: entry.id,
|
|
834
|
+
reason: identityResolution.unavailableReason ?? "missing_proxy_identity",
|
|
835
|
+
})),
|
|
836
|
+
);
|
|
837
|
+
}
|
|
718
838
|
if (options.cache && resolverChallengeIsCacheable(challenge)) {
|
|
719
839
|
const cached = await findCachedSolution(
|
|
720
840
|
options.cache,
|
|
721
841
|
challenge,
|
|
722
|
-
|
|
842
|
+
identity,
|
|
723
843
|
options.identityScope,
|
|
724
844
|
);
|
|
725
845
|
if (cached) return cached;
|
|
726
846
|
}
|
|
727
|
-
|
|
728
847
|
const attempts: ResolverChainAttempt[] = [];
|
|
729
848
|
for (const entry of supportingEntries) {
|
|
730
849
|
const adapter = entry.createAdapter();
|
|
@@ -745,7 +864,7 @@ function createResolverChainClient(options: {
|
|
|
745
864
|
const transport = unrestrictedTransport
|
|
746
865
|
? restrictResolverTransport(unrestrictedTransport, options.allowedHosts ?? [])
|
|
747
866
|
: undefined;
|
|
748
|
-
return adapter.solve(challenge,
|
|
867
|
+
return adapter.solve(challenge, identity, signal, traceRecorder, transport);
|
|
749
868
|
};
|
|
750
869
|
const solution = traceRecorder
|
|
751
870
|
? await traceRecorder.runSpan("resolver.vendor.attempt", solveAttempt, {
|
|
@@ -753,6 +872,7 @@ function createResolverChainClient(options: {
|
|
|
753
872
|
vendor: adapter.id,
|
|
754
873
|
challenge_kind: challenge.kind,
|
|
755
874
|
client_profile: options.clientProfile,
|
|
875
|
+
resolver_identity_source: identityResolution.userAgentSource,
|
|
756
876
|
},
|
|
757
877
|
onError(error) {
|
|
758
878
|
return error instanceof ResolverVendorUnavailableError
|
|
@@ -768,9 +888,9 @@ function createResolverChainClient(options: {
|
|
|
768
888
|
solutionExpiryMs(solution) !== undefined
|
|
769
889
|
) {
|
|
770
890
|
const issuingIdentity =
|
|
771
|
-
adapter.getIssuingIdentity?.(solution,
|
|
891
|
+
adapter.getIssuingIdentity?.(solution, identity, challenge) ??
|
|
772
892
|
resolverChallengeIssuingIdentity(challenge, {
|
|
773
|
-
...(
|
|
893
|
+
...(identity ? { proxyUrl: identity.proxyUrl } : {}),
|
|
774
894
|
userAgent: solution.userAgent,
|
|
775
895
|
});
|
|
776
896
|
if (issuingIdentity) {
|
|
@@ -804,6 +924,7 @@ export function createResolverClient(options: {
|
|
|
804
924
|
readonly unavailableReason?: string;
|
|
805
925
|
readonly cache?: ProviderCache;
|
|
806
926
|
readonly identity?: ResolverIdentity;
|
|
927
|
+
readonly proxyIntent?: ResolverRuntimeOptions["proxyIntent"];
|
|
807
928
|
readonly transport?: ResolverVendorTransport;
|
|
808
929
|
readonly createTransport?: ResolverRuntimeOptions["createTransport"];
|
|
809
930
|
readonly clientProfile?: string;
|
|
@@ -819,6 +940,7 @@ export function createResolverClient(options: {
|
|
|
819
940
|
unavailableReason: options.unavailableReason,
|
|
820
941
|
cache: options.cache,
|
|
821
942
|
identity: options.identity,
|
|
943
|
+
proxyIntent: options.proxyIntent,
|
|
822
944
|
transport: options.transport,
|
|
823
945
|
createTransport: options.createTransport,
|
|
824
946
|
clientProfile: options.clientProfile,
|
|
@@ -853,17 +975,6 @@ function resolveVendorAvailability(
|
|
|
853
975
|
: { vendor, available: false, reason: "missing_credentials" };
|
|
854
976
|
}
|
|
855
977
|
|
|
856
|
-
export function createUnsupportedResolverClient(reason?: string): ResolverContext {
|
|
857
|
-
return {
|
|
858
|
-
async solve() {
|
|
859
|
-
throw new ProviderError(reason ?? "Resolver runtime is not configured", {
|
|
860
|
-
code: "RESOLVER_UNAVAILABLE",
|
|
861
|
-
fix: "Declare resolver on the provider definition and configure vendor credentials.",
|
|
862
|
-
});
|
|
863
|
-
},
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
|
|
867
978
|
export function bindResolverSignal(
|
|
868
979
|
resolver: ResolverContext,
|
|
869
980
|
defaultSignal: AbortSignal | undefined,
|
|
@@ -921,6 +1032,7 @@ function createResolverClientFromEnvInternal(
|
|
|
921
1032
|
};
|
|
922
1033
|
}),
|
|
923
1034
|
cache: options.cache,
|
|
1035
|
+
proxyIntent: options.proxyIntent,
|
|
924
1036
|
identityScope: options.identityScope,
|
|
925
1037
|
transport: options.transport,
|
|
926
1038
|
createTransport: options.createTransport,
|