@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
@@ -1,5 +1,5 @@
1
1
  import { assertResolverHostAllowed } from "./hosts.js";
2
- import { ResolverVendorUnavailableError, resolverVendorSupports, } from "./types.js";
2
+ import { ResolverChallengeVerdictError, ResolverVendorUnavailableError, resolverVendorSupports, } from "./types.js";
3
3
  const TWOCAPTCHA_VENDOR_ID = "2captcha";
4
4
  const DEFAULT_TWOCAPTCHA_BASE_URL = "https://api.2captcha.com";
5
5
  const DEFAULT_POLL_INTERVAL_MS = 3_000;
@@ -16,7 +16,46 @@ function isJsonRecord(value) {
16
16
  function abortReason(signal) {
17
17
  return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
18
18
  }
19
- function raceWithAbort(operation, signal, phase) {
19
+ function containsSensitiveValue(value, sensitiveValues) {
20
+ const secrets = sensitiveValues.filter((secret) => secret.length > 0);
21
+ if (secrets.length === 0)
22
+ return false;
23
+ const seen = new Set();
24
+ const inspect = (candidate) => {
25
+ if (typeof candidate === "string") {
26
+ return secrets.some((secret) => candidate.includes(secret));
27
+ }
28
+ if (candidate === null ||
29
+ (typeof candidate !== "object" && typeof candidate !== "function")) {
30
+ return false;
31
+ }
32
+ if (seen.has(candidate))
33
+ return false;
34
+ seen.add(candidate);
35
+ try {
36
+ for (const property of Reflect.ownKeys(candidate)) {
37
+ if (typeof property === "string" && inspect(property))
38
+ return true;
39
+ const descriptor = Object.getOwnPropertyDescriptor(candidate, property);
40
+ if (!descriptor)
41
+ return true;
42
+ if ("value" in descriptor && inspect(descriptor.value))
43
+ return true;
44
+ if (descriptor.get !== undefined || descriptor.set !== undefined)
45
+ return true;
46
+ }
47
+ }
48
+ catch {
49
+ return true;
50
+ }
51
+ return false;
52
+ };
53
+ return inspect(value);
54
+ }
55
+ function safeCauseOptions(error, sensitiveValues) {
56
+ return containsSensitiveValue(error, sensitiveValues) ? {} : { cause: error };
57
+ }
58
+ function raceWithAbort(operation, signal, phase, sensitiveValues = []) {
20
59
  if (signal.aborted)
21
60
  return Promise.reject(abortReason(signal));
22
61
  return new Promise((resolve, reject) => {
@@ -36,7 +75,7 @@ function raceWithAbort(operation, signal, phase) {
36
75
  return;
37
76
  }
38
77
  reject(new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
39
- cause: error,
78
+ ...safeCauseOptions(error, sensitiveValues),
40
79
  phase,
41
80
  }));
42
81
  });
@@ -92,17 +131,23 @@ function isAllocationExhausted(payload) {
92
131
  return (code === "error_zero_balance" ||
93
132
  /(?:insufficient|zero|no|not enough)\s+(?:balance|funds|credit)/u.test(`${code} ${description}`));
94
133
  }
134
+ function isNegativeVerdict(payload) {
135
+ return errorText(payload, "errorCode").toLowerCase() === "error_captcha_unsolvable";
136
+ }
95
137
  function unavailableForPayload(payload, phase) {
138
+ if (isNegativeVerdict(payload)) {
139
+ return new ResolverChallengeVerdictError(TWOCAPTCHA_VENDOR_ID, "solve_failed");
140
+ }
96
141
  return new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, isAllocationExhausted(payload) ? "allocation_exhausted" : "transport_failure", { phase });
97
142
  }
98
- async function postJson(fetchImpl, url, body, signal, phase) {
143
+ async function postJson(fetchImpl, url, body, signal, phase, sensitiveValues) {
99
144
  const response = await raceWithAbort(() => fetchImpl(url, {
100
145
  method: "POST",
101
146
  headers: { "content-type": "application/json" },
102
147
  body: JSON.stringify(body),
103
148
  signal,
104
149
  redirect: "error",
105
- }), signal, phase);
150
+ }), signal, phase, sensitiveValues);
106
151
  let responseText;
