UNPKG

@thumbmarkjs/thumbmarkjs

Version:

![GitHub package.json dynamic](https://img.shields.io/github/package-json/version/ilkkapeltola/thumbmarkjs) ![NPM Version](https://img.shields.io/npm/v/@thumbmarkjs/thumbmarkjs) ![NPM Downloads](https://img.shields.io/npm/dm/%40thumbmarkjs%2Fthumbmarkjs

155 lines (135 loc) 5.71 kB
/** * ThumbmarkJS: Main fingerprinting and API logic * * This module handles component collection, API calls, uniqueness scoring, and data filtering * for the ThumbmarkJS browser fingerprinting library. * */ import {defaultOptions, OptionsAfterDefaults, optionsInterface} from "../options"; import { timeoutInstance, componentInterface, tm_component_promises, customComponents, tm_experimental_component_promises, includeComponent as globalIncludeComponent } from "../factory"; import { hash } from "../utils/hash"; import { raceAllPerformance } from "../utils/raceAll"; import { getVersion } from "../utils/version"; import { filterThumbmarkData } from './filterComponents' import { logThumbmarkData } from '../utils/log'; import { getApiPromise, infoInterface } from "./api"; import { stableStringify } from "../utils/stableStringify"; /** * Final thumbmark response structure */ interface thumbmarkResponse { components: componentInterface, info: { [key: string]: any }, version: string, thumbmark: string, visitorId?: string, elapsed?: any; error?: string; experimental?: componentInterface; } /** * Main entry point: collects all components, optionally calls API, and returns thumbmark data. * * @param options - Options for fingerprinting and API * @returns thumbmarkResponse (elapsed is present only if options.performance is true) */ export async function getThumbmark(options?: optionsInterface): Promise<thumbmarkResponse> { const _options = { ...defaultOptions, ...options } as OptionsAfterDefaults; // Early logging decision const shouldLog = (_options.logging && !sessionStorage.getItem("_tmjs_l") && Math.random() < 0.0001); // Merge built-in and user-registered components const allComponents = { ...tm_component_promises, ...customComponents }; const { elapsed, resolvedComponents: clientComponentsResult } = await resolveClientComponents(allComponents, _options); // Resolve experimental components only when logging let experimentalComponents = {}; let experimentalElapsed = {}; if (shouldLog || _options.experimental) { const { elapsed: expElapsed, resolvedComponents } = await resolveClientComponents(tm_experimental_component_promises, _options); experimentalComponents = resolvedComponents; experimentalElapsed = expElapsed; } const apiPromise = _options.api_key ? getApiPromise(_options, clientComponentsResult) : null; let apiResult = null; if (apiPromise) { try { apiResult = await apiPromise; } catch (error) { // Handle API key/quota errors if (error instanceof Error && error.message === 'INVALID_API_KEY') { return { error: 'Invalid API key or quota exceeded', components: {}, info: {}, version: getVersion(), thumbmark: '' }; } throw error; // Re-throw other errors } } // Only add 'elapsed' if performance is true const allElapsed = { ...elapsed, ...experimentalElapsed }; const maybeElapsed = _options.performance ? { elapsed: allElapsed } : {}; const apiComponents = filterThumbmarkData(apiResult?.components || {}, _options); const components = { ...clientComponentsResult, ...apiComponents }; const info: infoInterface = apiResult?.info || { uniqueness: { score: 'api only' } }; // Use API thumbmark if available to ensure API/client sync, otherwise calculate locally const thumbmark = apiResult?.thumbmark ?? hash(stableStringify(components)); const version = getVersion(); // Only log to server when not in debug mode if (shouldLog) { logThumbmarkData(thumbmark, components, _options, experimentalComponents).catch(() => { /* do nothing */ }); } const result: thumbmarkResponse = { ...(apiResult?.visitorId && { visitorId: apiResult.visitorId }), thumbmark, components: components, info, version, ...maybeElapsed, ...(Object.keys(experimentalComponents).length > 0 && _options.experimental && { experimental: experimentalComponents }), }; return result; } // ===================== Component Resolution & Performance ===================== /** * Resolves and times all filtered component promises from a component function map. * * @param comps - Map of component functions * @param options - Options for filtering and timing * @returns Object with elapsed times and filtered resolved components */ export async function resolveClientComponents( comps: { [key: string]: (options?: optionsInterface) => Promise<componentInterface | null> }, options?: optionsInterface ): Promise<{ elapsed: Record<string, number>, resolvedComponents: componentInterface }> { const opts = { ...defaultOptions, ...options }; const filtered = Object.entries(comps) .filter(([key]) => !opts?.exclude?.includes(key)) .filter(([key]) => opts?.include?.some(e => e.includes('.')) ? opts?.include?.some(e => e.startsWith(key)) : opts?.include?.length === 0 || opts?.include?.includes(key) ); const keys = filtered.map(([key]) => key); const promises = filtered.map(([_, fn]) => fn(options)); const resolvedValues = await raceAllPerformance(promises, opts?.timeout || 5000, timeoutInstance); const elapsed: Record<string, number> = {}; const resolvedComponentsRaw: Record<string, componentInterface> = {}; resolvedValues.forEach((value, index) => { if (value.value != null) { resolvedComponentsRaw[keys[index]] = value.value; elapsed[keys[index]] = value.elapsed ?? 0; } }); const resolvedComponents = filterThumbmarkData(resolvedComponentsRaw, opts); return { elapsed, resolvedComponents }; } export { globalIncludeComponent as includeComponent };