@apifuse/provider-sdk 2.2.0-beta.36 → 2.2.0-beta.37
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/AUTHORING.md +22 -8
- package/CHANGELOG.md +4 -0
- package/README.md +20 -8
- package/bin/apifuse-dev.ts +1 -1
- package/bin/apifuse-pack-types.ts +6 -5
- package/bin/apifuse-record.ts +1 -1
- package/bin/apifuse-submit-check.ts +23 -10
- package/dist/cli/templates/provider/index.ts.tpl +6 -3
- package/dist/cli/templates/provider/operations/ping.ts.tpl +2 -1
- package/dist/define.d.ts +39 -21
- package/dist/define.js +28 -9
- package/dist/index.d.ts +2 -2
- package/dist/provider.d.ts +2 -1
- package/dist/runtime/browser.js +19 -11
- package/dist/runtime/resolver-public.d.ts +1 -1
- package/dist/runtime/resolver-public.js +1 -1
- package/dist/runtime/resolver-vendors/browser.js +57 -14
- package/dist/runtime/resolver-vendors/types.d.ts +9 -1
- package/dist/runtime/resolver-vendors/types.js +15 -0
- package/dist/runtime/resolver.d.ts +1 -0
- package/dist/runtime/resolver.js +13 -7
- package/dist/server/serve-implementation.js +25 -0
- package/dist/types.d.ts +25 -17
- package/package.json +1 -1
- package/src/cli/templates/provider/index.ts.tpl +6 -3
- package/src/cli/templates/provider/operations/ping.ts.tpl +2 -1
- package/src/define.ts +135 -51
- package/src/index.ts +4 -2
- package/src/provider.ts +6 -1
- package/src/runtime/browser.ts +34 -11
- package/src/runtime/resolver-public.ts +2 -0
- package/src/runtime/resolver-vendors/browser.ts +69 -11
- package/src/runtime/resolver-vendors/types.ts +21 -0
- package/src/runtime/resolver.ts +17 -5
- package/src/server/serve-implementation.ts +39 -1
- package/src/testing/run.ts +3 -3
- package/src/types.ts +41 -17
package/dist/runtime/browser.js
CHANGED
|
@@ -548,8 +548,8 @@ class PlaywrightBrowserPage {
|
|
|
548
548
|
this.page = page;
|
|
549
549
|
this.proxy = proxy;
|
|
550
550
|
}
|
|
551
|
-
async goto(url) {
|
|
552
|
-
await this.page.goto(url);
|
|
551
|
+
async goto(url, options) {
|
|
552
|
+
await this.page.goto(url, options);
|
|
553
553
|
}
|
|
554
554
|
async evaluate(fn) {
|
|
555
555
|
if (typeof fn === "string") {
|
|
@@ -1101,16 +1101,22 @@ class CdpPoolBrowserPage {
|
|
|
1101
1101
|
get id() {
|
|
1102
1102
|
return this.pageId;
|
|
1103
1103
|
}
|
|
1104
|
-
async goto(url) {
|
|
1104
|
+
async goto(url, options) {
|
|
1105
1105
|
await this.initialize();
|
|
1106
1106
|
const startedAt = Date.now();
|
|
1107
|
-
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1107
|
+
const timeout = options?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
1108
|
+
const waitUntil = options?.waitUntil ?? "load";
|
|
1109
|
+
let expectedEventSeen = false;
|
|
1110
|
+
const eventName = waitUntil === "domcontentloaded" ? "Page.domContentEventFired" : "Page.loadEventFired";
|
|
1111
|
+
const unsubscribe = this.pageClient.on(eventName, () => {
|
|
1112
|
+
expectedEventSeen = true;
|
|
1110
1113
|
});
|
|
1111
1114
|
try {
|
|
1112
|
-
await this.pageClient.send("Page.navigate", { url });
|
|
1113
|
-
|
|
1115
|
+
const navigation = (await this.pageClient.send("Page.navigate", { url }));
|
|
1116
|
+
if (typeof navigation.errorText === "string" && navigation.errorText.length > 0) {
|
|
1117
|
+
throw new Error(`Page.navigate failed: ${navigation.errorText} at ${url}`);
|
|
1118
|
+
}
|
|
1119
|
+
await this.waitForDocumentReady(startedAt + timeout, () => expectedEventSeen, waitUntil);
|
|
1114
1120
|
}
|
|
1115
1121
|
finally {
|
|
1116
1122
|
unsubscribe();
|
|
@@ -1397,11 +1403,13 @@ class CdpPoolBrowserPage {
|
|
|
1397
1403
|
this.frameExecutionContexts.set(frameId, contextId);
|
|
1398
1404
|
return contextId;
|
|
1399
1405
|
}
|
|
1400
|
-
async waitForDocumentReady(deadline,
|
|
1406
|
+
async waitForDocumentReady(deadline, isExpectedEventSeen, waitUntil) {
|
|
1401
1407
|
while (Date.now() < deadline) {
|
|
1402
1408
|
const readyState = await this.evaluate("document.readyState");
|
|
1403
|
-
|
|
1404
|
-
|
|
1409
|
+
const documentReady = readyState === "complete" ||
|
|
1410
|
+
(waitUntil === "domcontentloaded" && readyState === "interactive");
|
|
1411
|
+
if (documentReady) {
|
|
1412
|
+
if (isExpectedEventSeen() || readyState === "complete") {
|
|
1405
1413
|
return;
|
|
1406
1414
|
}
|
|
1407
1415
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, bindResolverSignal, createResolverClient, createResolverClientFromEnv, createUnsupportedResolverClient, DEFAULT_RESOLVER_TIMEOUT_MS, invalidateResolverSolution, RESOLVER_ADAPTER_REGISTRY, RESOLVER_INSTRUMENTATION_METADATA, type ResolverAdapterFactory, type ResolverInstrumentationMetadata, type ResolverRuntimeOptions, } from "./resolver.js";
|
|
1
|
+
export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, bindResolverSignal, createResolverClient, createResolverClientFromEnv, createUnsupportedResolverClient, DEFAULT_RESOLVER_VENDOR_PREFERENCE, DEFAULT_RESOLVER_TIMEOUT_MS, invalidateResolverSolution, RESOLVER_ADAPTER_REGISTRY, RESOLVER_INSTRUMENTATION_METADATA, resolveProviderResolverVendors, type ResolverAdapterFactory, type ResolverInstrumentationMetadata, type ResolverRuntimeOptions, } from "./resolver.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, bindResolverSignal, createResolverClient, createResolverClientFromEnv, createUnsupportedResolverClient, DEFAULT_RESOLVER_TIMEOUT_MS, invalidateResolverSolution, RESOLVER_ADAPTER_REGISTRY, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver.js";
|
|
1
|
+
export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, bindResolverSignal, createResolverClient, createResolverClientFromEnv, createUnsupportedResolverClient, DEFAULT_RESOLVER_VENDOR_PREFERENCE, DEFAULT_RESOLVER_TIMEOUT_MS, invalidateResolverSolution, RESOLVER_ADAPTER_REGISTRY, RESOLVER_INSTRUMENTATION_METADATA, resolveProviderResolverVendors, } from "./resolver.js";
|
|
@@ -5,6 +5,7 @@ import { assertResolverHostAllowed, normalizedResolverHostname } from "./hosts.j
|
|
|
5
5
|
import { ResolverVendorUnavailableError, } from "./types.js";
|
|
6
6
|
const BROWSER_VENDOR_ID = "browser";
|
|
7
7
|
const DEFAULT_COOKIE_POLL_INTERVAL_MS = 100;
|
|
8
|
+
const NAVIGATION_BLOCKED_ERROR_TEXT = "net::ERR_BLOCKED_BY_CLIENT";
|
|
8
9
|
const AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX = ".awswaf.com";
|
|
9
10
|
const RESOLVER_DOCUMENT_CONTENT_SECURITY_POLICY = "connect-src http: https:; worker-src 'none'";
|
|
10
11
|
const SUCCESS_COOKIE_NAMES = {
|
|
@@ -25,11 +26,32 @@ export function swapBrowserResolverClientFactoryForTests(factory) {
|
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
28
|
class BrowserSolveTimeoutError extends Error {
|
|
28
|
-
constructor() {
|
|
29
|
-
super(
|
|
29
|
+
constructor(blockedRequests) {
|
|
30
|
+
super(`Browser resolver solve budget elapsed${formatBlockedRequests(blockedRequests)}`);
|
|
30
31
|
this.name = "BrowserSolveTimeoutError";
|
|
31
32
|
}
|
|
32
33
|
}
|
|
34
|
+
class BrowserNavigationBlockedError extends Error {
|
|
35
|
+
navigationUrl;
|
|
36
|
+
blockedUrls;
|
|
37
|
+
code = "RESOLVER_BROWSER_NAVIGATION_BLOCKED";
|
|
38
|
+
constructor(navigationUrl, blockedUrls, options) {
|
|
39
|
+
super(`Browser resolver navigation was blocked for ${navigationUrl}${formatBlockedRequests(blockedUrls)}`, options);
|
|
40
|
+
this.navigationUrl = navigationUrl;
|
|
41
|
+
this.blockedUrls = blockedUrls;
|
|
42
|
+
this.name = "BrowserNavigationBlockedError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function formatBlockedRequests(blockedRequests) {
|
|
46
|
+
if (blockedRequests.length === 0)
|
|
47
|
+
return "";
|
|
48
|
+
const displayed = blockedRequests.slice(0, 5);
|
|
49
|
+
const remainder = blockedRequests.length - displayed.length;
|
|
50
|
+
return `; blocked ${blockedRequests.length} requests: [${displayed.join(", ")}]${remainder > 0 ? ` (+${remainder} more)` : ""}`;
|
|
51
|
+
}
|
|
52
|
+
function isNavigationBlockedError(error) {
|
|
53
|
+
return error instanceof Error && error.message.includes(NAVIGATION_BLOCKED_ERROR_TEXT);
|
|
54
|
+
}
|
|
33
55
|
class BrowserCleanupTimeoutError extends Error {
|
|
34
56
|
constructor(timeoutMs) {
|
|
35
57
|
super(`Browser resolver cleanup exceeded ${timeoutMs}ms`);
|
|
@@ -141,23 +163,35 @@ function selectSuccessCookie(cookies, successCookieName, pageUrl) {
|
|
|
141
163
|
cookieDomainSpecificity(right) - cookieDomainSpecificity(left) ||
|
|
142
164
|
right.path.length - left.path.length)[0];
|
|
143
165
|
}
|
|
144
|
-
async function solveInPage(page, challengeKind, pageUrl, allowedHosts, successCookieName, pollIntervalMs, signal) {
|
|
166
|
+
async function solveInPage(page, challengeKind, pageUrl, allowedHosts, successCookieName, pollIntervalMs, gotoTimeoutMs, blockedRequests, signal) {
|
|
145
167
|
return await page.withResourcePolicy({
|
|
146
168
|
allowedMethods: ["GET", "HEAD", "POST"],
|
|
147
169
|
documentContentSecurityPolicy: RESOLVER_DOCUMENT_CONTENT_SECURITY_POLICY,
|
|
148
170
|
routes: [
|
|
149
171
|
{
|
|
150
172
|
match: () => true,
|
|
151
|
-
handle: (request) =>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
173
|
+
handle: (request) => {
|
|
174
|
+
if (isResolverBrowserRequestAllowed(request.url, challengeKind, allowedHosts)) {
|
|
175
|
+
return { action: "continue" };
|
|
176
|
+
}
|
|
177
|
+
blockedRequests.add(request.url);
|
|
178
|
+
return { action: "block" };
|
|
179
|
+
},
|
|
156
180
|
},
|
|
157
181
|
],
|
|
158
182
|
}, async () => {
|
|
159
183
|
const userAgent = await raceWithAbort(() => page.userAgent(), signal);
|
|
160
|
-
|
|
184
|
+
try {
|
|
185
|
+
await raceWithAbort(() => page.goto(pageUrl, {
|
|
186
|
+
timeout: gotoTimeoutMs,
|
|
187
|
+
waitUntil: "domcontentloaded",
|
|
188
|
+
}), signal);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (!isNavigationBlockedError(error))
|
|
192
|
+
throw error;
|
|
193
|
+
throw new BrowserNavigationBlockedError(pageUrl, [...blockedRequests], { cause: error });
|
|
194
|
+
}
|
|
161
195
|
while (true) {
|
|
162
196
|
const cookies = await raceWithAbort(() => page.cookies(), signal);
|
|
163
197
|
const successCookie = selectSuccessCookie(cookies, successCookieName, pageUrl);
|
|
@@ -203,7 +237,7 @@ function poolErrorCode(error) {
|
|
|
203
237
|
const code = error.code;
|
|
204
238
|
return typeof code === "number" ? code : undefined;
|
|
205
239
|
}
|
|
206
|
-
function knownUnavailableReason(error) {
|
|
240
|
+
function knownUnavailableReason(error, beforePageSolve = false) {
|
|
207
241
|
// Source-grounded mappings:
|
|
208
242
|
// - apps/cdp-pool/src/index.ts: the JSON-RPC codes and messages below.
|
|
209
243
|
// - src/runtime/browser.ts: BROWSER_CDP_POOL_REQUIRED and the two WebSocket messages.
|
|
@@ -230,7 +264,10 @@ function knownUnavailableReason(error) {
|
|
|
230
264
|
error.message.includes("WebSocket closed")) {
|
|
231
265
|
return "transport_failure";
|
|
232
266
|
}
|
|
233
|
-
|
|
267
|
+
// Browser creation, Playwright launch, and CDP connection all happen before the
|
|
268
|
+
// isolated-page handler is entered. Errors after that boundary belong to the
|
|
269
|
+
// challenge solve and must retain their existing classification.
|
|
270
|
+
return beforePageSolve ? "transport_failure" : undefined;
|
|
234
271
|
}
|
|
235
272
|
async function closeBrowserClient(client, timeoutMs, challengeKind, traceRecorder) {
|
|
236
273
|
const close = client?.close;
|
|
@@ -274,10 +311,11 @@ export function createBrowserResolverVendorAdapter(options) {
|
|
|
274
311
|
// Refuse so the chain can choose a vendor that can honor the resolved identity.
|
|
275
312
|
throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "not_implemented");
|
|
276
313
|
}
|
|
314
|
+
const blockedRequests = new Set();
|
|
277
315
|
const solveController = new AbortController();
|
|
278
316
|
const onCallerAbort = () => solveController.abort(abortReason(callerSignal));
|
|
279
317
|
callerSignal.addEventListener("abort", onCallerAbort, { once: true });
|
|
280
|
-
const timeout = setTimeout(() => solveController.abort(new BrowserSolveTimeoutError()), options.timeoutMs);
|
|
318
|
+
const timeout = setTimeout(() => solveController.abort(new BrowserSolveTimeoutError([...blockedRequests])), options.timeoutMs);
|
|
281
319
|
let client;
|
|
282
320
|
let handlerEntered = false;
|
|
283
321
|
try {
|
|
@@ -290,7 +328,7 @@ export function createBrowserResolverVendorAdapter(options) {
|
|
|
290
328
|
});
|
|
291
329
|
const contextOperation = client.withIsolatedContext(async (page) => {
|
|
292
330
|
handlerEntered = true;
|
|
293
|
-
return await solveInPage(page, challengeKind, challenge.pageUrl, options.allowedHosts, SUCCESS_COOKIE_NAMES[challengeKind], pollIntervalMs, solveController.signal);
|
|
331
|
+
return await solveInPage(page, challengeKind, challenge.pageUrl, options.allowedHosts, SUCCESS_COOKIE_NAMES[challengeKind], pollIntervalMs, options.timeoutMs, blockedRequests, solveController.signal);
|
|
294
332
|
});
|
|
295
333
|
try {
|
|
296
334
|
return await raceWithAbort(() => contextOperation, solveController.signal);
|
|
@@ -315,10 +353,15 @@ export function createBrowserResolverVendorAdapter(options) {
|
|
|
315
353
|
cause: error,
|
|
316
354
|
});
|
|
317
355
|
}
|
|
356
|
+
if (error instanceof BrowserNavigationBlockedError) {
|
|
357
|
+
throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "transport_failure", {
|
|
358
|
+
cause: error,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
318
361
|
if (error instanceof ResolverVendorUnavailableError) {
|
|
319
362
|
throw error;
|
|
320
363
|
}
|
|
321
|
-
const reason = knownUnavailableReason(error);
|
|
364
|
+
const reason = knownUnavailableReason(error, !handlerEntered);
|
|
322
365
|
if (reason) {
|
|
323
366
|
throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, reason, { cause: error });
|
|
324
367
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChallengeSolution, ProviderChallenge, ProviderChallengeKind, ProviderResolverVendor } from "../../types.js";
|
|
1
|
+
import type { ChallengeSolution, ProviderChallenge, ProviderChallengeKind, ProviderResolverConfig, ProviderResolverVendor } from "../../types.js";
|
|
2
2
|
import type { TraceRecorder } from "../trace.js";
|
|
3
3
|
export declare const RESOLVER_VENDOR_CAPABILITIES: {
|
|
4
4
|
readonly browser: readonly ["aws_waf", "cloudflare_interstitial"];
|
|
@@ -7,7 +7,15 @@ export declare const RESOLVER_VENDOR_CAPABILITIES: {
|
|
|
7
7
|
readonly capmonster: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha"];
|
|
8
8
|
readonly custom: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf", "akamai_sec_cpt", "akamai_sensor"];
|
|
9
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* SDK-owned fallback policy for hosted resolver vendors. Capability support is
|
|
12
|
+
* applied separately, so each provider receives only vendors that support one
|
|
13
|
+
* or more of its declared challenge kinds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DEFAULT_RESOLVER_VENDOR_PREFERENCE: readonly ["capsolver", "2captcha"];
|
|
10
16
|
export declare function resolverVendorSupports(vendor: ProviderResolverVendor, kind: ProviderChallengeKind): boolean;
|
|
17
|
+
/** Resolves an explicit provider override or the SDK-owned default vendor chain. */
|
|
18
|
+
export declare function resolveProviderResolverVendors(config: ProviderResolverConfig): readonly ProviderResolverVendor[];
|
|
11
19
|
export interface ResolverIdentity {
|
|
12
20
|
readonly proxyUrl: string;
|
|
13
21
|
readonly userAgent: string;
|
|
@@ -33,9 +33,24 @@ export const RESOLVER_VENDOR_CAPABILITIES = {
|
|
|
33
33
|
"akamai_sensor",
|
|
34
34
|
],
|
|
35
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* SDK-owned fallback policy for hosted resolver vendors. Capability support is
|
|
38
|
+
* applied separately, so each provider receives only vendors that support one
|
|
39
|
+
* or more of its declared challenge kinds.
|
|
40
|
+
*/
|
|
41
|
+
export const DEFAULT_RESOLVER_VENDOR_PREFERENCE = [
|
|
42
|
+
"capsolver",
|
|
43
|
+
"2captcha",
|
|
44
|
+
];
|
|
36
45
|
export function resolverVendorSupports(vendor, kind) {
|
|
37
46
|
return RESOLVER_VENDOR_CAPABILITIES[vendor].includes(kind);
|
|
38
47
|
}
|
|
48
|
+
/** Resolves an explicit provider override or the SDK-owned default vendor chain. */
|
|
49
|
+
export function resolveProviderResolverVendors(config) {
|
|
50
|
+
if (config.vendors !== undefined)
|
|
51
|
+
return config.vendors;
|
|
52
|
+
return DEFAULT_RESOLVER_VENDOR_PREFERENCE.filter((vendor) => config.kinds.some((kind) => resolverVendorSupports(vendor, kind)));
|
|
53
|
+
}
|
|
39
54
|
export class ResolverVendorUnavailableError extends Error {
|
|
40
55
|
vendor;
|
|
41
56
|
reason;
|
|
@@ -4,6 +4,7 @@ import { type ResolverIdentity, type ResolverVendorAdapter, type ResolverVendorT
|
|
|
4
4
|
import type { TraceRecorder } from "./trace.js";
|
|
5
5
|
export { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
|
|
6
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";
|
|
7
|
+
export { DEFAULT_RESOLVER_VENDOR_PREFERENCE, resolveProviderResolverVendors, } from "./resolver-vendors/types.js";
|
|
7
8
|
type EnvLike = Record<string, string | undefined>;
|
|
8
9
|
type ResolverChainClient = ResolverContext & {
|
|
9
10
|
solve(challenge: ProviderChallenge, signal?: AbortSignal, traceRecorder?: TraceRecorder): Promise<ChallengeSolution>;
|
package/dist/runtime/resolver.js
CHANGED
|
@@ -7,12 +7,13 @@ import { createBrowserResolverVendorAdapter } from "./resolver-vendors/browser.j
|
|
|
7
7
|
import { createCapsolverResolverVendorAdapter } from "./resolver-vendors/capsolver.js";
|
|
8
8
|
import { assertResolverHostAllowed } from "./resolver-vendors/hosts.js";
|
|
9
9
|
import { createTwoCaptchaResolverVendorAdapter } from "./resolver-vendors/twocaptcha.js";
|
|
10
|
-
import { RESOLVER_VENDOR_CAPABILITIES, ResolverVendorUnavailableError, resolverVendorSupports, } from "./resolver-vendors/types.js";
|
|
10
|
+
import { RESOLVER_VENDOR_CAPABILITIES, ResolverVendorUnavailableError, resolveProviderResolverVendors, resolverVendorSupports, } from "./resolver-vendors/types.js";
|
|
11
11
|
import { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
|
|
12
12
|
import { 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";
|
|
13
13
|
import { DEFAULT_PROFILE } from "./stealth.js";
|
|
14
14
|
export { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
|
|
15
15
|
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";
|
|
16
|
+
export { DEFAULT_RESOLVER_VENDOR_PREFERENCE, resolveProviderResolverVendors, } from "./resolver-vendors/types.js";
|
|
16
17
|
const RESOLVER_SOLUTION_CACHE_NAMESPACE = "resolver-solution";
|
|
17
18
|
const RESOLVER_SOLUTION_INDEX_CACHE_NAMESPACE = "resolver-solution-index";
|
|
18
19
|
const MIN_RESOLVER_CACHE_TTL_MS = 1_000;
|
|
@@ -169,10 +170,12 @@ function assertKnownResolverVendor(vendor) {
|
|
|
169
170
|
return;
|
|
170
171
|
throw new Error(`Unknown resolver vendor "${vendor}" in resolver configuration`);
|
|
171
172
|
}
|
|
172
|
-
function throwUnsupportedKind(kind) {
|
|
173
|
+
function throwUnsupportedKind(kind, usingDefaultVendors) {
|
|
173
174
|
throw new ProviderError(`Resolver vendor chain does not support kind "${kind}"`, {
|
|
174
175
|
code: "RESOLVER_KIND_UNSUPPORTED_BY_CHAIN",
|
|
175
|
-
fix:
|
|
176
|
+
fix: usingDefaultVendors
|
|
177
|
+
? `No SDK default resolver vendor supports "${kind}". Declare an explicit resolver.vendors override with a supporting vendor.`
|
|
178
|
+
: `Add a resolver vendor that supports "${kind}" to the provider's resolver.vendors declaration.`,
|
|
176
179
|
});
|
|
177
180
|
}
|
|
178
181
|
function throwExhausted(attempts) {
|
|
@@ -542,8 +545,9 @@ function createResolverChainClient(options) {
|
|
|
542
545
|
});
|
|
543
546
|
}
|
|
544
547
|
const supportingEntries = options.entries.filter((entry) => entry.supports(challenge.kind));
|
|
545
|
-
if (supportingEntries.length === 0)
|
|
546
|
-
throwUnsupportedKind(challenge.kind);
|
|
548
|
+
if (supportingEntries.length === 0) {
|
|
549
|
+
throwUnsupportedKind(challenge.kind, options.usingDefaultVendors ?? false);
|
|
550
|
+
}
|
|
547
551
|
signal.throwIfAborted();
|
|
548
552
|
const identityResolution = options.proxyIntent
|
|
549
553
|
? await resolveResolverIdentity(options.proxyIntent)
|
|
@@ -692,7 +696,8 @@ function createResolverClientFromEnvInternal(config, env, options, adapterFactor
|
|
|
692
696
|
return createUnsupportedResolverClient("Provider does not declare resolver capability");
|
|
693
697
|
}
|
|
694
698
|
assertClientProfileTransportContract(config.clientProfile, options.transport);
|
|
695
|
-
|
|
699
|
+
const vendors = resolveProviderResolverVendors(config);
|
|
700
|
+
if (config.vendors !== undefined && config.vendors.length === 0) {
|
|
696
701
|
return createResolverChainClient({
|
|
697
702
|
kinds: config.kinds,
|
|
698
703
|
entries: [],
|
|
@@ -704,7 +709,7 @@ function createResolverClientFromEnvInternal(config, env, options, adapterFactor
|
|
|
704
709
|
const allowedHosts = [...(options.allowedHosts ?? [])];
|
|
705
710
|
return createResolverChainClient({
|
|
706
711
|
kinds: config.kinds,
|
|
707
|
-
entries:
|
|
712
|
+
entries: vendors.map((configuredVendor) => {
|
|
708
713
|
assertKnownResolverVendor(configuredVendor);
|
|
709
714
|
const vendor = configuredVendor;
|
|
710
715
|
return {
|
|
@@ -713,6 +718,7 @@ function createResolverClientFromEnvInternal(config, env, options, adapterFactor
|
|
|
713
718
|
createAdapter: () => createAdapter(resolveVendorAvailability(vendor, env), timeoutMs, allowedHosts, adapterFactories),
|
|
714
719
|
};
|
|
715
720
|
}),
|
|
721
|
+
usingDefaultVendors: config.vendors === undefined,
|
|
716
722
|
cache: options.cache,
|
|
717
723
|
proxyIntent: options.proxyIntent,
|
|
718
724
|
identityScope: options.identityScope,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import { join } from "node:path";
|
|
@@ -924,6 +925,28 @@ function extractRequestId(raw) {
|
|
|
924
925
|
const value = Object.getOwnPropertyDescriptor(raw, "requestId")?.value;
|
|
925
926
|
return typeof value === "string" ? value : undefined;
|
|
926
927
|
}
|
|
928
|
+
const MAX_PROVIDER_ERROR_CAUSE_FRAMES = 5;
|
|
929
|
+
function providerErrorCauseChain(error) {
|
|
930
|
+
if (!(error instanceof Error) && !isProviderError(error))
|
|
931
|
+
return undefined;
|
|
932
|
+
const seen = new Set([error]);
|
|
933
|
+
const frames = [];
|
|
934
|
+
let cause = error.cause;
|
|
935
|
+
while (frames.length < MAX_PROVIDER_ERROR_CAUSE_FRAMES &&
|
|
936
|
+
(cause instanceof Error || isProviderError(cause)) &&
|
|
937
|
+
!seen.has(cause)) {
|
|
938
|
+
seen.add(cause);
|
|
939
|
+
const message = cause.message;
|
|
940
|
+
frames.push({
|
|
941
|
+
errorClass: cause.name,
|
|
942
|
+
...(isProviderError(cause) && typeof cause.code === "string" ? { code: cause.code } : {}),
|
|
943
|
+
messageLength: message.length,
|
|
944
|
+
messageFingerprint: createHash("sha256").update(message).digest("hex").slice(0, 12),
|
|
945
|
+
});
|
|
946
|
+
cause = cause.cause;
|
|
947
|
+
}
|
|
948
|
+
return frames.length > 0 ? frames : undefined;
|
|
949
|
+
}
|
|
927
950
|
function logProviderError(logger, provider, kind, route, requestId, error, status, cost, declaredErrorCode) {
|
|
928
951
|
const code = isProviderError(error)
|
|
929
952
|
? (error.code ?? "provider_error")
|
|
@@ -934,6 +957,7 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
|
|
|
934
957
|
: "internal_error";
|
|
935
958
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
936
959
|
const message = error instanceof Error ? error.message : String(error);
|
|
960
|
+
const causeChain = providerErrorCauseChain(error);
|
|
937
961
|
const details = errorObservabilityDetails(error, declaredErrorCode);
|
|
938
962
|
const isUnregisteredProviderErrorCode = status === 500 &&
|
|
939
963
|
isProviderError(error) &&
|
|
@@ -954,6 +978,7 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
|
|
|
954
978
|
code,
|
|
955
979
|
errorClass,
|
|
956
980
|
message,
|
|
981
|
+
...(causeChain ? { causeChain } : {}),
|
|
957
982
|
...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
|
|
958
983
|
errorCategory: details.category,
|
|
959
984
|
taxonomyVersion: details.taxonomyVersion,
|
package/dist/types.d.ts
CHANGED
|
@@ -252,16 +252,16 @@ export interface ProviderSttConfig {
|
|
|
252
252
|
mode: ProviderSttMode;
|
|
253
253
|
}
|
|
254
254
|
/**
|
|
255
|
-
*
|
|
256
|
-
* `createBrowserClient`) and is a first-class vendor rather than an escape hatch:
|
|
257
|
-
* for fingerprint-family kinds, it was measured faster than a paid vendor
|
|
258
|
-
* (4.5 s vs 17.5 s) at zero marginal cost.
|
|
255
|
+
* Union order is documentation only.
|
|
259
256
|
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
257
|
+
* The SDK owns the default hosted-vendor fallback policy and derives the chain
|
|
258
|
+
* from each provider's declared challenge kinds. Hosted solvers are preferred
|
|
259
|
+
* with `capsolver` ahead of `2captcha` in that policy.
|
|
262
260
|
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
261
|
+
* `browser` is the in-house CDP pool and remains opt-in; it is excluded from the
|
|
262
|
+
* default chain. `custom` is likewise reserved for provider-specific configuration.
|
|
263
|
+
*
|
|
264
|
+
* `ProviderResolverConfig.vendors` overrides the SDK policy when declared.
|
|
265
265
|
*/
|
|
266
266
|
export type ProviderResolverVendor = "browser" | "capsolver" | "capmonster" | "2captcha" | "custom";
|
|
267
267
|
/**
|
|
@@ -339,8 +339,8 @@ export type ChallengeSolution = {
|
|
|
339
339
|
readonly expires?: number;
|
|
340
340
|
};
|
|
341
341
|
export interface ProviderResolverConfig {
|
|
342
|
-
/**
|
|
343
|
-
readonly vendors
|
|
342
|
+
/** Optional ordered override for the SDK-owned vendor fallback chain. */
|
|
343
|
+
readonly vendors?: readonly ProviderResolverVendor[];
|
|
344
344
|
/** Challenge kinds this provider is permitted to request. */
|
|
345
345
|
readonly kinds: readonly ProviderChallengeKind[];
|
|
346
346
|
/**
|
|
@@ -1355,8 +1355,6 @@ export interface NativeNetworkClient {
|
|
|
1355
1355
|
export interface NativeContext {
|
|
1356
1356
|
readonly network: NativeNetworkClient;
|
|
1357
1357
|
}
|
|
1358
|
-
/** Consumer-facing alias for the native capability on provider contexts. */
|
|
1359
|
-
export type NativeProviderContext = NativeContext;
|
|
1360
1358
|
export interface NativeProviderConfig {
|
|
1361
1359
|
readonly network?: {
|
|
1362
1360
|
readonly tcp?: readonly NativeTcpEgressRule[];
|
|
@@ -1496,6 +1494,10 @@ export interface BrowserPage extends BrowserFrame {
|
|
|
1496
1494
|
cookies(): Promise<readonly BrowserCookie[]>;
|
|
1497
1495
|
fill(selector: string, text: string): Promise<void>;
|
|
1498
1496
|
goto(url: string): Promise<void>;
|
|
1497
|
+
goto(url: string, options?: {
|
|
1498
|
+
readonly timeout?: number;
|
|
1499
|
+
readonly waitUntil?: "load" | "domcontentloaded";
|
|
1500
|
+
}): Promise<void>;
|
|
1499
1501
|
pageId?: string;
|
|
1500
1502
|
screenshot(options?: {
|
|
1501
1503
|
fullPage?: boolean;
|
|
@@ -1762,7 +1764,7 @@ export interface FlowContext {
|
|
|
1762
1764
|
* un-keyed entries would be shared across all connectionless ceremonies. */
|
|
1763
1765
|
readonly state?: ProviderRuntimeState;
|
|
1764
1766
|
/** Present when the selected runtime supplies native network capabilities. */
|
|
1765
|
-
readonly native?:
|
|
1767
|
+
readonly native?: NativeContext;
|
|
1766
1768
|
stealth: StealthClient;
|
|
1767
1769
|
env: EnvContext;
|
|
1768
1770
|
credential?: CredentialContext;
|
|
@@ -1863,8 +1865,8 @@ export interface ProviderContext {
|
|
|
1863
1865
|
http: HttpClient;
|
|
1864
1866
|
/** Present for requests carrying runtime-resolvable file references. */
|
|
1865
1867
|
readonly files?: ProviderFilesContext;
|
|
1866
|
-
/**
|
|
1867
|
-
readonly native
|
|
1868
|
+
/** Native network capability selected by declaration-derived contexts. */
|
|
1869
|
+
readonly native: NativeContext;
|
|
1868
1870
|
cache: ProviderCache;
|
|
1869
1871
|
state: ProviderRuntimeState;
|
|
1870
1872
|
stealth: StealthClient;
|
|
@@ -1876,6 +1878,12 @@ export interface ProviderContext {
|
|
|
1876
1878
|
resolver: ResolverContext;
|
|
1877
1879
|
choice: ProviderChoiceContext;
|
|
1878
1880
|
}
|
|
1881
|
+
/**
|
|
1882
|
+
* The operation context exposed for one provider declaration. Capability
|
|
1883
|
+
* bindings are present only when their corresponding declaration is present;
|
|
1884
|
+
* trace and request remain ambient runtime bindings.
|
|
1885
|
+
*/
|
|
1886
|
+
export type ProviderContextFor<TConfig> = Pick<ProviderContext, "trace" | "request"> & ("env" extends keyof TConfig ? Pick<ProviderContext, "env"> : Record<never, never>) & ("credential" extends keyof TConfig ? Pick<ProviderContext, "credential"> : Record<never, never>) & ("http" extends keyof TConfig ? Pick<ProviderContext, "http"> : Record<never, never>) & ("files" extends keyof TConfig ? Pick<ProviderContext, "files"> : Record<never, never>) & ("native" extends keyof TConfig ? Pick<ProviderContext, "native"> : Record<never, never>) & ("cache" extends keyof TConfig ? Pick<ProviderContext, "cache"> : Record<never, never>) & ("state" extends keyof TConfig ? Pick<ProviderContext, "state"> : Record<never, never>) & ("stealth" extends keyof TConfig ? Pick<ProviderContext, "stealth"> : Record<never, never>) & ("browser" extends keyof TConfig ? Pick<ProviderContext, "browser"> : Record<never, never>) & ("auth" extends keyof TConfig ? Pick<ProviderContext, "auth"> : Record<never, never>) & ("ocr" extends keyof TConfig ? Pick<ProviderContext, "ocr"> : Record<never, never>) & ("stt" extends keyof TConfig ? Pick<ProviderContext, "stt"> : Record<never, never>) & ("resolver" extends keyof TConfig ? Pick<ProviderContext, "resolver"> : Record<never, never>) & ("choice" extends keyof TConfig ? Pick<ProviderContext, "choice"> : Record<never, never>);
|
|
1879
1887
|
export interface ProxiedOAuthConfig {
|
|
1880
1888
|
authorizeUrl: string;
|
|
1881
1889
|
tokenUrl: string;
|
|
@@ -1921,7 +1929,7 @@ export interface OperationContractMetadata {
|
|
|
1921
1929
|
lifecycle?: OperationLifecycle;
|
|
1922
1930
|
deprecation?: OperationDeprecationMetadata;
|
|
1923
1931
|
}
|
|
1924
|
-
export interface OperationDefinition<TInput extends SchemaLike = SchemaLike, TOutput extends SchemaLike = SchemaLike> {
|
|
1932
|
+
export interface OperationDefinition<TInput extends SchemaLike = SchemaLike, TOutput extends SchemaLike = SchemaLike, TContext = ProviderContext> {
|
|
1925
1933
|
/**
|
|
1926
1934
|
* Short English display title for the operation. The SDK passes it through
|
|
1927
1935
|
* verbatim; the APIFuse registry derives the operation's en locale title
|
|
@@ -1951,7 +1959,7 @@ export interface OperationDefinition<TInput extends SchemaLike = SchemaLike, TOu
|
|
|
1951
1959
|
retryOnAuthRefresh?: boolean;
|
|
1952
1960
|
input: TInput;
|
|
1953
1961
|
output: TOutput;
|
|
1954
|
-
handler(ctx:
|
|
1962
|
+
handler(ctx: TContext, input: InferSchemaOutput<TInput>): OperationHandlerResult<InferSchemaOutput<TOutput>> | Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
|
|
1955
1963
|
fixtures?: {
|
|
1956
1964
|
request: InferSchemaOutput<TInput>;
|
|
1957
1965
|
response: InferSchemaOutput<TOutput>;
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { defineProvider } from "@apifuse/provider-sdk/provider";
|
|
1
|
+
import { defineProvider, type ProviderContextOf } from "@apifuse/provider-sdk/provider";
|
|
2
2
|
|
|
3
3
|
import { providerMeta } from "./meta";
|
|
4
4
|
import { operations } from "./operations";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const buildProvider = defineProvider({
|
|
7
7
|
id: "{{PROVIDER_ID}}",
|
|
8
8
|
version: "1.0.0",
|
|
9
9
|
runtime: "{{RUNTIME}}"{{BROWSER_BLOCK}},
|
|
@@ -11,5 +11,8 @@ export default defineProvider({
|
|
|
11
11
|
reviewed: "community",
|
|
12
12
|
{{SECRETS_BLOCK}}{{CREDENTIAL_BLOCK}}auth: {{AUTH_BLOCK}},
|
|
13
13
|
meta: providerMeta,
|
|
14
|
-
operations: operations,
|
|
15
14
|
});
|
|
15
|
+
|
|
16
|
+
export type ProviderContext = ProviderContextOf<typeof buildProvider>;
|
|
17
|
+
|
|
18
|
+
export default buildProvider({ operations });
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { defineOperation } from "@apifuse/provider-sdk/provider";
|
|
2
|
+
import type { ProviderContext } from "../index";
|
|
2
3
|
|
|
3
4
|
import { pingInputSchema, pingOutputSchema } from "../schemas/ping";
|
|
4
5
|
|
|
5
|
-
export const pingOperation = defineOperation({
|
|
6
|
+
export const pingOperation = defineOperation<ProviderContext>()({
|
|
6
7
|
descriptionKey: "operations.ping.description",
|
|
7
8
|
input: pingInputSchema,
|
|
8
9
|
output: pingOutputSchema,
|