@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,2173 @@
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
+ import { Hono } from "hono";
6
+ import { z } from "zod";
7
+ import { AuthAbortError, createAuthFlowHelpers } from "../auth.js";
8
+ import { validateFailClosedDeclaration } from "../declaration-validation.js";
9
+ import { SDK_OWNED_PROVIDER_ERROR_CODES, SDK_RUNTIME_OWNED_ERROR_CODES, SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES, } from "../error-resolution.js";
10
+ import { AuthError, isProviderError, isSessionExpiredError, isTransportError, isValidationError, ProviderError, } from "../errors.js";
11
+ import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.js";
12
+ import { categoryForStatus, sourceForCategory, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
13
+ import { createScratchpad } from "../runtime/auth-flow.js";
14
+ import { createProviderCache } from "../runtime/cache.js";
15
+ import { createProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "../runtime/choice.js";
16
+ import { createCredentialContext } from "../runtime/credential.js";
17
+ import { createEnvContext } from "../runtime/env.js";
18
+ import { executeOperation } from "../runtime/executor.js";
19
+ import { createHttpClient } from "../runtime/http.js";
20
+ import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
21
+ import { getProviderBaseUrl } from "../runtime/provider.js";
22
+ import { createOcrClientFromEnv } from "../runtime/ocr.js";
23
+ import { PROXY_AUTH_IP_DENIED_CODE, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_EXHAUSTED_CODE, } from "../runtime/proxy-errors.js";
24
+ import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector, } from "../runtime/proxy-telemetry.js";
25
+ import { createUnsupportedResolverClient } from "../runtime/resolver-shared.js";
26
+ import { assertRequiredSecretsPresent, listMissingRequiredSecrets, MISSING_SECRET_CODE, } from "../runtime/secrets.js";
27
+ import { createProviderRuntimeStateFromEnv, createUnsupportedProviderRuntimeState, } from "../runtime/state.js";
28
+ import { StealthCookieJar } from "../runtime/stealth-cookies.js";
29
+ import { createSttClientFromEnv } from "../runtime/stt.js";
30
+ import { createTraceContext } from "../runtime/trace.js";
31
+ import { resolveTraceConfigFromEnv } from "../runtime/trace-config.js";
32
+ import { parseSchema } from "../schema.js";
33
+ import { STATEFUL_NONCE_HEADER as STATEFUL_FORWARDING_NONCE_HEADER, STATEFUL_SIGNATURE_HEADER as STATEFUL_FORWARDING_SIGNATURE_HEADER, STATEFUL_TIMESTAMP_HEADER as STATEFUL_FORWARDING_TIMESTAMP_HEADER, verifyStatefulRequestSignature, } from "../stateful-signing.js";
34
+ import { StatefulRoutingDeadlineError } from "../stateful/errors.js";
35
+ import { getStealthProfile } from "../stealth/profiles.js";
36
+ import { APIFUSE_STREAM_DONE_EVENT, APIFUSE_STREAM_ERROR_EVENT, encodeSseEvent, error as streamError, } from "../stream.js";
37
+ import { VALID_OPERATION_ERROR_STATUSES } from "../types.js";
38
+ import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
39
+ import { resolveServerTraceContextOptions } from "./trace-output.js";
40
+ import { AuthFlowRequestSchema, OperationConnectionSchema, OperationRequestSchema, } from "./types.js";
41
+ const DEFAULT_HOST = "0.0.0.0";
42
+ const DEFAULT_PORT = 3000;
43
+ /** Compact SDK-owned error classification emitted separately from the public response body. */
44
+ export const ERROR_OBSERVABILITY_HEADER = "X-ApiFuse-Error-Observability";
45
+ const AUTH_FLOW_LOCALES = ["en", "ko", "ja"];
46
+ const retryResponseMeta = new WeakMap();
47
+ const STATEFUL_INTERNAL_OPERATIONS_ROUTE = "/__apifuse/stateful/operations";
48
+ const STATEFUL_FORWARDING_SOURCE_POD_HEADER = "x-apifuse-stateful-source-pod";
49
+ const DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS = 5 * 60_000;
50
+ const DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES = 10_000;
51
+ const STATEFUL_FORWARDING_REPLAY_BUCKET_MS = 10_000;
52
+ const STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS = Math.ceil(STATEFUL_FORWARDING_REPLAY_BUCKET_MS / 1_000);
53
+ export const ProviderServerStatefulForwardEnvelopeSchema = z
54
+ .object({
55
+ requestId: z.string().min(1),
56
+ providerId: z.string().min(1),
57
+ operationId: z.string().min(1),
58
+ sessionKey: z.string().min(1),
59
+ connectionId: z.string().min(1),
60
+ serviceAccountId: z.string().min(1),
61
+ ownerPodId: z.string().min(1),
62
+ generation: z.number().int().positive(),
63
+ sourcePodId: z.string().min(1),
64
+ forwardedAt: z.string().refine((value) => Number.isFinite(Date.parse(value))),
65
+ deadlineAt: z
66
+ .string()
67
+ .refine((value) => Number.isFinite(Date.parse(value)))
68
+ .optional(),
69
+ idempotencyKey: z.string().min(1).optional(),
70
+ operationRequest: OperationRequestSchema.extend({
71
+ connection: OperationConnectionSchema.strict().optional(),
72
+ }).strict(),
73
+ })
74
+ .strict();
75
+ const require = createRequire(import.meta.url);
76
+ function normalizedCapabilityCause(error) {
77
+ if (error instanceof Error)
78
+ return error;
79
+ return new Error(String(error));
80
+ }
81
+ function capabilityLoadError(provider, failures) {
82
+ const normalizedFailures = failures.map(({ capability, error }) => ({
83
+ capability,
84
+ cause: normalizedCapabilityCause(error),
85
+ }));
86
+ const summary = normalizedFailures
87
+ .map(({ capability, cause }) => `${capability} (${cause.message})`)
88
+ .join(", ");
89
+ return new ProviderError(`Failed to load declared capabilities for provider "${provider.id}": ${summary}`, {
90
+ code: "PROVIDER_CAPABILITY_LOAD_FAILED",
91
+ details: {
92
+ providerId: provider.id,
93
+ failures: normalizedFailures.map(({ capability, cause }) => ({
94
+ capability,
95
+ reason: cause.message,
96
+ })),
97
+ },
98
+ cause: new AggregateError(normalizedFailures.map(({ cause }) => cause), `Provider capability loading failed for ${provider.id}`),
99
+ });
100
+ }
101
+ function requireCapabilityModule(provider, capability, specifier) {
102
+ try {
103
+ return require(specifier);
104
+ }
105
+ catch (error) {
106
+ if (error && typeof error === "object" && "code" in error && error.code === "ERR_REQUIRE_ESM") {
107
+ throw new ProviderError(`Synchronous capability loading is unavailable for provider "${provider.id}" on this runtime. Use createServerAppAsync() instead of createServerApp().`, {
108
+ code: "PROVIDER_CAPABILITY_SYNC_LOAD_UNSUPPORTED",
109
+ details: { providerId: provider.id, capability },
110
+ cause: normalizedCapabilityCause(error),
111
+ });
112
+ }
113
+ throw capabilityLoadError(provider, [{ capability, error }]);
114
+ }
115
+ }
116
+ function createAuthStub() {
117
+ return {
118
+ async requestField(name) {
119
+ throw new ProviderError(`Auth prompt is unavailable for ${name}`, {
120
+ code: "AUTH_PROMPT_UNAVAILABLE",
121
+ });
122
+ },
123
+ };
124
+ }
125
+ function createBrowserStub() {
126
+ return {
127
+ engine: "playwright-stealth",
128
+ async close() { },
129
+ async newPage() {
130
+ throw new ProviderError("Browser runtime is not available", {
131
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
132
+ });
133
+ },
134
+ async rawPage() {
135
+ throw new ProviderError("Browser runtime is not available", {
136
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
137
+ });
138
+ },
139
+ async withIsolatedContext() {
140
+ throw new ProviderError("Browser runtime is not available", {
141
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
142
+ });
143
+ },
144
+ async solveChallenge() {
145
+ throw new ProviderError("Browser runtime is not available", {
146
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
147
+ });
148
+ },
149
+ };
150
+ }
151
+ function createStealthStub() {
152
+ return {
153
+ async fetch() {
154
+ throw new ProviderError("Stealth runtime is not available", {
155
+ code: "STEALTH_RUNTIME_UNSUPPORTED",
156
+ });
157
+ },
158
+ createSession() {
159
+ throw new ProviderError("Stealth runtime is not available", {
160
+ code: "STEALTH_RUNTIME_UNSUPPORTED",
161
+ });
162
+ },
163
+ close() {
164
+ // no-op
165
+ },
166
+ };
167
+ }
168
+ let stealthRuntimeModulePromise;
169
+ function importStealthRuntime() {
170
+ stealthRuntimeModulePromise ??= import("../runtime/stealth.js");
171
+ return stealthRuntimeModulePromise;
172
+ }
173
+ function createForwardingStealthCookies(initialTarget) {
174
+ let target = initialTarget;
175
+ return {
176
+ setTarget(nextTarget) {
177
+ target = nextTarget;
178
+ },
179
+ get: (...args) => target.get(...args),
180
+ getAll: (...args) => target.getAll(...args),
181
+ has: (...args) => target.has(...args),
182
+ setFromCookieStrings: (...args) => target.setFromCookieStrings(...args),
183
+ toString: (url) => target.toString(url),
184
+ toHeader: (...args) => target.toHeader(...args),
185
+ snapshot: () => target.snapshot(),
186
+ restore: (...args) => target.restore(...args),
187
+ serialize: () => target.serialize(),
188
+ deserialize: (...args) => target.deserialize(...args),
189
+ clear: () => target.clear(),
190
+ find: (...args) => target.find?.(...args),
191
+ };
192
+ }
193
+ function createLazyStealthClient(onCleanupError, ...createArgs) {
194
+ let client;
195
+ let clientPromise;
196
+ function getClient() {
197
+ clientPromise ??= importStealthRuntime().then((runtime) => {
198
+ client ??= runtime.createStealthClient(...createArgs);
199
+ return client;
200
+ });
201
+ return clientPromise;
202
+ }
203
+ return {
204
+ async fetch(...args) {
205
+ return (await getClient()).fetch(...args);
206
+ },
207
+ createSession(options) {
208
+ const localCookies = new StealthCookieJar([], createArgs[0]);
209
+ const cookies = createForwardingStealthCookies(localCookies);
210
+ let closed = false;
211
+ let session;
212
+ let sessionPromise;
213
+ function getSession() {
214
+ sessionPromise ??= getClient().then((realClient) => {
215
+ session = realClient.createSession(options);
216
+ session.cookies.deserialize(localCookies.serialize());
217
+ cookies.setTarget(session.cookies);
218
+ if (closed)
219
+ session.close();
220
+ return session;
221
+ });
222
+ return sessionPromise;
223
+ }
224
+ return {
225
+ cookies,
226
+ async fetch(...args) {
227
+ return (await getSession()).fetch(...args);
228
+ },
229
+ redirects: {
230
+ async run(...args) {
231
+ return (await getSession()).redirects.run(...args);
232
+ },
233
+ },
234
+ close() {
235
+ closed = true;
236
+ session?.close();
237
+ },
238
+ };
239
+ },
240
+ close() {
241
+ client?.close?.();
242
+ if (!client && clientPromise) {
243
+ void clientPromise.then((loadedClient) => {
244
+ try {
245
+ loadedClient.close?.();
246
+ }
247
+ catch (error) {
248
+ try {
249
+ onCleanupError(error);
250
+ }
251
+ catch {
252
+ // A user logger must not turn handled cleanup into a process-level rejection.
253
+ }
254
+ }
255
+ }, () => {
256
+ // The request path owns reporting for a failed lazy import.
257
+ });
258
+ }
259
+ },
260
+ };
261
+ }
262
+ function bindResolverSignalWithoutRuntime(resolver, defaultSignal) {
263
+ if (!defaultSignal)
264
+ return resolver;
265
+ return {
266
+ solve(challenge, signal = defaultSignal) {
267
+ return resolver.solve(challenge, signal);
268
+ },
269
+ };
270
+ }
271
+ function getProviderStealthBaseUrl(provider) {
272
+ const baseUrl = getProviderBaseUrl(provider);
273
+ if (baseUrl) {
274
+ return baseUrl;
275
+ }
276
+ const firstHost = provider.allowedHosts?.[0];
277
+ return firstHost ? `https://${firstHost}` : undefined;
278
+ }
279
+ function getProviderStealthProfile(provider) {
280
+ return provider.stealth?.profile ? getStealthProfile(provider.stealth.profile) : undefined;
281
+ }
282
+ function isProductionProviderBrowserMode(provider, env = process.env) {
283
+ if (provider.runtime !== "browser") {
284
+ return false;
285
+ }
286
+ if (env.APIFUSE__PROVIDER__RUNTIME === "browser") {
287
+ return true;
288
+ }
289
+ return env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id;
290
+ }
291
+ function declaresStealthRuntime(provider) {
292
+ return provider.stealth !== undefined;
293
+ }
294
+ async function loadProviderCapabilityModules(provider) {
295
+ const results = await Promise.allSettled([
296
+ provider.runtime === "browser" ? import("../runtime/browser.js") : Promise.resolve(undefined),
297
+ provider.native ? import("../runtime/native-network.js") : Promise.resolve(undefined),
298
+ provider.resolver ? import("../runtime/resolver.js") : Promise.resolve(undefined),
299
+ declaresStealthRuntime(provider) ? import("../runtime/stealth.js") : Promise.resolve(undefined),
300
+ ]);
301
+ const capabilityNames = ["browser", "native", "resolver", "stealth"];
302
+ const failures = [];
303
+ for (const [index, result] of results.entries()) {
304
+ if (result.status === "rejected") {
305
+ failures.push({ capability: capabilityNames[index], error: result.reason });
306
+ }
307
+ }
308
+ if (failures.length > 0)
309
+ throw capabilityLoadError(provider, failures);
310
+ return {
311
+ browser: results[0].status === "fulfilled" ? results[0].value : undefined,
312
+ nativeNetwork: results[1].status === "fulfilled" ? results[1].value : undefined,
313
+ resolver: results[2].status === "fulfilled" ? results[2].value : undefined,
314
+ stealth: results[3].status === "fulfilled" ? results[3].value : undefined,
315
+ };
316
+ }
317
+ function loadProviderCapabilityModulesSync(provider) {
318
+ return {
319
+ browser: provider.runtime === "browser"
320
+ ? requireCapabilityModule(provider, "browser", "../runtime/browser.js")
321
+ : undefined,
322
+ nativeNetwork: provider.native
323
+ ? requireCapabilityModule(provider, "native", "../runtime/native-network.js")
324
+ : undefined,
325
+ resolver: provider.resolver
326
+ ? requireCapabilityModule(provider, "resolver", "../runtime/resolver.js")
327
+ : undefined,
328
+ stealth: declaresStealthRuntime(provider)
329
+ ? requireCapabilityModule(provider, "stealth", "../runtime/stealth.js")
330
+ : undefined,
331
+ };
332
+ }
333
+ export function resolveProviderProxyAffinityKey(provider, request, operationId) {
334
+ const connectionKey = resolveOperationConnectionId(request) ?? request.connection?.externalRef;
335
+ const affinity = typeof provider.proxy === "object" ? provider.proxy.session?.affinity : undefined;
336
+ if (affinity === "operation") {
337
+ return `${provider.id}/${operationId}`;
338
+ }
339
+ return connectionKey ?? provider.id;
340
+ }
341
+ export function resolveProviderResolverIdentityScope(provider, affinityKey, contextId) {
342
+ return JSON.stringify({
343
+ proxy: provider.proxy ?? null,
344
+ affinityKey,
345
+ contextId,
346
+ });
347
+ }
348
+ function resolveOperationConnectionId(request) {
349
+ // An empty string is a malformed identifier, not an identity: treat it as
350
+ // absent so it can never override a valid id or key a real scope. Requests
351
+ // without any usable id fall back to the documented missing-connection
352
+ // sentinel scope instead of scoping context/affinity/state under "".
353
+ return (normalizeConnectionId(request.connection?.id) ?? normalizeConnectionId(request.connectionId));
354
+ }
355
+ function normalizeConnectionId(id) {
356
+ return id === "" ? undefined : id;
357
+ }
358
+ function resolveNativeProxyPolicy(provider) {
359
+ if (typeof provider.proxy === "object")
360
+ return provider.proxy;
361
+ if (provider.proxy === true)
362
+ return { mode: "optional" };
363
+ if (provider.proxy === false)
364
+ return { mode: "disabled" };
365
+ return undefined;
366
+ }
367
+ function createProviderContext(provider, request, operationId, options, state = createUnsupportedProviderRuntimeState(), proxyTelemetry, signal) {
368
+ const traceConfig = resolveTraceConfigFromEnv();
369
+ const baseUrl = getProviderBaseUrl(provider);
370
+ const stealthBaseUrl = getProviderStealthBaseUrl(provider);
371
+ const stealthProfile = getProviderStealthProfile(provider);
372
+ const proxyPolicy = resolveNativeProxyPolicy(provider);
373
+ const proxyClientOptions = {
374
+ upstream: { proxy: provider.proxy },
375
+ affinityKey: resolveProviderProxyAffinityKey(provider, request, operationId),
376
+ telemetry: proxyTelemetry,
377
+ };
378
+ const resolverIdentityScope = resolveProviderResolverIdentityScope(provider, proxyClientOptions.affinityKey, request.requestId);
379
+ let wrappedContext;
380
+ const stealthClientOptions = {
381
+ upstream: proxyClientOptions.upstream,
382
+ affinityKey: proxyClientOptions.affinityKey,
383
+ telemetry: proxyTelemetry,
384
+ };
385
+ const { capabilityModules } = options;
386
+ const logStealthCleanupError = (error) => logProviderCleanupError(options.logger, provider, "operation", operationId, request.requestId, "stealth", error);
387
+ const env = createEnvContext([
388
+ ...(provider.secrets?.map((secret) => secret.name) ?? []),
389
+ PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
390
+ ]);
391
+ const credential = createCredentialContext({
392
+ allowedKeys: provider.credential?.keys,
393
+ mode: request.connection?.mode,
394
+ scopes: request.connection?.scopes,
395
+ values: request.connection?.secrets,
396
+ });
397
+ const requestContext = {
398
+ connectionId: resolveOperationConnectionId(request),
399
+ headers: request.headers ?? {},
400
+ };
401
+ const requestState = state.forConnection(requestContext.connectionId);
402
+ const cache = createProviderCache({ providerId: provider.id });
403
+ const context = wrapWithInstrumentation({
404
+ env,
405
+ credential,
406
+ request: requestContext,
407
+ http: createHttpClient(baseUrl, {
408
+ ...proxyClientOptions,
409
+ ...(signal ? { signal } : {}),
410
+ onRetrySummary: (summary) => {
411
+ if (summary.attempts <= 1 || !wrappedContext)
412
+ return;
413
+ retryResponseMeta.set(wrappedContext, summary);
414
+ },
415
+ }),
416
+ cache,
417
+ state: requestState,
418
+ stealth: stealthBaseUrl
419
+ ? capabilityModules.stealth
420
+ ? stealthProfile
421
+ ? capabilityModules.stealth.createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
422
+ : capabilityModules.stealth.createStealthClient(stealthBaseUrl, stealthClientOptions)
423
+ : stealthProfile
424
+ ? createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, stealthProfile.name, stealthClientOptions)
425
+ : createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, stealthClientOptions)
426
+ : createStealthStub(),
427
+ browser: provider.runtime === "browser"
428
+ ? capabilityModules.browser.createBrowserClient({
429
+ allowedHosts: provider.allowedHosts,
430
+ cdpUrl: process.env.APIFUSE__CDP_POOL__URL,
431
+ headless: true,
432
+ requireCdpPool: isProductionProviderBrowserMode(provider),
433
+ stealth: true,
434
+ engine: provider.browser?.engine,
435
+ })
436
+ : createBrowserStub(),
437
+ ...(provider.native
438
+ ? {
439
+ native: {
440
+ network: capabilityModules.nativeNetwork.createNativeNetworkClient({
441
+ egress: provider.native.network,
442
+ proxyPolicy: resolveNativeProxyPolicy(provider),
443
+ affinityKey: proxyClientOptions.affinityKey,
444
+ credentials: capabilityModules.nativeNetwork.createEnvVendorCredentialResolver(env),
445
+ }),
446
+ },
447
+ }
448
+ : {}),
449
+ trace: traceConfig
450
+ ? createTraceContext(resolveServerTraceContextOptions(traceConfig, {
451
+ request_id: request.requestId,
452
+ provider_id: provider.id,
453
+ operation_id: operationId,
454
+ }))
455
+ : createTraceContext(),
456
+ auth: createAuthStub(),
457
+ ocr: options.ocr ?? createOcrClientFromEnv(provider.ocr),
458
+ stt: options.stt ?? createSttClientFromEnv(provider.stt),
459
+ resolver: capabilityModules.resolver
460
+ ? capabilityModules.resolver.bindResolverSignal(options.resolver ??
461
+ capabilityModules.resolver.createResolverClientFromEnv(provider.resolver, undefined, {
462
+ allowedHosts: provider.allowedHosts,
463
+ cache,
464
+ identityScope: resolverIdentityScope,
465
+ ...(proxyPolicy
466
+ ? {
467
+ proxyIntent: {
468
+ mode: proxyPolicy.mode,
469
+ ...proxyClientOptions,
470
+ ...(stealthProfile ? { userAgent: stealthProfile.userAgent } : {}),
471
+ },
472
+ }
473
+ : {}),
474
+ }), signal)
475
+ : bindResolverSignalWithoutRuntime(options.resolver ??
476
+ createUnsupportedResolverClient("Provider does not declare resolver capability"), signal),
477
+ choice: createProviderChoiceContext({
478
+ providerId: provider.id,
479
+ env,
480
+ request: requestContext,
481
+ credential,
482
+ state: requestState,
483
+ onTelemetry: (event) => (options.logger ?? defaultProviderServerLogger)({
484
+ level: "info",
485
+ event: "provider_choice_token",
486
+ ...event,
487
+ }),
488
+ }),
489
+ });
490
+ wrappedContext = context;
491
+ return context;
492
+ }
493
+ function createFlowContextStore(allowedKeys, initialContext = {}) {
494
+ const context = createScratchpad(allowedKeys, initialContext);
495
+ return {
496
+ context,
497
+ getPatch() {
498
+ const next = context.toJSON();
499
+ const patch = new Map();
500
+ for (const [key, value] of Object.entries(next)) {
501
+ if (initialContext[key] !== value) {
502
+ patch.set(key, value);
503
+ }
504
+ }
505
+ for (const key of Object.keys(initialContext)) {
506
+ if (!(key in next)) {
507
+ patch.set(key, null);
508
+ }
509
+ }
510
+ if (patch.size === 0) {
511
+ return undefined;
512
+ }
513
+ return Object.fromEntries(patch.entries());
514
+ },
515
+ };
516
+ }
517
+ export function resolveAuthFlowProxyAffinityKey(provider, request) {
518
+ return (resolveOperationConnectionId(request) ??
519
+ request.externalRef ??
520
+ request.tenantId ??
521
+ request.providerId ??
522
+ provider.id);
523
+ }
524
+ function createAuthFlowContext(provider, request, options, state, proxyTelemetry, signal) {
525
+ const baseUrl = getProviderBaseUrl(provider);
526
+ const stealthBaseUrl = getProviderStealthBaseUrl(provider);
527
+ const stealthProfile = getProviderStealthProfile(provider);
528
+ const proxyPolicy = resolveNativeProxyPolicy(provider);
529
+ const contextData = request.context ?? {};
530
+ const flowContextStore = createFlowContextStore(provider.context?.keys ?? Object.keys(contextData), contextData);
531
+ const proxyClientOptions = {
532
+ upstream: { proxy: provider.proxy },
533
+ affinityKey: resolveAuthFlowProxyAffinityKey(provider, request),
534
+ telemetry: proxyTelemetry,
535
+ };
536
+ const resolverIdentityScope = resolveProviderResolverIdentityScope(provider, proxyClientOptions.affinityKey, request.requestId);
537
+ const stealthClientOptions = {
538
+ upstream: proxyClientOptions.upstream,
539
+ affinityKey: proxyClientOptions.affinityKey,
540
+ telemetry: proxyTelemetry,
541
+ };
542
+ const { capabilityModules } = options;
543
+ const logStealthCleanupError = (error) => logProviderCleanupError(options.logger, provider, "auth", "flow", request.requestId, "stealth", error);
544
+ const credential = request.connection
545
+ ? createCredentialContext({
546
+ allowedKeys: provider.credential?.keys,
547
+ mode: request.connection.mode,
548
+ scopes: request.connection.scopes,
549
+ values: request.connection.secrets,
550
+ })
551
+ : undefined;
552
+ const cache = createProviderCache({ providerId: provider.id });
553
+ return {
554
+ context: {
555
+ flowId: request.flowId,
556
+ connectionId: resolveOperationConnectionId(request),
557
+ externalRef: request.externalRef,
558
+ tenantId: request.tenantId ?? "",
559
+ providerId: request.providerId ?? provider.id,
560
+ http: createHttpClient(baseUrl, {
561
+ ...proxyClientOptions,
562
+ ...(signal ? { signal } : {}),
563
+ }),
564
+ state: state.forConnection(resolveOperationConnectionId(request)),
565
+ stealth: stealthBaseUrl
566
+ ? capabilityModules.stealth
567
+ ? stealthProfile
568
+ ? capabilityModules.stealth.createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
569
+ : capabilityModules.stealth.createStealthClient(stealthBaseUrl, stealthClientOptions)
570
+ : stealthProfile
571
+ ? createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, stealthProfile.name, stealthClientOptions)
572
+ : createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, stealthClientOptions)
573
+ : createStealthStub(),
574
+ ...(provider.native
575
+ ? {
576
+ native: {
577
+ network: capabilityModules.nativeNetwork.createNativeNetworkClient({
578
+ egress: provider.native.network,
579
+ proxyPolicy: resolveNativeProxyPolicy(provider),
580
+ affinityKey: proxyClientOptions.affinityKey,
581
+ credentials: capabilityModules.nativeNetwork.createEnvVendorCredentialResolver(createEnvContext(provider.secrets?.map((secret) => secret.name))),
582
+ }),
583
+ },
584
+ }
585
+ : {}),
586
+ env: createEnvContext([
587
+ ...(provider.secrets?.map((secret) => secret.name) ?? []),
588
+ ...(provider.auth?.mode === "oauth2_proxied" ? ["APIFUSE__AUTH_PROXY__URL"] : []),
589
+ ]),
590
+ credential,
591
+ context: flowContextStore.context,
592
+ ocr: options.ocr ?? createOcrClientFromEnv(provider.ocr),
593
+ stt: options.stt ?? createSttClientFromEnv(provider.stt),
594
+ resolver: capabilityModules.resolver
595
+ ? capabilityModules.resolver.bindResolverSignal(options.resolver ??
596
+ capabilityModules.resolver.createResolverClientFromEnv(provider.resolver, undefined, {
597
+ allowedHosts: provider.allowedHosts,
598
+ cache,
599
+ identityScope: resolverIdentityScope,
600
+ ...(proxyPolicy
601
+ ? {
602
+ proxyIntent: {
603
+ mode: proxyPolicy.mode,
604
+ ...proxyClientOptions,
605
+ ...(stealthProfile ? { userAgent: stealthProfile.userAgent } : {}),
606
+ },
607
+ }
608
+ : {}),
609
+ }), signal)
610
+ : bindResolverSignalWithoutRuntime(options.resolver ??
611
+ createUnsupportedResolverClient("Provider does not declare resolver capability"), signal),
612
+ auth: createAuthFlowHelpers({ signal }),
613
+ },
614
+ getPatch: flowContextStore.getPatch,
615
+ };
616
+ }
617
+ const defaultProviderServerLogger = (event) => {
618
+ const line = JSON.stringify(event);
619
+ if (event.level === "info") {
620
+ console.log(line);
621
+ return;
622
+ }
623
+ console.error(line);
624
+ };
625
+ function startRequestCost() {
626
+ return {
627
+ startedAtMs: performance.now(),
628
+ cpuStart: process.cpuUsage(),
629
+ };
630
+ }
631
+ function finishRequestCost(input) {
632
+ const cpuDelta = process.cpuUsage(input.cpuStart);
633
+ return {
634
+ durationMs: Math.max(0, Math.round(performance.now() - input.startedAtMs)),
635
+ cpuUserMicros: Math.max(0, cpuDelta.user),
636
+ cpuSystemMicros: Math.max(0, cpuDelta.system),
637
+ cpuTotalMicros: Math.max(0, cpuDelta.user + cpuDelta.system),
638
+ };
639
+ }
640
+ function zodDetails(error) {
641
+ return error.issues.map((issue) => ({
642
+ path: issue.path.join("."),
643
+ code: issue.code,
644
+ message: issue.message,
645
+ }));
646
+ }
647
+ // Category-level projection with code-aware honesty overrides: a missing
648
+ // deployment secret is an APIFuse-side defect even though its category
649
+ // (credential_unavailable) usually means a caller credential problem, an
650
+ // internal stateful-routing deadline is APIFuse-owned despite its timeout
651
+ // category, and the built-in upstream failure families keep their upstream
652
+ // attribution even when the author left the category at the provider_error
653
+ // default.
654
+ function publicErrorSource(error, category) {
655
+ if (error instanceof StatefulRoutingDeadlineError)
656
+ return "apifuse";
657
+ if (isProviderError(error)) {
658
+ if (error.code === MISSING_SECRET_CODE)
659
+ return "apifuse";
660
+ if (error.code === "UPSTREAM_ERROR" || error.code === "BLOCKED") {
661
+ return "upstream_failure";
662
+ }
663
+ }
664
+ return sourceForCategory(category);
665
+ }
666
+ function toErrorResponse(error, requestId, declaredErrorCode) {
667
+ const observability = errorObservabilityDetails(error, declaredErrorCode);
668
+ const source = publicErrorSource(error, observability.category);
669
+ if (error instanceof StatefulRoutingDeadlineError) {
670
+ return {
671
+ error: {
672
+ code: "STATEFUL_FORWARDING_DEADLINE_EXPIRED",
673
+ message: "Stateful forwarding deadline expired.",
674
+ ...(requestId ? { requestId } : {}),
675
+ retryable: observability.retryable,
676
+ source,
677
+ },
678
+ };
679
+ }
680
+ if (isProviderError(error)) {
681
+ const details = error.details;
682
+ return {
683
+ error: {
684
+ code: error.code ?? "provider_error",
685
+ message: publicProviderErrorMessage(error),
686
+ ...(requestId ? { requestId } : {}),
687
+ retryable: observability.retryable,
688
+ source,
689
+ ...(error.fix ? { fix: error.fix } : {}),
690
+ ...(details !== undefined ? { details } : {}),
691
+ },
692
+ };
693
+ }
694
+ if (error instanceof z.ZodError) {
695
+ return {
696
+ error: {
697
+ code: "invalid_request",
698
+ message: "Invalid request body",
699
+ ...(requestId ? { requestId } : {}),
700
+ retryable: observability.retryable,
701
+ source,
702
+ details: zodDetails(error),
703
+ },
704
+ };
705
+ }
706
+ // A masked internal error MUST NOT be advertised as retryable: without an
707
+ // explicit retryable:false the hub (bori provider-backed engine) defaults 5xx
708
+ // to retryable:true, which turns a deterministic pre-upstream crash into an
709
+ // infinite START->CONTINUE->restart loop (2026-07-22 catchtable reserve RCA).
710
+ // We still refuse to leak message/stack — only the error class name (or the
711
+ // primitive type for non-Error throwables) is surfaced for ops triage.
712
+ return {
713
+ error: {
714
+ code: "internal_error",
715
+ message: "Internal error",
716
+ ...(requestId ? { requestId } : {}),
717
+ retryable: observability.retryable,
718
+ source,
719
+ details: {
720
+ retryable: false,
721
+ category: "internal_error",
722
+ errorClass: error instanceof Error ? error.name : typeof error,
723
+ },
724
+ },
725
+ };
726
+ }
727
+ // Accepts `unknown` so the branded guards narrow cleanly from the top: the
728
+ // subtype error classes are structurally compatible with ProviderError, so
729
+ // narrowing from a ProviderError-typed value would collapse the negative branch
730
+ // to `never`. Narrowing from unknown avoids that while still recognizing errors
731
+ // from a duplicate SDK module instance.
732
+ function providerObservabilityDetails(error, declaredErrorCode) {
733
+ const declaredRetryable = sdkOwnsErrorResolution(error)
734
+ ? undefined
735
+ : declaredErrorCode?.retryable;
736
+ // Session-expiry surfaces the credential_expired category + the opt-in
737
+ // retryable signal so Gateway/Credential Service can refresh and re-drive the
738
+ // operation (see design.md §4.3 D3). Without this branch the auth error would
739
+ // serialize as a bare 401 with no retryable/category, losing the refresh
740
+ // signal for exactly the retryOnAuthRefresh operations it is meant to enable.
741
+ if (isSessionExpiredError(error)) {
742
+ return {
743
+ category: error.options?.category ?? "credential_expired",
744
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
745
+ retryable: error.options?.retryable ?? declaredRetryable ?? false,
746
+ };
747
+ }
748
+ // Missing-secret errors carry the canonical credential_unavailable category
749
+ // so Gateway/observability can attribute the failure to provisioning, not
750
+ // the upstream. Matched by code (not constructor) so both the SDK-owned
751
+ // runtime gate and any not-yet-migrated provider-thrown MISSING_SECRET
752
+ // serialize identically, including across duplicate SDK module instances.
753
+ if (isProviderError(error) && error.code === MISSING_SECRET_CODE) {
754
+ return {
755
+ category: error.options?.category ?? "credential_unavailable",
756
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
757
+ retryable: error.options?.retryable ?? declaredRetryable ?? false,
758
+ };
759
+ }
760
+ if (!isTransportError(error)) {
761
+ return undefined;
762
+ }
763
+ const isProxyPoolCode = error.code === PROXY_POOL_EXHAUSTED_CODE ||
764
+ error.code === PROXY_EDGE_AUTH_REJECTED_CODE ||
765
+ error.code === "PROXY_ALLOCATION_FAILED";
766
+ const category = error.options?.category ??
767
+ (isProxyPoolCode
768
+ ? "proxy_pool"
769
+ : error.code === PROXY_AUTH_IP_DENIED_CODE
770
+ ? "anti_bot_blocked"
771
+ : error.code === "transport_timeout"
772
+ ? "timeout"
773
+ : error.code === "transport_network_error"
774
+ ? "network"
775
+ : error.upstreamStatus
776
+ ? categoryForStatus(error.upstreamStatus)
777
+ : "upstream_http");
778
+ return {
779
+ category,
780
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
781
+ retryable: error.options?.retryable ??
782
+ (category === "upstream_http" && error.upstreamStatus
783
+ ? error.upstreamStatus >= 500
784
+ : isRetryableCategory(category)),
785
+ ...(error.upstreamStatus ? { upstreamStatus: error.upstreamStatus } : {}),
786
+ };
787
+ }
788
+ function errorObservabilityDetails(error, declaredErrorCode) {
789
+ const effectiveDeclaration = sdkOwnsErrorResolution(error) ? undefined : declaredErrorCode;
790
+ const providerDetails = providerObservabilityDetails(error, effectiveDeclaration);
791
+ if (providerDetails)
792
+ return providerDetails;
793
+ if (error instanceof z.ZodError || isValidationError(error)) {
794
+ const declaredStatus = effectiveDeclaration?.status;
795
+ return {
796
+ category: isProviderError(error) && error.options?.category
797
+ ? error.options.category
798
+ : isEmittableErrorStatus(declaredStatus) &&
799
+ categoryForStatus(declaredStatus) === "upstream_rejected"
800
+ ? "upstream_rejected"
801
+ : isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
802
+ ? "provider_error"
803
+ : "input_validation",
804
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
805
+ retryable: isProviderError(error)
806
+ ? (error.options?.retryable ?? effectiveDeclaration?.retryable ?? false)
807
+ : false,
808
+ };
809
+ }
810
+ if (error instanceof StatefulRoutingDeadlineError) {
811
+ return {
812
+ category: "timeout",
813
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
814
+ retryable: false,
815
+ };
816
+ }
817
+ if (isProviderError(error)) {
818
+ // Deterministic upstream refusals default to the rejection category:
819
+ // the UPSTREAM_REJECTED family and any operation-declared rejection
820
+ // status (409/410/422) classify as upstream_rejected unless the
821
+ // author set an explicit category.
822
+ const declaredStatus = effectiveDeclaration?.status;
823
+ const rejectionDefault = error.code === "UPSTREAM_REJECTED" ||
824
+ (isEmittableErrorStatus(declaredStatus) &&
825
+ categoryForStatus(declaredStatus) === "upstream_rejected")
826
+ ? "upstream_rejected"
827
+ : "provider_error";
828
+ return {
829
+ category: error.options?.category ?? rejectionDefault,
830
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
831
+ retryable: error.options?.retryable ?? effectiveDeclaration?.retryable ?? false,
832
+ };
833
+ }
834
+ return {
835
+ category: "internal_error",
836
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
837
+ retryable: false,
838
+ };
839
+ }
840
+ function responseWithErrorObservability(response, error, declaredErrorCode) {
841
+ const headers = new Headers(response.headers);
842
+ headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(errorObservabilityDetails(error, declaredErrorCode)));
843
+ return new Response(response.body, {
844
+ status: response.status,
845
+ statusText: response.statusText,
846
+ headers,
847
+ });
848
+ }
849
+ function publicProviderErrorMessage(error) {
850
+ if (isTransportError(error)) {
851
+ if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
852
+ return error.message;
853
+ }
854
+ if (error.code === PROXY_EDGE_AUTH_REJECTED_CODE) {
855
+ return error.message;
856
+ }
857
+ if (error.code === PROXY_POOL_EXHAUSTED_CODE) {
858
+ return error.message;
859
+ }
860
+ if (error.code === "transport_timeout")
861
+ return "Request timed out";
862
+ if (error.code === "transport_network_error")
863
+ return "Network error";
864
+ if (error.code === "upstream_http_error" && error.status) {
865
+ return `Upstream request failed with status ${error.status}`;
866
+ }
867
+ if (error.status) {
868
+ return `Upstream request failed with status ${error.status}`;
869
+ }
870
+ return "Upstream request failed";
871
+ }
872
+ return error.message;
873
+ }
874
+ function isEmittableErrorStatus(value) {
875
+ return (typeof value === "number" && VALID_OPERATION_ERROR_STATUSES.some((status) => status === value));
876
+ }
877
+ function toStatusCode(error, declaredErrorCode) {
878
+ if (error instanceof z.ZodError) {
879
+ return 400;
880
+ }
881
+ if (error instanceof StatefulRoutingDeadlineError) {
882
+ return 504;
883
+ }
884
+ if (isProviderError(error)) {
885
+ if (!sdkOwnsErrorResolution(error) && isEmittableErrorStatus(declaredErrorCode?.status)) {
886
+ return declaredErrorCode.status;
887
+ }
888
+ // Canonical SDK code → status mapping lives in error-resolution.ts so
889
+ // the authoring lint and this runtime path share one source of truth.
890
+ if (typeof error.code === "string") {
891
+ const mappedStatus = SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES.get(error.code);
892
+ if (mappedStatus !== undefined) {
893
+ return mappedStatus;
894
+ }
895
+ }
896
+ if (isTransportError(error)) {
897
+ return error.code === "transport_timeout" ? 504 : 502;
898
+ }
899
+ if (isValidationError(error)) {
900
+ return error.options?.category === "output_validation" ? 500 : 400;
901
+ }
902
+ return 500;
903
+ }
904
+ return 500;
905
+ }
906
+ function sdkOwnsErrorResolution(error) {
907
+ if (isSessionExpiredError(error))
908
+ return true;
909
+ if (isTransportError(error))
910
+ return true;
911
+ if (error instanceof z.ZodError)
912
+ return true;
913
+ if (error instanceof StatefulRoutingDeadlineError)
914
+ return true;
915
+ return (isProviderError(error) &&
916
+ typeof error.code === "string" &&
917
+ SDK_RUNTIME_OWNED_ERROR_CODES.has(error.code));
918
+ }
919
+ function buildOperationErrorCodeLookup(provider) {
920
+ return new Map(Object.entries(provider.operations).flatMap(([operationId, operation]) => {
921
+ const errorCodes = operation.docs?.errorCodes;
922
+ return errorCodes?.length
923
+ ? [[operationId, new Map(errorCodes.map((entry) => [entry.code, entry]))]]
924
+ : [];
925
+ }));
926
+ }
927
+ function declaredErrorCodeFor(error, operationId, lookup) {
928
+ if (!operationId || !isProviderError(error) || typeof error.code !== "string")
929
+ return undefined;
930
+ return lookup.get(operationId)?.get(error.code);
931
+ }
932
+ function extractRequestId(raw) {
933
+ if (!raw || typeof raw !== "object") {
934
+ return undefined;
935
+ }
936
+ const value = Object.getOwnPropertyDescriptor(raw, "requestId")?.value;
937
+ return typeof value === "string" ? value : undefined;
938
+ }
939
+ const MAX_PROVIDER_ERROR_CAUSE_FRAMES = 5;
940
+ function providerErrorCauseChain(error) {
941
+ if (!(error instanceof Error) && !isProviderError(error))
942
+ return undefined;
943
+ const seen = new Set([error]);
944
+ const frames = [];
945
+ let cause = error.cause;
946
+ while (frames.length < MAX_PROVIDER_ERROR_CAUSE_FRAMES &&
947
+ (cause instanceof Error || isProviderError(cause)) &&
948
+ !seen.has(cause)) {
949
+ seen.add(cause);
950
+ const message = cause.message;
951
+ frames.push({
952
+ errorClass: cause.name,
953
+ ...(isProviderError(cause) && typeof cause.code === "string" ? { code: cause.code } : {}),
954
+ messageLength: message.length,
955
+ messageFingerprint: createHash("sha256").update(message).digest("hex").slice(0, 12),
956
+ });
957
+ cause = cause.cause;
958
+ }
959
+ return frames.length > 0 ? frames : undefined;
960
+ }
961
+ function logProviderError(logger, provider, kind, route, requestId, error, status, cost, declaredErrorCode, proxyTelemetry) {
962
+ const code = isProviderError(error)
963
+ ? (error.code ?? "provider_error")
964
+ : error instanceof z.ZodError
965
+ ? "invalid_request"
966
+ : error instanceof StatefulRoutingDeadlineError
967
+ ? "STATEFUL_FORWARDING_DEADLINE_EXPIRED"
968
+ : "internal_error";
969
+ const errorClass = error instanceof Error ? error.name : typeof error;
970
+ const message = error instanceof Error ? error.message : String(error);
971
+ const causeChain = providerErrorCauseChain(error);
972
+ const details = errorObservabilityDetails(error, declaredErrorCode);
973
+ const isUnregisteredProviderErrorCode = status === 500 &&
974
+ isProviderError(error) &&
975
+ !isValidationError(error) &&
976
+ typeof error.code === "string" &&
977
+ !SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code) &&
978
+ declaredErrorCode === undefined;
979
+ const proxy = proxyTelemetry?.toLogPayload();
980
+ const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
981
+ emit({
982
+ level: status >= 500 ? "error" : "warn",
983
+ event: "provider_request_failed",
984
+ providerId: provider.id,
985
+ kind,
986
+ route,
987
+ ...(requestId ? { requestId } : {}),
988
+ status,
989
+ ...cost,
990
+ ...(proxy ? { proxy } : {}),
991
+ code,
992
+ errorClass,
993
+ message,
994
+ ...(causeChain ? { causeChain } : {}),
995
+ ...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
996
+ errorCategory: details.category,
997
+ taxonomyVersion: details.taxonomyVersion,
998
+ retryable: details.retryable,
999
+ ...(isUnregisteredProviderErrorCode
1000
+ ? {
1001
+ signal: "unregistered_provider_error_code",
1002
+ signalFix: "Declare this code (with status and retryable) in the operation's docs.errorCodes so it serves its intended status instead of 500.",
1003
+ }
1004
+ : {}),
1005
+ ...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
1006
+ });
1007
+ }
1008
+ function logProviderCleanupError(logger, provider, kind, operationId, requestId, resource, error) {
1009
+ const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
1010
+ const errorClass = error instanceof Error ? error.name : typeof error;
1011
+ const message = error instanceof Error ? error.message : String(error);
1012
+ emit({
1013
+ level: "warn",
1014
+ event: "provider_cleanup_failed",
1015
+ providerId: provider.id,
1016
+ kind,
1017
+ route: operationId,
1018
+ ...(requestId ? { requestId } : {}),
1019
+ resource,
1020
+ errorClass,
1021
+ message,
1022
+ });
1023
+ }
1024
+ function logProviderSuccess(logger, provider, kind, route, requestId, status, cost, proxyTelemetry) {
1025
+ const proxy = proxyTelemetry?.toLogPayload();
1026
+ const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
1027
+ emit({
1028
+ level: "info",
1029
+ event: "provider_request_completed",
1030
+ providerId: provider.id,
1031
+ kind,
1032
+ route,
1033
+ ...(requestId ? { requestId } : {}),
1034
+ status,
1035
+ ...cost,
1036
+ ...(proxy ? { proxy } : {}),
1037
+ });
1038
+ }
1039
+ function toJsonSuccessResponse(result, ctx) {
1040
+ if (result instanceof Response) {
1041
+ return result;
1042
+ }
1043
+ if (result instanceof ReadableStream) {
1044
+ return new Response(result);
1045
+ }
1046
+ const cacheMeta = ctx?.cache.responseMeta();
1047
+ const retryMeta = ctx ? retryResponseMeta.get(ctx) : undefined;
1048
+ const meta = cacheMeta || retryMeta
1049
+ ? {
1050
+ ...(cacheMeta
1051
+ ? {
1052
+ cached: cacheMeta.hit,
1053
+ stale: cacheMeta.stale,
1054
+ cache: cacheMeta,
1055
+ }
1056
+ : {}),
1057
+ ...(retryMeta ? { retry: retryMeta } : {}),
1058
+ }
1059
+ : undefined;
1060
+ return {
1061
+ data: result,
1062
+ ...(meta ? { meta } : {}),
1063
+ };
1064
+ }
1065
+ function isAsyncIterable(value) {
1066
+ if (!value || typeof value !== "object")
1067
+ return false;
1068
+ const iterator = Reflect.get(value, Symbol.asyncIterator);
1069
+ return typeof iterator === "function";
1070
+ }
1071
+ function responseWithCleanup(response, cleanup) {
1072
+ if (!response.body) {
1073
+ void cleanup();
1074
+ return response;
1075
+ }
1076
+ const reader = response.body.getReader();
1077
+ let cleaned = false;
1078
+ const runCleanup = async () => {
1079
+ if (cleaned)
1080
+ return;
1081
+ cleaned = true;
1082
+ await cleanup();
1083
+ };
1084
+ const body = new ReadableStream({
1085
+ async pull(controller) {
1086
+ try {
1087
+ const { done, value } = await reader.read();
1088
+ if (done) {
1089
+ controller.close();
1090
+ await runCleanup();
1091
+ return;
1092
+ }
1093
+ if (value)
1094
+ controller.enqueue(value);
1095
+ }
1096
+ catch (error) {
1097
+ await runCleanup();
1098
+ controller.error(error);
1099
+ }
1100
+ },
1101
+ async cancel(reason) {
1102
+ try {
1103
+ await reader.cancel(reason);
1104
+ }
1105
+ finally {
1106
+ await runCleanup();
1107
+ }
1108
+ },
1109
+ });
1110
+ return new Response(body, {
1111
+ headers: response.headers,
1112
+ status: response.status,
1113
+ statusText: response.statusText,
1114
+ });
1115
+ }
1116
+ async function validateSseEvent(operation, event) {
1117
+ const transport = getSseTransport(operation);
1118
+ const schema = transport?.events?.[event.event];
1119
+ if (!schema) {
1120
+ if (event.event === APIFUSE_STREAM_ERROR_EVENT || event.event === APIFUSE_STREAM_DONE_EVENT) {
1121
+ return event;
1122
+ }
1123
+ throw new ProviderError(`SSE event "${event.event}" is not declared in operation transport.events.`, {
1124
+ code: "SSE_EVENT_UNDECLARED",
1125
+ category: "output_validation",
1126
+ retryable: false,
1127
+ fix: `Add "${event.event}" to transport.events or stop emitting that event.`,
1128
+ });
1129
+ }
1130
+ const data = await parseSchema(schema, event.data, `transport.events.${event.event}`);
1131
+ return { ...event, data };
1132
+ }
1133
+ function byteLength(value) {
1134
+ if (typeof value === "string") {
1135
+ return new TextEncoder().encode(value).byteLength;
1136
+ }
1137
+ return value.byteLength;
1138
+ }
1139
+ function assertStreamPayloadWithinLimit(actualBytes, maxBytes, kind) {
1140
+ if (maxBytes === undefined || actualBytes <= maxBytes)
1141
+ return;
1142
+ throw new ProviderError(`Stream ${kind} exceeded declared byte limit (${actualBytes} > ${maxBytes}).`, {
1143
+ code: kind === "event" ? "STREAM_EVENT_TOO_LARGE" : "STREAM_CHUNK_TOO_LARGE",
1144
+ retryable: false,
1145
+ category: "input_validation",
1146
+ fix: kind === "event"
1147
+ ? "Emit smaller SSE events or increase transport.maxEventBytes."
1148
+ : "Emit smaller stream chunks or increase transport.maxChunkBytes.",
1149
+ });
1150
+ }
1151
+ function toSseResponse(operation, result, cleanup, requestId) {
1152
+ const encoder = new TextEncoder();
1153
+ const iterator = result[Symbol.asyncIterator]();
1154
+ const transport = getSseTransport(operation);
1155
+ let done = false;
1156
+ let cleaned = false;
1157
+ const runCleanup = async () => {
1158
+ if (cleaned)
1159
+ return;
1160
+ cleaned = true;
1161
+ await cleanup();
1162
+ };
1163
+ const body = new ReadableStream({
1164
+ async pull(controller) {
1165
+ try {
1166
+ if (done) {
1167
+ controller.close();
1168
+ await runCleanup();
1169
+ return;
1170
+ }
1171
+ const next = await iterator.next();
1172
+ if (next.done) {
1173
+ done = true;
1174
+ controller.close();
1175
+ await runCleanup();
1176
+ return;
1177
+ }
1178
+ const validated = await validateSseEvent(operation, next.value);
1179
+ const encodedEvent = encodeSseEvent(validated);
1180
+ const encodedBytes = encoder.encode(encodedEvent);
1181
+ assertStreamPayloadWithinLimit(encodedBytes.byteLength, transport?.maxEventBytes, "event");
1182
+ controller.enqueue(encodedBytes);
1183
+ }
1184
+ catch (error) {
1185
+ const message = error instanceof Error ? error.message : "Stream failed";
1186
+ controller.enqueue(encoder.encode(encodeSseEvent(streamError("stream_error", message, {
1187
+ ...(requestId ? { requestId } : {}),
1188
+ }))));
1189
+ controller.close();
1190
+ done = true;
1191
+ await runCleanup();
1192
+ }
1193
+ },
1194
+ async cancel(reason) {
1195
+ try {
1196
+ await iterator.return?.(reason);
1197
+ }
1198
+ finally {
1199
+ await runCleanup();
1200
+ }
1201
+ },
1202
+ });
1203
+ return new Response(body, {
1204
+ headers: {
1205
+ "Cache-Control": "no-cache, no-transform",
1206
+ Connection: "keep-alive",
1207
+ "Content-Type": "text/event-stream; charset=utf-8",
1208
+ },
1209
+ });
1210
+ }
1211
+ function enforceStreamChunkLimit(body, maxChunkBytes) {
1212
+ if (maxChunkBytes === undefined)
1213
+ return body;
1214
+ const reader = body.getReader();
1215
+ return new ReadableStream({
1216
+ async pull(controller) {
1217
+ try {
1218
+ const { done, value } = await reader.read();
1219
+ if (done) {
1220
+ controller.close();
1221
+ return;
1222
+ }
1223
+ if (value) {
1224
+ assertStreamPayloadWithinLimit(byteLength(value), maxChunkBytes, "chunk");
1225
+ controller.enqueue(value);
1226
+ }
1227
+ }
1228
+ catch (error) {
1229
+ controller.error(error);
1230
+ }
1231
+ },
1232
+ cancel(reason) {
1233
+ return reader.cancel(reason);
1234
+ },
1235
+ });
1236
+ }
1237
+ function toStreamingResponse(operation, result, cleanup, requestId) {
1238
+ const transport = operation.transport?.kind ?? "json";
1239
+ if (transport === "sse" && (result instanceof Response || result instanceof ReadableStream)) {
1240
+ void cleanup();
1241
+ throw new ProviderError("SSE operations must return an AsyncIterable of typed stream.event(...) values.", {
1242
+ code: "SSE_RESULT_UNSUPPORTED",
1243
+ category: "output_validation",
1244
+ retryable: false,
1245
+ fix: "Return an async generator that yields stream.event(name, data) so APIFuse can validate event schemas and enforce event byte limits.",
1246
+ });
1247
+ }
1248
+ if (result instanceof Response) {
1249
+ const httpTransport = getHttpStreamTransport(operation);
1250
+ if (httpTransport && result.body && httpTransport?.maxChunkBytes !== undefined) {
1251
+ return responseWithCleanup(new Response(enforceStreamChunkLimit(result.body, httpTransport.maxChunkBytes), {
1252
+ headers: result.headers,
1253
+ status: result.status,
1254
+ statusText: result.statusText,
1255
+ }), cleanup);
1256
+ }
1257
+ return responseWithCleanup(result, cleanup);
1258
+ }
1259
+ if (result instanceof ReadableStream) {
1260
+ const httpTransport = getHttpStreamTransport(operation);
1261
+ const stream = httpTransport !== undefined
1262
+ ? enforceStreamChunkLimit(result, httpTransport.maxChunkBytes)
1263
+ : result;
1264
+ return responseWithCleanup(new Response(stream, {
1265
+ headers: transport === "sse"
1266
+ ? { "Content-Type": "text/event-stream; charset=utf-8" }
1267
+ : {
1268
+ "Content-Type": operation.transport?.kind === "http-stream"
1269
+ ? (operation.transport.contentType ?? "application/octet-stream")
1270
+ : "application/octet-stream",
1271
+ },
1272
+ }), cleanup);
1273
+ }
1274
+ if (transport === "sse" && isAsyncIterable(result)) {
1275
+ return toSseResponse(operation, result, cleanup, requestId);
1276
+ }
1277
+ void cleanup();
1278
+ throw new ProviderError(`Streaming operation returned unsupported result for transport "${transport}"`, {
1279
+ code: "STREAM_RESULT_UNSUPPORTED",
1280
+ fix: "Return an AsyncIterable of stream.event(...) values, a ReadableStream, or a Response from streaming operations.",
1281
+ });
1282
+ }
1283
+ function getSseTransport(operation) {
1284
+ return operation.transport?.kind === "sse" ? operation.transport : undefined;
1285
+ }
1286
+ function getHttpStreamTransport(operation) {
1287
+ return operation.transport?.kind === "http-stream" ? operation.transport : undefined;
1288
+ }
1289
+ function toAuthFlowResponse(result, contextPatch) {
1290
+ if (result instanceof Response) {
1291
+ return result;
1292
+ }
1293
+ if (result instanceof ReadableStream) {
1294
+ return new Response(result);
1295
+ }
1296
+ return {
1297
+ data: result,
1298
+ ...(contextPatch ? { contextPatch } : {}),
1299
+ };
1300
+ }
1301
+ function authFlowLocaleFromHeaders(headers) {
1302
+ const header = Object.entries(headers ?? {}).find(([key]) => key.toLowerCase() === "accept-language")?.[1];
1303
+ for (const token of (header ?? "").split(",")) {
1304
+ const language = token.trim().split(";")[0]?.split("-")[0]?.toLowerCase();
1305
+ if (isAuthFlowLocale(language)) {
1306
+ return language;
1307
+ }
1308
+ }
1309
+ return "en";
1310
+ }
1311
+ function isAuthFlowLocale(value) {
1312
+ return value === "en" || value === "ko" || value === "ja";
1313
+ }
1314
+ function isAuthTurn(value) {
1315
+ return !!value && typeof value === "object" && "kind" in value && "turnId" in value;
1316
+ }
1317
+ function loadAuthFlowLocaleCatalogs(provider) {
1318
+ for (const providerDir of [
1319
+ process.cwd(),
1320
+ join(process.cwd(), "providers", provider.id),
1321
+ join(process.cwd(), "providers-staging", provider.id),
1322
+ ]) {
1323
+ if (!existsSync(join(providerDir, "locales", "en.json")))
1324
+ continue;
1325
+ try {
1326
+ return loadProviderLocaleCatalogs({
1327
+ providerDir,
1328
+ locales: AUTH_FLOW_LOCALES,
1329
+ });
1330
+ }
1331
+ catch {
1332
+ return undefined;
1333
+ }
1334
+ }
1335
+ return undefined;
1336
+ }
1337
+ function materializeAuthFlowTurn(provider, request, turn) {
1338
+ const catalogs = loadAuthFlowLocaleCatalogs(provider);
1339
+ if (!catalogs)
1340
+ return turn;
1341
+ return localizeAuthTurn(turn, {
1342
+ catalogs,
1343
+ locale: authFlowLocaleFromHeaders(request.headers),
1344
+ });
1345
+ }
1346
+ function withAuthRequestHeaders(request, headers) {
1347
+ return {
1348
+ ...request,
1349
+ headers: {
1350
+ ...(request.headers ?? {}),
1351
+ ...Object.fromEntries(headers.entries()),
1352
+ },
1353
+ };
1354
+ }
1355
+ async function handleOperation(provider, request, operationId, options, state = createUnsupportedProviderRuntimeState(), proxyTelemetry, signal) {
1356
+ const ctx = createProviderContext(provider, request, operationId, options, state, proxyTelemetry, signal);
1357
+ const operation = provider.operations[operationId];
1358
+ const streaming = operation?.transport?.kind && operation.transport.kind !== "json";
1359
+ let cleanupCalled = false;
1360
+ const cleanup = async () => {
1361
+ if (cleanupCalled)
1362
+ return;
1363
+ cleanupCalled = true;
1364
+ try {
1365
+ ctx.stealth.close?.();
1366
+ }
1367
+ catch (error) {
1368
+ logProviderCleanupError(options.logger, provider, "operation", operationId, request.requestId, "stealth", error);
1369
+ }
1370
+ try {
1371
+ await ctx.browser.close?.();
1372
+ }
1373
+ catch (error) {
1374
+ logProviderCleanupError(options.logger, provider, "operation", operationId, request.requestId, "browser", error);
1375
+ }
1376
+ };
1377
+ try {
1378
+ const result = options.operationExecutor
1379
+ ? await options.operationExecutor({
1380
+ provider,
1381
+ operationId,
1382
+ ctx,
1383
+ request,
1384
+ signal,
1385
+ })
1386
+ : await executeOperation(provider, operationId, ctx, request.input);
1387
+ if (streaming && operation) {
1388
+ return toStreamingResponse(operation, result, cleanup, request.requestId);
1389
+ }
1390
+ return toJsonSuccessResponse(result, ctx);
1391
+ }
1392
+ catch (error) {
1393
+ await cleanup();
1394
+ throw error;
1395
+ }
1396
+ finally {
1397
+ if (!streaming)
1398
+ await cleanup();
1399
+ }
1400
+ }
1401
+ function responseWithProviderTelemetry(response, proxyTelemetry) {
1402
+ const headerValue = proxyTelemetry?.toHeaderValue();
1403
+ const headers = new Headers(response.headers);
1404
+ headers.delete(PROVIDER_TELEMETRY_HEADER);
1405
+ if (headerValue)
1406
+ headers.set(PROVIDER_TELEMETRY_HEADER, headerValue);
1407
+ return new Response(response.body, {
1408
+ headers,
1409
+ status: response.status,
1410
+ statusText: response.statusText,
1411
+ });
1412
+ }
1413
+ async function handleAuthFlow(provider, request, route, options, state, proxyTelemetry, signal) {
1414
+ const flow = provider.auth?.flow;
1415
+ if (!flow) {
1416
+ throw new ProviderError("Auth flow is not configured", {
1417
+ code: "AUTH_FLOW_NOT_CONFIGURED",
1418
+ });
1419
+ }
1420
+ // Same SDK-owned gate as executeOperation: OAuth/credentials ceremonies
1421
+ // depend on declared secrets (client ids/secrets), so fail structured before
1422
+ // any flow code runs instead of at whatever point the ceremony first reads
1423
+ // the env. `abort` stays exempt: a user must always be able to cancel a
1424
+ // stranded flow even when provisioning is broken.
1425
+ const { context, getPatch } = createAuthFlowContext(provider, request, options, state, proxyTelemetry, signal);
1426
+ try {
1427
+ if (route !== "abort") {
1428
+ assertRequiredSecretsPresent(provider, context.env);
1429
+ }
1430
+ const result = route === "start"
1431
+ ? await flow.start(context)
1432
+ : route === "continue"
1433
+ ? await flow.continue(context, request.input ?? {})
1434
+ : route === "poll"
1435
+ ? flow.poll
1436
+ ? await flow.poll(context)
1437
+ : null
1438
+ : route === "abort"
1439
+ ? flow.abort
1440
+ ? await flow.abort(context)
1441
+ : null
1442
+ : flow.refresh
1443
+ ? await flow.refresh(context, request.input ?? {})
1444
+ : null;
1445
+ if (route === "refresh" && !flow.refresh) {
1446
+ throw new AuthError("Provider auth flow does not support refresh.", {
1447
+ code: "refresh_not_supported",
1448
+ });
1449
+ }
1450
+ const materializedResult = result &&
1451
+ !(result instanceof Response) &&
1452
+ !(result instanceof ReadableStream) &&
1453
+ isAuthTurn(result)
1454
+ ? materializeAuthFlowTurn(provider, request, result)
1455
+ : result;
1456
+ return toAuthFlowResponse(materializedResult, getPatch());
1457
+ }
1458
+ catch (error) {
1459
+ if (error instanceof AuthAbortError) {
1460
+ return toAuthFlowResponse(error.turn, getPatch());
1461
+ }
1462
+ throw error;
1463
+ }
1464
+ finally {
1465
+ try {
1466
+ context.stealth.close?.();
1467
+ }
1468
+ catch (error) {
1469
+ logProviderCleanupError(options.logger, provider, "auth", route, request.requestId, "stealth", error);
1470
+ }
1471
+ }
1472
+ }
1473
+ class StatefulForwardingReplayCache {
1474
+ maxEntries;
1475
+ #nonces = new Map();
1476
+ #expiryBuckets = new Map();
1477
+ #nextExpiryBucket;
1478
+ #latestExpiryBucket;
1479
+ constructor(maxEntries) {
1480
+ this.maxEntries = maxEntries;
1481
+ }
1482
+ claim(nonce, expiresAtMs, nowMs) {
1483
+ this.dropExpiredBuckets(nowMs);
1484
+ if (this.#nonces.has(nonce))
1485
+ return "replayed";
1486
+ if (this.#nonces.size >= this.maxEntries)
1487
+ return "full";
1488
+ const expiryBucket = Math.ceil(expiresAtMs / STATEFUL_FORWARDING_REPLAY_BUCKET_MS) *
1489
+ STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
1490
+ this.#nonces.set(nonce, expiryBucket);
1491
+ const bucket = this.#expiryBuckets.get(expiryBucket) ?? new Set();
1492
+ bucket.add(nonce);
1493
+ this.#expiryBuckets.set(expiryBucket, bucket);
1494
+ this.#nextExpiryBucket = Math.min(this.#nextExpiryBucket ?? expiryBucket, expiryBucket);
1495
+ this.#latestExpiryBucket = Math.max(this.#latestExpiryBucket ?? expiryBucket, expiryBucket);
1496
+ return "accepted";
1497
+ }
1498
+ dropExpiredBuckets(nowMs) {
1499
+ if (this.#nextExpiryBucket === undefined || this.#latestExpiryBucket === undefined)
1500
+ return;
1501
+ if (nowMs >= this.#latestExpiryBucket) {
1502
+ this.#nonces.clear();
1503
+ this.#expiryBuckets.clear();
1504
+ this.#nextExpiryBucket = undefined;
1505
+ this.#latestExpiryBucket = undefined;
1506
+ return;
1507
+ }
1508
+ while (this.#nextExpiryBucket <= nowMs) {
1509
+ const bucket = this.#expiryBuckets.get(this.#nextExpiryBucket);
1510
+ if (bucket) {
1511
+ for (const cachedNonce of bucket)
1512
+ this.#nonces.delete(cachedNonce);
1513
+ this.#expiryBuckets.delete(this.#nextExpiryBucket);
1514
+ }
1515
+ this.#nextExpiryBucket += STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
1516
+ }
1517
+ }
1518
+ }
1519
+ function verifyStatefulForwardingRequest(input) {
1520
+ const config = input.options.statefulForwarding;
1521
+ if (!config?.secret) {
1522
+ throw new ProviderError("Stateful forwarding is not configured.", {
1523
+ code: "STATEFUL_FORWARDING_NOT_CONFIGURED",
1524
+ });
1525
+ }
1526
+ const timestamp = input.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "";
1527
+ const signature = input.headers.get(STATEFUL_FORWARDING_SIGNATURE_HEADER) ?? "";
1528
+ const nonce = input.headers.get(STATEFUL_FORWARDING_NONCE_HEADER) ?? "";
1529
+ if (!timestamp || !signature || !nonce) {
1530
+ throw new ProviderError("Stateful forwarding signature headers are missing.", {
1531
+ code: "STATEFUL_FORWARDING_SIGNATURE_MISSING",
1532
+ });
1533
+ }
1534
+ if (nonce.length > 256) {
1535
+ throw new ProviderError("Stateful forwarding nonce is invalid.", {
1536
+ code: "STATEFUL_FORWARDING_NONCE_INVALID",
1537
+ });
1538
+ }
1539
+ const timestampMs = Date.parse(timestamp);
1540
+ const maxSkewMs = config.maxSkewMs ?? DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS;
1541
+ if (!Number.isFinite(timestampMs) || Math.abs(Date.now() - timestampMs) > maxSkewMs) {
1542
+ throw new ProviderError("Stateful forwarding signature timestamp is outside the allowed skew.", { code: "STATEFUL_FORWARDING_TIMESTAMP_INVALID" });
1543
+ }
1544
+ if (!verifyStatefulRequestSignature({
1545
+ secret: config.secret,
1546
+ timestamp,
1547
+ rawBody: input.rawBody,
1548
+ method: input.method,
1549
+ path: input.path,
1550
+ nonce,
1551
+ signature,
1552
+ })) {
1553
+ throw new ProviderError("Stateful forwarding signature is invalid.", {
1554
+ code: "STATEFUL_FORWARDING_SIGNATURE_INVALID",
1555
+ });
1556
+ }
1557
+ const replayResult = input.replayCache.claim(nonce, timestampMs + maxSkewMs, Date.now());
1558
+ if (replayResult === "replayed") {
1559
+ throw new ProviderError("Stateful forwarding nonce has already been used.", {
1560
+ code: "STATEFUL_FORWARDING_REPLAY_DETECTED",
1561
+ });
1562
+ }
1563
+ if (replayResult === "full") {
1564
+ throw new ProviderError("Stateful forwarding replay cache is at capacity.", {
1565
+ code: "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
1566
+ });
1567
+ }
1568
+ }
1569
+ function operationRequestFromForwardingEnvelope(envelope) {
1570
+ return {
1571
+ ...envelope.operationRequest,
1572
+ ...(envelope.deadlineAt !== undefined ? { deadlineAt: envelope.deadlineAt } : {}),
1573
+ };
1574
+ }
1575
+ function parseStatefulForwardingEnvelope(rawBody) {
1576
+ const parsed = ProviderServerStatefulForwardEnvelopeSchema.safeParse(rawBody);
1577
+ if (parsed.success)
1578
+ return parsed.data;
1579
+ throw new ProviderError("Stateful forwarding envelope is invalid.", {
1580
+ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
1581
+ details: zodDetails(parsed.error),
1582
+ });
1583
+ }
1584
+ /**
1585
+ * Primary, cross-runtime app factory. Declared capability ESM is preloaded
1586
+ * asynchronously, so this path works on Bun and every supported Node release.
1587
+ */
1588
+ export async function createServerAppAsync(provider, options = {}) {
1589
+ validateFailClosedDeclaration(provider);
1590
+ validateStatefulServerConfig(options);
1591
+ return createServerAppWithCapabilityModules(provider, options, await loadProviderCapabilityModules(provider));
1592
+ }
1593
+ /**
1594
+ * Synchronous compatibility factory. Standard providers remain synchronous on
1595
+ * every runtime because they load no capability modules. Providers declaring a
1596
+ * capability require Bun or Node >=22.12; older Node releases receive an
1597
+ * actionable error directing them to createServerAppAsync().
1598
+ */
1599
+ export function createServerApp(provider, options = {}) {
1600
+ validateFailClosedDeclaration(provider);
1601
+ validateStatefulServerConfig(options);
1602
+ return createServerAppWithCapabilityModules(provider, options, loadProviderCapabilityModulesSync(provider));
1603
+ }
1604
+ function createServerAppWithCapabilityModules(provider, serverOptions, capabilityModules) {
1605
+ const options = { ...serverOptions, capabilityModules };
1606
+ const app = new Hono();
1607
+ const logger = options.logger ?? defaultProviderServerLogger;
1608
+ const operationErrorCodes = buildOperationErrorCodeLookup(provider);
1609
+ const statefulForwardingReplayCache = new StatefulForwardingReplayCache(options.statefulForwarding?.replayCacheMaxEntries ??
1610
+ DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES);
1611
+ const state = options.state ??
1612
+ createProviderRuntimeStateFromEnv({
1613
+ providerId: provider.id,
1614
+ allowMemoryFallback: options.allowMemoryStateFallback === true,
1615
+ });
1616
+ // Boot-time visibility for unprovisioned declared secrets: emit a structured
1617
+ // warn so deploy tooling/alerting sees the gap the moment the pod boots,
1618
+ // instead of discovering it request-by-request. Deliberately log-only — a
1619
+ // boot crash would trade a structured MISSING_SECRET signal for
1620
+ // CrashLoopBackOff. Requests still fail closed via the executeOperation gate.
1621
+ const missingSecretsAtBoot = listMissingRequiredSecrets(provider, createEnvContext(provider.secrets?.map((secret) => secret.name)));
1622
+ if (missingSecretsAtBoot.length > 0) {
1623
+ logger({
1624
+ level: "warn",
1625
+ event: "provider_secrets_missing",
1626
+ providerId: provider.id,
1627
+ missingSecrets: missingSecretsAtBoot,
1628
+ });
1629
+ }
1630
+ app.notFound((c) => {
1631
+ const error = new ProviderError("Not found", { code: "not_found", retryable: false });
1632
+ return responseWithErrorObservability(c.json(toErrorResponse(error), 404), error);
1633
+ });
1634
+ app.get("/health", (c) => c.json({
1635
+ status: "ok",
1636
+ provider: provider.id,
1637
+ version: provider.version,
1638
+ }));
1639
+ app.post(STATEFUL_INTERNAL_OPERATIONS_ROUTE, async (c) => {
1640
+ let rawBodyText = "";
1641
+ let rawBody;
1642
+ let operationId;
1643
+ const operation = "stateful-internal";
1644
+ const requestCost = startRequestCost();
1645
+ try {
1646
+ if (!options.internalOperationExecutor) {
1647
+ throw new ProviderError("Stateful internal operation executor is not configured.", {
1648
+ code: "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
1649
+ });
1650
+ }
1651
+ rawBodyText = await c.req.raw.clone().text();
1652
+ verifyStatefulForwardingRequest({
1653
+ options,
1654
+ rawBody: rawBodyText,
1655
+ headers: c.req.raw.headers,
1656
+ method: c.req.raw.method,
1657
+ path: STATEFUL_INTERNAL_OPERATIONS_ROUTE,
1658
+ replayCache: statefulForwardingReplayCache,
1659
+ });
1660
+ try {
1661
+ rawBody = JSON.parse(rawBodyText);
1662
+ }
1663
+ catch {
1664
+ throw new ProviderError("Stateful forwarding envelope is not valid JSON.", {
1665
+ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
1666
+ });
1667
+ }
1668
+ const envelope = parseStatefulForwardingEnvelope(rawBody);
1669
+ if (envelope.providerId !== provider.id) {
1670
+ throw new ProviderError("Stateful forwarding envelope providerId does not match the served provider.", { code: "STATEFUL_FORWARDING_PROVIDER_MISMATCH" });
1671
+ }
1672
+ if (envelope.requestId !== envelope.operationRequest.requestId) {
1673
+ throw new ProviderError("Stateful forwarding requestId values do not match.", {
1674
+ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
1675
+ });
1676
+ }
1677
+ if (envelope.sourcePodId !==
1678
+ (c.req.raw.headers.get(STATEFUL_FORWARDING_SOURCE_POD_HEADER) ?? "")) {
1679
+ throw new ProviderError("Stateful forwarding source pod does not match its header.", {
1680
+ code: "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
1681
+ });
1682
+ }
1683
+ if (envelope.forwardedAt !== (c.req.raw.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "")) {
1684
+ throw new ProviderError("Stateful forwarding forwardedAt does not match its signature timestamp.", { code: "STATEFUL_FORWARDING_ENVELOPE_INVALID" });
1685
+ }
1686
+ const deadlineAtMs = envelope.deadlineAt ? Date.parse(envelope.deadlineAt) : undefined;
1687
+ if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
1688
+ throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
1689
+ }
1690
+ const remainingDeadlineMs = deadlineAtMs === undefined ? undefined : deadlineAtMs - Date.now();
1691
+ const deadlineSignal = remainingDeadlineMs === undefined ? undefined : AbortSignal.timeout(remainingDeadlineMs);
1692
+ const signal = deadlineSignal
1693
+ ? AbortSignal.any([c.req.raw.signal, deadlineSignal])
1694
+ : c.req.raw.signal;
1695
+ const ownerFenceValidation = Promise.resolve(options.statefulForwarding?.validateOwnerFence({
1696
+ providerId: envelope.providerId,
1697
+ sessionKey: envelope.sessionKey,
1698
+ ownerPodId: envelope.ownerPodId,
1699
+ generation: envelope.generation,
1700
+ sourcePodId: envelope.sourcePodId,
1701
+ forwardedAt: envelope.forwardedAt,
1702
+ requestId: envelope.requestId,
1703
+ ...(envelope.idempotencyKey ? { idempotencyKey: envelope.idempotencyKey } : {}),
1704
+ }, signal));
1705
+ let ownerFenceValid;
1706
+ try {
1707
+ ownerFenceValid = deadlineSignal
1708
+ ? await Promise.race([
1709
+ ownerFenceValidation,
1710
+ new Promise((_resolve, reject) => {
1711
+ deadlineSignal.addEventListener("abort", () => reject(new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt)), { once: true });
1712
+ }),
1713
+ ])
1714
+ : await ownerFenceValidation;
1715
+ }
1716
+ catch (error) {
1717
+ if (deadlineSignal?.aborted) {
1718
+ throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
1719
+ }
1720
+ throw error;
1721
+ }
1722
+ if (ownerFenceValid !== true) {
1723
+ throw new ProviderError("Stateful forwarding owner fence is no longer current.", {
1724
+ code: "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
1725
+ });
1726
+ }
1727
+ const request = operationRequestFromForwardingEnvelope(envelope);
1728
+ operationId = envelope.operationId;
1729
+ const ctx = createProviderContext(provider, request, operationId, options, state, undefined, signal);
1730
+ if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
1731
+ throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
1732
+ }
1733
+ const output = await options.internalOperationExecutor({
1734
+ provider,
1735
+ operationId,
1736
+ ctx,
1737
+ request,
1738
+ internalStatefulForward: envelope,
1739
+ signal,
1740
+ });
1741
+ logProviderSuccess(logger, provider, "operation", operationId || operation, request.requestId, 200, finishRequestCost(requestCost));
1742
+ return c.json({ data: output });
1743
+ }
1744
+ catch (error) {
1745
+ const declaredErrorCode = declaredErrorCodeFor(error, operationId, operationErrorCodes);
1746
+ const status = toStatusCode(error, declaredErrorCode);
1747
+ if (isProviderError(error) && error.code === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
1748
+ c.header("Retry-After", String(STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS));
1749
+ }
1750
+ const requestId = extractRequestId(rawBody);
1751
+ logProviderError(logger, provider, "operation", operationId || operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode);
1752
+ return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, declaredErrorCode), status), error, declaredErrorCode);
1753
+ }
1754
+ });
1755
+ app.post("/v1/:operation", async (c) => {
1756
+ let rawBody;
1757
+ const operation = c.req.param("operation");
1758
+ const proxyTelemetry = new ProxyTelemetryCollector();
1759
+ const requestCost = startRequestCost();
1760
+ try {
1761
+ rawBody = await c.req.raw
1762
+ .clone()
1763
+ .json()
1764
+ .catch(() => undefined);
1765
+ const body = OperationRequestSchema.parse(rawBody);
1766
+ const requestHeaders = Object.fromEntries(c.req.raw.headers.entries());
1767
+ body.headers = { ...requestHeaders, ...body.headers };
1768
+ const response = await handleOperation(provider, body, operation, options, state, proxyTelemetry, c.req.raw.signal);
1769
+ if (response instanceof Response) {
1770
+ logProviderSuccess(logger, provider, "operation", operation, body.requestId, response.status, finishRequestCost(requestCost), proxyTelemetry);
1771
+ return responseWithProviderTelemetry(response, proxyTelemetry);
1772
+ }
1773
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
1774
+ if (telemetryHeader)
1775
+ c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1776
+ logProviderSuccess(logger, provider, "operation", operation, body.requestId, 200, finishRequestCost(requestCost), proxyTelemetry);
1777
+ return c.json(response);
1778
+ }
1779
+ catch (error) {
1780
+ const declaredErrorCode = declaredErrorCodeFor(error, operation, operationErrorCodes);
1781
+ const status = toStatusCode(error, declaredErrorCode);
1782
+ const requestId = extractRequestId(rawBody);
1783
+ logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode, proxyTelemetry);
1784
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
1785
+ if (telemetryHeader)
1786
+ c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1787
+ return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, declaredErrorCode), status), error, declaredErrorCode);
1788
+ }
1789
+ });
1790
+ app.post("/auth/start", async (c) => {
1791
+ let rawBody;
1792
+ const proxyTelemetry = new ProxyTelemetryCollector();
1793
+ const requestCost = startRequestCost();
1794
+ try {
1795
+ rawBody = await c.req.raw
1796
+ .clone()
1797
+ .json()
1798
+ .catch(() => undefined);
1799
+ const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1800
+ const response = await handleAuthFlow(provider, body, "start", options, state, proxyTelemetry, c.req.raw.signal);
1801
+ logProviderSuccess(logger, provider, "auth", "start", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost), proxyTelemetry);
1802
+ if (response instanceof Response)
1803
+ return responseWithProviderTelemetry(response, proxyTelemetry);
1804
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
1805
+ if (telemetryHeader)
1806
+ c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1807
+ return c.json(response);
1808
+ }
1809
+ catch (error) {
1810
+ const status = toStatusCode(error);
1811
+ const requestId = extractRequestId(rawBody);
1812
+ logProviderError(logger, provider, "auth", "start", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry);
1813
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
1814
+ if (telemetryHeader)
1815
+ c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1816
+ return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
1817
+ }
1818
+ });
1819
+ app.post("/auth/continue", async (c) => {
1820
+ let rawBody;
1821
+ const proxyTelemetry = new ProxyTelemetryCollector();
1822
+ const requestCost = startRequestCost();
1823
+ try {
1824
+ rawBody = await c.req.raw
1825
+ .clone()
1826
+ .json()
1827
+ .catch(() => undefined);
1828
+ const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1829
+ const response = await handleAuthFlow(provider, body, "continue", options, state, proxyTelemetry, c.req.raw.signal);
1830
+ logProviderSuccess(logger, provider, "auth", "continue", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost), proxyTelemetry);
1831
+ if (response instanceof Response)
1832
+ return responseWithProviderTelemetry(response, proxyTelemetry);
1833
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
1834
+ if (telemetryHeader)
1835
+ c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1836
+ return c.json(response);
1837
+ }
1838
+ catch (error) {
1839
+ const status = toStatusCode(error);
1840
+ const requestId = extractRequestId(rawBody);
1841
+ logProviderError(logger, provider, "auth", "continue", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry);
1842
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
1843
+ if (telemetryHeader)
1844
+ c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1845
+ return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
1846
+ }
1847
+ });
1848
+ app.post("/auth/poll", async (c) => {
1849
+ let rawBody;
1850
+ const proxyTelemetry = new ProxyTelemetryCollector();
1851
+ const requestCost = startRequestCost();
1852
+ try {
1853
+ rawBody = await c.req.raw
1854
+ .clone()
1855
+ .json()
1856
+ .catch(() => undefined);
1857
+ const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1858
+ const response = await handleAuthFlow(provider, body, "poll", options, state, proxyTelemetry, c.req.raw.signal);
1859
+ logProviderSuccess(logger, provider, "auth", "poll", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost), proxyTelemetry);
1860
+ if (response instanceof Response)
1861
+ return responseWithProviderTelemetry(response, proxyTelemetry);
1862
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
1863
+ if (telemetryHeader)
1864
+ c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1865
+ return c.json(response);
1866
+ }
1867
+ catch (error) {
1868
+ const status = toStatusCode(error);
1869
+ const requestId = extractRequestId(rawBody);
1870
+ logProviderError(logger, provider, "auth", "poll", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry);
1871
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
1872
+ if (telemetryHeader)
1873
+ c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1874
+ return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
1875
+ }
1876
+ });
1877
+ app.post("/auth/refresh", async (c) => {
1878
+ let rawBody;
1879
+ const proxyTelemetry = new ProxyTelemetryCollector();
1880
+ const requestCost = startRequestCost();
1881
+ try {
1882
+ rawBody = await c.req.raw
1883
+ .clone()
1884
+ .json()
1885
+ .catch(() => undefined);
1886
+ const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1887
+ const response = await handleAuthFlow(provider, body, "refresh", options, state, proxyTelemetry, c.req.raw.signal);
1888
+ logProviderSuccess(logger, provider, "auth", "refresh", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost), proxyTelemetry);
1889
+ if (response instanceof Response)
1890
+ return responseWithProviderTelemetry(response, proxyTelemetry);
1891
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
1892
+ if (telemetryHeader)
1893
+ c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1894
+ return c.json(response);
1895
+ }
1896
+ catch (error) {
1897
+ const status = toStatusCode(error);
1898
+ const requestId = extractRequestId(rawBody);
1899
+ logProviderError(logger, provider, "auth", "refresh", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry);
1900
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
1901
+ if (telemetryHeader)
1902
+ c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1903
+ return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
1904
+ }
1905
+ });
1906
+ app.post("/auth/disconnect", async (c) => {
1907
+ let rawBody;
1908
+ const proxyTelemetry = new ProxyTelemetryCollector();
1909
+ const requestCost = startRequestCost();
1910
+ try {
1911
+ rawBody = await c.req.raw
1912
+ .clone()
1913
+ .json()
1914
+ .catch(() => undefined);
1915
+ const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1916
+ const response = await handleAuthFlow(provider, body, "abort", options, state, proxyTelemetry, c.req.raw.signal);
1917
+ logProviderSuccess(logger, provider, "auth", "disconnect", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost), proxyTelemetry);
1918
+ if (response instanceof Response)
1919
+ return responseWithProviderTelemetry(response, proxyTelemetry);
1920
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
1921
+ if (telemetryHeader)
1922
+ c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1923
+ return c.json(response);
1924
+ }
1925
+ catch (error) {
1926
+ const status = toStatusCode(error);
1927
+ const requestId = extractRequestId(rawBody);
1928
+ logProviderError(logger, provider, "auth", "disconnect", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry);
1929
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
1930
+ if (telemetryHeader)
1931
+ c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1932
+ return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
1933
+ }
1934
+ });
1935
+ return app;
1936
+ }
1937
+ function validateStatefulServerConfig(options) {
1938
+ if (options.statefulForwarding && !options.internalOperationExecutor) {
1939
+ throw new Error("Invalid provider server configuration: statefulForwarding requires internalOperationExecutor; missing option internalOperationExecutor.");
1940
+ }
1941
+ if (options.internalOperationExecutor && !options.statefulForwarding?.secret) {
1942
+ throw new Error("Invalid provider server configuration: internalOperationExecutor requires statefulForwarding.secret; missing option statefulForwarding.secret.");
1943
+ }
1944
+ if (options.statefulForwarding &&
1945
+ typeof options.statefulForwarding.validateOwnerFence !== "function") {
1946
+ throw new Error("Invalid provider server configuration: statefulForwarding requires validateOwnerFence.");
1947
+ }
1948
+ if (options.statefulForwarding?.maxSkewMs !== undefined &&
1949
+ (!Number.isFinite(options.statefulForwarding.maxSkewMs) ||
1950
+ options.statefulForwarding.maxSkewMs <= 0)) {
1951
+ throw new Error("Invalid provider server configuration: maxSkewMs must be positive.");
1952
+ }
1953
+ if (options.statefulForwarding?.replayCacheMaxEntries !== undefined &&
1954
+ (!Number.isInteger(options.statefulForwarding.replayCacheMaxEntries) ||
1955
+ options.statefulForwarding.replayCacheMaxEntries <= 0)) {
1956
+ throw new Error("Invalid provider server configuration: replayCacheMaxEntries must be a positive integer.");
1957
+ }
1958
+ }
1959
+ function getBunServeRuntime() {
1960
+ const bunValue = Object.getOwnPropertyDescriptor(globalThis, "Bun")?.value;
1961
+ if (!bunValue || typeof bunValue !== "object") {
1962
+ return undefined;
1963
+ }
1964
+ const serve = Object.getOwnPropertyDescriptor(bunValue, "serve")?.value;
1965
+ if (typeof serve !== "function") {
1966
+ return undefined;
1967
+ }
1968
+ return {
1969
+ serve(options) {
1970
+ return serve(options);
1971
+ },
1972
+ };
1973
+ }
1974
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
1975
+ const DEFAULT_SHUTDOWN_SIGNALS = ["SIGTERM", "SIGINT"];
1976
+ const processSignalCoordinators = new Map();
1977
+ export async function serve(provider, options = {}) {
1978
+ const bunRuntime = getBunServeRuntime();
1979
+ if (bunRuntime === undefined) {
1980
+ throw new ProviderError("Bun runtime is required to start the provider server", {
1981
+ code: "RUNTIME_UNSUPPORTED",
1982
+ });
1983
+ }
1984
+ const logger = options.logger ?? defaultProviderServerLogger;
1985
+ const configuredTimeoutMs = shutdownTimeout(options.shutdown?.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS);
1986
+ const configuredSignals = resolveShutdownSignals(options.shutdown?.signals ?? true);
1987
+ const selfTestSecrets = resolveSelfTestMasterSecrets();
1988
+ const serverAppOptions = {
1989
+ logger: options.logger,
1990
+ ocr: options.ocr,
1991
+ stt: options.stt,
1992
+ resolver: options.resolver,
1993
+ state: options.state,
1994
+ allowMemoryStateFallback: options.allowMemoryStateFallback,
1995
+ operationExecutor: options.operationExecutor,
1996
+ internalOperationExecutor: options.internalOperationExecutor,
1997
+ statefulForwarding: options.statefulForwarding,
1998
+ };
1999
+ const [app, selfTestModule] = await Promise.all([
2000
+ createServerAppAsync(provider, serverAppOptions),
2001
+ selfTestSecrets ? import("./self-test.js") : undefined,
2002
+ ]);
2003
+ const servers = [];
2004
+ try {
2005
+ servers.push(bunRuntime.serve({
2006
+ port: options.port ?? DEFAULT_PORT,
2007
+ hostname: options.host ?? DEFAULT_HOST,
2008
+ fetch: app.fetch,
2009
+ }));
2010
+ // Internal self-test listener (health dependency inversion): a SEPARATE
2011
+ // socket the tenant-facing gateway never dials. Off by default — it only
2012
+ // starts when the shared self-test master secret env is present.
2013
+ if (selfTestSecrets && selfTestModule) {
2014
+ const selfTestApp = selfTestModule.createSelfTestApp(provider, {
2015
+ secrets: selfTestSecrets,
2016
+ invoke: selfTestModule.createSelfTestInvoke(app),
2017
+ authFlow: selfTestModule.createSelfTestAuthFlowInvoke(app),
2018
+ logger,
2019
+ });
2020
+ servers.push(bunRuntime.serve({
2021
+ port: options.selfTestPort ?? selfTestModule.resolveSelfTestPort(),
2022
+ hostname: options.host ?? DEFAULT_HOST,
2023
+ fetch: selfTestApp.fetch,
2024
+ }));
2025
+ }
2026
+ }
2027
+ catch (error) {
2028
+ await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
2029
+ throw error;
2030
+ }
2031
+ const server = servers[0];
2032
+ if (!server)
2033
+ throw new Error("Provider server failed to create its primary listener.");
2034
+ let closePromise;
2035
+ let unregisterSignals = () => { };
2036
+ const close = (closeOptions = {}) => {
2037
+ if (closePromise)
2038
+ return closePromise;
2039
+ const timeoutMs = shutdownTimeout(closeOptions.timeoutMs ?? configuredTimeoutMs);
2040
+ closePromise = closeProviderServers({
2041
+ servers,
2042
+ hooks: options.shutdown?.hooks ?? [],
2043
+ timeoutMs,
2044
+ logger,
2045
+ providerId: provider.id,
2046
+ }).finally(() => unregisterSignals());
2047
+ return closePromise;
2048
+ };
2049
+ try {
2050
+ unregisterSignals = registerForProcessSignals(configuredSignals, () => close({ timeoutMs: configuredTimeoutMs }));
2051
+ }
2052
+ catch (error) {
2053
+ unregisterSignals();
2054
+ await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
2055
+ throw error;
2056
+ }
2057
+ return { port: server.port, close };
2058
+ }
2059
+ function registerForProcessSignals(signals, close) {
2060
+ if (signals.length === 0)
2061
+ return () => { };
2062
+ const registration = {
2063
+ signals: new Set(signals),
2064
+ close,
2065
+ };
2066
+ let registered = true;
2067
+ const unregister = () => {
2068
+ if (!registered)
2069
+ return;
2070
+ registered = false;
2071
+ for (const signal of registration.signals) {
2072
+ const coordinator = processSignalCoordinators.get(signal);
2073
+ if (!coordinator)
2074
+ continue;
2075
+ coordinator.registrations.delete(registration);
2076
+ if (coordinator.registrations.size === 0 && !coordinator.handling) {
2077
+ process.removeListener(signal, coordinator.listener);
2078
+ processSignalCoordinators.delete(signal);
2079
+ }
2080
+ }
2081
+ };
2082
+ try {
2083
+ for (const signal of signals) {
2084
+ let coordinator = processSignalCoordinators.get(signal);
2085
+ if (!coordinator) {
2086
+ const created = {
2087
+ registrations: new Set(),
2088
+ handling: false,
2089
+ listener: () => handleCoordinatedSignal(signal, created),
2090
+ };
2091
+ coordinator = created;
2092
+ processSignalCoordinators.set(signal, coordinator);
2093
+ process.on(signal, coordinator.listener);
2094
+ }
2095
+ coordinator.registrations.add(registration);
2096
+ }
2097
+ }
2098
+ catch (error) {
2099
+ unregister();
2100
+ throw error;
2101
+ }
2102
+ return unregister;
2103
+ }
2104
+ function handleCoordinatedSignal(signal, coordinator) {
2105
+ if (coordinator.handling)
2106
+ return;
2107
+ coordinator.handling = true;
2108
+ const registrations = [...coordinator.registrations];
2109
+ void Promise.allSettled(registrations.map((registration) => registration.close())).finally(() => {
2110
+ if (processSignalCoordinators.get(signal) === coordinator) {
2111
+ process.removeListener(signal, coordinator.listener);
2112
+ processSignalCoordinators.delete(signal);
2113
+ }
2114
+ try {
2115
+ process.kill(process.pid, signal);
2116
+ }
2117
+ catch {
2118
+ process.exitCode = 1;
2119
+ }
2120
+ });
2121
+ }
2122
+ async function closeProviderServers(input) {
2123
+ const deadline = Date.now() + input.timeoutMs;
2124
+ const gracefulStops = input.servers.map((server) => server.stop(false));
2125
+ for (const gracefulStop of gracefulStops)
2126
+ gracefulStop.catch(() => undefined);
2127
+ for (const [hookIndex, hook] of input.hooks.entries()) {
2128
+ try {
2129
+ await withinShutdownBudget(Promise.resolve().then(hook), deadline);
2130
+ }
2131
+ catch (error) {
2132
+ try {
2133
+ input.logger({
2134
+ level: "error",
2135
+ event: "provider_shutdown_hook_failed",
2136
+ providerId: input.providerId,
2137
+ hookIndex,
2138
+ errorClass: error instanceof Error ? error.name : "UnknownError",
2139
+ message: error instanceof Error ? error.message : "Shutdown hook failed.",
2140
+ });
2141
+ }
2142
+ catch { }
2143
+ }
2144
+ }
2145
+ const forcedStops = input.servers.map((server) => server.stop(true));
2146
+ await withinShutdownBudget(Promise.allSettled([...gracefulStops, ...forcedStops]).then(() => undefined), deadline).catch(() => undefined);
2147
+ }
2148
+ async function withinShutdownBudget(promise, deadline) {
2149
+ promise.catch(() => undefined);
2150
+ const remainingMs = Math.max(0, deadline - Date.now());
2151
+ let timer;
2152
+ const timeout = new Promise((_resolve, reject) => {
2153
+ timer = setTimeout(() => reject(new Error("Provider server shutdown timed out.")), remainingMs);
2154
+ });
2155
+ try {
2156
+ return await Promise.race([promise, timeout]);
2157
+ }
2158
+ finally {
2159
+ if (timer)
2160
+ clearTimeout(timer);
2161
+ }
2162
+ }
2163
+ function resolveShutdownSignals(signals) {
2164
+ if (signals === false)
2165
+ return [];
2166
+ return [...new Set(signals === true ? DEFAULT_SHUTDOWN_SIGNALS : signals)];
2167
+ }
2168
+ function shutdownTimeout(value) {
2169
+ if (!Number.isFinite(value) || value < 0) {
2170
+ throw new Error("Provider server shutdown timeoutMs must be a non-negative finite number.");
2171
+ }
2172
+ return value;
2173
+ }