@apifuse/provider-sdk 2.2.0-beta.7 → 2.2.0-beta.9
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/dist/auth-turn/index.d.ts +2 -2
- package/dist/config/loader.d.ts +147 -6
- package/dist/config/loader.js +360 -48
- package/dist/define.js +63 -8
- package/dist/runtime/choice.js +20 -2
- package/dist/runtime/http.js +115 -9
- package/dist/runtime/proxy-errors.js +6 -2
- package/dist/runtime/proxy-nodemaven.d.ts +35 -0
- package/dist/runtime/proxy-nodemaven.js +128 -0
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +39 -4
- package/dist/runtime/state.js +12 -1
- package/dist/runtime/stealth.js +60 -18
- package/dist/server/serve.js +11 -0
- package/dist/server/types.d.ts +9 -9
- package/dist/types.d.ts +30 -1
- package/package.json +4 -3
- package/src/config/loader.ts +514 -61
- package/src/define.ts +76 -13
- package/src/runtime/choice.ts +25 -4
- package/src/runtime/http.ts +134 -11
- package/src/runtime/proxy-errors.ts +12 -4
- package/src/runtime/proxy-nodemaven.ts +178 -0
- package/src/runtime/proxy-telemetry.ts +56 -5
- package/src/runtime/state.ts +11 -1
- package/src/runtime/stealth.ts +68 -22
- package/src/server/serve.ts +11 -0
- package/src/types.ts +30 -1
package/src/config/loader.ts
CHANGED
|
@@ -4,8 +4,25 @@ import path from "node:path";
|
|
|
4
4
|
|
|
5
5
|
import { Redis } from "ioredis";
|
|
6
6
|
|
|
7
|
-
import type { ProviderProxyPolicy, TraceConfig } from "../types.js";
|
|
8
|
-
|
|
7
|
+
import type { ProviderProxyPolicy, ProviderProxyProvider, TraceConfig } from "../types.js";
|
|
8
|
+
import {
|
|
9
|
+
NODEMAVEN_DEFAULT_PROTOCOL,
|
|
10
|
+
NODEMAVEN_MAX_POOL_SIZE,
|
|
11
|
+
type ProxyProtocol,
|
|
12
|
+
hasNodemavenCredentials,
|
|
13
|
+
nodemavenPoolSize,
|
|
14
|
+
synthesizeNodemavenProxy,
|
|
15
|
+
} from "../runtime/proxy-nodemaven.js";
|
|
16
|
+
|
|
17
|
+
export type { ProxyProtocol } from "../runtime/proxy-nodemaven.js";
|
|
18
|
+
|
|
19
|
+
/** Proxy vendors the SDK resolves natively (as opposed to the static env path). */
|
|
20
|
+
export type ProxyVendorName = "smartproxy" | "nodemaven";
|
|
21
|
+
|
|
22
|
+
// "smartproxy" here is api.smartproxy.org — a residential proxy with an IP
|
|
23
|
+
// extraction API (app_key → raw ip:port pool). It is NOT the company formerly
|
|
24
|
+
// named Smartproxy (smartproxy.com), which rebranded to Decodo in 2025 and is
|
|
25
|
+
// modelled separately as the `decodo` gateway vendor. Do not conflate them.
|
|
9
26
|
export const SMARTPROXY_APP_KEY_ENV = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
|
|
10
27
|
export const SMARTPROXY_MAX_LIFETIME_MINUTES = 2000;
|
|
11
28
|
export const DEFAULT_SMARTPROXY_POOL_SIZE = 20;
|
|
@@ -52,6 +69,26 @@ export type ProxyResolutionOptions = {
|
|
|
52
69
|
affinityKey?: string;
|
|
53
70
|
/** Zero-based proxy-pool attempt index used by SDK transports for failover. */
|
|
54
71
|
proxyAttempt?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Tunnelling protocols the calling transport can use. When a resolved
|
|
74
|
+
* protocol is not in this set the resolver fails with
|
|
75
|
+
* `PROXY_PROTOCOL_UNSUPPORTED` instead of silently downgrading. Unset means
|
|
76
|
+
* permissive (both protocols allowed).
|
|
77
|
+
*/
|
|
78
|
+
transportProtocols?: readonly ProxyProtocol[];
|
|
79
|
+
/**
|
|
80
|
+
* Explicit protocol override. Internal — for the verification harness and
|
|
81
|
+
* tests, or an advanced caller. Normal callers omit it and each vendor uses
|
|
82
|
+
* its own benchmarked default protocol (see VENDOR_DEFAULT_PROTOCOL). Not an
|
|
83
|
+
* env var and not a provider-policy field.
|
|
84
|
+
*/
|
|
85
|
+
protocol?: ProxyProtocol;
|
|
86
|
+
/**
|
|
87
|
+
* Gateway pool "refresh" generation. Bumped by transports on pool refresh to
|
|
88
|
+
* derive a fresh gateway session set (ignored by allocation-style vendors,
|
|
89
|
+
* whose refresh is driven by cache invalidation).
|
|
90
|
+
*/
|
|
91
|
+
proxyRefreshEpoch?: number;
|
|
55
92
|
telemetry?: ProxyTelemetrySink;
|
|
56
93
|
};
|
|
57
94
|
|
|
@@ -74,7 +111,8 @@ export type SmartproxyAllocatorBodyClass =
|
|
|
74
111
|
| "usable_proxy_endpoints";
|
|
75
112
|
|
|
76
113
|
export type ProxyResolutionTelemetryEvent = {
|
|
77
|
-
provider:
|
|
114
|
+
provider: ProxyVendorName;
|
|
115
|
+
protocol?: ProxyProtocol;
|
|
78
116
|
cacheStatus: ProxyCacheStatus;
|
|
79
117
|
cacheHit: boolean;
|
|
80
118
|
resolutionMs: number;
|
|
@@ -92,7 +130,7 @@ export type ProxyResolutionTelemetryEvent = {
|
|
|
92
130
|
};
|
|
93
131
|
|
|
94
132
|
export type ProxyAttemptTelemetryEvent = {
|
|
95
|
-
provider:
|
|
133
|
+
provider: ProxyVendorName;
|
|
96
134
|
attempt: number;
|
|
97
135
|
poolIndex?: number;
|
|
98
136
|
proxyHash?: string;
|
|
@@ -102,31 +140,60 @@ export type ProxyAttemptTelemetryEvent = {
|
|
|
102
140
|
durationMs?: number;
|
|
103
141
|
};
|
|
104
142
|
|
|
143
|
+
export type ProxyVendorFailoverTelemetryEvent = {
|
|
144
|
+
/** Vendor that failed or was skipped. */
|
|
145
|
+
vendor: ProxyVendorName;
|
|
146
|
+
/** Vendor tried next, or undefined when the chain is exhausted. */
|
|
147
|
+
nextVendor?: ProxyVendorName;
|
|
148
|
+
phase: "resolution" | "transport";
|
|
149
|
+
reason: "no_credentials" | "allocation_failed" | "pool_exhausted" | "protocol_unsupported";
|
|
150
|
+
attempt?: number;
|
|
151
|
+
};
|
|
152
|
+
|
|
105
153
|
export type ProxyTelemetrySink = {
|
|
106
154
|
recordProxyResolution(event: ProxyResolutionTelemetryEvent): void;
|
|
107
155
|
recordProxyAttempt?(event: ProxyAttemptTelemetryEvent): void;
|
|
156
|
+
recordProxyVendorFailover?(event: ProxyVendorFailoverTelemetryEvent): void;
|
|
108
157
|
};
|
|
109
158
|
|
|
110
159
|
export type ResolvedProxyConfig = {
|
|
111
160
|
shouldWarn: boolean;
|
|
112
161
|
url?: string;
|
|
113
|
-
source?: "explicit" | "env" | "config" | "smartproxy-allocator";
|
|
162
|
+
source?: "explicit" | "env" | "config" | "smartproxy-allocator" | "nodemaven-gateway";
|
|
163
|
+
protocol?: ProxyProtocol;
|
|
114
164
|
diagnostics?: Record<string, string | number | boolean>;
|
|
115
165
|
};
|
|
116
166
|
|
|
167
|
+
export type ProxyResolutionErrorCode =
|
|
168
|
+
| "PROXY_REQUIRED"
|
|
169
|
+
| "PROXY_ALLOCATION_FAILED"
|
|
170
|
+
| "PROXY_PROTOCOL_UNSUPPORTED";
|
|
171
|
+
|
|
117
172
|
export class ProxyResolutionError extends Error {
|
|
118
|
-
readonly code:
|
|
173
|
+
readonly code: ProxyResolutionErrorCode;
|
|
119
174
|
readonly telemetry?: ProxyResolutionTelemetryEvent;
|
|
175
|
+
readonly vendor?: ProxyVendorName;
|
|
176
|
+
readonly vendorChain?: ProxyVendorName[];
|
|
177
|
+
readonly protocol?: ProxyProtocol;
|
|
120
178
|
|
|
121
179
|
constructor(
|
|
122
|
-
code:
|
|
180
|
+
code: ProxyResolutionErrorCode,
|
|
123
181
|
message: string,
|
|
124
|
-
options?: {
|
|
182
|
+
options?: {
|
|
183
|
+
cause?: unknown;
|
|
184
|
+
telemetry?: ProxyResolutionTelemetryEvent;
|
|
185
|
+
vendor?: ProxyVendorName;
|
|
186
|
+
vendorChain?: ProxyVendorName[];
|
|
187
|
+
protocol?: ProxyProtocol;
|
|
188
|
+
},
|
|
125
189
|
) {
|
|
126
190
|
super(message, options);
|
|
127
191
|
this.name = "ProxyResolutionError";
|
|
128
192
|
this.code = code;
|
|
129
193
|
this.telemetry = options?.telemetry;
|
|
194
|
+
this.vendor = options?.vendor;
|
|
195
|
+
this.vendorChain = options?.vendorChain;
|
|
196
|
+
this.protocol = options?.protocol;
|
|
130
197
|
}
|
|
131
198
|
}
|
|
132
199
|
|
|
@@ -389,6 +456,11 @@ function applyStickyProxySession(proxyUrl: string): string {
|
|
|
389
456
|
return proxyUrl;
|
|
390
457
|
}
|
|
391
458
|
|
|
459
|
+
// This rewrites sticky-session usernames for a bring-your-own *gateway* URL
|
|
460
|
+
// (APIFUSE__PROXY__URL). The `smartproxy` host here means a smartproxy.com /
|
|
461
|
+
// Decodo-family gateway that authenticates by username — NOT the
|
|
462
|
+
// api.smartproxy.org allocation vendor, whose endpoints are raw ip:port with
|
|
463
|
+
// no credentials and therefore return early above.
|
|
392
464
|
const host = parsed.hostname.toLowerCase();
|
|
393
465
|
if (!host.includes("smartproxy") && !host.includes("decodo")) {
|
|
394
466
|
return proxyUrl;
|
|
@@ -477,60 +549,214 @@ export async function resolveProxyConfigAsync(
|
|
|
477
549
|
return { shouldWarn: false };
|
|
478
550
|
}
|
|
479
551
|
|
|
480
|
-
const
|
|
481
|
-
if (
|
|
552
|
+
const chain = resolveVendorChain(policy);
|
|
553
|
+
if (chain.length === 0) {
|
|
554
|
+
// decodo/custom/env-static providers keep the legacy static-URL path.
|
|
482
555
|
return resolveProxyConfig({
|
|
483
556
|
...options,
|
|
484
557
|
upstream: { proxy: true },
|
|
485
558
|
});
|
|
486
559
|
}
|
|
487
560
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
561
|
+
// Protocol is chosen per vendor (each vendor's benchmarked-best), with an
|
|
562
|
+
// optional explicit override for the harness/tests. Both are tunnelling
|
|
563
|
+
// schemes. transportProtocols is what the calling transport can actually use.
|
|
564
|
+
const transportProtocols = options.transportProtocols ?? (["http", "socks5"] as const);
|
|
565
|
+
|
|
566
|
+
const sizes = chain.map((vendor) => vendorPoolSize(vendor, policy));
|
|
567
|
+
const total = sizes.reduce((sum, size) => sum + size, 0);
|
|
568
|
+
const normalizedAttempt = normalizeAttemptIndex(options.proxyAttempt);
|
|
569
|
+
const { vendorIndex: startVendorIndex, poolIndex: startPoolIndex } = mapFlatAttempt(
|
|
570
|
+
total > 0 ? normalizedAttempt % total : 0,
|
|
571
|
+
sizes,
|
|
572
|
+
);
|
|
573
|
+
const refreshEpoch = normalizeAttemptIndex(options.proxyRefreshEpoch);
|
|
574
|
+
|
|
575
|
+
let lastError: unknown;
|
|
576
|
+
let blockedProtocol: ProxyProtocol | undefined;
|
|
577
|
+
for (let vendorIndex = startVendorIndex; vendorIndex < chain.length; vendorIndex++) {
|
|
578
|
+
const vendor = chain[vendorIndex] as ProxyVendorName;
|
|
579
|
+
const nextVendor = chain[vendorIndex + 1];
|
|
580
|
+
const poolIndex = vendorIndex === startVendorIndex ? startPoolIndex : 0;
|
|
581
|
+
const protocol = options.protocol ?? VENDOR_DEFAULT_PROTOCOL[vendor];
|
|
582
|
+
|
|
583
|
+
if (!vendorHasCredentials(vendor)) {
|
|
584
|
+
options.telemetry?.recordProxyVendorFailover?.({
|
|
585
|
+
vendor,
|
|
586
|
+
nextVendor,
|
|
587
|
+
phase: "resolution",
|
|
588
|
+
reason: "no_credentials",
|
|
589
|
+
});
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// The calling transport must be able to use this vendor's protocol; if not,
|
|
594
|
+
// fail over to the next vendor rather than silently downgrading.
|
|
595
|
+
if (!transportProtocols.includes(protocol)) {
|
|
596
|
+
blockedProtocol = protocol;
|
|
597
|
+
options.telemetry?.recordProxyVendorFailover?.({
|
|
598
|
+
vendor,
|
|
599
|
+
nextVendor,
|
|
600
|
+
phase: "resolution",
|
|
601
|
+
reason: "protocol_unsupported",
|
|
602
|
+
});
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
try {
|
|
607
|
+
return await resolveWithVendor(vendor, policy, options, {
|
|
608
|
+
protocol,
|
|
609
|
+
poolIndex,
|
|
610
|
+
refreshEpoch,
|
|
611
|
+
});
|
|
612
|
+
} catch (error) {
|
|
613
|
+
// Config/programming errors (invalid filter, etc.) are not vendor
|
|
614
|
+
// outages — propagate them rather than failing over.
|
|
615
|
+
if (!(error instanceof ProxyResolutionError)) {
|
|
616
|
+
throw error;
|
|
617
|
+
}
|
|
618
|
+
if (error.telemetry) {
|
|
619
|
+
options.telemetry?.recordProxyResolution(error.telemetry);
|
|
620
|
+
}
|
|
621
|
+
lastError = error;
|
|
622
|
+
options.telemetry?.recordProxyVendorFailover?.({
|
|
623
|
+
vendor,
|
|
624
|
+
nextVendor,
|
|
625
|
+
phase: "resolution",
|
|
626
|
+
reason: "allocation_failed",
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (policy.mode === "required") {
|
|
632
|
+
if (lastError) {
|
|
633
|
+
throw lastError instanceof ProxyResolutionError
|
|
634
|
+
? lastError
|
|
635
|
+
: new ProxyResolutionError(
|
|
636
|
+
"PROXY_ALLOCATION_FAILED",
|
|
637
|
+
`All proxy vendors [${chain.join(", ")}] failed for required proxy egress.`,
|
|
638
|
+
{ cause: lastError, vendorChain: chain },
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
if (blockedProtocol) {
|
|
491
642
|
throw new ProxyResolutionError(
|
|
492
|
-
"
|
|
493
|
-
`
|
|
643
|
+
"PROXY_PROTOCOL_UNSUPPORTED",
|
|
644
|
+
`No proxy vendor in [${chain.join(", ")}] could serve a protocol supported by this transport (supports: ${transportProtocols.join(", ")}; vendor wanted "${blockedProtocol}"). Route this provider through the stealth transport.`,
|
|
645
|
+
{ protocol: blockedProtocol, vendorChain: chain },
|
|
494
646
|
);
|
|
495
647
|
}
|
|
496
|
-
|
|
648
|
+
throw new ProxyResolutionError(
|
|
649
|
+
"PROXY_REQUIRED",
|
|
650
|
+
`Proxy egress is required but no vendor credentials are configured. Missing: ${chain
|
|
651
|
+
.map((vendor) => `${missingCredentialEnv(vendor)} (${vendor})`)
|
|
652
|
+
.join(", ")}.`,
|
|
653
|
+
{ vendorChain: chain },
|
|
654
|
+
);
|
|
497
655
|
}
|
|
498
|
-
|
|
656
|
+
return { shouldWarn: true };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Each vendor's default egress protocol, chosen from live KR benchmarks. HTTP
|
|
661
|
+
* CONNECT wins for nodemaven (socks5 adds ~500ms through the gateway) and ties
|
|
662
|
+
* for smartproxy, and is the only protocol ctx.http (Bun native fetch) supports.
|
|
663
|
+
* Override per call via ProxyResolutionOptions.protocol (harness/tests).
|
|
664
|
+
*/
|
|
665
|
+
const VENDOR_DEFAULT_PROTOCOL: Record<ProxyVendorName, ProxyProtocol> = {
|
|
666
|
+
smartproxy: "http",
|
|
667
|
+
nodemaven: NODEMAVEN_DEFAULT_PROTOCOL,
|
|
668
|
+
};
|
|
499
669
|
|
|
670
|
+
/**
|
|
671
|
+
* Guard the No-MITM invariant: a resolved proxy URL must use a tunnelling scheme
|
|
672
|
+
* (http CONNECT or socks5) so the client TLS handshake reaches the origin
|
|
673
|
+
* end-to-end. Anything else would intercept TLS and break fingerprinting.
|
|
674
|
+
*/
|
|
675
|
+
export function assertTunnelingScheme(url: string): void {
|
|
676
|
+
let scheme: string;
|
|
500
677
|
try {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
678
|
+
scheme = new URL(url).protocol.replace(/:$/, "").toLowerCase();
|
|
679
|
+
} catch {
|
|
680
|
+
throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `Malformed proxy URL: ${url}`);
|
|
681
|
+
}
|
|
682
|
+
if (scheme !== "http" && scheme !== "socks5") {
|
|
683
|
+
throw new ProxyResolutionError(
|
|
684
|
+
"PROXY_ALLOCATION_FAILED",
|
|
685
|
+
`Resolved proxy scheme "${scheme}" is not a tunnelling scheme (expected http or socks5). Refusing to route TLS through a non-tunnelling proxy.`,
|
|
506
686
|
);
|
|
507
|
-
|
|
508
|
-
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function resolveWithVendor(
|
|
691
|
+
vendor: ProxyVendorName,
|
|
692
|
+
policy: ProviderProxyPolicy,
|
|
693
|
+
options: ProxyResolutionOptions,
|
|
694
|
+
context: { protocol: ProxyProtocol; poolIndex: number; refreshEpoch: number },
|
|
695
|
+
): Promise<ResolvedProxyConfig> {
|
|
696
|
+
if (vendor === "nodemaven") {
|
|
697
|
+
const startedAt = Date.now();
|
|
698
|
+
const synthesized = synthesizeNodemavenProxy({
|
|
699
|
+
policy,
|
|
700
|
+
affinityKey: options.affinityKey,
|
|
701
|
+
protocol: context.protocol,
|
|
702
|
+
poolIndex: context.poolIndex,
|
|
703
|
+
refreshEpoch: context.refreshEpoch,
|
|
704
|
+
country: resolveSmartproxyCountry(policy),
|
|
705
|
+
});
|
|
706
|
+
options.telemetry?.recordProxyResolution({
|
|
707
|
+
provider: "nodemaven",
|
|
708
|
+
protocol: synthesized.protocol,
|
|
709
|
+
cacheStatus: "disabled",
|
|
710
|
+
cacheHit: false,
|
|
711
|
+
resolutionMs: Math.max(0, Date.now() - startedAt),
|
|
712
|
+
attempts: 1,
|
|
713
|
+
});
|
|
714
|
+
assertTunnelingScheme(synthesized.url);
|
|
509
715
|
return {
|
|
510
716
|
shouldWarn: false,
|
|
511
|
-
url:
|
|
512
|
-
source: "
|
|
717
|
+
url: synthesized.url,
|
|
718
|
+
source: "nodemaven-gateway",
|
|
719
|
+
protocol: synthesized.protocol,
|
|
513
720
|
diagnostics: {
|
|
514
|
-
...
|
|
515
|
-
|
|
516
|
-
poolIndex,
|
|
721
|
+
...synthesized.diagnostics,
|
|
722
|
+
poolIndex: context.poolIndex,
|
|
517
723
|
},
|
|
518
724
|
};
|
|
519
|
-
} catch (error) {
|
|
520
|
-
if (error instanceof ProxyResolutionError && error.telemetry) {
|
|
521
|
-
options.telemetry?.recordProxyResolution(error.telemetry);
|
|
522
|
-
}
|
|
523
|
-
if (policy.mode === "required") {
|
|
524
|
-
throw error instanceof ProxyResolutionError
|
|
525
|
-
? error
|
|
526
|
-
: new ProxyResolutionError(
|
|
527
|
-
"PROXY_ALLOCATION_FAILED",
|
|
528
|
-
"Smartproxy allocator failed for required proxy egress.",
|
|
529
|
-
{ cause: error },
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
return { shouldWarn: true };
|
|
533
725
|
}
|
|
726
|
+
|
|
727
|
+
// smartproxy allocation-style vendor.
|
|
728
|
+
const appKey = process.env[SMARTPROXY_APP_KEY_ENV]?.trim();
|
|
729
|
+
if (!appKey) {
|
|
730
|
+
// Guarded by vendorHasCredentials; treated as a vendor-internal failure.
|
|
731
|
+
throw new ProxyResolutionError(
|
|
732
|
+
"PROXY_ALLOCATION_FAILED",
|
|
733
|
+
`${SMARTPROXY_APP_KEY_ENV} is not configured.`,
|
|
734
|
+
{ vendor: "smartproxy" },
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
const lifetimeMinutes = resolveSmartproxyLifetime(policy);
|
|
738
|
+
const allocated = await allocateSmartproxy(
|
|
739
|
+
policy,
|
|
740
|
+
appKey,
|
|
741
|
+
lifetimeMinutes,
|
|
742
|
+
options.affinityKey,
|
|
743
|
+
context.protocol,
|
|
744
|
+
);
|
|
745
|
+
options.telemetry?.recordProxyResolution({ ...allocated.telemetry, protocol: context.protocol });
|
|
746
|
+
const poolIndex = selectProxyPoolIndex(allocated.pool.urls.length, context.poolIndex);
|
|
747
|
+
const url = allocated.pool.urls[poolIndex];
|
|
748
|
+
if (url) assertTunnelingScheme(url);
|
|
749
|
+
return {
|
|
750
|
+
shouldWarn: false,
|
|
751
|
+
url,
|
|
752
|
+
source: "smartproxy-allocator",
|
|
753
|
+
protocol: context.protocol,
|
|
754
|
+
diagnostics: {
|
|
755
|
+
...allocated.pool.diagnostics,
|
|
756
|
+
poolSize: allocated.pool.urls.length,
|
|
757
|
+
poolIndex,
|
|
758
|
+
},
|
|
759
|
+
};
|
|
534
760
|
}
|
|
535
761
|
|
|
536
762
|
function resolvePolicy(options: ProxyResolutionOptions): ProviderProxyPolicy | undefined {
|
|
@@ -544,10 +770,198 @@ function resolvePolicy(options: ProxyResolutionOptions): ProviderProxyPolicy | u
|
|
|
544
770
|
return undefined;
|
|
545
771
|
}
|
|
546
772
|
|
|
547
|
-
function
|
|
548
|
-
return
|
|
549
|
-
|
|
550
|
-
|
|
773
|
+
function isRegistryVendor(name: string | undefined): name is ProxyVendorName {
|
|
774
|
+
return name === "smartproxy" || name === "nodemaven";
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Ordered list of SDK-native proxy vendors declared by the policy. `providers`
|
|
779
|
+
* takes precedence over the legacy singular `provider`; the platform default
|
|
780
|
+
* env is the final fallback. Non-registry names (decodo/custom) are dropped so
|
|
781
|
+
* an all-static chain falls through to the legacy env-URL path unchanged.
|
|
782
|
+
*/
|
|
783
|
+
export function resolveVendorChain(policy: ProviderProxyPolicy): ProxyVendorName[] {
|
|
784
|
+
const declared: (ProviderProxyProvider | undefined)[] = policy.providers?.length
|
|
785
|
+
? policy.providers
|
|
786
|
+
: [policy.provider ?? envDefaultProvider()];
|
|
787
|
+
const chain: ProxyVendorName[] = [];
|
|
788
|
+
for (const name of declared) {
|
|
789
|
+
if (isRegistryVendor(name) && !chain.includes(name)) {
|
|
790
|
+
chain.push(name);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return chain;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function envDefaultProvider(): ProviderProxyProvider | undefined {
|
|
797
|
+
const raw = process.env[DEFAULT_PROXY_PROVIDER_ENV]?.trim().toLowerCase();
|
|
798
|
+
return (raw as ProviderProxyProvider | undefined) ?? undefined;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function vendorHasCredentials(vendor: ProxyVendorName): boolean {
|
|
802
|
+
if (vendor === "nodemaven") return hasNodemavenCredentials();
|
|
803
|
+
return Boolean(process.env[SMARTPROXY_APP_KEY_ENV]?.trim());
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function missingCredentialEnv(vendor: ProxyVendorName): string {
|
|
807
|
+
return vendor === "nodemaven" ? "APIFUSE__PROXY__NODEMAVEN_USERNAME" : SMARTPROXY_APP_KEY_ENV;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function vendorPoolSize(vendor: ProxyVendorName, policy: ProviderProxyPolicy): number {
|
|
811
|
+
return vendor === "nodemaven" ? nodemavenPoolSize(policy) : resolveSmartproxyPoolSize(policy);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Total attempt span across a policy's vendor chain — the sum of each vendor's
|
|
816
|
+
* pool size. Transports use this so successive attempts rotate a vendor's pool
|
|
817
|
+
* and then fail over to the next vendor via the flat attempt index. With one
|
|
818
|
+
* vendor this equals that vendor's pool size (today's behaviour).
|
|
819
|
+
*/
|
|
820
|
+
export function resolvePolicyProxyPoolSpan(policy: ProviderProxyPolicy): number {
|
|
821
|
+
const chain = resolveVendorChain(policy);
|
|
822
|
+
if (chain.length === 0) return resolveSmartproxyPoolSize(policy);
|
|
823
|
+
return chain.reduce((sum, vendor) => sum + vendorPoolSize(vendor, policy), 0);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function vendorMaxPoolSize(vendor: ProxyVendorName): number {
|
|
827
|
+
return vendor === "nodemaven" ? NODEMAVEN_MAX_POOL_SIZE : SMARTPROXY_MAX_POOL_SIZE;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Absolute upper bound on a chain's attempt span — the sum of each vendor's
|
|
832
|
+
* *maximum* pool size. Unlike `resolvePolicyProxyPoolSpan` (the configured
|
|
833
|
+
* span), this backstop is independent of `session.poolSize`, so it never
|
|
834
|
+
* truncates a legitimately large pool below the point where the flat attempt
|
|
835
|
+
* index would cross into the next vendor (e.g. a 50-slot NodeMaven pool).
|
|
836
|
+
*/
|
|
837
|
+
export function maxPolicyProxyPoolSpan(policy: ProviderProxyPolicy): number {
|
|
838
|
+
const chain = resolveVendorChain(policy);
|
|
839
|
+
if (chain.length === 0) return SMARTPROXY_MAX_POOL_SIZE;
|
|
840
|
+
return chain.reduce((sum, vendor) => sum + vendorMaxPoolSize(vendor), 0);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const UNSAFE_TRANSPORT_RETRY_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE", "TRACE"]);
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Transport-retry attempt cap for a policy-managed request. A transport failure
|
|
847
|
+
* rotates the flat attempt index onto the *next* endpoint (and, once the index
|
|
848
|
+
* passes the primary vendor's pool span, the *next vendor*), so the cap must be
|
|
849
|
+
* the chain's full pool span for failover to reach the fallback vendor — the
|
|
850
|
+
* per-endpoint retry budget (default 3) never gets there.
|
|
851
|
+
*
|
|
852
|
+
* The span only widens beyond the caller's retry budget when ALL hold:
|
|
853
|
+
* - the request is policy-allocator managed (not a caller-supplied proxy URL);
|
|
854
|
+
* - the caller did NOT pin an explicit retry policy — `HttpRetryOptions.attempts`
|
|
855
|
+
* is the documented total-attempt ceiling and must be honoured verbatim;
|
|
856
|
+
* - the method is safe/idempotent — an unsafe request must never be duplicated
|
|
857
|
+
* across the pool even if some framework default would allow it;
|
|
858
|
+
* - the policy resolves a non-empty *registry* vendor chain (smartproxy /
|
|
859
|
+
* nodemaven). Static vendors (custom / decodo) and credential-less policies
|
|
860
|
+
* resolve no allocator pool, so every attempt would hit the same endpoint
|
|
861
|
+
* with no possible crossover — they keep the retry budget.
|
|
862
|
+
*
|
|
863
|
+
* The widened cap is bounded by the chain's true maximum span (sum of each
|
|
864
|
+
* vendor's max pool size), so a large NodeMaven pool (≤50) stays reachable and
|
|
865
|
+
* a pathological chain can never spin unbounded.
|
|
866
|
+
*/
|
|
867
|
+
/**
|
|
868
|
+
* True when a policy request is in *implicit chain-rotation* mode: successive
|
|
869
|
+
* transport attempts rotate the flat index across the concatenated vendor pool
|
|
870
|
+
* spans (and, past the primary vendor's span, into the fallback vendor). This is
|
|
871
|
+
* the ONLY mode in which the transport loop widens its attempt cap AND
|
|
872
|
+
* de-duplicates repeated endpoints — the two behaviours must share one predicate
|
|
873
|
+
* so they never diverge. It holds when ALL of the widening conditions hold:
|
|
874
|
+
* - the request is policy-allocator managed (not a caller-supplied proxy URL);
|
|
875
|
+
* - the caller did NOT pin an explicit retry policy — its `attempts` ceiling is
|
|
876
|
+
* the documented contract and must be honoured verbatim against whatever
|
|
877
|
+
* endpoint each attempt resolves (even a repeated one), so no de-duplication;
|
|
878
|
+
* - the method is safe/idempotent — an unsafe request is never duplicated;
|
|
879
|
+
* - the policy resolves a non-empty registry vendor chain (smartproxy /
|
|
880
|
+
* nodemaven). Static vendors (custom / decodo) resolve the same URL every
|
|
881
|
+
* attempt, so there is nothing to rotate or de-duplicate.
|
|
882
|
+
*/
|
|
883
|
+
export function policyRotatesTransportVendorChain(input: {
|
|
884
|
+
policy: ProviderProxyPolicy | undefined;
|
|
885
|
+
usesPolicyAllocator: boolean;
|
|
886
|
+
explicitRetry: boolean;
|
|
887
|
+
method: string;
|
|
888
|
+
}): boolean {
|
|
889
|
+
if (!input.usesPolicyAllocator || !input.policy || input.explicitRetry) {
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
892
|
+
if (UNSAFE_TRANSPORT_RETRY_METHODS.has(input.method.toUpperCase())) {
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
return resolveVendorChain(input.policy).length > 0;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
export function resolvePolicyTransportAttemptCap(input: {
|
|
899
|
+
policy: ProviderProxyPolicy | undefined;
|
|
900
|
+
usesPolicyAllocator: boolean;
|
|
901
|
+
retryAttempts: number;
|
|
902
|
+
explicitRetry: boolean;
|
|
903
|
+
method: string;
|
|
904
|
+
}): number {
|
|
905
|
+
const budget = Math.max(1, Math.floor(input.retryAttempts));
|
|
906
|
+
if (
|
|
907
|
+
!policyRotatesTransportVendorChain({
|
|
908
|
+
policy: input.policy,
|
|
909
|
+
usesPolicyAllocator: input.usesPolicyAllocator,
|
|
910
|
+
explicitRetry: input.explicitRetry,
|
|
911
|
+
method: input.method,
|
|
912
|
+
})
|
|
913
|
+
) {
|
|
914
|
+
return budget;
|
|
915
|
+
}
|
|
916
|
+
const span = Math.min(maxPolicyProxyPoolSpan(input.policy as ProviderProxyPolicy), resolvePolicyProxyPoolSpan(input.policy as ProviderProxyPolicy));
|
|
917
|
+
return Math.max(budget, span);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* A registry vendor chain (smartproxy/nodemaven) resolves a potentially
|
|
922
|
+
* *different* endpoint per flat attempt index, so a transport retry should
|
|
923
|
+
* advance across endpoints and de-duplicate once the chain stops yielding new
|
|
924
|
+
* ones. Static/custom/decodo policies (empty registry chain) resolve the *same*
|
|
925
|
+
* URL every attempt by design — retrying that same endpoint is intended, so the
|
|
926
|
+
* transport loop must not de-duplicate them.
|
|
927
|
+
*/
|
|
928
|
+
export function policyResolvesRegistryVendorChain(
|
|
929
|
+
policy: ProviderProxyPolicy | undefined,
|
|
930
|
+
): boolean {
|
|
931
|
+
return Boolean(policy) && resolveVendorChain(policy as ProviderProxyPolicy).length > 0;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/** Map a resolved proxy source label to the vendor that served it. */
|
|
935
|
+
export function vendorFromResolvedSource(
|
|
936
|
+
source: ResolvedProxyConfig["source"],
|
|
937
|
+
): ProxyVendorName | undefined {
|
|
938
|
+
if (source === "nodemaven-gateway") return "nodemaven";
|
|
939
|
+
if (source === "smartproxy-allocator") return "smartproxy";
|
|
940
|
+
return undefined;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function normalizeAttemptIndex(attempt: number | undefined): number {
|
|
944
|
+
return Number.isFinite(attempt) ? Math.max(0, Math.floor(attempt as number)) : 0;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Map a flat attempt index into (vendorIndex, poolIndex) by concatenating each
|
|
949
|
+
* vendor's pool space in chain order. With a single vendor this reduces to
|
|
950
|
+
* `attempt % poolSize`, preserving today's behaviour exactly.
|
|
951
|
+
*/
|
|
952
|
+
export function mapFlatAttempt(
|
|
953
|
+
flat: number,
|
|
954
|
+
sizes: readonly number[],
|
|
955
|
+
): { vendorIndex: number; poolIndex: number } {
|
|
956
|
+
let cursor = flat;
|
|
957
|
+
for (let vendorIndex = 0; vendorIndex < sizes.length; vendorIndex++) {
|
|
958
|
+
const size = Math.max(1, sizes[vendorIndex] ?? 1);
|
|
959
|
+
if (cursor < size) {
|
|
960
|
+
return { vendorIndex, poolIndex: cursor };
|
|
961
|
+
}
|
|
962
|
+
cursor -= size;
|
|
963
|
+
}
|
|
964
|
+
return { vendorIndex: 0, poolIndex: 0 };
|
|
551
965
|
}
|
|
552
966
|
|
|
553
967
|
function resolveSmartproxyCountry(policy: ProviderProxyPolicy): string | undefined {
|
|
@@ -591,10 +1005,12 @@ function buildSmartproxyCacheKey(
|
|
|
591
1005
|
policy: ProviderProxyPolicy,
|
|
592
1006
|
affinityKey: string | undefined,
|
|
593
1007
|
lifetimeMinutes: number,
|
|
1008
|
+
protocol: ProxyProtocol,
|
|
594
1009
|
): string {
|
|
595
1010
|
const poolSize = resolveSmartproxyPoolSize(policy);
|
|
596
1011
|
return JSON.stringify({
|
|
597
1012
|
provider: "smartproxy",
|
|
1013
|
+
protocol,
|
|
598
1014
|
country: resolveSmartproxyCountry(policy),
|
|
599
1015
|
affinity: policy.session?.affinity ?? "request",
|
|
600
1016
|
affinityKey: (policy.session?.affinity ?? "request") === "request" ? undefined : affinityKey,
|
|
@@ -608,8 +1024,9 @@ async function allocateSmartproxy(
|
|
|
608
1024
|
appKey: string,
|
|
609
1025
|
lifetimeMinutes: number,
|
|
610
1026
|
affinityKey: string | undefined,
|
|
1027
|
+
protocol: ProxyProtocol,
|
|
611
1028
|
): Promise<SmartproxyAllocationResult> {
|
|
612
|
-
const cacheKey = buildSmartproxyCacheKey(policy, affinityKey, lifetimeMinutes);
|
|
1029
|
+
const cacheKey = buildSmartproxyCacheKey(policy, affinityKey, lifetimeMinutes, protocol);
|
|
613
1030
|
const startedAt = Date.now();
|
|
614
1031
|
const now = startedAt;
|
|
615
1032
|
const invalidatedUntil = invalidatedProxyKeys.get(cacheKey) ?? 0;
|
|
@@ -617,7 +1034,7 @@ async function allocateSmartproxy(
|
|
|
617
1034
|
const cached = proxyCache.get(cacheKey);
|
|
618
1035
|
if (!skipCached && cached && isFresh(cached, now)) {
|
|
619
1036
|
if (shouldSoftRefresh(cached, now)) {
|
|
620
|
-
void refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes);
|
|
1037
|
+
void refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, protocol);
|
|
621
1038
|
return {
|
|
622
1039
|
pool: cached,
|
|
623
1040
|
telemetry: telemetryForPool(cached, "soft_stale_refresh", startedAt, {
|
|
@@ -653,6 +1070,7 @@ async function allocateSmartproxy(
|
|
|
653
1070
|
appKey,
|
|
654
1071
|
lifetimeMinutes,
|
|
655
1072
|
startedAt,
|
|
1073
|
+
protocol,
|
|
656
1074
|
).finally(() => {
|
|
657
1075
|
proxyInflight.delete(cacheKey);
|
|
658
1076
|
});
|
|
@@ -691,11 +1109,20 @@ async function refreshSmartproxyPool(
|
|
|
691
1109
|
policy: ProviderProxyPolicy,
|
|
692
1110
|
appKey: string,
|
|
693
1111
|
lifetimeMinutes: number,
|
|
1112
|
+
protocol: ProxyProtocol,
|
|
694
1113
|
): Promise<void> {
|
|
695
1114
|
try {
|
|
696
|
-
await allocateSmartproxyShared(
|
|
697
|
-
|
|
698
|
-
|
|
1115
|
+
await allocateSmartproxyShared(
|
|
1116
|
+
cacheKey,
|
|
1117
|
+
policy,
|
|
1118
|
+
appKey,
|
|
1119
|
+
lifetimeMinutes,
|
|
1120
|
+
Date.now(),
|
|
1121
|
+
protocol,
|
|
1122
|
+
{
|
|
1123
|
+
background: true,
|
|
1124
|
+
},
|
|
1125
|
+
);
|
|
699
1126
|
} catch {
|
|
700
1127
|
// Soft refresh is opportunistic; current fresh pool remains usable.
|
|
701
1128
|
}
|
|
@@ -707,6 +1134,7 @@ async function allocateSmartproxyShared(
|
|
|
707
1134
|
appKey: string,
|
|
708
1135
|
lifetimeMinutes: number,
|
|
709
1136
|
startedAt: number,
|
|
1137
|
+
protocol: ProxyProtocol,
|
|
710
1138
|
options: { background?: boolean } = {},
|
|
711
1139
|
): Promise<SmartproxyAllocationResult> {
|
|
712
1140
|
const redis = getProxyRedis();
|
|
@@ -717,7 +1145,7 @@ async function allocateSmartproxyShared(
|
|
|
717
1145
|
appKey,
|
|
718
1146
|
lifetimeMinutes,
|
|
719
1147
|
startedAt,
|
|
720
|
-
{ cacheStatus: "allocator" },
|
|
1148
|
+
{ cacheStatus: "allocator", protocol },
|
|
721
1149
|
);
|
|
722
1150
|
}
|
|
723
1151
|
|
|
@@ -742,6 +1170,7 @@ async function allocateSmartproxyShared(
|
|
|
742
1170
|
cacheStatus: options.background ? "soft_stale_refresh" : "allocator",
|
|
743
1171
|
redis,
|
|
744
1172
|
poolKey,
|
|
1173
|
+
protocol,
|
|
745
1174
|
},
|
|
746
1175
|
);
|
|
747
1176
|
} finally {
|
|
@@ -897,10 +1326,17 @@ async function allocateAndStoreSmartproxyPool(
|
|
|
897
1326
|
cacheStatus: ProxyCacheStatus;
|
|
898
1327
|
redis?: ProxyRedisClient;
|
|
899
1328
|
poolKey?: string;
|
|
1329
|
+
protocol: ProxyProtocol;
|
|
900
1330
|
},
|
|
901
1331
|
): Promise<SmartproxyAllocationResult> {
|
|
902
1332
|
const poolSize = resolveSmartproxyPoolSize(policy);
|
|
903
|
-
const allocatorUrl = buildSmartproxyAllocatorUrl(
|
|
1333
|
+
const allocatorUrl = buildSmartproxyAllocatorUrl(
|
|
1334
|
+
policy,
|
|
1335
|
+
appKey,
|
|
1336
|
+
lifetimeMinutes,
|
|
1337
|
+
poolSize,
|
|
1338
|
+
options.protocol,
|
|
1339
|
+
);
|
|
904
1340
|
const allocatorStartedAt = Date.now();
|
|
905
1341
|
const allocatorDeadlineAt = allocatorStartedAt + smartproxyAllocatorDeadlineMs();
|
|
906
1342
|
let allocation: SmartproxyAllocatorSuccess | undefined;
|
|
@@ -914,6 +1350,7 @@ async function allocateAndStoreSmartproxyPool(
|
|
|
914
1350
|
allocatorUrl,
|
|
915
1351
|
attempt,
|
|
916
1352
|
allocatorDeadlineAt,
|
|
1353
|
+
options.protocol,
|
|
917
1354
|
);
|
|
918
1355
|
if (attemptResult.ok) {
|
|
919
1356
|
allocation = attemptResult;
|
|
@@ -1034,6 +1471,7 @@ async function fetchSmartproxyAllocatorAttempt(
|
|
|
1034
1471
|
allocatorUrl: string,
|
|
1035
1472
|
attempt: number,
|
|
1036
1473
|
deadlineAt: number,
|
|
1474
|
+
protocol: ProxyProtocol,
|
|
1037
1475
|
): Promise<SmartproxyAllocatorAttemptResult> {
|
|
1038
1476
|
const { controller, dispose } = createDeadlineAbortController(deadlineAt);
|
|
1039
1477
|
let response: Response;
|
|
@@ -1077,7 +1515,7 @@ async function fetchSmartproxyAllocatorAttempt(
|
|
|
1077
1515
|
};
|
|
1078
1516
|
}
|
|
1079
1517
|
|
|
1080
|
-
const urls = parseSmartproxyAllocatorProxies(body);
|
|
1518
|
+
const urls = parseSmartproxyAllocatorProxies(body, protocol);
|
|
1081
1519
|
const bodyClass = classifySmartproxyAllocatorBody(body, urls);
|
|
1082
1520
|
if (urls.length === 0) {
|
|
1083
1521
|
return {
|
|
@@ -1117,18 +1555,27 @@ function smartproxyAllocatorFailureMessage(
|
|
|
1117
1555
|
return "Smartproxy allocator response did not contain a usable proxy endpoint.";
|
|
1118
1556
|
}
|
|
1119
1557
|
|
|
1558
|
+
// Smartproxy get-ip-v3 `protocol` param: 1 = HTTP. The SOCKS5 value ("2") is a
|
|
1559
|
+
// best-effort mapping pending live vendor confirmation; http is the default and
|
|
1560
|
+
// the only value exercised in production today.
|
|
1561
|
+
const SMARTPROXY_PROTOCOL_PARAM: Record<ProxyProtocol, string> = {
|
|
1562
|
+
http: "1",
|
|
1563
|
+
socks5: "2",
|
|
1564
|
+
};
|
|
1565
|
+
|
|
1120
1566
|
function buildSmartproxyAllocatorUrl(
|
|
1121
1567
|
policy: ProviderProxyPolicy,
|
|
1122
1568
|
appKey: string,
|
|
1123
1569
|
lifetimeMinutes: number,
|
|
1124
1570
|
poolSize: number,
|
|
1571
|
+
protocol: ProxyProtocol,
|
|
1125
1572
|
): string {
|
|
1126
1573
|
const params = new URLSearchParams({
|
|
1127
1574
|
app_key: appKey,
|
|
1128
1575
|
pt: "9",
|
|
1129
1576
|
num: String(poolSize),
|
|
1130
1577
|
life: String(lifetimeMinutes),
|
|
1131
|
-
protocol:
|
|
1578
|
+
protocol: SMARTPROXY_PROTOCOL_PARAM[protocol],
|
|
1132
1579
|
format: "txt",
|
|
1133
1580
|
lb: "\\n",
|
|
1134
1581
|
});
|
|
@@ -1141,7 +1588,8 @@ function buildSmartproxyAllocatorUrl(
|
|
|
1141
1588
|
return `https://api.smartproxy.org/web_v1/ip/get-ip-v3?${params.toString()}`;
|
|
1142
1589
|
}
|
|
1143
1590
|
|
|
1144
|
-
function parseSmartproxyAllocatorProxies(body: string): string[] {
|
|
1591
|
+
function parseSmartproxyAllocatorProxies(body: string, protocol: ProxyProtocol): string[] {
|
|
1592
|
+
const scheme = protocol === "socks5" ? "socks5" : "http";
|
|
1145
1593
|
const trimmed = body.trim();
|
|
1146
1594
|
if (!trimmed) {
|
|
1147
1595
|
return [];
|
|
@@ -1162,7 +1610,7 @@ function parseSmartproxyAllocatorProxies(body: string): string[] {
|
|
|
1162
1610
|
"port" in item && (typeof item.port === "string" || typeof item.port === "number")
|
|
1163
1611
|
? item.port
|
|
1164
1612
|
: "";
|
|
1165
|
-
return ip && port ?
|
|
1613
|
+
return ip && port ? `${scheme}://${ip}:${port}` : null;
|
|
1166
1614
|
})
|
|
1167
1615
|
.filter((url): url is string => url !== null);
|
|
1168
1616
|
}
|
|
@@ -1174,7 +1622,7 @@ function parseSmartproxyAllocatorProxies(body: string): string[] {
|
|
|
1174
1622
|
.split(/\r?\n/)
|
|
1175
1623
|
.map((item) => item.trim())
|
|
1176
1624
|
.filter((item) => /^\d{1,3}(?:\.\d{1,3}){3}:\d{2,5}$/.test(item))
|
|
1177
|
-
.map((line) =>
|
|
1625
|
+
.map((line) => `${scheme}://${line}`);
|
|
1178
1626
|
}
|
|
1179
1627
|
|
|
1180
1628
|
function classifySmartproxyAllocatorBody(
|
|
@@ -1207,12 +1655,17 @@ function markSmartproxyCacheInvalidated(options: ProxyResolutionOptions = {}): s
|
|
|
1207
1655
|
if (!policy || policy.mode === "disabled") {
|
|
1208
1656
|
return undefined;
|
|
1209
1657
|
}
|
|
1210
|
-
if (
|
|
1658
|
+
if (!resolveVendorChain(policy).includes("smartproxy")) {
|
|
1211
1659
|
return undefined;
|
|
1212
1660
|
}
|
|
1213
1661
|
|
|
1214
1662
|
const lifetimeMinutes = resolveSmartproxyLifetime(policy);
|
|
1215
|
-
const cacheKey = buildSmartproxyCacheKey(
|
|
1663
|
+
const cacheKey = buildSmartproxyCacheKey(
|
|
1664
|
+
policy,
|
|
1665
|
+
options.affinityKey,
|
|
1666
|
+
lifetimeMinutes,
|
|
1667
|
+
options.protocol ?? VENDOR_DEFAULT_PROTOCOL.smartproxy,
|
|
1668
|
+
);
|
|
1216
1669
|
invalidatedProxyKeys.set(cacheKey, Date.now() + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS);
|
|
1217
1670
|
proxyCache.delete(cacheKey);
|
|
1218
1671
|
proxyInflight.delete(cacheKey);
|