@apifuse/provider-sdk 2.2.0-beta.23 → 2.2.0-beta.25

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bin/apifuse-dev.ts +2 -0
  3. package/bin/apifuse-pack-types.ts +234 -38
  4. package/bin/apifuse-perf.ts +15 -12
  5. package/bin/apifuse-record.ts +2 -0
  6. package/bin/apifuse-submit-check.ts +15 -2
  7. package/dist/config/loader.d.ts +8 -19
  8. package/dist/config/loader.js +28 -86
  9. package/dist/define.d.ts +4 -1
  10. package/dist/define.js +64 -6
  11. package/dist/index.d.ts +4 -3
  12. package/dist/index.js +2 -1
  13. package/dist/provider.d.ts +1 -1
  14. package/dist/runtime/auth-flow.js +2 -0
  15. package/dist/runtime/browser.js +50 -0
  16. package/dist/runtime/cache.d.ts +1 -0
  17. package/dist/runtime/cache.js +169 -15
  18. package/dist/runtime/http.js +0 -1
  19. package/dist/runtime/instrumentation.js +26 -1
  20. package/dist/runtime/resolver-vendors/bindings.d.ts +8 -0
  21. package/dist/runtime/resolver-vendors/bindings.js +15 -0
  22. package/dist/runtime/resolver-vendors/browser.d.ts +24 -0
  23. package/dist/runtime/resolver-vendors/browser.js +287 -0
  24. package/dist/runtime/resolver-vendors/types.d.ts +42 -0
  25. package/dist/runtime/resolver-vendors/types.js +57 -0
  26. package/dist/runtime/resolver.d.ts +39 -0
  27. package/dist/runtime/resolver.js +414 -0
  28. package/dist/runtime/state.d.ts +3 -0
  29. package/dist/runtime/state.js +245 -141
  30. package/dist/runtime/stealth.js +3 -6
  31. package/dist/server/serve.d.ts +4 -1
  32. package/dist/server/serve.js +35 -8
  33. package/dist/testing/run.js +7 -0
  34. package/dist/types.d.ts +115 -7
  35. package/package.json +1 -1
  36. package/src/config/loader.ts +35 -111
  37. package/src/define.ts +105 -8
  38. package/src/index.ts +21 -1
  39. package/src/provider.ts +1 -0
  40. package/src/runtime/auth-flow.ts +2 -0
  41. package/src/runtime/browser.ts +69 -0
  42. package/src/runtime/cache.ts +189 -14
  43. package/src/runtime/http.ts +0 -1
  44. package/src/runtime/instrumentation.ts +36 -2
  45. package/src/runtime/resolver-vendors/bindings.ts +31 -0
  46. package/src/runtime/resolver-vendors/browser.ts +420 -0
  47. package/src/runtime/resolver-vendors/types.ts +113 -0
  48. package/src/runtime/resolver.ts +668 -0
  49. package/src/runtime/state.ts +323 -166
  50. package/src/runtime/stealth.ts +3 -6
  51. package/src/server/serve.ts +73 -5
  52. package/src/testing/run.ts +8 -0
  53. package/src/types.ts +133 -7
