@apifuse/provider-sdk 2.1.0-beta.2 → 2.1.0-beta.4
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.
- package/AUTHORING.md +172 -8
- package/CHANGELOG.md +15 -1
- package/README.md +29 -15
- package/SUBMISSION.md +86 -0
- package/bin/apifuse-dev.ts +12 -5
- package/bin/apifuse-pack-check.ts +17 -2
- package/bin/apifuse-pack-smoke.ts +133 -6
- package/bin/apifuse-perf.ts +19 -15
- package/bin/apifuse-record.ts +41 -53
- package/bin/apifuse-submit-check.ts +1052 -0
- package/bin/apifuse.ts +1 -1
- package/package.json +19 -9
- package/src/choice-token.ts +164 -0
- package/src/cli/commands.ts +24 -3
- package/src/cli/create.ts +166 -51
- package/src/cli/templates/provider/README.md.tpl +66 -7
- package/src/cli/templates/provider/dev.ts.tpl +1 -1
- package/src/cli/templates/provider/domain/README.md.tpl +3 -0
- package/src/cli/templates/provider/index.ts.tpl +5 -47
- package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/src/cli/templates/provider/meta.ts.tpl +7 -0
- package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/src/cli/templates/provider/operations/ping.ts.tpl +23 -0
- package/src/cli/templates/provider/schemas/ping.ts.tpl +16 -0
- package/src/cli/templates/provider/start.ts.tpl +1 -1
- package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/src/config/loader.ts +1206 -9
- package/src/define.ts +1648 -43
- package/src/errors.ts +12 -0
- package/src/i18n/catalog.ts +121 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/keys.ts +64 -0
- package/src/index.ts +152 -8
- package/src/lint.ts +297 -42
- package/src/observability.ts +41 -0
- package/src/provider.ts +60 -3
- package/src/public-schema-field-lint.ts +237 -0
- package/src/runtime/auth-flow.ts +7 -0
- package/src/runtime/browser.ts +77 -21
- package/src/runtime/cache.ts +582 -0
- package/src/runtime/executor.ts +13 -1
- package/src/runtime/http.ts +939 -195
- package/src/runtime/insights.ts +11 -11
- package/src/runtime/instrumentation.ts +12 -4
- package/src/runtime/key-derivation.ts +1 -1
- package/src/runtime/keyring.ts +4 -3
- package/src/runtime/proxy-errors.ts +132 -0
- package/src/runtime/proxy-telemetry.ts +253 -0
- package/src/runtime/request-options.ts +66 -0
- package/src/runtime/state.ts +76 -0
- package/src/runtime/stealth.ts +1145 -0
- package/src/runtime/stt.ts +629 -0
- package/src/schema.ts +363 -1
- package/src/server/serve.ts +827 -60
- package/src/server/types.ts +35 -0
- package/src/stream.ts +210 -0
- package/src/testing/run.ts +17 -4
- package/src/types.ts +889 -50
- package/src/runtime/tls.ts +0 -434
- package/src/types/playwright-stealth.d.ts +0 -9
package/src/server/serve.ts
CHANGED
|
@@ -1,24 +1,56 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
|
|
4
3
|
import { ProviderError, TransportError } from "../errors";
|
|
4
|
+
import {
|
|
5
|
+
categoryForStatus,
|
|
6
|
+
isRetryableCategory,
|
|
7
|
+
PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
8
|
+
type ProviderErrorCategory,
|
|
9
|
+
} from "../observability";
|
|
5
10
|
import { createScratchpad } from "../runtime/auth-flow";
|
|
6
11
|
import { createBrowserClient } from "../runtime/browser";
|
|
12
|
+
import { createProviderCache } from "../runtime/cache";
|
|
7
13
|
import { createCredentialContext } from "../runtime/credential";
|
|
8
14
|
import { createEnvContext } from "../runtime/env";
|
|
9
15
|
import { executeOperation } from "../runtime/executor";
|
|
10
16
|
import { createHttpClient } from "../runtime/http";
|
|
17
|
+
import { wrapWithInstrumentation } from "../runtime/instrumentation";
|
|
11
18
|
import { getProviderBaseUrl } from "../runtime/provider";
|
|
12
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
PROXY_AUTH_IP_DENIED_CODE,
|
|
21
|
+
PROXY_EDGE_AUTH_REJECTED_CODE,
|
|
22
|
+
PROXY_POOL_EXHAUSTED_CODE,
|
|
23
|
+
} from "../runtime/proxy-errors";
|
|
24
|
+
import {
|
|
25
|
+
PROVIDER_TELEMETRY_HEADER,
|
|
26
|
+
ProxyTelemetryCollector,
|
|
27
|
+
} from "../runtime/proxy-telemetry";
|
|
28
|
+
import { createUnsupportedProviderRuntimeState } from "../runtime/state";
|
|
29
|
+
import { createStealthClient } from "../runtime/stealth";
|
|
30
|
+
import { createSttClientFromEnv } from "../runtime/stt";
|
|
13
31
|
import { createTraceContext } from "../runtime/trace";
|
|
32
|
+
import { parseSchema } from "../schema";
|
|
33
|
+
import { getStealthProfile } from "../stealth/profiles";
|
|
34
|
+
import {
|
|
35
|
+
APIFUSE_STREAM_DONE_EVENT,
|
|
36
|
+
APIFUSE_STREAM_ERROR_EVENT,
|
|
37
|
+
encodeSseEvent,
|
|
38
|
+
error as streamError,
|
|
39
|
+
} from "../stream";
|
|
14
40
|
import type {
|
|
15
41
|
AuthContext,
|
|
16
42
|
BrowserClient,
|
|
17
43
|
FlowContext,
|
|
18
44
|
FlowContextStore,
|
|
45
|
+
HttpRetrySummary,
|
|
46
|
+
OperationDefinition,
|
|
47
|
+
OperationHttpStreamTransport,
|
|
48
|
+
OperationSseTransport,
|
|
19
49
|
ProviderContext,
|
|
20
50
|
ProviderDefinition,
|
|
21
|
-
|
|
51
|
+
ProviderStreamEvent,
|
|
52
|
+
StealthClient,
|
|
53
|
+
SttContext,
|
|
22
54
|
} from "../types";
|
|
23
55
|
import {
|
|
24
56
|
type AuthFlowRequest,
|
|
@@ -34,6 +66,7 @@ import {
|
|
|
34
66
|
|
|
35
67
|
const DEFAULT_HOST = "0.0.0.0";
|
|
36
68
|
const DEFAULT_PORT = 3000;
|
|
69
|
+
const retryResponseMeta = new WeakMap<ProviderContext, HttpRetrySummary>();
|
|
37
70
|
|
|
38
71
|
function createAuthStub(): AuthContext {
|
|
39
72
|
return {
|
|
@@ -56,28 +89,85 @@ function createBrowserStub(): BrowserClient {
|
|
|
56
89
|
};
|
|
57
90
|
}
|
|
58
91
|
|
|
59
|
-
function
|
|
92
|
+
function createStealthStub(): StealthClient {
|
|
60
93
|
return {
|
|
61
94
|
async fetch() {
|
|
62
|
-
throw new ProviderError("
|
|
63
|
-
code: "
|
|
95
|
+
throw new ProviderError("Stealth runtime is not available", {
|
|
96
|
+
code: "STEALTH_RUNTIME_UNSUPPORTED",
|
|
64
97
|
});
|
|
65
98
|
},
|
|
66
99
|
createSession() {
|
|
67
|
-
throw new ProviderError("
|
|
68
|
-
code: "
|
|
100
|
+
throw new ProviderError("Stealth runtime is not available", {
|
|
101
|
+
code: "STEALTH_RUNTIME_UNSUPPORTED",
|
|
69
102
|
});
|
|
70
103
|
},
|
|
104
|
+
close() {
|
|
105
|
+
// no-op
|
|
106
|
+
},
|
|
71
107
|
};
|
|
72
108
|
}
|
|
73
109
|
|
|
110
|
+
function getProviderStealthBaseUrl(
|
|
111
|
+
provider: ProviderDefinition,
|
|
112
|
+
): string | undefined {
|
|
113
|
+
const baseUrl = getProviderBaseUrl(provider);
|
|
114
|
+
if (baseUrl) {
|
|
115
|
+
return baseUrl;
|
|
116
|
+
}
|
|
117
|
+
const firstHost = provider.allowedHosts?.[0];
|
|
118
|
+
return firstHost ? `https://${firstHost}` : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getProviderStealthProfile(provider: ProviderDefinition) {
|
|
122
|
+
return provider.stealth?.profile
|
|
123
|
+
? getStealthProfile(provider.stealth.profile)
|
|
124
|
+
: undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function resolveProviderProxyAffinityKey(
|
|
128
|
+
provider: ProviderDefinition,
|
|
129
|
+
request: OperationRequest,
|
|
130
|
+
operationId: string,
|
|
131
|
+
): string {
|
|
132
|
+
const connectionKey =
|
|
133
|
+
request.connection?.id ?? request.connection?.externalRef;
|
|
134
|
+
const affinity =
|
|
135
|
+
typeof provider.proxy === "object"
|
|
136
|
+
? provider.proxy.session?.affinity
|
|
137
|
+
: undefined;
|
|
138
|
+
if (affinity === "operation") {
|
|
139
|
+
return `${provider.id}/${operationId}`;
|
|
140
|
+
}
|
|
141
|
+
return connectionKey ?? provider.id;
|
|
142
|
+
}
|
|
143
|
+
|
|
74
144
|
function createProviderContext(
|
|
75
145
|
provider: ProviderDefinition,
|
|
76
146
|
request: OperationRequest,
|
|
147
|
+
operationId: string,
|
|
148
|
+
options: ProviderServerOptions = {},
|
|
149
|
+
proxyTelemetry?: ProxyTelemetryCollector,
|
|
77
150
|
): ProviderContext {
|
|
78
151
|
const baseUrl = getProviderBaseUrl(provider);
|
|
152
|
+
const stealthBaseUrl = getProviderStealthBaseUrl(provider);
|
|
153
|
+
const stealthProfile = getProviderStealthProfile(provider);
|
|
154
|
+
const proxyClientOptions = {
|
|
155
|
+
upstream: { proxy: provider.proxy },
|
|
156
|
+
affinityKey: resolveProviderProxyAffinityKey(
|
|
157
|
+
provider,
|
|
158
|
+
request,
|
|
159
|
+
operationId,
|
|
160
|
+
),
|
|
161
|
+
telemetry: proxyTelemetry,
|
|
162
|
+
};
|
|
163
|
+
let wrappedContext: ProviderContext | undefined;
|
|
164
|
+
const stealthClientOptions = {
|
|
165
|
+
upstream: proxyClientOptions.upstream,
|
|
166
|
+
affinityKey: proxyClientOptions.affinityKey,
|
|
167
|
+
telemetry: proxyTelemetry,
|
|
168
|
+
};
|
|
79
169
|
|
|
80
|
-
|
|
170
|
+
const context = wrapWithInstrumentation({
|
|
81
171
|
env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
|
|
82
172
|
credential: createCredentialContext({
|
|
83
173
|
allowedKeys: provider.credential?.keys,
|
|
@@ -89,13 +179,28 @@ function createProviderContext(
|
|
|
89
179
|
connectionId: request.connection?.id,
|
|
90
180
|
headers: request.headers ?? {},
|
|
91
181
|
},
|
|
92
|
-
http: createHttpClient(baseUrl
|
|
93
|
-
|
|
182
|
+
http: createHttpClient(baseUrl, {
|
|
183
|
+
...proxyClientOptions,
|
|
184
|
+
onRetrySummary: (summary) => {
|
|
185
|
+
if (summary.attempts <= 1 || !wrappedContext) return;
|
|
186
|
+
retryResponseMeta.set(wrappedContext, summary);
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
cache: createProviderCache({ providerId: provider.id }),
|
|
190
|
+
state: createUnsupportedProviderRuntimeState(),
|
|
191
|
+
stealth: stealthBaseUrl
|
|
192
|
+
? stealthProfile
|
|
193
|
+
? createStealthClient(
|
|
194
|
+
stealthBaseUrl,
|
|
195
|
+
stealthProfile.name,
|
|
196
|
+
stealthClientOptions,
|
|
197
|
+
)
|
|
198
|
+
: createStealthClient(stealthBaseUrl, stealthClientOptions)
|
|
199
|
+
: createStealthStub(),
|
|
94
200
|
browser:
|
|
95
201
|
provider.runtime === "browser"
|
|
96
202
|
? createBrowserClient({
|
|
97
|
-
cdpUrl:
|
|
98
|
-
process.env.CDP_POOL_URL ?? process.env.APIFUSE_CDP_POOL_URL,
|
|
203
|
+
cdpUrl: process.env.APIFUSE__CDP_POOL__URL,
|
|
99
204
|
headless: true,
|
|
100
205
|
stealth: true,
|
|
101
206
|
engine: provider.browser?.engine,
|
|
@@ -103,7 +208,10 @@ function createProviderContext(
|
|
|
103
208
|
: createBrowserStub(),
|
|
104
209
|
trace: createTraceContext(),
|
|
105
210
|
auth: createAuthStub(),
|
|
106
|
-
|
|
211
|
+
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
|
212
|
+
});
|
|
213
|
+
wrappedContext = context;
|
|
214
|
+
return context;
|
|
107
215
|
}
|
|
108
216
|
|
|
109
217
|
function createFlowContextStore(
|
|
@@ -145,16 +253,32 @@ function createFlowContextStore(
|
|
|
145
253
|
function createAuthFlowContext(
|
|
146
254
|
provider: ProviderDefinition,
|
|
147
255
|
request: AuthFlowRequest,
|
|
256
|
+
options: ProviderServerOptions = {},
|
|
148
257
|
): {
|
|
149
258
|
context: FlowContext;
|
|
150
259
|
getPatch: () => Record<string, unknown | null> | undefined;
|
|
151
260
|
} {
|
|
152
261
|
const baseUrl = getProviderBaseUrl(provider);
|
|
262
|
+
const stealthBaseUrl = getProviderStealthBaseUrl(provider);
|
|
263
|
+
const stealthProfile = getProviderStealthProfile(provider);
|
|
153
264
|
const contextData = request.context ?? {};
|
|
154
265
|
const flowContextStore = createFlowContextStore(
|
|
155
266
|
provider.context?.keys ?? Object.keys(contextData),
|
|
156
267
|
contextData,
|
|
157
268
|
);
|
|
269
|
+
const proxyClientOptions = {
|
|
270
|
+
upstream: { proxy: provider.proxy },
|
|
271
|
+
affinityKey:
|
|
272
|
+
request.connectionId ??
|
|
273
|
+
request.externalRef ??
|
|
274
|
+
request.tenantId ??
|
|
275
|
+
request.providerId ??
|
|
276
|
+
provider.id,
|
|
277
|
+
};
|
|
278
|
+
const stealthClientOptions = {
|
|
279
|
+
upstream: proxyClientOptions.upstream,
|
|
280
|
+
affinityKey: proxyClientOptions.affinityKey,
|
|
281
|
+
};
|
|
158
282
|
|
|
159
283
|
return {
|
|
160
284
|
context: {
|
|
@@ -162,39 +286,97 @@ function createAuthFlowContext(
|
|
|
162
286
|
externalRef: request.externalRef,
|
|
163
287
|
tenantId: request.tenantId ?? "",
|
|
164
288
|
providerId: request.providerId ?? provider.id,
|
|
165
|
-
http: createHttpClient(baseUrl),
|
|
289
|
+
http: createHttpClient(baseUrl, proxyClientOptions),
|
|
290
|
+
stealth: stealthBaseUrl
|
|
291
|
+
? stealthProfile
|
|
292
|
+
? createStealthClient(
|
|
293
|
+
stealthBaseUrl,
|
|
294
|
+
stealthProfile.name,
|
|
295
|
+
stealthClientOptions,
|
|
296
|
+
)
|
|
297
|
+
: createStealthClient(stealthBaseUrl, stealthClientOptions)
|
|
298
|
+
: createStealthStub(),
|
|
166
299
|
env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
|
|
167
300
|
context: flowContextStore.context,
|
|
301
|
+
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
|
168
302
|
},
|
|
169
303
|
getPatch: flowContextStore.getPatch,
|
|
170
304
|
};
|
|
171
305
|
}
|
|
172
306
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
307
|
+
type ProviderRequestCost = {
|
|
308
|
+
durationMs: number;
|
|
309
|
+
cpuUserMicros: number;
|
|
310
|
+
cpuSystemMicros: number;
|
|
311
|
+
cpuTotalMicros: number;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
type ProviderServerLogEventBase = ProviderRequestCost & {
|
|
176
315
|
providerId: string;
|
|
177
316
|
kind: "operation" | "auth";
|
|
178
317
|
route: string;
|
|
179
318
|
requestId?: string;
|
|
180
319
|
status: number;
|
|
181
|
-
code: string;
|
|
182
|
-
errorClass: string;
|
|
183
|
-
message: string;
|
|
184
|
-
upstreamStatus?: number;
|
|
185
|
-
issues?: Array<{ path: string; code: string; message: string }>;
|
|
186
320
|
};
|
|
187
321
|
|
|
322
|
+
export type ProviderServerLogEvent =
|
|
323
|
+
| (ProviderServerLogEventBase & {
|
|
324
|
+
level: "info";
|
|
325
|
+
event: "provider_request_completed";
|
|
326
|
+
})
|
|
327
|
+
| (ProviderServerLogEventBase & {
|
|
328
|
+
level: "warn" | "error";
|
|
329
|
+
event: "provider_request_failed";
|
|
330
|
+
code: string;
|
|
331
|
+
errorClass: string;
|
|
332
|
+
message: string;
|
|
333
|
+
upstreamStatus?: number;
|
|
334
|
+
errorCategory?: ProviderErrorCategory;
|
|
335
|
+
taxonomyVersion?: string;
|
|
336
|
+
retryable?: boolean;
|
|
337
|
+
issues?: Array<{ path: string; code: string; message: string }>;
|
|
338
|
+
});
|
|
339
|
+
|
|
188
340
|
export type ProviderServerLogger = (event: ProviderServerLogEvent) => void;
|
|
189
341
|
|
|
190
342
|
export type ProviderServerOptions = {
|
|
191
343
|
logger?: ProviderServerLogger;
|
|
344
|
+
/** Optional STT override for tests or custom hosts; local/prod normally resolves from env. */
|
|
345
|
+
stt?: SttContext;
|
|
192
346
|
};
|
|
193
347
|
|
|
194
348
|
const defaultProviderServerLogger: ProviderServerLogger = (event) => {
|
|
195
|
-
|
|
349
|
+
const line = JSON.stringify(event);
|
|
350
|
+
if (event.level === "info") {
|
|
351
|
+
console.log(line);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
console.error(line);
|
|
196
355
|
};
|
|
197
356
|
|
|
357
|
+
function startRequestCost(): {
|
|
358
|
+
startedAtMs: number;
|
|
359
|
+
cpuStart: NodeJS.CpuUsage;
|
|
360
|
+
} {
|
|
361
|
+
return {
|
|
362
|
+
startedAtMs: performance.now(),
|
|
363
|
+
cpuStart: process.cpuUsage(),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function finishRequestCost(input: {
|
|
368
|
+
startedAtMs: number;
|
|
369
|
+
cpuStart: NodeJS.CpuUsage;
|
|
370
|
+
}): ProviderRequestCost {
|
|
371
|
+
const cpuDelta = process.cpuUsage(input.cpuStart);
|
|
372
|
+
return {
|
|
373
|
+
durationMs: Math.max(0, Math.round(performance.now() - input.startedAtMs)),
|
|
374
|
+
cpuUserMicros: Math.max(0, cpuDelta.user),
|
|
375
|
+
cpuSystemMicros: Math.max(0, cpuDelta.system),
|
|
376
|
+
cpuTotalMicros: Math.max(0, cpuDelta.user + cpuDelta.system),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
198
380
|
function zodDetails(error: z.ZodError): Array<{
|
|
199
381
|
path: string;
|
|
200
382
|
code: string;
|
|
@@ -212,14 +394,14 @@ function toErrorResponse(
|
|
|
212
394
|
requestId?: string,
|
|
213
395
|
): OperationErrorResponse {
|
|
214
396
|
if (error instanceof ProviderError) {
|
|
397
|
+
const details = publicProviderErrorDetails(error);
|
|
215
398
|
return {
|
|
216
399
|
error: {
|
|
217
400
|
code: error.code ?? "provider_error",
|
|
218
401
|
message: publicProviderErrorMessage(error),
|
|
219
402
|
...(requestId ? { requestId } : {}),
|
|
220
|
-
...(error
|
|
221
|
-
|
|
222
|
-
: {}),
|
|
403
|
+
...(error.fix ? { fix: error.fix } : {}),
|
|
404
|
+
...(details ? { details } : {}),
|
|
223
405
|
},
|
|
224
406
|
};
|
|
225
407
|
}
|
|
@@ -244,8 +426,80 @@ function toErrorResponse(
|
|
|
244
426
|
};
|
|
245
427
|
}
|
|
246
428
|
|
|
429
|
+
function publicProviderErrorDetails(error: ProviderError): unknown {
|
|
430
|
+
const providerDetails = error.details;
|
|
431
|
+
const observabilityDetails = providerObservabilityDetails(error);
|
|
432
|
+
|
|
433
|
+
if (providerDetails === undefined) {
|
|
434
|
+
return observabilityDetails;
|
|
435
|
+
}
|
|
436
|
+
if (observabilityDetails === undefined) {
|
|
437
|
+
return providerDetails;
|
|
438
|
+
}
|
|
439
|
+
if (isPlainRecord(providerDetails) && isPlainRecord(observabilityDetails)) {
|
|
440
|
+
return { ...providerDetails, ...observabilityDetails };
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
provider: providerDetails,
|
|
444
|
+
observability: observabilityDetails,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
449
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function providerObservabilityDetails(error: ProviderError):
|
|
453
|
+
| {
|
|
454
|
+
category: ProviderErrorCategory;
|
|
455
|
+
taxonomyVersion: string;
|
|
456
|
+
retryable: boolean;
|
|
457
|
+
upstreamStatus?: number;
|
|
458
|
+
}
|
|
459
|
+
| undefined {
|
|
460
|
+
if (!(error instanceof TransportError)) {
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
const isProxyPoolCode =
|
|
464
|
+
error.code === PROXY_POOL_EXHAUSTED_CODE ||
|
|
465
|
+
error.code === PROXY_EDGE_AUTH_REJECTED_CODE ||
|
|
466
|
+
error.code === "PROXY_ALLOCATION_FAILED";
|
|
467
|
+
const category =
|
|
468
|
+
error.options?.category ??
|
|
469
|
+
(isProxyPoolCode
|
|
470
|
+
? "proxy_pool"
|
|
471
|
+
: error.code === PROXY_AUTH_IP_DENIED_CODE
|
|
472
|
+
? "anti_bot_blocked"
|
|
473
|
+
: error.code === "transport_timeout"
|
|
474
|
+
? "timeout"
|
|
475
|
+
: error.code === "transport_network_error"
|
|
476
|
+
? "network"
|
|
477
|
+
: error.upstreamStatus
|
|
478
|
+
? categoryForStatus(error.upstreamStatus)
|
|
479
|
+
: "upstream_http");
|
|
480
|
+
return {
|
|
481
|
+
category,
|
|
482
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
483
|
+
retryable:
|
|
484
|
+
error.options?.retryable ??
|
|
485
|
+
(category === "upstream_http" && error.upstreamStatus
|
|
486
|
+
? error.upstreamStatus >= 500
|
|
487
|
+
: isRetryableCategory(category)),
|
|
488
|
+
...(error.upstreamStatus ? { upstreamStatus: error.upstreamStatus } : {}),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
247
492
|
function publicProviderErrorMessage(error: ProviderError): string {
|
|
248
493
|
if (error instanceof TransportError) {
|
|
494
|
+
if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
|
|
495
|
+
return error.message;
|
|
496
|
+
}
|
|
497
|
+
if (error.code === PROXY_EDGE_AUTH_REJECTED_CODE) {
|
|
498
|
+
return error.message;
|
|
499
|
+
}
|
|
500
|
+
if (error.code === PROXY_POOL_EXHAUSTED_CODE) {
|
|
501
|
+
return error.message;
|
|
502
|
+
}
|
|
249
503
|
if (error.code === "transport_timeout") return "Request timed out";
|
|
250
504
|
if (error.code === "transport_network_error") return "Network error";
|
|
251
505
|
if (error.code === "upstream_http_error" && error.status) {
|
|
@@ -259,7 +513,7 @@ function publicProviderErrorMessage(error: ProviderError): string {
|
|
|
259
513
|
return error.message;
|
|
260
514
|
}
|
|
261
515
|
|
|
262
|
-
function toStatusCode(error: unknown): 400 | 404 | 500 | 502 | 504 {
|
|
516
|
+
function toStatusCode(error: unknown): 400 | 404 | 429 | 500 | 502 | 503 | 504 {
|
|
263
517
|
if (error instanceof z.ZodError) {
|
|
264
518
|
return 400;
|
|
265
519
|
}
|
|
@@ -269,8 +523,20 @@ function toStatusCode(error: unknown): 400 | 404 | 500 | 502 | 504 {
|
|
|
269
523
|
}
|
|
270
524
|
|
|
271
525
|
if (error instanceof ProviderError) {
|
|
272
|
-
|
|
273
|
-
|
|
526
|
+
switch (error.code) {
|
|
527
|
+
case "NOT_FOUND":
|
|
528
|
+
case "not_found":
|
|
529
|
+
case "NO_DATA":
|
|
530
|
+
return 404;
|
|
531
|
+
case "RATE_LIMITED":
|
|
532
|
+
case "UPSTREAM_RATE_LIMIT":
|
|
533
|
+
case "LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR":
|
|
534
|
+
return 429;
|
|
535
|
+
case "UPSTREAM_ERROR":
|
|
536
|
+
return 502;
|
|
537
|
+
case "STT_UNAVAILABLE":
|
|
538
|
+
case "UNSUPPORTED_STT_BACKEND":
|
|
539
|
+
return 503;
|
|
274
540
|
}
|
|
275
541
|
|
|
276
542
|
return 400;
|
|
@@ -296,6 +562,7 @@ function logProviderError(
|
|
|
296
562
|
requestId: string | undefined,
|
|
297
563
|
error: unknown,
|
|
298
564
|
status: number,
|
|
565
|
+
cost: ProviderRequestCost,
|
|
299
566
|
): void {
|
|
300
567
|
const code =
|
|
301
568
|
error instanceof ProviderError
|
|
@@ -305,6 +572,10 @@ function logProviderError(
|
|
|
305
572
|
: "internal_error";
|
|
306
573
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
307
574
|
const message = error instanceof Error ? error.message : String(error);
|
|
575
|
+
const details =
|
|
576
|
+
error instanceof ProviderError
|
|
577
|
+
? providerObservabilityDetails(error)
|
|
578
|
+
: undefined;
|
|
308
579
|
const emit =
|
|
309
580
|
typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
310
581
|
emit({
|
|
@@ -315,18 +586,50 @@ function logProviderError(
|
|
|
315
586
|
route,
|
|
316
587
|
...(requestId ? { requestId } : {}),
|
|
317
588
|
status,
|
|
589
|
+
...cost,
|
|
318
590
|
code,
|
|
319
591
|
errorClass,
|
|
320
592
|
message,
|
|
321
|
-
...(error instanceof TransportError && error.
|
|
322
|
-
? { upstreamStatus: error.
|
|
593
|
+
...(error instanceof TransportError && error.upstreamStatus
|
|
594
|
+
? { upstreamStatus: error.upstreamStatus }
|
|
595
|
+
: {}),
|
|
596
|
+
...(details
|
|
597
|
+
? {
|
|
598
|
+
errorCategory: details.category,
|
|
599
|
+
taxonomyVersion: details.taxonomyVersion,
|
|
600
|
+
retryable: details.retryable,
|
|
601
|
+
}
|
|
323
602
|
: {}),
|
|
324
603
|
...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
|
|
325
604
|
});
|
|
326
605
|
}
|
|
327
606
|
|
|
607
|
+
function logProviderSuccess(
|
|
608
|
+
logger: ProviderServerLogger | unknown,
|
|
609
|
+
provider: ProviderDefinition,
|
|
610
|
+
kind: "operation" | "auth",
|
|
611
|
+
route: string,
|
|
612
|
+
requestId: string | undefined,
|
|
613
|
+
status: number,
|
|
614
|
+
cost: ProviderRequestCost,
|
|
615
|
+
): void {
|
|
616
|
+
const emit =
|
|
617
|
+
typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
618
|
+
emit({
|
|
619
|
+
level: "info",
|
|
620
|
+
event: "provider_request_completed",
|
|
621
|
+
providerId: provider.id,
|
|
622
|
+
kind,
|
|
623
|
+
route,
|
|
624
|
+
...(requestId ? { requestId } : {}),
|
|
625
|
+
status,
|
|
626
|
+
...cost,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
328
630
|
function toJsonSuccessResponse(
|
|
329
631
|
result: unknown,
|
|
632
|
+
ctx?: ProviderContext,
|
|
330
633
|
): Response | OperationSuccessResponse {
|
|
331
634
|
if (result instanceof Response) {
|
|
332
635
|
return result;
|
|
@@ -336,7 +639,335 @@ function toJsonSuccessResponse(
|
|
|
336
639
|
return new Response(result);
|
|
337
640
|
}
|
|
338
641
|
|
|
339
|
-
|
|
642
|
+
const cacheMeta = ctx?.cache.responseMeta();
|
|
643
|
+
const retryMeta = ctx ? retryResponseMeta.get(ctx) : undefined;
|
|
644
|
+
const meta =
|
|
645
|
+
cacheMeta || retryMeta
|
|
646
|
+
? {
|
|
647
|
+
...(cacheMeta
|
|
648
|
+
? {
|
|
649
|
+
cached: cacheMeta.hit,
|
|
650
|
+
stale: cacheMeta.stale,
|
|
651
|
+
cache: cacheMeta,
|
|
652
|
+
}
|
|
653
|
+
: {}),
|
|
654
|
+
...(retryMeta ? { retry: retryMeta } : {}),
|
|
655
|
+
}
|
|
656
|
+
: undefined;
|
|
657
|
+
return {
|
|
658
|
+
data: result,
|
|
659
|
+
...(meta ? { meta } : {}),
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function isAsyncIterable<T = unknown>(
|
|
664
|
+
value: unknown,
|
|
665
|
+
): value is AsyncIterable<T> {
|
|
666
|
+
if (!value || typeof value !== "object") return false;
|
|
667
|
+
const iterator = Reflect.get(value, Symbol.asyncIterator);
|
|
668
|
+
return typeof iterator === "function";
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function responseWithCleanup(
|
|
672
|
+
response: Response,
|
|
673
|
+
cleanup: () => void,
|
|
674
|
+
): Response {
|
|
675
|
+
if (!response.body) {
|
|
676
|
+
cleanup();
|
|
677
|
+
return response;
|
|
678
|
+
}
|
|
679
|
+
const reader = response.body.getReader();
|
|
680
|
+
let cleaned = false;
|
|
681
|
+
const runCleanup = () => {
|
|
682
|
+
if (cleaned) return;
|
|
683
|
+
cleaned = true;
|
|
684
|
+
cleanup();
|
|
685
|
+
};
|
|
686
|
+
const body = new ReadableStream<Uint8Array>({
|
|
687
|
+
async pull(controller) {
|
|
688
|
+
try {
|
|
689
|
+
const { done, value } = await reader.read();
|
|
690
|
+
if (done) {
|
|
691
|
+
controller.close();
|
|
692
|
+
runCleanup();
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (value) controller.enqueue(value);
|
|
696
|
+
} catch (error) {
|
|
697
|
+
runCleanup();
|
|
698
|
+
controller.error(error);
|
|
699
|
+
}
|
|
700
|
+
},
|
|
701
|
+
async cancel(reason) {
|
|
702
|
+
try {
|
|
703
|
+
await reader.cancel(reason);
|
|
704
|
+
} finally {
|
|
705
|
+
runCleanup();
|
|
706
|
+
}
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
return new Response(body, {
|
|
710
|
+
headers: response.headers,
|
|
711
|
+
status: response.status,
|
|
712
|
+
statusText: response.statusText,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function validateSseEvent(
|
|
717
|
+
operation: OperationDefinition,
|
|
718
|
+
event: ProviderStreamEvent,
|
|
719
|
+
): Promise<ProviderStreamEvent> {
|
|
720
|
+
const transport = getSseTransport(operation);
|
|
721
|
+
const schema = transport?.events?.[event.event];
|
|
722
|
+
if (!schema) {
|
|
723
|
+
if (
|
|
724
|
+
event.event === APIFUSE_STREAM_ERROR_EVENT ||
|
|
725
|
+
event.event === APIFUSE_STREAM_DONE_EVENT
|
|
726
|
+
) {
|
|
727
|
+
return event;
|
|
728
|
+
}
|
|
729
|
+
throw new ProviderError(
|
|
730
|
+
`SSE event "${event.event}" is not declared in operation transport.events.`,
|
|
731
|
+
{
|
|
732
|
+
code: "SSE_EVENT_UNDECLARED",
|
|
733
|
+
category: "output_validation",
|
|
734
|
+
retryable: false,
|
|
735
|
+
fix: `Add "${event.event}" to transport.events or stop emitting that event.`,
|
|
736
|
+
},
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
const data = await parseSchema(
|
|
740
|
+
schema,
|
|
741
|
+
event.data,
|
|
742
|
+
`transport.events.${event.event}`,
|
|
743
|
+
);
|
|
744
|
+
return { ...event, data };
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function byteLength(value: Uint8Array | string): number {
|
|
748
|
+
if (typeof value === "string") {
|
|
749
|
+
return new TextEncoder().encode(value).byteLength;
|
|
750
|
+
}
|
|
751
|
+
return value.byteLength;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function assertStreamPayloadWithinLimit(
|
|
755
|
+
actualBytes: number,
|
|
756
|
+
maxBytes: number | undefined,
|
|
757
|
+
kind: "event" | "chunk",
|
|
758
|
+
): void {
|
|
759
|
+
if (maxBytes === undefined || actualBytes <= maxBytes) return;
|
|
760
|
+
throw new ProviderError(
|
|
761
|
+
`Stream ${kind} exceeded declared byte limit (${actualBytes} > ${maxBytes}).`,
|
|
762
|
+
{
|
|
763
|
+
code:
|
|
764
|
+
kind === "event" ? "STREAM_EVENT_TOO_LARGE" : "STREAM_CHUNK_TOO_LARGE",
|
|
765
|
+
retryable: false,
|
|
766
|
+
category: "input_validation",
|
|
767
|
+
fix:
|
|
768
|
+
kind === "event"
|
|
769
|
+
? "Emit smaller SSE events or increase transport.maxEventBytes."
|
|
770
|
+
: "Emit smaller stream chunks or increase transport.maxChunkBytes.",
|
|
771
|
+
},
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function toSseResponse(
|
|
776
|
+
operation: OperationDefinition,
|
|
777
|
+
result: AsyncIterable<ProviderStreamEvent>,
|
|
778
|
+
cleanup: () => void,
|
|
779
|
+
requestId?: string,
|
|
780
|
+
): Response {
|
|
781
|
+
const encoder = new TextEncoder();
|
|
782
|
+
const iterator = result[Symbol.asyncIterator]();
|
|
783
|
+
const transport = getSseTransport(operation);
|
|
784
|
+
let done = false;
|
|
785
|
+
let cleaned = false;
|
|
786
|
+
const runCleanup = () => {
|
|
787
|
+
if (cleaned) return;
|
|
788
|
+
cleaned = true;
|
|
789
|
+
cleanup();
|
|
790
|
+
};
|
|
791
|
+
const body = new ReadableStream<Uint8Array>({
|
|
792
|
+
async pull(controller) {
|
|
793
|
+
try {
|
|
794
|
+
if (done) {
|
|
795
|
+
controller.close();
|
|
796
|
+
runCleanup();
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const next = await iterator.next();
|
|
800
|
+
if (next.done) {
|
|
801
|
+
done = true;
|
|
802
|
+
controller.close();
|
|
803
|
+
runCleanup();
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const validated = await validateSseEvent(operation, next.value);
|
|
807
|
+
const encodedEvent = encodeSseEvent(validated);
|
|
808
|
+
const encodedBytes = encoder.encode(encodedEvent);
|
|
809
|
+
assertStreamPayloadWithinLimit(
|
|
810
|
+
encodedBytes.byteLength,
|
|
811
|
+
transport?.maxEventBytes,
|
|
812
|
+
"event",
|
|
813
|
+
);
|
|
814
|
+
controller.enqueue(encodedBytes);
|
|
815
|
+
} catch (error) {
|
|
816
|
+
const message =
|
|
817
|
+
error instanceof Error ? error.message : "Stream failed";
|
|
818
|
+
controller.enqueue(
|
|
819
|
+
encoder.encode(
|
|
820
|
+
encodeSseEvent(
|
|
821
|
+
streamError("stream_error", message, {
|
|
822
|
+
...(requestId ? { requestId } : {}),
|
|
823
|
+
}),
|
|
824
|
+
),
|
|
825
|
+
),
|
|
826
|
+
);
|
|
827
|
+
controller.close();
|
|
828
|
+
done = true;
|
|
829
|
+
runCleanup();
|
|
830
|
+
}
|
|
831
|
+
},
|
|
832
|
+
async cancel(reason) {
|
|
833
|
+
try {
|
|
834
|
+
await iterator.return?.(reason);
|
|
835
|
+
} finally {
|
|
836
|
+
runCleanup();
|
|
837
|
+
}
|
|
838
|
+
},
|
|
839
|
+
});
|
|
840
|
+
return new Response(body, {
|
|
841
|
+
headers: {
|
|
842
|
+
"Cache-Control": "no-cache, no-transform",
|
|
843
|
+
Connection: "keep-alive",
|
|
844
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function enforceStreamChunkLimit(
|
|
850
|
+
body: ReadableStream<Uint8Array>,
|
|
851
|
+
maxChunkBytes: number | undefined,
|
|
852
|
+
): ReadableStream<Uint8Array> {
|
|
853
|
+
if (maxChunkBytes === undefined) return body;
|
|
854
|
+
const reader = body.getReader();
|
|
855
|
+
return new ReadableStream<Uint8Array>({
|
|
856
|
+
async pull(controller) {
|
|
857
|
+
try {
|
|
858
|
+
const { done, value } = await reader.read();
|
|
859
|
+
if (done) {
|
|
860
|
+
controller.close();
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if (value) {
|
|
864
|
+
assertStreamPayloadWithinLimit(
|
|
865
|
+
byteLength(value),
|
|
866
|
+
maxChunkBytes,
|
|
867
|
+
"chunk",
|
|
868
|
+
);
|
|
869
|
+
controller.enqueue(value);
|
|
870
|
+
}
|
|
871
|
+
} catch (error) {
|
|
872
|
+
controller.error(error);
|
|
873
|
+
}
|
|
874
|
+
},
|
|
875
|
+
cancel(reason) {
|
|
876
|
+
return reader.cancel(reason);
|
|
877
|
+
},
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function toStreamingResponse(
|
|
882
|
+
operation: OperationDefinition,
|
|
883
|
+
result: unknown,
|
|
884
|
+
cleanup: () => void,
|
|
885
|
+
requestId?: string,
|
|
886
|
+
): Response {
|
|
887
|
+
const transport = operation.transport?.kind ?? "json";
|
|
888
|
+
if (
|
|
889
|
+
transport === "sse" &&
|
|
890
|
+
(result instanceof Response || result instanceof ReadableStream)
|
|
891
|
+
) {
|
|
892
|
+
cleanup();
|
|
893
|
+
throw new ProviderError(
|
|
894
|
+
"SSE operations must return an AsyncIterable of typed stream.event(...) values.",
|
|
895
|
+
{
|
|
896
|
+
code: "SSE_RESULT_UNSUPPORTED",
|
|
897
|
+
category: "output_validation",
|
|
898
|
+
retryable: false,
|
|
899
|
+
fix: "Return an async generator that yields stream.event(name, data) so APIFuse can validate event schemas and enforce event byte limits.",
|
|
900
|
+
},
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
if (result instanceof Response) {
|
|
904
|
+
const httpTransport = getHttpStreamTransport(operation);
|
|
905
|
+
if (
|
|
906
|
+
httpTransport &&
|
|
907
|
+
result.body &&
|
|
908
|
+
httpTransport?.maxChunkBytes !== undefined
|
|
909
|
+
) {
|
|
910
|
+
return responseWithCleanup(
|
|
911
|
+
new Response(
|
|
912
|
+
enforceStreamChunkLimit(result.body, httpTransport.maxChunkBytes),
|
|
913
|
+
{
|
|
914
|
+
headers: result.headers,
|
|
915
|
+
status: result.status,
|
|
916
|
+
statusText: result.statusText,
|
|
917
|
+
},
|
|
918
|
+
),
|
|
919
|
+
cleanup,
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
return responseWithCleanup(result, cleanup);
|
|
923
|
+
}
|
|
924
|
+
if (result instanceof ReadableStream) {
|
|
925
|
+
const httpTransport = getHttpStreamTransport(operation);
|
|
926
|
+
const stream =
|
|
927
|
+
httpTransport !== undefined
|
|
928
|
+
? enforceStreamChunkLimit(result, httpTransport.maxChunkBytes)
|
|
929
|
+
: result;
|
|
930
|
+
return responseWithCleanup(
|
|
931
|
+
new Response(stream, {
|
|
932
|
+
headers:
|
|
933
|
+
transport === "sse"
|
|
934
|
+
? { "Content-Type": "text/event-stream; charset=utf-8" }
|
|
935
|
+
: {
|
|
936
|
+
"Content-Type":
|
|
937
|
+
operation.transport?.kind === "http-stream"
|
|
938
|
+
? (operation.transport.contentType ??
|
|
939
|
+
"application/octet-stream")
|
|
940
|
+
: "application/octet-stream",
|
|
941
|
+
},
|
|
942
|
+
}),
|
|
943
|
+
cleanup,
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
if (transport === "sse" && isAsyncIterable<ProviderStreamEvent>(result)) {
|
|
947
|
+
return toSseResponse(operation, result, cleanup, requestId);
|
|
948
|
+
}
|
|
949
|
+
cleanup();
|
|
950
|
+
throw new ProviderError(
|
|
951
|
+
`Streaming operation returned unsupported result for transport "${transport}"`,
|
|
952
|
+
{
|
|
953
|
+
code: "STREAM_RESULT_UNSUPPORTED",
|
|
954
|
+
fix: "Return an AsyncIterable of stream.event(...) values, a ReadableStream, or a Response from streaming operations.",
|
|
955
|
+
},
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function getSseTransport(
|
|
960
|
+
operation: OperationDefinition,
|
|
961
|
+
): OperationSseTransport | undefined {
|
|
962
|
+
return operation.transport?.kind === "sse" ? operation.transport : undefined;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function getHttpStreamTransport(
|
|
966
|
+
operation: OperationDefinition,
|
|
967
|
+
): OperationHttpStreamTransport | undefined {
|
|
968
|
+
return operation.transport?.kind === "http-stream"
|
|
969
|
+
? operation.transport
|
|
970
|
+
: undefined;
|
|
340
971
|
}
|
|
341
972
|
|
|
342
973
|
function toAuthFlowResponse(
|
|
@@ -361,15 +992,57 @@ async function handleOperation(
|
|
|
361
992
|
provider: ProviderDefinition,
|
|
362
993
|
request: OperationRequest,
|
|
363
994
|
operationId: string,
|
|
995
|
+
options: ProviderServerOptions = {},
|
|
996
|
+
proxyTelemetry?: ProxyTelemetryCollector,
|
|
364
997
|
): Promise<Response | OperationResponse> {
|
|
365
|
-
const ctx = createProviderContext(
|
|
366
|
-
const result = await executeOperation(
|
|
998
|
+
const ctx = createProviderContext(
|
|
367
999
|
provider,
|
|
1000
|
+
request,
|
|
368
1001
|
operationId,
|
|
369
|
-
|
|
370
|
-
|
|
1002
|
+
options,
|
|
1003
|
+
proxyTelemetry,
|
|
371
1004
|
);
|
|
372
|
-
|
|
1005
|
+
const operation = provider.operations[operationId];
|
|
1006
|
+
const streaming =
|
|
1007
|
+
operation?.transport?.kind && operation.transport.kind !== "json";
|
|
1008
|
+
let cleanupCalled = false;
|
|
1009
|
+
const cleanup = () => {
|
|
1010
|
+
if (cleanupCalled) return;
|
|
1011
|
+
cleanupCalled = true;
|
|
1012
|
+
ctx.stealth.close?.();
|
|
1013
|
+
};
|
|
1014
|
+
try {
|
|
1015
|
+
const result = await executeOperation(
|
|
1016
|
+
provider,
|
|
1017
|
+
operationId,
|
|
1018
|
+
ctx,
|
|
1019
|
+
request.input,
|
|
1020
|
+
);
|
|
1021
|
+
if (streaming && operation) {
|
|
1022
|
+
return toStreamingResponse(operation, result, cleanup, request.requestId);
|
|
1023
|
+
}
|
|
1024
|
+
return toJsonSuccessResponse(result, ctx);
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
cleanup();
|
|
1027
|
+
throw error;
|
|
1028
|
+
} finally {
|
|
1029
|
+
if (!streaming) cleanup();
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function responseWithProviderTelemetry(
|
|
1034
|
+
response: Response,
|
|
1035
|
+
proxyTelemetry?: ProxyTelemetryCollector,
|
|
1036
|
+
): Response {
|
|
1037
|
+
const headerValue = proxyTelemetry?.toHeaderValue();
|
|
1038
|
+
const headers = new Headers(response.headers);
|
|
1039
|
+
headers.delete(PROVIDER_TELEMETRY_HEADER);
|
|
1040
|
+
if (headerValue) headers.set(PROVIDER_TELEMETRY_HEADER, headerValue);
|
|
1041
|
+
return new Response(response.body, {
|
|
1042
|
+
headers,
|
|
1043
|
+
status: response.status,
|
|
1044
|
+
statusText: response.statusText,
|
|
1045
|
+
});
|
|
373
1046
|
}
|
|
374
1047
|
|
|
375
1048
|
type AuthRoute = "start" | "continue" | "poll" | "abort";
|
|
@@ -378,6 +1051,7 @@ async function handleAuthFlow(
|
|
|
378
1051
|
provider: ProviderDefinition,
|
|
379
1052
|
request: AuthFlowRequest,
|
|
380
1053
|
route: AuthRoute,
|
|
1054
|
+
options: ProviderServerOptions = {},
|
|
381
1055
|
): Promise<Response | AuthFlowResponse> {
|
|
382
1056
|
const flow = provider.auth?.flow;
|
|
383
1057
|
if (!flow) {
|
|
@@ -386,22 +1060,29 @@ async function handleAuthFlow(
|
|
|
386
1060
|
});
|
|
387
1061
|
}
|
|
388
1062
|
|
|
389
|
-
const { context, getPatch } = createAuthFlowContext(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
:
|
|
401
|
-
?
|
|
402
|
-
|
|
1063
|
+
const { context, getPatch } = createAuthFlowContext(
|
|
1064
|
+
provider,
|
|
1065
|
+
request,
|
|
1066
|
+
options,
|
|
1067
|
+
);
|
|
1068
|
+
try {
|
|
1069
|
+
const result =
|
|
1070
|
+
route === "start"
|
|
1071
|
+
? await flow.start(context)
|
|
1072
|
+
: route === "continue"
|
|
1073
|
+
? await flow.continue(context, request.input ?? {})
|
|
1074
|
+
: route === "poll"
|
|
1075
|
+
? flow.poll
|
|
1076
|
+
? await flow.poll(context)
|
|
1077
|
+
: null
|
|
1078
|
+
: flow.abort
|
|
1079
|
+
? await flow.abort(context)
|
|
1080
|
+
: null;
|
|
403
1081
|
|
|
404
|
-
|
|
1082
|
+
return toAuthFlowResponse(result, getPatch());
|
|
1083
|
+
} finally {
|
|
1084
|
+
context.stealth.close?.();
|
|
1085
|
+
}
|
|
405
1086
|
}
|
|
406
1087
|
|
|
407
1088
|
export function createServerApp(
|
|
@@ -434,6 +1115,8 @@ export function createServerApp(
|
|
|
434
1115
|
app.post("/v1/:operation", async (c) => {
|
|
435
1116
|
let rawBody: unknown;
|
|
436
1117
|
const operation = c.req.param("operation");
|
|
1118
|
+
const proxyTelemetry = new ProxyTelemetryCollector();
|
|
1119
|
+
const requestCost = startRequestCost();
|
|
437
1120
|
try {
|
|
438
1121
|
rawBody = await c.req.raw
|
|
439
1122
|
.clone()
|
|
@@ -442,8 +1125,37 @@ export function createServerApp(
|
|
|
442
1125
|
const body = OperationRequestSchema.parse(rawBody);
|
|
443
1126
|
const requestHeaders = Object.fromEntries(c.req.raw.headers.entries());
|
|
444
1127
|
body.headers = { ...requestHeaders, ...body.headers };
|
|
445
|
-
const response = await handleOperation(
|
|
446
|
-
|
|
1128
|
+
const response = await handleOperation(
|
|
1129
|
+
provider,
|
|
1130
|
+
body,
|
|
1131
|
+
operation,
|
|
1132
|
+
options,
|
|
1133
|
+
proxyTelemetry,
|
|
1134
|
+
);
|
|
1135
|
+
if (response instanceof Response) {
|
|
1136
|
+
logProviderSuccess(
|
|
1137
|
+
logger,
|
|
1138
|
+
provider,
|
|
1139
|
+
"operation",
|
|
1140
|
+
operation,
|
|
1141
|
+
body.requestId,
|
|
1142
|
+
response.status,
|
|
1143
|
+
finishRequestCost(requestCost),
|
|
1144
|
+
);
|
|
1145
|
+
return responseWithProviderTelemetry(response, proxyTelemetry);
|
|
1146
|
+
}
|
|
1147
|
+
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1148
|
+
if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1149
|
+
logProviderSuccess(
|
|
1150
|
+
logger,
|
|
1151
|
+
provider,
|
|
1152
|
+
"operation",
|
|
1153
|
+
operation,
|
|
1154
|
+
body.requestId,
|
|
1155
|
+
200,
|
|
1156
|
+
finishRequestCost(requestCost),
|
|
1157
|
+
);
|
|
1158
|
+
return c.json(response);
|
|
447
1159
|
} catch (error) {
|
|
448
1160
|
const status = toStatusCode(error);
|
|
449
1161
|
const requestId = extractRequestId(rawBody);
|
|
@@ -455,20 +1167,33 @@ export function createServerApp(
|
|
|
455
1167
|
requestId,
|
|
456
1168
|
error,
|
|
457
1169
|
status,
|
|
1170
|
+
finishRequestCost(requestCost),
|
|
458
1171
|
);
|
|
1172
|
+
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1173
|
+
if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
459
1174
|
return c.json(toErrorResponse(error, requestId), status);
|
|
460
1175
|
}
|
|
461
1176
|
});
|
|
462
1177
|
|
|
463
1178
|
app.post("/auth/start", async (c) => {
|
|
464
1179
|
let rawBody: unknown;
|
|
1180
|
+
const requestCost = startRequestCost();
|
|
465
1181
|
try {
|
|
466
1182
|
rawBody = await c.req.raw
|
|
467
1183
|
.clone()
|
|
468
1184
|
.json()
|
|
469
1185
|
.catch(() => undefined);
|
|
470
1186
|
const body = AuthFlowRequestSchema.parse(rawBody);
|
|
471
|
-
const response = await handleAuthFlow(provider, body, "start");
|
|
1187
|
+
const response = await handleAuthFlow(provider, body, "start", options);
|
|
1188
|
+
logProviderSuccess(
|
|
1189
|
+
logger,
|
|
1190
|
+
provider,
|
|
1191
|
+
"auth",
|
|
1192
|
+
"start",
|
|
1193
|
+
body.requestId,
|
|
1194
|
+
response instanceof Response ? response.status : 200,
|
|
1195
|
+
finishRequestCost(requestCost),
|
|
1196
|
+
);
|
|
472
1197
|
return response instanceof Response ? response : c.json(response);
|
|
473
1198
|
} catch (error) {
|
|
474
1199
|
const status = toStatusCode(error);
|
|
@@ -481,6 +1206,7 @@ export function createServerApp(
|
|
|
481
1206
|
requestId,
|
|
482
1207
|
error,
|
|
483
1208
|
status,
|
|
1209
|
+
finishRequestCost(requestCost),
|
|
484
1210
|
);
|
|
485
1211
|
return c.json(toErrorResponse(error, requestId), status);
|
|
486
1212
|
}
|
|
@@ -488,13 +1214,28 @@ export function createServerApp(
|
|
|
488
1214
|
|
|
489
1215
|
app.post("/auth/continue", async (c) => {
|
|
490
1216
|
let rawBody: unknown;
|
|
1217
|
+
const requestCost = startRequestCost();
|
|
491
1218
|
try {
|
|
492
1219
|
rawBody = await c.req.raw
|
|
493
1220
|
.clone()
|
|
494
1221
|
.json()
|
|
495
1222
|
.catch(() => undefined);
|
|
496
1223
|
const body = AuthFlowRequestSchema.parse(rawBody);
|
|
497
|
-
const response = await handleAuthFlow(
|
|
1224
|
+
const response = await handleAuthFlow(
|
|
1225
|
+
provider,
|
|
1226
|
+
body,
|
|
1227
|
+
"continue",
|
|
1228
|
+
options,
|
|
1229
|
+
);
|
|
1230
|
+
logProviderSuccess(
|
|
1231
|
+
logger,
|
|
1232
|
+
provider,
|
|
1233
|
+
"auth",
|
|
1234
|
+
"continue",
|
|
1235
|
+
body.requestId,
|
|
1236
|
+
response instanceof Response ? response.status : 200,
|
|
1237
|
+
finishRequestCost(requestCost),
|
|
1238
|
+
);
|
|
498
1239
|
return response instanceof Response ? response : c.json(response);
|
|
499
1240
|
} catch (error) {
|
|
500
1241
|
const status = toStatusCode(error);
|
|
@@ -507,6 +1248,7 @@ export function createServerApp(
|
|
|
507
1248
|
requestId,
|
|
508
1249
|
error,
|
|
509
1250
|
status,
|
|
1251
|
+
finishRequestCost(requestCost),
|
|
510
1252
|
);
|
|
511
1253
|
return c.json(toErrorResponse(error, requestId), status);
|
|
512
1254
|
}
|
|
@@ -514,13 +1256,23 @@ export function createServerApp(
|
|
|
514
1256
|
|
|
515
1257
|
app.post("/auth/poll", async (c) => {
|
|
516
1258
|
let rawBody: unknown;
|
|
1259
|
+
const requestCost = startRequestCost();
|
|
517
1260
|
try {
|
|
518
1261
|
rawBody = await c.req.raw
|
|
519
1262
|
.clone()
|
|
520
1263
|
.json()
|
|
521
1264
|
.catch(() => undefined);
|
|
522
1265
|
const body = AuthFlowRequestSchema.parse(rawBody);
|
|
523
|
-
const response = await handleAuthFlow(provider, body, "poll");
|
|
1266
|
+
const response = await handleAuthFlow(provider, body, "poll", options);
|
|
1267
|
+
logProviderSuccess(
|
|
1268
|
+
logger,
|
|
1269
|
+
provider,
|
|
1270
|
+
"auth",
|
|
1271
|
+
"poll",
|
|
1272
|
+
body.requestId,
|
|
1273
|
+
response instanceof Response ? response.status : 200,
|
|
1274
|
+
finishRequestCost(requestCost),
|
|
1275
|
+
);
|
|
524
1276
|
return response instanceof Response ? response : c.json(response);
|
|
525
1277
|
} catch (error) {
|
|
526
1278
|
const status = toStatusCode(error);
|
|
@@ -533,6 +1285,7 @@ export function createServerApp(
|
|
|
533
1285
|
requestId,
|
|
534
1286
|
error,
|
|
535
1287
|
status,
|
|
1288
|
+
finishRequestCost(requestCost),
|
|
536
1289
|
);
|
|
537
1290
|
return c.json(toErrorResponse(error, requestId), status);
|
|
538
1291
|
}
|
|
@@ -540,13 +1293,23 @@ export function createServerApp(
|
|
|
540
1293
|
|
|
541
1294
|
app.post("/auth/disconnect", async (c) => {
|
|
542
1295
|
let rawBody: unknown;
|
|
1296
|
+
const requestCost = startRequestCost();
|
|
543
1297
|
try {
|
|
544
1298
|
rawBody = await c.req.raw
|
|
545
1299
|
.clone()
|
|
546
1300
|
.json()
|
|
547
1301
|
.catch(() => undefined);
|
|
548
1302
|
const body = AuthFlowRequestSchema.parse(rawBody);
|
|
549
|
-
const response = await handleAuthFlow(provider, body, "abort");
|
|
1303
|
+
const response = await handleAuthFlow(provider, body, "abort", options);
|
|
1304
|
+
logProviderSuccess(
|
|
1305
|
+
logger,
|
|
1306
|
+
provider,
|
|
1307
|
+
"auth",
|
|
1308
|
+
"disconnect",
|
|
1309
|
+
body.requestId,
|
|
1310
|
+
response instanceof Response ? response.status : 200,
|
|
1311
|
+
finishRequestCost(requestCost),
|
|
1312
|
+
);
|
|
550
1313
|
return response instanceof Response ? response : c.json(response);
|
|
551
1314
|
} catch (error) {
|
|
552
1315
|
const status = toStatusCode(error);
|
|
@@ -559,6 +1322,7 @@ export function createServerApp(
|
|
|
559
1322
|
requestId,
|
|
560
1323
|
error,
|
|
561
1324
|
status,
|
|
1325
|
+
finishRequestCost(requestCost),
|
|
562
1326
|
);
|
|
563
1327
|
return c.json(toErrorResponse(error, requestId), status);
|
|
564
1328
|
}
|
|
@@ -613,7 +1377,10 @@ export async function serve(
|
|
|
613
1377
|
);
|
|
614
1378
|
}
|
|
615
1379
|
|
|
616
|
-
const app = createServerApp(provider, {
|
|
1380
|
+
const app = createServerApp(provider, {
|
|
1381
|
+
logger: options.logger,
|
|
1382
|
+
stt: options.stt,
|
|
1383
|
+
});
|
|
617
1384
|
|
|
618
1385
|
bunRuntime.serve({
|
|
619
1386
|
port: options.port ?? DEFAULT_PORT,
|