@apifuse/provider-sdk 2.2.0-beta.26 → 2.2.0-beta.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/bin/apifuse-dev.ts +2 -2
- package/bin/apifuse-pack-smoke.ts +1 -1
- package/bin/apifuse-pack-types.ts +2 -1
- package/bin/apifuse-perf.ts +2 -4
- package/bin/apifuse-record.ts +1 -1
- package/dist/auth-turn/index.d.ts +1 -1
- package/dist/auth-turn/index.js +1 -1
- package/dist/ceremonies/index.js +52 -11
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.js +4 -2
- package/dist/contract-serialization.d.ts +1 -20
- package/dist/contract-serialization.js +8 -587
- package/dist/contract.d.ts +0 -2
- package/dist/contract.js +5 -9
- package/dist/index.d.ts +9 -8
- package/dist/index.js +6 -8
- package/dist/provider.d.ts +3 -2
- package/dist/provider.js +2 -2
- package/dist/runtime/auth-flow.js +1 -1
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.js +135 -12
- package/dist/runtime/instrumentation.js +1 -1
- package/dist/runtime/native-network-errors.d.ts +33 -0
- package/dist/runtime/native-network-errors.js +69 -0
- package/dist/runtime/native-network.d.ts +2 -33
- package/dist/runtime/native-network.js +2 -68
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +4 -2
- package/dist/runtime/resolver-config.d.ts +6 -0
- package/dist/runtime/resolver-config.js +6 -0
- package/dist/runtime/resolver-shared.d.ts +3 -0
- package/dist/runtime/resolver-shared.js +12 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -1
- package/dist/runtime/resolver-vendors/twocaptcha.js +80 -43
- package/dist/runtime/resolver-vendors/types.d.ts +1 -1
- package/dist/runtime/resolver.d.ts +6 -10
- package/dist/runtime/resolver.js +19 -18
- package/dist/runtime/state.js +5 -115
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.js +7 -130
- package/dist/schema.d.ts +0 -63
- package/dist/schema.js +8 -808
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/self-test.d.ts +13 -0
- package/dist/server/self-test.js +124 -46
- package/dist/server/serve-implementation.d.ts +199 -0
- package/dist/server/serve-implementation.js +2054 -0
- package/dist/server/serve.d.ts +1 -187
- package/dist/server/serve.js +1 -1827
- package/dist/stateful/errors.d.ts +5 -0
- package/dist/stateful/errors.js +10 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +1 -5
- package/dist/stateful/stateful-provider-session-routing.js +2 -10
- package/dist/stream.js +7 -1
- package/package.json +27 -2
- package/src/auth-turn/index.ts +1 -1
- package/src/ceremonies/index.ts +68 -18
- package/src/config/loader.ts +5 -2
- package/src/contract-serialization.ts +8 -859
- package/src/contract.ts +5 -16
- package/src/index.ts +18 -34
- package/src/provider.ts +12 -24
- package/src/runtime/auth-flow.ts +1 -1
- package/src/runtime/http.ts +155 -11
- package/src/runtime/instrumentation.ts +1 -1
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +16 -97
- package/src/runtime/redis.ts +7 -2
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-shared.ts +17 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +100 -49
- package/src/runtime/resolver-vendors/types.ts +1 -0
- package/src/runtime/resolver.ts +47 -22
- package/src/runtime/state.ts +5 -144
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +14 -157
- package/src/schema.ts +9 -1060
- package/src/serve.ts +6 -1
- package/src/server/index.ts +1 -0
- package/src/server/self-test.ts +184 -59
- package/src/server/serve-implementation.ts +3024 -0
- package/src/server/serve.ts +1 -2661
- package/src/stateful/errors.ts +12 -0
- package/src/stateful/stateful-provider-session-routing.ts +2 -11
- package/src/stream.ts +8 -1
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
VENDOR_DEFAULT_PROTOCOL,
|
|
13
13
|
resolveWithVendor,
|
|
14
14
|
} from "../config/loader.js";
|
|
15
|
-
import { TransportError } from "../errors.js";
|
|
16
15
|
import {
|
|
17
16
|
type DynamicEgressRuleSnapshot,
|
|
18
17
|
NativeEgressPolicyValidationError,
|
|
@@ -46,6 +45,14 @@ import type {
|
|
|
46
45
|
EnvContext,
|
|
47
46
|
} from "../types.js";
|
|
48
47
|
import { createEnvContext } from "./env.js";
|
|
48
|
+
import {
|
|
49
|
+
NativeEgressGrantExpiredError,
|
|
50
|
+
NativeEgressNotDeclaredError,
|
|
51
|
+
NativeIdleTimeoutError,
|
|
52
|
+
NativeNetworkError,
|
|
53
|
+
NativeProxyExpiredError,
|
|
54
|
+
safeDiagnosticEgressHost,
|
|
55
|
+
} from "./native-network-errors.js";
|
|
49
56
|
import {
|
|
50
57
|
NODEMAVEN_FILTER_ENV,
|
|
51
58
|
NODEMAVEN_PASSWORD_ENV,
|
|
@@ -55,102 +62,14 @@ import {
|
|
|
55
62
|
} from "./proxy-nodemaven.js";
|
|
56
63
|
import { redactSensitiveError } from "./request-options.js";
|
|
57
64
|
|
|
58
|
-
export
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
| "native_egress_grant_invalid"
|
|
67
|
-
| "native_egress_grant_limit_exceeded"
|
|
68
|
-
| "native_egress_input_invalid"
|
|
69
|
-
| "native_egress_not_declared"
|
|
70
|
-
| "native_egress_policy_invalid"
|
|
71
|
-
| "native_dynamic_egress_unsupported"
|
|
72
|
-
| "native_proxy_expired"
|
|
73
|
-
| "native_proxy_invalid";
|
|
74
|
-
|
|
75
|
-
export class NativeNetworkError extends TransportError {
|
|
76
|
-
constructor(message: string, code: NativeNetworkErrorCode, cause?: Error) {
|
|
77
|
-
const isEgressPolicyFailure =
|
|
78
|
-
code.startsWith("native_egress_") || code === "native_dynamic_egress_unsupported";
|
|
79
|
-
super(message, {
|
|
80
|
-
code,
|
|
81
|
-
status: 0,
|
|
82
|
-
...(cause ? { cause } : {}),
|
|
83
|
-
...(isEgressPolicyFailure ? { category: "provider_error" as const, retryable: false } : {}),
|
|
84
|
-
});
|
|
85
|
-
this.name = "NativeNetworkError";
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
override get code(): NativeNetworkErrorCode {
|
|
89
|
-
return super.code as NativeNetworkErrorCode;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function safeDiagnosticEgressHost(value: unknown): string {
|
|
94
|
-
const canonical = canonicalizeEgressHost(value);
|
|
95
|
-
return canonical.ok ? canonical.host : `<invalid-host:${canonical.reason}>`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export class NativeProxyExpiredError extends NativeNetworkError {
|
|
99
|
-
constructor(readonly expiresAt: string) {
|
|
100
|
-
super("Native connection closed at sticky proxy expiry", "native_proxy_expired");
|
|
101
|
-
this.name = "NativeProxyExpiredError";
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Raised before transport setup when a native destination is not authorized. */
|
|
106
|
-
export class NativeEgressNotDeclaredError extends NativeNetworkError {
|
|
107
|
-
readonly host: string;
|
|
108
|
-
|
|
109
|
-
constructor(
|
|
110
|
-
host: string,
|
|
111
|
-
readonly port: number,
|
|
112
|
-
readonly tls: "required" | "disabled",
|
|
113
|
-
) {
|
|
114
|
-
const diagnosticHost = safeDiagnosticEgressHost(host);
|
|
115
|
-
super(
|
|
116
|
-
`Native ${tls === "required" ? "TLS" : "TCP"} egress is not declared for ${diagnosticHost}:${port}`,
|
|
117
|
-
"native_egress_not_declared",
|
|
118
|
-
);
|
|
119
|
-
this.host = diagnosticHost;
|
|
120
|
-
this.name = "NativeEgressNotDeclaredError";
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Raised when the destination was authorized by a grant whose TTL elapsed and
|
|
126
|
-
* its expiry remains in the client's bounded recent-expiry evidence window.
|
|
127
|
-
*/
|
|
128
|
-
export class NativeEgressGrantExpiredError extends NativeNetworkError {
|
|
129
|
-
readonly host: string;
|
|
130
|
-
|
|
131
|
-
constructor(
|
|
132
|
-
host: string,
|
|
133
|
-
readonly port: number,
|
|
134
|
-
readonly tls: "required" | "disabled",
|
|
135
|
-
readonly expiresAt: string,
|
|
136
|
-
) {
|
|
137
|
-
const diagnosticHost = safeDiagnosticEgressHost(host);
|
|
138
|
-
super(
|
|
139
|
-
`Native ${tls === "required" ? "TLS" : "TCP"} egress grant expired for ${diagnosticHost}:${port}`,
|
|
140
|
-
"native_egress_grant_expired",
|
|
141
|
-
);
|
|
142
|
-
this.host = diagnosticHost;
|
|
143
|
-
this.name = "NativeEgressGrantExpiredError";
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Raised when an established connection exceeds its opt-in read-idle window. */
|
|
148
|
-
export class NativeIdleTimeoutError extends NativeNetworkError {
|
|
149
|
-
constructor() {
|
|
150
|
-
super("Native network socket timed out while reading.", "native_connection_idle_timeout");
|
|
151
|
-
this.name = "NativeIdleTimeoutError";
|
|
152
|
-
}
|
|
153
|
-
}
|
|
65
|
+
export {
|
|
66
|
+
NativeEgressGrantExpiredError,
|
|
67
|
+
NativeEgressNotDeclaredError,
|
|
68
|
+
NativeIdleTimeoutError,
|
|
69
|
+
NativeNetworkError,
|
|
70
|
+
NativeProxyExpiredError,
|
|
71
|
+
} from "./native-network-errors.js";
|
|
72
|
+
export type { NativeNetworkErrorCode } from "./native-network-errors.js";
|
|
154
73
|
|
|
155
74
|
export type NativeGatewayProxy = NativeProxyEgressInfo & {
|
|
156
75
|
readonly url: string;
|
package/src/runtime/redis.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
import type { Redis } from "ioredis";
|
|
4
|
+
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
2
6
|
|
|
3
7
|
export type ProviderRedisClient = Redis;
|
|
4
8
|
|
|
@@ -17,7 +21,8 @@ type RedisTimeoutOptions<T> = {
|
|
|
17
21
|
export function createProviderRedisClient(
|
|
18
22
|
options: ProviderRedisClientOptions,
|
|
19
23
|
): ProviderRedisClient {
|
|
20
|
-
const
|
|
24
|
+
const { Redis: RedisClient } = require("ioredis") as typeof import("ioredis");
|
|
25
|
+
const redis = new RedisClient(options.redisUrl, {
|
|
21
26
|
connectTimeout: options.timeoutMs,
|
|
22
27
|
enableOfflineQueue: false,
|
|
23
28
|
lazyConnect: true,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const APIFUSE__RESOLVER__2CAPTCHA__API_KEY = "APIFUSE__RESOLVER__2CAPTCHA__API_KEY";
|
|
2
|
+
export const APIFUSE__RESOLVER__CAPSOLVER__API_KEY = "APIFUSE__RESOLVER__CAPSOLVER__API_KEY";
|
|
3
|
+
export const APIFUSE__RESOLVER__CAPMONSTER__API_KEY = "APIFUSE__RESOLVER__CAPMONSTER__API_KEY";
|
|
4
|
+
export const APIFUSE__RESOLVER__TIMEOUT_MS = "APIFUSE__RESOLVER__TIMEOUT_MS";
|
|
5
|
+
export const APIFUSE__CDP_POOL__URL = "APIFUSE__CDP_POOL__URL";
|
|
6
|
+
export const DEFAULT_RESOLVER_TIMEOUT_MS = 180_000;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ProviderError } from "../errors.js";
|
|
2
|
+
import type { ResolverContext } from "../types.js";
|
|
3
|
+
|
|
4
|
+
export const RESOLVER_INSTRUMENTATION_METADATA = Symbol.for(
|
|
5
|
+
"@apifuse/provider-sdk/runtime/resolver-instrumentation-metadata",
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
export function createUnsupportedResolverClient(reason?: string): ResolverContext {
|
|
9
|
+
return {
|
|
10
|
+
async solve() {
|
|
11
|
+
throw new ProviderError(reason ?? "Resolver runtime is not configured", {
|
|
12
|
+
code: "RESOLVER_UNAVAILABLE",
|
|
13
|
+
fix: "Declare resolver on the provider definition and configure vendor credentials.",
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChallengeSolution, ProviderChallenge } from "../../types.js";
|
|
2
|
+
import type { TraceRecorder } from "../trace.js";
|
|
2
3
|
import { assertResolverHostAllowed } from "./hosts.js";
|
|
3
4
|
import {
|
|
4
5
|
type ResolverIdentity,
|
|
@@ -34,6 +35,7 @@ export interface TwoCaptchaResolverVendorAdapter extends ResolverVendorAdapter {
|
|
|
34
35
|
challenge: ProviderChallenge,
|
|
35
36
|
identity: ResolverIdentity | undefined,
|
|
36
37
|
signal: AbortSignal,
|
|
38
|
+
traceRecorder?: TraceRecorder,
|
|
37
39
|
): Promise<Extract<ChallengeSolution, { readonly form: "token" }>>;
|
|
38
40
|
}
|
|
39
41
|
|
|
@@ -231,6 +233,28 @@ function endpoint(baseUrl: string, path: string): string {
|
|
|
231
233
|
return `${baseUrl.replace(/\/+$/u, "")}/${path}`;
|
|
232
234
|
}
|
|
233
235
|
|
|
236
|
+
function spanErrorAttributes(
|
|
237
|
+
error: unknown,
|
|
238
|
+
phase: TwoCaptchaOperationPhase,
|
|
239
|
+
): Record<string, unknown> | undefined {
|
|
240
|
+
if (error instanceof ResolverVendorUnavailableError) {
|
|
241
|
+
return {
|
|
242
|
+
unavailability_reason: error.reason,
|
|
243
|
+
transport_phase: error.phase,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
// The solve budget raises TwoCaptchaSolveTimeoutError inside the spanned closure and
|
|
247
|
+
// the outer catch converts it only after the span has been finalized, so the span has
|
|
248
|
+
// to recognize it here or slow solves lose exactly the attribution these spans add.
|
|
249
|
+
if (error instanceof TwoCaptchaSolveTimeoutError) {
|
|
250
|
+
return {
|
|
251
|
+
unavailability_reason: "timeout",
|
|
252
|
+
transport_phase: phase,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
234
258
|
export function createTwoCaptchaResolverVendorAdapter(
|
|
235
259
|
options: TwoCaptchaResolverVendorOptions,
|
|
236
260
|
): TwoCaptchaResolverVendorAdapter {
|
|
@@ -248,7 +272,7 @@ export function createTwoCaptchaResolverVendorAdapter(
|
|
|
248
272
|
return resolverVendorSupports(TWOCAPTCHA_VENDOR_ID, kind);
|
|
249
273
|
},
|
|
250
274
|
|
|
251
|
-
async solve(challenge, identity, callerSignal) {
|
|
275
|
+
async solve(challenge, identity, callerSignal, traceRecorder) {
|
|
252
276
|
const apiKey = options.apiKey?.trim();
|
|
253
277
|
if (!apiKey) {
|
|
254
278
|
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "missing_credentials", {
|
|
@@ -285,62 +309,89 @@ export function createTwoCaptchaResolverVendorAdapter(
|
|
|
285
309
|
let phase: TwoCaptchaOperationPhase = "create_task";
|
|
286
310
|
|
|
287
311
|
try {
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
endpoint(baseUrl, "createTask"),
|
|
291
|
-
{
|
|
292
|
-
clientKey: apiKey,
|
|
293
|
-
task: {
|
|
294
|
-
type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
|
|
295
|
-
websiteURL: challenge.pageUrl,
|
|
296
|
-
websiteKey: challenge.siteKey,
|
|
297
|
-
isInvisible: false,
|
|
298
|
-
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
299
|
-
...(proxy ?? {}),
|
|
300
|
-
},
|
|
301
|
-
},
|
|
302
|
-
solveController.signal,
|
|
303
|
-
phase,
|
|
304
|
-
);
|
|
305
|
-
const taskId = taskIdFrom(createResult.payload);
|
|
306
|
-
if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
|
|
307
|
-
throw unavailableForPayload(createResult.payload, phase);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
phase = "poll_result";
|
|
311
|
-
while (true) {
|
|
312
|
-
callerSignal.throwIfAborted();
|
|
313
|
-
const remainingMs = timeoutMs - (now() - startedAt);
|
|
314
|
-
if (remainingMs <= 0) throw new TwoCaptchaSolveTimeoutError();
|
|
315
|
-
await delay(Math.min(pollIntervalMs, remainingMs), solveController.signal);
|
|
316
|
-
callerSignal.throwIfAborted();
|
|
317
|
-
if (now() - startedAt >= timeoutMs) throw new TwoCaptchaSolveTimeoutError();
|
|
318
|
-
|
|
319
|
-
const pollResult = await postJson(
|
|
312
|
+
const createTask = async () => {
|
|
313
|
+
const createResult = await postJson(
|
|
320
314
|
fetchImpl,
|
|
321
|
-
endpoint(baseUrl, "
|
|
322
|
-
{
|
|
315
|
+
endpoint(baseUrl, "createTask"),
|
|
316
|
+
{
|
|
317
|
+
clientKey: apiKey,
|
|
318
|
+
task: {
|
|
319
|
+
type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
|
|
320
|
+
websiteURL: challenge.pageUrl,
|
|
321
|
+
websiteKey: challenge.siteKey,
|
|
322
|
+
isInvisible: false,
|
|
323
|
+
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
324
|
+
...(proxy ?? {}),
|
|
325
|
+
},
|
|
326
|
+
},
|
|
323
327
|
solveController.signal,
|
|
324
328
|
phase,
|
|
325
329
|
);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (pollResult.payload.status === "processing") continue;
|
|
330
|
-
if (pollResult.payload.status !== "ready") {
|
|
331
|
-
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
|
|
332
|
-
phase,
|
|
333
|
-
});
|
|
330
|
+
const taskId = taskIdFrom(createResult.payload);
|
|
331
|
+
if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
|
|
332
|
+
throw unavailableForPayload(createResult.payload, phase);
|
|
334
333
|
}
|
|
334
|
+
return taskId;
|
|
335
|
+
};
|
|
336
|
+
const taskId = traceRecorder
|
|
337
|
+
? await traceRecorder.runSpan("resolver.vendor.create_task", createTask, {
|
|
338
|
+
attributes: {
|
|
339
|
+
vendor: TWOCAPTCHA_VENDOR_ID,
|
|
340
|
+
challenge_kind: challenge.kind,
|
|
341
|
+
},
|
|
342
|
+
onError: (error) => spanErrorAttributes(error, "create_task"),
|
|
343
|
+
})
|
|
344
|
+
: await createTask();
|
|
335
345
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
346
|
+
phase = "poll_result";
|
|
347
|
+
const pollResult = async () => {
|
|
348
|
+
while (true) {
|
|
349
|
+
callerSignal.throwIfAborted();
|
|
350
|
+
const remainingMs = timeoutMs - (now() - startedAt);
|
|
351
|
+
if (remainingMs <= 0) throw new TwoCaptchaSolveTimeoutError();
|
|
352
|
+
await delay(Math.min(pollIntervalMs, remainingMs), solveController.signal);
|
|
353
|
+
callerSignal.throwIfAborted();
|
|
354
|
+
if (now() - startedAt >= timeoutMs) throw new TwoCaptchaSolveTimeoutError();
|
|
355
|
+
|
|
356
|
+
const pollResult = await postJson(
|
|
357
|
+
fetchImpl,
|
|
358
|
+
endpoint(baseUrl, "getTaskResult"),
|
|
359
|
+
{ clientKey: apiKey, taskId },
|
|
360
|
+
solveController.signal,
|
|
339
361
|
phase,
|
|
340
|
-
|
|
362
|
+
);
|
|
363
|
+
if (!pollResult.ok || pollResult.payload.errorId !== 0) {
|
|
364
|
+
throw unavailableForPayload(pollResult.payload, phase);
|
|
365
|
+
}
|
|
366
|
+
if (pollResult.payload.status === "processing") continue;
|
|
367
|
+
if (pollResult.payload.status !== "ready") {
|
|
368
|
+
throw new ResolverVendorUnavailableError(
|
|
369
|
+
TWOCAPTCHA_VENDOR_ID,
|
|
370
|
+
"transport_failure",
|
|
371
|
+
{ phase },
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const token = tokenFrom(pollResult.payload);
|
|
376
|
+
if (!token?.trim()) {
|
|
377
|
+
throw new ResolverVendorUnavailableError(
|
|
378
|
+
TWOCAPTCHA_VENDOR_ID,
|
|
379
|
+
"transport_failure",
|
|
380
|
+
{ phase },
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
return { form: "token" as const, token };
|
|
341
384
|
}
|
|
342
|
-
|
|
343
|
-
|
|
385
|
+
};
|
|
386
|
+
return traceRecorder
|
|
387
|
+
? await traceRecorder.runSpan("resolver.vendor.poll_result", pollResult, {
|
|
388
|
+
attributes: {
|
|
389
|
+
vendor: TWOCAPTCHA_VENDOR_ID,
|
|
390
|
+
challenge_kind: challenge.kind,
|
|
391
|
+
},
|
|
392
|
+
onError: (error) => spanErrorAttributes(error, "poll_result"),
|
|
393
|
+
})
|
|
394
|
+
: await pollResult();
|
|
344
395
|
} catch (error) {
|
|
345
396
|
if (callerSignal.aborted) throw abortReason(callerSignal);
|
|
346
397
|
if (
|
package/src/runtime/resolver.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ProviderCache,
|
|
7
7
|
ProviderChallenge,
|
|
8
8
|
ProviderChallengeKind,
|
|
9
|
+
ProviderProxyMode,
|
|
9
10
|
ProviderResolverConfig,
|
|
10
11
|
ProviderResolverVendor,
|
|
11
12
|
ResolverContext,
|
|
@@ -29,14 +30,32 @@ import {
|
|
|
29
30
|
type ResolverVendorUnavailableReason,
|
|
30
31
|
resolverVendorSupports,
|
|
31
32
|
} from "./resolver-vendors/types.js";
|
|
33
|
+
import {
|
|
34
|
+
createUnsupportedResolverClient,
|
|
35
|
+
RESOLVER_INSTRUMENTATION_METADATA,
|
|
36
|
+
} from "./resolver-shared.js";
|
|
37
|
+
import {
|
|
38
|
+
APIFUSE__CDP_POOL__URL,
|
|
39
|
+
APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
|
|
40
|
+
APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
|
|
41
|
+
APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
|
|
42
|
+
APIFUSE__RESOLVER__TIMEOUT_MS,
|
|
43
|
+
DEFAULT_RESOLVER_TIMEOUT_MS,
|
|
44
|
+
} from "./resolver-config.js";
|
|
32
45
|
import type { TraceRecorder } from "./trace.js";
|
|
33
46
|
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export
|
|
39
|
-
|
|
47
|
+
export {
|
|
48
|
+
createUnsupportedResolverClient,
|
|
49
|
+
RESOLVER_INSTRUMENTATION_METADATA,
|
|
50
|
+
} from "./resolver-shared.js";
|
|
51
|
+
export {
|
|
52
|
+
APIFUSE__CDP_POOL__URL,
|
|
53
|
+
APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
|
|
54
|
+
APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
|
|
55
|
+
APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
|
|
56
|
+
APIFUSE__RESOLVER__TIMEOUT_MS,
|
|
57
|
+
DEFAULT_RESOLVER_TIMEOUT_MS,
|
|
58
|
+
} from "./resolver-config.js";
|
|
40
59
|
|
|
41
60
|
type EnvLike = Record<string, string | undefined>;
|
|
42
61
|
|
|
@@ -75,6 +94,8 @@ type ResolverChainClient = ResolverContext & {
|
|
|
75
94
|
export interface ResolverRuntimeOptions {
|
|
76
95
|
readonly allowedHosts?: readonly string[];
|
|
77
96
|
readonly cache?: ProviderCache;
|
|
97
|
+
/** Provider-declared proxy intent. The SDK never accepts a caller-built identity. */
|
|
98
|
+
readonly proxyMode?: ProviderProxyMode;
|
|
78
99
|
/** Server-owned context/proxy scope used only for identity-bound cache entries. */
|
|
79
100
|
readonly identityScope?: string;
|
|
80
101
|
/** SDK-owned transport already bound to the resolved proxy lease and client profile. */
|
|
@@ -143,10 +164,6 @@ const SAFE_CAUSE_MESSAGE_WORDS: ReadonlySet<string> = new Set([
|
|
|
143
164
|
"writing",
|
|
144
165
|
]);
|
|
145
166
|
|
|
146
|
-
export const RESOLVER_INSTRUMENTATION_METADATA = Symbol.for(
|
|
147
|
-
"@apifuse/provider-sdk/runtime/resolver-instrumentation-metadata",
|
|
148
|
-
);
|
|
149
|
-
|
|
150
167
|
export type ResolverInstrumentationMetadata = {
|
|
151
168
|
readonly target: ResolverContext;
|
|
152
169
|
readonly traceRecorder: TraceRecorder;
|
|
@@ -340,7 +357,9 @@ function sanitizeCauseMessage(message: string): string {
|
|
|
340
357
|
.map((token) => {
|
|
341
358
|
if (token === "[REDACTED]" || /^[a-z][a-z\d+.-]*:\/\/[^\s]+$/i.test(token)) return token;
|
|
342
359
|
const word = token.replace(/^[^a-z\d]+|[^a-z\d]+$/gi, "");
|
|
343
|
-
return word.length > 0 &&
|
|
360
|
+
return word.length > 0 &&
|
|
361
|
+
word.length <= 32 &&
|
|
362
|
+
SAFE_CAUSE_MESSAGE_WORDS.has(word.toLowerCase())
|
|
344
363
|
? token
|
|
345
364
|
: "[REDACTED]";
|
|
346
365
|
});
|
|
@@ -691,6 +710,7 @@ function createResolverChainClient(options: {
|
|
|
691
710
|
readonly unavailableReason?: string;
|
|
692
711
|
readonly cache?: ProviderCache;
|
|
693
712
|
readonly identity?: ResolverIdentity;
|
|
713
|
+
readonly proxyMode?: ProviderProxyMode;
|
|
694
714
|
readonly identityScope?: string;
|
|
695
715
|
readonly transport?: ResolverVendorTransport;
|
|
696
716
|
readonly createTransport?: ResolverRuntimeOptions["createTransport"];
|
|
@@ -715,6 +735,19 @@ function createResolverChainClient(options: {
|
|
|
715
735
|
const supportingEntries = options.entries.filter((entry) => entry.supports(challenge.kind));
|
|
716
736
|
if (supportingEntries.length === 0) throwUnsupportedKind(challenge.kind);
|
|
717
737
|
signal.throwIfAborted();
|
|
738
|
+
// A required proxy policy is checked before the cache. Solutions minted under a
|
|
739
|
+
// previous release are shared and long-lived, so consulting the cache first would
|
|
740
|
+
// keep reporting success without a proxy identity until every old entry expired.
|
|
741
|
+
const requiredProxyIdentityMissing =
|
|
742
|
+
options.proxyMode === "required" && options.identity === undefined;
|
|
743
|
+
if (requiredProxyIdentityMissing) {
|
|
744
|
+
throwExhausted(
|
|
745
|
+
supportingEntries.map((entry) => ({
|
|
746
|
+
vendor: entry.id,
|
|
747
|
+
reason: "missing_proxy_identity" as const,
|
|
748
|
+
})),
|
|
749
|
+
);
|
|
750
|
+
}
|
|
718
751
|
if (options.cache && resolverChallengeIsCacheable(challenge)) {
|
|
719
752
|
const cached = await findCachedSolution(
|
|
720
753
|
options.cache,
|
|
@@ -804,6 +837,7 @@ export function createResolverClient(options: {
|
|
|
804
837
|
readonly unavailableReason?: string;
|
|
805
838
|
readonly cache?: ProviderCache;
|
|
806
839
|
readonly identity?: ResolverIdentity;
|
|
840
|
+
readonly proxyMode?: ProviderProxyMode;
|
|
807
841
|
readonly transport?: ResolverVendorTransport;
|
|
808
842
|
readonly createTransport?: ResolverRuntimeOptions["createTransport"];
|
|
809
843
|
readonly clientProfile?: string;
|
|
@@ -819,6 +853,7 @@ export function createResolverClient(options: {
|
|
|
819
853
|
unavailableReason: options.unavailableReason,
|
|
820
854
|
cache: options.cache,
|
|
821
855
|
identity: options.identity,
|
|
856
|
+
proxyMode: options.proxyMode,
|
|
822
857
|
transport: options.transport,
|
|
823
858
|
createTransport: options.createTransport,
|
|
824
859
|
clientProfile: options.clientProfile,
|
|
@@ -853,17 +888,6 @@ function resolveVendorAvailability(
|
|
|
853
888
|
: { vendor, available: false, reason: "missing_credentials" };
|
|
854
889
|
}
|
|
855
890
|
|
|
856
|
-
export function createUnsupportedResolverClient(reason?: string): ResolverContext {
|
|
857
|
-
return {
|
|
858
|
-
async solve() {
|
|
859
|
-
throw new ProviderError(reason ?? "Resolver runtime is not configured", {
|
|
860
|
-
code: "RESOLVER_UNAVAILABLE",
|
|
861
|
-
fix: "Declare resolver on the provider definition and configure vendor credentials.",
|
|
862
|
-
});
|
|
863
|
-
},
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
|
|
867
891
|
export function bindResolverSignal(
|
|
868
892
|
resolver: ResolverContext,
|
|
869
893
|
defaultSignal: AbortSignal | undefined,
|
|
@@ -921,6 +945,7 @@ function createResolverClientFromEnvInternal(
|
|
|
921
945
|
};
|
|
922
946
|
}),
|
|
923
947
|
cache: options.cache,
|
|
948
|
+
proxyMode: options.proxyMode,
|
|
924
949
|
identityScope: options.identityScope,
|
|
925
950
|
transport: options.transport,
|
|
926
951
|
createTransport: options.createTransport,
|