@apifuse/provider-sdk 2.2.0-beta.1 → 2.2.0-beta.11

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.
Files changed (261) hide show
  1. package/AUTHORING.md +106 -0
  2. package/CHANGELOG.md +41 -0
  3. package/README.md +5 -1
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +44 -59
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +27 -52
  8. package/bin/apifuse-pack-check.ts +14 -0
  9. package/bin/apifuse-pack-smoke.ts +22 -81
  10. package/bin/apifuse-pack-types.ts +266 -0
  11. package/bin/apifuse-perf.ts +45 -127
  12. package/bin/apifuse-record.ts +53 -70
  13. package/bin/apifuse-submit-check.ts +510 -45
  14. package/bin/apifuse-sync-assets.ts +117 -0
  15. package/bin/apifuse.ts +1 -1
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/bin/submit-check-xml-semantics.ts +204 -0
  18. package/bin/submit-check-xml.ts +134 -0
  19. package/dist/auth-turn/index.d.ts +3 -3
  20. package/dist/auth.d.ts +2 -2
  21. package/dist/auth.js +9 -18
  22. package/dist/ceremonies/index.d.ts +1 -1
  23. package/dist/ceremonies/index.js +8 -18
  24. package/dist/cli/commands.d.ts +1 -1
  25. package/dist/cli/commands.js +8 -0
  26. package/dist/cli/create.d.ts +3 -0
  27. package/dist/cli/create.js +34 -35
  28. package/dist/cli/prompt-assets.d.ts +80 -0
  29. package/dist/cli/prompt-assets.js +743 -0
  30. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  31. package/dist/config/loader.d.ts +149 -8
  32. package/dist/config/loader.js +378 -89
  33. package/dist/contract-serialization.d.ts +2 -2
  34. package/dist/contract-serialization.js +3 -6
  35. package/dist/contract-types.d.ts +2 -2
  36. package/dist/contract.d.ts +3 -3
  37. package/dist/contract.js +4 -6
  38. package/dist/define.d.ts +8 -1
  39. package/dist/define.js +119 -122
  40. package/dist/dev.d.ts +1 -1
  41. package/dist/dev.js +1 -1
  42. package/dist/errors.d.ts +4 -1
  43. package/dist/errors.js +48 -0
  44. package/dist/i18n/catalog.d.ts +2 -2
  45. package/dist/i18n/catalog.js +4 -10
  46. package/dist/i18n/index.d.ts +2 -2
  47. package/dist/i18n/index.js +2 -2
  48. package/dist/i18n/keys.d.ts +2 -2
  49. package/dist/index.d.ts +44 -42
  50. package/dist/index.js +39 -37
  51. package/dist/lint.js +8 -15
  52. package/dist/provider.d.ts +11 -11
  53. package/dist/provider.js +9 -9
  54. package/dist/public-schema-field-lint.d.ts +1 -1
  55. package/dist/recipes/gov-api.js +1 -1
  56. package/dist/runtime/auth-flow.d.ts +1 -1
  57. package/dist/runtime/auth-flow.js +3 -3
  58. package/dist/runtime/browser.d.ts +1 -1
  59. package/dist/runtime/browser.js +15 -29
  60. package/dist/runtime/cache.d.ts +1 -1
  61. package/dist/runtime/cache.js +4 -8
  62. package/dist/runtime/choice.d.ts +1 -1
  63. package/dist/runtime/choice.js +31 -35
  64. package/dist/runtime/credential.d.ts +1 -1
  65. package/dist/runtime/credential.js +1 -1
  66. package/dist/runtime/env.d.ts +1 -1
  67. package/dist/runtime/executor.d.ts +1 -1
  68. package/dist/runtime/executor.js +15 -3
  69. package/dist/runtime/http.d.ts +2 -2
  70. package/dist/runtime/http.js +144 -348
  71. package/dist/runtime/insights.d.ts +1 -1
  72. package/dist/runtime/insights.js +6 -13
  73. package/dist/runtime/instrumentation.d.ts +2 -2
  74. package/dist/runtime/instrumentation.js +56 -19
  75. package/dist/runtime/keyring.js +1 -1
  76. package/dist/runtime/namespace.js +1 -1
  77. package/dist/runtime/otlp.d.ts +1 -1
  78. package/dist/runtime/perf.d.ts +1 -1
  79. package/dist/runtime/provider.d.ts +1 -1
  80. package/dist/runtime/provider.js +1 -2
  81. package/dist/runtime/proxy-errors.d.ts +1 -1
  82. package/dist/runtime/proxy-errors.js +9 -7
  83. package/dist/runtime/proxy-nodemaven.d.ts +35 -0
  84. package/dist/runtime/proxy-nodemaven.js +128 -0
  85. package/dist/runtime/proxy-retry-policy.d.ts +40 -0
  86. package/dist/runtime/proxy-retry-policy.js +326 -0
  87. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  88. package/dist/runtime/proxy-telemetry.js +55 -52
  89. package/dist/runtime/redis.d.ts +1 -1
  90. package/dist/runtime/redis.js +2 -4
  91. package/dist/runtime/request-options.d.ts +1 -1
  92. package/dist/runtime/secrets.d.ts +27 -0
  93. package/dist/runtime/secrets.js +51 -0
  94. package/dist/runtime/state.d.ts +2 -2
  95. package/dist/runtime/state.js +15 -4
  96. package/dist/runtime/stealth.d.ts +2 -2
  97. package/dist/runtime/stealth.js +108 -212
  98. package/dist/runtime/stt.d.ts +1 -1
  99. package/dist/runtime/stt.js +11 -15
  100. package/dist/runtime/trace.d.ts +2 -2
  101. package/dist/runtime/trace.js +2 -4
  102. package/dist/runtime/waterfall.d.ts +1 -1
  103. package/dist/schema.d.ts +1 -1
  104. package/dist/schema.js +7 -15
  105. package/dist/serve.d.ts +1 -1
  106. package/dist/serve.js +1 -1
  107. package/dist/server/index.d.ts +7 -7
  108. package/dist/server/index.js +6 -6
  109. package/dist/server/self-test-redaction.d.ts +1 -1
  110. package/dist/server/self-test-redaction.js +1 -1
  111. package/dist/server/self-test.d.ts +104 -3
  112. package/dist/server/self-test.js +673 -115
  113. package/dist/server/serve.d.ts +105 -4
  114. package/dist/server/serve.js +588 -80
  115. package/dist/server/types.d.ts +10 -9
  116. package/dist/server/types.js +3 -7
  117. package/dist/stateful/errors.d.ts +14 -0
  118. package/dist/stateful/errors.js +14 -0
  119. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  120. package/dist/stateful/http-provider-event-emitter.js +237 -0
  121. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  122. package/dist/stateful/http-session-owner-registry.js +210 -0
  123. package/dist/stateful/index.d.ts +18 -0
  124. package/dist/stateful/index.js +18 -0
  125. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  126. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  127. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  128. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  129. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  130. package/dist/stateful/provider-event-pipeline.js +1 -0
  131. package/dist/stateful/provider-events.d.ts +101 -0
  132. package/dist/stateful/provider-events.js +289 -0
  133. package/dist/stateful/session-key.d.ts +15 -0
  134. package/dist/stateful/session-key.js +86 -0
  135. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  136. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  137. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  138. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  139. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  140. package/dist/stateful/stateful-provider-adapter.js +287 -0
  141. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  142. package/dist/stateful/stateful-provider-observability.js +161 -0
  143. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  144. package/dist/stateful/stateful-provider-owner-forwarder.js +207 -0
  145. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  146. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  147. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  148. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  149. package/dist/stateful/stateful-provider-session-routing.d.ts +71 -0
  150. package/dist/stateful/stateful-provider-session-routing.js +353 -0
  151. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  152. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  153. package/dist/stateful-signing.d.ts +18 -0
  154. package/dist/stateful-signing.js +27 -0
  155. package/dist/stealth/profiles.d.ts +1 -1
  156. package/dist/stealth/profiles.js +5 -14
  157. package/dist/stream.d.ts +1 -1
  158. package/dist/testing/index.d.ts +2 -2
  159. package/dist/testing/index.js +2 -2
  160. package/dist/testing/run.d.ts +1 -1
  161. package/dist/testing/run.js +7 -14
  162. package/dist/types.d.ts +87 -1
  163. package/dist/user-input.d.ts +30 -0
  164. package/dist/user-input.js +66 -0
  165. package/package.json +14 -5
  166. package/src/auth-turn/index.ts +1 -1
  167. package/src/auth.ts +28 -86
  168. package/src/ceremonies/index.ts +24 -78
  169. package/src/cli/commands.ts +10 -0
  170. package/src/cli/create.ts +42 -35
  171. package/src/cli/prompt-assets.ts +865 -0
  172. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  173. package/src/config/loader.ts +543 -208
  174. package/src/contract-serialization.ts +5 -11
  175. package/src/contract-types.ts +2 -2
  176. package/src/contract.ts +12 -28
  177. package/src/define.ts +247 -505
  178. package/src/dev.ts +4 -9
  179. package/src/errors.ts +58 -5
  180. package/src/i18n/catalog.ts +10 -32
  181. package/src/i18n/index.ts +2 -2
  182. package/src/i18n/keys.ts +5 -11
  183. package/src/index.ts +49 -42
  184. package/src/lint.ts +89 -159
  185. package/src/provider.ts +15 -11
  186. package/src/public-schema-field-lint.ts +7 -33
  187. package/src/recipes/gov-api.ts +2 -5
  188. package/src/runtime/auth-flow.ts +5 -7
  189. package/src/runtime/browser.ts +64 -187
  190. package/src/runtime/cache.ts +20 -67
  191. package/src/runtime/choice.ts +79 -132
  192. package/src/runtime/credential.ts +2 -2
  193. package/src/runtime/env.ts +1 -1
  194. package/src/runtime/executor.ts +23 -20
  195. package/src/runtime/http.ts +195 -559
  196. package/src/runtime/insights.ts +15 -53
  197. package/src/runtime/instrumentation.ts +78 -65
  198. package/src/runtime/keyring.ts +7 -19
  199. package/src/runtime/namespace.ts +2 -7
  200. package/src/runtime/otlp.ts +12 -23
  201. package/src/runtime/perf.ts +1 -1
  202. package/src/runtime/provider.ts +4 -9
  203. package/src/runtime/proxy-errors.ts +29 -42
  204. package/src/runtime/proxy-nodemaven.ts +178 -0
  205. package/src/runtime/proxy-retry-policy.ts +469 -0
  206. package/src/runtime/proxy-telemetry.ts +79 -77
  207. package/src/runtime/redis.ts +4 -12
  208. package/src/runtime/request-options.ts +4 -13
  209. package/src/runtime/secrets.ts +64 -0
  210. package/src/runtime/state.ts +41 -110
  211. package/src/runtime/stealth.ts +162 -377
  212. package/src/runtime/stt.ts +38 -94
  213. package/src/runtime/trace.ts +14 -44
  214. package/src/runtime/waterfall.ts +5 -18
  215. package/src/schema.ts +23 -84
  216. package/src/serve.ts +1 -1
  217. package/src/server/index.ts +24 -7
  218. package/src/server/self-test-redaction.ts +2 -2
  219. package/src/server/self-test.ts +857 -132
  220. package/src/server/serve.ts +859 -266
  221. package/src/server/types.ts +7 -19
  222. package/src/stateful/README.md +146 -0
  223. package/src/stateful/errors.ts +23 -0
  224. package/src/stateful/http-provider-event-emitter.ts +314 -0
  225. package/src/stateful/http-session-owner-registry.ts +306 -0
  226. package/src/stateful/index.ts +18 -0
  227. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  228. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  229. package/src/stateful/provider-event-pipeline.ts +61 -0
  230. package/src/stateful/provider-events.ts +462 -0
  231. package/src/stateful/session-key.ts +111 -0
  232. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  233. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  234. package/src/stateful/stateful-provider-adapter.ts +562 -0
  235. package/src/stateful/stateful-provider-observability.ts +261 -0
  236. package/src/stateful/stateful-provider-owner-forwarder.ts +279 -0
  237. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  238. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  239. package/src/stateful/stateful-provider-session-routing.ts +555 -0
  240. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  241. package/src/stateful-signing.ts +46 -0
  242. package/src/stealth/profiles.ts +10 -26
  243. package/src/stream.ts +8 -19
  244. package/src/testing/index.ts +2 -2
  245. package/src/testing/run.ts +19 -67
  246. package/src/types.ts +88 -1
  247. package/src/user-input.ts +118 -0
  248. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  249. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  250. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  251. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  252. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  253. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  254. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  255. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  256. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  257. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  258. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  259. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  260. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  261. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -0,0 +1,178 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+
