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