@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,4 +1,4 @@
1
- import type { TraceSpan } from "../types";
1
+ import type { TraceSpan } from "../types.js";
2
2
  export interface OTLPExportOptions {
3
3
  endpoint: string;
4
4
  headers?: Record<string, string>;
@@ -1,4 +1,4 @@
1
- import type { Span } from "./trace";
1
+ import type { Span } from "./trace.js";
2
2
  export type PerfStats = {
3
3
  p50: number;
4
4
  p95: number;
@@ -1,2 +1,2 @@
1
- import type { ProviderDefinition } from "../types";
1
+ import type { ProviderDefinition } from "../types.js";
2
2
  export declare function getProviderBaseUrl(provider: ProviderDefinition): string | undefined;
@@ -1,8 +1,7 @@
1
1
  export function getProviderBaseUrl(provider) {
2
2
  const operations = Object.values(provider.operations);
3
3
  for (const operation of operations) {
4
- const baseUrl = operation.upstream
5
- ?.baseUrl;
4
+ const baseUrl = operation.upstream?.baseUrl;
6
5
  if (typeof baseUrl === "string" && baseUrl.length > 0) {
7
6
  return baseUrl;
8
7
  }
@@ -1,4 +1,4 @@
1
- import { TransportError } from "../errors";
1
+ import { TransportError } from "../errors.js";
2
2
  export declare const PROXY_AUTH_IP_DENIED_CODE = "PROXY_AUTH_IP_DENIED";
3
3
  export declare const PROXY_AUTH_IP_DENIED_MESSAGE = "Proxy source IP is not authorized. Add the runtime egress IP to the proxy provider allowlist.";
4
4
  export declare const PROXY_EDGE_AUTH_REJECTED_CODE = "PROXY_EDGE_AUTH_REJECTED";
@@ -1,4 +1,4 @@
1
- import { TransportError } from "../errors";
1
+ import { TransportError } from "../errors.js";
2
2
  export const PROXY_AUTH_IP_DENIED_CODE = "PROXY_AUTH_IP_DENIED";
3
3
  export const PROXY_AUTH_IP_DENIED_MESSAGE = "Proxy source IP is not authorized. Add the runtime egress IP to the proxy provider allowlist.";
4
4
  export const PROXY_EDGE_AUTH_REJECTED_CODE = "PROXY_EDGE_AUTH_REJECTED";
@@ -11,8 +11,12 @@ const PROXY_POOL_STALE_STATUS_CODES = new Set([509, 512]);
11
11
  const PROXY_EDGE_TLS_REJECTED_STATUS_CODES = new Set([495]);
12
12
  const PROXY_AUTH_IP_DENIED_PATTERN = /\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;
13
13
  const PROXY_EDGE_AUTH_REJECTED_PATTERN = /\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;
14
- const PROXY_POOL_STALE_MESSAGE_PATTERN = /\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;
15
- const PROXY_EDGE_TLS_REJECTED_MESSAGE_PATTERN = /\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;
14
+ // Vendor host tokens that can appear in upstream error strings. Adding a proxy
15
+ // vendor updates every classifier below in one place. `proxy` is the generic
16
+ // fallback so vendor-agnostic messages still classify.
17
+ const PROXY_VENDOR_ALTERNATION = "smartproxy|nodemaven|proxy";
18
+ const PROXY_POOL_STALE_MESSAGE_PATTERN = new RegExp(`\\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`, "i");
19
+ const PROXY_EDGE_TLS_REJECTED_MESSAGE_PATTERN = new RegExp(`\\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`, "i");
16
20
  export function isProxyAuthIpDeniedMessage(message) {
17
21
  return PROXY_AUTH_IP_DENIED_PATTERN.test(message);
18
22
  }
@@ -42,8 +46,7 @@ export function isProxyPoolStaleMessage(message) {
42
46
  return PROXY_POOL_STALE_MESSAGE_PATTERN.test(message);
43
47
  }
44
48
  export function isProxyPoolRefreshableError(error) {
45
- if (error instanceof TransportError &&
46
- error.code === PROXY_AUTH_IP_DENIED_CODE) {
49
+ if (error instanceof TransportError && error.code === PROXY_AUTH_IP_DENIED_CODE) {
47
50
  return false;
48
51
  }
49
52
  if (error instanceof TransportError &&
@@ -57,8 +60,7 @@ export function isProxyPoolRefreshableError(error) {
57
60
  error instanceof Error ? error.message : String(error),
58
61
  cause instanceof Error ? cause.message : "",
59
62
  ].join(" ");
60
- return (PROXY_POOL_STALE_MESSAGE_PATTERN.test(message) ||
61
- PROXY_EDGE_AUTH_REJECTED_PATTERN.test(message));
63
+ return (PROXY_POOL_STALE_MESSAGE_PATTERN.test(message) || PROXY_EDGE_AUTH_REJECTED_PATTERN.test(message));
62
64
  }
63
65
  export const isProxyPoolStaleError = isProxyPoolRefreshableError;
64
66
  export function createProxyPoolStaleError(status, cause) {
@@ -0,0 +1,56 @@
1
+ import type { ProviderProxyPolicy } from "../types.js";
2
+ export declare const NODEMAVEN_USERNAME_ENV = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
3
+ export declare const NODEMAVEN_PASSWORD_ENV = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
4
+ export declare const NODEMAVEN_FILTER_ENV = "APIFUSE__PROXY__NODEMAVEN_FILTER";
5
+ export declare const NODEMAVEN_GATEWAY_HOST = "gate.nodemaven.com";
6
+ export type NodemavenCredentials = {
7
+ readonly username: string;
8
+ readonly password: string;
9
+ readonly filter?: string;
10
+ };
11
+ /** Both schemes tunnel bytes end-to-end, preserving the client TLS handshake. */
12
+ export type ProxyProtocol = "http" | "socks5";
13
+ /**
14
+ * NodeMaven's fastest protocol: HTTP CONNECT. Benchmarks (KR, cold + warm)
15
+ * showed socks5 through the gateway adds ~500ms per request over http, so
16
+ * NodeMaven never defaults to socks5.
17
+ */
18
+ export declare const NODEMAVEN_DEFAULT_PROTOCOL: ProxyProtocol;
19
+ export declare const NODEMAVEN_MAX_POOL_SIZE = 50;
20
+ export declare function hasNodemavenCredentials(): boolean;
21
+ export declare function nodemavenPoolSize(policy: ProviderProxyPolicy): number;
22
+ export declare function nodemavenLifetimeMinutes(policy: ProviderProxyPolicy): number;
23
+ export type NodemavenSessionWindow = {
24
+ refreshEpoch: number;
25
+ expiresAt: string;
26
+ };
27
+ /** Stable wall-clock window shared by every process deriving the same sticky sid. */
28
+ export declare function nodemavenSessionWindow(policy: ProviderProxyPolicy, now?: number): NodemavenSessionWindow;
29
+ export type NodemavenSynthesisInput = {
30
+ policy: ProviderProxyPolicy;
31
+ /** Explicit credentials; synthesis never reads process-global state. */
32
+ credentials: NodemavenCredentials;
33
+ affinityKey: string | undefined;
34
+ protocol: ProxyProtocol;
35
+ poolIndex: number;
36
+ refreshEpoch: number;
37
+ /** ISO 3166-1 alpha-2, already resolved by the caller (falls back to env). */
38
+ country?: string;
39
+ /** Clock injection used by native sticky-expiry lifecycle tests. */
40
+ now?: number;
41
+ /** Stable caller-owned hard expiry when the refresh epoch is wall-clock-derived. */
42
+ expiresAt?: string;
43
+ };
44
+ export type NodemavenSynthesis = {
45
+ url: string;
46
+ protocol: ProxyProtocol;
47
+ sticky: boolean;
48
+ sessionId: string;
49
+ expiresAt?: string;
50
+ diagnostics: Record<string, string | number | boolean>;
51
+ };
52
+ /**
53
+ * Synthesize a NodeMaven gateway proxy URL locally from static credentials.
54
+ * There is no allocation API — geo/session are encoded in the username.
55
+ */
56
+ export declare function synthesizeNodemavenProxy(input: NodemavenSynthesisInput): NodemavenSynthesis;
@@ -0,0 +1,146 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ export const NODEMAVEN_USERNAME_ENV = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
3
+ export const NODEMAVEN_PASSWORD_ENV = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
4
+ export const NODEMAVEN_FILTER_ENV = "APIFUSE__PROXY__NODEMAVEN_FILTER";
5
+ export const NODEMAVEN_GATEWAY_HOST = "gate.nodemaven.com";
6
+ /**
7
+ * NodeMaven's fastest protocol: HTTP CONNECT. Benchmarks (KR, cold + warm)
8
+ * showed socks5 through the gateway adds ~500ms per request over http, so
9
+ * NodeMaven never defaults to socks5.
10
+ */
11
+ export const NODEMAVEN_DEFAULT_PROTOCOL = "http";
12
+ /** NodeMaven gateway port ranges per protocol (docs: HTTP 8080-9080, SOCKS5 1080-2080). */
13
+ const NODEMAVEN_PORTS = {
14
+ http: { min: 8080, max: 9080 },
15
+ socks5: { min: 1080, max: 2080 },
16
+ };
17
+ const NODEMAVEN_FILTERS = new Set(["medium", "high"]);
18
+ const DEFAULT_NODEMAVEN_FILTER = "medium";
19
+ const DEFAULT_NODEMAVEN_POOL_SIZE = 20;
20
+ export const NODEMAVEN_MAX_POOL_SIZE = 50;
21
+ /** NodeMaven sticky sessions persist up to 24h server-side, keyed by the sid. */
22
+ const NODEMAVEN_MAX_LIFETIME_MINUTES = 1440;
23
+ const SID_LENGTH = 10;
24
+ export function hasNodemavenCredentials() {
25
+ return Boolean(readNodemavenUsername() && readNodemavenPassword());
26
+ }
27
+ function readNodemavenUsername() {
28
+ return process.env[NODEMAVEN_USERNAME_ENV]?.trim() || undefined;
29
+ }
30
+ function readNodemavenPassword() {
31
+ return process.env[NODEMAVEN_PASSWORD_ENV]?.trim() || undefined;
32
+ }
33
+ function resolveNodemavenFilter(value) {
34
+ const raw = value?.trim().toLowerCase();
35
+ if (!raw)
36
+ return DEFAULT_NODEMAVEN_FILTER;
37
+ if (!NODEMAVEN_FILTERS.has(raw)) {
38
+ throw new Error(`${NODEMAVEN_FILTER_ENV} must be "medium" or "high"`);
39
+ }
40
+ return raw;
41
+ }
42
+ export function nodemavenPoolSize(policy) {
43
+ return Math.min(NODEMAVEN_MAX_POOL_SIZE, Math.max(1, Math.floor(policy.session?.poolSize ?? DEFAULT_NODEMAVEN_POOL_SIZE)));
44
+ }
45
+ export function nodemavenLifetimeMinutes(policy) {
46
+ const configured = policy.session?.lifetimeMinutes;
47
+ if (typeof configured !== "number" || !Number.isFinite(configured) || configured <= 0) {
48
+ return NODEMAVEN_MAX_LIFETIME_MINUTES;
49
+ }
50
+ return Math.min(NODEMAVEN_MAX_LIFETIME_MINUTES, Math.max(1, Math.floor(configured)));
51
+ }
52
+ /** Stable wall-clock window shared by every process deriving the same sticky sid. */
53
+ export function nodemavenSessionWindow(policy, now = Date.now()) {
54
+ const lifetimeMs = nodemavenLifetimeMinutes(policy) * 60_000;
55
+ const refreshEpoch = Math.floor(now / lifetimeMs);
56
+ return {
57
+ refreshEpoch,
58
+ expiresAt: new Date((refreshEpoch + 1) * lifetimeMs).toISOString(),
59
+ };
60
+ }
61
+ /** NodeMaven username tokens accept `[a-z0-9]`; slugify geo values to that set. */
62
+ function slugifyGeo(value) {
63
+ if (!value)
64
+ return undefined;
65
+ const slug = value
66
+ .trim()
67
+ .toLowerCase()
68
+ .replace(/[^a-z0-9]+/g, "");
69
+ return slug || undefined;
70
+ }
71
+ function isStickyAffinity(policy) {
72
+ return (policy.session?.affinity ?? "request") !== "request";
73
+ }
74
+ /**
75
+ * A sticky sid is deterministic from the affinity key so every process serving
76
+ * the same connection derives the same egress IP without shared storage. A
77
+ * rotating sid is random per call (a fresh egress IP per request).
78
+ */
79
+ function deriveSid(policy, affinityKey, poolIndex, refreshEpoch) {
80
+ if (!isStickyAffinity(policy) || !affinityKey) {
81
+ return randomBytes(SID_LENGTH).toString("hex").slice(0, SID_LENGTH);
82
+ }
83
+ const digest = createHash("sha256")
84
+ .update(`${affinityKey}:${refreshEpoch}:${poolIndex}`)
85
+ .digest("hex");
86
+ // hex digits are a subset of the allowed [a-z0-9] sid charset.
87
+ return digest.slice(0, SID_LENGTH);
88
+ }
89
+ function selectPort(protocol, sid, poolIndex) {
90
+ const { min, max } = NODEMAVEN_PORTS[protocol];
91
+ const span = max - min + 1;
92
+ const hashInt = Number.parseInt(createHash("sha256").update(`${sid}:${poolIndex}`).digest("hex").slice(0, 8), 16);
93
+ return min + (hashInt % span);
94
+ }
95
+ /**
96
+ * Synthesize a NodeMaven gateway proxy URL locally from static credentials.
97
+ * There is no allocation API — geo/session are encoded in the username.
98
+ */
99
+ export function synthesizeNodemavenProxy(input) {
100
+ const username = input.credentials.username.trim();
101
+ const password = input.credentials.password.trim();
102
+ if (!username || !password) {
103
+ throw new Error(`NodeMaven credentials missing: set ${NODEMAVEN_USERNAME_ENV} and ${NODEMAVEN_PASSWORD_ENV}.`);
104
+ }
105
+ const filter = resolveNodemavenFilter(input.credentials.filter);
106
+ const sid = deriveSid(input.policy, input.affinityKey, input.poolIndex, input.refreshEpoch);
107
+ const port = selectPort(input.protocol, sid, input.poolIndex);
108
+ const lifetimeMinutes = nodemavenLifetimeMinutes(input.policy);
109
+ const sticky = isStickyAffinity(input.policy);
110
+ const country = slugifyGeo(input.country ?? input.policy.geo?.country);
111
+ const region = slugifyGeo(input.policy.geo?.subdivision);
112
+ const city = slugifyGeo(input.policy.geo?.city);
113
+ const tokens = [username];
114
+ if (country)
115
+ tokens.push("country", country);
116
+ if (region)
117
+ tokens.push("region", region);
118
+ if (city)
119
+ tokens.push("city", city);
120
+ tokens.push("sid", sid);
121
+ tokens.push("filter", filter);
122
+ tokens.push("ipv4", "true");
123
+ const proxyUsername = tokens.join("-");
124
+ // Username tokens are [a-z0-9-] only, which survive URL encoding unchanged.
125
+ const url = `${input.protocol}://${proxyUsername}:${encodeURIComponent(password)}@${NODEMAVEN_GATEWAY_HOST}:${port}`;
126
+ return {
127
+ url,
128
+ protocol: input.protocol,
129
+ sticky,
130
+ sessionId: sid,
131
+ ...(sticky
132
+ ? {
133
+ expiresAt: input.expiresAt ??
134
+ new Date((input.now ?? Date.now()) + lifetimeMinutes * 60_000).toISOString(),
135
+ }
136
+ : {}),
137
+ diagnostics: {
138
+ vendor: "nodemaven",
139
+ protocol: input.protocol,
140
+ sticky,
141
+ filter,
142
+ lifetimeMinutes,
143
+ ...(country ? { country } : {}),
144
+ },
145
+ };
146
+ }
@@ -1,5 +1,5 @@
1
- import type { HttpMethod, HttpRetryOptions, RequestOptions } from "../types";
2
- import { HttpRetryPreset } from "../types";
1
+ import type { HttpMethod, HttpRetryOptions, RequestOptions } from "../types.js";
2
+ import { HttpRetryPreset } from "../types.js";
3
3
  export type NormalizedProxyTransportRetryOptions = Required<Pick<HttpRetryOptions, "attempts" | "delayStrategy" | "baseDelayMs" | "maxDelayMs" | "jitter" | "retryAfter" | "unsafeMethodPolicy">> & {
4
4
  preset?: HttpRetryPreset;
5
5
  methods: readonly string[];
@@ -1,5 +1,5 @@
1
- import { ProviderError, TransportError } from "../errors";
2
- import { HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, } from "../types";
1
+ import { ProviderError, TransportError } from "../errors.js";
2
+ import { HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, } from "../types.js";
3
3
  export const DEFAULT_PROXY_TRANSPORT_RETRY_METHODS = ["GET", "HEAD", "OPTIONS"];
4
4
  export const DEFAULT_PROXY_TRANSPORT_RETRY_ERROR_CODES = [
5
5
  "transport_network_error",
@@ -1,8 +1,9 @@
1
- import type { ProxyAttemptTelemetryEvent, ProxyResolutionTelemetryEvent, ProxyTelemetrySink } from "../config/loader";
1
+ import type { ProxyAttemptTelemetryEvent, ProxyResolutionTelemetryEvent, ProxyTelemetrySink, ProxyVendorFailoverTelemetryEvent } from "../config/loader.js";
2
2
  export declare const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
3
3
  export declare class ProxyTelemetryCollector implements ProxyTelemetrySink {
4
4
  #private;
5
5
  recordProxyResolution(event: ProxyResolutionTelemetryEvent): void;
6
+ recordProxyVendorFailover(event: ProxyVendorFailoverTelemetryEvent): void;
6
7
  recordProxyAttempt(event: ProxyAttemptTelemetryEvent): void;
7
8
  toHeaderValue(): string | undefined;
8
9
  }
@@ -1,6 +1,7 @@
1
1
  export const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
2
2
  const MAX_HEADER_BYTES = 4_096;
3
3
  const MAX_PROXY_ATTEMPT_SAMPLES = 24;
4
+ const MAX_PROXY_FAILOVER_SAMPLES = 12;
4
5
  const CACHE_STATUS_SEVERITY = {
5
6
  disabled: 0,
6
7
  memory_hit: 1,
@@ -20,9 +21,7 @@ function maxOptional(left, right) {
20
21
  return values.length > 0 ? Math.max(...values) : undefined;
21
22
  }
22
23
  function worseStatus(left, right) {
23
- return CACHE_STATUS_SEVERITY[right] > CACHE_STATUS_SEVERITY[left]
24
- ? right
25
- : left;
24
+ return CACHE_STATUS_SEVERITY[right] > CACHE_STATUS_SEVERITY[left] ? right : left;
26
25
  }
27
26
  function encodeBase64Url(value) {
28
27
  return Buffer.from(value, "utf8").toString("base64url");
@@ -30,15 +29,15 @@ function encodeBase64Url(value) {
30
29
  export class ProxyTelemetryCollector {
31
30
  #events = [];
32
31
  #attempts = [];
32
+ #failovers = [];
33
33
  recordProxyResolution(event) {
34
34
  this.#events.push({
35
- provider: "smartproxy",
35
+ provider: event.provider,
36
+ ...(event.protocol ? { protocol: event.protocol } : {}),
36
37
  cacheStatus: event.cacheStatus,
37
38
  cacheHit: event.cacheHit,
38
39
  resolutionMs: Math.max(0, Math.floor(event.resolutionMs)),
39
- allocatorMs: event.allocatorMs === undefined
40
- ? undefined
41
- : Math.max(0, Math.floor(event.allocatorMs)),
40
+ allocatorMs: event.allocatorMs === undefined ? undefined : Math.max(0, Math.floor(event.allocatorMs)),
42
41
  allocatorStatus: event.allocatorStatus === undefined
43
42
  ? undefined
44
43
  : Math.max(0, Math.floor(event.allocatorStatus)),
@@ -46,32 +45,33 @@ export class ProxyTelemetryCollector {
46
45
  allocatorAttempts: event.allocatorAttempts === undefined
47
46
  ? undefined
48
47
  : Math.max(1, Math.floor(event.allocatorAttempts)),
49
- lockWaitMs: event.lockWaitMs === undefined
50
- ? undefined
51
- : Math.max(0, Math.floor(event.lockWaitMs)),
52
- redisReadMs: event.redisReadMs === undefined
53
- ? undefined
54
- : Math.max(0, Math.floor(event.redisReadMs)),
55
- redisWriteMs: event.redisWriteMs === undefined
56
- ? undefined
57
- : Math.max(0, Math.floor(event.redisWriteMs)),
58
- poolAgeMs: event.poolAgeMs === undefined
59
- ? undefined
60
- : Math.max(0, Math.floor(event.poolAgeMs)),
48
+ lockWaitMs: event.lockWaitMs === undefined ? undefined : Math.max(0, Math.floor(event.lockWaitMs)),
49
+ redisReadMs: event.redisReadMs === undefined ? undefined : Math.max(0, Math.floor(event.redisReadMs)),
50
+ redisWriteMs: event.redisWriteMs === undefined ? undefined : Math.max(0, Math.floor(event.redisWriteMs)),
51
+ poolAgeMs: event.poolAgeMs === undefined ? undefined : Math.max(0, Math.floor(event.poolAgeMs)),
61
52
  poolExpiresInMs: event.poolExpiresInMs === undefined
62
53
  ? undefined
63
54
  : Math.max(0, Math.floor(event.poolExpiresInMs)),
64
55
  attempts: Math.max(1, Math.floor(event.attempts || 1)),
65
- refreshes: event.refreshes === undefined
66
- ? undefined
67
- : Math.max(0, Math.floor(event.refreshes)),
56
+ refreshes: event.refreshes === undefined ? undefined : Math.max(0, Math.floor(event.refreshes)),
57
+ });
58
+ }
59
+ recordProxyVendorFailover(event) {
60
+ if (this.#failovers.length >= MAX_PROXY_FAILOVER_SAMPLES)
61
+ return;
62
+ this.#failovers.push({
63
+ vendor: event.vendor,
64
+ ...(event.nextVendor ? { nextVendor: event.nextVendor } : {}),
65
+ phase: event.phase,
66
+ reason: event.reason,
67
+ ...(event.attempt === undefined ? {} : { attempt: Math.max(0, Math.floor(event.attempt)) }),
68
68
  });
69
69
  }
70
70
  recordProxyAttempt(event) {
71
71
  if (this.#attempts.length >= MAX_PROXY_ATTEMPT_SAMPLES)
72
72
  return;
73
73
  this.#attempts.push({
74
- provider: "smartproxy",
74
+ provider: event.provider,
75
75
  attempt: Math.max(1, Math.floor(event.attempt || 1)),
76
76
  ...(event.poolIndex === undefined
77
77
  ? {}
@@ -79,9 +79,7 @@ export class ProxyTelemetryCollector {
79
79
  ...(event.proxyHash ? { proxyHash: event.proxyHash.slice(0, 16) } : {}),
80
80
  outcome: event.outcome === "ok" ? "ok" : "error",
81
81
  ...(event.errorCode ? { errorCode: event.errorCode.slice(0, 80) } : {}),
82
- ...(event.status === undefined
83
- ? {}
84
- : { status: Math.max(0, Math.floor(event.status)) }),
82
+ ...(event.status === undefined ? {} : { status: Math.max(0, Math.floor(event.status)) }),
85
83
  ...(event.durationMs === undefined
86
84
  ? {}
87
85
  : { durationMs: Math.max(0, Math.floor(event.durationMs)) }),
@@ -91,8 +89,16 @@ export class ProxyTelemetryCollector {
91
89
  const [first, ...rest] = this.#events;
92
90
  if (!first)
93
91
  return undefined;
92
+ // The serving vendor/protocol is the last recorded resolution (a failed
93
+ // vendor records first, the vendor that served records last).
94
+ const serving = this.#events[this.#events.length - 1] ?? first;
95
+ const vendors = [];
96
+ for (const event of this.#events) {
97
+ if (!vendors.includes(event.provider))
98
+ vendors.push(event.provider);
99
+ }
94
100
  const aggregate = rest.reduce((acc, event) => ({
95
- provider: "smartproxy",
101
+ provider: event.provider,
96
102
  cacheStatus: worseStatus(acc.cacheStatus, event.cacheStatus),
97
103
  cacheHit: acc.cacheHit && event.cacheHit,
98
104
  resolutionMs: acc.resolutionMs + event.resolutionMs,
@@ -111,13 +117,12 @@ export class ProxyTelemetryCollector {
111
117
  const payload = {
112
118
  v: 1,
113
119
  proxy: {
114
- provider: "smartproxy",
120
+ provider: serving.provider,
121
+ ...(serving.protocol ? { protocol: serving.protocol } : {}),
115
122
  cacheStatus: aggregate.cacheStatus,
116
123
  cacheHit: aggregate.cacheHit,
117
124
  resolutionMs: aggregate.resolutionMs,
118
- ...(aggregate.allocatorMs !== undefined
119
- ? { allocatorMs: aggregate.allocatorMs }
120
- : {}),
125
+ ...(aggregate.allocatorMs !== undefined ? { allocatorMs: aggregate.allocatorMs } : {}),
121
126
  ...(aggregate.allocatorStatus !== undefined
122
127
  ? { allocatorStatus: aggregate.allocatorStatus }
123
128
  : {}),
@@ -127,40 +132,38 @@ export class ProxyTelemetryCollector {
127
132
  ...(aggregate.allocatorAttempts !== undefined
128
133
  ? { allocatorAttempts: aggregate.allocatorAttempts }
129
134
  : {}),
130
- ...(aggregate.lockWaitMs !== undefined
131
- ? { lockWaitMs: aggregate.lockWaitMs }
132
- : {}),
133
- ...(aggregate.redisReadMs !== undefined
134
- ? { redisReadMs: aggregate.redisReadMs }
135
- : {}),
136
- ...(aggregate.redisWriteMs !== undefined
137
- ? { redisWriteMs: aggregate.redisWriteMs }
138
- : {}),
139
- ...(aggregate.poolAgeMs !== undefined
140
- ? { poolAgeMs: aggregate.poolAgeMs }
141
- : {}),
135
+ ...(aggregate.lockWaitMs !== undefined ? { lockWaitMs: aggregate.lockWaitMs } : {}),
136
+ ...(aggregate.redisReadMs !== undefined ? { redisReadMs: aggregate.redisReadMs } : {}),
137
+ ...(aggregate.redisWriteMs !== undefined ? { redisWriteMs: aggregate.redisWriteMs } : {}),
138
+ ...(aggregate.poolAgeMs !== undefined ? { poolAgeMs: aggregate.poolAgeMs } : {}),
142
139
  ...(aggregate.poolExpiresInMs !== undefined
143
140
  ? { poolExpiresInMs: aggregate.poolExpiresInMs }
144
141
  : {}),
145
142
  attempts: aggregate.attempts,
146
- ...(aggregate.refreshes !== undefined
147
- ? { refreshes: aggregate.refreshes }
148
- : {}),
143
+ ...(aggregate.refreshes !== undefined ? { refreshes: aggregate.refreshes } : {}),
149
144
  ...(this.#attempts.length > 0
150
145
  ? {
151
146
  attemptSamples: this.#attempts.map((attempt, index) => ({
152
147
  n: index + 1,
153
148
  a: attempt.attempt,
154
- ...(attempt.poolIndex === undefined
155
- ? {}
156
- : { i: attempt.poolIndex }),
149
+ ...(attempt.poolIndex === undefined ? {} : { i: attempt.poolIndex }),
157
150
  ...(attempt.proxyHash ? { h: attempt.proxyHash } : {}),
158
151
  o: attempt.outcome,
159
152
  ...(attempt.errorCode ? { c: attempt.errorCode } : {}),
160
153
  ...(attempt.status === undefined ? {} : { s: attempt.status }),
161
- ...(attempt.durationMs === undefined
162
- ? {}
163
- : { d: attempt.durationMs }),
154
+ ...(attempt.durationMs === undefined ? {} : { d: attempt.durationMs }),
155
+ })),
156
+ }
157
+ : {}),
158
+ ...(vendors.length > 1 ? { vendors } : {}),
159
+ ...(this.#failovers.length > 0
160
+ ? {
161
+ failovers: this.#failovers.map((failover) => ({
162
+ v: failover.vendor,
163
+ ...(failover.nextVendor ? { nx: failover.nextVendor } : {}),
164
+ p: failover.phase,
165
+ r: failover.reason,
166
+ ...(failover.attempt === undefined ? {} : { a: failover.attempt }),
164
167
  })),
165
168
  }
166
169
  : {}),
@@ -0,0 +1,29 @@
1
+ import type { HttpRedirectFailureReason } from "../types.js";
2
+ export type RedirectHopDecision<TMethod extends string> = {
3
+ kind: "follow";
4
+ nextMethod: TMethod | "GET";
5
+ nextUrl: string;
6
+ } | {
7
+ kind: "stop";
8
+ reason: HttpRedirectFailureReason;
9
+ nextUrl?: string;
10
+ };
11
+ export declare function isRedirectStatus(status: number): boolean;
12
+ /** Shared fetch-compatible redirect method rewriting for stealth and ctx.http. */
13
+ export declare function nextRedirectMethod<TMethod extends string>(status: number, method: TMethod): TMethod | "GET";
14
+ /** Resolves a Location value against the response URL without issuing a request. */
15
+ export declare function resolveRedirectUrl(location: string | undefined, responseUrl: string): string | undefined;
16
+ /**
17
+ * Shared post-response decision ordering for both redirect walkers. The
18
+ * caller-owned stop hook is checked first, matching stealth's pre-follow
19
+ * contract, then structural termination and loop checks run before follow.
20
+ */
21
+ export declare function evaluateRedirectHop<TMethod extends string>(input: {
22
+ status: number;
23
+ method: TMethod;
24
+ nextUrl: string | undefined;
25
+ shouldStop: boolean;
26
+ redirectCount: number;
27
+ maxHops: number;
28
+ visitedRequests: ReadonlySet<string>;
29
+ }): RedirectHopDecision<TMethod>;
@@ -0,0 +1,36 @@
1
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
2
+ export function isRedirectStatus(status) {
3
+ return REDIRECT_STATUSES.has(status);
4
+ }
5
+ /** Shared fetch-compatible redirect method rewriting for stealth and ctx.http. */
6
+ export function nextRedirectMethod(status, method) {
7
+ if (status === 303 && method !== "HEAD")
8
+ return "GET";
9
+ if ((status === 301 || status === 302) && method === "POST")
10
+ return "GET";
11
+ return method;
12
+ }
13
+ /** Resolves a Location value against the response URL without issuing a request. */
14
+ export function resolveRedirectUrl(location, responseUrl) {
15
+ return location ? new URL(location, responseUrl).toString() : undefined;
16
+ }
17
+ /**
18
+ * Shared post-response decision ordering for both redirect walkers. The
19
+ * caller-owned stop hook is checked first, matching stealth's pre-follow
20
+ * contract, then structural termination and loop checks run before follow.
21
+ */
22
+ export function evaluateRedirectHop(input) {
23
+ if (input.shouldStop) {
24
+ return { kind: "stop", reason: "stopped", ...(input.nextUrl ? { nextUrl: input.nextUrl } : {}) };
25
+ }
26
+ if (!input.nextUrl)
27
+ return { kind: "stop", reason: "missing_location" };
28
+ if (input.redirectCount > input.maxHops) {
29
+ return { kind: "stop", reason: "max_hops", nextUrl: input.nextUrl };
30
+ }
31
+ const nextMethod = nextRedirectMethod(input.status, input.method);
32
+ if (input.visitedRequests.has(`${nextMethod} ${input.nextUrl}`)) {
33
+ return { kind: "stop", reason: "loop", nextUrl: input.nextUrl };
34
+ }
35
+ return { kind: "follow", nextMethod, nextUrl: input.nextUrl };
36
+ }
@@ -1,4 +1,4 @@
1
- import Redis from "ioredis";
1
+ import { Redis } from "ioredis";
2
2
  export type ProviderRedisClient = Redis;
3
3
  export type ProviderRedisClientOptions = {
4
4
  readonly redisUrl: string;
@@ -1,4 +1,4 @@
1
- import Redis from "ioredis";
1
+ import { Redis } from "ioredis";
2
2
  export function createProviderRedisClient(options) {
3
3
  const redis = new Redis(options.redisUrl, {
4
4
  connectTimeout: options.timeoutMs,
@@ -23,9 +23,7 @@ export async function withRedisTimeout(operation, options) {
23
23
  }
24
24
  }, options.timeoutMs);
25
25
  });
26
- const operationResult = options.onError
27
- ? operation().catch(options.onError)
28
- : operation();
26
+ const operationResult = options.onError ? operation().catch(options.onError) : operation();
29
27
  return await Promise.race([operationResult, timeout]);
30
28
  }
31
29
  finally {