@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
@@ -0,0 +1,4596 @@
1
+ import { bindCapabilityRoot } from "../runtime/capability-telemetry.js";
2
+ import { STATUS_CODES } from "node:http";
3
+ import { readDiagnosticEnv, withDiagnosticEnv } from "../runtime/diagnostic-env.js";
4
+ import { AsyncLocalStorage } from "node:async_hooks";
5
+ import { createHash } from "node:crypto";
6
+ import { existsSync } from "node:fs";
7
+ import { createRequire } from "node:module";
8
+ import { join } from "node:path";
9
+
10
+ import { Hono } from "hono";
11
+ import { z } from "zod";
12
+ import { AuthAbortError, createAuthFlowHelpers } from "../auth.js";
13
+ import { validateFailClosedDeclaration } from "../declaration-validation.js";
14
+ import {
15
+ ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAMES,
16
+ isEngineOwnedEnvName,
17
+ type ProviderEngine,
18
+ type ProviderEngineBindingCandidates,
19
+ type ProviderEngineMode,
20
+ readEngineProxyCredentials,
21
+ } from "../engine.js";
22
+ import {
23
+ createEngineForMode,
24
+ isUnavailableProviderEngine,
25
+ type ProviderEngineModeResolution,
26
+ resolveProviderEngineMode,
27
+ } from "../runtime/engine-mode.js";
28
+ import { SDK_VERSION } from "./sdk-version.js";
29
+ import { safeProviderErrorObservability } from "../error-observability.js";
30
+ import { providerErrorStackFrames } from "./error-stack-frames.js";
31
+ import {
32
+ registeredUpstreamErrorCategory,
33
+ SDK_OWNED_PROVIDER_ERROR_CODES,
34
+ SDK_RUNTIME_OWNED_ERROR_CODES,
35
+ SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES,
36
+ } from "../error-resolution.js";
37
+ import {
38
+ AuthError,
39
+ isProviderError,
40
+ isSessionExpiredError,
41
+ isTransportError,
42
+ isValidationError,
43
+ ProviderError,
44
+ SDKError,
45
+ type ProviderErrorObservability,
46
+ type ProviderErrorOptions,
47
+ } from "../errors.js";
48
+ import { sanitizeDiagnosticText } from "../fixture-sanitization.js";
49
+ import {
50
+ loadProviderLocaleCatalogs,
51
+ localizeAuthTurn,
52
+ type ProviderLocaleCatalogMap,
53
+ } from "../i18n/catalog.js";
54
+ import {
55
+ acceptLanguageHeaderValue,
56
+ DEFAULT_PROVIDER_ERROR_LOCALE,
57
+ localizeProviderErrorText,
58
+ PROVIDER_ERROR_LOCALES,
59
+ type ProviderErrorMessageParams,
60
+ type ProviderErrorTextCandidate,
61
+ resolveProviderErrorLocale,
62
+ } from "../i18n/error-messages.js";
63
+ import type { ProviderLocale } from "../i18n/keys.js";
64
+ import {
65
+ categoryForStatus,
66
+ isRetryableCategory,
67
+ PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
68
+ type ProviderErrorCategory,
69
+ type ProviderErrorSource,
70
+ sourceForCategory,
71
+ } from "../observability.js";
72
+ import { createScratchpad } from "../runtime/auth-flow.js";
73
+ import type * as BrowserRuntimeModule from "../runtime/browser.js";
74
+ import { createProviderCache } from "../runtime/cache.js";
75
+ import { bindCacheTelemetry, CacheTelemetryCollector } from "../runtime/cache-telemetry.js";
76
+ import type { HandleTelemetryEvent } from "../handle.js";
77
+ import { createHandleContext } from "../runtime/handle.js";
78
+ import { createCredentialContext } from "../runtime/credential.js";
79
+ import {
80
+ bindDiagnosticSensitiveRegistry,
81
+ type CompiledDiagnosticSensitiveValues,
82
+ compileProcessDiagnosticSensitiveValues,
83
+ createDiagnosticRedactor,
84
+ type DiagnosticRedactor,
85
+ REDACTION_FAILED,
86
+ isDiagnosticRedactionSuppressed,
87
+ bindCriticalDiagnosticRedactor,
88
+ redactCriticalDiagnosticText,
89
+ redactDiagnosticText,
90
+ registerDiagnosticSensitiveValues,
91
+ } from "../runtime/diagnostic-redactor.js";
92
+ import {
93
+ createCeremonyEgressLeaseRuntime,
94
+ ENGINE_CEREMONY_EGRESS_LEASE,
95
+ } from "../runtime/egress-lease.js";
96
+ import { createEnvContext } from "../runtime/env.js";
97
+ import { executeOperation } from "../runtime/executor.js";
98
+ import { createHttpClient } from "../runtime/http.js";
99
+ import { NativeTelemetryCollector } from "../runtime/native-telemetry.js";
100
+ import {
101
+ bindHttpTelemetry,
102
+ isSupportedHttpBinding,
103
+ warnUnsupportedHttpBinding,
104
+ } from "../runtime/http-telemetry-binding.js";
105
+ import { HttpTelemetryCollector, type HttpTelemetryLogPayload } from "../runtime/http-telemetry.js";
106
+ import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
107
+ import type * as NativeNetworkRuntimeModule from "../runtime/native-network.js";
108
+ import { createOcrClientFromEnv } from "../runtime/ocr.js";
109
+ import { bindOcrTelemetry, OcrTelemetryCollector } from "../runtime/ocr-telemetry.js";
110
+ import { getProviderBaseUrl, getProviderStealthBaseUrl } from "../runtime/provider.js";
111
+ import {
112
+ PROXY_AUTH_IP_DENIED_CODE,
113
+ PROXY_EDGE_AUTH_REJECTED_CODE,
114
+ PROXY_POOL_EXHAUSTED_CODE,
115
+ } from "../runtime/proxy-errors.js";
116
+ import {
117
+ PROVIDER_TELEMETRY_HEADER,
118
+ ProxyTelemetryCollector,
119
+ type ProxyTelemetryLogPayload,
120
+ } from "../runtime/proxy-telemetry.js";
121
+ import {
122
+ type ClosedEnum,
123
+ closedEnum,
124
+ RequestTelemetry,
125
+ type TenantNeutral,
126
+ type TenantOpaqueKeys,
127
+ tenantOpaqueKeys,
128
+ } from "../runtime/request-telemetry.js";
129
+ import type * as ResolverRuntimeModule from "../runtime/resolver.js";
130
+ import { resolverSpanAttributes } from "../runtime/resolver-span-attributes.js";
131
+ import {
132
+ bindResolverTelemetry,
133
+ createUnsupportedResolverClient,
134
+ inheritResolverTelemetryBinding,
135
+ type ResolverSolveWithRecorder,
136
+ } from "../runtime/resolver-shared.js";
137
+ import {
138
+ ResolverTelemetryCollector,
139
+ type ResolverTelemetryLogPayload,
140
+ } from "../runtime/resolver-telemetry.js";
141
+ import {
142
+ createResolverRuntimeOptions,
143
+ resolveNativeProxyPolicy,
144
+ } from "../runtime/resolver-runtime-options.js";
145
+ import {
146
+ assertRequiredSecretsPresent,
147
+ listMissingRequiredSecrets,
148
+ MISSING_SECRET_CODE,
149
+ } from "../runtime/secrets.js";
150
+ import {
151
+ createProviderRuntimeStateFromEnv,
152
+ createUnsupportedProviderRuntimeState,
153
+ } from "../runtime/state.js";
154
+ import {
155
+ StateTelemetryCollector,
156
+ instrumentProviderRuntimeState,
157
+ } from "../runtime/state-telemetry.js";
158
+ import type * as StealthRuntimeModule from "../runtime/stealth.js";
159
+ import type { StealthChallengeRuntime } from "../runtime/stealth-akamai-sbsd.js";
160
+ import { StealthCookieJar } from "../runtime/stealth-cookies.js";
161
+ import { StealthTelemetryCollector } from "../runtime/stealth-telemetry.js";
162
+ import { BrowserTelemetryCollector } from "../runtime/browser-telemetry.js";
163
+ import {
164
+ bindBrowserTelemetry,
165
+ isSupportedBrowserBinding,
166
+ warnUnsupportedBrowserBinding,
167
+ } from "../runtime/browser-telemetry-binding.js";
168
+ import { createSttClientFromEnv } from "../runtime/stt.js";
169
+ import { bindSttTelemetry, SttTelemetryCollector } from "../runtime/stt-telemetry.js";
170
+ import {
171
+ createTraceContext,
172
+ getTraceRecorder,
173
+ type TraceContext as RuntimeTraceContext,
174
+ type TraceRecorder,
175
+ updateTraceContextExportMetadata,
176
+ } from "../runtime/trace.js";
177
+ import { resolveTraceConfigFromEnv } from "../runtime/trace-config.js";
178
+ import { parseSchema } from "../schema.js";
179
+ import { StatefulRoutingDeadlineError } from "../stateful/errors.js";
180
+ import {
181
+ STATEFUL_NONCE_HEADER as STATEFUL_FORWARDING_NONCE_HEADER,
182
+ STATEFUL_SIGNATURE_HEADER as STATEFUL_FORWARDING_SIGNATURE_HEADER,
183
+ STATEFUL_TIMESTAMP_HEADER as STATEFUL_FORWARDING_TIMESTAMP_HEADER,
184
+ verifyStatefulRequestSignature,
185
+ } from "../stateful-signing.js";
186
+ import { getStealthProfile } from "../stealth/profiles.js";
187
+ import {
188
+ APIFUSE_STREAM_DONE_EVENT,
189
+ APIFUSE_STREAM_ERROR_EVENT,
190
+ encodeSseEvent,
191
+ error as streamError,
192
+ } from "../stream.js";
193
+ import { sanitizeSpanNameForOutput, sanitizeTraceAttributes } from "../trace-sanitization.js";
194
+ import type {
195
+ AuthContext,
196
+ AuthTurn,
197
+ BrowserClient,
198
+ FlowContext,
199
+ FlowContextStore,
200
+ OcrContext,
201
+ OperationDefinition,
202
+ OperationErrorCode,
203
+ OperationHttpStreamTransport,
204
+ OperationSseTransport,
205
+ ProviderCacheResponseMeta,
206
+ ProviderContext,
207
+ ProviderDefinition,
208
+ ProviderErrorStatus,
209
+ ProviderFilesContext,
210
+ ProviderRuntimeState,
211
+ ProviderRuntimeTarget,
212
+ ProviderStreamEvent,
213
+ ResolverContext,
214
+ StealthClient,
215
+ StealthSession,
216
+ StealthSessionCookies,
217
+ SttContext,
218
+ } from "../types.js";
219
+ import { VALID_OPERATION_ERROR_STATUSES } from "../types.js";
220
+ import type { SelfTestCancellationLogEvent } from "./self-test.js";
221
+ import {
222
+ type ProviderStateBackendLogEvent,
223
+ providerStateBackendLogEvent,
224
+ } from "./state-backend-report.js";
225
+ import {
226
+ collectRuntimeDiagnostics,
227
+ resolveRuntimeDiagnosticsWindowMs,
228
+ RUNTIME_DIAGNOSTICS_ROUTE,
229
+ RUNTIME_DIAGNOSTICS_WINDOW_QUERY,
230
+ } from "./runtime-diagnostics.js";
231
+ import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
232
+ import {
233
+ collectStaticDiagnosticSensitiveValues,
234
+ createDiagnosticEnvObserver,
235
+ } from "./sensitive-values.js";
236
+ import { resolveServerTraceContextOptions } from "./trace-output.js";
237
+ import {
238
+ type AuthFlowRequest,
239
+ AuthFlowRequestSchema,
240
+ type AuthFlowResponse,
241
+ type AuthFlowSuccessResponse,
242
+ OperationConnectionSchema,
243
+ type OperationErrorResponse,
244
+ type OperationRequest,
245
+ OperationRequestSchema,
246
+ type OperationResponse,
247
+ type OperationSuccessResponse,
248
+ } from "./types.js";
249
+
250
+ const DEFAULT_HOST = "0.0.0.0";
251
+ const DEFAULT_PORT = 3000;
252
+ /** Compact SDK-owned error classification emitted separately from the public response body. */
253
+ export const ERROR_OBSERVABILITY_HEADER = "X-ApiFuse-Error-Observability";
254
+ export type ErrorObservabilityDetails = {
255
+ category: ProviderErrorCategory;
256
+ taxonomyVersion: string;
257
+ retryable: boolean;
258
+ upstreamStatus?: number;
259
+ providerObservability?: ProviderErrorObservability;
260
+ };
261
+
262
+ // Provider errors normally expose `options` through an own data property, but
263
+ // providers can replace that property with a throwing accessor. Provider-controlled
264
+ // accessor failures are absorbed into canonical classification rather than
265
+ // propagated. By contract, read `options` directly instead of the public
266
+ // `fix`/`details` convenience getters; provider-controlled accessors are not trusted.
267
+ function providerErrorOption<K extends keyof ProviderErrorOptions>(
268
+ error: unknown,
269
+ key: K,
270
+ ): ProviderErrorOptions[K] | undefined {
271
+ if (!isProviderError(error)) return undefined;
272
+ try {
273
+ return (error as ProviderError).options?.[key] as ProviderErrorOptions[K] | undefined;
274
+ } catch {
275
+ return undefined;
276
+ }
277
+ }
278
+
279
+ function providerErrorCode(error: unknown): string | undefined {
280
+ if (!isProviderError(error)) return undefined;
281
+ try {
282
+ const code: unknown = (error as ProviderError).code;
283
+ return typeof code === "string" ? code : undefined;
284
+ } catch {
285
+ return undefined;
286
+ }
287
+ }
288
+
289
+ const retryResponseMeta = new WeakMap<ProviderContext, HttpTelemetryCollector>();
290
+ const STATEFUL_INTERNAL_OPERATIONS_ROUTE = "/__apifuse/stateful/operations";
291
+ const STATEFUL_FORWARDING_SOURCE_POD_HEADER = "x-apifuse-stateful-source-pod";
292
+ const DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS = 5 * 60_000;
293
+ const DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES = 10_000;
294
+ const STATEFUL_FORWARDING_REPLAY_BUCKET_MS = 10_000;
295
+ const STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS = Math.ceil(
296
+ STATEFUL_FORWARDING_REPLAY_BUCKET_MS / 1_000,
297
+ );
298
+
299
+ export const ProviderServerStatefulForwardEnvelopeSchema = z
300
+ .object({
301
+ requestId: z.string().min(1),
302
+ providerId: z.string().min(1),
303
+ operationId: z.string().min(1),
304
+ sessionKey: z.string().min(1),
305
+ connectionId: z.string().min(1),
306
+ serviceAccountId: z.string().min(1),
307
+ ownerPodId: z.string().min(1),
308
+ generation: z.number().int().positive(),
309
+ sourcePodId: z.string().min(1),
310
+ forwardedAt: z.string().refine((value) => Number.isFinite(Date.parse(value))),
311
+ deadlineAt: z
312
+ .string()
313
+ .refine((value) => Number.isFinite(Date.parse(value)))
314
+ .optional(),
315
+ idempotencyKey: z.string().min(1).optional(),
316
+ operationRequest: OperationRequestSchema.extend({
317
+ connection: OperationConnectionSchema.strict().optional(),
318
+ }).strict(),
319
+ })
320
+ .strict();
321
+
322
+ // Inbound compatibility boundary for rolling deploys, mirroring the outbound
323
+ // one in stateful-provider-owner-forwarder.ts: emitted envelopes stay strict
324
+ // (above), while a received envelope's nested operation request keeps the
325
+ // gateway route's tolerance and strips unknown keys. A source pod on a newer
326
+ // SDK carrying an additive OperationRequestSchema field (tenantId, #164) must
327
+ // not be rejected with STATEFUL_FORWARDING_ENVELOPE_INVALID by an owner pod
328
+ // that predates the field: the rejection is terminal for the operation and the
329
+ // two pods coexist for the whole rolling update. The envelope's own keys stay
330
+ // strict on both sides, because they are protocol rather than payload.
331
+ const ReceivedStatefulForwardEnvelopeSchema =
332
+ ProviderServerStatefulForwardEnvelopeSchema.extend({
333
+ operationRequest: z.object(
334
+ ProviderServerStatefulForwardEnvelopeSchema.shape.operationRequest.shape,
335
+ ),
336
+ });
337
+
338
+ export type ProviderServerStatefulForwardEnvelope = Readonly<
339
+ z.infer<typeof ProviderServerStatefulForwardEnvelopeSchema>
340
+ >;
341
+
342
+ export type ProviderServerStatefulOwnerFence = Readonly<
343
+ Pick<
344
+ ProviderServerStatefulForwardEnvelope,
345
+ | "providerId"
346
+ | "sessionKey"
347
+ | "ownerPodId"
348
+ | "generation"
349
+ | "sourcePodId"
350
+ | "forwardedAt"
351
+ | "requestId"
352
+ | "idempotencyKey"
353
+ >
354
+ >;
355
+
356
+ export type ProviderServerStatefulOwnerFenceValidator = (
357
+ fence: ProviderServerStatefulOwnerFence,
358
+ signal: AbortSignal,
359
+ ) => boolean | Promise<boolean>;
360
+
361
+ export type ProviderServerOperationExecutorInput<
362
+ TContext extends Partial<ProviderContext> = ProviderContext,
363
+ > = {
364
+ readonly provider: ProviderDefinition<TContext>;
365
+ readonly operationId: string;
366
+ readonly ctx: TContext;
367
+ readonly request: OperationRequest & { readonly deadlineAt?: string };
368
+ readonly signal?: AbortSignal;
369
+ readonly internalStatefulForward?: ProviderServerStatefulForwardEnvelope;
370
+ };
371
+
372
+ export type ProviderServerOperationExecutor<
373
+ TContext extends Partial<ProviderContext> = ProviderContext,
374
+ > = (input: ProviderServerOperationExecutorInput<TContext>) => Promise<unknown>;
375
+
376
+ type RequestTerminalOutcome =
377
+ | { kind: "completed"; status: number }
378
+ | { kind: "failed"; status?: number; error: unknown }
379
+ | { kind: "cancelled"; status?: number };
380
+
381
+ type RequestStreamLifecycle = {
382
+ redact: DiagnosticRedactor;
383
+ runStep<T>(fn: () => Promise<T>): Promise<T>;
384
+ registerCleanup(cleanup: () => void | Promise<void>): void;
385
+ terminalize(outcome: RequestTerminalOutcome): void;
386
+ cleanup(): Promise<void>;
387
+ };
388
+
389
+ type ProviderCapabilityModules = {
390
+ readonly browser?: typeof BrowserRuntimeModule;
391
+ readonly nativeNetwork?: typeof NativeNetworkRuntimeModule;
392
+ readonly resolver?: typeof ResolverRuntimeModule;
393
+ readonly stealth?: typeof StealthRuntimeModule;
394
+ };
395
+
396
+ type ProviderServerRuntimeOptions = ProviderServerOptions & {
397
+ readonly capabilityModules: ProviderCapabilityModules;
398
+ readonly engine: ProviderEngine;
399
+ };
400
+
401
+ const require = createRequire(import.meta.url);
402
+
403
+ type CapabilityLoadFailure = {
404
+ readonly capability: string;
405
+ readonly error: unknown;
406
+ };
407
+
408
+ function normalizedCapabilityCause(error: unknown): Error {
409
+ if (error instanceof Error) return error;
410
+ return new Error(String(error));
411
+ }
412
+
413
+ function capabilityLoadError(
414
+ provider: ProviderDefinition,
415
+ failures: readonly CapabilityLoadFailure[],
416
+ ): ProviderError {
417
+ const normalizedFailures = failures.map(({ capability, error }) => ({
418
+ capability,
419
+ cause: normalizedCapabilityCause(error),
420
+ }));
421
+ const summary = normalizedFailures
422
+ .map(({ capability, cause }) => `${capability} (${cause.message})`)
423
+ .join(", ");
424
+ return new ProviderError(
425
+ `Failed to load declared capabilities for provider "${provider.id}": ${summary}`,
426
+ {
427
+ code: "PROVIDER_CAPABILITY_LOAD_FAILED",
428
+ details: {
429
+ providerId: provider.id,
430
+ failures: normalizedFailures.map(({ capability, cause }) => ({
431
+ capability,
432
+ reason: cause.message,
433
+ })),
434
+ },
435
+ cause: new AggregateError(
436
+ normalizedFailures.map(({ cause }) => cause),
437
+ `Provider capability loading failed for ${provider.id}`,
438
+ ),
439
+ },
440
+ );
441
+ }
442
+
443
+ function requireCapabilityModule<T>(
444
+ provider: ProviderDefinition,
445
+ capability: string,
446
+ specifier: string,
447
+ ): T {
448
+ try {
449
+ return require(specifier) as T;
450
+ } catch (error) {
451
+ if (error && typeof error === "object" && "code" in error && error.code === "ERR_REQUIRE_ESM") {
452
+ throw new ProviderError(
453
+ `Synchronous capability loading is unavailable for provider "${provider.id}" on this runtime. Use createServerAppAsync() instead of createServerApp().`,
454
+ {
455
+ code: "PROVIDER_CAPABILITY_SYNC_LOAD_UNSUPPORTED",
456
+ details: { providerId: provider.id, capability },
457
+ cause: normalizedCapabilityCause(error),
458
+ },
459
+ );
460
+ }
461
+ throw capabilityLoadError(provider, [{ capability, error }]);
462
+ }
463
+ }
464
+
465
+ function createAuthStub(): AuthContext {
466
+ return {
467
+ async requestField(name) {
468
+ throw new ProviderError(`Auth prompt is unavailable for ${name}`, {
469
+ code: "AUTH_PROMPT_UNAVAILABLE",
470
+ });
471
+ },
472
+ };
473
+ }
474
+
475
+ function createBrowserStub(): BrowserClient {
476
+ return {
477
+ engine: "playwright-stealth",
478
+ async close() {},
479
+ async newPage() {
480
+ throw new ProviderError("Browser runtime is not available", {
481
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
482
+ });
483
+ },
484
+ async rawPage() {
485
+ throw new ProviderError("Browser runtime is not available", {
486
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
487
+ });
488
+ },
489
+ async withIsolatedContext() {
490
+ throw new ProviderError("Browser runtime is not available", {
491
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
492
+ });
493
+ },
494
+ async solveChallenge() {
495
+ throw new ProviderError("Browser runtime is not available", {
496
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
497
+ });
498
+ },
499
+ };
500
+ }
501
+
502
+ function createStealthStub(): StealthClient {
503
+ return {
504
+ async fetch() {
505
+ throw new ProviderError("Stealth runtime is not available", {
506
+ code: "STEALTH_RUNTIME_UNSUPPORTED",
507
+ });
508
+ },
509
+ createSession() {
510
+ throw new ProviderError("Stealth runtime is not available", {
511
+ code: "STEALTH_RUNTIME_UNSUPPORTED",
512
+ });
513
+ },
514
+ close() {
515
+ // no-op
516
+ },
517
+ };
518
+ }
519
+
520
+ let stealthRuntimeModulePromise: Promise<typeof StealthRuntimeModule> | undefined;
521
+
522
+ function importStealthRuntime(): Promise<typeof StealthRuntimeModule> {
523
+ stealthRuntimeModulePromise ??= import("../runtime/stealth.js");
524
+ return stealthRuntimeModulePromise;
525
+ }
526
+
527
+ function createForwardingStealthCookies(
528
+ initialTarget: StealthSessionCookies,
529
+ ): StealthSessionCookies & { setTarget(target: StealthSessionCookies): void } {
530
+ let target = initialTarget;
531
+ return {
532
+ setTarget(nextTarget) {
533
+ target = nextTarget;
534
+ },
535
+ get: (...args) => target.get(...args),
536
+ getAll: (...args) => target.getAll(...args),
537
+ has: (...args) => target.has(...args),
538
+ setFromCookieStrings: (...args) => target.setFromCookieStrings(...args),
539
+ toString: (url?: string) => target.toString(url),
540
+ toHeader: (...args) => target.toHeader(...args),
541
+ snapshot: () => target.snapshot(),
542
+ restore: (...args) => target.restore(...args),
543
+ serialize: () => target.serialize(),
544
+ deserialize: (...args) => target.deserialize(...args),
545
+ clear: () => target.clear(),
546
+ find: (...args) => target.find?.(...args),
547
+ };
548
+ }
549
+
550
+ function createLazyStealthClient(
551
+ onCleanupError: (error: unknown) => void,
552
+ ...createArgs: Parameters<typeof StealthRuntimeModule.createStealthClient>
553
+ ): StealthClient {
554
+ let client: StealthClient | undefined;
555
+ let clientPromise: Promise<StealthClient> | undefined;
556
+
557
+ function getClient(): Promise<StealthClient> {
558
+ clientPromise ??= importStealthRuntime().then((runtime) => {
559
+ client ??= runtime.createStealthClient(...createArgs);
560
+ return client;
561
+ });
562
+ return clientPromise;
563
+ }
564
+
565
+ return {
566
+ async fetch(...args) {
567
+ return (await getClient()).fetch(...args);
568
+ },
569
+ createSession(options) {
570
+ const localCookies = new StealthCookieJar([], createArgs[0]);
571
+ const cookies = createForwardingStealthCookies(localCookies);
572
+ let closed = false;
573
+ let session: StealthSession | undefined;
574
+ let sessionPromise: Promise<StealthSession> | undefined;
575
+
576
+ function getSession(): Promise<StealthSession> {
577
+ sessionPromise ??= getClient().then((realClient) => {
578
+ session = realClient.createSession(options);
579
+ session.cookies.deserialize(localCookies.serialize());
580
+ cookies.setTarget(session.cookies);
581
+ if (closed) session.close();
582
+ return session;
583
+ });
584
+ return sessionPromise;
585
+ }
586
+
587
+ return {
588
+ cookies,
589
+ async fetch(...args) {
590
+ return (await getSession()).fetch(...args);
591
+ },
592
+ async replayChallenged(...args) {
593
+ return (await getSession()).replayChallenged(...args);
594
+ },
595
+ redirects: {
596
+ async run(...args) {
597
+ return (await getSession()).redirects.run(...args);
598
+ },
599
+ },
600
+ close() {
601
+ closed = true;
602
+ session?.close();
603
+ },
604
+ };
605
+ },
606
+ close() {
607
+ client?.close?.();
608
+ if (!client && clientPromise) {
609
+ void clientPromise.then(
610
+ (loadedClient) => {
611
+ try {
612
+ loadedClient.close?.();
613
+ } catch (error) {
614
+ try {
615
+ onCleanupError(error);
616
+ } catch {
617
+ // A user logger must not turn handled cleanup into a process-level rejection.
618
+ }
619
+ }
620
+ },
621
+ () => {
622
+ // The request path owns reporting for a failed lazy import.
623
+ },
624
+ );
625
+ }
626
+ },
627
+ };
628
+ }
629
+
630
+ function bindResolverSignalWithoutRuntime(
631
+ resolver: ResolverContext,
632
+ defaultSignal: AbortSignal | undefined,
633
+ ): ResolverContext {
634
+ if (!defaultSignal) return resolver;
635
+ const boundResolver: ResolverContext = {
636
+ solve(challenge, signal = defaultSignal, traceRecorder?: TraceRecorder) {
637
+ // Forward the instrumentation trace recorder so resolver.vendor.* spans
638
+ // survive this wrapper. See ResolverSolveWithRecorder in resolver-shared.ts.
639
+ return (resolver as Partial<ResolverSolveWithRecorder> & ResolverContext).solve(
640
+ challenge,
641
+ signal,
642
+ traceRecorder,
643
+ );
644
+ },
645
+ };
646
+ inheritResolverTelemetryBinding(resolver, boundResolver);
647
+ return boundResolver;
648
+ }
649
+
650
+ function getProviderStealthProfile(provider: ProviderDefinition) {
651
+ return provider.stealth ? getStealthProfile(provider.stealth) : undefined;
652
+ }
653
+
654
+ type ResolverRuntimeOptions = ResolverRuntimeModule.ResolverRuntimeOptions;
655
+
656
+ /** Shared predicate for the SBSD challenge runtime and the ceremony egress lease. */
657
+ function declaresAkamaiSbsdResolver(provider: ProviderDefinition): boolean {
658
+ return provider.resolver?.kinds.some((kind) => kind === "akamai_sbsd") === true;
659
+ }
660
+
661
+ function createStealthChallengeDetection(
662
+ provider: ProviderDefinition,
663
+ resolverRuntime: typeof ResolverRuntimeModule | undefined,
664
+ resolverOverride: ResolverContext | undefined,
665
+ resolverOptions: ResolverRuntimeOptions,
666
+ trace: RuntimeTraceContext,
667
+ signal: AbortSignal | undefined,
668
+ ): StealthChallengeRuntime | undefined {
669
+ const resolverDeclared = declaresAkamaiSbsdResolver(provider);
670
+ const detectOnly = provider.stealth?.challengeDetection?.akamaiSbsd === true;
671
+ if (!resolverDeclared && !detectOnly) {
672
+ return undefined;
673
+ }
674
+ return {
675
+ akamaiSbsd: {
676
+ allowedHosts: provider.allowedHosts ?? [],
677
+ // The profile belongs to the SBSD resolver; a resolver declared for other kinds
678
+ // may name a different browser without affecting detect-only classification.
679
+ ...(resolverDeclared && provider.resolver?.clientProfile
680
+ ? { clientProfile: provider.resolver.clientProfile }
681
+ : {}),
682
+ ...(resolverDeclared && resolverRuntime
683
+ ? {
684
+ async solve(challenge, transport, solveSignal) {
685
+ const resolver = resolverRuntime.bindResolverSignal(
686
+ (resolverOverride
687
+ ? bindResolverTelemetry(
688
+ resolverOverride,
689
+ resolverOptions.telemetry,
690
+ resolverOptions.browserTelemetry,
691
+ )
692
+ : undefined) ??
693
+ resolverRuntime.createResolverClientFromEnv(provider.resolver, undefined, {
694
+ allowedHosts: resolverOptions.allowedHosts,
695
+ cache: resolverOptions.cache,
696
+ telemetry: resolverOptions.telemetry,
697
+ browserTelemetry: resolverOptions.browserTelemetry,
698
+ identityScope: resolverOptions.identityScope,
699
+ // No proxyIntent: the transport is already bound to the initiating
700
+ // request's lease, and a second identity resolution would allocate
701
+ // again (and fail closed without allocator credentials) for nothing
702
+ // the SBSD adapters consume.
703
+ createTransport: () => transport,
704
+ }),
705
+ signal,
706
+ ) as ResolverContext & Partial<ResolverSolveWithRecorder>;
707
+ // Same span and recorder threading as the instrumented ctx.resolver.solve.
708
+ const recorder = getTraceRecorder(trace);
709
+ const solve = () => resolver.solve(challenge, solveSignal, recorder ?? undefined);
710
+ return recorder
711
+ ? recorder.runSpan("resolver.solve", solve, {
712
+ attributes: resolverSpanAttributes({ challenge_kind: challenge.kind }),
713
+ })
714
+ : solve();
715
+ },
716
+ }
717
+ : {}),
718
+ },
719
+ };
720
+ }
721
+
722
+ function isProductionProviderBrowserMode(provider: ProviderDefinition, env = process.env): boolean {
723
+ if (provider.runtime !== "browser") {
724
+ return false;
725
+ }
726
+
727
+ if (env.APIFUSE__PROVIDER__RUNTIME === "browser") {
728
+ return true;
729
+ }
730
+
731
+ return env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id;
732
+ }
733
+
734
+ function declaresStealthRuntime(provider: ProviderDefinition): boolean {
735
+ return provider.stealth !== undefined;
736
+ }
737
+
738
+ async function loadProviderCapabilityModules(
739
+ provider: ProviderDefinition,
740
+ ): Promise<ProviderCapabilityModules> {
741
+ const results = await Promise.allSettled([
742
+ provider.runtime === "browser" ? import("../runtime/browser.js") : Promise.resolve(undefined),
743
+ provider.native ? import("../runtime/native-network.js") : Promise.resolve(undefined),
744
+ provider.resolver ? import("../runtime/resolver.js") : Promise.resolve(undefined),
745
+ declaresStealthRuntime(provider) ? import("../runtime/stealth.js") : Promise.resolve(undefined),
746
+ ]);
747
+ const capabilityNames = ["browser", "native", "resolver", "stealth"] as const;
748
+ const failures: CapabilityLoadFailure[] = [];
749
+ for (const [index, result] of results.entries()) {
750
+ if (result.status === "rejected") {
751
+ failures.push({ capability: capabilityNames[index]!, error: result.reason });
752
+ }
753
+ }
754
+ if (failures.length > 0) throw capabilityLoadError(provider, failures);
755
+ return {
756
+ browser: results[0].status === "fulfilled" ? results[0].value : undefined,
757
+ nativeNetwork: results[1].status === "fulfilled" ? results[1].value : undefined,
758
+ resolver: results[2].status === "fulfilled" ? results[2].value : undefined,
759
+ stealth: results[3].status === "fulfilled" ? results[3].value : undefined,
760
+ };
761
+ }
762
+
763
+ function loadProviderCapabilityModulesSync(
764
+ provider: ProviderDefinition,
765
+ ): ProviderCapabilityModules {
766
+ return {
767
+ browser:
768
+ provider.runtime === "browser"
769
+ ? requireCapabilityModule<typeof BrowserRuntimeModule>(
770
+ provider,
771
+ "browser",
772
+ "../runtime/browser.js",
773
+ )
774
+ : undefined,
775
+ nativeNetwork: provider.native
776
+ ? requireCapabilityModule<typeof NativeNetworkRuntimeModule>(
777
+ provider,
778
+ "native",
779
+ "../runtime/native-network.js",
780
+ )
781
+ : undefined,
782
+ resolver: provider.resolver
783
+ ? requireCapabilityModule<typeof ResolverRuntimeModule>(
784
+ provider,
785
+ "resolver",
786
+ "../runtime/resolver.js",
787
+ )
788
+ : undefined,
789
+ stealth: declaresStealthRuntime(provider)
790
+ ? requireCapabilityModule<typeof StealthRuntimeModule>(
791
+ provider,
792
+ "stealth",
793
+ "../runtime/stealth.js",
794
+ )
795
+ : undefined,
796
+ };
797
+ }
798
+
799
+ export function resolveProviderProxyAffinityKey(
800
+ provider: ProviderDefinition,
801
+ request: OperationRequest,
802
+ operationId: string,
803
+ ): string {
804
+ const connectionKey = resolveOperationConnectionId(request) ?? request.connection?.externalRef;
805
+ const affinity =
806
+ typeof provider.proxy === "object" ? provider.proxy.session?.affinity : undefined;
807
+ if (affinity === "operation") {
808
+ return `${provider.id}/${operationId}`;
809
+ }
810
+ return connectionKey ?? provider.id;
811
+ }
812
+
813
+ export function resolveProviderResolverIdentityScope(
814
+ provider: ProviderDefinition,
815
+ affinityKey: string,
816
+ contextId: string,
817
+ ): string {
818
+ return JSON.stringify({
819
+ proxy: provider.proxy ?? null,
820
+ affinityKey,
821
+ contextId,
822
+ });
823
+ }
824
+
825
+ function resolveOperationConnectionId(
826
+ request: Pick<OperationRequest, "connection" | "connectionId">,
827
+ ): string | undefined {
828
+ // An empty string is a malformed identifier, not an identity: treat it as
829
+ // absent so it can never override a valid id or key a real scope. Requests
830
+ // without any usable id fall back to the documented missing-connection
831
+ // sentinel scope instead of scoping context/affinity/state under "".
832
+ return normalizeIdentifier(request.connection?.id) ?? normalizeIdentifier(request.connectionId);
833
+ }
834
+
835
+ function resolveOperationTenantId(request: Pick<OperationRequest, "tenantId">): string | undefined {
836
+ // Only the gateway-asserted envelope field is consulted; caller headers can
837
+ // never populate the principal scope. Same rule as connection ids: "" is a
838
+ // malformed identifier, not a principal.
839
+ return normalizeIdentifier(request.tenantId);
840
+ }
841
+
842
+ function normalizeIdentifier(id: string | undefined): string | undefined {
843
+ return id === "" ? undefined : id;
844
+ }
845
+
846
+ function providerSecretNames(provider: ProviderDefinition): string[] {
847
+ return (provider.secrets?.map((secret) => secret.name) ?? []).filter(
848
+ (name) => !isEngineOwnedEnvName(name),
849
+ );
850
+ }
851
+
852
+ type RequestScopeContext = {
853
+ run<T>(fn: () => Promise<T>): Promise<T>;
854
+ redact: DiagnosticRedactor;
855
+ trace: RuntimeTraceContext;
856
+ telemetry: RequestTelemetry;
857
+ resolverTelemetry: ResolverTelemetryCollector;
858
+ nativeTelemetry: NativeTelemetryCollector;
859
+ httpTelemetry: HttpTelemetryCollector;
860
+ stealthTelemetry: StealthTelemetryCollector;
861
+ ocrTelemetry: OcrTelemetryCollector;
862
+ sttTelemetry: SttTelemetryCollector;
863
+ browserTelemetry: BrowserTelemetryCollector;
864
+ cacheTelemetry: CacheTelemetryCollector;
865
+ stateTelemetry: StateTelemetryCollector;
866
+ };
867
+
868
+ function createProviderContext(
869
+ provider: ProviderDefinition,
870
+ request: OperationRequest,
871
+ operationId: string,
872
+ options: ProviderServerRuntimeOptions,
873
+ state: ProviderRuntimeState = createUnsupportedProviderRuntimeState(),
874
+ scope: RequestScopeContext,
875
+ signal?: AbortSignal,
876
+ ): ProviderContext {
877
+ const baseUrl = getProviderBaseUrl(provider);
878
+ const stealthBaseUrl = getProviderStealthBaseUrl(provider);
879
+ const stealthProfile = getProviderStealthProfile(provider);
880
+ const proxyPolicy = resolveNativeProxyPolicy(provider);
881
+ const engineProxyCredentials = readEngineProxyCredentials();
882
+ const proxyClientOptions = {
883
+ upstream: { proxy: provider.proxy },
884
+ affinityKey: resolveProviderProxyAffinityKey(provider, request, operationId),
885
+ telemetry: scope.telemetry.proxy,
886
+ engineCredentials: engineProxyCredentials,
887
+ };
888
+ const resolverIdentityScope = resolveProviderResolverIdentityScope(
889
+ provider,
890
+ proxyClientOptions.affinityKey,
891
+ request.requestId,
892
+ );
893
+ const cache = createProviderCache(
894
+ bindCacheTelemetry({ providerId: provider.id }, scope.cacheTelemetry),
895
+ );
896
+ const { capabilityModules } = options;
897
+ const resolverOptions = createResolverRuntimeOptions(
898
+ provider,
899
+ cache,
900
+ resolverIdentityScope,
901
+ proxyPolicy,
902
+ proxyClientOptions,
903
+ stealthProfile,
904
+ scope.resolverTelemetry,
905
+ scope.browserTelemetry,
906
+ );
907
+ const challengeRuntime = createStealthChallengeDetection(
908
+ provider,
909
+ capabilityModules.resolver,
910
+ options.resolver,
911
+ resolverOptions,
912
+ scope.trace,
913
+ signal,
914
+ );
915
+ const stealthClientOptions = {
916
+ stealthTelemetry: scope.stealthTelemetry,
917
+ upstream: proxyClientOptions.upstream,
918
+ affinityKey: proxyClientOptions.affinityKey,
919
+ telemetry: scope.telemetry.proxy,
920
+ engineCredentials: engineProxyCredentials,
921
+ ...(signal ? { signal } : {}),
922
+ // A declared resolver without a `stealth` block still gets the default Chrome client
923
+ // (auth FlowContext is not capability-gated), so the runtime must ride along.
924
+ ...(provider.stealth || challengeRuntime
925
+ ? {
926
+ stealth: {
927
+ ...(provider.stealth ?? {}),
928
+ ...(challengeRuntime ? { challengeRuntime } : {}),
929
+ },
930
+ }
931
+ : {}),
932
+ };
933
+ const logStealthCleanupError = (error: unknown) =>
934
+ logProviderCleanupError(
935
+ options.logger,
936
+ provider,
937
+ "operation",
938
+ operationId,
939
+ request.requestId,
940
+ "stealth",
941
+ error,
942
+ scope.redact,
943
+ );
944
+
945
+ const env = createEnvContext([...providerSecretNames(provider)]);
946
+ const credential = createCredentialContext({
947
+ allowedKeys: provider.credential?.keys,
948
+ mode: request.connection?.mode,
949
+ scopes: request.connection?.scopes,
950
+ values: request.connection?.secrets,
951
+ });
952
+ const operationTenantId = resolveOperationTenantId(request);
953
+ const requestContext = {
954
+ connectionId: resolveOperationConnectionId(request),
955
+ // Absent stays absent: an own property with an undefined value would
956
+ // change the shape of ctx.request for every existing consumer.
957
+ ...(operationTenantId !== undefined ? { tenantId: operationTenantId } : {}),
958
+ headers: request.headers ?? {},
959
+ };
960
+ const requestState = instrumentProviderRuntimeState(
961
+ state,
962
+ scope.stateTelemetry,
963
+ scope.redact,
964
+ ).forConnection(requestContext.connectionId);
965
+ const bindings: ProviderEngineBindingCandidates = {
966
+ env,
967
+ credential,
968
+ ...(options.files ? { files: options.files } : {}),
969
+ request: requestContext,
970
+ http: createHttpClient(baseUrl, {
971
+ ...proxyClientOptions,
972
+ ...(signal ? { signal } : {}),
973
+ httpTelemetry: scope.httpTelemetry,
974
+ }),
975
+ cache,
976
+ state: requestState,
977
+ stealth: stealthBaseUrl
978
+ ? capabilityModules.stealth
979
+ ? capabilityModules.stealth.createStealthClient(stealthBaseUrl, stealthClientOptions)
980
+ : createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, stealthClientOptions)
981
+ : createStealthStub(),
982
+ browser:
983
+ provider.runtime === "browser"
984
+ ? capabilityModules.browser!.createBrowserClient({
985
+ allowedHosts: provider.allowedHosts,
986
+ cdpUrl: readDiagnosticEnv("APIFUSE__CDP_POOL__URL"),
987
+ headless: true,
988
+ requireCdpPool: isProductionProviderBrowserMode(provider),
989
+ stealth: true,
990
+ engine: provider.browser?.engine,
991
+ telemetry: scope.browserTelemetry,
992
+ })
993
+ : createBrowserStub(),
994
+ ...(provider.native
995
+ ? {
996
+ native: {
997
+ network: capabilityModules.nativeNetwork!.createNativeNetworkClient({
998
+ telemetry: scope.telemetry.proxy,
999
+ nativeTelemetry: scope.nativeTelemetry,
1000
+ egress: provider.native.network,
1001
+ proxyPolicy: resolveNativeProxyPolicy(provider),
1002
+ affinityKey: proxyClientOptions.affinityKey,
1003
+ credentials: capabilityModules.nativeNetwork!.createEnvVendorCredentialResolver(
1004
+ createEnvContext([...ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAMES]),
1005
+ ),
1006
+ }),
1007
+ },
1008
+ }
1009
+ : {}),
1010
+ trace: scope.trace,
1011
+ auth: createAuthStub(),
1012
+ ocr: bindCapabilityRoot(
1013
+ bindOcrTelemetry(options.ocr ?? createOcrClientFromEnv(provider.ocr), scope.ocrTelemetry),
1014
+ scope.run,
1015
+ ),
1016
+ stt: bindCapabilityRoot(
1017
+ bindSttTelemetry(options.stt ?? createSttClientFromEnv(provider.stt), scope.sttTelemetry),
1018
+ scope.run,
1019
+ ),
1020
+ resolver: capabilityModules.resolver
1021
+ ? capabilityModules.resolver.bindResolverSignal(
1022
+ (options.resolver
1023
+ ? bindResolverTelemetry(
1024
+ options.resolver,
1025
+ resolverOptions.telemetry,
1026
+ resolverOptions.browserTelemetry,
1027
+ )
1028
+ : undefined) ??
1029
+ capabilityModules.resolver.createResolverClientFromEnv(
1030
+ provider.resolver,
1031
+ undefined,
1032
+ resolverOptions,
1033
+ ),
1034
+ signal,
1035
+ )
1036
+ : bindResolverSignalWithoutRuntime(
1037
+ (options.resolver
1038
+ ? bindResolverTelemetry(
1039
+ options.resolver,
1040
+ resolverOptions.telemetry,
1041
+ resolverOptions.browserTelemetry,
1042
+ )
1043
+ : undefined) ??
1044
+ createUnsupportedResolverClient("Provider does not declare resolver capability"),
1045
+ signal,
1046
+ ),
1047
+ handle: createHandleContext({
1048
+ providerId: provider.id,
1049
+ request: requestContext,
1050
+ state: requestState,
1051
+ onTelemetry: (event) =>
1052
+ (options.logger ?? defaultProviderServerLogger)({
1053
+ level: "info",
1054
+ event: "provider_handle",
1055
+ ...event,
1056
+ }),
1057
+ }),
1058
+ };
1059
+ const candidateHttp = bindings.http;
1060
+ const candidateBrowser = bindings.browser;
1061
+ const attached = options.engine.attach({ provider, bindings }) as ProviderContext;
1062
+ const hostHttp = provider.http ? attached.http : undefined;
1063
+ const supportedHostHttp = provider.http && isSupportedHttpBinding(hostHttp);
1064
+ if (provider.http && !supportedHostHttp) warnUnsupportedHttpBinding();
1065
+ const observedHttp =
1066
+ supportedHostHttp && hostHttp !== candidateHttp
1067
+ ? bindHttpTelemetry(hostHttp, scope.httpTelemetry)
1068
+ : undefined;
1069
+ const hostBrowser = provider.runtime === "browser" ? attached.browser : undefined;
1070
+ const supportedHostBrowser =
1071
+ provider.runtime === "browser" && isSupportedBrowserBinding(hostBrowser);
1072
+ if (provider.runtime === "browser" && !supportedHostBrowser) warnUnsupportedBrowserBinding();
1073
+ const observedBrowser =
1074
+ supportedHostBrowser && hostBrowser !== candidateBrowser
1075
+ ? bindBrowserTelemetry(hostBrowser, scope.browserTelemetry)
1076
+ : undefined;
1077
+ const contextTarget =
1078
+ observedBrowser || observedHttp
1079
+ ? new Proxy(attached, {
1080
+ get(target, property, receiver) {
1081
+ if (property === "http" && observedHttp) return observedHttp;
1082
+ if (property === "http" && provider.http && !supportedHostHttp) return undefined;
1083
+ if (property === "browser" && observedBrowser) return observedBrowser;
1084
+ return Reflect.get(target, property, receiver);
1085
+ },
1086
+ })
1087
+ : provider.http && !supportedHostHttp
1088
+ ? new Proxy(attached, {
1089
+ get(target, property, receiver) {
1090
+ return property === "http" ? undefined : Reflect.get(target, property, receiver);
1091
+ },
1092
+ })
1093
+ : attached;
1094
+ const context = wrapWithInstrumentation(contextTarget);
1095
+ retryResponseMeta.set(context, scope.httpTelemetry);
1096
+ return context;
1097
+ }
1098
+
1099
+ function createFlowContextStore(
1100
+ allowedKeys: string[],
1101
+ initialContext: Record<string, unknown> = {},
1102
+ ): {
1103
+ context: FlowContextStore;
1104
+ getPatch: () => Record<string, unknown | null> | undefined;
1105
+ } {
1106
+ const context = createScratchpad(allowedKeys, initialContext);
1107
+
1108
+ return {
1109
+ context,
1110
+ getPatch() {
1111
+ const next = context.toJSON();
1112
+ const patch = new Map<string, unknown | null>();
1113
+
1114
+ for (const [key, value] of Object.entries(next)) {
1115
+ if (initialContext[key] !== value) {
1116
+ patch.set(key, value);
1117
+ }
1118
+ }
1119
+
1120
+ for (const key of Object.keys(initialContext)) {
1121
+ if (!(key in next)) {
1122
+ patch.set(key, null);
1123
+ }
1124
+ }
1125
+
1126
+ if (patch.size === 0) {
1127
+ return undefined;
1128
+ }
1129
+
1130
+ return Object.fromEntries(patch.entries());
1131
+ },
1132
+ };
1133
+ }
1134
+
1135
+ export function resolveAuthFlowProxyAffinityKey(
1136
+ provider: ProviderDefinition,
1137
+ request: Pick<
1138
+ AuthFlowRequest,
1139
+ "connection" | "connectionId" | "externalRef" | "tenantId" | "providerId"
1140
+ >,
1141
+ ): string {
1142
+ return (
1143
+ resolveOperationConnectionId(request) ??
1144
+ request.externalRef ??
1145
+ request.tenantId ??
1146
+ request.providerId ??
1147
+ provider.id
1148
+ );
1149
+ }
1150
+
1151
+ function createAuthFlowContext(
1152
+ provider: ProviderDefinition,
1153
+ request: AuthFlowRequest,
1154
+ route: AuthRoute,
1155
+ options: ProviderServerRuntimeOptions,
1156
+ state: ProviderRuntimeState,
1157
+ scope: RequestScopeContext,
1158
+ signal?: AbortSignal,
1159
+ ): {
1160
+ context: FlowContext;
1161
+ getPatch: () => Record<string, unknown | null> | undefined;
1162
+ getEngineState: () => AuthFlowSuccessResponse["engine"];
1163
+ } {
1164
+ const baseUrl = getProviderBaseUrl(provider);
1165
+ const stealthBaseUrl = getProviderStealthBaseUrl(provider);
1166
+ const stealthProfile = getProviderStealthProfile(provider);
1167
+ const proxyPolicy = resolveNativeProxyPolicy(provider);
1168
+ const engineProxyCredentials = readEngineProxyCredentials();
1169
+ const contextData = request.context ?? {};
1170
+ const flowContextStore = createFlowContextStore(
1171
+ provider.context?.keys ?? Object.keys(contextData),
1172
+ contextData,
1173
+ );
1174
+ const proxyClientOptions = {
1175
+ upstream: { proxy: provider.proxy },
1176
+ affinityKey: resolveAuthFlowProxyAffinityKey(provider, request),
1177
+ telemetry: scope.telemetry.proxy,
1178
+ engineCredentials: engineProxyCredentials,
1179
+ };
1180
+ // The lease pins the proxy egress a challenged ceremony solved on, so only providers
1181
+ // that declare the akamai_sbsd resolver kind carry one (same predicate as the challenge
1182
+ // runtime). `abort` never evaluates the key or tenant: a user must be able to cancel a
1183
+ // stranded flow even when engine provisioning is broken, and an abort is not a
1184
+ // continuation of the ceremony the lease pins.
1185
+ const ceremonyEgressLease =
1186
+ route !== "abort" &&
1187
+ declaresAkamaiSbsdResolver(provider) &&
1188
+ proxyPolicy &&
1189
+ proxyPolicy.mode !== "disabled"
1190
+ ? createCeremonyEgressLeaseRuntime({
1191
+ tenantId: request.tenantId,
1192
+ providerId: provider.id,
1193
+ flowId: request.flowId,
1194
+ affinityKey: proxyClientOptions.affinityKey,
1195
+ onHandle: (handle) => registerDiagnosticSensitiveValues(scope.trace, [handle]),
1196
+ ...(request.engine?.egressLease ? { handle: request.engine.egressLease } : {}),
1197
+ })
1198
+ : undefined;
1199
+ if (route !== "abort" && request.engine?.egressLease && !ceremonyEgressLease) {
1200
+ throw new SDKError(
1201
+ "An egress lease was supplied for an auth flow that does not use a ceremony lease",
1202
+ { code: "EGRESS_LEASE_INVALID" },
1203
+ );
1204
+ }
1205
+ const resolverIdentityScope = resolveProviderResolverIdentityScope(
1206
+ provider,
1207
+ proxyClientOptions.affinityKey,
1208
+ request.requestId,
1209
+ );
1210
+ const cache = createProviderCache(
1211
+ bindCacheTelemetry({ providerId: provider.id }, scope.cacheTelemetry),
1212
+ );
1213
+ const { capabilityModules } = options;
1214
+ const resolverOptions = createResolverRuntimeOptions(
1215
+ provider,
1216
+ cache,
1217
+ resolverIdentityScope,
1218
+ proxyPolicy,
1219
+ proxyClientOptions,
1220
+ stealthProfile,
1221
+ scope.resolverTelemetry,
1222
+ scope.browserTelemetry,
1223
+ );
1224
+ const challengeRuntime = createStealthChallengeDetection(
1225
+ provider,
1226
+ capabilityModules.resolver,
1227
+ options.resolver,
1228
+ resolverOptions,
1229
+ scope.trace,
1230
+ signal,
1231
+ );
1232
+ const stealthClientOptions = {
1233
+ stealthTelemetry: scope.stealthTelemetry,
1234
+ upstream: proxyClientOptions.upstream,
1235
+ affinityKey: proxyClientOptions.affinityKey,
1236
+ telemetry: scope.telemetry.proxy,
1237
+ engineCredentials: engineProxyCredentials,
1238
+ ...(ceremonyEgressLease ? { [ENGINE_CEREMONY_EGRESS_LEASE]: ceremonyEgressLease } : {}),
1239
+ ...(signal ? { signal } : {}),
1240
+ // A declared resolver without a `stealth` block still gets the default Chrome client
1241
+ // (auth FlowContext is not capability-gated), so the runtime must ride along.
1242
+ ...(provider.stealth || challengeRuntime
1243
+ ? {
1244
+ stealth: {
1245
+ ...(provider.stealth ?? {}),
1246
+ ...(challengeRuntime ? { challengeRuntime } : {}),
1247
+ },
1248
+ }
1249
+ : {}),
1250
+ };
1251
+ const logStealthCleanupError = (error: unknown) =>
1252
+ logProviderCleanupError(
1253
+ options.logger,
1254
+ provider,
1255
+ "auth",
1256
+ "flow",
1257
+ request.requestId,
1258
+ "stealth",
1259
+ error,
1260
+ scope.redact,
1261
+ );
1262
+ const credential = request.connection
1263
+ ? createCredentialContext({
1264
+ allowedKeys: provider.credential?.keys,
1265
+ mode: request.connection.mode,
1266
+ scopes: request.connection.scopes,
1267
+ values: request.connection.secrets,
1268
+ })
1269
+ : undefined;
1270
+ const context: FlowContext = wrapWithInstrumentation({
1271
+ flowId: request.flowId,
1272
+ connectionId: resolveOperationConnectionId(request),
1273
+ externalRef: request.externalRef,
1274
+ tenantId: request.tenantId ?? "",
1275
+ providerId: request.providerId ?? provider.id,
1276
+ trace: scope.trace,
1277
+ http: createHttpClient(baseUrl, {
1278
+ ...proxyClientOptions,
1279
+ ...(signal ? { signal } : {}),
1280
+ httpTelemetry: scope.httpTelemetry,
1281
+ }),
1282
+ state: instrumentProviderRuntimeState(
1283
+ state,
1284
+ scope.stateTelemetry,
1285
+ scope.redact,
1286
+ ).forConnection(resolveOperationConnectionId(request)),
1287
+ stealth: stealthBaseUrl
1288
+ ? capabilityModules.stealth
1289
+ ? capabilityModules.stealth.createStealthClient(stealthBaseUrl, stealthClientOptions)
1290
+ : createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, stealthClientOptions)
1291
+ : createStealthStub(),
1292
+ browser:
1293
+ provider.runtime === "browser"
1294
+ ? capabilityModules.browser!.createBrowserClient({
1295
+ allowedHosts: provider.allowedHosts,
1296
+ cdpUrl: readDiagnosticEnv("APIFUSE__CDP_POOL__URL"),
1297
+ headless: true,
1298
+ requireCdpPool: isProductionProviderBrowserMode(provider),
1299
+ stealth: true,
1300
+ engine: provider.browser?.engine,
1301
+ telemetry: scope.browserTelemetry,
1302
+ })
1303
+ : createBrowserStub(),
1304
+ ...(provider.native
1305
+ ? {
1306
+ native: {
1307
+ network: capabilityModules.nativeNetwork!.createNativeNetworkClient({
1308
+ telemetry: scope.telemetry.proxy,
1309
+ nativeTelemetry: scope.nativeTelemetry,
1310
+ egress: provider.native.network,
1311
+ proxyPolicy: resolveNativeProxyPolicy(provider),
1312
+ affinityKey: proxyClientOptions.affinityKey,
1313
+ credentials: capabilityModules.nativeNetwork!.createEnvVendorCredentialResolver(
1314
+ createEnvContext([...ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAMES]),
1315
+ ),
1316
+ }),
1317
+ },
1318
+ }
1319
+ : {}),
1320
+ env: createEnvContext([
1321
+ ...providerSecretNames(provider),
1322
+ ...(provider.auth?.mode === "oauth2_proxied" ? ["APIFUSE__AUTH_PROXY__URL"] : []),
1323
+ ]),
1324
+ credential,
1325
+ context: flowContextStore.context,
1326
+ ocr: bindCapabilityRoot(
1327
+ bindOcrTelemetry(options.ocr ?? createOcrClientFromEnv(provider.ocr), scope.ocrTelemetry),
1328
+ scope.run,
1329
+ ),
1330
+ stt: bindCapabilityRoot(
1331
+ bindSttTelemetry(options.stt ?? createSttClientFromEnv(provider.stt), scope.sttTelemetry),
1332
+ scope.run,
1333
+ ),
1334
+ resolver: capabilityModules.resolver
1335
+ ? capabilityModules.resolver.bindResolverSignal(
1336
+ (options.resolver
1337
+ ? bindResolverTelemetry(
1338
+ options.resolver,
1339
+ resolverOptions.telemetry,
1340
+ resolverOptions.browserTelemetry,
1341
+ )
1342
+ : undefined) ??
1343
+ capabilityModules.resolver.createResolverClientFromEnv(
1344
+ provider.resolver,
1345
+ undefined,
1346
+ resolverOptions,
1347
+ ),
1348
+ signal,
1349
+ )
1350
+ : bindResolverSignalWithoutRuntime(
1351
+ (options.resolver
1352
+ ? bindResolverTelemetry(
1353
+ options.resolver,
1354
+ resolverOptions.telemetry,
1355
+ resolverOptions.browserTelemetry,
1356
+ )
1357
+ : undefined) ??
1358
+ createUnsupportedResolverClient("Provider does not declare resolver capability"),
1359
+ signal,
1360
+ ),
1361
+ auth: createAuthFlowHelpers({ signal }),
1362
+ });
1363
+
1364
+ return {
1365
+ context,
1366
+ getPatch: flowContextStore.getPatch,
1367
+ getEngineState() {
1368
+ const egressLease = ceremonyEgressLease?.handle();
1369
+ return egressLease ? { egressLease } : undefined;
1370
+ },
1371
+ };
1372
+ }
1373
+
1374
+ type ProviderRequestCost = {
1375
+ durationMs: number;
1376
+ cpuUserMicros: number;
1377
+ cpuSystemMicros: number;
1378
+ cpuTotalMicros: number;
1379
+ };
1380
+
1381
+ type RequestCorrelationIds = {
1382
+ connectionId?: string;
1383
+ flowId?: string;
1384
+ tenantId?: string;
1385
+ requestedProviderId?: string;
1386
+ };
1387
+
1388
+ type ProviderServerLogEventBase = ProviderRequestCost & {
1389
+ providerId: string;
1390
+ kind: "operation" | "auth";
1391
+ route: string;
1392
+ requestId?: string;
1393
+ connectionId?: string;
1394
+ flowId?: string;
1395
+ tenantId?: string;
1396
+ requestedProviderId?: string;
1397
+ status: number;
1398
+ proxy?: ProxyTelemetryLogPayload;
1399
+ resolver?: ResolverTelemetryLogPayload;
1400
+ http?: HttpTelemetryLogPayload;
1401
+ };
1402
+
1403
+ export type ProviderServerLogEvent =
1404
+ | (ProviderServerLogEventBase & {
1405
+ level: "info";
1406
+ event: "provider_request_completed";
1407
+ })
1408
+ | (ProviderServerLogEventBase & {
1409
+ level: "warn" | "error";
1410
+ event: "provider_request_failed";
1411
+ code: string;
1412
+ errorClass: string;
1413
+ message: string;
1414
+ upstreamStatus?: number;
1415
+ errorCategory?: ProviderErrorCategory;
1416
+ taxonomyVersion?: string;
1417
+ retryable?: boolean;
1418
+ providerObservability?: ProviderErrorObservability;
1419
+ causeChain?: ProviderErrorCauseFrame[];
1420
+ stack?: string[];
1421
+ signal?: "unregistered_provider_error_code";
1422
+ signalFix?: string;
1423
+ issues?: Array<{ path: string; code: string; message: string }>;
1424
+ })
1425
+ | {
1426
+ level: "warn";
1427
+ event: "provider_secrets_missing";
1428
+ providerId: string;
1429
+ missingSecrets: string[];
1430
+ }
1431
+ | {
1432
+ /** Emitted once at boot when `locales/en.json` exists but cannot be read. */
1433
+ level: "warn";
1434
+ event: "provider_locale_catalogs_unavailable";
1435
+ providerId: string;
1436
+ reason: string;
1437
+ }
1438
+ | ({
1439
+ level: "info";
1440
+ event: "provider_handle";
1441
+ } & HandleTelemetryEvent)
1442
+ | {
1443
+ level: "warn";
1444
+ event: "provider_cleanup_failed";
1445
+ providerId: string;
1446
+ kind: "operation" | "auth";
1447
+ route: string;
1448
+ requestId?: string;
1449
+ resource: "browser" | "stealth";
1450
+ errorClass: string;
1451
+ message: string;
1452
+ }
1453
+ | {
1454
+ level: "error";
1455
+ event: "provider_shutdown_hook_failed";
1456
+ providerId: string;
1457
+ hookIndex: number;
1458
+ errorClass: string;
1459
+ message: string;
1460
+ }
1461
+ | {
1462
+ /** `warn` when the requested attachment could not be honoured, otherwise `info`. */
1463
+ level: "info" | "warn";
1464
+ event: "provider_engine_mode";
1465
+ providerId: string;
1466
+ /** Resolved attachment; `custom` for an opaque host engine. */
1467
+ mode: ProviderEngineMode | "custom";
1468
+ source: "engine" | "env" | "option" | "default";
1469
+ /** True for every non-remote attachment, whose removal is scheduled by ADR-0011. */
1470
+ deprecated: boolean;
1471
+ /** False when the engine refuses to attach; `/readyz` reports 503. */
1472
+ attached: boolean;
1473
+ sdkVersion: string;
1474
+ runtimeTarget?: ProviderRuntimeTarget;
1475
+ /**
1476
+ * Non-fatal selection findings, e.g. `invalid_env_value`,
1477
+ * `env_overrode_option`, `engine_object_overrode_requested_mode`,
1478
+ * `engine_env_ignored`. Absent when the selection was unambiguous.
1479
+ */
1480
+ warnings?: readonly string[];
1481
+ }
1482
+ | ProviderStateBackendLogEvent
1483
+ | SelfTestCancellationLogEvent;
1484
+
1485
+ export type ProviderServerLogger = (event: ProviderServerLogEvent) => void;
1486
+
1487
+ export type ProviderServerOptions<TContext extends Partial<ProviderContext> = ProviderContext> = {
1488
+ logger?: ProviderServerLogger;
1489
+ /** Capability attachment boundary. Defaults to the local in-process engine. */
1490
+ engine?: ProviderEngine;
1491
+ /**
1492
+ * Explicit engine attachment mode. Lowest precedence of the three inputs:
1493
+ * an explicit `engine` object wins, then `APIFUSE__ENGINE__MODE` (the
1494
+ * deployment outranks provider code), then this option, then the in-process
1495
+ * default. Disagreement is never fatal — it is reported on the
1496
+ * `provider_engine_mode` boot event (`env_overrode_option`,
1497
+ * `engine_object_overrode_requested_mode`), which only `serve` emits. A value
1498
+ * that is not a known mode throws `ValidationError`, because unlike the env it
1499
+ * can only come from calling code.
1500
+ */
1501
+ engineMode?: ProviderEngineMode;
1502
+ /** Request-file resolver supplied by the engine host when `files` is declared. */
1503
+ files?: ProviderFilesContext;
1504
+ /** Optional provider-specific operation executor. Stateful providers use this to preserve provider-local runtime semantics. */
1505
+ operationExecutor?: ProviderServerOperationExecutor<TContext>;
1506
+ /** Optional signed internal executor for stateful owner forwarding. */
1507
+ internalOperationExecutor?: ProviderServerOperationExecutor<TContext>;
1508
+ statefulForwarding?: {
1509
+ readonly secret: string;
1510
+ readonly maxSkewMs?: number;
1511
+ readonly replayCacheMaxEntries?: number;
1512
+ /** Required fail-closed check against the SDK/runtime owner registry. */
1513
+ readonly validateOwnerFence: ProviderServerStatefulOwnerFenceValidator;
1514
+ };
1515
+ /** Optional STT override for tests or custom hosts; local/prod normally resolves from env. */
1516
+ stt?: SttContext;
1517
+ /** Optional OCR override for tests or custom hosts; local/prod normally resolves from env. */
1518
+ ocr?: OcrContext;
1519
+ /**
1520
+ * Optional resolver override for tests or custom hosts; local/prod normally resolves from env.
1521
+ * SDK chains are reconstructed with each request's telemetry sink. An opaque host
1522
+ * resolver is reported as one custom invocation; its internal vendor work is not exposed.
1523
+ */
1524
+ resolver?: ResolverContext;
1525
+ /**
1526
+ * Pre-loaded provider locale catalogs used to localize auth turns and the
1527
+ * client-facing `message`/`fix` of provider errors.
1528
+ *
1529
+ * Defaults to reading `locales/{en,ko,ja}.json` from the provider directory
1530
+ * once at server construction. Supply this to bundle catalogs instead of
1531
+ * probing the filesystem, or to pin catalogs in tests.
1532
+ */
1533
+ localeCatalogs?: ProviderLocaleCatalogMap;
1534
+ /** Optional runtime state override for tests or custom hosts. Production resolves Redis from env and fails closed when unavailable. */
1535
+ state?: ProviderRuntimeState;
1536
+ /** Allow process-local runtime state only for local development and tests. */
1537
+ allowMemoryStateFallback?: boolean;
1538
+ /**
1539
+ * Graceful process shutdown. Hooks run in declaration order after listeners stop accepting work.
1540
+ *
1541
+ * @example
1542
+ * ```ts
1543
+ * await serve(provider, {
1544
+ * shutdown: {
1545
+ * hooks: [
1546
+ * async () => { await emitter.flush(); },
1547
+ * async () => { await sessionManager.closeAll("server-shutdown"); },
1548
+ * async () => { await lease.release(); },
1549
+ * async () => { await router.close(); },
1550
+ * ],
1551
+ * },
1552
+ * });
1553
+ * ```
1554
+ */
1555
+ shutdown?: {
1556
+ readonly hooks?: Array<() => Promise<void>>;
1557
+ readonly signals?: boolean | NodeJS.Signals[];
1558
+ readonly timeoutMs?: number;
1559
+ };
1560
+ };
1561
+
1562
+ const defaultProviderServerLogger: ProviderServerLogger = (event) => {
1563
+ const line = JSON.stringify(event);
1564
+ if (event.level === "info") {
1565
+ console.log(line);
1566
+ return;
1567
+ }
1568
+ console.error(line);
1569
+ };
1570
+
1571
+ function startRequestCost(): {
1572
+ startedAtMs: number;
1573
+ cpuStart: NodeJS.CpuUsage;
1574
+ } {
1575
+ return {
1576
+ startedAtMs: performance.now(),
1577
+ cpuStart: process.cpuUsage(),
1578
+ };
1579
+ }
1580
+
1581
+ function finishRequestCost(input: {
1582
+ startedAtMs: number;
1583
+ cpuStart: NodeJS.CpuUsage;
1584
+ }): ProviderRequestCost {
1585
+ const cpuDelta = process.cpuUsage(input.cpuStart);
1586
+ return {
1587
+ durationMs: Math.max(0, Math.round(performance.now() - input.startedAtMs)),
1588
+ cpuUserMicros: Math.max(0, cpuDelta.user),
1589
+ cpuSystemMicros: Math.max(0, cpuDelta.system),
1590
+ cpuTotalMicros: Math.max(0, cpuDelta.user + cpuDelta.system),
1591
+ };
1592
+ }
1593
+
1594
+ function zodDetails(
1595
+ error: z.ZodError,
1596
+ redact?: DiagnosticRedactor,
1597
+ ): Array<{
1598
+ path: string;
1599
+ code: string;
1600
+ message: string;
1601
+ }> {
1602
+ return error.issues.map((issue) => ({
1603
+ path: sanitizeSpanNameForOutput(issue.path.join("."), redact),
1604
+ code: sanitizeSpanNameForOutput(issue.code, redact),
1605
+ message: sanitizeSpanNameForOutput(issue.message, redact),
1606
+ }));
1607
+ }
1608
+
1609
+ // Category-level projection with code-aware honesty overrides: a missing
1610
+ // deployment secret is an APIFuse-side defect even though its category
1611
+ // (credential_unavailable) usually means a caller credential problem, an
1612
+ // internal stateful-routing deadline is APIFuse-owned despite its timeout
1613
+ // category, and the built-in upstream failure families keep their upstream
1614
+ // attribution even under an explicit non-upstream category. An unset category
1615
+ // needs no override: a registered upstream code already carries its category
1616
+ // (SDK_UPSTREAM_ERROR_CODE_REGISTRY), and the source follows from it.
1617
+ function publicErrorSource(error: unknown, category: ProviderErrorCategory): ProviderErrorSource {
1618
+ if (error instanceof StatefulRoutingDeadlineError) return "apifuse";
1619
+ if (isProviderError(error)) {
1620
+ const code = providerErrorCode(error);
1621
+ if (code === MISSING_SECRET_CODE) return "apifuse";
1622
+ if (code === "UPSTREAM_ERROR" || code === "BLOCKED") {
1623
+ return "upstream_failure";
1624
+ }
1625
+ }
1626
+ return sourceForCategory(category);
1627
+ }
1628
+
1629
+ /**
1630
+ * Serve-time inputs for localizing the client-facing error envelope.
1631
+ *
1632
+ * Only `error.message` and `error.fix` of the response body are localized.
1633
+ * Logs, cause frames, OTLP attributes and the error-observability header keep
1634
+ * the English literal the provider threw.
1635
+ */
1636
+ type ErrorEnvelopeLocalization = {
1637
+ readonly catalogs?: ProviderLocaleCatalogMap;
1638
+ readonly locale: ProviderLocale;
1639
+ readonly declaredErrorCode?: OperationErrorCode;
1640
+ };
1641
+
1642
+ function localeKeyOption(error: unknown, key: "messageKey" | "fixKey"): string | undefined {
1643
+ const value = providerErrorOption(error, key);
1644
+ return typeof value === "string" ? value : undefined;
1645
+ }
1646
+
1647
+ function declaredLocaleKey(
1648
+ declaration: OperationErrorCode | undefined,
1649
+ key: "messageKey" | "fixKey",
1650
+ ): string | undefined {
1651
+ const value = declaration?.[key];
1652
+ return typeof value === "string" ? value : undefined;
1653
+ }
1654
+
1655
+ function providerErrorParams(error: unknown): ProviderErrorMessageParams | undefined {
1656
+ const params = providerErrorOption(error, "params");
1657
+ return params !== null && typeof params === "object" && !Array.isArray(params)
1658
+ ? (params as ProviderErrorMessageParams)
1659
+ : undefined;
1660
+ }
1661
+
1662
+ /**
1663
+ * Resolution order for one localized envelope field:
1664
+ *
1665
+ * 1. the throw site's `messageKey`/`fixKey`;
1666
+ * 2. the declared `errorCodes[]` entry's key for the thrown code;
1667
+ * 3. the derived `errors.<code>.<field>` convention;
1668
+ * 4. the English literal the provider threw.
1669
+ *
1670
+ * Steps 2 and 3 are skipped for SDK-owned failures (transport, Zod, stateful
1671
+ * deadline, SDK runtime codes): their text is the SDK's contract, and a
1672
+ * provider catalog entry must not be able to relabel one. An explicit key on
1673
+ * the thrown error still wins, because that is the provider deliberately
1674
+ * describing an error it constructed itself.
1675
+ */
1676
+ function localizedErrorField(
1677
+ error: unknown,
1678
+ field: "message" | "fix",
1679
+ literal: string | undefined,
1680
+ localization: ErrorEnvelopeLocalization | undefined,
1681
+ ): string | undefined {
1682
+ if (!localization?.catalogs) return literal;
1683
+ const optionKey = field === "message" ? "messageKey" : "fixKey";
1684
+ const candidates: ProviderErrorTextCandidate[] = [
1685
+ { kind: "key", key: localeKeyOption(error, optionKey) },
1686
+ ];
1687
+ if (!sdkOwnsErrorResolution(error)) {
1688
+ candidates.push(
1689
+ { kind: "key", key: declaredLocaleKey(localization.declaredErrorCode, optionKey) },
1690
+ { kind: "derived", code: providerErrorCode(error), field },
1691
+ );
1692
+ }
1693
+ return localizeProviderErrorText({
1694
+ catalogs: localization.catalogs,
1695
+ locale: localization.locale,
1696
+ candidates,
1697
+ params: providerErrorParams(error),
1698
+ fallback: literal,
1699
+ });
1700
+ }
1701
+
1702
+ /**
1703
+ * Reads `accept-language` out of the request envelope's `headers` map.
1704
+ *
1705
+ * The gateway stamps the negotiated locale there for auth flows (it persists
1706
+ * the start locale on the flow state so continue/poll turns stay in one
1707
+ * language), and the operation route merges HTTP headers into the same map, so
1708
+ * the envelope is the higher-precedence source.
1709
+ */
1710
+ function requestBodyAcceptLanguage(rawBody: unknown): string | undefined {
1711
+ if (!rawBody || typeof rawBody !== "object") return undefined;
1712
+ const headers = Object.getOwnPropertyDescriptor(rawBody, "headers")?.value;
1713
+ if (!headers || typeof headers !== "object" || Array.isArray(headers)) return undefined;
1714
+ return acceptLanguageHeaderValue(headers as Record<string, unknown>);
1715
+ }
1716
+
1717
+ function errorEnvelopeLocalization(options: {
1718
+ readonly catalogs: ProviderLocaleCatalogMap | undefined;
1719
+ readonly requestHeaders?: Headers;
1720
+ readonly rawBody?: unknown;
1721
+ readonly declaredErrorCode?: OperationErrorCode;
1722
+ }): ErrorEnvelopeLocalization {
1723
+ return {
1724
+ ...(options.catalogs ? { catalogs: options.catalogs } : {}),
1725
+ locale: resolveProviderErrorLocale(
1726
+ requestBodyAcceptLanguage(options.rawBody),
1727
+ options.requestHeaders?.get("accept-language"),
1728
+ ),
1729
+ ...(options.declaredErrorCode ? { declaredErrorCode: options.declaredErrorCode } : {}),
1730
+ };
1731
+ }
1732
+
1733
+ function toErrorResponse(
1734
+ error: unknown,
1735
+ requestId: string | undefined,
1736
+ observabilityDetails: ErrorObservabilityDetails,
1737
+ redact?: DiagnosticRedactor,
1738
+ localization?: ErrorEnvelopeLocalization,
1739
+ ): OperationErrorResponse {
1740
+ const observability = observabilityDetails;
1741
+ const source = publicErrorSource(error, observability.category);
1742
+ if (error instanceof StatefulRoutingDeadlineError) {
1743
+ return {
1744
+ error: {
1745
+ code: "STATEFUL_FORWARDING_DEADLINE_EXPIRED",
1746
+ message: "Stateful forwarding deadline expired.",
1747
+ ...(requestId ? { requestId } : {}),
1748
+ retryable: observability.retryable,
1749
+ source,
1750
+ },
1751
+ };
1752
+ }
1753
+
1754
+ if (isProviderError(error)) {
1755
+ const details = providerErrorOption(error, "details");
1756
+ const literalFix = providerErrorOption(error, "fix");
1757
+ const fix = localizedErrorField(
1758
+ error,
1759
+ "fix",
1760
+ typeof literalFix === "string" && literalFix.length > 0 ? literalFix : undefined,
1761
+ localization,
1762
+ );
1763
+ const literalMessage = publicProviderErrorMessage(error);
1764
+ return {
1765
+ error: {
1766
+ code: providerErrorCode(error) ?? "provider_error",
1767
+ message:
1768
+ localizedErrorField(error, "message", literalMessage, localization) ?? literalMessage,
1769
+ ...(requestId ? { requestId } : {}),
1770
+ retryable: observability.retryable,
1771
+ source,
1772
+ ...(fix ? { fix } : {}),
1773
+ ...(details !== undefined ? { details } : {}),
1774
+ },
1775
+ };
1776
+ }
1777
+
1778
+ if (error instanceof z.ZodError) {
1779
+ return {
1780
+ error: {
1781
+ code: "invalid_request",
1782
+ message: "Invalid request body",
1783
+ ...(requestId ? { requestId } : {}),
1784
+ retryable: observability.retryable,
1785
+ source,
1786
+ details: zodDetails(error, redact),
1787
+ },
1788
+ };
1789
+ }
1790
+
1791
+ // A masked internal error MUST NOT be advertised as retryable: without an
1792
+ // explicit retryable:false the hub (bori provider-backed engine) defaults 5xx
1793
+ // to retryable:true, which turns a deterministic pre-upstream crash into an
1794
+ // infinite START->CONTINUE->restart loop (2026-07-22 catchtable reserve RCA).
1795
+ // We still refuse to leak message/stack — only the error class name (or the
1796
+ // primitive type for non-Error throwables) is surfaced for ops triage.
1797
+ return {
1798
+ error: {
1799
+ code: "internal_error",
1800
+ message: "Internal error",
1801
+ ...(requestId ? { requestId } : {}),
1802
+ retryable: observability.retryable,
1803
+ source,
1804
+ details: {
1805
+ retryable: false,
1806
+ category: "internal_error",
1807
+ errorClass: error instanceof Error ? error.name : typeof error,
1808
+ },
1809
+ },
1810
+ };
1811
+ }
1812
+
1813
+ // Accepts `unknown` so the branded guards narrow cleanly from the top: the
1814
+ // subtype error classes are structurally compatible with ProviderError, so
1815
+ // narrowing from a ProviderError-typed value would collapse the negative branch
1816
+ // to `never`. Narrowing from unknown avoids that while still recognizing errors
1817
+ // from a duplicate SDK module instance.
1818
+ function ownErrorCode(error: unknown): string | undefined {
1819
+ if (error === null || (typeof error !== "object" && typeof error !== "function")) {
1820
+ return undefined;
1821
+ }
1822
+ try {
1823
+ const descriptor = Object.getOwnPropertyDescriptor(error, "code");
1824
+ return descriptor && Object.hasOwn(descriptor, "value") && typeof descriptor.value === "string"
1825
+ ? descriptor.value
1826
+ : undefined;
1827
+ } catch {
1828
+ return undefined;
1829
+ }
1830
+ }
1831
+
1832
+ function errorChainHasCode(error: unknown, expected: string): boolean {
1833
+ const seen = new Set<object>();
1834
+ let current = error;
1835
+ for (let depth = 0; depth < 5; depth++) {
1836
+ if (current === null || (typeof current !== "object" && typeof current !== "function")) {
1837
+ return false;
1838
+ }
1839
+ if (seen.has(current)) return false;
1840
+ seen.add(current);
1841
+ let code: string | undefined;
1842
+ try {
1843
+ code = providerErrorCode(current) ?? ownErrorCode(current);
1844
+ } catch {
1845
+ code = ownErrorCode(current);
1846
+ }
1847
+ if (code === expected) return true;
1848
+ let cause: PropertyDescriptor | undefined;
1849
+ try {
1850
+ cause = Object.getOwnPropertyDescriptor(current, "cause");
1851
+ } catch {
1852
+ return false;
1853
+ }
1854
+ if (!cause || !Object.hasOwn(cause, "value")) return false;
1855
+ current = cause.value;
1856
+ }
1857
+ return false;
1858
+ }
1859
+
1860
+ function providerObservabilityDetails(
1861
+ error: unknown,
1862
+ declaredErrorCode?: OperationErrorCode,
1863
+ ): ErrorObservabilityDetails | undefined {
1864
+ const declaredRetryable = sdkOwnsErrorResolution(error)
1865
+ ? undefined
1866
+ : declaredErrorCode?.retryable;
1867
+ // Session-expiry surfaces the credential_expired category so Gateway and
1868
+ // Credential Service can distinguish an expired credential from other 401s.
1869
+ if (isSessionExpiredError(error)) {
1870
+ return {
1871
+ category: providerErrorOption(error, "category") ?? "credential_expired",
1872
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
1873
+ retryable: providerErrorOption(error, "retryable") ?? declaredRetryable ?? false,
1874
+ };
1875
+ }
1876
+ // Missing-secret errors carry the canonical credential_unavailable category
1877
+ // so Gateway/observability can attribute the failure to provisioning, not
1878
+ // the upstream. Matched by code (not constructor) so both the SDK-owned
1879
+ // runtime gate and any not-yet-migrated provider-thrown MISSING_SECRET
1880
+ // serialize identically, including across duplicate SDK module instances.
1881
+ if (isProviderError(error) && providerErrorCode(error) === MISSING_SECRET_CODE) {
1882
+ return {
1883
+ category: providerErrorOption(error, "category") ?? "credential_unavailable",
1884
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
1885
+ retryable: providerErrorOption(error, "retryable") ?? declaredRetryable ?? false,
1886
+ };
1887
+ }
1888
+ // Native proxy resolution throws ProxyResolutionError directly, while HTTP
1889
+ // normalizes it beneath a transport_network_error cause and stealth retains
1890
+ // the original code. Preserve proxy attribution across all three forms.
1891
+ if (errorChainHasCode(error, "PROXY_REQUIRED")) {
1892
+ return {
1893
+ category: "proxy_pool",
1894
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
1895
+ retryable: true,
1896
+ };
1897
+ }
1898
+ if (!isTransportError(error)) {
1899
+ return undefined;
1900
+ }
1901
+ const errorCode = providerErrorCode(error);
1902
+ const isProxyPoolCode =
1903
+ errorCode === PROXY_POOL_EXHAUSTED_CODE ||
1904
+ errorCode === PROXY_EDGE_AUTH_REJECTED_CODE ||
1905
+ errorCode === "PROXY_ALLOCATION_FAILED" ||
1906
+ errorCode === "native_proxy_expired" ||
1907
+ errorCode === "native_proxy_invalid";
1908
+ const category =
1909
+ providerErrorOption(error, "category") ??
1910
+ (isProxyPoolCode
1911
+ ? "proxy_pool"
1912
+ : errorCode === PROXY_AUTH_IP_DENIED_CODE
1913
+ ? "anti_bot_blocked"
1914
+ : errorCode === "transport_timeout" ||
1915
+ errorCode === "native_connection_timeout" ||
1916
+ errorCode === "native_connection_idle_timeout"
1917
+ ? "timeout"
1918
+ : errorCode === "transport_network_error" ||
1919
+ errorCode === "native_connection_failed" ||
1920
+ errorCode === "native_connection_closed"
1921
+ ? "network"
1922
+ : errorCode === "native_connection_aborted"
1923
+ ? "client_cancelled"
1924
+ : error.upstreamStatus
1925
+ ? categoryForStatus(error.upstreamStatus)
1926
+ : "upstream_http");
1927
+ return {
1928
+ category,
1929
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
1930
+ retryable:
1931
+ providerErrorOption(error, "retryable") ??
1932
+ (category === "upstream_http" && error.upstreamStatus
1933
+ ? error.upstreamStatus >= 500
1934
+ : isRetryableCategory(category)),
1935
+ ...(error.upstreamStatus ? { upstreamStatus: error.upstreamStatus } : {}),
1936
+ };
1937
+ }
1938
+
1939
+ // The category a `ProviderError` (validation errors included) gets when its
1940
+ // author set none, ahead of the branch's own default: an operation-declared
1941
+ // rejection status (409/410/422) → upstream_rejected, then the registered
1942
+ // upstream code's category (UPSTREAM_SCHEMA_ERROR → upstream_schema_drift,
1943
+ // NOT_FOUND → upstream_http, ...). `undefined` leaves the branch default.
1944
+ function derivedErrorCategory(
1945
+ error: unknown,
1946
+ declaredStatus: unknown,
1947
+ ): ProviderErrorCategory | undefined {
1948
+ if (
1949
+ isEmittableErrorStatus(declaredStatus) &&
1950
+ categoryForStatus(declaredStatus) === "upstream_rejected"
1951
+ ) {
1952
+ return "upstream_rejected";
1953
+ }
1954
+ return registeredUpstreamErrorCategory(providerErrorCode(error));
1955
+ }
1956
+
1957
+ function classifiedErrorObservabilityDetails(
1958
+ error: unknown,
1959
+ declaredErrorCode?: OperationErrorCode,
1960
+ ): ErrorObservabilityDetails {
1961
+ const effectiveDeclaration = sdkOwnsErrorResolution(error) ? undefined : declaredErrorCode;
1962
+ const providerDetails = providerObservabilityDetails(error, effectiveDeclaration);
1963
+ if (providerDetails) return providerDetails;
1964
+
1965
+ if (error instanceof z.ZodError || isValidationError(error)) {
1966
+ const declaredStatus = effectiveDeclaration?.status;
1967
+ const providerCategory = providerErrorOption(error, "category");
1968
+ return {
1969
+ category: providerCategory
1970
+ ? providerCategory
1971
+ : (derivedErrorCategory(error, declaredStatus) ??
1972
+ (isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
1973
+ ? "provider_error"
1974
+ : "input_validation")),
1975
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
1976
+ retryable: isProviderError(error)
1977
+ ? (providerErrorOption(error, "retryable") ?? effectiveDeclaration?.retryable ?? false)
1978
+ : false,
1979
+ };
1980
+ }
1981
+
1982
+ if (error instanceof StatefulRoutingDeadlineError) {
1983
+ return {
1984
+ category: "timeout",
1985
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
1986
+ retryable: false,
1987
+ };
1988
+ }
1989
+
1990
+ if (isProviderError(error)) {
1991
+ // Retryability does not follow the derived category: it stays
1992
+ // instance ?? declared ?? false below.
1993
+ return {
1994
+ category:
1995
+ providerErrorOption(error, "category") ??
1996
+ derivedErrorCategory(error, effectiveDeclaration?.status) ??
1997
+ "provider_error",
1998
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
1999
+ retryable:
2000
+ providerErrorOption(error, "retryable") ?? effectiveDeclaration?.retryable ?? false,
2001
+ };
2002
+ }
2003
+
2004
+ return {
2005
+ category: "internal_error",
2006
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
2007
+ retryable: false,
2008
+ };
2009
+ }
2010
+
2011
+ function errorObservabilityDetails(
2012
+ error: unknown,
2013
+ declaredErrorCode?: OperationErrorCode,
2014
+ ): ErrorObservabilityDetails {
2015
+ const details = classifiedErrorObservabilityDetails(error, declaredErrorCode);
2016
+ const providerObservability = safeProviderErrorObservability(error);
2017
+ return {
2018
+ ...details,
2019
+ ...(providerObservability ? { providerObservability } : {}),
2020
+ };
2021
+ }
2022
+
2023
+ function responseWithErrorObservability(
2024
+ response: Response,
2025
+ observabilityDetails: ErrorObservabilityDetails,
2026
+ ): Response {
2027
+ const headers = new Headers(response.headers);
2028
+ headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(observabilityDetails));
2029
+ return new Response(response.body, {
2030
+ status: response.status,
2031
+ statusText: response.statusText,
2032
+ headers,
2033
+ });
2034
+ }
2035
+
2036
+ function publicProviderErrorMessage(error: ProviderError): string {
2037
+ if (isTransportError(error)) {
2038
+ if (providerErrorCode(error) === PROXY_AUTH_IP_DENIED_CODE) {
2039
+ return error.message;
2040
+ }
2041
+ if (providerErrorCode(error) === PROXY_EDGE_AUTH_REJECTED_CODE) {
2042
+ return error.message;
2043
+ }
2044
+ if (providerErrorCode(error) === PROXY_POOL_EXHAUSTED_CODE) {
2045
+ return error.message;
2046
+ }
2047
+ if (providerErrorCode(error) === "transport_timeout") return "Request timed out";
2048
+ if (providerErrorCode(error) === "transport_network_error") return "Network error";
2049
+ // Provider-local fault: no request was issued, so an upstream-flavored
2050
+ // message would misattribute it (category is provider_error).
2051
+ if (providerErrorCode(error) === "http_header_factory_failed") {
2052
+ return "Request header preparation failed";
2053
+ }
2054
+ if (providerErrorCode(error) === "upstream_http_error" && error.status) {
2055
+ return `Upstream request failed with status ${error.status}`;
2056
+ }
2057
+ if (error.status) {
2058
+ return `Upstream request failed with status ${error.status}`;
2059
+ }
2060
+ return "Upstream request failed";
2061
+ }
2062
+ return error.message;
2063
+ }
2064
+
2065
+ function isEmittableErrorStatus(value: unknown): value is ProviderErrorStatus {
2066
+ return (
2067
+ typeof value === "number" && VALID_OPERATION_ERROR_STATUSES.some((status) => status === value)
2068
+ );
2069
+ }
2070
+
2071
+ function toStatusCode(error: unknown, declaredErrorCode?: OperationErrorCode): ProviderErrorStatus {
2072
+ if (error instanceof z.ZodError) {
2073
+ return 400;
2074
+ }
2075
+ if (error instanceof StatefulRoutingDeadlineError) {
2076
+ return 504;
2077
+ }
2078
+ if (isProviderError(error)) {
2079
+ if (!sdkOwnsErrorResolution(error) && isEmittableErrorStatus(declaredErrorCode?.status)) {
2080
+ return declaredErrorCode.status;
2081
+ }
2082
+ // Canonical SDK code → status mapping lives in error-resolution.ts so
2083
+ // the authoring lint and this runtime path share one source of truth.
2084
+ const code = providerErrorCode(error);
2085
+ if (typeof code === "string") {
2086
+ const mappedStatus = SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES.get(code);
2087
+ if (mappedStatus !== undefined) {
2088
+ return mappedStatus;
2089
+ }
2090
+ }
2091
+ if (isTransportError(error)) {
2092
+ return providerErrorCode(error) === "transport_timeout" ? 504 : 502;
2093
+ }
2094
+ if (isValidationError(error)) {
2095
+ return providerErrorOption(error, "category") === "output_validation" ? 500 : 400;
2096
+ }
2097
+
2098
+ return 500;
2099
+ }
2100
+
2101
+ return 500;
2102
+ }
2103
+
2104
+ function sdkOwnsErrorResolution(error: unknown): boolean {
2105
+ if (isSessionExpiredError(error)) return true;
2106
+ if (isTransportError(error)) return true;
2107
+ if (error instanceof z.ZodError) return true;
2108
+ if (error instanceof StatefulRoutingDeadlineError) return true;
2109
+ if (!isProviderError(error)) return false;
2110
+ const code = providerErrorCode(error);
2111
+ return typeof code === "string" && SDK_RUNTIME_OWNED_ERROR_CODES.has(code);
2112
+ }
2113
+
2114
+ type OperationErrorCodeLookup = ReadonlyMap<string, ReadonlyMap<string, OperationErrorCode>>;
2115
+
2116
+ function buildOperationErrorCodeLookup(provider: ProviderDefinition): OperationErrorCodeLookup {
2117
+ return new Map(
2118
+ Object.entries(provider.operations).flatMap(([operationId, operation]) => {
2119
+ const errorCodes = operation.errorCodes;
2120
+ return errorCodes?.length
2121
+ ? [[operationId, new Map(errorCodes.map((entry) => [entry.code, entry]))] as const]
2122
+ : [];
2123
+ }),
2124
+ );
2125
+ }
2126
+
2127
+ function declaredErrorCodeFor(
2128
+ error: unknown,
2129
+ operationId: string | undefined,
2130
+ lookup: OperationErrorCodeLookup,
2131
+ ): OperationErrorCode | undefined {
2132
+ const code = providerErrorCode(error);
2133
+ if (!operationId || !code) return undefined;
2134
+ return lookup.get(operationId)?.get(code);
2135
+ }
2136
+
2137
+ function extractRequestId(raw: unknown): string | undefined {
2138
+ if (!raw || typeof raw !== "object") {
2139
+ return undefined;
2140
+ }
2141
+
2142
+ const value = Object.getOwnPropertyDescriptor(raw, "requestId")?.value;
2143
+ return typeof value === "string" ? value : undefined;
2144
+ }
2145
+
2146
+ export type ProviderErrorCauseFrame = {
2147
+ errorClass: string;
2148
+ code?: string;
2149
+ message: string;
2150
+ messageLength: number;
2151
+ messageFingerprint: string;
2152
+ providerObservability?: ProviderErrorObservability;
2153
+ };
2154
+
2155
+ const MAX_PROVIDER_ERROR_CAUSE_FRAMES = 5;
2156
+ const MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH = 300;
2157
+
2158
+ function providerErrorCauseMessage(message: string, redact?: DiagnosticRedactor): string {
2159
+ const sanitized = sanitizeDiagnosticText(redactDiagnosticText(message, redact));
2160
+ return sanitized.length > MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH
2161
+ ? `${sanitized.slice(0, MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH)}… [truncated]`
2162
+ : sanitized;
2163
+ }
2164
+
2165
+ function providerErrorCauseChain(
2166
+ error: unknown,
2167
+ redact?: DiagnosticRedactor,
2168
+ ): ProviderErrorCauseFrame[] | undefined {
2169
+ if (!(error instanceof Error) && !isProviderError(error)) return undefined;
2170
+
2171
+ const seen = new Set<object>([error]);
2172
+ const frames: ProviderErrorCauseFrame[] = [];
2173
+ let cause = error.cause;
2174
+
2175
+ while (
2176
+ frames.length < MAX_PROVIDER_ERROR_CAUSE_FRAMES &&
2177
+ (cause instanceof Error || isProviderError(cause)) &&
2178
+ !seen.has(cause)
2179
+ ) {
2180
+ seen.add(cause);
2181
+ const message = cause.message;
2182
+ const providerObservability = safeProviderErrorObservability(cause);
2183
+ const causeCode = providerErrorCode(cause);
2184
+ frames.push({
2185
+ errorClass: redactDiagnosticText(cause.name, redact),
2186
+ ...(causeCode !== undefined ? { code: redactDiagnosticText(causeCode, redact) } : {}),
2187
+ message: providerErrorCauseMessage(message, redact),
2188
+ messageLength: message.length,
2189
+ messageFingerprint: createHash("sha256").update(message).digest("hex").slice(0, 12),
2190
+ ...(providerObservability ? { providerObservability } : {}),
2191
+ });
2192
+ cause = cause.cause;
2193
+ }
2194
+
2195
+ return frames.length > 0 ? frames : undefined;
2196
+ }
2197
+
2198
+ function logProviderError(
2199
+ logger: ProviderServerLogger | unknown,
2200
+ provider: ProviderDefinition,
2201
+ kind: "operation" | "auth",
2202
+ route: string,
2203
+ requestId: string | undefined,
2204
+ error: unknown,
2205
+ status: number,
2206
+ cost: ProviderRequestCost,
2207
+ declaredErrorCode: OperationErrorCode | undefined,
2208
+ telemetry: RequestTelemetry | undefined,
2209
+ observabilityDetails: ErrorObservabilityDetails,
2210
+ correlation: RequestCorrelationIds = {},
2211
+ redact?: DiagnosticRedactor,
2212
+ ): void {
2213
+ const providerCode = isProviderError(error) ? providerErrorCode(error) : undefined;
2214
+ const code = isProviderError(error)
2215
+ ? (providerCode ?? "provider_error")
2216
+ : error instanceof z.ZodError
2217
+ ? "invalid_request"
2218
+ : error instanceof StatefulRoutingDeadlineError
2219
+ ? "STATEFUL_FORWARDING_DEADLINE_EXPIRED"
2220
+ : "internal_error";
2221
+ const errorClass = error instanceof Error ? error.name : typeof error;
2222
+ const message = error instanceof Error ? error.message : String(error);
2223
+ const causeChain = providerErrorCauseChain(error, redact);
2224
+ // Masked 500 bodies carry only errorClass, so the log is the sole channel
2225
+ // that can locate the throw. Declared 5xx (502/504) are included too: their
2226
+ // body carries code+message, but frame[0] is still the throw site inside
2227
+ // provider code. 4xx failures keep the lean record.
2228
+ const stack = status >= 500 ? providerErrorStackFrames(error, redact) : undefined;
2229
+ const details = observabilityDetails;
2230
+ const isUnregisteredProviderErrorCode =
2231
+ status === 500 &&
2232
+ isProviderError(error) &&
2233
+ !isValidationError(error) &&
2234
+ typeof providerCode === "string" &&
2235
+ !SDK_OWNED_PROVIDER_ERROR_CODES.has(providerCode) &&
2236
+ declaredErrorCode === undefined;
2237
+ const telemetryPayload = telemetry?.toLogPayload();
2238
+ const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
2239
+ // The logger is caller-supplied and may mutate the event synchronously.
2240
+ // Give it an independent snapshot so those mutations cannot corrupt the
2241
+ // observability header serialized immediately afterwards.
2242
+ const providerObservability = details.providerObservability
2243
+ ? { ...details.providerObservability }
2244
+ : undefined;
2245
+ emit({
2246
+ level: status >= 500 ? "error" : "warn",
2247
+ event: "provider_request_failed",
2248
+ providerId: redactDiagnosticText(provider.id, redact),
2249
+ kind,
2250
+ route: redactCriticalDiagnosticText(route, redact),
2251
+ ...(requestId ? { requestId: redactCriticalDiagnosticText(requestId, redact) } : {}),
2252
+ ...(correlation.connectionId !== undefined
2253
+ ? { connectionId: redactDiagnosticText(correlation.connectionId, redact) }
2254
+ : {}),
2255
+ ...(correlation.flowId !== undefined
2256
+ ? { flowId: redactDiagnosticText(correlation.flowId, redact) }
2257
+ : {}),
2258
+ ...(correlation.tenantId !== undefined
2259
+ ? { tenantId: redactDiagnosticText(correlation.tenantId, redact) }
2260
+ : {}),
2261
+ ...(correlation.requestedProviderId !== undefined
2262
+ ? { requestedProviderId: redactDiagnosticText(correlation.requestedProviderId, redact) }
2263
+ : {}),
2264
+ status,
2265
+ ...cost,
2266
+ ...(telemetryPayload ?? {}),
2267
+ code: sanitizeSpanNameForOutput(redactCriticalDiagnosticText(code, redact)),
2268
+ errorClass: redactDiagnosticText(errorClass, redact),
2269
+ message: sanitizeDiagnosticText(redactDiagnosticText(message, redact)),
2270
+ ...(causeChain ? { causeChain } : {}),
2271
+ ...(stack ? { stack } : {}),
2272
+ ...(providerObservability ? { providerObservability } : {}),
2273
+ ...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
2274
+ errorCategory: details.category,
2275
+ taxonomyVersion: details.taxonomyVersion,
2276
+ retryable: details.retryable,
2277
+ ...(isUnregisteredProviderErrorCode
2278
+ ? {
2279
+ signal: "unregistered_provider_error_code" as const,
2280
+ signalFix:
2281
+ "Declare this code (with status and retryable) in the operation's errorCodes so it serves its intended status instead of 500.",
2282
+ }
2283
+ : {}),
2284
+ ...(error instanceof z.ZodError ? { issues: zodDetails(error, redact) } : {}),
2285
+ });
2286
+ }
2287
+
2288
+ function logProviderCleanupError(
2289
+ logger: ProviderServerLogger | unknown,
2290
+ provider: ProviderDefinition,
2291
+ kind: "operation" | "auth",
2292
+ operationId: string,
2293
+ requestId: string | undefined,
2294
+ resource: "browser" | "stealth",
2295
+ error: unknown,
2296
+ redact?: DiagnosticRedactor,
2297
+ ): void {
2298
+ const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
2299
+ const errorClass = error instanceof Error ? error.name : typeof error;
2300
+ const message = error instanceof Error ? error.message : String(error);
2301
+ emit({
2302
+ level: "warn",
2303
+ event: "provider_cleanup_failed",
2304
+ providerId: redactDiagnosticText(provider.id, redact),
2305
+ kind,
2306
+ route: redactCriticalDiagnosticText(operationId, redact),
2307
+ ...(requestId ? { requestId: redactCriticalDiagnosticText(requestId, redact) } : {}),
2308
+ resource,
2309
+ errorClass: redactDiagnosticText(errorClass, redact),
2310
+ message: sanitizeDiagnosticText(redactDiagnosticText(message, redact)),
2311
+ });
2312
+ }
2313
+
2314
+ function logProviderSuccess(
2315
+ logger: ProviderServerLogger | unknown,
2316
+ provider: ProviderDefinition,
2317
+ kind: "operation" | "auth",
2318
+ route: string,
2319
+ requestId: string | undefined,
2320
+ status: number,
2321
+ cost: ProviderRequestCost,
2322
+ telemetry?: RequestTelemetry,
2323
+ correlation: RequestCorrelationIds = {},
2324
+ redact?: DiagnosticRedactor,
2325
+ ): void {
2326
+ const telemetryPayload = telemetry?.toLogPayload();
2327
+ const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
2328
+ emit({
2329
+ level: "info",
2330
+ event: "provider_request_completed",
2331
+ providerId: redactDiagnosticText(provider.id, redact),
2332
+ kind,
2333
+ route: redactCriticalDiagnosticText(route, redact),
2334
+ ...(requestId ? { requestId: redactCriticalDiagnosticText(requestId, redact) } : {}),
2335
+ ...(correlation.connectionId !== undefined
2336
+ ? { connectionId: redactDiagnosticText(correlation.connectionId, redact) }
2337
+ : {}),
2338
+ ...(correlation.flowId !== undefined
2339
+ ? { flowId: redactDiagnosticText(correlation.flowId, redact) }
2340
+ : {}),
2341
+ ...(correlation.tenantId !== undefined
2342
+ ? { tenantId: redactDiagnosticText(correlation.tenantId, redact) }
2343
+ : {}),
2344
+ ...(correlation.requestedProviderId !== undefined
2345
+ ? { requestedProviderId: redactDiagnosticText(correlation.requestedProviderId, redact) }
2346
+ : {}),
2347
+ status,
2348
+ ...cost,
2349
+ ...(telemetryPayload ?? {}),
2350
+ });
2351
+ }
2352
+
2353
+ type RequestScopeFinishResult = {
2354
+ providerTelemetryHeader?: string;
2355
+ errorObservability?: ErrorObservabilityDetails;
2356
+ };
2357
+
2358
+ type RequestScope = RequestScopeContext & {
2359
+ seedCredentials(rawBody: unknown, kind: "operation" | "auth" | "stateful"): void;
2360
+ enrich(input: {
2361
+ route?: string;
2362
+ requestId?: string;
2363
+ operationId?: string;
2364
+ flowId?: string;
2365
+ headers?: Record<string, string>;
2366
+ correlation?: RequestCorrelationIds;
2367
+ }): void;
2368
+ run<T>(fn: () => Promise<T>): Promise<T>;
2369
+ runStreaming<T>(fn: () => Promise<T>): Promise<T>;
2370
+ runStreamStep<T>(fn: () => Promise<T>): Promise<T>;
2371
+ watchAbort(signal: AbortSignal): void;
2372
+ registerStreamCleanup(cleanup: () => void | Promise<void>): void;
2373
+ cleanupStream(): Promise<void>;
2374
+ snapshotHeaders(error?: unknown): RequestScopeFinishResult;
2375
+ terminalize(outcome: RequestTerminalOutcome): RequestScopeFinishResult;
2376
+ };
2377
+
2378
+ const STREAM_CLEANUP_TIMEOUT_MS = 100;
2379
+ const CLIENT_CANCELLED_STATUS = 400;
2380
+
2381
+ function clientCancelledError(): ProviderError {
2382
+ return new ProviderError("Request stream was cancelled by the client.", {
2383
+ code: "client_cancelled",
2384
+ category: "client_cancelled",
2385
+ retryable: false,
2386
+ });
2387
+ }
2388
+
2389
+ async function runBestEffortWithTimeout(cleanup: () => void | Promise<void>): Promise<void> {
2390
+ let timeout: ReturnType<typeof setTimeout> | undefined;
2391
+ const work = Promise.resolve()
2392
+ .then(cleanup)
2393
+ .catch(() => undefined);
2394
+ try {
2395
+ await Promise.race([
2396
+ work,
2397
+ new Promise<void>((resolve) => {
2398
+ timeout = setTimeout(resolve, STREAM_CLEANUP_TIMEOUT_MS);
2399
+ }),
2400
+ ]);
2401
+ } finally {
2402
+ if (timeout !== undefined) clearTimeout(timeout);
2403
+ }
2404
+ }
2405
+
2406
+ function traceIdFromTraceparent(headers: Record<string, string>): string | undefined {
2407
+ const traceparent = Object.entries(headers).find(
2408
+ ([name]) => name.toLowerCase() === "traceparent",
2409
+ )?.[1];
2410
+ if (!traceparent) return undefined;
2411
+ const match = /^(00)-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/.exec(traceparent);
2412
+ if (!match) return undefined;
2413
+ const traceId = match[2];
2414
+ const parentId = match[3];
2415
+ if (!traceId || !parentId || /^0+$/.test(traceId) || /^0+$/.test(parentId)) return undefined;
2416
+ return traceId;
2417
+ }
2418
+
2419
+ function requestTraceId(
2420
+ headers: Record<string, string>,
2421
+ requestId: string | undefined,
2422
+ ): string | undefined {
2423
+ return (
2424
+ traceIdFromTraceparent(headers) ??
2425
+ (requestId
2426
+ ? createHash("sha256").update(requestId, "utf8").digest("hex").slice(0, 32)
2427
+ : undefined)
2428
+ );
2429
+ }
2430
+
2431
+ /** Harvest only credential subtrees, iteratively and before schema diagnostics. */
2432
+ function rawRequestCredentials(
2433
+ rawBody: unknown,
2434
+ kind: "operation" | "auth" | "stateful",
2435
+ ): { values: string[]; exhausted: boolean } {
2436
+ const record = (value: unknown): Record<string, unknown> | undefined =>
2437
+ value !== null && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
2438
+ const own = (value: unknown, key: string): unknown => {
2439
+ const object = record(value);
2440
+ return object && Object.hasOwn(object, key) ? object[key] : undefined;
2441
+ };
2442
+ const body = kind === "stateful" ? own(rawBody, "operationRequest") : rawBody;
2443
+ const values: string[] = [];
2444
+ let remainingBytes = 64 * 1024;
2445
+ let exhausted = false;
2446
+ const add = (value: unknown): void => {
2447
+ if (typeof value !== "string") return;
2448
+ const bytes = Buffer.byteLength(value, "utf8");
2449
+ if (bytes > remainingBytes) {
2450
+ exhausted = true;
2451
+ return;
2452
+ }
2453
+ remainingBytes -= bytes;
2454
+ if (value) values.push(value);
2455
+ const trimmed = value.trim();
2456
+ if (trimmed && trimmed !== value) values.push(trimmed);
2457
+ };
2458
+ function* entries(object: Record<string, unknown>) {
2459
+ for (const key in object) {
2460
+ if (Object.hasOwn(object, key)) yield [key, object[key]] as const;
2461
+ }
2462
+ }
2463
+ const visit = (root: unknown): void => {
2464
+ const object = record(root);
2465
+ if (!object) {
2466
+ add(root);
2467
+ return;
2468
+ }
2469
+ // Connection inventories are usually flat. Walk the root directly and only
2470
+ // allocate iterator frames when a nested credential container is encountered.
2471
+ for (const key in object) {
2472
+ if (!Object.hasOwn(object, key)) continue;
2473
+ const value = object[key];
2474
+ add(key);
2475
+ if (exhausted) break;
2476
+ const child = record(value);
2477
+ if (!child) {
2478
+ add(value);
2479
+ if (exhausted) break;
2480
+ continue;
2481
+ }
2482
+ const stack = [{ iterator: entries(child), depth: 1 }];
2483
+ while (stack.length && !exhausted) {
2484
+ const frame = stack[stack.length - 1];
2485
+ const entry = frame.iterator.next();
2486
+ if (entry.done) {
2487
+ stack.pop();
2488
+ continue;
2489
+ }
2490
+ const [key, value] = entry.value;
2491
+ add(key);
2492
+ if (exhausted) break;
2493
+ const child = record(value);
2494
+ if (child) {
2495
+ if (frame.depth >= 8) {
2496
+ exhausted = true;
2497
+ break;
2498
+ }
2499
+ stack.push({ iterator: entries(child), depth: frame.depth + 1 });
2500
+ } else add(value);
2501
+ }
2502
+ if (exhausted) break;
2503
+ }
2504
+ };
2505
+ visit(own(own(body, "connection"), "secrets"));
2506
+ if (kind === "auth" && !exhausted) {
2507
+ add(own(own(body, "engine"), "egressLease"));
2508
+ if (!exhausted) visit(own(body, "input"));
2509
+ }
2510
+ return { values, exhausted };
2511
+ }
2512
+
2513
+ function createRequestScope(input: {
2514
+ provider: ProviderDefinition;
2515
+ staticSensitiveValues: CompiledDiagnosticSensitiveValues;
2516
+ kind: "operation" | "auth";
2517
+ route: string;
2518
+ requestId?: string;
2519
+ operationId?: string;
2520
+ flowId?: string;
2521
+ headers: Record<string, string>;
2522
+ correlation?: RequestCorrelationIds;
2523
+ logger?: ProviderServerLogger;
2524
+ setHeader?: (name: string, value: string) => void;
2525
+ declaredErrorCode?: (error: unknown) => OperationErrorCode | undefined;
2526
+ }): RequestScope {
2527
+ const sensitiveRegistry = createDiagnosticRedactor([], input.staticSensitiveValues);
2528
+ const redact = (text: string) => redactDiagnosticText(text, sensitiveRegistry.redact);
2529
+ bindCriticalDiagnosticRedactor(
2530
+ redact,
2531
+ sensitiveRegistry.redactCritical,
2532
+ sensitiveRegistry.redactStructured,
2533
+ () => sensitiveRegistry.suppressed,
2534
+ );
2535
+ const requestCost = startRequestCost();
2536
+ const observeEnv = {
2537
+ observe: createDiagnosticEnvObserver(
2538
+ input.provider,
2539
+ sensitiveRegistry.add,
2540
+ sensitiveRegistry.has,
2541
+ ),
2542
+ finished: false,
2543
+ staticValues: input.staticSensitiveValues,
2544
+ register: sensitiveRegistry.add,
2545
+ };
2546
+ const traceConfig = withDiagnosticEnv(observeEnv, () => resolveTraceConfigFromEnv());
2547
+ const details = {
2548
+ route: input.route,
2549
+ requestId: input.requestId,
2550
+ operationId: input.operationId,
2551
+ flowId: input.flowId,
2552
+ headers: input.headers,
2553
+ correlation: { ...(input.correlation ?? {}) },
2554
+ };
2555
+ const traceAttributes: Record<string, string> = {
2556
+ provider_id: input.provider.id,
2557
+ ...(input.operationId ? { operation_id: input.operationId } : {}),
2558
+ ...(input.flowId ? { flow_id: input.flowId } : {}),
2559
+ ...(input.requestId ? { request_id: input.requestId } : {}),
2560
+ route: input.route,
2561
+ };
2562
+ const initialTraceId = requestTraceId(input.headers, input.requestId);
2563
+ const trace: RuntimeTraceContext = traceConfig
2564
+ ? createTraceContext({
2565
+ ...withDiagnosticEnv(observeEnv, () =>
2566
+ resolveServerTraceContextOptions(traceConfig, traceAttributes, undefined, redact),
2567
+ ),
2568
+ redact,
2569
+ ...(initialTraceId ? { traceId: initialTraceId } : {}),
2570
+ })
2571
+ : createTraceContext({ redact });
2572
+ bindDiagnosticSensitiveRegistry(trace, sensitiveRegistry);
2573
+ const proxyCollector = new ProxyTelemetryCollector();
2574
+ const resolverCollector = new ResolverTelemetryCollector({ redact });
2575
+ const nativeCollector = new NativeTelemetryCollector({ redact });
2576
+ const httpCollector = new HttpTelemetryCollector({ redact });
2577
+ const stealthCollector = new StealthTelemetryCollector({ redact });
2578
+ const ocrCollector = new OcrTelemetryCollector({ redact });
2579
+ const sttCollector = new SttTelemetryCollector({ redact });
2580
+ const browserCollector = new BrowserTelemetryCollector({ redact });
2581
+ const cacheTelemetry = new CacheTelemetryCollector({ redact });
2582
+ const stateTelemetry = new StateTelemetryCollector({ redact });
2583
+ const telemetry = new RequestTelemetry(trace);
2584
+ telemetry.register(proxyCollector);
2585
+ telemetry.register(resolverCollector);
2586
+ telemetry.register(nativeCollector);
2587
+ telemetry.register(httpCollector);
2588
+ telemetry.register(stealthCollector);
2589
+ telemetry.register(browserCollector);
2590
+ telemetry.register(ocrCollector);
2591
+ telemetry.register(sttCollector);
2592
+ telemetry.register(cacheTelemetry);
2593
+ telemetry.register(stateTelemetry);
2594
+ let rootRunner: <T>(fn: () => Promise<T>) => Promise<T> = (fn) => fn();
2595
+ let resolveRoot!: (outcome: RequestTerminalOutcome) => void;
2596
+ const rootTerminal = new Promise<RequestTerminalOutcome>((resolve) => {
2597
+ resolveRoot = resolve;
2598
+ });
2599
+ let streamingSetupPending = false;
2600
+ let rootSettled = false;
2601
+ let headersSnapshotted = false;
2602
+ let terminalOutcome: RequestTerminalOutcome | undefined;
2603
+ let finishedResult: RequestScopeFinishResult | undefined;
2604
+ let abortSignal: AbortSignal | undefined;
2605
+ let abortListener: (() => void) | undefined;
2606
+ type CleanupEntry = {
2607
+ cleanup: () => void | Promise<void>;
2608
+ promise?: Promise<void>;
2609
+ };
2610
+ const streamCleanups: CleanupEntry[] = [];
2611
+
2612
+ const terminalError = (outcome: RequestTerminalOutcome): unknown | undefined => {
2613
+ if (outcome.kind === "failed") return outcome.error;
2614
+ if (outcome.kind === "cancelled") return clientCancelledError();
2615
+ return undefined;
2616
+ };
2617
+ const root = withDiagnosticEnv(observeEnv, () =>
2618
+ trace.span(`request:${input.kind}:${input.route}`, async () => {
2619
+ rootRunner = AsyncLocalStorage.snapshot();
2620
+ const outcome = await rootTerminal;
2621
+ const error = terminalError(outcome);
2622
+ if (error !== undefined) throw error;
2623
+ }),
2624
+ );
2625
+ void root.catch(() => undefined);
2626
+
2627
+ const settleRoot = (): void => {
2628
+ if (!terminalOutcome || streamingSetupPending || rootSettled) return;
2629
+ rootSettled = true;
2630
+ if (abortSignal && abortListener) {
2631
+ abortSignal.removeEventListener("abort", abortListener);
2632
+ }
2633
+ resolveRoot(terminalOutcome);
2634
+ };
2635
+ const runCleanupEntry = (entry: CleanupEntry): Promise<void> => {
2636
+ entry.promise ??= runBestEffortWithTimeout(entry.cleanup);
2637
+ return entry.promise;
2638
+ };
2639
+ const cleanupStream = async (): Promise<void> => {
2640
+ await Promise.all(streamCleanups.map(runCleanupEntry));
2641
+ };
2642
+ const headerSnapshot = (error?: unknown): RequestScopeFinishResult => {
2643
+ const providerTelemetryHeader = telemetry.toHeaderValue();
2644
+ const declaredErrorCode = error === undefined ? undefined : input.declaredErrorCode?.(error);
2645
+ const errorObservability =
2646
+ error === undefined ? undefined : errorObservabilityDetails(error, declaredErrorCode);
2647
+ return {
2648
+ ...(providerTelemetryHeader ? { providerTelemetryHeader } : {}),
2649
+ ...(errorObservability ? { errorObservability } : {}),
2650
+ };
2651
+ };
2652
+ const updateTraceMetadata = (): void => {
2653
+ if (details.requestId) traceAttributes.request_id = details.requestId;
2654
+ if (details.operationId) traceAttributes.operation_id = details.operationId;
2655
+ if (details.flowId) traceAttributes.flow_id = details.flowId;
2656
+ traceAttributes.route = details.route;
2657
+ const traceId = requestTraceId(details.headers, details.requestId);
2658
+ const sanitizedAttributes = Object.fromEntries(
2659
+ Object.entries(sanitizeTraceAttributes(traceAttributes, redact)).map(([key, value]) => [
2660
+ key,
2661
+ String(value),
2662
+ ]),
2663
+ );
2664
+ updateTraceContextExportMetadata(trace, {
2665
+ ...(traceId ? { traceId } : {}),
2666
+ resourceAttributes: sanitizedAttributes,
2667
+ });
2668
+ };
2669
+
2670
+ const scope: RequestScope = {
2671
+ redact,
2672
+ trace,
2673
+ telemetry,
2674
+ resolverTelemetry: resolverCollector,
2675
+ nativeTelemetry: nativeCollector,
2676
+ httpTelemetry: httpCollector,
2677
+ stealthTelemetry: stealthCollector,
2678
+ ocrTelemetry: ocrCollector,
2679
+ sttTelemetry: sttCollector,
2680
+ browserTelemetry: browserCollector,
2681
+ cacheTelemetry,
2682
+ stateTelemetry,
2683
+ seedCredentials(rawBody, kind): void {
2684
+ const harvested = rawRequestCredentials(rawBody, kind);
2685
+ sensitiveRegistry.add(harvested.values);
2686
+ // An incomplete inventory cannot certify free text: emit less.
2687
+ if (harvested.exhausted) sensitiveRegistry.suppress();
2688
+ },
2689
+ enrich(enrichment): void {
2690
+ if (terminalOutcome) return;
2691
+ if (enrichment.route !== undefined) details.route = enrichment.route;
2692
+ if (enrichment.requestId !== undefined) details.requestId = enrichment.requestId;
2693
+ if (enrichment.operationId !== undefined) details.operationId = enrichment.operationId;
2694
+ if (enrichment.flowId !== undefined) details.flowId = enrichment.flowId;
2695
+ if (enrichment.headers !== undefined) details.headers = enrichment.headers;
2696
+ if (enrichment.correlation !== undefined) {
2697
+ Object.assign(details.correlation, enrichment.correlation);
2698
+ }
2699
+ updateTraceMetadata();
2700
+ },
2701
+ run<T>(fn: () => Promise<T>): Promise<T> {
2702
+ return rootRunner(fn);
2703
+ },
2704
+ async runStreaming<T>(fn: () => Promise<T>): Promise<T> {
2705
+ streamingSetupPending = true;
2706
+ try {
2707
+ return await rootRunner(fn);
2708
+ } finally {
2709
+ streamingSetupPending = false;
2710
+ settleRoot();
2711
+ }
2712
+ },
2713
+ runStreamStep<T>(fn: () => Promise<T>): Promise<T> {
2714
+ return rootRunner(fn);
2715
+ },
2716
+ watchAbort(signal): void {
2717
+ if (terminalOutcome) return;
2718
+ abortSignal = signal;
2719
+ abortListener = () => {
2720
+ scope.terminalize({ kind: "cancelled", status: CLIENT_CANCELLED_STATUS });
2721
+ void cleanupStream();
2722
+ };
2723
+ if (signal.aborted) abortListener();
2724
+ else signal.addEventListener("abort", abortListener, { once: true });
2725
+ },
2726
+ registerStreamCleanup(cleanup): void {
2727
+ const entry = { cleanup };
2728
+ streamCleanups.push(entry);
2729
+ if (terminalOutcome) void runCleanupEntry(entry);
2730
+ },
2731
+ cleanupStream,
2732
+ snapshotHeaders(error): RequestScopeFinishResult {
2733
+ headersSnapshotted = true;
2734
+ return headerSnapshot(error);
2735
+ },
2736
+ terminalize(outcome): RequestScopeFinishResult {
2737
+ if (finishedResult) return finishedResult;
2738
+ observeEnv.finished = true;
2739
+ terminalOutcome = outcome;
2740
+ const error = terminalError(outcome);
2741
+ finishedResult = {};
2742
+ try {
2743
+ const declaredErrorCode =
2744
+ error === undefined ? undefined : input.declaredErrorCode?.(error);
2745
+ const status =
2746
+ outcome.status ?? (error === undefined ? 200 : toStatusCode(error, declaredErrorCode));
2747
+ finishedResult = headerSnapshot(error);
2748
+ const cost = finishRequestCost(requestCost);
2749
+ try {
2750
+ if (error === undefined) {
2751
+ logProviderSuccess(
2752
+ input.logger,
2753
+ input.provider,
2754
+ input.kind,
2755
+ details.route,
2756
+ details.requestId,
2757
+ status,
2758
+ cost,
2759
+ telemetry,
2760
+ details.correlation,
2761
+ redact,
2762
+ );
2763
+ } else {
2764
+ logProviderError(
2765
+ input.logger,
2766
+ input.provider,
2767
+ input.kind,
2768
+ details.route,
2769
+ details.requestId,
2770
+ error,
2771
+ status,
2772
+ cost,
2773
+ declaredErrorCode,
2774
+ telemetry,
2775
+ finishedResult.errorObservability as ErrorObservabilityDetails,
2776
+ details.correlation,
2777
+ redact,
2778
+ );
2779
+ }
2780
+ } catch {
2781
+ // Observer callbacks cannot hold the request root open or change the response.
2782
+ }
2783
+
2784
+ if (!headersSnapshotted) {
2785
+ if (finishedResult.providerTelemetryHeader) {
2786
+ try {
2787
+ input.setHeader?.(PROVIDER_TELEMETRY_HEADER, finishedResult.providerTelemetryHeader);
2788
+ } catch {
2789
+ // Header observers are isolated independently from request settlement.
2790
+ }
2791
+ }
2792
+ if (finishedResult.errorObservability) {
2793
+ try {
2794
+ input.setHeader?.(
2795
+ ERROR_OBSERVABILITY_HEADER,
2796
+ JSON.stringify(finishedResult.errorObservability),
2797
+ );
2798
+ } catch {
2799
+ // Header observers are isolated independently from request settlement.
2800
+ }
2801
+ }
2802
+ }
2803
+ } finally {
2804
+ settleRoot();
2805
+ void cleanupStream();
2806
+ }
2807
+ return finishedResult;
2808
+ },
2809
+ };
2810
+ return scope;
2811
+ }
2812
+
2813
+ // A raw `Response` result skips `toErrorResponse`. Its payload shape stays
2814
+ // provider-owned; known credentials in error payloads still need redaction.
2815
+ // The observability header is SDK-owned: derive it
2816
+ // from the status. Only timeouts, rate limits, and 5xx are retryable;
2817
+ // `isRetryableCategory` would otherwise mark every other 4xx (`upstream_http`)
2818
+ // as retryable.
2819
+ function rawResponseErrorObservability(status: number): ErrorObservabilityDetails {
2820
+ return {
2821
+ category: categoryForStatus(status),
2822
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
2823
+ retryable: status === 408 || status === 429 || status >= 500,
2824
+ };
2825
+ }
2826
+
2827
+ // `rawStatusFallback` is decided by the caller from the outcome, not from a
2828
+ // missing snapshot: a failed outcome whose thrown value is `undefined` also has
2829
+ // no `errorObservability`, and its 500 carries the SDK envelope, not a raw body.
2830
+ async function responseWithRequestScopeHeaders(
2831
+ response: Response,
2832
+ finished: RequestScopeFinishResult,
2833
+ rawStatusFallback: boolean,
2834
+ redact: DiagnosticRedactor,
2835
+ ): Promise<Response> {
2836
+ const headers = new Headers(response.headers);
2837
+ headers.delete(PROVIDER_TELEMETRY_HEADER);
2838
+ if (finished.providerTelemetryHeader) {
2839
+ headers.set(PROVIDER_TELEMETRY_HEADER, finished.providerTelemetryHeader);
2840
+ }
2841
+ headers.delete(ERROR_OBSERVABILITY_HEADER);
2842
+ const errorObservability =
2843
+ finished.errorObservability ??
2844
+ (rawStatusFallback && response.status >= 400
2845
+ ? rawResponseErrorObservability(response.status)
2846
+ : undefined);
2847
+ if (errorObservability) {
2848
+ headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(errorObservability));
2849
+ }
2850
+ // Raw Response bodies are provider-owned. Preserve their stream and representation.
2851
+ const body = response.body;
2852
+ const statusText =
2853
+ rawStatusFallback && response.status >= 400
2854
+ ? redactDiagnosticText(response.statusText, redact)
2855
+ : response.statusText;
2856
+ return new Response(body, {
2857
+ headers,
2858
+ status: response.status,
2859
+ // When free text cannot be certified, use the SDK's standard HTTP reason
2860
+ // phrase. It preserves the status contract without trusting provider text.
2861
+ statusText:
2862
+ statusText === REDACTION_FAILED && isDiagnosticRedactionSuppressed(redact)
2863
+ ? (STATUS_CODES[response.status] ?? "")
2864
+ : statusText,
2865
+ });
2866
+ }
2867
+
2868
+ async function finalizeRequestResponse(
2869
+ scope: RequestScope,
2870
+ response: Response,
2871
+ outcome: RequestTerminalOutcome,
2872
+ ): Promise<Response> {
2873
+ try {
2874
+ const error = outcome.kind === "failed" ? outcome.error : undefined;
2875
+ const finalResponse = await responseWithRequestScopeHeaders(
2876
+ response,
2877
+ scope.snapshotHeaders(error),
2878
+ outcome.kind === "completed",
2879
+ scope.redact,
2880
+ );
2881
+ scope.terminalize(outcome);
2882
+ return finalResponse;
2883
+ } catch (error) {
2884
+ scope.terminalize({ kind: "failed", status: 500, error });
2885
+ throw error;
2886
+ }
2887
+ }
2888
+
2889
+ function toJsonSuccessResponse(
2890
+ result: unknown,
2891
+ ctx?: ProviderContext,
2892
+ ): Response | OperationSuccessResponse {
2893
+ if (result instanceof Response) {
2894
+ return result;
2895
+ }
2896
+
2897
+ if (result instanceof ReadableStream) {
2898
+ return new Response(result);
2899
+ }
2900
+
2901
+ const cacheMeta = ctx && "cache" in ctx ? ctx.cache.responseMeta() : undefined;
2902
+ const retry = ctx ? retryResponseMeta.get(ctx)?.toTenantRetryPayload() : undefined;
2903
+ type TenantCacheMeta = {
2904
+ hit: boolean;
2905
+ stale: boolean;
2906
+ keys: TenantOpaqueKeys;
2907
+ source?: ClosedEnum<NonNullable<ProviderCacheResponseMeta["source"]>>;
2908
+ };
2909
+ const cache: TenantNeutral<TenantCacheMeta> | undefined = cacheMeta
2910
+ ? {
2911
+ hit: cacheMeta.hit,
2912
+ stale: cacheMeta.stale,
2913
+ // TODO(owner): keep or remove implementation-shaped cache keys from tenant meta.
2914
+ keys: tenantOpaqueKeys(cacheMeta.keys),
2915
+ ...(cacheMeta.source ? { source: closedEnum(cacheMeta.source) } : {}),
2916
+ }
2917
+ : undefined;
2918
+ const meta =
2919
+ cacheMeta || retry
2920
+ ? {
2921
+ ...(cache
2922
+ ? {
2923
+ cached: cache.hit,
2924
+ stale: cache.stale,
2925
+ cache,
2926
+ }
2927
+ : {}),
2928
+ ...(retry ? { retry } : {}),
2929
+ }
2930
+ : undefined;
2931
+ const _neutralMeta: TenantNeutral<NonNullable<typeof meta>> | undefined = meta;
2932
+ void _neutralMeta;
2933
+ return {
2934
+ data: result,
2935
+ ...(meta ? { meta } : {}),
2936
+ };
2937
+ }
2938
+
2939
+ function isAsyncIterable<T = unknown>(value: unknown): value is AsyncIterable<T> {
2940
+ if (!value || typeof value !== "object") return false;
2941
+ const iterator = Reflect.get(value, Symbol.asyncIterator);
2942
+ return typeof iterator === "function";
2943
+ }
2944
+
2945
+ function responseWithCleanup(response: Response, lifecycle: RequestStreamLifecycle): Response {
2946
+ if (!response.body) {
2947
+ lifecycle.terminalize({ kind: "completed", status: response.status });
2948
+ void lifecycle.cleanup();
2949
+ return response;
2950
+ }
2951
+ const reader = response.body.getReader();
2952
+ let cleanupReason: unknown = "request terminalized";
2953
+ lifecycle.registerCleanup(() => lifecycle.runStep(() => reader.cancel(cleanupReason)));
2954
+ const body = new ReadableStream<Uint8Array>(
2955
+ {
2956
+ async pull(controller) {
2957
+ try {
2958
+ const { done, value } = await lifecycle.runStep(() => reader.read());
2959
+ if (done) {
2960
+ controller.close();
2961
+ lifecycle.terminalize({ kind: "completed", status: response.status });
2962
+ await lifecycle.cleanup();
2963
+ return;
2964
+ }
2965
+ if (value) controller.enqueue(value);
2966
+ } catch (error) {
2967
+ lifecycle.terminalize({ kind: "failed", error });
2968
+ await lifecycle.cleanup();
2969
+ controller.error(error);
2970
+ }
2971
+ },
2972
+ async cancel(reason) {
2973
+ cleanupReason = reason;
2974
+ lifecycle.terminalize({ kind: "cancelled", status: CLIENT_CANCELLED_STATUS });
2975
+ await lifecycle.cleanup();
2976
+ },
2977
+ },
2978
+ { highWaterMark: 0 },
2979
+ );
2980
+ return new Response(body, {
2981
+ headers: response.headers,
2982
+ status: response.status,
2983
+ statusText: response.statusText,
2984
+ });
2985
+ }
2986
+
2987
+ async function validateSseEvent(
2988
+ operation: OperationDefinition,
2989
+ event: ProviderStreamEvent,
2990
+ ): Promise<ProviderStreamEvent> {
2991
+ const transport = getSseTransport(operation);
2992
+ const schema = transport?.events?.[event.event];
2993
+ if (!schema) {
2994
+ if (event.event === APIFUSE_STREAM_ERROR_EVENT || event.event === APIFUSE_STREAM_DONE_EVENT) {
2995
+ return event;
2996
+ }
2997
+ throw new ProviderError(
2998
+ `SSE event "${event.event}" is not declared in operation transport.events.`,
2999
+ {
3000
+ code: "SSE_EVENT_UNDECLARED",
3001
+ category: "output_validation",
3002
+ retryable: false,
3003
+ fix: `Add "${event.event}" to transport.events or stop emitting that event.`,
3004
+ },
3005
+ );
3006
+ }
3007
+ const data = await parseSchema(schema, event.data, `transport.events.${event.event}`);
3008
+ return { ...event, data };
3009
+ }
3010
+
3011
+ function byteLength(value: Uint8Array | string): number {
3012
+ if (typeof value === "string") {
3013
+ return new TextEncoder().encode(value).byteLength;
3014
+ }
3015
+ return value.byteLength;
3016
+ }
3017
+
3018
+ function assertStreamPayloadWithinLimit(
3019
+ actualBytes: number,
3020
+ maxBytes: number | undefined,
3021
+ kind: "event" | "chunk",
3022
+ ): void {
3023
+ if (maxBytes === undefined || actualBytes <= maxBytes) return;
3024
+ throw new ProviderError(
3025
+ `Stream ${kind} exceeded declared byte limit (${actualBytes} > ${maxBytes}).`,
3026
+ {
3027
+ code: kind === "event" ? "STREAM_EVENT_TOO_LARGE" : "STREAM_CHUNK_TOO_LARGE",
3028
+ retryable: false,
3029
+ category: "input_validation",
3030
+ fix:
3031
+ kind === "event"
3032
+ ? "Emit smaller SSE events or increase transport.maxEventBytes."
3033
+ : "Emit smaller stream chunks or increase transport.maxChunkBytes.",
3034
+ },
3035
+ );
3036
+ }
3037
+
3038
+ function toSseResponse(
3039
+ operation: OperationDefinition,
3040
+ result: AsyncIterable<ProviderStreamEvent>,
3041
+ lifecycle: RequestStreamLifecycle,
3042
+ requestId?: string,
3043
+ ): Response {
3044
+ const encoder = new TextEncoder();
3045
+ const iterator = result[Symbol.asyncIterator]();
3046
+ let cleanupReason: unknown;
3047
+ lifecycle.registerCleanup(() =>
3048
+ lifecycle.runStep(async () => {
3049
+ await iterator.return?.(cleanupReason);
3050
+ }),
3051
+ );
3052
+ const transport = getSseTransport(operation);
3053
+ let done = false;
3054
+ const body = new ReadableStream<Uint8Array>(
3055
+ {
3056
+ async pull(controller) {
3057
+ try {
3058
+ if (done) {
3059
+ controller.close();
3060
+ lifecycle.terminalize({ kind: "completed", status: 200 });
3061
+ await lifecycle.cleanup();
3062
+ return;
3063
+ }
3064
+ const next = await lifecycle.runStep(() => iterator.next());
3065
+ if (next.done) {
3066
+ done = true;
3067
+ controller.close();
3068
+ lifecycle.terminalize({ kind: "completed", status: 200 });
3069
+ await lifecycle.cleanup();
3070
+ return;
3071
+ }
3072
+ const encodedBytes = await lifecycle.runStep(async () => {
3073
+ const validated = await validateSseEvent(operation, next.value);
3074
+ const encodedEvent = encodeSseEvent(validated);
3075
+ const bytes = encoder.encode(encodedEvent);
3076
+ assertStreamPayloadWithinLimit(bytes.byteLength, transport?.maxEventBytes, "event");
3077
+ return bytes;
3078
+ });
3079
+ controller.enqueue(encodedBytes);
3080
+ } catch (error) {
3081
+ const message = error instanceof Error ? error.message : "Stream failed";
3082
+ controller.enqueue(
3083
+ encoder.encode(
3084
+ encodeSseEvent(
3085
+ streamError(
3086
+ "stream_error",
3087
+ sanitizeDiagnosticText(redactDiagnosticText(message, lifecycle.redact)),
3088
+ {
3089
+ ...(requestId ? { requestId } : {}),
3090
+ },
3091
+ ),
3092
+ ),
3093
+ ),
3094
+ );
3095
+ controller.close();
3096
+ done = true;
3097
+ lifecycle.terminalize({ kind: "failed", error });
3098
+ await lifecycle.cleanup();
3099
+ }
3100
+ },
3101
+ async cancel(reason) {
3102
+ cleanupReason = reason;
3103
+ lifecycle.terminalize({ kind: "cancelled", status: CLIENT_CANCELLED_STATUS });
3104
+ await lifecycle.cleanup();
3105
+ },
3106
+ },
3107
+ { highWaterMark: 0 },
3108
+ );
3109
+ return new Response(body, {
3110
+ headers: {
3111
+ "Cache-Control": "no-cache, no-transform",
3112
+ Connection: "keep-alive",
3113
+ "Content-Type": "text/event-stream; charset=utf-8",
3114
+ },
3115
+ });
3116
+ }
3117
+
3118
+ function enforceStreamChunkLimit(
3119
+ body: ReadableStream<Uint8Array>,
3120
+ maxChunkBytes: number | undefined,
3121
+ ): ReadableStream<Uint8Array> {
3122
+ if (maxChunkBytes === undefined) return body;
3123
+ const reader = body.getReader();
3124
+ return new ReadableStream<Uint8Array>({
3125
+ async pull(controller) {
3126
+ try {
3127
+ const { done, value } = await reader.read();
3128
+ if (done) {
3129
+ controller.close();
3130
+ return;
3131
+ }
3132
+ if (value) {
3133
+ assertStreamPayloadWithinLimit(byteLength(value), maxChunkBytes, "chunk");
3134
+ controller.enqueue(value);
3135
+ }
3136
+ } catch (error) {
3137
+ controller.error(error);
3138
+ }
3139
+ },
3140
+ cancel(reason) {
3141
+ return reader.cancel(reason);
3142
+ },
3143
+ });
3144
+ }
3145
+
3146
+ function toStreamingResponse(
3147
+ operation: OperationDefinition,
3148
+ result: unknown,
3149
+ lifecycle: RequestStreamLifecycle,
3150
+ requestId?: string,
3151
+ ): Response {
3152
+ const transport = operation.transport?.kind ?? "json";
3153
+ if (transport === "sse" && (result instanceof Response || result instanceof ReadableStream)) {
3154
+ throw new ProviderError(
3155
+ "SSE operations must return an AsyncIterable of typed stream.event(...) values.",
3156
+ {
3157
+ code: "SSE_RESULT_UNSUPPORTED",
3158
+ category: "output_validation",
3159
+ retryable: false,
3160
+ fix: "Return an async generator that yields stream.event(name, data) so APIFuse can validate event schemas and enforce event byte limits.",
3161
+ },
3162
+ );
3163
+ }
3164
+ if (result instanceof Response) {
3165
+ const httpTransport = getHttpStreamTransport(operation);
3166
+ if (httpTransport && result.body && httpTransport?.maxChunkBytes !== undefined) {
3167
+ return responseWithCleanup(
3168
+ new Response(enforceStreamChunkLimit(result.body, httpTransport.maxChunkBytes), {
3169
+ headers: result.headers,
3170
+ status: result.status,
3171
+ statusText: result.statusText,
3172
+ }),
3173
+ lifecycle,
3174
+ );
3175
+ }
3176
+ return responseWithCleanup(result, lifecycle);
3177
+ }
3178
+ if (result instanceof ReadableStream) {
3179
+ const httpTransport = getHttpStreamTransport(operation);
3180
+ const stream =
3181
+ httpTransport !== undefined
3182
+ ? enforceStreamChunkLimit(result, httpTransport.maxChunkBytes)
3183
+ : result;
3184
+ return responseWithCleanup(
3185
+ new Response(stream, {
3186
+ headers:
3187
+ transport === "sse"
3188
+ ? { "Content-Type": "text/event-stream; charset=utf-8" }
3189
+ : {
3190
+ "Content-Type":
3191
+ operation.transport?.kind === "http-stream"
3192
+ ? (operation.transport.contentType ?? "application/octet-stream")
3193
+ : "application/octet-stream",
3194
+ },
3195
+ }),
3196
+ lifecycle,
3197
+ );
3198
+ }
3199
+ if (transport === "sse" && isAsyncIterable<ProviderStreamEvent>(result)) {
3200
+ return toSseResponse(operation, result, lifecycle, requestId);
3201
+ }
3202
+ throw new ProviderError(
3203
+ `Streaming operation returned unsupported result for transport "${transport}"`,
3204
+ {
3205
+ code: "STREAM_RESULT_UNSUPPORTED",
3206
+ fix: "Return an AsyncIterable of stream.event(...) values, a ReadableStream, or a Response from streaming operations.",
3207
+ },
3208
+ );
3209
+ }
3210
+
3211
+ function getSseTransport(operation: OperationDefinition): OperationSseTransport | undefined {
3212
+ return operation.transport?.kind === "sse" ? operation.transport : undefined;
3213
+ }
3214
+
3215
+ function getHttpStreamTransport(
3216
+ operation: OperationDefinition,
3217
+ ): OperationHttpStreamTransport | undefined {
3218
+ return operation.transport?.kind === "http-stream" ? operation.transport : undefined;
3219
+ }
3220
+
3221
+ function toAuthFlowResponse(
3222
+ result: unknown,
3223
+ contextPatch: Record<string, unknown | null> | undefined,
3224
+ engine: AuthFlowSuccessResponse["engine"],
3225
+ ): Response | AuthFlowSuccessResponse {
3226
+ if (result instanceof Response) {
3227
+ return result;
3228
+ }
3229
+
3230
+ if (result instanceof ReadableStream) {
3231
+ return new Response(result);
3232
+ }
3233
+
3234
+ return {
3235
+ data: result,
3236
+ ...(contextPatch ? { contextPatch } : {}),
3237
+ ...(engine ? { engine } : {}),
3238
+ };
3239
+ }
3240
+
3241
+ function isAuthTurn(value: unknown): value is AuthTurn {
3242
+ return !!value && typeof value === "object" && "kind" in value && "turnId" in value;
3243
+ }
3244
+
3245
+ /**
3246
+ * Loads the provider's locale catalogs once, at `createServerApp` time.
3247
+ *
3248
+ * Only the locales that actually exist on disk are read: a provider with
3249
+ * `en`+`ko` but no `ja.json` used to make the whole load throw and silently
3250
+ * disable localization for every turn. Absence of catalogs is not an error —
3251
+ * every localized field falls back to its English literal — but a catalog that
3252
+ * exists and cannot be read is logged once so a packaging mistake is visible at
3253
+ * boot instead of per request.
3254
+ */
3255
+ function resolveProviderLocaleCatalogs(
3256
+ provider: ProviderDefinition,
3257
+ options: ProviderServerOptions,
3258
+ logger: ProviderServerLogger,
3259
+ ): ProviderLocaleCatalogMap | undefined {
3260
+ if (options.localeCatalogs) return options.localeCatalogs;
3261
+ for (const providerDir of [
3262
+ process.cwd(),
3263
+ join(process.cwd(), "providers", provider.id),
3264
+ join(process.cwd(), "providers-staging", provider.id),
3265
+ ]) {
3266
+ const locales = PROVIDER_ERROR_LOCALES.filter((locale) =>
3267
+ existsSync(join(providerDir, "locales", `${locale}.json`)),
3268
+ );
3269
+ if (!locales.includes(DEFAULT_PROVIDER_ERROR_LOCALE)) continue;
3270
+ try {
3271
+ return loadProviderLocaleCatalogs({ providerDir, locales });
3272
+ } catch (error) {
3273
+ logger({
3274
+ level: "warn",
3275
+ event: "provider_locale_catalogs_unavailable",
3276
+ providerId: provider.id,
3277
+ reason: sanitizeDiagnosticText(error instanceof Error ? error.message : String(error)),
3278
+ });
3279
+ return undefined;
3280
+ }
3281
+ }
3282
+ return undefined;
3283
+ }
3284
+
3285
+ function materializeAuthFlowTurn(
3286
+ turn: AuthTurn,
3287
+ catalogs: ProviderLocaleCatalogMap | undefined,
3288
+ locale: ProviderLocale,
3289
+ ): AuthTurn {
3290
+ if (!catalogs) return turn;
3291
+ return localizeAuthTurn(turn, { catalogs, locale });
3292
+ }
3293
+
3294
+ function withAuthRequestHeaders(request: AuthFlowRequest, headers: Headers): AuthFlowRequest {
3295
+ return {
3296
+ ...request,
3297
+ headers: {
3298
+ ...(request.headers ?? {}),
3299
+ ...Object.fromEntries(headers.entries()),
3300
+ },
3301
+ };
3302
+ }
3303
+
3304
+ async function handleOperation(
3305
+ provider: ProviderDefinition,
3306
+ request: OperationRequest,
3307
+ operationId: string,
3308
+ options: ProviderServerRuntimeOptions,
3309
+ state: ProviderRuntimeState = createUnsupportedProviderRuntimeState(),
3310
+ scope: RequestScope,
3311
+ signal?: AbortSignal,
3312
+ ): Promise<Response | OperationResponse> {
3313
+ const ctx = createProviderContext(provider, request, operationId, options, state, scope, signal);
3314
+ const operation = provider.operations[operationId];
3315
+ const streaming = operation?.transport?.kind && operation.transport.kind !== "json";
3316
+ let cleanupCalled = false;
3317
+ const cleanup = async () => {
3318
+ if (cleanupCalled) return;
3319
+ cleanupCalled = true;
3320
+ if ("stealth" in ctx) {
3321
+ try {
3322
+ ctx.stealth.close?.();
3323
+ } catch (error) {
3324
+ logProviderCleanupError(
3325
+ options.logger,
3326
+ provider,
3327
+ "operation",
3328
+ operationId,
3329
+ request.requestId,
3330
+ "stealth",
3331
+ error,
3332
+ scope.redact,
3333
+ );
3334
+ }
3335
+ }
3336
+ if ("browser" in ctx) {
3337
+ try {
3338
+ await ctx.browser.close?.();
3339
+ } catch (error) {
3340
+ logProviderCleanupError(
3341
+ options.logger,
3342
+ provider,
3343
+ "operation",
3344
+ operationId,
3345
+ request.requestId,
3346
+ "browser",
3347
+ error,
3348
+ scope.redact,
3349
+ );
3350
+ }
3351
+ }
3352
+ };
3353
+ const streamLifecycle: RequestStreamLifecycle = {
3354
+ redact: scope.redact,
3355
+ runStep<T>(fn: () => Promise<T>): Promise<T> {
3356
+ return scope.runStreamStep(fn);
3357
+ },
3358
+ registerCleanup(streamCleanup): void {
3359
+ scope.registerStreamCleanup(streamCleanup);
3360
+ },
3361
+ terminalize(outcome): void {
3362
+ scope.terminalize(outcome);
3363
+ },
3364
+ cleanup(): Promise<void> {
3365
+ return scope.cleanupStream();
3366
+ },
3367
+ };
3368
+ scope.registerStreamCleanup(() => scope.runStreamStep(cleanup));
3369
+ try {
3370
+ const result = options.operationExecutor
3371
+ ? await options.operationExecutor({
3372
+ provider,
3373
+ operationId,
3374
+ ctx,
3375
+ request,
3376
+ signal,
3377
+ })
3378
+ : await executeOperation(provider, operationId, ctx, request.input, {
3379
+ env: createEnvContext(providerSecretNames(provider)),
3380
+ });
3381
+ if (streaming && operation) {
3382
+ return toStreamingResponse(operation, result, streamLifecycle, request.requestId);
3383
+ }
3384
+ return toJsonSuccessResponse(result, ctx);
3385
+ } catch (error) {
3386
+ await cleanup();
3387
+ throw error;
3388
+ } finally {
3389
+ if (!streaming) await cleanup();
3390
+ }
3391
+ }
3392
+
3393
+ type AuthRoute = "start" | "continue" | "poll" | "abort" | "refresh";
3394
+
3395
+ async function handleAuthFlow(
3396
+ provider: ProviderDefinition,
3397
+ request: AuthFlowRequest,
3398
+ route: AuthRoute,
3399
+ options: ProviderServerRuntimeOptions,
3400
+ state: ProviderRuntimeState,
3401
+ scope: RequestScopeContext,
3402
+ /**
3403
+ * Negotiated once per request by the route so a successful turn and an error
3404
+ * envelope from the same request can never disagree. `withAuthRequestHeaders`
3405
+ * lets the HTTP header shadow the envelope's `Accept-Language`, but the
3406
+ * gateway stamps the flow's start locale into the envelope precisely so
3407
+ * continue/poll turns stay in one language, so the envelope wins here.
3408
+ */
3409
+ locale: ProviderLocale,
3410
+ signal?: AbortSignal,
3411
+ ): Promise<Response | AuthFlowResponse> {
3412
+ const flow = provider.auth?.flow;
3413
+ if (!flow) {
3414
+ throw new ProviderError("Auth flow is not configured", {
3415
+ code: "AUTH_FLOW_NOT_CONFIGURED",
3416
+ });
3417
+ }
3418
+
3419
+ // Same SDK-owned gate as executeOperation: OAuth/credentials ceremonies
3420
+ // depend on declared secrets (client ids/secrets), so fail structured before
3421
+ // any flow code runs instead of at whatever point the ceremony first reads
3422
+ // the env. `abort` stays exempt: a user must always be able to cancel a
3423
+ // stranded flow even when provisioning is broken (createAuthFlowContext
3424
+ // applies the same exemption to the engine ceremony lease).
3425
+ const { context, getPatch, getEngineState } = createAuthFlowContext(
3426
+ provider,
3427
+ request,
3428
+ route,
3429
+ options,
3430
+ state,
3431
+ scope,
3432
+ signal,
3433
+ );
3434
+ try {
3435
+ if (route !== "abort") {
3436
+ assertRequiredSecretsPresent(provider, context.env);
3437
+ }
3438
+ const result =
3439
+ route === "start"
3440
+ ? await flow.start(context)
3441
+ : route === "continue"
3442
+ ? await flow.continue(context, request.input ?? {})
3443
+ : route === "poll"
3444
+ ? flow.poll
3445
+ ? await flow.poll(context)
3446
+ : null
3447
+ : route === "abort"
3448
+ ? flow.abort
3449
+ ? await flow.abort(context)
3450
+ : null
3451
+ : flow.refresh
3452
+ ? await flow.refresh(context, request.input ?? {})
3453
+ : null;
3454
+
3455
+ if (route === "refresh" && !flow.refresh) {
3456
+ throw new AuthError("Provider auth flow does not support refresh.", {
3457
+ code: "refresh_not_supported",
3458
+ });
3459
+ }
3460
+
3461
+ const materializedResult =
3462
+ result &&
3463
+ !(result instanceof Response) &&
3464
+ !(result instanceof ReadableStream) &&
3465
+ isAuthTurn(result)
3466
+ ? materializeAuthFlowTurn(result, options.localeCatalogs, locale)
3467
+ : result;
3468
+ return toAuthFlowResponse(materializedResult, getPatch(), getEngineState());
3469
+ } catch (error) {
3470
+ if (error instanceof AuthAbortError) {
3471
+ return toAuthFlowResponse(error.turn, getPatch(), getEngineState());
3472
+ }
3473
+ throw error;
3474
+ } finally {
3475
+ try {
3476
+ context.stealth.close?.();
3477
+ } catch (error) {
3478
+ logProviderCleanupError(
3479
+ options.logger,
3480
+ provider,
3481
+ "auth",
3482
+ route,
3483
+ request.requestId,
3484
+ "stealth",
3485
+ error,
3486
+ scope.redact,
3487
+ );
3488
+ }
3489
+ }
3490
+ }
3491
+
3492
+ class StatefulForwardingReplayCache {
3493
+ readonly #nonces = new Map<string, number>();
3494
+ readonly #expiryBuckets = new Map<number, Set<string>>();
3495
+ #nextExpiryBucket?: number;
3496
+ #latestExpiryBucket?: number;
3497
+
3498
+ constructor(private readonly maxEntries: number) {}
3499
+
3500
+ claim(nonce: string, expiresAtMs: number, nowMs: number): "accepted" | "replayed" | "full" {
3501
+ this.dropExpiredBuckets(nowMs);
3502
+ if (this.#nonces.has(nonce)) return "replayed";
3503
+ if (this.#nonces.size >= this.maxEntries) return "full";
3504
+ const expiryBucket =
3505
+ Math.ceil(expiresAtMs / STATEFUL_FORWARDING_REPLAY_BUCKET_MS) *
3506
+ STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
3507
+ this.#nonces.set(nonce, expiryBucket);
3508
+ const bucket = this.#expiryBuckets.get(expiryBucket) ?? new Set<string>();
3509
+ bucket.add(nonce);
3510
+ this.#expiryBuckets.set(expiryBucket, bucket);
3511
+ this.#nextExpiryBucket = Math.min(this.#nextExpiryBucket ?? expiryBucket, expiryBucket);
3512
+ this.#latestExpiryBucket = Math.max(this.#latestExpiryBucket ?? expiryBucket, expiryBucket);
3513
+ return "accepted";
3514
+ }
3515
+
3516
+ private dropExpiredBuckets(nowMs: number): void {
3517
+ if (this.#nextExpiryBucket === undefined || this.#latestExpiryBucket === undefined) return;
3518
+ if (nowMs >= this.#latestExpiryBucket) {
3519
+ this.#nonces.clear();
3520
+ this.#expiryBuckets.clear();
3521
+ this.#nextExpiryBucket = undefined;
3522
+ this.#latestExpiryBucket = undefined;
3523
+ return;
3524
+ }
3525
+ while (this.#nextExpiryBucket <= nowMs) {
3526
+ const bucket = this.#expiryBuckets.get(this.#nextExpiryBucket);
3527
+ if (bucket) {
3528
+ for (const cachedNonce of bucket) this.#nonces.delete(cachedNonce);
3529
+ this.#expiryBuckets.delete(this.#nextExpiryBucket);
3530
+ }
3531
+ this.#nextExpiryBucket += STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
3532
+ }
3533
+ }
3534
+ }
3535
+
3536
+ function verifyStatefulForwardingRequest(input: {
3537
+ readonly options: ProviderServerOptions;
3538
+ readonly rawBody: string;
3539
+ readonly headers: Headers;
3540
+ readonly method: string;
3541
+ readonly path: string;
3542
+ readonly replayCache: StatefulForwardingReplayCache;
3543
+ }): void {
3544
+ const config = input.options.statefulForwarding;
3545
+ if (!config?.secret) {
3546
+ throw new ProviderError("Stateful forwarding is not configured.", {
3547
+ code: "STATEFUL_FORWARDING_NOT_CONFIGURED",
3548
+ });
3549
+ }
3550
+ const timestamp = input.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "";
3551
+ const signature = input.headers.get(STATEFUL_FORWARDING_SIGNATURE_HEADER) ?? "";
3552
+ const nonce = input.headers.get(STATEFUL_FORWARDING_NONCE_HEADER) ?? "";
3553
+ if (!timestamp || !signature || !nonce) {
3554
+ throw new ProviderError("Stateful forwarding signature headers are missing.", {
3555
+ code: "STATEFUL_FORWARDING_SIGNATURE_MISSING",
3556
+ });
3557
+ }
3558
+ if (nonce.length > 256) {
3559
+ throw new ProviderError("Stateful forwarding nonce is invalid.", {
3560
+ code: "STATEFUL_FORWARDING_NONCE_INVALID",
3561
+ });
3562
+ }
3563
+ const timestampMs = Date.parse(timestamp);
3564
+ const maxSkewMs = config.maxSkewMs ?? DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS;
3565
+ if (!Number.isFinite(timestampMs) || Math.abs(Date.now() - timestampMs) > maxSkewMs) {
3566
+ throw new ProviderError(
3567
+ "Stateful forwarding signature timestamp is outside the allowed skew.",
3568
+ { code: "STATEFUL_FORWARDING_TIMESTAMP_INVALID" },
3569
+ );
3570
+ }
3571
+ if (
3572
+ !verifyStatefulRequestSignature({
3573
+ secret: config.secret,
3574
+ timestamp,
3575
+ rawBody: input.rawBody,
3576
+ method: input.method,
3577
+ path: input.path,
3578
+ nonce,
3579
+ signature,
3580
+ })
3581
+ ) {
3582
+ throw new ProviderError("Stateful forwarding signature is invalid.", {
3583
+ code: "STATEFUL_FORWARDING_SIGNATURE_INVALID",
3584
+ });
3585
+ }
3586
+ const replayResult = input.replayCache.claim(nonce, timestampMs + maxSkewMs, Date.now());
3587
+ if (replayResult === "replayed") {
3588
+ throw new ProviderError("Stateful forwarding nonce has already been used.", {
3589
+ code: "STATEFUL_FORWARDING_REPLAY_DETECTED",
3590
+ });
3591
+ }
3592
+ if (replayResult === "full") {
3593
+ throw new ProviderError("Stateful forwarding replay cache is at capacity.", {
3594
+ code: "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
3595
+ });
3596
+ }
3597
+ }
3598
+
3599
+ function operationRequestFromForwardingEnvelope(
3600
+ envelope: ProviderServerStatefulForwardEnvelope,
3601
+ ): OperationRequest & { readonly deadlineAt?: string } {
3602
+ return {
3603
+ ...envelope.operationRequest,
3604
+ ...(envelope.deadlineAt !== undefined ? { deadlineAt: envelope.deadlineAt } : {}),
3605
+ };
3606
+ }
3607
+
3608
+ function parseStatefulForwardingEnvelope(
3609
+ rawBody: unknown,
3610
+ redact: DiagnosticRedactor,
3611
+ ): ProviderServerStatefulForwardEnvelope {
3612
+ const parsed = ReceivedStatefulForwardEnvelopeSchema.safeParse(rawBody);
3613
+ if (parsed.success) return parsed.data;
3614
+ throw new ProviderError("Stateful forwarding envelope is invalid.", {
3615
+ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
3616
+ details: zodDetails(parsed.error, redact),
3617
+ });
3618
+ }
3619
+
3620
+ /**
3621
+ * Primary, cross-runtime app factory. Declared capability ESM is preloaded
3622
+ * asynchronously, so this path works on Bun and every supported Node release.
3623
+ */
3624
+ export async function createServerAppAsync<
3625
+ TContext extends Partial<ProviderContext> = ProviderContext,
3626
+ >(
3627
+ provider: ProviderDefinition<TContext>,
3628
+ options: ProviderServerOptions<TContext> = {},
3629
+ ): Promise<Hono> {
3630
+ const runtimeProvider = provider as unknown as ProviderDefinition;
3631
+ const runtimeOptions = options as unknown as ProviderServerOptions;
3632
+ validateFailClosedDeclaration(runtimeProvider);
3633
+ validateStatefulServerConfig(runtimeOptions);
3634
+ return createServerAppWithCapabilityModules(
3635
+ runtimeProvider,
3636
+ runtimeOptions,
3637
+ await loadProviderCapabilityModules(runtimeProvider),
3638
+ );
3639
+ }
3640
+
3641
+ /**
3642
+ * Synchronous compatibility factory. Standard providers remain synchronous on
3643
+ * every runtime because they load no capability modules. Providers declaring a
3644
+ * capability require Bun or Node >=22.12; older Node releases receive an
3645
+ * actionable error directing them to createServerAppAsync().
3646
+ */
3647
+ export function createServerApp<TContext extends Partial<ProviderContext> = ProviderContext>(
3648
+ provider: ProviderDefinition<TContext>,
3649
+ options: ProviderServerOptions<TContext> = {},
3650
+ ): Hono {
3651
+ const runtimeProvider = provider as unknown as ProviderDefinition;
3652
+ const runtimeOptions = options as unknown as ProviderServerOptions;
3653
+ validateFailClosedDeclaration(runtimeProvider);
3654
+ validateStatefulServerConfig(runtimeOptions);
3655
+ return createServerAppWithCapabilityModules(
3656
+ runtimeProvider,
3657
+ runtimeOptions,
3658
+ loadProviderCapabilityModulesSync(runtimeProvider),
3659
+ );
3660
+ }
3661
+
3662
+ /**
3663
+ * Resolve the engine once at boot, before any route exists. An explicit engine
3664
+ * object is used as given; otherwise the resolved mode selects the engine.
3665
+ *
3666
+ * Deliberately never throws for deployment-authored input: a mode this release
3667
+ * cannot serve yields an engine that fails every request and a `/readyz` 503,
3668
+ * because a boot crash would trade a structured signal for CrashLoopBackOff
3669
+ * across the fleet (same trade-off as `provider_secrets_missing` above).
3670
+ */
3671
+ function resolveServerEngine(serverOptions: ProviderServerOptions): {
3672
+ engine: ProviderEngine;
3673
+ resolution: ProviderEngineModeResolution;
3674
+ } {
3675
+ const resolution = resolveProviderEngineMode({
3676
+ option: serverOptions.engineMode,
3677
+ engine: serverOptions.engine,
3678
+ });
3679
+ if (serverOptions.engine !== undefined) return { engine: serverOptions.engine, resolution };
3680
+ return { engine: createEngineForMode(resolution.mode), resolution };
3681
+ }
3682
+
3683
+ /**
3684
+ * `engine_env_ignored` is expected for the whole migration window: the manifest
3685
+ * generator projects the remote engine env fleet-wide before any mode flips, so
3686
+ * escalating on it would turn every healthy boot into a warn line. The
3687
+ * disagreement warnings still escalate.
3688
+ */
3689
+ const ENGINE_MODE_INFO_WARNINGS: ReadonlySet<string> = new Set(["engine_env_ignored"]);
3690
+
3691
+ function engineModeLogEvent(
3692
+ provider: ProviderDefinition,
3693
+ resolution: ProviderEngineModeResolution,
3694
+ attached: boolean,
3695
+ ): ProviderServerLogEvent {
3696
+ const escalating = resolution.warnings.filter(
3697
+ (warning) => !ENGINE_MODE_INFO_WARNINGS.has(warning),
3698
+ );
3699
+ return {
3700
+ level: attached && escalating.length === 0 ? "info" : "warn",
3701
+ event: "provider_engine_mode",
3702
+ providerId: provider.id,
3703
+ mode: resolution.mode,
3704
+ source: resolution.source,
3705
+ deprecated: resolution.deprecated,
3706
+ attached,
3707
+ sdkVersion: SDK_VERSION,
3708
+ ...(provider.runtimeTarget === undefined ? {} : { runtimeTarget: provider.runtimeTarget }),
3709
+ ...(resolution.warnings.length === 0 ? {} : { warnings: resolution.warnings }),
3710
+ };
3711
+ }
3712
+
3713
+ function createServerAppWithCapabilityModules(
3714
+ provider: ProviderDefinition,
3715
+ serverOptions: ProviderServerOptions,
3716
+ capabilityModules: ProviderCapabilityModules,
3717
+ ): Hono {
3718
+ const engineSelection = resolveServerEngine(serverOptions);
3719
+ // Read the provider's locale catalogs once here rather than per turn: the
3720
+ // previous auth-flow loader re-read all three JSON files on every turn and
3721
+ // threw the whole map away when one locale file was absent.
3722
+ const localeCatalogs = resolveProviderLocaleCatalogs(
3723
+ provider,
3724
+ serverOptions,
3725
+ serverOptions.logger ?? defaultProviderServerLogger,
3726
+ );
3727
+ const options: ProviderServerRuntimeOptions = {
3728
+ ...serverOptions,
3729
+ ...(localeCatalogs ? { localeCatalogs } : {}),
3730
+ capabilityModules,
3731
+ engine: engineSelection.engine,
3732
+ };
3733
+ const engineAttached = !isUnavailableProviderEngine(options.engine);
3734
+ const app = new Hono();
3735
+ // Compile the startup inventory once, composed with the shared outside-context fallback.
3736
+ const staticSensitiveValues = compileProcessDiagnosticSensitiveValues(
3737
+ collectStaticDiagnosticSensitiveValues(provider, {
3738
+ statefulForwardingSecret: options.statefulForwarding?.secret,
3739
+ }),
3740
+ );
3741
+ const logger = options.logger ?? defaultProviderServerLogger;
3742
+ const operationErrorCodes = buildOperationErrorCodeLookup(provider);
3743
+ const statefulForwardingReplayCache = new StatefulForwardingReplayCache(
3744
+ options.statefulForwarding?.replayCacheMaxEntries ??
3745
+ DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES,
3746
+ );
3747
+ const state =
3748
+ options.state ??
3749
+ createProviderRuntimeStateFromEnv({
3750
+ providerId: provider.id,
3751
+ allowMemoryFallback: options.allowMemoryStateFallback === true,
3752
+ });
3753
+
3754
+ // Boot-time visibility for unprovisioned declared secrets: emit a structured
3755
+ // warn so deploy tooling/alerting sees the gap the moment the pod boots,
3756
+ // instead of discovering it request-by-request. Deliberately log-only — a
3757
+ // boot crash would trade a structured MISSING_SECRET signal for
3758
+ // CrashLoopBackOff. Requests still fail closed via the executeOperation gate.
3759
+ const missingSecretsAtBoot = listMissingRequiredSecrets(
3760
+ provider,
3761
+ createEnvContext(providerSecretNames(provider)),
3762
+ );
3763
+ if (missingSecretsAtBoot.length > 0) {
3764
+ logger({
3765
+ level: "warn",
3766
+ event: "provider_secrets_missing",
3767
+ providerId: provider.id,
3768
+ missingSecrets: missingSecretsAtBoot,
3769
+ });
3770
+ }
3771
+
3772
+ app.notFound((c) => {
3773
+ // An unknown route is not an upstream answer: the explicit category keeps
3774
+ // the registered not_found code from attributing it to an upstream.
3775
+ const error = new ProviderError("Not found", {
3776
+ code: "not_found",
3777
+ category: "provider_error",
3778
+ retryable: false,
3779
+ });
3780
+ const observabilityDetails = errorObservabilityDetails(error);
3781
+ return responseWithErrorObservability(
3782
+ c.json(
3783
+ toErrorResponse(
3784
+ error,
3785
+ undefined,
3786
+ observabilityDetails,
3787
+ undefined,
3788
+ errorEnvelopeLocalization({
3789
+ catalogs: localeCatalogs,
3790
+ requestHeaders: c.req.raw.headers,
3791
+ }),
3792
+ ),
3793
+ 404,
3794
+ ),
3795
+ observabilityDetails,
3796
+ );
3797
+ });
3798
+
3799
+ // Liveness. Deliberately static and engine-blind: generated manifests point
3800
+ // liveness at this route, so making it depend on engine reachability would
3801
+ // restart every provider pod on one engine incident. Engine state belongs to
3802
+ // /readyz, which manifests wire to readiness and startup only.
3803
+ app.get("/health", (c) =>
3804
+ c.json({
3805
+ status: "ok",
3806
+ provider: provider.id,
3807
+ version: provider.version,
3808
+ }),
3809
+ );
3810
+
3811
+ // Readiness. Reports the boot-resolved attachment; does not probe the engine
3812
+ // per call, so it cannot amplify an engine incident into request load.
3813
+ app.get("/readyz", (c) =>
3814
+ c.json(
3815
+ {
3816
+ status: engineAttached ? "ok" : "unavailable",
3817
+ provider: provider.id,
3818
+ version: provider.version,
3819
+ engine: {
3820
+ // The resolution, not the raw engine `kind`: normalized, so this
3821
+ // body and the `provider_engine_mode` boot event never disagree.
3822
+ mode: engineSelection.resolution.mode,
3823
+ attached: engineAttached,
3824
+ },
3825
+ },
3826
+ engineAttached ? 200 : 503,
3827
+ ),
3828
+ );
3829
+
3830
+ // Internal runtime diagnostics: process CPU over a short window, memory,
3831
+ // the Node event-loop/active-resource APIs and, on Linux, per-thread CPU and
3832
+ // file-descriptor counts from /proc/self. Tells an idle pod that still burns
3833
+ // CPU apart from a busy one without exec. Read-only and secret-free, behind
3834
+ // the same boundary as /health and /readyz: the provider NetworkPolicy only
3835
+ // admits the gateway on this port and the gateway dials canonical operation
3836
+ // paths only, so operators reach it through `kubectl port-forward`.
3837
+ app.get(RUNTIME_DIAGNOSTICS_ROUTE, async (c) => {
3838
+ const report = await collectRuntimeDiagnostics({
3839
+ provider: { id: provider.id, version: provider.version },
3840
+ windowMs: resolveRuntimeDiagnosticsWindowMs(c.req.query(RUNTIME_DIAGNOSTICS_WINDOW_QUERY)),
3841
+ });
3842
+ c.header("Cache-Control", "no-store");
3843
+ return c.json(report);
3844
+ });
3845
+
3846
+ app.post(STATEFUL_INTERNAL_OPERATIONS_ROUTE, async (c) => {
3847
+ let rawBodyText = "";
3848
+ let rawBody: unknown;
3849
+ let operationId: string | undefined;
3850
+ const operation = "stateful-internal";
3851
+ const requestScope = createRequestScope({
3852
+ provider,
3853
+ staticSensitiveValues,
3854
+ kind: "operation",
3855
+ route: operation,
3856
+ headers: Object.fromEntries(c.req.raw.headers.entries()),
3857
+ logger,
3858
+ setHeader: (name, value) => c.header(name, value),
3859
+ declaredErrorCode: (error) => declaredErrorCodeFor(error, operationId, operationErrorCodes),
3860
+ });
3861
+ try {
3862
+ if (!options.internalOperationExecutor) {
3863
+ throw new ProviderError("Stateful internal operation executor is not configured.", {
3864
+ code: "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
3865
+ });
3866
+ }
3867
+ rawBodyText = await c.req.raw.clone().text();
3868
+ verifyStatefulForwardingRequest({
3869
+ options,
3870
+ rawBody: rawBodyText,
3871
+ headers: c.req.raw.headers,
3872
+ method: c.req.raw.method,
3873
+ path: STATEFUL_INTERNAL_OPERATIONS_ROUTE,
3874
+ replayCache: statefulForwardingReplayCache,
3875
+ });
3876
+ try {
3877
+ rawBody = JSON.parse(rawBodyText);
3878
+ } catch {
3879
+ throw new ProviderError("Stateful forwarding envelope is not valid JSON.", {
3880
+ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
3881
+ });
3882
+ }
3883
+ requestScope.seedCredentials(rawBody, "stateful");
3884
+ const envelope = parseStatefulForwardingEnvelope(rawBody, requestScope.redact);
3885
+ if (envelope.providerId !== provider.id) {
3886
+ throw new ProviderError(
3887
+ "Stateful forwarding envelope providerId does not match the served provider.",
3888
+ { code: "STATEFUL_FORWARDING_PROVIDER_MISMATCH" },
3889
+ );
3890
+ }
3891
+ if (envelope.requestId !== envelope.operationRequest.requestId) {
3892
+ throw new ProviderError("Stateful forwarding requestId values do not match.", {
3893
+ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
3894
+ });
3895
+ }
3896
+ if (
3897
+ envelope.sourcePodId !==
3898
+ (c.req.raw.headers.get(STATEFUL_FORWARDING_SOURCE_POD_HEADER) ?? "")
3899
+ ) {
3900
+ throw new ProviderError("Stateful forwarding source pod does not match its header.", {
3901
+ code: "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
3902
+ });
3903
+ }
3904
+ if (
3905
+ envelope.forwardedAt !== (c.req.raw.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "")
3906
+ ) {
3907
+ throw new ProviderError(
3908
+ "Stateful forwarding forwardedAt does not match its signature timestamp.",
3909
+ { code: "STATEFUL_FORWARDING_ENVELOPE_INVALID" },
3910
+ );
3911
+ }
3912
+ const deadlineAtMs = envelope.deadlineAt ? Date.parse(envelope.deadlineAt) : undefined;
3913
+ if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
3914
+ throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
3915
+ }
3916
+ const remainingDeadlineMs =
3917
+ deadlineAtMs === undefined ? undefined : deadlineAtMs - Date.now();
3918
+ const deadlineSignal =
3919
+ remainingDeadlineMs === undefined ? undefined : AbortSignal.timeout(remainingDeadlineMs);
3920
+ const signal = deadlineSignal
3921
+ ? AbortSignal.any([c.req.raw.signal, deadlineSignal])
3922
+ : c.req.raw.signal;
3923
+ const ownerFenceValidation = Promise.resolve(
3924
+ options.statefulForwarding?.validateOwnerFence(
3925
+ {
3926
+ providerId: envelope.providerId,
3927
+ sessionKey: envelope.sessionKey,
3928
+ ownerPodId: envelope.ownerPodId,
3929
+ generation: envelope.generation,
3930
+ sourcePodId: envelope.sourcePodId,
3931
+ forwardedAt: envelope.forwardedAt,
3932
+ requestId: envelope.requestId,
3933
+ ...(envelope.idempotencyKey ? { idempotencyKey: envelope.idempotencyKey } : {}),
3934
+ },
3935
+ signal,
3936
+ ),
3937
+ );
3938
+ let ownerFenceValid: boolean | undefined;
3939
+ try {
3940
+ ownerFenceValid = deadlineSignal
3941
+ ? await Promise.race([
3942
+ ownerFenceValidation,
3943
+ new Promise<never>((_resolve, reject) => {
3944
+ deadlineSignal.addEventListener(
3945
+ "abort",
3946
+ () =>
3947
+ reject(
3948
+ new StatefulRoutingDeadlineError(
3949
+ envelope.requestId,
3950
+ envelope.deadlineAt as string,
3951
+ ),
3952
+ ),
3953
+ { once: true },
3954
+ );
3955
+ }),
3956
+ ])
3957
+ : await ownerFenceValidation;
3958
+ } catch (error) {
3959
+ if (deadlineSignal?.aborted) {
3960
+ throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
3961
+ }
3962
+ throw error;
3963
+ }
3964
+ if (ownerFenceValid !== true) {
3965
+ throw new ProviderError("Stateful forwarding owner fence is no longer current.", {
3966
+ code: "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
3967
+ });
3968
+ }
3969
+ const request = operationRequestFromForwardingEnvelope(envelope);
3970
+ operationId = envelope.operationId;
3971
+ requestScope.enrich({
3972
+ route: operationId,
3973
+ requestId: request.requestId,
3974
+ operationId,
3975
+ headers: request.headers ?? {},
3976
+ correlation: {
3977
+ connectionId: envelope.connectionId,
3978
+ tenantId: resolveOperationTenantId(request),
3979
+ },
3980
+ });
3981
+ const response = await requestScope.run(async () => {
3982
+ const ctx = createProviderContext(
3983
+ provider,
3984
+ request,
3985
+ operationId as string,
3986
+ options,
3987
+ state,
3988
+ requestScope as RequestScope,
3989
+ signal,
3990
+ );
3991
+ if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
3992
+ throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
3993
+ }
3994
+ const output = await options.internalOperationExecutor!({
3995
+ provider,
3996
+ operationId: operationId as string,
3997
+ ctx,
3998
+ request,
3999
+ internalStatefulForward: envelope,
4000
+ signal,
4001
+ });
4002
+ return c.json({ data: output });
4003
+ });
4004
+ return finalizeRequestResponse(requestScope, response, {
4005
+ kind: "completed",
4006
+ status: 200,
4007
+ });
4008
+ } catch (error) {
4009
+ const declaredErrorCode = declaredErrorCodeFor(error, operationId, operationErrorCodes);
4010
+ const status = toStatusCode(error, declaredErrorCode);
4011
+ if (
4012
+ isProviderError(error) &&
4013
+ providerErrorCode(error) === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL"
4014
+ ) {
4015
+ c.header("Retry-After", String(STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS));
4016
+ }
4017
+ const requestId = extractRequestId(rawBody);
4018
+ requestScope.enrich({
4019
+ ...(operationId ? { route: operationId, operationId } : {}),
4020
+ ...(requestId ? { requestId } : {}),
4021
+ });
4022
+ const observabilityDetails = errorObservabilityDetails(error, declaredErrorCode);
4023
+ const response = c.json(
4024
+ toErrorResponse(
4025
+ error,
4026
+ requestId,
4027
+ observabilityDetails,
4028
+ requestScope.redact,
4029
+ errorEnvelopeLocalization({
4030
+ catalogs: localeCatalogs,
4031
+ requestHeaders: c.req.raw.headers,
4032
+ rawBody,
4033
+ ...(declaredErrorCode ? { declaredErrorCode } : {}),
4034
+ }),
4035
+ ),
4036
+ status,
4037
+ );
4038
+ return finalizeRequestResponse(requestScope, response, {
4039
+ kind: "failed",
4040
+ status,
4041
+ error,
4042
+ });
4043
+ }
4044
+ });
4045
+
4046
+ app.post("/v1/:operation", async (c) => {
4047
+ let rawBody: unknown;
4048
+ const operation = c.req.param("operation");
4049
+ const requestScope = createRequestScope({
4050
+ provider,
4051
+ staticSensitiveValues,
4052
+ kind: "operation",
4053
+ route: operation,
4054
+ operationId: operation,
4055
+ headers: Object.fromEntries(c.req.raw.headers.entries()),
4056
+ logger,
4057
+ setHeader: (name, value) => c.header(name, value),
4058
+ declaredErrorCode: (error) => declaredErrorCodeFor(error, operation, operationErrorCodes),
4059
+ });
4060
+ try {
4061
+ rawBody = await c.req.raw
4062
+ .clone()
4063
+ .json()
4064
+ .catch(() => undefined);
4065
+ requestScope.seedCredentials(rawBody, "operation");
4066
+ const body = OperationRequestSchema.parse(rawBody);
4067
+ const requestHeaders = Object.fromEntries(c.req.raw.headers.entries());
4068
+ body.headers = { ...requestHeaders, ...body.headers };
4069
+ requestScope.enrich({
4070
+ requestId: body.requestId,
4071
+ headers: body.headers,
4072
+ correlation: {
4073
+ connectionId: resolveOperationConnectionId(body),
4074
+ tenantId: resolveOperationTenantId(body),
4075
+ },
4076
+ });
4077
+ const streaming = provider.operations[operation]?.transport?.kind
4078
+ ? provider.operations[operation]?.transport?.kind !== "json"
4079
+ : false;
4080
+ if (streaming) requestScope.watchAbort(c.req.raw.signal);
4081
+ const execute = async () => {
4082
+ const handled = await handleOperation(
4083
+ provider,
4084
+ body,
4085
+ operation,
4086
+ options,
4087
+ state,
4088
+ requestScope,
4089
+ c.req.raw.signal,
4090
+ );
4091
+ const response = handled instanceof Response ? handled : c.json(handled);
4092
+ return streaming
4093
+ ? responseWithRequestScopeHeaders(
4094
+ response,
4095
+ requestScope.snapshotHeaders(),
4096
+ true,
4097
+ requestScope.redact,
4098
+ )
4099
+ : response;
4100
+ };
4101
+ const response = streaming
4102
+ ? await requestScope.runStreaming(execute)
4103
+ : await requestScope.run(execute);
4104
+ if (streaming) return response;
4105
+ return finalizeRequestResponse(requestScope, response, {
4106
+ kind: "completed",
4107
+ status: response.status,
4108
+ });
4109
+ } catch (error) {
4110
+ const declaredErrorCode = declaredErrorCodeFor(error, operation, operationErrorCodes);
4111
+ const status = toStatusCode(error, declaredErrorCode);
4112
+ const requestId = extractRequestId(rawBody);
4113
+ requestScope.enrich({ ...(requestId ? { requestId } : {}) });
4114
+ const observabilityDetails = errorObservabilityDetails(error, declaredErrorCode);
4115
+ const response = c.json(
4116
+ toErrorResponse(
4117
+ error,
4118
+ requestId,
4119
+ observabilityDetails,
4120
+ requestScope.redact,
4121
+ errorEnvelopeLocalization({
4122
+ catalogs: localeCatalogs,
4123
+ requestHeaders: c.req.raw.headers,
4124
+ rawBody,
4125
+ ...(declaredErrorCode ? { declaredErrorCode } : {}),
4126
+ }),
4127
+ ),
4128
+ status,
4129
+ );
4130
+ return finalizeRequestResponse(requestScope, response, {
4131
+ kind: "failed",
4132
+ status,
4133
+ error,
4134
+ });
4135
+ }
4136
+ });
4137
+
4138
+ const authRoutes = [
4139
+ { path: "/auth/start", flowRoute: "start", logRoute: "start" },
4140
+ { path: "/auth/continue", flowRoute: "continue", logRoute: "continue" },
4141
+ { path: "/auth/poll", flowRoute: "poll", logRoute: "poll" },
4142
+ { path: "/auth/refresh", flowRoute: "refresh", logRoute: "refresh" },
4143
+ { path: "/auth/disconnect", flowRoute: "abort", logRoute: "disconnect" },
4144
+ ] as const satisfies ReadonlyArray<{
4145
+ path: string;
4146
+ flowRoute: AuthRoute;
4147
+ logRoute: string;
4148
+ }>;
4149
+
4150
+ for (const { path, flowRoute, logRoute } of authRoutes) {
4151
+ app.post(path, async (c) => {
4152
+ let rawBody: unknown;
4153
+ // Negotiated once and shared by the success and failure paths so an
4154
+ // auth turn and an error envelope from the same request agree.
4155
+ let localization = errorEnvelopeLocalization({
4156
+ catalogs: localeCatalogs,
4157
+ requestHeaders: c.req.raw.headers,
4158
+ });
4159
+ const requestScope = createRequestScope({
4160
+ provider,
4161
+ staticSensitiveValues,
4162
+ kind: "auth",
4163
+ route: logRoute,
4164
+ headers: Object.fromEntries(c.req.raw.headers.entries()),
4165
+ logger,
4166
+ setHeader: (name, value) => c.header(name, value),
4167
+ });
4168
+ try {
4169
+ rawBody = await c.req.raw
4170
+ .clone()
4171
+ .json()
4172
+ .catch(() => undefined);
4173
+ localization = errorEnvelopeLocalization({
4174
+ catalogs: localeCatalogs,
4175
+ requestHeaders: c.req.raw.headers,
4176
+ rawBody,
4177
+ });
4178
+ requestScope.seedCredentials(rawBody, "auth");
4179
+ const body = withAuthRequestHeaders(
4180
+ AuthFlowRequestSchema.parse(rawBody),
4181
+ c.req.raw.headers,
4182
+ );
4183
+ requestScope.enrich({
4184
+ requestId: body.requestId,
4185
+ flowId: body.flowId,
4186
+ headers: body.headers ?? {},
4187
+ correlation: {
4188
+ connectionId: resolveOperationConnectionId(body),
4189
+ flowId: body.flowId,
4190
+ tenantId: normalizeIdentifier(body.tenantId),
4191
+ requestedProviderId:
4192
+ body.providerId !== undefined && body.providerId !== provider.id
4193
+ ? body.providerId
4194
+ : undefined,
4195
+ },
4196
+ });
4197
+ const response = await requestScope.run(async () => {
4198
+ const handled = await handleAuthFlow(
4199
+ provider,
4200
+ body,
4201
+ flowRoute,
4202
+ options,
4203
+ state,
4204
+ requestScope as RequestScope,
4205
+ localization.locale,
4206
+ c.req.raw.signal,
4207
+ );
4208
+ return handled instanceof Response ? handled : c.json(handled);
4209
+ });
4210
+ return finalizeRequestResponse(requestScope, response, {
4211
+ kind: "completed",
4212
+ status: response.status,
4213
+ });
4214
+ } catch (error) {
4215
+ const status = toStatusCode(error);
4216
+ const requestId = extractRequestId(rawBody);
4217
+ requestScope.enrich({ ...(requestId ? { requestId } : {}) });
4218
+ const observabilityDetails = errorObservabilityDetails(error);
4219
+ const response = c.json(
4220
+ toErrorResponse(
4221
+ error,
4222
+ requestId,
4223
+ observabilityDetails,
4224
+ requestScope.redact,
4225
+ localization,
4226
+ ),
4227
+ status,
4228
+ );
4229
+ return finalizeRequestResponse(requestScope, response, {
4230
+ kind: "failed",
4231
+ status,
4232
+ error,
4233
+ });
4234
+ }
4235
+ });
4236
+ }
4237
+
4238
+ return app;
4239
+ }
4240
+
4241
+ function validateStatefulServerConfig(options: ProviderServerOptions): void {
4242
+ if (options.statefulForwarding && !options.internalOperationExecutor) {
4243
+ throw new Error(
4244
+ "Invalid provider server configuration: statefulForwarding requires internalOperationExecutor; missing option internalOperationExecutor.",
4245
+ );
4246
+ }
4247
+ if (options.internalOperationExecutor && !options.statefulForwarding?.secret) {
4248
+ throw new Error(
4249
+ "Invalid provider server configuration: internalOperationExecutor requires statefulForwarding.secret; missing option statefulForwarding.secret.",
4250
+ );
4251
+ }
4252
+ if (
4253
+ options.statefulForwarding &&
4254
+ typeof options.statefulForwarding.validateOwnerFence !== "function"
4255
+ ) {
4256
+ throw new Error(
4257
+ "Invalid provider server configuration: statefulForwarding requires validateOwnerFence.",
4258
+ );
4259
+ }
4260
+ if (
4261
+ options.statefulForwarding?.maxSkewMs !== undefined &&
4262
+ (!Number.isFinite(options.statefulForwarding.maxSkewMs) ||
4263
+ options.statefulForwarding.maxSkewMs <= 0)
4264
+ ) {
4265
+ throw new Error("Invalid provider server configuration: maxSkewMs must be positive.");
4266
+ }
4267
+ if (
4268
+ options.statefulForwarding?.replayCacheMaxEntries !== undefined &&
4269
+ (!Number.isInteger(options.statefulForwarding.replayCacheMaxEntries) ||
4270
+ options.statefulForwarding.replayCacheMaxEntries <= 0)
4271
+ ) {
4272
+ throw new Error(
4273
+ "Invalid provider server configuration: replayCacheMaxEntries must be a positive integer.",
4274
+ );
4275
+ }
4276
+ }
4277
+
4278
+ type BunServeRuntime = {
4279
+ serve: (options: {
4280
+ port: number;
4281
+ hostname: string;
4282
+ fetch: (request: Request) => Response | Promise<Response>;
4283
+ }) => BunServerHandle;
4284
+ };
4285
+
4286
+ type BunServerHandle = {
4287
+ readonly port: number;
4288
+ stop(closeActiveConnections?: boolean): Promise<void>;
4289
+ };
4290
+
4291
+ function getBunServeRuntime(): BunServeRuntime | undefined {
4292
+ const bunValue = Object.getOwnPropertyDescriptor(globalThis, "Bun")?.value;
4293
+ if (!bunValue || typeof bunValue !== "object") {
4294
+ return undefined;
4295
+ }
4296
+
4297
+ const serve = Object.getOwnPropertyDescriptor(bunValue, "serve")?.value;
4298
+ if (typeof serve !== "function") {
4299
+ return undefined;
4300
+ }
4301
+
4302
+ return {
4303
+ serve(options) {
4304
+ return serve(options) as BunServerHandle;
4305
+ },
4306
+ };
4307
+ }
4308
+
4309
+ export type ProviderServerCloseOptions = {
4310
+ readonly timeoutMs?: number;
4311
+ };
4312
+
4313
+ export type ProviderServerHandle = {
4314
+ readonly port: number;
4315
+ close(options?: ProviderServerCloseOptions): Promise<void>;
4316
+ };
4317
+
4318
+ export interface ServeOptions<TContext extends Partial<ProviderContext> = ProviderContext>
4319
+ extends ProviderServerOptions<TContext> {
4320
+ host?: string;
4321
+ port?: number;
4322
+ /**
4323
+ * Port for the internal self-test listener (default 3001 or
4324
+ * APIFUSE__PROVIDER_RUNTIME__SELF_TEST_PORT). The listener only starts
4325
+ * when APIFUSE__PROVIDER_RUNTIME__SELF_TEST_MASTER_SECRET is present.
4326
+ */
4327
+ selfTestPort?: number;
4328
+ }
4329
+
4330
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
4331
+ const DEFAULT_SHUTDOWN_SIGNALS: NodeJS.Signals[] = ["SIGTERM", "SIGINT"];
4332
+
4333
+ type SignalServerRegistration = {
4334
+ readonly signals: ReadonlySet<NodeJS.Signals>;
4335
+ readonly close: () => Promise<void>;
4336
+ };
4337
+
4338
+ type ProcessSignalCoordinator = {
4339
+ readonly registrations: Set<SignalServerRegistration>;
4340
+ readonly listener: () => void;
4341
+ handling: boolean;
4342
+ };
4343
+
4344
+ const processSignalCoordinators = new Map<NodeJS.Signals, ProcessSignalCoordinator>();
4345
+
4346
+ export async function serve<TContext extends Partial<ProviderContext> = ProviderContext>(
4347
+ provider: ProviderDefinition<TContext>,
4348
+ options: ServeOptions<TContext> = {},
4349
+ ): Promise<ProviderServerHandle> {
4350
+ const bunRuntime = getBunServeRuntime();
4351
+
4352
+ if (bunRuntime === undefined) {
4353
+ throw new ProviderError("Bun runtime is required to start the provider server", {
4354
+ code: "RUNTIME_UNSUPPORTED",
4355
+ });
4356
+ }
4357
+ const logger = options.logger ?? defaultProviderServerLogger;
4358
+ const configuredTimeoutMs = shutdownTimeout(
4359
+ options.shutdown?.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS,
4360
+ );
4361
+ const configuredSignals = resolveShutdownSignals(options.shutdown?.signals ?? true);
4362
+ const selfTestSecrets = resolveSelfTestMasterSecrets();
4363
+ // Resolve the engine here so the boot event reports the truthful source
4364
+ // (option/env/default) and the app is handed the engine already selected —
4365
+ // one resolution per process, not one per app construction.
4366
+ const engineSelection = resolveServerEngine(options as unknown as ProviderServerOptions);
4367
+ const serverAppOptions: ProviderServerOptions<TContext> = {
4368
+ logger: options.logger,
4369
+ engine: engineSelection.engine,
4370
+ files: options.files,
4371
+ ocr: options.ocr,
4372
+ stt: options.stt,
4373
+ resolver: options.resolver,
4374
+ localeCatalogs: options.localeCatalogs,
4375
+ state: options.state,
4376
+ allowMemoryStateFallback: options.allowMemoryStateFallback,
4377
+ operationExecutor: options.operationExecutor,
4378
+ internalOperationExecutor: options.internalOperationExecutor,
4379
+ statefulForwarding: options.statefulForwarding,
4380
+ };
4381
+ const [app, selfTestModule] = await Promise.all([
4382
+ createServerAppAsync(provider, serverAppOptions),
4383
+ selfTestSecrets ? import("./self-test.js") : undefined,
4384
+ ]);
4385
+ // One structured line per process boot, before the listener binds: the fleet
4386
+ // audit (ADR-0011 migration) counts these.
4387
+ logger(
4388
+ engineModeLogEvent(
4389
+ provider as unknown as ProviderDefinition,
4390
+ engineSelection.resolution,
4391
+ !isUnavailableProviderEngine(engineSelection.engine),
4392
+ ),
4393
+ );
4394
+ // Same contract for the stores: one line per process boot naming the state
4395
+ // and cache backends this pod resolved, so "which Redis is this provider's
4396
+ // state actually on" is a log grep instead of an inference from the
4397
+ // Deployment spec (apifuse#2302).
4398
+ logger(
4399
+ providerStateBackendLogEvent({
4400
+ providerId: provider.id,
4401
+ sdkVersion: SDK_VERSION,
4402
+ injectedState: options.state !== undefined,
4403
+ allowMemoryFallback: options.allowMemoryStateFallback === true,
4404
+ }),
4405
+ );
4406
+
4407
+ const servers: BunServerHandle[] = [];
4408
+ try {
4409
+ servers.push(
4410
+ bunRuntime.serve({
4411
+ port: options.port ?? DEFAULT_PORT,
4412
+ hostname: options.host ?? DEFAULT_HOST,
4413
+ fetch: app.fetch,
4414
+ }),
4415
+ );
4416
+
4417
+ // Internal self-test listener (health dependency inversion): a SEPARATE
4418
+ // socket the tenant-facing gateway never dials. Off by default — it only
4419
+ // starts when the shared self-test master secret env is present.
4420
+ if (selfTestSecrets && selfTestModule) {
4421
+ const selfTestApp = selfTestModule.createSelfTestApp(
4422
+ provider as unknown as ProviderDefinition,
4423
+ {
4424
+ secrets: selfTestSecrets,
4425
+ invoke: selfTestModule.createSelfTestInvoke(app),
4426
+ authFlow: selfTestModule.createSelfTestAuthFlowInvoke(app),
4427
+ logger,
4428
+ },
4429
+ );
4430
+ servers.push(
4431
+ bunRuntime.serve({
4432
+ port: options.selfTestPort ?? selfTestModule.resolveSelfTestPort(),
4433
+ hostname: options.host ?? DEFAULT_HOST,
4434
+ fetch: selfTestApp.fetch,
4435
+ }),
4436
+ );
4437
+ }
4438
+ } catch (error) {
4439
+ await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
4440
+ throw error;
4441
+ }
4442
+
4443
+ const server = servers[0];
4444
+ if (!server) throw new Error("Provider server failed to create its primary listener.");
4445
+ let closePromise: Promise<void> | undefined;
4446
+ let unregisterSignals = () => {};
4447
+
4448
+ const close = (closeOptions: ProviderServerCloseOptions = {}): Promise<void> => {
4449
+ if (closePromise) return closePromise;
4450
+ const timeoutMs = shutdownTimeout(closeOptions.timeoutMs ?? configuredTimeoutMs);
4451
+ closePromise = closeProviderServers({
4452
+ servers,
4453
+ hooks: options.shutdown?.hooks ?? [],
4454
+ timeoutMs,
4455
+ logger,
4456
+ providerId: provider.id,
4457
+ }).finally(() => unregisterSignals());
4458
+ return closePromise;
4459
+ };
4460
+
4461
+ try {
4462
+ unregisterSignals = registerForProcessSignals(configuredSignals, () =>
4463
+ close({ timeoutMs: configuredTimeoutMs }),
4464
+ );
4465
+ } catch (error) {
4466
+ unregisterSignals();
4467
+ await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
4468
+ throw error;
4469
+ }
4470
+
4471
+ return { port: server.port, close };
4472
+ }
4473
+
4474
+ function registerForProcessSignals(
4475
+ signals: NodeJS.Signals[],
4476
+ close: () => Promise<void>,
4477
+ ): () => void {
4478
+ if (signals.length === 0) return () => {};
4479
+ const registration: SignalServerRegistration = {
4480
+ signals: new Set(signals),
4481
+ close,
4482
+ };
4483
+ let registered = true;
4484
+ const unregister = () => {
4485
+ if (!registered) return;
4486
+ registered = false;
4487
+ for (const signal of registration.signals) {
4488
+ const coordinator = processSignalCoordinators.get(signal);
4489
+ if (!coordinator) continue;
4490
+ coordinator.registrations.delete(registration);
4491
+ if (coordinator.registrations.size === 0 && !coordinator.handling) {
4492
+ process.removeListener(signal, coordinator.listener);
4493
+ processSignalCoordinators.delete(signal);
4494
+ }
4495
+ }
4496
+ };
4497
+ try {
4498
+ for (const signal of signals) {
4499
+ let coordinator = processSignalCoordinators.get(signal);
4500
+ if (!coordinator) {
4501
+ const created: ProcessSignalCoordinator = {
4502
+ registrations: new Set(),
4503
+ handling: false,
4504
+ listener: () => handleCoordinatedSignal(signal, created),
4505
+ };
4506
+ coordinator = created;
4507
+ processSignalCoordinators.set(signal, coordinator);
4508
+ process.on(signal, coordinator.listener);
4509
+ }
4510
+ coordinator.registrations.add(registration);
4511
+ }
4512
+ } catch (error) {
4513
+ unregister();
4514
+ throw error;
4515
+ }
4516
+ return unregister;
4517
+ }
4518
+
4519
+ function handleCoordinatedSignal(
4520
+ signal: NodeJS.Signals,
4521
+ coordinator: ProcessSignalCoordinator,
4522
+ ): void {
4523
+ if (coordinator.handling) return;
4524
+ coordinator.handling = true;
4525
+ const registrations = [...coordinator.registrations];
4526
+ void Promise.allSettled(registrations.map((registration) => registration.close())).finally(() => {
4527
+ if (processSignalCoordinators.get(signal) === coordinator) {
4528
+ process.removeListener(signal, coordinator.listener);
4529
+ processSignalCoordinators.delete(signal);
4530
+ }
4531
+ try {
4532
+ process.kill(process.pid, signal);
4533
+ } catch {
4534
+ process.exitCode = 1;
4535
+ }
4536
+ });
4537
+ }
4538
+
4539
+ async function closeProviderServers(input: {
4540
+ readonly servers: BunServerHandle[];
4541
+ readonly hooks: Array<() => Promise<void>>;
4542
+ readonly timeoutMs: number;
4543
+ readonly logger: ProviderServerLogger;
4544
+ readonly providerId: string;
4545
+ }): Promise<void> {
4546
+ const deadline = Date.now() + input.timeoutMs;
4547
+ const gracefulStops = input.servers.map((server) => server.stop(false));
4548
+ for (const gracefulStop of gracefulStops) gracefulStop.catch(() => undefined);
4549
+ for (const [hookIndex, hook] of input.hooks.entries()) {
4550
+ try {
4551
+ await withinShutdownBudget(Promise.resolve().then(hook), deadline);
4552
+ } catch (error) {
4553
+ try {
4554
+ input.logger({
4555
+ level: "error",
4556
+ event: "provider_shutdown_hook_failed",
4557
+ providerId: input.providerId,
4558
+ hookIndex,
4559
+ errorClass: error instanceof Error ? error.name : "UnknownError",
4560
+ message: error instanceof Error ? error.message : "Shutdown hook failed.",
4561
+ });
4562
+ } catch {}
4563
+ }
4564
+ }
4565
+ const forcedStops = input.servers.map((server) => server.stop(true));
4566
+ await withinShutdownBudget(
4567
+ Promise.allSettled([...gracefulStops, ...forcedStops]).then(() => undefined),
4568
+ deadline,
4569
+ ).catch(() => undefined);
4570
+ }
4571
+
4572
+ async function withinShutdownBudget<T>(promise: Promise<T>, deadline: number): Promise<T> {
4573
+ promise.catch(() => undefined);
4574
+ const remainingMs = Math.max(0, deadline - Date.now());
4575
+ let timer: ReturnType<typeof setTimeout> | undefined;
4576
+ const timeout = new Promise<never>((_resolve, reject) => {
4577
+ timer = setTimeout(() => reject(new Error("Provider server shutdown timed out.")), remainingMs);
4578
+ });
4579
+ try {
4580
+ return await Promise.race([promise, timeout]);
4581
+ } finally {
4582
+ if (timer) clearTimeout(timer);
4583
+ }
4584
+ }
4585
+
4586
+ function resolveShutdownSignals(signals: boolean | NodeJS.Signals[]): NodeJS.Signals[] {
4587
+ if (signals === false) return [];
4588
+ return [...new Set(signals === true ? DEFAULT_SHUTDOWN_SIGNALS : signals)];
4589
+ }
4590
+
4591
+ function shutdownTimeout(value: number): number {
4592
+ if (!Number.isFinite(value) || value < 0) {
4593
+ throw new Error("Provider server shutdown timeoutMs must be a non-negative finite number.");
4594
+ }
4595
+ return value;
4596
+ }