[
  {
    "path": ".babelrc",
    "content": "{\n  \"presets\": [\"es2015\"]\n}"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [main, master, fpscanner-v2]\n  pull_request:\n    branches: [main, master]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    \n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n      \n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 'lts/*'\n          cache: 'npm'\n      \n      - name: Install dependencies\n        run: npm ci\n      \n      - name: Build (non-obfuscated)\n        run: npm run build:dev\n      \n      - name: Build (obfuscated)\n        run: npm run build:obfuscate\n      \n      - name: Verify dist files exist\n        run: |\n          test -f dist/fpScanner.es.js\n          test -f dist/fpScanner.cjs.js\n          test -f dist/index.d.ts\n\n  test:\n    runs-on: ubuntu-latest\n    needs: build\n    \n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n      \n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 'lts/*'\n          cache: 'npm'\n      \n      - name: Install dependencies\n        run: npm ci\n      \n      - name: Install Playwright browsers\n        run: npx playwright install --with-deps chromium firefox webkit\n      \n      - name: Run Playwright tests\n        run: npm run test:playwright\n      \n      - name: Upload test results\n        uses: actions/upload-artifact@v4\n        if: failure()\n        with:\n          name: playwright-report\n          path: playwright-report/\n          retention-days: 7\n\n  package:\n    runs-on: ubuntu-latest\n    needs: build\n    \n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n      \n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 'lts/*'\n          cache: 'npm'\n      \n      - name: Install dependencies\n        run: npm ci\n      \n      - name: Build for packaging\n        run: npm run build:dev\n      \n      - name: Test npm pack\n        run: |\n          npm pack\n          ls -la *.tgz\n      \n      - name: Test package installation\n        run: |\n          mkdir /tmp/test-install\n          cd /tmp/test-install\n          npm init -y\n          npm install $GITHUB_WORKSPACE/fpscanner-*.tgz\n          # Verify the package installed correctly\n          test -f node_modules/fpscanner/dist/fpScanner.es.js\n          test -f node_modules/fpscanner/bin/cli.js\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea/\ndist/\n\n# Logs\nlogs\n*.log\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directory\n# Commenting this out is preferred by some people, see\n# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-\nnode_modules\nbower_components\ncoverage\ntmp\n\n# Users Environment Variables\n.lock-wscript\n\n\\.idea/dictionaries/avastel\\.xml\n\n\\.idea/fpscanner\\.iml\n\n\\.idea/workspace\\.xml\n\n\\.idea/vcs\\.xml\n\n\\.idea/modules\\.xml\n\n\\.idea/misc\\.xml\n\n\\.idea/jsLibraryMappings\\.xml\n\n*.pyc\n__pycache__/*\n\n# Playwright\ntest-results/\nplaywright-report/\nplaywright/.cache/\n\nvenv/"
  },
  {
    "path": ".npmignore",
    "content": "# Backup files created by custom build script\ndist/*.original\n\n# Test files\ntest/\ntest-results/\nplaywright-report/\n.github/\n\n# Development files\n*.spec.ts\n*.test.ts\nvite.config.ts\nplaywright.config.ts\ntsconfig.json\n\n# Examples\nexamples/\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2017 antoinevastel <antoine.vastel@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Fingerprint Scanner\n\n> **News:** After more than 7 years without any updates, the first release of the new FPScanner is here. This version combines fingerprinting and bot detection in a single library. Try the live demo at [fpscanner.com](https://fpscanner.com/). Expect new fingerprinting signals and more detection logic over time.\n\n[![CI](https://github.com/antoinevastel/fpscanner/actions/workflows/ci.yml/badge.svg)](https://github.com/antoinevastel/fpscanner/actions/workflows/ci.yml)\n\n## Sponsor\n\nThis project is sponsored by <a href=\"https://castle.io/?utm_source=github&utm_medium=oss&utm_campaign=fpscanner\">Castle.</a>\n\n<a href=\"https://castle.io/?utm_source=github&utm_medium=oss&utm_campaign=fpscanner\"><img src=\"assets/castle-logo.png\" alt=\"Castle\" height=\"48\" style=\"vertical-align: middle;\"></a>\n\nThis library focuses on self-hosted fingerprinting and bot detection primitives. In real-world fraud and bot prevention, teams often need additional capabilities such as traffic observability, historical analysis, rule iteration, and correlation across device, network, and behavioral signals.\n\nCastle provides a production-grade platform for bot and fraud detection, designed to operate at scale and handle these operational challenges end to end.\n\nFor a deeper explanation of what this library intentionally does not cover, see the **“Limits and non-goals”** section at the end of this README.\n\n\n## FPScanner: description\n\nA lightweight browser fingerprinting library for bot detection.\n\nScraping has become mainstream. AI and LLM-driven companies now crawl the web at a scale that was previously limited to specialized actors, often without clearly respecting `robots.txt` or rate limits. At the same time, fraudsters do not need to rely solely on public frameworks like OpenBullet or generic automation stacks anymore. With LLMs, writing a custom bot tailored to a specific website has become significantly easier, faster, and cheaper.\n\nThe result is a much broader and more diverse bot ecosystem:\n- More actors scraping content, training models, or extracting data\n- More custom automation, harder to fingerprint with outdated heuristics\n- More abuse at signup, login, and sensitive workflows, not just simple scraping\n\nOn the defender side, the situation is much more constrained.\n\nYou often have two options:\n- Very basic open source libraries that focus on naive or outdated signals\n- Expensive, black-box bot and fraud solutions that require routing traffic through third-party CDNs or vendors\n\nNot every website can afford enterprise-grade bot management products. And even when cost is not the main issue, you may not want to route all your traffic through a CDN or outsource all detection logic to a third party.\n\nThis library exists to fill that gap.\n\nIt is a **self-hosted, lightweight, and up-to-date** browser fingerprinting and bot detection library, designed with real-world constraints in mind. The goal is not to promise perfect detection, but to give you solid building blocks that reflect how bots actually behave today.\n\nThis includes practical considerations that are often ignored in toy implementations:\n- Anti-replay protections (timestamp + nonce)\n- Payload encryption to prevent trivial forgery\n- Optional obfuscation to raise the cost of reverse-engineering\n- Focus on strong, low-noise signals rather than brittle tricks\n\nThe design and trade-offs behind this library are directly inspired by real production experience and by the ideas discussed in these articles:\n- [Roll your own bot detection: fingerprinting (JavaScript)](https://blog.castle.io/roll-your-own-bot-detection-fingerprinting-javascript-part-1/)\n- [Roll your own bot detection: server-side detection](https://blog.castle.io/roll-your-own-bot-detection-server-side-detection-part-2/)\n\nThose articles are not documentation for this library, but they reflect the same philosophy: understand what attackers actually do, accept that no single signal is perfect, and build simple, composable primitives that you fully control.\n\n### Open Source, Production-Ready\n\nThis library is open source, but it is not naive about the implications of being open.\n\nIn bot detection, openness cuts both ways. Publishing detection logic makes it easier for attackers to study how they are detected. At the same time, defenders routinely study open and closed automation frameworks, anti-detect browsers, and bot tooling to discover new signals and weaknesses. This asymmetry already exists in the ecosystem, regardless of whether this library is open source or not.\n\nThe goal here is not to rely on obscurity. It is to acknowledge that attackers will read the code and still make abuse operationally expensive.\n\nThis is why the library combines transparency with pragmatic hardening:\n- **Anti-replay mechanisms** ensure that a valid fingerprint cannot simply be captured once and reused at scale.\n- **Build-time key injection** means attackers cannot trivially generate valid encrypted payloads without access to your specific build.\n- **Optional obfuscation** raises the cost of reverse-engineering and makes automated payload forgery harder without executing the code in a real browser.\n\nThese controls are not meant to be perfect or unbreakable. Their purpose is to remove the easy shortcuts. An attacker should not be able to look at the repository, reimplement a serializer, and start sending convincing fingerprints from a headless script.\n\nMore importantly, detection does not stop at a single boolean flag.\n\nEven if an attacker focuses on bypassing individual bot detection checks, producing **fully consistent fingerprints** over time is significantly harder. Fingerprints encode relationships between signals, contexts, and environments. Maintaining that consistency across sessions, IPs, and accounts requires real execution, careful state management, and stable tooling.\n\nIn practice, this creates leverage on the server side:\n- Fingerprints can be tracked over time\n- Reuse patterns and drift become visible\n- Inconsistencies surface when attackers partially emulate environments or rotate tooling incorrectly\n\nThis is how fingerprinting is used in production systems: not as a one-shot verdict, but as a way to observe structure, reuse, and anomalies at scale.\n\nOpen source does not weaken this approach. It makes the trade-offs explicit. Attackers are assumed to be capable and adaptive, not careless. The library is designed accordingly: to force real execution, limit replay, and preserve enough structure in the signals that automation leaves traces once you observe it over time.\n\n\n## Features\n\n| Feature | Description |\n|---------|-------------|\n| **Fast bot detection** | Client-side detection of strong automation signals such as `navigator.webdriver`, CDP usage, Playwright markers, and other common automation artifacts |\n| **Browser fingerprinting** | Short-lived fingerprint designed for attack detection, clustering, and session correlation rather than long-term device tracking |\n| **Encrypted payloads** | Optional payload encryption to prevent trivial forgery, with the encryption key injected at build time |\n| **Obfuscation** | Optional code obfuscation to increase the cost of reverse-engineering and make it harder to forge valid fingerprints without actually executing the collection code |\n| **Cross-context validation** | Detects inconsistencies across different JavaScript execution contexts (main page, iframes, and web workers) |\n\n\n---\n\n## Quick Start\n\n### Installation\n\n```bash\nnpm install fpscanner\n```\n\n> **Note**: Out of the box, fpscanner uses a default placeholder encryption key and no obfuscation. This is fine for development and testing, but for production deployments you should build with your own key and enable obfuscation. See [Advanced: Custom Builds](#advanced-custom-builds) for details.\n\n### Basic Usage\n\n```javascript\nimport FingerprintScanner from 'fpscanner';\n\nconst scanner = new FingerprintScanner();\nconst payload = await scanner.collectFingerprint();\n\n// Send payload to your server\nfetch('/api/fingerprint', {\n  method: 'POST',\n  body: JSON.stringify({ fingerprint: payload }),\n  headers: { 'Content-Type': 'application/json' }\n});\n```\n\n### Server-Side (Node.js)\n\n```javascript\n// Decrypt and validate the fingerprint\n// Use the same key you provided when building: npx fpscanner build --key=your-key\nconst key = 'your-secret-key'; // Your custom key\n\nfunction decryptFingerprint(ciphertext, key) {\n  const encrypted = Buffer.from(ciphertext, 'base64');\n  const keyBytes = Buffer.from(key, 'utf8');\n  const decrypted = Buffer.alloc(encrypted.length);\n\n  for (let i = 0; i < encrypted.length; i++) {\n    decrypted[i] = encrypted[i] ^ keyBytes[i % keyBytes.length];\n  }\n\n  return JSON.parse(decrypted.toString('utf8'));\n}\n\napp.post('/api/fingerprint', (req, res) => {\n  const fingerprint = decryptFingerprint(req.body.fingerprint, key);\n\n  // Check bot detection\n  if (fingerprint.fastBotDetection) {\n    console.log('🤖 Bot detected!', fingerprint.fastBotDetectionDetails);\n    return res.status(403).json({ error: 'Bot detected' });\n  }\n\n  // Validate timestamp (prevent replay attacks)\n  const ageMs = Date.now() - fingerprint.time;\n  if (ageMs > 60000) { // 60 seconds\n    return res.status(400).json({ error: 'Fingerprint expired' });\n  }\n\n  // Use fingerprint.fsid for session correlation\n  console.log('Fingerprint ID:', fingerprint.fsid);\n  res.json({ ok: true });\n});\n```\n\nThat's it! For most use cases, this is all you need.\n\n---\n\n## API Reference\n\n### `collectFingerprint(options?)`\n\nCollects browser signals and returns a fingerprint.\n\n```javascript\nconst scanner = new FingerprintScanner();\n\n// Default: returns encrypted base64 string\nconst encrypted = await scanner.collectFingerprint();\n\n// Explicit encryption\nconst encrypted = await scanner.collectFingerprint({ encrypt: true });\n\n// Raw object (no library encryption)\nconst fingerprint = await scanner.collectFingerprint({ encrypt: false });\n```\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `encrypt` | `boolean` | `true` | Whether to encrypt the payload |\n| `skipWorker` | `boolean` | `false` | Skip Web Worker signals (use if CSP blocks blob: URLs) |\n\n### Fingerprint Object\n\nWhen decrypted (or with `encrypt: false`), the fingerprint contains:\n\n```typescript\ninterface Fingerprint {\n  // Bot detection\n  fastBotDetection: boolean;           // true if any bot signal detected\n  fastBotDetectionDetails: {\n    hasWebdriver: boolean;             // navigator.webdriver === true\n    hasWebdriverWritable: boolean;     // webdriver property is writable\n    hasSeleniumProperty: boolean;      // Selenium-specific properties\n    hasCDP: boolean;                   // Chrome DevTools Protocol signals\n    hasPlaywright: boolean;            // Playwright-specific signals\n    hasWebdriverIframe: boolean;       // webdriver in iframe context\n    hasWebdriverWorker: boolean;       // webdriver in worker context\n    // ... more detection flags\n  };\n\n  // Fingerprint\n  fsid: string;                        // JA4-inspired fingerprint ID\n  signals: { /* raw signal data */ };\n\n  // Anti-replay\n  time: number;                        // Unix timestamp (ms)\n  nonce: string;                       // Random value for replay detection\n}\n```\n\n---\n\n## What It Detects\n\nThe library focuses on **strong, reliable signals** from major automation frameworks:\n\n| Detection | Signal | Frameworks |\n|-----------|--------|------------|\n| `hasWebdriver` | `navigator.webdriver === true` | Selenium, Puppeteer, Playwright |\n| `hasWebdriverWritable` | webdriver property descriptor | Puppeteer, Playwright |\n| `hasSeleniumProperty` | `document.$cdc_`, `$wdc_` | Selenium WebDriver |\n| `hasCDP` | CDP runtime markers | Chrome DevTools Protocol |\n| `hasPlaywright` | `__playwright`, `__pw_*` | Playwright |\n| `hasMissingChromeObject` | Missing `window.chrome` | Headless Chrome |\n| `headlessChromeScreenResolution` | 800x600 default | Headless browsers |\n| `hasHighCPUCount` | Unrealistic core count | VM/container environments |\n| `hasImpossibleDeviceMemory` | Unrealistic memory values | Spoofed environments |\n\n### Cross-Context Validation\n\nBots often fail to maintain consistency across execution contexts:\n\n| Detection | Description |\n|-----------|-------------|\n| `hasWebdriverIframe` | webdriver detected in iframe but not main |\n| `hasWebdriverWorker` | webdriver detected in web worker |\n| `hasMismatchPlatformIframe` | Platform differs between main and iframe |\n| `hasMismatchPlatformWorker` | Platform differs between main and worker |\n| `hasMismatchWebGLInWorker` | WebGL renderer differs in worker |\n\n---\n\n## Fingerprint ID (fsid) Format\n\nThe `fsid` is a JA4-inspired, locality-preserving fingerprint identifier. Unlike a simple hash, it's structured into semantic sections, making it both human-readable and useful for partial matching.\n\n### Format\n\n```\nFS1_<det>_<auto>_<dev>_<brw>_<gfx>_<cod>_<loc>_<ctx>\n```\n\n### Example\n\n```\nFS1_00000100000000_10010h3f2a_1728x1117c14m08b01011h4e7a9f_f1101011001e00000000p1100h2c8b1e_0h9d3f7a_1h6a2e4c_en4tEurope-Paris_hab12_0000h3e9f\n```\n\n### Section Breakdown\n\n| # | Section | Format | Example | Description |\n|---|---------|--------|---------|-------------|\n| 1 | **Version** | `FS1` | `FS1` | Fingerprint Scanner version 1 |\n| 2 | **Detection** | n-bit bitmask (21 bits in FS1) | `000001000000000000000` | All fastBotDetectionDetails booleans (extensible) |\n| 3 | **Automation** | `<5-bit>h<hash>` | `10010h3f2a` | Automation booleans + hash |\n| 4 | **Device** | `<W>x<H>c<cpu>m<mem>b<5-bit>h<hash>` | `1728x1117c14m08b01011h4e7a9f` | Screen, cpu, memory, device booleans + hash |\n| 5 | **Browser** | `f<10-bit>e<8-bit>p<4-bit>h<hash>` | `f1101011001e00000000p1100h2c8b1e` | Features + extensions + plugins bitmasks + hash |\n| 6 | **Graphics** | `<1-bit>h<hash>` | `0h9d3f7a` | hasModifiedCanvas + hash |\n| 7 | **Codecs** | `<1-bit>h<hash>` | `1h6a2e4c` | hasMediaSource + hash |\n| 8 | **Locale** | `<lang><n>t<tz>_h<hash>` | `en4tEurope-Paris_hab12` | Language code + count + timezone + hash |\n| 9 | **Contexts** | `<4-bit>h<hash>` | `0000h3e9f` | Mismatch + webdriver flags + hash |\n\n### Why This Format?\n\nInspired by [JA4+](https://github.com/FoxIO-LLC/ja4), this format enables:\n\n1. **Partial Matching** — Compare specific sections across fingerprints (same GPU but different screen?)\n2. **Human Readability** — `1728x1117c14m08` = 1728×1117 screen, 14 cores, 8GB RAM\n3. **Extensibility** — Adding a new boolean check appends a bit without breaking existing positions\n4. **Similarity Detection** — Bots from the same framework often share automation/browser hashes\n\n<details>\n<summary><strong>Bitmask Reference</strong></summary>\n\n#### Detection Bitmask (21 bits in FS1, extensible)\n\n> ⚠️ **Note**: The number of detection bits increases as new bot detection checks are added. Always check the fingerprint version (FS1, FS2, etc.) to know the exact bit count and meaning. New checks are appended to maintain backward compatibility.\n\n```\nBit 0:  headlessChromeScreenResolution\nBit 1:  hasWebdriver\nBit 2:  hasWebdriverWritable\nBit 3:  hasSeleniumProperty\nBit 4:  hasCDP\nBit 5:  hasPlaywright\nBit 6:  hasImpossibleDeviceMemory\nBit 7:  hasHighCPUCount\nBit 8:  hasMissingChromeObject\nBit 9:  hasWebdriverIframe\nBit 10: hasWebdriverWorker\nBit 11: hasMismatchWebGLInWorker\nBit 12: hasMismatchPlatformIframe\nBit 13: hasMismatchPlatformWorker\nBit 14: hasSwiftshaderRenderer\nBit 15: hasUTCTimezone\nBit 16: hasMismatchLanguages\nBit 17: hasInconsistentEtsl\nBit 18: hasBotUserAgent\nBit 19: hasGPUMismatch\nBit 20: hasPlatformMismatch\n```\n\n#### Automation Bitmask (5 bits)\n\n```\nBit 0: webdriver\nBit 1: webdriverWritable\nBit 2: selenium\nBit 3: cdp\nBit 4: playwright\n```\n\n#### Device Bitmask (5 bits)\n\n```\nBit 0: hasMultipleDisplays\nBit 1: prefersReducedMotion\nBit 2: prefersReducedTransparency\nBit 3: hover\nBit 4: anyHover\n```\n\n#### Browser Features Bitmask (28 bits in FS1, extensible)\n\n```\nBit 0:  chrome\nBit 1:  brave\nBit 2:  applePaySupport\nBit 3:  opera\nBit 4:  serial\nBit 5:  attachShadow\nBit 6:  caches\nBit 7:  webAssembly\nBit 8:  buffer\nBit 9:  showModalDialog\nBit 10: safari\nBit 11: webkitPrefixedFunction\nBit 12: mozPrefixedFunction\nBit 13: usb\nBit 14: browserCapture\nBit 15: paymentRequestUpdateEvent\nBit 16: pressureObserver\nBit 17: audioSession\nBit 18: selectAudioOutput\nBit 19: barcodeDetector\nBit 20: battery\nBit 21: devicePosture\nBit 22: documentPictureInPicture\nBit 23: eyeDropper\nBit 24: editContext\nBit 25: fencedFrame\nBit 26: sanitizer\nBit 27: otpCredential\n```\n\n#### Browser Extensions Bitmask (8 bits)\n\n```\nBit 0: grammarly\nBit 1: metamask\nBit 2: couponBirds\nBit 3: deepL\nBit 4: monicaAI\nBit 5: siderAI\nBit 6: requestly\nBit 7: veepn\n```\n\n#### Plugins Bitmask (4 bits)\n\n```\nBit 0: isValidPluginArray\nBit 1: pluginConsistency1\nBit 2: pluginOverflow\nBit 3: hasToSource\n```\n\n#### Contexts Bitmask (4 bits)\n\n```\nBit 0: iframe mismatch\nBit 1: worker mismatch\nBit 2: iframe.webdriver\nBit 3: webWorker.webdriver\n```\n\n</details>\n\n---\n\n## Server-Side Decryption\n\nThe library uses a simple XOR cipher with Base64 encoding. This is easy to implement in any language.\n\n### Node.js\n\n```javascript\nfunction decryptFingerprint(ciphertext, key) {\n  const encrypted = Buffer.from(ciphertext, 'base64');\n  const keyBytes = Buffer.from(key, 'utf8');\n  const decrypted = Buffer.alloc(encrypted.length);\n\n  for (let i = 0; i < encrypted.length; i++) {\n    decrypted[i] = encrypted[i] ^ keyBytes[i % keyBytes.length];\n  }\n\n  let fingerprint = JSON.parse(decrypted.toString('utf8'));\n  // Handle double-JSON-encoding if present\n  if (typeof fingerprint === 'string') {\n    fingerprint = JSON.parse(fingerprint);\n  }\n  return fingerprint;\n}\n```\n\n### Python\n\n```python\nimport base64\nimport json\n\ndef decrypt_fingerprint(ciphertext: str, key: str) -> dict:\n    encrypted = base64.b64decode(ciphertext)\n    key_bytes = key.encode('utf-8')\n\n    decrypted = bytearray(len(encrypted))\n    for i in range(len(encrypted)):\n        decrypted[i] = encrypted[i] ^ key_bytes[i % len(key_bytes)]\n\n    fingerprint = json.loads(decrypted.decode('utf-8'))\n    # Handle double-JSON-encoding if present\n    if isinstance(fingerprint, str):\n        fingerprint = json.loads(fingerprint)\n    return fingerprint\n```\n\n### Other Languages\n\nThe algorithm is straightforward to port:\n\n1. **Base64 decode** the ciphertext to get raw bytes\n2. **XOR each byte** with the corresponding key byte (cycling through the key)\n3. **Decode** the result as UTF-8 to get the JSON string\n4. **Parse** the JSON to get the fingerprint object\n\nSee the [`examples/`](./examples/) folder for complete Node.js and Python server examples.\n\n---\n\n## Advanced: Custom Builds\n\nBy default, fpscanner uses a placeholder key that gets replaced when you run the build command. For production, you should use your own encryption key and enable obfuscation to make it harder for attackers to forge payloads.\n\n### Bring Your Own Encryption/Obfuscation\n\nThe library provides built-in encryption and obfuscation, but **you're not required to use them**. If you prefer:\n\n- Use `collectFingerprint({ encrypt: false })` to get the raw fingerprint object\n- Apply your own encryption, signing, or encoding before sending to your server\n- Run your own obfuscation tool (Terser, JavaScript Obfuscator, etc.) on your bundle\n\nThe recommended approach is to use **some form of** encryption + obfuscation — whether that's the library's built-in solution or your own. The key is to prevent attackers from easily forging payloads without executing the actual collection code.\n\n### Why Custom Builds?\n\n| Threat | Without Protection | With Encryption + Obfuscation |\n|--------|---------------------|-------------------|\n| Payload forgery | Attacker can craft fake fingerprints | Key is hidden in obfuscated code |\n| Replay attacks | Attacker captures and replays fingerprints | Server validates timestamp + nonce |\n| Code inspection | Detection logic is readable | Control flow obfuscation makes analysis harder |\n\n> **Note**: Obfuscation is not encryption. A determined attacker can still reverse-engineer the code. The goal is to raise the bar and force attackers to invest significant effort, not to create an impenetrable system.\n\n### Build with Your Key\n\n```bash\nnpx fpscanner build --key=your-secret-key-here\n```\n\nThis will:\n1. Create backups of original files (for idempotent rebuilds)\n2. Inject your encryption key into the library\n3. Obfuscate the output to protect the key\n4. Overwrite the files in `node_modules/fpscanner/dist/`\n\n> **Note**: The build command is idempotent - you can run it multiple times safely. Original files are backed up as `.original` and restored before each build, preventing issues with re-obfuscating already-obfuscated code. This makes the build safe to use in watch mode or repeated CI/CD runs.\n\n### Key Injection Methods\n\nThe CLI supports multiple methods (in order of priority):\n\n```bash\n# 1. Command line argument (highest priority)\nnpx fpscanner build --key=your-secret-key\n\n# 2. Environment variable\nexport FINGERPRINT_KEY=your-secret-key\nnpx fpscanner build\n\n# 3. .env file\necho \"FINGERPRINT_KEY=your-secret-key\" >> .env\nnpx fpscanner build\n\n# 4. Custom env file\nnpx fpscanner build --env-file=.env.production\n```\n\n### CI/CD Integration\n\nAdd a `postinstall` script to automatically build with your key:\n\n```json\n{\n  \"scripts\": {\n    \"postinstall\": \"fpscanner build\"\n  }\n}\n```\n\nThen set `FINGERPRINT_KEY` as a secret in your CI/CD:\n\n**GitHub Actions:**\n\n```yaml\nenv:\n  FINGERPRINT_KEY: ${{ secrets.FINGERPRINT_KEY }}\n\nsteps:\n  - run: npm install  # postinstall runs fpscanner build automatically\n```\n\n### Build Options\n\n| Option | Description |\n|--------|-------------|\n| `--key=KEY` | Encryption key (highest priority) |\n| `--env-file=FILE` | Load key from custom env file |\n| `--no-obfuscate` | Skip obfuscation (faster, for development) |\n\n#### Skip Obfuscation\n\nObfuscation is enabled by default. For faster builds during development:\n\n```bash\n# Via CLI flag\nnpx fpscanner build --key=dev-key --no-obfuscate\n\n# Via environment variable\nFINGERPRINT_OBFUSCATE=false npx fpscanner build\n\n# In .env file\nFINGERPRINT_OBFUSCATE=false\n```\n\n> ⚠️ **Warning**: Without obfuscation, the encryption key is visible in plain text in the source code. This means attackers can easily extract the key and forge fingerprint payloads without running the actual collection code. If you skip the library's obfuscation, make sure you apply your own obfuscation to the final bundle.\n\n---\n\n## Development\n\n### Local Development Scripts\n\n```bash\n# Quick build (default key, no obfuscation)\nnpm run build\n\n# Build with dev-key, no obfuscation\nnpm run build:dev\n\n# Build + serve test/dev-source.html at localhost:3000\nnpm run dev\n\n# Build with obfuscation\nnpm run build:obfuscate\n\n# Production build (key from .env, with obfuscation)\nnpm run build:prod\n\n# Watch mode (rebuilds on changes)\nnpm run watch\n```\n\n### Testing\n\n```bash\nnpm test\n```\n\n---\n\n## Security Best Practices\n\n1. **Use a strong, random key and rotate it regularly**  \n   Use a high-entropy key (at least 32 random characters) and rotate it periodically. Because the encryption key is shipped client-side in the JavaScript bundle, long-lived keys give attackers more time to extract and reuse them. Rotating the key forces attackers to re-analyze and re-adapt, and requires rebuilding and redeploying the fingerprinting script.\n\n2. **Use obfuscation in production**  \n   Enable the library’s built-in obfuscation or apply your own obfuscation step to the final bundle. Without obfuscation, the encryption key is visible in plain text in the client-side code, making it trivial to forge payloads without executing the fingerprinting logic. Obfuscation raises the cost of key extraction and payload forgery.\n\n3. **Validate timestamps server-side**  \n   Reject fingerprints that are older than a reasonable threshold (for example, 60 seconds). This limits the usefulness of captured payloads and reduces the impact of replay attacks.\n\n4. **Track nonces**  \n   Optionally store recently seen nonces and reject duplicates. This provides an additional layer of replay protection, especially for high-value or abuse-prone endpoints.\n\n5. **Monitor fingerprint distributions over time**  \n   Do not treat fingerprinting as a one-shot decision. Monitor how fingerprints evolve and distribute over time. Sudden spikes, new dominant fingerprints, or unusual reuse patterns can indicate automated or malicious activity, even if individual requests do not trigger explicit bot detection flags.\n\n6. **Defense in depth on sensitive endpoints**  \n   When protecting sensitive flows (signup, login, password reset, API access), combine this library with other controls such as fingerprint-based rate limiting, behavioral analysis, disposable emails detection and challenge mechanisms like CAPTCHAs or risk-based authentication. Fingerprinting works best as one layer in a broader detection and mitigation strategy.\n\n\n---\n\n## Troubleshooting\n\n### \"No encryption key found!\"\n\nProvide a key via one of the supported methods:\n\n```bash\nnpx fpscanner build --key=your-key\n# or\nexport FINGERPRINT_KEY=your-key && npx fpscanner build\n# or\necho \"FINGERPRINT_KEY=your-key\" >> .env && npx fpscanner build\n```\n\n### Decryption returns garbage\n\nMake sure you're using the **exact same key** on your server that you used when building. Keys must match exactly.\n\n### Obfuscation is slow\n\nUse `--no-obfuscate` during development. Only enable obfuscation for production builds.\n\n### Build fails with \"heap out of memory\" in watch mode\n\nIf you're running the build command repeatedly (e.g., in a watch task), this is now fixed. The build script automatically backs up and restores original files to prevent re-obfuscating already-obfuscated code. If you still encounter this issue:\n\n1. Update to the latest version of fpscanner\n2. Use `--no-obfuscate` for development/watch mode (recommended)\n3. Only enable obfuscation for production builds\n\n### `postinstall` fails in CI\n\nEnsure `FINGERPRINT_KEY` is set as an environment variable before `npm install` runs.\n\n---\n\n## Limits and non-goals\n\nThis library provides building blocks, not a complete bot or fraud detection system. It is important to understand its limits before using it in production.\n\n### Open source and attacker adaptation\n\nThe library is open source, which means attackers can inspect the code and adapt their tooling. This is expected and reflects how the ecosystem already works. Defenders routinely analyze automation frameworks and anti-detect browsers, and attackers do the same with detection logic.\n\nThe goal is not secrecy, but to make abuse operationally expensive by forcing real execution, limiting replay, and preserving consistency constraints that are difficult to fake at scale.\n\n### Obfuscation is not a silver bullet\n\nThe optional obfuscation relies on an open source obfuscator, and some attackers maintain deobfuscation tooling for it. Obfuscation is a friction mechanism, not a guarantee. It slows down analysis and discourages low-effort abuse, but motivated attackers can still reverse-engineer the code.\n\n### Limits of client-side detection\n\nAll client-side fingerprinting and bot detection techniques can be spoofed or emulated. This library focuses on strong, low-noise signals, but no individual signal or fingerprint should be treated as definitive.\n\nFingerprints are representations, not verdicts. Their value comes from observing how they behave over time, how often they appear, and how they correlate with actions, IPs, and accounts.\n\n### Not an end-to-end solution\n\nReal-world bot and fraud detection requires server-side context, observability, and iteration: the ability to monitor traffic, build and test rules, and adapt over time. This library intentionally does not provide dashboards, rule engines, or managed mitigation.\n\nIf you need a production-grade, end-to-end solution with observability and ongoing maintenance, consider using a dedicated platform like [Castle](https://castle.io/).\n\n\n---\n\n## License\n\nMIT\n"
  },
  {
    "path": "bin/cli.js",
    "content": "#!/usr/bin/env node\n\nconst path = require('path');\nconst fs = require('fs');\n\n/**\n * Load environment variables from a file\n * Supports .env format: KEY=value\n */\nfunction loadEnvFile(filePath) {\n  if (!fs.existsSync(filePath)) {\n    return {};\n  }\n  \n  const content = fs.readFileSync(filePath, 'utf8');\n  const env = {};\n  \n  for (const line of content.split('\\n')) {\n    // Skip comments and empty lines\n    const trimmed = line.trim();\n    if (!trimmed || trimmed.startsWith('#')) continue;\n    \n    const match = trimmed.match(/^([^=]+)=(.*)$/);\n    if (match) {\n      const key = match[1].trim();\n      let value = match[2].trim();\n      // Remove surrounding quotes if present\n      if ((value.startsWith('\"') && value.endsWith('\"')) ||\n          (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n        value = value.slice(1, -1);\n      }\n      env[key] = value;\n    }\n  }\n  \n  return env;\n}\n\n/**\n * Resolve the encryption key from multiple sources\n * Priority: --key flag > environment variable > .env file\n */\nfunction resolveKey(args, cwd) {\n  // 1. Check for explicit --key=xxx argument (highest priority)\n  const keyArg = args.find(a => a.startsWith('--key='));\n  if (keyArg) {\n    const key = keyArg.split('=').slice(1).join('='); // Handle keys with = in them\n    console.log('🔑 Using key from --key argument');\n    return key;\n  }\n  \n  // 2. Check for environment variable (already loaded, e.g., from CI)\n  if (process.env.FINGERPRINT_KEY) {\n    console.log('🔑 Using FINGERPRINT_KEY from environment');\n    return process.env.FINGERPRINT_KEY;\n  }\n  \n  // 3. Try to load from .env file (or custom file via --env-file)\n  const envFileArg = args.find(a => a.startsWith('--env-file='));\n  const envFileName = envFileArg ? envFileArg.split('=')[1] : '.env';\n  const envFilePath = path.isAbsolute(envFileName) \n    ? envFileName \n    : path.join(cwd, envFileName);\n  \n  const envFromFile = loadEnvFile(envFilePath);\n  \n  if (envFromFile.FINGERPRINT_KEY) {\n    console.log(`🔑 Using FINGERPRINT_KEY from ${path.basename(envFilePath)}`);\n    return envFromFile.FINGERPRINT_KEY;\n  }\n  \n  // 4. No key found\n  return null;\n}\n\nfunction printHelp() {\n  console.log(`\n📦 fpscanner CLI\n\nCommands:\n  build     Build fpscanner with your custom encryption key\n\nUsage:\n  npx fpscanner build [options]\n\nOptions:\n  --key=KEY           Use KEY as the encryption key (highest priority)\n  --env-file=FILE     Load FINGERPRINT_KEY from FILE (default: .env)\n  --no-obfuscate      Skip obfuscation step (faster builds, readable output)\n\nEnvironment Variables:\n  FINGERPRINT_KEY         The encryption key (if not using --key)\n  FINGERPRINT_OBFUSCATE   Set to \"false\" to skip obfuscation (default: true)\n\nKey Resolution (in order of priority):\n  1. --key=xxx argument\n  2. FINGERPRINT_KEY environment variable\n  3. FINGERPRINT_KEY in .env file (or custom file via --env-file)\n\nObfuscation Control:\n  1. --no-obfuscate flag (highest priority)\n  2. FINGERPRINT_OBFUSCATE=false environment variable\n  3. Default: obfuscation enabled\n\nExamples:\n  # Using command line argument\n  npx fpscanner build --key=my-secret-key\n\n  # Using environment variable\n  export FINGERPRINT_KEY=my-secret-key\n  npx fpscanner build\n\n  # Using .env file\n  echo \"FINGERPRINT_KEY=my-secret-key\" >> .env\n  npx fpscanner build\n\n  # Using custom env file\n  npx fpscanner build --env-file=.env.production\n\n  # Skip obfuscation (CLI flag)\n  npx fpscanner build --key=my-key --no-obfuscate\n\n  # Skip obfuscation (environment variable)\n  FINGERPRINT_OBFUSCATE=false npx fpscanner build\n\n  # Production without obfuscation (postinstall compatible)\n  FINGERPRINT_KEY=xxx FINGERPRINT_OBFUSCATE=false npm install\n\nSetup (add to your package.json):\n  {\n    \"scripts\": {\n      \"postinstall\": \"fpscanner build\"\n    }\n  }\n\n  Then install with your chosen options:\n    FINGERPRINT_KEY=xxx npm install                         # With obfuscation\n    FINGERPRINT_KEY=xxx FINGERPRINT_OBFUSCATE=false npm install  # Without obfuscation\n`);\n}\n\n// Main\nconst args = process.argv.slice(2);\nconst command = args[0];\n\nif (!command || command === 'help' || command === '--help' || command === '-h') {\n  printHelp();\n  process.exit(0);\n}\n\nif (command === 'build') {\n  const cwd = process.cwd();\n  const key = resolveKey(args, cwd);\n  \n  if (!key) {\n    console.error(`\n❌ No encryption key found!\n\nProvide a key using one of these methods:\n\n  1. Command line argument:\n     npx fpscanner build --key=your-secret-key\n\n  2. Environment variable:\n     export FINGERPRINT_KEY=your-secret-key\n     npx fpscanner build\n\n  3. .env file in your project root:\n     echo \"FINGERPRINT_KEY=your-secret-key\" >> .env\n     npx fpscanner build\n\n  4. Custom env file:\n     npx fpscanner build --env-file=.env.production\n\nRun 'npx fpscanner --help' for more information.\n`);\n    process.exit(1);\n  }\n  \n  // Check for --no-obfuscate flag OR FINGERPRINT_OBFUSCATE=false environment variable\n  // Priority: CLI flag > environment variable > default (obfuscate)\n  let skipObfuscation = args.includes('--no-obfuscate');\n  if (!skipObfuscation && process.env.FINGERPRINT_OBFUSCATE !== undefined) {\n    const envValue = process.env.FINGERPRINT_OBFUSCATE.toLowerCase();\n    skipObfuscation = envValue === 'false' || envValue === '0' || envValue === 'no';\n    if (skipObfuscation) {\n      console.log('⚙️  Obfuscation disabled via FINGERPRINT_OBFUSCATE environment variable');\n    }\n  }\n  \n  // Run the build script\n  const packageDir = path.dirname(__dirname);\n  const buildScript = path.join(packageDir, 'scripts', 'build-custom.js');\n  \n  // Pass arguments to build script\n  const buildArgs = [\n    `--key=${key}`,\n    `--package-dir=${packageDir}`,\n  ];\n  if (skipObfuscation) {\n    buildArgs.push('--no-obfuscate');\n  }\n  \n  require(buildScript)(buildArgs)\n    .then(() => {\n      // Build completed successfully\n    })\n    .catch((err) => {\n      console.error('❌ Build failed:', err.message);\n      process.exit(1);\n    });\n} else {\n  console.error(`Unknown command: ${command}`);\n  console.log('Run \"npx fpscanner --help\" for usage information.');\n  process.exit(1);\n}\n"
  },
  {
    "path": "examples/nodejs/README.md",
    "content": "# FPScanner Node.js Demo\n\nThis example demonstrates how to use fpscanner with a Node.js backend server.\n\n## What it does\n\n1. **Client side**: The HTML page loads the fpscanner library and collects an encrypted fingerprint\n2. **Server side**: The Node.js server receives the encrypted fingerprint, decrypts it, and logs the result\n\n## Prerequisites\n\n- Node.js installed\n- fpscanner built with the `dev-key` (or your custom key)\n\n## Setup\n\n1. First, build fpscanner with the dev key (from the fpscanner root directory):\n\n```bash\nnpm run build:obfuscate\n```\n\nThis builds the library with the `dev-key` encryption key.\n\n2. Navigate to this example directory:\n\n```bash\ncd examples/nodejs\n```\n\n3. Start the demo server:\n\n```bash\nnode demo-server.js\n```\n\n4. Open your browser and navigate to:\n\n```\nhttp://localhost:3000\n```\n\n## Expected Output\n\nWhen you open the page, you should see:\n\n1. **In the browser**: A success message showing the fingerprint was received\n2. **In the terminal**: The decrypted fingerprint with a summary like:\n\n```\n════════════════════════════════════════════════════════════\n📥 Received fingerprint from client\n════════════════════════════════════════════════════════════\n\n🔓 Decrypted fingerprint:\n{\n  \"signals\": {\n    \"webdriver\": false,\n    \"userAgent\": \"Mozilla/5.0 ...\",\n    ...\n  },\n  \"fsid\": \"FS1_00000_abc123_...\",\n  ...\n}\n\n📊 Summary:\n   FSID: FS1_00000_abc123_...\n   Platform: MacIntel\n   CPU Count: 8\n   Memory: 16 GB\n   Screen: 1920x1080\n   Bot Detection: ✓ OK\n════════════════════════════════════════════════════════════\n```\n\n## Using a Custom Encryption Key\n\nTo use your own encryption key:\n\n1. Build fpscanner with your key:\n\n```bash\nFINGERPRINT_KEY=your-secret-key npm run build:prod\n```\n\n2. Set the same key when running the server:\n\n```bash\nFINGERPRINT_KEY=your-secret-key node demo-server.js\n```\n\n## Files\n\n- `index.html` - Client-side page that collects and sends the fingerprint\n- `demo-server.js` - Node.js server that decrypts and logs fingerprints\n- `README.md` - This file\n"
  },
  {
    "path": "examples/nodejs/demo-server.js",
    "content": "/**\n * FPScanner Demo Server (Node.js)\n * \n * This server demonstrates how to receive and decrypt fingerprints\n * collected by fpscanner on the client side.\n */\n\nconst http = require('http');\nconst fs = require('fs');\nconst path = require('path');\n\nconst PORT = 3000;\n\n// The encryption key - must match the key used when building fpscanner\n// In production, this should come from environment variables\nconst ENCRYPTION_KEY = process.env.FINGERPRINT_KEY || 'dev-key';\n\n/**\n * Decrypt an XOR-encrypted, base64-encoded string\n */\nfunction decryptString(ciphertext, key) {\n    const binaryString = Buffer.from(ciphertext, 'base64').toString('binary');\n    const encrypted = new Uint8Array(binaryString.length);\n    for (let i = 0; i < binaryString.length; i++) {\n        encrypted[i] = binaryString.charCodeAt(i);\n    }\n\n    const keyBytes = Buffer.from(key, 'utf8');\n    const decrypted = new Uint8Array(encrypted.length);\n\n    for (let i = 0; i < encrypted.length; i++) {\n        decrypted[i] = encrypted[i] ^ keyBytes[i % keyBytes.length];\n    }\n\n    return Buffer.from(decrypted).toString('utf8');\n}\n\n/**\n * Decrypt and parse a fingerprint payload\n */\nfunction decryptFingerprint(encryptedFingerprint) {\n    const decryptedJson = decryptString(encryptedFingerprint, ENCRYPTION_KEY);\n    let parsed = JSON.parse(decryptedJson);\n    // Handle double-JSON-encoding (string containing JSON)\n    if (typeof parsed === 'string') {\n        parsed = JSON.parse(parsed);\n    }\n    return parsed;\n}\n\n// MIME types for serving static files\nconst MIME_TYPES = {\n    '.html': 'text/html',\n    '.js': 'application/javascript',\n    '.css': 'text/css',\n    '.json': 'application/json',\n};\n\nconst server = http.createServer(async (req, res) => {\n    // Handle fingerprint submission\n    if (req.method === 'POST' && req.url === '/api/fingerprint') {\n        let body = '';\n        req.on('data', chunk => body += chunk);\n        req.on('end', () => {\n            try {\n                const { fingerprint: encryptedFingerprint } = JSON.parse(body);\n                \n                // Decrypt the fingerprint\n                const fingerprint = decryptFingerprint(encryptedFingerprint);\n                \n                // Log the decrypted fingerprint\n                console.log('\\n' + '='.repeat(60));\n                console.log('📥 Received fingerprint from client');\n                console.log('='.repeat(60));\n                console.log('\\n🔓 Decrypted fingerprint:');\n                console.log(JSON.stringify(fingerprint, null, 2));\n                console.log('\\n📊 Summary:');\n                console.log(`   FSID: ${fingerprint.fsid}`);\n                console.log(`   Platform: ${fingerprint.signals.device.platform}`);\n                console.log(`   User Agent: ${fingerprint.signals.browser.userAgent.substring(0, 50)}...`);\n                console.log(`   CPU Count: ${fingerprint.signals.device.cpuCount}`);\n                console.log(`   Memory: ${fingerprint.signals.device.memory} GB`);\n                console.log(`   Screen: ${fingerprint.signals.device.screenResolution.width}x${fingerprint.signals.device.screenResolution.height}`);\n                console.log(`   Bot Detection: ${fingerprint.fastBotDetection ? '⚠️  SUSPICIOUS' : '✓ OK'}`);\n                console.log('='.repeat(60) + '\\n');\n                \n                // Send response with full fingerprint\n                res.writeHead(200, { 'Content-Type': 'application/json' });\n                res.end(JSON.stringify({\n                    success: true,\n                    fingerprint: fingerprint\n                }));\n            } catch (error) {\n                console.error('❌ Error processing fingerprint:', error.message);\n                res.writeHead(400, { 'Content-Type': 'application/json' });\n                res.end(JSON.stringify({ success: false, error: error.message }));\n            }\n        });\n        return;\n    }\n    \n    // Serve static files\n    let filePath;\n    if (req.url === '/' || req.url === '/index.html') {\n        filePath = path.join(__dirname, 'index.html');\n    } else {\n        // Serve files from the fpscanner root (for dist folder access)\n        filePath = path.join(__dirname, '../..', req.url);\n    }\n    \n    const ext = path.extname(filePath);\n    const contentType = MIME_TYPES[ext] || 'text/plain';\n    \n    fs.readFile(filePath, (err, content) => {\n        if (err) {\n            res.writeHead(404);\n            res.end(`Not found: ${req.url}`);\n            return;\n        }\n        res.writeHead(200, { 'Content-Type': contentType });\n        res.end(content);\n    });\n});\n\nserver.listen(PORT, () => {\n    console.log(`\n╔════════════════════════════════════════════════════════╗\n║          FPScanner Demo Server (Node.js)               ║\n╠════════════════════════════════════════════════════════╣\n║  Server running at: http://localhost:${PORT}              ║\n║  Open this URL in your browser to test                 ║\n╚════════════════════════════════════════════════════════╝\n`);\n});\n"
  },
  {
    "path": "examples/nodejs/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>FPScanner Demo - Node.js</title>\n    <style>\n        body {\n            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n            max-width: 600px;\n            margin: 50px auto;\n            padding: 20px;\n            background: #f5f5f5;\n        }\n        .card {\n            background: white;\n            border-radius: 8px;\n            padding: 24px;\n            box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n        }\n        h1 { color: #333; margin-top: 0; }\n        .status {\n            padding: 12px;\n            border-radius: 4px;\n            margin-top: 16px;\n        }\n        .status.loading { background: #fff3cd; color: #856404; }\n        .status.success { background: #d4edda; color: #155724; }\n        .status.error { background: #f8d7da; color: #721c24; }\n        pre {\n            background: #1e1e1e;\n            color: #d4d4d4;\n            padding: 16px;\n            border-radius: 6px;\n            overflow-x: auto;\n            font-size: 13px;\n            line-height: 1.5;\n            max-height: 500px;\n            overflow-y: auto;\n        }\n        .key { color: #9cdcfe; }\n        .string { color: #ce9178; }\n        .number { color: #b5cea8; }\n        .boolean { color: #569cd6; }\n        .null { color: #569cd6; }\n        h3 { margin-top: 20px; color: #333; }\n    </style>\n</head>\n<body>\n    <div class=\"card\">\n        <h1>🔍 FPScanner Demo</h1>\n        <p>This page collects a browser fingerprint and sends it to the Node.js server for decryption.</p>\n        <div id=\"status\" class=\"status loading\">Collecting fingerprint...</div>\n        <div id=\"result\"></div>\n    </div>\n\n    <script type=\"module\">\n        // Import the fingerprint scanner from the dist folder\n        // In production, this would be your built fpscanner package\n        import FingerprintScanner from '../../dist/fpScanner.es.js';\n        \n        const statusEl = document.getElementById('status');\n        const resultEl = document.getElementById('result');\n        \n        async function run() {\n            try {\n                // Collect the encrypted fingerprint\n                const scanner = new FingerprintScanner();\n                const encryptedFingerprint = await scanner.collectFingerprint({ encrypt: true });\n                \n                statusEl.textContent = 'Sending to server...';\n                \n                // Send to the server\n                const response = await fetch('/api/fingerprint', {\n                    method: 'POST',\n                    headers: { 'Content-Type': 'application/json' },\n                    body: JSON.stringify({ fingerprint: encryptedFingerprint })\n                });\n                \n                const data = await response.json();\n                \n                if (data.success) {\n                    statusEl.className = 'status success';\n                    statusEl.textContent = '✓ Fingerprint received and decrypted by server!';\n                    resultEl.innerHTML = `\n                        <h3>Decrypted Fingerprint:</h3>\n                        <pre>${syntaxHighlight(data.fingerprint)}</pre>\n                    `;\n                } else {\n                    throw new Error(data.error || 'Server error');\n                }\n            } catch (error) {\n                statusEl.className = 'status error';\n                statusEl.textContent = '✗ Error: ' + error.message;\n                console.error(error);\n            }\n        }\n        \n        // Syntax highlighting for JSON\n        function syntaxHighlight(obj) {\n            let json = JSON.stringify(obj, null, 2);\n            json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n            return json.replace(/(\"(\\\\u[a-zA-Z0-9]{4}|\\\\[^u]|[^\\\\\"])*\"(\\s*:)?|\\b(true|false|null)\\b|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?)/g, function (match) {\n                let cls = 'number';\n                if (/^\"/.test(match)) {\n                    if (/:$/.test(match)) {\n                        cls = 'key';\n                    } else {\n                        cls = 'string';\n                    }\n                } else if (/true|false/.test(match)) {\n                    cls = 'boolean';\n                } else if (/null/.test(match)) {\n                    cls = 'null';\n                }\n                return '<span class=\"' + cls + '\">' + match + '</span>';\n            });\n        }\n        \n        run();\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "examples/python/README.md",
    "content": "# FPScanner Python Demo\n\nThis example demonstrates how to use fpscanner with a Python backend server.\n\n## What it does\n\n1. **Client side**: The HTML page loads the fpscanner library and collects an encrypted fingerprint\n2. **Server side**: The Python server receives the encrypted fingerprint, decrypts it, and logs the result\n\n## Prerequisites\n\n- Python 3.6+ installed\n- fpscanner built with the `dev-key` (or your custom key)\n\n## Setup\n\n1. First, build fpscanner with the dev key (from the fpscanner root directory):\n\n```bash\nnpm run build:obfuscate\n```\n\nThis builds the library with the `dev-key` encryption key.\n\n2. Navigate to this example directory:\n\n```bash\ncd examples/python\n```\n\n3. Start the demo server:\n\n```bash\npython3 demo-server.py\n```\n\n4. Open your browser and navigate to:\n\n```\nhttp://localhost:3000\n```\n\n## Expected Output\n\nWhen you open the page, you should see:\n\n1. **In the browser**: A success message showing the fingerprint was received\n2. **In the terminal**: The decrypted fingerprint with a summary like:\n\n```\n════════════════════════════════════════════════════════════\n📥 Received fingerprint from client\n════════════════════════════════════════════════════════════\n\n🔓 Decrypted fingerprint:\n{\n  \"signals\": {\n    \"webdriver\": false,\n    \"userAgent\": \"Mozilla/5.0 ...\",\n    ...\n  },\n  \"fsid\": \"FS1_00000_abc123_...\",\n  ...\n}\n\n📊 Summary:\n   FSID: FS1_00000_abc123_...\n   Platform: MacIntel\n   CPU Count: 8\n   Memory: 16 GB\n   Screen: 1920x1080\n   Bot Detection: ✓ OK\n════════════════════════════════════════════════════════════\n```\n\n## Using a Custom Encryption Key\n\nTo use your own encryption key:\n\n1. Build fpscanner with your key:\n\n```bash\nFINGERPRINT_KEY=your-secret-key npm run build:prod\n```\n\n2. Set the same key when running the server:\n\n```bash\nFINGERPRINT_KEY=your-secret-key python3 demo-server.py\n```\n\n## Decryption Function\n\nThe key part of the Python server is the decryption function:\n\n```python\nimport base64\n\ndef xor_decrypt(ciphertext_b64: str, key: str) -> str:\n    \"\"\"Decrypt an XOR-encrypted, base64-encoded string.\"\"\"\n    # Decode from base64\n    encrypted = base64.b64decode(ciphertext_b64)\n    key_bytes = key.encode('utf-8')\n    \n    # XOR decrypt\n    decrypted = bytearray(len(encrypted))\n    for i in range(len(encrypted)):\n        decrypted[i] = encrypted[i] ^ key_bytes[i % len(key_bytes)]\n    \n    return decrypted.decode('utf-8')\n```\n\nThis function can be easily integrated into any Python web framework (Flask, Django, FastAPI, etc.).\n\n## Files\n\n- `index.html` - Client-side page that collects and sends the fingerprint\n- `demo-server.py` - Python server that decrypts and logs fingerprints\n- `README.md` - This file\n"
  },
  {
    "path": "examples/python/demo-server.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nFPScanner Demo Server (Python)\n\nThis server demonstrates how to receive and decrypt fingerprints\ncollected by fpscanner on the client side.\n\"\"\"\n\nimport base64\nimport json\nimport os\nfrom http.server import HTTPServer, SimpleHTTPRequestHandler\nfrom urllib.parse import urlparse\n\nPORT = 3010\n\n# The encryption key - must match the key used when building fpscanner\n# In production, this should come from environment variables\nENCRYPTION_KEY = os.environ.get('FINGERPRINT_KEY', 'dev-key')\n\n\ndef xor_decrypt(ciphertext_b64: str, key: str) -> str:\n    \"\"\"\n    Decrypt an XOR-encrypted, base64-encoded string.\n    \n    Args:\n        ciphertext_b64: Base64-encoded encrypted data\n        key: The encryption key (must match the key used for encryption)\n    \n    Returns:\n        Decrypted string\n    \"\"\"\n    # Decode from base64\n    encrypted = base64.b64decode(ciphertext_b64)\n    key_bytes = key.encode('utf-8')\n    \n    # XOR decrypt\n    decrypted = bytearray(len(encrypted))\n    for i in range(len(encrypted)):\n        decrypted[i] = encrypted[i] ^ key_bytes[i % len(key_bytes)]\n    \n    return decrypted.decode('utf-8')\n\n\ndef decrypt_fingerprint(encrypted_fingerprint: str) -> dict:\n    \"\"\"\n    Decrypt and parse a fingerprint payload.\n    \n    Args:\n        encrypted_fingerprint: The encrypted fingerprint from the client\n    \n    Returns:\n        Parsed fingerprint dictionary\n    \"\"\"\n    decrypted_json = xor_decrypt(encrypted_fingerprint, ENCRYPTION_KEY)\n    parsed = json.loads(decrypted_json)\n    # Handle double-JSON-encoding (string containing JSON)\n    if isinstance(parsed, str):\n        parsed = json.loads(parsed)\n    return parsed\n\n\nclass FingerprintHandler(SimpleHTTPRequestHandler):\n    \"\"\"HTTP request handler for the fingerprint demo.\"\"\"\n    \n    def __init__(self, *args, **kwargs):\n        # Set the directory to serve static files from\n        super().__init__(*args, directory=os.path.dirname(os.path.abspath(__file__)), **kwargs)\n    \n    def do_POST(self):\n        \"\"\"Handle POST requests (fingerprint submission).\"\"\"\n        if self.path == '/api/fingerprint':\n            try:\n                # Read the request body\n                content_length = int(self.headers['Content-Length'])\n                body = self.rfile.read(content_length).decode('utf-8')\n                data = json.loads(body)\n                \n                encrypted_fingerprint = data.get('fingerprint')\n                if not encrypted_fingerprint:\n                    raise ValueError('No fingerprint provided')\n                \n                # Decrypt the fingerprint\n                fingerprint = decrypt_fingerprint(encrypted_fingerprint)\n                \n                # Log the decrypted fingerprint\n                print('\\n' + '=' * 60)\n                print('📥 Received fingerprint from client')\n                print('=' * 60)\n                print('\\n🔓 Decrypted fingerprint:')\n                print(json.dumps(fingerprint, indent=2))\n                print('\\n📊 Summary:')\n                print(f\"   FSID: {fingerprint['fsid']}\")\n                print(f\"   Platform: {fingerprint['signals']['device']['platform']}\")\n                print(f\"   User Agent: {fingerprint['signals']['browser']['userAgent'][:50]}...\")\n                print(f\"   CPU Count: {fingerprint['signals']['device']['cpuCount']}\")\n                print(f\"   Memory: {fingerprint['signals']['device']['memory']} GB\")\n                screen = fingerprint['signals']['device']['screenResolution']\n                print(f\"   Screen: {screen['width']}x{screen['height']}\")\n                bot_status = '⚠️  SUSPICIOUS' if fingerprint['fastBotDetection'] else '✓ OK'\n                print(f'   Bot Detection: {bot_status}')\n                print('=' * 60 + '\\n')\n                \n                # Send response with full fingerprint\n                response = {\n                    'success': True,\n                    'fingerprint': fingerprint\n                }\n                self._send_json(200, response)\n                \n            except Exception as e:\n                print(f'❌ Error processing fingerprint: {e}')\n                self._send_json(400, {'success': False, 'error': str(e)})\n        else:\n            self.send_error(404, 'Not Found')\n    \n    def do_GET(self):\n        \"\"\"Handle GET requests (serve static files).\"\"\"\n        parsed = urlparse(self.path)\n        path = parsed.path\n        \n        # Serve index.html for root\n        if path == '/' or path == '/index.html':\n            self.path = '/index.html'\n            return super().do_GET()\n        \n        # Serve files from fpscanner root (for dist folder access)\n        if path.startswith('/dist/'):\n            # Construct path relative to fpscanner root\n            file_path = os.path.join(\n                os.path.dirname(os.path.abspath(__file__)),\n                '../..',\n                path.lstrip('/')\n            )\n            if os.path.exists(file_path):\n                self.send_response(200)\n                if path.endswith('.js'):\n                    self.send_header('Content-Type', 'application/javascript')\n                else:\n                    self.send_header('Content-Type', 'application/octet-stream')\n                self.end_headers()\n                with open(file_path, 'rb') as f:\n                    self.wfile.write(f.read())\n                return\n        \n        return super().do_GET()\n    \n    def _send_json(self, status: int, data: dict):\n        \"\"\"Send a JSON response.\"\"\"\n        self.send_response(status)\n        self.send_header('Content-Type', 'application/json')\n        self.end_headers()\n        self.wfile.write(json.dumps(data).encode('utf-8'))\n    \n    def log_message(self, format, *args):\n        \"\"\"Suppress default logging for cleaner output.\"\"\"\n        if 'POST /api/fingerprint' not in format % args:\n            # Only log non-fingerprint requests\n            pass\n\n\ndef main():\n    server = HTTPServer(('', PORT), FingerprintHandler)\n    print(f'''\n╔════════════════════════════════════════════════════════╗\n║          FPScanner Demo Server (Python)                ║\n╠════════════════════════════════════════════════════════╣\n║  Server running at: http://localhost:{PORT}              ║\n║  Open this URL in your browser to test                 ║\n╚════════════════════════════════════════════════════════╝\n''')\n    try:\n        server.serve_forever()\n    except KeyboardInterrupt:\n        print('\\nServer stopped.')\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "examples/python/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>FPScanner Demo - Python</title>\n    <style>\n        body {\n            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n            max-width: 600px;\n            margin: 50px auto;\n            padding: 20px;\n            background: #f5f5f5;\n        }\n        .card {\n            background: white;\n            border-radius: 8px;\n            padding: 24px;\n            box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n        }\n        h1 { color: #333; margin-top: 0; }\n        .status {\n            padding: 12px;\n            border-radius: 4px;\n            margin-top: 16px;\n        }\n        .status.loading { background: #fff3cd; color: #856404; }\n        .status.success { background: #d4edda; color: #155724; }\n        .status.error { background: #f8d7da; color: #721c24; }\n        pre {\n            background: #1e1e1e;\n            color: #d4d4d4;\n            padding: 16px;\n            border-radius: 6px;\n            overflow-x: auto;\n            font-size: 13px;\n            line-height: 1.5;\n            max-height: 500px;\n            overflow-y: auto;\n        }\n        .key { color: #9cdcfe; }\n        .string { color: #ce9178; }\n        .number { color: #b5cea8; }\n        .boolean { color: #569cd6; }\n        .null { color: #569cd6; }\n        h3 { margin-top: 20px; color: #333; }\n    </style>\n</head>\n<body>\n    <div class=\"card\">\n        <h1>🐍 FPScanner Demo (Python)</h1>\n        <p>This page collects a browser fingerprint and sends it to the Python server for decryption.</p>\n        <div id=\"status\" class=\"status loading\">Collecting fingerprint...</div>\n        <div id=\"result\"></div>\n    </div>\n\n    <script type=\"module\">\n        // Import the fingerprint scanner from the dist folder\n        // In production, this would be your built fpscanner package\n        import FingerprintScanner from '../../dist/fpScanner.es.js';\n        \n        const statusEl = document.getElementById('status');\n        const resultEl = document.getElementById('result');\n        \n        async function run() {\n            try {\n                // Collect the encrypted fingerprint\n                const scanner = new FingerprintScanner();\n                const encryptedFingerprint = await scanner.collectFingerprint();\n                \n                statusEl.textContent = 'Sending to server...';\n                \n                // Send to the server\n                const response = await fetch('/api/fingerprint', {\n                    method: 'POST',\n                    headers: { 'Content-Type': 'application/json' },\n                    body: JSON.stringify({ fingerprint: encryptedFingerprint })\n                });\n                \n                const data = await response.json();\n                \n                if (data.success) {\n                    statusEl.className = 'status success';\n                    statusEl.textContent = '✓ Fingerprint received and decrypted by server!';\n                    resultEl.innerHTML = `\n                        <h3>Decrypted Fingerprint:</h3>\n                        <pre>${syntaxHighlight(data.fingerprint)}</pre>\n                    `;\n                } else {\n                    throw new Error(data.error || 'Server error');\n                }\n            } catch (error) {\n                statusEl.className = 'status error';\n                statusEl.textContent = '✗ Error: ' + error.message;\n                console.error(error);\n            }\n        }\n        \n        // Syntax highlighting for JSON\n        function syntaxHighlight(obj) {\n            let json = JSON.stringify(obj, null, 2);\n            json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n            return json.replace(/(\"(\\\\u[a-zA-Z0-9]{4}|\\\\[^u]|[^\\\\\"])*\"(\\s*:)?|\\b(true|false|null)\\b|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?)/g, function (match) {\n                let cls = 'number';\n                if (/^\"/.test(match)) {\n                    if (/:$/.test(match)) {\n                        cls = 'key';\n                    } else {\n                        cls = 'string';\n                    }\n                } else if (/true|false/.test(match)) {\n                    cls = 'boolean';\n                } else if (/null/.test(match)) {\n                    cls = 'null';\n                }\n                return '<span class=\"' + cls + '\">' + match + '</span>';\n            });\n        }\n        \n        run();\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"fpscanner\",\n  \"version\": \"1.0.2\",\n  \"description\": \"A lightweight browser fingerprinting and bot detection library with encryption, obfuscation, and cross-context validation\",\n  \"main\": \"dist/fpScanner.cjs.js\",\n  \"module\": \"dist/fpScanner.es.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"bin\": {\n    \"fpscanner\": \"bin/cli.js\"\n  },\n  \"files\": [\n    \"dist\",\n    \"src\",\n    \"bin\",\n    \"scripts\"\n  ],\n  \"scripts\": {\n    \"build\": \"vite build && tsc --emitDeclarationOnly\",\n    \"build:vite\": \"vite build\",\n    \"build:dev\": \"node bin/cli.js build --key=dev-key --no-obfuscate\",\n    \"build:prod\": \"node bin/cli.js build\",\n    \"build:prod:plain\": \"node bin/cli.js build --no-obfuscate\",\n    \"build:obfuscate\": \"node bin/cli.js build --key=dev-key\",\n    \"watch\": \"concurrently \\\"FP_ENCRYPTION_KEY=dev-key vite build --watch\\\" \\\"tsc --watch --emitDeclarationOnly\\\"\",\n    \"dev\": \"vite\",\n    \"dev:build\": \"npm run build:dev && vite\",\n    \"dev:obfuscate\": \"npm run build:obfuscate && vite\",\n    \"test\": \"npm run test:playwright\",\n    \"test:vitest\": \"vitest\",\n    \"test:playwright\": \"npm run build:obfuscate && npx playwright test\",\n    \"test:playwright:headed\": \"npm run build:obfuscate && npx playwright test --headed\",\n    \"prepublishOnly\": \"node scripts/verify-publish.js\",\n    \"publish:beta\": \"node scripts/safe-publish.js beta\",\n    \"publish:stable\": \"node scripts/safe-publish.js stable\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/antoinevastel/fpscanner.git\"\n  },\n  \"keywords\": [\n    \"fingerprinting\",\n    \"browser-fingerprint\",\n    \"bot-detection\",\n    \"automation-detection\",\n    \"fraud-detection\",\n    \"selenium\",\n    \"puppeteer\",\n    \"playwright\",\n    \"webdriver\",\n    \"headless\",\n    \"anti-bot\",\n    \"device-fingerprint\",\n    \"browser-detection\",\n    \"security\"\n  ],\n  \"author\": \"antoinevastel <antoine.vastel@gmail.com>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/antoinevastel/fpscanner/issues\"\n  },\n  \"homepage\": \"https://github.com/antoinevastel/fpscanner\",\n  \"dependencies\": {\n    \"javascript-obfuscator\": \"^5.1.0\",\n    \"terser\": \"^5.46.0\",\n    \"ua-parser-js\": \"^0.7.18\"\n  },\n  \"devDependencies\": {\n    \"@playwright/test\": \"^1.57.0\",\n    \"@vitest/ui\": \"^4.0.16\",\n    \"chai\": \"^4.2.0\",\n    \"concurrently\": \"^9.2.1\",\n    \"fpcollect\": \"^1.0.4\",\n    \"mocha\": \"^11.7.5\",\n    \"puppeteer\": \"^1.9.0\",\n    \"typescript\": \"^5.9.3\",\n    \"vite\": \"^7.3.0\",\n    \"vitest\": \"^4.0.16\"\n  }\n}\n"
  },
  {
    "path": "playwright.config.ts",
    "content": "import { defineConfig, devices } from '@playwright/test';\n\nexport default defineConfig({\n  testDir: './test',\n  testMatch: '**/*.spec.ts',\n  timeout: 30000,\n  retries: 0,\n  fullyParallel: true,\n  workers: process.env.CI ? 1 : 4,\n  use: {\n    headless: true,\n  },\n  projects: [\n    {\n      name: 'chromium',\n      use: { ...devices['Desktop Chrome'] },\n    },\n    {\n      name: 'firefox',\n      use: { ...devices['Desktop Firefox'] },\n    },\n    {\n      name: 'webkit',\n      use: { ...devices['Desktop Safari'] },\n    },\n    {\n      name: 'mobile-chromium',\n      use: { ...devices['Pixel 5'] },\n    },\n  ],\n  webServer: {\n    command: 'node test/server.js',\n    port: 3333,\n    reuseExistingServer: !process.env.CI,\n    timeout: 10000,\n  },\n});\n"
  },
  {
    "path": "scripts/build-custom.js",
    "content": "#!/usr/bin/env node\n\nconst { execSync } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\n\n/**\n * Build fpscanner with a custom encryption key and optional obfuscation.\n * This script:\n * 1. Runs Vite build with the key injected via environment variable\n * 2. Generates TypeScript declarations\n * 3. Optionally obfuscates the output\n * 4. Minifies with Terser (when obfuscating)\n * 5. Removes source maps (when obfuscating)\n */\nmodule.exports = async function build(args) {\n  // Parse arguments\n  const keyArg = args.find(a => a.startsWith('--key='));\n  const packageDirArg = args.find(a => a.startsWith('--package-dir='));\n  const skipObfuscation = args.includes('--no-obfuscate');\n  \n  if (!keyArg) {\n    throw new Error('Missing --key argument');\n  }\n  \n  const key = keyArg.split('=').slice(1).join('=');\n  const packageDir = packageDirArg \n    ? packageDirArg.split('=')[1] \n    : path.dirname(__dirname);\n  \n  const distDir = path.join(packageDir, 'dist');\n  const files = ['fpScanner.es.js', 'fpScanner.cjs.js'];\n  const sentinel = '__DEFAULT_FPSCANNER_KEY__';\n  \n  console.log('');\n  console.log('🔨 Building fpscanner with custom key...');\n  console.log(`   Package: ${packageDir}`);\n  console.log(`   Output:  ${distDir}`);\n  console.log(`   Obfuscation: ${skipObfuscation ? 'disabled' : 'enabled'}`);\n  console.log('');\n  \n  // Step 0: Backup/Restore mechanism to ensure idempotent builds\n  // This allows running the build multiple times without re-obfuscating already obfuscated code\n  console.log('🔄 Step 0/6: Ensuring clean build state...');\n  let restoredFromBackup = false;\n  \n  for (const file of files) {\n    const filePath = path.join(distDir, file);\n    const backupPath = filePath + '.original';\n    \n    if (!fs.existsSync(filePath)) {\n      continue;\n    }\n    \n    if (fs.existsSync(backupPath)) {\n      // Backup exists - restore from it to ensure clean state\n      fs.copyFileSync(backupPath, filePath);\n      restoredFromBackup = true;\n    } else {\n      // First run - create backup of pristine files\n      fs.copyFileSync(filePath, backupPath);\n      console.log(`   📦 Created backup: ${file}.original`);\n    }\n  }\n  \n  if (restoredFromBackup) {\n    console.log('   ✓ Restored files from backups (clean state for build)');\n  }\n  console.log('');\n  \n  // Check if we can build from source (more reliable than string replacement)\n  const viteConfigPath = path.join(packageDir, 'vite.config.ts');\n  const canBuildFromSource = fs.existsSync(viteConfigPath);\n  \n  if (canBuildFromSource) {\n    // Preferred method: Build from source with key injected via environment variable\n    // This is more reliable as Vite's define properly replaces the key during the build\n    console.log('📦 Step 1/6: Building from source with injected key...');\n    console.log('   (This is more reliable than post-build string replacement)');\n    try {\n      execSync('npm run build:vite', {\n        cwd: packageDir,\n        stdio: 'inherit',\n        env: {\n          ...process.env,\n          FP_ENCRYPTION_KEY: key,\n        },\n      });\n      console.log('');\n      console.log('📝 Generating TypeScript declarations...');\n      execSync('npx tsc --emitDeclarationOnly', {\n        cwd: packageDir,\n        stdio: 'inherit',\n      });\n      console.log('');\n      console.log('   ✓ Key injected during build');\n    } catch (err) {\n      throw new Error('Build from source failed. Make sure vite is installed (npm install)');\n    }\n  } else {\n    // Fallback method: String replacement in pre-built dist files\n    // Used when vite.config.ts is not available (npm consumers without dev dependencies)\n    console.log('📦 Step 1/6: Injecting encryption key via string replacement...');\n    console.log('   (Fallback method - vite.config.ts not found)');\n    \n    let keyInjected = false;\n    for (const file of files) {\n      const filePath = path.join(distDir, file);\n      \n      if (!fs.existsSync(filePath)) {\n        console.log(`   ⚠️  ${file} not found, skipping`);\n        continue;\n      }\n      \n      let code = fs.readFileSync(filePath, 'utf8');\n      \n      // Check if sentinel exists\n      if (!code.includes(sentinel)) {\n        console.log(`   ⚠️  ${file} does not contain the default key sentinel`);\n        console.log(`       Key may have already been replaced, or dist needs to be rebuilt`);\n        continue;\n      }\n      \n      // Replace all occurrences of the sentinel with the actual key\n      const escapedSentinel = sentinel.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n      const newCode = code.replace(new RegExp(`\"${escapedSentinel}\"`, 'g'), JSON.stringify(key));\n      \n      // Verify the replacement worked\n      if (newCode === code) {\n        console.log(`   ⚠️  ${file} - replacement had no effect`);\n        continue;\n      }\n      \n      if (newCode.includes(sentinel)) {\n        console.log(`   ⚠️  ${file} - sentinel still present after replacement`);\n        continue;\n      }\n      \n      fs.writeFileSync(filePath, newCode);\n      keyInjected = true;\n      console.log(`   ✓ ${file}`);\n    }\n    \n    if (!keyInjected) {\n      console.log('   ⚠️  Warning: No files were updated');\n      console.log('       The key may have already been injected, or dist files may need rebuilding');\n    }\n  }\n  \n  // Step 2: Skip TypeScript declarations (already generated)\n  console.log('');\n  console.log('⏭️  Step 2/6: TypeScript declarations already present, skipping...');\n  \n  // Step 3: Obfuscate (optional)\n  if (!skipObfuscation) {\n    console.log('');\n    console.log('🔒 Step 3/6: Obfuscating output...');\n    \n    let JavaScriptObfuscator;\n    try {\n      JavaScriptObfuscator = require('javascript-obfuscator');\n    } catch (err) {\n      console.log('   ⚠️  javascript-obfuscator not installed, skipping obfuscation');\n      console.log('   To enable obfuscation, run: npm install --save-dev javascript-obfuscator');\n      skipObfuscation = true;\n    }\n    \n    if (JavaScriptObfuscator) {\n      const files = ['fpScanner.es.js', 'fpScanner.cjs.js'];\n      \n      for (const file of files) {\n        const filePath = path.join(distDir, file);\n        \n        if (!fs.existsSync(filePath)) {\n          console.log(`   ⚠️  ${file} not found, skipping`);\n          continue;\n        }\n        \n        const code = fs.readFileSync(filePath, 'utf8');\n        \n        const obfuscated = JavaScriptObfuscator.obfuscate(code, {\n          compact: true,\n          controlFlowFlattening: true,\n          controlFlowFlatteningThreshold: 0.4,\n          deadCodeInjection: true,\n          deadCodeInjectionThreshold: 0.1,\n          stringArray: true,\n          stringArrayThreshold: 0.95,\n          stringArrayEncoding: ['rc4'],\n          transformObjectKeys: true,\n          unicodeEscapeSequence: false,\n          // Preserve functionality\n          selfDefending: false,\n          disableConsoleOutput: false,\n        });\n        \n        fs.writeFileSync(filePath, obfuscated.getObfuscatedCode());\n        console.log(`   ✓ ${file}`);\n      }\n      \n      // Step 4: Minify with Terser\n      console.log('');\n      console.log('📦 Step 4/6: Minifying with Terser...');\n      \n      let terser;\n      try {\n        terser = require('terser');\n      } catch (err) {\n        console.log('   ⚠️  terser not installed, skipping minification');\n        console.log('   To enable minification, run: npm install --save-dev terser');\n      }\n      \n      if (terser) {\n        for (const file of files) {\n          const filePath = path.join(distDir, file);\n          \n          if (!fs.existsSync(filePath)) {\n            continue;\n          }\n          \n          const code = fs.readFileSync(filePath, 'utf8');\n          const minified = await terser.minify(code, {\n            compress: {\n              drop_console: false,\n              dead_code: true,\n              unused: true,\n            },\n            mangle: {\n              toplevel: true,\n            },\n            format: {\n              comments: false,\n            },\n          });\n          \n          if (minified.error) {\n            console.log(`   ⚠️  Failed to minify ${file}: ${minified.error}`);\n          } else {\n            fs.writeFileSync(filePath, minified.code);\n            console.log(`   ✓ ${file}`);\n          }\n        }\n      }\n      \n      // Step 5: Delete all source map files so DevTools can't show original source\n      console.log('');\n      console.log('🗑️  Step 5/6: Removing source maps...');\n      \n      function deleteMapFiles(dir, prefix = '') {\n        if (!fs.existsSync(dir)) {\n          return;\n        }\n        const files = fs.readdirSync(dir);\n        for (const file of files) {\n          const fullPath = path.join(dir, file);\n          const stat = fs.statSync(fullPath);\n          \n          if (stat.isDirectory()) {\n            deleteMapFiles(fullPath, prefix + file + '/');\n          } else if (file.endsWith('.map')) {\n            fs.unlinkSync(fullPath);\n            console.log(`   ✓ Deleted ${prefix}${file}`);\n          }\n        }\n      }\n      \n      deleteMapFiles(distDir);\n    }\n  } else {\n    console.log('');\n    console.log('⏭️  Steps 3-5/6: Skipping obfuscation, minification, and source map removal (--no-obfuscate)');\n  }\n  \n  // Step 6: Note about backups\n  console.log('');\n  console.log('💡 Note: Original files backed up as *.original for future rebuilds');\n  \n  console.log('');\n  console.log('✅ Build complete!');\n  console.log('');\n  console.log('   Your custom fpscanner build is ready in:');\n  console.log(`   ${distDir}`);\n  console.log('');\n  console.log('   Import it in your code:');\n  console.log(\"   import FingerprintScanner from 'fpscanner';\");\n  console.log('');\n};\n\n// Allow running directly\nif (require.main === module) {\n  const args = process.argv.slice(2);\n  module.exports(args)\n    .catch((err) => {\n      console.error('❌ Build failed:', err.message);\n      process.exit(1);\n    });\n}\n"
  },
  {
    "path": "scripts/safe-publish.js",
    "content": "#!/usr/bin/env node\n\nconst { execSync } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\n\nconst publishType = process.argv[2]; // 'beta' or 'stable'\n\nif (!publishType || !['beta', 'stable'].includes(publishType)) {\n    console.error('❌ Usage: npm run publish:beta or npm run publish:stable');\n    process.exit(1);\n}\n\nconst ROOT_DIR = path.resolve(__dirname, '..');\nconst DIST_DIR = path.join(ROOT_DIR, 'dist');\n\nfunction run(command, description) {\n    console.log(`\\n🔄 ${description}...`);\n    try {\n        execSync(command, { cwd: ROOT_DIR, stdio: 'inherit' });\n        console.log(`✅ ${description} - Done`);\n    } catch (error) {\n        console.error(`❌ ${description} - Failed`);\n        process.exit(1);\n    }\n}\n\nfunction checkGitStatus() {\n    console.log('\\n🔍 Checking git status...');\n    try {\n        const status = execSync('git status --porcelain', { cwd: ROOT_DIR, encoding: 'utf8' });\n        if (status.trim()) {\n            console.error('❌ Git working directory is not clean. Please commit or stash changes first.');\n            console.error('\\nUncommitted changes:');\n            console.error(status);\n            process.exit(1);\n        }\n        console.log('✅ Git working directory is clean');\n    } catch (error) {\n        console.error('❌ Failed to check git status');\n        process.exit(1);\n    }\n}\n\nfunction getPackageVersion() {\n    const packageJson = require(path.join(ROOT_DIR, 'package.json'));\n    return packageJson.version;\n}\n\nconsole.log('═══════════════════════════════════════════════════════════');\nconsole.log(`🚀 Safe Publish Script - ${publishType.toUpperCase()}`);\nconsole.log('═══════════════════════════════════════════════════════════');\n\n// Step 0: Check git status\ncheckGitStatus();\n\n// Step 1: Clean dist directory\nconsole.log('\\n🧹 Cleaning dist directory...');\nif (fs.existsSync(DIST_DIR)) {\n    fs.rmSync(DIST_DIR, { recursive: true, force: true });\n    console.log('✅ Dist directory cleaned');\n} else {\n    console.log('✅ Dist directory already clean');\n}\n\n// Step 2: Build package with sentinel key (no key injection)\nrun('npm run build', 'Building package with sentinel key');\n\n// Step 3: Verify build\nrun('node scripts/verify-publish.js', 'Verifying build integrity');\n\n// Step 4: Get version and confirm\nconst version = getPackageVersion();\nconsole.log(`\\n📦 Package version: ${version}`);\n\nif (publishType === 'beta' && !version.includes('beta')) {\n    console.error(`❌ Version ${version} is not a beta version. Use publish:stable instead.`);\n    process.exit(1);\n}\n\nif (publishType === 'stable' && version.includes('beta')) {\n    console.error(`❌ Version ${version} is a beta version. Use publish:beta instead.`);\n    process.exit(1);\n}\n\n// Step 5: Publish\nconst publishTag = publishType === 'beta' ? '--tag beta' : '';\nrun(`npm publish ${publishTag}`, `Publishing to npm with ${publishType} tag`);\n\n// Step 6: Create git tag\nconst gitTag = `v${version}`;\nconsole.log(`\\n🏷️  Creating git tag: ${gitTag}...`);\ntry {\n    execSync(`git tag ${gitTag}`, { cwd: ROOT_DIR, stdio: 'inherit' });\n    console.log(`✅ Git tag created: ${gitTag}`);\n    \n    console.log('\\n💡 Don\\'t forget to push the tag:');\n    console.log(`   git push origin ${gitTag}`);\n} catch (error) {\n    console.log(`⚠️  Git tag might already exist: ${gitTag}`);\n}\n\nconsole.log('\\n═══════════════════════════════════════════════════════════');\nconsole.log('✅ PUBLISH SUCCESSFUL!');\nconsole.log('═══════════════════════════════════════════════════════════');\nconsole.log(`\\n📦 Published: fpscanner@${version}`);\nconsole.log(`🏷️  Tag: ${publishType}`);\nconsole.log('\\n📝 Next steps:');\nconsole.log(`   1. Push git tag: git push origin ${gitTag}`);\nconsole.log(`   2. Test installation: npm install fpscanner@${publishType}`);\nconsole.log('═══════════════════════════════════════════════════════════\\n');\n"
  },
  {
    "path": "scripts/verify-publish.js",
    "content": "#!/usr/bin/env node\n\n/**\n * Pre-publish verification script\n * Ensures the dist files contain the sentinel key for npm consumers\n */\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst distDir = path.join(__dirname, '..', 'dist');\nconst files = ['fpScanner.es.js', 'fpScanner.cjs.js'];\nconst sentinel = '__DEFAULT_FPSCANNER_KEY__';\n\nconsole.log('🔍 Verifying dist files before publish...\\n');\n\nlet allPassed = true;\n\nfor (const file of files) {\n  const filePath = path.join(distDir, file);\n  \n  if (!fs.existsSync(filePath)) {\n    console.log(`❌ ${file}: File not found`);\n    allPassed = false;\n    continue;\n  }\n  \n  const content = fs.readFileSync(filePath, 'utf8');\n  const matches = content.match(new RegExp(`\"${sentinel}\"`, 'g'));\n  \n  if (!matches || matches.length === 0) {\n    console.log(`❌ ${file}: Sentinel key \"${sentinel}\" NOT found`);\n    console.log(`   This file cannot be published - consumers won't be able to inject their keys!`);\n    allPassed = false;\n  } else {\n    console.log(`✅ ${file}: Sentinel key found (${matches.length} occurrence${matches.length > 1 ? 's' : ''})`);\n  }\n}\n\nconsole.log('');\n\nif (allPassed) {\n  console.log('✅ All checks passed! Safe to publish.');\n  process.exit(0);\n} else {\n  console.log('❌ Verification failed!');\n  console.log('');\n  console.log('To fix:');\n  console.log('  1. Make sure FP_ENCRYPTION_KEY is NOT set in your environment');\n  console.log('  2. Run: rm -rf dist && npm run build');\n  console.log('  3. Run this script again to verify');\n  console.log('');\n  process.exit(1);\n}\n"
  },
  {
    "path": "src/crypto-helpers.ts",
    "content": "/**\n * Simple and fast XOR-based encryption/decryption\n * Note: This is NOT cryptographically secure - use only for obfuscation\n */\n\n/**\n * Encrypts a string using XOR cipher with the provided key\n * @param plaintext - The string to encrypt\n * @param key - The encryption key as a string\n * @returns Encrypted string (base64 encoded)\n */\nexport async function encryptString(plaintext: string, key: string): Promise<string> {\n    const keyBytes = new TextEncoder().encode(key);\n    const textBytes = new TextEncoder().encode(plaintext);\n    const encrypted = new Uint8Array(textBytes.length);\n\n    for (let i = 0; i < textBytes.length; i++) {\n        encrypted[i] = textBytes[i] ^ keyBytes[i % keyBytes.length];\n    }\n\n    // Convert to base64 for safe string representation\n    const binaryString = String.fromCharCode(...encrypted);\n    return btoa(binaryString);\n}\n\n/**\n * Decrypts a string that was encrypted with encryptString\n * @param ciphertext - The encrypted string (base64 encoded)\n * @param key - The decryption key as a string (must match encryption key)\n * @returns Decrypted string\n */\nexport async function decryptString(ciphertext: string, key: string): Promise<string> {\n    // Decode from base64\n    const binaryString = atob(ciphertext);\n    const encrypted = new Uint8Array(binaryString.length);\n    for (let i = 0; i < binaryString.length; i++) {\n        encrypted[i] = binaryString.charCodeAt(i);\n    }\n\n    const keyBytes = new TextEncoder().encode(key);\n    const decrypted = new Uint8Array(encrypted.length);\n\n    // XOR is symmetric, so decryption is the same as encryption\n    for (let i = 0; i < encrypted.length; i++) {\n        decrypted[i] = encrypted[i] ^ keyBytes[i % keyBytes.length];\n    }\n\n    return new TextDecoder().decode(decrypted);\n}\n\n"
  },
  {
    "path": "src/detections/hasBotUserAgent.ts",
    "content": "import { Fingerprint } from \"../types\";\n\nexport function hasBotUserAgent(fingerprint: Fingerprint) {\n    const userAgents = [\n        fingerprint.signals.browser.userAgent,\n        fingerprint.signals.contexts.iframe.userAgent,\n        fingerprint.signals.contexts.webWorker.userAgent,\n    ];\n\n    return userAgents.some(userAgent => /bot|headless/i.test(userAgent.toLowerCase()));\n}"
  },
  {
    "path": "src/detections/hasCDP.ts",
    "content": "import { Fingerprint } from \"../types\";\n\nexport function hasCDP(fingerprint: Fingerprint) {\n    return fingerprint.signals.automation.cdp === true;\n}"
  },
  {
    "path": "src/detections/hasContextMismatch.ts",
    "content": "import { Fingerprint } from \"../types\";\n\n// Not used as a detection rule since, more like an indicator\nexport function hasContextMismatch(fingerprint: Fingerprint, context: 'iframe' | 'worker'): boolean {\n    const s = fingerprint.signals;\n    if (context === 'iframe') {\n        return s.contexts.iframe.webdriver !== s.automation.webdriver ||\n               s.contexts.iframe.userAgent !== s.browser.userAgent ||\n               s.contexts.iframe.platform !== s.device.platform ||\n               s.contexts.iframe.memory !== s.device.memory ||\n               s.contexts.iframe.cpuCount !== s.device.cpuCount;\n    } else {\n        return s.contexts.webWorker.webdriver !== s.automation.webdriver ||\n               s.contexts.webWorker.userAgent !== s.browser.userAgent ||\n               s.contexts.webWorker.platform !== s.device.platform ||\n               s.contexts.webWorker.memory !== s.device.memory ||\n               s.contexts.webWorker.cpuCount !== s.device.cpuCount;\n    }\n}"
  },
  {
    "path": "src/detections/hasGPUMismatch.ts",
    "content": "import { Fingerprint } from \"../types\";\n\n// For the moment, we only detect GPU mismatches related to Apple OS/GPU\n\nexport function hasGPUMismatch(fingerprint: Fingerprint) {\n    const gpu = fingerprint.signals.graphics.webgpu;\n    const webGL = fingerprint.signals.graphics.webGL;\n    const userAgent = fingerprint.signals.browser.userAgent;\n\n\n    // Inconsistencies around Apple OS/GPU\n    if ((webGL.vendor.includes('Apple') || webGL.renderer.includes('Apple')) && !userAgent.includes('Mac')) {\n        return true;\n    }\n\n    if (gpu.vendor.includes('apple') && !userAgent.includes('Mac')) {\n        return true;\n    }\n\n    if (gpu.vendor.includes('apple') && !webGL.renderer.includes('Apple')) {\n        return true;\n    }\n    \n    return false;\n}"
  },
  {
    "path": "src/detections/hasHeadlessChromeScreenResolution.ts",
    "content": "import { Fingerprint } from '../types';\n\nexport function hasHeadlessChromeScreenResolution(fingerprint: Fingerprint) {\n    const screen = fingerprint.signals.device.screenResolution;\n\n    return (screen.width === 800 && screen.height === 600) || (screen.availableWidth === 800 && screen.availableHeight === 600) || (screen.innerWidth === 800 && screen.innerHeight === 600);\n}"
  },
  {
    "path": "src/detections/hasHighCPUCount.ts",
    "content": "import { Fingerprint } from \"../types\";\n\nexport function hasHighCPUCount(fingerprint: Fingerprint) {\n    if (typeof fingerprint.signals.device.cpuCount !== 'number') {\n        return false;\n    }\n\n    return fingerprint.signals.device.cpuCount > 70;\n}"
  },
  {
    "path": "src/detections/hasImpossibleDeviceMemory.ts",
    "content": "import { Fingerprint } from \"../types\";\n\nexport function hasImpossibleDeviceMemory(fingerprint: Fingerprint) {\n    if (typeof fingerprint.signals.device.memory !== 'number') {\n        return false;\n    }\n\n    return (fingerprint.signals.device.memory > 32 || fingerprint.signals.device.memory < 0.25);\n}"
  },
  {
    "path": "src/detections/hasInconsistentEtsl.ts",
    "content": "import { Fingerprint } from \"../types\";\n\nexport function hasInconsistentEtsl(fingerprint: Fingerprint) {\n\n    // On Chromium-based browsers, ETSL should be 33\n    if (fingerprint.signals.browser.features.chrome && fingerprint.signals.browser.etsl !== 33) {\n        return true;\n    }\n\n    // On Safari, ETSL should be 37\n    if (fingerprint.signals.browser.features.safari && fingerprint.signals.browser.etsl !== 37) {\n        return true;\n    }\n\n    // On Firefox, ETSL should be 37\n    if (fingerprint.signals.browser.userAgent.includes('Firefox') && fingerprint.signals.browser.etsl !== 37) {\n        return true;\n    }\n\n    return false;\n}"
  },
  {
    "path": "src/detections/hasMismatchLanguages.ts",
    "content": "import { Fingerprint } from \"../types\";\n\nexport function hasMismatchLanguages(fingerprint: Fingerprint) {\n    const languages = fingerprint.signals.locale.languages.languages;\n    const language = fingerprint.signals.locale.languages.language;\n\n\n    if (language && languages && Array.isArray(languages) && languages.length > 0) {\n        return languages[0] !== language;\n    }\n\n    return false;\n}"
  },
  {
    "path": "src/detections/hasMismatchPlatformIframe.ts",
    "content": "import { Fingerprint } from \"../types\";\nimport { ERROR, NA } from \"../signals/utils\";\n\nexport function hasMismatchPlatformIframe(fingerprint: Fingerprint) {\n    if (fingerprint.signals.contexts.iframe.platform === NA || fingerprint.signals.contexts.iframe.platform === ERROR) {\n        return false;\n    }\n\n    return fingerprint.signals.device.platform !== fingerprint.signals.contexts.iframe.platform;\n}\n"
  },
  {
    "path": "src/detections/hasMismatchPlatformWorker.ts",
    "content": "import { Fingerprint } from \"../types\";\nimport { ERROR, NA, SKIPPED } from \"../signals/utils\";\n\nexport function hasMismatchPlatformWorker(fingerprint: Fingerprint) {\n    if (fingerprint.signals.contexts.webWorker.platform === NA || fingerprint.signals.contexts.webWorker.platform === ERROR || fingerprint.signals.contexts.webWorker.platform === SKIPPED) {\n        return false;\n    }\n\n    return fingerprint.signals.device.platform !== fingerprint.signals.contexts.webWorker.platform;\n}\n"
  },
  {
    "path": "src/detections/hasMismatchWebGLInWorker.ts",
    "content": "import { Fingerprint } from \"../types\";\nimport { ERROR, NA, SKIPPED } from \"../signals/utils\";\n\nexport function hasMismatchWebGLInWorker(fingerprint: Fingerprint) {\n    const worker = fingerprint.signals.contexts.webWorker;\n    const webGL = fingerprint.signals.graphics.webGL;\n    \n    if (worker.vendor === ERROR || worker.renderer === ERROR || webGL.vendor === NA || webGL.renderer === NA || worker.vendor === SKIPPED) {\n        return false;\n    }\n\n    return worker.vendor !== webGL.vendor || worker.renderer !== webGL.renderer;\n}\n"
  },
  {
    "path": "src/detections/hasMissingChromeObject.ts",
    "content": "import { Fingerprint } from \"../types\";\n\nexport function hasMissingChromeObject(fingerprint: Fingerprint) {\n    const userAgent = fingerprint.signals.browser.userAgent;\n    return fingerprint.signals.browser.features.chrome === false && typeof userAgent === 'string' && userAgent.includes('Chrome');\n}"
  },
  {
    "path": "src/detections/hasPlatformMismatch.ts",
    "content": "import { Fingerprint } from \"../types\";\nimport { ERROR, NA } from \"../signals/utils\";\n\nexport function hasPlatformMismatch(fingerprint: Fingerprint) {\n    const platform = fingerprint.signals.device.platform;\n    const userAgent = fingerprint.signals.browser.userAgent;\n    const highEntropyPlatform = fingerprint.signals.browser.highEntropyValues.platform;\n    \n    if (userAgent.includes('Mac') && (platform.includes('Win') || platform.includes('Linux'))) {\n        return true;\n    }\n\n    if (userAgent.includes('Windows') &&  (platform.includes('Mac') || platform.includes('Linux'))) {\n        return true;\n    }\n    \n    if (userAgent.includes('Linux') && (platform.includes('Mac') || platform.includes('Win'))) {\n        return true;\n    }\n\n\n    // Check applied only if highEntropyPlatform is not ERROR or NA\n    if (highEntropyPlatform !== ERROR && highEntropyPlatform !== NA) {\n        if (highEntropyPlatform.includes('Mac') && (platform.includes('Win') || platform.includes('Linux'))) {\n            return true;\n        }\n        \n        if (highEntropyPlatform.includes('Windows') && (platform.includes('Mac') || platform.includes('Linux'))) {\n            return true;\n        }\n\n        if (highEntropyPlatform.includes('Linux') && (platform.includes('Mac') || platform.includes('Win'))) {\n            return true;\n        }\n    }\n\n    return false;\n}"
  },
  {
    "path": "src/detections/hasPlaywright.ts",
    "content": "import { Fingerprint } from \"../types\";\n\nexport function hasPlaywright(fingerprint: Fingerprint) {\n    return fingerprint.signals.automation.playwright === true;\n}"
  },
  {
    "path": "src/detections/hasSeleniumProperty.ts",
    "content": "import { Fingerprint } from \"../types\";\n\nexport function hasSeleniumProperty(fingerprint: Fingerprint) {\n    return !!fingerprint.signals.automation.selenium;\n}\n"
  },
  {
    "path": "src/detections/hasSwiftshaderRenderer.ts",
    "content": "import { Fingerprint } from \"../types\";\n\nexport function hasSwiftshaderRenderer(fingerprint: Fingerprint) {\n    return fingerprint.signals.graphics.webGL.renderer.includes('SwiftShader');\n}\n"
  },
  {
    "path": "src/detections/hasUTCTimezone.ts",
    "content": "import { Fingerprint } from \"../types\";\n\nexport function hasUTCTimezone(fingerprint: Fingerprint) {\n    return fingerprint.signals.locale.internationalization.timezone === 'UTC';\n}"
  },
  {
    "path": "src/detections/hasWebdriver.ts",
    "content": "import { Fingerprint } from \"../types\";\n\nexport function hasWebdriver(fingerprint: Fingerprint) {\n    return fingerprint.signals.automation.webdriver === true;\n}"
  },
  {
    "path": "src/detections/hasWebdriverIframe.ts",
    "content": "import { Fingerprint } from \"../types\";\n\nexport function hasWebdriverIframe(fingerprint: Fingerprint) {\n    return fingerprint.signals.contexts.iframe.webdriver === true;\n}\n"
  },
  {
    "path": "src/detections/hasWebdriverWorker.ts",
    "content": "import { Fingerprint } from \"../types\";\n\nexport function hasWebdriverWorker(fingerprint: Fingerprint) {\n    return fingerprint.signals.contexts.webWorker.webdriver === true;\n}\n"
  },
  {
    "path": "src/detections/hasWebdriverWritable.ts",
    "content": "import { Fingerprint } from \"../types\";\n\nexport function hasWebdriverWritable(fingerprint: Fingerprint) {\n    return fingerprint.signals.automation.webdriverWritable === true;\n}\n"
  },
  {
    "path": "src/globals.d.ts",
    "content": "/**\n * Build-time constant injected via Vite's define option.\n * This is replaced with the actual encryption key during the build process.\n * \n * Customers provide their key via:\n *   - npx fpscanner build --key=their-key\n *   - FINGERPRINT_KEY environment variable\n *   - .env file with FINGERPRINT_KEY=their-key\n */\ndeclare const __FP_ENCRYPTION_KEY__: string;\n"
  },
  {
    "path": "src/index.ts",
    "content": "// Import all signals\nimport { webdriver } from './signals/webdriver';\nimport { userAgent } from './signals/userAgent';\nimport { platform } from './signals/platform';\nimport { cdp } from './signals/cdp';\nimport { webGL } from './signals/webGL';\nimport { playwright } from './signals/playwright';\nimport { cpuCount } from './signals/cpuCount';\nimport { maths } from './signals/maths';\nimport { memory } from './signals/memory';\nimport { etsl } from './signals/etsl';\nimport { internationalization } from './signals/internationalization';\nimport { screenResolution } from './signals/screenResolution';\nimport { languages } from './signals/languages';\nimport { webgpu } from './signals/webgpu';\nimport { hasSeleniumProperties } from './signals/seleniumProperties';\nimport { webdriverWritable } from './signals/webdriverWritable';\nimport { highEntropyValues } from './signals/highEntropyValues';\nimport { plugins } from './signals/plugins';\nimport { multimediaDevices } from './signals/multimediaDevices';\nimport { iframe } from './signals/iframe';\nimport { worker } from './signals/worker';\nimport { toSourceError } from './signals/toSourceError';\nimport { mediaCodecs } from './signals/mediaCodecs';\nimport { canvas } from './signals/canvas';\nimport { navigatorPropertyDescriptors } from './signals/navigatorPropertyDescriptors';\nimport { nonce } from './signals/nonce';\nimport { time } from './signals/time';\nimport { pageURL } from './signals/url';\nimport { hasContextMismatch } from './detections/hasContextMismatch';\nimport { browserExtensions } from './signals/browserExtensions';\nimport { browserFeatures } from './signals/browserFeatures';\nimport { mediaQueries } from './signals/mediaQueries';\n\n// Fast Bot Detection tests\nimport { hasHeadlessChromeScreenResolution } from './detections/hasHeadlessChromeScreenResolution';\nimport { hasWebdriver } from './detections/hasWebdriver';\nimport { hasSeleniumProperty } from './detections/hasSeleniumProperty';\nimport { hasCDP } from './detections/hasCDP';\nimport { hasPlaywright } from './detections/hasPlaywright';\nimport { hasImpossibleDeviceMemory } from './detections/hasImpossibleDeviceMemory';\nimport { hasHighCPUCount } from './detections/hasHighCPUCount';\nimport { hasMissingChromeObject } from './detections/hasMissingChromeObject';\nimport { hasWebdriverIframe } from './detections/hasWebdriverIframe';\nimport { hasWebdriverWorker } from './detections/hasWebdriverWorker';\nimport { hasMismatchWebGLInWorker } from './detections/hasMismatchWebGLInWorker';\nimport { hasMismatchPlatformWorker } from './detections/hasMismatchPlatformWorker';\nimport { hasMismatchPlatformIframe } from './detections/hasMismatchPlatformIframe';\nimport { hasWebdriverWritable } from './detections/hasWebdriverWritable';\nimport { hasSwiftshaderRenderer } from './detections/hasSwiftshaderRenderer';\nimport { hasUTCTimezone } from './detections/hasUTCTimezone';\nimport { hasMismatchLanguages } from './detections/hasMismatchLanguages';\nimport { hasInconsistentEtsl } from './detections/hasInconsistentEtsl';\nimport { hasBotUserAgent } from './detections/hasBotUserAgent';\nimport { hasGPUMismatch } from './detections/hasGPUMismatch';\nimport { hasPlatformMismatch } from './detections/hasPlatformMismatch';\n\nimport { ERROR, HIGH, INIT, LOW, MEDIUM, SKIPPED, hashCode } from './signals/utils';\nimport { encryptString } from './crypto-helpers';\nimport { Fingerprint, FastBotDetectionDetails, DetectionRule, CollectFingerprintOptions } from './types';\n\n\nclass FingerprintScanner {\n    private fingerprint: Fingerprint;\n\n    constructor() {\n        this.fingerprint = {\n            signals: {\n                // Automation/Bot detection signals\n                automation: {\n                    webdriver: INIT,\n                    webdriverWritable: INIT,\n                    selenium: INIT,\n                    cdp: INIT,\n                    playwright: INIT,\n                    navigatorPropertyDescriptors: INIT,\n                },\n                // Device hardware characteristics\n                device: {\n                    cpuCount: INIT,\n                    memory: INIT,\n                    platform: INIT,\n                    screenResolution: {\n                        width: INIT,\n                        height: INIT,\n                        pixelDepth: INIT,\n                        colorDepth: INIT,\n                        availableWidth: INIT,\n                        availableHeight: INIT,\n                        innerWidth: INIT,\n                        innerHeight: INIT,\n                        hasMultipleDisplays: INIT,\n                    },\n                    multimediaDevices: {\n                        speakers: INIT,\n                        microphones: INIT,\n                        webcams: INIT,\n                    },\n                    mediaQueries: {\n                        prefersColorScheme: INIT,\n                        prefersReducedMotion: INIT,\n                        prefersReducedTransparency: INIT,\n                        colorGamut: INIT,\n                        pointer: INIT,\n                        anyPointer: INIT,\n                        hover: INIT,\n                        anyHover: INIT,\n                        colorDepth: INIT,\n                    },\n                },\n                // Browser identity & features\n                browser: {\n                    userAgent: INIT,\n                    features: {\n                        bitmask: INIT,\n                        chrome: INIT,\n                        brave: INIT,\n                        applePaySupport: INIT,\n                        opera: INIT,\n                        serial: INIT,\n                        attachShadow: INIT,\n                        caches: INIT,\n                        webAssembly: INIT,\n                        buffer: INIT,\n                        showModalDialog: INIT,\n                        safari: INIT,\n                        webkitPrefixedFunction: INIT,\n                        mozPrefixedFunction: INIT,\n                        usb: INIT,\n                        browserCapture: INIT,\n                        paymentRequestUpdateEvent: INIT,\n                        pressureObserver: INIT,\n                        audioSession: INIT,\n                        selectAudioOutput: INIT,\n                        barcodeDetector: INIT,\n                        battery: INIT,\n                        devicePosture: INIT,\n                        documentPictureInPicture: INIT,\n                        eyeDropper: INIT,\n                        editContext: INIT,\n                        fencedFrame: INIT,\n                        sanitizer: INIT,\n                        otpCredential: INIT,\n                    },\n                    plugins: {\n                        isValidPluginArray: INIT,\n                        pluginCount: INIT,\n                        pluginNamesHash: INIT,\n                        pluginConsistency1: INIT,\n                        pluginOverflow: INIT,\n                    },\n                    extensions: {\n                        bitmask: INIT,\n                        extensions: INIT,\n                    },\n                    highEntropyValues: {\n                        architecture: INIT,\n                        bitness: INIT,\n                        brands: INIT,\n                        mobile: INIT,\n                        model: INIT,\n                        platform: INIT,\n                        platformVersion: INIT,\n                        uaFullVersion: INIT,\n                    },\n                    etsl: INIT,\n                    maths: INIT,\n                    toSourceError: {\n                        toSourceError: INIT,\n                        hasToSource: INIT,\n                    },\n                },\n                // Graphics & rendering\n                graphics: {\n                    webGL: {\n                        vendor: INIT,\n                        renderer: INIT,\n                    },\n                    webgpu: {\n                        vendor: INIT,\n                        architecture: INIT,\n                        device: INIT,\n                        description: INIT,\n                    },\n                    canvas: {\n                        hasModifiedCanvas: INIT,\n                        canvasFingerprint: INIT,\n                    },\n                },\n                // Media codecs (at root level)\n                codecs: {\n                    audioCanPlayTypeHash: INIT,\n                    videoCanPlayTypeHash: INIT,\n                    audioMediaSourceHash: INIT,\n                    videoMediaSourceHash: INIT,\n                    rtcAudioCapabilitiesHash: INIT,\n                    rtcVideoCapabilitiesHash: INIT,\n                    hasMediaSource: INIT,\n                },\n                // Locale & internationalization\n                locale: {\n                    internationalization: {\n                        timezone: INIT,\n                        localeLanguage: INIT,\n                    },\n                    languages: {\n                        languages: INIT,\n                        language: INIT,\n                    },\n                },\n                // Isolated execution contexts\n                contexts: {\n                    iframe: {\n                        webdriver: INIT,\n                        userAgent: INIT,\n                        platform: INIT,\n                        memory: INIT,\n                        cpuCount: INIT,\n                        language: INIT,\n                    },\n                    webWorker: {\n                        webdriver: INIT,\n                        userAgent: INIT,\n                        platform: INIT,\n                        memory: INIT,\n                        cpuCount: INIT,\n                        language: INIT,\n                        vendor: INIT,\n                        renderer: INIT,\n                    },\n                },\n            },\n            fsid: INIT,\n            nonce: INIT,\n            time: INIT,\n            url: INIT,\n            fastBotDetection: false,\n            fastBotDetectionDetails: {\n                headlessChromeScreenResolution: { detected: false, severity: 'high' },\n                hasWebdriver: { detected: false, severity: 'high' },\n                hasWebdriverWritable: { detected: false, severity: 'high' },\n                hasSeleniumProperty: { detected: false, severity: 'high' },\n                hasCDP: { detected: false, severity: 'high' },\n                hasPlaywright: { detected: false, severity: 'high' },\n                hasImpossibleDeviceMemory: { detected: false, severity: 'high' },\n                hasHighCPUCount: { detected: false, severity: 'high' },\n                hasMissingChromeObject: { detected: false, severity: 'high' },\n                hasWebdriverIframe: { detected: false, severity: 'high' },\n                hasWebdriverWorker: { detected: false, severity: 'high' },\n                hasMismatchWebGLInWorker: { detected: false, severity: 'high' },\n                hasMismatchPlatformIframe: { detected: false, severity: 'high' },\n                hasMismatchPlatformWorker: { detected: false, severity: 'high' },\n                hasSwiftshaderRenderer: { detected: false, severity: 'low' },\n                hasUTCTimezone: { detected: false, severity: 'medium' },\n                hasMismatchLanguages: { detected: false, severity: 'low' },\n                hasInconsistentEtsl: { detected: false, severity: 'high' },\n                hasBotUserAgent: { detected: false, severity: 'high' },\n                hasGPUMismatch: { detected: false, severity: 'high' },\n                hasPlatformMismatch: { detected: false, severity: 'high' },\n            },\n        };\n    }\n\n    private async collectSignal(signal: () => any) {\n        try {\n            return await signal();\n        } catch (e) {\n            return ERROR;\n        }\n    }\n\n    /**\n     * Generate a JA4-inspired fingerprint scanner ID\n     * Format: FS1_<det>_<auto>_<dev>_<brw>_<gfx>_<cod>_<loc>_<ctx>\n     * \n     * Each section is delimited by '_', allowing partial matching.\n     * Sections use the pattern: <bitmask>h<hash> where applicable.\n     * Bitmasks are extensible - new boolean fields are appended without breaking existing positions.\n     * \n     * Sections:\n     * - det:  fastBotDetectionDetails bitmask (21 bits: headlessChromeScreenResolution, hasWebdriver, \n     *         hasWebdriverWritable, hasSeleniumProperty, hasCDP, hasPlaywright, hasImpossibleDeviceMemory,\n     *         hasHighCPUCount, hasMissingChromeObject, hasWebdriverIframe, hasWebdriverWorker,\n     *         hasMismatchWebGLInWorker, hasMismatchPlatformIframe, hasMismatchPlatformWorker,\n     *         hasMismatchLanguages, hasInconsistentEtsl, hasBotUserAgent, hasGPUMismatch, hasPlatformMismatch)\n     * - auto: automation bitmask (5 bits: webdriver, webdriverWritable, selenium, cdp, playwright) + hash\n     * - dev:  WIDTHxHEIGHT + cpu + mem + device bitmask + hash of all device signals\n     * - brw:  features.bitmask + extensions.bitmask + plugins bitmask (3 bits) + hash of browser signals\n     * - gfx:  canvas bitmask (1 bit: hasModifiedCanvas) + hash of all graphics signals\n     * - cod:  codecs bitmask (1 bit: hasMediaSource) + hash of all codec hashes\n     * - loc:  language code (2 chars) + language count + hash of locale signals\n     * - ctx:  context mismatch bitmask (2 bits: iframe, worker) + hash of all context signals\n     */\n    private generateFingerprintScannerId(): string {\n        try {\n            const s = this.fingerprint.signals;\n            const det = this.fingerprint.fastBotDetectionDetails;\n\n            // Section 1: Version\n            const version = 'FS1';\n\n            // Section 2: Detection bitmask - all 21 fastBotDetectionDetails booleans\n            // Order matches FastBotDetectionDetails interface for consistency\n            const detBitmask = [\n                det.headlessChromeScreenResolution.detected,\n                det.hasWebdriver.detected,\n                det.hasWebdriverWritable.detected,\n                det.hasSeleniumProperty.detected,\n                det.hasCDP.detected,\n                det.hasPlaywright.detected,\n                det.hasImpossibleDeviceMemory.detected,\n                det.hasHighCPUCount.detected,\n                det.hasMissingChromeObject.detected,\n                det.hasWebdriverIframe.detected,\n                det.hasWebdriverWorker.detected,\n                det.hasMismatchWebGLInWorker.detected,\n                det.hasMismatchPlatformIframe.detected,\n                det.hasMismatchPlatformWorker.detected,\n                det.hasSwiftshaderRenderer.detected,\n                det.hasUTCTimezone.detected,\n                det.hasMismatchLanguages.detected,\n                det.hasInconsistentEtsl.detected,\n                det.hasBotUserAgent.detected,\n                det.hasGPUMismatch.detected,\n                det.hasPlatformMismatch.detected,\n                // Add other detection rules output here\n            ].map(b => b ? '1' : '0').join('');\n            const detSection = detBitmask;\n\n            // Section 3: Automation - bitmask + hash of non-boolean fields\n            const autoBitmask = [\n                s.automation.webdriver === true,\n                s.automation.webdriverWritable === true,\n                s.automation.selenium === true,\n                s.automation.cdp === true,\n                s.automation.playwright === true,\n            ].map(b => b ? '1' : '0').join('');\n            const autoHash = hashCode(String(s.automation.navigatorPropertyDescriptors)).slice(0, 4);\n            const autoSection = `${autoBitmask}h${autoHash}`;\n\n            // Section 4: Device - screen dims + cpu + mem + bitmask + hash\n            const width = typeof s.device.screenResolution.width === 'number' ? s.device.screenResolution.width : 0;\n            const height = typeof s.device.screenResolution.height === 'number' ? s.device.screenResolution.height : 0;\n            const cpu = typeof s.device.cpuCount === 'number' ? String(s.device.cpuCount).padStart(2, '0') : '00';\n            const mem = typeof s.device.memory === 'number' ? String(Math.round(s.device.memory)).padStart(2, '0') : '00';\n            const devBitmask = [\n                s.device.screenResolution.hasMultipleDisplays === true,\n                s.device.mediaQueries.prefersReducedMotion === true,\n                s.device.mediaQueries.prefersReducedTransparency === true,\n                s.device.mediaQueries.hover === true,\n                s.device.mediaQueries.anyHover === true,\n            ].map(b => b ? '1' : '0').join('');\n            const devStr = [\n                s.device.platform,\n                s.device.screenResolution.pixelDepth,\n                s.device.screenResolution.colorDepth,\n                s.device.multimediaDevices.speakers,\n                s.device.multimediaDevices.microphones,\n                s.device.multimediaDevices.webcams,\n                s.device.mediaQueries.prefersColorScheme,\n                s.device.mediaQueries.colorGamut,\n                s.device.mediaQueries.pointer,\n                s.device.mediaQueries.anyPointer,\n                s.device.mediaQueries.colorDepth,\n            ].map(v => String(v)).join('|');\n            const devHash = hashCode(devStr).slice(0, 6);\n            const devSection = `${width}x${height}c${cpu}m${mem}b${devBitmask}h${devHash}`;\n\n            // Section 5: Browser - use existing bitmasks + plugins bitmask + hash\n            const featuresBitmask = typeof s.browser.features.bitmask === 'string' ? s.browser.features.bitmask : '0000000000';\n            const extensionsBitmask = typeof s.browser.extensions.bitmask === 'string' ? s.browser.extensions.bitmask : '00000000';\n            const pluginsBitmask = [\n                s.browser.plugins.isValidPluginArray === true,\n                s.browser.plugins.pluginConsistency1 === true,\n                s.browser.plugins.pluginOverflow === true,\n                s.browser.toSourceError.hasToSource === true,\n            ].map(b => b ? '1' : '0').join('');\n            const brwStr = [\n                s.browser.userAgent,\n                s.browser.etsl,\n                s.browser.maths,\n                s.browser.plugins.pluginCount,\n                s.browser.plugins.pluginNamesHash,\n                s.browser.toSourceError.toSourceError,\n                s.browser.highEntropyValues.architecture,\n                s.browser.highEntropyValues.bitness,\n                s.browser.highEntropyValues.platform,\n                s.browser.highEntropyValues.platformVersion,\n                s.browser.highEntropyValues.uaFullVersion,\n                s.browser.highEntropyValues.mobile,\n            ].map(v => String(v)).join('|');\n            const brwHash = hashCode(brwStr).slice(0, 6);\n            const brwSection = `f${featuresBitmask}e${extensionsBitmask}p${pluginsBitmask}h${brwHash}`;\n\n            // Section 6: Graphics - bitmask + hash\n            const gfxBitmask = [\n                s.graphics.canvas.hasModifiedCanvas === true,\n            ].map(b => b ? '1' : '0').join('');\n            const gfxStr = [\n                s.graphics.webGL.vendor,\n                s.graphics.webGL.renderer,\n                s.graphics.webgpu.vendor,\n                s.graphics.webgpu.architecture,\n                s.graphics.webgpu.device,\n                s.graphics.webgpu.description,\n                s.graphics.canvas.canvasFingerprint,\n            ].map(v => String(v)).join('|');\n            const gfxHash = hashCode(gfxStr).slice(0, 6);\n            const gfxSection = `${gfxBitmask}h${gfxHash}`;\n\n            // Section 7: Codecs - bitmask + hash of all codec hashes\n            const codBitmask = [\n                s.codecs.hasMediaSource === true,\n            ].map(b => b ? '1' : '0').join('');\n            const codStr = [\n                s.codecs.audioCanPlayTypeHash,\n                s.codecs.videoCanPlayTypeHash,\n                s.codecs.audioMediaSourceHash,\n                s.codecs.videoMediaSourceHash,\n                s.codecs.rtcAudioCapabilitiesHash,\n                s.codecs.rtcVideoCapabilitiesHash,\n            ].map(v => String(v)).join('|');\n            const codHash = hashCode(codStr).slice(0, 6);\n            const codSection = `${codBitmask}h${codHash}`;\n\n            // Section 8: Locale - language code + count + timezone + hash\n            const primaryLang = typeof s.locale.languages.language === 'string'\n                ? s.locale.languages.language.slice(0, 2).toLowerCase()\n                : 'xx';\n            const langCount = Array.isArray(s.locale.languages.languages) ? s.locale.languages.languages.length : 0;\n            // Sanitize timezone: replace / and spaces with - for fingerprint compatibility\n            const rawTimezone = typeof s.locale.internationalization.timezone === 'string'\n                ? s.locale.internationalization.timezone\n                : 'unknown';\n            const sanitizedTimezone = rawTimezone.replace(/[\\/\\s]/g, '-');\n            const locStr = [\n                s.locale.internationalization.timezone,\n                s.locale.internationalization.localeLanguage,\n                Array.isArray(s.locale.languages.languages) ? s.locale.languages.languages.join(',') : s.locale.languages.languages,\n                s.locale.languages.language,\n            ].map(v => String(v)).join('|');\n            const locHash = hashCode(locStr).slice(0, 4);\n            const locSection = `${primaryLang}${langCount}t${sanitizedTimezone}_h${locHash}`;\n\n            // Section 9: Contexts - mismatch bitmask + hash of all context signals\n            const ctxBitmask = [\n                hasContextMismatch(this.fingerprint, 'iframe'),\n                hasContextMismatch(this.fingerprint, 'worker'),\n                s.contexts.iframe.webdriver === true,\n                s.contexts.webWorker.webdriver === true,\n            ].map(b => b ? '1' : '0').join('');\n            const ctxStr = [\n                s.contexts.iframe.userAgent,\n                s.contexts.iframe.platform,\n                s.contexts.iframe.memory,\n                s.contexts.iframe.cpuCount,\n                s.contexts.iframe.language,\n                s.contexts.webWorker.userAgent,\n                s.contexts.webWorker.platform,\n                s.contexts.webWorker.memory,\n                s.contexts.webWorker.cpuCount,\n                s.contexts.webWorker.language,\n                s.contexts.webWorker.vendor,\n                s.contexts.webWorker.renderer,\n            ].map(v => String(v)).join('|');\n            const ctxHash = hashCode(ctxStr).slice(0, 6);\n            const ctxSection = `${ctxBitmask}h${ctxHash}`;\n\n            return [\n                version,\n                detSection,\n                autoSection,\n                devSection,\n                brwSection,\n                gfxSection,\n                codSection,\n                locSection,\n                ctxSection,\n            ].join('_');\n        } catch (e) {\n            console.error('Error generating fingerprint scanner id', e);\n            return ERROR;\n        }\n    }\n\n    private async encryptFingerprint(fingerprint: string) {\n        // Key is injected at build time via Vite's define option\n        // Customers run: npx fpscanner build --key=their-key\n        const key = __FP_ENCRYPTION_KEY__;\n        \n        // Runtime safety check: warn if using the default sentinel key\n        // Use a dynamic check that prevents build-time optimization\n        if (key.length > 20 && key.indexOf('DEFAULT') > 0 && key.indexOf('FPSCANNER') > 0) {\n            console.warn(\n                '[fpscanner] WARNING: Using default encryption key! ' +\n                'Run \"npx fpscanner build --key=your-secret-key\" to inject your own key. ' +\n                'See: https://github.com/antoinevastel/fpscanner#advanced-custom-builds'\n            );\n        }\n        \n        const enc = await encryptString(JSON.stringify(fingerprint), key);\n\n        return enc;\n    }\n\n    /**\n     * Detection rules with name and severity.\n    */\n    private getDetectionRules(): DetectionRule[] {\n        return [\n            { name: 'headlessChromeScreenResolution', severity: HIGH, test: hasHeadlessChromeScreenResolution },\n            { name: 'hasWebdriver', severity: HIGH, test: hasWebdriver },\n            { name: 'hasWebdriverWritable', severity: HIGH, test: hasWebdriverWritable },\n            { name: 'hasSeleniumProperty', severity: HIGH, test: hasSeleniumProperty },\n            { name: 'hasCDP', severity: HIGH, test: hasCDP },\n            { name: 'hasPlaywright', severity: HIGH, test: hasPlaywright },\n            { name: 'hasImpossibleDeviceMemory', severity: HIGH, test: hasImpossibleDeviceMemory },\n            { name: 'hasHighCPUCount', severity: HIGH, test: hasHighCPUCount },\n            { name: 'hasMissingChromeObject', severity: HIGH, test: hasMissingChromeObject },\n            { name: 'hasWebdriverIframe', severity: HIGH, test: hasWebdriverIframe },\n            { name: 'hasWebdriverWorker', severity: HIGH, test: hasWebdriverWorker },\n            { name: 'hasMismatchWebGLInWorker', severity: HIGH, test: hasMismatchWebGLInWorker },\n            { name: 'hasMismatchPlatformIframe', severity: HIGH, test: hasMismatchPlatformIframe },\n            { name: 'hasMismatchPlatformWorker', severity: HIGH, test: hasMismatchPlatformWorker },\n            { name: 'hasSwiftshaderRenderer', severity: LOW, test: hasSwiftshaderRenderer },\n            { name: 'hasUTCTimezone', severity: MEDIUM, test: hasUTCTimezone },\n            { name: 'hasMismatchLanguages', severity: LOW, test: hasMismatchLanguages },\n            { name: 'hasInconsistentEtsl', severity: HIGH, test: hasInconsistentEtsl },\n            { name: 'hasBotUserAgent', severity: HIGH, test: hasBotUserAgent },\n            { name: 'hasGPUMismatch', severity: HIGH, test: hasGPUMismatch },\n            { name: 'hasPlatformMismatch', severity: HIGH, test: hasPlatformMismatch },\n        ];\n    }\n\n    private runDetectionRules(): FastBotDetectionDetails {\n        const rules = this.getDetectionRules();\n        const results: FastBotDetectionDetails = {\n            headlessChromeScreenResolution: { detected: false, severity: 'high' },\n            hasWebdriver: { detected: false, severity: 'high' },\n            hasWebdriverWritable: { detected: false, severity: 'high' },\n            hasSeleniumProperty: { detected: false, severity: 'high' },\n            hasCDP: { detected: false, severity: 'high' },\n            hasPlaywright: { detected: false, severity: 'high' },\n            hasImpossibleDeviceMemory: { detected: false, severity: 'high' },\n            hasHighCPUCount: { detected: false, severity: 'high' },\n            hasMissingChromeObject: { detected: false, severity: 'high' },\n            hasWebdriverIframe: { detected: false, severity: 'high' },\n            hasWebdriverWorker: { detected: false, severity: 'high' },\n            hasMismatchWebGLInWorker: { detected: false, severity: 'high' },\n            hasMismatchPlatformIframe: { detected: false, severity: 'high' },\n            hasMismatchPlatformWorker: { detected: false, severity: 'high' },\n            hasSwiftshaderRenderer: { detected: false, severity: 'low' },\n            hasUTCTimezone: { detected: false, severity: 'medium' },\n            hasMismatchLanguages: { detected: false, severity: 'low' },\n            hasInconsistentEtsl: { detected: false, severity: 'high' },\n            hasBotUserAgent: { detected: false, severity: 'high' },\n            hasGPUMismatch: { detected: false, severity: 'high' },\n            hasPlatformMismatch: { detected: false, severity: 'high' },\n        };\n\n        for (const rule of rules) {\n            try {\n                const detected = rule.test(this.fingerprint);\n                (results as any)[rule.name] = { detected, severity: rule.severity };\n            } catch (e) {\n                (results as any)[rule.name] = { detected: false, severity: rule.severity };\n            }\n        }\n\n        return results;\n    }\n\n    async collectFingerprint(options: CollectFingerprintOptions = { encrypt: true }) {\n        const { encrypt = true, skipWorker = false } = options;\n        const s = this.fingerprint.signals;\n\n        // Define all signal collection tasks to run in parallel\n        const signalTasks = {\n            // Automation signals\n            webdriver: this.collectSignal(webdriver),\n            webdriverWritable: this.collectSignal(webdriverWritable),\n            selenium: this.collectSignal(hasSeleniumProperties),\n            cdp: this.collectSignal(cdp),\n            playwright: this.collectSignal(playwright),\n            navigatorPropertyDescriptors: this.collectSignal(navigatorPropertyDescriptors),\n            // Device signals\n            cpuCount: this.collectSignal(cpuCount),\n            memory: this.collectSignal(memory),\n            platform: this.collectSignal(platform),\n            screenResolution: this.collectSignal(screenResolution),\n            multimediaDevices: this.collectSignal(multimediaDevices),\n            mediaQueries: this.collectSignal(mediaQueries),\n            // Browser signals\n            userAgent: this.collectSignal(userAgent),\n            browserFeatures: this.collectSignal(browserFeatures),\n            plugins: this.collectSignal(plugins),\n            browserExtensions: this.collectSignal(browserExtensions),\n            highEntropyValues: this.collectSignal(highEntropyValues),\n            etsl: this.collectSignal(etsl),\n            maths: this.collectSignal(maths),\n            toSourceError: this.collectSignal(toSourceError),\n            // Graphics signals\n            webGL: this.collectSignal(webGL),\n            webgpu: this.collectSignal(webgpu),\n            canvas: this.collectSignal(canvas),\n            // Codecs\n            mediaCodecs: this.collectSignal(mediaCodecs),\n            // Locale signals\n            internationalization: this.collectSignal(internationalization),\n            languages: this.collectSignal(languages),\n            // Context signals\n            iframe: this.collectSignal(iframe),\n            webWorker: skipWorker\n                ? Promise.resolve({\n                    webdriver: SKIPPED,\n                    userAgent: SKIPPED,\n                    platform: SKIPPED,\n                    memory: SKIPPED,\n                    cpuCount: SKIPPED,\n                    language: SKIPPED,\n                    vendor: SKIPPED,\n                    renderer: SKIPPED,\n                })\n                : this.collectSignal(worker),\n            // Meta signals\n            nonce: this.collectSignal(nonce),\n            time: this.collectSignal(time),\n            url: this.collectSignal(pageURL),\n        };\n\n        // Run all signal collections in parallel\n        const keys = Object.keys(signalTasks) as (keyof typeof signalTasks)[];\n        const results = await Promise.all(Object.values(signalTasks));\n        const r = Object.fromEntries(keys.map((key, i) => [key, results[i]])) as Record<keyof typeof signalTasks, any>;\n\n        // Assign results to fingerprint structure\n        // Automation\n        s.automation.webdriver = r.webdriver;\n        s.automation.webdriverWritable = r.webdriverWritable;\n        s.automation.selenium = r.selenium;\n        s.automation.cdp = r.cdp;\n        s.automation.playwright = r.playwright;\n        s.automation.navigatorPropertyDescriptors = r.navigatorPropertyDescriptors;\n        // Device\n        s.device.cpuCount = r.cpuCount;\n        s.device.memory = r.memory;\n        s.device.platform = r.platform;\n        s.device.screenResolution = r.screenResolution;\n        s.device.multimediaDevices = r.multimediaDevices;\n        s.device.mediaQueries = r.mediaQueries;\n        // Browser\n        s.browser.userAgent = r.userAgent;\n        s.browser.features = r.browserFeatures;\n        s.browser.plugins = r.plugins;\n        s.browser.extensions = r.browserExtensions;\n        s.browser.highEntropyValues = r.highEntropyValues;\n        s.browser.etsl = r.etsl;\n        s.browser.maths = r.maths;\n        s.browser.toSourceError = r.toSourceError;\n        // Graphics\n        s.graphics.webGL = r.webGL;\n        s.graphics.webgpu = r.webgpu;\n        s.graphics.canvas = r.canvas;\n        // Codecs\n        s.codecs = r.mediaCodecs;\n        // Locale\n        s.locale.internationalization = r.internationalization;\n        s.locale.languages = r.languages;\n        // Contexts\n        s.contexts.iframe = r.iframe;\n        s.contexts.webWorker = r.webWorker;\n        // Meta\n        this.fingerprint.nonce = r.nonce;\n        this.fingerprint.time = r.time;\n        this.fingerprint.url = r.url;\n\n        // Run detection rules (needed for fsid generation)\n        this.fingerprint.fastBotDetectionDetails = this.runDetectionRules();\n        \n        // fastBotDetection = true if any detection rule was triggered\n        this.fingerprint.fastBotDetection = Object.values(this.fingerprint.fastBotDetectionDetails)\n            .some(result => result.detected);\n\n        // Generate fsid after all signals and detections are collected\n        this.fingerprint.fsid = this.generateFingerprintScannerId();\n\n        if (encrypt) {\n            const encryptedFingerprint = await this.encryptFingerprint(JSON.stringify(this.fingerprint));\n            return encryptedFingerprint;\n        }\n\n        // Return the raw fingerprint if no encryption is requested\n        return this.fingerprint;\n    }\n}\n\nexport default FingerprintScanner;\nexport * from './types';"
  },
  {
    "path": "src/signals/browserExtensions.ts",
    "content": "import { INIT } from \"./utils\";\n\nexport function browserExtensions() {\n    const browserExtensionsData = {\n        bitmask: INIT,\n        extensions: [] as string[],\n    };\n\n    const hasGrammarly = document.body.hasAttribute('data-gr-ext-installed');\n    const hasMetamask = typeof (window as any).ethereum !=='undefined';\n    const hasCouponBirds = document.getElementById('coupon-birds-drop-div') !== null;\n    const hasDeepL = document.querySelector('deepl-input-controller') !== null;\n    const hasMonicaAI = document.getElementById('monica-content-root') !== null;\n    const hasSiderAI = document.querySelector('chatgpt-sidebar') !== null;\n    const hasRequestly = typeof (window as any).__REQUESTLY__ !== 'undefined';\n    const hasVeepn = Array.from(document.querySelectorAll('*'))\n    .filter(el => el.tagName.toLowerCase().startsWith('veepn-')).length > 0;\n\n    browserExtensionsData.bitmask = [\n        hasGrammarly ? '1' : '0',\n        hasMetamask ? '1' : '0',\n        hasCouponBirds ? '1' : '0',\n        hasDeepL ? '1' : '0',\n        hasMonicaAI ? '1' : '0',\n        hasSiderAI ? '1' : '0',\n        hasRequestly ? '1' : '0',\n        hasVeepn ? '1' : '0',\n    ].join('');\n\n\n    if (hasGrammarly) {\n        browserExtensionsData.extensions.push('grammarly');\n    }\n    if (hasMetamask) {\n        browserExtensionsData.extensions.push('metamask');\n    }\n    if (hasCouponBirds) {\n        browserExtensionsData.extensions.push('coupon-birds');\n    }\n    if (hasDeepL) {\n        browserExtensionsData.extensions.push('deepl');\n    }\n    if (hasMonicaAI) {\n        browserExtensionsData.extensions.push('monica-ai');\n    }\n    if (hasSiderAI) {\n        browserExtensionsData.extensions.push('sider-ai');\n    }\n    if (hasRequestly) {\n        browserExtensionsData.extensions.push('requestly');\n    }\n    if (hasVeepn) {\n        browserExtensionsData.extensions.push('veepn');\n    }\n    \n    return browserExtensionsData;\n}"
  },
  {
    "path": "src/signals/browserFeatures.ts",
    "content": "import { INIT } from \"./utils\";\n\nfunction safeCheck(check: () => boolean): boolean {\n    try {\n        return check();\n    } catch {\n        return false;\n    }\n}\n\nexport function browserFeatures() {\n    const browserFeaturesData = {\n        bitmask: INIT,\n        chrome: safeCheck(() => 'chrome' in window),\n        brave: safeCheck(() => 'brave' in navigator),\n        applePaySupport: safeCheck(() => 'ApplePaySetup' in window),\n        opera: safeCheck(() => (typeof (window as any).opr !== \"undefined\") || \n            (typeof (window as any).onoperadetachedviewchange === \"object\")),\n        serial: safeCheck(() => (window.navigator as any).serial !== undefined),\n        attachShadow: safeCheck(() => !!Element.prototype.attachShadow),\n        caches: safeCheck(() => !!window.caches),\n        webAssembly: safeCheck(() => !!window.WebAssembly && !!window.WebAssembly.instantiate),\n        buffer: safeCheck(() => 'Buffer' in window),\n        showModalDialog: safeCheck(() => 'showModalDialog' in window),\n        safari: safeCheck(() => 'safari' in window),\n        webkitPrefixedFunction: safeCheck(() => 'webkitCancelAnimationFrame' in window),\n        mozPrefixedFunction: safeCheck(() => 'mozGetUserMedia' in navigator),\n        usb: safeCheck(() => typeof (window as any).USB === 'function'),\n        browserCapture: safeCheck(() => typeof (window as any).BrowserCaptureMediaStreamTrack === 'function'),\n        paymentRequestUpdateEvent: safeCheck(() => typeof (window as any).PaymentRequestUpdateEvent === 'function'),\n        pressureObserver: safeCheck(() => typeof (window as any).PressureObserver === 'function'),\n        audioSession: safeCheck(() => 'audioSession' in navigator),\n        selectAudioOutput: safeCheck(() => typeof navigator !== 'undefined' && typeof navigator.mediaDevices !== 'undefined' && typeof (navigator.mediaDevices as any).selectAudioOutput === 'function'),\n        barcodeDetector: safeCheck(() => 'BarcodeDetector' in window),\n        battery: safeCheck(() => 'getBattery' in navigator),\n        devicePosture: safeCheck(() => 'DevicePosture' in window),\n        documentPictureInPicture: safeCheck(() => 'documentPictureInPicture' in window),\n        eyeDropper: safeCheck(() => 'EyeDropper' in window),\n        editContext: safeCheck(() => 'EditContext' in window),\n        fencedFrame: safeCheck(() => 'FencedFrameConfig' in window),\n        sanitizer: safeCheck(() => 'Sanitizer' in window),\n        otpCredential: safeCheck(() => 'OTPCredential' in window),\n    };\n\n    // set bitmask to 0/1 string based on browserFeaturesData, exclude bitmask property itself (you need to filter on the key)\n    // use the filter function to exclude the bitmask property itself\n    const bitmask = Object.keys(browserFeaturesData).filter((key) => key !== 'bitmask').map(key => (browserFeaturesData as any)[key] ? '1' : '0').join('');\n    browserFeaturesData.bitmask = bitmask;\n    return browserFeaturesData;\n}"
  },
  {
    "path": "src/signals/canvas.ts",
    "content": "import { ERROR, INIT, hashCode } from './utils';\nimport { SignalValue } from '../types';\n\nasync function hasModifiedCanvas(): Promise<SignalValue<boolean>> {\n    return new Promise((resolve) => {\n\n        try {\n            const img = new Image();\n            const ctx = document.createElement('canvas').getContext('2d') as CanvasRenderingContext2D;\n            img.onload = () => {\n                ctx.drawImage(img, 0, 0);\n                resolve(ctx.getImageData(0, 0, 1, 1).data.filter(x => x === 0).length != 4);\n            };\n\n            img.onerror = () => {\n                resolve(ERROR);\n            };\n            img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII=';\n        } catch (e) {\n            resolve(ERROR);\n        }\n    });\n}\n\n\nfunction getCanvasFingerprint(): SignalValue<string> {\n    var canvas = document.createElement('canvas');\n    canvas.width = 400;\n    canvas.height = 200;\n    canvas.style.display = \"inline\";\n    var context = canvas.getContext(\"2d\") as CanvasRenderingContext2D;\n\n    try {\n        context.rect(0, 0, 10, 10);\n        context.rect(2, 2, 6, 6);\n        context.textBaseline = \"alphabetic\";\n        context.fillStyle = \"#f60\";\n        context.fillRect(125, 1, 62, 20);\n        context.fillStyle = \"#069\";\n        context.font = \"11pt no-real-font-123\";\n        context.fillText(\"Cwm fjordbank glyphs vext quiz, 😃\", 2, 15);\n        context.fillStyle = \"rgba(102, 204, 0, 0.2)\";\n        context.font = \"18pt Arial\";\n        context.fillText(\"Cwm fjordbank glyphs vext quiz, 😃\", 4, 45);\n\n        context.globalCompositeOperation = \"multiply\";\n        context.fillStyle = \"rgb(255,0,255)\";\n        context.beginPath();\n        context.arc(50, 50, 50, 0, 2 * Math.PI, !0);\n        context.closePath();\n        context.fill();\n        context.fillStyle = \"rgb(0,255,255)\";\n        context.beginPath();\n        context.arc(100, 50, 50, 0, 2 * Math.PI, !0);\n        context.closePath();\n        context.fill();\n        context.fillStyle = \"rgb(255,255,0)\";\n        context.beginPath();\n        context.arc(75, 100, 50, 0, 2 * Math.PI, !0);\n        context.closePath();\n        context.fill();\n        context.fillStyle = \"rgb(255,0,255)\";\n        context.arc(75, 75, 75, 0, 2 * Math.PI, !0);\n        context.arc(75, 75, 25, 0, 2 * Math.PI, !0);\n        context.fill(\"evenodd\");\n        return hashCode(canvas.toDataURL());\n\n    } catch (e) {\n        return ERROR;\n    }\n}\n\nexport async function canvas() {\n    const canvasData = {\n        hasModifiedCanvas: INIT as SignalValue<boolean>,\n        canvasFingerprint: INIT as SignalValue<string>,\n    };\n\n    canvasData.hasModifiedCanvas = await hasModifiedCanvas();\n\n    canvasData.canvasFingerprint = getCanvasFingerprint();\n\n    return canvasData;\n}"
  },
  {
    "path": "src/signals/cdp.ts",
    "content": "import { ERROR } from './utils';\n\nexport function cdp() {\n    try {\n        let wasAccessed = false;\n        const originalPrepareStackTrace = (Error as any).prepareStackTrace;\n        (Error as any).prepareStackTrace = function () {\n            wasAccessed = true;\n            return originalPrepareStackTrace;\n        };\n        const err = new Error('');\n        console.log(err);\n\n        return wasAccessed;\n    } catch (e) {\n        return ERROR;\n    }\n}"
  },
  {
    "path": "src/signals/cpuCount.ts",
    "content": "import { NA } from './utils';\n\nexport function cpuCount() {\n    return navigator.hardwareConcurrency || NA;\n}"
  },
  {
    "path": "src/signals/etsl.ts",
    "content": "export function etsl() {\n    return eval.toString().length;\n}"
  },
  {
    "path": "src/signals/highEntropyValues.ts",
    "content": "import { ERROR, INIT, NA, setObjectValues } from \"./utils\";\n\nexport async function highEntropyValues() {\n    const navigator = window.navigator as any;\n    const highEntropyValues = {\n        architecture: INIT,\n        bitness: INIT,\n        brands: INIT,\n        mobile: INIT,\n        model: INIT,\n        platform: INIT,\n        platformVersion: INIT,\n        uaFullVersion: INIT,\n    };\n\n    if ('userAgentData' in navigator) {\n        try {\n            const ua = await navigator.userAgentData.getHighEntropyValues([\n                \"architecture\",\n                \"bitness\",\n                \"brands\",\n                \"mobile\",\n                \"model\",\n                \"platform\",\n                \"platformVersion\",\n                \"uaFullVersion\"\n            ]);\n\n            highEntropyValues.architecture = ua.architecture;\n            highEntropyValues.bitness = ua.bitness;\n            highEntropyValues.brands = ua.brands;\n            highEntropyValues.mobile = ua.mobile;\n            highEntropyValues.model = ua.model;\n            highEntropyValues.platform = ua.platform;\n            highEntropyValues.platformVersion = ua.platformVersion;\n            highEntropyValues.uaFullVersion = ua.uaFullVersion;\n\n\n        } catch (e) {\n            setObjectValues(highEntropyValues, ERROR);\n        }\n\n    } else {\n        setObjectValues(highEntropyValues, NA);\n    }\n\n    return highEntropyValues;\n}"
  },
  {
    "path": "src/signals/iframe.ts",
    "content": "import { ERROR, INIT, NA, setObjectValues } from './utils';\n\nexport function iframe() {\n    const iframeData = {\n        webdriver: INIT,\n        userAgent: INIT,\n        platform: INIT,\n        memory: INIT,\n        cpuCount: INIT,\n        language: INIT,\n    };\n    const iframe = document.createElement('iframe');\n    let iframeAdded = false;\n\n    try {\n        iframe.style.display = 'none';\n        iframe.src = 'about:blank';\n        document.body.appendChild(iframe);\n        iframeAdded = true;\n\n        const iframeWindowNavigator = (iframe.contentWindow?.navigator as any);\n\n        iframeData.webdriver = iframeWindowNavigator.webdriver ?? false;\n        iframeData.userAgent = iframeWindowNavigator.userAgent ?? NA;\n        iframeData.platform = iframeWindowNavigator.platform ?? NA;\n        iframeData.memory = iframeWindowNavigator.deviceMemory ?? NA;\n        iframeData.cpuCount = iframeWindowNavigator.hardwareConcurrency ?? NA;\n        iframeData.language = iframeWindowNavigator.language ?? NA;\n    } catch (e) {\n        setObjectValues(iframeData, ERROR);\n    } finally {\n        if (iframeAdded) {\n            try {\n                document.body.removeChild(iframe);\n            } catch (_) {\n                // Ignore removal errors\n            }\n        }\n    }\n\n    return iframeData;\n}"
  },
  {
    "path": "src/signals/internationalization.ts",
    "content": "import { INIT, ERROR, NA } from \"./utils\";\n\nexport function internationalization() {\n    const internationalizationData = {\n        timezone: INIT,\n        localeLanguage: INIT,\n    };\n\n    try {\n        if (typeof Intl !== 'undefined' && typeof Intl.DateTimeFormat !== 'undefined') {\n            const dtfOptions = Intl.DateTimeFormat().resolvedOptions();\n            internationalizationData.timezone = dtfOptions.timeZone;\n            internationalizationData.localeLanguage = dtfOptions.locale;\n        } else {\n            internationalizationData.timezone = NA;\n            internationalizationData.localeLanguage = NA;\n        }\n    } catch (e) {\n        internationalizationData.timezone = ERROR;\n        internationalizationData.localeLanguage = ERROR;\n    }\n\n    return internationalizationData;\n}"
  },
  {
    "path": "src/signals/languages.ts",
    "content": "export function languages() {\n    return {\n        languages: navigator.languages,\n        language: navigator.language,\n    }\n}"
  },
  {
    "path": "src/signals/maths.ts",
    "content": "import { hashCode } from './utils';\n\nexport function maths() {\n    const results: number[] = [];\n    const testValue = 0.123456789;\n\n    // Math constants\n    const constants = [\"E\", \"LN10\", \"LN2\", \"LOG10E\", \"LOG2E\", \"PI\", \"SQRT1_2\", \"SQRT2\"];\n    constants.forEach(function (name) {\n        try {\n            results.push((Math as any)[name]);\n        } catch (e) {\n            results.push(-1);\n        }\n    });\n\n    // Math functions (can reveal VM/browser differences)\n    const mathFunctions = [\"tan\", \"sin\", \"exp\", \"atan\", \"acosh\", \"asinh\", \"atanh\", \"expm1\", \"log1p\", \"sinh\"];\n\n\n    mathFunctions.forEach(function (name) {\n        try {\n            results.push((Math as any)[name](testValue));\n        } catch (e) {\n            results.push(-1);\n        }\n    });\n\n    return hashCode(results.map(String).join(\",\"));\n}"
  },
  {
    "path": "src/signals/mediaCodecs.ts",
    "content": "import { ERROR, NA, hashCode, setObjectValues } from './utils';\n\n\nconst AUDIO_CODECS = [\n    'audio/mp4; codecs=\"mp4a.40.2\"',\n    'audio/mpeg;',\n    'audio/webm; codecs=\"vorbis\"',\n    'audio/ogg; codecs=\"vorbis\"',\n    'audio/wav; codecs=\"1\"',\n    'audio/ogg; codecs=\"speex\"',\n    'audio/ogg; codecs=\"flac\"',\n    'audio/3gpp; codecs=\"samr\"',\n];\n\nconst VIDEO_CODECS = [\n    'video/mp4; codecs=\"avc1.42E01E, mp4a.40.2\"',\n    'video/mp4; codecs=\"avc1.42E01E\"',\n    'video/mp4; codecs=\"avc1.58A01E\"',\n    'video/mp4; codecs=\"avc1.4D401E\"',\n    'video/mp4; codecs=\"avc1.64001E\"',\n    'video/mp4; codecs=\"mp4v.20.8\"',\n    'video/mp4; codecs=\"mp4v.20.240\"',\n    'video/webm; codecs=\"vp8\"',\n    'video/ogg; codecs=\"theora\"',\n    'video/ogg; codecs=\"dirac\"',\n    'video/3gpp; codecs=\"mp4v.20.8\"',\n    'video/x-matroska; codecs=\"theora\"',\n];\n\n\nfunction getCanPlayTypeSupport(codecs: string[], mediaType: 'audio' | 'video'): Record<string, string | null> {\n    const result: Record<string, string | null> = {};\n    try {\n        const element = document.createElement(mediaType);\n        for (const codec of codecs) {\n            try {\n                result[codec] = element.canPlayType(codec) || null;\n            } catch {\n                result[codec] = null;\n            }\n        }\n    } catch {\n        for (const codec of codecs) {\n            result[codec] = null;\n        }\n    }\n    return result;\n}\n\nfunction getMediaSourceSupport(codecs: string[]): Record<string, boolean | null> {\n    const result: Record<string, boolean | null> = {};\n    const MediaSource = window.MediaSource;\n    \n    if (!MediaSource || typeof MediaSource.isTypeSupported !== 'function') {\n        for (const codec of codecs) {\n            result[codec] = null;\n        }\n        return result;\n    }\n    \n    for (const codec of codecs) {\n        try {\n            result[codec] = MediaSource.isTypeSupported(codec);\n        } catch {\n            result[codec] = null;\n        }\n    }\n    return result;\n}\n\nfunction getRtcCapabilities(kind: 'audio' | 'video'): string | typeof NA | typeof ERROR {\n    try {\n        const RTCRtpReceiver = window.RTCRtpReceiver;\n        if (RTCRtpReceiver && typeof RTCRtpReceiver.getCapabilities === 'function') {\n            const capabilities = RTCRtpReceiver.getCapabilities(kind);\n            return hashCode(JSON.stringify(capabilities));\n        }\n        return NA;\n    } catch (e) {\n        return ERROR;\n    }\n}\n\nexport function mediaCodecs() {\n    const mediaCodecsData = {\n        audioCanPlayTypeHash: NA as string | typeof NA | typeof ERROR,\n        videoCanPlayTypeHash: NA as string | typeof NA | typeof ERROR,\n        audioMediaSourceHash: NA as string | typeof NA | typeof ERROR,\n        videoMediaSourceHash: NA as string | typeof NA | typeof ERROR,\n        rtcAudioCapabilitiesHash: NA as string | typeof NA | typeof ERROR,\n        rtcVideoCapabilitiesHash: NA as string | typeof NA | typeof ERROR,\n        hasMediaSource: false,\n    };\n\n    try {\n        // Check MediaSource availability\n        mediaCodecsData.hasMediaSource = !!window.MediaSource;\n\n        // canPlayType support - hash the results\n        const audioCanPlayType = getCanPlayTypeSupport(AUDIO_CODECS, 'audio');\n        const videoCanPlayType = getCanPlayTypeSupport(VIDEO_CODECS, 'video');\n        mediaCodecsData.audioCanPlayTypeHash = hashCode(JSON.stringify(audioCanPlayType));\n        mediaCodecsData.videoCanPlayTypeHash = hashCode(JSON.stringify(videoCanPlayType));\n\n        // MediaSource.isTypeSupported - hash the results\n        const audioMediaSource = getMediaSourceSupport(AUDIO_CODECS);\n        const videoMediaSource = getMediaSourceSupport(VIDEO_CODECS);\n        mediaCodecsData.audioMediaSourceHash = hashCode(JSON.stringify(audioMediaSource));\n        mediaCodecsData.videoMediaSourceHash = hashCode(JSON.stringify(videoMediaSource));\n\n        // RTCRtpReceiver.getCapabilities - already returns hash\n        mediaCodecsData.rtcAudioCapabilitiesHash = getRtcCapabilities('audio');\n        mediaCodecsData.rtcVideoCapabilitiesHash = getRtcCapabilities('video');\n\n    } catch (e) {\n        setObjectValues(mediaCodecsData, ERROR);\n    }\n\n    return mediaCodecsData;\n}\n"
  },
  {
    "path": "src/signals/mediaQueries.ts",
    "content": "import { ERROR, INIT, setObjectValues } from './utils';\n\nexport function mediaQueries() {\n    const mediaQueriesData = {\n        prefersColorScheme: INIT as string | null | typeof INIT | typeof ERROR,\n        prefersReducedMotion: INIT as boolean | typeof INIT | typeof ERROR,\n        prefersReducedTransparency: INIT as boolean | typeof INIT | typeof ERROR,\n        colorGamut: INIT as string | null | typeof INIT | typeof ERROR,\n        pointer: INIT as string | null | typeof INIT | typeof ERROR,\n        anyPointer: INIT as string | null | typeof INIT | typeof ERROR,\n        hover: INIT as boolean | typeof INIT | typeof ERROR,\n        anyHover: INIT as boolean | typeof INIT | typeof ERROR,\n        colorDepth: INIT as number | typeof INIT | typeof ERROR,\n    };\n\n    try {\n        // Prefers color scheme\n        if (window.matchMedia('(prefers-color-scheme: dark)').matches) {\n            mediaQueriesData.prefersColorScheme = 'dark';\n        } else if (window.matchMedia('(prefers-color-scheme: light)').matches) {\n            mediaQueriesData.prefersColorScheme = 'light';\n        } else {\n            mediaQueriesData.prefersColorScheme = null;\n        }\n\n        // Prefers reduced motion\n        mediaQueriesData.prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n        // Prefers reduced transparency\n        mediaQueriesData.prefersReducedTransparency = window.matchMedia('(prefers-reduced-transparency: reduce)').matches;\n\n        // Color gamut\n        if (window.matchMedia('(color-gamut: rec2020)').matches) {\n            mediaQueriesData.colorGamut = 'rec2020';\n        } else if (window.matchMedia('(color-gamut: p3)').matches) {\n            mediaQueriesData.colorGamut = 'p3';\n        } else if (window.matchMedia('(color-gamut: srgb)').matches) {\n            mediaQueriesData.colorGamut = 'srgb';\n        } else {\n            mediaQueriesData.colorGamut = null;\n        }\n\n        // Pointer\n        if (window.matchMedia('(pointer: fine)').matches) {\n            mediaQueriesData.pointer = 'fine';\n        } else if (window.matchMedia('(pointer: coarse)').matches) {\n            mediaQueriesData.pointer = 'coarse';\n        } else if (window.matchMedia('(pointer: none)').matches) {\n            mediaQueriesData.pointer = 'none';\n        } else {\n            mediaQueriesData.pointer = null;\n        }\n\n        // Any pointer\n        if (window.matchMedia('(any-pointer: fine)').matches) {\n            mediaQueriesData.anyPointer = 'fine';\n        } else if (window.matchMedia('(any-pointer: coarse)').matches) {\n            mediaQueriesData.anyPointer = 'coarse';\n        } else if (window.matchMedia('(any-pointer: none)').matches) {\n            mediaQueriesData.anyPointer = 'none';\n        } else {\n            mediaQueriesData.anyPointer = null;\n        }\n\n        // Hover\n        mediaQueriesData.hover = window.matchMedia('(hover: hover)').matches;\n\n        // Any hover\n        mediaQueriesData.anyHover = window.matchMedia('(any-hover: hover)').matches;\n\n        // Color depth - find the maximum supported color depth\n        let maxColorDepth = 0;\n        for (let c = 0; c <= 16; c++) {\n            if (window.matchMedia(`(color: ${c})`).matches) {\n                maxColorDepth = c;\n            }\n        }\n        mediaQueriesData.colorDepth = maxColorDepth;\n\n    } catch (e) {\n        setObjectValues(mediaQueriesData, ERROR);\n    }\n\n    return mediaQueriesData;\n}\n"
  },
  {
    "path": "src/signals/memory.ts",
    "content": "import { NA } from \"./utils\";\n\nexport function memory() {\n    return (navigator as any).deviceMemory || NA;\n}"
  },
  {
    "path": "src/signals/multimediaDevices.ts",
    "content": "import { NA, setObjectValues } from \"./utils\";\n\nexport async function multimediaDevices() {\n    return new Promise(async function (resolve) {\n        var deviceToCount = {\n            \"audiooutput\": 0,\n            \"audioinput\": 0,\n            \"videoinput\": 0\n        };\n\n        if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {\n            const devices = await navigator.mediaDevices.enumerateDevices();\n            if (typeof devices !== \"undefined\") {\n                for (var i = 0; i < devices.length; i++) {\n                    var name = devices[i].kind as keyof typeof deviceToCount;\n                    deviceToCount[name] = deviceToCount[name] + 1;\n                }\n\n                return resolve({\n                    speakers: deviceToCount.audiooutput,\n                    microphones: deviceToCount.audioinput,\n                    webcams: deviceToCount.videoinput\n                });\n            } else {\n                setObjectValues(deviceToCount, NA);\n                return resolve(deviceToCount);\n            }\n\n        } else {\n            setObjectValues(deviceToCount, NA);\n            return resolve(deviceToCount);\n        }\n    });\n}"
  },
  {
    "path": "src/signals/navigatorPropertyDescriptors.ts",
    "content": "export function navigatorPropertyDescriptors() {\n    const properties = ['deviceMemory', 'hardwareConcurrency', 'language', 'languages', 'platform'];\n\n    const results = [];\n\n    for (const property of properties) {\n        const res = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(navigator), property);\n\n        if (res && res.value) {\n            results.push('1');\n        } else {\n            results.push('0');\n        }\n    }\n\n    return results.join('');\n}"
  },
  {
    "path": "src/signals/nonce.ts",
    "content": "export function nonce() {\n    return Math.random().toString(36).substring(2, 15);\n}\n"
  },
  {
    "path": "src/signals/platform.ts",
    "content": "export function platform() {\n    return navigator.platform;\n}"
  },
  {
    "path": "src/signals/playwright.ts",
    "content": "export function playwright() {\n    return '__pwInitScripts' in window || '__playwright__binding__' in window;\n}"
  },
  {
    "path": "src/signals/plugins.ts",
    "content": "import { SignalValue } from \"../types\";\nimport { INIT, NA, hashCode, ERROR, setObjectValues} from \"./utils\";\n\nfunction isValidPluginArray() {\n    if (!navigator.plugins) return false;\n    const str = typeof navigator.plugins.toString === \"function\" \n      ? navigator.plugins.toString() \n      : navigator.plugins.constructor && typeof navigator.plugins.constructor.toString === \"function\"\n        ? navigator.plugins.constructor.toString()\n        : typeof navigator.plugins;\n    return str === \"[object PluginArray]\" || \n           str === \"[object MSPluginsCollection]\" || \n           str === \"[object HTMLPluginsCollection]\";\n  }\n\n  function getPluginNamesHash() {\n    if (!navigator.plugins) return NA;\n\n    const pluginNames = [];\n    for (let i = 0; i < navigator.plugins.length; i++) {\n      pluginNames.push(navigator.plugins[i].name);\n    }\n    return hashCode(pluginNames.join(\",\"));\n  }\n\n\n  function getPluginCount() {\n    if (!navigator.plugins) return NA;\n    return navigator.plugins.length;\n  }\n\n  function getPluginConsistency1() {\n    if (!navigator.plugins) return NA;\n    try {\n        return navigator.plugins[0] === navigator.plugins[0][0].enabledPlugin;\n    } catch (e) {\n        return ERROR;\n    }\n  }\n\n  function getPluginOverflow() {\n    if (!navigator.plugins) return NA;\n\n    try {\n        return navigator.plugins.item(4294967296) !== navigator.plugins[0];\n    } catch (e) {\n        return ERROR;\n    }\n  }\n\nexport function plugins() {\n    const pluginsData = {\n        isValidPluginArray: INIT as SignalValue<boolean>,\n        pluginCount: INIT as SignalValue<number>,\n        pluginNamesHash: INIT as SignalValue<string>,\n        pluginConsistency1: INIT as SignalValue<boolean>,\n        pluginOverflow: INIT as SignalValue<boolean>,\n    }\n    \n    try {\n        pluginsData.isValidPluginArray = isValidPluginArray();\n        pluginsData.pluginCount = getPluginCount();\n        pluginsData.pluginNamesHash = getPluginNamesHash();\n        pluginsData.pluginConsistency1 = getPluginConsistency1();\n        pluginsData.pluginOverflow = getPluginOverflow();\n    } catch (e) {\n        setObjectValues(pluginsData, ERROR);\n    }\n    return pluginsData;\n}"
  },
  {
    "path": "src/signals/screenResolution.ts",
    "content": "import { NA } from './utils';\n\nexport function screenResolution() {\n    return {\n        width: window.screen.width,\n        height: window.screen.height,\n        pixelDepth: window.screen.pixelDepth,\n        colorDepth: window.screen.colorDepth,\n        availableWidth: window.screen.availWidth,\n        availableHeight: window.screen.availHeight,\n        innerWidth: window.innerWidth,\n        innerHeight: window.innerHeight,\n        hasMultipleDisplays: typeof (screen as any).isExtended !== 'undefined' ? (screen as any).isExtended : NA,\n    };\n}"
  },
  {
    "path": "src/signals/seleniumProperties.ts",
    "content": "export function hasSeleniumProperties() {\n    const seleniumProps = [\n        \"__driver_evaluate\",\n        \"__webdriver_evaluate\", \n        \"__selenium_evaluate\",\n        \"__fxdriver_evaluate\",\n        \"__driver_unwrapped\",\n        \"__webdriver_unwrapped\",\n        \"__selenium_unwrapped\", \n        \"__fxdriver_unwrapped\",\n        \"_Selenium_IDE_Recorder\",\n        \"_selenium\",\n        \"calledSelenium\",\n        \"$cdc_asdjflasutopfhvcZLmcfl_\",\n        \"$chrome_asyncScriptInfo\",\n        \"__$webdriverAsyncExecutor\",\n        \"webdriver\",\n        \"__webdriverFunc\",\n        \"domAutomation\",\n        \"domAutomationController\",\n        \"__lastWatirAlert\",\n        \"__lastWatirConfirm\",\n        \"__lastWatirPrompt\",\n        \"__webdriver_script_fn\",\n        \"_WEBDRIVER_ELEM_CACHE\"\n      ];\n\n      let hasSeleniumProperty = false;\n\n      for (let i = 0; i < seleniumProps.length; i++) {\n        if (seleniumProps[i] in window) {\n          hasSeleniumProperty = true;\n          break;\n        }\n      }\n\n      hasSeleniumProperty = hasSeleniumProperty || !!(document as any).__webdriver_script_fn || !!(window as any).domAutomation || !!(window as any).domAutomationController\n      \n      return hasSeleniumProperty;\n}"
  },
  {
    "path": "src/signals/time.ts",
    "content": "export function time() {\n    return new Date().getTime();\n}\n"
  },
  {
    "path": "src/signals/toSourceError.ts",
    "content": "import { INIT } from './utils';\n\nexport function toSourceError() {\n    const toSourceErrorData = {\n        toSourceError: INIT,\n        hasToSource: false,\n    };\n\n    try {\n        (null as any).usdfsh;\n    } catch (e) {\n        toSourceErrorData.toSourceError = (e as Error).toString();\n    }\n\n    try {\n        throw \"xyz\";\n    } catch (e: any) {\n        try {\n            e.toSource();\n            toSourceErrorData.hasToSource = true;\n        } catch (e2) {\n            toSourceErrorData.hasToSource = false;\n        }\n    }\n\n    return toSourceErrorData;\n}"
  },
  {
    "path": "src/signals/url.ts",
    "content": "export function pageURL() {\n    return window.location.href;\n}\n"
  },
  {
    "path": "src/signals/userAgent.ts",
    "content": "export function userAgent() {\n    return navigator.userAgent;\n}"
  },
  {
    "path": "src/signals/utils.ts",
    "content": "export const ERROR = 'ERROR';\nexport const INIT = 'INIT';\nexport const NA = 'NA';\nexport const SKIPPED = 'SKIPPED';\nexport const HIGH = 'high'\nexport const LOW = 'low'\nexport const MEDIUM = 'medium'\n\n\nexport function hashCode(str: string) {\n    let hash = 0;\n    for (let i = 0, len = str.length; i < len; i++) {\n        let chr = str.charCodeAt(i);\n        hash = (hash << 5) - hash + chr;\n        hash |= 0;\n    }\n    return hash.toString(16).padStart(8, \"0\");\n}\n\nexport function setObjectValues(object: any, value: any) {\n    for (const key in object) {\n        object[key] = value;\n    }\n}\n\n\nexport function isFirefox() {\n    return (navigator as any).buildID === \"20181001000000\";\n}\n"
  },
  {
    "path": "src/signals/webGL.ts",
    "content": "import { ERROR, INIT, NA, isFirefox, setObjectValues } from './utils';\n\nexport function webGL() {\n    const webGLData = {\n        vendor: INIT,\n        renderer: INIT,\n    };\n\n    if (isFirefox()) {\n        setObjectValues(webGLData, NA);\n        return webGLData;\n    }\n\n    try {\n        var canvas = document.createElement('canvas');\n        var ctx = (canvas.getContext(\"webgl\") || canvas.getContext(\"experimental-webgl\")) as any;\n        if (ctx.getSupportedExtensions().indexOf(\"WEBGL_debug_renderer_info\") >= 0) {\n            webGLData.vendor = ctx.getParameter(ctx.getExtension('WEBGL_debug_renderer_info').UNMASKED_VENDOR_WEBGL);\n            webGLData.renderer = ctx.getParameter(ctx.getExtension('WEBGL_debug_renderer_info').UNMASKED_RENDERER_WEBGL);\n        } else {\n            setObjectValues(webGLData, NA);\n        }\n    } catch (e) {\n        setObjectValues(webGLData, ERROR);\n    }\n\n    return webGLData;\n}"
  },
  {
    "path": "src/signals/webdriver.ts",
    "content": "export function webdriver() {\n    return navigator.webdriver;\n};"
  },
  {
    "path": "src/signals/webdriverWritable.ts",
    "content": "export function webdriverWritable() {\n    try {\n        const prop = \"webdriver\";\n        const navigator = window.navigator as any;\n        if (!navigator[prop] && !navigator.hasOwnProperty(prop)) {\n            navigator[prop] = 1;\n            const writable = navigator[prop] === 1;\n            delete navigator[prop];\n            return writable;\n        }\n        return true;\n    } catch (e) {\n        return false;\n    }\n}"
  },
  {
    "path": "src/signals/webgpu.ts",
    "content": "import { ERROR, INIT, NA, setObjectValues } from \"./utils\";\n\nexport async function webgpu() {\n    const webGPUData = {\n        vendor: INIT,\n        architecture: INIT,\n        device: INIT,\n        description: INIT,\n    };\n\n    if ('gpu' in navigator) {\n        try {\n            const adapter = await (navigator as any).gpu.requestAdapter();\n            if (adapter) {\n                webGPUData.vendor = adapter.info.vendor;\n                webGPUData.architecture = adapter.info.architecture;\n                webGPUData.device = adapter.info.device;\n                webGPUData.description = adapter.info.description;\n            }\n        } catch (e) {\n            setObjectValues(webGPUData, ERROR);\n        }\n    } else {\n        setObjectValues(webGPUData, NA);\n    }\n\n    return webGPUData;\n}"
  },
  {
    "path": "src/signals/worker.ts",
    "content": "import { ERROR, INIT, setObjectValues } from './utils';\n\nexport async function worker() {\n    return new Promise((resolve) => {\n        const workerData = {\n            vendor: INIT,\n            renderer: INIT,\n            userAgent: INIT,\n            language: INIT,\n            platform: INIT,\n            memory: INIT,\n            cpuCount: INIT,\n        };\n\n        let worker: Worker | null = null;\n        let workerUrl: string | null = null;\n        let timeoutId: number | null = null;\n\n        const cleanup = () => {\n            if (timeoutId) clearTimeout(timeoutId);\n            if (worker) worker.terminate();\n            if (workerUrl) URL.revokeObjectURL(workerUrl);\n        };\n\n        try {\n            const workerCode = `try {\n                var fingerprintWorker = {};\n\n                fingerprintWorker.userAgent = navigator.userAgent;\n                fingerprintWorker.language = navigator.language;\n                fingerprintWorker.cpuCount = navigator.hardwareConcurrency;\n                fingerprintWorker.platform = navigator.platform;\n                fingerprintWorker.memory = navigator.deviceMemory;\n                \n\n                var canvas = new OffscreenCanvas(1, 1);\n                fingerprintWorker.vendor = 'INIT';\n                fingerprintWorker.renderer = 'INIT';\n                var gl = canvas.getContext('webgl');\n                const isFirefox = navigator.userAgent.includes('Firefox');\n                try {\n                    if (gl && !isFirefox) {\n                        var glExt = gl.getExtension('WEBGL_debug_renderer_info');\n                        fingerprintWorker.vendor = gl.getParameter(glExt.UNMASKED_VENDOR_WEBGL);\n                        fingerprintWorker.renderer = gl.getParameter(glExt.UNMASKED_RENDERER_WEBGL);\n                    } else {\n                        fingerprintWorker.vendor = 'NA';\n                        fingerprintWorker.renderer = 'NA';\n                    }\n                } catch (_) {\n                    fingerprintWorker.vendor = 'ERROR';\n                    fingerprintWorker.renderer = 'ERROR';\n                }\n                self.postMessage(fingerprintWorker);\n            } catch (e) {\n                self.postMessage(fingerprintWorker);\n            }`\n\n            \n            const blob = new Blob([workerCode], { type: 'application/javascript' });\n            workerUrl = URL.createObjectURL(blob);\n            worker = new Worker(workerUrl);\n\n            // Set timeout to prevent infinite hang\n            timeoutId = window.setTimeout(() => {\n                cleanup();\n                setObjectValues(workerData, ERROR);\n                resolve(workerData);\n            }, 2000);\n\n            worker.onmessage = function (e) {\n                try {\n                    workerData.vendor = e.data.vendor;\n                    workerData.renderer = e.data.renderer;\n                    workerData.userAgent = e.data.userAgent;\n                    workerData.language = e.data.language;\n                    workerData.platform = e.data.platform;\n                    workerData.memory = e.data.memory;\n                    workerData.cpuCount = e.data.cpuCount;\n                } catch (_) {\n                    setObjectValues(workerData, ERROR);\n                } finally {\n                    cleanup();\n                    resolve(workerData);\n                }\n            };\n\n            worker.onerror = function () {\n                cleanup();\n                setObjectValues(workerData, ERROR);\n                resolve(workerData);\n            };\n        } catch (e) {\n            cleanup();\n            setObjectValues(workerData, ERROR);\n            resolve(workerData);\n        }\n\n    });\n\n}"
  },
  {
    "path": "src/types.ts",
    "content": "import { ERROR, INIT, NA, SKIPPED } from './signals/utils';\n\nexport type SignalValue<T> = T | typeof ERROR | typeof INIT | typeof NA | typeof SKIPPED;\n\nexport interface WebGLSignal {\n    vendor: SignalValue<string>;\n    renderer: SignalValue<string>;\n}\n\nexport interface InternationalizationSignal {\n    timezone: SignalValue<string>;\n    localeLanguage: SignalValue<string>;\n}\n\nexport interface ScreenResolutionSignal {\n    width: SignalValue<number>;\n    height: SignalValue<number>;\n    pixelDepth: SignalValue<number>;\n    colorDepth: SignalValue<number>;\n    availableWidth: SignalValue<number>;\n    availableHeight: SignalValue<number>;\n    innerWidth: SignalValue<number>;\n    innerHeight: SignalValue<number>;\n    hasMultipleDisplays: SignalValue<boolean>;\n}\n\nexport interface LanguagesSignal {\n    languages: SignalValue<string[]>;\n    language: SignalValue<string>;\n}\n\nexport interface WebGPUSignal {\n    vendor: SignalValue<string>;\n    architecture: SignalValue<string>;\n    device: SignalValue<string>;\n    description: SignalValue<string>;\n}\n\nexport interface IframeSignal {\n    webdriver: SignalValue<boolean>;\n    userAgent: SignalValue<string>;\n    platform: SignalValue<string>;\n    memory: SignalValue<number>;\n    cpuCount: SignalValue<number>;\n    language: SignalValue<string>;\n}\n\nexport interface WebWorkerSignal {\n    webdriver: SignalValue<boolean>;\n    userAgent: SignalValue<string>;\n    platform: SignalValue<string>;\n    memory: SignalValue<number>;\n    cpuCount: SignalValue<number>;\n    language: SignalValue<string>;\n    vendor: SignalValue<string>;\n    renderer: SignalValue<string>;\n}\n\nexport interface BrowserExtensionsSignal {\n    bitmask: SignalValue<string>;\n    extensions: SignalValue<string[]>;\n}\n\nexport interface BrowserFeaturesSignal {\n    bitmask: SignalValue<string>;\n    chrome: SignalValue<boolean>;\n    brave: SignalValue<boolean>;\n    applePaySupport: SignalValue<boolean>;\n    opera: SignalValue<boolean>;\n    serial: SignalValue<boolean>;\n    attachShadow: SignalValue<boolean>;\n    caches: SignalValue<boolean>;\n    webAssembly: SignalValue<boolean>;\n    buffer: SignalValue<boolean>;\n    showModalDialog: SignalValue<boolean>;\n    safari: SignalValue<boolean>;\n    webkitPrefixedFunction: SignalValue<boolean>;\n    mozPrefixedFunction: SignalValue<boolean>;\n    usb: SignalValue<boolean>;\n    browserCapture: SignalValue<boolean>;\n    paymentRequestUpdateEvent: SignalValue<boolean>;\n    pressureObserver: SignalValue<boolean>;\n    audioSession: SignalValue<boolean>;\n    selectAudioOutput: SignalValue<boolean>;\n    barcodeDetector: SignalValue<boolean>;\n    battery: SignalValue<boolean>;\n    devicePosture: SignalValue<boolean>;\n    documentPictureInPicture: SignalValue<boolean>;\n    eyeDropper: SignalValue<boolean>;\n    editContext: SignalValue<boolean>;\n    fencedFrame: SignalValue<boolean>;\n    sanitizer: SignalValue<boolean>;\n    otpCredential: SignalValue<boolean>;\n}\n\nexport interface MediaQueriesSignal {\n    prefersColorScheme: SignalValue<string | null>;\n    prefersReducedMotion: SignalValue<boolean>;\n    prefersReducedTransparency: SignalValue<boolean>;\n    colorGamut: SignalValue<string | null>;\n    pointer: SignalValue<string | null>;\n    anyPointer: SignalValue<string | null>;\n    hover: SignalValue<boolean>;\n    anyHover: SignalValue<boolean>;\n    colorDepth: SignalValue<number>;\n}\n\nexport interface ToSourceErrorSignal {\n    toSourceError: SignalValue<string>;\n    hasToSource: SignalValue<boolean>;\n}\n\nexport interface CanvasSignal {\n    hasModifiedCanvas: SignalValue<boolean>;\n    canvasFingerprint: SignalValue<string>;\n}\n\nexport interface HighEntropyValuesSignal {\n    architecture: SignalValue<string>;\n    bitness: SignalValue<string>;\n    brands: SignalValue<string[]>;\n    mobile: SignalValue<boolean>;\n    model: SignalValue<string>;\n    platform: SignalValue<string>;\n    platformVersion: SignalValue<string>;\n    uaFullVersion: SignalValue<string>;\n}\n\nexport interface PluginsSignal {\n    isValidPluginArray: SignalValue<boolean>;\n    pluginCount: SignalValue<number>;\n    pluginNamesHash: SignalValue<string>;\n    pluginConsistency1: SignalValue<boolean>;\n    pluginOverflow: SignalValue<boolean>;\n}\n\nexport interface MultimediaDevicesSignal {\n    speakers: SignalValue<number>;\n    microphones: SignalValue<number>;\n    webcams: SignalValue<number>;\n}\n\nexport interface MediaCodecsSignal {\n    audioCanPlayTypeHash: SignalValue<string>;\n    videoCanPlayTypeHash: SignalValue<string>;\n    audioMediaSourceHash: SignalValue<string>;\n    videoMediaSourceHash: SignalValue<string>;\n    rtcAudioCapabilitiesHash: SignalValue<string>;\n    rtcVideoCapabilitiesHash: SignalValue<string>;\n    hasMediaSource: SignalValue<boolean>;\n}\n\n// Grouped signal interfaces\nexport interface AutomationSignals {\n    webdriver: SignalValue<boolean>;\n    webdriverWritable: SignalValue<boolean>;\n    selenium: SignalValue<boolean>;\n    cdp: SignalValue<boolean>;\n    playwright: SignalValue<boolean>;\n    navigatorPropertyDescriptors: SignalValue<string>;\n}\n\nexport interface DeviceSignals {\n    cpuCount: SignalValue<number>;\n    memory: SignalValue<number>;\n    platform: SignalValue<string>;\n    screenResolution: ScreenResolutionSignal;\n    multimediaDevices: MultimediaDevicesSignal;\n    mediaQueries: MediaQueriesSignal;\n}\n\nexport interface BrowserSignals {\n    userAgent: SignalValue<string>;\n    features: BrowserFeaturesSignal;\n    plugins: PluginsSignal;\n    extensions: BrowserExtensionsSignal;\n    highEntropyValues: HighEntropyValuesSignal;\n    etsl: SignalValue<number>;\n    maths: SignalValue<string>;\n    toSourceError: ToSourceErrorSignal;\n}\n\nexport interface GraphicsSignals {\n    webGL: WebGLSignal;\n    webgpu: WebGPUSignal;\n    canvas: CanvasSignal;\n}\n\nexport interface LocaleSignals {\n    internationalization: InternationalizationSignal;\n    languages: LanguagesSignal;\n}\n\nexport interface ContextsSignals {\n    iframe: IframeSignal;\n    webWorker: WebWorkerSignal;\n}\n\nexport interface FingerprintSignals {\n    automation: AutomationSignals;\n    device: DeviceSignals;\n    browser: BrowserSignals;\n    graphics: GraphicsSignals;\n    codecs: MediaCodecsSignal;\n    locale: LocaleSignals;\n    contexts: ContextsSignals;\n}\n\nexport interface FastBotDetectionDetails {\n    headlessChromeScreenResolution: DetectionRuleResult;\n    hasWebdriver: DetectionRuleResult;\n    hasWebdriverWritable: DetectionRuleResult;\n    hasSeleniumProperty: DetectionRuleResult;\n    hasCDP: DetectionRuleResult;\n    hasPlaywright: DetectionRuleResult;\n    hasImpossibleDeviceMemory: DetectionRuleResult;\n    hasHighCPUCount: DetectionRuleResult;\n    hasMissingChromeObject: DetectionRuleResult;\n    hasWebdriverIframe: DetectionRuleResult;\n    hasWebdriverWorker: DetectionRuleResult;\n    hasMismatchWebGLInWorker: DetectionRuleResult;\n    hasMismatchPlatformIframe: DetectionRuleResult;\n    hasMismatchPlatformWorker: DetectionRuleResult;\n    hasSwiftshaderRenderer: DetectionRuleResult;\n    hasUTCTimezone: DetectionRuleResult;\n    hasMismatchLanguages: DetectionRuleResult;\n    hasInconsistentEtsl: DetectionRuleResult;\n    hasBotUserAgent: DetectionRuleResult;\n    hasGPUMismatch: DetectionRuleResult;\n    hasPlatformMismatch: DetectionRuleResult;\n}\nexport interface Fingerprint {\n    signals: FingerprintSignals;\n    fsid: string;\n    nonce: string;\n    time: SignalValue<number>;\n    url: string;\n    fastBotDetection: boolean;\n    fastBotDetectionDetails: FastBotDetectionDetails;\n}\n\nexport type DetectionSeverity = 'low' | 'medium' | 'high';\n\nexport interface DetectionRuleResult {\n    detected: boolean;\n    severity: DetectionSeverity;\n}\n\nexport interface DetectionRule {\n    name: string;\n    severity: DetectionSeverity;\n    test: (fingerprint: Fingerprint) => boolean;\n}\n\nexport interface CollectFingerprintOptions {\n    encrypt?: boolean;\n    timeout?: number;\n    skipWorker?: boolean;\n}\n\n"
  },
  {
    "path": "test/decrypt.js",
    "content": "/**\n * Server-side decryption helper for tests\n * This mimics what a real server would do to decrypt fingerprints\n */\n\nconst TEST_KEY = 'dev-key';\n\n/**\n * Decrypts a string that was encrypted with XOR cipher\n * @param {string} ciphertext - Base64 encoded encrypted string\n * @param {string} key - Decryption key\n * @returns {string} Decrypted string\n */\nfunction decryptString(ciphertext, key = TEST_KEY) {\n  // Decode from base64\n  const binaryString = Buffer.from(ciphertext, 'base64').toString('binary');\n  const encrypted = new Uint8Array(binaryString.length);\n  for (let i = 0; i < binaryString.length; i++) {\n    encrypted[i] = binaryString.charCodeAt(i);\n  }\n\n  const keyBytes = Buffer.from(key, 'utf8');\n  const decrypted = new Uint8Array(encrypted.length);\n\n  // XOR is symmetric\n  for (let i = 0; i < encrypted.length; i++) {\n    decrypted[i] = encrypted[i] ^ keyBytes[i % keyBytes.length];\n  }\n\n  return Buffer.from(decrypted).toString('utf8');\n}\n\n/**\n * Decrypts and parses a fingerprint payload\n * @param {string} encryptedFingerprint - Base64 encoded encrypted fingerprint JSON\n * @param {string} key - Decryption key\n * @returns {object} Parsed fingerprint object\n */\nfunction decryptFingerprint(encryptedFingerprint, key = TEST_KEY) {\n  const decryptedJson = decryptString(encryptedFingerprint, key);\n  // The fingerprint is double-JSON-stringified in the code (JSON.stringify(JSON.stringify(...)))\n  // So we need to parse twice\n  const parsed = JSON.parse(decryptedJson);\n  // If it's still a string, parse again\n  if (typeof parsed === 'string') {\n    return JSON.parse(parsed);\n  }\n  return parsed;\n}\n\nmodule.exports = {\n  decryptString,\n  decryptFingerprint,\n  TEST_KEY,\n};\n"
  },
  {
    "path": "test/detection/README.md",
    "content": "# Detection Quality Tests\n\nThese scripts are **not** part of the CI/CD pipeline. They are manual tools for evaluating how well fpscanner detects various automation frameworks.\n\nEach script navigates to the local dev page (`http://localhost:3000/test/dev-source.html`), waits for the fingerprint to be collected, then prints the `fastBotDetectionDetails` object along with a summary of which detections were triggered.\n\n## Prerequisites\n\nStart the Vite dev server in the project root before running any of the scripts:\n\n```bash\nnpm run dev\n```\n\n---\n\n## Node.js tests\n\nLocated in `nodejs/`. Two variants:\n\n| Script | Framework | Engine | Evasion |\n|---|---|---|---|\n| `puppeteer-headless.js` | Puppeteer | Chromium | None |\n| `puppeteer-stealth.js` | Puppeteer + stealth plugin | Chromium | Yes |\n| `playwright-chromium-headless.js` | Playwright | Chromium | None |\n| `playwright-firefox-headless.js` | Playwright | Firefox | None |\n| `playwright-webkit-headless.js` | Playwright | WebKit | None |\n| `playwright-iphone-headless.js` | Playwright | Chromium | None (iPhone 15 emulation) |\n| `playwright-android-headless.js` | Playwright | Chromium | None (Pixel 7 emulation) |\n\n### Setup\n\n```bash\ncd test/detection/nodejs\nnpm install\nnpx playwright install chromium firefox webkit   # download browser binaries\n```\n\n### Run\n\n```bash\nnode puppeteer-headless.js\nnode puppeteer-stealth.js\nnode playwright-chromium-headless.js\nnode playwright-firefox-headless.js\nnode playwright-webkit-headless.js\nnode playwright-iphone-headless.js\nnode playwright-android-headless.js\n```\n\nOr via npm scripts:\n\n```bash\nnpm run test:headless\nnpm run test:stealth\nnpm run test:chromium\nnpm run test:firefox\nnpm run test:webkit\nnpm run test:iphone\nnpm run test:android\n```\n\n---\n\n## Python tests\n\nLocated in `python/`. Two variants:\n\n| Script | Framework | Evasion |\n|---|---|---|\n| `selenium_headless_test.py` | Selenium + headless Chrome | None |\n| `undetected_chromedriver_test.py` | undetected-chromedriver | Yes (Chromium) |\n| `camoufox_test.py` | Camoufox (patched Firefox) | Yes (Firefox, C++ level) |\n\n### Setup\n\n```bash\ncd test/detection/python\npip install -r requirements.txt\npython -m camoufox fetch   # one-time download of the Camoufox browser binary\n```\n\n### Run\n\n```bash\n# Plain headless Chrome via Selenium (expect many detections)\npython selenium_headless_test.py\n\n# With undetected-chromedriver patches (fewer detections expected)\npython undetected_chromedriver_test.py\n\n# Camoufox — patched Firefox, C++-level fingerprint spoofing via Playwright\npython camoufox_test.py\n```\n"
  },
  {
    "path": "test/detection/nodejs/package.json",
    "content": "{\n  \"name\": \"fpscanner-detection-tests-nodejs\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Detection quality tests for fpscanner using Node.js automation tools\",\n  \"private\": true,\n  \"scripts\": {\n    \"test:headless\": \"node puppeteer-headless.js\",\n    \"test:stealth\": \"node puppeteer-stealth.js\",\n    \"test:chromium\": \"node playwright-chromium-headless.js\",\n    \"test:firefox\": \"node playwright-firefox-headless.js\",\n    \"test:webkit\": \"node playwright-webkit-headless.js\",\n    \"test:iphone\": \"node playwright-iphone-headless.js\",\n    \"test:android\": \"node playwright-android-headless.js\"\n  },\n  \"dependencies\": {\n    \"playwright\": \"^1.50.0\",\n    \"puppeteer\": \"^22.0.0\",\n    \"puppeteer-extra\": \"^3.3.6\",\n    \"puppeteer-extra-plugin-stealth\": \"^2.11.2\"\n  }\n}\n"
  },
  {
    "path": "test/detection/nodejs/playwright-android-headless.js",
    "content": "/**\n * Detection test: Playwright + Chromium + Pixel 7 (Android) device emulation (headless)\n *\n * Uses Playwright's built-in device descriptor for Pixel 7, which sets the\n * correct viewport, userAgent, deviceScaleFactor, and touch capabilities.\n *\n * Prerequisites:\n *   npm install  (inside test/detection/nodejs/)\n *   npx playwright install chromium\n *   npm run dev  (in the project root, to start the Vite server on port 3000)\n *\n * Run:\n *   node playwright-android-headless.js\n */\n\nconst { chromium, devices } = require('playwright');\n\nconst DEVICE = devices['Pixel 7'];\nconst TARGET_URL = 'http://localhost:3000/test/dev-source.html';\nconst WAIT_TIMEOUT_MS = 15000;\n\n(async () => {\n    console.log(`[playwright-android] Launching headless Chromium emulating Pixel 7...`);\n    console.log(`[playwright-android] Viewport: ${DEVICE.viewport.width}x${DEVICE.viewport.height}, dpr: ${DEVICE.deviceScaleFactor}`);\n\n    const browser = await chromium.launch({ headless: true });\n    const context = await browser.newContext({ ...DEVICE });\n    const page = await context.newPage();\n\n    console.log(`[playwright-android] Navigating to ${TARGET_URL}`);\n    await page.goto(TARGET_URL);\n\n    console.log('[playwright-android] Waiting for fingerprint result...');\n    await page.waitForFunction(() => window.result !== undefined, { timeout: WAIT_TIMEOUT_MS });\n\n    const fastBotDetectionDetails = await page.evaluate(() => window.result.fastBotDetectionDetails);\n\n    console.log('\\n=== fastBotDetectionDetails ===');\n    console.log(JSON.stringify(fastBotDetectionDetails, null, 2));\n\n    const triggered = Object.entries(fastBotDetectionDetails)\n        .filter(([, v]) => v.detected)\n        .map(([k]) => k);\n\n    console.log(`\\n=== Triggered detections (${triggered.length}) ===`);\n    if (triggered.length === 0) {\n        console.log('None');\n    } else {\n        triggered.forEach(name => console.log(` • ${name}`));\n    }\n\n    await browser.close();\n    console.log('\\n[playwright-android] Done.');\n})();\n"
  },
  {
    "path": "test/detection/nodejs/playwright-chromium-headless.js",
    "content": "/**\n * Detection test: Playwright + Chromium headless (no evasion)\n *\n * Prerequisites:\n *   npm install  (inside test/detection/nodejs/)\n *   npx playwright install chromium\n *   npm run dev  (in the project root, to start the Vite server on port 3000)\n *\n * Run:\n *   node playwright-chromium-headless.js\n */\n\nconst { chromium } = require('playwright');\n\nconst TARGET_URL = 'http://localhost:3000/test/dev-source.html';\nconst WAIT_TIMEOUT_MS = 15000;\n\n(async () => {\n    console.log('[playwright-chromium] Launching headless Chromium...');\n\n    const browser = await chromium.launch({ headless: true });\n    const page = await browser.newPage();\n\n    console.log(`[playwright-chromium] Navigating to ${TARGET_URL}`);\n    await page.goto(TARGET_URL);\n\n    console.log('[playwright-chromium] Waiting for fingerprint result...');\n    await page.waitForFunction(() => window.result !== undefined, { timeout: WAIT_TIMEOUT_MS });\n\n    const fastBotDetectionDetails = await page.evaluate(() => window.result.fastBotDetectionDetails);\n\n    console.log('\\n=== fastBotDetectionDetails ===');\n    console.log(JSON.stringify(fastBotDetectionDetails, null, 2));\n\n    const triggered = Object.entries(fastBotDetectionDetails)\n        .filter(([, v]) => v.detected)\n        .map(([k]) => k);\n\n    console.log(`\\n=== Triggered detections (${triggered.length}) ===`);\n    if (triggered.length === 0) {\n        console.log('None');\n    } else {\n        triggered.forEach(name => console.log(` • ${name}`));\n    }\n\n    await browser.close();\n    console.log('\\n[playwright-chromium] Done.');\n})();\n"
  },
  {
    "path": "test/detection/nodejs/playwright-firefox-headless.js",
    "content": "/**\n * Detection test: Playwright + Firefox headless (no evasion)\n *\n * Prerequisites:\n *   npm install  (inside test/detection/nodejs/)\n *   npx playwright install firefox\n *   npm run dev  (in the project root, to start the Vite server on port 3000)\n *\n * Run:\n *   node playwright-firefox-headless.js\n */\n\nconst { firefox } = require('playwright');\n\nconst TARGET_URL = 'http://localhost:3000/test/dev-source.html';\nconst WAIT_TIMEOUT_MS = 15000;\n\n(async () => {\n    console.log('[playwright-firefox] Launching headless Firefox...');\n\n    const browser = await firefox.launch({ headless: true });\n    const page = await browser.newPage();\n\n    console.log(`[playwright-firefox] Navigating to ${TARGET_URL}`);\n    await page.goto(TARGET_URL);\n\n    console.log('[playwright-firefox] Waiting for fingerprint result...');\n    await page.waitForFunction(() => window.result !== undefined, { timeout: WAIT_TIMEOUT_MS });\n\n    const fastBotDetectionDetails = await page.evaluate(() => window.result.fastBotDetectionDetails);\n\n    console.log('\\n=== fastBotDetectionDetails ===');\n    console.log(JSON.stringify(fastBotDetectionDetails, null, 2));\n\n    const triggered = Object.entries(fastBotDetectionDetails)\n        .filter(([, v]) => v.detected)\n        .map(([k]) => k);\n\n    console.log(`\\n=== Triggered detections (${triggered.length}) ===`);\n    if (triggered.length === 0) {\n        console.log('None');\n    } else {\n        triggered.forEach(name => console.log(` • ${name}`));\n    }\n\n    await browser.close();\n    console.log('\\n[playwright-firefox] Done.');\n})();\n"
  },
  {
    "path": "test/detection/nodejs/playwright-iphone-headless.js",
    "content": "/**\n * Detection test: Playwright + Chromium + iPhone 15 device emulation (headless)\n *\n * Uses Playwright's built-in device descriptor for iPhone 15, which sets the\n * correct viewport, userAgent, deviceScaleFactor, and touch capabilities.\n *\n * Prerequisites:\n *   npm install  (inside test/detection/nodejs/)\n *   npx playwright install chromium\n *   npm run dev  (in the project root, to start the Vite server on port 3000)\n *\n * Run:\n *   node playwright-iphone-headless.js\n */\n\nconst { chromium, devices } = require('playwright');\n\nconst DEVICE = devices['iPhone 15'];\nconst TARGET_URL = 'http://localhost:3000/test/dev-source.html';\nconst WAIT_TIMEOUT_MS = 15000;\n\n(async () => {\n    console.log(`[playwright-iphone] Launching headless Chromium emulating \"${DEVICE.userAgent.split(' ').slice(-1)[0]}\"...`);\n    console.log(`[playwright-iphone] Viewport: ${DEVICE.viewport.width}x${DEVICE.viewport.height}, dpr: ${DEVICE.deviceScaleFactor}`);\n\n    const browser = await chromium.launch({ headless: true });\n    const context = await browser.newContext({ ...DEVICE });\n    const page = await context.newPage();\n\n    console.log(`[playwright-iphone] Navigating to ${TARGET_URL}`);\n    await page.goto(TARGET_URL);\n\n    console.log('[playwright-iphone] Waiting for fingerprint result...');\n    await page.waitForFunction(() => window.result !== undefined, { timeout: WAIT_TIMEOUT_MS });\n\n    const fastBotDetectionDetails = await page.evaluate(() => window.result.fastBotDetectionDetails);\n\n    console.log('\\n=== fastBotDetectionDetails ===');\n    console.log(JSON.stringify(fastBotDetectionDetails, null, 2));\n\n    const triggered = Object.entries(fastBotDetectionDetails)\n        .filter(([, v]) => v.detected)\n        .map(([k]) => k);\n\n    console.log(`\\n=== Triggered detections (${triggered.length}) ===`);\n    if (triggered.length === 0) {\n        console.log('None');\n    } else {\n        triggered.forEach(name => console.log(` • ${name}`));\n    }\n\n    await browser.close();\n    console.log('\\n[playwright-iphone] Done.');\n})();\n"
  },
  {
    "path": "test/detection/nodejs/playwright-webkit-headless.js",
    "content": "/**\n * Detection test: Playwright + WebKit headless (no evasion)\n *\n * Prerequisites:\n *   npm install  (inside test/detection/nodejs/)\n *   npx playwright install webkit\n *   npm run dev  (in the project root, to start the Vite server on port 3000)\n *\n * Run:\n *   node playwright-webkit-headless.js\n */\n\nconst { webkit } = require('playwright');\n\nconst TARGET_URL = 'http://localhost:3000/test/dev-source.html';\nconst WAIT_TIMEOUT_MS = 15000;\n\n(async () => {\n    console.log('[playwright-webkit] Launching headless WebKit...');\n\n    const browser = await webkit.launch({ headless: true });\n    const page = await browser.newPage();\n\n    console.log(`[playwright-webkit] Navigating to ${TARGET_URL}`);\n    await page.goto(TARGET_URL);\n\n    console.log('[playwright-webkit] Waiting for fingerprint result...');\n    await page.waitForFunction(() => window.result !== undefined, { timeout: WAIT_TIMEOUT_MS });\n\n    const fastBotDetectionDetails = await page.evaluate(() => window.result.fastBotDetectionDetails);\n\n    console.log('\\n=== fastBotDetectionDetails ===');\n    console.log(JSON.stringify(fastBotDetectionDetails, null, 2));\n\n    const triggered = Object.entries(fastBotDetectionDetails)\n        .filter(([, v]) => v.detected)\n        .map(([k]) => k);\n\n    console.log(`\\n=== Triggered detections (${triggered.length}) ===`);\n    if (triggered.length === 0) {\n        console.log('None');\n    } else {\n        triggered.forEach(name => console.log(` • ${name}`));\n    }\n\n    await browser.close();\n    console.log('\\n[playwright-webkit] Done.');\n})();\n"
  },
  {
    "path": "test/detection/nodejs/puppeteer-headless.js",
    "content": "/**\n * Detection test: Puppeteer with headless Chrome (no evasion)\n *\n * Prerequisites:\n *   npm install  (inside test/detection/nodejs/)\n *   npm run dev  (in the project root, to start the Vite server on port 3000)\n *\n * Run:\n *   node puppeteer-headless.js\n */\n\nconst puppeteer = require('puppeteer');\n\nconst TARGET_URL = 'http://localhost:3000/test/dev-source.html';\nconst WAIT_TIMEOUT_MS = 15000;\n\n(async () => {\n    console.log('[puppeteer-headless] Launching headless Chrome...');\n\n    const browser = await puppeteer.launch({\n        headless: true,\n        args: ['--no-sandbox', '--disable-setuid-sandbox'],\n    });\n\n    const page = await browser.newPage();\n\n    console.log(`[puppeteer-headless] Navigating to ${TARGET_URL}`);\n    await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded' });\n\n    console.log('[puppeteer-headless] Waiting for fingerprint result...');\n    await page.waitForFunction(\n        () => window.result !== undefined,\n        { timeout: WAIT_TIMEOUT_MS }\n    );\n\n    const fastBotDetectionDetails = await page.evaluate(\n        () => window.result.fastBotDetectionDetails\n    );\n\n    console.log('\\n=== fastBotDetectionDetails ===');\n    console.log(JSON.stringify(fastBotDetectionDetails, null, 2));\n\n    const triggered = Object.entries(fastBotDetectionDetails)\n        .filter(([, v]) => v.detected)\n        .map(([k]) => k);\n\n    console.log(`\\n=== Triggered detections (${triggered.length}) ===`);\n    if (triggered.length === 0) {\n        console.log('None');\n    } else {\n        triggered.forEach(name => console.log(` • ${name}`));\n    }\n\n    await browser.close();\n    console.log('\\n[puppeteer-headless] Done.');\n})();\n"
  },
  {
    "path": "test/detection/nodejs/puppeteer-stealth.js",
    "content": "/**\n * Detection test: Puppeteer with puppeteer-extra-plugin-stealth\n *\n * The stealth plugin applies a collection of evasion techniques designed to\n * make headless Chrome look more like a real browser.\n *\n * Prerequisites:\n *   npm install  (inside test/detection/nodejs/)\n *   npm run dev  (in the project root, to start the Vite server on port 3000)\n *\n * Run:\n *   node puppeteer-stealth.js\n */\n\nconst puppeteerExtra = require('puppeteer-extra');\nconst StealthPlugin = require('puppeteer-extra-plugin-stealth');\n\npuppeteerExtra.use(StealthPlugin());\n\nconst TARGET_URL = 'http://localhost:3000/test/dev-source.html';\nconst WAIT_TIMEOUT_MS = 15000;\n\n(async () => {\n    console.log('[puppeteer-stealth] Launching headless Chrome with stealth plugin...');\n\n    const browser = await puppeteerExtra.launch({\n        headless: true,\n        args: ['--no-sandbox', '--disable-setuid-sandbox'],\n    });\n\n    const page = await browser.newPage();\n\n    console.log(`[puppeteer-stealth] Navigating to ${TARGET_URL}`);\n    await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded' });\n\n    console.log('[puppeteer-stealth] Waiting for fingerprint result...');\n    await page.waitForFunction(\n        () => window.result !== undefined,\n        { timeout: WAIT_TIMEOUT_MS }\n    );\n\n    const fastBotDetectionDetails = await page.evaluate(\n        () => window.result.fastBotDetectionDetails\n    );\n\n    console.log('\\n=== fastBotDetectionDetails ===');\n    console.log(JSON.stringify(fastBotDetectionDetails, null, 2));\n\n    const triggered = Object.entries(fastBotDetectionDetails)\n        .filter(([, v]) => v.detected)\n        .map(([k]) => k);\n\n    console.log(`\\n=== Triggered detections (${triggered.length}) ===`);\n    if (triggered.length === 0) {\n        console.log('None');\n    } else {\n        triggered.forEach(name => console.log(` • ${name}`));\n    }\n\n    await browser.close();\n    console.log('\\n[puppeteer-stealth] Done.');\n})();\n"
  },
  {
    "path": "test/detection/python/camoufox_test.py",
    "content": "\"\"\"\nDetection test: Camoufox (https://github.com/daijro/camoufox)\n\nCamoufox is a patched Firefox build that intercepts fingerprint calls at the\nC++ level, making spoofing undetectable through JavaScript inspection. It uses\nPlaywright's sync API under the hood.\n\nPrerequisites:\n    pip install -r requirements.txt\n    python -m camoufox fetch          # one-time download of the patched browser\n    # Start the Vite dev server in the project root:\n    npm run dev\n\nRun:\n    python camoufox_test.py\n\"\"\"\n\nfrom camoufox.sync_api import Camoufox\n\nTARGET_URL = \"http://localhost:3000/test/dev-source.html\"\nWAIT_TIMEOUT_MS = 15000\n\n\ndef main():\n    print(\"[camoufox] Launching Camoufox (headless Firefox)...\")\n\n    with Camoufox(headless=True) as browser:\n        page = browser.new_page()\n\n        print(f\"[camoufox] Navigating to {TARGET_URL}\")\n        page.goto(TARGET_URL)\n\n        print(\"[camoufox] Waiting for fingerprint result...\")\n        page.wait_for_function(\"() => window.result !== undefined\", timeout=WAIT_TIMEOUT_MS)\n\n        fast_bot_detection_details = page.evaluate(\"() => window.result.fastBotDetectionDetails\")\n\n        import json\n        print(\"\\n=== fastBotDetectionDetails ===\")\n        print(json.dumps(fast_bot_detection_details, indent=2))\n\n        triggered = [\n            name\n            for name, value in fast_bot_detection_details.items()\n            if value.get(\"detected\")\n        ]\n\n        print(f\"\\n=== Triggered detections ({len(triggered)}) ===\")\n        if not triggered:\n            print(\"None\")\n        else:\n            for name in triggered:\n                print(f\" • {name}\")\n\n    print(\"\\n[camoufox] Done.\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "test/detection/python/requirements.txt",
    "content": "setuptools\ncamoufox\nselenium\nundetected-chromedriver\n"
  },
  {
    "path": "test/detection/python/selenium_headless_test.py",
    "content": "\"\"\"\nDetection test: Selenium + headless Chrome (no evasion)\n\nPrerequisites:\n    pip install -r requirements.txt\n    # Start the Vite dev server in the project root:\n    npm run dev\n\nRun:\n    python selenium_headless_test.py\n\"\"\"\n\nimport json\nimport time\n\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\nfrom selenium.webdriver.chrome.service import Service\nfrom selenium.webdriver.support.ui import WebDriverWait\n\nTARGET_URL = \"http://localhost:3000/test/dev-source.html\"\nWAIT_TIMEOUT_SECONDS = 15\nPOLL_INTERVAL_SECONDS = 0.5\n\n\ndef wait_for_result(driver, timeout=WAIT_TIMEOUT_SECONDS):\n    \"\"\"Poll until window.result is populated by the fingerprint scanner.\"\"\"\n    deadline = time.time() + timeout\n    while time.time() < deadline:\n        ready = driver.execute_script(\"return window.result !== undefined;\")\n        if ready:\n            return\n        time.sleep(POLL_INTERVAL_SECONDS)\n    raise TimeoutError(\n        f\"window.result was not set within {timeout}s. \"\n        \"Make sure the Vite dev server is running on port 3000.\"\n    )\n\n\ndef main():\n    print(\"[selenium-headless] Launching headless Chrome...\")\n\n    options = Options()\n    options.add_argument(\"--headless=new\")\n    options.add_argument(\"--no-sandbox\")\n    options.add_argument(\"--disable-setuid-sandbox\")\n\n    driver = webdriver.Chrome(options=options)\n\n    try:\n        print(f\"[selenium-headless] Navigating to {TARGET_URL}\")\n        driver.get(TARGET_URL)\n\n        print(\"[selenium-headless] Waiting for fingerprint result...\")\n        wait_for_result(driver)\n\n        fast_bot_detection_details = driver.execute_script(\n            \"return window.result.fastBotDetectionDetails;\"\n        )\n\n        print(\"\\n=== fastBotDetectionDetails ===\")\n        print(json.dumps(fast_bot_detection_details, indent=2))\n\n        triggered = [\n            name\n            for name, value in fast_bot_detection_details.items()\n            if value.get(\"detected\")\n        ]\n\n        print(f\"\\n=== Triggered detections ({len(triggered)}) ===\")\n        if not triggered:\n            print(\"None\")\n        else:\n            for name in triggered:\n                print(f\" • {name}\")\n\n    finally:\n        driver.quit()\n        print(\"\\n[selenium-headless] Done.\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "test/detection/python/undetected_chromedriver_test.py",
    "content": "\"\"\"\nDetection test: undetected-chromedriver\n\nundetected-chromedriver patches the ChromeDriver binary to avoid triggering\ncommon bot-detection heuristics (navigator.webdriver removal, CDP fingerprint\npatches, etc.).\n\nPrerequisites:\n    pip install -r requirements.txt\n    # Start the Vite dev server in the project root:\n    npm run dev\n\nRun:\n    python undetected_chromedriver_test.py\n\"\"\"\n\nimport sys\n\n# Python 3.12 removed distutils from the stdlib; undetected-chromedriver still\n# depends on it. Inject the setuptools shim before the import so the module\n# resolver finds distutils.version without touching the installed package.\ntry:\n    import distutils  # noqa: F401\nexcept ImportError:\n    import setuptools  # noqa: F401 – registers the distutils meta-path finder\n    import setuptools._distutils as _distutils\n    import setuptools._distutils.version as _distutils_version\n    sys.modules.setdefault(\"distutils\", _distutils)\n    sys.modules.setdefault(\"distutils.version\", _distutils_version)\n\nimport json\nimport time\n\nimport undetected_chromedriver as uc\n\nTARGET_URL = \"http://localhost:3000/test/dev-source.html\"\nWAIT_TIMEOUT_SECONDS = 15\nPOLL_INTERVAL_SECONDS = 0.5\n\n\ndef wait_for_result(driver, timeout=WAIT_TIMEOUT_SECONDS):\n    \"\"\"Poll until window.result is populated by the fingerprint scanner.\"\"\"\n    deadline = time.time() + timeout\n    while time.time() < deadline:\n        ready = driver.execute_script(\"return window.result !== undefined;\")\n        if ready:\n            return\n        time.sleep(POLL_INTERVAL_SECONDS)\n    raise TimeoutError(\n        f\"window.result was not set within {timeout}s. \"\n        \"Make sure the Vite dev server is running on port 3000.\"\n    )\n\n\ndef main():\n    print(\"[undetected-chromedriver] Launching Chrome...\")\n\n    options = uc.ChromeOptions()\n    # Run headless so it matches the puppeteer examples\n    # options.add_argument(\"--headless=new\")\n\n    driver = uc.Chrome(options=options, use_subprocess=False)\n\n    try:\n        print(f\"[undetected-chromedriver] Navigating to {TARGET_URL}\")\n        driver.get(TARGET_URL)\n\n        print(\"[undetected-chromedriver] Waiting for fingerprint result...\")\n        wait_for_result(driver)\n\n        fast_bot_detection_details = driver.execute_script(\n            \"return window.result.fastBotDetectionDetails;\"\n        )\n\n        print(\"\\n=== fastBotDetectionDetails ===\")\n        print(json.dumps(fast_bot_detection_details, indent=2))\n\n        triggered = [\n            name\n            for name, value in fast_bot_detection_details.items()\n            if value.get(\"detected\")\n        ]\n\n        print(f\"\\n=== Triggered detections ({len(triggered)}) ===\")\n        if not triggered:\n            print(\"None\")\n        else:\n            for name in triggered:\n                print(f\" • {name}\")\n\n    finally:\n        driver.quit()\n        print(\"\\n[undetected-chromedriver] Done.\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "test/dev-dist.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Fingerprint Scanner Test (Obfuscated Build)</title>\n</head>\n<body>\n    <h1>Fingerprint Scanner Test (Obfuscated Build)</h1>\n    <p>This page uses the pre-built, obfuscated version from <code>dist/</code>.</p>\n    <p>Open the browser console to see the fingerprint results.</p>\n    <p><small>Build with: <code>npm run build:obfuscate</code> or <code>npm run build:prod</code></small></p>\n    \n    <script type=\"module\">\n        // Import from dist - the pre-built, potentially obfuscated version\n        import FingerprintScanner from '/dist/fpScanner.es.js';\n        \n        async function testFingerprint() {\n            const scanner = new FingerprintScanner();\n            const result = await scanner.collectFingerprint({ encrypt: false });\n            console.log('Fingerprint result:', result);\n        }\n        \n        testFingerprint().catch(console.error);\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "test/dev-source.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Fingerprint Scanner Test</title>\n    <style>\n        body {\n            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n            max-width: 1200px;\n            margin: 0 auto;\n            padding: 20px;\n            background: #f5f5f5;\n        }\n        h1 {\n            color: #333;\n        }\n        #fingerprint-container {\n            background: #1e1e1e;\n            border-radius: 8px;\n            padding: 20px;\n            overflow-x: auto;\n            box-shadow: 0 2px 8px rgba(0,0,0,0.1);\n        }\n        pre {\n            margin: 0;\n            color: #d4d4d4;\n            font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;\n            font-size: 13px;\n            line-height: 1.5;\n        }\n        .json-key {\n            color: #9cdcfe;\n        }\n        .json-string {\n            color: #ce9178;\n        }\n        .json-number {\n            color: #b5cea8;\n        }\n        .json-boolean {\n            color: #569cd6;\n        }\n        .json-null {\n            color: #569cd6;\n        }\n        .loading {\n            text-align: center;\n            padding: 40px;\n            color: #666;\n        }\n    </style>\n</head>\n<body>\n    <h1>Fingerprint Scanner Test</h1>\n    <p>Open the browser console to see the fingerprint results.</p>\n    \n    <div id=\"fingerprint-container\">\n        <pre class=\"loading\">Loading fingerprint...</pre>\n    </div>\n    \n    <script type=\"module\">\n        // Import from source - Vite transpiles TypeScript on the fly\n        import FingerprintScanner from '../src/index.ts';\n        \n        function syntaxHighlight(json) {\n            if (typeof json != 'string') {\n                json = JSON.stringify(json, null, 2);\n            }\n            json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n            return json.replace(/(\"(\\\\u[a-zA-Z0-9]{4}|\\\\[^u]|[^\\\\\"])*\"(\\s*:)?|\\b(true|false|null)\\b|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?)/g, function (match) {\n                let cls = 'json-number';\n                if (/^\"/.test(match)) {\n                    if (/:$/.test(match)) {\n                        cls = 'json-key';\n                    } else {\n                        cls = 'json-string';\n                    }\n                } else if (/true|false/.test(match)) {\n                    cls = 'json-boolean';\n                } else if (/null/.test(match)) {\n                    cls = 'json-null';\n                }\n                return '<span class=\"' + cls + '\">' + match + '</span>';\n            });\n        }\n        \n        async function testFingerprint() {\n            const scanner = new FingerprintScanner();\n            const result = await scanner.collectFingerprint({ encrypt: false });\n            console.log('Fingerprint result:', result);\n            window.result = result;\n            \n            // Display in the page with syntax highlighting\n            const container = document.getElementById('fingerprint-container');\n            const highlighted = syntaxHighlight(result);\n            container.innerHTML = '<pre>' + highlighted + '</pre>';\n        }\n        \n        setTimeout(() => {\n            testFingerprint().catch(console.error);\n        }, 1000);\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "test/fingerprint.spec.ts",
    "content": "import { test, expect, Page } from '@playwright/test';\nimport { decryptFingerprint } from './decrypt.js';\n\nlet fingerprint: any;\n\ntest.describe('FPScanner Obfuscated Build', () => {\n  test.beforeAll(async ({ browser }) => {\n    const page = await browser.newPage();\n    \n    // Navigate to the test page\n    await page.goto('http://localhost:3333/test/test-page.html');\n    \n    // Wait for the fingerprint to be collected\n    await page.waitForFunction(() => (window as any).__FINGERPRINT_READY__ === true, {\n      timeout: 10000,\n    });\n    \n    // Check if there was an error\n    const error = await page.evaluate(() => (window as any).__FINGERPRINT_ERROR__);\n    if (error) {\n      throw new Error(`Fingerprint collection failed: ${error}`);\n    }\n    \n    // Get the encrypted fingerprint from the browser\n    const encryptedFingerprint = await page.evaluate(() => (window as any).__ENCRYPTED_FINGERPRINT__);\n    \n    if (!encryptedFingerprint) {\n      throw new Error('Encrypted fingerprint is empty');\n    }\n    \n    // Decrypt the fingerprint (server-side simulation)\n    fingerprint = decryptFingerprint(encryptedFingerprint);\n    \n    await page.close();\n  });\n\n  // Structure validations\n  test('fingerprint should have signals property', () => {\n    expect(fingerprint).toHaveProperty('signals');\n  });\n\n  test('fingerprint should have fsid property', () => {\n    expect(fingerprint).toHaveProperty('fsid');\n  });\n\n  test('fingerprint should have nonce property', () => {\n    expect(fingerprint).toHaveProperty('nonce');\n  });\n\n  test('fingerprint should have time property', () => {\n    expect(fingerprint).toHaveProperty('time');\n  });\n\n  // FSID format validation\n  test('fsid should be a non-empty string starting with FS1_', () => {\n    expect(typeof fingerprint.fsid).toBe('string');\n    expect(fingerprint.fsid.length).toBeGreaterThan(0);\n    expect(fingerprint.fsid).toMatch(/^FS1_/);\n  });\n\n  // Signal validations - using nested structure\n  test('device.memory should be a number greater than 0 or NA', () => {\n    const memory = fingerprint.signals.device.memory;\n    if (memory === 'NA') {\n      // Firefox and WebKit don't support navigator.deviceMemory\n      expect(memory).toBe('NA');\n    } else {\n      expect(typeof memory).toBe('number');\n      expect(memory).toBeGreaterThan(0);\n    }\n  });\n\n  test('device.cpuCount should be a number greater than 0', () => {\n    const cpuCount = fingerprint.signals.device.cpuCount;\n    expect(typeof cpuCount).toBe('number');\n    expect(cpuCount).toBeGreaterThan(0);\n  });\n\n  test('browser.userAgent should be a non-empty string', () => {\n    expect(fingerprint.signals.browser).toHaveProperty('userAgent');\n    expect(typeof fingerprint.signals.browser.userAgent).toBe('string');\n    expect(fingerprint.signals.browser.userAgent.length).toBeGreaterThan(0);\n  });\n\n  test('device.platform should be a non-empty string', () => {\n    expect(fingerprint.signals.device).toHaveProperty('platform');\n    expect(typeof fingerprint.signals.device.platform).toBe('string');\n    expect(fingerprint.signals.device.platform.length).toBeGreaterThan(0);\n  });\n\n  test('automation.webdriver should be a boolean', () => {\n    expect(typeof fingerprint.signals.automation.webdriver).toBe('boolean');\n  });\n\n  test('device.screenResolution should have valid dimensions', () => {\n    const screen = fingerprint.signals.device.screenResolution;\n    expect(screen).toHaveProperty('width');\n    expect(screen).toHaveProperty('height');\n    expect(typeof screen.width).toBe('number');\n    expect(typeof screen.height).toBe('number');\n    expect(screen.width).toBeGreaterThan(0);\n    expect(screen.height).toBeGreaterThan(0);\n  });\n\n  test('locale.languages should have language property', () => {\n    expect(fingerprint.signals.locale.languages).toHaveProperty('language');\n    expect(typeof fingerprint.signals.locale.languages.language).toBe('string');\n  });\n\n  test('graphics.webGL should have vendor and renderer', () => {\n    const webGL = fingerprint.signals.graphics.webGL;\n    expect(typeof webGL.vendor).toBe('string');\n    expect(typeof webGL.renderer).toBe('string');\n    expect(webGL.vendor.length).toBeGreaterThan(0);\n    expect(webGL.renderer.length).toBeGreaterThan(0);\n  });\n\n  // Additional nested structure tests\n  test('automation signals should exist', () => {\n    expect(fingerprint.signals).toHaveProperty('automation');\n    expect(typeof fingerprint.signals.automation.cdp).toBe('boolean');\n    expect(typeof fingerprint.signals.automation.selenium).toBe('boolean');\n    expect(typeof fingerprint.signals.automation.playwright).toBe('boolean');\n  });\n\n  test('codecs signals should exist', () => {\n    expect(fingerprint.signals).toHaveProperty('codecs');\n    expect(typeof fingerprint.signals.codecs.hasMediaSource).toBe('boolean');\n  });\n\n  test('contexts signals should exist', () => {\n    expect(fingerprint.signals).toHaveProperty('contexts');\n    expect(fingerprint.signals.contexts).toHaveProperty('iframe');\n    expect(fingerprint.signals.contexts).toHaveProperty('webWorker');\n  });\n});\n"
  },
  {
    "path": "test/server.js",
    "content": "const http = require('http');\nconst fs = require('fs');\nconst path = require('path');\n\nconst PORT = 3333;\nconst ROOT = path.resolve(__dirname, '..');\n\nconst MIME_TYPES = {\n  '.html': 'text/html',\n  '.js': 'application/javascript',\n  '.css': 'text/css',\n  '.json': 'application/json',\n};\n\nconst server = http.createServer((req, res) => {\n  let filePath = path.join(ROOT, req.url === '/' ? 'test/test-page.html' : req.url);\n  \n  // Security: prevent directory traversal\n  if (!filePath.startsWith(ROOT)) {\n    res.writeHead(403);\n    res.end('Forbidden');\n    return;\n  }\n  \n  const ext = path.extname(filePath);\n  const contentType = MIME_TYPES[ext] || 'text/plain';\n  \n  fs.readFile(filePath, (err, content) => {\n    if (err) {\n      if (err.code === 'ENOENT') {\n        res.writeHead(404);\n        res.end(`Not found: ${req.url}`);\n      } else {\n        res.writeHead(500);\n        res.end(`Server error: ${err.message}`);\n      }\n      return;\n    }\n    \n    res.writeHead(200, { 'Content-Type': contentType });\n    res.end(content);\n  });\n});\n\nserver.listen(PORT, () => {\n  console.log(`Test server running at http://localhost:${PORT}`);\n});\n"
  },
  {
    "path": "test/test-page.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>FPScanner Test Page</title>\n</head>\n<body>\n    <h1>FPScanner Test Page</h1>\n    <p>This page is used for automated testing with Playwright.</p>\n    <div id=\"status\">Loading...</div>\n\n    <script type=\"module\">\n        import FingerprintScanner from '/dist/fpScanner.es.js';\n        \n        async function run() {\n            const statusEl = document.getElementById('status');\n            try {\n                const scanner = new FingerprintScanner();\n                const encryptedFingerprint = await scanner.collectFingerprint();\n                \n                // Store the encrypted fingerprint in a global variable for Playwright to access\n                window.__ENCRYPTED_FINGERPRINT__ = encryptedFingerprint;\n                window.__FINGERPRINT_READY__ = true;\n                \n                statusEl.textContent = 'Fingerprint collected successfully!';\n                console.log('Encrypted fingerprint collected');\n            } catch (error) {\n                window.__FINGERPRINT_ERROR__ = error.message;\n                window.__FINGERPRINT_READY__ = true;\n                statusEl.textContent = 'Error: ' + error.message;\n                console.error('Error collecting fingerprint:', error);\n            }\n        }\n        \n        run();\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES2020\", \"DOM\"],\n    \"moduleResolution\": \"node\",\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"sourceMap\": true,\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\", \"test\"]\n}\n\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import { defineConfig } from 'vite';\nimport { resolve } from 'path';\n\nexport default defineConfig({\n  define: {\n    // Inject encryption key from environment variable, fallback to sentinel for replacement\n    __FP_ENCRYPTION_KEY__: JSON.stringify(\n      process.env.FP_ENCRYPTION_KEY || '__DEFAULT_FPSCANNER_KEY__'\n    ),\n  },\n  server: {\n    port: 3000,\n    open: '/test/dev-source.html',\n  },\n  build: {\n    lib: {\n      entry: resolve(__dirname, 'src/index.ts'),\n      name: 'FingerprintScanner',\n      fileName: (format) => `fpScanner.${format}.js`,\n      formats: ['es', 'cjs'],\n    },\n    rollupOptions: {\n      output: {\n        exports: 'named',\n      },\n    },\n    outDir: 'dist',\n    sourcemap: false, // Disable source maps for production builds\n  },\n  test: {\n    globals: true,\n    environment: 'node',\n  },\n});\n\n"
  }
]