@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.35
4
+
5
+ - Release candidate for main commit 027fa0087b883a6281bc6d8bdaaf6d0be382000f.
6
+
7
+ ## 2.2.0-beta.34
8
+
9
+ - Release candidate for main commit 383a97e7d0ce995d098d5f95c9ffdcbeed13de31.
10
+
11
+ ## 2.2.0-beta.33
12
+
13
+ - Release candidate for main commit e7d854ec3e859702b22e2b37eaa2ae6bd4545c80.
14
+
3
15
  ## 2.2.0-beta.32
4
16
 
5
17
  - Release candidate for main commit 82f1c0c1de916a40c8c74b1d6470a493b134c0ed.
@@ -46,6 +58,8 @@
46
58
 
47
59
  ## Unreleased
48
60
 
61
+ - Server-stored provider choices now issue word-format tokens unconditionally. The runtime issuance setting and legacy issuance path were removed.
62
+ - **Breaking:** Legacy encrypted server-handle choice tokens are no longer parsed. They are rejected with the uniform choice-not-found error; server-stored choices are now word-only. Encrypted inline choice tokens remain supported.
49
63
  - Added the `thrown-error-code-undeclared` authoring lint (warning level): `apifuse check` now statically flags literal `ProviderError`/`ValidationError` codes that are neither SDK-registered nor declared in any operation's `docs.errorCodes`, surfacing the runtime `unregistered_provider_error_code` signal at check time. The canonical SDK code→status mapping moved to `SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES` in `error-resolution.ts`, shared by the runtime status resolver and the lint.
50
64
 
51
65
  ## 2.2.0-beta.21
@@ -18,7 +18,6 @@ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
18
18
  "RUNTIME_UNSUPPORTED",
19
19
  "PROVIDER_STATE_UNSUPPORTED",
20
20
  "CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
21
- "CHOICE_WORD_ISSUANCE_INVALID",
22
21
  "CHOICE_STATE_PAYLOAD_TOO_LARGE",
23
22
  "CHOICE_STATE_UNAVAILABLE",
24
23
  "CHOICE_CONTEXT_REQUIRED",
package/dist/index.d.ts CHANGED
@@ -17,7 +17,7 @@ export * from "./recipes/rest-api.js";
17
17
  export { createFlowContext, createScratchpad } from "./runtime/auth-flow.js";
18
18
  export type { BrowserClientOptions } from "./runtime/browser.js";
19
19
  export { APIFUSE__CACHE__KEY_PEPPER_ENV, createBypassProviderCache, createProviderCache, type ProviderCacheOptions, } from "./runtime/cache.js";
