@apifuse/provider-sdk 2.1.0-beta.4 → 2.1.0-beta.6
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 +24 -0
- package/CHANGELOG.md +11 -0
- package/README.md +23 -2
- package/SUBMISSION.md +2 -1
- package/bin/apifuse-check.ts +60 -6
- package/bin/apifuse-dev.ts +48 -5
- package/bin/apifuse-perf.ts +106 -26
- package/bin/apifuse-record.ts +142 -52
- package/bin/apifuse-submit-check.ts +1489 -3
- package/package.json +107 -92
- package/src/ceremonies/index.ts +8 -2
- package/src/choice-token.ts +1 -0
- package/src/cli/commands.ts +10 -8
- package/src/cli/create.ts +49 -1
- 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 +18 -0
- package/src/cli/templates/provider/operations/ping.ts.tpl +3 -2
- package/src/cli/templates/provider/schemas/ping.ts.tpl +8 -0
- package/src/config/loader.ts +19 -1
- 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 +40 -5
- package/src/errors.ts +15 -0
- package/src/i18n/catalog.ts +156 -0
- package/src/index.ts +22 -1
- package/src/lint.ts +265 -46
- package/src/provider.ts +45 -2
- package/src/runtime/browser.ts +685 -30
- package/src/runtime/cache.ts +35 -89
- package/src/runtime/choice.ts +760 -0
- package/src/runtime/executor.ts +19 -2
- package/src/runtime/redis.ts +116 -0
- package/src/runtime/state.ts +487 -0
- package/src/runtime/stealth.ts +8 -1
- package/src/runtime/trace.ts +1 -1
- package/src/server/serve.ts +361 -46
- package/src/server/types.ts +2 -0
- package/src/testing/run.ts +16 -3
- package/src/types.ts +225 -18
package/src/server/serve.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
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
|
-
import {
|
|
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";
|
|
4
18
|
import {
|
|
5
19
|
categoryForStatus,
|
|
6
20
|
isRetryableCategory,
|
|
@@ -10,6 +24,10 @@ import {
|
|
|
10
24
|
import { createScratchpad } from "../runtime/auth-flow";
|
|
11
25
|
import { createBrowserClient } from "../runtime/browser";
|
|
12
26
|
import { createProviderCache } from "../runtime/cache";
|
|
27
|
+
import {
|
|
28
|
+
createProviderChoiceContext,
|
|
29
|
+
PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
|
|
30
|
+
} from "../runtime/choice";
|
|
13
31
|
import { createCredentialContext } from "../runtime/credential";
|
|
14
32
|
import { createEnvContext } from "../runtime/env";
|
|
15
33
|
import { executeOperation } from "../runtime/executor";
|
|
@@ -25,7 +43,10 @@ import {
|
|
|
25
43
|
PROVIDER_TELEMETRY_HEADER,
|
|
26
44
|
ProxyTelemetryCollector,
|
|
27
45
|
} from "../runtime/proxy-telemetry";
|
|
28
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
createProviderRuntimeStateFromEnv,
|
|
48
|
+
createUnsupportedProviderRuntimeState,
|
|
49
|
+
} from "../runtime/state";
|
|
29
50
|
import { createStealthClient } from "../runtime/stealth";
|
|
30
51
|
import { createSttClientFromEnv } from "../runtime/stt";
|
|
31
52
|
import { createTraceContext } from "../runtime/trace";
|
|
@@ -39,6 +60,7 @@ import {
|
|
|
39
60
|
} from "../stream";
|
|
40
61
|
import type {
|
|
41
62
|
AuthContext,
|
|
63
|
+
AuthTurn,
|
|
42
64
|
BrowserClient,
|
|
43
65
|
FlowContext,
|
|
44
66
|
FlowContextStore,
|
|
@@ -48,6 +70,7 @@ import type {
|
|
|
48
70
|
OperationSseTransport,
|
|
49
71
|
ProviderContext,
|
|
50
72
|
ProviderDefinition,
|
|
73
|
+
ProviderRuntimeState,
|
|
51
74
|
ProviderStreamEvent,
|
|
52
75
|
StealthClient,
|
|
53
76
|
SttContext,
|
|
@@ -66,8 +89,11 @@ import {
|
|
|
66
89
|
|
|
67
90
|
const DEFAULT_HOST = "0.0.0.0";
|
|
68
91
|
const DEFAULT_PORT = 3000;
|
|
92
|
+
const AUTH_FLOW_LOCALES = ["en", "ko", "ja"] as const;
|
|
69
93
|
const retryResponseMeta = new WeakMap<ProviderContext, HttpRetrySummary>();
|
|
70
94
|
|
|
95
|
+
type RequestCleanup = () => void | Promise<void>;
|
|
96
|
+
|
|
71
97
|
function createAuthStub(): AuthContext {
|
|
72
98
|
return {
|
|
73
99
|
async requestField(name) {
|
|
@@ -81,11 +107,27 @@ function createAuthStub(): AuthContext {
|
|
|
81
107
|
function createBrowserStub(): BrowserClient {
|
|
82
108
|
return {
|
|
83
109
|
engine: "playwright-stealth",
|
|
110
|
+
async close() {},
|
|
84
111
|
async newPage() {
|
|
85
112
|
throw new ProviderError("Browser runtime is not available", {
|
|
86
113
|
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
87
114
|
});
|
|
88
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
|
+
},
|
|
89
131
|
};
|
|
90
132
|
}
|
|
91
133
|
|
|
@@ -124,6 +166,23 @@ function getProviderStealthProfile(provider: ProviderDefinition) {
|
|
|
124
166
|
: undefined;
|
|
125
167
|
}
|
|
126
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
|
+
|
|
127
186
|
export function resolveProviderProxyAffinityKey(
|
|
128
187
|
provider: ProviderDefinition,
|
|
129
188
|
request: OperationRequest,
|
|
@@ -146,6 +205,7 @@ function createProviderContext(
|
|
|
146
205
|
request: OperationRequest,
|
|
147
206
|
operationId: string,
|
|
148
207
|
options: ProviderServerOptions = {},
|
|
208
|
+
state: ProviderRuntimeState = createUnsupportedProviderRuntimeState(),
|
|
149
209
|
proxyTelemetry?: ProxyTelemetryCollector,
|
|
150
210
|
): ProviderContext {
|
|
151
211
|
const baseUrl = getProviderBaseUrl(provider);
|
|
@@ -167,18 +227,24 @@ function createProviderContext(
|
|
|
167
227
|
telemetry: proxyTelemetry,
|
|
168
228
|
};
|
|
169
229
|
|
|
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
|
+
};
|
|
170
244
|
const context = wrapWithInstrumentation({
|
|
171
|
-
env
|
|
172
|
-
credential
|
|
173
|
-
|
|
174
|
-
mode: request.connection?.mode,
|
|
175
|
-
scopes: request.connection?.scopes,
|
|
176
|
-
values: request.connection?.secrets,
|
|
177
|
-
}),
|
|
178
|
-
request: {
|
|
179
|
-
connectionId: request.connection?.id,
|
|
180
|
-
headers: request.headers ?? {},
|
|
181
|
-
},
|
|
245
|
+
env,
|
|
246
|
+
credential,
|
|
247
|
+
request: requestContext,
|
|
182
248
|
http: createHttpClient(baseUrl, {
|
|
183
249
|
...proxyClientOptions,
|
|
184
250
|
onRetrySummary: (summary) => {
|
|
@@ -187,7 +253,7 @@ function createProviderContext(
|
|
|
187
253
|
},
|
|
188
254
|
}),
|
|
189
255
|
cache: createProviderCache({ providerId: provider.id }),
|
|
190
|
-
state
|
|
256
|
+
state,
|
|
191
257
|
stealth: stealthBaseUrl
|
|
192
258
|
? stealthProfile
|
|
193
259
|
? createStealthClient(
|
|
@@ -200,8 +266,10 @@ function createProviderContext(
|
|
|
200
266
|
browser:
|
|
201
267
|
provider.runtime === "browser"
|
|
202
268
|
? createBrowserClient({
|
|
269
|
+
allowedHosts: provider.allowedHosts,
|
|
203
270
|
cdpUrl: process.env.APIFUSE__CDP_POOL__URL,
|
|
204
271
|
headless: true,
|
|
272
|
+
requireCdpPool: isProductionProviderBrowserMode(provider),
|
|
205
273
|
stealth: true,
|
|
206
274
|
engine: provider.browser?.engine,
|
|
207
275
|
})
|
|
@@ -209,6 +277,13 @@ function createProviderContext(
|
|
|
209
277
|
trace: createTraceContext(),
|
|
210
278
|
auth: createAuthStub(),
|
|
211
279
|
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
|
280
|
+
choice: createProviderChoiceContext({
|
|
281
|
+
providerId: provider.id,
|
|
282
|
+
env,
|
|
283
|
+
request: requestContext,
|
|
284
|
+
credential,
|
|
285
|
+
state,
|
|
286
|
+
}),
|
|
212
287
|
});
|
|
213
288
|
wrappedContext = context;
|
|
214
289
|
return context;
|
|
@@ -279,6 +354,14 @@ function createAuthFlowContext(
|
|
|
279
354
|
upstream: proxyClientOptions.upstream,
|
|
280
355
|
affinityKey: proxyClientOptions.affinityKey,
|
|
281
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;
|
|
282
365
|
|
|
283
366
|
return {
|
|
284
367
|
context: {
|
|
@@ -297,6 +380,7 @@ function createAuthFlowContext(
|
|
|
297
380
|
: createStealthClient(stealthBaseUrl, stealthClientOptions)
|
|
298
381
|
: createStealthStub(),
|
|
299
382
|
env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
|
|
383
|
+
credential,
|
|
300
384
|
context: flowContextStore.context,
|
|
301
385
|
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
|
302
386
|
},
|
|
@@ -335,7 +419,18 @@ export type ProviderServerLogEvent =
|
|
|
335
419
|
taxonomyVersion?: string;
|
|
336
420
|
retryable?: boolean;
|
|
337
421
|
issues?: Array<{ path: string; code: string; message: string }>;
|
|
338
|
-
})
|
|
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
|
+
};
|
|
339
434
|
|
|
340
435
|
export type ProviderServerLogger = (event: ProviderServerLogEvent) => void;
|
|
341
436
|
|
|
@@ -343,6 +438,10 @@ export type ProviderServerOptions = {
|
|
|
343
438
|
logger?: ProviderServerLogger;
|
|
344
439
|
/** Optional STT override for tests or custom hosts; local/prod normally resolves from env. */
|
|
345
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;
|
|
346
445
|
};
|
|
347
446
|
|
|
348
447
|
const defaultProviderServerLogger: ProviderServerLogger = (event) => {
|
|
@@ -457,6 +556,18 @@ function providerObservabilityDetails(error: ProviderError):
|
|
|
457
556
|
upstreamStatus?: number;
|
|
458
557
|
}
|
|
459
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
|
+
}
|
|
460
571
|
if (!(error instanceof TransportError)) {
|
|
461
572
|
return undefined;
|
|
462
573
|
}
|
|
@@ -513,7 +624,9 @@ function publicProviderErrorMessage(error: ProviderError): string {
|
|
|
513
624
|
return error.message;
|
|
514
625
|
}
|
|
515
626
|
|
|
516
|
-
function toStatusCode(
|
|
627
|
+
function toStatusCode(
|
|
628
|
+
error: unknown,
|
|
629
|
+
): 400 | 401 | 404 | 429 | 500 | 502 | 503 | 504 {
|
|
517
630
|
if (error instanceof z.ZodError) {
|
|
518
631
|
return 400;
|
|
519
632
|
}
|
|
@@ -524,6 +637,9 @@ function toStatusCode(error: unknown): 400 | 404 | 429 | 500 | 502 | 503 | 504 {
|
|
|
524
637
|
|
|
525
638
|
if (error instanceof ProviderError) {
|
|
526
639
|
switch (error.code) {
|
|
640
|
+
case "AUTH_REQUIRED":
|
|
641
|
+
case "reauth_required":
|
|
642
|
+
return 401;
|
|
527
643
|
case "NOT_FOUND":
|
|
528
644
|
case "not_found":
|
|
529
645
|
case "NO_DATA":
|
|
@@ -533,6 +649,7 @@ function toStatusCode(error: unknown): 400 | 404 | 429 | 500 | 502 | 503 | 504 {
|
|
|
533
649
|
case "LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR":
|
|
534
650
|
return 429;
|
|
535
651
|
case "UPSTREAM_ERROR":
|
|
652
|
+
case "BLOCKED":
|
|
536
653
|
return 502;
|
|
537
654
|
case "STT_UNAVAILABLE":
|
|
538
655
|
case "UNSUPPORTED_STT_BACKEND":
|
|
@@ -604,6 +721,31 @@ function logProviderError(
|
|
|
604
721
|
});
|
|
605
722
|
}
|
|
606
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
|
+
|
|
607
749
|
function logProviderSuccess(
|
|
608
750
|
logger: ProviderServerLogger | unknown,
|
|
609
751
|
provider: ProviderDefinition,
|
|
@@ -670,18 +812,18 @@ function isAsyncIterable<T = unknown>(
|
|
|
670
812
|
|
|
671
813
|
function responseWithCleanup(
|
|
672
814
|
response: Response,
|
|
673
|
-
cleanup:
|
|
815
|
+
cleanup: RequestCleanup,
|
|
674
816
|
): Response {
|
|
675
817
|
if (!response.body) {
|
|
676
|
-
cleanup();
|
|
818
|
+
void cleanup();
|
|
677
819
|
return response;
|
|
678
820
|
}
|
|
679
821
|
const reader = response.body.getReader();
|
|
680
822
|
let cleaned = false;
|
|
681
|
-
const runCleanup = () => {
|
|
823
|
+
const runCleanup = async () => {
|
|
682
824
|
if (cleaned) return;
|
|
683
825
|
cleaned = true;
|
|
684
|
-
cleanup();
|
|
826
|
+
await cleanup();
|
|
685
827
|
};
|
|
686
828
|
const body = new ReadableStream<Uint8Array>({
|
|
687
829
|
async pull(controller) {
|
|
@@ -689,12 +831,12 @@ function responseWithCleanup(
|
|
|
689
831
|
const { done, value } = await reader.read();
|
|
690
832
|
if (done) {
|
|
691
833
|
controller.close();
|
|
692
|
-
runCleanup();
|
|
834
|
+
await runCleanup();
|
|
693
835
|
return;
|
|
694
836
|
}
|
|
695
837
|
if (value) controller.enqueue(value);
|
|
696
838
|
} catch (error) {
|
|
697
|
-
runCleanup();
|
|
839
|
+
await runCleanup();
|
|
698
840
|
controller.error(error);
|
|
699
841
|
}
|
|
700
842
|
},
|
|
@@ -702,7 +844,7 @@ function responseWithCleanup(
|
|
|
702
844
|
try {
|
|
703
845
|
await reader.cancel(reason);
|
|
704
846
|
} finally {
|
|
705
|
-
runCleanup();
|
|
847
|
+
await runCleanup();
|
|
706
848
|
}
|
|
707
849
|
},
|
|
708
850
|
});
|
|
@@ -775,7 +917,7 @@ function assertStreamPayloadWithinLimit(
|
|
|
775
917
|
function toSseResponse(
|
|
776
918
|
operation: OperationDefinition,
|
|
777
919
|
result: AsyncIterable<ProviderStreamEvent>,
|
|
778
|
-
cleanup:
|
|
920
|
+
cleanup: RequestCleanup,
|
|
779
921
|
requestId?: string,
|
|
780
922
|
): Response {
|
|
781
923
|
const encoder = new TextEncoder();
|
|
@@ -783,24 +925,24 @@ function toSseResponse(
|
|
|
783
925
|
const transport = getSseTransport(operation);
|
|
784
926
|
let done = false;
|
|
785
927
|
let cleaned = false;
|
|
786
|
-
const runCleanup = () => {
|
|
928
|
+
const runCleanup = async () => {
|
|
787
929
|
if (cleaned) return;
|
|
788
930
|
cleaned = true;
|
|
789
|
-
cleanup();
|
|
931
|
+
await cleanup();
|
|
790
932
|
};
|
|
791
933
|
const body = new ReadableStream<Uint8Array>({
|
|
792
934
|
async pull(controller) {
|
|
793
935
|
try {
|
|
794
936
|
if (done) {
|
|
795
937
|
controller.close();
|
|
796
|
-
runCleanup();
|
|
938
|
+
await runCleanup();
|
|
797
939
|
return;
|
|
798
940
|
}
|
|
799
941
|
const next = await iterator.next();
|
|
800
942
|
if (next.done) {
|
|
801
943
|
done = true;
|
|
802
944
|
controller.close();
|
|
803
|
-
runCleanup();
|
|
945
|
+
await runCleanup();
|
|
804
946
|
return;
|
|
805
947
|
}
|
|
806
948
|
const validated = await validateSseEvent(operation, next.value);
|
|
@@ -826,14 +968,14 @@ function toSseResponse(
|
|
|
826
968
|
);
|
|
827
969
|
controller.close();
|
|
828
970
|
done = true;
|
|
829
|
-
runCleanup();
|
|
971
|
+
await runCleanup();
|
|
830
972
|
}
|
|
831
973
|
},
|
|
832
974
|
async cancel(reason) {
|
|
833
975
|
try {
|
|
834
976
|
await iterator.return?.(reason);
|
|
835
977
|
} finally {
|
|
836
|
-
runCleanup();
|
|
978
|
+
await runCleanup();
|
|
837
979
|
}
|
|
838
980
|
},
|
|
839
981
|
});
|
|
@@ -881,7 +1023,7 @@ function enforceStreamChunkLimit(
|
|
|
881
1023
|
function toStreamingResponse(
|
|
882
1024
|
operation: OperationDefinition,
|
|
883
1025
|
result: unknown,
|
|
884
|
-
cleanup:
|
|
1026
|
+
cleanup: RequestCleanup,
|
|
885
1027
|
requestId?: string,
|
|
886
1028
|
): Response {
|
|
887
1029
|
const transport = operation.transport?.kind ?? "json";
|
|
@@ -889,7 +1031,7 @@ function toStreamingResponse(
|
|
|
889
1031
|
transport === "sse" &&
|
|
890
1032
|
(result instanceof Response || result instanceof ReadableStream)
|
|
891
1033
|
) {
|
|
892
|
-
cleanup();
|
|
1034
|
+
void cleanup();
|
|
893
1035
|
throw new ProviderError(
|
|
894
1036
|
"SSE operations must return an AsyncIterable of typed stream.event(...) values.",
|
|
895
1037
|
{
|
|
@@ -946,7 +1088,7 @@ function toStreamingResponse(
|
|
|
946
1088
|
if (transport === "sse" && isAsyncIterable<ProviderStreamEvent>(result)) {
|
|
947
1089
|
return toSseResponse(operation, result, cleanup, requestId);
|
|
948
1090
|
}
|
|
949
|
-
cleanup();
|
|
1091
|
+
void cleanup();
|
|
950
1092
|
throw new ProviderError(
|
|
951
1093
|
`Streaming operation returned unsupported result for transport "${transport}"`,
|
|
952
1094
|
{
|
|
@@ -988,11 +1130,84 @@ function toAuthFlowResponse(
|
|
|
988
1130
|
};
|
|
989
1131
|
}
|
|
990
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
|
+
|
|
991
1205
|
async function handleOperation(
|
|
992
1206
|
provider: ProviderDefinition,
|
|
993
1207
|
request: OperationRequest,
|
|
994
1208
|
operationId: string,
|
|
995
1209
|
options: ProviderServerOptions = {},
|
|
1210
|
+
state: ProviderRuntimeState = createUnsupportedProviderRuntimeState(),
|
|
996
1211
|
proxyTelemetry?: ProxyTelemetryCollector,
|
|
997
1212
|
): Promise<Response | OperationResponse> {
|
|
998
1213
|
const ctx = createProviderContext(
|
|
@@ -1000,16 +1215,40 @@ async function handleOperation(
|
|
|
1000
1215
|
request,
|
|
1001
1216
|
operationId,
|
|
1002
1217
|
options,
|
|
1218
|
+
state,
|
|
1003
1219
|
proxyTelemetry,
|
|
1004
1220
|
);
|
|
1005
1221
|
const operation = provider.operations[operationId];
|
|
1006
1222
|
const streaming =
|
|
1007
1223
|
operation?.transport?.kind && operation.transport.kind !== "json";
|
|
1008
1224
|
let cleanupCalled = false;
|
|
1009
|
-
const cleanup = () => {
|
|
1225
|
+
const cleanup = async () => {
|
|
1010
1226
|
if (cleanupCalled) return;
|
|
1011
1227
|
cleanupCalled = true;
|
|
1012
|
-
|
|
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
|
+
}
|
|
1013
1252
|
};
|
|
1014
1253
|
try {
|
|
1015
1254
|
const result = await executeOperation(
|
|
@@ -1023,10 +1262,10 @@ async function handleOperation(
|
|
|
1023
1262
|
}
|
|
1024
1263
|
return toJsonSuccessResponse(result, ctx);
|
|
1025
1264
|
} catch (error) {
|
|
1026
|
-
cleanup();
|
|
1265
|
+
await cleanup();
|
|
1027
1266
|
throw error;
|
|
1028
1267
|
} finally {
|
|
1029
|
-
if (!streaming) cleanup();
|
|
1268
|
+
if (!streaming) await cleanup();
|
|
1030
1269
|
}
|
|
1031
1270
|
}
|
|
1032
1271
|
|
|
@@ -1045,7 +1284,7 @@ function responseWithProviderTelemetry(
|
|
|
1045
1284
|
});
|
|
1046
1285
|
}
|
|
1047
1286
|
|
|
1048
|
-
type AuthRoute = "start" | "continue" | "poll" | "abort";
|
|
1287
|
+
type AuthRoute = "start" | "continue" | "poll" | "abort" | "refresh";
|
|
1049
1288
|
|
|
1050
1289
|
async function handleAuthFlow(
|
|
1051
1290
|
provider: ProviderDefinition,
|
|
@@ -1075,11 +1314,28 @@ async function handleAuthFlow(
|
|
|
1075
1314
|
? flow.poll
|
|
1076
1315
|
? await flow.poll(context)
|
|
1077
1316
|
: null
|
|
1078
|
-
:
|
|
1079
|
-
?
|
|
1080
|
-
|
|
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;
|
|
1324
|
+
|
|
1325
|
+
if (route === "refresh" && !flow.refresh) {
|
|
1326
|
+
throw new AuthError("Provider auth flow does not support refresh.", {
|
|
1327
|
+
code: "refresh_not_supported",
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1081
1330
|
|
|
1082
|
-
|
|
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());
|
|
1083
1339
|
} finally {
|
|
1084
1340
|
context.stealth.close?.();
|
|
1085
1341
|
}
|
|
@@ -1091,6 +1347,12 @@ export function createServerApp(
|
|
|
1091
1347
|
): Hono {
|
|
1092
1348
|
const app = new Hono();
|
|
1093
1349
|
const logger = options.logger ?? defaultProviderServerLogger;
|
|
1350
|
+
const state =
|
|
1351
|
+
options.state ??
|
|
1352
|
+
createProviderRuntimeStateFromEnv({
|
|
1353
|
+
providerId: provider.id,
|
|
1354
|
+
allowMemoryFallback: options.allowMemoryStateFallback === true,
|
|
1355
|
+
});
|
|
1094
1356
|
|
|
1095
1357
|
app.notFound((c) =>
|
|
1096
1358
|
c.json(
|
|
@@ -1130,6 +1392,7 @@ export function createServerApp(
|
|
|
1130
1392
|
body,
|
|
1131
1393
|
operation,
|
|
1132
1394
|
options,
|
|
1395
|
+
state,
|
|
1133
1396
|
proxyTelemetry,
|
|
1134
1397
|
);
|
|
1135
1398
|
if (response instanceof Response) {
|
|
@@ -1183,7 +1446,10 @@ export function createServerApp(
|
|
|
1183
1446
|
.clone()
|
|
1184
1447
|
.json()
|
|
1185
1448
|
.catch(() => undefined);
|
|
1186
|
-
const body =
|
|
1449
|
+
const body = withAuthRequestHeaders(
|
|
1450
|
+
AuthFlowRequestSchema.parse(rawBody),
|
|
1451
|
+
c.req.raw.headers,
|
|
1452
|
+
);
|
|
1187
1453
|
const response = await handleAuthFlow(provider, body, "start", options);
|
|
1188
1454
|
logProviderSuccess(
|
|
1189
1455
|
logger,
|
|
@@ -1220,7 +1486,10 @@ export function createServerApp(
|
|
|
1220
1486
|
.clone()
|
|
1221
1487
|
.json()
|
|
1222
1488
|
.catch(() => undefined);
|
|
1223
|
-
const body =
|
|
1489
|
+
const body = withAuthRequestHeaders(
|
|
1490
|
+
AuthFlowRequestSchema.parse(rawBody),
|
|
1491
|
+
c.req.raw.headers,
|
|
1492
|
+
);
|
|
1224
1493
|
const response = await handleAuthFlow(
|
|
1225
1494
|
provider,
|
|
1226
1495
|
body,
|
|
@@ -1262,7 +1531,10 @@ export function createServerApp(
|
|
|
1262
1531
|
.clone()
|
|
1263
1532
|
.json()
|
|
1264
1533
|
.catch(() => undefined);
|
|
1265
|
-
const body =
|
|
1534
|
+
const body = withAuthRequestHeaders(
|
|
1535
|
+
AuthFlowRequestSchema.parse(rawBody),
|
|
1536
|
+
c.req.raw.headers,
|
|
1537
|
+
);
|
|
1266
1538
|
const response = await handleAuthFlow(provider, body, "poll", options);
|
|
1267
1539
|
logProviderSuccess(
|
|
1268
1540
|
logger,
|
|
@@ -1291,6 +1563,46 @@ export function createServerApp(
|
|
|
1291
1563
|
}
|
|
1292
1564
|
});
|
|
1293
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),
|
|
1601
|
+
);
|
|
1602
|
+
return c.json(toErrorResponse(error, requestId), status);
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
|
|
1294
1606
|
app.post("/auth/disconnect", async (c) => {
|
|
1295
1607
|
let rawBody: unknown;
|
|
1296
1608
|
const requestCost = startRequestCost();
|
|
@@ -1299,7 +1611,10 @@ export function createServerApp(
|
|
|
1299
1611
|
.clone()
|
|
1300
1612
|
.json()
|
|
1301
1613
|
.catch(() => undefined);
|
|
1302
|
-
const body =
|
|
1614
|
+
const body = withAuthRequestHeaders(
|
|
1615
|
+
AuthFlowRequestSchema.parse(rawBody),
|
|
1616
|
+
c.req.raw.headers,
|
|
1617
|
+
);
|
|
1303
1618
|
const response = await handleAuthFlow(provider, body, "abort", options);
|
|
1304
1619
|
logProviderSuccess(
|
|
1305
1620
|
logger,
|