├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .mocharc.js ├── .prettierignore ├── LICENSE.md ├── README.md ├── examples ├── README.md ├── multiple.js ├── multithreading.js ├── profiles.js ├── proxy.js └── stealth.js ├── package-lock.json ├── package.json ├── src ├── index.d.ts ├── index.js ├── loader.js └── utils.js └── test ├── basic.js ├── issues.js ├── plugin.js └── utils.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '[Bug]: ' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **Streps to reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Code example** 21 | A complete and formatted example of the code that leads to the described problem. 22 | Avoid specifying various secrets in the code, such as service keys an so on. 23 | 24 | **Expected behavior** 25 | A clear and concise description of what you expected to happen. 26 | 27 | **Screenshots** 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | **System (please complete the following information):** 31 | 32 | - Plugin version [e.g. 2.0.0] 33 | - Node version [e.g. 20.9.0] 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '[Feature]: ' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | examples/*.png 3 | examples/*.jpeg 4 | examples/profile 5 | .vscode 6 | *.env 7 | data 8 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | require: ['dotenv/config'], 5 | inlineDiffs: true, 6 | timeout: '300s', 7 | exit: true, 8 | }; 9 | 10 | process.env.NODE_ENV = 'test'; 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | prebuilds 2 | build 3 | data 4 | docs 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 CheshireCaat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # playwright-with-fingerprints 2 | 3 | This is the repo for `playwright-with-fingerprints`, a plugin for the [playwright](https://github.com/microsoft/playwright) framework that allows you to change a browser fingerprint, generate a virtual identity and improve your browser's stealth. 4 | 5 | In order to achieve this, the [FingerprintSwitcher](https://fp.bablosoft.com) service is used, which allows you to replace a list of important browser properties, and thus you will act like a completely new user. 6 | 7 | **WARNING:** plugin is still in beta stage, it means that bugs may happen, including critical. 8 | 9 | Adding a plugin to your project is very easy - it only takes a few lines of code. 10 | You just need to change the browser startup code a bit and add method calls to get and apply fingerprints. 11 | The rest of the code can remain unchanged. 12 | In general, only **four** basic steps are required, see the example below (code from the example may differ slightly from the real one): 13 | 14 | https://user-images.githubusercontent.com/30115373/198844008-de84d5f8-db14-498d-a7f0-630da931177e.mp4 15 | 16 | Current supported engine version - **135.0.7049.42**. 17 | 18 | **IMPORTANT NOTE:** plugin only work on **Windows** operating system, it cannot be installed and used on **Linux**, **macOS** and other systems! 19 | 20 | ## About 21 | 22 | This library allows you to change browser fingerprint and use **playwright** automation framework with enhanced anonymity. 23 | In order to migrate and use it, a minimum of modifications and code changes is required. 24 | 25 | **Browser fingerprinting** - is a technique that allows to identify the user by a combination of browser properties, such as - fonts, resolution, list of plugins, navigator properties, etc. 26 | By adding new factors and using browser **API** in a special way, a site can determine exactly which user is visiting it, even if the user is using a proxy. 27 | When using this package and replacing fingerprints, websites will not be able to identify you from other users, as all these properties and results of **API** calls will be replaced with values from real devices. 28 | 29 | Let's look at a small example of **WebGL** property substitution. 30 | In the screenshot below, the left column shows the values from the regular browser, and the right column shows the values substituted using ready-made fingerprints. 31 | This result cannot be achieved using only the replacement of various browser properties via **JavaScript**, that's what this plugin and service is for: 32 | 33 | ![WebGL](https://github.com/CheshireCaat/browser-with-fingerprints/raw/master/assets/webgl.jpg) 34 | 35 | You can learn more by following this [link](https://fp.bablosoft.com/#capabilities). 36 | 37 | ## Installation 38 | 39 | To use this plugin in your project, install it with your favorite package manager: 40 | 41 | ```bash 42 | npm i playwright-with-fingerprints 43 | # or 44 | pnpm i playwright-with-fingerprints 45 | # or 46 | yarn add playwright-with-fingerprints 47 | ``` 48 | 49 | The `playwright-core` or `playwright` packages must also be installed. If one of these packages is already installed, you can skip this step: 50 | 51 | ```bash 52 | npm i playwright 53 | # or 54 | npm i playwright-core 55 | ``` 56 | 57 | Both options will work correctly and do not require additional steps to make the plugin work with them. 58 | But keep in mind that some versions may not be supported. In this case, you will get an error when importing the plugin. 59 | You can always find supported version numbers [here](package.json#L64). 60 | 61 | Please note that this plugin only supports the default `playwright` library, wrappers have not been tested and may cause errors. 62 | Also, according to the [architecture](#architecture) section, this plugin can only be installed and used on **Windows** operating system. 63 | 64 | ## Creating new project 65 | 66 | Here's how to start working on a project from scratch. 67 | 68 | First you need to import the `playwright-with-fingerprints` library: 69 | 70 | ```js 71 | const { plugin } = require('playwright-with-fingerprints'); 72 | ``` 73 | 74 | No need to require the `playwright` or `playwright-core`. 75 | 76 | After that, you need to obtain the fingerprint from the server and apply it: 77 | 78 | ```js 79 | // Set the service key for the plugin (you can buy it here https://bablosoft.com/directbuy/FingerprintSwitcher/2). 80 | // Leave an empty string to use the free version. 81 | plugin.setServiceKey(''); 82 | 83 | const fingerprint = await plugin.fetch({ 84 | tags: ['Microsoft Windows', 'Chrome'], 85 | }); 86 | 87 | plugin.useFingerprint(fingerprint); 88 | ``` 89 | 90 | When this code is executed, the `fingerprint` variable will contain the data required to apply the fingerprint. 91 | It's a string, you can save it to a file and use it later. 92 | Running this code for the first time can be very slow. Additional time is needed to download the browser. 93 | 94 | Finally, you need to create the browser instance: 95 | 96 | ```js 97 | const browser = await plugin.launch(); 98 | ``` 99 | 100 | The parameters of the launch method are the same as the corresponding method in the playwright library, [link](https://playwright.dev/docs/api/class-browsertype#browser-type-launch). 101 | The `browser` variable will contain an instance of the [Browser](https://playwright.dev/docs/api/class-browser) class defined in the playwright library. 102 | It means that it can be used to write a new or use an existing playwright script without any changes. 103 | You need to rely on the documentation of the original framework on how to use control browser - [link](https://playwright.dev/docs/intro). 104 | 105 | Here is the complete code, you can copy/paste it and try: 106 | 107 | ```js 108 | const { plugin } = require('playwright-with-fingerprints'); 109 | 110 | // Set the service key for the plugin (you can buy it here https://bablosoft.com/directbuy/FingerprintSwitcher/2). 111 | // Leave an empty string to use the free version. 112 | plugin.setServiceKey(''); 113 | 114 | (async () => { 115 | // Get a fingerprint from the server: 116 | const fingerprint = await plugin.fetch({ 117 | tags: ['Microsoft Windows', 'Chrome'], 118 | }); 119 | 120 | // Apply fingerprint: 121 | plugin.useFingerprint(fingerprint); 122 | 123 | // Launch the browser instance: 124 | const browser = await plugin.launch(); 125 | 126 | // The rest of the code is the same as for a standard `playwright` library: 127 | const page = await browser.newPage(); 128 | await page.goto('https://example.com'); 129 | 130 | // Print the browser viewport size: 131 | console.log( 132 | 'Viewport:', 133 | await page.evaluate(() => ({ 134 | deviceScaleFactor: window.devicePixelRatio, 135 | width: document.documentElement.clientWidth, 136 | height: document.documentElement.clientHeight, 137 | })) 138 | ); 139 | 140 | await browser.close(); 141 | })(); 142 | ``` 143 | 144 | Also take a look at the **TypeScript** declarations [here](src/index.d.ts) for more details about the exported classes, methods, and properties. 145 | Thanks to them, when using the library, auto-completion with detailed descriptions will work. 146 | 147 | ## Migration 148 | 149 | There are a few steps required to start using fingerprints for your existing project: 150 | 151 | 1. Import `playwright-with-fingerprints` instead of `playwright` or `playwright-core`. 152 | 2. Call the `useFingerprint` and/or `useProxy` methods to apply the fingerprint and proxy before starting the browser. 153 | 3. Launch the browser using the `plugin.launch` method (the `plugin` variable was imported in the first step). 154 | 4. The rest of the code can be left unchanged. 155 | 156 | Consider you have the following project using the playwright: 157 | 158 | ```js 159 | /* Without fingerprints */ 160 | const { chromium } = require('playwright'); 161 | 162 | (async () => { 163 | const browser = await chromium.launch(); 164 | 165 | const page = await browser.newPage(); 166 | await page.goto('https://browserleaks.com/canvas', { waitUntil: 'networkidle' }); 167 | 168 | console.log('Canvas signature:', await page.$eval('#crc', (el) => el.innerText)); 169 | 170 | await browser.close(); 171 | })(); 172 | ``` 173 | 174 | It verifies the canvas signature by visiting [this](https://browserleaks.com/canvas) URL and parsing the corresponding value. 175 | No matter how many times you run this test on the same machine, the results will be the **same**. 176 | This is because this test depends on the hardware you are using - if the hardware stays the same, the results will also be the same. 177 | But if we change the fingerprint, the results will be different. 178 | 179 | Let's modify this project to add fingerprint support. The updated code will look like this: 180 | 181 | ```js 182 | /* With fingerprints */ 183 | 184 | // Import `playwright-with-fingerprints` instead of `playwright` 185 | // const { chromium } = require('playwright'); 186 | const { plugin } = require('playwright-with-fingerprints'); 187 | 188 | // Set the service key for the plugin (you can buy it here https://bablosoft.com/directbuy/FingerprintSwitcher/2). 189 | // Leave an empty string to use the free version. 190 | plugin.setServiceKey(''); 191 | 192 | (async () => { 193 | // Obtain a fingerprint from the server. The resulting variable contains a string - it can be stored for later use: 194 | const fingerprint = await plugin.fetch({ 195 | tags: ['Microsoft Windows', 'Chrome'], 196 | }); 197 | 198 | // Apply fingerprint - after calling the `useFingerprint` method, the browser will be launched with a fingerprint: 199 | plugin.useFingerprint(fingerprint); 200 | 201 | // Replace `chromium.launch` method call with `plugin.launch`: 202 | // const browser = await chromium.launch(); 203 | const browser = await plugin.launch(); 204 | 205 | // The rest of the code is the same as for the standard `playwright` library: 206 | const page = await browser.newPage(); 207 | await page.goto('https://browserleaks.com/canvas', { waitUntil: 'networkidle' }); 208 | 209 | console.log('Canvas signature:', await page.$eval('#crc', (el) => el.innerText)); 210 | 211 | await browser.close(); 212 | })(); 213 | ``` 214 | 215 | After running the updated code, a new fingerprint will be applied each time, so the scores will be different for each run. 216 | 217 | ## Common problems 218 | 219 | You can find information about known issues related to updates, as well as ways to solve them in [this](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/MIGRATION.md) guide. 220 | 221 | ## Launching the browser 222 | 223 | You can launch the browser in two different ways. There are two methods for this - **launch** and **spawn**. 224 | 225 | The parameters and return type of the **launch** method are exactly the same as for `BrowserType.launch` method. 226 | You can use the official [API](https://playwright.dev/docs/api/class-browsertype#browser-type-launch) documentation to learn more about them. 227 | The **launch** method also has the same purpose - to start a new browser instance with the given parameters and connect to it. 228 | 229 | In addition to the standard functionality, it allows you to change the fingerprint and proxy using the `useFingerprint` and `useProxy` methods. 230 | A detailed description and annotations can also be found [here](src/index.d.ts#L65). 231 | 232 | ```js 233 | const { plugin } = require('playwright-with-fingerprints'); 234 | 235 | const browser = await plugin.launch({ 236 | args: ['--mute-audio'], 237 | headless: true, 238 | }); 239 | ``` 240 | 241 | The **spawn** method works in a similar way, but uses a separate mechanism to launch the browser. 242 | The main difference is that this method just starts the process, but doesn't connect to it for automation - you can do it yourself later. 243 | 244 | This method returns a running browser instance that can be used to connect to an existing session using `playwright`: 245 | 246 | ```js 247 | const { chromium } = require('playwright'); 248 | const { plugin } = require('playwright-with-fingerprints'); 249 | 250 | const chrome = await plugin.spawn({ headless: true }); 251 | 252 | const browser = await chromium.connect(chrome.url); 253 | 254 | await browser.close(); 255 | await chrome.close(); 256 | ``` 257 | 258 | At [this](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/plugin/launcher/index.d.ts#L54) link you can find a detailed description of all the options allowed for **spawn** method. 259 | The same goes for the return type declaration, details of which can be found [here](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/plugin/launcher/index.d.ts#L6). 260 | 261 | If possible, use it only in extreme cases. It is much more convenient to use the **launch** method to launch the browser, which minimizes the number of steps for proper initialization and configuration. 262 | 263 | Annotations are described for all plugins methods directly in the library code via the **TypeScript** declarations, so when using it you will be able to see hints for all options and types. 264 | You can also find out about it directly [here](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts) and [here](src/index.d.ts). 265 | 266 | ## Configuring plugin 267 | 268 | At the moment, it is possible to change the service key, working folder and timeout for requests to the engine, which is used when fetching, applying fingerprints, and so on: 269 | 270 | ```js 271 | // Set the fingerprint service key for all plugin methods that require it. 272 | plugin.setServiceKey('SERVICE_KEY'); 273 | 274 | // Set the folder where the plugin engine will be installed: 275 | plugin.setWorkingFolder('./engine'); 276 | 277 | // Set the timeout used when fetching fingerprints and so on: 278 | plugin.setRequestTimeout(5 * 60000); 279 | 280 | // Set the timeout used when installing/downloading engine: 281 | plugin.setEngineTimeout(10 * 60000); 282 | ``` 283 | 284 | The methods from the example above change the settings globally, that is, for all instances of the plugin. 285 | 286 | The default values are the `./data` directory for the working folder and `300000` milliseconds for the request timeout. 287 | 288 | An empty string is used for the fingerprint service key by default, which means that the free version of the service will be used. 289 | 290 | ## Configuring browser 291 | 292 | In order to change the fingerprint and proxy for your browser, you should use special separate methods: 293 | 294 | - **useProxy** - to change the proxy configuration. 295 | - **useProfile** - to change the profile configuration. 296 | - **useFingerprint** - to change the fingerprint configuration. 297 | 298 | These methods directly affect only the next launch of the browser. So you should always use them before using the `launch` plugin method. 299 | 300 | You cannot change the settings once the browser is launched - more specifically, an already launched instance will not be affected by the new configuration. 301 | But you can safely change the options for the next run, or for a separate browser instance with a different unique configuration. 302 | 303 | You can also **chain** calls, since all these methods return the current plugin instance. It does not matter in which order the settings will be applied. It might look like this: 304 | 305 | ```js 306 | const { plugin } = require('playwright-with-fingerprints'); 307 | 308 | // Set the service key for the plugin (you can buy it here https://bablosoft.com/directbuy/FingerprintSwitcher/2). 309 | // Leave an empty string to use the free version. 310 | plugin.setServiceKey(''); 311 | 312 | const fingerprint = await plugin.fetch({ 313 | tags: ['Microsoft Windows', 'Chrome'], 314 | }); 315 | 316 | plugin.useProxy('127.0.0.1:8080').useFingerprint(fingerprint); 317 | ``` 318 | 319 | Use these links to see a detailed description of the methods: 320 | 321 | - [This](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts#L94) one for the **useFingerprint** method 322 | (also see additional options [here](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/types/fingerprint.d.ts#L4)). 323 | - [This](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts#L124) one for the **useProfile** method 324 | (also see additional options [here](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/types/profile.d.ts#L4)). 325 | - [This](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts#L152) one for the **useProxy** method 326 | (also see additional options [here](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/types/proxy.d.ts#L24)). 327 | 328 | The usage of these methods is very similar - each takes two parameters, the first of which is the configuration data itself, and the second is additional options. 329 | The fingerprint and proxy will not be changed unless the appropriate method is used. In this case, all settings related to browser fingerprinting will remain at their original values. 330 | 331 | Fingerprint and proxy aren't applied instantly when calling methods. Instead, the configuration is saved and used directly when the browser is launched using the **launch** or **spawn** methods. 332 | Thus, you can pre-configure the plugin in a certain way, or change something immediately before launching the browser. 333 | 334 | ### Configuring browser version 335 | 336 | You can change the browser version right while using the plugin - the engine may come with several different builds of the browser. 337 | 338 | In order to do this, use the **useBrowserVersion** method. 339 | The `default` value means that the latest available version will be used: 340 | 341 | ```js 342 | const { plugin } = require('playwright-with-fingerprints'); 343 | 344 | // Use a specific version: 345 | plugin.useBrowserVersion('128.0.6613.85'); 346 | 347 | // Use the latest available version: 348 | plugin.useBrowserVersion('default'); 349 | ``` 350 | 351 | If you specify an unavailable or invalid version, an appropriate error will be thrown when the browser starts. 352 | Also keep in mind that this property only affects a specific instance of the plugin, not the entire library. 353 | 354 | In order to get a list of available versions shipped with the engine, use the **versions** method. 355 | It returns a list of browser versions as strings or objects with additional information, depending on the format passed: 356 | 357 | ```js 358 | const { plugin } = require('playwright-with-fingerprints'); 359 | 360 | // The list of versions is always sorted in descending order: 361 | await plugin.versions('extended').then((versions) => { 362 | // The latest available browser version will be used: 363 | plugin.useBrowserVersion(versions[0]['browser_version']); 364 | }); 365 | ``` 366 | 367 | Thanks to this, you can, for example, use the version that corresponds to a certain fingerprint, and vice versa. 368 | 369 | ### Fingerprint usage 370 | 371 | In order to change the fingerprint, you need to run the `useFingerprint` method before starting the browser, i.e. before using the plugin's `launch` method. 372 | 373 | The `useFingerprint` method takes two parameters. 374 | The first is a string with fingerprint data that you can request from the service. 375 | The second is additional options for applying a fingerprint, most of which are applied automatically - for example, the safe replacement of the **BatteryAPI** and **AudioAPI** properties: 376 | 377 | ```js 378 | const { plugin } = require('playwright-with-fingerprints'); 379 | 380 | // Set the service key for the plugin (you can buy it here https://bablosoft.com/directbuy/FingerprintSwitcher/2). 381 | // Leave an empty string to use the free version. 382 | plugin.setServiceKey(''); 383 | 384 | const fingerprint = await plugin.fetch({ 385 | tags: ['Microsoft Windows', 'Chrome'], 386 | }); 387 | 388 | plugin.useFingerprint(fingerprint, { 389 | // It's disabled by default. 390 | safeElementSize: true, 391 | // It's enabled by default. 392 | safeBattery: false, 393 | }); 394 | ``` 395 | 396 | In order to obtain fingerprints you should use the **fetch** plugin method. 397 | Pass the service key as the first argument for the **serviceKey** and additional parameters for the **fetch**, if necessary: 398 | 399 | ```js 400 | const { plugin } = require('playwright-with-fingerprints'); 401 | 402 | // Set the service key for the plugin (you can buy it here https://bablosoft.com/directbuy/FingerprintSwitcher/2). 403 | // Leave an empty string to use the free version. 404 | plugin.setServiceKey(''); 405 | 406 | const fingerprint = await plugin.fetch({ 407 | tags: ['Microsoft Windows', 'Chrome'], 408 | // Fetch fingerprints only with a browser version higher than 130: 409 | minBrowserVersion: 130, 410 | // Fetch fingerprints only with a browser version lower than 132: 411 | maxBrowserVersion: 132, 412 | // Fetch fingerprints only collected in the last 15 days: 413 | timeLimit: '15 days', 414 | }); 415 | ``` 416 | 417 | In order to use filters and many other settings from the example above, you will need a premium key, the same applies to tags that differ from the default ones: 418 | 419 | ```js 420 | const { plugin } = require('playwright-with-fingerprints'); 421 | 422 | // Set the service key for the plugin (you can buy it here https://bablosoft.com/directbuy/FingerprintSwitcher/2). 423 | plugin.setServiceKey('SERVICE_KEY'); 424 | 425 | // In order to use custom tags you need the premium key: 426 | const fingerprint = await plugin.fetch({ 427 | tags: ['Android', 'Chrome'], 428 | }); 429 | plugin.useFingerprint(fingerprint); 430 | 431 | await plugin.launch(); 432 | ``` 433 | 434 | All possible settings for **fetch** method, as well as their descriptions, you can find [here](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/types/fetch.d.ts#L35). 435 | 436 | The special `current` value can be used to filter fingerprints by browser version - in this case, the version installed for the plugin will be used. 437 | It can be very convenient as the browser and fingerprint versions will be exactly the same and you don't have to enter the exact values in multiple places. 438 | 439 | You can **reuse** fingerprints instead of requesting new ones each time. 440 | To do this, you can save them to a file or to a database - use any option convenient for you. 441 | In this way, you can speed up the process of launching the browser with the parameters you need, organize your storage, filter and sort fingerprints locally, and much more: 442 | 443 | ```js 444 | const { readFile, writeFile } = require('fs/promises'); 445 | const { plugin } = require('playwright-with-fingerprints'); 446 | 447 | // Set the service key for the plugin (you can buy it here https://bablosoft.com/directbuy/FingerprintSwitcher/2). 448 | // Leave an empty string to use the free version. 449 | plugin.setServiceKey(''); 450 | 451 | // Save the fingerprint to a file: 452 | const fingerprint = await plugin.fetch({ 453 | tags: ['Microsoft Windows', 'Chrome'], 454 | }); 455 | await writeFile('fingerprint.json', fingerprint); 456 | 457 | // Load fingerprint from file at next run: 458 | plugin.useFingerprint(await readFile('fingerprint.json', 'utf8')); 459 | ``` 460 | 461 | You can learn more about the options directly when adding these methods - just use the built-in [annotations](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts#L214). 462 | 463 | You can use any [tags](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/types/fetch.d.ts#L13), filters 464 | (e.g. [time](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/types/fetch.d.ts#L6) limit) and settings if you have a service key. 465 | 466 | If you specify an empty string as the first argument for the `fetch` or the `setServiceKey` method, the free version will be used. 467 | For a free version you won't be able to use other tags than the default ones, as well as some other filters: 468 | 469 | ```js 470 | // Set the service key for the plugin (you can buy it here https://bablosoft.com/directbuy/FingerprintSwitcher/2). 471 | // Leave an empty string to use the free version. 472 | plugin.setServiceKey(''); 473 | 474 | const fingerprint = await plugin.fetch({ 475 | // You can only use these tags with the free version: 476 | tags: ['Microsoft Windows', 'Chrome'], 477 | // You also cannot use such filters in the free version: 478 | // minBrowserVersion: 132, 479 | }); 480 | ``` 481 | 482 | In the free version, the [PerfectCanvas](https://wiki.bablosoft.com/doku.php?id=perfectcanvas) technology is also not available. 483 | There are other limitations when using the free version - for example, limiting the number of requests in a certain period of time. 484 | To see the differences and limits of different versions, visit [this](https://fp.bablosoft.com/#pricing) website. 485 | 486 | You can buy a key [here](https://bablosoft.com/directbuy/FingerprintSwitcher/2) to avoid limitations. 487 | 488 | ### Profile usage 489 | 490 | In order to use a specific browser profile, you can, among other options, use the `useProfile` method. 491 | As the first parameter, it takes the path to the profile folder - the same value that you specify, for example, for the `user-data-dir` argument. 492 | The second parameter is additional options that are primarily responsible for loading fingerprint and proxy data from the profile folder: 493 | 494 | ```js 495 | const path = require('path'); 496 | const { plugin } = require('playwright-with-fingerprints'); 497 | 498 | // Set the service key for the plugin (you can buy it here https://bablosoft.com/directbuy/FingerprintSwitcher/2). 499 | // Leave an empty string to use the free version. 500 | plugin.setServiceKey(''); 501 | 502 | // The key may be required if the fingerprint will be used from the profile. 503 | plugin.useProfile(path.resolve('./profile'), { 504 | // Don't load fingerprint from profile folder: 505 | loadFingerprint: false, 506 | // Don't load proxy from profile folder: 507 | loadProxy: false, 508 | }); 509 | ``` 510 | 511 | By default, the plugin will load proxy and fingerprint data from the profile folder, if they are written there. 512 | You can change this behavior by setting the options to `false` - for example, if you are not sure that the stored proxy is working. 513 | Please note that if you add your fingerprint or proxy through the appropriate methods, then the data you specified will be used regardless of the options. 514 | 515 | You can also use other options to set the path to the profile folder, such as the `userDataDir` option, if you don't need additional settings: 516 | 517 | ```js 518 | const path = require('path'); 519 | const { plugin } = require('playwright-with-fingerprints'); 520 | 521 | const browser = await plugin.launch({ 522 | userDataDir: './profile', 523 | // Browser arguments can be used as well: 524 | args: [`--user-data-dir=${path.resolve('./profile')}`], 525 | }); 526 | ``` 527 | 528 | After launching a browser with your profile, the fingerprint and proxy data you specified will always be stored in the profile folder. 529 | This setting itself is saved between browser launches, that is, it behaves in the same way as other similar methods. 530 | To run different profiles, you need to call this method again with different values for the profile directory. 531 | 532 | You can learn more about the parameters and additional options for this method [here](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts#L124) 533 | and [here](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/types/profile.d.ts#L4). 534 | 535 | #### Temporary profiles 536 | 537 | Since version `1.3.0` the plugin uses its own logic to configure browser profiles. 538 | This primarily applies to situations where you don't specify options like `user-data-dir` yourself. 539 | 540 | Each framework makes temporary profiles in such cases in completely different places, but for the convenience and correct operation of some fingerprint components, a different solution is used. 541 | If you do not specify the path to the profile yourself, then the engine will create its own temporary one - such a profile will be located directly in the engine folder, for example: 542 | 543 | ```js 544 | const { plugin } = require('playwright-with-fingerprints'); 545 | 546 | // The profile will be located in the `data/profiles/UNIQUE_ID` folder: 547 | const browser = await plugin.launch(); 548 | ``` 549 | 550 | If you specify the path yourself, then everything will work as usual - just the plugin will add additional components, like `widevine` or default extensions, to the specified profile. 551 | 552 | Also keep in mind that if you specify your own folder for the engine using the `setWorkingFolder` method, then the profile path will be adjusted. 553 | 554 | ### Proxy usage 555 | 556 | In order to set up a proxy, you should use the `useProxy` method. 557 | The first parameter of this method is a string with information about the proxy. 558 | The second parameter is additional options that will be applied to the browser, for example, automatic change of language and time zone: 559 | 560 | ```js 561 | const { plugin } = require('playwright-with-fingerprints'); 562 | 563 | plugin.useProxy('127.0.0.1:8080', { 564 | // Change browser timezone according to proxy: 565 | changeTimezone: true, 566 | // Replace browser geolocation according to proxy: 567 | changeGeolocation: true, 568 | }); 569 | ``` 570 | 571 | You can learn more about the parameters and additional options for this method [here](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts#L152) 572 | and [here](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/types/proxy.d.ts#L24). 573 | 574 | The browser supports two types of proxies - **https** and **socks5**. 575 | It is better to always specify the proxy type in the address line - otherwise, **https** will be used by default. 576 | 577 | You can use aliases - **http** instead of **https** and **socks** instead of **socks5**. 578 | Proxies with authorization (with login and password) are also supported. 579 | 580 | In general, when specifying addresses, you can use many different formats, for example: 581 | 582 | - `127.0.0.1:8080` 583 | - `https://127.0.0.1:8080` 584 | - `socks5://127.0.0.1:8181` 585 | - `username:password@127.0.0.1:8080` 586 | - `socks:127.0.0.1:8080:username:password` 587 | - `https://username:password:127.0.0.1:8080` 588 | 589 | In order to preserve compatibility with the original library syntax, the proxy can be obtained from the arguments you specified. 590 | The `proxy-server` option will be used as the value, and all other options will be set to their default values. 591 | But this will be done only if you didn't call the appropriate method for the proxy configuration: 592 | 593 | ```js 594 | const { plugin } = require('playwright-with-fingerprints'); 595 | 596 | const browser = await plugin.launch({ 597 | // The syntax for specifying an argument value 598 | // is exactly the same as for using a separate method. 599 | args: ['--proxy-server=https://127.0.0.1:8080'], 600 | }); 601 | ``` 602 | 603 | It's better to replace such code with the `useProxy` method. This is much more convenient because you can immediately set the additional options you need. 604 | 605 | ### More info 606 | 607 | If you are having problems with the default plugin, or want to create multiple instances with different settings, you can use a separate exported [createPlugin](src/index.d.ts#L119) method. 608 | It allows you to create a standalone plugin instance that is used in the same way as a standard one. It takes a **playwright** compatible launcher object as a parameter: 609 | 610 | ```js 611 | const { chromium } = require('playwright'); 612 | const { createPlugin } = require('playwright-with-fingerprints'); 613 | 614 | const plugin1 = createPlugin({ 615 | // This method is required, must return the same 616 | // type and take the same parameters as the built-in. 617 | launch: (options) => chromium.launch(options), 618 | }); 619 | 620 | // The default instance is created in the same way. 621 | const plugin2 = createPlugin(chromium); 622 | ``` 623 | 624 | Use this with caution and only in extreme cases - for example, if you are using a wrapper library. It is much safer to work with a default plugin instance. 625 | 626 | If you want to learn more about fingerprint substitution technology, explore the list of replaceable properties and various options, such as tags, get or configure your service key, use [this](https://fp.bablosoft.com) link. 627 | There you can also get a test fingerprint and see ready-made values that can be applied to your browser. 628 | 629 | ### More examples 630 | 631 | Please take a look at the [examples](examples) directory with ready-made real code examples. 632 | You can run them yourself by cloning the repository locally and installing the dependencies. 633 | 634 | ## Architecture 635 | 636 | This plugin uses the [FingerprintSwitcher](https://fp.bablosoft.com) service to get fingerprints. 637 | The resulting fingerprints are used later directly when working with the browser and are applied in a special way using a custom configuration files. 638 | 639 | There are some **limitations** in using the package, which may be critical or non-critical depending on your task. 640 | For example, for the correct operation of the fingerprint substitution technology, a custom browser with various patches is required. 641 | This browser is downloaded and installed automatically the first time the **API** methods are called. 642 | It also implies a limitation - it will be impossible to use standard and other various engines. 643 | 644 | Fingerprints aren't generated, but downloaded from the service, as they are collected from real devices. 645 | This greatly improves the anonymity and quality of the substitution of various properties. 646 | 647 | Also keep in mind that this package only work on the **Windows** operating system. 648 | If you install or run it on other platforms, you will get the corresponding errors. 649 | This is a forced measure due to the presence of some critical **Windows-only** dependencies without which this implementation will not work. 650 | 651 | The plugin architecture can be summarized as the following diagram: 652 | 653 | ![Architecture](https://github.com/CheshireCaat/browser-with-fingerprints/raw/master/assets/plugin.jpg) 654 | 655 | All packages can only work with the **Chrome** browser, which comes bundled with the libraries and loads automatically. 656 | The path to the executable file is defined on the plugin side and cannot be changed. 657 | It means that you will not be able to use not only other versions of **Chrome** or **Chromium**, but also other browser engines. 658 | The same goes for some framework-specific launch options. 659 | 660 | This library tries to replicate the interfaces of the **playwright** framework as much as possible. 661 | Thus, it's convenient to use it not only for new projects, but also when migrating from the original version to this plugin. 662 | For things not related to launching the browser and the plugin directly, it's better to use the methods and properties of the original library. 663 | 664 | ### Limitations 665 | 666 | Please **note** that there are some restrictions at the moment: 667 | 668 | - Only **Windows** operating system is supported. 669 | - Parallel launch of browsers is synchronized between calls. 670 | - Working with **workers** is possible only when specifying a separate working folder for each of them. 671 | 672 | Also, there is no guarantee that each of these items will be changed in the future. 673 | 674 | ## Alternatives 675 | 676 | Check out other ready-made plugins for popular automation frameworks that have a similar **API** and architecture: 677 | 678 | - Plugin for **selenium** - [selenium-with-fingerprints](https://github.com/CheshireCaat/selenium-with-fingerprints) 679 | - Plugin for **puppeteer** - [puppeteer-with-fingerprints](https://github.com/CheshireCaat/puppeteer-with-fingerprints) 680 | 681 | Also check out [BAS](https://bablosoft.com/shop/BrowserAutomationStudio) - a great alternative to automate the **Chrome** browser without programming skills. 682 | It also supports fingerprint substitution, has simple and powerful multithreading and other advantages. 683 | 684 | ## Documentation 685 | 686 | Here you can find a brief description of methods and classes, as well as links to them. 687 | 688 | #### [Tag](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/types/fetch.d.ts#L13) 689 | 690 | Describes a tag value that can be used to filter fingerprints. 691 | 692 | --- 693 | 694 | #### [Time](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/types/fetch.d.ts#L6) 695 | 696 | Describes a time limit that can be used to filter fingerprints. 697 | 698 | --- 699 | 700 | #### [Version](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts#L16) 701 | 702 | Describes an object that provides complete information about the available browser version. 703 | 704 | --- 705 | 706 | #### [createPlugin(launcher)](src/index.d.ts#L119) 707 | 708 | - `launcher` **[Launcher](src/index.d.ts#L14)** Playwright (or **API** compatible) browser launcher. 709 | 710 | Returns: **PlaywrightFingerprintPlugin** A new separate plugin instance. 711 | 712 | Create a separate plugin instance using the provided **playwright** compatible browser launcher. 713 | 714 | --- 715 | 716 | #### [plugin.versions(format?)](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts#L64) 717 | 718 | - `format` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** The output format of the returned result. 719 | 720 | Returns: **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<Version[]|string[]>** The list of objects with detailed version information, or a list of strings. 721 | 722 | Get a list of all available browser versions. 723 | 724 | --- 725 | 726 | #### [plugin.spawn(options?)](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts#L250) 727 | 728 | - `options` **[Options](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/plugin/launcher/index.d.ts#L54)?** Launcher options that only apply to the browser when using the `spawn` method. 729 | 730 | Returns: **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<Browser>** Promise which resolves to a browser instance. 731 | 732 | Launches a browser instance with given arguments and options when specified. 733 | 734 | --- 735 | 736 | #### [plugin.launch(options?)](src/index.d.ts#L65) 737 | 738 | - `options` **[PluginLaunchOptions](src/index.d.ts#L7)?** Set of configurable options to set on the browser. 739 | 740 | Returns: **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<Playwright.Browser>** Promise which resolves to a browser instance. 741 | 742 | Launches **playwright** and launches a browser instance with given arguments and options when specified. 743 | 744 | --- 745 | 746 | #### [plugin.launchPersistentContext(userDataDir, options?)](src/index.d.ts#L77) 747 | 748 | - `userDataDir` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** Path to a user data directory, which stores browser session data like cookies and local storage. 749 | - `options` **[PluginLaunchOptions](src/index.d.ts#L7)?** Set of configurable options to set on the browser. 750 | 751 | Returns: **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<Playwright.BrowserContext>** Promise which resolves to a context instance. 752 | 753 | Returns the persistent browser context instance. 754 | 755 | Launches browser that uses persistent storage located at `userDataDir` and returns the only context. Closing this context will automatically close the browser. 756 | 757 | --- 758 | 759 | #### [plugin.fetch(options?)](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts#L214) 760 | 761 | - `options` **[FetchOptions](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/types/fetch.d.ts#L35)?** Set of configurable options for getting a browser fingerprint. 762 | 763 | Returns: **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)<string>** Promise which resolves to a fingerprint string. 764 | 765 | Obtain a fingerprint using the specified service key and additional options. 766 | 767 | --- 768 | 769 | #### [plugin.useBrowserVersion(version)](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts#L175) 770 | 771 | - `value` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Version value as a string. 772 | 773 | Returns: **this** The same plugin instance with an updated settings (for optional chaining). 774 | 775 | Set the current browser version used by the plugin instance. 776 | 777 | --- 778 | 779 | #### [plugin.useProxy(value?, options?)](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts#L152) 780 | 781 | - `value` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** Proxy value as a string. 782 | - `options` **[ProxyOptions](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/types/proxy.d.ts#L24)?** Set of configurable options for applying a proxy. 783 | 784 | Returns: **this** The same plugin instance with an updated settings (for optional chaining). 785 | 786 | Set the proxy settings using the specified proxy as a string and additional options when specified. 787 | 788 | --- 789 | 790 | #### [plugin.useProfile(value?, options?)](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts#L124) 791 | 792 | - `value` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** Profile value as a string. 793 | - `options` **[ProfileOptions](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/types/profile.d.ts#L4)?** Set of configurable options for applying a profile. 794 | 795 | Returns: **this** The same plugin instance with an updated settings (for optional chaining). 796 | 797 | Set the profile settings using the specified profile as a string and additional options when specified. 798 | 799 | --- 800 | 801 | #### [plugin.useFingerprint(value?, options?)](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts#L94) 802 | 803 | - `value` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** Fingerprint value as a string. 804 | - `options` **[FingerprintOptions](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/types/fingerprint.d.ts#L4)?** Set of configurable options for applying a fingerprint. 805 | 806 | Returns: **this** The same plugin instance with an updated settings (for optional chaining). 807 | 808 | Set the fingerprint settings using the specified fingerprint as a string and additional options when specified. 809 | 810 | --- 811 | 812 | #### [plugin.setWorkingFolder(folder)](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts#L301) 813 | 814 | - `folder` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** The working folder that the plugin engine will use. 815 | 816 | Set the working folder that the plugin uses to work with the engine. 817 | 818 | --- 819 | 820 | #### [plugin.setRequestTimeout(timeout)](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts#L267) 821 | 822 | - `timeout` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** The request timeout that the plugin engine will use. 823 | 824 | Set the timeout that the plugin uses when executing requests (pass `0` to disable it). 825 | 826 | --- 827 | 828 | #### [plugin.setEngineTimeout(timeout)](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts#L284) 829 | 830 | - `timeout` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** The engine timeout that the plugin engine will use. 831 | 832 | Set the timeout that the plugin uses when fetching engine (pass `0` to disable it). 833 | 834 | --- 835 | 836 | #### [plugin.setServiceKey(key)](https://github.com/CheshireCaat/browser-with-fingerprints/blob/master/src/index.d.ts#L318) 837 | 838 | - `key` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** The service key for obtaining and applying a fingerprint. 839 | 840 | Set the fingerprint service key for all plugin methods that require it. 841 | 842 | --- 843 | 844 | ## Troubleshooting 845 | 846 | If you encounter any issue or bug, please use the [issues](https://github.com/CheshireCaat/playwright-with-fingerprints/issues) section of the repository. 847 | 848 | Please describe the problem in as much detail as possible when creating tickets - indicate the sequence of actions (steps) to repeat the problem, error output, and so on. 849 | 850 | If the code is large, attach it as files or use sandboxes. At the same time, it's better to remove from it areas that do not relate to the problem - it will be much easier to figure it out. 851 | Format your code and wrap it in special **markdown** tags if you're adding it to an issue report, for example: 852 | 853 | ```js 854 | // your code 855 | ``` 856 | 857 | Please be careful not to attach various **secrets** in your code or screenshots - for example, fingerprint service keys, account passwords, and so on. 858 | In the case of service keys, this can lead to their blocking without a refund. 859 | 860 | If the recommendations are not followed, your ticket may be ignored. 861 | 862 | ## Testing 863 | 864 | The excellent [mocha](https://github.com/mochajs/mocha) framework is used for tests in this library. 865 | Use the command line or ready-made scripts if you want to run them yourself. 866 | 867 | You can also use the **FINGERPRINT_CWD** environment variable to specify the directory where the engine will be stored, for example: 868 | 869 | ```properties 870 | FINGERPRINT_CWD="../plugin-engine" 871 | ``` 872 | 873 | The **FINGERPRINT_TIMEOUT** variable can be set if it's necessary to change the default timeout for executing engine methods, such as applying or fetching a fingerprint: 874 | 875 | ```properties 876 | FINGERPRINT_TIMEOUT=300000 877 | ``` 878 | 879 | You can define it in any way convenient for you, but by default variables are read from the **env** files using the [dotenv](https://github.com/motdotla/dotenv) library. 880 | 881 | ## License 882 | 883 | Copyright © 2025, [CheshireCaat](https://github.com/CheshireCaat). Released under the [MIT](LICENSE.md) license. 884 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | This folder contains several different examples of using the plugin: 4 | 5 | - The `profiles.js` file - an example of using profiles in conjunction with a plugin. 6 | - The `proxy.js` file - an example of launching a browser with a proxy and additional settings installed, getting an external **IP**. 7 | - The `multithreading.js` file - an example of running multiple browsers at the same time and extracting different data from the site. 8 | - The `stealth.js` file - checking the fingerprint replacement on a site that has an anti-bot system, getting a screenshot with the results. 9 | - The `multiple.js` file - launching several browsers sequentially with different fingerprints, collect some data about the headers and the viewport size. 10 | 11 | To run them, install the necessary dependencies and use the contents of the files to work. 12 | You can also do this by cloning the repository locally and installing the dependencies. 13 | 14 | ## Configuration 15 | 16 | You can configure the fingerprint service key and proxy directly in the code, or through environment variables: 17 | 18 | ```shell 19 | FINGERPRINT_KEY="SERVICE_KEY" 20 | FINGERPRINT_PROXY="socks5://127.0.0.1:9762" 21 | ``` 22 | 23 | Environment variables can be set directly or using the `.env` file. 24 | -------------------------------------------------------------------------------- /examples/multiple.js: -------------------------------------------------------------------------------- 1 | // require('dotenv').config(); 2 | // Replace this import with `require('..')` if you are running the example from the repository: 3 | const { plugin } = require('playwright-with-fingerprints'); 4 | 5 | // Set the service key for the plugin (you can buy it here https://bablosoft.com/directbuy/FingerprintSwitcher/2). 6 | // Leave an empty string to use the free version. 7 | plugin.setServiceKey(process.env.FINGERPRINT_KEY ?? ''); 8 | 9 | (async () => { 10 | for (let i = 0; i < 2; ++i) { 11 | const fingerprint = await plugin.fetch({ tags: ['Microsoft Windows', 'Chrome'] }); 12 | const browser = await plugin.useFingerprint(fingerprint).launch(); 13 | 14 | const page = await browser.newPage(); 15 | await page.goto('https://httpbin.org/headers', { waitUntil: 'domcontentloaded' }); 16 | 17 | const { headers } = JSON.parse(await page.$eval('pre', (pre) => pre.innerText)); 18 | 19 | console.log(`Browser №${i + 1}:`, { 20 | headers: { 21 | userAgent: headers['User-Agent'], 22 | acceptLanguage: headers['Accept-Language'], 23 | }, 24 | viewport: await page.evaluate(() => ({ width: innerWidth, height: innerHeight })), 25 | }); 26 | 27 | await browser.close(); 28 | } 29 | })(); 30 | -------------------------------------------------------------------------------- /examples/multithreading.js: -------------------------------------------------------------------------------- 1 | // require('dotenv').config(); 2 | // Replace this import with `require('..')` if you are running the example from the repository: 3 | const { plugin } = require('playwright-with-fingerprints'); 4 | 5 | // Set the service key for the plugin (you can buy it here https://bablosoft.com/directbuy/FingerprintSwitcher/2). 6 | // Leave an empty string to use the free version. 7 | plugin.setServiceKey(process.env.FINGERPRINT_KEY ?? ''); 8 | 9 | async function main() { 10 | const fingerprint = await plugin.fetch({ tags: ['Microsoft Windows', 'Chrome'] }); 11 | const browser = await plugin.useFingerprint(fingerprint).launch(); 12 | 13 | const page = await browser.newPage(); 14 | const getText = (selector) => page.$eval(selector, (el) => el.innerText); 15 | await page.goto('https://browserleaks.com/javascript', { waitUntil: 'domcontentloaded' }); 16 | 17 | const result = { 18 | screen: { 19 | width: await getText('#js-innerWidth'), 20 | height: await getText('#js-innerHeight'), 21 | }, 22 | userAgent: await getText('#js-userAgent'), 23 | deviceMemory: await getText('#js-deviceMemory'), 24 | hardwareConcurrency: await getText('#js-hardwareConcurrency'), 25 | }; 26 | 27 | await browser.close(); 28 | return result; 29 | } 30 | 31 | Promise.all([...Array(3).keys()].map(main)).then(console.log); 32 | -------------------------------------------------------------------------------- /examples/profiles.js: -------------------------------------------------------------------------------- 1 | // require('dotenv').config(); 2 | // Replace this import with `require('..')` if you are running the example from the repository: 3 | const { plugin } = require('playwright-with-fingerprints'); 4 | 5 | (async () => { 6 | const context = await plugin.launchPersistentContext(`${__dirname}/profile`); 7 | 8 | const page = await context.newPage(); 9 | await page.goto('chrome://version'); 10 | 11 | const el = await page.waitForSelector('#profile_path'); 12 | console.log('Current profile:', await el.evaluate((el) => el.textContent)); 13 | 14 | await context.close(); 15 | })(); 16 | -------------------------------------------------------------------------------- /examples/proxy.js: -------------------------------------------------------------------------------- 1 | // require('dotenv').config(); 2 | // Replace this import with `require('..')` if you are running the example from the repository: 3 | const { plugin } = require('playwright-with-fingerprints'); 4 | 5 | // The default proxy value is just an example, it won't work. 6 | const proxy = process.env.FINGERPRINT_PROXY ?? 'socks5://127.0.0.1:9762'; 7 | 8 | (async () => { 9 | plugin.useProxy(proxy, { 10 | detectExternalIP: false, 11 | changeGeolocation: true, 12 | }); 13 | 14 | const browser = await plugin.launch({ 15 | // This argument will be ignored if the `useProxy` method has been called. 16 | args: [`--proxy-server=${proxy}`], 17 | }); 18 | 19 | const page = await browser.newPage(); 20 | await page.goto('https://canhazip.com/', { waitUntil: 'domcontentloaded' }); 21 | 22 | const result = await page.evaluate(() => document.body.innerText.trim()); 23 | console.log('External IP:', result); 24 | 25 | await browser.close(); 26 | })(); 27 | -------------------------------------------------------------------------------- /examples/stealth.js: -------------------------------------------------------------------------------- 1 | // require('dotenv').config(); 2 | // Replace this import with `require('..')` if you are running the example from the repository: 3 | const { plugin } = require('playwright-with-fingerprints'); 4 | 5 | // Set the service key for the plugin (you can buy it here https://bablosoft.com/directbuy/FingerprintSwitcher/2). 6 | // Leave an empty string to use the free version. 7 | plugin.setServiceKey(process.env.FINGERPRINT_KEY ?? ''); 8 | 9 | (async () => { 10 | const fingerprint = await plugin.fetch({ tags: ['Microsoft Windows', 'Chrome'] }); 11 | const browser = await plugin.useFingerprint(fingerprint).launch({ headless: false }); 12 | 13 | const page = await browser.newPage(); 14 | 15 | await page.goto('https://bot.sannysoft.com/'); 16 | await new Promise((fn) => setTimeout(fn, 5000)); 17 | 18 | await page.screenshot({ path: `${__dirname}/stealth.png`, fullPage: true }); 19 | await browser.close(); 20 | })(); 21 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playwright-with-fingerprints", 3 | "version": "2.2.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "playwright-with-fingerprints", 9 | "version": "2.2.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "browser-with-fingerprints": "2.2.0" 13 | }, 14 | "devDependencies": { 15 | "@cheshire-caat/prettier-config": "^1.0.0", 16 | "dotenv": "^16.4.7", 17 | "mocha": "^11.1.0", 18 | "playwright": "^1.51.1", 19 | "prettier": "^3.5.3" 20 | }, 21 | "engines": { 22 | "node": ">= 18" 23 | }, 24 | "peerDependencies": { 25 | "playwright": ">=1.27.1", 26 | "playwright-core": ">=1.27.1" 27 | }, 28 | "peerDependenciesMeta": { 29 | "playwright": { 30 | "optional": true 31 | }, 32 | "playwright-core": { 33 | "optional": true 34 | } 35 | } 36 | }, 37 | "node_modules/@cheshire-caat/prettier-config": { 38 | "version": "1.0.0", 39 | "resolved": "https://registry.npmjs.org/@cheshire-caat/prettier-config/-/prettier-config-1.0.0.tgz", 40 | "integrity": "sha512-yVSTyfjJZznfUYSvA56OxlM4yasKgT+Fsa2LtshSwSXo65idvctCuVNKDbMRGoFwjaR3SdZhSrW8ihpWTs8kkw==", 41 | "dev": true, 42 | "license": "MIT" 43 | }, 44 | "node_modules/@isaacs/cliui": { 45 | "version": "8.0.2", 46 | "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", 47 | "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", 48 | "dev": true, 49 | "license": "ISC", 50 | "dependencies": { 51 | "string-width": "^5.1.2", 52 | "string-width-cjs": "npm:string-width@^4.2.0", 53 | "strip-ansi": "^7.0.1", 54 | "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", 55 | "wrap-ansi": "^8.1.0", 56 | "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" 57 | }, 58 | "engines": { 59 | "node": ">=12" 60 | } 61 | }, 62 | "node_modules/@isaacs/cliui/node_modules/ansi-regex": { 63 | "version": "6.1.0", 64 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", 65 | "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", 66 | "dev": true, 67 | "license": "MIT", 68 | "engines": { 69 | "node": ">=12" 70 | }, 71 | "funding": { 72 | "url": "https://github.com/chalk/ansi-regex?sponsor=1" 73 | } 74 | }, 75 | "node_modules/@isaacs/cliui/node_modules/ansi-styles": { 76 | "version": "6.2.1", 77 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", 78 | "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", 79 | "dev": true, 80 | "license": "MIT", 81 | "engines": { 82 | "node": ">=12" 83 | }, 84 | "funding": { 85 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 86 | } 87 | }, 88 | "node_modules/@isaacs/cliui/node_modules/emoji-regex": { 89 | "version": "9.2.2", 90 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", 91 | "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", 92 | "dev": true, 93 | "license": "MIT" 94 | }, 95 | "node_modules/@isaacs/cliui/node_modules/string-width": { 96 | "version": "5.1.2", 97 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", 98 | "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", 99 | "dev": true, 100 | "license": "MIT", 101 | "dependencies": { 102 | "eastasianwidth": "^0.2.0", 103 | "emoji-regex": "^9.2.2", 104 | "strip-ansi": "^7.0.1" 105 | }, 106 | "engines": { 107 | "node": ">=12" 108 | }, 109 | "funding": { 110 | "url": "https://github.com/sponsors/sindresorhus" 111 | } 112 | }, 113 | "node_modules/@isaacs/cliui/node_modules/strip-ansi": { 114 | "version": "7.1.0", 115 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", 116 | "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", 117 | "dev": true, 118 | "license": "MIT", 119 | "dependencies": { 120 | "ansi-regex": "^6.0.1" 121 | }, 122 | "engines": { 123 | "node": ">=12" 124 | }, 125 | "funding": { 126 | "url": "https://github.com/chalk/strip-ansi?sponsor=1" 127 | } 128 | }, 129 | "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { 130 | "version": "8.1.0", 131 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", 132 | "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", 133 | "dev": true, 134 | "license": "MIT", 135 | "dependencies": { 136 | "ansi-styles": "^6.1.0", 137 | "string-width": "^5.0.1", 138 | "strip-ansi": "^7.0.1" 139 | }, 140 | "engines": { 141 | "node": ">=12" 142 | }, 143 | "funding": { 144 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 145 | } 146 | }, 147 | "node_modules/@nodelib/fs.scandir": { 148 | "version": "2.1.5", 149 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 150 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 151 | "license": "MIT", 152 | "dependencies": { 153 | "@nodelib/fs.stat": "2.0.5", 154 | "run-parallel": "^1.1.9" 155 | }, 156 | "engines": { 157 | "node": ">= 8" 158 | } 159 | }, 160 | "node_modules/@nodelib/fs.stat": { 161 | "version": "2.0.5", 162 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 163 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 164 | "license": "MIT", 165 | "engines": { 166 | "node": ">= 8" 167 | } 168 | }, 169 | "node_modules/@nodelib/fs.walk": { 170 | "version": "1.2.8", 171 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 172 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 173 | "license": "MIT", 174 | "dependencies": { 175 | "@nodelib/fs.scandir": "2.1.5", 176 | "fastq": "^1.6.0" 177 | }, 178 | "engines": { 179 | "node": ">= 8" 180 | } 181 | }, 182 | "node_modules/@pkgjs/parseargs": { 183 | "version": "0.11.0", 184 | "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", 185 | "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", 186 | "dev": true, 187 | "license": "MIT", 188 | "optional": true, 189 | "engines": { 190 | "node": ">=14" 191 | } 192 | }, 193 | "node_modules/@types/node": { 194 | "version": "22.7.4", 195 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", 196 | "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", 197 | "license": "MIT", 198 | "optional": true, 199 | "dependencies": { 200 | "undici-types": "~6.19.2" 201 | } 202 | }, 203 | "node_modules/@types/yauzl": { 204 | "version": "2.10.3", 205 | "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", 206 | "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", 207 | "license": "MIT", 208 | "optional": true, 209 | "dependencies": { 210 | "@types/node": "*" 211 | } 212 | }, 213 | "node_modules/ansi-colors": { 214 | "version": "4.1.3", 215 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", 216 | "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", 217 | "dev": true, 218 | "license": "MIT", 219 | "engines": { 220 | "node": ">=6" 221 | } 222 | }, 223 | "node_modules/ansi-regex": { 224 | "version": "5.0.1", 225 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 226 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 227 | "dev": true, 228 | "license": "MIT", 229 | "engines": { 230 | "node": ">=8" 231 | } 232 | }, 233 | "node_modules/ansi-styles": { 234 | "version": "4.3.0", 235 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 236 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 237 | "dev": true, 238 | "license": "MIT", 239 | "dependencies": { 240 | "color-convert": "^2.0.1" 241 | }, 242 | "engines": { 243 | "node": ">=8" 244 | }, 245 | "funding": { 246 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 247 | } 248 | }, 249 | "node_modules/anymatch": { 250 | "version": "3.1.3", 251 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 252 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 253 | "dev": true, 254 | "license": "ISC", 255 | "dependencies": { 256 | "normalize-path": "^3.0.0", 257 | "picomatch": "^2.0.4" 258 | }, 259 | "engines": { 260 | "node": ">= 8" 261 | } 262 | }, 263 | "node_modules/argparse": { 264 | "version": "2.0.1", 265 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 266 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 267 | "dev": true, 268 | "license": "Python-2.0" 269 | }, 270 | "node_modules/async-lock": { 271 | "version": "1.4.1", 272 | "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", 273 | "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", 274 | "license": "MIT" 275 | }, 276 | "node_modules/asynckit": { 277 | "version": "0.4.0", 278 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 279 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", 280 | "license": "MIT" 281 | }, 282 | "node_modules/axios": { 283 | "version": "1.8.4", 284 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", 285 | "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", 286 | "license": "MIT", 287 | "dependencies": { 288 | "follow-redirects": "^1.15.6", 289 | "form-data": "^4.0.0", 290 | "proxy-from-env": "^1.1.0" 291 | } 292 | }, 293 | "node_modules/balanced-match": { 294 | "version": "1.0.2", 295 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 296 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 297 | "dev": true, 298 | "license": "MIT" 299 | }, 300 | "node_modules/binary-extensions": { 301 | "version": "2.3.0", 302 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", 303 | "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", 304 | "dev": true, 305 | "license": "MIT", 306 | "engines": { 307 | "node": ">=8" 308 | }, 309 | "funding": { 310 | "url": "https://github.com/sponsors/sindresorhus" 311 | } 312 | }, 313 | "node_modules/brace-expansion": { 314 | "version": "2.0.1", 315 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 316 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 317 | "dev": true, 318 | "license": "MIT", 319 | "dependencies": { 320 | "balanced-match": "^1.0.0" 321 | } 322 | }, 323 | "node_modules/braces": { 324 | "version": "3.0.3", 325 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 326 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 327 | "license": "MIT", 328 | "dependencies": { 329 | "fill-range": "^7.1.1" 330 | }, 331 | "engines": { 332 | "node": ">=8" 333 | } 334 | }, 335 | "node_modules/browser-stdout": { 336 | "version": "1.3.1", 337 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 338 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 339 | "dev": true, 340 | "license": "ISC" 341 | }, 342 | "node_modules/browser-with-fingerprints": { 343 | "version": "2.2.0", 344 | "resolved": "https://registry.npmjs.org/browser-with-fingerprints/-/browser-with-fingerprints-2.2.0.tgz", 345 | "integrity": "sha512-EWsSC0z7sTz8Dg2Nxj84N/vDUr4puGNY2P4tfK/OBwOdNfenfqX9T5ouziq8TzbsiCN44bBceZ518tDZdMnjfQ==", 346 | "cpu": [ 347 | "ia32", 348 | "x64" 349 | ], 350 | "license": "MIT", 351 | "os": [ 352 | "win32" 353 | ], 354 | "dependencies": { 355 | "async-lock": "1.4.1", 356 | "axios": "1.8.4", 357 | "chokidar": "^4.0.3", 358 | "chrome-remote-interface": "0.33.3", 359 | "compare-versions": "6.1.1", 360 | "debug": "4.4.0", 361 | "dedent": "1.5.3", 362 | "extract-zip": "2.0.1", 363 | "fast-glob": "3.3.3", 364 | "once": "1.4.0", 365 | "proper-lockfile": "4.1.2" 366 | }, 367 | "engines": { 368 | "node": ">= 18" 369 | } 370 | }, 371 | "node_modules/browser-with-fingerprints/node_modules/chokidar": { 372 | "version": "4.0.3", 373 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", 374 | "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", 375 | "license": "MIT", 376 | "dependencies": { 377 | "readdirp": "^4.0.1" 378 | }, 379 | "engines": { 380 | "node": ">= 14.16.0" 381 | }, 382 | "funding": { 383 | "url": "https://paulmillr.com/funding/" 384 | } 385 | }, 386 | "node_modules/browser-with-fingerprints/node_modules/readdirp": { 387 | "version": "4.1.1", 388 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", 389 | "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", 390 | "license": "MIT", 391 | "engines": { 392 | "node": ">= 14.18.0" 393 | }, 394 | "funding": { 395 | "type": "individual", 396 | "url": "https://paulmillr.com/funding/" 397 | } 398 | }, 399 | "node_modules/buffer-crc32": { 400 | "version": "0.2.13", 401 | "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", 402 | "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", 403 | "license": "MIT", 404 | "engines": { 405 | "node": "*" 406 | } 407 | }, 408 | "node_modules/call-bind-apply-helpers": { 409 | "version": "1.0.2", 410 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 411 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 412 | "license": "MIT", 413 | "dependencies": { 414 | "es-errors": "^1.3.0", 415 | "function-bind": "^1.1.2" 416 | }, 417 | "engines": { 418 | "node": ">= 0.4" 419 | } 420 | }, 421 | "node_modules/camelcase": { 422 | "version": "6.3.0", 423 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", 424 | "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", 425 | "dev": true, 426 | "license": "MIT", 427 | "engines": { 428 | "node": ">=10" 429 | }, 430 | "funding": { 431 | "url": "https://github.com/sponsors/sindresorhus" 432 | } 433 | }, 434 | "node_modules/chalk": { 435 | "version": "4.1.2", 436 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 437 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 438 | "dev": true, 439 | "license": "MIT", 440 | "dependencies": { 441 | "ansi-styles": "^4.1.0", 442 | "supports-color": "^7.1.0" 443 | }, 444 | "engines": { 445 | "node": ">=10" 446 | }, 447 | "funding": { 448 | "url": "https://github.com/chalk/chalk?sponsor=1" 449 | } 450 | }, 451 | "node_modules/chalk/node_modules/supports-color": { 452 | "version": "7.2.0", 453 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 454 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 455 | "dev": true, 456 | "license": "MIT", 457 | "dependencies": { 458 | "has-flag": "^4.0.0" 459 | }, 460 | "engines": { 461 | "node": ">=8" 462 | } 463 | }, 464 | "node_modules/chokidar": { 465 | "version": "3.6.0", 466 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", 467 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", 468 | "dev": true, 469 | "license": "MIT", 470 | "dependencies": { 471 | "anymatch": "~3.1.2", 472 | "braces": "~3.0.2", 473 | "glob-parent": "~5.1.2", 474 | "is-binary-path": "~2.1.0", 475 | "is-glob": "~4.0.1", 476 | "normalize-path": "~3.0.0", 477 | "readdirp": "~3.6.0" 478 | }, 479 | "engines": { 480 | "node": ">= 8.10.0" 481 | }, 482 | "funding": { 483 | "url": "https://paulmillr.com/funding/" 484 | }, 485 | "optionalDependencies": { 486 | "fsevents": "~2.3.2" 487 | } 488 | }, 489 | "node_modules/chrome-remote-interface": { 490 | "version": "0.33.3", 491 | "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.33.3.tgz", 492 | "integrity": "sha512-zNnn0prUL86Teru6UCAZ1yU1XeXljHl3gj7OrfPcarEfU62OUU4IujDPdTDW3dAWwRqN3ZMG/Chhkh2gPL/wiw==", 493 | "license": "MIT", 494 | "dependencies": { 495 | "commander": "2.11.x", 496 | "ws": "^7.2.0" 497 | }, 498 | "bin": { 499 | "chrome-remote-interface": "bin/client.js" 500 | } 501 | }, 502 | "node_modules/cliui": { 503 | "version": "8.0.1", 504 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 505 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 506 | "dev": true, 507 | "license": "ISC", 508 | "dependencies": { 509 | "string-width": "^4.2.0", 510 | "strip-ansi": "^6.0.1", 511 | "wrap-ansi": "^7.0.0" 512 | }, 513 | "engines": { 514 | "node": ">=12" 515 | } 516 | }, 517 | "node_modules/color-convert": { 518 | "version": "2.0.1", 519 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 520 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 521 | "dev": true, 522 | "license": "MIT", 523 | "dependencies": { 524 | "color-name": "~1.1.4" 525 | }, 526 | "engines": { 527 | "node": ">=7.0.0" 528 | } 529 | }, 530 | "node_modules/color-name": { 531 | "version": "1.1.4", 532 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 533 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 534 | "dev": true, 535 | "license": "MIT" 536 | }, 537 | "node_modules/combined-stream": { 538 | "version": "1.0.8", 539 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 540 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 541 | "license": "MIT", 542 | "dependencies": { 543 | "delayed-stream": "~1.0.0" 544 | }, 545 | "engines": { 546 | "node": ">= 0.8" 547 | } 548 | }, 549 | "node_modules/commander": { 550 | "version": "2.11.0", 551 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", 552 | "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", 553 | "license": "MIT" 554 | }, 555 | "node_modules/compare-versions": { 556 | "version": "6.1.1", 557 | "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", 558 | "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", 559 | "license": "MIT" 560 | }, 561 | "node_modules/cross-spawn": { 562 | "version": "7.0.6", 563 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 564 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 565 | "dev": true, 566 | "license": "MIT", 567 | "dependencies": { 568 | "path-key": "^3.1.0", 569 | "shebang-command": "^2.0.0", 570 | "which": "^2.0.1" 571 | }, 572 | "engines": { 573 | "node": ">= 8" 574 | } 575 | }, 576 | "node_modules/debug": { 577 | "version": "4.4.0", 578 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 579 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 580 | "license": "MIT", 581 | "dependencies": { 582 | "ms": "^2.1.3" 583 | }, 584 | "engines": { 585 | "node": ">=6.0" 586 | }, 587 | "peerDependenciesMeta": { 588 | "supports-color": { 589 | "optional": true 590 | } 591 | } 592 | }, 593 | "node_modules/decamelize": { 594 | "version": "4.0.0", 595 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", 596 | "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", 597 | "dev": true, 598 | "license": "MIT", 599 | "engines": { 600 | "node": ">=10" 601 | }, 602 | "funding": { 603 | "url": "https://github.com/sponsors/sindresorhus" 604 | } 605 | }, 606 | "node_modules/dedent": { 607 | "version": "1.5.3", 608 | "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", 609 | "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", 610 | "license": "MIT", 611 | "peerDependencies": { 612 | "babel-plugin-macros": "^3.1.0" 613 | }, 614 | "peerDependenciesMeta": { 615 | "babel-plugin-macros": { 616 | "optional": true 617 | } 618 | } 619 | }, 620 | "node_modules/delayed-stream": { 621 | "version": "1.0.0", 622 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 623 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 624 | "license": "MIT", 625 | "engines": { 626 | "node": ">=0.4.0" 627 | } 628 | }, 629 | "node_modules/diff": { 630 | "version": "5.2.0", 631 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", 632 | "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", 633 | "dev": true, 634 | "license": "BSD-3-Clause", 635 | "engines": { 636 | "node": ">=0.3.1" 637 | } 638 | }, 639 | "node_modules/dotenv": { 640 | "version": "16.4.7", 641 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", 642 | "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", 643 | "dev": true, 644 | "license": "BSD-2-Clause", 645 | "engines": { 646 | "node": ">=12" 647 | }, 648 | "funding": { 649 | "url": "https://dotenvx.com" 650 | } 651 | }, 652 | "node_modules/dunder-proto": { 653 | "version": "1.0.1", 654 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 655 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 656 | "license": "MIT", 657 | "dependencies": { 658 | "call-bind-apply-helpers": "^1.0.1", 659 | "es-errors": "^1.3.0", 660 | "gopd": "^1.2.0" 661 | }, 662 | "engines": { 663 | "node": ">= 0.4" 664 | } 665 | }, 666 | "node_modules/eastasianwidth": { 667 | "version": "0.2.0", 668 | "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", 669 | "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", 670 | "dev": true, 671 | "license": "MIT" 672 | }, 673 | "node_modules/emoji-regex": { 674 | "version": "8.0.0", 675 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 676 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 677 | "dev": true, 678 | "license": "MIT" 679 | }, 680 | "node_modules/end-of-stream": { 681 | "version": "1.4.4", 682 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 683 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 684 | "license": "MIT", 685 | "dependencies": { 686 | "once": "^1.4.0" 687 | } 688 | }, 689 | "node_modules/es-define-property": { 690 | "version": "1.0.1", 691 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 692 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 693 | "license": "MIT", 694 | "engines": { 695 | "node": ">= 0.4" 696 | } 697 | }, 698 | "node_modules/es-errors": { 699 | "version": "1.3.0", 700 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 701 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 702 | "license": "MIT", 703 | "engines": { 704 | "node": ">= 0.4" 705 | } 706 | }, 707 | "node_modules/es-object-atoms": { 708 | "version": "1.1.1", 709 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 710 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 711 | "license": "MIT", 712 | "dependencies": { 713 | "es-errors": "^1.3.0" 714 | }, 715 | "engines": { 716 | "node": ">= 0.4" 717 | } 718 | }, 719 | "node_modules/es-set-tostringtag": { 720 | "version": "2.1.0", 721 | "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", 722 | "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", 723 | "license": "MIT", 724 | "dependencies": { 725 | "es-errors": "^1.3.0", 726 | "get-intrinsic": "^1.2.6", 727 | "has-tostringtag": "^1.0.2", 728 | "hasown": "^2.0.2" 729 | }, 730 | "engines": { 731 | "node": ">= 0.4" 732 | } 733 | }, 734 | "node_modules/escalade": { 735 | "version": "3.2.0", 736 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", 737 | "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", 738 | "dev": true, 739 | "license": "MIT", 740 | "engines": { 741 | "node": ">=6" 742 | } 743 | }, 744 | "node_modules/escape-string-regexp": { 745 | "version": "4.0.0", 746 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 747 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 748 | "dev": true, 749 | "license": "MIT", 750 | "engines": { 751 | "node": ">=10" 752 | }, 753 | "funding": { 754 | "url": "https://github.com/sponsors/sindresorhus" 755 | } 756 | }, 757 | "node_modules/extract-zip": { 758 | "version": "2.0.1", 759 | "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", 760 | "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", 761 | "license": "BSD-2-Clause", 762 | "dependencies": { 763 | "debug": "^4.1.1", 764 | "get-stream": "^5.1.0", 765 | "yauzl": "^2.10.0" 766 | }, 767 | "bin": { 768 | "extract-zip": "cli.js" 769 | }, 770 | "engines": { 771 | "node": ">= 10.17.0" 772 | }, 773 | "optionalDependencies": { 774 | "@types/yauzl": "^2.9.1" 775 | } 776 | }, 777 | "node_modules/fast-glob": { 778 | "version": "3.3.3", 779 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", 780 | "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", 781 | "license": "MIT", 782 | "dependencies": { 783 | "@nodelib/fs.stat": "^2.0.2", 784 | "@nodelib/fs.walk": "^1.2.3", 785 | "glob-parent": "^5.1.2", 786 | "merge2": "^1.3.0", 787 | "micromatch": "^4.0.8" 788 | }, 789 | "engines": { 790 | "node": ">=8.6.0" 791 | } 792 | }, 793 | "node_modules/fastq": { 794 | "version": "1.18.0", 795 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", 796 | "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", 797 | "license": "ISC", 798 | "dependencies": { 799 | "reusify": "^1.0.4" 800 | } 801 | }, 802 | "node_modules/fd-slicer": { 803 | "version": "1.1.0", 804 | "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", 805 | "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", 806 | "license": "MIT", 807 | "dependencies": { 808 | "pend": "~1.2.0" 809 | } 810 | }, 811 | "node_modules/fill-range": { 812 | "version": "7.1.1", 813 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 814 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 815 | "license": "MIT", 816 | "dependencies": { 817 | "to-regex-range": "^5.0.1" 818 | }, 819 | "engines": { 820 | "node": ">=8" 821 | } 822 | }, 823 | "node_modules/find-up": { 824 | "version": "5.0.0", 825 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 826 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 827 | "dev": true, 828 | "license": "MIT", 829 | "dependencies": { 830 | "locate-path": "^6.0.0", 831 | "path-exists": "^4.0.0" 832 | }, 833 | "engines": { 834 | "node": ">=10" 835 | }, 836 | "funding": { 837 | "url": "https://github.com/sponsors/sindresorhus" 838 | } 839 | }, 840 | "node_modules/flat": { 841 | "version": "5.0.2", 842 | "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", 843 | "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", 844 | "dev": true, 845 | "license": "BSD-3-Clause", 846 | "bin": { 847 | "flat": "cli.js" 848 | } 849 | }, 850 | "node_modules/follow-redirects": { 851 | "version": "1.15.9", 852 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", 853 | "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", 854 | "funding": [ 855 | { 856 | "type": "individual", 857 | "url": "https://github.com/sponsors/RubenVerborgh" 858 | } 859 | ], 860 | "license": "MIT", 861 | "engines": { 862 | "node": ">=4.0" 863 | }, 864 | "peerDependenciesMeta": { 865 | "debug": { 866 | "optional": true 867 | } 868 | } 869 | }, 870 | "node_modules/foreground-child": { 871 | "version": "3.3.0", 872 | "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", 873 | "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", 874 | "dev": true, 875 | "license": "ISC", 876 | "dependencies": { 877 | "cross-spawn": "^7.0.0", 878 | "signal-exit": "^4.0.1" 879 | }, 880 | "engines": { 881 | "node": ">=14" 882 | }, 883 | "funding": { 884 | "url": "https://github.com/sponsors/isaacs" 885 | } 886 | }, 887 | "node_modules/foreground-child/node_modules/signal-exit": { 888 | "version": "4.1.0", 889 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", 890 | "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", 891 | "dev": true, 892 | "license": "ISC", 893 | "engines": { 894 | "node": ">=14" 895 | }, 896 | "funding": { 897 | "url": "https://github.com/sponsors/isaacs" 898 | } 899 | }, 900 | "node_modules/form-data": { 901 | "version": "4.0.2", 902 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", 903 | "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", 904 | "license": "MIT", 905 | "dependencies": { 906 | "asynckit": "^0.4.0", 907 | "combined-stream": "^1.0.8", 908 | "es-set-tostringtag": "^2.1.0", 909 | "mime-types": "^2.1.12" 910 | }, 911 | "engines": { 912 | "node": ">= 6" 913 | } 914 | }, 915 | "node_modules/fsevents": { 916 | "version": "2.3.2", 917 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 918 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 919 | "dev": true, 920 | "hasInstallScript": true, 921 | "license": "MIT", 922 | "optional": true, 923 | "os": [ 924 | "darwin" 925 | ], 926 | "engines": { 927 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 928 | } 929 | }, 930 | "node_modules/function-bind": { 931 | "version": "1.1.2", 932 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 933 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 934 | "license": "MIT", 935 | "funding": { 936 | "url": "https://github.com/sponsors/ljharb" 937 | } 938 | }, 939 | "node_modules/get-caller-file": { 940 | "version": "2.0.5", 941 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 942 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 943 | "dev": true, 944 | "license": "ISC", 945 | "engines": { 946 | "node": "6.* || 8.* || >= 10.*" 947 | } 948 | }, 949 | "node_modules/get-intrinsic": { 950 | "version": "1.3.0", 951 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 952 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 953 | "license": "MIT", 954 | "dependencies": { 955 | "call-bind-apply-helpers": "^1.0.2", 956 | "es-define-property": "^1.0.1", 957 | "es-errors": "^1.3.0", 958 | "es-object-atoms": "^1.1.1", 959 | "function-bind": "^1.1.2", 960 | "get-proto": "^1.0.1", 961 | "gopd": "^1.2.0", 962 | "has-symbols": "^1.1.0", 963 | "hasown": "^2.0.2", 964 | "math-intrinsics": "^1.1.0" 965 | }, 966 | "engines": { 967 | "node": ">= 0.4" 968 | }, 969 | "funding": { 970 | "url": "https://github.com/sponsors/ljharb" 971 | } 972 | }, 973 | "node_modules/get-proto": { 974 | "version": "1.0.1", 975 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 976 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 977 | "license": "MIT", 978 | "dependencies": { 979 | "dunder-proto": "^1.0.1", 980 | "es-object-atoms": "^1.0.0" 981 | }, 982 | "engines": { 983 | "node": ">= 0.4" 984 | } 985 | }, 986 | "node_modules/get-stream": { 987 | "version": "5.2.0", 988 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", 989 | "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", 990 | "license": "MIT", 991 | "dependencies": { 992 | "pump": "^3.0.0" 993 | }, 994 | "engines": { 995 | "node": ">=8" 996 | }, 997 | "funding": { 998 | "url": "https://github.com/sponsors/sindresorhus" 999 | } 1000 | }, 1001 | "node_modules/glob": { 1002 | "version": "10.4.5", 1003 | "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", 1004 | "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", 1005 | "dev": true, 1006 | "license": "ISC", 1007 | "dependencies": { 1008 | "foreground-child": "^3.1.0", 1009 | "jackspeak": "^3.1.2", 1010 | "minimatch": "^9.0.4", 1011 | "minipass": "^7.1.2", 1012 | "package-json-from-dist": "^1.0.0", 1013 | "path-scurry": "^1.11.1" 1014 | }, 1015 | "bin": { 1016 | "glob": "dist/esm/bin.mjs" 1017 | }, 1018 | "funding": { 1019 | "url": "https://github.com/sponsors/isaacs" 1020 | } 1021 | }, 1022 | "node_modules/glob-parent": { 1023 | "version": "5.1.2", 1024 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 1025 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 1026 | "license": "ISC", 1027 | "dependencies": { 1028 | "is-glob": "^4.0.1" 1029 | }, 1030 | "engines": { 1031 | "node": ">= 6" 1032 | } 1033 | }, 1034 | "node_modules/glob/node_modules/minimatch": { 1035 | "version": "9.0.5", 1036 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", 1037 | "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", 1038 | "dev": true, 1039 | "license": "ISC", 1040 | "dependencies": { 1041 | "brace-expansion": "^2.0.1" 1042 | }, 1043 | "engines": { 1044 | "node": ">=16 || 14 >=14.17" 1045 | }, 1046 | "funding": { 1047 | "url": "https://github.com/sponsors/isaacs" 1048 | } 1049 | }, 1050 | "node_modules/gopd": { 1051 | "version": "1.2.0", 1052 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 1053 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 1054 | "license": "MIT", 1055 | "engines": { 1056 | "node": ">= 0.4" 1057 | }, 1058 | "funding": { 1059 | "url": "https://github.com/sponsors/ljharb" 1060 | } 1061 | }, 1062 | "node_modules/graceful-fs": { 1063 | "version": "4.2.11", 1064 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 1065 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", 1066 | "license": "ISC" 1067 | }, 1068 | "node_modules/has-flag": { 1069 | "version": "4.0.0", 1070 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 1071 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 1072 | "dev": true, 1073 | "license": "MIT", 1074 | "engines": { 1075 | "node": ">=8" 1076 | } 1077 | }, 1078 | "node_modules/has-symbols": { 1079 | "version": "1.1.0", 1080 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 1081 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 1082 | "license": "MIT", 1083 | "engines": { 1084 | "node": ">= 0.4" 1085 | }, 1086 | "funding": { 1087 | "url": "https://github.com/sponsors/ljharb" 1088 | } 1089 | }, 1090 | "node_modules/has-tostringtag": { 1091 | "version": "1.0.2", 1092 | "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", 1093 | "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", 1094 | "license": "MIT", 1095 | "dependencies": { 1096 | "has-symbols": "^1.0.3" 1097 | }, 1098 | "engines": { 1099 | "node": ">= 0.4" 1100 | }, 1101 | "funding": { 1102 | "url": "https://github.com/sponsors/ljharb" 1103 | } 1104 | }, 1105 | "node_modules/hasown": { 1106 | "version": "2.0.2", 1107 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 1108 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 1109 | "license": "MIT", 1110 | "dependencies": { 1111 | "function-bind": "^1.1.2" 1112 | }, 1113 | "engines": { 1114 | "node": ">= 0.4" 1115 | } 1116 | }, 1117 | "node_modules/he": { 1118 | "version": "1.2.0", 1119 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 1120 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 1121 | "dev": true, 1122 | "license": "MIT", 1123 | "bin": { 1124 | "he": "bin/he" 1125 | } 1126 | }, 1127 | "node_modules/is-binary-path": { 1128 | "version": "2.1.0", 1129 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 1130 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 1131 | "dev": true, 1132 | "license": "MIT", 1133 | "dependencies": { 1134 | "binary-extensions": "^2.0.0" 1135 | }, 1136 | "engines": { 1137 | "node": ">=8" 1138 | } 1139 | }, 1140 | "node_modules/is-extglob": { 1141 | "version": "2.1.1", 1142 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1143 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 1144 | "license": "MIT", 1145 | "engines": { 1146 | "node": ">=0.10.0" 1147 | } 1148 | }, 1149 | "node_modules/is-fullwidth-code-point": { 1150 | "version": "3.0.0", 1151 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 1152 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 1153 | "dev": true, 1154 | "license": "MIT", 1155 | "engines": { 1156 | "node": ">=8" 1157 | } 1158 | }, 1159 | "node_modules/is-glob": { 1160 | "version": "4.0.3", 1161 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 1162 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 1163 | "license": "MIT", 1164 | "dependencies": { 1165 | "is-extglob": "^2.1.1" 1166 | }, 1167 | "engines": { 1168 | "node": ">=0.10.0" 1169 | } 1170 | }, 1171 | "node_modules/is-number": { 1172 | "version": "7.0.0", 1173 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 1174 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 1175 | "license": "MIT", 1176 | "engines": { 1177 | "node": ">=0.12.0" 1178 | } 1179 | }, 1180 | "node_modules/is-plain-obj": { 1181 | "version": "2.1.0", 1182 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", 1183 | "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", 1184 | "dev": true, 1185 | "license": "MIT", 1186 | "engines": { 1187 | "node": ">=8" 1188 | } 1189 | }, 1190 | "node_modules/is-unicode-supported": { 1191 | "version": "0.1.0", 1192 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 1193 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 1194 | "dev": true, 1195 | "license": "MIT", 1196 | "engines": { 1197 | "node": ">=10" 1198 | }, 1199 | "funding": { 1200 | "url": "https://github.com/sponsors/sindresorhus" 1201 | } 1202 | }, 1203 | "node_modules/isexe": { 1204 | "version": "2.0.0", 1205 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 1206 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 1207 | "dev": true, 1208 | "license": "ISC" 1209 | }, 1210 | "node_modules/jackspeak": { 1211 | "version": "3.4.3", 1212 | "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", 1213 | "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", 1214 | "dev": true, 1215 | "license": "BlueOak-1.0.0", 1216 | "dependencies": { 1217 | "@isaacs/cliui": "^8.0.2" 1218 | }, 1219 | "funding": { 1220 | "url": "https://github.com/sponsors/isaacs" 1221 | }, 1222 | "optionalDependencies": { 1223 | "@pkgjs/parseargs": "^0.11.0" 1224 | } 1225 | }, 1226 | "node_modules/js-yaml": { 1227 | "version": "4.1.0", 1228 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 1229 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 1230 | "dev": true, 1231 | "license": "MIT", 1232 | "dependencies": { 1233 | "argparse": "^2.0.1" 1234 | }, 1235 | "bin": { 1236 | "js-yaml": "bin/js-yaml.js" 1237 | } 1238 | }, 1239 | "node_modules/locate-path": { 1240 | "version": "6.0.0", 1241 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 1242 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 1243 | "dev": true, 1244 | "license": "MIT", 1245 | "dependencies": { 1246 | "p-locate": "^5.0.0" 1247 | }, 1248 | "engines": { 1249 | "node": ">=10" 1250 | }, 1251 | "funding": { 1252 | "url": "https://github.com/sponsors/sindresorhus" 1253 | } 1254 | }, 1255 | "node_modules/log-symbols": { 1256 | "version": "4.1.0", 1257 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 1258 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 1259 | "dev": true, 1260 | "license": "MIT", 1261 | "dependencies": { 1262 | "chalk": "^4.1.0", 1263 | "is-unicode-supported": "^0.1.0" 1264 | }, 1265 | "engines": { 1266 | "node": ">=10" 1267 | }, 1268 | "funding": { 1269 | "url": "https://github.com/sponsors/sindresorhus" 1270 | } 1271 | }, 1272 | "node_modules/lru-cache": { 1273 | "version": "10.4.3", 1274 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", 1275 | "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", 1276 | "dev": true, 1277 | "license": "ISC" 1278 | }, 1279 | "node_modules/math-intrinsics": { 1280 | "version": "1.1.0", 1281 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 1282 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 1283 | "license": "MIT", 1284 | "engines": { 1285 | "node": ">= 0.4" 1286 | } 1287 | }, 1288 | "node_modules/merge2": { 1289 | "version": "1.4.1", 1290 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 1291 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 1292 | "license": "MIT", 1293 | "engines": { 1294 | "node": ">= 8" 1295 | } 1296 | }, 1297 | "node_modules/micromatch": { 1298 | "version": "4.0.8", 1299 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", 1300 | "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", 1301 | "license": "MIT", 1302 | "dependencies": { 1303 | "braces": "^3.0.3", 1304 | "picomatch": "^2.3.1" 1305 | }, 1306 | "engines": { 1307 | "node": ">=8.6" 1308 | } 1309 | }, 1310 | "node_modules/mime-db": { 1311 | "version": "1.52.0", 1312 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 1313 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 1314 | "license": "MIT", 1315 | "engines": { 1316 | "node": ">= 0.6" 1317 | } 1318 | }, 1319 | "node_modules/mime-types": { 1320 | "version": "2.1.35", 1321 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 1322 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 1323 | "license": "MIT", 1324 | "dependencies": { 1325 | "mime-db": "1.52.0" 1326 | }, 1327 | "engines": { 1328 | "node": ">= 0.6" 1329 | } 1330 | }, 1331 | "node_modules/minimatch": { 1332 | "version": "5.1.6", 1333 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", 1334 | "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", 1335 | "dev": true, 1336 | "license": "ISC", 1337 | "dependencies": { 1338 | "brace-expansion": "^2.0.1" 1339 | }, 1340 | "engines": { 1341 | "node": ">=10" 1342 | } 1343 | }, 1344 | "node_modules/minipass": { 1345 | "version": "7.1.2", 1346 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", 1347 | "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", 1348 | "dev": true, 1349 | "license": "ISC", 1350 | "engines": { 1351 | "node": ">=16 || 14 >=14.17" 1352 | } 1353 | }, 1354 | "node_modules/mocha": { 1355 | "version": "11.1.0", 1356 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", 1357 | "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", 1358 | "dev": true, 1359 | "license": "MIT", 1360 | "dependencies": { 1361 | "ansi-colors": "^4.1.3", 1362 | "browser-stdout": "^1.3.1", 1363 | "chokidar": "^3.5.3", 1364 | "debug": "^4.3.5", 1365 | "diff": "^5.2.0", 1366 | "escape-string-regexp": "^4.0.0", 1367 | "find-up": "^5.0.0", 1368 | "glob": "^10.4.5", 1369 | "he": "^1.2.0", 1370 | "js-yaml": "^4.1.0", 1371 | "log-symbols": "^4.1.0", 1372 | "minimatch": "^5.1.6", 1373 | "ms": "^2.1.3", 1374 | "serialize-javascript": "^6.0.2", 1375 | "strip-json-comments": "^3.1.1", 1376 | "supports-color": "^8.1.1", 1377 | "workerpool": "^6.5.1", 1378 | "yargs": "^17.7.2", 1379 | "yargs-parser": "^21.1.1", 1380 | "yargs-unparser": "^2.0.0" 1381 | }, 1382 | "bin": { 1383 | "_mocha": "bin/_mocha", 1384 | "mocha": "bin/mocha.js" 1385 | }, 1386 | "engines": { 1387 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1388 | } 1389 | }, 1390 | "node_modules/ms": { 1391 | "version": "2.1.3", 1392 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1393 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1394 | "license": "MIT" 1395 | }, 1396 | "node_modules/normalize-path": { 1397 | "version": "3.0.0", 1398 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1399 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1400 | "dev": true, 1401 | "license": "MIT", 1402 | "engines": { 1403 | "node": ">=0.10.0" 1404 | } 1405 | }, 1406 | "node_modules/once": { 1407 | "version": "1.4.0", 1408 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1409 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 1410 | "license": "ISC", 1411 | "dependencies": { 1412 | "wrappy": "1" 1413 | } 1414 | }, 1415 | "node_modules/p-limit": { 1416 | "version": "3.1.0", 1417 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 1418 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 1419 | "dev": true, 1420 | "license": "MIT", 1421 | "dependencies": { 1422 | "yocto-queue": "^0.1.0" 1423 | }, 1424 | "engines": { 1425 | "node": ">=10" 1426 | }, 1427 | "funding": { 1428 | "url": "https://github.com/sponsors/sindresorhus" 1429 | } 1430 | }, 1431 | "node_modules/p-locate": { 1432 | "version": "5.0.0", 1433 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 1434 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 1435 | "dev": true, 1436 | "license": "MIT", 1437 | "dependencies": { 1438 | "p-limit": "^3.0.2" 1439 | }, 1440 | "engines": { 1441 | "node": ">=10" 1442 | }, 1443 | "funding": { 1444 | "url": "https://github.com/sponsors/sindresorhus" 1445 | } 1446 | }, 1447 | "node_modules/package-json-from-dist": { 1448 | "version": "1.0.1", 1449 | "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", 1450 | "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", 1451 | "dev": true, 1452 | "license": "BlueOak-1.0.0" 1453 | }, 1454 | "node_modules/path-exists": { 1455 | "version": "4.0.0", 1456 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 1457 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 1458 | "dev": true, 1459 | "license": "MIT", 1460 | "engines": { 1461 | "node": ">=8" 1462 | } 1463 | }, 1464 | "node_modules/path-key": { 1465 | "version": "3.1.1", 1466 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 1467 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 1468 | "dev": true, 1469 | "license": "MIT", 1470 | "engines": { 1471 | "node": ">=8" 1472 | } 1473 | }, 1474 | "node_modules/path-scurry": { 1475 | "version": "1.11.1", 1476 | "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", 1477 | "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", 1478 | "dev": true, 1479 | "license": "BlueOak-1.0.0", 1480 | "dependencies": { 1481 | "lru-cache": "^10.2.0", 1482 | "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" 1483 | }, 1484 | "engines": { 1485 | "node": ">=16 || 14 >=14.18" 1486 | }, 1487 | "funding": { 1488 | "url": "https://github.com/sponsors/isaacs" 1489 | } 1490 | }, 1491 | "node_modules/pend": { 1492 | "version": "1.2.0", 1493 | "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", 1494 | "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", 1495 | "license": "MIT" 1496 | }, 1497 | "node_modules/picomatch": { 1498 | "version": "2.3.1", 1499 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1500 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1501 | "license": "MIT", 1502 | "engines": { 1503 | "node": ">=8.6" 1504 | }, 1505 | "funding": { 1506 | "url": "https://github.com/sponsors/jonschlinkert" 1507 | } 1508 | }, 1509 | "node_modules/playwright": { 1510 | "version": "1.51.1", 1511 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz", 1512 | "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==", 1513 | "dev": true, 1514 | "license": "Apache-2.0", 1515 | "dependencies": { 1516 | "playwright-core": "1.51.1" 1517 | }, 1518 | "bin": { 1519 | "playwright": "cli.js" 1520 | }, 1521 | "engines": { 1522 | "node": ">=18" 1523 | }, 1524 | "optionalDependencies": { 1525 | "fsevents": "2.3.2" 1526 | } 1527 | }, 1528 | "node_modules/playwright-core": { 1529 | "version": "1.51.1", 1530 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz", 1531 | "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==", 1532 | "devOptional": true, 1533 | "license": "Apache-2.0", 1534 | "bin": { 1535 | "playwright-core": "cli.js" 1536 | }, 1537 | "engines": { 1538 | "node": ">=18" 1539 | } 1540 | }, 1541 | "node_modules/prettier": { 1542 | "version": "3.5.3", 1543 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", 1544 | "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", 1545 | "dev": true, 1546 | "license": "MIT", 1547 | "bin": { 1548 | "prettier": "bin/prettier.cjs" 1549 | }, 1550 | "engines": { 1551 | "node": ">=14" 1552 | }, 1553 | "funding": { 1554 | "url": "https://github.com/prettier/prettier?sponsor=1" 1555 | } 1556 | }, 1557 | "node_modules/proper-lockfile": { 1558 | "version": "4.1.2", 1559 | "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", 1560 | "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", 1561 | "license": "MIT", 1562 | "dependencies": { 1563 | "graceful-fs": "^4.2.4", 1564 | "retry": "^0.12.0", 1565 | "signal-exit": "^3.0.2" 1566 | } 1567 | }, 1568 | "node_modules/proxy-from-env": { 1569 | "version": "1.1.0", 1570 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 1571 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", 1572 | "license": "MIT" 1573 | }, 1574 | "node_modules/pump": { 1575 | "version": "3.0.2", 1576 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", 1577 | "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", 1578 | "license": "MIT", 1579 | "dependencies": { 1580 | "end-of-stream": "^1.1.0", 1581 | "once": "^1.3.1" 1582 | } 1583 | }, 1584 | "node_modules/queue-microtask": { 1585 | "version": "1.2.3", 1586 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 1587 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 1588 | "funding": [ 1589 | { 1590 | "type": "github", 1591 | "url": "https://github.com/sponsors/feross" 1592 | }, 1593 | { 1594 | "type": "patreon", 1595 | "url": "https://www.patreon.com/feross" 1596 | }, 1597 | { 1598 | "type": "consulting", 1599 | "url": "https://feross.org/support" 1600 | } 1601 | ], 1602 | "license": "MIT" 1603 | }, 1604 | "node_modules/randombytes": { 1605 | "version": "2.1.0", 1606 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 1607 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 1608 | "dev": true, 1609 | "license": "MIT", 1610 | "dependencies": { 1611 | "safe-buffer": "^5.1.0" 1612 | } 1613 | }, 1614 | "node_modules/readdirp": { 1615 | "version": "3.6.0", 1616 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1617 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1618 | "dev": true, 1619 | "license": "MIT", 1620 | "dependencies": { 1621 | "picomatch": "^2.2.1" 1622 | }, 1623 | "engines": { 1624 | "node": ">=8.10.0" 1625 | } 1626 | }, 1627 | "node_modules/require-directory": { 1628 | "version": "2.1.1", 1629 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 1630 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 1631 | "dev": true, 1632 | "license": "MIT", 1633 | "engines": { 1634 | "node": ">=0.10.0" 1635 | } 1636 | }, 1637 | "node_modules/retry": { 1638 | "version": "0.12.0", 1639 | "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", 1640 | "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", 1641 | "license": "MIT", 1642 | "engines": { 1643 | "node": ">= 4" 1644 | } 1645 | }, 1646 | "node_modules/reusify": { 1647 | "version": "1.0.4", 1648 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 1649 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 1650 | "license": "MIT", 1651 | "engines": { 1652 | "iojs": ">=1.0.0", 1653 | "node": ">=0.10.0" 1654 | } 1655 | }, 1656 | "node_modules/run-parallel": { 1657 | "version": "1.2.0", 1658 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 1659 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 1660 | "funding": [ 1661 | { 1662 | "type": "github", 1663 | "url": "https://github.com/sponsors/feross" 1664 | }, 1665 | { 1666 | "type": "patreon", 1667 | "url": "https://www.patreon.com/feross" 1668 | }, 1669 | { 1670 | "type": "consulting", 1671 | "url": "https://feross.org/support" 1672 | } 1673 | ], 1674 | "license": "MIT", 1675 | "dependencies": { 1676 | "queue-microtask": "^1.2.2" 1677 | } 1678 | }, 1679 | "node_modules/safe-buffer": { 1680 | "version": "5.2.1", 1681 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1682 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1683 | "dev": true, 1684 | "funding": [ 1685 | { 1686 | "type": "github", 1687 | "url": "https://github.com/sponsors/feross" 1688 | }, 1689 | { 1690 | "type": "patreon", 1691 | "url": "https://www.patreon.com/feross" 1692 | }, 1693 | { 1694 | "type": "consulting", 1695 | "url": "https://feross.org/support" 1696 | } 1697 | ], 1698 | "license": "MIT" 1699 | }, 1700 | "node_modules/serialize-javascript": { 1701 | "version": "6.0.2", 1702 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", 1703 | "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", 1704 | "dev": true, 1705 | "license": "BSD-3-Clause", 1706 | "dependencies": { 1707 | "randombytes": "^2.1.0" 1708 | } 1709 | }, 1710 | "node_modules/shebang-command": { 1711 | "version": "2.0.0", 1712 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1713 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1714 | "dev": true, 1715 | "license": "MIT", 1716 | "dependencies": { 1717 | "shebang-regex": "^3.0.0" 1718 | }, 1719 | "engines": { 1720 | "node": ">=8" 1721 | } 1722 | }, 1723 | "node_modules/shebang-regex": { 1724 | "version": "3.0.0", 1725 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1726 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 1727 | "dev": true, 1728 | "license": "MIT", 1729 | "engines": { 1730 | "node": ">=8" 1731 | } 1732 | }, 1733 | "node_modules/signal-exit": { 1734 | "version": "3.0.7", 1735 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", 1736 | "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", 1737 | "license": "ISC" 1738 | }, 1739 | "node_modules/string-width": { 1740 | "version": "4.2.3", 1741 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1742 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1743 | "dev": true, 1744 | "license": "MIT", 1745 | "dependencies": { 1746 | "emoji-regex": "^8.0.0", 1747 | "is-fullwidth-code-point": "^3.0.0", 1748 | "strip-ansi": "^6.0.1" 1749 | }, 1750 | "engines": { 1751 | "node": ">=8" 1752 | } 1753 | }, 1754 | "node_modules/string-width-cjs": { 1755 | "name": "string-width", 1756 | "version": "4.2.3", 1757 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1758 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1759 | "dev": true, 1760 | "license": "MIT", 1761 | "dependencies": { 1762 | "emoji-regex": "^8.0.0", 1763 | "is-fullwidth-code-point": "^3.0.0", 1764 | "strip-ansi": "^6.0.1" 1765 | }, 1766 | "engines": { 1767 | "node": ">=8" 1768 | } 1769 | }, 1770 | "node_modules/strip-ansi": { 1771 | "version": "6.0.1", 1772 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1773 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1774 | "dev": true, 1775 | "license": "MIT", 1776 | "dependencies": { 1777 | "ansi-regex": "^5.0.1" 1778 | }, 1779 | "engines": { 1780 | "node": ">=8" 1781 | } 1782 | }, 1783 | "node_modules/strip-ansi-cjs": { 1784 | "name": "strip-ansi", 1785 | "version": "6.0.1", 1786 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1787 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1788 | "dev": true, 1789 | "license": "MIT", 1790 | "dependencies": { 1791 | "ansi-regex": "^5.0.1" 1792 | }, 1793 | "engines": { 1794 | "node": ">=8" 1795 | } 1796 | }, 1797 | "node_modules/strip-json-comments": { 1798 | "version": "3.1.1", 1799 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1800 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1801 | "dev": true, 1802 | "license": "MIT", 1803 | "engines": { 1804 | "node": ">=8" 1805 | }, 1806 | "funding": { 1807 | "url": "https://github.com/sponsors/sindresorhus" 1808 | } 1809 | }, 1810 | "node_modules/supports-color": { 1811 | "version": "8.1.1", 1812 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 1813 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 1814 | "dev": true, 1815 | "license": "MIT", 1816 | "dependencies": { 1817 | "has-flag": "^4.0.0" 1818 | }, 1819 | "engines": { 1820 | "node": ">=10" 1821 | }, 1822 | "funding": { 1823 | "url": "https://github.com/chalk/supports-color?sponsor=1" 1824 | } 1825 | }, 1826 | "node_modules/to-regex-range": { 1827 | "version": "5.0.1", 1828 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1829 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1830 | "license": "MIT", 1831 | "dependencies": { 1832 | "is-number": "^7.0.0" 1833 | }, 1834 | "engines": { 1835 | "node": ">=8.0" 1836 | } 1837 | }, 1838 | "node_modules/undici-types": { 1839 | "version": "6.19.8", 1840 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", 1841 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", 1842 | "license": "MIT", 1843 | "optional": true 1844 | }, 1845 | "node_modules/which": { 1846 | "version": "2.0.2", 1847 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1848 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1849 | "dev": true, 1850 | "license": "ISC", 1851 | "dependencies": { 1852 | "isexe": "^2.0.0" 1853 | }, 1854 | "bin": { 1855 | "node-which": "bin/node-which" 1856 | }, 1857 | "engines": { 1858 | "node": ">= 8" 1859 | } 1860 | }, 1861 | "node_modules/workerpool": { 1862 | "version": "6.5.1", 1863 | "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", 1864 | "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", 1865 | "dev": true, 1866 | "license": "Apache-2.0" 1867 | }, 1868 | "node_modules/wrap-ansi": { 1869 | "version": "7.0.0", 1870 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1871 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1872 | "dev": true, 1873 | "license": "MIT", 1874 | "dependencies": { 1875 | "ansi-styles": "^4.0.0", 1876 | "string-width": "^4.1.0", 1877 | "strip-ansi": "^6.0.0" 1878 | }, 1879 | "engines": { 1880 | "node": ">=10" 1881 | }, 1882 | "funding": { 1883 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1884 | } 1885 | }, 1886 | "node_modules/wrap-ansi-cjs": { 1887 | "name": "wrap-ansi", 1888 | "version": "7.0.0", 1889 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1890 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1891 | "dev": true, 1892 | "license": "MIT", 1893 | "dependencies": { 1894 | "ansi-styles": "^4.0.0", 1895 | "string-width": "^4.1.0", 1896 | "strip-ansi": "^6.0.0" 1897 | }, 1898 | "engines": { 1899 | "node": ">=10" 1900 | }, 1901 | "funding": { 1902 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1903 | } 1904 | }, 1905 | "node_modules/wrappy": { 1906 | "version": "1.0.2", 1907 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1908 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1909 | "license": "ISC" 1910 | }, 1911 | "node_modules/ws": { 1912 | "version": "7.5.10", 1913 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", 1914 | "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", 1915 | "license": "MIT", 1916 | "engines": { 1917 | "node": ">=8.3.0" 1918 | }, 1919 | "peerDependencies": { 1920 | "bufferutil": "^4.0.1", 1921 | "utf-8-validate": "^5.0.2" 1922 | }, 1923 | "peerDependenciesMeta": { 1924 | "bufferutil": { 1925 | "optional": true 1926 | }, 1927 | "utf-8-validate": { 1928 | "optional": true 1929 | } 1930 | } 1931 | }, 1932 | "node_modules/y18n": { 1933 | "version": "5.0.8", 1934 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1935 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 1936 | "dev": true, 1937 | "license": "ISC", 1938 | "engines": { 1939 | "node": ">=10" 1940 | } 1941 | }, 1942 | "node_modules/yargs": { 1943 | "version": "17.7.2", 1944 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", 1945 | "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", 1946 | "dev": true, 1947 | "license": "MIT", 1948 | "dependencies": { 1949 | "cliui": "^8.0.1", 1950 | "escalade": "^3.1.1", 1951 | "get-caller-file": "^2.0.5", 1952 | "require-directory": "^2.1.1", 1953 | "string-width": "^4.2.3", 1954 | "y18n": "^5.0.5", 1955 | "yargs-parser": "^21.1.1" 1956 | }, 1957 | "engines": { 1958 | "node": ">=12" 1959 | } 1960 | }, 1961 | "node_modules/yargs-parser": { 1962 | "version": "21.1.1", 1963 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 1964 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 1965 | "dev": true, 1966 | "license": "ISC", 1967 | "engines": { 1968 | "node": ">=12" 1969 | } 1970 | }, 1971 | "node_modules/yargs-unparser": { 1972 | "version": "2.0.0", 1973 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", 1974 | "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", 1975 | "dev": true, 1976 | "license": "MIT", 1977 | "dependencies": { 1978 | "camelcase": "^6.0.0", 1979 | "decamelize": "^4.0.0", 1980 | "flat": "^5.0.2", 1981 | "is-plain-obj": "^2.1.0" 1982 | }, 1983 | "engines": { 1984 | "node": ">=10" 1985 | } 1986 | }, 1987 | "node_modules/yauzl": { 1988 | "version": "2.10.0", 1989 | "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", 1990 | "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", 1991 | "license": "MIT", 1992 | "dependencies": { 1993 | "buffer-crc32": "~0.2.3", 1994 | "fd-slicer": "~1.1.0" 1995 | } 1996 | }, 1997 | "node_modules/yocto-queue": { 1998 | "version": "0.1.0", 1999 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 2000 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 2001 | "dev": true, 2002 | "license": "MIT", 2003 | "engines": { 2004 | "node": ">=10" 2005 | }, 2006 | "funding": { 2007 | "url": "https://github.com/sponsors/sindresorhus" 2008 | } 2009 | } 2010 | } 2011 | } 2012 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playwright-with-fingerprints", 3 | "description": "A plugin that improves the stealth of the playwright library using fingerprints", 4 | "homepage": "https://github.com/CheshireCaat/playwright-with-fingerprints#readme", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/CheshireCaat/playwright-with-fingerprints.git" 8 | }, 9 | "author": "CheshireCaat", 10 | "version": "2.2.0", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/CheshireCaat/playwright-with-fingerprints/issues", 14 | "email": "cheshirecat902@gmail.com" 15 | }, 16 | "type": "commonjs", 17 | "main": "./src/index.js", 18 | "types": "./src/index.d.ts", 19 | "scripts": { 20 | "test": "mocha", 21 | "format": "npx prettier -w .", 22 | "release": "npm test && npm run format && npm publish" 23 | }, 24 | "keywords": [ 25 | "browser-fingerprinting", 26 | "device-fingerprinting", 27 | "browser-fingerprint", 28 | "device-fingerprint", 29 | "privacy-protection", 30 | "detection-evasion", 31 | "fingerprinting", 32 | "stealth-mode", 33 | "chromedriver", 34 | "websecurity", 35 | "fingerprint", 36 | "automation", 37 | "playwright", 38 | "chromium", 39 | "headless", 40 | "devtools", 41 | "security", 42 | "stealth", 43 | "browser", 44 | "privacy", 45 | "chrome", 46 | "web" 47 | ], 48 | "engines": { 49 | "node": ">= 18" 50 | }, 51 | "files": [ 52 | "src" 53 | ], 54 | "dependencies": { 55 | "browser-with-fingerprints": "2.2.0" 56 | }, 57 | "devDependencies": { 58 | "@cheshire-caat/prettier-config": "^1.0.0", 59 | "dotenv": "^16.4.7", 60 | "mocha": "^11.1.0", 61 | "playwright": "^1.51.1", 62 | "prettier": "^3.5.3" 63 | }, 64 | "peerDependencies": { 65 | "playwright": ">=1.27.1", 66 | "playwright-core": ">=1.27.1" 67 | }, 68 | "peerDependenciesMeta": { 69 | "playwright": { 70 | "optional": true 71 | }, 72 | "playwright-core": { 73 | "optional": true 74 | } 75 | }, 76 | "prettier": "@cheshire-caat/prettier-config" 77 | } 78 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { BrowserType, BrowserContext } from 'playwright-core'; 2 | import type { FingerprintPlugin } from 'browser-with-fingerprints'; 3 | 4 | /** 5 | * Describes the **playwright** compatible launch options. 6 | */ 7 | export type PluginLaunchOptions = Parameters[1]; 8 | 9 | /** 10 | * Describes the **playwright** compatible browser launcher. 11 | * 12 | * See [playwright](https://playwright.dev/docs/api/class-browsertype#browser-type-launch) docs for more information. 13 | */ 14 | export type Launcher = Pick; 15 | 16 | /** 17 | * Describes a plugin that is capable of fetching a fingerprint and launching a browser instance using it. 18 | * 19 | * In order to use the plugin, create an instance of it using the {@link createPlugin} method or, even better, use the default instance that is already configured to work. 20 | * 21 | * @remarks 22 | * **NOTE**: This plugin works correctly only with the **playwright** framework or with its core version. 23 | * In case of using custom launchers that are incompatible with **playwright** or launchers that do not use it under the hood, errors may occur. 24 | */ 25 | export interface PlaywrightFingerprintPlugin extends FingerprintPlugin { 26 | /** 27 | * Launches **playwright** and launches a browser instance with given arguments and options when specified. 28 | * 29 | * This method uses the playwright's native {@link BrowserType.launch | launch} method under the hood and adds some functionality for applying fingerprints and proxies. 30 | * Before launching, the parameters that you specified using the {@link useProxy} and {@link useFingerprint} methods will also be applied for the browser. 31 | * 32 | * The options for are the same as the built-in ones, with a few exceptions that can break things. 33 | * For example, you can't use the `channel` option and will get an error when trying to specify it because the plugin doesn't support it: 34 | * 35 | * ```js 36 | * // This code will throw an error: 37 | * await plugin.launch({ channel: 'chrome-canary' }); 38 | * ``` 39 | * 40 | * You can get a list of unsupported options by importing the {@link UNSUPPORTED_OPTIONS} variable. 41 | * 42 | * If you need more information on how the native method works, use the **playwright** documentation - 43 | * [link](https://playwright.dev/docs/api/class-browsertype#browser-type-launch). 44 | * 45 | * @remarks 46 | * **NOTE**: This plugin only works with the `chromium` browser, which comes bundled with the plugin. 47 | * You will not be able to use default `chromium`, `firefox`, `webkit` and other engines that come with the **playwright** framework. 48 | * 49 | * If you need to use the default browsers without fingerprint spoofing, just use the **playwright** built-in `launch` method. 50 | * 51 | * You must specify the service key to apply the fingerprint when launching the browser (if the fingerprint was obtained using a paid key). 52 | * 53 | * @example 54 | * An example of launching the browser in visible mode: 55 | * 56 | * ```js 57 | * const browser = await plugin.launch({ 58 | * headless: false, 59 | * }); 60 | * ``` 61 | * 62 | * @param options - Set of configurable options to set on the browser. 63 | * @returns Promise which resolves to a browser instance. 64 | */ 65 | launch(options?: PluginLaunchOptions): Promise; 66 | 67 | /** 68 | * Returns the persistent browser context instance. 69 | * 70 | * Launches browser that uses persistent storage located at `userDataDir` and returns the only context. Closing this 71 | * context will automatically close the browser. 72 | * 73 | * @param userDataDir - Path to a user data directory, which stores browser session data like cookies and local storage. 74 | * @param options - Set of configurable options to set on the browser. 75 | * @returns Promise which resolves to a context instance. 76 | */ 77 | launchPersistentContext(userDataDir: string, options?: PluginLaunchOptions): Promise; 78 | 79 | /** 80 | * A **playwright** compatible launcher or the **playwright** itself. 81 | * 82 | * It's used to launch the browser directly via the plugin. 83 | */ 84 | readonly launcher: Launcher; 85 | } 86 | 87 | /** 88 | * A default instance of the fingerprint plugin for the **playwright** library. 89 | * It comes with a pre-configured launcher and is the easiest option to use. 90 | * 91 | * The default instance itself imports and uses the necessary dependencies, so you can replace 92 | * the **playwright** imports with a plugin if you don't need additional options. 93 | */ 94 | export declare const plugin: PlaywrightFingerprintPlugin; 95 | 96 | /** 97 | * A list of option names for the **playwright** built-in `launch` method that cannot be 98 | * used in conjunction with the plugin. 99 | * 100 | * @remarks 101 | * **NOTE**: If you use the options specified in this list to launch the browser, a 102 | * corresponding error will be thrown. 103 | */ 104 | export declare const UNSUPPORTED_OPTIONS: readonly string[]; 105 | 106 | /** 107 | * Create a separate plugin instance using the provided **playwright** compatible browser launcher. 108 | * 109 | * This method can be useful if you are working with any wrappers for the target library. 110 | * But use it with caution and respect the launcher signature. 111 | * 112 | * @remarks 113 | * **NOTE**: If you're using pure **playwright** or **playwright-core** packages without add-ons, 114 | * it's best to use the default plugin instance that is already configured to work. 115 | * 116 | * @param launcher - Playwright (or **API** compatible) browser launcher. 117 | * @returns A new separate plugin instance. 118 | */ 119 | export declare function createPlugin(launcher: Launcher): PlaywrightFingerprintPlugin; 120 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const defaultLauncher = require('./loader').load(); 2 | const { FingerprintPlugin } = require('browser-with-fingerprints'); 3 | const { onClose, bindHooks, getViewport, setViewport } = require('./utils'); 4 | 5 | const IGNORED_ARGUMENTS = ['--disable-extensions']; 6 | const UNSUPPORTED_OPTIONS = ['proxy', 'channel', 'firefoxUserPrefs']; 7 | 8 | const LAUNCH_FALLBACK_WARNING = [ 9 | 'The original "launch" method is temporarily unsupported.', 10 | 'Under the hood it will use the "launchPersistentContext" method.', 11 | 'Therefore, it is recommended to use the second one directly instead.', 12 | ].join('\n'); 13 | 14 | const Plugin = class PlaywrightFingerprintPlugin extends FingerprintPlugin { 15 | async launch(options = {}) { 16 | this.#validateOptions(options); 17 | console.warn(LAUNCH_FALLBACK_WARNING); 18 | return await this.launchPersistentContext('', options); 19 | } 20 | 21 | async launchPersistentContext(userDataDir, options = {}) { 22 | this.#validateOptions(options); 23 | const { ignoreDefaultArgs } = options; 24 | const method = 'launchPersistentContext'; 25 | 26 | if (!this.launcher[method]) { 27 | throw new Error(`The provided launcher doesn't support the "${method}" method`); 28 | } 29 | 30 | return await super.launch({ 31 | ...options, 32 | userDataDir, 33 | viewport: null, 34 | launcher: { 35 | launch: (options = {}) => { 36 | const [userDataDirArg] = options.args.splice( 37 | options.args.findIndex((arg) => arg.startsWith('--user-data-dir')), 38 | 1 39 | ); 40 | return this.launcher[method](userDataDirArg.split('=')[1], options); 41 | }, 42 | }, 43 | ignoreDefaultArgs: Array.isArray(ignoreDefaultArgs) 44 | ? ignoreDefaultArgs.concat(IGNORED_ARGUMENTS) 45 | : ignoreDefaultArgs || IGNORED_ARGUMENTS, 46 | }); 47 | } 48 | 49 | /** 50 | * Configures the browser, including viewport size, hook and event binding. 51 | * 52 | * @param {import('playwright').Browser} browser - The target browser instance. 53 | * @param {{width: number, height: number}} bounds - The size of the viewport. 54 | * @param {Promise} sync - Method for syncing browser settings. 55 | * @param {(target: any) => void} cleanup - The cleanup function. 56 | * 57 | * @internal 58 | */ 59 | async configure(cleanup, browser, bounds, sync) { 60 | onClose(browser, () => cleanup(browser)); 61 | 62 | // Resize pages only if size is set. 63 | if (bounds.width && bounds.height) { 64 | const resize = async (page) => { 65 | const { width, height } = await getViewport(page); 66 | 67 | if (width !== bounds.width || height !== bounds.height) { 68 | await sync(() => setViewport(page, bounds)); 69 | } 70 | }; 71 | bindHooks(browser, { onPageCreated: resize }); 72 | 73 | // Resize on startup only if there are open pages. 74 | if (browser.pages) { 75 | const [page] = await browser.pages(); 76 | if (page) await resize(page); 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * Check the options used to launch a browser for compatibility with plugin. 83 | * If any of the specified options are incompatible, an error will be thrown. 84 | * 85 | * @param options - Set of configurable options to set on the browser. 86 | * 87 | * @private 88 | */ 89 | #validateOptions(options = {}) { 90 | for (const option of UNSUPPORTED_OPTIONS) { 91 | if (option in options) { 92 | throw new Error(`The built-in "${option}" option is not supported in this plugin.`); 93 | } 94 | } 95 | } 96 | }; 97 | 98 | exports.plugin = new Plugin(defaultLauncher); 99 | 100 | exports.createPlugin = Plugin.create.bind(Plugin); 101 | 102 | exports.UNSUPPORTED_OPTIONS = UNSUPPORTED_OPTIONS; 103 | -------------------------------------------------------------------------------- /src/loader.js: -------------------------------------------------------------------------------- 1 | const Loader = require('browser-with-fingerprints/src/loader'); 2 | 3 | /** 4 | * The loader instance for the `playwright` framework that supports both the default and `core` versions. 5 | * 6 | * The minimum required framework version is `1.27.1`. 7 | * 8 | * @internal 9 | */ 10 | module.exports = new Loader('playwright', '1.27.1', ['playwright-core']); 11 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const { scripts, MAX_RESIZE_RETRIES } = require('browser-with-fingerprints/src/common'); 2 | 3 | /** 4 | * Add an event listener for the browser's `close` event. 5 | * 6 | * @param {import('playwright').Browser} target - The listener target. 7 | * @param {(() => void)} listener - The event listener. 8 | * 9 | * @internal 10 | */ 11 | exports.onClose = (target, listener) => { 12 | target.once(isBrowser(target) ? 'disconnected' : 'close', listener); 13 | }; 14 | 15 | /** 16 | * Modify the original browser methods and add additional hooks. 17 | * Hooks will be called before the corresponding method completes. 18 | * 19 | * @param {import('playwright').Browser} target - The target browser instance. 20 | * 21 | * @internal 22 | */ 23 | exports.bindHooks = (target, hooks = {}) => { 24 | if (isBrowser(target)) { 25 | target.newContext = new Proxy(target.newContext, { 26 | apply: (fn, ctx, [opts]) => fn.call(ctx, resetOptions(opts)).then(patchContext), 27 | }); 28 | } 29 | 30 | /** @param {import('playwright').BrowserContext} ctx */ 31 | function patchContext(ctx) { 32 | ctx.newPage = new Proxy(ctx.newPage, { 33 | async apply(fn, ctx, [opts]) { 34 | const page = await fn.call(ctx, resetOptions(opts)); 35 | await hooks.onPageCreated?.(page); 36 | return patchPage(page); 37 | }, 38 | }); 39 | 40 | return ctx; 41 | } 42 | 43 | /** @param {import('playwright').Page} page */ 44 | function patchPage(page) { 45 | page.setViewportSize = new Proxy(page.setViewportSize, { 46 | apply: async () => { 47 | console.warn('Warning: setting the viewport size is not allowed (limited by fingerprint).'); 48 | }, 49 | }); 50 | 51 | return page; 52 | } 53 | 54 | if (!target.newContext) patchContext(target); 55 | }; 56 | 57 | /** 58 | * Set the browser viewport size. 59 | * 60 | * @param {import('playwright').Page} page - The target page to set the viewport. 61 | * @param {{width: number, height: number}} bounds - New viewport size. 62 | * 63 | * @internal 64 | */ 65 | exports.setViewport = async (page, { diff, width = 0, height = 0 }) => { 66 | const delta = diff ? { ...diff } : { width: 16, height: 88 }; 67 | 68 | const cdp = await page.context().newCDPSession(page); 69 | const { windowId } = await cdp.send('Browser.getWindowForTarget'); 70 | 71 | for (let i = 0; i < MAX_RESIZE_RETRIES; ++i) { 72 | const bounds = { width: width + delta.width, height: height + delta.height }; 73 | await Promise.all([cdp.send('Browser.setWindowBounds', { bounds, windowId }), waitForResize(page)]); 74 | 75 | const viewport = await this.getViewport(page); 76 | 77 | if (width === viewport.width && height === viewport.height) { 78 | break; 79 | } else if (i === MAX_RESIZE_RETRIES - 1) { 80 | // TODO: improve handling of incorrect viewport size. 81 | console.warn('Unable to set correct viewport size.'); 82 | } 83 | 84 | delta.height += height - viewport.height; 85 | delta.width += width - viewport.width; 86 | } 87 | 88 | await cdp.detach(); 89 | }; 90 | 91 | /** 92 | * Get the browser viewport size. 93 | * 94 | * @param {import('playwright').Page} page - The target page to get the viewport. 95 | * @returns {Promise<{width: number, height: number}>} - Promise which resolves to a browser viewport size. 96 | * 97 | * @internal 98 | */ 99 | exports.getViewport = (page) => page.evaluate(scripts.getViewport); 100 | 101 | /** 102 | * Wait for the browser to resize. 103 | * 104 | * @param {import('playwright').Page} page - The target page to to wait for the browser to resize. 105 | */ 106 | const waitForResize = (page) => page.evaluate(scripts.waitForResize); 107 | 108 | const resetOptions = (options = {}) => ({ 109 | ...(options != null && typeof options === 'object' ? options : {}), 110 | viewport: null, 111 | }); 112 | 113 | const isBrowser = (target) => { 114 | return target && typeof target.version === 'function'; 115 | }; 116 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert').strict; 2 | const { FingerprintPlugin } = require('browser-with-fingerprints'); 3 | const { plugin, createPlugin, UNSUPPORTED_OPTIONS } = require('..'); 4 | 5 | describe('plugin', () => { 6 | it('should have additional exports', () => { 7 | assert.ok(createPlugin, 'createPlugin should be exported'); 8 | assert.ok(Array.isArray(UNSUPPORTED_OPTIONS), 'UNSUPPORTED_OPTIONS should be an array'); 9 | }); 10 | 11 | describe('instance', () => { 12 | it('should be an object', () => { 13 | assert.ok(plugin, 'Plugin should not be null'); 14 | assert.equal(typeof plugin, 'object', 'Plugin should be an object'); 15 | }); 16 | 17 | it('should be an instance of the base class', () => { 18 | assert.equal( 19 | Object.getPrototypeOf(plugin.constructor), 20 | FingerprintPlugin, 21 | 'Plugin should inherit from FingerprintPlugin' 22 | ); 23 | }); 24 | 25 | it('should be an instance of "PlaywrightFingerprintPlugin"', () => { 26 | assert.equal( 27 | plugin.constructor.name, 28 | 'PlaywrightFingerprintPlugin', 29 | 'Plugin constructor name should be PlaywrightFingerprintPlugin' 30 | ); 31 | }); 32 | 33 | it('should have a default launcher', () => { 34 | assert.ok(plugin.launcher, 'Launcher should not be null'); 35 | assert.equal(typeof plugin.launcher, 'object', 'Launcher should be an object'); 36 | 37 | ['launch', 'connect', 'executablePath'].forEach((property) => { 38 | assert.equal(typeof plugin.launcher[property], 'function', `Launcher should have a ${property} function`); 39 | }); 40 | }); 41 | 42 | it('should have all methods from the base plugin', () => { 43 | Object.getOwnPropertyNames(FingerprintPlugin.prototype).forEach((method) => { 44 | assert.ok(method in plugin, `Plugin should have method: ${method}`); 45 | }); 46 | }); 47 | }); 48 | 49 | describe('#createPlugin()', () => { 50 | it('should be a function', () => { 51 | assert.equal(typeof createPlugin, 'function', 'createPlugin should be a function'); 52 | }); 53 | 54 | it('should throw an error if an invalid launcher is passed', () => { 55 | [{}, null, { launch: null }].forEach((launcher) => { 56 | assert.throws(() => createPlugin(launcher), 'Should throw an error for invalid launcher'); 57 | }); 58 | }); 59 | 60 | it('should properly create a separate plugin instance', () => { 61 | const launcher = { launch() {} }; 62 | let instance; 63 | 64 | assert.doesNotThrow(() => { 65 | instance = createPlugin(launcher); 66 | }, 'Should not throw when creating a plugin instance'); 67 | 68 | assert.ok(instance, 'Instance should not be null'); 69 | assert.notEqual(instance, plugin, 'Instance should be different from the original plugin'); 70 | assert.equal(typeof instance, 'object', 'Instance should be an object'); 71 | assert.equal(instance.launcher, launcher, 'Instance should have the correct launcher'); 72 | assert.equal( 73 | instance.constructor, 74 | plugin.constructor, 75 | 'Instance constructor should match the plugin constructor' 76 | ); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/issues.js: -------------------------------------------------------------------------------- 1 | const { plugin } = require('..'); 2 | const assert = require('assert').strict; 3 | const { setTimeout } = require('timers/promises'); 4 | 5 | describe('plugin', () => { 6 | let browser; 7 | 8 | before(async () => { 9 | plugin.useProxy(process.env.FINGERPRINT_PROXY || ''); 10 | 11 | browser = await plugin.launch(); 12 | }); 13 | 14 | after(async () => { 15 | plugin.useProxy(''); 16 | 17 | if (browser) { 18 | await browser.close(); 19 | } 20 | }); 21 | 22 | it('should correctly open a new window in headless mode (puppeteer-with-fingerprints#117)', async () => { 23 | const page = await browser.newPage(); 24 | 25 | await page.goto('https://www.producthunt.com/products/etsy-geeks', { waitUntil: 'domcontentloaded' }); 26 | await Promise.all([page.click("a[href*='etsygeeks.org']"), setTimeout(5000)]); 27 | 28 | try { 29 | await page.waitForNavigation(); 30 | } catch (error) { 31 | // console.warn('Navigation error:', error); 32 | } 33 | 34 | const pages = await browser.pages(); 35 | const lastPage = pages.pop(); 36 | 37 | await assert.doesNotReject( 38 | lastPage.waitForFunction(() => !window.location.href.includes('about:blank'), { timeout: 5000 }), 39 | 'The new page must be opened and loaded correctly.' 40 | ); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/plugin.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert').strict; 2 | const { getViewport } = require('../src/utils'); 3 | const { plugin, UNSUPPORTED_OPTIONS } = require('..'); 4 | 5 | describe('plugin', () => { 6 | let browser; 7 | 8 | before(async () => { 9 | plugin.useProxy(process.env.FINGERPRINT_PROXY || ''); 10 | 11 | browser = await plugin.launch(); 12 | }); 13 | 14 | after(async () => { 15 | plugin.useProxy(''); 16 | 17 | if (browser) { 18 | await browser.close(); 19 | } 20 | }); 21 | 22 | describe('#configure()', () => { 23 | it('should correctly resize browser if bounds are set', async () => { 24 | const viewport = { width: 600, height: 700 }; 25 | 26 | const customBrowser = await plugin.launcher.launch({ defaultViewport: null }); 27 | await plugin.configure( 28 | () => {}, 29 | customBrowser, 30 | viewport, 31 | (fn) => fn() 32 | ); 33 | 34 | const newPage = await customBrowser.newPage(); 35 | const actual = await getViewport(newPage); 36 | 37 | await customBrowser.close(); 38 | assert.deepEqual(actual, viewport, 'The viewport should match the specified dimensions'); 39 | }); 40 | }); 41 | 42 | describe('#launch()', () => { 43 | it('should throw an error if an unsupported option is passed', async () => { 44 | for (const option of UNSUPPORTED_OPTIONS) { 45 | try { 46 | await plugin.launch({ [option]: null }); 47 | assert.fail(`Expected an error when passing unsupported option: ${option}`); 48 | } catch (error) { 49 | assert.ok(error.message.includes(option), `Error message should mention "${option}"`); 50 | } 51 | } 52 | }); 53 | 54 | it('should return the same type as vanilla "playwright" package', async () => { 55 | assert.notEqual(browser, null, 'Browser should not be null'); 56 | assert.match(browser.constructor.name, /Browser/, 'Browser constructor name should match expected type'); 57 | 58 | for (const method of ['close', 'newPage']) { 59 | assert.equal(typeof browser[method], 'function', `${method} should be a function`); 60 | } 61 | }); 62 | }); 63 | 64 | it('should work with the browser normally', async () => { 65 | try { 66 | const page = await browser.newPage(); 67 | await page.goto('https://google.com/'); 68 | } catch (error) { 69 | assert.fail(`Browser navigation failed: ${error.message}`); 70 | } 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const { plugin } = require('..'); 2 | const assert = require('assert').strict; 3 | const { bindHooks, getViewport, setViewport } = require('../src/utils'); 4 | 5 | describe('utils', () => { 6 | let browser = null; 7 | 8 | beforeEach(async function () { 9 | browser = await plugin.launch({ 10 | headless: !this.test.title.includes('headful'), 11 | }); 12 | }); 13 | 14 | afterEach(async () => { 15 | if (browser) { 16 | await browser.close(); 17 | } 18 | }); 19 | 20 | describe('#getViewport()', () => { 21 | ['headless', 'headful'].forEach((type) => { 22 | it(`should return the ${type} browser viewport size`, async () => { 23 | const page = await browser.newPage(); 24 | 25 | const viewport = await getViewport(page); 26 | 27 | assert.ok(viewport, 'Viewport should not be null'); 28 | assert.equal(typeof viewport, 'object', 'Viewport should be an object'); 29 | assert.equal(typeof viewport.width, 'number', 'Viewport width should be a number'); 30 | assert.equal(typeof viewport.height, 'number', 'Viewport height should be a number'); 31 | }); 32 | }); 33 | }); 34 | 35 | describe('#setViewport()', () => { 36 | ['headless', 'headful'].forEach((type) => { 37 | it(`should change the ${type} browser viewport size`, async () => { 38 | const page = await browser.newPage({ viewport: null }); 39 | 40 | for (let step = 5; step <= 10; ++step) { 41 | const viewport = { width: step * 100, height: step * 100 }; 42 | await setViewport(page, viewport); 43 | 44 | const actualViewport = await getViewport(page); 45 | assert.deepEqual(actualViewport, viewport, `Viewport should be ${JSON.stringify(viewport)}`); 46 | } 47 | }); 48 | }); 49 | }); 50 | 51 | describe('#bindHooks()', () => { 52 | beforeEach(() => { 53 | bindHooks(browser); 54 | }); 55 | 56 | it('should correctly modify original methods', async () => { 57 | let page; 58 | let ctx = browser; 59 | 60 | await assert.doesNotReject(async () => { 61 | if (browser.version) { 62 | ctx = await browser.newContext(); 63 | } 64 | }); 65 | assert.match(ctx.constructor.name, /Context/, 'Context constructor name should match the expected type'); 66 | 67 | for (const target of [ctx, browser]) { 68 | await assert.doesNotReject(async () => { 69 | page = await target.newPage(); 70 | }); 71 | assert.match(page.constructor.name, /Page/, 'Page constructor name should match the expected type'); 72 | } 73 | }); 74 | 75 | it('should prevent viewport resizing', async () => { 76 | const viewport = { width: 100, height: 100 }; 77 | const page = await browser.newPage({ viewport }); 78 | 79 | for (let i = 0; i < 2; ++i) { 80 | if (i > 0) await page.setViewportSize(viewport); 81 | const actualViewport = await getViewport(page); 82 | assert.notDeepEqual(actualViewport, viewport, `Viewport should not equal ${JSON.stringify(viewport)}`); 83 | } 84 | }); 85 | 86 | describe('page creation hook', () => { 87 | it('should execute a callback before page creation', async () => { 88 | const waitForHook = new Promise((resolve) => { 89 | bindHooks(browser, { onPageCreated: () => resolve() }); 90 | }); 91 | 92 | const result = await Promise.race([waitForHook, browser.newPage()]); 93 | assert.equal(result, undefined, 'Callback should execute before new page is created'); 94 | }); 95 | }); 96 | }); 97 | }); 98 | --------------------------------------------------------------------------------