├── .editorconfig ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── codeql.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── examples ├── demo.html ├── suspect-score-examples.ts ├── suspect-score-test.html └── usage-examples.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── scripts └── publish.sh ├── src ├── __tests__ │ ├── fingerprint.test.ts │ ├── stability.test.ts │ └── utils.test.ts ├── collectors │ ├── advanced.ts │ ├── basic.ts │ └── canvas.ts ├── fingerprint.ts ├── index.ts ├── suspect-analyzer.ts ├── types.ts └── utils.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [*.json] 18 | indent_size = 2 19 | 20 | [*.{js,ts}] 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | extends: ["eslint:recommended", "@typescript-eslint/recommended"], 4 | parserOptions: { 5 | ecmaVersion: 2020, 6 | sourceType: "module", 7 | }, 8 | rules: { 9 | "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], 10 | "@typescript-eslint/explicit-function-return-type": "warn", 11 | "@typescript-eslint/no-explicit-any": "warn", 12 | "prefer-const": "error", 13 | "no-var": "error", 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Lorenzo-Coslado] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL Advanced" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | schedule: 20 | - cron: '34 23 * * 5' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners (GitHub.com only) 29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | permissions: 32 | # required for all workflows 33 | security-events: write 34 | 35 | # required to fetch internal or private CodeQL packs 36 | packages: read 37 | 38 | # only required for workflows in private repositories 39 | actions: read 40 | contents: read 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | include: 46 | - language: javascript-typescript 47 | build-mode: none 48 | # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' 49 | # Use `c-cpp` to analyze code written in C, C++ or both 50 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 51 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 52 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 53 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 54 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 55 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v4 59 | 60 | # Add any setup steps before running the `github/codeql-action/init` action. 61 | # This includes steps like installing compilers or runtimes (`actions/setup-node` 62 | # or others). This is typically only required for manual builds. 63 | # - name: Setup runtime (example) 64 | # uses: actions/setup-example@v1 65 | 66 | # Initializes the CodeQL tools for scanning. 67 | - name: Initialize CodeQL 68 | uses: github/codeql-action/init@v3 69 | with: 70 | languages: ${{ matrix.language }} 71 | build-mode: ${{ matrix.build-mode }} 72 | # If you wish to specify custom queries, you can do so here or in a config file. 73 | # By default, queries listed here will override any specified in a config file. 74 | # Prefix the list here with "+" to use these queries and those in the config file. 75 | 76 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 77 | # queries: security-extended,security-and-quality 78 | 79 | # If the analyze step fails for one of the languages you are analyzing with 80 | # "We were unable to automatically build your code", modify the matrix above 81 | # to set the build mode to "manual" for that language. Then modify this step 82 | # to build your code. 83 | # ℹ️ Command-line programs to run using the OS shell. 84 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 85 | - if: matrix.build-mode == 'manual' 86 | shell: bash 87 | run: | 88 | echo 'If you are using a "manual" build mode for one or more of the' \ 89 | 'languages you are analyzing, replace this with the commands to build' \ 90 | 'your code, for example:' 91 | echo ' make bootstrap' 92 | echo ' make release' 93 | exit 1 94 | 95 | - name: Perform CodeQL Analysis 96 | uses: github/codeql-action/analyze@v3 97 | with: 98 | category: "/language:${{matrix.language}}" 99 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: "Version to publish" 10 | required: true 11 | default: "patch" 12 | type: choice 13 | options: 14 | - patch 15 | - minor 16 | - major 17 | 18 | jobs: 19 | test: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: "18" 29 | cache: "npm" 30 | 31 | - name: Install dependencies 32 | run: npm ci 33 | 34 | - name: Run tests 35 | run: npm test 36 | 37 | - name: Build 38 | run: npm run build 39 | 40 | publish-npm: 41 | needs: test 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Checkout 45 | uses: actions/checkout@v4 46 | 47 | - name: Setup Node.js for NPM 48 | uses: actions/setup-node@v4 49 | with: 50 | node-version: "18" 51 | registry-url: "https://registry.npmjs.org" 52 | cache: "npm" 53 | 54 | - name: Install dependencies 55 | run: npm ci 56 | 57 | - name: Build 58 | run: npm run build 59 | 60 | - name: Update version (if manual trigger) 61 | if: github.event_name == 'workflow_dispatch' 62 | run: npm version ${{ github.event.inputs.version }} --no-git-tag-version 63 | 64 | - name: Create package.json for NPM (without scope) 65 | run: | 66 | cp package.json package-npm.json 67 | sed -i 's/"@lorenzo-coslado\/fingerprinter-js"/"fingerprinter-js"/' package-npm.json 68 | sed -i '/"publishConfig"/,+2d' package-npm.json 69 | 70 | - name: Publish to NPM 71 | run: npm publish package-npm.json 72 | env: 73 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 74 | 75 | publish-github: 76 | needs: test 77 | runs-on: ubuntu-latest 78 | steps: 79 | - name: Checkout 80 | uses: actions/checkout@v4 81 | 82 | - name: Setup Node.js for GitHub Packages 83 | uses: actions/setup-node@v4 84 | with: 85 | node-version: "18" 86 | registry-url: "https://npm.pkg.github.com" 87 | cache: "npm" 88 | 89 | - name: Install dependencies 90 | run: npm ci 91 | 92 | - name: Build 93 | run: npm run build 94 | 95 | - name: Update version (if manual trigger) 96 | if: github.event_name == 'workflow_dispatch' 97 | run: npm version ${{ github.event.inputs.version }} --no-git-tag-version 98 | 99 | - name: Publish to GitHub Packages 100 | run: npm publish 101 | env: 102 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pnpm-debug.log* 7 | 8 | # Build outputs 9 | dist/ 10 | build/ 11 | lib/ 12 | *.tsbuildinfo 13 | 14 | # Testing 15 | coverage/ 16 | .nyc_output/ 17 | junit.xml 18 | test-results.xml 19 | 20 | # Environment variables 21 | .env 22 | .env.local 23 | .env.development.local 24 | .env.test.local 25 | .env.production.local 26 | 27 | # IDE and editors 28 | .vscode/ 29 | .idea/ 30 | *.swp 31 | *.swo 32 | *~ 33 | 34 | # OS generated files 35 | .DS_Store 36 | .DS_Store? 37 | ._* 38 | .Spotlight-V100 39 | .Trashes 40 | ehthumbs.db 41 | Thumbs.db 42 | 43 | # Temporary files 44 | *.tmp 45 | *.temp 46 | .cache/ 47 | 48 | # Runtime data 49 | pids 50 | *.pid 51 | *.seed 52 | *.pid.lock 53 | 54 | # Publishing documentation (internal use only) 55 | PUBLISHING.md 56 | PUBLISHING-DUAL.md 57 | 58 | # NPM configuration examples 59 | .npmrc.example 60 | 61 | # Package manager files 62 | package-lock.json 63 | yarn.lock 64 | pnpm-lock.yaml 65 | 66 | # TypeScript 67 | *.tsbuildinfo 68 | 69 | # Rollup cache 70 | .rollup.cache/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Source files (only dist/ should be published) 2 | 3 | src/ 4 | examples/ 5 | scripts/ 6 | 7 | # Development files 8 | 9 | .github/ 10 | .vscode/ 11 | .idea/ 12 | 13 | # Configuration files 14 | 15 | .gitignore 16 | .eslintrc._ 17 | .prettierrc._ 18 | .editorconfig 19 | tsconfig.json 20 | jest.config.js 21 | rollup.config.js 22 | 23 | # Testing 24 | 25 | **tests**/ 26 | _.test.ts 27 | _.test.js 28 | _.spec.ts 29 | _.spec.js 30 | coverage/ 31 | .nyc_output/ 32 | junit.xml 33 | test-results.xml 34 | 35 | # Documentation (keep README.md and LICENSE) 36 | 37 | CHANGELOG.md 38 | CONTRIBUTING.md 39 | SECURITY.md 40 | PUBLISHING.md 41 | PUBLISHING-DUAL.md 42 | 43 | # Build tools 44 | 45 | node_modules/ 46 | npm-debug.log* 47 | yarn-debug.log* 48 | yarn-error.log* 49 | pnpm-debug.log* 50 | 51 | # Environment and temp files 52 | 53 | .env\* 54 | _.tmp 55 | _.temp 56 | .cache/ 57 | .DS_Store 58 | 59 | # Package manager lock files 60 | 61 | package-lock.json 62 | yarn.lock 63 | pnpm-lock.yaml 64 | 65 | # NPM package files 66 | 67 | package-npm.json 68 | 69 | # Git 70 | 71 | .git/ 72 | .gitignore 73 | 74 | # TypeScript build info 75 | 76 | \*.tsbuildinfo 77 | .rollup.cache/ 78 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # Configuration pour GitHub Packages 2 | @lorenzo-coslado:registry=https://npm.pkg.github.com/ 3 | 4 | # Token d'authentification pour GitHub Packages (utilise NODE_AUTH_TOKEN en CI/CD) 5 | //npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN} 6 | 7 | # Configuration pour NPM (par défaut) 8 | # Utilise les credentials de `npm login` pour les publications locales 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.0] - 2025-08-01 9 | 10 | ### Added 11 | 12 | - Initial release of Fingerprinter.js 13 | - Complete browser fingerprinting with multiple collectors: 14 | - UserAgent collector (always included) 15 | - Language collector 16 | - Timezone collector 17 | - Screen resolution collector 18 | - Plugins collector 19 | - Canvas 2D fingerprinting 20 | - WebGL fingerprinting 21 | - Audio fingerprinting 22 | - Font detection 23 | - TypeScript support with full type definitions 24 | - Confidence scoring system (0-100%) 25 | - Automatic data stability with unstable data filtering 26 | - Suspect analysis and bot detection with scoring (0-100) 27 | - Multiple output formats: CommonJS, ESM, UMD 28 | - SHA-256 hashing with mathematical fallback 29 | - Comprehensive test suite with Jest 30 | - Cross-browser compatibility (Chrome 60+, Firefox 55+, Safari 12+, Edge 79+) 31 | 32 | ### Features 33 | 34 | - **Smart Stability**: Automatically filters temporal data (timestamps, UUIDs, random values) 35 | - **Suspect Analysis**: Advanced bot and fraud detection with configurable analysis 36 | - **Modular Design**: Ability to exclude specific collectors 37 | - **Custom Data**: Support for custom data with automatic stability filtering 38 | - **High Performance**: Optimized bundle with no external dependencies 39 | - **Browser APIs**: Comprehensive coverage of browser fingerprinting techniques 40 | 41 | ### Security 42 | 43 | - Automatic filtering of unstable data to prevent fingerprint instability 44 | - Built-in bot detection for automation tools (Selenium, PhantomJS, etc.) 45 | - Headless browser detection 46 | - Inconsistency analysis for timezone/language mismatches 47 | - Known bot signature detection 48 | 49 | ### Documentation 50 | 51 | - Complete API documentation 52 | - Usage examples for common scenarios 53 | - TypeScript type definitions 54 | - Contributing guidelines 55 | - Security considerations and GDPR compliance notes 56 | 57 | ### Technical Details 58 | 59 | - Built with TypeScript 5.2.2 60 | - Bundled with Rollup 3.29.2 61 | - Tested with Jest 29.7.0 62 | - Linted with ESLint 8.49.0 63 | - Compatible with modern browsers and build tools 64 | 65 | [1.0.0]: https://github.com/Lorenzo-Coslado/fingerprinter-js/releases/tag/v1.0.0 66 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Fingerprinter.js 2 | 3 | Thank you for your interest in contributing to Fingerprinter.js! This document provides guidelines and information for contributors. 4 | 5 | ## 🚀 Getting Started 6 | 7 | ### Prerequisites 8 | 9 | - Node.js 14+ 10 | - npm 6+ 11 | - Git 12 | 13 | ### Setup Development Environment 14 | 15 | 1. Fork the repository 16 | 2. Clone your fork: 17 | 18 | ```bash 19 | git clone https://github.com/Lorenzo-Coslado/fingerprinter-js.git 20 | cd fingerprinter-js 21 | ``` 22 | 23 | 3. Install dependencies: 24 | 25 | ```bash 26 | npm install 27 | ``` 28 | 29 | 4. Run tests to ensure everything works: 30 | 31 | ```bash 32 | npm test 33 | ``` 34 | 35 | ## 🛠️ Development Workflow 36 | 37 | ### Project Structure 38 | 39 | ``` 40 | src/ 41 | ├── collectors/ # Data collection modules 42 | │ ├── basic.ts # Basic collectors (userAgent, language, etc.) 43 | │ ├── canvas.ts # Canvas and WebGL collectors 44 | │ └── advanced.ts # Audio and font collectors 45 | ├── __tests__/ # Test files 46 | ├── types.ts # TypeScript interfaces 47 | ├── utils.ts # Utility functions 48 | ├── fingerprint.ts # Main fingerprint class 49 | ├── suspect-analyzer.ts # Suspect analysis functionality 50 | └── index.ts # Main export file 51 | ``` 52 | 53 | ### Available Scripts 54 | 55 | - `npm run build` - Build the library 56 | - `npm run dev` - Build in watch mode 57 | - `npm test` - Run tests 58 | - `npm test:watch` - Run tests in watch mode 59 | - `npm run lint` - Lint the code 60 | - `npm run lint:fix` - Fix linting issues 61 | 62 | ### Making Changes 63 | 64 | 1. Create a feature branch: 65 | 66 | ```bash 67 | git checkout -b feature/your-feature-name 68 | ``` 69 | 70 | 2. Make your changes 71 | 3. Add tests for new functionality 72 | 4. Ensure all tests pass: 73 | 74 | ```bash 75 | npm test 76 | ``` 77 | 78 | 5. Lint your code: 79 | 80 | ```bash 81 | npm run lint:fix 82 | ``` 83 | 84 | 6. Build the project: 85 | 86 | ```bash 87 | npm run build 88 | ``` 89 | 90 | ## 📝 Coding Standards 91 | 92 | ### TypeScript Guidelines 93 | 94 | - Use strict TypeScript configuration 95 | - Provide proper type definitions 96 | - Document public APIs with JSDoc comments 97 | - Follow existing code style 98 | 99 | ### Code Style 100 | 101 | - Use 2 spaces for indentation 102 | - Use single quotes for strings 103 | - Add trailing commas in multiline objects/arrays 104 | - Keep lines under 100 characters when possible 105 | 106 | ### Example Code Style 107 | 108 | ```typescript 109 | /** 110 | * Example collector implementation 111 | */ 112 | export class ExampleCollector implements ComponentCollector { 113 | readonly name = "example"; 114 | 115 | async collect(): Promise { 116 | try { 117 | // Implementation here 118 | return { 119 | property: "value", 120 | timestamp: Date.now(), 121 | }; 122 | } catch (error) { 123 | return { error: (error as Error).message }; 124 | } 125 | } 126 | } 127 | ``` 128 | 129 | ## 🧪 Testing 130 | 131 | ### Writing Tests 132 | 133 | - Place tests in `src/__tests__/` directory 134 | - Use Jest testing framework 135 | - Test both success and error cases 136 | - Mock browser APIs when necessary 137 | 138 | ### Test Example 139 | 140 | ```typescript 141 | describe("ExampleCollector", () => { 142 | it("should collect example data", async () => { 143 | const collector = new ExampleCollector(); 144 | const result = await collector.collect(); 145 | 146 | expect(result).toHaveProperty("property"); 147 | expect(result.property).toBe("value"); 148 | }); 149 | 150 | it("should handle errors gracefully", async () => { 151 | // Test error handling 152 | }); 153 | }); 154 | ``` 155 | 156 | ### Running Tests 157 | 158 | ```bash 159 | # Run all tests 160 | npm test 161 | 162 | # Run tests in watch mode 163 | npm run test:watch 164 | 165 | # Run specific test file 166 | npx jest fingerprint.test.ts 167 | ``` 168 | 169 | ## 🐛 Bug Reports 170 | 171 | When reporting bugs, please include: 172 | 173 | 1. **Description**: Clear description of the issue 174 | 2. **Steps to reproduce**: Minimal steps to reproduce the bug 175 | 3. **Expected behavior**: What should happen 176 | 4. **Actual behavior**: What actually happens 177 | 5. **Environment**: Browser, version, OS 178 | 6. **Code example**: Minimal code that demonstrates the issue 179 | 180 | ### Bug Report Template 181 | 182 | ````markdown 183 | **Bug Description** 184 | A clear description of the bug. 185 | 186 | **To Reproduce** 187 | Steps to reproduce: 188 | 189 | 1. Create fingerprint with options: `{ excludeCanvas: true }` 190 | 2. Call `generate()` 191 | 3. See error 192 | 193 | **Expected Behavior** 194 | Fingerprint should generate successfully. 195 | 196 | **Actual Behavior** 197 | Error thrown: "Canvas is required" 198 | 199 | **Environment** 200 | 201 | - Browser: Chrome 91 202 | - OS: macOS 11.4 203 | - Package version: 1.0.0 204 | 205 | **Code Example** 206 | 207 | ```javascript 208 | const fp = new Fingerprint({ excludeCanvas: true }); 209 | const result = await fp.generate(); // Error occurs here 210 | ``` 211 | ```` 212 | 213 | ## 💡 Feature Requests 214 | 215 | For new features: 216 | 217 | 1. Check existing issues first 218 | 2. Describe the use case clearly 219 | 3. Explain why it would be beneficial 220 | 4. Provide implementation ideas if possible 221 | 222 | ## 🔒 Security Issues 223 | 224 | For security-related issues: 225 | 226 | 1. **DO NOT** open public issues 227 | 2. Email security concerns to: [your-email@domain.com] 228 | 3. Include detailed description and steps to reproduce 229 | 4. We'll respond within 48 hours 230 | 231 | ## 📋 Pull Request Process 232 | 233 | ### Before Submitting 234 | 235 | 1. Ensure your code follows the coding standards 236 | 2. Add or update tests as needed 237 | 3. Update documentation if necessary 238 | 4. Verify all tests pass 239 | 5. Check that the build succeeds 240 | 241 | ### Pull Request Guidelines 242 | 243 | 1. **Title**: Clear, descriptive title 244 | 2. **Description**: Explain what changes were made and why 245 | 3. **Testing**: Describe how the changes were tested 246 | 4. **Breaking Changes**: Note any breaking changes 247 | 5. **Issue Link**: Link to related issues 248 | 249 | ### Pull Request Template 250 | 251 | ```markdown 252 | ## Description 253 | 254 | Brief description of changes made. 255 | 256 | ## Type of Change 257 | 258 | - [ ] Bug fix 259 | - [ ] New feature 260 | - [ ] Breaking change 261 | - [ ] Documentation update 262 | 263 | ## Testing 264 | 265 | Describe how this was tested: 266 | 267 | - [ ] Unit tests added/updated 268 | - [ ] Manual testing performed 269 | - [ ] All existing tests pass 270 | 271 | ## Checklist 272 | 273 | - [ ] Code follows project style guidelines 274 | - [ ] Self-review completed 275 | - [ ] Documentation updated 276 | - [ ] Tests added for new functionality 277 | ``` 278 | 279 | ## 🎯 Areas for Contribution 280 | 281 | We especially welcome contributions in these areas: 282 | 283 | ### New Collectors 284 | 285 | - Additional browser APIs 286 | - Mobile-specific detection 287 | - Performance optimizations 288 | 289 | ### Suspect Analysis 290 | 291 | - New bot detection techniques 292 | - Machine learning integration 293 | - Improved accuracy 294 | 295 | ### Documentation 296 | 297 | - Usage examples 298 | - API documentation 299 | - Performance guides 300 | 301 | ### Testing 302 | 303 | - Cross-browser testing 304 | - Edge case coverage 305 | - Performance benchmarks 306 | 307 | ## 📚 Resources 308 | 309 | - [TypeScript Documentation](https://www.typescriptlang.org/docs/) 310 | - [Jest Testing Framework](https://jestjs.io/docs/getting-started) 311 | - [Rollup Bundler](https://rollupjs.org/guide/en/) 312 | 313 | ## 🤝 Community 314 | 315 | - **Issues**: Use GitHub issues for bugs and features 316 | - **Discussions**: Use GitHub discussions for questions 317 | - **Code of Conduct**: Be respectful and constructive 318 | 319 | ## ❓ Questions 320 | 321 | If you have questions about contributing: 322 | 323 | 1. Check existing documentation 324 | 2. Search closed issues and discussions 325 | 3. Open a new discussion 326 | 4. Ask in the issue comments 327 | 328 | Thank you for contributing to Fingerprinter.js! 🎉 329 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Lorenzo Coslado 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FingerprinterJS 2 | 3 | A modern JavaScript library for generating unique and reliable browser fingerprints. 4 | 5 | [![Sponsor](https://img.shields.io/badge/Sponsor-❤️-red)](https://github.com/sponsors/Lorenzo-Coslado) 6 | [![npm version](https://img.shields.io/npm/v/fingerprinter-js)](https://www.npmjs.com/package/fingerprinter-js) 7 | [![npm downloads](https://img.shields.io/npm/dt/fingerprinter-js)](https://www.npmjs.com/package/fingerprinter-js) 8 | 9 | ## 🚀 Features 10 | 11 | - **Complete fingerprinting**: Uses multiple fingerprinting techniques (Canvas, WebGL, Audio, Fonts, etc.) 12 | - **TypeScript**: Full support with included types 13 | - **Modular**: Ability to exclude certain collectors 14 | - **Compatible**: Works in all modern browsers 15 | - **Lightweight**: Optimized bundle with no dependencies 16 | - **Secure**: Uses SHA-256 for hashing when available 17 | - **Smart Stability**: Automatically filters unstable data 18 | - **Suspect Analysis**: Built-in bot and fraud detection 19 | 20 | ## 📦 Installation 21 | 22 | ```bash 23 | npm install fingerprinter-js 24 | ``` 25 | 26 | ## 🔧 Usage 27 | 28 | ### Basic Usage 29 | 30 | ```javascript 31 | import Fingerprint from "fingerprinter-js"; 32 | 33 | // Simple generation 34 | const result = await Fingerprint.generate(); 35 | console.log(result.fingerprint); // "a1b2c3d4e5f6..." 36 | console.log(result.confidence); // 85 37 | ``` 38 | 39 | ### Advanced Usage 40 | 41 | ```javascript 42 | import { Fingerprint } from "fingerprinter-js"; 43 | 44 | // With custom options 45 | const fingerprint = new Fingerprint({ 46 | excludeCanvas: true, 47 | excludeWebGL: true, 48 | customData: { 49 | userId: "12345", 50 | sessionId: "abc-def-ghi", // ⚠️ Will be automatically filtered for stability 51 | version: "1.0", // ✅ Stable, will be kept 52 | }, 53 | }); 54 | 55 | // To include unstable data (not recommended) 56 | const unstableFingerprint = new Fingerprint({ 57 | allowUnstableData: true, 58 | customData: { 59 | timestamp: Date.now(), // ⚠️ Will make fingerprint unstable 60 | sessionId: "random-id", 61 | }, 62 | }); 63 | 64 | const result = await fingerprint.generate(); 65 | ``` 66 | 67 | ### With Suspect Analysis 68 | 69 | ```javascript 70 | // Enable bot/fraud detection 71 | const result = await Fingerprint.generate({ 72 | includeSuspectAnalysis: true, 73 | }); 74 | 75 | console.log(result.suspectAnalysis); 76 | // { 77 | // score: 15, // 0-100 (0=legitimate, 100=very suspicious) 78 | // riskLevel: 'LOW', // LOW/MEDIUM/HIGH 79 | // signals: [...], // Detected suspicious signals 80 | // details: {...} // Analysis details 81 | // } 82 | ``` 83 | 84 | ### Available Options 85 | 86 | ```typescript 87 | interface FingerprintOptions { 88 | excludeScreenResolution?: boolean; 89 | excludeTimezone?: boolean; 90 | excludeLanguage?: boolean; 91 | excludeCanvas?: boolean; 92 | excludeWebGL?: boolean; 93 | excludeAudio?: boolean; 94 | excludePlugins?: boolean; 95 | excludeFonts?: boolean; 96 | customData?: Record; 97 | allowUnstableData?: boolean; // Allow temporal data (default: false) 98 | includeSuspectAnalysis?: boolean; // Include suspect analysis (default: false) 99 | } 100 | ``` 101 | 102 | ⚠️ **Automatic Stability**: By default, FingerprintJS automatically filters unstable data from `customData` (timestamps, UUIDs, random values) to ensure fingerprint stability. Use `allowUnstableData: true` if you need to include this data. 103 | 104 | ### Get Components Only 105 | 106 | ```javascript 107 | const fingerprint = new Fingerprint(); 108 | const components = await fingerprint.getComponents(); 109 | 110 | console.log(components); 111 | // { 112 | // userAgent: "Mozilla/5.0...", 113 | // language: ["en-US", "en"], 114 | // timezone: "America/New_York", 115 | // screen: { width: 1920, height: 1080, ... }, 116 | // canvas: "data:image/png;base64,...", 117 | // webgl: { vendor: "Google Inc.", ... }, 118 | // ... 119 | // } 120 | ``` 121 | 122 | ## 🎯 Available Collectors 123 | 124 | | Collector | Description | Exclusion Option | 125 | | ----------- | -------------------------------- | ------------------------- | 126 | | `userAgent` | Browser User-Agent | ❌ (always included) | 127 | | `language` | Preferred languages | `excludeLanguage` | 128 | | `timezone` | Timezone | `excludeTimezone` | 129 | | `screen` | Screen resolution and properties | `excludeScreenResolution` | 130 | | `plugins` | Installed plugins | `excludePlugins` | 131 | | `canvas` | Canvas 2D fingerprint | `excludeCanvas` | 132 | | `webgl` | WebGL information | `excludeWebGL` | 133 | | `audio` | Audio fingerprint | `excludeAudio` | 134 | | `fonts` | Available fonts | `excludeFonts` | 135 | 136 | ## 📈 Confidence Level 137 | 138 | The confidence level indicates fingerprint reliability: 139 | 140 | - **90-100%**: Very reliable, many components available 141 | - **70-89%**: Reliable, some components missing 142 | - **50-69%**: Moderately reliable, several components unavailable 143 | - **< 50%**: Low reliability, few components available 144 | 145 | ## 🕵️ Suspect Analysis 146 | 147 | FingerprintJS includes advanced bot and fraud detection: 148 | 149 | ### Suspect Score (0-100) 150 | 151 | - **0-30**: Legitimate user ✅ 152 | - **30-70**: Requires vigilance ⚠️ 153 | - **70-100**: Likely malicious 🚨 154 | 155 | ### Detection Capabilities 156 | 157 | - **Automation Tools**: Selenium, PhantomJS, Puppeteer 158 | - **Headless Browsers**: Chrome headless, etc. 159 | - **Inconsistencies**: Timezone/language mismatches 160 | - **Bot Signatures**: Known crawler patterns 161 | - **Environmental Anomalies**: Missing APIs, suspicious user agents 162 | 163 | ### Example Usage 164 | 165 | ```javascript 166 | const result = await Fingerprint.generate({ 167 | includeSuspectAnalysis: true, 168 | }); 169 | 170 | if (result.suspectAnalysis.score > 70) { 171 | // Block or challenge suspicious users 172 | console.log("Suspicious activity detected"); 173 | console.log("Signals:", result.suspectAnalysis.signals); 174 | } else if (result.suspectAnalysis.score > 30) { 175 | // Require additional authentication 176 | console.log("Moderate risk detected"); 177 | } else { 178 | // Allow normal access 179 | console.log("Legitimate user"); 180 | } 181 | ``` 182 | 183 | ## 🛡️ Security Considerations 184 | 185 | This library is designed for: 186 | 187 | - Two-factor authentication 188 | - Fraud detection 189 | - Anonymous analytics 190 | - Experience personalization 191 | 192 | **Important**: Respect user privacy and local regulations (GDPR, etc.). 193 | 194 | ## 🔄 Fingerprint Stability 195 | 196 | FingerprintJS automatically ensures fingerprint stability by filtering unstable data: 197 | 198 | ### ✅ Stable Data (kept) 199 | 200 | - Static browser properties 201 | - Stable custom data (version, configuration, etc.) 202 | - Hardware characteristics 203 | 204 | ### ❌ Unstable Data (automatically filtered) 205 | 206 | - `timestamp`, `time`, `now`, `date` 207 | - `random`, `rand`, `nonce`, `salt` 208 | - `sessionId`, `requestId`, `uuid` 209 | - UUIDs and temporary identifiers 210 | - Numbers that look like timestamps 211 | 212 | ### Automatic Filtering Example 213 | 214 | ```javascript 215 | const fp = new Fingerprint({ 216 | customData: { 217 | // ✅ Kept (stable) 218 | version: "1.0", 219 | theme: "dark", 220 | 221 | // ❌ Automatically filtered (unstable) 222 | timestamp: Date.now(), 223 | sessionId: "123e4567-e89b-12d3-a456-426614174000", 224 | random: Math.random(), 225 | }, 226 | }); 227 | 228 | // Result: only version and theme will be included 229 | ``` 230 | 231 | If you absolutely need to include unstable data: 232 | 233 | ```javascript 234 | const fp = new Fingerprint({ 235 | allowUnstableData: true, // ⚠️ Disables filtering 236 | customData: { 237 | timestamp: Date.now(), // Will make fingerprint unstable 238 | }, 239 | }); 240 | ``` 241 | 242 | ## 🔧 Custom Collectors Usage 243 | 244 | ```javascript 245 | import { Fingerprint, CanvasCollector, WebGLCollector } from "fingerprinter-js"; 246 | 247 | // Use only specific collectors 248 | const canvas = new CanvasCollector(); 249 | const canvasData = await canvas.collect(); 250 | 251 | const webgl = new WebGLCollector(); 252 | const webglData = await webgl.collect(); 253 | ``` 254 | 255 | ## 📱 Compatibility 256 | 257 | - **Browsers**: Chrome 60+, Firefox 55+, Safari 12+, Edge 79+ 258 | - **Node.js**: Not supported (browser environment only) 259 | - **TypeScript**: Full support 260 | 261 | ## 🤝 API Reference 262 | 263 | ### Fingerprint Class 264 | 265 | #### `constructor(options?: FingerprintOptions)` 266 | 267 | Creates a new Fingerprint instance. 268 | 269 | #### `generate(): Promise` 270 | 271 | Generates a complete fingerprint. 272 | 273 | #### `getComponents(): Promise>` 274 | 275 | Gets components without generating hash. 276 | 277 | ### Static Methods 278 | 279 | #### `Fingerprint.generate(options?: FingerprintOptions): Promise` 280 | 281 | Quickly generates a fingerprint with default options. 282 | 283 | #### `Fingerprint.getAvailableCollectors(): string[]` 284 | 285 | Returns the list of available collectors. 286 | 287 | ### Types 288 | 289 | ```typescript 290 | interface FingerprintResult { 291 | fingerprint: string; // Fingerprint hash 292 | components: Record; // Collected data 293 | confidence: number; // Confidence level (0-100) 294 | suspectAnalysis?: SuspectAnalysis; // Optional suspect analysis 295 | } 296 | 297 | interface SuspectAnalysis { 298 | score: number; // Suspect score (0-100) 299 | riskLevel: "LOW" | "MEDIUM" | "HIGH"; 300 | signals: SuspectSignal[]; 301 | details: Record; 302 | } 303 | ``` 304 | 305 | ## 🚀 Use Cases 306 | 307 | ### Fraud Detection 308 | 309 | ```javascript 310 | const result = await Fingerprint.generate({ includeSuspectAnalysis: true }); 311 | if (result.suspectAnalysis.score > 80) { 312 | // Block transaction 313 | } 314 | ``` 315 | 316 | ### Bot Protection 317 | 318 | ```javascript 319 | const result = await Fingerprint.generate({ includeSuspectAnalysis: true }); 320 | const automationSignals = result.suspectAnalysis.signals.filter((s) => 321 | ["webdriver", "headless", "selenium"].includes(s.type) 322 | ); 323 | if (automationSignals.length > 0) { 324 | // Challenge with CAPTCHA 325 | } 326 | ``` 327 | 328 | ### Analytics Quality 329 | 330 | ```javascript 331 | const result = await Fingerprint.generate({ includeSuspectAnalysis: true }); 332 | if (result.suspectAnalysis.score < 40) { 333 | // Include in analytics 334 | track("page_view", { fingerprint: result.fingerprint }); 335 | } 336 | ``` 337 | 338 | ## 📄 License 339 | 340 | MIT © Lorenzo Coslado 341 | 342 | ## 🤝 Contributing 343 | 344 | Contributions are welcome! Please read the contributing guide before submitting a PR. 345 | 346 | ## 📞 Support 347 | 348 | - 🐛 **Issues**: [GitHub Issues](https://github.com/Lorenzo-Coslado/fingerprinter-js/issues) 349 | - 💬 **Discussions**: [GitHub Discussions](https://github.com/Lorenzo-Coslado/fingerprinter-js/discussions) 350 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # 🔐 Security Policy 2 | 3 | ## 📦 Supported Versions 4 | 5 | We currently support the following versions with security updates: 6 | 7 | | Version | Supported | 8 | | ------- | ---------- | 9 | | 1.x | ✅ Yes | 10 | | < 1.0 | ❌ No | 11 | 12 | If you're using an unsupported version, please consider upgrading to the latest stable release. 13 | 14 | --- 15 | 16 | ## 🛡️ Reporting a Vulnerability 17 | 18 | If you believe you've found a security vulnerability in `fingerprinter-js`, please report it **privately** using GitHub's security advisory workflow: 19 | 20 | 🔗 [Report a vulnerability](https://github.com/Lorenzo-Coslado/fingerprinter-js/security/advisories/new) 21 | 22 | > Please do **not** create a public issue. 23 | 24 | We aim to respond within **72 hours** and will keep you updated throughout the investigation and resolution process. 25 | 26 | Thank you for helping keep this project secure 🙏 27 | -------------------------------------------------------------------------------- /examples/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | FingerprintJS Demo 7 | 92 | 93 | 94 |

🔍 FingerprintJS Demo

95 | 96 |
97 |

Options

98 |
99 | 102 | 105 | 108 | 111 | 114 | 117 | 120 | 123 | 127 |
128 | 129 | 130 | 131 | 132 | 133 |
134 | 135 |
136 | 137 | 524 | 525 | 526 | -------------------------------------------------------------------------------- /examples/suspect-score-examples.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Exemples d'utilisation du Suspect Score 3 | * Montre comment utiliser l'analyse de suspicion dans différents cas d'usage 4 | */ 5 | 6 | import Fingerprint from "../src/index"; 7 | 8 | // Exemple 1: Authentification sécurisée 9 | async function secureAuthentication(username: string, password: string) { 10 | console.log("🔐 Authentification sécurisée"); 11 | 12 | const result = await Fingerprint.generate({ 13 | includeSuspectAnalysis: true, 14 | }); 15 | 16 | console.log(`Utilisateur: ${username}`); 17 | console.log(`Confiance: ${result.confidence}%`); 18 | console.log(`Score de suspicion: ${result.suspectAnalysis?.score}/100`); 19 | console.log(`Niveau de risque: ${result.suspectAnalysis?.riskLevel}`); 20 | 21 | // Logique d'authentification adaptative 22 | if (result.suspectAnalysis!.score > 70) { 23 | return { 24 | success: false, 25 | reason: "Environnement suspect détecté", 26 | requiresCaptcha: true, 27 | suspectSignals: result.suspectAnalysis!.signals.map((s) => s.type), 28 | }; 29 | } else if (result.suspectAnalysis!.score > 30) { 30 | return { 31 | success: true, 32 | requires2FA: true, 33 | reason: "Authentification à deux facteurs requise", 34 | }; 35 | } else { 36 | return { 37 | success: true, 38 | requires2FA: false, 39 | reason: "Connexion autorisée", 40 | }; 41 | } 42 | } 43 | 44 | // Exemple 2: Détection de fraude e-commerce 45 | async function fraudDetection(transaction: any) { 46 | console.log("💳 Détection de fraude e-commerce"); 47 | 48 | const result = await Fingerprint.generate({ 49 | includeSuspectAnalysis: true, 50 | customData: { 51 | transactionAmount: transaction.amount, 52 | currency: transaction.currency, 53 | merchantId: transaction.merchantId, 54 | }, 55 | }); 56 | 57 | const riskScore = calculateTransactionRisk(result, transaction); 58 | 59 | console.log(`Montant: ${transaction.amount} ${transaction.currency}`); 60 | console.log(`Score suspect: ${result.suspectAnalysis?.score}/100`); 61 | console.log(`Score de risque transaction: ${riskScore}/100`); 62 | 63 | if (riskScore > 80) { 64 | return { 65 | decision: "BLOCK", 66 | reason: "Transaction à haut risque", 67 | actions: ["Bloquer la transaction", "Alerter l'équipe fraude"], 68 | }; 69 | } else if (riskScore > 50) { 70 | return { 71 | decision: "REVIEW", 72 | reason: "Transaction suspecte", 73 | actions: ["Vérification manuelle requise", "Authentification renforcée"], 74 | }; 75 | } else { 76 | return { 77 | decision: "APPROVE", 78 | reason: "Transaction légitime", 79 | actions: ["Autoriser la transaction"], 80 | }; 81 | } 82 | } 83 | 84 | function calculateTransactionRisk( 85 | fingerprintResult: any, 86 | transaction: any 87 | ): number { 88 | let risk = fingerprintResult.suspectAnalysis?.score || 0; 89 | 90 | // Facteurs de risque spécifiques à la transaction 91 | if (transaction.amount > 1000) risk += 10; 92 | if (transaction.isInternational) risk += 15; 93 | if (transaction.isFirstTime) risk += 20; 94 | 95 | return Math.min(100, risk); 96 | } 97 | 98 | // Exemple 3: Protection contre les bots 99 | async function botProtection(request: any) { 100 | console.log("🤖 Protection contre les bots"); 101 | 102 | const result = await Fingerprint.generate({ 103 | includeSuspectAnalysis: true, 104 | }); 105 | 106 | const automationSignals = 107 | result.suspectAnalysis?.signals.filter((s) => 108 | ["webdriver", "headless", "selenium", "phantom"].includes(s.type) 109 | ) || []; 110 | 111 | console.log(`Signaux d'automation: ${automationSignals.length}`); 112 | console.log(`Score suspect: ${result.suspectAnalysis?.score}/100`); 113 | 114 | if (automationSignals.length > 0) { 115 | return { 116 | isBot: true, 117 | confidence: 90, 118 | detectedTools: automationSignals.map((s) => s.type), 119 | action: "BLOCK", 120 | }; 121 | } else if (result.suspectAnalysis!.score > 60) { 122 | return { 123 | isBot: "LIKELY", 124 | confidence: 70, 125 | action: "CAPTCHA", 126 | }; 127 | } else { 128 | return { 129 | isBot: false, 130 | confidence: 95, 131 | action: "ALLOW", 132 | }; 133 | } 134 | } 135 | 136 | // Exemple 4: Analytics avec filtrage qualité 137 | async function qualityAnalytics(pageView: any) { 138 | console.log("📊 Analytics avec filtrage qualité"); 139 | 140 | const result = await Fingerprint.generate({ 141 | includeSuspectAnalysis: true, 142 | customData: { 143 | page: pageView.url, 144 | referrer: pageView.referrer, 145 | sessionId: pageView.sessionId, 146 | }, 147 | }); 148 | 149 | // Ne pas inclure les données suspectes dans les analytics 150 | if (result.suspectAnalysis!.score < 40) { 151 | return { 152 | shouldTrack: true, 153 | visitorType: "HUMAN", 154 | dataQuality: "HIGH", 155 | fingerprint: result.fingerprint, 156 | }; 157 | } else if (result.suspectAnalysis!.score < 70) { 158 | return { 159 | shouldTrack: true, 160 | visitorType: "UNCERTAIN", 161 | dataQuality: "MEDIUM", 162 | fingerprint: result.fingerprint, 163 | flags: result.suspectAnalysis!.signals.map((s) => s.type), 164 | }; 165 | } else { 166 | return { 167 | shouldTrack: false, 168 | visitorType: "BOT", 169 | dataQuality: "LOW", 170 | reason: "Score de suspicion trop élevé", 171 | }; 172 | } 173 | } 174 | 175 | // Exemple 5: Personnalisation adaptative 176 | async function adaptivePersonalization(userId: string) { 177 | console.log("🎯 Personnalisation adaptative"); 178 | 179 | const result = await Fingerprint.generate({ 180 | includeSuspectAnalysis: true, 181 | customData: { 182 | userId: userId, 183 | preferences: "dark-theme", 184 | }, 185 | }); 186 | 187 | // Adapter le niveau de personnalisation selon la confiance 188 | if (result.confidence > 80 && result.suspectAnalysis!.score < 30) { 189 | return { 190 | personalizationLevel: "ADVANCED", 191 | features: [ 192 | "Recommandations IA", 193 | "Interface adaptée", 194 | "Contenu personnalisé", 195 | ], 196 | cacheFingerprint: true, 197 | }; 198 | } else if (result.confidence > 60 && result.suspectAnalysis!.score < 60) { 199 | return { 200 | personalizationLevel: "BASIC", 201 | features: ["Thème préféré", "Langue"], 202 | cacheFingerprint: false, 203 | }; 204 | } else { 205 | return { 206 | personalizationLevel: "NONE", 207 | features: ["Expérience par défaut"], 208 | cacheFingerprint: false, 209 | reason: "Environnement non fiable", 210 | }; 211 | } 212 | } 213 | 214 | // Démonstration d'utilisation 215 | async function demonstrateUsage() { 216 | console.log("🚀 Démonstration du Suspect Score\n"); 217 | 218 | // Test authentification 219 | const authResult = await secureAuthentication("user123", "password"); 220 | console.log("Résultat authentification:", authResult); 221 | console.log(""); 222 | 223 | // Test détection fraude 224 | const fraudResult = await fraudDetection({ 225 | amount: 2500, 226 | currency: "EUR", 227 | merchantId: "merchant_123", 228 | isInternational: true, 229 | isFirstTime: false, 230 | }); 231 | console.log("Résultat détection fraude:", fraudResult); 232 | console.log(""); 233 | 234 | // Test protection bots 235 | const botResult = await botProtection({ ip: "192.168.1.1" }); 236 | console.log("Résultat protection bot:", botResult); 237 | console.log(""); 238 | 239 | // Test analytics 240 | const analyticsResult = await qualityAnalytics({ 241 | url: "/product/123", 242 | referrer: "https://google.com", 243 | sessionId: "sess_456", 244 | }); 245 | console.log("Résultat analytics:", analyticsResult); 246 | console.log(""); 247 | 248 | // Test personnalisation 249 | const personalizationResult = await adaptivePersonalization("user_789"); 250 | console.log("Résultat personnalisation:", personalizationResult); 251 | } 252 | 253 | // Exporter pour utilisation 254 | export { 255 | adaptivePersonalization, 256 | botProtection, 257 | demonstrateUsage, 258 | fraudDetection, 259 | qualityAnalytics, 260 | secureAuthentication, 261 | }; 262 | 263 | // Si exécuté directement 264 | if (require.main === module) { 265 | demonstrateUsage().catch(console.error); 266 | } 267 | -------------------------------------------------------------------------------- /examples/suspect-score-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test Suspect Score - FingerprintJS 7 | 82 | 83 | 84 |
85 |

