@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.
- package/CHANGELOG.md +8 -0
- package/bin/apifuse-dev.ts +4 -0
- package/bin/apifuse-pack-types.ts +234 -38
- package/bin/apifuse-perf.ts +15 -12
- package/bin/apifuse-record.ts +4 -0
- package/dist/config/loader.d.ts +8 -19
- package/dist/config/loader.js +28 -86
- package/dist/contract-types.d.ts +1 -0
- package/dist/contract.js +2 -0
- package/dist/define.d.ts +5 -1
- package/dist/define.js +79 -6
- package/dist/error-resolution.js +5 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -0
- package/dist/provider.d.ts +1 -1
- package/dist/runtime/auth-flow.d.ts +2 -1
- package/dist/runtime/auth-flow.js +4 -0
- package/dist/runtime/browser.js +78 -9
- package/dist/runtime/http.js +0 -1
- package/dist/runtime/instrumentation.js +26 -1
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +440 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +8 -0
- package/dist/runtime/resolver-vendors/bindings.js +15 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +24 -0
- package/dist/runtime/resolver-vendors/browser.js +287 -0
- package/dist/runtime/resolver-vendors/types.d.ts +42 -0
- package/dist/runtime/resolver-vendors/types.js +57 -0
- package/dist/runtime/resolver.d.ts +39 -0
- package/dist/runtime/resolver.js +414 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +245 -141
- package/dist/runtime/stealth.js +3 -6
- package/dist/runtime/stt.js +1 -12
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/server/serve.d.ts +6 -1
- package/dist/server/serve.js +39 -8
- package/dist/testing/run.js +10 -0
- package/dist/types.d.ts +163 -4
- package/package.json +1 -1
- package/src/config/loader.ts +35 -111
- package/src/contract-types.ts +1 -0
- package/src/contract.ts +2 -0
- package/src/define.ts +121 -7
- package/src/error-resolution.ts +5 -0
- package/src/index.ts +45 -1
- package/src/provider.ts +1 -0
- package/src/runtime/auth-flow.ts +6 -0
- package/src/runtime/browser.ts +139 -19
- package/src/runtime/http.ts +0 -1
- package/src/runtime/instrumentation.ts +36 -2
- package/src/runtime/ocr.ts +523 -0
- package/src/runtime/resolver-vendors/bindings.ts +31 -0
- package/src/runtime/resolver-vendors/browser.ts +420 -0
- package/src/runtime/resolver-vendors/types.ts +113 -0
- package/src/runtime/resolver.ts +668 -0
- package/src/runtime/state.ts +323 -166
- package/src/runtime/stealth.ts +3 -6
- package/src/runtime/stt.ts +1 -19
- package/src/runtime/timeout.ts +18 -0
- package/src/server/serve.ts +80 -5
- package/src/testing/run.ts +15 -0
- package/src/types.ts +188 -4
package/src/config/loader.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
|
|
20
20
|
export type { ProxyProtocol } from "../runtime/proxy-nodemaven.js";
|
|
21
21
|
|
|
22
|
-
/** Proxy vendors
|
|
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
|
|
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
|
-
|
|
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-
|
|
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
|
|
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).
|
|
928
|
-
*
|
|
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).
|
|
949
|
-
*
|
|
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.
|
|
993
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
1840
|
-
syncProxyEnv(resolvedConfig);
|
|
1841
|
-
return resolvedConfig;
|
|
1765
|
+
return config ?? {};
|
|
1842
1766
|
}
|
|
1843
1767
|
|
|
1844
1768
|
return {};
|
package/src/contract-types.ts
CHANGED
|
@@ -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 (
|
|
136
|
-
//
|
|
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"
|
|
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(
|
|
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
|
-
{
|
|
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,
|
package/src/error-resolution.ts
CHANGED
|
@@ -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
package/src/runtime/auth-flow.ts
CHANGED
|
@@ -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
|
}
|