@apifuse/provider-sdk 2.2.0-beta.57 → 2.2.0-beta.59

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 (107) hide show
  1. package/AUTHORING.md +9 -0
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +22 -5
  4. package/SUBMISSION.md +2 -1
  5. package/bin/apifuse-pack-types.ts +49 -0
  6. package/bin/apifuse-record.ts +6 -0
  7. package/dist/cli/templates/provider/README.md.tpl +19 -5
  8. package/dist/config/loader.d.ts +7 -0
  9. package/dist/config/loader.js +30 -6
  10. package/dist/declaration-validation.d.ts +2 -1
  11. package/dist/declaration-validation.js +39 -0
  12. package/dist/define.d.ts +5 -4
  13. package/dist/define.js +25 -1
  14. package/dist/engine.d.ts +16 -0
  15. package/dist/engine.js +58 -1
  16. package/dist/error-resolution.js +13 -0
  17. package/dist/index.d.ts +6 -4
  18. package/dist/index.js +4 -2
  19. package/dist/lint.js +3 -3
  20. package/dist/provider.d.ts +3 -3
  21. package/dist/provider.js +2 -2
  22. package/dist/runtime/bounded-expiring-map.d.ts +21 -0
  23. package/dist/runtime/bounded-expiring-map.js +64 -0
  24. package/dist/runtime/cache.d.ts +2 -0
  25. package/dist/runtime/cache.js +8 -1
  26. package/dist/runtime/egress-lease.d.ts +63 -0
  27. package/dist/runtime/egress-lease.js +233 -0
  28. package/dist/runtime/http.js +1 -0
  29. package/dist/runtime/proxy-retry-policy.d.ts +4 -1
  30. package/dist/runtime/proxy-retry-policy.js +11 -2
  31. package/dist/runtime/request-telemetry.d.ts +7 -9
  32. package/dist/runtime/resolver-config.d.ts +7 -0
  33. package/dist/runtime/resolver-config.js +8 -0
  34. package/dist/runtime/resolver-public.d.ts +3 -1
  35. package/dist/runtime/resolver-public.js +1 -1
  36. package/dist/runtime/resolver-shared.d.ts +9 -0
  37. package/dist/runtime/resolver-shared.js +57 -0
  38. package/dist/runtime/resolver-telemetry.d.ts +134 -0
  39. package/dist/runtime/resolver-telemetry.js +204 -0
  40. package/dist/runtime/resolver-usage.d.ts +24 -0
  41. package/dist/runtime/resolver-usage.js +60 -0
  42. package/dist/runtime/resolver-vendors/bindings.d.ts +5 -0
  43. package/dist/runtime/resolver-vendors/bindings.js +5 -0
  44. package/dist/runtime/resolver-vendors/capsolver.d.ts +2 -2
  45. package/dist/runtime/resolver-vendors/capsolver.js +21 -9
  46. package/dist/runtime/resolver-vendors/hypersolutions.d.ts +27 -0
  47. package/dist/runtime/resolver-vendors/hypersolutions.js +369 -0
  48. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -2
  49. package/dist/runtime/resolver-vendors/twocaptcha.js +26 -10
  50. package/dist/runtime/resolver-vendors/types.d.ts +35 -5
  51. package/dist/runtime/resolver-vendors/types.js +20 -10
  52. package/dist/runtime/resolver.d.ts +5 -1
  53. package/dist/runtime/resolver.js +397 -109
  54. package/dist/runtime/stealth-akamai-sbsd.d.ts +54 -0
  55. package/dist/runtime/stealth-akamai-sbsd.js +142 -0
  56. package/dist/runtime/stealth.js +547 -53
  57. package/dist/schema.d.ts +9 -0
  58. package/dist/schema.js +16 -0
  59. package/dist/server/index.d.ts +2 -0
  60. package/dist/server/index.js +1 -0
  61. package/dist/server/self-test.d.ts +4 -0
  62. package/dist/server/self-test.js +10 -1
  63. package/dist/server/serve-implementation.d.ts +7 -1
  64. package/dist/server/serve-implementation.js +181 -53
  65. package/dist/server/types.d.ts +6 -0
  66. package/dist/server/types.js +14 -0
  67. package/dist/stealth/profiles.d.ts +7 -1
  68. package/dist/stealth/profiles.js +9 -0
  69. package/dist/testing/run.js +6 -0
  70. package/dist/types.d.ts +115 -12
  71. package/package.json +1 -1
  72. package/src/cli/templates/provider/README.md.tpl +19 -5
  73. package/src/config/loader.ts +34 -6
  74. package/src/declaration-validation.ts +47 -1
  75. package/src/define.ts +46 -3
  76. package/src/engine.ts +76 -1
  77. package/src/error-resolution.ts +13 -0
  78. package/src/index.ts +32 -1
  79. package/src/lint.ts +3 -3
  80. package/src/provider.ts +9 -0
  81. package/src/runtime/bounded-expiring-map.ts +67 -0
  82. package/src/runtime/cache.ts +10 -1
  83. package/src/runtime/egress-lease.ts +336 -0
  84. package/src/runtime/http.ts +1 -0
  85. package/src/runtime/proxy-retry-policy.ts +12 -1
  86. package/src/runtime/request-telemetry.ts +5 -7
  87. package/src/runtime/resolver-config.ts +12 -0
  88. package/src/runtime/resolver-public.ts +21 -0
  89. package/src/runtime/resolver-shared.ts +75 -0
  90. package/src/runtime/resolver-telemetry.ts +362 -0
  91. package/src/runtime/resolver-usage.ts +96 -0
  92. package/src/runtime/resolver-vendors/bindings.ts +5 -0
  93. package/src/runtime/resolver-vendors/capsolver.ts +22 -3
  94. package/src/runtime/resolver-vendors/hypersolutions.ts +480 -0
  95. package/src/runtime/resolver-vendors/twocaptcha.ts +26 -12
  96. package/src/runtime/resolver-vendors/types.ts +60 -12
  97. package/src/runtime/resolver.ts +465 -124
  98. package/src/runtime/stealth-akamai-sbsd.ts +207 -0
  99. package/src/runtime/stealth.ts +727 -95
  100. package/src/schema.ts +17 -0
  101. package/src/server/index.ts +21 -0
  102. package/src/server/self-test.ts +13 -0
  103. package/src/server/serve-implementation.ts +268 -48
  104. package/src/server/types.ts +14 -0
  105. package/src/stealth/profiles.ts +10 -0
  106. package/src/testing/run.ts +6 -0
  107. package/src/types.ts +130 -22
