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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/bin/apifuse-dev.ts +2 -0
  3. package/bin/apifuse-pack-types.ts +234 -38
  4. package/bin/apifuse-perf.ts +15 -12
  5. package/bin/apifuse-record.ts +2 -0
  6. package/dist/config/loader.d.ts +8 -19
  7. package/dist/config/loader.js +28 -86
  8. package/dist/define.d.ts +4 -1
  9. package/dist/define.js +64 -6
  10. package/dist/index.d.ts +3 -2
  11. package/dist/index.js +1 -0
  12. package/dist/provider.d.ts +1 -1
  13. package/dist/runtime/auth-flow.js +2 -0
  14. package/dist/runtime/browser.js +50 -0
  15. package/dist/runtime/http.js +0 -1
  16. package/dist/runtime/instrumentation.js +26 -1
  17. package/dist/runtime/resolver-vendors/bindings.d.ts +8 -0
  18. package/dist/runtime/resolver-vendors/bindings.js +15 -0
  19. package/dist/runtime/resolver-vendors/browser.d.ts +24 -0
  20. package/dist/runtime/resolver-vendors/browser.js +287 -0
  21. package/dist/runtime/resolver-vendors/types.d.ts +42 -0
  22. package/dist/runtime/resolver-vendors/types.js +57 -0
  23. package/dist/runtime/resolver.d.ts +39 -0
  24. package/dist/runtime/resolver.js +414 -0
  25. package/dist/runtime/state.d.ts +3 -0
  26. package/dist/runtime/state.js +245 -141
  27. package/dist/runtime/stealth.js +3 -6
  28. package/dist/server/serve.d.ts +4 -1
  29. package/dist/server/serve.js +35 -8
  30. package/dist/testing/run.js +7 -0
  31. package/dist/types.d.ts +112 -4
  32. package/package.json +1 -1
  33. package/src/config/loader.ts +35 -111
  34. package/src/define.ts +105 -8
  35. package/src/index.ts +20 -1
  36. package/src/provider.ts +1 -0
  37. package/src/runtime/auth-flow.ts +2 -0
  38. package/src/runtime/browser.ts +69 -0
  39. package/src/runtime/http.ts +0 -1
  40. package/src/runtime/instrumentation.ts +36 -2
  41. package/src/runtime/resolver-vendors/bindings.ts +31 -0
  42. package/src/runtime/resolver-vendors/browser.ts +420 -0
  43. package/src/runtime/resolver-vendors/types.ts +113 -0
  44. package/src/runtime/resolver.ts +668 -0
  45. package/src/runtime/state.ts +323 -166
  46. package/src/runtime/stealth.ts +3 -6
  47. package/src/server/serve.ts +73 -5
  48. package/src/testing/run.ts +8 -0
  49. package/src/types.ts +130 -4
