Browser Fingerprint Viewer — JavaScript source
See exactly what websites can learn about your browser without cookies — screen, GPU, fonts, timezone, language, and more. Educational, not tracking.
This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
// browser-fingerprint — JavaScript polyglot showcase port (pure analysis layer).
//
// Ported from the TypeScript reference at src/lib/browser-fingerprint.ts.
// Covers the portable core: the 18-signal registry with privacy-risk
// classifications, canonical category grouping, risk counts, and the stable
// SHA-256 fingerprint hash. The browser-API collection layer (canvas / WebGL
// / font probes) lives in the live lib. sha256Hex uses the Web Crypto API —
// available in every browser and in Node.js >= 18 as globalThis.crypto.
//
// Display source — part of CosmoDev's polyglot tool pages.
/** Privacy-risk classification: 'low' | 'medium' | 'high'. */
/**
* Static registry of every signal the tool collects, with label, risk and
* category. Single source of truth: buildSignal() derives from it, so a
* signal can never be collected without being classified.
*
* Risk rubric:
* high — near-unique on its own or part of a rare combination
* medium — narrows you to a sizable-but-specific population
* low — coarse; shared by millions of browsers
*/
const SIGNAL_DEFINITIONS = Object.freeze({
// High risk
canvas: { label: 'Canvas fingerprint', risk: 'high', category: 'Graphics' },
'webgl-renderer': { label: 'WebGL renderer', risk: 'high', category: 'Graphics' },
'webgl-vendor': { label: 'WebGL vendor', risk: 'high', category: 'Graphics' },
fonts: { label: 'Installed fonts', risk: 'high', category: 'Browser' },
timezone: { label: 'Timezone', risk: 'high', category: 'Browser' },
// Medium risk
screen: { label: 'Screen & color depth', risk: 'medium', category: 'Hardware' },
'device-pixel-ratio': { label: 'Device pixel ratio', risk: 'medium', category: 'Hardware' },
'hardware-concurrency': { label: 'CPU cores', risk: 'medium', category: 'Hardware' },
'device-memory': { label: 'Device Memory', risk: 'medium', category: 'Hardware' },
platform: { label: 'Platform', risk: 'medium', category: 'Browser' },
languages: { label: 'Languages', risk: 'medium', category: 'Browser' },
touch: { label: 'Touch support', risk: 'medium', category: 'Input' },
connection: { label: 'Connection type', risk: 'medium', category: 'Network' },
// Low risk
'user-agent': { label: 'User agent', risk: 'low', category: 'Browser' },
'do-not-track': { label: 'Do Not Track', risk: 'low', category: 'Browser' },
'cookies-enabled': { label: 'Cookies enabled', risk: 'low', category: 'Browser' },
online: { label: 'Online status', risk: 'low', category: 'Network' },
'pdf-viewer': { label: 'PDF viewer', risk: 'low', category: 'Browser' },
});
/** Canonical category order (card grid, top to bottom). */
const FINGERPRINT_CATEGORIES = Object.freeze([
'Hardware',
'Graphics',
'Network',
'Browser',
'Input',
]);
/** Risk level for a known signal id; undefined for unknown ids. */
function classifySignalRisk(id) {
return SIGNAL_DEFINITIONS[id]?.risk;
}
/**
* Build a signal from its id and collected value. Throws on unknown ids so a
* typo'd id fails loudly instead of silently rendering an unclassified row.
*/
function buildSignal(id, value) {
const def = SIGNAL_DEFINITIONS[id];
if (!def) throw new Error(`Unknown fingerprint signal id: ${id}`);
return { id, label: def.label, value, risk: def.risk, category: def.category };
}
/** Group signals by category in canonical order, omitting empty categories. */
function groupByCategory(signals) {
const groups = new Map();
for (const signal of signals) {
const list = groups.get(signal.category);
if (list) list.push(signal);
else groups.set(signal.category, [signal]);
}
return FINGERPRINT_CATEGORIES.filter((c) => groups.has(c)).map((category) => ({
category,
signals: groups.get(category),
}));
}
/** Count signals per risk level (drives the summary line). */
function countByRisk(signals) {
const counts = { low: 0, medium: 0, high: 0 };
for (const signal of signals) counts[signal.risk] += 1;
return counts;
}
/** The exact byte string the fingerprint hash is computed over. */
function concatSignalValues(signals) {
return signals.map((s) => s.value).join('|');
}
/** SHA-256 hex digest via the Web Crypto API (Node >= 18 exposes it too). */
async function sha256Hex(input) {
const data = new TextEncoder().encode(input);
const digest = await crypto.subtle.digest('SHA-256', data);
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, '0')).join('');
}
/**
* Stable fingerprint ID: SHA-256 over every signal value, joined in signal
* order. Same browser state → same hash; any single changed value → new hash.
*/
async function hashFingerprint(signals) {
return sha256Hex(concatSignalValues(signals));
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →