@apifuse/provider-sdk 2.2.0-beta.14 → 2.2.0-beta.16

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.
@@ -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) {
@@ -4,6 +4,10 @@ import { join } from "node:path";
4
4
  import { Hono } from "hono";
5
5
  import { z } from "zod";
6
6
  import { AuthAbortError, createAuthFlowHelpers } from "../auth.js";
7
+ import {
8
+ SDK_OWNED_PROVIDER_ERROR_CODES,
9
+ SDK_RUNTIME_OWNED_ERROR_CODES,
10
+ } from "../error-resolution.js";
7
11
  import {
8
12
  AuthError,
9
13
  isProviderError,
@@ -36,6 +40,7 @@ import { createEnvContext } from "../runtime/env.js";
36
40
  import { executeOperation } from "../runtime/executor.js";
37
41
  import { createHttpClient } from "../runtime/http.js";
38
42
  import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
43
+ import { createNativeNetworkClient } from "../runtime/native-network.js";
39
44
  import { getProviderBaseUrl } from "../runtime/provider.js";
40
45
  import {
41
46
  PROXY_AUTH_IP_DENIED_CODE,
@@ -78,15 +83,19 @@ import type {
78
83
  FlowContextStore,
79
84
  HttpRetrySummary,
80
85
  OperationDefinition,
86
+ OperationErrorCode,
81
87
  OperationHttpStreamTransport,
82
88
  OperationSseTransport,
89
+ ProviderErrorStatus,
83
90
  ProviderContext,
84
91
  ProviderDefinition,
92
+ ProviderProxyPolicy,
85
93
  ProviderRuntimeState,
86
94
  ProviderStreamEvent,
87
95
  StealthClient,
88
96
  SttContext,
89
97
  } from "../types.js";
98
+ import { VALID_OPERATION_ERROR_STATUSES } from "../types.js";
90
99
  import {
91
100
  createSelfTestApp,
92
101
  createSelfTestAuthFlowInvoke,
@@ -287,6 +296,13 @@ function resolveOperationConnectionId(request: OperationRequest): string | undef
287
296
  return request.connection?.id ?? request.connectionId;
288
297
  }
289
298
 
299
+ function resolveNativeProxyPolicy(provider: ProviderDefinition): ProviderProxyPolicy | undefined {
300
+ if (typeof provider.proxy === "object") return provider.proxy;
301
+ if (provider.proxy === true) return { mode: "optional" };
302
+ if (provider.proxy === false) return { mode: "disabled" };
303
+ return undefined;
304
+ }
305
+
290
306
  function createProviderContext(
291
307
  provider: ProviderDefinition,
292
308
  request: OperationRequest,
@@ -353,6 +369,17 @@ function createProviderContext(
353
369
  engine: provider.browser?.engine,
354
370
  })
355
371
  : createBrowserStub(),
372
+ ...(provider.native
373
+ ? {
374
+ native: {
375
+ network: createNativeNetworkClient({
376
+ egress: provider.native.network,
377
+ proxyPolicy: resolveNativeProxyPolicy(provider),
378
+ affinityKey: proxyClientOptions.affinityKey,
379
+ }),
380
+ },
381
+ }
382
+ : {}),
356
383
  trace: createTraceContext(),
357
384
  auth: createAuthStub(),
358
385
  stt: options.stt ?? createSttClientFromEnv(provider.stt),
@@ -455,6 +482,17 @@ function createAuthFlowContext(
455
482
  ? createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
456
483
  : createStealthClient(stealthBaseUrl, stealthClientOptions)
457
484
  : createStealthStub(),
485
+ ...(provider.native
486
+ ? {
487
+ native: {
488
+ network: createNativeNetworkClient({
489
+ egress: provider.native.network,
490
+ proxyPolicy: resolveNativeProxyPolicy(provider),
491
+ affinityKey: proxyClientOptions.affinityKey,
492
+ }),
493
+ },
494
+ }
495
+ : {}),
458
496
  env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
459
497
  credential,
460
498
  context: flowContextStore.context,
@@ -613,8 +651,12 @@ function zodDetails(error: z.ZodError): Array<{
613
651
  }));
614
652
  }
615
653
 
616
- function toErrorResponse(error: unknown, requestId?: string): OperationErrorResponse {
617
- const observability = errorObservabilityDetails(error);
654
+ function toErrorResponse(
655
+ error: unknown,
656
+ requestId?: string,
657
+ declaredErrorCode?: OperationErrorCode,
658
+ ): OperationErrorResponse {
659
+ const observability = errorObservabilityDetails(error, declaredErrorCode);
618
660
  if (error instanceof StatefulRoutingDeadlineError) {
619
661
  return {
620
662
  error: {
@@ -678,7 +720,13 @@ function toErrorResponse(error: unknown, requestId?: string): OperationErrorResp
678
720
  // narrowing from a ProviderError-typed value would collapse the negative branch
679
721
  // to `never`. Narrowing from unknown avoids that while still recognizing errors
680
722
  // from a duplicate SDK module instance.
681
- function providerObservabilityDetails(error: unknown): ErrorObservabilityDetails | undefined {
723
+ function providerObservabilityDetails(
724
+ error: unknown,
725
+ declaredErrorCode?: OperationErrorCode,
726
+ ): ErrorObservabilityDetails | undefined {
727
+ const declaredRetryable = sdkOwnsErrorResolution(error)
728
+ ? undefined
729
+ : declaredErrorCode?.retryable;
682
730
  // Session-expiry surfaces the credential_expired category + the opt-in
683
731
  // retryable signal so Gateway/Credential Service can refresh and re-drive the
684
732
  // operation (see design.md §4.3 D3). Without this branch the auth error would
@@ -688,7 +736,7 @@ function providerObservabilityDetails(error: unknown): ErrorObservabilityDetails
688
736
  return {
689
737
  category: error.options?.category ?? "credential_expired",
690
738
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
691
- retryable: error.options?.retryable ?? false,
739
+ retryable: error.options?.retryable ?? declaredRetryable ?? false,
692
740
  };
693
741
  }
694
742
  // Missing-secret errors carry the canonical credential_unavailable category
@@ -700,7 +748,7 @@ function providerObservabilityDetails(error: unknown): ErrorObservabilityDetails
700
748
  return {
701
749
  category: error.options?.category ?? "credential_unavailable",
702
750
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
703
- retryable: error.options?.retryable ?? false,
751
+ retryable: error.options?.retryable ?? declaredRetryable ?? false,
704
752
  };
705
753
  }
706
754
  if (!isTransportError(error)) {
@@ -735,18 +783,27 @@ function providerObservabilityDetails(error: unknown): ErrorObservabilityDetails
735
783
  };
736
784
  }
737
785
 
738
- function errorObservabilityDetails(error: unknown): ErrorObservabilityDetails {
739
- const providerDetails = providerObservabilityDetails(error);
786
+ function errorObservabilityDetails(
787
+ error: unknown,
788
+ declaredErrorCode?: OperationErrorCode,
789
+ ): ErrorObservabilityDetails {
790
+ const effectiveDeclaration = sdkOwnsErrorResolution(error) ? undefined : declaredErrorCode;
791
+ const providerDetails = providerObservabilityDetails(error, effectiveDeclaration);
740
792
  if (providerDetails) return providerDetails;
741
793
 
742
794
  if (error instanceof z.ZodError || isValidationError(error)) {
795
+ const declaredStatus = effectiveDeclaration?.status;
743
796
  return {
744
797
  category:
745
798
  isProviderError(error) && error.options?.category
746
799
  ? error.options.category
747
- : "input_validation",
800
+ : isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
801
+ ? "provider_error"
802
+ : "input_validation",
748
803
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
749
- retryable: isProviderError(error) ? (error.options?.retryable ?? false) : false,
804
+ retryable: isProviderError(error)
805
+ ? (error.options?.retryable ?? effectiveDeclaration?.retryable ?? false)
806
+ : false,
750
807
  };
751
808
  }
752
809
 
@@ -762,7 +819,7 @@ function errorObservabilityDetails(error: unknown): ErrorObservabilityDetails {
762
819
  return {
763
820
  category: error.options?.category ?? "provider_error",
764
821
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
765
- retryable: error.options?.retryable ?? false,
822
+ retryable: error.options?.retryable ?? effectiveDeclaration?.retryable ?? false,
766
823
  };
767
824
  }
768
825
 
@@ -773,9 +830,16 @@ function errorObservabilityDetails(error: unknown): ErrorObservabilityDetails {
773
830
  };
774
831
  }
775
832
 
776
- function responseWithErrorObservability(response: Response, error: unknown): Response {
833
+ function responseWithErrorObservability(
834
+ response: Response,
835
+ error: unknown,
836
+ declaredErrorCode?: OperationErrorCode,
837
+ ): Response {
777
838
  const headers = new Headers(response.headers);
778
- headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(errorObservabilityDetails(error)));
839
+ headers.set(
840
+ ERROR_OBSERVABILITY_HEADER,
841
+ JSON.stringify(errorObservabilityDetails(error, declaredErrorCode)),
842
+ );
779
843
  return new Response(response.body, {
780
844
  status: response.status,
781
845
  statusText: response.statusText,
@@ -807,7 +871,14 @@ function publicProviderErrorMessage(error: ProviderError): string {
807
871
  return error.message;
808
872
  }
809
873
 
810
- function toStatusCode(error: unknown): 400 | 401 | 404 | 429 | 500 | 502 | 503 | 504 {
874
+ function isEmittableErrorStatus(value: unknown): value is ProviderErrorStatus {
875
+ return (
876
+ typeof value === "number" &&
877
+ VALID_OPERATION_ERROR_STATUSES.some((status) => status === value)
878
+ );
879
+ }
880
+
881
+ function toStatusCode(error: unknown, declaredErrorCode?: OperationErrorCode): ProviderErrorStatus {
811
882
  if (error instanceof z.ZodError) {
812
883
  return 400;
813
884
  }
@@ -815,6 +886,12 @@ function toStatusCode(error: unknown): 400 | 401 | 404 | 429 | 500 | 502 | 503 |
815
886
  return 504;
816
887
  }
817
888
  if (isProviderError(error)) {
889
+ if (
890
+ !sdkOwnsErrorResolution(error) &&
891
+ isEmittableErrorStatus(declaredErrorCode?.status)
892
+ ) {
893
+ return declaredErrorCode.status;
894
+ }
818
895
  switch (error.code) {
819
896
  case "AUTH_REQUIRED":
820
897
  case "reauth_required":
@@ -852,77 +929,39 @@ function toStatusCode(error: unknown): 400 | 401 | 404 | 429 | 500 | 502 | 503 |
852
929
  return 500;
853
930
  }
854
931
 
855
- // Codes emitted by SDK-owned paths must never be attributed to provider
856
- // authors by the unregistered-code signal, even when their intentional status
857
- // is 500. Provider-authored codes not in this registry retain the signal.
858
- const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
859
- "AUTH_PROMPT_UNAVAILABLE",
860
- "BROWSER_CDP_POOL_REQUIRED",
861
- "BROWSER_RUNTIME_UNSUPPORTED",
862
- "STEALTH_RUNTIME_UNSUPPORTED",
863
- "SSE_EVENT_UNDECLARED",
864
- "STREAM_EVENT_TOO_LARGE",
865
- "STREAM_CHUNK_TOO_LARGE",
866
- "SSE_RESULT_UNSUPPORTED",
867
- "STREAM_RESULT_UNSUPPORTED",
868
- "AUTH_FLOW_NOT_CONFIGURED",
869
- "refresh_not_supported",
870
- "RUNTIME_UNSUPPORTED",
871
- "PROVIDER_STATE_UNSUPPORTED",
872
- "CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
873
- "CHOICE_STATE_PAYLOAD_TOO_LARGE",
874
- "CHOICE_STATE_UNAVAILABLE",
875
- "CHOICE_CONTEXT_REQUIRED",
876
- "unsupported_stealth_cookie_store_version",
877
- "provider_secret_error",
878
- "credential_key_error",
879
- "credential_mode_error",
880
- "flow_expired",
881
- "turn_validation_error",
882
- "context_access_error",
883
- "UNSUPPORTED_STT_OPTION",
884
- "INVALID_STT_AUDIO",
885
- "STT_AUDIO_TOO_LARGE",
886
- "STT_UPSTREAM_FAILED",
887
- "INVALID_STT_VERIFICATION_CODE_OPTIONS",
888
- "NO_CODE_FOUND",
889
- "AMBIGUOUS_CODE",
890
- "retry_invalid_policy",
891
- "retry_unsafe_method",
892
- "stealth_cookie_store_serialize_failed",
893
- "response_too_large",
894
- "transport_stream_unavailable",
895
- "transport_invalid_method",
896
- "http_transport_override_unsupported",
897
- "transport_invalid_url",
898
- "retry_exhausted",
899
- "auth_abort_unsafe_data",
900
- "credentials_auth_missing_credential_keys",
901
- "credentials_auth_missing_credential",
902
- "credentials_auth_invalid_login_result",
903
- "credentials_auth_unknown_challenge",
904
- "credentials_auth_unknown_pending_challenge",
905
- "STATEFUL_FORWARDING_NOT_CONFIGURED",
906
- "STATEFUL_FORWARDING_SIGNATURE_MISSING",
907
- "STATEFUL_FORWARDING_NONCE_INVALID",
908
- "STATEFUL_FORWARDING_TIMESTAMP_INVALID",
909
- "STATEFUL_FORWARDING_SIGNATURE_INVALID",
910
- "STATEFUL_FORWARDING_REPLAY_DETECTED",
911
- "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
912
- "STATEFUL_FORWARDING_ENVELOPE_INVALID",
913
- "STATEFUL_FORWARDING_PROVIDER_MISMATCH",
914
- "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
915
- "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
916
- "STATEFUL_FORWARDING_REQUEST_FAILED",
917
- "STATEFUL_FORWARDING_CONTEXT_MISSING",
918
- "STATEFUL_FORWARDING_BAD_RESPONSE",
919
- "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
920
- "STATEFUL_FILE_FORWARDING_UNSUPPORTED",
921
- "STATEFUL_CONTROL_PLANE_OPERATION_AMBIGUOUS",
922
- "STATEFUL_CONTROL_PLANE_REQUEST_FAILED",
923
- "STATEFUL_CONTROL_PLANE_HTTP_ERROR",
924
- "STATEFUL_CONTROL_PLANE_INVALID_RESPONSE",
925
- ]);
932
+ function sdkOwnsErrorResolution(error: unknown): boolean {
933
+ if (isSessionExpiredError(error)) return true;
934
+ if (isTransportError(error)) return true;
935
+ if (error instanceof z.ZodError) return true;
936
+ if (error instanceof StatefulRoutingDeadlineError) return true;
937
+ return (
938
+ isProviderError(error) &&
939
+ typeof error.code === "string" &&
940
+ SDK_RUNTIME_OWNED_ERROR_CODES.has(error.code)
941
+ );
942
+ }
943
+
944
+ type OperationErrorCodeLookup = ReadonlyMap<string, ReadonlyMap<string, OperationErrorCode>>;
945
+
946
+ function buildOperationErrorCodeLookup(provider: ProviderDefinition): OperationErrorCodeLookup {
947
+ return new Map(
948
+ Object.entries(provider.operations).flatMap(([operationId, operation]) => {
949
+ const errorCodes = operation.docs?.errorCodes;
950
+ return errorCodes?.length
951
+ ? [[operationId, new Map(errorCodes.map((entry) => [entry.code, entry]))] as const]
952
+ : [];
953
+ }),
954
+ );
955
+ }
956
+
957
+ function declaredErrorCodeFor(
958
+ error: unknown,
959
+ operationId: string | undefined,
960
+ lookup: OperationErrorCodeLookup,
961
+ ): OperationErrorCode | undefined {
962
+ if (!operationId || !isProviderError(error) || typeof error.code !== "string") return undefined;
963
+ return lookup.get(operationId)?.get(error.code);
964
+ }
926
965
 
927
966
  function extractRequestId(raw: unknown): string | undefined {
928
967
  if (!raw || typeof raw !== "object") {
@@ -942,6 +981,7 @@ function logProviderError(
942
981
  error: unknown,
943
982
  status: number,
944
983
  cost: ProviderRequestCost,
984
+ declaredErrorCode?: OperationErrorCode,
945
985
  ): void {
946
986
  const code = isProviderError(error)
947
987
  ? (error.code ?? "provider_error")
@@ -952,13 +992,14 @@ function logProviderError(
952
992
  : "internal_error";
953
993
  const errorClass = error instanceof Error ? error.name : typeof error;
954
994
  const message = error instanceof Error ? error.message : String(error);
955
- const details = errorObservabilityDetails(error);
995
+ const details = errorObservabilityDetails(error, declaredErrorCode);
956
996
  const isUnregisteredProviderErrorCode =
957
997
  status === 500 &&
958
998
  isProviderError(error) &&
959
999
  !isValidationError(error) &&
960
1000
  typeof error.code === "string" &&
961
- !SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code);
1001
+ !SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code) &&
1002
+ declaredErrorCode === undefined;
962
1003
  const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
963
1004
  emit({
964
1005
  level: status >= 500 ? "error" : "warn",
@@ -1693,6 +1734,7 @@ export function createServerApp(
1693
1734
  validateStatefulServerConfig(options);
1694
1735
  const app = new Hono();
1695
1736
  const logger = options.logger ?? defaultProviderServerLogger;
1737
+ const operationErrorCodes = buildOperationErrorCodeLookup(provider);
1696
1738
  const statefulForwardingReplayCache = new StatefulForwardingReplayCache(
1697
1739
  options.statefulForwarding?.replayCacheMaxEntries ??
1698
1740
  DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES,
@@ -1738,6 +1780,7 @@ export function createServerApp(
1738
1780
  app.post(STATEFUL_INTERNAL_OPERATIONS_ROUTE, async (c) => {
1739
1781
  let rawBodyText = "";
1740
1782
  let rawBody: unknown;
1783
+ let operationId: string | undefined;
1741
1784
  const operation = "stateful-internal";
1742
1785
  const requestCost = startRequestCost();
1743
1786
  try {
@@ -1848,7 +1891,7 @@ export function createServerApp(
1848
1891
  });
1849
1892
  }
1850
1893
  const request = operationRequestFromForwardingEnvelope(envelope);
1851
- const operationId = envelope.operationId;
1894
+ operationId = envelope.operationId;
1852
1895
  const ctx = createProviderContext(provider, request, operationId, options, state);
1853
1896
  if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
1854
1897
  throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
@@ -1872,7 +1915,8 @@ export function createServerApp(
1872
1915
  );
1873
1916
  return c.json({ data: output });
1874
1917
  } catch (error) {
1875
- const status = toStatusCode(error);
1918
+ const declaredErrorCode = declaredErrorCodeFor(error, operationId, operationErrorCodes);
1919
+ const status = toStatusCode(error, declaredErrorCode);
1876
1920
  if (isProviderError(error) && error.code === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
1877
1921
  c.header("Retry-After", String(STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS));
1878
1922
  }
@@ -1881,15 +1925,17 @@ export function createServerApp(
1881
1925
  logger,
1882
1926
  provider,
1883
1927
  "operation",
1884
- operation,
1928
+ operationId || operation,
1885
1929
  requestId,
1886
1930
  error,
1887
1931
  status,
1888
1932
  finishRequestCost(requestCost),
1933
+ declaredErrorCode,
1889
1934
  );
1890
1935
  return responseWithErrorObservability(
1891
- c.json(toErrorResponse(error, requestId), status),
1936
+ c.json(toErrorResponse(error, requestId, declaredErrorCode), status),
1892
1937
  error,
1938
+ declaredErrorCode,
1893
1939
  );
1894
1940
  }
1895
1941
  });
@@ -1940,7 +1986,8 @@ export function createServerApp(
1940
1986
  );
1941
1987
  return c.json(response);
1942
1988
  } catch (error) {
1943
- const status = toStatusCode(error);
1989
+ const declaredErrorCode = declaredErrorCodeFor(error, operation, operationErrorCodes);
1990
+ const status = toStatusCode(error, declaredErrorCode);
1944
1991
  const requestId = extractRequestId(rawBody);
1945
1992
  logProviderError(
1946
1993
  logger,
@@ -1951,12 +1998,14 @@ export function createServerApp(
1951
1998
  error,
1952
1999
  status,
1953
2000
  finishRequestCost(requestCost),
2001
+ declaredErrorCode,
1954
2002
  );
1955
2003
  const telemetryHeader = proxyTelemetry.toHeaderValue();
1956
2004
  if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1957
2005
  return responseWithErrorObservability(
1958
- c.json(toErrorResponse(error, requestId), status),
2006
+ c.json(toErrorResponse(error, requestId, declaredErrorCode), status),
1959
2007
  error,
2008
+ declaredErrorCode,
1960
2009
  );
1961
2010
  }
1962
2011
  });
@@ -4,6 +4,12 @@ import { createProviderCache } from "../runtime/cache.js";
4
4
  import { createTestProviderChoiceContext } from "../runtime/choice.js";
5
5
  import { createMemoryProviderRuntimeState } from "../runtime/state.js";
6
6
  import { createUnsupportedSttClient } from "../runtime/stt.js";
7
+ import {
8
+ createNativeEgressAuthorization,
9
+ NativeNetworkError,
10
+ snapshotNativeConnectInput,
11
+ snapshotNativeGrantInput,
12
+ } from "../runtime/native-network.js";
7
13
  import { safeParseSchemaSync } from "../schema.js";
8
14
  import { requestPathForFixture } from "../fixture-sanitization.js";
9
15
  import { findStreamCaptureGroup, replayStreamEvidence } from "../stream-evidence.js";
@@ -307,6 +313,17 @@ function createUpstreamContext(
307
313
  };
308
314
  const request = { headers: {} };
309
315
  const state = createMemoryProviderRuntimeState();
316
+ const nativeEgress = provider.native
317
+ ? createNativeEgressAuthorization({ egress: provider.native.network })
318
+ : undefined;
319
+ const requireNativeEgress = () => {
320
+ if (!nativeEgress)
321
+ throw new NativeNetworkError(
322
+ "Native egress authorization is unavailable",
323
+ "native_egress_policy_invalid",
324
+ );
325
+ return nativeEgress;
326
+ };
310
327
  const dispatch = async (
311
328
  call: Omit<StandardTestsUpstreamCall, "operationName">,
312
329
  ): Promise<NormalizedUpstreamResponse> => {
@@ -498,30 +515,38 @@ function createUpstreamContext(
498
515
  ...(provider.native
499
516
  ? {
500
517
  native: {
501
- network: {
502
- connectTcp: async (options) =>
503
- createNativeConnection(
518
+ network: {
519
+ connectTcp: async (options) => {
520
+ const request = snapshotNativeConnectInput(options);
521
+ requireNativeEgress().assertConnect(request, "disabled");
522
+ return createNativeConnection(
504
523
  await dispatch({
505
524
  transport: "native",
506
525
  method: "connectTcp",
507
- url: `tcp://${options.host}:${options.port}`,
508
- options,
526
+ url: `tcp://${request.host}:${request.port}`,
527
+ options: request,
509
528
  }),
510
529
  dispatch,
511
- `tcp://${options.host}:${options.port}`,
512
- ),
513
- connectTls: async (options) =>
514
- createNativeConnection(
530
+ `tcp://${request.host}:${request.port}`,
531
+ );
532
+ },
533
+ connectTls: async (options) => {
534
+ const request = snapshotNativeConnectInput(options);
535
+ requireNativeEgress().assertConnect(request, "required");
536
+ return createNativeConnection(
515
537
  await dispatch({
516
538
  transport: "native",
517
539
  method: "connectTls",
518
- url: `tls://${options.host}:${options.port}`,
519
- options,
540
+ url: `tls://${request.host}:${request.port}`,
541
+ options: request,
520
542
  }),
521
543
  dispatch,
522
- `tls://${options.host}:${options.port}`,
523
- ),
524
- grantTcpEgress: () => ({ revoke: () => {} }),
544
+ `tls://${request.host}:${request.port}`,
545
+ );
546
+ },
547
+ grantTcpEgress: (input) => {
548
+ return requireNativeEgress().grant(snapshotNativeGrantInput(input));
549
+ },
525
550
  },
526
551
  },
527
552
  }