@apifuse/provider-sdk 2.2.0-beta.32 → 2.2.0-beta.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/error-resolution.js +0 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/provider.d.ts +1 -1
  6. package/dist/provider.js +1 -1
  7. package/dist/runtime/auth-flow.d.ts +3 -1
  8. package/dist/runtime/auth-flow.js +1 -0
  9. package/dist/runtime/browser.d.ts +1 -0
  10. package/dist/runtime/browser.js +350 -27
  11. package/dist/runtime/choice.d.ts +0 -1
  12. package/dist/runtime/choice.js +10 -126
  13. package/dist/runtime/resolver-vendors/browser.d.ts +2 -0
  14. package/dist/runtime/resolver-vendors/browser.js +68 -16
  15. package/dist/runtime/resolver-vendors/capsolver.d.ts +1 -3
  16. package/dist/runtime/resolver-vendors/capsolver.js +148 -24
  17. package/dist/runtime/resolver-vendors/twocaptcha.js +57 -18
  18. package/dist/runtime/resolver-vendors/types.d.ts +5 -2
  19. package/dist/runtime/resolver-vendors/types.js +16 -4
  20. package/dist/runtime/resolver.d.ts +1 -1
  21. package/dist/runtime/resolver.js +24 -5
  22. package/dist/server/serve-implementation.d.ts +2 -1
  23. package/dist/server/serve-implementation.js +27 -17
  24. package/dist/testing/run.js +1 -0
  25. package/dist/types.d.ts +25 -3
  26. package/package.json +3 -2
  27. package/src/error-resolution.ts +0 -1
  28. package/src/index.ts +0 -1
  29. package/src/provider.ts +0 -1
  30. package/src/runtime/auth-flow.ts +4 -0
  31. package/src/runtime/browser.ts +438 -31
  32. package/src/runtime/choice.ts +10 -151
  33. package/src/runtime/resolver-vendors/browser.ts +83 -16
  34. package/src/runtime/resolver-vendors/capsolver.ts +170 -33
  35. package/src/runtime/resolver-vendors/twocaptcha.ts +54 -15
  36. package/src/runtime/resolver-vendors/types.ts +22 -4
  37. package/src/runtime/resolver.ts +31 -7
  38. package/src/server/serve-implementation.ts +74 -17
  39. package/src/testing/run.ts +1 -0
  40. package/src/types.ts +26 -3
@@ -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 !== "recaptcha_v2" && challenge.kind !== "aws_waf") {
239
- throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
240
- phase: "create_task",
241
- });
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
+ }
242
251
  }
243
- if (challenge.kind === "aws_waf" &&
244
- (!challenge.siteKey?.trim() ||
245
- !challenge.captchaScript?.trim() ||
246
- !challenge.context?.trim() ||
247
- !challenge.iv?.trim())) {
248
- throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
252
+ if (challenge.kind === "recaptcha_v3" && challenge.minScore === undefined) {
253
+ throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "missing_challenge_input", {
254
+ missingFields: ["minScore"],
249
255
  phase: "create_task",
250
256
  });
251
257
  }
@@ -278,14 +284,47 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
278
284
  ...(identity ? { userAgent: identity.userAgent } : {}),
279
285
  ...(proxy ?? {}),
280
286
  }
