@apifuse/provider-sdk 2.2.0-beta.13 → 2.2.0-beta.15

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 (44) hide show
  1. package/AUTHORING.md +70 -6
  2. package/CHANGELOG.md +12 -0
  3. package/dist/define.js +9 -0
  4. package/dist/errors.d.ts +13 -0
  5. package/dist/errors.js +25 -0
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +2 -2
  8. package/dist/native-egress-policy.d.ts +27 -0
  9. package/dist/native-egress-policy.js +225 -0
  10. package/dist/provider.d.ts +3 -3
  11. package/dist/provider.js +2 -2
  12. package/dist/runtime/executor.js +17 -2
  13. package/dist/runtime/http.js +189 -9
  14. package/dist/runtime/native-network.d.ts +39 -4
  15. package/dist/runtime/native-network.js +365 -20
  16. package/dist/runtime/redirects.d.ts +29 -0
  17. package/dist/runtime/redirects.js +36 -0
  18. package/dist/runtime/stealth.js +16 -44
  19. package/dist/server/index.d.ts +1 -1
  20. package/dist/server/index.js +1 -1
  21. package/dist/server/serve.d.ts +9 -0
  22. package/dist/server/serve.js +190 -51
  23. package/dist/server/types.d.ts +3 -0
  24. package/dist/server/types.js +1 -0
  25. package/dist/stateful/stateful-provider-owner-forwarder.js +9 -1
  26. package/dist/testing/run.js +32 -13
  27. package/dist/types.d.ts +23 -2
  28. package/package.json +1 -1
  29. package/src/define.ts +11 -0
  30. package/src/errors.ts +37 -0
  31. package/src/index.ts +12 -1
  32. package/src/native-egress-policy.ts +285 -0
  33. package/src/provider.ts +7 -0
  34. package/src/runtime/executor.ts +22 -2
  35. package/src/runtime/http.ts +217 -9
  36. package/src/runtime/native-network.ts +474 -22
  37. package/src/runtime/redirects.ts +66 -0
  38. package/src/runtime/stealth.ts +20 -47
  39. package/src/server/index.ts +2 -0
  40. package/src/server/serve.ts +226 -68
  41. package/src/server/types.ts +1 -0
  42. package/src/stateful/stateful-provider-owner-forwarder.ts +9 -1
  43. package/src/testing/run.ts +39 -14
  44. package/src/types.ts +32 -2
@@ -50,6 +50,11 @@ import {
50
50
  shouldRetryProxyTransportAttempt,
51
51
  validateUnsafeProxyTransportRetryMethods,
52
52
  } from "./proxy-retry-policy.js";
53
+ import {
54
+ evaluateRedirectHop,
55
+ isRedirectStatus,
56
+ resolveRedirectUrl,
57
+ } from "./redirects.js";
53
58
  import {
54
59
  isSensitiveKey,
55
60
  redactSensitiveError,
@@ -731,16 +736,6 @@ function normalizeMethod(method: HttpMethod | string): StealthMethod {
731
736
  }
732
737
  }
733
738
 
