@apifuse/provider-sdk 2.2.0-beta.32 → 2.2.0-beta.35
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 +14 -0
- package/dist/error-resolution.js +0 -1
- 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/auth-flow.d.ts +3 -1
- package/dist/runtime/auth-flow.js +1 -0
- package/dist/runtime/browser.d.ts +1 -0
- package/dist/runtime/browser.js +350 -27
- package/dist/runtime/choice.d.ts +0 -1
- package/dist/runtime/choice.js +10 -126
- package/dist/runtime/resolver-vendors/browser.d.ts +2 -0
- package/dist/runtime/resolver-vendors/browser.js +68 -16
- package/dist/runtime/resolver-vendors/capsolver.d.ts +1 -3
- package/dist/runtime/resolver-vendors/capsolver.js +148 -24
- package/dist/runtime/resolver-vendors/twocaptcha.js +57 -18
- package/dist/runtime/resolver-vendors/types.d.ts +5 -2
- package/dist/runtime/resolver-vendors/types.js +16 -4
- package/dist/runtime/resolver.d.ts +1 -1
- package/dist/runtime/resolver.js +24 -5
- package/dist/server/serve-implementation.d.ts +2 -1
- package/dist/server/serve-implementation.js +27 -17
- package/dist/testing/run.js +1 -0
- package/dist/types.d.ts +25 -3
- package/package.json +3 -2
- package/src/error-resolution.ts +0 -1
- package/src/index.ts +0 -1
- package/src/provider.ts +0 -1
- package/src/runtime/auth-flow.ts +4 -0
- package/src/runtime/browser.ts +438 -31
- package/src/runtime/choice.ts +10 -151
- package/src/runtime/resolver-vendors/browser.ts +83 -16
- package/src/runtime/resolver-vendors/capsolver.ts +170 -33
- package/src/runtime/resolver-vendors/twocaptcha.ts +54 -15
- package/src/runtime/resolver-vendors/types.ts +22 -4
- package/src/runtime/resolver.ts +31 -7
- package/src/server/serve-implementation.ts +74 -17
- package/src/testing/run.ts +1 -0
- package/src/types.ts +26 -3
|
@@ -235,17 +235,23 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
|
235
235
|
if (!resolverVendorSupports(TWOCAPTCHA_VENDOR_ID, challenge.kind)) {
|
|
236
236
|
throw new TypeError(`2captcha resolver does not support ${challenge.kind}`);
|
|
237
237
|
}
|
|
238
|
-
if (challenge.kind
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
238
|
+
if (challenge.kind === "aws_waf") {
|
|
239
|
+
const missingFields = [
|
|
240
|
+
...(challenge.siteKey?.trim() ? [] : ["siteKey"]),
|
|
241
|
+
...(challenge.captchaScript?.trim() ? [] : ["captchaScript"]),
|
|
242
|
+
...(challenge.context?.trim() ? [] : ["context"]),
|
|
243
|
+
...(challenge.iv?.trim() ? [] : ["iv"]),
|
|
244
|
+
];
|
|
245
|
+
if (missingFields.length > 0) {
|
|
246
|
+
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "missing_challenge_input", {
|
|
247
|
+
missingFields,
|
|
248
|
+
phase: "create_task",
|
|
249
|
+
});
|
|
250
|
+
}
|
|
242
251
|
}
|
|
243
|
-
if (challenge.kind === "
|
|
244
|
-
(
|
|
245
|
-
|
|
246
|
-
!challenge.context?.trim() ||
|
|
247
|
-
!challenge.iv?.trim())) {
|
|
248
|
-
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
|
|
252
|
+
if (challenge.kind === "recaptcha_v3" && challenge.minScore === undefined) {
|
|
253
|
+
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "missing_challenge_input", {
|
|
254
|
+
missingFields: ["minScore"],
|
|
249
255
|
phase: "create_task",
|
|
250
256
|
});
|
|
251
257
|
}
|
|
@@ -278,14 +284,47 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
|
|
|
278
284
|
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
279
285
|
...(proxy ?? {}),
|
|
280
286
|
}
|
|
281
|
-
:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
: challenge.kind === "recaptcha_v2"
|
|
288
|
+
? {
|
|
289
|
+
type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
|
|
290
|
+
websiteURL: challenge.pageUrl,
|
|
291
|
+
websiteKey: challenge.siteKey,
|
|
292
|
+
isInvisible: false,
|
|
293
|
+
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
294
|
+
...(proxy ?? {}),
|
|
295
|
+
}
|
|
296
|
+
: challenge.kind === "recaptcha_v3"
|
|
297
|
+
? {
|
|
298
|
+
type: "RecaptchaV3TaskProxyless",
|
|
299
|
+
websiteURL: challenge.pageUrl,
|
|
300
|
+
websiteKey: challenge.siteKey,
|
|
301
|
+
minScore: challenge.minScore,
|
|
302
|
+
pageAction: challenge.action,
|
|
303
|
+
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
304
|
+
}
|
|
305
|
+
: challenge.kind === "hcaptcha"
|
|
306
|
+
? {
|
|
307
|
+
type: proxy ? "HCaptchaTask" : "HCaptchaTaskProxyless",
|
|
308
|
+
websiteURL: challenge.pageUrl,
|
|
309
|
+
websiteKey: challenge.siteKey,
|
|
310
|
+
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
311
|
+
...(proxy ?? {}),
|
|
312
|
+
}
|
|
313
|
+
: challenge.kind === "turnstile"
|
|
314
|
+
? {
|
|
315
|
+
type: proxy ? "TurnstileTask" : "TurnstileTaskProxyless",
|
|
316
|
+
websiteURL: challenge.pageUrl,
|
|
317
|
+
websiteKey: challenge.siteKey,
|
|
318
|
+
...(challenge.action !== undefined ? { action: challenge.action } : {}),
|
|
319
|
+
...(challenge.cdata !== undefined ? { data: challenge.cdata } : {}),
|
|
320
|
+
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
321
|
+
...(proxy ?? {}),
|
|
322
|
+
}
|
|
323
|
+
: // `resolverVendorSupports` above already rejected every kind this
|
|
324
|
+
// adapter does not build a task for, so this branch is unreachable.
|
|
325
|
+
(() => {
|
|
326
|
+
throw new TypeError(`2captcha resolver does not support ${challenge.kind}`);
|
|
327
|
+
})();
|
|
289
328
|
const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), { clientKey: apiKey, task }, solveController.signal, phase, [apiKey]);
|
|
290
329
|
const taskId = taskIdFrom(createResult.payload);
|
|
291
330
|
if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
|
|
@@ -2,7 +2,7 @@ import type { ChallengeSolution, ProviderChallenge, ProviderChallengeKind, Provi
|
|
|
2
2
|
import type { TraceRecorder } from "../trace.js";
|
|
3
3
|
export declare const RESOLVER_VENDOR_CAPABILITIES: {
|
|
4
4
|
readonly browser: readonly ["aws_waf", "cloudflare_interstitial"];
|
|
5
|
-
readonly "2captcha": readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "
|
|
5
|
+
readonly "2captcha": readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "aws_waf"];
|
|
6
6
|
readonly capsolver: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf"];
|
|
7
7
|
readonly capmonster: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha"];
|
|
8
8
|
readonly custom: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf", "akamai_sec_cpt", "akamai_sensor"];
|
|
@@ -55,11 +55,13 @@ 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_client_profile" | "missing_transport" | "allocation_exhausted" | "transport_failure" | "timeout" | "not_implemented";
|
|
58
|
+
export type ResolverVendorUnavailableReason = "missing_credentials" | "missing_proxy_identity" | "missing_client_profile" | "missing_challenge_input" | "missing_transport" | "allocation_exhausted" | "transport_failure" | "timeout" | "not_implemented";
|
|
59
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;
|
|
63
|
+
/** Names of challenge fields required by this adapter but absent from this call's input. */
|
|
64
|
+
readonly missingFields?: readonly string[];
|
|
63
65
|
/** Upstream hostname only; never a URL. */
|
|
64
66
|
readonly upstreamHost?: string;
|
|
65
67
|
/** Adapter-defined sensor-loop phase, such as fetch_script or post_sensor. */
|
|
@@ -70,6 +72,7 @@ type ResolverErrorOptions = {
|
|
|
70
72
|
export declare class ResolverVendorUnavailableError extends Error {
|
|
71
73
|
readonly vendor: ProviderResolverVendor;
|
|
72
74
|
readonly reason: ResolverVendorUnavailableReason;
|
|
75
|
+
readonly missingFields?: readonly string[];
|
|
73
76
|
readonly upstreamHost?: string;
|
|
74
77
|
readonly phase?: string;
|
|
75
78
|
readonly round?: number;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
export const RESOLVER_VENDOR_CAPABILITIES = {
|
|
2
2
|
browser: ["aws_waf", "cloudflare_interstitial"],
|
|
3
|
+
// Every kind listed per vendor is implemented by that vendor's adapter; the
|
|
4
|
+
// per-adapter "agrees with every declared capability" tests iterate this
|
|
5
|
+
// table, so adding a kind here without an implementation fails the suite.
|
|
6
|
+
// 2captcha omits `cloudflare_interstitial`, `akamai_sec_cpt`, and
|
|
7
|
+
// `akamai_sensor`: their API offers no task type for them, so declaring them
|
|
8
|
+
// would route challenges to a vendor that can only refuse.
|
|
3
9
|
"2captcha": [
|
|
4
10
|
"turnstile",
|
|
5
11
|
"recaptcha_v2",
|
|
6
12
|
"recaptcha_v3",
|
|
7
13
|
"hcaptcha",
|
|
8
|
-
"cloudflare_interstitial",
|
|
9
14
|
"aws_waf",
|
|
10
|
-
"akamai_sec_cpt",
|
|
11
|
-
"akamai_sensor",
|
|
12
15
|
],
|
|
13
16
|
capsolver: [
|
|
14
17
|
"turnstile",
|
|
@@ -36,14 +39,23 @@ export function resolverVendorSupports(vendor, kind) {
|
|
|
36
39
|
export class ResolverVendorUnavailableError extends Error {
|
|
37
40
|
vendor;
|
|
38
41
|
reason;
|
|
42
|
+
missingFields;
|
|
39
43
|
upstreamHost;
|
|
40
44
|
phase;
|
|
41
45
|
round;
|
|
42
46
|
constructor(vendor, reason, options = {}) {
|
|
43
|
-
|
|
47
|
+
const missingFields = reason === "missing_challenge_input"
|
|
48
|
+
? options.missingFields?.filter((field) => /^[A-Za-z][A-Za-z0-9_]*$/u.test(field))
|
|
49
|
+
: undefined;
|
|
50
|
+
super(reason === "missing_challenge_input" && missingFields !== undefined && missingFields.length > 0
|
|
51
|
+
? `Resolver vendor ${vendor} cannot use incomplete challenge input; missing fields: ${missingFields.join(", ")}`
|
|
52
|
+
: `Resolver vendor ${vendor} is unavailable: ${reason}`);
|
|
44
53
|
this.vendor = vendor;
|
|
45
54
|
this.reason = reason;
|
|
46
55
|
this.name = "ResolverVendorUnavailableError";
|
|
56
|
+
if (missingFields !== undefined && missingFields.length > 0) {
|
|
57
|
+
this.missingFields = Object.freeze([...missingFields]);
|
|
58
|
+
}
|
|
47
59
|
if (options.cause !== undefined) {
|
|
48
60
|
this.cause = options.cause;
|
|
49
61
|
}
|
|
@@ -34,7 +34,7 @@ export type ResolverInstrumentationMetadata = {
|
|
|
34
34
|
readonly target: ResolverContext;
|
|
35
35
|
readonly traceRecorder: TraceRecorder;
|
|
36
36
|
};
|
|
37
|
-
export type ResolverAdapterFactory = (configuration: string, timeoutMs: number, allowedHosts: readonly string[]) => ResolverVendorAdapter;
|
|
37
|
+
export type ResolverAdapterFactory = (configuration: string | undefined, timeoutMs: number, allowedHosts: readonly string[]) => ResolverVendorAdapter;
|
|
38
38
|
export declare const RESOLVER_ADAPTER_REGISTRY: Partial<Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>>;
|
|
39
39
|
export declare function swapResolverAdapterFactoryForTests(vendor: ProviderResolverVendor, factory: ResolverAdapterFactory | undefined): () => void;
|
|
40
40
|
/** Internal test seam; deliberately not re-exported from the package root. */
|
package/dist/runtime/resolver.js
CHANGED
|
@@ -56,6 +56,9 @@ const SAFE_CAUSE_MESSAGE_WORDS = new Set([
|
|
|
56
56
|
]);
|
|
57
57
|
const resolverAdapterRegistry = {
|
|
58
58
|
"2captcha"(configuration, timeoutMs, allowedHosts) {
|
|
59
|
+
if (configuration === undefined) {
|
|
60
|
+
throw new Error("2captcha resolver adapter factory requires an API key");
|
|
61
|
+
}
|
|
59
62
|
return createTwoCaptchaResolverVendorAdapter({
|
|
60
63
|
allowedHosts,
|
|
61
64
|
apiKey: configuration,
|
|
@@ -173,10 +176,17 @@ function throwUnsupportedKind(kind) {
|
|
|
173
176
|
});
|
|
174
177
|
}
|
|
175
178
|
function throwExhausted(attempts) {
|
|
176
|
-
const
|
|
179
|
+
const hasMissingChallengeInput = attempts.some((attempt) => attempt.reason === "missing_challenge_input");
|
|
180
|
+
const summary = attempts
|
|
181
|
+
.map(({ vendor, reason, missingFields }) => missingFields === undefined || missingFields.length === 0
|
|
182
|
+
? `${vendor}: ${reason}`
|
|
183
|
+
: `${vendor}: ${reason} (missing fields: ${missingFields.join(", ")})`)
|
|
184
|
+
.join(", ");
|
|
177
185
|
throw new ProviderError(`Resolver vendor chain exhausted: ${summary}`, {
|
|
178
186
|
code: "RESOLVER_CHAIN_EXHAUSTED",
|
|
179
|
-
fix:
|
|
187
|
+
fix: hasMissingChallengeInput
|
|
188
|
+
? "Capture the named challenge fields or configure another supporting resolver vendor."
|
|
189
|
+
: "Configure another supporting resolver vendor or restore an unavailable vendor.",
|
|
180
190
|
details: attempts,
|
|
181
191
|
});
|
|
182
192
|
}
|
|
@@ -280,6 +290,7 @@ function unavailableAttempt(error) {
|
|
|
280
290
|
return {
|
|
281
291
|
vendor: error.vendor,
|
|
282
292
|
reason: error.reason,
|
|
293
|
+
...(error.missingFields ? { missingFields: [...error.missingFields] } : {}),
|
|
283
294
|
...(cause ? { cause } : {}),
|
|
284
295
|
...(upstreamHost ? { upstreamHost } : {}),
|
|
285
296
|
...(phase ? { phase } : {}),
|
|
@@ -290,6 +301,7 @@ function unavailableSpanAttributes(error) {
|
|
|
290
301
|
const attempt = unavailableAttempt(error);
|
|
291
302
|
return {
|
|
292
303
|
unavailability_reason: error.reason,
|
|
304
|
+
missing_fields: error.missingFields,
|
|
293
305
|
cause_name: attempt.cause?.name,
|
|
294
306
|
cause_message: attempt.cause?.message,
|
|
295
307
|
upstream_host: attempt.upstreamHost,
|
|
@@ -608,6 +620,8 @@ function createResolverChainClient(options) {
|
|
|
608
620
|
signal.throwIfAborted();
|
|
609
621
|
if (!(error instanceof ResolverVendorUnavailableError))
|
|
610
622
|
throw error;
|
|
623
|
+
// Every vendor-unavailable result, including missing_challenge_input, falls
|
|
624
|
+
// through so another adapter can solve with a different input contract.
|
|
611
625
|
attempts.push(unavailableAttempt(error));
|
|
612
626
|
}
|
|
613
627
|
}
|
|
@@ -643,13 +657,18 @@ function resolveVendorAvailability(vendor, env) {
|
|
|
643
657
|
reason: "missing_transport",
|
|
644
658
|
};
|
|
645
659
|
}
|
|
660
|
+
if (vendor === "browser") {
|
|
661
|
+
return {
|
|
662
|
+
vendor,
|
|
663
|
+
available: true,
|
|
664
|
+
configuration: normalizedEnvValue(env, APIFUSE__CDP_POOL__URL),
|
|
665
|
+
};
|
|
666
|
+
}
|
|
646
667
|
const envKey = vendor === "2captcha"
|
|
647
668
|
? APIFUSE__RESOLVER__2CAPTCHA__API_KEY
|
|
648
669
|
: vendor === "capsolver"
|
|
649
670
|
? APIFUSE__RESOLVER__CAPSOLVER__API_KEY
|
|
650
|
-
:
|
|
651
|
-
? APIFUSE__RESOLVER__CAPMONSTER__API_KEY
|
|
652
|
-
: APIFUSE__CDP_POOL__URL;
|
|
671
|
+
: APIFUSE__RESOLVER__CAPMONSTER__API_KEY;
|
|
653
672
|
const configuration = normalizedEnvValue(env, envKey);
|
|
654
673
|
return configuration
|
|
655
674
|
? { vendor, available: true, configuration }
|
|
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { type ProviderErrorCategory } from "../observability.js";
|
|
4
4
|
import type { OcrContext, ProviderContext, ProviderDefinition, ProviderRuntimeState, ResolverContext, SttContext } from "../types.js";
|
|
5
5
|
import type { SelfTestCancellationLogEvent } from "./self-test.js";
|
|
6
|
-
import { type OperationRequest } from "./types.js";
|
|
6
|
+
import { type AuthFlowRequest, type OperationRequest } from "./types.js";
|
|
7
7
|
/** Compact SDK-owned error classification emitted separately from the public response body. */
|
|
8
8
|
export declare const ERROR_OBSERVABILITY_HEADER = "X-ApiFuse-Error-Observability";
|
|
9
9
|
export type ErrorObservabilityDetails = {
|
|
@@ -63,6 +63,7 @@ export type ProviderServerOperationExecutorInput = {
|
|
|
63
63
|
export type ProviderServerOperationExecutor = (input: ProviderServerOperationExecutorInput) => Promise<unknown>;
|
|
64
64
|
export declare function resolveProviderProxyAffinityKey(provider: ProviderDefinition, request: OperationRequest, operationId: string): string;
|
|
65
65
|
export declare function resolveProviderResolverIdentityScope(provider: ProviderDefinition, affinityKey: string, contextId: string): string;
|
|
66
|
+
export declare function resolveAuthFlowProxyAffinityKey(provider: ProviderDefinition, request: Pick<AuthFlowRequest, "connection" | "connectionId" | "externalRef" | "tenantId" | "providerId">): string;
|
|
66
67
|
type ProviderRequestCost = {
|
|
67
68
|
durationMs: number;
|
|
68
69
|
cpuUserMicros: number;
|
|
@@ -11,7 +11,7 @@ import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.j
|
|
|
11
11
|
import { categoryForStatus, sourceForCategory, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
|
|
12
12
|
import { createScratchpad } from "../runtime/auth-flow.js";
|
|
13
13
|
import { createProviderCache } from "../runtime/cache.js";
|
|
14
|
-
import { createProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
|
|
14
|
+
import { createProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "../runtime/choice.js";
|
|
15
15
|
import { createCredentialContext } from "../runtime/credential.js";
|
|
16
16
|
import { createEnvContext } from "../runtime/env.js";
|
|
17
17
|
import { executeOperation } from "../runtime/executor.js";
|
|
@@ -343,7 +343,14 @@ export function resolveProviderResolverIdentityScope(provider, affinityKey, cont
|
|
|
343
343
|
});
|
|
344
344
|
}
|
|
345
345
|
function resolveOperationConnectionId(request) {
|
|
346
|
-
|
|
346
|
+
// An empty string is a malformed identifier, not an identity: treat it as
|
|
347
|
+
// absent so it can never override a valid id or key a real scope. Requests
|
|
348
|
+
// without any usable id fall back to the documented missing-connection
|
|
349
|
+
// sentinel scope instead of scoping context/affinity/state under "".
|
|
350
|
+
return normalizeConnectionId(request.connection?.id) ?? normalizeConnectionId(request.connectionId);
|
|
351
|
+
}
|
|
352
|
+
function normalizeConnectionId(id) {
|
|
353
|
+
return id === "" ? undefined : id;
|
|
347
354
|
}
|
|
348
355
|
function resolveNativeProxyPolicy(provider) {
|
|
349
356
|
if (typeof provider.proxy === "object")
|
|
@@ -376,7 +383,6 @@ function createProviderContext(provider, request, operationId, options, state =
|
|
|
376
383
|
const env = createEnvContext([
|
|
377
384
|
...(provider.secrets?.map((secret) => secret.name) ?? []),
|
|
378
385
|
PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
|
|
379
|
-
PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV,
|
|
380
386
|
]);
|
|
381
387
|
const credential = createCredentialContext({
|
|
382
388
|
allowedKeys: provider.credential?.keys,
|
|
@@ -498,7 +504,14 @@ function createFlowContextStore(allowedKeys, initialContext = {}) {
|
|
|
498
504
|
},
|
|
499
505
|
};
|
|
500
506
|
}
|
|
501
|
-
function
|
|
507
|
+
export function resolveAuthFlowProxyAffinityKey(provider, request) {
|
|
508
|
+
return (resolveOperationConnectionId(request) ??
|
|
509
|
+
request.externalRef ??
|
|
510
|
+
request.tenantId ??
|
|
511
|
+
request.providerId ??
|
|
512
|
+
provider.id);
|
|
513
|
+
}
|
|
514
|
+
function createAuthFlowContext(provider, request, options, state, signal) {
|
|
502
515
|
const baseUrl = getProviderBaseUrl(provider);
|
|
503
516
|
const stealthBaseUrl = getProviderStealthBaseUrl(provider);
|
|
504
517
|
const stealthProfile = getProviderStealthProfile(provider);
|
|
@@ -507,11 +520,7 @@ function createAuthFlowContext(provider, request, options, signal) {
|
|
|
507
520
|
const flowContextStore = createFlowContextStore(provider.context?.keys ?? Object.keys(contextData), contextData);
|
|
508
521
|
const proxyClientOptions = {
|
|
509
522
|
upstream: { proxy: provider.proxy },
|
|
510
|
-
affinityKey: request
|
|
511
|
-
request.externalRef ??
|
|
512
|
-
request.tenantId ??
|
|
513
|
-
request.providerId ??
|
|
514
|
-
provider.id,
|
|
523
|
+
affinityKey: resolveAuthFlowProxyAffinityKey(provider, request),
|
|
515
524
|
};
|
|
516
525
|
const resolverIdentityScope = resolveProviderResolverIdentityScope(provider, proxyClientOptions.affinityKey, request.requestId);
|
|
517
526
|
const stealthClientOptions = {
|
|
@@ -532,7 +541,7 @@ function createAuthFlowContext(provider, request, options, signal) {
|
|
|
532
541
|
return {
|
|
533
542
|
context: {
|
|
534
543
|
flowId: request.flowId,
|
|
535
|
-
connectionId: request
|
|
544
|
+
connectionId: resolveOperationConnectionId(request),
|
|
536
545
|
externalRef: request.externalRef,
|
|
537
546
|
tenantId: request.tenantId ?? "",
|
|
538
547
|
providerId: request.providerId ?? provider.id,
|
|
@@ -540,6 +549,7 @@ function createAuthFlowContext(provider, request, options, signal) {
|
|
|
540
549
|
...proxyClientOptions,
|
|
541
550
|
...(signal ? { signal } : {}),
|
|
542
551
|
}),
|
|
552
|
+
state: state.forConnection(resolveOperationConnectionId(request)),
|
|
543
553
|
stealth: stealthBaseUrl
|
|
544
554
|
? capabilityModules.stealth
|
|
545
555
|
? stealthProfile
|
|
@@ -1360,7 +1370,7 @@ function responseWithProviderTelemetry(response, proxyTelemetry) {
|
|
|
1360
1370
|
statusText: response.statusText,
|
|
1361
1371
|
});
|
|
1362
1372
|
}
|
|
1363
|
-
async function handleAuthFlow(provider, request, route, options, signal) {
|
|
1373
|
+
async function handleAuthFlow(provider, request, route, options, state, signal) {
|
|
1364
1374
|
const flow = provider.auth?.flow;
|
|
1365
1375
|
if (!flow) {
|
|
1366
1376
|
throw new ProviderError("Auth flow is not configured", {
|
|
@@ -1372,7 +1382,7 @@ async function handleAuthFlow(provider, request, route, options, signal) {
|
|
|
1372
1382
|
// any flow code runs instead of at whatever point the ceremony first reads
|
|
1373
1383
|
// the env. `abort` stays exempt: a user must always be able to cancel a
|
|
1374
1384
|
// stranded flow even when provisioning is broken.
|
|
1375
|
-
const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
|
|
1385
|
+
const { context, getPatch } = createAuthFlowContext(provider, request, options, state, signal);
|
|
1376
1386
|
try {
|
|
1377
1387
|
if (route !== "abort") {
|
|
1378
1388
|
assertRequiredSecretsPresent(provider, context.env);
|
|
@@ -1746,7 +1756,7 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1746
1756
|
.json()
|
|
1747
1757
|
.catch(() => undefined);
|
|
1748
1758
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1749
|
-
const response = await handleAuthFlow(provider, body, "start", options, c.req.raw.signal);
|
|
1759
|
+
const response = await handleAuthFlow(provider, body, "start", options, state, c.req.raw.signal);
|
|
1750
1760
|
logProviderSuccess(logger, provider, "auth", "start", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
1751
1761
|
return response instanceof Response ? response : c.json(response);
|
|
1752
1762
|
}
|
|
@@ -1766,7 +1776,7 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1766
1776
|
.json()
|
|
1767
1777
|
.catch(() => undefined);
|
|
1768
1778
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1769
|
-
const response = await handleAuthFlow(provider, body, "continue", options, c.req.raw.signal);
|
|
1779
|
+
const response = await handleAuthFlow(provider, body, "continue", options, state, c.req.raw.signal);
|
|
1770
1780
|
logProviderSuccess(logger, provider, "auth", "continue", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
1771
1781
|
return response instanceof Response ? response : c.json(response);
|
|
1772
1782
|
}
|
|
@@ -1786,7 +1796,7 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1786
1796
|
.json()
|
|
1787
1797
|
.catch(() => undefined);
|
|
1788
1798
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1789
|
-
const response = await handleAuthFlow(provider, body, "poll", options, c.req.raw.signal);
|
|
1799
|
+
const response = await handleAuthFlow(provider, body, "poll", options, state, c.req.raw.signal);
|
|
1790
1800
|
logProviderSuccess(logger, provider, "auth", "poll", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
1791
1801
|
return response instanceof Response ? response : c.json(response);
|
|
1792
1802
|
}
|
|
@@ -1806,7 +1816,7 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1806
1816
|
.json()
|
|
1807
1817
|
.catch(() => undefined);
|
|
1808
1818
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1809
|
-
const response = await handleAuthFlow(provider, body, "refresh", options, c.req.raw.signal);
|
|
1819
|
+
const response = await handleAuthFlow(provider, body, "refresh", options, state, c.req.raw.signal);
|
|
1810
1820
|
logProviderSuccess(logger, provider, "auth", "refresh", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
1811
1821
|
return response instanceof Response ? response : c.json(response);
|
|
1812
1822
|
}
|
|
@@ -1826,7 +1836,7 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1826
1836
|
.json()
|
|
1827
1837
|
.catch(() => undefined);
|
|
1828
1838
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1829
|
-
const response = await handleAuthFlow(provider, body, "abort", options, c.req.raw.signal);
|
|
1839
|
+
const response = await handleAuthFlow(provider, body, "abort", options, state, c.req.raw.signal);
|
|
1830
1840
|
logProviderSuccess(logger, provider, "auth", "disconnect", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
1831
1841
|
return response instanceof Response ? response : c.json(response);
|
|
1832
1842
|
}
|
package/dist/testing/run.js
CHANGED
|
@@ -230,6 +230,7 @@ function createUpstreamContext(provider, operationName, upstreamStub) {
|
|
|
230
230
|
id: `standard-test-${operationName}`,
|
|
231
231
|
url: async () => currentUrl,
|
|
232
232
|
title: async () => currentResponse?.text.match(/<title[^>]*>([^<]*)<\/title>/i)?.[1] ?? "",
|
|
233
|
+
userAgent: async () => String((await browserAction("userAgent")).data),
|
|
233
234
|
content: async () => currentResponse?.text ?? "",
|
|
234
235
|
evaluate: async (fn) => (await browserAction("evaluate", typeof fn === "string" ? fn : String(fn))).data,
|
|
235
236
|
locator: (selector) => ({
|
package/dist/types.d.ts
CHANGED
|
@@ -1451,7 +1451,7 @@ export interface BrowserCookie {
|
|
|
1451
1451
|
readonly secure: boolean;
|
|
1452
1452
|
readonly sameSite?: "Strict" | "Lax" | "None";
|
|
1453
1453
|
}
|
|
1454
|
-
export type BrowserResourceMethod = "GET" | "HEAD";
|
|
1454
|
+
export type BrowserResourceMethod = "GET" | "HEAD" | "POST";
|
|
1455
1455
|
export type BrowserResourceRequest = {
|
|
1456
1456
|
readonly url: string;
|
|
1457
1457
|
readonly method: BrowserResourceMethod;
|
|
@@ -1460,6 +1460,8 @@ export type BrowserResourceRequest = {
|
|
|
1460
1460
|
};
|
|
1461
1461
|
export type BrowserResourceBody = Buffer | Uint8Array | ArrayBuffer | string;
|
|
1462
1462
|
export type BrowserResourceDecision = {
|
|
1463
|
+
readonly action: "continue";
|
|
1464
|
+
} | {
|
|
1463
1465
|
readonly action: "fulfill";
|
|
1464
1466
|
readonly status?: number;
|
|
1465
1467
|
readonly headers?: Readonly<Record<string, string>>;
|
|
@@ -1475,10 +1477,18 @@ export type BrowserResourceRoute = {
|
|
|
1475
1477
|
export type BrowserResourcePolicy = {
|
|
1476
1478
|
readonly defaultAction?: "block";
|
|
1477
1479
|
readonly allowedMethods?: readonly BrowserResourceMethod[];
|
|
1480
|
+
/**
|
|
1481
|
+
* Appends an enforcing CSP header to every renderable document response
|
|
1482
|
+
* while the policy is active. Existing CSP headers are retained, so this
|
|
1483
|
+
* can only further restrict the document.
|
|
1484
|
+
*/
|
|
1485
|
+
readonly documentContentSecurityPolicy?: string;
|
|
1478
1486
|
readonly routes: readonly BrowserResourceRoute[];
|
|
1479
1487
|
};
|
|
1480
1488
|
export interface BrowserPage extends BrowserFrame {
|
|
1481
1489
|
close(): Promise<void>;
|
|
1490
|
+
/** Returns the user agent used by this page's browser context. */
|
|
1491
|
+
userAgent(): Promise<string>;
|
|
1482
1492
|
/**
|
|
1483
1493
|
* Reads the browser context's cookie jar, including httpOnly cookies.
|
|
1484
1494
|
* Cookie expiry values are Unix seconds and are absent for session cookies.
|
|
@@ -1571,7 +1581,7 @@ export type ProviderChoiceExplicitParseResult = {
|
|
|
1571
1581
|
readonly payload: Record<string, unknown>;
|
|
1572
1582
|
/** Stable, opaque key for provider-owned idempotency records. */
|
|
1573
1583
|
readonly replayKey: string;
|
|
1574
|
-
/** Atomically claims a word token.
|
|
1584
|
+
/** Atomically claims a word token. Inline tokens report unsupported. */
|
|
1575
1585
|
consume(): Promise<ProviderChoiceConsumeResult>;
|
|
1576
1586
|
} | {
|
|
1577
1587
|
readonly status: "consumed";
|
|
@@ -1618,7 +1628,7 @@ export interface ProviderChoiceParseOptions {
|
|
|
1618
1628
|
futureToleranceMs?: number;
|
|
1619
1629
|
bind?: ProviderChoiceBindingOptions;
|
|
1620
1630
|
storage?: ProviderChoiceStorageOptions;
|
|
1621
|
-
/** Defaults to never,
|
|
1631
|
+
/** Defaults to never, preserving reusable choice-token parse semantics. */
|
|
1622
1632
|
consume?: ProviderChoiceConsumeMode;
|
|
1623
1633
|
}
|
|
1624
1634
|
export interface ProviderChoiceContext {
|
|
@@ -1739,6 +1749,18 @@ export interface FlowContext {
|
|
|
1739
1749
|
tenantId: string;
|
|
1740
1750
|
providerId: string;
|
|
1741
1751
|
http: HttpClient;
|
|
1752
|
+
/** Durable connection-scoped runtime state. Present when the host runtime
|
|
1753
|
+
* supplies one; auth ceremonies must fail closed when absent rather than
|
|
1754
|
+
* fall back to bypassable in-process storage.
|
|
1755
|
+
*
|
|
1756
|
+
* Scoped via `ProviderRuntimeState.forConnection`: requests that resolve no
|
|
1757
|
+
* connection id (pre-connection ceremonies such as first-time logins) share
|
|
1758
|
+
* the documented isolated missing-connection scope. That sharing is the
|
|
1759
|
+
* intended semantic — it lets counters keyed by caller identity (e.g. a
|
|
1760
|
+
* login email) persist across separate ceremonies for the same caller.
|
|
1761
|
+
* Flows storing entries in that scope MUST key them by caller identity;
|
|
1762
|
+
* un-keyed entries would be shared across all connectionless ceremonies. */
|
|
1763
|
+
readonly state?: ProviderRuntimeState;
|
|
1742
1764
|
/** Present when the selected runtime supplies native network capabilities. */
|
|
1743
1765
|
readonly native?: NativeProviderContext;
|
|
1744
1766
|
stealth: StealthClient;
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.2.0-beta.
|
|
2
|
+
"version": "2.2.0-beta.35",
|
|
3
3
|
"name": "@apifuse/provider-sdk",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
@@ -106,10 +106,11 @@
|
|
|
106
106
|
"lint": "biome lint .",
|
|
107
107
|
"lint:fix": "biome lint --write",
|
|
108
108
|
"lint:deprecated": "bun run scripts/lint-deprecated-usage.ts",
|
|
109
|
+
"lint:test-typesafety": "bun scripts/check-test-typesafety.ts",
|
|
109
110
|
"format": "biome format --write",
|
|
110
111
|
"type-check": "tsc --noEmit",
|
|
111
112
|
"test": "bun test",
|
|
112
|
-
"check": "bun run lint && bun run type-check && bun run lint:deprecated && bun run build",
|
|
113
|
+
"check": "bun run lint && bun run type-check && bun run lint:deprecated && bun run lint:test-typesafety && bun run build",
|
|
113
114
|
"pack:check": "bun run build && bun bin/apifuse-pack-check.ts",
|
|
114
115
|
"pack:smoke": "bun run build && bun bin/apifuse-pack-smoke.ts",
|
|
115
116
|
"pack:types": "bun run build && bun bin/apifuse-pack-types.ts",
|
package/src/error-resolution.ts
CHANGED
|
@@ -20,7 +20,6 @@ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
|
|
|
20
20
|
"RUNTIME_UNSUPPORTED",
|
|
21
21
|
"PROVIDER_STATE_UNSUPPORTED",
|
|
22
22
|
"CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
|
|
23
|
-
"CHOICE_WORD_ISSUANCE_INVALID",
|
|
24
23
|
"CHOICE_STATE_PAYLOAD_TOO_LARGE",
|
|
25
24
|
"CHOICE_STATE_UNAVAILABLE",
|
|
26
25
|
"CHOICE_CONTEXT_REQUIRED",
|
package/src/index.ts
CHANGED
package/src/provider.ts
CHANGED
package/src/runtime/auth-flow.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
FlowContext,
|
|
7
7
|
HttpClient,
|
|
8
8
|
OcrContext,
|
|
9
|
+
ProviderRuntimeState,
|
|
9
10
|
StealthClient,
|
|
10
11
|
SttContext,
|
|
11
12
|
} from "../types.js";
|
|
@@ -59,6 +60,8 @@ export function createFlowContext(options: {
|
|
|
59
60
|
providerId: string;
|
|
60
61
|
connectionId?: string;
|
|
61
62
|
externalRef?: string;
|
|
63
|
+
/** Host-agnostic: callers pass an already-scoped runtime state, which this helper forwards verbatim. */
|
|
64
|
+
state?: ProviderRuntimeState;
|
|
62
65
|
allowedKeys: string[];
|
|
63
66
|
initialContext?: Record<string, unknown>;
|
|
64
67
|
ocr?: OcrContext;
|
|
@@ -71,6 +74,7 @@ export function createFlowContext(options: {
|
|
|
71
74
|
tenantId: options.tenantId,
|
|
72
75
|
providerId: options.providerId,
|
|
73
76
|
http: options.http,
|
|
77
|
+
state: options.state,
|
|
74
78
|
stealth: options.stealth,
|
|
75
79
|
env: options.env,
|
|
76
80
|
context: createScratchpad(options.allowedKeys, options.initialContext),
|