@apifuse/provider-sdk 2.2.0-beta.57 → 2.2.0-beta.59
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 +9 -0
- package/CHANGELOG.md +8 -0
- package/README.md +22 -5
- package/SUBMISSION.md +2 -1
- package/bin/apifuse-pack-types.ts +49 -0
- package/bin/apifuse-record.ts +6 -0
- package/dist/cli/templates/provider/README.md.tpl +19 -5
- package/dist/config/loader.d.ts +7 -0
- package/dist/config/loader.js +30 -6
- package/dist/declaration-validation.d.ts +2 -1
- package/dist/declaration-validation.js +39 -0
- package/dist/define.d.ts +5 -4
- package/dist/define.js +25 -1
- package/dist/engine.d.ts +16 -0
- package/dist/engine.js +58 -1
- package/dist/error-resolution.js +13 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -2
- package/dist/lint.js +3 -3
- package/dist/provider.d.ts +3 -3
- package/dist/provider.js +2 -2
- package/dist/runtime/bounded-expiring-map.d.ts +21 -0
- package/dist/runtime/bounded-expiring-map.js +64 -0
- package/dist/runtime/cache.d.ts +2 -0
- package/dist/runtime/cache.js +8 -1
- package/dist/runtime/egress-lease.d.ts +63 -0
- package/dist/runtime/egress-lease.js +233 -0
- package/dist/runtime/http.js +1 -0
- package/dist/runtime/proxy-retry-policy.d.ts +4 -1
- package/dist/runtime/proxy-retry-policy.js +11 -2
- package/dist/runtime/request-telemetry.d.ts +7 -9
- package/dist/runtime/resolver-config.d.ts +7 -0
- package/dist/runtime/resolver-config.js +8 -0
- package/dist/runtime/resolver-public.d.ts +3 -1
- package/dist/runtime/resolver-public.js +1 -1
- package/dist/runtime/resolver-shared.d.ts +9 -0
- package/dist/runtime/resolver-shared.js +57 -0
- package/dist/runtime/resolver-telemetry.d.ts +134 -0
- package/dist/runtime/resolver-telemetry.js +204 -0
- package/dist/runtime/resolver-usage.d.ts +24 -0
- package/dist/runtime/resolver-usage.js +60 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +5 -0
- package/dist/runtime/resolver-vendors/bindings.js +5 -0
- package/dist/runtime/resolver-vendors/capsolver.d.ts +2 -2
- package/dist/runtime/resolver-vendors/capsolver.js +21 -9
- package/dist/runtime/resolver-vendors/hypersolutions.d.ts +27 -0
- package/dist/runtime/resolver-vendors/hypersolutions.js +369 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -2
- package/dist/runtime/resolver-vendors/twocaptcha.js +26 -10
- package/dist/runtime/resolver-vendors/types.d.ts +35 -5
- package/dist/runtime/resolver-vendors/types.js +20 -10
- package/dist/runtime/resolver.d.ts +5 -1
- package/dist/runtime/resolver.js +397 -109
- package/dist/runtime/stealth-akamai-sbsd.d.ts +54 -0
- package/dist/runtime/stealth-akamai-sbsd.js +142 -0
- package/dist/runtime/stealth.js +547 -53
- package/dist/schema.d.ts +9 -0
- package/dist/schema.js +16 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +1 -0
- package/dist/server/self-test.d.ts +4 -0
- package/dist/server/self-test.js +10 -1
- package/dist/server/serve-implementation.d.ts +7 -1
- package/dist/server/serve-implementation.js +181 -53
- package/dist/server/types.d.ts +6 -0
- package/dist/server/types.js +14 -0
- package/dist/stealth/profiles.d.ts +7 -1
- package/dist/stealth/profiles.js +9 -0
- package/dist/testing/run.js +6 -0
- package/dist/types.d.ts +115 -12
- package/package.json +1 -1
- package/src/cli/templates/provider/README.md.tpl +19 -5
- package/src/config/loader.ts +34 -6
- package/src/declaration-validation.ts +47 -1
- package/src/define.ts +46 -3
- package/src/engine.ts +76 -1
- package/src/error-resolution.ts +13 -0
- package/src/index.ts +32 -1
- package/src/lint.ts +3 -3
- package/src/provider.ts +9 -0
- package/src/runtime/bounded-expiring-map.ts +67 -0
- package/src/runtime/cache.ts +10 -1
- package/src/runtime/egress-lease.ts +336 -0
- package/src/runtime/http.ts +1 -0
- package/src/runtime/proxy-retry-policy.ts +12 -1
- package/src/runtime/request-telemetry.ts +5 -7
- package/src/runtime/resolver-config.ts +12 -0
- package/src/runtime/resolver-public.ts +21 -0
- package/src/runtime/resolver-shared.ts +75 -0
- package/src/runtime/resolver-telemetry.ts +362 -0
- package/src/runtime/resolver-usage.ts +96 -0
- package/src/runtime/resolver-vendors/bindings.ts +5 -0
- package/src/runtime/resolver-vendors/capsolver.ts +22 -3
- package/src/runtime/resolver-vendors/hypersolutions.ts +480 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +26 -12
- package/src/runtime/resolver-vendors/types.ts +60 -12
- package/src/runtime/resolver.ts +465 -124
- package/src/runtime/stealth-akamai-sbsd.ts +207 -0
- package/src/runtime/stealth.ts +727 -95
- package/src/schema.ts +17 -0
- package/src/server/index.ts +21 -0
- package/src/server/self-test.ts +13 -0
- package/src/server/serve-implementation.ts +268 -48
- package/src/server/types.ts +14 -0
- package/src/stealth/profiles.ts +10 -0
- package/src/testing/run.ts +6 -0
- package/src/types.ts +130 -22
package/dist/runtime/resolver.js
CHANGED
|
@@ -2,17 +2,19 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { resolveProxyConfigAsync, } from "../config/loader.js";
|
|
3
3
|
import { ProviderError } from "../errors.js";
|
|
4
4
|
import { getStealthProfile } from "../stealth/profiles.js";
|
|
5
|
-
import {
|
|
5
|
+
import { isProviderCacheBypassed } from "./cache.js";
|
|
6
|
+
import { RESOLVER_CHALLENGE_BINDINGS, resolverChallengeAllowsDirectCache, resolverChallengeIsCacheable, resolverChallengeIsIdentityScoped, resolverChallengeIssuingIdentity, } from "./resolver-vendors/bindings.js";
|
|
6
7
|
import { createBrowserResolverVendorAdapter } from "./resolver-vendors/browser.js";
|
|
7
8
|
import { createCapsolverResolverVendorAdapter } from "./resolver-vendors/capsolver.js";
|
|
9
|
+
import { createHypersolutionsResolverVendorAdapter } from "./resolver-vendors/hypersolutions.js";
|
|
8
10
|
import { assertResolverHostAllowed } from "./resolver-vendors/hosts.js";
|
|
9
11
|
import { createTwoCaptchaResolverVendorAdapter } from "./resolver-vendors/twocaptcha.js";
|
|
10
|
-
import { RESOLVER_VENDOR_CAPABILITIES, ResolverVendorUnavailableError, resolveProviderResolverVendors, resolverVendorSupports, } from "./resolver-vendors/types.js";
|
|
11
|
-
import { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
|
|
12
|
-
import { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, } from "./resolver-config.js";
|
|
12
|
+
import { RESOLVER_VENDOR_CAPABILITIES, ResolverVendorUnavailableError, attachResolverVendorPollObserver, resolveProviderResolverVendors, resolverVendorSupports, } from "./resolver-vendors/types.js";
|
|
13
|
+
import { createUnsupportedResolverClient, inheritResolverTelemetryBinding, registerResolverTelemetryBinding, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
|
|
14
|
+
import { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, kindRequiresClientProfile, } from "./resolver-config.js";
|
|
13
15
|
import { DEFAULT_STEALTH_PROFILE } from "./stealth.js";
|
|
14
16
|
export { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
|
|
15
|
-
export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, } from "./resolver-config.js";
|
|
17
|
+
export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, } from "./resolver-config.js";
|
|
16
18
|
export { DEFAULT_RESOLVER_VENDOR_PREFERENCE, resolveProviderResolverVendors, } from "./resolver-vendors/types.js";
|
|
17
19
|
const RESOLVER_SOLUTION_CACHE_NAMESPACE = "resolver-solution";
|
|
18
20
|
const RESOLVER_SOLUTION_INDEX_CACHE_NAMESPACE = "resolver-solution-index";
|
|
@@ -81,6 +83,13 @@ const resolverAdapterRegistry = {
|
|
|
81
83
|
timeoutMs,
|
|
82
84
|
});
|
|
83
85
|
},
|
|
86
|
+
hypersolutions(configuration, timeoutMs, allowedHosts) {
|
|
87
|
+
return createHypersolutionsResolverVendorAdapter({
|
|
88
|
+
allowedHosts,
|
|
89
|
+
apiKey: configuration,
|
|
90
|
+
timeoutMs,
|
|
91
|
+
});
|
|
92
|
+
},
|
|
84
93
|
};
|
|
85
94
|
export const RESOLVER_ADAPTER_REGISTRY = resolverAdapterRegistry;
|
|
86
95
|
export function swapResolverAdapterFactoryForTests(vendor, factory) {
|
|
@@ -134,6 +143,12 @@ function readPositiveIntegerEnv(env, name) {
|
|
|
134
143
|
return raw;
|
|
135
144
|
}
|
|
136
145
|
function assertDeclaredKind(requestedKind, declaredKinds) {
|
|
146
|
+
if (!Object.hasOwn(RESOLVER_CHALLENGE_BINDINGS, requestedKind)) {
|
|
147
|
+
throw new ProviderError(`Unknown resolver kind "${requestedKind}"`, {
|
|
148
|
+
code: "RESOLVER_KIND_NOT_DECLARED",
|
|
149
|
+
fix: "Use a challenge kind exported by @apifuse/provider-sdk.",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
137
152
|
if (declaredKinds.includes(requestedKind))
|
|
138
153
|
return;
|
|
139
154
|
const declared = declaredKinds.length > 0 ? declaredKinds.join(", ") : "none";
|
|
@@ -179,19 +194,20 @@ function throwUnsupportedKind(kind, usingDefaultVendors) {
|
|
|
179
194
|
: `Add a resolver vendor that supports "${kind}" to the provider's resolver.vendors declaration.`,
|
|
180
195
|
});
|
|
181
196
|
}
|
|
182
|
-
function throwExhausted(attempts) {
|
|
197
|
+
function throwExhausted(challengeKind, attempts, executedAttemptCount = attempts.length) {
|
|
183
198
|
const hasMissingChallengeInput = attempts.some((attempt) => attempt.reason === "missing_challenge_input");
|
|
184
|
-
|
|
185
|
-
.map(({ vendor, reason, missingFields }) => missingFields === undefined || missingFields.length === 0
|
|
186
|
-
? `${vendor}: ${reason}`
|
|
187
|
-
: `${vendor}: ${reason} (missing fields: ${missingFields.join(", ")})`)
|
|
188
|
-
.join(", ");
|
|
189
|
-
throw new ProviderError(`Resolver vendor chain exhausted: ${summary}`, {
|
|
199
|
+
throw new ProviderError("The challenge could not be resolved.", {
|
|
190
200
|
code: "RESOLVER_CHAIN_EXHAUSTED",
|
|
191
201
|
fix: hasMissingChallengeInput
|
|
192
|
-
? "Capture
|
|
193
|
-
: "
|
|
194
|
-
|
|
202
|
+
? "Capture all required challenge fields and retry the request."
|
|
203
|
+
: "Retry the request or contact support if the challenge continues.",
|
|
204
|
+
retryable: false,
|
|
205
|
+
details: {
|
|
206
|
+
challengeKind,
|
|
207
|
+
attempts: executedAttemptCount,
|
|
208
|
+
outcome: "exhausted",
|
|
209
|
+
retryable: false,
|
|
210
|
+
},
|
|
195
211
|
});
|
|
196
212
|
}
|
|
197
213
|
function assertClientProfileTransportContract(clientProfile, transport) {
|
|
@@ -245,6 +261,16 @@ function sanitizeCauseMessage(message) {
|
|
|
245
261
|
}
|
|
246
262
|
function restrictResolverTransport(transport, allowedHosts) {
|
|
247
263
|
return {
|
|
264
|
+
sessionHeaders: transport.sessionHeaders,
|
|
265
|
+
...(transport.getCookie
|
|
266
|
+
? {
|
|
267
|
+
getCookie(name, url) {
|
|
268
|
+
// Jar reads are gated like fetches: an adapter may only read state for declared hosts.
|
|
269
|
+
assertResolverHostAllowed(url, allowedHosts);
|
|
270
|
+
return transport.getCookie?.(name, url);
|
|
271
|
+
},
|
|
272
|
+
}
|
|
273
|
+
: {}),
|
|
248
274
|
async fetch(url, init) {
|
|
249
275
|
// Empty declarations remain deny-by-default, matching the adapter-factory/browser path.
|
|
250
276
|
assertResolverHostAllowed(url, allowedHosts);
|
|
@@ -286,10 +312,17 @@ function safeUpstreamHost(host) {
|
|
|
286
312
|
function safePhase(phase) {
|
|
287
313
|
return phase !== undefined && /^[a-z\d_.:-]{1,64}$/i.test(phase) ? phase : undefined;
|
|
288
314
|
}
|
|
289
|
-
function unavailableAttempt(error) {
|
|
290
|
-
|
|
315
|
+
function unavailableAttempt(error, forTelemetry = false) {
|
|
316
|
+
// Preserve the full input for the collector's redact-then-bound path. Span
|
|
317
|
+
// attributes retain their existing sanitization and phase validation.
|
|
318
|
+
const cause = forTelemetry && error.cause !== undefined
|
|
319
|
+
? {
|
|
320
|
+
name: error.cause instanceof Error ? error.cause.name : "Error",
|
|
321
|
+
message: error.cause instanceof Error ? error.cause.message : String(error.cause),
|
|
322
|
+
}
|
|
323
|
+
: safeCause(error);
|
|
291
324
|
const upstreamHost = safeUpstreamHost(error.upstreamHost);
|
|
292
|
-
const phase = safePhase(error.phase);
|
|
325
|
+
const phase = forTelemetry ? error.phase : safePhase(error.phase);
|
|
293
326
|
const round = Number.isSafeInteger(error.round) && error.round > 0 ? error.round : undefined;
|
|
294
327
|
return {
|
|
295
328
|
vendor: error.vendor,
|
|
@@ -313,6 +346,103 @@ function unavailableSpanAttributes(error) {
|
|
|
313
346
|
transport_round: attempt.round,
|
|
314
347
|
};
|
|
315
348
|
}
|
|
349
|
+
function resolverTelemetryPhase(value) {
|
|
350
|
+
return value === "create_task" ||
|
|
351
|
+
value === "poll_result" ||
|
|
352
|
+
value === "cleanup" ||
|
|
353
|
+
value === "measure_ip" ||
|
|
354
|
+
value === "fetch_script" ||
|
|
355
|
+
value === "generate_payload" ||
|
|
356
|
+
value === "post_payload"
|
|
357
|
+
? value
|
|
358
|
+
: undefined;
|
|
359
|
+
}
|
|
360
|
+
function diagnosticString(value) {
|
|
361
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
362
|
+
}
|
|
363
|
+
function diagnosticCount(value) {
|
|
364
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
365
|
+
? Math.max(0, Math.floor(value))
|
|
366
|
+
: undefined;
|
|
367
|
+
}
|
|
368
|
+
function createResolverTelemetryTraceRecorder(input) {
|
|
369
|
+
if (!input.base && !input.telemetry)
|
|
370
|
+
return undefined;
|
|
371
|
+
const recorder = {
|
|
372
|
+
async runSpan(name, fn, spanOptions = {}) {
|
|
373
|
+
const adapterPhase = name.startsWith("resolver.vendor.")
|
|
374
|
+
? diagnosticString(name.replace("resolver.vendor.", ""))
|
|
375
|
+
: undefined;
|
|
376
|
+
const phase = resolverTelemetryPhase(adapterPhase);
|
|
377
|
+
const startedAt = Date.now();
|
|
378
|
+
let attributes;
|
|
379
|
+
let spanOutcome = "ok";
|
|
380
|
+
const wrappedOptions = {
|
|
381
|
+
...spanOptions,
|
|
382
|
+
onSuccess(value) {
|
|
383
|
+
attributes = spanOptions.onSuccess?.(value);
|
|
384
|
+
return attributes;
|
|
385
|
+
},
|
|
386
|
+
onError(error) {
|
|
387
|
+
attributes = spanOptions.onError?.(error);
|
|
388
|
+
return attributes;
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
try {
|
|
392
|
+
if (input.base)
|
|
393
|
+
return await input.base.runSpan(name, fn, wrappedOptions);
|
|
394
|
+
const value = await fn();
|
|
395
|
+
attributes = spanOptions.onSuccess?.(value);
|
|
396
|
+
return value;
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
spanOutcome = "error";
|
|
400
|
+
if (!input.base)
|
|
401
|
+
attributes = spanOptions.onError?.(error);
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
finally {
|
|
405
|
+
if (adapterPhase && adapterPhase !== "attempt") {
|
|
406
|
+
const reportedPhase = diagnosticString(attributes?.transport_phase) ?? adapterPhase;
|
|
407
|
+
const pollCount = diagnosticCount(attributes?.poll_count);
|
|
408
|
+
const vendorErrorCode = diagnosticString(attributes?.vendor_error_code);
|
|
409
|
+
const vendorErrorDescription = diagnosticString(attributes?.vendor_error_description ?? attributes?.error_message);
|
|
410
|
+
if (phase === "cleanup") {
|
|
411
|
+
input.telemetry?.recordVendorAttempt({
|
|
412
|
+
vendor: input.vendor,
|
|
413
|
+
phase: "cleanup",
|
|
414
|
+
diagnostics: { phase: reportedPhase, attemptIndex: input.attemptIndex },
|
|
415
|
+
outcome: spanOutcome,
|
|
416
|
+
ms: Date.now() - startedAt,
|
|
417
|
+
...(pollCount === undefined ? {} : { pollCount }),
|
|
418
|
+
...(vendorErrorCode ? { vendorErrorCode } : {}),
|
|
419
|
+
...(vendorErrorDescription ? { vendorErrorDescription } : {}),
|
|
420
|
+
...(spanOutcome === "error" ? { errorClass: "unexpected" } : {}),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
input.diagnostics.phase = reportedPhase;
|
|
425
|
+
if (pollCount !== undefined)
|
|
426
|
+
input.diagnostics.pollCount = pollCount;
|
|
427
|
+
if (vendorErrorCode)
|
|
428
|
+
input.diagnostics.vendorErrorCode = vendorErrorCode;
|
|
429
|
+
if (vendorErrorDescription) {
|
|
430
|
+
input.diagnostics.vendorErrorDescription = vendorErrorDescription;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
return attachResolverVendorPollObserver(recorder, () => {
|
|
438
|
+
input.diagnostics.pollCount = (input.diagnostics.pollCount ?? 0) + 1;
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
function resolverAttemptErrorClass(error, signal) {
|
|
442
|
+
if (signal.aborted)
|
|
443
|
+
return "aborted";
|
|
444
|
+
return error instanceof ResolverVendorUnavailableError ? error.reason : "unexpected";
|
|
445
|
+
}
|
|
316
446
|
function challengeOrigin(challenge) {
|
|
317
447
|
return new URL(challenge.pageUrl).origin;
|
|
318
448
|
}
|
|
@@ -448,27 +578,27 @@ async function writeResolverCacheIndex(cache, challenge, entries, now) {
|
|
|
448
578
|
}
|
|
449
579
|
async function cacheResolverSolution(cache, challenge, solution, identity, identityScope) {
|
|
450
580
|
if (!resolverChallengeIsCacheable(challenge))
|
|
451
|
-
return;
|
|
581
|
+
return { written: false, reason: "not_cacheable" };
|
|
452
582
|
const expiresAtMs = solutionExpiryMs(solution);
|
|
453
583
|
const now = Date.now();
|
|
454
584
|
if (expiresAtMs === undefined)
|
|
455
|
-
return;
|
|
585
|
+
return { written: false, reason: "no_expires" };
|
|
456
586
|
const ttlMs = Math.floor(expiresAtMs - now);
|
|
457
587
|
if (ttlMs <= MIN_RESOLVER_CACHE_TTL_MS)
|
|
458
|
-
return;
|
|
588
|
+
return { written: false, reason: "no_expires" };
|
|
459
589
|
const scopedDigest = identityScope !== undefined && resolverChallengeIsIdentityScoped(challenge)
|
|
460
590
|
? resolverIdentityScopeDigest(identityScope)
|
|
461
591
|
: undefined;
|
|
462
592
|
if (!resolverChallengeAllowsDirectCache(challenge) &&
|
|
463
593
|
scopedDigest === undefined &&
|
|
464
594
|
identity.proxyUrl === undefined) {
|
|
465
|
-
return;
|
|
595
|
+
return { written: false, reason: "not_cacheable" };
|
|
466
596
|
}
|
|
467
597
|
const issuerDigest = scopedDigest ?? resolverIdentityDigest(identity);
|
|
468
598
|
await cache.set(resolverSolutionCacheKey(cache, challenge, issuerDigest), { expiresAtMs, issuerDigest, solution }, { ttlMs });
|
|
469
599
|
rememberSolutionIssuer(solution, issuerDigest);
|
|
470
600
|
if (scopedDigest !== undefined || identity.proxyUrl !== undefined)
|
|
471
|
-
return;
|
|
601
|
+
return { written: true };
|
|
472
602
|
const indexKey = resolverSolutionIndexCacheKey(cache, challenge);
|
|
473
603
|
const current = await cache.get(indexKey);
|
|
474
604
|
const currentEntries = isResolverCacheIndex(current?.value) ? current.value.entries : [];
|
|
@@ -476,6 +606,7 @@ async function cacheResolverSolution(cache, challenge, solution, identity, ident
|
|
|
476
606
|
...currentEntries.filter((entry) => entry.issuerDigest !== issuerDigest),
|
|
477
607
|
{ direct: true, expiresAtMs, issuerDigest },
|
|
478
608
|
], now);
|
|
609
|
+
return { written: true };
|
|
479
610
|
}
|
|
480
611
|
async function invalidateResolverSolutionWithOutcome(resolver, challenge, solution) {
|
|
481
612
|
const metadata = resolver[RESOLVER_INSTRUMENTATION_METADATA];
|
|
@@ -566,107 +697,258 @@ async function resolveResolverIdentity(proxyIntent) {
|
|
|
566
697
|
return { unavailableReason: "missing_client_profile", userAgentSource };
|
|
567
698
|
}
|
|
568
699
|
}
|
|
700
|
+
function createChainTelemetryBinding(options) {
|
|
701
|
+
return (telemetry) => createResolverChainClient({ ...options, telemetry });
|
|
702
|
+
}
|
|
569
703
|
function createResolverChainClient(options) {
|
|
570
704
|
assertClientProfileTransportContract(options.clientProfile, options.transport);
|
|
705
|
+
const cache = options.cache && !isProviderCacheBypassed(options.cache) ? options.cache : undefined;
|
|
571
706
|
const client = {
|
|
572
707
|
async solve(challenge, signal = new AbortController().signal, traceRecorder) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
708
|
+
const solveStartedAt = Date.now();
|
|
709
|
+
let terminalOutcome = "error";
|
|
710
|
+
try {
|
|
711
|
+
assertDeclaredKind(challenge.kind, options.kinds);
|
|
712
|
+
if (options.unavailableReason) {
|
|
713
|
+
throw new ProviderError(options.unavailableReason, {
|
|
714
|
+
code: "RESOLVER_UNAVAILABLE",
|
|
715
|
+
fix: "Configure at least one declared resolver vendor or provide a test ResolverContext override.",
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
const supportingEntries = options.entries.filter((entry) => entry.supports(challenge.kind));
|
|
719
|
+
if (supportingEntries.length === 0) {
|
|
720
|
+
throwUnsupportedKind(challenge.kind, options.usingDefaultVendors ?? false);
|
|
721
|
+
}
|
|
722
|
+
if (kindRequiresClientProfile(challenge.kind) && !options.clientProfile?.trim()) {
|
|
723
|
+
options.telemetry?.recordIdentity({ source: "none", failure: "missing_client_profile" });
|
|
724
|
+
throwExhausted(challenge.kind, supportingEntries.map((entry) => ({
|
|
725
|
+
vendor: entry.id,
|
|
726
|
+
reason: "missing_client_profile",
|
|
727
|
+
})), 0);
|
|
728
|
+
}
|
|
729
|
+
signal.throwIfAborted();
|
|
730
|
+
const identityResolution = options.proxyIntent
|
|
731
|
+
? await resolveResolverIdentity(options.proxyIntent)
|
|
732
|
+
: { identity: options.identity };
|
|
733
|
+
const identity = identityResolution.identity;
|
|
734
|
+
options.telemetry?.recordIdentity({
|
|
735
|
+
// For SBSD, "declared" identifies the configured client profile and its bound
|
|
736
|
+
// session identity; the engine independently supplies solver credentials.
|
|
737
|
+
source: kindRequiresClientProfile(challenge.kind) && options.clientProfile
|
|
738
|
+
? "declared"
|
|
739
|
+
: options.proxyIntent
|
|
740
|
+
? (identityResolution.userAgentSource ?? "defaulted")
|
|
741
|
+
: identity
|
|
742
|
+
? "declared"
|
|
743
|
+
: "none",
|
|
744
|
+
...(identityResolution.unavailableReason
|
|
745
|
+
? { failure: identityResolution.unavailableReason }
|
|
746
|
+
: {}),
|
|
578
747
|
});
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
throwExhausted(supportingEntries.map((entry) => ({
|
|
596
|
-
vendor: entry.id,
|
|
597
|
-
reason: identityResolution.unavailableReason ?? "missing_proxy_identity",
|
|
598
|
-
})));
|
|
599
|
-
}
|
|
600
|
-
if (options.cache && resolverChallengeIsCacheable(challenge)) {
|
|
601
|
-
const cached = await findCachedSolution(options.cache, challenge, identity, options.identityScope);
|
|
602
|
-
if (cached)
|
|
603
|
-
return cached;
|
|
604
|
-
}
|
|
605
|
-
const attempts = [];
|
|
606
|
-
for (const entry of supportingEntries) {
|
|
607
|
-
const adapter = entry.createAdapter();
|
|
608
|
-
try {
|
|
609
|
-
const solveAttempt = () => {
|
|
610
|
-
const requiresTransport = adapterRequiresTransport(adapter, challenge.kind);
|
|
611
|
-
const unrestrictedTransport = options.transport ??
|
|
612
|
-
(requiresTransport
|
|
613
|
-
? options.createTransport?.({
|
|
614
|
-
clientProfile: options.clientProfile,
|
|
615
|
-
identityScope: options.identityScope,
|
|
616
|
-
})
|
|
617
|
-
: undefined);
|
|
618
|
-
if (requiresTransport && unrestrictedTransport === undefined) {
|
|
619
|
-
throw new ResolverVendorUnavailableError(adapter.id, "missing_transport");
|
|
748
|
+
signal.throwIfAborted();
|
|
749
|
+
const requiredProxyIdentityMissing = options.proxyIntent?.mode === "required" && identity === undefined;
|
|
750
|
+
if (requiredProxyIdentityMissing) {
|
|
751
|
+
const reason = identityResolution.unavailableReason ?? "missing_proxy_identity";
|
|
752
|
+
for (const [entryIndex, entry] of supportingEntries.entries()) {
|
|
753
|
+
options.telemetry?.recordVendorAttempt({
|
|
754
|
+
vendor: entry.id,
|
|
755
|
+
phase: "create_task",
|
|
756
|
+
diagnostics: { attemptIndex: entryIndex + 1 },
|
|
757
|
+
outcome: "error",
|
|
758
|
+
ms: 0,
|
|
759
|
+
errorClass: reason,
|
|
760
|
+
});
|
|
761
|
+
const next = supportingEntries[entryIndex + 1];
|
|
762
|
+
if (next) {
|
|
763
|
+
options.telemetry?.recordFailover({ from: entry.id, to: next.id, reason });
|
|
620
764
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
};
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
765
|
+
}
|
|
766
|
+
throwExhausted(challenge.kind, supportingEntries.map((entry) => ({
|
|
767
|
+
vendor: entry.id,
|
|
768
|
+
reason,
|
|
769
|
+
})));
|
|
770
|
+
}
|
|
771
|
+
if (!cache) {
|
|
772
|
+
options.telemetry?.recordCacheRead({ status: "disabled", challengeKind: challenge.kind });
|
|
773
|
+
}
|
|
774
|
+
else if (!resolverChallengeIsCacheable(challenge)) {
|
|
775
|
+
options.telemetry?.recordCacheRead({
|
|
776
|
+
status: "not_cacheable",
|
|
777
|
+
challengeKind: challenge.kind,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
const cached = await findCachedSolution(cache, challenge, identity, options.identityScope);
|
|
782
|
+
options.telemetry?.recordCacheRead({
|
|
783
|
+
status: cached ? "hit" : "miss",
|
|
784
|
+
challengeKind: challenge.kind,
|
|
785
|
+
});
|
|
786
|
+
if (cached) {
|
|
787
|
+
terminalOutcome = "cached";
|
|
788
|
+
return cached;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const attempts = [];
|
|
792
|
+
for (const [entryIndex, entry] of supportingEntries.entries()) {
|
|
793
|
+
const adapter = entry.createAdapter();
|
|
794
|
+
const attemptStartedAt = Date.now();
|
|
795
|
+
const diagnostics = {};
|
|
796
|
+
const telemetryTraceRecorder = createResolverTelemetryTraceRecorder({
|
|
797
|
+
base: traceRecorder,
|
|
798
|
+
vendor: adapter.id,
|
|
799
|
+
attemptIndex: entryIndex + 1,
|
|
800
|
+
telemetry: options.telemetry,
|
|
801
|
+
diagnostics,
|
|
802
|
+
});
|
|
803
|
+
let solution;
|
|
804
|
+
try {
|
|
805
|
+
const solveAttempt = () => {
|
|
806
|
+
const requiresTransport = adapterRequiresTransport(adapter, challenge.kind);
|
|
807
|
+
const unrestrictedTransport = options.transport ??
|
|
808
|
+
(requiresTransport
|
|
809
|
+
? options.createTransport?.({
|
|
810
|
+
clientProfile: options.clientProfile,
|
|
811
|
+
identityScope: options.identityScope,
|
|
812
|
+
})
|
|
813
|
+
: undefined);
|
|
814
|
+
if (requiresTransport && unrestrictedTransport === undefined) {
|
|
815
|
+
throw new ResolverVendorUnavailableError(adapter.id, "missing_transport");
|
|
816
|
+
}
|
|
817
|
+
const transport = unrestrictedTransport
|
|
818
|
+
? restrictResolverTransport(unrestrictedTransport, [
|
|
819
|
+
...(options.allowedHosts ?? []),
|
|
820
|
+
...(adapter.transportAllowedHosts ?? []),
|
|
821
|
+
])
|
|
822
|
+
: undefined;
|
|
823
|
+
const usage = {
|
|
824
|
+
// Position in the declared chain, not in the kind-filtered list.
|
|
825
|
+
vendorIndex: options.entries.indexOf(entry) + 1,
|
|
826
|
+
...(options.identityScope
|
|
827
|
+
? {
|
|
828
|
+
resolverIdentityScope: createHash("sha256")
|
|
829
|
+
.update(options.identityScope, "utf8")
|
|
830
|
+
.digest("hex"),
|
|
831
|
+
}
|
|
832
|
+
: {}),
|
|
833
|
+
};
|
|
834
|
+
return adapter.solve(challenge, identity, signal, telemetryTraceRecorder, transport, usage);
|
|
835
|
+
};
|
|
836
|
+
solution = traceRecorder
|
|
837
|
+
? await traceRecorder.runSpan("resolver.vendor.attempt", solveAttempt, {
|
|
838
|
+
attributes: {
|
|
839
|
+
vendor: adapter.id,
|
|
840
|
+
challenge_kind: challenge.kind,
|
|
841
|
+
client_profile: options.clientProfile,
|
|
842
|
+
resolver_identity_source: identityResolution.userAgentSource,
|
|
843
|
+
},
|
|
844
|
+
onError(error) {
|
|
845
|
+
return error instanceof ResolverVendorUnavailableError
|
|
846
|
+
? unavailableSpanAttributes(error)
|
|
847
|
+
: undefined;
|
|
848
|
+
},
|
|
849
|
+
})
|
|
850
|
+
: await solveAttempt();
|
|
851
|
+
}
|
|
852
|
+
catch (error) {
|
|
853
|
+
const unavailable = error instanceof ResolverVendorUnavailableError ? error : undefined;
|
|
854
|
+
const attempt = unavailable ? unavailableAttempt(unavailable, true) : undefined;
|
|
855
|
+
options.telemetry?.recordVendorAttempt({
|
|
856
|
+
vendor: adapter.id,
|
|
857
|
+
phase: resolverTelemetryPhase(unavailable?.phase) ??
|
|
858
|
+
resolverTelemetryPhase(diagnostics.phase) ??
|
|
859
|
+
"create_task",
|
|
860
|
+
diagnostics: {
|
|
861
|
+
...attempt,
|
|
862
|
+
attemptIndex: entryIndex + 1,
|
|
863
|
+
phase: attempt?.phase ?? diagnostics.phase,
|
|
638
864
|
},
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
865
|
+
outcome: "error",
|
|
866
|
+
ms: Date.now() - attemptStartedAt,
|
|
867
|
+
errorClass: resolverAttemptErrorClass(error, signal),
|
|
868
|
+
...(diagnostics.pollCount === undefined ? {} : { pollCount: diagnostics.pollCount }),
|
|
869
|
+
...(diagnostics.vendorErrorCode
|
|
870
|
+
? { vendorErrorCode: diagnostics.vendorErrorCode }
|
|
871
|
+
: {}),
|
|
872
|
+
...(diagnostics.vendorErrorDescription
|
|
873
|
+
? { vendorErrorDescription: diagnostics.vendorErrorDescription }
|
|
874
|
+
: {}),
|
|
875
|
+
});
|
|
876
|
+
signal.throwIfAborted();
|
|
877
|
+
if (!unavailable || !attempt)
|
|
878
|
+
throw error;
|
|
879
|
+
attempts.push(attempt);
|
|
880
|
+
const next = supportingEntries[entryIndex + 1];
|
|
881
|
+
if (next) {
|
|
882
|
+
options.telemetry?.recordFailover({
|
|
883
|
+
from: adapter.id,
|
|
884
|
+
to: next.id,
|
|
885
|
+
reason: unavailable.reason,
|
|
649
886
|
});
|
|
650
|
-
|
|
651
|
-
|
|
887
|
+
}
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
options.telemetry?.recordVendorAttempt({
|
|
891
|
+
vendor: adapter.id,
|
|
892
|
+
phase: resolverTelemetryPhase(diagnostics.phase) ?? "poll_result",
|
|
893
|
+
diagnostics: { attemptIndex: entryIndex + 1, phase: diagnostics.phase },
|
|
894
|
+
outcome: "ok",
|
|
895
|
+
ms: Date.now() - attemptStartedAt,
|
|
896
|
+
...(diagnostics.pollCount === undefined ? {} : { pollCount: diagnostics.pollCount }),
|
|
897
|
+
});
|
|
898
|
+
if (cache) {
|
|
899
|
+
try {
|
|
900
|
+
let cacheWrite;
|
|
901
|
+
if (!resolverChallengeIsCacheable(challenge)) {
|
|
902
|
+
cacheWrite = { written: false, reason: "not_cacheable" };
|
|
903
|
+
}
|
|
904
|
+
else if (solution.form !== "cookies" || solutionExpiryMs(solution) === undefined) {
|
|
905
|
+
cacheWrite = { written: false, reason: "no_expires" };
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
const issuingIdentity = adapter.getIssuingIdentity?.(solution, identity, challenge) ??
|
|
909
|
+
resolverChallengeIssuingIdentity(challenge, {
|
|
910
|
+
...(identity ? { proxyUrl: identity.proxyUrl } : {}),
|
|
911
|
+
userAgent: solution.userAgent,
|
|
912
|
+
});
|
|
913
|
+
cacheWrite = issuingIdentity
|
|
914
|
+
? await cacheResolverSolution(cache, challenge, solution, issuingIdentity, options.identityScope)
|
|
915
|
+
: { written: false, reason: "not_cacheable" };
|
|
916
|
+
}
|
|
917
|
+
options.telemetry?.recordCacheWrite(cacheWrite);
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
options.telemetry?.recordCacheWrite({ written: false, reason: "error" });
|
|
921
|
+
throw error;
|
|
652
922
|
}
|
|
653
923
|
}
|
|
654
924
|
rememberSolutionSource(solution, "vendor");
|
|
925
|
+
// Adapter success includes SBSD payload acceptance with verified:false.
|
|
926
|
+
// The caller reports protected-refetch verification separately.
|
|
927
|
+
terminalOutcome = "solved";
|
|
655
928
|
return solution;
|
|
656
929
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
930
|
+
throwExhausted(challenge.kind, attempts);
|
|
931
|
+
}
|
|
932
|
+
catch (error) {
|
|
933
|
+
terminalOutcome = signal.aborted
|
|
934
|
+
? "aborted"
|
|
935
|
+
: error instanceof ProviderError && error.code === "RESOLVER_CHAIN_EXHAUSTED"
|
|
936
|
+
? "exhausted"
|
|
937
|
+
: "error";
|
|
938
|
+
throw error;
|
|
939
|
+
}
|
|
940
|
+
finally {
|
|
941
|
+
options.telemetry?.recordOutcome({
|
|
942
|
+
outcome: terminalOutcome,
|
|
943
|
+
solveMs: Date.now() - solveStartedAt,
|
|
944
|
+
challengeKind: challenge.kind,
|
|
945
|
+
});
|
|
665
946
|
}
|
|
666
|
-
throwExhausted(attempts);
|
|
667
947
|
},
|
|
668
948
|
};
|
|
669
|
-
resolverCaches.set(client,
|
|
949
|
+
resolverCaches.set(client, cache ?? null);
|
|
950
|
+
const reusableOptions = { ...options, telemetry: undefined };
|
|
951
|
+
registerResolverTelemetryBinding(client, createChainTelemetryBinding(reusableOptions));
|
|
670
952
|
return client;
|
|
671
953
|
}
|
|
672
954
|
export function createResolverClient(options) {
|
|
@@ -685,6 +967,7 @@ export function createResolverClient(options) {
|
|
|
685
967
|
createTransport: options.createTransport,
|
|
686
968
|
clientProfile: options.clientProfile,
|
|
687
969
|
allowedHosts: options.allowedHosts,
|
|
970
|
+
telemetry: options.telemetry,
|
|
688
971
|
});
|
|
689
972
|
}
|
|
690
973
|
function resolveVendorAvailability(vendor, env) {
|
|
@@ -706,7 +989,9 @@ function resolveVendorAvailability(vendor, env) {
|
|
|
706
989
|
? APIFUSE__RESOLVER__2CAPTCHA__API_KEY
|
|
707
990
|
: vendor === "capsolver"
|
|
708
991
|
? APIFUSE__RESOLVER__CAPSOLVER__API_KEY
|
|
709
|
-
:
|
|
992
|
+
: vendor === "capmonster"
|
|
993
|
+
? APIFUSE__RESOLVER__CAPMONSTER__API_KEY
|
|
994
|
+
: APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY;
|
|
710
995
|
const configuration = normalizedEnvValue(env, envKey);
|
|
711
996
|
return configuration
|
|
712
997
|
? { vendor, available: true, configuration }
|
|
@@ -723,6 +1008,7 @@ export function bindResolverSignal(resolver, defaultSignal) {
|
|
|
723
1008
|
if (resolverCaches.has(resolver)) {
|
|
724
1009
|
resolverCaches.set(boundResolver, resolverCaches.get(resolver) ?? null);
|
|
725
1010
|
}
|
|
1011
|
+
inheritResolverTelemetryBinding(resolver, boundResolver);
|
|
726
1012
|
return boundResolver;
|
|
727
1013
|
}
|
|
728
1014
|
function createResolverClientFromEnvInternal(config, env, options, adapterFactories) {
|
|
@@ -736,6 +1022,7 @@ function createResolverClientFromEnvInternal(config, env, options, adapterFactor
|
|
|
736
1022
|
kinds: config.kinds,
|
|
737
1023
|
entries: [],
|
|
738
1024
|
unavailableReason: "Provider resolver vendor chain is empty",
|
|
1025
|
+
telemetry: options.telemetry,
|
|
739
1026
|
});
|
|
740
1027
|
}
|
|
741
1028
|
const timeoutValue = readPositiveIntegerEnv(env, APIFUSE__RESOLVER__TIMEOUT_MS);
|
|
@@ -760,6 +1047,7 @@ function createResolverClientFromEnvInternal(config, env, options, adapterFactor
|
|
|
760
1047
|
createTransport: options.createTransport,
|
|
761
1048
|
clientProfile: config.clientProfile,
|
|
762
1049
|
allowedHosts,
|
|
1050
|
+
telemetry: options.telemetry,
|
|
763
1051
|
});
|
|
764
1052
|
}
|
|
765
1053
|
export function createResolverClientFromEnv(config, env = process.env, options = {}) {
|