107
152
  try {
108
153
  responseText = await raceWithAbort(() => response.text(), signal, phase);
@@ -136,10 +181,13 @@ function taskIdFrom(payload) {
136
181
  const taskId = payload.taskId;
137
182
  return typeof taskId === "string" || typeof taskId === "number" ? taskId : undefined;
138
183
  }
139
- function tokenFrom(payload) {
184
+ function tokenFrom(payload, challenge) {
140
185
  const solution = payload.solution;
141
186
  if (!isJsonRecord(solution))
142
187
  return undefined;
188
+ if (challenge.kind === "aws_waf") {
189
+ return typeof solution.existing_token === "string" ? solution.existing_token : undefined;
190
+ }
143
191
  if (typeof solution.gRecaptchaResponse === "string")
144
192
  return solution.gRecaptchaResponse;
145
193
  return typeof solution.token === "string" ? solution.token : undefined;
@@ -187,8 +235,16 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
187
235
  if (!resolverVendorSupports(TWOCAPTCHA_VENDOR_ID, challenge.kind)) {
188
236
  throw new TypeError(`2captcha resolver does not support ${challenge.kind}`);
189
237
  }
190
- if (challenge.kind !== "recaptcha_v2") {
191
- // AWS WAF remains deferred because its challenge variant has no required site key.
238
+ if (challenge.kind !== "recaptcha_v2" && challenge.kind !== "aws_waf") {
239
+ throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
240
+ phase: "create_task",
241
+ });
242
+ }
243
+ if (challenge.kind === "aws_waf" &&
244
+ (!challenge.siteKey?.trim() ||
245
+ !challenge.captchaScript?.trim() ||
246
+ !challenge.context?.trim() ||
247
+ !challenge.iv?.trim())) {
192
248
  throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
193
249
  phase: "create_task",
194
250
  });
@@ -211,17 +267,26 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
211
267
  let phase = "create_task";
212
268
  try {
213
269
  const createTask = async () => {
214
- const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), {
215
- clientKey: apiKey,
216
- task: {
270
+ const task = challenge.kind === "aws_waf"
271
+ ? {
272
+ type: proxy ? "AmazonTask" : "AmazonTaskProxyless",
273
+ websiteURL: challenge.pageUrl,
274
+ websiteKey: challenge.siteKey,
275
+ captchaScript: challenge.captchaScript,
276
+ context: challenge.context,
277
+ iv: challenge.iv,
278
+ ...(identity ? { userAgent: identity.userAgent } : {}),
279
+ ...(proxy ?? {}),
280
+ }
281
+ : {
217
282
  type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
218
283
  websiteURL: challenge.pageUrl,
219
284
  websiteKey: challenge.siteKey,
220
285
  isInvisible: false,
221
286
  ...(identity ? { userAgent: identity.userAgent } : {}),
222
287
  ...(proxy ?? {}),
223
- },
224
- }, solveController.signal, phase);
288
+ };
289
+ const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), { clientKey: apiKey, task }, solveController.signal, phase, [apiKey]);
225
290
  const taskId = taskIdFrom(createResult.payload);
226
291
  if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
227
292
  throw unavailableForPayload(createResult.payload, phase);
@@ -248,7 +313,7 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
248
313
  callerSignal.throwIfAborted();
249
314
  if (now() - startedAt >= timeoutMs)
250
315
  throw new TwoCaptchaSolveTimeoutError();
251
- const pollResult = await postJson(fetchImpl, endpoint(baseUrl, "getTaskResult"), { clientKey: apiKey, taskId }, solveController.signal, phase);
316
+ const pollResult = await postJson(fetchImpl, endpoint(baseUrl, "getTaskResult"), { clientKey: apiKey, taskId }, solveController.signal, phase, [apiKey]);
252
317
  if (!pollResult.ok || pollResult.payload.errorId !== 0) {
253
318
  throw unavailableForPayload(pollResult.payload, phase);
254
319
  }
@@ -257,7 +322,7 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
257
322
  if (pollResult.payload.status !== "ready") {
258
323
  throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", { phase });
259
324
  }
260
- const token = tokenFrom(pollResult.payload);
325
+ const token = tokenFrom(pollResult.payload, challenge);
261
326
  if (!token?.trim()) {
262
327
  throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", { phase });
263
328
  }
@@ -284,10 +349,12 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
284
349
  phase,
285
350
  });
286
351
  }
287
- if (error instanceof ResolverVendorUnavailableError)
352
+ if (error instanceof ResolverVendorUnavailableError ||
353
+ error instanceof ResolverChallengeVerdictError) {
288
354
  throw error;
355
+ }
289
356
  throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
