@apifuse/provider-sdk 2.2.0-beta.12 → 2.2.0-beta.14

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 (65) hide show
  1. package/AUTHORING.md +271 -6
  2. package/CHANGELOG.md +16 -0
  3. package/README.md +26 -2
  4. package/bin/apifuse-pack-types.ts +30 -1
  5. package/bin/apifuse-record.ts +622 -57
  6. package/bin/apifuse-submit-check.ts +43 -10
  7. package/dist/define.d.ts +2 -1
  8. package/dist/define.js +61 -3
  9. package/dist/errors.d.ts +1 -0
  10. package/dist/errors.js +6 -0
  11. package/dist/fixture-sanitization.d.ts +26 -0
  12. package/dist/fixture-sanitization.js +216 -0
  13. package/dist/index.d.ts +3 -2
  14. package/dist/index.js +2 -1
  15. package/dist/provider.d.ts +2 -1
  16. package/dist/provider.js +1 -0
  17. package/dist/runtime/executor.js +17 -2
  18. package/dist/runtime/http.js +86 -32
  19. package/dist/runtime/instrumentation.js +295 -9
  20. package/dist/runtime/native-network.d.ts +53 -0
  21. package/dist/runtime/native-network.js +477 -0
  22. package/dist/runtime/proxy-nodemaven.d.ts +14 -0
  23. package/dist/runtime/proxy-nodemaven.js +20 -2
  24. package/dist/runtime/request-options.d.ts +68 -1
  25. package/dist/runtime/request-options.js +548 -0
  26. package/dist/runtime/stealth.d.ts +3 -1
  27. package/dist/runtime/stealth.js +239 -39
  28. package/dist/server/index.d.ts +2 -2
  29. package/dist/server/index.js +2 -2
  30. package/dist/server/self-test-input-tokens.d.ts +2 -1
  31. package/dist/server/self-test-input-tokens.js +18 -14
  32. package/dist/server/serve.d.ts +9 -0
  33. package/dist/server/serve.js +153 -51
  34. package/dist/server/types.d.ts +3 -0
  35. package/dist/server/types.js +1 -0
  36. package/dist/stateful/stateful-provider-owner-forwarder.js +9 -1
  37. package/dist/stream-evidence.d.ts +74 -0
  38. package/dist/stream-evidence.js +785 -0
  39. package/dist/testing/index.d.ts +1 -1
  40. package/dist/testing/index.js +1 -1
  41. package/dist/testing/run.d.ts +32 -2
  42. package/dist/testing/run.js +451 -19
  43. package/dist/types.d.ts +162 -0
  44. package/package.json +2 -1
  45. package/src/define.ts +81 -3
  46. package/src/errors.ts +9 -0
  47. package/src/fixture-sanitization.ts +247 -0
  48. package/src/index.ts +43 -1
  49. package/src/provider.ts +37 -0
  50. package/src/runtime/executor.ts +22 -2
  51. package/src/runtime/http.ts +144 -38
  52. package/src/runtime/instrumentation.ts +424 -8
  53. package/src/runtime/native-network.ts +600 -0
  54. package/src/runtime/proxy-nodemaven.ts +37 -2
  55. package/src/runtime/request-options.ts +680 -1
  56. package/src/runtime/stealth.ts +293 -40
  57. package/src/server/index.ts +6 -1
  58. package/src/server/self-test-input-tokens.ts +29 -14
  59. package/src/server/serve.ts +190 -68
  60. package/src/server/types.ts +1 -0
  61. package/src/stateful/stateful-provider-owner-forwarder.ts +9 -1
  62. package/src/stream-evidence.ts +988 -0
  63. package/src/testing/index.ts +9 -1
  64. package/src/testing/run.ts +608 -12
  65. package/src/types.ts +194 -0
@@ -9,6 +9,7 @@ import {
9
9
  isProviderError,
10
10
  isSessionExpiredError,
11
11
  isTransportError,
12
+ isValidationError,
12
13
  ProviderError,
13
14
  } from "../errors.js";
