@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/dist/runtime/choice.js
CHANGED
|
@@ -117,10 +117,9 @@ export function createProviderChoiceContext(options) {
|
|
|
117
117
|
consumeMode,
|
|
118
118
|
});
|
|
119
119
|
}
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
// consumption, and binding failures can never enter this branch.
|
|
120
|
+
// Inline choices continue to use the encrypted envelope. A structurally
|
|
121
|
+
// valid word token returns above, so lookup, expiry, consumption, and
|
|
122
|
+
// binding failures can never enter this branch.
|
|
124
123
|
try {
|
|
125
124
|
const [actualPrefix, tokenKid, encodedIv, encryptedPayload, authTag, signature] = parseManagedChoiceTokenParts(parseOptions.token);
|
|
126
125
|
if (actualPrefix !== parseOptions.prefix ||
|
|
@@ -166,15 +165,12 @@ export function createProviderChoiceContext(options) {
|
|
|
166
165
|
required: true,
|
|
167
166
|
}),
|
|
168
167
|
});
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
contextState: options.state,
|
|
174
|
-
})
|
|
175
|
-
: envelope.payload;
|
|
168
|
+
if (isServerChoiceHandlePayload(envelope.payload)) {
|
|
169
|
+
throw wordChoiceNotFoundError();
|
|
170
|
+
}
|
|
171
|
+
const payload = envelope.payload;
|
|
176
172
|
const parsed = consumeMode === "explicit"
|
|
177
|
-
? Promise.resolve(payload).then((resolvedPayload) =>
|
|
173
|
+
? Promise.resolve(payload).then((resolvedPayload) => createInlineExplicitParseResult({
|
|
178
174
|
payload: resolvedPayload,
|
|
179
175
|
replayKey: digestChoiceReplayKey(parseOptions.token),
|
|
180
176
|
onConsume: () => emitChoiceTelemetry(options.onTelemetry, {
|
|
@@ -274,7 +270,7 @@ function emitChoiceTelemetry(onTelemetry, event) {
|
|
|
274
270
|
// Observability must never change provider token semantics.
|
|
275
271
|
}
|
|
276
272
|
}
|
|
277
|
-
function
|
|
273
|
+
function createInlineExplicitParseResult(options) {
|
|
278
274
|
return {
|
|
279
275
|
status: "active",
|
|
280
276
|
payload: options.payload,
|
|
@@ -411,13 +407,6 @@ async function parseWordServerStoredChoice(options) {
|
|
|
411
407
|
record.prefix !== options.parseOptions.prefix) {
|
|
412
408
|
throw wordChoiceNotFoundError();
|
|
413
409
|
}
|
|
414
|
-
assertFreshProviderChoiceIssuedAt(record.issued_at_ms, {
|
|
415
|
-
ttlMs: options.parseOptions.ttlMs != null
|
|
416
|
-
? Math.min(options.parseOptions.ttlMs, record.ttl_ms)
|
|
417
|
-
: record.ttl_ms,
|
|
418
|
-
nowMs: options.parseOptions.nowMs,
|
|
419
|
-
futureToleranceMs: options.parseOptions.futureToleranceMs,
|
|
420
|
-
});
|
|
421
410
|
assertPayloadDigestMatches({
|
|
422
411
|
actual: digestChoicePayload(serializeChoicePayload(record.payload)),
|
|
423
412
|
expected: record.payload_digest,
|
|
@@ -438,6 +427,30 @@ async function parseWordServerStoredChoice(options) {
|
|
|
438
427
|
}
|
|
439
428
|
throw error;
|
|
440
429
|
}
|
|
430
|
+
// Freshness is classified last, reachable only after every identity,
|
|
431
|
+
// integrity, and binding check above has passed (ADR 0006, amended
|
|
432
|
+
// 2026-08-20): a caller that proved the record's binding may observe the
|
|
433
|
+
// canonical stale error, while an unbound record keeps the collapsed
|
|
434
|
+
// not-found error so expiry never becomes an existence signal for
|
|
435
|
+
// guessable tokens.
|
|
436
|
+
try {
|
|
437
|
+
assertFreshProviderChoiceIssuedAt(record.issued_at_ms, {
|
|
438
|
+
ttlMs: options.parseOptions.ttlMs != null
|
|
439
|
+
? Math.min(options.parseOptions.ttlMs, record.ttl_ms)
|
|
440
|
+
: record.ttl_ms,
|
|
441
|
+
nowMs: options.parseOptions.nowMs,
|
|
442
|
+
futureToleranceMs: options.parseOptions.futureToleranceMs,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
const recordIsBound = Boolean(record.binding?.connection_hash || record.binding?.credential_hash);
|
|
447
|
+
if (recordIsBound && error instanceof ProviderChoiceTokenError && error.reason === "stale") {
|
|
448
|
+
throw error;
|
|
449
|
+
}
|
|
450
|
+
if (error instanceof ProviderChoiceTokenError)
|
|
451
|
+
throw wordChoiceNotFoundError();
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
441
454
|
const consumeMode = options.parseOptions.consume ?? "never";
|
|
442
455
|
if (record.status === "consumed") {
|
|
443
456
|
if (consumeMode === "explicit") {
|
|
@@ -502,41 +515,6 @@ async function consumeWordServerStoredChoice(options) {
|
|
|
502
515
|
throw wordChoiceNotFoundError();
|
|
503
516
|
}
|
|
504
517
|
}
|
|
505
|
-
async function parseLegacyServerStoredChoice(options) {
|
|
506
|
-
const storage = resolveParseStorage(options.storage);
|
|
507
|
-
const namespace = resolveChoiceStateNamespace({
|
|
508
|
-
storage,
|
|
509
|
-
contextState: options.contextState,
|
|
510
|
-
});
|
|
511
|
-
// Reading a server-stored choice back deserializes a persisted value. A
|
|
512
|
-
// corrupt/undecodable value would otherwise surface as a raw JSON.parse
|
|
513
|
-
// SyntaxError (or another unexpected throwable) that escapes the choice error
|
|
514
|
-
// taxonomy, gets masked as internal_error 500, and is treated as retryable by
|
|
515
|
-
// the hub -> reservation restart loop (2026-07-22 catchtable RCA, candidate A).
|
|
516
|
-
// Convert any non-branded throwable into a branded invalid_payload so it maps
|
|
517
|
-
// to a clean, non-retryable 400. Branded ProviderChoiceTokenError and genuine
|
|
518
|
-
// ProviderError (e.g. Redis-unavailable / state-unavailable) pass through so
|
|
519
|
-
// their category/retryable semantics are preserved.
|
|
520
|
-
let record;
|
|
521
|
-
try {
|
|
522
|
-
record = await namespace.get(optionsStateKey(options.handle.state_id));
|
|
523
|
-
}
|
|
524
|
-
catch (error) {
|
|
525
|
-
if (error instanceof ProviderChoiceTokenError || isProviderError(error)) {
|
|
526
|
-
throw error;
|
|
527
|
-
}
|
|
528
|
-
throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token state payload could not be decoded.");
|
|
529
|
-
}
|
|
530
|
-
if (!record) {
|
|
531
|
-
throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token state payload is missing.");
|
|
532
|
-
}
|
|
533
|
-
const serializedPayload = serializeChoicePayload(record.value);
|
|
534
|
-
assertPayloadDigestMatches({
|
|
535
|
-
actual: digestChoicePayload(serializedPayload),
|
|
536
|
-
expected: options.handle.payload_digest,
|
|
537
|
-
});
|
|
538
|
-
return record.value;
|
|
539
|
-
}
|
|
540
518
|
function generateChoiceWordSequence(wordCount) {
|
|
541
519
|
return Array.from({ length: wordCount }, () => choiceWordAt(randomInt(CHOICE_WORDLIST_SIZE))).join("-");
|
|
542
520
|
}
|
|
@@ -156,7 +156,7 @@ async function solveInPage(page, challengeKind, pageUrl, allowedHosts, successCo
|
|
|
156
156
|
},
|
|
157
157
|
],
|
|
158
158
|
}, async () => {
|
|
159
|
-
const userAgent = await raceWithAbort(() => page.
|
|
159
|
+
const userAgent = await raceWithAbort(() => page.userAgent(), signal);
|
|
160
160
|
await raceWithAbort(() => page.goto(pageUrl), signal);
|
|
161
161
|
while (true) {
|
|
162
162
|
const cookies = await raceWithAbort(() => page.cookies(), signal);
|
|
@@ -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 === "aws_waf"
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
+
}
|
|
246
251
|
}
|
|
247
252
|
if (challenge.kind === "recaptcha_v3" && challenge.minScore === undefined) {
|
|
248
|
-
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "
|
|
253
|
+
throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "missing_challenge_input", {
|
|
254
|
+
missingFields: ["minScore"],
|
|
249
255
|
phase: "create_task",
|
|
250
256
|
});
|
|
251
257
|
}
|
|
@@ -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;
|
|
@@ -39,14 +39,23 @@ export function resolverVendorSupports(vendor, kind) {
|
|
|
39
39
|
export class ResolverVendorUnavailableError extends Error {
|
|
40
40
|
vendor;
|
|
41
41
|
reason;
|
|
42
|
+
missingFields;
|
|
42
43
|
upstreamHost;
|
|
43
44
|
phase;
|
|
44
45
|
round;
|
|
45
46
|
constructor(vendor, reason, options = {}) {
|
|
46
|
-
|
|
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}`);
|
|
47
53
|
this.vendor = vendor;
|
|
48
54
|
this.reason = reason;
|
|
49
55
|
this.name = "ResolverVendorUnavailableError";
|
|
56
|
+
if (missingFields !== undefined && missingFields.length > 0) {
|
|
57
|
+
this.missingFields = Object.freeze([...missingFields]);
|
|
58
|
+
}
|
|
50
59
|
if (options.cause !== undefined) {
|
|
51
60
|
this.cause = options.cause;
|
|
52
61
|
}
|
package/dist/runtime/resolver.js
CHANGED
|
@@ -176,10 +176,17 @@ function throwUnsupportedKind(kind) {
|
|
|
176
176
|
});
|
|
177
177
|
}
|
|
178
178
|
function throwExhausted(attempts) {
|
|
179
|
-
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(", ");
|
|
180
185
|
throw new ProviderError(`Resolver vendor chain exhausted: ${summary}`, {
|
|
181
186
|
code: "RESOLVER_CHAIN_EXHAUSTED",
|
|
182
|
-
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.",
|
|
183
190
|
details: attempts,
|
|
184
191
|
});
|
|
185
192
|
}
|
|
@@ -283,6 +290,7 @@ function unavailableAttempt(error) {
|
|
|
283
290
|
return {
|
|
284
291
|
vendor: error.vendor,
|
|
285
292
|
reason: error.reason,
|
|
293
|
+
...(error.missingFields ? { missingFields: [...error.missingFields] } : {}),
|
|
286
294
|
...(cause ? { cause } : {}),
|
|
287
295
|
...(upstreamHost ? { upstreamHost } : {}),
|
|
288
296
|
...(phase ? { phase } : {}),
|
|
@@ -293,6 +301,7 @@ function unavailableSpanAttributes(error) {
|
|
|
293
301
|
const attempt = unavailableAttempt(error);
|
|
294
302
|
return {
|
|
295
303
|
unavailability_reason: error.reason,
|
|
304
|
+
missing_fields: error.missingFields,
|
|
296
305
|
cause_name: attempt.cause?.name,
|
|
297
306
|
cause_message: attempt.cause?.message,
|
|
298
307
|
upstream_host: attempt.upstreamHost,
|
|
@@ -611,6 +620,8 @@ function createResolverChainClient(options) {
|
|
|
611
620
|
signal.throwIfAborted();
|
|
612
621
|
if (!(error instanceof ResolverVendorUnavailableError))
|
|
613
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.
|
|
614
625
|
attempts.push(unavailableAttempt(error));
|
|
615
626
|
}
|
|
616
627
|
}
|
|
@@ -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;
|
|
@@ -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")
|
|
@@ -497,7 +504,14 @@ function createFlowContextStore(allowedKeys, initialContext = {}) {
|
|
|
497
504
|
},
|
|
498
505
|
};
|
|
499
506
|
}
|
|
500
|
-
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) {
|
|
501
515
|
const baseUrl = getProviderBaseUrl(provider);
|
|
502
516
|
const stealthBaseUrl = getProviderStealthBaseUrl(provider);
|
|
503
517
|
const stealthProfile = getProviderStealthProfile(provider);
|
|
@@ -506,11 +520,7 @@ function createAuthFlowContext(provider, request, options, signal) {
|
|
|
506
520
|
const flowContextStore = createFlowContextStore(provider.context?.keys ?? Object.keys(contextData), contextData);
|
|
507
521
|
const proxyClientOptions = {
|
|
508
522
|
upstream: { proxy: provider.proxy },
|
|
509
|
-
affinityKey: request
|
|
510
|
-
request.externalRef ??
|
|
511
|
-
request.tenantId ??
|
|
512
|
-
request.providerId ??
|
|
513
|
-
provider.id,
|
|
523
|
+
affinityKey: resolveAuthFlowProxyAffinityKey(provider, request),
|
|
514
524
|
};
|
|
515
525
|
const resolverIdentityScope = resolveProviderResolverIdentityScope(provider, proxyClientOptions.affinityKey, request.requestId);
|
|
516
526
|
const stealthClientOptions = {
|
|
@@ -531,7 +541,7 @@ function createAuthFlowContext(provider, request, options, signal) {
|
|
|
531
541
|
return {
|
|
532
542
|
context: {
|
|
533
543
|
flowId: request.flowId,
|
|
534
|
-
connectionId: request
|
|
544
|
+
connectionId: resolveOperationConnectionId(request),
|
|
535
545
|
externalRef: request.externalRef,
|
|
536
546
|
tenantId: request.tenantId ?? "",
|
|
537
547
|
providerId: request.providerId ?? provider.id,
|
|
@@ -539,6 +549,7 @@ function createAuthFlowContext(provider, request, options, signal) {
|
|
|
539
549
|
...proxyClientOptions,
|
|
540
550
|
...(signal ? { signal } : {}),
|
|
541
551
|
}),
|
|
552
|
+
state: state.forConnection(resolveOperationConnectionId(request)),
|
|
542
553
|
stealth: stealthBaseUrl
|
|
543
554
|
? capabilityModules.stealth
|
|
544
555
|
? stealthProfile
|
|
@@ -1359,7 +1370,7 @@ function responseWithProviderTelemetry(response, proxyTelemetry) {
|
|
|
1359
1370
|
statusText: response.statusText,
|
|
1360
1371
|
});
|
|
1361
1372
|
}
|
|
1362
|
-
async function handleAuthFlow(provider, request, route, options, signal) {
|
|
1373
|
+
async function handleAuthFlow(provider, request, route, options, state, signal) {
|
|
1363
1374
|
const flow = provider.auth?.flow;
|
|
1364
1375
|
if (!flow) {
|
|
1365
1376
|
throw new ProviderError("Auth flow is not configured", {
|
|
@@ -1371,7 +1382,7 @@ async function handleAuthFlow(provider, request, route, options, signal) {
|
|
|
1371
1382
|
// any flow code runs instead of at whatever point the ceremony first reads
|
|
1372
1383
|
// the env. `abort` stays exempt: a user must always be able to cancel a
|
|
1373
1384
|
// stranded flow even when provisioning is broken.
|
|
1374
|
-
const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
|
|
1385
|
+
const { context, getPatch } = createAuthFlowContext(provider, request, options, state, signal);
|
|
1375
1386
|
try {
|
|
1376
1387
|
if (route !== "abort") {
|
|
1377
1388
|
assertRequiredSecretsPresent(provider, context.env);
|
|
@@ -1745,7 +1756,7 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1745
1756
|
.json()
|
|
1746
1757
|
.catch(() => undefined);
|
|
1747
1758
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1748
|
-
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);
|
|
1749
1760
|
logProviderSuccess(logger, provider, "auth", "start", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
1750
1761
|
return response instanceof Response ? response : c.json(response);
|
|
1751
1762
|
}
|
|
@@ -1765,7 +1776,7 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1765
1776
|
.json()
|
|
1766
1777
|
.catch(() => undefined);
|
|
1767
1778
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1768
|
-
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);
|
|
1769
1780
|
logProviderSuccess(logger, provider, "auth", "continue", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
1770
1781
|
return response instanceof Response ? response : c.json(response);
|
|
1771
1782
|
}
|
|
@@ -1785,7 +1796,7 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1785
1796
|
.json()
|
|
1786
1797
|
.catch(() => undefined);
|
|
1787
1798
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1788
|
-
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);
|
|
1789
1800
|
logProviderSuccess(logger, provider, "auth", "poll", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
1790
1801
|
return response instanceof Response ? response : c.json(response);
|
|
1791
1802
|
}
|
|
@@ -1805,7 +1816,7 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1805
1816
|
.json()
|
|
1806
1817
|
.catch(() => undefined);
|
|
1807
1818
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1808
|
-
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);
|
|
1809
1820
|
logProviderSuccess(logger, provider, "auth", "refresh", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
1810
1821
|
return response instanceof Response ? response : c.json(response);
|
|
1811
1822
|
}
|
|
@@ -1825,7 +1836,7 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
|
|
|
1825
1836
|
.json()
|
|
1826
1837
|
.catch(() => undefined);
|
|
1827
1838
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1828
|
-
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);
|
|
1829
1840
|
logProviderSuccess(logger, provider, "auth", "disconnect", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
1830
1841
|
return response instanceof Response ? response : c.json(response);
|
|
1831
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
|
@@ -1487,6 +1487,8 @@ export type BrowserResourcePolicy = {
|
|
|
1487
1487
|
};
|
|
1488
1488
|
export interface BrowserPage extends BrowserFrame {
|
|
1489
1489
|
close(): Promise<void>;
|
|
1490
|
+
/** Returns the user agent used by this page's browser context. */
|
|
1491
|
+
userAgent(): Promise<string>;
|
|
1490
1492
|
/**
|
|
1491
1493
|
* Reads the browser context's cookie jar, including httpOnly cookies.
|
|
1492
1494
|
* Cookie expiry values are Unix seconds and are absent for session cookies.
|
|
@@ -1579,7 +1581,7 @@ export type ProviderChoiceExplicitParseResult = {
|
|
|
1579
1581
|
readonly payload: Record<string, unknown>;
|
|
1580
1582
|
/** Stable, opaque key for provider-owned idempotency records. */
|
|
1581
1583
|
readonly replayKey: string;
|
|
1582
|
-
/** Atomically claims a word token.
|
|
1584
|
+
/** Atomically claims a word token. Inline tokens report unsupported. */
|
|
1583
1585
|
consume(): Promise<ProviderChoiceConsumeResult>;
|
|
1584
1586
|
} | {
|
|
1585
1587
|
readonly status: "consumed";
|
|
@@ -1626,7 +1628,7 @@ export interface ProviderChoiceParseOptions {
|
|
|
1626
1628
|
futureToleranceMs?: number;
|
|
1627
1629
|
bind?: ProviderChoiceBindingOptions;
|
|
1628
1630
|
storage?: ProviderChoiceStorageOptions;
|
|
1629
|
-
/** Defaults to never,
|
|
1631
|
+
/** Defaults to never, preserving reusable choice-token parse semantics. */
|
|
1630
1632
|
consume?: ProviderChoiceConsumeMode;
|
|
1631
1633
|
}
|
|
1632
1634
|
export interface ProviderChoiceContext {
|
|
@@ -1747,6 +1749,18 @@ export interface FlowContext {
|
|
|
1747
1749
|
tenantId: string;
|
|
1748
1750
|
providerId: string;
|
|
1749
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;
|
|
1750
1764
|
/** Present when the selected runtime supplies native network capabilities. */
|
|
1751
1765
|
readonly native?: NativeProviderContext;
|
|
1752
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.36",
|
|
3
3
|
"name": "@apifuse/provider-sdk",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
@@ -106,10 +106,14 @@
|
|
|
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",
|
|
114
|
+
"api:update": "bun run build && bun scripts/api-reports.ts update",
|
|
115
|
+
"api:check": "bun run build && bun scripts/api-reports.ts check",
|
|
116
|
+
"changeset:check": "bun scripts/check-changeset.ts",
|
|
113
117
|
"pack:check": "bun run build && bun bin/apifuse-pack-check.ts",
|
|
114
118
|
"pack:smoke": "bun run build && bun bin/apifuse-pack-smoke.ts",
|
|
115
119
|
"pack:types": "bun run build && bun bin/apifuse-pack-types.ts",
|
|
@@ -120,6 +124,8 @@
|
|
|
120
124
|
"devDependencies": {
|
|
121
125
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
122
126
|
"@biomejs/biome": "^2.5.0",
|
|
127
|
+
"@changesets/cli": "^3.0.1",
|
|
128
|
+
"@microsoft/api-extractor": "^7.58.13",
|
|
123
129
|
"@types/bun": "latest",
|
|
124
130
|
"@types/node": "^25.9.3",
|
|
125
131
|
"ajv": "^8.17",
|