UNPKG

userdnajs

Version:

Community edition of UserDNA JS. A fingerprint generator library for creating unique user identifiers

646 lines (636 loc) 22.4 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.UserDNACommunity = {})); })(this, (function (exports) { 'use strict'; /** * Utility functions for hashing and data manipulation */ /** * Creates a simple hash from a string - used in free version * @param input - String to hash * @returns Hashed string */ function simpleHash(input) { let hash = 0; if (input.length === 0) return hash.toString(16); for (let i = 0; i < input.length; i++) { const char = input.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return Math.abs(hash).toString(16); } /** * Combines multiple values into a single hash * @param values - Array of values to combine * @returns Combined hash */ function combineHashes(values) { const stringToHash = values .map(value => { if (value === null || value === undefined) { return 'null'; } if (typeof value === 'object') { return JSON.stringify(value); } return String(value); }) .join('|'); return simpleHash(stringToHash); } /** * Storage key for the fingerprint */ const DEFAULT_STORAGE_KEY = 'userdna_fingerprint'; /** * Checks if localStorage is available * @returns Whether localStorage is available */ function isLocalStorageAvailable() { try { const testKey = '__test__'; localStorage.setItem(testKey, testKey); localStorage.removeItem(testKey); return true; } catch (e) { return false; } } /** * Checks if sessionStorage is available * @returns Whether sessionStorage is available */ function isSessionStorageAvailable() { try { const testKey = '__test__'; sessionStorage.setItem(testKey, testKey); sessionStorage.removeItem(testKey); return true; } catch (e) { return false; } } /** * Saves a value to localStorage * @param key - Storage key * @param value - Value to store * @returns Whether the operation was successful */ function saveToLocalStorage(key, value) { if (!isLocalStorageAvailable()) return false; try { localStorage.setItem(key, value); return true; } catch (e) { return false; } } /** * Retrieves a value from localStorage * @param key - Storage key * @returns Retrieved value or null if not found */ function getFromLocalStorage(key) { if (!isLocalStorageAvailable()) return null; try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : null; } catch (e) { return null; } } /** * Saves a value to sessionStorage * @param key - Storage key * @param value - Value to store * @returns Whether the operation was successful */ function saveToSessionStorage(key, value) { if (!isSessionStorageAvailable()) return false; try { sessionStorage.setItem(key, value); return true; } catch (e) { return false; } } /** * Retrieves a value from sessionStorage * @param key - Storage key * @returns Retrieved value or null if not found */ function getFromSessionStorage(key) { if (!isSessionStorageAvailable()) return null; try { const item = sessionStorage.getItem(key); return item ? JSON.parse(item) : null; } catch (e) { return null; } } /** * Storage utilities for UserDNA */ const Storage = { getKey: (prefix = 'userdna') => `${prefix}_${DEFAULT_STORAGE_KEY}`, /** * Saves fingerprint data to the specified storage * @param data - Data to store * @param options - Storage options * @returns Whether the operation was successful */ save: (data, options = {}) => { const { storageType = 'localStorage', storagePrefix = 'userdna' } = options; const key = Storage.getKey(storagePrefix); const stringifiedData = JSON.stringify(data); switch (storageType) { case 'localStorage': return saveToLocalStorage(key, stringifiedData); case 'sessionStorage': return saveToSessionStorage(key, stringifiedData); case 'none': default: return false; } }, /** * Retrieves fingerprint data from the specified storage * @param options - Storage options * @returns Retrieved data or null if not found */ get: (options = {}) => { const { storageType = 'localStorage', storagePrefix = 'userdna' } = options; const key = Storage.getKey(storagePrefix); switch (storageType) { case 'localStorage': return getFromLocalStorage(key); case 'sessionStorage': return getFromSessionStorage(key); case 'none': default: return null; } } }; /** * Gets screen information for fingerprinting * @returns Screen information */ function getScreenInfo() { if (typeof window === 'undefined' || !window.screen) { // Fallback for non-browser environments return { width: 0, height: 0, availWidth: 0, availHeight: 0, colorDepth: 0, pixelRatio: 1 }; } const { screen } = window; return { width: screen.width, height: screen.height, availWidth: screen.availWidth, availHeight: screen.availHeight, colorDepth: screen.colorDepth, pixelRatio: window.devicePixelRatio || 1 }; } /** * Gets browser information for fingerprinting * @returns Browser information */ function getBrowserInfo() { if (typeof navigator === 'undefined') { // Fallback for non-browser environments return { userAgent: '', browserName: 'unknown', browserVersion: 'unknown', os: 'unknown', osVersion: 'unknown', device: 'unknown', vendor: 'unknown', cookiesEnabled: false, doNotTrack: null }; } const { userAgent } = navigator; // Browser name and version detection const browserData = detectBrowser(userAgent); // OS detection const osData = detectOS(userAgent); // Device detection const device = detectDevice(userAgent); return { userAgent, browserName: browserData.name, browserVersion: browserData.version, os: osData.name, osVersion: osData.version, device, vendor: navigator.vendor || 'unknown', cookiesEnabled: navigator.cookieEnabled, doNotTrack: getDoNotTrack() }; } /** * Detects browser name and version from user agent * @param userAgent - User agent string * @returns Browser name and version */ function detectBrowser(userAgent) { var _a, _b, _c, _d, _e, _f; // Default values let name = 'unknown'; let version = 'unknown'; // Edge (Chromium-based) if (userAgent.includes('Edg/')) { name = 'Edge'; version = ((_a = userAgent.match(/Edg\/([0-9.]+)/)) === null || _a === void 0 ? void 0 : _a[1]) || 'unknown'; } // Chrome else if (userAgent.includes('Chrome/')) { name = 'Chrome'; version = ((_b = userAgent.match(/Chrome\/([0-9.]+)/)) === null || _b === void 0 ? void 0 : _b[1]) || 'unknown'; } // Safari else if (userAgent.includes('Safari/') && !userAgent.includes('Chrome/')) { name = 'Safari'; version = ((_c = userAgent.match(/Version\/([0-9.]+)/)) === null || _c === void 0 ? void 0 : _c[1]) || 'unknown'; } // Firefox else if (userAgent.includes('Firefox/')) { name = 'Firefox'; version = ((_d = userAgent.match(/Firefox\/([0-9.]+)/)) === null || _d === void 0 ? void 0 : _d[1]) || 'unknown'; } // Opera else if (userAgent.includes('OPR/') || userAgent.includes('Opera/')) { name = 'Opera'; version = ((_e = userAgent.match(/(?:OPR|Opera)\/([0-9.]+)/)) === null || _e === void 0 ? void 0 : _e[1]) || 'unknown'; } // IE else if (userAgent.includes('Trident/')) { name = 'Internet Explorer'; version = ((_f = userAgent.match(/rv:([0-9.]+)/)) === null || _f === void 0 ? void 0 : _f[1]) || 'unknown'; } return { name, version }; } /** * Detects operating system from user agent * @param userAgent - User agent string * @returns OS name and version */ function detectOS(userAgent) { var _a, _b, _c, _d, _e, _f; // Default values let name = 'unknown'; let version = 'unknown'; // Windows if (userAgent.includes('Windows')) { name = 'Windows'; if (userAgent.includes('Windows NT 10.0')) version = '10'; else if (userAgent.includes('Windows NT 6.3')) version = '8.1'; else if (userAgent.includes('Windows NT 6.2')) version = '8'; else if (userAgent.includes('Windows NT 6.1')) version = '7'; else if (userAgent.includes('Windows NT 6.0')) version = 'Vista'; else if (userAgent.includes('Windows NT 5.1')) version = 'XP'; else if (userAgent.includes('Windows NT 5.0')) version = '2000'; } // macOS else if (userAgent.includes('Macintosh') || userAgent.includes('Mac OS X')) { name = 'macOS'; const macVersion = ((_a = userAgent.match(/Mac OS X (\d+[._]\d+[._]\d+)/)) === null || _a === void 0 ? void 0 : _a[1]) || ((_b = userAgent.match(/Mac OS X (\d+[._]\d+)/)) === null || _b === void 0 ? void 0 : _b[1]); if (macVersion) { version = macVersion.replace(/_/g, '.'); } } // iOS else if (/iPhone|iPad|iPod/.test(userAgent)) { name = 'iOS'; version = ((_d = (_c = userAgent.match(/OS (\d+[._]\d+[._]?\d*)/)) === null || _c === void 0 ? void 0 : _c[1]) === null || _d === void 0 ? void 0 : _d.replace(/_/g, '.')) || 'unknown'; } // Android else if (userAgent.includes('Android')) { name = 'Android'; version = ((_f = (_e = userAgent.match(/Android (\d+[._]\d+[._]?\d*)/)) === null || _e === void 0 ? void 0 : _e[1]) === null || _f === void 0 ? void 0 : _f.replace(/_/g, '.')) || 'unknown'; } // Linux else if (userAgent.includes('Linux')) { name = 'Linux'; } return { name, version }; } /** * Detects device type from user agent * @param userAgent - User agent string * @returns Device type */ function detectDevice(userAgent) { if (/iPhone|iPod/.test(userAgent)) { return 'iPhone'; } else if (/iPad/.test(userAgent)) { return 'iPad'; } else if (/Android/.test(userAgent)) { if (/Mobile/.test(userAgent)) { return 'Android Mobile'; } return 'Android Tablet'; } else if (/Windows Phone/.test(userAgent)) { return 'Windows Phone'; } else if (/Macintosh|Mac OS X/.test(userAgent)) { return 'Mac'; } else if (/Windows/.test(userAgent)) { return 'Windows'; } else if (/Linux/.test(userAgent)) { return 'Linux'; } return 'unknown'; } /** * Gets the Do Not Track preference * @returns Do Not Track value */ function getDoNotTrack() { if (typeof navigator === 'undefined') { return null; } if (navigator.doNotTrack) { return navigator.doNotTrack; } else if ('doNotTrack' in window) { // @ts-ignore return window.doNotTrack; } else if ('msDoNotTrack' in navigator) { // @ts-ignore return navigator.msDoNotTrack; } return null; } /** * Default options for UserDNA Community Edition */ const DEFAULT_OPTIONS = { includeBrowserInfo: true, includeDeviceInfo: true, includeScreenInfo: true, includeTimezone: true, includeLanguage: true, includeStorage: true, storagePrefix: 'userdna', storageType: 'localStorage', customComponents: [] }; /** * Main class for generating and managing fingerprints */ class FingerprintGenerator { /** * Creates a new instance of FingerprintGenerator * @param options - Configuration options */ constructor(options = {}) { /** * Last generated fingerprint result */ this.lastResult = null; this.options = { ...DEFAULT_OPTIONS, ...options }; // Limit number of custom components if (this.options.customComponents && this.options.customComponents.length > 2) { this.options.customComponents = this.options.customComponents.slice(0, 2); } } /** * Gets the fingerprint asynchronously * @param options - Options for getting the fingerprint * @returns Promise that resolves to the fingerprint result */ async getFingerprint(options = {}) { // Check cache first if (this.lastResult) { return this.lastResult; } // Check storage for existing fingerprint const cachedResult = this.getFromStorage(); if (cachedResult) { this.lastResult = cachedResult; return cachedResult; } // Generate a new fingerprint const components = await this.collectComponents(); const hashValues = Object.entries(components).map(([key, value]) => { if (value === null || value === undefined) { return null; } if (typeof value === 'object') { return JSON.stringify(value); } return String(value); }).filter(Boolean); // Generate the fingerprint ID const id = combineHashes(hashValues); // Create result const result = { id, components, createdAt: Date.now(), fromCache: false }; // Save to storage if enabled if (this.options.storageType !== 'none') { this.saveToStorage(result); } // Update last result this.lastResult = result; return result; } /** * Gets a visitor ID that is unique for the current browser * @returns Promise that resolves to the visitor ID */ async getVisitorId() { const result = await this.getFingerprint(); return result.id; } /** * Checks if the current fingerprint is likely from the same visitor * @param fingerprintId - Fingerprint ID to compare against * @returns Promise that resolves to whether the fingerprints match */ async isSameVisitor(fingerprintId) { const result = await this.getFingerprint(); return result.id === fingerprintId; } /** * Collects all the components for the fingerprint * @returns Promise that resolves to the fingerprint components */ async collectComponents() { const components = {}; // Browser info if (this.options.includeBrowserInfo) { components.browserInfo = getBrowserInfo(); } // Screen info if (this.options.includeScreenInfo) { components.screenInfo = getScreenInfo(); } // Timezone if (this.options.includeTimezone) { try { components.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (e) { components.timezone = String(new Date().getTimezoneOffset()); } } // Language if (this.options.includeLanguage) { components.language = navigator.language || // @ts-ignore navigator.userLanguage || // @ts-ignore navigator.browserLanguage || 'unknown'; } // Storage capabilities if (this.options.includeStorage) { components.storage = { localStorage: Storage.save({ test: true }, { storageType: 'localStorage' }), sessionStorage: Storage.save({ test: true }, { storageType: 'sessionStorage' }), cookies: false, indexedDB: 'indexedDB' in window }; } // Custom components (limited to 2) if (this.options.customComponents && this.options.customComponents.length > 0) { components.custom = {}; // Limit to maximum 2 custom components const customComponents = this.options.customComponents.slice(0, 2); await Promise.all(customComponents.map(async (component) => { try { components.custom[component.name] = await component.getValue(); } catch (e) { components.custom[component.name] = null; } })); } return components; } /** * Gets fingerprint result from storage * @returns Fingerprint result or null if not found */ getFromStorage() { if (this.options.storageType === 'none') { return null; } const data = Storage.get({ storageType: this.options.storageType, storagePrefix: this.options.storagePrefix }); if (!data || typeof data !== 'object') { return null; } try { const result = data; if (!result.id || !result.components || !result.createdAt) { return null; } // Mark as from cache result.fromCache = true; return result; } catch (e) { return null; } } /** * Saves fingerprint result to storage * @param result - Fingerprint result to save */ saveToStorage(result) { if (this.options.storageType === 'none') { return; } Storage.save(result, { storageType: this.options.storageType, storagePrefix: this.options.storagePrefix }); } /** * Updates the options for the fingerprint generator * @param options - New options to apply */ updateOptions(options) { // Update options this.options = { ...this.options, ...options }; // Limit number of custom components if (this.options.customComponents && this.options.customComponents.length > 2) { this.options.customComponents = this.options.customComponents.slice(0, 2); } // Clear last result to force regeneration with new options this.lastResult = null; } /** * Gets the current options for the fingerprint generator * @returns Current options */ getOptions() { return { ...this.options }; } } /** * UserDNA-Community - Community edition of fingerprint generator library for creating unique user identifiers * @module UserDNA-Community */ /** * Creates a new instance of UserDNA-Community fingerprint generator * @param options - Configuration options for UserDNA-Community * @returns FingerprintGenerator instance */ function createUserDNA(options) { return new FingerprintGenerator(options); } // Default export for easier access const UserDNACommunity = { createUserDNA, FingerprintGenerator }; exports.FingerprintGenerator = FingerprintGenerator; exports.createUserDNA = createUserDNA; exports["default"] = UserDNACommunity; Object.defineProperty(exports, '__esModule', { value: true }); })); //# sourceMappingURL=index.umd.js.map