14
15
  import {
@@ -108,6 +109,14 @@ import {
108
109
 
109
110
  const DEFAULT_HOST = "0.0.0.0";
110
111
  const DEFAULT_PORT = 3000;
112
+ /** Compact SDK-owned error classification emitted separately from the public response body. */
113
+ export const ERROR_OBSERVABILITY_HEADER = "X-ApiFuse-Error-Observability";
114
+ export type ErrorObservabilityDetails = {
115
+ category: ProviderErrorCategory;
116
+ taxonomyVersion: string;
117
+ retryable: boolean;
118
+ upstreamStatus?: number;
119
+ };
111
120
  const AUTH_FLOW_LOCALES = ["en", "ko", "ja"] as const;
112
121
  const retryResponseMeta = new WeakMap<ProviderContext, HttpRetrySummary>();
113
122
  const STATEFUL_INTERNAL_OPERATIONS_ROUTE = "/__apifuse/stateful/operations";
@@ -486,6 +495,7 @@ export type ProviderServerLogEvent =
486
495
  errorCategory?: ProviderErrorCategory;
487
496
  taxonomyVersion?: string;
488
497
  retryable?: boolean;
498
+ signal?: "unregistered_provider_error_code";
489
499
  issues?: Array<{ path: string; code: string; message: string }>;
490
500
  })
491
501
  | {
@@ -604,26 +614,28 @@ function zodDetails(error: z.ZodError): Array<{
604
614
  }
605
615
 
606
616
  function toErrorResponse(error: unknown, requestId?: string): OperationErrorResponse {
617
+ const observability = errorObservabilityDetails(error);
607
618
  if (error instanceof StatefulRoutingDeadlineError) {
608
619
  return {
609
620
  error: {
610
621
  code: "STATEFUL_FORWARDING_DEADLINE_EXPIRED",
611
622
  message: "Stateful forwarding deadline expired.",
612
623
  ...(requestId ? { requestId } : {}),
613
- details: { retryable: false },
624
+ retryable: observability.retryable,
614
625
  },
615
626
  };
616
627
  }
617
628
 
618
629
  if (isProviderError(error)) {
619
- const details = publicProviderErrorDetails(error);
630
+ const details = error.details;
620
631
  return {
621
632
  error: {
622
633
  code: error.code ?? "provider_error",
623
634
  message: publicProviderErrorMessage(error),
624
635
  ...(requestId ? { requestId } : {}),
636
+ retryable: observability.retryable,
625
637
  ...(error.fix ? { fix: error.fix } : {}),
626
- ...(details ? { details } : {}),
638
+ ...(details !== undefined ? { details } : {}),
627
639
  },
628
640
  };
629
641
  }
@@ -634,6 +646,7 @@ function toErrorResponse(error: unknown, requestId?: string): OperationErrorResp
634
646
  code: "invalid_request",
635
647
  message: "Invalid request body",
636
648
  ...(requestId ? { requestId } : {}),
649
+ retryable: observability.retryable,
637
650
  details: zodDetails(error),
638
651
  },
639
652
  };
@@ -650,6 +663,7 @@ function toErrorResponse(error: unknown, requestId?: string): OperationErrorResp
650
663
  code: "internal_error",
651
664
  message: "Internal error",
652
665
  ...(requestId ? { requestId } : {}),
666
+ retryable: observability.retryable,
653
667
  details: {
654
668
  retryable: false,
655
669
  category: "internal_error",
@@ -659,42 +673,12 @@ function toErrorResponse(error: unknown, requestId?: string): OperationErrorResp
659
673
  };
660
674
  }
661
675
 
