fingerprinter-js
Version:
Enterprise-grade browser fingerprinting with 19 collectors and advanced bot detection
1,578 lines (1,562 loc) • 67 kB
JavaScript
/**
* Suspect Score Analyzer v2.0
* Advanced bot, fraud, and automation detection
*/
class SuspectAnalyzer {
/**
* Analyzes a fingerprint to detect suspicious signals
*/
static analyze(components) {
const signals = [];
// 1. Automation detection
signals.push(...this.checkAutomation(components));
// 2. Technical inconsistencies
signals.push(...this.checkConsistency(components));
// 3. Suspicious environment
signals.push(...this.checkEnvironment(components));
// 4. Bot patterns
signals.push(...this.checkBotPatterns(components));
// 5. Privacy tools detection
signals.push(...this.checkPrivacyTools(components));
// Calculate total score
const detectedSignals = signals.filter((s) => s.detected);
let totalScore = detectedSignals.reduce((sum, signal) => sum + signal.severity * 10, 0);
totalScore = Math.min(100, totalScore);
const riskLevel = this.calculateRiskLevel(totalScore);
return {
score: totalScore,
signals: detectedSignals,
riskLevel,
details: {
totalSignalsDetected: detectedSignals.length,
highSeveritySignals: detectedSignals.filter((s) => s.severity >= 8)
.length,
automationDetected: signals
.filter((s) => s.category === "automation")
.some((s) => s.detected),
inconsistenciesFound: signals
.filter((s) => s.category === "inconsistency")
.some((s) => s.detected),
privacyToolsDetected: signals
.filter((s) => s.category === "privacy")
.some((s) => s.detected),
},
};
}
/**
* Detects automation signals
*/
static checkAutomation(components) {
const signals = [];
// WebDriver detection
signals.push({
type: "webdriver",
severity: 9,
description: "WebDriver automation detected",
detected: this.hasWebDriver(),
category: "automation",
});
// Headless browser
signals.push({
type: "headless",
severity: 8,
description: "Headless browser detected",
detected: this.isHeadless(components),
category: "automation",
});
// PhantomJS signatures
signals.push({
type: "phantom",
severity: 7,
description: "PhantomJS signatures detected",
detected: this.hasPhantomSignatures(components),
category: "automation",
});
// Selenium signatures
signals.push({
type: "selenium",
severity: 8,
description: "Selenium signatures detected",
detected: this.hasSeleniumSignatures(),
category: "automation",
});
// Puppeteer detection
signals.push({
type: "puppeteer",
severity: 9,
description: "Puppeteer automation detected",
detected: this.hasPuppeteerSignatures(),
category: "automation",
});
// Playwright detection
signals.push({
type: "playwright",
severity: 9,
description: "Playwright automation detected",
detected: this.hasPlaywrightSignatures(),
category: "automation",
});
// Chrome DevTools Protocol
signals.push({
type: "cdp",
severity: 7,
description: "Chrome DevTools Protocol artifacts detected",
detected: this.hasCDPArtifacts(),
category: "automation",
});
return signals;
}
/**
* Checks data consistency
*/
static checkConsistency(components) {
const signals = [];
// Timezone vs Language inconsistency
signals.push({
type: "timezone_language",
severity: 5,
description: "Timezone/language inconsistency",
detected: this.hasTimezoneLanguageInconsistency(components),
category: "inconsistency",
});
// Screen resolution inconsistency
signals.push({
type: "screen_consistency",
severity: 6,
description: "Screen resolution inconsistency",
detected: this.hasScreenInconsistency(components),
category: "inconsistency",
});
// Canvas fingerprint too generic
signals.push({
type: "generic_canvas",
severity: 4,
description: "Canvas fingerprint too generic",
detected: this.hasGenericCanvas(components),
category: "inconsistency",
});
// User agent vs platform mismatch
signals.push({
type: "ua_platform_mismatch",
severity: 6,
description: "User-Agent doesn't match platform",
detected: this.hasUAPlatformMismatch(components),
category: "inconsistency",
});
// Hardware inconsistency (0 cores or 0 memory)
signals.push({
type: "hardware_inconsistency",
severity: 5,
description: "Suspicious hardware values",
detected: this.hasHardwareInconsistency(components),
category: "inconsistency",
});
return signals;
}
/**
* Checks execution environment
*/
static checkEnvironment(components) {
const signals = [];
// Missing expected APIs
signals.push({
type: "missing_apis",
severity: 6,
description: "Missing browser APIs",
detected: this.hasMissingAPIs(components),
category: "environment",
});
// Too many errors in collection
signals.push({
type: "collection_errors",
severity: 5,
description: "Too many collection errors",
detected: this.hasTooManyErrors(components),
category: "environment",
});
// Suspicious user agent
signals.push({
type: "suspicious_ua",
severity: 7,
description: "Suspicious user agent",
detected: this.hasSuspiciousUserAgent(components),
category: "environment",
});
// Zero touch points on mobile UA
signals.push({
type: "touch_ua_mismatch",
severity: 5,
description: "Mobile UA but no touch support",
detected: this.hasTouchUAMismatch(components),
category: "environment",
});
return signals;
}
/**
* Detects bot patterns
*/
static checkBotPatterns(components) {
const signals = [];
// Perfect fingerprint (too stable)
signals.push({
type: "too_perfect",
severity: 3,
description: "Fingerprint too perfect/stable",
detected: this.isTooStable(components),
category: "bot",
});
// Known bot signatures
signals.push({
type: "bot_signature",
severity: 9,
description: "Known bot signature detected",
detected: this.hasKnownBotSignature(components),
category: "bot",
});
// Unusual WebGL vendor
signals.push({
type: "webgl_vendor",
severity: 4,
description: "Unusual WebGL vendor",
detected: this.hasUnusualWebGLVendor(components),
category: "bot",
});
return signals;
}
/**
* Detects privacy tools and fingerprint spoofing
*/
static checkPrivacyTools(components) {
const signals = [];
// Canvas noise detection (anti-fingerprint extensions)
signals.push({
type: "canvas_noise",
severity: 5,
description: "Canvas fingerprint noise detected",
detected: this.hasCanvasNoise(components),
category: "privacy",
});
// Property tampering detection
signals.push({
type: "property_tampering",
severity: 6,
description: "Browser property tampering detected",
detected: this.hasPropertyTampering(),
category: "privacy",
});
// Audio context tampering
signals.push({
type: "audio_tampering",
severity: 5,
description: "Audio context tampering detected",
detected: this.hasAudioTampering(components),
category: "privacy",
});
return signals;
}
// ================== Detection Methods ==================
static hasWebDriver() {
if (typeof window === "undefined")
return false;
return (navigator.webdriver === true ||
!!window.callPhantom ||
!!window._phantom);
}
static isHeadless(components) {
const ua = components.userAgent || "";
if (ua.includes("HeadlessChrome") ||
ua.includes("PhantomJS") ||
ua.includes("Headless")) {
return true;
}
if (typeof window !== "undefined") {
// Check for zero dimensions (headless indicators)
if (window.outerHeight === 0 || window.outerWidth === 0)
return true;
// Check for missing chrome object in Chrome
if (ua.includes("Chrome") && !window.chrome)
return true;
}
return false;
}
static hasPhantomSignatures(components) {
if (typeof window === "undefined")
return false;
const ua = components.userAgent || "";
return (ua.includes("PhantomJS") ||
!!window._phantom ||
!!window.callPhantom ||
!!window.__phantomas);
}
static hasSeleniumSignatures() {
if (typeof window === "undefined")
return false;
return !!(window.selenium ||
window.webdriver ||
window.document.__selenium_unwrapped ||
window.document.__webdriver_evaluate ||
window.document.__driver_evaluate ||
window.document.__webdriver_script_function ||
window.document.__webdriver_script_func ||
window.document.__webdriver_script_fn ||
window.document.__fxdriver_evaluate ||
window.document.__driver_unwrapped ||
window.document.__webdriver_unwrapped ||
window.document.$cdc_asdjflasutopfhvcZLmcfl_ ||
window.document.$chrome_asyncScriptInfo);
}
static hasPuppeteerSignatures() {
if (typeof window === "undefined")
return false;
return !!(window.__puppeteer_evaluation_script__ ||
navigator.webdriver ||
window.puppeteerBinding);
}
static hasPlaywrightSignatures() {
if (typeof window === "undefined")
return false;
return !!(window.__playwright ||
window.__pw_manual ||
window._playwright);
}
static hasCDPArtifacts() {
if (typeof window === "undefined")
return false;
return !!(window.__cdp__ ||
window.cdc_adoQpoasnfa76pfcZLmcfl_Array ||
window.cdc_adoQpoasnfa76pfcZLmcfl_Promise ||
window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol);
}
static hasTimezoneLanguageInconsistency(components) {
const timezone = components.timezone;
const language = components.language;
if (!timezone || !language)
return false;
// Suspicious combinations
const suspiciousCombinations = [
{ tz: "America/New_York", lang: "zh-CN" },
{ tz: "Europe/Paris", lang: "ja-JP" },
{ tz: "Asia/Tokyo", lang: "es-ES" },
{ tz: "Europe/London", lang: "zh-CN" },
{ tz: "America/Los_Angeles", lang: "ru-RU" },
];
return suspiciousCombinations.some((combo) => timezone.includes(combo.tz) &&
Array.isArray(language) &&
language.some((l) => l.includes(combo.lang)));
}
static hasScreenInconsistency(components) {
const screen = components.screen;
if (!screen)
return false;
// Very common resolutions used by bots
const suspiciousResolutions = ["800x600", "1024x768"];
const resolution = `${screen.width}x${screen.height}`;
return (suspiciousResolutions.includes(resolution) && screen.colorDepth === 24);
}
static hasGenericCanvas(components) {
const canvas = components.canvas;
if (!canvas || typeof canvas !== "string")
return false;
return canvas.length < 100 || canvas === "no-canvas";
}
static hasUAPlatformMismatch(components) {
const ua = components.userAgent || "";
const hardware = components.hardware;
if (!hardware)
return false;
const platform = hardware.platform || "";
// Windows UA but Mac/Linux platform
if (ua.includes("Windows") && !platform.toLowerCase().includes("win")) {
return true;
}
// Mac UA but Windows/Linux platform
if (ua.includes("Macintosh") &&
!platform.toLowerCase().includes("mac")) {
return true;
}
return false;
}
static hasHardwareInconsistency(components) {
const hardware = components.hardware;
if (!hardware)
return false;
const cores = hardware.hardwareConcurrency;
const memory = hardware.deviceMemory;
// 0 cores is impossible
if (cores === 0)
return true;
// More than 128 cores is suspicious
if (cores > 128)
return true;
// Less than 0.25GB is suspicious for desktop
if (memory !== null && memory < 0.25)
return true;
return false;
}
static hasMissingAPIs(components) {
const expectedAPIs = ["userAgent", "language", "screen"];
const missingAPIs = expectedAPIs.filter((api) => {
const value = components[api];
return !value || (typeof value === "object" && value.error);
});
return missingAPIs.length > 1;
}
static hasTooManyErrors(components) {
const errorCount = Object.values(components).filter((value) => value && typeof value === "object" && value.error).length;
return errorCount > 5;
}
static hasSuspiciousUserAgent(components) {
const ua = components.userAgent || "";
const suspiciousPatterns = [
"HeadlessChrome",
"PhantomJS",
"bot",
"crawler",
"spider",
"scraper",
"python-requests",
"curl",
"wget",
"node-fetch",
"axios",
];
return suspiciousPatterns.some((pattern) => ua.toLowerCase().includes(pattern.toLowerCase()));
}
static hasTouchUAMismatch(components) {
const ua = components.userAgent || "";
const touch = components.touch;
if (!touch)
return false;
const maxTouchPoints = touch.maxTouchPoints;
const isMobileUA = ua.includes("Mobile") || ua.includes("Android") || ua.includes("iPhone");
// Mobile UA but no touch support
return isMobileUA && maxTouchPoints === 0;
}
static isTooStable(components) {
const perfectComponents = Object.values(components).filter((value) => value && !(typeof value === "object" && value.error) && value !== "unknown").length;
// If every single component is perfect with no errors, it's suspicious
return perfectComponents === Object.keys(components).length;
}
static hasKnownBotSignature(components) {
const ua = components.userAgent || "";
const botSignatures = [
"Googlebot",
"Bingbot",
"Slurp",
"DuckDuckBot",
"Baiduspider",
"YandexBot",
"facebookexternalhit",
"Twitterbot",
"LinkedInBot",
"WhatsApp",
"python-requests",
"curl/",
"wget",
"Scrapy",
"HttpClient",
];
return botSignatures.some((signature) => ua.includes(signature));
}
static hasUnusualWebGLVendor(components) {
const webgl = components.webgl;
if (!webgl || webgl.error)
return false;
const vendor = webgl.vendor || "";
const renderer = webgl.renderer || "";
// SwiftShader is commonly used by headless browsers
if (renderer.includes("SwiftShader") ||
renderer.includes("llvmpipe") ||
renderer.includes("VMware")) {
return true;
}
// Brian Paul Mesa is software rendering
if (vendor.includes("Brian Paul") || renderer.includes("Mesa")) {
return true;
}
return false;
}
static hasCanvasNoise(components) {
// This would require comparing multiple canvas renders
// For now, check if canvas is suspiciously different from expected
const canvas = components.canvas;
if (!canvas || typeof canvas !== "string")
return false;
// Very short canvas data might indicate blocking
if (canvas.length < 50)
return true;
return false;
}
static hasPropertyTampering() {
if (typeof window === "undefined")
return false;
try {
// Check if navigator.webdriver is modified
const descriptor = Object.getOwnPropertyDescriptor(Navigator.prototype, "webdriver");
if (descriptor && descriptor.get && descriptor.get.toString) {
const getterStr = descriptor.get.toString();
if (getterStr.includes("proxy") ||
getterStr.includes("return false") ||
getterStr.includes("undefined")) {
return true;
}
}
}
catch (_a) {
// If it throws, might be tampered
return true;
}
return false;
}
static hasAudioTampering(components) {
const audio = components.audio;
if (!audio || audio.error)
return false;
// Check for impossible values
const sampleRate = audio.sampleRate;
if (sampleRate === 0 || sampleRate > 192000)
return true;
// Standard sample rates
const standardRates = [8000, 11025, 16000, 22050, 44100, 48000, 96000];
if (!standardRates.includes(sampleRate))
return true;
return false;
}
static calculateRiskLevel(score) {
if (score < 30)
return "LOW";
if (score < 70)
return "MEDIUM";
return "HIGH";
}
}
/**
* FingerprinterJS v2.0 Types
* Enterprise-grade type definitions
*/
/**
* Default options
*/
const DEFAULT_OPTIONS = {
timeout: 5000,
parallel: true,
allowUnstableData: false,
};
// ============================================================
// Error Types
// ============================================================
class FingerprintError extends Error {
constructor(message, code, collector) {
super(message);
this.code = code;
this.collector = collector;
this.name = "FingerprintError";
}
}
// ============================================================
// Library Version
// ============================================================
const VERSION = "2.0.0";
/**
* Utility functions for fingerprinting
*/
/**
* Simple hash function to convert string to number
*/
function simpleHash(str) {
let hash = 0;
if (str.length === 0)
return hash;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash);
}
/**
* Generate SHA-256 hash (using Web Crypto API if available)
*/
async function sha256(message) {
if (typeof crypto !== "undefined" && crypto.subtle) {
const msgBuffer = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return hashHex;
}
// Fallback to simple hash if crypto API is not available
return simpleHash(message).toString(16);
}
/**
* Check if running in browser environment
*/
function isBrowser() {
return typeof window !== "undefined" && typeof document !== "undefined";
}
/**
* Safe JSON stringify that handles circular references
*/
function safeStringify(obj) {
const seen = new WeakSet();
return JSON.stringify(obj, (_key, val) => {
if (val != null && typeof val === "object") {
if (seen.has(val)) {
return "[Circular]";
}
seen.add(val);
}
return val;
});
}
/**
* Get a value safely from an object with error handling
*/
function safeGet(fn, defaultValue) {
try {
return fn();
}
catch (_a) {
return defaultValue;
}
}
/**
* Base Collector Class
* Abstract base class for all fingerprint collectors
*/
/**
* Abstract base class for collectors
*/
class BaseCollector {
/**
* Check if collector is supported
* Override in subclasses for specific checks
*/
isSupported() {
return isBrowser();
}
/**
* Safe collection with error handling
*/
safeCollect(fn, defaultValue) {
return safeGet(fn, defaultValue);
}
/**
* Create metadata helper
*/
static createMetadata(name, options = {}) {
var _a, _b, _c, _d;
return {
name,
weight: (_a = options.weight) !== null && _a !== void 0 ? _a : 5,
entropy: (_b = options.entropy) !== null && _b !== void 0 ? _b : 2,
stable: (_c = options.stable) !== null && _c !== void 0 ? _c : true,
category: (_d = options.category) !== null && _d !== void 0 ? _d : "browser",
};
}
}
/**
* Async base collector with timeout support
*/
class AsyncBaseCollector extends BaseCollector {
constructor() {
super(...arguments);
this.timeout = 5000;
}
/**
* Set collector timeout
*/
setTimeout(ms) {
this.timeout = ms;
return this;
}
/**
* Wrap async operation with timeout
*/
async withTimeout(promise, fallback) {
return Promise.race([
promise,
new Promise((resolve) => setTimeout(() => resolve(fallback), this.timeout)),
]);
}
}
/**
* Basic Collectors
* Simple browser property collectors
*/
/**
* User Agent Collector
*/
class UserAgentCollector extends BaseCollector {
constructor() {
super(...arguments);
this.name = "userAgent";
this.metadata = BaseCollector.createMetadata("userAgent", {
weight: 8,
entropy: 10,
stable: true,
category: "browser",
});
}
collect() {
return this.safeCollect(() => navigator.userAgent, "unknown");
}
}
/**
* Language Collector
*/
class LanguageCollector extends BaseCollector {
constructor() {
super(...arguments);
this.name = "language";
this.metadata = BaseCollector.createMetadata("language", {
weight: 6,
entropy: 5,
stable: true,
category: "browser",
});
}
collect() {
return this.safeCollect(() => {
const languages = [];
if (navigator.language) {
languages.push(navigator.language);
}
if (navigator.languages) {
languages.push(...navigator.languages);
}
return [...new Set(languages)];
}, ["unknown"]);
}
}
/**
* Timezone Collector
*/
class TimezoneCollector extends BaseCollector {
constructor() {
super(...arguments);
this.name = "timezone";
this.metadata = BaseCollector.createMetadata("timezone", {
weight: 7,
entropy: 6,
stable: true,
category: "browser",
});
}
collect() {
return this.safeCollect(() => {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
}, "unknown");
}
}
/**
* Screen Collector
*/
class ScreenCollector extends BaseCollector {
constructor() {
super(...arguments);
this.name = "screen";
this.metadata = BaseCollector.createMetadata("screen", {
weight: 7,
entropy: 8,
stable: true,
category: "hardware",
});
}
collect() {
return this.safeCollect(() => ({
width: screen.width,
height: screen.height,
availWidth: screen.availWidth,
availHeight: screen.availHeight,
colorDepth: screen.colorDepth,
pixelDepth: screen.pixelDepth,
devicePixelRatio: window.devicePixelRatio || 1,
}), {
width: 0,
height: 0,
availWidth: 0,
availHeight: 0,
colorDepth: 0,
pixelDepth: 0,
devicePixelRatio: 1,
});
}
}
/**
* Plugins Collector
*/
class PluginsCollector extends BaseCollector {
constructor() {
super(...arguments);
this.name = "plugins";
this.metadata = BaseCollector.createMetadata("plugins", {
weight: 5,
entropy: 6,
stable: true,
category: "browser",
});
}
collect() {
return this.safeCollect(() => {
const plugins = [];
for (let i = 0; i < navigator.plugins.length; i++) {
const plugin = navigator.plugins[i];
plugins.push({
name: plugin.name,
description: plugin.description,
filename: plugin.filename,
});
}
return plugins;
}, []);
}
}
/**
* Canvas and WebGL Collectors
* Graphics-based fingerprinting
*/
/**
* Canvas Collector
* Creates a unique canvas fingerprint based on rendering differences
*/
class CanvasCollector extends BaseCollector {
constructor() {
super(...arguments);
this.name = "canvas";
this.metadata = BaseCollector.createMetadata("canvas", {
weight: 9,
entropy: 12,
stable: true,
category: "graphics",
});
}
isSupported() {
return (super.isSupported() &&
typeof document !== "undefined" &&
typeof HTMLCanvasElement !== "undefined");
}
collect() {
return this.safeCollect(() => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx)
return "no-canvas-context";
// Set canvas size
canvas.width = 200;
canvas.height = 50;
// Draw text with various styles
ctx.textBaseline = "top";
ctx.font = "14px Arial";
ctx.fillStyle = "#f60";
ctx.fillRect(125, 1, 62, 20);
ctx.fillStyle = "#069";
ctx.fillText("Canvas fingerprint 🎨", 2, 15);
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
ctx.fillText("Canvas fingerprint 🎨", 4, 17);
// Draw some shapes
ctx.globalCompositeOperation = "multiply";
ctx.fillStyle = "rgb(255,0,255)";
ctx.beginPath();
ctx.arc(50, 50, 50, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "rgb(0,255,255)";
ctx.beginPath();
ctx.arc(100, 50, 50, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "rgb(255,255,0)";
ctx.beginPath();
ctx.arc(75, 100, 50, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
return canvas.toDataURL();
}, "no-canvas");
}
}
/**
* WebGL Collector
* Collects WebGL rendering context information
*/
class WebGLCollector extends BaseCollector {
constructor() {
super(...arguments);
this.name = "webgl";
this.metadata = BaseCollector.createMetadata("webgl", {
weight: 9,
entropy: 15,
stable: true,
category: "graphics",
});
}
isSupported() {
return (super.isSupported() &&
typeof document !== "undefined" &&
typeof HTMLCanvasElement !== "undefined");
}
collect() {
return this.safeCollect(() => {
const canvas = document.createElement("canvas");
const gl = canvas.getContext("webgl") ||
canvas.getContext("experimental-webgl");
if (!gl)
return { error: "no-webgl-context" };
const result = {};
// Get basic WebGL info
result.vendor = gl.getParameter(gl.VENDOR);
result.renderer = gl.getParameter(gl.RENDERER);
result.version = gl.getParameter(gl.VERSION);
result.shadingLanguageVersion = gl.getParameter(gl.SHADING_LANGUAGE_VERSION);
// Get supported extensions
const extensions = gl.getSupportedExtensions();
result.extensions = extensions ? extensions.sort() : [];
// Get additional parameters
result.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
result.maxViewportDims = Array.from(gl.getParameter(gl.MAX_VIEWPORT_DIMS));
result.maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
result.maxVertexUniformVectors = gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS);
result.maxFragmentUniformVectors = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS);
result.maxVaryingVectors = gl.getParameter(gl.MAX_VARYING_VECTORS);
// Get unmasked info if available
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
if (debugInfo) {
result.unmaskedVendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
result.unmaskedRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
}
return result;
}, { error: "webgl-not-available" });
}
}
/**
* Advanced Collectors
* Audio and Fonts fingerprinting
*/
/**
* Audio Collector
* Creates a fingerprint based on audio processing
*/
class AudioCollector extends AsyncBaseCollector {
constructor() {
super(...arguments);
this.name = "audio";
this.metadata = BaseCollector.createMetadata("audio", {
weight: 8,
entropy: 10,
stable: false, // Audio fingerprint can vary on first run
category: "audio",
});
}
isSupported() {
return (super.isSupported() &&
(typeof AudioContext !== "undefined" ||
typeof window.webkitAudioContext !== "undefined"));
}
async collect() {
if (!this.isSupported()) {
return { error: "audio-not-available" };
}
return this.withTimeout(this.collectAudio(), {
error: "audio-timeout",
});
}
async collectAudio() {
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
if (!AudioContextClass) {
return { error: "no-audio-context" };
}
const audioContext = new AudioContextClass();
const result = {};
try {
result.sampleRate = audioContext.sampleRate;
result.state = audioContext.state;
result.maxChannelCount = audioContext.destination.maxChannelCount;
result.channelCount = audioContext.destination.channelCount;
result.channelCountMode = audioContext.destination.channelCountMode;
result.channelInterpretation =
audioContext.destination.channelInterpretation;
// Create audio fingerprint using oscillator
const oscillator = audioContext.createOscillator();
const analyser = audioContext.createAnalyser();
const gainNode = audioContext.createGain();
const scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
oscillator.type = "triangle";
oscillator.frequency.setValueAtTime(10000, audioContext.currentTime);
oscillator.connect(analyser);
analyser.connect(scriptProcessor);
scriptProcessor.connect(gainNode);
gainNode.connect(audioContext.destination);
let audioFingerprint = "";
scriptProcessor.onaudioprocess = () => {
const freqData = new Float32Array(analyser.frequencyBinCount);
analyser.getFloatFrequencyData(freqData);
let sum = 0;
for (let i = 0; i < freqData.length; i++) {
sum += Math.abs(freqData[i]);
}
audioFingerprint = sum.toString();
};
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.1);
await new Promise((resolve) => setTimeout(resolve, 100));
result.audioFingerprint = audioFingerprint;
await audioContext.close();
return result;
}
catch (error) {
try {
await audioContext.close();
}
catch (_a) { }
return {
error: "audio-processing-failed",
};
}
}
}
/**
* Fonts Collector
* Detects available fonts on the system
*/
class FontsCollector extends BaseCollector {
constructor() {
super(...arguments);
this.name = "fonts";
this.metadata = BaseCollector.createMetadata("fonts", {
weight: 8,
entropy: 12,
stable: true,
category: "browser",
});
this.testFonts = [
"Arial",
"Arial Black",
"Arial Narrow",
"Arial Rounded MT Bold",
"Bookman Old Style",
"Bradley Hand ITC",
"Century",
"Century Gothic",
"Comic Sans MS",
"Courier",
"Courier New",
"Georgia",
"Gentium",
"Helvetica",
"Helvetica Neue",
"Impact",
"King",
"Lucida Console",
"Lalit",
"Modena",
"Monotype Corsiva",
"Papyrus",
"Tahoma",
"TeX",
"Times",
"Times New Roman",
"Trebuchet MS",
"Verdana",
"Verona",
// Additional common fonts
"Monaco",
"Consolas",
"Lucida Sans Unicode",
"Palatino Linotype",
"Segoe UI",
"Candara",
"Cambria",
"Garamond",
"Perpetua",
"Rockwell",
"Franklin Gothic Medium",
];
}
isSupported() {
return super.isSupported() && typeof document !== "undefined";
}
collect() {
if (!this.isSupported()) {
return [];
}
return this.safeCollect(() => {
const availableFonts = [];
const testString = "mmmmmmmmmmlli";
const testSize = "72px";
const body = document.getElementsByTagName("body")[0];
if (!body)
return [];
// Create test span element
const span = document.createElement("span");
span.style.fontSize = testSize;
span.style.position = "absolute";
span.style.left = "-9999px";
span.style.visibility = "hidden";
span.innerHTML = testString;
body.appendChild(span);
// Get default width and height
span.style.fontFamily = "monospace";
const defaultWidth = span.offsetWidth;
const defaultHeight = span.offsetHeight;
// Test each font
for (const font of this.testFonts) {
span.style.fontFamily = `'${font}', monospace`;
if (span.offsetWidth !== defaultWidth ||
span.offsetHeight !== defaultHeight) {
availableFonts.push(font);
}
}
body.removeChild(span);
return availableFonts;
}, []);
}
}
/**
* Battery Collector
* Collects battery status information (where available)
*/
/**
* Battery Collector
* Note: Battery API is deprecated but still works in some browsers
*/
class BatteryCollector extends AsyncBaseCollector {
constructor() {
super(...arguments);
this.name = "battery";
this.metadata = BaseCollector.createMetadata("battery", {
weight: 3,
entropy: 3,
stable: false, // Battery stats change
category: "hardware",
});
}
isSupported() {
return (super.isSupported() &&
"getBattery" in navigator);
}
async collect() {
if (!this.isSupported()) {
return { supported: false };
}
return this.withTimeout(this.collectBattery(), { supported: false });
}
async collectBattery() {
try {
const battery = await navigator.getBattery();
return {
supported: true,
charging: battery.charging,
level: battery.level,
chargingTime: battery.chargingTime,
dischargingTime: battery.dischargingTime,
};
}
catch (_a) {
return { supported: false };
}
}
}
/**
* Client Hints Collector
* Collects modern User-Agent Client Hints data
*/
/**
* Client Hints Collector
* Modern alternative to User-Agent parsing
*/
class ClientHintsCollector extends AsyncBaseCollector {
constructor() {
super(...arguments);
this.name = "clientHints";
this.metadata = BaseCollector.createMetadata("clientHints", {
weight: 7,
entropy: 10,
stable: true,
category: "browser",
});
}
isSupported() {
var _a;
return (super.isSupported() &&
"userAgentData" in navigator &&
typeof ((_a = navigator.userAgentData) === null || _a === void 0 ? void 0 : _a.getHighEntropyValues) ===
"function");
}
async collect() {
if (!this.isSupported()) {
return {
brands: [],
mobile: false,
platform: "unknown",
};
}
return this.withTimeout(this.collectClientHints(), {
brands: [],
mobile: false,
platform: "unknown",
});
}
async collectClientHints() {
try {
const uaData = navigator.userAgentData;
const result = {
brands: uaData.brands || [],
mobile: uaData.mobile || false,
platform: uaData.platform || "unknown",
};
// Try to get high entropy values
try {
const highEntropy = await uaData.getHighEntropyValues([
"architecture",
"bitness",
"model",
"platformVersion",
"fullVersionList",
]);
result.architecture = highEntropy.architecture;
result.bitness = highEntropy.bitness;
result.model = highEntropy.model;
result.platformVersion = highEntropy.platformVersion;
result.fullVersionList = highEntropy.fullVersionList;
}
catch (_a) {
// High entropy values may be blocked
}
return result;
}
catch (error) {
return {
brands: [],
mobile: false,
platform: "unknown",
};
}
}
}
/**
* Connection Collector
* Collects network connection information
*/
/**
* Connection Collector
* Uses Network Information API
*/
class ConnectionCollector extends BaseCollector {
constructor() {
super(...arguments);
this.name = "connection";
this.metadata = BaseCollector.createMetadata("connection", {
weight: 4,
entropy: 4,
stable: false, // Connection can change
category: "network",
});
}
isSupported() {
return (super.isSupported() &&
"connection" in navigator);
}
collect() {
if (!this.isSupported()) {
return { supported: false };
}
return this.safeCollect(() => {
const connection = navigator.connection;
return {
supported: true,
effectiveType: connection.effectiveType,
downlink: connection.downlink,
rtt: connection.rtt,
saveData: connection.saveData,
type: connection.type,
};
}, { supported: false });
}
}
/**
* Hardware Collector
* Collects hardware-related information
*/
/**
* Hardware Collector
* Collects CPU, memory, and device information
*/
class HardwareCollector extends BaseCollector {
constructor() {
super(...arguments);
this.name = "hardware";
this.metadata = BaseCollector.createMetadata("hardware", {
weight: 8,
entropy: 8,
stable: true,
category: "hardware",
});
}
collect() {
return this.safeCollect(() => {
const nav = navigator;
return {
hardwareConcurrency: nav.hardwareConcurrency || 0,
deviceMemory: nav.deviceMemory || null,
platform: nav.platform || "unknown",
maxTouchPoints: nav.maxTouchPoints || 0,
oscpu: nav.oscpu, // Firefox only
};
}, {
hardwareConcurrency: 0,
deviceMemory: null,
platform: "unknown",
maxTouchPoints: 0,
});
}
}
/**
* Math Collector
* Creates fingerprint based on math precision differences
*/
/**
* Math Collector
* Different browsers/CPUs can produce slightly different math results
*/
class MathCollector extends BaseCollector {
constructor() {
super(...arguments);
this.name = "math";
this.metadata = BaseCollector.createMetadata("math", {
weight: 6,
entropy: 6,
stable: true,
category: "browser",
});
}
collect() {
return this.safeCollect(() => ({
tan: Math.tan(-1e300).toString(),
sin: Math.sin(-1e300).toString(),
atan2: Math.atan2(0.5, 0.5).toString(),
log: Math.log(1000).toString(),
pow: Math.pow(Math.PI, -100).toString(),
sqrt: Math.sqrt(2).toString(),
}), {
tan: "",
sin: "",
atan2: "",
log: "",
pow: "",
sqrt: "",
});
}
}
/**
* Media Devices Collector
* Enumerates available media input/output devices
*/
/**
* Media Devices Collector
* Counts cameras, microphones, and speakers
*/
class MediaDevicesCollector extends AsyncBaseCollector {
constructor() {
super(...arguments);
this.name = "mediaDevices";
this.metadata = BaseCollector.createMetadata("mediaDevices", {
weight: 5,
entropy: 5,
stable: true,
category: "hardware",
});
}
isSupported() {
return (super.isSupported() &&
"mediaDevices" in navigator &&
typeof navigator.mediaDevices.enumerateDevices === "function");
}
async collect() {
if (!this.isSupported()) {
return {
audioInputs: 0,
audioOutputs: 0,
videoInputs: 0,
hasMediaDevices: false,
};
}
return this.withTimeout(this.collectDevices(), {
audioInputs: 0,
audioOutputs: 0,
videoInputs: 0,
hasMediaDevices: true,
});
}
async collectDevices() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
return {
audioInputs: devices.filter((d) => d.kind === "audioinput").length,
audioOutputs: devices.filter((d) => d.kind === "audiooutput").length,
videoInputs: devices.filter((d) => d.kind === "videoinput").length,
hasMediaDevices: true,
};
}
catch (_a) {
return {
audioInputs: 0,
audioOutputs: 0,
videoInputs: 0,
hasMediaDevices: true,
};
}
}
}
/**
* Permissions Collector
* Checks permission states for various APIs
*/
/**
* Permissions Collector
* Uses Permissions API to check permission states
*/
class PermissionsCollector extends AsyncBaseCollector {
constructor() {
super(...arguments);
this.name = "permissions";
this.metadata = BaseCollector.createMetadata("permissions", {
weight: 4,
entropy: 5,
stable: false, // Permissions can change
category: "permissions",
});
}
isSupported() {
return (super.isSupported() &&
"permissions" in navigator &&
typeof navigator.permissions.query === "function");
}
async collect() {
if (!this.isSupported()) {
return {};
}
const permissionNames = [
"notifications",
"geolocation",
"camera",
"microphone",
"push",
];
const result = {};
await Promise.all(permissionNames.map(async (name) => {
try {
const permission = await navigator.permissions.query({
name: name,
});
result[name] = permission.state;
}
catch (_a) {
// Some permissions may not be queryable
}
}));
return result;
}
}
/**
* Storage Collector
* Detects available storage mechanisms
*/
/**
* Storage Collector
* Checks for localStorage, sessionStorage, IndexedDB, and cookies
*/
class StorageCollector extends AsyncBaseCollector {
constructor() {
super(...arguments);
this.name = "storage";
this.metadata = BaseCollector.createMetadata("storage", {
weight: 5,
entropy: 4,
stable: true,
category: "storage",
});
}
async collect() {
return {
localStorage: this.checkLocalStorage(),
sessionStorage: this.checkSessionStorage(),
indexedDB: this.checkIndexedDB(),
cookiesEnabled: this.checkCookies(),
quotaEstimate: await this.getQuotaEstimate(),
};
}
checkLocalStorage() {
try {
const test = "__fp_test__";
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
}
catch (_a) {
return false;
}
}
checkSessionStorage() {
try {
const test = "__fp_test__";
sessionStorage.setItem(test, test);
sessionStorage.removeItem(test);
return true;
}
catch (_a) {
return false;
}
}
checkIndexedDB() {
try {
return (typeof indexedDB !== "undefined" &&
typeof indexedDB.open === "function");
}
catch (_a) {
return false;
}
}
checkCookies() {
try {
return navigator.cookieEnabled;
}
catch (_a) {
return false;
}
}
async getQuotaEstimate() {
try {
if ("storage" in navigator && "estimate" in navigator.storage) {
const estimate = await navigator.storage.estimate();
return estimate.quota || null;
}
return null;
}
catch (_a) {
return null;
}
}
}
/**
* Touch Collector
* Collects touch and pointer capabilities
*/
/**
* Touch Collector
* Detects touch screen capabilities
*/
class TouchCollector extends BaseCollector {
constructor() {
super(...arguments);
this.name = "touch";
this.metadata = BaseCollector.createMetadata("touch", {
weight: 5,
entropy: 4,
stable: true,
category: "hardware",
});
}
collect() {