@apifuse/provider-sdk 2.2.0-beta.28 → 2.2.0-beta.29

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 (38) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/bin/apifuse-dev.ts +32 -3
  3. package/bin/apifuse-pack-types.ts +24 -1
  4. package/bin/apifuse-record.ts +39 -6
  5. package/dist/auth.d.ts +14 -0
  6. package/dist/auth.js +38 -0
  7. package/dist/config/loader.d.ts +2 -0
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/runtime/browser.js +45 -2
  11. package/dist/runtime/proxy-telemetry.js +3 -0
  12. package/dist/runtime/resolver-public.d.ts +1 -0
  13. package/dist/runtime/resolver-public.js +1 -0
  14. package/dist/runtime/resolver-vendors/browser.js +14 -4
  15. package/dist/runtime/resolver-vendors/twocaptcha.js +84 -17
  16. package/dist/runtime/resolver-vendors/types.d.ts +2 -2
  17. package/dist/runtime/resolver-vendors/types.js +3 -1
  18. package/dist/runtime/resolver.d.ts +12 -3
  19. package/dist/runtime/resolver.js +80 -12
  20. package/dist/runtime/stealth.d.ts +1 -0
  21. package/dist/runtime/stealth.js +1 -1
  22. package/dist/server/serve-implementation.js +20 -2
  23. package/dist/testing/index.d.ts +1 -0
  24. package/dist/testing/index.js +1 -0
  25. package/package.json +4 -4
  26. package/src/auth.ts +78 -0
  27. package/src/config/loader.ts +3 -0
  28. package/src/index.ts +0 -1
  29. package/src/runtime/browser.ts +50 -2
  30. package/src/runtime/proxy-telemetry.ts +5 -0
  31. package/src/runtime/resolver-public.ts +18 -0
  32. package/src/runtime/resolver-vendors/browser.ts +14 -4
  33. package/src/runtime/resolver-vendors/twocaptcha.ts +102 -19
  34. package/src/runtime/resolver-vendors/types.ts +7 -2
  35. package/src/runtime/resolver.ts +104 -17
  36. package/src/runtime/stealth.ts +1 -1
  37. package/src/server/serve-implementation.ts +20 -2
  38. package/src/testing/index.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.29
4
+
5
+ - Release candidate for main commit b7243459ab12403e11d7b5b93292f09b734b0a96.
6
+
3
7
  ## 2.2.0-beta.28
4
8
 
5
9
  - Release candidate for main commit 2936f1891d4e2326502f0585081f8a58060d6dd7.
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { existsSync } from "node:fs";
4
4
  import { dirname, relative, resolve } from "node:path";
