@fingerprintjs/fingerprintjs
Version:
Browser fingerprinting library with the highest accuracy and stability
1,238 lines (1,227 loc) • 133 kB
JavaScript
/**
* FingerprintJS v5.1.0 - Copyright (c) FingerprintJS, Inc, 2026 (https://fingerprint.com)
*
* Licensed under MIT License
*
* Copyright (c) 2025 FingerprintJS, Inc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
var FingerprintJS = (function (exports) {
'use strict';
var version = "5.1.0";
function wait(durationMs, resolveWith) {
return new Promise((resolve) => setTimeout(resolve, durationMs, resolveWith));
}
/**
* Allows asynchronous actions and microtasks to happen.
*/
function releaseEventLoop() {
// Don't use setTimeout because Chrome throttles it in some cases causing very long agent execution:
// https://stackoverflow.com/a/6032591/1118709
// https://github.com/chromium/chromium/commit/0295dd09496330f3a9103ef7e543fa9b6050409b
// Reusing a MessageChannel object gives no noticeable benefits
return new Promise((resolve) => {
const channel = new MessageChannel();
channel.port1.onmessage = () => resolve();
channel.port2.postMessage(null);
});
}
function requestIdleCallbackIfAvailable(fallbackTimeout, deadlineTimeout = Infinity) {
const { requestIdleCallback } = window;
if (requestIdleCallback) {
// The function `requestIdleCallback` loses the binding to `window` here.
// `globalThis` isn't always equal `window` (see https://github.com/fingerprintjs/fingerprintjs/issues/683).
// Therefore, an error can occur. `call(window,` prevents the error.
return new Promise((resolve) => requestIdleCallback.call(window, () => resolve(), { timeout: deadlineTimeout }));
}
else {
return wait(Math.min(fallbackTimeout, deadlineTimeout));
}
}
function isPromise(value) {
return !!value && typeof value.then === 'function';
}
/**
* Calls a maybe asynchronous function without creating microtasks when the function is synchronous.
* Catches errors in both cases.
*
* If just you run a code like this:
* ```
* console.time('Action duration')
* await action()
* console.timeEnd('Action duration')
* ```
* The synchronous function time can be measured incorrectly because another microtask may run before the `await`
* returns the control back to the code.
*/
function awaitIfAsync(action, callback) {
try {
const returnedValue = action();
if (isPromise(returnedValue)) {
returnedValue.then((result) => callback(true, result), (error) => callback(false, error));
}
else {
callback(true, returnedValue);
}
}
catch (error) {
callback(false, error);
}
}
/**
* If you run many synchronous tasks without using this function, the JS main loop will be busy and asynchronous tasks
* (e.g. completing a network request, rendering the page) won't be able to happen.
* This function allows running many synchronous tasks such way that asynchronous tasks can run too in background.
*/
async function mapWithBreaks(items, callback, loopReleaseInterval = 16) {
const results = Array(items.length);
let lastLoopReleaseTime = Date.now();
for (let i = 0; i < items.length; ++i) {
results[i] = callback(items[i], i);
const now = Date.now();
if (now >= lastLoopReleaseTime + loopReleaseInterval) {
lastLoopReleaseTime = now;
await releaseEventLoop();
}
}
return results;
}
/**
* Makes the given promise never emit an unhandled promise rejection console warning.
* The promise will still pass errors to the next promises.
* Returns the input promise for convenience.
*
* Otherwise, promise emits a console warning unless it has a `catch` listener.
*/
function suppressUnhandledRejectionWarning(promise) {
promise.then(undefined, () => undefined);
return promise;
}
/*
* This file contains functions to work with pure data only (no browser features, DOM, side effects, etc).
*/
/**
* Does the same as Array.prototype.includes but has better typing
*/
function includes(haystack, needle) {
for (let i = 0, l = haystack.length; i < l; ++i) {
if (haystack[i] === needle) {
return true;
}
}
return false;
}
/**
* Like `!includes()` but with proper typing
*/
function excludes(haystack, needle) {
return !includes(haystack, needle);
}
/**
* Be careful, NaN can return
*/
function toInt(value) {
return parseInt(value);
}
/**
* Be careful, NaN can return
*/
function toFloat(value) {
return parseFloat(value);
}
function replaceNaN(value, replacement) {
return typeof value === 'number' && isNaN(value) ? replacement : value;
}
function countTruthy(values) {
return values.reduce((sum, value) => sum + (value ? 1 : 0), 0);
}
function round(value, base = 1) {
if (Math.abs(base) >= 1) {
return Math.round(value / base) * base;
}
else {
// Sometimes when a number is multiplied by a small number, precision is lost,
// for example 1234 * 0.0001 === 0.12340000000000001, and it's more precise divide: 1234 / (1 / 0.0001) === 0.1234.
const counterBase = 1 / base;
return Math.round(value * counterBase) / counterBase;
}
}
/**
* Parses a CSS selector into tag name with HTML attributes.
* Only single element selector are supported (without operators like space, +, >, etc).
*
* Multiple values can be returned for each attribute. You decide how to handle them.
*/
function parseSimpleCssSelector(selector) {
var _a, _b;
const errorMessage = `Unexpected syntax '${selector}'`;
const tagMatch = /^\s*([a-z-]*)(.*)$/i.exec(selector);
const tag = tagMatch[1] || undefined;
const attributes = {};
const partsRegex = /([.:#][\w-]+|\[.+?\])/gi;
const addAttribute = (name, value) => {
attributes[name] = attributes[name] || [];
attributes[name].push(value);
};
for (;;) {
const match = partsRegex.exec(tagMatch[2]);
if (!match) {
break;
}
const part = match[0];
switch (part[0]) {
case '.':
addAttribute('class', part.slice(1));
break;
case '#':
addAttribute('id', part.slice(1));
break;
case '[': {
const attributeMatch = /^\[([\w-]+)([~|^$*]?=("(.*?)"|([\w-]+)))?(\s+[is])?\]$/.exec(part);
if (attributeMatch) {
addAttribute(attributeMatch[1], (_b = (_a = attributeMatch[4]) !== null && _a !== void 0 ? _a : attributeMatch[5]) !== null && _b !== void 0 ? _b : '');
}
else {
throw new Error(errorMessage);
}
break;
}
default:
throw new Error(errorMessage);
}
}
return [tag, attributes];
}
/**
* Converts a string to UTF8 bytes
*/
function getUTF8Bytes(input) {
// Benchmark: https://jsbench.me/b6klaaxgwq/1
// If you want to just count bytes, see solutions at https://jsbench.me/ehklab415e/1
const result = new Uint8Array(input.length);
for (let i = 0; i < input.length; i++) {
// `charCode` is faster than encoding, so we prefer that when it's possible
const charCode = input.charCodeAt(i);
// In case of non-ASCII symbols we use proper encoding
if (charCode > 127) {
return new TextEncoder().encode(input);
}
result[i] = charCode;
}
return result;
}
/*
* Based on https://github.com/karanlyons/murmurHash3.js/blob/a33d0723127e2e5415056c455f8aed2451ace208/murmurHash3.js
*/
/**
* Adds two 64-bit values (provided as tuples of 32-bit values)
* and updates (mutates) first value to write the result
*/
function x64Add(m, n) {
const m0 = m[0] >>> 16, m1 = m[0] & 0xffff, m2 = m[1] >>> 16, m3 = m[1] & 0xffff;
const n0 = n[0] >>> 16, n1 = n[0] & 0xffff, n2 = n[1] >>> 16, n3 = n[1] & 0xffff;
let o0 = 0, o1 = 0, o2 = 0, o3 = 0;
o3 += m3 + n3;
o2 += o3 >>> 16;
o3 &= 0xffff;
o2 += m2 + n2;
o1 += o2 >>> 16;
o2 &= 0xffff;
o1 += m1 + n1;
o0 += o1 >>> 16;
o1 &= 0xffff;
o0 += m0 + n0;
o0 &= 0xffff;
m[0] = (o0 << 16) | o1;
m[1] = (o2 << 16) | o3;
}
/**
* Multiplies two 64-bit values (provided as tuples of 32-bit values)
* and updates (mutates) first value to write the result
*/
function x64Multiply(m, n) {
const m0 = m[0] >>> 16, m1 = m[0] & 0xffff, m2 = m[1] >>> 16, m3 = m[1] & 0xffff;
const n0 = n[0] >>> 16, n1 = n[0] & 0xffff, n2 = n[1] >>> 16, n3 = n[1] & 0xffff;
let o0 = 0, o1 = 0, o2 = 0, o3 = 0;
o3 += m3 * n3;
o2 += o3 >>> 16;
o3 &= 0xffff;
o2 += m2 * n3;
o1 += o2 >>> 16;
o2 &= 0xffff;
o2 += m3 * n2;
o1 += o2 >>> 16;
o2 &= 0xffff;
o1 += m1 * n3;
o0 += o1 >>> 16;
o1 &= 0xffff;
o1 += m2 * n2;
o0 += o1 >>> 16;
o1 &= 0xffff;
o1 += m3 * n1;
o0 += o1 >>> 16;
o1 &= 0xffff;
o0 += m0 * n3 + m1 * n2 + m2 * n1 + m3 * n0;
o0 &= 0xffff;
m[0] = (o0 << 16) | o1;
m[1] = (o2 << 16) | o3;
}
/**
* Provides left rotation of the given int64 value (provided as tuple of two int32)
* by given number of bits. Result is written back to the value
*/
function x64Rotl(m, bits) {
const m0 = m[0];
bits %= 64;
if (bits === 32) {
m[0] = m[1];
m[1] = m0;
}
else if (bits < 32) {
m[0] = (m0 << bits) | (m[1] >>> (32 - bits));
m[1] = (m[1] << bits) | (m0 >>> (32 - bits));
}
else {
bits -= 32;
m[0] = (m[1] << bits) | (m0 >>> (32 - bits));
m[1] = (m0 << bits) | (m[1] >>> (32 - bits));
}
}
/**
* Provides a left shift of the given int32 value (provided as tuple of [0, int32])
* by given number of bits. Result is written back to the value
*/
function x64LeftShift(m, bits) {
bits %= 64;
if (bits === 0) {
return;
}
else if (bits < 32) {
m[0] = m[1] >>> (32 - bits);
m[1] = m[1] << bits;
}
else {
m[0] = m[1] << (bits - 32);
m[1] = 0;
}
}
/**
* Provides a XOR of the given int64 values(provided as tuple of two int32).
* Result is written back to the first value
*/
function x64Xor(m, n) {
m[0] ^= n[0];
m[1] ^= n[1];
}
const F1 = [0xff51afd7, 0xed558ccd];
const F2 = [0xc4ceb9fe, 0x1a85ec53];
/**
* Calculates murmurHash3's final x64 mix of that block and writes result back to the input value.
* (`[0, h[0] >>> 1]` is a 33 bit unsigned right shift. This is the
* only place where we need to right shift 64bit ints.)
*/
function x64Fmix(h) {
const shifted = [0, h[0] >>> 1];
x64Xor(h, shifted);
x64Multiply(h, F1);
shifted[1] = h[0] >>> 1;
x64Xor(h, shifted);
x64Multiply(h, F2);
shifted[1] = h[0] >>> 1;
x64Xor(h, shifted);
}
const C1 = [0x87c37b91, 0x114253d5];
const C2 = [0x4cf5ad43, 0x2745937f];
const M$1 = [0, 5];
const N1 = [0, 0x52dce729];
const N2 = [0, 0x38495ab5];
/**
* Given a string and an optional seed as an int, returns a 128 bit
* hash using the x64 flavor of MurmurHash3, as an unsigned hex.
* All internal functions mutates passed value to achieve minimal memory allocations and GC load
*
* Benchmark https://jsbench.me/p4lkpaoabi/1
*/
function x64hash128(input, seed) {
const key = getUTF8Bytes(input);
seed = seed || 0;
const length = [0, key.length];
const remainder = length[1] % 16;
const bytes = length[1] - remainder;
const h1 = [0, seed];
const h2 = [0, seed];
const k1 = [0, 0];
const k2 = [0, 0];
let i;
for (i = 0; i < bytes; i = i + 16) {
k1[0] = key[i + 4] | (key[i + 5] << 8) | (key[i + 6] << 16) | (key[i + 7] << 24);
k1[1] = key[i] | (key[i + 1] << 8) | (key[i + 2] << 16) | (key[i + 3] << 24);
k2[0] = key[i + 12] | (key[i + 13] << 8) | (key[i + 14] << 16) | (key[i + 15] << 24);
k2[1] = key[i + 8] | (key[i + 9] << 8) | (key[i + 10] << 16) | (key[i + 11] << 24);
x64Multiply(k1, C1);
x64Rotl(k1, 31);
x64Multiply(k1, C2);
x64Xor(h1, k1);
x64Rotl(h1, 27);
x64Add(h1, h2);
x64Multiply(h1, M$1);
x64Add(h1, N1);
x64Multiply(k2, C2);
x64Rotl(k2, 33);
x64Multiply(k2, C1);
x64Xor(h2, k2);
x64Rotl(h2, 31);
x64Add(h2, h1);
x64Multiply(h2, M$1);
x64Add(h2, N2);
}
k1[0] = 0;
k1[1] = 0;
k2[0] = 0;
k2[1] = 0;
const val = [0, 0];
switch (remainder) {
case 15:
val[1] = key[i + 14];
x64LeftShift(val, 48);
x64Xor(k2, val);
// fallthrough
case 14:
val[1] = key[i + 13];
x64LeftShift(val, 40);
x64Xor(k2, val);
// fallthrough
case 13:
val[1] = key[i + 12];
x64LeftShift(val, 32);
x64Xor(k2, val);
// fallthrough
case 12:
val[1] = key[i + 11];
x64LeftShift(val, 24);
x64Xor(k2, val);
// fallthrough
case 11:
val[1] = key[i + 10];
x64LeftShift(val, 16);
x64Xor(k2, val);
// fallthrough
case 10:
val[1] = key[i + 9];
x64LeftShift(val, 8);
x64Xor(k2, val);
// fallthrough
case 9:
val[1] = key[i + 8];
x64Xor(k2, val);
x64Multiply(k2, C2);
x64Rotl(k2, 33);
x64Multiply(k2, C1);
x64Xor(h2, k2);
// fallthrough
case 8:
val[1] = key[i + 7];
x64LeftShift(val, 56);
x64Xor(k1, val);
// fallthrough
case 7:
val[1] = key[i + 6];
x64LeftShift(val, 48);
x64Xor(k1, val);
// fallthrough
case 6:
val[1] = key[i + 5];
x64LeftShift(val, 40);
x64Xor(k1, val);
// fallthrough
case 5:
val[1] = key[i + 4];
x64LeftShift(val, 32);
x64Xor(k1, val);
// fallthrough
case 4:
val[1] = key[i + 3];
x64LeftShift(val, 24);
x64Xor(k1, val);
// fallthrough
case 3:
val[1] = key[i + 2];
x64LeftShift(val, 16);
x64Xor(k1, val);
// fallthrough
case 2:
val[1] = key[i + 1];
x64LeftShift(val, 8);
x64Xor(k1, val);
// fallthrough
case 1:
val[1] = key[i];
x64Xor(k1, val);
x64Multiply(k1, C1);
x64Rotl(k1, 31);
x64Multiply(k1, C2);
x64Xor(h1, k1);
// fallthrough
}
x64Xor(h1, length);
x64Xor(h2, length);
x64Add(h1, h2);
x64Add(h2, h1);
x64Fmix(h1);
x64Fmix(h2);
x64Add(h1, h2);
x64Add(h2, h1);
return (('00000000' + (h1[0] >>> 0).toString(16)).slice(-8) +
('00000000' + (h1[1] >>> 0).toString(16)).slice(-8) +
('00000000' + (h2[0] >>> 0).toString(16)).slice(-8) +
('00000000' + (h2[1] >>> 0).toString(16)).slice(-8));
}
/**
* Converts an error object to a plain object that can be used with `JSON.stringify`.
* If you just run `JSON.stringify(error)`, you'll get `'{}'`.
*/
function errorToObject(error) {
var _a;
return {
name: error.name,
message: error.message,
stack: (_a = error.stack) === null || _a === void 0 ? void 0 : _a.split('\n'),
// The fields are not enumerable, so TS is wrong saying that they will be overridden
...error,
};
}
function isFunctionNative(func) {
return /^function\s.*?\{\s*\[native code]\s*}$/.test(String(func));
}
/* eslint-disable @typescript-eslint/no-explicit-any */
function isFinalResultLoaded(loadResult) {
return typeof loadResult !== 'function';
}
/**
* Loads the given entropy source. Returns a function that gets an entropy component from the source.
*
* The result is returned synchronously to prevent `loadSources` from
* waiting for one source to load before getting the components from the other sources.
*/
function loadSource(source, sourceOptions) {
const sourceLoadPromise = suppressUnhandledRejectionWarning(new Promise((resolveLoad) => {
const loadStartTime = Date.now();
// `awaitIfAsync` is used instead of just `await` in order to measure the duration of synchronous sources
// correctly (other microtasks won't affect the duration).
awaitIfAsync(source.bind(null, sourceOptions), (...loadArgs) => {
const loadDuration = Date.now() - loadStartTime;
// Source loading failed
if (!loadArgs[0]) {
return resolveLoad(() => ({ error: loadArgs[1], duration: loadDuration }));
}
const loadResult = loadArgs[1];
// Source loaded with the final result
if (isFinalResultLoaded(loadResult)) {
return resolveLoad(() => ({ value: loadResult, duration: loadDuration }));
}
// Source loaded with "get" stage
resolveLoad(() => new Promise((resolveGet) => {
const getStartTime = Date.now();
awaitIfAsync(loadResult, (...getArgs) => {
const duration = loadDuration + Date.now() - getStartTime;
// Source getting failed
if (!getArgs[0]) {
return resolveGet({ error: getArgs[1], duration });
}
// Source getting succeeded
resolveGet({ value: getArgs[1], duration });
});
}));
});
}));
return function getComponent() {
return sourceLoadPromise.then((finalizeSource) => finalizeSource());
};
}
/**
* Loads the given entropy sources. Returns a function that collects the entropy components.
*
* The result is returned synchronously in order to allow start getting the components
* before the sources are loaded completely.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function loadSources(sources, sourceOptions, excludeSources, loopReleaseInterval) {
const includedSources = Object.keys(sources).filter((sourceKey) => excludes(excludeSources, sourceKey));
// Using `mapWithBreaks` allows asynchronous sources to complete between synchronous sources
// and measure the duration correctly
const sourceGettersPromise = suppressUnhandledRejectionWarning(mapWithBreaks(includedSources, (sourceKey) => loadSource(sources[sourceKey], sourceOptions), loopReleaseInterval));
return async function getComponents() {
const sourceGetters = await sourceGettersPromise;
const componentPromises = await mapWithBreaks(sourceGetters, (sourceGetter) => suppressUnhandledRejectionWarning(sourceGetter()), loopReleaseInterval);
const componentArray = await Promise.all(componentPromises);
// Keeping the component keys order the same as the source keys order
const components = {};
for (let index = 0; index < includedSources.length; ++index) {
components[includedSources[index]] = componentArray[index];
}
return components;
};
}
/**
* Modifies an entropy source by transforming its returned value with the given function.
* Keeps the source properties: sync/async, 1/2 stages.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function transformSource(source, transformValue) {
const transformLoadResult = (loadResult) => {
if (isFinalResultLoaded(loadResult)) {
return transformValue(loadResult);
}
return () => {
const getResult = loadResult();
if (isPromise(getResult)) {
return getResult.then(transformValue);
}
return transformValue(getResult);
};
};
return (options) => {
const loadResult = source(options);
if (isPromise(loadResult)) {
return loadResult.then(transformLoadResult);
}
return transformLoadResult(loadResult);
};
}
/*
* Functions to help with features that vary through browsers
*/
/**
* Checks whether the browser is based on Trident (the Internet Explorer engine) without using user-agent.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function isTrident() {
const w = window;
const n = navigator;
// The properties are checked to be in IE 10, IE 11 and not to be in other browsers in October 2020
return (countTruthy([
'MSCSSMatrix' in w,
'msSetImmediate' in w,
'msIndexedDB' in w,
'msMaxTouchPoints' in n,
'msPointerEnabled' in n,
]) >= 4);
}
/**
* Checks whether the browser is based on EdgeHTML (the pre-Chromium Edge engine) without using user-agent.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function isEdgeHTML() {
// Based on research in October 2020
const w = window;
const n = navigator;
return (countTruthy(['msWriteProfilerMark' in w, 'MSStream' in w, 'msLaunchUri' in n, 'msSaveBlob' in n]) >= 3 &&
!isTrident());
}
/**
* Checks whether the browser is based on Chromium without using user-agent.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function isChromium() {
// Based on research in October 2020. Tested to detect Chromium 42-86.
const w = window;
const n = navigator;
return (countTruthy([
'webkitPersistentStorage' in n,
'webkitTemporaryStorage' in n,
(n.vendor || '').indexOf('Google') === 0,
'webkitResolveLocalFileSystemURL' in w,
'BatteryManager' in w,
'webkitMediaStream' in w,
'webkitSpeechGrammar' in w,
]) >= 5);
}
/**
* Checks whether the browser is based on mobile or desktop Safari without using user-agent.
* All iOS browsers use WebKit (the Safari engine).
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function isWebKit() {
// Based on research in August 2024
const w = window;
const n = navigator;
return (countTruthy([
'ApplePayError' in w,
'CSSPrimitiveValue' in w,
'Counter' in w,
n.vendor.indexOf('Apple') === 0,
'RGBColor' in w,
'WebKitMediaKeys' in w,
]) >= 4);
}
/**
* Checks whether this WebKit browser is a desktop browser.
* It doesn't check that the browser is based on WebKit, there is a separate function for this.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function isDesktopWebKit() {
// Checked in Safari and DuckDuckGo
const w = window;
const { HTMLElement, Document } = w;
return (countTruthy([
'safari' in w,
!('ongestureend' in w),
!('TouchEvent' in w),
!('orientation' in w),
HTMLElement && !('autocapitalize' in HTMLElement.prototype),
Document && 'pointerLockElement' in Document.prototype,
]) >= 4);
}
/**
* Checks whether this WebKit browser is Safari.
* It doesn't check that the browser is based on WebKit, there is a separate function for this.
*
* Warning! The function works properly only for Safari version 15.4 and newer.
*/
function isSafariWebKit() {
// Checked in Safari, Chrome, Firefox, Yandex, UC Browser, Opera, Edge and DuckDuckGo.
// iOS Safari and Chrome were checked on iOS 11-18. DuckDuckGo was checked on iOS 17-18 and macOS 14-15.
// Desktop Safari versions 12-18 were checked.
// The other browsers were checked on iOS 17 and 18; there was no chance to check them on the other OS versions.
const w = window;
return (
// Filters-out Chrome, Yandex, DuckDuckGo (macOS and iOS), Edge
isFunctionNative(w.print) &&
// Doesn't work in Safari < 15.4
String(w.browser) === '[object WebPageNamespace]');
}
/**
* Checks whether the browser is based on Gecko (Firefox engine) without using user-agent.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function isGecko() {
var _a, _b;
const w = window;
// Based on research in September 2020
return (countTruthy([
'buildID' in navigator,
'MozAppearance' in ((_b = (_a = document.documentElement) === null || _a === void 0 ? void 0 : _a.style) !== null && _b !== void 0 ? _b : {}),
'onmozfullscreenchange' in w,
'mozInnerScreenX' in w,
'CSSMozDocumentRule' in w,
'CanvasCaptureMediaStream' in w,
]) >= 4);
}
/**
* Checks whether the browser is based on Gecko version ≥120 (Firefox ≥120) without using user-agent.
* It doesn't check that the browser is based on Gecko; there is a separate function for this.
*
* @see https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/120 Firefox 120 release notes
*/
function isGecko120OrNewer() {
// Checked in Firefox 119 vs. Firefox 120+
const w = window;
const n = navigator;
const { CSS } = w;
// We use a threshold of 3 out of 4 because `globalPrivacyControl` was added in Firefox 120 on desktop,
// but only in Firefox 122 on mobile. Using >= 3 ensures detection works on both platforms for Firefox 120+.
// @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/globalPrivacyControl#browser_compatibility
return (countTruthy([
// User Activation API - added in Firefox 120
'userActivation' in n,
// CSS light-dark() function - added in Firefox 120
CSS.supports('color', 'light-dark(#000, #fff)'),
// CSS lh unit - added in Firefox 120
CSS.supports('height', '1lh'),
// Global Privacy Control - added in Firefox 120 (desktop) / Firefox 122 (mobile)
'globalPrivacyControl' in n,
]) >= 3);
}
/**
* Checks whether the browser is based on Gecko version ≥143 (Firefox ≥143) without using user-agent.
* It doesn't check that the browser is based on Gecko; there is a separate function for this.
*
* Firefox 143 shipped Phase 2 fingerprinting protections (screen resolution, processor count, touch points)
* in Private Browsing and ETP Strict mode.
*
* @see https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/143 Firefox 143 release notes
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=1978414 Bug that shipped these protections
*/
function isGecko143OrNewer() {
// Checked in Firefox 142 vs. Firefox 143+
const { CSS } = window;
return (countTruthy([
// CSS ::details-content pseudo-element - added in Firefox 143
CSS.supports('selector(::details-content)'),
// CSS ::before::marker nested pseudo-element - added in Firefox 143
CSS.supports('selector(::before::marker)'),
// CSS ::after::marker nested pseudo-element - added in Firefox 143
CSS.supports('selector(::after::marker)'),
// CompositionEvent.locale property - removed in Firefox 143 (Bug 1700969)
!('locale' in CompositionEvent.prototype),
]) >= 3);
}
/**
* Checks whether the browser is based on Chromium version ≥86 without using user-agent.
* It doesn't check that the browser is based on Chromium, there is a separate function for this.
*/
function isChromium86OrNewer() {
// Checked in Chrome 85 vs Chrome 86 both on desktop and Android. Checked in macOS Chrome 128, Android Chrome 127.
const w = window;
return (countTruthy([
!('MediaSettingsRange' in w),
'RTCEncodedAudioFrame' in w,
'' + w.Intl === '[object Intl]',
'' + w.Reflect === '[object Reflect]',
]) >= 3);
}
/**
* Checks whether the browser is based on Chromium version ≥122 without using user-agent.
* It doesn't check that the browser is based on Chromium, there is a separate function for this.
*/
function isChromium122OrNewer() {
// Checked in Chrome 121 vs Chrome 122 and 129 both on desktop and Android
const w = window;
const { URLPattern } = w;
return (countTruthy([
'union' in Set.prototype,
'Iterator' in w,
URLPattern && 'hasRegExpGroups' in URLPattern.prototype,
'RGB8' in WebGLRenderingContext.prototype,
]) >= 3);
}
/**
* Checks whether the browser is based on WebKit version ≥606 (Safari ≥12) without using user-agent.
* It doesn't check that the browser is based on WebKit, there is a separate function for this.
*
* @see https://en.wikipedia.org/wiki/Safari_version_history#Release_history Safari-WebKit versions map
*/
function isWebKit606OrNewer() {
// Checked in Safari 9–18
const w = window;
return (countTruthy([
'DOMRectList' in w,
'RTCPeerConnectionIceEvent' in w,
'SVGGeometryElement' in w,
'ontransitioncancel' in w,
]) >= 3);
}
/**
* Checks whether the browser is based on WebKit version ≥616 (Safari ≥17) without using user-agent.
* It doesn't check that the browser is based on WebKit, there is a separate function for this.
*
* @see https://developer.apple.com/documentation/safari-release-notes/safari-17-release-notes Safari 17 release notes
* @see https://tauri.app/v1/references/webview-versions/#webkit-versions-in-safari Safari-WebKit versions map
*/
function isWebKit616OrNewer() {
const w = window;
const n = navigator;
const { CSS, HTMLButtonElement } = w;
return (countTruthy([
!('getStorageUpdates' in n),
HTMLButtonElement && 'popover' in HTMLButtonElement.prototype,
'CSSCounterStyleRule' in w,
CSS.supports('font-size-adjust: ex-height 0.5'),
CSS.supports('text-transform: full-width'),
]) >= 4);
}
/**
* Checks whether the device is an iPad.
* It doesn't check that the engine is WebKit and that the WebKit isn't desktop.
*/
function isIPad() {
// Checked on:
// Safari on iPadOS (both mobile and desktop modes): 8, 11-18
// Chrome on iPadOS (both mobile and desktop modes): 11-18
// Safari on iOS (both mobile and desktop modes): 9-18
// Chrome on iOS (both mobile and desktop modes): 9-18
// Before iOS 13. Safari tampers the value in "request desktop site" mode since iOS 13.
if (navigator.platform === 'iPad') {
return true;
}
const s = screen;
const screenRatio = s.width / s.height;
return (countTruthy([
// Since iOS 13. Doesn't work in Chrome on iPadOS <15, but works in desktop mode.
'MediaSource' in window,
// Since iOS 12. Doesn't work in Chrome on iPadOS.
!!Element.prototype.webkitRequestFullscreen,
// iPhone 4S that runs iOS 9 matches this, but it is not supported
// Doesn't work in incognito mode of Safari ≥17 with split screen because of tracking prevention
screenRatio > 0.65 && screenRatio < 1.53,
]) >= 2);
}
/**
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function getFullscreenElement() {
const d = document;
return d.fullscreenElement || d.msFullscreenElement || d.mozFullScreenElement || d.webkitFullscreenElement || null;
}
function exitFullscreen() {
const d = document;
// `call` is required because the function throws an error without a proper "this" context
return (d.exitFullscreen || d.msExitFullscreen || d.mozCancelFullScreen || d.webkitExitFullscreen).call(d);
}
/**
* Checks whether the device runs on Android without using user-agent.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function isAndroid() {
const isItChromium = isChromium();
const isItGecko = isGecko();
const w = window;
const n = navigator;
const c = 'connection';
// Chrome removes all words "Android" from `navigator` when desktop version is requested
// Firefox keeps "Android" in `navigator.appVersion` when desktop version is requested
if (isItChromium) {
return (countTruthy([
!('SharedWorker' in w),
// `typechange` is deprecated, but it's still present on Android (tested on Chrome Mobile 117)
// Removal proposal https://bugs.chromium.org/p/chromium/issues/detail?id=699892
// Note: this expression returns true on ChromeOS, so additional detectors are required to avoid false-positives
n[c] && 'ontypechange' in n[c],
!('sinkId' in new Audio()),
]) >= 2);
}
else if (isItGecko) {
return countTruthy(['onorientationchange' in w, 'orientation' in w, /android/i.test(n.appVersion)]) >= 2;
}
else {
// Only 2 browser engines are presented on Android.
// Actually, there is also Android 4.1 browser, but it's not worth detecting it at the moment.
return false;
}
}
/**
* Checks whether the browser is Samsung Internet without using user-agent.
* It doesn't check that the browser is based on Chromium, please use `isChromium` before using this function.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function isSamsungInternet() {
// Checked in Samsung Internet 21, 25 and 27
const n = navigator;
const w = window;
const audioPrototype = Audio.prototype;
const { visualViewport } = w;
return (countTruthy([
'srLatency' in audioPrototype,
'srChannelCount' in audioPrototype,
'devicePosture' in n,
visualViewport && 'segments' in visualViewport,
'getTextInformation' in Image.prototype, // Not available in Samsung Internet 21
]) >= 3);
}
/**
* A deep description: https://fingerprint.com/blog/audio-fingerprinting/
* Inspired by and based on https://github.com/cozylife/audio-fingerprint
*
* A version of the entropy source with stabilization to make it suitable for static fingerprinting.
* Audio signal is noised in private mode of Safari 17, so audio fingerprinting is skipped in Safari 17.
*/
function getAudioFingerprint() {
if (doesBrowserPerformAntifingerprinting()) {
return -4 /* SpecialFingerprint.KnownForAntifingerprinting */;
}
return getUnstableAudioFingerprint();
}
/**
* A version of the entropy source without stabilization.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function getUnstableAudioFingerprint() {
const w = window;
const AudioContext = w.OfflineAudioContext || w.webkitOfflineAudioContext;
if (!AudioContext) {
return -2 /* SpecialFingerprint.NotSupported */;
}
// In some browsers, audio context always stays suspended unless the context is started in response to a user action
// (e.g. a click or a tap). It prevents audio fingerprint from being taken at an arbitrary moment of time.
// Such browsers are old and unpopular, so the audio fingerprinting is just skipped in them.
// See a similar case explanation at https://stackoverflow.com/questions/46363048/onaudioprocess-not-called-on-ios11#46534088
if (doesBrowserSuspendAudioContext()) {
return -1 /* SpecialFingerprint.KnownForSuspending */;
}
const hashFromIndex = 4500;
const hashToIndex = 5000;
const context = new AudioContext(1, hashToIndex, 44100);
const oscillator = context.createOscillator();
oscillator.type = 'triangle';
oscillator.frequency.value = 10000;
const compressor = context.createDynamicsCompressor();
compressor.threshold.value = -50;
compressor.knee.value = 40;
compressor.ratio.value = 12;
compressor.attack.value = 0;
compressor.release.value = 0.25;
oscillator.connect(compressor);
compressor.connect(context.destination);
oscillator.start(0);
const [renderPromise, finishRendering] = startRenderingAudio(context);
// Suppresses the console error message in case when the fingerprint fails before requested
const fingerprintPromise = suppressUnhandledRejectionWarning(renderPromise.then((buffer) => getHash(buffer.getChannelData(0).subarray(hashFromIndex)), (error) => {
if (error.name === "timeout" /* InnerErrorName.Timeout */ || error.name === "suspended" /* InnerErrorName.Suspended */) {
return -3 /* SpecialFingerprint.Timeout */;
}
throw error;
}));
return () => {
finishRendering();
return fingerprintPromise;
};
}
/**
* Checks if the current browser is known for always suspending audio context
*/
function doesBrowserSuspendAudioContext() {
// Mobile Safari 11 and older
return isWebKit() && !isDesktopWebKit() && !isWebKit606OrNewer();
}
/**
* Checks if the current browser is known for applying anti-fingerprinting measures in all or some critical modes
*/
function doesBrowserPerformAntifingerprinting() {
return (
// Safari ≥17
(isWebKit() && isWebKit616OrNewer() && isSafariWebKit()) ||
// Samsung Internet ≥26
(isChromium() && isSamsungInternet() && isChromium122OrNewer()));
}
/**
* Starts rendering the audio context.
* When the returned function is called, the render process starts finishing.
*/
function startRenderingAudio(context) {
const renderTryMaxCount = 3;
const renderRetryDelay = 500;
const runningMaxAwaitTime = 500;
const runningSufficientTime = 5000;
let finalize = () => undefined;
const resultPromise = new Promise((resolve, reject) => {
let isFinalized = false;
let renderTryCount = 0;
let startedRunningAt = 0;
context.oncomplete = (event) => resolve(event.renderedBuffer);
const startRunningTimeout = () => {
setTimeout(() => reject(makeInnerError("timeout" /* InnerErrorName.Timeout */)), Math.min(runningMaxAwaitTime, startedRunningAt + runningSufficientTime - Date.now()));
};
const tryRender = () => {
try {
const renderingPromise = context.startRendering();
// `context.startRendering` has two APIs: Promise and callback, we check that it's really a promise just in case
if (isPromise(renderingPromise)) {
// Suppresses all unhandled rejections in case of scheduled redundant retries after successful rendering
suppressUnhandledRejectionWarning(renderingPromise);
}
switch (context.state) {
case 'running':
startedRunningAt = Date.now();
if (isFinalized) {
startRunningTimeout();
}
break;
// Sometimes the audio context doesn't start after calling `startRendering` (in addition to the cases where
// audio context doesn't start at all). A known case is starting an audio context when the browser tab is in
// background on iPhone. Retries usually help in this case.
case 'suspended':
// The audio context can reject starting until the tab is in foreground. Long fingerprint duration
// in background isn't a problem, therefore the retry attempts don't count in background. It can lead to
// a situation when a fingerprint takes very long time and finishes successfully. FYI, the audio context
// can be suspended when `document.hidden === false` and start running after a retry.
if (!document.hidden) {
renderTryCount++;
}
if (isFinalized && renderTryCount >= renderTryMaxCount) {
reject(makeInnerError("suspended" /* InnerErrorName.Suspended */));
}
else {
setTimeout(tryRender, renderRetryDelay);
}
break;
}
}
catch (error) {
reject(error);
}
};
tryRender();
finalize = () => {
if (!isFinalized) {
isFinalized = true;
if (startedRunningAt > 0) {
startRunningTimeout();
}
}
};
});
return [resultPromise, finalize];
}
function getHash(signal) {
let hash = 0;
for (let i = 0; i < signal.length; ++i) {
hash += Math.abs(signal[i]);
}
return hash;
}
function makeInnerError(name) {
const error = new Error(name);
error.name = name;
return error;
}
/**
* Creates and keeps an invisible iframe while the given function runs.
* The given function is called when the iframe is loaded and has a body.
* The iframe allows to measure DOM sizes inside itself.
*
* Notice: passing an initial HTML code doesn't work in IE.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
async function withIframe(action, initialHtml, domPollInterval = 50) {
var _a, _b, _c;
const d = document;
// document.body can be null while the page is loading
while (!d.body) {
await wait(domPollInterval);
}
const iframe = d.createElement('iframe');
try {
await new Promise((_resolve, _reject) => {
let isComplete = false;
const resolve = () => {
isComplete = true;
_resolve();
};
const reject = (error) => {
isComplete = true;
_reject(error);
};
iframe.onload = resolve;
iframe.onerror = reject;
const { style } = iframe;
style.setProperty('display', 'block', 'important'); // Required for browsers to calculate the layout
style.position = 'absolute';
style.top = '0';
style.left = '0';
style.visibility = 'hidden';
if (initialHtml && 'srcdoc' in iframe) {
iframe.srcdoc = initialHtml;
}
else {
iframe.src = 'about:blank';
}
d.body.appendChild(iframe);
// WebKit in WeChat doesn't fire the iframe's `onload` for some reason.
// This code checks for the loading state manually.
// See https://github.com/fingerprintjs/fingerprintjs/issues/645
const checkReadyState = () => {
var _a, _b;
// The ready state may never become 'complete' in Firefox despite the 'load' event being fired.
// So an infinite setTimeout loop can happen without this check.
// See https://github.com/fingerprintjs/fingerprintjs/pull/716#issuecomment-986898796
if (isComplete) {
return;
}
// Make sure iframe.contentWindow and iframe.contentWindow.document are both loaded
// The contentWindow.document can miss in JSDOM (https://github.com/jsdom/jsdom).
if (((_b = (_a = iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.document) === null || _b === void 0 ? void 0 : _b.readyState) === 'complete') {
resolve();
}
else {
setTimeout(checkReadyState, 10);
}
};
checkReadyState();
});
while (!((_b = (_a = iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.document) === null || _b === void 0 ? void 0 : _b.body)) {
await wait(domPollInterval);
}
return await action(iframe, iframe.contentWindow);
}
finally {
(_c = iframe.parentNode) === null || _c === void 0 ? void 0 : _c.removeChild(iframe);
}
}
/**
* Creates a DOM element that matches the given selector.
* Only single element selector are supported (without operators like space, +, >, etc).
*/
function selectorToElement(selector) {
const [tag, attributes] = parseSimpleCssSelector(selector);
const element = document.createElement(tag !== null && tag !== void 0 ? tag : 'div');
for (const name of Object.keys(attributes)) {
const value = attributes[name].join(' ');
// Changing the `style` attribute can cause a CSP error, therefore we change the `style.cssText` property.
// https://github.com/fingerprintjs/fingerprintjs/issues/733
if (name === 'style') {
addStyleString(element.style, value);
}
else {
element.setAttribute(name, value);
}
}
return element;
}
/**
* Adds CSS styles from a string in such a way that doesn't trigger a CSP warning (unsafe-inline or unsafe-eval)
*/
function addStyleString(style, source) {
// We don't use `style.cssText` because browsers must block it when no `unsafe-eval` CSP is presented: https://csplite.com/csp145/#w3c_note
// Even though the browsers ignore this standard, we don't use `cssText` just in case.
for (const property of source.split(';')) {
const match = /^\s*([\w-]+)\s*:\s*(.+?)(\s*!([\w-]+))?\s*$/.exec(property);
if (match) {
const [, name, value, , priority] = match;
style.setProperty(name, value, priority || ''); // The last argument can't be undefined in IE11
}
}
}
/**
* Returns true if the code runs in an iframe, and any parent page's origin doesn't match the current origin
*/
function isAnyParentCrossOrigin() {
let currentWindow = window;
for (;;) {
const parentWindow = currentWindow.parent;
if (!parentWindow || parentWindow === currentWindow) {
return false; // The top page is reached
}
try {
if (parentWindow.location.origin !== currentWindow.location.origin) {