@apifuse/provider-sdk 2.1.0-beta.2 → 2.1.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 (231) hide show
  1. package/AUTHORING.md +330 -8
  2. package/CHANGELOG.md +89 -1
  3. package/README.md +64 -17
  4. package/SUBMISSION.md +86 -0
  5. package/bin/apifuse-check.ts +60 -6
  6. package/bin/apifuse-dev.ts +58 -8
  7. package/bin/apifuse-pack-check.ts +32 -2
  8. package/bin/apifuse-pack-smoke.ts +133 -6
  9. package/bin/apifuse-perf.ts +142 -49
  10. package/bin/apifuse-record.ts +182 -104
  11. package/bin/apifuse-submit-check.ts +3243 -0
  12. package/bin/apifuse.ts +1 -1
  13. package/dist/auth.d.ts +76 -0
  14. package/dist/auth.js +436 -0
  15. package/dist/ceremonies/index.d.ts +41 -0
  16. package/dist/ceremonies/index.js +490 -0
  17. package/dist/choice-token.d.ts +24 -0
  18. package/dist/choice-token.js +74 -0
  19. package/dist/cli/commands.d.ts +10 -0
  20. package/dist/cli/commands.js +80 -0
  21. package/dist/cli/create.d.ts +47 -0
  22. package/dist/cli/create.js +777 -0
  23. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  24. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  25. package/dist/cli/templates/provider/AGENTS.md.tpl +87 -0
  26. package/dist/cli/templates/provider/CLAUDE.md.tpl +1 -0
  27. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  28. package/dist/cli/templates/provider/README.md.tpl +163 -0
  29. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  30. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  31. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  32. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  33. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  34. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  35. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  36. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  37. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  38. package/dist/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  39. package/dist/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  40. package/dist/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  41. package/dist/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  42. package/dist/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  43. package/dist/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  44. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  45. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  46. package/dist/config/loader.d.ts +107 -0
  47. package/dist/config/loader.js +935 -0
  48. package/dist/contract-json.d.ts +9 -0
  49. package/dist/contract-json.js +51 -0
  50. package/dist/contract-serialization.d.ts +4 -0
  51. package/dist/contract-serialization.js +78 -0
  52. package/dist/contract-types.d.ts +49 -0
  53. package/dist/contract-types.js +1 -0
  54. package/dist/contract.d.ts +6 -0
  55. package/dist/contract.js +156 -0
  56. package/dist/define.d.ts +100 -0
  57. package/dist/define.js +1383 -0
  58. package/dist/dev.d.ts +9 -0
  59. package/dist/dev.js +15 -0
  60. package/dist/errors.d.ts +59 -0
  61. package/dist/errors.js +97 -0
  62. package/dist/i18n/catalog.d.ts +29 -0
  63. package/dist/i18n/catalog.js +159 -0
  64. package/dist/i18n/index.d.ts +2 -0
  65. package/dist/i18n/index.js +2 -0
  66. package/dist/i18n/keys.d.ts +10 -0
  67. package/dist/i18n/keys.js +34 -0
  68. package/dist/index.d.ts +42 -0
  69. package/dist/index.js +38 -0
  70. package/dist/lint.d.ts +74 -0
  71. package/dist/lint.js +729 -0
  72. package/dist/observability.d.ts +5 -0
  73. package/dist/observability.js +39 -0
  74. package/dist/provider.d.ts +11 -0
  75. package/dist/provider.js +9 -0
  76. package/dist/public-schema-field-lint.d.ts +2 -0
  77. package/dist/public-schema-field-lint.js +158 -0
  78. package/dist/recipes/gov-api.d.ts +19 -0
  79. package/dist/recipes/gov-api.js +72 -0
  80. package/dist/recipes/rest-api.d.ts +21 -0
  81. package/dist/recipes/rest-api.js +115 -0
  82. package/dist/runtime/auth-flow.d.ts +14 -0
  83. package/dist/runtime/auth-flow.js +46 -0
  84. package/dist/runtime/browser.d.ts +25 -0
  85. package/dist/runtime/browser.js +1237 -0
  86. package/dist/runtime/cache.d.ts +10 -0
  87. package/dist/runtime/cache.js +372 -0
  88. package/dist/runtime/choice.d.ts +15 -0
  89. package/dist/runtime/choice.js +435 -0
  90. package/dist/runtime/credential.d.ts +8 -0
  91. package/dist/runtime/credential.js +61 -0
  92. package/dist/runtime/env.d.ts +2 -0
  93. package/dist/runtime/env.js +10 -0
  94. package/dist/runtime/executor.d.ts +16 -0
  95. package/dist/runtime/executor.js +51 -0
  96. package/dist/runtime/http.d.ts +8 -0
  97. package/dist/runtime/http.js +726 -0
  98. package/dist/runtime/insights.d.ts +9 -0
  99. package/dist/runtime/insights.js +324 -0
  100. package/dist/runtime/instrumentation.d.ts +8 -0
  101. package/dist/runtime/instrumentation.js +269 -0
  102. package/dist/runtime/key-derivation.d.ts +24 -0
  103. package/dist/runtime/key-derivation.js +73 -0
  104. package/dist/runtime/keyring.d.ts +25 -0
  105. package/dist/runtime/keyring.js +93 -0
  106. package/dist/runtime/namespace.d.ts +9 -0
  107. package/dist/runtime/namespace.js +19 -0
  108. package/dist/runtime/otlp.d.ts +39 -0
  109. package/dist/runtime/otlp.js +103 -0
  110. package/dist/runtime/perf.d.ts +12 -0
  111. package/dist/runtime/perf.js +52 -0
  112. package/dist/runtime/prevalidate.d.ts +12 -0
  113. package/dist/runtime/prevalidate.js +173 -0
  114. package/dist/runtime/provider.d.ts +2 -0
  115. package/dist/runtime/provider.js +11 -0
  116. package/dist/runtime/proxy-errors.d.ts +21 -0
  117. package/dist/runtime/proxy-errors.js +83 -0
  118. package/dist/runtime/proxy-telemetry.d.ts +8 -0
  119. package/dist/runtime/proxy-telemetry.js +174 -0
  120. package/dist/runtime/redis.d.ts +17 -0
  121. package/dist/runtime/redis.js +82 -0
  122. package/dist/runtime/request-options.d.ts +3 -0
  123. package/dist/runtime/request-options.js +42 -0
  124. package/dist/runtime/state.d.ts +17 -0
  125. package/dist/runtime/state.js +344 -0
  126. package/dist/runtime/stealth.d.ts +21 -0
  127. package/dist/runtime/stealth.js +980 -0
  128. package/dist/runtime/stt.d.ts +22 -0
  129. package/dist/runtime/stt.js +480 -0
  130. package/dist/runtime/trace.d.ts +26 -0
  131. package/dist/runtime/trace.js +142 -0
  132. package/dist/runtime/waterfall.d.ts +12 -0
  133. package/dist/runtime/waterfall.js +147 -0
  134. package/dist/schema.d.ts +74 -0
  135. package/dist/schema.js +243 -0
  136. package/dist/serve.d.ts +1 -0
  137. package/dist/serve.js +1 -0
  138. package/dist/server/index.d.ts +3 -0
  139. package/dist/server/index.js +2 -0
  140. package/dist/server/serve.d.ts +64 -0
  141. package/dist/server/serve.js +1118 -0
  142. package/dist/server/types.d.ts +136 -0
  143. package/dist/server/types.js +86 -0
  144. package/dist/stealth/profiles.d.ts +4 -0
  145. package/dist/stealth/profiles.js +259 -0
  146. package/dist/stream.d.ts +44 -0
  147. package/dist/stream.js +151 -0
  148. package/dist/testing/helpers.d.ts +23 -0
  149. package/dist/testing/helpers.js +95 -0
  150. package/dist/testing/index.d.ts +2 -0
  151. package/dist/testing/index.js +2 -0
  152. package/dist/testing/run.d.ts +34 -0
  153. package/dist/testing/run.js +307 -0
  154. package/dist/types.d.ts +1467 -0
  155. package/dist/types.js +61 -0
  156. package/dist/utils/date.d.ts +6 -0
  157. package/dist/utils/date.js +101 -0
  158. package/dist/utils/parse.d.ts +16 -0
  159. package/dist/utils/parse.js +51 -0
  160. package/dist/utils/text.d.ts +4 -0
  161. package/dist/utils/text.js +14 -0
  162. package/dist/utils/transform.d.ts +8 -0
  163. package/dist/utils/transform.js +48 -0
  164. package/package.json +57 -29
  165. package/src/auth.ts +786 -0
  166. package/src/ceremonies/index.ts +8 -2
  167. package/src/choice-token.ts +165 -0
  168. package/src/cli/commands.ts +34 -11
  169. package/src/cli/create.ts +254 -128
  170. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  171. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  172. package/src/cli/templates/provider/AGENTS.md.tpl +87 -0
  173. package/src/cli/templates/provider/CLAUDE.md.tpl +1 -0
  174. package/src/cli/templates/provider/README.md.tpl +87 -7
  175. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  176. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  177. package/src/cli/templates/provider/index.ts.tpl +5 -47
  178. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  179. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  180. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  181. package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
  182. package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  183. package/src/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  184. package/src/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  185. package/src/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  186. package/src/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  187. package/src/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  188. package/src/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  189. package/src/cli/templates/provider/start.ts.tpl +1 -1
  190. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  191. package/src/config/loader.ts +1224 -9
  192. package/src/contract-json.ts +75 -0
  193. package/src/contract-serialization.ts +89 -0
  194. package/src/contract-types.ts +52 -0
  195. package/src/contract.ts +216 -0
  196. package/src/define.ts +1820 -70
  197. package/src/errors.ts +27 -0
  198. package/src/i18n/catalog.ts +277 -0
  199. package/src/i18n/index.ts +2 -0
  200. package/src/i18n/keys.ts +64 -0
  201. package/src/index.ts +189 -9
  202. package/src/lint.ts +580 -73
  203. package/src/observability.ts +41 -0
  204. package/src/provider.ts +131 -4
  205. package/src/public-schema-field-lint.ts +237 -0
  206. package/src/runtime/auth-flow.ts +9 -0
  207. package/src/runtime/browser.ts +1054 -51
  208. package/src/runtime/cache.ts +528 -0
  209. package/src/runtime/choice.ts +760 -0
  210. package/src/runtime/executor.ts +32 -3
  211. package/src/runtime/http.ts +980 -195
  212. package/src/runtime/insights.ts +11 -11
  213. package/src/runtime/instrumentation.ts +12 -4
  214. package/src/runtime/key-derivation.ts +1 -1
  215. package/src/runtime/keyring.ts +4 -3
  216. package/src/runtime/proxy-errors.ts +132 -0
  217. package/src/runtime/proxy-telemetry.ts +253 -0
  218. package/src/runtime/redis.ts +116 -0
  219. package/src/runtime/request-options.ts +66 -0
  220. package/src/runtime/state.ts +563 -0
  221. package/src/runtime/stealth.ts +1336 -0
  222. package/src/runtime/stt.ts +629 -0
  223. package/src/runtime/trace.ts +1 -1
  224. package/src/schema.ts +363 -1
  225. package/src/server/serve.ts +1192 -75
  226. package/src/server/types.ts +37 -0
  227. package/src/stream.ts +210 -0
  228. package/src/testing/run.ts +40 -6
  229. package/src/types.ts +1283 -59
  230. package/src/runtime/tls.ts +0 -434
  231. package/src/types/playwright-stealth.d.ts +0 -9
