@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/bin/apifuse-check.ts +61 -0
  3. package/bin/apifuse-migrate-shape.ts +84 -0
  4. package/bin/apifuse-submit-check.ts +1760 -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-provider-shape.d.ts +52 -0
  9. package/dist/cli/migrate-provider-shape.js +515 -0
  10. package/dist/cli/templates/provider/provider.json.tpl +6 -0
  11. package/dist/contract.js +1 -0
  12. package/dist/define.js +22 -1
  13. package/dist/error-observability.d.ts +7 -0
  14. package/dist/error-observability.js +61 -0
  15. package/dist/errors.d.ts +15 -0
  16. package/dist/fixture-sanitization.js +13 -3
  17. package/dist/index.d.ts +1 -1
  18. package/dist/provider.d.ts +1 -1
  19. package/dist/runtime/executor.js +11 -1
  20. package/dist/server/error-observability.d.ts +1 -0
  21. package/dist/server/error-observability.js +1 -0
  22. package/dist/server/index.d.ts +2 -1
  23. package/dist/server/self-test.js +3 -0
  24. package/dist/server/serve-implementation.d.ts +12 -0
  25. package/dist/server/serve-implementation.js +135 -66
  26. package/dist/types.d.ts +18 -10
  27. package/package.json +3 -3
  28. package/src/cli/commands.ts +10 -0
  29. package/src/cli/create.ts +6 -0
  30. package/src/cli/migrate-provider-shape.ts +701 -0
  31. package/src/cli/templates/provider/provider.json.tpl +6 -0
  32. package/src/contract.ts +1 -0
  33. package/src/define.ts +33 -1
  34. package/src/error-observability.ts +64 -0
  35. package/src/errors.ts +16 -0
  36. package/src/fixture-sanitization.ts +19 -3
  37. package/src/index.ts +1 -0
  38. package/src/provider.ts +1 -0
  39. package/src/runtime/executor.ts +13 -1
  40. package/src/server/error-observability.ts +1 -0
  41. package/src/server/index.ts +2 -0
  42. package/src/server/self-test.ts +5 -0
  43. package/src/server/serve-implementation.ts +172 -84
  44. 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
- 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,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
- ...(isProviderError(cause) && typeof cause.code === "string" ? { code: cause.code } : {}),
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
- ? (error.code ?? "provider_error")
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 = errorObservabilityDetails(error, declaredErrorCode);
1025
+ const details = observabilityDetails;
973
1026
  const isUnregisteredProviderErrorCode = status === 500 &&
974
1027
  isProviderError(error) &&
975
1028
  !isValidationError(error) &&
976
- typeof error.code === "string" &&
977
- !SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code) &&
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
- return responseWithErrorObservability(c.json(toErrorResponse(error), 404), error);
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) && error.code === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
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
- logProviderError(logger, provider, "operation", operationId || operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode);
1752
- return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, declaredErrorCode), status), error, declaredErrorCode);
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
- logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode, proxyTelemetry);
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, declaredErrorCode), status), error, declaredErrorCode);
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
- logProviderError(logger, provider, "auth", "start", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry);
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), error);
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
- logProviderError(logger, provider, "auth", "continue", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry);
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), error);
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
- logProviderError(logger, provider, "auth", "poll", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry);
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), error);
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
- logProviderError(logger, provider, "auth", "refresh", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry);
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), error);
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
- logProviderError(logger, provider, "auth", "disconnect", requestId, error, status, finishRequestCost(requestCost), undefined, proxyTelemetry);
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), error);
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 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.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
  },
@@ -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", {}),