3
+ import type { ProviderProxyPolicy } from "../types.js";
4
+
5
+ export const NODEMAVEN_USERNAME_ENV = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
6
+ export const NODEMAVEN_PASSWORD_ENV = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
7
+ export const NODEMAVEN_FILTER_ENV = "APIFUSE__PROXY__NODEMAVEN_FILTER";
8
+
9
+ export const NODEMAVEN_GATEWAY_HOST = "gate.nodemaven.com";
10
+
11
+ /** Both schemes tunnel bytes end-to-end, preserving the client TLS handshake. */
12
+ export type ProxyProtocol = "http" | "socks5";
13
+
14
+ /**
15
+ * NodeMaven's fastest protocol: HTTP CONNECT. Benchmarks (KR, cold + warm)
16
+ * showed socks5 through the gateway adds ~500ms per request over http, so
17
+ * NodeMaven never defaults to socks5.
18
+ */
19
+ export const NODEMAVEN_DEFAULT_PROTOCOL: ProxyProtocol = "http";
20
+
21
+ /** NodeMaven gateway port ranges per protocol (docs: HTTP 8080-9080, SOCKS5 1080-2080). */
22
+ const NODEMAVEN_PORTS: Record<ProxyProtocol, { min: number; max: number }> = {
23
+ http: { min: 8080, max: 9080 },
24
+ socks5: { min: 1080, max: 2080 },
25
+ };
26
+
27
+ const NODEMAVEN_FILTERS = new Set(["medium", "high"]);
28
+ const DEFAULT_NODEMAVEN_FILTER = "medium";
29
+ const DEFAULT_NODEMAVEN_POOL_SIZE = 20;
30
+ export const NODEMAVEN_MAX_POOL_SIZE = 50;
31
+ /** NodeMaven sticky sessions persist up to 24h server-side, keyed by the sid. */
32
+ const NODEMAVEN_MAX_LIFETIME_MINUTES = 1440;
33
+ const SID_LENGTH = 10;
34
+
35
+ export function hasNodemavenCredentials(): boolean {
36
+ return Boolean(readNodemavenUsername() && readNodemavenPassword());
37
+ }
38
+
39
+ function readNodemavenUsername(): string | undefined {
40
+ return process.env[NODEMAVEN_USERNAME_ENV]?.trim() || undefined;
41
+ }
42
+
43
+ function readNodemavenPassword(): string | undefined {
44
+ return process.env[NODEMAVEN_PASSWORD_ENV]?.trim() || undefined;
45
+ }
46
+
47
+ function resolveNodemavenFilter(): string {
48
+ const raw = process.env[NODEMAVEN_FILTER_ENV]?.trim().toLowerCase();
49
+ if (!raw) return DEFAULT_NODEMAVEN_FILTER;
50
+ if (!NODEMAVEN_FILTERS.has(raw)) {
51
+ throw new Error(`${NODEMAVEN_FILTER_ENV} must be "medium" or "high"`);
52
+ }
53
+ return raw;
54
+ }
55
+
56
+ export function nodemavenPoolSize(policy: ProviderProxyPolicy): number {
57
+ return Math.min(
58
+ NODEMAVEN_MAX_POOL_SIZE,
59
+ Math.max(1, Math.floor(policy.session?.poolSize ?? DEFAULT_NODEMAVEN_POOL_SIZE)),
60
+ );
61
+ }
62
+
63
+ function nodemavenLifetimeMinutes(policy: ProviderProxyPolicy): number {
64
+ const configured = policy.session?.lifetimeMinutes;
65
+ if (typeof configured !== "number" || !Number.isFinite(configured) || configured <= 0) {
66
+ return NODEMAVEN_MAX_LIFETIME_MINUTES;
67
+ }
68
+ return Math.min(NODEMAVEN_MAX_LIFETIME_MINUTES, Math.max(1, Math.floor(configured)));
69
+ }
70
+
71
+ /** NodeMaven username tokens accept `[a-z0-9]`; slugify geo values to that set. */
72
+ function slugifyGeo(value: string | undefined): string | undefined {
73
+ if (!value) return undefined;
74
+ const slug = value
75
+ .trim()
76
+ .toLowerCase()
77
+ .replace(/[^a-z0-9]+/g, "");
78
+ return slug || undefined;
79
+ }
80
+
81
+ function isStickyAffinity(policy: ProviderProxyPolicy): boolean {
82
+ return (policy.session?.affinity ?? "request") !== "request";
83
+ }
84
+
85
+ /**
86
+ * A sticky sid is deterministic from the affinity key so every process serving
87
+ * the same connection derives the same egress IP without shared storage. A
88
+ * rotating sid is random per call (a fresh egress IP per request).
89
+ */
90
+ function deriveSid(
91
+ policy: ProviderProxyPolicy,
92
+ affinityKey: string | undefined,
93
+ poolIndex: number,
94
+ refreshEpoch: number,
95
+ ): string {
96
+ if (!isStickyAffinity(policy) || !affinityKey) {
97
+ return randomBytes(SID_LENGTH).toString("hex").slice(0, SID_LENGTH);
98
+ }
99
+ const digest = createHash("sha256")
100
+ .update(`${affinityKey}:${refreshEpoch}:${poolIndex}`)
101
+ .digest("hex");
102
+ // hex digits are a subset of the allowed [a-z0-9] sid charset.
103
+ return digest.slice(0, SID_LENGTH);
104
+ }
105
+
106
+ function selectPort(protocol: ProxyProtocol, sid: string, poolIndex: number): number {
107
+ const { min, max } = NODEMAVEN_PORTS[protocol];
108
+ const span = max - min + 1;
109
+ const hashInt = Number.parseInt(
110
+ createHash("sha256").update(`${sid}:${poolIndex}`).digest("hex").slice(0, 8),
111
+ 16,
112
+ );
113
+ return min + (hashInt % span);
114
+ }
115
+
116
+ export type NodemavenSynthesisInput = {
117
+ policy: ProviderProxyPolicy;
118
+ affinityKey: string | undefined;
119
+ protocol: ProxyProtocol;
120
+ poolIndex: number;
121
+ refreshEpoch: number;
122
+ /** ISO 3166-1 alpha-2, already resolved by the caller (falls back to env). */
123
+ country?: string;
124
+ };
125
+
126
+ export type NodemavenSynthesis = {
127
+ url: string;
128
+ protocol: ProxyProtocol;
129
+ diagnostics: Record<string, string | number | boolean>;
130
+ };
131
+
132
+ /**
133
+ * Synthesize a NodeMaven gateway proxy URL locally from static credentials.
134
+ * There is no allocation API — geo/session are encoded in the username.
135
+ */
136
+ export function synthesizeNodemavenProxy(input: NodemavenSynthesisInput): NodemavenSynthesis {
137
+ const username = readNodemavenUsername();
138
+ const password = readNodemavenPassword();
139
+ if (!username || !password) {
140
+ throw new Error(
141
+ `NodeMaven credentials missing: set ${NODEMAVEN_USERNAME_ENV} and ${NODEMAVEN_PASSWORD_ENV}.`,
142
+ );
143
+ }
144
+
145
+ const filter = resolveNodemavenFilter();
146
+ const sid = deriveSid(input.policy, input.affinityKey, input.poolIndex, input.refreshEpoch);
147
+ const port = selectPort(input.protocol, sid, input.poolIndex);
148
+ const lifetimeMinutes = nodemavenLifetimeMinutes(input.policy);
149
+
150
+ const country = slugifyGeo(input.country ?? input.policy.geo?.country);
151
+ const region = slugifyGeo(input.policy.geo?.subdivision);
152
+ const city = slugifyGeo(input.policy.geo?.city);
153
+
154
+ const tokens = [username];
155
+ if (country) tokens.push("country", country);
156
+ if (region) tokens.push("region", region);
157
+ if (city) tokens.push("city", city);
158
+ tokens.push("sid", sid);
159
+ tokens.push("filter", filter);
160
+ tokens.push("ipv4", "true");
161
+ const proxyUsername = tokens.join("-");
162
+
163
+ // Username tokens are [a-z0-9-] only, which survive URL encoding unchanged.
164
+ const url = `${input.protocol}://${proxyUsername}:${encodeURIComponent(password)}@${NODEMAVEN_GATEWAY_HOST}:${port}`;
165
+
166
+ return {
167
+ url,
168
+ protocol: input.protocol,
169
+ diagnostics: {
170
+ vendor: "nodemaven",
171
+ protocol: input.protocol,
172
+ sticky: isStickyAffinity(input.policy),
173
+ filter,
174
+ lifetimeMinutes,
175
+ ...(country ? { country } : {}),
176
+ },
177
+ };
178
+ }
@@ -0,0 +1,469 @@
1
+ import { ProviderError, TransportError } from "../errors.js";
2
+ import type { HttpMethod, HttpRetryOptions, RequestOptions } from "../types.js";
3
+ import {
4
+ HttpRetryAfterPolicy,
5
+ HttpRetryDelayStrategy,
6
+ HttpRetryJitter,
7
+ HttpRetryPreset,
8
+ HttpRetryUnsafeMethodPolicy,
9
+ } from "../types.js";
10
+
11
+ export type NormalizedProxyTransportRetryOptions = Required<
12
+ Pick<
13
+ HttpRetryOptions,
14
+ | "attempts"
15
+ | "delayStrategy"
16
+ | "baseDelayMs"
17
+ | "maxDelayMs"
18
+ | "jitter"
19
+ | "retryAfter"
20
+ | "unsafeMethodPolicy"
21
+ >
22
+ > & {
23
+ preset?: HttpRetryPreset;
24
+ methods: readonly string[];
25
+ statusCodes: readonly number[];
26
+ errorCodes: readonly string[];
27
+ };
28
+
29
+ export const DEFAULT_PROXY_TRANSPORT_RETRY_METHODS = ["GET", "HEAD", "OPTIONS"] as const;
30
+ export const DEFAULT_PROXY_TRANSPORT_RETRY_ERROR_CODES = [
31
+ "transport_network_error",
32
+ "transport_timeout",
33
+ ] as const;
34
+ const SAFE_RETRY_STATUS_CODES = [408, 429, 500, 502, 503, 504] as const;
35
+ const RATE_LIMIT_RETRY_STATUS_CODES = [429, 503] as const;
36
+ const RATE_LIMIT_RETRY_ERROR_CODES = ["transport_timeout"] as const;
37
+ const KNOWN_RETRY_METHODS = new Set([
38
+ "GET",
39
+ "HEAD",
40
+ "POST",
41
+ "PUT",
42
+ "DELETE",
43
+ "OPTIONS",
44
+ "TRACE",
45
+ "PATCH",
46
+ ]);
47
+ const UNSAFE_RETRY_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE", "TRACE"]);
48
+ export const MAX_PROXY_TRANSPORT_RETRY_ATTEMPTS = 8;
49
+ const MAX_RETRY_DELAY_MS = 30_000;
50
+
51
+ type RetryPolicyLabel = "HTTP" | "Stealth" | "Proxy transport";
52
+
53
+ function hasOwnValue<T extends string>(values: Record<string, T>, value: unknown): value is T {
54
+ if (typeof value !== "string") return false;
55
+ return Object.values(values).some((candidate) => candidate === value);
56
+ }
57
+
58
+ function createInvalidRetryPolicyError(message: string, label: RetryPolicyLabel): ProviderError {
59
+ return new ProviderError(message.replace("{{label}}", label), {
60
+ code: "retry_invalid_policy",
61
+ });
62
+ }
63
+
64
+ function retryErrorCode(error: unknown): string | undefined {
65
+ if (error instanceof TransportError) {
66
+ return error.code;
67
+ }
68
+ if (error && typeof error === "object" && "code" in error) {
69
+ const code = Reflect.get(error, "code");
70
+ return typeof code === "string" ? code : undefined;
71
+ }
72
+ return undefined;
73
+ }
74
+
75
+ function retryErrorStatus(error: unknown): number | undefined {
76
+ if (error instanceof TransportError) {
77
+ return error.status ?? error.upstreamStatus;
78
+ }
79
+ return undefined;
80
+ }
81
+
82
+ export function proxyTransportRetryErrorCode(error: unknown): string | undefined {
83
+ return retryErrorCode(error);
84
+ }
85
+
86
+ export function proxyTransportRetryErrorStatus(error: unknown): number | undefined {
87
+ return retryErrorStatus(error);
88
+ }
89
+
90
+ function createRetryOptions(
91
+ preset: HttpRetryPreset,
92
+ extraErrorCodes: readonly string[],
93
+ label: RetryPolicyLabel,
94
+ ): NormalizedProxyTransportRetryOptions {
95
+ const defaultErrorCodes = [...DEFAULT_PROXY_TRANSPORT_RETRY_ERROR_CODES, ...extraErrorCodes];
96
+ switch (preset) {
97
+ case HttpRetryPreset.Off:
98
+ return {
99
+ preset,
100
+ attempts: 1,
101
+ methods: DEFAULT_PROXY_TRANSPORT_RETRY_METHODS,
102
+ statusCodes: [],
103
+ errorCodes: defaultErrorCodes,
104
+ delayStrategy: HttpRetryDelayStrategy.Exponential,
105
+ baseDelayMs: 100,
106
+ maxDelayMs: 1_000,
107
+ jitter: HttpRetryJitter.Full,
108
+ retryAfter: HttpRetryAfterPolicy.Ignore,
109
+ unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
110
+ };
111
+ case HttpRetryPreset.SafeRead:
112
+ return {
113
+ preset,
114
+ attempts: 3,
115
+ methods: DEFAULT_PROXY_TRANSPORT_RETRY_METHODS,
116
+ statusCodes: SAFE_RETRY_STATUS_CODES,
117
+ errorCodes: defaultErrorCodes,
118
+ delayStrategy: HttpRetryDelayStrategy.Exponential,
119
+ baseDelayMs: 100,
120
+ maxDelayMs: 2_000,
121
+ jitter: HttpRetryJitter.Full,
122
+ retryAfter: HttpRetryAfterPolicy.Cap,
123
+ unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
124
+ };
125
+ case HttpRetryPreset.AggressiveRead:
126
+ return {
127
+ preset,
128
+ attempts: 4,
129
+ methods: DEFAULT_PROXY_TRANSPORT_RETRY_METHODS,
130
+ statusCodes: SAFE_RETRY_STATUS_CODES,
131
+ errorCodes: defaultErrorCodes,
132
+ delayStrategy: HttpRetryDelayStrategy.Exponential,
133
+ baseDelayMs: 150,
134
+ maxDelayMs: 5_000,
135
+ jitter: HttpRetryJitter.Full,
136
+ retryAfter: HttpRetryAfterPolicy.Cap,
137
+ unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
138
+ };
139
+ case HttpRetryPreset.RateLimitAware:
140
+ return {
141
+ preset,
142
+ attempts: 3,
143
+ methods: DEFAULT_PROXY_TRANSPORT_RETRY_METHODS,
144
+ statusCodes: RATE_LIMIT_RETRY_STATUS_CODES,
145
+ errorCodes: RATE_LIMIT_RETRY_ERROR_CODES,
146
+ delayStrategy: HttpRetryDelayStrategy.Exponential,
147
+ baseDelayMs: 250,
148
+ maxDelayMs: 5_000,
149
+ jitter: HttpRetryJitter.Equal,
150
+ retryAfter: HttpRetryAfterPolicy.Respect,
151
+ unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
152
+ };
153
+ case HttpRetryPreset.TransportTransient:
154
+ return {
155
+ preset,
156
+ attempts: 3,
157
+ methods: DEFAULT_PROXY_TRANSPORT_RETRY_METHODS,
158
+ statusCodes: [],
159
+ errorCodes: defaultErrorCodes,
160
+ delayStrategy: HttpRetryDelayStrategy.Exponential,
161
+ baseDelayMs: 100,
162
+ maxDelayMs: 1_000,
163
+ jitter: HttpRetryJitter.Full,
164
+ retryAfter: HttpRetryAfterPolicy.Ignore,
165
+ unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
166
+ };
167
+ }
168
+ throw createInvalidRetryPolicyError(`Unknown ${label} retry preset: ${preset}`, label);
169
+ }
170
+
171
+ function clampPositiveInteger(value: number | undefined, fallback: number, max: number): number {
172
+ if (value === undefined) return fallback;
173
+ if (!Number.isFinite(value) || value < 1) return fallback;
174
+ return Math.min(Math.floor(value), max);
175
+ }
176
+
177
+ function clampDelay(value: number | undefined, fallback: number): number {
178
+ if (value === undefined) return fallback;
179
+ if (!Number.isFinite(value) || value < 0) return fallback;
180
+ return Math.min(Math.floor(value), MAX_RETRY_DELAY_MS);
181
+ }
182
+
183
+ export function createDefaultProxyTransportRetryOptions(
184
+ options: { extraErrorCodes?: readonly string[]; label?: RetryPolicyLabel } = {},
185
+ ): NormalizedProxyTransportRetryOptions {
186
+ return createRetryOptions(
187
+ HttpRetryPreset.TransportTransient,
188
+ options.extraErrorCodes ?? [],
189
+ options.label ?? "Proxy transport",
190
+ );
191
+ }
192
+
193
+ export function normalizeProxyTransportRetryOptions(
194
+ retry: RequestOptions["retry"],
195
+ options: {
196
+ extraErrorCodes?: readonly string[];
197
+ label?: RetryPolicyLabel;
198
+ } = {},
199
+ ): NormalizedProxyTransportRetryOptions | undefined {
200
+ const label = options.label ?? "Proxy transport";
201
+ const extraErrorCodes = options.extraErrorCodes ?? [];
202
+ if (retry === undefined || retry === false) {
203
+ return undefined;
204
+ }
205
+ if (retry === true) {
206
+ return createRetryOptions(HttpRetryPreset.TransportTransient, extraErrorCodes, label);
207
+ }
208
+ if (typeof retry === "string") {
209
+ if (!hasOwnValue(HttpRetryPreset, retry)) {
210
+ throw createInvalidRetryPolicyError(`Unknown ${label} retry preset: ${retry}`, label);
211
+ }
212
+ return createRetryOptions(retry, extraErrorCodes, label);
213
+ }
214
+ if (typeof retry !== "object" || retry === null) {
215
+ throw createInvalidRetryPolicyError(`${label} retry policy must be an object`, label);
216
+ }
217
+ if (Array.isArray(retry)) {
218
+ throw createInvalidRetryPolicyError(`${label} retry policy must be a plain object`, label);
219
+ }
220
+
221
+ validateRetryOptionsShape(retry, label);
222
+ const base = createRetryOptions(
223
+ retry.preset ?? HttpRetryPreset.TransportTransient,
224
+ extraErrorCodes,
225
+ label,
226
+ );
227
+ const maxDelayMs = clampDelay(retry.maxDelayMs, base.maxDelayMs);
228
+ return {
229
+ preset: retry.preset ?? base.preset,
230
+ attempts: clampPositiveInteger(
231
+ retry.attempts,
232
+ base.attempts,
233
+ MAX_PROXY_TRANSPORT_RETRY_ATTEMPTS,
234
+ ),
235
+ methods: retry.methods?.map((method) => method.toUpperCase()) ?? base.methods,
236
+ statusCodes:
237
+ retry.statusCodes?.filter((status) => Number.isInteger(status)) ?? base.statusCodes,
238
+ errorCodes: retry.errorCodes ?? base.errorCodes,
239
+ delayStrategy: retry.delayStrategy ?? base.delayStrategy,
240
+ baseDelayMs: clampDelay(retry.baseDelayMs, base.baseDelayMs),
241
+ maxDelayMs,
242
+ jitter: retry.jitter ?? base.jitter,
243
+ retryAfter: retry.retryAfter ?? base.retryAfter,
244
+ unsafeMethodPolicy: retry.unsafeMethodPolicy ?? base.unsafeMethodPolicy,
245
+ };
246
+ }
247
+
248
+ function validateRetryOptionsShape(retry: HttpRetryOptions, label: RetryPolicyLabel): void {
249
+ if (retry.preset !== undefined && !hasOwnValue(HttpRetryPreset, retry.preset)) {
250
+ throw createInvalidRetryPolicyError(
251
+ `Unknown ${label} retry preset: ${String(retry.preset)}`,
252
+ label,
253
+ );
254
+ }
255
+ if (
256
+ retry.delayStrategy !== undefined &&
257
+ !hasOwnValue(HttpRetryDelayStrategy, retry.delayStrategy)
258
+ ) {
259
+ throw createInvalidRetryPolicyError(
260
+ `Unknown ${label} retry delay strategy: ${String(retry.delayStrategy)}`,
261
+ label,
262
+ );
263
+ }
264
+ if (retry.jitter !== undefined && !hasOwnValue(HttpRetryJitter, retry.jitter)) {
265
+ throw createInvalidRetryPolicyError(
266
+ `Unknown ${label} retry jitter policy: ${String(retry.jitter)}`,
267
+ label,
268
+ );
269
+ }
270
+ if (retry.retryAfter !== undefined && !hasOwnValue(HttpRetryAfterPolicy, retry.retryAfter)) {
271
+ throw createInvalidRetryPolicyError(
272
+ `Unknown ${label} retry-after policy: ${String(retry.retryAfter)}`,
273
+ label,
274
+ );
275
+ }
276
+ if (
277
+ retry.unsafeMethodPolicy !== undefined &&
278
+ !hasOwnValue(HttpRetryUnsafeMethodPolicy, retry.unsafeMethodPolicy)
279
+ ) {
280
+ throw createInvalidRetryPolicyError(
281
+ `Unknown ${label} retry unsafe method policy: ${String(retry.unsafeMethodPolicy)}`,
282
+ label,
283
+ );
284
+ }
285
+ if (retry.methods !== undefined) {
286
+ if (!Array.isArray(retry.methods)) {
287
+ throw createInvalidRetryPolicyError(`${label} retry methods must be an array`, label);
288
+ }
289
+ const nonStringMethods = retry.methods.filter((method) => typeof method !== "string");
290
+ if (nonStringMethods.length > 0) {
291
+ throw createInvalidRetryPolicyError(
292
+ `${label} retry methods must contain only strings`,
293
+ label,
294
+ );
295
+ }
296
+ const unknownMethods = retry.methods
297
+ .map((method) => method.toUpperCase())
298
+ .filter((method) => !KNOWN_RETRY_METHODS.has(method));
299
+ if (unknownMethods.length > 0) {
300
+ throw createInvalidRetryPolicyError(
301
+ `Unknown ${label} retry method(s): ${unknownMethods.join(", ")}`,
302
+ label,
303
+ );
304
+ }
305
+ }
306
+ if (retry.statusCodes !== undefined) {
307
+ if (!Array.isArray(retry.statusCodes)) {
308
+ throw createInvalidRetryPolicyError(`${label} retry statusCodes must be an array`, label);
309
+ }
310
+ const invalidStatusCodes = retry.statusCodes.filter(
311
+ (status) => !Number.isInteger(status) || Number(status) < 100 || Number(status) > 599,
312
+ );
313
+ if (invalidStatusCodes.length > 0) {
314
+ throw createInvalidRetryPolicyError(
315
+ `${label} retry statusCodes must contain HTTP status integers in [100, 599]`,
316
+ label,
317
+ );
318
+ }
319
+ }
320
+ if (retry.errorCodes !== undefined) {
321
+ if (!Array.isArray(retry.errorCodes)) {
322
+ throw createInvalidRetryPolicyError(`${label} retry errorCodes must be an array`, label);
323
+ }
324
+ const nonStringErrorCodes = retry.errorCodes.filter(
325
+ (errorCode) => typeof errorCode !== "string",
326
+ );
327
+ if (nonStringErrorCodes.length > 0) {
328
+ throw createInvalidRetryPolicyError(
329
+ `${label} retry errorCodes must contain only strings`,
330
+ label,
331
+ );
332
+ }
333
+ }
334
+ if (
335
+ retry.preset === HttpRetryPreset.Off &&
336
+ ((retry.attempts !== undefined && retry.attempts > 1) ||
337
+ (retry.statusCodes !== undefined && retry.statusCodes.length > 0))
338
+ ) {
339
+ throw createInvalidRetryPolicyError(
340
+ `${label} retry preset off cannot be combined with retry-enabling overrides`,
341
+ label,
342
+ );
343
+ }
344
+ }
345
+
346
+ export function validateUnsafeProxyTransportRetryMethods(
347
+ options: NormalizedProxyTransportRetryOptions,
348
+ label: RetryPolicyLabel = "Proxy transport",
349
+ ): void {
350
+ if (options.unsafeMethodPolicy === HttpRetryUnsafeMethodPolicy.AllowExplicitUnsafe) {
351
+ return;
352
+ }
353
+ const unsafeMethods = options.methods.filter((method) =>
354
+ UNSAFE_RETRY_METHODS.has(method.toUpperCase()),
355
+ );
356
+ if (unsafeMethods.length === 0) return;
357
+
358
+ throw new ProviderError(
359
+ `${label} retry methods include unsafe method(s): ${unsafeMethods.join(", ")}`,
360
+ { code: "retry_unsafe_method" },
361
+ );
362
+ }
363
+
364
+ export function isProxyTransportRetryMethod(
365
+ method: HttpMethod | string,
366
+ options: NormalizedProxyTransportRetryOptions,
367
+ ): boolean {
368
+ return options.methods
369
+ .map((allowedMethod) => allowedMethod.toUpperCase())
370
+ .includes(method.toUpperCase());
371
+ }
372
+
373
+ export function shouldRetryProxyTransportError(
374
+ error: unknown,
375
+ options: NormalizedProxyTransportRetryOptions,
376
+ ): boolean {
377
+ const code = retryErrorCode(error);
378
+ return Boolean(code && options.errorCodes.includes(code));
379
+ }
380
+
381
+ export function shouldRetryProxyTransportAttempt(input: {
382
+ error: unknown;
383
+ explicitRetry: boolean;
384
+ method: HttpMethod | string;
385
+ options: NormalizedProxyTransportRetryOptions | undefined;
386
+ proxyUsed: boolean;
387
+ }): boolean {
388
+ const { error, explicitRetry, method, options, proxyUsed } = input;
389
+ if (!options || options.attempts <= 1) return false;
390
+ if (!explicitRetry && !proxyUsed) return false;
391
+ return (
392
+ isProxyTransportRetryMethod(method, options) && shouldRetryProxyTransportError(error, options)
393
+ );
394
+ }
395
+
396
+ function retryAfterHeader(headers: Record<string, string>): string | undefined {
397
+ for (const [name, value] of Object.entries(headers)) {
398
+ if (name.toLowerCase() === "retry-after") return value;
399
+ }
400
+ return undefined;
401
+ }
402
+
403
+ function parseRetryAfterMs(
404
+ headers: Record<string, string>,
405
+ now: number = Date.now(),
406
+ ): number | undefined {
407
+ const value = retryAfterHeader(headers);
408
+ if (!value) return undefined;
409
+ const seconds = Number(value);
410
+ if (Number.isFinite(seconds)) {
411
+ return Math.max(0, Math.floor(seconds * 1_000));
412
+ }
413
+ const dateMs = Date.parse(value);
414
+ if (!Number.isNaN(dateMs)) {
415
+ return Math.max(0, dateMs - now);
416
+ }
417
+ return undefined;
418
+ }
419
+
420
+ export function computeProxyTransportRetryDelayMs(
421
+ options: NormalizedProxyTransportRetryOptions,
422
+ attemptIndex: number,
423
+ headers?: Record<string, string>,
424
+ ): number {
425
+ const multiplier =
426
+ options.delayStrategy === HttpRetryDelayStrategy.Exponential
427
+ ? 2 ** Math.max(0, attemptIndex - 1)
428
+ : 1;
429
+ const configuredDelay = Math.min(options.baseDelayMs * multiplier, options.maxDelayMs);
430
+ const retryAfterMs =
431
+ options.retryAfter === HttpRetryAfterPolicy.Ignore
432
+ ? undefined
433
+ : headers
434
+ ? parseRetryAfterMs(headers)
435
+ : undefined;
436
+ if (retryAfterMs !== undefined) {
437
+ const boundedRetryAfterMs = Math.min(retryAfterMs, options.maxDelayMs);
438
+ if (options.retryAfter === HttpRetryAfterPolicy.Cap) {
439
+ return Math.min(boundedRetryAfterMs, configuredDelay);
440
+ }
441
+ return boundedRetryAfterMs;
442
+ }
443
+
444
+ switch (options.jitter) {
445
+ case HttpRetryJitter.None:
446
+ return configuredDelay;
447
+ case HttpRetryJitter.Equal:
448
+ return Math.floor(configuredDelay / 2 + Math.random() * (configuredDelay / 2));
449
+ case HttpRetryJitter.Full:
450
+ return Math.floor(Math.random() * configuredDelay);
451
+ }
452
+ }
453
+
454
+ export function normalizeProxyAttemptIndex(value: number | undefined): number {
455
+ if (value === undefined || !Number.isFinite(value)) return 0;
456
+ return Math.max(0, Math.floor(value));
457
+ }
458
+
459
+ export function computeProxyAttemptIndex(options: {
460
+ baseProxyAttempt?: number;
461
+ proxyAttemptOffset?: number;
462
+ retryAttemptOffset?: number;
463
+ }): number {
464
+ return (
465
+ normalizeProxyAttemptIndex(options.baseProxyAttempt) +
466
+ normalizeProxyAttemptIndex(options.proxyAttemptOffset) +
467
+ normalizeProxyAttemptIndex(options.retryAttemptOffset)
468
+ );
469
+ }