@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,16 +1,77 @@
1
- import { resolveProxyConfigAsync } from "../config/loader.js";
1
+ import { policyRotatesTransportVendorChain, resolvePolicyTransportAttemptCap, resolveProxyConfigAsync, } from "../config/loader.js";
2
2
  import { 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 { httpTelemetryErrorCode, } from "./http-telemetry.js";
6
+ import { invalidRedirectPolicy, isRedirectStatus, nextSameOriginRedirectHop, normalizeRedirectPolicy, } from "./redirects.js";
7
+ import { normalizeHttpRequestBody, redactSensitiveError, redactSensitiveRequestError, serializeRequestUrl, } from "./request-options.js";
8
+ import { startHttpTelemetry } from "./http-telemetry-guard.js";
9
+ import { scopedHttpTelemetry } from "./http-telemetry-binding.js";
6
10
  const DEFAULT_HTTP_BASE_URL = "http://localhost";
11
+ function httpAttemptFailure(error, status) {
12
+ try {
13
+ return {
14
+ e: httpTelemetryErrorCode(proxyTransportRetryErrorCode(error)),
15
+ status: proxyTransportRetryErrorStatus(error) ?? status,
16
+ ...(error instanceof Error
17
+ ? {
18
+ diagnostics: {
19
+ name: error.name,
20
+ message: error.message,
21
+ ...(error.cause instanceof Error
22
+ ? { cause: { name: error.cause.name, message: error.cause.message } }
23
+ : {}),
24
+ },
25
+ }
26
+ : {}),
27
+ };
28
+ }
29
+ catch {
30
+ return { e: "other" };
31
+ }
32
+ }
7
33
  function isHttpStatusOutcome(outcome) {
8
34
  return "kind" in outcome && outcome.kind === "http-status";
9
35
  }
10
- async function sleep(ms) {
36
+ function isDedupeSkipOutcome(outcome) {
37
+ return "kind" in outcome && outcome.kind === "dedupe-skip";
38
+ }
39
+ function toAmbientCancellationError(signal, error = signal.reason) {
40
+ if (error instanceof TransportError && error.code === "transport_cancelled") {
41
+ return error;
42
+ }
43
+ return new TransportError("Request cancelled", {
44
+ code: "transport_cancelled",
45
+ status: 0,
46
+ retryable: false,
47
+ ...(error !== undefined
48
+ ? { cause: error instanceof Error ? error : new Error(String(error)) }
49
+ : {}),
50
+ });
51
+ }
52
+ function throwIfAmbientAborted(signal) {
53
+ if (signal?.aborted)
54
+ throw toAmbientCancellationError(signal);
55
+ }
56
+ async function sleep(ms, signal) {
57
+ throwIfAmbientAborted(signal);
11
58
  if (ms <= 0)
12
59
  return;
13
- await new Promise((resolve) => setTimeout(resolve, ms));
60
+ if (!signal) {
61
+ await new Promise((resolve) => setTimeout(resolve, ms));
62
+ return;
63
+ }
64
+ await new Promise((resolve, reject) => {
65
+ const onAbort = () => {
66
+ clearTimeout(timer);
67
+ reject(toAmbientCancellationError(signal));
68
+ };
69
+ const timer = setTimeout(() => {
70
+ signal.removeEventListener("abort", onAbort);
71
+ resolve();
72
+ }, ms);
73
+ signal.addEventListener("abort", onAbort, { once: true });
74
+ });
14
75
  }
15
76
  function toUpstreamHttpError(status) {
16
77
  return new TransportError(`Upstream request failed with status ${status}`, {
@@ -23,19 +84,92 @@ function hasHeader(headers, name) {
23
84
  const needle = name.toLowerCase();
24
85
  return Object.keys(headers).some((key) => key.toLowerCase() === needle);
25
86
  }
26
- function withClientHeaders(options, clientOptions, body) {
27
- const headers = {
87
+ function withClientHeaderDefaults(headers, clientOptions, body) {
88
+ const merged = {
28
89
  ...(clientOptions.userAgent ? { "User-Agent": clientOptions.userAgent } : {}),
29
- ...options?.headers,
90
+ ...headers,
30
91
  };
31
- if (body !== undefined && !hasHeader(headers, "Content-Type")) {
32
- headers["Content-Type"] = "application/json";
92
+ if (body !== undefined && !hasHeader(merged, "Content-Type")) {
93
+ merged["Content-Type"] = "application/json";
33
94
  }
95
+ return merged;
96
+ }
97
+ function withClientHeaders(options, clientOptions, body) {
98
+ // The ambient client signal is merged into the per-request one here so every
99
+ // downstream read (retry loop, sleep, body consumption) sees a single signal.
100
+ // The header-factory branch needs it too, otherwise factory-header requests
101
+ // would lose ambient cancellation entirely.
102
+ const signal = mergeAbortSignals(clientOptions.signal, options?.signal);
103
+ // A header factory is resolved per issued attempt (resolveAttemptHeaders);
104
+ // the client defaults are applied to its output at that point.
105
+ if (typeof options?.headers === "function")
106
+ return { ...options, signal };
34
107
  return {
35
108
  ...options,
36
- headers,
109
+ headers: withClientHeaderDefaults(options?.headers, clientOptions, body),
110
+ signal,
37
111
  };
38
112
  }
113
+ function headerFactoryFailure(message, cause) {
114
+ // A dedicated code keeps a broken proof minter out of the retry predicate:
115
+ // toHttpTransportError would otherwise brand a plain throw as
116
+ // transport_network_error, which the default retry policy re-issues.
117
+ // retryable:false is explicit so the served error envelope does not
118
+ // advertise a deterministic signing failure as retryable, and
119
+ // category:provider_error keeps a provider-local minting bug out of the
120
+ // upstream-failure signal (status 0 would otherwise fall through the
121
+ // observability ladder to upstream_http / source upstream_failure).
122
+ return new TransportError(message, {
123
+ code: "http_header_factory_failed",
124
+ status: 0,
125
+ retryable: false,
126
+ category: "provider_error",
127
+ ...(cause instanceof Error ? { cause } : {}),
128
+ });
129
+ }
130
+ function isHeaderRecord(value) {
131
+ return value !== null && typeof value === "object" && !Array.isArray(value);
132
+ }
133
+ // Settle `promise`, or reject with the signal's reason as soon as it aborts, so
134
+ // a pending header factory cannot outlive the request deadline or an ambient
135
+ // cancellation (native fetch has not started yet, so nothing else observes them).
136
+ function settleBeforeAbort(promise, signal) {
137
+ if (!signal)
138
+ return promise;
139
+ return new Promise((resolve, reject) => {
140
+ const onAbort = () => reject(signal.reason);
141
+ if (signal.aborted)
142
+ onAbort();
143
+ else
144
+ signal.addEventListener("abort", onAbort, { once: true });
145
+ // Always attach handlers: a late rejection after the abort must settle
146
+ // here (a no-op on the already-rejected promise), never go unhandled.
147
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
148
+ });
149
+ }
150
+ async function resolveAttemptHeaders(options, clientOptions, attempt, signal) {
151
+ const { headers } = options;
152
+ if (typeof headers !== "function")
153
+ return headers;
154
+ // The per-call timeout may have fired during proxy resolution: do not start
155
+ // the factory at all once the request is already aborted.
156
+ if (signal?.aborted)
157
+ throw signal.reason;
158
+ let minted;
159
+ try {
160
+ minted = await settleBeforeAbort(Promise.resolve().then(() => headers(attempt())), signal);
161
+ }
162
+ catch (error) {
163
+ // An abort is classified by toHttpTransportError (cancelled / timed out).
164
+ if (signal?.aborted)
165
+ throw error;
166
+ throw headerFactoryFailure("Request header factory failed", error);
167
+ }
168
+ if (!isHeaderRecord(minted)) {
169
+ throw headerFactoryFailure("Request header factory must return a header record");
170
+ }
171
+ return withClientHeaderDefaults(minted, clientOptions, options.body);
172
+ }
39
173
  function parseHttpData(body, headers) {
40
174
  const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
41
175
  if (contentType?.includes("application/json")) {
@@ -49,7 +183,17 @@ function parseJson(body) {
49
183
  function isTimeoutMessage(message) {
50
184
  return /\b(timed out|timeout|deadline exceeded)\b/i.test(message);
51
185
  }
52
- function toHttpTransportError(error) {
186
+ function toHttpTransportError(error, ambientSignal, timeoutSignal) {
187
+ if (ambientSignal?.aborted) {
188
+ return toAmbientCancellationError(ambientSignal, error);
189
+ }
190
+ if (timeoutSignal?.aborted) {
191
+ return new TransportError("Request timed out", {
192
+ code: "transport_timeout",
193
+ status: 0,
194
+ ...(error instanceof Error ? { cause: error } : {}),
195
+ });
196
+ }
53
197
  if (error instanceof TransportError) {
54
198
  if (error.code) {
55
199
  return error;
@@ -121,9 +265,108 @@ function requireNativeResponseBody(response) {
121
265
  }
122
266
  return response.body;
123
267
  }
124
- function toNativeHttpStreamResponse(response) {
268
+ function mergeAbortSignals(...signals) {
269
+ const activeSignals = signals.filter((signal) => signal != null);
270
+ if (activeSignals.length === 0)
271
+ return undefined;
272
+ if (activeSignals.length === 1)
273
+ return activeSignals[0];
274
+ return AbortSignal.any(activeSignals);
275
+ }
276
+ function cancelStreamOnAbort(body, signal) {
277
+ if (!signal)
278
+ return body;
279
+ let reader;
280
+ let finished = false;
281
+ let streamController;
282
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
283
+ const onAbort = () => {
284
+ if (finished)
285
+ return;
286
+ finished = true;
287
+ cleanup();
288
+ const reason = toAmbientCancellationError(signal);
289
+ streamController?.error(reason);
290
+ const cancellation = reader ? reader.cancel(reason) : body.cancel(reason);
291
+ void cancellation.catch(() => undefined).finally(() => reader?.releaseLock());
292
+ };
293
+ return new ReadableStream({
294
+ start(controller) {
295
+ streamController = controller;
296
+ signal.addEventListener("abort", onAbort, { once: true });
297
+ if (signal.aborted)
298
+ onAbort();
299
+ },
300
+ async pull(controller) {
301
+ try {
302
+ reader ??= body.getReader();
303
+ const chunk = await reader.read();
304
+ if (finished)
305
+ return;
306
+ if (chunk.done) {
307
+ finished = true;
308
+ cleanup();
309
+ controller.close();
310
+ reader.releaseLock();
311
+ return;
312
+ }
313
+ controller.enqueue(chunk.value);
314
+ }
315
+ catch (error) {
316
+ if (finished)
317
+ return;
318
+ finished = true;
319
+ cleanup();
320
+ controller.error(error);
321
+ reader?.releaseLock();
322
+ }
323
+ },
324
+ async cancel(reason) {
325
+ if (finished)
326
+ return;
327
+ finished = true;
328
+ cleanup();
329
+ try {
330
+ await (reader ? reader.cancel(reason) : body.cancel(reason));
331
+ }
332
+ finally {
333
+ reader?.releaseLock();
334
+ }
335
+ },
336
+ }, { highWaterMark: 0 });
337
+ }
338
+ function sanitizeStreamErrors(body, serializedUrl) {
339
+ if (serializedUrl.sensitiveValues.length === 0)
340
+ return body;
341
+ let reader;
342
+ return new ReadableStream({
343
+ async pull(controller) {
344
+ try {
345
+ reader ??= body.getReader();
346
+ const chunk = await reader.read();
347
+ if (chunk.done) {
348
+ controller.close();
349
+ return;
350
+ }
351
+ controller.enqueue(chunk.value);
352
+ }
353
+ catch (error) {
354
+ controller.error(redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl));
355
+ }
356
+ },
357
+ async cancel(reason) {
358
+ try {
359
+ await (reader ? reader.cancel(reason) : body.cancel(reason));
360
+ }
361
+ catch (error) {
362
+ throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
363
+ }
364
+ },
365
+ }, { highWaterMark: 0 });
366
+ }
367
+ function toNativeHttpStreamResponse(response, serializedUrl, signal) {
125
368
  const headers = Object.fromEntries(response.headers.entries());
126
- const body = requireNativeResponseBody(response);
369
+ const body = sanitizeStreamErrors(cancelStreamOnAbort(requireNativeResponseBody(response), signal), serializedUrl);
127
370
  return {
128
371
  body,
129
372
  headers,
@@ -158,24 +401,116 @@ function normalizeHttpMethod(method) {
158
401
  });
159
402
  }
160
403
  }
161
- function isAbsoluteUrl(url) {
404
+ /** Whether `url` carries its own scheme, so it needs no `upstream.baseUrl` to resolve. */
405
+ export function isAbsoluteUrl(url) {
162
406
  return /^[a-z][a-z\d+\-.]*:/i.test(url);
163
407
  }
164
408
  function resolveHttpUrl(baseUrl, url) {
165
409
  return new URL(url, baseUrl ?? DEFAULT_HTTP_BASE_URL).toString();
166
410
  }
411
+ const MAX_HTTP_REDIRECT_HOPS = 20;
412
+ const HTTP_REDIRECT_POLICY_TRANSPORT = {
413
+ label: "ctx.http",
414
+ maxHops: MAX_HTTP_REDIRECT_HOPS,
415
+ };
416
+ const REDIRECT_BODY_HEADERS = new Set([
417
+ "content-encoding",
418
+ "content-language",
419
+ "content-location",
420
+ "content-type",
421
+ ]);
422
+ function snapshotHttpRedirectPolicy(options) {
423
+ try {
424
+ return normalizeRedirectPolicy(options.redirectPolicy, HTTP_REDIRECT_POLICY_TRANSPORT);
425
+ }
426
+ catch (error) {
427
+ if (error instanceof TransportError)
428
+ throw error;
429
+ throw invalidRedirectPolicy(HTTP_REDIRECT_POLICY_TRANSPORT, "could not be read safely", error instanceof Error ? error : undefined);
430
+ }
431
+ }
432
+ function withoutRedirectBodyHeaders(headers) {
433
+ const nextHeaders = new Headers(headers);
434
+ for (const name of REDIRECT_BODY_HEADERS)
435
+ nextHeaders.delete(name);
436
+ return nextHeaders;
437
+ }
438
+ function discardRedirectResponseBody(response) {
439
+ try {
440
+ const cancellation = response.body?.cancel();
441
+ if (cancellation)
442
+ void cancellation.catch(() => undefined);
443
+ }
444
+ catch {
445
+ // The redirect decision is security-significant and must not be replaced
446
+ // or delayed by an upstream body's cancellation failure. Cancellation was
447
+ // attempted; redirect evaluation continues without awaiting its completion.
448
+ }
449
+ }
450
+ async function fetchWithHttpRedirectPolicy(requestUrl, requestInit, policy) {
451
+ if (!policy)
452
+ return fetch(requestUrl, requestInit);
453
+ const initialUrl = new URL(requestUrl);
454
+ if (initialUrl.protocol !== "http:" && initialUrl.protocol !== "https:") {
455
+ throw new TransportError("ctx.http redirectPolicy requires an HTTP(S) origin", {
456
+ code: "transport_invalid_url",
457
+ });
458
+ }
459
+ const initialOrigin = initialUrl.origin;
460
+ let currentUrl = requestUrl;
461
+ let method = normalizeHttpMethod(requestInit.method ?? "GET");
462
+ let body = requestInit.body;
463
+ let headers = requestInit.headers;
464
+ let followedHops = 0;
465
+ const visitedRequests = new Set([`${method} ${currentUrl}`]);
466
+ while (true) {
467
+ const response = await fetch(currentUrl, {
468
+ ...requestInit,
469
+ body,
470
+ headers,
471
+ method,
472
+ redirect: "manual",
473
+ });
474
+ if (!isRedirectStatus(response.status))
475
+ return response;
476
+ const location = response.headers.get("location");
477
+ discardRedirectResponseBody(response);
478
+ const decision = nextSameOriginRedirectHop({
479
+ policy,
480
+ initialOrigin,
481
+ currentUrl,
482
+ responseUrl: currentUrl,
483
+ status: response.status,
484
+ method,
485
+ location: location ?? undefined,
486
+ followedHops,
487
+ visitedRequests,
488
+ });
489
+ if (decision.nextMethod !== method) {
490
+ body = undefined;
491
+ headers = withoutRedirectBodyHeaders(headers);
492
+ }
493
+ method = decision.nextMethod;
494
+ currentUrl = decision.nextUrl;
495
+ followedHops += 1;
496
+ visitedRequests.add(`${method} ${currentUrl}`);
497
+ }
498
+ }
167
499
  async function resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset = 0) {
168
500
  const resolvedProxy = await resolveProxyConfigAsync({
169
501
  proxy: options.proxy ?? clientOptions.proxy,
170
502
  upstream: clientOptions.upstream,
171
- apifuseConfig: clientOptions.apifuseConfig,
172
503
  proxyPolicy: clientOptions.proxyPolicy,
173
504
  affinityKey: clientOptions.affinityKey,
174
505
  proxyAttempt: computeProxyAttemptIndex({
175
506
  baseProxyAttempt: clientOptions.proxyAttempt,
176
507
  retryAttemptOffset: proxyAttemptOffset,
177
508
  }),
509
+ // Bun's native fetch proxy option tunnels HTTP CONNECT only; SOCKS5 is not
510
+ // supported here, so a socks5 policy fails loudly rather than downgrading.
511
+ transportProtocols: ["http"],
178
512
  telemetry: clientOptions.telemetry,
513
+ engineCredentials: clientOptions.engineCredentials,
179
514
  });
180
515
  if (resolvedProxy.shouldWarn) {
181
516
  warn("[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.");
@@ -196,27 +531,62 @@ function normalizeNativeFetchBody(body) {
196
531
  copied.set(normalized);
197
532
  return copied.buffer;
198
533
  }
199
- async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0) {
200
- const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
534
+ function serializeHttpRequestUrl(baseUrl, url, options) {
535
+ try {
536
+ return serializeRequestUrl(resolveHttpUrl(baseUrl, url), options.params, options.sensitiveParams);
537
+ }
538
+ catch (error) {
539
+ throw redactSensitiveRequestError(error, url, options.sensitiveParams);
540
+ }
541
+ }
542
+ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0, dedupe, observeProxy, nextHeaderAttempt = () => 1) {
543
+ const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
544
+ const { requestUrl } = serializedUrl;
201
545
  const controller = options.timeout ? new AbortController() : undefined;
202
- const timeoutHandle = options.timeout
546
+ const signal = mergeAbortSignals(options.signal, controller?.signal);
547
+ let timeoutHandle = options.timeout
203
548
  ? setTimeout(() => controller?.abort(), options.timeout)
204
549
  : undefined;
550
+ const disarmTimeout = () => {
551
+ if (!timeoutHandle)
552
+ return;
553
+ clearTimeout(timeoutHandle);
554
+ timeoutHandle = undefined;
555
+ };
205
556
  let proxy;
206
557
  try {
558
+ throwIfAmbientAborted(options.signal);
559
+ // Resolve inside the try (and after the timeout is armed) so allocator
560
+ // failures are branded as TransportErrors and count against the request
561
+ // deadline, exactly as an inline resolve would.
207
562
  proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
563
+ observeProxy?.(Boolean(proxy));
564
+ throwIfAmbientAborted(options.signal);
565
+ // For a registry allocator chain, skip an endpoint a prior attempt already
566
+ // tried rather than re-issuing the same request. Returning the sentinel
567
+ // (instead of breaking) lets the loop keep advancing the flat offset until
568
+ // it crosses into the fallback vendor's pool span.
569
+ if (dedupe && proxy) {
570
+ if (dedupe.attempted.has(proxy)) {
571
+ return { kind: "dedupe-skip" };
572
+ }
573
+ dedupe.attempted.add(proxy);
574
+ }
575
+ // Resolve after the dedupe check so a skipped duplicate offset never burns
576
+ // a single-use proof, and with the serialized URL so URL-bound proofs
577
+ // match what is actually issued.
578
+ const requestHeaders = await resolveAttemptHeaders(options, clientOptions, () => ({ attempt: nextHeaderAttempt(), url: requestUrl, method }), signal);
208
579
  const requestInit = {
209
- headers: options.headers,
580
+ headers: requestHeaders,
210
581
  method,
211
582
  ...(proxy ? { proxy } : {}),
212
- signal: controller?.signal,
583
+ ...(signal ? { signal } : {}),
213
584
  };
214
585
  if (options.body !== undefined) {
215
586
  requestInit.body = normalizeNativeFetchBody(options.body);
216
587
  }
217
- const response = await fetch(requestUrl, {
218
- ...requestInit,
219
- });
588
+ const response = await fetchWithHttpRedirectPolicy(requestUrl, requestInit, options.redirectPolicy);
589
+ observeProxy?.(Boolean(proxy), response.status);
220
590
  const headers = Object.fromEntries(response.headers.entries());
221
591
  if (statusRetryCodes && response.status >= 400) {
222
592
  await drainNativeResponseBody(response);
@@ -235,41 +605,53 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
235
605
  status: response.status,
236
606
  });
237
607
  }
238
- return toNativeHttpResponse(response);
608
+ // `timeout` bounds the header phase (types.ts RequestOptions.timeout), so it
609
+ // is disarmed before the body is read: awaiting the buffered body inside
610
+ // this try brands a mid-body failure as a transport error, but must not
611
+ // extend the deadline over the download. Cancellation still applies —
612
+ // `signal` stays attached to the native response. The discarded non-2xx
613
+ // bodies drained above stay under the deadline on purpose: nothing else
614
+ // bounds them, and their content is never returned to the caller.
615
+ disarmTimeout();
616
+ return await toNativeHttpResponse(response);
239
617
  }
240
618
  catch (error) {
241
619
  if (error instanceof SyntaxError) {
242
- throw error;
620
+ throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
243
621
  }
244
- const transportError = toHttpTransportError(error);
622
+ const transportError = redactSensitiveError(toHttpTransportError(error, options.signal, controller?.signal), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
245
623
  transportError.proxyUsed = Boolean(proxy);
246
624
  throw transportError;
247
625
  }
248
626
  finally {
249
- if (timeoutHandle)
250
- clearTimeout(timeoutHandle);
627
+ disarmTimeout();
251
628
  }
252
629
  }
253
- async function fetchNativeHttpStream(baseUrl, url, method, options, clientOptions, warn) {
254
- const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
630
+ async function fetchNativeHttpStream(baseUrl, url, method, options, clientOptions, warn, observeProxy) {
631
+ const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
632
+ const { requestUrl } = serializedUrl;
255
633
  const controller = options.timeout ? new AbortController() : undefined;
634
+ const signal = mergeAbortSignals(options.signal, controller?.signal);
256
635
  const timeoutHandle = options.timeout
257
636
  ? setTimeout(() => controller?.abort(), options.timeout)
258
637
  : undefined;
259
638
  try {
639
+ throwIfAmbientAborted(options.signal);
260
640
  const proxy = await resolveNativeProxy(options, clientOptions, warn);
641
+ observeProxy?.(Boolean(proxy));
642
+ throwIfAmbientAborted(options.signal);
643
+ // Streams never retry: a header factory is resolved once, as attempt 1.
644
+ const headers = await resolveAttemptHeaders(options, clientOptions, () => ({ attempt: 1, url: requestUrl, method }), signal);
261
645
  const requestInit = {
262
- headers: options.headers,
646
+ headers,
263
647
  method,
264
648
  ...(proxy ? { proxy } : {}),
265
- signal: controller?.signal,
649
+ ...(signal ? { signal } : {}),
266
650
  };
267
651
  if (options.body !== undefined) {
268
652
  requestInit.body = normalizeNativeFetchBody(options.body);
269
653
  }
270
- const response = await fetch(requestUrl, {
271
- ...requestInit,
272
- });
654
+ const response = await fetchWithHttpRedirectPolicy(requestUrl, requestInit, options.redirectPolicy);
273
655
  if (response.status >= 400 && options.throwOnHttpError !== false) {
274
656
  await drainNativeResponseBody(response);
275
657
  throw new TransportError(`Upstream request failed with status ${response.status}`, {
@@ -277,13 +659,15 @@ async function fetchNativeHttpStream(baseUrl, url, method, options, clientOption
277
659
  status: response.status,
278
660
  });
279
661
  }
280
- return toNativeHttpStreamResponse(response);
662
+ // The stream timeout remains header-scoped; composed ambient and local
663
+ // cancellation stays attached to native fetch for the body's full lifetime.
664
+ return toNativeHttpStreamResponse(response, serializedUrl, options.signal);
281
665
  }
282
666
  catch (error) {
283
667
  if (error instanceof SyntaxError) {
284
- throw error;
668
+ throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
285
669
  }
286
- throw toHttpTransportError(error);
670
+ throw redactSensitiveError(toHttpTransportError(error, options.signal, controller?.signal), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
287
671
  }
288
672
  finally {
289
673
  if (timeoutHandle)
@@ -301,98 +685,268 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
301
685
  warn(message);
302
686
  };
303
687
  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");
317
- const retryEnabled = Boolean(retryOptions &&
318
- retryOptions.attempts > 1 &&
319
- isProxyTransportRetryMethod(methodName, retryOptions));
320
- const statusRetryEnabled = Boolean(retryEnabled &&
321
- explicitRetry &&
322
- retryOptions &&
323
- retryOptions.statusCodes.length > 0 &&
324
- headersOptions.throwOnHttpError !== false);
325
- const attemptOptions = statusRetryEnabled
326
- ? { ...headersOptions, throwOnHttpError: false }
327
- : headersOptions;
328
- const executeOnce = (proxyAttemptOffset = 0) => fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined, proxyAttemptOffset);
329
- if (!retryEnabled || !retryOptions) {
330
- const outcome = await executeOnce();
331
- if (isHttpStatusOutcome(outcome)) {
332
- throw toUpstreamHttpError(outcome.status);
688
+ const { explicitRetry, headersOptions, methodName, retryOptions } = (() => {
689
+ try {
690
+ if (!baseUrl && !isAbsoluteUrl(url)) {
691
+ throw new TransportError("ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared", { code: "transport_invalid_url" });
692
+ }
693
+ assertNoHttpTransportOverrides(options);
694
+ const redirectPolicy = snapshotHttpRedirectPolicy(options);
695
+ const headersOptions = {
696
+ ...withClientHeaders(options, clientOptions, options.body),
697
+ redirectPolicy,
698
+ };
699
+ const methodName = normalizeHttpMethod(method);
700
+ const explicitRetry = headersOptions.retry !== undefined;
701
+ const retryOptions = normalizeProxyTransportRetryOptions(headersOptions.retry, {
702
+ label: "HTTP",
703
+ }) ??
704
+ (explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
705
+ if (retryOptions)
706
+ validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
707
+ return { explicitRetry, headersOptions, methodName, retryOptions };
333
708
  }
334
- return outcome;
709
+ catch (error) {
710
+ throw redactSensitiveRequestError(error, url, options.sensitiveParams);
711
+ }
712
+ })();
713
+ const requestStarted = performance.now();
714
+ const telemetry = startHttpTelemetry(scopedHttpTelemetry(clientOptions.httpTelemetry), retryOptions?.preset);
715
+ try {
716
+ return await runRequest();
335
717
  }
336
- let lastErrorCode;
337
- let lastStatus;
338
- for (let attempt = 1; attempt <= retryOptions.attempts; attempt += 1) {
339
- try {
340
- const outcome = await executeOnce(attempt - 1);
718
+ finally {
719
+ telemetry.finish(performance.now() - requestStarted);
720
+ }
721
+ async function runRequest() {
722
+ const retryEnabled = Boolean(retryOptions &&
723
+ retryOptions.attempts > 1 &&
724
+ isProxyTransportRetryMethod(methodName, retryOptions));
725
+ const statusRetryEnabled = Boolean(retryEnabled &&
726
+ explicitRetry &&
727
+ retryOptions &&
728
+ retryOptions.statusCodes.length > 0 &&
729
+ headersOptions.throwOnHttpError !== false);
730
+ const attemptOptions = statusRetryEnabled
731
+ ? { ...headersOptions, throwOnHttpError: false }
732
+ : headersOptions;
733
+ // Span the whole vendor chain on transport failures. Like ctx.stealth, a
734
+ // policy-managed proxy resolves a *different* endpoint/vendor per attempt
735
+ // (the flat proxyAttemptOffset rotates across the concatenated vendor pool
736
+ // spans), so a transport failure should advance to the next endpoint —
737
+ // potentially crossing into the fallback vendor — rather than stopping at
738
+ // the per-endpoint retry budget and stranding the request on the primary
739
+ // vendor. resolvePolicyTransportAttemptCap widens the cap to the chain span
740
+ // only for implicit, safe-method allocator requests; explicit retry
741
+ // policies (their documented `attempts` ceiling), unsafe methods, and
742
+ // static/non-registry vendors keep the retry budget. Status-code retries
743
+ // stay bounded by the retry budget regardless; only transport rotation gets
744
+ // the full span.
745
+ const policyProxy = (() => {
746
+ const policy = clientOptions.proxyPolicy ?? clientOptions.upstream?.proxy;
747
+ return policy && typeof policy === "object" ? policy : undefined;
748
+ })();
749
+ const usesPolicyAllocator = Boolean(policyProxy) && !options.proxy && !clientOptions.proxy;
750
+ const transportAttemptCap = retryOptions
751
+ ? resolvePolicyTransportAttemptCap({
752
+ policy: policyProxy,
753
+ usesPolicyAllocator,
754
+ retryAttempts: retryOptions.attempts,
755
+ explicitRetry,
756
+ method: methodName,
757
+ })
758
+ : 1;
759
+ // Track resolved endpoints across a policy-allocator chain. Successive
760
+ // attempts rotate the flat offset across the concatenated vendor pool
761
+ // spans, but an under-filled allocation (fewer live endpoints than the
762
+ // configured pool size) makes the modulo mapping repeat endpoints before
763
+ // the offset reaches the next vendor. Rather than re-hammering an
764
+ // already-tried endpoint under backoff, fetchNativeHttp returns a skip
765
+ // sentinel for a duplicate; the loop then advances the flat offset without
766
+ // issuing the request, so it keeps walking toward — and into — the fallback
767
+ // vendor's pool span instead of stalling on the primary vendor.
768
+ // De-duplication is gated on the SAME predicate that widens the attempt cap
769
+ // (implicit, safe-method, registry-chain rotation). It must NOT engage for
770
+ // an explicit retry policy: there the caller's `attempts` count is the
771
+ // contract and each attempt must issue against whatever endpoint it resolves
772
+ // — even a repeat — instead of being silently skipped (which would collapse
773
+ // a `poolSize: 1` + `attempts: 3` request to a single fetch).
774
+ const dedupeAllocatorEndpoints = policyRotatesTransportVendorChain({
775
+ policy: policyProxy,
776
+ usesPolicyAllocator,
777
+ explicitRetry,
778
+ method: methodName,
779
+ });
780
+ const dedupeContext = dedupeAllocatorEndpoints ? { attempted: new Set() } : undefined;
781
+ // Numbers the attempts a header factory sees: only requests that reached
782
+ // header resolution count, so a failed proxy allocation (which increments
783
+ // the telemetry `issued` count below) or a dedupe-skipped offset does not
784
+ // make the first request that is actually built report `attempt: 2`.
785
+ let headerAttempts = 0;
786
+ const nextHeaderAttempt = () => {
787
+ headerAttempts += 1;
788
+ return headerAttempts;
789
+ };
790
+ const executeOnce = async (proxyAttemptOffset = 0) => {
791
+ const started = performance.now();
792
+ let proxyUsed = false;
793
+ let observedStatus;
794
+ const record = (event) => {
795
+ telemetry.recordAttempt({ ...event, ms: performance.now() - started, proxyUsed });
796
+ };
797
+ try {
798
+ const outcome = await fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined, proxyAttemptOffset, dedupeContext, (used, status) => {
799
+ proxyUsed = used;
800
+ observedStatus = status;
801
+ }, nextHeaderAttempt);
802
+ if (!isDedupeSkipOutcome(outcome))
803
+ record({
804
+ status: outcome.status,
805
+ ...(isHttpStatusOutcome(outcome)
806
+ ? { e: "upstream_http_error", statusRetry: true }
807
+ : {}),
808
+ });
809
+ return outcome;
810
+ }
811
+ catch (error) {
812
+ // Record before the retry loop can catch/rethrow a terminal status.
813
+ record(httpAttemptFailure(error, observedStatus));
814
+ throw error;
815
+ }
816
+ };
817
+ if (!retryEnabled || !retryOptions) {
818
+ throwIfAmbientAborted(headersOptions.signal);
819
+ const outcome = await executeOnce();
820
+ if (isDedupeSkipOutcome(outcome)) {
821
+ // Single-shot path never de-duplicates (dedupeContext is undefined),
822
+ // but keep the union total.
823
+ throw new TransportError("HTTP request produced no terminal result", {
824
+ code: "retry_exhausted",
825
+ });
826
+ }
341
827
  if (isHttpStatusOutcome(outcome)) {
342
- lastStatus = outcome.status;
343
- if (outcome.retryable && attempt < retryOptions.attempts) {
344
- await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers));
345
- continue;
346
- }
347
828
  throw toUpstreamHttpError(outcome.status);
348
829
  }
349
- const response = outcome;
350
- if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
351
- throw toUpstreamHttpError(response.status);
830
+ return outcome;
831
+ }
832
+ let lastError;
833
+ // `attempt` walks the flat proxy offset across the full chain span; `issued`
834
+ // counts requests that were actually sent (skipped duplicate offsets do not
835
+ // increment it). Retry summaries and the status-retry budget must reflect
836
+ // issued requests, not the raw offset, so they stay accurate when partial
837
+ // allocations skip offsets.
838
+ let issued = 0;
839
+ for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
840
+ throwIfAmbientAborted(headersOptions.signal);
841
+ // Whether this offset actually issued a request (vs. a skipped duplicate),
842
+ // so the catch counts a thrown *transport* failure once without
843
+ // double-counting a status outcome that already incremented before it
844
+ // re-threw as an upstream HTTP error.
845
+ let issuedThisAttempt = false;
846
+ try {
847
+ const outcome = await executeOnce(attempt - 1);
848
+ if (isDedupeSkipOutcome(outcome)) {
849
+ // Duplicate endpoint from a partial allocation: advance the flat
850
+ // offset without issuing the request (no backoff, not a failure) so
851
+ // the loop keeps rotating toward the fallback vendor.
852
+ continue;
853
+ }
854
+ issued += 1;
855
+ issuedThisAttempt = true;
856
+ if (isHttpStatusOutcome(outcome)) {
857
+ if (outcome.retryable && issued < retryOptions.attempts) {
858
+ await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers), headersOptions.signal);
859
+ continue;
860
+ }
861
+ throw toUpstreamHttpError(outcome.status);
862
+ }
863
+ const response = outcome;
864
+ if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
865
+ throw toUpstreamHttpError(response.status);
866
+ }
867
+ if (issued > 1) {
868
+ const summary = telemetry.toTenantRetryPayload();
869
+ if (summary)
870
+ telemetry.observe(() => clientOptions.onRetrySummary?.(summary));
871
+ }
872
+ return response;
352
873
  }
353
- if (attempt > 1) {
354
- const summary = {
355
- attempts: attempt,
356
- retries: attempt - 1,
357
- ...(retryOptions.preset ? { preset: retryOptions.preset } : {}),
358
- transport: "native",
359
- ...(lastErrorCode ? { lastErrorCode } : {}),
360
- ...(lastStatus ? { lastStatus } : {}),
361
- };
362
- clientOptions.onRetrySummary?.(summary);
874
+ catch (error) {
875
+ throwIfAmbientAborted(headersOptions.signal);
876
+ if (!issuedThisAttempt)
877
+ issued += 1;
878
+ lastError = error;
879
+ const proxyUsed = Boolean(error.proxyUsed);
880
+ if (attempt < transportAttemptCap &&
881
+ shouldRetryProxyTransportAttempt({
882
+ error,
883
+ explicitRetry,
884
+ method: methodName,
885
+ options: retryOptions,
886
+ proxyUsed,
887
+ })) {
888
+ await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt), headersOptions.signal);
889
+ continue;
890
+ }
891
+ throw error;
363
892
  }
364
- return response;
365
893
  }
366
- catch (error) {
367
- lastErrorCode = proxyTransportRetryErrorCode(error);
368
- lastStatus = proxyTransportRetryErrorStatus(error);
369
- const proxyUsed = Boolean(error.proxyUsed);
370
- if (attempt < retryOptions.attempts &&
371
- shouldRetryProxyTransportAttempt({
372
- error,
373
- explicitRetry,
374
- method: methodName,
375
- options: retryOptions,
376
- proxyUsed,
377
- })) {
378
- await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt));
379
- continue;
380
- }
381
- throw error;
894
+ // Reached when the attempt cap is consumed without a terminal outcome —
895
+ // e.g. the final offsets of a partial allocation all resolved to
896
+ // already-tried endpoints and were skipped. Surface the last real transport
897
+ // failure rather than a synthetic exhaustion error.
898
+ if (lastError !== undefined) {
899
+ throw lastError;
382
900
  }
901
+ throw new TransportError("HTTP retry exhausted without a terminal result", {
902
+ code: "retry_exhausted",
903
+ });
383
904
  }
384
- throw new TransportError("HTTP retry exhausted without a terminal result", {
385
- code: "retry_exhausted",
386
- });
387
905
  }
388
906
  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" });
907
+ const { headersOptions, methodName } = (() => {
908
+ try {
909
+ if (!baseUrl && !isAbsoluteUrl(url)) {
910
+ throw new TransportError("ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared", { code: "transport_invalid_url" });
911
+ }
912
+ assertNoHttpTransportOverrides(options);
913
+ const redirectPolicy = snapshotHttpRedirectPolicy(options);
914
+ return {
915
+ headersOptions: {
916
+ ...withClientHeaders(options, clientOptions, options.body),
917
+ redirectPolicy,
918
+ },
919
+ methodName: normalizeHttpMethod(method),
920
+ };
921
+ }
922
+ catch (error) {
923
+ throw redactSensitiveRequestError(error, url, options.sensitiveParams);
924
+ }
925
+ })();
926
+ // Streams never retry. Observe the header fetch without consuming its body.
927
+ const telemetry = startHttpTelemetry(scopedHttpTelemetry(clientOptions.httpTelemetry), "off");
928
+ const started = performance.now();
929
+ const alreadyAborted = headersOptions.signal?.aborted === true;
930
+ let proxyUsed = false;
931
+ const record = (event) => {
932
+ if (alreadyAborted)
933
+ return;
934
+ telemetry.recordAttempt({ ...event, ms: performance.now() - started, proxyUsed });
935
+ };
936
+ try {
937
+ const response = await fetchNativeHttpStream(baseUrl, url, methodName, headersOptions, clientOptions, warnOnce, (used) => {
938
+ proxyUsed = used;
939
+ });
940
+ record({ status: response.status });
941
+ return response;
942
+ }
943
+ catch (error) {
944
+ record(httpAttemptFailure(error));
945
+ throw error;
946
+ }
947
+ finally {
948
+ telemetry.finish(performance.now() - started);
391
949
  }
392
- assertNoHttpTransportOverrides(options);
393
- const headersOptions = withClientHeaders(options, clientOptions, options.body);
394
- const methodName = normalizeHttpMethod(method);
395
- return fetchNativeHttpStream(baseUrl, url, methodName, headersOptions, clientOptions, warnOnce);
396
950
  }
397
951
  return {
398
952
  request: async (url, options = {}) => request(url, options.method ?? "GET", options),
@@ -402,10 +956,15 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
402
956
  delete: async (url, options) => request(url, "DELETE", options),
403
957
  stream: async (url, options = {}) => streamRequest(url, options.method ?? "GET", options),
404
958
  sse: async (url, options = {}) => {
405
- const headers = {
406
- Accept: "text/event-stream",
407
- ...options.headers,
408
- };
959
+ const callerHeaders = options.headers;
960
+ const headers = typeof callerHeaders === "function"
961
+ ? async (attempt) => {
962
+ // Pass a malformed result through unchanged so the factory
963
+ // validation rejects it instead of spreading it into `{}`.
964
+ const minted = await callerHeaders(attempt);
965
+ return isHeaderRecord(minted) ? { Accept: "text/event-stream", ...minted } : minted;
966
+ }
967
+ : { Accept: "text/event-stream", ...callerHeaders };
409
968
  const response = await streamRequest(url, options.method ?? "GET", {
410
969
  ...options,
411
970
  headers,