290
- cause: error,
357
+ ...safeCauseOptions(error, [apiKey]),
291
358
  phase,
292
359
  });
293
360
  }
@@ -55,8 +55,8 @@ export interface ResolverVendorAdapter {
55
55
  getIssuingIdentity?(solution: ChallengeSolution, requestedIdentity: ResolverIdentity | undefined, challenge: ProviderChallenge): ResolverIssuingIdentity | undefined;
56
56
  solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder, transport?: ResolverVendorTransport): Promise<ChallengeSolution>;
57
57
  }
58
- export type ResolverVendorUnavailableReason = "missing_credentials" | "missing_proxy_identity" | "missing_transport" | "allocation_exhausted" | "transport_failure" | "timeout" | "not_implemented";
59
- export type ResolverChallengeVerdictReason = "human_puzzle";
58
+ export type ResolverVendorUnavailableReason = "missing_credentials" | "missing_proxy_identity" | "missing_client_profile" | "missing_transport" | "allocation_exhausted" | "transport_failure" | "timeout" | "not_implemented";
59
+ export type ResolverChallengeVerdictReason = "human_puzzle" | "solve_failed";
60
60
  type ResolverErrorOptions = {
61
61
  /** Raw cause; adapters must not place bodies, cookies, headers, credentials, or proxy URLs here. */
62
62
  readonly cause?: unknown;
@@ -56,7 +56,9 @@ export class ResolverChallengeVerdictError extends Error {
56
56
  vendor;
57
57
  reason;
58
58
  constructor(vendor, reason, options = {}) {
59
- super(`Resolver vendor ${vendor} returned a challenge verdict: ${reason}`);
59
+ super(reason === "solve_failed"
60
+ ? `Resolver vendor ${vendor} attempted the challenge but did not solve it`
61
+ : `Resolver vendor ${vendor} returned a challenge verdict: ${reason}`);
60
62
  this.vendor = vendor;
61
63
  this.reason = reason;
62
64
  this.name = "ResolverChallengeVerdictError";
@@ -1,3 +1,4 @@
1
+ import { type ProxyResolutionOptions } from "../config/loader.js";
1
2
  import type { ChallengeSolution, ProviderCache, ProviderChallenge, ProviderChallengeKind, ProviderProxyMode, ProviderResolverConfig, ProviderResolverVendor, ResolverContext } from "../types.js";
2
3
  import { type ResolverIdentity, type ResolverVendorAdapter, type ResolverVendorTransport } from "./resolver-vendors/types.js";
3
4
  import type { TraceRecorder } from "./trace.js";
@@ -10,8 +11,14 @@ type ResolverChainClient = ResolverContext & {
10
11
  export interface ResolverRuntimeOptions {
11
12
  readonly allowedHosts?: readonly string[];
12
13
  readonly cache?: ProviderCache;
13
- /** Provider-declared proxy intent. The SDK never accepts a caller-built identity. */
14
- readonly proxyMode?: ProviderProxyMode;
14
+ /** Inputs for SDK-owned lazy proxy resolution. The SDK never accepts a caller-built identity. */
15
+ readonly proxyIntent?: {
16
+ readonly mode: ProviderProxyMode;
17
+ readonly upstream: NonNullable<ProxyResolutionOptions["upstream"]>;
18
+ readonly affinityKey?: ProxyResolutionOptions["affinityKey"];
19
+ readonly telemetry?: ProxyResolutionOptions["telemetry"];
20
+ readonly userAgent?: string;
21
+ };
15
22
  /** Server-owned context/proxy scope used only for identity-bound cache entries. */
16
23
  readonly identityScope?: string;
17
24
  /** SDK-owned transport already bound to the resolved proxy lease and client profile. */
@@ -30,6 +37,8 @@ export type ResolverInstrumentationMetadata = {
30
37
  export type ResolverAdapterFactory = (configuration: string, timeoutMs: number, allowedHosts: readonly string[]) => ResolverVendorAdapter;
31
38
  export declare const RESOLVER_ADAPTER_REGISTRY: Partial<Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>>;
32
39
  export declare function swapResolverAdapterFactoryForTests(vendor: ProviderResolverVendor, factory: ResolverAdapterFactory | undefined): () => void;
40
+ /** Internal test seam; deliberately not re-exported from the package root. */
41
+ export declare function swapResolverDefaultUserAgentForTests(resolver: (() => string | undefined) | undefined): () => void;
33
42
  /** Remove the cached entry for the exact solution object returned by this resolver. */
34
43
  export declare function invalidateResolverSolution(resolver: ResolverContext, challenge: ProviderChallenge, solution: ChallengeSolution): Promise<void>;
35
44
  export declare function createResolverClient(options: {
@@ -38,7 +47,7 @@ export declare function createResolverClient(options: {
38
47
  readonly unavailableReason?: string;
39
48
  readonly cache?: ProviderCache;
40
49
  readonly identity?: ResolverIdentity;
41
- readonly proxyMode?: ProviderProxyMode;
50
+ readonly proxyIntent?: ResolverRuntimeOptions["proxyIntent"];
42
51
  readonly transport?: ResolverVendorTransport;
43
52
  readonly createTransport?: ResolverRuntimeOptions["createTransport"];
44
53
  readonly clientProfile?: string;
@@ -1,5 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { resolveProxyConfigAsync, } from "../config/loader.js";
2
3
  import { ProviderError } from "../errors.js";
4
+ import { getStealthProfile } from "../stealth/profiles.js";
3
5
  import { resolverChallengeAllowsDirectCache, resolverChallengeIsCacheable, resolverChallengeIsIdentityScoped, resolverChallengeIssuingIdentity, } from "./resolver-vendors/bindings.js";
4
6
  import { createBrowserResolverVendorAdapter } from "./resolver-vendors/browser.js";
5
7
  import { assertResolverHostAllowed } from "./resolver-vendors/hosts.js";
@@ -7,6 +9,7 @@ import { createTwoCaptchaResolverVendorAdapter } from "./resolver-vendors/twocap
7
9
  import { RESOLVER_VENDOR_CAPABILITIES, ResolverVendorUnavailableError, resolverVendorSupports, } from "./resolver-vendors/types.js";
8
10
  import { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
9
11
  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 { DEFAULT_PROFILE } from "./stealth.js";
10
13
  export { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
11
14
  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";
12
15
  const RESOLVER_SOLUTION_CACHE_NAMESPACE = "resolver-solution";
@@ -84,6 +87,20 @@ export function swapResolverAdapterFactoryForTests(vendor, factory) {
84
87
  resolverAdapterRegistry[vendor] = original;
85
88
  };
86
89
  }
90
+ let resolveDefaultResolverUserAgent = () => getStealthProfile(DEFAULT_PROFILE).userAgent;
91
+ /** Internal test seam; deliberately not re-exported from the package root. */
92
+ export function swapResolverDefaultUserAgentForTests(resolver) {
93
+ const original = resolveDefaultResolverUserAgent;
94
+ resolveDefaultResolverUserAgent =
95
+ resolver ?? (() => getStealthProfile(DEFAULT_PROFILE).userAgent);
96
+ let restored = false;
97
+ return () => {
98
+ if (restored)
99
+ return;
100
+ restored = true;
101
+ resolveDefaultResolverUserAgent = original;
102
+ };
103
+ }
87
104
  // This is the sole allowlist for declared vendors whose registry entry may be absent.
88
105
  // Remove a vendor here when its adapter is registered.
89
106
  const KNOWN_UNIMPLEMENTED_RESOLVER_VENDORS = new Set([
@@ -172,6 +189,8 @@ function adapterRequiresTransport(adapter, kind) {
172
189
  function sanitizeDiagnosticUrl(rawUrl) {
173
190
  try {
174
191
  const parsed = new URL(rawUrl);
192
+ if (parsed.username || parsed.password)
193
+ return "[REDACTED_PROXY_URL]";
175
194
  return `${parsed.protocol}//${parsed.host}`;
176
195
  }
177
196
  catch {
@@ -187,7 +206,9 @@ function sanitizeCauseMessage(message) {
187
206
  .split(/\s+/)
188
207
  .filter(Boolean)
189
208
  .map((token) => {
190
- if (token === "[REDACTED]" || /^[a-z][a-z\d+.-]*:\/\/[^\s]+$/i.test(token))
209
+ if (token === "[REDACTED]" ||
210
+ token === "[REDACTED_PROXY_URL]" ||
211
+ /^[a-z][a-z\d+.-]*:\/\/[^\s]+$/i.test(token))
191
212
  return token;
192
213
  const word = token.replace(/^[^a-z\d]+|[^a-z\d]+$/gi, "");
193
214
  return word.length > 0 &&
@@ -449,6 +470,47 @@ export async function invalidateResolverSolution(resolver, challenge, solution)
449
470
  onSuccess: (outcome) => ({ outcome }),
450
471
  });
451
472
  }
473
+ async function resolveResolverIdentity(proxyIntent) {
474
+ const userAgentSource = proxyIntent.userAgent ? "declared" : "defaulted";
475
+ let proxyUrl;
476
+ try {
477
+ const resolved = await resolveProxyConfigAsync({
478
+ upstream: proxyIntent.upstream,
479
+ affinityKey: proxyIntent.affinityKey,
480
+ telemetry: proxyIntent.telemetry
481
+ ? {
482
+ ...proxyIntent.telemetry,
483
+ recordProxyResolution(event) {
484
+ proxyIntent.telemetry?.recordProxyResolution({
485
+ ...event,
486
+ userAgentSource,
487
+ });
488
+ },
489
+ }
490
+ : undefined,
491
+ });
492
+ proxyUrl = resolved.url;
493
+ if (!proxyUrl)
494
+ return { unavailableReason: "missing_proxy_identity", userAgentSource };
495
+ }
496
+ catch {
497
+ // Lease failures contain infrastructure detail that must not cross the resolver
498
+ // boundary. A required policy is classified by the existing fail-closed guard.
499
+ return { unavailableReason: "missing_proxy_identity", userAgentSource };
500
+ }
501
+ try {
502
+ const userAgent = proxyIntent.userAgent || resolveDefaultResolverUserAgent();
503
+ if (!userAgent)
504
+ return { unavailableReason: "missing_client_profile", userAgentSource };
505
+ return {
506
+ identity: { proxyUrl, userAgent },
507
+ userAgentSource,
508
+ };
509
+ }
510
+ catch {
511
+ return { unavailableReason: "missing_client_profile", userAgentSource };
512
+ }
513
+ }
452
514
  function createResolverChainClient(options) {
453
515
  assertClientProfileTransportContract(options.clientProfile, options.transport);
454
516
  const client = {
@@ -464,18 +526,23 @@ function createResolverChainClient(options) {
464
526
  if (supportingEntries.length === 0)
465
527
  throwUnsupportedKind(challenge.kind);
466
528
  signal.throwIfAborted();
467
- // A required proxy policy is checked before the cache. Solutions minted under a
468
- // previous release are shared and long-lived, so consulting the cache first would
469
- // keep reporting success without a proxy identity until every old entry expired.
470
- const requiredProxyIdentityMissing = options.proxyMode === "required" && options.identity === undefined;
529
+ const identityResolution = options.proxyIntent
530
+ ? await resolveResolverIdentity(options.proxyIntent)
531
+ : { identity: options.identity };
532
+ const identity = identityResolution.identity;
533
+ signal.throwIfAborted();
534
+ // Resolve a required proxy lease before consulting the cache. Solutions minted
535
+ // under a previous release are shared and long-lived, but a portable cached token
536
+ // must not bypass the upstream admission policy when no lease can be resolved.
537
+ const requiredProxyIdentityMissing = options.proxyIntent?.mode === "required" && identity === undefined;
471
538
  if (requiredProxyIdentityMissing) {
472
539
  throwExhausted(supportingEntries.map((entry) => ({
473
540
  vendor: entry.id,
474
- reason: "missing_proxy_identity",
541
+ reason: identityResolution.unavailableReason ?? "missing_proxy_identity",
475
542
  })));
476
543
  }
477
544
  if (options.cache && resolverChallengeIsCacheable(challenge)) {
478
- const cached = await findCachedSolution(options.cache, challenge, options.identity, options.identityScope);
545
+ const cached = await findCachedSolution(options.cache, challenge, identity, options.identityScope);
479
546
  if (cached)
480
547
  return cached;
481
548
  }
@@ -498,7 +565,7 @@ function createResolverChainClient(options) {
498
565
  const transport = unrestrictedTransport
499
566
  ? restrictResolverTransport(unrestrictedTransport, options.allowedHosts ?? [])
500
567
  : undefined;
501
- return adapter.solve(challenge, options.identity, signal, traceRecorder, transport);
568
+ return adapter.solve(challenge, identity, signal, traceRecorder, transport);
502
569
  };
503
570
  const solution = traceRecorder
504
571
  ? await traceRecorder.runSpan("resolver.vendor.attempt", solveAttempt, {
@@ -506,6 +573,7 @@ function createResolverChainClient(options) {
506
573
  vendor: adapter.id,
507
574
  challenge_kind: challenge.kind,
508
575
  client_profile: options.clientProfile,
576
+ resolver_identity_source: identityResolution.userAgentSource,
509
577
  },
510
578
  onError(error) {
511
579
  return error instanceof ResolverVendorUnavailableError
@@ -518,9 +586,9 @@ function createResolverChainClient(options) {
518
586
  resolverChallengeIsCacheable(challenge) &&
519
587
  solution.form === "cookies" &&
520
588
  solutionExpiryMs(solution) !== undefined) {
521
- const issuingIdentity = adapter.getIssuingIdentity?.(solution, options.identity, challenge) ??
589
+ const issuingIdentity = adapter.getIssuingIdentity?.(solution, identity, challenge) ??
522
590
  resolverChallengeIssuingIdentity(challenge, {
523
- ...(options.identity ? { proxyUrl: options.identity.proxyUrl } : {}),
591
+ ...(identity ? { proxyUrl: identity.proxyUrl } : {}),
524
592
  userAgent: solution.userAgent,
525
593
  });
526
594
  if (issuingIdentity) {
@@ -553,7 +621,7 @@ export function createResolverClient(options) {
553
621
  unavailableReason: options.unavailableReason,
554
622
  cache: options.cache,
555
623
  identity: options.identity,
556
- proxyMode: options.proxyMode,
624
+ proxyIntent: options.proxyIntent,
557
625
  transport: options.transport,
558
626
  createTransport: options.createTransport,
559
627
  clientProfile: options.clientProfile,
@@ -620,7 +688,7 @@ function createResolverClientFromEnvInternal(config, env, options, adapterFactor
620
688
  };
621
689
  }),
622
690
  cache: options.cache,
623
- proxyMode: options.proxyMode,
691
+ proxyIntent: options.proxyIntent,
624
692
  identityScope: options.identityScope,
625
693
  transport: options.transport,
626
694
  createTransport: options.createTransport,
@@ -1,6 +1,7 @@
1
1
  import type { BrowserProfile, EmulationOS } from "wreq-js";
2
2
  import type { ProxyResolutionOptions } from "../config/loader.js";
3
3
  import type { StealthClient, StealthResponse } from "../types.js";
4
+ export declare const DEFAULT_PROFILE = "chrome-146";
4
5
  export type StealthClientOptions = ProxyResolutionOptions & {
5
6
  warn?: (message: string) => void;
6
7
  /**
@@ -7,7 +7,7 @@ import { createProxyAuthIpDeniedError, createProxyEdgeAuthRejectedError, createP
7
7
  import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, normalizeProxyTransportRetryOptions, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy.js";
8
8
  import { evaluateRedirectHop, isRedirectStatus, nextRedirectMethod, resolveRedirectUrl, } from "./redirects.js";
9
9
  import { isSensitiveKey, normalizeSensitiveParams, redactSensitiveError, redactSensitiveRequestError, redactSensitiveText, redactUrlQueryParams, serializeRequestUrl, } from "./request-options.js";
10
- const DEFAULT_PROFILE = "chrome-146";
10
+ export const DEFAULT_PROFILE = "chrome-146";
11
11
  const MISSING_PROXY_WARNING = "[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
12
12
  const MAX_POLICY_PROXY_POOL_REFRESHES = 1;
13
13
  const PROXY_CONNECT_FAILURE_CODE = "proxy_connect_failed";
@@ -358,6 +358,7 @@ function createProviderContext(provider, request, operationId, options, state =
358
358
  const baseUrl = getProviderBaseUrl(provider);
359
359
  const stealthBaseUrl = getProviderStealthBaseUrl(provider);
360
360
  const stealthProfile = getProviderStealthProfile(provider);
361
+ const proxyPolicy = resolveNativeProxyPolicy(provider);
361
362
  const proxyClientOptions = {
362
363
  upstream: { proxy: provider.proxy },
363
364
  affinityKey: resolveProviderProxyAffinityKey(provider, request, operationId),
@@ -444,7 +445,15 @@ function createProviderContext(provider, request, operationId, options, state =
444
445
  allowedHosts: provider.allowedHosts,
445
446
  cache,
446
447
  identityScope: resolverIdentityScope,
447
- proxyMode: resolveNativeProxyPolicy(provider)?.mode,
448
+ ...(proxyPolicy
449
+ ? {
450
+ proxyIntent: {
451
+ mode: proxyPolicy.mode,
452
+ ...proxyClientOptions,
453
+ ...(stealthProfile ? { userAgent: stealthProfile.userAgent } : {}),
454
+ },
455
+ }
456
+ : {}),
448
457
  }), signal)
449
458
  : bindResolverSignalWithoutRuntime(options.resolver ??
450
459
  createUnsupportedResolverClient("Provider does not declare resolver capability"), signal),
@@ -487,6 +496,7 @@ function createAuthFlowContext(provider, request, options, signal) {
487
496
  const baseUrl = getProviderBaseUrl(provider);
488
497
  const stealthBaseUrl = getProviderStealthBaseUrl(provider);
489
498
  const stealthProfile = getProviderStealthProfile(provider);
499
+ const proxyPolicy = resolveNativeProxyPolicy(provider);
490
500
  const contextData = request.context ?? {};
491
501
  const flowContextStore = createFlowContextStore(provider.context?.keys ?? Object.keys(contextData), contextData);
492
502
  const proxyClientOptions = {
@@ -559,7 +569,15 @@ function createAuthFlowContext(provider, request, options, signal) {
559
569
  allowedHosts: provider.allowedHosts,
560
570
  cache,
561
571
  identityScope: resolverIdentityScope,
562
- proxyMode: resolveNativeProxyPolicy(provider)?.mode,
572
+ ...(proxyPolicy
573
+ ? {
574
+ proxyIntent: {
575
+ mode: proxyPolicy.mode,
576
+ ...proxyClientOptions,
577
+ ...(stealthProfile ? { userAgent: stealthProfile.userAgent } : {}),
578
+ },
579
+ }
580
+ : {}),
563
581
  }), signal)
564
582
  : bindResolverSignalWithoutRuntime(options.resolver ??
565
583
  createUnsupportedResolverClient("Provider does not declare resolver capability"), signal),
@@ -1,2 +1,3 @@
1
1
  export { describeTransform, snapshotTransform, toMatchShape } from "./helpers.js";
2
+ export { resetProviderCacheForTests } from "../runtime/cache.js";
2
3
  export { runStandardTests, type StandardTestsManifest, type StandardTestsOptions, type StandardTestsResult, type StandardTestsUpstreamCall, type StandardTestsUpstreamResponse, type StandardTestsUpstreamStub, } from "./run.js";
@@ -1,2 +1,3 @@
1
1
  export { describeTransform, snapshotTransform, toMatchShape } from "./helpers.js";
2
+ export { resetProviderCacheForTests } from "../runtime/cache.js";
2
3
  export { runStandardTests, } from "./run.js";
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0-beta.28",
2
+ "version": "2.2.0-beta.29",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
@@ -77,9 +77,9 @@
77
77
  "default": "./dist/runtime/prevalidate.js"
78
78
  },
79
79
  "./runtime/resolver": {
80
- "types": "./dist/runtime/resolver.d.ts",
81
- "import": "./dist/runtime/resolver.js",
82
- "default": "./dist/runtime/resolver.js"
80
+ "types": "./dist/runtime/resolver-public.d.ts",
81
+ "import": "./dist/runtime/resolver-public.js",
82
+ "default": "./dist/runtime/resolver-public.js"
83
83
  },
84
84
  "./runtime/stealth": {
85
85
  "types": "./dist/runtime/stealth.d.ts",
package/src/auth.ts CHANGED
@@ -296,6 +296,25 @@ export interface DefineCredentialsAuthOptions<
296
296
  ):
297
297
  | CredentialsAuthLoginResult<TCredentialKeys, keyof TChallenges & string>
298
298
  | Promise<CredentialsAuthLoginResult<TCredentialKeys, keyof TChallenges & string>>;
299
+ /**
300
+ * Optional re-mint of an expired session from the stored credential, wired
301
+ * to `auth.flow.refresh`.
302
+ *
303
+ * Credential-auth upstreams routinely invalidate a session well before the
304
+ * `expiresAt` the provider advertised, which leaves every operation failing
305
+ * with a reauth error until a human repeats the whole interactive login.
306
+ * Implement this to re-establish the session from what is already stored on
307
+ * the connection; the result is resolved exactly like `login`, so it may
308
+ * also raise a challenge when the upstream demands one. Omit it when the
309
+ * upstream has no non-interactive path and re-authentication genuinely
310
+ * requires the user.
311
+ */
312
+ refresh?(
313
+ ctx: FlowContext,
314
+ input: Partial<CredentialsAuthInput<TFields>>,
315
+ ):
316
+ | CredentialsAuthLoginResult<TCredentialKeys, keyof TChallenges & string>
317
+ | Promise<CredentialsAuthLoginResult<TCredentialKeys, keyof TChallenges & string>>;
299
318
  }
300
319
 
301
320
  export interface DefinedCredentialsAuth {
@@ -376,6 +395,27 @@ function normalizeInput<TFields extends CredentialsAuthFields>(
376
395
  return result as CredentialsAuthInput<TFields>;
377
396
  }
378
397
 
398
+ /**
399
+ * Refresh variant of {@link normalizeInput}. `login` coerces every declared
400
+ * field to a string because the interactive turn has already enforced that they
401
+ * are present; refresh runs with no user present, so an absent field is omitted
402
+ * rather than turned into an empty string. That keeps "the user did not supply
403
+ * this" distinguishable from "the user supplied an empty value".
404
+ */
405
+ function normalizePartialInput<TFields extends CredentialsAuthFields>(
406
+ fields: TFields,
407
+ input: Record<string, unknown> | undefined,
408
+ ): Partial<CredentialsAuthInput<TFields>> {
409
+ const result: Record<string, string> = {};
410
+ for (const name of Object.keys(fields)) {
411
+ const value = input?.[name];
412
+ if (typeof value === "string") {
413
+ result[name] = value;
414
+ }
415
+ }
416
+ return result as Partial<CredentialsAuthInput<TFields>>;
417
+ }
418
+
379
419
  function assertCredentialKeys<TCredentialKeys extends readonly string[]>(
380
420
  credentialKeys: TCredentialKeys,
381
421
  credential: Record<string, unknown>,
@@ -716,6 +756,44 @@ export function defineCredentialsAuth<
716
756
  completeTurnId,
717
757
  );
718
758
  },
759
+ // Only advertise refresh when the provider implements it: the
760
+ // protocol treats the hook's presence as "this connection can be
761
+ // re-established without the user", and exposing a stub that
762
+ // cannot actually re-mint would turn a recoverable expiry into a
763
+ // silent failure.
764
+ ...(options.refresh
765
+ ? {
766
+ refresh: async (ctx: FlowContext, rawInput?: Record<string, unknown>) => {
767
+ // A pending challenge belongs to the interactive flow that
768
+ // raised it; finish it there rather than restarting.
769
+ const pending = getPendingChallenge(ctx);
770
+ if (pending) {
771
+ return await continuePendingChallenge(
772
+ ctx,
773
+ options.credentialKeys,
774
+ challenges,
775
+ pending,
776
+ rawInput,
777
+ completeTurnId,
778
+ );
779
+ }
780
+
781
+ // Refresh runs without user input, so fields are optional
782
+ // here — unlike `continue`, missing ones are not a retry.
783
+ const result = await options.refresh!(
784
+ ctx,
785
+ normalizePartialInput(options.fields, rawInput),
786
+ );
787
+ return await resolveAuthResult(
788
+ ctx,
789
+ options.credentialKeys,
790
+ challenges,
791
+ result,
792
+ completeTurnId,
793
+ );
794
+ },
795
+ }
796
+ : {}),
719
797
  },
720
798
  },
721
799
  credential: {
@@ -103,8 +103,11 @@ export type SmartproxyAllocatorBodyClass =
103
103
  | "text_without_proxies"
104
104
  | "usable_proxy_endpoints";
105
105
 
106
+ export type ProxyUserAgentSource = "declared" | "defaulted";
107
+
106
108
  export type ProxyResolutionTelemetryEvent = {
107
109
  provider: ProxyVendorName;
110
+ userAgentSource?: ProxyUserAgentSource;
108
111
  protocol?: ProxyProtocol;
109
112
  cacheStatus: ProxyCacheStatus;
110
113
  cacheHit: boolean;
package/src/index.ts CHANGED
@@ -55,7 +55,6 @@ export {
55
55
  createBypassProviderCache,
56
56
  createProviderCache,
57
57
  type ProviderCacheOptions,
58
- resetProviderCacheForTests,
59
58
  } from "./runtime/cache.js";
60
59
  export {
61
60
  type CreateProviderChoiceContextOptions,