@apifuse/provider-sdk 2.2.0-beta.27 → 2.2.0-beta.28

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 (81) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/bin/apifuse-dev.ts +2 -2
  3. package/bin/apifuse-pack-smoke.ts +1 -1
  4. package/bin/apifuse-pack-types.ts +2 -1
  5. package/bin/apifuse-perf.ts +2 -4
  6. package/bin/apifuse-record.ts +1 -1
  7. package/dist/auth-turn/index.d.ts +1 -1
  8. package/dist/auth-turn/index.js +1 -1
  9. package/dist/ceremonies/index.js +52 -11
  10. package/dist/config/loader.d.ts +1 -1
  11. package/dist/config/loader.js +4 -2
  12. package/dist/index.d.ts +7 -6
  13. package/dist/index.js +4 -6
  14. package/dist/provider.d.ts +2 -1
  15. package/dist/provider.js +1 -1
  16. package/dist/runtime/auth-flow.js +1 -1
  17. package/dist/runtime/http.d.ts +1 -0
  18. package/dist/runtime/http.js +135 -12
  19. package/dist/runtime/instrumentation.js +1 -1
  20. package/dist/runtime/native-network-errors.d.ts +33 -0
  21. package/dist/runtime/native-network-errors.js +69 -0
  22. package/dist/runtime/native-network.d.ts +2 -33
  23. package/dist/runtime/native-network.js +2 -68
  24. package/dist/runtime/redis.d.ts +1 -1
  25. package/dist/runtime/redis.js +4 -2
  26. package/dist/runtime/resolver-config.d.ts +6 -0
  27. package/dist/runtime/resolver-config.js +6 -0
  28. package/dist/runtime/resolver-shared.d.ts +3 -0
  29. package/dist/runtime/resolver-shared.js +12 -0
  30. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -1
  31. package/dist/runtime/resolver-vendors/twocaptcha.js +80 -43
  32. package/dist/runtime/resolver-vendors/types.d.ts +1 -1
  33. package/dist/runtime/resolver.d.ts +6 -10
  34. package/dist/runtime/resolver.js +19 -18
  35. package/dist/runtime/state.js +5 -115
  36. package/dist/runtime/stealth-cookies.d.ts +20 -0
  37. package/dist/runtime/stealth-cookies.js +111 -0
  38. package/dist/runtime/stealth.js +7 -130
  39. package/dist/serve.d.ts +1 -1
  40. package/dist/serve.js +1 -1
  41. package/dist/server/index.d.ts +1 -1
  42. package/dist/server/index.js +1 -1
  43. package/dist/server/self-test.d.ts +13 -0
  44. package/dist/server/self-test.js +124 -46
  45. package/dist/server/serve-implementation.d.ts +199 -0
  46. package/dist/server/serve-implementation.js +2054 -0
  47. package/dist/server/serve.d.ts +1 -187
  48. package/dist/server/serve.js +1 -1827
  49. package/dist/stateful/errors.d.ts +5 -0
  50. package/dist/stateful/errors.js +10 -0
  51. package/dist/stateful/stateful-provider-session-routing.d.ts +1 -5
  52. package/dist/stateful/stateful-provider-session-routing.js +2 -10
  53. package/dist/stream.js +7 -1
  54. package/package.json +27 -2
  55. package/src/auth-turn/index.ts +1 -1
  56. package/src/ceremonies/index.ts +68 -18
  57. package/src/config/loader.ts +5 -2
  58. package/src/index.ts +18 -23
  59. package/src/provider.ts +12 -14
  60. package/src/runtime/auth-flow.ts +1 -1
  61. package/src/runtime/http.ts +155 -11
  62. package/src/runtime/instrumentation.ts +1 -1
  63. package/src/runtime/native-network-errors.ts +99 -0
  64. package/src/runtime/native-network.ts +16 -97
  65. package/src/runtime/redis.ts +7 -2
  66. package/src/runtime/resolver-config.ts +6 -0
  67. package/src/runtime/resolver-shared.ts +17 -0
  68. package/src/runtime/resolver-vendors/twocaptcha.ts +100 -49
  69. package/src/runtime/resolver-vendors/types.ts +1 -0
  70. package/src/runtime/resolver.ts +47 -22
  71. package/src/runtime/state.ts +5 -144
  72. package/src/runtime/stealth-cookies.ts +132 -0
  73. package/src/runtime/stealth.ts +14 -157
  74. package/src/serve.ts +6 -1
  75. package/src/server/index.ts +1 -0
  76. package/src/server/self-test.ts +184 -59
  77. package/src/server/serve-implementation.ts +3024 -0
  78. package/src/server/serve.ts +1 -2661
  79. package/src/stateful/errors.ts +12 -0
  80. package/src/stateful/stateful-provider-session-routing.ts +2 -11
  81. package/src/stream.ts +8 -1