@@ -1,6 +1,6 @@
1
1
  import type { ChallengeSolution, ProviderChallenge } from "../../types.js";
2
2
  import type { TraceRecorder } from "../trace.js";
3
- import { type ResolverIdentity, type ResolverVendorAdapter } from "./types.js";
3
+ import { type ResolverIdentity, type ResolverPaidUsageContext, type ResolverVendorAdapter, type ResolverVendorTransport } from "./types.js";
4
4
  type Delay = (ms: number, signal: AbortSignal) => Promise<void>;
5
5
  export interface CapsolverResolverVendorOptions {
6
6
  readonly apiKey?: string;
@@ -16,7 +16,7 @@ export interface CapsolverResolverVendorOptions {
16
16
  }
17
17
  export interface CapsolverResolverVendorAdapter extends ResolverVendorAdapter {
18
18
  readonly id: "capsolver";
19
- solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder): Promise<ChallengeSolution>;
19
+ solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder, transport?: ResolverVendorTransport, usage?: ResolverPaidUsageContext): Promise<ChallengeSolution>;
20
20
  }
21
21
  export declare function createCapsolverResolverVendorAdapter(options: CapsolverResolverVendorOptions): CapsolverResolverVendorAdapter;
22
22
  export {};
@@ -1,8 +1,9 @@
1
1
  import { getStealthProfile } from "../../stealth/profiles.js";
2
2
  import { redactSensitiveText } from "../request-options.js";
3
3
  import { DEFAULT_STEALTH_PROFILE } from "../stealth.js";
4
+ import { recordPaidResolverCreate } from "../resolver-usage.js";
4
5
  import { assertResolverHostAllowed } from "./hosts.js";
5
- import { ResolverChallengeVerdictError, ResolverVendorUnavailableError, resolverVendorSupports, } from "./types.js";
6
+ import { ResolverChallengeVerdictError, ResolverVendorUnavailableError, recordResolverVendorPoll, resolverVendorSupports, } from "./types.js";
6
7
  const CAPSOLVER_VENDOR_ID = "capsolver";
7
8
  const DEFAULT_CAPSOLVER_BASE_URL = "https://api.capsolver.com";
8
9
  const DEFAULT_POLL_INTERVAL_MS = 2_000;
@@ -304,7 +305,7 @@ export function createCapsolverResolverVendorAdapter(options) {
304
305
  supports(kind) {
305
306
  return resolverVendorSupports(CAPSOLVER_VENDOR_ID, kind);
306
307
  },
307
- async solve(challenge, identity, callerSignal, traceRecorder) {
308
+ async solve(challenge, identity, callerSignal, traceRecorder, _transport, usage) {
308
309
  const apiKey = options.apiKey?.trim();
309
310
  if (!apiKey) {
310
311
  throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "missing_credentials", {
@@ -416,12 +417,22 @@ export function createCapsolverResolverVendorAdapter(options) {
416
417
  : (() => {
417
418
  throw new TypeError(`Capsolver resolver does not support ${challenge.kind}`);
418
419
  })();
419
- const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), { clientKey: apiKey, task }, solveController.signal, phase, sensitiveValues, parseCreateTaskResponse);
420
- const taskId = createResult.payload.taskId;
421
- if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
422
- throw unavailableForPayload(createResult.payload, phase, sensitiveValues);
423
- }
424
- return taskId;
420
+ return await recordPaidResolverCreate({
421
+ traceRecorder,
422
+ vendor: CAPSOLVER_VENDOR_ID,
423
+ kind: challenge.kind,
424
+ endpoint: "capsolver:create_task",
425
+ signal: solveController.signal,
426
+ usage,
427
+ create: async () => {
428
+ const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), { clientKey: apiKey, task }, solveController.signal, phase, sensitiveValues, parseCreateTaskResponse);
429
+ const taskId = createResult.payload.taskId;
430
+ if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
431
+ throw unavailableForPayload(createResult.payload, phase, sensitiveValues);
432
+ }
433
+ return taskId;
434
+ },
435
+ });
425
436
  };
426
437
  const taskId = traceRecorder
