@apifuse/provider-sdk 2.2.0-beta.36 → 2.2.0-beta.38
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/cli/templates/provider/index.ts.tpl +6 -3
- package/dist/cli/templates/provider/operations/ping.ts.tpl +2 -1
- package/dist/declaration-validation.d.ts +2 -0
- package/dist/declaration-validation.js +29 -3
- package/dist/define.d.ts +39 -21
- package/dist/define.js +33 -9
- package/dist/health-scenario.d.ts +1842 -0
- package/dist/health-scenario.js +624 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -0
- package/dist/provider.d.ts +5 -1
- package/dist/provider.js +1 -0
- 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.d.ts +1 -1
- package/dist/server/serve-implementation.js +25 -0
- package/dist/server/types.d.ts +4 -4
- package/dist/types.d.ts +35 -25
- 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/declaration-validation.ts +30 -3
- package/src/define.ts +144 -51
- package/src/health-scenario.ts +875 -0
- package/src/index.ts +78 -2
- package/src/provider.ts +81 -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 +52 -25
package/src/index.ts
CHANGED
|
@@ -34,8 +34,84 @@ export {
|
|
|
34
34
|
defineStreamOperation,
|
|
35
35
|
every,
|
|
36
36
|
type AuthStartNoInputGuard,
|
|
37
|
-
type
|
|
37
|
+
type ProviderBuilder,
|
|
38
|
+
type ProviderContextOf,
|
|
39
|
+
type ProviderDeclaration,
|
|
38
40
|
} from "./define.js";
|
|
41
|
+
export {
|
|
42
|
+
AssertionExpressionSchema,
|
|
43
|
+
AssertionPredicateSchema,
|
|
44
|
+
AssertStepSchema,
|
|
45
|
+
BoundedJsonPathSchema,
|
|
46
|
+
CandidateBlockSchema,
|
|
47
|
+
CandidatePolicySchema,
|
|
48
|
+
CredentialRefDeclarationSchema,
|
|
49
|
+
defineHealthScenario,
|
|
50
|
+
HealthScenarioSchema,
|
|
51
|
+
HealthStepSchema,
|
|
52
|
+
ExtractStepSchema,
|
|
53
|
+
FindFirstSchema,
|
|
54
|
+
GuardStepSchema,
|
|
55
|
+
JournalPolicySchema,
|
|
56
|
+
JsonTemplateSchema,
|
|
57
|
+
ManualTriggerPolicySchema,
|
|
58
|
+
OperandSchema,
|
|
59
|
+
OperationStepSchema,
|
|
60
|
+
QuantifierSchema,
|
|
61
|
+
ReferenceSchema,
|
|
62
|
+
RetryPolicySchema,
|
|
63
|
+
SafeRegexSchema,
|
|
64
|
+
ScopedAssertionExpressionSchema,
|
|
65
|
+
ScopedAssertionPredicateSchema,
|
|
66
|
+
ScopedItemReferenceSchema,
|
|
67
|
+
ScopedOperandSchema,
|
|
68
|
+
StepReferenceSchema,
|
|
69
|
+
RelativeDateNodeSchema,
|
|
70
|
+
ValueTypeSchema,
|
|
71
|
+
} from "./health-scenario.js";
|
|
72
|
+
export type {
|
|
73
|
+
AssertionExpression,
|
|
74
|
+
AssertionPredicate,
|
|
75
|
+
AttemptReference,
|
|
76
|
+
AssertResult,
|
|
77
|
+
AssertStep,
|
|
78
|
+
BoundedJsonPath,
|
|
79
|
+
CandidateBlock,
|
|
80
|
+
CandidatePolicy,
|
|
81
|
+
CandidateReference,
|
|
82
|
+
CredentialReference,
|
|
83
|
+
CredentialRefDeclaration,
|
|
84
|
+
EstablishedConnectionReference,
|
|
85
|
+
ExtractStep,
|
|
86
|
+
FindFirst,
|
|
87
|
+
GuardAttribution,
|
|
88
|
+
GuardReasonCode,
|
|
89
|
+
GuardResult,
|
|
90
|
+
GuardStep,
|
|
91
|
+
HealthScenario,
|
|
92
|
+
HealthStep,
|
|
93
|
+
JsonTemplate,
|
|
94
|
+
JournalPolicy,
|
|
95
|
+
ManualTriggerPolicy,
|
|
96
|
+
NonEmpty,
|
|
97
|
+
OperationResult,
|
|
98
|
+
OperationStep,
|
|
99
|
+
Operand,
|
|
100
|
+
Quantifier,
|
|
101
|
+
ReferenceNode,
|
|
102
|
+
Reference,
|
|
103
|
+
RelativeDateNode,
|
|
104
|
+
RetryPolicy,
|
|
105
|
+
SafeRegex,
|
|
106
|
+
ScopedAssertionExpression,
|
|
107
|
+
ScopedAssertionPredicate,
|
|
108
|
+
ScopedItemReference,
|
|
109
|
+
ScopedOperand,
|
|
110
|
+
StepBase,
|
|
111
|
+
StepReference,
|
|
112
|
+
ExtractResult,
|
|
113
|
+
ValueType,
|
|
114
|
+
} from "./health-scenario.js";
|
|
39
115
|
export type { DevServerOptions } from "./dev.js";
|
|
40
116
|
export { createDevServer, startDevServer } from "./dev.js";
|
|
41
117
|
export * from "./errors.js";
|
|
@@ -244,7 +320,6 @@ export type {
|
|
|
244
320
|
NativeNetworkDynamicGrantOptions,
|
|
245
321
|
NativeNetworkEgressGrant,
|
|
246
322
|
NativeProviderConfig,
|
|
247
|
-
NativeProviderContext,
|
|
248
323
|
NativeProxyDrainHandler,
|
|
249
324
|
NativeProxyEgressInfo,
|
|
250
325
|
NativeProxyExpiringEvent,
|
|
@@ -301,6 +376,7 @@ export type {
|
|
|
301
376
|
ProviderChallenge,
|
|
302
377
|
ProviderChallengeKind,
|
|
303
378
|
ProviderContext,
|
|
379
|
+
ProviderContextFor,
|
|
304
380
|
ProviderDefinition,
|
|
305
381
|
ProviderDeploymentOverrides,
|
|
306
382
|
ProviderFileRef,
|
package/src/provider.ts
CHANGED
|
@@ -35,6 +35,86 @@ export {
|
|
|
35
35
|
defineSmsOtpMatcher,
|
|
36
36
|
every,
|
|
37
37
|
} from "./define.js";
|
|
38
|
+
export type {
|
|
39
|
+
ProviderBuilder,
|
|
40
|
+
ProviderContextOf,
|
|
41
|
+
ProviderDeclaration,
|
|
42
|
+
} from "./define.js";
|
|
43
|
+
export type { JsonPrimitive, JsonValue } from "./contract-json.js";
|
|
44
|
+
export {
|
|
45
|
+
AssertionExpressionSchema,
|
|
46
|
+
AssertionPredicateSchema,
|
|
47
|
+
AssertStepSchema,
|
|
48
|
+
BoundedJsonPathSchema,
|
|
49
|
+
CandidateBlockSchema,
|
|
50
|
+
CandidatePolicySchema,
|
|
51
|
+
CredentialRefDeclarationSchema,
|
|
52
|
+
defineHealthScenario,
|
|
53
|
+
HealthScenarioSchema,
|
|
54
|
+
HealthStepSchema,
|
|
55
|
+
ExtractStepSchema,
|
|
56
|
+
FindFirstSchema,
|
|
57
|
+
GuardStepSchema,
|
|
58
|
+
JournalPolicySchema,
|
|
59
|
+
JsonTemplateSchema,
|
|
60
|
+
ManualTriggerPolicySchema,
|
|
61
|
+
OperandSchema,
|
|
62
|
+
OperationStepSchema,
|
|
63
|
+
QuantifierSchema,
|
|
64
|
+
ReferenceSchema,
|
|
65
|
+
RetryPolicySchema,
|
|
66
|
+
SafeRegexSchema,
|
|
67
|
+
ScopedAssertionExpressionSchema,
|
|
68
|
+
ScopedAssertionPredicateSchema,
|
|
69
|
+
ScopedItemReferenceSchema,
|
|
70
|
+
ScopedOperandSchema,
|
|
71
|
+
StepReferenceSchema,
|
|
72
|
+
RelativeDateNodeSchema,
|
|
73
|
+
ValueTypeSchema,
|
|
74
|
+
} from "./health-scenario.js";
|
|
75
|
+
export type {
|
|
76
|
+
AssertionExpression,
|
|
77
|
+
AssertionPredicate,
|
|
78
|
+
AttemptReference,
|
|
79
|
+
AssertResult,
|
|
80
|
+
AssertStep,
|
|
81
|
+
BoundedJsonPath,
|
|
82
|
+
CandidateBlock,
|
|
83
|
+
CandidatePolicy,
|
|
84
|
+
CandidateReference,
|
|
85
|
+
CredentialReference,
|
|
86
|
+
CredentialRefDeclaration,
|
|
87
|
+
EstablishedConnectionReference,
|
|
88
|
+
ExtractStep,
|
|
89
|
+
FindFirst,
|
|
90
|
+
GuardAttribution,
|
|
91
|
+
GuardReasonCode,
|
|
92
|
+
GuardResult,
|
|
93
|
+
GuardStep,
|
|
94
|
+
HealthScenario,
|
|
95
|
+
HealthStep,
|
|
96
|
+
JsonTemplate,
|
|
97
|
+
JournalPolicy,
|
|
98
|
+
ManualTriggerPolicy,
|
|
99
|
+
NonEmpty,
|
|
100
|
+
OperationResult,
|
|
101
|
+
OperationStep,
|
|
102
|
+
Operand,
|
|
103
|
+
Quantifier,
|
|
104
|
+
ReferenceNode,
|
|
105
|
+
Reference,
|
|
106
|
+
RelativeDateNode,
|
|
107
|
+
RetryPolicy,
|
|
108
|
+
SafeRegex,
|
|
109
|
+
ScopedAssertionExpression,
|
|
110
|
+
ScopedAssertionPredicate,
|
|
111
|
+
ScopedItemReference,
|
|
112
|
+
ScopedOperand,
|
|
113
|
+
StepBase,
|
|
114
|
+
StepReference,
|
|
115
|
+
ExtractResult,
|
|
116
|
+
ValueType,
|
|
117
|
+
} from "./health-scenario.js";
|
|
38
118
|
export {
|
|
39
119
|
AuthError,
|
|
40
120
|
HttpRedirectError,
|
|
@@ -107,7 +187,6 @@ export type {
|
|
|
107
187
|
NativeNetworkDynamicGrantOptions,
|
|
108
188
|
NativeNetworkEgressGrant,
|
|
109
189
|
NativeProviderConfig,
|
|
110
|
-
NativeProviderContext,
|
|
111
190
|
NativeProxyDrainHandler,
|
|
112
191
|
NativeProxyEgressInfo,
|
|
113
192
|
NativeProxyExpiringEvent,
|
|
@@ -142,6 +221,7 @@ export type {
|
|
|
142
221
|
ProviderChoiceIssueOptions,
|
|
143
222
|
ProviderChoiceParseOptions,
|
|
144
223
|
ProviderContext,
|
|
224
|
+
ProviderContextFor,
|
|
145
225
|
ProviderDefinition,
|
|
146
226
|
ProviderDeploymentOverrides,
|
|
147
227
|
ProviderFileRef,
|
package/src/runtime/browser.ts
CHANGED
|
@@ -804,8 +804,11 @@ class PlaywrightBrowserPage implements BrowserPageContract {
|
|
|
804
804
|
private readonly proxy: LaunchOptions["proxy"] = undefined,
|
|
805
805
|
) {}
|
|
806
806
|
|
|
807
|
-
async goto(
|
|
808
|
-
|
|
807
|
+
async goto(
|
|
808
|
+
url: string,
|
|
809
|
+
options?: { readonly timeout?: number; readonly waitUntil?: "load" | "domcontentloaded" },
|
|
810
|
+
): Promise<void> {
|
|
811
|
+
await this.page.goto(url, options);
|
|
809
812
|
}
|
|
810
813
|
|
|
811
814
|
async evaluate<T>(fn: string | (() => T)): Promise<T> {
|
|
@@ -1492,17 +1495,33 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1492
1495
|
return this.pageId;
|
|
1493
1496
|
}
|
|
1494
1497
|
|
|
1495
|
-
async goto(
|
|
1498
|
+
async goto(
|
|
1499
|
+
url: string,
|
|
1500
|
+
options?: { readonly timeout?: number; readonly waitUntil?: "load" | "domcontentloaded" },
|
|
1501
|
+
): Promise<void> {
|
|
1496
1502
|
await this.initialize();
|
|
1497
1503
|
const startedAt = Date.now();
|
|
1498
|
-
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1504
|
+
const timeout = options?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
1505
|
+
const waitUntil = options?.waitUntil ?? "load";
|
|
1506
|
+
let expectedEventSeen = false;
|
|
1507
|
+
const eventName =
|
|
1508
|
+
waitUntil === "domcontentloaded" ? "Page.domContentEventFired" : "Page.loadEventFired";
|
|
1509
|
+
const unsubscribe = this.pageClient.on(eventName, () => {
|
|
1510
|
+
expectedEventSeen = true;
|
|
1501
1511
|
});
|
|
1502
1512
|
|
|
1503
1513
|
try {
|
|
1504
|
-
await this.pageClient.send("Page.navigate", { url })
|
|
1505
|
-
|
|
1514
|
+
const navigation = (await this.pageClient.send("Page.navigate", { url })) as {
|
|
1515
|
+
readonly errorText?: unknown;
|
|
1516
|
+
};
|
|
1517
|
+
if (typeof navigation.errorText === "string" && navigation.errorText.length > 0) {
|
|
1518
|
+
throw new Error(`Page.navigate failed: ${navigation.errorText} at ${url}`);
|
|
1519
|
+
}
|
|
1520
|
+
await this.waitForDocumentReady(
|
|
1521
|
+
startedAt + timeout,
|
|
1522
|
+
() => expectedEventSeen,
|
|
1523
|
+
waitUntil,
|
|
1524
|
+
);
|
|
1506
1525
|
} finally {
|
|
1507
1526
|
unsubscribe();
|
|
1508
1527
|
}
|
|
@@ -1866,12 +1885,16 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1866
1885
|
|
|
1867
1886
|
private async waitForDocumentReady(
|
|
1868
1887
|
deadline: number,
|
|
1869
|
-
|
|
1888
|
+
isExpectedEventSeen: () => boolean,
|
|
1889
|
+
waitUntil: "load" | "domcontentloaded",
|
|
1870
1890
|
): Promise<void> {
|
|
1871
1891
|
while (Date.now() < deadline) {
|
|
1872
1892
|
const readyState = await this.evaluate<string>("document.readyState");
|
|
1873
|
-
|
|
1874
|
-
|
|
1893
|
+
const documentReady =
|
|
1894
|
+
readyState === "complete" ||
|
|
1895
|
+
(waitUntil === "domcontentloaded" && readyState === "interactive");
|
|
1896
|
+
if (documentReady) {
|
|
1897
|
+
if (isExpectedEventSeen() || readyState === "complete") {
|
|
1875
1898
|
return;
|
|
1876
1899
|
}
|
|
1877
1900
|
}
|
|
@@ -8,10 +8,12 @@ export {
|
|
|
8
8
|
createResolverClient,
|
|
9
9
|
createResolverClientFromEnv,
|
|
10
10
|
createUnsupportedResolverClient,
|
|
11
|
+
DEFAULT_RESOLVER_VENDOR_PREFERENCE,
|
|
11
12
|
DEFAULT_RESOLVER_TIMEOUT_MS,
|
|
12
13
|
invalidateResolverSolution,
|
|
13
14
|
RESOLVER_ADAPTER_REGISTRY,
|
|
14
15
|
RESOLVER_INSTRUMENTATION_METADATA,
|
|
16
|
+
resolveProviderResolverVendors,
|
|
15
17
|
type ResolverAdapterFactory,
|
|
16
18
|
type ResolverInstrumentationMetadata,
|
|
17
19
|
type ResolverRuntimeOptions,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
|
|
19
19
|
const BROWSER_VENDOR_ID = "browser" as const;
|
|
20
20
|
const DEFAULT_COOKIE_POLL_INTERVAL_MS = 100;
|
|
21
|
+
const NAVIGATION_BLOCKED_ERROR_TEXT = "net::ERR_BLOCKED_BY_CLIENT";
|
|
21
22
|
const AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX = ".awswaf.com";
|
|
22
23
|
const RESOLVER_DOCUMENT_CONTENT_SECURITY_POLICY = "connect-src http: https:; worker-src 'none'";
|
|
23
24
|
|
|
@@ -65,12 +66,41 @@ export interface BrowserResolverVendorAdapter extends ResolverVendorAdapter {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
class BrowserSolveTimeoutError extends Error {
|
|
68
|
-
constructor() {
|
|
69
|
-
super(
|
|
69
|
+
constructor(blockedRequests: readonly string[]) {
|
|
70
|
+
super(
|
|
71
|
+
`Browser resolver solve budget elapsed${formatBlockedRequests(blockedRequests)}`,
|
|
72
|
+
);
|
|
70
73
|
this.name = "BrowserSolveTimeoutError";
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
class BrowserNavigationBlockedError extends Error {
|
|
78
|
+
readonly code = "RESOLVER_BROWSER_NAVIGATION_BLOCKED";
|
|
79
|
+
|
|
80
|
+
constructor(
|
|
81
|
+
readonly navigationUrl: string,
|
|
82
|
+
readonly blockedUrls: readonly string[],
|
|
83
|
+
options: ErrorOptions,
|
|
84
|
+
) {
|
|
85
|
+
super(
|
|
86
|
+
`Browser resolver navigation was blocked for ${navigationUrl}${formatBlockedRequests(blockedUrls)}`,
|
|
87
|
+
options,
|
|
88
|
+
);
|
|
89
|
+
this.name = "BrowserNavigationBlockedError";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatBlockedRequests(blockedRequests: readonly string[]): string {
|
|
94
|
+
if (blockedRequests.length === 0) return "";
|
|
95
|
+
const displayed = blockedRequests.slice(0, 5);
|
|
96
|
+
const remainder = blockedRequests.length - displayed.length;
|
|
97
|
+
return `; blocked ${blockedRequests.length} requests: [${displayed.join(", ")}]${remainder > 0 ? ` (+${remainder} more)` : ""}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isNavigationBlockedError(error: unknown): error is Error {
|
|
101
|
+
return error instanceof Error && error.message.includes(NAVIGATION_BLOCKED_ERROR_TEXT);
|
|
102
|
+
}
|
|
103
|
+
|
|
74
104
|
class BrowserCleanupTimeoutError extends Error {
|
|
75
105
|
constructor(timeoutMs: number) {
|
|
76
106
|
super(`Browser resolver cleanup exceeded ${timeoutMs}ms`);
|
|
@@ -223,6 +253,8 @@ async function solveInPage(
|
|
|
223
253
|
allowedHosts: readonly string[],
|
|
224
254
|
successCookieName: string,
|
|
225
255
|
pollIntervalMs: number,
|
|
256
|
+
gotoTimeoutMs: number,
|
|
257
|
+
blockedRequests: Set<string>,
|
|
226
258
|
signal: AbortSignal,
|
|
227
259
|
): Promise<Extract<ChallengeSolution, { readonly form: "cookies" }>> {
|
|
228
260
|
return await page.withResourcePolicy(
|
|
@@ -232,11 +264,13 @@ async function solveInPage(
|
|
|
232
264
|
routes: [
|
|
233
265
|
{
|
|
234
266
|
match: () => true,
|
|
235
|
-
handle: (request) =>
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
267
|
+
handle: (request) => {
|
|
268
|
+
if (isResolverBrowserRequestAllowed(request.url, challengeKind, allowedHosts)) {
|
|
269
|
+
return { action: "continue" };
|
|
270
|
+
}
|
|
271
|
+
blockedRequests.add(request.url);
|
|
272
|
+
return { action: "block" };
|
|
273
|
+
},
|
|
240
274
|
},
|
|
241
275
|
],
|
|
242
276
|
},
|
|
@@ -245,7 +279,19 @@ async function solveInPage(
|
|
|
245
279
|
() => page.userAgent(),
|
|
246
280
|
signal,
|
|
247
281
|
);
|
|
248
|
-
|
|
282
|
+
try {
|
|
283
|
+
await raceWithAbort(
|
|
284
|
+
() =>
|
|
285
|
+
page.goto(pageUrl, {
|
|
286
|
+
timeout: gotoTimeoutMs,
|
|
287
|
+
waitUntil: "domcontentloaded",
|
|
288
|
+
}),
|
|
289
|
+
signal,
|
|
290
|
+
);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (!isNavigationBlockedError(error)) throw error;
|
|
293
|
+
throw new BrowserNavigationBlockedError(pageUrl, [...blockedRequests], { cause: error });
|
|
294
|
+
}
|
|
249
295
|
|
|
250
296
|
while (true) {
|
|
251
297
|
const cookies = await raceWithAbort(() => page.cookies(), signal);
|
|
@@ -304,6 +350,7 @@ function poolErrorCode(error: Error): number | undefined {
|
|
|
304
350
|
|
|
305
351
|
function knownUnavailableReason(
|
|
306
352
|
error: unknown,
|
|
353
|
+
beforePageSolve = false,
|
|
307
354
|
): "allocation_exhausted" | "missing_credentials" | "transport_failure" | undefined {
|
|
308
355
|
// Source-grounded mappings:
|
|
309
356
|
// - apps/cdp-pool/src/index.ts: the JSON-RPC codes and messages below.
|
|
@@ -338,7 +385,10 @@ function knownUnavailableReason(
|
|
|
338
385
|
return "transport_failure";
|
|
339
386
|
}
|
|
340
387
|
|
|
341
|
-
|
|
388
|
+
// Browser creation, Playwright launch, and CDP connection all happen before the
|
|
389
|
+
// isolated-page handler is entered. Errors after that boundary belong to the
|
|
390
|
+
// challenge solve and must retain their existing classification.
|
|
391
|
+
return beforePageSolve ? "transport_failure" : undefined;
|
|
342
392
|
}
|
|
343
393
|
|
|
344
394
|
async function closeBrowserClient(
|
|
@@ -400,11 +450,12 @@ export function createBrowserResolverVendorAdapter(
|
|
|
400
450
|
throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "not_implemented");
|
|
401
451
|
}
|
|
402
452
|
|
|
453
|
+
const blockedRequests = new Set<string>();
|
|
403
454
|
const solveController = new AbortController();
|
|
404
455
|
const onCallerAbort = () => solveController.abort(abortReason(callerSignal));
|
|
405
456
|
callerSignal.addEventListener("abort", onCallerAbort, { once: true });
|
|
406
457
|
const timeout = setTimeout(
|
|
407
|
-
() => solveController.abort(new BrowserSolveTimeoutError()),
|
|
458
|
+
() => solveController.abort(new BrowserSolveTimeoutError([...blockedRequests])),
|
|
408
459
|
options.timeoutMs,
|
|
409
460
|
);
|
|
410
461
|
|
|
@@ -427,6 +478,8 @@ export function createBrowserResolverVendorAdapter(
|
|
|
427
478
|
options.allowedHosts,
|
|
428
479
|
SUCCESS_COOKIE_NAMES[challengeKind],
|
|
429
480
|
pollIntervalMs,
|
|
481
|
+
options.timeoutMs,
|
|
482
|
+
blockedRequests,
|
|
430
483
|
solveController.signal,
|
|
431
484
|
);
|
|
432
485
|
});
|
|
@@ -457,10 +510,15 @@ export function createBrowserResolverVendorAdapter(
|
|
|
457
510
|
cause: error,
|
|
458
511
|
});
|
|
459
512
|
}
|
|
513
|
+
if (error instanceof BrowserNavigationBlockedError) {
|
|
514
|
+
throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "transport_failure", {
|
|
515
|
+
cause: error,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
460
518
|
if (error instanceof ResolverVendorUnavailableError) {
|
|
461
519
|
throw error;
|
|
462
520
|
}
|
|
463
|
-
const reason = knownUnavailableReason(error);
|
|
521
|
+
const reason = knownUnavailableReason(error, !handlerEntered);
|
|
464
522
|
if (reason) {
|
|
465
523
|
throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, reason, { cause: error });
|
|
466
524
|
}
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
ChallengeSolution,
|
|
3
3
|
ProviderChallenge,
|
|
4
4
|
ProviderChallengeKind,
|
|
5
|
+
ProviderResolverConfig,
|
|
5
6
|
ProviderResolverVendor,
|
|
6
7
|
} from "../../types.js";
|
|
7
8
|
import type { TraceRecorder } from "../trace.js";
|
|
@@ -42,6 +43,16 @@ export const RESOLVER_VENDOR_CAPABILITIES = {
|
|
|
42
43
|
],
|
|
43
44
|
} as const satisfies Readonly<Record<ProviderResolverVendor, readonly ProviderChallengeKind[]>>;
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* SDK-owned fallback policy for hosted resolver vendors. Capability support is
|
|
48
|
+
* applied separately, so each provider receives only vendors that support one
|
|
49
|
+
* or more of its declared challenge kinds.
|
|
50
|
+
*/
|
|
51
|
+
export const DEFAULT_RESOLVER_VENDOR_PREFERENCE = [
|
|
52
|
+
"capsolver",
|
|
53
|
+
"2captcha",
|
|
54
|
+
] as const satisfies readonly ProviderResolverVendor[];
|
|
55
|
+
|
|
45
56
|
export function resolverVendorSupports(
|
|
46
57
|
vendor: ProviderResolverVendor,
|
|
47
58
|
kind: ProviderChallengeKind,
|
|
@@ -49,6 +60,16 @@ export function resolverVendorSupports(
|
|
|
49
60
|
return (RESOLVER_VENDOR_CAPABILITIES[vendor] as readonly ProviderChallengeKind[]).includes(kind);
|
|
50
61
|
}
|
|
51
62
|
|
|
63
|
+
/** Resolves an explicit provider override or the SDK-owned default vendor chain. */
|
|
64
|
+
export function resolveProviderResolverVendors(
|
|
65
|
+
config: ProviderResolverConfig,
|
|
66
|
+
): readonly ProviderResolverVendor[] {
|
|
67
|
+
if (config.vendors !== undefined) return config.vendors;
|
|
68
|
+
return DEFAULT_RESOLVER_VENDOR_PREFERENCE.filter((vendor) =>
|
|
69
|
+
config.kinds.some((kind) => resolverVendorSupports(vendor, kind)),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
52
73
|
export interface ResolverIdentity {
|
|
53
74
|
readonly proxyUrl: string;
|
|
54
75
|
readonly userAgent: string;
|
package/src/runtime/resolver.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
type ResolverVendorTransport,
|
|
36
36
|
ResolverVendorUnavailableError,
|
|
37
37
|
type ResolverVendorUnavailableReason,
|
|
38
|
+
resolveProviderResolverVendors,
|
|
38
39
|
resolverVendorSupports,
|
|
39
40
|
} from "./resolver-vendors/types.js";
|
|
40
41
|
import {
|
|
@@ -64,6 +65,10 @@ export {
|
|
|
64
65
|
APIFUSE__RESOLVER__TIMEOUT_MS,
|
|
65
66
|
DEFAULT_RESOLVER_TIMEOUT_MS,
|
|
66
67
|
} from "./resolver-config.js";
|
|
68
|
+
export {
|
|
69
|
+
DEFAULT_RESOLVER_VENDOR_PREFERENCE,
|
|
70
|
+
resolveProviderResolverVendors,
|
|
71
|
+
} from "./resolver-vendors/types.js";
|
|
67
72
|
|
|
68
73
|
type EnvLike = Record<string, string | undefined>;
|
|
69
74
|
|
|
@@ -340,10 +345,12 @@ function assertKnownResolverVendor(vendor: string): asserts vendor is ProviderRe
|
|
|
340
345
|
throw new Error(`Unknown resolver vendor "${vendor}" in resolver configuration`);
|
|
341
346
|
}
|
|
342
347
|
|
|
343
|
-
function throwUnsupportedKind(kind: ProviderChallengeKind): never {
|
|
348
|
+
function throwUnsupportedKind(kind: ProviderChallengeKind, usingDefaultVendors: boolean): never {
|
|
344
349
|
throw new ProviderError(`Resolver vendor chain does not support kind "${kind}"`, {
|
|
345
350
|
code: "RESOLVER_KIND_UNSUPPORTED_BY_CHAIN",
|
|
346
|
-
fix:
|
|
351
|
+
fix: usingDefaultVendors
|
|
352
|
+
? `No SDK default resolver vendor supports "${kind}". Declare an explicit resolver.vendors override with a supporting vendor.`
|
|
353
|
+
: `Add a resolver vendor that supports "${kind}" to the provider's resolver.vendors declaration.`,
|
|
347
354
|
});
|
|
348
355
|
}
|
|
349
356
|
|
|
@@ -814,6 +821,7 @@ function createResolverChainClient(options: {
|
|
|
814
821
|
readonly kinds: readonly ProviderChallengeKind[];
|
|
815
822
|
readonly entries: readonly ResolverChainEntry[];
|
|
816
823
|
readonly unavailableReason?: string;
|
|
824
|
+
readonly usingDefaultVendors?: boolean;
|
|
817
825
|
readonly cache?: ProviderCache;
|
|
818
826
|
readonly identity?: ResolverIdentity;
|
|
819
827
|
readonly proxyIntent?: ResolverRuntimeOptions["proxyIntent"];
|
|
@@ -839,7 +847,9 @@ function createResolverChainClient(options: {
|
|
|
839
847
|
}
|
|
840
848
|
|
|
841
849
|
const supportingEntries = options.entries.filter((entry) => entry.supports(challenge.kind));
|
|
842
|
-
if (supportingEntries.length === 0)
|
|
850
|
+
if (supportingEntries.length === 0) {
|
|
851
|
+
throwUnsupportedKind(challenge.kind, options.usingDefaultVendors ?? false);
|
|
852
|
+
}
|
|
843
853
|
signal.throwIfAborted();
|
|
844
854
|
const identityResolution = options.proxyIntent
|
|
845
855
|
? await resolveResolverIdentity(options.proxyIntent)
|
|
@@ -1033,7 +1043,8 @@ function createResolverClientFromEnvInternal(
|
|
|
1033
1043
|
}
|
|
1034
1044
|
assertClientProfileTransportContract(config.clientProfile, options.transport);
|
|
1035
1045
|
|
|
1036
|
-
|
|
1046
|
+
const vendors = resolveProviderResolverVendors(config);
|
|
1047
|
+
if (config.vendors !== undefined && config.vendors.length === 0) {
|
|
1037
1048
|
return createResolverChainClient({
|
|
1038
1049
|
kinds: config.kinds,
|
|
1039
1050
|
entries: [],
|
|
@@ -1047,7 +1058,7 @@ function createResolverClientFromEnvInternal(
|
|
|
1047
1058
|
|
|
1048
1059
|
return createResolverChainClient({
|
|
1049
1060
|
kinds: config.kinds,
|
|
1050
|
-
entries:
|
|
1061
|
+
entries: vendors.map((configuredVendor) => {
|
|
1051
1062
|
assertKnownResolverVendor(configuredVendor);
|
|
1052
1063
|
const vendor = configuredVendor;
|
|
1053
1064
|
return {
|
|
@@ -1062,6 +1073,7 @@ function createResolverClientFromEnvInternal(
|
|
|
1062
1073
|
),
|
|
1063
1074
|
};
|
|
1064
1075
|
}),
|
|
1076
|
+
usingDefaultVendors: config.vendors === undefined,
|
|
1065
1077
|
cache: options.cache,
|
|
1066
1078
|
proxyIntent: options.proxyIntent,
|
|
1067
1079
|
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";
|
|
@@ -745,7 +746,7 @@ function createProviderContext(
|
|
|
745
746
|
...event,
|
|
746
747
|
}),
|
|
747
748
|
}),
|
|
748
|
-
});
|
|
749
|
+
} as ProviderContext);
|
|
749
750
|
wrappedContext = context;
|
|
750
751
|
return context;
|
|
751
752
|
}
|
|
@@ -1441,6 +1442,41 @@ function extractRequestId(raw: unknown): string | undefined {
|
|
|
1441
1442
|
return typeof value === "string" ? value : undefined;
|
|
1442
1443
|
}
|
|
1443
1444
|
|
|
1445
|
+
type ProviderErrorCauseFrame = {
|
|
1446
|
+
errorClass: string;
|
|
1447
|
+
code?: string;
|
|
1448
|
+
messageLength: number;
|
|
1449
|
+
messageFingerprint: string;
|
|
1450
|
+
};
|
|
1451
|
+
|
|
1452
|
+
const MAX_PROVIDER_ERROR_CAUSE_FRAMES = 5;
|
|
1453
|
+
|
|
1454
|
+
function providerErrorCauseChain(error: unknown): ProviderErrorCauseFrame[] | undefined {
|
|
1455
|
+
if (!(error instanceof Error) && !isProviderError(error)) return undefined;
|
|
1456
|
+
|
|
1457
|
+
const seen = new Set<object>([error]);
|
|
1458
|
+
const frames: ProviderErrorCauseFrame[] = [];
|
|
1459
|
+
let cause = error.cause;
|
|
1460
|
+
|
|
1461
|
+
while (
|
|
1462
|
+
frames.length < MAX_PROVIDER_ERROR_CAUSE_FRAMES &&
|
|
1463
|
+
(cause instanceof Error || isProviderError(cause)) &&
|
|
1464
|
+
!seen.has(cause)
|
|
1465
|
+
) {
|
|
1466
|
+
seen.add(cause);
|
|
1467
|
+
const message = cause.message;
|
|
1468
|
+
frames.push({
|
|
1469
|
+
errorClass: cause.name,
|
|
1470
|
+
...(isProviderError(cause) && typeof cause.code === "string" ? { code: cause.code } : {}),
|
|
1471
|
+
messageLength: message.length,
|
|
1472
|
+
messageFingerprint: createHash("sha256").update(message).digest("hex").slice(0, 12),
|
|
1473
|
+
});
|
|
1474
|
+
cause = cause.cause;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
return frames.length > 0 ? frames : undefined;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1444
1480
|
function logProviderError(
|
|
1445
1481
|
logger: ProviderServerLogger | unknown,
|
|
1446
1482
|
provider: ProviderDefinition,
|
|
@@ -1461,6 +1497,7 @@ function logProviderError(
|
|
|
1461
1497
|
: "internal_error";
|
|
1462
1498
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
1463
1499
|
const message = error instanceof Error ? error.message : String(error);
|
|
1500
|
+
const causeChain = providerErrorCauseChain(error);
|
|
1464
1501
|
const details = errorObservabilityDetails(error, declaredErrorCode);
|
|
1465
1502
|
const isUnregisteredProviderErrorCode =
|
|
1466
1503
|
status === 500 &&
|
|
@@ -1482,6 +1519,7 @@ function logProviderError(
|
|
|
1482
1519
|
code,
|
|
1483
1520
|
errorClass,
|
|
1484
1521
|
message,
|
|
1522
|
+
...(causeChain ? { causeChain } : {}),
|
|
1485
1523
|
...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
|
|
1486
1524
|
errorCategory: details.category,
|
|
1487
1525
|
taxonomyVersion: details.taxonomyVersion,
|
package/src/testing/run.ts
CHANGED
|
@@ -525,7 +525,7 @@ function createUpstreamContext(
|
|
|
525
525
|
...(provider.native
|
|
526
526
|
? {
|
|
527
527
|
native: {
|
|
528
|
-
|
|
528
|
+
network: {
|
|
529
529
|
connectTcp: async (options) => {
|
|
530
530
|
const request = snapshotNativeConnectInput(options);
|
|
531
531
|
requireNativeEgress().assertConnect(request, "disabled");
|
|
@@ -578,7 +578,7 @@ function createUpstreamContext(
|
|
|
578
578
|
credential,
|
|
579
579
|
state,
|
|
580
580
|
}),
|
|
581
|
-
};
|
|
581
|
+
} as ProviderContext;
|
|
582
582
|
}
|
|
583
583
|
|
|
584
584
|
function createNativeConnection(
|
|
@@ -709,7 +709,7 @@ export function createSnapshotContext(rawFixture: unknown): ProviderContext {
|
|
|
709
709
|
credential,
|
|
710
710
|
state,
|
|
711
711
|
}),
|
|
712
|
-
};
|
|
712
|
+
} satisfies Omit<ProviderContext, "native"> as unknown as ProviderContext;
|
|
713
713
|
snapshotCaptureStates.set(context, {
|
|
714
714
|
assertConsumed() {
|
|
715
715
|
if (streamCaptureGroup && nextCaptureItem !== streamCaptureGroup.items.length) {
|