20
- export { type CreateProviderChoiceContextOptions, createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV, } from "./runtime/choice.js";
20
+ export { type CreateProviderChoiceContextOptions, createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
21
21
  export { type CreateCredentialContextOptions, createCredentialContext, } from "./runtime/credential.js";
22
22
  export { createEnvContext } from "./runtime/env.js";
23
23
  export { executeOperation } from "./runtime/executor.js";
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ export * from "./recipes/gov-api.js";
15
15
  export * from "./recipes/rest-api.js";
16
16
  export { createFlowContext, createScratchpad } from "./runtime/auth-flow.js";
17
17
  export { APIFUSE__CACHE__KEY_PEPPER_ENV, createBypassProviderCache, createProviderCache, } from "./runtime/cache.js";
18
- export { createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV, } from "./runtime/choice.js";
18
+ export { createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
19
19
  export { createCredentialContext, } from "./runtime/credential.js";
20
20
  export { createEnvContext } from "./runtime/env.js";
21
21
  export { executeOperation } from "./runtime/executor.js";
@@ -5,7 +5,7 @@ export { assertFreshProviderChoiceIssuedAt, createProviderChoiceToken, ProviderC
5
5
  export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, every, } from "./define.js";
6
6
  export { AuthError, HttpRedirectError, isProviderError, isSessionExpiredError, isTransportError, ProviderError, SessionExpiredError, TransportError, ValidationError, } from "./errors.js";
7
7
  export { getProviderLocalePath, providerLocaleKey, qualifyProviderLocaleKey, } from "./i18n/index.js";
8
- export { type CreateProviderChoiceContextOptions, createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV, } from "./runtime/choice.js";
8
+ export { type CreateProviderChoiceContextOptions, createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
9
9
  export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, collectSensitivePaths, describeKey, field, fields, isSensitiveSchema, redactPayload, type SensitiveFieldKind, type SensitiveFieldOptions, type SensitivePath, sensitive, z, } from "./schema.js";
10
10
  export type { AuthAbortData, AuthAbortRetry, AuthFlowTerminalContext, AuthMode, AuthSafeData, AuthSafeJson, FlowContext, HealthCheckAssertionContext, HealthCheckCase, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthScheduleRandomization, HttpRedirectFailureReason, HttpRedirectPolicy, HttpRedirectPolicyMode, HttpRetryOptions, HttpRetrySummary, InferSchemaOutput, NativeContext, NativeNetworkClient, NativeNetworkCloseReason, NativeNetworkConnection, NativeNetworkConnectInput, NativeNetworkConnectOptions, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProviderContext, NativeProxyDrainHandler, NativeProxyEgressInfo, NativeProxyExpiringEvent, NativeProxyExpiringReason, NativeTcpDynamicEgressRule, NativeTcpEgressGrant, NativeTcpEgressRule, NativeTcpPortRange, NativeTcpTlsMode, NativeTlsConnectOptions, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDocMeta, OperationErrorCode, ProviderErrorStatus, OperationInputExample, OperationLifecycle, OperationObservabilityConfig, OperationObservabilitySensitiveConfig, OperationRelationships, OperationRiskClass, OperationSensitivePath, OperationToolRouterMetadata, OperationTransport, ProviderAccessVisibility, ProviderChoiceBindingOptions, ProviderChoiceConsumeMode, ProviderChoiceConsumeResult, ProviderChoiceContext, ProviderChoiceExplicitParseResult, ProviderChoiceIssueOptions, ProviderChoiceParseOptions, ProviderContext, ProviderDefinition, ProviderDeploymentOverrides, ProviderFileRef, ProviderFilesContext, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderProxyPolicy, ProxiedOAuthConfig, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderResolvedFile, ProviderRuntimeState, ProviderStateDurationString, ProviderStateNamespace, ProviderSupportLevel, RedirectRunReason, SchemaLike, SmsOtpMatcherDefinition, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateNamespaceScope, StateValue, StateWriteOptions, } from "./types.js";
11
11
  export { NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, } from "./runtime/native-network-errors.js";
package/dist/provider.js CHANGED
@@ -4,7 +4,7 @@ export { assertFreshProviderChoiceIssuedAt, createProviderChoiceToken, ProviderC
4
4
  export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, every, } from "./define.js";
5
5
  export { AuthError, HttpRedirectError, isProviderError, isSessionExpiredError, isTransportError, ProviderError, SessionExpiredError, TransportError, ValidationError, } from "./errors.js";
6
6
  export { getProviderLocalePath, providerLocaleKey, qualifyProviderLocaleKey, } from "./i18n/index.js";
7
- export { createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV, } from "./runtime/choice.js";
7
+ export { createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
8
8
  export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, collectSensitivePaths, describeKey, field, fields, isSensitiveSchema, redactPayload, sensitive, z, } from "./schema.js";
9
9
  export { NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, } from "./runtime/native-network-errors.js";
10
10
  export { HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, } from "./types.js";
@@ -1,4 +1,4 @@
1
- import type { ContextScratchpad, EnvContext, FlowContext, HttpClient, OcrContext, StealthClient, SttContext } from "../types.js";
1
+ import type { ContextScratchpad, EnvContext, FlowContext, HttpClient, OcrContext, ProviderRuntimeState, StealthClient, SttContext } from "../types.js";
2
2
  export declare function createScratchpad(allowedKeys: string[], initial?: Record<string, unknown>): ContextScratchpad;
3
3
  export declare function createFlowContext(options: {
4
4
  flowId?: string;
@@ -9,6 +9,8 @@ export declare function createFlowContext(options: {
9
9
  providerId: string;
10
10
  connectionId?: string;
11
11
  externalRef?: string;
12
+ /** Host-agnostic: callers pass an already-scoped runtime state, which this helper forwards verbatim. */
13
+ state?: ProviderRuntimeState;
12
14
  allowedKeys: string[];
13
15
  initialContext?: Record<string, unknown>;
14
16
  ocr?: OcrContext;
@@ -40,6 +40,7 @@ export function createFlowContext(options) {
40
40
  tenantId: options.tenantId,
41
41
  providerId: options.providerId,
42
42
  http: options.http,
43
+ state: options.state,
43
44
  stealth: options.stealth,
44
45
  env: options.env,
45
46
  context: createScratchpad(options.allowedKeys, options.initialContext),
@@ -5,6 +5,7 @@ export type BrowserClientOptions = BrowserOptions & {
5
5
  cdpUrl?: string;
6
6
  executablePath?: string;
7
7
  extraArgs?: string[];
8
+ serviceWorkers?: "allow" | "block";
8
9
  };
9
10
  export declare class BrowserClient implements BrowserClientContract {
10
11
  private readonly client;
@@ -15,7 +15,7 @@ function toResourceBody(body) {
15
15
  return Buffer.from(body);
16
16
  }
17
17
  function isResourceMethod(method) {
18
- return method === "GET" || method === "HEAD";
18
+ return method === "GET" || method === "HEAD" || method === "POST";
19
19
  }
20
20
  async function toResourceRequest(request) {
21
21
  const method = request.method().toUpperCase();
@@ -59,6 +59,68 @@ function getCdpPausedRequestId(params) {
59
59
  }
60
60
  return params.requestId;
61
61
  }
62
+ function isCdpResponseStage(params) {
63
+ return (isRecord(params) &&
64
+ (typeof params.responseStatusCode === "number" ||
65
+ typeof params.responseErrorReason === "string"));
66
+ }
67
+ function toCdpPausedDocumentResponse(params) {
68
+ if (!isRecord(params) ||
69
+ typeof params.requestId !== "string" ||
70
+ typeof params.responseStatusCode !== "number" ||
71
+ params.resourceType !== "Document" ||
72
+ !isRecord(params.request)) {
73
+ return null;
74
+ }
75
+ const responseCode = params.responseStatusCode;
76
+ const responseHeaders = Array.isArray(params.responseHeaders)
77
+ ? params.responseHeaders.flatMap((header) => {
78
+ if (!isRecord(header) ||
79
+ typeof header.name !== "string" ||
80
+ typeof header.value !== "string") {
81
+ return [];
82
+ }
83
+ return [{ name: header.name, value: header.value }];
84
+ })
85
+ : [];
86
+ return {
87
+ hasBody: String(params.request.method ?? "").toUpperCase() !== "HEAD" &&
88
+ responseCode >= 200 &&
89
+ (responseCode < 300 || responseCode >= 400) &&
90
+ responseCode !== 204 &&
91
+ responseCode !== 205 &&
92
+ responseCode !== 304,
93
+ requestId: params.requestId,
94
+ responseCode,
95
+ responseHeaders,
96
+ };
97
+ }
98
+ function toCdpDocumentFulfillParams(response, body, contentSecurityPolicy) {
99
+ const responseBody = typeof body.body === "string" ? body.body : "";
100
+ return {
101
+ body: body.base64Encoded ? responseBody : Buffer.from(responseBody).toString("base64"),
102
+ requestId: response.requestId,
103
+ responseCode: response.responseCode,
104
+ responseHeaders: [
105
+ ...response.responseHeaders,
106
+ { name: "Content-Security-Policy", value: contentSecurityPolicy },
107
+ ],
108
+ };
109
+ }
110
+ function resourcePolicyFetchPatterns(policy) {
111
+ return [
112
+ { requestStage: "Request", urlPattern: "*" },
113
+ ...(policy.documentContentSecurityPolicy
114
+ ? [
115
+ {
116
+ requestStage: "Response",
117
+ resourceType: "Document",
118
+ urlPattern: "*",
119
+ },
120
+ ]
121
+ : []),
122
+ ];
123
+ }
62
124
  function toCdpResourceHeaders(value) {
63
125
  if (!isRecord(value)) {
64
126
  return {};
@@ -104,6 +166,128 @@ async function fulfillResourceRoute(route, decision) {
104
166
  status: decision.status ?? 200,
105
167
  });
106
168
  }
169
+ async function decideResourceRequest(policy, request) {
170
+ for (const resourceRoute of policy.routes) {
171
+ if (matchesResourceRoute(resourceRoute.match, request)) {
172
+ return await resourceRoute.handle(request);
173
+ }
174
+ }
175
+ return { action: "block" };
176
+ }
177
+ async function abortResourceRoute(route) {
178
+ await route.abort("blockedbyclient");
179
+ }
180
+ async function handleCdpResourceRequest(session, policy, allowedMethods, params) {
181
+ const paused = toCdpResourceRequest(params);
182
+ const requestId = paused?.requestId ?? getCdpPausedRequestId(params);
183
+ if (!requestId)
184
+ return;
185
+ try {
186
+ if (!paused || !allowedMethods.has(paused.request.method)) {
187
+ await session.send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId });
188
+ return;
189
+ }
190
+ const decision = await decideResourceRequest(policy, paused.request);
191
+ switch (decision.action) {
192
+ case "continue":
193
+ await session.send("Fetch.continueRequest", { requestId });
194
+ return;
195
+ case "fulfill":
196
+ await session.send("Fetch.fulfillRequest", toCdpFulfillParams(requestId, decision));
197
+ return;
198
+ case "block":
199
+ await session.send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId });
200
+ return;
201
+ }
202
+ }
203
+ catch {
204
+ await session
205
+ .send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId })
206
+ .catch(() => undefined);
207
+ }
208
+ }
209
+ async function handleCdpDocumentResponse(session, contentSecurityPolicy, params) {
210
+ const requestId = getCdpPausedRequestId(params);
211
+ if (!requestId)
212
+ return;
213
+ try {
214
+ const response = toCdpPausedDocumentResponse(params);
215
+ if (!response)
216
+ throw new Error("CDP document response metadata is unavailable");
217
+ if (!response.hasBody) {
218
+ await session.send("Fetch.continueResponse", {
219
+ requestId,
220
+ responseCode: response.responseCode,
221
+ responseHeaders: [...response.responseHeaders],
222
+ });
223
+ return;
224
+ }
225
+ const body = await session.send("Fetch.getResponseBody", {
226
+ requestId: response.requestId,
227
+ });
228
+ await session.send("Fetch.fulfillRequest", toCdpDocumentFulfillParams(response, body, contentSecurityPolicy));
229
+ }
230
+ catch {
231
+ await session
232
+ .send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId })
233
+ .catch(() => undefined);
234
+ }
235
+ }
236
+ function getCdpAuthRequiredRequestId(params) {
237
+ if (!isRecord(params) || typeof params.requestId !== "string") {
238
+ return null;
239
+ }
240
+ return params.requestId;
241
+ }
242
+ function isCdpProxyAuthChallenge(params) {
243
+ return (isRecord(params) &&
244
+ isRecord(params.authChallenge) &&
245
+ params.authChallenge.source === "Proxy");
246
+ }
247
+ async function handleCdpAuthRequired(session, proxy, authAttempts, params) {
248
+ const requestId = getCdpAuthRequiredRequestId(params);
249
+ if (!requestId)
250
+ return;
251
+ // Chromium can emit another challenge after rejected credentials. Cancel a
252
+ // retry so incorrect credentials fail promptly instead of looping forever.
253
+ const response = isCdpProxyAuthChallenge(params) && !authAttempts.has(requestId)
254
+ ? (() => {
255
+ authAttempts.add(requestId);
256
+ return {
257
+ authChallengeResponse: {
258
+ password: proxy.password,
259
+ response: "ProvideCredentials",
260
+ username: proxy.username,
261
+ },
262
+ requestId,
263
+ };
264
+ })()
265
+ : {
266
+ authChallengeResponse: { response: "CancelAuth" },
267
+ requestId,
268
+ };
269
+ await session.send("Fetch.continueWithAuth", response).catch(() => undefined);
270
+ }
271
+ function createResourcePolicyHandler(policy, allowedMethods) {
272
+ return async (route) => {
273
+ const request = await toResourceRequest(route.request());
274
+ if (!request || !allowedMethods.has(request.method)) {
275
+ await abortResourceRoute(route);
276
+ return;
277
+ }
278
+ const decision = await decideResourceRequest(policy, request);
279
+ switch (decision.action) {
280
+ case "continue":
281
+ await route.continue();
282
+ return;
283
+ case "fulfill":
284
+ await fulfillResourceRoute(route, decision);
285
+ return;
286
+ case "block":
287
+ await abortResourceRoute(route);
288
+ }
289
+ };
290
+ }
107
291
  function getDefaultCdpPoolUrl(env = process.env) {
108
292
  return env.APIFUSE__CDP_POOL__URL;
109
293
  }