281
- : {
282
- type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
283
- websiteURL: challenge.pageUrl,
284
- websiteKey: challenge.siteKey,
285
- isInvisible: false,
286
- ...(identity ? { userAgent: identity.userAgent } : {}),
287
- ...(proxy ?? {}),
288
- };
287
+ : challenge.kind === "recaptcha_v2"
288
+ ? {
289
+ type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
290
+ websiteURL: challenge.pageUrl,
291
+ websiteKey: challenge.siteKey,
292
+ isInvisible: false,
293
+ ...(identity ? { userAgent: identity.userAgent } : {}),
294
+ ...(proxy ?? {}),
295
+ }
296
+ : challenge.kind === "recaptcha_v3"
297
+ ? {
298
+ type: "RecaptchaV3TaskProxyless",
299
+ websiteURL: challenge.pageUrl,
300
+ websiteKey: challenge.siteKey,
301
+ minScore: challenge.minScore,
302
+ pageAction: challenge.action,
303
+ ...(identity ? { userAgent: identity.userAgent } : {}),
304
+ }
305
+ : challenge.kind === "hcaptcha"
306
+ ? {
307
+ type: proxy ? "HCaptchaTask" : "HCaptchaTaskProxyless",
308
+ websiteURL: challenge.pageUrl,
309
+ websiteKey: challenge.siteKey,
310
+ ...(identity ? { userAgent: identity.userAgent } : {}),
311
+ ...(proxy ?? {}),
312
+ }
313
+ : challenge.kind === "turnstile"
314
+ ? {
315
+ type: proxy ? "TurnstileTask" : "TurnstileTaskProxyless",
316
+ websiteURL: challenge.pageUrl,
317
+ websiteKey: challenge.siteKey,
318
+ ...(challenge.action !== undefined ? { action: challenge.action } : {}),
319
+ ...(challenge.cdata !== undefined ? { data: challenge.cdata } : {}),
320
+ ...(identity ? { userAgent: identity.userAgent } : {}),
321
+ ...(proxy ?? {}),
322
+ }
323
+ : // `resolverVendorSupports` above already rejected every kind this
324
+ // adapter does not build a task for, so this branch is unreachable.
325
+ (() => {
326
+ throw new TypeError(`2captcha resolver does not support ${challenge.kind}`);
327
+ })();
289
328
  const createResult = await postJson(fetchImpl, endpoint(baseUrl, "createTask"), { clientKey: apiKey, task }, solveController.signal, phase, [apiKey]);
290
329
  const taskId = taskIdFrom(createResult.payload);
