@apifuse/provider-sdk 2.1.0-beta.3 → 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 +163 -8
- package/CHANGELOG.md +8 -1
- package/README.md +17 -16
- package/SUBMISSION.md +4 -4
- package/bin/apifuse-dev.ts +12 -5
- package/bin/apifuse-pack-check.ts +9 -2
- package/bin/apifuse-pack-smoke.ts +127 -6
- package/bin/apifuse-perf.ts +19 -15
- package/bin/apifuse-record.ts +41 -53
- package/bin/apifuse-submit-check.ts +179 -7
- package/bin/apifuse.ts +1 -1
- package/package.json +17 -8
- package/src/choice-token.ts +164 -0
- package/src/cli/commands.ts +1 -3
- package/src/cli/create.ts +159 -50
- package/src/cli/templates/provider/README.md.tpl +24 -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 +1618 -104
- 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 +149 -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 +816 -58
- package/src/server/types.ts +35 -0
- package/src/stream.ts +210 -0
- package/src/testing/run.ts +17 -4
- package/src/types.ts +869 -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 | 429 | 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
|
}
|
|
@@ -280,6 +534,9 @@ function toStatusCode(error: unknown): 400 | 404 | 429 | 500 | 502 | 504 {
|
|
|
280
534
|
return 429;
|
|
281
535
|
case "UPSTREAM_ERROR":
|
|
282
536
|
return 502;
|
|
537
|
+
case "STT_UNAVAILABLE":
|
|
538
|
+
case "UNSUPPORTED_STT_BACKEND":
|
|
539
|
+
return 503;
|
|
283
540
|
}
|
|
284
541
|
|
|
285
542
|
return 400;
|
|
@@ -305,6 +562,7 @@ function logProviderError(
|
|
|
305
562
|
requestId: string | undefined,
|
|
306
563
|
error: unknown,
|
|
307
564
|
status: number,
|
|
565
|
+
cost: ProviderRequestCost,
|
|
308
566
|
): void {
|
|
309
567
|
const code =
|
|
310
568
|
error instanceof ProviderError
|
|
@@ -314,6 +572,10 @@ function logProviderError(
|
|
|
314
572
|
: "internal_error";
|
|
315
573
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
316
574
|
const message = error instanceof Error ? error.message : String(error);
|
|
575
|
+
const details =
|
|
576
|
+
error instanceof ProviderError
|
|
577
|
+
? providerObservabilityDetails(error)
|
|
578
|
+
: undefined;
|
|
317
579
|
const emit =
|
|
318
580
|
typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
319
581
|
emit({
|
|
@@ -324,18 +586,50 @@ function logProviderError(
|
|
|
324
586
|
route,
|
|
325
587
|
...(requestId ? { requestId } : {}),
|
|
326
588
|
status,
|
|
589
|
+
...cost,
|
|
327
590
|
code,
|
|
328
591
|
errorClass,
|
|
329
592
|
message,
|
|
330
|
-
...(error instanceof TransportError && error.
|
|
331
|
-
? { 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
|
+
}
|
|
332
602
|
: {}),
|
|
333
603
|
...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
|
|
334
604
|
});
|
|
335
605
|
}
|
|
336
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
|
+
|
|
337
630
|
function toJsonSuccessResponse(
|
|
338
631
|
result: unknown,
|
|
632
|
+
ctx?: ProviderContext,
|
|
339
633
|
): Response | OperationSuccessResponse {
|
|
340
634
|
if (result instanceof Response) {
|
|
341
635
|
return result;
|
|
@@ -345,7 +639,335 @@ function toJsonSuccessResponse(
|
|
|
345
639
|
return new Response(result);
|
|
346
640
|
}
|
|
347
641
|
|
|
348
|
-
|
|
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;
|
|
349
971
|
}
|
|
350
972
|
|
|
351
973
|
function toAuthFlowResponse(
|
|
@@ -370,15 +992,57 @@ async function handleOperation(
|
|
|
370
992
|
provider: ProviderDefinition,
|
|
371
993
|
request: OperationRequest,
|
|
372
994
|
operationId: string,
|
|
995
|
+
options: ProviderServerOptions = {},
|
|
996
|
+
proxyTelemetry?: ProxyTelemetryCollector,
|
|
373
997
|
): Promise<Response | OperationResponse> {
|
|
374
|
-
const ctx = createProviderContext(
|
|
375
|
-
const result = await executeOperation(
|
|
998
|
+
const ctx = createProviderContext(
|
|
376
999
|
provider,
|
|
1000
|
+
request,
|
|
377
1001
|
operationId,
|
|
378
|
-
|
|
379
|
-
|
|
1002
|
+
options,
|
|
1003
|
+
proxyTelemetry,
|
|
380
1004
|
);
|
|
381
|
-
|
|
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
|
+
});
|
|
382
1046
|
}
|
|
383
1047
|
|
|
384
1048
|
type AuthRoute = "start" | "continue" | "poll" | "abort";
|
|
@@ -387,6 +1051,7 @@ async function handleAuthFlow(
|
|
|
387
1051
|
provider: ProviderDefinition,
|
|
388
1052
|
request: AuthFlowRequest,
|
|
389
1053
|
route: AuthRoute,
|
|
1054
|
+
options: ProviderServerOptions = {},
|
|
390
1055
|
): Promise<Response | AuthFlowResponse> {
|
|
391
1056
|
const flow = provider.auth?.flow;
|
|
392
1057
|
if (!flow) {
|
|
@@ -395,22 +1060,29 @@ async function handleAuthFlow(
|
|
|
395
1060
|
});
|
|
396
1061
|
}
|
|
397
1062
|
|
|
398
|
-
const { context, getPatch } = createAuthFlowContext(
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
:
|
|
410
|
-
?
|
|
411
|
-
|
|
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;
|
|
412
1081
|
|
|
413
|
-
|
|
1082
|
+
return toAuthFlowResponse(result, getPatch());
|
|
1083
|
+
} finally {
|
|
1084
|
+
context.stealth.close?.();
|
|
1085
|
+
}
|
|
414
1086
|
}
|
|
415
1087
|
|
|
416
1088
|
export function createServerApp(
|
|
@@ -443,6 +1115,8 @@ export function createServerApp(
|
|
|
443
1115
|
app.post("/v1/:operation", async (c) => {
|
|
444
1116
|
let rawBody: unknown;
|
|
445
1117
|
const operation = c.req.param("operation");
|
|
1118
|
+
const proxyTelemetry = new ProxyTelemetryCollector();
|
|
1119
|
+
const requestCost = startRequestCost();
|
|
446
1120
|
try {
|
|
447
1121
|
rawBody = await c.req.raw
|
|
448
1122
|
.clone()
|
|
@@ -451,8 +1125,37 @@ export function createServerApp(
|
|
|
451
1125
|
const body = OperationRequestSchema.parse(rawBody);
|
|
452
1126
|
const requestHeaders = Object.fromEntries(c.req.raw.headers.entries());
|
|
453
1127
|
body.headers = { ...requestHeaders, ...body.headers };
|
|
454
|
-
const response = await handleOperation(
|
|
455
|
-
|
|
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);
|
|
456
1159
|
} catch (error) {
|
|
457
1160
|
const status = toStatusCode(error);
|
|
458
1161
|
const requestId = extractRequestId(rawBody);
|
|
@@ -464,20 +1167,33 @@ export function createServerApp(
|
|
|
464
1167
|
requestId,
|
|
465
1168
|
error,
|
|
466
1169
|
status,
|
|
1170
|
+
finishRequestCost(requestCost),
|
|
467
1171
|
);
|
|
1172
|
+
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1173
|
+
if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
468
1174
|
return c.json(toErrorResponse(error, requestId), status);
|
|
469
1175
|
}
|
|
470
1176
|
});
|
|
471
1177
|
|
|
472
1178
|
app.post("/auth/start", async (c) => {
|
|
473
1179
|
let rawBody: unknown;
|
|
1180
|
+
const requestCost = startRequestCost();
|
|
474
1181
|
try {
|
|
475
1182
|
rawBody = await c.req.raw
|
|
476
1183
|
.clone()
|
|
477
1184
|
.json()
|
|
478
1185
|
.catch(() => undefined);
|
|
479
1186
|
const body = AuthFlowRequestSchema.parse(rawBody);
|
|
480
|
-
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
|
+
);
|
|
481
1197
|
return response instanceof Response ? response : c.json(response);
|
|
482
1198
|
} catch (error) {
|
|
483
1199
|
const status = toStatusCode(error);
|
|
@@ -490,6 +1206,7 @@ export function createServerApp(
|
|
|
490
1206
|
requestId,
|
|
491
1207
|
error,
|
|
492
1208
|
status,
|
|
1209
|
+
finishRequestCost(requestCost),
|
|
493
1210
|
);
|
|
494
1211
|
return c.json(toErrorResponse(error, requestId), status);
|
|
495
1212
|
}
|
|
@@ -497,13 +1214,28 @@ export function createServerApp(
|
|
|
497
1214
|
|
|
498
1215
|
app.post("/auth/continue", async (c) => {
|
|
499
1216
|
let rawBody: unknown;
|
|
1217
|
+
const requestCost = startRequestCost();
|
|
500
1218
|
try {
|
|
501
1219
|
rawBody = await c.req.raw
|
|
502
1220
|
.clone()
|
|
503
1221
|
.json()
|
|
504
1222
|
.catch(() => undefined);
|
|
505
1223
|
const body = AuthFlowRequestSchema.parse(rawBody);
|
|
506
|
-
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
|
+
);
|
|
507
1239
|
return response instanceof Response ? response : c.json(response);
|
|
508
1240
|
} catch (error) {
|
|
509
1241
|
const status = toStatusCode(error);
|
|
@@ -516,6 +1248,7 @@ export function createServerApp(
|
|
|
516
1248
|
requestId,
|
|
517
1249
|
error,
|
|
518
1250
|
status,
|
|
1251
|
+
finishRequestCost(requestCost),
|
|
519
1252
|
);
|
|
520
1253
|
return c.json(toErrorResponse(error, requestId), status);
|
|
521
1254
|
}
|
|
@@ -523,13 +1256,23 @@ export function createServerApp(
|
|
|
523
1256
|
|
|
524
1257
|
app.post("/auth/poll", async (c) => {
|
|
525
1258
|
let rawBody: unknown;
|
|
1259
|
+
const requestCost = startRequestCost();
|
|
526
1260
|
try {
|
|
527
1261
|
rawBody = await c.req.raw
|
|
528
1262
|
.clone()
|
|
529
1263
|
.json()
|
|
530
1264
|
.catch(() => undefined);
|
|
531
1265
|
const body = AuthFlowRequestSchema.parse(rawBody);
|
|
532
|
-
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
|
+
);
|
|
533
1276
|
return response instanceof Response ? response : c.json(response);
|
|
534
1277
|
} catch (error) {
|
|
535
1278
|
const status = toStatusCode(error);
|
|
@@ -542,6 +1285,7 @@ export function createServerApp(
|
|
|
542
1285
|
requestId,
|
|
543
1286
|
error,
|
|
544
1287
|
status,
|
|
1288
|
+
finishRequestCost(requestCost),
|
|
545
1289
|
);
|
|
546
1290
|
return c.json(toErrorResponse(error, requestId), status);
|
|
547
1291
|
}
|
|
@@ -549,13 +1293,23 @@ export function createServerApp(
|
|
|
549
1293
|
|
|
550
1294
|
app.post("/auth/disconnect", async (c) => {
|
|
551
1295
|
let rawBody: unknown;
|
|
1296
|
+
const requestCost = startRequestCost();
|
|
552
1297
|
try {
|
|
553
1298
|
rawBody = await c.req.raw
|
|
554
1299
|
.clone()
|
|
555
1300
|
.json()
|
|
556
1301
|
.catch(() => undefined);
|
|
557
1302
|
const body = AuthFlowRequestSchema.parse(rawBody);
|
|
558
|
-
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
|
+
);
|
|
559
1313
|
return response instanceof Response ? response : c.json(response);
|
|
560
1314
|
} catch (error) {
|
|
561
1315
|
const status = toStatusCode(error);
|
|
@@ -568,6 +1322,7 @@ export function createServerApp(
|
|
|
568
1322
|
requestId,
|
|
569
1323
|
error,
|
|
570
1324
|
status,
|
|
1325
|
+
finishRequestCost(requestCost),
|
|
571
1326
|
);
|
|
572
1327
|
return c.json(toErrorResponse(error, requestId), status);
|
|
573
1328
|
}
|
|
@@ -622,7 +1377,10 @@ export async function serve(
|
|
|
622
1377
|
);
|
|
623
1378
|
}
|
|
624
1379
|
|
|
625
|
-
const app = createServerApp(provider, {
|
|
1380
|
+
const app = createServerApp(provider, {
|
|
1381
|
+
logger: options.logger,
|
|
1382
|
+
stt: options.stt,
|
|
1383
|
+
});
|
|
626
1384
|
|
|
627
1385
|
bunRuntime.serve({
|
|
628
1386
|
port: options.port ?? DEFAULT_PORT,
|