@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
@@ -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 (challenge.kind !== "recaptcha_v2" && challenge.kind !== "aws_waf") {
342
- throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
343
- phase: "create_task",
344
- });
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
+ }
345
354
  }
346
- if (
347
- challenge.kind === "aws_waf" &&
348
- (!challenge.siteKey?.trim() ||
349
- !challenge.captchaScript?.trim() ||
350
- !challenge.context?.trim() ||
351
- !challenge.iv?.trim())
352
- ) {
353
- throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "not_implemented", {
355
+ if (challenge.kind === "recaptcha_v3" && challenge.minScore === undefined) {
356
+ throw new ResolverVendorUnavailableError(TWOCAPTCHA_VENDOR_ID, "missing_challenge_input", {
357
+ missingFields: ["minScore"],
354
358
  phase: "create_task",
355
359
  });
356
360
  }
@@ -388,14 +392,49 @@ export function createTwoCaptchaResolverVendorAdapter(
388
392
  ...(identity ? { userAgent: identity.userAgent } : {}),
389
393
  ...(proxy ?? {}),
390
394
  }
391
- : {
392
- type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
395
+ : challenge.kind === "recaptcha_v2"
396
+ ? {
397
+ type: proxy ? "RecaptchaV2Task" : "RecaptchaV2TaskProxyless",
393
398
  websiteURL: challenge.pageUrl,
394
399
  websiteKey: challenge.siteKey,
395
400
  isInvisible: false,
396
401
  ...(identity ? { userAgent: identity.userAgent } : {}),
402
+ ...(proxy ?? {}),
403
+ }
404
+ : challenge.kind === "recaptcha_v3"
405
+ ? {
406
+ type: "RecaptchaV3TaskProxyless",
407
+ websiteURL: challenge.pageUrl,
408
+ websiteKey: challenge.siteKey,
409
+ minScore: challenge.minScore,
410
+ pageAction: challenge.action,
411
+ ...(identity ? { userAgent: identity.userAgent } : {}),
412
+ }
413
+ : challenge.kind === "hcaptcha"
414
+ ? {
415
+ type: proxy ? "HCaptchaTask" : "HCaptchaTaskProxyless",
416
+ websiteURL: challenge.pageUrl,
417
+ websiteKey: challenge.siteKey,
418
+ ...(identity ? { userAgent: identity.userAgent } : {}),
419
+ ...(proxy ?? {}),
420
+ }
421
+ : challenge.kind === "turnstile"
422
+ ? {
423
+ type: proxy ? "TurnstileTask" : "TurnstileTaskProxyless",
424
+ websiteURL: challenge.pageUrl,
425
+ websiteKey: challenge.siteKey,
426
+ ...(challenge.action !== undefined ? { action: challenge.action } : {}),
427
+ ...(challenge.cdata !== undefined ? { data: challenge.cdata } : {}),
428
+ ...(identity ? { userAgent: identity.userAgent } : {}),
397
429
  ...(proxy ?? {}),
398
- };
430
+ }
431
+ : // `resolverVendorSupports` above already rejected every kind this
432
+ // adapter does not build a task for, so this branch is unreachable.
433
+ (() => {
434
+ throw new TypeError(
435
+ `2captcha resolver does not support ${challenge.kind}`,
436
+ );
437
+ })();
399
438
  const createResult = await postJson(
400
439
  fetchImpl,
401
440
  endpoint(baseUrl, "createTask"),
@@ -8,15 +8,18 @@ import type { TraceRecorder } from "../trace.js";
8
8
 
9
9
  export const RESOLVER_VENDOR_CAPABILITIES = {
10
10
  browser: ["aws_waf", "cloudflare_interstitial"],
11
+ // Every kind listed per vendor is implemented by that vendor's adapter; the
12
+ // per-adapter "agrees with every declared capability" tests iterate this
13
+ // table, so adding a kind here without an implementation fails the suite.
14
+ // 2captcha omits `cloudflare_interstitial`, `akamai_sec_cpt`, and
15
+ // `akamai_sensor`: their API offers no task type for them, so declaring them
16
+ // would route challenges to a vendor that can only refuse.
11
17
  "2captcha": [
12
18
  "turnstile",
13
19
  "recaptcha_v2",
14
20
  "recaptcha_v3",
15
21
  "hcaptcha",
16
- "cloudflare_interstitial",
17
22
  "aws_waf",
18
- "akamai_sec_cpt",
19
- "akamai_sensor",
20
23
  ],
21
24
  capsolver: [
22
25
  "turnstile",
@@ -114,6 +117,7 @@ export type ResolverVendorUnavailableReason =
114
117
  | "missing_credentials"
115
118
  | "missing_proxy_identity"
116
119
  | "missing_client_profile"
120
+ | "missing_challenge_input"
117
121
  | "missing_transport"
118
122
  | "allocation_exhausted"
119
123
  | "transport_failure"
@@ -125,6 +129,8 @@ export type ResolverChallengeVerdictReason = "human_puzzle" | "solve_failed";
125
129
  type ResolverErrorOptions = {
126
130
  /** Raw cause; adapters must not place bodies, cookies, headers, credentials, or proxy URLs here. */
127
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[];
128
134
  /** Upstream hostname only; never a URL. */
129
135
  readonly upstreamHost?: string;
130
136
  /** Adapter-defined sensor-loop phase, such as fetch_script or post_sensor. */
@@ -134,6 +140,7 @@ type ResolverErrorOptions = {
134
140
  };
135
141
 
136
142
  export class ResolverVendorUnavailableError extends Error {
143
+ readonly missingFields?: readonly string[];
137
144
  readonly upstreamHost?: string;
138
145
  readonly phase?: string;
139
146
  readonly round?: number;
@@ -143,8 +150,19 @@ export class ResolverVendorUnavailableError extends Error {
143
150
  readonly reason: ResolverVendorUnavailableReason,
144
151
  options: ResolverErrorOptions = {},
145
152
  ) {
146
- 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
+ );
147
162
  this.name = "ResolverVendorUnavailableError";
163
+ if (missingFields !== undefined && missingFields.length > 0) {
164
+ this.missingFields = Object.freeze([...missingFields]);
165
+ }
148
166
  if (options.cause !== undefined) {
149
167
  this.cause = options.cause;
150
168
  }
@@ -71,7 +71,7 @@ type ResolvedResolverVendor =
71
71
  | {
72
72
  readonly vendor: Exclude<ProviderResolverVendor, "custom">;
73
73
  readonly available: true;
74
- readonly configuration: string;
74
+ readonly configuration: string | undefined;
75
75
  }
76
76
  | {
77
77
  readonly vendor: ProviderResolverVendor;
@@ -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;
@@ -184,13 +185,16 @@ export type ResolverInstrumentationMetadata = {
184
185
  };
185
186
 
186
187
  export type ResolverAdapterFactory = (
187
- configuration: string,
188
+ configuration: string | undefined,
188
189
  timeoutMs: number,
189
190
  allowedHosts: readonly string[],
190
191
  ) => ResolverVendorAdapter;
191
192
 
192
193
  const resolverAdapterRegistry: Partial<Record<ProviderResolverVendor, ResolverAdapterFactory>> = {
193
194
  "2captcha"(configuration, timeoutMs, allowedHosts) {
195
+ if (configuration === undefined) {
196
+ throw new Error("2captcha resolver adapter factory requires an API key");
197
+ }
194
198
  return createTwoCaptchaResolverVendorAdapter({
195
199
  allowedHosts,
196
200
  apiKey: configuration,
@@ -344,10 +348,21 @@ function throwUnsupportedKind(kind: ProviderChallengeKind): never {
344
348
  }
345
349
 
346
350
  function throwExhausted(attempts: readonly ResolverChainAttempt[]): never {
347
- 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(", ");
348
361
  throw new ProviderError(`Resolver vendor chain exhausted: ${summary}`, {
349
362
  code: "RESOLVER_CHAIN_EXHAUSTED",
350
- 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.",
351
366
  details: attempts,
352
367
  });
353
368
  }
@@ -470,6 +485,7 @@ function unavailableAttempt(error: ResolverVendorUnavailableError): ResolverChai
470
485
  return {
471
486
  vendor: error.vendor,
472
487
  reason: error.reason,
488
+ ...(error.missingFields ? { missingFields: [...error.missingFields] } : {}),
473
489
  ...(cause ? { cause } : {}),
474
490
  ...(upstreamHost ? { upstreamHost } : {}),
475
491
  ...(phase ? { phase } : {}),
@@ -481,6 +497,7 @@ function unavailableSpanAttributes(error: ResolverVendorUnavailableError): Recor
481
497
  const attempt = unavailableAttempt(error);
482
498
  return {
483
499
  unavailability_reason: error.reason,
500
+ missing_fields: error.missingFields,
484
501
  cause_name: attempt.cause?.name,
485
502
  cause_message: attempt.cause?.message,
486
503
  upstream_host: attempt.upstreamHost,
@@ -914,6 +931,8 @@ function createResolverChainClient(options: {
914
931
  } catch (error) {
915
932
  signal.throwIfAborted();
916
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.
917
936
  attempts.push(unavailableAttempt(error));
918
937
  }
919
938
  }
@@ -966,15 +985,20 @@ function resolveVendorAvailability(
966
985
  reason: "missing_transport",
967
986
  };
968
987
  }
988
+ if (vendor === "browser") {
989
+ return {
990
+ vendor,
991
+ available: true,
992
+ configuration: normalizedEnvValue(env, APIFUSE__CDP_POOL__URL),
993
+ };
994
+ }
969
995
 
970
996
  const envKey =
971
997
  vendor === "2captcha"
972
998
  ? APIFUSE__RESOLVER__2CAPTCHA__API_KEY
973
999
  : vendor === "capsolver"
974
1000
  ? APIFUSE__RESOLVER__CAPSOLVER__API_KEY
975
- : vendor === "capmonster"
976
- ? APIFUSE__RESOLVER__CAPMONSTER__API_KEY
977
- : APIFUSE__CDP_POOL__URL;
1001
+ : APIFUSE__RESOLVER__CAPMONSTER__API_KEY;
978
1002
 
979
1003
  const configuration = normalizedEnvValue(env, envKey);
980
1004
  return configuration
@@ -39,7 +39,6 @@ import { createProviderCache } from "../runtime/cache.js";
39
39
  import {
40
40
  createProviderChoiceContext,
41
41
  PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
42
- PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV,
43
42
  } from "../runtime/choice.js";
44
43
  import { createCredentialContext } from "../runtime/credential.js";
45
44
  import { createEnvContext } from "../runtime/env.js";
@@ -572,8 +571,18 @@ export function resolveProviderResolverIdentityScope(
572
571
  });
573
572
  }
574
573
 
575
- function resolveOperationConnectionId(request: OperationRequest): string | undefined {
576
- 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;
577
586
  }
578
587
 
579
588
  function resolveNativeProxyPolicy(provider: ProviderDefinition): ProviderProxyPolicy | undefined {
@@ -627,7 +636,6 @@ function createProviderContext(
627
636
  const env = createEnvContext([
628
637
  ...(provider.secrets?.map((secret) => secret.name) ?? []),
629
638
  PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
630
- PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV,
631
639
  ]);
632
640
  const credential = createCredentialContext({
633
641
  allowedKeys: provider.credential?.keys,
@@ -778,10 +786,27 @@ function createFlowContextStore(
778
786
  };
779
787
  }
780
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
+
781
805
  function createAuthFlowContext(
782
806
  provider: ProviderDefinition,
783
807
  request: AuthFlowRequest,
784
808
  options: ProviderServerRuntimeOptions,
809
+ state: ProviderRuntimeState,
785
810
  signal?: AbortSignal,
786
811
  ): {
787
812
  context: FlowContext;
@@ -798,12 +823,7 @@ function createAuthFlowContext(
798
823
  );
799
824
  const proxyClientOptions = {
800
825
  upstream: { proxy: provider.proxy },
801
- affinityKey:
802
- request.connectionId ??
803
- request.externalRef ??
804
- request.tenantId ??
805
- request.providerId ??
806
- provider.id,
826
+ affinityKey: resolveAuthFlowProxyAffinityKey(provider, request),
807
827
  };
808
828
  const resolverIdentityScope = resolveProviderResolverIdentityScope(
809
829
  provider,
@@ -838,7 +858,7 @@ function createAuthFlowContext(
838
858
  return {
839
859
  context: {
840
860
  flowId: request.flowId,
841
- connectionId: request.connectionId,
861
+ connectionId: resolveOperationConnectionId(request),
842
862
  externalRef: request.externalRef,
843
863
  tenantId: request.tenantId ?? "",
844
864
  providerId: request.providerId ?? provider.id,
@@ -846,6 +866,7 @@ function createAuthFlowContext(
846
866
  ...proxyClientOptions,
847
867
  ...(signal ? { signal } : {}),
848
868
  }),
869
+ state: state.forConnection(resolveOperationConnectionId(request)),
849
870
  stealth: stealthBaseUrl
850
871
  ? capabilityModules.stealth
851
872
  ? stealthProfile
@@ -2007,6 +2028,7 @@ async function handleAuthFlow(
2007
2028
  request: AuthFlowRequest,
2008
2029
  route: AuthRoute,
2009
2030
  options: ProviderServerRuntimeOptions,
2031
+ state: ProviderRuntimeState,
2010
2032
  signal?: AbortSignal,
2011
2033
  ): Promise<Response | AuthFlowResponse> {
2012
2034
  const flow = provider.auth?.flow;
@@ -2021,7 +2043,7 @@ async function handleAuthFlow(
2021
2043
  // any flow code runs instead of at whatever point the ceremony first reads
2022
2044
  // the env. `abort` stays exempt: a user must always be able to cancel a
2023
2045
  // stranded flow even when provisioning is broken.
2024
- const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
2046
+ const { context, getPatch } = createAuthFlowContext(provider, request, options, state, signal);
2025
2047
  try {
2026
2048
  if (route !== "abort") {
2027
2049
  assertRequiredSecretsPresent(provider, context.env);
@@ -2542,7 +2564,14 @@ function createServerAppWithCapabilityModules(
2542
2564
  .json()
2543
2565
  .catch(() => undefined);
2544
2566
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
2545
- 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
+ );
2546
2575
  logProviderSuccess(
2547
2576
  logger,
2548
2577
  provider,
@@ -2582,7 +2611,14 @@ function createServerAppWithCapabilityModules(
2582
2611
  .json()
2583
2612
  .catch(() => undefined);
2584
2613
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
2585
- 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
+ );
2586
2622
  logProviderSuccess(
2587
2623
  logger,
2588
2624
  provider,
@@ -2622,7 +2658,14 @@ function createServerAppWithCapabilityModules(
2622
2658
  .json()
2623
2659
  .catch(() => undefined);
2624
2660
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
2625
- 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
+ );
2626
2669
  logProviderSuccess(
2627
2670
  logger,
2628
2671
  provider,
@@ -2662,7 +2705,14 @@ function createServerAppWithCapabilityModules(
2662
2705
  .json()
2663
2706
  .catch(() => undefined);
2664
2707
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
2665
- 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
+ );
2666
2716
  logProviderSuccess(
2667
2717
  logger,
2668
2718
  provider,
@@ -2702,7 +2752,14 @@ function createServerAppWithCapabilityModules(
2702
2752
  .json()
2703
2753
  .catch(() => undefined);
2704
2754
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
2705
- 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
+ );
2706
2763
  logProviderSuccess(
2707
2764
  logger,
2708
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
@@ -1757,7 +1757,7 @@ export interface BrowserCookie {
1757
1757
  readonly sameSite?: "Strict" | "Lax" | "None";
1758
1758
  }
1759
1759
 
1760
- export type BrowserResourceMethod = "GET" | "HEAD";
1760
+ export type BrowserResourceMethod = "GET" | "HEAD" | "POST";
1761
1761
 
1762
1762
  export type BrowserResourceRequest = {
1763
1763
  readonly url: string;
@@ -1769,6 +1769,9 @@ export type BrowserResourceRequest = {
1769
1769
  export type BrowserResourceBody = Buffer | Uint8Array | ArrayBuffer | string;
1770
1770
 
1771
1771
  export type BrowserResourceDecision =
1772
+ | {
1773
+ readonly action: "continue";
1774
+ }
1772
1775
  | {
1773
1776
  readonly action: "fulfill";
1774
1777
  readonly status?: number;
@@ -1793,11 +1796,19 @@ export type BrowserResourceRoute = {
1793
1796
  export type BrowserResourcePolicy = {
1794
1797
  readonly defaultAction?: "block";
1795
1798
  readonly allowedMethods?: readonly BrowserResourceMethod[];
1799
+ /**
1800
+ * Appends an enforcing CSP header to every renderable document response
1801
+ * while the policy is active. Existing CSP headers are retained, so this
1802
+ * can only further restrict the document.
1803
+ */
1804
+ readonly documentContentSecurityPolicy?: string;
1796
1805
  readonly routes: readonly BrowserResourceRoute[];
1797
1806
  };
1798
1807
 
1799
1808
  export interface BrowserPage extends BrowserFrame {
1800
1809
  close(): Promise<void>;
1810
+ /** Returns the user agent used by this page's browser context. */
1811
+ userAgent(): Promise<string>;
1801
1812
  /**
1802
1813
  * Reads the browser context's cookie jar, including httpOnly cookies.
1803
1814
  * Cookie expiry values are Unix seconds and are absent for session cookies.
@@ -1905,7 +1916,7 @@ export type ProviderChoiceExplicitParseResult =
1905
1916
  readonly payload: Record<string, unknown>;
1906
1917
  /** Stable, opaque key for provider-owned idempotency records. */
1907
1918
  readonly replayKey: string;
1908
- /** Atomically claims a word token. Legacy managed tokens report unsupported. */
1919
+ /** Atomically claims a word token. Inline tokens report unsupported. */
1909
1920
  consume(): Promise<ProviderChoiceConsumeResult>;
1910
1921
  }
1911
1922
  | {
@@ -1961,7 +1972,7 @@ export interface ProviderChoiceParseOptions {
1961
1972
  futureToleranceMs?: number;
1962
1973
  bind?: ProviderChoiceBindingOptions;
1963
1974
  storage?: ProviderChoiceStorageOptions;
1964
- /** Defaults to never, matching legacy managed-token parse semantics. */
1975
+ /** Defaults to never, preserving reusable choice-token parse semantics. */
1965
1976
  consume?: ProviderChoiceConsumeMode;
1966
1977
  }
1967
1978
 
@@ -2115,6 +2126,18 @@ export interface FlowContext {
2115
2126
  tenantId: string;
2116
2127
  providerId: string;
2117
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;
2118
2141
  /** Present when the selected runtime supplies native network capabilities. */
2119
2142
  readonly native?: NativeProviderContext;
2120
2143
  stealth: StealthClient;