@wordbricks/playwright-mcp 0.1.22 → 0.1.24

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 (83) hide show
  1. package/package.json +6 -6
  2. package/lib/browserContextFactory.js +0 -399
  3. package/lib/browserServerBackend.js +0 -86
  4. package/lib/config.js +0 -300
  5. package/lib/context.js +0 -311
  6. package/lib/extension/cdpRelay.js +0 -352
  7. package/lib/extension/extensionContextFactory.js +0 -56
  8. package/lib/frameworkPatterns.js +0 -35
  9. package/lib/hooks/antiBotDetectionHook.js +0 -178
  10. package/lib/hooks/core.js +0 -145
  11. package/lib/hooks/eventConsumer.js +0 -52
  12. package/lib/hooks/events.js +0 -42
  13. package/lib/hooks/formatToolCallEvent.js +0 -12
  14. package/lib/hooks/frameworkStateHook.js +0 -182
  15. package/lib/hooks/grouping.js +0 -72
  16. package/lib/hooks/jsonLdDetectionHook.js +0 -182
  17. package/lib/hooks/networkFilters.js +0 -82
  18. package/lib/hooks/networkSetup.js +0 -61
  19. package/lib/hooks/networkTrackingHook.js +0 -67
  20. package/lib/hooks/pageHeightHook.js +0 -75
  21. package/lib/hooks/registry.js +0 -41
  22. package/lib/hooks/requireTabHook.js +0 -26
  23. package/lib/hooks/schema.js +0 -89
  24. package/lib/hooks/waitHook.js +0 -33
  25. package/lib/index.js +0 -41
  26. package/lib/mcp/inProcessTransport.js +0 -71
  27. package/lib/mcp/proxyBackend.js +0 -130
  28. package/lib/mcp/server.js +0 -91
  29. package/lib/mcp/tool.js +0 -44
  30. package/lib/mcp/transport.js +0 -188
  31. package/lib/playwrightTransformer.js +0 -520
  32. package/lib/program.js +0 -112
  33. package/lib/response.js +0 -192
  34. package/lib/sessionLog.js +0 -123
  35. package/lib/tab.js +0 -251
  36. package/lib/tools/common.js +0 -55
  37. package/lib/tools/console.js +0 -33
  38. package/lib/tools/dialogs.js +0 -50
  39. package/lib/tools/evaluate.js +0 -62
  40. package/lib/tools/extractFrameworkState.js +0 -225
  41. package/lib/tools/files.js +0 -48
  42. package/lib/tools/form.js +0 -66
  43. package/lib/tools/getSnapshot.js +0 -36
  44. package/lib/tools/getVisibleHtml.js +0 -68
  45. package/lib/tools/install.js +0 -51
  46. package/lib/tools/keyboard.js +0 -83
  47. package/lib/tools/mouse.js +0 -97
  48. package/lib/tools/navigate.js +0 -66
  49. package/lib/tools/network.js +0 -121
  50. package/lib/tools/networkDetail.js +0 -238
  51. package/lib/tools/networkSearch/bodySearch.js +0 -161
  52. package/lib/tools/networkSearch/grouping.js +0 -37
  53. package/lib/tools/networkSearch/helpers.js +0 -32
  54. package/lib/tools/networkSearch/searchHtml.js +0 -76
  55. package/lib/tools/networkSearch/types.js +0 -1
  56. package/lib/tools/networkSearch/urlSearch.js +0 -124
  57. package/lib/tools/networkSearch.js +0 -278
  58. package/lib/tools/pdf.js +0 -41
  59. package/lib/tools/repl.js +0 -414
  60. package/lib/tools/screenshot.js +0 -103
  61. package/lib/tools/scroll.js +0 -131
  62. package/lib/tools/snapshot.js +0 -161
  63. package/lib/tools/tabs.js +0 -62
  64. package/lib/tools/tool.js +0 -35
  65. package/lib/tools/utils.js +0 -78
  66. package/lib/tools/wait.js +0 -60
  67. package/lib/tools.js +0 -68
  68. package/lib/utils/adBlockFilter.js +0 -90
  69. package/lib/utils/codegen.js +0 -55
  70. package/lib/utils/extensionPath.js +0 -10
  71. package/lib/utils/fileUtils.js +0 -40
  72. package/lib/utils/graphql.js +0 -269
  73. package/lib/utils/guid.js +0 -22
  74. package/lib/utils/httpServer.js +0 -39
  75. package/lib/utils/log.js +0 -21
  76. package/lib/utils/manualPromise.js +0 -111
  77. package/lib/utils/networkFormat.js +0 -14
  78. package/lib/utils/package.js +0 -20
  79. package/lib/utils/result.js +0 -2
  80. package/lib/utils/sanitizeHtml.js +0 -130
  81. package/lib/utils/truncate.js +0 -103
  82. package/lib/utils/withTimeout.js +0 -7
  83. package/src/index.ts +0 -60