@@ -0,0 +1,42 @@
1
+ import type { ChallengeSolution, ProviderChallenge, ProviderChallengeKind, ProviderResolverVendor } from "../../types.js";
2
+ import type { TraceRecorder } from "../trace.js";
3
+ export declare const RESOLVER_VENDOR_CAPABILITIES: {
4
+ readonly browser: readonly ["aws_waf", "cloudflare_interstitial"];
5
+ readonly "2captcha": readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf"];
6
+ readonly capsolver: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf"];
7
+ readonly capmonster: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha"];
8
+ readonly custom: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf"];
9
+ };
10
+ export declare function resolverVendorSupports(vendor: ProviderResolverVendor, kind: ProviderChallengeKind): boolean;
11
+ export interface ResolverIdentity {
12
+ readonly proxyUrl: string;
13
+ readonly userAgent: string;
14
+ }
15
+ export interface ResolverIssuingIdentity {
16
+ /** Absent when the adapter genuinely solved without a proxy. */
17
+ readonly proxyUrl?: string;
18
+ readonly userAgent: string;
19
+ }
20
+ export interface ResolverVendorAdapter {
21
+ readonly id: ProviderResolverVendor;
22
+ supports(kind: ProviderChallengeKind): boolean;
23
+ /** Identity the adapter actually used, reported after a successful solve. */
24
+ getIssuingIdentity?(solution: ChallengeSolution, requestedIdentity: ResolverIdentity | undefined, challenge: ProviderChallenge): ResolverIssuingIdentity | undefined;
25
+ solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder): Promise<ChallengeSolution>;
26
+ }
27
+ export type ResolverVendorUnavailableReason = "missing_credentials" | "missing_transport" | "allocation_exhausted" | "transport_failure" | "timeout" | "not_implemented";
28
+ export type ResolverChallengeVerdictReason = "human_puzzle";
29
+ type ResolverErrorOptions = {
30
+ readonly cause?: unknown;
31
+ };
32
+ export declare class ResolverVendorUnavailableError extends Error {
33
+ readonly vendor: ProviderResolverVendor;
34
+ readonly reason: ResolverVendorUnavailableReason;
35
+ constructor(vendor: ProviderResolverVendor, reason: ResolverVendorUnavailableReason, options?: ResolverErrorOptions);
36
+ }
37
+ export declare class ResolverChallengeVerdictError extends Error {
38
+ readonly vendor: ProviderResolverVendor;
39
+ readonly reason: ResolverChallengeVerdictReason;
40
+ constructor(vendor: ProviderResolverVendor, reason: ResolverChallengeVerdictReason, options?: ResolverErrorOptions);
41
+ }
42
+ export {};
@@ -0,0 +1,57 @@
1
+ export const RESOLVER_VENDOR_CAPABILITIES = {
2
+ browser: ["aws_waf", "cloudflare_interstitial"],
3
+ "2captcha": [
4
+ "turnstile",
5
+ "recaptcha_v2",
6
+ "recaptcha_v3",
7
+ "hcaptcha",
8
+ "cloudflare_interstitial",
9
+ "aws_waf",
10
+ ],
11
+ capsolver: [
12
+ "turnstile",
13
+ "recaptcha_v2",
14
+ "recaptcha_v3",
15
+ "hcaptcha",
16
+ "cloudflare_interstitial",
17
+ "aws_waf",
18
+ ],
19
+ capmonster: ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha"],
20
+ custom: [
21
+ "turnstile",
22
+ "recaptcha_v2",
23
+ "recaptcha_v3",
24
+ "hcaptcha",
25
+ "cloudflare_interstitial",
26
+ "aws_waf",
27
+ ],
28
+ };
29
+ export function resolverVendorSupports(vendor, kind) {
30
+ return RESOLVER_VENDOR_CAPABILITIES[vendor].includes(kind);
31
+ }
32
+ export class ResolverVendorUnavailableError extends Error {
33
+ vendor;
34
+ reason;
35
+ constructor(vendor, reason, options = {}) {
36
+ super(`Resolver vendor ${vendor} is unavailable: ${reason}`);
37
+ this.vendor = vendor;
38
+ this.reason = reason;
39
+ this.name = "ResolverVendorUnavailableError";
40
+ if (options.cause !== undefined) {
41
+ this.cause = options.cause;
42
+ }
43
+ }
44
+ }
45
+ export class ResolverChallengeVerdictError extends Error {
46
+ vendor;
47
+ reason;
48
+ constructor(vendor, reason, options = {}) {
49
+ super(`Resolver vendor ${vendor} returned a challenge verdict: ${reason}`);
50
+ this.vendor = vendor;
51
+ this.reason = reason;
52
+ this.name = "ResolverChallengeVerdictError";
53
+ if (options.cause !== undefined) {
54
+ this.cause = options.cause;
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,39 @@
1
+ import type { ChallengeSolution, ProviderCache, ProviderChallenge, ProviderChallengeKind, ProviderResolverConfig, ProviderResolverVendor, ResolverContext } from "../types.js";
2
+ import { type ResolverIdentity, type ResolverVendorAdapter } from "./resolver-vendors/types.js";
3
+ import type { TraceRecorder } from "./trace.js";
4
+ export declare const APIFUSE__RESOLVER__2CAPTCHA__API_KEY = "APIFUSE__RESOLVER__2CAPTCHA__API_KEY";
5
+ export declare const APIFUSE__RESOLVER__CAPSOLVER__API_KEY = "APIFUSE__RESOLVER__CAPSOLVER__API_KEY";
6
+ export declare const APIFUSE__RESOLVER__CAPMONSTER__API_KEY = "APIFUSE__RESOLVER__CAPMONSTER__API_KEY";
7
+ export declare const APIFUSE__RESOLVER__TIMEOUT_MS = "APIFUSE__RESOLVER__TIMEOUT_MS";
8
+ export declare const APIFUSE__CDP_POOL__URL = "APIFUSE__CDP_POOL__URL";
9
+ export declare const DEFAULT_RESOLVER_TIMEOUT_MS = 180000;
10
+ type EnvLike = Record<string, string | undefined>;
11
+ type ResolverChainClient = ResolverContext & {
12
+ solve(challenge: ProviderChallenge, signal?: AbortSignal, traceRecorder?: TraceRecorder): Promise<ChallengeSolution>;
13
+ };
14
+ export interface ResolverRuntimeOptions {
15
+ readonly allowedHosts?: readonly string[];
16
+ readonly cache?: ProviderCache;
17
+ /** Server-owned context/proxy scope used only for identity-bound cache entries. */
18
+ readonly identityScope?: string;
19
+ }
20
+ export declare const RESOLVER_INSTRUMENTATION_METADATA: unique symbol;
21
+ export type ResolverInstrumentationMetadata = {
22
+ readonly target: ResolverContext;
23
+ readonly traceRecorder: TraceRecorder;
24
+ };
25
+ type ResolverAdapterFactory = (configuration: string, timeoutMs: number, allowedHosts: readonly string[]) => ResolverVendorAdapter;
26
+ export declare const RESOLVER_ADAPTER_REGISTRY: Partial<Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>>;
27
+ /** Remove the cached entry for the exact solution object returned by this resolver. */
28
+ export declare function invalidateResolverSolution(resolver: ResolverContext, challenge: ProviderChallenge, solution: ChallengeSolution): Promise<void>;
29
+ export declare function createResolverClient(options: {
30
+ readonly kinds: readonly ProviderChallengeKind[];
31
+ readonly adapters: readonly ResolverVendorAdapter[];
32
+ readonly unavailableReason?: string;
33
+ readonly cache?: ProviderCache;
34
+ readonly identity?: ResolverIdentity;
35
+ }): ResolverChainClient;
36
+ export declare function createUnsupportedResolverClient(reason?: string): ResolverContext;
37
+ export declare function bindResolverSignal(resolver: ResolverContext, defaultSignal: AbortSignal | undefined): ResolverContext;
38
+ export declare function createResolverClientFromEnv(config: ProviderResolverConfig | undefined, env?: EnvLike, options?: ResolverRuntimeOptions): ResolverContext;
39
+ export {};
@@ -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
+ }
@@ -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);