@apifuse/provider-sdk 2.2.0-beta.22 → 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 (64) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bin/apifuse-dev.ts +4 -0
  3. package/bin/apifuse-pack-types.ts +234 -38
  4. package/bin/apifuse-perf.ts +15 -12
  5. package/bin/apifuse-record.ts +4 -0
  6. package/dist/config/loader.d.ts +8 -19
  7. package/dist/config/loader.js +28 -86
  8. package/dist/contract-types.d.ts +1 -0
  9. package/dist/contract.js +2 -0
  10. package/dist/define.d.ts +5 -1
  11. package/dist/define.js +79 -6
  12. package/dist/error-resolution.js +5 -0
  13. package/dist/index.d.ts +4 -2
  14. package/dist/index.js +2 -0
  15. package/dist/provider.d.ts +1 -1
  16. package/dist/runtime/auth-flow.d.ts +2 -1
  17. package/dist/runtime/auth-flow.js +4 -0
  18. package/dist/runtime/browser.js +78 -9
  19. package/dist/runtime/http.js +0 -1
  20. package/dist/runtime/instrumentation.js +26 -1
  21. package/dist/runtime/ocr.d.ts +29 -0
  22. package/dist/runtime/ocr.js +440 -0
  23. package/dist/runtime/resolver-vendors/bindings.d.ts +8 -0
  24. package/dist/runtime/resolver-vendors/bindings.js +15 -0
  25. package/dist/runtime/resolver-vendors/browser.d.ts +24 -0
  26. package/dist/runtime/resolver-vendors/browser.js +287 -0
  27. package/dist/runtime/resolver-vendors/types.d.ts +42 -0
  28. package/dist/runtime/resolver-vendors/types.js +57 -0
  29. package/dist/runtime/resolver.d.ts +39 -0
  30. package/dist/runtime/resolver.js +414 -0
  31. package/dist/runtime/state.d.ts +3 -0
  32. package/dist/runtime/state.js +245 -141
  33. package/dist/runtime/stealth.js +3 -6
  34. package/dist/runtime/stt.js +1 -12
  35. package/dist/runtime/timeout.d.ts +5 -0
  36. package/dist/runtime/timeout.js +12 -0
  37. package/dist/server/serve.d.ts +6 -1
  38. package/dist/server/serve.js +39 -8
  39. package/dist/testing/run.js +10 -0
  40. package/dist/types.d.ts +163 -4
  41. package/package.json +1 -1
  42. package/src/config/loader.ts +35 -111
  43. package/src/contract-types.ts +1 -0
  44. package/src/contract.ts +2 -0
  45. package/src/define.ts +121 -7
  46. package/src/error-resolution.ts +5 -0
  47. package/src/index.ts +45 -1
  48. package/src/provider.ts +1 -0
  49. package/src/runtime/auth-flow.ts +6 -0
  50. package/src/runtime/browser.ts +139 -19
  51. package/src/runtime/http.ts +0 -1
  52. package/src/runtime/instrumentation.ts +36 -2
  53. package/src/runtime/ocr.ts +523 -0
  54. package/src/runtime/resolver-vendors/bindings.ts +31 -0
  55. package/src/runtime/resolver-vendors/browser.ts +420 -0
  56. package/src/runtime/resolver-vendors/types.ts +113 -0
  57. package/src/runtime/resolver.ts +668 -0
  58. package/src/runtime/state.ts +323 -166
  59. package/src/runtime/stealth.ts +3 -6
  60. package/src/runtime/stt.ts +1 -19
  61. package/src/runtime/timeout.ts +18 -0
  62. package/src/server/serve.ts +80 -5
  63. package/src/testing/run.ts +15 -0
  64. package/src/types.ts +188 -4