@@ -139,7 +323,7 @@ function formatExpression(fn) {
139
323
  }
140
324
  return `(${fn.toString()})()`;
141
325
  }
142
- function toLaunchOptions(options) {
326
+ function toLaunchOptions(options, proxy) {
143
327
  return {
144
328
  // `extraArgs` is optional, but playwright-extra's stealth evasions mutate
145
329
  // `options.args` unguarded (navigator.webdriver does
@@ -150,7 +334,49 @@ function toLaunchOptions(options) {
150
334
  args: options.extraArgs ?? [],
151
335
  executablePath: options.executablePath,
152
336
  headless: options.headless ?? true,
153
- proxy: options.proxy ? { server: options.proxy } : undefined,
337
+ proxy,
338
+ };
339
+ }
340
+ function hasProxyCredentials(proxy) {
341
+ return proxy?.username !== undefined && proxy.password !== undefined;
342
+ }
343
+ function toPlaywrightProxy(proxy) {
344
+ if (!proxy)
345
+ return undefined;
346
+ const schemeEnd = proxy.indexOf("://");
347
+ const authorityStart = schemeEnd >= 0 ? schemeEnd + 3 : 0;
348
+ const remainder = proxy.slice(authorityStart);
349
+ const authorityEnd = remainder.search(/[/?#]/);
350
+ const authority = remainder.slice(0, authorityEnd >= 0 ? authorityEnd : undefined);
351
+ if (!authority.includes("@")) {
352
+ return { server: proxy };
353
+ }
354
+ let parsed;
355
+ try {
356
+ parsed = new URL(proxy);
357
+ }
358
+ catch {
359
+ throw new ProviderError("Browser proxy URL is invalid", {
360
+ code: "BROWSER_PROXY_INVALID",
361
+ fix: "Use a valid proxy URL.",
362
+ });
363
+ }
364
+ let username;
365
+ let password;
366
+ try {
367
+ username = decodeURIComponent(parsed.username);
368
+ password = decodeURIComponent(parsed.password);
369
+ }
370
+ catch {
371
+ throw new ProviderError("Browser proxy credentials use invalid percent-encoding", {
372
+ code: "BROWSER_PROXY_INVALID",
373
+ fix: "Percent-encode the proxy username and password as URL userinfo.",
374
+ });
375
+ }
376
+ return {
377
+ server: `${parsed.protocol}//${parsed.host}`,
378
+ username,
379
+ password,
154
380
  };
155
381
  }
156
382
  class PlaywrightBrowserLocator {
@@ -315,10 +541,12 @@ async function loadSeleniumBase() {
315
541
  }
316
542
  class PlaywrightBrowserPage {
317
543
  page;
544
+ proxy;
318
545
  id = "main";
319
546
  pageId;
320
- constructor(page) {
547
+ constructor(page, proxy = undefined) {
321
548
  this.page = page;
549
+ this.proxy = proxy;
322
550
  }
323
551
  async goto(url) {
324
552
  await this.page.goto(url);
@@ -329,6 +557,9 @@ class PlaywrightBrowserPage {
329
557
  }
330
558
  return await this.page.evaluate(fn);
331
559
  }
560
+ async userAgent() {
561
+ return await this.evaluate("navigator.userAgent");
562
+ }
332
563
  async waitForSelector(selector, options) {
333
564
  await this.page.waitForSelector(selector, options);
334
565
  }
@@ -368,40 +599,102 @@ class PlaywrightBrowserPage {
368
599
  }
369
600
  async withResourcePolicy(policy, run) {
370
601
  const allowedMethods = new Set(policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS);
602
+ const policyHandler = createResourcePolicyHandler(policy, allowedMethods);
603
+ const context = this.page.context();
371
604
  const handler = async (route) => {
372
- const request = await toResourceRequest(route.request());
373
- if (!request || !allowedMethods.has(request.method)) {
374
- await route.abort("blockedbyclient");
605
+ try {
606
+ // Context routing sees a popup's first request before the late `page`
607
+ // event. Fail every secondary page closed so no redirect can escape
608
+ // the per-page CDP interception while that session is being attached.
609
+ if (route.request().frame().page() !== this.page) {
610
+ await abortResourceRoute(route);
611
+ return;
612
+ }
613
+ }
614
+ catch {
615
+ await abortResourceRoute(route);
375
616
  return;
376
617
  }
377
- for (const resourceRoute of policy.routes) {
378
- if (!matchesResourceRoute(resourceRoute.match, request)) {
379
- continue;
618
+ await policyHandler(route);
619
+ };
620
+ const sessions = new Set();
621
+ const pendingAttachments = new Set();
622
+ const proxy = hasProxyCredentials(this.proxy) ? this.proxy : undefined;
623
+ let active = true;
624
+ const attachPage = async (page) => {
625
+ const session = await context.newCDPSession(page);
626
+ if (!active) {
627
+ await session.detach();
628
+ return;
629
+ }
630
+ const onRequestPaused = (params) => {
631
+ if (isCdpResponseStage(params) && policy.documentContentSecurityPolicy) {
632
+ void handleCdpDocumentResponse(session, policy.documentContentSecurityPolicy, params);
633
+ return;
380
634
  }
381
- const decision = await resourceRoute.handle(request);
382
- switch (decision.action) {
383
- case "fulfill":
384
- await fulfillResourceRoute(route, decision);
385
- return;
386
- case "block":
387
- await route.abort("blockedbyclient");
388
- return;
635
+ void handleCdpResourceRequest(session, policy, allowedMethods, params);
636
+ };
637
+ const authAttempts = new Set();
638
+ const onAuthRequired = (params) => {
639
+ if (!proxy)
640
+ return;
641
+ void handleCdpAuthRequired(session, proxy, authAttempts, params);
642
+ };
643
+ session.on("Fetch.requestPaused", onRequestPaused);
644
+ if (proxy) {
645
+ session.on("Fetch.authRequired", onAuthRequired);
646
+ }
647
+ try {
648
+ await session.send("Fetch.enable", {
649
+ ...(proxy ? { handleAuthRequests: true } : {}),
650
+ patterns: resourcePolicyFetchPatterns(policy),
651
+ });
652
+ sessions.add(session);
653
+ }
654
+ catch (error) {
655
+ session.off("Fetch.requestPaused", onRequestPaused);
656
+ if (proxy) {
657
+ session.off("Fetch.authRequired", onAuthRequired);
389
658
  }
659
+ await session.detach().catch(() => undefined);
660
+ throw error;
390
661
  }
391
- await route.abort("blockedbyclient");
392
662
  };
393
- await this.page.route(RESOURCE_POLICY_ROUTE_PATTERN, handler);
663
+ const onPage = (page) => {
664
+ const attachment = attachPage(page)
665
+ .catch(async () => {
666
+ await page.close().catch(() => undefined);
667
+ })
668
+ .finally(() => pendingAttachments.delete(attachment));
669
+ pendingAttachments.add(attachment);
670
+ };
671
+ await context.route(RESOURCE_POLICY_ROUTE_PATTERN, handler);
394
672
  try {
395
- return await run();
673
+ context.on("page", onPage);
674
+ try {
675
+ await attachPage(this.page);
676
+ return await run();
677
+ }
678
+ finally {
679
+ context.off("page", onPage);
680
+ }
396
681
  }
397
682
  finally {
398
- await this.page.unroute(RESOURCE_POLICY_ROUTE_PATTERN, handler);
683
+ active = false;
684
+ await context.unroute(RESOURCE_POLICY_ROUTE_PATTERN, handler);
685
+ await Promise.allSettled(pendingAttachments);
686
+ await Promise.all([...sessions].map(async (session) => {
687
+ await session.send("Fetch.disable").catch(() => undefined);
688
+ await session.detach().catch(() => undefined);
689
+ }));
399
690
  }
400
691
  }
401
692
  }
402
693
  class PlaywrightBrowserClient {
403
694
  options;
404
695
  browser = null;
696
+ parsedProxy;
697
+ proxyParsed = false;
405
698
  engine = "playwright-stealth";
406
699
  constructor(options = {}) {
407
700
  this.options = options;
@@ -411,13 +704,17 @@ class PlaywrightBrowserClient {
411
704
  return this.browser;
412
705
  }
413
706
  const chromium = await loadChromiumLauncher(this.options);
414
- this.browser = await chromium.launch(toLaunchOptions(this.options));
707
+ if (!this.proxyParsed) {
708
+ this.parsedProxy = toPlaywrightProxy(this.options.proxy);
709
+ this.proxyParsed = true;
710
+ }
711
+ this.browser = await chromium.launch(toLaunchOptions(this.options, this.parsedProxy));
415
712
  return this.browser;
416
713
  }
417
714
  async newPage() {
418
715
  const browser = await this.ensureBrowser();
419
716
  const page = await browser.newPage();
420
- return new PlaywrightBrowserPage(page);
717
+ return new PlaywrightBrowserPage(page, this.parsedProxy);
421
718
  }
422
719
  async rawPage() {
423
720
  throw new ProviderError("ctx.browser.rawPage() requires a CDP pool", {
@@ -427,9 +724,9 @@ class PlaywrightBrowserClient {
427
724
  }
428
725
  async withIsolatedContext(handler) {
429
726
  const browser = await this.ensureBrowser();
430
- const context = await browser.newContext();
727
+ const context = await browser.newContext({ serviceWorkers: this.options.serviceWorkers });
431
728
  const page = await context.newPage();
432
- const browserPage = new PlaywrightBrowserPage(page);
729
+ const browserPage = new PlaywrightBrowserPage(page, this.parsedProxy);
433
730
  try {
434
731
  return await handler(browserPage);
435
732
  }
@@ -823,6 +1120,9 @@ class CdpPoolBrowserPage {
823
1120
  await this.initialize();
824
1121
  return await this.evaluateWithContext(fn);
825
1122
  }
1123
+ async userAgent() {
1124
+ return await this.evaluate("navigator.userAgent");
1125
+ }
826
1126
  async evaluateInFrame(frameId, fn) {
827
1127
  await this.initialize();
828
1128
  const contextId = await this.getFrameExecutionContextId(frameId);
@@ -966,7 +1266,7 @@ class CdpPoolBrowserPage {
966
1266
  const unsubscribe = this.pageClient.on("Fetch.requestPaused", handlePausedRequest);
967
1267
  try {
968
1268
  await this.pageClient.send("Fetch.enable", {
969
- patterns: [{ requestStage: "Request", urlPattern: "*" }],
1269
+ patterns: resourcePolicyFetchPatterns(policy),
970
1270
  });
971
1271
  }
972
1272
  catch (error) {
@@ -991,6 +1291,24 @@ class CdpPoolBrowserPage {
991
1291
  return;
992
1292
  }
993
1293
  try {
1294
+ if (isCdpResponseStage(params) && policy.documentContentSecurityPolicy) {
1295
+ const response = toCdpPausedDocumentResponse(params);
1296
+ if (!response)
1297
+ throw new Error("CDP document response metadata is unavailable");
1298
+ if (!response.hasBody) {
1299
+ await this.pageClient.send("Fetch.continueResponse", {
1300
+ requestId,
1301
+ responseCode: response.responseCode,
1302
+ responseHeaders: [...response.responseHeaders],
1303
+ });
1304
+ return;
1305
+ }
1306
+ const body = await this.pageClient.send("Fetch.getResponseBody", {
1307
+ requestId: response.requestId,
1308
+ });
1309
+ await this.pageClient.send("Fetch.fulfillRequest", toCdpDocumentFulfillParams(response, body, policy.documentContentSecurityPolicy));
1310
+ return;
1311
+ }
994
1312
  const parsed = toCdpResourceRequest(params);
995
1313
  if (!parsed || !allowedMethods.has(parsed.request.method)) {
996
1314
  await this.failCdpResourceRequest(requestId);
@@ -1002,6 +1320,11 @@ class CdpPoolBrowserPage {
1002
1320
  }
1003
1321
  const decision = await resourceRoute.handle(parsed.request);
1004
1322
  switch (decision.action) {
1323
+ case "continue":
1324
+ await this.pageClient.send("Fetch.continueRequest", {
1325
+ requestId: parsed.requestId,
1326
+ });
1327
+ return;
1005
1328
  case "fulfill":
1006
1329
  await this.pageClient.send("Fetch.fulfillRequest", toCdpFulfillParams(parsed.requestId, decision));
1007
1330
  return;
@@ -1,6 +1,5 @@
1
1
  import type { CredentialContext, EnvContext, ProviderChoiceConsumeMode, ProviderChoiceContext, ProviderRequestContext, ProviderRuntimeState } from "../types.js";
2
2
  export declare const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
3
- export declare const PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_WORD_ISSUANCE";
4
3
  export type ProviderChoiceTelemetryEvent = {
5
4
  readonly providerId: string;
6
5
  readonly purpose: string;