@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readableBytes, readableLines, readableTextChunks } from "../stream.js";
|
|
2
2
|
import { parseHttpRequestInvocation, isSensitiveKey, redactSensitiveError, redactSensitiveText, redactUrlQueryParams, requestOptionsFromHttpInvocation, serializeRequestUrl, } from "./request-options.js";
|
|
3
3
|
import { createTraceContext, getTraceRecorder, } from "./trace.js";
|
|
4
|
-
import { RESOLVER_INSTRUMENTATION_METADATA } from "./resolver.js";
|
|
4
|
+
import { RESOLVER_INSTRUMENTATION_METADATA } from "./resolver-shared.js";
|
|
5
5
|
const BROWSER_PAGE_METHODS = new Set(["goto", "fill", "click", "type", "waitForSelector"]);
|
|
6
6
|
const DIAGNOSTIC_BASE_URL = "http://apifuse-instrumentation.invalid";
|
|
7
7
|
function isThenable(value) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { TransportError } from "../errors.js";
|
|
2
|
+
export type NativeNetworkErrorCode = "native_connection_aborted" | "native_connection_closed" | "native_connection_failed" | "native_connection_idle_timeout" | "native_connection_timeout" | "native_egress_authorization_failed" | "native_egress_grant_expired" | "native_egress_grant_invalid" | "native_egress_grant_limit_exceeded" | "native_egress_input_invalid" | "native_egress_not_declared" | "native_egress_policy_invalid" | "native_dynamic_egress_unsupported" | "native_proxy_expired" | "native_proxy_invalid";
|
|
3
|
+
export declare class NativeNetworkError extends TransportError {
|
|
4
|
+
constructor(message: string, code: NativeNetworkErrorCode, cause?: Error);
|
|
5
|
+
get code(): NativeNetworkErrorCode;
|
|
6
|
+
}
|
|
7
|
+
export declare function safeDiagnosticEgressHost(value: unknown): string;
|
|
8
|
+
export declare class NativeProxyExpiredError extends NativeNetworkError {
|
|
9
|
+
readonly expiresAt: string;
|
|
10
|
+
constructor(expiresAt: string);
|
|
11
|
+
}
|
|
12
|
+
/** Raised before transport setup when a native destination is not authorized. */
|
|
13
|
+
export declare class NativeEgressNotDeclaredError extends NativeNetworkError {
|
|
14
|
+
readonly port: number;
|
|
15
|
+
readonly tls: "required" | "disabled";
|
|
16
|
+
readonly host: string;
|
|
17
|
+
constructor(host: string, port: number, tls: "required" | "disabled");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Raised when the destination was authorized by a grant whose TTL elapsed and
|
|
21
|
+
* its expiry remains in the client's bounded recent-expiry evidence window.
|
|
22
|
+
*/
|
|
23
|
+
export declare class NativeEgressGrantExpiredError extends NativeNetworkError {
|
|
24
|
+
readonly port: number;
|
|
25
|
+
readonly tls: "required" | "disabled";
|
|
26
|
+
readonly expiresAt: string;
|
|
27
|
+
readonly host: string;
|
|
28
|
+
constructor(host: string, port: number, tls: "required" | "disabled", expiresAt: string);
|
|
29
|
+
}
|
|
30
|
+
/** Raised when an established connection exceeds its opt-in read-idle window. */
|
|
31
|
+
export declare class NativeIdleTimeoutError extends NativeNetworkError {
|
|
32
|
+
constructor();
|
|
33
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { TransportError } from "../errors.js";
|
|
2
|
+
import { canonicalizeEgressHost } from "../native-address.js";
|
|
3
|
+
export class NativeNetworkError extends TransportError {
|
|
4
|
+
constructor(message, code, cause) {
|
|
5
|
+
const isEgressPolicyFailure = code.startsWith("native_egress_") || code === "native_dynamic_egress_unsupported";
|
|
6
|
+
super(message, {
|
|
7
|
+
code,
|
|
8
|
+
status: 0,
|
|
9
|
+
...(cause ? { cause } : {}),
|
|
10
|
+
...(isEgressPolicyFailure ? { category: "provider_error", retryable: false } : {}),
|
|
11
|
+
});
|
|
12
|
+
this.name = "NativeNetworkError";
|
|
13
|
+
}
|
|
14
|
+
get code() {
|
|
15
|
+
return super.code;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function safeDiagnosticEgressHost(value) {
|
|
19
|
+
const canonical = canonicalizeEgressHost(value);
|
|
20
|
+
return canonical.ok ? canonical.host : `<invalid-host:${canonical.reason}>`;
|
|
21
|
+
}
|
|
22
|
+
export class NativeProxyExpiredError extends NativeNetworkError {
|
|
23
|
+
expiresAt;
|
|
24
|
+
constructor(expiresAt) {
|
|
25
|
+
super("Native connection closed at sticky proxy expiry", "native_proxy_expired");
|
|
26
|
+
this.expiresAt = expiresAt;
|
|
27
|
+
this.name = "NativeProxyExpiredError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Raised before transport setup when a native destination is not authorized. */
|
|
31
|
+
export class NativeEgressNotDeclaredError extends NativeNetworkError {
|
|
32
|
+
port;
|
|
33
|
+
tls;
|
|
34
|
+
host;
|
|
35
|
+
constructor(host, port, tls) {
|
|
36
|
+
const diagnosticHost = safeDiagnosticEgressHost(host);
|
|
37
|
+
super(`Native ${tls === "required" ? "TLS" : "TCP"} egress is not declared for ${diagnosticHost}:${port}`, "native_egress_not_declared");
|
|
38
|
+
this.port = port;
|
|
39
|
+
this.tls = tls;
|
|
40
|
+
this.host = diagnosticHost;
|
|
41
|
+
this.name = "NativeEgressNotDeclaredError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Raised when the destination was authorized by a grant whose TTL elapsed and
|
|
46
|
+
* its expiry remains in the client's bounded recent-expiry evidence window.
|
|
47
|
+
*/
|
|
48
|
+
export class NativeEgressGrantExpiredError extends NativeNetworkError {
|
|
49
|
+
port;
|
|
50
|
+
tls;
|
|
51
|
+
expiresAt;
|
|
52
|
+
host;
|
|
53
|
+
constructor(host, port, tls, expiresAt) {
|
|
54
|
+
const diagnosticHost = safeDiagnosticEgressHost(host);
|
|
55
|
+
super(`Native ${tls === "required" ? "TLS" : "TCP"} egress grant expired for ${diagnosticHost}:${port}`, "native_egress_grant_expired");
|
|
56
|
+
this.port = port;
|
|
57
|
+
this.tls = tls;
|
|
58
|
+
this.expiresAt = expiresAt;
|
|
59
|
+
this.host = diagnosticHost;
|
|
60
|
+
this.name = "NativeEgressGrantExpiredError";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Raised when an established connection exceeds its opt-in read-idle window. */
|
|
64
|
+
export class NativeIdleTimeoutError extends NativeNetworkError {
|
|
65
|
+
constructor() {
|
|
66
|
+
super("Native network socket timed out while reading.", "native_connection_idle_timeout");
|
|
67
|
+
this.name = "NativeIdleTimeoutError";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -1,40 +1,10 @@
|
|
|
1
1
|
import { Socket } from "node:net";
|
|
2
2
|
import { type TLSSocket } from "node:tls";
|
|
3
3
|
import { type ProxyProtocol } from "../config/loader.js";
|
|
4
|
-
import { TransportError } from "../errors.js";
|
|
5
4
|
import { type DynamicEgressRuleSnapshot } from "../native-egress-policy.js";
|
|
6
5
|
import type { NativeNetworkClient, NativeNetworkConnectInput, NativeNetworkConnection, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProxyEgressInfo, ProviderProxyPolicy, ProviderProxyProvider, EnvContext } from "../types.js";
|
|
7
|
-
export
|
|
8
|
-
export
|
|
9
|
-
constructor(message: string, code: NativeNetworkErrorCode, cause?: Error);
|
|
10
|
-
get code(): NativeNetworkErrorCode;
|
|
11
|
-
}
|
|
12
|
-
export declare class NativeProxyExpiredError extends NativeNetworkError {
|
|
13
|
-
readonly expiresAt: string;
|
|
14
|
-
constructor(expiresAt: string);
|
|
15
|
-
}
|
|
16
|
-
/** Raised before transport setup when a native destination is not authorized. */
|
|
17
|
-
export declare class NativeEgressNotDeclaredError extends NativeNetworkError {
|
|
18
|
-
readonly port: number;
|
|
19
|
-
readonly tls: "required" | "disabled";
|
|
20
|
-
readonly host: string;
|
|
21
|
-
constructor(host: string, port: number, tls: "required" | "disabled");
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Raised when the destination was authorized by a grant whose TTL elapsed and
|
|
25
|
-
* its expiry remains in the client's bounded recent-expiry evidence window.
|
|
26
|
-
*/
|
|
27
|
-
export declare class NativeEgressGrantExpiredError extends NativeNetworkError {
|
|
28
|
-
readonly port: number;
|
|
29
|
-
readonly tls: "required" | "disabled";
|
|
30
|
-
readonly expiresAt: string;
|
|
31
|
-
readonly host: string;
|
|
32
|
-
constructor(host: string, port: number, tls: "required" | "disabled", expiresAt: string);
|
|
33
|
-
}
|
|
34
|
-
/** Raised when an established connection exceeds its opt-in read-idle window. */
|
|
35
|
-
export declare class NativeIdleTimeoutError extends NativeNetworkError {
|
|
36
|
-
constructor();
|
|
37
|
-
}
|
|
6
|
+
export { NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, } from "./native-network-errors.js";
|
|
7
|
+
export type { NativeNetworkErrorCode } from "./native-network-errors.js";
|
|
38
8
|
export type NativeGatewayProxy = NativeProxyEgressInfo & {
|
|
39
9
|
readonly url: string;
|
|
40
10
|
};
|
|
@@ -124,4 +94,3 @@ export declare function createNativeEgressAuthorization(options: NativeNetworkCl
|
|
|
124
94
|
};
|
|
125
95
|
/** Create the SDK byte-stream runtime with provider-declared egress enforcement. */
|
|
126
96
|
export declare function createNativeNetworkClient(options?: NativeNetworkClientOptions): NativeNetworkClient;
|
|
127
|
-
export {};
|
|
@@ -3,79 +3,13 @@ import { Socket } from "node:net";
|
|
|
3
3
|
import { connect as connectTlsSocket } from "node:tls";
|
|
4
4
|
import { SocksClient } from "socks";
|
|
5
5
|
import { assertTunnelingScheme, ProxyResolutionError, SMARTPROXY_APP_KEY_ENV, VENDOR_DEFAULT_PROTOCOL, resolveWithVendor, } from "../config/loader.js";
|
|
6
|
-
import { TransportError } from "../errors.js";
|
|
7
6
|
import { NativeEgressPolicyValidationError, parseNativeEgressPolicy, } from "../native-egress-policy.js";
|
|
8
7
|
import { canonicalizeEgressHost, classifyEgressHost, embeddedIpv4FromIpv6, ipv4InCidr, ipv6InCidr, parseIpv6, parseStrictIpv4, } from "../native-address.js";
|
|
9
8
|
import { createEnvContext } from "./env.js";
|
|
9
|
+
import { NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, safeDiagnosticEgressHost, } from "./native-network-errors.js";
|
|
10
10
|
import { NODEMAVEN_FILTER_ENV, NODEMAVEN_PASSWORD_ENV, NODEMAVEN_USERNAME_ENV, nodemavenSessionWindow, synthesizeNodemavenProxy, } from "./proxy-nodemaven.js";
|
|
11
11
|
import { redactSensitiveError } from "./request-options.js";
|
|
12
|
-
export
|
|
13
|
-
constructor(message, code, cause) {
|
|
14
|
-
const isEgressPolicyFailure = code.startsWith("native_egress_") || code === "native_dynamic_egress_unsupported";
|
|
15
|
-
super(message, {
|
|
16
|
-
code,
|
|
17
|
-
status: 0,
|
|
18
|
-
...(cause ? { cause } : {}),
|
|
19
|
-
...(isEgressPolicyFailure ? { category: "provider_error", retryable: false } : {}),
|
|
20
|
-
});
|
|
21
|
-
this.name = "NativeNetworkError";
|
|
22
|
-
}
|
|
23
|
-
get code() {
|
|
24
|
-
return super.code;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function safeDiagnosticEgressHost(value) {
|
|
28
|
-
const canonical = canonicalizeEgressHost(value);
|
|
29
|
-
return canonical.ok ? canonical.host : `<invalid-host:${canonical.reason}>`;
|
|
30
|
-
}
|
|
31
|
-
export class NativeProxyExpiredError extends NativeNetworkError {
|
|
32
|
-
expiresAt;
|
|
33
|
-
constructor(expiresAt) {
|
|
34
|
-
super("Native connection closed at sticky proxy expiry", "native_proxy_expired");
|
|
35
|
-
this.expiresAt = expiresAt;
|
|
36
|
-
this.name = "NativeProxyExpiredError";
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
/** Raised before transport setup when a native destination is not authorized. */
|
|
40
|
-
export class NativeEgressNotDeclaredError extends NativeNetworkError {
|
|
41
|
-
port;
|
|
42
|
-
tls;
|
|
43
|
-
host;
|
|
44
|
-
constructor(host, port, tls) {
|
|
45
|
-
const diagnosticHost = safeDiagnosticEgressHost(host);
|
|
46
|
-
super(`Native ${tls === "required" ? "TLS" : "TCP"} egress is not declared for ${diagnosticHost}:${port}`, "native_egress_not_declared");
|
|
47
|
-
this.port = port;
|
|
48
|
-
this.tls = tls;
|
|
49
|
-
this.host = diagnosticHost;
|
|
50
|
-
this.name = "NativeEgressNotDeclaredError";
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Raised when the destination was authorized by a grant whose TTL elapsed and
|
|
55
|
-
* its expiry remains in the client's bounded recent-expiry evidence window.
|
|
56
|
-
*/
|
|
57
|
-
export class NativeEgressGrantExpiredError extends NativeNetworkError {
|
|
58
|
-
port;
|
|
59
|
-
tls;
|
|
60
|
-
expiresAt;
|
|
61
|
-
host;
|
|
62
|
-
constructor(host, port, tls, expiresAt) {
|
|
63
|
-
const diagnosticHost = safeDiagnosticEgressHost(host);
|
|
64
|
-
super(`Native ${tls === "required" ? "TLS" : "TCP"} egress grant expired for ${diagnosticHost}:${port}`, "native_egress_grant_expired");
|
|
65
|
-
this.port = port;
|
|
66
|
-
this.tls = tls;
|
|
67
|
-
this.expiresAt = expiresAt;
|
|
68
|
-
this.host = diagnosticHost;
|
|
69
|
-
this.name = "NativeEgressGrantExpiredError";
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
/** Raised when an established connection exceeds its opt-in read-idle window. */
|
|
73
|
-
export class NativeIdleTimeoutError extends NativeNetworkError {
|
|
74
|
-
constructor() {
|
|
75
|
-
super("Native network socket timed out while reading.", "native_connection_idle_timeout");
|
|
76
|
-
this.name = "NativeIdleTimeoutError";
|
|
77
|
-
}
|
|
78
|
-
}
|
|
12
|
+
export { NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, } from "./native-network-errors.js";
|
|
79
13
|
const VENDOR_CREDENTIAL_NAMES = {
|
|
80
14
|
smartproxy: [SMARTPROXY_APP_KEY_ENV],
|
|
81
15
|
nodemaven: [NODEMAVEN_USERNAME_ENV, NODEMAVEN_PASSWORD_ENV],
|
package/dist/runtime/redis.d.ts
CHANGED
package/dist/runtime/redis.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
2
3
|
export function createProviderRedisClient(options) {
|
|
3
|
-
const
|
|
4
|
+
const { Redis: RedisClient } = require("ioredis");
|
|
5
|
+
const redis = new RedisClient(options.redisUrl, {
|
|
4
6
|
connectTimeout: options.timeoutMs,
|
|
5
7
|
enableOfflineQueue: false,
|
|
6
8
|
lazyConnect: true,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const APIFUSE__RESOLVER__2CAPTCHA__API_KEY = "APIFUSE__RESOLVER__2CAPTCHA__API_KEY";
|
|
2
|
+
export declare const APIFUSE__RESOLVER__CAPSOLVER__API_KEY = "APIFUSE__RESOLVER__CAPSOLVER__API_KEY";
|
|
3
|
+
export declare const APIFUSE__RESOLVER__CAPMONSTER__API_KEY = "APIFUSE__RESOLVER__CAPMONSTER__API_KEY";
|
|
4
|
+
export declare const APIFUSE__RESOLVER__TIMEOUT_MS = "APIFUSE__RESOLVER__TIMEOUT_MS";
|
|
5
|
+
export declare const APIFUSE__CDP_POOL__URL = "APIFUSE__CDP_POOL__URL";
|
|
6
|
+
export declare const DEFAULT_RESOLVER_TIMEOUT_MS = 180000;
|
|
@@ -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,12 @@
|
|
|
1
|
+
import { ProviderError } from "../errors.js";
|
|
2
|
+
export const RESOLVER_INSTRUMENTATION_METADATA = Symbol.for("@apifuse/provider-sdk/runtime/resolver-instrumentation-metadata");
|
|
3
|
+
export function createUnsupportedResolverClient(reason) {
|
|
4
|
+
return {
|
|
5
|
+
async solve() {
|
|
6
|
+
throw new ProviderError(reason ?? "Resolver runtime is not configured", {
|
|
7
|
+
code: "RESOLVER_UNAVAILABLE",
|
|
8
|
+
fix: "Declare resolver on the provider definition and configure vendor credentials.",
|
|
9
|
+
});
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChallengeSolution, ProviderChallenge } from "../../types.js";
|
|
2
|
+
import type { TraceRecorder } from "../trace.js";
|
|
2
3
|
import { type ResolverIdentity, type ResolverVendorAdapter } from "./types.js";
|
|
3
4
|
type Delay = (ms: number, signal: AbortSignal) => Promise<void>;
|
|
4
5
|
export interface TwoCaptchaResolverVendorOptions {
|
|
@@ -15,7 +16,7 @@ export interface TwoCaptchaResolverVendorOptions {
|
|
|
15
16
|
}
|
|
16
17
|
export interface TwoCaptchaResolverVendorAdapter extends ResolverVendorAdapter {
|
|
17
18
|
readonly id: "2captcha";
|
|
18
|
-
solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal): Promise<Extract<ChallengeSolution, {
|
|
19
|
+
solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder): Promise<Extract<ChallengeSolution, {
|
|
19
20
|
readonly form: "token";
|
|
20
21
|
}>>;
|
|
21
22
|
}
|
|
@@ -147,6 +147,24 @@ function tokenFrom(payload) {
|
|
|
147
147
|
function endpoint(baseUrl, path) {
|
|
148
148
|
return `${baseUrl.replace(/\/+$/u, "")}/${path}`;
|
|
149
149
|
}
|
|
150
|
+
function spanErrorAttributes(error, phase) {
|
|
151
|
+
if (error instanceof ResolverVendorUnavailableError) {
|
|
152
|
+
return {
|
|
153
|
+
unavailability_reason: error.reason,
|
|
154
|
+
transport_phase: error.phase,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// The solve budget raises TwoCaptchaSolveTimeoutError inside the spanned closure and
|
|
158
|
+
// the outer catch converts it only after the span has been finalized, so the span has
|
|
159
|
+
// to recognize it here or slow solves lose exactly the attribution these spans add.
|
|
160
|
+
if (error instanceof TwoCaptchaSolveTimeoutError) {
|
|
161
|
+
return {
|
|
162
|
+
unavailability_reason: "timeout",
|
|
163
|
+
transport_phase: phase,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
150
168
|
export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
151
169
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
152
170
|
const baseUrl = options.baseUrl ?? DEFAULT_TWOCAPTCHA_BASE_URL;
|
|
@@ -159,7 +177,7 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
|
159
177
|
supports(kind) {
|
|
160
178
|
return resolverVendorSupports(TWOCAPTCHA_VENDOR_ID, kind);
|
|
161
179
|
},
|
|
162
|
-
async solve(challenge, identity, callerSignal) {
|
|
180
|
+
async solve(challenge, identity, callerSignal, traceRecorder) {
|
|
163
181
|
const apiKey = options.apiKey?.trim();
|
|
164
182
|
if (!apiKey) {
|
|
165
183
|
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "missing_credentials", {
|
|
@@ -192,50 +210,69 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
|
192
210
|
const startedAt = now();
|
|
193
211
|
let phase = "create_task";
|
|
194
212
|
try {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
phase = "poll_result";
|
|
211
|
-
while (true) {
|
|
212
|
-
callerSignal.throwIfAborted();
|
|
213
|
-
const remainingMs = timeoutMs - (now() - startedAt);
|
|
214
|
-
if (remainingMs <= 0)
|
|
215
|
-
throw new TwoCaptchaSolveTimeoutError();
|
|
216
|
-
await delay(Math.min(pollIntervalMs, remainingMs), solveController.signal);
|
|
217
|
-
callerSignal.throwIfAborted();
|
|
218
|
-
if (now() - startedAt >= timeoutMs)
|
|
219
|
-
throw new TwoCaptchaSolveTimeoutError();
|
|
220
|
-
const pollResult = await postJson(fetchImpl, endpoint(baseUrl, "getTaskResult"), { clientKey: apiKey, taskId }, solveController.signal, phase);
|
|
221
|
-
if (!pollResult.ok || pollResult.payload.errorId !== 0) {
|
|
222
|
-
throw unavailableForPayload(pollResult.payload, phase);
|
|
213
|
+
const createTask = async () => {
|
|
214
|
+
const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), {
|
|
215
|
+
clientKey: apiKey,
|
|
216
|
+
task: {
|
|
217
|
+
type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
|
|
218
|
+
websiteURL: challenge.pageUrl,
|
|
219
|
+
websiteKey: challenge.siteKey,
|
|
220
|
+
isInvisible: false,
|
|
221
|
+
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
222
|
+
...(proxy ?? {}),
|
|
223
|
+
},
|
|
224
|
+
}, solveController.signal, phase);
|
|
225
|
+
const taskId = taskIdFrom(createResult.payload);
|
|
226
|
+
if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
|
|
227
|
+
throw unavailableForPayload(createResult.payload, phase);
|
|
223
228
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
229
|
+
return taskId;
|
|
230
|
+
};
|
|
231
|
+
const taskId = traceRecorder
|
|
232
|
+
? await traceRecorder.runSpan("resolver.vendor.create_task", createTask, {
|
|
233
|
+
attributes: {
|
|
234
|
+
vendor: TWOCAPTCHA_VENDOR_ID,
|
|
235
|
+
challenge_kind: challenge.kind,
|
|
236
|
+
},
|
|
237
|
+
onError: (error) => spanErrorAttributes(error, "create_task"),
|
|
238
|
+
})
|
|
239
|
+
: await createTask();
|
|
240
|
+
phase = "poll_result";
|
|
241
|
+
const pollResult = async () => {
|
|
242
|
+
while (true) {
|
|
243
|
+
callerSignal.throwIfAborted();
|
|
244
|
+
const remainingMs = timeoutMs - (now() - startedAt);
|
|
245
|
+
if (remainingMs <= 0)
|
|
246
|
+
throw new TwoCaptchaSolveTimeoutError();
|
|
247
|
+
await delay(Math.min(pollIntervalMs, remainingMs), solveController.signal);
|
|
248
|
+
callerSignal.throwIfAborted();
|
|
249
|
+
if (now() - startedAt >= timeoutMs)
|
|
250
|
+
throw new TwoCaptchaSolveTimeoutError();
|
|
251
|
+
const pollResult = await postJson(fetchImpl, endpoint(baseUrl, "getTaskResult"), { clientKey: apiKey, taskId }, solveController.signal, phase);
|
|
252
|
+
if (!pollResult.ok || pollResult.payload.errorId !== 0) {
|
|
253
|
+
throw unavailableForPayload(pollResult.payload, phase);
|
|
254
|
+
}
|
|
255
|
+
if (pollResult.payload.status === "processing")
|
|
256
|
+
continue;
|
|
257
|
+
if (pollResult.payload.status !== "ready") {
|
|
258
|
+
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", { phase });
|
|
259
|
+
}
|
|
260
|
+
const token = tokenFrom(pollResult.payload);
|
|
261
|
+
if (!token?.trim()) {
|
|
262
|
+
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", { phase });
|
|
263
|
+
}
|
|
264
|
+
return { form: "token", token };
|
|
236
265
|
}
|
|
237
|
-
|
|
238
|
-
|
|
266
|
+
};
|
|
267
|
+
return traceRecorder
|
|
268
|
+
? await traceRecorder.runSpan("resolver.vendor.poll_result", pollResult, {
|
|
269
|
+
attributes: {
|
|
270
|
+
vendor: TWOCAPTCHA_VENDOR_ID,
|
|
271
|
+
challenge_kind: challenge.kind,
|
|
272
|
+
},
|
|
273
|
+
onError: (error) => spanErrorAttributes(error, "poll_result"),
|
|
274
|
+
})
|
|
275
|
+
: await pollResult();
|
|
239
276
|
}
|
|
240
277
|
catch (error) {
|
|
241
278
|
if (callerSignal.aborted)
|
|
@@ -55,7 +55,7 @@ export interface ResolverVendorAdapter {
|
|
|
55
55
|
getIssuingIdentity?(solution: ChallengeSolution, requestedIdentity: ResolverIdentity | undefined, challenge: ProviderChallenge): ResolverIssuingIdentity | undefined;
|
|
56
56
|
solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder, transport?: ResolverVendorTransport): Promise<ChallengeSolution>;
|
|
57
57
|
}
|
|
58
|
-
export type ResolverVendorUnavailableReason = "missing_credentials" | "missing_transport" | "allocation_exhausted" | "transport_failure" | "timeout" | "not_implemented";
|
|
58
|
+
export type ResolverVendorUnavailableReason = "missing_credentials" | "missing_proxy_identity" | "missing_transport" | "allocation_exhausted" | "transport_failure" | "timeout" | "not_implemented";
|
|
59
59
|
export type ResolverChallengeVerdictReason = "human_puzzle";
|
|
60
60
|
type ResolverErrorOptions = {
|
|
61
61
|
/** Raw cause; adapters must not place bodies, cookies, headers, credentials, or proxy URLs here. */
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import type { ChallengeSolution, ProviderCache, ProviderChallenge, ProviderChallengeKind, ProviderResolverConfig, ProviderResolverVendor, ResolverContext } from "../types.js";
|
|
1
|
+
import type { ChallengeSolution, ProviderCache, ProviderChallenge, ProviderChallengeKind, ProviderProxyMode, ProviderResolverConfig, ProviderResolverVendor, ResolverContext } from "../types.js";
|
|
2
2
|
import { type ResolverIdentity, type ResolverVendorAdapter, type ResolverVendorTransport } from "./resolver-vendors/types.js";
|
|
3
3
|
import type { TraceRecorder } from "./trace.js";
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export declare const APIFUSE__RESOLVER__CAPMONSTER__API_KEY = "APIFUSE__RESOLVER__CAPMONSTER__API_KEY";
|
|
7
|
-
export declare const APIFUSE__RESOLVER__TIMEOUT_MS = "APIFUSE__RESOLVER__TIMEOUT_MS";
|
|
8
|
-
export declare const APIFUSE__CDP_POOL__URL = "APIFUSE__CDP_POOL__URL";
|
|
9
|
-
export declare const DEFAULT_RESOLVER_TIMEOUT_MS = 180000;
|
|
4
|
+
export { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
|
|
5
|
+
export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, } from "./resolver-config.js";
|
|
10
6
|
type EnvLike = Record<string, string | undefined>;
|
|
11
7
|
type ResolverChainClient = ResolverContext & {
|
|
12
8
|
solve(challenge: ProviderChallenge, signal?: AbortSignal, traceRecorder?: TraceRecorder): Promise<ChallengeSolution>;
|
|
@@ -14,6 +10,8 @@ type ResolverChainClient = ResolverContext & {
|
|
|
14
10
|
export interface ResolverRuntimeOptions {
|
|
15
11
|
readonly allowedHosts?: readonly string[];
|
|
16
12
|
readonly cache?: ProviderCache;
|
|
13
|
+
/** Provider-declared proxy intent. The SDK never accepts a caller-built identity. */
|
|
14
|
+
readonly proxyMode?: ProviderProxyMode;
|
|
17
15
|
/** Server-owned context/proxy scope used only for identity-bound cache entries. */
|
|
18
16
|
readonly identityScope?: string;
|
|
19
17
|
/** SDK-owned transport already bound to the resolved proxy lease and client profile. */
|
|
@@ -25,7 +23,6 @@ export interface ResolverRuntimeOptions {
|
|
|
25
23
|
readonly identityScope?: string;
|
|
26
24
|
}) => ResolverVendorTransport;
|
|
27
25
|
}
|
|
28
|
-
export declare const RESOLVER_INSTRUMENTATION_METADATA: unique symbol;
|
|
29
26
|
export type ResolverInstrumentationMetadata = {
|
|
30
27
|
readonly target: ResolverContext;
|
|
31
28
|
readonly traceRecorder: TraceRecorder;
|
|
@@ -41,14 +38,13 @@ export declare function createResolverClient(options: {
|
|
|
41
38
|
readonly unavailableReason?: string;
|
|
42
39
|
readonly cache?: ProviderCache;
|
|
43
40
|
readonly identity?: ResolverIdentity;
|
|
41
|
+
readonly proxyMode?: ProviderProxyMode;
|
|
44
42
|
readonly transport?: ResolverVendorTransport;
|
|
45
43
|
readonly createTransport?: ResolverRuntimeOptions["createTransport"];
|
|
46
44
|
readonly clientProfile?: string;
|
|
47
45
|
readonly allowedHosts?: readonly string[];
|
|
48
46
|
}): ResolverChainClient;
|
|
49
|
-
export declare function createUnsupportedResolverClient(reason?: string): ResolverContext;
|
|
50
47
|
export declare function bindResolverSignal(resolver: ResolverContext, defaultSignal: AbortSignal | undefined): ResolverContext;
|
|
51
48
|
export declare function createResolverClientFromEnv(config: ProviderResolverConfig | undefined, env?: EnvLike, options?: ResolverRuntimeOptions): ResolverContext;
|
|
52
49
|
/** Internal test seam; deliberately not re-exported from the package root. */
|
|
53
50
|
export declare function createResolverClientFromEnvForTests(config: ProviderResolverConfig | undefined, env: EnvLike, options: ResolverRuntimeOptions, adapterFactories: Partial<Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>>): ResolverContext;
|
|
54
|
-
export {};
|
package/dist/runtime/resolver.js
CHANGED
|
@@ -5,12 +5,10 @@ import { createBrowserResolverVendorAdapter } from "./resolver-vendors/browser.j
|
|
|
5
5
|
import { assertResolverHostAllowed } from "./resolver-vendors/hosts.js";
|
|
6
6
|
import { createTwoCaptchaResolverVendorAdapter } from "./resolver-vendors/twocaptcha.js";
|
|
7
7
|
import { RESOLVER_VENDOR_CAPABILITIES, ResolverVendorUnavailableError, resolverVendorSupports, } from "./resolver-vendors/types.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
export
|
|
12
|
-
export const APIFUSE__CDP_POOL__URL = "APIFUSE__CDP_POOL__URL";
|
|
13
|
-
export const DEFAULT_RESOLVER_TIMEOUT_MS = 180_000;
|
|
8
|
+
import { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
|
|
9
|
+
import { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, } from "./resolver-config.js";
|
|
10
|
+
export { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
|
|
11
|
+
export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, } from "./resolver-config.js";
|
|
14
12
|
const RESOLVER_SOLUTION_CACHE_NAMESPACE = "resolver-solution";
|
|
15
13
|
const RESOLVER_SOLUTION_INDEX_CACHE_NAMESPACE = "resolver-solution-index";
|
|
16
14
|
const MIN_RESOLVER_CACHE_TTL_MS = 1_000;
|
|
@@ -52,7 +50,6 @@ const SAFE_CAUSE_MESSAGE_WORDS = new Set([
|
|
|
52
50
|
"while",
|
|
53
51
|
"writing",
|
|
54
52
|
]);
|
|
55
|
-
export const RESOLVER_INSTRUMENTATION_METADATA = Symbol.for("@apifuse/provider-sdk/runtime/resolver-instrumentation-metadata");
|
|
56
53
|
const resolverAdapterRegistry = {
|
|
57
54
|
"2captcha"(configuration, timeoutMs, allowedHosts) {
|
|
58
55
|
return createTwoCaptchaResolverVendorAdapter({
|
|
@@ -193,7 +190,9 @@ function sanitizeCauseMessage(message) {
|
|
|
193
190
|
if (token === "[REDACTED]" || /^[a-z][a-z\d+.-]*:\/\/[^\s]+$/i.test(token))
|
|
194
191
|
return token;
|
|
195
192
|
const word = token.replace(/^[^a-z\d]+|[^a-z\d]+$/gi, "");
|
|
196
|
-
return word.length > 0 &&
|
|
193
|
+
return word.length > 0 &&
|
|
194
|
+
word.length <= 32 &&
|
|
195
|
+
SAFE_CAUSE_MESSAGE_WORDS.has(word.toLowerCase())
|
|
197
196
|
? token
|
|
198
197
|
: "[REDACTED]";
|
|
199
198
|
});
|
|
@@ -465,6 +464,16 @@ function createResolverChainClient(options) {
|
|
|
465
464
|
if (supportingEntries.length === 0)
|
|
466
465
|
throwUnsupportedKind(challenge.kind);
|
|
467
466
|
signal.throwIfAborted();
|
|
467
|
+
// A required proxy policy is checked before the cache. Solutions minted under a
|
|
468
|
+
// previous release are shared and long-lived, so consulting the cache first would
|
|
469
|
+
// keep reporting success without a proxy identity until every old entry expired.
|
|
470
|
+
const requiredProxyIdentityMissing = options.proxyMode === "required" && options.identity === undefined;
|
|
471
|
+
if (requiredProxyIdentityMissing) {
|
|
472
|
+
throwExhausted(supportingEntries.map((entry) => ({
|
|
473
|
+
vendor: entry.id,
|
|
474
|
+
reason: "missing_proxy_identity",
|
|
475
|
+
})));
|
|
476
|
+
}
|
|
468
477
|
if (options.cache && resolverChallengeIsCacheable(challenge)) {
|
|
469
478
|
const cached = await findCachedSolution(options.cache, challenge, options.identity, options.identityScope);
|
|
470
479
|
if (cached)
|
|
@@ -544,6 +553,7 @@ export function createResolverClient(options) {
|
|
|
544
553
|
unavailableReason: options.unavailableReason,
|
|
545
554
|
cache: options.cache,
|
|
546
555
|
identity: options.identity,
|
|
556
|
+
proxyMode: options.proxyMode,
|
|
547
557
|
transport: options.transport,
|
|
548
558
|
createTransport: options.createTransport,
|
|
549
559
|
clientProfile: options.clientProfile,
|
|
@@ -570,16 +580,6 @@ function resolveVendorAvailability(vendor, env) {
|
|
|
570
580
|
? { vendor, available: true, configuration }
|
|
571
581
|
: { vendor, available: false, reason: "missing_credentials" };
|
|
572
582
|
}
|
|
573
|
-
export function createUnsupportedResolverClient(reason) {
|
|
574
|
-
return {
|
|
575
|
-
async solve() {
|
|
576
|
-
throw new ProviderError(reason ?? "Resolver runtime is not configured", {
|
|
577
|
-
code: "RESOLVER_UNAVAILABLE",
|
|
578
|
-
fix: "Declare resolver on the provider definition and configure vendor credentials.",
|
|
579
|
-
});
|
|
580
|
-
},
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
583
|
export function bindResolverSignal(resolver, defaultSignal) {
|
|
584
584
|
if (!defaultSignal)
|
|
585
585
|
return resolver;
|
|
@@ -620,6 +620,7 @@ function createResolverClientFromEnvInternal(config, env, options, adapterFactor
|
|
|
620
620
|
};
|
|
621
621
|
}),
|
|
622
622
|
cache: options.cache,
|
|
623
|
+
proxyMode: options.proxyMode,
|
|
623
624
|
identityScope: options.identityScope,
|
|
624
625
|
transport: options.transport,
|
|
625
626
|
createTransport: options.createTransport,
|