@apifuse/provider-sdk 2.2.0-beta.40 → 2.2.0-beta.41
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/CHANGELOG.md +4 -0
- package/bin/apifuse-check.ts +61 -0
- package/bin/apifuse-migrate-shape.ts +84 -0
- package/bin/apifuse-submit-check.ts +1760 -222
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.js +6 -0
- package/dist/cli/migrate-provider-shape.d.ts +52 -0
- package/dist/cli/migrate-provider-shape.js +515 -0
- package/dist/cli/templates/provider/provider.json.tpl +6 -0
- package/dist/contract.js +1 -0
- package/dist/define.js +22 -1
- package/dist/error-observability.d.ts +7 -0
- package/dist/error-observability.js +61 -0
- package/dist/errors.d.ts +15 -0
- package/dist/fixture-sanitization.js +13 -3
- package/dist/index.d.ts +1 -1
- package/dist/provider.d.ts +1 -1
- package/dist/runtime/executor.js +11 -1
- package/dist/server/error-observability.d.ts +1 -0
- package/dist/server/error-observability.js +1 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/self-test.js +3 -0
- package/dist/server/serve-implementation.d.ts +12 -0
- package/dist/server/serve-implementation.js +135 -66
- package/dist/types.d.ts +18 -10
- package/package.json +3 -3
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +6 -0
- package/src/cli/migrate-provider-shape.ts +701 -0
- package/src/cli/templates/provider/provider.json.tpl +6 -0
- package/src/contract.ts +1 -0
- package/src/define.ts +33 -1
- package/src/error-observability.ts +64 -0
- package/src/errors.ts +16 -0
- package/src/fixture-sanitization.ts +19 -3
- package/src/index.ts +1 -0
- package/src/provider.ts +1 -0
- package/src/runtime/executor.ts +13 -1
- package/src/server/error-observability.ts +1 -0
- package/src/server/index.ts +2 -0
- package/src/server/self-test.ts +5 -0
- package/src/server/serve-implementation.ts +172 -84
- package/src/types.ts +38 -27
|
@@ -6,8 +6,10 @@ import { Hono } from "hono";
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { AuthAbortError, createAuthFlowHelpers } from "../auth.js";
|
|
8
8
|
import { validateFailClosedDeclaration } from "../declaration-validation.js";
|
|
9
|
+
import { safeProviderErrorObservability } from "../error-observability.js";
|
|
9
10
|
import { SDK_OWNED_PROVIDER_ERROR_CODES, SDK_RUNTIME_OWNED_ERROR_CODES, SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES, } from "../error-resolution.js";
|
|
10
11
|
import { AuthError, isProviderError, isSessionExpiredError, isTransportError, isValidationError, ProviderError, } from "../errors.js";
|
|
12
|
+
import { sanitizeDiagnosticText } from "../fixture-sanitization.js";
|
|
11
13
|
import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.js";
|
|
12
14
|
import { categoryForStatus, sourceForCategory, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
|
|
13
15
|
import { createScratchpad } from "../runtime/auth-flow.js";
|
|
@@ -42,6 +44,32 @@ const DEFAULT_HOST = "0.0.0.0";
|
|
|
42
44
|
const DEFAULT_PORT = 3000;
|
|
43
45
|
/** Compact SDK-owned error classification emitted separately from the public response body. */
|
|
44
46
|
export const ERROR_OBSERVABILITY_HEADER = "X-ApiFuse-Error-Observability";
|
|
47
|
+
// Provider errors normally expose `options` through an own data property, but
|
|
48
|
+
// providers can replace that property with a throwing accessor. Provider-controlled
|
|
49
|
+
// accessor failures are absorbed into canonical classification rather than
|
|
50
|
+
// propagated. By contract, read `options` directly instead of the public
|
|
51
|
+
// `fix`/`details` convenience getters; provider-controlled accessors are not trusted.
|
|
52
|
+
function providerErrorOption(error, key) {
|
|
53
|
+
if (!isProviderError(error))
|
|
54
|
+
return undefined;
|
|
55
|
+
try {
|
|
56
|
+
return error.options?.[key];
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function providerErrorCode(error) {
|
|
63
|
+
if (!isProviderError(error))
|
|
64
|
+
return undefined;
|
|
65
|
+
try {
|
|
66
|
+
const code = error.code;
|
|
67
|
+
return typeof code === "string" ? code : undefined;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
45
73
|
const AUTH_FLOW_LOCALES = ["en", "ko", "ja"];
|
|
46
74
|
const retryResponseMeta = new WeakMap();
|
|
47
75
|
const STATEFUL_INTERNAL_OPERATIONS_ROUTE = "/__apifuse/stateful/operations";
|
|
@@ -655,16 +683,17 @@ function publicErrorSource(error, category) {
|
|
|
655
683
|
if (error instanceof StatefulRoutingDeadlineError)
|
|
656
684
|
return "apifuse";
|
|
657
685
|
if (isProviderError(error)) {
|
|
658
|
-
|
|
686
|
+
const code = providerErrorCode(error);
|
|
687
|
+
if (code === MISSING_SECRET_CODE)
|
|
659
688
|
return "apifuse";
|
|
660
|
-
if (
|
|
689
|
+
if (code === "UPSTREAM_ERROR" || code === "BLOCKED") {
|
|
661
690
|
return "upstream_failure";
|
|
662
691
|
}
|
|
663
692
|
}
|
|
664
693
|
return sourceForCategory(category);
|
|
665
694
|
}
|
|
666
|
-
function toErrorResponse(error, requestId,
|
|
667
|
-
const observability =
|
|
695
|
+
function toErrorResponse(error, requestId, observabilityDetails) {
|
|
696
|
+
const observability = observabilityDetails;
|
|
668
697
|
const source = publicErrorSource(error, observability.category);
|
|
669
698
|
if (error instanceof StatefulRoutingDeadlineError) {
|
|
670
699
|
return {
|
|
@@ -678,15 +707,15 @@ function toErrorResponse(error, requestId, declaredErrorCode) {
|
|
|
678
707
|
};
|
|
679
708
|
}
|
|
680
709
|
if (isProviderError(error)) {
|
|
681
|
-
const details = error
|
|
710
|
+
const details = providerErrorOption(error, "details");
|
|
682
711
|
return {
|
|
683
712
|
error: {
|
|
684
|
-
code: error
|
|
713
|
+
code: providerErrorCode(error) ?? "provider_error",
|
|
685
714
|
message: publicProviderErrorMessage(error),
|
|
686
715
|
...(requestId ? { requestId } : {}),
|
|
687
716
|
retryable: observability.retryable,
|
|
688
717
|
source,
|
|
689
|
-
...(error
|
|
718
|
+
...(providerErrorOption(error, "fix") ? { fix: providerErrorOption(error, "fix") } : {}),
|
|
690
719
|
...(details !== undefined ? { details } : {}),
|
|
691
720
|
},
|
|
692
721
|
};
|
|
@@ -740,9 +769,9 @@ function providerObservabilityDetails(error, declaredErrorCode) {
|
|
|
740
769
|
// signal for exactly the retryOnAuthRefresh operations it is meant to enable.
|
|
741
770
|
if (isSessionExpiredError(error)) {
|
|
742
771
|
return {
|
|
743
|
-
category: error
|
|
772
|
+
category: providerErrorOption(error, "category") ?? "credential_expired",
|
|
744
773
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
745
|
-
retryable: error
|
|
774
|
+
retryable: providerErrorOption(error, "retryable") ?? declaredRetryable ?? false,
|
|
746
775
|
};
|
|
747
776
|
}
|
|
748
777
|
// Missing-secret errors carry the canonical credential_unavailable category
|
|
@@ -750,27 +779,27 @@ function providerObservabilityDetails(error, declaredErrorCode) {
|
|
|
750
779
|
// the upstream. Matched by code (not constructor) so both the SDK-owned
|
|
751
780
|
// runtime gate and any not-yet-migrated provider-thrown MISSING_SECRET
|
|
752
781
|
// serialize identically, including across duplicate SDK module instances.
|
|
753
|
-
if (isProviderError(error) && error
|
|
782
|
+
if (isProviderError(error) && providerErrorCode(error) === MISSING_SECRET_CODE) {
|
|
754
783
|
return {
|
|
755
|
-
category: error
|
|
784
|
+
category: providerErrorOption(error, "category") ?? "credential_unavailable",
|
|
756
785
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
757
|
-
retryable: error
|
|
786
|
+
retryable: providerErrorOption(error, "retryable") ?? declaredRetryable ?? false,
|
|
758
787
|
};
|
|
759
788
|
}
|
|
760
789
|
if (!isTransportError(error)) {
|
|
761
790
|
return undefined;
|
|
762
791
|
}
|
|
763
|
-
const isProxyPoolCode = error
|
|
764
|
-
error
|
|
765
|
-
error
|
|
766
|
-
const category = error
|
|
792
|
+
const isProxyPoolCode = providerErrorCode(error) === PROXY_POOL_EXHAUSTED_CODE ||
|
|
793
|
+
providerErrorCode(error) === PROXY_EDGE_AUTH_REJECTED_CODE ||
|
|
794
|
+
providerErrorCode(error) === "PROXY_ALLOCATION_FAILED";
|
|
795
|
+
const category = providerErrorOption(error, "category") ??
|
|
767
796
|
(isProxyPoolCode
|
|
768
797
|
? "proxy_pool"
|
|
769
|
-
: error
|
|
798
|
+
: providerErrorCode(error) === PROXY_AUTH_IP_DENIED_CODE
|
|
770
799
|
? "anti_bot_blocked"
|
|
771
|
-
: error
|
|
800
|
+
: providerErrorCode(error) === "transport_timeout"
|
|
772
801
|
? "timeout"
|
|
773
|
-
: error
|
|
802
|
+
: providerErrorCode(error) === "transport_network_error"
|
|
774
803
|
? "network"
|
|
775
804
|
: error.upstreamStatus
|
|
776
805
|
? categoryForStatus(error.upstreamStatus)
|
|
@@ -778,23 +807,24 @@ function providerObservabilityDetails(error, declaredErrorCode) {
|
|
|
778
807
|
return {
|
|
779
808
|
category,
|
|
780
809
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
781
|
-
retryable: error
|
|
810
|
+
retryable: providerErrorOption(error, "retryable") ??
|
|
782
811
|
(category === "upstream_http" && error.upstreamStatus
|
|
783
812
|
? error.upstreamStatus >= 500
|
|
784
813
|
: isRetryableCategory(category)),
|
|
785
814
|
...(error.upstreamStatus ? { upstreamStatus: error.upstreamStatus } : {}),
|
|
786
815
|
};
|
|
787
816
|
}
|
|
788
|
-
function
|
|
817
|
+
function classifiedErrorObservabilityDetails(error, declaredErrorCode) {
|
|
789
818
|
const effectiveDeclaration = sdkOwnsErrorResolution(error) ? undefined : declaredErrorCode;
|
|
790
819
|
const providerDetails = providerObservabilityDetails(error, effectiveDeclaration);
|
|
791
820
|
if (providerDetails)
|
|
792
821
|
return providerDetails;
|
|
793
822
|
if (error instanceof z.ZodError || isValidationError(error)) {
|
|
794
823
|
const declaredStatus = effectiveDeclaration?.status;
|
|
824
|
+
const providerCategory = providerErrorOption(error, "category");
|
|
795
825
|
return {
|
|
796
|
-
category:
|
|
797
|
-
?
|
|
826
|
+
category: providerCategory
|
|
827
|
+
? providerCategory
|
|
798
828
|
: isEmittableErrorStatus(declaredStatus) &&
|
|
799
829
|
categoryForStatus(declaredStatus) === "upstream_rejected"
|
|
800
830
|
? "upstream_rejected"
|
|
@@ -803,7 +833,7 @@ function errorObservabilityDetails(error, declaredErrorCode) {
|
|
|
803
833
|
: "input_validation",
|
|
804
834
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
805
835
|
retryable: isProviderError(error)
|
|
806
|
-
? (error
|
|
836
|
+
? (providerErrorOption(error, "retryable") ?? effectiveDeclaration?.retryable ?? false)
|
|
807
837
|
: false,
|
|
808
838
|
};
|
|
809
839
|
}
|
|
@@ -820,15 +850,15 @@ function errorObservabilityDetails(error, declaredErrorCode) {
|
|
|
820
850
|
// status (409/410/422) classify as upstream_rejected unless the
|
|
821
851
|
// author set an explicit category.
|
|
822
852
|
const declaredStatus = effectiveDeclaration?.status;
|
|
823
|
-
const rejectionDefault = error
|
|
853
|
+
const rejectionDefault = providerErrorCode(error) === "UPSTREAM_REJECTED" ||
|
|
824
854
|
(isEmittableErrorStatus(declaredStatus) &&
|
|
825
855
|
categoryForStatus(declaredStatus) === "upstream_rejected")
|
|
826
856
|
? "upstream_rejected"
|
|
827
857
|
: "provider_error";
|
|
828
858
|
return {
|
|
829
|
-
category: error
|
|
859
|
+
category: providerErrorOption(error, "category") ?? rejectionDefault,
|
|
830
860
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
831
|
-
retryable: error
|
|
861
|
+
retryable: providerErrorOption(error, "retryable") ?? effectiveDeclaration?.retryable ?? false,
|
|
832
862
|
};
|
|
833
863
|
}
|
|
834
864
|
return {
|
|
@@ -837,9 +867,17 @@ function errorObservabilityDetails(error, declaredErrorCode) {
|
|
|
837
867
|
retryable: false,
|
|
838
868
|
};
|
|
839
869
|
}
|
|
840
|
-
function
|
|
870
|
+
function errorObservabilityDetails(error, declaredErrorCode) {
|
|
871
|
+
const details = classifiedErrorObservabilityDetails(error, declaredErrorCode);
|
|
872
|
+
const providerObservability = safeProviderErrorObservability(error);
|
|
873
|
+
return {
|
|
874
|
+
...details,
|
|
875
|
+
...(providerObservability ? { providerObservability } : {}),
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function responseWithErrorObservability(response, observabilityDetails) {
|
|
841
879
|
const headers = new Headers(response.headers);
|
|
842
|
-
headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(
|
|
880
|
+
headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(observabilityDetails));
|
|
843
881
|
return new Response(response.body, {
|
|
844
882
|
status: response.status,
|
|
845
883
|
statusText: response.statusText,
|
|
@@ -848,20 +886,20 @@ function responseWithErrorObservability(response, error, declaredErrorCode) {
|
|
|
848
886
|
}
|
|
849
887
|
function publicProviderErrorMessage(error) {
|
|
850
888
|
if (isTransportError(error)) {
|
|
851
|
-
if (error
|
|
889
|
+
if (providerErrorCode(error) === PROXY_AUTH_IP_DENIED_CODE) {
|
|
852
890
|
return error.message;
|
|
853
891
|
}
|
|
854
|
-
if (error
|
|
892
|
+
if (providerErrorCode(error) === PROXY_EDGE_AUTH_REJECTED_CODE) {
|
|
855
893
|
return error.message;
|
|
856
894
|
}
|
|
857
|
-
if (error
|
|
895
|
+
if (providerErrorCode(error) === PROXY_POOL_EXHAUSTED_CODE) {
|
|
858
896
|
return error.message;
|
|
859
897
|
}
|
|
860
|
-
if (error
|
|
898
|
+
if (providerErrorCode(error) === "transport_timeout")
|
|
861
899
|
return "Request timed out";
|
|
862
|
-
if (error
|
|
900
|
+
if (providerErrorCode(error) === "transport_network_error")
|
|
863
901
|
return "Network error";
|
|
864
|
-
if (error
|
|
902
|
+
if (providerErrorCode(error) === "upstream_http_error" && error.status) {
|
|
865
903
|
return `Upstream request failed with status ${error.status}`;
|
|
866
904
|
}
|
|
867
905
|
if (error.status) {
|
|
@@ -887,17 +925,18 @@ function toStatusCode(error, declaredErrorCode) {
|
|
|
887
925
|
}
|
|
888
926
|
// Canonical SDK code → status mapping lives in error-resolution.ts so
|
|
889
927
|
// the authoring lint and this runtime path share one source of truth.
|
|
890
|
-
|
|
891
|
-
|
|
928
|
+
const code = providerErrorCode(error);
|
|
929
|
+
if (typeof code === "string") {
|
|
930
|
+
const mappedStatus = SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES.get(code);
|
|
892
931
|
if (mappedStatus !== undefined) {
|
|
893
932
|
return mappedStatus;
|
|
894
933
|
}
|
|
895
934
|
}
|
|
896
935
|
if (isTransportError(error)) {
|
|
897
|
-
return error
|
|
936
|
+
return providerErrorCode(error) === "transport_timeout" ? 504 : 502;
|
|
898
937
|
}
|
|
899
938
|
if (isValidationError(error)) {
|
|
900
|
-
return error
|
|
939
|
+
return providerErrorOption(error, "category") === "output_validation" ? 500 : 400;
|
|
901
940
|
}
|
|
902
941
|
return 500;
|
|
903
942
|
}
|
|
@@ -912,9 +951,10 @@ function sdkOwnsErrorResolution(error) {
|
|
|
912
951
|
return true;
|
|
913
952
|
if (error instanceof StatefulRoutingDeadlineError)
|
|
914
953
|
return true;
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
954
|
+
if (!isProviderError(error))
|
|
955
|
+
return false;
|
|
956
|
+
const code = providerErrorCode(error);
|
|
957
|
+
return typeof code === "string" && SDK_RUNTIME_OWNED_ERROR_CODES.has(code);
|
|
918
958
|
}
|
|
919
959
|
function buildOperationErrorCodeLookup(provider) {
|
|
920
960
|
return new Map(Object.entries(provider.operations).flatMap(([operationId, operation]) => {
|
|
@@ -925,9 +965,10 @@ function buildOperationErrorCodeLookup(provider) {
|
|
|
925
965
|
}));
|
|
926
966
|
}
|
|
927
967
|
function declaredErrorCodeFor(error, operationId, lookup) {
|
|
928
|
-
|
|
968
|
+
const code = providerErrorCode(error);
|
|
969
|
+
if (!operationId || !code)
|
|
929
970
|
return undefined;
|
|
930
|
-
return lookup.get(operationId)?.get(
|
|
971
|
+
return lookup.get(operationId)?.get(code);
|
|
931
972
|
}
|
|
932
973
|
function extractRequestId(raw) {
|
|
933
974
|
if (!raw || typeof raw !== "object") {
|
|
@@ -937,6 +978,13 @@ function extractRequestId(raw) {
|
|
|
937
978
|
return typeof value === "string" ? value : undefined;
|
|
938
979
|
}
|
|
939
980
|
const MAX_PROVIDER_ERROR_CAUSE_FRAMES = 5;
|
|
981
|
+
const MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH = 300;
|
|
982
|
+
function providerErrorCauseMessage(message) {
|
|
983
|
+
const sanitized = sanitizeDiagnosticText(message);
|
|
984
|
+
return sanitized.length > MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH
|
|
985
|
+
? `${sanitized.slice(0, MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH)}… [truncated]`
|
|
986
|
+
: sanitized;
|
|
987
|
+
}
|
|
940
988
|
function providerErrorCauseChain(error) {
|
|
941
989
|
if (!(error instanceof Error) && !isProviderError(error))
|
|
942
990
|
return undefined;
|
|
@@ -948,19 +996,24 @@ function providerErrorCauseChain(error) {
|
|
|
948
996
|
!seen.has(cause)) {
|
|
949
997
|
seen.add(cause);
|
|
950
998
|
const message = cause.message;
|
|
999
|
+
const providerObservability = safeProviderErrorObservability(cause);
|
|
1000
|
+
const causeCode = providerErrorCode(cause);
|
|
951
1001
|
frames.push({
|
|
952
1002
|
errorClass: cause.name,
|
|
953
|
-
...(
|
|
1003
|
+
...(causeCode !== undefined ? { code: causeCode } : {}),
|
|
1004
|
+
message: providerErrorCauseMessage(message),
|
|
954
1005
|
messageLength: message.length,
|
|
955
1006
|
messageFingerprint: createHash("sha256").update(message).digest("hex").slice(0, 12),
|
|
1007
|
+
...(providerObservability ? { providerObservability } : {}),
|
|
956
1008
|
});
|
|
957
1009
|
cause = cause.cause;
|
|
958
1010
|
}
|
|
959
1011
|
return frames.length > 0 ? frames : undefined;
|
|
960
1012
|
}
|
|
961
|
-
function logProviderError(logger, provider, kind, route, requestId, error, status, cost, declaredErrorCode, proxyTelemetry) {
|
|
1013
|
+
function logProviderError(logger, provider, kind, route, requestId, error, status, cost, declaredErrorCode, proxyTelemetry, observabilityDetails) {
|
|
1014
|
+
const providerCode = isProviderError(error) ? providerErrorCode(error) : undefined;
|
|
962
1015
|
const code = isProviderError(error)
|
|
963
|
-
? (
|
|
1016
|
+
? (providerCode ?? "provider_error")
|
|
964
1017
|
: error instanceof z.ZodError
|
|
965
1018
|
? "invalid_request"
|
|
966
1019
|
: error instanceof StatefulRoutingDeadlineError
|
|
@@ -969,15 +1022,21 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
|
|
|
969
1022
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
970
1023
|
const message = error instanceof Error ? error.message : String(error);
|
|
971
1024
|
const causeChain = providerErrorCauseChain(error);
|
|
972
|
-
const details =
|
|
1025
|
+
const details = observabilityDetails;
|
|
973
1026
|
const isUnregisteredProviderErrorCode = status === 500 &&
|
|
974
1027
|
isProviderError(error) &&
|
|
975
1028
|
!isValidationError(error) &&
|
|
976
|
-
typeof
|
|
977
|
-
!SDK_OWNED_PROVIDER_ERROR_CODES.has(
|
|
1029
|
+
typeof providerCode === "string" &&
|
|
1030
|
+
!SDK_OWNED_PROVIDER_ERROR_CODES.has(providerCode) &&
|
|
978
1031
|
declaredErrorCode === undefined;
|
|
979
1032
|
const proxy = proxyTelemetry?.toLogPayload();
|
|
980
1033
|
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
1034
|
+
// The logger is caller-supplied and may mutate the event synchronously.
|
|
1035
|
+
// Give it an independent snapshot so those mutations cannot corrupt the
|
|
1036
|
+
// observability header serialized immediately afterwards.
|
|
1037
|
+
const providerObservability = details.providerObservability
|
|
1038
|
+
? { ...details.providerObservability }
|
|
1039
|
+
: undefined;
|
|
981
1040
|
emit({
|
|
982
1041
|
level: status >= 500 ? "error" : "warn",
|
|
983
1042
|
event: "provider_request_failed",
|
|
@@ -992,6 +1051,7 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
|
|
|
992
1051
|
errorClass,
|
|
993
1052
|
message,
|
|
994
1053
|
...(causeChain ? { causeChain } : {}),
|
|
1054
|
+
...(providerObservability ? { providerObservability } : {}),
|
|
995
1055
|
...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
|
|
996
1056
|
errorCategory: details.category,
|
|
997
1057
|
taxonomyVersion: details.taxonomyVersion,
|
|
@@ -1629,7 +1689,8 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1629
1689
|
}
|
|
1630
1690
|
app.notFound((c) => {
|
|
1631
1691
|
const error = new ProviderError("Not found", { code: "not_found", retryable: false });
|
|
1632
|
-
|
|
1692
|
+
const observabilityDetails = errorObservabilityDetails(error);
|
|
1693
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, undefined, observabilityDetails), 404), observabilityDetails);
|
|
1633
1694
|
});
|
|
1634
1695
|
app.get("/health", (c) => c.json({
|
|
1635
1696
|
status: "ok",
|
|
@@ -1744,12 +1805,14 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1744
1805
|
catch (error) {
|
|
1745
1806
|
const declaredErrorCode = declaredErrorCodeFor(error, operationId, operationErrorCodes);
|
|
1746
1807
|
const status = toStatusCode(error, declaredErrorCode);
|
|
1747
|
-
if (isProviderError(error) &&
|
|
1808
|
+
if (isProviderError(error) &&
|
|
1809
|
+
providerErrorCode(error) === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
|
|
1748
1810
|
c.header("Retry-After", String(STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS));
|
|
1749
1811
|
}
|
|
1750
1812
|
const requestId = extractRequestId(rawBody);
|
|
1751
|
-
|
|
1752
|
-
|
|
1813
|
+
const observabilityDetails = errorObservabilityDetails(error, declaredErrorCode);
|
|
1814
|
+
logProviderError(logger, provider, "operation", operationId || operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode, undefined, observabilityDetails);
|
|
1815
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, observabilityDetails), status), observabilityDetails);
|
|
1753
1816
|
}
|
|
1754
1817
|
});
|
|
1755
1818
|
app.post("/v1/:operation", async (c) => {
|
|
@@ -1780,11 +1843,12 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1780
1843
|
const declaredErrorCode = declaredErrorCodeFor(error, operation, operationErrorCodes);
|
|
1781
1844
|
const status = toStatusCode(error, declaredErrorCode);
|
|
1782
1845
|
const requestId = extractRequestId(rawBody);
|
|
1783
|
-
|
|
1846
|
+
const observabilityDetails = errorObservabilityDetails(error, declaredErrorCode);
|
|
1847
|
+
logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode, proxyTelemetry, observabilityDetails);
|
|
1784
1848
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1785
1849
|
if (telemetryHeader)
|
|
1786
1850
|
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1787
|
-
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId,
|
|
1851
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, observabilityDetails), status), observabilityDetails);
|
|
1788
1852
|
}
|
|
1789
1853
|
});
|
|
1790
1854
|
app.post("/auth/start", async (c) => {
|
|
@@ -1809,11 +1873,12 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1809
1873
|
catch (error) {
|
|
1810
1874
|
const status = toStatusCode(error);
|
|
1811
1875
|
const requestId = extractRequestId(rawBody);
|
|
1812
|
-
|
|
1876
|
+
const observabilityDetails = errorObservabilityDetails(error);
|
|
1877
|
+
logProviderError(logger, provider, "auth", "start", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry, observabilityDetails);
|
|
1813
1878
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1814
1879
|
if (telemetryHeader)
|
|
1815
1880
|
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1816
|
-
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status),
|
|
1881
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, observabilityDetails), status), observabilityDetails);
|
|
1817
1882
|
}
|
|
1818
1883
|
});
|
|
1819
1884
|
app.post("/auth/continue", async (c) => {
|
|
@@ -1838,11 +1903,12 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1838
1903
|
catch (error) {
|
|
1839
1904
|
const status = toStatusCode(error);
|
|
1840
1905
|
const requestId = extractRequestId(rawBody);
|
|
1841
|
-
|
|
1906
|
+
const observabilityDetails = errorObservabilityDetails(error);
|
|
1907
|
+
logProviderError(logger, provider, "auth", "continue", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry, observabilityDetails);
|
|
1842
1908
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1843
1909
|
if (telemetryHeader)
|
|
1844
1910
|
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1845
|
-
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status),
|
|
1911
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, observabilityDetails), status), observabilityDetails);
|
|
1846
1912
|
}
|
|
1847
1913
|
});
|
|
1848
1914
|
app.post("/auth/poll", async (c) => {
|
|
@@ -1867,11 +1933,12 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1867
1933
|
catch (error) {
|
|
1868
1934
|
const status = toStatusCode(error);
|
|
1869
1935
|
const requestId = extractRequestId(rawBody);
|
|
1870
|
-
|
|
1936
|
+
const observabilityDetails = errorObservabilityDetails(error);
|
|
1937
|
+
logProviderError(logger, provider, "auth", "poll", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry, observabilityDetails);
|
|
1871
1938
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1872
1939
|
if (telemetryHeader)
|
|
1873
1940
|
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1874
|
-
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status),
|
|
1941
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, observabilityDetails), status), observabilityDetails);
|
|
1875
1942
|
}
|
|
1876
1943
|
});
|
|
1877
1944
|
app.post("/auth/refresh", async (c) => {
|
|
@@ -1896,11 +1963,12 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1896
1963
|
catch (error) {
|
|
1897
1964
|
const status = toStatusCode(error);
|
|
1898
1965
|
const requestId = extractRequestId(rawBody);
|
|
1899
|
-
|
|
1966
|
+
const observabilityDetails = errorObservabilityDetails(error);
|
|
1967
|
+
logProviderError(logger, provider, "auth", "refresh", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry, observabilityDetails);
|
|
1900
1968
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1901
1969
|
if (telemetryHeader)
|
|
1902
1970
|
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1903
|
-
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status),
|
|
1971
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, observabilityDetails), status), observabilityDetails);
|
|
1904
1972
|
}
|
|
1905
1973
|
});
|
|
1906
1974
|
app.post("/auth/disconnect", async (c) => {
|
|
@@ -1925,11 +1993,12 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1925
1993
|
catch (error) {
|
|
1926
1994
|
const status = toStatusCode(error);
|
|
1927
1995
|
const requestId = extractRequestId(rawBody);
|
|
1928
|
-
|
|
1996
|
+
const observabilityDetails = errorObservabilityDetails(error);
|
|
1997
|
+
logProviderError(logger, provider, "auth", "disconnect", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry, observabilityDetails);
|
|
1929
1998
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1930
1999
|
if (telemetryHeader)
|
|
1931
2000
|
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1932
|
-
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status),
|
|
2001
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, observabilityDetails), status), observabilityDetails);
|
|
1933
2002
|
}
|
|
1934
2003
|
});
|
|
1935
2004
|
return app;
|
package/dist/types.d.ts
CHANGED
|
@@ -640,13 +640,22 @@ export interface HealthCheckCaseResult {
|
|
|
640
640
|
* so authors get IntelliSense and compile-time errors when accessing fields
|
|
641
641
|
* that do not exist on the operation's declared output schema.
|
|
642
642
|
*/
|
|
643
|
-
export
|
|
643
|
+
export type HealthCheckCase<TInput = unknown, TOutput = unknown> = {
|
|
644
644
|
/** Human-readable case name; unique within the suite. */
|
|
645
645
|
name: string;
|
|
646
646
|
/** Optional longer description shown on ops dashboards. */
|
|
647
647
|
description?: string;
|
|
648
648
|
/** Input passed to the operation handler for this case. */
|
|
649
649
|
input: TInput;
|
|
650
|
+
/** Override per-case degradation threshold (ms); falls back to the suite default. */
|
|
651
|
+
degradedThresholdMs?: number;
|
|
652
|
+
/** Override per-case timeout in milliseconds; falls back to the suite/provider/runtime default. */
|
|
653
|
+
timeoutMs?: number;
|
|
654
|
+
/** Expected outcome for "negative" cases (e.g., expecting a degraded baseline). Default: `"ok"`. */
|
|
655
|
+
expectedStatus?: "ok" | "degraded";
|
|
656
|
+
/** Runtime gate (env-driven); if returns false the case is skipped & logged. */
|
|
657
|
+
enabled?: () => boolean;
|
|
658
|
+
} & ({
|
|
650
659
|
/**
|
|
651
660
|
* Optional runtime input preparation hook for volatile probes. Use this when
|
|
652
661
|
* the durable probe input must be derived from a live read-only operation
|
|
@@ -664,15 +673,14 @@ export interface HealthCheckCase<TInput = unknown, TOutput = unknown> {
|
|
|
664
673
|
* + lambda only.
|
|
665
674
|
*/
|
|
666
675
|
assertions: (ctx: HealthCheckAssertionContext<TOutput>) => void | Promise<void> | HealthCheckCaseResult | Promise<HealthCheckCaseResult>;
|
|
667
|
-
/**
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
}
|
|
676
|
+
/** Declarative scenarios replace the imperative preparation and assertion hooks. */
|
|
677
|
+
scenario?: never;
|
|
678
|
+
} | {
|
|
679
|
+
/** Declarative scenario executed by the health-monitor runtime. */
|
|
680
|
+
scenario: HealthScenario;
|
|
681
|
+
prepareInput?: never;
|
|
682
|
+
assertions?: never;
|
|
683
|
+
});
|
|
676
684
|
/**
|
|
677
685
|
* Operation-level health-check suite. At least one case is required when
|
|
678
686
|
* present. All cases share the suite's interval and default timeout.
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.2.0-beta.
|
|
2
|
+
"version": "2.2.0-beta.41",
|
|
3
3
|
"name": "@apifuse/provider-sdk",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
@@ -128,8 +128,7 @@
|
|
|
128
128
|
"@microsoft/api-extractor": "^7.58.13",
|
|
129
129
|
"@types/bun": "latest",
|
|
130
130
|
"@types/node": "^25.9.3",
|
|
131
|
-
"ajv": "^8.17"
|
|
132
|
-
"typescript": "6.0.3"
|
|
131
|
+
"ajv": "^8.17"
|
|
133
132
|
},
|
|
134
133
|
"dependencies": {
|
|
135
134
|
"@clack/prompts": "^1.5.1",
|
|
@@ -146,6 +145,7 @@
|
|
|
146
145
|
"safe-regex": "^2.1",
|
|
147
146
|
"socks": "^2.8.9",
|
|
148
147
|
"tough-cookie": "^6.0.2",
|
|
148
|
+
"typescript": "6.0.3",
|
|
149
149
|
"wreq-js": "3.0.0",
|
|
150
150
|
"zod": "^4.4.3"
|
|
151
151
|
},
|
package/src/cli/commands.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type ApifuseCommandName =
|
|
|
3
3
|
| "dev"
|
|
4
4
|
| "check"
|
|
5
5
|
| "sync-assets"
|
|
6
|
+
| "migrate-shape"
|
|
6
7
|
| "submit-check"
|
|
7
8
|
| "bounty-check"
|
|
8
9
|
| "record"
|
|
@@ -55,6 +56,14 @@ export const COMMAND_MANIFEST: Record<
|
|
|
55
56
|
examples: ["apifuse sync-assets .", "apifuse sync-assets . --check"],
|
|
56
57
|
modulePath: "./apifuse-sync-assets",
|
|
57
58
|
},
|
|
59
|
+
"migrate-shape": {
|
|
60
|
+
name: "migrate-shape",
|
|
61
|
+
summary:
|
|
62
|
+
"Migrate a provider index.ts from the single-phase defineProvider shape to the two-phase declaration builder.",
|
|
63
|
+
usage: "apifuse migrate-shape [path] [--check] [--json]",
|
|
64
|
+
examples: ["apifuse migrate-shape .", "apifuse migrate-shape . --check"],
|
|
65
|
+
modulePath: "./apifuse-migrate-shape",
|
|
66
|
+
},
|
|
58
67
|
"submit-check": {
|
|
59
68
|
name: "submit-check",
|
|
60
69
|
summary:
|
|
@@ -113,6 +122,7 @@ export const COMMAND_ORDER: ApifuseCommandName[] = [
|
|
|
113
122
|
"dev",
|
|
114
123
|
"check",
|
|
115
124
|
"sync-assets",
|
|
125
|
+
"migrate-shape",
|
|
116
126
|
"submit-check",
|
|
117
127
|
"record",
|
|
118
128
|
"test",
|
package/src/cli/create.ts
CHANGED
|
@@ -519,6 +519,12 @@ export async function buildProviderCreatePlan(
|
|
|
519
519
|
sdkSpecifier,
|
|
520
520
|
}),
|
|
521
521
|
},
|
|
522
|
+
{
|
|
523
|
+
path: resolve(providerRoot, "provider.json"),
|
|
524
|
+
content: await renderTemplate("provider.json.tpl", {
|
|
525
|
+
PROVIDER_ID: options.name,
|
|
526
|
+
}),
|
|
527
|
+
},
|
|
522
528
|
{
|
|
523
529
|
path: resolve(providerRoot, "Dockerfile"),
|
|
524
530
|
content: await renderTemplate("Dockerfile.tpl", {}),
|