@apifuse/provider-sdk 2.2.0-beta.28 → 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 +4 -0
- package/bin/apifuse-dev.ts +32 -3
- package/bin/apifuse-pack-types.ts +24 -1
- package/bin/apifuse-record.ts +39 -6
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +38 -0
- package/dist/config/loader.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime/browser.js +45 -2
- package/dist/runtime/proxy-telemetry.js +3 -0
- package/dist/runtime/resolver-public.d.ts +1 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-vendors/browser.js +14 -4
- package/dist/runtime/resolver-vendors/twocaptcha.js +84 -17
- 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 +12 -3
- package/dist/runtime/resolver.js +80 -12
- package/dist/runtime/stealth.d.ts +1 -0
- package/dist/runtime/stealth.js +1 -1
- package/dist/server/serve-implementation.js +20 -2
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/package.json +4 -4
- package/src/auth.ts +78 -0
- package/src/config/loader.ts +3 -0
- package/src/index.ts +0 -1
- package/src/runtime/browser.ts +50 -2
- package/src/runtime/proxy-telemetry.ts +5 -0
- package/src/runtime/resolver-public.ts +18 -0
- package/src/runtime/resolver-vendors/browser.ts +14 -4
- package/src/runtime/resolver-vendors/twocaptcha.ts +102 -19
- package/src/runtime/resolver-vendors/types.ts +7 -2
- package/src/runtime/resolver.ts +104 -17
- package/src/runtime/stealth.ts +1 -1
- package/src/server/serve-implementation.ts +20 -2
- package/src/testing/index.ts +1 -0
package/src/runtime/browser.ts
CHANGED
|
@@ -290,7 +290,13 @@ function formatExpression<T>(fn: string | (() => T)): string {
|
|
|
290
290
|
|
|
291
291
|
function toLaunchOptions(options: BrowserClientOptions): LaunchOptions {
|
|
292
292
|
return {
|
|
293
|
-
|
|
293
|
+
// `extraArgs` is optional, but playwright-extra's stealth evasions mutate
|
|
294
|
+
// `options.args` unguarded (navigator.webdriver does
|
|
295
|
+
// `options.args.findIndex(...)` in beforeLaunch). Forwarding `undefined`
|
|
296
|
+
// therefore crashes every stealth launch that omits extraArgs with
|
|
297
|
+
// "TypeError: undefined is not an object (evaluating 'options.args.findIndex')".
|
|
298
|
+
// Always hand the launcher a concrete array.
|
|
299
|
+
args: options.extraArgs ?? [],
|
|
294
300
|
executablePath: options.executablePath,
|
|
295
301
|
headless: options.headless ?? true,
|
|
296
302
|
proxy: options.proxy ? { server: options.proxy } : undefined,
|
|
@@ -975,6 +981,20 @@ function getCdpExecutionContext(params: unknown): {
|
|
|
975
981
|
};
|
|
976
982
|
}
|
|
977
983
|
|
|
984
|
+
function getCdpDestroyedExecutionContextId(params: unknown): number | undefined {
|
|
985
|
+
if (!isRecord(params)) {
|
|
986
|
+
return undefined;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return typeof params.executionContextId === "number"
|
|
990
|
+
? params.executionContextId
|
|
991
|
+
: undefined;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function isMissingExecutionContextError(error: unknown): boolean {
|
|
995
|
+
return error instanceof Error && /\b(?:cannot|failed to) find context\b/i.test(error.message);
|
|
996
|
+
}
|
|
997
|
+
|
|
978
998
|
class CdpBrowserLocator implements BrowserLocator {
|
|
979
999
|
constructor(
|
|
980
1000
|
private readonly frame: {
|
|
@@ -1120,7 +1140,20 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1120
1140
|
async evaluateInFrame<T>(frameId: string, fn: string | (() => T)): Promise<T> {
|
|
1121
1141
|
await this.initialize();
|
|
1122
1142
|
const contextId = await this.getFrameExecutionContextId(frameId);
|
|
1123
|
-
|
|
1143
|
+
try {
|
|
1144
|
+
return await this.evaluateWithContext<T>(fn, contextId);
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
if (!isMissingExecutionContextError(error)) {
|
|
1147
|
+
throw error;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (this.frameExecutionContexts.get(frameId) === contextId) {
|
|
1151
|
+
this.frameExecutionContexts.delete(frameId);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const refreshedContextId = await this.getFrameExecutionContextId(frameId);
|
|
1155
|
+
return await this.evaluateWithContext<T>(fn, refreshedContextId);
|
|
1156
|
+
}
|
|
1124
1157
|
}
|
|
1125
1158
|
|
|
1126
1159
|
async waitForSelectorInFrame(
|
|
@@ -1384,6 +1417,21 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1384
1417
|
this.frameExecutionContexts.set(context.frameId, context.id);
|
|
1385
1418
|
}
|
|
1386
1419
|
});
|
|
1420
|
+
this.pageClient.on("Runtime.executionContextDestroyed", (params) => {
|
|
1421
|
+
const destroyedContextId = getCdpDestroyedExecutionContextId(params);
|
|
1422
|
+
if (destroyedContextId === undefined) {
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
for (const [frameId, contextId] of this.frameExecutionContexts) {
|
|
1427
|
+
if (contextId === destroyedContextId) {
|
|
1428
|
+
this.frameExecutionContexts.delete(frameId);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
this.pageClient.on("Runtime.executionContextsCleared", () => {
|
|
1433
|
+
this.frameExecutionContexts.clear();
|
|
1434
|
+
});
|
|
1387
1435
|
await this.pageClient.send("Page.enable");
|
|
1388
1436
|
await this.pageClient.send("Runtime.enable");
|
|
1389
1437
|
this.initialized = true;
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ProxyProtocol,
|
|
5
5
|
ProxyResolutionTelemetryEvent,
|
|
6
6
|
ProxyTelemetrySink,
|
|
7
|
+
ProxyUserAgentSource,
|
|
7
8
|
ProxyVendorFailoverTelemetryEvent,
|
|
8
9
|
ProxyVendorName,
|
|
9
10
|
SmartproxyAllocatorBodyClass,
|
|
@@ -15,6 +16,7 @@ type ProviderTelemetryHeader = {
|
|
|
15
16
|
v: 1;
|
|
16
17
|
proxy?: {
|
|
17
18
|
provider: ProxyVendorName;
|
|
19
|
+
userAgentSource?: ProxyUserAgentSource;
|
|
18
20
|
protocol?: ProxyProtocol;
|
|
19
21
|
cacheStatus: ProxyCacheStatus;
|
|
20
22
|
cacheHit: boolean;
|
|
@@ -98,6 +100,7 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
|
|
|
98
100
|
recordProxyResolution(event: ProxyResolutionTelemetryEvent): void {
|
|
99
101
|
this.#events.push({
|
|
100
102
|
provider: event.provider,
|
|
103
|
+
...(event.userAgentSource ? { userAgentSource: event.userAgentSource } : {}),
|
|
101
104
|
...(event.protocol ? { protocol: event.protocol } : {}),
|
|
102
105
|
cacheStatus: event.cacheStatus,
|
|
103
106
|
cacheHit: event.cacheHit,
|
|
@@ -175,6 +178,7 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
|
|
|
175
178
|
const aggregate = rest.reduce<ProxyResolutionTelemetryEvent>(
|
|
176
179
|
(acc, event) => ({
|
|
177
180
|
provider: event.provider,
|
|
181
|
+
userAgentSource: event.userAgentSource ?? acc.userAgentSource,
|
|
178
182
|
cacheStatus: worseStatus(acc.cacheStatus, event.cacheStatus),
|
|
179
183
|
cacheHit: acc.cacheHit && event.cacheHit,
|
|
180
184
|
resolutionMs: acc.resolutionMs + event.resolutionMs,
|
|
@@ -196,6 +200,7 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
|
|
|
196
200
|
v: 1,
|
|
197
201
|
proxy: {
|
|
198
202
|
provider: serving.provider,
|
|
203
|
+
...(aggregate.userAgentSource ? { userAgentSource: aggregate.userAgentSource } : {}),
|
|
199
204
|
...(serving.protocol ? { protocol: serving.protocol } : {}),
|
|
200
205
|
cacheStatus: aggregate.cacheStatus,
|
|
201
206
|
cacheHit: aggregate.cacheHit,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
APIFUSE__CDP_POOL__URL,
|
|
3
|
+
APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
|
|
4
|
+
APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
|
|
5
|
+
APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
|
|
6
|
+
APIFUSE__RESOLVER__TIMEOUT_MS,
|
|
7
|
+
bindResolverSignal,
|
|
8
|
+
createResolverClient,
|
|
9
|
+
createResolverClientFromEnv,
|
|
10
|
+
createUnsupportedResolverClient,
|
|
11
|
+
DEFAULT_RESOLVER_TIMEOUT_MS,
|
|
12
|
+
invalidateResolverSolution,
|
|
13
|
+
RESOLVER_ADAPTER_REGISTRY,
|
|
14
|
+
RESOLVER_INSTRUMENTATION_METADATA,
|
|
15
|
+
type ResolverAdapterFactory,
|
|
16
|
+
type ResolverInstrumentationMetadata,
|
|
17
|
+
type ResolverRuntimeOptions,
|
|
18
|
+
} from "./resolver.js";
|
|
@@ -316,8 +316,9 @@ export function createBrowserResolverVendorAdapter(
|
|
|
316
316
|
},
|
|
317
317
|
|
|
318
318
|
async solve(challenge, identity, callerSignal, traceRecorder) {
|
|
319
|
-
|
|
320
|
-
|
|
319
|
+
const cdpUrl = options.cdpUrl?.trim();
|
|
320
|
+
const proxyUrl = identity?.proxyUrl;
|
|
321
|
+
if (!cdpUrl && proxyUrl === undefined) {
|
|
321
322
|
throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "missing_credentials");
|
|
322
323
|
}
|
|
323
324
|
if (!isSupportedKind(challenge.kind)) {
|
|
@@ -326,6 +327,14 @@ export function createBrowserResolverVendorAdapter(
|
|
|
326
327
|
assertResolverHostAllowed(challenge.pageUrl, options.allowedHosts);
|
|
327
328
|
const challengeKind = challenge.kind;
|
|
328
329
|
callerSignal.throwIfAborted();
|
|
330
|
+
if (proxyUrl !== undefined && proxyUrl.length === 0) {
|
|
331
|
+
throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "missing_proxy_identity");
|
|
332
|
+
}
|
|
333
|
+
if (cdpUrl && proxyUrl !== undefined) {
|
|
334
|
+
// The current pool acquire protocol cannot bind a proxy to its browser context.
|
|
335
|
+
// Refuse so the chain can choose a vendor that can honor the resolved identity.
|
|
336
|
+
throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "not_implemented");
|
|
337
|
+
}
|
|
329
338
|
|
|
330
339
|
const solveController = new AbortController();
|
|
331
340
|
const onCallerAbort = () => solveController.abort(abortReason(callerSignal));
|
|
@@ -340,8 +349,9 @@ export function createBrowserResolverVendorAdapter(
|
|
|
340
349
|
try {
|
|
341
350
|
client = createClient({
|
|
342
351
|
allowedHosts: [...options.allowedHosts],
|
|
343
|
-
cdpUrl:
|
|
344
|
-
|
|
352
|
+
cdpUrl: cdpUrl ?? "",
|
|
353
|
+
...(proxyUrl === undefined ? {} : { proxy: proxyUrl }),
|
|
354
|
+
requireCdpPool: cdpUrl !== undefined,
|
|
345
355
|
});
|
|
346
356
|
const contextOperation = client.withIsolatedContext(async (page) => {
|
|
347
357
|
handlerEntered = true;
|
|
@@ -4,6 +4,7 @@ import { assertResolverHostAllowed } from "./hosts.js";
|
|
|
4
4
|
import {
|
|
5
5
|
type ResolverIdentity,
|
|
6
6
|
type ResolverVendorAdapter,
|
|
7
|
+
ResolverChallengeVerdictError,
|
|
7
8
|
ResolverVendorUnavailableError,
|
|
8
9
|
resolverVendorSupports,
|
|
9
10
|
} from "./types.js";
|
|
@@ -64,10 +65,53 @@ function abortReason(signal: AbortSignal): unknown {
|
|
|
64
65
|
return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
65
66
|
}
|
|
66
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
|
+
|
|
67
110
|
function raceWithAbort<T>(
|
|
68
111
|
operation: () => Promise<T>,
|
|
69
112
|
signal: AbortSignal,
|
|
70
113
|
phase?: TwoCaptchaOperationPhase,
|
|
114
|
+
sensitiveValues: readonly string[] = [],
|
|
71
115
|
): Promise<T> {
|
|
72
116
|
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
73
117
|
|
|
@@ -91,7 +135,7 @@ function raceWithAbort<T>(
|
|
|
91
135
|
}
|
|
92
136
|
reject(
|
|
93
137
|
new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
|
|
94
|
-
|
|
138
|
+
...safeCauseOptions(error, sensitiveValues),
|
|
95
139
|
phase,
|
|
96
140
|
}),
|
|
97
141
|
);
|
|
@@ -158,10 +202,17 @@ function isAllocationExhausted(payload: JsonRecord): boolean {
|
|
|
158
202
|
);
|
|
159
203
|
}
|
|
160
204
|
|
|
205
|
+
function isNegativeVerdict(payload: JsonRecord): boolean {
|
|
206
|
+
return errorText(payload, "errorCode").toLowerCase() === "error_captcha_unsolvable";
|
|
207
|
+
}
|
|
208
|
+
|
|
161
209
|
function unavailableForPayload(
|
|
162
210
|
payload: JsonRecord,
|
|
163
211
|
phase: TwoCaptchaOperationPhase,
|
|
164
|
-
): ResolverVendorUnavailableError {
|
|
212
|
+
): ResolverVendorUnavailableError | ResolverChallengeVerdictError {
|
|
213
|
+
if (isNegativeVerdict(payload)) {
|
|
214
|
+
return new ResolverChallengeVerdictError(TWOCAPTCHA_VENDOR_ID, "solve_failed");
|
|
215
|
+
}
|
|
165
216
|
return new ResolverVendorUnavailableError(
|
|
166
217
|
TWOCAPTCHA_VENDOR_ID,
|
|
167
218
|
isAllocationExhausted(payload) ? "allocation_exhausted" : "transport_failure",
|
|
@@ -175,6 +226,7 @@ async function postJson(
|
|
|
175
226
|
body: JsonRecord,
|
|
176
227
|
signal: AbortSignal,
|
|
177
228
|
phase: TwoCaptchaOperationPhase,
|
|
229
|
+
sensitiveValues: readonly string[],
|
|
178
230
|
): Promise<{ readonly ok: boolean; readonly payload: JsonRecord }> {
|
|
179
231
|
const response = await raceWithAbort(
|
|
180
232
|
() =>
|
|
@@ -187,6 +239,7 @@ async function postJson(
|
|
|
187
239
|
}),
|
|
188
240
|
signal,
|
|
189
241
|
phase,
|
|
242
|
+
sensitiveValues,
|
|
190
243
|
);
|
|
191
244
|
|
|
192
245
|
let responseText: string;
|
|
@@ -222,9 +275,12 @@ function taskIdFrom(payload: JsonRecord): string | number | undefined {
|
|
|
222
275
|
return typeof taskId === "string" || typeof taskId === "number" ? taskId : undefined;
|
|
223
276
|
}
|
|
224
277
|
|
|
225
|
-
function tokenFrom(payload: JsonRecord): string | undefined {
|
|
278
|
+
function tokenFrom(payload: JsonRecord, challenge: ProviderChallenge): string | undefined {
|
|
226
279
|
const solution = payload.solution;
|
|
227
280
|
if (!isJsonRecord(solution)) return undefined;
|
|
281
|
+
if (challenge.kind === "aws_waf") {
|
|
282
|
+
return typeof solution.existing_token === "string" ? solution.existing_token : undefined;
|
|
283
|
+
}
|
|
228
284
|
if (typeof solution.gRecaptchaResponse === "string") return solution.gRecaptchaResponse;
|
|
229
285
|
return typeof solution.token === "string" ? solution.token : undefined;
|
|
230
286
|
}
|
|
@@ -282,8 +338,18 @@ export function createTwoCaptchaResolverVendorAdapter(
|
|
|
282
338
|
if (!resolverVendorSupports(TWOCAPTCHA_VENDOR_ID, challenge.kind)) {
|
|
283
339
|
throw new TypeError(`2captcha resolver does not support ${challenge.kind}`);
|
|
284
340
|
}
|
|
285
|
-
if (challenge.kind !== "recaptcha_v2") {
|
|
286
|
-
|
|
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
|
+
) {
|
|
287
353
|
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
|
|
288
354
|
phase: "create_task",
|
|
289
355
|
});
|
|
@@ -310,22 +376,33 @@ export function createTwoCaptchaResolverVendorAdapter(
|
|
|
310
376
|
|
|
311
377
|
try {
|
|
312
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
|
+
};
|
|
313
399
|
const createResult = await postJson(
|
|
314
400
|
fetchImpl,
|
|
315
401
|
endpoint(baseUrl, "createTask"),
|
|
316
|
-
{
|
|
317
|
-
clientKey: apiKey,
|
|
318
|
-
task: {
|
|
319
|
-
type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
|
|
320
|
-
websiteURL: challenge.pageUrl,
|
|
321
|
-
websiteKey: challenge.siteKey,
|
|
322
|
-
isInvisible: false,
|
|
323
|
-
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
324
|
-
...(proxy ?? {}),
|
|
325
|
-
},
|
|
326
|
-
},
|
|
402
|
+
{ clientKey: apiKey, task },
|
|
327
403
|
solveController.signal,
|
|
328
404
|
phase,
|
|
405
|
+
[apiKey],
|
|
329
406
|
);
|
|
330
407
|
const taskId = taskIdFrom(createResult.payload);
|
|
331
408
|
if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
|
|
@@ -359,6 +436,7 @@ export function createTwoCaptchaResolverVendorAdapter(
|
|
|
359
436
|
{ clientKey: apiKey, taskId },
|
|
360
437
|
solveController.signal,
|
|
361
438
|
phase,
|
|
439
|
+
[apiKey],
|
|
362
440
|
);
|
|
363
441
|
if (!pollResult.ok || pollResult.payload.errorId !== 0) {
|
|
364
442
|
throw unavailableForPayload(pollResult.payload, phase);
|
|
@@ -372,7 +450,7 @@ export function createTwoCaptchaResolverVendorAdapter(
|
|
|
372
450
|
);
|
|
373
451
|
}
|
|
374
452
|
|
|
375
|
-
const token = tokenFrom(pollResult.payload);
|
|
453
|
+
const token = tokenFrom(pollResult.payload, challenge);
|
|
376
454
|
if (!token?.trim()) {
|
|
377
455
|
throw new ResolverVendorUnavailableError(
|
|
378
456
|
TWOCAPTCHA_VENDOR_ID,
|
|
@@ -403,9 +481,14 @@ export function createTwoCaptchaResolverVendorAdapter(
|
|
|
403
481
|
phase,
|
|
404
482
|
});
|
|
405
483
|
}
|
|
406
|
-
if (
|
|
484
|
+
if (
|
|
485
|
+
error instanceof ResolverVendorUnavailableError ||
|
|
486
|
+
error instanceof ResolverChallengeVerdictError
|
|
487
|
+
) {
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
407
490
|
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
|
|
408
|
-
|
|
491
|
+
...safeCauseOptions(error, [apiKey]),
|
|
409
492
|
phase,
|
|
410
493
|
});
|
|
411
494
|
} finally {
|
|
@@ -113,13 +113,14 @@ export interface ResolverVendorAdapter {
|
|
|
113
113
|
export type ResolverVendorUnavailableReason =
|
|
114
114
|
| "missing_credentials"
|
|
115
115
|
| "missing_proxy_identity"
|
|
116
|
+
| "missing_client_profile"
|
|
116
117
|
| "missing_transport"
|
|
117
118
|
| "allocation_exhausted"
|
|
118
119
|
| "transport_failure"
|
|
119
120
|
| "timeout"
|
|
120
121
|
| "not_implemented";
|
|
121
122
|
|
|
122
|
-
export type ResolverChallengeVerdictReason = "human_puzzle";
|
|
123
|
+
export type ResolverChallengeVerdictReason = "human_puzzle" | "solve_failed";
|
|
123
124
|
|
|
124
125
|
type ResolverErrorOptions = {
|
|
125
126
|
/** Raw cause; adapters must not place bodies, cookies, headers, credentials, or proxy URLs here. */
|
|
@@ -159,7 +160,11 @@ export class ResolverChallengeVerdictError extends Error {
|
|
|
159
160
|
readonly reason: ResolverChallengeVerdictReason,
|
|
160
161
|
options: ResolverErrorOptions = {},
|
|
161
162
|
) {
|
|
162
|
-
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
|
+
);
|
|
163
168
|
this.name = "ResolverChallengeVerdictError";
|
|
164
169
|
if (options.cause !== undefined) {
|
|
165
170
|
this.cause = options.cause;
|
package/src/runtime/resolver.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
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,
|
|
@@ -42,6 +48,7 @@ import {
|
|
|
42
48
|
APIFUSE__RESOLVER__TIMEOUT_MS,
|
|
43
49
|
DEFAULT_RESOLVER_TIMEOUT_MS,
|
|
44
50
|
} from "./resolver-config.js";
|
|
51
|
+
import { DEFAULT_PROFILE } from "./stealth.js";
|
|
45
52
|
import type { TraceRecorder } from "./trace.js";
|
|
46
53
|
|
|
47
54
|
export {
|
|
@@ -94,8 +101,14 @@ type ResolverChainClient = ResolverContext & {
|
|
|
94
101
|
export interface ResolverRuntimeOptions {
|
|
95
102
|
readonly allowedHosts?: readonly string[];
|
|
96
103
|
readonly cache?: ProviderCache;
|
|
97
|
-
/**
|
|
98
|
-
readonly
|
|
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
|
+
};
|
|
99
112
|
/** Server-owned context/proxy scope used only for identity-bound cache entries. */
|
|
100
113
|
readonly identityScope?: string;
|
|
101
114
|
/** SDK-owned transport already bound to the resolved proxy lease and client profile. */
|
|
@@ -212,6 +225,24 @@ export function swapResolverAdapterFactoryForTests(
|
|
|
212
225
|
};
|
|
213
226
|
}
|
|
214
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
|
+
|
|
215
246
|
// This is the sole allowlist for declared vendors whose registry entry may be absent.
|
|
216
247
|
// Remove a vendor here when its adapter is registered.
|
|
217
248
|
const KNOWN_UNIMPLEMENTED_RESOLVER_VENDORS: ReadonlySet<ProviderResolverVendor> = new Set([
|
|
@@ -340,6 +371,7 @@ function adapterRequiresTransport(
|
|
|
340
371
|
function sanitizeDiagnosticUrl(rawUrl: string): string {
|
|
341
372
|
try {
|
|
342
373
|
const parsed = new URL(rawUrl);
|
|
374
|
+
if (parsed.username || parsed.password) return "[REDACTED_PROXY_URL]";
|
|
343
375
|
return `${parsed.protocol}//${parsed.host}`;
|
|
344
376
|
} catch {
|
|
345
377
|
return "[REDACTED_URL]";
|
|
@@ -355,7 +387,12 @@ function sanitizeCauseMessage(message: string): string {
|
|
|
355
387
|
.split(/\s+/)
|
|
356
388
|
.filter(Boolean)
|
|
357
389
|
.map((token) => {
|
|
358
|
-
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;
|
|
359
396
|
const word = token.replace(/^[^a-z\d]+|[^a-z\d]+$/gi, "");
|
|
360
397
|
return word.length > 0 &&
|
|
361
398
|
word.length <= 32 &&
|
|
@@ -704,13 +741,58 @@ export async function invalidateResolverSolution(
|
|
|
704
741
|
});
|
|
705
742
|
}
|
|
706
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
|
+
|
|
707
789
|
function createResolverChainClient(options: {
|
|
708
790
|
readonly kinds: readonly ProviderChallengeKind[];
|
|
709
791
|
readonly entries: readonly ResolverChainEntry[];
|
|
710
792
|
readonly unavailableReason?: string;
|
|
711
793
|
readonly cache?: ProviderCache;
|
|
712
794
|
readonly identity?: ResolverIdentity;
|
|
713
|
-
readonly
|
|
795
|
+
readonly proxyIntent?: ResolverRuntimeOptions["proxyIntent"];
|
|
714
796
|
readonly identityScope?: string;
|
|
715
797
|
readonly transport?: ResolverVendorTransport;
|
|
716
798
|
readonly createTransport?: ResolverRuntimeOptions["createTransport"];
|
|
@@ -735,16 +817,21 @@ function createResolverChainClient(options: {
|
|
|
735
817
|
const supportingEntries = options.entries.filter((entry) => entry.supports(challenge.kind));
|
|
736
818
|
if (supportingEntries.length === 0) throwUnsupportedKind(challenge.kind);
|
|
737
819
|
signal.throwIfAborted();
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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.
|
|
741
828
|
const requiredProxyIdentityMissing =
|
|
742
|
-
options.
|
|
829
|
+
options.proxyIntent?.mode === "required" && identity === undefined;
|
|
743
830
|
if (requiredProxyIdentityMissing) {
|
|
744
831
|
throwExhausted(
|
|
745
832
|
supportingEntries.map((entry) => ({
|
|
746
833
|
vendor: entry.id,
|
|
747
|
-
reason: "missing_proxy_identity"
|
|
834
|
+
reason: identityResolution.unavailableReason ?? "missing_proxy_identity",
|
|
748
835
|
})),
|
|
749
836
|
);
|
|
750
837
|
}
|
|
@@ -752,12 +839,11 @@ function createResolverChainClient(options: {
|
|
|
752
839
|
const cached = await findCachedSolution(
|
|
753
840
|
options.cache,
|
|
754
841
|
challenge,
|
|
755
|
-
|
|
842
|
+
identity,
|
|
756
843
|
options.identityScope,
|
|
757
844
|
);
|
|
758
845
|
if (cached) return cached;
|
|
759
846
|
}
|
|
760
|
-
|
|
761
847
|
const attempts: ResolverChainAttempt[] = [];
|
|
762
848
|
for (const entry of supportingEntries) {
|
|
763
849
|
const adapter = entry.createAdapter();
|
|
@@ -778,7 +864,7 @@ function createResolverChainClient(options: {
|
|
|
778
864
|
const transport = unrestrictedTransport
|
|
779
865
|
? restrictResolverTransport(unrestrictedTransport, options.allowedHosts ?? [])
|
|
780
866
|
: undefined;
|
|
781
|
-
return adapter.solve(challenge,
|
|
867
|
+
return adapter.solve(challenge, identity, signal, traceRecorder, transport);
|
|
782
868
|
};
|
|
783
869
|
const solution = traceRecorder
|
|
784
870
|
? await traceRecorder.runSpan("resolver.vendor.attempt", solveAttempt, {
|
|
@@ -786,6 +872,7 @@ function createResolverChainClient(options: {
|
|
|
786
872
|
vendor: adapter.id,
|
|
787
873
|
challenge_kind: challenge.kind,
|
|
788
874
|
client_profile: options.clientProfile,
|
|
875
|
+
resolver_identity_source: identityResolution.userAgentSource,
|
|
789
876
|
},
|
|
790
877
|
onError(error) {
|
|
791
878
|
return error instanceof ResolverVendorUnavailableError
|
|
@@ -801,9 +888,9 @@ function createResolverChainClient(options: {
|
|
|
801
888
|
solutionExpiryMs(solution) !== undefined
|
|
802
889
|
) {
|
|
803
890
|
const issuingIdentity =
|
|
804
|
-
adapter.getIssuingIdentity?.(solution,
|
|
891
|
+
adapter.getIssuingIdentity?.(solution, identity, challenge) ??
|
|
805
892
|
resolverChallengeIssuingIdentity(challenge, {
|
|
806
|
-
...(
|
|
893
|
+
...(identity ? { proxyUrl: identity.proxyUrl } : {}),
|
|
807
894
|
userAgent: solution.userAgent,
|
|
808
895
|
});
|
|
809
896
|
if (issuingIdentity) {
|
|
@@ -837,7 +924,7 @@ export function createResolverClient(options: {
|
|
|
837
924
|
readonly unavailableReason?: string;
|
|
838
925
|
readonly cache?: ProviderCache;
|
|
839
926
|
readonly identity?: ResolverIdentity;
|
|
840
|
-
readonly
|
|
927
|
+
readonly proxyIntent?: ResolverRuntimeOptions["proxyIntent"];
|
|
841
928
|
readonly transport?: ResolverVendorTransport;
|
|
842
929
|
readonly createTransport?: ResolverRuntimeOptions["createTransport"];
|
|
843
930
|
readonly clientProfile?: string;
|
|
@@ -853,7 +940,7 @@ export function createResolverClient(options: {
|
|
|
853
940
|
unavailableReason: options.unavailableReason,
|
|
854
941
|
cache: options.cache,
|
|
855
942
|
identity: options.identity,
|
|
856
|
-
|
|
943
|
+
proxyIntent: options.proxyIntent,
|
|
857
944
|
transport: options.transport,
|
|
858
945
|
createTransport: options.createTransport,
|
|
859
946
|
clientProfile: options.clientProfile,
|
|
@@ -945,7 +1032,7 @@ function createResolverClientFromEnvInternal(
|
|
|
945
1032
|
};
|
|
946
1033
|
}),
|
|
947
1034
|
cache: options.cache,
|
|
948
|
-
|
|
1035
|
+
proxyIntent: options.proxyIntent,
|
|
949
1036
|
identityScope: options.identityScope,
|
|
950
1037
|
transport: options.transport,
|
|
951
1038
|
createTransport: options.createTransport,
|
package/src/runtime/stealth.ts
CHANGED
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
serializeRequestUrl,
|
|
68
68
|
} from "./request-options.js";
|
|
69
69
|
|
|
70
|
-
const DEFAULT_PROFILE = "chrome-146";
|
|
70
|
+
export const DEFAULT_PROFILE = "chrome-146";
|
|
71
71
|
|
|
72
72
|
const MISSING_PROXY_WARNING =
|
|
73
73
|
"[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
|