734
- function isRedirectStatus(status: number): boolean {
735
- return [301, 302, 303, 307, 308].includes(status);
736
- }
737
-
738
- function nextRedirectMethod(status: number, method: StealthMethod): StealthMethod {
739
- if (status === 303 && method !== "HEAD") return "GET";
740
- if ((status === 301 || status === 302) && method === "POST") return "GET";
741
- return method;
742
- }
743
-
744
739
  function locationHeader(headers: Record<string, string>): string | undefined {
745
740
  for (const [name, value] of Object.entries(headers)) {
746
741
  if (name.toLowerCase() === "location") return value;
@@ -1256,7 +1251,7 @@ function createSessionFetcher(
1256
1251
  const redactedLocation = location ? redactRedirectUrl(location) : undefined;
1257
1252
  let nextUrl: string | undefined;
1258
1253
  try {
1259
- nextUrl = location ? new URL(location, responseUrl).toString() : undefined;
1254
+ nextUrl = resolveRedirectUrl(location, responseUrl);
1260
1255
  } catch (error) {
1261
1256
  throw redactSensitiveError(error, [...sensitiveValues], location, redactedLocation);
1262
1257
  }
@@ -1297,51 +1292,29 @@ function createSessionFetcher(
1297
1292
  throw sanitizedError;
1298
1293
  }
1299
1294
  }
1300
- if (shouldStop) {
1301
- return {
1302
- final: response,
1303
- hops,
1304
- reason: "stopped",
1305
- cookies: cookieJar.snapshot(),
1306
- cookieStore: cookieJar.serialize(),
1307
- };
1308
- }
1309
-
1310
- if (!nextUrl) {
1311
- return {
1312
- final: response,
1313
- hops,
1314
- reason: "missing_location",
1315
- cookies: cookieJar.snapshot(),
1316
- cookieStore: cookieJar.serialize(),
1317
- };
1318
- }
1319
-
1320
- if (hops.length > maxHops) {
1295
+ const decision = evaluateRedirectHop({
1296
+ status: response.status,
1297
+ method,
1298
+ nextUrl,
1299
+ shouldStop,
1300
+ redirectCount: hops.length,
1301
+ maxHops,
1302
+ visitedRequests,
1303
+ });
1304
+ if (decision.kind === "stop") {
1321
1305
  return {
1322
1306
  final: response,
1323
1307
  hops,
1324
- reason: "max_hops",
1308
+ reason: decision.reason,
1325
1309
  cookies: cookieJar.snapshot(),
1326
1310
  cookieStore: cookieJar.serialize(),
1327
1311
  };
1328
1312
  }
1329
-
1330
- const nextMethod = nextRedirectMethod(response.status, method);
1331
- if (nextMethod !== method) {
1313
+ if (decision.nextMethod !== method) {
1332
1314
  body = undefined;
1333
1315
  }
1334
- if (visitedRequests.has(`${nextMethod} ${nextUrl}`)) {
1335
- return {
1336
- final: response,
1337
- hops,
1338
- reason: "loop",
1339
- cookies: cookieJar.snapshot(),
1340
- cookieStore: cookieJar.serialize(),
1341
- };
1342
- }
1343
- method = nextMethod;
1344
- currentUrl = nextUrl;
1316
+ method = decision.nextMethod;
1317
+ currentUrl = decision.nextUrl;
1345
1318
  }
1346
1319
 
1347
1320
  if (!response) {
@@ -1,5 +1,7 @@
1
1
  export {
2
2
  createServerApp,
3
+ ERROR_OBSERVABILITY_HEADER,
4
+ type ErrorObservabilityDetails,
3
5
  type ProviderServerCloseOptions,
4
6
  type ProviderServerHandle,
5
7
  type ProviderServerLogEvent,
@@ -9,6 +9,7 @@ import {
9
9
  isProviderError,
10
10
  isSessionExpiredError,
11
11
  isTransportError,
12
+ isValidationError,
12
13
  ProviderError,
13
14
  } from "../errors.js";
14
15
  import {
@@ -35,6 +36,7 @@ import { createEnvContext } from "../runtime/env.js";
35
36
  import { executeOperation } from "../runtime/executor.js";
36
37
  import { createHttpClient } from "../runtime/http.js";
37
38
  import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
39
+ import { createNativeNetworkClient } from "../runtime/native-network.js";
38
40
  import { getProviderBaseUrl } from "../runtime/provider.js";
39
41
  import {
40
42
  PROXY_AUTH_IP_DENIED_CODE,
@@ -81,6 +83,7 @@ import type {
81
83
  OperationSseTransport,
82
84
  ProviderContext,
83
85
  ProviderDefinition,
86
+ ProviderProxyPolicy,
84
87
  ProviderRuntimeState,
85
88
  ProviderStreamEvent,
86
89
  StealthClient,
@@ -108,6 +111,14 @@ import {
108
111
 
109
112
  const DEFAULT_HOST = "0.0.0.0";
110
113
  const DEFAULT_PORT = 3000;
114
+ /** Compact SDK-owned error classification emitted separately from the public response body. */
115
+ export const ERROR_OBSERVABILITY_HEADER = "X-ApiFuse-Error-Observability";
116
+ export type ErrorObservabilityDetails = {
117
+ category: ProviderErrorCategory;
118
+ taxonomyVersion: string;
119
+ retryable: boolean;
120
+ upstreamStatus?: number;
121
+ };
111
122
  const AUTH_FLOW_LOCALES = ["en", "ko", "ja"] as const;
112
123
  const retryResponseMeta = new WeakMap<ProviderContext, HttpRetrySummary>();
113
124
  const STATEFUL_INTERNAL_OPERATIONS_ROUTE = "/__apifuse/stateful/operations";
@@ -278,6 +289,13 @@ function resolveOperationConnectionId(request: OperationRequest): string | undef
278
289
  return request.connection?.id ?? request.connectionId;
279
290
  }
280
291
 
292
+ function resolveNativeProxyPolicy(provider: ProviderDefinition): ProviderProxyPolicy | undefined {
293
+ if (typeof provider.proxy === "object") return provider.proxy;
294
+ if (provider.proxy === true) return { mode: "optional" };
295
+ if (provider.proxy === false) return { mode: "disabled" };
296
+ return undefined;
297
+ }
298
+
281
299
  function createProviderContext(
282
300
  provider: ProviderDefinition,
283
301
  request: OperationRequest,
@@ -344,6 +362,17 @@ function createProviderContext(
344
362
  engine: provider.browser?.engine,
345
363
  })
346
364
  : createBrowserStub(),
365
+ ...(provider.native
366
+ ? {
367
+ native: {
368
+ network: createNativeNetworkClient({
369
+ egress: provider.native.network,
370
+ proxyPolicy: resolveNativeProxyPolicy(provider),
371
+ affinityKey: proxyClientOptions.affinityKey,
372
+ }),
373
+ },
374
+ }
375
+ : {}),
347
376
  trace: createTraceContext(),
348
377
  auth: createAuthStub(),
349
378
  stt: options.stt ?? createSttClientFromEnv(provider.stt),
@@ -446,6 +475,17 @@ function createAuthFlowContext(
446
475
  ? createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
447
476
  : createStealthClient(stealthBaseUrl, stealthClientOptions)
448
477
  : createStealthStub(),
478
+ ...(provider.native
479
+ ? {
480
+ native: {
481
+ network: createNativeNetworkClient({
482
+ egress: provider.native.network,
483
+ proxyPolicy: resolveNativeProxyPolicy(provider),
484
+ affinityKey: proxyClientOptions.affinityKey,
485
+ }),
486
+ },
487
+ }
488
+ : {}),
449
489
  env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
450
490
  credential,
451
491
  context: flowContextStore.context,
@@ -486,6 +526,7 @@ export type ProviderServerLogEvent =
486
526
  errorCategory?: ProviderErrorCategory;
487
527
  taxonomyVersion?: string;
488
528
  retryable?: boolean;
529
+ signal?: "unregistered_provider_error_code";
489
530
  issues?: Array<{ path: string; code: string; message: string }>;
490
531
  })
491
532
  | {
@@ -604,26 +645,28 @@ function zodDetails(error: z.ZodError): Array<{
604
645
  }
605
646
 
606
647
  function toErrorResponse(error: unknown, requestId?: string): OperationErrorResponse {
648
+ const observability = errorObservabilityDetails(error);
607
649
  if (error instanceof StatefulRoutingDeadlineError) {
608
650
  return {
609
651
  error: {
610
652
  code: "STATEFUL_FORWARDING_DEADLINE_EXPIRED",
611
653
  message: "Stateful forwarding deadline expired.",
612
654
  ...(requestId ? { requestId } : {}),
613
- details: { retryable: false },
655
+ retryable: observability.retryable,
614
656
  },
615
657
  };
616
658
  }
617
659
 
618
660
  if (isProviderError(error)) {
619
- const details = publicProviderErrorDetails(error);
661
+ const details = error.details;
620
662
  return {
621
663
  error: {
622
664
  code: error.code ?? "provider_error",
623
665
  message: publicProviderErrorMessage(error),
624
666
  ...(requestId ? { requestId } : {}),
667
+ retryable: observability.retryable,
625
668
  ...(error.fix ? { fix: error.fix } : {}),
626
- ...(details ? { details } : {}),
669
+ ...(details !== undefined ? { details } : {}),
627
670
  },
628
671
  };
629
672
  }
@@ -634,6 +677,7 @@ function toErrorResponse(error: unknown, requestId?: string): OperationErrorResp
634
677
  code: "invalid_request",
635
678
  message: "Invalid request body",
636
679
  ...(requestId ? { requestId } : {}),
680
+ retryable: observability.retryable,
637
681
  details: zodDetails(error),
638
682
  },
639
683
  };
@@ -650,6 +694,7 @@ function toErrorResponse(error: unknown, requestId?: string): OperationErrorResp
650
694
  code: "internal_error",
651
695
  message: "Internal error",
652
696
  ...(requestId ? { requestId } : {}),
697
+ retryable: observability.retryable,
653
698
  details: {
654
699
  retryable: false,
655
700
  category: "internal_error",
@@ -659,42 +704,12 @@ function toErrorResponse(error: unknown, requestId?: string): OperationErrorResp
659
704
  };
660
705
  }
661
706
 
662
- function publicProviderErrorDetails(error: ProviderError): unknown {
663
- const providerDetails = error.details;
664
- const observabilityDetails = providerObservabilityDetails(error);
665
-
666
- if (providerDetails === undefined) {
667
- return observabilityDetails;
668
- }
669
- if (observabilityDetails === undefined) {
670
- return providerDetails;
671
- }
672
- if (isPlainRecord(providerDetails) && isPlainRecord(observabilityDetails)) {
673
- return { ...providerDetails, ...observabilityDetails };
674
- }
675
- return {
676
- provider: providerDetails,
677
- observability: observabilityDetails,
678
- };
679
- }
680
-
681
- function isPlainRecord(value: unknown): value is Record<string, unknown> {
682
- return value !== null && typeof value === "object" && !Array.isArray(value);
683
- }
684
-
685
707
  // Accepts `unknown` so the branded guards narrow cleanly from the top: the
686
708
  // subtype error classes are structurally compatible with ProviderError, so
687
709
  // narrowing from a ProviderError-typed value would collapse the negative branch
688
710
  // to `never`. Narrowing from unknown avoids that while still recognizing errors
689
711
  // from a duplicate SDK module instance.
690
- function providerObservabilityDetails(error: unknown):
691
- | {
692
- category: ProviderErrorCategory;
693
- taxonomyVersion: string;
694
- retryable: boolean;
695
- upstreamStatus?: number;
696
- }
697
- | undefined {
712
+ function providerObservabilityDetails(error: unknown): ErrorObservabilityDetails | undefined {
698
713
  // Session-expiry surfaces the credential_expired category + the opt-in
699
714
  // retryable signal so Gateway/Credential Service can refresh and re-drive the
700
715
  // operation (see design.md §4.3 D3). Without this branch the auth error would
@@ -751,6 +766,54 @@ function providerObservabilityDetails(error: unknown):
751
766
  };
752
767
  }
753
768
 
769
+ function errorObservabilityDetails(error: unknown): ErrorObservabilityDetails {
770
+ const providerDetails = providerObservabilityDetails(error);
771
+ if (providerDetails) return providerDetails;
772
+
773
+ if (error instanceof z.ZodError || isValidationError(error)) {
774
+ return {
775
+ category:
776
+ isProviderError(error) && error.options?.category
777
+ ? error.options.category
778
+ : "input_validation",
779
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
780
+ retryable: isProviderError(error) ? (error.options?.retryable ?? false) : false,
781
+ };
782
+ }
783
+
784
+ if (error instanceof StatefulRoutingDeadlineError) {
785
+ return {
786
+ category: "timeout",
787
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
788
+ retryable: false,
789
+ };
790
+ }
791
+
792
+ if (isProviderError(error)) {
793
+ return {
794
+ category: error.options?.category ?? "provider_error",
795
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
796
+ retryable: error.options?.retryable ?? false,
797
+ };
798
+ }
799
+
800
+ return {
801
+ category: "internal_error",
802
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
803
+ retryable: false,
804
+ };
805
+ }
806
+
807
+ function responseWithErrorObservability(response: Response, error: unknown): Response {
808
+ const headers = new Headers(response.headers);
809
+ headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(errorObservabilityDetails(error)));
810
+ return new Response(response.body, {
811
+ status: response.status,
812
+ statusText: response.statusText,
813
+ headers,
814
+ });
815
+ }
816
+
754
817
  function publicProviderErrorMessage(error: ProviderError): string {
755
818
  if (isTransportError(error)) {
756
819
  if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
@@ -782,11 +845,6 @@ function toStatusCode(error: unknown): 400 | 401 | 404 | 429 | 500 | 502 | 503 |
782
845
  if (error instanceof StatefulRoutingDeadlineError) {
783
846
  return 504;
784
847
  }
785
-
786
- if (isTransportError(error)) {
787
- return error.code === "transport_timeout" ? 504 : 502;
788
- }
789
-
790
848
  if (isProviderError(error)) {
791
849
  switch (error.code) {
792
850
  case "AUTH_REQUIRED":
@@ -812,13 +870,96 @@ function toStatusCode(error: unknown): 400 | 401 | 404 | 429 | 500 | 502 | 503 |
812
870
  case "STATEFUL_FORWARDING_REPLAY_CACHE_FULL":
813
871
  return 503;
814
872
  }
873
+ if (isTransportError(error)) {
874
+ return error.code === "transport_timeout" ? 504 : 502;
875
+ }
876
+ if (isValidationError(error)) {
877
+ return error.options?.category === "output_validation" ? 500 : 400;
878
+ }
815
879
 
816
- return 400;
880
+ return 500;
817
881
  }
818
882
 
819
883
  return 500;
820
884
  }
821
885
 
886
+ // Codes emitted by SDK-owned paths must never be attributed to provider
887
+ // authors by the unregistered-code signal, even when their intentional status
888
+ // is 500. Provider-authored codes not in this registry retain the signal.
889
+ const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
890
+ "AUTH_PROMPT_UNAVAILABLE",
891
+ "BROWSER_CDP_POOL_REQUIRED",
892
+ "BROWSER_RUNTIME_UNSUPPORTED",
893
+ "STEALTH_RUNTIME_UNSUPPORTED",
894
+ "SSE_EVENT_UNDECLARED",
895
+ "STREAM_EVENT_TOO_LARGE",
896
+ "STREAM_CHUNK_TOO_LARGE",
897
+ "SSE_RESULT_UNSUPPORTED",
898
+ "STREAM_RESULT_UNSUPPORTED",
899
+ "AUTH_FLOW_NOT_CONFIGURED",
900
+ "refresh_not_supported",
901
+ "RUNTIME_UNSUPPORTED",
902
+ "PROVIDER_STATE_UNSUPPORTED",
903
+ "CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
904
+ "CHOICE_STATE_PAYLOAD_TOO_LARGE",
905
+ "CHOICE_STATE_UNAVAILABLE",
906
+ "CHOICE_CONTEXT_REQUIRED",
907
+ "unsupported_stealth_cookie_store_version",
908
+ "provider_secret_error",
909
+ "credential_key_error",
910
+ "credential_mode_error",
911
+ "flow_expired",
912
+ "turn_validation_error",
913
+ "context_access_error",
914
+ "UNSUPPORTED_STT_OPTION",
915
+ "INVALID_STT_AUDIO",
916
+ "STT_AUDIO_TOO_LARGE",
917
+ "STT_UPSTREAM_FAILED",
918
+ "INVALID_STT_VERIFICATION_CODE_OPTIONS",
919
+ "NO_CODE_FOUND",
920
+ "AMBIGUOUS_CODE",
921
+ "retry_invalid_policy",
922
+ "retry_unsafe_method",
923
+ "stealth_cookie_store_serialize_failed",
924
+ "response_too_large",
925
+ "transport_stream_unavailable",
926
+ "transport_invalid_method",
927
+ "http_transport_override_unsupported",
928
+ "http_redirect_policy_invalid",
929
+ "http_redirect_stopped",
930
+ "http_redirect_max_hops",
931
+ "http_redirect_missing_location",
932
+ "http_redirect_loop",
933
+ "transport_invalid_url",
934
+ "retry_exhausted",
935
+ "auth_abort_unsafe_data",
936
+ "credentials_auth_missing_credential_keys",
937
+ "credentials_auth_missing_credential",
938
+ "credentials_auth_invalid_login_result",
939
+ "credentials_auth_unknown_challenge",
940
+ "credentials_auth_unknown_pending_challenge",
941
+ "STATEFUL_FORWARDING_NOT_CONFIGURED",
942
+ "STATEFUL_FORWARDING_SIGNATURE_MISSING",
943
+ "STATEFUL_FORWARDING_NONCE_INVALID",
944
+ "STATEFUL_FORWARDING_TIMESTAMP_INVALID",
945
+ "STATEFUL_FORWARDING_SIGNATURE_INVALID",
946
+ "STATEFUL_FORWARDING_REPLAY_DETECTED",
947
+ "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
948
+ "STATEFUL_FORWARDING_ENVELOPE_INVALID",
949
+ "STATEFUL_FORWARDING_PROVIDER_MISMATCH",
950
+ "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
951
+ "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
952
+ "STATEFUL_FORWARDING_REQUEST_FAILED",
953
+ "STATEFUL_FORWARDING_CONTEXT_MISSING",
954
+ "STATEFUL_FORWARDING_BAD_RESPONSE",
955
+ "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
956
+ "STATEFUL_FILE_FORWARDING_UNSUPPORTED",
957
+ "STATEFUL_CONTROL_PLANE_OPERATION_AMBIGUOUS",
958
+ "STATEFUL_CONTROL_PLANE_REQUEST_FAILED",
959
+ "STATEFUL_CONTROL_PLANE_HTTP_ERROR",
960
+ "STATEFUL_CONTROL_PLANE_INVALID_RESPONSE",
961
+ ]);
962
+
822
963
  function extractRequestId(raw: unknown): string | undefined {
823
964
  if (!raw || typeof raw !== "object") {
824
965
  return undefined;
@@ -847,7 +988,13 @@ function logProviderError(
847
988
  : "internal_error";
848
989
  const errorClass = error instanceof Error ? error.name : typeof error;
849
990
  const message = error instanceof Error ? error.message : String(error);
850
- const details = isProviderError(error) ? providerObservabilityDetails(error) : undefined;
991
+ const details = errorObservabilityDetails(error);
992
+ const isUnregisteredProviderErrorCode =
993
+ status === 500 &&
994
+ isProviderError(error) &&
995
+ !isValidationError(error) &&
996
+ typeof error.code === "string" &&
997
+ !SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code);
851
998
  const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
852
999
  emit({
853
1000
  level: status >= 500 ? "error" : "warn",
@@ -861,15 +1008,12 @@ function logProviderError(
861
1008
  code,
862
1009
  errorClass,
863
1010
  message,
864
- ...(isTransportError(error) && error.upstreamStatus
865
- ? { upstreamStatus: error.upstreamStatus }
866
- : {}),
867
- ...(details
868
- ? {
869
- errorCategory: details.category,
870
- taxonomyVersion: details.taxonomyVersion,
871
- retryable: details.retryable,
872
- }
1011
+ ...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
1012
+ errorCategory: details.category,
1013
+ taxonomyVersion: details.taxonomyVersion,
1014
+ retryable: details.retryable,
1015
+ ...(isUnregisteredProviderErrorCode
1016
+ ? { signal: "unregistered_provider_error_code" as const }
873
1017
  : {}),
874
1018
  ...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
875
1019
  });
@@ -1614,17 +1758,10 @@ export function createServerApp(
1614
1758
  });
1615
1759
  }
1616
1760
 
1617
- app.notFound((c) =>
1618
- c.json(
1619
- {
1620
- error: {
1621
- code: "not_found",
1622
- message: "Not found",
1623
- },
1624
- },
1625
- 404,
1626
- ),
1627
- );
1761
+ app.notFound((c) => {
1762
+ const error = new ProviderError("Not found", { code: "not_found", retryable: false });
1763
+ return responseWithErrorObservability(c.json(toErrorResponse(error), 404), error);
1764
+ });
1628
1765
 
1629
1766
  app.get("/health", (c) =>
1630
1767
  c.json({
@@ -1786,7 +1923,10 @@ export function createServerApp(
1786
1923
  status,
1787
1924
  finishRequestCost(requestCost),
1788
1925
  );
1789
- return c.json(toErrorResponse(error, requestId), status);
1926
+ return responseWithErrorObservability(
1927
+ c.json(toErrorResponse(error, requestId), status),
1928
+ error,
1929
+ );
1790
1930
  }
1791
1931
  });
1792
1932
 
@@ -1850,7 +1990,10 @@ export function createServerApp(
1850
1990
  );
1851
1991
  const telemetryHeader = proxyTelemetry.toHeaderValue();
1852
1992
  if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1853
- return c.json(toErrorResponse(error, requestId), status);
1993
+ return responseWithErrorObservability(
1994
+ c.json(toErrorResponse(error, requestId), status),
1995
+ error,
1996
+ );
1854
1997
  }
1855
1998
  });
1856
1999
 
@@ -1887,7 +2030,10 @@ export function createServerApp(
1887
2030
  status,
1888
2031
  finishRequestCost(requestCost),
1889
2032
  );
1890
- return c.json(toErrorResponse(error, requestId), status);
2033
+ return responseWithErrorObservability(
2034
+ c.json(toErrorResponse(error, requestId), status),
2035
+ error,
2036
+ );
1891
2037
  }
1892
2038
  });
