@fingerprintjs/fingerprintjs
Version:
Browser fingerprinting library with the highest accuracy and stability
1,288 lines (1,277 loc) • 131 kB
JavaScript
/**
* FingerprintJS v4.6.1 - Copyright (c) FingerprintJS, Inc, 2025 (https://fingerprint.com)
*
* Licensed under Business Source License 1.1 https://mariadb.com/bsl11/
* Licensor: FingerprintJS, Inc.
* Licensed Work: FingerprintJS browser fingerprinting library
* Additional Use Grant: None
* Change Date: Four years from first release for the specific version.
* Change License: MIT, text at https://opensource.org/license/mit/ with the following copyright notice:
* Copyright 2015-present FingerprintJS, Inc.
*/
import { __awaiter, __generator, __assign, __spreadArray } from 'tslib';
var version = "4.6.1";
function wait(durationMs, resolveWith) {
return new Promise(function (resolve) { return 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(function (resolve) {
var channel = new MessageChannel();
channel.port1.onmessage = function () { return resolve(); };
channel.port2.postMessage(null);
});
}
function requestIdleCallbackIfAvailable(fallbackTimeout, deadlineTimeout) {
if (deadlineTimeout === void 0) { deadlineTimeout = Infinity; }
var requestIdleCallback = window.requestIdleCallback;
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(function (resolve) { return requestIdleCallback.call(window, function () { return 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 {
var returnedValue = action();
if (isPromise(returnedValue)) {
returnedValue.then(function (result) { return callback(true, result); }, function (error) { return 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.
*/
function mapWithBreaks(items, callback, loopReleaseInterval) {
if (loopReleaseInterval === void 0) { loopReleaseInterval = 16; }
return __awaiter(this, void 0, void 0, function () {
var results, lastLoopReleaseTime, i, now;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
results = Array(items.length);
lastLoopReleaseTime = Date.now();
i = 0;
_a.label = 1;
case 1:
if (!(i < items.length)) return [3 /*break*/, 4];
results[i] = callback(items[i], i);
now = Date.now();
if (!(now >= lastLoopReleaseTime + loopReleaseInterval)) return [3 /*break*/, 3];
lastLoopReleaseTime = now;
return [4 /*yield*/, releaseEventLoop()];
case 2:
_a.sent();
_a.label = 3;
case 3:
++i;
return [3 /*break*/, 1];
case 4: return [2 /*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, function () { return 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 (var 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(function (sum, value) { return sum + (value ? 1 : 0); }, 0);
}
function round(value, base) {
if (base === void 0) { 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.
var 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;
var errorMessage = "Unexpected syntax '".concat(selector, "'");
var tagMatch = /^\s*([a-z-]*)(.*)$/i.exec(selector);
var tag = tagMatch[1] || undefined;
var attributes = {};
var partsRegex = /([.:#][\w-]+|\[.+?\])/gi;
var addAttribute = function (name, value) {
attributes[name] = attributes[name] || [];
attributes[name].push(value);
};
for (;;) {
var match = partsRegex.exec(tagMatch[2]);
if (!match) {
break;
}
var part = match[0];
switch (part[0]) {
case '.':
addAttribute('class', part.slice(1));
break;
case '#':
addAttribute('id', part.slice(1));
break;
case '[': {
var 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
var result = new Uint8Array(input.length);
for (var i = 0; i < input.length; i++) {
// `charCode` is faster than encoding, so we prefer that when it's possible
var 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) {
var m0 = m[0] >>> 16, m1 = m[0] & 0xffff, m2 = m[1] >>> 16, m3 = m[1] & 0xffff;
var n0 = n[0] >>> 16, n1 = n[0] & 0xffff, n2 = n[1] >>> 16, n3 = n[1] & 0xffff;
var 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) {
var m0 = m[0] >>> 16, m1 = m[0] & 0xffff, m2 = m[1] >>> 16, m3 = m[1] & 0xffff;
var n0 = n[0] >>> 16, n1 = n[0] & 0xffff, n2 = n[1] >>> 16, n3 = n[1] & 0xffff;
var 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) {
var 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];
}
var F1 = [0xff51afd7, 0xed558ccd];
var 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) {
var 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);
}
var C1 = [0x87c37b91, 0x114253d5];
var C2 = [0x4cf5ad43, 0x2745937f];
var M$1 = [0, 5];
var N1 = [0, 0x52dce729];
var 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) {
var key = getUTF8Bytes(input);
seed = seed || 0;
var length = [0, key.length];
var remainder = length[1] % 16;
var bytes = length[1] - remainder;
var h1 = [0, seed];
var h2 = [0, seed];
var k1 = [0, 0];
var k2 = [0, 0];
var 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;
var 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 __assign({ name: error.name, message: error.message, stack: (_a = error.stack) === null || _a === void 0 ? void 0 : _a.split('\n') }, error);
}
function isFunctionNative(func) {
return /^function\s.*?\{\s*\[native code]\s*}$/.test(String(func));
}
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) {
var sourceLoadPromise = suppressUnhandledRejectionWarning(new Promise(function (resolveLoad) {
var 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), function () {
var loadArgs = [];
for (var _i = 0; _i < arguments.length; _i++) {
loadArgs[_i] = arguments[_i];
}
var loadDuration = Date.now() - loadStartTime;
// Source loading failed
if (!loadArgs[0]) {
return resolveLoad(function () { return ({ error: loadArgs[1], duration: loadDuration }); });
}
var loadResult = loadArgs[1];
// Source loaded with the final result
if (isFinalResultLoaded(loadResult)) {
return resolveLoad(function () { return ({ value: loadResult, duration: loadDuration }); });
}
// Source loaded with "get" stage
resolveLoad(function () {
return new Promise(function (resolveGet) {
var getStartTime = Date.now();
awaitIfAsync(loadResult, function () {
var getArgs = [];
for (var _i = 0; _i < arguments.length; _i++) {
getArgs[_i] = arguments[_i];
}
var duration = loadDuration + Date.now() - getStartTime;
// Source getting failed
if (!getArgs[0]) {
return resolveGet({ error: getArgs[1], duration: duration });
}
// Source getting succeeded
resolveGet({ value: getArgs[1], duration: duration });
});
});
});
});
}));
return function getComponent() {
return sourceLoadPromise.then(function (finalizeSource) { return 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) {
var includedSources = Object.keys(sources).filter(function (sourceKey) { return excludes(excludeSources, sourceKey); });
// Using `mapWithBreaks` allows asynchronous sources to complete between synchronous sources
// and measure the duration correctly
var sourceGettersPromise = suppressUnhandledRejectionWarning(mapWithBreaks(includedSources, function (sourceKey) { return loadSource(sources[sourceKey], sourceOptions); }, loopReleaseInterval));
return function getComponents() {
return __awaiter(this, void 0, void 0, function () {
var sourceGetters, componentPromises, componentArray, components, index;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, sourceGettersPromise];
case 1:
sourceGetters = _a.sent();
return [4 /*yield*/, mapWithBreaks(sourceGetters, function (sourceGetter) { return suppressUnhandledRejectionWarning(sourceGetter()); }, loopReleaseInterval)];
case 2:
componentPromises = _a.sent();
return [4 /*yield*/, Promise.all(componentPromises)
// Keeping the component keys order the same as the source keys order
];
case 3:
componentArray = _a.sent();
components = {};
for (index = 0; index < includedSources.length; ++index) {
components[includedSources[index]] = componentArray[index];
}
return [2 /*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) {
var transformLoadResult = function (loadResult) {
if (isFinalResultLoaded(loadResult)) {
return transformValue(loadResult);
}
return function () {
var getResult = loadResult();
if (isPromise(getResult)) {
return getResult.then(transformValue);
}
return transformValue(getResult);
};
};
return function (options) {
var 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() {
var w = window;
var 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
var w = window;
var 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.
var w = window;
var 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
var w = window;
var 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
var w = window;
var HTMLElement = w.HTMLElement, Document = w.Document;
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.
var 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;
var 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.
var 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
var w = window;
var URLPattern = w.URLPattern;
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
var 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() {
var w = window;
var n = navigator;
var CSS = w.CSS, HTMLButtonElement = w.HTMLButtonElement;
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;
}
var s = screen;
var 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() {
var d = document;
return d.fullscreenElement || d.msFullscreenElement || d.mozFullScreenElement || d.webkitFullscreenElement || null;
}
function exitFullscreen() {
var 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() {
var isItChromium = isChromium();
var isItGecko = isGecko();
var w = window;
var n = navigator;
var 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
var n = navigator;
var w = window;
var audioPrototype = Audio.prototype;
var visualViewport = w.visualViewport;
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() {
var w = window;
var 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 */;
}
var hashFromIndex = 4500;
var hashToIndex = 5000;
var context = new AudioContext(1, hashToIndex, 44100);
var oscillator = context.createOscillator();
oscillator.type = 'triangle';
oscillator.frequency.value = 10000;
var 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);
var _a = startRenderingAudio(context), renderPromise = _a[0], finishRendering = _a[1];
// Suppresses the console error message in case when the fingerprint fails before requested
var fingerprintPromise = suppressUnhandledRejectionWarning(renderPromise.then(function (buffer) { return getHash(buffer.getChannelData(0).subarray(hashFromIndex)); }, function (error) {
if (error.name === "timeout" /* InnerErrorName.Timeout */ || error.name === "suspended" /* InnerErrorName.Suspended */) {
return -3 /* SpecialFingerprint.Timeout */;
}
throw error;
}));
return function () {
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) {
var renderTryMaxCount = 3;
var renderRetryDelay = 500;
var runningMaxAwaitTime = 500;
var runningSufficientTime = 5000;
var finalize = function () { return undefined; };
var resultPromise = new Promise(function (resolve, reject) {
var isFinalized = false;
var renderTryCount = 0;
var startedRunningAt = 0;
context.oncomplete = function (event) { return resolve(event.renderedBuffer); };
var startRunningTimeout = function () {
setTimeout(function () { return reject(makeInnerError("timeout" /* InnerErrorName.Timeout */)); }, Math.min(runningMaxAwaitTime, startedRunningAt + runningSufficientTime - Date.now()));
};
var tryRender = function () {
try {
var 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 = function () {
if (!isFinalized) {
isFinalized = true;
if (startedRunningAt > 0) {
startRunningTimeout();
}
}
};
});
return [resultPromise, finalize];
}
function getHash(signal) {
var hash = 0;
for (var i = 0; i < signal.length; ++i) {
hash += Math.abs(signal[i]);
}
return hash;
}
function makeInnerError(name) {
var 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.
*/
function withIframe(action, initialHtml, domPollInterval) {
var _a, _b, _c;
if (domPollInterval === void 0) { domPollInterval = 50; }
return __awaiter(this, void 0, void 0, function () {
var d, iframe;
return __generator(this, function (_d) {
switch (_d.label) {
case 0:
d = document;
_d.label = 1;
case 1:
if (!!d.body) return [3 /*break*/, 3];
return [4 /*yield*/, wait(domPollInterval)];
case 2:
_d.sent();
return [3 /*break*/, 1];
case 3:
iframe = d.createElement('iframe');
_d.label = 4;
case 4:
_d.trys.push([4, , 10, 11]);
return [4 /*yield*/, new Promise(function (_resolve, _reject) {
var isComplete = false;
var resolve = function () {
isComplete = true;
_resolve();
};
var reject = function (error) {
isComplete = true;
_reject(error);
};
iframe.onload = resolve;
iframe.onerror = reject;
var style = iframe.style;
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
var checkReadyState = function () {
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();
})];
case 5:
_d.sent();
_d.label = 6;
case 6:
if (!!((_b = (_a = iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.document) === null || _b === void 0 ? void 0 : _b.body)) return [3 /*break*/, 8];
return [4 /*yield*/, wait(domPollInterval)];
case 7:
_d.sent();
return [3 /*break*/, 6];
case 8: return [4 /*yield*/, action(iframe, iframe.contentWindow)];
case 9: return [2 /*return*/, _d.sent()];
case 10:
(_c = iframe.parentNode) === null || _c === void 0 ? void 0 : _c.removeChild(iframe);
return [7 /*endfinally*/];
case 11: return [2 /*return*/];
}
});
});
}
/**
* Creates a DOM element that matches the given selector.
* Only single element selector are supported (without operators like space, +, >, etc).
*/
function selectorToElement(selector) {
var _a = parseSimpleCssSelector(selector), tag = _a[0], attributes = _a[1];
var element = document.createElement(tag !== null && tag !== void 0 ? tag : 'div');
for (var _i = 0, _b = Object.keys(attributes); _i < _b.length; _i++) {
var name_1 = _b[_i];
var value = attributes[name_1].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_1 === 'style') {
addStyleString(element.style, value);
}
else {
element.setAttribute(name_1, 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 (var _i = 0, _a = source.split(';'); _i < _a.length; _i++) {
var property = _a[_i];
var match = /^\s*([\w-]+)\s*:\s*(.+?)(\s*!([\w-]+))?\s*$/.exec(property);
if (match) {
var name_2 = match[1], value = match[2], priority = match[4];
style.setProperty(name_2, 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() {
var currentWindow = window;
for (;;) {
var 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.
var testString = 'mmMwWLliI0O&1';
// We test using 48px font size, we may use any size. I guess larger the better.
var 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.
var baseFonts = ['monospace', 'sans-serif', 'serif'];
var 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 Spec