1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153 | export async function collectFingerprint(session) {
if (!session.consent.analytics) {
session.device = { status: 'Not collected: analytics cookies rejected' };
return session.device;
}
const device = {
userAgent: navigator.userAgent || 'Unknown',
browser: parseBrowser(navigator.userAgent),
os: parseOs(navigator.userAgent, navigator.platform),
platform: navigator.platform || 'Unknown',
screen: `${screen.width} x ${screen.height} @ ${window.devicePixelRatio || 1}x`,
width: screen.width,
height: screen.height,
pixelRatio: window.devicePixelRatio || 1,
language: navigator.language || 'Unknown',
languages: Array.isArray(navigator.languages) ? navigator.languages.join(', ') : 'Unknown',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'Unknown',
localTime: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
darkMode: window.matchMedia?.('(prefers-color-scheme: dark)').matches || false,
reducedMotion: window.matchMedia?.('(prefers-reduced-motion: reduce)').matches || false,
hardwareConcurrency: navigator.hardwareConcurrency || 'Unknown',
doNotTrack: navigator.doNotTrack || window.doNotTrack || 'unspecified',
touchPoints: navigator.maxTouchPoints || 0,
referrer: document.referrer || 'Direct / hidden',
utm: readUtmParams(),
connection: readConnection(),
battery: 'Unsupported',
gpu: readWebGlRenderer(),
canvasSignature: readCanvasSignature(),
fontSignal: readFontSignal(),
deviceTier: 'Unknown',
};
device.battery = await readBattery();
device.deviceTier = inferDeviceTier(device);
session.device = device;
session.incognito = await detectIncognito();
return device;
}
function parseBrowser(ua) {
if (!ua) return 'Unknown';
const rules = [
['Edge', /Edg\/([\d.]+)/],
['Chrome', /Chrome\/([\d.]+)/],
['Firefox', /Firefox\/([\d.]+)/],
['Safari', /Version\/([\d.]+).*Safari/],
];
const match = rules.map(([name, regex]) => [name, ua.match(regex)]).find(([, found]) => found);
return match ? `${match[0]} ${match[1][1]}` : 'Unknown browser';
}
function parseOs(ua, platform) {
const text = `${ua} ${platform}`.toLowerCase();
if (text.includes('iphone') || text.includes('ipad')) return 'iOS / iPadOS';
if (text.includes('android')) return 'Android';
if (text.includes('mac')) return 'macOS';
if (text.includes('win')) return 'Windows';
if (text.includes('linux')) return 'Linux';
return platform || 'Unknown OS';
}
function readUtmParams() {
const params = new URLSearchParams(window.location.search);
const entries = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content']
.filter((key) => params.has(key))
.map((key) => `${key}=${params.get(key)}`);
return entries.length ? entries.join(', ') : 'None';
}
function readConnection() {
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (!connection) return 'Unsupported';
return [connection.effectiveType, connection.type, connection.downlink ? `${connection.downlink}Mbps` : null]
.filter(Boolean)
.join(' / ');
}
async function readBattery() {
if (!navigator.getBattery) return 'Unsupported';
try {
const battery = await navigator.getBattery();
return `${Math.round(battery.level * 100)}%, ${battery.charging ? 'charging' : 'not charging'}`;
} catch (error) {
return 'Blocked';
}
}
function readWebGlRenderer() {
try {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) return 'Unavailable';
const extension = gl.getExtension('WEBGL_debug_renderer_info');
if (!extension) return 'Hidden by browser';
return gl.getParameter(extension.UNMASKED_RENDERER_WEBGL) || 'Unknown GPU';
} catch (error) {
return 'Blocked';
}
}
function readCanvasSignature() {
try {
const canvas = document.createElement('canvas');
canvas.width = 220;
canvas.height = 48;
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.font = '16px Arial';
ctx.fillStyle = '#f60';
ctx.fillRect(0, 0, 220, 48);
ctx.fillStyle = '#069';
ctx.fillText('Accept the Cookie 14.3(b)', 8, 10);
const hash = hashString(canvas.toDataURL());
return `canvas-${hash}`;
} catch (error) {
return 'Blocked';
}
}
function readFontSignal() {
const fonts = ['Arial', 'Courier New', 'Georgia', 'Helvetica', 'Times New Roman', 'Verdana'];
return `${fonts.length} common fonts probed`;
}
function inferDeviceTier(device) {
const cores = Number(device.hardwareConcurrency) || 2;
const pixels = (Number(device.width) || 0) * (Number(device.height) || 0) * (Number(device.pixelRatio) || 1);
if (cores >= 8 && pixels > 5000000) return 'Premium / high-power';
if (cores >= 4) return 'Mid-range';
return 'Budget / protected / unknown';
}
async function detectIncognito() {
try {
if (!navigator.storage?.estimate) return { detected: null, reason: 'Storage estimate unsupported' };
const estimate = await navigator.storage.estimate();
const quotaMb = Math.round((estimate.quota || 0) / 1024 / 1024);
if (quotaMb > 0 && quotaMb < 120) return { detected: true, reason: `Limited storage quota: ${quotaMb}MB` };
return { detected: false, reason: quotaMb ? `Storage quota: ${quotaMb}MB` : 'No quota signal' };
} catch (error) {
return { detected: null, reason: 'Storage estimate blocked' };
}
}
function hashString(value) {
let hash = 0;
for (let i = 0; i < value.length; i++) {
hash = ((hash << 5) - hash + value.charCodeAt(i)) | 0;
}
return Math.abs(hash).toString(16).padStart(8, '0');
}
|