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