@fingerprintjs/fingerprintjs
Version:
Browser fingerprinting library with the highest accuracy and stability
1,273 lines (1,262 loc) • 128 kB
JavaScript
/**
* FingerprintJS v5.0.1 - Copyright (c) FingerprintJS, Inc, 2025 (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.0.1";
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 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$1()) {
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$1() {
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) {
return true;
}
}
catch (error) {
// The error is thrown when `origin` is accessed on `parentWindow.location` when the parent is cross-origin
if (error instanceof Error && error.name === 'SecurityError') {
return true;
}
throw error;
}
currentWindow = parentWindow;
}
}
// We use m or w because these two characters take up the maximum width.
// And we use a LLi so that the same matching fonts can get separated.
const testString = 'mmMwWLliI0O&1';
// We test using 48px font size, we may use any size. I guess larger the better.
const textSize = '48px';
// A font will be compared against all the three default fonts.
// And if for any default fonts it doesn't match, then that font is available.
const baseFonts = ['monospace', 'sans-serif', 'serif'];
const fontList = [
// This is android-specific font from "Roboto" family
'sans-serif-thin',
'ARNO PRO',
'Agency FB',
'Arabic Typesetting',
'Arial Unicode MS',
'AvantGarde Bk BT',
'BankGothic Md BT',
'Batang',
'Bitstream Vera Sans Mono',
'Calibri',
'Century',
'Century Gothic',
'Clarendon',
'EUROSTILE',
'Franklin Gothic',
'Futura Bk BT',
'Futura Md BT',
'GOTHAM',
'Gill Sans',
'HELV',
'Haettenschweiler',
'Helvetica Neue',
'Humanst521 BT',
'Leelawadee',
'Letter Gothic',
'Levenim MT',
'Lucida Bright',
'Lucida Sans',
'Menlo',
'MS Mincho',
'MS Outlook',
'MS Reference Specialty',
'MS UI Gothic',
'MT Extra',
'MYRIAD PRO',
'Marlett',
'Meiryo UI',
'Microsoft Uighur',
'Minion Pro',
'Monotype Corsiva',
'PMingLiU',
'Pristina',
'SCRIPTINA',
'Segoe UI Light',
'Serifa',
'SimHei',
'Small Fonts',
'Staccato222 BT',
'TRAJAN PRO',
'Univers CE 55 Medium',
'Vrinda',
'ZWAdobeF',
];
// kudos to http://www.lalit.org/lab/javascript-css-font-detect/
function getFonts() {
// Running the script in an iframe makes it not affect the page look and not be affected by the page CSS. See:
// https://github.com/fingerprintjs/fingerprintjs/issues/592
// https://github.com/fingerprintjs/fingerprintjs/issues/628
return withIframe(async (_, { document }) => {
const holder = document.body;
holder.style.fontSize