@apifuse/provider-sdk 2.2.0-beta.4 → 2.2.0-beta.40

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 (303) hide show
  1. package/AUTHORING.md +501 -13
  2. package/CHANGELOG.md +165 -1
  3. package/README.md +72 -14
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +88 -4
  6. package/bin/apifuse-dev.ts +38 -5
  7. package/bin/apifuse-pack-check.ts +22 -2
  8. package/bin/apifuse-pack-smoke.ts +57 -2
  9. package/bin/apifuse-pack-types.ts +356 -38
  10. package/bin/apifuse-perf.ts +14 -13
  11. package/bin/apifuse-record.ts +691 -68
  12. package/bin/apifuse-submit-check.ts +518 -37
  13. package/bin/apifuse-sync-assets.ts +117 -0
  14. package/bin/submit-check-delimited-text.ts +50 -0
  15. package/dist/auth-turn/index.d.ts +3 -3
  16. package/dist/auth-turn/index.js +1 -1
  17. package/dist/auth.d.ts +14 -0
  18. package/dist/auth.js +67 -0
  19. package/dist/ceremonies/index.d.ts +16 -0
  20. package/dist/ceremonies/index.js +141 -36
  21. package/dist/cli/commands.d.ts +1 -1
  22. package/dist/cli/commands.js +8 -0
  23. package/dist/cli/create.d.ts +3 -0
  24. package/dist/cli/create.js +34 -35
  25. package/dist/cli/prompt-assets.d.ts +80 -0
  26. package/dist/cli/prompt-assets.js +743 -0
  27. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  28. package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
  29. package/dist/cli/templates/provider/README.md.tpl +4 -4
  30. package/dist/cli/templates/provider/index.ts.tpl +6 -3
  31. package/dist/cli/templates/provider/operations/ping.ts.tpl +2 -1
  32. package/dist/config/loader.d.ts +177 -16
  33. package/dist/config/loader.js +424 -127
  34. package/dist/contract-serialization.js +4 -8
  35. package/dist/contract-types.d.ts +1 -0
  36. package/dist/contract.js +2 -0
  37. package/dist/declaration-validation.d.ts +32 -0
  38. package/dist/declaration-validation.js +207 -0
  39. package/dist/define.d.ts +51 -25
  40. package/dist/define.js +752 -38
  41. package/dist/error-resolution.d.ts +4 -0
  42. package/dist/error-resolution.js +122 -0
  43. package/dist/errors.d.ts +18 -0
  44. package/dist/errors.js +40 -0
  45. package/dist/fixture-sanitization.d.ts +28 -0
  46. package/dist/fixture-sanitization.js +217 -0
  47. package/dist/health-scenario.d.ts +1842 -0
  48. package/dist/health-scenario.js +624 -0
  49. package/dist/index.d.ts +19 -9
  50. package/dist/index.js +10 -6
  51. package/dist/lint.d.ts +6 -1
  52. package/dist/lint.js +362 -3
  53. package/dist/native-address.d.ts +43 -0
  54. package/dist/native-address.js +281 -0
  55. package/dist/native-egress-policy.d.ts +31 -0
  56. package/dist/native-egress-policy.js +288 -0
  57. package/dist/observability.d.ts +5 -2
  58. package/dist/observability.js +48 -1
  59. package/dist/provider.d.ts +8 -2
  60. package/dist/provider.js +3 -1
  61. package/dist/runtime/auth-flow.d.ts +5 -1
  62. package/dist/runtime/auth-flow.js +6 -0
  63. package/dist/runtime/browser.d.ts +1 -0
  64. package/dist/runtime/browser.js +492 -49
  65. package/dist/runtime/cache.d.ts +1 -0
  66. package/dist/runtime/cache.js +169 -15
  67. package/dist/runtime/choice-wordlist.d.ts +9 -0
  68. package/dist/runtime/choice-wordlist.js +138 -0
  69. package/dist/runtime/choice.d.ts +13 -1
  70. package/dist/runtime/choice.js +490 -102
  71. package/dist/runtime/executor.d.ts +2 -2
  72. package/dist/runtime/executor.js +26 -2
  73. package/dist/runtime/http.d.ts +1 -0
  74. package/dist/runtime/http.js +515 -53
  75. package/dist/runtime/instrumentation.d.ts +2 -2
  76. package/dist/runtime/instrumentation.js +366 -8
  77. package/dist/runtime/native-network-errors.d.ts +33 -0
  78. package/dist/runtime/native-network-errors.js +69 -0
  79. package/dist/runtime/native-network.d.ts +96 -0
  80. package/dist/runtime/native-network.js +1232 -0
  81. package/dist/runtime/ocr.d.ts +29 -0
  82. package/dist/runtime/ocr.js +440 -0
  83. package/dist/runtime/proxy-errors.js +6 -2
  84. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  85. package/dist/runtime/proxy-nodemaven.js +146 -0
  86. package/dist/runtime/proxy-telemetry.d.ts +80 -1
  87. package/dist/runtime/proxy-telemetry.js +154 -47
  88. package/dist/runtime/redirects.d.ts +29 -0
  89. package/dist/runtime/redirects.js +36 -0
  90. package/dist/runtime/redis.d.ts +1 -1
  91. package/dist/runtime/redis.js +4 -2
  92. package/dist/runtime/request-options.d.ts +68 -1
  93. package/dist/runtime/request-options.js +548 -0
  94. package/dist/runtime/resolver-config.d.ts +6 -0
  95. package/dist/runtime/resolver-config.js +6 -0
  96. package/dist/runtime/resolver-public.d.ts +1 -0
  97. package/dist/runtime/resolver-public.js +1 -0
  98. package/dist/runtime/resolver-shared.d.ts +3 -0
  99. package/dist/runtime/resolver-shared.js +12 -0
  100. package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
  101. package/dist/runtime/resolver-vendors/bindings.js +40 -0
  102. package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
  103. package/dist/runtime/resolver-vendors/browser.js +377 -0
  104. package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
  105. package/dist/runtime/resolver-vendors/capsolver.js +526 -0
  106. package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
  107. package/dist/runtime/resolver-vendors/hosts.js +33 -0
  108. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
  109. package/dist/runtime/resolver-vendors/twocaptcha.js +407 -0
  110. package/dist/runtime/resolver-vendors/types.d.ts +94 -0
  111. package/dist/runtime/resolver-vendors/types.js +96 -0
  112. package/dist/runtime/resolver.d.ts +60 -0
  113. package/dist/runtime/resolver.js +737 -0
  114. package/dist/runtime/secrets.d.ts +27 -0
  115. package/dist/runtime/secrets.js +51 -0
  116. package/dist/runtime/state.d.ts +3 -0
  117. package/dist/runtime/state.js +277 -71
  118. package/dist/runtime/stealth-cookies.d.ts +20 -0
  119. package/dist/runtime/stealth-cookies.js +111 -0
  120. package/dist/runtime/stealth.d.ts +28 -3
  121. package/dist/runtime/stealth.js +519 -255
  122. package/dist/runtime/stt.js +1 -12
  123. package/dist/runtime/timeout.d.ts +5 -0
  124. package/dist/runtime/timeout.js +12 -0
  125. package/dist/runtime/trace-config.d.ts +12 -0
  126. package/dist/runtime/trace-config.js +61 -0
  127. package/dist/serve.d.ts +1 -1
  128. package/dist/serve.js +1 -1
  129. package/dist/server/index.d.ts +5 -3
  130. package/dist/server/index.js +3 -3
  131. package/dist/server/self-test-input-tokens.d.ts +2 -1
  132. package/dist/server/self-test-input-tokens.js +18 -14
  133. package/dist/server/self-test.d.ts +114 -0
  134. package/dist/server/self-test.js +784 -148
  135. package/dist/server/serve-implementation.d.ts +213 -0
  136. package/dist/server/serve-implementation.js +2173 -0
  137. package/dist/server/serve.d.ts +1 -70
  138. package/dist/server/serve.js +1 -1130
  139. package/dist/server/trace-output.d.ts +4 -0
  140. package/dist/server/trace-output.js +20 -0
  141. package/dist/server/types.d.ts +30 -5
  142. package/dist/server/types.js +13 -1
  143. package/dist/stateful/errors.d.ts +19 -0
  144. package/dist/stateful/errors.js +24 -0
  145. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  146. package/dist/stateful/http-provider-event-emitter.js +237 -0
  147. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  148. package/dist/stateful/http-session-owner-registry.js +210 -0
  149. package/dist/stateful/index.d.ts +18 -0
  150. package/dist/stateful/index.js +18 -0
  151. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  152. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  153. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  154. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  155. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  156. package/dist/stateful/provider-event-pipeline.js +1 -0
  157. package/dist/stateful/provider-events.d.ts +101 -0
  158. package/dist/stateful/provider-events.js +289 -0
  159. package/dist/stateful/session-key.d.ts +15 -0
  160. package/dist/stateful/session-key.js +86 -0
  161. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  162. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  163. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  164. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  165. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  166. package/dist/stateful/stateful-provider-adapter.js +287 -0
  167. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  168. package/dist/stateful/stateful-provider-observability.js +161 -0
  169. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  170. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  171. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  172. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  173. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  174. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  175. package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
  176. package/dist/stateful/stateful-provider-session-routing.js +345 -0
  177. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  178. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  179. package/dist/stateful-signing.d.ts +18 -0
  180. package/dist/stateful-signing.js +27 -0
  181. package/dist/stealth/profiles.js +16 -7
  182. package/dist/stream-evidence.d.ts +74 -0
  183. package/dist/stream-evidence.js +785 -0
  184. package/dist/stream.js +7 -1
  185. package/dist/testing/index.d.ts +2 -1
  186. package/dist/testing/index.js +2 -1
  187. package/dist/testing/run.d.ts +32 -2
  188. package/dist/testing/run.js +489 -21
  189. package/dist/trace-sanitization.d.ts +5 -0
  190. package/dist/trace-sanitization.js +45 -0
  191. package/dist/types.d.ts +545 -23
  192. package/dist/types.js +1 -0
  193. package/package.json +44 -5
  194. package/src/auth-turn/index.ts +1 -1
  195. package/src/auth.ts +118 -0
  196. package/src/ceremonies/index.ts +189 -46
  197. package/src/cli/commands.ts +10 -0
  198. package/src/cli/create.ts +42 -35
  199. package/src/cli/prompt-assets.ts +865 -0
  200. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  201. package/src/cli/templates/provider/Dockerfile.tpl +1 -1
  202. package/src/cli/templates/provider/README.md.tpl +4 -4
  203. package/src/cli/templates/provider/index.ts.tpl +6 -3
  204. package/src/cli/templates/provider/operations/ping.ts.tpl +2 -1
  205. package/src/config/loader.ts +665 -163
  206. package/src/contract-serialization.ts +5 -7
  207. package/src/contract-types.ts +1 -0
  208. package/src/contract.ts +2 -0
  209. package/src/declaration-validation.ts +266 -0
  210. package/src/define.ts +970 -87
  211. package/src/error-resolution.ts +127 -0
  212. package/src/errors.ts +52 -0
  213. package/src/fixture-sanitization.ts +248 -0
  214. package/src/health-scenario.ts +875 -0
  215. package/src/index.ts +204 -8
  216. package/src/lint.ts +408 -4
  217. package/src/native-address.ts +340 -0
  218. package/src/native-egress-policy.ts +358 -0
  219. package/src/observability.ts +51 -1
  220. package/src/provider.ts +133 -0
  221. package/src/runtime/auth-flow.ts +12 -0
  222. package/src/runtime/browser.ts +661 -63
  223. package/src/runtime/cache.ts +189 -14
  224. package/src/runtime/choice-wordlist.ts +145 -0
  225. package/src/runtime/choice.ts +631 -120
  226. package/src/runtime/executor.ts +40 -7
  227. package/src/runtime/http.ts +641 -61
  228. package/src/runtime/instrumentation.ts +520 -15
  229. package/src/runtime/native-network-errors.ts +99 -0
  230. package/src/runtime/native-network.ts +1605 -0
  231. package/src/runtime/ocr.ts +523 -0
  232. package/src/runtime/proxy-errors.ts +12 -4
  233. package/src/runtime/proxy-nodemaven.ts +221 -0
  234. package/src/runtime/proxy-telemetry.ts +244 -75
  235. package/src/runtime/redirects.ts +66 -0
  236. package/src/runtime/redis.ts +7 -2
  237. package/src/runtime/request-options.ts +680 -1
  238. package/src/runtime/resolver-config.ts +6 -0
  239. package/src/runtime/resolver-public.ts +20 -0
  240. package/src/runtime/resolver-shared.ts +17 -0
  241. package/src/runtime/resolver-vendors/bindings.ts +56 -0
  242. package/src/runtime/resolver-vendors/browser.ts +533 -0
  243. package/src/runtime/resolver-vendors/capsolver.ts +700 -0
  244. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  245. package/src/runtime/resolver-vendors/twocaptcha.ts +539 -0
  246. package/src/runtime/resolver-vendors/types.ts +212 -0
  247. package/src/runtime/resolver.ts +1103 -0
  248. package/src/runtime/secrets.ts +64 -0
  249. package/src/runtime/state.ts +394 -77
  250. package/src/runtime/stealth-cookies.ts +132 -0
  251. package/src/runtime/stealth.ts +675 -289
  252. package/src/runtime/stt.ts +1 -19
  253. package/src/runtime/timeout.ts +18 -0
  254. package/src/runtime/trace-config.ts +77 -0
  255. package/src/serve.ts +6 -1
  256. package/src/server/index.ts +37 -2
  257. package/src/server/self-test-input-tokens.ts +29 -14
  258. package/src/server/self-test.ts +1025 -175
  259. package/src/server/serve-implementation.ts +3250 -0
  260. package/src/server/serve.ts +1 -1626
  261. package/src/server/trace-output.ts +32 -0
  262. package/src/server/types.ts +13 -1
  263. package/src/stateful/README.md +146 -0
  264. package/src/stateful/errors.ts +35 -0
  265. package/src/stateful/http-provider-event-emitter.ts +314 -0
  266. package/src/stateful/http-session-owner-registry.ts +306 -0
  267. package/src/stateful/index.ts +18 -0
  268. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  269. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  270. package/src/stateful/provider-event-pipeline.ts +61 -0
  271. package/src/stateful/provider-events.ts +462 -0
  272. package/src/stateful/session-key.ts +111 -0
  273. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  274. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  275. package/src/stateful/stateful-provider-adapter.ts +562 -0
  276. package/src/stateful/stateful-provider-observability.ts +261 -0
  277. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  278. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  279. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  280. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  281. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  282. package/src/stateful-signing.ts +46 -0
  283. package/src/stealth/profiles.ts +17 -7
  284. package/src/stream-evidence.ts +988 -0
  285. package/src/stream.ts +8 -1
  286. package/src/testing/index.ts +10 -1
  287. package/src/testing/run.ts +658 -15
  288. package/src/trace-sanitization.ts +63 -0
  289. package/src/types.ts +660 -38
  290. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  291. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  292. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  293. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  294. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  295. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  296. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  297. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  298. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  299. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  300. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  301. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  302. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  303. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -0,0 +1,3250 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { join } from "node:path";