@@ -1,24 +1,80 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
1
4
  import { Hono } from "hono";
2
5
  import { z } from "zod";
3
-
4
- import { ProviderError, TransportError } from "../errors";
6
+ import { AuthAbortError, createAuthFlowHelpers } from "../auth";
7
+ import {
8
+ AuthError,
9
+ ProviderError,
10
+ SessionExpiredError,
11
+ TransportError,
12
+ } from "../errors";
13
+ import {
14
+ loadProviderLocaleCatalogs,
15
+ localizeAuthTurn,
16
+ type ProviderLocaleCatalogMap,
17
+ } from "../i18n/catalog";
18
+ import type { ProviderLocale } from "../i18n/keys";
19
+ import {
20
+ categoryForStatus,
21
+ isRetryableCategory,
22
+ PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
23
+ type ProviderErrorCategory,
24
+ } from "../observability";
5
25
  import { createScratchpad } from "../runtime/auth-flow";
6
26
  import { createBrowserClient } from "../runtime/browser";
27
+ import { createProviderCache } from "../runtime/cache";
28
+ import {
29
+ createProviderChoiceContext,
30
+ PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
31
+ } from "../runtime/choice";
7
32
  import { createCredentialContext } from "../runtime/credential";
8
33
  import { createEnvContext } from "../runtime/env";
9
34
  import { executeOperation } from "../runtime/executor";
10
35
  import { createHttpClient } from "../runtime/http";
36
+ import { wrapWithInstrumentation } from "../runtime/instrumentation";
11
37
  import { getProviderBaseUrl } from "../runtime/provider";
12
- import { createTlsClient } from "../runtime/tls";
38
+ import {
39
+ PROXY_AUTH_IP_DENIED_CODE,
40
+ PROXY_EDGE_AUTH_REJECTED_CODE,
41
+ PROXY_POOL_EXHAUSTED_CODE,
42
+ } from "../runtime/proxy-errors";
43
+ import {
44
+ PROVIDER_TELEMETRY_HEADER,
45
+ ProxyTelemetryCollector,
46
+ } from "../runtime/proxy-telemetry";
47
+ import {
48
+ createProviderRuntimeStateFromEnv,
49
+ createUnsupportedProviderRuntimeState,
50
+ } from "../runtime/state";
51
+ import { createStealthClient } from "../runtime/stealth";
52
+ import { createSttClientFromEnv } from "../runtime/stt";
13
53
  import { createTraceContext } from "../runtime/trace";
54
+ import { parseSchema } from "../schema";
55
+ import { getStealthProfile } from "../stealth/profiles";
56
+ import {
57
+ APIFUSE_STREAM_DONE_EVENT,
58
+ APIFUSE_STREAM_ERROR_EVENT,
59
+ encodeSseEvent,
60
+ error as streamError,
61
+ } from "../stream";
14
62
  import type {
15
63
  AuthContext,
64
+ AuthTurn,
16
65
  BrowserClient,
17
66
  FlowContext,
18
67
  FlowContextStore,
68
+ HttpRetrySummary,
69
+ OperationDefinition,
70
+ OperationHttpStreamTransport,
71
+ OperationSseTransport,
19
72
  ProviderContext,
20
73
  ProviderDefinition,
21
- TlsClient,
74
+ ProviderRuntimeState,
75
+ ProviderStreamEvent,
76
+ StealthClient,
77
+ SttContext,
22
78
  } from "../types";
