@apifuse/provider-sdk 2.2.0-beta.7 → 2.2.0-beta.70

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 (478) hide show
  1. package/AUTHORING.md +943 -51
  2. package/CHANGELOG.md +273 -1
  3. package/LICENSE +21 -0
  4. package/README.md +117 -29
  5. package/SUBMISSION.md +3 -2
  6. package/bin/apifuse-check.ts +293 -13
  7. package/bin/apifuse-dev.ts +42 -19
  8. package/bin/apifuse-migrate-deployment.ts +200 -0
  9. package/bin/apifuse-migrate-operation-declaration.ts +55 -0
  10. package/bin/apifuse-migrate-shape.ts +202 -0
  11. package/bin/apifuse-pack-check.ts +19 -6
  12. package/bin/apifuse-pack-smoke.ts +57 -2
  13. package/bin/apifuse-pack-types.ts +736 -38
  14. package/bin/apifuse-perf.ts +59 -23
  15. package/bin/apifuse-record.ts +923 -94
  16. package/bin/apifuse-submit-check.ts +2313 -281
  17. package/bin/apifuse-test.ts +3 -1
  18. package/bin/submit-check-delimited-text.ts +50 -0
  19. package/dist/auth-turn/index.d.ts +3 -3
  20. package/dist/auth-turn/index.js +1 -1
  21. package/dist/auth.d.ts +14 -0
  22. package/dist/auth.js +67 -0
  23. package/dist/ceremonies/index.d.ts +16 -0
  24. package/dist/ceremonies/index.js +141 -36
  25. package/dist/cli/commands.d.ts +1 -1
  26. package/dist/cli/commands.js +30 -0
  27. package/dist/cli/create.d.ts +1 -1
  28. package/dist/cli/create.js +8 -2
  29. package/dist/cli/deployment-intent.d.ts +178 -0
  30. package/dist/cli/deployment-intent.js +277 -0
  31. package/dist/cli/local-connection.d.ts +111 -0
  32. package/dist/cli/local-connection.js +151 -0
  33. package/dist/cli/migrate-deployment-intent.d.ts +132 -0
  34. package/dist/cli/migrate-deployment-intent.js +1552 -0
  35. package/dist/cli/migrate-operation-declaration.d.ts +70 -0
  36. package/dist/cli/migrate-operation-declaration.js +2206 -0
  37. package/dist/cli/migrate-operation-shape.d.ts +44 -0
  38. package/dist/cli/migrate-operation-shape.js +113 -0
  39. package/dist/cli/migrate-provider-shape.d.ts +56 -0
  40. package/dist/cli/migrate-provider-shape.js +580 -0
  41. package/dist/cli/resolver-runtime.d.ts +21 -0
  42. package/dist/cli/resolver-runtime.js +31 -0
  43. package/dist/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +141 -23
  44. package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
  45. package/dist/cli/templates/provider/README.md.tpl +41 -19
  46. package/dist/cli/templates/provider/index.ts.tpl +6 -3
  47. package/dist/cli/templates/provider/operations/ping.ts.tpl +4 -1
  48. package/dist/cli/templates/provider/provider.json.tpl +6 -0
  49. package/dist/config/loader.d.ts +202 -16
  50. package/dist/config/loader.js +491 -141
  51. package/dist/contract-serialization.js +4 -8
  52. package/dist/contract-types.d.ts +12 -5
  53. package/dist/contract.js +24 -10
  54. package/dist/declaration-validation.d.ts +34 -0
  55. package/dist/declaration-validation.js +305 -0
  56. package/dist/define.d.ts +71 -29
  57. package/dist/define.js +807 -69
  58. package/dist/dev.d.ts +5 -0
  59. package/dist/dev.js +8 -1
  60. package/dist/engine.d.ts +129 -0
  61. package/dist/engine.js +209 -0
  62. package/dist/error-observability.d.ts +7 -0
  63. package/dist/error-observability.js +61 -0
  64. package/dist/error-resolution.d.ts +18 -0
  65. package/dist/error-resolution.js +243 -0
  66. package/dist/errors.d.ts +74 -0
  67. package/dist/errors.js +99 -0
  68. package/dist/fixture-sanitization.d.ts +28 -0
  69. package/dist/fixture-sanitization.js +371 -0
  70. package/dist/handle-meta.d.ts +90 -0
  71. package/dist/handle-meta.js +73 -0
  72. package/dist/handle.d.ts +226 -0
  73. package/dist/handle.js +345 -0
  74. package/dist/health-scenario.d.ts +1901 -0
  75. package/dist/health-scenario.js +653 -0
  76. package/dist/i18n/catalog.js +21 -1
  77. package/dist/i18n/error-messages.d.ts +86 -0
  78. package/dist/i18n/error-messages.js +192 -0
  79. package/dist/i18n/index.d.ts +1 -0
  80. package/dist/i18n/index.js +1 -0
  81. package/dist/i18n/keys.d.ts +12 -0
  82. package/dist/i18n/keys.js +76 -5
  83. package/dist/i18n/operation-locale-namespace.d.ts +2 -0
  84. package/dist/i18n/operation-locale-namespace.js +7 -0
  85. package/dist/index.d.ts +40 -15
  86. package/dist/index.js +29 -11
  87. package/dist/lint.d.ts +43 -11
  88. package/dist/lint.js +2512 -177
  89. package/dist/native-address.d.ts +43 -0
  90. package/dist/native-address.js +281 -0
  91. package/dist/native-egress-policy.d.ts +31 -0
  92. package/dist/native-egress-policy.js +288 -0
  93. package/dist/observability.d.ts +5 -2
  94. package/dist/observability.js +48 -1
  95. package/dist/provider.d.ts +18 -8
  96. package/dist/provider.js +11 -6
  97. package/dist/runtime/auth-flow.d.ts +6 -1
  98. package/dist/runtime/auth-flow.js +8 -0
  99. package/dist/runtime/bounded-expiring-map.d.ts +21 -0
  100. package/dist/runtime/bounded-expiring-map.js +64 -0
  101. package/dist/runtime/browser-telemetry-binding.d.ts +5 -0
  102. package/dist/runtime/browser-telemetry-binding.js +83 -0
  103. package/dist/runtime/browser-telemetry.d.ts +76 -0
  104. package/dist/runtime/browser-telemetry.js +165 -0
  105. package/dist/runtime/browser.d.ts +4 -0
  106. package/dist/runtime/browser.js +688 -83
  107. package/dist/runtime/cache-telemetry.d.ts +71 -0
  108. package/dist/runtime/cache-telemetry.js +177 -0
  109. package/dist/runtime/cache.d.ts +3 -0
  110. package/dist/runtime/cache.js +245 -23
  111. package/dist/runtime/capability-response.d.ts +3 -0
  112. package/dist/runtime/capability-response.js +56 -0
  113. package/dist/runtime/capability-telemetry.d.ts +46 -0
  114. package/dist/runtime/capability-telemetry.js +68 -0
  115. package/dist/runtime/chrome149-header-order.d.ts +58 -0
  116. package/dist/runtime/chrome149-header-order.js +289 -0
  117. package/dist/runtime/diagnostic-env-values.d.ts +5 -0
  118. package/dist/runtime/diagnostic-env-values.js +57 -0
  119. package/dist/runtime/diagnostic-env.d.ts +14 -0
  120. package/dist/runtime/diagnostic-env.js +44 -0
  121. package/dist/runtime/diagnostic-redactor.d.ts +54 -0
  122. package/dist/runtime/diagnostic-redactor.js +571 -0
  123. package/dist/runtime/egress-lease.d.ts +65 -0
  124. package/dist/runtime/egress-lease.js +236 -0
  125. package/dist/runtime/engine-mode.d.ts +74 -0
  126. package/dist/runtime/engine-mode.js +152 -0
  127. package/dist/runtime/env.js +14 -1
  128. package/dist/runtime/executor.d.ts +3 -2
  129. package/dist/runtime/executor.js +23 -26
  130. package/dist/runtime/handle-wordlist.d.ts +43 -0
  131. package/dist/runtime/handle-wordlist.js +256 -0
  132. package/dist/runtime/handle.d.ts +71 -0
  133. package/dist/runtime/handle.js +908 -0
  134. package/dist/runtime/http-telemetry-binding.d.ts +10 -0
  135. package/dist/runtime/http-telemetry-binding.js +152 -0
  136. package/dist/runtime/http-telemetry-guard.d.ts +16 -0
  137. package/dist/runtime/http-telemetry-guard.js +202 -0
  138. package/dist/runtime/http-telemetry.d.ts +104 -0
  139. package/dist/runtime/http-telemetry.js +190 -0
  140. package/dist/runtime/http.d.ts +34 -0
  141. package/dist/runtime/http.js +680 -121
  142. package/dist/runtime/insights.js +2 -2
  143. package/dist/runtime/instrumentation.d.ts +2 -2
  144. package/dist/runtime/instrumentation.js +462 -19
  145. package/dist/runtime/native-network-errors.d.ts +33 -0
  146. package/dist/runtime/native-network-errors.js +69 -0
  147. package/dist/runtime/native-network.d.ts +105 -0
  148. package/dist/runtime/native-network.js +1433 -0
  149. package/dist/runtime/native-telemetry.d.ts +142 -0
  150. package/dist/runtime/native-telemetry.js +231 -0
  151. package/dist/runtime/ocr-telemetry.d.ts +82 -0
  152. package/dist/runtime/ocr-telemetry.js +192 -0
  153. package/dist/runtime/ocr.d.ts +29 -0
  154. package/dist/runtime/ocr.js +468 -0
  155. package/dist/runtime/otlp.d.ts +71 -2
  156. package/dist/runtime/otlp.js +398 -16
  157. package/dist/runtime/provider.d.ts +7 -0
  158. package/dist/runtime/provider.js +14 -0
  159. package/dist/runtime/proxy-errors.js +6 -2
  160. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  161. package/dist/runtime/proxy-nodemaven.js +147 -0
  162. package/dist/runtime/proxy-retry-policy.d.ts +4 -1
  163. package/dist/runtime/proxy-retry-policy.js +11 -2
  164. package/dist/runtime/proxy-telemetry.d.ts +133 -3
  165. package/dist/runtime/proxy-telemetry.js +218 -70
  166. package/dist/runtime/redirect-hop-headers.d.ts +19 -0
  167. package/dist/runtime/redirect-hop-headers.js +58 -0
  168. package/dist/runtime/redirects.d.ts +61 -0
  169. package/dist/runtime/redirects.js +149 -0
  170. package/dist/runtime/redis.d.ts +1 -1
  171. package/dist/runtime/redis.js +4 -2
  172. package/dist/runtime/request-options.d.ts +75 -1
  173. package/dist/runtime/request-options.js +588 -0
  174. package/dist/runtime/request-telemetry.d.ts +70 -0
  175. package/dist/runtime/request-telemetry.js +229 -0
  176. package/dist/runtime/resolver-config.d.ts +13 -0
  177. package/dist/runtime/resolver-config.js +14 -0
  178. package/dist/runtime/resolver-public.d.ts +3 -0
  179. package/dist/runtime/resolver-public.js +1 -0
  180. package/dist/runtime/resolver-runtime-options.d.ts +14 -0
  181. package/dist/runtime/resolver-runtime-options.js +33 -0
  182. package/dist/runtime/resolver-shared.d.ts +24 -0
  183. package/dist/runtime/resolver-shared.js +69 -0
  184. package/dist/runtime/resolver-span-attributes.d.ts +34 -0
  185. package/dist/runtime/resolver-span-attributes.js +93 -0
  186. package/dist/runtime/resolver-telemetry.d.ts +135 -0
  187. package/dist/runtime/resolver-telemetry.js +204 -0
  188. package/dist/runtime/resolver-usage.d.ts +32 -0
  189. package/dist/runtime/resolver-usage.js +54 -0
  190. package/dist/runtime/resolver-vendors/bindings.d.ts +53 -0
  191. package/dist/runtime/resolver-vendors/bindings.js +45 -0
  192. package/dist/runtime/resolver-vendors/browser.d.ts +24 -0
  193. package/dist/runtime/resolver-vendors/browser.js +423 -0
  194. package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
  195. package/dist/runtime/resolver-vendors/capsolver.js +544 -0
  196. package/dist/runtime/resolver-vendors/hosts.d.ts +23 -0
  197. package/dist/runtime/resolver-vendors/hosts.js +68 -0
  198. package/dist/runtime/resolver-vendors/hypersolutions.d.ts +27 -0
  199. package/dist/runtime/resolver-vendors/hypersolutions.js +369 -0
  200. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
  201. package/dist/runtime/resolver-vendors/twocaptcha.js +425 -0
  202. package/dist/runtime/resolver-vendors/types.d.ts +125 -0
  203. package/dist/runtime/resolver-vendors/types.js +106 -0
  204. package/dist/runtime/resolver.d.ts +79 -0
  205. package/dist/runtime/resolver.js +1094 -0
  206. package/dist/runtime/state-telemetry.d.ts +79 -0
  207. package/dist/runtime/state-telemetry.js +194 -0
  208. package/dist/runtime/state.d.ts +10 -1
  209. package/dist/runtime/state.js +322 -82
  210. package/dist/runtime/stealth-akamai-sbsd.d.ts +54 -0
  211. package/dist/runtime/stealth-akamai-sbsd.js +142 -0
  212. package/dist/runtime/stealth-cookies.d.ts +24 -0
  213. package/dist/runtime/stealth-cookies.js +131 -0
  214. package/dist/runtime/stealth-owned-headers.d.ts +39 -0
  215. package/dist/runtime/stealth-owned-headers.js +57 -0
  216. package/dist/runtime/stealth-telemetry.d.ts +92 -0
  217. package/dist/runtime/stealth-telemetry.js +179 -0
  218. package/dist/runtime/stealth.d.ts +45 -6
  219. package/dist/runtime/stealth.js +1775 -320
  220. package/dist/runtime/stt-telemetry.d.ts +77 -0
  221. package/dist/runtime/stt-telemetry.js +176 -0
  222. package/dist/runtime/stt.js +18 -19
  223. package/dist/runtime/timeout.d.ts +5 -0
  224. package/dist/runtime/timeout.js +12 -0
  225. package/dist/runtime/trace-config.d.ts +12 -0
  226. package/dist/runtime/trace-config.js +62 -0
  227. package/dist/runtime/trace.d.ts +17 -0
  228. package/dist/runtime/trace.js +176 -25
  229. package/dist/runtime-boundary-lint.d.ts +137 -0
  230. package/dist/runtime-boundary-lint.js +590 -0
  231. package/dist/schema.d.ts +20 -0
  232. package/dist/schema.js +72 -2
  233. package/dist/serve.d.ts +1 -1
  234. package/dist/serve.js +1 -1
  235. package/dist/server/error-observability.d.ts +1 -0
  236. package/dist/server/error-observability.js +1 -0
  237. package/dist/server/error-stack-frames.d.ts +35 -0
  238. package/dist/server/error-stack-frames.js +219 -0
  239. package/dist/server/index.d.ts +21 -2
  240. package/dist/server/index.js +11 -2
  241. package/dist/server/runtime-diagnostics.d.ts +165 -0
  242. package/dist/server/runtime-diagnostics.js +360 -0
  243. package/dist/server/sdk-version.d.ts +3 -0
  244. package/dist/server/sdk-version.js +19 -0
  245. package/dist/server/self-test-input-tokens.d.ts +2 -1
  246. package/dist/server/self-test-input-tokens.js +18 -14
  247. package/dist/server/self-test-redaction.js +8 -33
  248. package/dist/server/self-test.d.ts +18 -3
  249. package/dist/server/self-test.js +139 -76
  250. package/dist/server/sensitive-values.d.ts +26 -0
  251. package/dist/server/sensitive-values.js +137 -0
  252. package/dist/server/serve-implementation.d.ts +285 -0
  253. package/dist/server/serve-implementation.js +3172 -0
  254. package/dist/server/serve.d.ts +1 -75
  255. package/dist/server/serve.js +1 -1170
  256. package/dist/server/state-backend-report.d.ts +70 -0
  257. package/dist/server/state-backend-report.js +113 -0
  258. package/dist/server/trace-output.d.ts +7 -0
  259. package/dist/server/trace-output.js +111 -0
  260. package/dist/server/types.d.ts +37 -5
  261. package/dist/server/types.js +34 -1
  262. package/dist/stateful/errors.d.ts +19 -0
  263. package/dist/stateful/errors.js +24 -0
  264. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  265. package/dist/stateful/http-provider-event-emitter.js +237 -0
  266. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  267. package/dist/stateful/http-session-owner-registry.js +210 -0
  268. package/dist/stateful/index.d.ts +18 -0
  269. package/dist/stateful/index.js +18 -0
  270. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  271. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  272. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  273. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  274. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  275. package/dist/stateful/provider-event-pipeline.js +1 -0
  276. package/dist/stateful/provider-events.d.ts +101 -0
  277. package/dist/stateful/provider-events.js +289 -0
  278. package/dist/stateful/session-key.d.ts +15 -0
  279. package/dist/stateful/session-key.js +86 -0
  280. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  281. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  282. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  283. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  284. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  285. package/dist/stateful/stateful-provider-adapter.js +287 -0
  286. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  287. package/dist/stateful/stateful-provider-observability.js +161 -0
  288. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  289. package/dist/stateful/stateful-provider-owner-forwarder.js +237 -0
  290. package/dist/stateful/stateful-provider-runtime-context.d.ts +35 -0
  291. package/dist/stateful/stateful-provider-runtime-context.js +61 -0
  292. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  293. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  294. package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
  295. package/dist/stateful/stateful-provider-session-routing.js +345 -0
  296. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  297. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  298. package/dist/stateful-signing.d.ts +18 -0
  299. package/dist/stateful-signing.js +27 -0
  300. package/dist/stealth/profiles.d.ts +15 -4
  301. package/dist/stealth/profiles.js +169 -211
  302. package/dist/stream-evidence.d.ts +74 -0
  303. package/dist/stream-evidence.js +785 -0
  304. package/dist/stream.js +7 -1
  305. package/dist/testing/index.d.ts +2 -1
  306. package/dist/testing/index.js +2 -1
  307. package/dist/testing/run.d.ts +32 -2
  308. package/dist/testing/run.js +509 -25
  309. package/dist/trace-sanitization.d.ts +6 -0
  310. package/dist/trace-sanitization.js +68 -0
  311. package/dist/types.d.ts +899 -210
  312. package/dist/types.js +1 -0
  313. package/dist/typescript-module.d.ts +39 -0
  314. package/dist/typescript-module.js +72 -0
  315. package/package.json +52 -6
  316. package/src/auth-turn/index.ts +1 -1
  317. package/src/auth.ts +118 -0
  318. package/src/ceremonies/index.ts +189 -46
  319. package/src/cli/commands.ts +37 -0
  320. package/src/cli/create.ts +8 -2
  321. package/src/cli/deployment-intent.ts +364 -0
  322. package/src/cli/local-connection.ts +221 -0
  323. package/src/cli/migrate-deployment-intent.ts +1908 -0
  324. package/src/cli/migrate-operation-declaration.ts +3026 -0
  325. package/src/cli/migrate-operation-shape.ts +184 -0
  326. package/src/cli/migrate-provider-shape.ts +778 -0
  327. package/src/cli/resolver-runtime.ts +72 -0
  328. package/src/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +141 -23
  329. package/src/cli/templates/provider/Dockerfile.tpl +1 -1
  330. package/src/cli/templates/provider/README.md.tpl +41 -19
  331. package/src/cli/templates/provider/index.ts.tpl +6 -3
  332. package/src/cli/templates/provider/operations/ping.ts.tpl +4 -1
  333. package/src/cli/templates/provider/provider.json.tpl +6 -0
  334. package/src/config/loader.ts +774 -181
  335. package/src/contract-serialization.ts +5 -7
  336. package/src/contract-types.ts +12 -5
  337. package/src/contract.ts +24 -10
  338. package/src/declaration-validation.ts +376 -0
  339. package/src/define.ts +1105 -131
  340. package/src/dev.ts +13 -1
  341. package/src/engine.ts +390 -0
  342. package/src/error-observability.ts +64 -0
  343. package/src/error-resolution.ts +269 -0
  344. package/src/errors.ts +157 -0
  345. package/src/fixture-sanitization.ts +422 -0
  346. package/src/handle-meta.ts +143 -0
  347. package/src/handle.ts +694 -0
  348. package/src/health-scenario.ts +960 -0
  349. package/src/i18n/catalog.ts +23 -1
  350. package/src/i18n/error-messages.ts +255 -0
  351. package/src/i18n/index.ts +1 -0
  352. package/src/i18n/keys.ts +80 -6
  353. package/src/i18n/operation-locale-namespace.ts +10 -0
  354. package/src/index.ts +390 -23
  355. package/src/lint.ts +2975 -234
  356. package/src/native-address.ts +340 -0
  357. package/src/native-egress-policy.ts +358 -0
  358. package/src/observability.ts +51 -1
  359. package/src/provider.ts +207 -27
  360. package/src/runtime/auth-flow.ts +15 -0
  361. package/src/runtime/bounded-expiring-map.ts +67 -0
  362. package/src/runtime/browser-telemetry-binding.ts +92 -0
  363. package/src/runtime/browser-telemetry.ts +289 -0
  364. package/src/runtime/browser.ts +882 -99
  365. package/src/runtime/cache-telemetry.ts +247 -0
  366. package/src/runtime/cache.ts +258 -22
  367. package/src/runtime/capability-response.ts +53 -0
  368. package/src/runtime/capability-telemetry.ts +101 -0
  369. package/src/runtime/chrome149-header-order.ts +330 -0
  370. package/src/runtime/diagnostic-env-values.ts +56 -0
  371. package/src/runtime/diagnostic-env.ts +65 -0
  372. package/src/runtime/diagnostic-redactor.ts +668 -0
  373. package/src/runtime/egress-lease.ts +341 -0
  374. package/src/runtime/engine-mode.ts +233 -0
  375. package/src/runtime/env.ts +15 -1
  376. package/src/runtime/executor.ts +39 -32
  377. package/src/runtime/handle-wordlist.ts +266 -0
  378. package/src/runtime/handle.ts +1180 -0
  379. package/src/runtime/http-telemetry-binding.ts +162 -0
  380. package/src/runtime/http-telemetry-guard.ts +208 -0
  381. package/src/runtime/http-telemetry.ts +292 -0
  382. package/src/runtime/http.ts +896 -150
  383. package/src/runtime/insights.ts +2 -2
  384. package/src/runtime/instrumentation.ts +628 -28
  385. package/src/runtime/native-network-errors.ts +99 -0
  386. package/src/runtime/native-network.ts +1853 -0
  387. package/src/runtime/native-telemetry.ts +366 -0
  388. package/src/runtime/ocr-telemetry.ts +308 -0
  389. package/src/runtime/ocr.ts +564 -0
  390. package/src/runtime/otlp.ts +468 -21
  391. package/src/runtime/provider.ts +15 -0
  392. package/src/runtime/proxy-errors.ts +12 -4
  393. package/src/runtime/proxy-nodemaven.ts +222 -0
  394. package/src/runtime/proxy-retry-policy.ts +12 -1
  395. package/src/runtime/proxy-telemetry.ts +358 -103
  396. package/src/runtime/redirect-hop-headers.ts +71 -0
  397. package/src/runtime/redirects.ts +215 -0
  398. package/src/runtime/redis.ts +7 -2
  399. package/src/runtime/request-options.ts +728 -1
  400. package/src/runtime/request-telemetry.ts +375 -0
  401. package/src/runtime/resolver-config.ts +18 -0
  402. package/src/runtime/resolver-public.ts +44 -0
  403. package/src/runtime/resolver-runtime-options.ts +50 -0
  404. package/src/runtime/resolver-shared.ts +113 -0
  405. package/src/runtime/resolver-span-attributes.ts +139 -0
  406. package/src/runtime/resolver-telemetry.ts +362 -0
  407. package/src/runtime/resolver-usage.ts +99 -0
  408. package/src/runtime/resolver-vendors/bindings.ts +61 -0
  409. package/src/runtime/resolver-vendors/browser.ts +591 -0
  410. package/src/runtime/resolver-vendors/capsolver.ts +727 -0
  411. package/src/runtime/resolver-vendors/hosts.ts +81 -0
  412. package/src/runtime/resolver-vendors/hypersolutions.ts +480 -0
  413. package/src/runtime/resolver-vendors/twocaptcha.ts +555 -0
  414. package/src/runtime/resolver-vendors/types.ts +261 -0
  415. package/src/runtime/resolver.ts +1543 -0
  416. package/src/runtime/state-telemetry.ts +298 -0
  417. package/src/runtime/state.ts +435 -89
  418. package/src/runtime/stealth-akamai-sbsd.ts +207 -0
  419. package/src/runtime/stealth-cookies.ts +155 -0
  420. package/src/runtime/stealth-owned-headers.ts +67 -0
  421. package/src/runtime/stealth-telemetry.ts +309 -0
  422. package/src/runtime/stealth.ts +2275 -372
  423. package/src/runtime/stt-telemetry.ts +282 -0
  424. package/src/runtime/stt.ts +96 -91
  425. package/src/runtime/timeout.ts +18 -0
  426. package/src/runtime/trace-config.ts +78 -0
  427. package/src/runtime/trace.ts +257 -35
  428. package/src/runtime-boundary-lint.ts +707 -0
  429. package/src/schema.ts +80 -2
  430. package/src/serve.ts +6 -1
  431. package/src/server/error-observability.ts +1 -0
  432. package/src/server/error-stack-frames.ts +214 -0
  433. package/src/server/index.ts +184 -2
  434. package/src/server/runtime-diagnostics.ts +522 -0
  435. package/src/server/sdk-version.ts +22 -0
  436. package/src/server/self-test-input-tokens.ts +29 -14
  437. package/src/server/self-test-redaction.ts +10 -40
  438. package/src/server/self-test.ts +205 -89
  439. package/src/server/sensitive-values.ts +211 -0
  440. package/src/server/serve-implementation.ts +4596 -0
  441. package/src/server/serve.ts +1 -1685
  442. package/src/server/state-backend-report.ts +185 -0
  443. package/src/server/trace-output.ts +189 -0
  444. package/src/server/types.ts +34 -1
  445. package/src/stateful/README.md +146 -0
  446. package/src/stateful/errors.ts +35 -0
  447. package/src/stateful/http-provider-event-emitter.ts +314 -0
  448. package/src/stateful/http-session-owner-registry.ts +306 -0
  449. package/src/stateful/index.ts +18 -0
  450. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  451. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  452. package/src/stateful/provider-event-pipeline.ts +61 -0
  453. package/src/stateful/provider-events.ts +462 -0
  454. package/src/stateful/session-key.ts +111 -0
  455. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  456. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  457. package/src/stateful/stateful-provider-adapter.ts +562 -0
  458. package/src/stateful/stateful-provider-observability.ts +261 -0
  459. package/src/stateful/stateful-provider-owner-forwarder.ts +310 -0
  460. package/src/stateful/stateful-provider-runtime-context.ts +96 -0
  461. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  462. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  463. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  464. package/src/stateful-signing.ts +46 -0
  465. package/src/stealth/profiles.ts +212 -231
  466. package/src/stream-evidence.ts +988 -0
  467. package/src/stream.ts +8 -1
  468. package/src/testing/index.ts +10 -1
  469. package/src/testing/run.ts +685 -19
  470. package/src/trace-sanitization.ts +110 -0
  471. package/src/types.ts +1014 -273
  472. package/src/typescript-module.ts +100 -0
  473. package/dist/choice-token.d.ts +0 -24
  474. package/dist/choice-token.js +0 -74
  475. package/dist/runtime/choice.d.ts +0 -15
  476. package/dist/runtime/choice.js +0 -413
  477. package/src/choice-token.ts +0 -165
  478. package/src/runtime/choice.ts +0 -686
