@apifuse/provider-sdk 2.2.0-beta.2 → 2.2.0-beta.21
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/AUTHORING.md +487 -0
- package/CHANGELOG.md +90 -0
- package/README.md +49 -3
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +44 -59
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +27 -52
- package/bin/apifuse-pack-check.ts +14 -0
- package/bin/apifuse-pack-smoke.ts +36 -81
- package/bin/apifuse-pack-types.ts +305 -0
- package/bin/apifuse-perf.ts +45 -127
- package/bin/apifuse-record.ts +659 -111
- package/bin/apifuse-submit-check.ts +546 -48
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/apifuse.ts +1 -1
- package/bin/submit-check-delimited-text.ts +50 -0
- package/bin/submit-check-xml-semantics.ts +204 -0
- package/bin/submit-check-xml.ts +134 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +9 -18
- package/dist/ceremonies/index.d.ts +9 -1
- package/dist/ceremonies/index.js +65 -18
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.d.ts +3 -0
- package/dist/cli/create.js +34 -35
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/config/loader.d.ts +176 -8
- package/dist/config/loader.js +424 -95
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +3 -6
- package/dist/contract-types.d.ts +2 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +4 -6
- package/dist/define.d.ts +9 -1
- package/dist/define.js +304 -119
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/error-resolution.d.ts +2 -0
- package/dist/error-resolution.js +90 -0
- package/dist/errors.d.ts +22 -1
- package/dist/errors.js +88 -0
- package/dist/fixture-sanitization.d.ts +26 -0
- package/dist/fixture-sanitization.js +216 -0
- package/dist/i18n/catalog.d.ts +2 -2
- package/dist/i18n/catalog.js +4 -10
- package/dist/i18n/index.d.ts +2 -2
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/keys.d.ts +2 -2
- package/dist/index.d.ts +46 -42
- package/dist/index.js +41 -37
- package/dist/lint.d.ts +1 -1
- package/dist/lint.js +8 -15
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +12 -11
- package/dist/provider.js +10 -9
- package/dist/public-schema-field-lint.d.ts +1 -1
- package/dist/recipes/gov-api.js +1 -1
- package/dist/runtime/auth-flow.d.ts +2 -1
- package/dist/runtime/auth-flow.js +4 -3
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +15 -29
- package/dist/runtime/cache.d.ts +1 -1
- package/dist/runtime/cache.js +4 -8
- package/dist/runtime/choice.d.ts +1 -1
- package/dist/runtime/choice.js +31 -35
- package/dist/runtime/credential.d.ts +1 -1
- package/dist/runtime/credential.js +1 -1
- package/dist/runtime/env.d.ts +1 -1
- package/dist/runtime/executor.d.ts +1 -1
- package/dist/runtime/executor.js +31 -4
- package/dist/runtime/http.d.ts +2 -2
- package/dist/runtime/http.js +387 -47
- package/dist/runtime/insights.d.ts +1 -1
- package/dist/runtime/insights.js +6 -13
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +345 -22
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- package/dist/runtime/native-network.d.ts +127 -0
- package/dist/runtime/native-network.js +1298 -0
- package/dist/runtime/otlp.d.ts +1 -1
- package/dist/runtime/perf.d.ts +1 -1
- package/dist/runtime/provider.d.ts +1 -1
- package/dist/runtime/provider.js +1 -2
- package/dist/runtime/proxy-errors.d.ts +1 -1
- package/dist/runtime/proxy-errors.js +9 -7
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-retry-policy.d.ts +2 -2
- package/dist/runtime/proxy-retry-policy.js +2 -2
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +55 -52
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +2 -4
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +2 -2
- package/dist/runtime/state.js +238 -26
- package/dist/runtime/stealth.d.ts +5 -3
- package/dist/runtime/stealth.js +423 -143
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +11 -15
- package/dist/runtime/trace.d.ts +2 -2
- package/dist/runtime/trace.js +2 -4
- package/dist/runtime/waterfall.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +7 -15
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +6 -6
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test-redaction.d.ts +1 -1
- package/dist/server/self-test-redaction.js +1 -1
- package/dist/server/self-test.d.ts +104 -3
- package/dist/server/self-test.js +673 -115
- package/dist/server/serve.d.ts +116 -4
- package/dist/server/serve.js +799 -128
- package/dist/server/types.d.ts +34 -9
- package/dist/server/types.js +8 -1
- package/dist/stateful/errors.d.ts +14 -0
- package/dist/stateful/errors.js +14 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +71 -0
- package/dist/stateful/stateful-provider-session-routing.js +353 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.d.ts +1 -1
- package/dist/stealth/profiles.js +5 -14
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.d.ts +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +478 -28
- package/dist/types.d.ts +342 -14
- package/dist/types.js +1 -0
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +16 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +28 -86
- package/src/ceremonies/index.ts +103 -78
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +42 -35
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/config/loader.ts +652 -204
- package/src/contract-serialization.ts +5 -11
- package/src/contract-types.ts +2 -2
- package/src/contract.ts +12 -28
- package/src/define.ts +499 -498
- package/src/dev.ts +4 -9
- package/src/error-resolution.ts +91 -0
- package/src/errors.ts +110 -5
- package/src/fixture-sanitization.ts +247 -0
- package/src/i18n/catalog.ts +10 -32
- package/src/i18n/index.ts +2 -2
- package/src/i18n/keys.ts +5 -11
- package/src/index.ts +112 -42
- package/src/lint.ts +88 -152
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +66 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +7 -7
- package/src/runtime/browser.ts +64 -187
- package/src/runtime/cache.ts +20 -67
- package/src/runtime/choice.ts +79 -132
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +43 -20
- package/src/runtime/http.ts +494 -57
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +495 -66
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- package/src/runtime/native-network.ts +1686 -0
- package/src/runtime/otlp.ts +12 -23
- package/src/runtime/perf.ts +1 -1
- package/src/runtime/provider.ts +4 -9
- package/src/runtime/proxy-errors.ts +29 -42
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-retry-policy.ts +3 -3
- package/src/runtime/proxy-telemetry.ts +79 -77
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +4 -12
- package/src/runtime/request-options.ts +679 -9
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +353 -133
- package/src/runtime/stealth.ts +505 -154
- package/src/runtime/stt.ts +38 -94
- package/src/runtime/trace.ts +14 -44
- package/src/runtime/waterfall.ts +5 -18
- package/src/schema.ts +23 -84
- package/src/serve.ts +1 -1
- package/src/server/index.ts +29 -7
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test-redaction.ts +2 -2
- package/src/server/self-test.ts +857 -132
- package/src/server/serve.ts +1151 -328
- package/src/server/types.ts +12 -13
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +23 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +555 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +10 -26
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -19
- package/src/testing/index.ts +10 -2
- package/src/testing/run.ts +653 -74
- package/src/types.ts +408 -28
- package/src/user-input.ts +118 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/dist/config/loader.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import Redis from "ioredis";
|
|
4
|
+
import { Redis } from "ioredis";
|
|
5
|
+
import { NODEMAVEN_DEFAULT_PROTOCOL, NODEMAVEN_FILTER_ENV, NODEMAVEN_MAX_POOL_SIZE, NODEMAVEN_PASSWORD_ENV, NODEMAVEN_USERNAME_ENV, hasNodemavenCredentials, nodemavenPoolSize, synthesizeNodemavenProxy, } from "../runtime/proxy-nodemaven.js";
|
|
6
|
+
// "smartproxy" here is api.smartproxy.org — a residential proxy with an IP
|
|
7
|
+
// extraction API (app_key → raw ip:port pool). It is NOT the company formerly
|
|
8
|
+
// named Smartproxy (smartproxy.com), which rebranded to Decodo in 2025 and is
|
|
9
|
+
// modelled separately as the `decodo` gateway vendor. Do not conflate them.
|
|
5
10
|
export const SMARTPROXY_APP_KEY_ENV = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
|
|
6
11
|
export const SMARTPROXY_MAX_LIFETIME_MINUTES = 2000;
|
|
7
12
|
export const DEFAULT_SMARTPROXY_POOL_SIZE = 20;
|
|
@@ -15,11 +20,17 @@ export const REDIS_URL_ENV = "APIFUSE__REDIS__URL";
|
|
|
15
20
|
export class ProxyResolutionError extends Error {
|
|
16
21
|
code;
|
|
17
22
|
telemetry;
|
|
23
|
+
vendor;
|
|
24
|
+
vendorChain;
|
|
25
|
+
protocol;
|
|
18
26
|
constructor(code, message, options) {
|
|
19
27
|
super(message, options);
|
|
20
28
|
this.name = "ProxyResolutionError";
|
|
21
29
|
this.code = code;
|
|
22
30
|
this.telemetry = options?.telemetry;
|
|
31
|
+
this.vendor = options?.vendor;
|
|
32
|
+
this.vendorChain = options?.vendorChain;
|
|
33
|
+
this.protocol = options?.protocol;
|
|
23
34
|
}
|
|
24
35
|
}
|
|
25
36
|
const proxyCache = new Map();
|
|
@@ -160,9 +171,7 @@ function toProxyDiagnostics(value) {
|
|
|
160
171
|
return undefined;
|
|
161
172
|
const diagnostics = {};
|
|
162
173
|
for (const [key, item] of Object.entries(value)) {
|
|
163
|
-
if (typeof item === "string" ||
|
|
164
|
-
typeof item === "number" ||
|
|
165
|
-
typeof item === "boolean") {
|
|
174
|
+
if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
|
|
166
175
|
diagnostics[key] = item;
|
|
167
176
|
}
|
|
168
177
|
}
|
|
@@ -234,6 +243,11 @@ function applyStickyProxySession(proxyUrl) {
|
|
|
234
243
|
if (!parsed.hostname || !parsed.username || !parsed.password) {
|
|
235
244
|
return proxyUrl;
|
|
236
245
|
}
|
|
246
|
+
// This rewrites sticky-session usernames for a bring-your-own *gateway* URL
|
|
247
|
+
// (APIFUSE__PROXY__URL). The `smartproxy` host here means a smartproxy.com /
|
|
248
|
+
// Decodo-family gateway that authenticates by username — NOT the
|
|
249
|
+
// api.smartproxy.org allocation vendor, whose endpoints are raw ip:port with
|
|
250
|
+
// no credentials and therefore return early above.
|
|
237
251
|
const host = parsed.hostname.toLowerCase();
|
|
238
252
|
if (!host.includes("smartproxy") && !host.includes("decodo")) {
|
|
239
253
|
return proxyUrl;
|
|
@@ -249,9 +263,7 @@ function applyStickyProxySession(proxyUrl) {
|
|
|
249
263
|
}
|
|
250
264
|
function buildSmartproxyUsername(username, sessionId, sessionDuration) {
|
|
251
265
|
const parts = username.split("_");
|
|
252
|
-
const configuredLife = parts
|
|
253
|
-
.find((part) => part.startsWith("life-"))
|
|
254
|
-
?.slice("life-".length);
|
|
266
|
+
const configuredLife = parts.find((part) => part.startsWith("life-"))?.slice("life-".length);
|
|
255
267
|
const baseUsername = parts
|
|
256
268
|
.filter((part) => !part.startsWith("session-") && !part.startsWith("life-"))
|
|
257
269
|
.join("_");
|
|
@@ -259,9 +271,7 @@ function buildSmartproxyUsername(username, sessionId, sessionDuration) {
|
|
|
259
271
|
}
|
|
260
272
|
function buildDecodoUsername(username, sessionId, sessionDuration) {
|
|
261
273
|
const withoutSticky = username.replace(/-session-.+-sessionduration-\d+$/, "");
|
|
262
|
-
const baseUsername = withoutSticky.startsWith("user-")
|
|
263
|
-
? withoutSticky
|
|
264
|
-
: `user-${withoutSticky}`;
|
|
274
|
+
const baseUsername = withoutSticky.startsWith("user-") ? withoutSticky : `user-${withoutSticky}`;
|
|
265
275
|
return `${baseUsername}-session-${sessionId}-sessionduration-${sessionDuration}`;
|
|
266
276
|
}
|
|
267
277
|
function syncProxyEnv(config) {
|
|
@@ -305,47 +315,201 @@ export async function resolveProxyConfigAsync(options = {}) {
|
|
|
305
315
|
if (policy.mode === "disabled") {
|
|
306
316
|
return { shouldWarn: false };
|
|
307
317
|
}
|
|
308
|
-
const
|
|
309
|
-
if (
|
|
318
|
+
const chain = resolveVendorChain(policy);
|
|
319
|
+
if (chain.length === 0) {
|
|
320
|
+
// decodo/custom/env-static providers keep the legacy static-URL path.
|
|
310
321
|
return resolveProxyConfig({
|
|
311
322
|
...options,
|
|
312
323
|
upstream: { proxy: true },
|
|
313
324
|
});
|
|
314
325
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
326
|
+
// Protocol is chosen per vendor (each vendor's benchmarked-best), with an
|
|
327
|
+
// optional explicit override for the harness/tests. Both are tunnelling
|
|
328
|
+
// schemes. transportProtocols is what the calling transport can actually use.
|
|
329
|
+
const transportProtocols = options.transportProtocols ?? ["http", "socks5"];
|
|
330
|
+
const sizes = chain.map((vendor) => vendorPoolSize(vendor, policy));
|
|
331
|
+
const total = sizes.reduce((sum, size) => sum + size, 0);
|
|
332
|
+
const normalizedAttempt = normalizeAttemptIndex(options.proxyAttempt);
|
|
333
|
+
const { vendorIndex: startVendorIndex, poolIndex: startPoolIndex } = mapFlatAttempt(total > 0 ? normalizedAttempt % total : 0, sizes);
|
|
334
|
+
const refreshEpoch = normalizeAttemptIndex(options.proxyRefreshEpoch);
|
|
335
|
+
let lastError;
|
|
336
|
+
let blockedProtocol;
|
|
337
|
+
for (let vendorIndex = startVendorIndex; vendorIndex < chain.length; vendorIndex++) {
|
|
338
|
+
const vendor = chain[vendorIndex];
|
|
339
|
+
const nextVendor = chain[vendorIndex + 1];
|
|
340
|
+
const poolIndex = vendorIndex === startVendorIndex ? startPoolIndex : 0;
|
|
341
|
+
const protocol = options.protocol ?? VENDOR_DEFAULT_PROTOCOL[vendor];
|
|
342
|
+
if (!vendorHasCredentials(vendor)) {
|
|
343
|
+
options.telemetry?.recordProxyVendorFailover?.({
|
|
344
|
+
vendor,
|
|
345
|
+
nextVendor,
|
|
346
|
+
phase: "resolution",
|
|
347
|
+
reason: "no_credentials",
|
|
348
|
+
});
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
// The calling transport must be able to use this vendor's protocol; if not,
|
|
352
|
+
// fail over to the next vendor rather than silently downgrading.
|
|
353
|
+
if (!transportProtocols.includes(protocol)) {
|
|
354
|
+
blockedProtocol = protocol;
|
|
355
|
+
options.telemetry?.recordProxyVendorFailover?.({
|
|
356
|
+
vendor,
|
|
357
|
+
nextVendor,
|
|
358
|
+
phase: "resolution",
|
|
359
|
+
reason: "protocol_unsupported",
|
|
360
|
+
});
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
return await resolveWithVendor(vendor, policy, options, {
|
|
365
|
+
protocol,
|
|
366
|
+
poolIndex,
|
|
367
|
+
refreshEpoch,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
// Config/programming errors (invalid filter, etc.) are not vendor
|
|
372
|
+
// outages — propagate them rather than failing over.
|
|
373
|
+
if (!(error instanceof ProxyResolutionError)) {
|
|
374
|
+
throw error;
|
|
375
|
+
}
|
|
376
|
+
if (error.telemetry) {
|
|
377
|
+
options.telemetry?.recordProxyResolution(error.telemetry);
|
|
378
|
+
}
|
|
379
|
+
lastError = error;
|
|
380
|
+
options.telemetry?.recordProxyVendorFailover?.({
|
|
381
|
+
vendor,
|
|
382
|
+
nextVendor,
|
|
383
|
+
phase: "resolution",
|
|
384
|
+
reason: "allocation_failed",
|
|
385
|
+
});
|
|
319
386
|
}
|
|
320
|
-
return { shouldWarn: true };
|
|
321
387
|
}
|
|
322
|
-
|
|
388
|
+
if (policy.mode === "required") {
|
|
389
|
+
if (lastError) {
|
|
390
|
+
throw lastError instanceof ProxyResolutionError
|
|
391
|
+
? lastError
|
|
392
|
+
: new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `All proxy vendors [${chain.join(", ")}] failed for required proxy egress.`, { cause: lastError, vendorChain: chain });
|
|
393
|
+
}
|
|
394
|
+
if (blockedProtocol) {
|
|
395
|
+
throw new ProxyResolutionError("PROXY_PROTOCOL_UNSUPPORTED", `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.`, { protocol: blockedProtocol, vendorChain: chain });
|
|
396
|
+
}
|
|
397
|
+
throw new ProxyResolutionError("PROXY_REQUIRED", `Proxy egress is required but no vendor credentials are configured. Missing: ${chain
|
|
398
|
+
.map((vendor) => `${missingCredentialEnv(vendor)} (${vendor})`)
|
|
399
|
+
.join(", ")}.`, { vendorChain: chain });
|
|
400
|
+
}
|
|
401
|
+
return { shouldWarn: true };
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Resolve the proxy URL for a provider-owned consumer such as a CAPTCHA solver.
|
|
405
|
+
* Vendor allocation and failover remain owned by the SDK.
|
|
406
|
+
*/
|
|
407
|
+
export async function resolveProxy(options = {}) {
|
|
408
|
+
const resolved = await resolveProxyConfigAsync(options);
|
|
409
|
+
const vendor = vendorFromResolvedSource(resolved.source);
|
|
410
|
+
return vendor ? { ...resolved, vendor } : resolved;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Each vendor's default egress protocol, chosen from live KR benchmarks. HTTP
|
|
414
|
+
* CONNECT wins for nodemaven (socks5 adds ~500ms through the gateway) and ties
|
|
415
|
+
* for smartproxy, and is the only protocol ctx.http (Bun native fetch) supports.
|
|
416
|
+
* Override per call via ProxyResolutionOptions.protocol (harness/tests).
|
|
417
|
+
*/
|
|
418
|
+
export const VENDOR_DEFAULT_PROTOCOL = {
|
|
419
|
+
smartproxy: "http",
|
|
420
|
+
nodemaven: NODEMAVEN_DEFAULT_PROTOCOL,
|
|
421
|
+
};
|
|
422
|
+
/**
|
|
423
|
+
* Guard the No-MITM invariant: a resolved proxy URL must use a tunnelling scheme
|
|
424
|
+
* (http CONNECT or socks5) so the client TLS handshake reaches the origin
|
|
425
|
+
* end-to-end. Anything else would intercept TLS and break fingerprinting.
|
|
426
|
+
*/
|
|
427
|
+
export function assertTunnelingScheme(url) {
|
|
428
|
+
let scheme;
|
|
323
429
|
try {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
430
|
+
scheme = new URL(url).protocol.replace(/:$/, "").toLowerCase();
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `Malformed proxy URL: ${url}`);
|
|
434
|
+
}
|
|
435
|
+
if (scheme !== "http" && scheme !== "socks5") {
|
|
436
|
+
throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `Resolved proxy scheme "${scheme}" is not a tunnelling scheme (expected http or socks5). Refusing to route TLS through a non-tunnelling proxy.`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
export async function resolveWithVendor(vendor, policy, options, context) {
|
|
440
|
+
if (vendor === "nodemaven") {
|
|
441
|
+
const startedAt = Date.now();
|
|
442
|
+
const username = (context.credentials === undefined
|
|
443
|
+
? process.env[NODEMAVEN_USERNAME_ENV]
|
|
444
|
+
: context.credentials[NODEMAVEN_USERNAME_ENV])?.trim();
|
|
445
|
+
const password = (context.credentials === undefined
|
|
446
|
+
? process.env[NODEMAVEN_PASSWORD_ENV]
|
|
447
|
+
: context.credentials[NODEMAVEN_PASSWORD_ENV])?.trim();
|
|
448
|
+
const filter = context.credentials === undefined
|
|
449
|
+
? process.env[NODEMAVEN_FILTER_ENV]
|
|
450
|
+
: context.credentials[NODEMAVEN_FILTER_ENV];
|
|
451
|
+
if (!username || !password) {
|
|
452
|
+
throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `NodeMaven credentials missing: set ${NODEMAVEN_USERNAME_ENV} and ${NODEMAVEN_PASSWORD_ENV}.`, { vendor: "nodemaven" });
|
|
453
|
+
}
|
|
454
|
+
const synthesized = synthesizeNodemavenProxy({
|
|
455
|
+
policy,
|
|
456
|
+
credentials: {
|
|
457
|
+
username,
|
|
458
|
+
password,
|
|
459
|
+
...(filter ? { filter } : {}),
|
|
460
|
+
},
|
|
461
|
+
affinityKey: options.affinityKey,
|
|
462
|
+
protocol: context.protocol,
|
|
463
|
+
poolIndex: context.poolIndex,
|
|
464
|
+
refreshEpoch: context.refreshEpoch,
|
|
465
|
+
country: resolveSmartproxyCountry(policy, context.ambientDefaults !== false),
|
|
466
|
+
});
|
|
467
|
+
options.telemetry?.recordProxyResolution({
|
|
468
|
+
provider: "nodemaven",
|
|
469
|
+
protocol: synthesized.protocol,
|
|
470
|
+
cacheStatus: "disabled",
|
|
471
|
+
cacheHit: false,
|
|
472
|
+
resolutionMs: Math.max(0, Date.now() - startedAt),
|
|
473
|
+
attempts: 1,
|
|
474
|
+
});
|
|
475
|
+
assertTunnelingScheme(synthesized.url);
|
|
327
476
|
return {
|
|
328
477
|
shouldWarn: false,
|
|
329
|
-
url:
|
|
330
|
-
source: "
|
|
478
|
+
url: synthesized.url,
|
|
479
|
+
source: "nodemaven-gateway",
|
|
480
|
+
protocol: synthesized.protocol,
|
|
331
481
|
diagnostics: {
|
|
332
|
-
...
|
|
333
|
-
|
|
334
|
-
poolIndex,
|
|
482
|
+
...synthesized.diagnostics,
|
|
483
|
+
poolIndex: context.poolIndex,
|
|
335
484
|
},
|
|
336
485
|
};
|
|
337
486
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
487
|
+
// smartproxy allocation-style vendor.
|
|
488
|
+
const appKey = (context.credentials === undefined
|
|
489
|
+
? process.env[SMARTPROXY_APP_KEY_ENV]
|
|
490
|
+
: context.credentials[SMARTPROXY_APP_KEY_ENV])?.trim();
|
|
491
|
+
if (!appKey) {
|
|
492
|
+
// Guarded by vendorHasCredentials; treated as a vendor-internal failure.
|
|
493
|
+
throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `${SMARTPROXY_APP_KEY_ENV} is not configured.`, { vendor: "smartproxy" });
|
|
494
|
+
}
|
|
495
|
+
const lifetimeMinutes = resolveSmartproxyLifetime(policy, context.ambientDefaults !== false);
|
|
496
|
+
const allocated = await allocateSmartproxy(policy, appKey, lifetimeMinutes, options.affinityKey, context.protocol, context.ambientDefaults !== false, context.sharedCache !== false);
|
|
497
|
+
options.telemetry?.recordProxyResolution({ ...allocated.telemetry, protocol: context.protocol });
|
|
498
|
+
const poolIndex = selectProxyPoolIndex(allocated.pool.urls.length, context.poolIndex);
|
|
499
|
+
const url = allocated.pool.urls[poolIndex];
|
|
500
|
+
if (url)
|
|
501
|
+
assertTunnelingScheme(url);
|
|
502
|
+
return {
|
|
503
|
+
shouldWarn: false,
|
|
504
|
+
url,
|
|
505
|
+
source: "smartproxy-allocator",
|
|
506
|
+
protocol: context.protocol,
|
|
507
|
+
diagnostics: {
|
|
508
|
+
...allocated.pool.diagnostics,
|
|
509
|
+
poolSize: allocated.pool.urls.length,
|
|
510
|
+
poolIndex,
|
|
511
|
+
},
|
|
512
|
+
};
|
|
349
513
|
}
|
|
350
514
|
function resolvePolicy(options) {
|
|
351
515
|
if (options.proxyPolicy) {
|
|
@@ -357,19 +521,178 @@ function resolvePolicy(options) {
|
|
|
357
521
|
}
|
|
358
522
|
return undefined;
|
|
359
523
|
}
|
|
360
|
-
function
|
|
361
|
-
return
|
|
362
|
-
|
|
363
|
-
|
|
524
|
+
function isRegistryVendor(name) {
|
|
525
|
+
return name === "smartproxy" || name === "nodemaven";
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Ordered list of SDK-native proxy vendors declared by the policy. `providers`
|
|
529
|
+
* takes precedence over the legacy singular `provider`; the platform default
|
|
530
|
+
* env is the final fallback. Non-registry names (decodo/custom) are dropped so
|
|
531
|
+
* an all-static chain falls through to the legacy env-URL path unchanged.
|
|
532
|
+
*/
|
|
533
|
+
export function resolveVendorChain(policy) {
|
|
534
|
+
const declared = policy.providers?.length
|
|
535
|
+
? policy.providers
|
|
536
|
+
: [policy.provider ?? envDefaultProvider()];
|
|
537
|
+
const chain = [];
|
|
538
|
+
for (const name of declared) {
|
|
539
|
+
if (isRegistryVendor(name) && !chain.includes(name)) {
|
|
540
|
+
chain.push(name);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return chain;
|
|
544
|
+
}
|
|
545
|
+
function envDefaultProvider() {
|
|
546
|
+
const raw = process.env[DEFAULT_PROXY_PROVIDER_ENV]?.trim().toLowerCase();
|
|
547
|
+
return raw ?? undefined;
|
|
548
|
+
}
|
|
549
|
+
function vendorHasCredentials(vendor) {
|
|
550
|
+
if (vendor === "nodemaven")
|
|
551
|
+
return hasNodemavenCredentials();
|
|
552
|
+
return Boolean(process.env[SMARTPROXY_APP_KEY_ENV]?.trim());
|
|
553
|
+
}
|
|
554
|
+
function missingCredentialEnv(vendor) {
|
|
555
|
+
return vendor === "nodemaven" ? "APIFUSE__PROXY__NODEMAVEN_USERNAME" : SMARTPROXY_APP_KEY_ENV;
|
|
556
|
+
}
|
|
557
|
+
function vendorPoolSize(vendor, policy) {
|
|
558
|
+
return vendor === "nodemaven" ? nodemavenPoolSize(policy) : resolveSmartproxyPoolSize(policy);
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Total attempt span across a policy's vendor chain — the sum of each vendor's
|
|
562
|
+
* pool size. Transports use this so successive attempts rotate a vendor's pool
|
|
563
|
+
* and then fail over to the next vendor via the flat attempt index. With one
|
|
564
|
+
* vendor this equals that vendor's pool size (today's behaviour).
|
|
565
|
+
*/
|
|
566
|
+
export function resolvePolicyProxyPoolSpan(policy) {
|
|
567
|
+
const chain = resolveVendorChain(policy);
|
|
568
|
+
if (chain.length === 0)
|
|
569
|
+
return resolveSmartproxyPoolSize(policy);
|
|
570
|
+
return chain.reduce((sum, vendor) => sum + vendorPoolSize(vendor, policy), 0);
|
|
571
|
+
}
|
|
572
|
+
function vendorMaxPoolSize(vendor) {
|
|
573
|
+
return vendor === "nodemaven" ? NODEMAVEN_MAX_POOL_SIZE : SMARTPROXY_MAX_POOL_SIZE;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Absolute upper bound on a chain's attempt span — the sum of each vendor's
|
|
577
|
+
* *maximum* pool size. Unlike `resolvePolicyProxyPoolSpan` (the configured
|
|
578
|
+
* span), this backstop is independent of `session.poolSize`, so it never
|
|
579
|
+
* truncates a legitimately large pool below the point where the flat attempt
|
|
580
|
+
* index would cross into the next vendor (e.g. a 50-slot NodeMaven pool).
|
|
581
|
+
*/
|
|
582
|
+
export function maxPolicyProxyPoolSpan(policy) {
|
|
583
|
+
const chain = resolveVendorChain(policy);
|
|
584
|
+
if (chain.length === 0)
|
|
585
|
+
return SMARTPROXY_MAX_POOL_SIZE;
|
|
586
|
+
return chain.reduce((sum, vendor) => sum + vendorMaxPoolSize(vendor), 0);
|
|
587
|
+
}
|
|
588
|
+
const UNSAFE_TRANSPORT_RETRY_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE", "TRACE"]);
|
|
589
|
+
/**
|
|
590
|
+
* Transport-retry attempt cap for a policy-managed request. A transport failure
|
|
591
|
+
* rotates the flat attempt index onto the *next* endpoint (and, once the index
|
|
592
|
+
* passes the primary vendor's pool span, the *next vendor*), so the cap must be
|
|
593
|
+
* the chain's full pool span for failover to reach the fallback vendor — the
|
|
594
|
+
* per-endpoint retry budget (default 3) never gets there.
|
|
595
|
+
*
|
|
596
|
+
* The span only widens beyond the caller's retry budget when ALL hold:
|
|
597
|
+
* - the request is policy-allocator managed (not a caller-supplied proxy URL);
|
|
598
|
+
* - the caller did NOT pin an explicit retry policy — `HttpRetryOptions.attempts`
|
|
599
|
+
* is the documented total-attempt ceiling and must be honoured verbatim;
|
|
600
|
+
* - the method is safe/idempotent — an unsafe request must never be duplicated
|
|
601
|
+
* across the pool even if some framework default would allow it;
|
|
602
|
+
* - the policy resolves a non-empty *registry* vendor chain (smartproxy /
|
|
603
|
+
* nodemaven). Static vendors (custom / decodo) and credential-less policies
|
|
604
|
+
* resolve no allocator pool, so every attempt would hit the same endpoint
|
|
605
|
+
* with no possible crossover — they keep the retry budget.
|
|
606
|
+
*
|
|
607
|
+
* The widened cap is bounded by the chain's true maximum span (sum of each
|
|
608
|
+
* vendor's max pool size), so a large NodeMaven pool (≤50) stays reachable and
|
|
609
|
+
* a pathological chain can never spin unbounded.
|
|
610
|
+
*/
|
|
611
|
+
/**
|
|
612
|
+
* True when a policy request is in *implicit chain-rotation* mode: successive
|
|
613
|
+
* transport attempts rotate the flat index across the concatenated vendor pool
|
|
614
|
+
* spans (and, past the primary vendor's span, into the fallback vendor). This is
|
|
615
|
+
* the ONLY mode in which the transport loop widens its attempt cap AND
|
|
616
|
+
* de-duplicates repeated endpoints — the two behaviours must share one predicate
|
|
617
|
+
* so they never diverge. It holds when ALL of the widening conditions hold:
|
|
618
|
+
* - the request is policy-allocator managed (not a caller-supplied proxy URL);
|
|
619
|
+
* - the caller did NOT pin an explicit retry policy — its `attempts` ceiling is
|
|
620
|
+
* the documented contract and must be honoured verbatim against whatever
|
|
621
|
+
* endpoint each attempt resolves (even a repeated one), so no de-duplication;
|
|
622
|
+
* - the method is safe/idempotent — an unsafe request is never duplicated;
|
|
623
|
+
* - the policy resolves a non-empty registry vendor chain (smartproxy /
|
|
624
|
+
* nodemaven). Static vendors (custom / decodo) resolve the same URL every
|
|
625
|
+
* attempt, so there is nothing to rotate or de-duplicate.
|
|
626
|
+
*/
|
|
627
|
+
export function policyRotatesTransportVendorChain(input) {
|
|
628
|
+
if (!input.usesPolicyAllocator || !input.policy || input.explicitRetry) {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
if (UNSAFE_TRANSPORT_RETRY_METHODS.has(input.method.toUpperCase())) {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
return resolveVendorChain(input.policy).length > 0;
|
|
635
|
+
}
|
|
636
|
+
export function resolvePolicyTransportAttemptCap(input) {
|
|
637
|
+
const budget = Math.max(1, Math.floor(input.retryAttempts));
|
|
638
|
+
if (!policyRotatesTransportVendorChain({
|
|
639
|
+
policy: input.policy,
|
|
640
|
+
usesPolicyAllocator: input.usesPolicyAllocator,
|
|
641
|
+
explicitRetry: input.explicitRetry,
|
|
642
|
+
method: input.method,
|
|
643
|
+
})) {
|
|
644
|
+
return budget;
|
|
645
|
+
}
|
|
646
|
+
const span = Math.min(maxPolicyProxyPoolSpan(input.policy), resolvePolicyProxyPoolSpan(input.policy));
|
|
647
|
+
return Math.max(budget, span);
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* A registry vendor chain (smartproxy/nodemaven) resolves a potentially
|
|
651
|
+
* *different* endpoint per flat attempt index, so a transport retry should
|
|
652
|
+
* advance across endpoints and de-duplicate once the chain stops yielding new
|
|
653
|
+
* ones. Static/custom/decodo policies (empty registry chain) resolve the *same*
|
|
654
|
+
* URL every attempt by design — retrying that same endpoint is intended, so the
|
|
655
|
+
* transport loop must not de-duplicate them.
|
|
656
|
+
*/
|
|
657
|
+
export function policyResolvesRegistryVendorChain(policy) {
|
|
658
|
+
return Boolean(policy) && resolveVendorChain(policy).length > 0;
|
|
659
|
+
}
|
|
660
|
+
/** Map a resolved proxy source label to the vendor that served it. */
|
|
661
|
+
export function vendorFromResolvedSource(source) {
|
|
662
|
+
if (source === "nodemaven-gateway")
|
|
663
|
+
return "nodemaven";
|
|
664
|
+
if (source === "smartproxy-allocator")
|
|
665
|
+
return "smartproxy";
|
|
666
|
+
return undefined;
|
|
667
|
+
}
|
|
668
|
+
function normalizeAttemptIndex(attempt) {
|
|
669
|
+
return Number.isFinite(attempt) ? Math.max(0, Math.floor(attempt)) : 0;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Map a flat attempt index into (vendorIndex, poolIndex) by concatenating each
|
|
673
|
+
* vendor's pool space in chain order. With a single vendor this reduces to
|
|
674
|
+
* `attempt % poolSize`, preserving today's behaviour exactly.
|
|
675
|
+
*/
|
|
676
|
+
export function mapFlatAttempt(flat, sizes) {
|
|
677
|
+
let cursor = flat;
|
|
678
|
+
for (let vendorIndex = 0; vendorIndex < sizes.length; vendorIndex++) {
|
|
679
|
+
const size = Math.max(1, sizes[vendorIndex] ?? 1);
|
|
680
|
+
if (cursor < size) {
|
|
681
|
+
return { vendorIndex, poolIndex: cursor };
|
|
682
|
+
}
|
|
683
|
+
cursor -= size;
|
|
684
|
+
}
|
|
685
|
+
return { vendorIndex: 0, poolIndex: 0 };
|
|
364
686
|
}
|
|
365
|
-
function resolveSmartproxyCountry(policy) {
|
|
687
|
+
function resolveSmartproxyCountry(policy, ambientDefaults = true) {
|
|
366
688
|
return (policy.geo?.country ??
|
|
367
|
-
|
|
368
|
-
|
|
689
|
+
(ambientDefaults
|
|
690
|
+
? process.env[DEFAULT_PROXY_COUNTRY_ENV]?.trim().toUpperCase() || undefined
|
|
691
|
+
: undefined));
|
|
369
692
|
}
|
|
370
|
-
function resolveSmartproxyLifetime(policy) {
|
|
693
|
+
function resolveSmartproxyLifetime(policy, ambientDefaults = true) {
|
|
371
694
|
const configuredLifetime = policy.session?.lifetimeMinutes ??
|
|
372
|
-
readPositiveNumberEnv(DEFAULT_PROXY_LIFETIME_ENV, 30);
|
|
695
|
+
(ambientDefaults ? readPositiveNumberEnv(DEFAULT_PROXY_LIFETIME_ENV, 30) : 30);
|
|
373
696
|
return Math.min(SMARTPROXY_MAX_LIFETIME_MINUTES, Math.max(1, Math.floor(configuredLifetime)));
|
|
374
697
|
}
|
|
375
698
|
function readPositiveNumberEnv(name, fallback) {
|
|
@@ -389,26 +712,27 @@ function selectProxyPoolIndex(poolSize, attempt = 0) {
|
|
|
389
712
|
if (poolSize <= 1) {
|
|
390
713
|
return 0;
|
|
391
714
|
}
|
|
392
|
-
const normalizedAttempt = Number.isFinite(attempt)
|
|
393
|
-
? Math.max(0, Math.floor(attempt))
|
|
394
|
-
: 0;
|
|
715
|
+
const normalizedAttempt = Number.isFinite(attempt) ? Math.max(0, Math.floor(attempt)) : 0;
|
|
395
716
|
return normalizedAttempt % poolSize;
|
|
396
717
|
}
|
|
397
|
-
function buildSmartproxyCacheKey(policy, affinityKey, lifetimeMinutes) {
|
|
718
|
+
function buildSmartproxyCacheKey(policy, appKey, affinityKey, lifetimeMinutes, protocol, ambientDefaults = true) {
|
|
398
719
|
const poolSize = resolveSmartproxyPoolSize(policy);
|
|
399
720
|
return JSON.stringify({
|
|
400
721
|
provider: "smartproxy",
|
|
401
|
-
|
|
722
|
+
credentialHash: createHash("sha256")
|
|
723
|
+
.update("apifuse-smartproxy-credential:v1\0")
|
|
724
|
+
.update(appKey)
|
|
725
|
+
.digest("hex"),
|
|
726
|
+
protocol,
|
|
727
|
+
country: resolveSmartproxyCountry(policy, ambientDefaults),
|
|
402
728
|
affinity: policy.session?.affinity ?? "request",
|
|
403
|
-
affinityKey: (policy.session?.affinity ?? "request") === "request"
|
|
404
|
-
? undefined
|
|
405
|
-
: affinityKey,
|
|
729
|
+
affinityKey: (policy.session?.affinity ?? "request") === "request" ? undefined : affinityKey,
|
|
406
730
|
lifetimeMinutes,
|
|
407
731
|
poolSize,
|
|
408
732
|
});
|
|
409
733
|
}
|
|
410
|
-
async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey) {
|
|
411
|
-
const cacheKey = buildSmartproxyCacheKey(policy, affinityKey, lifetimeMinutes);
|
|
734
|
+
async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey, protocol, ambientDefaults = true, sharedCache = true) {
|
|
735
|
+
const cacheKey = buildSmartproxyCacheKey(policy, appKey, affinityKey, lifetimeMinutes, protocol, ambientDefaults);
|
|
412
736
|
const startedAt = Date.now();
|
|
413
737
|
const now = startedAt;
|
|
414
738
|
const invalidatedUntil = invalidatedProxyKeys.get(cacheKey) ?? 0;
|
|
@@ -416,7 +740,7 @@ async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey)
|
|
|
416
740
|
const cached = proxyCache.get(cacheKey);
|
|
417
741
|
if (!skipCached && cached && isFresh(cached, now)) {
|
|
418
742
|
if (shouldSoftRefresh(cached, now)) {
|
|
419
|
-
void refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes);
|
|
743
|
+
void refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, protocol, ambientDefaults, sharedCache);
|
|
420
744
|
return {
|
|
421
745
|
pool: cached,
|
|
422
746
|
telemetry: telemetryForPool(cached, "soft_stale_refresh", startedAt, {
|
|
@@ -429,7 +753,7 @@ async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey)
|
|
|
429
753
|
telemetry: telemetryForPool(cached, "memory_hit", startedAt),
|
|
430
754
|
};
|
|
431
755
|
}
|
|
432
|
-
if (!skipCached) {
|
|
756
|
+
if (!skipCached && sharedCache) {
|
|
433
757
|
const redisResult = await readSmartproxyRedisPool(cacheKey, startedAt);
|
|
434
758
|
if (redisResult)
|
|
435
759
|
return redisResult;
|
|
@@ -444,7 +768,7 @@ async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey)
|
|
|
444
768
|
}),
|
|
445
769
|
};
|
|
446
770
|
}
|
|
447
|
-
const promise = allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, startedAt).finally(() => {
|
|
771
|
+
const promise = allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, startedAt, protocol, ambientDefaults, sharedCache).finally(() => {
|
|
448
772
|
proxyInflight.delete(cacheKey);
|
|
449
773
|
});
|
|
450
774
|
proxyInflight.set(cacheKey, promise);
|
|
@@ -475,9 +799,9 @@ async function readSmartproxyRedisPool(cacheKey, startedAt) {
|
|
|
475
799
|
telemetry: telemetryForPool(pool, "redis_hit", startedAt, { redisReadMs }),
|
|
476
800
|
};
|
|
477
801
|
}
|
|
478
|
-
async function refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes) {
|
|
802
|
+
async function refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, protocol, ambientDefaults, sharedCache) {
|
|
479
803
|
try {
|
|
480
|
-
await allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, Date.now(), {
|
|
804
|
+
await allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, Date.now(), protocol, ambientDefaults, sharedCache, {
|
|
481
805
|
background: true,
|
|
482
806
|
});
|
|
483
807
|
}
|
|
@@ -485,10 +809,10 @@ async function refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes)
|
|
|
485
809
|
// Soft refresh is opportunistic; current fresh pool remains usable.
|
|
486
810
|
}
|
|
487
811
|
}
|
|
488
|
-
async function allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, startedAt, options = {}) {
|
|
489
|
-
const redis = getProxyRedis();
|
|
812
|
+
async function allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, startedAt, protocol, ambientDefaults, sharedCache, options = {}) {
|
|
813
|
+
const redis = sharedCache ? getProxyRedis() : undefined;
|
|
490
814
|
if (!redis || !(await ensureRedisReady(redis))) {
|
|
491
|
-
return await allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, startedAt, { cacheStatus: "allocator" });
|
|
815
|
+
return await allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, startedAt, { cacheStatus: "allocator", protocol, ambientDefaults });
|
|
492
816
|
}
|
|
493
817
|
const poolKey = smartproxyRedisPoolKey(cacheKey);
|
|
494
818
|
const lockKey = smartproxyRedisLockKey(cacheKey);
|
|
@@ -499,11 +823,11 @@ async function allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinute
|
|
|
499
823
|
if (acquired === "OK") {
|
|
500
824
|
try {
|
|
501
825
|
return await allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, startedAt, {
|
|
502
|
-
cacheStatus: options.background
|
|
503
|
-
? "soft_stale_refresh"
|
|
504
|
-
: "allocator",
|
|
826
|
+
cacheStatus: options.background ? "soft_stale_refresh" : "allocator",
|
|
505
827
|
redis,
|
|
506
828
|
poolKey,
|
|
829
|
+
protocol,
|
|
830
|
+
ambientDefaults,
|
|
507
831
|
});
|
|
508
832
|
}
|
|
509
833
|
finally {
|
|
@@ -526,8 +850,7 @@ async function allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinute
|
|
|
526
850
|
if (typeof pttl === "number" && pttl <= 0) {
|
|
527
851
|
continue;
|
|
528
852
|
}
|
|
529
|
-
if (Date.now() - startedAt >
|
|
530
|
-
SMARTPROXY_LOCK_POLL_MAX_MS - SMARTPROXY_DEADLINE_MARGIN_MS) {
|
|
853
|
+
if (Date.now() - startedAt > SMARTPROXY_LOCK_POLL_MAX_MS - SMARTPROXY_DEADLINE_MARGIN_MS) {
|
|
531
854
|
break;
|
|
532
855
|
}
|
|
533
856
|
await sleep(Math.min(500, Math.max(50, typeof pttl === "number" ? pttl : 100)));
|
|
@@ -559,7 +882,7 @@ function sleep(ms, signal) {
|
|
|
559
882
|
});
|
|
560
883
|
}
|
|
561
884
|
function smartproxyAllocatorDeadlineMs() {
|
|
562
|
-
return
|
|
885
|
+
return smartproxyAllocatorDeadlineMsForTests ?? SMARTPROXY_ALLOCATOR_DEADLINE_MS;
|
|
563
886
|
}
|
|
564
887
|
function createDeadlineAbortController(deadlineAt) {
|
|
565
888
|
const controller = new AbortController();
|
|
@@ -617,7 +940,7 @@ async function readSmartproxyAllocatorBodyWithDeadline(response, signal) {
|
|
|
617
940
|
}
|
|
618
941
|
async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, startedAt, options) {
|
|
619
942
|
const poolSize = resolveSmartproxyPoolSize(policy);
|
|
620
|
-
const allocatorUrl = buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize);
|
|
943
|
+
const allocatorUrl = buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize, options.protocol, options.ambientDefaults);
|
|
621
944
|
const allocatorStartedAt = Date.now();
|
|
622
945
|
const allocatorDeadlineAt = allocatorStartedAt + smartproxyAllocatorDeadlineMs();
|
|
623
946
|
let allocation;
|
|
@@ -627,7 +950,7 @@ async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetime
|
|
|
627
950
|
lastFailure = smartproxyAllocatorDeadlineFailure(attempt);
|
|
628
951
|
break;
|
|
629
952
|
}
|
|
630
|
-
const attemptResult = await fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, allocatorDeadlineAt);
|
|
953
|
+
const attemptResult = await fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, allocatorDeadlineAt, options.protocol);
|
|
631
954
|
if (attemptResult.ok) {
|
|
632
955
|
allocation = attemptResult;
|
|
633
956
|
break;
|
|
@@ -649,9 +972,7 @@ async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetime
|
|
|
649
972
|
}
|
|
650
973
|
}
|
|
651
974
|
const allocatorMs = Math.max(0, Date.now() - allocatorStartedAt);
|
|
652
|
-
const allocatorAttempts = allocation?.attempt ??
|
|
653
|
-
lastFailure?.attempt ??
|
|
654
|
-
SMARTPROXY_ALLOCATOR_MAX_ATTEMPTS;
|
|
975
|
+
const allocatorAttempts = allocation?.attempt ?? lastFailure?.attempt ?? SMARTPROXY_ALLOCATOR_MAX_ATTEMPTS;
|
|
655
976
|
const allocatorBodyClass = lastFailure?.bodyClass ?? allocation?.bodyClass ?? "usable_proxy_endpoints";
|
|
656
977
|
const allocatorStatus = lastFailure ? lastFailure.status : allocation?.status;
|
|
657
978
|
if (!allocation) {
|
|
@@ -677,7 +998,7 @@ async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetime
|
|
|
677
998
|
expiresAt: allocatedAt + ttlMs,
|
|
678
999
|
diagnostics: {
|
|
679
1000
|
provider: "smartproxy",
|
|
680
|
-
country: resolveSmartproxyCountry(policy) ?? "default",
|
|
1001
|
+
country: resolveSmartproxyCountry(policy, options.ambientDefaults) ?? "default",
|
|
681
1002
|
lifetimeMinutes,
|
|
682
1003
|
affinity: policy.session?.affinity ?? "request",
|
|
683
1004
|
rawConnect: true,
|
|
@@ -712,7 +1033,7 @@ async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetime
|
|
|
712
1033
|
}),
|
|
713
1034
|
};
|
|
714
1035
|
}
|
|
715
|
-
async function fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, deadlineAt) {
|
|
1036
|
+
async function fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, deadlineAt, protocol) {
|
|
716
1037
|
const { controller, dispose } = createDeadlineAbortController(deadlineAt);
|
|
717
1038
|
let response;
|
|
718
1039
|
try {
|
|
@@ -755,7 +1076,7 @@ async function fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, deadlineAt
|
|
|
755
1076
|
bodyClass: "http_error",
|
|
756
1077
|
};
|
|
757
1078
|
}
|
|
758
|
-
const urls = parseSmartproxyAllocatorProxies(body);
|
|
1079
|
+
const urls = parseSmartproxyAllocatorProxies(body, protocol);
|
|
759
1080
|
const bodyClass = classifySmartproxyAllocatorBody(body, urls);
|
|
760
1081
|
if (urls.length === 0) {
|
|
761
1082
|
return {
|
|
@@ -790,35 +1111,41 @@ function smartproxyAllocatorFailureMessage(failure) {
|
|
|
790
1111
|
}
|
|
791
1112
|
return "Smartproxy allocator response did not contain a usable proxy endpoint.";
|
|
792
1113
|
}
|
|
793
|
-
|
|
1114
|
+
// Smartproxy get-ip-v3 `protocol` param: 1 = HTTP. The SOCKS5 value ("2") is a
|
|
1115
|
+
// best-effort mapping pending live vendor confirmation; http is the default and
|
|
1116
|
+
// the only value exercised in production today.
|
|
1117
|
+
const SMARTPROXY_PROTOCOL_PARAM = {
|
|
1118
|
+
http: "1",
|
|
1119
|
+
socks5: "2",
|
|
1120
|
+
};
|
|
1121
|
+
function buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize, protocol, ambientDefaults = true) {
|
|
794
1122
|
const params = new URLSearchParams({
|
|
795
1123
|
app_key: appKey,
|
|
796
1124
|
pt: "9",
|
|
797
1125
|
num: String(poolSize),
|
|
798
1126
|
life: String(lifetimeMinutes),
|
|
799
|
-
protocol:
|
|
1127
|
+
protocol: SMARTPROXY_PROTOCOL_PARAM[protocol],
|
|
800
1128
|
format: "txt",
|
|
801
1129
|
lb: "\\n",
|
|
802
1130
|
});
|
|
803
|
-
const country = resolveSmartproxyCountry(policy);
|
|
1131
|
+
const country = resolveSmartproxyCountry(policy, ambientDefaults);
|
|
804
1132
|
if (country) {
|
|
805
1133
|
params.set("cc", country);
|
|
806
1134
|
}
|
|
807
|
-
|
|
1135
|
+
// www.smartproxy.org stopped serving the extraction API on 2026-07-21 (the
|
|
1136
|
+
// old path 404s into the marketing site); the API lives on the api host.
|
|
1137
|
+
return `https://api.smartproxy.org/web_v1/ip/get-ip-v3?${params.toString()}`;
|
|
808
1138
|
}
|
|
809
|
-
function parseSmartproxyAllocatorProxies(body) {
|
|
1139
|
+
function parseSmartproxyAllocatorProxies(body, protocol) {
|
|
1140
|
+
const scheme = protocol === "socks5" ? "socks5" : "http";
|
|
810
1141
|
const trimmed = body.trim();
|
|
811
1142
|
if (!trimmed) {
|
|
812
1143
|
return [];
|
|
813
1144
|
}
|
|
814
1145
|
try {
|
|
815
1146
|
const parsed = JSON.parse(trimmed);
|
|
816
|
-
const data = parsed && typeof parsed === "object" && "data" in parsed
|
|
817
|
-
|
|
818
|
-
: undefined;
|
|
819
|
-
const list = data && typeof data === "object" && "list" in data
|
|
820
|
-
? data.list
|
|
821
|
-
: undefined;
|
|
1147
|
+
const data = parsed && typeof parsed === "object" && "data" in parsed ? parsed.data : undefined;
|
|
1148
|
+
const list = data && typeof data === "object" && "list" in data ? data.list : undefined;
|
|
822
1149
|
if (Array.isArray(list)) {
|
|
823
1150
|
return list
|
|
824
1151
|
.map((item) => {
|
|
@@ -826,11 +1153,10 @@ function parseSmartproxyAllocatorProxies(body) {
|
|
|
826
1153
|
return null;
|
|
827
1154
|
}
|
|
828
1155
|
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")
|
|
1156
|
+
const port = "port" in item && (typeof item.port === "string" || typeof item.port === "number")
|
|
831
1157
|
? item.port
|
|
832
1158
|
: "";
|
|
833
|
-
return ip && port ?
|
|
1159
|
+
return ip && port ? `${scheme}://${ip}:${port}` : null;
|
|
834
1160
|
})
|
|
835
1161
|
.filter((url) => url !== null);
|
|
836
1162
|
}
|
|
@@ -842,7 +1168,7 @@ function parseSmartproxyAllocatorProxies(body) {
|
|
|
842
1168
|
.split(/\r?\n/)
|
|
843
1169
|
.map((item) => item.trim())
|
|
844
1170
|
.filter((item) => /^\d{1,3}(?:\.\d{1,3}){3}:\d{2,5}$/.test(item))
|
|
845
|
-
.map((line) =>
|
|
1171
|
+
.map((line) => `${scheme}://${line}`);
|
|
846
1172
|
}
|
|
847
1173
|
function classifySmartproxyAllocatorBody(body, urls) {
|
|
848
1174
|
if (urls.length > 0) {
|
|
@@ -870,11 +1196,14 @@ function markSmartproxyCacheInvalidated(options = {}) {
|
|
|
870
1196
|
if (!policy || policy.mode === "disabled") {
|
|
871
1197
|
return undefined;
|
|
872
1198
|
}
|
|
873
|
-
if (
|
|
1199
|
+
if (!resolveVendorChain(policy).includes("smartproxy")) {
|
|
874
1200
|
return undefined;
|
|
875
1201
|
}
|
|
876
1202
|
const lifetimeMinutes = resolveSmartproxyLifetime(policy);
|
|
877
|
-
const
|
|
1203
|
+
const appKey = process.env[SMARTPROXY_APP_KEY_ENV]?.trim();
|
|
1204
|
+
if (!appKey)
|
|
1205
|
+
return undefined;
|
|
1206
|
+
const cacheKey = buildSmartproxyCacheKey(policy, appKey, options.affinityKey, lifetimeMinutes, options.protocol ?? VENDOR_DEFAULT_PROTOCOL.smartproxy);
|
|
878
1207
|
invalidatedProxyKeys.set(cacheKey, Date.now() + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS);
|
|
879
1208
|
proxyCache.delete(cacheKey);
|
|
880
1209
|
proxyInflight.delete(cacheKey);
|