@apifuse/provider-sdk 2.1.0-beta.6 → 2.1.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 +15 -0
- package/bin/apifuse-perf.ts +18 -9
- package/dist/ceremonies/index.d.ts +41 -0
- package/dist/ceremonies/index.js +490 -0
- package/dist/choice-token.d.ts +24 -0
- package/dist/choice-token.js +74 -0
- package/dist/cli/commands.d.ts +10 -0
- package/dist/cli/commands.js +80 -0
- package/dist/cli/create.d.ts +47 -0
- package/dist/cli/create.js +762 -0
- package/dist/config/loader.d.ts +107 -0
- package/dist/config/loader.js +935 -0
- package/dist/contract-json.d.ts +9 -0
- package/dist/contract-json.js +51 -0
- package/dist/contract-serialization.d.ts +4 -0
- package/dist/contract-serialization.js +78 -0
- package/dist/contract-types.d.ts +49 -0
- package/dist/contract-types.js +1 -0
- package/dist/contract.d.ts +6 -0
- package/dist/contract.js +155 -0
- package/dist/define.d.ts +97 -0
- package/dist/define.js +1320 -0
- package/dist/dev.d.ts +9 -0
- package/dist/dev.js +15 -0
- package/dist/errors.d.ts +59 -0
- package/dist/errors.js +97 -0
- package/dist/i18n/catalog.d.ts +29 -0
- package/dist/i18n/catalog.js +159 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.js +2 -0
- package/dist/i18n/keys.d.ts +10 -0
- package/dist/i18n/keys.js +34 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +37 -0
- package/dist/lint.d.ts +73 -0
- package/dist/lint.js +702 -0
- package/dist/observability.d.ts +5 -0
- package/dist/observability.js +39 -0
- package/dist/provider.d.ts +9 -0
- package/dist/provider.js +8 -0
- package/dist/public-schema-field-lint.d.ts +2 -0
- package/dist/public-schema-field-lint.js +158 -0
- package/dist/recipes/gov-api.d.ts +19 -0
- package/dist/recipes/gov-api.js +72 -0
- package/dist/recipes/rest-api.d.ts +21 -0
- package/dist/recipes/rest-api.js +115 -0
- package/dist/runtime/auth-flow.d.ts +14 -0
- package/dist/runtime/auth-flow.js +44 -0
- package/dist/runtime/browser.d.ts +25 -0
- package/dist/runtime/browser.js +1034 -0
- package/dist/runtime/cache.d.ts +10 -0
- package/dist/runtime/cache.js +372 -0
- package/dist/runtime/choice.d.ts +15 -0
- package/dist/runtime/choice.js +435 -0
- package/dist/runtime/credential.d.ts +8 -0
- package/dist/runtime/credential.js +61 -0
- package/dist/runtime/env.d.ts +2 -0
- package/dist/runtime/env.js +10 -0
- package/dist/runtime/executor.d.ts +16 -0
- package/dist/runtime/executor.js +51 -0
- package/dist/runtime/http.d.ts +8 -0
- package/dist/runtime/http.js +706 -0
- package/dist/runtime/insights.d.ts +9 -0
- package/dist/runtime/insights.js +324 -0
- package/dist/runtime/instrumentation.d.ts +8 -0
- package/dist/runtime/instrumentation.js +269 -0
- package/dist/runtime/key-derivation.d.ts +24 -0
- package/dist/runtime/key-derivation.js +73 -0
- package/dist/runtime/keyring.d.ts +25 -0
- package/dist/runtime/keyring.js +93 -0
- package/dist/runtime/namespace.d.ts +9 -0
- package/dist/runtime/namespace.js +19 -0
- package/dist/runtime/otlp.d.ts +39 -0
- package/dist/runtime/otlp.js +103 -0
- package/dist/runtime/perf.d.ts +12 -0
- package/dist/runtime/perf.js +52 -0
- package/dist/runtime/prevalidate.d.ts +12 -0
- package/dist/runtime/prevalidate.js +173 -0
- package/dist/runtime/provider.d.ts +2 -0
- package/dist/runtime/provider.js +11 -0
- package/dist/runtime/proxy-errors.d.ts +21 -0
- package/dist/runtime/proxy-errors.js +83 -0
- package/dist/runtime/proxy-telemetry.d.ts +8 -0
- package/dist/runtime/proxy-telemetry.js +174 -0
- package/dist/runtime/redis.d.ts +17 -0
- package/dist/runtime/redis.js +82 -0
- package/dist/runtime/request-options.d.ts +3 -0
- package/dist/runtime/request-options.js +42 -0
- package/dist/runtime/state.d.ts +17 -0
- package/dist/runtime/state.js +344 -0
- package/dist/runtime/stealth.d.ts +18 -0
- package/dist/runtime/stealth.js +834 -0
- package/dist/runtime/stt.d.ts +22 -0
- package/dist/runtime/stt.js +480 -0
- package/dist/runtime/trace.d.ts +26 -0
- package/dist/runtime/trace.js +142 -0
- package/dist/runtime/waterfall.d.ts +12 -0
- package/dist/runtime/waterfall.js +147 -0
- package/dist/schema.d.ts +74 -0
- package/dist/schema.js +243 -0
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +2 -0
- package/dist/server/serve.d.ts +64 -0
- package/dist/server/serve.js +1110 -0
- package/dist/server/types.d.ts +136 -0
- package/dist/server/types.js +86 -0
- package/dist/stealth/profiles.d.ts +4 -0
- package/dist/stealth/profiles.js +259 -0
- package/dist/stream.d.ts +44 -0
- package/dist/stream.js +151 -0
- package/dist/testing/helpers.d.ts +23 -0
- package/dist/testing/helpers.js +95 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/run.d.ts +34 -0
- package/dist/testing/run.js +303 -0
- package/dist/types.d.ts +1326 -0
- package/dist/types.js +61 -0
- package/dist/utils/date.d.ts +6 -0
- package/dist/utils/date.js +101 -0
- package/dist/utils/parse.d.ts +16 -0
- package/dist/utils/parse.js +51 -0
- package/dist/utils/text.d.ts +4 -0
- package/dist/utils/text.js +14 -0
- package/dist/utils/transform.d.ts +8 -0
- package/dist/utils/transform.js +48 -0
- package/package.json +109 -107
- package/src/runtime/stealth.ts +8 -1
- package/src/types.ts +2 -0
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import Redis from "ioredis";
|
|
5
|
+
export const SMARTPROXY_APP_KEY_ENV = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
|
|
6
|
+
export const SMARTPROXY_MAX_LIFETIME_MINUTES = 2000;
|
|
7
|
+
export const DEFAULT_SMARTPROXY_POOL_SIZE = 20;
|
|
8
|
+
export const SMARTPROXY_MAX_POOL_SIZE = 20;
|
|
9
|
+
export const DEFAULT_PROXY_PROVIDER_ENV = "APIFUSE__PROXY__PROVIDER";
|
|
10
|
+
export const DEFAULT_PROXY_COUNTRY_ENV = "APIFUSE__PROXY__DEFAULT_COUNTRY";
|
|
11
|
+
export const DEFAULT_PROXY_LIFETIME_ENV = "APIFUSE__PROXY__DEFAULT_LIFETIME_MINUTES";
|
|
12
|
+
export const PROVIDER_CACHE_REDIS_URL_ENV = "APIFUSE__PROVIDER__CACHE_REDIS_URL";
|
|
13
|
+
export const PROVIDER_STATE_REDIS_URL_ENV = "APIFUSE__PROVIDER__STATE_REDIS_URL";
|
|
14
|
+
export const REDIS_URL_ENV = "APIFUSE__REDIS__URL";
|
|
15
|
+
export class ProxyResolutionError extends Error {
|
|
16
|
+
code;
|
|
17
|
+
telemetry;
|
|
18
|
+
constructor(code, message, options) {
|
|
19
|
+
super(message, options);
|
|
20
|
+
this.name = "ProxyResolutionError";
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.telemetry = options?.telemetry;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const proxyCache = new Map();
|
|
26
|
+
const proxyInflight = new Map();
|
|
27
|
+
const invalidatedProxyKeys = new Map();
|
|
28
|
+
const redisClients = new Map();
|
|
29
|
+
let proxyRedisForTests;
|
|
30
|
+
let smartproxyAllocatorDeadlineMsForTests;
|
|
31
|
+
const PROXY_CACHE_PREFIX = "apifuse:proxy:smartproxy:v1";
|
|
32
|
+
const REDIS_TIMEOUT_MS = 150;
|
|
33
|
+
const SMARTPROXY_LOCK_TTL_MS = 10_000;
|
|
34
|
+
const SMARTPROXY_LOCK_POLL_MAX_MS = 9_000;
|
|
35
|
+
const SMARTPROXY_DEADLINE_MARGIN_MS = 1_000;
|
|
36
|
+
const SMARTPROXY_INVALIDATION_SKIP_REDIS_MS = 30_000;
|
|
37
|
+
const SMARTPROXY_ALLOCATOR_MAX_ATTEMPTS = 3;
|
|
38
|
+
const SMARTPROXY_ALLOCATOR_DEADLINE_MS = SMARTPROXY_LOCK_TTL_MS - SMARTPROXY_DEADLINE_MARGIN_MS;
|
|
39
|
+
const SMARTPROXY_ALLOCATOR_RETRY_BASE_MS = 25;
|
|
40
|
+
// Smartproxy API extraction returns fresh IP:port candidates; the `life`
|
|
41
|
+
// parameter controls session duration intent, not a hard endpoint lease. Keep
|
|
42
|
+
// successful extractions only briefly to collapse concurrent requests and avoid
|
|
43
|
+
// reusing stale raw CONNECT endpoints as if they were valid for `life` minutes.
|
|
44
|
+
const SMARTPROXY_EXTRACTION_CACHE_TTL_MS = 15_000;
|
|
45
|
+
const SMARTPROXY_EXTRACTION_SOFT_REFRESH_MS = 10_000;
|
|
46
|
+
function redisUrlFromEnv() {
|
|
47
|
+
return (process.env.APIFUSE__PROVIDER__CACHE_REDIS_URL?.trim() ||
|
|
48
|
+
process.env[REDIS_URL_ENV]?.trim() ||
|
|
49
|
+
undefined);
|
|
50
|
+
}
|
|
51
|
+
export function providerCacheRedisUrlFromEnv() {
|
|
52
|
+
return redisUrlFromEnv();
|
|
53
|
+
}
|
|
54
|
+
export function providerStateRedisUrlFromEnv() {
|
|
55
|
+
return (process.env[PROVIDER_STATE_REDIS_URL_ENV]?.trim() ||
|
|
56
|
+
process.env[PROVIDER_CACHE_REDIS_URL_ENV]?.trim() ||
|
|
57
|
+
process.env[REDIS_URL_ENV]?.trim() ||
|
|
58
|
+
undefined);
|
|
59
|
+
}
|
|
60
|
+
/** @internal Test-only hook for exercising shared proxy-cache behavior. */
|
|
61
|
+
export function __setProxyRedisForTests(redis) {
|
|
62
|
+
proxyRedisForTests = redis;
|
|
63
|
+
}
|
|
64
|
+
export function __setSmartproxyAllocatorDeadlineMsForTests(deadlineMs) {
|
|
65
|
+
smartproxyAllocatorDeadlineMsForTests = deadlineMs;
|
|
66
|
+
}
|
|
67
|
+
function getProxyRedis() {
|
|
68
|
+
if (proxyRedisForTests)
|
|
69
|
+
return proxyRedisForTests;
|
|
70
|
+
const redisUrl = redisUrlFromEnv();
|
|
71
|
+
if (!redisUrl)
|
|
72
|
+
return undefined;
|
|
73
|
+
const existing = redisClients.get(redisUrl);
|
|
74
|
+
if (existing)
|
|
75
|
+
return existing;
|
|
76
|
+
const redis = new Redis(redisUrl, {
|
|
77
|
+
connectTimeout: REDIS_TIMEOUT_MS,
|
|
78
|
+
enableOfflineQueue: false,
|
|
79
|
+
lazyConnect: true,
|
|
80
|
+
maxRetriesPerRequest: 0,
|
|
81
|
+
retryStrategy: () => null,
|
|
82
|
+
});
|
|
83
|
+
redis.on("error", () => {
|
|
84
|
+
// Fail-open to allocator/memory; Redis connectivity must not choose direct egress.
|
|
85
|
+
});
|
|
86
|
+
redisClients.set(redisUrl, redis);
|
|
87
|
+
return redis;
|
|
88
|
+
}
|
|
89
|
+
async function withRedisTimeout(operation) {
|
|
90
|
+
let timeoutId;
|
|
91
|
+
try {
|
|
92
|
+
const timeout = new Promise((resolve) => {
|
|
93
|
+
timeoutId = setTimeout(() => resolve(undefined), REDIS_TIMEOUT_MS);
|
|
94
|
+
});
|
|
95
|
+
return await Promise.race([operation().catch(() => undefined), timeout]);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
if (timeoutId)
|
|
99
|
+
clearTimeout(timeoutId);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function redisStatus(redis) {
|
|
103
|
+
return redis.status;
|
|
104
|
+
}
|
|
105
|
+
async function ensureRedisReady(redis) {
|
|
106
|
+
if (redisStatus(redis) === "ready")
|
|
107
|
+
return true;
|
|
108
|
+
if (redisStatus(redis) === "wait" || redisStatus(redis) === "end") {
|
|
109
|
+
const connected = await withRedisTimeout(async () => {
|
|
110
|
+
await redis.connect();
|
|
111
|
+
return true;
|
|
112
|
+
});
|
|
113
|
+
return connected === true && redisStatus(redis) === "ready";
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
function sha256(value) {
|
|
118
|
+
return createHash("sha256").update(value).digest("hex");
|
|
119
|
+
}
|
|
120
|
+
function smartproxyRedisPoolKey(cacheKey) {
|
|
121
|
+
return `${PROXY_CACHE_PREFIX}:pool:${sha256(cacheKey)}`;
|
|
122
|
+
}
|
|
123
|
+
function smartproxyRedisLockKey(cacheKey) {
|
|
124
|
+
return `${PROXY_CACHE_PREFIX}:lock:${sha256(cacheKey)}`;
|
|
125
|
+
}
|
|
126
|
+
function isFresh(pool, now) {
|
|
127
|
+
return pool.expiresAt > now;
|
|
128
|
+
}
|
|
129
|
+
function shouldSoftRefresh(pool, now) {
|
|
130
|
+
return isFresh(pool, now) && pool.refreshAfter <= now;
|
|
131
|
+
}
|
|
132
|
+
function telemetryForPool(pool, cacheStatus, startedAt, extra = {}) {
|
|
133
|
+
const now = Date.now();
|
|
134
|
+
return {
|
|
135
|
+
provider: "smartproxy",
|
|
136
|
+
cacheStatus,
|
|
137
|
+
cacheHit: cacheStatus !== "allocator",
|
|
138
|
+
resolutionMs: Math.max(0, now - startedAt),
|
|
139
|
+
poolAgeMs: Math.max(0, now - pool.allocatedAt),
|
|
140
|
+
poolExpiresInMs: Math.max(0, pool.expiresAt - now),
|
|
141
|
+
attempts: 1,
|
|
142
|
+
...extra,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function telemetryForFailure(cacheStatus, startedAt, extra = {}) {
|
|
146
|
+
return {
|
|
147
|
+
provider: "smartproxy",
|
|
148
|
+
cacheStatus,
|
|
149
|
+
cacheHit: false,
|
|
150
|
+
resolutionMs: Math.max(0, Date.now() - startedAt),
|
|
151
|
+
attempts: 1,
|
|
152
|
+
...extra,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function isRecord(value) {
|
|
156
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
157
|
+
}
|
|
158
|
+
function toProxyDiagnostics(value) {
|
|
159
|
+
if (!isRecord(value))
|
|
160
|
+
return undefined;
|
|
161
|
+
const diagnostics = {};
|
|
162
|
+
for (const [key, item] of Object.entries(value)) {
|
|
163
|
+
if (typeof item === "string" ||
|
|
164
|
+
typeof item === "number" ||
|
|
165
|
+
typeof item === "boolean") {
|
|
166
|
+
diagnostics[key] = item;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return Object.keys(diagnostics).length > 0 ? diagnostics : undefined;
|
|
170
|
+
}
|
|
171
|
+
function safeParseSmartproxyPool(raw) {
|
|
172
|
+
if (!raw)
|
|
173
|
+
return null;
|
|
174
|
+
try {
|
|
175
|
+
const parsed = JSON.parse(raw);
|
|
176
|
+
if (!isRecord(parsed))
|
|
177
|
+
return null;
|
|
178
|
+
const record = parsed;
|
|
179
|
+
if (record.version !== 1 ||
|
|
180
|
+
record.proxyProvider !== "smartproxy" ||
|
|
181
|
+
!Array.isArray(record.urls) ||
|
|
182
|
+
typeof record.allocatedAt !== "number" ||
|
|
183
|
+
typeof record.refreshAfter !== "number" ||
|
|
184
|
+
typeof record.expiresAt !== "number") {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const urls = record.urls.filter((url) => typeof url === "string" && url.startsWith("http"));
|
|
188
|
+
if (urls.length === 0)
|
|
189
|
+
return null;
|
|
190
|
+
return {
|
|
191
|
+
urls,
|
|
192
|
+
allocatedAt: record.allocatedAt,
|
|
193
|
+
refreshAfter: record.refreshAfter,
|
|
194
|
+
expiresAt: record.expiresAt,
|
|
195
|
+
diagnostics: toProxyDiagnostics(record.diagnostics),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function serializeSmartproxyPool(pool) {
|
|
203
|
+
return JSON.stringify({
|
|
204
|
+
version: 1,
|
|
205
|
+
proxyProvider: "smartproxy",
|
|
206
|
+
urls: pool.urls,
|
|
207
|
+
allocatedAt: pool.allocatedAt,
|
|
208
|
+
refreshAfter: pool.refreshAfter,
|
|
209
|
+
expiresAt: pool.expiresAt,
|
|
210
|
+
diagnostics: pool.diagnostics,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function normalizeProxyUrl(url) {
|
|
214
|
+
const normalized = url?.trim();
|
|
215
|
+
return normalized ? applyStickyProxySession(normalized) : undefined;
|
|
216
|
+
}
|
|
217
|
+
function readPositiveIntegerEnv(name) {
|
|
218
|
+
const raw = process.env[name]?.trim();
|
|
219
|
+
if (!raw)
|
|
220
|
+
return undefined;
|
|
221
|
+
if (!/^[1-9]\d*$/.test(raw)) {
|
|
222
|
+
throw new Error(`${name} must be a positive integer`);
|
|
223
|
+
}
|
|
224
|
+
return raw;
|
|
225
|
+
}
|
|
226
|
+
function applyStickyProxySession(proxyUrl) {
|
|
227
|
+
let parsed;
|
|
228
|
+
try {
|
|
229
|
+
parsed = new URL(proxyUrl);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return proxyUrl;
|
|
233
|
+
}
|
|
234
|
+
if (!parsed.hostname || !parsed.username || !parsed.password) {
|
|
235
|
+
return proxyUrl;
|
|
236
|
+
}
|
|
237
|
+
const host = parsed.hostname.toLowerCase();
|
|
238
|
+
if (!host.includes("smartproxy") && !host.includes("decodo")) {
|
|
239
|
+
return proxyUrl;
|
|
240
|
+
}
|
|
241
|
+
const username = decodeURIComponent(parsed.username);
|
|
242
|
+
const sessionId = process.env.APIFUSE__PROXY__SESSION_ID?.trim() || "apifuse-shared";
|
|
243
|
+
const sessionDuration = readPositiveIntegerEnv("APIFUSE__PROXY__SESSION_DURATION");
|
|
244
|
+
const stickyUsername = host.includes("smartproxy")
|
|
245
|
+
? buildSmartproxyUsername(username, sessionId, sessionDuration)
|
|
246
|
+
: buildDecodoUsername(username, sessionId, sessionDuration ?? "60");
|
|
247
|
+
parsed.username = stickyUsername;
|
|
248
|
+
return parsed.toString();
|
|
249
|
+
}
|
|
250
|
+
function buildSmartproxyUsername(username, sessionId, sessionDuration) {
|
|
251
|
+
const parts = username.split("_");
|
|
252
|
+
const configuredLife = parts
|
|
253
|
+
.find((part) => part.startsWith("life-"))
|
|
254
|
+
?.slice("life-".length);
|
|
255
|
+
const baseUsername = parts
|
|
256
|
+
.filter((part) => !part.startsWith("session-") && !part.startsWith("life-"))
|
|
257
|
+
.join("_");
|
|
258
|
+
return `${baseUsername}_session-${sessionId}_life-${sessionDuration ?? configuredLife ?? "60"}`;
|
|
259
|
+
}
|
|
260
|
+
function buildDecodoUsername(username, sessionId, sessionDuration) {
|
|
261
|
+
const withoutSticky = username.replace(/-session-.+-sessionduration-\d+$/, "");
|
|
262
|
+
const baseUsername = withoutSticky.startsWith("user-")
|
|
263
|
+
? withoutSticky
|
|
264
|
+
: `user-${withoutSticky}`;
|
|
265
|
+
return `${baseUsername}-session-${sessionId}-sessionduration-${sessionDuration}`;
|
|
266
|
+
}
|
|
267
|
+
function syncProxyEnv(config) {
|
|
268
|
+
const configProxyUrl = normalizeProxyUrl(config.proxy?.url);
|
|
269
|
+
if (!process.env.APIFUSE__PROXY__URL && configProxyUrl) {
|
|
270
|
+
process.env.APIFUSE__PROXY__URL = configProxyUrl;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
export function resolveProxyConfig(options = {}) {
|
|
274
|
+
const explicitProxyUrl = normalizeProxyUrl(options.proxy);
|
|
275
|
+
if (explicitProxyUrl) {
|
|
276
|
+
return { shouldWarn: false, url: explicitProxyUrl };
|
|
277
|
+
}
|
|
278
|
+
const policy = resolvePolicy(options);
|
|
279
|
+
if (policy?.mode === "disabled") {
|
|
280
|
+
return { shouldWarn: false };
|
|
281
|
+
}
|
|
282
|
+
const legacyProxyRequested = options.upstream?.proxy === true || (!policy && options.upstream?.proxy);
|
|
283
|
+
if (!legacyProxyRequested) {
|
|
284
|
+
return { shouldWarn: false };
|
|
285
|
+
}
|
|
286
|
+
const envProxyUrl = normalizeProxyUrl(process.env.APIFUSE__PROXY__URL);
|
|
287
|
+
if (envProxyUrl) {
|
|
288
|
+
return { shouldWarn: false, url: envProxyUrl };
|
|
289
|
+
}
|
|
290
|
+
const configuredProxyUrl = normalizeProxyUrl(options.apifuseConfig?.proxy?.url);
|
|
291
|
+
if (configuredProxyUrl) {
|
|
292
|
+
return { shouldWarn: false, url: configuredProxyUrl };
|
|
293
|
+
}
|
|
294
|
+
return { shouldWarn: true };
|
|
295
|
+
}
|
|
296
|
+
export async function resolveProxyConfigAsync(options = {}) {
|
|
297
|
+
const explicitProxyUrl = normalizeProxyUrl(options.proxy);
|
|
298
|
+
if (explicitProxyUrl) {
|
|
299
|
+
return { shouldWarn: false, url: explicitProxyUrl, source: "explicit" };
|
|
300
|
+
}
|
|
301
|
+
const policy = resolvePolicy(options);
|
|
302
|
+
if (!policy) {
|
|
303
|
+
return resolveProxyConfig(options);
|
|
304
|
+
}
|
|
305
|
+
if (policy.mode === "disabled") {
|
|
306
|
+
return { shouldWarn: false };
|
|
307
|
+
}
|
|
308
|
+
const provider = resolveProxyProvider(policy);
|
|
309
|
+
if (provider !== "smartproxy") {
|
|
310
|
+
return resolveProxyConfig({
|
|
311
|
+
...options,
|
|
312
|
+
upstream: { proxy: true },
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
const appKey = process.env[SMARTPROXY_APP_KEY_ENV]?.trim();
|
|
316
|
+
if (!appKey) {
|
|
317
|
+
if (policy.mode === "required") {
|
|
318
|
+
throw new ProxyResolutionError("PROXY_REQUIRED", `Smartproxy egress is required but ${SMARTPROXY_APP_KEY_ENV} is not configured.`);
|
|
319
|
+
}
|
|
320
|
+
return { shouldWarn: true };
|
|
321
|
+
}
|
|
322
|
+
const lifetimeMinutes = resolveSmartproxyLifetime(policy);
|
|
323
|
+
try {
|
|
324
|
+
const allocated = await allocateSmartproxy(policy, appKey, lifetimeMinutes, options.affinityKey);
|
|
325
|
+
options.telemetry?.recordProxyResolution(allocated.telemetry);
|
|
326
|
+
const poolIndex = selectProxyPoolIndex(allocated.pool.urls.length, options.proxyAttempt);
|
|
327
|
+
return {
|
|
328
|
+
shouldWarn: false,
|
|
329
|
+
url: allocated.pool.urls[poolIndex],
|
|
330
|
+
source: "smartproxy-allocator",
|
|
331
|
+
diagnostics: {
|
|
332
|
+
...allocated.pool.diagnostics,
|
|
333
|
+
poolSize: allocated.pool.urls.length,
|
|
334
|
+
poolIndex,
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
if (error instanceof ProxyResolutionError && error.telemetry) {
|
|
340
|
+
options.telemetry?.recordProxyResolution(error.telemetry);
|
|
341
|
+
}
|
|
342
|
+
if (policy.mode === "required") {
|
|
343
|
+
throw error instanceof ProxyResolutionError
|
|
344
|
+
? error
|
|
345
|
+
: new ProxyResolutionError("PROXY_ALLOCATION_FAILED", "Smartproxy allocator failed for required proxy egress.", { cause: error });
|
|
346
|
+
}
|
|
347
|
+
return { shouldWarn: true };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function resolvePolicy(options) {
|
|
351
|
+
if (options.proxyPolicy) {
|
|
352
|
+
return options.proxyPolicy;
|
|
353
|
+
}
|
|
354
|
+
const upstreamProxy = options.upstream?.proxy;
|
|
355
|
+
if (upstreamProxy && typeof upstreamProxy === "object") {
|
|
356
|
+
return upstreamProxy;
|
|
357
|
+
}
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
function resolveProxyProvider(policy) {
|
|
361
|
+
return (policy.provider ??
|
|
362
|
+
process.env[DEFAULT_PROXY_PROVIDER_ENV]?.trim().toLowerCase() ??
|
|
363
|
+
"custom");
|
|
364
|
+
}
|
|
365
|
+
function resolveSmartproxyCountry(policy) {
|
|
366
|
+
return (policy.geo?.country ??
|
|
367
|
+
process.env[DEFAULT_PROXY_COUNTRY_ENV]?.trim().toUpperCase() ??
|
|
368
|
+
undefined);
|
|
369
|
+
}
|
|
370
|
+
function resolveSmartproxyLifetime(policy) {
|
|
371
|
+
const configuredLifetime = policy.session?.lifetimeMinutes ??
|
|
372
|
+
readPositiveNumberEnv(DEFAULT_PROXY_LIFETIME_ENV, 30);
|
|
373
|
+
return Math.min(SMARTPROXY_MAX_LIFETIME_MINUTES, Math.max(1, Math.floor(configuredLifetime)));
|
|
374
|
+
}
|
|
375
|
+
function readPositiveNumberEnv(name, fallback) {
|
|
376
|
+
const raw = process.env[name]?.trim();
|
|
377
|
+
if (!raw)
|
|
378
|
+
return fallback;
|
|
379
|
+
const parsed = Number(raw);
|
|
380
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
381
|
+
throw new Error(`${name} must be a positive number`);
|
|
382
|
+
}
|
|
383
|
+
return parsed;
|
|
384
|
+
}
|
|
385
|
+
function resolveSmartproxyPoolSize(policy) {
|
|
386
|
+
return Math.min(SMARTPROXY_MAX_POOL_SIZE, Math.max(1, Math.floor(policy.session?.poolSize ?? DEFAULT_SMARTPROXY_POOL_SIZE)));
|
|
387
|
+
}
|
|
388
|
+
function selectProxyPoolIndex(poolSize, attempt = 0) {
|
|
389
|
+
if (poolSize <= 1) {
|
|
390
|
+
return 0;
|
|
391
|
+
}
|
|
392
|
+
const normalizedAttempt = Number.isFinite(attempt)
|
|
393
|
+
? Math.max(0, Math.floor(attempt))
|
|
394
|
+
: 0;
|
|
395
|
+
return normalizedAttempt % poolSize;
|
|
396
|
+
}
|
|
397
|
+
function buildSmartproxyCacheKey(policy, affinityKey, lifetimeMinutes) {
|
|
398
|
+
const poolSize = resolveSmartproxyPoolSize(policy);
|
|
399
|
+
return JSON.stringify({
|
|
400
|
+
provider: "smartproxy",
|
|
401
|
+
country: resolveSmartproxyCountry(policy),
|
|
402
|
+
affinity: policy.session?.affinity ?? "request",
|
|
403
|
+
affinityKey: (policy.session?.affinity ?? "request") === "request"
|
|
404
|
+
? undefined
|
|
405
|
+
: affinityKey,
|
|
406
|
+
lifetimeMinutes,
|
|
407
|
+
poolSize,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey) {
|
|
411
|
+
const cacheKey = buildSmartproxyCacheKey(policy, affinityKey, lifetimeMinutes);
|
|
412
|
+
const startedAt = Date.now();
|
|
413
|
+
const now = startedAt;
|
|
414
|
+
const invalidatedUntil = invalidatedProxyKeys.get(cacheKey) ?? 0;
|
|
415
|
+
const skipCached = invalidatedUntil > now;
|
|
416
|
+
const cached = proxyCache.get(cacheKey);
|
|
417
|
+
if (!skipCached && cached && isFresh(cached, now)) {
|
|
418
|
+
if (shouldSoftRefresh(cached, now)) {
|
|
419
|
+
void refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes);
|
|
420
|
+
return {
|
|
421
|
+
pool: cached,
|
|
422
|
+
telemetry: telemetryForPool(cached, "soft_stale_refresh", startedAt, {
|
|
423
|
+
refreshes: 1,
|
|
424
|
+
}),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
pool: cached,
|
|
429
|
+
telemetry: telemetryForPool(cached, "memory_hit", startedAt),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
if (!skipCached) {
|
|
433
|
+
const redisResult = await readSmartproxyRedisPool(cacheKey, startedAt);
|
|
434
|
+
if (redisResult)
|
|
435
|
+
return redisResult;
|
|
436
|
+
}
|
|
437
|
+
const existingInflight = proxyInflight.get(cacheKey);
|
|
438
|
+
if (existingInflight) {
|
|
439
|
+
const result = await existingInflight;
|
|
440
|
+
return {
|
|
441
|
+
pool: result.pool,
|
|
442
|
+
telemetry: telemetryForPool(result.pool, "lock_wait", startedAt, {
|
|
443
|
+
lockWaitMs: Math.max(0, Date.now() - startedAt),
|
|
444
|
+
}),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
const promise = allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, startedAt).finally(() => {
|
|
448
|
+
proxyInflight.delete(cacheKey);
|
|
449
|
+
});
|
|
450
|
+
proxyInflight.set(cacheKey, promise);
|
|
451
|
+
const result = await promise;
|
|
452
|
+
invalidatedProxyKeys.delete(cacheKey);
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
async function readSmartproxyRedisPool(cacheKey, startedAt) {
|
|
456
|
+
const redis = getProxyRedis();
|
|
457
|
+
if (!redis || !(await ensureRedisReady(redis)))
|
|
458
|
+
return null;
|
|
459
|
+
const redisStartedAt = Date.now();
|
|
460
|
+
const raw = await withRedisTimeout(() => redis.get(smartproxyRedisPoolKey(cacheKey)));
|
|
461
|
+
const redisReadMs = Math.max(0, Date.now() - redisStartedAt);
|
|
462
|
+
if (typeof raw !== "string")
|
|
463
|
+
return null;
|
|
464
|
+
const pool = safeParseSmartproxyPool(raw);
|
|
465
|
+
if (!pool) {
|
|
466
|
+
await withRedisTimeout(() => redis.del(smartproxyRedisPoolKey(cacheKey)));
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
const now = Date.now();
|
|
470
|
+
if (!isFresh(pool, now))
|
|
471
|
+
return null;
|
|
472
|
+
proxyCache.set(cacheKey, pool);
|
|
473
|
+
return {
|
|
474
|
+
pool,
|
|
475
|
+
telemetry: telemetryForPool(pool, "redis_hit", startedAt, { redisReadMs }),
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
async function refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes) {
|
|
479
|
+
try {
|
|
480
|
+
await allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, Date.now(), {
|
|
481
|
+
background: true,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
// Soft refresh is opportunistic; current fresh pool remains usable.
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
async function allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, startedAt, options = {}) {
|
|
489
|
+
const redis = getProxyRedis();
|
|
490
|
+
if (!redis || !(await ensureRedisReady(redis))) {
|
|
491
|
+
return await allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, startedAt, { cacheStatus: "allocator" });
|
|
492
|
+
}
|
|
493
|
+
const poolKey = smartproxyRedisPoolKey(cacheKey);
|
|
494
|
+
const lockKey = smartproxyRedisLockKey(cacheKey);
|
|
495
|
+
const waitStartedAt = Date.now();
|
|
496
|
+
while (Date.now() - waitStartedAt < SMARTPROXY_LOCK_POLL_MAX_MS) {
|
|
497
|
+
const token = randomUUID();
|
|
498
|
+
const acquired = await withRedisTimeout(() => redis.set(lockKey, token, "PX", SMARTPROXY_LOCK_TTL_MS, "NX"));
|
|
499
|
+
if (acquired === "OK") {
|
|
500
|
+
try {
|
|
501
|
+
return await allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, startedAt, {
|
|
502
|
+
cacheStatus: options.background
|
|
503
|
+
? "soft_stale_refresh"
|
|
504
|
+
: "allocator",
|
|
505
|
+
redis,
|
|
506
|
+
poolKey,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
await releaseSmartproxyRedisLock(redis, lockKey, token);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const redisResult = await readSmartproxyRedisPool(cacheKey, startedAt);
|
|
514
|
+
if (redisResult) {
|
|
515
|
+
return {
|
|
516
|
+
pool: redisResult.pool,
|
|
517
|
+
telemetry: {
|
|
518
|
+
...redisResult.telemetry,
|
|
519
|
+
cacheStatus: "lock_wait",
|
|
520
|
+
cacheHit: true,
|
|
521
|
+
lockWaitMs: Math.max(0, Date.now() - waitStartedAt),
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
const pttl = await withRedisTimeout(() => redis.pttl(lockKey));
|
|
526
|
+
if (typeof pttl === "number" && pttl <= 0) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (Date.now() - startedAt >
|
|
530
|
+
SMARTPROXY_LOCK_POLL_MAX_MS - SMARTPROXY_DEADLINE_MARGIN_MS) {
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
await sleep(Math.min(500, Math.max(50, typeof pttl === "number" ? pttl : 100)));
|
|
534
|
+
}
|
|
535
|
+
throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", "Smartproxy allocator lock did not produce a usable proxy pool before the request deadline.", {
|
|
536
|
+
telemetry: telemetryForFailure("lock_wait", startedAt, {
|
|
537
|
+
lockWaitMs: Math.max(0, Date.now() - waitStartedAt),
|
|
538
|
+
}),
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
async function releaseSmartproxyRedisLock(redis, lockKey, token) {
|
|
542
|
+
await withRedisTimeout(() => redis.eval('if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end', 1, lockKey, token));
|
|
543
|
+
}
|
|
544
|
+
function sleep(ms, signal) {
|
|
545
|
+
if (signal?.aborted)
|
|
546
|
+
return Promise.reject(signal.reason);
|
|
547
|
+
return new Promise((resolve, reject) => {
|
|
548
|
+
let timeout;
|
|
549
|
+
const onAbort = () => {
|
|
550
|
+
clearTimeout(timeout);
|
|
551
|
+
signal?.removeEventListener("abort", onAbort);
|
|
552
|
+
reject(signal?.reason);
|
|
553
|
+
};
|
|
554
|
+
timeout = setTimeout(() => {
|
|
555
|
+
signal?.removeEventListener("abort", onAbort);
|
|
556
|
+
resolve();
|
|
557
|
+
}, ms);
|
|
558
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
function smartproxyAllocatorDeadlineMs() {
|
|
562
|
+
return (smartproxyAllocatorDeadlineMsForTests ?? SMARTPROXY_ALLOCATOR_DEADLINE_MS);
|
|
563
|
+
}
|
|
564
|
+
function createDeadlineAbortController(deadlineAt) {
|
|
565
|
+
const controller = new AbortController();
|
|
566
|
+
const remainingMs = deadlineAt - Date.now();
|
|
567
|
+
if (remainingMs <= 0) {
|
|
568
|
+
controller.abort(new Error("Smartproxy allocator deadline exceeded."));
|
|
569
|
+
return { controller, dispose: () => undefined };
|
|
570
|
+
}
|
|
571
|
+
const timeout = setTimeout(() => {
|
|
572
|
+
controller.abort(new Error("Smartproxy allocator deadline exceeded."));
|
|
573
|
+
}, remainingMs);
|
|
574
|
+
return {
|
|
575
|
+
controller,
|
|
576
|
+
dispose: () => clearTimeout(timeout),
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function smartproxyAllocatorDeadlineFailure(attempt) {
|
|
580
|
+
return {
|
|
581
|
+
ok: false,
|
|
582
|
+
attempt,
|
|
583
|
+
bodyClass: "network_error",
|
|
584
|
+
cause: new Error("Smartproxy allocator deadline exceeded."),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function smartproxyAllocatorDeadlineError() {
|
|
588
|
+
return new Error("Smartproxy allocator deadline exceeded.");
|
|
589
|
+
}
|
|
590
|
+
async function readSmartproxyAllocatorBodyWithDeadline(response, signal) {
|
|
591
|
+
if (signal.aborted) {
|
|
592
|
+
throw signal.reason ?? smartproxyAllocatorDeadlineError();
|
|
593
|
+
}
|
|
594
|
+
return await new Promise((resolve, reject) => {
|
|
595
|
+
let settled = false;
|
|
596
|
+
const cleanup = () => {
|
|
597
|
+
signal.removeEventListener("abort", onAbort);
|
|
598
|
+
};
|
|
599
|
+
const settle = (callback) => {
|
|
600
|
+
if (settled)
|
|
601
|
+
return;
|
|
602
|
+
settled = true;
|
|
603
|
+
cleanup();
|
|
604
|
+
callback();
|
|
605
|
+
};
|
|
606
|
+
const onAbort = () => {
|
|
607
|
+
settle(() => reject(signal.reason ?? smartproxyAllocatorDeadlineError()));
|
|
608
|
+
};
|
|
609
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
610
|
+
try {
|
|
611
|
+
void response.text().then((body) => settle(() => resolve(body)), (error) => settle(() => reject(error)));
|
|
612
|
+
}
|
|
613
|
+
catch (error) {
|
|
614
|
+
settle(() => reject(error));
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, startedAt, options) {
|
|
619
|
+
const poolSize = resolveSmartproxyPoolSize(policy);
|
|
620
|
+
const allocatorUrl = buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize);
|
|
621
|
+
const allocatorStartedAt = Date.now();
|
|
622
|
+
const allocatorDeadlineAt = allocatorStartedAt + smartproxyAllocatorDeadlineMs();
|
|
623
|
+
let allocation;
|
|
624
|
+
let lastFailure;
|
|
625
|
+
for (let attempt = 1; attempt <= SMARTPROXY_ALLOCATOR_MAX_ATTEMPTS; attempt += 1) {
|
|
626
|
+
if (Date.now() >= allocatorDeadlineAt) {
|
|
627
|
+
lastFailure = smartproxyAllocatorDeadlineFailure(attempt);
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
const attemptResult = await fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, allocatorDeadlineAt);
|
|
631
|
+
if (attemptResult.ok) {
|
|
632
|
+
allocation = attemptResult;
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
lastFailure = attemptResult;
|
|
636
|
+
if (attempt < SMARTPROXY_ALLOCATOR_MAX_ATTEMPTS) {
|
|
637
|
+
if (Date.now() >= allocatorDeadlineAt)
|
|
638
|
+
break;
|
|
639
|
+
const { controller, dispose } = createDeadlineAbortController(allocatorDeadlineAt);
|
|
640
|
+
try {
|
|
641
|
+
await sleep(smartproxyAllocatorBackoffMs(attempt), controller.signal);
|
|
642
|
+
}
|
|
643
|
+
catch {
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
finally {
|
|
647
|
+
dispose();
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
const allocatorMs = Math.max(0, Date.now() - allocatorStartedAt);
|
|
652
|
+
const allocatorAttempts = allocation?.attempt ??
|
|
653
|
+
lastFailure?.attempt ??
|
|
654
|
+
SMARTPROXY_ALLOCATOR_MAX_ATTEMPTS;
|
|
655
|
+
const allocatorBodyClass = lastFailure?.bodyClass ?? allocation?.bodyClass ?? "usable_proxy_endpoints";
|
|
656
|
+
const allocatorStatus = lastFailure ? lastFailure.status : allocation?.status;
|
|
657
|
+
if (!allocation) {
|
|
658
|
+
throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", smartproxyAllocatorFailureMessage(lastFailure), {
|
|
659
|
+
cause: lastFailure?.cause,
|
|
660
|
+
telemetry: telemetryForFailure(options.cacheStatus, startedAt, {
|
|
661
|
+
allocatorMs,
|
|
662
|
+
allocatorAttempts,
|
|
663
|
+
allocatorBodyClass,
|
|
664
|
+
...(allocatorStatus === undefined ? {} : { allocatorStatus }),
|
|
665
|
+
attempts: allocatorAttempts,
|
|
666
|
+
}),
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
const urls = allocation.urls;
|
|
670
|
+
const allocatedAt = Date.now();
|
|
671
|
+
const ttlMs = SMARTPROXY_EXTRACTION_CACHE_TTL_MS;
|
|
672
|
+
const refreshAfter = allocatedAt + SMARTPROXY_EXTRACTION_SOFT_REFRESH_MS;
|
|
673
|
+
const result = {
|
|
674
|
+
urls,
|
|
675
|
+
allocatedAt,
|
|
676
|
+
refreshAfter,
|
|
677
|
+
expiresAt: allocatedAt + ttlMs,
|
|
678
|
+
diagnostics: {
|
|
679
|
+
provider: "smartproxy",
|
|
680
|
+
country: resolveSmartproxyCountry(policy) ?? "default",
|
|
681
|
+
lifetimeMinutes,
|
|
682
|
+
affinity: policy.session?.affinity ?? "request",
|
|
683
|
+
rawConnect: true,
|
|
684
|
+
},
|
|
685
|
+
};
|
|
686
|
+
proxyCache.set(cacheKey, result);
|
|
687
|
+
if (options.redis && options.poolKey) {
|
|
688
|
+
const redis = options.redis;
|
|
689
|
+
const poolKey = options.poolKey;
|
|
690
|
+
const redisWriteStartedAt = Date.now();
|
|
691
|
+
await withRedisTimeout(() => redis.set(poolKey, serializeSmartproxyPool(result), "PX", Math.max(1_000, result.expiresAt - Date.now())));
|
|
692
|
+
return {
|
|
693
|
+
pool: result,
|
|
694
|
+
telemetry: telemetryForPool(result, options.cacheStatus, startedAt, {
|
|
695
|
+
allocatorMs,
|
|
696
|
+
allocatorAttempts,
|
|
697
|
+
allocatorBodyClass,
|
|
698
|
+
...(allocatorStatus === undefined ? {} : { allocatorStatus }),
|
|
699
|
+
attempts: allocatorAttempts,
|
|
700
|
+
redisWriteMs: Math.max(0, Date.now() - redisWriteStartedAt),
|
|
701
|
+
}),
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
pool: result,
|
|
706
|
+
telemetry: telemetryForPool(result, options.cacheStatus, startedAt, {
|
|
707
|
+
allocatorMs,
|
|
708
|
+
allocatorAttempts,
|
|
709
|
+
allocatorBodyClass,
|
|
710
|
+
...(allocatorStatus === undefined ? {} : { allocatorStatus }),
|
|
711
|
+
attempts: allocatorAttempts,
|
|
712
|
+
}),
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
async function fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, deadlineAt) {
|
|
716
|
+
const { controller, dispose } = createDeadlineAbortController(deadlineAt);
|
|
717
|
+
let response;
|
|
718
|
+
try {
|
|
719
|
+
response = await fetch(allocatorUrl, {
|
|
720
|
+
headers: { Accept: "application/json,text/plain;q=0.9,*/*;q=0.8" },
|
|
721
|
+
signal: controller.signal,
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
catch (error) {
|
|
725
|
+
dispose();
|
|
726
|
+
return {
|
|
727
|
+
ok: false,
|
|
728
|
+
attempt,
|
|
729
|
+
bodyClass: "network_error",
|
|
730
|
+
cause: error,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
let body;
|
|
734
|
+
try {
|
|
735
|
+
body = await readSmartproxyAllocatorBodyWithDeadline(response, controller.signal);
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
dispose();
|
|
739
|
+
return {
|
|
740
|
+
ok: false,
|
|
741
|
+
attempt,
|
|
742
|
+
status: response.status,
|
|
743
|
+
bodyClass: "network_error",
|
|
744
|
+
cause: error,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
finally {
|
|
748
|
+
dispose();
|
|
749
|
+
}
|
|
750
|
+
if (!response.ok) {
|
|
751
|
+
return {
|
|
752
|
+
ok: false,
|
|
753
|
+
attempt,
|
|
754
|
+
status: response.status,
|
|
755
|
+
bodyClass: "http_error",
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
const urls = parseSmartproxyAllocatorProxies(body);
|
|
759
|
+
const bodyClass = classifySmartproxyAllocatorBody(body, urls);
|
|
760
|
+
if (urls.length === 0) {
|
|
761
|
+
return {
|
|
762
|
+
ok: false,
|
|
763
|
+
attempt,
|
|
764
|
+
status: response.status,
|
|
765
|
+
bodyClass,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
return {
|
|
769
|
+
ok: true,
|
|
770
|
+
attempt,
|
|
771
|
+
status: response.status,
|
|
772
|
+
bodyClass,
|
|
773
|
+
urls,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function smartproxyAllocatorBackoffMs(attempt) {
|
|
777
|
+
const base = SMARTPROXY_ALLOCATOR_RETRY_BASE_MS * 2 ** Math.max(0, attempt - 1);
|
|
778
|
+
const jitter = Math.floor(Math.random() * SMARTPROXY_ALLOCATOR_RETRY_BASE_MS);
|
|
779
|
+
return base + jitter;
|
|
780
|
+
}
|
|
781
|
+
function smartproxyAllocatorFailureMessage(failure) {
|
|
782
|
+
if (!failure) {
|
|
783
|
+
return "Smartproxy allocator failed.";
|
|
784
|
+
}
|
|
785
|
+
if (failure.bodyClass === "network_error") {
|
|
786
|
+
return "Smartproxy allocator request failed.";
|
|
787
|
+
}
|
|
788
|
+
if (failure.bodyClass === "http_error") {
|
|
789
|
+
return `Smartproxy allocator returned HTTP ${failure.status ?? "error"}.`;
|
|
790
|
+
}
|
|
791
|
+
return "Smartproxy allocator response did not contain a usable proxy endpoint.";
|
|
792
|
+
}
|
|
793
|
+
function buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize) {
|
|
794
|
+
const params = new URLSearchParams({
|
|
795
|
+
app_key: appKey,
|
|
796
|
+
pt: "9",
|
|
797
|
+
num: String(poolSize),
|
|
798
|
+
life: String(lifetimeMinutes),
|
|
799
|
+
protocol: "1",
|
|
800
|
+
format: "txt",
|
|
801
|
+
lb: "\\n",
|
|
802
|
+
});
|
|
803
|
+
const country = resolveSmartproxyCountry(policy);
|
|
804
|
+
if (country) {
|
|
805
|
+
params.set("cc", country);
|
|
806
|
+
}
|
|
807
|
+
return `https://www.smartproxy.org/web_v1/ip/get-ip-v3?${params.toString()}`;
|
|
808
|
+
}
|
|
809
|
+
function parseSmartproxyAllocatorProxies(body) {
|
|
810
|
+
const trimmed = body.trim();
|
|
811
|
+
if (!trimmed) {
|
|
812
|
+
return [];
|
|
813
|
+
}
|
|
814
|
+
try {
|
|
815
|
+
const parsed = JSON.parse(trimmed);
|
|
816
|
+
const data = parsed && typeof parsed === "object" && "data" in parsed
|
|
817
|
+
? parsed.data
|
|
818
|
+
: undefined;
|
|
819
|
+
const list = data && typeof data === "object" && "list" in data
|
|
820
|
+
? data.list
|
|
821
|
+
: undefined;
|
|
822
|
+
if (Array.isArray(list)) {
|
|
823
|
+
return list
|
|
824
|
+
.map((item) => {
|
|
825
|
+
if (!item || typeof item !== "object") {
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
const ip = "ip" in item && typeof item.ip === "string" ? item.ip : "";
|
|
829
|
+
const port = "port" in item &&
|
|
830
|
+
(typeof item.port === "string" || typeof item.port === "number")
|
|
831
|
+
? item.port
|
|
832
|
+
: "";
|
|
833
|
+
return ip && port ? `http://${ip}:${port}` : null;
|
|
834
|
+
})
|
|
835
|
+
.filter((url) => url !== null);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
// Text allocator format falls through.
|
|
840
|
+
}
|
|
841
|
+
return trimmed
|
|
842
|
+
.split(/\r?\n/)
|
|
843
|
+
.map((item) => item.trim())
|
|
844
|
+
.filter((item) => /^\d{1,3}(?:\.\d{1,3}){3}:\d{2,5}$/.test(item))
|
|
845
|
+
.map((line) => `http://${line}`);
|
|
846
|
+
}
|
|
847
|
+
function classifySmartproxyAllocatorBody(body, urls) {
|
|
848
|
+
if (urls.length > 0) {
|
|
849
|
+
return "usable_proxy_endpoints";
|
|
850
|
+
}
|
|
851
|
+
const trimmed = body.trim();
|
|
852
|
+
if (!trimmed) {
|
|
853
|
+
return "empty";
|
|
854
|
+
}
|
|
855
|
+
try {
|
|
856
|
+
JSON.parse(trimmed);
|
|
857
|
+
return "json_without_proxies";
|
|
858
|
+
}
|
|
859
|
+
catch {
|
|
860
|
+
return "text_without_proxies";
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
export function clearProxyResolutionCache() {
|
|
864
|
+
proxyCache.clear();
|
|
865
|
+
proxyInflight.clear();
|
|
866
|
+
invalidatedProxyKeys.clear();
|
|
867
|
+
}
|
|
868
|
+
function markSmartproxyCacheInvalidated(options = {}) {
|
|
869
|
+
const policy = resolvePolicy(options);
|
|
870
|
+
if (!policy || policy.mode === "disabled") {
|
|
871
|
+
return undefined;
|
|
872
|
+
}
|
|
873
|
+
if (resolveProxyProvider(policy) !== "smartproxy") {
|
|
874
|
+
return undefined;
|
|
875
|
+
}
|
|
876
|
+
const lifetimeMinutes = resolveSmartproxyLifetime(policy);
|
|
877
|
+
const cacheKey = buildSmartproxyCacheKey(policy, options.affinityKey, lifetimeMinutes);
|
|
878
|
+
invalidatedProxyKeys.set(cacheKey, Date.now() + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS);
|
|
879
|
+
proxyCache.delete(cacheKey);
|
|
880
|
+
proxyInflight.delete(cacheKey);
|
|
881
|
+
return cacheKey;
|
|
882
|
+
}
|
|
883
|
+
export function invalidateProxyResolutionCache(options = {}) {
|
|
884
|
+
return markSmartproxyCacheInvalidated(options) !== undefined;
|
|
885
|
+
}
|
|
886
|
+
export async function invalidateProxyResolutionCacheAsync(options = {}) {
|
|
887
|
+
const cacheKey = markSmartproxyCacheInvalidated(options);
|
|
888
|
+
if (!cacheKey)
|
|
889
|
+
return false;
|
|
890
|
+
const redis = getProxyRedis();
|
|
891
|
+
try {
|
|
892
|
+
if (redis && (await ensureRedisReady(redis))) {
|
|
893
|
+
await withRedisTimeout(() => redis.del(smartproxyRedisPoolKey(cacheKey)));
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
catch {
|
|
897
|
+
// Cache invalidation must not turn a stale proxy retry into a Redis outage.
|
|
898
|
+
}
|
|
899
|
+
return true;
|
|
900
|
+
}
|
|
901
|
+
export function defineConfig(config) {
|
|
902
|
+
return config;
|
|
903
|
+
}
|
|
904
|
+
async function importConfig(filePath) {
|
|
905
|
+
try {
|
|
906
|
+
const moduleUrl = new URL(`file://${encodeURI(filePath)}`);
|
|
907
|
+
const mod = (await import(moduleUrl.href));
|
|
908
|
+
if (mod.default && typeof mod.default === "object") {
|
|
909
|
+
return mod.default;
|
|
910
|
+
}
|
|
911
|
+
console.warn(`[provider-sdk] Ignoring invalid config export: ${filePath}`);
|
|
912
|
+
return {};
|
|
913
|
+
}
|
|
914
|
+
catch (error) {
|
|
915
|
+
console.warn(`[provider-sdk] Failed to load config ${filePath}:`, error);
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
export async function loadApiFuseConfig(dir = process.cwd()) {
|
|
920
|
+
const tsPath = path.resolve(dir, "apifuse.config.ts");
|
|
921
|
+
if (existsSync(tsPath)) {
|
|
922
|
+
const config = await importConfig(tsPath);
|
|
923
|
+
const resolvedConfig = config ?? {};
|
|
924
|
+
syncProxyEnv(resolvedConfig);
|
|
925
|
+
return resolvedConfig;
|
|
926
|
+
}
|
|
927
|
+
const jsPath = path.resolve(dir, "apifuse.config.js");
|
|
928
|
+
if (existsSync(jsPath)) {
|
|
929
|
+
const config = await importConfig(jsPath);
|
|
930
|
+
const resolvedConfig = config ?? {};
|
|
931
|
+
syncProxyEnv(resolvedConfig);
|
|
932
|
+
return resolvedConfig;
|
|
933
|
+
}
|
|
934
|
+
return {};
|
|
935
|
+
}
|