23
79
  import {
24
80
  type AuthFlowRequest,
@@ -34,6 +90,10 @@ import {
34
90
 
35
91
  const DEFAULT_HOST = "0.0.0.0";
36
92
  const DEFAULT_PORT = 3000;
93
+ const AUTH_FLOW_LOCALES = ["en", "ko", "ja"] as const;
94
+ const retryResponseMeta = new WeakMap<ProviderContext, HttpRetrySummary>();
95
+
96
+ type RequestCleanup = () => void | Promise<void>;
37
97
 
38
98
  function createAuthStub(): AuthContext {
39
99
  return {
@@ -48,62 +108,186 @@ function createAuthStub(): AuthContext {
48
108
  function createBrowserStub(): BrowserClient {
49
109
  return {
50
110
  engine: "playwright-stealth",
111
+ async close() {},
51
112
  async newPage() {
52
113
  throw new ProviderError("Browser runtime is not available", {
53
114
  code: "BROWSER_RUNTIME_UNSUPPORTED",
54
115
  });
55
116
  },
117
+ async rawPage() {
118
+ throw new ProviderError("Browser runtime is not available", {
119
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
120
+ });
121
+ },
122
+ async withIsolatedContext() {
123
+ throw new ProviderError("Browser runtime is not available", {
124
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
125
+ });
126
+ },
127
+ async solveChallenge() {
128
+ throw new ProviderError("Browser runtime is not available", {
129
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
130
+ });
131
+ },
56
132
  };
57
133
  }
58
134
 
59
- function createTlsStub(): TlsClient {
135
+ function createStealthStub(): StealthClient {
60
136
  return {
61
137
  async fetch() {
62
- throw new ProviderError("TLS runtime is not available", {
63
- code: "TLS_RUNTIME_UNSUPPORTED",
138
+ throw new ProviderError("Stealth runtime is not available", {
139
+ code: "STEALTH_RUNTIME_UNSUPPORTED",
64
140
  });
65
141
  },
66
142
  createSession() {
67
- throw new ProviderError("TLS runtime is not available", {
68
- code: "TLS_RUNTIME_UNSUPPORTED",
143
+ throw new ProviderError("Stealth runtime is not available", {
144
+ code: "STEALTH_RUNTIME_UNSUPPORTED",
69
145
  });
70
146
  },
147
+ close() {
148
+ // no-op
149
+ },
71
150
  };
72
151
  }
73
152
 
153
+ function getProviderStealthBaseUrl(
154
+ provider: ProviderDefinition,
155
+ ): string | undefined {
156
+ const baseUrl = getProviderBaseUrl(provider);
157
+ if (baseUrl) {
158
+ return baseUrl;
159
+ }
160
+ const firstHost = provider.allowedHosts?.[0];
161
+ return firstHost ? `https://${firstHost}` : undefined;
162
+ }
163
+
164
+ function getProviderStealthProfile(provider: ProviderDefinition) {
165
+ return provider.stealth?.profile
166
+ ? getStealthProfile(provider.stealth.profile)
167
+ : undefined;
168
+ }
169
+
170
+ function isProductionProviderBrowserMode(
171
+ provider: ProviderDefinition,
172
+ env = process.env,
173
+ ): boolean {
174
+ if (provider.runtime !== "browser") {
175
+ return false;
176
+ }
177
+
178
+ if (env.APIFUSE__PROVIDER__RUNTIME === "browser") {
179
+ return true;
180
+ }
181
+
182
+ return (
183
+ env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id
184
+ );
185
+ }
186
+
187
+ export function resolveProviderProxyAffinityKey(
188
+ provider: ProviderDefinition,
189
+ request: OperationRequest,
190
+ operationId: string,
191
+ ): string {
192
+ const connectionKey =
193
+ request.connection?.id ?? request.connection?.externalRef;
194
+ const affinity =
195
+ typeof provider.proxy === "object"
196
+ ? provider.proxy.session?.affinity
197
+ : undefined;
198
+ if (affinity === "operation") {
199
+ return `${provider.id}/${operationId}`;
200
+ }
201
+ return connectionKey ?? provider.id;
202
+ }
203
+
74
204
  function createProviderContext(
75
205
  provider: ProviderDefinition,
76
206
  request: OperationRequest,
207
+ operationId: string,
208
+ options: ProviderServerOptions = {},
209
+ state: ProviderRuntimeState = createUnsupportedProviderRuntimeState(),
210
+ proxyTelemetry?: ProxyTelemetryCollector,
77
211
  ): ProviderContext {
78
212
  const baseUrl = getProviderBaseUrl(provider);
213
+ const stealthBaseUrl = getProviderStealthBaseUrl(provider);
214
+ const stealthProfile = getProviderStealthProfile(provider);
215
+ const proxyClientOptions = {
216
+ upstream: { proxy: provider.proxy },
217
+ affinityKey: resolveProviderProxyAffinityKey(
218
+ provider,
219
+ request,
220
+ operationId,
221
+ ),
222
+ telemetry: proxyTelemetry,
223
+ };
224
+ let wrappedContext: ProviderContext | undefined;
225
+ const stealthClientOptions = {
226
+ upstream: proxyClientOptions.upstream,
227
+ affinityKey: proxyClientOptions.affinityKey,
228
+ telemetry: proxyTelemetry,
229
+ };
79
230
 
80
- return {
81
- env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
82
- credential: createCredentialContext({
83
- allowedKeys: provider.credential?.keys,
84
- mode: request.connection?.mode,
85
- scopes: request.connection?.scopes,
86
- values: request.connection?.secrets,
231
+ const env = createEnvContext([
232
+ ...(provider.secrets?.map((secret) => secret.name) ?? []),
233
+ PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
234
+ ]);
235
+ const credential = createCredentialContext({
236
+ allowedKeys: provider.credential?.keys,
237
+ mode: request.connection?.mode,
238
+ scopes: request.connection?.scopes,
239
+ values: request.connection?.secrets,
240
+ });
241
+ const requestContext = {
242
+ connectionId: request.connection?.id,
243
+ headers: request.headers ?? {},
244
+ };
245
+ const context = wrapWithInstrumentation({
246
+ env,
247
+ credential,
248
+ request: requestContext,
249
+ http: createHttpClient(baseUrl, {
250
+ ...proxyClientOptions,
251
+ onRetrySummary: (summary) => {
252
+ if (summary.attempts <= 1 || !wrappedContext) return;
253
+ retryResponseMeta.set(wrappedContext, summary);
254
+ },
87
255
  }),
88
- request: {
89
- connectionId: request.connection?.id,
90
- headers: request.headers ?? {},
91
- },
92
- http: createHttpClient(baseUrl),
93
- tls: baseUrl ? createTlsClient(baseUrl) : createTlsStub(),
256
+ cache: createProviderCache({ providerId: provider.id }),
257
+ state,
258
+ stealth: stealthBaseUrl
259
+ ? stealthProfile
260
+ ? createStealthClient(
261
+ stealthBaseUrl,
262
+ stealthProfile.name,
263
+ stealthClientOptions,
264
+ )
265
+ : createStealthClient(stealthBaseUrl, stealthClientOptions)
266
+ : createStealthStub(),
94
267
  browser:
95
268
  provider.runtime === "browser"
96
269
  ? createBrowserClient({
97
- cdpUrl:
98
- process.env.CDP_POOL_URL ?? process.env.APIFUSE_CDP_POOL_URL,
270
+ allowedHosts: provider.allowedHosts,
271
+ cdpUrl: process.env.APIFUSE__CDP_POOL__URL,
99
272
  headless: true,
273
+ requireCdpPool: isProductionProviderBrowserMode(provider),
100
274
  stealth: true,
101
275
  engine: provider.browser?.engine,
102
276
  })
103
277
  : createBrowserStub(),
104
278
  trace: createTraceContext(),
105
279
  auth: createAuthStub(),
106
- };
280
+ stt: options.stt ?? createSttClientFromEnv(provider.stt),
281
+ choice: createProviderChoiceContext({
282
+ providerId: provider.id,
283
+ env,
284
+ request: requestContext,
285
+ credential,
286
+ state,
287
+ }),
288
+ });
289
+ wrappedContext = context;
290
+ return context;
107
291
  }
108
292
 
109
293
  function createFlowContextStore(
@@ -145,16 +329,41 @@ function createFlowContextStore(
145
329
  function createAuthFlowContext(
146
330
  provider: ProviderDefinition,
147
331
  request: AuthFlowRequest,
332
+ options: ProviderServerOptions = {},
333
+ signal?: AbortSignal,
148
334
  ): {
149
335
  context: FlowContext;
150
336
  getPatch: () => Record<string, unknown | null> | undefined;
151
337
  } {
152
338
  const baseUrl = getProviderBaseUrl(provider);
339
+ const stealthBaseUrl = getProviderStealthBaseUrl(provider);
340
+ const stealthProfile = getProviderStealthProfile(provider);
153
341
  const contextData = request.context ?? {};
154
342
  const flowContextStore = createFlowContextStore(
155
343
  provider.context?.keys ?? Object.keys(contextData),
156
344
  contextData,
157
345
  );
346
+ const proxyClientOptions = {
347
+ upstream: { proxy: provider.proxy },
348
+ affinityKey:
349
+ request.connectionId ??
350
+ request.externalRef ??
351
+ request.tenantId ??
352
+ request.providerId ??
353
+ provider.id,
354
+ };
355
+ const stealthClientOptions = {
356
+ upstream: proxyClientOptions.upstream,
357
+ affinityKey: proxyClientOptions.affinityKey,
358
+ };
359
+ const credential = request.connection
360
+ ? createCredentialContext({
361
+ allowedKeys: provider.credential?.keys,
362
+ mode: request.connection.mode,
363
+ scopes: request.connection.scopes,
364
+ values: request.connection.secrets,
365
+ })
366
+ : undefined;
158
367
 
159
368
  return {
160
369
  context: {
@@ -162,39 +371,114 @@ function createAuthFlowContext(
162
371
  externalRef: request.externalRef,
163
372
  tenantId: request.tenantId ?? "",
164
373
  providerId: request.providerId ?? provider.id,
165
- http: createHttpClient(baseUrl),
374
+ http: createHttpClient(baseUrl, proxyClientOptions),
375
+ stealth: stealthBaseUrl
376
+ ? stealthProfile
377
+ ? createStealthClient(
378
+ stealthBaseUrl,
379
+ stealthProfile.name,
380
+ stealthClientOptions,
381
+ )
382
+ : createStealthClient(stealthBaseUrl, stealthClientOptions)
383
+ : createStealthStub(),
166
384
  env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
385
+ credential,
167
386
  context: flowContextStore.context,
387
+ stt: options.stt ?? createSttClientFromEnv(provider.stt),
388
+ auth: createAuthFlowHelpers({ signal }),
168
389
  },
169
390
  getPatch: flowContextStore.getPatch,
170
391
  };
171
392
  }
172
393
 
173
- export type ProviderServerLogEvent = {
174
- level: "warn" | "error";
175
- event: "provider_request_failed";
394
+ type ProviderRequestCost = {
395
+ durationMs: number;
396
+ cpuUserMicros: number;
397
+ cpuSystemMicros: number;
398
+ cpuTotalMicros: number;
399
+ };
400
+
401
+ type ProviderServerLogEventBase = ProviderRequestCost & {
176
402
  providerId: string;
177
403
  kind: "operation" | "auth";
178
404
  route: string;
179
405
  requestId?: string;
180
406
  status: number;
181
- code: string;
182
- errorClass: string;
183
- message: string;
184
- upstreamStatus?: number;
185
- issues?: Array<{ path: string; code: string; message: string }>;
186
407
  };
187
408
 
409
+ export type ProviderServerLogEvent =
410
+ | (ProviderServerLogEventBase & {
411
+ level: "info";
412
+ event: "provider_request_completed";
413
+ })
414
+ | (ProviderServerLogEventBase & {
415
+ level: "warn" | "error";
416
+ event: "provider_request_failed";
417
+ code: string;
418
+ errorClass: string;
419
+ message: string;
420
+ upstreamStatus?: number;
421
+ errorCategory?: ProviderErrorCategory;
422
+ taxonomyVersion?: string;
423
+ retryable?: boolean;
424
+ issues?: Array<{ path: string; code: string; message: string }>;
425
+ })
426
+ | {
427
+ level: "warn";
428
+ event: "provider_cleanup_failed";
429
+ providerId: string;
430
+ kind: "operation";
431
+ route: string;
432
+ requestId?: string;
433
+ resource: "browser" | "stealth";
434
+ errorClass: string;
435
+ message: string;
436
+ };
437
+
188
438
  export type ProviderServerLogger = (event: ProviderServerLogEvent) => void;
189
439
 
190
440
  export type ProviderServerOptions = {
191
441
  logger?: ProviderServerLogger;
442
+ /** Optional STT override for tests or custom hosts; local/prod normally resolves from env. */
443
+ stt?: SttContext;
444
+ /** Optional runtime state override for tests or custom hosts. Production resolves Redis from env and fails closed when unavailable. */
445
+ state?: ProviderRuntimeState;
446
+ /** Allow process-local runtime state only for local development and tests. */
447
+ allowMemoryStateFallback?: boolean;
192
448
  };
193
449
 
194
450
  const defaultProviderServerLogger: ProviderServerLogger = (event) => {
195
- console.error(JSON.stringify(event));
451
+ const line = JSON.stringify(event);
452
+ if (event.level === "info") {
453
+ console.log(line);
454
+ return;
455
+ }
456
+ console.error(line);
196
457
  };
197
458
 
459
+ function startRequestCost(): {
460
+ startedAtMs: number;
461
+ cpuStart: NodeJS.CpuUsage;
462
+ } {
463
+ return {
464
+ startedAtMs: performance.now(),
465
+ cpuStart: process.cpuUsage(),
466
+ };
467
+ }
468
+
469
+ function finishRequestCost(input: {
470
+ startedAtMs: number;
471
+ cpuStart: NodeJS.CpuUsage;
472
+ }): ProviderRequestCost {
473
+ const cpuDelta = process.cpuUsage(input.cpuStart);
474
+ return {
475
+ durationMs: Math.max(0, Math.round(performance.now() - input.startedAtMs)),
476
+ cpuUserMicros: Math.max(0, cpuDelta.user),
477
+ cpuSystemMicros: Math.max(0, cpuDelta.system),
478
+ cpuTotalMicros: Math.max(0, cpuDelta.user + cpuDelta.system),
479
+ };
480
+ }
481
+
198
482
  function zodDetails(error: z.ZodError): Array<{
199
483
  path: string;
200
484
  code: string;
@@ -212,14 +496,14 @@ function toErrorResponse(
212
496
  requestId?: string,
213
497
  ): OperationErrorResponse {
214
498
  if (error instanceof ProviderError) {
499
+ const details = publicProviderErrorDetails(error);
215
500
  return {
216
501
  error: {
217
502
  code: error.code ?? "provider_error",
218
503
  message: publicProviderErrorMessage(error),
219
504
  ...(requestId ? { requestId } : {}),
220
- ...(error instanceof TransportError && error.status
221
- ? { details: { upstreamStatus: error.status } }
222
- : {}),
505
+ ...(error.fix ? { fix: error.fix } : {}),
506
+ ...(details ? { details } : {}),
223
507
  },
224
508
  };
225
509
  }
@@ -244,8 +528,92 @@ function toErrorResponse(
244
528
  };
245
529
  }
246
530
 
531
+ function publicProviderErrorDetails(error: ProviderError): unknown {
532
+ const providerDetails = error.details;
533
+ const observabilityDetails = providerObservabilityDetails(error);
534
+
535
+ if (providerDetails === undefined) {
536
+ return observabilityDetails;
537
+ }
538
+ if (observabilityDetails === undefined) {
539
+ return providerDetails;
540
+ }
541
+ if (isPlainRecord(providerDetails) && isPlainRecord(observabilityDetails)) {
542
+ return { ...providerDetails, ...observabilityDetails };
543
+ }
544
+ return {
545
+ provider: providerDetails,
546
+ observability: observabilityDetails,
547
+ };
548
+ }
549
+
550
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
551
+ return value !== null && typeof value === "object" && !Array.isArray(value);
552
+ }
553
+
554
+ function providerObservabilityDetails(error: ProviderError):
555
+ | {
556
+ category: ProviderErrorCategory;
557
+ taxonomyVersion: string;
558
+ retryable: boolean;
559
+ upstreamStatus?: number;
560
+ }
561
+ | undefined {
562
+ // Session-expiry surfaces the credential_expired category + the opt-in
563
+ // retryable signal so Gateway/Credential Service can refresh and re-drive the
564
+ // operation (see design.md §4.3 D3). Without this branch the auth error would
565
+ // serialize as a bare 401 with no retryable/category, losing the refresh
566
+ // signal for exactly the retryOnAuthRefresh operations it is meant to enable.
567
+ if (error instanceof SessionExpiredError) {
568
+ return {
569
+ category: error.options?.category ?? "credential_expired",
570
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
571
+ retryable: error.options?.retryable ?? false,
572
+ };
573
+ }
574
+ if (!(error instanceof TransportError)) {
575
+ return undefined;
576
+ }
577
+ const isProxyPoolCode =
578
+ error.code === PROXY_POOL_EXHAUSTED_CODE ||
579
+ error.code === PROXY_EDGE_AUTH_REJECTED_CODE ||
580
+ error.code === "PROXY_ALLOCATION_FAILED";
581
+ const category =
582
+ error.options?.category ??
583
+ (isProxyPoolCode
584
+ ? "proxy_pool"
585
+ : error.code === PROXY_AUTH_IP_DENIED_CODE
586
+ ? "anti_bot_blocked"
587
+ : error.code === "transport_timeout"
588
+ ? "timeout"
589
+ : error.code === "transport_network_error"
590
+ ? "network"
591
+ : error.upstreamStatus
592
+ ? categoryForStatus(error.upstreamStatus)
593
+ : "upstream_http");
594
+ return {
595
+ category,
596
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
597
+ retryable:
598
+ error.options?.retryable ??
599
+ (category === "upstream_http" && error.upstreamStatus
600
+ ? error.upstreamStatus >= 500
601
+ : isRetryableCategory(category)),
602
+ ...(error.upstreamStatus ? { upstreamStatus: error.upstreamStatus } : {}),
603
+ };
604
+ }
605
+
247
606
  function publicProviderErrorMessage(error: ProviderError): string {
248
607
  if (error instanceof TransportError) {
608
+ if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
609
+ return error.message;
610
+ }
611
+ if (error.code === PROXY_EDGE_AUTH_REJECTED_CODE) {
612
+ return error.message;
613
+ }
614
+ if (error.code === PROXY_POOL_EXHAUSTED_CODE) {
615
+ return error.message;
616
+ }
249
617
  if (error.code === "transport_timeout") return "Request timed out";
250
618
  if (error.code === "transport_network_error") return "Network error";
251
619
  if (error.code === "upstream_http_error" && error.status) {
@@ -259,7 +627,9 @@ function publicProviderErrorMessage(error: ProviderError): string {
259
627
  return error.message;
260
628
  }
261
629
 
262
- function toStatusCode(error: unknown): 400 | 404 | 500 | 502 | 504 {
630
+ function toStatusCode(
631
+ error: unknown,
632
+ ): 400 | 401 | 404 | 429 | 500 | 502 | 503 | 504 {
263
633
  if (error instanceof z.ZodError) {
264
634
  return 400;
265
635
  }
@@ -269,8 +639,24 @@ function toStatusCode(error: unknown): 400 | 404 | 500 | 502 | 504 {
269
639
  }
270
640
 
271
641
  if (error instanceof ProviderError) {
272
- if (error.code === "NOT_FOUND" || error.code === "NO_DATA") {
273
- return 404;
642
+ switch (error.code) {
643
+ case "AUTH_REQUIRED":
644
+ case "reauth_required":
645
+ return 401;
646
+ case "NOT_FOUND":
647
+ case "not_found":
648
+ case "NO_DATA":
649
+ return 404;
650
+ case "RATE_LIMITED":
651
+ case "UPSTREAM_RATE_LIMIT":
652
+ case "LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR":
653
+ return 429;
654
+ case "UPSTREAM_ERROR":
655
+ case "BLOCKED":
656
+ return 502;
657
+ case "STT_UNAVAILABLE":
658
+ case "UNSUPPORTED_STT_BACKEND":
659
+ return 503;
274
660
  }
275
661
 
276
662
  return 400;
@@ -296,6 +682,7 @@ function logProviderError(
296
682
  requestId: string | undefined,
297
683
  error: unknown,
298
684
  status: number,
685
+ cost: ProviderRequestCost,
299
686
  ): void {
300
687
  const code =
301
688
  error instanceof ProviderError
@@ -305,6 +692,10 @@ function logProviderError(
305
692
  : "internal_error";
306
693
  const errorClass = error instanceof Error ? error.name : typeof error;
307
694
  const message = error instanceof Error ? error.message : String(error);
695
+ const details =
696
+ error instanceof ProviderError
697
+ ? providerObservabilityDetails(error)
698
+ : undefined;
308
699
  const emit =
309
700
  typeof logger === "function" ? logger : defaultProviderServerLogger;
310
701
  emit({
@@ -315,18 +706,75 @@ function logProviderError(
315
706
  route,
316
707
  ...(requestId ? { requestId } : {}),
317
708
  status,
709
+ ...cost,
318
710
  code,
319
711
  errorClass,
320
712
  message,
321
- ...(error instanceof TransportError && error.status
322
- ? { upstreamStatus: error.status }
713
+ ...(error instanceof TransportError && error.upstreamStatus
714
+ ? { upstreamStatus: error.upstreamStatus }
715
+ : {}),
716
+ ...(details
717
+ ? {
718
+ errorCategory: details.category,
719
+ taxonomyVersion: details.taxonomyVersion,
720
+ retryable: details.retryable,
721
+ }
323
722
  : {}),
324
723
  ...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
325
724
  });
326
725
  }
327
726
 
727
+ function logProviderCleanupError(
728
+ logger: ProviderServerLogger | unknown,
729
+ provider: ProviderDefinition,
730
+ operationId: string,
731
+ requestId: string | undefined,
732
+ resource: "browser" | "stealth",
733
+ error: unknown,
734
+ ): void {
735
+ const emit =
736
+ typeof logger === "function" ? logger : defaultProviderServerLogger;
737
+ const errorClass = error instanceof Error ? error.name : typeof error;
738
+ const message = error instanceof Error ? error.message : String(error);
739
+ emit({
740
+ level: "warn",
741
+ event: "provider_cleanup_failed",
742
+ providerId: provider.id,
743
+ kind: "operation",
744
+ route: operationId,
745
+ ...(requestId ? { requestId } : {}),
746
+ resource,
747
+ errorClass,
748
+ message,
749
+ });
750
+ }
751
+
752
+ function logProviderSuccess(
753
+ logger: ProviderServerLogger | unknown,
754
+ provider: ProviderDefinition,
755
+ kind: "operation" | "auth",
756
+ route: string,
757
+ requestId: string | undefined,
758
+ status: number,
759
+ cost: ProviderRequestCost,
760
+ ): void {
761
+ const emit =
762
+ typeof logger === "function" ? logger : defaultProviderServerLogger;
763
+ emit({
764
+ level: "info",
765
+ event: "provider_request_completed",
766
+ providerId: provider.id,
767
+ kind,
768
+ route,
769
+ ...(requestId ? { requestId } : {}),
770
+ status,
771
+ ...cost,
772
+ });
773
+ }
774
+
328
775
  function toJsonSuccessResponse(
329
776
  result: unknown,
777
+ ctx?: ProviderContext,
330
778
  ): Response | OperationSuccessResponse {
331
779
  if (result instanceof Response) {
332
780
  return result;
@@ -336,7 +784,335 @@ function toJsonSuccessResponse(
336
784
  return new Response(result);
337
785
  }
338
786
 
339
- return { data: result };
787
+ const cacheMeta = ctx?.cache.responseMeta();
788
+ const retryMeta = ctx ? retryResponseMeta.get(ctx) : undefined;
789
+ const meta =
790
+ cacheMeta || retryMeta
791
+ ? {
792
+ ...(cacheMeta
793
+ ? {
794
+ cached: cacheMeta.hit,
795
+ stale: cacheMeta.stale,
796
+ cache: cacheMeta,
797
+ }
798
+ : {}),
799
+ ...(retryMeta ? { retry: retryMeta } : {}),
800
+ }
801
+ : undefined;
802
+ return {
803
+ data: result,
804
+ ...(meta ? { meta } : {}),
805
+ };
806
+ }
807
+
808
+ function isAsyncIterable<T = unknown>(
809
+ value: unknown,
810
+ ): value is AsyncIterable<T> {
811
+ if (!value || typeof value !== "object") return false;
812
+ const iterator = Reflect.get(value, Symbol.asyncIterator);
813
+ return typeof iterator === "function";
814
+ }
815
+
816
+ function responseWithCleanup(
817
+ response: Response,
818
+ cleanup: RequestCleanup,
819
+ ): Response {
820
+ if (!response.body) {
821
+ void cleanup();
822
+ return response;
823
+ }
824
+ const reader = response.body.getReader();
825
+ let cleaned = false;
826
+ const runCleanup = async () => {
827
+ if (cleaned) return;
828
+ cleaned = true;
829
+ await cleanup();
830
+ };
831
+ const body = new ReadableStream<Uint8Array>({
832
+ async pull(controller) {
833
+ try {
834
+ const { done, value } = await reader.read();
835
+ if (done) {
836
+ controller.close();
837
+ await runCleanup();
838
+ return;
839
+ }
840
+ if (value) controller.enqueue(value);
841
+ } catch (error) {
842
+ await runCleanup();
843
+ controller.error(error);
844
+ }
845
+ },
846
+ async cancel(reason) {
847
+ try {
848
+ await reader.cancel(reason);
849
+ } finally {
850
+ await runCleanup();
851
+ }
852
+ },
853
+ });
854
+ return new Response(body, {
855
+ headers: response.headers,
856
+ status: response.status,
857
+ statusText: response.statusText,
858
+ });
859
+ }
860
+
861
+ async function validateSseEvent(
862
+ operation: OperationDefinition,
863
+ event: ProviderStreamEvent,
864
+ ): Promise<ProviderStreamEvent> {
865
+ const transport = getSseTransport(operation);
866
+ const schema = transport?.events?.[event.event];
867
+ if (!schema) {
868
+ if (
869
+ event.event === APIFUSE_STREAM_ERROR_EVENT ||
870
+ event.event === APIFUSE_STREAM_DONE_EVENT
871
+ ) {
872
+ return event;
873
+ }
874
+ throw new ProviderError(
875
+ `SSE event "${event.event}" is not declared in operation transport.events.`,
876
+ {
877
+ code: "SSE_EVENT_UNDECLARED",
878
+ category: "output_validation",
879
+ retryable: false,
880
+ fix: `Add "${event.event}" to transport.events or stop emitting that event.`,
881
+ },
882
+ );
883
+ }
884
+ const data = await parseSchema(
885
+ schema,
886
+ event.data,
887
+ `transport.events.${event.event}`,
888
+ );
889
+ return { ...event, data };
890
+ }
891
+
892
+ function byteLength(value: Uint8Array | string): number {
893
+ if (typeof value === "string") {
894
+ return new TextEncoder().encode(value).byteLength;
895
+ }
896
+ return value.byteLength;
897
+ }
898
+
899
+ function assertStreamPayloadWithinLimit(
900
+ actualBytes: number,
901
+ maxBytes: number | undefined,
902
+ kind: "event" | "chunk",
903
+ ): void {
904
+ if (maxBytes === undefined || actualBytes <= maxBytes) return;
905
+ throw new ProviderError(
906
+ `Stream ${kind} exceeded declared byte limit (${actualBytes} > ${maxBytes}).`,
907
+ {
908
+ code:
909
+ kind === "event" ? "STREAM_EVENT_TOO_LARGE" : "STREAM_CHUNK_TOO_LARGE",
910
+ retryable: false,
911
+ category: "input_validation",
912
+ fix:
913
+ kind === "event"
914
+ ? "Emit smaller SSE events or increase transport.maxEventBytes."
915
+ : "Emit smaller stream chunks or increase transport.maxChunkBytes.",
916
+ },
917
+ );
918
+ }
919
+
920
+ function toSseResponse(
921
+ operation: OperationDefinition,
922
+ result: AsyncIterable<ProviderStreamEvent>,
923
+ cleanup: RequestCleanup,
924
+ requestId?: string,
925
+ ): Response {
926
+ const encoder = new TextEncoder();
927
+ const iterator = result[Symbol.asyncIterator]();
928
+ const transport = getSseTransport(operation);
929
+ let done = false;
930
+ let cleaned = false;
931
+ const runCleanup = async () => {
932
+ if (cleaned) return;
933
+ cleaned = true;
934
+ await cleanup();
935
+ };
936
+ const body = new ReadableStream<Uint8Array>({
937
+ async pull(controller) {
938
+ try {
939
+ if (done) {
940
+ controller.close();
941
+ await runCleanup();
942
+ return;
943
+ }
944
+ const next = await iterator.next();
945
+ if (next.done) {
946
+ done = true;
947
+ controller.close();
948
+ await runCleanup();
949
+ return;
950
+ }
951
+ const validated = await validateSseEvent(operation, next.value);
952
+ const encodedEvent = encodeSseEvent(validated);
953
+ const encodedBytes = encoder.encode(encodedEvent);
954
+ assertStreamPayloadWithinLimit(
955
+ encodedBytes.byteLength,
956
+ transport?.maxEventBytes,
957
+ "event",
958
+ );
959
+ controller.enqueue(encodedBytes);
960
+ } catch (error) {
961
+ const message =
962
+ error instanceof Error ? error.message : "Stream failed";
963
+ controller.enqueue(
964
+ encoder.encode(
965
+ encodeSseEvent(
966
+ streamError("stream_error", message, {
967
+ ...(requestId ? { requestId } : {}),
968
+ }),
969
+ ),
970
+ ),
971
+ );
972
+ controller.close();
973
+ done = true;
974
+ await runCleanup();
975
+ }
976
+ },
977
+ async cancel(reason) {
978
+ try {
979
+ await iterator.return?.(reason);
980
+ } finally {
981
+ await runCleanup();
982
+ }
983
+ },
984
+ });
985
+ return new Response(body, {
986
+ headers: {
987
+ "Cache-Control": "no-cache, no-transform",
988
+ Connection: "keep-alive",
989
+ "Content-Type": "text/event-stream; charset=utf-8",
990
+ },
991
+ });
992
+ }
993
+
994
+ function enforceStreamChunkLimit(
995
+ body: ReadableStream<Uint8Array>,
996
+ maxChunkBytes: number | undefined,
997
+ ): ReadableStream<Uint8Array> {
998
+ if (maxChunkBytes === undefined) return body;
999
+ const reader = body.getReader();
1000
+ return new ReadableStream<Uint8Array>({
1001
+ async pull(controller) {
1002
+ try {
1003
+ const { done, value } = await reader.read();
1004
+ if (done) {
1005
+ controller.close();
1006
+ return;
1007
+ }
1008
+ if (value) {
1009
+ assertStreamPayloadWithinLimit(
1010
+ byteLength(value),
1011
+ maxChunkBytes,
1012
+ "chunk",
1013
+ );
1014
+ controller.enqueue(value);
1015
+ }
1016
+ } catch (error) {
1017
+ controller.error(error);
1018
+ }
1019
+ },
1020
+ cancel(reason) {
1021
+ return reader.cancel(reason);
1022
+ },
1023
+ });
1024
+ }
1025
+
1026
+ function toStreamingResponse(
1027
+ operation: OperationDefinition,
1028
+ result: unknown,
1029
+ cleanup: RequestCleanup,
1030
+ requestId?: string,
1031
+ ): Response {
1032
+ const transport = operation.transport?.kind ?? "json";
1033
+ if (
1034
+ transport === "sse" &&
1035
+ (result instanceof Response || result instanceof ReadableStream)
1036
+ ) {
1037
+ void cleanup();
1038
+ throw new ProviderError(
1039
+ "SSE operations must return an AsyncIterable of typed stream.event(...) values.",
1040
+ {
1041
+ code: "SSE_RESULT_UNSUPPORTED",
1042
+ category: "output_validation",
1043
+ retryable: false,
1044
+ fix: "Return an async generator that yields stream.event(name, data) so APIFuse can validate event schemas and enforce event byte limits.",
1045
+ },
1046
+ );
1047
+ }
1048
+ if (result instanceof Response) {
1049
+ const httpTransport = getHttpStreamTransport(operation);
1050
+ if (
1051
+ httpTransport &&
1052
+ result.body &&
1053
+ httpTransport?.maxChunkBytes !== undefined
1054
+ ) {
1055
+ return responseWithCleanup(
1056
+ new Response(
1057
+ enforceStreamChunkLimit(result.body, httpTransport.maxChunkBytes),
1058
+ {
1059
+ headers: result.headers,
1060
+ status: result.status,
1061
+ statusText: result.statusText,
1062
+ },
1063
+ ),
1064
+ cleanup,
1065
+ );
1066
+ }
1067
+ return responseWithCleanup(result, cleanup);
1068
+ }
1069
+ if (result instanceof ReadableStream) {
1070
+ const httpTransport = getHttpStreamTransport(operation);
1071
+ const stream =
1072
+ httpTransport !== undefined
1073
+ ? enforceStreamChunkLimit(result, httpTransport.maxChunkBytes)
1074
+ : result;
1075
+ return responseWithCleanup(
1076
+ new Response(stream, {
1077
+ headers:
1078
+ transport === "sse"
1079
+ ? { "Content-Type": "text/event-stream; charset=utf-8" }
1080
+ : {
1081
+ "Content-Type":
1082
+ operation.transport?.kind === "http-stream"
1083
+ ? (operation.transport.contentType ??
1084
+ "application/octet-stream")
1085
+ : "application/octet-stream",
1086
+ },
1087
+ }),
1088
+ cleanup,
1089
+ );
1090
+ }
1091
+ if (transport === "sse" && isAsyncIterable<ProviderStreamEvent>(result)) {
1092
+ return toSseResponse(operation, result, cleanup, requestId);
1093
+ }
1094
+ void cleanup();
1095
+ throw new ProviderError(
1096
+ `Streaming operation returned unsupported result for transport "${transport}"`,
1097
+ {
1098
+ code: "STREAM_RESULT_UNSUPPORTED",
1099
+ fix: "Return an AsyncIterable of stream.event(...) values, a ReadableStream, or a Response from streaming operations.",
1100
+ },
1101
+ );
1102
+ }
1103
+
1104
+ function getSseTransport(
1105
+ operation: OperationDefinition,
1106
+ ): OperationSseTransport | undefined {
1107
+ return operation.transport?.kind === "sse" ? operation.transport : undefined;
1108
+ }
1109
+
1110
+ function getHttpStreamTransport(
1111
+ operation: OperationDefinition,
1112
+ ): OperationHttpStreamTransport | undefined {
1113
+ return operation.transport?.kind === "http-stream"
1114
+ ? operation.transport
1115
+ : undefined;
340
1116
  }
341
1117
 
342
1118
  function toAuthFlowResponse(
@@ -357,27 +1133,168 @@ function toAuthFlowResponse(
357
1133
  };
358
1134
  }
359
1135
 
1136
+ function authFlowLocaleFromHeaders(
1137
+ headers?: Record<string, string>,
1138
+ ): ProviderLocale {
1139
+ const header = Object.entries(headers ?? {}).find(
1140
+ ([key]) => key.toLowerCase() === "accept-language",
1141
+ )?.[1];
1142
+ for (const token of (header ?? "").split(",")) {
1143
+ const language = token.trim().split(";")[0]?.split("-")[0]?.toLowerCase();
1144
+ if (isAuthFlowLocale(language)) {
1145
+ return language;
1146
+ }
1147
+ }
1148
+ return "en";
1149
+ }
1150
+
1151
+ function isAuthFlowLocale(value: string | undefined): value is ProviderLocale {
1152
+ return value === "en" || value === "ko" || value === "ja";
1153
+ }
1154
+
1155
+ function isAuthTurn(value: unknown): value is AuthTurn {
1156
+ return (
1157
+ !!value && typeof value === "object" && "kind" in value && "turnId" in value
1158
+ );
1159
+ }
1160
+
1161
+ function loadAuthFlowLocaleCatalogs(
1162
+ provider: ProviderDefinition,
1163
+ ): ProviderLocaleCatalogMap | undefined {
1164
+ for (const providerDir of [
1165
+ process.cwd(),
1166
+ join(process.cwd(), "providers", provider.id),
1167
+ join(process.cwd(), "providers-staging", provider.id),
1168
+ ]) {
1169
+ if (!existsSync(join(providerDir, "locales", "en.json"))) continue;
1170
+ try {
1171
+ return loadProviderLocaleCatalogs({
1172
+ providerDir,
1173
+ locales: AUTH_FLOW_LOCALES,
1174
+ });
1175
+ } catch {
1176
+ return undefined;
1177
+ }
1178
+ }
1179
+ return undefined;
1180
+ }
1181
+
1182
+ function materializeAuthFlowTurn(
1183
+ provider: ProviderDefinition,
1184
+ request: AuthFlowRequest,
1185
+ turn: AuthTurn,
1186
+ ): AuthTurn {
1187
+ const catalogs = loadAuthFlowLocaleCatalogs(provider);
1188
+ if (!catalogs) return turn;
1189
+ return localizeAuthTurn(turn, {
1190
+ catalogs,
1191
+ locale: authFlowLocaleFromHeaders(request.headers),
1192
+ });
1193
+ }
1194
+
1195
+ function withAuthRequestHeaders(
1196
+ request: AuthFlowRequest,
1197
+ headers: Headers,
1198
+ ): AuthFlowRequest {
1199
+ return {
1200
+ ...request,
1201
+ headers: {
1202
+ ...(request.headers ?? {}),
1203
+ ...Object.fromEntries(headers.entries()),
1204
+ },
1205
+ };
1206
+ }
1207
+
360
1208
  async function handleOperation(
361
1209
  provider: ProviderDefinition,
362
1210
  request: OperationRequest,
363
1211
  operationId: string,
1212
+ options: ProviderServerOptions = {},
1213
+ state: ProviderRuntimeState = createUnsupportedProviderRuntimeState(),
1214
+ proxyTelemetry?: ProxyTelemetryCollector,
364
1215
  ): Promise<Response | OperationResponse> {
365
- const ctx = createProviderContext(provider, request);
366
- const result = await executeOperation(
1216
+ const ctx = createProviderContext(
367
1217
  provider,
1218
+ request,
368
1219
  operationId,
369
- ctx,
370
- request.input,
1220
+ options,
1221
+ state,
1222
+ proxyTelemetry,
371
1223
  );
372
- return toJsonSuccessResponse(result);
1224
+ const operation = provider.operations[operationId];
1225
+ const streaming =
1226
+ operation?.transport?.kind && operation.transport.kind !== "json";
1227
+ let cleanupCalled = false;
1228
+ const cleanup = async () => {
1229
+ if (cleanupCalled) return;
1230
+ cleanupCalled = true;
1231
+ try {
1232
+ ctx.stealth.close?.();
1233
+ } catch (error) {
1234
+ logProviderCleanupError(
1235
+ options.logger,
1236
+ provider,
1237
+ operationId,
1238
+ request.requestId,
1239
+ "stealth",
1240
+ error,
1241
+ );
1242
+ }
1243
+ try {
1244
+ await ctx.browser.close?.();
1245
+ } catch (error) {
1246
+ logProviderCleanupError(
1247
+ options.logger,
1248
+ provider,
1249
+ operationId,
1250
+ request.requestId,
1251
+ "browser",
1252
+ error,
1253
+ );
1254
+ }
1255
+ };
1256
+ try {
1257
+ const result = await executeOperation(
1258
+ provider,
1259
+ operationId,
1260
+ ctx,
1261
+ request.input,
1262
+ );
1263
+ if (streaming && operation) {
1264
+ return toStreamingResponse(operation, result, cleanup, request.requestId);
1265
+ }
1266
+ return toJsonSuccessResponse(result, ctx);
1267
+ } catch (error) {
1268
+ await cleanup();
1269
+ throw error;
1270
+ } finally {
1271
+ if (!streaming) await cleanup();
1272
+ }
373
1273
  }
374
1274
 
375
- type AuthRoute = "start" | "continue" | "poll" | "abort";
1275
+ function responseWithProviderTelemetry(
1276
+ response: Response,
1277
+ proxyTelemetry?: ProxyTelemetryCollector,
1278
+ ): Response {
1279
+ const headerValue = proxyTelemetry?.toHeaderValue();
1280
+ const headers = new Headers(response.headers);
1281
+ headers.delete(PROVIDER_TELEMETRY_HEADER);
1282
+ if (headerValue) headers.set(PROVIDER_TELEMETRY_HEADER, headerValue);
1283
+ return new Response(response.body, {
1284
+ headers,
1285
+ status: response.status,
1286
+ statusText: response.statusText,
1287
+ });
1288
+ }
1289
+
1290
+ type AuthRoute = "start" | "continue" | "poll" | "abort" | "refresh";
376
1291
 
377
1292
  async function handleAuthFlow(
378
1293
  provider: ProviderDefinition,
379
1294
  request: AuthFlowRequest,
380
1295
  route: AuthRoute,
1296
+ options: ProviderServerOptions = {},
1297
+ signal?: AbortSignal,
381
1298
  ): Promise<Response | AuthFlowResponse> {
382
1299
  const flow = provider.auth?.flow;
383
1300
  if (!flow) {
@@ -386,22 +1303,52 @@ async function handleAuthFlow(
386
1303
  });
387
1304
  }
388
1305
 
389
- const { context, getPatch } = createAuthFlowContext(provider, request);
1306
+ const { context, getPatch } = createAuthFlowContext(
1307
+ provider,
1308
+ request,
1309
+ options,
1310
+ signal,
1311
+ );
1312
+ try {
1313
+ const result =
1314
+ route === "start"
1315
+ ? await flow.start(context)
1316
+ : route === "continue"
1317
+ ? await flow.continue(context, request.input ?? {})
1318
+ : route === "poll"
1319
+ ? flow.poll
1320
+ ? await flow.poll(context)
1321
+ : null
1322
+ : route === "abort"
1323
+ ? flow.abort
1324
+ ? await flow.abort(context)
1325
+ : null
1326
+ : flow.refresh
1327
+ ? await flow.refresh(context, request.input ?? {})
1328
+ : null;
390
1329
 
391
- const result =
392
- route === "start"
393
- ? await flow.start(context)
394
- : route === "continue"
395
- ? await flow.continue(context, request.input ?? {})
396
- : route === "poll"
397
- ? flow.poll
398
- ? await flow.poll(context)
399
- : null
400
- : flow.abort
401
- ? await flow.abort(context)
402
- : null;
1330
+ if (route === "refresh" && !flow.refresh) {
1331
+ throw new AuthError("Provider auth flow does not support refresh.", {
1332
+ code: "refresh_not_supported",
1333
+ });
1334
+ }
403
1335
 
404
- return toAuthFlowResponse(result, getPatch());
1336
+ const materializedResult =
1337
+ result &&
1338
+ !(result instanceof Response) &&
1339
+ !(result instanceof ReadableStream) &&
1340
+ isAuthTurn(result)
1341
+ ? materializeAuthFlowTurn(provider, request, result)
1342
+ : result;
1343
+ return toAuthFlowResponse(materializedResult, getPatch());
1344
+ } catch (error) {
1345
+ if (error instanceof AuthAbortError) {
1346
+ return toAuthFlowResponse(error.turn, getPatch());
1347
+ }
1348
+ throw error;
1349
+ } finally {
1350
+ context.stealth.close?.();
1351
+ }
405
1352
  }
406
1353
 
407
1354
  export function createServerApp(
@@ -410,6 +1357,12 @@ export function createServerApp(
410
1357
  ): Hono {
411
1358
  const app = new Hono();
412
1359
  const logger = options.logger ?? defaultProviderServerLogger;
1360
+ const state =
1361
+ options.state ??
1362
+ createProviderRuntimeStateFromEnv({
1363
+ providerId: provider.id,
1364
+ allowMemoryFallback: options.allowMemoryStateFallback === true,
1365
+ });
413
1366
 
414
1367
  app.notFound((c) =>
415
1368
  c.json(
@@ -434,6 +1387,8 @@ export function createServerApp(
434
1387
  app.post("/v1/:operation", async (c) => {
435
1388
  let rawBody: unknown;
436
1389
  const operation = c.req.param("operation");
1390
+ const proxyTelemetry = new ProxyTelemetryCollector();
1391
+ const requestCost = startRequestCost();
437
1392
  try {
438
1393
  rawBody = await c.req.raw
439
1394
  .clone()
@@ -442,8 +1397,38 @@ export function createServerApp(
442
1397
  const body = OperationRequestSchema.parse(rawBody);
443
1398
  const requestHeaders = Object.fromEntries(c.req.raw.headers.entries());
444
1399
  body.headers = { ...requestHeaders, ...body.headers };
445
- const response = await handleOperation(provider, body, operation);
446
- return response instanceof Response ? response : c.json(response);
1400
+ const response = await handleOperation(
1401
+ provider,
1402
+ body,
1403
+ operation,
1404
+ options,
1405
+ state,
1406
+ proxyTelemetry,
1407
+ );
1408
+ if (response instanceof Response) {
1409
+ logProviderSuccess(
1410
+ logger,
1411
+ provider,
1412
+ "operation",
1413
+ operation,
1414
+ body.requestId,
1415
+ response.status,
1416
+ finishRequestCost(requestCost),
1417
+ );
1418
+ return responseWithProviderTelemetry(response, proxyTelemetry);
1419
+ }
1420
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
1421
+ if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1422
+ logProviderSuccess(
1423
+ logger,
1424
+ provider,
1425
+ "operation",
1426
+ operation,
1427
+ body.requestId,
1428
+ 200,
1429
+ finishRequestCost(requestCost),
1430
+ );
1431
+ return c.json(response);
447
1432
  } catch (error) {
448
1433
  const status = toStatusCode(error);
449
1434
  const requestId = extractRequestId(rawBody);
@@ -455,20 +1440,42 @@ export function createServerApp(
455
1440
  requestId,
456
1441
  error,
457
1442
  status,
1443
+ finishRequestCost(requestCost),
458
1444
  );
1445
+ const telemetryHeader = proxyTelemetry.toHeaderValue();
1446
+ if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
459
1447
  return c.json(toErrorResponse(error, requestId), status);
460
1448
  }
461
1449
  });
462
1450
 
463
1451
  app.post("/auth/start", async (c) => {
464
1452
  let rawBody: unknown;
1453
+ const requestCost = startRequestCost();
465
1454
  try {
466
1455
  rawBody = await c.req.raw
467
1456
  .clone()
468
1457
  .json()
469
1458
  .catch(() => undefined);
470
- const body = AuthFlowRequestSchema.parse(rawBody);
471
- const response = await handleAuthFlow(provider, body, "start");
1459
+ const body = withAuthRequestHeaders(
1460
+ AuthFlowRequestSchema.parse(rawBody),
1461
+ c.req.raw.headers,
1462
+ );
1463
+ const response = await handleAuthFlow(
1464
+ provider,
1465
+ body,
1466
+ "start",
1467
+ options,
1468
+ c.req.raw.signal,
1469
+ );
1470
+ logProviderSuccess(
1471
+ logger,
1472
+ provider,
1473
+ "auth",
1474
+ "start",
1475
+ body.requestId,
1476
+ response instanceof Response ? response.status : 200,
1477
+ finishRequestCost(requestCost),
1478
+ );
472
1479
  return response instanceof Response ? response : c.json(response);
473
1480
  } catch (error) {
474
1481
  const status = toStatusCode(error);
@@ -481,6 +1488,7 @@ export function createServerApp(
481
1488
  requestId,
482
1489
  error,
483
1490
  status,
1491
+ finishRequestCost(requestCost),
484
1492
  );
485
1493
  return c.json(toErrorResponse(error, requestId), status);
486
1494
  }
@@ -488,13 +1496,32 @@ export function createServerApp(
488
1496
 
489
1497
  app.post("/auth/continue", async (c) => {
490
1498
  let rawBody: unknown;
1499
+ const requestCost = startRequestCost();
491
1500
  try {
492
1501
  rawBody = await c.req.raw
493
1502
  .clone()
494
1503
  .json()
495
1504
  .catch(() => undefined);
496
- const body = AuthFlowRequestSchema.parse(rawBody);
497
- const response = await handleAuthFlow(provider, body, "continue");
1505
+ const body = withAuthRequestHeaders(
1506
+ AuthFlowRequestSchema.parse(rawBody),
1507
+ c.req.raw.headers,
1508
+ );
1509
+ const response = await handleAuthFlow(
1510
+ provider,
1511
+ body,
1512
+ "continue",
1513
+ options,
1514
+ c.req.raw.signal,
1515
+ );
1516
+ logProviderSuccess(
1517
+ logger,
1518
+ provider,
1519
+ "auth",
1520
+ "continue",
1521
+ body.requestId,
1522
+ response instanceof Response ? response.status : 200,
1523
+ finishRequestCost(requestCost),
1524
+ );
498
1525
  return response instanceof Response ? response : c.json(response);
499
1526
  } catch (error) {
500
1527
  const status = toStatusCode(error);
@@ -507,6 +1534,7 @@ export function createServerApp(
507
1534
  requestId,
508
1535
  error,
509
1536
  status,
1537
+ finishRequestCost(requestCost),
510
1538
  );
511
1539
  return c.json(toErrorResponse(error, requestId), status);
512
1540
  }
@@ -514,13 +1542,32 @@ export function createServerApp(
514
1542
 
515
1543
  app.post("/auth/poll", async (c) => {
516
1544
  let rawBody: unknown;
1545
+ const requestCost = startRequestCost();
517
1546
  try {
518
1547
  rawBody = await c.req.raw
519
1548
  .clone()
520
1549
  .json()
521
1550
  .catch(() => undefined);
522
- const body = AuthFlowRequestSchema.parse(rawBody);
523
- const response = await handleAuthFlow(provider, body, "poll");
1551
+ const body = withAuthRequestHeaders(
1552
+ AuthFlowRequestSchema.parse(rawBody),
1553
+ c.req.raw.headers,
1554
+ );
1555
+ const response = await handleAuthFlow(
1556
+ provider,
1557
+ body,
1558
+ "poll",
1559
+ options,
1560
+ c.req.raw.signal,
1561
+ );
1562
+ logProviderSuccess(
1563
+ logger,
1564
+ provider,
1565
+ "auth",
1566
+ "poll",
1567
+ body.requestId,
1568
+ response instanceof Response ? response.status : 200,
1569
+ finishRequestCost(requestCost),
1570
+ );
524
1571
  return response instanceof Response ? response : c.json(response);
525
1572
  } catch (error) {
526
1573
  const status = toStatusCode(error);
@@ -533,6 +1580,53 @@ export function createServerApp(
533
1580
  requestId,
534
1581
  error,
535
1582
  status,
1583
+ finishRequestCost(requestCost),
1584
+ );
1585
+ return c.json(toErrorResponse(error, requestId), status);
1586
+ }
1587
+ });
1588
+
1589
+ app.post("/auth/refresh", async (c) => {
1590
+ let rawBody: unknown;
1591
+ const requestCost = startRequestCost();
1592
+ try {
1593
+ rawBody = await c.req.raw
1594
+ .clone()
1595
+ .json()
1596
+ .catch(() => undefined);
1597
+ const body = withAuthRequestHeaders(
1598
+ AuthFlowRequestSchema.parse(rawBody),
1599
+ c.req.raw.headers,
1600
+ );
1601
+ const response = await handleAuthFlow(
1602
+ provider,
1603
+ body,
1604
+ "refresh",
1605
+ options,
1606
+ c.req.raw.signal,
1607
+ );
1608
+ logProviderSuccess(
1609
+ logger,
1610
+ provider,
1611
+ "auth",
1612
+ "refresh",
1613
+ body.requestId,
1614
+ response instanceof Response ? response.status : 200,
1615
+ finishRequestCost(requestCost),
1616
+ );
1617
+ return response instanceof Response ? response : c.json(response);
1618
+ } catch (error) {
1619
+ const status = toStatusCode(error);
1620
+ const requestId = extractRequestId(rawBody);
1621
+ logProviderError(
1622
+ logger,
1623
+ provider,
1624
+ "auth",
1625
+ "refresh",
1626
+ requestId,
1627
+ error,
1628
+ status,
1629
+ finishRequestCost(requestCost),
536
1630
  );
537
1631
  return c.json(toErrorResponse(error, requestId), status);
538
1632
  }
@@ -540,13 +1634,32 @@ export function createServerApp(
540
1634
 
541
1635
  app.post("/auth/disconnect", async (c) => {
542
1636
  let rawBody: unknown;
1637
+ const requestCost = startRequestCost();
543
1638
  try {
544
1639
  rawBody = await c.req.raw
545
1640
  .clone()
546
1641
  .json()
547
1642
  .catch(() => undefined);
548
- const body = AuthFlowRequestSchema.parse(rawBody);
549
- const response = await handleAuthFlow(provider, body, "abort");
1643
+ const body = withAuthRequestHeaders(
1644
+ AuthFlowRequestSchema.parse(rawBody),
1645
+ c.req.raw.headers,
1646
+ );
1647
+ const response = await handleAuthFlow(
1648
+ provider,
1649
+ body,
1650
+ "abort",
1651
+ options,
1652
+ c.req.raw.signal,
1653
+ );
1654
+ logProviderSuccess(
1655
+ logger,
1656
+ provider,
1657
+ "auth",
1658
+ "disconnect",
1659
+ body.requestId,
1660
+ response instanceof Response ? response.status : 200,
1661
+ finishRequestCost(requestCost),
1662
+ );
550
1663
  return response instanceof Response ? response : c.json(response);
551
1664
  } catch (error) {
552
1665
  const status = toStatusCode(error);
@@ -559,6 +1672,7 @@ export function createServerApp(
559
1672
  requestId,
560
1673
  error,
561
1674
  status,
1675
+ finishRequestCost(requestCost),
562
1676
  );
563
1677
  return c.json(toErrorResponse(error, requestId), status);
564
1678
  }
@@ -613,7 +1727,10 @@ export async function serve(
613
1727
  );
614
1728
  }
615
1729
 
616
- const app = createServerApp(provider, { logger: options.logger });
1730
+ const app = createServerApp(provider, {
1731
+ logger: options.logger,
1732
+ stt: options.stt,
1733
+ });
617
1734
 
618
1735
  bunRuntime.serve({
619
1736
  port: options.port ?? DEFAULT_PORT,