@apifuse/provider-sdk 2.2.0-beta.24 → 2.2.0-beta.26
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/AUTHORING.md +7 -6
- package/CHANGELOG.md +9 -1
- package/README.md +3 -3
- package/bin/apifuse-check.ts +62 -3
- package/bin/apifuse-pack-check.ts +8 -2
- package/bin/apifuse-pack-smoke.ts +43 -2
- package/bin/apifuse-pack-types.ts +58 -0
- package/bin/apifuse-submit-check.ts +15 -2
- package/dist/auth.js +29 -0
- package/dist/cli/templates/provider/README.md.tpl +4 -4
- package/dist/contract-serialization.d.ts +20 -1
- package/dist/contract-serialization.js +583 -8
- package/dist/contract.d.ts +2 -0
- package/dist/contract.js +9 -5
- package/dist/declaration-validation.d.ts +23 -0
- package/dist/declaration-validation.js +159 -0
- package/dist/define.d.ts +1 -1
- package/dist/define.js +13 -2
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -3
- package/dist/lint.js +85 -3
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +1 -1
- package/dist/runtime/cache.d.ts +1 -0
- package/dist/runtime/cache.js +169 -15
- package/dist/runtime/resolver-vendors/bindings.d.ts +42 -2
- package/dist/runtime/resolver-vendors/bindings.js +31 -6
- package/dist/runtime/resolver-vendors/browser.d.ts +3 -7
- package/dist/runtime/resolver-vendors/browser.js +7 -22
- package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
- package/dist/runtime/resolver-vendors/hosts.js +33 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +23 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +264 -0
- package/dist/runtime/resolver-vendors/types.d.ts +44 -3
- package/dist/runtime/resolver-vendors/types.js +10 -0
- package/dist/runtime/resolver.d.ts +17 -2
- package/dist/runtime/resolver.js +237 -15
- package/dist/runtime/stealth.d.ts +26 -4
- package/dist/runtime/stealth.js +224 -114
- package/dist/schema.d.ts +63 -0
- package/dist/schema.js +808 -8
- package/dist/server/serve.js +8 -0
- package/dist/stealth/profiles.js +16 -7
- package/dist/types.d.ts +37 -4
- package/package.json +2 -2
- package/src/auth.ts +40 -0
- package/src/cli/templates/provider/README.md.tpl +4 -4
- package/src/contract-serialization.ts +857 -8
- package/src/contract.ts +16 -5
- package/src/declaration-validation.ts +202 -0
- package/src/define.ts +23 -2
- package/src/index.ts +13 -0
- package/src/lint.ts +98 -3
- package/src/provider.ts +10 -0
- package/src/runtime/cache.ts +189 -14
- package/src/runtime/resolver-vendors/bindings.ts +40 -15
- package/src/runtime/resolver-vendors/browser.ts +9 -31
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +366 -0
- package/src/runtime/resolver-vendors/types.ts +54 -0
- package/src/runtime/resolver.ts +304 -24
- package/src/runtime/stealth.ts +317 -136
- package/src/schema.ts +1060 -9
- package/src/server/serve.ts +8 -0
- package/src/stealth/profiles.ts +17 -7
- package/src/types.ts +39 -6
package/src/runtime/resolver.ts
CHANGED
|
@@ -11,15 +11,20 @@ import type {
|
|
|
11
11
|
ResolverContext,
|
|
12
12
|
} from "../types.js";
|
|
13
13
|
import {
|
|
14
|
+
resolverChallengeAllowsDirectCache,
|
|
15
|
+
resolverChallengeIsCacheable,
|
|
14
16
|
resolverChallengeIsIdentityScoped,
|
|
15
17
|
resolverChallengeIssuingIdentity,
|
|
16
18
|
} from "./resolver-vendors/bindings.js";
|
|
17
19
|
import { createBrowserResolverVendorAdapter } from "./resolver-vendors/browser.js";
|
|
20
|
+
import { assertResolverHostAllowed } from "./resolver-vendors/hosts.js";
|
|
21
|
+
import { createTwoCaptchaResolverVendorAdapter } from "./resolver-vendors/twocaptcha.js";
|
|
18
22
|
import {
|
|
19
23
|
RESOLVER_VENDOR_CAPABILITIES,
|
|
20
24
|
type ResolverIdentity,
|
|
21
25
|
type ResolverIssuingIdentity,
|
|
22
26
|
type ResolverVendorAdapter,
|
|
27
|
+
type ResolverVendorTransport,
|
|
23
28
|
ResolverVendorUnavailableError,
|
|
24
29
|
type ResolverVendorUnavailableReason,
|
|
25
30
|
resolverVendorSupports,
|
|
@@ -50,6 +55,13 @@ type ResolvedResolverVendor =
|
|
|
50
55
|
type ResolverChainAttempt = {
|
|
51
56
|
readonly vendor: ProviderResolverVendor;
|
|
52
57
|
readonly reason: ResolverVendorUnavailableReason;
|
|
58
|
+
readonly cause?: {
|
|
59
|
+
readonly name: string;
|
|
60
|
+
readonly message: string;
|
|
61
|
+
};
|
|
62
|
+
readonly upstreamHost?: string;
|
|
63
|
+
readonly phase?: string;
|
|
64
|
+
readonly round?: number;
|
|
53
65
|
};
|
|
54
66
|
|
|
55
67
|
type ResolverChainClient = ResolverContext & {
|
|
@@ -65,6 +77,14 @@ export interface ResolverRuntimeOptions {
|
|
|
65
77
|
readonly cache?: ProviderCache;
|
|
66
78
|
/** Server-owned context/proxy scope used only for identity-bound cache entries. */
|
|
67
79
|
readonly identityScope?: string;
|
|
80
|
+
/** SDK-owned transport already bound to the resolved proxy lease and client profile. */
|
|
81
|
+
readonly transport?: ResolverVendorTransport;
|
|
82
|
+
/** Creates an SDK-owned transport bound to the declared profile and server-owned scope. */
|
|
83
|
+
readonly createTransport?: (input: {
|
|
84
|
+
readonly clientProfile?: string;
|
|
85
|
+
/** Server-owned proxy/context scope; the SDK never accepts a caller-built identity. */
|
|
86
|
+
readonly identityScope?: string;
|
|
87
|
+
}) => ResolverVendorTransport;
|
|
68
88
|
}
|
|
69
89
|
|
|
70
90
|
type CachedResolverSolution = {
|
|
@@ -86,6 +106,42 @@ const RESOLVER_SOLUTION_INDEX_CACHE_NAMESPACE = "resolver-solution-index";
|
|
|
86
106
|
const MIN_RESOLVER_CACHE_TTL_MS = 1_000;
|
|
87
107
|
const resolverCaches = new WeakMap<object, ProviderCache | null>();
|
|
88
108
|
const solutionIssuerDigests = new WeakMap<object, string>();
|
|
109
|
+
const SAFE_CAUSE_MESSAGE_WORDS: ReadonlySet<string> = new Set([
|
|
110
|
+
"abort",
|
|
111
|
+
"aborted",
|
|
112
|
+
"at",
|
|
113
|
+
"closed",
|
|
114
|
+
"connect",
|
|
115
|
+
"connection",
|
|
116
|
+
"dns",
|
|
117
|
+
"during",
|
|
118
|
+
"econnrefused",
|
|
119
|
+
"econnreset",
|
|
120
|
+
"error",
|
|
121
|
+
"etimedout",
|
|
122
|
+
"failed",
|
|
123
|
+
"failure",
|
|
124
|
+
"fetch",
|
|
125
|
+
"from",
|
|
126
|
+
"get",
|
|
127
|
+
"lookup",
|
|
128
|
+
"network",
|
|
129
|
+
"post",
|
|
130
|
+
"reading",
|
|
131
|
+
"refused",
|
|
132
|
+
"request",
|
|
133
|
+
"reset",
|
|
134
|
+
"response",
|
|
135
|
+
"socket",
|
|
136
|
+
"timed",
|
|
137
|
+
"timeout",
|
|
138
|
+
"tls",
|
|
139
|
+
"to",
|
|
140
|
+
"unavailable",
|
|
141
|
+
"upstream",
|
|
142
|
+
"while",
|
|
143
|
+
"writing",
|
|
144
|
+
]);
|
|
89
145
|
|
|
90
146
|
export const RESOLVER_INSTRUMENTATION_METADATA = Symbol.for(
|
|
91
147
|
"@apifuse/provider-sdk/runtime/resolver-instrumentation-metadata",
|
|
@@ -96,15 +152,20 @@ export type ResolverInstrumentationMetadata = {
|
|
|
96
152
|
readonly traceRecorder: TraceRecorder;
|
|
97
153
|
};
|
|
98
154
|
|
|
99
|
-
type ResolverAdapterFactory = (
|
|
155
|
+
export type ResolverAdapterFactory = (
|
|
100
156
|
configuration: string,
|
|
101
157
|
timeoutMs: number,
|
|
102
158
|
allowedHosts: readonly string[],
|
|
103
159
|
) => ResolverVendorAdapter;
|
|
104
160
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
161
|
+
const resolverAdapterRegistry: Partial<Record<ProviderResolverVendor, ResolverAdapterFactory>> = {
|
|
162
|
+
"2captcha"(configuration, timeoutMs, allowedHosts) {
|
|
163
|
+
return createTwoCaptchaResolverVendorAdapter({
|
|
164
|
+
allowedHosts,
|
|
165
|
+
apiKey: configuration,
|
|
166
|
+
timeoutMs,
|
|
167
|
+
});
|
|
168
|
+
},
|
|
108
169
|
browser(configuration, timeoutMs, allowedHosts) {
|
|
109
170
|
return createBrowserResolverVendorAdapter({
|
|
110
171
|
allowedHosts,
|
|
@@ -114,10 +175,29 @@ export const RESOLVER_ADAPTER_REGISTRY: Partial<
|
|
|
114
175
|
},
|
|
115
176
|
};
|
|
116
177
|
|
|
178
|
+
export const RESOLVER_ADAPTER_REGISTRY: Partial<
|
|
179
|
+
Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>
|
|
180
|
+
> = resolverAdapterRegistry;
|
|
181
|
+
|
|
182
|
+
export function swapResolverAdapterFactoryForTests(
|
|
183
|
+
vendor: ProviderResolverVendor,
|
|
184
|
+
factory: ResolverAdapterFactory | undefined,
|
|
185
|
+
): () => void {
|
|
186
|
+
const original = resolverAdapterRegistry[vendor];
|
|
187
|
+
if (factory === undefined) delete resolverAdapterRegistry[vendor];
|
|
188
|
+
else resolverAdapterRegistry[vendor] = factory;
|
|
189
|
+
let restored = false;
|
|
190
|
+
return () => {
|
|
191
|
+
if (restored) return;
|
|
192
|
+
restored = true;
|
|
193
|
+
if (original === undefined) delete resolverAdapterRegistry[vendor];
|
|
194
|
+
else resolverAdapterRegistry[vendor] = original;
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
117
198
|
// This is the sole allowlist for declared vendors whose registry entry may be absent.
|
|
118
199
|
// Remove a vendor here when its adapter is registered.
|
|
119
200
|
const KNOWN_UNIMPLEMENTED_RESOLVER_VENDORS: ReadonlySet<ProviderResolverVendor> = new Set([
|
|
120
|
-
"2captcha",
|
|
121
201
|
"capsolver",
|
|
122
202
|
"capmonster",
|
|
123
203
|
"custom",
|
|
@@ -178,8 +258,9 @@ function createAdapter(
|
|
|
178
258
|
vendor: ResolvedResolverVendor,
|
|
179
259
|
timeoutMs: number,
|
|
180
260
|
allowedHosts: readonly string[],
|
|
261
|
+
adapterFactories: Partial<Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>>,
|
|
181
262
|
): ResolverVendorAdapter {
|
|
182
|
-
const factory =
|
|
263
|
+
const factory = adapterFactories[vendor.vendor];
|
|
183
264
|
if (!factory && !KNOWN_UNIMPLEMENTED_RESOLVER_VENDORS.has(vendor.vendor)) {
|
|
184
265
|
throw new Error(
|
|
185
266
|
`Resolver adapter factory is missing for implemented vendor "${vendor.vendor}"`,
|
|
@@ -216,6 +297,135 @@ function throwExhausted(attempts: readonly ResolverChainAttempt[]): never {
|
|
|
216
297
|
});
|
|
217
298
|
}
|
|
218
299
|
|
|
300
|
+
function assertClientProfileTransportContract(
|
|
301
|
+
clientProfile: string | undefined,
|
|
302
|
+
transport: ResolverVendorTransport | undefined,
|
|
303
|
+
): void {
|
|
304
|
+
if (clientProfile === undefined || transport === undefined) return;
|
|
305
|
+
throw new ProviderError(
|
|
306
|
+
`Resolver client profile "${clientProfile}" cannot be applied to a pre-bound transport`,
|
|
307
|
+
{
|
|
308
|
+
code: "RESOLVER_CLIENT_PROFILE_TRANSPORT_CONFLICT",
|
|
309
|
+
fix: "Remove the pre-bound transport and provide createTransport({ clientProfile, identityScope }) so the SDK can apply the provider-declared profile.",
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function adapterRequiresTransport(
|
|
315
|
+
adapter: ResolverVendorAdapter,
|
|
316
|
+
kind: ProviderChallengeKind,
|
|
317
|
+
): boolean {
|
|
318
|
+
return typeof adapter.requiresTransport === "function"
|
|
319
|
+
? adapter.requiresTransport(kind)
|
|
320
|
+
: adapter.requiresTransport === true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function sanitizeDiagnosticUrl(rawUrl: string): string {
|
|
324
|
+
try {
|
|
325
|
+
const parsed = new URL(rawUrl);
|
|
326
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
327
|
+
} catch {
|
|
328
|
+
return "[REDACTED_URL]";
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function sanitizeCauseMessage(message: string): string {
|
|
333
|
+
const withoutUrls = message.replace(/\b[a-z][a-z\d+.-]*:\/\/[^\s"'<>]+/gi, sanitizeDiagnosticUrl);
|
|
334
|
+
const withoutAssignments = withoutUrls.replace(/(?:^|\s)[^\s=]+=[^\s]*/g, " [REDACTED]");
|
|
335
|
+
const safeTokens = withoutAssignments
|
|
336
|
+
.replaceAll("\r", " ")
|
|
337
|
+
.replaceAll("\n", " ")
|
|
338
|
+
.split(/\s+/)
|
|
339
|
+
.filter(Boolean)
|
|
340
|
+
.map((token) => {
|
|
341
|
+
if (token === "[REDACTED]" || /^[a-z][a-z\d+.-]*:\/\/[^\s]+$/i.test(token)) return token;
|
|
342
|
+
const word = token.replace(/^[^a-z\d]+|[^a-z\d]+$/gi, "");
|
|
343
|
+
return word.length > 0 && word.length <= 32 && SAFE_CAUSE_MESSAGE_WORDS.has(word.toLowerCase())
|
|
344
|
+
? token
|
|
345
|
+
: "[REDACTED]";
|
|
346
|
+
});
|
|
347
|
+
return safeTokens
|
|
348
|
+
.filter((token, index) => token !== "[REDACTED]" || safeTokens[index - 1] !== token)
|
|
349
|
+
.join(" ")
|
|
350
|
+
.slice(0, 512);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function restrictResolverTransport(
|
|
354
|
+
transport: ResolverVendorTransport,
|
|
355
|
+
allowedHosts: readonly string[],
|
|
356
|
+
): ResolverVendorTransport {
|
|
357
|
+
return {
|
|
358
|
+
async fetch(url, init) {
|
|
359
|
+
// Empty declarations remain deny-by-default, matching the adapter-factory/browser path.
|
|
360
|
+
assertResolverHostAllowed(url, allowedHosts);
|
|
361
|
+
const response = await transport.fetch(url, { ...init, redirect: "manual" });
|
|
362
|
+
const hasLocationHeader = Object.keys(response.headers).some(
|
|
363
|
+
(name) => name.toLowerCase() === "location",
|
|
364
|
+
);
|
|
365
|
+
if (response.status >= 300 && response.status < 400 && hasLocationHeader) {
|
|
366
|
+
throw new ProviderError("Resolver transport refused a redirect response", {
|
|
367
|
+
code: "RESOLVER_HOST_NOT_ALLOWED",
|
|
368
|
+
fix: "Use a non-redirecting http or https URL on a host declared in allowedHosts.",
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return response;
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function safeCause(error: ResolverVendorUnavailableError): ResolverChainAttempt["cause"] {
|
|
377
|
+
if (error.cause === undefined) return undefined;
|
|
378
|
+
const cause = error.cause;
|
|
379
|
+
const rawName = cause instanceof Error ? cause.name : "Error";
|
|
380
|
+
return {
|
|
381
|
+
name: /^[a-z\d_.:-]{1,64}$/i.test(rawName) ? rawName : "Error",
|
|
382
|
+
message: sanitizeCauseMessage(cause instanceof Error ? cause.message : String(cause)),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function safeUpstreamHost(host: string | undefined): string | undefined {
|
|
387
|
+
if (host === undefined) return undefined;
|
|
388
|
+
const trimmed = host.trim();
|
|
389
|
+
if (/^[a-z\d.-]+$/i.test(trimmed)) return trimmed.toLowerCase();
|
|
390
|
+
try {
|
|
391
|
+
return new URL(trimmed).hostname.toLowerCase();
|
|
392
|
+
} catch {
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function safePhase(phase: string | undefined): string | undefined {
|
|
398
|
+
return phase !== undefined && /^[a-z\d_.:-]{1,64}$/i.test(phase) ? phase : undefined;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function unavailableAttempt(error: ResolverVendorUnavailableError): ResolverChainAttempt {
|
|
402
|
+
const cause = safeCause(error);
|
|
403
|
+
const upstreamHost = safeUpstreamHost(error.upstreamHost);
|
|
404
|
+
const phase = safePhase(error.phase);
|
|
405
|
+
const round =
|
|
406
|
+
Number.isSafeInteger(error.round) && (error.round as number) > 0 ? error.round : undefined;
|
|
407
|
+
return {
|
|
408
|
+
vendor: error.vendor,
|
|
409
|
+
reason: error.reason,
|
|
410
|
+
...(cause ? { cause } : {}),
|
|
411
|
+
...(upstreamHost ? { upstreamHost } : {}),
|
|
412
|
+
...(phase ? { phase } : {}),
|
|
413
|
+
...(round !== undefined ? { round } : {}),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function unavailableSpanAttributes(error: ResolverVendorUnavailableError): Record<string, unknown> {
|
|
418
|
+
const attempt = unavailableAttempt(error);
|
|
419
|
+
return {
|
|
420
|
+
unavailability_reason: error.reason,
|
|
421
|
+
cause_name: attempt.cause?.name,
|
|
422
|
+
cause_message: attempt.cause?.message,
|
|
423
|
+
upstream_host: attempt.upstreamHost,
|
|
424
|
+
transport_phase: attempt.phase,
|
|
425
|
+
transport_round: attempt.round,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
219
429
|
function challengeOrigin(challenge: ProviderChallenge): string {
|
|
220
430
|
return new URL(challenge.pageUrl).origin;
|
|
221
431
|
}
|
|
@@ -299,7 +509,7 @@ function isResolverCacheIndex(value: unknown): value is ResolverCacheIndex {
|
|
|
299
509
|
|
|
300
510
|
function solutionExpiryMs(solution: ChallengeSolution): number | undefined {
|
|
301
511
|
if (solution.form !== "cookies") return undefined;
|
|
302
|
-
const expires =
|
|
512
|
+
const expires = solution.expires;
|
|
303
513
|
if (typeof expires !== "number" || !Number.isFinite(expires)) return undefined;
|
|
304
514
|
return expires * 1_000;
|
|
305
515
|
}
|
|
@@ -374,13 +584,14 @@ async function writeResolverCacheIndex(
|
|
|
374
584
|
await cache.set(indexKey, { entries: liveEntries } satisfies ResolverCacheIndex, { ttlMs });
|
|
375
585
|
}
|
|
376
586
|
|
|
377
|
-
async function
|
|
587
|
+
async function cacheResolverSolution(
|
|
378
588
|
cache: ProviderCache,
|
|
379
589
|
challenge: ProviderChallenge,
|
|
380
590
|
solution: ChallengeSolution,
|
|
381
591
|
identity: ResolverIssuingIdentity,
|
|
382
592
|
identityScope: string | undefined,
|
|
383
593
|
): Promise<void> {
|
|
594
|
+
if (!resolverChallengeIsCacheable(challenge)) return;
|
|
384
595
|
const expiresAtMs = solutionExpiryMs(solution);
|
|
385
596
|
const now = Date.now();
|
|
386
597
|
if (expiresAtMs === undefined) return;
|
|
@@ -391,6 +602,13 @@ async function cacheBrowserSolution(
|
|
|
391
602
|
identityScope !== undefined && resolverChallengeIsIdentityScoped(challenge)
|
|
392
603
|
? resolverIdentityScopeDigest(identityScope)
|
|
393
604
|
: undefined;
|
|
605
|
+
if (
|
|
606
|
+
!resolverChallengeAllowsDirectCache(challenge) &&
|
|
607
|
+
scopedDigest === undefined &&
|
|
608
|
+
identity.proxyUrl === undefined
|
|
609
|
+
) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
394
612
|
const issuerDigest = scopedDigest ?? resolverIdentityDigest(identity);
|
|
395
613
|
await cache.set(
|
|
396
614
|
resolverSolutionCacheKey(cache, challenge, issuerDigest),
|
|
@@ -474,7 +692,12 @@ function createResolverChainClient(options: {
|
|
|
474
692
|
readonly cache?: ProviderCache;
|
|
475
693
|
readonly identity?: ResolverIdentity;
|
|
476
694
|
readonly identityScope?: string;
|
|
695
|
+
readonly transport?: ResolverVendorTransport;
|
|
696
|
+
readonly createTransport?: ResolverRuntimeOptions["createTransport"];
|
|
697
|
+
readonly clientProfile?: string;
|
|
698
|
+
readonly allowedHosts?: readonly string[];
|
|
477
699
|
}): ResolverChainClient {
|
|
700
|
+
assertClientProfileTransportContract(options.clientProfile, options.transport);
|
|
478
701
|
const client: ResolverChainClient = {
|
|
479
702
|
async solve(
|
|
480
703
|
challenge: ProviderChallenge,
|
|
@@ -492,7 +715,7 @@ function createResolverChainClient(options: {
|
|
|
492
715
|
const supportingEntries = options.entries.filter((entry) => entry.supports(challenge.kind));
|
|
493
716
|
if (supportingEntries.length === 0) throwUnsupportedKind(challenge.kind);
|
|
494
717
|
signal.throwIfAborted();
|
|
495
|
-
if (options.cache &&
|
|
718
|
+
if (options.cache && resolverChallengeIsCacheable(challenge)) {
|
|
496
719
|
const cached = await findCachedSolution(
|
|
497
720
|
options.cache,
|
|
498
721
|
challenge,
|
|
@@ -506,29 +729,52 @@ function createResolverChainClient(options: {
|
|
|
506
729
|
for (const entry of supportingEntries) {
|
|
507
730
|
const adapter = entry.createAdapter();
|
|
508
731
|
try {
|
|
509
|
-
const solveAttempt = () =>
|
|
510
|
-
adapter
|
|
732
|
+
const solveAttempt = () => {
|
|
733
|
+
const requiresTransport = adapterRequiresTransport(adapter, challenge.kind);
|
|
734
|
+
const unrestrictedTransport =
|
|
735
|
+
options.transport ??
|
|
736
|
+
(requiresTransport
|
|
737
|
+
? options.createTransport?.({
|
|
738
|
+
clientProfile: options.clientProfile,
|
|
739
|
+
identityScope: options.identityScope,
|
|
740
|
+
})
|
|
741
|
+
: undefined);
|
|
742
|
+
if (requiresTransport && unrestrictedTransport === undefined) {
|
|
743
|
+
throw new ResolverVendorUnavailableError(adapter.id, "missing_transport");
|
|
744
|
+
}
|
|
745
|
+
const transport = unrestrictedTransport
|
|
746
|
+
? restrictResolverTransport(unrestrictedTransport, options.allowedHosts ?? [])
|
|
747
|
+
: undefined;
|
|
748
|
+
return adapter.solve(challenge, options.identity, signal, traceRecorder, transport);
|
|
749
|
+
};
|
|
511
750
|
const solution = traceRecorder
|
|
512
751
|
? await traceRecorder.runSpan("resolver.vendor.attempt", solveAttempt, {
|
|
513
752
|
attributes: {
|
|
514
753
|
vendor: adapter.id,
|
|
515
754
|
challenge_kind: challenge.kind,
|
|
755
|
+
client_profile: options.clientProfile,
|
|
516
756
|
},
|
|
517
757
|
onError(error) {
|
|
518
758
|
return error instanceof ResolverVendorUnavailableError
|
|
519
|
-
?
|
|
759
|
+
? unavailableSpanAttributes(error)
|
|
520
760
|
: undefined;
|
|
521
761
|
},
|
|
522
762
|
})
|
|
523
763
|
: await solveAttempt();
|
|
524
|
-
if (
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
764
|
+
if (
|
|
765
|
+
options.cache &&
|
|
766
|
+
resolverChallengeIsCacheable(challenge) &&
|
|
767
|
+
solution.form === "cookies" &&
|
|
768
|
+
solutionExpiryMs(solution) !== undefined
|
|
769
|
+
) {
|
|
770
|
+
const issuingIdentity =
|
|
771
|
+
adapter.getIssuingIdentity?.(solution, options.identity, challenge) ??
|
|
772
|
+
resolverChallengeIssuingIdentity(challenge, {
|
|
773
|
+
...(options.identity ? { proxyUrl: options.identity.proxyUrl } : {}),
|
|
774
|
+
userAgent: solution.userAgent,
|
|
775
|
+
});
|
|
530
776
|
if (issuingIdentity) {
|
|
531
|
-
await
|
|
777
|
+
await cacheResolverSolution(
|
|
532
778
|
options.cache,
|
|
533
779
|
challenge,
|
|
534
780
|
solution,
|
|
@@ -541,7 +787,7 @@ function createResolverChainClient(options: {
|
|
|
541
787
|
} catch (error) {
|
|
542
788
|
signal.throwIfAborted();
|
|
543
789
|
if (!(error instanceof ResolverVendorUnavailableError)) throw error;
|
|
544
|
-
attempts.push(
|
|
790
|
+
attempts.push(unavailableAttempt(error));
|
|
545
791
|
}
|
|
546
792
|
}
|
|
547
793
|
|
|
@@ -558,6 +804,10 @@ export function createResolverClient(options: {
|
|
|
558
804
|
readonly unavailableReason?: string;
|
|
559
805
|
readonly cache?: ProviderCache;
|
|
560
806
|
readonly identity?: ResolverIdentity;
|
|
807
|
+
readonly transport?: ResolverVendorTransport;
|
|
808
|
+
readonly createTransport?: ResolverRuntimeOptions["createTransport"];
|
|
809
|
+
readonly clientProfile?: string;
|
|
810
|
+
readonly allowedHosts?: readonly string[];
|
|
561
811
|
}): ResolverChainClient {
|
|
562
812
|
return createResolverChainClient({
|
|
563
813
|
kinds: options.kinds,
|
|
@@ -569,6 +819,10 @@ export function createResolverClient(options: {
|
|
|
569
819
|
unavailableReason: options.unavailableReason,
|
|
570
820
|
cache: options.cache,
|
|
571
821
|
identity: options.identity,
|
|
822
|
+
transport: options.transport,
|
|
823
|
+
createTransport: options.createTransport,
|
|
824
|
+
clientProfile: options.clientProfile,
|
|
825
|
+
allowedHosts: options.allowedHosts,
|
|
572
826
|
});
|
|
573
827
|
}
|
|
574
828
|
|
|
@@ -626,14 +880,16 @@ export function bindResolverSignal(
|
|
|
626
880
|
return boundResolver;
|
|
627
881
|
}
|
|
628
882
|
|
|
629
|
-
|
|
883
|
+
function createResolverClientFromEnvInternal(
|
|
630
884
|
config: ProviderResolverConfig | undefined,
|
|
631
|
-
env: EnvLike
|
|
632
|
-
options: ResolverRuntimeOptions
|
|
885
|
+
env: EnvLike,
|
|
886
|
+
options: ResolverRuntimeOptions,
|
|
887
|
+
adapterFactories: Partial<Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>>,
|
|
633
888
|
): ResolverContext {
|
|
634
889
|
if (!config) {
|
|
635
890
|
return createUnsupportedResolverClient("Provider does not declare resolver capability");
|
|
636
891
|
}
|
|
892
|
+
assertClientProfileTransportContract(config.clientProfile, options.transport);
|
|
637
893
|
|
|
638
894
|
if (config.vendors.length === 0) {
|
|
639
895
|
return createResolverChainClient({
|
|
@@ -645,6 +901,7 @@ export function createResolverClientFromEnv(
|
|
|
645
901
|
|
|
646
902
|
const timeoutValue = readPositiveIntegerEnv(env, APIFUSE__RESOLVER__TIMEOUT_MS);
|
|
647
903
|
const timeoutMs = timeoutValue === undefined ? DEFAULT_RESOLVER_TIMEOUT_MS : Number(timeoutValue);
|
|
904
|
+
const allowedHosts = [...(options.allowedHosts ?? [])];
|
|
648
905
|
|
|
649
906
|
return createResolverChainClient({
|
|
650
907
|
kinds: config.kinds,
|
|
@@ -658,11 +915,34 @@ export function createResolverClientFromEnv(
|
|
|
658
915
|
createAdapter(
|
|
659
916
|
resolveVendorAvailability(vendor, env),
|
|
660
917
|
timeoutMs,
|
|
661
|
-
|
|
918
|
+
allowedHosts,
|
|
919
|
+
adapterFactories,
|
|
662
920
|
),
|
|
663
921
|
};
|
|
664
922
|
}),
|
|
665
923
|
cache: options.cache,
|
|
666
924
|
identityScope: options.identityScope,
|
|
925
|
+
transport: options.transport,
|
|
926
|
+
createTransport: options.createTransport,
|
|
927
|
+
clientProfile: config.clientProfile,
|
|
928
|
+
allowedHosts,
|
|
667
929
|
});
|
|
668
930
|
}
|
|
931
|
+
|
|
932
|
+
export function createResolverClientFromEnv(
|
|
933
|
+
config: ProviderResolverConfig | undefined,
|
|
934
|
+
env: EnvLike = process.env,
|
|
935
|
+
options: ResolverRuntimeOptions = {},
|
|
936
|
+
): ResolverContext {
|
|
937
|
+
return createResolverClientFromEnvInternal(config, env, options, RESOLVER_ADAPTER_REGISTRY);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/** Internal test seam; deliberately not re-exported from the package root. */
|
|
941
|
+
export function createResolverClientFromEnvForTests(
|
|
942
|
+
config: ProviderResolverConfig | undefined,
|
|
943
|
+
env: EnvLike,
|
|
944
|
+
options: ResolverRuntimeOptions,
|
|
945
|
+
adapterFactories: Partial<Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>>,
|
|
946
|
+
): ResolverContext {
|
|
947
|
+
return createResolverClientFromEnvInternal(config, env, options, adapterFactories);
|
|
948
|
+
}
|