@apifuse/provider-sdk 2.2.0-beta.5 → 2.2.0-beta.50

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 (365) hide show
  1. package/AUTHORING.md +537 -33
  2. package/CHANGELOG.md +201 -1
  3. package/README.md +83 -22
  4. package/SUBMISSION.md +2 -2
  5. package/bin/apifuse-check.ts +165 -11
  6. package/bin/apifuse-dev.ts +57 -13
  7. package/bin/apifuse-migrate-operation-declaration.ts +55 -0
  8. package/bin/apifuse-migrate-shape.ts +202 -0
  9. package/bin/apifuse-pack-check.ts +22 -2
  10. package/bin/apifuse-pack-smoke.ts +57 -2
  11. package/bin/apifuse-pack-types.ts +357 -38
  12. package/bin/apifuse-perf.ts +14 -13
  13. package/bin/apifuse-record.ts +709 -72
  14. package/bin/apifuse-submit-check.ts +2344 -324
  15. package/bin/apifuse-sync-assets.ts +117 -0
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/dist/auth-turn/index.d.ts +3 -3
  18. package/dist/auth-turn/index.js +1 -1
  19. package/dist/auth.d.ts +14 -0
  20. package/dist/auth.js +67 -0
  21. package/dist/ceremonies/index.d.ts +16 -0
  22. package/dist/ceremonies/index.js +141 -36
  23. package/dist/cli/commands.d.ts +1 -1
  24. package/dist/cli/commands.js +27 -0
  25. package/dist/cli/create.d.ts +4 -1
  26. package/dist/cli/create.js +42 -37
  27. package/dist/cli/migrate-operation-declaration.d.ts +59 -0
  28. package/dist/cli/migrate-operation-declaration.js +1178 -0
  29. package/dist/cli/migrate-operation-shape.d.ts +44 -0
  30. package/dist/cli/migrate-operation-shape.js +113 -0
  31. package/dist/cli/migrate-provider-shape.d.ts +52 -0
  32. package/dist/cli/migrate-provider-shape.js +578 -0
  33. package/dist/cli/prompt-assets.d.ts +80 -0
  34. package/dist/cli/prompt-assets.js +743 -0
  35. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  36. package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
  37. package/dist/cli/templates/provider/README.md.tpl +5 -5
  38. package/dist/cli/templates/provider/index.ts.tpl +6 -3
  39. package/dist/cli/templates/provider/operations/ping.ts.tpl +4 -1
  40. package/dist/cli/templates/provider/provider.json.tpl +6 -0
  41. package/dist/config/loader.d.ts +179 -16
  42. package/dist/config/loader.js +435 -127
  43. package/dist/contract-serialization.js +4 -8
  44. package/dist/contract-types.d.ts +12 -5
  45. package/dist/contract.js +24 -10
  46. package/dist/declaration-validation.d.ts +33 -0
  47. package/dist/declaration-validation.js +266 -0
  48. package/dist/define.d.ts +66 -29
  49. package/dist/define.js +778 -69
  50. package/dist/dev.d.ts +3 -0
  51. package/dist/dev.js +1 -1
  52. package/dist/engine.d.ts +78 -0
  53. package/dist/engine.js +133 -0
  54. package/dist/error-observability.d.ts +7 -0
  55. package/dist/error-observability.js +61 -0
  56. package/dist/error-resolution.d.ts +4 -0
  57. package/dist/error-resolution.js +122 -0
  58. package/dist/errors.d.ts +33 -0
  59. package/dist/errors.js +40 -0
  60. package/dist/fixture-sanitization.d.ts +28 -0
  61. package/dist/fixture-sanitization.js +227 -0
  62. package/dist/health-scenario.d.ts +1842 -0
  63. package/dist/health-scenario.js +624 -0
  64. package/dist/index.d.ts +21 -10
  65. package/dist/index.js +12 -7
  66. package/dist/lint.d.ts +23 -11
  67. package/dist/lint.js +572 -69
  68. package/dist/native-address.d.ts +43 -0
  69. package/dist/native-address.js +281 -0
  70. package/dist/native-egress-policy.d.ts +31 -0
  71. package/dist/native-egress-policy.js +288 -0
  72. package/dist/observability.d.ts +5 -2
  73. package/dist/observability.js +48 -1
  74. package/dist/provider.d.ts +10 -2
  75. package/dist/provider.js +4 -1
  76. package/dist/runtime/auth-flow.d.ts +5 -1
  77. package/dist/runtime/auth-flow.js +6 -0
  78. package/dist/runtime/browser.d.ts +1 -0
  79. package/dist/runtime/browser.js +492 -49
  80. package/dist/runtime/cache.d.ts +1 -0
  81. package/dist/runtime/cache.js +169 -15
  82. package/dist/runtime/choice-wordlist.d.ts +9 -0
  83. package/dist/runtime/choice-wordlist.js +138 -0
  84. package/dist/runtime/choice.d.ts +13 -1
  85. package/dist/runtime/choice.js +490 -102
  86. package/dist/runtime/chrome149-header-order.d.ts +58 -0
  87. package/dist/runtime/chrome149-header-order.js +289 -0
  88. package/dist/runtime/env.js +12 -0
  89. package/dist/runtime/executor.d.ts +3 -2
  90. package/dist/runtime/executor.js +26 -25
  91. package/dist/runtime/http.d.ts +1 -0
  92. package/dist/runtime/http.js +515 -53
  93. package/dist/runtime/insights.js +2 -2
  94. package/dist/runtime/instrumentation.d.ts +2 -2
  95. package/dist/runtime/instrumentation.js +366 -8
  96. package/dist/runtime/native-network-errors.d.ts +33 -0
  97. package/dist/runtime/native-network-errors.js +69 -0
  98. package/dist/runtime/native-network.d.ts +96 -0
  99. package/dist/runtime/native-network.js +1232 -0
  100. package/dist/runtime/ocr.d.ts +29 -0
  101. package/dist/runtime/ocr.js +440 -0
  102. package/dist/runtime/otlp.d.ts +71 -2
  103. package/dist/runtime/otlp.js +397 -16
  104. package/dist/runtime/proxy-errors.js +6 -2
  105. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  106. package/dist/runtime/proxy-nodemaven.js +146 -0
  107. package/dist/runtime/proxy-telemetry.d.ts +80 -1
  108. package/dist/runtime/proxy-telemetry.js +154 -47
  109. package/dist/runtime/redirects.d.ts +29 -0
  110. package/dist/runtime/redirects.js +36 -0
  111. package/dist/runtime/redis.d.ts +1 -1
  112. package/dist/runtime/redis.js +4 -2
  113. package/dist/runtime/request-options.d.ts +68 -1
  114. package/dist/runtime/request-options.js +548 -0
  115. package/dist/runtime/resolver-config.d.ts +6 -0
  116. package/dist/runtime/resolver-config.js +6 -0
  117. package/dist/runtime/resolver-public.d.ts +1 -0
  118. package/dist/runtime/resolver-public.js +1 -0
  119. package/dist/runtime/resolver-shared.d.ts +14 -0
  120. package/dist/runtime/resolver-shared.js +12 -0
  121. package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
  122. package/dist/runtime/resolver-vendors/bindings.js +40 -0
  123. package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
  124. package/dist/runtime/resolver-vendors/browser.js +377 -0
  125. package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
  126. package/dist/runtime/resolver-vendors/capsolver.js +531 -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 +408 -0
  131. package/dist/runtime/resolver-vendors/types.d.ts +94 -0
  132. package/dist/runtime/resolver-vendors/types.js +96 -0
  133. package/dist/runtime/resolver.d.ts +72 -0
  134. package/dist/runtime/resolver.js +771 -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 +3 -0
  138. package/dist/runtime/state.js +277 -71
  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 +41 -5
  142. package/dist/runtime/stealth.js +997 -291
  143. package/dist/runtime/stt.js +1 -12
  144. package/dist/runtime/timeout.d.ts +5 -0
  145. package/dist/runtime/timeout.js +12 -0
  146. package/dist/runtime/trace-config.d.ts +12 -0
  147. package/dist/runtime/trace-config.js +62 -0
  148. package/dist/runtime/trace.d.ts +5 -0
  149. package/dist/runtime/trace.js +43 -10
  150. package/dist/serve.d.ts +1 -1
  151. package/dist/serve.js +1 -1
  152. package/dist/server/error-observability.d.ts +1 -0
  153. package/dist/server/error-observability.js +1 -0
  154. package/dist/server/index.d.ts +5 -2
  155. package/dist/server/index.js +2 -2
  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.d.ts +14 -3
  159. package/dist/server/self-test.js +129 -58
  160. package/dist/server/serve-implementation.d.ts +230 -0
  161. package/dist/server/serve-implementation.js +2265 -0
  162. package/dist/server/serve.d.ts +1 -70
  163. package/dist/server/serve.js +1 -1131
  164. package/dist/server/trace-output.d.ts +6 -0
  165. package/dist/server/trace-output.js +79 -0
  166. package/dist/server/types.d.ts +30 -5
  167. package/dist/server/types.js +13 -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 +9 -4
  207. package/dist/stealth/profiles.js +160 -211
  208. package/dist/stream-evidence.d.ts +74 -0
  209. package/dist/stream-evidence.js +785 -0
  210. package/dist/stream.js +7 -1
  211. package/dist/testing/index.d.ts +2 -1
  212. package/dist/testing/index.js +2 -1
  213. package/dist/testing/run.d.ts +32 -2
  214. package/dist/testing/run.js +489 -21
  215. package/dist/trace-sanitization.d.ts +5 -0
  216. package/dist/trace-sanitization.js +45 -0
  217. package/dist/types.d.ts +673 -131
  218. package/dist/types.js +1 -0
  219. package/package.json +45 -5
  220. package/src/auth-turn/index.ts +1 -1
  221. package/src/auth.ts +118 -0
  222. package/src/ceremonies/index.ts +189 -46
  223. package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +6 -0
  224. package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +3 -0
  225. package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +10 -0
  226. package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +8 -0
  227. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +5 -0
  228. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +16 -0
  229. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +3 -0
  230. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +31 -0
  231. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +11 -0
  232. package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +11 -0
  233. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +11 -0
  234. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +21 -0
  235. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +11 -0
  236. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +14 -0
  237. package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +7 -0
  238. package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +6 -0
  239. package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +7 -0
  240. package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +6 -0
  241. package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +7 -0
  242. package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +7 -0
  243. package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +15 -0
  244. package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +4 -0
  245. package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +12 -0
  246. package/src/cli/commands.ts +33 -0
  247. package/src/cli/create.ts +50 -37
  248. package/src/cli/migrate-operation-declaration.ts +1654 -0
  249. package/src/cli/migrate-operation-shape.ts +184 -0
  250. package/src/cli/migrate-provider-shape.ts +772 -0
  251. package/src/cli/prompt-assets.ts +865 -0
  252. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  253. package/src/cli/templates/provider/Dockerfile.tpl +1 -1
  254. package/src/cli/templates/provider/README.md.tpl +5 -5
  255. package/src/cli/templates/provider/index.ts.tpl +6 -3
  256. package/src/cli/templates/provider/operations/ping.ts.tpl +4 -1
  257. package/src/cli/templates/provider/provider.json.tpl +6 -0
  258. package/src/config/loader.ts +690 -163
  259. package/src/contract-serialization.ts +5 -7
  260. package/src/contract-types.ts +12 -5
  261. package/src/contract.ts +24 -10
  262. package/src/declaration-validation.ts +330 -0
  263. package/src/define.ts +1048 -131
  264. package/src/dev.ts +4 -1
  265. package/src/engine.ts +279 -0
  266. package/src/error-observability.ts +64 -0
  267. package/src/error-resolution.ts +127 -0
  268. package/src/errors.ts +68 -0
  269. package/src/fixture-sanitization.ts +264 -0
  270. package/src/health-scenario.ts +875 -0
  271. package/src/index.ts +220 -13
  272. package/src/lint.ts +698 -90
  273. package/src/native-address.ts +340 -0
  274. package/src/native-egress-policy.ts +358 -0
  275. package/src/observability.ts +51 -1
  276. package/src/provider.ts +161 -3
  277. package/src/runtime/auth-flow.ts +12 -0
  278. package/src/runtime/browser.ts +661 -63
  279. package/src/runtime/cache.ts +189 -14
  280. package/src/runtime/choice-wordlist.ts +145 -0
  281. package/src/runtime/choice.ts +631 -120
  282. package/src/runtime/chrome149-header-order.ts +330 -0
  283. package/src/runtime/env.ts +13 -0
  284. package/src/runtime/executor.ts +43 -31
  285. package/src/runtime/http.ts +641 -61
  286. package/src/runtime/insights.ts +2 -2
  287. package/src/runtime/instrumentation.ts +520 -15
  288. package/src/runtime/native-network-errors.ts +99 -0
  289. package/src/runtime/native-network.ts +1605 -0
  290. package/src/runtime/ocr.ts +523 -0
  291. package/src/runtime/otlp.ts +467 -21
  292. package/src/runtime/proxy-errors.ts +12 -4
  293. package/src/runtime/proxy-nodemaven.ts +221 -0
  294. package/src/runtime/proxy-telemetry.ts +244 -75
  295. package/src/runtime/redirects.ts +66 -0
  296. package/src/runtime/redis.ts +7 -2
  297. package/src/runtime/request-options.ts +680 -1
  298. package/src/runtime/resolver-config.ts +6 -0
  299. package/src/runtime/resolver-public.ts +23 -0
  300. package/src/runtime/resolver-shared.ts +33 -0
  301. package/src/runtime/resolver-vendors/bindings.ts +56 -0
  302. package/src/runtime/resolver-vendors/browser.ts +533 -0
  303. package/src/runtime/resolver-vendors/capsolver.ts +707 -0
  304. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  305. package/src/runtime/resolver-vendors/twocaptcha.ts +540 -0
  306. package/src/runtime/resolver-vendors/types.ts +212 -0
  307. package/src/runtime/resolver.ts +1157 -0
  308. package/src/runtime/secrets.ts +64 -0
  309. package/src/runtime/state.ts +394 -77
  310. package/src/runtime/stealth-cookies.ts +132 -0
  311. package/src/runtime/stealth.ts +1280 -336
  312. package/src/runtime/stt.ts +1 -19
  313. package/src/runtime/timeout.ts +18 -0
  314. package/src/runtime/trace-config.ts +78 -0
  315. package/src/runtime/trace.ts +57 -17
  316. package/src/serve.ts +6 -1
  317. package/src/server/error-observability.ts +1 -0
  318. package/src/server/index.ts +34 -2
  319. package/src/server/self-test-input-tokens.ts +29 -14
  320. package/src/server/self-test.ts +191 -68
  321. package/src/server/serve-implementation.ts +3378 -0
  322. package/src/server/serve.ts +1 -1632
  323. package/src/server/trace-output.ts +129 -0
  324. package/src/server/types.ts +13 -1
  325. package/src/stateful/README.md +146 -0
  326. package/src/stateful/errors.ts +35 -0
  327. package/src/stateful/http-provider-event-emitter.ts +314 -0
  328. package/src/stateful/http-session-owner-registry.ts +306 -0
  329. package/src/stateful/index.ts +18 -0
  330. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  331. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  332. package/src/stateful/provider-event-pipeline.ts +61 -0
  333. package/src/stateful/provider-events.ts +462 -0
  334. package/src/stateful/session-key.ts +111 -0
  335. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  336. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  337. package/src/stateful/stateful-provider-adapter.ts +562 -0
  338. package/src/stateful/stateful-provider-observability.ts +261 -0
  339. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  340. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  341. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  342. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  343. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  344. package/src/stateful-signing.ts +46 -0
  345. package/src/stealth/profiles.ts +202 -231
  346. package/src/stream-evidence.ts +988 -0
  347. package/src/stream.ts +8 -1
  348. package/src/testing/index.ts +10 -1
  349. package/src/testing/run.ts +658 -15
  350. package/src/trace-sanitization.ts +63 -0
  351. package/src/types.ts +788 -174
  352. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  353. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  354. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  355. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  356. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  357. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  358. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  359. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  360. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  361. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  362. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  363. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  364. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  365. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -1,16 +1,52 @@
