@apifuse/provider-sdk 2.2.0-beta.3 → 2.2.0-beta.30

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 (342) hide show
  1. package/AUTHORING.md +493 -5
  2. package/CHANGELOG.md +127 -1
  3. package/README.md +52 -6
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +106 -62
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +63 -55
  8. package/bin/apifuse-pack-check.ts +22 -2
  9. package/bin/apifuse-pack-smoke.ts +78 -82
  10. package/bin/apifuse-pack-types.ts +583 -0
  11. package/bin/apifuse-perf.ts +59 -140
  12. package/bin/apifuse-record.ts +698 -113
  13. package/bin/apifuse-submit-check.ts +517 -44
  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.ts +1 -1
  18. package/dist/auth-turn/index.d.ts +4 -4
  19. package/dist/auth-turn/index.js +1 -1
  20. package/dist/auth.d.ts +16 -2
  21. package/dist/auth.js +76 -18
  22. package/dist/ceremonies/index.d.ts +9 -1
  23. package/dist/ceremonies/index.js +117 -29
  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/cli/templates/provider/README.md.tpl +4 -4
  32. package/dist/config/loader.d.ts +176 -17
  33. package/dist/config/loader.js +434 -161
  34. package/dist/contract-serialization.d.ts +2 -2
  35. package/dist/contract-serialization.js +7 -14
  36. package/dist/contract-types.d.ts +3 -2
  37. package/dist/contract.d.ts +3 -3
  38. package/dist/contract.js +6 -6
  39. package/dist/declaration-validation.d.ts +23 -0
  40. package/dist/declaration-validation.js +159 -0
  41. package/dist/define.d.ts +13 -1
  42. package/dist/define.js +391 -122
  43. package/dist/dev.d.ts +1 -1
  44. package/dist/dev.js +1 -1
  45. package/dist/error-resolution.d.ts +4 -0
  46. package/dist/error-resolution.js +122 -0
  47. package/dist/errors.d.ts +19 -1
  48. package/dist/errors.js +41 -3
  49. package/dist/fixture-sanitization.d.ts +26 -0
  50. package/dist/fixture-sanitization.js +216 -0
  51. package/dist/i18n/catalog.d.ts +2 -2
  52. package/dist/i18n/catalog.js +4 -10
  53. package/dist/i18n/index.d.ts +2 -2
  54. package/dist/i18n/index.js +2 -2
  55. package/dist/i18n/keys.d.ts +2 -2
  56. package/dist/index.d.ts +50 -42
  57. package/dist/index.js +41 -37
  58. package/dist/lint.d.ts +6 -1
  59. package/dist/lint.js +370 -18
  60. package/dist/native-address.d.ts +43 -0
  61. package/dist/native-address.js +281 -0
  62. package/dist/native-egress-policy.d.ts +31 -0
  63. package/dist/native-egress-policy.js +288 -0
  64. package/dist/observability.d.ts +5 -2
  65. package/dist/observability.js +48 -1
  66. package/dist/provider.d.ts +13 -11
  67. package/dist/provider.js +10 -9
  68. package/dist/public-schema-field-lint.d.ts +1 -1
  69. package/dist/recipes/gov-api.js +1 -1
  70. package/dist/runtime/auth-flow.d.ts +3 -1
  71. package/dist/runtime/auth-flow.js +8 -3
  72. package/dist/runtime/browser.d.ts +1 -1
  73. package/dist/runtime/browser.js +138 -40
  74. package/dist/runtime/cache.d.ts +2 -1
  75. package/dist/runtime/cache.js +173 -23
  76. package/dist/runtime/choice-wordlist.d.ts +9 -0
  77. package/dist/runtime/choice-wordlist.js +138 -0
  78. package/dist/runtime/choice.d.ts +13 -1
  79. package/dist/runtime/choice.js +514 -124
  80. package/dist/runtime/credential.d.ts +1 -1
  81. package/dist/runtime/credential.js +1 -1
  82. package/dist/runtime/env.d.ts +1 -1
  83. package/dist/runtime/executor.d.ts +1 -1
  84. package/dist/runtime/executor.js +25 -3
  85. package/dist/runtime/http.d.ts +3 -2
  86. package/dist/runtime/http.js +517 -55
  87. package/dist/runtime/insights.d.ts +1 -1
  88. package/dist/runtime/insights.js +6 -13
  89. package/dist/runtime/instrumentation.d.ts +2 -2
  90. package/dist/runtime/instrumentation.js +371 -23
  91. package/dist/runtime/keyring.js +1 -1
  92. package/dist/runtime/namespace.js +1 -1
  93. package/dist/runtime/native-network-errors.d.ts +33 -0
  94. package/dist/runtime/native-network-errors.js +69 -0
  95. package/dist/runtime/native-network.d.ts +96 -0
  96. package/dist/runtime/native-network.js +1232 -0
  97. package/dist/runtime/ocr.d.ts +29 -0
  98. package/dist/runtime/ocr.js +440 -0
  99. package/dist/runtime/otlp.d.ts +1 -1
  100. package/dist/runtime/perf.d.ts +1 -1
  101. package/dist/runtime/provider.d.ts +1 -1
  102. package/dist/runtime/provider.js +1 -2
  103. package/dist/runtime/proxy-errors.d.ts +1 -1
  104. package/dist/runtime/proxy-errors.js +9 -7
  105. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  106. package/dist/runtime/proxy-nodemaven.js +146 -0
  107. package/dist/runtime/proxy-retry-policy.d.ts +2 -2
  108. package/dist/runtime/proxy-retry-policy.js +2 -2
  109. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  110. package/dist/runtime/proxy-telemetry.js +58 -52
  111. package/dist/runtime/redirects.d.ts +29 -0
  112. package/dist/runtime/redirects.js +36 -0
  113. package/dist/runtime/redis.d.ts +1 -1
  114. package/dist/runtime/redis.js +5 -5
  115. package/dist/runtime/request-options.d.ts +68 -1
  116. package/dist/runtime/request-options.js +548 -0
  117. package/dist/runtime/resolver-config.d.ts +6 -0
  118. package/dist/runtime/resolver-config.js +6 -0
  119. package/dist/runtime/resolver-public.d.ts +1 -0
  120. package/dist/runtime/resolver-public.js +1 -0
  121. package/dist/runtime/resolver-shared.d.ts +3 -0
  122. package/dist/runtime/resolver-shared.js +12 -0
  123. package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
  124. package/dist/runtime/resolver-vendors/bindings.js +40 -0
  125. package/dist/runtime/resolver-vendors/browser.d.ts +20 -0
  126. package/dist/runtime/resolver-vendors/browser.js +282 -0
  127. package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
  128. package/dist/runtime/resolver-vendors/hosts.js +33 -0
  129. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
  130. package/dist/runtime/resolver-vendors/twocaptcha.js +368 -0
  131. package/dist/runtime/resolver-vendors/types.d.ts +83 -0
  132. package/dist/runtime/resolver-vendors/types.js +69 -0
  133. package/dist/runtime/resolver.d.ts +59 -0
  134. package/dist/runtime/resolver.js +705 -0
  135. package/dist/runtime/secrets.d.ts +27 -0
  136. package/dist/runtime/secrets.js +51 -0
  137. package/dist/runtime/state.d.ts +5 -2
  138. package/dist/runtime/state.js +280 -74
  139. package/dist/runtime/stealth-cookies.d.ts +20 -0
  140. package/dist/runtime/stealth-cookies.js +111 -0
  141. package/dist/runtime/stealth.d.ts +30 -5
  142. package/dist/runtime/stealth.js +523 -259
  143. package/dist/runtime/stt.d.ts +1 -1
  144. package/dist/runtime/stt.js +12 -27
  145. package/dist/runtime/timeout.d.ts +5 -0
  146. package/dist/runtime/timeout.js +12 -0
  147. package/dist/runtime/trace.d.ts +2 -2
  148. package/dist/runtime/trace.js +2 -4
  149. package/dist/runtime/waterfall.d.ts +1 -1
  150. package/dist/schema.d.ts +1 -1
  151. package/dist/schema.js +7 -15
  152. package/dist/serve.d.ts +1 -1
  153. package/dist/serve.js +1 -1
  154. package/dist/server/index.d.ts +7 -7
  155. package/dist/server/index.js +6 -6
  156. package/dist/server/self-test-input-tokens.d.ts +2 -1
  157. package/dist/server/self-test-input-tokens.js +18 -14
  158. package/dist/server/self-test-redaction.d.ts +1 -1
  159. package/dist/server/self-test-redaction.js +1 -1
  160. package/dist/server/self-test.d.ts +117 -3
  161. package/dist/server/self-test.js +787 -151
  162. package/dist/server/serve-implementation.d.ts +210 -0
  163. package/dist/server/serve-implementation.js +2077 -0
  164. package/dist/server/serve.d.ts +1 -70
  165. package/dist/server/serve.js +1 -1143
  166. package/dist/server/types.d.ts +34 -9
  167. package/dist/server/types.js +8 -1
  168. package/dist/stateful/errors.d.ts +19 -0
  169. package/dist/stateful/errors.js +24 -0
  170. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  171. package/dist/stateful/http-provider-event-emitter.js +237 -0
  172. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  173. package/dist/stateful/http-session-owner-registry.js +210 -0
  174. package/dist/stateful/index.d.ts +18 -0
  175. package/dist/stateful/index.js +18 -0
  176. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  177. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  178. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  179. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  180. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  181. package/dist/stateful/provider-event-pipeline.js +1 -0
  182. package/dist/stateful/provider-events.d.ts +101 -0
  183. package/dist/stateful/provider-events.js +289 -0
  184. package/dist/stateful/session-key.d.ts +15 -0
  185. package/dist/stateful/session-key.js +86 -0
  186. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  187. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  188. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  189. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  190. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  191. package/dist/stateful/stateful-provider-adapter.js +287 -0
  192. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  193. package/dist/stateful/stateful-provider-observability.js +161 -0
  194. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  195. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  196. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  197. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  198. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  199. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  200. package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
  201. package/dist/stateful/stateful-provider-session-routing.js +345 -0
  202. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  203. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  204. package/dist/stateful-signing.d.ts +18 -0
  205. package/dist/stateful-signing.js +27 -0
  206. package/dist/stealth/profiles.d.ts +1 -1
  207. package/dist/stealth/profiles.js +21 -21
  208. package/dist/stream-evidence.d.ts +74 -0
  209. package/dist/stream-evidence.js +785 -0
  210. package/dist/stream.d.ts +1 -1
  211. package/dist/stream.js +7 -1
  212. package/dist/testing/index.d.ts +3 -2
  213. package/dist/testing/index.js +3 -2
  214. package/dist/testing/run.d.ts +32 -2
  215. package/dist/testing/run.js +488 -28
  216. package/dist/types.d.ts +566 -19
  217. package/dist/types.js +1 -0
  218. package/dist/user-input.d.ts +30 -0
  219. package/dist/user-input.js +66 -0
  220. package/package.json +42 -7
  221. package/src/auth-turn/index.ts +2 -2
  222. package/src/auth.ts +146 -86
  223. package/src/ceremonies/index.ts +167 -92
  224. package/src/cli/commands.ts +10 -0
  225. package/src/cli/create.ts +42 -35
  226. package/src/cli/prompt-assets.ts +865 -0
  227. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  228. package/src/cli/templates/provider/README.md.tpl +4 -4
  229. package/src/config/loader.ts +667 -289
  230. package/src/contract-serialization.ts +10 -18
  231. package/src/contract-types.ts +3 -2
  232. package/src/contract.ts +14 -28
  233. package/src/declaration-validation.ts +202 -0
  234. package/src/define.ts +631 -495
  235. package/src/dev.ts +4 -9
  236. package/src/error-resolution.ts +127 -0
  237. package/src/errors.ts +56 -11
  238. package/src/fixture-sanitization.ts +247 -0
  239. package/src/i18n/catalog.ts +10 -32
  240. package/src/i18n/index.ts +2 -2
  241. package/src/i18n/keys.ts +5 -11
  242. package/src/index.ts +157 -44
  243. package/src/lint.ts +488 -154
  244. package/src/native-address.ts +340 -0
  245. package/src/native-egress-policy.ts +358 -0
  246. package/src/observability.ts +51 -1
  247. package/src/provider.ts +65 -11
  248. package/src/public-schema-field-lint.ts +7 -33
  249. package/src/recipes/gov-api.ts +2 -5
  250. package/src/runtime/auth-flow.ts +13 -7
  251. package/src/runtime/browser.ts +252 -207
  252. package/src/runtime/cache.ts +209 -81
  253. package/src/runtime/choice-wordlist.ts +145 -0
  254. package/src/runtime/choice.ts +685 -216
  255. package/src/runtime/credential.ts +2 -2
  256. package/src/runtime/env.ts +1 -1
  257. package/src/runtime/executor.ts +37 -19
  258. package/src/runtime/http.ts +645 -65
  259. package/src/runtime/insights.ts +15 -53
  260. package/src/runtime/instrumentation.ts +530 -67
  261. package/src/runtime/keyring.ts +7 -19
  262. package/src/runtime/namespace.ts +2 -7
  263. package/src/runtime/native-network-errors.ts +99 -0
  264. package/src/runtime/native-network.ts +1605 -0
  265. package/src/runtime/ocr.ts +523 -0
  266. package/src/runtime/otlp.ts +12 -23
  267. package/src/runtime/perf.ts +1 -1
  268. package/src/runtime/provider.ts +4 -9
  269. package/src/runtime/proxy-errors.ts +29 -42
  270. package/src/runtime/proxy-nodemaven.ts +221 -0
  271. package/src/runtime/proxy-retry-policy.ts +3 -3
  272. package/src/runtime/proxy-telemetry.ts +84 -77
  273. package/src/runtime/redirects.ts +66 -0
  274. package/src/runtime/redis.ts +10 -13
  275. package/src/runtime/request-options.ts +679 -9
  276. package/src/runtime/resolver-config.ts +6 -0
  277. package/src/runtime/resolver-public.ts +18 -0
  278. package/src/runtime/resolver-shared.ts +17 -0
  279. package/src/runtime/resolver-vendors/bindings.ts +56 -0
  280. package/src/runtime/resolver-vendors/browser.ts +408 -0
  281. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  282. package/src/runtime/resolver-vendors/twocaptcha.ts +500 -0
  283. package/src/runtime/resolver-vendors/types.ts +173 -0
  284. package/src/runtime/resolver.ts +1060 -0
  285. package/src/runtime/secrets.ts +64 -0
  286. package/src/runtime/state.ts +399 -161
  287. package/src/runtime/stealth-cookies.ts +132 -0
  288. package/src/runtime/stealth.ts +681 -295
  289. package/src/runtime/stt.ts +39 -113
  290. package/src/runtime/timeout.ts +18 -0
  291. package/src/runtime/trace.ts +14 -44
  292. package/src/runtime/waterfall.ts +5 -18
  293. package/src/schema.ts +23 -84
  294. package/src/serve.ts +6 -1
  295. package/src/server/index.ts +30 -7
  296. package/src/server/self-test-input-tokens.ts +29 -14
  297. package/src/server/self-test-redaction.ts +2 -2
  298. package/src/server/self-test.ts +1030 -180
  299. package/src/server/serve-implementation.ts +3060 -0
  300. package/src/server/serve.ts +1 -1781
  301. package/src/server/types.ts +12 -13
  302. package/src/stateful/README.md +146 -0
  303. package/src/stateful/errors.ts +35 -0
  304. package/src/stateful/http-provider-event-emitter.ts +314 -0
  305. package/src/stateful/http-session-owner-registry.ts +306 -0
  306. package/src/stateful/index.ts +18 -0
  307. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  308. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  309. package/src/stateful/provider-event-pipeline.ts +61 -0
  310. package/src/stateful/provider-events.ts +462 -0
  311. package/src/stateful/session-key.ts +111 -0
  312. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  313. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  314. package/src/stateful/stateful-provider-adapter.ts +562 -0
  315. package/src/stateful/stateful-provider-observability.ts +261 -0
  316. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  317. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  318. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  319. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  320. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  321. package/src/stateful-signing.ts +46 -0
  322. package/src/stealth/profiles.ts +27 -33
  323. package/src/stream-evidence.ts +988 -0
  324. package/src/stream.ts +16 -20
  325. package/src/testing/index.ts +11 -2
  326. package/src/testing/run.ts +668 -74
  327. package/src/types.ts +665 -35
  328. package/src/user-input.ts +118 -0
  329. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  330. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  331. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  332. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  333. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  334. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  335. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  336. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  337. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  338. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  339. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  340. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  341. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  342. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -1,4 +1,4 @@