5
+
6
+ import { Hono } from "hono";
7
+ import { z } from "zod";
8
+ import { AuthAbortError, createAuthFlowHelpers } from "../auth.js";
9
+ import { validateFailClosedDeclaration } from "../declaration-validation.js";
10
+ import {
11
+ SDK_OWNED_PROVIDER_ERROR_CODES,
12
+ SDK_RUNTIME_OWNED_ERROR_CODES,
13
+ SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES,
14
+ } from "../error-resolution.js";
15
+ import {
16
+ AuthError,
17
+ isProviderError,
18
+ isSessionExpiredError,
19
+ isTransportError,
20
+ isValidationError,
21
+ ProviderError,
22
+ } from "../errors.js";
23
+ import {
24
+ loadProviderLocaleCatalogs,
25
+ localizeAuthTurn,
26
+ type ProviderLocaleCatalogMap,
27
+ } from "../i18n/catalog.js";
28
+ import type { ProviderLocale } from "../i18n/keys.js";
29
+ import {
30
+ categoryForStatus,
31
+ type ProviderErrorSource,
32
+ sourceForCategory,
33
+ isRetryableCategory,
34
+ PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
35
+ type ProviderErrorCategory,
36
+ } from "../observability.js";
37
+ import { createScratchpad } from "../runtime/auth-flow.js";
38
+ import type * as BrowserRuntimeModule from "../runtime/browser.js";
39
+ import { createProviderCache } from "../runtime/cache.js";
40
+ import {
41
+ createProviderChoiceContext,
42
+ PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
43
+ } from "../runtime/choice.js";
44
+ import { createCredentialContext } from "../runtime/credential.js";
45
+ import { createEnvContext } from "../runtime/env.js";
46
+ import { executeOperation } from "../runtime/executor.js";
47
+ import { createHttpClient } from "../runtime/http.js";
48
+ import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
49
+ import type * as NativeNetworkRuntimeModule from "../runtime/native-network.js";
50
+ import { getProviderBaseUrl } from "../runtime/provider.js";
51
+ import { createOcrClientFromEnv } from "../runtime/ocr.js";
52
+ import {
53
+ PROXY_AUTH_IP_DENIED_CODE,
54
+ PROXY_EDGE_AUTH_REJECTED_CODE,
55
+ PROXY_POOL_EXHAUSTED_CODE,
56
+ } from "../runtime/proxy-errors.js";
57
+ import {
58
+ PROVIDER_TELEMETRY_HEADER,
59
+ ProxyTelemetryCollector,
60
+ type ProxyTelemetryLogPayload,
61
+ } from "../runtime/proxy-telemetry.js";
62
+ import type * as ResolverRuntimeModule from "../runtime/resolver.js";
63
+ import { createUnsupportedResolverClient } from "../runtime/resolver-shared.js";
64
+ import {
65
+ assertRequiredSecretsPresent,
66
+ listMissingRequiredSecrets,
67
+ MISSING_SECRET_CODE,
68
+ } from "../runtime/secrets.js";
69
+ import {
70
+ createProviderRuntimeStateFromEnv,
71
+ createUnsupportedProviderRuntimeState,
72
+ } from "../runtime/state.js";
73
+ import { StealthCookieJar } from "../runtime/stealth-cookies.js";
74
+ import type * as StealthRuntimeModule from "../runtime/stealth.js";
75
+ import { createSttClientFromEnv } from "../runtime/stt.js";
76
+ import { createTraceContext } from "../runtime/trace.js";
77
+ import { resolveTraceConfigFromEnv } from "../runtime/trace-config.js";
78
+ import { parseSchema } from "../schema.js";
79
+ import {
80
+ STATEFUL_NONCE_HEADER as STATEFUL_FORWARDING_NONCE_HEADER,
81
+ STATEFUL_SIGNATURE_HEADER as STATEFUL_FORWARDING_SIGNATURE_HEADER,
82
+ STATEFUL_TIMESTAMP_HEADER as STATEFUL_FORWARDING_TIMESTAMP_HEADER,
83
+ verifyStatefulRequestSignature,
84
+ } from "../stateful-signing.js";
85
+ import { StatefulRoutingDeadlineError } from "../stateful/errors.js";
86
+ import { getStealthProfile } from "../stealth/profiles.js";
87
+ import {
88
+ APIFUSE_STREAM_DONE_EVENT,
89
+ APIFUSE_STREAM_ERROR_EVENT,
90
+ encodeSseEvent,
91
+ error as streamError,
92
+ } from "../stream.js";
93
+ import type {
94
+ AuthContext,
95
+ AuthTurn,
96
+ BrowserClient,
97
+ FlowContext,
98
+ FlowContextStore,
99
+ HttpRetrySummary,
100
+ OperationDefinition,
101
+ OperationErrorCode,
102
+ OperationHttpStreamTransport,
103
+ OperationSseTransport,
104
+ OcrContext,
105
+ ProviderErrorStatus,
106
+ ProviderContext,
107
+ ProviderDefinition,
108
+ ProviderProxyPolicy,
109
+ ProviderRuntimeState,
110
+ ProviderStreamEvent,
111
+ ResolverContext,
112
+ StealthClient,
113
+ StealthSession,
114
+ StealthSessionCookies,
115
+ SttContext,
116
+ } from "../types.js";
117
+ import { VALID_OPERATION_ERROR_STATUSES } from "../types.js";
118
+ import type { SelfTestCancellationLogEvent } from "./self-test.js";
119
+ import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
120
+ import { resolveServerTraceContextOptions } from "./trace-output.js";
121
+ import {
122
+ type AuthFlowRequest,
123
+ AuthFlowRequestSchema,
124
+ type AuthFlowResponse,
125
+ type AuthFlowSuccessResponse,
126
+ OperationConnectionSchema,
127
+ type OperationErrorResponse,
128
+ type OperationRequest,
129
+ OperationRequestSchema,
130
+ type OperationResponse,
131
+ type OperationSuccessResponse,
132
+ } from "./types.js";
133
+
134
+ const DEFAULT_HOST = "0.0.0.0";
135
+ const DEFAULT_PORT = 3000;
136
+ /** Compact SDK-owned error classification emitted separately from the public response body. */
137
+ export const ERROR_OBSERVABILITY_HEADER = "X-ApiFuse-Error-Observability";
138
+ export type ErrorObservabilityDetails = {
139
+ category: ProviderErrorCategory;
140
+ taxonomyVersion: string;
141
+ retryable: boolean;
142
+ upstreamStatus?: number;
143
+ };
144
+ const AUTH_FLOW_LOCALES = ["en", "ko", "ja"] as const;
145
+ const retryResponseMeta = new WeakMap<ProviderContext, HttpRetrySummary>();
146
+ const STATEFUL_INTERNAL_OPERATIONS_ROUTE = "/__apifuse/stateful/operations";
147
+ const STATEFUL_FORWARDING_SOURCE_POD_HEADER = "x-apifuse-stateful-source-pod";
148
+ const DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS = 5 * 60_000;
149
+ const DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES = 10_000;
150
+ const STATEFUL_FORWARDING_REPLAY_BUCKET_MS = 10_000;
151
+ const STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS = Math.ceil(
152
+ STATEFUL_FORWARDING_REPLAY_BUCKET_MS / 1_000,
153
+ );
154
+
155
+ export const ProviderServerStatefulForwardEnvelopeSchema = z
156
+ .object({
157
+ requestId: z.string().min(1),
158
+ providerId: z.string().min(1),
159
+ operationId: z.string().min(1),
160
+ sessionKey: z.string().min(1),
161
+ connectionId: z.string().min(1),
162
+ serviceAccountId: z.string().min(1),
163
+ ownerPodId: z.string().min(1),
164
+ generation: z.number().int().positive(),
165
+ sourcePodId: z.string().min(1),
166
+ forwardedAt: z.string().refine((value) => Number.isFinite(Date.parse(value))),
167
+ deadlineAt: z
168
+ .string()
169
+ .refine((value) => Number.isFinite(Date.parse(value)))
170
+ .optional(),
171
+ idempotencyKey: z.string().min(1).optional(),
172
+ operationRequest: OperationRequestSchema.extend({
173
+ connection: OperationConnectionSchema.strict().optional(),
174
+ }).strict(),
175
+ })
176
+ .strict();
177
+
178
+ export type ProviderServerStatefulForwardEnvelope = Readonly<
179
+ z.infer<typeof ProviderServerStatefulForwardEnvelopeSchema>
180
+ >;
181
+
182
+ export type ProviderServerStatefulOwnerFence = Readonly<
183
+ Pick<
184
+ ProviderServerStatefulForwardEnvelope,
185
+ | "providerId"
186
+ | "sessionKey"
187
+ | "ownerPodId"
188
+ | "generation"
189
+ | "sourcePodId"
190
+ | "forwardedAt"
191
+ | "requestId"
192
+ | "idempotencyKey"
193
+ >
194
+ >;
195
+
196
+ export type ProviderServerStatefulOwnerFenceValidator = (
197
+ fence: ProviderServerStatefulOwnerFence,
198
+ signal: AbortSignal,
199
+ ) => boolean | Promise<boolean>;
200
+
201
+ export type ProviderServerOperationExecutorInput = {
202
+ readonly provider: ProviderDefinition;
203
+ readonly operationId: string;
204
+ readonly ctx: ProviderContext;
205
+ readonly request: OperationRequest & { readonly deadlineAt?: string };
206
+ readonly signal?: AbortSignal;
207
+ readonly internalStatefulForward?: ProviderServerStatefulForwardEnvelope;
208
+ };
209
+
210
+ export type ProviderServerOperationExecutor = (
211
+ input: ProviderServerOperationExecutorInput,
212
+ ) => Promise<unknown>;
213
+
214
+ type RequestCleanup = () => void | Promise<void>;
215
+
216
+ type ProviderCapabilityModules = {
217
+ readonly browser?: typeof BrowserRuntimeModule;
218
+ readonly nativeNetwork?: typeof NativeNetworkRuntimeModule;
219
+ readonly resolver?: typeof ResolverRuntimeModule;
220
+ readonly stealth?: typeof StealthRuntimeModule;
221
+ };
222
+
223
+ type ProviderServerRuntimeOptions = ProviderServerOptions & {
224
+ readonly capabilityModules: ProviderCapabilityModules;
225
+ };
226
+
227
+ const require = createRequire(import.meta.url);
228
+
229
+ type CapabilityLoadFailure = {
230
+ readonly capability: string;
231
+ readonly error: unknown;
232
+ };
233
+
234
+ function normalizedCapabilityCause(error: unknown): Error {
235
+ if (error instanceof Error) return error;
236
+ return new Error(String(error));
237
+ }
238
+
239
+ function capabilityLoadError(
240
+ provider: ProviderDefinition,
241
+ failures: readonly CapabilityLoadFailure[],
242
+ ): ProviderError {
243
+ const normalizedFailures = failures.map(({ capability, error }) => ({
244
+ capability,
245
+ cause: normalizedCapabilityCause(error),
246
+ }));
247
+ const summary = normalizedFailures
248
+ .map(({ capability, cause }) => `${capability} (${cause.message})`)
249
+ .join(", ");
250
+ return new ProviderError(
251
+ `Failed to load declared capabilities for provider "${provider.id}": ${summary}`,
252
+ {
253
+ code: "PROVIDER_CAPABILITY_LOAD_FAILED",
254
+ details: {
255
+ providerId: provider.id,
256
+ failures: normalizedFailures.map(({ capability, cause }) => ({
257
+ capability,
258
+ reason: cause.message,
259
+ })),
260
+ },
261
+ cause: new AggregateError(
262
+ normalizedFailures.map(({ cause }) => cause),
263
+ `Provider capability loading failed for ${provider.id}`,
264
+ ),
265
+ },
266
+ );
267
+ }
268
+
269
+ function requireCapabilityModule<T>(
270
+ provider: ProviderDefinition,
271
+ capability: string,
272
+ specifier: string,
273
+ ): T {
274
+ try {
275
+ return require(specifier) as T;
276
+ } catch (error) {
277
+ if (error && typeof error === "object" && "code" in error && error.code === "ERR_REQUIRE_ESM") {
278
+ throw new ProviderError(
279
+ `Synchronous capability loading is unavailable for provider "${provider.id}" on this runtime. Use createServerAppAsync() instead of createServerApp().`,
280
+ {
281
+ code: "PROVIDER_CAPABILITY_SYNC_LOAD_UNSUPPORTED",
282
+ details: { providerId: provider.id, capability },
283
+ cause: normalizedCapabilityCause(error),
284
+ },
285
+ );
286
+ }
287
+ throw capabilityLoadError(provider, [{ capability, error }]);
288
+ }
289
+ }
290
+
291
+ function createAuthStub(): AuthContext {
292
+ return {
293
+ async requestField(name) {
294
+ throw new ProviderError(`Auth prompt is unavailable for ${name}`, {
295
+ code: "AUTH_PROMPT_UNAVAILABLE",
296
+ });
297
+ },
298
+ };
299
+ }
300
+
301
+ function createBrowserStub(): BrowserClient {
302
+ return {
303
+ engine: "playwright-stealth",
304
+ async close() {},
305
+ async newPage() {
306
+ throw new ProviderError("Browser runtime is not available", {
307
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
308
+ });
309
+ },
310
+ async rawPage() {
311
+ throw new ProviderError("Browser runtime is not available", {
312
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
313
+ });
314
+ },
315
+ async withIsolatedContext() {
316
+ throw new ProviderError("Browser runtime is not available", {
317
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
318
+ });
319
+ },
320
+ async solveChallenge() {
321
+ throw new ProviderError("Browser runtime is not available", {
322
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
323
+ });
324
+ },
325
+ };
326
+ }
327
+
328
+ function createStealthStub(): StealthClient {
329
+ return {
330
+ async fetch() {
331
+ throw new ProviderError("Stealth runtime is not available", {
332
+ code: "STEALTH_RUNTIME_UNSUPPORTED",
333
+ });
334
+ },
335
+ createSession() {
336
+ throw new ProviderError("Stealth runtime is not available", {
337
+ code: "STEALTH_RUNTIME_UNSUPPORTED",
338
+ });
339
+ },
340
+ close() {
341
+ // no-op
342
+ },
343
+ };
344
+ }
345
+
346
+ let stealthRuntimeModulePromise: Promise<typeof StealthRuntimeModule> | undefined;
347
+
348
+ function importStealthRuntime(): Promise<typeof StealthRuntimeModule> {
349
+ stealthRuntimeModulePromise ??= import("../runtime/stealth.js");
350
+ return stealthRuntimeModulePromise;
351
+ }
352
+
353
+ function createForwardingStealthCookies(
354
+ initialTarget: StealthSessionCookies,
355
+ ): StealthSessionCookies & { setTarget(target: StealthSessionCookies): void } {
356
+ let target = initialTarget;
357
+ return {
358
+ setTarget(nextTarget) {
359
+ target = nextTarget;
360
+ },
361
+ get: (...args) => target.get(...args),
362
+ getAll: (...args) => target.getAll(...args),
363
+ has: (...args) => target.has(...args),
364
+ setFromCookieStrings: (...args) => target.setFromCookieStrings(...args),
365
+ toString: (url?: string) => target.toString(url),
366
+ toHeader: (...args) => target.toHeader(...args),
367
+ snapshot: () => target.snapshot(),
368
+ restore: (...args) => target.restore(...args),
369
+ serialize: () => target.serialize(),
370
+ deserialize: (...args) => target.deserialize(...args),
371
+ clear: () => target.clear(),
372
+ find: (...args) => target.find?.(...args),
373
+ };
374
+ }
375
+
376
+ function createLazyStealthClient(
377
+ onCleanupError: (error: unknown) => void,
378
+ ...createArgs: Parameters<typeof StealthRuntimeModule.createStealthClient>
379
+ ): StealthClient {
380
+ let client: StealthClient | undefined;
381
+ let clientPromise: Promise<StealthClient> | undefined;
382
+
383
+ function getClient(): Promise<StealthClient> {
384
+ clientPromise ??= importStealthRuntime().then((runtime) => {
385
+ client ??= runtime.createStealthClient(...createArgs);
386
+ return client;
387
+ });
388
+ return clientPromise;
389
+ }
390
+
391
+ return {
392
+ async fetch(...args) {
393
+ return (await getClient()).fetch(...args);
394
+ },
395
+ createSession(options) {
396
+ const localCookies = new StealthCookieJar([], createArgs[0]);
397
+ const cookies = createForwardingStealthCookies(localCookies);
398
+ let closed = false;
399
+ let session: StealthSession | undefined;
400
+ let sessionPromise: Promise<StealthSession> | undefined;
401
+
402
+ function getSession(): Promise<StealthSession> {
403
+ sessionPromise ??= getClient().then((realClient) => {
404
+ session = realClient.createSession(options);
405
+ session.cookies.deserialize(localCookies.serialize());
406
+ cookies.setTarget(session.cookies);
407
+ if (closed) session.close();
408
+ return session;
409
+ });
410
+ return sessionPromise;
411
+ }
412
+
413
+ return {
414
+ cookies,
415
+ async fetch(...args) {
416
+ return (await getSession()).fetch(...args);
417
+ },
418
+ redirects: {
419
+ async run(...args) {
420
+ return (await getSession()).redirects.run(...args);
421
+ },
422
+ },
423
+ close() {
424
+ closed = true;
425
+ session?.close();
426
+ },
427
+ };
428
+ },
429
+ close() {
430
+ client?.close?.();
431
+ if (!client && clientPromise) {
432
+ void clientPromise.then(
433
+ (loadedClient) => {
434
+ try {
435
+ loadedClient.close?.();
436
+ } catch (error) {
437
+ try {
438
+ onCleanupError(error);
439
+ } catch {
440
+ // A user logger must not turn handled cleanup into a process-level rejection.
441
+ }
442
+ }
443
+ },
444
+ () => {
445
+ // The request path owns reporting for a failed lazy import.
446
+ },
447
+ );
448
+ }
449
+ },
450
+ };
451
+ }
452
+
453
+ function bindResolverSignalWithoutRuntime(
454
+ resolver: ResolverContext,
455
+ defaultSignal: AbortSignal | undefined,
456
+ ): ResolverContext {
457
+ if (!defaultSignal) return resolver;
458
+ return {
459
+ solve(challenge, signal = defaultSignal) {
460
+ return resolver.solve(challenge, signal);
461
+ },
462
+ };
463
+ }
464
+
465
+ function getProviderStealthBaseUrl(provider: ProviderDefinition): string | undefined {
466
+ const baseUrl = getProviderBaseUrl(provider);
467
+ if (baseUrl) {
468
+ return baseUrl;
469
+ }
470
+ const firstHost = provider.allowedHosts?.[0];
471
+ return firstHost ? `https://${firstHost}` : undefined;
472
+ }
473
+
474
+ function getProviderStealthProfile(provider: ProviderDefinition) {
475
+ return provider.stealth?.profile ? getStealthProfile(provider.stealth.profile) : undefined;
476
+ }
477
+
478
+ function isProductionProviderBrowserMode(provider: ProviderDefinition, env = process.env): boolean {
479
+ if (provider.runtime !== "browser") {
480
+ return false;
481
+ }
482
+
483
+ if (env.APIFUSE__PROVIDER__RUNTIME === "browser") {
484
+ return true;
485
+ }
486
+
487
+ return env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id;
488
+ }
489
+
490
+ function declaresStealthRuntime(provider: ProviderDefinition): boolean {
491
+ return provider.stealth !== undefined;
492
+ }
493
+
494
+ async function loadProviderCapabilityModules(
495
+ provider: ProviderDefinition,
496
+ ): Promise<ProviderCapabilityModules> {
497
+ const results = await Promise.allSettled([
498
+ provider.runtime === "browser" ? import("../runtime/browser.js") : Promise.resolve(undefined),
499
+ provider.native ? import("../runtime/native-network.js") : Promise.resolve(undefined),
500
+ provider.resolver ? import("../runtime/resolver.js") : Promise.resolve(undefined),
501
+ declaresStealthRuntime(provider) ? import("../runtime/stealth.js") : Promise.resolve(undefined),
502
+ ]);
503
+ const capabilityNames = ["browser", "native", "resolver", "stealth"] as const;
504
+ const failures: CapabilityLoadFailure[] = [];
505
+ for (const [index, result] of results.entries()) {
506
+ if (result.status === "rejected") {
507
+ failures.push({ capability: capabilityNames[index]!, error: result.reason });
508
+ }
509
+ }
510
+ if (failures.length > 0) throw capabilityLoadError(provider, failures);
511
+ return {
512
+ browser: results[0].status === "fulfilled" ? results[0].value : undefined,
513
+ nativeNetwork: results[1].status === "fulfilled" ? results[1].value : undefined,
514
+ resolver: results[2].status === "fulfilled" ? results[2].value : undefined,
515
+ stealth: results[3].status === "fulfilled" ? results[3].value : undefined,
516
+ };
517
+ }
518
+
519
+ function loadProviderCapabilityModulesSync(
520
+ provider: ProviderDefinition,
521
+ ): ProviderCapabilityModules {
522
+ return {
523
+ browser:
524
+ provider.runtime === "browser"
525
+ ? requireCapabilityModule<typeof BrowserRuntimeModule>(
526
+ provider,
527
+ "browser",
528
+ "../runtime/browser.js",
529
+ )
530
+ : undefined,
531
+ nativeNetwork: provider.native
532
+ ? requireCapabilityModule<typeof NativeNetworkRuntimeModule>(
533
+ provider,
534
+ "native",
535
+ "../runtime/native-network.js",
536
+ )
537
+ : undefined,
538
+ resolver: provider.resolver
539
+ ? requireCapabilityModule<typeof ResolverRuntimeModule>(
540
+ provider,
541
+ "resolver",
542
+ "../runtime/resolver.js",
543
+ )
544
+ : undefined,
545
+ stealth: declaresStealthRuntime(provider)
546
+ ? requireCapabilityModule<typeof StealthRuntimeModule>(
547
+ provider,
548
+ "stealth",
549
+ "../runtime/stealth.js",
550
+ )
551
+ : undefined,
552
+ };
553
+ }
554
+
555
+ export function resolveProviderProxyAffinityKey(
556
+ provider: ProviderDefinition,
557
+ request: OperationRequest,
558
+ operationId: string,
559
+ ): string {
560
+ const connectionKey = resolveOperationConnectionId(request) ?? request.connection?.externalRef;
561
+ const affinity =
562
+ typeof provider.proxy === "object" ? provider.proxy.session?.affinity : undefined;
563
+ if (affinity === "operation") {
564
+ return `${provider.id}/${operationId}`;
565
+ }
566
+ return connectionKey ?? provider.id;
567
+ }
568
+
569
+ export function resolveProviderResolverIdentityScope(
570
+ provider: ProviderDefinition,
571
+ affinityKey: string,
572
+ contextId: string,
573
+ ): string {
574
+ return JSON.stringify({
575
+ proxy: provider.proxy ?? null,
576
+ affinityKey,
577
+ contextId,
578
+ });
579
+ }
580
+
581
+ function resolveOperationConnectionId(
582
+ request: Pick<OperationRequest, "connection" | "connectionId">,
583
+ ): string | undefined {
584
+ // An empty string is a malformed identifier, not an identity: treat it as
585
+ // absent so it can never override a valid id or key a real scope. Requests
586
+ // without any usable id fall back to the documented missing-connection
587
+ // sentinel scope instead of scoping context/affinity/state under "".
588
+ return (
589
+ normalizeConnectionId(request.connection?.id) ?? normalizeConnectionId(request.connectionId)
590
+ );
591
+ }
592
+
593
+ function normalizeConnectionId(id: string | undefined): string | undefined {
594
+ return id === "" ? undefined : id;
595
+ }
596
+
597
+ function resolveNativeProxyPolicy(provider: ProviderDefinition): ProviderProxyPolicy | undefined {
598
+ if (typeof provider.proxy === "object") return provider.proxy;
599
+ if (provider.proxy === true) return { mode: "optional" };
600
+ if (provider.proxy === false) return { mode: "disabled" };
601
+ return undefined;
602
+ }
603
+
604
+ function createProviderContext(
605
+ provider: ProviderDefinition,
606
+ request: OperationRequest,
607
+ operationId: string,
608
+ options: ProviderServerRuntimeOptions,
609
+ state: ProviderRuntimeState = createUnsupportedProviderRuntimeState(),
610
+ proxyTelemetry?: ProxyTelemetryCollector,
611
+ signal?: AbortSignal,
612
+ ): ProviderContext {
613
+ const traceConfig = resolveTraceConfigFromEnv();
614
+ const baseUrl = getProviderBaseUrl(provider);
615
+ const stealthBaseUrl = getProviderStealthBaseUrl(provider);
616
+ const stealthProfile = getProviderStealthProfile(provider);
617
+ const proxyPolicy = resolveNativeProxyPolicy(provider);
618
+ const proxyClientOptions = {
619
+ upstream: { proxy: provider.proxy },
620
+ affinityKey: resolveProviderProxyAffinityKey(provider, request, operationId),
621
+ telemetry: proxyTelemetry,
622
+ };
623
+ const resolverIdentityScope = resolveProviderResolverIdentityScope(
624
+ provider,
625
+ proxyClientOptions.affinityKey,
626
+ request.requestId,
627
+ );
628
+ let wrappedContext: ProviderContext | undefined;
629
+ const stealthClientOptions = {
630
+ upstream: proxyClientOptions.upstream,
631
+ affinityKey: proxyClientOptions.affinityKey,
632
+ telemetry: proxyTelemetry,
633
+ };
634
+ const { capabilityModules } = options;
635
+ const logStealthCleanupError = (error: unknown) =>
636
+ logProviderCleanupError(
637
+ options.logger,
638
+ provider,
639
+ "operation",
640
+ operationId,
641
+ request.requestId,
642
+ "stealth",
643
+ error,
644
+ );
645
+
646
+ const env = createEnvContext([
647
+ ...(provider.secrets?.map((secret) => secret.name) ?? []),
648
+ PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
649
+ ]);
650
+ const credential = createCredentialContext({
651
+ allowedKeys: provider.credential?.keys,
652
+ mode: request.connection?.mode,
653
+ scopes: request.connection?.scopes,
654
+ values: request.connection?.secrets,
655
+ });
656
+ const requestContext = {
657
+ connectionId: resolveOperationConnectionId(request),
658
+ headers: request.headers ?? {},
659
+ };
660
+ const requestState = state.forConnection(requestContext.connectionId);
661
+ const cache = createProviderCache({ providerId: provider.id });
662
+ const context = wrapWithInstrumentation({
663
+ env,
664
+ credential,
665
+ request: requestContext,
666
+ http: createHttpClient(baseUrl, {
667
+ ...proxyClientOptions,
668
+ ...(signal ? { signal } : {}),
669
+ onRetrySummary: (summary) => {
670
+ if (summary.attempts <= 1 || !wrappedContext) return;
671
+ retryResponseMeta.set(wrappedContext, summary);
672
+ },
673
+ }),
674
+ cache,
675
+ state: requestState,
676
+ stealth: stealthBaseUrl
677
+ ? capabilityModules.stealth
678
+ ? stealthProfile
679
+ ? capabilityModules.stealth.createStealthClient(
680
+ stealthBaseUrl,
681
+ stealthProfile.name,
682
+ stealthClientOptions,
683
+ )
684
+ : capabilityModules.stealth.createStealthClient(stealthBaseUrl, stealthClientOptions)
685
+ : stealthProfile
686
+ ? createLazyStealthClient(
687
+ logStealthCleanupError,
688
+ stealthBaseUrl,
689
+ stealthProfile.name,
690
+ stealthClientOptions,
691
+ )
692
+ : createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, stealthClientOptions)
693
+ : createStealthStub(),
694
+ browser:
695
+ provider.runtime === "browser"
696
+ ? capabilityModules.browser!.createBrowserClient({
697
+ allowedHosts: provider.allowedHosts,
698
+ cdpUrl: process.env.APIFUSE__CDP_POOL__URL,
699
+ headless: true,
700
+ requireCdpPool: isProductionProviderBrowserMode(provider),
701
+ stealth: true,
702
+ engine: provider.browser?.engine,
703
+ })
704
+ : createBrowserStub(),
705
+ ...(provider.native
706
+ ? {
707
+ native: {
708
+ network: capabilityModules.nativeNetwork!.createNativeNetworkClient({
709
+ egress: provider.native.network,
710
+ proxyPolicy: resolveNativeProxyPolicy(provider),
711
+ affinityKey: proxyClientOptions.affinityKey,
712
+ credentials: capabilityModules.nativeNetwork!.createEnvVendorCredentialResolver(env),
713
+ }),
714
+ },
715
+ }
716
+ : {}),
717
+ trace: traceConfig
718
+ ? createTraceContext(
719
+ resolveServerTraceContextOptions(traceConfig, {
720
+ request_id: request.requestId,
721
+ provider_id: provider.id,
722
+ operation_id: operationId,
723
+ }),
724
+ )
725
+ : createTraceContext(),
726
+ auth: createAuthStub(),
727
+ ocr: options.ocr ?? createOcrClientFromEnv(provider.ocr),
728
+ stt: options.stt ?? createSttClientFromEnv(provider.stt),
729
+ resolver: capabilityModules.resolver
730
+ ? capabilityModules.resolver.bindResolverSignal(
731
+ options.resolver ??
732
+ capabilityModules.resolver.createResolverClientFromEnv(provider.resolver, undefined, {
733
+ allowedHosts: provider.allowedHosts,
734
+ cache,
735
+ identityScope: resolverIdentityScope,
736
+ ...(proxyPolicy
737
+ ? {
738
+ proxyIntent: {
739
+ mode: proxyPolicy.mode,
740
+ ...proxyClientOptions,
741
+ ...(stealthProfile ? { userAgent: stealthProfile.userAgent } : {}),
742
+ },
743
+ }
744
+ : {}),
745
+ }),
746
+ signal,
747
+ )
748
+ : bindResolverSignalWithoutRuntime(
749
+ options.resolver ??
750
+ createUnsupportedResolverClient("Provider does not declare resolver capability"),
751
+ signal,
752
+ ),
753
+ choice: createProviderChoiceContext({
754
+ providerId: provider.id,
755
+ env,
756
+ request: requestContext,
757
+ credential,
758
+ state: requestState,
759
+ onTelemetry: (event) =>
760
+ (options.logger ?? defaultProviderServerLogger)({
761
+ level: "info",
762
+ event: "provider_choice_token",
763
+ ...event,
764
+ }),
765
+ }),
766
+ } as ProviderContext);
767
+ wrappedContext = context;
768
+ return context;
769
+ }
770
+
771
+ function createFlowContextStore(
772
+ allowedKeys: string[],
773
+ initialContext: Record<string, unknown> = {},
774
+ ): {
775
+ context: FlowContextStore;
776
+ getPatch: () => Record<string, unknown | null> | undefined;
777
+ } {
778
+ const context = createScratchpad(allowedKeys, initialContext);
779
+
780
+ return {
781
+ context,
782
+ getPatch() {
783
+ const next = context.toJSON();
784
+ const patch = new Map<string, unknown | null>();
785
+
786
+ for (const [key, value] of Object.entries(next)) {
787
+ if (initialContext[key] !== value) {
788
+ patch.set(key, value);
789
+ }
790
+ }
791
+
792
+ for (const key of Object.keys(initialContext)) {
793
+ if (!(key in next)) {
794
+ patch.set(key, null);
795
+ }
796
+ }
797
+
798
+ if (patch.size === 0) {
799
+ return undefined;
800
+ }
801
+
802
+ return Object.fromEntries(patch.entries());
803
+ },
804
+ };
805
+ }
806
+
807
+ export function resolveAuthFlowProxyAffinityKey(
808
+ provider: ProviderDefinition,
809
+ request: Pick<
810
+ AuthFlowRequest,
811
+ "connection" | "connectionId" | "externalRef" | "tenantId" | "providerId"
812
+ >,
813
+ ): string {
814
+ return (
815
+ resolveOperationConnectionId(request) ??
816
+ request.externalRef ??
817
+ request.tenantId ??
818
+ request.providerId ??
819
+ provider.id
820
+ );
821
+ }
822
+
823
+ function createAuthFlowContext(
824
+ provider: ProviderDefinition,
825
+ request: AuthFlowRequest,
826
+ options: ProviderServerRuntimeOptions,
827
+ state: ProviderRuntimeState,
828
+ proxyTelemetry?: ProxyTelemetryCollector,
829
+ signal?: AbortSignal,
830
+ ): {
831
+ context: FlowContext;
832
+ getPatch: () => Record<string, unknown | null> | undefined;
833
+ } {
834
+ const baseUrl = getProviderBaseUrl(provider);
835
+ const stealthBaseUrl = getProviderStealthBaseUrl(provider);
836
+ const stealthProfile = getProviderStealthProfile(provider);
837
+ const proxyPolicy = resolveNativeProxyPolicy(provider);
838
+ const contextData = request.context ?? {};
839
+ const flowContextStore = createFlowContextStore(
840
+ provider.context?.keys ?? Object.keys(contextData),
841
+ contextData,
842
+ );
843
+ const proxyClientOptions = {
844
+ upstream: { proxy: provider.proxy },
845
+ affinityKey: resolveAuthFlowProxyAffinityKey(provider, request),
846
+ telemetry: proxyTelemetry,
847
+ };
848
+ const resolverIdentityScope = resolveProviderResolverIdentityScope(
849
+ provider,
850
+ proxyClientOptions.affinityKey,
851
+ request.requestId,
852
+ );
853
+ const stealthClientOptions = {
854
+ upstream: proxyClientOptions.upstream,
855
+ affinityKey: proxyClientOptions.affinityKey,
856
+ telemetry: proxyTelemetry,
857
+ };
858
+ const { capabilityModules } = options;
859
+ const logStealthCleanupError = (error: unknown) =>
860
+ logProviderCleanupError(
861
+ options.logger,
862
+ provider,
863
+ "auth",
864
+ "flow",
865
+ request.requestId,
866
+ "stealth",
867
+ error,
868
+ );
869
+ const credential = request.connection
870
+ ? createCredentialContext({
871
+ allowedKeys: provider.credential?.keys,
872
+ mode: request.connection.mode,
873
+ scopes: request.connection.scopes,
874
+ values: request.connection.secrets,
875
+ })
876
+ : undefined;
877
+ const cache = createProviderCache({ providerId: provider.id });
878
+
879
+ return {
880
+ context: {
881
+ flowId: request.flowId,
882
+ connectionId: resolveOperationConnectionId(request),
883
+ externalRef: request.externalRef,
884
+ tenantId: request.tenantId ?? "",
885
+ providerId: request.providerId ?? provider.id,
886
+ http: createHttpClient(baseUrl, {
887
+ ...proxyClientOptions,
888
+ ...(signal ? { signal } : {}),
889
+ }),
890
+ state: state.forConnection(resolveOperationConnectionId(request)),
891
+ stealth: stealthBaseUrl
892
+ ? capabilityModules.stealth
893
+ ? stealthProfile
894
+ ? capabilityModules.stealth.createStealthClient(
895
+ stealthBaseUrl,
896
+ stealthProfile.name,
897
+ stealthClientOptions,
898
+ )
899
+ : capabilityModules.stealth.createStealthClient(stealthBaseUrl, stealthClientOptions)
900
+ : stealthProfile
901
+ ? createLazyStealthClient(
902
+ logStealthCleanupError,
903
+ stealthBaseUrl,
904
+ stealthProfile.name,
905
+ stealthClientOptions,
906
+ )
907
+ : createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, stealthClientOptions)
908
+ : createStealthStub(),
909
+ ...(provider.native
910
+ ? {
911
+ native: {
912
+ network: capabilityModules.nativeNetwork!.createNativeNetworkClient({
913
+ egress: provider.native.network,
914
+ proxyPolicy: resolveNativeProxyPolicy(provider),
915
+ affinityKey: proxyClientOptions.affinityKey,
916
+ credentials: capabilityModules.nativeNetwork!.createEnvVendorCredentialResolver(
917
+ createEnvContext(provider.secrets?.map((secret) => secret.name)),
918
+ ),
919
+ }),
920
+ },
921
+ }
922
+ : {}),
923
+ env: createEnvContext([
924
+ ...(provider.secrets?.map((secret) => secret.name) ?? []),
925
+ ...(provider.auth?.mode === "oauth2_proxied" ? ["APIFUSE__AUTH_PROXY__URL"] : []),
926
+ ]),
927
+ credential,
928
+ context: flowContextStore.context,
929
+ ocr: options.ocr ?? createOcrClientFromEnv(provider.ocr),
930
+ stt: options.stt ?? createSttClientFromEnv(provider.stt),
931
+ resolver: capabilityModules.resolver
932
+ ? capabilityModules.resolver.bindResolverSignal(
933
+ options.resolver ??
934
+ capabilityModules.resolver.createResolverClientFromEnv(provider.resolver, undefined, {
935
+ allowedHosts: provider.allowedHosts,
936
+ cache,
937
+ identityScope: resolverIdentityScope,
938
+ ...(proxyPolicy
939
+ ? {
940
+ proxyIntent: {
941
+ mode: proxyPolicy.mode,
942
+ ...proxyClientOptions,
943
+ ...(stealthProfile ? { userAgent: stealthProfile.userAgent } : {}),
944
+ },
945
+ }
946
+ : {}),
947
+ }),
948
+ signal,
949
+ )
950
+ : bindResolverSignalWithoutRuntime(
951
+ options.resolver ??
952
+ createUnsupportedResolverClient("Provider does not declare resolver capability"),
953
+ signal,
954
+ ),
955
+ auth: createAuthFlowHelpers({ signal }),
956
+ },
957
+ getPatch: flowContextStore.getPatch,
958
+ };
959
+ }
960
+
961
+ type ProviderRequestCost = {
962
+ durationMs: number;
963
+ cpuUserMicros: number;
964
+ cpuSystemMicros: number;
965
+ cpuTotalMicros: number;
966
+ };
967
+
968
+ type ProviderServerLogEventBase = ProviderRequestCost & {
969
+ providerId: string;
970
+ kind: "operation" | "auth";
971
+ route: string;
972
+ requestId?: string;
973
+ status: number;
974
+ proxy?: ProxyTelemetryLogPayload;
975
+ };
976
+
977
+ export type ProviderServerLogEvent =
978
+ | (ProviderServerLogEventBase & {
979
+ level: "info";
980
+ event: "provider_request_completed";
981
+ })
982
+ | (ProviderServerLogEventBase & {
983
+ level: "warn" | "error";
984
+ event: "provider_request_failed";
985
+ code: string;
986
+ errorClass: string;
987
+ message: string;
988
+ upstreamStatus?: number;
989
+ errorCategory?: ProviderErrorCategory;
990
+ taxonomyVersion?: string;
991
+ retryable?: boolean;
992
+ signal?: "unregistered_provider_error_code";
993
+ signalFix?: string;
994
+ issues?: Array<{ path: string; code: string; message: string }>;
995
+ })
996
+ | {
997
+ level: "warn";
998
+ event: "provider_secrets_missing";
999
+ providerId: string;
1000
+ missingSecrets: string[];
1001
+ }
1002
+ | {
1003
+ level: "info";
1004
+ event: "provider_choice_token";
1005
+ providerId: string;
1006
+ purpose: string;
1007
+ operation: "parse" | "consume";
1008
+ format: "word" | "legacy";
1009
+ outcome: "success" | "not-found" | "invalid" | "unsupported" | "error";
1010
+ consumeMode: "never" | "on-parse" | "explicit";
1011
+ consumed: boolean;
1012
+ replay: boolean;
1013
+ }
1014
+ | {
1015
+ level: "warn";
1016
+ event: "provider_cleanup_failed";
1017
+ providerId: string;
1018
+ kind: "operation" | "auth";
1019
+ route: string;
1020
+ requestId?: string;
1021
+ resource: "browser" | "stealth";
1022
+ errorClass: string;
1023
+ message: string;
1024
+ }
1025
+ | {
1026
+ level: "error";
1027
+ event: "provider_shutdown_hook_failed";
1028
+ providerId: string;
1029
+ hookIndex: number;
1030
+ errorClass: string;
1031
+ message: string;
1032
+ }
1033
+ | SelfTestCancellationLogEvent;
1034
+
1035
+ export type ProviderServerLogger = (event: ProviderServerLogEvent) => void;
1036
+
1037
+ export type ProviderServerOptions = {
1038
+ logger?: ProviderServerLogger;
1039
+ /** Optional provider-specific operation executor. Stateful providers use this to preserve provider-local runtime semantics. */
1040
+ operationExecutor?: ProviderServerOperationExecutor;
1041
+ /** Optional signed internal executor for stateful owner forwarding. */
1042
+ internalOperationExecutor?: ProviderServerOperationExecutor;
1043
+ statefulForwarding?: {
1044
+ readonly secret: string;
1045
+ readonly maxSkewMs?: number;
1046
+ readonly replayCacheMaxEntries?: number;
1047
+ /** Required fail-closed check against the SDK/runtime owner registry. */
1048
+ readonly validateOwnerFence: ProviderServerStatefulOwnerFenceValidator;
1049
+ };
1050
+ /** Optional STT override for tests or custom hosts; local/prod normally resolves from env. */
1051
+ stt?: SttContext;
1052
+ /** Optional OCR override for tests or custom hosts; local/prod normally resolves from env. */
1053
+ ocr?: OcrContext;
1054
+ /** Optional resolver override for tests or custom hosts; local/prod normally resolves from env. */
1055
+ resolver?: ResolverContext;
1056
+ /** Optional runtime state override for tests or custom hosts. Production resolves Redis from env and fails closed when unavailable. */
1057
+ state?: ProviderRuntimeState;
1058
+ /** Allow process-local runtime state only for local development and tests. */
1059
+ allowMemoryStateFallback?: boolean;
1060
+ /**
1061
+ * Graceful process shutdown. Hooks run in declaration order after listeners stop accepting work.
1062
+ *
1063
+ * @example
1064
+ * ```ts
1065
+ * await serve(provider, {
1066
+ * shutdown: {
1067
+ * hooks: [
1068
+ * async () => { await emitter.flush(); },
1069
+ * async () => { await sessionManager.closeAll("server-shutdown"); },
1070
+ * async () => { await lease.release(); },
1071
+ * async () => { await router.close(); },
1072
+ * ],
1073
+ * },
1074
+ * });
1075
+ * ```
1076
+ */
1077
+ shutdown?: {
1078
+ readonly hooks?: Array<() => Promise<void>>;
1079
+ readonly signals?: boolean | NodeJS.Signals[];
1080
+ readonly timeoutMs?: number;
1081
+ };
1082
+ };
1083
+
1084
+ const defaultProviderServerLogger: ProviderServerLogger = (event) => {
1085
+ const line = JSON.stringify(event);
1086
+ if (event.level === "info") {
1087
+ console.log(line);
1088
+ return;
1089
+ }
1090
+ console.error(line);
1091
+ };
1092
+
1093
+ function startRequestCost(): {
1094
+ startedAtMs: number;
1095
+ cpuStart: NodeJS.CpuUsage;
1096
+ } {
1097
+ return {
1098
+ startedAtMs: performance.now(),
1099
+ cpuStart: process.cpuUsage(),
1100
+ };
1101
+ }
1102
+
1103
+ function finishRequestCost(input: {
1104
+ startedAtMs: number;
1105
+ cpuStart: NodeJS.CpuUsage;
1106
+ }): ProviderRequestCost {
1107
+ const cpuDelta = process.cpuUsage(input.cpuStart);
1108
+ return {
1109
+ durationMs: Math.max(0, Math.round(performance.now() - input.startedAtMs)),
1110
+ cpuUserMicros: Math.max(0, cpuDelta.user),
1111
+ cpuSystemMicros: Math.max(0, cpuDelta.system),
1112
+ cpuTotalMicros: Math.max(0, cpuDelta.user + cpuDelta.system),
1113
+ };
1114
+ }
1115
+
1116
+ function zodDetails(error: z.ZodError): Array<{
1117
+ path: string;
1118
+ code: string;
1119
+ message: string;
1120
+ }> {
1121
+ return error.issues.map((issue) => ({
1122
+ path: issue.path.join("."),
1123
+ code: issue.code,
1124
+ message: issue.message,
1125
+ }));
1126
+ }
1127
+
1128
+ // Category-level projection with code-aware honesty overrides: a missing
1129
+ // deployment secret is an APIFuse-side defect even though its category
1130
+ // (credential_unavailable) usually means a caller credential problem, an
1131
+ // internal stateful-routing deadline is APIFuse-owned despite its timeout
1132
+ // category, and the built-in upstream failure families keep their upstream
1133
+ // attribution even when the author left the category at the provider_error
1134
+ // default.
1135
+ function publicErrorSource(error: unknown, category: ProviderErrorCategory): ProviderErrorSource {
1136
+ if (error instanceof StatefulRoutingDeadlineError) return "apifuse";
1137
+ if (isProviderError(error)) {
1138
+ if (error.code === MISSING_SECRET_CODE) return "apifuse";
1139
+ if (error.code === "UPSTREAM_ERROR" || error.code === "BLOCKED") {
1140
+ return "upstream_failure";
1141
+ }
1142
+ }
1143
+ return sourceForCategory(category);
1144
+ }
1145
+
1146
+ function toErrorResponse(
1147
+ error: unknown,
1148
+ requestId?: string,
1149
+ declaredErrorCode?: OperationErrorCode,
1150
+ ): OperationErrorResponse {
1151
+ const observability = errorObservabilityDetails(error, declaredErrorCode);
1152
+ const source = publicErrorSource(error, observability.category);
1153
+ if (error instanceof StatefulRoutingDeadlineError) {
1154
+ return {
1155
+ error: {
1156
+ code: "STATEFUL_FORWARDING_DEADLINE_EXPIRED",
1157
+ message: "Stateful forwarding deadline expired.",
1158
+ ...(requestId ? { requestId } : {}),
1159
+ retryable: observability.retryable,
1160
+ source,
1161
+ },
1162
+ };
1163
+ }
1164
+
1165
+ if (isProviderError(error)) {
1166
+ const details = error.details;
1167
+ return {
1168
+ error: {
1169
+ code: error.code ?? "provider_error",
1170
+ message: publicProviderErrorMessage(error),
1171
+ ...(requestId ? { requestId } : {}),
1172
+ retryable: observability.retryable,
1173
+ source,
1174
+ ...(error.fix ? { fix: error.fix } : {}),
1175
+ ...(details !== undefined ? { details } : {}),
1176
+ },
1177
+ };
1178
+ }
1179
+
1180
+ if (error instanceof z.ZodError) {
1181
+ return {
1182
+ error: {
1183
+ code: "invalid_request",
1184
+ message: "Invalid request body",
1185
+ ...(requestId ? { requestId } : {}),
1186
+ retryable: observability.retryable,
1187
+ source,
1188
+ details: zodDetails(error),
1189
+ },
1190
+ };
1191
+ }
1192
+
1193
+ // A masked internal error MUST NOT be advertised as retryable: without an
1194
+ // explicit retryable:false the hub (bori provider-backed engine) defaults 5xx
1195
+ // to retryable:true, which turns a deterministic pre-upstream crash into an
1196
+ // infinite START->CONTINUE->restart loop (2026-07-22 catchtable reserve RCA).
1197
+ // We still refuse to leak message/stack — only the error class name (or the
1198
+ // primitive type for non-Error throwables) is surfaced for ops triage.
1199
+ return {
1200
+ error: {
1201
+ code: "internal_error",
1202
+ message: "Internal error",
1203
+ ...(requestId ? { requestId } : {}),
1204
+ retryable: observability.retryable,
1205
+ source,
1206
+ details: {
1207
+ retryable: false,
1208
+ category: "internal_error",
1209
+ errorClass: error instanceof Error ? error.name : typeof error,
1210
+ },
1211
+ },
1212
+ };
1213
+ }
1214
+
1215
+ // Accepts `unknown` so the branded guards narrow cleanly from the top: the
1216
+ // subtype error classes are structurally compatible with ProviderError, so
1217
+ // narrowing from a ProviderError-typed value would collapse the negative branch
1218
+ // to `never`. Narrowing from unknown avoids that while still recognizing errors
1219
+ // from a duplicate SDK module instance.
1220
+ function providerObservabilityDetails(
1221
+ error: unknown,
1222
+ declaredErrorCode?: OperationErrorCode,
1223
+ ): ErrorObservabilityDetails | undefined {
1224
+ const declaredRetryable = sdkOwnsErrorResolution(error)
1225
+ ? undefined
1226
+ : declaredErrorCode?.retryable;
1227
+ // Session-expiry surfaces the credential_expired category + the opt-in
1228
+ // retryable signal so Gateway/Credential Service can refresh and re-drive the
1229
+ // operation (see design.md §4.3 D3). Without this branch the auth error would
1230
+ // serialize as a bare 401 with no retryable/category, losing the refresh
1231
+ // signal for exactly the retryOnAuthRefresh operations it is meant to enable.
1232
+ if (isSessionExpiredError(error)) {
1233
+ return {
1234
+ category: error.options?.category ?? "credential_expired",
1235
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
1236
+ retryable: error.options?.retryable ?? declaredRetryable ?? false,
1237
+ };
1238
+ }
1239
+ // Missing-secret errors carry the canonical credential_unavailable category
1240
+ // so Gateway/observability can attribute the failure to provisioning, not
1241
+ // the upstream. Matched by code (not constructor) so both the SDK-owned
1242
+ // runtime gate and any not-yet-migrated provider-thrown MISSING_SECRET
1243
+ // serialize identically, including across duplicate SDK module instances.
1244
+ if (isProviderError(error) && error.code === MISSING_SECRET_CODE) {
1245
+ return {
1246
+ category: error.options?.category ?? "credential_unavailable",
1247
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
1248
+ retryable: error.options?.retryable ?? declaredRetryable ?? false,
1249
+ };
1250
+ }
1251
+ if (!isTransportError(error)) {
1252
+ return undefined;
1253
+ }
1254
+ const isProxyPoolCode =
1255
+ error.code === PROXY_POOL_EXHAUSTED_CODE ||
1256
+ error.code === PROXY_EDGE_AUTH_REJECTED_CODE ||
1257
+ error.code === "PROXY_ALLOCATION_FAILED";
1258
+ const category =
1259
+ error.options?.category ??
1260
+ (isProxyPoolCode
1261
+ ? "proxy_pool"
1262
+ : error.code === PROXY_AUTH_IP_DENIED_CODE
1263
+ ? "anti_bot_blocked"
1264
+ : error.code === "transport_timeout"
1265
+ ? "timeout"
1266
+ : error.code === "transport_network_error"
1267
+ ? "network"
1268
+ : error.upstreamStatus
1269
+ ? categoryForStatus(error.upstreamStatus)
1270
+ : "upstream_http");
1271
+ return {
1272
+ category,
1273
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
1274
+ retryable:
1275
+ error.options?.retryable ??
1276
+ (category === "upstream_http" && error.upstreamStatus
1277
+ ? error.upstreamStatus >= 500
1278
+ : isRetryableCategory(category)),
1279
+ ...(error.upstreamStatus ? { upstreamStatus: error.upstreamStatus } : {}),
1280
+ };
1281
+ }
1282
+
1283
+ function errorObservabilityDetails(
1284
+ error: unknown,
1285
+ declaredErrorCode?: OperationErrorCode,
1286
+ ): ErrorObservabilityDetails {
1287
+ const effectiveDeclaration = sdkOwnsErrorResolution(error) ? undefined : declaredErrorCode;
1288
+ const providerDetails = providerObservabilityDetails(error, effectiveDeclaration);
1289
+ if (providerDetails) return providerDetails;
1290
+
1291
+ if (error instanceof z.ZodError || isValidationError(error)) {
1292
+ const declaredStatus = effectiveDeclaration?.status;
1293
+ return {
1294
+ category:
1295
+ isProviderError(error) && error.options?.category
1296
+ ? error.options.category
1297
+ : isEmittableErrorStatus(declaredStatus) &&
1298
+ categoryForStatus(declaredStatus) === "upstream_rejected"
1299
+ ? "upstream_rejected"
1300
+ : isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
1301
+ ? "provider_error"
1302
+ : "input_validation",
1303
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
1304
+ retryable: isProviderError(error)
1305
+ ? (error.options?.retryable ?? effectiveDeclaration?.retryable ?? false)
1306
+ : false,
1307
+ };
1308
+ }
1309
+
1310
+ if (error instanceof StatefulRoutingDeadlineError) {
1311
+ return {
1312
+ category: "timeout",
1313
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
1314
+ retryable: false,
1315
+ };
1316
+ }
1317
+
1318
+ if (isProviderError(error)) {
1319
+ // Deterministic upstream refusals default to the rejection category:
1320
+ // the UPSTREAM_REJECTED family and any operation-declared rejection
1321
+ // status (409/410/422) classify as upstream_rejected unless the
1322
+ // author set an explicit category.
1323
+ const declaredStatus = effectiveDeclaration?.status;
1324
+ const rejectionDefault =
1325
+ error.code === "UPSTREAM_REJECTED" ||
1326
+ (isEmittableErrorStatus(declaredStatus) &&
1327
+ categoryForStatus(declaredStatus) === "upstream_rejected")
1328
+ ? ("upstream_rejected" as const)
1329
+ : ("provider_error" as const);
1330
+ return {
1331
+ category: error.options?.category ?? rejectionDefault,
1332
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
1333
+ retryable: error.options?.retryable ?? effectiveDeclaration?.retryable ?? false,
1334
+ };
1335
+ }
1336
+
1337
+ return {
1338
+ category: "internal_error",
1339
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
1340
+ retryable: false,
1341
+ };
1342
+ }
1343
+
1344
+ function responseWithErrorObservability(
1345
+ response: Response,
1346
+ error: unknown,
1347
+ declaredErrorCode?: OperationErrorCode,
1348
+ ): Response {
1349
+ const headers = new Headers(response.headers);
1350
+ headers.set(
1351
+ ERROR_OBSERVABILITY_HEADER,
1352
+ JSON.stringify(errorObservabilityDetails(error, declaredErrorCode)),
1353
+ );
1354
+ return new Response(response.body, {
1355
+ status: response.status,
1356
+ statusText: response.statusText,
1357
+ headers,
1358
+ });
1359
+ }
1360
+
1361
+ function publicProviderErrorMessage(error: ProviderError): string {
1362
+ if (isTransportError(error)) {
1363
+ if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
1364
+ return error.message;
1365
+ }
1366
+ if (error.code === PROXY_EDGE_AUTH_REJECTED_CODE) {
1367
+ return error.message;
1368
+ }
1369
+ if (error.code === PROXY_POOL_EXHAUSTED_CODE) {
1370
+ return error.message;
1371
+ }
1372
+ if (error.code === "transport_timeout") return "Request timed out";
1373
+ if (error.code === "transport_network_error") return "Network error";
1374
+ if (error.code === "upstream_http_error" && error.status) {
1375
+ return `Upstream request failed with status ${error.status}`;
1376
+ }
1377
+ if (error.status) {
1378
+ return `Upstream request failed with status ${error.status}`;
1379
+ }
1380
+ return "Upstream request failed";
1381
+ }
1382
+ return error.message;
1383
+ }
1384
+
1385
+ function isEmittableErrorStatus(value: unknown): value is ProviderErrorStatus {
1386
+ return (
1387
+ typeof value === "number" && VALID_OPERATION_ERROR_STATUSES.some((status) => status === value)
1388
+ );
1389
+ }
1390
+
1391
+ function toStatusCode(error: unknown, declaredErrorCode?: OperationErrorCode): ProviderErrorStatus {
1392
+ if (error instanceof z.ZodError) {
1393
+ return 400;
1394
+ }
1395
+ if (error instanceof StatefulRoutingDeadlineError) {
1396
+ return 504;
1397
+ }
1398
+ if (isProviderError(error)) {
1399
+ if (!sdkOwnsErrorResolution(error) && isEmittableErrorStatus(declaredErrorCode?.status)) {
1400
+ return declaredErrorCode.status;
1401
+ }
1402
+ // Canonical SDK code → status mapping lives in error-resolution.ts so
1403
+ // the authoring lint and this runtime path share one source of truth.
1404
+ if (typeof error.code === "string") {
1405
+ const mappedStatus = SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES.get(error.code);
1406
+ if (mappedStatus !== undefined) {
1407
+ return mappedStatus;
1408
+ }
1409
+ }
1410
+ if (isTransportError(error)) {
1411
+ return error.code === "transport_timeout" ? 504 : 502;
1412
+ }
1413
+ if (isValidationError(error)) {
1414
+ return error.options?.category === "output_validation" ? 500 : 400;
1415
+ }
1416
+
1417
+ return 500;
1418
+ }
1419
+
1420
+ return 500;
1421
+ }
1422
+
1423
+ function sdkOwnsErrorResolution(error: unknown): boolean {
1424
+ if (isSessionExpiredError(error)) return true;
1425
+ if (isTransportError(error)) return true;
1426
+ if (error instanceof z.ZodError) return true;
1427
+ if (error instanceof StatefulRoutingDeadlineError) return true;
1428
+ return (
1429
+ isProviderError(error) &&
1430
+ typeof error.code === "string" &&
1431
+ SDK_RUNTIME_OWNED_ERROR_CODES.has(error.code)
1432
+ );
1433
+ }
1434
+
1435
+ type OperationErrorCodeLookup = ReadonlyMap<string, ReadonlyMap<string, OperationErrorCode>>;
1436
+
1437
+ function buildOperationErrorCodeLookup(provider: ProviderDefinition): OperationErrorCodeLookup {
1438
+ return new Map(
1439
+ Object.entries(provider.operations).flatMap(([operationId, operation]) => {
1440
+ const errorCodes = operation.docs?.errorCodes;
1441
+ return errorCodes?.length
1442
+ ? [[operationId, new Map(errorCodes.map((entry) => [entry.code, entry]))] as const]
1443
+ : [];
1444
+ }),
1445
+ );
1446
+ }
1447
+
1448
+ function declaredErrorCodeFor(
1449
+ error: unknown,
1450
+ operationId: string | undefined,
1451
+ lookup: OperationErrorCodeLookup,
1452
+ ): OperationErrorCode | undefined {
1453
+ if (!operationId || !isProviderError(error) || typeof error.code !== "string") return undefined;
1454
+ return lookup.get(operationId)?.get(error.code);
1455
+ }
1456
+
1457
+ function extractRequestId(raw: unknown): string | undefined {
1458
+ if (!raw || typeof raw !== "object") {
1459
+ return undefined;
1460
+ }
1461
+
1462
+ const value = Object.getOwnPropertyDescriptor(raw, "requestId")?.value;
1463
+ return typeof value === "string" ? value : undefined;
1464
+ }
1465
+
1466
+ type ProviderErrorCauseFrame = {
1467
+ errorClass: string;
1468
+ code?: string;
1469
+ messageLength: number;
1470
+ messageFingerprint: string;
1471
+ };
1472
+
1473
+ const MAX_PROVIDER_ERROR_CAUSE_FRAMES = 5;
1474
+
1475
+ function providerErrorCauseChain(error: unknown): ProviderErrorCauseFrame[] | undefined {
1476
+ if (!(error instanceof Error) && !isProviderError(error)) return undefined;
1477
+
1478
+ const seen = new Set<object>([error]);
1479
+ const frames: ProviderErrorCauseFrame[] = [];
1480
+ let cause = error.cause;
1481
+
1482
+ while (
1483
+ frames.length < MAX_PROVIDER_ERROR_CAUSE_FRAMES &&
1484
+ (cause instanceof Error || isProviderError(cause)) &&
1485
+ !seen.has(cause)
1486
+ ) {
1487
+ seen.add(cause);
1488
+ const message = cause.message;
1489
+ frames.push({
1490
+ errorClass: cause.name,
1491
+ ...(isProviderError(cause) && typeof cause.code === "string" ? { code: cause.code } : {}),
1492
+ messageLength: message.length,
1493
+ messageFingerprint: createHash("sha256").update(message).digest("hex").slice(0, 12),
1494
+ });
1495
+ cause = cause.cause;
1496
+ }
1497
+
1498
+ return frames.length > 0 ? frames : undefined;
1499
+ }
1500
+
1501
+ function logProviderError(
1502
+ logger: ProviderServerLogger | unknown,
1503
+ provider: ProviderDefinition,
1504
+ kind: "operation" | "auth",
1505
+ route: string,
1506
+ requestId: string | undefined,
1507
+ error: unknown,
1508
+ status: number,
1509
+ cost: ProviderRequestCost,
1510
+ declaredErrorCode?: OperationErrorCode,
1511
+ proxyTelemetry?: ProxyTelemetryCollector,
1512
+ ): void {
1513
+ const code = isProviderError(error)
1514
+ ? (error.code ?? "provider_error")
1515
+ : error instanceof z.ZodError
1516
+ ? "invalid_request"
1517
+ : error instanceof StatefulRoutingDeadlineError
1518
+ ? "STATEFUL_FORWARDING_DEADLINE_EXPIRED"
1519
+ : "internal_error";
1520
+ const errorClass = error instanceof Error ? error.name : typeof error;
1521
+ const message = error instanceof Error ? error.message : String(error);
1522
+ const causeChain = providerErrorCauseChain(error);
1523
+ const details = errorObservabilityDetails(error, declaredErrorCode);
1524
+ const isUnregisteredProviderErrorCode =
1525
+ status === 500 &&
1526
+ isProviderError(error) &&
1527
+ !isValidationError(error) &&
1528
+ typeof error.code === "string" &&
1529
+ !SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code) &&
1530
+ declaredErrorCode === undefined;
1531
+ const proxy = proxyTelemetry?.toLogPayload();
1532
+ const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
1533
+ emit({
1534
+ level: status >= 500 ? "error" : "warn",
1535
+ event: "provider_request_failed",
1536
+ providerId: provider.id,
1537
+ kind,
1538
+ route,
1539
+ ...(requestId ? { requestId } : {}),
1540
+ status,
1541
+ ...cost,
1542
+ ...(proxy ? { proxy } : {}),
1543
+ code,
1544
+ errorClass,
1545
+ message,
1546
+ ...(causeChain ? { causeChain } : {}),
1547
+ ...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
1548
+ errorCategory: details.category,
1549
+ taxonomyVersion: details.taxonomyVersion,
1550
+ retryable: details.retryable,
1551
+ ...(isUnregisteredProviderErrorCode
1552
+ ? {
1553
+ signal: "unregistered_provider_error_code" as const,
1554
+ signalFix:
1555
+ "Declare this code (with status and retryable) in the operation's docs.errorCodes so it serves its intended status instead of 500.",
1556
+ }
1557
+ : {}),
1558
+ ...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
1559
+ });
1560
+ }
1561
+
1562
+ function logProviderCleanupError(
1563
+ logger: ProviderServerLogger | unknown,
1564
+ provider: ProviderDefinition,
1565
+ kind: "operation" | "auth",
1566
+ operationId: string,
1567
+ requestId: string | undefined,
1568
+ resource: "browser" | "stealth",
1569
+ error: unknown,
1570
+ ): void {
1571
+ const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
1572
+ const errorClass = error instanceof Error ? error.name : typeof error;
1573
+ const message = error instanceof Error ? error.message : String(error);
1574
+ emit({
1575
+ level: "warn",
1576
+ event: "provider_cleanup_failed",
1577
+ providerId: provider.id,
1578
+ kind,
1579
+ route: operationId,
1580
+ ...(requestId ? { requestId } : {}),
1581
+ resource,
1582
+ errorClass,
1583
+ message,
1584
+ });
1585
+ }
1586
+
1587
+ function logProviderSuccess(
1588
+ logger: ProviderServerLogger | unknown,
1589
+ provider: ProviderDefinition,
1590
+ kind: "operation" | "auth",
1591
+ route: string,
1592
+ requestId: string | undefined,
1593
+ status: number,
1594
+ cost: ProviderRequestCost,
1595
+ proxyTelemetry?: ProxyTelemetryCollector,
1596
+ ): void {
1597
+ const proxy = proxyTelemetry?.toLogPayload();
1598
+ const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
1599
+ emit({
1600
+ level: "info",
1601
+ event: "provider_request_completed",
1602
+ providerId: provider.id,
1603
+ kind,
1604
+ route,
1605
+ ...(requestId ? { requestId } : {}),
1606
+ status,
1607
+ ...cost,
1608
+ ...(proxy ? { proxy } : {}),
1609
+ });
1610
+ }
1611
+
1612
+ function toJsonSuccessResponse(
1613
+ result: unknown,
1614
+ ctx?: ProviderContext,
1615
+ ): Response | OperationSuccessResponse {
1616
+ if (result instanceof Response) {
1617
+ return result;
1618
+ }
1619
+
1620
+ if (result instanceof ReadableStream) {
1621
+ return new Response(result);
1622
+ }
1623
+
1624
+ const cacheMeta = ctx?.cache.responseMeta();
1625
+ const retryMeta = ctx ? retryResponseMeta.get(ctx) : undefined;
1626
+ const meta =
1627
+ cacheMeta || retryMeta
1628
+ ? {
1629
+ ...(cacheMeta
1630
+ ? {
1631
+ cached: cacheMeta.hit,
1632
+ stale: cacheMeta.stale,
1633
+ cache: cacheMeta,
1634
+ }
1635
+ : {}),
1636
+ ...(retryMeta ? { retry: retryMeta } : {}),
1637
+ }
1638
+ : undefined;
1639
+ return {
1640
+ data: result,
1641
+ ...(meta ? { meta } : {}),
1642
+ };
1643
+ }
1644
+
1645
+ function isAsyncIterable<T = unknown>(value: unknown): value is AsyncIterable<T> {
1646
+ if (!value || typeof value !== "object") return false;
1647
+ const iterator = Reflect.get(value, Symbol.asyncIterator);
1648
+ return typeof iterator === "function";
1649
+ }
1650
+
1651
+ function responseWithCleanup(response: Response, cleanup: RequestCleanup): Response {
1652
+ if (!response.body) {
1653
+ void cleanup();
1654
+ return response;
1655
+ }
1656
+ const reader = response.body.getReader();
1657
+ let cleaned = false;
1658
+ const runCleanup = async () => {
1659
+ if (cleaned) return;
1660
+ cleaned = true;
1661
+ await cleanup();
1662
+ };
1663
+ const body = new ReadableStream<Uint8Array>({
1664
+ async pull(controller) {
1665
+ try {
1666
+ const { done, value } = await reader.read();
1667
+ if (done) {
1668
+ controller.close();
1669
+ await runCleanup();
1670
+ return;
1671
+ }
1672
+ if (value) controller.enqueue(value);
1673
+ } catch (error) {
1674
+ await runCleanup();
1675
+ controller.error(error);
1676
+ }
1677
+ },
1678
+ async cancel(reason) {
1679
+ try {
1680
+ await reader.cancel(reason);
1681
+ } finally {
1682
+ await runCleanup();
1683
+ }
1684
+ },
1685
+ });
1686
+ return new Response(body, {
1687
+ headers: response.headers,
1688
+ status: response.status,
1689
+ statusText: response.statusText,
1690
+ });
1691
+ }
1692
+
1693
+ async function validateSseEvent(
1694
+ operation: OperationDefinition,
1695
+ event: ProviderStreamEvent,
1696
+ ): Promise<ProviderStreamEvent> {
1697
+ const transport = getSseTransport(operation);
1698
+ const schema = transport?.events?.[event.event];
1699
+ if (!schema) {
1700
+ if (event.event === APIFUSE_STREAM_ERROR_EVENT || event.event === APIFUSE_STREAM_DONE_EVENT) {
1701
+ return event;
1702
+ }
1703
+ throw new ProviderError(
1704
+ `SSE event "${event.event}" is not declared in operation transport.events.`,
1705
+ {
1706
+ code: "SSE_EVENT_UNDECLARED",
1707
+ category: "output_validation",
1708
+ retryable: false,
1709
+ fix: `Add "${event.event}" to transport.events or stop emitting that event.`,
1710
+ },
1711
+ );
1712
+ }
1713
+ const data = await parseSchema(schema, event.data, `transport.events.${event.event}`);
1714
+ return { ...event, data };
1715
+ }
1716
+
1717
+ function byteLength(value: Uint8Array | string): number {
1718
+ if (typeof value === "string") {
1719
+ return new TextEncoder().encode(value).byteLength;
1720
+ }
1721
+ return value.byteLength;
1722
+ }
1723
+
1724
+ function assertStreamPayloadWithinLimit(
1725
+ actualBytes: number,
1726
+ maxBytes: number | undefined,
1727
+ kind: "event" | "chunk",
1728
+ ): void {
1729
+ if (maxBytes === undefined || actualBytes <= maxBytes) return;
1730
+ throw new ProviderError(
1731
+ `Stream ${kind} exceeded declared byte limit (${actualBytes} > ${maxBytes}).`,
1732
+ {
1733
+ code: kind === "event" ? "STREAM_EVENT_TOO_LARGE" : "STREAM_CHUNK_TOO_LARGE",
1734
+ retryable: false,
1735
+ category: "input_validation",
1736
+ fix:
1737
+ kind === "event"
1738
+ ? "Emit smaller SSE events or increase transport.maxEventBytes."
1739
+ : "Emit smaller stream chunks or increase transport.maxChunkBytes.",
1740
+ },
1741
+ );
1742
+ }
1743
+
1744
+ function toSseResponse(
1745
+ operation: OperationDefinition,
1746
+ result: AsyncIterable<ProviderStreamEvent>,
1747
+ cleanup: RequestCleanup,
1748
+ requestId?: string,
1749
+ ): Response {
1750
+ const encoder = new TextEncoder();
1751
+ const iterator = result[Symbol.asyncIterator]();
1752
+ const transport = getSseTransport(operation);
1753
+ let done = false;
1754
+ let cleaned = false;
1755
+ const runCleanup = async () => {
1756
+ if (cleaned) return;
1757
+ cleaned = true;
1758
+ await cleanup();
1759
+ };
1760
+ const body = new ReadableStream<Uint8Array>({
1761
+ async pull(controller) {
1762
+ try {
1763
+ if (done) {
1764
+ controller.close();
1765
+ await runCleanup();
1766
+ return;
1767
+ }
1768
+ const next = await iterator.next();
1769
+ if (next.done) {
1770
+ done = true;
1771
+ controller.close();
1772
+ await runCleanup();
1773
+ return;
1774
+ }
1775
+ const validated = await validateSseEvent(operation, next.value);
1776
+ const encodedEvent = encodeSseEvent(validated);
1777
+ const encodedBytes = encoder.encode(encodedEvent);
1778
+ assertStreamPayloadWithinLimit(encodedBytes.byteLength, transport?.maxEventBytes, "event");
1779
+ controller.enqueue(encodedBytes);
1780
+ } catch (error) {
1781
+ const message = error instanceof Error ? error.message : "Stream failed";
1782
+ controller.enqueue(
1783
+ encoder.encode(
1784
+ encodeSseEvent(
1785
+ streamError("stream_error", message, {
1786
+ ...(requestId ? { requestId } : {}),
1787
+ }),
1788
+ ),
1789
+ ),
1790
+ );
1791
+ controller.close();
1792
+ done = true;
1793
+ await runCleanup();
1794
+ }
1795
+ },
1796
+ async cancel(reason) {
1797
+ try {
1798
+ await iterator.return?.(reason);
1799
+ } finally {
1800
+ await runCleanup();
1801
+ }
1802
+ },
1803
+ });
1804
+ return new Response(body, {
1805
+ headers: {
1806
+ "Cache-Control": "no-cache, no-transform",
1807
+ Connection: "keep-alive",
1808
+ "Content-Type": "text/event-stream; charset=utf-8",
1809
+ },
1810
+ });
1811
+ }
1812
+
1813
+ function enforceStreamChunkLimit(
1814
+ body: ReadableStream<Uint8Array>,
1815
+ maxChunkBytes: number | undefined,
1816
+ ): ReadableStream<Uint8Array> {
1817
+ if (maxChunkBytes === undefined) return body;
1818
+ const reader = body.getReader();
1819
+ return new ReadableStream<Uint8Array>({
1820
+ async pull(controller) {
1821
+ try {
1822
+ const { done, value } = await reader.read();
1823
+ if (done) {
1824
+ controller.close();
1825
+ return;
1826
+ }
1827
+ if (value) {
1828
+ assertStreamPayloadWithinLimit(byteLength(value), maxChunkBytes, "chunk");
1829
+ controller.enqueue(value);
1830
+ }
1831
+ } catch (error) {
1832
+ controller.error(error);
1833
+ }
1834
+ },
1835
+ cancel(reason) {
1836
+ return reader.cancel(reason);
1837
+ },
1838
+ });
1839
+ }
1840
+
1841
+ function toStreamingResponse(
1842
+ operation: OperationDefinition,
1843
+ result: unknown,
1844
+ cleanup: RequestCleanup,
1845
+ requestId?: string,
1846
+ ): Response {
1847
+ const transport = operation.transport?.kind ?? "json";
1848
+ if (transport === "sse" && (result instanceof Response || result instanceof ReadableStream)) {
1849
+ void cleanup();
1850
+ throw new ProviderError(
1851
+ "SSE operations must return an AsyncIterable of typed stream.event(...) values.",
1852
+ {
1853
+ code: "SSE_RESULT_UNSUPPORTED",
1854
+ category: "output_validation",
1855
+ retryable: false,
1856
+ fix: "Return an async generator that yields stream.event(name, data) so APIFuse can validate event schemas and enforce event byte limits.",
1857
+ },
1858
+ );
1859
+ }
1860
+ if (result instanceof Response) {
1861
+ const httpTransport = getHttpStreamTransport(operation);
1862
+ if (httpTransport && result.body && httpTransport?.maxChunkBytes !== undefined) {
1863
+ return responseWithCleanup(
1864
+ new Response(enforceStreamChunkLimit(result.body, httpTransport.maxChunkBytes), {
1865
+ headers: result.headers,
1866
+ status: result.status,
1867
+ statusText: result.statusText,
1868
+ }),
1869
+ cleanup,
1870
+ );
1871
+ }
1872
+ return responseWithCleanup(result, cleanup);
1873
+ }
1874
+ if (result instanceof ReadableStream) {
1875
+ const httpTransport = getHttpStreamTransport(operation);
1876
+ const stream =
1877
+ httpTransport !== undefined
1878
+ ? enforceStreamChunkLimit(result, httpTransport.maxChunkBytes)
1879
+ : result;
1880
+ return responseWithCleanup(
1881
+ new Response(stream, {
1882
+ headers:
1883
+ transport === "sse"
1884
+ ? { "Content-Type": "text/event-stream; charset=utf-8" }
1885
+ : {
1886
+ "Content-Type":
1887
+ operation.transport?.kind === "http-stream"
1888
+ ? (operation.transport.contentType ?? "application/octet-stream")
1889
+ : "application/octet-stream",
1890
+ },
1891
+ }),
1892
+ cleanup,
1893
+ );
1894
+ }
1895
+ if (transport === "sse" && isAsyncIterable<ProviderStreamEvent>(result)) {
1896
+ return toSseResponse(operation, result, cleanup, requestId);
1897
+ }
1898
+ void cleanup();
1899
+ throw new ProviderError(
1900
+ `Streaming operation returned unsupported result for transport "${transport}"`,
1901
+ {
1902
+ code: "STREAM_RESULT_UNSUPPORTED",
1903
+ fix: "Return an AsyncIterable of stream.event(...) values, a ReadableStream, or a Response from streaming operations.",
1904
+ },
1905
+ );
1906
+ }
1907
+
1908
+ function getSseTransport(operation: OperationDefinition): OperationSseTransport | undefined {
1909
+ return operation.transport?.kind === "sse" ? operation.transport : undefined;
1910
+ }
1911
+
1912
+ function getHttpStreamTransport(
1913
+ operation: OperationDefinition,
1914
+ ): OperationHttpStreamTransport | undefined {
1915
+ return operation.transport?.kind === "http-stream" ? operation.transport : undefined;
1916
+ }
1917
+
1918
+ function toAuthFlowResponse(
1919
+ result: unknown,
1920
+ contextPatch: Record<string, unknown | null> | undefined,
1921
+ ): Response | AuthFlowSuccessResponse {
1922
+ if (result instanceof Response) {
1923
+ return result;
1924
+ }
1925
+
1926
+ if (result instanceof ReadableStream) {
1927
+ return new Response(result);
1928
+ }
1929
+
1930
+ return {
1931
+ data: result,
1932
+ ...(contextPatch ? { contextPatch } : {}),
1933
+ };
1934
+ }
1935
+
1936
+ function authFlowLocaleFromHeaders(headers?: Record<string, string>): ProviderLocale {
1937
+ const header = Object.entries(headers ?? {}).find(
1938
+ ([key]) => key.toLowerCase() === "accept-language",
1939
+ )?.[1];
1940
+ for (const token of (header ?? "").split(",")) {
1941
+ const language = token.trim().split(";")[0]?.split("-")[0]?.toLowerCase();
1942
+ if (isAuthFlowLocale(language)) {
1943
+ return language;
1944
+ }
1945
+ }
1946
+ return "en";
1947
+ }
1948
+
1949
+ function isAuthFlowLocale(value: string | undefined): value is ProviderLocale {
1950
+ return value === "en" || value === "ko" || value === "ja";
1951
+ }
1952
+
1953
+ function isAuthTurn(value: unknown): value is AuthTurn {
1954
+ return !!value && typeof value === "object" && "kind" in value && "turnId" in value;
1955
+ }
1956
+
1957
+ function loadAuthFlowLocaleCatalogs(
1958
+ provider: ProviderDefinition,
1959
+ ): ProviderLocaleCatalogMap | undefined {
1960
+ for (const providerDir of [
1961
+ process.cwd(),
1962
+ join(process.cwd(), "providers", provider.id),
1963
+ join(process.cwd(), "providers-staging", provider.id),
1964
+ ]) {
1965
+ if (!existsSync(join(providerDir, "locales", "en.json"))) continue;
1966
+ try {
1967
+ return loadProviderLocaleCatalogs({
1968
+ providerDir,
1969
+ locales: AUTH_FLOW_LOCALES,
1970
+ });
1971
+ } catch {
1972
+ return undefined;
1973
+ }
1974
+ }
1975
+ return undefined;
1976
+ }
1977
+
1978
+ function materializeAuthFlowTurn(
1979
+ provider: ProviderDefinition,
1980
+ request: AuthFlowRequest,
1981
+ turn: AuthTurn,
1982
+ ): AuthTurn {
1983
+ const catalogs = loadAuthFlowLocaleCatalogs(provider);
1984
+ if (!catalogs) return turn;
1985
+ return localizeAuthTurn(turn, {
1986
+ catalogs,
1987
+ locale: authFlowLocaleFromHeaders(request.headers),
1988
+ });
1989
+ }
1990
+
1991
+ function withAuthRequestHeaders(request: AuthFlowRequest, headers: Headers): AuthFlowRequest {
1992
+ return {
1993
+ ...request,
1994
+ headers: {
1995
+ ...(request.headers ?? {}),
1996
+ ...Object.fromEntries(headers.entries()),
1997
+ },
1998
+ };
1999
+ }
2000
+
2001
+ async function handleOperation(
2002
+ provider: ProviderDefinition,
2003
+ request: OperationRequest,
2004
+ operationId: string,
2005
+ options: ProviderServerRuntimeOptions,
2006
+ state: ProviderRuntimeState = createUnsupportedProviderRuntimeState(),
2007
+ proxyTelemetry?: ProxyTelemetryCollector,
2008
+ signal?: AbortSignal,
2009
+ ): Promise<Response | OperationResponse> {
2010
+ const ctx = createProviderContext(
2011
+ provider,
2012
+ request,
2013
+ operationId,
2014
+ options,
2015
+ state,
2016
+ proxyTelemetry,
2017
+ signal,
2018
+ );
2019
+ const operation = provider.operations[operationId];
2020
+ const streaming = operation?.transport?.kind && operation.transport.kind !== "json";
2021
+ let cleanupCalled = false;
2022
+ const cleanup = async () => {
2023
+ if (cleanupCalled) return;
2024
+ cleanupCalled = true;
2025
+ try {
2026
+ ctx.stealth.close?.();
2027
+ } catch (error) {
2028
+ logProviderCleanupError(
2029
+ options.logger,
2030
+ provider,
2031
+ "operation",
2032
+ operationId,
2033
+ request.requestId,
2034
+ "stealth",
2035
+ error,
2036
+ );
2037
+ }
2038
+ try {
2039
+ await ctx.browser.close?.();
2040
+ } catch (error) {
2041
+ logProviderCleanupError(
2042
+ options.logger,
2043
+ provider,
2044
+ "operation",
2045
+ operationId,
2046
+ request.requestId,
2047
+ "browser",
2048
+ error,
2049
+ );
2050
+ }
2051
+ };
2052
+ try {
2053
+ const result = options.operationExecutor
2054
+ ? await options.operationExecutor({
2055
+ provider,
2056
+ operationId,
2057
+ ctx,
2058
+ request,
2059
+ signal,
2060
+ })
2061
+ : await executeOperation(provider, operationId, ctx, request.input);
2062
+ if (streaming && operation) {
2063
+ return toStreamingResponse(operation, result, cleanup, request.requestId);
2064
+ }
2065
+ return toJsonSuccessResponse(result, ctx);
2066
+ } catch (error) {
2067
+ await cleanup();
2068
+ throw error;
2069
+ } finally {
2070
+ if (!streaming) await cleanup();
2071
+ }
2072
+ }
2073
+
2074
+ function responseWithProviderTelemetry(
2075
+ response: Response,
2076
+ proxyTelemetry?: ProxyTelemetryCollector,
2077
+ ): Response {
2078
+ const headerValue = proxyTelemetry?.toHeaderValue();
2079
+ const headers = new Headers(response.headers);
2080
+ headers.delete(PROVIDER_TELEMETRY_HEADER);
2081
+ if (headerValue) headers.set(PROVIDER_TELEMETRY_HEADER, headerValue);
2082
+ return new Response(response.body, {
2083
+ headers,
2084
+ status: response.status,
2085
+ statusText: response.statusText,
2086
+ });
2087
+ }
2088
+
2089
+ type AuthRoute = "start" | "continue" | "poll" | "abort" | "refresh";
2090
+
2091
+ async function handleAuthFlow(
2092
+ provider: ProviderDefinition,
2093
+ request: AuthFlowRequest,
2094
+ route: AuthRoute,
2095
+ options: ProviderServerRuntimeOptions,
2096
+ state: ProviderRuntimeState,
2097
+ proxyTelemetry?: ProxyTelemetryCollector,
2098
+ signal?: AbortSignal,
2099
+ ): Promise<Response | AuthFlowResponse> {
2100
+ const flow = provider.auth?.flow;
2101
+ if (!flow) {
2102
+ throw new ProviderError("Auth flow is not configured", {
2103
+ code: "AUTH_FLOW_NOT_CONFIGURED",
2104
+ });
2105
+ }
2106
+
2107
+ // Same SDK-owned gate as executeOperation: OAuth/credentials ceremonies
2108
+ // depend on declared secrets (client ids/secrets), so fail structured before
2109
+ // any flow code runs instead of at whatever point the ceremony first reads
2110
+ // the env. `abort` stays exempt: a user must always be able to cancel a
2111
+ // stranded flow even when provisioning is broken.
2112
+ const { context, getPatch } = createAuthFlowContext(
2113
+ provider,
2114
+ request,
2115
+ options,
2116
+ state,
2117
+ proxyTelemetry,
2118
+ signal,
2119
+ );
2120
+ try {
2121
+ if (route !== "abort") {
2122
+ assertRequiredSecretsPresent(provider, context.env);
2123
+ }
2124
+ const result =
2125
+ route === "start"
2126
+ ? await flow.start(context)
2127
+ : route === "continue"
2128
+ ? await flow.continue(context, request.input ?? {})
2129
+ : route === "poll"
2130
+ ? flow.poll
2131
+ ? await flow.poll(context)
2132
+ : null
2133
+ : route === "abort"
2134
+ ? flow.abort
2135
+ ? await flow.abort(context)
2136
+ : null
2137
+ : flow.refresh
2138
+ ? await flow.refresh(context, request.input ?? {})
2139
+ : null;
2140
+
2141
+ if (route === "refresh" && !flow.refresh) {
2142
+ throw new AuthError("Provider auth flow does not support refresh.", {
2143
+ code: "refresh_not_supported",
2144
+ });
2145
+ }
2146
+
2147
+ const materializedResult =
2148
+ result &&
2149
+ !(result instanceof Response) &&
2150
+ !(result instanceof ReadableStream) &&
2151
+ isAuthTurn(result)
2152
+ ? materializeAuthFlowTurn(provider, request, result)
2153
+ : result;
2154
+ return toAuthFlowResponse(materializedResult, getPatch());
2155
+ } catch (error) {
2156
+ if (error instanceof AuthAbortError) {
2157
+ return toAuthFlowResponse(error.turn, getPatch());
2158
+ }
2159
+ throw error;
2160
+ } finally {
2161
+ try {
2162
+ context.stealth.close?.();
2163
+ } catch (error) {
2164
+ logProviderCleanupError(
2165
+ options.logger,
2166
+ provider,
2167
+ "auth",
2168
+ route,
2169
+ request.requestId,
2170
+ "stealth",
2171
+ error,
2172
+ );
2173
+ }
2174
+ }
2175
+ }
2176
+
2177
+ class StatefulForwardingReplayCache {
2178
+ readonly #nonces = new Map<string, number>();
2179
+ readonly #expiryBuckets = new Map<number, Set<string>>();
2180
+ #nextExpiryBucket?: number;
2181
+ #latestExpiryBucket?: number;
2182
+
2183
+ constructor(private readonly maxEntries: number) {}
2184
+
2185
+ claim(nonce: string, expiresAtMs: number, nowMs: number): "accepted" | "replayed" | "full" {
2186
+ this.dropExpiredBuckets(nowMs);
2187
+ if (this.#nonces.has(nonce)) return "replayed";
2188
+ if (this.#nonces.size >= this.maxEntries) return "full";
2189
+ const expiryBucket =
2190
+ Math.ceil(expiresAtMs / STATEFUL_FORWARDING_REPLAY_BUCKET_MS) *
2191
+ STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
2192
+ this.#nonces.set(nonce, expiryBucket);
2193
+ const bucket = this.#expiryBuckets.get(expiryBucket) ?? new Set<string>();
2194
+ bucket.add(nonce);
2195
+ this.#expiryBuckets.set(expiryBucket, bucket);
2196
+ this.#nextExpiryBucket = Math.min(this.#nextExpiryBucket ?? expiryBucket, expiryBucket);
2197
+ this.#latestExpiryBucket = Math.max(this.#latestExpiryBucket ?? expiryBucket, expiryBucket);
2198
+ return "accepted";
2199
+ }
2200
+
2201
+ private dropExpiredBuckets(nowMs: number): void {
2202
+ if (this.#nextExpiryBucket === undefined || this.#latestExpiryBucket === undefined) return;
2203
+ if (nowMs >= this.#latestExpiryBucket) {
2204
+ this.#nonces.clear();
2205
+ this.#expiryBuckets.clear();
2206
+ this.#nextExpiryBucket = undefined;
2207
+ this.#latestExpiryBucket = undefined;
2208
+ return;
2209
+ }
2210
+ while (this.#nextExpiryBucket <= nowMs) {
2211
+ const bucket = this.#expiryBuckets.get(this.#nextExpiryBucket);
2212
+ if (bucket) {
2213
+ for (const cachedNonce of bucket) this.#nonces.delete(cachedNonce);
2214
+ this.#expiryBuckets.delete(this.#nextExpiryBucket);
2215
+ }
2216
+ this.#nextExpiryBucket += STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
2217
+ }
2218
+ }
2219
+ }
2220
+
2221
+ function verifyStatefulForwardingRequest(input: {
2222
+ readonly options: ProviderServerOptions;
2223
+ readonly rawBody: string;
2224
+ readonly headers: Headers;
2225
+ readonly method: string;
2226
+ readonly path: string;
2227
+ readonly replayCache: StatefulForwardingReplayCache;
2228
+ }): void {
2229
+ const config = input.options.statefulForwarding;
2230
+ if (!config?.secret) {
2231
+ throw new ProviderError("Stateful forwarding is not configured.", {
2232
+ code: "STATEFUL_FORWARDING_NOT_CONFIGURED",
2233
+ });
2234
+ }
2235
+ const timestamp = input.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "";
2236
+ const signature = input.headers.get(STATEFUL_FORWARDING_SIGNATURE_HEADER) ?? "";
2237
+ const nonce = input.headers.get(STATEFUL_FORWARDING_NONCE_HEADER) ?? "";
2238
+ if (!timestamp || !signature || !nonce) {
2239
+ throw new ProviderError("Stateful forwarding signature headers are missing.", {
2240
+ code: "STATEFUL_FORWARDING_SIGNATURE_MISSING",
2241
+ });
2242
+ }
2243
+ if (nonce.length > 256) {
2244
+ throw new ProviderError("Stateful forwarding nonce is invalid.", {
2245
+ code: "STATEFUL_FORWARDING_NONCE_INVALID",
2246
+ });
2247
+ }
2248
+ const timestampMs = Date.parse(timestamp);
2249
+ const maxSkewMs = config.maxSkewMs ?? DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS;
2250
+ if (!Number.isFinite(timestampMs) || Math.abs(Date.now() - timestampMs) > maxSkewMs) {
2251
+ throw new ProviderError(
2252
+ "Stateful forwarding signature timestamp is outside the allowed skew.",
2253
+ { code: "STATEFUL_FORWARDING_TIMESTAMP_INVALID" },
2254
+ );
2255
+ }
2256
+ if (
2257
+ !verifyStatefulRequestSignature({
2258
+ secret: config.secret,
2259
+ timestamp,
2260
+ rawBody: input.rawBody,
2261
+ method: input.method,
2262
+ path: input.path,
2263
+ nonce,
2264
+ signature,
2265
+ })
2266
+ ) {
2267
+ throw new ProviderError("Stateful forwarding signature is invalid.", {
2268
+ code: "STATEFUL_FORWARDING_SIGNATURE_INVALID",
2269
+ });
2270
+ }
2271
+ const replayResult = input.replayCache.claim(nonce, timestampMs + maxSkewMs, Date.now());
2272
+ if (replayResult === "replayed") {
2273
+ throw new ProviderError("Stateful forwarding nonce has already been used.", {
2274
+ code: "STATEFUL_FORWARDING_REPLAY_DETECTED",
2275
+ });
2276
+ }
2277
+ if (replayResult === "full") {
2278
+ throw new ProviderError("Stateful forwarding replay cache is at capacity.", {
2279
+ code: "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
2280
+ });
2281
+ }
2282
+ }
2283
+
2284
+ function operationRequestFromForwardingEnvelope(
2285
+ envelope: ProviderServerStatefulForwardEnvelope,
2286
+ ): OperationRequest & { readonly deadlineAt?: string } {
2287
+ return {
2288
+ ...envelope.operationRequest,
2289
+ ...(envelope.deadlineAt !== undefined ? { deadlineAt: envelope.deadlineAt } : {}),
2290
+ };
2291
+ }
2292
+
2293
+ function parseStatefulForwardingEnvelope(rawBody: unknown): ProviderServerStatefulForwardEnvelope {
2294
+ const parsed = ProviderServerStatefulForwardEnvelopeSchema.safeParse(rawBody);
2295
+ if (parsed.success) return parsed.data;
2296
+ throw new ProviderError("Stateful forwarding envelope is invalid.", {
2297
+ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
2298
+ details: zodDetails(parsed.error),
2299
+ });
2300
+ }
2301
+
2302
+ /**
2303
+ * Primary, cross-runtime app factory. Declared capability ESM is preloaded
2304
+ * asynchronously, so this path works on Bun and every supported Node release.
2305
+ */
2306
+ export async function createServerAppAsync(
2307
+ provider: ProviderDefinition,
2308
+ options: ProviderServerOptions = {},
2309
+ ): Promise<Hono> {
2310
+ validateFailClosedDeclaration(provider);
2311
+ validateStatefulServerConfig(options);
2312
+ return createServerAppWithCapabilityModules(
2313
+ provider,
2314
+ options,
2315
+ await loadProviderCapabilityModules(provider),
2316
+ );
2317
+ }
2318
+
2319
+ /**
2320
+ * Synchronous compatibility factory. Standard providers remain synchronous on
2321
+ * every runtime because they load no capability modules. Providers declaring a
2322
+ * capability require Bun or Node >=22.12; older Node releases receive an
2323
+ * actionable error directing them to createServerAppAsync().
2324
+ */
2325
+ export function createServerApp(
2326
+ provider: ProviderDefinition,
2327
+ options: ProviderServerOptions = {},
2328
+ ): Hono {
2329
+ validateFailClosedDeclaration(provider);
2330
+ validateStatefulServerConfig(options);
2331
+ return createServerAppWithCapabilityModules(
2332
+ provider,
2333
+ options,
2334
+ loadProviderCapabilityModulesSync(provider),
2335
+ );
2336
+ }
2337
+
2338
+ function createServerAppWithCapabilityModules(
2339
+ provider: ProviderDefinition,
2340
+ serverOptions: ProviderServerOptions,
2341
+ capabilityModules: ProviderCapabilityModules,
2342
+ ): Hono {
2343
+ const options: ProviderServerRuntimeOptions = { ...serverOptions, capabilityModules };
2344
+ const app = new Hono();
2345
+ const logger = options.logger ?? defaultProviderServerLogger;
2346
+ const operationErrorCodes = buildOperationErrorCodeLookup(provider);
2347
+ const statefulForwardingReplayCache = new StatefulForwardingReplayCache(
2348
+ options.statefulForwarding?.replayCacheMaxEntries ??
2349
+ DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES,
2350
+ );
2351
+ const state =
2352
+ options.state ??
2353
+ createProviderRuntimeStateFromEnv({
2354
+ providerId: provider.id,
2355
+ allowMemoryFallback: options.allowMemoryStateFallback === true,
2356
+ });
2357
+
2358
+ // Boot-time visibility for unprovisioned declared secrets: emit a structured
2359
+ // warn so deploy tooling/alerting sees the gap the moment the pod boots,
2360
+ // instead of discovering it request-by-request. Deliberately log-only — a
2361
+ // boot crash would trade a structured MISSING_SECRET signal for
2362
+ // CrashLoopBackOff. Requests still fail closed via the executeOperation gate.
2363
+ const missingSecretsAtBoot = listMissingRequiredSecrets(
2364
+ provider,
2365
+ createEnvContext(provider.secrets?.map((secret) => secret.name)),
2366
+ );
2367
+ if (missingSecretsAtBoot.length > 0) {
2368
+ logger({
2369
+ level: "warn",
2370
+ event: "provider_secrets_missing",
2371
+ providerId: provider.id,
2372
+ missingSecrets: missingSecretsAtBoot,
2373
+ });
2374
+ }
2375
+
2376
+ app.notFound((c) => {
2377
+ const error = new ProviderError("Not found", { code: "not_found", retryable: false });
2378
+ return responseWithErrorObservability(c.json(toErrorResponse(error), 404), error);
2379
+ });
2380
+
2381
+ app.get("/health", (c) =>
2382
+ c.json({
2383
+ status: "ok",
2384
+ provider: provider.id,
2385
+ version: provider.version,
2386
+ }),
2387
+ );
2388
+
2389
+ app.post(STATEFUL_INTERNAL_OPERATIONS_ROUTE, async (c) => {
2390
+ let rawBodyText = "";
2391
+ let rawBody: unknown;
2392
+ let operationId: string | undefined;
2393
+ const operation = "stateful-internal";
2394
+ const requestCost = startRequestCost();
2395
+ try {
2396
+ if (!options.internalOperationExecutor) {
2397
+ throw new ProviderError("Stateful internal operation executor is not configured.", {
2398
+ code: "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
2399
+ });
2400
+ }
2401
+ rawBodyText = await c.req.raw.clone().text();
2402
+ verifyStatefulForwardingRequest({
2403
+ options,
2404
+ rawBody: rawBodyText,
2405
+ headers: c.req.raw.headers,
2406
+ method: c.req.raw.method,
2407
+ path: STATEFUL_INTERNAL_OPERATIONS_ROUTE,
2408
+ replayCache: statefulForwardingReplayCache,
2409
+ });
2410
+ try {
2411
+ rawBody = JSON.parse(rawBodyText);
2412
+ } catch {
2413
+ throw new ProviderError("Stateful forwarding envelope is not valid JSON.", {
2414
+ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
2415
+ });
2416
+ }
2417
+ const envelope = parseStatefulForwardingEnvelope(rawBody);
2418
+ if (envelope.providerId !== provider.id) {
2419
+ throw new ProviderError(
2420
+ "Stateful forwarding envelope providerId does not match the served provider.",
2421
+ { code: "STATEFUL_FORWARDING_PROVIDER_MISMATCH" },
2422
+ );
2423
+ }
2424
+ if (envelope.requestId !== envelope.operationRequest.requestId) {
2425
+ throw new ProviderError("Stateful forwarding requestId values do not match.", {
2426
+ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
2427
+ });
2428
+ }
2429
+ if (
2430
+ envelope.sourcePodId !==
2431
+ (c.req.raw.headers.get(STATEFUL_FORWARDING_SOURCE_POD_HEADER) ?? "")
2432
+ ) {
2433
+ throw new ProviderError("Stateful forwarding source pod does not match its header.", {
2434
+ code: "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
2435
+ });
2436
+ }
2437
+ if (
2438
+ envelope.forwardedAt !== (c.req.raw.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "")
2439
+ ) {
2440
+ throw new ProviderError(
2441
+ "Stateful forwarding forwardedAt does not match its signature timestamp.",
2442
+ { code: "STATEFUL_FORWARDING_ENVELOPE_INVALID" },
2443
+ );
2444
+ }
2445
+ const deadlineAtMs = envelope.deadlineAt ? Date.parse(envelope.deadlineAt) : undefined;
2446
+ if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
2447
+ throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
2448
+ }
2449
+ const remainingDeadlineMs =
2450
+ deadlineAtMs === undefined ? undefined : deadlineAtMs - Date.now();
2451
+ const deadlineSignal =
2452
+ remainingDeadlineMs === undefined ? undefined : AbortSignal.timeout(remainingDeadlineMs);
2453
+ const signal = deadlineSignal
2454
+ ? AbortSignal.any([c.req.raw.signal, deadlineSignal])
2455
+ : c.req.raw.signal;
2456
+ const ownerFenceValidation = Promise.resolve(
2457
+ options.statefulForwarding?.validateOwnerFence(
2458
+ {
2459
+ providerId: envelope.providerId,
2460
+ sessionKey: envelope.sessionKey,
2461
+ ownerPodId: envelope.ownerPodId,
2462
+ generation: envelope.generation,
2463
+ sourcePodId: envelope.sourcePodId,
2464
+ forwardedAt: envelope.forwardedAt,
2465
+ requestId: envelope.requestId,
2466
+ ...(envelope.idempotencyKey ? { idempotencyKey: envelope.idempotencyKey } : {}),
2467
+ },
2468
+ signal,
2469
+ ),
2470
+ );
2471
+ let ownerFenceValid: boolean | undefined;
2472
+ try {
2473
+ ownerFenceValid = deadlineSignal
2474
+ ? await Promise.race([
2475
+ ownerFenceValidation,
2476
+ new Promise<never>((_resolve, reject) => {
2477
+ deadlineSignal.addEventListener(
2478
+ "abort",
2479
+ () =>
2480
+ reject(
2481
+ new StatefulRoutingDeadlineError(
2482
+ envelope.requestId,
2483
+ envelope.deadlineAt as string,
2484
+ ),
2485
+ ),
2486
+ { once: true },
2487
+ );
2488
+ }),
2489
+ ])
2490
+ : await ownerFenceValidation;
2491
+ } catch (error) {
2492
+ if (deadlineSignal?.aborted) {
2493
+ throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
2494
+ }
2495
+ throw error;
2496
+ }
2497
+ if (ownerFenceValid !== true) {
2498
+ throw new ProviderError("Stateful forwarding owner fence is no longer current.", {
2499
+ code: "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
2500
+ });
2501
+ }
2502
+ const request = operationRequestFromForwardingEnvelope(envelope);
2503
+ operationId = envelope.operationId;
2504
+ const ctx = createProviderContext(
2505
+ provider,
2506
+ request,
2507
+ operationId,
2508
+ options,
2509
+ state,
2510
+ undefined,
2511
+ signal,
2512
+ );
2513
+ if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
2514
+ throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
2515
+ }
2516
+ const output = await options.internalOperationExecutor({
2517
+ provider,
2518
+ operationId,
2519
+ ctx,
2520
+ request,
2521
+ internalStatefulForward: envelope,
2522
+ signal,
2523
+ });
2524
+ logProviderSuccess(
2525
+ logger,
2526
+ provider,
2527
+ "operation",
2528
+ operationId || operation,
2529
+ request.requestId,
2530
+ 200,
2531
+ finishRequestCost(requestCost),
2532
+ );
2533
+ return c.json({ data: output });
2534
+ } catch (error) {
2535
+ const declaredErrorCode = declaredErrorCodeFor(error, operationId, operationErrorCodes);
2536
+ const status = toStatusCode(error, declaredErrorCode);
2537
+ if (isProviderError(error) && error.code === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
2538
+ c.header("Retry-After", String(STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS));
2539
+ }
2540
+ const requestId = extractRequestId(rawBody);
2541
+ logProviderError(
2542
+ logger,
2543
+ provider,
2544
+ "operation",
2545
+ operationId || operation,
2546
+ requestId,
2547
+ error,
2548
+ status,
2549
+ finishRequestCost(requestCost),
2550
+ declaredErrorCode,
2551
+ );
2552
+ return responseWithErrorObservability(
2553
+ c.json(toErrorResponse(error, requestId, declaredErrorCode), status),
2554
+ error,
2555
+ declaredErrorCode,
2556
+ );
2557
+ }
2558
+ });
2559
+
2560
+ app.post("/v1/:operation", async (c) => {
2561
+ let rawBody: unknown;
2562
+ const operation = c.req.param("operation");
2563
+ const proxyTelemetry = new ProxyTelemetryCollector();
2564
+ const requestCost = startRequestCost();
2565
+ try {
2566
+ rawBody = await c.req.raw
2567
+ .clone()
2568
+ .json()
2569
+ .catch(() => undefined);
2570
+ const body = OperationRequestSchema.parse(rawBody);
2571
+ const requestHeaders = Object.fromEntries(c.req.raw.headers.entries());
2572
+ body.headers = { ...requestHeaders, ...body.headers };
2573
+ const response = await handleOperation(
2574
+ provider,
2575
+ body,
2576
+ operation,
2577
+ options,
2578
+ state,
2579
+ proxyTelemetry,
2580
+ c.req.raw.signal,
2581
+ );
2582
+ if (response instanceof Response) {
2583
+ logProviderSuccess(
2584
+ logger,
2585
+ provider,
2586
+ "operation",
2587
+ operation,
2588
+ body.requestId,
2589
+ response.status,
2590
+ finishRequestCost(requestCost),
2591
+ proxyTelemetry,
2592
+ );
2593
+ return responseWithProviderTelemetry(response, proxyTelemetry);
2594
+ }
2595
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
2596
+ if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
2597
+ logProviderSuccess(
2598
+ logger,
2599
+ provider,
2600
+ "operation",
2601
+ operation,
2602
+ body.requestId,
2603
+ 200,
2604
+ finishRequestCost(requestCost),
2605
+ proxyTelemetry,
2606
+ );
2607
+ return c.json(response);
2608
+ } catch (error) {
2609
+ const declaredErrorCode = declaredErrorCodeFor(error, operation, operationErrorCodes);
2610
+ const status = toStatusCode(error, declaredErrorCode);
2611
+ const requestId = extractRequestId(rawBody);
2612
+ logProviderError(
2613
+ logger,
2614
+ provider,
2615
+ "operation",
2616
+ operation,
2617
+ requestId,
2618
+ error,
2619
+ status,
2620
+ finishRequestCost(requestCost),
2621
+ declaredErrorCode,
2622
+ proxyTelemetry,
2623
+ );
2624
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
2625
+ if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
2626
+ return responseWithErrorObservability(
2627
+ c.json(toErrorResponse(error, requestId, declaredErrorCode), status),
2628
+ error,
2629
+ declaredErrorCode,
2630
+ );
2631
+ }
2632
+ });
2633
+
2634
+ app.post("/auth/start", async (c) => {
2635
+ let rawBody: unknown;
2636
+ const proxyTelemetry = new ProxyTelemetryCollector();
2637
+ const requestCost = startRequestCost();
2638
+ try {
2639
+ rawBody = await c.req.raw
2640
+ .clone()
2641
+ .json()
2642
+ .catch(() => undefined);
2643
+ const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
2644
+ const response = await handleAuthFlow(
2645
+ provider,
2646
+ body,
2647
+ "start",
2648
+ options,
2649
+ state,
2650
+ proxyTelemetry,
2651
+ c.req.raw.signal,
2652
+ );
2653
+ logProviderSuccess(
2654
+ logger,
2655
+ provider,
2656
+ "auth",
2657
+ "start",
2658
+ body.requestId,
2659
+ response instanceof Response ? response.status : 200,
2660
+ finishRequestCost(requestCost),
2661
+ proxyTelemetry,
2662
+ );
2663
+ if (response instanceof Response)
2664
+ return responseWithProviderTelemetry(response, proxyTelemetry);
2665
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
2666
+ if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
2667
+ return c.json(response);
2668
+ } catch (error) {
2669
+ const status = toStatusCode(error);
2670
+ const requestId = extractRequestId(rawBody);
2671
+ logProviderError(
2672
+ logger,
2673
+ provider,
2674
+ "auth",
2675
+ "start",
2676
+ requestId,
2677
+ error,
2678
+ status,
2679
+ finishRequestCost(requestCost),
2680
+ undefined,
2681
+ proxyTelemetry,
2682
+ );
2683
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
2684
+ if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
2685
+ return responseWithErrorObservability(
2686
+ c.json(toErrorResponse(error, requestId), status),
2687
+ error,
2688
+ );
2689
+ }
2690
+ });
2691
+
2692
+ app.post("/auth/continue", async (c) => {
2693
+ let rawBody: unknown;
2694
+ const proxyTelemetry = new ProxyTelemetryCollector();
2695
+ const requestCost = startRequestCost();
2696
+ try {
2697
+ rawBody = await c.req.raw
2698
+ .clone()
2699
+ .json()
2700
+ .catch(() => undefined);
2701
+ const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
2702
+ const response = await handleAuthFlow(
2703
+ provider,
2704
+ body,
2705
+ "continue",
2706
+ options,
2707
+ state,
2708
+ proxyTelemetry,
2709
+ c.req.raw.signal,
2710
+ );
2711
+ logProviderSuccess(
2712
+ logger,
2713
+ provider,
2714
+ "auth",
2715
+ "continue",
2716
+ body.requestId,
2717
+ response instanceof Response ? response.status : 200,
2718
+ finishRequestCost(requestCost),
2719
+ proxyTelemetry,
2720
+ );
2721
+ if (response instanceof Response)
2722
+ return responseWithProviderTelemetry(response, proxyTelemetry);
2723
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
2724
+ if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
2725
+ return c.json(response);
2726
+ } catch (error) {
2727
+ const status = toStatusCode(error);
2728
+ const requestId = extractRequestId(rawBody);
2729
+ logProviderError(
2730
+ logger,
2731
+ provider,
2732
+ "auth",
2733
+ "continue",
2734
+ requestId,
2735
+ error,
2736
+ status,
2737
+ finishRequestCost(requestCost),
2738
+ undefined,
2739
+ proxyTelemetry,
2740
+ );
2741
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
2742
+ if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
2743
+ return responseWithErrorObservability(
2744
+ c.json(toErrorResponse(error, requestId), status),
2745
+ error,
2746
+ );
2747
+ }
2748
+ });
2749
+
2750
+ app.post("/auth/poll", async (c) => {
2751
+ let rawBody: unknown;
2752
+ const proxyTelemetry = new ProxyTelemetryCollector();
2753
+ const requestCost = startRequestCost();
2754
+ try {
2755
+ rawBody = await c.req.raw
2756
+ .clone()
2757
+ .json()
2758
+ .catch(() => undefined);
2759
+ const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
2760
+ const response = await handleAuthFlow(
2761
+ provider,
2762
+ body,
2763
+ "poll",
2764
+ options,
2765
+ state,
2766
+ proxyTelemetry,
2767
+ c.req.raw.signal,
2768
+ );
2769
+ logProviderSuccess(
2770
+ logger,
2771
+ provider,
2772
+ "auth",
2773
+ "poll",
2774
+ body.requestId,
2775
+ response instanceof Response ? response.status : 200,
2776
+ finishRequestCost(requestCost),
2777
+ proxyTelemetry,
2778
+ );
2779
+ if (response instanceof Response)
2780
+ return responseWithProviderTelemetry(response, proxyTelemetry);
2781
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
2782
+ if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
2783
+ return c.json(response);
2784
+ } catch (error) {
2785
+ const status = toStatusCode(error);
2786
+ const requestId = extractRequestId(rawBody);
2787
+ logProviderError(
2788
+ logger,
2789
+ provider,
2790
+ "auth",
2791
+ "poll",
2792
+ requestId,
2793
+ error,
2794
+ status,
2795
+ finishRequestCost(requestCost),
2796
+ undefined,
2797
+ proxyTelemetry,
2798
+ );
2799
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
2800
+ if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
2801
+ return responseWithErrorObservability(
2802
+ c.json(toErrorResponse(error, requestId), status),
2803
+ error,
2804
+ );
2805
+ }
2806
+ });
2807
+
2808
+ app.post("/auth/refresh", async (c) => {
2809
+ let rawBody: unknown;
2810
+ const proxyTelemetry = new ProxyTelemetryCollector();
2811
+ const requestCost = startRequestCost();
2812
+ try {
2813
+ rawBody = await c.req.raw
2814
+ .clone()
2815
+ .json()
2816
+ .catch(() => undefined);
2817
+ const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
2818
+ const response = await handleAuthFlow(
2819
+ provider,
2820
+ body,
2821
+ "refresh",
2822
+ options,
2823
+ state,
2824
+ proxyTelemetry,
2825
+ c.req.raw.signal,
2826
+ );
2827
+ logProviderSuccess(
2828
+ logger,
2829
+ provider,
2830
+ "auth",
2831
+ "refresh",
2832
+ body.requestId,
2833
+ response instanceof Response ? response.status : 200,
2834
+ finishRequestCost(requestCost),
2835
+ proxyTelemetry,
2836
+ );
2837
+ if (response instanceof Response)
2838
+ return responseWithProviderTelemetry(response, proxyTelemetry);
2839
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
2840
+ if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
2841
+ return c.json(response);
2842
+ } catch (error) {
2843
+ const status = toStatusCode(error);
2844
+ const requestId = extractRequestId(rawBody);
2845
+ logProviderError(
2846
+ logger,
2847
+ provider,
2848
+ "auth",
2849
+ "refresh",
2850
+ requestId,
2851
+ error,
2852
+ status,
2853
+ finishRequestCost(requestCost),
2854
+ undefined,
2855
+ proxyTelemetry,
2856
+ );
2857
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
2858
+ if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
2859
+ return responseWithErrorObservability(
2860
+ c.json(toErrorResponse(error, requestId), status),
2861
+ error,
2862
+ );
2863
+ }
2864
+ });
2865
+
2866
+ app.post("/auth/disconnect", async (c) => {
2867
+ let rawBody: unknown;
2868
+ const proxyTelemetry = new ProxyTelemetryCollector();
2869
+ const requestCost = startRequestCost();
2870
+ try {
2871
+ rawBody = await c.req.raw
2872
+ .clone()
2873
+ .json()
2874
+ .catch(() => undefined);
2875
+ const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
2876
+ const response = await handleAuthFlow(
2877
+ provider,
2878
+ body,
2879
+ "abort",
2880
+ options,
2881
+ state,
2882
+ proxyTelemetry,
2883
+ c.req.raw.signal,
2884
+ );
2885
+ logProviderSuccess(
2886
+ logger,
2887
+ provider,
2888
+ "auth",
2889
+ "disconnect",
2890
+ body.requestId,
2891
+ response instanceof Response ? response.status : 200,
2892
+ finishRequestCost(requestCost),
2893
+ proxyTelemetry,
2894
+ );
2895
+ if (response instanceof Response)
2896
+ return responseWithProviderTelemetry(response, proxyTelemetry);
2897
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
2898
+ if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
2899
+ return c.json(response);
2900
+ } catch (error) {
2901
+ const status = toStatusCode(error);
2902
+ const requestId = extractRequestId(rawBody);
2903
+ logProviderError(
2904
+ logger,
2905
+ provider,
2906
+ "auth",
2907
+ "disconnect",
2908
+ requestId,
2909
+ error,
2910
+ status,
2911
+ finishRequestCost(requestCost),
2912
+ undefined,
2913
+ proxyTelemetry,
2914
+ );
2915
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
2916
+ if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
2917
+ return responseWithErrorObservability(
2918
+ c.json(toErrorResponse(error, requestId), status),
2919
+ error,
2920
+ );
2921
+ }
2922
+ });
2923
+
2924
+ return app;
2925
+ }
2926
+
2927
+ function validateStatefulServerConfig(options: ProviderServerOptions): void {
2928
+ if (options.statefulForwarding && !options.internalOperationExecutor) {
2929
+ throw new Error(
2930
+ "Invalid provider server configuration: statefulForwarding requires internalOperationExecutor; missing option internalOperationExecutor.",
2931
+ );
2932
+ }
2933
+ if (options.internalOperationExecutor && !options.statefulForwarding?.secret) {
2934
+ throw new Error(
2935
+ "Invalid provider server configuration: internalOperationExecutor requires statefulForwarding.secret; missing option statefulForwarding.secret.",
2936
+ );
2937
+ }
2938
+ if (
2939
+ options.statefulForwarding &&
2940
+ typeof options.statefulForwarding.validateOwnerFence !== "function"
2941
+ ) {
2942
+ throw new Error(
2943
+ "Invalid provider server configuration: statefulForwarding requires validateOwnerFence.",
2944
+ );
2945
+ }
2946
+ if (
2947
+ options.statefulForwarding?.maxSkewMs !== undefined &&
2948
+ (!Number.isFinite(options.statefulForwarding.maxSkewMs) ||
2949
+ options.statefulForwarding.maxSkewMs <= 0)
2950
+ ) {
2951
+ throw new Error("Invalid provider server configuration: maxSkewMs must be positive.");
2952
+ }
2953
+ if (
2954
+ options.statefulForwarding?.replayCacheMaxEntries !== undefined &&
2955
+ (!Number.isInteger(options.statefulForwarding.replayCacheMaxEntries) ||
2956
+ options.statefulForwarding.replayCacheMaxEntries <= 0)
2957
+ ) {
2958
+ throw new Error(
2959
+ "Invalid provider server configuration: replayCacheMaxEntries must be a positive integer.",
2960
+ );
2961
+ }
2962
+ }
2963
+
2964
+ type BunServeRuntime = {
2965
+ serve: (options: {
2966
+ port: number;
2967
+ hostname: string;
2968
+ fetch: (request: Request) => Response | Promise<Response>;
2969
+ }) => BunServerHandle;
2970
+ };
2971
+
2972
+ type BunServerHandle = {
2973
+ readonly port: number;
2974
+ stop(closeActiveConnections?: boolean): Promise<void>;
2975
+ };
2976
+
2977
+ function getBunServeRuntime(): BunServeRuntime | undefined {
2978
+ const bunValue = Object.getOwnPropertyDescriptor(globalThis, "Bun")?.value;
2979
+ if (!bunValue || typeof bunValue !== "object") {
2980
+ return undefined;
2981
+ }
2982
+
2983
+ const serve = Object.getOwnPropertyDescriptor(bunValue, "serve")?.value;
2984
+ if (typeof serve !== "function") {
2985
+ return undefined;
2986
+ }
2987
+
2988
+ return {
2989
+ serve(options) {
2990
+ return serve(options) as BunServerHandle;
2991
+ },
2992
+ };
2993
+ }
2994
+
2995
+ export type ProviderServerCloseOptions = {
2996
+ readonly timeoutMs?: number;
2997
+ };
2998
+
2999
+ export type ProviderServerHandle = {
3000
+ readonly port: number;
3001
+ close(options?: ProviderServerCloseOptions): Promise<void>;
3002
+ };
3003
+
3004
+ export interface ServeOptions extends ProviderServerOptions {
3005
+ host?: string;
3006
+ port?: number;
3007
+ /**
3008
+ * Port for the internal self-test listener (default 3001 or
3009
+ * APIFUSE__PROVIDER_RUNTIME__SELF_TEST_PORT). The listener only starts
3010
+ * when APIFUSE__PROVIDER_RUNTIME__SELF_TEST_MASTER_SECRET is present.
3011
+ */
3012
+ selfTestPort?: number;
3013
+ }
3014
+
3015
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
3016
+ const DEFAULT_SHUTDOWN_SIGNALS: NodeJS.Signals[] = ["SIGTERM", "SIGINT"];
3017
+
3018
+ type SignalServerRegistration = {
3019
+ readonly signals: ReadonlySet<NodeJS.Signals>;
3020
+ readonly close: () => Promise<void>;
3021
+ };
3022
+
3023
+ type ProcessSignalCoordinator = {
3024
+ readonly registrations: Set<SignalServerRegistration>;
3025
+ readonly listener: () => void;
3026
+ handling: boolean;
3027
+ };
3028
+
3029
+ const processSignalCoordinators = new Map<NodeJS.Signals, ProcessSignalCoordinator>();
3030
+
3031
+ export async function serve(
3032
+ provider: ProviderDefinition,
3033
+ options: ServeOptions = {},
3034
+ ): Promise<ProviderServerHandle> {
3035
+ const bunRuntime = getBunServeRuntime();
3036
+
3037
+ if (bunRuntime === undefined) {
3038
+ throw new ProviderError("Bun runtime is required to start the provider server", {
3039
+ code: "RUNTIME_UNSUPPORTED",
3040
+ });
3041
+ }
3042
+ const logger = options.logger ?? defaultProviderServerLogger;
3043
+ const configuredTimeoutMs = shutdownTimeout(
3044
+ options.shutdown?.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS,
3045
+ );
3046
+ const configuredSignals = resolveShutdownSignals(options.shutdown?.signals ?? true);
3047
+ const selfTestSecrets = resolveSelfTestMasterSecrets();
3048
+ const serverAppOptions: ProviderServerOptions = {
3049
+ logger: options.logger,
3050
+ ocr: options.ocr,
3051
+ stt: options.stt,
3052
+ resolver: options.resolver,
3053
+ state: options.state,
3054
+ allowMemoryStateFallback: options.allowMemoryStateFallback,
3055
+ operationExecutor: options.operationExecutor,
3056
+ internalOperationExecutor: options.internalOperationExecutor,
3057
+ statefulForwarding: options.statefulForwarding,
3058
+ };
3059
+ const [app, selfTestModule] = await Promise.all([
3060
+ createServerAppAsync(provider, serverAppOptions),
3061
+ selfTestSecrets ? import("./self-test.js") : undefined,
3062
+ ]);
3063
+
3064
+ const servers: BunServerHandle[] = [];
3065
+ try {
3066
+ servers.push(
3067
+ bunRuntime.serve({
3068
+ port: options.port ?? DEFAULT_PORT,
3069
+ hostname: options.host ?? DEFAULT_HOST,
3070
+ fetch: app.fetch,
3071
+ }),
3072
+ );
3073
+
3074
+ // Internal self-test listener (health dependency inversion): a SEPARATE
3075
+ // socket the tenant-facing gateway never dials. Off by default — it only
3076
+ // starts when the shared self-test master secret env is present.
3077
+ if (selfTestSecrets && selfTestModule) {
3078
+ const selfTestApp = selfTestModule.createSelfTestApp(provider, {
3079
+ secrets: selfTestSecrets,
3080
+ invoke: selfTestModule.createSelfTestInvoke(app),
3081
+ authFlow: selfTestModule.createSelfTestAuthFlowInvoke(app),
3082
+ logger,
3083
+ });
3084
+ servers.push(
3085
+ bunRuntime.serve({
3086
+ port: options.selfTestPort ?? selfTestModule.resolveSelfTestPort(),
3087
+ hostname: options.host ?? DEFAULT_HOST,
3088
+ fetch: selfTestApp.fetch,
3089
+ }),
3090
+ );
3091
+ }
3092
+ } catch (error) {
3093
+ await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
3094
+ throw error;
3095
+ }
3096
+
3097
+ const server = servers[0];
3098
+ if (!server) throw new Error("Provider server failed to create its primary listener.");
3099
+ let closePromise: Promise<void> | undefined;
3100
+ let unregisterSignals = () => {};
3101
+
3102
+ const close = (closeOptions: ProviderServerCloseOptions = {}): Promise<void> => {
3103
+ if (closePromise) return closePromise;
3104
+ const timeoutMs = shutdownTimeout(closeOptions.timeoutMs ?? configuredTimeoutMs);
3105
+ closePromise = closeProviderServers({
3106
+ servers,
3107
+ hooks: options.shutdown?.hooks ?? [],
3108
+ timeoutMs,
3109
+ logger,
3110
+ providerId: provider.id,
3111
+ }).finally(() => unregisterSignals());
3112
+ return closePromise;
3113
+ };
3114
+
3115
+ try {
3116
+ unregisterSignals = registerForProcessSignals(configuredSignals, () =>
3117
+ close({ timeoutMs: configuredTimeoutMs }),
3118
+ );
3119
+ } catch (error) {
3120
+ unregisterSignals();
3121
+ await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
3122
+ throw error;
3123
+ }
3124
+
3125
+ return { port: server.port, close };
3126
+ }
3127
+
3128
+ function registerForProcessSignals(
3129
+ signals: NodeJS.Signals[],
3130
+ close: () => Promise<void>,
3131
+ ): () => void {
3132
+ if (signals.length === 0) return () => {};
3133
+ const registration: SignalServerRegistration = {
3134
+ signals: new Set(signals),
3135
+ close,
3136
+ };
3137
+ let registered = true;
3138
+ const unregister = () => {
3139
+ if (!registered) return;
3140
+ registered = false;
3141
+ for (const signal of registration.signals) {
3142
+ const coordinator = processSignalCoordinators.get(signal);
3143
+ if (!coordinator) continue;
3144
+ coordinator.registrations.delete(registration);
3145
+ if (coordinator.registrations.size === 0 && !coordinator.handling) {
3146
+ process.removeListener(signal, coordinator.listener);
3147
+ processSignalCoordinators.delete(signal);
3148
+ }
3149
+ }
3150
+ };
3151
+ try {
3152
+ for (const signal of signals) {
3153
+ let coordinator = processSignalCoordinators.get(signal);
3154
+ if (!coordinator) {
3155
+ const created: ProcessSignalCoordinator = {
3156
+ registrations: new Set(),
3157
+ handling: false,
3158
+ listener: () => handleCoordinatedSignal(signal, created),
3159
+ };
3160
+ coordinator = created;
3161
+ processSignalCoordinators.set(signal, coordinator);
3162
+ process.on(signal, coordinator.listener);
3163
+ }
3164
+ coordinator.registrations.add(registration);
3165
+ }
3166
+ } catch (error) {
3167
+ unregister();
3168
+ throw error;
3169
+ }
3170
+ return unregister;
3171
+ }
3172
+
3173
+ function handleCoordinatedSignal(
3174
+ signal: NodeJS.Signals,
3175
+ coordinator: ProcessSignalCoordinator,
3176
+ ): void {
3177
+ if (coordinator.handling) return;
3178
+ coordinator.handling = true;
3179
+ const registrations = [...coordinator.registrations];
3180
+ void Promise.allSettled(registrations.map((registration) => registration.close())).finally(() => {
3181
+ if (processSignalCoordinators.get(signal) === coordinator) {
3182
+ process.removeListener(signal, coordinator.listener);
3183
+ processSignalCoordinators.delete(signal);
3184
+ }
3185
+ try {
3186
+ process.kill(process.pid, signal);
3187
+ } catch {
3188
+ process.exitCode = 1;
3189
+ }
3190
+ });
3191
+ }
3192
+
3193
+ async function closeProviderServers(input: {
3194
+ readonly servers: BunServerHandle[];
3195
+ readonly hooks: Array<() => Promise<void>>;
3196
+ readonly timeoutMs: number;
3197
+ readonly logger: ProviderServerLogger;
3198
+ readonly providerId: string;
3199
+ }): Promise<void> {
3200
+ const deadline = Date.now() + input.timeoutMs;
3201
+ const gracefulStops = input.servers.map((server) => server.stop(false));
3202
+ for (const gracefulStop of gracefulStops) gracefulStop.catch(() => undefined);
3203
+ for (const [hookIndex, hook] of input.hooks.entries()) {
3204
+ try {
3205
+ await withinShutdownBudget(Promise.resolve().then(hook), deadline);
3206
+ } catch (error) {
3207
+ try {
3208
+ input.logger({
3209
+ level: "error",
3210
+ event: "provider_shutdown_hook_failed",
3211
+ providerId: input.providerId,
3212
+ hookIndex,
3213
+ errorClass: error instanceof Error ? error.name : "UnknownError",
3214
+ message: error instanceof Error ? error.message : "Shutdown hook failed.",
3215
+ });
3216
+ } catch {}
3217
+ }
3218
+ }
3219
+ const forcedStops = input.servers.map((server) => server.stop(true));
3220
+ await withinShutdownBudget(
3221
+ Promise.allSettled([...gracefulStops, ...forcedStops]).then(() => undefined),
3222
+ deadline,
3223
+ ).catch(() => undefined);
3224
+ }
3225
+
3226
+ async function withinShutdownBudget<T>(promise: Promise<T>, deadline: number): Promise<T> {
3227
+ promise.catch(() => undefined);
3228
+ const remainingMs = Math.max(0, deadline - Date.now());
3229
+ let timer: ReturnType<typeof setTimeout> | undefined;
3230
+ const timeout = new Promise<never>((_resolve, reject) => {
3231
+ timer = setTimeout(() => reject(new Error("Provider server shutdown timed out.")), remainingMs);
3232
+ });
3233
+ try {
3234
+ return await Promise.race([promise, timeout]);
3235
+ } finally {
3236
+ if (timer) clearTimeout(timer);
3237
+ }
3238
+ }
3239
+
3240
+ function resolveShutdownSignals(signals: boolean | NodeJS.Signals[]): NodeJS.Signals[] {
3241
+ if (signals === false) return [];
3242
+ return [...new Set(signals === true ? DEFAULT_SHUTDOWN_SIGNALS : signals)];
3243
+ }
3244
+
3245
+ function shutdownTimeout(value: number): number {
3246
+ if (!Number.isFinite(value) || value < 0) {
3247
+ throw new Error("Provider server shutdown timeoutMs must be a non-negative finite number.");
3248
+ }
3249
+ return value;
3250
+ }