@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.
@@ -263,10 +263,9 @@ export function createProviderChoiceContext(
263
263
  );
264
264
  }
265
265
 
266
- // Legacy encrypted-envelope compatibility fallback. Removal is gated on
267
- // the last legacy mint plus the maximum issued TTL; see ADR 0006.
268
- // A structurally valid word token returns above, so lookup, expiry,
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
- const payload = isServerChoiceHandlePayload(envelope.payload)
325
- ? parseLegacyServerStoredChoice({
326
- handle: envelope.payload,
327
- storage: parseOptions.storage,
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
- createLegacyExplicitParseResult({
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 createLegacyExplicitParseResult(options: {
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.evaluate<string>("navigator.userAgent"),
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
- challenge.kind === "aws_waf" &&
343
- (!challenge.siteKey?.trim() ||
344
- !challenge.captchaScript?.trim() ||
345
- !challenge.context?.trim() ||
346
- !challenge.iv?.trim())
347
- ) {
348
- throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
349
- phase: "create_task",
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, "transport_failure", {
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
- super(`Resolver vendor ${vendor} is unavailable: ${reason}`);
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
  }
@@ -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 summary = attempts.map(({ vendor, reason }) => `${vendor}: ${reason}`).join(", ");
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: "Configure another supporting resolver vendor or restore an unavailable vendor.",
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(request: OperationRequest): string | undefined {
575
- return request.connection?.id ?? request.connectionId;
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.connectionId,
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(provider, body, "start", options, c.req.raw.signal);
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(provider, body, "continue", options, c.req.raw.signal);
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(provider, body, "poll", options, c.req.raw.signal);
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(provider, body, "refresh", options, c.req.raw.signal);
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(provider, body, "abort", options, c.req.raw.signal);
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,
@@ -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. Legacy managed tokens report unsupported. */
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, matching legacy managed-token parse semantics. */
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;