UNPKG

@govuk-one-login/frontend-device-intelligence

Version:
1,117 lines (1,038 loc) 38.6 kB
var ThumbmarkJS = (function (exports) { 'use strict'; // Define a function to parse the user agent string and return the browser name and version function getBrowser() { if (typeof navigator === "undefined") { return { name: "unknown", version: "unknown", }; } const ua = navigator.userAgent; // Define some regular expressions to match different browsers and their versions const regexes = [ // Edge /(?<name>Edge|Edg)\/(?<version>\d+(?:\.\d+)?)/, // Chrome, Chromium, Opera, Vivaldi, Brave, etc. /(?<name>(?:Chrome|Chromium|OPR|Opera|Vivaldi|Brave))\/(?<version>\d+(?:\.\d+)?)/, // Firefox, Waterfox, etc. /(?<name>(?:Firefox|Waterfox|Iceweasel|IceCat))\/(?<version>\d+(?:\.\d+)?)/, // Safari, Mobile Safari, etc. /(?<name>Safari)\/(?<version>\d+(?:\.\d+)?)/, // Internet Explorer, IE Mobile, etc. /(?<name>MSIE|Trident|IEMobile).+?(?<version>\d+(?:\.\d+)?)/, // Other browsers that use the format "BrowserName/version" /(?<name>[A-Za-z]+)\/(?<version>\d+\.?\d*)/, // Samsung internet browser /(?<name>SamsungBrowser)\/(?<version>\d+(?:\.\d+)?)/, ]; // Define a map for browser name translations const browserNameMap = { Edg: "Edge", OPR: "Opera", }; // Loop through the regexes and try to find a match for (const regex of regexes) { const match = regex.exec(ua); if (match === null || match === void 0 ? void 0 : match.groups) { // Translate the browser name if necessary const name = browserNameMap[match.groups.name] || match.groups.name; // Return the browser name and version return { name: name, version: match.groups.version, }; } } // If no match is found, return unknown return { name: "unknown", version: "unknown", }; } async function ephemeralIFrame(callback) { var _a; while (!document.body) { await wait(50); } const iframe = document.createElement("iframe"); iframe.setAttribute("frameBorder", "0"); const style = iframe.style; style.setProperty("position", "fixed"); style.setProperty("display", "block", "important"); style.setProperty("visibility", "visible"); style.setProperty("border", "0"); style.setProperty("opacity", "0"); iframe.src = "about:blank"; document.body.appendChild(iframe); const iframeDocument = iframe.contentDocument || ((_a = iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.document); if (!iframeDocument) { throw new Error("Iframe document is not accessible"); } // Execute the callback function with access to the iframe's document callback({ iframe: iframeDocument }); // Clean up after running the callback setTimeout(() => { document.body.removeChild(iframe); }, 0); } function wait(durationMs, resolveWith) { return new Promise((resolve) => setTimeout(resolve, durationMs, resolveWith)); } /** * This code is taken from https://github.com/LinusU/murmur-128/blob/master/index.js * But instead of dependencies to encode-utf8 and fmix, I've implemented them here. */ function encodeUtf8(text) { const encoder = new TextEncoder(); return encoder.encode(text).buffer; } function fmix(input) { input ^= input >>> 16; input = Math.imul(input, 0x85ebca6b); input ^= input >>> 13; input = Math.imul(input, 0xc2b2ae35); input ^= input >>> 16; return input >>> 0; } const C = new Uint32Array([0x239b961b, 0xab0e9789, 0x38b34ae5, 0xa1e38b93]); function rotl(m, n) { return (m << n) | (m >>> (32 - n)); } function body(key, hash) { const blocks = (key.byteLength / 16) | 0; const view32 = new Uint32Array(key, 0, blocks * 4); for (let i = 0; i < blocks; i++) { const k = view32.subarray(i * 4, (i + 1) * 4); k[0] = Math.imul(k[0], C[0]); k[0] = rotl(k[0], 15); k[0] = Math.imul(k[0], C[1]); hash[0] = hash[0] ^ k[0]; hash[0] = rotl(hash[0], 19); hash[0] = hash[0] + hash[1]; hash[0] = Math.imul(hash[0], 5) + 0x561ccd1b; k[1] = Math.imul(k[1], C[1]); k[1] = rotl(k[1], 16); k[1] = Math.imul(k[1], C[2]); hash[1] = hash[1] ^ k[1]; hash[1] = rotl(hash[1], 17); hash[1] = hash[1] + hash[2]; hash[1] = Math.imul(hash[1], 5) + 0x0bcaa747; k[2] = Math.imul(k[2], C[2]); k[2] = rotl(k[2], 17); k[2] = Math.imul(k[2], C[3]); hash[2] = hash[2] ^ k[2]; hash[2] = rotl(hash[2], 15); hash[2] = hash[2] + hash[3]; hash[2] = Math.imul(hash[2], 5) + 0x96cd1c35; k[3] = Math.imul(k[3], C[3]); k[3] = rotl(k[3], 18); k[3] = Math.imul(k[3], C[0]); hash[3] = hash[3] ^ k[3]; hash[3] = rotl(hash[3], 13); hash[3] = hash[3] + hash[0]; hash[3] = Math.imul(hash[3], 5) + 0x32ac3b17; } } function tail(key, hash) { const blocks = (key.byteLength / 16) | 0; const reminder = key.byteLength % 16; const k = new Uint32Array(4); const tail = new Uint8Array(key, blocks * 16, reminder); switch (reminder) { case 15: k[3] = k[3] ^ (tail[14] << 16); // fallthrough case 14: k[3] = k[3] ^ (tail[13] << 8); // fallthrough case 13: k[3] = k[3] ^ (tail[12] << 0); k[3] = Math.imul(k[3], C[3]); k[3] = rotl(k[3], 18); k[3] = Math.imul(k[3], C[0]); hash[3] = hash[3] ^ k[3]; // fallthrough case 12: k[2] = k[2] ^ (tail[11] << 24); // fallthrough case 11: k[2] = k[2] ^ (tail[10] << 16); // fallthrough case 10: k[2] = k[2] ^ (tail[9] << 8); // fallthrough case 9: k[2] = k[2] ^ (tail[8] << 0); k[2] = Math.imul(k[2], C[2]); k[2] = rotl(k[2], 17); k[2] = Math.imul(k[2], C[3]); hash[2] = hash[2] ^ k[2]; // fallthrough case 8: k[1] = k[1] ^ (tail[7] << 24); // fallthrough case 7: k[1] = k[1] ^ (tail[6] << 16); // fallthrough case 6: k[1] = k[1] ^ (tail[5] << 8); // fallthrough case 5: k[1] = k[1] ^ (tail[4] << 0); k[1] = Math.imul(k[1], C[1]); k[1] = rotl(k[1], 16); k[1] = Math.imul(k[1], C[2]); hash[1] = hash[1] ^ k[1]; // fallthrough case 4: k[0] = k[0] ^ (tail[3] << 24); // fallthrough case 3: k[0] = k[0] ^ (tail[2] << 16); // fallthrough case 2: k[0] = k[0] ^ (tail[1] << 8); // fallthrough case 1: k[0] = k[0] ^ (tail[0] << 0); k[0] = Math.imul(k[0], C[0]); k[0] = rotl(k[0], 15); k[0] = Math.imul(k[0], C[1]); hash[0] = hash[0] ^ k[0]; } } function finalize(key, hash) { hash[0] = hash[0] ^ key.byteLength; hash[1] = hash[1] ^ key.byteLength; hash[2] = hash[2] ^ key.byteLength; hash[3] = hash[3] ^ key.byteLength; hash[0] = (hash[0] + hash[1]) | 0; hash[0] = (hash[0] + hash[2]) | 0; hash[0] = (hash[0] + hash[3]) | 0; hash[1] = (hash[1] + hash[0]) | 0; hash[2] = (hash[2] + hash[0]) | 0; hash[3] = (hash[3] + hash[0]) | 0; hash[0] = fmix(hash[0]); hash[1] = fmix(hash[1]); hash[2] = fmix(hash[2]); hash[3] = fmix(hash[3]); hash[0] = (hash[0] + hash[1]) | 0; hash[0] = (hash[0] + hash[2]) | 0; hash[0] = (hash[0] + hash[3]) | 0; hash[1] = (hash[1] + hash[0]) | 0; hash[2] = (hash[2] + hash[0]) | 0; hash[3] = (hash[3] + hash[0]) | 0; } function hash(key, seed = 0) { seed = seed ? seed | 0 : 0; if (typeof key === "string") { key = encodeUtf8(key); } if (!(key instanceof ArrayBuffer)) { throw new TypeError("Expected key to be ArrayBuffer or string"); } const hash = new Uint32Array([seed, seed, seed, seed]); body(key, hash); tail(key, hash); finalize(key, hash); const byteArray = new Uint8Array(hash.buffer); return Array.from(byteArray) .map((byte) => byte.toString(16).padStart(2, "0")) .join(""); } function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } var loglevel$1 = {exports: {}}; /* * loglevel - https://github.com/pimterry/loglevel * * Copyright (c) 2013 Tim Perry * Licensed under the MIT license. */ var loglevel = loglevel$1.exports; var hasRequiredLoglevel; function requireLoglevel () { if (hasRequiredLoglevel) return loglevel$1.exports; hasRequiredLoglevel = 1; (function (module) { (function (root, definition) { if (module.exports) { module.exports = definition(); } else { root.log = definition(); } }(loglevel, function () { // Slightly dubious tricks to cut down minimized file size var noop = function() {}; var undefinedType = "undefined"; var isIE = (typeof window !== undefinedType) && (typeof window.navigator !== undefinedType) && ( /Trident\/|MSIE /.test(window.navigator.userAgent) ); var logMethods = [ "trace", "debug", "info", "warn", "error" ]; var _loggersByName = {}; var defaultLogger = null; // Cross-browser bind equivalent that works at least back to IE6 function bindMethod(obj, methodName) { var method = obj[methodName]; if (typeof method.bind === 'function') { return method.bind(obj); } else { try { return Function.prototype.bind.call(method, obj); } catch (e) { // Missing bind shim or IE8 + Modernizr, fallback to wrapping return function() { return Function.prototype.apply.apply(method, [obj, arguments]); }; } } } // Trace() doesn't print the message in IE, so for that case we need to wrap it function traceForIE() { if (console.log) { if (console.log.apply) { console.log.apply(console, arguments); } else { // In old IE, native console methods themselves don't have apply(). Function.prototype.apply.apply(console.log, [console, arguments]); } } if (console.trace) console.trace(); } // Build the best logging method possible for this env // Wherever possible we want to bind, not wrap, to preserve stack traces function realMethod(methodName) { if (methodName === 'debug') { methodName = 'log'; } if (typeof console === undefinedType) { return false; // No method possible, for now - fixed later by enableLoggingWhenConsoleArrives } else if (methodName === 'trace' && isIE) { return traceForIE; } else if (console[methodName] !== undefined) { return bindMethod(console, methodName); } else if (console.log !== undefined) { return bindMethod(console, 'log'); } else { return noop; } } // These private functions always need `this` to be set properly function replaceLoggingMethods() { /*jshint validthis:true */ var level = this.getLevel(); // Replace the actual methods. for (var i = 0; i < logMethods.length; i++) { var methodName = logMethods[i]; this[methodName] = (i < level) ? noop : this.methodFactory(methodName, level, this.name); } // Define log.log as an alias for log.debug this.log = this.debug; // Return any important warnings. if (typeof console === undefinedType && level < this.levels.SILENT) { return "No console available for logging"; } } // In old IE versions, the console isn't present until you first open it. // We build realMethod() replacements here that regenerate logging methods function enableLoggingWhenConsoleArrives(methodName) { return function () { if (typeof console !== undefinedType) { replaceLoggingMethods.call(this); this[methodName].apply(this, arguments); } }; } // By default, we use closely bound real methods wherever possible, and // otherwise we wait for a console to appear, and then try again. function defaultMethodFactory(methodName, _level, _loggerName) { /*jshint validthis:true */ return realMethod(methodName) || enableLoggingWhenConsoleArrives.apply(this, arguments); } function Logger(name, factory) { // Private instance variables. var self = this; /** * The level inherited from a parent logger (or a global default). We * cache this here rather than delegating to the parent so that it stays * in sync with the actual logging methods that we have installed (the * parent could change levels but we might not have rebuilt the loggers * in this child yet). * @type {number} */ var inheritedLevel; /** * The default level for this logger, if any. If set, this overrides * `inheritedLevel`. * @type {number|null} */ var defaultLevel; /** * A user-specific level for this logger. If set, this overrides * `defaultLevel`. * @type {number|null} */ var userLevel; var storageKey = "loglevel"; if (typeof name === "string") { storageKey += ":" + name; } else if (typeof name === "symbol") { storageKey = undefined; } function persistLevelIfPossible(levelNum) { var levelName = (logMethods[levelNum] || 'silent').toUpperCase(); if (typeof window === undefinedType || !storageKey) return; // Use localStorage if available try { window.localStorage[storageKey] = levelName; return; } catch (ignore) {} // Use session cookie as fallback try { window.document.cookie = encodeURIComponent(storageKey) + "=" + levelName + ";"; } catch (ignore) {} } function getPersistedLevel() { var storedLevel; if (typeof window === undefinedType || !storageKey) return; try { storedLevel = window.localStorage[storageKey]; } catch (ignore) {} // Fallback to cookies if local storage gives us nothing if (typeof storedLevel === undefinedType) { try { var cookie = window.document.cookie; var cookieName = encodeURIComponent(storageKey); var location = cookie.indexOf(cookieName + "="); if (location !== -1) { storedLevel = /^([^;]+)/.exec( cookie.slice(location + cookieName.length + 1) )[1]; } } catch (ignore) {} } // If the stored level is not valid, treat it as if nothing was stored. if (self.levels[storedLevel] === undefined) { storedLevel = undefined; } return storedLevel; } function clearPersistedLevel() { if (typeof window === undefinedType || !storageKey) return; // Use localStorage if available try { window.localStorage.removeItem(storageKey); } catch (ignore) {} // Use session cookie as fallback try { window.document.cookie = encodeURIComponent(storageKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC"; } catch (ignore) {} } function normalizeLevel(input) { var level = input; if (typeof level === "string" && self.levels[level.toUpperCase()] !== undefined) { level = self.levels[level.toUpperCase()]; } if (typeof level === "number" && level >= 0 && level <= self.levels.SILENT) { return level; } else { throw new TypeError("log.setLevel() called with invalid level: " + input); } } /* * * Public logger API - see https://github.com/pimterry/loglevel for details * */ self.name = name; self.levels = { "TRACE": 0, "DEBUG": 1, "INFO": 2, "WARN": 3, "ERROR": 4, "SILENT": 5}; self.methodFactory = factory || defaultMethodFactory; self.getLevel = function () { if (userLevel != null) { return userLevel; } else if (defaultLevel != null) { return defaultLevel; } else { return inheritedLevel; } }; self.setLevel = function (level, persist) { userLevel = normalizeLevel(level); if (persist !== false) { // defaults to true persistLevelIfPossible(userLevel); } // NOTE: in v2, this should call rebuild(), which updates children. return replaceLoggingMethods.call(self); }; self.setDefaultLevel = function (level) { defaultLevel = normalizeLevel(level); if (!getPersistedLevel()) { self.setLevel(level, false); } }; self.resetLevel = function () { userLevel = null; clearPersistedLevel(); replaceLoggingMethods.call(self); }; self.enableAll = function(persist) { self.setLevel(self.levels.TRACE, persist); }; self.disableAll = function(persist) { self.setLevel(self.levels.SILENT, persist); }; self.rebuild = function () { if (defaultLogger !== self) { inheritedLevel = normalizeLevel(defaultLogger.getLevel()); } replaceLoggingMethods.call(self); if (defaultLogger === self) { for (var childName in _loggersByName) { _loggersByName[childName].rebuild(); } } }; // Initialize all the internal levels. inheritedLevel = normalizeLevel( defaultLogger ? defaultLogger.getLevel() : "WARN" ); var initialLevel = getPersistedLevel(); if (initialLevel != null) { userLevel = normalizeLevel(initialLevel); } replaceLoggingMethods.call(self); } /* * * Top-level API * */ defaultLogger = new Logger(); defaultLogger.getLogger = function getLogger(name) { if ((typeof name !== "symbol" && typeof name !== "string") || name === "") { throw new TypeError("You must supply a name when creating a logger."); } var logger = _loggersByName[name]; if (!logger) { logger = _loggersByName[name] = new Logger( name, defaultLogger.methodFactory ); } return logger; }; // Grab the current global log variable in case of overwrite var _log = (typeof window !== undefinedType) ? window.log : undefined; defaultLogger.noConflict = function() { if (typeof window !== undefinedType && window.log === defaultLogger) { window.log = _log; } return defaultLogger; }; defaultLogger.getLoggers = function getLoggers() { return _loggersByName; }; // ES6 default export, for compatibility defaultLogger['default'] = defaultLogger; return defaultLogger; })); } (loglevel$1)); return loglevel$1.exports; } var loglevelExports = requireLoglevel(); var log = /*@__PURE__*/getDefaultExportFromCjs(loglevelExports); const logger = log.getLogger("device-intelligence"); //set the default log level to off logger.setLevel("silent"); const availableFonts = [ "Arial", "Arial Black", "Arial Narrow", "Arial Rounded MT", "Arimo", "Archivo", "Barlow", "Bebas Neue", "Bitter", "Bookman", "Calibri", "Cabin", "Candara", "Century", "Century Gothic", "Comic Sans MS", "Constantia", "Courier", "Courier New", "Crimson Text", "DM Mono", "DM Sans", "DM Serif Display", "DM Serif Text", "Dosis", "Droid Sans", "Exo", "Fira Code", "Fira Sans", "Franklin Gothic Medium", "Garamond", "Geneva", "Georgia", "Gill Sans", "Helvetica", "Impact", "Inconsolata", "Indie Flower", "Inter", "Josefin Sans", "Karla", "Lato", "Lexend", "Lucida Bright", "Lucida Console", "Lucida Sans Unicode", "Manrope", "Merriweather", "Merriweather Sans", "Montserrat", "Myriad", "Noto Sans", "Nunito", "Nunito Sans", "Open Sans", "Optima", "Orbitron", "Oswald", "Pacifico", "Palatino", "Perpetua", "PT Sans", "PT Serif", "Poppins", "Prompt", "Public Sans", "Quicksand", "Rajdhani", "Recursive", "Roboto", "Roboto Condensed", "Rockwell", "Rubik", "Segoe Print", "Segoe Script", "Segoe UI", "Sora", "Source Sans Pro", "Space Mono", "Tahoma", "Taviraj", "Times", "Times New Roman", "Titillium Web", "Trebuchet MS", "Ubuntu", "Varela Round", "Verdana", "Work Sans", ]; async function getFontMetrics() { return new Promise((resolve) => { ephemeralIFrame(({ iframe }) => { const canvas = iframe.createElement("canvas"); const ctx = canvas.getContext("2d"); const baseFonts = ["monospaces", "sans serif", "serif"]; const defaultWidths = baseFonts.map((font) => { return measureSingleFont(ctx, font); }); const detectedFontResults = {}; availableFonts.forEach((font) => { const fontWidth = measureSingleFont(ctx, font); if (!defaultWidths.includes(fontWidth)) detectedFontResults[font] = fontWidth; }); const fontHash = hash(JSON.stringify(detectedFontResults)); resolve({ fontHash }); }).catch((error) => { logger.error("error retrieving the font hash frame", error); }); }); } function measureSingleFont(ctx, font) { if (!ctx) { throw new Error("Canvas context not supported"); } const text = "WwMmLli0Oo"; ctx.font = `72px ${font}`; return ctx.measureText(text).width; } function getHardwareInfo() { return new Promise((resolve) => { var _a; const navigatorWithMemory = navigator; const deviceMemory = (_a = navigatorWithMemory.deviceMemory) !== null && _a !== void 0 ? _a : 0; const extendedPerformance = window.performance; const memoryInfo = window.performance && extendedPerformance.memory ? extendedPerformance.memory : 0; resolve({ videocard: getVideoCard(), architecture: getArchitecture(), deviceMemory: deviceMemory.toString() || "undefined", jsHeapSizeLimit: memoryInfo ? memoryInfo === null || memoryInfo === void 0 ? void 0 : memoryInfo.jsHeapSizeLimit : 0, }); }); } function getVideoCard() { var _a; const canvas = document.createElement("canvas"); const gl = (_a = canvas.getContext("webgl")) !== null && _a !== void 0 ? _a : canvas.getContext("experimental-webgl"); if (!gl || !("getParameter" in gl)) { return "undefined"; } const result = buildVideoCardInfo(gl); return addUnmaskedInfo(gl, result); } function buildVideoCardInfo(gl) { return { vendor: (gl.getParameter(gl.VENDOR) || "").toString(), renderer: (gl.getParameter(gl.RENDERER) || "").toString(), version: (gl.getParameter(gl.VERSION) || "").toString(), shadingLanguageVersion: (gl.getParameter(gl.SHADING_LANGUAGE_VERSION) || "").toString(), }; } function addUnmaskedInfo(gl, result) { if ((typeof result.vendor === "string" && !result.vendor.length) || (typeof result.renderer === "string" && !result.renderer.length)) { const debugInfo = gl.getExtension("WEBGL_debug_renderer_info"); if (debugInfo) { const vendorUnmasked = (gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || "").toString(); const rendererUnmasked = (gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || "").toString(); if (vendorUnmasked) result.vendorUnmasked = vendorUnmasked; if (rendererUnmasked) result.rendererUnmasked = rendererUnmasked; } } return result; } function getArchitecture() { const f = new Float32Array(1); const u8 = new Uint8Array(f.buffer); f[0] = Infinity; f[0] = f[0] - f[0]; return u8[3]; } function getLocales() { return Promise.resolve({ languages: getUserLanguage(), timezone: getUserTimezone(), }); } function getUserLanguage() { return navigator.language; } function getUserTimezone() { return Intl.DateTimeFormat().resolvedOptions().timeZone; } function mostFrequentValue(arr) { if (arr.length === 0) { return null; // Return null for an empty array } const frequencyMap = {}; // Count occurrences of each element in the array arr.forEach((element) => { const key = String(element); frequencyMap[key] = (frequencyMap[key] || 0) + 1; }); let mostFrequent = String(arr[0]); // Assume the first element is the most frequent let highestFrequency = 1; // Frequency of the assumed most frequent element // Find the element with the highest frequency Object.keys(frequencyMap).forEach((key) => { if (frequencyMap[key] > highestFrequency) { mostFrequent = key; highestFrequency = frequencyMap[key]; } }); return mostFrequent; } function mostFrequentValuesInArrayOfDictionaries(arr, keys) { const result = {}; keys.forEach((key) => { const valuesForKey = arr .map((obj) => (key in obj ? obj[key] : undefined)) .filter((val) => val !== undefined); const mostFrequentValueForKey = mostFrequentValue(valuesForKey); if (mostFrequentValueForKey) result[key] = mostFrequentValueForKey; }); return result; } function getPermissionKeys() { return [ "accelerometer", "accessibility", "accessibility-events", "ambient-light-sensor", "background-fetch", "background-sync", "bluetooth", "camera", "clipboard-read", "clipboard-write", "device-info", "display-capture", "gyroscope", "geolocation", "local-fonts", "magnetometer", "microphone", "midi", "nfc", "notifications", "payment-handler", "persistent-storage", "push", "speaker", "storage-access", "top-level-storage-access", "window-management", "query", ]; } async function getBrowserPermissions() { const permissionKeys = getPermissionKeys(); const permissionPromises = Array.from({ length: 3 }, () => getBrowserPermissionsOnce(permissionKeys)); return Promise.all(permissionPromises).then((resolvedPermissions) => { const permission = mostFrequentValuesInArrayOfDictionaries(resolvedPermissions, permissionKeys); return permission; }); } async function getBrowserPermissionsOnce(permissionKeys) { const permissionStatus = {}; for (const feature of permissionKeys) { try { // Request permission status for each feature const status = await navigator.permissions.query({ name: feature }); // Assign permission status to the object permissionStatus[feature] = status.state.toString(); // eslint-disable-next-line } catch (error) { logger.error("feature not supported"); // In case of errors (unsupported features, etc.), do nothing. Not listing them is the same as not supported } } return permissionStatus; } function getInstalledPlugins() { const plugins = []; if (navigator.plugins) { for (const plugin of navigator.plugins) { plugins.push([plugin.name, plugin.filename, plugin.description].join("|")); } } return Promise.resolve({ plugins }); } function screenDetails() { return Promise.resolve({ is_touchscreen: navigator.maxTouchPoints > 0, maxTouchPoints: navigator.maxTouchPoints, colorDepth: screen.colorDepth, mediaMatches: matchMedias(), }); } function matchMedias() { const results = []; /** * @see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries */ const mediaQueries = { "prefers-contrast": [ "high", "more", "low", "less", "forced", "no-preference", ], "any-hover": ["hover", "none"], "any-pointer": ["none", "coarse", "fine"], pointer: ["none", "coarse", "fine"], hover: ["hover", "none"], update: ["fast", "slow"], "inverted-colors": ["inverted", "none"], "prefers-reduced-motion": ["reduce", "no-preference"], "prefers-reduced-transparency": ["reduce", "no-preference"], scripting: ["none", "initial-only", "enabled"], "forced-colors": ["active", "none"], }; Object.keys(mediaQueries).forEach((key) => { mediaQueries[key].forEach((value) => { if (matchMedia(`(${key}: ${value})`).matches) results.push(`${key}: ${value}`); }); }); return results; } function getSystemDetails() { return new Promise((resolve) => { const browser = getBrowser(); resolve({ platform: window.navigator.platform, cookieEnabled: window.navigator.cookieEnabled, productSub: navigator.productSub, product: navigator.product, useragent: navigator.userAgent, hardwareConcurrency: navigator.hardwareConcurrency, browser: { name: browser.name, version: browser.version }, applePayVersion: getApplePayVersion(), }); }); } /** * @returns applePayCanMakePayments: boolean, applePayMaxSupportedVersion: number */ function getApplePayVersion() { const extendedWindow = window; if (window.location.protocol === "https:" && typeof extendedWindow.ApplePaySession === "function") { try { const versionCheck = extendedWindow.ApplePaySession.supportsVersion; for (let i = 15; i > 0; i--) { if (versionCheck(i)) { return i; } } // eslint-disable-next-line } catch (error) { logger.error("No supported version available for the payment method"); return 0; } } return 0; } const components = { hardware: getHardwareInfo, locales: getLocales, permissions: getBrowserPermissions, plugins: getInstalledPlugins, screen: screenDetails, system: getSystemDetails, }; if (getBrowser().name != "Firefox") { components.fonts = getFontMetrics; } const getComponentPromises = () => { return Object.fromEntries(Object.entries(components).map(([key, value]) => [key, value()])); }; function delay(t, val) { return new Promise((resolve) => { setTimeout(() => resolve(val), t); }); } function raceAll(promises, timeoutTime, timeoutVal) { return Promise.all(promises.map((p) => { return Promise.race([p, delay(timeoutTime, timeoutVal)]); })); } const timeoutInstance = { timeout: "true", }; async function getFingerprintData() { const promiseMap = getComponentPromises(); const keys = Object.keys(promiseMap); const promises = Object.values(promiseMap); const resolvedValues = await raceAll(promises, 1000, timeoutInstance); const validValues = resolvedValues.filter((value) => value !== undefined); const resolvedComponents = {}; validValues.forEach((value, index) => { resolvedComponents[keys[index]] = value; }); const deviceHash = hash(JSON.stringify(resolvedComponents)); resolvedComponents.thumbmark = { deviceHash }; try { const { fontHash } = await getFontMetrics(); resolvedComponents.fonts = { fontHash }; } catch (error) { logger.error("Error Retrieving the font hash:", error); } return filterFingerprintData(resolvedComponents, [], [], ""); } /** * This function filters the fingerprint data based on the exclude and include list * @param {ComponentInterface} obj - components objects from main ComponentInterface * @param {string[]} excludeList - elements to exclude from components objects (e.g : 'canvas', 'system.browser') * @param {string[]} includeList - elements to only include from components objects (e.g : 'canvas', 'system.browser') * @param {string} path - auto-increment path iterating on key objects from components objects * @returns {ComponentInterface} result - returns the final object before hashing in order to get fingerprint */ function filterFingerprintData(obj, excludeList, includeList, path = "") { const result = {}; for (const [key, value] of Object.entries(obj)) { const currentPath = path + key + "."; if (typeof value === "object" && !Array.isArray(value)) { const filtered = filterFingerprintData(value, excludeList, includeList, currentPath); if (Object.keys(filtered).length > 0) { result[key] = filtered; } } else { const isExcluded = excludeList.some((exclusion) => currentPath.startsWith(exclusion)); const isIncluded = includeList.some((inclusion) => currentPath.startsWith(inclusion)); if (!isExcluded || isIncluded) { result[key] = value; } } } return result; } async function getFingerprint(includeData) { const fingerprintData = await getFingerprintData(); const thisHash = hash(JSON.stringify(fingerprintData)); if (includeData) { return { hash: thisHash.toString(), data: fingerprintData }; } else { return thisHash.toString(); } } async function setFingerprintCookie(cookieDomain = "account.gov.uk") { if (typeof window === "undefined") { logger.warn("fingerprint cookie logic should only run on the client side"); return; } try { const fingerprint = await getFingerprintData(); const encodedFingerprint = btoa(JSON.stringify(fingerprint)); document.cookie = `di-device-intelligence=${encodedFingerprint}; path=/; domain=${cookieDomain}; secure; SameSite=Strict`; } catch (error) { logger.error("Error setting fingerprint cookie:", error); } } const setLogLevel = (level) => { logger.setLevel(level); }; exports.getFingerprint = getFingerprint; exports.getFingerprintData = getFingerprintData; exports.setFingerprintCookie = setFingerprintCookie; exports.setLogLevel = setLogLevel; return exports; })({});