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

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