@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
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
- import type { Frame, LaunchOptions, Locator, Page, Request, Route } from "playwright";
2
+ import type { CDPSession, Frame, LaunchOptions, Locator, Page, Request, Route } from "playwright";
3
3
 
4
4
  import { ProviderError } from "../errors.js";
5
5
  import type {
@@ -87,13 +87,28 @@ type CdpFrameTreeNode = {
87
87
  type CdpFetchFulfillParams = {
88
88
  readonly requestId: string;
89
89
  readonly responseCode: number;
90
- readonly responseHeaders?: readonly {
90
+ readonly responseHeaders?: {
91
91
  readonly name: string;
92
92
  readonly value: string;
93
93
  }[];
94
94
  readonly body?: string;
95
95
  };
96
96
 
97
+ type CdpPausedDocumentResponse = {
98
+ readonly hasBody: boolean;
99
+ readonly requestId: string;
100
+ readonly responseCode: number;
101
+ readonly responseHeaders: readonly {
102
+ readonly name: string;
103
+ readonly value: string;
104
+ }[];
105
+ };
106
+
107
+ type CdpResponseBody = {
108
+ readonly base64Encoded?: boolean;
109
+ readonly body?: string;
110
+ };
111
+
97
112
  type BrowserPageContract = BrowserPage;
98
113
 
99
114
  function toResourceBody(body: BrowserResourceBody | undefined): Buffer | string | undefined {
@@ -109,7 +124,7 @@ function toResourceBody(body: BrowserResourceBody | undefined): Buffer | string
109
124
  }
110
125
 
111
126
  function isResourceMethod(method: string): method is BrowserResourceMethod {
112
- return method === "GET" || method === "HEAD";
127
+ return method === "GET" || method === "HEAD" || method === "POST";
113
128
  }
114
129
 
115
130
  async function toResourceRequest(request: Request): Promise<BrowserResourceRequest | null> {
@@ -164,6 +179,85 @@ function getCdpPausedRequestId(params: unknown): string | null {
164
179
  return params.requestId;
165
180
  }
166
181
 
182
+ function isCdpResponseStage(params: unknown): boolean {
183
+ return (
184
+ isRecord(params) &&
185
+ (typeof params.responseStatusCode === "number" ||
186
+ typeof params.responseErrorReason === "string")
187
+ );
188
+ }
189
+
190
+ function toCdpPausedDocumentResponse(params: unknown): CdpPausedDocumentResponse | null {
191
+ if (
192
+ !isRecord(params) ||
193
+ typeof params.requestId !== "string" ||
194
+ typeof params.responseStatusCode !== "number" ||
195
+ params.resourceType !== "Document" ||
196
+ !isRecord(params.request)
197
+ ) {
198
+ return null;
199
+ }
200
+
201
+ const responseCode = params.responseStatusCode;
202
+ const responseHeaders = Array.isArray(params.responseHeaders)
203
+ ? params.responseHeaders.flatMap((header) => {
204
+ if (
205
+ !isRecord(header) ||
206
+ typeof header.name !== "string" ||
207
+ typeof header.value !== "string"
208
+ ) {
209
+ return [];
210
+ }
211
+ return [{ name: header.name, value: header.value }];
212
+ })
213
+ : [];
214
+
215
+ return {
216
+ hasBody:
217
+ String(params.request.method ?? "").toUpperCase() !== "HEAD" &&
218
+ responseCode >= 200 &&
219
+ (responseCode < 300 || responseCode >= 400) &&
220
+ responseCode !== 204 &&
221
+ responseCode !== 205 &&
222
+ responseCode !== 304,
223
+ requestId: params.requestId,
224
+ responseCode,
225
+ responseHeaders,
226
+ };
227
+ }
228
+
229
+ function toCdpDocumentFulfillParams(
230
+ response: CdpPausedDocumentResponse,
231
+ body: CdpResponseBody,
232
+ contentSecurityPolicy: string,
233
+ ): CdpFetchFulfillParams {
234
+ const responseBody = typeof body.body === "string" ? body.body : "";
235
+ return {
236
+ body: body.base64Encoded ? responseBody : Buffer.from(responseBody).toString("base64"),
237
+ requestId: response.requestId,
238
+ responseCode: response.responseCode,
239
+ responseHeaders: [
240
+ ...response.responseHeaders,
241
+ { name: "Content-Security-Policy", value: contentSecurityPolicy },
242
+ ],
243
+ };
244
+ }
245
+
246
+ function resourcePolicyFetchPatterns(policy: BrowserResourcePolicy) {
247
+ return [
248
+ { requestStage: "Request" as const, urlPattern: "*" },
249
+ ...(policy.documentContentSecurityPolicy
250
+ ? [
251
+ {
252
+ requestStage: "Response" as const,
253
+ resourceType: "Document" as const,
254
+ urlPattern: "*",
255
+ },
256
+ ]
257
+ : []),
258
+ ];
259
+ }
260
+
167
261
  function toCdpResourceHeaders(value: unknown): Record<string, string> {
168
262
  if (!isRecord(value)) {
169
263
  return {};
@@ -225,11 +319,171 @@ async function fulfillResourceRoute(
225
319
  });
226
320
  }
227
321
 
322
+ async function decideResourceRequest(
323
+ policy: BrowserResourcePolicy,
324
+ request: BrowserResourceRequest,
325
+ ): Promise<BrowserResourceDecision> {
326
+ for (const resourceRoute of policy.routes) {
327
+ if (matchesResourceRoute(resourceRoute.match, request)) {
328
+ return await resourceRoute.handle(request);
329
+ }
330
+ }
331
+
332
+ return { action: "block" };
333
+ }
334
+
335
+ async function abortResourceRoute(route: Route): Promise<void> {
336
+ await route.abort("blockedbyclient");
337
+ }
338
+
339
+ async function handleCdpResourceRequest(
340
+ session: CDPSession,
341
+ policy: BrowserResourcePolicy,
342
+ allowedMethods: ReadonlySet<BrowserResourceMethod>,
343
+ params: unknown,
344
+ ): Promise<void> {
345
+ const paused = toCdpResourceRequest(params);
346
+ const requestId = paused?.requestId ?? getCdpPausedRequestId(params);
347
+ if (!requestId) return;
348
+
349
+ try {
350
+ if (!paused || !allowedMethods.has(paused.request.method)) {
351
+ await session.send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId });
352
+ return;
353
+ }
354
+
355
+ const decision = await decideResourceRequest(policy, paused.request);
356
+ switch (decision.action) {
357
+ case "continue":
358
+ await session.send("Fetch.continueRequest", { requestId });
359
+ return;
360
+ case "fulfill":
361
+ await session.send("Fetch.fulfillRequest", toCdpFulfillParams(requestId, decision));
362
+ return;
363
+ case "block":
364
+ await session.send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId });
365
+ return;
366
+ }
367
+ } catch {
368
+ await session
369
+ .send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId })
370
+ .catch(() => undefined);
371
+ }
372
+ }
373
+
374
+ async function handleCdpDocumentResponse(
375
+ session: CDPSession,
376
+ contentSecurityPolicy: string,
377
+ params: unknown,
378
+ ): Promise<void> {
379
+ const requestId = getCdpPausedRequestId(params);
380
+ if (!requestId) return;
381
+
382
+ try {
383
+ const response = toCdpPausedDocumentResponse(params);
384
+ if (!response) throw new Error("CDP document response metadata is unavailable");
385
+ if (!response.hasBody) {
386
+ await session.send("Fetch.continueResponse", {
387
+ requestId,
388
+ responseCode: response.responseCode,
389
+ responseHeaders: [...response.responseHeaders],
390
+ });
391
+ return;
392
+ }
393
+
394
+ const body = await session.send("Fetch.getResponseBody", {
395
+ requestId: response.requestId,
396
+ });
397
+ await session.send(
398
+ "Fetch.fulfillRequest",
399
+ toCdpDocumentFulfillParams(response, body, contentSecurityPolicy),
400
+ );
401
+ } catch {
402
+ await session
403
+ .send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId })
404
+ .catch(() => undefined);
405
+ }
406
+ }
407
+
408
+ function getCdpAuthRequiredRequestId(params: unknown): string | null {
409
+ if (!isRecord(params) || typeof params.requestId !== "string") {
410
+ return null;
411
+ }
412
+
413
+ return params.requestId;
414
+ }
415
+
416
+ function isCdpProxyAuthChallenge(params: unknown): boolean {
417
+ return (
418
+ isRecord(params) &&
419
+ isRecord(params.authChallenge) &&
420
+ params.authChallenge.source === "Proxy"
421
+ );
422
+ }
423
+
424
+ async function handleCdpAuthRequired(
425
+ session: CDPSession,
426
+ proxy: PlaywrightProxy & { username: string; password: string },
427
+ authAttempts: Set<string>,
428
+ params: unknown,
429
+ ): Promise<void> {
430
+ const requestId = getCdpAuthRequiredRequestId(params);
431
+ if (!requestId) return;
432
+
433
+ // Chromium can emit another challenge after rejected credentials. Cancel a
434
+ // retry so incorrect credentials fail promptly instead of looping forever.
435
+ const response =
436
+ isCdpProxyAuthChallenge(params) && !authAttempts.has(requestId)
437
+ ? (() => {
438
+ authAttempts.add(requestId);
439
+ return {
440
+ authChallengeResponse: {
441
+ password: proxy.password,
442
+ response: "ProvideCredentials" as const,
443
+ username: proxy.username,
444
+ },
445
+ requestId,
446
+ };
447
+ })()
448
+ : {
449
+ authChallengeResponse: { response: "CancelAuth" as const },
450
+ requestId,
451
+ };
452
+
453
+ await session.send("Fetch.continueWithAuth", response).catch(() => undefined);
454
+ }
455
+
456
+ function createResourcePolicyHandler(
457
+ policy: BrowserResourcePolicy,
458
+ allowedMethods: ReadonlySet<BrowserResourceMethod>,
459
+ ): (route: Route) => Promise<void> {
460
+ return async (route: Route): Promise<void> => {
461
+ const request = await toResourceRequest(route.request());
462
+ if (!request || !allowedMethods.has(request.method)) {
463
+ await abortResourceRoute(route);
464
+ return;
465
+ }
466
+
467
+ const decision = await decideResourceRequest(policy, request);
468
+ switch (decision.action) {
469
+ case "continue":
470
+ await route.continue();
471
+ return;
472
+ case "fulfill":
473
+ await fulfillResourceRoute(route, decision);
474
+ return;
475
+ case "block":
476
+ await abortResourceRoute(route);
477
+ }
478
+ };
479
+ }
480
+
228
481
  export type BrowserClientOptions = BrowserOptions & {
229
482
  allowedHosts?: string[];
230
483
  cdpUrl?: string;
231
484
  executablePath?: string;
232
485
  extraArgs?: string[];
486
+ serviceWorkers?: "allow" | "block";
233
487
  };