@@ -1,13 +1,21 @@
1
1
  import type { ProxyResolutionOptions } from "../config/loader.js";
2
- import { resolveProxyConfigAsync } from "../config/loader.js";
2
+ import {
3
+ policyRotatesTransportVendorChain,
4
+ resolvePolicyTransportAttemptCap,
5
+ resolveProxyConfigAsync,
6
+ } from "../config/loader.js";
3
7
  import { ProviderError, TransportError } from "../errors.js";
4
8
  import { parseSseStream, readableBytes, readableLines, readableTextChunks } from "../stream.js";
5
9
  import type {
10
+ HttpAttemptContext,
6
11
  HttpClient,
12
+ HttpHeadersFactory,
7
13
  HttpMethod,
14
+ HttpRedirectPolicy,
8
15
  HttpResponse,
9
16
  HttpRetrySummary,
10
17
  HttpStreamResponse,
18
+ ProviderProxyPolicy,
11
19
  RequestOptions,
12
20
  RequestWithMethodOptions,
13
21
  SseMessage,
@@ -23,14 +31,91 @@ import {
23
31
  shouldRetryProxyTransportAttempt,
24
32
  validateUnsafeProxyTransportRetryMethods,
25
33
  } from "./proxy-retry-policy.js";
26
- import { appendQueryParams, normalizeHttpRequestBody } from "./request-options.js";
34
+ import {
35
+ httpTelemetryErrorCode,
36
+ type HttpTelemetrySink,
37
+ type HttpAttemptTelemetryEvent,
38
+ } from "./http-telemetry.js";
39
+ import {
40
+ invalidRedirectPolicy,
41
+ isRedirectStatus,
42
+ nextSameOriginRedirectHop,
43
+ normalizeRedirectPolicy,
44
+ type RedirectPolicyTransport,
45
+ } from "./redirects.js";
46
+ import {
47
+ normalizeHttpRequestBody,
48
+ redactSensitiveError,
49
+ redactSensitiveRequestError,
50
+ type SerializedRequestUrl,
51
+ serializeRequestUrl,
52
+ } from "./request-options.js";
53
+
54
+ import { startHttpTelemetry } from "./http-telemetry-guard.js";
55
+ import { scopedHttpTelemetry } from "./http-telemetry-binding.js";
27
56
 
28
57
  const DEFAULT_HTTP_BASE_URL = "http://localhost";
29
58
 
59
+ function httpAttemptFailure(
60
+ error: unknown,
61
+ status?: number,
62
+ ): Omit<HttpAttemptTelemetryEvent, "ms" | "proxyUsed"> {
63
+ try {
64
+ return {
65
+ e: httpTelemetryErrorCode(proxyTransportRetryErrorCode(error)),
66
+ status: proxyTransportRetryErrorStatus(error) ?? status,
67
+ ...(error instanceof Error
68
+ ? {
69
+ diagnostics: {
70
+ name: error.name,
71
+ message: error.message,
72
+ ...(error.cause instanceof Error
73
+ ? { cause: { name: error.cause.name, message: error.cause.message } }
74
+ : {}),
75
+ },
76
+ }
77
+ : {}),
78
+ };
79
+ } catch {
80
+ return { e: "other" };
81
+ }
82
+ }
83
+
30
84
  export type HttpClientOptions = ProxyResolutionOptions & {
31
85
  warn?: (message: string) => void;
32
86
  userAgent?: string;
33
87
  onRetrySummary?: (summary: HttpRetrySummary) => void;
88
+ /**
89
+ * Synchronous observer for HTTP attempts, retries, timeouts, status, duration,
90
+ * and observed proxy use. Observer failures never change transport behavior.
91
+ * All hooks and their property reads are guarded, including `onRetrySummary`
92
+ * and the failure reporter. Invalid asynchronous results are ignored and never
93
+ * awaited. Observer failures set the log-only `telemetryFailed` marker; the
94
+ * marker and redacted error diagnostics are excluded from headers and tenant
95
+ * metadata. Tenant `meta.retry` retains its existing response shape.
96
+ *
97
+ * After `ProviderEngine.attach`, the server wraps supported host HTTP bindings
98
+ * with the request collector. The adapter preserves arguments, direct-call,
99
+ * explicit `.call`/`.apply`, and unbound receivers, plus the original returned
100
+ * promise, response, stream, and error. Streams and SSE are observed at headers
101
+ * without consuming their bodies. SDK clients report every observed attempt
102
+ * and keep any observer configured here; routing is scoped to each call.
103
+ *
104
+ * Binding an existing adapter reuses its identity and replaces its collector:
105
+ * the latest bind receives subsequent calls, the earlier collector receives no
106
+ * further calls, and in-flight calls keep the collector captured at invocation.
107
+ * An opaque host invocation is one observed attempt with zero observed retries;
108
+ * its undisclosed retries and proxy settings cannot be inferred, and tenant
109
+ * `meta.retry` is omitted. Status, duration, and error class remain available
110
+ * in logs and telemetry headers.
111
+ *
112
+ * Non-object bindings and objects without a callable `request`, `get`, `post`,
113
+ * `put`, `delete`, `stream`, or `sse` method are left untouched, with no `http`
114
+ * telemetry sibling and one process-wide warning for this reason:
115
+ * `[apifuse] http telemetry not attached; reason=unsupported_binding_shape`.
116
+ */
117
+ httpTelemetry?: HttpTelemetrySink;
118
+ signal?: AbortSignal;
34
119
  };
35
120
 
36
121
  type HttpStatusOutcome = {
@@ -41,19 +126,67 @@ type HttpStatusOutcome = {
41
126
  proxyUsed: boolean;
42
127
  };
43
128
 
44
- type NativeHttpAttemptOutcome = HttpResponse | HttpStatusOutcome;
129
+ /**
130
+ * Sentinel returned when a policy-allocator attempt resolved an endpoint that a
131
+ * prior attempt already tried (an under-filled pool repeats endpoints via the
132
+ * modulo mapping before the flat offset crosses into the next vendor). The retry
133
+ * loop advances to the next offset rather than re-issuing the request — but does
134
+ * NOT treat it as chain exhaustion, so the offset still walks into the fallback
135
+ * vendor's span.
136
+ */
137
+ type NativeHttpSkipOutcome = { kind: "dedupe-skip" };
138
+
139
+ type NativeHttpAttemptOutcome = HttpResponse | HttpStatusOutcome | NativeHttpSkipOutcome;
45
140
 
46
141
  type NativeHttpAttemptError = TransportError & { proxyUsed?: boolean };
47
142
 
48
- function isHttpStatusOutcome(
49
- outcome: HttpResponse | HttpStatusOutcome,
50
- ): outcome is HttpStatusOutcome {
143
+ function isHttpStatusOutcome(outcome: NativeHttpAttemptOutcome): outcome is HttpStatusOutcome {
51
144
  return "kind" in outcome && outcome.kind === "http-status";
52
145
  }
53
146
 
54
- async function sleep(ms: number): Promise<void> {
147
+ function isDedupeSkipOutcome(outcome: NativeHttpAttemptOutcome): outcome is NativeHttpSkipOutcome {
148
+ return "kind" in outcome && outcome.kind === "dedupe-skip";
149
+ }
150
+
151
+ function toAmbientCancellationError(
152
+ signal: AbortSignal,
153
+ error: unknown = signal.reason,
154
+ ): TransportError {
155
+ if (error instanceof TransportError && error.code === "transport_cancelled") {
156
+ return error;
157
+ }
158
+ return new TransportError("Request cancelled", {
159
+ code: "transport_cancelled",
160
+ status: 0,
161
+ retryable: false,
162
+ ...(error !== undefined
163
+ ? { cause: error instanceof Error ? error : new Error(String(error)) }
164
+ : {}),
165
+ });
166
+ }
167
+
168
+ function throwIfAmbientAborted(signal: AbortSignal | undefined): void {
169
+ if (signal?.aborted) throw toAmbientCancellationError(signal);
170
+ }
171
+
172
+ async function sleep(ms: number, signal?: AbortSignal): Promise<void> {
173
+ throwIfAmbientAborted(signal);
55
174
  if (ms <= 0) return;
56
- await new Promise((resolve) => setTimeout(resolve, ms));
175
+ if (!signal) {
176
+ await new Promise((resolve) => setTimeout(resolve, ms));
177
+ return;
178
+ }
179
+ await new Promise<void>((resolve, reject) => {
180
+ const onAbort = () => {
181
+ clearTimeout(timer);
182
+ reject(toAmbientCancellationError(signal));
183
+ };
184
+ const timer = setTimeout(() => {
185
+ signal.removeEventListener("abort", onAbort);
186
+ resolve();
187
+ }, ms);
188
+ signal.addEventListener("abort", onAbort, { once: true });
189
+ });
57
190
  }
58
191
 
59
192
  function toUpstreamHttpError(status: number): TransportError {
@@ -69,26 +202,108 @@ function hasHeader(headers: Record<string, string>, name: string): boolean {
69
202
  return Object.keys(headers).some((key) => key.toLowerCase() === needle);
70
203
  }
71
204
 
72
- function withClientHeaders(
73
- options: RequestOptions | undefined,
205
+ function withClientHeaderDefaults(
206
+ headers: Record<string, string> | undefined,
74
207
  clientOptions: HttpClientOptions,
75
208
  body: unknown,
76
- ): RequestOptions {
77
- const headers: Record<string, string> = {
209
+ ): Record<string, string> {
210
+ const merged: Record<string, string> = {
78
211
  ...(clientOptions.userAgent ? { "User-Agent": clientOptions.userAgent } : {}),
79
- ...options?.headers,
212
+ ...headers,
80
213
  };
81
214
 
82
- if (body !== undefined && !hasHeader(headers, "Content-Type")) {
83
- headers["Content-Type"] = "application/json";
215
+ if (body !== undefined && !hasHeader(merged, "Content-Type")) {
216
+ merged["Content-Type"] = "application/json";
84
217
  }
85
218
 
219
+ return merged;
220
+ }
221
+
222
+ function withClientHeaders(
223
+ options: RequestOptions | undefined,
224
+ clientOptions: HttpClientOptions,
225
+ body: unknown,
226
+ ): RequestOptions {
227
+ // The ambient client signal is merged into the per-request one here so every
228
+ // downstream read (retry loop, sleep, body consumption) sees a single signal.
229
+ // The header-factory branch needs it too, otherwise factory-header requests
230
+ // would lose ambient cancellation entirely.
231
+ const signal = mergeAbortSignals(clientOptions.signal, options?.signal);
232
+ // A header factory is resolved per issued attempt (resolveAttemptHeaders);
233
+ // the client defaults are applied to its output at that point.
234
+ if (typeof options?.headers === "function") return { ...options, signal };
86
235
  return {
87
236
  ...options,
88
- headers,
237
+ headers: withClientHeaderDefaults(options?.headers, clientOptions, body),
238
+ signal,
89
239
  };
90
240
  }
91
241
 
242
+ function headerFactoryFailure(message: string, cause?: unknown): TransportError {
243
+ // A dedicated code keeps a broken proof minter out of the retry predicate:
244
+ // toHttpTransportError would otherwise brand a plain throw as
245
+ // transport_network_error, which the default retry policy re-issues.
246
+ // retryable:false is explicit so the served error envelope does not
247
+ // advertise a deterministic signing failure as retryable, and
248
+ // category:provider_error keeps a provider-local minting bug out of the
249
+ // upstream-failure signal (status 0 would otherwise fall through the
250
+ // observability ladder to upstream_http / source upstream_failure).
251
+ return new TransportError(message, {
252
+ code: "http_header_factory_failed",
253
+ status: 0,
254
+ retryable: false,
255
+ category: "provider_error",
256
+ ...(cause instanceof Error ? { cause } : {}),
257
+ });
258
+ }
259
+
260
+ function isHeaderRecord(value: unknown): value is Record<string, string> {
261
+ return value !== null && typeof value === "object" && !Array.isArray(value);
262
+ }
263
+
264
+ // Settle `promise`, or reject with the signal's reason as soon as it aborts, so
265
+ // a pending header factory cannot outlive the request deadline or an ambient
266
+ // cancellation (native fetch has not started yet, so nothing else observes them).
267
+ function settleBeforeAbort<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T> {
268
+ if (!signal) return promise;
269
+ return new Promise<T>((resolve, reject) => {
270
+ const onAbort = () => reject(signal.reason);
271
+ if (signal.aborted) onAbort();
272
+ else signal.addEventListener("abort", onAbort, { once: true });
273
+ // Always attach handlers: a late rejection after the abort must settle
274
+ // here (a no-op on the already-rejected promise), never go unhandled.
275
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
276
+ });
277
+ }
278
+
279
+ async function resolveAttemptHeaders(
280
+ options: RequestOptions & { body?: unknown },
281
+ clientOptions: HttpClientOptions,
282
+ attempt: () => HttpAttemptContext,
283
+ signal: AbortSignal | undefined,
284
+ ): Promise<Record<string, string> | undefined> {
285
+ const { headers } = options;
286
+ if (typeof headers !== "function") return headers;
287
+ // The per-call timeout may have fired during proxy resolution: do not start
288
+ // the factory at all once the request is already aborted.
289
+ if (signal?.aborted) throw signal.reason;
290
+ let minted: unknown;
291
+ try {
292
+ minted = await settleBeforeAbort(
293
+ Promise.resolve().then(() => headers(attempt())),
294
+ signal,
295
+ );
296
+ } catch (error) {
297
+ // An abort is classified by toHttpTransportError (cancelled / timed out).
298
+ if (signal?.aborted) throw error;
299
+ throw headerFactoryFailure("Request header factory failed", error);
300
+ }
301
+ if (!isHeaderRecord(minted)) {
302
+ throw headerFactoryFailure("Request header factory must return a header record");
303
+ }
304
+ return withClientHeaderDefaults(minted, clientOptions, options.body);
305
+ }
306
+
92
307
  function parseHttpData(body: string, headers: Record<string, string>): unknown {
93
308
  const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
94
309
 
@@ -107,7 +322,21 @@ function isTimeoutMessage(message: string): boolean {
107
322
  return /\b(timed out|timeout|deadline exceeded)\b/i.test(message);
108
323
  }
109
324
 
110
- function toHttpTransportError(error: unknown): TransportError {
325
+ function toHttpTransportError(
326
+ error: unknown,
327
+ ambientSignal?: AbortSignal,
328
+ timeoutSignal?: AbortSignal,
329
+ ): TransportError {
330
+ if (ambientSignal?.aborted) {
331
+ return toAmbientCancellationError(ambientSignal, error);
332
+ }
333
+ if (timeoutSignal?.aborted) {
334
+ return new TransportError("Request timed out", {
335
+ code: "transport_timeout",
336
+ status: 0,
337
+ ...(error instanceof Error ? { cause: error } : {}),
338
+ });
339
+ }
111
340
  if (error instanceof TransportError) {
112
341
  if (error.code) {
113
342
  return error;
@@ -191,9 +420,134 @@ function requireNativeResponseBody(response: Response): ReadableStream<Uint8Arra
191
420
  return response.body;
192
421
  }
193
422
 
194
- function toNativeHttpStreamResponse(response: Response): HttpStreamResponse {
423
+ function mergeAbortSignals(
424
+ ...signals: Array<AbortSignal | null | undefined>
425
+ ): AbortSignal | undefined {
426
+ const activeSignals = signals.filter((signal): signal is AbortSignal => signal != null);
427
+ if (activeSignals.length === 0) return undefined;
428
+ if (activeSignals.length === 1) return activeSignals[0];
429
+ return AbortSignal.any(activeSignals);
430
+ }
431
+
432
+ function cancelStreamOnAbort(
433
+ body: ReadableStream<Uint8Array>,
434
+ signal: AbortSignal | undefined,
435
+ ): ReadableStream<Uint8Array> {
436
+ if (!signal) return body;
437
+
438
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
439
+ let finished = false;
440
+ let streamController: ReadableStreamDefaultController<Uint8Array> | undefined;
441
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
442
+ const onAbort = () => {
443
+ if (finished) return;
444
+ finished = true;
445
+ cleanup();
446
+ const reason = toAmbientCancellationError(signal);
447
+ streamController?.error(reason);
448
+ const cancellation = reader ? reader.cancel(reason) : body.cancel(reason);
449
+ void cancellation.catch(() => undefined).finally(() => reader?.releaseLock());
450
+ };
451
+
452
+ return new ReadableStream<Uint8Array>(
453
+ {
454
+ start(controller) {
455
+ streamController = controller;
456
+ signal.addEventListener("abort", onAbort, { once: true });
457
+ if (signal.aborted) onAbort();
458
+ },
459
+ async pull(controller) {
460
+ try {
461
+ reader ??= body.getReader();
462
+ const chunk = await reader.read();
463
+ if (finished) return;
464
+ if (chunk.done) {
465
+ finished = true;
466
+ cleanup();
467
+ controller.close();
468
+ reader.releaseLock();
469
+ return;
470
+ }
471
+ controller.enqueue(chunk.value);
472
+ } catch (error) {
473
+ if (finished) return;
474
+ finished = true;
475
+ cleanup();
476
+ controller.error(error);
477
+ reader?.releaseLock();
478
+ }
479
+ },
480
+ async cancel(reason) {
481
+ if (finished) return;
482
+ finished = true;
483
+ cleanup();
484
+ try {
485
+ await (reader ? reader.cancel(reason) : body.cancel(reason));
486
+ } finally {
487
+ reader?.releaseLock();
488
+ }
489
+ },
490
+ },
491
+ { highWaterMark: 0 },
492
+ );
493
+ }
494
+
495
+ function sanitizeStreamErrors(
496
+ body: ReadableStream<Uint8Array>,
497
+ serializedUrl: SerializedRequestUrl,
498
+ ): ReadableStream<Uint8Array> {
499
+ if (serializedUrl.sensitiveValues.length === 0) return body;
500
+
501
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
502
+ return new ReadableStream<Uint8Array>(
503
+ {
504
+ async pull(controller) {
505
+ try {
506
+ reader ??= body.getReader();
507
+ const chunk = await reader.read();
508
+ if (chunk.done) {
509
+ controller.close();
510
+ return;
511
+ }
512
+ controller.enqueue(chunk.value);
513
+ } catch (error) {
514
+ controller.error(
515
+ redactSensitiveError(
516
+ error,
517
+ serializedUrl.sensitiveValues,
518
+ serializedUrl.requestUrl,
519
+ serializedUrl.redactedUrl,
520
+ ),
521
+ );
522
+ }
523
+ },
524
+ async cancel(reason) {
525
+ try {
526
+ await (reader ? reader.cancel(reason) : body.cancel(reason));
527
+ } catch (error) {
528
+ throw redactSensitiveError(
529
+ error,
530
+ serializedUrl.sensitiveValues,
531
+ serializedUrl.requestUrl,
532
+ serializedUrl.redactedUrl,
533
+ );
534
+ }
535
+ },
536
+ },
537
+ { highWaterMark: 0 },
538
+ );
539
+ }
540
+
541
+ function toNativeHttpStreamResponse(
542
+ response: Response,
543
+ serializedUrl: SerializedRequestUrl,
544
+ signal?: AbortSignal,
545
+ ): HttpStreamResponse {
195
546
  const headers = Object.fromEntries(response.headers.entries());
196
- const body = requireNativeResponseBody(response);
547
+ const body = sanitizeStreamErrors(
548
+ cancelStreamOnAbort(requireNativeResponseBody(response), signal),
549
+ serializedUrl,
550
+ );
197
551
  return {
198
552
  body,
199
553
  headers,
@@ -205,7 +559,7 @@ function toNativeHttpStreamResponse(response: Response): HttpStreamResponse {
205
559
  };
206
560
  }
207
561
 
208
- function normalizeHttpMethod(method: string): HttpMethod {
562
+ function normalizeHttpMethod(method: string): Uppercase<HttpMethod> {
209
563
  switch (method.toUpperCase()) {
210
564
  case "HEAD":
211
565
  return "HEAD";
@@ -230,7 +584,8 @@ function normalizeHttpMethod(method: string): HttpMethod {
230
584
  }
231
585
  }
232
586
 
233
- function isAbsoluteUrl(url: string): boolean {
587
+ /** Whether `url` carries its own scheme, so it needs no `upstream.baseUrl` to resolve. */
588
+ export function isAbsoluteUrl(url: string): boolean {
234
589
  return /^[a-z][a-z\d+\-.]*:/i.test(url);
235
590
  }
236
591
 
@@ -240,6 +595,105 @@ function resolveHttpUrl(baseUrl: string | undefined, url: string): string {
240
595
 
241
596
  type NativeFetchInit = RequestInit & { proxy?: string };
242
597
 
598
+ const MAX_HTTP_REDIRECT_HOPS = 20;
599
+ const HTTP_REDIRECT_POLICY_TRANSPORT: RedirectPolicyTransport = {
600
+ label: "ctx.http",
601
+ maxHops: MAX_HTTP_REDIRECT_HOPS,
602
+ };
603
+ const REDIRECT_BODY_HEADERS = new Set([
604
+ "content-encoding",
605
+ "content-language",
606
+ "content-location",
607
+ "content-type",
608
+ ]);
609
+
610
+ function snapshotHttpRedirectPolicy(options: RequestOptions): HttpRedirectPolicy | undefined {
611
+ try {
612
+ return normalizeRedirectPolicy(options.redirectPolicy, HTTP_REDIRECT_POLICY_TRANSPORT);
613
+ } catch (error) {
614
+ if (error instanceof TransportError) throw error;
615
+ throw invalidRedirectPolicy(
616
+ HTTP_REDIRECT_POLICY_TRANSPORT,
617
+ "could not be read safely",
618
+ error instanceof Error ? error : undefined,
619
+ );
620
+ }
621
+ }
622
+
623
+ function withoutRedirectBodyHeaders(headers: HeadersInit | undefined): Headers {
624
+ const nextHeaders = new Headers(headers);
625
+ for (const name of REDIRECT_BODY_HEADERS) nextHeaders.delete(name);
626
+ return nextHeaders;
627
+ }
628
+
629
+ function discardRedirectResponseBody(response: Response): void {
630
+ try {
631
+ const cancellation = response.body?.cancel();
632
+ if (cancellation) void cancellation.catch(() => undefined);
633
+ } catch {
634
+ // The redirect decision is security-significant and must not be replaced
635
+ // or delayed by an upstream body's cancellation failure. Cancellation was
636
+ // attempted; redirect evaluation continues without awaiting its completion.
637
+ }
638
+ }
639
+
640
+ async function fetchWithHttpRedirectPolicy(
641
+ requestUrl: string,
642
+ requestInit: NativeFetchInit,
643
+ policy: HttpRedirectPolicy | undefined,
644
+ ): Promise<Response> {
645
+ if (!policy) return fetch(requestUrl, requestInit);
646
+
647
+ const initialUrl = new URL(requestUrl);
648
+ if (initialUrl.protocol !== "http:" && initialUrl.protocol !== "https:") {
649
+ throw new TransportError("ctx.http redirectPolicy requires an HTTP(S) origin", {
650
+ code: "transport_invalid_url",
651
+ });
652
+ }
653
+
654
+ const initialOrigin = initialUrl.origin;
655
+ let currentUrl = requestUrl;
656
+ let method = normalizeHttpMethod(requestInit.method ?? "GET");
657
+ let body = requestInit.body;
658
+ let headers = requestInit.headers;
659
+ let followedHops = 0;
660
+ const visitedRequests = new Set([`${method} ${currentUrl}`]);
661
+
662
+ while (true) {
663
+ const response = await fetch(currentUrl, {
664
+ ...requestInit,
665
+ body,
666
+ headers,
667
+ method,
668
+ redirect: "manual",
669
+ });
670
+ if (!isRedirectStatus(response.status)) return response;
671
+
672
+ const location = response.headers.get("location");
673
+ discardRedirectResponseBody(response);
674
+ const decision = nextSameOriginRedirectHop({
675
+ policy,
676
+ initialOrigin,
677
+ currentUrl,
678
+ responseUrl: currentUrl,
679
+ status: response.status,
680
+ method,
681
+ location: location ?? undefined,
682
+ followedHops,
683
+ visitedRequests,
684
+ });
685
+ if (decision.nextMethod !== method) {
686
+ body = undefined;
687
+ headers = withoutRedirectBodyHeaders(headers);
688
+ }
689
+
690
+ method = decision.nextMethod;
691
+ currentUrl = decision.nextUrl;
692
+ followedHops += 1;
693
+ visitedRequests.add(`${method} ${currentUrl}`);
694
+ }
695
+ }
696
+
243
697
  async function resolveNativeProxy(
244
698
  options: RequestOptions,
245
699
  clientOptions: HttpClientOptions,
@@ -249,14 +703,17 @@ async function resolveNativeProxy(
249
703
  const resolvedProxy = await resolveProxyConfigAsync({
250
704
  proxy: options.proxy ?? clientOptions.proxy,
251
705
  upstream: clientOptions.upstream,
252
- apifuseConfig: clientOptions.apifuseConfig,
253
706
  proxyPolicy: clientOptions.proxyPolicy,
254
707
  affinityKey: clientOptions.affinityKey,
255
708
  proxyAttempt: computeProxyAttemptIndex({
256
709
  baseProxyAttempt: clientOptions.proxyAttempt,
257
710
  retryAttemptOffset: proxyAttemptOffset,
258
711
  }),
712
+ // Bun's native fetch proxy option tunnels HTTP CONNECT only; SOCKS5 is not
713
+ // supported here, so a socks5 policy fails loudly rather than downgrading.
714
+ transportProtocols: ["http"],
259
715
  telemetry: clientOptions.telemetry,
716
+ engineCredentials: clientOptions.engineCredentials,
260
717
  });
261
718
  if (resolvedProxy.shouldWarn) {
262
719
  warn(
@@ -285,37 +742,91 @@ function normalizeNativeFetchBody(body: unknown): string | ArrayBuffer | undefin
285
742
  return copied.buffer;
286
743
  }
287
744
 
745
+ function serializeHttpRequestUrl(
746
+ baseUrl: string | undefined,
747
+ url: string,
748
+ options: RequestOptions,
749
+ ): SerializedRequestUrl {
750
+ try {
751
+ return serializeRequestUrl(
752
+ resolveHttpUrl(baseUrl, url),
753
+ options.params,
754
+ options.sensitiveParams,
755
+ );
756
+ } catch (error) {
757
+ throw redactSensitiveRequestError(error, url, options.sensitiveParams);
758
+ }
759
+ }
760
+
288
761
  async function fetchNativeHttp(
289
762
  baseUrl: string | undefined,
290
763
  url: string,
291
- method: HttpMethod,
764
+ method: Uppercase<HttpMethod>,
292
765
  options: RequestOptions & { body?: unknown },
293
766
  clientOptions: HttpClientOptions,
294
767
  warn: (message: string) => void,
295
768
  statusRetryCodes?: readonly number[],
296
769
  proxyAttemptOffset = 0,
770
+ dedupe?: { attempted: Set<string> },
771
+ observeProxy?: (used: boolean, status?: number) => void,
772
+ nextHeaderAttempt: () => number = () => 1,
297
773
  ): Promise<NativeHttpAttemptOutcome> {
298
- const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
774
+ const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
775
+ const { requestUrl } = serializedUrl;
299
776
  const controller = options.timeout ? new AbortController() : undefined;
300
- const timeoutHandle = options.timeout
777
+ const signal = mergeAbortSignals(options.signal, controller?.signal);
778
+ let timeoutHandle = options.timeout
301
779
  ? setTimeout(() => controller?.abort(), options.timeout)
302
780
  : undefined;
781
+ const disarmTimeout = () => {
782
+ if (!timeoutHandle) return;
783
+ clearTimeout(timeoutHandle);
784
+ timeoutHandle = undefined;
785
+ };
303
786
 
304
787
  let proxy: string | undefined;
305
788
  try {
789
+ throwIfAmbientAborted(options.signal);
790
+ // Resolve inside the try (and after the timeout is armed) so allocator
791
+ // failures are branded as TransportErrors and count against the request
792
+ // deadline, exactly as an inline resolve would.
306
793
  proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
794
+ observeProxy?.(Boolean(proxy));
795
+ throwIfAmbientAborted(options.signal);
796
+ // For a registry allocator chain, skip an endpoint a prior attempt already
797
+ // tried rather than re-issuing the same request. Returning the sentinel
798
+ // (instead of breaking) lets the loop keep advancing the flat offset until
799
+ // it crosses into the fallback vendor's pool span.
800
+ if (dedupe && proxy) {
801
+ if (dedupe.attempted.has(proxy)) {
802
+ return { kind: "dedupe-skip" };
803
+ }
804
+ dedupe.attempted.add(proxy);
805
+ }
806
+ // Resolve after the dedupe check so a skipped duplicate offset never burns
807
+ // a single-use proof, and with the serialized URL so URL-bound proofs
808
+ // match what is actually issued.
809
+ const requestHeaders = await resolveAttemptHeaders(
810
+ options,
811
+ clientOptions,
812
+ () => ({ attempt: nextHeaderAttempt(), url: requestUrl, method }),
813
+ signal,
814
+ );
307
815
  const requestInit: NativeFetchInit = {
308
- headers: options.headers,
816
+ headers: requestHeaders,
309
817
  method,
310
818
  ...(proxy ? { proxy } : {}),
311
- signal: controller?.signal,
819
+ ...(signal ? { signal } : {}),
312
820
  };
313
821
  if (options.body !== undefined) {
314
822
  requestInit.body = normalizeNativeFetchBody(options.body);
315
823
  }
316
- const response = await fetch(requestUrl, {
317
- ...requestInit,
318
- });
824
+ const response = await fetchWithHttpRedirectPolicy(
825
+ requestUrl,
826
+ requestInit,
827
+ options.redirectPolicy,
828
+ );
829
+ observeProxy?.(Boolean(proxy), response.status);
319
830
  const headers = Object.fromEntries(response.headers.entries());
320
831
 
321
832
  if (statusRetryCodes && response.status >= 400) {
@@ -337,47 +848,80 @@ async function fetchNativeHttp(
337
848
  });
338
849
  }
339
850
 
340
- return toNativeHttpResponse(response);
851
+ // `timeout` bounds the header phase (types.ts RequestOptions.timeout), so it
852
+ // is disarmed before the body is read: awaiting the buffered body inside
853
+ // this try brands a mid-body failure as a transport error, but must not
854
+ // extend the deadline over the download. Cancellation still applies —
855
+ // `signal` stays attached to the native response. The discarded non-2xx
856
+ // bodies drained above stay under the deadline on purpose: nothing else
857
+ // bounds them, and their content is never returned to the caller.
858
+ disarmTimeout();
859
+ return await toNativeHttpResponse(response);
341
860
  } catch (error) {
342
861
  if (error instanceof SyntaxError) {
343
- throw error;
862
+ throw redactSensitiveError(
863
+ error,
864
+ serializedUrl.sensitiveValues,
865
+ serializedUrl.requestUrl,
866
+ serializedUrl.redactedUrl,
867
+ );
344
868
  }
345
- const transportError = toHttpTransportError(error) as NativeHttpAttemptError;
869
+ const transportError: NativeHttpAttemptError = redactSensitiveError(
870
+ toHttpTransportError(error, options.signal, controller?.signal),
871
+ serializedUrl.sensitiveValues,
872
+ serializedUrl.requestUrl,
873
+ serializedUrl.redactedUrl,
874
+ );
346
875
  transportError.proxyUsed = Boolean(proxy);
347
876
  throw transportError;
348
877
  } finally {
349
- if (timeoutHandle) clearTimeout(timeoutHandle);
878
+ disarmTimeout();
350
879
  }
351
880
  }
352
881
 
353
882
  async function fetchNativeHttpStream(
354
883
  baseUrl: string | undefined,
355
884
  url: string,
356
- method: HttpMethod,
885
+ method: Uppercase<HttpMethod>,
357
886
  options: RequestOptions & { body?: unknown },
358
887
  clientOptions: HttpClientOptions,
359
888
  warn: (message: string) => void,
889
+ observeProxy?: (used: boolean) => void,
360
890
  ): Promise<HttpStreamResponse> {
361
- const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
891
+ const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
892
+ const { requestUrl } = serializedUrl;
362
893
  const controller = options.timeout ? new AbortController() : undefined;
894
+ const signal = mergeAbortSignals(options.signal, controller?.signal);
363
895
  const timeoutHandle = options.timeout
364
896
  ? setTimeout(() => controller?.abort(), options.timeout)
365
897
  : undefined;
366
898
 
367
899
  try {
900
+ throwIfAmbientAborted(options.signal);
368
901
  const proxy = await resolveNativeProxy(options, clientOptions, warn);
902
+ observeProxy?.(Boolean(proxy));
903
+ throwIfAmbientAborted(options.signal);
904
+ // Streams never retry: a header factory is resolved once, as attempt 1.
905
+ const headers = await resolveAttemptHeaders(
906
+ options,
907
+ clientOptions,
908
+ () => ({ attempt: 1, url: requestUrl, method }),
909
+ signal,
910
+ );
369
911
  const requestInit: NativeFetchInit = {
370
- headers: options.headers,
912
+ headers,
371
913
  method,
372
914
  ...(proxy ? { proxy } : {}),
373
- signal: controller?.signal,
915
+ ...(signal ? { signal } : {}),
374
916
  };
375
917
  if (options.body !== undefined) {
376
918
  requestInit.body = normalizeNativeFetchBody(options.body);
377
919
  }
378
- const response = await fetch(requestUrl, {
379
- ...requestInit,
380
- });
920
+ const response = await fetchWithHttpRedirectPolicy(
921
+ requestUrl,
922
+ requestInit,
923
+ options.redirectPolicy,
924
+ );
381
925
 
382
926
  if (response.status >= 400 && options.throwOnHttpError !== false) {
383
927
  await drainNativeResponseBody(response);
@@ -387,12 +931,24 @@ async function fetchNativeHttpStream(
387
931
  });
388
932
  }
389
933
 
390
- return toNativeHttpStreamResponse(response);
934
+ // The stream timeout remains header-scoped; composed ambient and local
935
+ // cancellation stays attached to native fetch for the body's full lifetime.
936
+ return toNativeHttpStreamResponse(response, serializedUrl, options.signal);
391
937
  } catch (error) {
392
938
  if (error instanceof SyntaxError) {
393
- throw error;
939
+ throw redactSensitiveError(
940
+ error,
941
+ serializedUrl.sensitiveValues,
942
+ serializedUrl.requestUrl,
943
+ serializedUrl.redactedUrl,
944
+ );
394
945
  }
395
- throw toHttpTransportError(error);
946
+ throw redactSensitiveError(
947
+ toHttpTransportError(error, options.signal, controller?.signal),
948
+ serializedUrl.sensitiveValues,
949
+ serializedUrl.requestUrl,
950
+ serializedUrl.redactedUrl,
951
+ );
396
952
  } finally {
397
953
  if (timeoutHandle) clearTimeout(timeoutHandle);
398
954
  }
@@ -417,113 +973,257 @@ export function createHttpClient(
417
973
  method: string,
418
974
  options: RequestOptions & { body?: unknown } = {},
419
975
  ): Promise<HttpResponse> {
420
- if (!baseUrl && !isAbsoluteUrl(url)) {
421
- throw new TransportError(
422
- "ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared",
423
- { code: "transport_invalid_url" },
424
- );
425
- }
426
- assertNoHttpTransportOverrides(options);
427
- const headersOptions = withClientHeaders(options, clientOptions, options.body);
428
- const methodName = normalizeHttpMethod(method);
429
- const explicitRetry = headersOptions.retry !== undefined;
430
- const retryOptions =
431
- normalizeProxyTransportRetryOptions(headersOptions.retry, {
432
- label: "HTTP",
433
- }) ??
434
- (explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
435
- if (retryOptions) validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
436
- const retryEnabled = Boolean(
437
- retryOptions &&
438
- retryOptions.attempts > 1 &&
439
- isProxyTransportRetryMethod(methodName, retryOptions),
440
- );
441
- const statusRetryEnabled = Boolean(
442
- retryEnabled &&
443
- explicitRetry &&
444
- retryOptions &&
445
- retryOptions.statusCodes.length > 0 &&
446
- headersOptions.throwOnHttpError !== false,
976
+ const { explicitRetry, headersOptions, methodName, retryOptions } = (() => {
977
+ try {
978
+ if (!baseUrl && !isAbsoluteUrl(url)) {
979
+ throw new TransportError(
980
+ "ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared",
981
+ { code: "transport_invalid_url" },
982
+ );
983
+ }
984
+ assertNoHttpTransportOverrides(options);
985
+ const redirectPolicy = snapshotHttpRedirectPolicy(options);
986
+ const headersOptions = {
987
+ ...withClientHeaders(options, clientOptions, options.body),
988
+ redirectPolicy,
989
+ };
990
+ const methodName = normalizeHttpMethod(method);
991
+ const explicitRetry = headersOptions.retry !== undefined;
992
+ const retryOptions =
993
+ normalizeProxyTransportRetryOptions(headersOptions.retry, {
994
+ label: "HTTP",
995
+ }) ??
996
+ (explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
997
+ if (retryOptions) validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
998
+ return { explicitRetry, headersOptions, methodName, retryOptions };
999
+ } catch (error) {
1000
+ throw redactSensitiveRequestError(error, url, options.sensitiveParams);
1001
+ }
1002
+ })();
1003
+ const requestStarted = performance.now();
1004
+ const telemetry = startHttpTelemetry(
1005
+ scopedHttpTelemetry(clientOptions.httpTelemetry),
1006
+ retryOptions?.preset,
447
1007
  );
448
- const attemptOptions: RequestOptions & { body?: unknown } = statusRetryEnabled
449
- ? { ...headersOptions, throwOnHttpError: false }
450
- : headersOptions;
1008
+ try {
1009
+ return await runRequest();
1010
+ } finally {
1011
+ telemetry.finish(performance.now() - requestStarted);
1012
+ }
451
1013
 
452
- const executeOnce = (proxyAttemptOffset = 0): Promise<NativeHttpAttemptOutcome> =>
453
- fetchNativeHttp(
454
- baseUrl,
455
- url,
456
- methodName,
457
- attemptOptions,
458
- clientOptions,
459
- warnOnce,
460
- statusRetryEnabled ? retryOptions?.statusCodes : undefined,
461
- proxyAttemptOffset,
1014
+ async function runRequest(): Promise<HttpResponse> {
1015
+ const retryEnabled = Boolean(
1016
+ retryOptions &&
1017
+ retryOptions.attempts > 1 &&
1018
+ isProxyTransportRetryMethod(methodName, retryOptions),
462
1019
  );
1020
+ const statusRetryEnabled = Boolean(
1021
+ retryEnabled &&
1022
+ explicitRetry &&
1023
+ retryOptions &&
1024
+ retryOptions.statusCodes.length > 0 &&
1025
+ headersOptions.throwOnHttpError !== false,
1026
+ );
1027
+ const attemptOptions: RequestOptions & { body?: unknown } = statusRetryEnabled
1028
+ ? { ...headersOptions, throwOnHttpError: false }
1029
+ : headersOptions;
463
1030
 
464
- if (!retryEnabled || !retryOptions) {
465
- const outcome = await executeOnce();
466
- if (isHttpStatusOutcome(outcome)) {
467
- throw toUpstreamHttpError(outcome.status);
468
- }
469
- return outcome;
470
- }
1031
+ // Span the whole vendor chain on transport failures. Like ctx.stealth, a
1032
+ // policy-managed proxy resolves a *different* endpoint/vendor per attempt
1033
+ // (the flat proxyAttemptOffset rotates across the concatenated vendor pool
1034
+ // spans), so a transport failure should advance to the next endpoint —
1035
+ // potentially crossing into the fallback vendor — rather than stopping at
1036
+ // the per-endpoint retry budget and stranding the request on the primary
1037
+ // vendor. resolvePolicyTransportAttemptCap widens the cap to the chain span
1038
+ // only for implicit, safe-method allocator requests; explicit retry
1039
+ // policies (their documented `attempts` ceiling), unsafe methods, and
1040
+ // static/non-registry vendors keep the retry budget. Status-code retries
1041
+ // stay bounded by the retry budget regardless; only transport rotation gets
1042
+ // the full span.
1043
+ const policyProxy: ProviderProxyPolicy | undefined = (() => {
1044
+ const policy = clientOptions.proxyPolicy ?? clientOptions.upstream?.proxy;
1045
+ return policy && typeof policy === "object" ? policy : undefined;
1046
+ })();
1047
+ const usesPolicyAllocator = Boolean(policyProxy) && !options.proxy && !clientOptions.proxy;
1048
+ const transportAttemptCap = retryOptions
1049
+ ? resolvePolicyTransportAttemptCap({
1050
+ policy: policyProxy,
1051
+ usesPolicyAllocator,
1052
+ retryAttempts: retryOptions.attempts,
1053
+ explicitRetry,
1054
+ method: methodName,
1055
+ })
1056
+ : 1;
471
1057
 
472
- let lastErrorCode: string | undefined;
473
- let lastStatus: number | undefined;
474
- for (let attempt = 1; attempt <= retryOptions.attempts; attempt += 1) {
475
- try {
476
- const outcome = await executeOnce(attempt - 1);
1058
+ // Track resolved endpoints across a policy-allocator chain. Successive
1059
+ // attempts rotate the flat offset across the concatenated vendor pool
1060
+ // spans, but an under-filled allocation (fewer live endpoints than the
1061
+ // configured pool size) makes the modulo mapping repeat endpoints before
1062
+ // the offset reaches the next vendor. Rather than re-hammering an
1063
+ // already-tried endpoint under backoff, fetchNativeHttp returns a skip
1064
+ // sentinel for a duplicate; the loop then advances the flat offset without
1065
+ // issuing the request, so it keeps walking toward — and into — the fallback
1066
+ // vendor's pool span instead of stalling on the primary vendor.
1067
+ // De-duplication is gated on the SAME predicate that widens the attempt cap
1068
+ // (implicit, safe-method, registry-chain rotation). It must NOT engage for
1069
+ // an explicit retry policy: there the caller's `attempts` count is the
1070
+ // contract and each attempt must issue against whatever endpoint it resolves
1071
+ // — even a repeat — instead of being silently skipped (which would collapse
1072
+ // a `poolSize: 1` + `attempts: 3` request to a single fetch).
1073
+ const dedupeAllocatorEndpoints = policyRotatesTransportVendorChain({
1074
+ policy: policyProxy,
1075
+ usesPolicyAllocator,
1076
+ explicitRetry,
1077
+ method: methodName,
1078
+ });
1079
+ const dedupeContext = dedupeAllocatorEndpoints ? { attempted: new Set<string>() } : undefined;
1080
+
1081
+ // Numbers the attempts a header factory sees: only requests that reached
1082
+ // header resolution count, so a failed proxy allocation (which increments
1083
+ // the telemetry `issued` count below) or a dedupe-skipped offset does not
1084
+ // make the first request that is actually built report `attempt: 2`.
1085
+ let headerAttempts = 0;
1086
+ const nextHeaderAttempt = () => {
1087
+ headerAttempts += 1;
1088
+ return headerAttempts;
1089
+ };
1090
+
1091
+ const executeOnce = async (proxyAttemptOffset = 0): Promise<NativeHttpAttemptOutcome> => {
1092
+ const started = performance.now();
1093
+ let proxyUsed = false;
1094
+ let observedStatus: number | undefined;
1095
+ const record = (event: Omit<HttpAttemptTelemetryEvent, "ms" | "proxyUsed">) => {
1096
+ telemetry.recordAttempt({ ...event, ms: performance.now() - started, proxyUsed });
1097
+ };
1098
+ try {
1099
+ const outcome = await fetchNativeHttp(
1100
+ baseUrl,
1101
+ url,
1102
+ methodName,
1103
+ attemptOptions,
1104
+ clientOptions,
1105
+ warnOnce,
1106
+ statusRetryEnabled ? retryOptions?.statusCodes : undefined,
1107
+ proxyAttemptOffset,
1108
+ dedupeContext,
1109
+ (used, status) => {
1110
+ proxyUsed = used;
1111
+ observedStatus = status;
1112
+ },
1113
+ nextHeaderAttempt,
1114
+ );
1115
+ if (!isDedupeSkipOutcome(outcome))
1116
+ record({
1117
+ status: outcome.status,
1118
+ ...(isHttpStatusOutcome(outcome)
1119
+ ? { e: "upstream_http_error", statusRetry: true }
1120
+ : {}),
1121
+ });
1122
+ return outcome;
1123
+ } catch (error) {
1124
+ // Record before the retry loop can catch/rethrow a terminal status.
1125
+ record(httpAttemptFailure(error, observedStatus));
1126
+ throw error;
1127
+ }
1128
+ };
1129
+
1130
+ if (!retryEnabled || !retryOptions) {
1131
+ throwIfAmbientAborted(headersOptions.signal);
1132
+ const outcome = await executeOnce();
1133
+ if (isDedupeSkipOutcome(outcome)) {
1134
+ // Single-shot path never de-duplicates (dedupeContext is undefined),
1135
+ // but keep the union total.
1136
+ throw new TransportError("HTTP request produced no terminal result", {
1137
+ code: "retry_exhausted",
1138
+ });
1139
+ }
477
1140
  if (isHttpStatusOutcome(outcome)) {
478
- lastStatus = outcome.status;
479
- if (outcome.retryable && attempt < retryOptions.attempts) {
480
- await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers));
481
- continue;
482
- }
483
1141
  throw toUpstreamHttpError(outcome.status);
484
1142
  }
1143
+ return outcome;
1144
+ }
485
1145
 
486
- const response = outcome;
487
- if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
488
- throw toUpstreamHttpError(response.status);
489
- }
1146
+ let lastError: unknown;
1147
+ // `attempt` walks the flat proxy offset across the full chain span; `issued`
1148
+ // counts requests that were actually sent (skipped duplicate offsets do not
1149
+ // increment it). Retry summaries and the status-retry budget must reflect
1150
+ // issued requests, not the raw offset, so they stay accurate when partial
1151
+ // allocations skip offsets.
1152
+ let issued = 0;
1153
+ for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
1154
+ throwIfAmbientAborted(headersOptions.signal);
1155
+ // Whether this offset actually issued a request (vs. a skipped duplicate),
1156
+ // so the catch counts a thrown *transport* failure once without
1157
+ // double-counting a status outcome that already incremented before it
1158
+ // re-threw as an upstream HTTP error.
1159
+ let issuedThisAttempt = false;
1160
+ try {
1161
+ const outcome = await executeOnce(attempt - 1);
1162
+ if (isDedupeSkipOutcome(outcome)) {
1163
+ // Duplicate endpoint from a partial allocation: advance the flat
1164
+ // offset without issuing the request (no backoff, not a failure) so
1165
+ // the loop keeps rotating toward the fallback vendor.
1166
+ continue;
1167
+ }
1168
+ issued += 1;
1169
+ issuedThisAttempt = true;
1170
+ if (isHttpStatusOutcome(outcome)) {
1171
+ if (outcome.retryable && issued < retryOptions.attempts) {
1172
+ await sleep(
1173
+ computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers),
1174
+ headersOptions.signal,
1175
+ );
1176
+ continue;
1177
+ }
1178
+ throw toUpstreamHttpError(outcome.status);
1179
+ }
490
1180
 
491
- if (attempt > 1) {
492
- const summary: HttpRetrySummary = {
493
- attempts: attempt,
494
- retries: attempt - 1,
495
- ...(retryOptions.preset ? { preset: retryOptions.preset } : {}),
496
- transport: "native",
497
- ...(lastErrorCode ? { lastErrorCode } : {}),
498
- ...(lastStatus ? { lastStatus } : {}),
499
- };
500
- clientOptions.onRetrySummary?.(summary);
501
- }
502
- return response;
503
- } catch (error) {
504
- lastErrorCode = proxyTransportRetryErrorCode(error);
505
- lastStatus = proxyTransportRetryErrorStatus(error);
506
- const proxyUsed = Boolean((error as NativeHttpAttemptError).proxyUsed);
507
- if (
508
- attempt < retryOptions.attempts &&
509
- shouldRetryProxyTransportAttempt({
510
- error,
511
- explicitRetry,
512
- method: methodName,
513
- options: retryOptions,
514
- proxyUsed,
515
- })
516
- ) {
517
- await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt));
518
- continue;
1181
+ const response = outcome;
1182
+ if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
1183
+ throw toUpstreamHttpError(response.status);
1184
+ }
1185
+
1186
+ if (issued > 1) {
1187
+ const summary = telemetry.toTenantRetryPayload();
1188
+ if (summary) telemetry.observe(() => clientOptions.onRetrySummary?.(summary));
1189
+ }
1190
+ return response;
1191
+ } catch (error) {
1192
+ throwIfAmbientAborted(headersOptions.signal);
1193
+ if (!issuedThisAttempt) issued += 1;
1194
+ lastError = error;
1195
+ const proxyUsed = Boolean((error as NativeHttpAttemptError).proxyUsed);
1196
+ if (
1197
+ attempt < transportAttemptCap &&
1198
+ shouldRetryProxyTransportAttempt({
1199
+ error,
1200
+ explicitRetry,
1201
+ method: methodName,
1202
+ options: retryOptions,
1203
+ proxyUsed,
1204
+ })
1205
+ ) {
1206
+ await sleep(
1207
+ computeProxyTransportRetryDelayMs(retryOptions, attempt),
1208
+ headersOptions.signal,
1209
+ );
1210
+ continue;
1211
+ }
1212
+ throw error;
519
1213
  }
520
- throw error;
521
1214
  }
522
- }
523
1215
 
524
- throw new TransportError("HTTP retry exhausted without a terminal result", {
525
- code: "retry_exhausted",
526
- });
1216
+ // Reached when the attempt cap is consumed without a terminal outcome —
1217
+ // e.g. the final offsets of a partial allocation all resolved to
1218
+ // already-tried endpoints and were skipped. Surface the last real transport
1219
+ // failure rather than a synthetic exhaustion error.
1220
+ if (lastError !== undefined) {
1221
+ throw lastError;
1222
+ }
1223
+ throw new TransportError("HTTP retry exhausted without a terminal result", {
1224
+ code: "retry_exhausted",
1225
+ });
1226
+ }
527
1227
  }
528
1228
 
529
1229
  async function streamRequest(
@@ -531,16 +1231,56 @@ export function createHttpClient(
531
1231
  method: string,
532
1232
  options: RequestOptions & { body?: unknown } = {},
533
1233
  ): Promise<HttpStreamResponse> {
534
- if (!baseUrl && !isAbsoluteUrl(url)) {
535
- throw new TransportError(
536
- "ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared",
537
- { code: "transport_invalid_url" },
1234
+ const { headersOptions, methodName } = (() => {
1235
+ try {
1236
+ if (!baseUrl && !isAbsoluteUrl(url)) {
1237
+ throw new TransportError(
1238
+ "ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared",
1239
+ { code: "transport_invalid_url" },
1240
+ );
1241
+ }
1242
+ assertNoHttpTransportOverrides(options);
1243
+ const redirectPolicy = snapshotHttpRedirectPolicy(options);
1244
+ return {
1245
+ headersOptions: {
1246
+ ...withClientHeaders(options, clientOptions, options.body),
1247
+ redirectPolicy,
1248
+ },
1249
+ methodName: normalizeHttpMethod(method),
1250
+ };
1251
+ } catch (error) {
1252
+ throw redactSensitiveRequestError(error, url, options.sensitiveParams);
1253
+ }
1254
+ })();
1255
+ // Streams never retry. Observe the header fetch without consuming its body.
1256
+ const telemetry = startHttpTelemetry(scopedHttpTelemetry(clientOptions.httpTelemetry), "off");
1257
+ const started = performance.now();
1258
+ const alreadyAborted = headersOptions.signal?.aborted === true;
1259
+ let proxyUsed = false;
1260
+ const record = (event: Omit<HttpAttemptTelemetryEvent, "ms" | "proxyUsed">) => {
1261
+ if (alreadyAborted) return;
1262
+ telemetry.recordAttempt({ ...event, ms: performance.now() - started, proxyUsed });
1263
+ };
1264
+ try {
1265
+ const response = await fetchNativeHttpStream(
1266
+ baseUrl,
1267
+ url,
1268
+ methodName,
1269
+ headersOptions,
1270
+ clientOptions,
1271
+ warnOnce,
1272
+ (used) => {
1273
+ proxyUsed = used;
1274
+ },
538
1275
  );
1276
+ record({ status: response.status });
1277
+ return response;
1278
+ } catch (error) {
1279
+ record(httpAttemptFailure(error));
1280
+ throw error;
1281
+ } finally {
1282
+ telemetry.finish(performance.now() - started);
539
1283
  }
540
- assertNoHttpTransportOverrides(options);
541
- const headersOptions = withClientHeaders(options, clientOptions, options.body);
542
- const methodName = normalizeHttpMethod(method);
543
- return fetchNativeHttpStream(baseUrl, url, methodName, headersOptions, clientOptions, warnOnce);
544
1284
  }
545
1285
 
546
1286
  return {
@@ -556,10 +1296,16 @@ export function createHttpClient(
556
1296
  url,
557
1297
  options: RequestWithMethodOptions = {},
558
1298
  ): Promise<AsyncIterable<SseMessage>> => {
559
- const headers = {
560
- Accept: "text/event-stream",
561
- ...options.headers,
562
- };
1299
+ const callerHeaders = options.headers;
1300
+ const headers: Record<string, string> | HttpHeadersFactory =
1301
+ typeof callerHeaders === "function"
1302
+ ? async (attempt) => {
1303
+ // Pass a malformed result through unchanged so the factory
1304
+ // validation rejects it instead of spreading it into `{}`.
1305
+ const minted = await callerHeaders(attempt);
1306
+ return isHeaderRecord(minted) ? { Accept: "text/event-stream", ...minted } : minted;
1307
+ }
1308
+ : { Accept: "text/event-stream", ...callerHeaders };
563
1309
  const response = await streamRequest(url, options.method ?? "GET", {
564
1310
  ...options,
565
1311
  headers,