@apifuse/provider-sdk 2.2.0-beta.1 → 2.2.0-beta.11

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 (261) hide show
  1. package/AUTHORING.md +106 -0
  2. package/CHANGELOG.md +41 -0
  3. package/README.md +5 -1
  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 +22 -81
  10. package/bin/apifuse-pack-types.ts +266 -0
  11. package/bin/apifuse-perf.ts +45 -127
  12. package/bin/apifuse-record.ts +53 -70
  13. package/bin/apifuse-submit-check.ts +510 -45
  14. package/bin/apifuse-sync-assets.ts +117 -0
  15. package/bin/apifuse.ts +1 -1
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/bin/submit-check-xml-semantics.ts +204 -0
  18. package/bin/submit-check-xml.ts +134 -0
  19. package/dist/auth-turn/index.d.ts +3 -3
  20. package/dist/auth.d.ts +2 -2
  21. package/dist/auth.js +9 -18
  22. package/dist/ceremonies/index.d.ts +1 -1
  23. package/dist/ceremonies/index.js +8 -18
  24. package/dist/cli/commands.d.ts +1 -1
  25. package/dist/cli/commands.js +8 -0
  26. package/dist/cli/create.d.ts +3 -0
  27. package/dist/cli/create.js +34 -35
  28. package/dist/cli/prompt-assets.d.ts +80 -0
  29. package/dist/cli/prompt-assets.js +743 -0
  30. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  31. package/dist/config/loader.d.ts +149 -8
  32. package/dist/config/loader.js +378 -89
  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 +8 -1
  39. package/dist/define.js +119 -122
  40. package/dist/dev.d.ts +1 -1
  41. package/dist/dev.js +1 -1
  42. package/dist/errors.d.ts +4 -1
  43. package/dist/errors.js +48 -0
  44. package/dist/i18n/catalog.d.ts +2 -2
  45. package/dist/i18n/catalog.js +4 -10
  46. package/dist/i18n/index.d.ts +2 -2
  47. package/dist/i18n/index.js +2 -2
  48. package/dist/i18n/keys.d.ts +2 -2
  49. package/dist/index.d.ts +44 -42
  50. package/dist/index.js +39 -37
  51. package/dist/lint.js +8 -15
  52. package/dist/provider.d.ts +11 -11
  53. package/dist/provider.js +9 -9
  54. package/dist/public-schema-field-lint.d.ts +1 -1
  55. package/dist/recipes/gov-api.js +1 -1
  56. package/dist/runtime/auth-flow.d.ts +1 -1
  57. package/dist/runtime/auth-flow.js +3 -3
  58. package/dist/runtime/browser.d.ts +1 -1
  59. package/dist/runtime/browser.js +15 -29
  60. package/dist/runtime/cache.d.ts +1 -1
  61. package/dist/runtime/cache.js +4 -8
  62. package/dist/runtime/choice.d.ts +1 -1
  63. package/dist/runtime/choice.js +31 -35
  64. package/dist/runtime/credential.d.ts +1 -1
  65. package/dist/runtime/credential.js +1 -1
  66. package/dist/runtime/env.d.ts +1 -1
  67. package/dist/runtime/executor.d.ts +1 -1
  68. package/dist/runtime/executor.js +15 -3
  69. package/dist/runtime/http.d.ts +2 -2
  70. package/dist/runtime/http.js +144 -348
  71. package/dist/runtime/insights.d.ts +1 -1
  72. package/dist/runtime/insights.js +6 -13
  73. package/dist/runtime/instrumentation.d.ts +2 -2
  74. package/dist/runtime/instrumentation.js +56 -19
  75. package/dist/runtime/keyring.js +1 -1
  76. package/dist/runtime/namespace.js +1 -1
  77. package/dist/runtime/otlp.d.ts +1 -1
  78. package/dist/runtime/perf.d.ts +1 -1
  79. package/dist/runtime/provider.d.ts +1 -1
  80. package/dist/runtime/provider.js +1 -2
  81. package/dist/runtime/proxy-errors.d.ts +1 -1
  82. package/dist/runtime/proxy-errors.js +9 -7
  83. package/dist/runtime/proxy-nodemaven.d.ts +35 -0
  84. package/dist/runtime/proxy-nodemaven.js +128 -0
  85. package/dist/runtime/proxy-retry-policy.d.ts +40 -0
  86. package/dist/runtime/proxy-retry-policy.js +326 -0
  87. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  88. package/dist/runtime/proxy-telemetry.js +55 -52
  89. package/dist/runtime/redis.d.ts +1 -1
  90. package/dist/runtime/redis.js +2 -4
  91. package/dist/runtime/request-options.d.ts +1 -1
  92. package/dist/runtime/secrets.d.ts +27 -0
  93. package/dist/runtime/secrets.js +51 -0
  94. package/dist/runtime/state.d.ts +2 -2
  95. package/dist/runtime/state.js +15 -4
  96. package/dist/runtime/stealth.d.ts +2 -2
  97. package/dist/runtime/stealth.js +108 -212
  98. package/dist/runtime/stt.d.ts +1 -1
  99. package/dist/runtime/stt.js +11 -15
  100. package/dist/runtime/trace.d.ts +2 -2
  101. package/dist/runtime/trace.js +2 -4
  102. package/dist/runtime/waterfall.d.ts +1 -1
  103. package/dist/schema.d.ts +1 -1
  104. package/dist/schema.js +7 -15
  105. package/dist/serve.d.ts +1 -1
  106. package/dist/serve.js +1 -1
  107. package/dist/server/index.d.ts +7 -7
  108. package/dist/server/index.js +6 -6
  109. package/dist/server/self-test-redaction.d.ts +1 -1
  110. package/dist/server/self-test-redaction.js +1 -1
  111. package/dist/server/self-test.d.ts +104 -3
  112. package/dist/server/self-test.js +673 -115
  113. package/dist/server/serve.d.ts +105 -4
  114. package/dist/server/serve.js +588 -80
  115. package/dist/server/types.d.ts +10 -9
  116. package/dist/server/types.js +3 -7
  117. package/dist/stateful/errors.d.ts +14 -0
  118. package/dist/stateful/errors.js +14 -0
  119. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  120. package/dist/stateful/http-provider-event-emitter.js +237 -0
  121. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  122. package/dist/stateful/http-session-owner-registry.js +210 -0
  123. package/dist/stateful/index.d.ts +18 -0
  124. package/dist/stateful/index.js +18 -0
  125. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  126. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  127. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  128. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  129. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  130. package/dist/stateful/provider-event-pipeline.js +1 -0
  131. package/dist/stateful/provider-events.d.ts +101 -0
  132. package/dist/stateful/provider-events.js +289 -0
  133. package/dist/stateful/session-key.d.ts +15 -0
  134. package/dist/stateful/session-key.js +86 -0
  135. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  136. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  137. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  138. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  139. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  140. package/dist/stateful/stateful-provider-adapter.js +287 -0
  141. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  142. package/dist/stateful/stateful-provider-observability.js +161 -0
  143. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  144. package/dist/stateful/stateful-provider-owner-forwarder.js +207 -0
  145. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  146. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  147. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  148. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  149. package/dist/stateful/stateful-provider-session-routing.d.ts +71 -0
  150. package/dist/stateful/stateful-provider-session-routing.js +353 -0
  151. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  152. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  153. package/dist/stateful-signing.d.ts +18 -0
  154. package/dist/stateful-signing.js +27 -0
  155. package/dist/stealth/profiles.d.ts +1 -1
  156. package/dist/stealth/profiles.js +5 -14
  157. package/dist/stream.d.ts +1 -1
  158. package/dist/testing/index.d.ts +2 -2
  159. package/dist/testing/index.js +2 -2
  160. package/dist/testing/run.d.ts +1 -1
  161. package/dist/testing/run.js +7 -14
  162. package/dist/types.d.ts +87 -1
  163. package/dist/user-input.d.ts +30 -0
  164. package/dist/user-input.js +66 -0
  165. package/package.json +14 -5
  166. package/src/auth-turn/index.ts +1 -1
  167. package/src/auth.ts +28 -86
  168. package/src/ceremonies/index.ts +24 -78
  169. package/src/cli/commands.ts +10 -0
  170. package/src/cli/create.ts +42 -35
  171. package/src/cli/prompt-assets.ts +865 -0
  172. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  173. package/src/config/loader.ts +543 -208
  174. package/src/contract-serialization.ts +5 -11
  175. package/src/contract-types.ts +2 -2
  176. package/src/contract.ts +12 -28
  177. package/src/define.ts +247 -505
  178. package/src/dev.ts +4 -9
  179. package/src/errors.ts +58 -5
  180. package/src/i18n/catalog.ts +10 -32
  181. package/src/i18n/index.ts +2 -2
  182. package/src/i18n/keys.ts +5 -11
  183. package/src/index.ts +49 -42
  184. package/src/lint.ts +89 -159
  185. package/src/provider.ts +15 -11
  186. package/src/public-schema-field-lint.ts +7 -33
  187. package/src/recipes/gov-api.ts +2 -5
  188. package/src/runtime/auth-flow.ts +5 -7
  189. package/src/runtime/browser.ts +64 -187
  190. package/src/runtime/cache.ts +20 -67
  191. package/src/runtime/choice.ts +79 -132
  192. package/src/runtime/credential.ts +2 -2
  193. package/src/runtime/env.ts +1 -1
  194. package/src/runtime/executor.ts +23 -20
  195. package/src/runtime/http.ts +195 -559
  196. package/src/runtime/insights.ts +15 -53
  197. package/src/runtime/instrumentation.ts +78 -65
  198. package/src/runtime/keyring.ts +7 -19
  199. package/src/runtime/namespace.ts +2 -7
  200. package/src/runtime/otlp.ts +12 -23
  201. package/src/runtime/perf.ts +1 -1
  202. package/src/runtime/provider.ts +4 -9
  203. package/src/runtime/proxy-errors.ts +29 -42
  204. package/src/runtime/proxy-nodemaven.ts +178 -0
  205. package/src/runtime/proxy-retry-policy.ts +469 -0
  206. package/src/runtime/proxy-telemetry.ts +79 -77
  207. package/src/runtime/redis.ts +4 -12
  208. package/src/runtime/request-options.ts +4 -13
  209. package/src/runtime/secrets.ts +64 -0
  210. package/src/runtime/state.ts +41 -110
  211. package/src/runtime/stealth.ts +162 -377
  212. package/src/runtime/stt.ts +38 -94
  213. package/src/runtime/trace.ts +14 -44
  214. package/src/runtime/waterfall.ts +5 -18
  215. package/src/schema.ts +23 -84
  216. package/src/serve.ts +1 -1
  217. package/src/server/index.ts +24 -7
  218. package/src/server/self-test-redaction.ts +2 -2
  219. package/src/server/self-test.ts +857 -132
  220. package/src/server/serve.ts +859 -266
  221. package/src/server/types.ts +7 -19
  222. package/src/stateful/README.md +146 -0
  223. package/src/stateful/errors.ts +23 -0
  224. package/src/stateful/http-provider-event-emitter.ts +314 -0
  225. package/src/stateful/http-session-owner-registry.ts +306 -0
  226. package/src/stateful/index.ts +18 -0
  227. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  228. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  229. package/src/stateful/provider-event-pipeline.ts +61 -0
  230. package/src/stateful/provider-events.ts +462 -0
  231. package/src/stateful/session-key.ts +111 -0
  232. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  233. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  234. package/src/stateful/stateful-provider-adapter.ts +562 -0
  235. package/src/stateful/stateful-provider-observability.ts +261 -0
  236. package/src/stateful/stateful-provider-owner-forwarder.ts +279 -0
  237. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  238. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  239. package/src/stateful/stateful-provider-session-routing.ts +555 -0
  240. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  241. package/src/stateful-signing.ts +46 -0
  242. package/src/stealth/profiles.ts +10 -26
  243. package/src/stream.ts +8 -19
  244. package/src/testing/index.ts +2 -2
  245. package/src/testing/run.ts +19 -67
  246. package/src/types.ts +88 -1
  247. package/src/user-input.ts +118 -0
  248. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  249. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  250. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  251. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  252. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  253. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  254. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  255. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  256. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  257. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  258. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  259. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  260. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  261. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -2,36 +2,67 @@ import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { Hono } from "hono";
