@apifuse/provider-sdk 2.2.0-beta.22 → 2.2.0-beta.24
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 +4 -0
- package/bin/apifuse-pack-types.ts +234 -38
- package/bin/apifuse-perf.ts +15 -12
- package/bin/apifuse-record.ts +4 -0
- package/dist/config/loader.d.ts +8 -19
- package/dist/config/loader.js +28 -86
- package/dist/contract-types.d.ts +1 -0
- package/dist/contract.js +2 -0
- package/dist/define.d.ts +5 -1
- package/dist/define.js +79 -6
- package/dist/error-resolution.js +5 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -0
- package/dist/provider.d.ts +1 -1
- package/dist/runtime/auth-flow.d.ts +2 -1
- package/dist/runtime/auth-flow.js +4 -0
- package/dist/runtime/browser.js +78 -9
- package/dist/runtime/http.js +0 -1
- package/dist/runtime/instrumentation.js +26 -1
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +440 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +8 -0
- package/dist/runtime/resolver-vendors/bindings.js +15 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +24 -0
- package/dist/runtime/resolver-vendors/browser.js +287 -0
- package/dist/runtime/resolver-vendors/types.d.ts +42 -0
- package/dist/runtime/resolver-vendors/types.js +57 -0
- package/dist/runtime/resolver.d.ts +39 -0
- package/dist/runtime/resolver.js +414 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +245 -141
- package/dist/runtime/stealth.js +3 -6
- package/dist/runtime/stt.js +1 -12
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/server/serve.d.ts +6 -1
- package/dist/server/serve.js +39 -8
- package/dist/testing/run.js +10 -0
- package/dist/types.d.ts +163 -4
- package/package.json +1 -1
- package/src/config/loader.ts +35 -111
- package/src/contract-types.ts +1 -0
- package/src/contract.ts +2 -0
- package/src/define.ts +121 -7
- package/src/error-resolution.ts +5 -0
- package/src/index.ts +45 -1
- package/src/provider.ts +1 -0
- package/src/runtime/auth-flow.ts +6 -0
- package/src/runtime/browser.ts +139 -19
- package/src/runtime/http.ts +0 -1
- package/src/runtime/instrumentation.ts +36 -2
- package/src/runtime/ocr.ts +523 -0
- package/src/runtime/resolver-vendors/bindings.ts +31 -0
- package/src/runtime/resolver-vendors/browser.ts +420 -0
- package/src/runtime/resolver-vendors/types.ts +113 -0
- package/src/runtime/resolver.ts +668 -0
- package/src/runtime/state.ts +323 -166
- package/src/runtime/stealth.ts +3 -6
- package/src/runtime/stt.ts +1 -19
- package/src/runtime/timeout.ts +18 -0
- package/src/server/serve.ts +80 -5
- package/src/testing/run.ts +15 -0
- package/src/types.ts +188 -4
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { ProviderError } from "../errors.js";
|
|
3
|
+
import { resolverChallengeIsIdentityScoped, resolverChallengeIssuingIdentity, } from "./resolver-vendors/bindings.js";
|
|
4
|
+
import { createBrowserResolverVendorAdapter } from "./resolver-vendors/browser.js";
|
|
5
|
+
import { RESOLVER_VENDOR_CAPABILITIES, ResolverVendorUnavailableError, resolverVendorSupports, } from "./resolver-vendors/types.js";
|
|
6
|
+
export const APIFUSE__RESOLVER__2CAPTCHA__API_KEY = "APIFUSE__RESOLVER__2CAPTCHA__API_KEY";
|
|
7
|
+
export const APIFUSE__RESOLVER__CAPSOLVER__API_KEY = "APIFUSE__RESOLVER__CAPSOLVER__API_KEY";
|
|
8
|
+
export const APIFUSE__RESOLVER__CAPMONSTER__API_KEY = "APIFUSE__RESOLVER__CAPMONSTER__API_KEY";
|
|
9
|
+
export const APIFUSE__RESOLVER__TIMEOUT_MS = "APIFUSE__RESOLVER__TIMEOUT_MS";
|
|
10
|
+
export const APIFUSE__CDP_POOL__URL = "APIFUSE__CDP_POOL__URL";
|
|
11
|
+
export const DEFAULT_RESOLVER_TIMEOUT_MS = 180_000;
|
|
12
|
+
const RESOLVER_SOLUTION_CACHE_NAMESPACE = "resolver-solution";
|
|
13
|
+
const RESOLVER_SOLUTION_INDEX_CACHE_NAMESPACE = "resolver-solution-index";
|
|
14
|
+
const MIN_RESOLVER_CACHE_TTL_MS = 1_000;
|
|
15
|
+
const resolverCaches = new WeakMap();
|
|
16
|
+
const solutionIssuerDigests = new WeakMap();
|
|
17
|
+
export const RESOLVER_INSTRUMENTATION_METADATA = Symbol.for("@apifuse/provider-sdk/runtime/resolver-instrumentation-metadata");
|
|
18
|
+
export const RESOLVER_ADAPTER_REGISTRY = {
|
|
19
|
+
browser(configuration, timeoutMs, allowedHosts) {
|
|
20
|
+
return createBrowserResolverVendorAdapter({
|
|
21
|
+
allowedHosts,
|
|
22
|
+
cdpUrl: configuration,
|
|
23
|
+
timeoutMs,
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
// This is the sole allowlist for declared vendors whose registry entry may be absent.
|
|
28
|
+
// Remove a vendor here when its adapter is registered.
|
|
29
|
+
const KNOWN_UNIMPLEMENTED_RESOLVER_VENDORS = new Set([
|
|
30
|
+
"2captcha",
|
|
31
|
+
"capsolver",
|
|
32
|
+
"capmonster",
|
|
33
|
+
"custom",
|
|
34
|
+
]);
|
|
35
|
+
function normalizedEnvValue(env, key) {
|
|
36
|
+
const value = env[key]?.trim();
|
|
37
|
+
return value ? value : undefined;
|
|
38
|
+
}
|
|
39
|
+
function readPositiveIntegerEnv(env, name) {
|
|
40
|
+
const raw = env[name]?.trim();
|
|
41
|
+
if (!raw)
|
|
42
|
+
return undefined;
|
|
43
|
+
if (!/^[1-9]\d*$/.test(raw)) {
|
|
44
|
+
throw new Error(`${name} must be a positive integer`);
|
|
45
|
+
}
|
|
46
|
+
return raw;
|
|
47
|
+
}
|
|
48
|
+
function assertDeclaredKind(requestedKind, declaredKinds) {
|
|
49
|
+
if (declaredKinds.includes(requestedKind))
|
|
50
|
+
return;
|
|
51
|
+
const declared = declaredKinds.length > 0 ? declaredKinds.join(", ") : "none";
|
|
52
|
+
throw new ProviderError(`Resolver kind "${requestedKind}" is not declared; declared kinds: ${declared}`, {
|
|
53
|
+
code: "RESOLVER_KIND_NOT_DECLARED",
|
|
54
|
+
fix: `Add "${requestedKind}" to the provider's resolver.kinds declaration.`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function createUnavailableAdapter(vendor, reason) {
|
|
58
|
+
return {
|
|
59
|
+
id: vendor,
|
|
60
|
+
supports(kind) {
|
|
61
|
+
return resolverVendorSupports(vendor, kind);
|
|
62
|
+
},
|
|
63
|
+
async solve() {
|
|
64
|
+
throw new ResolverVendorUnavailableError(vendor, reason);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function createAdapter(vendor, timeoutMs, allowedHosts) {
|
|
69
|
+
const factory = RESOLVER_ADAPTER_REGISTRY[vendor.vendor];
|
|
70
|
+
if (!factory && !KNOWN_UNIMPLEMENTED_RESOLVER_VENDORS.has(vendor.vendor)) {
|
|
71
|
+
throw new Error(`Resolver adapter factory is missing for implemented vendor "${vendor.vendor}"`);
|
|
72
|
+
}
|
|
73
|
+
if (!vendor.available) {
|
|
74
|
+
return createUnavailableAdapter(vendor.vendor, vendor.reason);
|
|
75
|
+
}
|
|
76
|
+
if (factory) {
|
|
77
|
+
return factory(vendor.configuration, timeoutMs, allowedHosts);
|
|
78
|
+
}
|
|
79
|
+
return createUnavailableAdapter(vendor.vendor, "not_implemented");
|
|
80
|
+
}
|
|
81
|
+
function assertKnownResolverVendor(vendor) {
|
|
82
|
+
if (Object.hasOwn(RESOLVER_VENDOR_CAPABILITIES, vendor))
|
|
83
|
+
return;
|
|
84
|
+
throw new Error(`Unknown resolver vendor "${vendor}" in resolver configuration`);
|
|
85
|
+
}
|
|
86
|
+
function throwUnsupportedKind(kind) {
|
|
87
|
+
throw new ProviderError(`Resolver vendor chain does not support kind "${kind}"`, {
|
|
88
|
+
code: "RESOLVER_KIND_UNSUPPORTED_BY_CHAIN",
|
|
89
|
+
fix: `Add a resolver vendor that supports "${kind}" to the provider's resolver.vendors declaration.`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function throwExhausted(attempts) {
|
|
93
|
+
const summary = attempts.map(({ vendor, reason }) => `${vendor}: ${reason}`).join(", ");
|
|
94
|
+
throw new ProviderError(`Resolver vendor chain exhausted: ${summary}`, {
|
|
95
|
+
code: "RESOLVER_CHAIN_EXHAUSTED",
|
|
96
|
+
fix: "Configure another supporting resolver vendor or restore an unavailable vendor.",
|
|
97
|
+
details: attempts,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function challengeOrigin(challenge) {
|
|
101
|
+
return new URL(challenge.pageUrl).origin;
|
|
102
|
+
}
|
|
103
|
+
function resolverIdentityDigest(identity) {
|
|
104
|
+
return createHash("sha256")
|
|
105
|
+
.update(JSON.stringify({
|
|
106
|
+
proxyUrl: identity.proxyUrl ?? null,
|
|
107
|
+
userAgent: identity.userAgent,
|
|
108
|
+
}))
|
|
109
|
+
.digest("hex");
|
|
110
|
+
}
|
|
111
|
+
function resolverIdentityScopeDigest(identityScope) {
|
|
112
|
+
return createHash("sha256").update(JSON.stringify({ identityScope })).digest("hex");
|
|
113
|
+
}
|
|
114
|
+
function resolverSolutionCacheKey(cache, challenge, issuerDigest) {
|
|
115
|
+
return cache.key(RESOLVER_SOLUTION_CACHE_NAMESPACE, {
|
|
116
|
+
kind: challenge.kind,
|
|
117
|
+
origin: challengeOrigin(challenge),
|
|
118
|
+
issuerDigest,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function resolverSolutionIndexCacheKey(cache, challenge) {
|
|
122
|
+
return cache.key(RESOLVER_SOLUTION_INDEX_CACHE_NAMESPACE, {
|
|
123
|
+
kind: challenge.kind,
|
|
124
|
+
origin: challengeOrigin(challenge),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function isCachedResolverSolution(value) {
|
|
128
|
+
try {
|
|
129
|
+
if (value === null || typeof value !== "object")
|
|
130
|
+
return false;
|
|
131
|
+
const candidate = value;
|
|
132
|
+
if (typeof candidate.expiresAtMs !== "number" ||
|
|
133
|
+
typeof candidate.issuerDigest !== "string" ||
|
|
134
|
+
candidate.solution === null ||
|
|
135
|
+
typeof candidate.solution !== "object" ||
|
|
136
|
+
candidate.solution.form !== "cookies" ||
|
|
137
|
+
typeof candidate.solution.userAgent !== "string") {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const cookies = candidate.solution.cookies;
|
|
141
|
+
return (cookies !== null &&
|
|
142
|
+
typeof cookies === "object" &&
|
|
143
|
+
!Array.isArray(cookies) &&
|
|
144
|
+
Object.values(cookies).every((cookie) => typeof cookie === "string"));
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function isResolverCacheIndex(value) {
|
|
151
|
+
if (value === null || typeof value !== "object")
|
|
152
|
+
return false;
|
|
153
|
+
const entries = value.entries;
|
|
154
|
+
return (Array.isArray(entries) &&
|
|
155
|
+
entries.every((entry) => entry !== null &&
|
|
156
|
+
typeof entry === "object" &&
|
|
157
|
+
entry.direct === true &&
|
|
158
|
+
typeof entry.expiresAtMs === "number" &&
|
|
159
|
+
typeof entry.issuerDigest === "string"));
|
|
160
|
+
}
|
|
161
|
+
function solutionExpiryMs(solution) {
|
|
162
|
+
if (solution.form !== "cookies")
|
|
163
|
+
return undefined;
|
|
164
|
+
const expires = solution.expires;
|
|
165
|
+
if (typeof expires !== "number" || !Number.isFinite(expires))
|
|
166
|
+
return undefined;
|
|
167
|
+
return expires * 1_000;
|
|
168
|
+
}
|
|
169
|
+
function rememberSolutionIssuer(solution, issuerDigest) {
|
|
170
|
+
if (typeof solution === "object" && solution !== null) {
|
|
171
|
+
solutionIssuerDigests.set(solution, issuerDigest);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function readCachedSolution(cache, challenge, issuerDigest, now) {
|
|
175
|
+
const cached = await cache.get(resolverSolutionCacheKey(cache, challenge, issuerDigest));
|
|
176
|
+
if (!cached || !isCachedResolverSolution(cached.value))
|
|
177
|
+
return undefined;
|
|
178
|
+
if (cached.value.issuerDigest !== issuerDigest || cached.value.expiresAtMs <= now)
|
|
179
|
+
return undefined;
|
|
180
|
+
rememberSolutionIssuer(cached.value.solution, issuerDigest);
|
|
181
|
+
return cached.value.solution;
|
|
182
|
+
}
|
|
183
|
+
async function findCachedSolution(cache, challenge, identity, identityScope) {
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
if (identityScope !== undefined && resolverChallengeIsIdentityScoped(challenge)) {
|
|
186
|
+
return await readCachedSolution(cache, challenge, resolverIdentityScopeDigest(identityScope), now);
|
|
187
|
+
}
|
|
188
|
+
if (identity) {
|
|
189
|
+
const lookupIdentity = resolverChallengeIssuingIdentity(challenge, identity);
|
|
190
|
+
return await readCachedSolution(cache, challenge, resolverIdentityDigest(lookupIdentity), now);
|
|
191
|
+
}
|
|
192
|
+
const index = await cache.get(resolverSolutionIndexCacheKey(cache, challenge));
|
|
193
|
+
if (!index || !isResolverCacheIndex(index.value))
|
|
194
|
+
return undefined;
|
|
195
|
+
for (const entry of index.value.entries) {
|
|
196
|
+
if (entry.expiresAtMs <= now)
|
|
197
|
+
continue;
|
|
198
|
+
const solution = await readCachedSolution(cache, challenge, entry.issuerDigest, now);
|
|
199
|
+
if (solution)
|
|
200
|
+
return solution;
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
async function writeResolverCacheIndex(cache, challenge, entries, now) {
|
|
205
|
+
const indexKey = resolverSolutionIndexCacheKey(cache, challenge);
|
|
206
|
+
const liveEntries = entries.filter((entry) => entry.expiresAtMs > now);
|
|
207
|
+
if (liveEntries.length === 0) {
|
|
208
|
+
await cache.delete(indexKey);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const ttlMs = Math.max(1, Math.floor(Math.max(...liveEntries.map((entry) => entry.expiresAtMs)) - now));
|
|
212
|
+
await cache.set(indexKey, { entries: liveEntries }, { ttlMs });
|
|
213
|
+
}
|
|
214
|
+
async function cacheBrowserSolution(cache, challenge, solution, identity, identityScope) {
|
|
215
|
+
const expiresAtMs = solutionExpiryMs(solution);
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
if (expiresAtMs === undefined)
|
|
218
|
+
return;
|
|
219
|
+
const ttlMs = Math.floor(expiresAtMs - now);
|
|
220
|
+
if (ttlMs <= MIN_RESOLVER_CACHE_TTL_MS)
|
|
221
|
+
return;
|
|
222
|
+
const scopedDigest = identityScope !== undefined && resolverChallengeIsIdentityScoped(challenge)
|
|
223
|
+
? resolverIdentityScopeDigest(identityScope)
|
|
224
|
+
: undefined;
|
|
225
|
+
const issuerDigest = scopedDigest ?? resolverIdentityDigest(identity);
|
|
226
|
+
await cache.set(resolverSolutionCacheKey(cache, challenge, issuerDigest), { expiresAtMs, issuerDigest, solution }, { ttlMs });
|
|
227
|
+
rememberSolutionIssuer(solution, issuerDigest);
|
|
228
|
+
if (scopedDigest !== undefined || identity.proxyUrl !== undefined)
|
|
229
|
+
return;
|
|
230
|
+
const indexKey = resolverSolutionIndexCacheKey(cache, challenge);
|
|
231
|
+
const current = await cache.get(indexKey);
|
|
232
|
+
const currentEntries = isResolverCacheIndex(current?.value) ? current.value.entries : [];
|
|
233
|
+
await writeResolverCacheIndex(cache, challenge, [
|
|
234
|
+
...currentEntries.filter((entry) => entry.issuerDigest !== issuerDigest),
|
|
235
|
+
{ direct: true, expiresAtMs, issuerDigest },
|
|
236
|
+
], now);
|
|
237
|
+
}
|
|
238
|
+
/** Remove the cached entry for the exact solution object returned by this resolver. */
|
|
239
|
+
export async function invalidateResolverSolution(resolver, challenge, solution) {
|
|
240
|
+
const metadata = resolver[RESOLVER_INSTRUMENTATION_METADATA];
|
|
241
|
+
const cacheOwner = metadata?.target ?? resolver;
|
|
242
|
+
const invalidate = async () => {
|
|
243
|
+
if (solution.form !== "cookies")
|
|
244
|
+
return "not_cookie_solution";
|
|
245
|
+
if (!resolverCaches.has(cacheOwner)) {
|
|
246
|
+
throw new Error("Resolver cache registration lookup failed during solution invalidation");
|
|
247
|
+
}
|
|
248
|
+
const cache = resolverCaches.get(cacheOwner);
|
|
249
|
+
if (cache === null || cache === undefined)
|
|
250
|
+
return "cache_disabled";
|
|
251
|
+
const issuerDigest = solutionIssuerDigests.get(solution);
|
|
252
|
+
if (!issuerDigest)
|
|
253
|
+
return "solution_not_cached";
|
|
254
|
+
await cache.delete(resolverSolutionCacheKey(cache, challenge, issuerDigest));
|
|
255
|
+
const index = await cache.get(resolverSolutionIndexCacheKey(cache, challenge));
|
|
256
|
+
if (!index)
|
|
257
|
+
return "entry_deleted";
|
|
258
|
+
if (!isResolverCacheIndex(index.value)) {
|
|
259
|
+
throw new Error("Resolver solution cache index is malformed during invalidation");
|
|
260
|
+
}
|
|
261
|
+
await writeResolverCacheIndex(cache, challenge, index.value.entries.filter((entry) => entry.issuerDigest !== issuerDigest), Date.now());
|
|
262
|
+
return "index_entry_deleted";
|
|
263
|
+
};
|
|
264
|
+
if (!metadata) {
|
|
265
|
+
await invalidate();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
await metadata.traceRecorder.runSpan("resolver.cache.invalidate", invalidate, {
|
|
269
|
+
attributes: { challenge_kind: challenge.kind },
|
|
270
|
+
onSuccess: (outcome) => ({ outcome }),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
function createResolverChainClient(options) {
|
|
274
|
+
const client = {
|
|
275
|
+
async solve(challenge, signal = new AbortController().signal, traceRecorder) {
|
|
276
|
+
assertDeclaredKind(challenge.kind, options.kinds);
|
|
277
|
+
if (options.unavailableReason) {
|
|
278
|
+
throw new ProviderError(options.unavailableReason, {
|
|
279
|
+
code: "RESOLVER_UNAVAILABLE",
|
|
280
|
+
fix: "Configure at least one declared resolver vendor or provide a test ResolverContext override.",
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
const supportingEntries = options.entries.filter((entry) => entry.supports(challenge.kind));
|
|
284
|
+
if (supportingEntries.length === 0)
|
|
285
|
+
throwUnsupportedKind(challenge.kind);
|
|
286
|
+
signal.throwIfAborted();
|
|
287
|
+
if (options.cache && supportingEntries.some((entry) => entry.id === "browser")) {
|
|
288
|
+
const cached = await findCachedSolution(options.cache, challenge, options.identity, options.identityScope);
|
|
289
|
+
if (cached)
|
|
290
|
+
return cached;
|
|
291
|
+
}
|
|
292
|
+
const attempts = [];
|
|
293
|
+
for (const entry of supportingEntries) {
|
|
294
|
+
const adapter = entry.createAdapter();
|
|
295
|
+
try {
|
|
296
|
+
const solveAttempt = () => adapter.solve(challenge, options.identity, signal, traceRecorder);
|
|
297
|
+
const solution = traceRecorder
|
|
298
|
+
? await traceRecorder.runSpan("resolver.vendor.attempt", solveAttempt, {
|
|
299
|
+
attributes: {
|
|
300
|
+
vendor: adapter.id,
|
|
301
|
+
challenge_kind: challenge.kind,
|
|
302
|
+
},
|
|
303
|
+
onError(error) {
|
|
304
|
+
return error instanceof ResolverVendorUnavailableError
|
|
305
|
+
? { unavailability_reason: error.reason }
|
|
306
|
+
: undefined;
|
|
307
|
+
},
|
|
308
|
+
})
|
|
309
|
+
: await solveAttempt();
|
|
310
|
+
if (options.cache && entry.id === "browser" && solution.form === "cookies") {
|
|
311
|
+
const issuingIdentity = adapter.getIssuingIdentity?.(solution, options.identity, challenge);
|
|
312
|
+
if (issuingIdentity) {
|
|
313
|
+
await cacheBrowserSolution(options.cache, challenge, solution, issuingIdentity, options.identityScope);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return solution;
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
signal.throwIfAborted();
|
|
320
|
+
if (!(error instanceof ResolverVendorUnavailableError))
|
|
321
|
+
throw error;
|
|
322
|
+
attempts.push({ vendor: adapter.id, reason: error.reason });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
throwExhausted(attempts);
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
resolverCaches.set(client, options.cache ?? null);
|
|
329
|
+
return client;
|
|
330
|
+
}
|
|
331
|
+
export function createResolverClient(options) {
|
|
332
|
+
return createResolverChainClient({
|
|
333
|
+
kinds: options.kinds,
|
|
334
|
+
entries: options.adapters.map((adapter) => ({
|
|
335
|
+
id: adapter.id,
|
|
336
|
+
supports: (kind) => adapter.supports(kind),
|
|
337
|
+
createAdapter: () => adapter,
|
|
338
|
+
})),
|
|
339
|
+
unavailableReason: options.unavailableReason,
|
|
340
|
+
cache: options.cache,
|
|
341
|
+
identity: options.identity,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
function resolveVendorAvailability(vendor, env) {
|
|
345
|
+
if (vendor === "custom") {
|
|
346
|
+
return {
|
|
347
|
+
vendor,
|
|
348
|
+
available: false,
|
|
349
|
+
reason: "missing_transport",
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const envKey = vendor === "2captcha"
|
|
353
|
+
? APIFUSE__RESOLVER__2CAPTCHA__API_KEY
|
|
354
|
+
: vendor === "capsolver"
|
|
355
|
+
? APIFUSE__RESOLVER__CAPSOLVER__API_KEY
|
|
356
|
+
: vendor === "capmonster"
|
|
357
|
+
? APIFUSE__RESOLVER__CAPMONSTER__API_KEY
|
|
358
|
+
: APIFUSE__CDP_POOL__URL;
|
|
359
|
+
const configuration = normalizedEnvValue(env, envKey);
|
|
360
|
+
return configuration
|
|
361
|
+
? { vendor, available: true, configuration }
|
|
362
|
+
: { vendor, available: false, reason: "missing_credentials" };
|
|
363
|
+
}
|
|
364
|
+
export function createUnsupportedResolverClient(reason) {
|
|
365
|
+
return {
|
|
366
|
+
async solve() {
|
|
367
|
+
throw new ProviderError(reason ?? "Resolver runtime is not configured", {
|
|
368
|
+
code: "RESOLVER_UNAVAILABLE",
|
|
369
|
+
fix: "Declare resolver on the provider definition and configure vendor credentials.",
|
|
370
|
+
});
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
export function bindResolverSignal(resolver, defaultSignal) {
|
|
375
|
+
if (!defaultSignal)
|
|
376
|
+
return resolver;
|
|
377
|
+
const boundResolver = {
|
|
378
|
+
solve(challenge, signal = defaultSignal) {
|
|
379
|
+
return resolver.solve(challenge, signal);
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
if (resolverCaches.has(resolver)) {
|
|
383
|
+
resolverCaches.set(boundResolver, resolverCaches.get(resolver) ?? null);
|
|
384
|
+
}
|
|
385
|
+
return boundResolver;
|
|
386
|
+
}
|
|
387
|
+
export function createResolverClientFromEnv(config, env = process.env, options = {}) {
|
|
388
|
+
if (!config) {
|
|
389
|
+
return createUnsupportedResolverClient("Provider does not declare resolver capability");
|
|
390
|
+
}
|
|
391
|
+
if (config.vendors.length === 0) {
|
|
392
|
+
return createResolverChainClient({
|
|
393
|
+
kinds: config.kinds,
|
|
394
|
+
entries: [],
|
|
395
|
+
unavailableReason: "Provider resolver vendor chain is empty",
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
const timeoutValue = readPositiveIntegerEnv(env, APIFUSE__RESOLVER__TIMEOUT_MS);
|
|
399
|
+
const timeoutMs = timeoutValue === undefined ? DEFAULT_RESOLVER_TIMEOUT_MS : Number(timeoutValue);
|
|
400
|
+
return createResolverChainClient({
|
|
401
|
+
kinds: config.kinds,
|
|
402
|
+
entries: config.vendors.map((configuredVendor) => {
|
|
403
|
+
assertKnownResolverVendor(configuredVendor);
|
|
404
|
+
const vendor = configuredVendor;
|
|
405
|
+
return {
|
|
406
|
+
id: vendor,
|
|
407
|
+
supports: (kind) => resolverVendorSupports(vendor, kind),
|
|
408
|
+
createAdapter: () => createAdapter(resolveVendorAvailability(vendor, env), timeoutMs, options.allowedHosts ?? []),
|
|
409
|
+
};
|
|
410
|
+
}),
|
|
411
|
+
cache: options.cache,
|
|
412
|
+
identityScope: options.identityScope,
|
|
413
|
+
});
|
|
414
|
+
}
|
package/dist/runtime/state.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { ProviderError } from "../errors.js";
|
|
2
2
|
import type { ProviderRuntimeState } from "../types.js";
|
|
3
|
+
import { type ProviderRedisClient } from "./redis.js";
|
|
3
4
|
type RedisProviderRuntimeStateOptions = {
|
|
4
5
|
readonly redisUrl: string;
|
|
5
6
|
readonly providerId?: string;
|
|
7
|
+
/** Test seam; production callers use redisUrl-backed client sharing. */
|
|
8
|
+
readonly __redisClient?: ProviderRedisClient;
|
|
6
9
|
};
|
|
7
10
|
export declare class UnsupportedProviderStateError extends ProviderError {
|
|
8
11
|
constructor(message?: string);
|