427
438
  ? await traceRecorder.runSpan("resolver.vendor.create_task", createTask, {
@@ -443,6 +454,7 @@ export function createCapsolverResolverVendorAdapter(options) {
443
454
  callerSignal.throwIfAborted();
444
455
  if (now() - startedAt >= timeoutMs)
445
456
  throw new CapsolverSolveTimeoutError();
457
+ recordResolverVendorPoll(traceRecorder);
446
458
  const result = await postJson(fetchImpl, endpoint(baseUrl, "getTaskResult"), { clientKey: apiKey, taskId }, solveController.signal, phase, sensitiveValues, parsePollResultResponse);
447
459
  if (!result.ok || result.payload.errorId !== 0) {
448
460
  throw unavailableForPayload(result.payload, phase, sensitiveValues);
@@ -460,7 +472,7 @@ export function createCapsolverResolverVendorAdapter(options) {
460
472
  }
461
473
  const solutionValue = challenge.kind === "aws_waf"
462
474
  ? result.payload.solution?.cookie
463
- : result.payload.solution?.token ?? result.payload.solution?.gRecaptchaResponse;
475
+ : (result.payload.solution?.token ?? result.payload.solution?.gRecaptchaResponse);
464
476
  if (challenge.kind === "cloudflare_interstitial") {
465
477
  const cookies = result.payload.solution?.cookies;
466
478
  const clearance = cookies?.cf_clearance ?? solutionValue;
@@ -0,0 +1,27 @@
1
+ import type { ChallengeSolution, ProviderChallenge } from "../../types.js";
2
+ import type { TraceRecorder } from "../trace.js";
3
+ import { type ResolverIdentity, type ResolverPaidUsageContext, type ResolverVendorAdapter, type ResolverVendorTransport } from "./types.js";
4
+ /**
5
+ * Only the observed-IP reflector rides the bound transport: Hyper must see the
6
+ * egress address the upstream will see. The payload-generation POST carries no
7
+ * identity and goes direct, as in the measured zozotown source.
8
+ */
9
+ declare const HYPER_TRANSPORT_HOSTS: readonly ["ip.hypersolutions.co"];
10
+ export interface HypersolutionsResolverVendorOptions {
11
+ readonly apiKey?: string;
12
+ readonly timeoutMs?: number;
13
+ readonly allowedHosts: readonly string[];
14
+ /** Direct egress used only for the Hyper payload-generation POST. */
15
+ readonly fetchImpl?: typeof fetch;
16
+ }
17
+ export type AkamaiSbsdChallengeSolution = Extract<ChallengeSolution, {
18
+ readonly form: "cookie_state";
19
+ }>;
20
+ export interface HypersolutionsResolverVendorAdapter extends ResolverVendorAdapter {
21
+ readonly id: "hypersolutions";
22
+ readonly requiresTransport: true;
23
+ readonly transportAllowedHosts: typeof HYPER_TRANSPORT_HOSTS;
24
+ solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder, transport?: ResolverVendorTransport, usage?: ResolverPaidUsageContext): Promise<AkamaiSbsdChallengeSolution>;
25
+ }
26
+ export declare function createHypersolutionsResolverVendorAdapter(options: HypersolutionsResolverVendorOptions): HypersolutionsResolverVendorAdapter;
27
+ export {};
@@ -0,0 +1,369 @@
1
+ import { isIP } from "node:net";
2
+ import { recordPaidResolverCreate } from "../resolver-usage.js";
3
+ import { assertResolverHostAllowed } from "./hosts.js";
4
+ import { ResolverChallengeVerdictError, ResolverVendorUnavailableError, } from "./types.js";
5
+ const HYPERSOLUTIONS_VENDOR_ID = "hypersolutions";
6
+ const HYPER_SBSD_URL = "https://akm.hypersolutions.co/sbsd";
7
+ const HYPER_IP_URL = "https://ip.hypersolutions.co/ip";
8
+ /**
9
+ * Only the observed-IP reflector rides the bound transport: Hyper must see the
10
+ * egress address the upstream will see. The payload-generation POST carries no
11
+ * identity and goes direct, as in the measured zozotown source.
12
+ */
13
+ const HYPER_TRANSPORT_HOSTS = ["ip.hypersolutions.co"];
14
+ const IP_RESPONSE_MAX_BYTES = 4_096;
15
+ /** Measured bound shared by the script, the Hyper envelope, and the payload submission. */
16
+ const BODY_MAX_BYTES = 1_000_000;
17
+ function headerValue(headers, name) {
18
+ const target = name.toLowerCase();
19
+ return Object.entries(headers)
20
+ .find(([header]) => header.toLowerCase() === target)?.[1]
21
+ ?.trim();
22
+ }
23
+ function declaredLengthExceeds(declared, maxBytes) {
24
+ return declared !== undefined && /^\d+$/u.test(declared) && BigInt(declared) > BigInt(maxBytes);
25
+ }
26
+ function transportFailure(phase, cause) {
27
+ return new ResolverVendorUnavailableError(HYPERSOLUTIONS_VENDOR_ID, "transport_failure", {
28
+ phase,
29
+ ...(cause === undefined ? {} : { cause }),
30
+ });
31
+ }
32
+ function assertBoundedBody(response, maxBytes, phase) {
33
+ if (declaredLengthExceeds(headerValue(response.headers, "content-length"), maxBytes) ||
34
+ new TextEncoder().encode(response.body).byteLength > maxBytes) {
35
+ throw transportFailure(phase);
36
+ }
37
+ }
38
+ function requireSuccess(status, phase) {
39
+ if (status >= 200 && status < 300)
40
+ return;
41
+ if (phase === "measure_ip" || phase === "generate_payload") {
42
+ // Hyper's own service: a rejected key is a credential fault, anything else is transport.
43
+ throw new ResolverVendorUnavailableError(HYPERSOLUTIONS_VENDOR_ID, status === 401 || status === 403 ? "missing_credentials" : "transport_failure", { phase });
44
+ }
45
+ // The upstream refused the script GET or the payload POST: that is the challenge verdict.
46
+ throw new ResolverChallengeVerdictError(HYPERSOLUTIONS_VENDOR_ID, "solve_failed", { phase });
47
+ }
48
+ function parseObservedIp(body) {
49
+ let value = body.trim();
50
+ try {
51
+ value = JSON.parse(body);
52
+ }
53
+ catch {
54
+ // Hyper's reflector has returned both JSON and plain text in measured clients.
55
+ }
56
+ if (value && typeof value === "object" && !Array.isArray(value) && "ip" in value) {
57
+ value = value.ip;
58
+ }
59
+ if (typeof value !== "string")
60
+ return undefined;
61
+ const candidate = value.trim();
62
+ return isIP(candidate) ? candidate : undefined;
63
+ }
64
+ function parsePayload(body) {
65
+ try {
66
+ const parsed = JSON.parse(body);
67
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !("payload" in parsed)) {
68
+ return undefined;
69
+ }
70
+ return typeof parsed.payload === "string" && parsed.payload.length > 0
71
+ ? parsed.payload
72
+ : undefined;
73
+ }
74
+ catch {
75
+ return undefined;
76
+ }
77
+ }
78
+ function scriptExchangeUrls(scriptUrl, laterChallengeToken) {
79
+ // `?v=&t=` is the hard SBSD variant and `?v=` is passive SBSD. A no-query
80
+ // Akamai script is a sensor-family artifact and stays outside this adapter.
81
+ const parsed = new URL(scriptUrl);
82
+ const uuid = parsed.searchParams.get("v")?.trim();
83
+ if (!uuid) {
84
+ throw new ResolverVendorUnavailableError(HYPERSOLUTIONS_VENDOR_ID, "missing_challenge_input", {
85
+ missingFields: ["scriptUrl"],
86
+ phase: "fetch_script",
87
+ });
88
+ }
89
+ const scriptToken = parsed.searchParams.get("t")?.trim();
90
+ const laterToken = laterChallengeToken?.trim();
91
+ if (scriptToken && laterToken) {
92
+ throw new ResolverVendorUnavailableError(HYPERSOLUTIONS_VENDOR_ID, "missing_challenge_input", {
93
+ missingFields: ["challengeToken"],
94
+ phase: "fetch_script",
95
+ });
96
+ }
97
+ // Clone the validated URL and only replace its query: reparsing `pathname`
98
+ // against `origin` would read a `//host/...` pathname as a network-path
99
+ // reference and swap the host that assertChallengeInput just admitted.
100
+ const fetchUrl = new URL(parsed);
101
+ fetchUrl.search = "";
102
+ fetchUrl.hash = "";
103
+ fetchUrl.searchParams.set("v", uuid);
104
+ if (scriptToken)
105
+ fetchUrl.searchParams.set("t", scriptToken);
106
+ const postUrl = new URL(fetchUrl);
107
+ postUrl.search = "";
108
+ const postToken = laterToken || scriptToken;
109
+ if (postToken)
110
+ postUrl.searchParams.set("t", postToken);
111
+ return {
112
+ fetchUrl: fetchUrl.toString(),
113
+ postUrl: postUrl.toString(),
114
+ uuid,
115
+ indices: postToken ? [0] : [0, 1],
116
+ };
117
+ }
118
+ function assertChallengeInput(challenge, allowedHosts) {
119
+ const missingFields = [
120
+ ...(challenge.pageUrl.trim() ? [] : ["pageUrl"]),
121
+ ...(challenge.scriptUrl.trim() ? [] : ["scriptUrl"]),
122
+ ...(challenge.stateCookieName === "sbsd_o" || challenge.stateCookieName === "bm_so"
123
+ ? []
124
+ : ["stateCookieName"]),
125
+ ...(challenge.challengeToken === undefined || challenge.challengeToken.trim()
126
+ ? []
127
+ : ["challengeToken"]),
128
+ ];
129
+ if (missingFields.length > 0) {
130
+ throw new ResolverVendorUnavailableError(HYPERSOLUTIONS_VENDOR_ID, "missing_challenge_input", {
131
+ missingFields,
132
+ });
133
+ }
134
+ assertResolverHostAllowed(challenge.pageUrl, allowedHosts);
135
+ assertResolverHostAllowed(challenge.scriptUrl, allowedHosts);
136
+ if (new URL(challenge.pageUrl).origin !== new URL(challenge.scriptUrl).origin) {
137
+ // The state cookie is read for pageUrl and posted to scriptUrl; they must share an origin.
138
+ throw new ResolverVendorUnavailableError(HYPERSOLUTIONS_VENDOR_ID, "missing_challenge_input", {
139
+ missingFields: ["scriptUrl"],
140
+ });
141
+ }
142
+ }
143
+ async function boundFetch(transport, url, init, phase) {
144
+ try {
145
+ return await transport.fetch(url, init);
146
+ }
147
+ catch (cause) {
148
+ if (cause instanceof ResolverVendorUnavailableError ||
149
+ cause instanceof ResolverChallengeVerdictError) {
150
+ throw cause;
151
+ }
152
+ // An abort is the caller's (or the adapter timeout's) verdict, not a transport fault.
153
+ if (init.signal.aborted)
154
+ throw init.signal.reason ?? cause;
155
+ throw transportFailure(phase, cause);
156
+ }
157
+ }
158
+ async function readBoundedText(response, maxBytes) {
159
+ if (declaredLengthExceeds(response.headers.get("content-length")?.trim(), maxBytes)) {
160
+ await response.body?.cancel();
161
+ return undefined;
162
+ }
163
+ if (!response.body)
164
+ return "";
165
+ const reader = response.body.getReader();
166
+ const decoder = new TextDecoder();
167
+ let received = 0;
168
+ let text = "";
169
+ for (;;) {
170
+ const { done, value } = await reader.read();
171
+ if (done)
172
+ break;
173
+ received += value.byteLength;
174
+ if (received > maxBytes) {
175
+ await reader.cancel();
176
+ return undefined;
177
+ }
178
+ text += decoder.decode(value, { stream: true });
179
+ }
180
+ return text + decoder.decode();
181
+ }
182
+ async function generatePayload(fetchImpl, apiKey, body, signal) {
183
+ try {
184
+ const response = await fetchImpl(HYPER_SBSD_URL, {
185
+ method: "POST",
186
+ headers: {
187
+ accept: "application/json",
188
+ "content-type": "application/json",
189
+ "x-api-key": apiKey,
190
+ },
191
+ body,
192
+ signal,
193
+ redirect: "error",
194
+ });
195
+ // A body that fails mid-read is as much a transport fault as a refused connection.
196
+ return { status: response.status, body: await readBoundedText(response, BODY_MAX_BYTES) };
197
+ }
198
+ catch (cause) {
199
+ if (signal.aborted)
200
+ throw signal.reason ?? cause;
201
+ throw transportFailure("generate_payload", cause);
202
+ }
203
+ }
204
+ export function createHypersolutionsResolverVendorAdapter(options) {
205
+ const apiKey = options.apiKey?.trim();
206
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
207
+ return {
208
+ id: HYPERSOLUTIONS_VENDOR_ID,
209
+ requiresTransport: true,
210
+ transportAllowedHosts: HYPER_TRANSPORT_HOSTS,
211
+ supports: (kind) => kind === "akamai_sbsd",
212
+ async solve(challenge, _identity, signal, traceRecorder, transport, usage) {
213
+ const timeoutController = new AbortController();
214
+ const operationSignal = options.timeoutMs
215
+ ? AbortSignal.any([signal, timeoutController.signal])
216
+ : signal;
217
+ const timeout = options.timeoutMs
218
+ ? setTimeout(
219
+ // A named reason lets the usage span record `timeout`, not a caller abort.
220
+ () => timeoutController.abort(new DOMException("Hyper solve timed out", "TimeoutError")), options.timeoutMs)
221
+ : undefined;
222
+ const runPhase = (phase, fn) => traceRecorder ? traceRecorder.runSpan(`resolver.vendor.${phase}`, fn) : fn();
223
+ try {
224
+ if (challenge.kind !== "akamai_sbsd") {
225
+ throw new ResolverVendorUnavailableError(HYPERSOLUTIONS_VENDOR_ID, "not_implemented");
226
+ }
227
+ if (!apiKey) {
228
+ throw new ResolverVendorUnavailableError(HYPERSOLUTIONS_VENDOR_ID, "missing_credentials");
229
+ }
230
+ if (!transport?.getCookie) {
231
+ throw new ResolverVendorUnavailableError(HYPERSOLUTIONS_VENDOR_ID, "missing_transport");
232
+ }
233
+ const sessionHeaders = transport.sessionHeaders;
234
+ const userAgent = sessionHeaders ? headerValue(sessionHeaders, "user-agent") : undefined;
235
+ const acceptLanguage = sessionHeaders
236
+ ? headerValue(sessionHeaders, "accept-language")
237
+ : undefined;
238
+ if (!sessionHeaders || !userAgent || !acceptLanguage) {
239
+ throw new ResolverVendorUnavailableError(HYPERSOLUTIONS_VENDOR_ID, "missing_client_profile");
240
+ }
241
+ assertChallengeInput(challenge, options.allowedHosts);
242
+ const exchange = scriptExchangeUrls(challenge.scriptUrl, challenge.challengeToken);
243
+ // Hyper documents /ip as an authenticated service request but does not
244
+ // publish an explicit exclusion from request quota. Meter conservatively.
245
+ const ip = await recordPaidResolverCreate({
246
+ traceRecorder,
247
+ vendor: HYPERSOLUTIONS_VENDOR_ID,
248
+ kind: challenge.kind,
249
+ endpoint: "hyper:ip",
250
+ signal: operationSignal,
251
+ usage,
252
+ create: () => runPhase("measure_ip", async () => {
253
+ const ipResponse = await boundFetch(transport, HYPER_IP_URL, {
254
+ method: "GET",
255
+ headers: {
256
+ accept: "application/json, text/plain;q=0.9",
257
+ "x-api-key": apiKey,
258
+ },
259
+ signal: operationSignal,
260
+ redirect: "manual",
261
+ maxBodyBytes: IP_RESPONSE_MAX_BYTES,
262
+ }, "measure_ip");
263
+ requireSuccess(ipResponse.status, "measure_ip");
264
+ assertBoundedBody(ipResponse, IP_RESPONSE_MAX_BYTES, "measure_ip");
265
+ const observedIp = parseObservedIp(ipResponse.body);
266
+ if (!observedIp)
267
+ throw transportFailure("measure_ip");
268
+ return observedIp;
269
+ }),
270
+ });
271
+ const scriptResponse = await runPhase("fetch_script", async () => {
272
+ const scriptResponse = await boundFetch(transport, exchange.fetchUrl, {
273
+ method: "GET",
274
+ headers: { ...sessionHeaders, Referer: challenge.pageUrl },
275
+ signal: operationSignal,
276
+ redirect: "manual",
277
+ maxBodyBytes: BODY_MAX_BYTES,
278
+ }, "fetch_script");
279
+ requireSuccess(scriptResponse.status, "fetch_script");
280
+ assertBoundedBody(scriptResponse, BODY_MAX_BYTES, "fetch_script");
281
+ if (!scriptResponse.body) {
282
+ throw new ResolverChallengeVerdictError(HYPERSOLUTIONS_VENDOR_ID, "solve_failed", {
283
+ phase: "fetch_script",
284
+ });
285
+ }
286
+ return scriptResponse;
287
+ });
288
+ const stateCookie = transport
289
+ .getCookie(challenge.stateCookieName, challenge.pageUrl)
290
+ ?.trim();
291
+ if (!stateCookie) {
292
+ throw new ResolverVendorUnavailableError(HYPERSOLUTIONS_VENDOR_ID, "missing_challenge_input", { missingFields: [challenge.stateCookieName], phase: "fetch_script" });
293
+ }
294
+ let expires;
295
+ for (const [roundIndex, index] of exchange.indices.entries()) {
296
+ const round = roundIndex + 1;
297
+ const payload = await recordPaidResolverCreate({
298
+ traceRecorder,
299
+ vendor: HYPERSOLUTIONS_VENDOR_ID,
300
+ kind: challenge.kind,
301
+ endpoint: "hyper:sbsd_create",
302
+ round,
303
+ signal: operationSignal,
304
+ usage,
305
+ create: () => runPhase("generate_payload", async () => {
306
+ const hyperResponse = await generatePayload(fetchImpl, apiKey, JSON.stringify({
307
+ index,
308
+ uuid: exchange.uuid,
309
+ o: stateCookie,
310
+ pageUrl: challenge.pageUrl,
311
+ userAgent,
312
+ script: scriptResponse.body,
313
+ ip,
314
+ acceptLanguage,
315
+ }), operationSignal);
316
+ requireSuccess(hyperResponse.status, "generate_payload");
317
+ const generated = hyperResponse.body === undefined ? undefined : parsePayload(hyperResponse.body);
318
+ if (!generated) {
319
+ throw new ResolverVendorUnavailableError(HYPERSOLUTIONS_VENDOR_ID, "transport_failure", { phase: "generate_payload", round });
320
+ }
321
+ return generated;
322
+ }),
323
+ });
324
+ const postResponse = await runPhase("post_payload", async () => {
325
+ const postResponse = await boundFetch(transport, exchange.postUrl, {
326
+ method: "POST",
327
+ headers: {
328
+ ...sessionHeaders,
329
+ "content-type": "application/json",
330
+ Referer: challenge.pageUrl,
331
+ },
332
+ body: JSON.stringify({ body: payload }),
333
+ signal: operationSignal,
334
+ redirect: "manual",
335
+ maxBodyBytes: BODY_MAX_BYTES,
336
+ }, "post_payload");
337
+ requireSuccess(postResponse.status, "post_payload");
338
+ assertBoundedBody(postResponse, BODY_MAX_BYTES, "post_payload");
339
+ return postResponse;
340
+ });
341
+ expires =
342
+ postResponse.cookies.find((cookie) => cookie.name === challenge.stateCookieName)
343
+ ?.expires ?? expires;
344
+ }
345
+ // A 2xx payload POST is not proof of a solve and the state cookie is not
346
+ // required to rotate (measured zozotown source and Hyper's docs both verify
347
+ // only through the next protected GET, which is Phase 2).
348
+ return {
349
+ form: "cookie_state",
350
+ kind: "akamai_sbsd",
351
+ outcome: "payload_accepted",
352
+ verified: false,
353
+ stateCookieName: challenge.stateCookieName,
354
+ ...(expires === undefined ? {} : { expires }),
355
+ };
356
+ }
357
+ catch (error) {
358
+ if (timeoutController.signal.aborted && !signal.aborted) {
359
+ throw new ResolverVendorUnavailableError(HYPERSOLUTIONS_VENDOR_ID, "timeout");
360
+ }
361
+ throw error;
362
+ }
363
+ finally {
364
+ if (timeout !== undefined)
365
+ clearTimeout(timeout);
366
+ }
367
+ },
368
+ };
369
+ }
@@ -1,6 +1,6 @@
1
1
  import type { ChallengeSolution, ProviderChallenge } from "../../types.js";
2
2
  import type { TraceRecorder } from "../trace.js";
3
- import { type ResolverIdentity, type ResolverVendorAdapter } from "./types.js";
3
+ import { type ResolverIdentity, type ResolverPaidUsageContext, type ResolverVendorAdapter, type ResolverVendorTransport } from "./types.js";
4
4
  type Delay = (ms: number, signal: AbortSignal) => Promise<void>;
5
5
  export interface TwoCaptchaResolverVendorOptions {
6
6
  readonly apiKey?: string;
@@ -16,7 +16,7 @@ export interface TwoCaptchaResolverVendorOptions {
16
16
  }
17
17
  export interface TwoCaptchaResolverVendorAdapter extends ResolverVendorAdapter {
18
18
  readonly id: "2captcha";
19
- solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder): Promise<Extract<ChallengeSolution, {
19
+ solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder, transport?: ResolverVendorTransport, usage?: ResolverPaidUsageContext): Promise<Extract<ChallengeSolution, {
20
20
  readonly form: "token";
21
21
  }>>;
22
22
  }
@@ -1,5 +1,6 @@
1
+ import { recordPaidResolverCreate } from "../resolver-usage.js";
1
2
  import { assertResolverHostAllowed } from "./hosts.js";
2
- import { ResolverChallengeVerdictError, ResolverVendorUnavailableError, resolverVendorSupports, } from "./types.js";
3
+ import { ResolverChallengeVerdictError, ResolverVendorUnavailableError, recordResolverVendorPoll, resolverVendorSupports, } from "./types.js";
3
4
  const TWOCAPTCHA_VENDOR_ID = "2captcha";
4
5
  const DEFAULT_TWOCAPTCHA_BASE_URL = "https://api.2captcha.com";
5
6
  const DEFAULT_POLL_INTERVAL_MS = 3_000;
@@ -225,7 +226,7 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
225
226
  supports(kind) {
226
227
  return resolverVendorSupports(TWOCAPTCHA_VENDOR_ID, kind);
227
228
  },
228
- async solve(challenge, identity, callerSignal, traceRecorder) {
229
+ async solve(challenge, identity, callerSignal, traceRecorder, _transport, usage) {
229
230
  const apiKey = options.apiKey?.trim();
230
231
  if (!apiKey) {
231
232
  throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "missing_credentials", {
@@ -325,12 +326,22 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
325
326
  (() => {
326
327
  throw new TypeError(`2captcha resolver does not support ${challenge.kind}`);
327
328
  })();
328
- const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), { clientKey: apiKey, task }, solveController.signal, phase, [apiKey]);
329
- const taskId = taskIdFrom(createResult.payload);
330
- if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
331
- throw unavailableForPayload(createResult.payload, phase);
332
- }
333
- return taskId;
329
+ return await recordPaidResolverCreate({
330
+ traceRecorder,
331
+ vendor: TWOCAPTCHA_VENDOR_ID,
332
+ kind: challenge.kind,
333
+ endpoint: "twocaptcha:create_task",
334
+ signal: solveController.signal,
335
+ usage,
336
+ create: async () => {
337
+ const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), { clientKey: apiKey, task }, solveController.signal, phase, [apiKey]);
338
+ const taskId = taskIdFrom(createResult.payload);
339
+ if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
340
+ throw unavailableForPayload(createResult.payload, phase);
341
+ }
342
+ return taskId;
343
+ },
344
+ });
334
345
  };