4
4
  import { z } from "zod";
5
- import { AuthAbortError, createAuthFlowHelpers } from "../auth";
6
- import { AuthError, ProviderError, SessionExpiredError, TransportError, } from "../errors";
7
- import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog";
8
- import { categoryForStatus, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability";
9
- import { createScratchpad } from "../runtime/auth-flow";
10
- import { createBrowserClient } from "../runtime/browser";
11
- import { createProviderCache } from "../runtime/cache";
12
- import { createProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "../runtime/choice";
13
- import { createCredentialContext } from "../runtime/credential";
14
- import { createEnvContext } from "../runtime/env";
15
- import { executeOperation } from "../runtime/executor";
16
- import { createHttpClient } from "../runtime/http";
17
- import { wrapWithInstrumentation } from "../runtime/instrumentation";
18
- import { getProviderBaseUrl } from "../runtime/provider";
19
- import { PROXY_AUTH_IP_DENIED_CODE, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_EXHAUSTED_CODE, } from "../runtime/proxy-errors";
20
- import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector, } from "../runtime/proxy-telemetry";
21
- import { createProviderRuntimeStateFromEnv, createUnsupportedProviderRuntimeState, } from "../runtime/state";
22
- import { createStealthClient } from "../runtime/stealth";
23
- import { createSttClientFromEnv } from "../runtime/stt";
24
- import { createTraceContext } from "../runtime/trace";
25
- import { parseSchema } from "../schema";
26
- import { getStealthProfile } from "../stealth/profiles";
27
- import { APIFUSE_STREAM_DONE_EVENT, APIFUSE_STREAM_ERROR_EVENT, encodeSseEvent, error as streamError, } from "../stream";
28
- import { createSelfTestApp, createSelfTestInvoke, resolveSelfTestPort, } from "./self-test";
29
- import { resolveSelfTestMasterSecrets } from "./self-test-token";
30
- import { AuthFlowRequestSchema, OperationRequestSchema, } from "./types";
5
+ import { AuthAbortError, createAuthFlowHelpers } from "../auth.js";
6
+ import { AuthError, isProviderError, isSessionExpiredError, isTransportError, ProviderError, } from "../errors.js";
7
+ import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.js";
8
+ import { categoryForStatus, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
9
+ import { createScratchpad } from "../runtime/auth-flow.js";
10
+ import { createBrowserClient } from "../runtime/browser.js";
11
+ import { createProviderCache } from "../runtime/cache.js";
12
+ import { createProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "../runtime/choice.js";
13
+ import { createCredentialContext } from "../runtime/credential.js";
14
+ import { createEnvContext } from "../runtime/env.js";
15
+ import { executeOperation } from "../runtime/executor.js";
16
+ import { createHttpClient } from "../runtime/http.js";
17
+ import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
18
+ import { getProviderBaseUrl } from "../runtime/provider.js";
19
+ import { PROXY_AUTH_IP_DENIED_CODE, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_EXHAUSTED_CODE, } from "../runtime/proxy-errors.js";
20
+ import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector } from "../runtime/proxy-telemetry.js";
21
+ import { assertRequiredSecretsPresent, listMissingRequiredSecrets, MISSING_SECRET_CODE, } from "../runtime/secrets.js";
22
+ import { createProviderRuntimeStateFromEnv, createUnsupportedProviderRuntimeState, } from "../runtime/state.js";
23
+ import { createStealthClient } from "../runtime/stealth.js";
24
+ import { createSttClientFromEnv } from "../runtime/stt.js";
25
+ import { createTraceContext } from "../runtime/trace.js";
26
+ import { parseSchema } from "../schema.js";
27
+ import { STATEFUL_NONCE_HEADER as STATEFUL_FORWARDING_NONCE_HEADER, STATEFUL_SIGNATURE_HEADER as STATEFUL_FORWARDING_SIGNATURE_HEADER, STATEFUL_TIMESTAMP_HEADER as STATEFUL_FORWARDING_TIMESTAMP_HEADER, verifyStatefulRequestSignature, } from "../stateful-signing.js";
28
+ import { StatefulRoutingDeadlineError } from "../stateful/stateful-provider-session-routing.js";
29
+ import { getStealthProfile } from "../stealth/profiles.js";
30
+ import { APIFUSE_STREAM_DONE_EVENT, APIFUSE_STREAM_ERROR_EVENT, encodeSseEvent, error as streamError, } from "../stream.js";
31
+ import { createSelfTestApp, createSelfTestAuthFlowInvoke, createSelfTestInvoke, resolveSelfTestPort, } from "./self-test.js";
32
+ import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
33
+ import { AuthFlowRequestSchema, OperationConnectionSchema, OperationRequestSchema, } from "./types.js";
31
34
  const DEFAULT_HOST = "0.0.0.0";
32
35
  const DEFAULT_PORT = 3000;
33
36
  const AUTH_FLOW_LOCALES = ["en", "ko", "ja"];
34
37
  const retryResponseMeta = new WeakMap();
38
+ const STATEFUL_INTERNAL_OPERATIONS_ROUTE = "/__apifuse/stateful/operations";
39
+ const STATEFUL_FORWARDING_SOURCE_POD_HEADER = "x-apifuse-stateful-source-pod";
40
+ const DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS = 5 * 60_000;
41
+ const DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES = 10_000;
42
+ const STATEFUL_FORWARDING_REPLAY_BUCKET_MS = 10_000;
43
+ const STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS = Math.ceil(STATEFUL_FORWARDING_REPLAY_BUCKET_MS / 1_000);
44
+ export const ProviderServerStatefulForwardEnvelopeSchema = z
45
+ .object({
46
+ requestId: z.string().min(1),
47
+ providerId: z.string().min(1),
48
+ operationId: z.string().min(1),
49
+ sessionKey: z.string().min(1),
50
+ connectionId: z.string().min(1),
51
+ serviceAccountId: z.string().min(1),
52
+ ownerPodId: z.string().min(1),
53
+ generation: z.number().int().positive(),
54
+ sourcePodId: z.string().min(1),
55
+ forwardedAt: z.string().refine((value) => Number.isFinite(Date.parse(value))),
56
+ deadlineAt: z
57
+ .string()
58
+ .refine((value) => Number.isFinite(Date.parse(value)))
59
+ .optional(),
60
+ idempotencyKey: z.string().min(1).optional(),
61
+ operationRequest: OperationRequestSchema.extend({
62
+ connection: OperationConnectionSchema.strict().optional(),
63
+ }).strict(),
64
+ })
65
+ .strict();
35
66
  function createAuthStub() {
36
67
  return {
37
68
  async requestField(name) {
@@ -93,9 +124,7 @@ function getProviderStealthBaseUrl(provider) {
93
124
  return firstHost ? `https://${firstHost}` : undefined;
94
125
  }
95
126
  function getProviderStealthProfile(provider) {
96
- return provider.stealth?.profile
97
- ? getStealthProfile(provider.stealth.profile)
98
- : undefined;
127
+ return provider.stealth?.profile ? getStealthProfile(provider.stealth.profile) : undefined;
99
128
  }
100
129
  function isProductionProviderBrowserMode(provider, env = process.env) {
101
130
  if (provider.runtime !== "browser") {
@@ -104,18 +133,19 @@ function isProductionProviderBrowserMode(provider, env = process.env) {
104
133
  if (env.APIFUSE__PROVIDER__RUNTIME === "browser") {
105
134
  return true;
106
135
  }
107
- return (env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id);
136
+ return env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id;
108
137
  }
109
138
  export function resolveProviderProxyAffinityKey(provider, request, operationId) {
110
- const connectionKey = request.connection?.id ?? request.connection?.externalRef;
111
- const affinity = typeof provider.proxy === "object"
112
- ? provider.proxy.session?.affinity
113
- : undefined;
139
+ const connectionKey = resolveOperationConnectionId(request) ?? request.connection?.externalRef;
140
+ const affinity = typeof provider.proxy === "object" ? provider.proxy.session?.affinity : undefined;
114
141
  if (affinity === "operation") {
115
142
  return `${provider.id}/${operationId}`;
116
143
  }
117
144
  return connectionKey ?? provider.id;
118
145
  }
146
+ function resolveOperationConnectionId(request) {
147
+ return request.connection?.id ?? request.connectionId;
148
+ }
119
149
  function createProviderContext(provider, request, operationId, options = {}, state = createUnsupportedProviderRuntimeState(), proxyTelemetry) {
120
150
  const baseUrl = getProviderBaseUrl(provider);
121
151
  const stealthBaseUrl = getProviderStealthBaseUrl(provider);
@@ -142,7 +172,7 @@ function createProviderContext(provider, request, operationId, options = {}, sta
142
172
  values: request.connection?.secrets,
143
173
  });
144
174
  const requestContext = {
145
- connectionId: request.connection?.id,
175
+ connectionId: resolveOperationConnectionId(request),
146
176
  headers: request.headers ?? {},
147
177
  };
148
178
  const context = wrapWithInstrumentation({
@@ -290,7 +320,17 @@ function zodDetails(error) {
290
320
  }));
291
321
  }
292
322
  function toErrorResponse(error, requestId) {
293
- if (error instanceof ProviderError) {
323
+ if (error instanceof StatefulRoutingDeadlineError) {
324
+ return {
325
+ error: {
326
+ code: "STATEFUL_FORWARDING_DEADLINE_EXPIRED",
327
+ message: "Stateful forwarding deadline expired.",
328
+ ...(requestId ? { requestId } : {}),
329
+ details: { retryable: false },
330
+ },
331
+ };
332
+ }
333
+ if (isProviderError(error)) {
294
334
  const details = publicProviderErrorDetails(error);
295
335
  return {
296
336
  error: {
@@ -312,11 +352,22 @@ function toErrorResponse(error, requestId) {
312
352
  },
313
353
  };
314
354
  }
355
+ // A masked internal error MUST NOT be advertised as retryable: without an
356
+ // explicit retryable:false the hub (bori provider-backed engine) defaults 5xx
357
+ // to retryable:true, which turns a deterministic pre-upstream crash into an
358
+ // infinite START->CONTINUE->restart loop (2026-07-22 catchtable reserve RCA).
359
+ // We still refuse to leak message/stack — only the error class name (or the
360
+ // primitive type for non-Error throwables) is surfaced for ops triage.
315
361
  return {
316
362
  error: {
317
363
  code: "internal_error",
318
364
  message: "Internal error",
319
365
  ...(requestId ? { requestId } : {}),
366
+ details: {
367
+ retryable: false,
368
+ category: "internal_error",
369
+ errorClass: error instanceof Error ? error.name : typeof error,
370
+ },
320
371
  },
321
372
  };
322
373
  }
@@ -340,20 +391,37 @@ function publicProviderErrorDetails(error) {
340
391
  function isPlainRecord(value) {
341
392
  return value !== null && typeof value === "object" && !Array.isArray(value);
342
393
  }
394
+ // Accepts `unknown` so the branded guards narrow cleanly from the top: the
395
+ // subtype error classes are structurally compatible with ProviderError, so
396
+ // narrowing from a ProviderError-typed value would collapse the negative branch
397
+ // to `never`. Narrowing from unknown avoids that while still recognizing errors
398
+ // from a duplicate SDK module instance.
343
399
  function providerObservabilityDetails(error) {
344
400
  // Session-expiry surfaces the credential_expired category + the opt-in
345
401
  // retryable signal so Gateway/Credential Service can refresh and re-drive the
346
402
  // operation (see design.md §4.3 D3). Without this branch the auth error would
347
403
  // serialize as a bare 401 with no retryable/category, losing the refresh
348
404
  // signal for exactly the retryOnAuthRefresh operations it is meant to enable.
349
- if (error instanceof SessionExpiredError) {
405
+ if (isSessionExpiredError(error)) {
350
406
  return {
351
407
  category: error.options?.category ?? "credential_expired",
352
408
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
353
409
  retryable: error.options?.retryable ?? false,
354
410
  };
355
411
  }
356
- if (!(error instanceof TransportError)) {
412
+ // Missing-secret errors carry the canonical credential_unavailable category
413
+ // so Gateway/observability can attribute the failure to provisioning, not
414
+ // the upstream. Matched by code (not constructor) so both the SDK-owned
415
+ // runtime gate and any not-yet-migrated provider-thrown MISSING_SECRET
416
+ // serialize identically, including across duplicate SDK module instances.
417
+ if (isProviderError(error) && error.code === MISSING_SECRET_CODE) {
418
+ return {
419
+ category: error.options?.category ?? "credential_unavailable",
420
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
421
+ retryable: error.options?.retryable ?? false,
422
+ };
423
+ }
424
+ if (!isTransportError(error)) {
357
425
  return undefined;
358
426
  }
359
427
  const isProxyPoolCode = error.code === PROXY_POOL_EXHAUSTED_CODE ||
@@ -382,7 +450,7 @@ function providerObservabilityDetails(error) {
382
450
  };
383
451
  }
384
452
  function publicProviderErrorMessage(error) {
385
- if (error instanceof TransportError) {
453
+ if (isTransportError(error)) {
386
454
  if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
387
455
  return error.message;
388
456
  }
@@ -410,14 +478,21 @@ function toStatusCode(error) {
410
478
  if (error instanceof z.ZodError) {
411
479
  return 400;
412
480
  }
413
- if (error instanceof TransportError) {
481
+ if (error instanceof StatefulRoutingDeadlineError) {
482
+ return 504;
483
+ }
484
+ if (isTransportError(error)) {
414
485
  return error.code === "transport_timeout" ? 504 : 502;
415
486
  }
416
- if (error instanceof ProviderError) {
487
+ if (isProviderError(error)) {
417
488
  switch (error.code) {
418
489
  case "AUTH_REQUIRED":
419
490
  case "reauth_required":
420
491
  return 401;
492
+ // Unprovisioned declared secret: a deployment/config defect, never an
493
+ // upstream failure — explicit 400 (was only reached via fallthrough).
494
+ case MISSING_SECRET_CODE:
495
+ return 400;
421
496
  case "NOT_FOUND":
422
497
  case "not_found":
423
498
  case "NO_DATA":
@@ -431,6 +506,7 @@ function toStatusCode(error) {
431
506
  return 502;
432
507
  case "STT_UNAVAILABLE":
433
508
  case "UNSUPPORTED_STT_BACKEND":
509
+ case "STATEFUL_FORWARDING_REPLAY_CACHE_FULL":
434
510
  return 503;
435
511
  }
436
512
  return 400;
@@ -445,16 +521,16 @@ function extractRequestId(raw) {
445
521
  return typeof value === "string" ? value : undefined;
446
522
  }
447
523
  function logProviderError(logger, provider, kind, route, requestId, error, status, cost) {
448
- const code = error instanceof ProviderError
524
+ const code = isProviderError(error)
449
525
  ? (error.code ?? "provider_error")
450
526
  : error instanceof z.ZodError
451
527
  ? "invalid_request"
452
- : "internal_error";
528
+ : error instanceof StatefulRoutingDeadlineError
529
+ ? "STATEFUL_FORWARDING_DEADLINE_EXPIRED"
530
+ : "internal_error";
453
531
  const errorClass = error instanceof Error ? error.name : typeof error;
454
532
  const message = error instanceof Error ? error.message : String(error);
455
- const details = error instanceof ProviderError
456
- ? providerObservabilityDetails(error)
457
- : undefined;
533
+ const details = isProviderError(error) ? providerObservabilityDetails(error) : undefined;
458
534
  const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
459
535
  emit({
460
536
  level: status >= 500 ? "error" : "warn",
@@ -468,7 +544,7 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
468
544
  code,
469
545
  errorClass,
470
546
  message,
471
- ...(error instanceof TransportError && error.upstreamStatus
547
+ ...(isTransportError(error) && error.upstreamStatus
472
548
  ? { upstreamStatus: error.upstreamStatus }
473
549
  : {}),
474
550
  ...(details
@@ -591,8 +667,7 @@ async function validateSseEvent(operation, event) {
591
667
  const transport = getSseTransport(operation);
592
668
  const schema = transport?.events?.[event.event];
593
669
  if (!schema) {
594
- if (event.event === APIFUSE_STREAM_ERROR_EVENT ||
595
- event.event === APIFUSE_STREAM_DONE_EVENT) {
670
+ if (event.event === APIFUSE_STREAM_ERROR_EVENT || event.event === APIFUSE_STREAM_DONE_EVENT) {
596
671
  return event;
597
672
  }
598
673
  throw new ProviderError(`SSE event "${event.event}" is not declared in operation transport.events.`, {
@@ -711,8 +786,7 @@ function enforceStreamChunkLimit(body, maxChunkBytes) {
711
786
  }
712
787
  function toStreamingResponse(operation, result, cleanup, requestId) {
713
788
  const transport = operation.transport?.kind ?? "json";
714
- if (transport === "sse" &&
715
- (result instanceof Response || result instanceof ReadableStream)) {
789
+ if (transport === "sse" && (result instanceof Response || result instanceof ReadableStream)) {
716
790
  void cleanup();
717
791
  throw new ProviderError("SSE operations must return an AsyncIterable of typed stream.event(...) values.", {
718
792
  code: "SSE_RESULT_UNSUPPORTED",
@@ -723,9 +797,7 @@ function toStreamingResponse(operation, result, cleanup, requestId) {
723
797
  }
724
798
  if (result instanceof Response) {
725
799
  const httpTransport = getHttpStreamTransport(operation);
726
- if (httpTransport &&
727
- result.body &&
728
- httpTransport?.maxChunkBytes !== undefined) {
800
+ if (httpTransport && result.body && httpTransport?.maxChunkBytes !== undefined) {
729
801
  return responseWithCleanup(new Response(enforceStreamChunkLimit(result.body, httpTransport.maxChunkBytes), {
730
802
  headers: result.headers,
731
803
  status: result.status,
@@ -744,8 +816,7 @@ function toStreamingResponse(operation, result, cleanup, requestId) {
744
816
  ? { "Content-Type": "text/event-stream; charset=utf-8" }
745
817
  : {
746
818
  "Content-Type": operation.transport?.kind === "http-stream"
747
- ? (operation.transport.contentType ??
748
- "application/octet-stream")
819
+ ? (operation.transport.contentType ?? "application/octet-stream")
749
820
  : "application/octet-stream",
750
821
  },
751
822
  }), cleanup);
@@ -763,9 +834,7 @@ function getSseTransport(operation) {
763
834
  return operation.transport?.kind === "sse" ? operation.transport : undefined;
764
835
  }
765
836
  function getHttpStreamTransport(operation) {
766
- return operation.transport?.kind === "http-stream"
767
- ? operation.transport
768
- : undefined;
837
+ return operation.transport?.kind === "http-stream" ? operation.transport : undefined;
769
838
  }
770
839
  function toAuthFlowResponse(result, contextPatch) {
771
840
  if (result instanceof Response) {
@@ -793,7 +862,7 @@ function isAuthFlowLocale(value) {
793
862
  return value === "en" || value === "ko" || value === "ja";
794
863
  }
795
864
  function isAuthTurn(value) {
796
- return (!!value && typeof value === "object" && "kind" in value && "turnId" in value);
865
+ return !!value && typeof value === "object" && "kind" in value && "turnId" in value;
797
866
  }
798
867
  function loadAuthFlowLocaleCatalogs(provider) {
799
868
  for (const providerDir of [
@@ -856,7 +925,14 @@ async function handleOperation(provider, request, operationId, options = {}, sta
856
925
  }
857
926
  };
858
927
  try {
859
- const result = await executeOperation(provider, operationId, ctx, request.input);
928
+ const result = options.operationExecutor
929
+ ? await options.operationExecutor({
930
+ provider,
931
+ operationId,
932
+ ctx,
933
+ request,
934
+ })
935
+ : await executeOperation(provider, operationId, ctx, request.input);
860
936
  if (streaming && operation) {
861
937
  return toStreamingResponse(operation, result, cleanup, request.requestId);
862
938
  }
@@ -890,8 +966,16 @@ async function handleAuthFlow(provider, request, route, options = {}, signal) {
890
966
  code: "AUTH_FLOW_NOT_CONFIGURED",
891
967
  });
892
968
  }
969
+ // Same SDK-owned gate as executeOperation: OAuth/credentials ceremonies
970
+ // depend on declared secrets (client ids/secrets), so fail structured before
971
+ // any flow code runs instead of at whatever point the ceremony first reads
972
+ // the env. `abort` stays exempt: a user must always be able to cancel a
973
+ // stranded flow even when provisioning is broken.
893
974
  const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
894
975
  try {
976
+ if (route !== "abort") {
977
+ assertRequiredSecretsPresent(provider, context.env);
978
+ }
895
979
  const result = route === "start"
896
980
  ? await flow.start(context)
897
981
  : route === "continue"
@@ -930,14 +1014,142 @@ async function handleAuthFlow(provider, request, route, options = {}, signal) {
930
1014
  context.stealth.close?.();
931
1015
  }
932
1016
  }
1017
+ class StatefulForwardingReplayCache {
1018
+ maxEntries;
1019
+ #nonces = new Map();
1020
+ #expiryBuckets = new Map();
1021
+ #nextExpiryBucket;
1022
+ #latestExpiryBucket;
1023
+ constructor(maxEntries) {
1024
+ this.maxEntries = maxEntries;
1025
+ }
1026
+ claim(nonce, expiresAtMs, nowMs) {
1027
+ this.dropExpiredBuckets(nowMs);
1028
+ if (this.#nonces.has(nonce))
1029
+ return "replayed";
1030
+ if (this.#nonces.size >= this.maxEntries)
1031
+ return "full";
1032
+ const expiryBucket = Math.ceil(expiresAtMs / STATEFUL_FORWARDING_REPLAY_BUCKET_MS) *
1033
+ STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
1034
+ this.#nonces.set(nonce, expiryBucket);
1035
+ const bucket = this.#expiryBuckets.get(expiryBucket) ?? new Set();
1036
+ bucket.add(nonce);
1037
+ this.#expiryBuckets.set(expiryBucket, bucket);
1038
+ this.#nextExpiryBucket = Math.min(this.#nextExpiryBucket ?? expiryBucket, expiryBucket);
1039
+ this.#latestExpiryBucket = Math.max(this.#latestExpiryBucket ?? expiryBucket, expiryBucket);
1040
+ return "accepted";
1041
+ }
1042
+ dropExpiredBuckets(nowMs) {
1043
+ if (this.#nextExpiryBucket === undefined || this.#latestExpiryBucket === undefined)
1044
+ return;
1045
+ if (nowMs >= this.#latestExpiryBucket) {
1046
+ this.#nonces.clear();
1047
+ this.#expiryBuckets.clear();
1048
+ this.#nextExpiryBucket = undefined;
1049
+ this.#latestExpiryBucket = undefined;
1050
+ return;
1051
+ }
1052
+ while (this.#nextExpiryBucket <= nowMs) {
1053
+ const bucket = this.#expiryBuckets.get(this.#nextExpiryBucket);
1054
+ if (bucket) {
1055
+ for (const cachedNonce of bucket)
1056
+ this.#nonces.delete(cachedNonce);
1057
+ this.#expiryBuckets.delete(this.#nextExpiryBucket);
1058
+ }
1059
+ this.#nextExpiryBucket += STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
1060
+ }
1061
+ }
1062
+ }
1063
+ function verifyStatefulForwardingRequest(input) {
1064
+ const config = input.options.statefulForwarding;
1065
+ if (!config?.secret) {
1066
+ throw new ProviderError("Stateful forwarding is not configured.", {
1067
+ code: "STATEFUL_FORWARDING_NOT_CONFIGURED",
1068
+ });
1069
+ }
1070
+ const timestamp = input.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "";
1071
+ const signature = input.headers.get(STATEFUL_FORWARDING_SIGNATURE_HEADER) ?? "";
1072
+ const nonce = input.headers.get(STATEFUL_FORWARDING_NONCE_HEADER) ?? "";
1073
+ if (!timestamp || !signature || !nonce) {
1074
+ throw new ProviderError("Stateful forwarding signature headers are missing.", {
1075
+ code: "STATEFUL_FORWARDING_SIGNATURE_MISSING",
1076
+ });
1077
+ }
1078
+ if (nonce.length > 256) {
1079
+ throw new ProviderError("Stateful forwarding nonce is invalid.", {
1080
+ code: "STATEFUL_FORWARDING_NONCE_INVALID",
1081
+ });
1082
+ }
1083
+ const timestampMs = Date.parse(timestamp);
1084
+ const maxSkewMs = config.maxSkewMs ?? DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS;
1085
+ if (!Number.isFinite(timestampMs) || Math.abs(Date.now() - timestampMs) > maxSkewMs) {
1086
+ throw new ProviderError("Stateful forwarding signature timestamp is outside the allowed skew.", { code: "STATEFUL_FORWARDING_TIMESTAMP_INVALID" });
1087
+ }
1088
+ if (!verifyStatefulRequestSignature({
1089
+ secret: config.secret,
1090
+ timestamp,
1091
+ rawBody: input.rawBody,
1092
+ method: input.method,
1093
+ path: input.path,
1094
+ nonce,
1095
+ signature,
1096
+ })) {
1097
+ throw new ProviderError("Stateful forwarding signature is invalid.", {
1098
+ code: "STATEFUL_FORWARDING_SIGNATURE_INVALID",
1099
+ });
1100
+ }
1101
+ const replayResult = input.replayCache.claim(nonce, timestampMs + maxSkewMs, Date.now());
1102
+ if (replayResult === "replayed") {
1103
+ throw new ProviderError("Stateful forwarding nonce has already been used.", {
1104
+ code: "STATEFUL_FORWARDING_REPLAY_DETECTED",
1105
+ });
1106
+ }
1107
+ if (replayResult === "full") {
1108
+ throw new ProviderError("Stateful forwarding replay cache is at capacity.", {
1109
+ code: "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
1110
+ });
1111
+ }
1112
+ }
1113
+ function operationRequestFromForwardingEnvelope(envelope) {
1114
+ return {
1115
+ ...envelope.operationRequest,
1116
+ ...(envelope.deadlineAt !== undefined ? { deadlineAt: envelope.deadlineAt } : {}),
1117
+ };
1118
+ }
1119
+ function parseStatefulForwardingEnvelope(rawBody) {
1120
+ const parsed = ProviderServerStatefulForwardEnvelopeSchema.safeParse(rawBody);
1121
+ if (parsed.success)
1122
+ return parsed.data;
1123
+ throw new ProviderError("Stateful forwarding envelope is invalid.", {
1124
+ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
1125
+ details: zodDetails(parsed.error),
1126
+ });
1127
+ }
933
1128
  export function createServerApp(provider, options = {}) {
1129
+ validateStatefulServerConfig(options);
934
1130
  const app = new Hono();
935
1131
  const logger = options.logger ?? defaultProviderServerLogger;
1132
+ const statefulForwardingReplayCache = new StatefulForwardingReplayCache(options.statefulForwarding?.replayCacheMaxEntries ??
1133
+ DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES);
936
1134
  const state = options.state ??
937
1135
  createProviderRuntimeStateFromEnv({
938
1136
  providerId: provider.id,
939
1137
  allowMemoryFallback: options.allowMemoryStateFallback === true,
940
1138
  });
1139
+ // Boot-time visibility for unprovisioned declared secrets: emit a structured
1140
+ // warn so deploy tooling/alerting sees the gap the moment the pod boots,
1141
+ // instead of discovering it request-by-request. Deliberately log-only — a
1142
+ // boot crash would trade a structured MISSING_SECRET signal for
1143
+ // CrashLoopBackOff. Requests still fail closed via the executeOperation gate.
1144
+ const missingSecretsAtBoot = listMissingRequiredSecrets(provider, createEnvContext(provider.secrets?.map((secret) => secret.name)));
1145
+ if (missingSecretsAtBoot.length > 0) {
1146
+ logger({
1147
+ level: "warn",
1148
+ event: "provider_secrets_missing",
1149
+ providerId: provider.id,
1150
+ missingSecrets: missingSecretsAtBoot,
1151
+ });
1152
+ }
941
1153
  app.notFound((c) => c.json({
942
1154
  error: {
943
1155
  code: "not_found",
@@ -949,6 +1161,120 @@ export function createServerApp(provider, options = {}) {
949
1161
  provider: provider.id,
950
1162
  version: provider.version,
951
1163
  }));
1164
+ app.post(STATEFUL_INTERNAL_OPERATIONS_ROUTE, async (c) => {
1165
+ let rawBodyText = "";
1166
+ let rawBody;
1167
+ const operation = "stateful-internal";
1168
+ const requestCost = startRequestCost();
1169
+ try {
1170
+ if (!options.internalOperationExecutor) {
1171
+ throw new ProviderError("Stateful internal operation executor is not configured.", {
1172
+ code: "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
1173
+ });
1174
+ }
1175
+ rawBodyText = await c.req.raw.clone().text();
1176
+ verifyStatefulForwardingRequest({
1177
+ options,
1178
+ rawBody: rawBodyText,
1179
+ headers: c.req.raw.headers,
1180
+ method: c.req.raw.method,
1181
+ path: STATEFUL_INTERNAL_OPERATIONS_ROUTE,
1182
+ replayCache: statefulForwardingReplayCache,
1183
+ });
1184
+ try {
1185
+ rawBody = JSON.parse(rawBodyText);
1186
+ }
1187
+ catch {
1188
+ throw new ProviderError("Stateful forwarding envelope is not valid JSON.", {
1189
+ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
1190
+ });
1191
+ }
1192
+ const envelope = parseStatefulForwardingEnvelope(rawBody);
1193
+ if (envelope.providerId !== provider.id) {
1194
+ throw new ProviderError("Stateful forwarding envelope providerId does not match the served provider.", { code: "STATEFUL_FORWARDING_PROVIDER_MISMATCH" });
1195
+ }
1196
+ if (envelope.requestId !== envelope.operationRequest.requestId) {
1197
+ throw new ProviderError("Stateful forwarding requestId values do not match.", {
1198
+ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
1199
+ });
1200
+ }
1201
+ if (envelope.sourcePodId !==
1202
+ (c.req.raw.headers.get(STATEFUL_FORWARDING_SOURCE_POD_HEADER) ?? "")) {
1203
+ throw new ProviderError("Stateful forwarding source pod does not match its header.", {
1204
+ code: "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
1205
+ });
1206
+ }
1207
+ if (envelope.forwardedAt !== (c.req.raw.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "")) {
1208
+ throw new ProviderError("Stateful forwarding forwardedAt does not match its signature timestamp.", { code: "STATEFUL_FORWARDING_ENVELOPE_INVALID" });
1209
+ }
1210
+ const deadlineAtMs = envelope.deadlineAt ? Date.parse(envelope.deadlineAt) : undefined;
1211
+ if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
1212
+ throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
1213
+ }
1214
+ const remainingDeadlineMs = deadlineAtMs === undefined ? undefined : deadlineAtMs - Date.now();
1215
+ const deadlineSignal = remainingDeadlineMs === undefined ? undefined : AbortSignal.timeout(remainingDeadlineMs);
1216
+ const signal = deadlineSignal
1217
+ ? AbortSignal.any([c.req.raw.signal, deadlineSignal])
1218
+ : c.req.raw.signal;
1219
+ const ownerFenceValidation = Promise.resolve(options.statefulForwarding?.validateOwnerFence({
1220
+ providerId: envelope.providerId,
1221
+ sessionKey: envelope.sessionKey,
1222
+ ownerPodId: envelope.ownerPodId,
1223
+ generation: envelope.generation,
1224
+ sourcePodId: envelope.sourcePodId,
1225
+ forwardedAt: envelope.forwardedAt,
1226
+ requestId: envelope.requestId,
1227
+ ...(envelope.idempotencyKey ? { idempotencyKey: envelope.idempotencyKey } : {}),
1228
+ }, signal));
1229
+ let ownerFenceValid;
1230
+ try {
1231
+ ownerFenceValid = deadlineSignal
1232
+ ? await Promise.race([
1233
+ ownerFenceValidation,
1234
+ new Promise((_resolve, reject) => {
1235
+ deadlineSignal.addEventListener("abort", () => reject(new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt)), { once: true });
1236
+ }),
1237
+ ])
1238
+ : await ownerFenceValidation;
1239
+ }
1240
+ catch (error) {
1241
+ if (deadlineSignal?.aborted) {
1242
+ throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
1243
+ }
1244
+ throw error;
1245
+ }
1246
+ if (ownerFenceValid !== true) {
1247
+ throw new ProviderError("Stateful forwarding owner fence is no longer current.", {
1248
+ code: "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
1249
+ });
1250
+ }
1251
+ const request = operationRequestFromForwardingEnvelope(envelope);
1252
+ const operationId = envelope.operationId;
1253
+ const ctx = createProviderContext(provider, request, operationId, options, state);
1254
+ if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
1255
+ throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
1256
+ }
1257
+ const output = await options.internalOperationExecutor({
1258
+ provider,
1259
+ operationId,
1260
+ ctx,
1261
+ request,
1262
+ internalStatefulForward: envelope,
1263
+ signal,
1264
+ });
1265
+ logProviderSuccess(logger, provider, "operation", operationId || operation, request.requestId, 200, finishRequestCost(requestCost));
1266
+ return c.json({ data: output });
1267
+ }
1268
+ catch (error) {
1269
+ const status = toStatusCode(error);
1270
+ if (isProviderError(error) && error.code === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
1271
+ c.header("Retry-After", String(STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS));
1272
+ }
1273
+ const requestId = extractRequestId(rawBody);
1274
+ logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost));
1275
+ return c.json(toErrorResponse(error, requestId), status);
1276
+ }
1277
+ });
952
1278
  app.post("/v1/:operation", async (c) => {
953
1279
  let rawBody;
954
1280
  const operation = c.req.param("operation");
@@ -1085,6 +1411,28 @@ export function createServerApp(provider, options = {}) {
1085
1411
  });
1086
1412
  return app;
1087
1413
  }
1414
+ function validateStatefulServerConfig(options) {
1415
+ if (options.statefulForwarding && !options.internalOperationExecutor) {
1416
+ throw new Error("Invalid provider server configuration: statefulForwarding requires internalOperationExecutor; missing option internalOperationExecutor.");
1417
+ }
1418
+ if (options.internalOperationExecutor && !options.statefulForwarding?.secret) {
1419
+ throw new Error("Invalid provider server configuration: internalOperationExecutor requires statefulForwarding.secret; missing option statefulForwarding.secret.");
1420
+ }
1421
+ if (options.statefulForwarding &&
1422
+ typeof options.statefulForwarding.validateOwnerFence !== "function") {
1423
+ throw new Error("Invalid provider server configuration: statefulForwarding requires validateOwnerFence.");
1424
+ }
1425
+ if (options.statefulForwarding?.maxSkewMs !== undefined &&
1426
+ (!Number.isFinite(options.statefulForwarding.maxSkewMs) ||
1427
+ options.statefulForwarding.maxSkewMs <= 0)) {
1428
+ throw new Error("Invalid provider server configuration: maxSkewMs must be positive.");
1429
+ }
1430
+ if (options.statefulForwarding?.replayCacheMaxEntries !== undefined &&
1431
+ (!Number.isInteger(options.statefulForwarding.replayCacheMaxEntries) ||
1432
+ options.statefulForwarding.replayCacheMaxEntries <= 0)) {
1433
+ throw new Error("Invalid provider server configuration: replayCacheMaxEntries must be a positive integer.");
1434
+ }
1435
+ }
1088
1436
  function getBunServeRuntime() {
1089
1437
  const bunValue = Object.getOwnPropertyDescriptor(globalThis, "Bun")?.value;
1090
1438
  if (!bunValue || typeof bunValue !== "object") {
@@ -1100,6 +1448,9 @@ function getBunServeRuntime() {
1100
1448
  },
1101
1449
  };
1102
1450
  }
1451
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
1452
+ const DEFAULT_SHUTDOWN_SIGNALS = ["SIGTERM", "SIGINT"];
1453
+ const processSignalCoordinators = new Map();
1103
1454
  export async function serve(provider, options = {}) {
1104
1455
  const bunRuntime = getBunServeRuntime();
1105
1456
  if (bunRuntime === undefined) {
@@ -1107,29 +1458,186 @@ export async function serve(provider, options = {}) {
1107
1458
  code: "RUNTIME_UNSUPPORTED",
1108
1459
  });
1109
1460
  }
1461
+ const logger = options.logger ?? defaultProviderServerLogger;
1462
+ const configuredTimeoutMs = shutdownTimeout(options.shutdown?.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS);
1463
+ const configuredSignals = resolveShutdownSignals(options.shutdown?.signals ?? true);
1110
1464
  const app = createServerApp(provider, {
1111
1465
  logger: options.logger,
1112
1466
  stt: options.stt,
1467
+ state: options.state,
1468
+ allowMemoryStateFallback: options.allowMemoryStateFallback,
1469
+ operationExecutor: options.operationExecutor,
1470
+ internalOperationExecutor: options.internalOperationExecutor,
1471
+ statefulForwarding: options.statefulForwarding,
1113
1472
  });
1114
- bunRuntime.serve({
1115
- port: options.port ?? DEFAULT_PORT,
1116
- hostname: options.host ?? DEFAULT_HOST,
1117
- fetch: app.fetch,
1118
- });
1119
- // Internal self-test listener (health dependency inversion): a SEPARATE
1120
- // socket the tenant-facing gateway never dials. Off by default — it only
1121
- // starts when the shared self-test master secret env is present.
1122
- const selfTestSecrets = resolveSelfTestMasterSecrets();
1123
- if (selfTestSecrets) {
1124
- const selfTestApp = createSelfTestApp(provider, {
1125
- secrets: selfTestSecrets,
1126
- invoke: createSelfTestInvoke(app),
1127
- });
1128
- bunRuntime.serve({
1129
- port: options.selfTestPort ?? resolveSelfTestPort(),
1473
+ const servers = [];
1474
+ try {
1475
+ servers.push(bunRuntime.serve({
1476
+ port: options.port ?? DEFAULT_PORT,
1130
1477
  hostname: options.host ?? DEFAULT_HOST,
1131
- fetch: selfTestApp.fetch,
1132
- });
1478
+ fetch: app.fetch,
1479
+ }));
1480
+ // Internal self-test listener (health dependency inversion): a SEPARATE
1481
+ // socket the tenant-facing gateway never dials. Off by default — it only
1482
+ // starts when the shared self-test master secret env is present.
1483
+ const selfTestSecrets = resolveSelfTestMasterSecrets();
1484
+ if (selfTestSecrets) {
1485
+ const selfTestApp = createSelfTestApp(provider, {
1486
+ secrets: selfTestSecrets,
1487
+ invoke: createSelfTestInvoke(app),
1488
+ authFlow: createSelfTestAuthFlowInvoke(app),
1489
+ });
1490
+ servers.push(bunRuntime.serve({
1491
+ port: options.selfTestPort ?? resolveSelfTestPort(),
1492
+ hostname: options.host ?? DEFAULT_HOST,
1493
+ fetch: selfTestApp.fetch,
1494
+ }));
1495
+ }
1496
+ }
1497
+ catch (error) {
1498
+ await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
1499
+ throw error;
1500
+ }
1501
+ const server = servers[0];
1502
+ if (!server)
1503
+ throw new Error("Provider server failed to create its primary listener.");
1504
+ let closePromise;
1505
+ let unregisterSignals = () => { };
1506
+ const close = (closeOptions = {}) => {
1507
+ if (closePromise)
1508
+ return closePromise;
1509
+ const timeoutMs = shutdownTimeout(closeOptions.timeoutMs ?? configuredTimeoutMs);
1510
+ closePromise = closeProviderServers({
1511
+ servers,
1512
+ hooks: options.shutdown?.hooks ?? [],
1513
+ timeoutMs,
1514
+ logger,
1515
+ providerId: provider.id,
1516
+ }).finally(() => unregisterSignals());
1517
+ return closePromise;
1518
+ };
1519
+ try {
1520
+ unregisterSignals = registerForProcessSignals(configuredSignals, () => close({ timeoutMs: configuredTimeoutMs }));
1521
+ }
1522
+ catch (error) {
1523
+ unregisterSignals();
1524
+ await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
1525
+ throw error;
1526
+ }
1527
+ return { port: server.port, close };
1528
+ }
1529
+ function registerForProcessSignals(signals, close) {
1530
+ if (signals.length === 0)
1531
+ return () => { };
1532
+ const registration = {
1533
+ signals: new Set(signals),
1534
+ close,
1535
+ };
1536
+ let registered = true;
1537
+ const unregister = () => {
1538
+ if (!registered)
1539
+ return;
1540
+ registered = false;
1541
+ for (const signal of registration.signals) {
1542
+ const coordinator = processSignalCoordinators.get(signal);
1543
+ if (!coordinator)
1544
+ continue;
1545
+ coordinator.registrations.delete(registration);
1546
+ if (coordinator.registrations.size === 0 && !coordinator.handling) {
1547
+ process.removeListener(signal, coordinator.listener);
1548
+ processSignalCoordinators.delete(signal);
1549
+ }
1550
+ }
1551
+ };
1552
+ try {
1553
+ for (const signal of signals) {
1554
+ let coordinator = processSignalCoordinators.get(signal);
1555
+ if (!coordinator) {
1556
+ const created = {
1557
+ registrations: new Set(),
1558
+ handling: false,
1559
+ listener: () => handleCoordinatedSignal(signal, created),
1560
+ };
1561
+ coordinator = created;
1562
+ processSignalCoordinators.set(signal, coordinator);
1563
+ process.on(signal, coordinator.listener);
1564
+ }
1565
+ coordinator.registrations.add(registration);
1566
+ }
1567
+ }
1568
+ catch (error) {
1569
+ unregister();
1570
+ throw error;
1571
+ }
1572
+ return unregister;
1573
+ }
1574
+ function handleCoordinatedSignal(signal, coordinator) {
1575
+ if (coordinator.handling)
1576
+ return;
1577
+ coordinator.handling = true;
1578
+ const registrations = [...coordinator.registrations];
1579
+ void Promise.allSettled(registrations.map((registration) => registration.close())).finally(() => {
1580
+ if (processSignalCoordinators.get(signal) === coordinator) {
1581
+ process.removeListener(signal, coordinator.listener);
1582
+ processSignalCoordinators.delete(signal);
1583
+ }
1584
+ try {
1585
+ process.kill(process.pid, signal);
1586
+ }
1587
+ catch {
1588
+ process.exitCode = 1;
1589
+ }
1590
+ });
1591
+ }
1592
+ async function closeProviderServers(input) {
1593
+ const deadline = Date.now() + input.timeoutMs;
1594
+ const gracefulStops = input.servers.map((server) => server.stop(false));
1595
+ for (const gracefulStop of gracefulStops)
1596
+ gracefulStop.catch(() => undefined);
1597
+ for (const [hookIndex, hook] of input.hooks.entries()) {
1598
+ try {
1599
+ await withinShutdownBudget(Promise.resolve().then(hook), deadline);
1600
+ }
1601
+ catch (error) {
1602
+ try {
1603
+ input.logger({
1604
+ level: "error",
1605
+ event: "provider_shutdown_hook_failed",
1606
+ providerId: input.providerId,
1607
+ hookIndex,
1608
+ errorClass: error instanceof Error ? error.name : "UnknownError",
1609
+ message: error instanceof Error ? error.message : "Shutdown hook failed.",
1610
+ });
1611
+ }
1612
+ catch { }
1613
+ }
1614
+ }
1615
+ const forcedStops = input.servers.map((server) => server.stop(true));
1616
+ await withinShutdownBudget(Promise.allSettled([...gracefulStops, ...forcedStops]).then(() => undefined), deadline).catch(() => undefined);
1617
+ }
1618
+ async function withinShutdownBudget(promise, deadline) {
1619
+ promise.catch(() => undefined);
1620
+ const remainingMs = Math.max(0, deadline - Date.now());
1621
+ let timer;
1622
+ const timeout = new Promise((_resolve, reject) => {
1623
+ timer = setTimeout(() => reject(new Error("Provider server shutdown timed out.")), remainingMs);
1624
+ });
1625
+ try {
1626
+ return await Promise.race([promise, timeout]);
1627
+ }
1628
+ finally {
1629
+ if (timer)
1630
+ clearTimeout(timer);
1631
+ }
1632
+ }
1633
+ function resolveShutdownSignals(signals) {
1634
+ if (signals === false)
1635
+ return [];
1636
+ return [...new Set(signals === true ? DEFAULT_SHUTDOWN_SIGNALS : signals)];
1637
+ }
1638
+ function shutdownTimeout(value) {
1639
+ if (!Number.isFinite(value) || value < 0) {
1640
+ throw new Error("Provider server shutdown timeoutMs must be a non-negative finite number.");
1133
1641
  }
1134
- await Promise.resolve();
1642
+ return value;
1135
1643
  }