662
- function publicProviderErrorDetails(error: ProviderError): unknown {
663
- const providerDetails = error.details;
664
- const observabilityDetails = providerObservabilityDetails(error);
665
-
666
- if (providerDetails === undefined) {
667
- return observabilityDetails;
668
- }
669
- if (observabilityDetails === undefined) {
670
- return providerDetails;
671
- }
672
- if (isPlainRecord(providerDetails) && isPlainRecord(observabilityDetails)) {
673
- return { ...providerDetails, ...observabilityDetails };
674
- }
675
- return {
676
- provider: providerDetails,
677
- observability: observabilityDetails,
678
- };
679
- }
680
-
681
- function isPlainRecord(value: unknown): value is Record<string, unknown> {
682
- return value !== null && typeof value === "object" && !Array.isArray(value);
683
- }
684
-
685
676
  // Accepts `unknown` so the branded guards narrow cleanly from the top: the
686
677
  // subtype error classes are structurally compatible with ProviderError, so
687
678
  // narrowing from a ProviderError-typed value would collapse the negative branch
688
679
  // to `never`. Narrowing from unknown avoids that while still recognizing errors
689
680
  // from a duplicate SDK module instance.
690
- function providerObservabilityDetails(error: unknown):
691
- | {
692
- category: ProviderErrorCategory;
693
- taxonomyVersion: string;
694
- retryable: boolean;
695
- upstreamStatus?: number;
696
- }
697
- | undefined {
681
+ function providerObservabilityDetails(error: unknown): ErrorObservabilityDetails | undefined {
698
682
  // Session-expiry surfaces the credential_expired category + the opt-in
699
683
  // retryable signal so Gateway/Credential Service can refresh and re-drive the
700
684
  // operation (see design.md §4.3 D3). Without this branch the auth error would
@@ -751,6 +735,54 @@ function providerObservabilityDetails(error: unknown):
751
735
  };
752
736
  }
753
737
 
