@apifuse/provider-sdk 2.2.0-beta.40 → 2.2.0-beta.42
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 +8 -0
- package/bin/apifuse-check.ts +61 -0
- package/bin/apifuse-migrate-shape.ts +202 -0
- package/bin/apifuse-submit-check.ts +1773 -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-operation-shape.d.ts +44 -0
- package/dist/cli/migrate-operation-shape.js +113 -0
- package/dist/cli/migrate-provider-shape.d.ts +52 -0
- package/dist/cli/migrate-provider-shape.js +578 -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 +174 -66
- package/dist/types.d.ts +18 -10
- package/package.json +1 -1
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +6 -0
- package/src/cli/migrate-operation-shape.ts +184 -0
- package/src/cli/migrate-provider-shape.ts +772 -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 +214 -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 { REDACTED_FIXTURE_VALUE, 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,52 @@ 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
|
+
const UNSTRUCTURED_PROVIDER_ERROR_CAUSE_MESSAGE = "[UNSTRUCTURED_UPSTREAM_TEXT]";
|
|
983
|
+
const PROVIDER_ERROR_CAUSE_RETAINED_URL_RUN = /https?:\/\/[^\s"'<>]+/giu;
|
|
984
|
+
const PROVIDER_ERROR_CAUSE_TOKEN_RUN = /\S+/gu;
|
|
985
|
+
const PROVIDER_ERROR_CAUSE_TOKEN_EDGE_PUNCTUATION = /^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu;
|
|
986
|
+
const STRUCTURALLY_SAFE_PROVIDER_ERROR_CAUSE_WORDS = new Set([
|
|
987
|
+
"completion",
|
|
988
|
+
"diagnostic",
|
|
989
|
+
"provider",
|
|
990
|
+
"rejected",
|
|
991
|
+
"returned",
|
|
992
|
+
"upstream",
|
|
993
|
+
]);
|
|
994
|
+
/**
|
|
995
|
+
* Cause frames fail closed when sanitization leaves a plausible credential-shaped free-text run.
|
|
996
|
+
* Redaction sentinels and retained URLs are ignored. Every other whitespace token (or each side
|
|
997
|
+
* of a structured key=value token) with at least eight Unicode characters must reduce to this
|
|
998
|
+
* small vocabulary drawn from SDK diagnostics; counting punctuation keeps bare passwords opaque.
|
|
999
|
+
*/
|
|
1000
|
+
function isStructurallySafeProviderErrorCauseMessage(message) {
|
|
1001
|
+
const classifiableMessage = message
|
|
1002
|
+
.replaceAll(REDACTED_FIXTURE_VALUE, " ")
|
|
1003
|
+
.replace(PROVIDER_ERROR_CAUSE_RETAINED_URL_RUN, " ");
|
|
1004
|
+
for (const match of classifiableMessage.matchAll(PROVIDER_ERROR_CAUSE_TOKEN_RUN)) {
|
|
1005
|
+
const token = match[0];
|
|
1006
|
+
for (const run of token.split("=")) {
|
|
1007
|
+
const diagnosticWord = run
|
|
1008
|
+
.replace(PROVIDER_ERROR_CAUSE_TOKEN_EDGE_PUNCTUATION, "")
|
|
1009
|
+
.toLowerCase();
|
|
1010
|
+
if (STRUCTURALLY_SAFE_PROVIDER_ERROR_CAUSE_WORDS.has(diagnosticWord))
|
|
1011
|
+
continue;
|
|
1012
|
+
if ([...run].length >= 8)
|
|
1013
|
+
return false;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return true;
|
|
1017
|
+
}
|
|
1018
|
+
function providerErrorCauseMessage(message) {
|
|
1019
|
+
const sanitized = sanitizeDiagnosticText(message);
|
|
1020
|
+
if (!isStructurallySafeProviderErrorCauseMessage(sanitized)) {
|
|
1021
|
+
return UNSTRUCTURED_PROVIDER_ERROR_CAUSE_MESSAGE;
|
|
1022
|
+
}
|
|
1023
|
+
return sanitized.length > MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH
|
|
1024
|
+
? `${sanitized.slice(0, MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH)}… [truncated]`
|
|
1025
|
+
: sanitized;
|
|
1026
|
+
}
|
|
940
1027
|
function providerErrorCauseChain(error) {
|
|
941
1028
|
if (!(error instanceof Error) && !isProviderError(error))
|
|
942
1029
|
return undefined;
|
|
@@ -948,19 +1035,24 @@ function providerErrorCauseChain(error) {
|
|
|
948
1035
|
!seen.has(cause)) {
|
|
949
1036
|
seen.add(cause);
|
|
950
1037
|
const message = cause.message;
|
|
1038
|
+
const providerObservability = safeProviderErrorObservability(cause);
|
|
1039
|
+
const causeCode = providerErrorCode(cause);
|
|
951
1040
|
frames.push({
|
|
952
1041
|
errorClass: cause.name,
|
|
953
|
-
...(
|
|
1042
|
+
...(causeCode !== undefined ? { code: causeCode } : {}),
|
|
1043
|
+
message: providerErrorCauseMessage(message),
|
|
954
1044
|
messageLength: message.length,
|
|
955
1045
|
messageFingerprint: createHash("sha256").update(message).digest("hex").slice(0, 12),
|
|
1046
|
+
...(providerObservability ? { providerObservability } : {}),
|
|
956
1047
|
});
|
|
957
1048
|
cause = cause.cause;
|
|
958
1049
|
}
|
|
959
1050
|
return frames.length > 0 ? frames : undefined;
|
|
960
1051
|
}
|
|
961
|
-
function logProviderError(logger, provider, kind, route, requestId, error, status, cost, declaredErrorCode, proxyTelemetry) {
|
|
1052
|
+
function logProviderError(logger, provider, kind, route, requestId, error, status, cost, declaredErrorCode, proxyTelemetry, observabilityDetails) {
|
|
1053
|
+
const providerCode = isProviderError(error) ? providerErrorCode(error) : undefined;
|
|
962
1054
|
const code = isProviderError(error)
|
|
963
|
-
? (
|
|
1055
|
+
? (providerCode ?? "provider_error")
|
|
964
1056
|
: error instanceof z.ZodError
|
|
965
1057
|
? "invalid_request"
|
|
966
1058
|
: error instanceof StatefulRoutingDeadlineError
|
|
@@ -969,15 +1061,21 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
|
|
|
969
1061
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
970
1062
|
const message = error instanceof Error ? error.message : String(error);
|
|
971
1063
|
const causeChain = providerErrorCauseChain(error);
|
|
972
|
-
const details =
|
|
1064
|
+
const details = observabilityDetails;
|
|
973
1065
|
const isUnregisteredProviderErrorCode = status === 500 &&
|
|
974
1066
|
isProviderError(error) &&
|
|
975
1067
|
!isValidationError(error) &&
|
|
976
|
-
typeof
|
|
977
|
-
!SDK_OWNED_PROVIDER_ERROR_CODES.has(
|
|
1068
|
+
typeof providerCode === "string" &&
|
|
1069
|
+
!SDK_OWNED_PROVIDER_ERROR_CODES.has(providerCode) &&
|
|
978
1070
|
declaredErrorCode === undefined;
|
|
979
1071
|
const proxy = proxyTelemetry?.toLogPayload();
|
|
980
1072
|
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
1073
|
+
// The logger is caller-supplied and may mutate the event synchronously.
|
|
1074
|
+
// Give it an independent snapshot so those mutations cannot corrupt the
|
|
1075
|
+
// observability header serialized immediately afterwards.
|
|
1076
|
+
const providerObservability = details.providerObservability
|
|
1077
|
+
? { ...details.providerObservability }
|
|
1078
|
+
: undefined;
|
|
981
1079
|
emit({
|
|
982
1080
|
level: status >= 500 ? "error" : "warn",
|
|
983
1081
|
event: "provider_request_failed",
|
|
@@ -992,6 +1090,7 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
|
|
|
992
1090
|
errorClass,
|
|
993
1091
|
message,
|
|
994
1092
|
...(causeChain ? { causeChain } : {}),
|
|
1093
|
+
...(providerObservability ? { providerObservability } : {}),
|
|
995
1094
|
...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
|
|
996
1095
|
errorCategory: details.category,
|
|
997
1096
|
taxonomyVersion: details.taxonomyVersion,
|
|
@@ -1629,7 +1728,8 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1629
1728
|
}
|
|
1630
1729
|
app.notFound((c) => {
|
|
1631
1730
|
const error = new ProviderError("Not found", { code: "not_found", retryable: false });
|
|
1632
|
-
|
|
1731
|
+
const observabilityDetails = errorObservabilityDetails(error);
|
|
1732
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, undefined, observabilityDetails), 404), observabilityDetails);
|
|
1633
1733
|
});
|
|
1634
1734
|
app.get("/health", (c) => c.json({
|
|
1635
1735
|
status: "ok",
|
|
@@ -1744,12 +1844,14 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1744
1844
|
catch (error) {
|
|
1745
1845
|
const declaredErrorCode = declaredErrorCodeFor(error, operationId, operationErrorCodes);
|
|
1746
1846
|
const status = toStatusCode(error, declaredErrorCode);
|
|
1747
|
-
if (isProviderError(error) &&
|
|
1847
|
+
if (isProviderError(error) &&
|
|
1848
|
+
providerErrorCode(error) === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
|
|
1748
1849
|
c.header("Retry-After", String(STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS));
|
|
1749
1850
|
}
|
|
1750
1851
|
const requestId = extractRequestId(rawBody);
|
|
1751
|
-
|
|
1752
|
-
|
|
1852
|
+
const observabilityDetails = errorObservabilityDetails(error, declaredErrorCode);
|
|
1853
|
+
logProviderError(logger, provider, "operation", operationId || operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode, undefined, observabilityDetails);
|
|
1854
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, observabilityDetails), status), observabilityDetails);
|
|
1753
1855
|
}
|
|
1754
1856
|
});
|
|
1755
1857
|
app.post("/v1/:operation", async (c) => {
|
|
@@ -1780,11 +1882,12 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1780
1882
|
const declaredErrorCode = declaredErrorCodeFor(error, operation, operationErrorCodes);
|
|
1781
1883
|
const status = toStatusCode(error, declaredErrorCode);
|
|
1782
1884
|
const requestId = extractRequestId(rawBody);
|
|
1783
|
-
|
|
1885
|
+
const observabilityDetails = errorObservabilityDetails(error, declaredErrorCode);
|
|
1886
|
+
logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode, proxyTelemetry, observabilityDetails);
|
|
1784
1887
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1785
1888
|
if (telemetryHeader)
|
|
1786
1889
|
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1787
|
-
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId,
|
|
1890
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, observabilityDetails), status), observabilityDetails);
|
|
1788
1891
|
}
|
|
1789
1892
|
});
|
|
1790
1893
|
app.post("/auth/start", async (c) => {
|
|
@@ -1809,11 +1912,12 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1809
1912
|
catch (error) {
|
|
1810
1913
|
const status = toStatusCode(error);
|
|
1811
1914
|
const requestId = extractRequestId(rawBody);
|
|
1812
|
-
|
|
1915
|
+
const observabilityDetails = errorObservabilityDetails(error);
|
|
1916
|
+
logProviderError(logger, provider, "auth", "start", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry, observabilityDetails);
|
|
1813
1917
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1814
1918
|
if (telemetryHeader)
|
|
1815
1919
|
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1816
|
-
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status),
|
|
1920
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, observabilityDetails), status), observabilityDetails);
|
|
1817
1921
|
}
|
|
1818
1922
|
});
|
|
1819
1923
|
app.post("/auth/continue", async (c) => {
|
|
@@ -1838,11 +1942,12 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1838
1942
|
catch (error) {
|
|
1839
1943
|
const status = toStatusCode(error);
|
|
1840
1944
|
const requestId = extractRequestId(rawBody);
|
|
1841
|
-
|
|
1945
|
+
const observabilityDetails = errorObservabilityDetails(error);
|
|
1946
|
+
logProviderError(logger, provider, "auth", "continue", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry, observabilityDetails);
|
|
1842
1947
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1843
1948
|
if (telemetryHeader)
|
|
1844
1949
|
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1845
|
-
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status),
|
|
1950
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, observabilityDetails), status), observabilityDetails);
|
|
1846
1951
|
}
|
|
1847
1952
|
});
|
|
1848
1953
|
app.post("/auth/poll", async (c) => {
|
|
@@ -1867,11 +1972,12 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1867
1972
|
catch (error) {
|
|
1868
1973
|
const status = toStatusCode(error);
|
|
1869
1974
|
const requestId = extractRequestId(rawBody);
|
|
1870
|
-
|
|
1975
|
+
const observabilityDetails = errorObservabilityDetails(error);
|
|
1976
|
+
logProviderError(logger, provider, "auth", "poll", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry, observabilityDetails);
|
|
1871
1977
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1872
1978
|
if (telemetryHeader)
|
|
1873
1979
|
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1874
|
-
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status),
|
|
1980
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, observabilityDetails), status), observabilityDetails);
|
|
1875
1981
|
}
|
|
1876
1982
|
});
|
|
1877
1983
|
app.post("/auth/refresh", async (c) => {
|
|
@@ -1896,11 +2002,12 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1896
2002
|
catch (error) {
|
|
1897
2003
|
const status = toStatusCode(error);
|
|
1898
2004
|
const requestId = extractRequestId(rawBody);
|
|
1899
|
-
|
|
2005
|
+
const observabilityDetails = errorObservabilityDetails(error);
|
|
2006
|
+
logProviderError(logger, provider, "auth", "refresh", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry, observabilityDetails);
|
|
1900
2007
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1901
2008
|
if (telemetryHeader)
|
|
1902
2009
|
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1903
|
-
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status),
|
|
2010
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, observabilityDetails), status), observabilityDetails);
|
|
1904
2011
|
}
|
|
1905
2012
|
});
|
|
1906
2013
|
app.post("/auth/disconnect", async (c) => {
|
|
@@ -1925,11 +2032,12 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1925
2032
|
catch (error) {
|
|
1926
2033
|
const status = toStatusCode(error);
|
|
1927
2034
|
const requestId = extractRequestId(rawBody);
|
|
1928
|
-
|
|
2035
|
+
const observabilityDetails = errorObservabilityDetails(error);
|
|
2036
|
+
logProviderError(logger, provider, "auth", "disconnect", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry, observabilityDetails);
|
|
1929
2037
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1930
2038
|
if (telemetryHeader)
|
|
1931
2039
|
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1932
|
-
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status),
|
|
2040
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, observabilityDetails), status), observabilityDetails);
|
|
1933
2041
|
}
|
|
1934
2042
|
});
|
|
1935
2043
|
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
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 to the phase-separated SDK: two-phase defineProvider in index.ts and curried defineOperation across sources.",
|
|
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", {}),
|