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

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