@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
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ProviderError } from "../errors.js";
|
|
2
|
+
export const RESOLVER_INSTRUMENTATION_METADATA = Symbol.for("@apifuse/provider-sdk/runtime/resolver-instrumentation-metadata");
|
|
3
|
+
export function createUnsupportedResolverClient(reason) {
|
|
4
|
+
return {
|
|
5
|
+
async solve() {
|
|
6
|
+
throw new ProviderError(reason ?? "Resolver runtime is not configured", {
|
|
7
|
+
code: "RESOLVER_UNAVAILABLE",
|
|
8
|
+
fix: "Declare resolver on the provider definition and configure vendor credentials.",
|
|
9
|
+
});
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -204,8 +204,9 @@ export function createBrowserResolverVendorAdapter(options) {
|
|
|
204
204
|
});
|
|
205
205
|
},
|
|
206
206
|
async solve(challenge, identity, callerSignal, traceRecorder) {
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
const cdpUrl = options.cdpUrl?.trim();
|
|
208
|
+
const proxyUrl = identity?.proxyUrl;
|
|
209
|
+
if (!cdpUrl && proxyUrl === undefined) {
|
|
209
210
|
throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "missing_credentials");
|
|
210
211
|
}
|
|
211
212
|
if (!isSupportedKind(challenge.kind)) {
|
|
@@ -214,6 +215,14 @@ export function createBrowserResolverVendorAdapter(options) {
|
|
|
214
215
|
assertResolverHostAllowed(challenge.pageUrl, options.allowedHosts);
|
|
215
216
|
const challengeKind = challenge.kind;
|
|
216
217
|
callerSignal.throwIfAborted();
|
|
218
|
+
if (proxyUrl !== undefined && proxyUrl.length === 0) {
|
|
219
|
+
throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "missing_proxy_identity");
|
|
220
|
+
}
|
|
221
|
+
if (cdpUrl && proxyUrl !== undefined) {
|
|
222
|
+
// The current pool acquire protocol cannot bind a proxy to its browser context.
|
|
223
|
+
// Refuse so the chain can choose a vendor that can honor the resolved identity.
|
|
224
|
+
throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "not_implemented");
|
|
225
|
+
}
|
|
217
226
|
const solveController = new AbortController();
|
|
218
227
|
const onCallerAbort = () => solveController.abort(abortReason(callerSignal));
|
|
219
228
|
callerSignal.addEventListener("abort", onCallerAbort, { once: true });
|
|
@@ -223,8 +232,9 @@ export function createBrowserResolverVendorAdapter(options) {
|
|
|
223
232
|
try {
|
|
224
233
|
client = createClient({
|
|
225
234
|
allowedHosts: [...options.allowedHosts],
|
|
226
|
-
cdpUrl:
|
|
227
|
-
|
|
235
|
+
cdpUrl: cdpUrl ?? "",
|
|
236
|
+
...(proxyUrl === undefined ? {} : { proxy: proxyUrl }),
|
|
237
|
+
requireCdpPool: cdpUrl !== undefined,
|
|
228
238
|
});
|
|
229
239
|
const contextOperation = client.withIsolatedContext(async (page) => {
|
|
230
240
|
handlerEntered = true;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChallengeSolution, ProviderChallenge } from "../../types.js";
|
|
2
|
+
import type { TraceRecorder } from "../trace.js";
|
|
2
3
|
import { type ResolverIdentity, type ResolverVendorAdapter } from "./types.js";
|
|
3
4
|
type Delay = (ms: number, signal: AbortSignal) => Promise<void>;
|
|
4
5
|
export interface TwoCaptchaResolverVendorOptions {
|
|
@@ -15,7 +16,7 @@ export interface TwoCaptchaResolverVendorOptions {
|
|
|
15
16
|
}
|
|
16
17
|
export interface TwoCaptchaResolverVendorAdapter extends ResolverVendorAdapter {
|
|
17
18
|
readonly id: "2captcha";
|
|
18
|
-
solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal): Promise<Extract<ChallengeSolution, {
|
|
19
|
+
solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder): Promise<Extract<ChallengeSolution, {
|
|
19
20
|
readonly form: "token";
|
|
20
21
|
}>>;
|
|
21
22
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assertResolverHostAllowed } from "./hosts.js";
|
|
2
|
-
import { ResolverVendorUnavailableError, resolverVendorSupports, } from "./types.js";
|
|
2
|
+
import { ResolverChallengeVerdictError, ResolverVendorUnavailableError, resolverVendorSupports, } from "./types.js";
|
|
3
3
|
const TWOCAPTCHA_VENDOR_ID = "2captcha";
|
|
4
4
|
const DEFAULT_TWOCAPTCHA_BASE_URL = "https://api.2captcha.com";
|
|
5
5
|
const DEFAULT_POLL_INTERVAL_MS = 3_000;
|
|
@@ -16,7 +16,46 @@ function isJsonRecord(value) {
|
|
|
16
16
|
function abortReason(signal) {
|
|
17
17
|
return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
18
18
|
}
|
|
19
|
-
function
|
|
19
|
+
function containsSensitiveValue(value, sensitiveValues) {
|
|
20
|
+
const secrets = sensitiveValues.filter((secret) => secret.length > 0);
|
|
21
|
+
if (secrets.length === 0)
|
|
22
|
+
return false;
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
const inspect = (candidate) => {
|
|
25
|
+
if (typeof candidate === "string") {
|
|
26
|
+
return secrets.some((secret) => candidate.includes(secret));
|
|
27
|
+
}
|
|
28
|
+
if (candidate === null ||
|
|
29
|
+
(typeof candidate !== "object" && typeof candidate !== "function")) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (seen.has(candidate))
|
|
33
|
+
return false;
|
|
34
|
+
seen.add(candidate);
|
|
35
|
+
try {
|
|
36
|
+
for (const property of Reflect.ownKeys(candidate)) {
|
|
37
|
+
if (typeof property === "string" && inspect(property))
|
|
38
|
+
return true;
|
|
39
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, property);
|
|
40
|
+
if (!descriptor)
|
|
41
|
+
return true;
|
|
42
|
+
if ("value" in descriptor && inspect(descriptor.value))
|
|
43
|
+
return true;
|
|
44
|
+
if (descriptor.get !== undefined || descriptor.set !== undefined)
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
};
|
|
53
|
+
return inspect(value);
|
|
54
|
+
}
|
|
55
|
+
function safeCauseOptions(error, sensitiveValues) {
|
|
56
|
+
return containsSensitiveValue(error, sensitiveValues) ? {} : { cause: error };
|
|
57
|
+
}
|
|
58
|
+
function raceWithAbort(operation, signal, phase, sensitiveValues = []) {
|
|
20
59
|
if (signal.aborted)
|
|
21
60
|
return Promise.reject(abortReason(signal));
|
|
22
61
|
return new Promise((resolve, reject) => {
|
|
@@ -36,7 +75,7 @@ function raceWithAbort(operation, signal, phase) {
|
|
|
36
75
|
return;
|
|
37
76
|
}
|
|
38
77
|
reject(new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
|
|
39
|
-
|
|
78
|
+
...safeCauseOptions(error, sensitiveValues),
|
|
40
79
|
phase,
|
|
41
80
|
}));
|
|
42
81
|
});
|
|
@@ -92,17 +131,23 @@ function isAllocationExhausted(payload) {
|
|
|
92
131
|
return (code === "error_zero_balance" ||
|
|
93
132
|
/(?:insufficient|zero|no|not enough)\s+(?:balance|funds|credit)/u.test(`${code} ${description}`));
|
|
94
133
|
}
|
|
134
|
+
function isNegativeVerdict(payload) {
|
|
135
|
+
return errorText(payload, "errorCode").toLowerCase() === "error_captcha_unsolvable";
|
|
136
|
+
}
|
|
95
137
|
function unavailableForPayload(payload, phase) {
|
|
138
|
+
if (isNegativeVerdict(payload)) {
|
|
139
|
+
return new ResolverChallengeVerdictError(TWOCAPTCHA_VENDOR_ID, "solve_failed");
|
|
140
|
+
}
|
|
96
141
|
return new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, isAllocationExhausted(payload) ? "allocation_exhausted" : "transport_failure", { phase });
|
|
97
142
|
}
|
|
98
|
-
async function postJson(fetchImpl, url, body, signal, phase) {
|
|
143
|
+
async function postJson(fetchImpl, url, body, signal, phase, sensitiveValues) {
|
|
99
144
|
const response = await raceWithAbort(() => fetchImpl(url, {
|
|
100
145
|
method: "POST",
|
|
101
146
|
headers: { "content-type": "application/json" },
|
|
102
147
|
body: JSON.stringify(body),
|
|
103
148
|
signal,
|
|
104
149
|
redirect: "error",
|
|
105
|
-
}), signal, phase);
|
|
150
|
+
}), signal, phase, sensitiveValues);
|
|
106
151
|
let responseText;
|
|
107
152
|
try {
|
|
108
153
|
responseText = await raceWithAbort(() => response.text(), signal, phase);
|
|
@@ -136,10 +181,13 @@ function taskIdFrom(payload) {
|
|
|
136
181
|
const taskId = payload.taskId;
|
|
137
182
|
return typeof taskId === "string" || typeof taskId === "number" ? taskId : undefined;
|
|
138
183
|
}
|
|
139
|
-
function tokenFrom(payload) {
|
|
184
|
+
function tokenFrom(payload, challenge) {
|
|
140
185
|
const solution = payload.solution;
|
|
141
186
|
if (!isJsonRecord(solution))
|
|
142
187
|
return undefined;
|
|
188
|
+
if (challenge.kind === "aws_waf") {
|
|
189
|
+
return typeof solution.existing_token === "string" ? solution.existing_token : undefined;
|
|
190
|
+
}
|
|
143
191
|
if (typeof solution.gRecaptchaResponse === "string")
|
|
144
192
|
return solution.gRecaptchaResponse;
|
|
145
193
|
return typeof solution.token === "string" ? solution.token : undefined;
|
|
@@ -147,6 +195,24 @@ function tokenFrom(payload) {
|
|
|
147
195
|
function endpoint(baseUrl, path) {
|
|
148
196
|
return `${baseUrl.replace(/\/+$/u, "")}/${path}`;
|
|
149
197
|
}
|
|
198
|
+
function spanErrorAttributes(error, phase) {
|
|
199
|
+
if (error instanceof ResolverVendorUnavailableError) {
|
|
200
|
+
return {
|
|
201
|
+
unavailability_reason: error.reason,
|
|
202
|
+
transport_phase: error.phase,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// The solve budget raises TwoCaptchaSolveTimeoutError inside the spanned closure and
|
|
206
|
+
// the outer catch converts it only after the span has been finalized, so the span has
|
|
207
|
+
// to recognize it here or slow solves lose exactly the attribution these spans add.
|
|
208
|
+
if (error instanceof TwoCaptchaSolveTimeoutError) {
|
|
209
|
+
return {
|
|
210
|
+
unavailability_reason: "timeout",
|
|
211
|
+
transport_phase: phase,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
150
216
|
export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
151
217
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
152
218
|
const baseUrl = options.baseUrl ?? DEFAULT_TWOCAPTCHA_BASE_URL;
|
|
@@ -159,7 +225,7 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
|
159
225
|
supports(kind) {
|
|
160
226
|
return resolverVendorSupports(TWOCAPTCHA_VENDOR_ID, kind);
|
|
161
227
|
},
|
|
162
|
-
async solve(challenge, identity, callerSignal) {
|
|
228
|
+
async solve(challenge, identity, callerSignal, traceRecorder) {
|
|
163
229
|
const apiKey = options.apiKey?.trim();
|
|
164
230
|
if (!apiKey) {
|
|
165
231
|
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "missing_credentials", {
|
|
@@ -169,8 +235,16 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
|
169
235
|
if (!resolverVendorSupports(TWOCAPTCHA_VENDOR_ID, challenge.kind)) {
|
|
170
236
|
throw new TypeError(`2captcha resolver does not support ${challenge.kind}`);
|
|
171
237
|
}
|
|
172
|
-
if (challenge.kind !== "recaptcha_v2") {
|
|
173
|
-
|
|
238
|
+
if (challenge.kind !== "recaptcha_v2" && challenge.kind !== "aws_waf") {
|
|
239
|
+
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
|
|
240
|
+
phase: "create_task",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (challenge.kind === "aws_waf" &&
|
|
244
|
+
(!challenge.siteKey?.trim() ||
|
|
245
|
+
!challenge.captchaScript?.trim() ||
|
|
246
|
+
!challenge.context?.trim() ||
|
|
247
|
+
!challenge.iv?.trim())) {
|
|
174
248
|
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
|
|
175
249
|
phase: "create_task",
|
|
176
250
|
});
|
|
@@ -192,50 +266,78 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
|
192
266
|
const startedAt = now();
|
|
193
267
|
let phase = "create_task";
|
|
194
268
|
try {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
throw new TwoCaptchaSolveTimeoutError();
|
|
220
|
-
const pollResult = await postJson(fetchImpl, endpoint(baseUrl, "getTaskResult"), { clientKey: apiKey, taskId }, solveController.signal, phase);
|
|
221
|
-
if (!pollResult.ok || pollResult.payload.errorId !== 0) {
|
|
222
|
-
throw unavailableForPayload(pollResult.payload, phase);
|
|
223
|
-
}
|
|
224
|
-
if (pollResult.payload.status === "processing")
|
|
225
|
-
continue;
|
|
226
|
-
if (pollResult.payload.status !== "ready") {
|
|
227
|
-
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
|
|
228
|
-
phase,
|
|
229
|
-
});
|
|
269
|
+
const createTask = async () => {
|
|
270
|
+
const task = challenge.kind === "aws_waf"
|
|
271
|
+
? {
|
|
272
|
+
type: proxy ? "AmazonTask" : "AmazonTaskProxyless",
|
|
273
|
+
websiteURL: challenge.pageUrl,
|
|
274
|
+
websiteKey: challenge.siteKey,
|
|
275
|
+
captchaScript: challenge.captchaScript,
|
|
276
|
+
context: challenge.context,
|
|
277
|
+
iv: challenge.iv,
|
|
278
|
+
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
279
|
+
...(proxy ?? {}),
|
|
280
|
+
}
|
|
281
|
+
: {
|
|
282
|
+
type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
|
|
283
|
+
websiteURL: challenge.pageUrl,
|
|
284
|
+
websiteKey: challenge.siteKey,
|
|
285
|
+
isInvisible: false,
|
|
286
|
+
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
287
|
+
...(proxy ?? {}),
|
|
288
|
+
};
|
|
289
|
+
const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), { clientKey: apiKey, task }, solveController.signal, phase, [apiKey]);
|
|
290
|
+
const taskId = taskIdFrom(createResult.payload);
|
|
291
|
+
if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
|
|
292
|
+
throw unavailableForPayload(createResult.payload, phase);
|
|
230
293
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
294
|
+
return taskId;
|
|
295
|
+
};
|
|
296
|
+
const taskId = traceRecorder
|
|
297
|
+
? await traceRecorder.runSpan("resolver.vendor.create_task", createTask, {
|
|
298
|
+
attributes: {
|
|
299
|
+
vendor: TWOCAPTCHA_VENDOR_ID,
|
|
300
|
+
challenge_kind: challenge.kind,
|
|
301
|
+
},
|
|
302
|
+
onError: (error) => spanErrorAttributes(error, "create_task"),
|
|
303
|
+
})
|
|
304
|
+
: await createTask();
|
|
305
|
+
phase = "poll_result";
|
|
306
|
+
const pollResult = async () => {
|
|
307
|
+
while (true) {
|
|
308
|
+
callerSignal.throwIfAborted();
|
|
309
|
+
const remainingMs = timeoutMs - (now() - startedAt);
|
|
310
|
+
if (remainingMs <= 0)
|
|
311
|
+
throw new TwoCaptchaSolveTimeoutError();
|
|
312
|
+
await delay(Math.min(pollIntervalMs, remainingMs), solveController.signal);
|
|
313
|
+
callerSignal.throwIfAborted();
|
|
314
|
+
if (now() - startedAt >= timeoutMs)
|
|
315
|
+
throw new TwoCaptchaSolveTimeoutError();
|
|
316
|
+
const pollResult = await postJson(fetchImpl, endpoint(baseUrl, "getTaskResult"), { clientKey: apiKey, taskId }, solveController.signal, phase, [apiKey]);
|
|
317
|
+
if (!pollResult.ok || pollResult.payload.errorId !== 0) {
|
|
318
|
+
throw unavailableForPayload(pollResult.payload, phase);
|
|
319
|
+
}
|
|
320
|
+
if (pollResult.payload.status === "processing")
|
|
321
|
+
continue;
|
|
322
|
+
if (pollResult.payload.status !== "ready") {
|
|
323
|
+
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", { phase });
|
|
324
|
+
}
|
|
325
|
+
const token = tokenFrom(pollResult.payload, challenge);
|
|
326
|
+
if (!token?.trim()) {
|
|
327
|
+
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", { phase });
|
|
328
|
+
}
|
|
329
|
+
return { form: "token", token };
|
|
236
330
|
}
|
|
237
|
-
|
|
238
|
-
|
|
331
|
+
};
|
|
332
|
+
return traceRecorder
|
|
333
|
+
? await traceRecorder.runSpan("resolver.vendor.poll_result", pollResult, {
|
|
334
|
+
attributes: {
|
|
335
|
+
vendor: TWOCAPTCHA_VENDOR_ID,
|
|
336
|
+
challenge_kind: challenge.kind,
|
|
337
|
+
},
|
|
338
|
+
onError: (error) => spanErrorAttributes(error, "poll_result"),
|
|
339
|
+
})
|
|
340
|
+
: await pollResult();
|
|
239
341
|
}
|
|
240
342
|
catch (error) {
|
|
241
343
|
if (callerSignal.aborted)
|
|
@@ -247,10 +349,12 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
|
247
349
|
phase,
|
|
248
350
|
});
|
|
249
351
|
}
|
|
250
|
-
if (error instanceof ResolverVendorUnavailableError
|
|
352
|
+
if (error instanceof ResolverVendorUnavailableError ||
|
|
353
|
+
error instanceof ResolverChallengeVerdictError) {
|
|
251
354
|
throw error;
|
|
355
|
+
}
|
|
252
356
|
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
|
|
253
|
-
|
|
357
|
+
...safeCauseOptions(error, [apiKey]),
|
|
254
358
|
phase,
|
|
255
359
|
});
|
|
256
360
|
}
|
|
@@ -55,8 +55,8 @@ export interface ResolverVendorAdapter {
|
|
|
55
55
|
getIssuingIdentity?(solution: ChallengeSolution, requestedIdentity: ResolverIdentity | undefined, challenge: ProviderChallenge): ResolverIssuingIdentity | undefined;
|
|
56
56
|
solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder, transport?: ResolverVendorTransport): Promise<ChallengeSolution>;
|
|
57
57
|
}
|
|
58
|
-
export type ResolverVendorUnavailableReason = "missing_credentials" | "missing_transport" | "allocation_exhausted" | "transport_failure" | "timeout" | "not_implemented";
|
|
59
|
-
export type ResolverChallengeVerdictReason = "human_puzzle";
|
|
58
|
+
export type ResolverVendorUnavailableReason = "missing_credentials" | "missing_proxy_identity" | "missing_client_profile" | "missing_transport" | "allocation_exhausted" | "transport_failure" | "timeout" | "not_implemented";
|
|
59
|
+
export type ResolverChallengeVerdictReason = "human_puzzle" | "solve_failed";
|
|
60
60
|
type ResolverErrorOptions = {
|
|
61
61
|
/** Raw cause; adapters must not place bodies, cookies, headers, credentials, or proxy URLs here. */
|
|
62
62
|
readonly cause?: unknown;
|
|
@@ -56,7 +56,9 @@ export class ResolverChallengeVerdictError extends Error {
|
|
|
56
56
|
vendor;
|
|
57
57
|
reason;
|
|
58
58
|
constructor(vendor, reason, options = {}) {
|
|
59
|
-
super(
|
|
59
|
+
super(reason === "solve_failed"
|
|
60
|
+
? `Resolver vendor ${vendor} attempted the challenge but did not solve it`
|
|
61
|
+
: `Resolver vendor ${vendor} returned a challenge verdict: ${reason}`);
|
|
60
62
|
this.vendor = vendor;
|
|
61
63
|
this.reason = reason;
|
|
62
64
|
this.name = "ResolverChallengeVerdictError";
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type ProxyResolutionOptions } from "../config/loader.js";
|
|
2
|
+
import type { ChallengeSolution, ProviderCache, ProviderChallenge, ProviderChallengeKind, ProviderProxyMode, ProviderResolverConfig, ProviderResolverVendor, ResolverContext } from "../types.js";
|
|
2
3
|
import { type ResolverIdentity, type ResolverVendorAdapter, type ResolverVendorTransport } from "./resolver-vendors/types.js";
|
|
3
4
|
import type { TraceRecorder } from "./trace.js";
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export declare const APIFUSE__RESOLVER__CAPMONSTER__API_KEY = "APIFUSE__RESOLVER__CAPMONSTER__API_KEY";
|
|
7
|
-
export declare const APIFUSE__RESOLVER__TIMEOUT_MS = "APIFUSE__RESOLVER__TIMEOUT_MS";
|
|
8
|
-
export declare const APIFUSE__CDP_POOL__URL = "APIFUSE__CDP_POOL__URL";
|
|
9
|
-
export declare const DEFAULT_RESOLVER_TIMEOUT_MS = 180000;
|
|
5
|
+
export { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
|
|
6
|
+
export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, } from "./resolver-config.js";
|
|
10
7
|
type EnvLike = Record<string, string | undefined>;
|
|
11
8
|
type ResolverChainClient = ResolverContext & {
|
|
12
9
|
solve(challenge: ProviderChallenge, signal?: AbortSignal, traceRecorder?: TraceRecorder): Promise<ChallengeSolution>;
|
|
@@ -14,6 +11,14 @@ type ResolverChainClient = ResolverContext & {
|
|
|
14
11
|
export interface ResolverRuntimeOptions {
|
|
15
12
|
readonly allowedHosts?: readonly string[];
|
|
16
13
|
readonly cache?: ProviderCache;
|
|
14
|
+
/** Inputs for SDK-owned lazy proxy resolution. The SDK never accepts a caller-built identity. */
|
|
15
|
+
readonly proxyIntent?: {
|
|
16
|
+
readonly mode: ProviderProxyMode;
|
|
17
|
+
readonly upstream: NonNullable<ProxyResolutionOptions["upstream"]>;
|
|
18
|
+
readonly affinityKey?: ProxyResolutionOptions["affinityKey"];
|
|
19
|
+
readonly telemetry?: ProxyResolutionOptions["telemetry"];
|
|
20
|
+
readonly userAgent?: string;
|
|
21
|
+
};
|
|
17
22
|
/** Server-owned context/proxy scope used only for identity-bound cache entries. */
|
|
18
23
|
readonly identityScope?: string;
|
|
19
24
|
/** SDK-owned transport already bound to the resolved proxy lease and client profile. */
|
|
@@ -25,7 +30,6 @@ export interface ResolverRuntimeOptions {
|
|
|
25
30
|
readonly identityScope?: string;
|
|
26
31
|
}) => ResolverVendorTransport;
|
|
27
32
|
}
|
|
28
|
-
export declare const RESOLVER_INSTRUMENTATION_METADATA: unique symbol;
|
|
29
33
|
export type ResolverInstrumentationMetadata = {
|
|
30
34
|
readonly target: ResolverContext;
|
|
31
35
|
readonly traceRecorder: TraceRecorder;
|
|
@@ -33,6 +37,8 @@ export type ResolverInstrumentationMetadata = {
|
|
|
33
37
|
export type ResolverAdapterFactory = (configuration: string, timeoutMs: number, allowedHosts: readonly string[]) => ResolverVendorAdapter;
|
|
34
38
|
export declare const RESOLVER_ADAPTER_REGISTRY: Partial<Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>>;
|
|
35
39
|
export declare function swapResolverAdapterFactoryForTests(vendor: ProviderResolverVendor, factory: ResolverAdapterFactory | undefined): () => void;
|
|
40
|
+
/** Internal test seam; deliberately not re-exported from the package root. */
|
|
41
|
+
export declare function swapResolverDefaultUserAgentForTests(resolver: (() => string | undefined) | undefined): () => void;
|
|
36
42
|
/** Remove the cached entry for the exact solution object returned by this resolver. */
|
|
37
43
|
export declare function invalidateResolverSolution(resolver: ResolverContext, challenge: ProviderChallenge, solution: ChallengeSolution): Promise<void>;
|
|
38
44
|
export declare function createResolverClient(options: {
|
|
@@ -41,14 +47,13 @@ export declare function createResolverClient(options: {
|
|
|
41
47
|
readonly unavailableReason?: string;
|
|
42
48
|
readonly cache?: ProviderCache;
|
|
43
49
|
readonly identity?: ResolverIdentity;
|
|
50
|
+
readonly proxyIntent?: ResolverRuntimeOptions["proxyIntent"];
|
|
44
51
|
readonly transport?: ResolverVendorTransport;
|
|
45
52
|
readonly createTransport?: ResolverRuntimeOptions["createTransport"];
|
|
46
53
|
readonly clientProfile?: string;
|
|
47
54
|
readonly allowedHosts?: readonly string[];
|
|
48
55
|
}): ResolverChainClient;
|
|
49
|
-
export declare function createUnsupportedResolverClient(reason?: string): ResolverContext;
|
|
50
56
|
export declare function bindResolverSignal(resolver: ResolverContext, defaultSignal: AbortSignal | undefined): ResolverContext;
|
|
51
57
|
export declare function createResolverClientFromEnv(config: ProviderResolverConfig | undefined, env?: EnvLike, options?: ResolverRuntimeOptions): ResolverContext;
|
|
52
58
|
/** Internal test seam; deliberately not re-exported from the package root. */
|
|
53
59
|
export declare function createResolverClientFromEnvForTests(config: ProviderResolverConfig | undefined, env: EnvLike, options: ResolverRuntimeOptions, adapterFactories: Partial<Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>>): ResolverContext;
|
|
54
|
-
export {};
|