@apifuse/provider-sdk 2.2.0-beta.28 → 2.2.0-beta.29
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 +32 -3
- package/bin/apifuse-pack-types.ts +24 -1
- package/bin/apifuse-record.ts +39 -6
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +38 -0
- package/dist/config/loader.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime/browser.js +45 -2
- package/dist/runtime/proxy-telemetry.js +3 -0
- package/dist/runtime/resolver-public.d.ts +1 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-vendors/browser.js +14 -4
- package/dist/runtime/resolver-vendors/twocaptcha.js +84 -17
- package/dist/runtime/resolver-vendors/types.d.ts +2 -2
- package/dist/runtime/resolver-vendors/types.js +3 -1
- package/dist/runtime/resolver.d.ts +12 -3
- package/dist/runtime/resolver.js +80 -12
- package/dist/runtime/stealth.d.ts +1 -0
- package/dist/runtime/stealth.js +1 -1
- package/dist/server/serve-implementation.js +20 -2
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/package.json +4 -4
- package/src/auth.ts +78 -0
- package/src/config/loader.ts +3 -0
- package/src/index.ts +0 -1
- package/src/runtime/browser.ts +50 -2
- package/src/runtime/proxy-telemetry.ts +5 -0
- package/src/runtime/resolver-public.ts +18 -0
- package/src/runtime/resolver-vendors/browser.ts +14 -4
- package/src/runtime/resolver-vendors/twocaptcha.ts +102 -19
- package/src/runtime/resolver-vendors/types.ts +7 -2
- package/src/runtime/resolver.ts +104 -17
- package/src/runtime/stealth.ts +1 -1
- package/src/server/serve-implementation.ts +20 -2
- package/src/testing/index.ts +1 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assertResolverHostAllowed } from "./hosts.js";
|
|
2
|
-
import { ResolverVendorUnavailableError, resolverVendorSupports, } from "./types.js";
|
|
2
|
+
import { ResolverChallengeVerdictError, ResolverVendorUnavailableError, resolverVendorSupports, } from "./types.js";
|
|
3
3
|
const TWOCAPTCHA_VENDOR_ID = "2captcha";
|
|
4
4
|
const DEFAULT_TWOCAPTCHA_BASE_URL = "https://api.2captcha.com";
|
|
5
5
|
const DEFAULT_POLL_INTERVAL_MS = 3_000;
|
|
@@ -16,7 +16,46 @@ function isJsonRecord(value) {
|
|
|
16
16
|
function abortReason(signal) {
|
|
17
17
|
return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
18
18
|
}
|
|
19
|
-
function
|
|
19
|
+
function containsSensitiveValue(value, sensitiveValues) {
|
|
20
|
+
const secrets = sensitiveValues.filter((secret) => secret.length > 0);
|
|
21
|
+
if (secrets.length === 0)
|
|
22
|
+
return false;
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
const inspect = (candidate) => {
|
|
25
|
+
if (typeof candidate === "string") {
|
|
26
|
+
return secrets.some((secret) => candidate.includes(secret));
|
|
27
|
+
}
|
|
28
|
+
if (candidate === null ||
|
|
29
|
+
(typeof candidate !== "object" && typeof candidate !== "function")) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (seen.has(candidate))
|
|
33
|
+
return false;
|
|
34
|
+
seen.add(candidate);
|
|
35
|
+
try {
|
|
36
|
+
for (const property of Reflect.ownKeys(candidate)) {
|
|
37
|
+
if (typeof property === "string" && inspect(property))
|
|
38
|
+
return true;
|
|
39
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, property);
|
|
40
|
+
if (!descriptor)
|
|
41
|
+
return true;
|
|
42
|
+
if ("value" in descriptor && inspect(descriptor.value))
|
|
43
|
+
return true;
|
|
44
|
+
if (descriptor.get !== undefined || descriptor.set !== undefined)
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
};
|
|
53
|
+
return inspect(value);
|
|
54
|
+
}
|
|
55
|
+
function safeCauseOptions(error, sensitiveValues) {
|
|
56
|
+
return containsSensitiveValue(error, sensitiveValues) ? {} : { cause: error };
|
|
57
|
+
}
|
|
58
|
+
function raceWithAbort(operation, signal, phase, sensitiveValues = []) {
|
|
20
59
|
if (signal.aborted)
|
|
21
60
|
return Promise.reject(abortReason(signal));
|
|
22
61
|
return new Promise((resolve, reject) => {
|
|
@@ -36,7 +75,7 @@ function raceWithAbort(operation, signal, phase) {
|
|
|
36
75
|
return;
|
|
37
76
|
}
|
|
38
77
|
reject(new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
|
|
39
|
-
|
|
78
|
+
...safeCauseOptions(error, sensitiveValues),
|
|
40
79
|
phase,
|
|
41
80
|
}));
|
|
42
81
|
});
|
|
@@ -92,17 +131,23 @@ function isAllocationExhausted(payload) {
|
|
|
92
131
|
return (code === "error_zero_balance" ||
|
|
93
132
|
/(?:insufficient|zero|no|not enough)\s+(?:balance|funds|credit)/u.test(`${code} ${description}`));
|
|
94
133
|
}
|
|
134
|
+
function isNegativeVerdict(payload) {
|
|
135
|
+
return errorText(payload, "errorCode").toLowerCase() === "error_captcha_unsolvable";
|
|
136
|
+
}
|
|
95
137
|
function unavailableForPayload(payload, phase) {
|
|
138
|
+
if (isNegativeVerdict(payload)) {
|
|
139
|
+
return new ResolverChallengeVerdictError(TWOCAPTCHA_VENDOR_ID, "solve_failed");
|
|
140
|
+
}
|
|
96
141
|
return new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, isAllocationExhausted(payload) ? "allocation_exhausted" : "transport_failure", { phase });
|
|
97
142
|
}
|
|
98
|
-
async function postJson(fetchImpl, url, body, signal, phase) {
|
|
143
|
+
async function postJson(fetchImpl, url, body, signal, phase, sensitiveValues) {
|
|
99
144
|
const response = await raceWithAbort(() => fetchImpl(url, {
|
|
100
145
|
method: "POST",
|
|
101
146
|
headers: { "content-type": "application/json" },
|
|
102
147
|
body: JSON.stringify(body),
|
|
103
148
|
signal,
|
|
104
149
|
redirect: "error",
|
|
105
|
-
}), signal, phase);
|
|
150
|
+
}), signal, phase, sensitiveValues);
|
|
106
151
|
let responseText;
|
|
107
152
|
try {
|
|
108
153
|
responseText = await raceWithAbort(() => response.text(), signal, phase);
|
|
@@ -136,10 +181,13 @@ function taskIdFrom(payload) {
|
|
|
136
181
|
const taskId = payload.taskId;
|
|
137
182
|
return typeof taskId === "string" || typeof taskId === "number" ? taskId : undefined;
|
|
138
183
|
}
|
|
139
|
-
function tokenFrom(payload) {
|
|
184
|
+
function tokenFrom(payload, challenge) {
|
|
140
185
|
const solution = payload.solution;
|
|
141
186
|
if (!isJsonRecord(solution))
|
|
142
187
|
return undefined;
|
|
188
|
+
if (challenge.kind === "aws_waf") {
|
|
189
|
+
return typeof solution.existing_token === "string" ? solution.existing_token : undefined;
|
|
190
|
+
}
|
|
143
191
|
if (typeof solution.gRecaptchaResponse === "string")
|
|
144
192
|
return solution.gRecaptchaResponse;
|
|
145
193
|
return typeof solution.token === "string" ? solution.token : undefined;
|
|
@@ -187,8 +235,16 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
|
187
235
|
if (!resolverVendorSupports(TWOCAPTCHA_VENDOR_ID, challenge.kind)) {
|
|
188
236
|
throw new TypeError(`2captcha resolver does not support ${challenge.kind}`);
|
|
189
237
|
}
|
|
190
|
-
if (challenge.kind !== "recaptcha_v2") {
|
|
191
|
-
|
|
238
|
+
if (challenge.kind !== "recaptcha_v2" && challenge.kind !== "aws_waf") {
|
|
239
|
+
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
|
|
240
|
+
phase: "create_task",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (challenge.kind === "aws_waf" &&
|
|
244
|
+
(!challenge.siteKey?.trim() ||
|
|
245
|
+
!challenge.captchaScript?.trim() ||
|
|
246
|
+
!challenge.context?.trim() ||
|
|
247
|
+
!challenge.iv?.trim())) {
|
|
192
248
|
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
|
|
193
249
|
phase: "create_task",
|
|
194
250
|
});
|
|
@@ -211,17 +267,26 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
|
211
267
|
let phase = "create_task";
|
|
212
268
|
try {
|
|
213
269
|
const createTask = async () => {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
270
|
+
const task = challenge.kind === "aws_waf"
|
|
271
|
+
? {
|
|
272
|
+
type: proxy ? "AmazonTask" : "AmazonTaskProxyless",
|
|
273
|
+
websiteURL: challenge.pageUrl,
|
|
274
|
+
websiteKey: challenge.siteKey,
|
|
275
|
+
captchaScript: challenge.captchaScript,
|
|
276
|
+
context: challenge.context,
|
|
277
|
+
iv: challenge.iv,
|
|
278
|
+
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
279
|
+
...(proxy ?? {}),
|
|
280
|
+
}
|
|
281
|
+
: {
|
|
217
282
|
type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
|
|
218
283
|
websiteURL: challenge.pageUrl,
|
|
219
284
|
websiteKey: challenge.siteKey,
|
|
220
285
|
isInvisible: false,
|
|
221
286
|
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
222
287
|
...(proxy ?? {}),
|
|
223
|
-
}
|
|
224
|
-
}, solveController.signal, phase);
|
|
288
|
+
};
|
|
289
|
+
const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), { clientKey: apiKey, task }, solveController.signal, phase, [apiKey]);
|
|
225
290
|
const taskId = taskIdFrom(createResult.payload);
|
|
226
291
|
if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
|
|
227
292
|
throw unavailableForPayload(createResult.payload, phase);
|
|
@@ -248,7 +313,7 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
|
248
313
|
callerSignal.throwIfAborted();
|
|
249
314
|
if (now() - startedAt >= timeoutMs)
|
|
250
315
|
throw new TwoCaptchaSolveTimeoutError();
|
|
251
|
-
const pollResult = await postJson(fetchImpl, endpoint(baseUrl, "getTaskResult"), { clientKey: apiKey, taskId }, solveController.signal, phase);
|
|
316
|
+
const pollResult = await postJson(fetchImpl, endpoint(baseUrl, "getTaskResult"), { clientKey: apiKey, taskId }, solveController.signal, phase, [apiKey]);
|
|
252
317
|
if (!pollResult.ok || pollResult.payload.errorId !== 0) {
|
|
253
318
|
throw unavailableForPayload(pollResult.payload, phase);
|
|
254
319
|
}
|
|
@@ -257,7 +322,7 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
|
257
322
|
if (pollResult.payload.status !== "ready") {
|
|
258
323
|
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", { phase });
|
|
259
324
|
}
|
|
260
|
-
const token = tokenFrom(pollResult.payload);
|
|
325
|
+
const token = tokenFrom(pollResult.payload, challenge);
|
|
261
326
|
if (!token?.trim()) {
|
|
262
327
|
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", { phase });
|
|
263
328
|
}
|
|
@@ -284,10 +349,12 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
|
284
349
|
phase,
|
|
285
350
|
});
|
|
286
351
|
}
|
|
287
|
-
if (error instanceof ResolverVendorUnavailableError
|
|
352
|
+
if (error instanceof ResolverVendorUnavailableError ||
|
|
353
|
+
error instanceof ResolverChallengeVerdictError) {
|
|
288
354
|
throw error;
|
|
355
|
+
}
|
|
289
356
|
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "transport_failure", {
|
|
290
|
-
|
|
357
|
+
...safeCauseOptions(error, [apiKey]),
|
|
291
358
|
phase,
|
|
292
359
|
});
|
|
293
360
|
}
|
|
@@ -55,8 +55,8 @@ 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_proxy_identity" | "missing_transport" | "allocation_exhausted" | "transport_failure" | "timeout" | "not_implemented";
|
|
59
|
-
export type ResolverChallengeVerdictReason = "human_puzzle";
|
|
58
|
+
export type ResolverVendorUnavailableReason = "missing_credentials" | "missing_proxy_identity" | "missing_client_profile" | "missing_transport" | "allocation_exhausted" | "transport_failure" | "timeout" | "not_implemented";
|
|
59
|
+
export type ResolverChallengeVerdictReason = "human_puzzle" | "solve_failed";
|
|
60
60
|
type ResolverErrorOptions = {
|
|
61
61
|
/** Raw cause; adapters must not place bodies, cookies, headers, credentials, or proxy URLs here. */
|
|
62
62
|
readonly cause?: unknown;
|
|
@@ -56,7 +56,9 @@ export class ResolverChallengeVerdictError extends Error {
|
|
|
56
56
|
vendor;
|
|
57
57
|
reason;
|
|
58
58
|
constructor(vendor, reason, options = {}) {
|
|
59
|
-
super(
|
|
59
|
+
super(reason === "solve_failed"
|
|
60
|
+
? `Resolver vendor ${vendor} attempted the challenge but did not solve it`
|
|
61
|
+
: `Resolver vendor ${vendor} returned a challenge verdict: ${reason}`);
|
|
60
62
|
this.vendor = vendor;
|
|
61
63
|
this.reason = reason;
|
|
62
64
|
this.name = "ResolverChallengeVerdictError";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ProxyResolutionOptions } from "../config/loader.js";
|
|
1
2
|
import type { ChallengeSolution, ProviderCache, ProviderChallenge, ProviderChallengeKind, ProviderProxyMode, ProviderResolverConfig, ProviderResolverVendor, ResolverContext } from "../types.js";
|
|
2
3
|
import { type ResolverIdentity, type ResolverVendorAdapter, type ResolverVendorTransport } from "./resolver-vendors/types.js";
|
|
3
4
|
import type { TraceRecorder } from "./trace.js";
|
|
@@ -10,8 +11,14 @@ type ResolverChainClient = ResolverContext & {
|
|
|
10
11
|
export interface ResolverRuntimeOptions {
|
|
11
12
|
readonly allowedHosts?: readonly string[];
|
|
12
13
|
readonly cache?: ProviderCache;
|
|
13
|
-
/**
|
|
14
|
-
readonly
|
|
14
|
+
/** Inputs for SDK-owned lazy proxy resolution. The SDK never accepts a caller-built identity. */
|
|
15
|
+
readonly proxyIntent?: {
|
|
16
|
+
readonly mode: ProviderProxyMode;
|
|
17
|
+
readonly upstream: NonNullable<ProxyResolutionOptions["upstream"]>;
|
|
18
|
+
readonly affinityKey?: ProxyResolutionOptions["affinityKey"];
|
|
19
|
+
readonly telemetry?: ProxyResolutionOptions["telemetry"];
|
|
20
|
+
readonly userAgent?: string;
|
|
21
|
+
};
|
|
15
22
|
/** Server-owned context/proxy scope used only for identity-bound cache entries. */
|
|
16
23
|
readonly identityScope?: string;
|
|
17
24
|
/** SDK-owned transport already bound to the resolved proxy lease and client profile. */
|
|
@@ -30,6 +37,8 @@ export type ResolverInstrumentationMetadata = {
|
|
|
30
37
|
export type ResolverAdapterFactory = (configuration: string, timeoutMs: number, allowedHosts: readonly string[]) => ResolverVendorAdapter;
|
|
31
38
|
export declare const RESOLVER_ADAPTER_REGISTRY: Partial<Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>>;
|
|
32
39
|
export declare function swapResolverAdapterFactoryForTests(vendor: ProviderResolverVendor, factory: ResolverAdapterFactory | undefined): () => void;
|
|
40
|
+
/** Internal test seam; deliberately not re-exported from the package root. */
|
|
41
|
+
export declare function swapResolverDefaultUserAgentForTests(resolver: (() => string | undefined) | undefined): () => void;
|
|
33
42
|
/** Remove the cached entry for the exact solution object returned by this resolver. */
|
|
34
43
|
export declare function invalidateResolverSolution(resolver: ResolverContext, challenge: ProviderChallenge, solution: ChallengeSolution): Promise<void>;
|
|
35
44
|
export declare function createResolverClient(options: {
|
|
@@ -38,7 +47,7 @@ export declare function createResolverClient(options: {
|
|
|
38
47
|
readonly unavailableReason?: string;
|
|
39
48
|
readonly cache?: ProviderCache;
|
|
40
49
|
readonly identity?: ResolverIdentity;
|
|
41
|
-
readonly
|
|
50
|
+
readonly proxyIntent?: ResolverRuntimeOptions["proxyIntent"];
|
|
42
51
|
readonly transport?: ResolverVendorTransport;
|
|
43
52
|
readonly createTransport?: ResolverRuntimeOptions["createTransport"];
|
|
44
53
|
readonly clientProfile?: string;
|
package/dist/runtime/resolver.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { resolveProxyConfigAsync, } from "../config/loader.js";
|
|
2
3
|
import { ProviderError } from "../errors.js";
|
|
4
|
+
import { getStealthProfile } from "../stealth/profiles.js";
|
|
3
5
|
import { resolverChallengeAllowsDirectCache, resolverChallengeIsCacheable, resolverChallengeIsIdentityScoped, resolverChallengeIssuingIdentity, } from "./resolver-vendors/bindings.js";
|
|
4
6
|
import { createBrowserResolverVendorAdapter } from "./resolver-vendors/browser.js";
|
|
5
7
|
import { assertResolverHostAllowed } from "./resolver-vendors/hosts.js";
|
|
@@ -7,6 +9,7 @@ import { createTwoCaptchaResolverVendorAdapter } from "./resolver-vendors/twocap
|
|
|
7
9
|
import { RESOLVER_VENDOR_CAPABILITIES, ResolverVendorUnavailableError, resolverVendorSupports, } from "./resolver-vendors/types.js";
|
|
8
10
|
import { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
|
|
9
11
|
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";
|
|
12
|
+
import { DEFAULT_PROFILE } from "./stealth.js";
|
|
10
13
|
export { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
|
|
11
14
|
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";
|
|
12
15
|
const RESOLVER_SOLUTION_CACHE_NAMESPACE = "resolver-solution";
|
|
@@ -84,6 +87,20 @@ export function swapResolverAdapterFactoryForTests(vendor, factory) {
|
|
|
84
87
|
resolverAdapterRegistry[vendor] = original;
|
|
85
88
|
};
|
|
86
89
|
}
|
|
90
|
+
let resolveDefaultResolverUserAgent = () => getStealthProfile(DEFAULT_PROFILE).userAgent;
|
|
91
|
+
/** Internal test seam; deliberately not re-exported from the package root. */
|
|
92
|
+
export function swapResolverDefaultUserAgentForTests(resolver) {
|
|
93
|
+
const original = resolveDefaultResolverUserAgent;
|
|
94
|
+
resolveDefaultResolverUserAgent =
|
|
95
|
+
resolver ?? (() => getStealthProfile(DEFAULT_PROFILE).userAgent);
|
|
96
|
+
let restored = false;
|
|
97
|
+
return () => {
|
|
98
|
+
if (restored)
|
|
99
|
+
return;
|
|
100
|
+
restored = true;
|
|
101
|
+
resolveDefaultResolverUserAgent = original;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
87
104
|
// This is the sole allowlist for declared vendors whose registry entry may be absent.
|
|
88
105
|
// Remove a vendor here when its adapter is registered.
|
|
89
106
|
const KNOWN_UNIMPLEMENTED_RESOLVER_VENDORS = new Set([
|
|
@@ -172,6 +189,8 @@ function adapterRequiresTransport(adapter, kind) {
|
|
|
172
189
|
function sanitizeDiagnosticUrl(rawUrl) {
|
|
173
190
|
try {
|
|
174
191
|
const parsed = new URL(rawUrl);
|
|
192
|
+
if (parsed.username || parsed.password)
|
|
193
|
+
return "[REDACTED_PROXY_URL]";
|
|
175
194
|
return `${parsed.protocol}//${parsed.host}`;
|
|
176
195
|
}
|
|
177
196
|
catch {
|
|
@@ -187,7 +206,9 @@ function sanitizeCauseMessage(message) {
|
|
|
187
206
|
.split(/\s+/)
|
|
188
207
|
.filter(Boolean)
|
|
189
208
|
.map((token) => {
|
|
190
|
-
if (token === "[REDACTED]" ||
|
|
209
|
+
if (token === "[REDACTED]" ||
|
|
210
|
+
token === "[REDACTED_PROXY_URL]" ||
|
|
211
|
+
/^[a-z][a-z\d+.-]*:\/\/[^\s]+$/i.test(token))
|
|
191
212
|
return token;
|
|
192
213
|
const word = token.replace(/^[^a-z\d]+|[^a-z\d]+$/gi, "");
|
|
193
214
|
return word.length > 0 &&
|
|
@@ -449,6 +470,47 @@ export async function invalidateResolverSolution(resolver, challenge, solution)
|
|
|
449
470
|
onSuccess: (outcome) => ({ outcome }),
|
|
450
471
|
});
|
|
451
472
|
}
|
|
473
|
+
async function resolveResolverIdentity(proxyIntent) {
|
|
474
|
+
const userAgentSource = proxyIntent.userAgent ? "declared" : "defaulted";
|
|
475
|
+
let proxyUrl;
|
|
476
|
+
try {
|
|
477
|
+
const resolved = await resolveProxyConfigAsync({
|
|
478
|
+
upstream: proxyIntent.upstream,
|
|
479
|
+
affinityKey: proxyIntent.affinityKey,
|
|
480
|
+
telemetry: proxyIntent.telemetry
|
|
481
|
+
? {
|
|
482
|
+
...proxyIntent.telemetry,
|
|
483
|
+
recordProxyResolution(event) {
|
|
484
|
+
proxyIntent.telemetry?.recordProxyResolution({
|
|
485
|
+
...event,
|
|
486
|
+
userAgentSource,
|
|
487
|
+
});
|
|
488
|
+
},
|
|
489
|
+
}
|
|
490
|
+
: undefined,
|
|
491
|
+
});
|
|
492
|
+
proxyUrl = resolved.url;
|
|
493
|
+
if (!proxyUrl)
|
|
494
|
+
return { unavailableReason: "missing_proxy_identity", userAgentSource };
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
// Lease failures contain infrastructure detail that must not cross the resolver
|
|
498
|
+
// boundary. A required policy is classified by the existing fail-closed guard.
|
|
499
|
+
return { unavailableReason: "missing_proxy_identity", userAgentSource };
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
const userAgent = proxyIntent.userAgent || resolveDefaultResolverUserAgent();
|
|
503
|
+
if (!userAgent)
|
|
504
|
+
return { unavailableReason: "missing_client_profile", userAgentSource };
|
|
505
|
+
return {
|
|
506
|
+
identity: { proxyUrl, userAgent },
|
|
507
|
+
userAgentSource,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
return { unavailableReason: "missing_client_profile", userAgentSource };
|
|
512
|
+
}
|
|
513
|
+
}
|
|
452
514
|
function createResolverChainClient(options) {
|
|
453
515
|
assertClientProfileTransportContract(options.clientProfile, options.transport);
|
|
454
516
|
const client = {
|
|
@@ -464,18 +526,23 @@ function createResolverChainClient(options) {
|
|
|
464
526
|
if (supportingEntries.length === 0)
|
|
465
527
|
throwUnsupportedKind(challenge.kind);
|
|
466
528
|
signal.throwIfAborted();
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const
|
|
529
|
+
const identityResolution = options.proxyIntent
|
|
530
|
+
? await resolveResolverIdentity(options.proxyIntent)
|
|
531
|
+
: { identity: options.identity };
|
|
532
|
+
const identity = identityResolution.identity;
|
|
533
|
+
signal.throwIfAborted();
|
|
534
|
+
// Resolve a required proxy lease before consulting the cache. Solutions minted
|
|
535
|
+
// under a previous release are shared and long-lived, but a portable cached token
|
|
536
|
+
// must not bypass the upstream admission policy when no lease can be resolved.
|
|
537
|
+
const requiredProxyIdentityMissing = options.proxyIntent?.mode === "required" && identity === undefined;
|
|
471
538
|
if (requiredProxyIdentityMissing) {
|
|
472
539
|
throwExhausted(supportingEntries.map((entry) => ({
|
|
473
540
|
vendor: entry.id,
|
|
474
|
-
reason: "missing_proxy_identity",
|
|
541
|
+
reason: identityResolution.unavailableReason ?? "missing_proxy_identity",
|
|
475
542
|
})));
|
|
476
543
|
}
|
|
477
544
|
if (options.cache && resolverChallengeIsCacheable(challenge)) {
|
|
478
|
-
const cached = await findCachedSolution(options.cache, challenge,
|
|
545
|
+
const cached = await findCachedSolution(options.cache, challenge, identity, options.identityScope);
|
|
479
546
|
if (cached)
|
|
480
547
|
return cached;
|
|
481
548
|
}
|
|
@@ -498,7 +565,7 @@ function createResolverChainClient(options) {
|
|
|
498
565
|
const transport = unrestrictedTransport
|
|
499
566
|
? restrictResolverTransport(unrestrictedTransport, options.allowedHosts ?? [])
|
|
500
567
|
: undefined;
|
|
501
|
-
return adapter.solve(challenge,
|
|
568
|
+
return adapter.solve(challenge, identity, signal, traceRecorder, transport);
|
|
502
569
|
};
|
|
503
570
|
const solution = traceRecorder
|
|
504
571
|
? await traceRecorder.runSpan("resolver.vendor.attempt", solveAttempt, {
|
|
@@ -506,6 +573,7 @@ function createResolverChainClient(options) {
|
|
|
506
573
|
vendor: adapter.id,
|
|
507
574
|
challenge_kind: challenge.kind,
|
|
508
575
|
client_profile: options.clientProfile,
|
|
576
|
+
resolver_identity_source: identityResolution.userAgentSource,
|
|
509
577
|
},
|
|
510
578
|
onError(error) {
|
|
511
579
|
return error instanceof ResolverVendorUnavailableError
|
|
@@ -518,9 +586,9 @@ function createResolverChainClient(options) {
|
|
|
518
586
|
resolverChallengeIsCacheable(challenge) &&
|
|
519
587
|
solution.form === "cookies" &&
|
|
520
588
|
solutionExpiryMs(solution) !== undefined) {
|
|
521
|
-
const issuingIdentity = adapter.getIssuingIdentity?.(solution,
|
|
589
|
+
const issuingIdentity = adapter.getIssuingIdentity?.(solution, identity, challenge) ??
|
|
522
590
|
resolverChallengeIssuingIdentity(challenge, {
|
|
523
|
-
...(
|
|
591
|
+
...(identity ? { proxyUrl: identity.proxyUrl } : {}),
|
|
524
592
|
userAgent: solution.userAgent,
|
|
525
593
|
});
|
|
526
594
|
if (issuingIdentity) {
|
|
@@ -553,7 +621,7 @@ export function createResolverClient(options) {
|
|
|
553
621
|
unavailableReason: options.unavailableReason,
|
|
554
622
|
cache: options.cache,
|
|
555
623
|
identity: options.identity,
|
|
556
|
-
|
|
624
|
+
proxyIntent: options.proxyIntent,
|
|
557
625
|
transport: options.transport,
|
|
558
626
|
createTransport: options.createTransport,
|
|
559
627
|
clientProfile: options.clientProfile,
|
|
@@ -620,7 +688,7 @@ function createResolverClientFromEnvInternal(config, env, options, adapterFactor
|
|
|
620
688
|
};
|
|
621
689
|
}),
|
|
622
690
|
cache: options.cache,
|
|
623
|
-
|
|
691
|
+
proxyIntent: options.proxyIntent,
|
|
624
692
|
identityScope: options.identityScope,
|
|
625
693
|
transport: options.transport,
|
|
626
694
|
createTransport: options.createTransport,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BrowserProfile, EmulationOS } from "wreq-js";
|
|
2
2
|
import type { ProxyResolutionOptions } from "../config/loader.js";
|
|
3
3
|
import type { StealthClient, StealthResponse } from "../types.js";
|
|
4
|
+
export declare const DEFAULT_PROFILE = "chrome-146";
|
|
4
5
|
export type StealthClientOptions = ProxyResolutionOptions & {
|
|
5
6
|
warn?: (message: string) => void;
|
|
6
7
|
/**
|
package/dist/runtime/stealth.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createProxyAuthIpDeniedError, createProxyEdgeAuthRejectedError, createP
|
|
|
7
7
|
import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, normalizeProxyTransportRetryOptions, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy.js";
|
|
8
8
|
import { evaluateRedirectHop, isRedirectStatus, nextRedirectMethod, resolveRedirectUrl, } from "./redirects.js";
|
|
9
9
|
import { isSensitiveKey, normalizeSensitiveParams, redactSensitiveError, redactSensitiveRequestError, redactSensitiveText, redactUrlQueryParams, serializeRequestUrl, } from "./request-options.js";
|
|
10
|
-
const DEFAULT_PROFILE = "chrome-146";
|
|
10
|
+
export const DEFAULT_PROFILE = "chrome-146";
|
|
11
11
|
const MISSING_PROXY_WARNING = "[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
|
|
12
12
|
const MAX_POLICY_PROXY_POOL_REFRESHES = 1;
|
|
13
13
|
const PROXY_CONNECT_FAILURE_CODE = "proxy_connect_failed";
|
|
@@ -358,6 +358,7 @@ function createProviderContext(provider, request, operationId, options, state =
|
|
|
358
358
|
const baseUrl = getProviderBaseUrl(provider);
|
|
359
359
|
const stealthBaseUrl = getProviderStealthBaseUrl(provider);
|
|
360
360
|
const stealthProfile = getProviderStealthProfile(provider);
|
|
361
|
+
const proxyPolicy = resolveNativeProxyPolicy(provider);
|
|
361
362
|
const proxyClientOptions = {
|
|
362
363
|
upstream: { proxy: provider.proxy },
|
|
363
364
|
affinityKey: resolveProviderProxyAffinityKey(provider, request, operationId),
|
|
@@ -444,7 +445,15 @@ function createProviderContext(provider, request, operationId, options, state =
|
|
|
444
445
|
allowedHosts: provider.allowedHosts,
|
|
445
446
|
cache,
|
|
446
447
|
identityScope: resolverIdentityScope,
|
|
447
|
-
|
|
448
|
+
...(proxyPolicy
|
|
449
|
+
? {
|
|
450
|
+
proxyIntent: {
|
|
451
|
+
mode: proxyPolicy.mode,
|
|
452
|
+
...proxyClientOptions,
|
|
453
|
+
...(stealthProfile ? { userAgent: stealthProfile.userAgent } : {}),
|
|
454
|
+
},
|
|
455
|
+
}
|
|
456
|
+
: {}),
|
|
448
457
|
}), signal)
|
|
449
458
|
: bindResolverSignalWithoutRuntime(options.resolver ??
|
|
450
459
|
createUnsupportedResolverClient("Provider does not declare resolver capability"), signal),
|
|
@@ -487,6 +496,7 @@ function createAuthFlowContext(provider, request, options, signal) {
|
|
|
487
496
|
const baseUrl = getProviderBaseUrl(provider);
|
|
488
497
|
const stealthBaseUrl = getProviderStealthBaseUrl(provider);
|
|
489
498
|
const stealthProfile = getProviderStealthProfile(provider);
|
|
499
|
+
const proxyPolicy = resolveNativeProxyPolicy(provider);
|
|
490
500
|
const contextData = request.context ?? {};
|
|
491
501
|
const flowContextStore = createFlowContextStore(provider.context?.keys ?? Object.keys(contextData), contextData);
|
|
492
502
|
const proxyClientOptions = {
|
|
@@ -559,7 +569,15 @@ function createAuthFlowContext(provider, request, options, signal) {
|
|
|
559
569
|
allowedHosts: provider.allowedHosts,
|
|
560
570
|
cache,
|
|
561
571
|
identityScope: resolverIdentityScope,
|
|
562
|
-
|
|
572
|
+
...(proxyPolicy
|
|
573
|
+
? {
|
|
574
|
+
proxyIntent: {
|
|
575
|
+
mode: proxyPolicy.mode,
|
|
576
|
+
...proxyClientOptions,
|
|
577
|
+
...(stealthProfile ? { userAgent: stealthProfile.userAgent } : {}),
|
|
578
|
+
},
|
|
579
|
+
}
|
|
580
|
+
: {}),
|
|
563
581
|
}), signal)
|
|
564
582
|
: bindResolverSignalWithoutRuntime(options.resolver ??
|
|
565
583
|
createUnsupportedResolverClient("Provider does not declare resolver capability"), signal),
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { describeTransform, snapshotTransform, toMatchShape } from "./helpers.js";
|
|
2
|
+
export { resetProviderCacheForTests } from "../runtime/cache.js";
|
|
2
3
|
export { runStandardTests, type StandardTestsManifest, type StandardTestsOptions, type StandardTestsResult, type StandardTestsUpstreamCall, type StandardTestsUpstreamResponse, type StandardTestsUpstreamStub, } from "./run.js";
|
package/dist/testing/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.2.0-beta.
|
|
2
|
+
"version": "2.2.0-beta.29",
|
|
3
3
|
"name": "@apifuse/provider-sdk",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
@@ -77,9 +77,9 @@
|
|
|
77
77
|
"default": "./dist/runtime/prevalidate.js"
|
|
78
78
|
},
|
|
79
79
|
"./runtime/resolver": {
|
|
80
|
-
"types": "./dist/runtime/resolver.d.ts",
|
|
81
|
-
"import": "./dist/runtime/resolver.js",
|
|
82
|
-
"default": "./dist/runtime/resolver.js"
|
|
80
|
+
"types": "./dist/runtime/resolver-public.d.ts",
|
|
81
|
+
"import": "./dist/runtime/resolver-public.js",
|
|
82
|
+
"default": "./dist/runtime/resolver-public.js"
|
|
83
83
|
},
|
|
84
84
|
"./runtime/stealth": {
|
|
85
85
|
"types": "./dist/runtime/stealth.d.ts",
|
package/src/auth.ts
CHANGED
|
@@ -296,6 +296,25 @@ export interface DefineCredentialsAuthOptions<
|
|
|
296
296
|
):
|
|
297
297
|
| CredentialsAuthLoginResult<TCredentialKeys, keyof TChallenges & string>
|
|
298
298
|
| Promise<CredentialsAuthLoginResult<TCredentialKeys, keyof TChallenges & string>>;
|
|
299
|
+
/**
|
|
300
|
+
* Optional re-mint of an expired session from the stored credential, wired
|
|
301
|
+
* to `auth.flow.refresh`.
|
|
302
|
+
*
|
|
303
|
+
* Credential-auth upstreams routinely invalidate a session well before the
|
|
304
|
+
* `expiresAt` the provider advertised, which leaves every operation failing
|
|
305
|
+
* with a reauth error until a human repeats the whole interactive login.
|
|
306
|
+
* Implement this to re-establish the session from what is already stored on
|
|
307
|
+
* the connection; the result is resolved exactly like `login`, so it may
|
|
308
|
+
* also raise a challenge when the upstream demands one. Omit it when the
|
|
309
|
+
* upstream has no non-interactive path and re-authentication genuinely
|
|
310
|
+
* requires the user.
|
|
311
|
+
*/
|
|
312
|
+
refresh?(
|
|
313
|
+
ctx: FlowContext,
|
|
314
|
+
input: Partial<CredentialsAuthInput<TFields>>,
|
|
315
|
+
):
|
|
316
|
+
| CredentialsAuthLoginResult<TCredentialKeys, keyof TChallenges & string>
|
|
317
|
+
| Promise<CredentialsAuthLoginResult<TCredentialKeys, keyof TChallenges & string>>;
|
|
299
318
|
}
|
|
300
319
|
|
|
301
320
|
export interface DefinedCredentialsAuth {
|
|
@@ -376,6 +395,27 @@ function normalizeInput<TFields extends CredentialsAuthFields>(
|
|
|
376
395
|
return result as CredentialsAuthInput<TFields>;
|
|
377
396
|
}
|
|
378
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Refresh variant of {@link normalizeInput}. `login` coerces every declared
|
|
400
|
+
* field to a string because the interactive turn has already enforced that they
|
|
401
|
+
* are present; refresh runs with no user present, so an absent field is omitted
|
|
402
|
+
* rather than turned into an empty string. That keeps "the user did not supply
|
|
403
|
+
* this" distinguishable from "the user supplied an empty value".
|
|
404
|
+
*/
|
|
405
|
+
function normalizePartialInput<TFields extends CredentialsAuthFields>(
|
|
406
|
+
fields: TFields,
|
|
407
|
+
input: Record<string, unknown> | undefined,
|
|
408
|
+
): Partial<CredentialsAuthInput<TFields>> {
|
|
409
|
+
const result: Record<string, string> = {};
|
|
410
|
+
for (const name of Object.keys(fields)) {
|
|
411
|
+
const value = input?.[name];
|
|
412
|
+
if (typeof value === "string") {
|
|
413
|
+
result[name] = value;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return result as Partial<CredentialsAuthInput<TFields>>;
|
|
417
|
+
}
|
|
418
|
+
|
|
379
419
|
function assertCredentialKeys<TCredentialKeys extends readonly string[]>(
|
|
380
420
|
credentialKeys: TCredentialKeys,
|
|
381
421
|
credential: Record<string, unknown>,
|
|
@@ -716,6 +756,44 @@ export function defineCredentialsAuth<
|
|
|
716
756
|
completeTurnId,
|
|
717
757
|
);
|
|
718
758
|
},
|
|
759
|
+
// Only advertise refresh when the provider implements it: the
|
|
760
|
+
// protocol treats the hook's presence as "this connection can be
|
|
761
|
+
// re-established without the user", and exposing a stub that
|
|
762
|
+
// cannot actually re-mint would turn a recoverable expiry into a
|
|
763
|
+
// silent failure.
|
|
764
|
+
...(options.refresh
|
|
765
|
+
? {
|
|
766
|
+
refresh: async (ctx: FlowContext, rawInput?: Record<string, unknown>) => {
|
|
767
|
+
// A pending challenge belongs to the interactive flow that
|
|
768
|
+
// raised it; finish it there rather than restarting.
|
|
769
|
+
const pending = getPendingChallenge(ctx);
|
|
770
|
+
if (pending) {
|
|
771
|
+
return await continuePendingChallenge(
|
|
772
|
+
ctx,
|
|
773
|
+
options.credentialKeys,
|
|
774
|
+
challenges,
|
|
775
|
+
pending,
|
|
776
|
+
rawInput,
|
|
777
|
+
completeTurnId,
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Refresh runs without user input, so fields are optional
|
|
782
|
+
// here — unlike `continue`, missing ones are not a retry.
|
|
783
|
+
const result = await options.refresh!(
|
|
784
|
+
ctx,
|
|
785
|
+
normalizePartialInput(options.fields, rawInput),
|
|
786
|
+
);
|
|
787
|
+
return await resolveAuthResult(
|
|
788
|
+
ctx,
|
|
789
|
+
options.credentialKeys,
|
|
790
|
+
challenges,
|
|
791
|
+
result,
|
|
792
|
+
completeTurnId,
|
|
793
|
+
);
|
|
794
|
+
},
|
|
795
|
+
}
|
|
796
|
+
: {}),
|
|
719
797
|
},
|
|
720
798
|
},
|
|
721
799
|
credential: {
|
package/src/config/loader.ts
CHANGED
|
@@ -103,8 +103,11 @@ export type SmartproxyAllocatorBodyClass =
|
|
|
103
103
|
| "text_without_proxies"
|
|
104
104
|
| "usable_proxy_endpoints";
|
|
105
105
|
|
|
106
|
+
export type ProxyUserAgentSource = "declared" | "defaulted";
|
|
107
|
+
|
|
106
108
|
export type ProxyResolutionTelemetryEvent = {
|
|
107
109
|
provider: ProxyVendorName;
|
|
110
|
+
userAgentSource?: ProxyUserAgentSource;
|
|
108
111
|
protocol?: ProxyProtocol;
|
|
109
112
|
cacheStatus: ProxyCacheStatus;
|
|
110
113
|
cacheHit: boolean;
|