@apifuse/provider-sdk 2.2.0-beta.2 → 2.2.0-beta.20

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 (290) hide show
  1. package/AUTHORING.md +487 -0
  2. package/CHANGELOG.md +85 -0
  3. package/README.md +49 -3
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +44 -59
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +27 -52
  8. package/bin/apifuse-pack-check.ts +14 -0
  9. package/bin/apifuse-pack-smoke.ts +36 -81
  10. package/bin/apifuse-pack-types.ts +305 -0
  11. package/bin/apifuse-perf.ts +45 -127
  12. package/bin/apifuse-record.ts +659 -111
  13. package/bin/apifuse-submit-check.ts +546 -48
  14. package/bin/apifuse-sync-assets.ts +117 -0
  15. package/bin/apifuse.ts +1 -1
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/bin/submit-check-xml-semantics.ts +204 -0
  18. package/bin/submit-check-xml.ts +134 -0
  19. package/dist/auth-turn/index.d.ts +3 -3
  20. package/dist/auth.d.ts +2 -2
  21. package/dist/auth.js +9 -18
  22. package/dist/ceremonies/index.d.ts +1 -1
  23. package/dist/ceremonies/index.js +8 -18
  24. package/dist/cli/commands.d.ts +1 -1
  25. package/dist/cli/commands.js +8 -0
  26. package/dist/cli/create.d.ts +3 -0
  27. package/dist/cli/create.js +34 -35
  28. package/dist/cli/prompt-assets.d.ts +80 -0
  29. package/dist/cli/prompt-assets.js +743 -0
  30. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  31. package/dist/config/loader.d.ts +176 -8
  32. package/dist/config/loader.js +424 -95
  33. package/dist/contract-serialization.d.ts +2 -2
  34. package/dist/contract-serialization.js +3 -6
  35. package/dist/contract-types.d.ts +2 -2
  36. package/dist/contract.d.ts +3 -3
  37. package/dist/contract.js +4 -6
  38. package/dist/define.d.ts +9 -1
  39. package/dist/define.js +208 -125
  40. package/dist/dev.d.ts +1 -1
  41. package/dist/dev.js +1 -1
  42. package/dist/error-resolution.d.ts +2 -0
  43. package/dist/error-resolution.js +90 -0
  44. package/dist/errors.d.ts +22 -1
  45. package/dist/errors.js +88 -0
  46. package/dist/fixture-sanitization.d.ts +26 -0
  47. package/dist/fixture-sanitization.js +216 -0
  48. package/dist/i18n/catalog.d.ts +2 -2
  49. package/dist/i18n/catalog.js +4 -10
  50. package/dist/i18n/index.d.ts +2 -2
  51. package/dist/i18n/index.js +2 -2
  52. package/dist/i18n/keys.d.ts +2 -2
  53. package/dist/index.d.ts +46 -42
  54. package/dist/index.js +41 -37
  55. package/dist/lint.js +8 -15
  56. package/dist/native-address.d.ts +43 -0
  57. package/dist/native-address.js +281 -0
  58. package/dist/native-egress-policy.d.ts +31 -0
  59. package/dist/native-egress-policy.js +288 -0
  60. package/dist/observability.d.ts +5 -2
  61. package/dist/observability.js +48 -1
  62. package/dist/provider.d.ts +12 -11
  63. package/dist/provider.js +10 -9
  64. package/dist/public-schema-field-lint.d.ts +1 -1
  65. package/dist/recipes/gov-api.js +1 -1
  66. package/dist/runtime/auth-flow.d.ts +1 -1
  67. package/dist/runtime/auth-flow.js +3 -3
  68. package/dist/runtime/browser.d.ts +1 -1
  69. package/dist/runtime/browser.js +15 -29
  70. package/dist/runtime/cache.d.ts +1 -1
  71. package/dist/runtime/cache.js +4 -8
  72. package/dist/runtime/choice.d.ts +1 -1
  73. package/dist/runtime/choice.js +31 -35
  74. package/dist/runtime/credential.d.ts +1 -1
  75. package/dist/runtime/credential.js +1 -1
  76. package/dist/runtime/env.d.ts +1 -1
  77. package/dist/runtime/executor.d.ts +1 -1
  78. package/dist/runtime/executor.js +31 -4
  79. package/dist/runtime/http.d.ts +2 -2
  80. package/dist/runtime/http.js +387 -47
  81. package/dist/runtime/insights.d.ts +1 -1
  82. package/dist/runtime/insights.js +6 -13
  83. package/dist/runtime/instrumentation.d.ts +2 -2
  84. package/dist/runtime/instrumentation.js +345 -22
  85. package/dist/runtime/keyring.js +1 -1
  86. package/dist/runtime/namespace.js +1 -1
  87. package/dist/runtime/native-network.d.ts +127 -0
  88. package/dist/runtime/native-network.js +1298 -0
  89. package/dist/runtime/otlp.d.ts +1 -1
  90. package/dist/runtime/perf.d.ts +1 -1
  91. package/dist/runtime/provider.d.ts +1 -1
  92. package/dist/runtime/provider.js +1 -2
  93. package/dist/runtime/proxy-errors.d.ts +1 -1
  94. package/dist/runtime/proxy-errors.js +9 -7
  95. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  96. package/dist/runtime/proxy-nodemaven.js +146 -0
  97. package/dist/runtime/proxy-retry-policy.d.ts +2 -2
  98. package/dist/runtime/proxy-retry-policy.js +2 -2
  99. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  100. package/dist/runtime/proxy-telemetry.js +55 -52
  101. package/dist/runtime/redirects.d.ts +29 -0
  102. package/dist/runtime/redirects.js +36 -0
  103. package/dist/runtime/redis.d.ts +1 -1
  104. package/dist/runtime/redis.js +2 -4
  105. package/dist/runtime/request-options.d.ts +68 -1
  106. package/dist/runtime/request-options.js +548 -0
  107. package/dist/runtime/secrets.d.ts +27 -0
  108. package/dist/runtime/secrets.js +51 -0
  109. package/dist/runtime/state.d.ts +2 -2
  110. package/dist/runtime/state.js +15 -4
  111. package/dist/runtime/stealth.d.ts +5 -3
  112. package/dist/runtime/stealth.js +423 -143
  113. package/dist/runtime/stt.d.ts +1 -1
  114. package/dist/runtime/stt.js +11 -15
  115. package/dist/runtime/trace.d.ts +2 -2
  116. package/dist/runtime/trace.js +2 -4
  117. package/dist/runtime/waterfall.d.ts +1 -1
  118. package/dist/schema.d.ts +1 -1
  119. package/dist/schema.js +7 -15
  120. package/dist/serve.d.ts +1 -1
  121. package/dist/serve.js +1 -1
  122. package/dist/server/index.d.ts +7 -7
  123. package/dist/server/index.js +6 -6
  124. package/dist/server/self-test-input-tokens.d.ts +2 -1
  125. package/dist/server/self-test-input-tokens.js +18 -14
  126. package/dist/server/self-test-redaction.d.ts +1 -1
  127. package/dist/server/self-test-redaction.js +1 -1
  128. package/dist/server/self-test.d.ts +104 -3
  129. package/dist/server/self-test.js +673 -115
  130. package/dist/server/serve.d.ts +115 -4
  131. package/dist/server/serve.js +792 -127
  132. package/dist/server/types.d.ts +30 -9
  133. package/dist/server/types.js +8 -7
  134. package/dist/stateful/errors.d.ts +14 -0
  135. package/dist/stateful/errors.js +14 -0
  136. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  137. package/dist/stateful/http-provider-event-emitter.js +237 -0
  138. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  139. package/dist/stateful/http-session-owner-registry.js +210 -0
  140. package/dist/stateful/index.d.ts +18 -0
  141. package/dist/stateful/index.js +18 -0
  142. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  143. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  144. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  145. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  146. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  147. package/dist/stateful/provider-event-pipeline.js +1 -0
  148. package/dist/stateful/provider-events.d.ts +101 -0
  149. package/dist/stateful/provider-events.js +289 -0
  150. package/dist/stateful/session-key.d.ts +15 -0
  151. package/dist/stateful/session-key.js +86 -0
  152. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  153. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  154. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  155. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  156. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  157. package/dist/stateful/stateful-provider-adapter.js +287 -0
  158. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  159. package/dist/stateful/stateful-provider-observability.js +161 -0
  160. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  161. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  162. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  163. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  164. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  165. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  166. package/dist/stateful/stateful-provider-session-routing.d.ts +71 -0
  167. package/dist/stateful/stateful-provider-session-routing.js +353 -0
  168. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  169. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  170. package/dist/stateful-signing.d.ts +18 -0
  171. package/dist/stateful-signing.js +27 -0
  172. package/dist/stealth/profiles.d.ts +1 -1
  173. package/dist/stealth/profiles.js +5 -14
  174. package/dist/stream-evidence.d.ts +74 -0
  175. package/dist/stream-evidence.js +785 -0
  176. package/dist/stream.d.ts +1 -1
  177. package/dist/testing/index.d.ts +2 -2
  178. package/dist/testing/index.js +2 -2
  179. package/dist/testing/run.d.ts +32 -2
  180. package/dist/testing/run.js +477 -33
  181. package/dist/types.d.ts +323 -11
  182. package/dist/types.js +1 -0
  183. package/dist/user-input.d.ts +30 -0
  184. package/dist/user-input.js +66 -0
  185. package/package.json +16 -5
  186. package/src/auth-turn/index.ts +1 -1
  187. package/src/auth.ts +28 -86
  188. package/src/ceremonies/index.ts +24 -78
  189. package/src/cli/commands.ts +10 -0
  190. package/src/cli/create.ts +42 -35
  191. package/src/cli/prompt-assets.ts +865 -0
  192. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  193. package/src/config/loader.ts +652 -204
  194. package/src/contract-serialization.ts +5 -11
  195. package/src/contract-types.ts +2 -2
  196. package/src/contract.ts +12 -28
  197. package/src/define.ts +368 -504
  198. package/src/dev.ts +4 -9
  199. package/src/error-resolution.ts +91 -0
  200. package/src/errors.ts +110 -5
  201. package/src/fixture-sanitization.ts +247 -0
  202. package/src/i18n/catalog.ts +10 -32
  203. package/src/i18n/index.ts +2 -2
  204. package/src/i18n/keys.ts +5 -11
  205. package/src/index.ts +111 -42
  206. package/src/lint.ts +89 -159
  207. package/src/native-address.ts +340 -0
  208. package/src/native-egress-policy.ts +358 -0
  209. package/src/observability.ts +51 -1
  210. package/src/provider.ts +65 -11
  211. package/src/public-schema-field-lint.ts +7 -33
  212. package/src/recipes/gov-api.ts +2 -5
  213. package/src/runtime/auth-flow.ts +5 -7
  214. package/src/runtime/browser.ts +64 -187
  215. package/src/runtime/cache.ts +20 -67
  216. package/src/runtime/choice.ts +79 -132
  217. package/src/runtime/credential.ts +2 -2
  218. package/src/runtime/env.ts +1 -1
  219. package/src/runtime/executor.ts +43 -20
  220. package/src/runtime/http.ts +494 -57
  221. package/src/runtime/insights.ts +15 -53
  222. package/src/runtime/instrumentation.ts +495 -66
  223. package/src/runtime/keyring.ts +7 -19
  224. package/src/runtime/namespace.ts +2 -7
  225. package/src/runtime/native-network.ts +1686 -0
  226. package/src/runtime/otlp.ts +12 -23
  227. package/src/runtime/perf.ts +1 -1
  228. package/src/runtime/provider.ts +4 -9
  229. package/src/runtime/proxy-errors.ts +29 -42
  230. package/src/runtime/proxy-nodemaven.ts +221 -0
  231. package/src/runtime/proxy-retry-policy.ts +3 -3
  232. package/src/runtime/proxy-telemetry.ts +79 -77
  233. package/src/runtime/redirects.ts +66 -0
  234. package/src/runtime/redis.ts +4 -12
  235. package/src/runtime/request-options.ts +679 -9
  236. package/src/runtime/secrets.ts +64 -0
  237. package/src/runtime/state.ts +41 -110
  238. package/src/runtime/stealth.ts +505 -154
  239. package/src/runtime/stt.ts +38 -94
  240. package/src/runtime/trace.ts +14 -44
  241. package/src/runtime/waterfall.ts +5 -18
  242. package/src/schema.ts +23 -84
  243. package/src/serve.ts +1 -1
  244. package/src/server/index.ts +29 -7
  245. package/src/server/self-test-input-tokens.ts +29 -14
  246. package/src/server/self-test-redaction.ts +2 -2
  247. package/src/server/self-test.ts +857 -132
  248. package/src/server/serve.ts +1141 -324
  249. package/src/server/types.ts +12 -19
  250. package/src/stateful/README.md +146 -0
  251. package/src/stateful/errors.ts +23 -0
  252. package/src/stateful/http-provider-event-emitter.ts +314 -0
  253. package/src/stateful/http-session-owner-registry.ts +306 -0
  254. package/src/stateful/index.ts +18 -0
  255. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  256. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  257. package/src/stateful/provider-event-pipeline.ts +61 -0
  258. package/src/stateful/provider-events.ts +462 -0
  259. package/src/stateful/session-key.ts +111 -0
  260. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  261. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  262. package/src/stateful/stateful-provider-adapter.ts +562 -0
  263. package/src/stateful/stateful-provider-observability.ts +261 -0
  264. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  265. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  266. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  267. package/src/stateful/stateful-provider-session-routing.ts +555 -0
  268. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  269. package/src/stateful-signing.ts +46 -0
  270. package/src/stealth/profiles.ts +10 -26
  271. package/src/stream-evidence.ts +988 -0
  272. package/src/stream.ts +8 -19
  273. package/src/testing/index.ts +10 -2
  274. package/src/testing/run.ts +652 -79
  275. package/src/types.ts +369 -11
  276. package/src/user-input.ts +118 -0
  277. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  278. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  279. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  280. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  281. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  282. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  283. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  284. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  285. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  286. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  287. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  288. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  289. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  290. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -2,36 +2,72 @@ import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { Hono } from "hono";
