@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.
@@ -117,10 +117,9 @@ export function createProviderChoiceContext(options) {
117
117
  consumeMode,
118
118
  });
119
119
  }
120
- // Legacy encrypted-envelope compatibility fallback. Removal is gated on
121
- // the last legacy mint plus the maximum issued TTL; see ADR 0006.
122
- // A structurally valid word token returns above, so lookup, expiry,
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
- const payload = isServerChoiceHandlePayload(envelope.payload)
170
- ? parseLegacyServerStoredChoice({
171
- handle: envelope.payload,
172
- storage: parseOptions.storage,
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) => createLegacyExplicitParseResult({
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 createLegacyExplicitParseResult(options) {
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.evaluate("navigator.userAgent"), signal);
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
- (!challenge.siteKey?.trim() ||
240
- !challenge.captchaScript?.trim() ||
241
- !challenge.context?.trim() ||
242
- !challenge.iv?.trim())) {
243
- throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
244
- phase: "create_task",
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, "transport_failure", {
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
- super(`Resolver vendor ${vendor} is unavailable: ${reason}`);
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
  }
@@ -176,10 +176,17 @@ function throwUnsupportedKind(kind) {
176
176
  });
177
177
  }
178
178
  function throwExhausted(attempts) {
179
- const summary = attempts.map(({ vendor, reason }) => `${vendor}: ${reason}`).join(", ");
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: "Configure another supporting resolver vendor or restore an unavailable vendor.",
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
- return request.connection?.id ?? request.connectionId;
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 createAuthFlowContext(provider, request, options, signal) {
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.connectionId ??
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.connectionId,
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
  }
@@ -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. Legacy managed tokens report unsupported. */
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, matching legacy managed-token parse semantics. */
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.33",
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",