@@ -44,6 +44,7 @@ export type HttpClientOptions = ProxyResolutionOptions & {
44
44
  warn?: (message: string) => void;
45
45
  userAgent?: string;
46
46
  onRetrySummary?: (summary: HttpRetrySummary) => void;
47
+ signal?: AbortSignal;
47
48
  };
48
49
 
49
50
  type HttpStatusOutcome = {
@@ -76,9 +77,45 @@ function isDedupeSkipOutcome(outcome: NativeHttpAttemptOutcome): outcome is Nati
76
77
  return "kind" in outcome && outcome.kind === "dedupe-skip";
77
78
  }
78
79
 
79
- async function sleep(ms: number): Promise<void> {
80
+ function toAmbientCancellationError(
81
+ signal: AbortSignal,
82
+ error: unknown = signal.reason,
83
+ ): TransportError {
84
+ if (error instanceof TransportError && error.code === "transport_cancelled") {
85
+ return error;
86
+ }
87
+ return new TransportError("Request cancelled", {
88
+ code: "transport_cancelled",
89
+ status: 0,
90
+ retryable: false,
91
+ ...(error !== undefined
92
+ ? { cause: error instanceof Error ? error : new Error(String(error)) }
93
+ : {}),
94
+ });
95
+ }
96
+
97
+ function throwIfAmbientAborted(signal: AbortSignal | undefined): void {
98
+ if (signal?.aborted) throw toAmbientCancellationError(signal);
99
+ }
100
+
101
+ async function sleep(ms: number, signal?: AbortSignal): Promise<void> {
102
+ throwIfAmbientAborted(signal);
80
103
  if (ms <= 0) return;
81
- await new Promise((resolve) => setTimeout(resolve, ms));
104
+ if (!signal) {
105
+ await new Promise((resolve) => setTimeout(resolve, ms));
106
+ return;
107
+ }
108
+ await new Promise<void>((resolve, reject) => {
109
+ const onAbort = () => {
110
+ clearTimeout(timer);
111
+ reject(toAmbientCancellationError(signal));
112
+ };
113
+ const timer = setTimeout(() => {
114
+ signal.removeEventListener("abort", onAbort);
115
+ resolve();
116
+ }, ms);
117
+ signal.addEventListener("abort", onAbort, { once: true });
118
+ });
82
119
  }
83
120
 
84
121
  function toUpstreamHttpError(status: number): TransportError {
@@ -132,7 +169,21 @@ function isTimeoutMessage(message: string): boolean {
132
169
  return /\b(timed out|timeout|deadline exceeded)\b/i.test(message);
133
170
  }
134
171
 
135
- function toHttpTransportError(error: unknown): TransportError {
172
+ function toHttpTransportError(
173
+ error: unknown,
174
+ ambientSignal?: AbortSignal,
175
+ timeoutSignal?: AbortSignal,
176
+ ): TransportError {
177
+ if (ambientSignal?.aborted) {
178
+ return toAmbientCancellationError(ambientSignal, error);
179
+ }
180
+ if (timeoutSignal?.aborted) {
181
+ return new TransportError("Request timed out", {
182
+ code: "transport_timeout",
183
+ status: 0,
184
+ ...(error instanceof Error ? { cause: error } : {}),
185
+ });
186
+ }
136
187
  if (error instanceof TransportError) {
137
188
  if (error.code) {
138
189
  return error;
@@ -216,6 +267,78 @@ function requireNativeResponseBody(response: Response): ReadableStream<Uint8Arra
216
267
  return response.body;
217
268
  }
218
269
 
270
+ function mergeAbortSignals(
271
+ ...signals: Array<AbortSignal | null | undefined>
272
+ ): AbortSignal | undefined {
273
+ const activeSignals = signals.filter((signal): signal is AbortSignal => signal != null);
274
+ if (activeSignals.length === 0) return undefined;
275
+ if (activeSignals.length === 1) return activeSignals[0];
276
+ return AbortSignal.any(activeSignals);
277
+ }
278
+
279
+ function cancelStreamOnAbort(
280
+ body: ReadableStream<Uint8Array>,
281
+ signal: AbortSignal | undefined,
282
+ ): ReadableStream<Uint8Array> {
283
+ if (!signal) return body;
284
+
285
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
286
+ let finished = false;
287
+ let streamController: ReadableStreamDefaultController<Uint8Array> | undefined;
288
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
289
+ const onAbort = () => {
290
+ if (finished) return;
291
+ finished = true;
292
+ cleanup();
293
+ const reason = toAmbientCancellationError(signal);
294
+ streamController?.error(reason);
295
+ const cancellation = reader ? reader.cancel(reason) : body.cancel(reason);
296
+ void cancellation.catch(() => undefined).finally(() => reader?.releaseLock());
297
+ };
298
+
299
+ return new ReadableStream<Uint8Array>(
300
+ {
301
+ start(controller) {
302
+ streamController = controller;
303
+ signal.addEventListener("abort", onAbort, { once: true });
304
+ if (signal.aborted) onAbort();
305
+ },
306
+ async pull(controller) {
307
+ try {
308
+ reader ??= body.getReader();
309
+ const chunk = await reader.read();
310
+ if (finished) return;
311
+ if (chunk.done) {
312
+ finished = true;
313
+ cleanup();
314
+ controller.close();
315
+ reader.releaseLock();
316
+ return;
317
+ }
318
+ controller.enqueue(chunk.value);
319
+ } catch (error) {
320
+ if (finished) return;
321
+ finished = true;
322
+ cleanup();
323
+ controller.error(error);
324
+ reader?.releaseLock();
325
+ }
326
+ },
327
+ async cancel(reason) {
328
+ if (finished) return;
329
+ finished = true;
330
+ cleanup();
331
+ try {
332
+ await (reader ? reader.cancel(reason) : body.cancel(reason));
333
+ } finally {
334
+ reader?.releaseLock();
335
+ }
336
+ },
337
+ },
338
+ { highWaterMark: 0 },
339
+ );
340
+ }
341
+
219
342
  function sanitizeStreamErrors(
220
343
  body: ReadableStream<Uint8Array>,
221
344
  serializedUrl: SerializedRequestUrl,
@@ -265,9 +388,13 @@ function sanitizeStreamErrors(
265
388
  function toNativeHttpStreamResponse(
266
389
  response: Response,
267
390
  serializedUrl: SerializedRequestUrl,
391
+ signal?: AbortSignal,
268
392
  ): HttpStreamResponse {
269
393
  const headers = Object.fromEntries(response.headers.entries());
270
- const body = sanitizeStreamErrors(requireNativeResponseBody(response), serializedUrl);
394
+ const body = sanitizeStreamErrors(
395
+ cancelStreamOnAbort(requireNativeResponseBody(response), signal),
396
+ serializedUrl,
397
+ );
271
398
  return {
272
399
  body,
273
400
  headers,
@@ -585,16 +712,19 @@ async function fetchNativeHttp(
585
712
  const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
586
713
  const { requestUrl } = serializedUrl;
587
714
  const controller = options.timeout ? new AbortController() : undefined;
715
+ const signal = mergeAbortSignals(clientOptions.signal, controller?.signal);
588
716
  const timeoutHandle = options.timeout
589
717
  ? setTimeout(() => controller?.abort(), options.timeout)
590
718
  : undefined;
591
719
 
592
720
  let proxy: string | undefined;
593
721
  try {
722
+ throwIfAmbientAborted(clientOptions.signal);
594
723
  // Resolve inside the try (and after the timeout is armed) so allocator
595
724
  // failures are branded as TransportErrors and count against the request
596
725
  // deadline, exactly as an inline resolve would.
597
726
  proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
727
+ throwIfAmbientAborted(clientOptions.signal);
598
728
  // For a registry allocator chain, skip an endpoint a prior attempt already
599
729
  // tried rather than re-issuing the same request. Returning the sentinel
600
730
  // (instead of breaking) lets the loop keep advancing the flat offset until
@@ -609,7 +739,7 @@ async function fetchNativeHttp(
609
739
  headers: options.headers,
610
740
  method,
611
741
  ...(proxy ? { proxy } : {}),
612
- signal: controller?.signal,
742
+ ...(signal ? { signal } : {}),
613
743
  };
614
744
  if (options.body !== undefined) {
615
745
  requestInit.body = normalizeNativeFetchBody(options.body);
@@ -651,7 +781,7 @@ async function fetchNativeHttp(
651
781
  );
652
782
  }
653
783
  const transportError: NativeHttpAttemptError = redactSensitiveError(
654
- toHttpTransportError(error),
784
+ toHttpTransportError(error, clientOptions.signal, controller?.signal),
655
785
  serializedUrl.sensitiveValues,
656
786
  serializedUrl.requestUrl,
657
787
  serializedUrl.redactedUrl,
@@ -674,17 +804,20 @@ async function fetchNativeHttpStream(
674
804
  const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
675
805
  const { requestUrl } = serializedUrl;
676
806
  const controller = options.timeout ? new AbortController() : undefined;
807
+ const signal = mergeAbortSignals(clientOptions.signal, controller?.signal);
677
808
  const timeoutHandle = options.timeout
678
809
  ? setTimeout(() => controller?.abort(), options.timeout)
679
810
  : undefined;
680
811
 
681
812
  try {
813
+ throwIfAmbientAborted(clientOptions.signal);
682
814
  const proxy = await resolveNativeProxy(options, clientOptions, warn);
815
+ throwIfAmbientAborted(clientOptions.signal);
683
816
  const requestInit: NativeFetchInit = {
684
817
  headers: options.headers,
685
818
  method,
686
819
  ...(proxy ? { proxy } : {}),
687
- signal: controller?.signal,
820
+ ...(signal ? { signal } : {}),
688
821
  };
689
822
  if (options.body !== undefined) {
690
823
  requestInit.body = normalizeNativeFetchBody(options.body);
@@ -703,7 +836,9 @@ async function fetchNativeHttpStream(
703
836
  });
704
837
  }
705
838
 
706
- return toNativeHttpStreamResponse(response, serializedUrl);
839
+ // Per-call timeout remains header-scoped, while the ambient request signal
840
+ // stays attached to the response body for its full consumption lifetime.
841
+ return toNativeHttpStreamResponse(response, serializedUrl, clientOptions.signal);
707
842
  } catch (error) {
708
843
  if (error instanceof SyntaxError) {
709
844
  throw redactSensitiveError(
@@ -714,7 +849,7 @@ async function fetchNativeHttpStream(
714
849
  );
715
850
  }
716
851
  throw redactSensitiveError(
717
- toHttpTransportError(error),
852
+ toHttpTransportError(error, clientOptions.signal, controller?.signal),
718
853
  serializedUrl.sensitiveValues,
719
854
  serializedUrl.requestUrl,
720
855
  serializedUrl.redactedUrl,
@@ -850,6 +985,7 @@ export function createHttpClient(
850
985
  );
851
986
 
852
987
  if (!retryEnabled || !retryOptions) {
988
+ throwIfAmbientAborted(clientOptions.signal);
853
989
  const outcome = await executeOnce();
854
990
  if (isDedupeSkipOutcome(outcome)) {
855
991
  // Single-shot path never de-duplicates (dedupeContext is undefined),
@@ -874,6 +1010,7 @@ export function createHttpClient(
874
1010
  // allocations skip offsets.
875
1011
  let issued = 0;
876
1012
  for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
1013
+ throwIfAmbientAborted(clientOptions.signal);
877
1014
  // Whether this offset actually issued a request (vs. a skipped duplicate),
878
1015
  // so the catch counts a thrown *transport* failure once without
879
1016
  // double-counting a status outcome that already incremented before it
@@ -892,7 +1029,10 @@ export function createHttpClient(
892
1029
  if (isHttpStatusOutcome(outcome)) {
893
1030
  lastStatus = outcome.status;
894
1031
  if (outcome.retryable && issued < retryOptions.attempts) {
895
- await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers));
1032
+ await sleep(
1033
+ computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers),
1034
+ clientOptions.signal,
1035
+ );
896
1036
  continue;
897
1037
  }
898
1038
  throw toUpstreamHttpError(outcome.status);
@@ -916,6 +1056,7 @@ export function createHttpClient(
916
1056
  }
917
1057
  return response;
918
1058
  } catch (error) {
1059
+ throwIfAmbientAborted(clientOptions.signal);
919
1060
  if (!issuedThisAttempt) issued += 1;
920
1061
  lastError = error;
921
1062
  lastErrorCode = proxyTransportRetryErrorCode(error);
@@ -931,7 +1072,10 @@ export function createHttpClient(
931
1072
  proxyUsed,
932
1073
  })
933
1074
  ) {
934
- await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt));
1075
+ await sleep(
1076
+ computeProxyTransportRetryDelayMs(retryOptions, attempt),
1077
+ clientOptions.signal,
1078
+ );
935
1079
  continue;
936
1080
  }
937
1081
  throw error;
@@ -23,7 +23,7 @@ import {
23
23
  getTraceRecorder,
24
24
  type TraceContext,
25
25
  } from "./trace.js";
26
- import { RESOLVER_INSTRUMENTATION_METADATA } from "./resolver.js";
26
+ import { RESOLVER_INSTRUMENTATION_METADATA } from "./resolver-shared.js";
27
27
 
28
28
  export interface InstrumentationOptions extends CreateTraceContextOptions {}
29
29
 
@@ -0,0 +1,99 @@
1
+ import { TransportError } from "../errors.js";
2
+ import { canonicalizeEgressHost } from "../native-address.js";
3
+
4
+ export type NativeNetworkErrorCode =
5
+ | "native_connection_aborted"
6
+ | "native_connection_closed"
7
+ | "native_connection_failed"
8
+ | "native_connection_idle_timeout"
9
+ | "native_connection_timeout"
10
+ | "native_egress_authorization_failed"
11
+ | "native_egress_grant_expired"
12
+ | "native_egress_grant_invalid"
13
+ | "native_egress_grant_limit_exceeded"
14
+ | "native_egress_input_invalid"
15
+ | "native_egress_not_declared"
16
+ | "native_egress_policy_invalid"
17
+ | "native_dynamic_egress_unsupported"
18
+ | "native_proxy_expired"
19
+ | "native_proxy_invalid";
20
+
21
+ export class NativeNetworkError extends TransportError {
22
+ constructor(message: string, code: NativeNetworkErrorCode, cause?: Error) {
23
+ const isEgressPolicyFailure =
24
+ code.startsWith("native_egress_") || code === "native_dynamic_egress_unsupported";
25
+ super(message, {
26
+ code,
27
+ status: 0,
28
+ ...(cause ? { cause } : {}),
29
+ ...(isEgressPolicyFailure ? { category: "provider_error" as const, retryable: false } : {}),
30
+ });
31
+ this.name = "NativeNetworkError";
32
+ }
33
+
34
+ override get code(): NativeNetworkErrorCode {
35
+ return super.code as NativeNetworkErrorCode;
36
+ }
37
+ }
38
+
39
+ export function safeDiagnosticEgressHost(value: unknown): string {
40
+ const canonical = canonicalizeEgressHost(value);
41
+ return canonical.ok ? canonical.host : `<invalid-host:${canonical.reason}>`;
42
+ }
43
+
44
+ export class NativeProxyExpiredError extends NativeNetworkError {
45
+ constructor(readonly expiresAt: string) {
46
+ super("Native connection closed at sticky proxy expiry", "native_proxy_expired");
47
+ this.name = "NativeProxyExpiredError";
48
+ }
49
+ }
50
+
51
+ /** Raised before transport setup when a native destination is not authorized. */
52
+ export class NativeEgressNotDeclaredError extends NativeNetworkError {
53
+ readonly host: string;
54
+
55
+ constructor(
56
+ host: string,
57
+ readonly port: number,
58
+ readonly tls: "required" | "disabled",
59
+ ) {
60
+ const diagnosticHost = safeDiagnosticEgressHost(host);
61
+ super(
62
+ `Native ${tls === "required" ? "TLS" : "TCP"} egress is not declared for ${diagnosticHost}:${port}`,
63
+ "native_egress_not_declared",
64
+ );
65
+ this.host = diagnosticHost;
66
+ this.name = "NativeEgressNotDeclaredError";
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Raised when the destination was authorized by a grant whose TTL elapsed and
72
+ * its expiry remains in the client's bounded recent-expiry evidence window.
73
+ */
74
+ export class NativeEgressGrantExpiredError extends NativeNetworkError {
75
+ readonly host: string;
76
+
77
+ constructor(
78
+ host: string,
79
+ readonly port: number,
80
+ readonly tls: "required" | "disabled",
81
+ readonly expiresAt: string,
82
+ ) {
83
+ const diagnosticHost = safeDiagnosticEgressHost(host);
84
+ super(
85
+ `Native ${tls === "required" ? "TLS" : "TCP"} egress grant expired for ${diagnosticHost}:${port}`,
86
+ "native_egress_grant_expired",
87
+ );
88
+ this.host = diagnosticHost;
89
+ this.name = "NativeEgressGrantExpiredError";
90
+ }
91
+ }
92
+
93
+ /** Raised when an established connection exceeds its opt-in read-idle window. */
94
+ export class NativeIdleTimeoutError extends NativeNetworkError {
95
+ constructor() {
96
+ super("Native network socket timed out while reading.", "native_connection_idle_timeout");
97
+ this.name = "NativeIdleTimeoutError";
98
+ }
99
+ }
@@ -12,7 +12,6 @@ import {
12
12
  VENDOR_DEFAULT_PROTOCOL,
13
13
  resolveWithVendor,
14
14
  } from "../config/loader.js";
15
- import { TransportError } from "../errors.js";
16
15
  import {
17
16
  type DynamicEgressRuleSnapshot,
18
17
  NativeEgressPolicyValidationError,
@@ -46,6 +45,14 @@ import type {
46
45
  EnvContext,
47
46
  } from "../types.js";
48
47
  import { createEnvContext } from "./env.js";
48
+ import {
49
+ NativeEgressGrantExpiredError,
50
+ NativeEgressNotDeclaredError,
51
+ NativeIdleTimeoutError,
52
+ NativeNetworkError,
53
+ NativeProxyExpiredError,
54
+ safeDiagnosticEgressHost,
55
+ } from "./native-network-errors.js";
49
56
  import {
50
57
  NODEMAVEN_FILTER_ENV,
51
58
  NODEMAVEN_PASSWORD_ENV,
@@ -55,102 +62,14 @@ import {
55
62
  } from "./proxy-nodemaven.js";
56
63
  import { redactSensitiveError } from "./request-options.js";
57
64
 
58
- export type NativeNetworkErrorCode =
59
- | "native_connection_aborted"
60
- | "native_connection_closed"
61
- | "native_connection_failed"
62
- | "native_connection_idle_timeout"
63
- | "native_connection_timeout"
64
- | "native_egress_authorization_failed"
65
- | "native_egress_grant_expired"
66
- | "native_egress_grant_invalid"
67
- | "native_egress_grant_limit_exceeded"
68
- | "native_egress_input_invalid"
69
- | "native_egress_not_declared"
70
- | "native_egress_policy_invalid"
71
- | "native_dynamic_egress_unsupported"
72
- | "native_proxy_expired"
73
- | "native_proxy_invalid";
74
-
75
- export class NativeNetworkError extends TransportError {
76
- constructor(message: string, code: NativeNetworkErrorCode, cause?: Error) {
77
- const isEgressPolicyFailure =
78
- code.startsWith("native_egress_") || code === "native_dynamic_egress_unsupported";
79
- super(message, {
80
- code,
81
- status: 0,
82
- ...(cause ? { cause } : {}),
83
- ...(isEgressPolicyFailure ? { category: "provider_error" as const, retryable: false } : {}),
84
- });
85
- this.name = "NativeNetworkError";
86
- }
87
-
88
- override get code(): NativeNetworkErrorCode {
89
- return super.code as NativeNetworkErrorCode;
90
- }
91
- }
92
-
93
- function safeDiagnosticEgressHost(value: unknown): string {
94
- const canonical = canonicalizeEgressHost(value);
95
- return canonical.ok ? canonical.host : `<invalid-host:${canonical.reason}>`;
96
- }
97
-
98
- export class NativeProxyExpiredError extends NativeNetworkError {
99
- constructor(readonly expiresAt: string) {
100
- super("Native connection closed at sticky proxy expiry", "native_proxy_expired");
101
- this.name = "NativeProxyExpiredError";
102
- }
103
- }
104
-
105
- /** Raised before transport setup when a native destination is not authorized. */
106
- export class NativeEgressNotDeclaredError extends NativeNetworkError {
107
- readonly host: string;
108
-
109
- constructor(
110
- host: string,
111
- readonly port: number,
112
- readonly tls: "required" | "disabled",
113
- ) {
114
- const diagnosticHost = safeDiagnosticEgressHost(host);
115
- super(
116
- `Native ${tls === "required" ? "TLS" : "TCP"} egress is not declared for ${diagnosticHost}:${port}`,
117
- "native_egress_not_declared",
118
- );
119
- this.host = diagnosticHost;
120
- this.name = "NativeEgressNotDeclaredError";
121
- }
122
- }
123
-
124
- /**
125
- * Raised when the destination was authorized by a grant whose TTL elapsed and
126
- * its expiry remains in the client's bounded recent-expiry evidence window.
127
- */
128
- export class NativeEgressGrantExpiredError extends NativeNetworkError {
129
- readonly host: string;
130
-
131
- constructor(
132
- host: string,
133
- readonly port: number,
134
- readonly tls: "required" | "disabled",
135
- readonly expiresAt: string,
136
- ) {
137
- const diagnosticHost = safeDiagnosticEgressHost(host);
138
- super(
139
- `Native ${tls === "required" ? "TLS" : "TCP"} egress grant expired for ${diagnosticHost}:${port}`,
140
- "native_egress_grant_expired",
141
- );
142
- this.host = diagnosticHost;
143
- this.name = "NativeEgressGrantExpiredError";
144
- }
145
- }
146
-
147
- /** Raised when an established connection exceeds its opt-in read-idle window. */
148
- export class NativeIdleTimeoutError extends NativeNetworkError {
149
- constructor() {
150
- super("Native network socket timed out while reading.", "native_connection_idle_timeout");
151
- this.name = "NativeIdleTimeoutError";
152
- }
153
- }
65
+ export {
66
+ NativeEgressGrantExpiredError,
67
+ NativeEgressNotDeclaredError,
68
+ NativeIdleTimeoutError,
69
+ NativeNetworkError,
70
+ NativeProxyExpiredError,
71
+ } from "./native-network-errors.js";
72
+ export type { NativeNetworkErrorCode } from "./native-network-errors.js";
154
73
 
155
74
  export type NativeGatewayProxy = NativeProxyEgressInfo & {
156
75
  readonly url: string;
@@ -1,4 +1,8 @@
1
- import { Redis } from "ioredis";
1
+ import { createRequire } from "node:module";
2
+
3
+ import type { Redis } from "ioredis";
4
+
5
+ const require = createRequire(import.meta.url);
2
6
 
3
7
  export type ProviderRedisClient = Redis;
4
8
 
@@ -17,7 +21,8 @@ type RedisTimeoutOptions<T> = {
17
21
  export function createProviderRedisClient(
18
22
  options: ProviderRedisClientOptions,
19
23
  ): ProviderRedisClient {
20
- const redis = new Redis(options.redisUrl, {
24
+ const { Redis: RedisClient } = require("ioredis") as typeof import("ioredis");
25
+ const redis = new RedisClient(options.redisUrl, {
21
26
  connectTimeout: options.timeoutMs,
22
27
  enableOfflineQueue: false,
23
28
  lazyConnect: true,
@@ -0,0 +1,6 @@
1
+ export const APIFUSE__RESOLVER__2CAPTCHA__API_KEY = "APIFUSE__RESOLVER__2CAPTCHA__API_KEY";
2
+ export const APIFUSE__RESOLVER__CAPSOLVER__API_KEY = "APIFUSE__RESOLVER__CAPSOLVER__API_KEY";
3
+ export const APIFUSE__RESOLVER__CAPMONSTER__API_KEY = "APIFUSE__RESOLVER__CAPMONSTER__API_KEY";
4
+ export const APIFUSE__RESOLVER__TIMEOUT_MS = "APIFUSE__RESOLVER__TIMEOUT_MS";
5
+ export const APIFUSE__CDP_POOL__URL = "APIFUSE__CDP_POOL__URL";
6
+ export const DEFAULT_RESOLVER_TIMEOUT_MS = 180_000;
@@ -0,0 +1,17 @@
1
+ import { ProviderError } from "../errors.js";
2
+ import type { ResolverContext } from "../types.js";
3
+
4
+ export const RESOLVER_INSTRUMENTATION_METADATA = Symbol.for(
5
+ "@apifuse/provider-sdk/runtime/resolver-instrumentation-metadata",
6
+ );
7
+
8
+ export function createUnsupportedResolverClient(reason?: string): ResolverContext {
9
+ return {
10
+ async solve() {
11
+ throw new ProviderError(reason ?? "Resolver runtime is not configured", {
12
+ code: "RESOLVER_UNAVAILABLE",
13
+ fix: "Declare resolver on the provider definition and configure vendor credentials.",
14
+ });
15
+ },
16
+ };
17
+ }