@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.
Files changed (291) hide show
  1. package/AUTHORING.md +487 -0
  2. package/CHANGELOG.md +90 -0
  3. package/README.md +49 -3
  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 +36 -81
  10. package/bin/apifuse-pack-types.ts +305 -0
  11. package/bin/apifuse-perf.ts +45 -127
  12. package/bin/apifuse-record.ts +659 -111
  13. package/bin/apifuse-submit-check.ts +546 -48
  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 +9 -1
  23. package/dist/ceremonies/index.js +65 -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 +176 -8
  32. package/dist/config/loader.js +424 -95
  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 +9 -1
  39. package/dist/define.js +304 -119
  40. package/dist/dev.d.ts +1 -1
  41. package/dist/dev.js +1 -1
  42. package/dist/error-resolution.d.ts +2 -0
  43. package/dist/error-resolution.js +90 -0
  44. package/dist/errors.d.ts +22 -1
  45. package/dist/errors.js +88 -0
  46. package/dist/fixture-sanitization.d.ts +26 -0
  47. package/dist/fixture-sanitization.js +216 -0
  48. package/dist/i18n/catalog.d.ts +2 -2
  49. package/dist/i18n/catalog.js +4 -10
  50. package/dist/i18n/index.d.ts +2 -2
  51. package/dist/i18n/index.js +2 -2
  52. package/dist/i18n/keys.d.ts +2 -2
  53. package/dist/index.d.ts +46 -42
  54. package/dist/index.js +41 -37
  55. package/dist/lint.d.ts +1 -1
  56. package/dist/lint.js +8 -15
  57. package/dist/native-address.d.ts +43 -0
  58. package/dist/native-address.js +281 -0
  59. package/dist/native-egress-policy.d.ts +31 -0
  60. package/dist/native-egress-policy.js +288 -0
  61. package/dist/observability.d.ts +5 -2
  62. package/dist/observability.js +48 -1
  63. package/dist/provider.d.ts +12 -11
  64. package/dist/provider.js +10 -9
  65. package/dist/public-schema-field-lint.d.ts +1 -1
  66. package/dist/recipes/gov-api.js +1 -1
  67. package/dist/runtime/auth-flow.d.ts +2 -1
  68. package/dist/runtime/auth-flow.js +4 -3
  69. package/dist/runtime/browser.d.ts +1 -1
  70. package/dist/runtime/browser.js +15 -29
  71. package/dist/runtime/cache.d.ts +1 -1
  72. package/dist/runtime/cache.js +4 -8
  73. package/dist/runtime/choice.d.ts +1 -1
  74. package/dist/runtime/choice.js +31 -35
  75. package/dist/runtime/credential.d.ts +1 -1
  76. package/dist/runtime/credential.js +1 -1
  77. package/dist/runtime/env.d.ts +1 -1
  78. package/dist/runtime/executor.d.ts +1 -1
  79. package/dist/runtime/executor.js +31 -4
  80. package/dist/runtime/http.d.ts +2 -2
  81. package/dist/runtime/http.js +387 -47
  82. package/dist/runtime/insights.d.ts +1 -1
  83. package/dist/runtime/insights.js +6 -13
  84. package/dist/runtime/instrumentation.d.ts +2 -2
  85. package/dist/runtime/instrumentation.js +345 -22
  86. package/dist/runtime/keyring.js +1 -1
  87. package/dist/runtime/namespace.js +1 -1
  88. package/dist/runtime/native-network.d.ts +127 -0
  89. package/dist/runtime/native-network.js +1298 -0
  90. package/dist/runtime/otlp.d.ts +1 -1
  91. package/dist/runtime/perf.d.ts +1 -1
  92. package/dist/runtime/provider.d.ts +1 -1
  93. package/dist/runtime/provider.js +1 -2
  94. package/dist/runtime/proxy-errors.d.ts +1 -1
  95. package/dist/runtime/proxy-errors.js +9 -7
  96. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  97. package/dist/runtime/proxy-nodemaven.js +146 -0
  98. package/dist/runtime/proxy-retry-policy.d.ts +2 -2
  99. package/dist/runtime/proxy-retry-policy.js +2 -2
  100. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  101. package/dist/runtime/proxy-telemetry.js +55 -52
  102. package/dist/runtime/redirects.d.ts +29 -0
  103. package/dist/runtime/redirects.js +36 -0
  104. package/dist/runtime/redis.d.ts +1 -1
  105. package/dist/runtime/redis.js +2 -4
  106. package/dist/runtime/request-options.d.ts +68 -1
  107. package/dist/runtime/request-options.js +548 -0
  108. package/dist/runtime/secrets.d.ts +27 -0
  109. package/dist/runtime/secrets.js +51 -0
  110. package/dist/runtime/state.d.ts +2 -2
  111. package/dist/runtime/state.js +238 -26
  112. package/dist/runtime/stealth.d.ts +5 -3
  113. package/dist/runtime/stealth.js +423 -143
  114. package/dist/runtime/stt.d.ts +1 -1
  115. package/dist/runtime/stt.js +11 -15
  116. package/dist/runtime/trace.d.ts +2 -2
  117. package/dist/runtime/trace.js +2 -4
  118. package/dist/runtime/waterfall.d.ts +1 -1
  119. package/dist/schema.d.ts +1 -1
  120. package/dist/schema.js +7 -15
  121. package/dist/serve.d.ts +1 -1
  122. package/dist/serve.js +1 -1
  123. package/dist/server/index.d.ts +7 -7
  124. package/dist/server/index.js +6 -6
  125. package/dist/server/self-test-input-tokens.d.ts +2 -1
  126. package/dist/server/self-test-input-tokens.js +18 -14
  127. package/dist/server/self-test-redaction.d.ts +1 -1
  128. package/dist/server/self-test-redaction.js +1 -1
  129. package/dist/server/self-test.d.ts +104 -3
  130. package/dist/server/self-test.js +673 -115
  131. package/dist/server/serve.d.ts +116 -4
  132. package/dist/server/serve.js +799 -128
  133. package/dist/server/types.d.ts +34 -9
  134. package/dist/server/types.js +8 -1
  135. package/dist/stateful/errors.d.ts +14 -0
  136. package/dist/stateful/errors.js +14 -0
  137. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  138. package/dist/stateful/http-provider-event-emitter.js +237 -0
  139. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  140. package/dist/stateful/http-session-owner-registry.js +210 -0
  141. package/dist/stateful/index.d.ts +18 -0
  142. package/dist/stateful/index.js +18 -0
  143. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  144. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  145. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  146. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  147. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  148. package/dist/stateful/provider-event-pipeline.js +1 -0
  149. package/dist/stateful/provider-events.d.ts +101 -0
  150. package/dist/stateful/provider-events.js +289 -0
  151. package/dist/stateful/session-key.d.ts +15 -0
  152. package/dist/stateful/session-key.js +86 -0
  153. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  154. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  155. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  156. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  157. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  158. package/dist/stateful/stateful-provider-adapter.js +287 -0
  159. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  160. package/dist/stateful/stateful-provider-observability.js +161 -0
  161. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  162. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  163. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  164. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  165. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  166. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  167. package/dist/stateful/stateful-provider-session-routing.d.ts +71 -0
  168. package/dist/stateful/stateful-provider-session-routing.js +353 -0
  169. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  170. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  171. package/dist/stateful-signing.d.ts +18 -0
  172. package/dist/stateful-signing.js +27 -0
  173. package/dist/stealth/profiles.d.ts +1 -1
  174. package/dist/stealth/profiles.js +5 -14
  175. package/dist/stream-evidence.d.ts +74 -0
  176. package/dist/stream-evidence.js +785 -0
  177. package/dist/stream.d.ts +1 -1
  178. package/dist/testing/index.d.ts +2 -2
  179. package/dist/testing/index.js +2 -2
  180. package/dist/testing/run.d.ts +32 -2
  181. package/dist/testing/run.js +478 -28
  182. package/dist/types.d.ts +342 -14
  183. package/dist/types.js +1 -0
  184. package/dist/user-input.d.ts +30 -0
  185. package/dist/user-input.js +66 -0
  186. package/package.json +16 -5
  187. package/src/auth-turn/index.ts +1 -1
  188. package/src/auth.ts +28 -86
  189. package/src/ceremonies/index.ts +103 -78
  190. package/src/cli/commands.ts +10 -0
  191. package/src/cli/create.ts +42 -35
  192. package/src/cli/prompt-assets.ts +865 -0
  193. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  194. package/src/config/loader.ts +652 -204
  195. package/src/contract-serialization.ts +5 -11
  196. package/src/contract-types.ts +2 -2
  197. package/src/contract.ts +12 -28
  198. package/src/define.ts +499 -498
  199. package/src/dev.ts +4 -9
  200. package/src/error-resolution.ts +91 -0
  201. package/src/errors.ts +110 -5
  202. package/src/fixture-sanitization.ts +247 -0
  203. package/src/i18n/catalog.ts +10 -32
  204. package/src/i18n/index.ts +2 -2
  205. package/src/i18n/keys.ts +5 -11
  206. package/src/index.ts +112 -42
  207. package/src/lint.ts +88 -152
  208. package/src/native-address.ts +340 -0
  209. package/src/native-egress-policy.ts +358 -0
  210. package/src/observability.ts +51 -1
  211. package/src/provider.ts +66 -11
  212. package/src/public-schema-field-lint.ts +7 -33
  213. package/src/recipes/gov-api.ts +2 -5
  214. package/src/runtime/auth-flow.ts +7 -7
  215. package/src/runtime/browser.ts +64 -187
  216. package/src/runtime/cache.ts +20 -67
  217. package/src/runtime/choice.ts +79 -132
  218. package/src/runtime/credential.ts +2 -2
  219. package/src/runtime/env.ts +1 -1
  220. package/src/runtime/executor.ts +43 -20
  221. package/src/runtime/http.ts +494 -57
  222. package/src/runtime/insights.ts +15 -53
  223. package/src/runtime/instrumentation.ts +495 -66
  224. package/src/runtime/keyring.ts +7 -19
  225. package/src/runtime/namespace.ts +2 -7
  226. package/src/runtime/native-network.ts +1686 -0
  227. package/src/runtime/otlp.ts +12 -23
  228. package/src/runtime/perf.ts +1 -1
  229. package/src/runtime/provider.ts +4 -9
  230. package/src/runtime/proxy-errors.ts +29 -42
  231. package/src/runtime/proxy-nodemaven.ts +221 -0
  232. package/src/runtime/proxy-retry-policy.ts +3 -3
  233. package/src/runtime/proxy-telemetry.ts +79 -77
  234. package/src/runtime/redirects.ts +66 -0
  235. package/src/runtime/redis.ts +4 -12
  236. package/src/runtime/request-options.ts +679 -9
  237. package/src/runtime/secrets.ts +64 -0
  238. package/src/runtime/state.ts +353 -133
  239. package/src/runtime/stealth.ts +505 -154
  240. package/src/runtime/stt.ts +38 -94
  241. package/src/runtime/trace.ts +14 -44
  242. package/src/runtime/waterfall.ts +5 -18
  243. package/src/schema.ts +23 -84
  244. package/src/serve.ts +1 -1
  245. package/src/server/index.ts +29 -7
  246. package/src/server/self-test-input-tokens.ts +29 -14
  247. package/src/server/self-test-redaction.ts +2 -2
  248. package/src/server/self-test.ts +857 -132
  249. package/src/server/serve.ts +1151 -328
  250. package/src/server/types.ts +12 -13
  251. package/src/stateful/README.md +146 -0
  252. package/src/stateful/errors.ts +23 -0
  253. package/src/stateful/http-provider-event-emitter.ts +314 -0
  254. package/src/stateful/http-session-owner-registry.ts +306 -0
  255. package/src/stateful/index.ts +18 -0
  256. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  257. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  258. package/src/stateful/provider-event-pipeline.ts +61 -0
  259. package/src/stateful/provider-events.ts +462 -0
  260. package/src/stateful/session-key.ts +111 -0
  261. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  262. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  263. package/src/stateful/stateful-provider-adapter.ts +562 -0
  264. package/src/stateful/stateful-provider-observability.ts +261 -0
  265. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  266. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  267. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  268. package/src/stateful/stateful-provider-session-routing.ts +555 -0
  269. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  270. package/src/stateful-signing.ts +46 -0
  271. package/src/stealth/profiles.ts +10 -26
  272. package/src/stream-evidence.ts +988 -0
  273. package/src/stream.ts +8 -19
  274. package/src/testing/index.ts +10 -2
  275. package/src/testing/run.ts +653 -74
  276. package/src/types.ts +408 -28
  277. package/src/user-input.ts +118 -0
  278. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  279. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  280. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  281. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  282. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  283. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  284. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  285. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  286. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  287. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  288. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  289. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  290. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  291. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -1,17 +1,21 @@
