@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
@@ -3,62 +3,83 @@ import { join } from "node:path";
3
3
 
4
4
  import { Hono } from "hono";
5
5
  import { z } from "zod";
6
- import { AuthAbortError, createAuthFlowHelpers } from "../auth";
6
+ import { AuthAbortError, createAuthFlowHelpers } from "../auth.js";
7
+ import {
8
+ SDK_OWNED_PROVIDER_ERROR_CODES,
9
+ SDK_RUNTIME_OWNED_ERROR_CODES,
10
+ } from "../error-resolution.js";
7
11
  import {
8
12
  AuthError,
13
+ isProviderError,
14
+ isSessionExpiredError,
15
+ isTransportError,
16
+ isValidationError,
9
17
  ProviderError,
10
- SessionExpiredError,
11
- TransportError,
12
- } from "../errors";
18
+ } from "../errors.js";
13
19
  import {
14
20
  loadProviderLocaleCatalogs,
15
21
  localizeAuthTurn,
16
22
  type ProviderLocaleCatalogMap,
17
- } from "../i18n/catalog";
18
- import type { ProviderLocale } from "../i18n/keys";
23
+ } from "../i18n/catalog.js";
24
+ import type { ProviderLocale } from "../i18n/keys.js";
19
25
  import {
20
26
  categoryForStatus,
27
+ type ProviderErrorSource,
28
+ sourceForCategory,
21
29
  isRetryableCategory,
22
30
  PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
23
31
  type ProviderErrorCategory,
24
- } from "../observability";
25
- import { createScratchpad } from "../runtime/auth-flow";
26
- import { createBrowserClient } from "../runtime/browser";
27
- import { createProviderCache } from "../runtime/cache";
32
+ } from "../observability.js";
33
+ import { createScratchpad } from "../runtime/auth-flow.js";
34
+ import { createBrowserClient } from "../runtime/browser.js";
35
+ import { createProviderCache } from "../runtime/cache.js";
28
36
  import {
29
37
  createProviderChoiceContext,
30
38
  PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
31
- } from "../runtime/choice";
32
- import { createCredentialContext } from "../runtime/credential";
33
- import { createEnvContext } from "../runtime/env";
34
- import { executeOperation } from "../runtime/executor";
35
- import { createHttpClient } from "../runtime/http";
36
- import { wrapWithInstrumentation } from "../runtime/instrumentation";
37
- import { getProviderBaseUrl } from "../runtime/provider";
39
+ } from "../runtime/choice.js";
40
+ import { createCredentialContext } from "../runtime/credential.js";
41
+ import { createEnvContext } from "../runtime/env.js";
42
+ import { executeOperation } from "../runtime/executor.js";
43
+ import { createHttpClient } from "../runtime/http.js";
44
+ import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
45
+ import {
46
+ createEnvVendorCredentialResolver,
47
+ createNativeNetworkClient,
48
+ } from "../runtime/native-network.js";
49
+ import { getProviderBaseUrl } from "../runtime/provider.js";
38
50
  import {
39
51
  PROXY_AUTH_IP_DENIED_CODE,
40
52
  PROXY_EDGE_AUTH_REJECTED_CODE,
41
53
  PROXY_POOL_EXHAUSTED_CODE,
42
- } from "../runtime/proxy-errors";
54
+ } from "../runtime/proxy-errors.js";
55
+ import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector } from "../runtime/proxy-telemetry.js";
43
56
  import {
44
- PROVIDER_TELEMETRY_HEADER,
45
- ProxyTelemetryCollector,
46
- } from "../runtime/proxy-telemetry";
57
+ assertRequiredSecretsPresent,
58
+ listMissingRequiredSecrets,
59
+ MISSING_SECRET_CODE,
60
+ } from "../runtime/secrets.js";
47
61
  import {
48
62
  createProviderRuntimeStateFromEnv,
49
63
  createUnsupportedProviderRuntimeState,
50
- } from "../runtime/state";
51
- import { createStealthClient } from "../runtime/stealth";
52
- import { createSttClientFromEnv } from "../runtime/stt";
53
- import { createTraceContext } from "../runtime/trace";
54
- import { parseSchema } from "../schema";
55
- import { getStealthProfile } from "../stealth/profiles";
64
+ } from "../runtime/state.js";
65
+ import { createStealthClient } from "../runtime/stealth.js";
66
+ import { createSttClientFromEnv } from "../runtime/stt.js";
67
+ import { createTraceContext } from "../runtime/trace.js";
68
+ import { parseSchema } from "../schema.js";
69
+ import {
70
+ STATEFUL_NONCE_HEADER as STATEFUL_FORWARDING_NONCE_HEADER,
71
+ STATEFUL_SIGNATURE_HEADER as STATEFUL_FORWARDING_SIGNATURE_HEADER,
72
+ STATEFUL_TIMESTAMP_HEADER as STATEFUL_FORWARDING_TIMESTAMP_HEADER,
73
+ verifyStatefulRequestSignature,
74
+ } from "../stateful-signing.js";
75
+ import { StatefulRoutingDeadlineError } from "../stateful/stateful-provider-session-routing.js";
76
+ import { getStealthProfile } from "../stealth/profiles.js";
56
77
  import {
57
78
  APIFUSE_STREAM_DONE_EVENT,
58
79
  APIFUSE_STREAM_ERROR_EVENT,
59
80
  encodeSseEvent,
60
81
  error as streamError,
61
- } from "../stream";
82
+ } from "../stream.js";
62
83
  import type {
63
84
  AuthContext,
64
85
  AuthTurn,
@@ -67,37 +88,118 @@ import type {
67
88
  FlowContextStore,
68
89
  HttpRetrySummary,
69
90
  OperationDefinition,
91
+ OperationErrorCode,
70
92
  OperationHttpStreamTransport,
71
93
  OperationSseTransport,
94
+ ProviderErrorStatus,
72
95
  ProviderContext,
73
96
  ProviderDefinition,
97
+ ProviderProxyPolicy,
74
98
  ProviderRuntimeState,
75
99
  ProviderStreamEvent,
76
100
  StealthClient,
77
101
  SttContext,
78
- } from "../types";
102
+ } from "../types.js";
103
+ import { VALID_OPERATION_ERROR_STATUSES } from "../types.js";
79
104
  import {
80
105
  createSelfTestApp,
106
+ createSelfTestAuthFlowInvoke,
81
107
  createSelfTestInvoke,
82
108
  resolveSelfTestPort,
83
- } from "./self-test";
84
- import { resolveSelfTestMasterSecrets } from "./self-test-token";
109
+ } from "./self-test.js";
110
+ import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
85
111
  import {
86
112
  type AuthFlowRequest,
87
113
  AuthFlowRequestSchema,
88
114
  type AuthFlowResponse,
89
115
  type AuthFlowSuccessResponse,
116
+ OperationConnectionSchema,
90
117
  type OperationErrorResponse,
91
118
  type OperationRequest,
92
119
  OperationRequestSchema,
93
120
  type OperationResponse,
94
121
  type OperationSuccessResponse,
95
- } from "./types";
122
+ } from "./types.js";
96
123
 
97
124
  const DEFAULT_HOST = "0.0.0.0";
98
125
  const DEFAULT_PORT = 3000;
126
+ /** Compact SDK-owned error classification emitted separately from the public response body. */
127
+ export const ERROR_OBSERVABILITY_HEADER = "X-ApiFuse-Error-Observability";
128
+ export type ErrorObservabilityDetails = {
129
+ category: ProviderErrorCategory;
130
+ taxonomyVersion: string;
131
+ retryable: boolean;
132
+ upstreamStatus?: number;
133
+ };
99
134
  const AUTH_FLOW_LOCALES = ["en", "ko", "ja"] as const;
100
135
  const retryResponseMeta = new WeakMap<ProviderContext, HttpRetrySummary>();
136
+ const STATEFUL_INTERNAL_OPERATIONS_ROUTE = "/__apifuse/stateful/operations";
137
+ const STATEFUL_FORWARDING_SOURCE_POD_HEADER = "x-apifuse-stateful-source-pod";
138
+ const DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS = 5 * 60_000;
139
+ const DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES = 10_000;
140
+ const STATEFUL_FORWARDING_REPLAY_BUCKET_MS = 10_000;
141
+ const STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS = Math.ceil(
142
+ STATEFUL_FORWARDING_REPLAY_BUCKET_MS / 1_000,
143
+ );
144
+
145
+ export const ProviderServerStatefulForwardEnvelopeSchema = z
146
+ .object({
147
+ requestId: z.string().min(1),
148
+ providerId: z.string().min(1),
149
+ operationId: z.string().min(1),
150
+ sessionKey: z.string().min(1),
151
+ connectionId: z.string().min(1),
152
+ serviceAccountId: z.string().min(1),
153
+ ownerPodId: z.string().min(1),
154
+ generation: z.number().int().positive(),
155
+ sourcePodId: z.string().min(1),
156
+ forwardedAt: z.string().refine((value) => Number.isFinite(Date.parse(value))),
157
+ deadlineAt: z
158
+ .string()
159
+ .refine((value) => Number.isFinite(Date.parse(value)))
160
+ .optional(),
161
+ idempotencyKey: z.string().min(1).optional(),
162
+ operationRequest: OperationRequestSchema.extend({
163
+ connection: OperationConnectionSchema.strict().optional(),
164
+ }).strict(),
165
+ })
166
+ .strict();
167
+
168
+ export type ProviderServerStatefulForwardEnvelope = Readonly<
169
+ z.infer<typeof ProviderServerStatefulForwardEnvelopeSchema>
170
+ >;
171
+
172
+ export type ProviderServerStatefulOwnerFence = Readonly<
173
+ Pick<
174
+ ProviderServerStatefulForwardEnvelope,
175
+ | "providerId"
176
+ | "sessionKey"
177
+ | "ownerPodId"
178
+ | "generation"
179
+ | "sourcePodId"
180
+ | "forwardedAt"
181
+ | "requestId"
182
+ | "idempotencyKey"
183
+ >
184
+ >;
185
+
186
+ export type ProviderServerStatefulOwnerFenceValidator = (
187
+ fence: ProviderServerStatefulOwnerFence,
188
+ signal: AbortSignal,
189
+ ) => boolean | Promise<boolean>;
190
+
191
+ export type ProviderServerOperationExecutorInput = {
192
+ readonly provider: ProviderDefinition;
193
+ readonly operationId: string;
194
+ readonly ctx: ProviderContext;
195
+ readonly request: OperationRequest & { readonly deadlineAt?: string };
196
+ readonly signal?: AbortSignal;
197
+ readonly internalStatefulForward?: ProviderServerStatefulForwardEnvelope;
198
+ };
199
+
200
+ export type ProviderServerOperationExecutor = (
201
+ input: ProviderServerOperationExecutorInput,
202
+ ) => Promise<unknown>;
101
203
 
102
204
  type RequestCleanup = () => void | Promise<void>;
103
205
 