335
346
  const taskId = traceRecorder
336
347
  ? await traceRecorder.runSpan("resolver.vendor.create_task", createTask, {
@@ -352,6 +363,7 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
352
363
  callerSignal.throwIfAborted();
353
364
  if (now() - startedAt >= timeoutMs)
354
365
  throw new TwoCaptchaSolveTimeoutError();
366
+ recordResolverVendorPoll(traceRecorder);
355
367
  const pollResult = await postJson(fetchImpl, endpoint(baseUrl, "getTaskResult"), { clientKey: apiKey, taskId }, solveController.signal, phase, [apiKey]);
356
368
  if (!pollResult.ok || pollResult.payload.errorId !== 0) {
357
369
  throw unavailableForPayload(pollResult.payload, phase);
@@ -359,11 +371,15 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
359
371
  if (pollResult.payload.status === "processing")
360
372
  continue;
361
373
  if (pollResult.payload.status !== "ready") {
362
- throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", { phase });
374
+ throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
375
+ phase,
376
+ });
363
377
  }
364
378
  const token = tokenFrom(pollResult.payload, challenge);
365
379
  if (!token?.trim()) {
366
- throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", { phase });
380
+ throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
381
+ phase,
382
+ });
367
383
  }
368
384
  // AWS WAF remains a token solution here, so resolver cookie caching does not apply.