738
+ function errorObservabilityDetails(error: unknown): ErrorObservabilityDetails {
739
+ const providerDetails = providerObservabilityDetails(error);
740
+ if (providerDetails) return providerDetails;
741
+
742
+ if (error instanceof z.ZodError || isValidationError(error)) {
743
+ return {
744
+ category:
745
+ isProviderError(error) && error.options?.category
746
+ ? error.options.category
747
+ : "input_validation",
748
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
749
+ retryable: isProviderError(error) ? (error.options?.retryable ?? false) : false,
750
+ };
751
+ }
752
+
753
+ if (error instanceof StatefulRoutingDeadlineError) {
754
+ return {
755
+ category: "timeout",
756
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
757
+ retryable: false,
758
+ };
759
+ }
760
+
761
+ if (isProviderError(error)) {
762
+ return {
763
+ category: error.options?.category ?? "provider_error",
764
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
765
+ retryable: error.options?.retryable ?? false,
766
+ };
767
+ }
768
+
769
+ return {
770
+ category: "internal_error",
771
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
772
+ retryable: false,
773
+ };
774
+ }
775
+
776
+ function responseWithErrorObservability(response: Response, error: unknown): Response {
777
+ const headers = new Headers(response.headers);
778
+ headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(errorObservabilityDetails(error)));
779
+ return new Response(response.body, {
780
+ status: response.status,
781
+ statusText: response.statusText,
782
+ headers,
783
+ });
784
+ }
785
+
754
786
  function publicProviderErrorMessage(error: ProviderError): string {
755
787
  if (isTransportError(error)) {
756
788
  if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
@@ -782,11 +814,6 @@ function toStatusCode(error: unknown): 400 | 401 | 404 | 429 | 500 | 502 | 503 |
782
814
  if (error instanceof StatefulRoutingDeadlineError) {
783
815
  return 504;
784
816
  }
785
-
786
- if (isTransportError(error)) {
787
- return error.code === "transport_timeout" ? 504 : 502;
788
- }
789
-
790
817
  if (isProviderError(error)) {
791
818
  switch (error.code) {
792
819
  case "AUTH_REQUIRED":
@@ -812,13 +839,91 @@ function toStatusCode(error: unknown): 400 | 401 | 404 | 429 | 500 | 502 | 503 |
812
839
  case "STATEFUL_FORWARDING_REPLAY_CACHE_FULL":
813
840
  return 503;
814
841
  }
842
+ if (isTransportError(error)) {
843
+ return error.code === "transport_timeout" ? 504 : 502;
844
+ }
845
+ if (isValidationError(error)) {
846
+ return error.options?.category === "output_validation" ? 500 : 400;
847
+ }
815
848
 
816
- return 400;
849
+ return 500;
817
850
  }
818
851
 
819
852
  return 500;
820
853
  }
821
854
 
855
+ // Codes emitted by SDK-owned paths must never be attributed to provider
856
+ // authors by the unregistered-code signal, even when their intentional status
857
+ // is 500. Provider-authored codes not in this registry retain the signal.
858
+ const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
859
+ "AUTH_PROMPT_UNAVAILABLE",
860
+ "BROWSER_CDP_POOL_REQUIRED",
861
+ "BROWSER_RUNTIME_UNSUPPORTED",
862
+ "STEALTH_RUNTIME_UNSUPPORTED",
863
+ "SSE_EVENT_UNDECLARED",
864
+ "STREAM_EVENT_TOO_LARGE",
865
+ "STREAM_CHUNK_TOO_LARGE",
866
+ "SSE_RESULT_UNSUPPORTED",
867
+ "STREAM_RESULT_UNSUPPORTED",
868
+ "AUTH_FLOW_NOT_CONFIGURED",
869
+ "refresh_not_supported",
870
+ "RUNTIME_UNSUPPORTED",
871
+ "PROVIDER_STATE_UNSUPPORTED",
872
+ "CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
873
+ "CHOICE_STATE_PAYLOAD_TOO_LARGE",
874
+ "CHOICE_STATE_UNAVAILABLE",
875
+ "CHOICE_CONTEXT_REQUIRED",
876
+ "unsupported_stealth_cookie_store_version",
877
+ "provider_secret_error",
878
+ "credential_key_error",
879
+ "credential_mode_error",
880
+ "flow_expired",
881
+ "turn_validation_error",
882
+ "context_access_error",
883
+ "UNSUPPORTED_STT_OPTION",
884
+ "INVALID_STT_AUDIO",
885
+ "STT_AUDIO_TOO_LARGE",
886
+ "STT_UPSTREAM_FAILED",
887
+ "INVALID_STT_VERIFICATION_CODE_OPTIONS",
888
+ "NO_CODE_FOUND",
889
+ "AMBIGUOUS_CODE",
890
+ "retry_invalid_policy",
891
+ "retry_unsafe_method",
892
+ "stealth_cookie_store_serialize_failed",
893
+ "response_too_large",
894
+ "transport_stream_unavailable",
895
+ "transport_invalid_method",
896
+ "http_transport_override_unsupported",
897
+ "transport_invalid_url",
898
+ "retry_exhausted",
899
+ "auth_abort_unsafe_data",
900
+ "credentials_auth_missing_credential_keys",
901
+ "credentials_auth_missing_credential",
902
+ "credentials_auth_invalid_login_result",
903
+ "credentials_auth_unknown_challenge",
904
+ "credentials_auth_unknown_pending_challenge",
905
+ "STATEFUL_FORWARDING_NOT_CONFIGURED",
906
+ "STATEFUL_FORWARDING_SIGNATURE_MISSING",
907
+ "STATEFUL_FORWARDING_NONCE_INVALID",
908
+ "STATEFUL_FORWARDING_TIMESTAMP_INVALID",
909
+ "STATEFUL_FORWARDING_SIGNATURE_INVALID",
910
+ "STATEFUL_FORWARDING_REPLAY_DETECTED",
911
+ "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
912
+ "STATEFUL_FORWARDING_ENVELOPE_INVALID",
913
+ "STATEFUL_FORWARDING_PROVIDER_MISMATCH",
914
+ "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
915
+ "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
916
+ "STATEFUL_FORWARDING_REQUEST_FAILED",
917
+ "STATEFUL_FORWARDING_CONTEXT_MISSING",
918
+ "STATEFUL_FORWARDING_BAD_RESPONSE",
919
+ "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
920
+ "STATEFUL_FILE_FORWARDING_UNSUPPORTED",
921
+ "STATEFUL_CONTROL_PLANE_OPERATION_AMBIGUOUS",
922
+ "STATEFUL_CONTROL_PLANE_REQUEST_FAILED",
923
+ "STATEFUL_CONTROL_PLANE_HTTP_ERROR",
924
+ "STATEFUL_CONTROL_PLANE_INVALID_RESPONSE",
925
+ ]);
926
+
822
927
  function extractRequestId(raw: unknown): string | undefined {
823
928
  if (!raw || typeof raw !== "object") {
824
929
  return undefined;
@@ -847,7 +952,13 @@ function logProviderError(
847
952
  : "internal_error";
848
953
  const errorClass = error instanceof Error ? error.name : typeof error;
849
954
  const message = error instanceof Error ? error.message : String(error);
850
- const details = isProviderError(error) ? providerObservabilityDetails(error) : undefined;
955
+ const details = errorObservabilityDetails(error);
956
+ const isUnregisteredProviderErrorCode =
957
+ status === 500 &&
958
+ isProviderError(error) &&
959
+ !isValidationError(error) &&
960
+ typeof error.code === "string" &&
961
+ !SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code);
851
962
  const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
852
963
  emit({
853
964
  level: status >= 500 ? "error" : "warn",
@@ -861,15 +972,12 @@ function logProviderError(
861
972
  code,
862
973
  errorClass,
863
974
  message,
864
- ...(isTransportError(error) && error.upstreamStatus
865
- ? { upstreamStatus: error.upstreamStatus }
866
- : {}),
867
- ...(details
868
- ? {
869
- errorCategory: details.category,
870
- taxonomyVersion: details.taxonomyVersion,
871
- retryable: details.retryable,
872
- }
975
+ ...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
976
+ errorCategory: details.category,
977
+ taxonomyVersion: details.taxonomyVersion,
978
+ retryable: details.retryable,
979
+ ...(isUnregisteredProviderErrorCode
980
+ ? { signal: "unregistered_provider_error_code" as const }
873
981
  : {}),
874
982
  ...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
875
983
  });
@@ -1614,17 +1722,10 @@ export function createServerApp(
1614
1722
  });
1615
1723
  }
1616
1724
 
1617
- app.notFound((c) =>
1618
- c.json(
1619
- {
1620
- error: {
1621
- code: "not_found",
1622
- message: "Not found",
1623
- },
1624
- },
1625
- 404,
1626
- ),
1627
- );
1725
+ app.notFound((c) => {
1726
+ const error = new ProviderError("Not found", { code: "not_found", retryable: false });
1727
+ return responseWithErrorObservability(c.json(toErrorResponse(error), 404), error);
1728
+ });
1628
1729
 
1629
1730
  app.get("/health", (c) =>
1630
1731
  c.json({
@@ -1786,7 +1887,10 @@ export function createServerApp(
1786
1887
  status,
1787
1888
  finishRequestCost(requestCost),
1788
1889
  );
1789
- return c.json(toErrorResponse(error, requestId), status);
1890
+ return responseWithErrorObservability(
1891
+ c.json(toErrorResponse(error, requestId), status),
1892
+ error,
1893
+ );
1790
1894
  }
1791
1895
  });
1792
1896
 
@@ -1850,7 +1954,10 @@ export function createServerApp(
1850
1954
  );
1851
1955
  const telemetryHeader = proxyTelemetry.toHeaderValue();
1852
1956
  if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
1853
- return c.json(toErrorResponse(error, requestId), status);
1957
+ return responseWithErrorObservability(
1958
+ c.json(toErrorResponse(error, requestId), status),
1959
+ error,
1960
+ );
1854
1961
  }
1855
1962
  });