291
330
  if (!createResult.ok || createResult.payload.errorId !== 0 || taskId === undefined) {
@@ -2,7 +2,7 @@ import type { ChallengeSolution, ProviderChallenge, ProviderChallengeKind, Provi
2
2
  import type { TraceRecorder } from "../trace.js";
3
3
  export declare const RESOLVER_VENDOR_CAPABILITIES: {
4
4
  readonly browser: readonly ["aws_waf", "cloudflare_interstitial"];
5
- readonly "2captcha": readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf", "akamai_sec_cpt", "akamai_sensor"];
5
+ readonly "2captcha": readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "aws_waf"];
6
6
  readonly capsolver: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf"];
7
7
  readonly capmonster: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha"];
8
8
  readonly custom: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf", "akamai_sec_cpt", "akamai_sensor"];
@@ -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;
@@ -1,14 +1,17 @@
1
1
  export const RESOLVER_VENDOR_CAPABILITIES = {
2
2
  browser: ["aws_waf", "cloudflare_interstitial"],
3
+ // Every kind listed per vendor is implemented by that vendor's adapter; the
4
+ // per-adapter "agrees with every declared capability" tests iterate this
5
+ // table, so adding a kind here without an implementation fails the suite.
6
+ // 2captcha omits `cloudflare_interstitial`, `akamai_sec_cpt`, and
7
+ // `akamai_sensor`: their API offers no task type for them, so declaring them
8
+ // would route challenges to a vendor that can only refuse.
3
9
  "2captcha": [
4
10
  "turnstile",
5
11
  "recaptcha_v2",
6
12
  "recaptcha_v3",
7
13
  "hcaptcha",
8
- "cloudflare_interstitial",
9
14
  "aws_waf",
10
- "akamai_sec_cpt",
11
- "akamai_sensor",
12
15
  ],
13
16
  capsolver: [
14
17
  "turnstile",
@@ -36,14 +39,23 @@ export function resolverVendorSupports(vendor, kind) {
36
39
  export class ResolverVendorUnavailableError extends Error {
37
40
  vendor;
38
41
  reason;
42
+ missingFields;
39
43
  upstreamHost;
40
44
  phase;
41
45
  round;
42
46
  constructor(vendor, reason, options = {}) {
43
- 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}`);
44
53
  this.vendor = vendor;
45
54
  this.reason = reason;
46
55
  this.name = "ResolverVendorUnavailableError";
56
+ if (missingFields !== undefined && missingFields.length > 0) {
57
+ this.missingFields = Object.freeze([...missingFields]);
58
+ }
47
59
  if (options.cause !== undefined) {
48
60
  this.cause = options.cause;
49
61
  }
@@ -34,7 +34,7 @@ export type ResolverInstrumentationMetadata = {
34
34
  readonly target: ResolverContext;
35
35
  readonly traceRecorder: TraceRecorder;
36
36
  };
37
- export type ResolverAdapterFactory = (configuration: string, timeoutMs: number, allowedHosts: readonly string[]) => ResolverVendorAdapter;
37
+ export type ResolverAdapterFactory = (configuration: string | undefined, timeoutMs: number, allowedHosts: readonly string[]) => ResolverVendorAdapter;
38
38
  export declare const RESOLVER_ADAPTER_REGISTRY: Partial<Readonly<Record<ProviderResolverVendor, ResolverAdapterFactory>>>;
39
39
  export declare function swapResolverAdapterFactoryForTests(vendor: ProviderResolverVendor, factory: ResolverAdapterFactory | undefined): () => void;
40
40
  /** Internal test seam; deliberately not re-exported from the package root. */
@@ -56,6 +56,9 @@ const SAFE_CAUSE_MESSAGE_WORDS = new Set([
56
56
  ]);
57
57
  const resolverAdapterRegistry = {
58
58
  "2captcha"(configuration, timeoutMs, allowedHosts) {
59
+ if (configuration === undefined) {
60
+ throw new Error("2captcha resolver adapter factory requires an API key");
61
+ }
59
62
  return createTwoCaptchaResolverVendorAdapter({
60
63
  allowedHosts,
61
64
  apiKey: configuration,
@@ -173,10 +176,17 @@ function throwUnsupportedKind(kind) {
173
176
  });
174
177
  }
175
178
  function throwExhausted(attempts) {
176
- 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(", ");
177
185
  throw new ProviderError(`Resolver vendor chain exhausted: ${summary}`, {
178
186
  code: "RESOLVER_CHAIN_EXHAUSTED",
179
- 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.",
180
190
  details: attempts,
181
191
  });
182
192
  }
@@ -280,6 +290,7 @@ function unavailableAttempt(error) {
280
290
  return {
281
291
  vendor: error.vendor,
282
292
  reason: error.reason,
293
+ ...(error.missingFields ? { missingFields: [...error.missingFields] } : {}),
283
294
  ...(cause ? { cause } : {}),
284
295
  ...(upstreamHost ? { upstreamHost } : {}),
285
296
  ...(phase ? { phase } : {}),
@@ -290,6 +301,7 @@ function unavailableSpanAttributes(error) {
290
301
  const attempt = unavailableAttempt(error);
291
302
  return {
292
303
  unavailability_reason: error.reason,
304
+ missing_fields: error.missingFields,
293
305
  cause_name: attempt.cause?.name,
294
306
  cause_message: attempt.cause?.message,
295
307
  upstream_host: attempt.upstreamHost,
@@ -608,6 +620,8 @@ function createResolverChainClient(options) {
608
620
  signal.throwIfAborted();
609
621
  if (!(error instanceof ResolverVendorUnavailableError))
610
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.
611
625
  attempts.push(unavailableAttempt(error));
612
626
  }
613
627
  }
@@ -643,13 +657,18 @@ function resolveVendorAvailability(vendor, env) {
643
657
  reason: "missing_transport",
644
658
  };
645
659
  }
660
+ if (vendor === "browser") {
661
+ return {
662
+ vendor,
663
+ available: true,
664
+ configuration: normalizedEnvValue(env, APIFUSE__CDP_POOL__URL),
665
+ };
666
+ }
646
667
  const envKey = vendor === "2captcha"
647
668
  ? APIFUSE__RESOLVER__2CAPTCHA__API_KEY
648
669
  : vendor === "capsolver"
649
670
  ? APIFUSE__RESOLVER__CAPSOLVER__API_KEY
650
- : vendor === "capmonster"
651
- ? APIFUSE__RESOLVER__CAPMONSTER__API_KEY
652
- : APIFUSE__CDP_POOL__URL;
671
+ : APIFUSE__RESOLVER__CAPMONSTER__API_KEY;
653
672
  const configuration = normalizedEnvValue(env, envKey);
654
673
  return configuration
655
674
  ? { vendor, available: true, configuration }
@@ -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;
@@ -11,7 +11,7 @@ import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.j
11
11
  import { categoryForStatus, sourceForCategory, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
12
12
  import { createScratchpad } from "../runtime/auth-flow.js";
13
13
  import { createProviderCache } from "../runtime/cache.js";
14
- import { createProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV, } from "../runtime/choice.js";
14
+ import { createProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "../runtime/choice.js";
15
15
  import { createCredentialContext } from "../runtime/credential.js";
16
16
  import { createEnvContext } from "../runtime/env.js";
17
17
  import { executeOperation } from "../runtime/executor.js";
@@ -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")
@@ -376,7 +383,6 @@ function createProviderContext(provider, request, operationId, options, state =
376
383
  const env = createEnvContext([
377
384
  ...(provider.secrets?.map((secret) => secret.name) ?? []),
378
385
  PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
379
- PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV,
380
386
  ]);
381
387
  const credential = createCredentialContext({
382
388
  allowedKeys: provider.credential?.keys,
@@ -498,7 +504,14 @@ function createFlowContextStore(allowedKeys, initialContext = {}) {
498
504
  },
499
505
  };
500
506
  }
501
- 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) {
502
515
  const baseUrl = getProviderBaseUrl(provider);
503
516
  const stealthBaseUrl = getProviderStealthBaseUrl(provider);
504
517
  const stealthProfile = getProviderStealthProfile(provider);
@@ -507,11 +520,7 @@ function createAuthFlowContext(provider, request, options, signal) {
507
520
  const flowContextStore = createFlowContextStore(provider.context?.keys ?? Object.keys(contextData), contextData);
508
521
  const proxyClientOptions = {
509
522
  upstream: { proxy: provider.proxy },
510
- affinityKey: request.connectionId ??
511
- request.externalRef ??
512
- request.tenantId ??
513
- request.providerId ??
514
- provider.id,
523
+ affinityKey: resolveAuthFlowProxyAffinityKey(provider, request),
515
524
  };
516
525
  const resolverIdentityScope = resolveProviderResolverIdentityScope(provider, proxyClientOptions.affinityKey, request.requestId);
517
526
  const stealthClientOptions = {
@@ -532,7 +541,7 @@ function createAuthFlowContext(provider, request, options, signal) {
532
541
  return {
533
542
  context: {
534
543
  flowId: request.flowId,
535
- connectionId: request.connectionId,
544
+ connectionId: resolveOperationConnectionId(request),
536
545
  externalRef: request.externalRef,
537
546
  tenantId: request.tenantId ?? "",
538
547
  providerId: request.providerId ?? provider.id,
@@ -540,6 +549,7 @@ function createAuthFlowContext(provider, request, options, signal) {
540
549
  ...proxyClientOptions,
541
550
  ...(signal ? { signal } : {}),
542
551
  }),
552
+ state: state.forConnection(resolveOperationConnectionId(request)),
543
553
  stealth: stealthBaseUrl
544
554
  ? capabilityModules.stealth
545
555
  ? stealthProfile
@@ -1360,7 +1370,7 @@ function responseWithProviderTelemetry(response, proxyTelemetry) {
1360
1370
  statusText: response.statusText,
1361
1371
  });
1362
1372
  }
1363
- async function handleAuthFlow(provider, request, route, options, signal) {
1373
+ async function handleAuthFlow(provider, request, route, options, state, signal) {
1364
1374
  const flow = provider.auth?.flow;
1365
1375
  if (!flow) {
1366
1376
  throw new ProviderError("Auth flow is not configured", {
@@ -1372,7 +1382,7 @@ async function handleAuthFlow(provider, request, route, options, signal) {
1372
1382
  // any flow code runs instead of at whatever point the ceremony first reads
1373
1383
  // the env. `abort` stays exempt: a user must always be able to cancel a
1374
1384
  // stranded flow even when provisioning is broken.
1375
- const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
1385
+ const { context, getPatch } = createAuthFlowContext(provider, request, options, state, signal);
1376
1386
  try {
1377
1387
  if (route !== "abort") {
1378
1388
  assertRequiredSecretsPresent(provider, context.env);
@@ -1746,7 +1756,7 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
1746
1756
  .json()
1747
1757
  .catch(() => undefined);
1748
1758
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1749
- 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);
1750
1760
  logProviderSuccess(logger, provider, "auth", "start", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
1751
1761
  return response instanceof Response ? response : c.json(response);
1752
1762
  }
@@ -1766,7 +1776,7 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
1766
1776
  .json()
1767
1777
  .catch(() => undefined);
1768
1778
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1769
- 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);
1770
1780
  logProviderSuccess(logger, provider, "auth", "continue", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
1771
1781
  return response instanceof Response ? response : c.json(response);
1772
1782
  }
@@ -1786,7 +1796,7 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
1786
1796
  .json()
1787
1797
  .catch(() => undefined);
1788
1798
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1789
- 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);
1790
1800
  logProviderSuccess(logger, provider, "auth", "poll", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
1791
1801
  return response instanceof Response ? response : c.json(response);
1792
1802
  }
@@ -1806,7 +1816,7 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
1806
1816
  .json()
1807
1817
  .catch(() => undefined);
1808
1818
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1809
- 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);
1810
1820
  logProviderSuccess(logger, provider, "auth", "refresh", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
1811
1821
  return response instanceof Response ? response : c.json(response);
1812
1822
  }
@@ -1826,7 +1836,7 @@ function createServerAppWithCapabilityModules(provider, serverOptions, capabilit
1826
1836
  .json()
1827
1837
  .catch(() => undefined);
1828
1838
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1829
- 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);
1830
1840
  logProviderSuccess(logger, provider, "auth", "disconnect", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
1831
1841
  return response instanceof Response ? response : c.json(response);
1832
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
@@ -1451,7 +1451,7 @@ export interface BrowserCookie {
1451
1451
  readonly secure: boolean;
1452
1452
  readonly sameSite?: "Strict" | "Lax" | "None";
1453
1453
  }
1454
- export type BrowserResourceMethod = "GET" | "HEAD";
1454
+ export type BrowserResourceMethod = "GET" | "HEAD" | "POST";
1455
1455
  export type BrowserResourceRequest = {
1456
1456
  readonly url: string;
1457
1457
  readonly method: BrowserResourceMethod;
@@ -1460,6 +1460,8 @@ export type BrowserResourceRequest = {
1460
1460
  };
1461
1461
  export type BrowserResourceBody = Buffer | Uint8Array | ArrayBuffer | string;
1462
1462
  export type BrowserResourceDecision = {
1463
+ readonly action: "continue";
1464
+ } | {
1463
1465
  readonly action: "fulfill";
1464
1466
  readonly status?: number;
1465
1467
  readonly headers?: Readonly<Record<string, string>>;
@@ -1475,10 +1477,18 @@ export type BrowserResourceRoute = {
1475
1477
  export type BrowserResourcePolicy = {
1476
1478
  readonly defaultAction?: "block";
1477
1479
  readonly allowedMethods?: readonly BrowserResourceMethod[];
1480
+ /**
1481
+ * Appends an enforcing CSP header to every renderable document response
1482
+ * while the policy is active. Existing CSP headers are retained, so this
1483
+ * can only further restrict the document.
1484
+ */
1485
+ readonly documentContentSecurityPolicy?: string;
1478
1486
  readonly routes: readonly BrowserResourceRoute[];
1479
1487
  };
1480
1488
  export interface BrowserPage extends BrowserFrame {
1481
1489
  close(): Promise<void>;
1490
+ /** Returns the user agent used by this page's browser context. */
1491
+ userAgent(): Promise<string>;
1482
1492
  /**
1483
1493
  * Reads the browser context's cookie jar, including httpOnly cookies.
1484
1494
  * Cookie expiry values are Unix seconds and are absent for session cookies.
@@ -1571,7 +1581,7 @@ export type ProviderChoiceExplicitParseResult = {
1571
1581
  readonly payload: Record<string, unknown>;
1572
1582
  /** Stable, opaque key for provider-owned idempotency records. */
1573
1583
  readonly replayKey: string;
1574
- /** Atomically claims a word token. Legacy managed tokens report unsupported. */
1584
+ /** Atomically claims a word token. Inline tokens report unsupported. */
1575
1585
  consume(): Promise<ProviderChoiceConsumeResult>;
1576
1586
  } | {
1577
1587
  readonly status: "consumed";
@@ -1618,7 +1628,7 @@ export interface ProviderChoiceParseOptions {
1618
1628
  futureToleranceMs?: number;
1619
1629
  bind?: ProviderChoiceBindingOptions;
1620
1630
  storage?: ProviderChoiceStorageOptions;
1621
- /** Defaults to never, matching legacy managed-token parse semantics. */
1631
+ /** Defaults to never, preserving reusable choice-token parse semantics. */
1622
1632
  consume?: ProviderChoiceConsumeMode;
1623
1633
  }
1624
1634
  export interface ProviderChoiceContext {
@@ -1739,6 +1749,18 @@ export interface FlowContext {
1739
1749
  tenantId: string;
1740
1750
  providerId: string;
1741
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;
1742
1764
  /** Present when the selected runtime supplies native network capabilities. */
1743
1765
  readonly native?: NativeProviderContext;
1744
1766
  stealth: StealthClient;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0-beta.32",
2
+ "version": "2.2.0-beta.35",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
@@ -106,10 +106,11 @@
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",
113
114
  "pack:check": "bun run build && bun bin/apifuse-pack-check.ts",
114
115
  "pack:smoke": "bun run build && bun bin/apifuse-pack-smoke.ts",
115
116
  "pack:types": "bun run build && bun bin/apifuse-pack-types.ts",
@@ -20,7 +20,6 @@ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
20
20
  "RUNTIME_UNSUPPORTED",
21
21
  "PROVIDER_STATE_UNSUPPORTED",
22
22
  "CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
23
- "CHOICE_WORD_ISSUANCE_INVALID",
24
23
  "CHOICE_STATE_PAYLOAD_TOO_LARGE",
25
24
  "CHOICE_STATE_UNAVAILABLE",
26
25
  "CHOICE_CONTEXT_REQUIRED",
package/src/index.ts CHANGED
@@ -61,7 +61,6 @@ export {
61
61
  createProviderChoiceContext,
62
62
  createTestProviderChoiceContext,
63
63
  PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
64
- PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV,
65
64
  } from "./runtime/choice.js";
66
65
  export {
67
66
  type CreateCredentialContextOptions,
package/src/provider.ts CHANGED
@@ -56,7 +56,6 @@ export {
56
56
  createProviderChoiceContext,
57
57
  createTestProviderChoiceContext,
58
58
  PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
59
- PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV,
60
59
  } from "./runtime/choice.js";
61
60
  export {
62
61
  APIFUSE_DESCRIPTION_KEY_META_KEY,
@@ -6,6 +6,7 @@ import type {
6
6
  FlowContext,
7
7
  HttpClient,
8
8
  OcrContext,
9
+ ProviderRuntimeState,
9
10
  StealthClient,
10
11
  SttContext,
11
12
  } from "../types.js";
@@ -59,6 +60,8 @@ export function createFlowContext(options: {
59
60
  providerId: string;
60
61
  connectionId?: string;
61
62
  externalRef?: string;
63
+ /** Host-agnostic: callers pass an already-scoped runtime state, which this helper forwards verbatim. */
64
+ state?: ProviderRuntimeState;
62
65
  allowedKeys: string[];
63
66
  initialContext?: Record<string, unknown>;
64
67
  ocr?: OcrContext;
@@ -71,6 +74,7 @@ export function createFlowContext(options: {
71
74
  tenantId: options.tenantId,
72
75
  providerId: options.providerId,
73
76
  http: options.http,
77
+ state: options.state,
74
78
  stealth: options.stealth,
75
79
  env: options.env,
76
80
  context: createScratchpad(options.allowedKeys, options.initialContext),