@apifuse/provider-sdk 2.2.0-beta.30 → 2.2.0-beta.32
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 +8 -0
- package/dist/error-resolution.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +1 -1
- package/dist/runtime/choice.d.ts +1 -0
- package/dist/runtime/choice.js +96 -21
- package/dist/runtime/resolver-vendors/capsolver.d.ts +24 -0
- package/dist/runtime/resolver-vendors/capsolver.js +402 -0
- package/dist/runtime/resolver.js +8 -1
- package/dist/server/serve-implementation.js +2 -1
- package/package.json +1 -1
- package/src/error-resolution.ts +1 -0
- package/src/index.ts +1 -0
- package/src/provider.ts +1 -0
- package/src/runtime/choice.ts +133 -41
- package/src/runtime/resolver-vendors/capsolver.ts +563 -0
- package/src/runtime/resolver.ts +8 -1
- package/src/server/serve-implementation.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @apifuse/provider-sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0-beta.32
|
|
4
|
+
|
|
5
|
+
- Release candidate for main commit 82f1c0c1de916a40c8c74b1d6470a493b134c0ed.
|
|
6
|
+
|
|
7
|
+
## 2.2.0-beta.31
|
|
8
|
+
|
|
9
|
+
- Release candidate for main commit 94006be0852918867d254dc23d0fa8420a07be9c.
|
|
10
|
+
|
|
3
11
|
## 2.2.0-beta.30
|
|
4
12
|
|
|
5
13
|
- Release candidate for main commit ccf3d56bfcbe9683c6e7ad45dde8e6703467f9e7.
|
package/dist/error-resolution.js
CHANGED
|
@@ -18,6 +18,7 @@ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
|
|
|
18
18
|
"RUNTIME_UNSUPPORTED",
|
|
19
19
|
"PROVIDER_STATE_UNSUPPORTED",
|
|
20
20
|
"CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
|
|
21
|
+
"CHOICE_WORD_ISSUANCE_INVALID",
|
|
21
22
|
"CHOICE_STATE_PAYLOAD_TOO_LARGE",
|
|
22
23
|
"CHOICE_STATE_UNAVAILABLE",
|
|
23
24
|
"CHOICE_CONTEXT_REQUIRED",
|
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export * from "./recipes/rest-api.js";
|
|
|
17
17
|
export { createFlowContext, createScratchpad } from "./runtime/auth-flow.js";
|
|
18
18
|
export type { BrowserClientOptions } from "./runtime/browser.js";
|
|
19
19
|
export { APIFUSE__CACHE__KEY_PEPPER_ENV, createBypassProviderCache, createProviderCache, type ProviderCacheOptions, } from "./runtime/cache.js";
|
|
20
|
-
export { type CreateProviderChoiceContextOptions, createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
|
|
20
|
+
export { type CreateProviderChoiceContextOptions, createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV, } from "./runtime/choice.js";
|
|
21
21
|
export { type CreateCredentialContextOptions, createCredentialContext, } from "./runtime/credential.js";
|
|
22
22
|
export { createEnvContext } from "./runtime/env.js";
|
|
23
23
|
export { executeOperation } from "./runtime/executor.js";
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ export * from "./recipes/gov-api.js";
|
|
|
15
15
|
export * from "./recipes/rest-api.js";
|
|
16
16
|
export { createFlowContext, createScratchpad } from "./runtime/auth-flow.js";
|
|
17
17
|
export { APIFUSE__CACHE__KEY_PEPPER_ENV, createBypassProviderCache, createProviderCache, } from "./runtime/cache.js";
|
|
18
|
-
export { createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
|
|
18
|
+
export { createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV, } from "./runtime/choice.js";
|
|
19
19
|
export { createCredentialContext, } from "./runtime/credential.js";
|
|
20
20
|
export { createEnvContext } from "./runtime/env.js";
|
|
21
21
|
export { executeOperation } from "./runtime/executor.js";
|
package/dist/provider.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { assertFreshProviderChoiceIssuedAt, createProviderChoiceToken, ProviderC
|
|
|
5
5
|
export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, every, } from "./define.js";
|
|
6
6
|
export { AuthError, HttpRedirectError, isProviderError, isSessionExpiredError, isTransportError, ProviderError, SessionExpiredError, TransportError, ValidationError, } from "./errors.js";
|
|
7
7
|
export { getProviderLocalePath, providerLocaleKey, qualifyProviderLocaleKey, } from "./i18n/index.js";
|
|
8
|
-
export { type CreateProviderChoiceContextOptions, createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
|
|
8
|
+
export { type CreateProviderChoiceContextOptions, createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV, } from "./runtime/choice.js";
|
|
9
9
|
export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, collectSensitivePaths, describeKey, field, fields, isSensitiveSchema, redactPayload, type SensitiveFieldKind, type SensitiveFieldOptions, type SensitivePath, sensitive, z, } from "./schema.js";
|
|
10
10
|
export type { AuthAbortData, AuthAbortRetry, AuthFlowTerminalContext, AuthMode, AuthSafeData, AuthSafeJson, FlowContext, HealthCheckAssertionContext, HealthCheckCase, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthScheduleRandomization, HttpRedirectFailureReason, HttpRedirectPolicy, HttpRedirectPolicyMode, HttpRetryOptions, HttpRetrySummary, InferSchemaOutput, NativeContext, NativeNetworkClient, NativeNetworkCloseReason, NativeNetworkConnection, NativeNetworkConnectInput, NativeNetworkConnectOptions, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProviderContext, NativeProxyDrainHandler, NativeProxyEgressInfo, NativeProxyExpiringEvent, NativeProxyExpiringReason, NativeTcpDynamicEgressRule, NativeTcpEgressGrant, NativeTcpEgressRule, NativeTcpPortRange, NativeTcpTlsMode, NativeTlsConnectOptions, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDocMeta, OperationErrorCode, ProviderErrorStatus, OperationInputExample, OperationLifecycle, OperationObservabilityConfig, OperationObservabilitySensitiveConfig, OperationRelationships, OperationRiskClass, OperationSensitivePath, OperationToolRouterMetadata, OperationTransport, ProviderAccessVisibility, ProviderChoiceBindingOptions, ProviderChoiceConsumeMode, ProviderChoiceConsumeResult, ProviderChoiceContext, ProviderChoiceExplicitParseResult, ProviderChoiceIssueOptions, ProviderChoiceParseOptions, ProviderContext, ProviderDefinition, ProviderDeploymentOverrides, ProviderFileRef, ProviderFilesContext, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderProxyPolicy, ProxiedOAuthConfig, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderResolvedFile, ProviderRuntimeState, ProviderStateDurationString, ProviderStateNamespace, ProviderSupportLevel, RedirectRunReason, SchemaLike, SmsOtpMatcherDefinition, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateNamespaceScope, StateValue, StateWriteOptions, } from "./types.js";
|
|
11
11
|
export { NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, } from "./runtime/native-network-errors.js";
|
package/dist/provider.js
CHANGED
|
@@ -4,7 +4,7 @@ export { assertFreshProviderChoiceIssuedAt, createProviderChoiceToken, ProviderC
|
|
|
4
4
|
export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, every, } from "./define.js";
|
|
5
5
|
export { AuthError, HttpRedirectError, isProviderError, isSessionExpiredError, isTransportError, ProviderError, SessionExpiredError, TransportError, ValidationError, } from "./errors.js";
|
|
6
6
|
export { getProviderLocalePath, providerLocaleKey, qualifyProviderLocaleKey, } from "./i18n/index.js";
|
|
7
|
-
export { createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
|
|
7
|
+
export { createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV, } from "./runtime/choice.js";
|
|
8
8
|
export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, collectSensitivePaths, describeKey, field, fields, isSensitiveSchema, redactPayload, sensitive, z, } from "./schema.js";
|
|
9
9
|
export { NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, } from "./runtime/native-network-errors.js";
|
|
10
10
|
export { HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, } from "./types.js";
|
package/dist/runtime/choice.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CredentialContext, EnvContext, ProviderChoiceConsumeMode, ProviderChoiceContext, ProviderRequestContext, ProviderRuntimeState } from "../types.js";
|
|
2
2
|
export declare const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
|
|
3
|
+
export declare const PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_WORD_ISSUANCE";
|
|
3
4
|
export type ProviderChoiceTelemetryEvent = {
|
|
4
5
|
readonly providerId: string;
|
|
5
6
|
readonly purpose: string;
|
package/dist/runtime/choice.js
CHANGED
|
@@ -3,6 +3,7 @@ import { assertFreshProviderChoiceIssuedAt, ProviderChoiceTokenError, } from "..
|
|
|
3
3
|
import { isProviderError, ProviderError } from "../errors.js";
|
|
4
4
|
import { CHOICE_WORDLIST_SIZE, choiceWordAt, HIGH_CHOICE_WORD_COUNT, isChoiceWord, STANDARD_CHOICE_WORD_COUNT, } from "./choice-wordlist.js";
|
|
5
5
|
export const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
|
|
6
|
+
export const PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_WORD_ISSUANCE";
|
|
6
7
|
const PRIMARY_CHOICE_TOKEN_KID = "v1";
|
|
7
8
|
const MANAGED_CHOICE_TOKEN_VERSION = 1;
|
|
8
9
|
const SERVER_STORED_CHOICE_RECORD_VERSION = 1;
|
|
@@ -15,28 +16,53 @@ export function createProviderChoiceContext(options) {
|
|
|
15
16
|
const issuedAtMs = issueOptions.nowMs ?? Date.now();
|
|
16
17
|
const resolvedStorage = resolveIssueStorage(issueOptions.storage, issueOptions.payload);
|
|
17
18
|
if (resolvedStorage.mode === "server") {
|
|
19
|
+
const issuance = resolveChoiceWordIssuance(options.env);
|
|
20
|
+
const keys = hasRequestedChoiceBinding(issueOptions.bind)
|
|
21
|
+
? deriveManagedChoiceKeys({
|
|
22
|
+
masterSecret: resolveMasterSecret(),
|
|
23
|
+
providerId: options.providerId,
|
|
24
|
+
purpose: issueOptions.purpose,
|
|
25
|
+
kid,
|
|
26
|
+
})
|
|
27
|
+
: undefined;
|
|
18
28
|
const binding = hasRequestedChoiceBinding(issueOptions.bind)
|
|
19
29
|
? createChoiceBinding({
|
|
20
|
-
keys:
|
|
21
|
-
masterSecret: resolveMasterSecret(),
|
|
22
|
-
providerId: options.providerId,
|
|
23
|
-
purpose: issueOptions.purpose,
|
|
24
|
-
kid,
|
|
25
|
-
}),
|
|
30
|
+
keys: keys,
|
|
26
31
|
options: issueOptions.bind,
|
|
27
32
|
request: options.request,
|
|
28
33
|
credential: options.credential,
|
|
29
34
|
required: true,
|
|
30
35
|
})
|
|
31
36
|
: undefined;
|
|
32
|
-
|
|
37
|
+
const baseEnvelope = {
|
|
38
|
+
v: MANAGED_CHOICE_TOKEN_VERSION,
|
|
39
|
+
provider_id: options.providerId,
|
|
40
|
+
purpose: issueOptions.purpose,
|
|
41
|
+
issued_at_ms: issuedAtMs,
|
|
42
|
+
ttl_ms: issueOptions.ttlMs,
|
|
43
|
+
binding,
|
|
44
|
+
};
|
|
45
|
+
if (issuance === "legacy") {
|
|
46
|
+
const legacyKeys = keys ??
|
|
47
|
+
deriveManagedChoiceKeys({
|
|
48
|
+
masterSecret: resolveMasterSecret(),
|
|
49
|
+
providerId: options.providerId,
|
|
50
|
+
purpose: issueOptions.purpose,
|
|
51
|
+
kid,
|
|
52
|
+
});
|
|
53
|
+
return issueLegacyServerStoredChoice({
|
|
54
|
+
baseEnvelope,
|
|
55
|
+
issueOptions,
|
|
56
|
+
storage: resolvedStorage.storage,
|
|
57
|
+
contextState: options.state,
|
|
58
|
+
kid,
|
|
59
|
+
keys: legacyKeys,
|
|
60
|
+
issuedAtMs,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return issueWordServerStoredChoice({
|
|
33
64
|
baseEnvelope: {
|
|
34
|
-
|
|
35
|
-
provider_id: options.providerId,
|
|
36
|
-
purpose: issueOptions.purpose,
|
|
37
|
-
issued_at_ms: issuedAtMs,
|
|
38
|
-
ttl_ms: issueOptions.ttlMs,
|
|
39
|
-
binding,
|
|
65
|
+
...baseEnvelope,
|
|
40
66
|
},
|
|
41
67
|
issueOptions,
|
|
42
68
|
storage: resolvedStorage.storage,
|
|
@@ -133,13 +159,7 @@ export function createProviderChoiceContext(options) {
|
|
|
133
159
|
purpose: parseOptions.purpose,
|
|
134
160
|
kid: tokenKid,
|
|
135
161
|
});
|
|
136
|
-
const signedBody = [
|
|
137
|
-
parseOptions.prefix,
|
|
138
|
-
tokenKid,
|
|
139
|
-
encodedIv,
|
|
140
|
-
encryptedPayload,
|
|
141
|
-
authTag,
|
|
142
|
-
].join(".");
|
|
162
|
+
const signedBody = [parseOptions.prefix, tokenKid, encodedIv, encryptedPayload, authTag].join(".");
|
|
143
163
|
assertManagedChoiceSignature({
|
|
144
164
|
signedBody,
|
|
145
165
|
signature,
|
|
@@ -287,6 +307,20 @@ function createLegacyExplicitParseResult(options) {
|
|
|
287
307
|
},
|
|
288
308
|
};
|
|
289
309
|
}
|
|
310
|
+
function resolveChoiceWordIssuance(env) {
|
|
311
|
+
const configured = env?.get(PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV);
|
|
312
|
+
const value = configured?.trim() ?? "";
|
|
313
|
+
if (value === "" || value === "legacy")
|
|
314
|
+
return "legacy";
|
|
315
|
+
if (value === "word")
|
|
316
|
+
return "word";
|
|
317
|
+
throw new ProviderError(`Unsupported provider choice word issuance mode "${value}". Expected "legacy" or "word".`, {
|
|
318
|
+
code: "CHOICE_WORD_ISSUANCE_INVALID",
|
|
319
|
+
category: "input_validation",
|
|
320
|
+
retryable: false,
|
|
321
|
+
details: { env: PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV },
|
|
322
|
+
});
|
|
323
|
+
}
|
|
290
324
|
function resolveChoiceMasterSecret(options) {
|
|
291
325
|
const configured = options.masterSecret ?? options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
|
|
292
326
|
const trimmed = configured?.trim();
|
|
@@ -336,7 +370,7 @@ function encryptManagedChoiceToken(options) {
|
|
|
336
370
|
.digest("base64url");
|
|
337
371
|
return `${signedBody}.${signature}`;
|
|
338
372
|
}
|
|
339
|
-
async function
|
|
373
|
+
async function issueWordServerStoredChoice(options) {
|
|
340
374
|
const serializedPayload = serializeChoicePayload(options.issueOptions.payload);
|
|
341
375
|
const payloadDigest = digestChoicePayload(serializedPayload);
|
|
342
376
|
const namespace = resolveChoiceStateNamespace({
|
|
@@ -386,6 +420,47 @@ async function issueServerStoredChoice(options) {
|
|
|
386
420
|
retryable: false,
|
|
387
421
|
});
|
|
388
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Beta.28-compatible server handle issuance. The state stores the payload and
|
|
425
|
+
* the client receives the existing six-part encrypted envelope whose payload
|
|
426
|
+
* is a server-state handle.
|
|
427
|
+
*/
|
|
428
|
+
async function issueLegacyServerStoredChoice(options) {
|
|
429
|
+
const serializedPayload = serializeChoicePayload(options.issueOptions.payload);
|
|
430
|
+
const payloadBytes = Buffer.byteLength(serializedPayload, "utf8");
|
|
431
|
+
if (payloadBytes > options.storage.maxValueBytes) {
|
|
432
|
+
throw new ProviderError("Provider choice payload exceeds state storage policy.", {
|
|
433
|
+
code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
|
|
434
|
+
category: "input_validation",
|
|
435
|
+
retryable: false,
|
|
436
|
+
details: { maxValueBytes: options.storage.maxValueBytes, payloadBytes },
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
const stateId = `choice_${randomBytes(16).toString("base64url")}`;
|
|
440
|
+
const namespace = resolveChoiceStateNamespace({
|
|
441
|
+
storage: options.storage,
|
|
442
|
+
contextState: options.contextState,
|
|
443
|
+
ttlMs: options.issueOptions.ttlMs,
|
|
444
|
+
});
|
|
445
|
+
await namespace.set(optionsStateKey(stateId), options.issueOptions.payload, {
|
|
446
|
+
ttl: stateTtl(options.storage, options.issueOptions.ttlMs),
|
|
447
|
+
});
|
|
448
|
+
const envelope = {
|
|
449
|
+
...options.baseEnvelope,
|
|
450
|
+
payload: {
|
|
451
|
+
storage: "server",
|
|
452
|
+
state_id: stateId,
|
|
453
|
+
payload_digest: digestChoicePayload(serializedPayload),
|
|
454
|
+
created_at_ms: options.issuedAtMs,
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
return encryptManagedChoiceToken({
|
|
458
|
+
prefix: options.issueOptions.prefix,
|
|
459
|
+
kid: options.kid,
|
|
460
|
+
envelope,
|
|
461
|
+
keys: options.keys,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
389
464
|
async function parseWordServerStoredChoice(options) {
|
|
390
465
|
const storage = resolveParseStorage(options.parseOptions.storage);
|
|
391
466
|
const namespace = resolveChoiceStateNamespace({
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ChallengeSolution, ProviderChallenge } from "../../types.js";
|
|
2
|
+
import type { TraceRecorder } from "../trace.js";
|
|
3
|
+
import { type ResolverIdentity, type ResolverVendorAdapter } from "./types.js";
|
|
4
|
+
type Delay = (ms: number, signal: AbortSignal) => Promise<void>;
|
|
5
|
+
export interface CapsolverResolverVendorOptions {
|
|
6
|
+
readonly apiKey?: string;
|
|
7
|
+
readonly timeoutMs?: number;
|
|
8
|
+
readonly pollIntervalMs?: number;
|
|
9
|
+
readonly allowedHosts: readonly string[];
|
|
10
|
+
readonly fetchImpl?: typeof fetch;
|
|
11
|
+
readonly baseUrl?: string;
|
|
12
|
+
/** Test-only clock override; supplying it disables the real-time deadline timer. */
|
|
13
|
+
readonly now?: () => number;
|
|
14
|
+
/** Test-only delay override used with `now` to exercise polling without sleeping. */
|
|
15
|
+
readonly delay?: Delay;
|
|
16
|
+
}
|
|
17
|
+
export interface CapsolverResolverVendorAdapter extends ResolverVendorAdapter {
|
|
18
|
+
readonly id: "capsolver";
|
|
19
|
+
solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder): Promise<Extract<ChallengeSolution, {
|
|
20
|
+
readonly form: "token";
|
|
21
|
+
}>>;
|
|
22
|
+
}
|
|
23
|
+
export declare function createCapsolverResolverVendorAdapter(options: CapsolverResolverVendorOptions): CapsolverResolverVendorAdapter;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { redactSensitiveText } from "../request-options.js";
|
|
2
|
+
import { assertResolverHostAllowed } from "./hosts.js";
|
|
3
|
+
import { ResolverChallengeVerdictError, ResolverVendorUnavailableError, resolverVendorSupports, } from "./types.js";
|
|
4
|
+
const CAPSOLVER_VENDOR_ID = "capsolver";
|
|
5
|
+
const DEFAULT_CAPSOLVER_BASE_URL = "https://api.capsolver.com";
|
|
6
|
+
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
8
|
+
class CapsolverSolveTimeoutError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super("Capsolver resolver solve budget elapsed");
|
|
11
|
+
this.name = "CapsolverSolveTimeoutError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
class CapsolverUnavailableError extends ResolverVendorUnavailableError {
|
|
15
|
+
errorCode;
|
|
16
|
+
errorDescription;
|
|
17
|
+
responseStatus;
|
|
18
|
+
responseContentType;
|
|
19
|
+
responseLength;
|
|
20
|
+
constructor(reason, options) {
|
|
21
|
+
super(CAPSOLVER_VENDOR_ID, reason, options);
|
|
22
|
+
this.errorCode = options.errorCode;
|
|
23
|
+
this.errorDescription = options.errorDescription;
|
|
24
|
+
this.responseStatus = options.responseStatus;
|
|
25
|
+
this.responseContentType = options.responseContentType;
|
|
26
|
+
this.responseLength = options.responseLength;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
class CapsolverVerdictError extends ResolverChallengeVerdictError {
|
|
30
|
+
phase;
|
|
31
|
+
errorCode;
|
|
32
|
+
errorDescription;
|
|
33
|
+
constructor(options) {
|
|
34
|
+
super(CAPSOLVER_VENDOR_ID, "solve_failed", options);
|
|
35
|
+
this.phase = options.phase;
|
|
36
|
+
this.errorCode = options.errorCode;
|
|
37
|
+
this.errorDescription = options.errorDescription;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function isJsonRecord(value) {
|
|
41
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
function abortReason(signal) {
|
|
44
|
+
return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
45
|
+
}
|
|
46
|
+
function containsSensitiveValue(value, sensitiveValues) {
|
|
47
|
+
const secrets = sensitiveValues.filter((secret) => secret.length > 0);
|
|
48
|
+
if (secrets.length === 0)
|
|
49
|
+
return false;
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
const inspect = (candidate) => {
|
|
52
|
+
if (typeof candidate === "string") {
|
|
53
|
+
return secrets.some((secret) => candidate.includes(secret));
|
|
54
|
+
}
|
|
55
|
+
if (candidate === null || (typeof candidate !== "object" && typeof candidate !== "function")) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (seen.has(candidate))
|
|
59
|
+
return false;
|
|
60
|
+
seen.add(candidate);
|
|
61
|
+
try {
|
|
62
|
+
for (const property of Reflect.ownKeys(candidate)) {
|
|
63
|
+
if (typeof property === "string" && inspect(property))
|
|
64
|
+
return true;
|
|
65
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, property);
|
|
66
|
+
if (!descriptor)
|
|
67
|
+
return true;
|
|
68
|
+
if ("value" in descriptor && inspect(descriptor.value))
|
|
69
|
+
return true;
|
|
70
|
+
if (descriptor.get !== undefined || descriptor.set !== undefined)
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
};
|
|
79
|
+
return inspect(value);
|
|
80
|
+
}
|
|
81
|
+
function safeCauseOptions(error, sensitiveValues) {
|
|
82
|
+
return containsSensitiveValue(error, sensitiveValues) ? {} : { cause: error };
|
|
83
|
+
}
|
|
84
|
+
function sanitizedJsonParseCause() {
|
|
85
|
+
return new SyntaxError("Upstream response failed");
|
|
86
|
+
}
|
|
87
|
+
function raceWithAbort(operation, signal, phase, sensitiveValues = []) {
|
|
88
|
+
if (signal.aborted)
|
|
89
|
+
return Promise.reject(abortReason(signal));
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
92
|
+
const onAbort = () => {
|
|
93
|
+
cleanup();
|
|
94
|
+
reject(abortReason(signal));
|
|
95
|
+
};
|
|
96
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
97
|
+
operation().then((value) => {
|
|
98
|
+
cleanup();
|
|
99
|
+
resolve(value);
|
|
100
|
+
}, (error) => {
|
|
101
|
+
cleanup();
|
|
102
|
+
if (phase === undefined) {
|
|
103
|
+
reject(error);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
reject(new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
107
|
+
...safeCauseOptions(error, sensitiveValues),
|
|
108
|
+
phase,
|
|
109
|
+
}));
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async function abortableDelay(ms, signal) {
|
|
114
|
+
let timer;
|
|
115
|
+
try {
|
|
116
|
+
await raceWithAbort(() => new Promise((resolve) => {
|
|
117
|
+
timer = setTimeout(resolve, ms);
|
|
118
|
+
}), signal);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
if (timer !== undefined)
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function responseErrorFields(payload) {
|
|
126
|
+
return {
|
|
127
|
+
errorId: typeof payload.errorId === "number" ? payload.errorId : undefined,
|
|
128
|
+
errorCode: typeof payload.errorCode === "string" ? payload.errorCode : undefined,
|
|
129
|
+
errorDescription: typeof payload.errorDescription === "string" ? payload.errorDescription : undefined,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function parseCreateTaskResponse(payload) {
|
|
133
|
+
const taskId = payload.taskId;
|
|
134
|
+
return {
|
|
135
|
+
...responseErrorFields(payload),
|
|
136
|
+
taskId: typeof taskId === "string" || typeof taskId === "number" ? taskId : undefined,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function parsePollResultResponse(payload) {
|
|
140
|
+
const solution = isJsonRecord(payload.solution) ? payload.solution : undefined;
|
|
141
|
+
return {
|
|
142
|
+
...responseErrorFields(payload),
|
|
143
|
+
status: typeof payload.status === "string" ? payload.status : undefined,
|
|
144
|
+
solution: solution
|
|
145
|
+
? { token: typeof solution.token === "string" ? solution.token : undefined }
|
|
146
|
+
: undefined,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function isAllocationExhausted(payload) {
|
|
150
|
+
const code = payload.errorCode?.toLowerCase() ?? "";
|
|
151
|
+
const description = payload.errorDescription?.toLowerCase() ?? "";
|
|
152
|
+
return (code === "error_zero_balance" ||
|
|
153
|
+
/(?:insufficient|zero|no|not enough)\s+(?:balance|funds|credit)/u.test(`${code} ${description}`));
|
|
154
|
+
}
|
|
155
|
+
function isNegativeVerdict(payload) {
|
|
156
|
+
return payload.errorCode?.toLowerCase() === "error_captcha_unsolvable";
|
|
157
|
+
}
|
|
158
|
+
function safeVendorDetail(value, sensitiveValues) {
|
|
159
|
+
const redacted = redactSensitiveText(value, sensitiveValues);
|
|
160
|
+
return containsSensitiveValue(redacted, sensitiveValues) ? "[REDACTED]" : redacted;
|
|
161
|
+
}
|
|
162
|
+
function vendorErrorDetails(payload, sensitiveValues) {
|
|
163
|
+
const errorCode = payload.errorCode?.trim();
|
|
164
|
+
const errorDescription = payload.errorDescription?.trim();
|
|
165
|
+
return {
|
|
166
|
+
...(errorCode ? { errorCode: safeVendorDetail(errorCode, sensitiveValues) } : {}),
|
|
167
|
+
...(errorDescription
|
|
168
|
+
? { errorDescription: safeVendorDetail(payload.errorDescription ?? "", sensitiveValues) }
|
|
169
|
+
: {}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function unavailableForPayload(payload, phase, sensitiveValues) {
|
|
173
|
+
const details = vendorErrorDetails(payload, sensitiveValues);
|
|
174
|
+
if (isNegativeVerdict(payload)) {
|
|
175
|
+
return new CapsolverVerdictError({ phase, ...details });
|
|
176
|
+
}
|
|
177
|
+
return new CapsolverUnavailableError(isAllocationExhausted(payload) ? "allocation_exhausted" : "transport_failure", { phase, ...details });
|
|
178
|
+
}
|
|
179
|
+
async function postJson(fetchImpl, url, body, signal, phase, sensitiveValues, parseResponse) {
|
|
180
|
+
const response = await raceWithAbort(() => fetchImpl(url, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: { "content-type": "application/json" },
|
|
183
|
+
body: JSON.stringify(body),
|
|
184
|
+
signal,
|
|
185
|
+
redirect: "error",
|
|
186
|
+
}), signal, phase, sensitiveValues);
|
|
187
|
+
let responseText;
|
|
188
|
+
try {
|
|
189
|
+
responseText = await raceWithAbort(() => response.text(), signal, phase, sensitiveValues);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (error instanceof ResolverVendorUnavailableError)
|
|
193
|
+
throw error;
|
|
194
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
195
|
+
cause: error,
|
|
196
|
+
phase,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
let payload;
|
|
200
|
+
try {
|
|
201
|
+
payload = JSON.parse(responseText);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
const contentType = response.headers.get("content-type");
|
|
205
|
+
throw new CapsolverUnavailableError("transport_failure", {
|
|
206
|
+
cause: sanitizedJsonParseCause(),
|
|
207
|
+
phase,
|
|
208
|
+
responseStatus: response.status,
|
|
209
|
+
responseContentType: contentType
|
|
210
|
+
? safeVendorDetail(contentType.slice(0, 128), sensitiveValues)
|
|
211
|
+
: undefined,
|
|
212
|
+
responseLength: responseText.length,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (!isJsonRecord(payload)) {
|
|
216
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
217
|
+
phase,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return { ok: response.ok, payload: parseResponse(payload) };
|
|
221
|
+
}
|
|
222
|
+
function endpoint(baseUrl, path) {
|
|
223
|
+
return `${baseUrl.replace(/\/+$/u, "")}/${path}`;
|
|
224
|
+
}
|
|
225
|
+
function spanErrorAttributes(error, phase) {
|
|
226
|
+
if (error instanceof ResolverVendorUnavailableError) {
|
|
227
|
+
return {
|
|
228
|
+
unavailability_reason: error.reason,
|
|
229
|
+
transport_phase: error.phase,
|
|
230
|
+
...(error instanceof CapsolverUnavailableError
|
|
231
|
+
? {
|
|
232
|
+
vendor_error_code: error.errorCode,
|
|
233
|
+
vendor_error_description: error.errorDescription,
|
|
234
|
+
response_status: error.responseStatus,
|
|
235
|
+
response_content_type: error.responseContentType,
|
|
236
|
+
response_length: error.responseLength,
|
|
237
|
+
}
|
|
238
|
+
: {}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (error instanceof CapsolverVerdictError) {
|
|
242
|
+
return {
|
|
243
|
+
verdict_reason: error.reason,
|
|
244
|
+
transport_phase: error.phase,
|
|
245
|
+
vendor_error_code: error.errorCode,
|
|
246
|
+
vendor_error_description: error.errorDescription,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (error instanceof CapsolverSolveTimeoutError) {
|
|
250
|
+
return {
|
|
251
|
+
unavailability_reason: "timeout",
|
|
252
|
+
transport_phase: phase,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
export function createCapsolverResolverVendorAdapter(options) {
|
|
258
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
259
|
+
const baseUrl = options.baseUrl ?? DEFAULT_CAPSOLVER_BASE_URL;
|
|
260
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
261
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
262
|
+
const now = options.now ?? Date.now;
|
|
263
|
+
const delay = options.delay ?? abortableDelay;
|
|
264
|
+
return {
|
|
265
|
+
id: CAPSOLVER_VENDOR_ID,
|
|
266
|
+
supports(kind) {
|
|
267
|
+
return resolverVendorSupports(CAPSOLVER_VENDOR_ID, kind);
|
|
268
|
+
},
|
|
269
|
+
async solve(challenge, _identity, callerSignal, traceRecorder) {
|
|
270
|
+
const apiKey = options.apiKey?.trim();
|
|
271
|
+
if (!apiKey) {
|
|
272
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "missing_credentials", {
|
|
273
|
+
phase: "create_task",
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (!resolverVendorSupports(CAPSOLVER_VENDOR_ID, challenge.kind)) {
|
|
277
|
+
throw new TypeError(`Capsolver resolver does not support ${challenge.kind}`);
|
|
278
|
+
}
|
|
279
|
+
if (challenge.kind !== "turnstile") {
|
|
280
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "not_implemented", {
|
|
281
|
+
phase: "create_task",
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const sensitiveValues = [
|
|
285
|
+
apiKey,
|
|
286
|
+
challenge.pageUrl,
|
|
287
|
+
challenge.siteKey,
|
|
288
|
+
...(challenge.action !== undefined ? [challenge.action] : []),
|
|
289
|
+
...(challenge.cdata !== undefined ? [challenge.cdata] : []),
|
|
290
|
+
];
|
|
291
|
+
assertResolverHostAllowed(challenge.pageUrl, options.allowedHosts);
|
|
292
|
+
callerSignal.throwIfAborted();
|
|
293
|
+
const solveController = new AbortController();
|
|
294
|
+
const onCallerAbort = () => solveController.abort(abortReason(callerSignal));
|
|
295
|
+
callerSignal.addEventListener("abort", onCallerAbort, { once: true });
|
|
296
|
+
const timeout = options.now
|
|
297
|
+
? undefined
|
|
298
|
+
: setTimeout(() => solveController.abort(new CapsolverSolveTimeoutError()), timeoutMs);
|
|
299
|
+
const startedAt = now();
|
|
300
|
+
let phase = "create_task";
|
|
301
|
+
try {
|
|
302
|
+
const createTask = async () => {
|
|
303
|
+
const metadata = {
|
|
304
|
+
...(challenge.action !== undefined ? { action: challenge.action } : {}),
|
|
305
|
+
...(challenge.cdata !== undefined ? { cdata: challenge.cdata } : {}),
|
|
306
|
+
};
|
|
307
|
+
const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), {
|
|
308
|
+
clientKey: apiKey,
|
|
309
|
+
task: {
|
|
310
|
+
type: "AntiTurnstileTaskProxyLess",
|
|
311
|
+
websiteURL: challenge.pageUrl,
|
|
312
|
+
websiteKey: challenge.siteKey,
|
|
313
|
+
...(challenge.action !== undefined || challenge.cdata !== undefined
|
|
314
|
+
? { metadata }
|
|
315
|
+
: {}),
|
|
316
|
+
},
|
|
317
|
+
}, solveController.signal, phase, sensitiveValues, parseCreateTaskResponse);
|
|
318
|
+
const taskId = createResult.payload.taskId;
|
|
319
|
+
if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
|
|
320
|
+
throw unavailableForPayload(createResult.payload, phase, sensitiveValues);
|
|
321
|
+
}
|
|
322
|
+
return taskId;
|
|
323
|
+
};
|
|
324
|
+
const taskId = traceRecorder
|
|
325
|
+
? await traceRecorder.runSpan("resolver.vendor.create_task", createTask, {
|
|
326
|
+
attributes: {
|
|
327
|
+
vendor: CAPSOLVER_VENDOR_ID,
|
|
328
|
+
challenge_kind: challenge.kind,
|
|
329
|
+
},
|
|
330
|
+
onError: (error) => spanErrorAttributes(error, "create_task"),
|
|
331
|
+
})
|
|
332
|
+
: await createTask();
|
|
333
|
+
phase = "poll_result";
|
|
334
|
+
const pollResult = async () => {
|
|
335
|
+
while (true) {
|
|
336
|
+
callerSignal.throwIfAborted();
|
|
337
|
+
const remainingMs = timeoutMs - (now() - startedAt);
|
|
338
|
+
if (remainingMs <= 0)
|
|
339
|
+
throw new CapsolverSolveTimeoutError();
|
|
340
|
+
await delay(Math.min(pollIntervalMs, remainingMs), solveController.signal);
|
|
341
|
+
callerSignal.throwIfAborted();
|
|
342
|
+
if (now() - startedAt >= timeoutMs)
|
|
343
|
+
throw new CapsolverSolveTimeoutError();
|
|
344
|
+
const result = await postJson(fetchImpl, endpoint(baseUrl, "getTaskResult"), { clientKey: apiKey, taskId }, solveController.signal, phase, sensitiveValues, parsePollResultResponse);
|
|
345
|
+
if (!result.ok || result.payload.errorId !== 0) {
|
|
346
|
+
throw unavailableForPayload(result.payload, phase, sensitiveValues);
|
|
347
|
+
}
|
|
348
|
+
switch (result.payload.status) {
|
|
349
|
+
case "idle":
|
|
350
|
+
case "processing":
|
|
351
|
+
continue;
|
|
352
|
+
case "ready":
|
|
353
|
+
break;
|
|
354
|
+
default:
|
|
355
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", { phase });
|
|
356
|
+
}
|
|
357
|
+
const token = result.payload.solution?.token;
|
|
358
|
+
if (!token?.trim()) {
|
|
359
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
360
|
+
phase,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
return { form: "token", token };
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
return traceRecorder
|
|
367
|
+
? await traceRecorder.runSpan("resolver.vendor.poll_result", pollResult, {
|
|
368
|
+
attributes: {
|
|
369
|
+
vendor: CAPSOLVER_VENDOR_ID,
|
|
370
|
+
challenge_kind: challenge.kind,
|
|
371
|
+
},
|
|
372
|
+
onError: (error) => spanErrorAttributes(error, "poll_result"),
|
|
373
|
+
})
|
|
374
|
+
: await pollResult();
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
if (callerSignal.aborted)
|
|
378
|
+
throw abortReason(callerSignal);
|
|
379
|
+
if (error instanceof CapsolverSolveTimeoutError ||
|
|
380
|
+
solveController.signal.reason instanceof CapsolverSolveTimeoutError) {
|
|
381
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "timeout", {
|
|
382
|
+
cause: error,
|
|
383
|
+
phase,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
if (error instanceof ResolverVendorUnavailableError ||
|
|
387
|
+
error instanceof ResolverChallengeVerdictError) {
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
throw new ResolverVendorUnavailableError(CAPSOLVER_VENDOR_ID, "transport_failure", {
|
|
391
|
+
...safeCauseOptions(error, sensitiveValues),
|
|
392
|
+
phase,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
finally {
|
|
396
|
+
if (timeout !== undefined)
|
|
397
|
+
clearTimeout(timeout);
|
|
398
|
+
callerSignal.removeEventListener("abort", onCallerAbort);
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|