1
- import { TransportError } from "../errors";
1
+ import { TransportError } from "../errors.js";
2
2
 
3
3
  export const PROXY_AUTH_IP_DENIED_CODE = "PROXY_AUTH_IP_DENIED";
4
4
  export const PROXY_AUTH_IP_DENIED_MESSAGE =
@@ -19,10 +19,18 @@ const PROXY_AUTH_IP_DENIED_PATTERN =
19
19
  /\b(?:source|egress|client)\s+ip\b.{0,120}\b(?:deny|denied|unauthori[sz]ed|not\s+authori[sz]ed|white\s*list|allow\s*list)\b|\b(?:white\s*list|allow\s*list)\b.{0,120}\b(?:source|egress|client)\s+ip\b/i;
20
20
  const PROXY_EDGE_AUTH_REJECTED_PATTERN =
21
21
  /\bauth\s+ip\s+err\b|\bproxy\b.{0,120}\bauth(?:entication)?\b.{0,120}\b(?:reject(?:ed)?|fail(?:ed)?|invalid|den(?:y|ied)|unauthori[sz]ed)\b|\bauth(?:entication)?\b.{0,120}\b(?:reject(?:ed)?|fail(?:ed)?|invalid|den(?:y|ied)|unauthori[sz]ed)\b.{0,120}\bproxy\b/i;