@@ -787,7 +787,6 @@ function createSessionFetcher(
787
787
  const resolvedProxy = await resolveProxyConfigAsync({
788
788
  proxy: options?.proxy ?? clientOptions.proxy,
789
789
  upstream: clientOptions.upstream,
790
- apifuseConfig: clientOptions.apifuseConfig,
791
790
  affinityKey: clientOptions.affinityKey,
792
791
  proxyAttempt: computeProxyAttemptIndex({
793
792
  baseProxyAttempt: clientOptions.proxyAttempt,
@@ -860,11 +859,9 @@ function createSessionFetcher(
860
859
  // A registry vendor chain (smartproxy/nodemaven) is the only policy whose
861
860
  // successive attempts resolve a *different* endpoint, so it is the only one
862
861
  // that may widen the attempt cap to the pool span, de-duplicate endpoints,
863
- // and drive allocator stale-pool refresh. A static custom/decodo policy
864
- // resolves the same URL every attempt: widening/refreshing it would resend
865
- // the request dozens of times (up to maxAttempts × refreshes) and bypass
866
- // retry:false and unsafe-method controls. Static policies therefore follow
867
- // the ordinary transport-retry budget instead.
862
+ // and drive allocator stale-pool refresh. Deprecated custom/decodo policies
863
+ // have no managed endpoint to rotate or refresh, so they follow the ordinary
864
+ // transport-retry budget instead.
868
865
  const rotatesRegistryChain =
869
866
  usesPolicyAllocator && policyResolvesRegistryVendorChain(policyProxy);
870
867
  const maxAttempts = rotatesRegistryChain ? policyProxyAttemptCap : retryAttemptCap;
@@ -55,6 +55,7 @@ import {
55
55
  PROXY_POOL_EXHAUSTED_CODE,
56
56
  } from "../runtime/proxy-errors.js";
57
57
  import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector } from "../runtime/proxy-telemetry.js";
58
+ import { bindResolverSignal, createResolverClientFromEnv } from "../runtime/resolver.js";
58
59
  import {
59
60
  assertRequiredSecretsPresent,
60
61
  listMissingRequiredSecrets,
@@ -100,6 +101,7 @@ import type {
100
101
  ProviderProxyPolicy,
101
102
  ProviderRuntimeState,
102
103
  ProviderStreamEvent,
104
+ ResolverContext,
103
105
  StealthClient,
104
106
  SttContext,
105
107
  } from "../types.js";
@@ -300,6 +302,18 @@ export function resolveProviderProxyAffinityKey(
300
302
  return connectionKey ?? provider.id;
301
303
  }
302
304
 
305
+ export function resolveProviderResolverIdentityScope(
306
+ provider: ProviderDefinition,
307
+ affinityKey: string,
308
+ contextId: string,
309
+ ): string {
310
+ return JSON.stringify({
311
+ proxy: provider.proxy ?? null,
312
+ affinityKey,
313
+ contextId,
314
+ });
315
+ }
316
+
303
317
  function resolveOperationConnectionId(request: OperationRequest): string | undefined {
304
318
  return request.connection?.id ?? request.connectionId;
305
319
  }
@@ -318,6 +332,7 @@ function createProviderContext(
318
332
  options: ProviderServerOptions = {},
319
333
  state: ProviderRuntimeState = createUnsupportedProviderRuntimeState(),
320
334
  proxyTelemetry?: ProxyTelemetryCollector,
335
+ signal?: AbortSignal,
321
336
  ): ProviderContext {
322
337
  const baseUrl = getProviderBaseUrl(provider);
323
338
  const stealthBaseUrl = getProviderStealthBaseUrl(provider);
@@ -327,6 +342,11 @@ function createProviderContext(
327
342
  affinityKey: resolveProviderProxyAffinityKey(provider, request, operationId),
328
343
  telemetry: proxyTelemetry,
329
344
  };
345
+ const resolverIdentityScope = resolveProviderResolverIdentityScope(
346
+ provider,
347
+ proxyClientOptions.affinityKey,
348
+ request.requestId,
349
+ );
330
350
  let wrappedContext: ProviderContext | undefined;
331
351
  const stealthClientOptions = {
332
352
  upstream: proxyClientOptions.upstream,
@@ -348,6 +368,8 @@ function createProviderContext(
348
368
  connectionId: resolveOperationConnectionId(request),
349
369
  headers: request.headers ?? {},
350
370
  };
371
+ const requestState = state.forConnection(requestContext.connectionId);
372
+ const cache = createProviderCache({ providerId: provider.id });
351
373
  const context = wrapWithInstrumentation({
352
374
  env,
353
375
  credential,
@@ -359,8 +381,8 @@ function createProviderContext(
359
381
  retryResponseMeta.set(wrappedContext, summary);
360
382
  },
361
383
  }),
362
- cache: createProviderCache({ providerId: provider.id }),
363
- state,
384
+ cache,
385
+ state: requestState,
364
386
  stealth: stealthBaseUrl
365
387
  ? stealthProfile
366
388
  ? createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
@@ -393,12 +415,21 @@ function createProviderContext(
393
415
  auth: createAuthStub(),
394
416
  ocr: options.ocr ?? createOcrClientFromEnv(provider.ocr),
395
417
  stt: options.stt ?? createSttClientFromEnv(provider.stt),
418
+ resolver: bindResolverSignal(
419
+ options.resolver ??
420
+ createResolverClientFromEnv(provider.resolver, undefined, {
421
+ allowedHosts: provider.allowedHosts,
422
+ cache,
423
+ identityScope: resolverIdentityScope,
424
+ }),
425
+ signal,
426
+ ),
396
427
  choice: createProviderChoiceContext({
397
428
  providerId: provider.id,
398
429
  env,
399
430
  request: requestContext,
400
431
  credential,
401
- state,
432
+ state: requestState,
402
433
  }),
403
434
  });
404
435
  wrappedContext = context;
@@ -467,6 +498,11 @@ function createAuthFlowContext(
467
498
  request.providerId ??
468
499
  provider.id,
469
500
  };
501
+ const resolverIdentityScope = resolveProviderResolverIdentityScope(
502
+ provider,
503
+ proxyClientOptions.affinityKey,
504
+ request.requestId,
505
+ );
470
506
  const stealthClientOptions = {
471
507
  upstream: proxyClientOptions.upstream,
472
508
  affinityKey: proxyClientOptions.affinityKey,
@@ -479,6 +515,7 @@ function createAuthFlowContext(
479
515
  values: request.connection.secrets,
480
516
  })
481
517
  : undefined;
518
+ const cache = createProviderCache({ providerId: provider.id });
482
519
 
483
520
  return {
484
521
  context: {
@@ -517,6 +554,15 @@ function createAuthFlowContext(
517
554
  context: flowContextStore.context,
518
555
  ocr: options.ocr ?? createOcrClientFromEnv(provider.ocr),
519
556
  stt: options.stt ?? createSttClientFromEnv(provider.stt),
557
+ resolver: bindResolverSignal(
558
+ options.resolver ??
559
+ createResolverClientFromEnv(provider.resolver, undefined, {
560
+ allowedHosts: provider.allowedHosts,
561
+ cache,
562
+ identityScope: resolverIdentityScope,
563
+ }),
564
+ signal,
565
+ ),
520
566
  auth: createAuthFlowHelpers({ signal }),
521
567
  },
522
568
  getPatch: flowContextStore.getPatch,
@@ -602,6 +648,8 @@ export type ProviderServerOptions = {
602
648
  stt?: SttContext;
603
649
  /** Optional OCR override for tests or custom hosts; local/prod normally resolves from env. */
604
650
  ocr?: OcrContext;
651
+ /** Optional resolver override for tests or custom hosts; local/prod normally resolves from env. */
652
+ resolver?: ResolverContext;
605
653
  /** Optional runtime state override for tests or custom hosts. Production resolves Redis from env and fails closed when unavailable. */
606
654
  state?: ProviderRuntimeState;
607
655
  /** Allow process-local runtime state only for local development and tests. */
@@ -1517,8 +1565,17 @@ async function handleOperation(
1517
1565
  options: ProviderServerOptions = {},
1518
1566
  state: ProviderRuntimeState = createUnsupportedProviderRuntimeState(),
1519
1567
  proxyTelemetry?: ProxyTelemetryCollector,
1568
+ signal?: AbortSignal,
1520
1569
  ): Promise<Response | OperationResponse> {
1521
- const ctx = createProviderContext(provider, request, operationId, options, state, proxyTelemetry);
1570
+ const ctx = createProviderContext(
1571
+ provider,
1572
+ request,
1573
+ operationId,
1574
+ options,
1575
+ state,
1576
+ proxyTelemetry,
1577
+ signal,
1578
+ );
1522
1579
  const operation = provider.operations[operationId];
1523
1580
  const streaming = operation?.transport?.kind && operation.transport.kind !== "json";
1524
1581
  let cleanupCalled = false;
@@ -1557,6 +1614,7 @@ async function handleOperation(
1557
1614
  operationId,
1558
1615
  ctx,
1559
1616
  request,
1617
+ signal,
1560
1618
  })
1561
1619
  : await executeOperation(provider, operationId, ctx, request.input);
1562
1620
  if (streaming && operation) {
@@ -1943,7 +2001,15 @@ export function createServerApp(
1943
2001
  }
1944
2002
  const request = operationRequestFromForwardingEnvelope(envelope);
1945
2003
  operationId = envelope.operationId;
1946
- const ctx = createProviderContext(provider, request, operationId, options, state);
2004
+ const ctx = createProviderContext(
2005
+ provider,
2006
+ request,
2007
+ operationId,
2008
+ options,
2009
+ state,
2010
+ undefined,
2011
+ signal,
2012
+ );
1947
2013
  if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
1948
2014
  throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
1949
2015
  }
@@ -2011,6 +2077,7 @@ export function createServerApp(
2011
2077
  options,
2012
2078
  state,
2013
2079
  proxyTelemetry,
2080
+ c.req.raw.signal,
2014
2081
  );
2015
2082
  if (response instanceof Response) {
2016
2083
  logProviderSuccess(
@@ -2389,6 +2456,7 @@ export async function serve(
2389
2456
  logger: options.logger,
2390
2457
  ocr: options.ocr,
2391
2458
  stt: options.stt,
2459
+ resolver: options.resolver,
2392
2460
  state: options.state,
2393
2461
  allowMemoryStateFallback: options.allowMemoryStateFallback,
2394
2462
  operationExecutor: options.operationExecutor,
@@ -396,6 +396,8 @@ function createUpstreamContext(
396
396
  },
397
397
  }),
398
398
  close: async () => {},
399
+ cookies: async () =>
400
+ (await browserAction("cookies")).data as Awaited<ReturnType<BrowserPage["cookies"]>>,
399
401
  fill: async (selector, textValue) => {
400
402
  await browserAction("fill", { selector, text: textValue });
401
403
  },
@@ -566,6 +568,9 @@ function createUpstreamContext(
566
568
  stt: createUnsupportedSttClient(
567
569
  "Standard test upstream context does not support ctx.stt.transcribe",
568
570
  ),
571
+ resolver: {
572
+ solve: async () => unsupported("ctx.resolver.solve"),
573
+ },
569
574
  choice: createTestProviderChoiceContext({
570
575
  providerId: `standard-test-${operationName}`,
571
576
  request,
@@ -694,6 +699,9 @@ export function createSnapshotContext(rawFixture: unknown): ProviderContext {
694
699
  stt: createUnsupportedSttClient(
695
700
  "Standard test snapshot context does not support ctx.stt.transcribe",
696
701
  ),
702
+ resolver: {
703
+ solve: async () => unsupported("ctx.resolver.solve"),
704
+ },
697
705
  choice: createTestProviderChoiceContext({
698
706
  providerId: "standard-test",
699
707
  request,
package/src/types.ts CHANGED
@@ -307,6 +307,94 @@ export interface ProviderSttConfig {
307
307
  mode: ProviderSttMode;
308
308
  }
309
309
 
310
+ /**
311
+ * `browser` is the in-house CDP pool (`apps/cdp-pool`, reached through
312
+ * `createBrowserClient`) and is a first-class vendor rather than an escape hatch:
313
+ * for fingerprint-family kinds, it was measured faster than a paid vendor
314
+ * (4.5 s vs 17.5 s) at zero marginal cost.
315
+ *
316
+ * `2captcha` is the vendor already carrying production traffic in
317
+ * `apifuse-provider-tabelog`.
318
+ *
319
+ * Union order is documentation only; the effective fallback order is whatever
320
+ * `ProviderResolverConfig.vendors` declares.
321
+ */
322
+ export type ProviderResolverVendor =
323
+ | "browser"
324
+ | "capsolver"
325
+ | "capmonster"
326
+ | "2captcha"
327
+ | "custom";
328
+
329
+ /**
330
+ * Token-family kinds resolve to `{ form: "token" }`. Cookie-family kinds resolve
331
+ * to `{ form: "cookies" }`; network-identity binding is defined per kind. `aws_waf`
332
+ * was measured portable across residential leases on buyee, while `cf_clearance`
333
+ * remains unmeasured here and is treated as identity-scoped because it is widely
334
+ * described as IP-bound.
335
+ */
336
+ export type ProviderChallenge =
337
+ | {
338
+ readonly kind: "turnstile";
339
+ readonly siteKey: string;
340
+ readonly pageUrl: string;
341
+ readonly action?: string;
342
+ readonly cdata?: string;
343
+ }
344
+ | {
345
+ readonly kind: "recaptcha_v2";
346
+ readonly siteKey: string;
347
+ readonly pageUrl: string;
348
+ }
349
+ | {
350
+ readonly kind: "recaptcha_v3";
351
+ readonly siteKey: string;
352
+ readonly pageUrl: string;
353
+ readonly action: string;
354
+ readonly minScore?: number;
355
+ }
356
+ | {
357
+ readonly kind: "hcaptcha";
358
+ readonly siteKey: string;
359
+ readonly pageUrl: string;
360
+ }
361
+ | {
362
+ readonly kind: "cloudflare_interstitial";
363
+ readonly pageUrl: string;
364
+ readonly blockedHtml?: string;
365
+ }
366
+ | {
367
+ readonly kind: "aws_waf";
368
+ readonly pageUrl: string;
369
+ readonly captchaScript?: string;
370
+ readonly context?: string;
371
+ readonly iv?: string;
372
+ };
373
+
374
+ export type ProviderChallengeKind = ProviderChallenge["kind"];
375
+
376
+ /**
377
+ * Token solutions carry no network-identity binding. Cookie-solution binding is
378
+ * per challenge kind: `aws_waf` was measured portable across residential leases
379
+ * on buyee, while `cf_clearance` is unmeasured here and treated as scoped to the
380
+ * identity that produced it. The provider attaches the returned cookies to its
381
+ * own requests.
382
+ */
383
+ export type ChallengeSolution =
384
+ | { readonly form: "token"; readonly token: string }
385
+ | {
386
+ readonly form: "cookies";
387
+ readonly cookies: Readonly<Record<string, string>>;
388
+ readonly userAgent: string;
389
+ };
390
+
391
+ export interface ProviderResolverConfig {
392
+ /** Ordered vendor fallback chain, tried first to last. */
393
+ readonly vendors: readonly ProviderResolverVendor[];
394
+ /** Challenge kinds this provider is permitted to request. */
395
+ readonly kinds: readonly ProviderChallengeKind[];
396
+ }
397
+
310
398
  export type SttAudioInput = {
311
399
  kind: "base64";
312
400
  data: string;
@@ -385,6 +473,10 @@ export interface SttContext {
385
473
  ): VerificationCodeExtractionResult;
386
474
  }
387
475
 
476
+ export interface ResolverContext {
477
+ solve(challenge: ProviderChallenge, signal?: AbortSignal): Promise<ChallengeSolution>;
478
+ }
479
+
388
480
  export interface HealthJourneySchedule {
389
481
  kind: "interval";
390
482
  /** ISO 8601 duration, for example PT8H. */
@@ -849,10 +941,12 @@ export type ProviderProxyMode = "disabled" | "optional" | "required";
849
941
  * - `decodo` — **decodo.com**, the *gateway* proxy that was named "Smartproxy"
850
942
  * (smartproxy.com) before its 2025 rebrand to Decodo. Sticky sessions via
851
943
  * username params. A different company from `smartproxy` above.
852
- * **@deprecated** — unused; no managed adapter. Use `smartproxy`/`nodemaven`,
853
- * or the `APIFUSE__PROXY__URL` bring-your-own escape hatch.
854
- * - `custom` **@deprecated** bring-your-own static proxy URL marker. The
855
- * `APIFUSE__PROXY__URL` env still works without declaring this value.
944
+ * **@deprecated** — unused; no managed adapter. Declare a
945
+ * `ProviderProxyPolicy` using `smartproxy` or `nodemaven` instead; the
946
+ * `smartproxy` allocator requires `APIFUSE__PROXY__SMARTPROXY_APP_KEY`.
947
+ * - `custom` **@deprecated** static proxy marker with no managed adapter.
948
+ * Use `ProviderProxyPolicy` with `smartproxy`/`nodemaven`; the `smartproxy`
949
+ * allocator requires `APIFUSE__PROXY__SMARTPROXY_APP_KEY`.
856
950
  */
857
951
  export type ProviderProxyProvider = "smartproxy" | "nodemaven" | "decodo" | "custom";
858
952
 
@@ -1618,6 +1712,18 @@ export interface BrowserFrame {
1618
1712
  locator(selector: string): BrowserLocator;
1619
1713
  }
1620
1714
 
1715
+ export interface BrowserCookie {
1716
+ readonly name: string;
1717
+ readonly value: string;
1718
+ readonly domain: string;
1719
+ readonly path: string;
1720
+ /** Unix seconds. Absent for a session cookie. */
1721
+ readonly expires?: number;
1722
+ readonly httpOnly: boolean;
1723
+ readonly secure: boolean;
1724
+ readonly sameSite?: "Strict" | "Lax" | "None";
1725
+ }
1726
+
1621
1727
  export type BrowserResourceMethod = "GET" | "HEAD";
1622
1728
 
1623
1729
  export type BrowserResourceRequest = {
@@ -1659,6 +1765,11 @@ export type BrowserResourcePolicy = {
1659
1765
 
1660
1766
  export interface BrowserPage extends BrowserFrame {
1661
1767
  close(): Promise<void>;
1768
+ /**
1769
+ * Reads the browser context's cookie jar, including httpOnly cookies.
1770
+ * Cookie expiry values are Unix seconds and are absent for session cookies.
1771
+ */
1772
+ cookies(): Promise<readonly BrowserCookie[]>;
1662
1773
  fill(selector: string, text: string): Promise<void>;
1663
1774
  goto(url: string): Promise<void>;
1664
1775
  pageId?: string;
@@ -1946,6 +2057,7 @@ export interface FlowContext {
1946
2057
  context: ContextScratchpad;
1947
2058
  ocr: OcrContext;
1948
2059
  stt: SttContext;
2060
+ resolver: ResolverContext;
1949
2061
  auth: AuthFlowTerminalContext;
1950
2062
  }
1951
2063
 
@@ -1988,7 +2100,14 @@ export type ProviderStateDurationString =
1988
2100
  | `${number}${"ms" | "s" | "m" | "h" | "d"}`
1989
2101
  | `PT${string}`;
1990
2102
 
2103
+ export type StateNamespaceScope = "connection" | "provider";
2104
+
1991
2105
  export interface StateNamespaceOptions {
2106
+ /**
2107
+ * State isolation boundary. Connection scope is the default; provider scope
2108
+ * must be selected explicitly for provider-wide coordination state.
2109
+ */
2110
+ scope?: StateNamespaceScope;
1992
2111
  /** Default TTL used when a write omits ttl. Required to avoid unbounded state. */
1993
2112
  defaultTtl: ProviderStateDurationString;
1994
2113
  /** Maximum allowed TTL; writes are rejected when they exceed this policy. */
@@ -2049,6 +2168,11 @@ export interface ProviderStateNamespace {
2049
2168
  }
2050
2169
 
2051
2170
  export interface ProviderRuntimeState {
2171
+ /**
2172
+ * Returns an immutable view bound to one request connection. An unresolved
2173
+ * connection uses an isolated reserved sentinel, never the provider-global scope.
2174
+ */
2175
+ forConnection(connectionId: string | undefined): ProviderRuntimeState;
2052
2176
  namespace(
2053
2177
  name: string,
2054
2178
  options: StateNamespaceOptions,
@@ -2072,6 +2196,7 @@ export interface ProviderContext {
2072
2196
  auth: AuthContext;
2073
2197
  ocr: OcrContext;
2074
2198
  stt: SttContext;
2199
+ resolver: ResolverContext;
2075
2200
  choice: ProviderChoiceContext;
2076
2201
  }
2077
2202
 
@@ -2246,6 +2371,7 @@ export interface ProviderDefinition {
2246
2371
  proxy?: ProviderProxyConfig;
2247
2372
  ocr?: ProviderOcrConfig;
2248
2373
  stt?: ProviderSttConfig;
2374
+ resolver?: ProviderResolverConfig;
2249
2375
  browser?: {
2250
2376
  engine: BrowserEngine;
2251
2377
  };