234
488
 
235
489
  type SupportedBrowserClient = {
@@ -288,7 +542,10 @@ function formatExpression<T>(fn: string | (() => T)): string {
288
542
  return `(${fn.toString()})()`;
289
543
  }
290
544
 
291
- function toLaunchOptions(options: BrowserClientOptions): LaunchOptions {
545
+ function toLaunchOptions(
546
+ options: BrowserClientOptions,
547
+ proxy: LaunchOptions["proxy"],
548
+ ): LaunchOptions {
292
549
  return {
293
550
  // `extraArgs` is optional, but playwright-extra's stealth evasions mutate
294
551
  // `options.args` unguarded (navigator.webdriver does
@@ -299,7 +556,56 @@ function toLaunchOptions(options: BrowserClientOptions): LaunchOptions {
299
556
  args: options.extraArgs ?? [],
300
557
  executablePath: options.executablePath,
301
558
  headless: options.headless ?? true,
302
- proxy: options.proxy ? { server: options.proxy } : undefined,
559
+ proxy,
560
+ };
561
+ }
562
+
563
+ type PlaywrightProxy = NonNullable<LaunchOptions["proxy"]>;
564
+
565
+ function hasProxyCredentials(
566
+ proxy: LaunchOptions["proxy"] | undefined,
567
+ ): proxy is PlaywrightProxy & { username: string; password: string } {
568
+ return proxy?.username !== undefined && proxy.password !== undefined;
569
+ }
570
+
571
+ function toPlaywrightProxy(proxy: string | undefined): LaunchOptions["proxy"] {
572
+ if (!proxy) return undefined;
573
+
574
+ const schemeEnd = proxy.indexOf("://");
575
+ const authorityStart = schemeEnd >= 0 ? schemeEnd + 3 : 0;
576
+ const remainder = proxy.slice(authorityStart);
577
+ const authorityEnd = remainder.search(/[/?#]/);
578
+ const authority = remainder.slice(0, authorityEnd >= 0 ? authorityEnd : undefined);
579
+ if (!authority.includes("@")) {
580
+ return { server: proxy };
581
+ }
582
+
583
+ let parsed: URL;
584
+ try {
585
+ parsed = new URL(proxy);
586
+ } catch {
587
+ throw new ProviderError("Browser proxy URL is invalid", {
588
+ code: "BROWSER_PROXY_INVALID",
589
+ fix: "Use a valid proxy URL.",
590
+ });
591
+ }
592
+
593
+ let username: string;
594
+ let password: string;
595
+ try {
596
+ username = decodeURIComponent(parsed.username);
597
+ password = decodeURIComponent(parsed.password);
598
+ } catch {
599
+ throw new ProviderError("Browser proxy credentials use invalid percent-encoding", {
600
+ code: "BROWSER_PROXY_INVALID",
601
+ fix: "Percent-encode the proxy username and password as URL userinfo.",
602
+ });
603
+ }
604
+
605
+ return {
606
+ server: `${parsed.protocol}//${parsed.host}`,
607
+ username,
608
+ password,
303
609
  };
304
610
  }
305
611
 
@@ -493,7 +799,10 @@ class PlaywrightBrowserPage implements BrowserPageContract {
493
799
  readonly id = "main";
494
800
  readonly pageId?: string;
495
801
 
496
- constructor(private readonly page: Page) {}
802
+ constructor(
803
+ private readonly page: Page,
804
+ private readonly proxy: LaunchOptions["proxy"] = undefined,
805
+ ) {}
497
806
 
498
807
  async goto(url: string): Promise<void> {
499
808
  await this.page.goto(url);
@@ -507,6 +816,10 @@ class PlaywrightBrowserPage implements BrowserPageContract {
507
816
  return await this.page.evaluate(fn);
508
817
  }
509
818
 
819
+ async userAgent(): Promise<string> {
820
+ return await this.evaluate<string>("navigator.userAgent");
821
+ }
822
+
510
823
  async waitForSelector(selector: string, options?: { timeout?: number }): Promise<void> {
511
824
  await this.page.waitForSelector(selector, options);
512
825
  }
@@ -558,43 +871,102 @@ class PlaywrightBrowserPage implements BrowserPageContract {
558
871
 
559
872
  async withResourcePolicy<T>(policy: BrowserResourcePolicy, run: () => Promise<T>): Promise<T> {
560
873
  const allowedMethods = new Set(policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS);
874
+ const policyHandler = createResourcePolicyHandler(policy, allowedMethods);
875
+ const context = this.page.context();
561
876
  const handler = async (route: Route): Promise<void> => {
562
- const request = await toResourceRequest(route.request());
563
- if (!request || !allowedMethods.has(request.method)) {
564
- await route.abort("blockedbyclient");
877
+ try {
878
+ // Context routing sees a popup's first request before the late `page`
879
+ // event. Fail every secondary page closed so no redirect can escape
880
+ // the per-page CDP interception while that session is being attached.
881
+ if (route.request().frame().page() !== this.page) {
882
+ await abortResourceRoute(route);
883
+ return;
884
+ }
885
+ } catch {
886
+ await abortResourceRoute(route);
565
887
  return;
566
888
  }
567
889
 
568
- for (const resourceRoute of policy.routes) {
569
- if (!matchesResourceRoute(resourceRoute.match, request)) {
570
- continue;
571
- }
890
+ await policyHandler(route);
891
+ };
892
+ const sessions = new Set<CDPSession>();
893
+ const pendingAttachments = new Set<Promise<void>>();
894
+ const proxy = hasProxyCredentials(this.proxy) ? this.proxy : undefined;
895
+ let active = true;
896
+ const attachPage = async (page: Page): Promise<void> => {
897
+ const session = await context.newCDPSession(page);
898
+ if (!active) {
899
+ await session.detach();
900
+ return;
901
+ }
572
902
 
573
- const decision = await resourceRoute.handle(request);
574
- switch (decision.action) {
575
- case "fulfill":
576
- await fulfillResourceRoute(route, decision);
577
- return;
578
- case "block":
579
- await route.abort("blockedbyclient");
580
- return;
903
+ const onRequestPaused = (params: unknown) => {
904
+ if (isCdpResponseStage(params) && policy.documentContentSecurityPolicy) {
905
+ void handleCdpDocumentResponse(session, policy.documentContentSecurityPolicy, params);
906
+ return;
581
907
  }
908
+ void handleCdpResourceRequest(session, policy, allowedMethods, params);
909
+ };
910
+ const authAttempts = new Set<string>();
911
+ const onAuthRequired = (params: unknown) => {
912
+ if (!proxy) return;
913
+ void handleCdpAuthRequired(session, proxy, authAttempts, params);
914
+ };
915
+ session.on("Fetch.requestPaused", onRequestPaused);
916
+ if (proxy) {
917
+ session.on("Fetch.authRequired", onAuthRequired);
582
918
  }
583
-
584
- await route.abort("blockedbyclient");
919
+ try {
920
+ await session.send("Fetch.enable", {
921
+ ...(proxy ? { handleAuthRequests: true } : {}),
922
+ patterns: resourcePolicyFetchPatterns(policy),
923
+ });
924
+ sessions.add(session);
925
+ } catch (error) {
926
+ session.off("Fetch.requestPaused", onRequestPaused);
927
+ if (proxy) {
928
+ session.off("Fetch.authRequired", onAuthRequired);
929
+ }
930
+ await session.detach().catch(() => undefined);
931
+ throw error;
932
+ }
933
+ };
934
+ const onPage = (page: Page) => {
935
+ const attachment = attachPage(page)
936
+ .catch(async () => {
937
+ await page.close().catch(() => undefined);
938
+ })
939
+ .finally(() => pendingAttachments.delete(attachment));
940
+ pendingAttachments.add(attachment);
585
941
  };
586
942
 
587
- await this.page.route(RESOURCE_POLICY_ROUTE_PATTERN, handler);
943
+ await context.route(RESOURCE_POLICY_ROUTE_PATTERN, handler);
588
944
  try {
589
- return await run();
945
+ context.on("page", onPage);
946
+ try {
947
+ await attachPage(this.page);
948
+ return await run();
949
+ } finally {
950
+ context.off("page", onPage);
951
+ }
590
952
  } finally {
591
- await this.page.unroute(RESOURCE_POLICY_ROUTE_PATTERN, handler);
953
+ active = false;
954
+ await context.unroute(RESOURCE_POLICY_ROUTE_PATTERN, handler);
955
+ await Promise.allSettled(pendingAttachments);
956
+ await Promise.all(
957
+ [...sessions].map(async (session) => {
958
+ await session.send("Fetch.disable").catch(() => undefined);
959
+ await session.detach().catch(() => undefined);
960
+ }),
961
+ );
592
962
  }
593
963
  }
594
964
  }
595
965
 
596
966
  class PlaywrightBrowserClient implements SupportedBrowserClient {
597
967
  private browser: import("playwright").Browser | null = null;
968
+ private parsedProxy: LaunchOptions["proxy"] | undefined;
969
+ private proxyParsed = false;
598
970
  readonly engine = "playwright-stealth" satisfies BrowserEngine;
599
971
 
600
972
  constructor(private readonly options: BrowserClientOptions = {}) {}
@@ -605,7 +977,11 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
605
977
  }
606
978
 
607
979
  const chromium = await loadChromiumLauncher(this.options);
608
- this.browser = await chromium.launch(toLaunchOptions(this.options));
980
+ if (!this.proxyParsed) {
981
+ this.parsedProxy = toPlaywrightProxy(this.options.proxy);
982
+ this.proxyParsed = true;
983
+ }
984
+ this.browser = await chromium.launch(toLaunchOptions(this.options, this.parsedProxy));
609
985
  return this.browser;
610
986
  }
611
987
 
@@ -613,7 +989,7 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
613
989
  const browser = await this.ensureBrowser();
614
990
  const page = await browser.newPage();
615
991
 
616
- return new PlaywrightBrowserPage(page);
992
+ return new PlaywrightBrowserPage(page, this.parsedProxy);
617
993
  }
618
994
 
619
995
  async rawPage(): Promise<BrowserPageContract> {
@@ -625,9 +1001,9 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
625
1001
 
626
1002
  async withIsolatedContext<T>(handler: (page: BrowserPageContract) => Promise<T>): Promise<T> {
627
1003
  const browser = await this.ensureBrowser();
628
- const context = await browser.newContext();
1004
+ const context = await browser.newContext({ serviceWorkers: this.options.serviceWorkers });
629
1005
  const page = await context.newPage();
630
- const browserPage = new PlaywrightBrowserPage(page);
1006
+ const browserPage = new PlaywrightBrowserPage(page, this.parsedProxy);
631
1007
 
632
1008
  try {
633
1009
  return await handler(browserPage);
@@ -1137,6 +1513,10 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1137
1513
  return await this.evaluateWithContext<T>(fn);
1138
1514
  }
1139
1515
 
1516
+ async userAgent(): Promise<string> {
1517
+ return await this.evaluate<string>("navigator.userAgent");
1518
+ }
1519
+
1140
1520
  async evaluateInFrame<T>(frameId: string, fn: string | (() => T)): Promise<T> {
1141
1521
  await this.initialize();
1142
1522
  const contextId = await this.getFrameExecutionContextId(frameId);
@@ -1329,7 +1709,7 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1329
1709
 
1330
1710
  try {
1331
1711
  await this.pageClient.send("Fetch.enable", {
1332
- patterns: [{ requestStage: "Request", urlPattern: "*" }],
1712
+ patterns: resourcePolicyFetchPatterns(policy),
1333
1713
  });
1334
1714
  } catch (error) {
1335
1715
  unsubscribe();
@@ -1362,6 +1742,28 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1362
1742
  }
1363
1743
 
1364
1744
  try {
1745
+ if (isCdpResponseStage(params) && policy.documentContentSecurityPolicy) {
1746
+ const response = toCdpPausedDocumentResponse(params);
1747
+ if (!response) throw new Error("CDP document response metadata is unavailable");
1748
+ if (!response.hasBody) {
1749
+ await this.pageClient.send("Fetch.continueResponse", {
1750
+ requestId,
1751
+ responseCode: response.responseCode,
1752
+ responseHeaders: [...response.responseHeaders],
1753
+ });
1754
+ return;
1755
+ }
1756
+
1757
+ const body = await this.pageClient.send("Fetch.getResponseBody", {
1758
+ requestId: response.requestId,
1759
+ });
1760
+ await this.pageClient.send(
1761
+ "Fetch.fulfillRequest",
1762
+ toCdpDocumentFulfillParams(response, body, policy.documentContentSecurityPolicy),
1763
+ );
1764
+ return;
1765
+ }
1766
+
1365
1767
  const parsed = toCdpResourceRequest(params);
1366
1768
  if (!parsed || !allowedMethods.has(parsed.request.method)) {
1367
1769
  await this.failCdpResourceRequest(requestId);
@@ -1375,6 +1777,11 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1375
1777
 
1376
1778
  const decision = await resourceRoute.handle(parsed.request);
1377
1779
  switch (decision.action) {
1780
+ case "continue":
1781
+ await this.pageClient.send("Fetch.continueRequest", {
1782
+ requestId: parsed.requestId,
1783
+ });
1784
+ return;
1378
1785
  case "fulfill":
1379
1786
  await this.pageClient.send(
1380
1787
  "Fetch.fulfillRequest",