1856
1963
 
@@ -1887,7 +1994,10 @@ export function createServerApp(
1887
1994
  status,
1888
1995
  finishRequestCost(requestCost),
1889
1996
  );
1890
- return c.json(toErrorResponse(error, requestId), status);
1997
+ return responseWithErrorObservability(
1998
+ c.json(toErrorResponse(error, requestId), status),
1999
+ error,
2000
+ );
1891
2001
  }
1892
2002
  });
1893
2003
 
@@ -1924,7 +2034,10 @@ export function createServerApp(
1924
2034
  status,
1925
2035
  finishRequestCost(requestCost),
1926
2036
  );
1927
- return c.json(toErrorResponse(error, requestId), status);
2037
+ return responseWithErrorObservability(
2038
+ c.json(toErrorResponse(error, requestId), status),
2039
+ error,
2040
+ );
1928
2041
  }
1929
2042
  });
1930
2043
 
@@ -1961,7 +2074,10 @@ export function createServerApp(
1961
2074
  status,
1962
2075
  finishRequestCost(requestCost),
1963
2076
  );
1964
- return c.json(toErrorResponse(error, requestId), status);
2077
+ return responseWithErrorObservability(
2078
+ c.json(toErrorResponse(error, requestId), status),
2079
+ error,
2080
+ );
1965
2081
  }
