@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bin/apifuse-check.ts +61 -0
  3. package/bin/apifuse-migrate-shape.ts +202 -0
  4. package/bin/apifuse-submit-check.ts +1773 -222
  5. package/dist/cli/commands.d.ts +1 -1
  6. package/dist/cli/commands.js +8 -0
  7. package/dist/cli/create.js +6 -0
  8. package/dist/cli/migrate-operation-shape.d.ts +44 -0
  9. package/dist/cli/migrate-operation-shape.js +113 -0
  10. package/dist/cli/migrate-provider-shape.d.ts +52 -0
  11. package/dist/cli/migrate-provider-shape.js +578 -0
  12. package/dist/cli/templates/provider/provider.json.tpl +6 -0
  13. package/dist/contract.js +1 -0
  14. package/dist/define.js +22 -1
  15. package/dist/error-observability.d.ts +7 -0
  16. package/dist/error-observability.js +61 -0
  17. package/dist/errors.d.ts +15 -0
  18. package/dist/fixture-sanitization.js +13 -3
  19. package/dist/index.d.ts +1 -1
  20. package/dist/provider.d.ts +1 -1
  21. package/dist/runtime/executor.js +11 -1
  22. package/dist/server/error-observability.d.ts +1 -0
  23. package/dist/server/error-observability.js +1 -0
  24. package/dist/server/index.d.ts +2 -1
  25. package/dist/server/self-test.js +3 -0
  26. package/dist/server/serve-implementation.d.ts +12 -0
  27. package/dist/server/serve-implementation.js +174 -66
  28. package/dist/types.d.ts +18 -10
  29. package/package.json +1 -1
  30. package/src/cli/commands.ts +10 -0
  31. package/src/cli/create.ts +6 -0
  32. package/src/cli/migrate-operation-shape.ts +184 -0
  33. package/src/cli/migrate-provider-shape.ts +772 -0
  34. package/src/cli/templates/provider/provider.json.tpl +6 -0
  35. package/src/contract.ts +1 -0
  36. package/src/define.ts +33 -1
  37. package/src/error-observability.ts +64 -0
  38. package/src/errors.ts +16 -0
  39. package/src/fixture-sanitization.ts +19 -3
  40. package/src/index.ts +1 -0
  41. package/src/provider.ts +1 -0
  42. package/src/runtime/executor.ts +13 -1
  43. package/src/server/error-observability.ts +1 -0
  44. package/src/server/index.ts +2 -0
  45. package/src/server/self-test.ts +5 -0
  46. package/src/server/serve-implementation.ts +214 -84
  47. 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
- if (error.code === MISSING_SECRET_CODE)
686
+ const code = providerErrorCode(error);
687
+ if (code === MISSING_SECRET_CODE)
659
688
  return "apifuse";
