@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.
- package/AUTHORING.md +52 -13
- package/CHANGELOG.md +11 -0
- package/dist/define.js +29 -1
- package/dist/error-resolution.d.ts +2 -0
- package/dist/error-resolution.js +90 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +19 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/native-egress-policy.d.ts +27 -0
- package/dist/native-egress-policy.js +225 -0
- package/dist/provider.d.ts +3 -3
- package/dist/provider.js +2 -2
- package/dist/runtime/http.js +189 -9
- package/dist/runtime/native-network.d.ts +39 -4
- package/dist/runtime/native-network.js +365 -20
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/stealth.js +16 -44
- package/dist/server/serve.js +105 -94
- package/dist/testing/run.js +32 -13
- package/dist/types.d.ts +26 -3
- package/dist/types.js +1 -0
- package/package.json +1 -1
- package/src/define.ts +44 -0
- package/src/error-resolution.ts +91 -0
- package/src/errors.ts +28 -0
- package/src/index.ts +7 -0
- package/src/native-egress-policy.ts +285 -0
- package/src/provider.ts +8 -0
- package/src/runtime/http.ts +217 -9
- package/src/runtime/native-network.ts +474 -22
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/stealth.ts +20 -47
- package/src/server/serve.ts +141 -92
- package/src/testing/run.ts +39 -14
- package/src/types.ts +37 -3
package/src/runtime/stealth.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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:
|
|
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
|
-
|
|
1335
|
-
|
|
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) {
|
package/src/server/serve.ts
CHANGED
|
@@ -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(
|
|
617
|
-
|
|
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(
|
|
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(
|
|
739
|
-
|
|
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
|
-
:
|
|
800
|
+
: isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
|
|
801
|
+
? "provider_error"
|
|
802
|
+
: "input_validation",
|
|
748
803
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
749
|
-
retryable: isProviderError(error)
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
"
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
});
|
package/src/testing/run.ts
CHANGED
|
@@ -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
|
-
|
|
502
|
-
connectTcp: async (options) =>
|
|
503
|
-
|
|
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://${
|
|
508
|
-
options,
|
|
526
|
+
url: `tcp://${request.host}:${request.port}`,
|
|
527
|
+
options: request,
|
|
509
528
|
}),
|
|
510
529
|
dispatch,
|
|
511
|
-
`tcp://${
|
|
512
|
-
)
|
|
513
|
-
|
|
514
|
-
|
|
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://${
|
|
519
|
-
options,
|
|
540
|
+
url: `tls://${request.host}:${request.port}`,
|
|
541
|
+
options: request,
|
|
520
542
|
}),
|
|
521
543
|
dispatch,
|
|
522
|
-
`tls://${
|
|
523
|
-
)
|
|
524
|
-
|
|
544
|
+
`tls://${request.host}:${request.port}`,
|
|
545
|
+
);
|
|
546
|
+
},
|
|
547
|
+
grantTcpEgress: (input) => {
|
|
548
|
+
return requireNativeEgress().grant(snapshotNativeGrantInput(input));
|
|
549
|
+
},
|
|
525
550
|
},
|
|
526
551
|
},
|
|
527
552
|
}
|