1893
2039
 
@@ -1924,7 +2070,10 @@ export function createServerApp(
1924
2070
  status,
1925
2071
  finishRequestCost(requestCost),
1926
2072
  );
1927
- return c.json(toErrorResponse(error, requestId), status);
2073
+ return responseWithErrorObservability(
2074
+ c.json(toErrorResponse(error, requestId), status),
2075
+ error,
2076
+ );
1928
2077
  }
1929
2078
  });
1930
2079
 
@@ -1961,7 +2110,10 @@ export function createServerApp(
1961
2110
  status,
1962
2111
  finishRequestCost(requestCost),
1963
2112
  );
1964
- return c.json(toErrorResponse(error, requestId), status);
2113
+ return responseWithErrorObservability(
2114
+ c.json(toErrorResponse(error, requestId), status),
2115
+ error,
2116
+ );
1965
2117
  }
1966
2118
  });
1967
2119
 
@@ -1998,7 +2150,10 @@ export function createServerApp(
1998
2150
  status,
1999
2151
  finishRequestCost(requestCost),
2000
2152
  );
2001
- return c.json(toErrorResponse(error, requestId), status);
2153
+ return responseWithErrorObservability(
2154
+ c.json(toErrorResponse(error, requestId), status),
2155
+ error,
2156
+ );
2002
2157
  }