22
- const PROXY_POOL_STALE_MESSAGE_PATTERN =
23
- /\bproxy\b.{0,120}\b(?:pool|lease|expired|unavailable|exhausted|non[\s-]?200\s+code:\s*(?:509|512))\b|\bnon[\s-]?200\s+code:\s*(?:509|512)\b.{0,120}\bproxy\b|\bsmartproxy\b.{0,120}\b(?:509|512)\b/i;
24
- const PROXY_EDGE_TLS_REJECTED_MESSAGE_PATTERN =
25
- /\b(?:smartproxy|proxy)\b.{0,160}\b(?:495|ssl|tls|cert(?:ificate)?|handshake|edge|connect|non[\s-]?200)\b|\b(?:495|ssl|tls|cert(?:ificate)?|handshake|edge|connect|non[\s-]?200)\b.{0,160}\b(?:smartproxy|proxy)\b/i;
22
+ // Vendor host tokens that can appear in upstream error strings. Adding a proxy
23
+ // vendor updates every classifier below in one place. `proxy` is the generic
24
+ // fallback so vendor-agnostic messages still classify.
25
+ const PROXY_VENDOR_ALTERNATION = "smartproxy|nodemaven|proxy";
26
+ const PROXY_POOL_STALE_MESSAGE_PATTERN = new RegExp(
27
+ `\\bproxy\\b.{0,120}\\b(?:pool|lease|expired|unavailable|exhausted|non[\\s-]?200\\s+code:\\s*(?:509|512))\\b|\\bnon[\\s-]?200\\s+code:\\s*(?:509|512)\\b.{0,120}\\bproxy\\b|\\b(?:${PROXY_VENDOR_ALTERNATION})\\b.{0,120}\\b(?:509|512)\\b`,
28
+ "i",
29
+ );
30
+ const PROXY_EDGE_TLS_REJECTED_MESSAGE_PATTERN = new RegExp(
31
+ `\\b(?:${PROXY_VENDOR_ALTERNATION})\\b.{0,160}\\b(?:495|ssl|tls|cert(?:ificate)?|handshake|edge|connect|non[\\s-]?200)\\b|\\b(?:495|ssl|tls|cert(?:ificate)?|handshake|edge|connect|non[\\s-]?200)\\b.{0,160}\\b(?:${PROXY_VENDOR_ALTERNATION})\\b`,
32
+ "i",
33
+ );
26
34
 
