userdnajs
Version:
Community edition of UserDNA JS. A fingerprint generator library for creating unique user identifiers
646 lines (636 loc) • 22.4 kB
JavaScript
(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