@apifuse/provider-sdk 2.2.0-beta.33 → 2.2.0-beta.36
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 -1
- package/dist/ceremonies/index.d.ts +8 -0
- package/dist/ceremonies/index.js +32 -25
- package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
- package/dist/define.d.ts +7 -4
- package/dist/define.js +353 -0
- package/dist/index.d.ts +1 -1
- package/dist/runtime/auth-flow.d.ts +3 -1
- package/dist/runtime/auth-flow.js +1 -0
- package/dist/runtime/browser.js +6 -0
- package/dist/runtime/choice.js +33 -55
- package/dist/runtime/resolver-vendors/browser.js +1 -1
- package/dist/runtime/resolver-vendors/twocaptcha.js +15 -9
- package/dist/runtime/resolver-vendors/types.d.ts +4 -1
- package/dist/runtime/resolver-vendors/types.js +10 -1
- package/dist/runtime/resolver.js +13 -2
- package/dist/server/serve-implementation.d.ts +2 -1
- package/dist/server/serve-implementation.js +26 -15
- package/dist/testing/run.js +1 -0
- package/dist/types.d.ts +16 -2
- package/package.json +8 -2
- package/src/ceremonies/index.ts +45 -31
- package/src/cli/templates/provider/Dockerfile.tpl +1 -1
- package/src/define.ts +327 -8
- package/src/index.ts +1 -0
- package/src/runtime/auth-flow.ts +4 -0
- package/src/runtime/browser.ts +8 -0
- package/src/runtime/choice.ts +34 -68
- package/src/runtime/resolver-vendors/browser.ts +1 -1
- package/src/runtime/resolver-vendors/twocaptcha.ts +15 -11
- package/src/runtime/resolver-vendors/types.ts +16 -1
- package/src/runtime/resolver.ts +18 -2
- package/src/server/serve-implementation.ts +74 -15
- package/src/testing/run.ts +1 -0
- package/src/types.ts +16 -2
package/src/runtime/choice.ts
CHANGED
|
@@ -263,10 +263,9 @@ export function createProviderChoiceContext(
|
|
|
263
263
|
);
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
// consumption, and binding failures can never enter this branch.
|
|
266
|
+
// Inline choices continue to use the encrypted envelope. A structurally
|
|
267
|
+
// valid word token returns above, so lookup, expiry, consumption, and
|
|
268
|
+
// binding failures can never enter this branch.
|
|
270
269
|
try {
|
|
271
270
|
const [actualPrefix, tokenKid, encodedIv, encryptedPayload, authTag, signature] =
|
|
272
271
|
parseManagedChoiceTokenParts(parseOptions.token);
|
|
@@ -321,17 +320,14 @@ export function createProviderChoiceContext(
|
|
|
321
320
|
required: true,
|
|
322
321
|
}),
|
|
323
322
|
});
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
contextState: options.state,
|
|
329
|
-
})
|
|
330
|
-
: envelope.payload;
|
|
323
|
+
if (isServerChoiceHandlePayload(envelope.payload)) {
|
|
324
|
+
throw wordChoiceNotFoundError();
|
|
325
|
+
}
|
|
326
|
+
const payload = envelope.payload;
|
|
331
327
|
const parsed =
|
|
332
328
|
consumeMode === "explicit"
|
|
333
329
|
? Promise.resolve(payload).then((resolvedPayload) =>
|
|
334
|
-
|
|
330
|
+
createInlineExplicitParseResult({
|
|
335
331
|
payload: resolvedPayload,
|
|
336
332
|
replayKey: digestChoiceReplayKey(parseOptions.token),
|
|
337
333
|
onConsume: () =>
|
|
@@ -471,7 +467,7 @@ function emitChoiceTelemetry(
|
|
|
471
467
|
}
|
|
472
468
|
}
|
|
473
469
|
|
|
474
|
-
function
|
|
470
|
+
function createInlineExplicitParseResult(options: {
|
|
475
471
|
readonly payload: ProviderChoiceTokenPayload;
|
|
476
472
|
readonly replayKey: string;
|
|
477
473
|
readonly onConsume: () => void;
|
|
@@ -656,14 +652,6 @@ async function parseWordServerStoredChoice(options: {
|
|
|
656
652
|
) {
|
|
657
653
|
throw wordChoiceNotFoundError();
|
|
658
654
|
}
|
|
659
|
-
assertFreshProviderChoiceIssuedAt(record.issued_at_ms, {
|
|
660
|
-
ttlMs:
|
|
661
|
-
options.parseOptions.ttlMs != null
|
|
662
|
-
? Math.min(options.parseOptions.ttlMs, record.ttl_ms)
|
|
663
|
-
: record.ttl_ms,
|
|
664
|
-
nowMs: options.parseOptions.nowMs,
|
|
665
|
-
futureToleranceMs: options.parseOptions.futureToleranceMs,
|
|
666
|
-
});
|
|
667
655
|
assertPayloadDigestMatches({
|
|
668
656
|
actual: digestChoicePayload(serializeChoicePayload(record.payload)),
|
|
669
657
|
expected: record.payload_digest,
|
|
@@ -685,6 +673,31 @@ async function parseWordServerStoredChoice(options: {
|
|
|
685
673
|
}
|
|
686
674
|
throw error;
|
|
687
675
|
}
|
|
676
|
+
// Freshness is classified last, reachable only after every identity,
|
|
677
|
+
// integrity, and binding check above has passed (ADR 0006, amended
|
|
678
|
+
// 2026-08-20): a caller that proved the record's binding may observe the
|
|
679
|
+
// canonical stale error, while an unbound record keeps the collapsed
|
|
680
|
+
// not-found error so expiry never becomes an existence signal for
|
|
681
|
+
// guessable tokens.
|
|
682
|
+
try {
|
|
683
|
+
assertFreshProviderChoiceIssuedAt(record.issued_at_ms, {
|
|
684
|
+
ttlMs:
|
|
685
|
+
options.parseOptions.ttlMs != null
|
|
686
|
+
? Math.min(options.parseOptions.ttlMs, record.ttl_ms)
|
|
687
|
+
: record.ttl_ms,
|
|
688
|
+
nowMs: options.parseOptions.nowMs,
|
|
689
|
+
futureToleranceMs: options.parseOptions.futureToleranceMs,
|
|
690
|
+
});
|
|
691
|
+
} catch (error) {
|
|
692
|
+
const recordIsBound = Boolean(
|
|
693
|
+
record.binding?.connection_hash || record.binding?.credential_hash,
|
|
694
|
+
);
|
|
695
|
+
if (recordIsBound && error instanceof ProviderChoiceTokenError && error.reason === "stale") {
|
|
696
|
+
throw error;
|
|
697
|
+
}
|
|
698
|
+
if (error instanceof ProviderChoiceTokenError) throw wordChoiceNotFoundError();
|
|
699
|
+
throw error;
|
|
700
|
+
}
|
|
688
701
|
|
|
689
702
|
const consumeMode = options.parseOptions.consume ?? "never";
|
|
690
703
|
if (record.status === "consumed") {
|
|
@@ -759,53 +772,6 @@ async function consumeWordServerStoredChoice(options: {
|
|
|
759
772
|
}
|
|
760
773
|
}
|
|
761
774
|
|
|
762
|
-
async function parseLegacyServerStoredChoice(options: {
|
|
763
|
-
readonly handle: ServerChoiceHandlePayload;
|
|
764
|
-
readonly storage?: ProviderChoiceStorageOptions;
|
|
765
|
-
readonly contextState?: ProviderRuntimeState;
|
|
766
|
-
}): Promise<ProviderChoiceTokenPayload> {
|
|
767
|
-
const storage = resolveParseStorage(options.storage);
|
|
768
|
-
const namespace = resolveChoiceStateNamespace({
|
|
769
|
-
storage,
|
|
770
|
-
contextState: options.contextState,
|
|
771
|
-
});
|
|
772
|
-
// Reading a server-stored choice back deserializes a persisted value. A
|
|
773
|
-
// corrupt/undecodable value would otherwise surface as a raw JSON.parse
|
|
774
|
-
// SyntaxError (or another unexpected throwable) that escapes the choice error
|
|
775
|
-
// taxonomy, gets masked as internal_error 500, and is treated as retryable by
|
|
776
|
-
// the hub -> reservation restart loop (2026-07-22 catchtable RCA, candidate A).
|
|
777
|
-
// Convert any non-branded throwable into a branded invalid_payload so it maps
|
|
778
|
-
// to a clean, non-retryable 400. Branded ProviderChoiceTokenError and genuine
|
|
779
|
-
// ProviderError (e.g. Redis-unavailable / state-unavailable) pass through so
|
|
780
|
-
// their category/retryable semantics are preserved.
|
|
781
|
-
let record: StateValue<ProviderChoiceTokenPayload> | null;
|
|
782
|
-
try {
|
|
783
|
-
record = await namespace.get<ProviderChoiceTokenPayload>(
|
|
784
|
-
optionsStateKey(options.handle.state_id),
|
|
785
|
-
);
|
|
786
|
-
} catch (error) {
|
|
787
|
-
if (error instanceof ProviderChoiceTokenError || isProviderError(error)) {
|
|
788
|
-
throw error;
|
|
789
|
-
}
|
|
790
|
-
throw new ProviderChoiceTokenError(
|
|
791
|
-
"invalid_payload",
|
|
792
|
-
"Provider choice token state payload could not be decoded.",
|
|
793
|
-
);
|
|
794
|
-
}
|
|
795
|
-
if (!record) {
|
|
796
|
-
throw new ProviderChoiceTokenError(
|
|
797
|
-
"invalid_payload",
|
|
798
|
-
"Provider choice token state payload is missing.",
|
|
799
|
-
);
|
|
800
|
-
}
|
|
801
|
-
const serializedPayload = serializeChoicePayload(record.value);
|
|
802
|
-
assertPayloadDigestMatches({
|
|
803
|
-
actual: digestChoicePayload(serializedPayload),
|
|
804
|
-
expected: options.handle.payload_digest,
|
|
805
|
-
});
|
|
806
|
-
return record.value;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
775
|
function generateChoiceWordSequence(wordCount: number): string {
|
|
810
776
|
return Array.from({ length: wordCount }, () =>
|
|
811
777
|
choiceWordAt(randomInt(CHOICE_WORDLIST_SIZE)),
|
|
@@ -242,7 +242,7 @@ async function solveInPage(
|
|
|
242
242
|
},
|
|
243
243
|
async () => {
|
|
244
244
|
const userAgent = await raceWithAbort(
|
|
245
|
-
() => page.
|
|
245
|
+
() => page.userAgent(),
|
|
246
246
|
signal,
|
|
247
247
|
);
|
|
248
248
|
await raceWithAbort(() => page.goto(pageUrl), signal);
|
|
@@ -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 (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
+
}
|
|
351
354
|
}
|
|
352
355
|
if (challenge.kind === "recaptcha_v3" && challenge.minScore === undefined) {
|
|
353
|
-
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "
|
|
356
|
+
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "missing_challenge_input", {
|
|
357
|
+
missingFields: ["minScore"],
|
|
354
358
|
phase: "create_task",
|
|
355
359
|
});
|
|
356
360
|
}
|
|
@@ -117,6 +117,7 @@ export type ResolverVendorUnavailableReason =
|
|
|
117
117
|
| "missing_credentials"
|
|
118
118
|
| "missing_proxy_identity"
|
|
119
119
|
| "missing_client_profile"
|
|
120
|
+
| "missing_challenge_input"
|
|
120
121
|
| "missing_transport"
|
|
121
122
|
| "allocation_exhausted"
|
|
122
123
|
| "transport_failure"
|
|
@@ -128,6 +129,8 @@ export type ResolverChallengeVerdictReason = "human_puzzle" | "solve_failed";
|
|
|
128
129
|
type ResolverErrorOptions = {
|
|
129
130
|
/** Raw cause; adapters must not place bodies, cookies, headers, credentials, or proxy URLs here. */
|
|
130
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[];
|
|
131
134
|
/** Upstream hostname only; never a URL. */
|
|
132
135
|
readonly upstreamHost?: string;
|
|
133
136
|
/** Adapter-defined sensor-loop phase, such as fetch_script or post_sensor. */
|
|
@@ -137,6 +140,7 @@ type ResolverErrorOptions = {
|
|
|
137
140
|
};
|
|
138
141
|
|
|
139
142
|
export class ResolverVendorUnavailableError extends Error {
|
|
143
|
+
readonly missingFields?: readonly string[];
|
|
140
144
|
readonly upstreamHost?: string;
|
|
141
145
|
readonly phase?: string;
|
|
142
146
|
readonly round?: number;
|
|
@@ -146,8 +150,19 @@ export class ResolverVendorUnavailableError extends Error {
|
|
|
146
150
|
readonly reason: ResolverVendorUnavailableReason,
|
|
147
151
|
options: ResolverErrorOptions = {},
|
|
148
152
|
) {
|
|
149
|
-
|
|
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
|
+
);
|
|
150
162
|
this.name = "ResolverVendorUnavailableError";
|
|
163
|
+
if (missingFields !== undefined && missingFields.length > 0) {
|
|
164
|
+
this.missingFields = Object.freeze([...missingFields]);
|
|
165
|
+
}
|
|
151
166
|
if (options.cause !== undefined) {
|
|
152
167
|
this.cause = options.cause;
|
|
153
168
|
}
|
package/src/runtime/resolver.ts
CHANGED
|
@@ -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;
|
|
@@ -347,10 +348,21 @@ function throwUnsupportedKind(kind: ProviderChallengeKind): never {
|
|
|
347
348
|
}
|
|
348
349
|
|
|
349
350
|
function throwExhausted(attempts: readonly ResolverChainAttempt[]): never {
|
|
350
|
-
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(", ");
|
|
351
361
|
throw new ProviderError(`Resolver vendor chain exhausted: ${summary}`, {
|
|
352
362
|
code: "RESOLVER_CHAIN_EXHAUSTED",
|
|
353
|
-
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.",
|
|
354
366
|
details: attempts,
|
|
355
367
|
});
|
|
356
368
|
}
|
|
@@ -473,6 +485,7 @@ function unavailableAttempt(error: ResolverVendorUnavailableError): ResolverChai
|
|
|
473
485
|
return {
|
|
474
486
|
vendor: error.vendor,
|
|
475
487
|
reason: error.reason,
|
|
488
|
+
...(error.missingFields ? { missingFields: [...error.missingFields] } : {}),
|
|
476
489
|
...(cause ? { cause } : {}),
|
|
477
490
|
...(upstreamHost ? { upstreamHost } : {}),
|
|
478
491
|
...(phase ? { phase } : {}),
|
|
@@ -484,6 +497,7 @@ function unavailableSpanAttributes(error: ResolverVendorUnavailableError): Recor
|
|
|
484
497
|
const attempt = unavailableAttempt(error);
|
|
485
498
|
return {
|
|
486
499
|
unavailability_reason: error.reason,
|
|
500
|
+
missing_fields: error.missingFields,
|
|
487
501
|
cause_name: attempt.cause?.name,
|
|
488
502
|
cause_message: attempt.cause?.message,
|
|
489
503
|
upstream_host: attempt.upstreamHost,
|
|
@@ -917,6 +931,8 @@ function createResolverChainClient(options: {
|
|
|
917
931
|
} catch (error) {
|
|
918
932
|
signal.throwIfAborted();
|
|
919
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.
|
|
920
936
|
attempts.push(unavailableAttempt(error));
|
|
921
937
|
}
|
|
922
938
|
}
|
|
@@ -571,8 +571,18 @@ export function resolveProviderResolverIdentityScope(
|
|
|
571
571
|
});
|
|
572
572
|
}
|
|
573
573
|
|
|
574
|
-
function resolveOperationConnectionId(
|
|
575
|
-
|
|
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;
|
|
576
586
|
}
|
|
577
587
|
|
|
578
588
|
function resolveNativeProxyPolicy(provider: ProviderDefinition): ProviderProxyPolicy | undefined {
|
|
@@ -776,10 +786,27 @@ function createFlowContextStore(
|
|
|
776
786
|
};
|
|
777
787
|
}
|
|
778
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
|
+
|
|
779
805
|
function createAuthFlowContext(
|
|
780
806
|
provider: ProviderDefinition,
|
|
781
807
|
request: AuthFlowRequest,
|
|
782
808
|
options: ProviderServerRuntimeOptions,
|
|
809
|
+
state: ProviderRuntimeState,
|
|
783
810
|
signal?: AbortSignal,
|
|
784
811
|
): {
|
|
785
812
|
context: FlowContext;
|
|
@@ -796,12 +823,7 @@ function createAuthFlowContext(
|
|
|
796
823
|
);
|
|
797
824
|
const proxyClientOptions = {
|
|
798
825
|
upstream: { proxy: provider.proxy },
|
|
799
|
-
affinityKey:
|
|
800
|
-
request.connectionId ??
|
|
801
|
-
request.externalRef ??
|
|
802
|
-
request.tenantId ??
|
|
803
|
-
request.providerId ??
|
|
804
|
-
provider.id,
|
|
826
|
+
affinityKey: resolveAuthFlowProxyAffinityKey(provider, request),
|
|
805
827
|
};
|
|
806
828
|
const resolverIdentityScope = resolveProviderResolverIdentityScope(
|
|
807
829
|
provider,
|
|
@@ -836,7 +858,7 @@ function createAuthFlowContext(
|
|
|
836
858
|
return {
|
|
837
859
|
context: {
|
|
838
860
|
flowId: request.flowId,
|
|
839
|
-
connectionId: request
|
|
861
|
+
connectionId: resolveOperationConnectionId(request),
|
|
840
862
|
externalRef: request.externalRef,
|
|
841
863
|
tenantId: request.tenantId ?? "",
|
|
842
864
|
providerId: request.providerId ?? provider.id,
|
|
@@ -844,6 +866,7 @@ function createAuthFlowContext(
|
|
|
844
866
|
...proxyClientOptions,
|
|
845
867
|
...(signal ? { signal } : {}),
|
|
846
868
|
}),
|
|
869
|
+
state: state.forConnection(resolveOperationConnectionId(request)),
|
|
847
870
|
stealth: stealthBaseUrl
|
|
848
871
|
? capabilityModules.stealth
|
|
849
872
|
? stealthProfile
|
|
@@ -2005,6 +2028,7 @@ async function handleAuthFlow(
|
|
|
2005
2028
|
request: AuthFlowRequest,
|
|
2006
2029
|
route: AuthRoute,
|
|
2007
2030
|
options: ProviderServerRuntimeOptions,
|
|
2031
|
+
state: ProviderRuntimeState,
|
|
2008
2032
|
signal?: AbortSignal,
|
|
2009
2033
|
): Promise<Response | AuthFlowResponse> {
|
|
2010
2034
|
const flow = provider.auth?.flow;
|
|
@@ -2019,7 +2043,7 @@ async function handleAuthFlow(
|
|
|
2019
2043
|
// any flow code runs instead of at whatever point the ceremony first reads
|
|
2020
2044
|
// the env. `abort` stays exempt: a user must always be able to cancel a
|
|
2021
2045
|
// stranded flow even when provisioning is broken.
|
|
2022
|
-
const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
|
|
2046
|
+
const { context, getPatch } = createAuthFlowContext(provider, request, options, state, signal);
|
|
2023
2047
|
try {
|
|
2024
2048
|
if (route !== "abort") {
|
|
2025
2049
|
assertRequiredSecretsPresent(provider, context.env);
|
|
@@ -2540,7 +2564,14 @@ function createServerAppWithCapabilityModules(
|
|
|
2540
2564
|
.json()
|
|
2541
2565
|
.catch(() => undefined);
|
|
2542
2566
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2543
|
-
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
|
+
);
|
|
2544
2575
|
logProviderSuccess(
|
|
2545
2576
|
logger,
|
|
2546
2577
|
provider,
|
|
@@ -2580,7 +2611,14 @@ function createServerAppWithCapabilityModules(
|
|
|
2580
2611
|
.json()
|
|
2581
2612
|
.catch(() => undefined);
|
|
2582
2613
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2583
|
-
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
|
+
);
|
|
2584
2622
|
logProviderSuccess(
|
|
2585
2623
|
logger,
|
|
2586
2624
|
provider,
|
|
@@ -2620,7 +2658,14 @@ function createServerAppWithCapabilityModules(
|
|
|
2620
2658
|
.json()
|
|
2621
2659
|
.catch(() => undefined);
|
|
2622
2660
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2623
|
-
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
|
+
);
|
|
2624
2669
|
logProviderSuccess(
|
|
2625
2670
|
logger,
|
|
2626
2671
|
provider,
|
|
@@ -2660,7 +2705,14 @@ function createServerAppWithCapabilityModules(
|
|
|
2660
2705
|
.json()
|
|
2661
2706
|
.catch(() => undefined);
|
|
2662
2707
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2663
|
-
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
|
+
);
|
|
2664
2716
|
logProviderSuccess(
|
|
2665
2717
|
logger,
|
|
2666
2718
|
provider,
|
|
@@ -2700,7 +2752,14 @@ function createServerAppWithCapabilityModules(
|
|
|
2700
2752
|
.json()
|
|
2701
2753
|
.catch(() => undefined);
|
|
2702
2754
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2703
|
-
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
|
+
);
|
|
2704
2763
|
logProviderSuccess(
|
|
2705
2764
|
logger,
|
|
2706
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
|
@@ -1807,6 +1807,8 @@ export type BrowserResourcePolicy = {
|
|
|
1807
1807
|
|
|
1808
1808
|
export interface BrowserPage extends BrowserFrame {
|
|
1809
1809
|
close(): Promise<void>;
|
|
1810
|
+
/** Returns the user agent used by this page's browser context. */
|
|
1811
|
+
userAgent(): Promise<string>;
|
|
1810
1812
|
/**
|
|
1811
1813
|
* Reads the browser context's cookie jar, including httpOnly cookies.
|
|
1812
1814
|
* Cookie expiry values are Unix seconds and are absent for session cookies.
|
|
@@ -1914,7 +1916,7 @@ export type ProviderChoiceExplicitParseResult =
|
|
|
1914
1916
|
readonly payload: Record<string, unknown>;
|
|
1915
1917
|
/** Stable, opaque key for provider-owned idempotency records. */
|
|
1916
1918
|
readonly replayKey: string;
|
|
1917
|
-
/** Atomically claims a word token.
|
|
1919
|
+
/** Atomically claims a word token. Inline tokens report unsupported. */
|
|
1918
1920
|
consume(): Promise<ProviderChoiceConsumeResult>;
|
|
1919
1921
|
}
|
|
1920
1922
|
| {
|
|
@@ -1970,7 +1972,7 @@ export interface ProviderChoiceParseOptions {
|
|
|
1970
1972
|
futureToleranceMs?: number;
|
|
1971
1973
|
bind?: ProviderChoiceBindingOptions;
|
|
1972
1974
|
storage?: ProviderChoiceStorageOptions;
|
|
1973
|
-
/** Defaults to never,
|
|
1975
|
+
/** Defaults to never, preserving reusable choice-token parse semantics. */
|
|
1974
1976
|
consume?: ProviderChoiceConsumeMode;
|
|
1975
1977
|
}
|
|
1976
1978
|
|
|
@@ -2124,6 +2126,18 @@ export interface FlowContext {
|
|
|
2124
2126
|
tenantId: string;
|
|
2125
2127
|
providerId: string;
|
|
2126
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;
|
|
2127
2141
|
/** Present when the selected runtime supplies native network capabilities. */
|
|
2128
2142
|
readonly native?: NativeProviderContext;
|
|
2129
2143
|
stealth: StealthClient;
|