660
- if (error.code === "UPSTREAM_ERROR" || error.code === "BLOCKED") {
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, declaredErrorCode) {
667
- const observability = errorObservabilityDetails(error, declaredErrorCode);
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.details;
710
+ const details = providerErrorOption(error, "details");
682
711
  return {
683
712
  error: {
684
- code: error.code ?? "provider_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.fix ? { fix: error.fix } : {}),
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.options?.category ?? "credential_expired",
772
+ category: providerErrorOption(error, "category") ?? "credential_expired",
744
773
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
745
- retryable: error.options?.retryable ?? declaredRetryable ?? false,
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.code === MISSING_SECRET_CODE) {
782
+ if (isProviderError(error) && providerErrorCode(error) === MISSING_SECRET_CODE) {
754
783
  return {
755
- category: error.options?.category ?? "credential_unavailable",
784
+ category: providerErrorOption(error, "category") ?? "credential_unavailable",
756
785
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
757
- retryable: error.options?.retryable ?? declaredRetryable ?? false,
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.code === PROXY_POOL_EXHAUSTED_CODE ||
764
- error.code === PROXY_EDGE_AUTH_REJECTED_CODE ||
765
- error.code === "PROXY_ALLOCATION_FAILED";
766
- const category = error.options?.category ??
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.code === PROXY_AUTH_IP_DENIED_CODE
798
+ : providerErrorCode(error) === PROXY_AUTH_IP_DENIED_CODE
770
799
  ? "anti_bot_blocked"
771
- : error.code === "transport_timeout"
800
+ : providerErrorCode(error) === "transport_timeout"
772
801
  ? "timeout"
773
- : error.code === "transport_network_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.options?.retryable ??
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 errorObservabilityDetails(error, declaredErrorCode) {
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: isProviderError(error) && error.options?.category
797
- ? error.options.category
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.options?.retryable ?? effectiveDeclaration?.retryable ?? false)
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.code === "UPSTREAM_REJECTED" ||
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.options?.category ?? rejectionDefault,
859
+ category: providerErrorOption(error, "category") ?? rejectionDefault,
830
860
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
831
- retryable: error.options?.retryable ?? effectiveDeclaration?.retryable ?? false,
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 responseWithErrorObservability(response, error, declaredErrorCode) {
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(errorObservabilityDetails(error, declaredErrorCode)));
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.code === PROXY_AUTH_IP_DENIED_CODE) {
889
+ if (providerErrorCode(error) === PROXY_AUTH_IP_DENIED_CODE) {
852
890
  return error.message;
853
891
  }
854
- if (error.code === PROXY_EDGE_AUTH_REJECTED_CODE) {
892
+ if (providerErrorCode(error) === PROXY_EDGE_AUTH_REJECTED_CODE) {
855
893
  return error.message;
856
894
  }
857
- if (error.code === PROXY_POOL_EXHAUSTED_CODE) {
895
+ if (providerErrorCode(error) === PROXY_POOL_EXHAUSTED_CODE) {
858
896
  return error.message;
859
897
  }
860
- if (error.code === "transport_timeout")
898
+ if (providerErrorCode(error) === "transport_timeout")
861
899
  return "Request timed out";
862
- if (error.code === "transport_network_error")
900
+ if (providerErrorCode(error) === "transport_network_error")
863
901
  return "Network error";
864
- if (error.code === "upstream_http_error" && error.status) {
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
- if (typeof error.code === "string") {
891
- const mappedStatus = SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES.get(error.code);
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.code === "transport_timeout" ? 504 : 502;
936
+ return providerErrorCode(error) === "transport_timeout" ? 504 : 502;
898
937
  }
899
938
  if (isValidationError(error)) {
900
- return error.options?.category === "output_validation" ? 500 : 400;
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
- return (isProviderError(error) &&
916
- typeof error.code === "string" &&
917
- SDK_RUNTIME_OWNED_ERROR_CODES.has(error.code));
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
- if (!operationId || !isProviderError(error) || typeof error.code !== "string")
968
+ const code = providerErrorCode(error);
969
+ if (!operationId || !code)
929
970
  return undefined;
930
- return lookup.get(operationId)?.get(error.code);
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
- ...(isProviderError(cause) && typeof cause.code === "string" ? { code: cause.code } : {}),
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
- ? (error.code ?? "provider_error")
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 = errorObservabilityDetails(error, declaredErrorCode);
1064
+ const details = observabilityDetails;
973
1065
  const isUnregisteredProviderErrorCode = status === 500 &&
974
1066
  isProviderError(error) &&
975
1067
  !isValidationError(error) &&
976
- typeof error.code === "string" &&
977
- !SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code) &&
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
- return responseWithErrorObservability(c.json(toErrorResponse(error), 404), error);
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) && error.code === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
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
- logProviderError(logger, provider, "operation", operationId || operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode);
1752
- return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, declaredErrorCode), status), error, declaredErrorCode);
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
- logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode, proxyTelemetry);
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, declaredErrorCode), status), error, declaredErrorCode);
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
- logProviderError(logger, provider, "auth", "start", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry);
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), error);
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
- logProviderError(logger, provider, "auth", "continue", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry);
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), error);
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
- logProviderError(logger, provider, "auth", "poll", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry);
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), error);
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
- logProviderError(logger, provider, "auth", "refresh", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry);
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), error);
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
- logProviderError(logger, provider, "auth", "disconnect", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry);
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), error);
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 interface HealthCheckCase<TInput = unknown, TOutput = unknown> {
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
- /** Override per-case degradation threshold (ms); falls back to the suite default. */
668
- degradedThresholdMs?: number;
669
- /** Override per-case timeout in milliseconds; falls back to the suite/provider/runtime default. */
670
- timeoutMs?: number;
671
- /** Expected outcome for "negative" cases (e.g., expecting a degraded baseline). Default: `"ok"`. */
672
- expectedStatus?: "ok" | "degraded";
673
- /** Runtime gate (env-driven); if returns false the case is skipped & logged. */
674
- enabled?: () => boolean;
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.40",
2
+ "version": "2.2.0-beta.42",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
@@ -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", {}),