@@ -19,7 +19,7 @@ import {
19
19
 
20
20
  export type { ProxyProtocol } from "../runtime/proxy-nodemaven.js";
21
21
 
22
- /** Proxy vendors the SDK resolves natively (as opposed to the static env path). */
22
+ /** Proxy vendors with SDK-managed resolution. */
23
23
  export type ProxyVendorName = "smartproxy" | "nodemaven";
24
24
 
25
25
  // "smartproxy" here is api.smartproxy.org — a residential proxy with an IP
@@ -37,15 +37,6 @@ export const PROVIDER_CACHE_REDIS_URL_ENV = "APIFUSE__PROVIDER__CACHE_REDIS_URL"
37
37
  export const PROVIDER_STATE_REDIS_URL_ENV = "APIFUSE__PROVIDER__STATE_REDIS_URL";
38
38
  export const REDIS_URL_ENV = "APIFUSE__REDIS__URL";
39
39
 
40
- export type ProxyOptions = {
41
- url: string;
42
- };
43
-
44
- export type ProxyConfig = Partial<ProxyOptions> & {
45
- provider?: string;
46
- apiKey?: string;
47
- };
48
-
49
40
  export type BrowserConfig = {
50
41
  executablePath?: string;
51
42
  headless?: boolean;
@@ -57,7 +48,6 @@ export type SessionConfig = {
57
48
  };
58
49
 
59
50
  export type ApiFuseConfig = {
60
- proxy?: ProxyConfig;
61
51
  browser?: BrowserConfig;
62
52
  session?: SessionConfig;
63
53
  trace?: TraceConfig;
@@ -67,7 +57,6 @@ export type ApiFuseConfig = {
67
57
  export type ProxyResolutionOptions = {
68
58
  proxy?: string;
69
59
  upstream?: { proxy?: boolean | ProviderProxyPolicy };
70
- apifuseConfig?: Pick<ApiFuseConfig, "proxy">;
71
60
  proxyPolicy?: ProviderProxyPolicy;
72
61
  affinityKey?: string;
73
62
  /** Zero-based proxy-pool attempt index used by SDK transports for failover. */
@@ -444,75 +433,7 @@ function serializeSmartproxyPool(pool: CachedProxyPool): string {
444
433
 
445
434
  function normalizeProxyUrl(url?: string): string | undefined {
446
435
  const normalized = url?.trim();
447
- return normalized ? applyStickyProxySession(normalized) : undefined;
448
- }
449
-
450
- function readPositiveIntegerEnv(name: string): string | undefined {
451
- const raw = process.env[name]?.trim();
452
- if (!raw) return undefined;
453
- if (!/^[1-9]\d*$/.test(raw)) {
454
- throw new Error(`${name} must be a positive integer`);
455
- }
456
- return raw;
457
- }
458
-
459
- function applyStickyProxySession(proxyUrl: string): string {
460
- let parsed: URL;
461
- try {
462
- parsed = new URL(proxyUrl);
463
- } catch {
464
- return proxyUrl;
465
- }
466
-
467
- if (!parsed.hostname || !parsed.username || !parsed.password) {
468
- return proxyUrl;
469
- }
470
-
471
- // This rewrites sticky-session usernames for a bring-your-own *gateway* URL
472
- // (APIFUSE__PROXY__URL). The `smartproxy` host here means a smartproxy.com /
473
- // Decodo-family gateway that authenticates by username — NOT the
474
- // api.smartproxy.org allocation vendor, whose endpoints are raw ip:port with
475
- // no credentials and therefore return early above.
476
- const host = parsed.hostname.toLowerCase();
477
- if (!host.includes("smartproxy") && !host.includes("decodo")) {
478
- return proxyUrl;
479
- }
480
-
481
- const username = decodeURIComponent(parsed.username);
482
- const sessionId = process.env.APIFUSE__PROXY__SESSION_ID?.trim() || "apifuse-shared";
483
- const sessionDuration = readPositiveIntegerEnv("APIFUSE__PROXY__SESSION_DURATION");
484
- const stickyUsername = host.includes("smartproxy")
485
- ? buildSmartproxyUsername(username, sessionId, sessionDuration)
486
- : buildDecodoUsername(username, sessionId, sessionDuration ?? "60");
487
-
488
- parsed.username = stickyUsername;
489
- return parsed.toString();
490
- }
491
-
492
- function buildSmartproxyUsername(
493
- username: string,
494
- sessionId: string,
495
- sessionDuration?: string,
496
- ): string {
497
- const parts = username.split("_");
498
- const configuredLife = parts.find((part) => part.startsWith("life-"))?.slice("life-".length);
499
- const baseUsername = parts
500
- .filter((part) => !part.startsWith("session-") && !part.startsWith("life-"))
501
- .join("_");
502
- return `${baseUsername}_session-${sessionId}_life-${sessionDuration ?? configuredLife ?? "60"}`;
503
- }
504
-
505
- function buildDecodoUsername(username: string, sessionId: string, sessionDuration: string): string {
506
- const withoutSticky = username.replace(/-session-.+-sessionduration-\d+$/, "");
507
- const baseUsername = withoutSticky.startsWith("user-") ? withoutSticky : `user-${withoutSticky}`;
508
- return `${baseUsername}-session-${sessionId}-sessionduration-${sessionDuration}`;
509
- }
510
-
511
- function syncProxyEnv(config: ApiFuseConfig): void {
512
- const configProxyUrl = normalizeProxyUrl(config.proxy?.url);
513
- if (!process.env.APIFUSE__PROXY__URL && configProxyUrl) {
514
- process.env.APIFUSE__PROXY__URL = configProxyUrl;
515
- }
436
+ return normalized || undefined;
516
437
  }
517
438
 
518
439
  export function resolveProxyConfig(options: ProxyResolutionOptions = {}): ResolvedProxyConfig {
@@ -532,16 +453,6 @@ export function resolveProxyConfig(options: ProxyResolutionOptions = {}): Resolv
532
453
  return { shouldWarn: false };
533
454
  }
534
455
 
535
- const envProxyUrl = normalizeProxyUrl(process.env.APIFUSE__PROXY__URL);
536
- if (envProxyUrl) {
537
- return { shouldWarn: false, url: envProxyUrl };
538
- }
539
-
540
- const configuredProxyUrl = normalizeProxyUrl(options.apifuseConfig?.proxy?.url);
541
- if (configuredProxyUrl) {
542
- return { shouldWarn: false, url: configuredProxyUrl };
543
- }
544
-
545
456
  return { shouldWarn: true };
546
457
  }
547
458
 
@@ -563,7 +474,22 @@ export async function resolveProxyConfigAsync(
563
474
 
564
475
  const chain = resolveVendorChain(policy);
565
476
  if (chain.length === 0) {
566
- // decodo/custom/env-static providers keep the legacy static-URL path.
477
+ const declared = declaredVendorChain(policy);
478
+ const deprecated = declared.filter((vendor) => vendor === "decodo" || vendor === "custom");
479
+ if (policy.mode === "required") {
480
+ const providerIds =
481
+ declared.length > 0 ? declared.map((vendor) => `"${vendor}"`).join(", ") : "none";
482
+ const deprecatedDetail =
483
+ deprecated.length > 0
484
+ ? ` Deprecated vendor(s): ${deprecated.map((vendor) => `"${vendor}"`).join(", ")}.`
485
+ : "";
486
+ throw new ProxyResolutionError(
487
+ "PROXY_REQUIRED",
488
+ `Required proxy policy has no SDK-managed adapter for provider id(s): ${providerIds}.${deprecatedDetail} Use "smartproxy" or "nodemaven".`,
489
+ );
490
+ }
491
+ // Deprecated decodo/custom providers have no SDK-managed adapter. Optional
492
+ // policies preserve the warning-only behavior and may continue directly.
567
493
  return resolveProxyConfig({
568
494
  ...options,
569
495
  upstream: { proxy: true },
@@ -842,18 +768,22 @@ function isRegistryVendor(name: string | undefined): name is ProxyVendorName {
842
768
  return name === "smartproxy" || name === "nodemaven";
843
769
  }
844
770
 
771
+ function declaredVendorChain(policy: ProviderProxyPolicy): ProviderProxyProvider[] {
772
+ const declared = policy.providers?.length
773
+ ? policy.providers
774
+ : [policy.provider ?? envDefaultProvider()];
775
+ return declared.filter((vendor): vendor is ProviderProxyProvider => vendor !== undefined);
776
+ }
777
+
845
778
  /**
846
779
  * Ordered list of SDK-native proxy vendors declared by the policy. `providers`
847
780
  * takes precedence over the legacy singular `provider`; the platform default
848
781
  * env is the final fallback. Non-registry names (decodo/custom) are dropped so
849
- * an all-static chain falls through to the legacy env-URL path unchanged.
782
+ * an all-deprecated chain has no managed adapter.
850
783
  */
851
784
  export function resolveVendorChain(policy: ProviderProxyPolicy): ProxyVendorName[] {
852
- const declared: (ProviderProxyProvider | undefined)[] = policy.providers?.length
853
- ? policy.providers
854
- : [policy.provider ?? envDefaultProvider()];
855
785
  const chain: ProxyVendorName[] = [];
856
- for (const name of declared) {
786
+ for (const name of declaredVendorChain(policy)) {
857
787
  if (isRegistryVendor(name) && !chain.includes(name)) {
858
788
  chain.push(name);
859
789
  }
@@ -924,9 +854,8 @@ const UNSAFE_TRANSPORT_RETRY_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"
924
854
  * - the method is safe/idempotent — an unsafe request must never be duplicated
925
855
  * across the pool even if some framework default would allow it;
926
856
  * - the policy resolves a non-empty *registry* vendor chain (smartproxy /
927
- * nodemaven). Static vendors (custom / decodo) and credential-less policies
928
- * resolve no allocator pool, so every attempt would hit the same endpoint
929
- * with no possible crossover — they keep the retry budget.
857
+ * nodemaven). Deprecated vendors (custom / decodo) resolve no managed pool,
858
+ * so there is no possible endpoint crossover they keep the retry budget.
930
859
  *
931
860
  * The widened cap is bounded by the chain's true maximum span (sum of each
932
861
  * vendor's max pool size), so a large NodeMaven pool (≤50) stays reachable and
@@ -945,8 +874,8 @@ const UNSAFE_TRANSPORT_RETRY_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"
945
874
  * endpoint each attempt resolves (even a repeated one), so no de-duplication;
946
875
  * - the method is safe/idempotent — an unsafe request is never duplicated;
947
876
  * - the policy resolves a non-empty registry vendor chain (smartproxy /
948
- * nodemaven). Static vendors (custom / decodo) resolve the same URL every
949
- * attempt, so there is nothing to rotate or de-duplicate.
877
+ * nodemaven). Deprecated vendors (custom / decodo) resolve no managed
878
+ * endpoint, so there is nothing to rotate or de-duplicate.
950
879
  */
951
880
  export function policyRotatesTransportVendorChain(input: {
952
881
  policy: ProviderProxyPolicy | undefined;
@@ -989,9 +918,8 @@ export function resolvePolicyTransportAttemptCap(input: {
989
918
  * A registry vendor chain (smartproxy/nodemaven) resolves a potentially
990
919
  * *different* endpoint per flat attempt index, so a transport retry should
991
920
  * advance across endpoints and de-duplicate once the chain stops yielding new
992
- * ones. Static/custom/decodo policies (empty registry chain) resolve the *same*
993
- * URL every attempt by design retrying that same endpoint is intended, so the
994
- * transport loop must not de-duplicate them.
921
+ * ones. Deprecated custom/decodo policies have an empty registry chain and no
922
+ * managed endpoint, so the transport loop has nothing to rotate or de-duplicate.
995
923
  */
996
924
  export function policyResolvesRegistryVendorChain(
997
925
  policy: ProviderProxyPolicy | undefined,
@@ -1828,17 +1756,13 @@ export async function loadApiFuseConfig(dir: string = process.cwd()): Promise<Ap
1828
1756
  const tsPath = path.resolve(dir, "apifuse.config.ts");
1829
1757
  if (existsSync(tsPath)) {
1830
1758
  const config = await importConfig(tsPath);
1831
- const resolvedConfig = config ?? {};
1832
- syncProxyEnv(resolvedConfig);
1833
- return resolvedConfig;
1759
+ return config ?? {};
1834
1760
  }
1835
1761
 
1836
1762
  const jsPath = path.resolve(dir, "apifuse.config.js");
1837
1763
  if (existsSync(jsPath)) {
1838
1764
  const config = await importConfig(jsPath);
1839
- const resolvedConfig = config ?? {};
1840
- syncProxyEnv(resolvedConfig);
1841
- return resolvedConfig;
1765
+ return config ?? {};
1842
1766
  }
1843
1767
 
1844
1768
  return {};
@@ -13,6 +13,7 @@ export interface ProviderContractSnapshot {
13
13
  readonly allowedHosts?: readonly string[];
14
14
  readonly stealth?: JsonValue;
15
15
  readonly proxy?: JsonValue;
16
+ readonly ocr?: JsonValue;
16
17
  readonly stt?: JsonValue;
17
18
  readonly browser?: JsonValue;
18
19
  readonly auth?: JsonValue;
package/src/contract.ts CHANGED
@@ -35,6 +35,7 @@ export function extractProviderContract(provider: ProviderDefinition): ProviderC
35
35
  const auth = extractAuth(provider.auth);
36
36
  const stealth = toJsonValue(provider.stealth);
37
37
  const proxy = toJsonValue(provider.proxy);
38
+ const ocr = toJsonValue(provider.ocr);
38
39
  const stt = toJsonValue(provider.stt);
39
40
  const browser = toJsonValue(provider.browser);
40
41
  const reviewed = toJsonValue(provider.reviewed);
@@ -59,6 +60,7 @@ export function extractProviderContract(provider: ProviderDefinition): ProviderC
59
60
  ...(provider.allowedHosts ? { allowedHosts: [...provider.allowedHosts].sort() } : {}),
60
61
  ...(stealth === undefined ? {} : { stealth }),
61
62
  ...(proxy === undefined ? {} : { proxy }),
63
+ ...(ocr === undefined ? {} : { ocr }),
62
64
  ...(stt === undefined ? {} : { stt }),
63
65
  ...(browser === undefined ? {} : { browser }),
64
66
  ...(auth === undefined ? {} : { auth }),
package/src/define.ts CHANGED
@@ -20,20 +20,24 @@ import type {
20
20
  HealthJourneySchedule,
21
21
  HealthScheduleRandomization,
22
22
  InferSchemaOutput,
23
+ NativeProviderConfig,
23
24
  OperationDefinition,
24
25
  OperationHandlerResult,
25
26
  OperationHttpStreamTransport,
26
27
  OperationSseTransport,
27
28
  OperationTransport,
28
29
  OperationWebSocketTransport,
29
- NativeProviderConfig,
30
30
  ProviderAccessConfig,
31
+ ProviderChallengeKind,
31
32
  ProviderDefinition,
33
+ ProviderOcrConfig,
32
34
  ProviderDeploymentOverrides,
33
35
  ProviderHealthMonitorConfig,
34
36
  ProviderProxyConfig,
35
37
  ProviderProxyProvider,
36
38
  ProviderPublicProfile,
39
+ ProviderResolverConfig,
40
+ ProviderResolverVendor,
37
41
  ProviderReviewed,
38
42
  ProviderSecretDeclaration,
39
43
  ProviderStreamEvent,
@@ -123,7 +127,32 @@ const VALID_PROVIDER_PROXY_AFFINITIES = [
123
127
  "auth-flow",
124
128
  "connection",
125
129
  ] as const;
130
+ const VALID_PROVIDER_OCR_MODES = ["optional", "required"] as const;
126
131
  const VALID_PROVIDER_STT_MODES = ["optional", "required"] as const;
132
+ function exhaustiveLiteralArray<TUnion extends string>() {
133
+ return <const TValues extends readonly TUnion[]>(
134
+ values: TValues,
135
+ ..._missing: Exclude<TUnion, TValues[number]> extends never
136
+ ? []
137
+ : ["Missing runtime values", Exclude<TUnion, TValues[number]>]
138
+ ): TValues => values;
139
+ }
140
+
141
+ export const VALID_PROVIDER_RESOLVER_VENDORS = exhaustiveLiteralArray<ProviderResolverVendor>()([
142
+ "browser",
143
+ "capsolver",
144
+ "capmonster",
145
+ "2captcha",
146
+ "custom",
147
+ ] as const);
148
+ export const VALID_PROVIDER_CHALLENGE_KINDS = exhaustiveLiteralArray<ProviderChallengeKind>()([
149
+ "turnstile",
150
+ "recaptcha_v2",
151
+ "recaptcha_v3",
152
+ "hcaptcha",
153
+ "cloudflare_interstitial",
154
+ "aws_waf",
155
+ ] as const);
127
156
  const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
128
157
  const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
129
158
  const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
@@ -132,9 +161,8 @@ const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
132
161
  // credential fails at build/validation time rather than during a live outage: a
133
162
  // declared-but-uncredentialed fallback leg is a silently dead SPOF, which is
134
163
  // exactly the failure class the multi-vendor chain exists to remove. Vendors
135
- // absent from this map (e.g. `custom`/`decodo`, whose credentials come from the
136
- // `APIFUSE__PROXY__URL` bring-your-own escape hatch, not provider secrets) impose
137
- // no declaration requirement.
164
+ // absent from this map (the deprecated `custom`/`decodo` values have no managed
165
+ // adapter) impose no declaration requirement.
138
166
  const VENDOR_REQUIRED_SECRETS: Partial<Record<ProviderProxyProvider, readonly string[]>> = {
139
167
  smartproxy: [SMARTPROXY_APP_KEY_SECRET],
140
168
  nodemaven: [NODEMAVEN_USERNAME_SECRET, NODEMAVEN_PASSWORD_SECRET],
@@ -248,7 +276,9 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
248
276
  platform: StealthPlatform;
249
277
  };
250
278
  proxy?: ProviderProxyConfig;
279
+ ocr?: ProviderOcrConfig;
251
280
  stt?: ProviderSttConfig;
281
+ resolver?: ProviderResolverConfig;
252
282
  browser?: { engine: BrowserEngine };
253
283
  auth?: AuthConfig;
254
284
  reviewed?: ProviderReviewed;
@@ -678,9 +708,21 @@ function validateProviderProxy(config: {
678
708
  const deprecatedVendors = vendorChain.filter(
679
709
  (vendor) => vendor === "decodo" || vendor === "custom",
680
710
  );
711
+ if (
712
+ proxy.mode === "required" &&
713
+ vendorChain.length > 0 &&
714
+ deprecatedVendors.length === vendorChain.length
715
+ ) {
716
+ throw new ValidationError(
717
+ `Provider "${config.id}" requires proxy egress but declares only deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}.`,
718
+ {
719
+ fix: `Use proxy.provider or proxy.providers with "smartproxy" and/or "nodemaven".`,
720
+ },
721
+ );
722
+ }
681
723
  if (deprecatedVendors.length > 0) {
682
724
  console.warn(
683
- `[provider-sdk] Provider "${config.id}" uses deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}. Use "smartproxy"/"nodemaven", or the APIFUSE__PROXY__URL bring-your-own escape hatch.`,
725
+ `[provider-sdk] Provider "${config.id}" uses deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}. Use "smartproxy"/"nodemaven".`,
684
726
  );
685
727
  }
686
728
  }
@@ -697,6 +739,65 @@ function validateProviderStt(config: { id: string; stt?: ProviderSttConfig }): v
697
739
  assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
698
740
  }
699
741
 
742
+ function validateProviderOcr(config: { id: string; ocr?: ProviderOcrConfig }): void {
743
+ const ocr = config.ocr;
744
+ if (ocr === undefined) return;
745
+ if (!ocr || typeof ocr !== "object" || Array.isArray(ocr)) {
746
+ throw new ValidationError(`Provider "${config.id}" has invalid ocr: must be an object.`, {
747
+ fix: `Use ocr: { mode: "required" } or ocr: { mode: "optional" }.`,
748
+ });
749
+ }
750
+ rejectUnknownFields(ocr, new Set(["mode"]), "ocr");
751
+ assertLiteralField(ocr.mode, "ocr.mode", VALID_PROVIDER_OCR_MODES, config.id);
752
+ }
753
+
754
+ function validateProviderResolver(config: { id: string; resolver?: ProviderResolverConfig }): void {
755
+ const resolver = config.resolver;
756
+ if (resolver === undefined) return;
757
+ if (!resolver || typeof resolver !== "object" || Array.isArray(resolver)) {
758
+ throw new ValidationError(`Provider "${config.id}" has invalid resolver: must be an object.`, {
759
+ fix: `Set resolver for provider "${config.id}" to { vendors: ["2captcha"], kinds: ["turnstile"] }.`,
760
+ });
761
+ }
762
+ rejectUnknownFields(resolver, new Set(["vendors", "kinds"]), "resolver", config.id);
763
+ validateResolverLiteralArray(
764
+ resolver.vendors,
765
+ "resolver.vendors",
766
+ VALID_PROVIDER_RESOLVER_VENDORS,
767
+ config.id,
768
+ );
769
+ validateResolverLiteralArray(
770
+ resolver.kinds,
771
+ "resolver.kinds",
772
+ VALID_PROVIDER_CHALLENGE_KINDS,
773
+ config.id,
774
+ );
775
+ }
776
+
777
+ function validateResolverLiteralArray<TValue extends string>(
778
+ value: readonly TValue[],
779
+ field: string,
780
+ validValues: readonly TValue[],
781
+ providerId: string,
782
+ ): void {
783
+ if (!Array.isArray(value)) {
784
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an array.`, {
785
+ fix: `Set ${field} for provider "${providerId}" to an array containing only: ${validValues.join(", ")}.`,
786
+ });
787
+ }
788
+ for (const [index, item] of value.entries()) {
789
+ if (typeof item === "string" && validValues.some((validValue) => validValue === item)) {
790
+ continue;
791
+ }
792
+ throw new ValidationError(
793
+ `Provider "${providerId}" has invalid ${field}[${index}]: ${JSON.stringify(item)}. Expected one of: ${validValues.join(", ")}`,
794
+ {
795
+ fix: `Set ${field}[${index}] for provider "${providerId}" to one of ${validValues.map((validValue) => `"${validValue}"`).join(", ")}.`,
796
+ },
797
+ );
798
+ }
799
+ }
800
+
700
801
  function validateOperationIds(
701
802
  providerId: string,
702
803
  operations: Record<string, ProviderOperation>,
@@ -1294,7 +1395,12 @@ function suggestField(unknown: string, candidates: ReadonlySet<string>): string
1294
1395
  return best;
1295
1396
  }
1296
1397
 
1297
- function rejectUnknownFields(value: object, allowed: ReadonlySet<string>, fieldPath: string): void {
1398
+ function rejectUnknownFields(
1399
+ value: object,
1400
+ allowed: ReadonlySet<string>,
1401
+ fieldPath: string,
1402
+ providerId?: string,
1403
+ ): void {
1298
1404
  for (const key of Object.keys(value)) {
1299
1405
  if (allowed.has(key)) continue;
1300
1406
  const hint = suggestField(key, allowed);
@@ -1302,7 +1408,11 @@ function rejectUnknownFields(value: object, allowed: ReadonlySet<string>, fieldP
1302
1408
  hint
1303
1409
  ? `Unknown field "${key}" on ${fieldPath}. Did you mean "${hint}"?`
1304
1410
  : `Unknown field "${key}" on ${fieldPath}.`,
1305
- { fix: `Remove ${fieldPath}.${key} or rename it.` },
1411
+ {
1412
+ fix: providerId
1413
+ ? `Remove ${fieldPath}.${key} from provider "${providerId}" or rename it.`
1414
+ : `Remove ${fieldPath}.${key} or rename it.`,
1415
+ },
1306
1416
  );
1307
1417
  }
1308
1418
  }
@@ -2386,7 +2496,9 @@ export function defineProvider<
2386
2496
  throw error;
2387
2497
  }
2388
2498
  validateProviderProxy(config);
2499
+ validateProviderOcr(config);
2389
2500
  validateProviderStt(config);
2501
+ validateProviderResolver(config);
2390
2502
  if (config.runtime === "browser" && !config.browser)
2391
2503
  throw new ProviderError(
2392
2504
  `Provider "${config.id}" must define browser.engine when runtime is "browser"`,
@@ -2410,7 +2522,9 @@ export function defineProvider<
2410
2522
  native: config.native,
2411
2523
  stealth: config.stealth,
2412
2524
  proxy: config.proxy,
2525
+ ocr: config.ocr,
2413
2526
  stt: config.stt,
2527
+ resolver: config.resolver,
2414
2528
  browser: config.browser,
2415
2529
  auth: config.auth,
2416
2530
  reviewed: config.reviewed,
@@ -30,6 +30,7 @@ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
30
30
  "flow_expired",
31
31
  "turn_validation_error",
32
32
  "context_access_error",
33
+ "OCR_UPSTREAM_FAILED",
33
34
  "UNSUPPORTED_STT_OPTION",
34
35
  "INVALID_STT_AUDIO",
35
36
  "STT_AUDIO_TOO_LARGE",
@@ -85,6 +86,8 @@ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
85
86
  export const SDK_RUNTIME_OWNED_ERROR_CODES = new Set([
86
87
  ...SDK_OWNED_PROVIDER_ERROR_CODES,
87
88
  "reauth_required",
89
+ "OCR_UNAVAILABLE",
90
+ "UNSUPPORTED_OCR_BACKEND",
88
91
  "STT_UNAVAILABLE",
89
92
  "UNSUPPORTED_STT_BACKEND",
90
93
  "OUTPUT_VALIDATION_FAILED",
@@ -116,6 +119,8 @@ export const SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES: ReadonlyMap<string, Provide
116
119
  ["UPSTREAM_REJECTED", 409],
117
120
  ["UPSTREAM_ERROR", 502],
118
121
  ["BLOCKED", 502],
122
+ ["OCR_UNAVAILABLE", 503],
123
+ ["UNSUPPORTED_OCR_BACKEND", 503],
119
124
  ["STT_UNAVAILABLE", 503],
120
125
  ["UNSUPPORTED_STT_BACKEND", 503],
121
126
  ["STATEFUL_FORWARDING_REPLAY_CACHE_FULL", 503],
package/src/index.ts CHANGED
@@ -6,7 +6,6 @@ export * from "./choice-token.js";
6
6
  export type {
7
7
  ApiFuseConfig,
8
8
  BrowserConfig,
9
- ProxyConfig,
10
9
  ProxyProtocol,
11
10
  ProxyResolutionOptions,
12
11
  ProxyResolutionSource,
@@ -101,6 +100,18 @@ export {
101
100
  } from "./runtime/instrumentation.js";
102
101
  export { type PrevalidateResult, prevalidate } from "./runtime/prevalidate.js";
103
102
  export { getProviderBaseUrl } from "./runtime/provider.js";
103
+ export {
104
+ APIFUSE__CDP_POOL__URL,
105
+ APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
106
+ APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
107
+ APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
108
+ APIFUSE__RESOLVER__TIMEOUT_MS,
109
+ createResolverClientFromEnv,
110
+ createUnsupportedResolverClient,
111
+ DEFAULT_RESOLVER_TIMEOUT_MS,
112
+ invalidateResolverSolution,
113
+ type ResolverRuntimeOptions,
114
+ } from "./runtime/resolver.js";
104
115
  export {
105
116
  assertRequiredSecretsPresent,
106
117
  listMissingRequiredSecrets,
@@ -111,6 +122,22 @@ export {
111
122
  UnsupportedProviderStateError,
112
123
  } from "./runtime/state.js";
113
124
  export { createStealthClient } from "./runtime/stealth.js";
125
+ export {
126
+ APIFUSE__OCR__API_KEY_ENV,
127
+ APIFUSE__OCR__BACKEND_ENV,
128
+ APIFUSE__OCR__BASE_URL_ENV,
129
+ APIFUSE__OCR__CLOUDFLARE_API_TOKEN_ENV,
130
+ APIFUSE__OCR__MODEL_ENV,
131
+ CLOUDFLARE_ACCOUNT_ID_ENV,
132
+ CLOUDFLARE_WORKERS_AI_OCR_BACKEND,
133
+ createCloudflareWorkersAiOcrClient,
134
+ createOcrClientFromEnv,
135
+ createOpenAiCompatibleOcrClient,
136
+ createUnsupportedOcrClient,
137
+ DEFAULT_CLOUDFLARE_WORKERS_AI_OCR_MODEL,
138
+ extractCaptchaCandidates,
139
+ OPENAI_COMPATIBLE_OCR_BACKEND,
140
+ } from "./runtime/ocr.js";
114
141
  export {
115
142
  APIFUSE__STT__BACKEND_ENV,
116
143
  APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV,
@@ -161,6 +188,7 @@ export type {
161
188
  AuthMode,
162
189
  AuthTurn,
163
190
  Bcp47Locale,
191
+ BrowserCookie,
164
192
  BrowserEngine,
165
193
  BrowserOptions,
166
194
  BrowserResourceBody,
@@ -169,6 +197,7 @@ export type {
169
197
  BrowserResourcePolicy,
170
198
  BrowserResourceRequest,
171
199
  BrowserResourceRoute,
200
+ ChallengeSolution,
172
201
  ConnectionMode,
173
202
  ContextDeclaration,
174
203
  CookieJar,
@@ -229,6 +258,14 @@ export type {
229
258
  NativeTcpPortRange,
230
259
  NativeTcpTlsMode,
231
260
  NativeTlsConnectOptions,
261
+ OcrCaptchaCandidate,
262
+ OcrCaptchaOptions,
263
+ OcrCaptchaResult,
264
+ OcrContext,
265
+ OcrImageInput,
266
+ OcrRecognizeRequest,
267
+ OcrResult,
268
+ OcrWarning,
232
269
  OperationAnnotations,
233
270
  OperationApprovalPolicy,
234
271
  OperationContractMetadata,
@@ -261,6 +298,8 @@ export type {
261
298
  ProviderChoiceContext,
262
299
  ProviderChoiceIssueOptions,
263
300
  ProviderChoiceParseOptions,
301
+ ProviderChallenge,
302
+ ProviderChallengeKind,
264
303
  ProviderContext,
265
304
  ProviderDefinition,
266
305
  ProviderDeploymentOverrides,
@@ -274,6 +313,7 @@ export type {
274
313
  ProviderLogoProfile,
275
314
  ProviderLogoSource,
276
315
  ProviderMeta,
316
+ ProviderOcrConfig,
277
317
  ProviderProxyConfig,
278
318
  ProviderProxyMode,
279
319
  ProviderProxyPolicy,
@@ -284,6 +324,8 @@ export type {
284
324
  ProviderPublicProfile,
285
325
  ProviderReviewed,
286
326
  ProviderResolvedFile,
327
+ ProviderResolverConfig,
328
+ ProviderResolverVendor,
287
329
  ProviderRuntimeState,
288
330
  ProviderSecretDeclaration,
289
331
  ProviderStateDurationString,
@@ -294,6 +336,7 @@ export type {
294
336
  ProviderSupportLevel,
295
337
  RequestOptions,
296
338
  RedirectRunReason,
339
+ ResolverContext,
297
340
  Rfc3339Instant,
298
341
  SchemaLike,
299
342
  SmsOrigin,
@@ -303,6 +346,7 @@ export type {
303
346
  StandardSchemaV1,
304
347
  StateCasResult,
305
348
  StateNamespaceOptions,
349
+ StateNamespaceScope,
306
350
  StateValue,
307
351
  StateWriteOptions,
308
352
  StealthClient,
package/src/provider.ts CHANGED
@@ -162,6 +162,7 @@ export type {
162
162
  StandardSchemaV1,
163
163
  StateCasResult,
164
164
  StateNamespaceOptions,
165
+ StateNamespaceScope,
165
166
  StateValue,
166
167
  StateWriteOptions,
167
168
  } from "./types.js";
@@ -5,9 +5,12 @@ import type {
5
5
  EnvContext,
6
6
  FlowContext,
7
7
  HttpClient,
8
+ OcrContext,
8
9
  StealthClient,
9
10
  SttContext,
10
11
  } from "../types.js";
12
+ import { createUnsupportedOcrClient } from "./ocr.js";
13
+ import { createUnsupportedResolverClient } from "./resolver.js";
11
14
  import { createUnsupportedSttClient } from "./stt.js";
12
15
 
13
16
  function normalizeAllowedKeys(allowedKeys: string[]): Set<string> {
@@ -58,6 +61,7 @@ export function createFlowContext(options: {
58
61
  externalRef?: string;
59
62
  allowedKeys: string[];
60
63
  initialContext?: Record<string, unknown>;
64
+ ocr?: OcrContext;
61
65
  stt?: SttContext;
62
66
  }): FlowContext {
63
67
  return {
@@ -70,7 +74,9 @@ export function createFlowContext(options: {
70
74
  stealth: options.stealth,
71
75
  env: options.env,
72
76
  context: createScratchpad(options.allowedKeys, options.initialContext),
77
+ ocr: options.ocr ?? createUnsupportedOcrClient(),
73
78
  stt: options.stt ?? createUnsupportedSttClient(),
79
+ resolver: createUnsupportedResolverClient("Resolver is not available in auth flow context"),
74
80
  auth: createAuthFlowHelpers(),
75
81
  };
76
82
  }