5
- import type { ProviderDefinition } from "../src/index.js";
6
5
  import {
7
6
  createCredentialContext,
8
7
  createEnvContext,
@@ -13,12 +12,16 @@ import {
13
12
  createUnsupportedResolverClient,
14
13
  createSttClientFromEnv,
15
14
  PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
15
+ type ProviderDefinition,
16
16
  ProviderError,
17
+ type ProviderProxyPolicy,
17
18
  } from "../src/index.js";
18
19
  import { createBrowserClient } from "../src/runtime/browser.js";
20
+ import { createResolverClientFromEnv } from "../src/runtime/resolver.js";
19
21
  import { createMemoryProviderRuntimeState } from "../src/runtime/state.js";
20
22
  import { createStealthClient } from "../src/runtime/stealth.js";
21
23
  import { createTraceContext } from "../src/runtime/trace.js";
24
+ import { getStealthProfile } from "../src/stealth/profiles.js";
22
25
  import type { BrowserClient, ProviderContext } from "../src/types.js";
23
26
 
24
27
  const HELP_TEXT = `Usage: apifuse dev [path]
@@ -82,6 +85,11 @@ export function createProviderContext(provider: ProviderDefinition): {
82
85
  ]);
83
86
  const credential = createCredentialContext();
84
87
  const state = createMemoryProviderRuntimeState();
88
+ const cache = createProviderCache({ providerId: provider.id });
89
+ const proxyPolicy = resolveNativeProxyPolicy(provider);
90
+ const stealthProfile = provider.stealth?.profile
91
+ ? getStealthProfile(provider.stealth.profile)
92
+ : undefined;
85
93
  const ctx: ProviderContext = {
86
94
  env,
87
95
  credential,
@@ -94,13 +102,27 @@ export function createProviderContext(provider: ProviderDefinition): {
94
102
  })
95
103
  : createUnsupportedBrowserStub(),
96
104
  http: createHttpClient(),
97
- cache: createProviderCache({ providerId: provider.id }),
105
+ cache,
98
106
  state,
99
107
  trace: createTraceContext(),
100
108
  stealth: createStealthClient("http://localhost"),
101
109
  ocr: createOcrClientFromEnv(provider.ocr),
102
110
  stt: createSttClientFromEnv(provider.stt),
103
- resolver: createUnsupportedResolverClient("Resolver is not available in apifuse dev"),
111
+ resolver: provider.resolver
112
+ ? createResolverClientFromEnv(provider.resolver, undefined, {
113
+ allowedHosts: provider.allowedHosts,
114
+ cache,
115
+ ...(proxyPolicy
116
+ ? {
117
+ proxyIntent: {
118
+ mode: proxyPolicy.mode,
119
+ upstream: { proxy: provider.proxy },
120
+ ...(stealthProfile ? { userAgent: stealthProfile.userAgent } : {}),
121
+ },
122
+ }
123
+ : {}),
124
+ })
125
+ : createUnsupportedResolverClient("Provider does not declare resolver capability"),
104
126
  choice: createProviderChoiceContext({
105
127
  providerId: provider.id,
106
128
  env,
@@ -112,6 +134,13 @@ export function createProviderContext(provider: ProviderDefinition): {
112
134
  return { ctx };
113
135
  }
114
136
 
137
+ function resolveNativeProxyPolicy(provider: ProviderDefinition): ProviderProxyPolicy | undefined {
138
+ if (typeof provider.proxy === "object") return provider.proxy;
139
+ if (provider.proxy === true) return { mode: "optional" };
140
+ if (provider.proxy === false) return { mode: "disabled" };
141
+ return undefined;
142
+ }
143
+
115
144
  function normalizeArgs(argv: string[]): string[] {
116
145
  return argv[0] === "dev" ? argv.slice(1) : argv;
117
146
  }
@@ -242,6 +242,28 @@ const NEGATIVE_CONTROLS = [
242
242
  "",
243
243
  ].join("\n"),
244
244
  },
245
+ {
246
+ filename: "negative-control-resolver-default-user-agent-test-seam.ts",
247
+ expectedCode: "TS2305",
248
+ description: "the resolver subpath does not expose its default user-agent test seam",
249
+ source: [
250
+ 'import { swapResolverDefaultUserAgentForTests } from "@apifuse/provider-sdk/runtime/resolver";',
251
+ "",
252
+ "export const mustNotCompile = swapResolverDefaultUserAgentForTests;",
253
+ "",
254
+ ].join("\n"),
255
+ },
256
+ {
257
+ filename: "negative-control-cache-reset-test-seam.ts",
258
+ expectedCode: "TS2305",
259
+ description: "the package root does not expose its cache-reset test seam",
260
+ source: [
261
+ 'import { resetProviderCacheForTests } from "@apifuse/provider-sdk";',
262
+ "",
263
+ "export const mustNotCompile = resetProviderCacheForTests;",
264
+ "",
265
+ ].join("\n"),
266
+ },
245
267
  {
246
268
  filename: "negative-control-resolver-runtime-allowed-hosts.ts",
247
269
  expectedCode: "TS2322",
@@ -366,7 +388,7 @@ function setUpFixtureConsumer(consumerDir: string, tarballPath: string): void {
366
388
  'import { extractProviderContract } from "@apifuse/provider-sdk/contract";',
367
389
  'import { AUTH_TURN_SCHEMA } from "@apifuse/provider-sdk/auth-turn";',
368
390
  'import { serve } from "@apifuse/provider-sdk/server";',
369
- 'import { runStandardTests } from "@apifuse/provider-sdk/testing";',
391
+ 'import { resetProviderCacheForTests, runStandardTests } from "@apifuse/provider-sdk/testing";',
370
392
  "",
371
393
  "// ProviderError must keep its inherited Error members under nodenext.",
372
394
  "// When dist d.ts imports fail to resolve, the class type degrades and",
@@ -451,6 +473,7 @@ function setUpFixtureConsumer(consumerDir: string, tarballPath: string): void {
451
473
  " extractProviderContract,",
452
474
  " AUTH_TURN_SCHEMA,",
453
475
  " serve,",
476
+ " resetProviderCacheForTests,",
454
477
  " runStandardTests,",
455
478
  "};",
456
479
  "",
@@ -19,6 +19,7 @@ import {
19
19
  type ProviderContext,
20
20
  type ProviderDefinition,
21
21
  ProviderError,
22
+ type ProviderProxyPolicy,
22
23
  type RequestOptions,
23
24
  type StealthClient,
24
25
  TransportError,
@@ -31,13 +32,12 @@ import {
31
32
  sanitizeDiagnosticText,
32
33
  sanitizeFixtureString,
33
34
  } from "../src/fixture-sanitization.js";
34
- import { createMemoryProviderRuntimeState } from "../src/runtime/state.js";
35
- import { createStealthClient } from "../src/runtime/stealth.js";
35
+ import { createResolverClientFromEnv } from "../src/runtime/resolver.js";
36
36
  import {
37
- REDACTED_QUERY_VALUE,
38
37
  isSensitiveKey,
39
38
  normalizeSensitiveParams,
40
39
  parseHttpRequestInvocation,
40
+ REDACTED_QUERY_VALUE,
41
41
  redactSensitiveError,
42
42
  redactSensitiveText,
43
43
  redactUrlQueryParams,
@@ -45,7 +45,10 @@ import {
45
45
  requestOptionsFromHttpInvocation,
46
46
  serializeRequestUrl,
47
47
  } from "../src/runtime/request-options.js";
48
+ import { createMemoryProviderRuntimeState } from "../src/runtime/state.js";
49
+ import { createStealthClient } from "../src/runtime/stealth.js";
48
50
  import { parseSchema } from "../src/schema.js";
51
+ import { getStealthProfile } from "../src/stealth/profiles.js";
49
52
  import {
50
53
  captureStreamEvidence,
51
54
  createStreamCaptureEnvelope,
@@ -422,7 +425,18 @@ function resolveOperationBaseUrl(provider: ProviderRuntime, operationName: strin
422
425
  return baseUrl;
423
426
  }
424
427
 
425
- function createCaptureContext(provider: ProviderRuntime, baseUrl: string, sanitize: boolean) {
428
+ function resolveNativeProxyPolicy(provider: ProviderDefinition): ProviderProxyPolicy | undefined {
429
+ if (typeof provider.proxy === "object") return provider.proxy;
430
+ if (provider.proxy === true) return { mode: "optional" };
431
+ if (provider.proxy === false) return { mode: "disabled" };
432
+ return undefined;
433
+ }
434
+
435
+ export function createCaptureContext(
436
+ provider: ProviderRuntime,
437
+ baseUrl: string,
438
+ sanitize: boolean,
439
+ ) {
426
440
  let nextCaptureOrder = 0;
427
441
  let nextStreamOrdinal = 0;
428
442
  let capturedRaw: JsonValue | undefined;
@@ -506,12 +520,17 @@ function createCaptureContext(provider: ProviderRuntime, baseUrl: string, saniti
506
520
  getScopes: () => [],
507
521
  };
508
522
  const state = createMemoryProviderRuntimeState();
523
+ const cache = createBypassProviderCache({ providerId: provider.id });
524
+ const proxyPolicy = resolveNativeProxyPolicy(provider);
525
+ const stealthProfile = provider.stealth?.profile
526
+ ? getStealthProfile(provider.stealth.profile)
527
+ : undefined;
509
528
  const ctx: ProviderContext = {
510
529
  env,
511
530
  credential,
512
531
  request: { headers: {} },
513
532
  http,
514
- cache: createBypassProviderCache({ providerId: provider.id }),
533
+ cache,
515
534
  state,
516
535
  stealth,
517
536
  browser: {
@@ -540,7 +559,21 @@ function createCaptureContext(provider: ProviderRuntime, baseUrl: string, saniti
540
559
  },
541
560
  ocr: createOcrClientFromEnv(provider.ocr),
542
561
  stt: createSttClientFromEnv(provider.stt),
543
- resolver: createUnsupportedResolverClient("Resolver is not available in apifuse record"),
562
+ resolver: provider.resolver
563
+ ? createResolverClientFromEnv(provider.resolver, undefined, {
564
+ allowedHosts: provider.allowedHosts,
565
+ cache,
566
+ ...(proxyPolicy
567
+ ? {
568
+ proxyIntent: {
569
+ mode: proxyPolicy.mode,
570
+ upstream: { proxy: provider.proxy },
571
+ ...(stealthProfile ? { userAgent: stealthProfile.userAgent } : {}),
572
+ },
573
+ }
574
+ : {}),
575
+ })
576
+ : createUnsupportedResolverClient("Provider does not declare resolver capability"),
544
577
  choice: createProviderChoiceContext({
545
578
  providerId: provider.id,
546
579
  env,
package/dist/auth.d.ts CHANGED
@@ -66,6 +66,20 @@ export interface DefineCredentialsAuthOptions<TFields extends CredentialsAuthFie
66
66
  /** Extra auth-flow context keys used by custom login/challenge code. */
67
67
  contextKeys?: readonly string[];
68
68
  login(ctx: FlowContext, input: CredentialsAuthInput<TFields>): CredentialsAuthLoginResult<TCredentialKeys, keyof TChallenges & string> | Promise<CredentialsAuthLoginResult<TCredentialKeys, keyof TChallenges & string>>;
69
+ /**
70
+ * Optional re-mint of an expired session from the stored credential, wired
71
+ * to `auth.flow.refresh`.
72
+ *
73
+ * Credential-auth upstreams routinely invalidate a session well before the
74
+ * `expiresAt` the provider advertised, which leaves every operation failing
75
+ * with a reauth error until a human repeats the whole interactive login.
76
+ * Implement this to re-establish the session from what is already stored on
77
+ * the connection; the result is resolved exactly like `login`, so it may
78
+ * also raise a challenge when the upstream demands one. Omit it when the
79
+ * upstream has no non-interactive path and re-authentication genuinely
80
+ * requires the user.
81
+ */
82
+ refresh?(ctx: FlowContext, input: Partial<CredentialsAuthInput<TFields>>): CredentialsAuthLoginResult<TCredentialKeys, keyof TChallenges & string> | Promise<CredentialsAuthLoginResult<TCredentialKeys, keyof TChallenges & string>>;
69
83
  }
70
84
  export interface DefinedCredentialsAuth {
71
85
  auth: AuthConfig;
package/dist/auth.js CHANGED
@@ -199,6 +199,23 @@ function normalizeInput(fields, input) {
199
199
  }
200
200
  return result;
201
201
  }
202
+ /**
203
+ * Refresh variant of {@link normalizeInput}. `login` coerces every declared
204
+ * field to a string because the interactive turn has already enforced that they
205
+ * are present; refresh runs with no user present, so an absent field is omitted
206
+ * rather than turned into an empty string. That keeps "the user did not supply
207
+ * this" distinguishable from "the user supplied an empty value".
208
+ */
209
+ function normalizePartialInput(fields, input) {
210
+ const result = {};
211
+ for (const name of Object.keys(fields)) {
212
+ const value = input?.[name];
213
+ if (typeof value === "string") {
214
+ result[name] = value;
215
+ }
216
+ }
217
+ return result;
218
+ }
202
219
  function assertCredentialKeys(credentialKeys, credential) {
203
220
  const missing = credentialKeys.filter((key) => {
204
221
  const value = credential[key];
@@ -413,6 +430,27 @@ export function defineCredentialsAuth(options) {
413
430
  }
414
431
  return await pollPendingChallenge(ctx, options.credentialKeys, challenges, pending, completeTurnId);
415
432
  },
433
+ // Only advertise refresh when the provider implements it: the
434
+ // protocol treats the hook's presence as "this connection can be
435
+ // re-established without the user", and exposing a stub that
436
+ // cannot actually re-mint would turn a recoverable expiry into a
437
+ // silent failure.
438
+ ...(options.refresh
439
+ ? {
440
+ refresh: async (ctx, rawInput) => {
441
+ // A pending challenge belongs to the interactive flow that
442
+ // raised it; finish it there rather than restarting.
443
+ const pending = getPendingChallenge(ctx);
444
+ if (pending) {
445
+ return await continuePendingChallenge(ctx, options.credentialKeys, challenges, pending, rawInput, completeTurnId);
446
+ }
447
+ // Refresh runs without user input, so fields are optional
448
+ // here — unlike `continue`, missing ones are not a retry.
449
+ const result = await options.refresh(ctx, normalizePartialInput(options.fields, rawInput));
450
+ return await resolveAuthResult(ctx, options.credentialKeys, challenges, result, completeTurnId);
451
+ },
452
+ }
453
+ : {}),
416
454
  },
417
455
  },
418
456
  credential: {
@@ -61,8 +61,10 @@ export type ProxyResolutionOptions = {
61
61
  };
62
62
  export type ProxyCacheStatus = "memory_hit" | "redis_hit" | "allocator" | "soft_stale_refresh" | "lock_wait" | "redis_error" | "redis_corrupt" | "disabled";
63
63
  export type SmartproxyAllocatorBodyClass = "network_error" | "http_error" | "empty" | "json_without_proxies" | "text_without_proxies" | "usable_proxy_endpoints";
64
+ export type ProxyUserAgentSource = "declared" | "defaulted";
64
65
  export type ProxyResolutionTelemetryEvent = {
65
66
  provider: ProxyVendorName;
67
+ userAgentSource?: ProxyUserAgentSource;
66
68
  protocol?: ProxyProtocol;
67
69
  cacheStatus: ProxyCacheStatus;
68
70
  cacheHit: boolean;
package/dist/index.d.ts CHANGED
@@ -16,7 +16,7 @@ export * from "./recipes/gov-api.js";
16
16
  export * from "./recipes/rest-api.js";
17
17
  export { createFlowContext, createScratchpad } from "./runtime/auth-flow.js";
18
18
  export type { BrowserClientOptions } from "./runtime/browser.js";
19
- export { APIFUSE__CACHE__KEY_PEPPER_ENV, createBypassProviderCache, createProviderCache, type ProviderCacheOptions, resetProviderCacheForTests, } from "./runtime/cache.js";
19
+ export { APIFUSE__CACHE__KEY_PEPPER_ENV, createBypassProviderCache, createProviderCache, type ProviderCacheOptions, } from "./runtime/cache.js";
20
20
  export { type CreateProviderChoiceContextOptions, createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
21
21
  export { type CreateCredentialContextOptions, createCredentialContext, } from "./runtime/credential.js";
22
22
  export { createEnvContext } from "./runtime/env.js";
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ export { lintOperation, lintProvider, } from "./lint.js";
14
14
  export * from "./recipes/gov-api.js";
15
15
  export * from "./recipes/rest-api.js";
16
16
  export { createFlowContext, createScratchpad } from "./runtime/auth-flow.js";
17
- export { APIFUSE__CACHE__KEY_PEPPER_ENV, createBypassProviderCache, createProviderCache, resetProviderCacheForTests, } from "./runtime/cache.js";
17
+ export { APIFUSE__CACHE__KEY_PEPPER_ENV, createBypassProviderCache, createProviderCache, } from "./runtime/cache.js";
18
18
  export { createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
19
19
  export { createCredentialContext, } from "./runtime/credential.js";
20
20
  export { createEnvContext } from "./runtime/env.js";
@@ -141,7 +141,13 @@ function formatExpression(fn) {
141
141
  }
142
142
  function toLaunchOptions(options) {
143
143
  return {
144
- args: options.extraArgs,
144
+ // `extraArgs` is optional, but playwright-extra's stealth evasions mutate
145
+ // `options.args` unguarded (navigator.webdriver does
146
+ // `options.args.findIndex(...)` in beforeLaunch). Forwarding `undefined`
147
+ // therefore crashes every stealth launch that omits extraArgs with
148
+ // "TypeError: undefined is not an object (evaluating 'options.args.findIndex')".
149
+ // Always hand the launcher a concrete array.
150
+ args: options.extraArgs ?? [],
145
151
  executablePath: options.executablePath,
146
152
  headless: options.headless ?? true,
147
153
  proxy: options.proxy ? { server: options.proxy } : undefined,
@@ -684,6 +690,17 @@ function getCdpExecutionContext(params) {
684
690
  id: typeof contextId === "number" ? contextId : undefined,
685
691
  };
686
692
  }
693
+ function getCdpDestroyedExecutionContextId(params) {
694
+ if (!isRecord(params)) {
695
+ return undefined;
696
+ }
697
+ return typeof params.executionContextId === "number"
698
+ ? params.executionContextId
699
+ : undefined;
700
+ }
701
+ function isMissingExecutionContextError(error) {
702
+ return error instanceof Error && /\b(?:cannot|failed to) find context\b/i.test(error.message);
703
+ }
687
704
  class CdpBrowserLocator {
688
705
  frame;
689
706
  selector;
@@ -809,7 +826,19 @@ class CdpPoolBrowserPage {
809
826
  async evaluateInFrame(frameId, fn) {
810
827
  await this.initialize();
811
828
  const contextId = await this.getFrameExecutionContextId(frameId);
812
- return await this.evaluateWithContext(fn, contextId);
829
+ try {
830
+ return await this.evaluateWithContext(fn, contextId);
831
+ }
832
+ catch (error) {
833
+ if (!isMissingExecutionContextError(error)) {
834
+ throw error;
835
+ }
836
+ if (this.frameExecutionContexts.get(frameId) === contextId) {
837
+ this.frameExecutionContexts.delete(frameId);
838
+ }
839
+ const refreshedContextId = await this.getFrameExecutionContextId(frameId);
840
+ return await this.evaluateWithContext(fn, refreshedContextId);
841
+ }
813
842
  }
814
843
  async waitForSelectorInFrame(frameId, selector, options) {
815
844
  const timeout = options?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
@@ -1010,6 +1039,20 @@ class CdpPoolBrowserPage {
1010
1039
  this.frameExecutionContexts.set(context.frameId, context.id);
1011
1040
  }
1012
1041
  });
1042
+ this.pageClient.on("Runtime.executionContextDestroyed", (params) => {
1043
+ const destroyedContextId = getCdpDestroyedExecutionContextId(params);
1044
+ if (destroyedContextId === undefined) {
1045
+ return;
1046
+ }
1047
+ for (const [frameId, contextId] of this.frameExecutionContexts) {
1048
+ if (contextId === destroyedContextId) {
1049
+ this.frameExecutionContexts.delete(frameId);
1050
+ }
1051
+ }
1052
+ });
1053
+ this.pageClient.on("Runtime.executionContextsCleared", () => {
1054
+ this.frameExecutionContexts.clear();
1055
+ });
1013
1056
  await this.pageClient.send("Page.enable");
1014
1057
  await this.pageClient.send("Runtime.enable");
1015
1058
  this.initialized = true;
@@ -33,6 +33,7 @@ export class ProxyTelemetryCollector {
33
33
  recordProxyResolution(event) {
34
34
  this.#events.push({
35
35
  provider: event.provider,
36
+ ...(event.userAgentSource ? { userAgentSource: event.userAgentSource } : {}),
36
37
  ...(event.protocol ? { protocol: event.protocol } : {}),
37
38
  cacheStatus: event.cacheStatus,
38
39
  cacheHit: event.cacheHit,
@@ -99,6 +100,7 @@ export class ProxyTelemetryCollector {
99
100
  }
100
101
  const aggregate = rest.reduce((acc, event) => ({
101
102
  provider: event.provider,
103
+ userAgentSource: event.userAgentSource ?? acc.userAgentSource,
102
104
  cacheStatus: worseStatus(acc.cacheStatus, event.cacheStatus),
103
105
  cacheHit: acc.cacheHit && event.cacheHit,
104
106
  resolutionMs: acc.resolutionMs + event.resolutionMs,
@@ -118,6 +120,7 @@ export class ProxyTelemetryCollector {
118
120
  v: 1,
119
121
  proxy: {
120
122
  provider: serving.provider,
123
+ ...(aggregate.userAgentSource ? { userAgentSource: aggregate.userAgentSource } : {}),
121
124
  ...(serving.protocol ? { protocol: serving.protocol } : {}),
122
125
  cacheStatus: aggregate.cacheStatus,
123
126
  cacheHit: aggregate.cacheHit,
@@ -0,0 +1 @@
1
+ export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, bindResolverSignal, createResolverClient, createResolverClientFromEnv, createUnsupportedResolverClient, DEFAULT_RESOLVER_TIMEOUT_MS, invalidateResolverSolution, RESOLVER_ADAPTER_REGISTRY, RESOLVER_INSTRUMENTATION_METADATA, type ResolverAdapterFactory, type ResolverInstrumentationMetadata, type ResolverRuntimeOptions, } from "./resolver.js";
@@ -0,0 +1 @@
1
+ export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, bindResolverSignal, createResolverClient, createResolverClientFromEnv, createUnsupportedResolverClient, DEFAULT_RESOLVER_TIMEOUT_MS, invalidateResolverSolution, RESOLVER_ADAPTER_REGISTRY, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver.js";
@@ -204,8 +204,9 @@ export function createBrowserResolverVendorAdapter(options) {
204
204
  });
205
205
  },
206
206
  async solve(challenge, identity, callerSignal, traceRecorder) {
207
- void identity;
208
- if (!options.cdpUrl?.trim()) {
207
+ const cdpUrl = options.cdpUrl?.trim();
208
+ const proxyUrl = identity?.proxyUrl;
209
+ if (!cdpUrl && proxyUrl === undefined) {
209
210
  throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "missing_credentials");
210
211
  }
211
212
  if (!isSupportedKind(challenge.kind)) {
@@ -214,6 +215,14 @@ export function createBrowserResolverVendorAdapter(options) {
214
215
  assertResolverHostAllowed(challenge.pageUrl, options.allowedHosts);
215
216
  const challengeKind = challenge.kind;
216
217
  callerSignal.throwIfAborted();
218
+ if (proxyUrl !== undefined && proxyUrl.length === 0) {
219
+ throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "missing_proxy_identity");
220
+ }
221
+ if (cdpUrl && proxyUrl !== undefined) {
222
+ // The current pool acquire protocol cannot bind a proxy to its browser context.
223
+ // Refuse so the chain can choose a vendor that can honor the resolved identity.
224
+ throw new ResolverVendorUnavailableError(BROWSER_VENDOR_ID, "not_implemented");
225
+ }
217
226
  const solveController = new AbortController();
218
227
  const onCallerAbort = () => solveController.abort(abortReason(callerSignal));
219
228
  callerSignal.addEventListener("abort", onCallerAbort, { once: true });
@@ -223,8 +232,9 @@ export function createBrowserResolverVendorAdapter(options) {
223
232
  try {
224
233
  client = createClient({
225
234
  allowedHosts: [...options.allowedHosts],
226
- cdpUrl: options.cdpUrl.trim(),
227
- requireCdpPool: true,
235
+ cdpUrl: cdpUrl ?? "",
236
+ ...(proxyUrl === undefined ? {} : { proxy: proxyUrl }),
237
+ requireCdpPool: cdpUrl !== undefined,
228
238
  });
229
239
  const contextOperation = client.withIsolatedContext(async (page) => {
230
240
  handlerEntered = true;