369
385
  return { form: "token", token };
@@ -1,18 +1,22 @@
1
1
  import type { ChallengeSolution, ProviderChallenge, ProviderChallengeKind, ProviderResolverConfig, ProviderResolverVendor } from "../../types.js";
2
2
  import type { TraceRecorder } from "../trace.js";
3
+ /** Internal chain-runner hook; vendor adapters never receive the telemetry sink itself. */
4
+ export declare function recordResolverVendorPoll(traceRecorder: TraceRecorder | undefined): void;
5
+ export declare function attachResolverVendorPollObserver(traceRecorder: TraceRecorder, observer: () => void): TraceRecorder;
3
6
  export declare const RESOLVER_VENDOR_CAPABILITIES: {
4
7
  readonly browser: readonly ["aws_waf", "cloudflare_interstitial"];
5
8
  readonly "2captcha": readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "aws_waf"];
6
9
  readonly capsolver: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf"];
7
10
  readonly capmonster: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha"];
8
- readonly custom: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf", "akamai_sec_cpt", "akamai_sensor"];
11
+ readonly hypersolutions: readonly ["akamai_sbsd"];
12
+ readonly custom: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf", "akamai_sec_cpt", "akamai_sensor", "akamai_sbsd"];
9
13
  };
10
14
  /**
11
15
  * SDK-owned fallback policy for hosted resolver vendors. Capability support is
12
16
  * applied separately, so each provider receives only vendors that support one
13
17
  * or more of its declared challenge kinds.
14
18
  */
