@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
|
@@ -338,19 +338,23 @@ export function createTwoCaptchaResolverVendorAdapter(
|
|
|
338
338
|
if (!resolverVendorSupports(TWOCAPTCHA_VENDOR_ID, challenge.kind)) {
|
|
339
339
|
throw new TypeError(`2captcha resolver does not support ${challenge.kind}`);
|
|
340
340
|
}
|
|
341
|
-
if (challenge.kind
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
341
|
+
if (challenge.kind === "aws_waf") {
|
|
342
|
+
const missingFields = [
|
|
343
|
+
...(challenge.siteKey?.trim() ? [] : ["siteKey"]),
|
|
344
|
+
...(challenge.captchaScript?.trim() ? [] : ["captchaScript"]),
|
|
345
|
+
...(challenge.context?.trim() ? [] : ["context"]),
|
|
346
|
+
...(challenge.iv?.trim() ? [] : ["iv"]),
|
|
347
|
+
];
|
|
348
|
+
if (missingFields.length > 0) {
|
|
349
|
+
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "missing_challenge_input", {
|
|
350
|
+
missingFields,
|
|
351
|
+
phase: "create_task",
|
|
352
|
+
});
|
|
353
|
+
}
|
|
345
354
|
}
|
|
346
|
-
if (
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
!challenge.captchaScript?.trim() ||
|
|
350
|
-
!challenge.context?.trim() ||
|
|
351
|
-
!challenge.iv?.trim())
|
|
352
|
-
) {
|
|
353
|
-
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
|
|
355
|
+
if (challenge.kind === "recaptcha_v3" && challenge.minScore === undefined) {
|
|
356
|
+
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "missing_challenge_input", {
|
|
357
|
+
missingFields: ["minScore"],
|
|
354
358
|
phase: "create_task",
|
|
355
359
|
});
|
|
356
360
|
}
|
|
@@ -388,14 +392,49 @@ export function createTwoCaptchaResolverVendorAdapter(
|
|
|
388
392
|
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
389
393
|
...(proxy ?? {}),
|
|
390
394
|
}
|
|
391
|
-
|
|
392
|
-
|
|
395
|
+
: challenge.kind === "recaptcha_v2"
|
|
396
|
+
? {
|
|
397
|
+
type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
|
|
393
398
|
websiteURL: challenge.pageUrl,
|
|
394
399
|
websiteKey: challenge.siteKey,
|
|
395
400
|
isInvisible: false,
|
|
396
401
|
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
402
|
+
...(proxy ?? {}),
|
|
403
|
+
}
|
|
404
|
+
: challenge.kind === "recaptcha_v3"
|
|
405
|
+
? {
|
|
406
|
+
type: "RecaptchaV3TaskProxyless",
|
|
407
|
+
websiteURL: challenge.pageUrl,
|
|
408
|
+
websiteKey: challenge.siteKey,
|
|
409
|
+
minScore: challenge.minScore,
|
|
410
|
+
pageAction: challenge.action,
|
|
411
|
+
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
412
|
+
}
|
|
413
|
+
: challenge.kind === "hcaptcha"
|
|
414
|
+
? {
|
|
415
|
+
type: proxy ? "HCaptchaTask" : "HCaptchaTaskProxyless",
|
|
416
|
+
websiteURL: challenge.pageUrl,
|
|
417
|
+
websiteKey: challenge.siteKey,
|
|
418
|
+
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
419
|
+
...(proxy ?? {}),
|
|
420
|
+
}
|
|
421
|
+
: challenge.kind === "turnstile"
|
|
422
|
+
? {
|
|
423
|
+
type: proxy ? "TurnstileTask" : "TurnstileTaskProxyless",
|
|
424
|
+
websiteURL: challenge.pageUrl,
|
|
425
|
+
websiteKey: challenge.siteKey,
|
|
426
|
+
...(challenge.action !== undefined ? { action: challenge.action } : {}),
|
|
427
|
+
...(challenge.cdata !== undefined ? { data: challenge.cdata } : {}),
|
|
428
|
+
...(identity ? { userAgent: identity.userAgent } : {}),
|
|
397
429
|
...(proxy ?? {}),
|
|
398
|
-
}
|
|
430
|
+
}
|
|
431
|
+
: // `resolverVendorSupports` above already rejected every kind this
|
|
432
|
+
// adapter does not build a task for, so this branch is unreachable.
|
|
433
|
+
(() => {
|
|
434
|
+
throw new TypeError(
|
|
435
|
+
`2captcha resolver does not support ${challenge.kind}`,
|
|
436
|
+
);
|
|
437
|
+
})();
|
|
399
438
|
const createResult = await postJson(
|
|
400
439
|
fetchImpl,
|
|
401
440
|
endpoint(baseUrl, "createTask"),
|
|
@@ -8,15 +8,18 @@ import type { TraceRecorder } from "../trace.js";
|
|
|
8
8
|
|
|
9
9
|
export const RESOLVER_VENDOR_CAPABILITIES = {
|
|
10
10
|
browser: ["aws_waf", "cloudflare_interstitial"],
|
|
11
|
+
// Every kind listed per vendor is implemented by that vendor's adapter; the
|
|
12
|
+
// per-adapter "agrees with every declared capability" tests iterate this
|
|
13
|
+
// table, so adding a kind here without an implementation fails the suite.
|
|
14
|
+
// 2captcha omits `cloudflare_interstitial`, `akamai_sec_cpt`, and
|
|
15
|
+
// `akamai_sensor`: their API offers no task type for them, so declaring them
|
|
16
|
+
// would route challenges to a vendor that can only refuse.
|
|
11
17
|
"2captcha": [
|
|
12
18
|
"turnstile",
|
|
13
19
|
"recaptcha_v2",
|
|
14
20
|
"recaptcha_v3",
|
|
15
21
|
"hcaptcha",
|
|
16
|
-
"cloudflare_interstitial",
|
|
17
22
|
"aws_waf",
|
|
18
|
-
"akamai_sec_cpt",
|
|
19
|
-
"akamai_sensor",
|
|
20
23
|
],
|
|
21
24
|
capsolver: [
|
|
22
25
|
"turnstile",
|
|
@@ -114,6 +117,7 @@ export type ResolverVendorUnavailableReason =
|
|
|
114
117
|
| "missing_credentials"
|
|
115
118
|
| "missing_proxy_identity"
|
|
116
119
|
| "missing_client_profile"
|
|
120
|
+
| "missing_challenge_input"
|
|
117
121
|
| "missing_transport"
|
|
118
122
|
| "allocation_exhausted"
|
|
119
123
|
| "transport_failure"
|
|
@@ -125,6 +129,8 @@ export type ResolverChallengeVerdictReason = "human_puzzle" | "solve_failed";
|
|
|
125
129
|
type ResolverErrorOptions = {
|
|
126
130
|
/** Raw cause; adapters must not place bodies, cookies, headers, credentials, or proxy URLs here. */
|
|
127
131
|
readonly cause?: unknown;
|
|
132
|
+
/** Names of challenge fields required by this adapter but absent from this call's input. */
|
|
133
|
+
readonly missingFields?: readonly string[];
|
|
128
134
|
/** Upstream hostname only; never a URL. */
|
|
129
135
|
readonly upstreamHost?: string;
|
|
130
136
|
/** Adapter-defined sensor-loop phase, such as fetch_script or post_sensor. */
|
|
@@ -134,6 +140,7 @@ type ResolverErrorOptions = {
|
|
|
134
140
|
};
|
|
135
141
|
|
|
136
142
|
export class ResolverVendorUnavailableError extends Error {
|
|
143
|
+
readonly missingFields?: readonly string[];
|
|
137
144
|
readonly upstreamHost?: string;
|
|
138
145
|
readonly phase?: string;
|
|
139
146
|
readonly round?: number;
|
|
@@ -143,8 +150,19 @@ export class ResolverVendorUnavailableError extends Error {
|
|
|
143
150
|
readonly reason: ResolverVendorUnavailableReason,
|
|
144
151
|
options: ResolverErrorOptions = {},
|
|
145
152
|
) {
|
|
146
|
-
|
|
153
|
+
const missingFields =
|
|
154
|
+
reason === "missing_challenge_input"
|
|
155
|
+
? options.missingFields?.filter((field) => /^[A-Za-z][A-Za-z0-9_]*$/u.test(field))
|
|
156
|
+
: undefined;
|
|
157
|
+
super(
|
|
158
|
+
reason === "missing_challenge_input" && missingFields !== undefined && missingFields.length > 0
|
|
159
|
+
? `Resolver vendor ${vendor} cannot use incomplete challenge input; missing fields: ${missingFields.join(", ")}`
|
|
160
|
+
: `Resolver vendor ${vendor} is unavailable: ${reason}`,
|
|
161
|
+
);
|
|
147
162
|
this.name = "ResolverVendorUnavailableError";
|
|
163
|
+
if (missingFields !== undefined && missingFields.length > 0) {
|
|
164
|
+
this.missingFields = Object.freeze([...missingFields]);
|
|
165
|
+
}
|
|
148
166
|
if (options.cause !== undefined) {
|
|
149
167
|
this.cause = options.cause;
|
|
150
168
|
}
|
package/src/runtime/resolver.ts
CHANGED
|
@@ -71,7 +71,7 @@ type ResolvedResolverVendor =
|
|
|
71
71
|
| {
|
|
72
72
|
readonly vendor: Exclude<ProviderResolverVendor, "custom">;
|
|
73
73
|
readonly available: true;
|
|
74
|
-
readonly configuration: string;
|
|
74
|
+
readonly configuration: string | undefined;
|
|
75
75
|
}
|
|
76
76
|
| {
|
|
77
77
|
readonly vendor: ProviderResolverVendor;
|
|
@@ -82,6 +82,7 @@ type ResolvedResolverVendor =
|
|
|
82
82
|
type ResolverChainAttempt = {
|
|
83
83
|
readonly vendor: ProviderResolverVendor;
|
|
84
84
|
readonly reason: ResolverVendorUnavailableReason;
|
|
85
|
+
readonly missingFields?: readonly string[];
|
|
85
86
|
readonly cause?: {
|
|
86
87
|
readonly name: string;
|
|
87
88
|
readonly message: string;
|
|
@@ -184,13 +185,16 @@ export type ResolverInstrumentationMetadata = {
|
|
|
184
185
|
};
|
|
185
186
|
|
|
186
187
|
export type ResolverAdapterFactory = (
|
|
187
|
-
configuration: string,
|
|
188
|
+
configuration: string | undefined,
|
|
188
189
|
timeoutMs: number,
|
|
189
190
|
allowedHosts: readonly string[],
|
|
190
191
|
) => ResolverVendorAdapter;
|
|
191
192
|
|
|
192
193
|
const resolverAdapterRegistry: Partial<Record<ProviderResolverVendor, ResolverAdapterFactory>> = {
|
|
193
194
|
"2captcha"(configuration, timeoutMs, allowedHosts) {
|
|
195
|
+
if (configuration === undefined) {
|
|
196
|
+
throw new Error("2captcha resolver adapter factory requires an API key");
|
|
197
|
+
}
|
|
194
198
|
return createTwoCaptchaResolverVendorAdapter({
|
|
195
199
|
allowedHosts,
|
|
196
200
|
apiKey: configuration,
|
|
@@ -344,10 +348,21 @@ function throwUnsupportedKind(kind: ProviderChallengeKind): never {
|
|
|
344
348
|
}
|
|
345
349
|
|
|
346
350
|
function throwExhausted(attempts: readonly ResolverChainAttempt[]): never {
|
|
347
|
-
const
|
|
351
|
+
const hasMissingChallengeInput = attempts.some(
|
|
352
|
+
(attempt) => attempt.reason === "missing_challenge_input",
|
|
353
|
+
);
|
|
354
|
+
const summary = attempts
|
|
355
|
+
.map(({ vendor, reason, missingFields }) =>
|
|
356
|
+
missingFields === undefined || missingFields.length === 0
|
|
357
|
+
? `${vendor}: ${reason}`
|
|
358
|
+
: `${vendor}: ${reason} (missing fields: ${missingFields.join(", ")})`,
|
|
359
|
+
)
|
|
360
|
+
.join(", ");
|
|
348
361
|
throw new ProviderError(`Resolver vendor chain exhausted: ${summary}`, {
|
|
349
362
|
code: "RESOLVER_CHAIN_EXHAUSTED",
|
|
350
|
-
fix:
|
|
363
|
+
fix: hasMissingChallengeInput
|
|
364
|
+
? "Capture the named challenge fields or configure another supporting resolver vendor."
|
|
365
|
+
: "Configure another supporting resolver vendor or restore an unavailable vendor.",
|
|
351
366
|
details: attempts,
|
|
352
367
|
});
|
|
353
368
|
}
|
|
@@ -470,6 +485,7 @@ function unavailableAttempt(error: ResolverVendorUnavailableError): ResolverChai
|
|
|
470
485
|
return {
|
|
471
486
|
vendor: error.vendor,
|
|
472
487
|
reason: error.reason,
|
|
488
|
+
...(error.missingFields ? { missingFields: [...error.missingFields] } : {}),
|
|
473
489
|
...(cause ? { cause } : {}),
|
|
474
490
|
...(upstreamHost ? { upstreamHost } : {}),
|
|
475
491
|
...(phase ? { phase } : {}),
|
|
@@ -481,6 +497,7 @@ function unavailableSpanAttributes(error: ResolverVendorUnavailableError): Recor
|
|
|
481
497
|
const attempt = unavailableAttempt(error);
|
|
482
498
|
return {
|
|
483
499
|
unavailability_reason: error.reason,
|
|
500
|
+
missing_fields: error.missingFields,
|
|
484
501
|
cause_name: attempt.cause?.name,
|
|
485
502
|
cause_message: attempt.cause?.message,
|
|
486
503
|
upstream_host: attempt.upstreamHost,
|
|
@@ -914,6 +931,8 @@ function createResolverChainClient(options: {
|
|
|
914
931
|
} catch (error) {
|
|
915
932
|
signal.throwIfAborted();
|
|
916
933
|
if (!(error instanceof ResolverVendorUnavailableError)) throw error;
|
|
934
|
+
// Every vendor-unavailable result, including missing_challenge_input, falls
|
|
935
|
+
// through so another adapter can solve with a different input contract.
|
|
917
936
|
attempts.push(unavailableAttempt(error));
|
|
918
937
|
}
|
|
919
938
|
}
|
|
@@ -966,15 +985,20 @@ function resolveVendorAvailability(
|
|
|
966
985
|
reason: "missing_transport",
|
|
967
986
|
};
|
|
968
987
|
}
|
|
988
|
+
if (vendor === "browser") {
|
|
989
|
+
return {
|
|
990
|
+
vendor,
|
|
991
|
+
available: true,
|
|
992
|
+
configuration: normalizedEnvValue(env, APIFUSE__CDP_POOL__URL),
|
|
993
|
+
};
|
|
994
|
+
}
|
|
969
995
|
|
|
970
996
|
const envKey =
|
|
971
997
|
vendor === "2captcha"
|
|
972
998
|
? APIFUSE__RESOLVER__2CAPTCHA__API_KEY
|
|
973
999
|
: vendor === "capsolver"
|
|
974
1000
|
? APIFUSE__RESOLVER__CAPSOLVER__API_KEY
|
|
975
|
-
:
|
|
976
|
-
? APIFUSE__RESOLVER__CAPMONSTER__API_KEY
|
|
977
|
-
: APIFUSE__CDP_POOL__URL;
|
|
1001
|
+
: APIFUSE__RESOLVER__CAPMONSTER__API_KEY;
|
|
978
1002
|
|
|
979
1003
|
const configuration = normalizedEnvValue(env, envKey);
|
|
980
1004
|
return configuration
|
|
@@ -39,7 +39,6 @@ import { createProviderCache } from "../runtime/cache.js";
|
|
|
39
39
|
import {
|
|
40
40
|
createProviderChoiceContext,
|
|
41
41
|
PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
|
|
42
|
-
PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV,
|
|
43
42
|
} from "../runtime/choice.js";
|
|
44
43
|
import { createCredentialContext } from "../runtime/credential.js";
|
|
45
44
|
import { createEnvContext } from "../runtime/env.js";
|
|
@@ -572,8 +571,18 @@ export function resolveProviderResolverIdentityScope(
|
|
|
572
571
|
});
|
|
573
572
|
}
|
|
574
573
|
|
|
575
|
-
function resolveOperationConnectionId(
|
|
576
|
-
|
|
574
|
+
function resolveOperationConnectionId(
|
|
575
|
+
request: Pick<OperationRequest, "connection" | "connectionId">,
|
|
576
|
+
): string | undefined {
|
|
577
|
+
// An empty string is a malformed identifier, not an identity: treat it as
|
|
578
|
+
// absent so it can never override a valid id or key a real scope. Requests
|
|
579
|
+
// without any usable id fall back to the documented missing-connection
|
|
580
|
+
// sentinel scope instead of scoping context/affinity/state under "".
|
|
581
|
+
return normalizeConnectionId(request.connection?.id) ?? normalizeConnectionId(request.connectionId);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function normalizeConnectionId(id: string | undefined): string | undefined {
|
|
585
|
+
return id === "" ? undefined : id;
|
|
577
586
|
}
|
|
578
587
|
|
|
579
588
|
function resolveNativeProxyPolicy(provider: ProviderDefinition): ProviderProxyPolicy | undefined {
|
|
@@ -627,7 +636,6 @@ function createProviderContext(
|
|
|
627
636
|
const env = createEnvContext([
|
|
628
637
|
...(provider.secrets?.map((secret) => secret.name) ?? []),
|
|
629
638
|
PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
|
|
630
|
-
PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV,
|
|
631
639
|
]);
|
|
632
640
|
const credential = createCredentialContext({
|
|
633
641
|
allowedKeys: provider.credential?.keys,
|
|
@@ -778,10 +786,27 @@ function createFlowContextStore(
|
|
|
778
786
|
};
|
|
779
787
|
}
|
|
780
788
|
|
|
789
|
+
export function resolveAuthFlowProxyAffinityKey(
|
|
790
|
+
provider: ProviderDefinition,
|
|
791
|
+
request: Pick<
|
|
792
|
+
AuthFlowRequest,
|
|
793
|
+
"connection" | "connectionId" | "externalRef" | "tenantId" | "providerId"
|
|
794
|
+
>,
|
|
795
|
+
): string {
|
|
796
|
+
return (
|
|
797
|
+
resolveOperationConnectionId(request) ??
|
|
798
|
+
request.externalRef ??
|
|
799
|
+
request.tenantId ??
|
|
800
|
+
request.providerId ??
|
|
801
|
+
provider.id
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
|
|
781
805
|
function createAuthFlowContext(
|
|
782
806
|
provider: ProviderDefinition,
|
|
783
807
|
request: AuthFlowRequest,
|
|
784
808
|
options: ProviderServerRuntimeOptions,
|
|
809
|
+
state: ProviderRuntimeState,
|
|
785
810
|
signal?: AbortSignal,
|
|
786
811
|
): {
|
|
787
812
|
context: FlowContext;
|
|
@@ -798,12 +823,7 @@ function createAuthFlowContext(
|
|
|
798
823
|
);
|
|
799
824
|
const proxyClientOptions = {
|
|
800
825
|
upstream: { proxy: provider.proxy },
|
|
801
|
-
affinityKey:
|
|
802
|
-
request.connectionId ??
|
|
803
|
-
request.externalRef ??
|
|
804
|
-
request.tenantId ??
|
|
805
|
-
request.providerId ??
|
|
806
|
-
provider.id,
|
|
826
|
+
affinityKey: resolveAuthFlowProxyAffinityKey(provider, request),
|
|
807
827
|
};
|
|
808
828
|
const resolverIdentityScope = resolveProviderResolverIdentityScope(
|
|
809
829
|
provider,
|
|
@@ -838,7 +858,7 @@ function createAuthFlowContext(
|
|
|
838
858
|
return {
|
|
839
859
|
context: {
|
|
840
860
|
flowId: request.flowId,
|
|
841
|
-
connectionId: request
|
|
861
|
+
connectionId: resolveOperationConnectionId(request),
|
|
842
862
|
externalRef: request.externalRef,
|
|
843
863
|
tenantId: request.tenantId ?? "",
|
|
844
864
|
providerId: request.providerId ?? provider.id,
|
|
@@ -846,6 +866,7 @@ function createAuthFlowContext(
|
|
|
846
866
|
...proxyClientOptions,
|
|
847
867
|
...(signal ? { signal } : {}),
|
|
848
868
|
}),
|
|
869
|
+
state: state.forConnection(resolveOperationConnectionId(request)),
|
|
849
870
|
stealth: stealthBaseUrl
|
|
850
871
|
? capabilityModules.stealth
|
|
851
872
|
? stealthProfile
|
|
@@ -2007,6 +2028,7 @@ async function handleAuthFlow(
|
|
|
2007
2028
|
request: AuthFlowRequest,
|
|
2008
2029
|
route: AuthRoute,
|
|
2009
2030
|
options: ProviderServerRuntimeOptions,
|
|
2031
|
+
state: ProviderRuntimeState,
|
|
2010
2032
|
signal?: AbortSignal,
|
|
2011
2033
|
): Promise<Response | AuthFlowResponse> {
|
|
2012
2034
|
const flow = provider.auth?.flow;
|
|
@@ -2021,7 +2043,7 @@ async function handleAuthFlow(
|
|
|
2021
2043
|
// any flow code runs instead of at whatever point the ceremony first reads
|
|
2022
2044
|
// the env. `abort` stays exempt: a user must always be able to cancel a
|
|
2023
2045
|
// stranded flow even when provisioning is broken.
|
|
2024
|
-
const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
|
|
2046
|
+
const { context, getPatch } = createAuthFlowContext(provider, request, options, state, signal);
|
|
2025
2047
|
try {
|
|
2026
2048
|
if (route !== "abort") {
|
|
2027
2049
|
assertRequiredSecretsPresent(provider, context.env);
|
|
@@ -2542,7 +2564,14 @@ function createServerAppWithCapabilityModules(
|
|
|
2542
2564
|
.json()
|
|
2543
2565
|
.catch(() => undefined);
|
|
2544
2566
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2545
|
-
const response = await handleAuthFlow(
|
|
2567
|
+
const response = await handleAuthFlow(
|
|
2568
|
+
provider,
|
|
2569
|
+
body,
|
|
2570
|
+
"start",
|
|
2571
|
+
options,
|
|
2572
|
+
state,
|
|
2573
|
+
c.req.raw.signal,
|
|
2574
|
+
);
|
|
2546
2575
|
logProviderSuccess(
|
|
2547
2576
|
logger,
|
|
2548
2577
|
provider,
|
|
@@ -2582,7 +2611,14 @@ function createServerAppWithCapabilityModules(
|
|
|
2582
2611
|
.json()
|
|
2583
2612
|
.catch(() => undefined);
|
|
2584
2613
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2585
|
-
const response = await handleAuthFlow(
|
|
2614
|
+
const response = await handleAuthFlow(
|
|
2615
|
+
provider,
|
|
2616
|
+
body,
|
|
2617
|
+
"continue",
|
|
2618
|
+
options,
|
|
2619
|
+
state,
|
|
2620
|
+
c.req.raw.signal,
|
|
2621
|
+
);
|
|
2586
2622
|
logProviderSuccess(
|
|
2587
2623
|
logger,
|
|
2588
2624
|
provider,
|
|
@@ -2622,7 +2658,14 @@ function createServerAppWithCapabilityModules(
|
|
|
2622
2658
|
.json()
|
|
2623
2659
|
.catch(() => undefined);
|
|
2624
2660
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2625
|
-
const response = await handleAuthFlow(
|
|
2661
|
+
const response = await handleAuthFlow(
|
|
2662
|
+
provider,
|
|
2663
|
+
body,
|
|
2664
|
+
"poll",
|
|
2665
|
+
options,
|
|
2666
|
+
state,
|
|
2667
|
+
c.req.raw.signal,
|
|
2668
|
+
);
|
|
2626
2669
|
logProviderSuccess(
|
|
2627
2670
|
logger,
|
|
2628
2671
|
provider,
|
|
@@ -2662,7 +2705,14 @@ function createServerAppWithCapabilityModules(
|
|
|
2662
2705
|
.json()
|
|
2663
2706
|
.catch(() => undefined);
|
|
2664
2707
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2665
|
-
const response = await handleAuthFlow(
|
|
2708
|
+
const response = await handleAuthFlow(
|
|
2709
|
+
provider,
|
|
2710
|
+
body,
|
|
2711
|
+
"refresh",
|
|
2712
|
+
options,
|
|
2713
|
+
state,
|
|
2714
|
+
c.req.raw.signal,
|
|
2715
|
+
);
|
|
2666
2716
|
logProviderSuccess(
|
|
2667
2717
|
logger,
|
|
2668
2718
|
provider,
|
|
@@ -2702,7 +2752,14 @@ function createServerAppWithCapabilityModules(
|
|
|
2702
2752
|
.json()
|
|
2703
2753
|
.catch(() => undefined);
|
|
2704
2754
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2705
|
-
const response = await handleAuthFlow(
|
|
2755
|
+
const response = await handleAuthFlow(
|
|
2756
|
+
provider,
|
|
2757
|
+
body,
|
|
2758
|
+
"abort",
|
|
2759
|
+
options,
|
|
2760
|
+
state,
|
|
2761
|
+
c.req.raw.signal,
|
|
2762
|
+
);
|
|
2706
2763
|
logProviderSuccess(
|
|
2707
2764
|
logger,
|
|
2708
2765
|
provider,
|
package/src/testing/run.ts
CHANGED
|
@@ -377,6 +377,7 @@ function createUpstreamContext(
|
|
|
377
377
|
id: `standard-test-${operationName}`,
|
|
378
378
|
url: async () => currentUrl,
|
|
379
379
|
title: async () => currentResponse?.text.match(/<title[^>]*>([^<]*)<\/title>/i)?.[1] ?? "",
|
|
380
|
+
userAgent: async () => String((await browserAction("userAgent")).data),
|
|
380
381
|
content: async () => currentResponse?.text ?? "",
|
|
381
382
|
evaluate: async <T>(fn: string | (() => T)) =>
|
|
382
383
|
(await browserAction("evaluate", typeof fn === "string" ? fn : String(fn))).data as T,
|
package/src/types.ts
CHANGED
|
@@ -1757,7 +1757,7 @@ export interface BrowserCookie {
|
|
|
1757
1757
|
readonly sameSite?: "Strict" | "Lax" | "None";
|
|
1758
1758
|
}
|
|
1759
1759
|
|
|
1760
|
-
export type BrowserResourceMethod = "GET" | "HEAD";
|
|
1760
|
+
export type BrowserResourceMethod = "GET" | "HEAD" | "POST";
|
|
1761
1761
|
|
|
1762
1762
|
export type BrowserResourceRequest = {
|
|
1763
1763
|
readonly url: string;
|
|
@@ -1769,6 +1769,9 @@ export type BrowserResourceRequest = {
|
|
|
1769
1769
|
export type BrowserResourceBody = Buffer | Uint8Array | ArrayBuffer | string;
|
|
1770
1770
|
|
|
1771
1771
|
export type BrowserResourceDecision =
|
|
1772
|
+
| {
|
|
1773
|
+
readonly action: "continue";
|
|
1774
|
+
}
|
|
1772
1775
|
| {
|
|
1773
1776
|
readonly action: "fulfill";
|
|
1774
1777
|
readonly status?: number;
|
|
@@ -1793,11 +1796,19 @@ export type BrowserResourceRoute = {
|
|
|
1793
1796
|
export type BrowserResourcePolicy = {
|
|
1794
1797
|
readonly defaultAction?: "block";
|
|
1795
1798
|
readonly allowedMethods?: readonly BrowserResourceMethod[];
|
|
1799
|
+
/**
|
|
1800
|
+
* Appends an enforcing CSP header to every renderable document response
|
|
1801
|
+
* while the policy is active. Existing CSP headers are retained, so this
|
|
1802
|
+
* can only further restrict the document.
|
|
1803
|
+
*/
|
|
1804
|
+
readonly documentContentSecurityPolicy?: string;
|
|
1796
1805
|
readonly routes: readonly BrowserResourceRoute[];
|
|
1797
1806
|
};
|
|
1798
1807
|
|
|
1799
1808
|
export interface BrowserPage extends BrowserFrame {
|
|
1800
1809
|
close(): Promise<void>;
|
|
1810
|
+
/** Returns the user agent used by this page's browser context. */
|
|
1811
|
+
userAgent(): Promise<string>;
|
|
1801
1812
|
/**
|
|
1802
1813
|
* Reads the browser context's cookie jar, including httpOnly cookies.
|
|
1803
1814
|
* Cookie expiry values are Unix seconds and are absent for session cookies.
|
|
@@ -1905,7 +1916,7 @@ export type ProviderChoiceExplicitParseResult =
|
|
|
1905
1916
|
readonly payload: Record<string, unknown>;
|
|
1906
1917
|
/** Stable, opaque key for provider-owned idempotency records. */
|
|
1907
1918
|
readonly replayKey: string;
|
|
1908
|
-
/** Atomically claims a word token.
|
|
1919
|
+
/** Atomically claims a word token. Inline tokens report unsupported. */
|
|
1909
1920
|
consume(): Promise<ProviderChoiceConsumeResult>;
|
|
1910
1921
|
}
|
|
1911
1922
|
| {
|
|
@@ -1961,7 +1972,7 @@ export interface ProviderChoiceParseOptions {
|
|
|
1961
1972
|
futureToleranceMs?: number;
|
|
1962
1973
|
bind?: ProviderChoiceBindingOptions;
|
|
1963
1974
|
storage?: ProviderChoiceStorageOptions;
|
|
1964
|
-
/** Defaults to never,
|
|
1975
|
+
/** Defaults to never, preserving reusable choice-token parse semantics. */
|
|
1965
1976
|
consume?: ProviderChoiceConsumeMode;
|
|
1966
1977
|
}
|
|
1967
1978
|
|
|
@@ -2115,6 +2126,18 @@ export interface FlowContext {
|
|
|
2115
2126
|
tenantId: string;
|
|
2116
2127
|
providerId: string;
|
|
2117
2128
|
http: HttpClient;
|
|
2129
|
+
/** Durable connection-scoped runtime state. Present when the host runtime
|
|
2130
|
+
* supplies one; auth ceremonies must fail closed when absent rather than
|
|
2131
|
+
* fall back to bypassable in-process storage.
|
|
2132
|
+
*
|
|
2133
|
+
* Scoped via `ProviderRuntimeState.forConnection`: requests that resolve no
|
|
2134
|
+
* connection id (pre-connection ceremonies such as first-time logins) share
|
|
2135
|
+
* the documented isolated missing-connection scope. That sharing is the
|
|
2136
|
+
* intended semantic — it lets counters keyed by caller identity (e.g. a
|
|
2137
|
+
* login email) persist across separate ceremonies for the same caller.
|
|
2138
|
+
* Flows storing entries in that scope MUST key them by caller identity;
|
|
2139
|
+
* un-keyed entries would be shared across all connectionless ceremonies. */
|
|
2140
|
+
readonly state?: ProviderRuntimeState;
|
|
2118
2141
|
/** Present when the selected runtime supplies native network capabilities. */
|
|
2119
2142
|
readonly native?: NativeProviderContext;
|
|
2120
2143
|
stealth: StealthClient;
|