1
- import { resolveProxyConfigAsync } from "../config/loader.js";
2
- import { ProviderError, TransportError } from "../errors.js";
1
+ import { policyRotatesTransportVendorChain, resolvePolicyTransportAttemptCap, resolveProxyConfigAsync, } from "../config/loader.js";
2
+ import { HttpRedirectError, ProviderError, TransportError } from "../errors.js";
3
3
  import { parseSseStream, readableBytes, readableLines, readableTextChunks } from "../stream.js";
4
4
  import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, isProxyTransportRetryMethod, normalizeProxyTransportRetryOptions, proxyTransportRetryErrorCode, proxyTransportRetryErrorStatus, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy.js";
5
- import { appendQueryParams, normalizeHttpRequestBody } from "./request-options.js";
5
+ import { evaluateRedirectHop, isRedirectStatus, resolveRedirectUrl } from "./redirects.js";
6
+ import { normalizeHttpRequestBody, redactSensitiveError, redactSensitiveRequestError, serializeRequestUrl, } from "./request-options.js";
6
7
  const DEFAULT_HTTP_BASE_URL = "http://localhost";
7
8
  function isHttpStatusOutcome(outcome) {
8
9
  return "kind" in outcome && outcome.kind === "http-status";
9
10
  }
10
- async function sleep(ms) {
11
+ function isDedupeSkipOutcome(outcome) {
12
+ return "kind" in outcome && outcome.kind === "dedupe-skip";
13
+ }
14
+ function toAmbientCancellationError(signal, error = signal.reason) {
15
+ if (error instanceof TransportError && error.code === "transport_cancelled") {
16
+ return error;
17
+ }
18
+ return new TransportError("Request cancelled", {
19
+ code: "transport_cancelled",
20
+ status: 0,
21
+ retryable: false,
22
+ ...(error !== undefined
23
+ ? { cause: error instanceof Error ? error : new Error(String(error)) }
24
+ : {}),
25
+ });
26
+ }
27
+ function throwIfAmbientAborted(signal) {
28
+ if (signal?.aborted)
29
+ throw toAmbientCancellationError(signal);
30
+ }
31
+ async function sleep(ms, signal) {
32
+ throwIfAmbientAborted(signal);
11
33
  if (ms <= 0)
12
34
  return;
13
- await new Promise((resolve) => setTimeout(resolve, ms));
35
+ if (!signal) {
36
+ await new Promise((resolve) => setTimeout(resolve, ms));
37
+ return;
38
+ }
39
+ await new Promise((resolve, reject) => {
40
+ const onAbort = () => {
41
+ clearTimeout(timer);
42
+ reject(toAmbientCancellationError(signal));
43
+ };
44
+ const timer = setTimeout(() => {
45
+ signal.removeEventListener("abort", onAbort);
46
+ resolve();
47
+ }, ms);
48
+ signal.addEventListener("abort", onAbort, { once: true });
49
+ });
14
50
  }
15
51
  function toUpstreamHttpError(status) {
16
52
  return new TransportError(`Upstream request failed with status ${status}`, {
@@ -49,7 +85,17 @@ function parseJson(body) {
49
85
  function isTimeoutMessage(message) {
50
86
  return /\b(timed out|timeout|deadline exceeded)\b/i.test(message);
51
87
  }
52
- function toHttpTransportError(error) {
88
+ function toHttpTransportError(error, ambientSignal, timeoutSignal) {
89
+ if (ambientSignal?.aborted) {
90
+ return toAmbientCancellationError(ambientSignal, error);
91
+ }
92
+ if (timeoutSignal?.aborted) {
93
+ return new TransportError("Request timed out", {
94
+ code: "transport_timeout",
95
+ status: 0,
96
+ ...(error instanceof Error ? { cause: error } : {}),
97
+ });
98
+ }
53
99
  if (error instanceof TransportError) {
54
100
  if (error.code) {
55
101
  return error;
@@ -121,9 +167,108 @@ function requireNativeResponseBody(response) {
121
167
  }
122
168
  return response.body;
123
169
  }
124
- function toNativeHttpStreamResponse(response) {
170
+ function mergeAbortSignals(...signals) {
171
+ const activeSignals = signals.filter((signal) => signal != null);
172
+ if (activeSignals.length === 0)
173
+ return undefined;
174
+ if (activeSignals.length === 1)
175
+ return activeSignals[0];
176
+ return AbortSignal.any(activeSignals);
177
+ }
178
+ function cancelStreamOnAbort(body, signal) {
179
+ if (!signal)
180
+ return body;
181
+ let reader;
182
+ let finished = false;
183
+ let streamController;
184
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
185
+ const onAbort = () => {
186
+ if (finished)
187
+ return;
188
+ finished = true;
189
+ cleanup();
190
+ const reason = toAmbientCancellationError(signal);
191
+ streamController?.error(reason);
192
+ const cancellation = reader ? reader.cancel(reason) : body.cancel(reason);
193
+ void cancellation.catch(() => undefined).finally(() => reader?.releaseLock());
194
+ };
195
+ return new ReadableStream({
196
+ start(controller) {
197
+ streamController = controller;
198
+ signal.addEventListener("abort", onAbort, { once: true });
199
+ if (signal.aborted)
200
+ onAbort();
201
+ },
202
+ async pull(controller) {
203
+ try {
204
+ reader ??= body.getReader();
205
+ const chunk = await reader.read();
206
+ if (finished)
207
+ return;
208
+ if (chunk.done) {
209
+ finished = true;
210
+ cleanup();
211
+ controller.close();
212
+ reader.releaseLock();
213
+ return;
214
+ }
215
+ controller.enqueue(chunk.value);
216
+ }
217
+ catch (error) {
218
+ if (finished)
219
+ return;
220
+ finished = true;
221
+ cleanup();
222
+ controller.error(error);
223
+ reader?.releaseLock();
224
+ }
225
+ },
226
+ async cancel(reason) {
227
+ if (finished)
228
+ return;
229
+ finished = true;
230
+ cleanup();
231
+ try {
232
+ await (reader ? reader.cancel(reason) : body.cancel(reason));
233
+ }
234
+ finally {
235
+ reader?.releaseLock();
236
+ }
237
+ },
238
+ }, { highWaterMark: 0 });
239
+ }
240
+ function sanitizeStreamErrors(body, serializedUrl) {
241
+ if (serializedUrl.sensitiveValues.length === 0)
242
+ return body;
243
+ let reader;
244
+ return new ReadableStream({
245
+ async pull(controller) {
246
+ try {
247
+ reader ??= body.getReader();
248
+ const chunk = await reader.read();
249
+ if (chunk.done) {
250
+ controller.close();
251
+ return;
252
+ }
253
+ controller.enqueue(chunk.value);
254
+ }
255
+ catch (error) {
256
+ controller.error(redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl));
257
+ }
258
+ },
259
+ async cancel(reason) {
260
+ try {
261
+ await (reader ? reader.cancel(reason) : body.cancel(reason));
262
+ }
263
+ catch (error) {
264
+ throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
265
+ }
266
+ },
267
+ }, { highWaterMark: 0 });
268
+ }
269
+ function toNativeHttpStreamResponse(response, serializedUrl, signal) {
125
270
  const headers = Object.fromEntries(response.headers.entries());
126
- const body = requireNativeResponseBody(response);
271
+ const body = sanitizeStreamErrors(cancelStreamOnAbort(requireNativeResponseBody(response), signal), serializedUrl);
127
272
  return {
128
273
  body,
129
274
  headers,
@@ -164,17 +309,194 @@ function isAbsoluteUrl(url) {
164
309
  function resolveHttpUrl(baseUrl, url) {
165
310
  return new URL(url, baseUrl ?? DEFAULT_HTTP_BASE_URL).toString();
166
311
  }
312
+ const MAX_HTTP_REDIRECT_HOPS = 20;
313
+ const HTTP_REDIRECT_POLICY_FIELDS = new Set(["mode", "maxHops"]);
314
+ const REDIRECT_BODY_HEADERS = new Set([
315
+ "content-encoding",
316
+ "content-language",
317
+ "content-location",
318
+ "content-type",
319
+ ]);
320
+ const MALFORMED_REDIRECT_TARGET = "[malformed redirect target]";
321
+ function invalidHttpRedirectPolicy(message, cause) {
322
+ return new TransportError(`Invalid ctx.http redirectPolicy: ${message}`, {
323
+ code: "http_redirect_policy_invalid",
324
+ ...(cause ? { cause } : {}),
325
+ });
326
+ }
327
+ /** Snapshot untrusted caller input synchronously, before proxy resolution or fetch. */
328
+ function normalizeHttpRedirectPolicy(value) {
329
+ if (value === undefined)
330
+ return undefined;
331
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
332
+ throw invalidHttpRedirectPolicy("expected an object");
333
+ }
334
+ try {
335
+ const keys = Reflect.ownKeys(value);
336
+ for (const key of keys) {
337
+ if (typeof key !== "string" || !HTTP_REDIRECT_POLICY_FIELDS.has(key)) {
338
+ throw invalidHttpRedirectPolicy(`unknown field ${String(key)}`);
339
+ }
340
+ }
341
+ for (const field of HTTP_REDIRECT_POLICY_FIELDS) {
342
+ const descriptor = Object.getOwnPropertyDescriptor(value, field);
343
+ if (!descriptor || !("value" in descriptor)) {
344
+ throw invalidHttpRedirectPolicy(`${field} must be an own data property`);
345
+ }
346
+ }
347
+ const record = value;
348
+ if (record.mode !== "same-origin") {
349
+ throw invalidHttpRedirectPolicy('mode must be "same-origin"');
350
+ }
351
+ if (typeof record.maxHops !== "number" ||
352
+ !Number.isInteger(record.maxHops) ||
353
+ record.maxHops < 0 ||
354
+ record.maxHops > MAX_HTTP_REDIRECT_HOPS) {
355
+ throw invalidHttpRedirectPolicy(`maxHops must be an integer from 0 to ${MAX_HTTP_REDIRECT_HOPS}`);
356
+ }
357
+ return { mode: "same-origin", maxHops: record.maxHops };
358
+ }
359
+ catch (error) {
360
+ if (error instanceof TransportError)
361
+ throw error;
362
+ throw invalidHttpRedirectPolicy("could not be inspected safely", error instanceof Error ? error : undefined);
363
+ }
364
+ }
365
+ function snapshotHttpRedirectPolicy(options) {
366
+ try {
367
+ return normalizeHttpRedirectPolicy(options.redirectPolicy);
368
+ }
369
+ catch (error) {
370
+ if (error instanceof TransportError)
371
+ throw error;
372
+ throw invalidHttpRedirectPolicy("could not be read safely", error instanceof Error ? error : undefined);
373
+ }
374
+ }
375
+ function withoutRedirectBodyHeaders(headers) {
376
+ const nextHeaders = new Headers(headers);
377
+ for (const name of REDIRECT_BODY_HEADERS)
378
+ nextHeaders.delete(name);
379
+ return nextHeaders;
380
+ }
381
+ function redirectDiagnosticTarget(value) {
382
+ try {
383
+ const parsed = new URL(value);
384
+ // Origin omits URL userinfo. Keeping only origin + path makes diagnostics
385
+ // useful while structurally excluding every query value and fragment,
386
+ // including attacker-chosen keys the provider did not declare sensitive.
387
+ const redactedQuery = parsed.search ? "?[REDACTED]" : "";
388
+ return parsed.origin === "null"
389
+ ? `${parsed.protocol}<opaque-target>`
390
+ : `${parsed.origin}${parsed.pathname}${redactedQuery}`;
391
+ }
392
+ catch {
393
+ return MALFORMED_REDIRECT_TARGET;
394
+ }
395
+ }
396
+ function discardRedirectResponseBody(response) {
397
+ try {
398
+ const cancellation = response.body?.cancel();
399
+ if (cancellation)
400
+ void cancellation.catch(() => undefined);
401
+ }
402
+ catch {
403
+ // The redirect decision is security-significant and must not be replaced
404
+ // or delayed by an upstream body's cancellation failure. Cancellation was
405
+ // attempted; redirect evaluation continues without awaiting its completion.
406
+ }
407
+ }
408
+ async function fetchWithHttpRedirectPolicy(requestUrl, requestInit, policy) {
409
+ if (!policy)
410
+ return fetch(requestUrl, requestInit);
411
+ const initialUrl = new URL(requestUrl);
412
+ if (initialUrl.protocol !== "http:" && initialUrl.protocol !== "https:") {
413
+ throw new TransportError("ctx.http redirectPolicy requires an HTTP(S) origin", {
414
+ code: "transport_invalid_url",
415
+ });
416
+ }
417
+ const initialOrigin = initialUrl.origin;
418
+ let currentUrl = requestUrl;
419
+ let method = normalizeHttpMethod(requestInit.method ?? "GET");
420
+ let body = requestInit.body;
421
+ let headers = requestInit.headers;
422
+ let followedHops = 0;
423
+ const visitedRequests = new Set([`${method} ${currentUrl}`]);
424
+ while (true) {
425
+ const response = await fetch(currentUrl, {
426
+ ...requestInit,
427
+ body,
428
+ headers,
429
+ method,
430
+ redirect: "manual",
431
+ });
432
+ if (!isRedirectStatus(response.status))
433
+ return response;
434
+ const location = response.headers.get("location");
435
+ discardRedirectResponseBody(response);
436
+ let nextUrlString;
437
+ try {
438
+ nextUrlString = resolveRedirectUrl(location || undefined, currentUrl);
439
+ }
440
+ catch {
441
+ const target = MALFORMED_REDIRECT_TARGET;
442
+ throw new HttpRedirectError(`Redirect response has malformed Location target ${target}`, {
443
+ reason: "missing_location",
444
+ target,
445
+ status: response.status,
446
+ });
447
+ }
448
+ const decision = evaluateRedirectHop({
449
+ status: response.status,
450
+ method,
451
+ nextUrl: nextUrlString,
452
+ shouldStop: nextUrlString ? new URL(nextUrlString).origin !== initialOrigin : false,
453
+ redirectCount: followedHops + 1,
454
+ maxHops: policy.maxHops,
455
+ visitedRequests,
456
+ });
457
+ if (decision.kind === "stop") {
458
+ const target = decision.nextUrl ? redirectDiagnosticTarget(decision.nextUrl) : undefined;
459
+ const message = (() => {
460
+ switch (decision.reason) {
461
+ case "stopped":
462
+ return `Redirect policy refused cross-origin target ${target}`;
463
+ case "max_hops":
464
+ return `Redirect policy reached maxHops before target ${target}`;
465
+ case "loop":
466
+ return `Redirect loop refused target ${target}`;
467
+ case "missing_location":
468
+ return `Redirect response from ${redirectDiagnosticTarget(currentUrl)} is missing Location`;
469
+ }
470
+ })();
471
+ throw new HttpRedirectError(message, {
472
+ reason: decision.reason,
473
+ ...(target ? { target } : {}),
474
+ status: response.status,
475
+ });
476
+ }
477
+ if (decision.nextMethod !== method) {
478
+ body = undefined;
479
+ headers = withoutRedirectBodyHeaders(headers);
480
+ }
481
+ method = decision.nextMethod;
482
+ currentUrl = decision.nextUrl;
483
+ followedHops += 1;
484
+ visitedRequests.add(`${method} ${currentUrl}`);
485
+ }
486
+ }
167
487
  async function resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset = 0) {
168
488
  const resolvedProxy = await resolveProxyConfigAsync({
169
489
  proxy: options.proxy ?? clientOptions.proxy,
170
490
  upstream: clientOptions.upstream,
171
- apifuseConfig: clientOptions.apifuseConfig,
172
491
  proxyPolicy: clientOptions.proxyPolicy,
173
492
  affinityKey: clientOptions.affinityKey,
174
493
  proxyAttempt: computeProxyAttemptIndex({
175
494
  baseProxyAttempt: clientOptions.proxyAttempt,
176
495
  retryAttemptOffset: proxyAttemptOffset,
177
496
  }),
497
+ // Bun's native fetch proxy option tunnels HTTP CONNECT only; SOCKS5 is not
498
+ // supported here, so a socks5 policy fails loudly rather than downgrading.
499
+ transportProtocols: ["http"],
178
500
  telemetry: clientOptions.telemetry,
179
501
  });
180
502
  if (resolvedProxy.shouldWarn) {
@@ -196,27 +518,50 @@ function normalizeNativeFetchBody(body) {
196
518
  copied.set(normalized);
197
519
  return copied.buffer;
198
520
  }
199
- async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0) {
200
- const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
521
+ function serializeHttpRequestUrl(baseUrl, url, options) {
522
+ try {
523
+ return serializeRequestUrl(resolveHttpUrl(baseUrl, url), options.params, options.sensitiveParams);
524
+ }
525
+ catch (error) {
526
+ throw redactSensitiveRequestError(error, url, options.sensitiveParams);
527
+ }
528
+ }
529
+ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0, dedupe) {
530
+ const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
531
+ const { requestUrl } = serializedUrl;
201
532
  const controller = options.timeout ? new AbortController() : undefined;
533
+ const signal = mergeAbortSignals(clientOptions.signal, controller?.signal);
202
534
  const timeoutHandle = options.timeout
203
535
  ? setTimeout(() => controller?.abort(), options.timeout)
204
536
  : undefined;
205
537
  let proxy;
206
538
  try {
539
+ throwIfAmbientAborted(clientOptions.signal);
540
+ // Resolve inside the try (and after the timeout is armed) so allocator
541
+ // failures are branded as TransportErrors and count against the request
542
+ // deadline, exactly as an inline resolve would.
207
543
  proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
544
+ throwIfAmbientAborted(clientOptions.signal);
545
+ // For a registry allocator chain, skip an endpoint a prior attempt already
546
+ // tried rather than re-issuing the same request. Returning the sentinel
547
+ // (instead of breaking) lets the loop keep advancing the flat offset until
548
+ // it crosses into the fallback vendor's pool span.
549
+ if (dedupe && proxy) {
550
+ if (dedupe.attempted.has(proxy)) {
551
+ return { kind: "dedupe-skip" };
552
+ }
553
+ dedupe.attempted.add(proxy);
554
+ }
208
555
  const requestInit = {
209
556
  headers: options.headers,
210
557
  method,
211
558
  ...(proxy ? { proxy } : {}),
212
- signal: controller?.signal,
559
+ ...(signal ? { signal } : {}),
213
560
  };
214
561
  if (options.body !== undefined) {
215
562
  requestInit.body = normalizeNativeFetchBody(options.body);
216
563
  }
217
- const response = await fetch(requestUrl, {
218
- ...requestInit,
219
- });
564
+ const response = await fetchWithHttpRedirectPolicy(requestUrl, requestInit, options.redirectPolicy);
220
565
  const headers = Object.fromEntries(response.headers.entries());
221
566
  if (statusRetryCodes && response.status >= 400) {
222
567
  await drainNativeResponseBody(response);
@@ -239,9 +584,9 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
239
584
  }
240
585
  catch (error) {
241
586
  if (error instanceof SyntaxError) {
242
- throw error;
587
+ throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
243
588
  }
244
- const transportError = toHttpTransportError(error);
589
+ const transportError = redactSensitiveError(toHttpTransportError(error, clientOptions.signal, controller?.signal), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
245
590
  transportError.proxyUsed = Boolean(proxy);
246
591
  throw transportError;
247
592
  }
@@ -251,25 +596,27 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
251
596
  }
252
597
  }
253
598
  async function fetchNativeHttpStream(baseUrl, url, method, options, clientOptions, warn) {
254
- const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
599
+ const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
600
+ const { requestUrl } = serializedUrl;
255
601
  const controller = options.timeout ? new AbortController() : undefined;
602
+ const signal = mergeAbortSignals(clientOptions.signal, controller?.signal);
256
603
  const timeoutHandle = options.timeout
257
604
  ? setTimeout(() => controller?.abort(), options.timeout)
258
605
  : undefined;
259
606
  try {
607
+ throwIfAmbientAborted(clientOptions.signal);
260
608
  const proxy = await resolveNativeProxy(options, clientOptions, warn);
609
+ throwIfAmbientAborted(clientOptions.signal);
261
610
  const requestInit = {
262
611
  headers: options.headers,
263
612
  method,
264
613
  ...(proxy ? { proxy } : {}),
265
- signal: controller?.signal,
614
+ ...(signal ? { signal } : {}),
266
615
  };
267
616
  if (options.body !== undefined) {
268
617
  requestInit.body = normalizeNativeFetchBody(options.body);
269
618
  }
270
- const response = await fetch(requestUrl, {
271
- ...requestInit,
272
- });
619
+ const response = await fetchWithHttpRedirectPolicy(requestUrl, requestInit, options.redirectPolicy);
273
620
  if (response.status >= 400 && options.throwOnHttpError !== false) {
274
621
  await drainNativeResponseBody(response);
275
622
  throw new TransportError(`Upstream request failed with status ${response.status}`, {
@@ -277,13 +624,15 @@ async function fetchNativeHttpStream(baseUrl, url, method, options, clientOption
277
624
  status: response.status,
278
625
  });
279
626
  }
280
- return toNativeHttpStreamResponse(response);
627
+ // Per-call timeout remains header-scoped, while the ambient request signal
628
+ // stays attached to the response body for its full consumption lifetime.
629
+ return toNativeHttpStreamResponse(response, serializedUrl, clientOptions.signal);
281
630
  }
282
631
  catch (error) {
283
632
  if (error instanceof SyntaxError) {
284
- throw error;
633
+ throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
285
634
  }
286
- throw toHttpTransportError(error);
635
+ throw redactSensitiveError(toHttpTransportError(error, clientOptions.signal, controller?.signal), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
287
636
  }
288
637
  finally {
289
638
  if (timeoutHandle)
@@ -301,19 +650,31 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
301
650
  warn(message);
302
651
  };
303
652
  async function request(url, method, options = {}) {
304
- if (!baseUrl && !isAbsoluteUrl(url)) {
305
- throw new TransportError("ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared", { code: "transport_invalid_url" });
306
- }
307
- assertNoHttpTransportOverrides(options);
308
- const headersOptions = withClientHeaders(options, clientOptions, options.body);
309
- const methodName = normalizeHttpMethod(method);
310
- const explicitRetry = headersOptions.retry !== undefined;
311
- const retryOptions = normalizeProxyTransportRetryOptions(headersOptions.retry, {
312
- label: "HTTP",
313
- }) ??
314
- (explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
315
- if (retryOptions)
316
- validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
653
+ const { explicitRetry, headersOptions, methodName, retryOptions } = (() => {
654
+ try {
655
+ if (!baseUrl && !isAbsoluteUrl(url)) {
656
+ throw new TransportError("ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared", { code: "transport_invalid_url" });
657
+ }
658
+ assertNoHttpTransportOverrides(options);
659
+ const redirectPolicy = snapshotHttpRedirectPolicy(options);
660
+ const headersOptions = {
661
+ ...withClientHeaders(options, clientOptions, options.body),
662
+ redirectPolicy,
663
+ };
664
+ const methodName = normalizeHttpMethod(method);
665
+ const explicitRetry = headersOptions.retry !== undefined;
666
+ const retryOptions = normalizeProxyTransportRetryOptions(headersOptions.retry, {
667
+ label: "HTTP",
668
+ }) ??
669
+ (explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
670
+ if (retryOptions)
671
+ validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
672
+ return { explicitRetry, headersOptions, methodName, retryOptions };
673
+ }
674
+ catch (error) {
675
+ throw redactSensitiveRequestError(error, url, options.sensitiveParams);
676
+ }
677
+ })();
317
678
  const retryEnabled = Boolean(retryOptions &&
318
679
  retryOptions.attempts > 1 &&
319
680
  isProxyTransportRetryMethod(methodName, retryOptions));
@@ -325,23 +686,100 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
325
686
  const attemptOptions = statusRetryEnabled
326
687
  ? { ...headersOptions, throwOnHttpError: false }
327
688
  : headersOptions;
328
- const executeOnce = (proxyAttemptOffset = 0) => fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined, proxyAttemptOffset);
689
+ // Span the whole vendor chain on transport failures. Like ctx.stealth, a
690
+ // policy-managed proxy resolves a *different* endpoint/vendor per attempt
691
+ // (the flat proxyAttemptOffset rotates across the concatenated vendor pool
692
+ // spans), so a transport failure should advance to the next endpoint —
693
+ // potentially crossing into the fallback vendor — rather than stopping at
694
+ // the per-endpoint retry budget and stranding the request on the primary
695
+ // vendor. resolvePolicyTransportAttemptCap widens the cap to the chain span
696
+ // only for implicit, safe-method allocator requests; explicit retry
697
+ // policies (their documented `attempts` ceiling), unsafe methods, and
698
+ // static/non-registry vendors keep the retry budget. Status-code retries
699
+ // stay bounded by the retry budget regardless; only transport rotation gets
700
+ // the full span.
701
+ const policyProxy = (() => {
702
+ const policy = clientOptions.proxyPolicy ?? clientOptions.upstream?.proxy;
703
+ return policy && typeof policy === "object" ? policy : undefined;
704
+ })();
705
+ const usesPolicyAllocator = Boolean(policyProxy) && !options.proxy && !clientOptions.proxy;
706
+ const transportAttemptCap = retryOptions
707
+ ? resolvePolicyTransportAttemptCap({
708
+ policy: policyProxy,
709
+ usesPolicyAllocator,
710
+ retryAttempts: retryOptions.attempts,
711
+ explicitRetry,
712
+ method: methodName,
713
+ })
714
+ : 1;
715
+ // Track resolved endpoints across a policy-allocator chain. Successive
716
+ // attempts rotate the flat offset across the concatenated vendor pool
717
+ // spans, but an under-filled allocation (fewer live endpoints than the
718
+ // configured pool size) makes the modulo mapping repeat endpoints before
719
+ // the offset reaches the next vendor. Rather than re-hammering an
720
+ // already-tried endpoint under backoff, fetchNativeHttp returns a skip
721
+ // sentinel for a duplicate; the loop then advances the flat offset without
722
+ // issuing the request, so it keeps walking toward — and into — the fallback
723
+ // vendor's pool span instead of stalling on the primary vendor.
724
+ // De-duplication is gated on the SAME predicate that widens the attempt cap
725
+ // (implicit, safe-method, registry-chain rotation). It must NOT engage for
726
+ // an explicit retry policy: there the caller's `attempts` count is the
727
+ // contract and each attempt must issue against whatever endpoint it resolves
728
+ // — even a repeat — instead of being silently skipped (which would collapse
729
+ // a `poolSize: 1` + `attempts: 3` request to a single fetch).
730
+ const dedupeAllocatorEndpoints = policyRotatesTransportVendorChain({
731
+ policy: policyProxy,
732
+ usesPolicyAllocator,
733
+ explicitRetry,
734
+ method: methodName,
735
+ });
736
+ const dedupeContext = dedupeAllocatorEndpoints ? { attempted: new Set() } : undefined;
737
+ const executeOnce = (proxyAttemptOffset = 0) => fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined, proxyAttemptOffset, dedupeContext);
329
738
  if (!retryEnabled || !retryOptions) {
739
+ throwIfAmbientAborted(clientOptions.signal);
330
740
  const outcome = await executeOnce();
741
+ if (isDedupeSkipOutcome(outcome)) {
742
+ // Single-shot path never de-duplicates (dedupeContext is undefined),
743
+ // but keep the union total.
744
+ throw new TransportError("HTTP request produced no terminal result", {
745
+ code: "retry_exhausted",
746
+ });
747
+ }
331
748
  if (isHttpStatusOutcome(outcome)) {
332
749
  throw toUpstreamHttpError(outcome.status);
333
750
  }
334
751
  return outcome;
335
752
  }
753
+ let lastError;
336
754
  let lastErrorCode;
337
755
  let lastStatus;
338
- for (let attempt = 1; attempt <= retryOptions.attempts; attempt += 1) {
756
+ // `attempt` walks the flat proxy offset across the full chain span; `issued`
757
+ // counts requests that were actually sent (skipped duplicate offsets do not
758
+ // increment it). Retry summaries and the status-retry budget must reflect
759
+ // issued requests, not the raw offset, so they stay accurate when partial
760
+ // allocations skip offsets.
761
+ let issued = 0;
762
+ for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
763
+ throwIfAmbientAborted(clientOptions.signal);
764
+ // Whether this offset actually issued a request (vs. a skipped duplicate),
765
+ // so the catch counts a thrown *transport* failure once without
766
+ // double-counting a status outcome that already incremented before it
767
+ // re-threw as an upstream HTTP error.
768
+ let issuedThisAttempt = false;
339
769
  try {
340
770
  const outcome = await executeOnce(attempt - 1);
771
+ if (isDedupeSkipOutcome(outcome)) {
772
+ // Duplicate endpoint from a partial allocation: advance the flat
773
+ // offset without issuing the request (no backoff, not a failure) so
774
+ // the loop keeps rotating toward the fallback vendor.
775
+ continue;
776
+ }
777
+ issued += 1;
778
+ issuedThisAttempt = true;
341
779
  if (isHttpStatusOutcome(outcome)) {
342
780
  lastStatus = outcome.status;
343
- if (outcome.retryable && attempt < retryOptions.attempts) {
344
- await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers));
781
+ if (outcome.retryable && issued < retryOptions.attempts) {
782
+ await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers), clientOptions.signal);
345
783
  continue;
346
784
  }
347
785
  throw toUpstreamHttpError(outcome.status);
@@ -350,10 +788,10 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
350
788
  if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
351
789
  throw toUpstreamHttpError(response.status);
352
790
  }
353
- if (attempt > 1) {
791
+ if (issued > 1) {
354
792
  const summary = {
355
- attempts: attempt,
356
- retries: attempt - 1,
793
+ attempts: issued,
794
+ retries: issued - 1,
357
795
  ...(retryOptions.preset ? { preset: retryOptions.preset } : {}),
358
796
  transport: "native",
359
797
  ...(lastErrorCode ? { lastErrorCode } : {}),
@@ -364,10 +802,14 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
364
802
  return response;
365
803
  }
366
804
  catch (error) {
805
+ throwIfAmbientAborted(clientOptions.signal);
806
+ if (!issuedThisAttempt)
807
+ issued += 1;
808
+ lastError = error;
367
809
  lastErrorCode = proxyTransportRetryErrorCode(error);
368
810
  lastStatus = proxyTransportRetryErrorStatus(error);
369
811
  const proxyUsed = Boolean(error.proxyUsed);
370
- if (attempt < retryOptions.attempts &&
812
+ if (attempt < transportAttemptCap &&
371
813
  shouldRetryProxyTransportAttempt({
372
814
  error,
373
815
  explicitRetry,
@@ -375,23 +817,43 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
375
817
  options: retryOptions,
376
818
  proxyUsed,
377
819
  })) {
378
- await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt));
820
+ await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt), clientOptions.signal);
379
821
  continue;
380
822
  }
381
823
  throw error;
382
824
  }
383
825
  }
826
+ // Reached when the attempt cap is consumed without a terminal outcome —
827
+ // e.g. the final offsets of a partial allocation all resolved to
828
+ // already-tried endpoints and were skipped. Surface the last real transport
829
+ // failure rather than a synthetic exhaustion error.
830
+ if (lastError !== undefined) {
831
+ throw lastError;
832
+ }
384
833
  throw new TransportError("HTTP retry exhausted without a terminal result", {
385
834
  code: "retry_exhausted",
386
835
  });
387
836
  }
388
837
  async function streamRequest(url, method, options = {}) {
389
- if (!baseUrl && !isAbsoluteUrl(url)) {
390
- throw new TransportError("ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared", { code: "transport_invalid_url" });
391
- }
392
- assertNoHttpTransportOverrides(options);
393
- const headersOptions = withClientHeaders(options, clientOptions, options.body);
394
- const methodName = normalizeHttpMethod(method);
838
+ const { headersOptions, methodName } = (() => {
839
+ try {
840
+ if (!baseUrl && !isAbsoluteUrl(url)) {
841
+ throw new TransportError("ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared", { code: "transport_invalid_url" });
842
+ }
843
+ assertNoHttpTransportOverrides(options);
844
+ const redirectPolicy = snapshotHttpRedirectPolicy(options);
845
+ return {
846
+ headersOptions: {
847
+ ...withClientHeaders(options, clientOptions, options.body),
848
+ redirectPolicy,
849
+ },
850
+ methodName: normalizeHttpMethod(method),
851
+ };
852
+ }
853
+ catch (error) {
854
+ throw redactSensitiveRequestError(error, url, options.sensitiveParams);
855
+ }
856
+ })();
395
857
  return fetchNativeHttpStream(baseUrl, url, methodName, headersOptions, clientOptions, warnOnce);
396
858
  }
397
859
  return {