1
1
  import type {
2
2
  ProxyAttemptTelemetryEvent,
3
3
  ProxyCacheStatus,
4
+ ProxyProtocol,
4
5
  ProxyResolutionTelemetryEvent,
5
6
  ProxyTelemetrySink,
7
+ ProxyVendorFailoverTelemetryEvent,
8
+ ProxyVendorName,
6
9
  SmartproxyAllocatorBodyClass,
7
- } from "../config/loader";
10
+ } from "../config/loader.js";
8
11
 
9
12
  export const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
10
13
 
11
14
  type ProviderTelemetryHeader = {
12
15
  v: 1;
13
16
  proxy?: {
14
- provider: "smartproxy";
17
+ provider: ProxyVendorName;
18
+ protocol?: ProxyProtocol;
15
19
  cacheStatus: ProxyCacheStatus;
16
20
  cacheHit: boolean;
17
21
  resolutionMs: number;
@@ -27,6 +31,10 @@ type ProviderTelemetryHeader = {
27
31
  attempts: number;
28
32
  refreshes?: number;
29
33
  attemptSamples?: CompactProxyAttemptSample[];
34
+ /** Distinct vendors attempted across the resolution chain, in order seen. */
35
+ vendors?: ProxyVendorName[];
36
+ /** Cross-vendor failover events (bounded). */
37
+ failovers?: CompactVendorFailoverSample[];
30
38
  };
31
39
  };
32
40
 
@@ -41,8 +49,17 @@ type CompactProxyAttemptSample = {
41
49
  d?: number;
42
50
  };
43
51
 
52
+ type CompactVendorFailoverSample = {
53
+ v: ProxyVendorName;
54
+ nx?: ProxyVendorName;
55
+ p: "resolution" | "transport";
56
+ r: ProxyVendorFailoverTelemetryEvent["reason"];
57
+ a?: number;
58
+ };
59
+
44
60
  const MAX_HEADER_BYTES = 4_096;
45
61
  const MAX_PROXY_ATTEMPT_SAMPLES = 24;
62
+ const MAX_PROXY_FAILOVER_SAMPLES = 12;
46
63
 
47
64
  const CACHE_STATUS_SEVERITY: Record<ProxyCacheStatus, number> = {
48
65
  disabled: 0,
@@ -55,31 +72,18 @@ const CACHE_STATUS_SEVERITY: Record<ProxyCacheStatus, number> = {
55
72
  allocator: 7,
56
73
  };
57
74
 
58
- function sumOptional(
59
- left: number | undefined,
60
- right: number | undefined,
61
- ): number | undefined {
75
+ function sumOptional(left: number | undefined, right: number | undefined): number | undefined {
62
76
  const total = (left ?? 0) + (right ?? 0);
63
77
  return total > 0 ? total : undefined;
64
78
  }
65
79
 
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
- );
80
+ function maxOptional(left: number | undefined, right: number | undefined): number | undefined {
81
+ const values = [left, right].filter((value): value is number => typeof value === "number");
73
82
  return values.length > 0 ? Math.max(...values) : undefined;
74
83
  }
75
84
 
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;
85
+ function worseStatus(left: ProxyCacheStatus, right: ProxyCacheStatus): ProxyCacheStatus {
86
+ return CACHE_STATUS_SEVERITY[right] > CACHE_STATUS_SEVERITY[left] ? right : left;
83
87
  }
84
88
 
85
89
  function encodeBase64Url(value: string): string {
@@ -89,17 +93,17 @@ function encodeBase64Url(value: string): string {
89
93
  export class ProxyTelemetryCollector implements ProxyTelemetrySink {
90
94
  #events: ProxyResolutionTelemetryEvent[] = [];
91
95
  #attempts: ProxyAttemptTelemetryEvent[] = [];
96
+ #failovers: ProxyVendorFailoverTelemetryEvent[] = [];
92
97
 
93
98
  recordProxyResolution(event: ProxyResolutionTelemetryEvent): void {
94
99
  this.#events.push({
95
- provider: "smartproxy",
100
+ provider: event.provider,
101
+ ...(event.protocol ? { protocol: event.protocol } : {}),
96
102
  cacheStatus: event.cacheStatus,
97
103
  cacheHit: event.cacheHit,
98
104
  resolutionMs: Math.max(0, Math.floor(event.resolutionMs)),
99
105
  allocatorMs:
100
- event.allocatorMs === undefined
101
- ? undefined
102
- : Math.max(0, Math.floor(event.allocatorMs)),
106
+ event.allocatorMs === undefined ? undefined : Math.max(0, Math.floor(event.allocatorMs)),
103
107
  allocatorStatus:
104
108
  event.allocatorStatus === undefined
105
109
  ? undefined
@@ -110,37 +114,38 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
110
114
  ? undefined
111
115
  : Math.max(1, Math.floor(event.allocatorAttempts)),
112
116
  lockWaitMs:
113
- event.lockWaitMs === undefined
114
- ? undefined
115
- : Math.max(0, Math.floor(event.lockWaitMs)),
117
+ event.lockWaitMs === undefined ? undefined : Math.max(0, Math.floor(event.lockWaitMs)),
116
118
  redisReadMs:
117
- event.redisReadMs === undefined
118
- ? undefined
119
- : Math.max(0, Math.floor(event.redisReadMs)),
119
+ event.redisReadMs === undefined ? undefined : Math.max(0, Math.floor(event.redisReadMs)),
120
120
  redisWriteMs:
121
- event.redisWriteMs === undefined
122
- ? undefined
123
- : Math.max(0, Math.floor(event.redisWriteMs)),
121
+ event.redisWriteMs === undefined ? undefined : Math.max(0, Math.floor(event.redisWriteMs)),
124
122
  poolAgeMs:
125
- event.poolAgeMs === undefined
126
- ? undefined
127
- : Math.max(0, Math.floor(event.poolAgeMs)),
123
+ event.poolAgeMs === undefined ? undefined : Math.max(0, Math.floor(event.poolAgeMs)),
128
124
  poolExpiresInMs:
129
125
  event.poolExpiresInMs === undefined
130
126
  ? undefined
131
127
  : Math.max(0, Math.floor(event.poolExpiresInMs)),
132
128
  attempts: Math.max(1, Math.floor(event.attempts || 1)),
133
129
  refreshes:
134
- event.refreshes === undefined
135
- ? undefined
136
- : Math.max(0, Math.floor(event.refreshes)),
130
+ event.refreshes === undefined ? undefined : Math.max(0, Math.floor(event.refreshes)),
131
+ });
132
+ }
133
+
134
+ recordProxyVendorFailover(event: ProxyVendorFailoverTelemetryEvent): void {
135
+ if (this.#failovers.length >= MAX_PROXY_FAILOVER_SAMPLES) return;
136
+ this.#failovers.push({
137
+ vendor: event.vendor,
138
+ ...(event.nextVendor ? { nextVendor: event.nextVendor } : {}),
139
+ phase: event.phase,
140
+ reason: event.reason,
141
+ ...(event.attempt === undefined ? {} : { attempt: Math.max(0, Math.floor(event.attempt)) }),
137
142
  });
138
143
  }
139
144
 
140
145
  recordProxyAttempt(event: ProxyAttemptTelemetryEvent): void {
141
146
  if (this.#attempts.length >= MAX_PROXY_ATTEMPT_SAMPLES) return;
142
147
  this.#attempts.push({
143
- provider: "smartproxy",
148
+ provider: event.provider,
144
149
  attempt: Math.max(1, Math.floor(event.attempt || 1)),
145
150
  ...(event.poolIndex === undefined
146
151
  ? {}
@@ -148,9 +153,7 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
148
153
  ...(event.proxyHash ? { proxyHash: event.proxyHash.slice(0, 16) } : {}),
149
154
  outcome: event.outcome === "ok" ? "ok" : "error",
150
155
  ...(event.errorCode ? { errorCode: event.errorCode.slice(0, 80) } : {}),
151
- ...(event.status === undefined
152
- ? {}
153
- : { status: Math.max(0, Math.floor(event.status)) }),
156
+ ...(event.status === undefined ? {} : { status: Math.max(0, Math.floor(event.status)) }),
154
157
  ...(event.durationMs === undefined
155
158
  ? {}
156
159
  : { durationMs: Math.max(0, Math.floor(event.durationMs)) }),
@@ -161,27 +164,29 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
161
164
  const [first, ...rest] = this.#events;
162
165
  if (!first) return undefined;
163
166
 
167
+ // The serving vendor/protocol is the last recorded resolution (a failed
168
+ // vendor records first, the vendor that served records last).
169
+ const serving = this.#events[this.#events.length - 1] ?? first;
170
+ const vendors: ProxyVendorName[] = [];
171
+ for (const event of this.#events) {
172
+ if (!vendors.includes(event.provider)) vendors.push(event.provider);
173
+ }
174
+
164
175
  const aggregate = rest.reduce<ProxyResolutionTelemetryEvent>(
165
176
  (acc, event) => ({
166
- provider: "smartproxy",
177
+ provider: event.provider,
167
178
  cacheStatus: worseStatus(acc.cacheStatus, event.cacheStatus),
168
179
  cacheHit: acc.cacheHit && event.cacheHit,
169
180
  resolutionMs: acc.resolutionMs + event.resolutionMs,
170
181
  allocatorMs: sumOptional(acc.allocatorMs, event.allocatorMs),
171
182
  allocatorStatus: event.allocatorStatus ?? acc.allocatorStatus,
172
183
  allocatorBodyClass: event.allocatorBodyClass ?? acc.allocatorBodyClass,
173
- allocatorAttempts: sumOptional(
174
- acc.allocatorAttempts,
175
- event.allocatorAttempts,
176
- ),
184
+ allocatorAttempts: sumOptional(acc.allocatorAttempts, event.allocatorAttempts),
177
185
  lockWaitMs: sumOptional(acc.lockWaitMs, event.lockWaitMs),
178
186
  redisReadMs: sumOptional(acc.redisReadMs, event.redisReadMs),
179
187
  redisWriteMs: sumOptional(acc.redisWriteMs, event.redisWriteMs),
180
188
  poolAgeMs: maxOptional(acc.poolAgeMs, event.poolAgeMs),
181
- poolExpiresInMs: maxOptional(
182
- acc.poolExpiresInMs,
183
- event.poolExpiresInMs,
184
- ),
189
+ poolExpiresInMs: maxOptional(acc.poolExpiresInMs, event.poolExpiresInMs),
185
190
  attempts: acc.attempts + event.attempts,
186
191
  refreshes: sumOptional(acc.refreshes, event.refreshes),
187
192
  }),
@@ -190,13 +195,12 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
190
195
  const payload: ProviderTelemetryHeader = {
191
196
  v: 1,
192
197
  proxy: {
193
- provider: "smartproxy",
198
+ provider: serving.provider,
199
+ ...(serving.protocol ? { protocol: serving.protocol } : {}),
194
200
  cacheStatus: aggregate.cacheStatus,
195
201
  cacheHit: aggregate.cacheHit,
196
202
  resolutionMs: aggregate.resolutionMs,
197
- ...(aggregate.allocatorMs !== undefined
198
- ? { allocatorMs: aggregate.allocatorMs }
199
- : {}),
203
+ ...(aggregate.allocatorMs !== undefined ? { allocatorMs: aggregate.allocatorMs } : {}),
200
204
  ...(aggregate.allocatorStatus !== undefined
201
205
  ? { allocatorStatus: aggregate.allocatorStatus }
202
206
  : {}),
@@ -206,40 +210,38 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
206
210
  ...(aggregate.allocatorAttempts !== undefined
207
211
  ? { allocatorAttempts: aggregate.allocatorAttempts }
208
212
  : {}),
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
- : {}),
213
+ ...(aggregate.lockWaitMs !== undefined ? { lockWaitMs: aggregate.lockWaitMs } : {}),
214
+ ...(aggregate.redisReadMs !== undefined ? { redisReadMs: aggregate.redisReadMs } : {}),
215
+ ...(aggregate.redisWriteMs !== undefined ? { redisWriteMs: aggregate.redisWriteMs } : {}),
216
+ ...(aggregate.poolAgeMs !== undefined ? { poolAgeMs: aggregate.poolAgeMs } : {}),
221
217
  ...(aggregate.poolExpiresInMs !== undefined
222
218
  ? { poolExpiresInMs: aggregate.poolExpiresInMs }
223
219
  : {}),
224
220
  attempts: aggregate.attempts,
225
- ...(aggregate.refreshes !== undefined
226
- ? { refreshes: aggregate.refreshes }
227
- : {}),
221
+ ...(aggregate.refreshes !== undefined ? { refreshes: aggregate.refreshes } : {}),
228
222
  ...(this.#attempts.length > 0
229
223
  ? {
230
224
  attemptSamples: this.#attempts.map((attempt, index) => ({
231
225
  n: index + 1,
232
226
  a: attempt.attempt,
233
- ...(attempt.poolIndex === undefined
234
- ? {}
235
- : { i: attempt.poolIndex }),
227
+ ...(attempt.poolIndex === undefined ? {} : { i: attempt.poolIndex }),
236
228
  ...(attempt.proxyHash ? { h: attempt.proxyHash } : {}),
237
229
  o: attempt.outcome,
238
230
  ...(attempt.errorCode ? { c: attempt.errorCode } : {}),
239
231
  ...(attempt.status === undefined ? {} : { s: attempt.status }),
240
- ...(attempt.durationMs === undefined
241
- ? {}
242
- : { d: attempt.durationMs }),
232
+ ...(attempt.durationMs === undefined ? {} : { d: attempt.durationMs }),
233
+ })),
234
+ }
235
+ : {}),
236
+ ...(vendors.length > 1 ? { vendors } : {}),
237
+ ...(this.#failovers.length > 0
238
+ ? {
239
+ failovers: this.#failovers.map((failover) => ({
240
+ v: failover.vendor,
241
+ ...(failover.nextVendor ? { nx: failover.nextVendor } : {}),
242
+ p: failover.phase,
243
+ r: failover.reason,
244
+ ...(failover.attempt === undefined ? {} : { a: failover.attempt }),
243
245
  })),
244
246
  }
245
247
  : {}),
@@ -0,0 +1,66 @@
1
+ import type { HttpRedirectFailureReason } from "../types.js";
2
+
3
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
4
+
5
+ export type RedirectHopDecision<TMethod extends string> =
6
+ | {
7
+ kind: "follow";
8
+ nextMethod: TMethod | "GET";
9
+ nextUrl: string;
10
+ }
11
+ | {
12
+ kind: "stop";
13
+ reason: HttpRedirectFailureReason;
14
+ nextUrl?: string;
15
+ };
16
+
17
+ export function isRedirectStatus(status: number): boolean {
18
+ return REDIRECT_STATUSES.has(status);
19
+ }
20
+
21
+ /** Shared fetch-compatible redirect method rewriting for stealth and ctx.http. */
22
+ export function nextRedirectMethod<TMethod extends string>(
23
+ status: number,
24
+ method: TMethod,
25
+ ): TMethod | "GET" {
26
+ if (status === 303 && method !== "HEAD") return "GET";
27
+ if ((status === 301 || status === 302) && method === "POST") return "GET";
28
+ return method;
29
+ }
30
+
31
+ /** Resolves a Location value against the response URL without issuing a request. */
32
+ export function resolveRedirectUrl(
33
+ location: string | undefined,
34
+ responseUrl: string,
35
+ ): string | undefined {
36
+ return location ? new URL(location, responseUrl).toString() : undefined;
37
+ }
38
+
39
+ /**
40
+ * Shared post-response decision ordering for both redirect walkers. The
41
+ * caller-owned stop hook is checked first, matching stealth's pre-follow
42
+ * contract, then structural termination and loop checks run before follow.
43
+ */
44
+ export function evaluateRedirectHop<TMethod extends string>(input: {
45
+ status: number;
46
+ method: TMethod;
47
+ nextUrl: string | undefined;
48
+ shouldStop: boolean;
49
+ redirectCount: number;
50
+ maxHops: number;
51
+ visitedRequests: ReadonlySet<string>;
52
+ }): RedirectHopDecision<TMethod> {
53
+ if (input.shouldStop) {
54
+ return { kind: "stop", reason: "stopped", ...(input.nextUrl ? { nextUrl: input.nextUrl } : {}) };
55
+ }
56
+ if (!input.nextUrl) return { kind: "stop", reason: "missing_location" };
57
+ if (input.redirectCount > input.maxHops) {
58
+ return { kind: "stop", reason: "max_hops", nextUrl: input.nextUrl };
59
+ }
60
+
61
+ const nextMethod = nextRedirectMethod(input.status, input.method);
62
+ if (input.visitedRequests.has(`${nextMethod} ${input.nextUrl}`)) {
63
+ return { kind: "stop", reason: "loop", nextUrl: input.nextUrl };
64
+ }
65
+ return { kind: "follow", nextMethod, nextUrl: input.nextUrl };
66
+ }
@@ -1,4 +1,4 @@
1
- import Redis from "ioredis";
1
+ import { Redis } from "ioredis";
2
2
 
3
3
  export type ProviderRedisClient = Redis;
4
4
 
@@ -43,9 +43,7 @@ export async function withRedisTimeout<T>(
43
43
  }
44
44
  }, options.timeoutMs);
