const BROWSER_FINGERPRINT_COOKIE_NAME = "zapi:bf"; async function sha256(source: string) { const sourceBytes = new TextEncoder().encode(source); const digest = await crypto.subtle.digest("SHA-256", sourceBytes); const resultBytes = [...new Uint8Array(digest)]; return resultBytes.map((x) => x.toString(16).padStart(2, "0")).join(""); } function getRandomUUID() { if (crypto?.randomUUID) { return crypto.randomUUID(); } const randomValues = new Uint32Array(4); crypto.getRandomValues(randomValues); return `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`.replace(/[xy]/g, function (c) { const r = (randomValues[0] + Math.random() * 16) % 16 | 0; randomValues[0] = Math.floor(randomValues[0] / 16); return (c === "x" ? r : (r & 0x3) | 0x8).toString(16); }); } function getCookieSliding(name: string, days: number, domain?: string | null | undefined) { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); const result = parts.length === 2 ? parts.pop()?.split(";").shift() : null; if (result) { setCookie(name, result, days, domain); } return result; } function setCookie(name: string, value: string, days: number, domain?: string | null | undefined) { const date = new Date(); date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); domain = !domain || domain.startsWith(".") ? domain : `.${domain}`; let cookieValue = `${name}=${value}; expires=${date.toUTCString()};path=/;Secure;SameSite=Strict`; if (domain && window.location.hostname.endsWith(domain)) { cookieValue += `; domain=${domain}`; } document.cookie = cookieValue; } function getLocalStorageSliding(key: string, days: number) { const raw = localStorage.getItem(key); if (raw) { const data = JSON.parse(raw) as ExpirableLocalStorageItem; if (data.expires < Date.now()) { localStorage.removeItem(key); return null; } else { setLocalStorage(key, data.value, days); return data.value; } } return null; } function setLocalStorage(key: string, value: string, days: number) { const expires = Date.now() + days * 24 * 60 * 60 * 1000; const data = JSON.stringify({ value, expires }); localStorage.setItem(key, data); } function matchMedia(query: string) { return window.matchMedia(`(${query})`).matches; } class Fingerprinter { static get instance() { return (this.#instance ??= new this()); } static #instance: Fingerprinter | undefined; #canvasFingerprint?: string | null | undefined; #browserFingerprint?: string | null | undefined; // increment this if the canvas fingerprinting method changes #canvasFingerprintVersion = 1; private constructor() {} async getAttributes(opts: FingerPrinterOptions): Promise { this.#configureBrowserFingerprint(opts.storageTtlDays, opts.cookieDomain); await this.#configureCanvasFingerprint(); return { canvas: `v${this.#canvasFingerprintVersion}|${this.#canvasFingerprint}`, storage: this.#browserFingerprint, screenWidth: window.screen.width, screenHeight: window.screen.height, timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, prefersReducedMotion: matchMedia("prefers-reduced-motion: reduce"), prefersColorScheme: matchMedia("prefers-color-scheme: dark") ? "dark" : "light", gpc: navigator.globalPrivacyControl ?? null, doNotTrack: navigator.doNotTrack ?? null, // webkit seems to yield undefined when the spec indicates null }; } #configureBrowserFingerprint(ttlDays: number, cookieDomain?: string | null | undefined) { if (this.#browserFingerprint) { return; } let fingerprint = getCookieSliding(BROWSER_FINGERPRINT_COOKIE_NAME, ttlDays, cookieDomain); if (!fingerprint) { fingerprint = getLocalStorageSliding(BROWSER_FINGERPRINT_COOKIE_NAME, ttlDays); if (fingerprint) { // restore cookie from localStorage setCookie(BROWSER_FINGERPRINT_COOKIE_NAME, fingerprint, ttlDays, cookieDomain); } } if (!fingerprint) { fingerprint = getRandomUUID(); setLocalStorage(BROWSER_FINGERPRINT_COOKIE_NAME, fingerprint, ttlDays); setCookie(BROWSER_FINGERPRINT_COOKIE_NAME, fingerprint, ttlDays, cookieDomain); } this.#browserFingerprint = fingerprint; } async #configureCanvasFingerprint() { if (this.#canvasFingerprint) { return; } try { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); if (!ctx) { return; } const txt = "ZYWAVE"; ctx.textBaseline = "top"; ctx.textBaseline = "top"; ctx.font = "14px 'Arial'"; ctx.textBaseline = "alphabetic"; ctx.fillStyle = "#f60"; ctx.fillRect(125, 1, 62, 20); ctx.fillStyle = "#069"; ctx.fillText(txt, 2, 15); ctx.fillStyle = "rgba(102, 204, 0, 0.7)"; ctx.fillText(txt, 4, 17); const dataUrl = canvas.toDataURL(); const data = dataUrl.split(",")[1]; this.#canvasFingerprint = await sha256(data); } catch (e) { // left empty on purpose } } } const fingerPrinter = Fingerprinter.instance; export { fingerPrinter as Fingerprinter }; // property names end with an underscore for terser export type FingerprintAttributes = { canvas: string | null | undefined; storage: string | null | undefined; screenWidth: number; screenHeight: number; timeZone: string; prefersReducedMotion: boolean; prefersColorScheme: "light" | "dark"; gpc: boolean | null; doNotTrack: string | null; }; type FingerPrinterOptions = { cookieDomain?: string | null | undefined; storageTtlDays: number; }; type ExpirableLocalStorageItem = { value: string; expires: number; }; declare global { interface Navigator { globalPrivacyControl?: boolean; } }