@@ -156,9 +258,7 @@ function createStealthStub(): StealthClient {
156
258
  };
157
259
  }
158
260
 
159
- function getProviderStealthBaseUrl(
160
- provider: ProviderDefinition,
161
- ): string | undefined {
261
+ function getProviderStealthBaseUrl(provider: ProviderDefinition): string | undefined {
162
262
  const baseUrl = getProviderBaseUrl(provider);
163
263
  if (baseUrl) {
164
264
  return baseUrl;
@@ -168,15 +268,10 @@ function getProviderStealthBaseUrl(
168
268
  }
169
269
 
170
270
  function getProviderStealthProfile(provider: ProviderDefinition) {
171
- return provider.stealth?.profile
172
- ? getStealthProfile(provider.stealth.profile)
173
- : undefined;
271
+ return provider.stealth?.profile ? getStealthProfile(provider.stealth.profile) : undefined;
174
272
  }
175
273
 
176
- function isProductionProviderBrowserMode(
177
- provider: ProviderDefinition,
178
- env = process.env,
179
- ): boolean {
274
+ function isProductionProviderBrowserMode(provider: ProviderDefinition, env = process.env): boolean {
180
275
  if (provider.runtime !== "browser") {
181
276
  return false;
182
277
  }
@@ -185,9 +280,7 @@ function isProductionProviderBrowserMode(
185
280
  return true;
186
281
  }
187
282
 
188
- return (
189
- env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id
190
- );
283
+ return env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id;
191
284
  }
192
285
 
193
286
  export function resolveProviderProxyAffinityKey(
@@ -195,24 +288,26 @@ export function resolveProviderProxyAffinityKey(
195
288
  request: OperationRequest,
196
289
  operationId: string,
197
290
  ): string {
198
- const connectionKey =
199
- resolveOperationConnectionId(request) ?? request.connection?.externalRef;
291
+ const connectionKey = resolveOperationConnectionId(request) ?? request.connection?.externalRef;
200
292
  const affinity =
201
- typeof provider.proxy === "object"
202
- ? provider.proxy.session?.affinity
203
- : undefined;
293
+ typeof provider.proxy === "object" ? provider.proxy.session?.affinity : undefined;
204
294
  if (affinity === "operation") {
205
295
  return `${provider.id}/${operationId}`;
206
296
  }
207
297
  return connectionKey ?? provider.id;
208
298
  }
209
299
 
210
- function resolveOperationConnectionId(
211
- request: OperationRequest,
212
- ): string | undefined {
300
+ function resolveOperationConnectionId(request: OperationRequest): string | undefined {
213
301
  return request.connection?.id ?? request.connectionId;
214
302
  }
215
303
 
304
+ function resolveNativeProxyPolicy(provider: ProviderDefinition): ProviderProxyPolicy | undefined {
305
+ if (typeof provider.proxy === "object") return provider.proxy;
306
+ if (provider.proxy === true) return { mode: "optional" };
307
+ if (provider.proxy === false) return { mode: "disabled" };
308
+ return undefined;
309
+ }
310
+
216
311
  function createProviderContext(
217
312
  provider: ProviderDefinition,
218
313
  request: OperationRequest,
@@ -226,11 +321,7 @@ function createProviderContext(
226
321
  const stealthProfile = getProviderStealthProfile(provider);
227
322
  const proxyClientOptions = {
228
323
  upstream: { proxy: provider.proxy },
229
- affinityKey: resolveProviderProxyAffinityKey(
230
- provider,
231
- request,
232
- operationId,
233
- ),
324
+ affinityKey: resolveProviderProxyAffinityKey(provider, request, operationId),
234
325
  telemetry: proxyTelemetry,
235
326
  };
236
327
  let wrappedContext: ProviderContext | undefined;
@@ -269,11 +360,7 @@ function createProviderContext(
269
360
  state,
270
361
  stealth: stealthBaseUrl
271
362
  ? stealthProfile
272
- ? createStealthClient(
273
- stealthBaseUrl,
274
- stealthProfile.name,
275
- stealthClientOptions,
276
- )
363
+ ? createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
277
364
  : createStealthClient(stealthBaseUrl, stealthClientOptions)
278
365
  : createStealthStub(),
279
366
  browser:
@@ -287,6 +374,18 @@ function createProviderContext(
287
374
  engine: provider.browser?.engine,
288
375
  })
289
376
  : createBrowserStub(),
377
+ ...(provider.native
378
+ ? {
379
+ native: {
380
+ network: createNativeNetworkClient({
381
+ egress: provider.native.network,
382
+ proxyPolicy: resolveNativeProxyPolicy(provider),
383
+ affinityKey: proxyClientOptions.affinityKey,
384
+ credentials: createEnvVendorCredentialResolver(env),
385
+ }),
386
+ },
387
+ }
388
+ : {}),
290
389
  trace: createTraceContext(),
291
390
  auth: createAuthStub(),
292
391
  stt: options.stt ?? createSttClientFromEnv(provider.stt),
@@ -386,13 +485,23 @@ function createAuthFlowContext(
386
485
  http: createHttpClient(baseUrl, proxyClientOptions),
387
486
  stealth: stealthBaseUrl
388
487
  ? stealthProfile
389
- ? createStealthClient(
390
- stealthBaseUrl,
391
- stealthProfile.name,
392
- stealthClientOptions,
393
- )
488
+ ? createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
394
489
  : createStealthClient(stealthBaseUrl, stealthClientOptions)
395
490
  : createStealthStub(),
491
+ ...(provider.native
492
+ ? {
493
+ native: {
494
+ network: createNativeNetworkClient({
495
+ egress: provider.native.network,
496
+ proxyPolicy: resolveNativeProxyPolicy(provider),
497
+ affinityKey: proxyClientOptions.affinityKey,
498
+ credentials: createEnvVendorCredentialResolver(
499
+ createEnvContext(provider.secrets?.map((secret) => secret.name)),
500
+ ),
501
+ }),
502
+ },
503
+ }
504
+ : {}),
396
505
  env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
397
506
  credential,
398
507
  context: flowContextStore.context,
@@ -433,8 +542,16 @@ export type ProviderServerLogEvent =
433
542
  errorCategory?: ProviderErrorCategory;
434
543
  taxonomyVersion?: string;
435
544
  retryable?: boolean;
545
+ signal?: "unregistered_provider_error_code";
546
+ signalFix?: string;
436
547
  issues?: Array<{ path: string; code: string; message: string }>;
437
548
  })
549
+ | {
550
+ level: "warn";
551
+ event: "provider_secrets_missing";
552
+ providerId: string;
553
+ missingSecrets: string[];
554
+ }
438
555
  | {
439
556
  level: "warn";
440
557
  event: "provider_cleanup_failed";
@@ -445,18 +562,59 @@ export type ProviderServerLogEvent =
445
562
  resource: "browser" | "stealth";
446
563
  errorClass: string;
447
564
  message: string;
565
+ }
566
+ | {
567
+ level: "error";
568
+ event: "provider_shutdown_hook_failed";
569
+ providerId: string;
570
+ hookIndex: number;
571
+ errorClass: string;
572
+ message: string;
448
573
  };
449
574
 
450
575
  export type ProviderServerLogger = (event: ProviderServerLogEvent) => void;
451
576
 
452
577
  export type ProviderServerOptions = {
453
578
  logger?: ProviderServerLogger;
579
+ /** Optional provider-specific operation executor. Stateful providers use this to preserve provider-local runtime semantics. */
580
+ operationExecutor?: ProviderServerOperationExecutor;
581
+ /** Optional signed internal executor for stateful owner forwarding. */
582
+ internalOperationExecutor?: ProviderServerOperationExecutor;
583
+ statefulForwarding?: {
584
+ readonly secret: string;
585
+ readonly maxSkewMs?: number;
586
+ readonly replayCacheMaxEntries?: number;
587
+ /** Required fail-closed check against the SDK/runtime owner registry. */
588
+ readonly validateOwnerFence: ProviderServerStatefulOwnerFenceValidator;
589
+ };
454
590
  /** Optional STT override for tests or custom hosts; local/prod normally resolves from env. */
455
591
  stt?: SttContext;
456
592
  /** Optional runtime state override for tests or custom hosts. Production resolves Redis from env and fails closed when unavailable. */
457
593
  state?: ProviderRuntimeState;
458
594
  /** Allow process-local runtime state only for local development and tests. */
459
595
  allowMemoryStateFallback?: boolean;
596
+ /**
597
+ * Graceful process shutdown. Hooks run in declaration order after listeners stop accepting work.
598
+ *
599
+ * @example
600
+ * ```ts
601
+ * await serve(provider, {
602
+ * shutdown: {
603
+ * hooks: [
604
+ * async () => { await emitter.flush(); },
605
+ * async () => { await sessionManager.closeAll("server-shutdown"); },
606
+ * async () => { await lease.release(); },
607
+ * async () => { await router.close(); },
608
+ * ],
609
+ * },
610
+ * });
611
+ * ```
612
+ */
613
+ shutdown?: {
614
+ readonly hooks?: Array<() => Promise<void>>;
615
+ readonly signals?: boolean | NodeJS.Signals[];
616
+ readonly timeoutMs?: number;
617
+ };
460
618
  };
461
619
 
462
620
  const defaultProviderServerLogger: ProviderServerLogger = (event) => {
@@ -503,19 +661,57 @@ function zodDetails(error: z.ZodError): Array<{
503
661
  }));
504
662
  }
505
663
 
664
+ // Category-level projection with code-aware honesty overrides: a missing
665
+ // deployment secret is an APIFuse-side defect even though its category
666
+ // (credential_unavailable) usually means a caller credential problem, an
667
+ // internal stateful-routing deadline is APIFuse-owned despite its timeout
668
+ // category, and the built-in upstream failure families keep their upstream
669
+ // attribution even when the author left the category at the provider_error
670
+ // default.
671
+ function publicErrorSource(
672
+ error: unknown,
673
+ category: ProviderErrorCategory,
674
+ ): ProviderErrorSource {
675
+ if (error instanceof StatefulRoutingDeadlineError) return "apifuse";
676
+ if (isProviderError(error)) {
677
+ if (error.code === MISSING_SECRET_CODE) return "apifuse";
678
+ if (error.code === "UPSTREAM_ERROR" || error.code === "BLOCKED") {
679
+ return "upstream_failure";
680
+ }
681
+ }
682
+ return sourceForCategory(category);
683
+ }
684
+
506
685
  function toErrorResponse(
507
686
  error: unknown,
508
687
  requestId?: string,
688
+ declaredErrorCode?: OperationErrorCode,
509
689
  ): OperationErrorResponse {
510
- if (error instanceof ProviderError) {
511
- const details = publicProviderErrorDetails(error);
690
+ const observability = errorObservabilityDetails(error, declaredErrorCode);
691
+ const source = publicErrorSource(error, observability.category);
692
+ if (error instanceof StatefulRoutingDeadlineError) {
693
+ return {
694
+ error: {
695
+ code: "STATEFUL_FORWARDING_DEADLINE_EXPIRED",
696
+ message: "Stateful forwarding deadline expired.",
697
+ ...(requestId ? { requestId } : {}),
698
+ retryable: observability.retryable,
699
+ source,
700
+ },
701
+ };
702
+ }
703
+
704
+ if (isProviderError(error)) {
705
+ const details = error.details;
512
706
  return {
513
707
  error: {
514
708
  code: error.code ?? "provider_error",
515
709
  message: publicProviderErrorMessage(error),
516
710
  ...(requestId ? { requestId } : {}),
711
+ retryable: observability.retryable,
712
+ source,
517
713
  ...(error.fix ? { fix: error.fix } : {}),
518
- ...(details ? { details } : {}),
714
+ ...(details !== undefined ? { details } : {}),
519
715
  },
520
716
  };
521
717
  }
@@ -526,64 +722,72 @@ function toErrorResponse(
526
722
  code: "invalid_request",
527
723
  message: "Invalid request body",
528
724
  ...(requestId ? { requestId } : {}),
725
+ retryable: observability.retryable,
726
+ source,
529
727
  details: zodDetails(error),
530
728
  },
531
729
  };
532
730
  }
533
731
 
732
+ // A masked internal error MUST NOT be advertised as retryable: without an
733
+ // explicit retryable:false the hub (bori provider-backed engine) defaults 5xx
734
+ // to retryable:true, which turns a deterministic pre-upstream crash into an
735
+ // infinite START->CONTINUE->restart loop (2026-07-22 catchtable reserve RCA).
736
+ // We still refuse to leak message/stack — only the error class name (or the
737
+ // primitive type for non-Error throwables) is surfaced for ops triage.
534
738
  return {
535
739
  error: {
536
740
  code: "internal_error",
537
741
  message: "Internal error",
538
742
  ...(requestId ? { requestId } : {}),
743
+ retryable: observability.retryable,
744
+ source,
745
+ details: {
746
+ retryable: false,
747
+ category: "internal_error",
748
+ errorClass: error instanceof Error ? error.name : typeof error,
749
+ },
539
750
  },
540
751
  };
541
752
  }
542
753
 
543
- function publicProviderErrorDetails(error: ProviderError): unknown {
544
- const providerDetails = error.details;
545
- const observabilityDetails = providerObservabilityDetails(error);
546
-
547
- if (providerDetails === undefined) {
548
- return observabilityDetails;
549
- }
550
- if (observabilityDetails === undefined) {
551
- return providerDetails;
552
- }
553
- if (isPlainRecord(providerDetails) && isPlainRecord(observabilityDetails)) {
554
- return { ...providerDetails, ...observabilityDetails };
555
- }
556
- return {
557
- provider: providerDetails,
558
- observability: observabilityDetails,
559
- };
560
- }
561
-
562
- function isPlainRecord(value: unknown): value is Record<string, unknown> {
563
- return value !== null && typeof value === "object" && !Array.isArray(value);
564
- }
565
-
566
- function providerObservabilityDetails(error: ProviderError):
567
- | {
568
- category: ProviderErrorCategory;
569
- taxonomyVersion: string;
570
- retryable: boolean;
571
- upstreamStatus?: number;
572
- }
573
- | undefined {
754
+ // Accepts `unknown` so the branded guards narrow cleanly from the top: the
755
+ // subtype error classes are structurally compatible with ProviderError, so
756
+ // narrowing from a ProviderError-typed value would collapse the negative branch
757
+ // to `never`. Narrowing from unknown avoids that while still recognizing errors
758
+ // from a duplicate SDK module instance.
759
+ function providerObservabilityDetails(
760
+ error: unknown,
761
+ declaredErrorCode?: OperationErrorCode,
762
+ ): ErrorObservabilityDetails | undefined {
763
+ const declaredRetryable = sdkOwnsErrorResolution(error)
764
+ ? undefined
765
+ : declaredErrorCode?.retryable;
574
766
  // Session-expiry surfaces the credential_expired category + the opt-in
575
767
  // retryable signal so Gateway/Credential Service can refresh and re-drive the
576
768
  // operation (see design.md §4.3 D3). Without this branch the auth error would
577
769
  // serialize as a bare 401 with no retryable/category, losing the refresh
578
770
  // signal for exactly the retryOnAuthRefresh operations it is meant to enable.
579
- if (error instanceof SessionExpiredError) {
771
+ if (isSessionExpiredError(error)) {
580
772
  return {
581
773
  category: error.options?.category ?? "credential_expired",
582
774
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
583
- retryable: error.options?.retryable ?? false,
775
+ retryable: error.options?.retryable ?? declaredRetryable ?? false,
776
+ };
777
+ }
778
+ // Missing-secret errors carry the canonical credential_unavailable category
779
+ // so Gateway/observability can attribute the failure to provisioning, not
780
+ // the upstream. Matched by code (not constructor) so both the SDK-owned
781
+ // runtime gate and any not-yet-migrated provider-thrown MISSING_SECRET
782
+ // serialize identically, including across duplicate SDK module instances.
783
+ if (isProviderError(error) && error.code === MISSING_SECRET_CODE) {
784
+ return {
785
+ category: error.options?.category ?? "credential_unavailable",
786
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
787
+ retryable: error.options?.retryable ?? declaredRetryable ?? false,
584
788
  };
585
789
  }
586
- if (!(error instanceof TransportError)) {
790
+ if (!isTransportError(error)) {
587
791
  return undefined;
588
792
  }
589
793
  const isProxyPoolCode =
@@ -615,8 +819,86 @@ function providerObservabilityDetails(error: ProviderError):
615
819
  };
616
820
  }
617
821
 
822
+ function errorObservabilityDetails(
823
+ error: unknown,
824
+ declaredErrorCode?: OperationErrorCode,
825
+ ): ErrorObservabilityDetails {
826
+ const effectiveDeclaration = sdkOwnsErrorResolution(error) ? undefined : declaredErrorCode;
827
+ const providerDetails = providerObservabilityDetails(error, effectiveDeclaration);
828
+ if (providerDetails) return providerDetails;
829
+
830
+ if (error instanceof z.ZodError || isValidationError(error)) {
831
+ const declaredStatus = effectiveDeclaration?.status;
832
+ return {
833
+ category:
834
+ isProviderError(error) && error.options?.category
835
+ ? error.options.category
836
+ : isEmittableErrorStatus(declaredStatus) &&
837
+ categoryForStatus(declaredStatus) === "upstream_rejected"
838
+ ? "upstream_rejected"
839
+ : isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
840
+ ? "provider_error"
841
+ : "input_validation",
842
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
843
+ retryable: isProviderError(error)
844
+ ? (error.options?.retryable ?? effectiveDeclaration?.retryable ?? false)
845
+ : false,
846
+ };
847
+ }
848
+
849
+ if (error instanceof StatefulRoutingDeadlineError) {
850
+ return {
851
+ category: "timeout",
852
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
853
+ retryable: false,
854
+ };
855
+ }
856
+
857
+ if (isProviderError(error)) {
858
+ // Deterministic upstream refusals default to the rejection category:
859
+ // the UPSTREAM_REJECTED family and any operation-declared rejection
860
+ // status (409/410/422) classify as upstream_rejected unless the
861
+ // author set an explicit category.
862
+ const declaredStatus = effectiveDeclaration?.status;
863
+ const rejectionDefault =
864
+ error.code === "UPSTREAM_REJECTED" ||
865
+ (isEmittableErrorStatus(declaredStatus) &&
866
+ categoryForStatus(declaredStatus) === "upstream_rejected")
867
+ ? ("upstream_rejected" as const)
868
+ : ("provider_error" as const);
869
+ return {
870
+ category: error.options?.category ?? rejectionDefault,
871
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
872
+ retryable: error.options?.retryable ?? effectiveDeclaration?.retryable ?? false,
873
+ };
874
+ }
875
+
876
+ return {
877
+ category: "internal_error",
878
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
879
+ retryable: false,
880
+ };
881
+ }
882
+
883
+ function responseWithErrorObservability(
884
+ response: Response,
885
+ error: unknown,
886
+ declaredErrorCode?: OperationErrorCode,
887
+ ): Response {
888
+ const headers = new Headers(response.headers);
889
+ headers.set(
890
+ ERROR_OBSERVABILITY_HEADER,
891
+ JSON.stringify(errorObservabilityDetails(error, declaredErrorCode)),
892
+ );
893
+ return new Response(response.body, {
894
+ status: response.status,
895
+ statusText: response.statusText,
896
+ headers,
897
+ });
898
+ }
899
+
618
900
  function publicProviderErrorMessage(error: ProviderError): string {
619
- if (error instanceof TransportError) {
901
+ if (isTransportError(error)) {
620
902
  if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
621
903
  return error.message;
622
904
  }
@@ -639,22 +921,35 @@ function publicProviderErrorMessage(error: ProviderError): string {
639
921
  return error.message;
640
922
  }
641
923
 
642
- function toStatusCode(
643
- error: unknown,
644
- ): 400 | 401 | 404 | 429 | 500 | 502 | 503 | 504 {
924
+ function isEmittableErrorStatus(value: unknown): value is ProviderErrorStatus {
925
+ return (
926
+ typeof value === "number" &&
927
+ VALID_OPERATION_ERROR_STATUSES.some((status) => status === value)
928
+ );
929
+ }
930
+
931
+ function toStatusCode(error: unknown, declaredErrorCode?: OperationErrorCode): ProviderErrorStatus {
645
932
  if (error instanceof z.ZodError) {
646
933
  return 400;
647
934
  }
648
-
649
- if (error instanceof TransportError) {
650
- return error.code === "transport_timeout" ? 504 : 502;
935
+ if (error instanceof StatefulRoutingDeadlineError) {
936
+ return 504;
651
937
  }
652
-
653
- if (error instanceof ProviderError) {
938
+ if (isProviderError(error)) {
939
+ if (
940
+ !sdkOwnsErrorResolution(error) &&
941
+ isEmittableErrorStatus(declaredErrorCode?.status)
942
+ ) {
943
+ return declaredErrorCode.status;
944
+ }
654
945
  switch (error.code) {
655
946
  case "AUTH_REQUIRED":
656
947
  case "reauth_required":
657
948
  return 401;
949
+ // Unprovisioned declared secret: a deployment/config defect, never an
950
+ // upstream failure — explicit 400 (was only reached via fallthrough).
951
+ case MISSING_SECRET_CODE:
952
+ return 400;
658
953
  case "NOT_FOUND":
659
954
  case "not_found":
660
955
  case "NO_DATA":
@@ -663,20 +958,66 @@ function toStatusCode(
663
958
  case "UPSTREAM_RATE_LIMIT":
664
959
  case "LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR":
665
960
  return 429;
961
+ // Deterministic upstream business refusal (honest-provider-error-
962
+ // contract): the upstream evaluated the request and said no under
963
+ // its own rules — a conflict with upstream state, never a 5xx.
964
+ case "UPSTREAM_REJECTED":
965
+ return 409;
666
966
  case "UPSTREAM_ERROR":
667
967
  case "BLOCKED":
668
968
  return 502;
669
969
  case "STT_UNAVAILABLE":
670
970
  case "UNSUPPORTED_STT_BACKEND":
971
+ case "STATEFUL_FORWARDING_REPLAY_CACHE_FULL":
671
972
  return 503;
672
973
  }
974
+ if (isTransportError(error)) {
975
+ return error.code === "transport_timeout" ? 504 : 502;
976
+ }
977
+ if (isValidationError(error)) {
978
+ return error.options?.category === "output_validation" ? 500 : 400;
979
+ }
673
980
 
674
- return 400;
981
+ return 500;
675
982
  }
676
983
 
677
984
  return 500;
678
985
  }
679
986
 
987
+ function sdkOwnsErrorResolution(error: unknown): boolean {
988
+ if (isSessionExpiredError(error)) return true;
989
+ if (isTransportError(error)) return true;
990
+ if (error instanceof z.ZodError) return true;
991
+ if (error instanceof StatefulRoutingDeadlineError) return true;
992
+ return (
993
+ isProviderError(error) &&
994
+ typeof error.code === "string" &&
995
+ SDK_RUNTIME_OWNED_ERROR_CODES.has(error.code)
996
+ );
997
+ }
998
+
999
+ type OperationErrorCodeLookup = ReadonlyMap<string, ReadonlyMap<string, OperationErrorCode>>;
1000
+
1001
+ function buildOperationErrorCodeLookup(provider: ProviderDefinition): OperationErrorCodeLookup {
1002
+ return new Map(
1003
+ Object.entries(provider.operations).flatMap(([operationId, operation]) => {
1004
+ const errorCodes = operation.docs?.errorCodes;
1005
+ return errorCodes?.length
1006
+ ? [[operationId, new Map(errorCodes.map((entry) => [entry.code, entry]))] as const]
1007
+ : [];
1008
+ }),
1009
+ );
1010
+ }
1011
+
1012
+ function declaredErrorCodeFor(
1013
+ error: unknown,
1014
+ operationId: string | undefined,
1015
+ lookup: OperationErrorCodeLookup,
1016
+ ): OperationErrorCode | undefined {
1017
+ if (!operationId || !isProviderError(error) || typeof error.code !== "string") return undefined;
1018
+ return lookup.get(operationId)?.get(error.code);
1019
+ }
1020
+
680
1021
  function extractRequestId(raw: unknown): string | undefined {
681
1022
  if (!raw || typeof raw !== "object") {
682
1023
  return undefined;
@@ -695,21 +1036,26 @@ function logProviderError(
695
1036
  error: unknown,
696
1037
  status: number,
697
1038
  cost: ProviderRequestCost,
1039
+ declaredErrorCode?: OperationErrorCode,
698
1040
  ): void {
699
- const code =
700
- error instanceof ProviderError
701
- ? (error.code ?? "provider_error")
702
- : error instanceof z.ZodError
703
- ? "invalid_request"
1041
+ const code = isProviderError(error)
1042
+ ? (error.code ?? "provider_error")
1043
+ : error instanceof z.ZodError
1044
+ ? "invalid_request"
1045
+ : error instanceof StatefulRoutingDeadlineError
1046
+ ? "STATEFUL_FORWARDING_DEADLINE_EXPIRED"
704
1047
  : "internal_error";
705
1048
  const errorClass = error instanceof Error ? error.name : typeof error;
706
1049
  const message = error instanceof Error ? error.message : String(error);
707
- const details =
708
- error instanceof ProviderError
709
- ? providerObservabilityDetails(error)
710
- : undefined;
711
- const emit =
712
- typeof logger === "function" ? logger : defaultProviderServerLogger;
1050
+ const details = errorObservabilityDetails(error, declaredErrorCode);
1051
+ const isUnregisteredProviderErrorCode =
1052
+ status === 500 &&
1053
+ isProviderError(error) &&
1054
+ !isValidationError(error) &&
1055
+ typeof error.code === "string" &&
1056
+ !SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code) &&
1057
+ declaredErrorCode === undefined;
1058
+ const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
713
1059
  emit({
714
1060
  level: status >= 500 ? "error" : "warn",
715
1061
  event: "provider_request_failed",
@@ -722,14 +1068,15 @@ function logProviderError(
722
1068
  code,
723
1069
  errorClass,
724
1070
  message,
725
- ...(error instanceof TransportError && error.upstreamStatus
726
- ? { upstreamStatus: error.upstreamStatus }
727
- : {}),
728
- ...(details
1071
+ ...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
1072
+ errorCategory: details.category,
1073
+ taxonomyVersion: details.taxonomyVersion,
1074
+ retryable: details.retryable,
1075
+ ...(isUnregisteredProviderErrorCode
729
1076
  ? {
730
- errorCategory: details.category,
731
- taxonomyVersion: details.taxonomyVersion,
732
- retryable: details.retryable,
1077
+ signal: "unregistered_provider_error_code" as const,
1078
+ signalFix:
1079
+ "Declare this code (with status and retryable) in the operation's docs.errorCodes so it serves its intended status instead of 500.",
733
1080
  }
734
1081
  : {}),
735
1082
  ...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
@@ -744,8 +1091,7 @@ function logProviderCleanupError(
744
1091
  resource: "browser" | "stealth",
745
1092
  error: unknown,
746
1093
  ): void {
747
- const emit =
748
- typeof logger === "function" ? logger : defaultProviderServerLogger;
1094
+ const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
749
1095
  const errorClass = error instanceof Error ? error.name : typeof error;
750
1096
  const message = error instanceof Error ? error.message : String(error);
751
1097
  emit({
@@ -770,8 +1116,7 @@ function logProviderSuccess(
770
1116
  status: number,
771
1117
  cost: ProviderRequestCost,
772
1118
  ): void {
773
- const emit =
774
- typeof logger === "function" ? logger : defaultProviderServerLogger;
1119
+ const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
775
1120
  emit({
776
1121
  level: "info",
777
1122
  event: "provider_request_completed",
@@ -817,18 +1162,13 @@ function toJsonSuccessResponse(
817
1162
  };
818
1163
  }
819
1164
 
820
- function isAsyncIterable<T = unknown>(
821
- value: unknown,
822
- ): value is AsyncIterable<T> {
1165
+ function isAsyncIterable<T = unknown>(value: unknown): value is AsyncIterable<T> {
823
1166
  if (!value || typeof value !== "object") return false;
824
1167
  const iterator = Reflect.get(value, Symbol.asyncIterator);
825
1168
  return typeof iterator === "function";
826
1169
  }
827
1170
 
828
- function responseWithCleanup(
829
- response: Response,
830
- cleanup: RequestCleanup,
831
- ): Response {
1171
+ function responseWithCleanup(response: Response, cleanup: RequestCleanup): Response {
832
1172
  if (!response.body) {
833
1173
  void cleanup();
834
1174
  return response;
@@ -877,10 +1217,7 @@ async function validateSseEvent(
877
1217
  const transport = getSseTransport(operation);
878
1218
  const schema = transport?.events?.[event.event];
879
1219
  if (!schema) {
880
- if (
881
- event.event === APIFUSE_STREAM_ERROR_EVENT ||
882
- event.event === APIFUSE_STREAM_DONE_EVENT
883
- ) {
1220
+ if (event.event === APIFUSE_STREAM_ERROR_EVENT || event.event === APIFUSE_STREAM_DONE_EVENT) {
884
1221
  return event;
885
1222
  }
886
1223
  throw new ProviderError(
@@ -893,11 +1230,7 @@ async function validateSseEvent(
893
1230
  },
894
1231
  );
895
1232
  }
896
- const data = await parseSchema(
897
- schema,
898
- event.data,
899
- `transport.events.${event.event}`,
900
- );
1233
+ const data = await parseSchema(schema, event.data, `transport.events.${event.event}`);
901
1234
  return { ...event, data };
902
1235
  }
903
1236
 
@@ -917,8 +1250,7 @@ function assertStreamPayloadWithinLimit(
917
1250
  throw new ProviderError(
918
1251
  `Stream ${kind} exceeded declared byte limit (${actualBytes} > ${maxBytes}).`,
919
1252
  {
920
- code:
921
- kind === "event" ? "STREAM_EVENT_TOO_LARGE" : "STREAM_CHUNK_TOO_LARGE",
1253
+ code: kind === "event" ? "STREAM_EVENT_TOO_LARGE" : "STREAM_CHUNK_TOO_LARGE",
922
1254
  retryable: false,
923
1255
  category: "input_validation",
924
1256
  fix:
@@ -963,15 +1295,10 @@ function toSseResponse(
963
1295
  const validated = await validateSseEvent(operation, next.value);
964
1296
  const encodedEvent = encodeSseEvent(validated);
965
1297
  const encodedBytes = encoder.encode(encodedEvent);
966
- assertStreamPayloadWithinLimit(
967
- encodedBytes.byteLength,
968
- transport?.maxEventBytes,
969
- "event",
970
- );
1298
+ assertStreamPayloadWithinLimit(encodedBytes.byteLength, transport?.maxEventBytes, "event");
971
1299
  controller.enqueue(encodedBytes);
972
1300
  } catch (error) {
973
- const message =
974
- error instanceof Error ? error.message : "Stream failed";
1301
+ const message = error instanceof Error ? error.message : "Stream failed";
975
1302
  controller.enqueue(
976
1303
  encoder.encode(
977
1304
  encodeSseEvent(
@@ -1018,11 +1345,7 @@ function enforceStreamChunkLimit(
1018
1345
  return;
1019
1346
  }
1020
1347
  if (value) {
1021
- assertStreamPayloadWithinLimit(
1022
- byteLength(value),
1023
- maxChunkBytes,
1024
- "chunk",
1025
- );
1348
+ assertStreamPayloadWithinLimit(byteLength(value), maxChunkBytes, "chunk");
1026
1349
  controller.enqueue(value);
1027
1350
  }
1028
1351
  } catch (error) {
@@ -1042,10 +1365,7 @@ function toStreamingResponse(
1042
1365
  requestId?: string,
1043
1366
  ): Response {
1044
1367
  const transport = operation.transport?.kind ?? "json";
1045
- if (
1046
- transport === "sse" &&
1047
- (result instanceof Response || result instanceof ReadableStream)
1048
- ) {
1368
+ if (transport === "sse" && (result instanceof Response || result instanceof ReadableStream)) {
1049
1369
  void cleanup();
1050
1370
  throw new ProviderError(
1051
1371
  "SSE operations must return an AsyncIterable of typed stream.event(...) values.",
@@ -1059,20 +1379,13 @@ function toStreamingResponse(
1059
1379
  }
1060
1380
  if (result instanceof Response) {
1061
1381
  const httpTransport = getHttpStreamTransport(operation);
1062
- if (
1063
- httpTransport &&
1064
- result.body &&
1065
- httpTransport?.maxChunkBytes !== undefined
1066
- ) {
1382
+ if (httpTransport && result.body && httpTransport?.maxChunkBytes !== undefined) {
1067
1383
  return responseWithCleanup(
1068
- new Response(
1069
- enforceStreamChunkLimit(result.body, httpTransport.maxChunkBytes),
1070
- {
1071
- headers: result.headers,
1072
- status: result.status,
1073
- statusText: result.statusText,
1074
- },
1075
- ),
1384
+ new Response(enforceStreamChunkLimit(result.body, httpTransport.maxChunkBytes), {
1385
+ headers: result.headers,
1386
+ status: result.status,
1387
+ statusText: result.statusText,
1388
+ }),
1076
1389
  cleanup,
1077
1390
  );
1078
1391
  }
@@ -1092,8 +1405,7 @@ function toStreamingResponse(
1092
1405
  : {
1093
1406
  "Content-Type":
1094
1407
  operation.transport?.kind === "http-stream"
1095
- ? (operation.transport.contentType ??
1096
- "application/octet-stream")
1408
+ ? (operation.transport.contentType ?? "application/octet-stream")
1097
1409
  : "application/octet-stream",
1098
1410
  },
1099
1411
  }),
@@ -1113,18 +1425,14 @@ function toStreamingResponse(
1113
1425
  );
1114
1426
  }
1115
1427
 
1116
- function getSseTransport(
1117
- operation: OperationDefinition,
1118
- ): OperationSseTransport | undefined {
1428
+ function getSseTransport(operation: OperationDefinition): OperationSseTransport | undefined {
1119
1429
  return operation.transport?.kind === "sse" ? operation.transport : undefined;
1120
1430
  }
1121
1431
 
1122
1432
  function getHttpStreamTransport(
1123
1433
  operation: OperationDefinition,
1124
1434
  ): OperationHttpStreamTransport | undefined {
1125
- return operation.transport?.kind === "http-stream"
1126
- ? operation.transport
1127
- : undefined;
1435
+ return operation.transport?.kind === "http-stream" ? operation.transport : undefined;
1128
1436
  }
1129
1437
 
1130
1438
  function toAuthFlowResponse(
@@ -1145,9 +1453,7 @@ function toAuthFlowResponse(
1145
1453
  };
1146
1454
  }
1147
1455
 
1148
- function authFlowLocaleFromHeaders(
1149
- headers?: Record<string, string>,
1150
- ): ProviderLocale {
1456
+ function authFlowLocaleFromHeaders(headers?: Record<string, string>): ProviderLocale {
1151
1457
  const header = Object.entries(headers ?? {}).find(
1152
1458
  ([key]) => key.toLowerCase() === "accept-language",
1153
1459
  )?.[1];
@@ -1165,9 +1471,7 @@ function isAuthFlowLocale(value: string | undefined): value is ProviderLocale {
1165
1471
  }
1166
1472
 
1167
1473
  function isAuthTurn(value: unknown): value is AuthTurn {
1168
- return (
1169
- !!value && typeof value === "object" && "kind" in value && "turnId" in value
1170
- );
1474
+ return !!value && typeof value === "object" && "kind" in value && "turnId" in value;
1171
1475
  }
1172
1476
 
1173
1477
  function loadAuthFlowLocaleCatalogs(
@@ -1204,10 +1508,7 @@ function materializeAuthFlowTurn(
1204
1508
  });
1205
1509
  }
1206
1510
 
1207
- function withAuthRequestHeaders(
1208
- request: AuthFlowRequest,
1209
- headers: Headers,
1210
- ): AuthFlowRequest {
1511
+ function withAuthRequestHeaders(request: AuthFlowRequest, headers: Headers): AuthFlowRequest {
1211
1512
  return {
1212
1513
  ...request,
1213
1514
  headers: {
@@ -1225,17 +1526,9 @@ async function handleOperation(
1225
1526
  state: ProviderRuntimeState = createUnsupportedProviderRuntimeState(),
1226
1527
  proxyTelemetry?: ProxyTelemetryCollector,
1227
1528
  ): Promise<Response | OperationResponse> {
1228
- const ctx = createProviderContext(
1229
- provider,
1230
- request,
1231
- operationId,
1232
- options,
1233
- state,
1234
- proxyTelemetry,
1235
- );
1529
+ const ctx = createProviderContext(provider, request, operationId, options, state, proxyTelemetry);
1236
1530
  const operation = provider.operations[operationId];
1237
- const streaming =
1238
- operation?.transport?.kind && operation.transport.kind !== "json";
1531
+ const streaming = operation?.transport?.kind && operation.transport.kind !== "json";
1239
1532
  let cleanupCalled = false;
1240
1533
  const cleanup = async () => {
1241
1534
  if (cleanupCalled) return;
@@ -1266,12 +1559,14 @@ async function handleOperation(
1266
1559
  }
1267
1560
  };
1268
1561
  try {
1269
- const result = await executeOperation(
1270
- provider,
1271
- operationId,
1272
- ctx,
1273
- request.input,
1274
- );
1562
+ const result = options.operationExecutor
1563
+ ? await options.operationExecutor({
1564
+ provider,
1565
+ operationId,
1566
+ ctx,
1567
+ request,
1568
+ })
1569
+ : await executeOperation(provider, operationId, ctx, request.input);
1275
1570
  if (streaming && operation) {
1276
1571
  return toStreamingResponse(operation, result, cleanup, request.requestId);
1277
1572
  }
@@ -1315,13 +1610,16 @@ async function handleAuthFlow(
1315
1610
  });
1316
1611
  }
1317
1612
 
1318
- const { context, getPatch } = createAuthFlowContext(
1319
- provider,
1320
- request,
1321
- options,
1322
- signal,
1323
- );
1613
+ // Same SDK-owned gate as executeOperation: OAuth/credentials ceremonies
1614
+ // depend on declared secrets (client ids/secrets), so fail structured before
1615
+ // any flow code runs instead of at whatever point the ceremony first reads
1616
+ // the env. `abort` stays exempt: a user must always be able to cancel a
1617
+ // stranded flow even when provisioning is broken.
1618
+ const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
1324
1619
  try {
1620
+ if (route !== "abort") {
1621
+ assertRequiredSecretsPresent(provider, context.env);
1622
+ }
1325
1623
  const result =
1326
1624
  route === "start"
1327
1625
  ? await flow.start(context)
@@ -1363,12 +1661,143 @@ async function handleAuthFlow(
1363
1661
  }
1364
1662
  }
1365
1663
 
1664
+ class StatefulForwardingReplayCache {
1665
+ readonly #nonces = new Map<string, number>();
1666
+ readonly #expiryBuckets = new Map<number, Set<string>>();
1667
+ #nextExpiryBucket?: number;
1668
+ #latestExpiryBucket?: number;
1669
+
1670
+ constructor(private readonly maxEntries: number) {}
1671
+
1672
+ claim(nonce: string, expiresAtMs: number, nowMs: number): "accepted" | "replayed" | "full" {
1673
+ this.dropExpiredBuckets(nowMs);
1674
+ if (this.#nonces.has(nonce)) return "replayed";
1675
+ if (this.#nonces.size >= this.maxEntries) return "full";
1676
+ const expiryBucket =
1677
+ Math.ceil(expiresAtMs / STATEFUL_FORWARDING_REPLAY_BUCKET_MS) *
1678
+ STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
1679
+ this.#nonces.set(nonce, expiryBucket);
1680
+ const bucket = this.#expiryBuckets.get(expiryBucket) ?? new Set<string>();
1681
+ bucket.add(nonce);
1682
+ this.#expiryBuckets.set(expiryBucket, bucket);
1683
+ this.#nextExpiryBucket = Math.min(this.#nextExpiryBucket ?? expiryBucket, expiryBucket);
1684
+ this.#latestExpiryBucket = Math.max(this.#latestExpiryBucket ?? expiryBucket, expiryBucket);
1685
+ return "accepted";
1686
+ }
1687
+
1688
+ private dropExpiredBuckets(nowMs: number): void {
1689
+ if (this.#nextExpiryBucket === undefined || this.#latestExpiryBucket === undefined) return;
1690
+ if (nowMs >= this.#latestExpiryBucket) {
1691
+ this.#nonces.clear();
1692
+ this.#expiryBuckets.clear();
1693
+ this.#nextExpiryBucket = undefined;
1694
+ this.#latestExpiryBucket = undefined;
1695
+ return;
1696
+ }
1697
+ while (this.#nextExpiryBucket <= nowMs) {
1698
+ const bucket = this.#expiryBuckets.get(this.#nextExpiryBucket);
1699
+ if (bucket) {
1700
+ for (const cachedNonce of bucket) this.#nonces.delete(cachedNonce);
1701
+ this.#expiryBuckets.delete(this.#nextExpiryBucket);
1702
+ }
1703
+ this.#nextExpiryBucket += STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
1704
+ }
1705
+ }
1706
+ }
1707
+
1708
+ function verifyStatefulForwardingRequest(input: {
1709
+ readonly options: ProviderServerOptions;
1710
+ readonly rawBody: string;
1711
+ readonly headers: Headers;
1712
+ readonly method: string;
1713
+ readonly path: string;
1714
+ readonly replayCache: StatefulForwardingReplayCache;
1715
+ }): void {
1716
+ const config = input.options.statefulForwarding;
1717
+ if (!config?.secret) {
1718
+ throw new ProviderError("Stateful forwarding is not configured.", {
1719
+ code: "STATEFUL_FORWARDING_NOT_CONFIGURED",
1720
+ });
1721
+ }
1722
+ const timestamp = input.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "";
1723
+ const signature = input.headers.get(STATEFUL_FORWARDING_SIGNATURE_HEADER) ?? "";
1724
+ const nonce = input.headers.get(STATEFUL_FORWARDING_NONCE_HEADER) ?? "";
1725
+ if (!timestamp || !signature || !nonce) {
1726
+ throw new ProviderError("Stateful forwarding signature headers are missing.", {
1727
+ code: "STATEFUL_FORWARDING_SIGNATURE_MISSING",
1728
+ });
1729
+ }
1730
+ if (nonce.length > 256) {
1731
+ throw new ProviderError("Stateful forwarding nonce is invalid.", {
1732
+ code: "STATEFUL_FORWARDING_NONCE_INVALID",
1733
+ });
1734
+ }
1735
+ const timestampMs = Date.parse(timestamp);
1736
+ const maxSkewMs = config.maxSkewMs ?? DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS;
1737
+ if (!Number.isFinite(timestampMs) || Math.abs(Date.now() - timestampMs) > maxSkewMs) {
1738
+ throw new ProviderError(
1739
+ "Stateful forwarding signature timestamp is outside the allowed skew.",
1740
+ { code: "STATEFUL_FORWARDING_TIMESTAMP_INVALID" },
1741
+ );
1742
+ }
1743
+ if (
1744
+ !verifyStatefulRequestSignature({
1745
+ secret: config.secret,
1746
+ timestamp,
1747
+ rawBody: input.rawBody,
1748
+ method: input.method,
1749
+ path: input.path,
1750
+ nonce,
1751
+ signature,
1752
+ })
1753
+ ) {
1754
+ throw new ProviderError("Stateful forwarding signature is invalid.", {
1755
+ code: "STATEFUL_FORWARDING_SIGNATURE_INVALID",
1756
+ });
1757
+ }
1758
+ const replayResult = input.replayCache.claim(nonce, timestampMs + maxSkewMs, Date.now());
1759
+ if (replayResult === "replayed") {
1760
+ throw new ProviderError("Stateful forwarding nonce has already been used.", {
1761
+ code: "STATEFUL_FORWARDING_REPLAY_DETECTED",
1762
+ });
1763
+ }
1764
+ if (replayResult === "full") {
1765
+ throw new ProviderError("Stateful forwarding replay cache is at capacity.", {
1766
+ code: "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
1767
+ });
1768
+ }
1769
+ }
1770
+
1771
+ function operationRequestFromForwardingEnvelope(
1772
+ envelope: ProviderServerStatefulForwardEnvelope,
1773
+ ): OperationRequest & { readonly deadlineAt?: string } {
1774
+ return {
1775
+ ...envelope.operationRequest,
1776
+ ...(envelope.deadlineAt !== undefined ? { deadlineAt: envelope.deadlineAt } : {}),
1777
+ };
1778
+ }
1779
+
1780
+ function parseStatefulForwardingEnvelope(rawBody: unknown): ProviderServerStatefulForwardEnvelope {
1781
+ const parsed = ProviderServerStatefulForwardEnvelopeSchema.safeParse(rawBody);
1782
+ if (parsed.success) return parsed.data;
1783
+ throw new ProviderError("Stateful forwarding envelope is invalid.", {
1784
+ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
1785
+ details: zodDetails(parsed.error),
1786
+ });
1787
+ }
1788
+
1366
1789
  export function createServerApp(
1367
1790
  provider: ProviderDefinition,
1368
1791
  options: ProviderServerOptions = {},
1369
1792
  ): Hono {
1793
+ validateStatefulServerConfig(options);
1370
1794
  const app = new Hono();
1371
1795
  const logger = options.logger ?? defaultProviderServerLogger;
1796
+ const operationErrorCodes = buildOperationErrorCodeLookup(provider);
1797
+ const statefulForwardingReplayCache = new StatefulForwardingReplayCache(
1798
+ options.statefulForwarding?.replayCacheMaxEntries ??
1799
+ DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES,
1800
+ );
1372
1801
  const state =
1373
1802
  options.state ??
1374
1803
  createProviderRuntimeStateFromEnv({
@@ -1376,17 +1805,28 @@ export function createServerApp(
1376
1805
  allowMemoryFallback: options.allowMemoryStateFallback === true,
1377
1806
  });
1378
1807
 
1379
- app.notFound((c) =>
1380
- c.json(
1381
- {
1382
- error: {
1383
- code: "not_found",
1384
- message: "Not found",
1385
- },
1386
- },
1387
- 404,
1388
- ),
1808
+ // Boot-time visibility for unprovisioned declared secrets: emit a structured
1809
+ // warn so deploy tooling/alerting sees the gap the moment the pod boots,
1810
+ // instead of discovering it request-by-request. Deliberately log-only — a
1811
+ // boot crash would trade a structured MISSING_SECRET signal for
1812
+ // CrashLoopBackOff. Requests still fail closed via the executeOperation gate.
1813
+ const missingSecretsAtBoot = listMissingRequiredSecrets(
1814
+ provider,
1815
+ createEnvContext(provider.secrets?.map((secret) => secret.name)),
1389
1816
  );
1817
+ if (missingSecretsAtBoot.length > 0) {
1818
+ logger({
1819
+ level: "warn",
1820
+ event: "provider_secrets_missing",
1821
+ providerId: provider.id,
1822
+ missingSecrets: missingSecretsAtBoot,
1823
+ });
1824
+ }
1825
+
1826
+ app.notFound((c) => {
1827
+ const error = new ProviderError("Not found", { code: "not_found", retryable: false });
1828
+ return responseWithErrorObservability(c.json(toErrorResponse(error), 404), error);
1829
+ });
1390
1830
 
1391
1831
  app.get("/health", (c) =>
1392
1832
  c.json({
@@ -1396,6 +1836,169 @@ export function createServerApp(
1396
1836
  }),
1397
1837
  );
1398
1838
 
1839
+ app.post(STATEFUL_INTERNAL_OPERATIONS_ROUTE, async (c) => {
1840
+ let rawBodyText = "";
1841
+ let rawBody: unknown;
1842
+ let operationId: string | undefined;
1843
+ const operation = "stateful-internal";
1844
+ const requestCost = startRequestCost();
1845
+ try {
1846
+ if (!options.internalOperationExecutor) {
1847
+ throw new ProviderError("Stateful internal operation executor is not configured.", {
1848
+ code: "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
1849
+ });
1850
+ }
1851
+ rawBodyText = await c.req.raw.clone().text();
1852
+ verifyStatefulForwardingRequest({
1853
+ options,
1854
+ rawBody: rawBodyText,
1855
+ headers: c.req.raw.headers,
1856
+ method: c.req.raw.method,
1857
+ path: STATEFUL_INTERNAL_OPERATIONS_ROUTE,
1858
+ replayCache: statefulForwardingReplayCache,
1859
+ });
1860
+ try {
1861
+ rawBody = JSON.parse(rawBodyText);
1862
+ } catch {
1863
+ throw new ProviderError("Stateful forwarding envelope is not valid JSON.", {
1864
+ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
1865
+ });
1866
+ }
1867
+ const envelope = parseStatefulForwardingEnvelope(rawBody);
1868
+ if (envelope.providerId !== provider.id) {
1869
+ throw new ProviderError(
1870
+ "Stateful forwarding envelope providerId does not match the served provider.",
1871
+ { code: "STATEFUL_FORWARDING_PROVIDER_MISMATCH" },
1872
+ );
1873
+ }
1874
+ if (envelope.requestId !== envelope.operationRequest.requestId) {
1875
+ throw new ProviderError("Stateful forwarding requestId values do not match.", {
1876
+ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
1877
+ });
1878
+ }
1879
+ if (
1880
+ envelope.sourcePodId !==
1881
+ (c.req.raw.headers.get(STATEFUL_FORWARDING_SOURCE_POD_HEADER) ?? "")
1882
+ ) {
1883
+ throw new ProviderError("Stateful forwarding source pod does not match its header.", {
1884
+ code: "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
1885
+ });
1886
+ }
1887
+ if (
1888
+ envelope.forwardedAt !== (c.req.raw.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "")
1889
+ ) {
1890
+ throw new ProviderError(
1891
+ "Stateful forwarding forwardedAt does not match its signature timestamp.",
1892
+ { code: "STATEFUL_FORWARDING_ENVELOPE_INVALID" },
1893
+ );
1894
+ }
1895
+ const deadlineAtMs = envelope.deadlineAt ? Date.parse(envelope.deadlineAt) : undefined;
1896
+ if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
1897
+ throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
1898
+ }
1899
+ const remainingDeadlineMs =
1900
+ deadlineAtMs === undefined ? undefined : deadlineAtMs - Date.now();
1901
+ const deadlineSignal =
1902
+ remainingDeadlineMs === undefined ? undefined : AbortSignal.timeout(remainingDeadlineMs);
1903
+ const signal = deadlineSignal
1904
+ ? AbortSignal.any([c.req.raw.signal, deadlineSignal])
1905
+ : c.req.raw.signal;
1906
+ const ownerFenceValidation = Promise.resolve(
1907
+ options.statefulForwarding?.validateOwnerFence(
1908
+ {
1909
+ providerId: envelope.providerId,
1910
+ sessionKey: envelope.sessionKey,
1911
+ ownerPodId: envelope.ownerPodId,
1912
+ generation: envelope.generation,
1913
+ sourcePodId: envelope.sourcePodId,
1914
+ forwardedAt: envelope.forwardedAt,
1915
+ requestId: envelope.requestId,
1916
+ ...(envelope.idempotencyKey ? { idempotencyKey: envelope.idempotencyKey } : {}),
1917
+ },
1918
+ signal,
1919
+ ),
1920
+ );
1921
+ let ownerFenceValid: boolean | undefined;
1922
+ try {
1923
+ ownerFenceValid = deadlineSignal
1924
+ ? await Promise.race([
1925
+ ownerFenceValidation,
1926
+ new Promise<never>((_resolve, reject) => {
1927
+ deadlineSignal.addEventListener(
1928
+ "abort",
1929
+ () =>
1930
+ reject(
1931
+ new StatefulRoutingDeadlineError(
1932
+ envelope.requestId,
1933
+ envelope.deadlineAt as string,
1934
+ ),
1935
+ ),
1936
+ { once: true },
1937
+ );
1938
+ }),
1939
+ ])
1940
+ : await ownerFenceValidation;
1941
+ } catch (error) {
1942
+ if (deadlineSignal?.aborted) {
1943
+ throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
1944
+ }
1945
+ throw error;
1946
+ }
1947
+ if (ownerFenceValid !== true) {
1948
+ throw new ProviderError("Stateful forwarding owner fence is no longer current.", {
1949
+ code: "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
1950
+ });
1951
+ }
1952
+ const request = operationRequestFromForwardingEnvelope(envelope);
1953
+ operationId = envelope.operationId;
1954
+ const ctx = createProviderContext(provider, request, operationId, options, state);
1955
+ if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
1956
+ throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
1957
+ }
1958
+ const output = await options.internalOperationExecutor({
1959
+ provider,
1960
+ operationId,
1961
+ ctx,
1962
+ request,
1963
+ internalStatefulForward: envelope,
1964
+ signal,
1965
+ });
1966
+ logProviderSuccess(
1967
+ logger,
1968
+ provider,
1969
+ "operation",
1970
+ operationId || operation,
1971
+ request.requestId,
1972
+ 200,
1973
+ finishRequestCost(requestCost),
1974
+ );
1975
+ return c.json({ data: output });
1976
+ } catch (error) {
1977
+ const declaredErrorCode = declaredErrorCodeFor(error, operationId, operationErrorCodes);
1978
+ const status = toStatusCode(error, declaredErrorCode);
1979
+ if (isProviderError(error) && error.code === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
1980
+ c.header("Retry-After", String(STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS));
1981
+ }
1982
+ const requestId = extractRequestId(rawBody);
1983
+ logProviderError(
1984
+ logger,
1985
+ provider,
1986
+ "operation",
1987
+ operationId || operation,
1988
+ requestId,
1989
+ error,
1990
+ status,
1991
+ finishRequestCost(requestCost),
1992
+ declaredErrorCode,
1993
+ );
1994
+ return responseWithErrorObservability(
1995
+ c.json(toErrorResponse(error, requestId, declaredErrorCode), status),
1996
+ error,
1997
+ declaredErrorCode,
1998
+ );
1999
+ }
2000
+ });
2001
+
1399
2002
  app.post("/v1/:operation", async (c) => {
1400
2003
  let rawBody: unknown;
1401
2004
  const operation = c.req.param("operation");
@@ -1442,7 +2045,8 @@ export function createServerApp(
1442
2045
  );
1443
2046
  return c.json(response);
1444
2047
  } catch (error) {
1445
- const status = toStatusCode(error);
2048
+ const declaredErrorCode = declaredErrorCodeFor(error, operation, operationErrorCodes);
2049
+ const status = toStatusCode(error, declaredErrorCode);
1446
2050
  const requestId = extractRequestId(rawBody);
1447
2051
  logProviderError(
1448
2052
  logger,
@@ -1453,10 +2057,15 @@ export function createServerApp(
1453
2057
  error,
1454
2058
  status,
1455
2059
  finishRequestCost(requestCost),
2060
+ declaredErrorCode,
1456
2061
  );
1457
2062
  const telemetryHeader = proxyTelemetry.toHeaderValue();
1458
2063
  if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1459
- return c.json(toErrorResponse(error, requestId), status);
2064
+ return responseWithErrorObservability(
2065
+ c.json(toErrorResponse(error, requestId, declaredErrorCode), status),
2066
+ error,
2067
+ declaredErrorCode,
2068
+ );
1460
2069
  }
1461
2070
  });
1462
2071
 
@@ -1468,17 +2077,8 @@ export function createServerApp(
1468
2077
  .clone()
1469
2078
  .json()
1470
2079
  .catch(() => undefined);
1471
- const body = withAuthRequestHeaders(
1472
- AuthFlowRequestSchema.parse(rawBody),
1473
- c.req.raw.headers,
1474
- );
1475
- const response = await handleAuthFlow(
1476
- provider,
1477
- body,
1478
- "start",
1479
- options,
1480
- c.req.raw.signal,
1481
- );
2080
+ const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
2081
+ const response = await handleAuthFlow(provider, body, "start", options, c.req.raw.signal);
1482
2082
  logProviderSuccess(
1483
2083
  logger,
1484
2084
  provider,
@@ -1502,7 +2102,10 @@ export function createServerApp(
1502
2102
  status,
1503
2103
  finishRequestCost(requestCost),
1504
2104
  );
1505
- return c.json(toErrorResponse(error, requestId), status);
2105
+ return responseWithErrorObservability(
2106
+ c.json(toErrorResponse(error, requestId), status),
2107
+ error,
2108
+ );
1506
2109
  }
1507
2110
  });
1508
2111
 
@@ -1514,17 +2117,8 @@ export function createServerApp(
1514
2117
  .clone()
1515
2118
  .json()
1516
2119
  .catch(() => undefined);
1517
- const body = withAuthRequestHeaders(
1518
- AuthFlowRequestSchema.parse(rawBody),
1519
- c.req.raw.headers,
1520
- );
1521
- const response = await handleAuthFlow(
1522
- provider,
1523
- body,
1524
- "continue",
1525
- options,
1526
- c.req.raw.signal,
1527
- );
2120
+ const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
2121
+ const response = await handleAuthFlow(provider, body, "continue", options, c.req.raw.signal);
1528
2122
  logProviderSuccess(
1529
2123
  logger,
1530
2124
  provider,
@@ -1548,7 +2142,10 @@ export function createServerApp(
1548
2142
  status,
1549
2143
  finishRequestCost(requestCost),
1550
2144
  );
1551
- return c.json(toErrorResponse(error, requestId), status);
2145
+ return responseWithErrorObservability(
2146
+ c.json(toErrorResponse(error, requestId), status),
2147
+ error,
2148
+ );
1552
2149
  }
1553
2150
  });
1554
2151
 
@@ -1560,17 +2157,8 @@ export function createServerApp(
1560
2157
  .clone()
1561
2158
  .json()
1562
2159
  .catch(() => undefined);
1563
- const body = withAuthRequestHeaders(
1564
- AuthFlowRequestSchema.parse(rawBody),
1565
- c.req.raw.headers,
1566
- );
1567
- const response = await handleAuthFlow(
1568
- provider,
1569
- body,
1570
- "poll",
1571
- options,
1572
- c.req.raw.signal,
1573
- );
2160
+ const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
2161
+ const response = await handleAuthFlow(provider, body, "poll", options, c.req.raw.signal);
1574
2162
  logProviderSuccess(
1575
2163
  logger,
1576
2164
  provider,
@@ -1594,7 +2182,10 @@ export function createServerApp(
1594
2182
  status,
1595
2183
  finishRequestCost(requestCost),
1596
2184
  );
1597
- return c.json(toErrorResponse(error, requestId), status);
2185
+ return responseWithErrorObservability(
2186
+ c.json(toErrorResponse(error, requestId), status),
2187
+ error,
2188
+ );
1598
2189
  }
1599
2190
  });
1600
2191
 
@@ -1606,17 +2197,8 @@ export function createServerApp(
1606
2197
  .clone()
1607
2198
  .json()
1608
2199
  .catch(() => undefined);
1609
- const body = withAuthRequestHeaders(
1610
- AuthFlowRequestSchema.parse(rawBody),
1611
- c.req.raw.headers,
1612
- );
1613
- const response = await handleAuthFlow(
1614
- provider,
1615
- body,
1616
- "refresh",
1617
- options,
1618
- c.req.raw.signal,
1619
- );
2200
+ const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
2201
+ const response = await handleAuthFlow(provider, body, "refresh", options, c.req.raw.signal);
1620
2202
  logProviderSuccess(
1621
2203
  logger,
1622
2204
  provider,
@@ -1640,7 +2222,10 @@ export function createServerApp(
1640
2222
  status,
1641
2223
  finishRequestCost(requestCost),
1642
2224
  );
1643
- return c.json(toErrorResponse(error, requestId), status);
2225
+ return responseWithErrorObservability(
2226
+ c.json(toErrorResponse(error, requestId), status),
2227
+ error,
2228
+ );
1644
2229
  }
1645
2230
  });
1646
2231
 
@@ -1652,17 +2237,8 @@ export function createServerApp(
1652
2237
  .clone()
1653
2238
  .json()
1654
2239
  .catch(() => undefined);
1655
- const body = withAuthRequestHeaders(
1656
- AuthFlowRequestSchema.parse(rawBody),
1657
- c.req.raw.headers,
1658
- );
1659
- const response = await handleAuthFlow(
1660
- provider,
1661
- body,
1662
- "abort",
1663
- options,
1664
- c.req.raw.signal,
1665
- );
2240
+ const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
2241
+ const response = await handleAuthFlow(provider, body, "abort", options, c.req.raw.signal);
1666
2242
  logProviderSuccess(
1667
2243
  logger,
1668
2244
  provider,
@@ -1686,19 +2262,64 @@ export function createServerApp(
1686
2262
  status,
1687
2263
  finishRequestCost(requestCost),
1688
2264
  );
1689
- return c.json(toErrorResponse(error, requestId), status);
2265
+ return responseWithErrorObservability(
2266
+ c.json(toErrorResponse(error, requestId), status),
2267
+ error,
2268
+ );
1690
2269
  }
1691
2270
  });
1692
2271
 
1693
2272
  return app;
1694
2273
  }
1695
2274
 
2275
+ function validateStatefulServerConfig(options: ProviderServerOptions): void {
2276
+ if (options.statefulForwarding && !options.internalOperationExecutor) {
2277
+ throw new Error(
2278
+ "Invalid provider server configuration: statefulForwarding requires internalOperationExecutor; missing option internalOperationExecutor.",
2279
+ );
2280
+ }
2281
+ if (options.internalOperationExecutor && !options.statefulForwarding?.secret) {
2282
+ throw new Error(
2283
+ "Invalid provider server configuration: internalOperationExecutor requires statefulForwarding.secret; missing option statefulForwarding.secret.",
2284
+ );
2285
+ }
2286
+ if (
2287
+ options.statefulForwarding &&
2288
+ typeof options.statefulForwarding.validateOwnerFence !== "function"
2289
+ ) {
2290
+ throw new Error(
2291
+ "Invalid provider server configuration: statefulForwarding requires validateOwnerFence.",
2292
+ );
2293
+ }
2294
+ if (
2295
+ options.statefulForwarding?.maxSkewMs !== undefined &&
2296
+ (!Number.isFinite(options.statefulForwarding.maxSkewMs) ||
2297
+ options.statefulForwarding.maxSkewMs <= 0)
2298
+ ) {
2299
+ throw new Error("Invalid provider server configuration: maxSkewMs must be positive.");
2300
+ }
2301
+ if (
2302
+ options.statefulForwarding?.replayCacheMaxEntries !== undefined &&
2303
+ (!Number.isInteger(options.statefulForwarding.replayCacheMaxEntries) ||
2304
+ options.statefulForwarding.replayCacheMaxEntries <= 0)
2305
+ ) {
2306
+ throw new Error(
2307
+ "Invalid provider server configuration: replayCacheMaxEntries must be a positive integer.",
2308
+ );
2309
+ }
2310
+ }
2311
+
1696
2312
  type BunServeRuntime = {
1697
2313
  serve: (options: {
1698
2314
  port: number;
1699
2315
  hostname: string;
1700
2316
  fetch: (request: Request) => Response | Promise<Response>;
1701
- }) => unknown;
2317
+ }) => BunServerHandle;
2318
+ };
2319
+
2320
+ type BunServerHandle = {
2321
+ readonly port: number;
2322
+ stop(closeActiveConnections?: boolean): Promise<void>;
1702
2323
  };
1703
2324
 
1704
2325
  function getBunServeRuntime(): BunServeRuntime | undefined {
@@ -1714,11 +2335,20 @@ function getBunServeRuntime(): BunServeRuntime | undefined {
1714
2335
 
1715
2336
  return {
1716
2337
  serve(options) {
1717
- return serve(options);
2338
+ return serve(options) as BunServerHandle;
1718
2339
  },
1719
2340
  };
1720
2341
  }
1721
2342
 
2343
+ export type ProviderServerCloseOptions = {
2344
+ readonly timeoutMs?: number;
2345
+ };
2346
+
2347
+ export type ProviderServerHandle = {
2348
+ readonly port: number;
2349
+ close(options?: ProviderServerCloseOptions): Promise<void>;
2350
+ };
2351
+
1722
2352
  export interface ServeOptions extends ProviderServerOptions {
1723
2353
  host?: string;
1724
2354
  port?: number;
@@ -1730,46 +2360,233 @@ export interface ServeOptions extends ProviderServerOptions {
1730
2360
  selfTestPort?: number;
1731
2361
  }
1732
2362
 
2363
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
2364
+ const DEFAULT_SHUTDOWN_SIGNALS: NodeJS.Signals[] = ["SIGTERM", "SIGINT"];
2365
+
2366
+ type SignalServerRegistration = {
2367
+ readonly signals: ReadonlySet<NodeJS.Signals>;
2368
+ readonly close: () => Promise<void>;
2369
+ };
2370
+
2371
+ type ProcessSignalCoordinator = {
2372
+ readonly registrations: Set<SignalServerRegistration>;
2373
+ readonly listener: () => void;
2374
+ handling: boolean;
2375
+ };
2376
+
2377
+ const processSignalCoordinators = new Map<NodeJS.Signals, ProcessSignalCoordinator>();
2378
+
1733
2379
  export async function serve(
1734
2380
  provider: ProviderDefinition,
1735
2381
  options: ServeOptions = {},
1736
- ): Promise<void> {
2382
+ ): Promise<ProviderServerHandle> {
1737
2383
  const bunRuntime = getBunServeRuntime();
1738
2384
 
1739
2385
  if (bunRuntime === undefined) {
1740
- throw new ProviderError(
1741
- "Bun runtime is required to start the provider server",
1742
- {
1743
- code: "RUNTIME_UNSUPPORTED",
1744
- },
1745
- );
2386
+ throw new ProviderError("Bun runtime is required to start the provider server", {
2387
+ code: "RUNTIME_UNSUPPORTED",
2388
+ });
1746
2389
  }
2390
+ const logger = options.logger ?? defaultProviderServerLogger;
2391
+ const configuredTimeoutMs = shutdownTimeout(
2392
+ options.shutdown?.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS,
2393
+ );
2394
+ const configuredSignals = resolveShutdownSignals(options.shutdown?.signals ?? true);
1747
2395
 
1748
2396
  const app = createServerApp(provider, {
1749
2397
  logger: options.logger,
1750
2398
  stt: options.stt,
2399
+ state: options.state,
2400
+ allowMemoryStateFallback: options.allowMemoryStateFallback,
2401
+ operationExecutor: options.operationExecutor,
2402
+ internalOperationExecutor: options.internalOperationExecutor,
2403
+ statefulForwarding: options.statefulForwarding,
2404
+ });
2405
+
2406
+ const servers: BunServerHandle[] = [];
2407
+ try {
2408
+ servers.push(
2409
+ bunRuntime.serve({
2410
+ port: options.port ?? DEFAULT_PORT,
2411
+ hostname: options.host ?? DEFAULT_HOST,
2412
+ fetch: app.fetch,
2413
+ }),
2414
+ );
2415
+
2416
+ // Internal self-test listener (health dependency inversion): a SEPARATE
2417
+ // socket the tenant-facing gateway never dials. Off by default — it only
2418
+ // starts when the shared self-test master secret env is present.
2419
+ const selfTestSecrets = resolveSelfTestMasterSecrets();
2420
+ if (selfTestSecrets) {
2421
+ const selfTestApp = createSelfTestApp(provider, {
2422
+ secrets: selfTestSecrets,
2423
+ invoke: createSelfTestInvoke(app),
2424
+ authFlow: createSelfTestAuthFlowInvoke(app),
2425
+ });
2426
+ servers.push(
2427
+ bunRuntime.serve({
2428
+ port: options.selfTestPort ?? resolveSelfTestPort(),
2429
+ hostname: options.host ?? DEFAULT_HOST,
2430
+ fetch: selfTestApp.fetch,
2431
+ }),
2432
+ );
2433
+ }
2434
+ } catch (error) {
2435
+ await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
2436
+ throw error;
2437
+ }
2438
+
2439
+ const server = servers[0];
2440
+ if (!server) throw new Error("Provider server failed to create its primary listener.");
2441
+ let closePromise: Promise<void> | undefined;
2442
+ let unregisterSignals = () => {};
2443
+
2444
+ const close = (closeOptions: ProviderServerCloseOptions = {}): Promise<void> => {
2445
+ if (closePromise) return closePromise;
2446
+ const timeoutMs = shutdownTimeout(closeOptions.timeoutMs ?? configuredTimeoutMs);
2447
+ closePromise = closeProviderServers({
2448
+ servers,
2449
+ hooks: options.shutdown?.hooks ?? [],
2450
+ timeoutMs,
2451
+ logger,
2452
+ providerId: provider.id,
2453
+ }).finally(() => unregisterSignals());
2454
+ return closePromise;
2455
+ };
2456
+
2457
+ try {
2458
+ unregisterSignals = registerForProcessSignals(configuredSignals, () =>
2459
+ close({ timeoutMs: configuredTimeoutMs }),
2460
+ );
2461
+ } catch (error) {
2462
+ unregisterSignals();
2463
+ await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
2464
+ throw error;
2465
+ }
2466
+
2467
+ return { port: server.port, close };
2468
+ }
2469
+
2470
+ function registerForProcessSignals(
2471
+ signals: NodeJS.Signals[],
2472
+ close: () => Promise<void>,
2473
+ ): () => void {
2474
+ if (signals.length === 0) return () => {};
2475
+ const registration: SignalServerRegistration = {
2476
+ signals: new Set(signals),
2477
+ close,
2478
+ };
2479
+ let registered = true;
2480
+ const unregister = () => {
2481
+ if (!registered) return;
2482
+ registered = false;
2483
+ for (const signal of registration.signals) {
2484
+ const coordinator = processSignalCoordinators.get(signal);
2485
+ if (!coordinator) continue;
2486
+ coordinator.registrations.delete(registration);
2487
+ if (coordinator.registrations.size === 0 && !coordinator.handling) {
2488
+ process.removeListener(signal, coordinator.listener);
2489
+ processSignalCoordinators.delete(signal);
2490
+ }
2491
+ }
2492
+ };
2493
+ try {
2494
+ for (const signal of signals) {
2495
+ let coordinator = processSignalCoordinators.get(signal);
2496
+ if (!coordinator) {
2497
+ const created: ProcessSignalCoordinator = {
2498
+ registrations: new Set(),
2499
+ handling: false,
2500
+ listener: () => handleCoordinatedSignal(signal, created),
2501
+ };
2502
+ coordinator = created;
2503
+ processSignalCoordinators.set(signal, coordinator);
2504
+ process.on(signal, coordinator.listener);
2505
+ }
2506
+ coordinator.registrations.add(registration);
2507
+ }
2508
+ } catch (error) {
2509
+ unregister();
2510
+ throw error;
2511
+ }
2512
+ return unregister;
2513
+ }
2514
+
2515
+ function handleCoordinatedSignal(
2516
+ signal: NodeJS.Signals,
2517
+ coordinator: ProcessSignalCoordinator,
2518
+ ): void {
2519
+ if (coordinator.handling) return;
2520
+ coordinator.handling = true;
2521
+ const registrations = [...coordinator.registrations];
2522
+ void Promise.allSettled(registrations.map((registration) => registration.close())).finally(() => {
2523
+ if (processSignalCoordinators.get(signal) === coordinator) {
2524
+ process.removeListener(signal, coordinator.listener);
2525
+ processSignalCoordinators.delete(signal);
2526
+ }
2527
+ try {
2528
+ process.kill(process.pid, signal);
2529
+ } catch {
2530
+ process.exitCode = 1;
2531
+ }
1751
2532
  });
2533
+ }
2534
+
2535
+ async function closeProviderServers(input: {
2536
+ readonly servers: BunServerHandle[];
2537
+ readonly hooks: Array<() => Promise<void>>;
2538
+ readonly timeoutMs: number;
2539
+ readonly logger: ProviderServerLogger;
2540
+ readonly providerId: string;
2541
+ }): Promise<void> {
2542
+ const deadline = Date.now() + input.timeoutMs;
2543
+ const gracefulStops = input.servers.map((server) => server.stop(false));
2544
+ for (const gracefulStop of gracefulStops) gracefulStop.catch(() => undefined);
2545
+ for (const [hookIndex, hook] of input.hooks.entries()) {
2546
+ try {
2547
+ await withinShutdownBudget(Promise.resolve().then(hook), deadline);
2548
+ } catch (error) {
2549
+ try {
2550
+ input.logger({
2551
+ level: "error",
2552
+ event: "provider_shutdown_hook_failed",
2553
+ providerId: input.providerId,
2554
+ hookIndex,
2555
+ errorClass: error instanceof Error ? error.name : "UnknownError",
2556
+ message: error instanceof Error ? error.message : "Shutdown hook failed.",
2557
+ });
2558
+ } catch {}
2559
+ }
2560
+ }
2561
+ const forcedStops = input.servers.map((server) => server.stop(true));
2562
+ await withinShutdownBudget(
2563
+ Promise.allSettled([...gracefulStops, ...forcedStops]).then(() => undefined),
2564
+ deadline,
2565
+ ).catch(() => undefined);
2566
+ }
1752
2567
 
1753
- bunRuntime.serve({
1754
- port: options.port ?? DEFAULT_PORT,
1755
- hostname: options.host ?? DEFAULT_HOST,
1756
- fetch: app.fetch,
2568
+ async function withinShutdownBudget<T>(promise: Promise<T>, deadline: number): Promise<T> {
2569
+ promise.catch(() => undefined);
2570
+ const remainingMs = Math.max(0, deadline - Date.now());
2571
+ let timer: ReturnType<typeof setTimeout> | undefined;
2572
+ const timeout = new Promise<never>((_resolve, reject) => {
2573
+ timer = setTimeout(() => reject(new Error("Provider server shutdown timed out.")), remainingMs);
1757
2574
  });
2575
+ try {
2576
+ return await Promise.race([promise, timeout]);
2577
+ } finally {
2578
+ if (timer) clearTimeout(timer);
2579
+ }
2580
+ }
1758
2581
 
1759
- // Internal self-test listener (health dependency inversion): a SEPARATE
1760
- // socket the tenant-facing gateway never dials. Off by default — it only
1761
- // starts when the shared self-test master secret env is present.
1762
- const selfTestSecrets = resolveSelfTestMasterSecrets();
1763
- if (selfTestSecrets) {
1764
- const selfTestApp = createSelfTestApp(provider, {
1765
- secrets: selfTestSecrets,
1766
- invoke: createSelfTestInvoke(app),
1767
- });
1768
- bunRuntime.serve({
1769
- port: options.selfTestPort ?? resolveSelfTestPort(),
1770
- hostname: options.host ?? DEFAULT_HOST,
1771
- fetch: selfTestApp.fetch,
1772
- });
2582
+ function resolveShutdownSignals(signals: boolean | NodeJS.Signals[]): NodeJS.Signals[] {
2583
+ if (signals === false) return [];
2584
+ return [...new Set(signals === true ? DEFAULT_SHUTDOWN_SIGNALS : signals)];
2585
+ }
2586
+
2587
+ function shutdownTimeout(value: number): number {
2588
+ if (!Number.isFinite(value) || value < 0) {
2589
+ throw new Error("Provider server shutdown timeoutMs must be a non-negative finite number.");
1773
2590
  }
1774
- await Promise.resolve();
2591
+ return value;
1775
2592
  }