@apifuse/provider-sdk 2.2.0-beta.31 → 2.2.0-beta.33

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.
@@ -3,7 +3,6 @@ import { assertFreshProviderChoiceIssuedAt, ProviderChoiceTokenError, } from "..
3
3
  import { isProviderError, ProviderError } from "../errors.js";
4
4
  import { CHOICE_WORDLIST_SIZE, choiceWordAt, HIGH_CHOICE_WORD_COUNT, isChoiceWord, STANDARD_CHOICE_WORD_COUNT, } from "./choice-wordlist.js";
5
5
  export const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
6
- export const PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_WORD_ISSUANCE";
7
6
  const PRIMARY_CHOICE_TOKEN_KID = "v1";
8
7
  const MANAGED_CHOICE_TOKEN_VERSION = 1;
9
8
  const SERVER_STORED_CHOICE_RECORD_VERSION = 1;
@@ -16,7 +15,6 @@ export function createProviderChoiceContext(options) {
16
15
  const issuedAtMs = issueOptions.nowMs ?? Date.now();
17
16
  const resolvedStorage = resolveIssueStorage(issueOptions.storage, issueOptions.payload);
18
17
  if (resolvedStorage.mode === "server") {
19
- const issuance = resolveChoiceWordIssuance(options.env);
20
18
  const keys = hasRequestedChoiceBinding(issueOptions.bind)
21
19
  ? deriveManagedChoiceKeys({
22
20
  masterSecret: resolveMasterSecret(),
@@ -42,28 +40,8 @@ export function createProviderChoiceContext(options) {
42
40
  ttl_ms: issueOptions.ttlMs,
43
41
  binding,
44
42
  };
45
- if (issuance === "legacy") {
46
- const legacyKeys = keys ??
47
- deriveManagedChoiceKeys({
48
- masterSecret: resolveMasterSecret(),
49
- providerId: options.providerId,
50
- purpose: issueOptions.purpose,
51
- kid,
52
- });
53
- return issueLegacyServerStoredChoice({
54
- baseEnvelope,
55
- issueOptions,
56
- storage: resolvedStorage.storage,
57
- contextState: options.state,
58
- kid,
59
- keys: legacyKeys,
60
- issuedAtMs,
61
- });
62
- }
63
43
  return issueWordServerStoredChoice({
64
- baseEnvelope: {
65
- ...baseEnvelope,
66
- },
44
+ baseEnvelope,
67
45
  issueOptions,
68
46
  storage: resolvedStorage.storage,
69
47
  contextState: options.state,
@@ -307,20 +285,6 @@ function createLegacyExplicitParseResult(options) {
307
285
  },
308
286
  };
309
287
  }
310
- function resolveChoiceWordIssuance(env) {
311
- const configured = env?.get(PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV);
312
- const value = configured?.trim() ?? "";
313
- if (value === "" || value === "legacy")
314
- return "legacy";
315
- if (value === "word")
316
- return "word";
317
- throw new ProviderError(`Unsupported provider choice word issuance mode "${value}". Expected "legacy" or "word".`, {
318
- code: "CHOICE_WORD_ISSUANCE_INVALID",
319
- category: "input_validation",
320
- retryable: false,
321
- details: { env: PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV },
322
- });
323
- }
324
288
  function resolveChoiceMasterSecret(options) {
325
289
  const configured = options.masterSecret ?? options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
326
290
  const trimmed = configured?.trim();
@@ -420,47 +384,6 @@ async function issueWordServerStoredChoice(options) {
420
384
  retryable: false,
421
385
  });
422
386
  }
423
- /**
424
- * Beta.28-compatible server handle issuance. The state stores the payload and
425
- * the client receives the existing six-part encrypted envelope whose payload
426
- * is a server-state handle.
427
- */
428
- async function issueLegacyServerStoredChoice(options) {
429
- const serializedPayload = serializeChoicePayload(options.issueOptions.payload);
430
- const payloadBytes = Buffer.byteLength(serializedPayload, "utf8");
431
- if (payloadBytes > options.storage.maxValueBytes) {
432
- throw new ProviderError("Provider choice payload exceeds state storage policy.", {
433
- code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
434
- category: "input_validation",
435
- retryable: false,
436
- details: { maxValueBytes: options.storage.maxValueBytes, payloadBytes },
437
- });
438
- }
439
- const stateId = `choice_${randomBytes(16).toString("base64url")}`;
440
- const namespace = resolveChoiceStateNamespace({
441
- storage: options.storage,
442
- contextState: options.contextState,
443
- ttlMs: options.issueOptions.ttlMs,
444
- });
445
- await namespace.set(optionsStateKey(stateId), options.issueOptions.payload, {
446
- ttl: stateTtl(options.storage, options.issueOptions.ttlMs),
447
- });
448
- const envelope = {
449
- ...options.baseEnvelope,
450
- payload: {
451
- storage: "server",
452
- state_id: stateId,
453
- payload_digest: digestChoicePayload(serializedPayload),
454
- created_at_ms: options.issuedAtMs,
455
- },
456
- };
457
- return encryptManagedChoiceToken({
458
- prefix: options.issueOptions.prefix,
459
- kid: options.kid,
460
- envelope,
461
- keys: options.keys,
462
- });
463
- }
464
387
  async function parseWordServerStoredChoice(options) {
465
388
  const storage = resolveParseStorage(options.parseOptions.storage);
466
389
  const namespace = resolveChoiceStateNamespace({
@@ -3,6 +3,8 @@ import { type BrowserClientOptions } from "../browser.js";
3
3
  import type { TraceRecorder } from "../trace.js";
4
4
  import { type ResolverIdentity, type ResolverVendorAdapter } from "./types.js";
5
5
  type BrowserClientFactory = (options: BrowserClientOptions) => BrowserClient;
6
+ /** Internal test seam; deliberately not re-exported from the package root. */
7
+ export declare function swapBrowserResolverClientFactoryForTests(factory: BrowserClientFactory | undefined): () => void;
6
8
  export interface BrowserResolverVendorOptions {
7
9
  readonly cdpUrl?: string;
8
10
  readonly timeoutMs: number;
@@ -5,10 +5,25 @@ import { assertResolverHostAllowed, normalizedResolverHostname } from "./hosts.j
5
5
  import { ResolverVendorUnavailableError, } from "./types.js";
6
6
  const BROWSER_VENDOR_ID = "browser";
7
7
  const DEFAULT_COOKIE_POLL_INTERVAL_MS = 100;
8
+ const AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX = ".awswaf.com";
9
+ const RESOLVER_DOCUMENT_CONTENT_SECURITY_POLICY = "connect-src http: https:; worker-src 'none'";
8
10
  const SUCCESS_COOKIE_NAMES = {
9
11
  aws_waf: "aws-waf-token",
10
12
  cloudflare_interstitial: "cf_clearance",
11
13
  };
14
+ let createResolverBrowserClient = createBrowserClient;
15
+ /** Internal test seam; deliberately not re-exported from the package root. */
16
+ export function swapBrowserResolverClientFactoryForTests(factory) {
17
+ const original = createResolverBrowserClient;
18
+ createResolverBrowserClient = factory ?? createBrowserClient;
19
+ let restored = false;
20
+ return () => {
21
+ if (restored)
22
+ return;
23
+ restored = true;
24
+ createResolverBrowserClient = original;
25
+ };
26
+ }
12
27
  class BrowserSolveTimeoutError extends Error {
13
28
  constructor() {
14
29
  super("Browser resolver solve budget elapsed");
@@ -126,22 +141,58 @@ function selectSuccessCookie(cookies, successCookieName, pageUrl) {
126
141
  cookieDomainSpecificity(right) - cookieDomainSpecificity(left) ||
127
142
  right.path.length - left.path.length)[0];
128
143
  }
129
- async function solveInPage(page, pageUrl, successCookieName, pollIntervalMs, signal) {
130
- await raceWithAbort(() => page.goto(pageUrl), signal);
131
- while (true) {
132
- const cookies = await raceWithAbort(() => page.cookies(), signal);
133
- const successCookie = selectSuccessCookie(cookies, successCookieName, pageUrl);
134
- if (successCookie) {
135
- const userAgent = await raceWithAbort(() => page.evaluate("navigator.userAgent"), signal);
136
- return {
137
- form: "cookies",
138
- cookies: { [successCookieName]: successCookie.value },
139
- userAgent,
140
- ...(successCookie.expires === undefined ? {} : { expires: successCookie.expires }),
141
- };
144
+ async function solveInPage(page, challengeKind, pageUrl, allowedHosts, successCookieName, pollIntervalMs, signal) {
145
+ return await page.withResourcePolicy({
146
+ allowedMethods: ["GET", "HEAD", "POST"],
147
+ documentContentSecurityPolicy: RESOLVER_DOCUMENT_CONTENT_SECURITY_POLICY,
148
+ routes: [
149
+ {
150
+ match: () => true,
151
+ handle: (request) => ({
152
+ action: isResolverBrowserRequestAllowed(request.url, challengeKind, allowedHosts)
153
+ ? "continue"
154
+ : "block",
155
+ }),
156
+ },
157
+ ],
158
+ }, async () => {
159
+ const userAgent = await raceWithAbort(() => page.evaluate("navigator.userAgent"), signal);
160
+ await raceWithAbort(() => page.goto(pageUrl), signal);
161
+ while (true) {
162
+ const cookies = await raceWithAbort(() => page.cookies(), signal);
163
+ const successCookie = selectSuccessCookie(cookies, successCookieName, pageUrl);
164
+ if (successCookie) {
165
+ return {
166
+ form: "cookies",
167
+ cookies: { [successCookieName]: successCookie.value },
168
+ userAgent,
169
+ ...(successCookie.expires === undefined ? {} : { expires: successCookie.expires }),
170
+ };
171
+ }
172
+ await abortableDelay(pollIntervalMs, signal);
142
173
  }
143
- await abortableDelay(pollIntervalMs, signal);
174
+ });
175
+ }
176
+ function isResolverBrowserRequestAllowed(targetUrl, challengeKind, allowedHosts) {
177
+ try {
178
+ assertResolverHostAllowed(targetUrl, allowedHosts);
179
+ return true;
180
+ }
181
+ catch {
182
+ if (challengeKind !== "aws_waf")
183
+ return false;
184
+ }
185
+ let target;
186
+ try {
187
+ target = new URL(targetUrl);
188
+ }
189
+ catch {
190
+ return false;
144
191
  }
192
+ const hostname = normalizedResolverHostname(target.hostname);
193
+ return (target.protocol === "https:" &&
194
+ hostname.length > AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX.length &&
195
+ hostname.endsWith(AWS_WAF_CHALLENGE_INFRASTRUCTURE_HOST_SUFFIX));
145
196
  }
146
197
  const POOL_ALLOCATION_EXHAUSTED_CODES = new Set([
147
198
  -32_001, // queue full
@@ -188,7 +239,7 @@ async function closeBrowserClient(client, timeoutMs, challengeKind, traceRecorde
188
239
  await runBoundedCleanup(() => close.call(client), timeoutMs, challengeKind, "client.close", traceRecorder);
189
240
  }
190
241
  export function createBrowserResolverVendorAdapter(options) {
191
- const createClient = options.createClient ?? createBrowserClient;
242
+ const createClient = options.createClient ?? createResolverBrowserClient;
192
243
  const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_COOKIE_POLL_INTERVAL_MS;
193
244
  return {
194
245
  id: BROWSER_VENDOR_ID,
@@ -235,10 +286,11 @@ export function createBrowserResolverVendorAdapter(options) {
235
286
  cdpUrl: cdpUrl ?? "",
236
287
  ...(proxyUrl === undefined ? {} : { proxy: proxyUrl }),
237
288
  requireCdpPool: cdpUrl !== undefined,
289
+ serviceWorkers: "block",
238
290
  });
239
291
  const contextOperation = client.withIsolatedContext(async (page) => {
240
292
  handlerEntered = true;
241
- return await solveInPage(page, challenge.pageUrl, SUCCESS_COOKIE_NAMES[challengeKind], pollIntervalMs, solveController.signal);
293
+ return await solveInPage(page, challengeKind, challenge.pageUrl, options.allowedHosts, SUCCESS_COOKIE_NAMES[challengeKind], pollIntervalMs, solveController.signal);
242
294
  });
243
295
  try {
244
296
  return await raceWithAbort(() => contextOperation, solveController.signal);
@@ -0,0 +1,22 @@
1
+ import type { ChallengeSolution, ProviderChallenge } from "../../types.js";
2
+ import type { TraceRecorder } from "../trace.js";
3
+ import { type ResolverIdentity, type ResolverVendorAdapter } from "./types.js";
4
+ type Delay = (ms: number, signal: AbortSignal) => Promise<void>;
5
+ export interface CapsolverResolverVendorOptions {
6
+ readonly apiKey?: string;
7
+ readonly timeoutMs?: number;
8
+ readonly pollIntervalMs?: number;
9
+ readonly allowedHosts: readonly string[];
10
+ readonly fetchImpl?: typeof fetch;
11
+ readonly baseUrl?: string;
12
+ /** Test-only clock override; supplying it disables the real-time deadline timer. */
13
+ readonly now?: () => number;
14
+ /** Test-only delay override used with `now` to exercise polling without sleeping. */
15
+ readonly delay?: Delay;
16
+ }
17
+ export interface CapsolverResolverVendorAdapter extends ResolverVendorAdapter {
18
+ readonly id: "capsolver";
19
+ solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder): Promise<ChallengeSolution>;
20
+ }
21
+ export declare function createCapsolverResolverVendorAdapter(options: CapsolverResolverVendorOptions): CapsolverResolverVendorAdapter;
22
+ export {};