15
- export declare const DEFAULT_RESOLVER_VENDOR_PREFERENCE: readonly ["capsolver", "2captcha"];
19
+ export declare const DEFAULT_RESOLVER_VENDOR_PREFERENCE: readonly ["capsolver", "2captcha", "hypersolutions"];
16
20
  export declare function resolverVendorSupports(vendor: ProviderResolverVendor, kind: ProviderChallengeKind): boolean;
17
21
  /** Resolves an explicit provider override or the SDK-owned default vendor chain. */
18
22
  export declare function resolveProviderResolverVendors(config: ProviderResolverConfig): readonly ProviderResolverVendor[];
@@ -27,9 +31,19 @@ export interface ResolverIssuingIdentity {
27
31
  }
28
32
  export interface ResolverVendorTransport {
29
33
  /**
30
- * Bound to the resolved proxy lease and client profile.
31
- * Implementations MUST NOT follow redirects and MUST return the initial redirect response.
34
+ * Exact stable upstream headers owned by the proxy/profile-bound transport.
35
+ * This does not include dynamic Cookie headers; the transport injects those from its jar.
36
+ * Adapter requests do not receive these headers implicitly and must forward
37
+ * them when their measured protocol requires session-header continuity.
32
38
  */
39
+ readonly sessionHeaders?: Readonly<Record<string, string>>;
40
+ /**
41
+ * Read a cookie from the transport's bound jar for the supplied URL. Responses
42
+ * returned by `fetch` MUST already have been applied to that jar before this
43
+ * method is called. Profile-bound adapters fail closed when this seam is absent.
44
+ */
45
+ readonly getCookie?: (name: string, url: string) => string | undefined;
46
+ /** Implementations MUST NOT follow redirects and must return the initial response. */
33
47
  fetch(url: string, init: {
34
48
  method: "GET" | "POST";
35
49
  headers?: Readonly<Record<string, string>>;
@@ -37,6 +51,8 @@ export interface ResolverVendorTransport {
37
51
  signal: AbortSignal;
38
52
  /** Implementations MUST honor manual redirect handling when the SDK guard sets it. */
39
53
  redirect?: "manual";
54
+ /** Maximum UTF-8 response body size; implementations must stop reading at this bound. */
55
+ maxBodyBytes?: number;
40
56
  }): Promise<{
41
57
  readonly status: number;
42
58
  readonly headers: Readonly<Record<string, string>>;
@@ -55,13 +71,27 @@ export interface ResolverVendorTransport {
55
71
  }[];
56
72
  }>;
57
73
  }
74
+ /** Metering context the resolver chain hands each vendor adapter for its paid calls. */
75
+ export type ResolverPaidUsageContext = {
76
+ /** 1-based position of the vendor in the provider's declared resolver chain. */
77
+ readonly vendorIndex: number;
78
+ /** SHA-256 scope digest; never the raw affinity, proxy URL, cookie, or IP. */
79
+ readonly resolverIdentityScope?: string;
80
+ };
58
81
  export interface ResolverVendorAdapter {
59
82
  readonly id: ProviderResolverVendor;
60
83
  readonly requiresTransport?: boolean | ((kind: ProviderChallengeKind) => boolean);
84
+ /**
85
+ * Exact SDK-owned service hosts this adapter may reach through its bound transport.
86
+ * Honored for every adapter the chain runs, so whoever supplies `adapters` to
87
+ * `createResolverClient` is the trust boundary; the engine path
88
+ * (`createResolverClientFromEnv`) only constructs registry adapters.
89
+ */
90
+ readonly transportAllowedHosts?: readonly string[];
61
91
  supports(kind: ProviderChallengeKind): boolean;
62
92
  /** Identity the adapter actually used, reported after a successful solve. */
63
93
  getIssuingIdentity?(solution: ChallengeSolution, requestedIdentity: ResolverIdentity | undefined, challenge: ProviderChallenge): ResolverIssuingIdentity | undefined;
64
- solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder, transport?: ResolverVendorTransport): Promise<ChallengeSolution>;
94
+ solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder, transport?: ResolverVendorTransport, usage?: ResolverPaidUsageContext): Promise<ChallengeSolution>;
65
95
  }
66
96
  export type ResolverVendorUnavailableReason = "missing_credentials" | "missing_proxy_identity" | "missing_client_profile" | "missing_challenge_input" | "missing_transport" | "allocation_exhausted" | "transport_failure" | "timeout" | "not_implemented";
67
97
  export type ResolverChallengeVerdictReason = "human_puzzle" | "solve_failed";