@apifuse/provider-sdk 2.1.0-beta.0 → 2.1.0-beta.10

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