@apifuse/provider-sdk 2.2.0-beta.35 → 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 +8 -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/ceremonies/index.d.ts +8 -0
- package/dist/ceremonies/index.js +32 -25
- package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
- 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 +46 -25
- package/dist/define.js +381 -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/choice.js +24 -7
- 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 +6 -1
- package/src/ceremonies/index.ts +45 -31
- package/src/cli/templates/provider/Dockerfile.tpl +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 +462 -59
- package/src/index.ts +5 -2
- package/src/provider.ts +6 -1
- package/src/runtime/browser.ts +34 -11
- package/src/runtime/choice.ts +25 -8
- 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
|
@@ -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,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.2.0-beta.
|
|
2
|
+
"version": "2.2.0-beta.37",
|
|
3
3
|
"name": "@apifuse/provider-sdk",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
@@ -111,6 +111,9 @@
|
|
|
111
111
|
"type-check": "tsc --noEmit",
|
|
112
112
|
"test": "bun test",
|
|
113
113
|
"check": "bun run lint && bun run type-check && bun run lint:deprecated && bun run lint:test-typesafety && bun run build",
|
|
114
|
+
"api:update": "bun run build && bun scripts/api-reports.ts update",
|
|
115
|
+
"api:check": "bun run build && bun scripts/api-reports.ts check",
|
|
116
|
+
"changeset:check": "bun scripts/check-changeset.ts",
|
|
114
117
|
"pack:check": "bun run build && bun bin/apifuse-pack-check.ts",
|
|
115
118
|
"pack:smoke": "bun run build && bun bin/apifuse-pack-smoke.ts",
|
|
116
119
|
"pack:types": "bun run build && bun bin/apifuse-pack-types.ts",
|
|
@@ -121,6 +124,8 @@
|
|
|
121
124
|
"devDependencies": {
|
|
122
125
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
123
126
|
"@biomejs/biome": "^2.5.0",
|
|
127
|
+
"@changesets/cli": "^3.0.1",
|
|
128
|
+
"@microsoft/api-extractor": "^7.58.13",
|
|
124
129
|
"@types/bun": "latest",
|
|
125
130
|
"@types/node": "^25.9.3",
|
|
126
131
|
"ajv": "^8.17",
|
package/src/ceremonies/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
TurnValidationError,
|
|
8
8
|
ValidationError,
|
|
9
9
|
} from "../errors.js";
|
|
10
|
+
import type { AuthStartNoInputGuard } from "../define.js";
|
|
10
11
|
import type { AuthFlowDefinition, AuthFlowInputHandler, AuthTurn, FlowContext } from "../types.js";
|
|
11
12
|
|
|
12
13
|
type TurnKind = KnownAuthTurnKind;
|
|
@@ -224,6 +225,18 @@ export function validateCeremonyOutput(turn: unknown): AuthTurn {
|
|
|
224
225
|
return turn as AuthTurn;
|
|
225
226
|
}
|
|
226
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Defines an auth flow while preserving its concrete type for downstream checks.
|
|
230
|
+
* The compile-time guard validates the inferred literal; annotating or widening
|
|
231
|
+
* a flow to `AuthFlowDefinition` before passing it defeats the check. Full
|
|
232
|
+
* enforcement requires branding, which is deferred to a future major.
|
|
233
|
+
*/
|
|
234
|
+
export function defineAuthFlow<const TFlow extends AuthFlowDefinition>(
|
|
235
|
+
flow: TFlow & AuthStartNoInputGuard<TFlow>,
|
|
236
|
+
): TFlow {
|
|
237
|
+
return flow;
|
|
238
|
+
}
|
|
239
|
+
|
|
227
240
|
export function createOAuth2Ceremony(options: {
|
|
228
241
|
authorizeUrl: string;
|
|
229
242
|
tokenUrl: string;
|
|
@@ -232,7 +245,7 @@ export function createOAuth2Ceremony(options: {
|
|
|
232
245
|
scopes: string[];
|
|
233
246
|
usePKCE?: boolean;
|
|
234
247
|
}): AuthFlowDefinition {
|
|
235
|
-
return {
|
|
248
|
+
return defineAuthFlow({
|
|
236
249
|
start: (ctx) =>
|
|
237
250
|
runCeremonyHandler(
|
|
238
251
|
async () => {
|
|
@@ -306,7 +319,7 @@ export function createOAuth2Ceremony(options: {
|
|
|
306
319
|
input,
|
|
307
320
|
),
|
|
308
321
|
abort: async () => validateCeremonyOutput(createTurn("abort", { hint: "OAuth flow aborted." })),
|
|
309
|
-
};
|
|
322
|
+
});
|
|
310
323
|
}
|
|
311
324
|
|
|
312
325
|
/**
|
|
@@ -384,7 +397,7 @@ export function createDeviceFlowCeremony(options: {
|
|
|
384
397
|
clientSecretEnvKey?: string;
|
|
385
398
|
scopes: string[];
|
|
386
399
|
}): AuthFlowDefinition {
|
|
387
|
-
return {
|
|
400
|
+
return defineAuthFlow({
|
|
388
401
|
start: (ctx) =>
|
|
389
402
|
runCeremonyHandler(
|
|
390
403
|
async () => {
|
|
@@ -461,7 +474,7 @@ export function createDeviceFlowCeremony(options: {
|
|
|
461
474
|
),
|
|
462
475
|
abort: async () =>
|
|
463
476
|
validateCeremonyOutput(createTurn("abort", { hint: "Device flow aborted." })),
|
|
464
|
-
};
|
|
477
|
+
});
|
|
465
478
|
}
|
|
466
479
|
|
|
467
480
|
export function createWebAuthnCeremony(options: {
|
|
@@ -470,7 +483,7 @@ export function createWebAuthnCeremony(options: {
|
|
|
470
483
|
verifyUrl?: string;
|
|
471
484
|
timeoutMs?: number;
|
|
472
485
|
}): AuthFlowDefinition {
|
|
473
|
-
return {
|
|
486
|
+
return defineAuthFlow({
|
|
474
487
|
start: (ctx) =>
|
|
475
488
|
runCeremonyHandler(
|
|
476
489
|
async () => {
|
|
@@ -529,7 +542,7 @@ export function createWebAuthnCeremony(options: {
|
|
|
529
542
|
),
|
|
530
543
|
abort: async () =>
|
|
531
544
|
validateCeremonyOutput(createTurn("abort", { hint: "WebAuthn ceremony aborted." })),
|
|
532
|
-
};
|
|
545
|
+
});
|
|
533
546
|
}
|
|
534
547
|
|
|
535
548
|
export function createMagicLinkCeremony(options: {
|
|
@@ -539,23 +552,31 @@ export function createMagicLinkCeremony(options: {
|
|
|
539
552
|
expiresInMs?: number;
|
|
540
553
|
}): AuthFlowDefinition {
|
|
541
554
|
const emailField = options.emailField ?? "email";
|
|
555
|
+
const buildEmailForm = () =>
|
|
556
|
+
buildJsonSchemaForm(
|
|
557
|
+
{
|
|
558
|
+
type: "object",
|
|
559
|
+
required: [emailField],
|
|
560
|
+
properties: {
|
|
561
|
+
[emailField]: { type: "string", format: "email" },
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
"Provide the email address to receive a magic link.",
|
|
565
|
+
);
|
|
542
566
|
|
|
543
|
-
return {
|
|
544
|
-
start: (ctx
|
|
567
|
+
return defineAuthFlow({
|
|
568
|
+
start: (ctx) =>
|
|
569
|
+
runCeremonyHandler(
|
|
570
|
+
async () => buildEmailForm(),
|
|
571
|
+
"Magic link start failed",
|
|
572
|
+
ctx,
|
|
573
|
+
),
|
|
574
|
+
continue: (ctx, input = {}) =>
|
|
545
575
|
runCeremonyHandler(
|
|
546
576
|
async () => {
|
|
547
577
|
const email = getString(input, emailField);
|
|
548
578
|
if (!email) {
|
|
549
|
-
return
|
|
550
|
-
{
|
|
551
|
-
type: "object",
|
|
552
|
-
required: [emailField],
|
|
553
|
-
properties: {
|
|
554
|
-
[emailField]: { type: "string", format: "email" },
|
|
555
|
-
},
|
|
556
|
-
},
|
|
557
|
-
"Provide the email address to receive a magic link.",
|
|
558
|
-
);
|
|
579
|
+
return buildEmailForm();
|
|
559
580
|
}
|
|
560
581
|
|
|
561
582
|
await ctx.http.post(options.sendUrl, { email });
|
|
@@ -570,17 +591,10 @@ export function createMagicLinkCeremony(options: {
|
|
|
570
591
|
timing: { suggestedPollIntervalMs: 5_000, maxWaitMs: 300_000 },
|
|
571
592
|
});
|
|
572
593
|
},
|
|
573
|
-
"Magic link
|
|
594
|
+
"Magic link continuation failed",
|
|
574
595
|
ctx,
|
|
575
596
|
input,
|
|
576
597
|
),
|
|
577
|
-
continue: async () =>
|
|
578
|
-
validateCeremonyOutput(
|
|
579
|
-
createTurn("poll", {
|
|
580
|
-
hint: "Continue polling for magic link completion.",
|
|
581
|
-
timing: { suggestedPollIntervalMs: 5_000, maxWaitMs: 300_000 },
|
|
582
|
-
}),
|
|
583
|
-
),
|
|
584
598
|
poll: (ctx) =>
|
|
585
599
|
runCeremonyHandler(
|
|
586
600
|
async () => {
|
|
@@ -616,7 +630,7 @@ export function createMagicLinkCeremony(options: {
|
|
|
616
630
|
),
|
|
617
631
|
abort: async () =>
|
|
618
632
|
validateCeremonyOutput(createTurn("abort", { hint: "Magic link flow aborted." })),
|
|
619
|
-
};
|
|
633
|
+
});
|
|
620
634
|
}
|
|
621
635
|
|
|
622
636
|
export function createFormCeremony(options: {
|
|
@@ -624,7 +638,7 @@ export function createFormCeremony(options: {
|
|
|
624
638
|
hint?: string;
|
|
625
639
|
mapCredential?: (input: Record<string, unknown>) => JsonObject;
|
|
626
640
|
}): AuthFlowDefinition {
|
|
627
|
-
return {
|
|
641
|
+
return defineAuthFlow({
|
|
628
642
|
start: async () =>
|
|
629
643
|
validateCeremonyOutput(
|
|
630
644
|
buildJsonSchemaForm(
|
|
@@ -656,7 +670,7 @@ export function createFormCeremony(options: {
|
|
|
656
670
|
),
|
|
657
671
|
abort: async () =>
|
|
658
672
|
validateCeremonyOutput(createTurn("abort", { hint: "Form ceremony aborted." })),
|
|
659
|
-
};
|
|
673
|
+
});
|
|
660
674
|
}
|
|
661
675
|
|
|
662
676
|
export function combineCeremonies(...ceremonies: AuthFlowDefinition[]): AuthFlowDefinition {
|
|
@@ -739,7 +753,7 @@ export function createSwitchCeremony(options: {
|
|
|
739
753
|
}): AuthFlowDefinition {
|
|
740
754
|
const choiceKeys = Object.keys(options.choices);
|
|
741
755
|
|
|
742
|
-
return {
|
|
756
|
+
return defineAuthFlow({
|
|
743
757
|
start: async () =>
|
|
744
758
|
validateCeremonyOutput(
|
|
745
759
|
createTurn("multi_choice", {
|
|
@@ -811,5 +825,5 @@ export function createSwitchCeremony(options: {
|
|
|
811
825
|
"Switch ceremony abort failed",
|
|
812
826
|
ctx,
|
|
813
827
|
),
|
|
814
|
-
};
|
|
828
|
+
});
|
|
815
829
|
}
|