27
35
  export function isProxyAuthIpDeniedMessage(message: string): boolean {
28
36
  return PROXY_AUTH_IP_DENIED_PATTERN.test(message);
@@ -39,9 +47,7 @@ export function isProxyEdgeAuthRejectedMessage(message: string): boolean {
39
47
  return PROXY_EDGE_AUTH_REJECTED_PATTERN.test(message);
40
48
  }
41
49
 
42
- export function createProxyEdgeAuthRejectedError(
43
- cause?: Error,
44
- ): TransportError {
50
+ export function createProxyEdgeAuthRejectedError(cause?: Error): TransportError {
45
51
  return new TransportError(PROXY_EDGE_AUTH_REJECTED_MESSAGE, {
46
52
  code: PROXY_EDGE_AUTH_REJECTED_CODE,
47
53
  cause,
@@ -52,10 +58,7 @@ export function isProxyPoolStaleStatus(status: number): boolean {
52
58
  return PROXY_POOL_STALE_STATUS_CODES.has(status);
53
59
  }
54
60
 
55
- export function isProxyEdgeTlsRejectedResponse(
56
- status: number,
57
- evidence: string,
58
- ): boolean {
61
+ export function isProxyEdgeTlsRejectedResponse(status: number, evidence: string): boolean {
59
62
  return (
60
63
  PROXY_EDGE_TLS_REJECTED_STATUS_CODES.has(status) &&
61
64
  PROXY_EDGE_TLS_REJECTED_MESSAGE_PATTERN.test(evidence)
@@ -67,10 +70,7 @@ export function isProxyPoolStaleMessage(message: string): boolean {
67
70
  }
68
71
 
69
72
  export function isProxyPoolRefreshableError(error: unknown): boolean {
70
- if (
71
- error instanceof TransportError &&
72
- error.code === PROXY_AUTH_IP_DENIED_CODE
73
- ) {
73
+ if (error instanceof TransportError && error.code === PROXY_AUTH_IP_DENIED_CODE) {
74
74
  return false;
75
75
  }
76
76
 
@@ -89,39 +89,26 @@ export function isProxyPoolRefreshableError(error: unknown): boolean {
89
89
  cause instanceof Error ? cause.message : "",
90
90
  ].join(" ");
91
91
  return (
92
- PROXY_POOL_STALE_MESSAGE_PATTERN.test(message) ||
93
- PROXY_EDGE_AUTH_REJECTED_PATTERN.test(message)
92
+ PROXY_POOL_STALE_MESSAGE_PATTERN.test(message) || PROXY_EDGE_AUTH_REJECTED_PATTERN.test(message)
94
93
  );
95
94
  }
96
95
 
97
96
  export const isProxyPoolStaleError = isProxyPoolRefreshableError;
98
97
 
99
- export function createProxyPoolStaleError(
100
- status: number,
101
- cause?: Error,
102
- ): TransportError {
103
- return new TransportError(
104
- `Proxy provider pool failed with status ${status}`,
105
- {
106
- code: PROXY_POOL_STALE_CODE,
107
- status,
108
- cause,
109
- },
110
- );
98
+ export function createProxyPoolStaleError(status: number, cause?: Error): TransportError {
99
+ return new TransportError(`Proxy provider pool failed with status ${status}`, {
100
+ code: PROXY_POOL_STALE_CODE,
101
+ status,
102
+ cause,
103
+ });
111
104
  }
112
105
 
113
- export function createProxyEdgeTlsRejectedError(
114
- status: number,
115
- cause?: Error,
116
- ): TransportError {
117
- return new TransportError(
118
- `Proxy edge TLS request was rejected with status ${status}`,
119
- {
120
- code: PROXY_EDGE_TLS_REJECTED_CODE,
121
- status,
122
- cause,
123
- },
124
- );
106
+ export function createProxyEdgeTlsRejectedError(status: number, cause?: Error): TransportError {
107
+ return new TransportError(`Proxy edge TLS request was rejected with status ${status}`, {
108
+ code: PROXY_EDGE_TLS_REJECTED_CODE,
109
+ status,
110
+ cause,
111
+ });
125
112
  }
126
113
 
127
114
  export function createProxyPoolExhaustedError(cause?: Error): TransportError {
@@ -0,0 +1,221 @@
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
+ export type NodemavenCredentials = {
12
+ readonly username: string;
13
+ readonly password: string;
14
+ readonly filter?: string;
15
+ };
16
+
17
+ /** Both schemes tunnel bytes end-to-end, preserving the client TLS handshake. */
18
+ export type ProxyProtocol = "http" | "socks5";
19
+
20
+ /**
21
+ * NodeMaven's fastest protocol: HTTP CONNECT. Benchmarks (KR, cold + warm)
22
+ * showed socks5 through the gateway adds ~500ms per request over http, so
23
+ * NodeMaven never defaults to socks5.
24
+ */
25
+ export const NODEMAVEN_DEFAULT_PROTOCOL: ProxyProtocol = "http";
26
+
27
+ /** NodeMaven gateway port ranges per protocol (docs: HTTP 8080-9080, SOCKS5 1080-2080). */
28
+ const NODEMAVEN_PORTS: Record<ProxyProtocol, { min: number; max: number }> = {
29
+ http: { min: 8080, max: 9080 },
30
+ socks5: { min: 1080, max: 2080 },
31
+ };
32
+
33
+ const NODEMAVEN_FILTERS = new Set(["medium", "high"]);
34
+ const DEFAULT_NODEMAVEN_FILTER = "medium";
35
+ const DEFAULT_NODEMAVEN_POOL_SIZE = 20;
36
+ export const NODEMAVEN_MAX_POOL_SIZE = 50;
37
+ /** NodeMaven sticky sessions persist up to 24h server-side, keyed by the sid. */
38
+ const NODEMAVEN_MAX_LIFETIME_MINUTES = 1440;
39
+ const SID_LENGTH = 10;
40
+
41
+ export function hasNodemavenCredentials(): boolean {
42
+ return Boolean(readNodemavenUsername() && readNodemavenPassword());
43
+ }
44
+
45
+ function readNodemavenUsername(): string | undefined {
46
+ return process.env[NODEMAVEN_USERNAME_ENV]?.trim() || undefined;
47
+ }
48
+
49
+ function readNodemavenPassword(): string | undefined {
50
+ return process.env[NODEMAVEN_PASSWORD_ENV]?.trim() || undefined;
51
+ }
52
+
53
+ function resolveNodemavenFilter(value: string | undefined): string {
54
+ const raw = value?.trim().toLowerCase();
55
+ if (!raw) return DEFAULT_NODEMAVEN_FILTER;
56
+ if (!NODEMAVEN_FILTERS.has(raw)) {
57
+ throw new Error(`${NODEMAVEN_FILTER_ENV} must be "medium" or "high"`);
58
+ }
59
+ return raw;
60
+ }
61
+
62
+ export function nodemavenPoolSize(policy: ProviderProxyPolicy): number {
63
+ return Math.min(
64
+ NODEMAVEN_MAX_POOL_SIZE,
65
+ Math.max(1, Math.floor(policy.session?.poolSize ?? DEFAULT_NODEMAVEN_POOL_SIZE)),
66
+ );
67
+ }
68
+
69
+ export function nodemavenLifetimeMinutes(policy: ProviderProxyPolicy): number {
70
+ const configured = policy.session?.lifetimeMinutes;
71
+ if (typeof configured !== "number" || !Number.isFinite(configured) || configured <= 0) {
72
+ return NODEMAVEN_MAX_LIFETIME_MINUTES;
73
+ }
74
+ return Math.min(NODEMAVEN_MAX_LIFETIME_MINUTES, Math.max(1, Math.floor(configured)));
75
+ }
76
+
77
+ export type NodemavenSessionWindow = {
78
+ refreshEpoch: number;
79
+ expiresAt: string;
80
+ };
81
+
82
+ /** Stable wall-clock window shared by every process deriving the same sticky sid. */
83
+ export function nodemavenSessionWindow(
84
+ policy: ProviderProxyPolicy,
85
+ now = Date.now(),
86
+ ): NodemavenSessionWindow {
87
+ const lifetimeMs = nodemavenLifetimeMinutes(policy) * 60_000;
88
+ const refreshEpoch = Math.floor(now / lifetimeMs);
89
+ return {
90
+ refreshEpoch,
91
+ expiresAt: new Date((refreshEpoch + 1) * lifetimeMs).toISOString(),
92
+ };
93
+ }
94
+
95
+ /** NodeMaven username tokens accept `[a-z0-9]`; slugify geo values to that set. */
96
+ function slugifyGeo(value: string | undefined): string | undefined {
97
+ if (!value) return undefined;
98
+ const slug = value
99
+ .trim()
100
+ .toLowerCase()
101
+ .replace(/[^a-z0-9]+/g, "");
102
+ return slug || undefined;
103
+ }
104
+
105
+ function isStickyAffinity(policy: ProviderProxyPolicy): boolean {
106
+ return (policy.session?.affinity ?? "request") !== "request";
107
+ }
108
+
109
+ /**
110
+ * A sticky sid is deterministic from the affinity key so every process serving
111
+ * the same connection derives the same egress IP without shared storage. A
112
+ * rotating sid is random per call (a fresh egress IP per request).
113
+ */
114
+ function deriveSid(
115
+ policy: ProviderProxyPolicy,
116
+ affinityKey: string | undefined,
117
+ poolIndex: number,
118
+ refreshEpoch: number,
119
+ ): string {
120
+ if (!isStickyAffinity(policy) || !affinityKey) {
121
+ return randomBytes(SID_LENGTH).toString("hex").slice(0, SID_LENGTH);
122
+ }
123
+ const digest = createHash("sha256")
124
+ .update(`${affinityKey}:${refreshEpoch}:${poolIndex}`)
125
+ .digest("hex");
126
+ // hex digits are a subset of the allowed [a-z0-9] sid charset.
127
+ return digest.slice(0, SID_LENGTH);
128
+ }
129
+
130
+ function selectPort(protocol: ProxyProtocol, sid: string, poolIndex: number): number {
131
+ const { min, max } = NODEMAVEN_PORTS[protocol];
132
+ const span = max - min + 1;
133
+ const hashInt = Number.parseInt(
134
+ createHash("sha256").update(`${sid}:${poolIndex}`).digest("hex").slice(0, 8),
135
+ 16,
136
+ );
137
+ return min + (hashInt % span);
138
+ }
139
+
140
+ export type NodemavenSynthesisInput = {
141
+ policy: ProviderProxyPolicy;
142
+ /** Explicit credentials; synthesis never reads process-global state. */
143
+ credentials: NodemavenCredentials;
144
+ affinityKey: string | undefined;
145
+ protocol: ProxyProtocol;
146
+ poolIndex: number;
147
+ refreshEpoch: number;
148
+ /** ISO 3166-1 alpha-2, already resolved by the caller (falls back to env). */
149
+ country?: string;
150
+ /** Clock injection used by native sticky-expiry lifecycle tests. */
151
+ now?: number;
152
+ /** Stable caller-owned hard expiry when the refresh epoch is wall-clock-derived. */
153
+ expiresAt?: string;
154
+ };
155
+
156
+ export type NodemavenSynthesis = {
157
+ url: string;
158
+ protocol: ProxyProtocol;
159
+ sticky: boolean;
160
+ sessionId: string;
161
+ expiresAt?: string;
162
+ diagnostics: Record<string, string | number | boolean>;
163
+ };
164
+
165
+ /**
166
+ * Synthesize a NodeMaven gateway proxy URL locally from static credentials.
167
+ * There is no allocation API — geo/session are encoded in the username.
168
+ */
169
+ export function synthesizeNodemavenProxy(input: NodemavenSynthesisInput): NodemavenSynthesis {
170
+ const username = input.credentials.username.trim();
171
+ const password = input.credentials.password.trim();
172
+ if (!username || !password) {
173
+ throw new Error(
174
+ `NodeMaven credentials missing: set ${NODEMAVEN_USERNAME_ENV} and ${NODEMAVEN_PASSWORD_ENV}.`,
175
+ );
176
+ }
177
+
178
+ const filter = resolveNodemavenFilter(input.credentials.filter);
179
+ const sid = deriveSid(input.policy, input.affinityKey, input.poolIndex, input.refreshEpoch);
180
+ const port = selectPort(input.protocol, sid, input.poolIndex);
181
+ const lifetimeMinutes = nodemavenLifetimeMinutes(input.policy);
182
+ const sticky = isStickyAffinity(input.policy);
183
+
184
+ const country = slugifyGeo(input.country ?? input.policy.geo?.country);
185
+ const region = slugifyGeo(input.policy.geo?.subdivision);
186
+ const city = slugifyGeo(input.policy.geo?.city);
187
+
188
+ const tokens = [username];
189
+ if (country) tokens.push("country", country);
190
+ if (region) tokens.push("region", region);
191
+ if (city) tokens.push("city", city);
192
+ tokens.push("sid", sid);
193
+ tokens.push("filter", filter);
194
+ tokens.push("ipv4", "true");
195
+ const proxyUsername = tokens.join("-");
196
+
197
+ // Username tokens are [a-z0-9-] only, which survive URL encoding unchanged.
198
+ const url = `${input.protocol}://${proxyUsername}:${encodeURIComponent(password)}@${NODEMAVEN_GATEWAY_HOST}:${port}`;
199
+
200
+ return {
201
+ url,
202
+ protocol: input.protocol,
203
+ sticky,
204
+ sessionId: sid,
205
+ ...(sticky
206
+ ? {
207
+ expiresAt:
208
+ input.expiresAt ??
209
+ new Date((input.now ?? Date.now()) + lifetimeMinutes * 60_000).toISOString(),
210
+ }
211
+ : {}),
212
+ diagnostics: {
213
+ vendor: "nodemaven",
214
+ protocol: input.protocol,
215
+ sticky,
216
+ filter,
217
+ lifetimeMinutes,
218
+ ...(country ? { country } : {}),
219
+ },
220
+ };
221
+ }
@@ -1,12 +1,12 @@
1
- import { ProviderError, TransportError } from "../errors";
2
- import type { HttpMethod, HttpRetryOptions, RequestOptions } from "../types";
1
+ import { ProviderError, TransportError } from "../errors.js";
2
+ import type { HttpMethod, HttpRetryOptions, RequestOptions } from "../types.js";
3
3
  import {
4
4
  HttpRetryAfterPolicy,
5
5
  HttpRetryDelayStrategy,
6
6
  HttpRetryJitter,
7
7
  HttpRetryPreset,
8
8
  HttpRetryUnsafeMethodPolicy,
9
- } from "../types";
9
+ } from "../types.js";
10
10
 
11
11
  export type NormalizedProxyTransportRetryOptions = Required<
12
12
  Pick<
@@ -1,17 +1,23 @@
1
1
  import type {
2
2
  ProxyAttemptTelemetryEvent,
3
3
  ProxyCacheStatus,
4
+ ProxyProtocol,
4
5
  ProxyResolutionTelemetryEvent,
5
6
  ProxyTelemetrySink,
7
+ ProxyUserAgentSource,
8
+ ProxyVendorFailoverTelemetryEvent,
9
+ ProxyVendorName,
6
10
  SmartproxyAllocatorBodyClass,
7
- } from "../config/loader";
11
+ } from "../config/loader.js";
8
12
 
9
13
  export const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
10
14
 
11
15
  type ProviderTelemetryHeader = {
12
16
  v: 1;
13
17
  proxy?: {
14
- provider: "smartproxy";
18
+ provider: ProxyVendorName;
19
+ userAgentSource?: ProxyUserAgentSource;
20
+ protocol?: ProxyProtocol;
15
21
  cacheStatus: ProxyCacheStatus;
16
22
  cacheHit: boolean;
17
23
  resolutionMs: number;
@@ -27,6 +33,10 @@ type ProviderTelemetryHeader = {
27
33
  attempts: number;
28
34
  refreshes?: number;
29
35
  attemptSamples?: CompactProxyAttemptSample[];
36
+ /** Distinct vendors attempted across the resolution chain, in order seen. */
37
+ vendors?: ProxyVendorName[];
38
+ /** Cross-vendor failover events (bounded). */
39
+ failovers?: CompactVendorFailoverSample[];
30
40
  };
31
41
  };
32
42
 
@@ -41,8 +51,17 @@ type CompactProxyAttemptSample = {
41
51
  d?: number;
42
52
  };
43
53
 
54
+ type CompactVendorFailoverSample = {
55
+ v: ProxyVendorName;
56
+ nx?: ProxyVendorName;
57
+ p: "resolution" | "transport";
58
+ r: ProxyVendorFailoverTelemetryEvent["reason"];
59
+ a?: number;
60
+ };
61
+
44
62
  const MAX_HEADER_BYTES = 4_096;
45
63
  const MAX_PROXY_ATTEMPT_SAMPLES = 24;
64
+ const MAX_PROXY_FAILOVER_SAMPLES = 12;
46
65
 
47
66
  const CACHE_STATUS_SEVERITY: Record<ProxyCacheStatus, number> = {
48
67
  disabled: 0,
@@ -55,31 +74,18 @@ const CACHE_STATUS_SEVERITY: Record<ProxyCacheStatus, number> = {
55
74
  allocator: 7,
56
75
  };
57
76
 
58
- function sumOptional(
59
- left: number | undefined,
60
- right: number | undefined,
61
- ): number | undefined {
77
+ function sumOptional(left: number | undefined, right: number | undefined): number | undefined {
62
78
  const total = (left ?? 0) + (right ?? 0);
63
79
  return total > 0 ? total : undefined;
64
80
  }
65
81
 
66
- function maxOptional(
67
- left: number | undefined,
68
- right: number | undefined,
69
- ): number | undefined {
70
- const values = [left, right].filter(
71
- (value): value is number => typeof value === "number",
72
- );
82
+ function maxOptional(left: number | undefined, right: number | undefined): number | undefined {
83
+ const values = [left, right].filter((value): value is number => typeof value === "number");
73
84
  return values.length > 0 ? Math.max(...values) : undefined;
74
85
  }
75
86
 
76
- function worseStatus(
77
- left: ProxyCacheStatus,
78
- right: ProxyCacheStatus,
79
- ): ProxyCacheStatus {
80
- return CACHE_STATUS_SEVERITY[right] > CACHE_STATUS_SEVERITY[left]
81
- ? right
82
- : left;
87
+ function worseStatus(left: ProxyCacheStatus, right: ProxyCacheStatus): ProxyCacheStatus {
88
+ return CACHE_STATUS_SEVERITY[right] > CACHE_STATUS_SEVERITY[left] ? right : left;
83
89
  }
84
90
 
85
91
  function encodeBase64Url(value: string): string {
@@ -89,17 +95,18 @@ function encodeBase64Url(value: string): string {
89
95
  export class ProxyTelemetryCollector implements ProxyTelemetrySink {
90
96
  #events: ProxyResolutionTelemetryEvent[] = [];
91
97
  #attempts: ProxyAttemptTelemetryEvent[] = [];
98
+ #failovers: ProxyVendorFailoverTelemetryEvent[] = [];
92
99
 
93
100
  recordProxyResolution(event: ProxyResolutionTelemetryEvent): void {
94
101
  this.#events.push({
95
- provider: "smartproxy",
102
+ provider: event.provider,
103
+ ...(event.userAgentSource ? { userAgentSource: event.userAgentSource } : {}),
104
+ ...(event.protocol ? { protocol: event.protocol } : {}),
96
105
  cacheStatus: event.cacheStatus,
97
106
  cacheHit: event.cacheHit,
98
107
  resolutionMs: Math.max(0, Math.floor(event.resolutionMs)),
99
108
  allocatorMs:
100
- event.allocatorMs === undefined
101
- ? undefined
102
- : Math.max(0, Math.floor(event.allocatorMs)),
109
+ event.allocatorMs === undefined ? undefined : Math.max(0, Math.floor(event.allocatorMs)),
103
110
  allocatorStatus:
104
111
  event.allocatorStatus === undefined
105
112
  ? undefined
@@ -110,37 +117,38 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
110
117
  ? undefined
111
118
  : Math.max(1, Math.floor(event.allocatorAttempts)),
112
119
  lockWaitMs:
113
- event.lockWaitMs === undefined
114
- ? undefined
115
- : Math.max(0, Math.floor(event.lockWaitMs)),
120
+ event.lockWaitMs === undefined ? undefined : Math.max(0, Math.floor(event.lockWaitMs)),
116
121
  redisReadMs:
117
- event.redisReadMs === undefined
118
- ? undefined
119
- : Math.max(0, Math.floor(event.redisReadMs)),
122
+ event.redisReadMs === undefined ? undefined : Math.max(0, Math.floor(event.redisReadMs)),
120
123
  redisWriteMs:
121
- event.redisWriteMs === undefined
122
- ? undefined
123
- : Math.max(0, Math.floor(event.redisWriteMs)),
124
+ event.redisWriteMs === undefined ? undefined : Math.max(0, Math.floor(event.redisWriteMs)),
124
125
  poolAgeMs:
125
- event.poolAgeMs === undefined
126
- ? undefined
127
- : Math.max(0, Math.floor(event.poolAgeMs)),
126
+ event.poolAgeMs === undefined ? undefined : Math.max(0, Math.floor(event.poolAgeMs)),
128
127
  poolExpiresInMs:
129
128
  event.poolExpiresInMs === undefined
130
129
  ? undefined
131
130
  : Math.max(0, Math.floor(event.poolExpiresInMs)),
132
131
  attempts: Math.max(1, Math.floor(event.attempts || 1)),
133
132
  refreshes:
134
- event.refreshes === undefined
135
- ? undefined
136
- : Math.max(0, Math.floor(event.refreshes)),
133
+ event.refreshes === undefined ? undefined : Math.max(0, Math.floor(event.refreshes)),
134
+ });
135
+ }
136
+
137
+ recordProxyVendorFailover(event: ProxyVendorFailoverTelemetryEvent): void {
138
+ if (this.#failovers.length >= MAX_PROXY_FAILOVER_SAMPLES) return;
139
+ this.#failovers.push({
140
+ vendor: event.vendor,
141
+ ...(event.nextVendor ? { nextVendor: event.nextVendor } : {}),
142
+ phase: event.phase,
143
+ reason: event.reason,
144
+ ...(event.attempt === undefined ? {} : { attempt: Math.max(0, Math.floor(event.attempt)) }),
137
145
  });
138
146
  }
139
147
 
140
148
  recordProxyAttempt(event: ProxyAttemptTelemetryEvent): void {
141
149
  if (this.#attempts.length >= MAX_PROXY_ATTEMPT_SAMPLES) return;
142
150
  this.#attempts.push({
143
- provider: "smartproxy",
151
+ provider: event.provider,
144
152
  attempt: Math.max(1, Math.floor(event.attempt || 1)),
145
153
  ...(event.poolIndex === undefined
146
154
  ? {}
@@ -148,9 +156,7 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
148
156
  ...(event.proxyHash ? { proxyHash: event.proxyHash.slice(0, 16) } : {}),
149
157
  outcome: event.outcome === "ok" ? "ok" : "error",
150
158
  ...(event.errorCode ? { errorCode: event.errorCode.slice(0, 80) } : {}),
151
- ...(event.status === undefined
152
- ? {}
153
- : { status: Math.max(0, Math.floor(event.status)) }),
159
+ ...(event.status === undefined ? {} : { status: Math.max(0, Math.floor(event.status)) }),
154
160
  ...(event.durationMs === undefined
155
161
  ? {}
156
162
  : { durationMs: Math.max(0, Math.floor(event.durationMs)) }),
@@ -161,27 +167,30 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
161
167
  const [first, ...rest] = this.#events;
162
168
  if (!first) return undefined;
163
169
 
170
+ // The serving vendor/protocol is the last recorded resolution (a failed
171
+ // vendor records first, the vendor that served records last).
172
+ const serving = this.#events[this.#events.length - 1] ?? first;
173
+ const vendors: ProxyVendorName[] = [];
174
+ for (const event of this.#events) {
175
+ if (!vendors.includes(event.provider)) vendors.push(event.provider);
176
+ }
177
+
164
178
  const aggregate = rest.reduce<ProxyResolutionTelemetryEvent>(
165
179
  (acc, event) => ({
166
- provider: "smartproxy",
180
+ provider: event.provider,
181
+ userAgentSource: event.userAgentSource ?? acc.userAgentSource,
167
182
  cacheStatus: worseStatus(acc.cacheStatus, event.cacheStatus),
168
183
  cacheHit: acc.cacheHit && event.cacheHit,
169
184
  resolutionMs: acc.resolutionMs + event.resolutionMs,
170
185
  allocatorMs: sumOptional(acc.allocatorMs, event.allocatorMs),
171
186
  allocatorStatus: event.allocatorStatus ?? acc.allocatorStatus,
172
187
  allocatorBodyClass: event.allocatorBodyClass ?? acc.allocatorBodyClass,
173
- allocatorAttempts: sumOptional(
174
- acc.allocatorAttempts,
175
- event.allocatorAttempts,
176
- ),
188
+ allocatorAttempts: sumOptional(acc.allocatorAttempts, event.allocatorAttempts),
177
189
  lockWaitMs: sumOptional(acc.lockWaitMs, event.lockWaitMs),
178
190
  redisReadMs: sumOptional(acc.redisReadMs, event.redisReadMs),
179
191
  redisWriteMs: sumOptional(acc.redisWriteMs, event.redisWriteMs),
180
192
  poolAgeMs: maxOptional(acc.poolAgeMs, event.poolAgeMs),
181
- poolExpiresInMs: maxOptional(
182
- acc.poolExpiresInMs,
183
- event.poolExpiresInMs,
184
- ),
193
+ poolExpiresInMs: maxOptional(acc.poolExpiresInMs, event.poolExpiresInMs),
185
194
  attempts: acc.attempts + event.attempts,
186
195
  refreshes: sumOptional(acc.refreshes, event.refreshes),
187
196
  }),
@@ -190,13 +199,13 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
190
199
  const payload: ProviderTelemetryHeader = {
191
200
  v: 1,
192
201
  proxy: {
193
- provider: "smartproxy",
202
+ provider: serving.provider,
203
+ ...(aggregate.userAgentSource ? { userAgentSource: aggregate.userAgentSource } : {}),
204
+ ...(serving.protocol ? { protocol: serving.protocol } : {}),
194
205
  cacheStatus: aggregate.cacheStatus,
195
206
  cacheHit: aggregate.cacheHit,
196
207
  resolutionMs: aggregate.resolutionMs,
197
- ...(aggregate.allocatorMs !== undefined
198
- ? { allocatorMs: aggregate.allocatorMs }
199
- : {}),
208
+ ...(aggregate.allocatorMs !== undefined ? { allocatorMs: aggregate.allocatorMs } : {}),
200
209
  ...(aggregate.allocatorStatus !== undefined
201
210
  ? { allocatorStatus: aggregate.allocatorStatus }
202
211
  : {}),
@@ -206,40 +215,38 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
206
215
  ...(aggregate.allocatorAttempts !== undefined
207
216
  ? { allocatorAttempts: aggregate.allocatorAttempts }
208
217
  : {}),
209
- ...(aggregate.lockWaitMs !== undefined
210
- ? { lockWaitMs: aggregate.lockWaitMs }
211
- : {}),
212
- ...(aggregate.redisReadMs !== undefined
213
- ? { redisReadMs: aggregate.redisReadMs }
214
- : {}),
215
- ...(aggregate.redisWriteMs !== undefined
216
- ? { redisWriteMs: aggregate.redisWriteMs }
217
- : {}),
218
- ...(aggregate.poolAgeMs !== undefined
219
- ? { poolAgeMs: aggregate.poolAgeMs }
220
- : {}),
218
+ ...(aggregate.lockWaitMs !== undefined ? { lockWaitMs: aggregate.lockWaitMs } : {}),
219
+ ...(aggregate.redisReadMs !== undefined ? { redisReadMs: aggregate.redisReadMs } : {}),
220
+ ...(aggregate.redisWriteMs !== undefined ? { redisWriteMs: aggregate.redisWriteMs } : {}),
221
+ ...(aggregate.poolAgeMs !== undefined ? { poolAgeMs: aggregate.poolAgeMs } : {}),
221
222
  ...(aggregate.poolExpiresInMs !== undefined
222
223
  ? { poolExpiresInMs: aggregate.poolExpiresInMs }
223
224
  : {}),
224
225
  attempts: aggregate.attempts,
225
- ...(aggregate.refreshes !== undefined
226
- ? { refreshes: aggregate.refreshes }
227
- : {}),
226
+ ...(aggregate.refreshes !== undefined ? { refreshes: aggregate.refreshes } : {}),
228
227
  ...(this.#attempts.length > 0
229
228
  ? {
230
229
  attemptSamples: this.#attempts.map((attempt, index) => ({
231
230
  n: index + 1,
232
231
  a: attempt.attempt,
233
- ...(attempt.poolIndex === undefined
234
- ? {}
235
- : { i: attempt.poolIndex }),
232
+ ...(attempt.poolIndex === undefined ? {} : { i: attempt.poolIndex }),
236
233
  ...(attempt.proxyHash ? { h: attempt.proxyHash } : {}),
237
234
  o: attempt.outcome,
238
235
  ...(attempt.errorCode ? { c: attempt.errorCode } : {}),
239
236
  ...(attempt.status === undefined ? {} : { s: attempt.status }),
240
- ...(attempt.durationMs === undefined
241
- ? {}
242
- : { d: attempt.durationMs }),
237
+ ...(attempt.durationMs === undefined ? {} : { d: attempt.durationMs }),
238
+ })),
239
+ }
240
+ : {}),
241
+ ...(vendors.length > 1 ? { vendors } : {}),
242
+ ...(this.#failovers.length > 0
243
+ ? {
244
+ failovers: this.#failovers.map((failover) => ({
245
+ v: failover.vendor,
246
+ ...(failover.nextVendor ? { nx: failover.nextVendor } : {}),
247
+ p: failover.phase,
248
+ r: failover.reason,
249
+ ...(failover.attempt === undefined ? {} : { a: failover.attempt }),
243
250
  })),
244
251
  }
245
252
  : {}),