🔍 Test du Suspect Score

86 |

Cette page teste l'analyse de suspicion de votre navigateur.

87 | 88 | 89 | 90 |
91 |
92 | 93 | 94 | 261 | 262 | 263 | -------------------------------------------------------------------------------- /examples/usage-examples.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Fingerprinter.js Usage Examples 3 | * Demonstrates various use cases and implementations 4 | */ 5 | 6 | import Fingerprint from "../src/index"; 7 | 8 | // Example 1: Basic Fraud Detection 9 | async function fraudDetection(transactionData: any) { 10 | console.log("💳 Fraud Detection System"); 11 | 12 | const result = await Fingerprint.generate({ 13 | includeSuspectAnalysis: true, 14 | customData: { 15 | transactionId: transactionData.id, 16 | amount: transactionData.amount, 17 | currency: transactionData.currency, 18 | }, 19 | }); 20 | 21 | console.log( 22 | `Transaction: ${transactionData.amount} ${transactionData.currency}` 23 | ); 24 | console.log(`Fingerprint confidence: ${result.confidence}%`); 25 | console.log(`Suspect score: ${result.suspectAnalysis?.score}/100`); 26 | console.log(`Risk level: ${result.suspectAnalysis?.riskLevel}`); 27 | 28 | // Calculate transaction risk 29 | const riskScore = calculateTransactionRisk(result, transactionData); 30 | 31 | if (riskScore > 80) { 32 | return { 33 | decision: "BLOCK", 34 | reason: "High-risk transaction detected", 35 | actions: [ 36 | "Block transaction", 37 | "Alert fraud team", 38 | "Require manual review", 39 | ], 40 | }; 41 | } else if (riskScore > 50) { 42 | return { 43 | decision: "REVIEW", 44 | reason: "Suspicious transaction patterns", 45 | actions: ["Manual verification required", "Additional authentication"], 46 | }; 47 | } else { 48 | return { 49 | decision: "APPROVE", 50 | reason: "Transaction appears legitimate", 51 | actions: ["Process normally"], 52 | }; 53 | } 54 | } 55 | 56 | function calculateTransactionRisk( 57 | fingerprintResult: any, 58 | transaction: any 59 | ): number { 60 | let risk = fingerprintResult.suspectAnalysis?.score || 0; 61 | 62 | // Transaction-specific risk factors 63 | if (transaction.amount > 1000) risk += 10; 64 | if (transaction.isInternational) risk += 15; 65 | if (transaction.isFirstTime) risk += 20; 66 | if (transaction.timeOfDay < 6 || transaction.timeOfDay > 22) risk += 5; 67 | 68 | return Math.min(100, risk); 69 | } 70 | 71 | // Example 2: Bot Protection 72 | async function botProtection() { 73 | console.log("🤖 Bot Protection System"); 74 | 75 | const result = await Fingerprint.generate({ 76 | includeSuspectAnalysis: true, 77 | }); 78 | 79 | // Check for automation signals 80 | const automationSignals = 81 | result.suspectAnalysis?.signals.filter((s) => 82 | ["webdriver", "headless", "selenium", "phantom"].includes(s.type) 83 | ) || []; 84 | 85 | console.log(`Automation signals detected: ${automationSignals.length}`); 86 | console.log(`Overall suspect score: ${result.suspectAnalysis?.score}/100`); 87 | 88 | if (automationSignals.length > 0) { 89 | return { 90 | isBot: true, 91 | confidence: 95, 92 | detectedTools: automationSignals.map((s) => s.type), 93 | action: "BLOCK", 94 | reason: "Automation tools detected", 95 | }; 96 | } else if (result.suspectAnalysis!.score > 70) { 97 | return { 98 | isBot: "LIKELY", 99 | confidence: 80, 100 | action: "CHALLENGE", 101 | reason: "High suspect score", 102 | }; 103 | } else if (result.suspectAnalysis!.score > 30) { 104 | return { 105 | isBot: "POSSIBLE", 106 | confidence: 60, 107 | action: "MONITOR", 108 | reason: "Moderate suspect score", 109 | }; 110 | } else { 111 | return { 112 | isBot: false, 113 | confidence: 95, 114 | action: "ALLOW", 115 | reason: "Appears to be human user", 116 | }; 117 | } 118 | } 119 | 120 | // Example 3: Quality Analytics 121 | async function qualityAnalytics(pageViewData: any) { 122 | console.log("📊 Quality Analytics System"); 123 | 124 | const result = await Fingerprint.generate({ 125 | includeSuspectAnalysis: true, 126 | customData: { 127 | page: pageViewData.url, 128 | referrer: pageViewData.referrer, 129 | userAgent: navigator.userAgent, 130 | }, 131 | }); 132 | 133 | // Filter out low-quality data 134 | const shouldTrack = 135 | result.suspectAnalysis!.score < 50 && result.confidence > 60; 136 | 137 | if (shouldTrack) { 138 | return { 139 | track: true, 140 | visitorType: "HUMAN", 141 | dataQuality: getDataQuality( 142 | result.confidence, 143 | result.suspectAnalysis!.score 144 | ), 145 | fingerprint: result.fingerprint, 146 | metadata: { 147 | confidence: result.confidence, 148 | suspectScore: result.suspectAnalysis!.score, 149 | components: Object.keys(result.components).length, 150 | }, 151 | }; 152 | } else { 153 | return { 154 | track: false, 155 | visitorType: result.suspectAnalysis!.score > 70 ? "BOT" : "UNCERTAIN", 156 | dataQuality: "LOW", 157 | reason: `High suspect score (${ 158 | result.suspectAnalysis!.score 159 | }) or low confidence (${result.confidence})`, 160 | }; 161 | } 162 | } 163 | 164 | function getDataQuality(confidence: number, suspectScore: number): string { 165 | if (confidence > 80 && suspectScore < 20) return "EXCELLENT"; 166 | if (confidence > 70 && suspectScore < 30) return "GOOD"; 167 | if (confidence > 60 && suspectScore < 40) return "FAIR"; 168 | return "POOR"; 169 | } 170 | 171 | // Example 4: Adaptive Authentication 172 | async function adaptiveAuthentication(username: string, loginAttempt: any) { 173 | console.log("🔐 Adaptive Authentication System"); 174 | 175 | const result = await Fingerprint.generate({ 176 | includeSuspectAnalysis: true, 177 | customData: { 178 | username: username, 179 | loginTime: new Date().getHours(), // Will be filtered for stability 180 | ipRegion: loginAttempt.ipRegion, 181 | }, 182 | }); 183 | 184 | console.log(`User: ${username}`); 185 | console.log(`Fingerprint confidence: ${result.confidence}%`); 186 | console.log(`Suspect score: ${result.suspectAnalysis?.score}/100`); 187 | console.log(`Risk level: ${result.suspectAnalysis?.riskLevel}`); 188 | 189 | // Determine authentication requirements 190 | const riskLevel = calculateAuthRisk(result, loginAttempt); 191 | 192 | if (riskLevel === "HIGH") { 193 | return { 194 | allow: false, 195 | reason: "High-risk login attempt", 196 | requirements: ["CAPTCHA", "Email verification", "Manual review"], 197 | message: "Additional verification required", 198 | }; 199 | } else if (riskLevel === "MEDIUM") { 200 | return { 201 | allow: true, 202 | reason: "Moderate risk detected", 203 | requirements: ["2FA", "SMS verification"], 204 | message: "Please complete two-factor authentication", 205 | }; 206 | } else { 207 | return { 208 | allow: true, 209 | reason: "Low risk login", 210 | requirements: [], 211 | message: "Login successful", 212 | }; 213 | } 214 | } 215 | 216 | function calculateAuthRisk( 217 | result: any, 218 | loginAttempt: any 219 | ): "LOW" | "MEDIUM" | "HIGH" { 220 | let riskFactors = 0; 221 | 222 | if (result.suspectAnalysis?.score > 70) riskFactors += 3; 223 | else if (result.suspectAnalysis?.score > 40) riskFactors += 1; 224 | 225 | if (result.confidence < 50) riskFactors += 2; 226 | else if (result.confidence < 70) riskFactors += 1; 227 | 228 | if (loginAttempt.unusualLocation) riskFactors += 2; 229 | if (loginAttempt.newDevice) riskFactors += 1; 230 | if (loginAttempt.offHours) riskFactors += 1; 231 | 232 | if (riskFactors >= 5) return "HIGH"; 233 | if (riskFactors >= 2) return "MEDIUM"; 234 | return "LOW"; 235 | } 236 | 237 | // Example 5: Personalization Engine 238 | async function personalizationEngine(userId: string) { 239 | console.log("🎯 Personalization Engine"); 240 | 241 | const result = await Fingerprint.generate({ 242 | includeSuspectAnalysis: true, 243 | customData: { 244 | userId: userId, 245 | preferences: "personalized-content", 246 | }, 247 | }); 248 | 249 | // Determine personalization level based on trust 250 | const trustScore = calculateTrustScore(result); 251 | 252 | if (trustScore > 80) { 253 | return { 254 | personalizationLevel: "ADVANCED", 255 | features: [ 256 | "AI-powered recommendations", 257 | "Adaptive interface", 258 | "Predictive content", 259 | "Custom workflows", 260 | ], 261 | cacheFingerprint: true, 262 | trustScore: trustScore, 263 | }; 264 | } else if (trustScore > 60) { 265 | return { 266 | personalizationLevel: "STANDARD", 267 | features: [ 268 | "Basic recommendations", 269 | "Theme preferences", 270 | "Language settings", 271 | ], 272 | cacheFingerprint: true, 273 | trustScore: trustScore, 274 | }; 275 | } else { 276 | return { 277 | personalizationLevel: "MINIMAL", 278 | features: ["Default experience", "Basic settings only"], 279 | cacheFingerprint: false, 280 | trustScore: trustScore, 281 | reason: "Low trust score - limited personalization", 282 | }; 283 | } 284 | } 285 | 286 | function calculateTrustScore(result: any): number { 287 | let trust = 100; 288 | 289 | // Reduce trust based on suspect score 290 | trust -= result.suspectAnalysis?.score || 0; 291 | 292 | // Adjust based on confidence 293 | if (result.confidence < 70) trust -= 20; 294 | else if (result.confidence < 50) trust -= 40; 295 | 296 | // Boost trust for high confidence 297 | if (result.confidence > 90) trust += 10; 298 | 299 | return Math.max(0, Math.min(100, trust)); 300 | } 301 | 302 | // Example 6: Rate Limiting 303 | async function intelligentRateLimit(apiKey: string, endpoint: string) { 304 | console.log("🚦 Intelligent Rate Limiting"); 305 | 306 | const result = await Fingerprint.generate({ 307 | includeSuspectAnalysis: true, 308 | customData: { 309 | apiKey: apiKey, 310 | endpoint: endpoint, 311 | }, 312 | }); 313 | 314 | // Calculate rate limit based on trust level 315 | const baseLimit = 100; // requests per hour 316 | let multiplier = 1; 317 | 318 | if (result.suspectAnalysis!.score > 70) { 319 | multiplier = 0.1; // Very restrictive 320 | } else if (result.suspectAnalysis!.score > 40) { 321 | multiplier = 0.5; // Moderately restrictive 322 | } else if (result.suspectAnalysis!.score < 20 && result.confidence > 80) { 323 | multiplier = 2; // More generous 324 | } 325 | 326 | const rateLimit = Math.floor(baseLimit * multiplier); 327 | 328 | return { 329 | rateLimit: rateLimit, 330 | suspectScore: result.suspectAnalysis!.score, 331 | confidence: result.confidence, 332 | fingerprint: result.fingerprint, 333 | recommendations: getRateLimitRecommendations(result.suspectAnalysis!.score), 334 | }; 335 | } 336 | 337 | function getRateLimitRecommendations(suspectScore: number): string[] { 338 | if (suspectScore > 70) { 339 | return [ 340 | "Consider blocking this client", 341 | "Implement CAPTCHA verification", 342 | "Monitor for abuse patterns", 343 | ]; 344 | } else if (suspectScore > 40) { 345 | return [ 346 | "Apply stricter rate limits", 347 | "Monitor API usage patterns", 348 | "Consider additional authentication", 349 | ]; 350 | } else { 351 | return [ 352 | "Normal rate limiting", 353 | "Monitor for sudden spikes", 354 | "Consider premium rate limits for trusted users", 355 | ]; 356 | } 357 | } 358 | 359 | // Demo runner 360 | async function runExamples() { 361 | console.log("🚀 Running Fingerprinter.js Examples\n"); 362 | 363 | // Fraud detection example 364 | const fraudResult = await fraudDetection({ 365 | id: "txn_12345", 366 | amount: 2500, 367 | currency: "USD", 368 | isInternational: true, 369 | isFirstTime: false, 370 | timeOfDay: 14, 371 | }); 372 | console.log("Fraud Detection Result:", fraudResult); 373 | console.log(""); 374 | 375 | // Bot protection example 376 | const botResult = await botProtection(); 377 | console.log("Bot Protection Result:", botResult); 378 | console.log(""); 379 | 380 | // Analytics example 381 | const analyticsResult = await qualityAnalytics({ 382 | url: "/product/laptop-pro", 383 | referrer: "https://google.com", 384 | }); 385 | console.log("Analytics Result:", analyticsResult); 386 | console.log(""); 387 | 388 | // Authentication example 389 | const authResult = await adaptiveAuthentication("john.doe", { 390 | ipRegion: "US-CA", 391 | unusualLocation: false, 392 | newDevice: true, 393 | offHours: false, 394 | }); 395 | console.log("Authentication Result:", authResult); 396 | console.log(""); 397 | 398 | // Personalization example 399 | const personalizationResult = await personalizationEngine("user_789"); 400 | console.log("Personalization Result:", personalizationResult); 401 | console.log(""); 402 | 403 | // Rate limiting example 404 | const rateLimitResult = await intelligentRateLimit( 405 | "api_key_123", 406 | "/api/data" 407 | ); 408 | console.log("Rate Limit Result:", rateLimitResult); 409 | } 410 | 411 | // Export for use 412 | export { 413 | adaptiveAuthentication, 414 | botProtection, 415 | fraudDetection, 416 | intelligentRateLimit, 417 | personalizationEngine, 418 | qualityAnalytics, 419 | runExamples, 420 | }; 421 | 422 | // Run examples if executed directly 423 | if (typeof window !== "undefined") { 424 | // Browser environment 425 | (window as any).runExamples = runExamples; 426 | } else if (require.main === module) { 427 | // Node.js environment 428 | runExamples().catch(console.error); 429 | } 430 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "jsdom", 4 | roots: ["/src"], 5 | testMatch: [ 6 | "**/__tests__/**/*.+(ts|tsx|js)", 7 | "**/*.(test|spec).+(ts|tsx|js)", 8 | ], 9 | transform: { 10 | "^.+\\.(ts|tsx)$": "ts-jest", 11 | }, 12 | collectCoverageFrom: [ 13 | "src/**/*.{ts,tsx}", 14 | "!src/**/*.d.ts", 15 | "!src/**/*.test.{ts,tsx}", 16 | "!src/**/*.spec.{ts,tsx}", 17 | ], 18 | coverageDirectory: "coverage", 19 | coverageReporters: ["text", "lcov", "html"], 20 | }; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lorenzo-coslado/fingerprinter-js", 3 | "version": "1.0.3", 4 | "description": "A modern JavaScript library for generating unique and reliable browser fingerprints with built-in bot detection", 5 | "main": "dist/index.js", 6 | "module": "dist/index.esm.js", 7 | "types": "dist/index.d.ts", 8 | "files": [ 9 | "dist/**/*", 10 | "README.md", 11 | "LICENSE" 12 | ], 13 | "scripts": { 14 | "build": "rollup -c", 15 | "dev": "rollup -c -w", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "lint": "eslint src/**/*.ts", 19 | "lint:fix": "eslint src/**/*.ts --fix", 20 | "prepublishOnly": "npm run build", 21 | "prepare": "npm run build", 22 | "publish:both": "./scripts/publish.sh", 23 | "publish:patch": "./scripts/publish.sh patch", 24 | "publish:minor": "./scripts/publish.sh minor", 25 | "publish:major": "./scripts/publish.sh major" 26 | }, 27 | "keywords": [ 28 | "fingerprinting", 29 | "browser-fingerprint", 30 | "device-detection", 31 | "fraud-detection", 32 | "bot-detection", 33 | "security", 34 | "canvas-fingerprint", 35 | "webgl-fingerprint", 36 | "audio-fingerprint", 37 | "user-tracking", 38 | "browser-identification", 39 | "typescript", 40 | "privacy", 41 | "analytics" 42 | ], 43 | "author": "Lorenzo Coslado", 44 | "license": "MIT", 45 | "repository": { 46 | "type": "git", 47 | "url": "https://github.com/Lorenzo-Coslado/fingerprinter-js.git" 48 | }, 49 | "bugs": { 50 | "url": "https://github.com/Lorenzo-Coslado/fingerprinter-js/issues" 51 | }, 52 | "homepage": "https://github.com/Lorenzo-Coslado/fingerprinter-js#readme", 53 | "publishConfig": { 54 | "registry": "https://npm.pkg.github.com/" 55 | }, 56 | "devDependencies": { 57 | "@types/jest": "^29.5.5", 58 | "@typescript-eslint/eslint-plugin": "^6.7.2", 59 | "@typescript-eslint/parser": "^6.7.2", 60 | "eslint": "^8.49.0", 61 | "jest": "^29.7.0", 62 | "jest-environment-jsdom": "^30.0.5", 63 | "rollup": "^3.29.2", 64 | "rollup-plugin-typescript2": "^0.35.0", 65 | "ts-jest": "^29.1.1", 66 | "typescript": "^5.2.2" 67 | }, 68 | "engines": { 69 | "node": ">=14.0.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const typescript = require("rollup-plugin-typescript2"); 2 | 3 | module.exports = { 4 | input: "src/index.ts", 5 | output: [ 6 | { 7 | file: "dist/index.js", 8 | format: "cjs", 9 | sourcemap: true, 10 | }, 11 | { 12 | file: "dist/index.esm.js", 13 | format: "esm", 14 | sourcemap: true, 15 | }, 16 | { 17 | file: "dist/index.umd.js", 18 | format: "umd", 19 | name: "FingerprintJS", 20 | sourcemap: true, 21 | }, 22 | ], 23 | plugins: [ 24 | typescript({ 25 | typescript: require("typescript"), 26 | clean: true, 27 | }), 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script pour publier sur NPM et GitHub Packages 4 | # Usage: ./scripts/publish.sh [patch|minor|major] 5 | 6 | set -e 7 | 8 | VERSION_TYPE=${1:-patch} 9 | 10 | echo "🚀 Publication de fingerprinter-js sur NPM et GitHub Packages" 11 | echo "Type de version: $VERSION_TYPE" 12 | 13 | # Vérifier les tokens 14 | if [ -z "$NODE_AUTH_TOKEN" ]; then 15 | echo "⚠️ Variable NODE_AUTH_TOKEN non définie." 16 | echo "💡 Pour publier sur GitHub Packages, créé un token sur:" 17 | echo " https://github.com/settings/tokens (avec permission 'write:packages')" 18 | echo "" 19 | read -p "🔑 Entre ton token GitHub (ghp_...): " GITHUB_TOKEN 20 | if [ -z "$GITHUB_TOKEN" ]; then 21 | echo "❌ Token requis pour publier sur GitHub Packages" 22 | exit 1 23 | fi 24 | export NODE_AUTH_TOKEN="$GITHUB_TOKEN" 25 | fi 26 | 27 | # Vérifier que tout est commité 28 | if [[ -n $(git status --porcelain) ]]; then 29 | echo "❌ Il y a des fichiers non commités. Veuillez commiter avant de publier." 30 | exit 1 31 | fi 32 | 33 | # Build du projet 34 | echo "📦 Build du projet..." 35 | npm run build 36 | 37 | # Tests 38 | echo "🧪 Exécution des tests..." 39 | npm test 40 | 41 | # Mise à jour de la version 42 | echo "📈 Mise à jour de la version ($VERSION_TYPE)..." 43 | npm version $VERSION_TYPE 44 | 45 | # Publication sur NPM (sans scope) 46 | echo "📤 Publication sur NPM..." 47 | 48 | # Sauvegarder le package.json original 49 | cp package.json package-original.json 50 | 51 | # Modifier temporairement le package.json pour NPM 52 | sed -i '' 's/"@lorenzo-coslado\/fingerprinter-js"/"fingerprinter-js"/' package.json 53 | sed -i '' '/"publishConfig"/,+2d' package.json 54 | 55 | # Publier sur NPM 56 | npm config set registry https://registry.npmjs.org 57 | npm publish 58 | 59 | # Restaurer le package.json original 60 | mv package-original.json package.json 61 | 62 | # Publication sur GitHub Packages (avec scope) 63 | echo "📤 Publication sur GitHub Packages..." 64 | npm config set registry https://npm.pkg.github.com 65 | npm publish 66 | 67 | # Restaurer le registre NPM par défaut 68 | npm config set registry https://registry.npmjs.org 69 | 70 | # Push des tags 71 | echo "🏷️ Push des tags..." 72 | git push origin main --tags 73 | 74 | echo "✅ Publication terminée avec succès !" 75 | echo "📦 NPM: https://www.npmjs.com/package/fingerprinter-js" 76 | echo "📦 GitHub: https://github.com/Lorenzo-Coslado/fingerprinter-js/packages" 77 | -------------------------------------------------------------------------------- /src/__tests__/fingerprint.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { Fingerprint } from "../fingerprint"; 6 | import { isBrowser } from "../utils"; 7 | 8 | // Mock some browser APIs that might not be available in jsdom 9 | beforeAll(() => { 10 | // Mock TextEncoder/TextDecoder 11 | global.TextEncoder = jest.fn().mockImplementation(() => ({ 12 | encode: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4])), 13 | })); 14 | 15 | global.TextDecoder = jest.fn().mockImplementation(() => ({ 16 | decode: jest.fn().mockReturnValue("decoded"), 17 | })); 18 | 19 | // Mock crypto for tests 20 | Object.defineProperty(global, "crypto", { 21 | value: { 22 | subtle: { 23 | digest: jest.fn().mockResolvedValue(new ArrayBuffer(32)), 24 | }, 25 | }, 26 | writable: true, 27 | }); 28 | 29 | // Mock canvas methods 30 | HTMLCanvasElement.prototype.getContext = jest.fn(() => ({ 31 | fillRect: jest.fn(), 32 | fillText: jest.fn(), 33 | arc: jest.fn(), 34 | beginPath: jest.fn(), 35 | closePath: jest.fn(), 36 | fill: jest.fn(), 37 | })) as any; 38 | 39 | HTMLCanvasElement.prototype.toDataURL = jest.fn( 40 | () => "data:image/png;base64,test" 41 | ); 42 | 43 | // Mock audio context 44 | global.AudioContext = jest.fn().mockImplementation(() => ({ 45 | sampleRate: 44100, 46 | state: "suspended", 47 | destination: { 48 | maxChannelCount: 2, 49 | channelCount: 2, 50 | channelCountMode: "max", 51 | channelInterpretation: "speakers", 52 | }, 53 | createOscillator: jest.fn(() => ({ 54 | type: "sine", 55 | frequency: { setValueAtTime: jest.fn() }, 56 | connect: jest.fn(), 57 | start: jest.fn(), 58 | stop: jest.fn(), 59 | })), 60 | createAnalyser: jest.fn(() => ({ 61 | frequencyBinCount: 1024, 62 | getFloatFrequencyData: jest.fn(), 63 | connect: jest.fn(), 64 | })), 65 | createGain: jest.fn(() => ({ 66 | gain: { setValueAtTime: jest.fn() }, 67 | connect: jest.fn(), 68 | })), 69 | createScriptProcessor: jest.fn(() => ({ 70 | onaudioprocess: null, 71 | connect: jest.fn(), 72 | })), 73 | close: jest.fn().mockResolvedValue(undefined), 74 | currentTime: 0, 75 | })); 76 | }); 77 | 78 | describe("Fingerprint", () => { 79 | beforeEach(() => { 80 | jest.clearAllMocks(); 81 | }); 82 | 83 | describe("isBrowser", () => { 84 | it("should detect browser environment", () => { 85 | expect(isBrowser()).toBe(true); 86 | }); 87 | }); 88 | 89 | describe("Fingerprint class", () => { 90 | it("should create instance with default options", () => { 91 | const fp = new Fingerprint(); 92 | expect(fp).toBeInstanceOf(Fingerprint); 93 | }); 94 | 95 | it("should create instance with custom options", () => { 96 | const options = { 97 | excludeCanvas: true, 98 | excludeWebGL: true, 99 | }; 100 | const fp = new Fingerprint(options); 101 | expect(fp).toBeInstanceOf(Fingerprint); 102 | }); 103 | 104 | it("should generate fingerprint", async () => { 105 | const fp = new Fingerprint(); 106 | const result = await fp.generate(); 107 | 108 | expect(result).toHaveProperty("fingerprint"); 109 | expect(result).toHaveProperty("components"); 110 | expect(result).toHaveProperty("confidence"); 111 | expect(typeof result.fingerprint).toBe("string"); 112 | expect(typeof result.components).toBe("object"); 113 | expect(typeof result.confidence).toBe("number"); 114 | }); 115 | 116 | it("should get components without generating hash", async () => { 117 | const fp = new Fingerprint(); 118 | const components = await fp.getComponents(); 119 | 120 | expect(typeof components).toBe("object"); 121 | expect(components).toHaveProperty("userAgent"); 122 | }); 123 | 124 | it("should include custom data when provided", async () => { 125 | const customData = { customField: "customValue" }; 126 | const fp = new Fingerprint({ customData }); 127 | const result = await fp.generate(); 128 | 129 | expect(result.components).toHaveProperty("custom"); 130 | expect(result.components.custom).toEqual(customData); 131 | }); 132 | 133 | it("should exclude components based on options", async () => { 134 | const fp = new Fingerprint({ 135 | excludeLanguage: true, 136 | excludeTimezone: true, 137 | }); 138 | const components = await fp.getComponents(); 139 | 140 | expect(components).not.toHaveProperty("language"); 141 | expect(components).not.toHaveProperty("timezone"); 142 | expect(components).toHaveProperty("userAgent"); 143 | }); 144 | }); 145 | 146 | describe("Static methods", () => { 147 | it("should generate fingerprint with static method", async () => { 148 | const result = await Fingerprint.generate(); 149 | 150 | expect(result).toHaveProperty("fingerprint"); 151 | expect(result).toHaveProperty("components"); 152 | expect(result).toHaveProperty("confidence"); 153 | }); 154 | 155 | it("should return available collectors", () => { 156 | const collectors = Fingerprint.getAvailableCollectors(); 157 | 158 | expect(Array.isArray(collectors)).toBe(true); 159 | expect(collectors.length).toBeGreaterThan(0); 160 | expect(collectors).toContain("userAgent"); 161 | expect(collectors).toContain("canvas"); 162 | }); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /src/__tests__/stability.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { Fingerprint } from "../fingerprint"; 6 | 7 | // Mock some browser APIs that might not be available in jsdom 8 | beforeAll(() => { 9 | // Mock TextEncoder/TextDecoder 10 | global.TextEncoder = jest.fn().mockImplementation(() => ({ 11 | encode: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4])), 12 | })); 13 | 14 | global.TextDecoder = jest.fn().mockImplementation(() => ({ 15 | decode: jest.fn().mockReturnValue("decoded"), 16 | })); 17 | 18 | // Mock crypto for tests 19 | Object.defineProperty(global, "crypto", { 20 | value: { 21 | subtle: { 22 | digest: jest.fn().mockImplementation(async (algorithm, data) => { 23 | // Simulate different hashes for different data 24 | const dataString = new TextDecoder().decode(data); 25 | const hash = new ArrayBuffer(32); 26 | const view = new Uint8Array(hash); 27 | 28 | // Simple hash simulation based on data content 29 | for (let i = 0; i < dataString.length && i < 32; i++) { 30 | view[i] = dataString.charCodeAt(i) % 256; 31 | } 32 | 33 | return hash; 34 | }), 35 | }, 36 | }, 37 | writable: true, 38 | }); 39 | 40 | // Mock canvas methods 41 | HTMLCanvasElement.prototype.getContext = jest.fn(() => ({ 42 | fillRect: jest.fn(), 43 | fillText: jest.fn(), 44 | arc: jest.fn(), 45 | beginPath: jest.fn(), 46 | closePath: jest.fn(), 47 | fill: jest.fn(), 48 | })) as any; 49 | 50 | HTMLCanvasElement.prototype.toDataURL = jest.fn( 51 | () => "data:image/png;base64,test" 52 | ); 53 | 54 | // Mock audio context 55 | global.AudioContext = jest.fn().mockImplementation(() => ({ 56 | sampleRate: 44100, 57 | state: "suspended", 58 | destination: { 59 | maxChannelCount: 2, 60 | channelCount: 2, 61 | channelCountMode: "max", 62 | channelInterpretation: "speakers", 63 | }, 64 | createOscillator: jest.fn(() => ({ 65 | type: "sine", 66 | frequency: { setValueAtTime: jest.fn() }, 67 | connect: jest.fn(), 68 | start: jest.fn(), 69 | stop: jest.fn(), 70 | })), 71 | createAnalyser: jest.fn(() => ({ 72 | frequencyBinCount: 1024, 73 | getFloatFrequencyData: jest.fn(), 74 | connect: jest.fn(), 75 | })), 76 | createGain: jest.fn(() => ({ 77 | gain: { setValueAtTime: jest.fn() }, 78 | connect: jest.fn(), 79 | })), 80 | createScriptProcessor: jest.fn(() => ({ 81 | onaudioprocess: null, 82 | connect: jest.fn(), 83 | })), 84 | close: jest.fn().mockResolvedValue(undefined), 85 | currentTime: 0, 86 | })); 87 | }); 88 | 89 | describe("Fingerprint Stability", () => { 90 | beforeEach(() => { 91 | jest.clearAllMocks(); 92 | }); 93 | 94 | describe("Custom data normalization", () => { 95 | it("should remove unstable timestamp data by default", async () => { 96 | const fp1 = new Fingerprint({ 97 | customData: { 98 | timestamp: Date.now(), 99 | stableValue: "test", 100 | time: 1722420569123, 101 | }, 102 | }); 103 | 104 | const fp2 = new Fingerprint({ 105 | customData: { 106 | timestamp: Date.now() + 1000, // Different timestamp 107 | stableValue: "test", 108 | time: 1722420570456, // Different time 109 | }, 110 | }); 111 | 112 | const result1 = await fp1.generate(); 113 | const result2 = await fp2.generate(); 114 | 115 | // Fingerprints should be identical despite different timestamps 116 | expect(result1.fingerprint).toBe(result2.fingerprint); 117 | 118 | // Custom data should only contain stable values 119 | expect(result1.components.custom).toEqual({ stableValue: "test" }); 120 | expect(result2.components.custom).toEqual({ stableValue: "test" }); 121 | }); 122 | 123 | it("should remove random and UUID-like data", async () => { 124 | const fp1 = new Fingerprint({ 125 | customData: { 126 | uuid: "123e4567-e89b-12d3-a456-426614174000", 127 | random: Math.random(), 128 | sessionId: "random-session-123", 129 | stableValue: "test", 130 | }, 131 | }); 132 | 133 | const fp2 = new Fingerprint({ 134 | customData: { 135 | uuid: "987e6543-e21b-12d3-a456-426614174999", 136 | random: Math.random(), 137 | sessionId: "random-session-456", 138 | stableValue: "test", 139 | }, 140 | }); 141 | 142 | const result1 = await fp1.generate(); 143 | const result2 = await fp2.generate(); 144 | 145 | // Should be identical after normalization 146 | expect(result1.fingerprint).toBe(result2.fingerprint); 147 | expect(result1.components.custom).toEqual({ stableValue: "test" }); 148 | }); 149 | 150 | it("should allow unstable data when allowUnstableData is true", async () => { 151 | const timestamp1 = Date.now(); 152 | const timestamp2 = timestamp1 + 1000; 153 | 154 | const fp1 = new Fingerprint({ 155 | allowUnstableData: true, 156 | customData: { 157 | timestamp: timestamp1, 158 | stableValue: "test", 159 | }, 160 | }); 161 | 162 | const fp2 = new Fingerprint({ 163 | allowUnstableData: true, 164 | customData: { 165 | timestamp: timestamp2, 166 | stableValue: "test", 167 | }, 168 | }); 169 | 170 | const components1 = await fp1.getComponents(); 171 | const components2 = await fp2.getComponents(); 172 | 173 | // Custom data should include timestamps when allowUnstableData is true 174 | expect(components1.custom.timestamp).toBe(timestamp1); 175 | expect(components2.custom.timestamp).toBe(timestamp2); 176 | expect(components1.custom.stableValue).toBe("test"); 177 | expect(components2.custom.stableValue).toBe("test"); 178 | }); 179 | 180 | it("should generate identical fingerprints with identical data", async () => { 181 | const fp1 = new Fingerprint({ 182 | customData: { 183 | version: "1.0", 184 | app: "test", 185 | }, 186 | }); 187 | 188 | const fp2 = new Fingerprint({ 189 | customData: { 190 | version: "1.0", 191 | app: "test", 192 | }, 193 | }); 194 | 195 | const result1 = await fp1.generate(); 196 | const result2 = await fp2.generate(); 197 | 198 | expect(result1.fingerprint).toBe(result2.fingerprint); 199 | }); 200 | 201 | it("should handle empty or missing custom data", async () => { 202 | const fp1 = new Fingerprint({ customData: {} }); 203 | const fp2 = new Fingerprint({ customData: undefined }); 204 | const fp3 = new Fingerprint({}); 205 | 206 | const result1 = await fp1.generate(); 207 | const result2 = await fp2.generate(); 208 | const result3 = await fp3.generate(); 209 | 210 | // All should be identical when no custom data 211 | expect(result1.fingerprint).toBe(result2.fingerprint); 212 | expect(result2.fingerprint).toBe(result3.fingerprint); 213 | }); 214 | }); 215 | }); 216 | -------------------------------------------------------------------------------- /src/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { safeGet, safeStringify, sha256, simpleHash } from "../utils"; 2 | 3 | describe("Utils", () => { 4 | describe("simpleHash", () => { 5 | it("should generate consistent hash for same input", () => { 6 | const input = "test string"; 7 | const hash1 = simpleHash(input); 8 | const hash2 = simpleHash(input); 9 | 10 | expect(hash1).toBe(hash2); 11 | expect(typeof hash1).toBe("number"); 12 | }); 13 | 14 | it("should generate different hashes for different inputs", () => { 15 | const hash1 = simpleHash("string1"); 16 | const hash2 = simpleHash("string2"); 17 | 18 | expect(hash1).not.toBe(hash2); 19 | }); 20 | 21 | it("should handle empty string", () => { 22 | const hash = simpleHash(""); 23 | expect(hash).toBe(0); 24 | }); 25 | }); 26 | 27 | describe("sha256", () => { 28 | it("should generate hash string", async () => { 29 | const hash = await sha256("test"); 30 | expect(typeof hash).toBe("string"); 31 | expect(hash.length).toBeGreaterThan(0); 32 | }); 33 | 34 | it("should generate consistent hash for same input", async () => { 35 | const input = "consistent test"; 36 | const hash1 = await sha256(input); 37 | const hash2 = await sha256(input); 38 | 39 | expect(hash1).toBe(hash2); 40 | }); 41 | }); 42 | 43 | describe("safeStringify", () => { 44 | it("should stringify regular objects", () => { 45 | const obj = { a: 1, b: "test" }; 46 | const result = safeStringify(obj); 47 | 48 | expect(result).toBe('{"a":1,"b":"test"}'); 49 | }); 50 | 51 | it("should handle circular references", () => { 52 | const obj: any = { a: 1 }; 53 | obj.circular = obj; 54 | 55 | const result = safeStringify(obj); 56 | expect(result).toContain("[Circular]"); 57 | }); 58 | 59 | it("should handle arrays", () => { 60 | const arr = [1, 2, 3]; 61 | const result = safeStringify(arr); 62 | 63 | expect(result).toBe("[1,2,3]"); 64 | }); 65 | }); 66 | 67 | describe("safeGet", () => { 68 | it("should return result when function succeeds", () => { 69 | const result = safeGet(() => "success", "default"); 70 | expect(result).toBe("success"); 71 | }); 72 | 73 | it("should return default when function throws", () => { 74 | const result = safeGet(() => { 75 | throw new Error("test error"); 76 | }, "default"); 77 | expect(result).toBe("default"); 78 | }); 79 | 80 | it("should work with complex operations", () => { 81 | const result = safeGet(() => { 82 | const obj = { nested: { value: 42 } }; 83 | return obj.nested.value * 2; 84 | }, 0); 85 | expect(result).toBe(84); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/collectors/advanced.ts: -------------------------------------------------------------------------------- 1 | import { ComponentCollector } from "../types"; 2 | import { safeGet } from "../utils"; 3 | 4 | export class AudioCollector implements ComponentCollector { 5 | name = "audio"; 6 | 7 | async collect(): Promise { 8 | return safeGet(async () => { 9 | // Check if AudioContext is available 10 | const AudioContext = 11 | window.AudioContext || (window as any).webkitAudioContext; 12 | if (!AudioContext) { 13 | return { error: "no-audio-context" }; 14 | } 15 | 16 | const audioContext = new AudioContext(); 17 | const result: Record = {}; 18 | 19 | try { 20 | // Get basic audio context info 21 | result.sampleRate = audioContext.sampleRate; 22 | result.state = audioContext.state; 23 | result.maxChannelCount = audioContext.destination.maxChannelCount; 24 | result.channelCount = audioContext.destination.channelCount; 25 | result.channelCountMode = audioContext.destination.channelCountMode; 26 | result.channelInterpretation = 27 | audioContext.destination.channelInterpretation; 28 | 29 | // Create audio fingerprint using oscillator 30 | const oscillator = audioContext.createOscillator(); 31 | const analyser = audioContext.createAnalyser(); 32 | const gainNode = audioContext.createGain(); 33 | const scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1); 34 | 35 | gainNode.gain.setValueAtTime(0, audioContext.currentTime); 36 | oscillator.type = "triangle"; 37 | oscillator.frequency.setValueAtTime(10000, audioContext.currentTime); 38 | 39 | oscillator.connect(analyser); 40 | analyser.connect(scriptProcessor); 41 | scriptProcessor.connect(gainNode); 42 | gainNode.connect(audioContext.destination); 43 | 44 | scriptProcessor.onaudioprocess = () => { 45 | const freqData = new Float32Array(analyser.frequencyBinCount); 46 | analyser.getFloatFrequencyData(freqData); 47 | 48 | let sum = 0; 49 | for (let i = 0; i < freqData.length; i++) { 50 | sum += Math.abs(freqData[i]); 51 | } 52 | result.audioFingerprint = sum.toString(); 53 | }; 54 | 55 | oscillator.start(audioContext.currentTime); 56 | oscillator.stop(audioContext.currentTime + 0.1); 57 | 58 | // Wait a bit for processing 59 | await new Promise((resolve) => setTimeout(resolve, 100)); 60 | 61 | // Clean up 62 | await audioContext.close(); 63 | 64 | return result; 65 | } catch (error) { 66 | try { 67 | await audioContext.close(); 68 | } catch {} 69 | return { 70 | error: "audio-processing-failed", 71 | details: (error as Error).message, 72 | }; 73 | } 74 | }, Promise.resolve({ error: "audio-not-available" })); 75 | } 76 | } 77 | 78 | export class FontsCollector implements ComponentCollector { 79 | name = "fonts"; 80 | 81 | collect(): string[] { 82 | return safeGet(() => { 83 | const testFonts = [ 84 | "Arial", 85 | "Arial Black", 86 | "Arial Narrow", 87 | "Arial Rounded MT Bold", 88 | "Bookman Old Style", 89 | "Bradley Hand ITC", 90 | "Century", 91 | "Century Gothic", 92 | "Comic Sans MS", 93 | "Courier", 94 | "Courier New", 95 | "Georgia", 96 | "Gentium", 97 | "Helvetica", 98 | "Helvetica Neue", 99 | "Impact", 100 | "King", 101 | "Lucida Console", 102 | "Lalit", 103 | "Modena", 104 | "Monotype Corsiva", 105 | "Papyrus", 106 | "Tahoma", 107 | "TeX", 108 | "Times", 109 | "Times New Roman", 110 | "Trebuchet MS", 111 | "Verdana", 112 | "Verona", 113 | ]; 114 | 115 | const availableFonts: string[] = []; 116 | const testString = "mmmmmmmmmmlli"; 117 | const testSize = "72px"; 118 | const h = document.getElementsByTagName("body")[0]; 119 | 120 | // Create a test span element 121 | const s = document.createElement("span"); 122 | s.style.fontSize = testSize; 123 | s.style.position = "absolute"; 124 | s.style.left = "-9999px"; 125 | s.style.visibility = "hidden"; 126 | s.innerHTML = testString; 127 | h.appendChild(s); 128 | 129 | // Get default width and height 130 | s.style.fontFamily = "monospace"; 131 | const defaultWidth = s.offsetWidth; 132 | const defaultHeight = s.offsetHeight; 133 | 134 | // Test each font 135 | for (const font of testFonts) { 136 | s.style.fontFamily = `'${font}', monospace`; 137 | if ( 138 | s.offsetWidth !== defaultWidth || 139 | s.offsetHeight !== defaultHeight 140 | ) { 141 | availableFonts.push(font); 142 | } 143 | } 144 | 145 | // Clean up 146 | h.removeChild(s); 147 | 148 | return availableFonts; 149 | }, []); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/collectors/basic.ts: -------------------------------------------------------------------------------- 1 | import { ComponentCollector } from "../types"; 2 | import { safeGet } from "../utils"; 3 | 4 | export class UserAgentCollector implements ComponentCollector { 5 | name = "userAgent"; 6 | 7 | collect(): string { 8 | return safeGet(() => navigator.userAgent, "unknown"); 9 | } 10 | } 11 | 12 | export class LanguageCollector implements ComponentCollector { 13 | name = "language"; 14 | 15 | collect(): string[] { 16 | return safeGet(() => { 17 | const languages = []; 18 | if (navigator.language) { 19 | languages.push(navigator.language); 20 | } 21 | if (navigator.languages) { 22 | languages.push(...navigator.languages); 23 | } 24 | return [...new Set(languages)]; // Remove duplicates 25 | }, ["unknown"]); 26 | } 27 | } 28 | 29 | export class TimezoneCollector implements ComponentCollector { 30 | name = "timezone"; 31 | 32 | collect(): string { 33 | return safeGet(() => { 34 | return Intl.DateTimeFormat().resolvedOptions().timeZone; 35 | }, "unknown"); 36 | } 37 | } 38 | 39 | export class ScreenCollector implements ComponentCollector { 40 | name = "screen"; 41 | 42 | collect(): object { 43 | return safeGet( 44 | () => ({ 45 | width: screen.width, 46 | height: screen.height, 47 | availWidth: screen.availWidth, 48 | availHeight: screen.availHeight, 49 | colorDepth: screen.colorDepth, 50 | pixelDepth: screen.pixelDepth, 51 | devicePixelRatio: window.devicePixelRatio || 1, 52 | }), 53 | { 54 | width: 0, 55 | height: 0, 56 | availWidth: 0, 57 | availHeight: 0, 58 | colorDepth: 0, 59 | pixelDepth: 0, 60 | devicePixelRatio: 1, 61 | } 62 | ); 63 | } 64 | } 65 | 66 | export class PluginsCollector implements ComponentCollector { 67 | name = "plugins"; 68 | 69 | collect(): Array<{ name: string; description: string; filename: string }> { 70 | return safeGet(() => { 71 | const plugins = []; 72 | for (let i = 0; i < navigator.plugins.length; i++) { 73 | const plugin = navigator.plugins[i]; 74 | plugins.push({ 75 | name: plugin.name, 76 | description: plugin.description, 77 | filename: plugin.filename, 78 | }); 79 | } 80 | return plugins; 81 | }, []); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/collectors/canvas.ts: -------------------------------------------------------------------------------- 1 | import { ComponentCollector } from "../types"; 2 | import { safeGet } from "../utils"; 3 | 4 | export class CanvasCollector implements ComponentCollector { 5 | name = "canvas"; 6 | 7 | collect(): string { 8 | return safeGet(() => { 9 | const canvas = document.createElement("canvas"); 10 | const ctx = canvas.getContext("2d"); 11 | 12 | if (!ctx) return "no-canvas-context"; 13 | 14 | // Set canvas size 15 | canvas.width = 200; 16 | canvas.height = 50; 17 | 18 | // Draw text with various styles 19 | ctx.textBaseline = "top"; 20 | ctx.font = "14px Arial"; 21 | ctx.fillStyle = "#f60"; 22 | ctx.fillRect(125, 1, 62, 20); 23 | 24 | ctx.fillStyle = "#069"; 25 | ctx.fillText("Canvas fingerprint 🎨", 2, 15); 26 | 27 | ctx.fillStyle = "rgba(102, 204, 0, 0.7)"; 28 | ctx.fillText("Canvas fingerprint 🎨", 4, 17); 29 | 30 | // Draw some shapes 31 | ctx.globalCompositeOperation = "multiply"; 32 | ctx.fillStyle = "rgb(255,0,255)"; 33 | ctx.beginPath(); 34 | ctx.arc(50, 50, 50, 0, Math.PI * 2, true); 35 | ctx.closePath(); 36 | ctx.fill(); 37 | 38 | ctx.fillStyle = "rgb(0,255,255)"; 39 | ctx.beginPath(); 40 | ctx.arc(100, 50, 50, 0, Math.PI * 2, true); 41 | ctx.closePath(); 42 | ctx.fill(); 43 | 44 | ctx.fillStyle = "rgb(255,255,0)"; 45 | ctx.beginPath(); 46 | ctx.arc(75, 100, 50, 0, Math.PI * 2, true); 47 | ctx.closePath(); 48 | ctx.fill(); 49 | 50 | return canvas.toDataURL(); 51 | }, "no-canvas"); 52 | } 53 | } 54 | 55 | export class WebGLCollector implements ComponentCollector { 56 | name = "webgl"; 57 | 58 | collect(): object { 59 | return safeGet( 60 | () => { 61 | const canvas = document.createElement("canvas"); 62 | const gl = 63 | canvas.getContext("webgl") || 64 | (canvas.getContext( 65 | "experimental-webgl" 66 | ) as WebGLRenderingContext | null); 67 | 68 | if (!gl) return { error: "no-webgl-context" }; 69 | 70 | const result: Record = {}; 71 | 72 | // Get basic WebGL info 73 | result.vendor = gl.getParameter(gl.VENDOR); 74 | result.renderer = gl.getParameter(gl.RENDERER); 75 | result.version = gl.getParameter(gl.VERSION); 76 | result.shadingLanguageVersion = gl.getParameter( 77 | gl.SHADING_LANGUAGE_VERSION 78 | ); 79 | 80 | // Get supported extensions 81 | const extensions = gl.getSupportedExtensions(); 82 | result.extensions = extensions ? extensions.sort() : []; 83 | 84 | // Get additional parameters 85 | result.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); 86 | result.maxViewportDims = gl.getParameter(gl.MAX_VIEWPORT_DIMS); 87 | result.maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS); 88 | result.maxVertexUniformVectors = gl.getParameter( 89 | gl.MAX_VERTEX_UNIFORM_VECTORS 90 | ); 91 | result.maxFragmentUniformVectors = gl.getParameter( 92 | gl.MAX_FRAGMENT_UNIFORM_VECTORS 93 | ); 94 | result.maxVaryingVectors = gl.getParameter(gl.MAX_VARYING_VECTORS); 95 | 96 | // Get unmasked info if available 97 | const debugInfo = gl.getExtension("WEBGL_debug_renderer_info"); 98 | if (debugInfo) { 99 | result.unmaskedVendor = gl.getParameter( 100 | debugInfo.UNMASKED_VENDOR_WEBGL 101 | ); 102 | result.unmaskedRenderer = gl.getParameter( 103 | debugInfo.UNMASKED_RENDERER_WEBGL 104 | ); 105 | } 106 | 107 | return result; 108 | }, 109 | { error: "webgl-not-available" } 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/fingerprint.ts: -------------------------------------------------------------------------------- 1 | import { SuspectAnalyzer } from "./suspect-analyzer"; 2 | import { 3 | ComponentCollector, 4 | FingerprintOptions, 5 | FingerprintResult, 6 | } from "./types"; 7 | import { isBrowser, safeStringify, sha256 } from "./utils"; 8 | 9 | // Import all collectors 10 | import { AudioCollector, FontsCollector } from "./collectors/advanced"; 11 | import { 12 | LanguageCollector, 13 | PluginsCollector, 14 | ScreenCollector, 15 | TimezoneCollector, 16 | UserAgentCollector, 17 | } from "./collectors/basic"; 18 | import { CanvasCollector, WebGLCollector } from "./collectors/canvas"; 19 | 20 | /** 21 | * Main Fingerprint class 22 | */ 23 | export class Fingerprint { 24 | private collectors: ComponentCollector[] = []; 25 | private options: FingerprintOptions; 26 | 27 | constructor(options: FingerprintOptions = {}) { 28 | this.options = options; 29 | this.initializeCollectors(); 30 | } 31 | 32 | private initializeCollectors(): void { 33 | // Always include basic collectors 34 | this.collectors.push(new UserAgentCollector()); 35 | 36 | if (!this.options.excludeLanguage) { 37 | this.collectors.push(new LanguageCollector()); 38 | } 39 | 40 | if (!this.options.excludeTimezone) { 41 | this.collectors.push(new TimezoneCollector()); 42 | } 43 | 44 | if (!this.options.excludeScreenResolution) { 45 | this.collectors.push(new ScreenCollector()); 46 | } 47 | 48 | if (!this.options.excludePlugins) { 49 | this.collectors.push(new PluginsCollector()); 50 | } 51 | 52 | if (!this.options.excludeCanvas) { 53 | this.collectors.push(new CanvasCollector()); 54 | } 55 | 56 | if (!this.options.excludeWebGL) { 57 | this.collectors.push(new WebGLCollector()); 58 | } 59 | 60 | if (!this.options.excludeAudio) { 61 | this.collectors.push(new AudioCollector()); 62 | } 63 | 64 | if (!this.options.excludeFonts) { 65 | this.collectors.push(new FontsCollector()); 66 | } 67 | } 68 | 69 | /** 70 | * Normalize custom data to ensure stability by removing temporal values 71 | */ 72 | private normalizeCustomData(data: Record): Record { 73 | if (!data || typeof data !== "object") { 74 | return data; 75 | } 76 | 77 | const normalized = { ...data }; 78 | 79 | // List of keys that are known to cause instability 80 | const unstableKeys = [ 81 | "timestamp", 82 | "time", 83 | "now", 84 | "date", 85 | "random", 86 | "rand", 87 | "nonce", 88 | "salt", 89 | "sessionId", 90 | "requestId", 91 | "uuid", 92 | "performance", 93 | "timing", 94 | ]; 95 | 96 | // Remove unstable keys 97 | for (const key of unstableKeys) { 98 | if (key in normalized) { 99 | delete normalized[key]; 100 | } 101 | } 102 | 103 | // Also check for values that look like timestamps or random data 104 | for (const [key, value] of Object.entries(normalized)) { 105 | if (typeof value === "number") { 106 | // Remove values that look like timestamps (very large numbers) 107 | if (value > 1000000000000 && value < 9999999999999) { 108 | // Likely timestamp 109 | delete normalized[key]; 110 | } 111 | // Remove values that change too rapidly (potential random numbers) 112 | if (Math.random && value === Math.random()) { 113 | delete normalized[key]; 114 | } 115 | } 116 | 117 | if (typeof value === "string") { 118 | // Remove UUIDs or similar patterns 119 | if ( 120 | /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( 121 | value 122 | ) 123 | ) { 124 | delete normalized[key]; 125 | } 126 | } 127 | } 128 | 129 | return normalized; 130 | } 131 | 132 | /** 133 | * Generate a fingerprint 134 | */ 135 | async generate(): Promise { 136 | if (!isBrowser()) { 137 | throw new Error( 138 | "Fingerprinting is only available in browser environments" 139 | ); 140 | } 141 | 142 | const components: Record = {}; 143 | let confidence = 0; 144 | 145 | // Collect all components 146 | for (const collector of this.collectors) { 147 | try { 148 | const data = await collector.collect(); 149 | components[collector.name] = data; 150 | 151 | // Calculate confidence based on available components 152 | if (data && data !== "unknown" && data !== "no-canvas" && !data.error) { 153 | confidence += 1; 154 | } 155 | } catch (error) { 156 | components[collector.name] = { error: (error as Error).message }; 157 | } 158 | } 159 | 160 | // Add custom data if provided 161 | if (this.options.customData) { 162 | let customDataToUse = this.options.customData; 163 | 164 | // Normalize custom data unless explicitly disabled 165 | if (!this.options.allowUnstableData) { 166 | customDataToUse = this.normalizeCustomData(this.options.customData); 167 | } 168 | 169 | if (Object.keys(customDataToUse).length > 0) { 170 | components.custom = customDataToUse; 171 | confidence += 0.5; 172 | } 173 | } 174 | 175 | // Calculate confidence percentage 176 | const maxConfidence = 177 | this.collectors.length + (this.options.customData ? 0.5 : 0); 178 | const confidencePercentage = Math.round((confidence / maxConfidence) * 100); 179 | 180 | // Generate fingerprint hash 181 | const dataString = safeStringify(components); 182 | const fingerprint = await sha256(dataString); 183 | 184 | // Generate suspect analysis if requested 185 | let suspectAnalysis = undefined; 186 | if (this.options.includeSuspectAnalysis) { 187 | suspectAnalysis = SuspectAnalyzer.analyze(components); 188 | } 189 | 190 | return { 191 | fingerprint, 192 | components, 193 | confidence: confidencePercentage, 194 | suspectAnalysis, 195 | }; 196 | } 197 | 198 | /** 199 | * Get component data without generating hash 200 | */ 201 | async getComponents(): Promise> { 202 | if (!isBrowser()) { 203 | throw new Error( 204 | "Fingerprinting is only available in browser environments" 205 | ); 206 | } 207 | 208 | const components: Record = {}; 209 | 210 | for (const collector of this.collectors) { 211 | try { 212 | const data = await collector.collect(); 213 | components[collector.name] = data; 214 | } catch (error) { 215 | components[collector.name] = { error: (error as Error).message }; 216 | } 217 | } 218 | 219 | if (this.options.customData) { 220 | let customDataToUse = this.options.customData; 221 | 222 | // Normalize custom data unless explicitly disabled 223 | if (!this.options.allowUnstableData) { 224 | customDataToUse = this.normalizeCustomData(this.options.customData); 225 | } 226 | 227 | if (Object.keys(customDataToUse).length > 0) { 228 | components.custom = customDataToUse; 229 | } 230 | } 231 | 232 | return components; 233 | } 234 | 235 | /** 236 | * Static method to quickly generate a fingerprint with default options 237 | */ 238 | static async generate( 239 | options: FingerprintOptions = {} 240 | ): Promise { 241 | const fp = new Fingerprint(options); 242 | return fp.generate(); 243 | } 244 | 245 | /** 246 | * Static method to get available collectors 247 | */ 248 | static getAvailableCollectors(): string[] { 249 | return [ 250 | "userAgent", 251 | "language", 252 | "timezone", 253 | "screen", 254 | "plugins", 255 | "canvas", 256 | "webgl", 257 | "audio", 258 | "fonts", 259 | ]; 260 | } 261 | } 262 | 263 | // Export for convenience 264 | export default Fingerprint; 265 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Main exports 2 | export { Fingerprint as default, Fingerprint } from "./fingerprint"; 3 | 4 | // Type exports 5 | export type { 6 | ComponentCollector, 7 | FingerprintOptions, 8 | FingerprintResult, 9 | } from "./types"; 10 | 11 | // Utility exports 12 | export { isBrowser, safeGet, safeStringify, sha256, simpleHash } from "./utils"; 13 | 14 | // Collector exports for advanced usage 15 | export { 16 | LanguageCollector, 17 | PluginsCollector, 18 | ScreenCollector, 19 | TimezoneCollector, 20 | UserAgentCollector, 21 | } from "./collectors/basic"; 22 | 23 | export { CanvasCollector, WebGLCollector } from "./collectors/canvas"; 24 | 25 | export { AudioCollector, FontsCollector } from "./collectors/advanced"; 26 | -------------------------------------------------------------------------------- /src/suspect-analyzer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Suspect Score Analyzer 3 | * Analyzes suspicious signals in browser fingerprints 4 | */ 5 | 6 | export interface SuspectAnalysis { 7 | score: number; // 0-100 (0 = légitime, 100 = très suspect) 8 | signals: SuspectSignal[]; 9 | riskLevel: "LOW" | "MEDIUM" | "HIGH"; 10 | details: Record; 11 | } 12 | 13 | export interface SuspectSignal { 14 | type: string; 15 | severity: number; // 0-10 16 | description: string; 17 | detected: boolean; 18 | } 19 | 20 | export class SuspectAnalyzer { 21 | /** 22 | * Analyzes a fingerprint to detect suspicious signals 23 | */ 24 | static analyze(components: Record): SuspectAnalysis { 25 | const signals: SuspectSignal[] = []; 26 | let totalScore = 0; 27 | 28 | // 1. Automation detection 29 | const automationSignals = this.checkAutomation(components); 30 | signals.push(...automationSignals); 31 | 32 | // 2. Technical inconsistencies 33 | const consistencySignals = this.checkConsistency(components); 34 | signals.push(...consistencySignals); 35 | 36 | // 3. Suspicious environment 37 | const environmentSignals = this.checkEnvironment(components); 38 | signals.push(...environmentSignals); 39 | 40 | // 4. Bot patterns 41 | const botSignals = this.checkBotPatterns(components); 42 | signals.push(...botSignals); 43 | 44 | // Calcul du score total 45 | const detectedSignals = signals.filter((s) => s.detected); 46 | totalScore = detectedSignals.reduce( 47 | (sum, signal) => sum + signal.severity * 10, 48 | 0 49 | ); 50 | totalScore = Math.min(100, totalScore); // Cap à 100 51 | 52 | const riskLevel = this.calculateRiskLevel(totalScore); 53 | 54 | return { 55 | score: totalScore, 56 | signals: detectedSignals, 57 | riskLevel, 58 | details: { 59 | totalSignalsDetected: detectedSignals.length, 60 | highSeveritySignals: detectedSignals.filter((s) => s.severity >= 8) 61 | .length, 62 | automationDetected: automationSignals.some((s) => s.detected), 63 | inconsistenciesFound: consistencySignals.some((s) => s.detected), 64 | }, 65 | }; 66 | } 67 | 68 | /** 69 | * Detects automation signals 70 | */ 71 | private static checkAutomation( 72 | components: Record 73 | ): SuspectSignal[] { 74 | const signals: SuspectSignal[] = []; 75 | 76 | // WebDriver detection 77 | signals.push({ 78 | type: "webdriver", 79 | severity: 9, 80 | description: "WebDriver automation detected", 81 | detected: this.hasWebDriver(), 82 | }); 83 | 84 | // Headless browser 85 | signals.push({ 86 | type: "headless", 87 | severity: 8, 88 | description: "Headless browser detected", 89 | detected: this.isHeadless(components), 90 | }); 91 | 92 | // Phantom/Automation signatures 93 | signals.push({ 94 | type: "phantom", 95 | severity: 7, 96 | description: "PhantomJS signatures detected", 97 | detected: this.hasPhantomSignatures(components), 98 | }); 99 | 100 | // Selenium signatures 101 | signals.push({ 102 | type: "selenium", 103 | severity: 8, 104 | description: "Selenium signatures detected", 105 | detected: this.hasSeleniumSignatures(), 106 | }); 107 | 108 | return signals; 109 | } 110 | 111 | /** 112 | * Checks data consistency 113 | */ 114 | private static checkConsistency( 115 | components: Record 116 | ): SuspectSignal[] { 117 | const signals: SuspectSignal[] = []; 118 | 119 | // Timezone vs Language inconsistency 120 | signals.push({ 121 | type: "timezone_language", 122 | severity: 5, 123 | description: "Timezone/language inconsistency", 124 | detected: this.hasTimezoneLanguageInconsistency(components), 125 | }); 126 | 127 | // Screen resolution inconsistency 128 | signals.push({ 129 | type: "screen_consistency", 130 | severity: 6, 131 | description: "Screen resolution inconsistency", 132 | detected: this.hasScreenInconsistency(components), 133 | }); 134 | 135 | // Canvas fingerprint too generic 136 | signals.push({ 137 | type: "generic_canvas", 138 | severity: 4, 139 | description: "Canvas fingerprint too generic", 140 | detected: this.hasGenericCanvas(components), 141 | }); 142 | 143 | return signals; 144 | } 145 | 146 | /** 147 | * Checks execution environment 148 | */ 149 | private static checkEnvironment( 150 | components: Record 151 | ): SuspectSignal[] { 152 | const signals: SuspectSignal[] = []; 153 | 154 | // Missing expected APIs 155 | signals.push({ 156 | type: "missing_apis", 157 | severity: 6, 158 | description: "Missing browser APIs", 159 | detected: this.hasMissingAPIs(components), 160 | }); 161 | 162 | // Too many errors in collection 163 | signals.push({ 164 | type: "collection_errors", 165 | severity: 5, 166 | description: "Too many collection errors", 167 | detected: this.hasTooManyErrors(components), 168 | }); 169 | 170 | // Suspicious user agent 171 | signals.push({ 172 | type: "suspicious_ua", 173 | severity: 7, 174 | description: "Suspicious user agent", 175 | detected: this.hasSuspiciousUserAgent(components), 176 | }); 177 | 178 | return signals; 179 | } 180 | 181 | /** 182 | * Detects bot patterns 183 | */ 184 | private static checkBotPatterns( 185 | components: Record 186 | ): SuspectSignal[] { 187 | const signals: SuspectSignal[] = []; 188 | 189 | // Perfect fingerprint (too stable) 190 | signals.push({ 191 | type: "too_perfect", 192 | severity: 3, 193 | description: "Fingerprint too perfect/stable", 194 | detected: this.isTooStable(components), 195 | }); 196 | 197 | // Known bot signatures 198 | signals.push({ 199 | type: "bot_signature", 200 | severity: 9, 201 | description: "Known bot signature detected", 202 | detected: this.hasKnownBotSignature(components), 203 | }); 204 | 205 | return signals; 206 | } 207 | 208 | // Specific detection methods 209 | private static hasWebDriver(): boolean { 210 | return ( 211 | typeof window !== "undefined" && 212 | (window.navigator as any).webdriver === true 213 | ); 214 | } 215 | 216 | private static isHeadless(components: Record): boolean { 217 | const ua = components.userAgent || ""; 218 | return ( 219 | ua.includes("HeadlessChrome") || 220 | ua.includes("PhantomJS") || 221 | (typeof window !== "undefined" && window.outerHeight === 0) 222 | ); 223 | } 224 | 225 | private static hasPhantomSignatures( 226 | components: Record 227 | ): boolean { 228 | const ua = components.userAgent || ""; 229 | return ( 230 | ua.includes("PhantomJS") || 231 | (typeof window !== "undefined" && (window as any)._phantom) 232 | ); 233 | } 234 | 235 | private static hasSeleniumSignatures(): boolean { 236 | if (typeof window === "undefined") return false; 237 | 238 | return !!( 239 | (window as any).selenium || 240 | (window as any).webdriver || 241 | (window.document as any).selenium || 242 | (window.document as any).webdriver || 243 | (window.navigator as any).webdriver 244 | ); 245 | } 246 | 247 | private static hasTimezoneLanguageInconsistency( 248 | components: Record 249 | ): boolean { 250 | const timezone = components.timezone; 251 | const language = components.language; 252 | 253 | if (!timezone || !language) return false; 254 | 255 | // Simplified logic - in a real system, use a database 256 | // of timezone/country/language mappings 257 | const suspiciousCombinations = [ 258 | { tz: "America/New_York", lang: "zh-CN" }, 259 | { tz: "Europe/Paris", lang: "ja-JP" }, 260 | { tz: "Asia/Tokyo", lang: "es-ES" }, 261 | ]; 262 | 263 | return suspiciousCombinations.some( 264 | (combo) => 265 | timezone.includes(combo.tz) && 266 | Array.isArray(language) && 267 | language.some((l) => l.includes(combo.lang)) 268 | ); 269 | } 270 | 271 | private static hasScreenInconsistency( 272 | components: Record 273 | ): boolean { 274 | const screen = components.screen; 275 | if (!screen) return false; 276 | 277 | // Résolutions trop parfaites ou communes aux bots 278 | const suspiciousResolutions = [ 279 | "1024x768", 280 | "800x600", 281 | "1280x720", 282 | "1920x1080", 283 | ]; 284 | 285 | const resolution = `${screen.width}x${screen.height}`; 286 | return ( 287 | suspiciousResolutions.includes(resolution) && screen.colorDepth === 24 288 | ); // Combinaison suspecte 289 | } 290 | 291 | private static hasGenericCanvas(components: Record): boolean { 292 | const canvas = components.canvas; 293 | if (!canvas || typeof canvas !== "string") return false; 294 | 295 | // MD5 hashes of very common canvas (bots) 296 | const commonCanvasHashes = [ 297 | "e3b0c44298fc1c149afbf4c8996fb924", // Empty canvas 298 | "da39a3ee5e6b4b0d3255bfef95601890", // Generic canvas 299 | ]; 300 | 301 | // Simplified - in a real system, hash the canvas 302 | return canvas.length < 100 || canvas === "no-canvas"; 303 | } 304 | 305 | private static hasMissingAPIs(components: Record): boolean { 306 | const expectedAPIs = ["userAgent", "language", "screen"]; 307 | const missingAPIs = expectedAPIs.filter( 308 | (api) => !components[api] || components[api].error 309 | ); 310 | 311 | return missingAPIs.length > 1; 312 | } 313 | 314 | private static hasTooManyErrors(components: Record): boolean { 315 | const errorCount = Object.values(components).filter( 316 | (value) => value && typeof value === "object" && value.error 317 | ).length; 318 | 319 | return errorCount > 3; 320 | } 321 | 322 | private static hasSuspiciousUserAgent( 323 | components: Record 324 | ): boolean { 325 | const ua = components.userAgent || ""; 326 | const suspiciousPatterns = [ 327 | "HeadlessChrome", 328 | "PhantomJS", 329 | "bot", 330 | "crawler", 331 | "spider", 332 | "scraper", 333 | ]; 334 | 335 | return suspiciousPatterns.some((pattern) => 336 | ua.toLowerCase().includes(pattern.toLowerCase()) 337 | ); 338 | } 339 | 340 | private static isTooStable(components: Record): boolean { 341 | // Si tous les composants sont parfaits, c'est suspect 342 | const perfectComponents = Object.values(components).filter( 343 | (value) => value && !value.error && value !== "unknown" 344 | ).length; 345 | 346 | return perfectComponents === Object.keys(components).length; 347 | } 348 | 349 | private static hasKnownBotSignature( 350 | components: Record 351 | ): boolean { 352 | const ua = components.userAgent || ""; 353 | 354 | // Database of known bot signatures 355 | const botSignatures = [ 356 | "Googlebot", 357 | "Bingbot", 358 | "facebookexternalhit", 359 | "Twitterbot", 360 | "LinkedInBot", 361 | "WhatsApp", 362 | "python-requests", 363 | "curl/", 364 | "wget", 365 | ]; 366 | 367 | return botSignatures.some((signature) => ua.includes(signature)); 368 | } 369 | 370 | private static calculateRiskLevel(score: number): "LOW" | "MEDIUM" | "HIGH" { 371 | if (score < 30) return "LOW"; 372 | if (score < 70) return "MEDIUM"; 373 | return "HIGH"; 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { SuspectAnalysis } from "./suspect-analyzer"; 2 | 3 | /** 4 | * Interface for fingerprint options 5 | */ 6 | export interface FingerprintOptions { 7 | excludeScreenResolution?: boolean; 8 | excludeTimezone?: boolean; 9 | excludeLanguage?: boolean; 10 | excludeCanvas?: boolean; 11 | excludeWebGL?: boolean; 12 | excludeAudio?: boolean; 13 | excludePlugins?: boolean; 14 | excludeFonts?: boolean; 15 | customData?: Record; 16 | allowUnstableData?: boolean; // Allow including temporal data (default: false) 17 | includeSuspectAnalysis?: boolean; // Include suspect analysis (default: false) 18 | } 19 | 20 | /** 21 | * Interface for fingerprint result 22 | */ 23 | export interface FingerprintResult { 24 | fingerprint: string; 25 | components: Record; 26 | confidence: number; 27 | suspectAnalysis?: SuspectAnalysis; // Optional suspect analysis 28 | } 29 | 30 | /** 31 | * Interface for component collectors 32 | */ 33 | export interface ComponentCollector { 34 | name: string; 35 | collect(): Promise | any; 36 | } 37 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility functions for fingerprinting 3 | */ 4 | 5 | /** 6 | * Simple hash function to convert string to number 7 | */ 8 | export function simpleHash(str: string): number { 9 | let hash = 0; 10 | if (str.length === 0) return hash; 11 | 12 | for (let i = 0; i < str.length; i++) { 13 | const char = str.charCodeAt(i); 14 | hash = (hash << 5) - hash + char; 15 | hash = hash & hash; // Convert to 32-bit integer 16 | } 17 | 18 | return Math.abs(hash); 19 | } 20 | 21 | /** 22 | * Generate SHA-256 hash (using Web Crypto API if available) 23 | */ 24 | export async function sha256(message: string): Promise { 25 | if (typeof crypto !== "undefined" && crypto.subtle) { 26 | const msgBuffer = new TextEncoder().encode(message); 27 | const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer); 28 | const hashArray = Array.from(new Uint8Array(hashBuffer)); 29 | const hashHex = hashArray 30 | .map((b) => b.toString(16).padStart(2, "0")) 31 | .join(""); 32 | return hashHex; 33 | } 34 | 35 | // Fallback to simple hash if crypto API is not available 36 | return simpleHash(message).toString(16); 37 | } 38 | 39 | /** 40 | * Check if running in browser environment 41 | */ 42 | export function isBrowser(): boolean { 43 | return typeof window !== "undefined" && typeof document !== "undefined"; 44 | } 45 | 46 | /** 47 | * Safe JSON stringify that handles circular references 48 | */ 49 | export function safeStringify(obj: any): string { 50 | const seen = new WeakSet(); 51 | return JSON.stringify(obj, (key, val) => { 52 | if (val != null && typeof val === "object") { 53 | if (seen.has(val)) { 54 | return "[Circular]"; 55 | } 56 | seen.add(val); 57 | } 58 | return val; 59 | }); 60 | } 61 | 62 | /** 63 | * Get a value safely from an object with error handling 64 | */ 65 | export function safeGet(fn: () => T, defaultValue: T): T { 66 | try { 67 | return fn(); 68 | } catch { 69 | return defaultValue; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "esnext", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": false, 16 | "declaration": true, 17 | "declarationMap": true, 18 | "sourceMap": true, 19 | "outDir": "./dist", 20 | "rootDir": "./src" 21 | }, 22 | "include": ["src/**/*"], 23 | "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] 24 | } 25 | --------------------------------------------------------------------------------