@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/src/config/loader.ts
CHANGED
|
@@ -2,22 +2,39 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
import Redis from "ioredis";
|
|
6
|
-
|
|
7
|
-
import type { ProviderProxyPolicy, TraceConfig } from "../types";
|
|
8
|
-
|
|
5
|
+
import { Redis } from "ioredis";
|
|
6
|
+
|
|
7
|
+
import type { ProviderProxyPolicy, ProviderProxyProvider, TraceConfig } from "../types.js";
|
|
8
|
+
import {
|
|
9
|
+
NODEMAVEN_DEFAULT_PROTOCOL,
|
|
10
|
+
NODEMAVEN_FILTER_ENV,
|
|
11
|
+
NODEMAVEN_MAX_POOL_SIZE,
|
|
12
|
+
NODEMAVEN_PASSWORD_ENV,
|
|
13
|
+
NODEMAVEN_USERNAME_ENV,
|
|
14
|
+
type ProxyProtocol,
|
|
15
|
+
hasNodemavenCredentials,
|
|
16
|
+
nodemavenPoolSize,
|
|
17
|
+
synthesizeNodemavenProxy,
|
|
18
|
+
} from "../runtime/proxy-nodemaven.js";
|
|
19
|
+
|
|
20
|
+
export type { ProxyProtocol } from "../runtime/proxy-nodemaven.js";
|
|
21
|
+
|
|
22
|
+
/** Proxy vendors the SDK resolves natively (as opposed to the static env path). */
|
|
23
|
+
export type ProxyVendorName = "smartproxy" | "nodemaven";
|
|
24
|
+
|
|
25
|
+
// "smartproxy" here is api.smartproxy.org — a residential proxy with an IP
|
|
26
|
+
// extraction API (app_key → raw ip:port pool). It is NOT the company formerly
|
|
27
|
+
// named Smartproxy (smartproxy.com), which rebranded to Decodo in 2025 and is
|
|
28
|
+
// modelled separately as the `decodo` gateway vendor. Do not conflate them.
|
|
9
29
|
export const SMARTPROXY_APP_KEY_ENV = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
|
|
10
30
|
export const SMARTPROXY_MAX_LIFETIME_MINUTES = 2000;
|
|
11
31
|
export const DEFAULT_SMARTPROXY_POOL_SIZE = 20;
|
|
12
32
|
export const SMARTPROXY_MAX_POOL_SIZE = 20;
|
|
13
33
|
export const DEFAULT_PROXY_PROVIDER_ENV = "APIFUSE__PROXY__PROVIDER";
|
|
14
34
|
export const DEFAULT_PROXY_COUNTRY_ENV = "APIFUSE__PROXY__DEFAULT_COUNTRY";
|
|
15
|
-
export const DEFAULT_PROXY_LIFETIME_ENV =
|
|
16
|
-
|
|
17
|
-
export const
|
|
18
|
-
"APIFUSE__PROVIDER__CACHE_REDIS_URL";
|
|
19
|
-
export const PROVIDER_STATE_REDIS_URL_ENV =
|
|
20
|
-
"APIFUSE__PROVIDER__STATE_REDIS_URL";
|
|
35
|
+
export const DEFAULT_PROXY_LIFETIME_ENV = "APIFUSE__PROXY__DEFAULT_LIFETIME_MINUTES";
|
|
36
|
+
export const PROVIDER_CACHE_REDIS_URL_ENV = "APIFUSE__PROVIDER__CACHE_REDIS_URL";
|
|
37
|
+
export const PROVIDER_STATE_REDIS_URL_ENV = "APIFUSE__PROVIDER__STATE_REDIS_URL";
|
|
21
38
|
export const REDIS_URL_ENV = "APIFUSE__REDIS__URL";
|
|
22
39
|
|
|
23
40
|
export type ProxyOptions = {
|
|
@@ -55,6 +72,26 @@ export type ProxyResolutionOptions = {
|
|
|
55
72
|
affinityKey?: string;
|
|
56
73
|
/** Zero-based proxy-pool attempt index used by SDK transports for failover. */
|
|
57
74
|
proxyAttempt?: number;
|
|
75
|
+
/**
|
|
76
|
+
* Tunnelling protocols the calling transport can use. When a resolved
|
|
77
|
+
* protocol is not in this set the resolver fails with
|
|
78
|
+
* `PROXY_PROTOCOL_UNSUPPORTED` instead of silently downgrading. Unset means
|
|
79
|
+
* permissive (both protocols allowed).
|
|
80
|
+
*/
|
|
81
|
+
transportProtocols?: readonly ProxyProtocol[];
|
|
82
|
+
/**
|
|
83
|
+
* Explicit protocol override. Internal — for the verification harness and
|
|
84
|
+
* tests, or an advanced caller. Normal callers omit it and each vendor uses
|
|
85
|
+
* its own benchmarked default protocol (see VENDOR_DEFAULT_PROTOCOL). Not an
|
|
86
|
+
* env var and not a provider-policy field.
|
|
87
|
+
*/
|
|
88
|
+
protocol?: ProxyProtocol;
|
|
89
|
+
/**
|
|
90
|
+
* Gateway pool "refresh" generation. Bumped by transports on pool refresh to
|
|
91
|
+
* derive a fresh gateway session set (ignored by allocation-style vendors,
|
|
92
|
+
* whose refresh is driven by cache invalidation).
|
|
93
|
+
*/
|
|
94
|
+
proxyRefreshEpoch?: number;
|
|
58
95
|
telemetry?: ProxyTelemetrySink;
|
|
59
96
|
};
|
|
60
97
|
|
|
@@ -77,7 +114,8 @@ export type SmartproxyAllocatorBodyClass =
|
|
|
77
114
|
| "usable_proxy_endpoints";
|
|
78
115
|
|
|
79
116
|
export type ProxyResolutionTelemetryEvent = {
|
|
80
|
-
provider:
|
|
117
|
+
provider: ProxyVendorName;
|
|
118
|
+
protocol?: ProxyProtocol;
|
|
81
119
|
cacheStatus: ProxyCacheStatus;
|
|
82
120
|
cacheHit: boolean;
|
|
83
121
|
resolutionMs: number;
|
|
@@ -95,7 +133,7 @@ export type ProxyResolutionTelemetryEvent = {
|
|
|
95
133
|
};
|
|
96
134
|
|
|
97
135
|
export type ProxyAttemptTelemetryEvent = {
|
|
98
|
-
provider:
|
|
136
|
+
provider: ProxyVendorName;
|
|
99
137
|
attempt: number;
|
|
100
138
|
poolIndex?: number;
|
|
101
139
|
proxyHash?: string;
|
|
@@ -105,31 +143,69 @@ export type ProxyAttemptTelemetryEvent = {
|
|
|
105
143
|
durationMs?: number;
|
|
106
144
|
};
|
|
107
145
|
|
|
146
|
+
export type ProxyVendorFailoverTelemetryEvent = {
|
|
147
|
+
/** Vendor that failed or was skipped. */
|
|
148
|
+
vendor: ProxyVendorName;
|
|
149
|
+
/** Vendor tried next, or undefined when the chain is exhausted. */
|
|
150
|
+
nextVendor?: ProxyVendorName;
|
|
151
|
+
phase: "resolution" | "transport";
|
|
152
|
+
reason: "no_credentials" | "allocation_failed" | "pool_exhausted" | "protocol_unsupported";
|
|
153
|
+
attempt?: number;
|
|
154
|
+
};
|
|
155
|
+
|
|
108
156
|
export type ProxyTelemetrySink = {
|
|
109
157
|
recordProxyResolution(event: ProxyResolutionTelemetryEvent): void;
|
|
110
158
|
recordProxyAttempt?(event: ProxyAttemptTelemetryEvent): void;
|
|
159
|
+
recordProxyVendorFailover?(event: ProxyVendorFailoverTelemetryEvent): void;
|
|
111
160
|
};
|
|
112
161
|
|
|
162
|
+
export type ProxyResolutionSource =
|
|
163
|
+
| "explicit"
|
|
164
|
+
| "env"
|
|
165
|
+
| "config"
|
|
166
|
+
| "smartproxy-allocator"
|
|
167
|
+
| "nodemaven-gateway";
|
|
168
|
+
|
|
113
169
|
export type ResolvedProxyConfig = {
|
|
114
170
|
shouldWarn: boolean;
|
|
115
171
|
url?: string;
|
|
116
|
-
|
|
172
|
+
/** SDK-native vendor that supplied the URL, when applicable. */
|
|
173
|
+
vendor?: ProxyVendorName;
|
|
174
|
+
source?: ProxyResolutionSource;
|
|
175
|
+
protocol?: ProxyProtocol;
|
|
117
176
|
diagnostics?: Record<string, string | number | boolean>;
|
|
118
177
|
};
|
|
119
178
|
|
|
179
|
+
export type ProxyResolutionErrorCode =
|
|
180
|
+
| "PROXY_REQUIRED"
|
|
181
|
+
| "PROXY_ALLOCATION_FAILED"
|
|
182
|
+
| "PROXY_PROTOCOL_UNSUPPORTED";
|
|
183
|
+
|
|
120
184
|
export class ProxyResolutionError extends Error {
|
|
121
|
-
readonly code:
|
|
185
|
+
readonly code: ProxyResolutionErrorCode;
|
|
122
186
|
readonly telemetry?: ProxyResolutionTelemetryEvent;
|
|
187
|
+
readonly vendor?: ProxyVendorName;
|
|
188
|
+
readonly vendorChain?: ProxyVendorName[];
|
|
189
|
+
readonly protocol?: ProxyProtocol;
|
|
123
190
|
|
|
124
191
|
constructor(
|
|
125
|
-
code:
|
|
192
|
+
code: ProxyResolutionErrorCode,
|
|
126
193
|
message: string,
|
|
127
|
-
options?: {
|
|
194
|
+
options?: {
|
|
195
|
+
cause?: unknown;
|
|
196
|
+
telemetry?: ProxyResolutionTelemetryEvent;
|
|
197
|
+
vendor?: ProxyVendorName;
|
|
198
|
+
vendorChain?: ProxyVendorName[];
|
|
199
|
+
protocol?: ProxyProtocol;
|
|
200
|
+
},
|
|
128
201
|
) {
|
|
129
202
|
super(message, options);
|
|
130
203
|
this.name = "ProxyResolutionError";
|
|
131
204
|
this.code = code;
|
|
132
205
|
this.telemetry = options?.telemetry;
|
|
206
|
+
this.vendor = options?.vendor;
|
|
207
|
+
this.vendorChain = options?.vendorChain;
|
|
208
|
+
this.protocol = options?.protocol;
|
|
133
209
|
}
|
|
134
210
|
}
|
|
135
211
|
|
|
@@ -165,8 +241,7 @@ const SMARTPROXY_LOCK_POLL_MAX_MS = 9_000;
|
|
|
165
241
|
const SMARTPROXY_DEADLINE_MARGIN_MS = 1_000;
|
|
166
242
|
const SMARTPROXY_INVALIDATION_SKIP_REDIS_MS = 30_000;
|
|
167
243
|
const SMARTPROXY_ALLOCATOR_MAX_ATTEMPTS = 3;
|
|
168
|
-
const SMARTPROXY_ALLOCATOR_DEADLINE_MS =
|
|
169
|
-
SMARTPROXY_LOCK_TTL_MS - SMARTPROXY_DEADLINE_MARGIN_MS;
|
|
244
|
+
const SMARTPROXY_ALLOCATOR_DEADLINE_MS = SMARTPROXY_LOCK_TTL_MS - SMARTPROXY_DEADLINE_MARGIN_MS;
|
|
170
245
|
const SMARTPROXY_ALLOCATOR_RETRY_BASE_MS = 25;
|
|
171
246
|
// Smartproxy API extraction returns fresh IP:port candidates; the `life`
|
|
172
247
|
// parameter controls session duration intent, not a hard endpoint lease. Keep
|
|
@@ -197,15 +272,11 @@ export function providerStateRedisUrlFromEnv(): string | undefined {
|
|
|
197
272
|
}
|
|
198
273
|
|
|
199
274
|
/** @internal Test-only hook for exercising shared proxy-cache behavior. */
|
|
200
|
-
export function __setProxyRedisForTests(
|
|
201
|
-
redis: ProxyRedisClient | undefined,
|
|
202
|
-
): void {
|
|
275
|
+
export function __setProxyRedisForTests(redis: ProxyRedisClient | undefined): void {
|
|
203
276
|
proxyRedisForTests = redis;
|
|
204
277
|
}
|
|
205
278
|
|
|
206
|
-
export function __setSmartproxyAllocatorDeadlineMsForTests(
|
|
207
|
-
deadlineMs: number | undefined,
|
|
208
|
-
): void {
|
|
279
|
+
export function __setSmartproxyAllocatorDeadlineMsForTests(deadlineMs: number | undefined): void {
|
|
209
280
|
smartproxyAllocatorDeadlineMsForTests = deadlineMs;
|
|
210
281
|
}
|
|
211
282
|
|
|
@@ -230,9 +301,7 @@ function getProxyRedis(): ProxyRedisClient | undefined {
|
|
|
230
301
|
return redis;
|
|
231
302
|
}
|
|
232
303
|
|
|
233
|
-
async function withRedisTimeout<T>(
|
|
234
|
-
operation: () => Promise<T>,
|
|
235
|
-
): Promise<T | undefined> {
|
|
304
|
+
async function withRedisTimeout<T>(operation: () => Promise<T>): Promise<T | undefined> {
|
|
236
305
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
237
306
|
try {
|
|
238
307
|
const timeout = new Promise<undefined>((resolve) => {
|
|
@@ -318,17 +387,11 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
318
387
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
319
388
|
}
|
|
320
389
|
|
|
321
|
-
function toProxyDiagnostics(
|
|
322
|
-
value: unknown,
|
|
323
|
-
): Record<string, string | number | boolean> | undefined {
|
|
390
|
+
function toProxyDiagnostics(value: unknown): Record<string, string | number | boolean> | undefined {
|
|
324
391
|
if (!isRecord(value)) return undefined;
|
|
325
392
|
const diagnostics: Record<string, string | number | boolean> = {};
|
|
326
393
|
for (const [key, item] of Object.entries(value)) {
|
|
327
|
-
if (
|
|
328
|
-
typeof item === "string" ||
|
|
329
|
-
typeof item === "number" ||
|
|
330
|
-
typeof item === "boolean"
|
|
331
|
-
) {
|
|
394
|
+
if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
|
|
332
395
|
diagnostics[key] = item;
|
|
333
396
|
}
|
|
334
397
|
}
|
|
@@ -405,17 +468,19 @@ function applyStickyProxySession(proxyUrl: string): string {
|
|
|
405
468
|
return proxyUrl;
|
|
406
469
|
}
|
|
407
470
|
|
|
471
|
+
// This rewrites sticky-session usernames for a bring-your-own *gateway* URL
|
|
472
|
+
// (APIFUSE__PROXY__URL). The `smartproxy` host here means a smartproxy.com /
|
|
473
|
+
// Decodo-family gateway that authenticates by username — NOT the
|
|
474
|
+
// api.smartproxy.org allocation vendor, whose endpoints are raw ip:port with
|
|
475
|
+
// no credentials and therefore return early above.
|
|
408
476
|
const host = parsed.hostname.toLowerCase();
|
|
409
477
|
if (!host.includes("smartproxy") && !host.includes("decodo")) {
|
|
410
478
|
return proxyUrl;
|
|
411
479
|
}
|
|
412
480
|
|
|
413
481
|
const username = decodeURIComponent(parsed.username);
|
|
414
|
-
const sessionId =
|
|
415
|
-
|
|
416
|
-
const sessionDuration = readPositiveIntegerEnv(
|
|
417
|
-
"APIFUSE__PROXY__SESSION_DURATION",
|
|
418
|
-
);
|
|
482
|
+
const sessionId = process.env.APIFUSE__PROXY__SESSION_ID?.trim() || "apifuse-shared";
|
|
483
|
+
const sessionDuration = readPositiveIntegerEnv("APIFUSE__PROXY__SESSION_DURATION");
|
|
419
484
|
const stickyUsername = host.includes("smartproxy")
|
|
420
485
|
? buildSmartproxyUsername(username, sessionId, sessionDuration)
|
|
421
486
|
: buildDecodoUsername(username, sessionId, sessionDuration ?? "60");
|
|
@@ -430,27 +495,16 @@ function buildSmartproxyUsername(
|
|
|
430
495
|
sessionDuration?: string,
|
|
431
496
|
): string {
|
|
432
497
|
const parts = username.split("_");
|
|
433
|
-
const configuredLife = parts
|
|
434
|
-
.find((part) => part.startsWith("life-"))
|
|
435
|
-
?.slice("life-".length);
|
|
498
|
+
const configuredLife = parts.find((part) => part.startsWith("life-"))?.slice("life-".length);
|
|
436
499
|
const baseUsername = parts
|
|
437
500
|
.filter((part) => !part.startsWith("session-") && !part.startsWith("life-"))
|
|
438
501
|
.join("_");
|
|
439
502
|
return `${baseUsername}_session-${sessionId}_life-${sessionDuration ?? configuredLife ?? "60"}`;
|
|
440
503
|
}
|
|
441
504
|
|
|
442
|
-
function buildDecodoUsername(
|
|
443
|
-
username
|
|
444
|
-
|
|
445
|
-
sessionDuration: string,
|
|
446
|
-
): string {
|
|
447
|
-
const withoutSticky = username.replace(
|
|
448
|
-
/-session-.+-sessionduration-\d+$/,
|
|
449
|
-
"",
|
|
450
|
-
);
|
|
451
|
-
const baseUsername = withoutSticky.startsWith("user-")
|
|
452
|
-
? withoutSticky
|
|
453
|
-
: `user-${withoutSticky}`;
|
|
505
|
+
function buildDecodoUsername(username: string, sessionId: string, sessionDuration: string): string {
|
|
506
|
+
const withoutSticky = username.replace(/-session-.+-sessionduration-\d+$/, "");
|
|
507
|
+
const baseUsername = withoutSticky.startsWith("user-") ? withoutSticky : `user-${withoutSticky}`;
|
|
454
508
|
return `${baseUsername}-session-${sessionId}-sessionduration-${sessionDuration}`;
|
|
455
509
|
}
|
|
456
510
|
|
|
@@ -461,9 +515,7 @@ function syncProxyEnv(config: ApiFuseConfig): void {
|
|
|
461
515
|
}
|
|
462
516
|
}
|
|
463
517
|
|
|
464
|
-
export function resolveProxyConfig(
|
|
465
|
-
options: ProxyResolutionOptions = {},
|
|
466
|
-
): ResolvedProxyConfig {
|
|
518
|
+
export function resolveProxyConfig(options: ProxyResolutionOptions = {}): ResolvedProxyConfig {
|
|
467
519
|
const explicitProxyUrl = normalizeProxyUrl(options.proxy);
|
|
468
520
|
if (explicitProxyUrl) {
|
|
469
521
|
return { shouldWarn: false, url: explicitProxyUrl };
|
|
@@ -485,9 +537,7 @@ export function resolveProxyConfig(
|
|
|
485
537
|
return { shouldWarn: false, url: envProxyUrl };
|
|
486
538
|
}
|
|
487
539
|
|
|
488
|
-
const configuredProxyUrl = normalizeProxyUrl(
|
|
489
|
-
options.apifuseConfig?.proxy?.url,
|
|
490
|
-
);
|
|
540
|
+
const configuredProxyUrl = normalizeProxyUrl(options.apifuseConfig?.proxy?.url);
|
|
491
541
|
if (configuredProxyUrl) {
|
|
492
542
|
return { shouldWarn: false, url: configuredProxyUrl };
|
|
493
543
|
}
|
|
@@ -511,68 +561,273 @@ export async function resolveProxyConfigAsync(
|
|
|
511
561
|
return { shouldWarn: false };
|
|
512
562
|
}
|
|
513
563
|
|
|
514
|
-
const
|
|
515
|
-
if (
|
|
564
|
+
const chain = resolveVendorChain(policy);
|
|
565
|
+
if (chain.length === 0) {
|
|
566
|
+
// decodo/custom/env-static providers keep the legacy static-URL path.
|
|
516
567
|
return resolveProxyConfig({
|
|
517
568
|
...options,
|
|
518
569
|
upstream: { proxy: true },
|
|
519
570
|
});
|
|
520
571
|
}
|
|
521
572
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
573
|
+
// Protocol is chosen per vendor (each vendor's benchmarked-best), with an
|
|
574
|
+
// optional explicit override for the harness/tests. Both are tunnelling
|
|
575
|
+
// schemes. transportProtocols is what the calling transport can actually use.
|
|
576
|
+
const transportProtocols = options.transportProtocols ?? (["http", "socks5"] as const);
|
|
577
|
+
|
|
578
|
+
const sizes = chain.map((vendor) => vendorPoolSize(vendor, policy));
|
|
579
|
+
const total = sizes.reduce((sum, size) => sum + size, 0);
|
|
580
|
+
const normalizedAttempt = normalizeAttemptIndex(options.proxyAttempt);
|
|
581
|
+
const { vendorIndex: startVendorIndex, poolIndex: startPoolIndex } = mapFlatAttempt(
|
|
582
|
+
total > 0 ? normalizedAttempt % total : 0,
|
|
583
|
+
sizes,
|
|
584
|
+
);
|
|
585
|
+
const refreshEpoch = normalizeAttemptIndex(options.proxyRefreshEpoch);
|
|
586
|
+
|
|
587
|
+
let lastError: unknown;
|
|
588
|
+
let blockedProtocol: ProxyProtocol | undefined;
|
|
589
|
+
for (let vendorIndex = startVendorIndex; vendorIndex < chain.length; vendorIndex++) {
|
|
590
|
+
const vendor = chain[vendorIndex] as ProxyVendorName;
|
|
591
|
+
const nextVendor = chain[vendorIndex + 1];
|
|
592
|
+
const poolIndex = vendorIndex === startVendorIndex ? startPoolIndex : 0;
|
|
593
|
+
const protocol = options.protocol ?? VENDOR_DEFAULT_PROTOCOL[vendor];
|
|
594
|
+
|
|
595
|
+
if (!vendorHasCredentials(vendor)) {
|
|
596
|
+
options.telemetry?.recordProxyVendorFailover?.({
|
|
597
|
+
vendor,
|
|
598
|
+
nextVendor,
|
|
599
|
+
phase: "resolution",
|
|
600
|
+
reason: "no_credentials",
|
|
601
|
+
});
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// The calling transport must be able to use this vendor's protocol; if not,
|
|
606
|
+
// fail over to the next vendor rather than silently downgrading.
|
|
607
|
+
if (!transportProtocols.includes(protocol)) {
|
|
608
|
+
blockedProtocol = protocol;
|
|
609
|
+
options.telemetry?.recordProxyVendorFailover?.({
|
|
610
|
+
vendor,
|
|
611
|
+
nextVendor,
|
|
612
|
+
phase: "resolution",
|
|
613
|
+
reason: "protocol_unsupported",
|
|
614
|
+
});
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
return await resolveWithVendor(vendor, policy, options, {
|
|
620
|
+
protocol,
|
|
621
|
+
poolIndex,
|
|
622
|
+
refreshEpoch,
|
|
623
|
+
});
|
|
624
|
+
} catch (error) {
|
|
625
|
+
// Config/programming errors (invalid filter, etc.) are not vendor
|
|
626
|
+
// outages — propagate them rather than failing over.
|
|
627
|
+
if (!(error instanceof ProxyResolutionError)) {
|
|
628
|
+
throw error;
|
|
629
|
+
}
|
|
630
|
+
if (error.telemetry) {
|
|
631
|
+
options.telemetry?.recordProxyResolution(error.telemetry);
|
|
632
|
+
}
|
|
633
|
+
lastError = error;
|
|
634
|
+
options.telemetry?.recordProxyVendorFailover?.({
|
|
635
|
+
vendor,
|
|
636
|
+
nextVendor,
|
|
637
|
+
phase: "resolution",
|
|
638
|
+
reason: "allocation_failed",
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (policy.mode === "required") {
|
|
644
|
+
if (lastError) {
|
|
645
|
+
throw lastError instanceof ProxyResolutionError
|
|
646
|
+
? lastError
|
|
647
|
+
: new ProxyResolutionError(
|
|
648
|
+
"PROXY_ALLOCATION_FAILED",
|
|
649
|
+
`All proxy vendors [${chain.join(", ")}] failed for required proxy egress.`,
|
|
650
|
+
{ cause: lastError, vendorChain: chain },
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
if (blockedProtocol) {
|
|
525
654
|
throw new ProxyResolutionError(
|
|
526
|
-
"
|
|
527
|
-
`
|
|
655
|
+
"PROXY_PROTOCOL_UNSUPPORTED",
|
|
656
|
+
`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.`,
|
|
657
|
+
{ protocol: blockedProtocol, vendorChain: chain },
|
|
528
658
|
);
|
|
529
659
|
}
|
|
530
|
-
|
|
660
|
+
throw new ProxyResolutionError(
|
|
661
|
+
"PROXY_REQUIRED",
|
|
662
|
+
`Proxy egress is required but no vendor credentials are configured. Missing: ${chain
|
|
663
|
+
.map((vendor) => `${missingCredentialEnv(vendor)} (${vendor})`)
|
|
664
|
+
.join(", ")}.`,
|
|
665
|
+
{ vendorChain: chain },
|
|
666
|
+
);
|
|
531
667
|
}
|
|
532
|
-
|
|
668
|
+
return { shouldWarn: true };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Resolve the proxy URL for a provider-owned consumer such as a CAPTCHA solver.
|
|
673
|
+
* Vendor allocation and failover remain owned by the SDK.
|
|
674
|
+
*/
|
|
675
|
+
export async function resolveProxy(
|
|
676
|
+
options: ProxyResolutionOptions = {},
|
|
677
|
+
): Promise<ResolvedProxyConfig> {
|
|
678
|
+
const resolved = await resolveProxyConfigAsync(options);
|
|
679
|
+
const vendor = vendorFromResolvedSource(resolved.source);
|
|
680
|
+
return vendor ? { ...resolved, vendor } : resolved;
|
|
681
|
+
}
|
|
533
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Each vendor's default egress protocol, chosen from live KR benchmarks. HTTP
|
|
685
|
+
* CONNECT wins for nodemaven (socks5 adds ~500ms through the gateway) and ties
|
|
686
|
+
* for smartproxy, and is the only protocol ctx.http (Bun native fetch) supports.
|
|
687
|
+
* Override per call via ProxyResolutionOptions.protocol (harness/tests).
|
|
688
|
+
*/
|
|
689
|
+
export const VENDOR_DEFAULT_PROTOCOL: Readonly<Record<ProxyVendorName, ProxyProtocol>> = {
|
|
690
|
+
smartproxy: "http",
|
|
691
|
+
nodemaven: NODEMAVEN_DEFAULT_PROTOCOL,
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Guard the No-MITM invariant: a resolved proxy URL must use a tunnelling scheme
|
|
696
|
+
* (http CONNECT or socks5) so the client TLS handshake reaches the origin
|
|
697
|
+
* end-to-end. Anything else would intercept TLS and break fingerprinting.
|
|
698
|
+
*/
|
|
699
|
+
export function assertTunnelingScheme(url: string): void {
|
|
700
|
+
let scheme: string;
|
|
534
701
|
try {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
allocated.pool.urls.length,
|
|
544
|
-
options.proxyAttempt,
|
|
702
|
+
scheme = new URL(url).protocol.replace(/:$/, "").toLowerCase();
|
|
703
|
+
} catch {
|
|
704
|
+
throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `Malformed proxy URL: ${url}`);
|
|
705
|
+
}
|
|
706
|
+
if (scheme !== "http" && scheme !== "socks5") {
|
|
707
|
+
throw new ProxyResolutionError(
|
|
708
|
+
"PROXY_ALLOCATION_FAILED",
|
|
709
|
+
`Resolved proxy scheme "${scheme}" is not a tunnelling scheme (expected http or socks5). Refusing to route TLS through a non-tunnelling proxy.`,
|
|
545
710
|
);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export type ProxyVendorResolutionContext = {
|
|
715
|
+
readonly protocol: ProxyProtocol;
|
|
716
|
+
readonly poolIndex: number;
|
|
717
|
+
readonly refreshEpoch: number;
|
|
718
|
+
/** Explicit vendor credentials. Omit only on the legacy ambient-env path. */
|
|
719
|
+
readonly credentials?: Readonly<Record<string, string>>;
|
|
720
|
+
/** Disable non-policy env defaults for deterministic injected adapters. */
|
|
721
|
+
readonly ambientDefaults?: boolean;
|
|
722
|
+
/** Disable env-discovered Redis sharing for deterministic injected adapters. */
|
|
723
|
+
readonly sharedCache?: boolean;
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
export async function resolveWithVendor(
|
|
727
|
+
vendor: ProxyVendorName,
|
|
728
|
+
policy: ProviderProxyPolicy,
|
|
729
|
+
options: ProxyResolutionOptions,
|
|
730
|
+
context: ProxyVendorResolutionContext,
|
|
731
|
+
): Promise<ResolvedProxyConfig> {
|
|
732
|
+
if (vendor === "nodemaven") {
|
|
733
|
+
const startedAt = Date.now();
|
|
734
|
+
const username = (
|
|
735
|
+
context.credentials === undefined
|
|
736
|
+
? process.env[NODEMAVEN_USERNAME_ENV]
|
|
737
|
+
: context.credentials[NODEMAVEN_USERNAME_ENV]
|
|
738
|
+
)?.trim();
|
|
739
|
+
const password = (
|
|
740
|
+
context.credentials === undefined
|
|
741
|
+
? process.env[NODEMAVEN_PASSWORD_ENV]
|
|
742
|
+
: context.credentials[NODEMAVEN_PASSWORD_ENV]
|
|
743
|
+
)?.trim();
|
|
744
|
+
const filter =
|
|
745
|
+
context.credentials === undefined
|
|
746
|
+
? process.env[NODEMAVEN_FILTER_ENV]
|
|
747
|
+
: context.credentials[NODEMAVEN_FILTER_ENV];
|
|
748
|
+
if (!username || !password) {
|
|
749
|
+
throw new ProxyResolutionError(
|
|
750
|
+
"PROXY_ALLOCATION_FAILED",
|
|
751
|
+
`NodeMaven credentials missing: set ${NODEMAVEN_USERNAME_ENV} and ${NODEMAVEN_PASSWORD_ENV}.`,
|
|
752
|
+
{ vendor: "nodemaven" },
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
const synthesized = synthesizeNodemavenProxy({
|
|
756
|
+
policy,
|
|
757
|
+
credentials: {
|
|
758
|
+
username,
|
|
759
|
+
password,
|
|
760
|
+
...(filter ? { filter } : {}),
|
|
761
|
+
},
|
|
762
|
+
affinityKey: options.affinityKey,
|
|
763
|
+
protocol: context.protocol,
|
|
764
|
+
poolIndex: context.poolIndex,
|
|
765
|
+
refreshEpoch: context.refreshEpoch,
|
|
766
|
+
country: resolveSmartproxyCountry(policy, context.ambientDefaults !== false),
|
|
767
|
+
});
|
|
768
|
+
options.telemetry?.recordProxyResolution({
|
|
769
|
+
provider: "nodemaven",
|
|
770
|
+
protocol: synthesized.protocol,
|
|
771
|
+
cacheStatus: "disabled",
|
|
772
|
+
cacheHit: false,
|
|
773
|
+
resolutionMs: Math.max(0, Date.now() - startedAt),
|
|
774
|
+
attempts: 1,
|
|
775
|
+
});
|
|
776
|
+
assertTunnelingScheme(synthesized.url);
|
|
546
777
|
return {
|
|
547
778
|
shouldWarn: false,
|
|
548
|
-
url:
|
|
549
|
-
source: "
|
|
779
|
+
url: synthesized.url,
|
|
780
|
+
source: "nodemaven-gateway",
|
|
781
|
+
protocol: synthesized.protocol,
|
|
550
782
|
diagnostics: {
|
|
551
|
-
...
|
|
552
|
-
|
|
553
|
-
poolIndex,
|
|
783
|
+
...synthesized.diagnostics,
|
|
784
|
+
poolIndex: context.poolIndex,
|
|
554
785
|
},
|
|
555
786
|
};
|
|
556
|
-
} catch (error) {
|
|
557
|
-
if (error instanceof ProxyResolutionError && error.telemetry) {
|
|
558
|
-
options.telemetry?.recordProxyResolution(error.telemetry);
|
|
559
|
-
}
|
|
560
|
-
if (policy.mode === "required") {
|
|
561
|
-
throw error instanceof ProxyResolutionError
|
|
562
|
-
? error
|
|
563
|
-
: new ProxyResolutionError(
|
|
564
|
-
"PROXY_ALLOCATION_FAILED",
|
|
565
|
-
"Smartproxy allocator failed for required proxy egress.",
|
|
566
|
-
{ cause: error },
|
|
567
|
-
);
|
|
568
|
-
}
|
|
569
|
-
return { shouldWarn: true };
|
|
570
787
|
}
|
|
788
|
+
|
|
789
|
+
// smartproxy allocation-style vendor.
|
|
790
|
+
const appKey = (
|
|
791
|
+
context.credentials === undefined
|
|
792
|
+
? process.env[SMARTPROXY_APP_KEY_ENV]
|
|
793
|
+
: context.credentials[SMARTPROXY_APP_KEY_ENV]
|
|
794
|
+
)?.trim();
|
|
795
|
+
if (!appKey) {
|
|
796
|
+
// Guarded by vendorHasCredentials; treated as a vendor-internal failure.
|
|
797
|
+
throw new ProxyResolutionError(
|
|
798
|
+
"PROXY_ALLOCATION_FAILED",
|
|
799
|
+
`${SMARTPROXY_APP_KEY_ENV} is not configured.`,
|
|
800
|
+
{ vendor: "smartproxy" },
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
const lifetimeMinutes = resolveSmartproxyLifetime(policy, context.ambientDefaults !== false);
|
|
804
|
+
const allocated = await allocateSmartproxy(
|
|
805
|
+
policy,
|
|
806
|
+
appKey,
|
|
807
|
+
lifetimeMinutes,
|
|
808
|
+
options.affinityKey,
|
|
809
|
+
context.protocol,
|
|
810
|
+
context.ambientDefaults !== false,
|
|
811
|
+
context.sharedCache !== false,
|
|
812
|
+
);
|
|
813
|
+
options.telemetry?.recordProxyResolution({ ...allocated.telemetry, protocol: context.protocol });
|
|
814
|
+
const poolIndex = selectProxyPoolIndex(allocated.pool.urls.length, context.poolIndex);
|
|
815
|
+
const url = allocated.pool.urls[poolIndex];
|
|
816
|
+
if (url) assertTunnelingScheme(url);
|
|
817
|
+
return {
|
|
818
|
+
shouldWarn: false,
|
|
819
|
+
url,
|
|
820
|
+
source: "smartproxy-allocator",
|
|
821
|
+
protocol: context.protocol,
|
|
822
|
+
diagnostics: {
|
|
823
|
+
...allocated.pool.diagnostics,
|
|
824
|
+
poolSize: allocated.pool.urls.length,
|
|
825
|
+
poolIndex,
|
|
826
|
+
},
|
|
827
|
+
};
|
|
571
828
|
}
|
|
572
829
|
|
|
573
|
-
function resolvePolicy(
|
|
574
|
-
options: ProxyResolutionOptions,
|
|
575
|
-
): ProviderProxyPolicy | undefined {
|
|
830
|
+
function resolvePolicy(options: ProxyResolutionOptions): ProviderProxyPolicy | undefined {
|
|
576
831
|
if (options.proxyPolicy) {
|
|
577
832
|
return options.proxyPolicy;
|
|
578
833
|
}
|
|
@@ -583,32 +838,217 @@ function resolvePolicy(
|
|
|
583
838
|
return undefined;
|
|
584
839
|
}
|
|
585
840
|
|
|
586
|
-
function
|
|
587
|
-
return
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
841
|
+
function isRegistryVendor(name: string | undefined): name is ProxyVendorName {
|
|
842
|
+
return name === "smartproxy" || name === "nodemaven";
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Ordered list of SDK-native proxy vendors declared by the policy. `providers`
|
|
847
|
+
* takes precedence over the legacy singular `provider`; the platform default
|
|
848
|
+
* env is the final fallback. Non-registry names (decodo/custom) are dropped so
|
|
849
|
+
* an all-static chain falls through to the legacy env-URL path unchanged.
|
|
850
|
+
*/
|
|
851
|
+
export function resolveVendorChain(policy: ProviderProxyPolicy): ProxyVendorName[] {
|
|
852
|
+
const declared: (ProviderProxyProvider | undefined)[] = policy.providers?.length
|
|
853
|
+
? policy.providers
|
|
854
|
+
: [policy.provider ?? envDefaultProvider()];
|
|
855
|
+
const chain: ProxyVendorName[] = [];
|
|
856
|
+
for (const name of declared) {
|
|
857
|
+
if (isRegistryVendor(name) && !chain.includes(name)) {
|
|
858
|
+
chain.push(name);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return chain;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function envDefaultProvider(): ProviderProxyProvider | undefined {
|
|
865
|
+
const raw = process.env[DEFAULT_PROXY_PROVIDER_ENV]?.trim().toLowerCase();
|
|
866
|
+
return (raw as ProviderProxyProvider | undefined) ?? undefined;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function vendorHasCredentials(vendor: ProxyVendorName): boolean {
|
|
870
|
+
if (vendor === "nodemaven") return hasNodemavenCredentials();
|
|
871
|
+
return Boolean(process.env[SMARTPROXY_APP_KEY_ENV]?.trim());
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function missingCredentialEnv(vendor: ProxyVendorName): string {
|
|
875
|
+
return vendor === "nodemaven" ? "APIFUSE__PROXY__NODEMAVEN_USERNAME" : SMARTPROXY_APP_KEY_ENV;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function vendorPoolSize(vendor: ProxyVendorName, policy: ProviderProxyPolicy): number {
|
|
879
|
+
return vendor === "nodemaven" ? nodemavenPoolSize(policy) : resolveSmartproxyPoolSize(policy);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Total attempt span across a policy's vendor chain — the sum of each vendor's
|
|
884
|
+
* pool size. Transports use this so successive attempts rotate a vendor's pool
|
|
885
|
+
* and then fail over to the next vendor via the flat attempt index. With one
|
|
886
|
+
* vendor this equals that vendor's pool size (today's behaviour).
|
|
887
|
+
*/
|
|
888
|
+
export function resolvePolicyProxyPoolSpan(policy: ProviderProxyPolicy): number {
|
|
889
|
+
const chain = resolveVendorChain(policy);
|
|
890
|
+
if (chain.length === 0) return resolveSmartproxyPoolSize(policy);
|
|
891
|
+
return chain.reduce((sum, vendor) => sum + vendorPoolSize(vendor, policy), 0);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function vendorMaxPoolSize(vendor: ProxyVendorName): number {
|
|
895
|
+
return vendor === "nodemaven" ? NODEMAVEN_MAX_POOL_SIZE : SMARTPROXY_MAX_POOL_SIZE;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Absolute upper bound on a chain's attempt span — the sum of each vendor's
|
|
900
|
+
* *maximum* pool size. Unlike `resolvePolicyProxyPoolSpan` (the configured
|
|
901
|
+
* span), this backstop is independent of `session.poolSize`, so it never
|
|
902
|
+
* truncates a legitimately large pool below the point where the flat attempt
|
|
903
|
+
* index would cross into the next vendor (e.g. a 50-slot NodeMaven pool).
|
|
904
|
+
*/
|
|
905
|
+
export function maxPolicyProxyPoolSpan(policy: ProviderProxyPolicy): number {
|
|
906
|
+
const chain = resolveVendorChain(policy);
|
|
907
|
+
if (chain.length === 0) return SMARTPROXY_MAX_POOL_SIZE;
|
|
908
|
+
return chain.reduce((sum, vendor) => sum + vendorMaxPoolSize(vendor), 0);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const UNSAFE_TRANSPORT_RETRY_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE", "TRACE"]);
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Transport-retry attempt cap for a policy-managed request. A transport failure
|
|
915
|
+
* rotates the flat attempt index onto the *next* endpoint (and, once the index
|
|
916
|
+
* passes the primary vendor's pool span, the *next vendor*), so the cap must be
|
|
917
|
+
* the chain's full pool span for failover to reach the fallback vendor — the
|
|
918
|
+
* per-endpoint retry budget (default 3) never gets there.
|
|
919
|
+
*
|
|
920
|
+
* The span only widens beyond the caller's retry budget when ALL hold:
|
|
921
|
+
* - the request is policy-allocator managed (not a caller-supplied proxy URL);
|
|
922
|
+
* - the caller did NOT pin an explicit retry policy — `HttpRetryOptions.attempts`
|
|
923
|
+
* is the documented total-attempt ceiling and must be honoured verbatim;
|
|
924
|
+
* - the method is safe/idempotent — an unsafe request must never be duplicated
|
|
925
|
+
* across the pool even if some framework default would allow it;
|
|
926
|
+
* - the policy resolves a non-empty *registry* vendor chain (smartproxy /
|
|
927
|
+
* nodemaven). Static vendors (custom / decodo) and credential-less policies
|
|
928
|
+
* resolve no allocator pool, so every attempt would hit the same endpoint
|
|
929
|
+
* with no possible crossover — they keep the retry budget.
|
|
930
|
+
*
|
|
931
|
+
* The widened cap is bounded by the chain's true maximum span (sum of each
|
|
932
|
+
* vendor's max pool size), so a large NodeMaven pool (≤50) stays reachable and
|
|
933
|
+
* a pathological chain can never spin unbounded.
|
|
934
|
+
*/
|
|
935
|
+
/**
|
|
936
|
+
* True when a policy request is in *implicit chain-rotation* mode: successive
|
|
937
|
+
* transport attempts rotate the flat index across the concatenated vendor pool
|
|
938
|
+
* spans (and, past the primary vendor's span, into the fallback vendor). This is
|
|
939
|
+
* the ONLY mode in which the transport loop widens its attempt cap AND
|
|
940
|
+
* de-duplicates repeated endpoints — the two behaviours must share one predicate
|
|
941
|
+
* so they never diverge. It holds when ALL of the widening conditions hold:
|
|
942
|
+
* - the request is policy-allocator managed (not a caller-supplied proxy URL);
|
|
943
|
+
* - the caller did NOT pin an explicit retry policy — its `attempts` ceiling is
|
|
944
|
+
* the documented contract and must be honoured verbatim against whatever
|
|
945
|
+
* endpoint each attempt resolves (even a repeated one), so no de-duplication;
|
|
946
|
+
* - the method is safe/idempotent — an unsafe request is never duplicated;
|
|
947
|
+
* - the policy resolves a non-empty registry vendor chain (smartproxy /
|
|
948
|
+
* nodemaven). Static vendors (custom / decodo) resolve the same URL every
|
|
949
|
+
* attempt, so there is nothing to rotate or de-duplicate.
|
|
950
|
+
*/
|
|
951
|
+
export function policyRotatesTransportVendorChain(input: {
|
|
952
|
+
policy: ProviderProxyPolicy | undefined;
|
|
953
|
+
usesPolicyAllocator: boolean;
|
|
954
|
+
explicitRetry: boolean;
|
|
955
|
+
method: string;
|
|
956
|
+
}): boolean {
|
|
957
|
+
if (!input.usesPolicyAllocator || !input.policy || input.explicitRetry) {
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
if (UNSAFE_TRANSPORT_RETRY_METHODS.has(input.method.toUpperCase())) {
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
return resolveVendorChain(input.policy).length > 0;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
export function resolvePolicyTransportAttemptCap(input: {
|
|
967
|
+
policy: ProviderProxyPolicy | undefined;
|
|
968
|
+
usesPolicyAllocator: boolean;
|
|
969
|
+
retryAttempts: number;
|
|
970
|
+
explicitRetry: boolean;
|
|
971
|
+
method: string;
|
|
972
|
+
}): number {
|
|
973
|
+
const budget = Math.max(1, Math.floor(input.retryAttempts));
|
|
974
|
+
if (
|
|
975
|
+
!policyRotatesTransportVendorChain({
|
|
976
|
+
policy: input.policy,
|
|
977
|
+
usesPolicyAllocator: input.usesPolicyAllocator,
|
|
978
|
+
explicitRetry: input.explicitRetry,
|
|
979
|
+
method: input.method,
|
|
980
|
+
})
|
|
981
|
+
) {
|
|
982
|
+
return budget;
|
|
983
|
+
}
|
|
984
|
+
const span = Math.min(maxPolicyProxyPoolSpan(input.policy as ProviderProxyPolicy), resolvePolicyProxyPoolSpan(input.policy as ProviderProxyPolicy));
|
|
985
|
+
return Math.max(budget, span);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* A registry vendor chain (smartproxy/nodemaven) resolves a potentially
|
|
990
|
+
* *different* endpoint per flat attempt index, so a transport retry should
|
|
991
|
+
* advance across endpoints and de-duplicate once the chain stops yielding new
|
|
992
|
+
* ones. Static/custom/decodo policies (empty registry chain) resolve the *same*
|
|
993
|
+
* URL every attempt by design — retrying that same endpoint is intended, so the
|
|
994
|
+
* transport loop must not de-duplicate them.
|
|
995
|
+
*/
|
|
996
|
+
export function policyResolvesRegistryVendorChain(
|
|
997
|
+
policy: ProviderProxyPolicy | undefined,
|
|
998
|
+
): boolean {
|
|
999
|
+
return Boolean(policy) && resolveVendorChain(policy as ProviderProxyPolicy).length > 0;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/** Map a resolved proxy source label to the vendor that served it. */
|
|
1003
|
+
export function vendorFromResolvedSource(
|
|
1004
|
+
source: ResolvedProxyConfig["source"],
|
|
1005
|
+
): ProxyVendorName | undefined {
|
|
1006
|
+
if (source === "nodemaven-gateway") return "nodemaven";
|
|
1007
|
+
if (source === "smartproxy-allocator") return "smartproxy";
|
|
1008
|
+
return undefined;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function normalizeAttemptIndex(attempt: number | undefined): number {
|
|
1012
|
+
return Number.isFinite(attempt) ? Math.max(0, Math.floor(attempt as number)) : 0;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Map a flat attempt index into (vendorIndex, poolIndex) by concatenating each
|
|
1017
|
+
* vendor's pool space in chain order. With a single vendor this reduces to
|
|
1018
|
+
* `attempt % poolSize`, preserving today's behaviour exactly.
|
|
1019
|
+
*/
|
|
1020
|
+
export function mapFlatAttempt(
|
|
1021
|
+
flat: number,
|
|
1022
|
+
sizes: readonly number[],
|
|
1023
|
+
): { vendorIndex: number; poolIndex: number } {
|
|
1024
|
+
let cursor = flat;
|
|
1025
|
+
for (let vendorIndex = 0; vendorIndex < sizes.length; vendorIndex++) {
|
|
1026
|
+
const size = Math.max(1, sizes[vendorIndex] ?? 1);
|
|
1027
|
+
if (cursor < size) {
|
|
1028
|
+
return { vendorIndex, poolIndex: cursor };
|
|
1029
|
+
}
|
|
1030
|
+
cursor -= size;
|
|
1031
|
+
}
|
|
1032
|
+
return { vendorIndex: 0, poolIndex: 0 };
|
|
592
1033
|
}
|
|
593
1034
|
|
|
594
1035
|
function resolveSmartproxyCountry(
|
|
595
1036
|
policy: ProviderProxyPolicy,
|
|
1037
|
+
ambientDefaults = true,
|
|
596
1038
|
): string | undefined {
|
|
597
1039
|
return (
|
|
598
1040
|
policy.geo?.country ??
|
|
599
|
-
|
|
600
|
-
|
|
1041
|
+
(ambientDefaults
|
|
1042
|
+
? process.env[DEFAULT_PROXY_COUNTRY_ENV]?.trim().toUpperCase() || undefined
|
|
1043
|
+
: undefined)
|
|
601
1044
|
);
|
|
602
1045
|
}
|
|
603
1046
|
|
|
604
|
-
function resolveSmartproxyLifetime(policy: ProviderProxyPolicy): number {
|
|
1047
|
+
function resolveSmartproxyLifetime(policy: ProviderProxyPolicy, ambientDefaults = true): number {
|
|
605
1048
|
const configuredLifetime =
|
|
606
1049
|
policy.session?.lifetimeMinutes ??
|
|
607
|
-
readPositiveNumberEnv(DEFAULT_PROXY_LIFETIME_ENV, 30);
|
|
608
|
-
return Math.min(
|
|
609
|
-
SMARTPROXY_MAX_LIFETIME_MINUTES,
|
|
610
|
-
Math.max(1, Math.floor(configuredLifetime)),
|
|
611
|
-
);
|
|
1050
|
+
(ambientDefaults ? readPositiveNumberEnv(DEFAULT_PROXY_LIFETIME_ENV, 30) : 30);
|
|
1051
|
+
return Math.min(SMARTPROXY_MAX_LIFETIME_MINUTES, Math.max(1, Math.floor(configuredLifetime)));
|
|
612
1052
|
}
|
|
613
1053
|
|
|
614
1054
|
function readPositiveNumberEnv(name: string, fallback: number): number {
|
|
@@ -624,10 +1064,7 @@ function readPositiveNumberEnv(name: string, fallback: number): number {
|
|
|
624
1064
|
function resolveSmartproxyPoolSize(policy: ProviderProxyPolicy): number {
|
|
625
1065
|
return Math.min(
|
|
626
1066
|
SMARTPROXY_MAX_POOL_SIZE,
|
|
627
|
-
Math.max(
|
|
628
|
-
1,
|
|
629
|
-
Math.floor(policy.session?.poolSize ?? DEFAULT_SMARTPROXY_POOL_SIZE),
|
|
630
|
-
),
|
|
1067
|
+
Math.max(1, Math.floor(policy.session?.poolSize ?? DEFAULT_SMARTPROXY_POOL_SIZE)),
|
|
631
1068
|
);
|
|
632
1069
|
}
|
|
633
1070
|
|
|
@@ -635,26 +1072,29 @@ function selectProxyPoolIndex(poolSize: number, attempt = 0): number {
|
|
|
635
1072
|
if (poolSize <= 1) {
|
|
636
1073
|
return 0;
|
|
637
1074
|
}
|
|
638
|
-
const normalizedAttempt = Number.isFinite(attempt)
|
|
639
|
-
? Math.max(0, Math.floor(attempt))
|
|
640
|
-
: 0;
|
|
1075
|
+
const normalizedAttempt = Number.isFinite(attempt) ? Math.max(0, Math.floor(attempt)) : 0;
|
|
641
1076
|
return normalizedAttempt % poolSize;
|
|
642
1077
|
}
|
|
643
1078
|
|
|
644
1079
|
function buildSmartproxyCacheKey(
|
|
645
1080
|
policy: ProviderProxyPolicy,
|
|
1081
|
+
appKey: string,
|
|
646
1082
|
affinityKey: string | undefined,
|
|
647
1083
|
lifetimeMinutes: number,
|
|
1084
|
+
protocol: ProxyProtocol,
|
|
1085
|
+
ambientDefaults = true,
|
|
648
1086
|
): string {
|
|
649
1087
|
const poolSize = resolveSmartproxyPoolSize(policy);
|
|
650
1088
|
return JSON.stringify({
|
|
651
1089
|
provider: "smartproxy",
|
|
652
|
-
|
|
1090
|
+
credentialHash: createHash("sha256")
|
|
1091
|
+
.update("apifuse-smartproxy-credential:v1\0")
|
|
1092
|
+
.update(appKey)
|
|
1093
|
+
.digest("hex"),
|
|
1094
|
+
protocol,
|
|
1095
|
+
country: resolveSmartproxyCountry(policy, ambientDefaults),
|
|
653
1096
|
affinity: policy.session?.affinity ?? "request",
|
|
654
|
-
affinityKey:
|
|
655
|
-
(policy.session?.affinity ?? "request") === "request"
|
|
656
|
-
? undefined
|
|
657
|
-
: affinityKey,
|
|
1097
|
+
affinityKey: (policy.session?.affinity ?? "request") === "request" ? undefined : affinityKey,
|
|
658
1098
|
lifetimeMinutes,
|
|
659
1099
|
poolSize,
|
|
660
1100
|
});
|
|
@@ -665,11 +1105,17 @@ async function allocateSmartproxy(
|
|
|
665
1105
|
appKey: string,
|
|
666
1106
|
lifetimeMinutes: number,
|
|
667
1107
|
affinityKey: string | undefined,
|
|
1108
|
+
protocol: ProxyProtocol,
|
|
1109
|
+
ambientDefaults = true,
|
|
1110
|
+
sharedCache = true,
|
|
668
1111
|
): Promise<SmartproxyAllocationResult> {
|
|
669
1112
|
const cacheKey = buildSmartproxyCacheKey(
|
|
670
1113
|
policy,
|
|
1114
|
+
appKey,
|
|
671
1115
|
affinityKey,
|
|
672
1116
|
lifetimeMinutes,
|
|
1117
|
+
protocol,
|
|
1118
|
+
ambientDefaults,
|
|
673
1119
|
);
|
|
674
1120
|
const startedAt = Date.now();
|
|
675
1121
|
const now = startedAt;
|
|
@@ -678,7 +1124,15 @@ async function allocateSmartproxy(
|
|
|
678
1124
|
const cached = proxyCache.get(cacheKey);
|
|
679
1125
|
if (!skipCached && cached && isFresh(cached, now)) {
|
|
680
1126
|
if (shouldSoftRefresh(cached, now)) {
|
|
681
|
-
void refreshSmartproxyPool(
|
|
1127
|
+
void refreshSmartproxyPool(
|
|
1128
|
+
cacheKey,
|
|
1129
|
+
policy,
|
|
1130
|
+
appKey,
|
|
1131
|
+
lifetimeMinutes,
|
|
1132
|
+
protocol,
|
|
1133
|
+
ambientDefaults,
|
|
1134
|
+
sharedCache,
|
|
1135
|
+
);
|
|
682
1136
|
return {
|
|
683
1137
|
pool: cached,
|
|
684
1138
|
telemetry: telemetryForPool(cached, "soft_stale_refresh", startedAt, {
|
|
@@ -692,7 +1146,7 @@ async function allocateSmartproxy(
|
|
|
692
1146
|
};
|
|
693
1147
|
}
|
|
694
1148
|
|
|
695
|
-
if (!skipCached) {
|
|
1149
|
+
if (!skipCached && sharedCache) {
|
|
696
1150
|
const redisResult = await readSmartproxyRedisPool(cacheKey, startedAt);
|
|
697
1151
|
if (redisResult) return redisResult;
|
|
698
1152
|
}
|
|
@@ -714,6 +1168,9 @@ async function allocateSmartproxy(
|
|
|
714
1168
|
appKey,
|
|
715
1169
|
lifetimeMinutes,
|
|
716
1170
|
startedAt,
|
|
1171
|
+
protocol,
|
|
1172
|
+
ambientDefaults,
|
|
1173
|
+
sharedCache,
|
|
717
1174
|
).finally(() => {
|
|
718
1175
|
proxyInflight.delete(cacheKey);
|
|
719
1176
|
});
|
|
@@ -730,9 +1187,7 @@ async function readSmartproxyRedisPool(
|
|
|
730
1187
|
const redis = getProxyRedis();
|
|
731
1188
|
if (!redis || !(await ensureRedisReady(redis))) return null;
|
|
732
1189
|
const redisStartedAt = Date.now();
|
|
733
|
-
const raw = await withRedisTimeout(() =>
|
|
734
|
-
redis.get(smartproxyRedisPoolKey(cacheKey)),
|
|
735
|
-
);
|
|
1190
|
+
const raw = await withRedisTimeout(() => redis.get(smartproxyRedisPoolKey(cacheKey)));
|
|
736
1191
|
const redisReadMs = Math.max(0, Date.now() - redisStartedAt);
|
|
737
1192
|
if (typeof raw !== "string") return null;
|
|
738
1193
|
const pool = safeParseSmartproxyPool(raw);
|
|
@@ -754,6 +1209,9 @@ async function refreshSmartproxyPool(
|
|
|
754
1209
|
policy: ProviderProxyPolicy,
|
|
755
1210
|
appKey: string,
|
|
756
1211
|
lifetimeMinutes: number,
|
|
1212
|
+
protocol: ProxyProtocol,
|
|
1213
|
+
ambientDefaults: boolean,
|
|
1214
|
+
sharedCache: boolean,
|
|
757
1215
|
): Promise<void> {
|
|
758
1216
|
try {
|
|
759
1217
|
await allocateSmartproxyShared(
|
|
@@ -762,6 +1220,9 @@ async function refreshSmartproxyPool(
|
|
|
762
1220
|
appKey,
|
|
763
1221
|
lifetimeMinutes,
|
|
764
1222
|
Date.now(),
|
|
1223
|
+
protocol,
|
|
1224
|
+
ambientDefaults,
|
|
1225
|
+
sharedCache,
|
|
765
1226
|
{
|
|
766
1227
|
background: true,
|
|
767
1228
|
},
|
|
@@ -777,9 +1238,12 @@ async function allocateSmartproxyShared(
|
|
|
777
1238
|
appKey: string,
|
|
778
1239
|
lifetimeMinutes: number,
|
|
779
1240
|
startedAt: number,
|
|
1241
|
+
protocol: ProxyProtocol,
|
|
1242
|
+
ambientDefaults: boolean,
|
|
1243
|
+
sharedCache: boolean,
|
|
780
1244
|
options: { background?: boolean } = {},
|
|
781
1245
|
): Promise<SmartproxyAllocationResult> {
|
|
782
|
-
const redis = getProxyRedis();
|
|
1246
|
+
const redis = sharedCache ? getProxyRedis() : undefined;
|
|
783
1247
|
if (!redis || !(await ensureRedisReady(redis))) {
|
|
784
1248
|
return await allocateAndStoreSmartproxyPool(
|
|
785
1249
|
cacheKey,
|
|
@@ -787,7 +1251,7 @@ async function allocateSmartproxyShared(
|
|
|
787
1251
|
appKey,
|
|
788
1252
|
lifetimeMinutes,
|
|
789
1253
|
startedAt,
|
|
790
|
-
{ cacheStatus: "allocator" },
|
|
1254
|
+
{ cacheStatus: "allocator", protocol, ambientDefaults },
|
|
791
1255
|
);
|
|
792
1256
|
}
|
|
793
1257
|
|
|
@@ -809,11 +1273,11 @@ async function allocateSmartproxyShared(
|
|
|
809
1273
|
lifetimeMinutes,
|
|
810
1274
|
startedAt,
|
|
811
1275
|
{
|
|
812
|
-
cacheStatus: options.background
|
|
813
|
-
? "soft_stale_refresh"
|
|
814
|
-
: "allocator",
|
|
1276
|
+
cacheStatus: options.background ? "soft_stale_refresh" : "allocator",
|
|
815
1277
|
redis,
|
|
816
1278
|
poolKey,
|
|
1279
|
+
protocol,
|
|
1280
|
+
ambientDefaults,
|
|
817
1281
|
},
|
|
818
1282
|
);
|
|
819
1283
|
} finally {
|
|
@@ -838,15 +1302,10 @@ async function allocateSmartproxyShared(
|
|
|
838
1302
|
if (typeof pttl === "number" && pttl <= 0) {
|
|
839
1303
|
continue;
|
|
840
1304
|
}
|
|
841
|
-
if (
|
|
842
|
-
Date.now() - startedAt >
|
|
843
|
-
SMARTPROXY_LOCK_POLL_MAX_MS - SMARTPROXY_DEADLINE_MARGIN_MS
|
|
844
|
-
) {
|
|
1305
|
+
if (Date.now() - startedAt > SMARTPROXY_LOCK_POLL_MAX_MS - SMARTPROXY_DEADLINE_MARGIN_MS) {
|
|
845
1306
|
break;
|
|
846
1307
|
}
|
|
847
|
-
await sleep(
|
|
848
|
-
Math.min(500, Math.max(50, typeof pttl === "number" ? pttl : 100)),
|
|
849
|
-
);
|
|
1308
|
+
await sleep(Math.min(500, Math.max(50, typeof pttl === "number" ? pttl : 100)));
|
|
850
1309
|
}
|
|
851
1310
|
|
|
852
1311
|
throw new ProxyResolutionError(
|
|
@@ -893,9 +1352,7 @@ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
893
1352
|
}
|
|
894
1353
|
|
|
895
1354
|
function smartproxyAllocatorDeadlineMs(): number {
|
|
896
|
-
return
|
|
897
|
-
smartproxyAllocatorDeadlineMsForTests ?? SMARTPROXY_ALLOCATOR_DEADLINE_MS
|
|
898
|
-
);
|
|
1355
|
+
return smartproxyAllocatorDeadlineMsForTests ?? SMARTPROXY_ALLOCATOR_DEADLINE_MS;
|
|
899
1356
|
}
|
|
900
1357
|
|
|
901
1358
|
function createDeadlineAbortController(deadlineAt: number): {
|
|
@@ -917,9 +1374,7 @@ function createDeadlineAbortController(deadlineAt: number): {
|
|
|
917
1374
|
};
|
|
918
1375
|
}
|
|
919
1376
|
|
|
920
|
-
function smartproxyAllocatorDeadlineFailure(
|
|
921
|
-
attempt: number,
|
|
922
|
-
): SmartproxyAllocatorFailure {
|
|
1377
|
+
function smartproxyAllocatorDeadlineFailure(attempt: number): SmartproxyAllocatorFailure {
|
|
923
1378
|
return {
|
|
924
1379
|
ok: false,
|
|
925
1380
|
attempt,
|
|
@@ -978,6 +1433,8 @@ async function allocateAndStoreSmartproxyPool(
|
|
|
978
1433
|
cacheStatus: ProxyCacheStatus;
|
|
979
1434
|
redis?: ProxyRedisClient;
|
|
980
1435
|
poolKey?: string;
|
|
1436
|
+
protocol: ProxyProtocol;
|
|
1437
|
+
ambientDefaults: boolean;
|
|
981
1438
|
},
|
|
982
1439
|
): Promise<SmartproxyAllocationResult> {
|
|
983
1440
|
const poolSize = resolveSmartproxyPoolSize(policy);
|
|
@@ -986,17 +1443,14 @@ async function allocateAndStoreSmartproxyPool(
|
|
|
986
1443
|
appKey,
|
|
987
1444
|
lifetimeMinutes,
|
|
988
1445
|
poolSize,
|
|
1446
|
+
options.protocol,
|
|
1447
|
+
options.ambientDefaults,
|
|
989
1448
|
);
|
|
990
1449
|
const allocatorStartedAt = Date.now();
|
|
991
|
-
const allocatorDeadlineAt =
|
|
992
|
-
allocatorStartedAt + smartproxyAllocatorDeadlineMs();
|
|
1450
|
+
const allocatorDeadlineAt = allocatorStartedAt + smartproxyAllocatorDeadlineMs();
|
|
993
1451
|
let allocation: SmartproxyAllocatorSuccess | undefined;
|
|
994
1452
|
let lastFailure: SmartproxyAllocatorFailure | undefined;
|
|
995
|
-
for (
|
|
996
|
-
let attempt = 1;
|
|
997
|
-
attempt <= SMARTPROXY_ALLOCATOR_MAX_ATTEMPTS;
|
|
998
|
-
attempt += 1
|
|
999
|
-
) {
|
|
1453
|
+
for (let attempt = 1; attempt <= SMARTPROXY_ALLOCATOR_MAX_ATTEMPTS; attempt += 1) {
|
|
1000
1454
|
if (Date.now() >= allocatorDeadlineAt) {
|
|
1001
1455
|
lastFailure = smartproxyAllocatorDeadlineFailure(attempt);
|
|
1002
1456
|
break;
|
|
@@ -1005,6 +1459,7 @@ async function allocateAndStoreSmartproxyPool(
|
|
|
1005
1459
|
allocatorUrl,
|
|
1006
1460
|
attempt,
|
|
1007
1461
|
allocatorDeadlineAt,
|
|
1462
|
+
options.protocol,
|
|
1008
1463
|
);
|
|
1009
1464
|
if (attemptResult.ok) {
|
|
1010
1465
|
allocation = attemptResult;
|
|
@@ -1013,8 +1468,7 @@ async function allocateAndStoreSmartproxyPool(
|
|
|
1013
1468
|
lastFailure = attemptResult;
|
|
1014
1469
|
if (attempt < SMARTPROXY_ALLOCATOR_MAX_ATTEMPTS) {
|
|
1015
1470
|
if (Date.now() >= allocatorDeadlineAt) break;
|
|
1016
|
-
const { controller, dispose } =
|
|
1017
|
-
createDeadlineAbortController(allocatorDeadlineAt);
|
|
1471
|
+
const { controller, dispose } = createDeadlineAbortController(allocatorDeadlineAt);
|
|
1018
1472
|
try {
|
|
1019
1473
|
await sleep(smartproxyAllocatorBackoffMs(attempt), controller.signal);
|
|
1020
1474
|
} catch {
|
|
@@ -1027,9 +1481,7 @@ async function allocateAndStoreSmartproxyPool(
|
|
|
1027
1481
|
|
|
1028
1482
|
const allocatorMs = Math.max(0, Date.now() - allocatorStartedAt);
|
|
1029
1483
|
const allocatorAttempts =
|
|
1030
|
-
allocation?.attempt ??
|
|
1031
|
-
lastFailure?.attempt ??
|
|
1032
|
-
SMARTPROXY_ALLOCATOR_MAX_ATTEMPTS;
|
|
1484
|
+
allocation?.attempt ?? lastFailure?.attempt ?? SMARTPROXY_ALLOCATOR_MAX_ATTEMPTS;
|
|
1033
1485
|
const allocatorBodyClass =
|
|
1034
1486
|
lastFailure?.bodyClass ?? allocation?.bodyClass ?? "usable_proxy_endpoints";
|
|
1035
1487
|
const allocatorStatus = lastFailure ? lastFailure.status : allocation?.status;
|
|
@@ -1063,7 +1515,7 @@ async function allocateAndStoreSmartproxyPool(
|
|
|
1063
1515
|
expiresAt: allocatedAt + ttlMs,
|
|
1064
1516
|
diagnostics: {
|
|
1065
1517
|
provider: "smartproxy",
|
|
1066
|
-
country: resolveSmartproxyCountry(policy) ?? "default",
|
|
1518
|
+
country: resolveSmartproxyCountry(policy, options.ambientDefaults) ?? "default",
|
|
1067
1519
|
lifetimeMinutes,
|
|
1068
1520
|
affinity: policy.session?.affinity ?? "request",
|
|
1069
1521
|
rawConnect: true,
|
|
@@ -1106,9 +1558,7 @@ async function allocateAndStoreSmartproxyPool(
|
|
|
1106
1558
|
};
|
|
1107
1559
|
}
|
|
1108
1560
|
|
|
1109
|
-
type SmartproxyAllocatorAttemptResult =
|
|
1110
|
-
| SmartproxyAllocatorSuccess
|
|
1111
|
-
| SmartproxyAllocatorFailure;
|
|
1561
|
+
type SmartproxyAllocatorAttemptResult = SmartproxyAllocatorSuccess | SmartproxyAllocatorFailure;
|
|
1112
1562
|
|
|
1113
1563
|
type SmartproxyAllocatorSuccess = {
|
|
1114
1564
|
ok: true;
|
|
@@ -1130,6 +1580,7 @@ async function fetchSmartproxyAllocatorAttempt(
|
|
|
1130
1580
|
allocatorUrl: string,
|
|
1131
1581
|
attempt: number,
|
|
1132
1582
|
deadlineAt: number,
|
|
1583
|
+
protocol: ProxyProtocol,
|
|
1133
1584
|
): Promise<SmartproxyAllocatorAttemptResult> {
|
|
1134
1585
|
const { controller, dispose } = createDeadlineAbortController(deadlineAt);
|
|
1135
1586
|
let response: Response;
|
|
@@ -1150,10 +1601,7 @@ async function fetchSmartproxyAllocatorAttempt(
|
|
|
1150
1601
|
|
|
1151
1602
|
let body: string;
|
|
1152
1603
|
try {
|
|
1153
|
-
body = await readSmartproxyAllocatorBodyWithDeadline(
|
|
1154
|
-
response,
|
|
1155
|
-
controller.signal,
|
|
1156
|
-
);
|
|
1604
|
+
body = await readSmartproxyAllocatorBodyWithDeadline(response, controller.signal);
|
|
1157
1605
|
} catch (error) {
|
|
1158
1606
|
dispose();
|
|
1159
1607
|
return {
|
|
@@ -1176,7 +1624,7 @@ async function fetchSmartproxyAllocatorAttempt(
|
|
|
1176
1624
|
};
|
|
1177
1625
|
}
|
|
1178
1626
|
|
|
1179
|
-
const urls = parseSmartproxyAllocatorProxies(body);
|
|
1627
|
+
const urls = parseSmartproxyAllocatorProxies(body, protocol);
|
|
1180
1628
|
const bodyClass = classifySmartproxyAllocatorBody(body, urls);
|
|
1181
1629
|
if (urls.length === 0) {
|
|
1182
1630
|
return {
|
|
@@ -1196,8 +1644,7 @@ async function fetchSmartproxyAllocatorAttempt(
|
|
|
1196
1644
|
}
|
|
1197
1645
|
|
|
1198
1646
|
function smartproxyAllocatorBackoffMs(attempt: number): number {
|
|
1199
|
-
const base =
|
|
1200
|
-
SMARTPROXY_ALLOCATOR_RETRY_BASE_MS * 2 ** Math.max(0, attempt - 1);
|
|
1647
|
+
const base = SMARTPROXY_ALLOCATOR_RETRY_BASE_MS * 2 ** Math.max(0, attempt - 1);
|
|
1201
1648
|
const jitter = Math.floor(Math.random() * SMARTPROXY_ALLOCATOR_RETRY_BASE_MS);
|
|
1202
1649
|
return base + jitter;
|
|
1203
1650
|
}
|
|
@@ -1217,29 +1664,42 @@ function smartproxyAllocatorFailureMessage(
|
|
|
1217
1664
|
return "Smartproxy allocator response did not contain a usable proxy endpoint.";
|
|
1218
1665
|
}
|
|
1219
1666
|
|
|
1667
|
+
// Smartproxy get-ip-v3 `protocol` param: 1 = HTTP. The SOCKS5 value ("2") is a
|
|
1668
|
+
// best-effort mapping pending live vendor confirmation; http is the default and
|
|
1669
|
+
// the only value exercised in production today.
|
|
1670
|
+
const SMARTPROXY_PROTOCOL_PARAM: Record<ProxyProtocol, string> = {
|
|
1671
|
+
http: "1",
|
|
1672
|
+
socks5: "2",
|
|
1673
|
+
};
|
|
1674
|
+
|
|
1220
1675
|
function buildSmartproxyAllocatorUrl(
|
|
1221
1676
|
policy: ProviderProxyPolicy,
|
|
1222
1677
|
appKey: string,
|
|
1223
1678
|
lifetimeMinutes: number,
|
|
1224
1679
|
poolSize: number,
|
|
1680
|
+
protocol: ProxyProtocol,
|
|
1681
|
+
ambientDefaults = true,
|
|
1225
1682
|
): string {
|
|
1226
1683
|
const params = new URLSearchParams({
|
|
1227
1684
|
app_key: appKey,
|
|
1228
1685
|
pt: "9",
|
|
1229
1686
|
num: String(poolSize),
|
|
1230
1687
|
life: String(lifetimeMinutes),
|
|
1231
|
-
protocol:
|
|
1688
|
+
protocol: SMARTPROXY_PROTOCOL_PARAM[protocol],
|
|
1232
1689
|
format: "txt",
|
|
1233
1690
|
lb: "\\n",
|
|
1234
1691
|
});
|
|
1235
|
-
const country = resolveSmartproxyCountry(policy);
|
|
1692
|
+
const country = resolveSmartproxyCountry(policy, ambientDefaults);
|
|
1236
1693
|
if (country) {
|
|
1237
1694
|
params.set("cc", country);
|
|
1238
1695
|
}
|
|
1239
|
-
|
|
1696
|
+
// www.smartproxy.org stopped serving the extraction API on 2026-07-21 (the
|
|
1697
|
+
// old path 404s into the marketing site); the API lives on the api host.
|
|
1698
|
+
return `https://api.smartproxy.org/web_v1/ip/get-ip-v3?${params.toString()}`;
|
|
1240
1699
|
}
|
|
1241
1700
|
|
|
1242
|
-
function parseSmartproxyAllocatorProxies(body: string): string[] {
|
|
1701
|
+
function parseSmartproxyAllocatorProxies(body: string, protocol: ProxyProtocol): string[] {
|
|
1702
|
+
const scheme = protocol === "socks5" ? "socks5" : "http";
|
|
1243
1703
|
const trimmed = body.trim();
|
|
1244
1704
|
if (!trimmed) {
|
|
1245
1705
|
return [];
|
|
@@ -1247,14 +1707,8 @@ function parseSmartproxyAllocatorProxies(body: string): string[] {
|
|
|
1247
1707
|
|
|
1248
1708
|
try {
|
|
1249
1709
|
const parsed: unknown = JSON.parse(trimmed);
|
|
1250
|
-
const data =
|
|
1251
|
-
|
|
1252
|
-
? parsed.data
|
|
1253
|
-
: undefined;
|
|
1254
|
-
const list =
|
|
1255
|
-
data && typeof data === "object" && "list" in data
|
|
1256
|
-
? data.list
|
|
1257
|
-
: undefined;
|
|
1710
|
+
const data = parsed && typeof parsed === "object" && "data" in parsed ? parsed.data : undefined;
|
|
1711
|
+
const list = data && typeof data === "object" && "list" in data ? data.list : undefined;
|
|
1258
1712
|
if (Array.isArray(list)) {
|
|
1259
1713
|
return list
|
|
1260
1714
|
.map((item) => {
|
|
@@ -1263,11 +1717,10 @@ function parseSmartproxyAllocatorProxies(body: string): string[] {
|
|
|
1263
1717
|
}
|
|
1264
1718
|
const ip = "ip" in item && typeof item.ip === "string" ? item.ip : "";
|
|
1265
1719
|
const port =
|
|
1266
|
-
"port" in item &&
|
|
1267
|
-
(typeof item.port === "string" || typeof item.port === "number")
|
|
1720
|
+
"port" in item && (typeof item.port === "string" || typeof item.port === "number")
|
|
1268
1721
|
? item.port
|
|
1269
1722
|
: "";
|
|
1270
|
-
return ip && port ?
|
|
1723
|
+
return ip && port ? `${scheme}://${ip}:${port}` : null;
|
|
1271
1724
|
})
|
|
1272
1725
|
.filter((url): url is string => url !== null);
|
|
1273
1726
|
}
|
|
@@ -1279,7 +1732,7 @@ function parseSmartproxyAllocatorProxies(body: string): string[] {
|
|
|
1279
1732
|
.split(/\r?\n/)
|
|
1280
1733
|
.map((item) => item.trim())
|
|
1281
1734
|
.filter((item) => /^\d{1,3}(?:\.\d{1,3}){3}:\d{2,5}$/.test(item))
|
|
1282
|
-
.map((line) =>
|
|
1735
|
+
.map((line) => `${scheme}://${line}`);
|
|
1283
1736
|
}
|
|
1284
1737
|
|
|
1285
1738
|
function classifySmartproxyAllocatorBody(
|
|
@@ -1307,35 +1760,32 @@ export function clearProxyResolutionCache(): void {
|
|
|
1307
1760
|
invalidatedProxyKeys.clear();
|
|
1308
1761
|
}
|
|
1309
1762
|
|
|
1310
|
-
function markSmartproxyCacheInvalidated(
|
|
1311
|
-
options: ProxyResolutionOptions = {},
|
|
1312
|
-
): string | undefined {
|
|
1763
|
+
function markSmartproxyCacheInvalidated(options: ProxyResolutionOptions = {}): string | undefined {
|
|
1313
1764
|
const policy = resolvePolicy(options);
|
|
1314
1765
|
if (!policy || policy.mode === "disabled") {
|
|
1315
1766
|
return undefined;
|
|
1316
1767
|
}
|
|
1317
|
-
if (
|
|
1768
|
+
if (!resolveVendorChain(policy).includes("smartproxy")) {
|
|
1318
1769
|
return undefined;
|
|
1319
1770
|
}
|
|
1320
1771
|
|
|
1321
1772
|
const lifetimeMinutes = resolveSmartproxyLifetime(policy);
|
|
1773
|
+
const appKey = process.env[SMARTPROXY_APP_KEY_ENV]?.trim();
|
|
1774
|
+
if (!appKey) return undefined;
|
|
1322
1775
|
const cacheKey = buildSmartproxyCacheKey(
|
|
1323
1776
|
policy,
|
|
1777
|
+
appKey,
|
|
1324
1778
|
options.affinityKey,
|
|
1325
1779
|
lifetimeMinutes,
|
|
1780
|
+
options.protocol ?? VENDOR_DEFAULT_PROTOCOL.smartproxy,
|
|
1326
1781
|
);
|
|
1327
|
-
invalidatedProxyKeys.set(
|
|
1328
|
-
cacheKey,
|
|
1329
|
-
Date.now() + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS,
|
|
1330
|
-
);
|
|
1782
|
+
invalidatedProxyKeys.set(cacheKey, Date.now() + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS);
|
|
1331
1783
|
proxyCache.delete(cacheKey);
|
|
1332
1784
|
proxyInflight.delete(cacheKey);
|
|
1333
1785
|
return cacheKey;
|
|
1334
1786
|
}
|
|
1335
1787
|
|
|
1336
|
-
export function invalidateProxyResolutionCache(
|
|
1337
|
-
options: ProxyResolutionOptions = {},
|
|
1338
|
-
): boolean {
|
|
1788
|
+
export function invalidateProxyResolutionCache(options: ProxyResolutionOptions = {}): boolean {
|
|
1339
1789
|
return markSmartproxyCacheInvalidated(options) !== undefined;
|
|
1340
1790
|
}
|
|
1341
1791
|
|
|
@@ -1374,9 +1824,7 @@ async function importConfig(filePath: string): Promise<ApiFuseConfig | null> {
|
|
|
1374
1824
|
}
|
|
1375
1825
|
}
|
|
1376
1826
|
|
|
1377
|
-
export async function loadApiFuseConfig(
|
|
1378
|
-
dir: string = process.cwd(),
|
|
1379
|
-
): Promise<ApiFuseConfig> {
|
|
1827
|
+
export async function loadApiFuseConfig(dir: string = process.cwd()): Promise<ApiFuseConfig> {
|
|
1380
1828
|
const tsPath = path.resolve(dir, "apifuse.config.ts");
|
|
1381
1829
|
if (existsSync(tsPath)) {
|
|
1382
1830
|
const config = await importConfig(tsPath);
|