@apifuse/provider-sdk 2.1.0-beta.0 → 2.1.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORING.md +218 -21
- package/CHANGELOG.md +54 -0
- package/README.md +147 -10
- package/SUBMISSION.md +87 -0
- package/bin/apifuse-check.ts +86 -4
- package/bin/apifuse-dev.ts +87 -13
- package/bin/apifuse-pack-check.ts +120 -0
- package/bin/apifuse-pack-smoke.ts +423 -0
- package/bin/apifuse-perf.ts +142 -49
- package/bin/apifuse-record.ts +182 -104
- package/bin/apifuse-submit-check.ts +2538 -0
- package/bin/apifuse.ts +1 -1
- package/dist/ceremonies/index.d.ts +41 -0
- package/dist/ceremonies/index.js +490 -0
- package/dist/choice-token.d.ts +24 -0
- package/dist/choice-token.js +74 -0
- package/dist/cli/commands.d.ts +10 -0
- package/dist/cli/commands.js +80 -0
- package/dist/cli/create.d.ts +47 -0
- package/dist/cli/create.js +762 -0
- package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
- package/dist/cli/templates/provider/.gitignore.tpl +22 -0
- package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
- package/dist/cli/templates/provider/README.md.tpl +160 -0
- package/dist/cli/templates/provider/dev.ts.tpl +5 -0
- package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
- package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
- package/dist/cli/templates/provider/index.ts.tpl +15 -0
- package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/dist/cli/templates/provider/meta.ts.tpl +7 -0
- package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/start.ts.tpl +5 -0
- package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/dist/config/loader.d.ts +107 -0
- package/dist/config/loader.js +935 -0
- package/dist/contract-json.d.ts +9 -0
- package/dist/contract-json.js +51 -0
- package/dist/contract-serialization.d.ts +4 -0
- package/dist/contract-serialization.js +78 -0
- package/dist/contract-types.d.ts +49 -0
- package/dist/contract-types.js +1 -0
- package/dist/contract.d.ts +6 -0
- package/dist/contract.js +155 -0
- package/dist/define.d.ts +97 -0
- package/dist/define.js +1320 -0
- package/dist/dev.d.ts +9 -0
- package/dist/dev.js +15 -0
- package/dist/errors.d.ts +59 -0
- package/dist/errors.js +97 -0
- package/dist/i18n/catalog.d.ts +29 -0
- package/dist/i18n/catalog.js +159 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.js +2 -0
- package/dist/i18n/keys.d.ts +10 -0
- package/dist/i18n/keys.js +34 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +37 -0
- package/dist/lint.d.ts +73 -0
- package/dist/lint.js +702 -0
- package/dist/observability.d.ts +5 -0
- package/dist/observability.js +39 -0
- package/dist/provider.d.ts +9 -0
- package/dist/provider.js +8 -0
- package/dist/public-schema-field-lint.d.ts +2 -0
- package/dist/public-schema-field-lint.js +158 -0
- package/dist/recipes/gov-api.d.ts +19 -0
- package/dist/recipes/gov-api.js +72 -0
- package/dist/recipes/rest-api.d.ts +21 -0
- package/dist/recipes/rest-api.js +115 -0
- package/dist/runtime/auth-flow.d.ts +14 -0
- package/dist/runtime/auth-flow.js +44 -0
- package/dist/runtime/browser.d.ts +25 -0
- package/dist/runtime/browser.js +1034 -0
- package/dist/runtime/cache.d.ts +10 -0
- package/dist/runtime/cache.js +372 -0
- package/dist/runtime/choice.d.ts +15 -0
- package/dist/runtime/choice.js +435 -0
- package/dist/runtime/credential.d.ts +8 -0
- package/dist/runtime/credential.js +61 -0
- package/dist/runtime/env.d.ts +2 -0
- package/dist/runtime/env.js +10 -0
- package/dist/runtime/executor.d.ts +16 -0
- package/dist/runtime/executor.js +51 -0
- package/dist/runtime/http.d.ts +8 -0
- package/dist/runtime/http.js +706 -0
- package/dist/runtime/insights.d.ts +9 -0
- package/dist/runtime/insights.js +324 -0
- package/dist/runtime/instrumentation.d.ts +8 -0
- package/dist/runtime/instrumentation.js +269 -0
- package/dist/runtime/key-derivation.d.ts +24 -0
- package/dist/runtime/key-derivation.js +73 -0
- package/dist/runtime/keyring.d.ts +25 -0
- package/dist/runtime/keyring.js +93 -0
- package/dist/runtime/namespace.d.ts +9 -0
- package/dist/runtime/namespace.js +19 -0
- package/dist/runtime/otlp.d.ts +39 -0
- package/dist/runtime/otlp.js +103 -0
- package/dist/runtime/perf.d.ts +12 -0
- package/dist/runtime/perf.js +52 -0
- package/dist/runtime/prevalidate.d.ts +12 -0
- package/dist/runtime/prevalidate.js +173 -0
- package/dist/runtime/provider.d.ts +2 -0
- package/dist/runtime/provider.js +11 -0
- package/dist/runtime/proxy-errors.d.ts +21 -0
- package/dist/runtime/proxy-errors.js +83 -0
- package/dist/runtime/proxy-telemetry.d.ts +8 -0
- package/dist/runtime/proxy-telemetry.js +174 -0
- package/dist/runtime/redis.d.ts +17 -0
- package/dist/runtime/redis.js +82 -0
- package/dist/runtime/request-options.d.ts +3 -0
- package/dist/runtime/request-options.js +42 -0
- package/dist/runtime/state.d.ts +17 -0
- package/dist/runtime/state.js +344 -0
- package/dist/runtime/stealth.d.ts +18 -0
- package/dist/runtime/stealth.js +834 -0
- package/dist/runtime/stt.d.ts +22 -0
- package/dist/runtime/stt.js +480 -0
- package/dist/runtime/trace.d.ts +26 -0
- package/dist/runtime/trace.js +142 -0
- package/dist/runtime/waterfall.d.ts +12 -0
- package/dist/runtime/waterfall.js +147 -0
- package/dist/schema.d.ts +74 -0
- package/dist/schema.js +243 -0
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +2 -0
- package/dist/server/serve.d.ts +64 -0
- package/dist/server/serve.js +1110 -0
- package/dist/server/types.d.ts +136 -0
- package/dist/server/types.js +86 -0
- package/dist/stealth/profiles.d.ts +4 -0
- package/dist/stealth/profiles.js +259 -0
- package/dist/stream.d.ts +44 -0
- package/dist/stream.js +151 -0
- package/dist/testing/helpers.d.ts +23 -0
- package/dist/testing/helpers.js +95 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/run.d.ts +34 -0
- package/dist/testing/run.js +303 -0
- package/dist/types.d.ts +1326 -0
- package/dist/types.js +61 -0
- package/dist/utils/date.d.ts +6 -0
- package/dist/utils/date.js +101 -0
- package/dist/utils/parse.d.ts +16 -0
- package/dist/utils/parse.js +51 -0
- package/dist/utils/text.d.ts +4 -0
- package/dist/utils/text.js +14 -0
- package/dist/utils/transform.d.ts +8 -0
- package/dist/utils/transform.js +48 -0
- package/package.json +57 -29
- package/src/ceremonies/index.ts +30 -3
- package/src/choice-token.ts +165 -0
- package/src/cli/commands.ts +34 -11
- package/src/cli/create.ts +214 -52
- package/src/cli/templates/provider/.dockerignore.tpl +22 -0
- package/src/cli/templates/provider/.gitignore.tpl +22 -0
- package/src/cli/templates/provider/README.md.tpl +134 -2
- 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 -44
- 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 +24 -0
- package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -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 +1282 -7
- package/src/contract-json.ts +75 -0
- package/src/contract-serialization.ts +89 -0
- package/src/contract-types.ts +52 -0
- package/src/contract.ts +215 -0
- package/src/define.ts +1726 -48
- package/src/errors.ts +27 -0
- package/src/i18n/catalog.ts +277 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/keys.ts +64 -0
- package/src/index.ts +174 -15
- package/src/lint.ts +547 -73
- package/src/observability.ts +41 -0
- package/src/provider.ts +104 -5
- package/src/public-schema-field-lint.ts +237 -0
- package/src/runtime/auth-flow.ts +7 -0
- package/src/runtime/browser.ts +762 -51
- package/src/runtime/cache.ts +528 -0
- package/src/runtime/choice.ts +760 -0
- package/src/runtime/executor.ts +32 -3
- package/src/runtime/http.ts +945 -185
- 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/redis.ts +116 -0
- package/src/runtime/request-options.ts +66 -0
- package/src/runtime/state.ts +563 -0
- package/src/runtime/stealth.ts +1159 -0
- package/src/runtime/stt.ts +629 -0
- package/src/runtime/trace.ts +1 -1
- package/src/schema.ts +363 -1
- package/src/server/serve.ts +1172 -76
- package/src/server/types.ts +37 -0
- package/src/stream.ts +210 -0
- package/src/testing/run.ts +31 -5
- package/src/types.ts +1118 -44
- package/src/composite.ts +0 -43
- package/src/runtime/tls.ts +0 -425
- package/src/types/playwright-stealth.d.ts +0 -9
|
@@ -0,0 +1,1110 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { AuthError, ProviderError, SessionExpiredError, TransportError, } from "../errors";
|
|
6
|
+
import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog";
|
|
7
|
+
import { categoryForStatus, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability";
|
|
8
|
+
import { createScratchpad } from "../runtime/auth-flow";
|
|
9
|
+
import { createBrowserClient } from "../runtime/browser";
|
|
10
|
+
import { createProviderCache } from "../runtime/cache";
|
|
11
|
+
import { createProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "../runtime/choice";
|
|
12
|
+
import { createCredentialContext } from "../runtime/credential";
|
|
13
|
+
import { createEnvContext } from "../runtime/env";
|
|
14
|
+
import { executeOperation } from "../runtime/executor";
|
|
15
|
+
import { createHttpClient } from "../runtime/http";
|
|
16
|
+
import { wrapWithInstrumentation } from "../runtime/instrumentation";
|
|
17
|
+
import { getProviderBaseUrl } from "../runtime/provider";
|
|
18
|
+
import { PROXY_AUTH_IP_DENIED_CODE, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_EXHAUSTED_CODE, } from "../runtime/proxy-errors";
|
|
19
|
+
import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector, } from "../runtime/proxy-telemetry";
|
|
20
|
+
import { createProviderRuntimeStateFromEnv, createUnsupportedProviderRuntimeState, } from "../runtime/state";
|
|
21
|
+
import { createStealthClient } from "../runtime/stealth";
|
|
22
|
+
import { createSttClientFromEnv } from "../runtime/stt";
|
|
23
|
+
import { createTraceContext } from "../runtime/trace";
|
|
24
|
+
import { parseSchema } from "../schema";
|
|
25
|
+
import { getStealthProfile } from "../stealth/profiles";
|
|
26
|
+
import { APIFUSE_STREAM_DONE_EVENT, APIFUSE_STREAM_ERROR_EVENT, encodeSseEvent, error as streamError, } from "../stream";
|
|
27
|
+
import { AuthFlowRequestSchema, OperationRequestSchema, } from "./types";
|
|
28
|
+
const DEFAULT_HOST = "0.0.0.0";
|
|
29
|
+
const DEFAULT_PORT = 3000;
|
|
30
|
+
const AUTH_FLOW_LOCALES = ["en", "ko", "ja"];
|
|
31
|
+
const retryResponseMeta = new WeakMap();
|
|
32
|
+
function createAuthStub() {
|
|
33
|
+
return {
|
|
34
|
+
async requestField(name) {
|
|
35
|
+
throw new ProviderError(`Auth prompt is unavailable for ${name}`, {
|
|
36
|
+
code: "AUTH_PROMPT_UNAVAILABLE",
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function createBrowserStub() {
|
|
42
|
+
return {
|
|
43
|
+
engine: "playwright-stealth",
|
|
44
|
+
async close() { },
|
|
45
|
+
async newPage() {
|
|
46
|
+
throw new ProviderError("Browser runtime is not available", {
|
|
47
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
async rawPage() {
|
|
51
|
+
throw new ProviderError("Browser runtime is not available", {
|
|
52
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
async withIsolatedContext() {
|
|
56
|
+
throw new ProviderError("Browser runtime is not available", {
|
|
57
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
async solveChallenge() {
|
|
61
|
+
throw new ProviderError("Browser runtime is not available", {
|
|
62
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function createStealthStub() {
|
|
68
|
+
return {
|
|
69
|
+
async fetch() {
|
|
70
|
+
throw new ProviderError("Stealth runtime is not available", {
|
|
71
|
+
code: "STEALTH_RUNTIME_UNSUPPORTED",
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
createSession() {
|
|
75
|
+
throw new ProviderError("Stealth runtime is not available", {
|
|
76
|
+
code: "STEALTH_RUNTIME_UNSUPPORTED",
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
close() {
|
|
80
|
+
// no-op
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function getProviderStealthBaseUrl(provider) {
|
|
85
|
+
const baseUrl = getProviderBaseUrl(provider);
|
|
86
|
+
if (baseUrl) {
|
|
87
|
+
return baseUrl;
|
|
88
|
+
}
|
|
89
|
+
const firstHost = provider.allowedHosts?.[0];
|
|
90
|
+
return firstHost ? `https://${firstHost}` : undefined;
|
|
91
|
+
}
|
|
92
|
+
function getProviderStealthProfile(provider) {
|
|
93
|
+
return provider.stealth?.profile
|
|
94
|
+
? getStealthProfile(provider.stealth.profile)
|
|
95
|
+
: undefined;
|
|
96
|
+
}
|
|
97
|
+
function isProductionProviderBrowserMode(provider, env = process.env) {
|
|
98
|
+
if (provider.runtime !== "browser") {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
if (env.APIFUSE__PROVIDER__RUNTIME === "browser") {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return (env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id);
|
|
105
|
+
}
|
|
106
|
+
export function resolveProviderProxyAffinityKey(provider, request, operationId) {
|
|
107
|
+
const connectionKey = request.connection?.id ?? request.connection?.externalRef;
|
|
108
|
+
const affinity = typeof provider.proxy === "object"
|
|
109
|
+
? provider.proxy.session?.affinity
|
|
110
|
+
: undefined;
|
|
111
|
+
if (affinity === "operation") {
|
|
112
|
+
return `${provider.id}/${operationId}`;
|
|
113
|
+
}
|
|
114
|
+
return connectionKey ?? provider.id;
|
|
115
|
+
}
|
|
116
|
+
function createProviderContext(provider, request, operationId, options = {}, state = createUnsupportedProviderRuntimeState(), proxyTelemetry) {
|
|
117
|
+
const baseUrl = getProviderBaseUrl(provider);
|
|
118
|
+
const stealthBaseUrl = getProviderStealthBaseUrl(provider);
|
|
119
|
+
const stealthProfile = getProviderStealthProfile(provider);
|
|
120
|
+
const proxyClientOptions = {
|
|
121
|
+
upstream: { proxy: provider.proxy },
|
|
122
|
+
affinityKey: resolveProviderProxyAffinityKey(provider, request, operationId),
|
|
123
|
+
telemetry: proxyTelemetry,
|
|
124
|
+
};
|
|
125
|
+
let wrappedContext;
|
|
126
|
+
const stealthClientOptions = {
|
|
127
|
+
upstream: proxyClientOptions.upstream,
|
|
128
|
+
affinityKey: proxyClientOptions.affinityKey,
|
|
129
|
+
telemetry: proxyTelemetry,
|
|
130
|
+
};
|
|
131
|
+
const env = createEnvContext([
|
|
132
|
+
...(provider.secrets?.map((secret) => secret.name) ?? []),
|
|
133
|
+
PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
|
|
134
|
+
]);
|
|
135
|
+
const credential = createCredentialContext({
|
|
136
|
+
allowedKeys: provider.credential?.keys,
|
|
137
|
+
mode: request.connection?.mode,
|
|
138
|
+
scopes: request.connection?.scopes,
|
|
139
|
+
values: request.connection?.secrets,
|
|
140
|
+
});
|
|
141
|
+
const requestContext = {
|
|
142
|
+
connectionId: request.connection?.id,
|
|
143
|
+
headers: request.headers ?? {},
|
|
144
|
+
};
|
|
145
|
+
const context = wrapWithInstrumentation({
|
|
146
|
+
env,
|
|
147
|
+
credential,
|
|
148
|
+
request: requestContext,
|
|
149
|
+
http: createHttpClient(baseUrl, {
|
|
150
|
+
...proxyClientOptions,
|
|
151
|
+
onRetrySummary: (summary) => {
|
|
152
|
+
if (summary.attempts <= 1 || !wrappedContext)
|
|
153
|
+
return;
|
|
154
|
+
retryResponseMeta.set(wrappedContext, summary);
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
cache: createProviderCache({ providerId: provider.id }),
|
|
158
|
+
state,
|
|
159
|
+
stealth: stealthBaseUrl
|
|
160
|
+
? stealthProfile
|
|
161
|
+
? createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
|
|
162
|
+
: createStealthClient(stealthBaseUrl, stealthClientOptions)
|
|
163
|
+
: createStealthStub(),
|
|
164
|
+
browser: provider.runtime === "browser"
|
|
165
|
+
? createBrowserClient({
|
|
166
|
+
allowedHosts: provider.allowedHosts,
|
|
167
|
+
cdpUrl: process.env.APIFUSE__CDP_POOL__URL,
|
|
168
|
+
headless: true,
|
|
169
|
+
requireCdpPool: isProductionProviderBrowserMode(provider),
|
|
170
|
+
stealth: true,
|
|
171
|
+
engine: provider.browser?.engine,
|
|
172
|
+
})
|
|
173
|
+
: createBrowserStub(),
|
|
174
|
+
trace: createTraceContext(),
|
|
175
|
+
auth: createAuthStub(),
|
|
176
|
+
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
|
177
|
+
choice: createProviderChoiceContext({
|
|
178
|
+
providerId: provider.id,
|
|
179
|
+
env,
|
|
180
|
+
request: requestContext,
|
|
181
|
+
credential,
|
|
182
|
+
state,
|
|
183
|
+
}),
|
|
184
|
+
});
|
|
185
|
+
wrappedContext = context;
|
|
186
|
+
return context;
|
|
187
|
+
}
|
|
188
|
+
function createFlowContextStore(allowedKeys, initialContext = {}) {
|
|
189
|
+
const context = createScratchpad(allowedKeys, initialContext);
|
|
190
|
+
return {
|
|
191
|
+
context,
|
|
192
|
+
getPatch() {
|
|
193
|
+
const next = context.toJSON();
|
|
194
|
+
const patch = new Map();
|
|
195
|
+
for (const [key, value] of Object.entries(next)) {
|
|
196
|
+
if (initialContext[key] !== value) {
|
|
197
|
+
patch.set(key, value);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (const key of Object.keys(initialContext)) {
|
|
201
|
+
if (!(key in next)) {
|
|
202
|
+
patch.set(key, null);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (patch.size === 0) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
return Object.fromEntries(patch.entries());
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function createAuthFlowContext(provider, request, options = {}) {
|
|
213
|
+
const baseUrl = getProviderBaseUrl(provider);
|
|
214
|
+
const stealthBaseUrl = getProviderStealthBaseUrl(provider);
|
|
215
|
+
const stealthProfile = getProviderStealthProfile(provider);
|
|
216
|
+
const contextData = request.context ?? {};
|
|
217
|
+
const flowContextStore = createFlowContextStore(provider.context?.keys ?? Object.keys(contextData), contextData);
|
|
218
|
+
const proxyClientOptions = {
|
|
219
|
+
upstream: { proxy: provider.proxy },
|
|
220
|
+
affinityKey: request.connectionId ??
|
|
221
|
+
request.externalRef ??
|
|
222
|
+
request.tenantId ??
|
|
223
|
+
request.providerId ??
|
|
224
|
+
provider.id,
|
|
225
|
+
};
|
|
226
|
+
const stealthClientOptions = {
|
|
227
|
+
upstream: proxyClientOptions.upstream,
|
|
228
|
+
affinityKey: proxyClientOptions.affinityKey,
|
|
229
|
+
};
|
|
230
|
+
const credential = request.connection
|
|
231
|
+
? createCredentialContext({
|
|
232
|
+
allowedKeys: provider.credential?.keys,
|
|
233
|
+
mode: request.connection.mode,
|
|
234
|
+
scopes: request.connection.scopes,
|
|
235
|
+
values: request.connection.secrets,
|
|
236
|
+
})
|
|
237
|
+
: undefined;
|
|
238
|
+
return {
|
|
239
|
+
context: {
|
|
240
|
+
connectionId: request.connectionId,
|
|
241
|
+
externalRef: request.externalRef,
|
|
242
|
+
tenantId: request.tenantId ?? "",
|
|
243
|
+
providerId: request.providerId ?? provider.id,
|
|
244
|
+
http: createHttpClient(baseUrl, proxyClientOptions),
|
|
245
|
+
stealth: stealthBaseUrl
|
|
246
|
+
? stealthProfile
|
|
247
|
+
? createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
|
|
248
|
+
: createStealthClient(stealthBaseUrl, stealthClientOptions)
|
|
249
|
+
: createStealthStub(),
|
|
250
|
+
env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
|
|
251
|
+
credential,
|
|
252
|
+
context: flowContextStore.context,
|
|
253
|
+
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
|
254
|
+
},
|
|
255
|
+
getPatch: flowContextStore.getPatch,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const defaultProviderServerLogger = (event) => {
|
|
259
|
+
const line = JSON.stringify(event);
|
|
260
|
+
if (event.level === "info") {
|
|
261
|
+
console.log(line);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
console.error(line);
|
|
265
|
+
};
|
|
266
|
+
function startRequestCost() {
|
|
267
|
+
return {
|
|
268
|
+
startedAtMs: performance.now(),
|
|
269
|
+
cpuStart: process.cpuUsage(),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function finishRequestCost(input) {
|
|
273
|
+
const cpuDelta = process.cpuUsage(input.cpuStart);
|
|
274
|
+
return {
|
|
275
|
+
durationMs: Math.max(0, Math.round(performance.now() - input.startedAtMs)),
|
|
276
|
+
cpuUserMicros: Math.max(0, cpuDelta.user),
|
|
277
|
+
cpuSystemMicros: Math.max(0, cpuDelta.system),
|
|
278
|
+
cpuTotalMicros: Math.max(0, cpuDelta.user + cpuDelta.system),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function zodDetails(error) {
|
|
282
|
+
return error.issues.map((issue) => ({
|
|
283
|
+
path: issue.path.join("."),
|
|
284
|
+
code: issue.code,
|
|
285
|
+
message: issue.message,
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
function toErrorResponse(error, requestId) {
|
|
289
|
+
if (error instanceof ProviderError) {
|
|
290
|
+
const details = publicProviderErrorDetails(error);
|
|
291
|
+
return {
|
|
292
|
+
error: {
|
|
293
|
+
code: error.code ?? "provider_error",
|
|
294
|
+
message: publicProviderErrorMessage(error),
|
|
295
|
+
...(requestId ? { requestId } : {}),
|
|
296
|
+
...(error.fix ? { fix: error.fix } : {}),
|
|
297
|
+
...(details ? { details } : {}),
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (error instanceof z.ZodError) {
|
|
302
|
+
return {
|
|
303
|
+
error: {
|
|
304
|
+
code: "invalid_request",
|
|
305
|
+
message: "Invalid request body",
|
|
306
|
+
...(requestId ? { requestId } : {}),
|
|
307
|
+
details: zodDetails(error),
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
error: {
|
|
313
|
+
code: "internal_error",
|
|
314
|
+
message: "Internal error",
|
|
315
|
+
...(requestId ? { requestId } : {}),
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function publicProviderErrorDetails(error) {
|
|
320
|
+
const providerDetails = error.details;
|
|
321
|
+
const observabilityDetails = providerObservabilityDetails(error);
|
|
322
|
+
if (providerDetails === undefined) {
|
|
323
|
+
return observabilityDetails;
|
|
324
|
+
}
|
|
325
|
+
if (observabilityDetails === undefined) {
|
|
326
|
+
return providerDetails;
|
|
327
|
+
}
|
|
328
|
+
if (isPlainRecord(providerDetails) && isPlainRecord(observabilityDetails)) {
|
|
329
|
+
return { ...providerDetails, ...observabilityDetails };
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
provider: providerDetails,
|
|
333
|
+
observability: observabilityDetails,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function isPlainRecord(value) {
|
|
337
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
338
|
+
}
|
|
339
|
+
function providerObservabilityDetails(error) {
|
|
340
|
+
// Session-expiry surfaces the credential_expired category + the opt-in
|
|
341
|
+
// retryable signal so Gateway/Credential Service can refresh and re-drive the
|
|
342
|
+
// operation (see design.md §4.3 D3). Without this branch the auth error would
|
|
343
|
+
// serialize as a bare 401 with no retryable/category, losing the refresh
|
|
344
|
+
// signal for exactly the retryOnAuthRefresh operations it is meant to enable.
|
|
345
|
+
if (error instanceof SessionExpiredError) {
|
|
346
|
+
return {
|
|
347
|
+
category: error.options?.category ?? "credential_expired",
|
|
348
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
349
|
+
retryable: error.options?.retryable ?? false,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (!(error instanceof TransportError)) {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
const isProxyPoolCode = error.code === PROXY_POOL_EXHAUSTED_CODE ||
|
|
356
|
+
error.code === PROXY_EDGE_AUTH_REJECTED_CODE ||
|
|
357
|
+
error.code === "PROXY_ALLOCATION_FAILED";
|
|
358
|
+
const category = error.options?.category ??
|
|
359
|
+
(isProxyPoolCode
|
|
360
|
+
? "proxy_pool"
|
|
361
|
+
: error.code === PROXY_AUTH_IP_DENIED_CODE
|
|
362
|
+
? "anti_bot_blocked"
|
|
363
|
+
: error.code === "transport_timeout"
|
|
364
|
+
? "timeout"
|
|
365
|
+
: error.code === "transport_network_error"
|
|
366
|
+
? "network"
|
|
367
|
+
: error.upstreamStatus
|
|
368
|
+
? categoryForStatus(error.upstreamStatus)
|
|
369
|
+
: "upstream_http");
|
|
370
|
+
return {
|
|
371
|
+
category,
|
|
372
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
373
|
+
retryable: error.options?.retryable ??
|
|
374
|
+
(category === "upstream_http" && error.upstreamStatus
|
|
375
|
+
? error.upstreamStatus >= 500
|
|
376
|
+
: isRetryableCategory(category)),
|
|
377
|
+
...(error.upstreamStatus ? { upstreamStatus: error.upstreamStatus } : {}),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function publicProviderErrorMessage(error) {
|
|
381
|
+
if (error instanceof TransportError) {
|
|
382
|
+
if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
|
|
383
|
+
return error.message;
|
|
384
|
+
}
|
|
385
|
+
if (error.code === PROXY_EDGE_AUTH_REJECTED_CODE) {
|
|
386
|
+
return error.message;
|
|
387
|
+
}
|
|
388
|
+
if (error.code === PROXY_POOL_EXHAUSTED_CODE) {
|
|
389
|
+
return error.message;
|
|
390
|
+
}
|
|
391
|
+
if (error.code === "transport_timeout")
|
|
392
|
+
return "Request timed out";
|
|
393
|
+
if (error.code === "transport_network_error")
|
|
394
|
+
return "Network error";
|
|
395
|
+
if (error.code === "upstream_http_error" && error.status) {
|
|
396
|
+
return `Upstream request failed with status ${error.status}`;
|
|
397
|
+
}
|
|
398
|
+
if (error.status) {
|
|
399
|
+
return `Upstream request failed with status ${error.status}`;
|
|
400
|
+
}
|
|
401
|
+
return "Upstream request failed";
|
|
402
|
+
}
|
|
403
|
+
return error.message;
|
|
404
|
+
}
|
|
405
|
+
function toStatusCode(error) {
|
|
406
|
+
if (error instanceof z.ZodError) {
|
|
407
|
+
return 400;
|
|
408
|
+
}
|
|
409
|
+
if (error instanceof TransportError) {
|
|
410
|
+
return error.code === "transport_timeout" ? 504 : 502;
|
|
411
|
+
}
|
|
412
|
+
if (error instanceof ProviderError) {
|
|
413
|
+
switch (error.code) {
|
|
414
|
+
case "AUTH_REQUIRED":
|
|
415
|
+
case "reauth_required":
|
|
416
|
+
return 401;
|
|
417
|
+
case "NOT_FOUND":
|
|
418
|
+
case "not_found":
|
|
419
|
+
case "NO_DATA":
|
|
420
|
+
return 404;
|
|
421
|
+
case "RATE_LIMITED":
|
|
422
|
+
case "UPSTREAM_RATE_LIMIT":
|
|
423
|
+
case "LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR":
|
|
424
|
+
return 429;
|
|
425
|
+
case "UPSTREAM_ERROR":
|
|
426
|
+
case "BLOCKED":
|
|
427
|
+
return 502;
|
|
428
|
+
case "STT_UNAVAILABLE":
|
|
429
|
+
case "UNSUPPORTED_STT_BACKEND":
|
|
430
|
+
return 503;
|
|
431
|
+
}
|
|
432
|
+
return 400;
|
|
433
|
+
}
|
|
434
|
+
return 500;
|
|
435
|
+
}
|
|
436
|
+
function extractRequestId(raw) {
|
|
437
|
+
if (!raw || typeof raw !== "object") {
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
const value = Object.getOwnPropertyDescriptor(raw, "requestId")?.value;
|
|
441
|
+
return typeof value === "string" ? value : undefined;
|
|
442
|
+
}
|
|
443
|
+
function logProviderError(logger, provider, kind, route, requestId, error, status, cost) {
|
|
444
|
+
const code = error instanceof ProviderError
|
|
445
|
+
? (error.code ?? "provider_error")
|
|
446
|
+
: error instanceof z.ZodError
|
|
447
|
+
? "invalid_request"
|
|
448
|
+
: "internal_error";
|
|
449
|
+
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
450
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
451
|
+
const details = error instanceof ProviderError
|
|
452
|
+
? providerObservabilityDetails(error)
|
|
453
|
+
: undefined;
|
|
454
|
+
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
455
|
+
emit({
|
|
456
|
+
level: status >= 500 ? "error" : "warn",
|
|
457
|
+
event: "provider_request_failed",
|
|
458
|
+
providerId: provider.id,
|
|
459
|
+
kind,
|
|
460
|
+
route,
|
|
461
|
+
...(requestId ? { requestId } : {}),
|
|
462
|
+
status,
|
|
463
|
+
...cost,
|
|
464
|
+
code,
|
|
465
|
+
errorClass,
|
|
466
|
+
message,
|
|
467
|
+
...(error instanceof TransportError && error.upstreamStatus
|
|
468
|
+
? { upstreamStatus: error.upstreamStatus }
|
|
469
|
+
: {}),
|
|
470
|
+
...(details
|
|
471
|
+
? {
|
|
472
|
+
errorCategory: details.category,
|
|
473
|
+
taxonomyVersion: details.taxonomyVersion,
|
|
474
|
+
retryable: details.retryable,
|
|
475
|
+
}
|
|
476
|
+
: {}),
|
|
477
|
+
...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
function logProviderCleanupError(logger, provider, operationId, requestId, resource, error) {
|
|
481
|
+
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
482
|
+
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
483
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
484
|
+
emit({
|
|
485
|
+
level: "warn",
|
|
486
|
+
event: "provider_cleanup_failed",
|
|
487
|
+
providerId: provider.id,
|
|
488
|
+
kind: "operation",
|
|
489
|
+
route: operationId,
|
|
490
|
+
...(requestId ? { requestId } : {}),
|
|
491
|
+
resource,
|
|
492
|
+
errorClass,
|
|
493
|
+
message,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
function logProviderSuccess(logger, provider, kind, route, requestId, status, cost) {
|
|
497
|
+
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
498
|
+
emit({
|
|
499
|
+
level: "info",
|
|
500
|
+
event: "provider_request_completed",
|
|
501
|
+
providerId: provider.id,
|
|
502
|
+
kind,
|
|
503
|
+
route,
|
|
504
|
+
...(requestId ? { requestId } : {}),
|
|
505
|
+
status,
|
|
506
|
+
...cost,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
function toJsonSuccessResponse(result, ctx) {
|
|
510
|
+
if (result instanceof Response) {
|
|
511
|
+
return result;
|
|
512
|
+
}
|
|
513
|
+
if (result instanceof ReadableStream) {
|
|
514
|
+
return new Response(result);
|
|
515
|
+
}
|
|
516
|
+
const cacheMeta = ctx?.cache.responseMeta();
|
|
517
|
+
const retryMeta = ctx ? retryResponseMeta.get(ctx) : undefined;
|
|
518
|
+
const meta = cacheMeta || retryMeta
|
|
519
|
+
? {
|
|
520
|
+
...(cacheMeta
|
|
521
|
+
? {
|
|
522
|
+
cached: cacheMeta.hit,
|
|
523
|
+
stale: cacheMeta.stale,
|
|
524
|
+
cache: cacheMeta,
|
|
525
|
+
}
|
|
526
|
+
: {}),
|
|
527
|
+
...(retryMeta ? { retry: retryMeta } : {}),
|
|
528
|
+
}
|
|
529
|
+
: undefined;
|
|
530
|
+
return {
|
|
531
|
+
data: result,
|
|
532
|
+
...(meta ? { meta } : {}),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function isAsyncIterable(value) {
|
|
536
|
+
if (!value || typeof value !== "object")
|
|
537
|
+
return false;
|
|
538
|
+
const iterator = Reflect.get(value, Symbol.asyncIterator);
|
|
539
|
+
return typeof iterator === "function";
|
|
540
|
+
}
|
|
541
|
+
function responseWithCleanup(response, cleanup) {
|
|
542
|
+
if (!response.body) {
|
|
543
|
+
void cleanup();
|
|
544
|
+
return response;
|
|
545
|
+
}
|
|
546
|
+
const reader = response.body.getReader();
|
|
547
|
+
let cleaned = false;
|
|
548
|
+
const runCleanup = async () => {
|
|
549
|
+
if (cleaned)
|
|
550
|
+
return;
|
|
551
|
+
cleaned = true;
|
|
552
|
+
await cleanup();
|
|
553
|
+
};
|
|
554
|
+
const body = new ReadableStream({
|
|
555
|
+
async pull(controller) {
|
|
556
|
+
try {
|
|
557
|
+
const { done, value } = await reader.read();
|
|
558
|
+
if (done) {
|
|
559
|
+
controller.close();
|
|
560
|
+
await runCleanup();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (value)
|
|
564
|
+
controller.enqueue(value);
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
await runCleanup();
|
|
568
|
+
controller.error(error);
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
async cancel(reason) {
|
|
572
|
+
try {
|
|
573
|
+
await reader.cancel(reason);
|
|
574
|
+
}
|
|
575
|
+
finally {
|
|
576
|
+
await runCleanup();
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
return new Response(body, {
|
|
581
|
+
headers: response.headers,
|
|
582
|
+
status: response.status,
|
|
583
|
+
statusText: response.statusText,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
async function validateSseEvent(operation, event) {
|
|
587
|
+
const transport = getSseTransport(operation);
|
|
588
|
+
const schema = transport?.events?.[event.event];
|
|
589
|
+
if (!schema) {
|
|
590
|
+
if (event.event === APIFUSE_STREAM_ERROR_EVENT ||
|
|
591
|
+
event.event === APIFUSE_STREAM_DONE_EVENT) {
|
|
592
|
+
return event;
|
|
593
|
+
}
|
|
594
|
+
throw new ProviderError(`SSE event "${event.event}" is not declared in operation transport.events.`, {
|
|
595
|
+
code: "SSE_EVENT_UNDECLARED",
|
|
596
|
+
category: "output_validation",
|
|
597
|
+
retryable: false,
|
|
598
|
+
fix: `Add "${event.event}" to transport.events or stop emitting that event.`,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
const data = await parseSchema(schema, event.data, `transport.events.${event.event}`);
|
|
602
|
+
return { ...event, data };
|
|
603
|
+
}
|
|
604
|
+
function byteLength(value) {
|
|
605
|
+
if (typeof value === "string") {
|
|
606
|
+
return new TextEncoder().encode(value).byteLength;
|
|
607
|
+
}
|
|
608
|
+
return value.byteLength;
|
|
609
|
+
}
|
|
610
|
+
function assertStreamPayloadWithinLimit(actualBytes, maxBytes, kind) {
|
|
611
|
+
if (maxBytes === undefined || actualBytes <= maxBytes)
|
|
612
|
+
return;
|
|
613
|
+
throw new ProviderError(`Stream ${kind} exceeded declared byte limit (${actualBytes} > ${maxBytes}).`, {
|
|
614
|
+
code: kind === "event" ? "STREAM_EVENT_TOO_LARGE" : "STREAM_CHUNK_TOO_LARGE",
|
|
615
|
+
retryable: false,
|
|
616
|
+
category: "input_validation",
|
|
617
|
+
fix: kind === "event"
|
|
618
|
+
? "Emit smaller SSE events or increase transport.maxEventBytes."
|
|
619
|
+
: "Emit smaller stream chunks or increase transport.maxChunkBytes.",
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
function toSseResponse(operation, result, cleanup, requestId) {
|
|
623
|
+
const encoder = new TextEncoder();
|
|
624
|
+
const iterator = result[Symbol.asyncIterator]();
|
|
625
|
+
const transport = getSseTransport(operation);
|
|
626
|
+
let done = false;
|
|
627
|
+
let cleaned = false;
|
|
628
|
+
const runCleanup = async () => {
|
|
629
|
+
if (cleaned)
|
|
630
|
+
return;
|
|
631
|
+
cleaned = true;
|
|
632
|
+
await cleanup();
|
|
633
|
+
};
|
|
634
|
+
const body = new ReadableStream({
|
|
635
|
+
async pull(controller) {
|
|
636
|
+
try {
|
|
637
|
+
if (done) {
|
|
638
|
+
controller.close();
|
|
639
|
+
await runCleanup();
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const next = await iterator.next();
|
|
643
|
+
if (next.done) {
|
|
644
|
+
done = true;
|
|
645
|
+
controller.close();
|
|
646
|
+
await runCleanup();
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const validated = await validateSseEvent(operation, next.value);
|
|
650
|
+
const encodedEvent = encodeSseEvent(validated);
|
|
651
|
+
const encodedBytes = encoder.encode(encodedEvent);
|
|
652
|
+
assertStreamPayloadWithinLimit(encodedBytes.byteLength, transport?.maxEventBytes, "event");
|
|
653
|
+
controller.enqueue(encodedBytes);
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
const message = error instanceof Error ? error.message : "Stream failed";
|
|
657
|
+
controller.enqueue(encoder.encode(encodeSseEvent(streamError("stream_error", message, {
|
|
658
|
+
...(requestId ? { requestId } : {}),
|
|
659
|
+
}))));
|
|
660
|
+
controller.close();
|
|
661
|
+
done = true;
|
|
662
|
+
await runCleanup();
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
async cancel(reason) {
|
|
666
|
+
try {
|
|
667
|
+
await iterator.return?.(reason);
|
|
668
|
+
}
|
|
669
|
+
finally {
|
|
670
|
+
await runCleanup();
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
return new Response(body, {
|
|
675
|
+
headers: {
|
|
676
|
+
"Cache-Control": "no-cache, no-transform",
|
|
677
|
+
Connection: "keep-alive",
|
|
678
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
679
|
+
},
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
function enforceStreamChunkLimit(body, maxChunkBytes) {
|
|
683
|
+
if (maxChunkBytes === undefined)
|
|
684
|
+
return body;
|
|
685
|
+
const reader = body.getReader();
|
|
686
|
+
return new ReadableStream({
|
|
687
|
+
async pull(controller) {
|
|
688
|
+
try {
|
|
689
|
+
const { done, value } = await reader.read();
|
|
690
|
+
if (done) {
|
|
691
|
+
controller.close();
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (value) {
|
|
695
|
+
assertStreamPayloadWithinLimit(byteLength(value), maxChunkBytes, "chunk");
|
|
696
|
+
controller.enqueue(value);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
controller.error(error);
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
cancel(reason) {
|
|
704
|
+
return reader.cancel(reason);
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
function toStreamingResponse(operation, result, cleanup, requestId) {
|
|
709
|
+
const transport = operation.transport?.kind ?? "json";
|
|
710
|
+
if (transport === "sse" &&
|
|
711
|
+
(result instanceof Response || result instanceof ReadableStream)) {
|
|
712
|
+
void cleanup();
|
|
713
|
+
throw new ProviderError("SSE operations must return an AsyncIterable of typed stream.event(...) values.", {
|
|
714
|
+
code: "SSE_RESULT_UNSUPPORTED",
|
|
715
|
+
category: "output_validation",
|
|
716
|
+
retryable: false,
|
|
717
|
+
fix: "Return an async generator that yields stream.event(name, data) so APIFuse can validate event schemas and enforce event byte limits.",
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
if (result instanceof Response) {
|
|
721
|
+
const httpTransport = getHttpStreamTransport(operation);
|
|
722
|
+
if (httpTransport &&
|
|
723
|
+
result.body &&
|
|
724
|
+
httpTransport?.maxChunkBytes !== undefined) {
|
|
725
|
+
return responseWithCleanup(new Response(enforceStreamChunkLimit(result.body, httpTransport.maxChunkBytes), {
|
|
726
|
+
headers: result.headers,
|
|
727
|
+
status: result.status,
|
|
728
|
+
statusText: result.statusText,
|
|
729
|
+
}), cleanup);
|
|
730
|
+
}
|
|
731
|
+
return responseWithCleanup(result, cleanup);
|
|
732
|
+
}
|
|
733
|
+
if (result instanceof ReadableStream) {
|
|
734
|
+
const httpTransport = getHttpStreamTransport(operation);
|
|
735
|
+
const stream = httpTransport !== undefined
|
|
736
|
+
? enforceStreamChunkLimit(result, httpTransport.maxChunkBytes)
|
|
737
|
+
: result;
|
|
738
|
+
return responseWithCleanup(new Response(stream, {
|
|
739
|
+
headers: transport === "sse"
|
|
740
|
+
? { "Content-Type": "text/event-stream; charset=utf-8" }
|
|
741
|
+
: {
|
|
742
|
+
"Content-Type": operation.transport?.kind === "http-stream"
|
|
743
|
+
? (operation.transport.contentType ??
|
|
744
|
+
"application/octet-stream")
|
|
745
|
+
: "application/octet-stream",
|
|
746
|
+
},
|
|
747
|
+
}), cleanup);
|
|
748
|
+
}
|
|
749
|
+
if (transport === "sse" && isAsyncIterable(result)) {
|
|
750
|
+
return toSseResponse(operation, result, cleanup, requestId);
|
|
751
|
+
}
|
|
752
|
+
void cleanup();
|
|
753
|
+
throw new ProviderError(`Streaming operation returned unsupported result for transport "${transport}"`, {
|
|
754
|
+
code: "STREAM_RESULT_UNSUPPORTED",
|
|
755
|
+
fix: "Return an AsyncIterable of stream.event(...) values, a ReadableStream, or a Response from streaming operations.",
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
function getSseTransport(operation) {
|
|
759
|
+
return operation.transport?.kind === "sse" ? operation.transport : undefined;
|
|
760
|
+
}
|
|
761
|
+
function getHttpStreamTransport(operation) {
|
|
762
|
+
return operation.transport?.kind === "http-stream"
|
|
763
|
+
? operation.transport
|
|
764
|
+
: undefined;
|
|
765
|
+
}
|
|
766
|
+
function toAuthFlowResponse(result, contextPatch) {
|
|
767
|
+
if (result instanceof Response) {
|
|
768
|
+
return result;
|
|
769
|
+
}
|
|
770
|
+
if (result instanceof ReadableStream) {
|
|
771
|
+
return new Response(result);
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
data: result,
|
|
775
|
+
...(contextPatch ? { contextPatch } : {}),
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
function authFlowLocaleFromHeaders(headers) {
|
|
779
|
+
const header = Object.entries(headers ?? {}).find(([key]) => key.toLowerCase() === "accept-language")?.[1];
|
|
780
|
+
for (const token of (header ?? "").split(",")) {
|
|
781
|
+
const language = token.trim().split(";")[0]?.split("-")[0]?.toLowerCase();
|
|
782
|
+
if (isAuthFlowLocale(language)) {
|
|
783
|
+
return language;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return "en";
|
|
787
|
+
}
|
|
788
|
+
function isAuthFlowLocale(value) {
|
|
789
|
+
return value === "en" || value === "ko" || value === "ja";
|
|
790
|
+
}
|
|
791
|
+
function isAuthTurn(value) {
|
|
792
|
+
return (!!value && typeof value === "object" && "kind" in value && "turnId" in value);
|
|
793
|
+
}
|
|
794
|
+
function loadAuthFlowLocaleCatalogs(provider) {
|
|
795
|
+
for (const providerDir of [
|
|
796
|
+
process.cwd(),
|
|
797
|
+
join(process.cwd(), "providers", provider.id),
|
|
798
|
+
join(process.cwd(), "providers-staging", provider.id),
|
|
799
|
+
]) {
|
|
800
|
+
if (!existsSync(join(providerDir, "locales", "en.json")))
|
|
801
|
+
continue;
|
|
802
|
+
try {
|
|
803
|
+
return loadProviderLocaleCatalogs({
|
|
804
|
+
providerDir,
|
|
805
|
+
locales: AUTH_FLOW_LOCALES,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
catch {
|
|
809
|
+
return undefined;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return undefined;
|
|
813
|
+
}
|
|
814
|
+
function materializeAuthFlowTurn(provider, request, turn) {
|
|
815
|
+
const catalogs = loadAuthFlowLocaleCatalogs(provider);
|
|
816
|
+
if (!catalogs)
|
|
817
|
+
return turn;
|
|
818
|
+
return localizeAuthTurn(turn, {
|
|
819
|
+
catalogs,
|
|
820
|
+
locale: authFlowLocaleFromHeaders(request.headers),
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
function withAuthRequestHeaders(request, headers) {
|
|
824
|
+
return {
|
|
825
|
+
...request,
|
|
826
|
+
headers: {
|
|
827
|
+
...(request.headers ?? {}),
|
|
828
|
+
...Object.fromEntries(headers.entries()),
|
|
829
|
+
},
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
async function handleOperation(provider, request, operationId, options = {}, state = createUnsupportedProviderRuntimeState(), proxyTelemetry) {
|
|
833
|
+
const ctx = createProviderContext(provider, request, operationId, options, state, proxyTelemetry);
|
|
834
|
+
const operation = provider.operations[operationId];
|
|
835
|
+
const streaming = operation?.transport?.kind && operation.transport.kind !== "json";
|
|
836
|
+
let cleanupCalled = false;
|
|
837
|
+
const cleanup = async () => {
|
|
838
|
+
if (cleanupCalled)
|
|
839
|
+
return;
|
|
840
|
+
cleanupCalled = true;
|
|
841
|
+
try {
|
|
842
|
+
ctx.stealth.close?.();
|
|
843
|
+
}
|
|
844
|
+
catch (error) {
|
|
845
|
+
logProviderCleanupError(options.logger, provider, operationId, request.requestId, "stealth", error);
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
await ctx.browser.close?.();
|
|
849
|
+
}
|
|
850
|
+
catch (error) {
|
|
851
|
+
logProviderCleanupError(options.logger, provider, operationId, request.requestId, "browser", error);
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
try {
|
|
855
|
+
const result = await executeOperation(provider, operationId, ctx, request.input);
|
|
856
|
+
if (streaming && operation) {
|
|
857
|
+
return toStreamingResponse(operation, result, cleanup, request.requestId);
|
|
858
|
+
}
|
|
859
|
+
return toJsonSuccessResponse(result, ctx);
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
await cleanup();
|
|
863
|
+
throw error;
|
|
864
|
+
}
|
|
865
|
+
finally {
|
|
866
|
+
if (!streaming)
|
|
867
|
+
await cleanup();
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
function responseWithProviderTelemetry(response, proxyTelemetry) {
|
|
871
|
+
const headerValue = proxyTelemetry?.toHeaderValue();
|
|
872
|
+
const headers = new Headers(response.headers);
|
|
873
|
+
headers.delete(PROVIDER_TELEMETRY_HEADER);
|
|
874
|
+
if (headerValue)
|
|
875
|
+
headers.set(PROVIDER_TELEMETRY_HEADER, headerValue);
|
|
876
|
+
return new Response(response.body, {
|
|
877
|
+
headers,
|
|
878
|
+
status: response.status,
|
|
879
|
+
statusText: response.statusText,
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
async function handleAuthFlow(provider, request, route, options = {}) {
|
|
883
|
+
const flow = provider.auth?.flow;
|
|
884
|
+
if (!flow) {
|
|
885
|
+
throw new ProviderError("Auth flow is not configured", {
|
|
886
|
+
code: "AUTH_FLOW_NOT_CONFIGURED",
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
const { context, getPatch } = createAuthFlowContext(provider, request, options);
|
|
890
|
+
try {
|
|
891
|
+
const result = route === "start"
|
|
892
|
+
? await flow.start(context)
|
|
893
|
+
: route === "continue"
|
|
894
|
+
? await flow.continue(context, request.input ?? {})
|
|
895
|
+
: route === "poll"
|
|
896
|
+
? flow.poll
|
|
897
|
+
? await flow.poll(context)
|
|
898
|
+
: null
|
|
899
|
+
: route === "abort"
|
|
900
|
+
? flow.abort
|
|
901
|
+
? await flow.abort(context)
|
|
902
|
+
: null
|
|
903
|
+
: flow.refresh
|
|
904
|
+
? await flow.refresh(context, request.input ?? {})
|
|
905
|
+
: null;
|
|
906
|
+
if (route === "refresh" && !flow.refresh) {
|
|
907
|
+
throw new AuthError("Provider auth flow does not support refresh.", {
|
|
908
|
+
code: "refresh_not_supported",
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
const materializedResult = result &&
|
|
912
|
+
!(result instanceof Response) &&
|
|
913
|
+
!(result instanceof ReadableStream) &&
|
|
914
|
+
isAuthTurn(result)
|
|
915
|
+
? materializeAuthFlowTurn(provider, request, result)
|
|
916
|
+
: result;
|
|
917
|
+
return toAuthFlowResponse(materializedResult, getPatch());
|
|
918
|
+
}
|
|
919
|
+
finally {
|
|
920
|
+
context.stealth.close?.();
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
export function createServerApp(provider, options = {}) {
|
|
924
|
+
const app = new Hono();
|
|
925
|
+
const logger = options.logger ?? defaultProviderServerLogger;
|
|
926
|
+
const state = options.state ??
|
|
927
|
+
createProviderRuntimeStateFromEnv({
|
|
928
|
+
providerId: provider.id,
|
|
929
|
+
allowMemoryFallback: options.allowMemoryStateFallback === true,
|
|
930
|
+
});
|
|
931
|
+
app.notFound((c) => c.json({
|
|
932
|
+
error: {
|
|
933
|
+
code: "not_found",
|
|
934
|
+
message: "Not found",
|
|
935
|
+
},
|
|
936
|
+
}, 404));
|
|
937
|
+
app.get("/health", (c) => c.json({
|
|
938
|
+
status: "ok",
|
|
939
|
+
provider: provider.id,
|
|
940
|
+
version: provider.version,
|
|
941
|
+
}));
|
|
942
|
+
app.post("/v1/:operation", async (c) => {
|
|
943
|
+
let rawBody;
|
|
944
|
+
const operation = c.req.param("operation");
|
|
945
|
+
const proxyTelemetry = new ProxyTelemetryCollector();
|
|
946
|
+
const requestCost = startRequestCost();
|
|
947
|
+
try {
|
|
948
|
+
rawBody = await c.req.raw
|
|
949
|
+
.clone()
|
|
950
|
+
.json()
|
|
951
|
+
.catch(() => undefined);
|
|
952
|
+
const body = OperationRequestSchema.parse(rawBody);
|
|
953
|
+
const requestHeaders = Object.fromEntries(c.req.raw.headers.entries());
|
|
954
|
+
body.headers = { ...requestHeaders, ...body.headers };
|
|
955
|
+
const response = await handleOperation(provider, body, operation, options, state, proxyTelemetry);
|
|
956
|
+
if (response instanceof Response) {
|
|
957
|
+
logProviderSuccess(logger, provider, "operation", operation, body.requestId, response.status, finishRequestCost(requestCost));
|
|
958
|
+
return responseWithProviderTelemetry(response, proxyTelemetry);
|
|
959
|
+
}
|
|
960
|
+
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
961
|
+
if (telemetryHeader)
|
|
962
|
+
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
963
|
+
logProviderSuccess(logger, provider, "operation", operation, body.requestId, 200, finishRequestCost(requestCost));
|
|
964
|
+
return c.json(response);
|
|
965
|
+
}
|
|
966
|
+
catch (error) {
|
|
967
|
+
const status = toStatusCode(error);
|
|
968
|
+
const requestId = extractRequestId(rawBody);
|
|
969
|
+
logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost));
|
|
970
|
+
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
971
|
+
if (telemetryHeader)
|
|
972
|
+
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
973
|
+
return c.json(toErrorResponse(error, requestId), status);
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
app.post("/auth/start", async (c) => {
|
|
977
|
+
let rawBody;
|
|
978
|
+
const requestCost = startRequestCost();
|
|
979
|
+
try {
|
|
980
|
+
rawBody = await c.req.raw
|
|
981
|
+
.clone()
|
|
982
|
+
.json()
|
|
983
|
+
.catch(() => undefined);
|
|
984
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
985
|
+
const response = await handleAuthFlow(provider, body, "start", options);
|
|
986
|
+
logProviderSuccess(logger, provider, "auth", "start", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
987
|
+
return response instanceof Response ? response : c.json(response);
|
|
988
|
+
}
|
|
989
|
+
catch (error) {
|
|
990
|
+
const status = toStatusCode(error);
|
|
991
|
+
const requestId = extractRequestId(rawBody);
|
|
992
|
+
logProviderError(logger, provider, "auth", "start", requestId, error, status, finishRequestCost(requestCost));
|
|
993
|
+
return c.json(toErrorResponse(error, requestId), status);
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
app.post("/auth/continue", async (c) => {
|
|
997
|
+
let rawBody;
|
|
998
|
+
const requestCost = startRequestCost();
|
|
999
|
+
try {
|
|
1000
|
+
rawBody = await c.req.raw
|
|
1001
|
+
.clone()
|
|
1002
|
+
.json()
|
|
1003
|
+
.catch(() => undefined);
|
|
1004
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1005
|
+
const response = await handleAuthFlow(provider, body, "continue", options);
|
|
1006
|
+
logProviderSuccess(logger, provider, "auth", "continue", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
1007
|
+
return response instanceof Response ? response : c.json(response);
|
|
1008
|
+
}
|
|
1009
|
+
catch (error) {
|
|
1010
|
+
const status = toStatusCode(error);
|
|
1011
|
+
const requestId = extractRequestId(rawBody);
|
|
1012
|
+
logProviderError(logger, provider, "auth", "continue", requestId, error, status, finishRequestCost(requestCost));
|
|
1013
|
+
return c.json(toErrorResponse(error, requestId), status);
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
app.post("/auth/poll", async (c) => {
|
|
1017
|
+
let rawBody;
|
|
1018
|
+
const requestCost = startRequestCost();
|
|
1019
|
+
try {
|
|
1020
|
+
rawBody = await c.req.raw
|
|
1021
|
+
.clone()
|
|
1022
|
+
.json()
|
|
1023
|
+
.catch(() => undefined);
|
|
1024
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1025
|
+
const response = await handleAuthFlow(provider, body, "poll", options);
|
|
1026
|
+
logProviderSuccess(logger, provider, "auth", "poll", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
1027
|
+
return response instanceof Response ? response : c.json(response);
|
|
1028
|
+
}
|
|
1029
|
+
catch (error) {
|
|
1030
|
+
const status = toStatusCode(error);
|
|
1031
|
+
const requestId = extractRequestId(rawBody);
|
|
1032
|
+
logProviderError(logger, provider, "auth", "poll", requestId, error, status, finishRequestCost(requestCost));
|
|
1033
|
+
return c.json(toErrorResponse(error, requestId), status);
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
app.post("/auth/refresh", async (c) => {
|
|
1037
|
+
let rawBody;
|
|
1038
|
+
const requestCost = startRequestCost();
|
|
1039
|
+
try {
|
|
1040
|
+
rawBody = await c.req.raw
|
|
1041
|
+
.clone()
|
|
1042
|
+
.json()
|
|
1043
|
+
.catch(() => undefined);
|
|
1044
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1045
|
+
const response = await handleAuthFlow(provider, body, "refresh", options);
|
|
1046
|
+
logProviderSuccess(logger, provider, "auth", "refresh", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
1047
|
+
return response instanceof Response ? response : c.json(response);
|
|
1048
|
+
}
|
|
1049
|
+
catch (error) {
|
|
1050
|
+
const status = toStatusCode(error);
|
|
1051
|
+
const requestId = extractRequestId(rawBody);
|
|
1052
|
+
logProviderError(logger, provider, "auth", "refresh", requestId, error, status, finishRequestCost(requestCost));
|
|
1053
|
+
return c.json(toErrorResponse(error, requestId), status);
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
app.post("/auth/disconnect", async (c) => {
|
|
1057
|
+
let rawBody;
|
|
1058
|
+
const requestCost = startRequestCost();
|
|
1059
|
+
try {
|
|
1060
|
+
rawBody = await c.req.raw
|
|
1061
|
+
.clone()
|
|
1062
|
+
.json()
|
|
1063
|
+
.catch(() => undefined);
|
|
1064
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1065
|
+
const response = await handleAuthFlow(provider, body, "abort", options);
|
|
1066
|
+
logProviderSuccess(logger, provider, "auth", "disconnect", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
1067
|
+
return response instanceof Response ? response : c.json(response);
|
|
1068
|
+
}
|
|
1069
|
+
catch (error) {
|
|
1070
|
+
const status = toStatusCode(error);
|
|
1071
|
+
const requestId = extractRequestId(rawBody);
|
|
1072
|
+
logProviderError(logger, provider, "auth", "disconnect", requestId, error, status, finishRequestCost(requestCost));
|
|
1073
|
+
return c.json(toErrorResponse(error, requestId), status);
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
return app;
|
|
1077
|
+
}
|
|
1078
|
+
function getBunServeRuntime() {
|
|
1079
|
+
const bunValue = Object.getOwnPropertyDescriptor(globalThis, "Bun")?.value;
|
|
1080
|
+
if (!bunValue || typeof bunValue !== "object") {
|
|
1081
|
+
return undefined;
|
|
1082
|
+
}
|
|
1083
|
+
const serve = Object.getOwnPropertyDescriptor(bunValue, "serve")?.value;
|
|
1084
|
+
if (typeof serve !== "function") {
|
|
1085
|
+
return undefined;
|
|
1086
|
+
}
|
|
1087
|
+
return {
|
|
1088
|
+
serve(options) {
|
|
1089
|
+
return serve(options);
|
|
1090
|
+
},
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
export async function serve(provider, options = {}) {
|
|
1094
|
+
const bunRuntime = getBunServeRuntime();
|
|
1095
|
+
if (bunRuntime === undefined) {
|
|
1096
|
+
throw new ProviderError("Bun runtime is required to start the provider server", {
|
|
1097
|
+
code: "RUNTIME_UNSUPPORTED",
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
const app = createServerApp(provider, {
|
|
1101
|
+
logger: options.logger,
|
|
1102
|
+
stt: options.stt,
|
|
1103
|
+
});
|
|
1104
|
+
bunRuntime.serve({
|
|
1105
|
+
port: options.port ?? DEFAULT_PORT,
|
|
1106
|
+
hostname: options.host ?? DEFAULT_HOST,
|
|
1107
|
+
fetch: app.fetch,
|
|
1108
|
+
});
|
|
1109
|
+
await Promise.resolve();
|
|
1110
|
+
}
|