4
4
  import { z } from "zod";
5
- import { AuthAbortError, createAuthFlowHelpers } from "../auth";
6
- import { AuthError, ProviderError, SessionExpiredError, TransportError, } from "../errors";
7
- import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog";
8
- import { categoryForStatus, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability";
9
- import { createScratchpad } from "../runtime/auth-flow";
10
- import { createBrowserClient } from "../runtime/browser";
11
- import { createProviderCache } from "../runtime/cache";
12
- import { createProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "../runtime/choice";
13
- import { createCredentialContext } from "../runtime/credential";
14
- import { createEnvContext } from "../runtime/env";
15
- import { executeOperation } from "../runtime/executor";
16
- import { createHttpClient } from "../runtime/http";
17
- import { wrapWithInstrumentation } from "../runtime/instrumentation";
18
- import { getProviderBaseUrl } from "../runtime/provider";
19
- import { PROXY_AUTH_IP_DENIED_CODE, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_EXHAUSTED_CODE, } from "../runtime/proxy-errors";
20
- import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector, } from "../runtime/proxy-telemetry";
21
- import { createProviderRuntimeStateFromEnv, createUnsupportedProviderRuntimeState, } from "../runtime/state";
22
- import { createStealthClient } from "../runtime/stealth";
23
- import { createSttClientFromEnv } from "../runtime/stt";
24
- import { createTraceContext } from "../runtime/trace";
25
- import { parseSchema } from "../schema";
26
- import { getStealthProfile } from "../stealth/profiles";
27
- import { APIFUSE_STREAM_DONE_EVENT, APIFUSE_STREAM_ERROR_EVENT, encodeSseEvent, error as streamError, } from "../stream";
28
- import { createSelfTestApp, createSelfTestInvoke, resolveSelfTestPort, } from "./self-test";
29
- import { resolveSelfTestMasterSecrets } from "./self-test-token";
30
- import { AuthFlowRequestSchema, OperationRequestSchema, } from "./types";
5
+ import { AuthAbortError, createAuthFlowHelpers } from "../auth.js";
6
+ import { SDK_OWNED_PROVIDER_ERROR_CODES, SDK_RUNTIME_OWNED_ERROR_CODES, } from "../error-resolution.js";
7
+ import { AuthError, isProviderError, isSessionExpiredError, isTransportError, isValidationError, ProviderError, } from "../errors.js";
8
+ import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.js";
9
+ import { categoryForStatus, sourceForCategory, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
10
+ import { createScratchpad } from "../runtime/auth-flow.js";
11
+ import { createBrowserClient } from "../runtime/browser.js";
12
+ import { createProviderCache } from "../runtime/cache.js";
13
+ import { createProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "../runtime/choice.js";
14
+ import { createCredentialContext } from "../runtime/credential.js";
15
+ import { createEnvContext } from "../runtime/env.js";
16
+ import { executeOperation } from "../runtime/executor.js";
17
+ import { createHttpClient } from "../runtime/http.js";
18
+ import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
19
+ import { createEnvVendorCredentialResolver, createNativeNetworkClient, } from "../runtime/native-network.js";
20
+ import { getProviderBaseUrl } from "../runtime/provider.js";
21
+ import { PROXY_AUTH_IP_DENIED_CODE, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_EXHAUSTED_CODE, } from "../runtime/proxy-errors.js";
22
+ import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector } from "../runtime/proxy-telemetry.js";
23
+ import { assertRequiredSecretsPresent, listMissingRequiredSecrets, MISSING_SECRET_CODE, } from "../runtime/secrets.js";
24
+ import { createProviderRuntimeStateFromEnv, createUnsupportedProviderRuntimeState, } from "../runtime/state.js";
25
+ import { createStealthClient } from "../runtime/stealth.js";
26
+ import { createSttClientFromEnv } from "../runtime/stt.js";
27
+ import { createTraceContext } from "../runtime/trace.js";
28
+ import { parseSchema } from "../schema.js";
29
+ 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";
30
+ import { StatefulRoutingDeadlineError } from "../stateful/stateful-provider-session-routing.js";
31
+ import { getStealthProfile } from "../stealth/profiles.js";
32
+ import { APIFUSE_STREAM_DONE_EVENT, APIFUSE_STREAM_ERROR_EVENT, encodeSseEvent, error as streamError, } from "../stream.js";
33
+ import { VALID_OPERATION_ERROR_STATUSES } from "../types.js";
34
+ import { createSelfTestApp, createSelfTestAuthFlowInvoke, createSelfTestInvoke, resolveSelfTestPort, } from "./self-test.js";
35
+ import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
36
+ import { AuthFlowRequestSchema, OperationConnectionSchema, OperationRequestSchema, } from "./types.js";
31
37
  const DEFAULT_HOST = "0.0.0.0";
32
38
  const DEFAULT_PORT = 3000;
39
+ /** Compact SDK-owned error classification emitted separately from the public response body. */
40
+ export const ERROR_OBSERVABILITY_HEADER = "X-ApiFuse-Error-Observability";
33
41
  const AUTH_FLOW_LOCALES = ["en", "ko", "ja"];
34
42
  const retryResponseMeta = new WeakMap();
43
+ const STATEFUL_INTERNAL_OPERATIONS_ROUTE = "/__apifuse/stateful/operations";
44
+ const STATEFUL_FORWARDING_SOURCE_POD_HEADER = "x-apifuse-stateful-source-pod";
45
+ const DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS = 5 * 60_000;
46
+ const DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES = 10_000;
47
+ const STATEFUL_FORWARDING_REPLAY_BUCKET_MS = 10_000;
48
+ const STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS = Math.ceil(STATEFUL_FORWARDING_REPLAY_BUCKET_MS / 1_000);
49
+ export const ProviderServerStatefulForwardEnvelopeSchema = z
50
+ .object({
51
+ requestId: z.string().min(1),
52
+ providerId: z.string().min(1),
53
+ operationId: z.string().min(1),
54
+ sessionKey: z.string().min(1),
55
+ connectionId: z.string().min(1),
56
+ serviceAccountId: z.string().min(1),
57
+ ownerPodId: z.string().min(1),
58
+ generation: z.number().int().positive(),
59
+ sourcePodId: z.string().min(1),
60
+ forwardedAt: z.string().refine((value) => Number.isFinite(Date.parse(value))),
61
+ deadlineAt: z
62
+ .string()
63
+ .refine((value) => Number.isFinite(Date.parse(value)))
64
+ .optional(),
65
+ idempotencyKey: z.string().min(1).optional(),
66
+ operationRequest: OperationRequestSchema.extend({
67
+ connection: OperationConnectionSchema.strict().optional(),
68
+ }).strict(),
69
+ })
70
+ .strict();
35
71
  function createAuthStub() {
36
72
  return {
37
73
  async requestField(name) {
@@ -93,9 +129,7 @@ function getProviderStealthBaseUrl(provider) {
93
129
  return firstHost ? `https://${firstHost}` : undefined;
94
130
  }
95
131
  function getProviderStealthProfile(provider) {
96
- return provider.stealth?.profile
97
- ? getStealthProfile(provider.stealth.profile)
98
- : undefined;
132
+ return provider.stealth?.profile ? getStealthProfile(provider.stealth.profile) : undefined;
99
133
  }
100
134
  function isProductionProviderBrowserMode(provider, env = process.env) {
101
135
  if (provider.runtime !== "browser") {
@@ -104,13 +138,11 @@ function isProductionProviderBrowserMode(provider, env = process.env) {
104
138
  if (env.APIFUSE__PROVIDER__RUNTIME === "browser") {
105
139
  return true;
106
140
  }
107
- return (env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id);
141
+ return env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id;
108
142
  }
109
143
  export function resolveProviderProxyAffinityKey(provider, request, operationId) {
110
144
  const connectionKey = resolveOperationConnectionId(request) ?? request.connection?.externalRef;
111
- const affinity = typeof provider.proxy === "object"
112
- ? provider.proxy.session?.affinity
113
- : undefined;
145
+ const affinity = typeof provider.proxy === "object" ? provider.proxy.session?.affinity : undefined;
114
146
  if (affinity === "operation") {
115
147
  return `${provider.id}/${operationId}`;
116
148
  }
@@ -119,6 +151,15 @@ export function resolveProviderProxyAffinityKey(provider, request, operationId)
119
151
  function resolveOperationConnectionId(request) {
120
152
  return request.connection?.id ?? request.connectionId;
121
153
  }
154
+ function resolveNativeProxyPolicy(provider) {
155
+ if (typeof provider.proxy === "object")
156
+ return provider.proxy;
157
+ if (provider.proxy === true)
158
+ return { mode: "optional" };
159
+ if (provider.proxy === false)
160
+ return { mode: "disabled" };
161
+ return undefined;
162
+ }
122
163
  function createProviderContext(provider, request, operationId, options = {}, state = createUnsupportedProviderRuntimeState(), proxyTelemetry) {
123
164
  const baseUrl = getProviderBaseUrl(provider);
124
165
  const stealthBaseUrl = getProviderStealthBaseUrl(provider);
@@ -177,6 +218,18 @@ function createProviderContext(provider, request, operationId, options = {}, sta
177
218
  engine: provider.browser?.engine,
178
219
  })
179
220
  : createBrowserStub(),
221
+ ...(provider.native
222
+ ? {
223
+ native: {
224
+ network: createNativeNetworkClient({
225
+ egress: provider.native.network,
226
+ proxyPolicy: resolveNativeProxyPolicy(provider),
227
+ affinityKey: proxyClientOptions.affinityKey,
228
+ credentials: createEnvVendorCredentialResolver(env),
229
+ }),
230
+ },
231
+ }
232
+ : {}),
180
233
  trace: createTraceContext(),
181
234
  auth: createAuthStub(),
182
235
  stt: options.stt ?? createSttClientFromEnv(provider.stt),
@@ -253,6 +306,18 @@ function createAuthFlowContext(provider, request, options = {}, signal) {
253
306
  ? createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
254
307
  : createStealthClient(stealthBaseUrl, stealthClientOptions)
255
308
  : createStealthStub(),
309
+ ...(provider.native
310
+ ? {
311
+ native: {
312
+ network: createNativeNetworkClient({
313
+ egress: provider.native.network,
314
+ proxyPolicy: resolveNativeProxyPolicy(provider),
315
+ affinityKey: proxyClientOptions.affinityKey,
316
+ credentials: createEnvVendorCredentialResolver(createEnvContext(provider.secrets?.map((secret) => secret.name))),
317
+ }),
318
+ },
319
+ }
320
+ : {}),
256
321
  env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
257
322
  credential,
258
323
  context: flowContextStore.context,
@@ -292,16 +357,50 @@ function zodDetails(error) {
292
357
  message: issue.message,
293
358
  }));
294
359
  }
295
- function toErrorResponse(error, requestId) {
296
- if (error instanceof ProviderError) {
297
- const details = publicProviderErrorDetails(error);
360
+ // Category-level projection with code-aware honesty overrides: a missing
361
+ // deployment secret is an APIFuse-side defect even though its category
362
+ // (credential_unavailable) usually means a caller credential problem, an
363
+ // internal stateful-routing deadline is APIFuse-owned despite its timeout
364
+ // category, and the built-in upstream failure families keep their upstream
365
+ // attribution even when the author left the category at the provider_error
366
+ // default.
367
+ function publicErrorSource(error, category) {
368
+ if (error instanceof StatefulRoutingDeadlineError)
369
+ return "apifuse";
370
+ if (isProviderError(error)) {
371
+ if (error.code === MISSING_SECRET_CODE)
372
+ return "apifuse";
373
+ if (error.code === "UPSTREAM_ERROR" || error.code === "BLOCKED") {
374
+ return "upstream_failure";
375
+ }
376
+ }
377
+ return sourceForCategory(category);
378
+ }
379
+ function toErrorResponse(error, requestId, declaredErrorCode) {
380
+ const observability = errorObservabilityDetails(error, declaredErrorCode);
381
+ const source = publicErrorSource(error, observability.category);
382
+ if (error instanceof StatefulRoutingDeadlineError) {
383
+ return {
384
+ error: {
385
+ code: "STATEFUL_FORWARDING_DEADLINE_EXPIRED",
386
+ message: "Stateful forwarding deadline expired.",
387
+ ...(requestId ? { requestId } : {}),
388
+ retryable: observability.retryable,
389
+ source,
390
+ },
391
+ };
392
+ }
393
+ if (isProviderError(error)) {
394
+ const details = error.details;
298
395
  return {
299
396
  error: {
300
397
  code: error.code ?? "provider_error",
301
398
  message: publicProviderErrorMessage(error),
302
399
  ...(requestId ? { requestId } : {}),
400
+ retryable: observability.retryable,
401
+ source,
303
402
  ...(error.fix ? { fix: error.fix } : {}),
304
- ...(details ? { details } : {}),
403
+ ...(details !== undefined ? { details } : {}),
305
404
  },
306
405
  };
307
406
  }
@@ -311,52 +410,67 @@ function toErrorResponse(error, requestId) {
311
410
  code: "invalid_request",
312
411
  message: "Invalid request body",
313
412
  ...(requestId ? { requestId } : {}),
413
+ retryable: observability.retryable,
414
+ source,
314
415
  details: zodDetails(error),
315
416
  },
316
417
  };
317
418
  }
419
+ // A masked internal error MUST NOT be advertised as retryable: without an
420
+ // explicit retryable:false the hub (bori provider-backed engine) defaults 5xx
421
+ // to retryable:true, which turns a deterministic pre-upstream crash into an
422
+ // infinite START->CONTINUE->restart loop (2026-07-22 catchtable reserve RCA).
423
+ // We still refuse to leak message/stack — only the error class name (or the
424
+ // primitive type for non-Error throwables) is surfaced for ops triage.
318
425
  return {
319
426
  error: {
320
427
  code: "internal_error",
321
428
  message: "Internal error",
322
429
  ...(requestId ? { requestId } : {}),
430
+ retryable: observability.retryable,
431
+ source,
432
+ details: {
433
+ retryable: false,
434
+ category: "internal_error",
435
+ errorClass: error instanceof Error ? error.name : typeof error,
436
+ },
323
437
  },
324
438
  };
325
439
  }
326
- function publicProviderErrorDetails(error) {
327
- const providerDetails = error.details;
328
- const observabilityDetails = providerObservabilityDetails(error);
329
- if (providerDetails === undefined) {
330
- return observabilityDetails;
331
- }
332
- if (observabilityDetails === undefined) {
333
- return providerDetails;
334
- }
335
- if (isPlainRecord(providerDetails) && isPlainRecord(observabilityDetails)) {
336
- return { ...providerDetails, ...observabilityDetails };
337
- }
338
- return {
339
- provider: providerDetails,
340
- observability: observabilityDetails,
341
- };
342
- }
343
- function isPlainRecord(value) {
344
- return value !== null && typeof value === "object" && !Array.isArray(value);
345
- }
346
- function providerObservabilityDetails(error) {
440
+ // Accepts `unknown` so the branded guards narrow cleanly from the top: the
441
+ // subtype error classes are structurally compatible with ProviderError, so
442
+ // narrowing from a ProviderError-typed value would collapse the negative branch
443
+ // to `never`. Narrowing from unknown avoids that while still recognizing errors
444
+ // from a duplicate SDK module instance.
445
+ function providerObservabilityDetails(error, declaredErrorCode) {
446
+ const declaredRetryable = sdkOwnsErrorResolution(error)
447
+ ? undefined
448
+ : declaredErrorCode?.retryable;
347
449
  // Session-expiry surfaces the credential_expired category + the opt-in
348
450
  // retryable signal so Gateway/Credential Service can refresh and re-drive the
349
451
  // operation (see design.md §4.3 D3). Without this branch the auth error would
350
452
  // serialize as a bare 401 with no retryable/category, losing the refresh
351
453
  // signal for exactly the retryOnAuthRefresh operations it is meant to enable.
352
- if (error instanceof SessionExpiredError) {
454
+ if (isSessionExpiredError(error)) {
353
455
  return {
354
456
  category: error.options?.category ?? "credential_expired",
355
457
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
356
- retryable: error.options?.retryable ?? false,
458
+ retryable: error.options?.retryable ?? declaredRetryable ?? false,
459
+ };
460
+ }
461
+ // Missing-secret errors carry the canonical credential_unavailable category
462
+ // so Gateway/observability can attribute the failure to provisioning, not
463
+ // the upstream. Matched by code (not constructor) so both the SDK-owned
464
+ // runtime gate and any not-yet-migrated provider-thrown MISSING_SECRET
465
+ // serialize identically, including across duplicate SDK module instances.
466
+ if (isProviderError(error) && error.code === MISSING_SECRET_CODE) {
467
+ return {
468
+ category: error.options?.category ?? "credential_unavailable",
469
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
470
+ retryable: error.options?.retryable ?? declaredRetryable ?? false,
357
471
  };
358
472
  }
359
- if (!(error instanceof TransportError)) {
473
+ if (!isTransportError(error)) {
360
474
  return undefined;
361
475
  }
362
476
  const isProxyPoolCode = error.code === PROXY_POOL_EXHAUSTED_CODE ||
@@ -384,8 +498,69 @@ function providerObservabilityDetails(error) {
384
498
  ...(error.upstreamStatus ? { upstreamStatus: error.upstreamStatus } : {}),
385
499
  };
386
500
  }
501
+ function errorObservabilityDetails(error, declaredErrorCode) {
502
+ const effectiveDeclaration = sdkOwnsErrorResolution(error) ? undefined : declaredErrorCode;
503
+ const providerDetails = providerObservabilityDetails(error, effectiveDeclaration);
504
+ if (providerDetails)
505
+ return providerDetails;
506
+ if (error instanceof z.ZodError || isValidationError(error)) {
507
+ const declaredStatus = effectiveDeclaration?.status;
508
+ return {
509
+ category: isProviderError(error) && error.options?.category
510
+ ? error.options.category
511
+ : isEmittableErrorStatus(declaredStatus) &&
512
+ categoryForStatus(declaredStatus) === "upstream_rejected"
513
+ ? "upstream_rejected"
514
+ : isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
515
+ ? "provider_error"
516
+ : "input_validation",
517
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
518
+ retryable: isProviderError(error)
519
+ ? (error.options?.retryable ?? effectiveDeclaration?.retryable ?? false)
520
+ : false,
521
+ };
522
+ }
523
+ if (error instanceof StatefulRoutingDeadlineError) {
524
+ return {
525
+ category: "timeout",
526
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
527
+ retryable: false,
528
+ };
529
+ }
530
+ if (isProviderError(error)) {
531
+ // Deterministic upstream refusals default to the rejection category:
532
+ // the UPSTREAM_REJECTED family and any operation-declared rejection
533
+ // status (409/410/422) classify as upstream_rejected unless the
534
+ // author set an explicit category.
535
+ const declaredStatus = effectiveDeclaration?.status;
536
+ const rejectionDefault = error.code === "UPSTREAM_REJECTED" ||
537
+ (isEmittableErrorStatus(declaredStatus) &&
538
+ categoryForStatus(declaredStatus) === "upstream_rejected")
539
+ ? "upstream_rejected"
540
+ : "provider_error";
541
+ return {
542
+ category: error.options?.category ?? rejectionDefault,
543
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
544
+ retryable: error.options?.retryable ?? effectiveDeclaration?.retryable ?? false,
545
+ };
546
+ }
547
+ return {
548
+ category: "internal_error",
549
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
550
+ retryable: false,
551
+ };
552
+ }
553
+ function responseWithErrorObservability(response, error, declaredErrorCode) {
554
+ const headers = new Headers(response.headers);
555
+ headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(errorObservabilityDetails(error, declaredErrorCode)));
556
+ return new Response(response.body, {
557
+ status: response.status,
558
+ statusText: response.statusText,
559
+ headers,
560
+ });
561
+ }
387
562
  function publicProviderErrorMessage(error) {
388
- if (error instanceof TransportError) {
563
+ if (isTransportError(error)) {
389
564
  if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
390
565
  return error.message;
391
566
  }
@@ -409,18 +584,30 @@ function publicProviderErrorMessage(error) {
409
584
  }
410
585
  return error.message;
411
586
  }
412
- function toStatusCode(error) {
587
+ function isEmittableErrorStatus(value) {
588
+ return (typeof value === "number" &&
589
+ VALID_OPERATION_ERROR_STATUSES.some((status) => status === value));
590
+ }
591
+ function toStatusCode(error, declaredErrorCode) {
413
592
  if (error instanceof z.ZodError) {
414
593
  return 400;
415
594
  }
416
- if (error instanceof TransportError) {
417
- return error.code === "transport_timeout" ? 504 : 502;
595
+ if (error instanceof StatefulRoutingDeadlineError) {
596
+ return 504;
418
597
  }
419
- if (error instanceof ProviderError) {
598
+ if (isProviderError(error)) {
599
+ if (!sdkOwnsErrorResolution(error) &&
600
+ isEmittableErrorStatus(declaredErrorCode?.status)) {
601
+ return declaredErrorCode.status;
602
+ }
420
603
  switch (error.code) {
421
604
  case "AUTH_REQUIRED":
422
605
  case "reauth_required":
423
606
  return 401;
607
+ // Unprovisioned declared secret: a deployment/config defect, never an
608
+ // upstream failure — explicit 400 (was only reached via fallthrough).
609
+ case MISSING_SECRET_CODE:
610
+ return 400;
424
611
  case "NOT_FOUND":
425
612
  case "not_found":
426
613
  case "NO_DATA":
@@ -429,17 +616,55 @@ function toStatusCode(error) {
429
616
  case "UPSTREAM_RATE_LIMIT":
430
617
  case "LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR":
431
618
  return 429;
619
+ // Deterministic upstream business refusal (honest-provider-error-
620
+ // contract): the upstream evaluated the request and said no under
621
+ // its own rules — a conflict with upstream state, never a 5xx.
622
+ case "UPSTREAM_REJECTED":
623
+ return 409;
432
624
  case "UPSTREAM_ERROR":
433
625
  case "BLOCKED":
434
626
  return 502;
435
627
  case "STT_UNAVAILABLE":
436
628
  case "UNSUPPORTED_STT_BACKEND":
629
+ case "STATEFUL_FORWARDING_REPLAY_CACHE_FULL":
437
630
  return 503;
438
631
  }
439
- return 400;
632
+ if (isTransportError(error)) {
633
+ return error.code === "transport_timeout" ? 504 : 502;
634
+ }
635
+ if (isValidationError(error)) {
636
+ return error.options?.category === "output_validation" ? 500 : 400;
637
+ }
638
+ return 500;
440
639
  }
441
640
  return 500;
442
641
  }
642
+ function sdkOwnsErrorResolution(error) {
643
+ if (isSessionExpiredError(error))
644
+ return true;
645
+ if (isTransportError(error))
646
+ return true;
647
+ if (error instanceof z.ZodError)
648
+ return true;
649
+ if (error instanceof StatefulRoutingDeadlineError)
650
+ return true;
651
+ return (isProviderError(error) &&
652
+ typeof error.code === "string" &&
653
+ SDK_RUNTIME_OWNED_ERROR_CODES.has(error.code));
654
+ }
655
+ function buildOperationErrorCodeLookup(provider) {
656
+ return new Map(Object.entries(provider.operations).flatMap(([operationId, operation]) => {
657
+ const errorCodes = operation.docs?.errorCodes;
658
+ return errorCodes?.length
659
+ ? [[operationId, new Map(errorCodes.map((entry) => [entry.code, entry]))]]
660
+ : [];
661
+ }));
662
+ }
663
+ function declaredErrorCodeFor(error, operationId, lookup) {
664
+ if (!operationId || !isProviderError(error) || typeof error.code !== "string")
665
+ return undefined;
666
+ return lookup.get(operationId)?.get(error.code);
667
+ }
443
668
  function extractRequestId(raw) {
444
669
  if (!raw || typeof raw !== "object") {
445
670
  return undefined;
@@ -447,17 +672,23 @@ function extractRequestId(raw) {
447
672
  const value = Object.getOwnPropertyDescriptor(raw, "requestId")?.value;
448
673
  return typeof value === "string" ? value : undefined;
449
674
  }
450
- function logProviderError(logger, provider, kind, route, requestId, error, status, cost) {
451
- const code = error instanceof ProviderError
675
+ function logProviderError(logger, provider, kind, route, requestId, error, status, cost, declaredErrorCode) {
676
+ const code = isProviderError(error)
452
677
  ? (error.code ?? "provider_error")
453
678
  : error instanceof z.ZodError
454
679
  ? "invalid_request"
455
- : "internal_error";
680
+ : error instanceof StatefulRoutingDeadlineError
681
+ ? "STATEFUL_FORWARDING_DEADLINE_EXPIRED"
682
+ : "internal_error";
456
683
  const errorClass = error instanceof Error ? error.name : typeof error;
457
684
  const message = error instanceof Error ? error.message : String(error);
458
- const details = error instanceof ProviderError
459
- ? providerObservabilityDetails(error)
460
- : undefined;
685
+ const details = errorObservabilityDetails(error, declaredErrorCode);
686
+ const isUnregisteredProviderErrorCode = status === 500 &&
687
+ isProviderError(error) &&
688
+ !isValidationError(error) &&
689
+ typeof error.code === "string" &&
690
+ !SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code) &&
691
+ declaredErrorCode === undefined;
461
692
  const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
462
693
  emit({
463
694
  level: status >= 500 ? "error" : "warn",
@@ -471,14 +702,14 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
471
702
  code,
472
703
  errorClass,
473
704
  message,
474
- ...(error instanceof TransportError && error.upstreamStatus
475
- ? { upstreamStatus: error.upstreamStatus }
476
- : {}),
477
- ...(details
705
+ ...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
706
+ errorCategory: details.category,
707
+ taxonomyVersion: details.taxonomyVersion,
708
+ retryable: details.retryable,
709
+ ...(isUnregisteredProviderErrorCode
478
710
  ? {
479
- errorCategory: details.category,
480
- taxonomyVersion: details.taxonomyVersion,
481
- retryable: details.retryable,
711
+ signal: "unregistered_provider_error_code",
712
+ signalFix: "Declare this code (with status and retryable) in the operation's docs.errorCodes so it serves its intended status instead of 500.",
482
713
  }
483
714
  : {}),
484
715
  ...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
@@ -594,8 +825,7 @@ async function validateSseEvent(operation, event) {
594
825
  const transport = getSseTransport(operation);
595
826
  const schema = transport?.events?.[event.event];
596
827
  if (!schema) {
597
- if (event.event === APIFUSE_STREAM_ERROR_EVENT ||
598
- event.event === APIFUSE_STREAM_DONE_EVENT) {
828
+ if (event.event === APIFUSE_STREAM_ERROR_EVENT || event.event === APIFUSE_STREAM_DONE_EVENT) {
599
829
  return event;
600
830
  }
601
831
  throw new ProviderError(`SSE event "${event.event}" is not declared in operation transport.events.`, {
@@ -714,8 +944,7 @@ function enforceStreamChunkLimit(body, maxChunkBytes) {
714
944
  }
715
945
  function toStreamingResponse(operation, result, cleanup, requestId) {
716
946
  const transport = operation.transport?.kind ?? "json";
717
- if (transport === "sse" &&
718
- (result instanceof Response || result instanceof ReadableStream)) {
947
+ if (transport === "sse" && (result instanceof Response || result instanceof ReadableStream)) {
719
948
  void cleanup();
720
949
  throw new ProviderError("SSE operations must return an AsyncIterable of typed stream.event(...) values.", {
721
950
  code: "SSE_RESULT_UNSUPPORTED",
@@ -726,9 +955,7 @@ function toStreamingResponse(operation, result, cleanup, requestId) {
726
955
  }
727
956
  if (result instanceof Response) {
728
957
  const httpTransport = getHttpStreamTransport(operation);
729
- if (httpTransport &&
730
- result.body &&
731
- httpTransport?.maxChunkBytes !== undefined) {
958
+ if (httpTransport && result.body && httpTransport?.maxChunkBytes !== undefined) {
732
959
  return responseWithCleanup(new Response(enforceStreamChunkLimit(result.body, httpTransport.maxChunkBytes), {
733
960
  headers: result.headers,
734
961
  status: result.status,
@@ -747,8 +974,7 @@ function toStreamingResponse(operation, result, cleanup, requestId) {
747
974
  ? { "Content-Type": "text/event-stream; charset=utf-8" }
748
975
  : {
749
976
  "Content-Type": operation.transport?.kind === "http-stream"
750
- ? (operation.transport.contentType ??
751
- "application/octet-stream")
977
+ ? (operation.transport.contentType ?? "application/octet-stream")
752
978
  : "application/octet-stream",
753
979
  },
754
980
  }), cleanup);
@@ -766,9 +992,7 @@ function getSseTransport(operation) {
766
992
  return operation.transport?.kind === "sse" ? operation.transport : undefined;
767
993
  }
768
994
  function getHttpStreamTransport(operation) {
769
- return operation.transport?.kind === "http-stream"
770
- ? operation.transport
771
- : undefined;
995
+ return operation.transport?.kind === "http-stream" ? operation.transport : undefined;
772
996
  }
773
997
  function toAuthFlowResponse(result, contextPatch) {
774
998
  if (result instanceof Response) {
@@ -796,7 +1020,7 @@ function isAuthFlowLocale(value) {
796
1020
  return value === "en" || value === "ko" || value === "ja";
797
1021
  }
798
1022
  function isAuthTurn(value) {
799
- return (!!value && typeof value === "object" && "kind" in value && "turnId" in value);
1023
+ return !!value && typeof value === "object" && "kind" in value && "turnId" in value;
800
1024
  }
801
1025
  function loadAuthFlowLocaleCatalogs(provider) {
802
1026
  for (const providerDir of [
@@ -859,7 +1083,14 @@ async function handleOperation(provider, request, operationId, options = {}, sta
859
1083
  }
860
1084
  };
861
1085
  try {
862
- const result = await executeOperation(provider, operationId, ctx, request.input);
1086
+ const result = options.operationExecutor
1087
+ ? await options.operationExecutor({
1088
+ provider,
1089
+ operationId,
1090
+ ctx,
1091
+ request,
1092
+ })
1093
+ : await executeOperation(provider, operationId, ctx, request.input);
863
1094
  if (streaming && operation) {
864
1095
  return toStreamingResponse(operation, result, cleanup, request.requestId);
865
1096
  }
@@ -893,8 +1124,16 @@ async function handleAuthFlow(provider, request, route, options = {}, signal) {
893
1124
  code: "AUTH_FLOW_NOT_CONFIGURED",
894
1125
  });
895
1126
  }
1127
+ // Same SDK-owned gate as executeOperation: OAuth/credentials ceremonies
1128
+ // depend on declared secrets (client ids/secrets), so fail structured before
1129
+ // any flow code runs instead of at whatever point the ceremony first reads
1130
+ // the env. `abort` stays exempt: a user must always be able to cancel a
1131
+ // stranded flow even when provisioning is broken.
896
1132
  const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
897
1133
  try {
1134
+ if (route !== "abort") {
1135
+ assertRequiredSecretsPresent(provider, context.env);
1136
+ }
898
1137
  const result = route === "start"
899
1138
  ? await flow.start(context)
900
1139
  : route === "continue"
@@ -933,25 +1172,268 @@ async function handleAuthFlow(provider, request, route, options = {}, signal) {
933
1172
  context.stealth.close?.();
934
1173
  }
935
1174
  }
1175
+ class StatefulForwardingReplayCache {
1176
+ maxEntries;
1177
+ #nonces = new Map();
1178
+ #expiryBuckets = new Map();
1179
+ #nextExpiryBucket;
1180
+ #latestExpiryBucket;
1181
+ constructor(maxEntries) {
1182
+ this.maxEntries = maxEntries;
1183
+ }
1184
+ claim(nonce, expiresAtMs, nowMs) {
1185
+ this.dropExpiredBuckets(nowMs);
1186
+ if (this.#nonces.has(nonce))
1187
+ return "replayed";
1188
+ if (this.#nonces.size >= this.maxEntries)
1189
+ return "full";
1190
+ const expiryBucket = Math.ceil(expiresAtMs / STATEFUL_FORWARDING_REPLAY_BUCKET_MS) *
1191
+ STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
1192
+ this.#nonces.set(nonce, expiryBucket);
1193
+ const bucket = this.#expiryBuckets.get(expiryBucket) ?? new Set();
1194
+ bucket.add(nonce);
1195
+ this.#expiryBuckets.set(expiryBucket, bucket);
1196
+ this.#nextExpiryBucket = Math.min(this.#nextExpiryBucket ?? expiryBucket, expiryBucket);
1197
+ this.#latestExpiryBucket = Math.max(this.#latestExpiryBucket ?? expiryBucket, expiryBucket);
1198
+ return "accepted";
1199
+ }
1200
+ dropExpiredBuckets(nowMs) {
1201
+ if (this.#nextExpiryBucket === undefined || this.#latestExpiryBucket === undefined)
1202
+ return;
1203
+ if (nowMs >= this.#latestExpiryBucket) {
1204
+ this.#nonces.clear();
1205
+ this.#expiryBuckets.clear();
1206
+ this.#nextExpiryBucket = undefined;
1207
+ this.#latestExpiryBucket = undefined;
1208
+ return;
1209
+ }
1210
+ while (this.#nextExpiryBucket <= nowMs) {
1211
+ const bucket = this.#expiryBuckets.get(this.#nextExpiryBucket);
1212
+ if (bucket) {
1213
+ for (const cachedNonce of bucket)
1214
+ this.#nonces.delete(cachedNonce);
1215
+ this.#expiryBuckets.delete(this.#nextExpiryBucket);
1216
+ }
1217
+ this.#nextExpiryBucket += STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
1218
+ }
1219
+ }
1220
+ }
1221
+ function verifyStatefulForwardingRequest(input) {
1222
+ const config = input.options.statefulForwarding;
1223
+ if (!config?.secret) {
1224
+ throw new ProviderError("Stateful forwarding is not configured.", {
1225
+ code: "STATEFUL_FORWARDING_NOT_CONFIGURED",
1226
+ });
1227
+ }
1228
+ const timestamp = input.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "";
1229
+ const signature = input.headers.get(STATEFUL_FORWARDING_SIGNATURE_HEADER) ?? "";
1230
+ const nonce = input.headers.get(STATEFUL_FORWARDING_NONCE_HEADER) ?? "";
1231
+ if (!timestamp || !signature || !nonce) {
1232
+ throw new ProviderError("Stateful forwarding signature headers are missing.", {
1233
+ code: "STATEFUL_FORWARDING_SIGNATURE_MISSING",
1234
+ });
1235
+ }
1236
+ if (nonce.length > 256) {
1237
+ throw new ProviderError("Stateful forwarding nonce is invalid.", {
1238
+ code: "STATEFUL_FORWARDING_NONCE_INVALID",
1239
+ });
1240
+ }
1241
+ const timestampMs = Date.parse(timestamp);
1242
+ const maxSkewMs = config.maxSkewMs ?? DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS;
1243
+ if (!Number.isFinite(timestampMs) || Math.abs(Date.now() - timestampMs) > maxSkewMs) {
1244
+ throw new ProviderError("Stateful forwarding signature timestamp is outside the allowed skew.", { code: "STATEFUL_FORWARDING_TIMESTAMP_INVALID" });
1245
+ }
1246
+ if (!verifyStatefulRequestSignature({
1247
+ secret: config.secret,
1248
+ timestamp,
1249
+ rawBody: input.rawBody,
1250
+ method: input.method,
1251
+ path: input.path,
1252
+ nonce,
1253
+ signature,
1254
+ })) {
1255
+ throw new ProviderError("Stateful forwarding signature is invalid.", {
1256
+ code: "STATEFUL_FORWARDING_SIGNATURE_INVALID",
1257
+ });
1258
+ }
1259
+ const replayResult = input.replayCache.claim(nonce, timestampMs + maxSkewMs, Date.now());
1260
+ if (replayResult === "replayed") {
1261
+ throw new ProviderError("Stateful forwarding nonce has already been used.", {
1262
+ code: "STATEFUL_FORWARDING_REPLAY_DETECTED",
1263
+ });
1264
+ }
1265
+ if (replayResult === "full") {
1266
+ throw new ProviderError("Stateful forwarding replay cache is at capacity.", {
1267
+ code: "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
1268
+ });
1269
+ }
1270
+ }
1271
+ function operationRequestFromForwardingEnvelope(envelope) {
1272
+ return {
1273
+ ...envelope.operationRequest,
1274
+ ...(envelope.deadlineAt !== undefined ? { deadlineAt: envelope.deadlineAt } : {}),
1275
+ };
1276
+ }
1277
+ function parseStatefulForwardingEnvelope(rawBody) {
1278
+ const parsed = ProviderServerStatefulForwardEnvelopeSchema.safeParse(rawBody);
1279
+ if (parsed.success)
1280
+ return parsed.data;
1281
+ throw new ProviderError("Stateful forwarding envelope is invalid.", {
1282
+ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
1283
+ details: zodDetails(parsed.error),
1284
+ });
1285
+ }
936
1286
  export function createServerApp(provider, options = {}) {
1287
+ validateStatefulServerConfig(options);
937
1288
  const app = new Hono();
938
1289
  const logger = options.logger ?? defaultProviderServerLogger;
1290
+ const operationErrorCodes = buildOperationErrorCodeLookup(provider);
1291
+ const statefulForwardingReplayCache = new StatefulForwardingReplayCache(options.statefulForwarding?.replayCacheMaxEntries ??
1292
+ DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES);
939
1293
  const state = options.state ??
940
1294
  createProviderRuntimeStateFromEnv({
941
1295
  providerId: provider.id,
942
1296
  allowMemoryFallback: options.allowMemoryStateFallback === true,
943
1297
  });
944
- app.notFound((c) => c.json({
945
- error: {
946
- code: "not_found",
947
- message: "Not found",
948
- },
949
- }, 404));
1298
+ // Boot-time visibility for unprovisioned declared secrets: emit a structured
1299
+ // warn so deploy tooling/alerting sees the gap the moment the pod boots,
1300
+ // instead of discovering it request-by-request. Deliberately log-only — a
1301
+ // boot crash would trade a structured MISSING_SECRET signal for
1302
+ // CrashLoopBackOff. Requests still fail closed via the executeOperation gate.
1303
+ const missingSecretsAtBoot = listMissingRequiredSecrets(provider, createEnvContext(provider.secrets?.map((secret) => secret.name)));
1304
+ if (missingSecretsAtBoot.length > 0) {
1305
+ logger({
1306
+ level: "warn",
1307
+ event: "provider_secrets_missing",
1308
+ providerId: provider.id,
1309
+ missingSecrets: missingSecretsAtBoot,
1310
+ });
1311
+ }
1312
+ app.notFound((c) => {
1313
+ const error = new ProviderError("Not found", { code: "not_found", retryable: false });
1314
+ return responseWithErrorObservability(c.json(toErrorResponse(error), 404), error);
1315
+ });
950
1316
  app.get("/health", (c) => c.json({
951
1317
  status: "ok",
952
1318
  provider: provider.id,
953
1319
  version: provider.version,
954
1320
  }));
1321
+ app.post(STATEFUL_INTERNAL_OPERATIONS_ROUTE, async (c) => {
1322
+ let rawBodyText = "";
1323
+ let rawBody;
1324
+ let operationId;
1325
+ const operation = "stateful-internal";
1326
+ const requestCost = startRequestCost();
1327
+ try {
1328
+ if (!options.internalOperationExecutor) {
1329
+ throw new ProviderError("Stateful internal operation executor is not configured.", {
1330
+ code: "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
1331
+ });
1332
+ }
1333
+ rawBodyText = await c.req.raw.clone().text();
1334
+ verifyStatefulForwardingRequest({
1335
+ options,
1336
+ rawBody: rawBodyText,
1337
+ headers: c.req.raw.headers,
1338
+ method: c.req.raw.method,
1339
+ path: STATEFUL_INTERNAL_OPERATIONS_ROUTE,
1340
+ replayCache: statefulForwardingReplayCache,
1341
+ });
1342
+ try {
1343
+ rawBody = JSON.parse(rawBodyText);
1344
+ }
1345
+ catch {
1346
+ throw new ProviderError("Stateful forwarding envelope is not valid JSON.", {
1347
+ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
1348
+ });
1349
+ }
1350
+ const envelope = parseStatefulForwardingEnvelope(rawBody);
1351
+ if (envelope.providerId !== provider.id) {
1352
+ throw new ProviderError("Stateful forwarding envelope providerId does not match the served provider.", { code: "STATEFUL_FORWARDING_PROVIDER_MISMATCH" });
1353
+ }
1354
+ if (envelope.requestId !== envelope.operationRequest.requestId) {
1355
+ throw new ProviderError("Stateful forwarding requestId values do not match.", {
1356
+ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
1357
+ });
1358
+ }
1359
+ if (envelope.sourcePodId !==
1360
+ (c.req.raw.headers.get(STATEFUL_FORWARDING_SOURCE_POD_HEADER) ?? "")) {
1361
+ throw new ProviderError("Stateful forwarding source pod does not match its header.", {
1362
+ code: "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
1363
+ });
1364
+ }
1365
+ if (envelope.forwardedAt !== (c.req.raw.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "")) {
1366
+ throw new ProviderError("Stateful forwarding forwardedAt does not match its signature timestamp.", { code: "STATEFUL_FORWARDING_ENVELOPE_INVALID" });
1367
+ }
1368
+ const deadlineAtMs = envelope.deadlineAt ? Date.parse(envelope.deadlineAt) : undefined;
1369
+ if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
1370
+ throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
1371
+ }
1372
+ const remainingDeadlineMs = deadlineAtMs === undefined ? undefined : deadlineAtMs - Date.now();
1373
+ const deadlineSignal = remainingDeadlineMs === undefined ? undefined : AbortSignal.timeout(remainingDeadlineMs);
1374
+ const signal = deadlineSignal
1375
+ ? AbortSignal.any([c.req.raw.signal, deadlineSignal])
1376
+ : c.req.raw.signal;
1377
+ const ownerFenceValidation = Promise.resolve(options.statefulForwarding?.validateOwnerFence({
1378
+ providerId: envelope.providerId,
1379
+ sessionKey: envelope.sessionKey,
1380
+ ownerPodId: envelope.ownerPodId,
1381
+ generation: envelope.generation,
1382
+ sourcePodId: envelope.sourcePodId,
1383
+ forwardedAt: envelope.forwardedAt,
1384
+ requestId: envelope.requestId,
1385
+ ...(envelope.idempotencyKey ? { idempotencyKey: envelope.idempotencyKey } : {}),
1386
+ }, signal));
1387
+ let ownerFenceValid;
1388
+ try {
1389
+ ownerFenceValid = deadlineSignal
1390
+ ? await Promise.race([
1391
+ ownerFenceValidation,
1392
+ new Promise((_resolve, reject) => {
1393
+ deadlineSignal.addEventListener("abort", () => reject(new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt)), { once: true });
1394
+ }),
1395
+ ])
1396
+ : await ownerFenceValidation;
1397
+ }
1398
+ catch (error) {
1399
+ if (deadlineSignal?.aborted) {
1400
+ throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
1401
+ }
1402
+ throw error;
1403
+ }
1404
+ if (ownerFenceValid !== true) {
1405
+ throw new ProviderError("Stateful forwarding owner fence is no longer current.", {
1406
+ code: "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
1407
+ });
1408
+ }
1409
+ const request = operationRequestFromForwardingEnvelope(envelope);
1410
+ operationId = envelope.operationId;
1411
+ const ctx = createProviderContext(provider, request, operationId, options, state);
1412
+ if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
1413
+ throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
1414
+ }
1415
+ const output = await options.internalOperationExecutor({
1416
+ provider,
1417
+ operationId,
1418
+ ctx,
1419
+ request,
1420
+ internalStatefulForward: envelope,
1421
+ signal,
1422
+ });
1423
+ logProviderSuccess(logger, provider, "operation", operationId || operation, request.requestId, 200, finishRequestCost(requestCost));
1424
+ return c.json({ data: output });
1425
+ }
1426
+ catch (error) {
1427
+ const declaredErrorCode = declaredErrorCodeFor(error, operationId, operationErrorCodes);
1428
+ const status = toStatusCode(error, declaredErrorCode);
1429
+ if (isProviderError(error) && error.code === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
1430
+ c.header("Retry-After", String(STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS));
1431
+ }
1432
+ const requestId = extractRequestId(rawBody);
1433
+ logProviderError(logger, provider, "operation", operationId || operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode);
1434
+ return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, declaredErrorCode), status), error, declaredErrorCode);
1435
+ }
1436
+ });
955
1437
  app.post("/v1/:operation", async (c) => {
956
1438
  let rawBody;
957
1439
  const operation = c.req.param("operation");
@@ -977,13 +1459,14 @@ export function createServerApp(provider, options = {}) {
977
1459
  return c.json(response);
978
1460
  }
979
1461
  catch (error) {
980
- const status = toStatusCode(error);
1462
+ const declaredErrorCode = declaredErrorCodeFor(error, operation, operationErrorCodes);
1463
+ const status = toStatusCode(error, declaredErrorCode);
981
1464
  const requestId = extractRequestId(rawBody);
982
- logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost));
1465
+ logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode);
983
1466
  const telemetryHeader = proxyTelemetry.toHeaderValue();
984
1467
  if (telemetryHeader)
985
1468
  c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
986
- return c.json(toErrorResponse(error, requestId), status);
1469
+ return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, declaredErrorCode), status), error, declaredErrorCode);
987
1470
  }
988
1471
  });
989
1472
  app.post("/auth/start", async (c) => {
@@ -1003,7 +1486,7 @@ export function createServerApp(provider, options = {}) {
1003
1486
  const status = toStatusCode(error);
1004
1487
  const requestId = extractRequestId(rawBody);
1005
1488
  logProviderError(logger, provider, "auth", "start", requestId, error, status, finishRequestCost(requestCost));
1006
- return c.json(toErrorResponse(error, requestId), status);
1489
+ return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
1007
1490
  }
1008
1491
  });
1009
1492
  app.post("/auth/continue", async (c) => {
@@ -1023,7 +1506,7 @@ export function createServerApp(provider, options = {}) {
1023
1506
  const status = toStatusCode(error);
1024
1507
  const requestId = extractRequestId(rawBody);
1025
1508
  logProviderError(logger, provider, "auth", "continue", requestId, error, status, finishRequestCost(requestCost));
1026
- return c.json(toErrorResponse(error, requestId), status);
1509
+ return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
1027
1510
  }
1028
1511
  });
1029
1512
  app.post("/auth/poll", async (c) => {
@@ -1043,7 +1526,7 @@ export function createServerApp(provider, options = {}) {
1043
1526
  const status = toStatusCode(error);
1044
1527
  const requestId = extractRequestId(rawBody);
1045
1528
  logProviderError(logger, provider, "auth", "poll", requestId, error, status, finishRequestCost(requestCost));
1046
- return c.json(toErrorResponse(error, requestId), status);
1529
+ return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
1047
1530
  }
1048
1531
  });
1049
1532
  app.post("/auth/refresh", async (c) => {
@@ -1063,7 +1546,7 @@ export function createServerApp(provider, options = {}) {
1063
1546
  const status = toStatusCode(error);
1064
1547
  const requestId = extractRequestId(rawBody);
1065
1548
  logProviderError(logger, provider, "auth", "refresh", requestId, error, status, finishRequestCost(requestCost));
1066
- return c.json(toErrorResponse(error, requestId), status);
1549
+ return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
1067
1550
  }
1068
1551
  });
1069
1552
  app.post("/auth/disconnect", async (c) => {
@@ -1083,11 +1566,33 @@ export function createServerApp(provider, options = {}) {
1083
1566
  const status = toStatusCode(error);
1084
1567
  const requestId = extractRequestId(rawBody);
1085
1568
  logProviderError(logger, provider, "auth", "disconnect", requestId, error, status, finishRequestCost(requestCost));
1086
- return c.json(toErrorResponse(error, requestId), status);
1569
+ return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
1087
1570
  }
1088
1571
  });
1089
1572
  return app;
1090
1573
  }
1574
+ function validateStatefulServerConfig(options) {
1575
+ if (options.statefulForwarding && !options.internalOperationExecutor) {
1576
+ throw new Error("Invalid provider server configuration: statefulForwarding requires internalOperationExecutor; missing option internalOperationExecutor.");
1577
+ }
1578
+ if (options.internalOperationExecutor && !options.statefulForwarding?.secret) {
1579
+ throw new Error("Invalid provider server configuration: internalOperationExecutor requires statefulForwarding.secret; missing option statefulForwarding.secret.");
1580
+ }
1581
+ if (options.statefulForwarding &&
1582
+ typeof options.statefulForwarding.validateOwnerFence !== "function") {
1583
+ throw new Error("Invalid provider server configuration: statefulForwarding requires validateOwnerFence.");
1584
+ }
1585
+ if (options.statefulForwarding?.maxSkewMs !== undefined &&
1586
+ (!Number.isFinite(options.statefulForwarding.maxSkewMs) ||
1587
+ options.statefulForwarding.maxSkewMs <= 0)) {
1588
+ throw new Error("Invalid provider server configuration: maxSkewMs must be positive.");
1589
+ }
1590
+ if (options.statefulForwarding?.replayCacheMaxEntries !== undefined &&
1591
+ (!Number.isInteger(options.statefulForwarding.replayCacheMaxEntries) ||
1592
+ options.statefulForwarding.replayCacheMaxEntries <= 0)) {
1593
+ throw new Error("Invalid provider server configuration: replayCacheMaxEntries must be a positive integer.");
1594
+ }
1595
+ }
1091
1596
  function getBunServeRuntime() {
1092
1597
  const bunValue = Object.getOwnPropertyDescriptor(globalThis, "Bun")?.value;
1093
1598
  if (!bunValue || typeof bunValue !== "object") {
@@ -1103,6 +1608,9 @@ function getBunServeRuntime() {
1103
1608
  },
1104
1609
  };
1105
1610
  }
1611
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
1612
+ const DEFAULT_SHUTDOWN_SIGNALS = ["SIGTERM", "SIGINT"];
1613
+ const processSignalCoordinators = new Map();
1106
1614
  export async function serve(provider, options = {}) {
1107
1615
  const bunRuntime = getBunServeRuntime();
1108
1616
  if (bunRuntime === undefined) {
@@ -1110,29 +1618,186 @@ export async function serve(provider, options = {}) {
1110
1618
  code: "RUNTIME_UNSUPPORTED",
1111
1619
  });
1112
1620
  }
1621
+ const logger = options.logger ?? defaultProviderServerLogger;
1622
+ const configuredTimeoutMs = shutdownTimeout(options.shutdown?.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS);
1623
+ const configuredSignals = resolveShutdownSignals(options.shutdown?.signals ?? true);
1113
1624
  const app = createServerApp(provider, {
1114
1625
  logger: options.logger,
1115
1626
  stt: options.stt,
1627
+ state: options.state,
1628
+ allowMemoryStateFallback: options.allowMemoryStateFallback,
1629
+ operationExecutor: options.operationExecutor,
1630
+ internalOperationExecutor: options.internalOperationExecutor,
1631
+ statefulForwarding: options.statefulForwarding,
1116
1632
  });
1117
- bunRuntime.serve({
1118
- port: options.port ?? DEFAULT_PORT,
1119
- hostname: options.host ?? DEFAULT_HOST,
1120
- fetch: app.fetch,
1121
- });
1122
- // Internal self-test listener (health dependency inversion): a SEPARATE
1123
- // socket the tenant-facing gateway never dials. Off by default — it only
1124
- // starts when the shared self-test master secret env is present.
1125
- const selfTestSecrets = resolveSelfTestMasterSecrets();
1126
- if (selfTestSecrets) {
1127
- const selfTestApp = createSelfTestApp(provider, {
1128
- secrets: selfTestSecrets,
1129
- invoke: createSelfTestInvoke(app),
1130
- });
1131
- bunRuntime.serve({
1132
- port: options.selfTestPort ?? resolveSelfTestPort(),
1633
+ const servers = [];
1634
+ try {
1635
+ servers.push(bunRuntime.serve({
1636
+ port: options.port ?? DEFAULT_PORT,
1133
1637
  hostname: options.host ?? DEFAULT_HOST,
1134
- fetch: selfTestApp.fetch,
1135
- });
1638
+ fetch: app.fetch,
1639
+ }));
1640
+ // Internal self-test listener (health dependency inversion): a SEPARATE
1641
+ // socket the tenant-facing gateway never dials. Off by default — it only
1642
+ // starts when the shared self-test master secret env is present.
1643
+ const selfTestSecrets = resolveSelfTestMasterSecrets();
1644
+ if (selfTestSecrets) {
1645
+ const selfTestApp = createSelfTestApp(provider, {
1646
+ secrets: selfTestSecrets,
1647
+ invoke: createSelfTestInvoke(app),
1648
+ authFlow: createSelfTestAuthFlowInvoke(app),
1649
+ });
1650
+ servers.push(bunRuntime.serve({
1651
+ port: options.selfTestPort ?? resolveSelfTestPort(),
1652
+ hostname: options.host ?? DEFAULT_HOST,
1653
+ fetch: selfTestApp.fetch,
1654
+ }));
1655
+ }
1656
+ }
1657
+ catch (error) {
1658
+ await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
1659
+ throw error;
1660
+ }
1661
+ const server = servers[0];
1662
+ if (!server)
1663
+ throw new Error("Provider server failed to create its primary listener.");
1664
+ let closePromise;
1665
+ let unregisterSignals = () => { };
1666
+ const close = (closeOptions = {}) => {
1667
+ if (closePromise)
1668
+ return closePromise;
1669
+ const timeoutMs = shutdownTimeout(closeOptions.timeoutMs ?? configuredTimeoutMs);
1670
+ closePromise = closeProviderServers({
1671
+ servers,
1672
+ hooks: options.shutdown?.hooks ?? [],
1673
+ timeoutMs,
1674
+ logger,
1675
+ providerId: provider.id,
1676
+ }).finally(() => unregisterSignals());
1677
+ return closePromise;
1678
+ };
1679
+ try {
1680
+ unregisterSignals = registerForProcessSignals(configuredSignals, () => close({ timeoutMs: configuredTimeoutMs }));
1681
+ }
1682
+ catch (error) {
1683
+ unregisterSignals();
1684
+ await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
1685
+ throw error;
1686
+ }
1687
+ return { port: server.port, close };
1688
+ }
1689
+ function registerForProcessSignals(signals, close) {
1690
+ if (signals.length === 0)
1691
+ return () => { };
1692
+ const registration = {
1693
+ signals: new Set(signals),
1694
+ close,
1695
+ };
1696
+ let registered = true;
1697
+ const unregister = () => {
1698
+ if (!registered)
1699
+ return;
1700
+ registered = false;
1701
+ for (const signal of registration.signals) {
1702
+ const coordinator = processSignalCoordinators.get(signal);
1703
+ if (!coordinator)
1704
+ continue;
1705
+ coordinator.registrations.delete(registration);
1706
+ if (coordinator.registrations.size === 0 && !coordinator.handling) {
1707
+ process.removeListener(signal, coordinator.listener);
1708
+ processSignalCoordinators.delete(signal);
1709
+ }
1710
+ }
1711
+ };
1712
+ try {
1713
+ for (const signal of signals) {
1714
+ let coordinator = processSignalCoordinators.get(signal);
1715
+ if (!coordinator) {
1716
+ const created = {
1717
+ registrations: new Set(),
1718
+ handling: false,
1719
+ listener: () => handleCoordinatedSignal(signal, created),
1720
+ };
1721
+ coordinator = created;
1722
+ processSignalCoordinators.set(signal, coordinator);
1723
+ process.on(signal, coordinator.listener);
1724
+ }
1725
+ coordinator.registrations.add(registration);
1726
+ }
1727
+ }
1728
+ catch (error) {
1729
+ unregister();
1730
+ throw error;
1731
+ }
1732
+ return unregister;
1733
+ }
1734
+ function handleCoordinatedSignal(signal, coordinator) {
1735
+ if (coordinator.handling)
1736
+ return;
1737
+ coordinator.handling = true;
1738
+ const registrations = [...coordinator.registrations];
1739
+ void Promise.allSettled(registrations.map((registration) => registration.close())).finally(() => {
1740
+ if (processSignalCoordinators.get(signal) === coordinator) {
1741
+ process.removeListener(signal, coordinator.listener);
1742
+ processSignalCoordinators.delete(signal);
1743
+ }
1744
+ try {
1745
+ process.kill(process.pid, signal);
1746
+ }
1747
+ catch {
1748
+ process.exitCode = 1;
1749
+ }
1750
+ });
1751
+ }
1752
+ async function closeProviderServers(input) {
1753
+ const deadline = Date.now() + input.timeoutMs;
1754
+ const gracefulStops = input.servers.map((server) => server.stop(false));
1755
+ for (const gracefulStop of gracefulStops)
1756
+ gracefulStop.catch(() => undefined);
1757
+ for (const [hookIndex, hook] of input.hooks.entries()) {
1758
+ try {
1759
+ await withinShutdownBudget(Promise.resolve().then(hook), deadline);
1760
+ }
1761
+ catch (error) {
1762
+ try {
1763
+ input.logger({
1764
+ level: "error",
1765
+ event: "provider_shutdown_hook_failed",
1766
+ providerId: input.providerId,
1767
+ hookIndex,
1768
+ errorClass: error instanceof Error ? error.name : "UnknownError",
1769
+ message: error instanceof Error ? error.message : "Shutdown hook failed.",
1770
+ });
1771
+ }
1772
+ catch { }
1773
+ }
1774
+ }
1775
+ const forcedStops = input.servers.map((server) => server.stop(true));
1776
+ await withinShutdownBudget(Promise.allSettled([...gracefulStops, ...forcedStops]).then(() => undefined), deadline).catch(() => undefined);
1777
+ }
1778
+ async function withinShutdownBudget(promise, deadline) {
1779
+ promise.catch(() => undefined);
1780
+ const remainingMs = Math.max(0, deadline - Date.now());
1781
+ let timer;
1782
+ const timeout = new Promise((_resolve, reject) => {
1783
+ timer = setTimeout(() => reject(new Error("Provider server shutdown timed out.")), remainingMs);
1784
+ });
1785
+ try {
1786
+ return await Promise.race([promise, timeout]);
1787
+ }
1788
+ finally {
1789
+ if (timer)
1790
+ clearTimeout(timer);
1791
+ }
1792
+ }
1793
+ function resolveShutdownSignals(signals) {
1794
+ if (signals === false)
1795
+ return [];
1796
+ return [...new Set(signals === true ? DEFAULT_SHUTDOWN_SIGNALS : signals)];
1797
+ }
1798
+ function shutdownTimeout(value) {
1799
+ if (!Number.isFinite(value) || value < 0) {
1800
+ throw new Error("Provider server shutdown timeoutMs must be a non-negative finite number.");
1136
1801
  }
1137
- await Promise.resolve();
1802
+ return value;
1138
1803
  }