@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
@@ -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 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";
8
- export declare class NativeNetworkError extends TransportError {
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 class NativeNetworkError extends TransportError {
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],
@@ -1,4 +1,4 @@
1
- import { Redis } from "ioredis";
1
+ import type { Redis } from "ioredis";
2
2
  export type ProviderRedisClient = Redis;
3
3
  export type ProviderRedisClientOptions = {
4
4
  readonly redisUrl: string;
@@ -1,6 +1,8 @@
1
- import { Redis } from "ioredis";
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
2
3
  export function createProviderRedisClient(options) {
3
- const redis = new Redis(options.redisUrl, {
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,3 @@
1
+ import type { ResolverContext } from "../types.js";
2
+ export declare const RESOLVER_INSTRUMENTATION_METADATA: unique symbol;
3
+ export declare function createUnsupportedResolverClient(reason?: string): ResolverContext;
@@ -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 createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), {
196
- clientKey: apiKey,
197
- task: {
198
- type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
199
- websiteURL: challenge.pageUrl,
200
- websiteKey: challenge.siteKey,
201
- isInvisible: false,
202
- ...(identity ? { userAgent: identity.userAgent } : {}),
203
- ...(proxy ?? {}),
204
- },
205
- }, solveController.signal, phase);
206
- const taskId = taskIdFrom(createResult.payload);
207
- if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
208
- throw unavailableForPayload(createResult.payload, phase);
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
- if (pollResult.payload.status === "processing")
225
- continue;
226
- if (pollResult.payload.status !== "ready") {
227
- throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
228
- phase,
229
- });
230
- }
231
- const token = tokenFrom(pollResult.payload);
232
- if (!token?.trim()) {
233
- throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
234
- phase,
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
- return { form: "token", token };
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 declare const APIFUSE__RESOLVER__2CAPTCHA__API_KEY = "APIFUSE__RESOLVER__2CAPTCHA__API_KEY";
5
- export declare const APIFUSE__RESOLVER__CAPSOLVER__API_KEY = "APIFUSE__RESOLVER__CAPSOLVER__API_KEY";
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 {};
@@ -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
- export const APIFUSE__RESOLVER__2CAPTCHA__API_KEY = "APIFUSE__RESOLVER__2CAPTCHA__API_KEY";
9
- export const APIFUSE__RESOLVER__CAPSOLVER__API_KEY = "APIFUSE__RESOLVER__CAPSOLVER__API_KEY";
10
- export const APIFUSE__RESOLVER__CAPMONSTER__API_KEY = "APIFUSE__RESOLVER__CAPMONSTER__API_KEY";
11
- export const APIFUSE__RESOLVER__TIMEOUT_MS = "APIFUSE__RESOLVER__TIMEOUT_MS";
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 && word.length <= 32 && SAFE_CAUSE_MESSAGE_WORDS.has(word.toLowerCase())
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,