@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.
- package/CHANGELOG.md +4 -0
- package/bin/apifuse-dev.ts +2 -2
- package/bin/apifuse-pack-smoke.ts +1 -1
- package/bin/apifuse-pack-types.ts +2 -1
- package/bin/apifuse-perf.ts +2 -4
- package/bin/apifuse-record.ts +1 -1
- package/dist/auth-turn/index.d.ts +1 -1
- package/dist/auth-turn/index.js +1 -1
- package/dist/ceremonies/index.js +52 -11
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.js +4 -2
- package/dist/index.d.ts +7 -6
- package/dist/index.js +4 -6
- package/dist/provider.d.ts +2 -1
- package/dist/provider.js +1 -1
- package/dist/runtime/auth-flow.js +1 -1
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.js +135 -12
- package/dist/runtime/instrumentation.js +1 -1
- package/dist/runtime/native-network-errors.d.ts +33 -0
- package/dist/runtime/native-network-errors.js +69 -0
- package/dist/runtime/native-network.d.ts +2 -33
- package/dist/runtime/native-network.js +2 -68
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +4 -2
- package/dist/runtime/resolver-config.d.ts +6 -0
- package/dist/runtime/resolver-config.js +6 -0
- package/dist/runtime/resolver-shared.d.ts +3 -0
- package/dist/runtime/resolver-shared.js +12 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -1
- package/dist/runtime/resolver-vendors/twocaptcha.js +80 -43
- package/dist/runtime/resolver-vendors/types.d.ts +1 -1
- package/dist/runtime/resolver.d.ts +6 -10
- package/dist/runtime/resolver.js +19 -18
- package/dist/runtime/state.js +5 -115
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.js +7 -130
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/self-test.d.ts +13 -0
- package/dist/server/self-test.js +124 -46
- package/dist/server/serve-implementation.d.ts +199 -0
- package/dist/server/serve-implementation.js +2054 -0
- package/dist/server/serve.d.ts +1 -187
- package/dist/server/serve.js +1 -1827
- package/dist/stateful/errors.d.ts +5 -0
- package/dist/stateful/errors.js +10 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +1 -5
- package/dist/stateful/stateful-provider-session-routing.js +2 -10
- package/dist/stream.js +7 -1
- package/package.json +27 -2
- package/src/auth-turn/index.ts +1 -1
- package/src/ceremonies/index.ts +68 -18
- package/src/config/loader.ts +5 -2
- package/src/index.ts +18 -23
- package/src/provider.ts +12 -14
- package/src/runtime/auth-flow.ts +1 -1
- package/src/runtime/http.ts +155 -11
- package/src/runtime/instrumentation.ts +1 -1
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +16 -97
- package/src/runtime/redis.ts +7 -2
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-shared.ts +17 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +100 -49
- package/src/runtime/resolver-vendors/types.ts +1 -0
- package/src/runtime/resolver.ts +47 -22
- package/src/runtime/state.ts +5 -144
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +14 -157
- package/src/serve.ts +6 -1
- package/src/server/index.ts +1 -0
- package/src/server/self-test.ts +184 -59
- package/src/server/serve-implementation.ts +3024 -0
- package/src/server/serve.ts +1 -2661
- package/src/stateful/errors.ts +12 -0
- package/src/stateful/stateful-provider-session-routing.ts +2 -11
- package/src/stream.ts +8 -1
package/src/runtime/http.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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;
|
package/src/runtime/redis.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
+
}
|