1966
2082
  });
1967
2083
 
@@ -1998,7 +2114,10 @@ export function createServerApp(
1998
2114
  status,
1999
2115
  finishRequestCost(requestCost),
2000
2116
  );
2001
- return c.json(toErrorResponse(error, requestId), status);
2117
+ return responseWithErrorObservability(
2118
+ c.json(toErrorResponse(error, requestId), status),
2119
+ error,
2120
+ );
2002
2121
  }
2003
2122
  });
2004
2123
 
@@ -2035,7 +2154,10 @@ export function createServerApp(
2035
2154
  status,
2036
2155
  finishRequestCost(requestCost),
2037
2156
  );
2038
- return c.json(toErrorResponse(error, requestId), status);
2157
+ return responseWithErrorObservability(
2158
+ c.json(toErrorResponse(error, requestId), status),
2159
+ error,
2160
+ );
2039
2161
  }
2040
2162
  });
2041
2163
 
@@ -26,6 +26,7 @@ export const ErrorEnvelopeSchema = z.object({
26
26
  code: z.string(),
27
27
  message: z.string(),
28
28
  requestId: z.string().optional(),
29
+ retryable: z.boolean(),
29
30
  fix: z.string().optional(),
30
31
  details: z.unknown().optional(),
31
32
  });
@@ -35,6 +35,14 @@ type FetchTransport = (url: string | URL | Request, init?: RequestInit) => Promi
35
35
 
36
36
  const MAX_FORWARDED_HEADERS = 32;
37
37
  const MAX_FORWARDED_HEADER_BYTES = 8 * 1024;
38
+ // Inbound compatibility boundary for rolling deploys: older owner pods omit
39
+ // top-level retryable. Emitted responses remain strict via
40
+ // OperationErrorResponseSchema.
41
+ const ForwardedOperationErrorResponseSchema = OperationErrorResponseSchema.extend({
42
+ error: OperationErrorResponseSchema.shape.error.extend({
43
+ retryable: z.boolean().optional().default(false),
44
+ }),
45
+ });
38
46
  const SENSITIVE_HEADER_NAMES = new Set([
39
47
  "authorization",
40
48
  "cookie",
@@ -253,7 +261,7 @@ async function parseForwardedResponse(response: Response): Promise<StatefulOpera
253
261
  return { output: success.data.data };
254
262
  }
255
263
 
256
- const error = OperationErrorResponseSchema.safeParse(body);
264
+ const error = ForwardedOperationErrorResponseSchema.safeParse(body);
257
265
  if (error.success) {
258
266
  throw new StatefulOwnerForwardingError({
259
267
  code: error.data.error.code,