@@ -1,352 +0,0 @@
1
- /**
2
- * Copyright (c) Microsoft Corporation.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
- /**
17
- * WebSocket server that bridges Playwright MCP and Chrome Extension
18
- *
19
- * Endpoints:
20
- * - /cdp/guid - Full CDP interface for Playwright MCP
21
- * - /extension/guid - Extension connection for chrome.debugger forwarding
22
- */
23
- import { spawn } from "child_process";
24
- import debug from "debug";
25
- import { WebSocket, WebSocketServer } from "ws";
26
- import { httpAddressToString } from "../utils/httpServer.js";
27
- import { logUnhandledError } from "../utils/log.js";
28
- import { ManualPromise } from "../utils/manualPromise.js";
29
- // @ts-expect-error
30
- const { registry } = await import("playwright-core/lib/server/registry/index");
31
- const debugLogger = debug("pw:mcp:relay");
32
- export class CDPRelayServer {
33
- _wsHost;
34
- _browserChannel;
35
- _userDataDir;
36
- _cdpPath;
37
- _extensionPath;
38
- _wss;
39
- _playwrightConnection = null;
40
- _extensionConnection = null;
41
- _connectedTabInfo;
42
- _nextSessionId = 1;
43
- _extensionConnectionPromise;
44
- constructor(server, browserChannel, userDataDir) {
45
- this._wsHost = httpAddressToString(server.address()).replace(/^http/, "ws");
46
- this._browserChannel = browserChannel;
47
- this._userDataDir = userDataDir;
48
- const uuid = crypto.randomUUID();
49
- this._cdpPath = `/cdp/${uuid}`;
50
- this._extensionPath = `/extension/${uuid}`;
51
- this._resetExtensionConnection();
52
- this._wss = new WebSocketServer({ server });
53
- this._wss.on("connection", this._onConnection.bind(this));
54
- }
55
- cdpEndpoint() {
56
- return `${this._wsHost}${this._cdpPath}`;
57
- }
58
- extensionEndpoint() {
59
- return `${this._wsHost}${this._extensionPath}`;
60
- }
61
- async ensureExtensionConnectionForMCPContext(clientInfo, abortSignal) {
62
- debugLogger("Ensuring extension connection for MCP context");
63
- if (this._extensionConnection)
64
- return;
65
- this._connectBrowser(clientInfo);
66
- debugLogger("Waiting for incoming extension connection");
67
- await Promise.race([
68
- this._extensionConnectionPromise,
69
- new Promise((_, reject) => abortSignal.addEventListener("abort", reject)),
70
- ]);
71
- debugLogger("Extension connection established");
72
- }
73
- _connectBrowser(clientInfo) {
74
- const mcpRelayEndpoint = `${this._wsHost}${this._extensionPath}`;
75
- // Need to specify "key" in the manifest.json to make the id stable when loading from file.
76
- const url = new URL("chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html");
77
- url.searchParams.set("mcpRelayUrl", mcpRelayEndpoint);
78
- url.searchParams.set("client", JSON.stringify(clientInfo));
79
- const href = url.toString();
80
- const executableInfo = registry.findExecutable(this._browserChannel);
81
- if (!executableInfo)
82
- throw new Error(`Unsupported channel: "${this._browserChannel}"`);
83
- const executablePath = executableInfo.executablePath();
84
- if (!executablePath)
85
- throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`);
86
- const args = [];
87
- if (this._userDataDir)
88
- args.push(`--user-data-dir=${this._userDataDir}`);
89
- args.push(href);
90
- spawn(executablePath, args, {
91
- windowsHide: true,
92
- detached: true,
93
- shell: false,
94
- stdio: "ignore",
95
- });
96
- }
97
- stop() {
98
- this.closeConnections("Server stopped");
99
- this._wss.close();
100
- }
101
- closeConnections(reason) {
102
- this._closePlaywrightConnection(reason);
103
- this._closeExtensionConnection(reason);
104
- }
105
- _onConnection(ws, request) {
106
- const url = new URL(`http://localhost${request.url}`);
107
- debugLogger(`New connection to ${url.pathname}`);
108
- if (url.pathname === this._cdpPath) {
109
- this._handlePlaywrightConnection(ws);
110
- }
111
- else if (url.pathname === this._extensionPath) {
112
- this._handleExtensionConnection(ws);
113
- }
114
- else {
115
- debugLogger(`Invalid path: ${url.pathname}`);
116
- ws.close(4004, "Invalid path");
117
- }
118
- }
119
- _handlePlaywrightConnection(ws) {
120
- if (this._playwrightConnection) {
121
- debugLogger("Rejecting second Playwright connection");
122
- ws.close(1000, "Another CDP client already connected");
123
- return;
124
- }
125
- this._playwrightConnection = ws;
126
- ws.on("message", async (data) => {
127
- try {
128
- const message = JSON.parse(data.toString());
129
- await this._handlePlaywrightMessage(message);
130
- }
131
- catch (error) {
132
- debugLogger(`Error while handling Playwright message\n${data.toString()}\n`, error);
133
- }
134
- });
135
- ws.on("close", () => {
136
- if (this._playwrightConnection !== ws)
137
- return;
138
- this._playwrightConnection = null;
139
- this._closeExtensionConnection("Playwright client disconnected");
140
- debugLogger("Playwright WebSocket closed");
141
- });
142
- ws.on("error", (error) => {
143
- debugLogger("Playwright WebSocket error:", error);
144
- });
145
- debugLogger("Playwright MCP connected");
146
- }
147
- _closeExtensionConnection(reason) {
148
- this._extensionConnection?.close(reason);
149
- this._extensionConnectionPromise.reject(new Error(reason));
150
- this._resetExtensionConnection();
151
- }
152
- _resetExtensionConnection() {
153
- this._connectedTabInfo = undefined;
154
- this._extensionConnection = null;
155
- this._extensionConnectionPromise = new ManualPromise();
156
- void this._extensionConnectionPromise.catch(logUnhandledError);
157
- }
158
- _closePlaywrightConnection(reason) {
159
- if (this._playwrightConnection?.readyState === WebSocket.OPEN)
160
- this._playwrightConnection.close(1000, reason);
161
- this._playwrightConnection = null;
162
- }
163
- _handleExtensionConnection(ws) {
164
- if (this._extensionConnection) {
165
- ws.close(1000, "Another extension connection already established");
166
- return;
167
- }
168
- this._extensionConnection = new ExtensionConnection(ws);
169
- this._extensionConnection.onclose = (c, reason) => {
170
- debugLogger("Extension WebSocket closed:", reason, c === this._extensionConnection);
171
- if (this._extensionConnection !== c)
172
- return;
173
- this._resetExtensionConnection();
174
- this._closePlaywrightConnection(`Extension disconnected: ${reason}`);
175
- };
176
- this._extensionConnection.onmessage =
177
- this._handleExtensionMessage.bind(this);
178
- this._extensionConnectionPromise.resolve();
179
- }
180
- _handleExtensionMessage(method, params) {
181
- switch (method) {
182
- case "forwardCDPEvent": {
183
- const sessionId = params.sessionId || this._connectedTabInfo?.sessionId;
184
- this._sendToPlaywright({
185
- sessionId,
186
- method: params.method,
187
- params: params.params,
188
- });
189
- break;
190
- }
191
- case "detachedFromTab":
192
- debugLogger("← Debugger detached from tab:", params);
193
- this._connectedTabInfo = undefined;
194
- break;
195
- }
196
- }
197
- async _handlePlaywrightMessage(message) {
198
- debugLogger("← Playwright:", `${message.method} (id=${message.id})`);
199
- const { id, sessionId, method, params } = message;
200
- try {
201
- const result = await this._handleCDPCommand(method, params, sessionId);
202
- this._sendToPlaywright({ id, sessionId, result });
203
- }
204
- catch (e) {
205
- debugLogger("Error in the extension:", e);
206
- this._sendToPlaywright({
207
- id,
208
- sessionId,
209
- error: { message: e.message },
210
- });
211
- }
212
- }
213
- async _handleCDPCommand(method, params, sessionId) {
214
- switch (method) {
215
- case "Browser.getVersion": {
216
- return {
217
- protocolVersion: "1.3",
218
- product: "Chrome/Extension-Bridge",
219
- userAgent: "CDP-Bridge-Server/1.0.0",
220
- };
221
- }
222
- case "Browser.setDownloadBehavior": {
223
- return {};
224
- }
225
- case "Target.setAutoAttach": {
226
- // Forward child session handling.
227
- if (sessionId)
228
- break;
229
- // Simulate auto-attach behavior with real target info
230
- const { targetInfo } = await this._extensionConnection.send("attachToTab");
231
- this._connectedTabInfo = {
232
- targetInfo,
233
- sessionId: `pw-tab-${this._nextSessionId++}`,
234
- };
235
- debugLogger("Simulating auto-attach");
236
- this._sendToPlaywright({
237
- method: "Target.attachedToTarget",
238
- params: {
239
- sessionId: this._connectedTabInfo.sessionId,
240
- targetInfo: {
241
- ...this._connectedTabInfo.targetInfo,
242
- attached: true,
243
- },
244
- waitingForDebugger: false,
245
- },
246
- });
247
- return {};
248
- }
249
- case "Target.getTargetInfo": {
250
- return this._connectedTabInfo?.targetInfo;
251
- }
252
- }
253
- return await this._forwardToExtension(method, params, sessionId);
254
- }
255
- async _forwardToExtension(method, params, sessionId) {
256
- if (!this._extensionConnection)
257
- throw new Error("Extension not connected");
258
- // Top level sessionId is only passed between the relay and the client.
259
- if (this._connectedTabInfo?.sessionId === sessionId)
260
- sessionId = undefined;
261
- return await this._extensionConnection.send("forwardCDPCommand", {
262
- sessionId,
263
- method,
264
- params,
265
- });
266
- }
267
- _sendToPlaywright(message) {
268
- debugLogger("→ Playwright:", `${message.method ?? `response(id=${message.id})`}`);
269
- this._playwrightConnection?.send(JSON.stringify(message));
270
- }
271
- }
272
- class ExtensionConnection {
273
- _ws;
274
- _callbacks = new Map();
275
- _lastId = 0;
276
- onmessage;
277
- onclose;
278
- constructor(ws) {
279
- this._ws = ws;
280
- this._ws.on("message", this._onMessage.bind(this));
281
- this._ws.on("close", this._onClose.bind(this));
282
- this._ws.on("error", this._onError.bind(this));
283
- }
284
- async send(method, params, sessionId) {
285
- if (this._ws.readyState !== WebSocket.OPEN)
286
- throw new Error(`Unexpected WebSocket state: ${this._ws.readyState}`);
287
- const id = ++this._lastId;
288
- this._ws.send(JSON.stringify({ id, method, params, sessionId }));
289
- const error = new Error(`Protocol error: ${method}`);
290
- return new Promise((resolve, reject) => {
291
- this._callbacks.set(id, { resolve, reject, error });
292
- });
293
- }
294
- close(message) {
295
- debugLogger("closing extension connection:", message);
296
- if (this._ws.readyState === WebSocket.OPEN)
297
- this._ws.close(1000, message);
298
- }
299
- _onMessage(event) {
300
- const eventData = event.toString();
301
- let parsedJson;
302
- try {
303
- parsedJson = JSON.parse(eventData);
304
- }
305
- catch (e) {
306
- debugLogger(`<closing ws> Closing websocket due to malformed JSON. eventData=${eventData} e=${e?.message}`);
307
- this._ws.close();
308
- return;
309
- }
310
- try {
311
- this._handleParsedMessage(parsedJson);
312
- }
313
- catch (e) {
314
- debugLogger(`<closing ws> Closing websocket due to failed onmessage callback. eventData=${eventData} e=${e?.message}`);
315
- this._ws.close();
316
- }
317
- }
318
- _handleParsedMessage(object) {
319
- if (object.id && this._callbacks.has(object.id)) {
320
- const callback = this._callbacks.get(object.id);
321
- this._callbacks.delete(object.id);
322
- if (object.error) {
323
- const error = callback.error;
324
- error.message = object.error;
325
- callback.reject(error);
326
- }
327
- else {
328
- callback.resolve(object.result);
329
- }
330
- }
331
- else if (object.id) {
332
- debugLogger("← Extension: unexpected response", object);
333
- }
334
- else {
335
- this.onmessage?.(object.method, object.params);
336
- }
337
- }
338
- _onClose(event) {
339
- debugLogger(`<ws closed> code=${event.code} reason=${event.reason}`);
340
- this._dispose();
341
- this.onclose?.(this, event.reason);
342
- }
343
- _onError(event) {
344
- debugLogger(`<ws error> message=${event.message} type=${event.type} target=${event.target}`);
345
- this._dispose();
346
- }
347
- _dispose() {
348
- for (const callback of this._callbacks.values())
349
- callback.reject(new Error("WebSocket closed"));
350
- this._callbacks.clear();
351
- }
352
- }
@@ -1,56 +0,0 @@
1
- /**
2
- * Copyright (c) Microsoft Corporation.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
- import debug from "debug";
17
- import * as playwright from "playwright-core";
18
- import { startHttpServer } from "../utils/httpServer.js";
19
- import { CDPRelayServer } from "./cdpRelay.js";
20
- const debugLogger = debug("pw:mcp:relay");
21
- export class ExtensionContextFactory {
22
- name = "extension";
23
- description = "Connect to a browser using the Playwright MCP extension";
24
- _browserChannel;
25
- _userDataDir;
26
- constructor(browserChannel, userDataDir) {
27
- this._browserChannel = browserChannel;
28
- this._userDataDir = userDataDir;
29
- }
30
- async createContext(clientInfo, abortSignal) {
31
- const browser = await this._obtainBrowser(clientInfo, abortSignal);
32
- return {
33
- browserContext: browser.contexts()[0],
34
- close: async () => {
35
- debugLogger("close() called for browser context");
36
- await browser.close();
37
- },
38
- };
39
- }
40
- async _obtainBrowser(clientInfo, abortSignal) {
41
- const relay = await this._startRelay(abortSignal);
42
- await relay.ensureExtensionConnectionForMCPContext(clientInfo, abortSignal);
43
- return await playwright.chromium.connectOverCDP(relay.cdpEndpoint());
44
- }
45
- async _startRelay(abortSignal) {
46
- const httpServer = await startHttpServer({});
47
- if (abortSignal.aborted) {
48
- httpServer.close();
49
- throw new Error(abortSignal.reason);
50
- }
51
- const cdpRelayServer = new CDPRelayServer(httpServer, this._browserChannel, this._userDataDir);
52
- abortSignal.addEventListener("abort", () => cdpRelayServer.stop());
53
- debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`);
54
- return cdpRelayServer;
55
- }
56
- }
@@ -1,35 +0,0 @@
1
- /**
2
- * Shared framework state patterns used by framework state detection hook
3
- */
4
- export const FRAMEWORK_STATE_PATTERNS = [
5
- // React/Next.js
6
- "__NEXT_DATA__",
7
- "__reactServerState",
8
- // Remix
9
- "__remixContext",
10
- "__remixManifest",
11
- "__remixRouteModules",
12
- // Apollo GraphQL
13
- "__APOLLO_STATE__",
14
- "__APOLLO_CLIENT__",
15
- // Redux/State Management
16
- "__PRELOADED_STATE__",
17
- "__INITIAL_STATE__",
18
- "__REDUX_STATE__",
19
- // Vue/Nuxt
20
- "__NUXT__",
21
- // Gatsby
22
- "___gatsby",
23
- "___loader",
24
- // Generic SSR
25
- "__SSR_DATA__",
26
- "__APP_STATE__",
27
- "__SERVER_STATE__",
28
- // Others
29
- "__QWIK_STATE__",
30
- "__SVELTE__",
31
- "__ANGULAR__",
32
- "__SOLID__",
33
- "__ASTRO_DATA__",
34
- ];
35
- export const MAX_DISPLAY_ITEMS = 5;
@@ -1,178 +0,0 @@
1
- import ms from "ms";
2
- import { Ok } from "../utils/result.js";
3
- import { getEventsAfter, isEventType, trackEvent } from "./events.js";
4
- import { hookNameSchema } from "./schema.js";
5
- const RESOLUTION_WAIT_MS = ms("10s");
6
- const lastProcessedEventIdByContext = new WeakMap();
7
- const detectedProvidersByContext = new WeakMap();
8
- const isLikelyResolved = async (ctx) => {
9
- if (!ctx.tab?.page)
10
- return false;
11
- const host = ctx.tab.page.url();
12
- if (host.includes("challenges.cloudflare.com"))
13
- return false;
14
- return ctx.tab.page.evaluate(() => {
15
- const challengeSelectors = [
16
- "#challenge-stage",
17
- "#cf-challenge-running",
18
- 'iframe[src*="turnstile"]',
19
- 'form[action*="/cdn-cgi/challenge-platform"]',
20
- "[data-cf-challenge]",
21
- ];
22
- const hasChallengeDom = challengeSelectors.some((selector) => document.querySelector(selector));
23
- if (hasChallengeDom)
24
- return false;
25
- const title = document.title.toLowerCase();
26
- const bodyText = (document.body?.innerText || "")
27
- .slice(0, 2000)
28
- .toLowerCase();
29
- if (title.includes("just a moment") || bodyText.includes("just a moment"))
30
- return false;
31
- if (title.includes("checking your browser") ||
32
- bodyText.includes("checking your browser"))
33
- return false;
34
- return true;
35
- });
36
- };
37
- const getDetectedProviders = (context) => {
38
- const detectedProviders = detectedProvidersByContext.get(context);
39
- if (detectedProviders)
40
- return detectedProviders;
41
- const newSet = new Set();
42
- detectedProvidersByContext.set(context, newSet);
43
- return newSet;
44
- };
45
- const updateLastProcessedEventId = (context, events) => {
46
- const lastEvent = events[events.length - 1];
47
- if (!lastEvent)
48
- return;
49
- lastProcessedEventIdByContext.set(context, lastEvent.id);
50
- };
51
- const isStatusOk = (status) => status >= 200 && status < 400;
52
- const providerConfigs = [
53
- {
54
- provider: "cloudflare-turnstile",
55
- match: (event) => isStatusOk(event.data.status) &&
56
- event.data.url.includes("challenges.cloudflare.com") &&
57
- event.data.url.includes("/turnstile/"),
58
- },
59
- {
60
- provider: "aws-waf",
61
- match: (event) => isStatusOk(event.data.status) &&
62
- event.data.method.toUpperCase() === "POST" &&
63
- event.data.url.includes(".awswaf.com") &&
64
- event.data.url.includes("/telemetry"),
65
- },
66
- ];
67
- export const getAntiBotProviderConfigs = () => providerConfigs;
68
- const waitForResolution = async (ctx) => {
69
- const start = Date.now();
70
- if (!ctx.tab?.page)
71
- return { resolved: false, waitedMs: 0 };
72
- await ctx.tab.page.waitForTimeout(RESOLUTION_WAIT_MS);
73
- const resolved = await isLikelyResolved(ctx);
74
- return { resolved, waitedMs: Date.now() - start };
75
- };
76
- const detectAntiBot = (ctx, events) => {
77
- const detectedProviders = getDetectedProviders(ctx.context);
78
- const networkEvents = events.filter(isEventType("network-request"));
79
- return providerConfigs.reduce((acc, config) => {
80
- if (detectedProviders.has(config.provider))
81
- return acc;
82
- const match = networkEvents.find(config.match);
83
- if (!match)
84
- return acc;
85
- detectedProviders.add(config.provider);
86
- return {
87
- hits: [
88
- ...acc.hits,
89
- {
90
- provider: config.provider,
91
- url: match.data.url,
92
- status: match.data.status,
93
- },
94
- ],
95
- };
96
- }, { hits: [] });
97
- };
98
- export const antiBotDetectionPreHook = {
99
- name: hookNameSchema.enum["anti-bot-detection-pre"],
100
- handler: async (ctx) => {
101
- if (lastProcessedEventIdByContext.has(ctx.context))
102
- return Ok(undefined);
103
- if (typeof ctx.eventStore.lastSeenEventId === "number")
104
- lastProcessedEventIdByContext.set(ctx.context, ctx.eventStore.lastSeenEventId);
105
- return Ok(undefined);
106
- },
107
- };
108
- export const antiBotDetectionPostHook = {
109
- name: hookNameSchema.enum["anti-bot-detection-post"],
110
- handler: async (ctx) => {
111
- const newEvents = getEventsAfter(ctx.eventStore, lastProcessedEventIdByContext.get(ctx.context));
112
- if (newEvents.length === 0)
113
- return Ok(undefined);
114
- const detection = detectAntiBot(ctx, newEvents);
115
- if (detection.hits.length > 0) {
116
- detection.hits.forEach((hit) => {
117
- trackEvent(ctx.context, {
118
- type: "anti-bot",
119
- data: {
120
- provider: hit.provider,
121
- detectionMethod: "network-request",
122
- url: hit.url,
123
- status: hit.status,
124
- action: "detected",
125
- waitMs: RESOLUTION_WAIT_MS,
126
- },
127
- });
128
- });
129
- const waitResult = await waitForResolution(ctx);
130
- detection.hits.forEach((hit) => {
131
- trackEvent(ctx.context, {
132
- type: "anti-bot",
133
- data: {
134
- provider: hit.provider,
135
- detectionMethod: "network-request",
136
- url: hit.url,
137
- status: hit.status,
138
- action: waitResult.resolved ? "resolved" : "still-blocked",
139
- waitMs: waitResult.waitedMs,
140
- },
141
- });
142
- });
143
- }
144
- updateLastProcessedEventId(ctx.context, newEvents);
145
- return Ok(undefined);
146
- },
147
- };
148
- export const antiBotDetectionHooks = {
149
- pre: antiBotDetectionPreHook,
150
- post: antiBotDetectionPostHook,
151
- };
152
- export const formatAntiBotEvent = (event) => {
153
- if (event.data.action === "still-blocked") {
154
- const waitSeconds = event.data.waitMs
155
- ? Math.round(event.data.waitMs / 1000)
156
- : 0;
157
- if (event.data.provider === "cloudflare-turnstile")
158
- return `Anti-bot still active: Cloudflare Turnstile after ${waitSeconds || "<1"}s wait`;
159
- if (event.data.provider === "aws-waf")
160
- return `Anti-bot still active: AWS WAF after ${waitSeconds || "<1"}s wait`;
161
- return "Anti-bot mechanism still active";
162
- }
163
- if (event.data.action === "resolved") {
164
- const waitSeconds = event.data.waitMs
165
- ? Math.round(event.data.waitMs / 1000)
166
- : 0;
167
- if (event.data.provider === "cloudflare-turnstile")
168
- return `Anti-bot resolved: Cloudflare Turnstile after ${waitSeconds || "<1"}s wait`;
169
- if (event.data.provider === "aws-waf")
170
- return `Anti-bot resolved: AWS WAF after ${waitSeconds || "<1"}s wait`;
171
- return "Anti-bot mechanism resolved";
172
- }
173
- if (event.data.provider === "cloudflare-turnstile")
174
- return `Anti-bot detected: Cloudflare Turnstile (${event.data.status})`;
175
- if (event.data.provider === "aws-waf")
176
- return `Anti-bot detected: AWS WAF telemetry request (${event.data.status})`;
177
- return "Anti-bot mechanism detected";
178
- };