@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.
Files changed (42) hide show
  1. package/AUTHORING.md +24 -0
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +23 -2
  4. package/SUBMISSION.md +2 -1
  5. package/bin/apifuse-check.ts +60 -6
  6. package/bin/apifuse-dev.ts +48 -5
  7. package/bin/apifuse-perf.ts +106 -26
  8. package/bin/apifuse-record.ts +142 -52
  9. package/bin/apifuse-submit-check.ts +1489 -3
  10. package/package.json +107 -92
  11. package/src/ceremonies/index.ts +8 -2
  12. package/src/choice-token.ts +1 -0
  13. package/src/cli/commands.ts +10 -8
  14. package/src/cli/create.ts +49 -1
  15. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  16. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  17. package/src/cli/templates/provider/README.md.tpl +18 -0
  18. package/src/cli/templates/provider/operations/ping.ts.tpl +3 -2
  19. package/src/cli/templates/provider/schemas/ping.ts.tpl +8 -0
  20. package/src/config/loader.ts +19 -1
  21. package/src/contract-json.ts +75 -0
  22. package/src/contract-serialization.ts +89 -0
  23. package/src/contract-types.ts +52 -0
  24. package/src/contract.ts +215 -0
  25. package/src/define.ts +40 -5
  26. package/src/errors.ts +15 -0
  27. package/src/i18n/catalog.ts +156 -0
  28. package/src/index.ts +22 -1
  29. package/src/lint.ts +265 -46
  30. package/src/provider.ts +45 -2
  31. package/src/runtime/browser.ts +685 -30
  32. package/src/runtime/cache.ts +35 -89
  33. package/src/runtime/choice.ts +760 -0
  34. package/src/runtime/executor.ts +19 -2
  35. package/src/runtime/redis.ts +116 -0
  36. package/src/runtime/state.ts +487 -0
  37. package/src/runtime/stealth.ts +8 -1
  38. package/src/runtime/trace.ts +1 -1
  39. package/src/server/serve.ts +361 -46
  40. package/src/server/types.ts +2 -0
  41. package/src/testing/run.ts +16 -3
  42. package/src/types.ts +225 -18
@@ -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 { ProviderError, TransportError } from "../errors";
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 { createUnsupportedProviderRuntimeState } from "../runtime/state";
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: createEnvContext(provider.secrets?.map((secret) => secret.name)),
172
- credential: createCredentialContext({
173
- allowedKeys: provider.credential?.keys,
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: createUnsupportedProviderRuntimeState(),
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(error: unknown): 400 | 404 | 429 | 500 | 502 | 503 | 504 {
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: () => void,
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: () => void,
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: () => void,
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
- ctx.stealth.close?.();
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
- : flow.abort
1079
- ? await flow.abort(context)
1080
- : null;
1317
+ : route === "abort"
1318
+ ? flow.abort
1319
+ ? await flow.abort(context)
1320
+ : null
1321
+ : flow.refresh
1322
+ ? await flow.refresh(context, request.input ?? {})
1323
+ : null;
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
- return toAuthFlowResponse(result, getPatch());
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 = AuthFlowRequestSchema.parse(rawBody);
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 = AuthFlowRequestSchema.parse(rawBody);
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 = AuthFlowRequestSchema.parse(rawBody);
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 = AuthFlowRequestSchema.parse(rawBody);
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,