45
45
  });
46
- const operationResult = options.onError
47
- ? operation().catch(options.onError)
48
- : operation();
46
+ const operationResult = options.onError ? operation().catch(options.onError) : operation();
49
47
  return await Promise.race([operationResult, timeout]);
50
48
  } finally {
51
49
  if (timeoutId) clearTimeout(timeoutId);
@@ -56,10 +54,7 @@ export function redisStatus(redis: ProviderRedisClient): string {
56
54
  return redis.status;
57
55
  }
58
56
 
59
- async function waitForRedisReady(
60
- redis: ProviderRedisClient,
61
- timeoutMs: number,
62
- ): Promise<boolean> {
57
+ async function waitForRedisReady(redis: ProviderRedisClient, timeoutMs: number): Promise<boolean> {
63
58
  let timeoutId: ReturnType<typeof setTimeout> | undefined;
64
59
  let settled = false;
65
60
 
@@ -80,10 +75,7 @@ async function waitForRedisReady(
80
75
  const onReady = () => finish(true);
81
76
  const onUnavailable = () => finish(false);
82
77
 
83
- timeoutId = setTimeout(
84
- () => finish(redisStatus(redis) === "ready"),
85
- timeoutMs,
86
- );
78
+ timeoutId = setTimeout(() => finish(redisStatus(redis) === "ready"), timeoutMs);
87
79
  redis.once("ready", onReady);
88
80
  redis.once("close", onUnavailable);
89
81
  redis.once("end", onUnavailable);