2003
2158
  });
2004
2159
 
@@ -2035,7 +2190,10 @@ export function createServerApp(
2035
2190
  status,
2036
2191
  finishRequestCost(requestCost),
2037
2192
  );
2038
- return c.json(toErrorResponse(error, requestId), status);
2193
+ return responseWithErrorObservability(
2194
+ c.json(toErrorResponse(error, requestId), status),
2195
+ error,
2196
+ );
2039
2197
  }
2040
2198
  });
2041
2199
 
@@ -26,6 +26,7 @@ export const ErrorEnvelopeSchema = z.object({
26
26
  code: z.string(),
27
27
  message: z.string(),
28
28
  requestId: z.string().optional(),
29
+ retryable: z.boolean(),
29
30
  fix: z.string().optional(),
30
31
  details: z.unknown().optional(),
31
32
  });
@@ -35,6 +35,14 @@ type FetchTransport = (url: string | URL | Request, init?: RequestInit) => Promi
35
35
 
36
36
  const MAX_FORWARDED_HEADERS = 32;
37
37
  const MAX_FORWARDED_HEADER_BYTES = 8 * 1024;
38
+ // Inbound compatibility boundary for rolling deploys: older owner pods omit
39
+ // top-level retryable. Emitted responses remain strict via
40
+ // OperationErrorResponseSchema.
41
+ const ForwardedOperationErrorResponseSchema = OperationErrorResponseSchema.extend({
42
+ error: OperationErrorResponseSchema.shape.error.extend({
43
+ retryable: z.boolean().optional().default(false),
44
+ }),
45
+ });
38
46
  const SENSITIVE_HEADER_NAMES = new Set([
39
47
  "authorization",
40
48
  "cookie",
@@ -253,7 +261,7 @@ async function parseForwardedResponse(response: Response): Promise<StatefulOpera
253
261
  return { output: success.data.data };
254
262
  }
255
263
 
256
- const error = OperationErrorResponseSchema.safeParse(body);
264
+ const error = ForwardedOperationErrorResponseSchema.safeParse(body);
257
265
  if (error.success) {
258
266
  throw new StatefulOwnerForwardingError({
259
267
  code: error.data.error.code,