@versini/ui-fingerprint 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # @versini/ui-fingerprint
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@versini/ui-fingerprint?style=flat-square)](https://www.npmjs.com/package/@versini/ui-fingerprint)
4
+ ![npm package minimized gzipped size](<https://img.shields.io/bundlejs/size/%40versini%2Fui-fingerprint?style=flat-square&label=size%20(gzip)>)
4
5
 
5
6
  > A React component for generating unique visual fingerprints built with TypeScript and TailwindCSS.
6
7
 
package/dist/index.d.ts CHANGED
@@ -1,77 +1,25 @@
1
- type BrowserFP = { browser: string };
2
-
3
- type VideoCardFP = {
4
- vendor: string;
5
- vendorUnmasked: string;
6
- renderer: string;
7
- rendererUnmasked: string;
8
- version: string;
9
- shadingLanguageVersion: string;
10
- };
11
-
12
- type HardwareFP = {
13
- hardware: {
14
- videocard: VideoCardFP;
15
- architecture: number;
16
- deviceMemory: string;
17
- jsHeapSizeLimit: number;
18
- };
19
- };
20
-
21
- type CanvasFP = {
22
- canvas: {
23
- data: string;
24
- };
25
- };
26
-
27
- type AudioFP = {
28
- audio: {
29
- sampleHash: string;
30
- oscillator: string;
31
- maxChannels: number;
32
- channelCountMode: string;
33
- };
34
- };
35
-
36
- type SystemFP = {
37
- system: {
38
- platform: string;
39
- cookieEnabled: boolean;
40
- productSub: string;
41
- product: string;
42
- };
43
- };
44
-
45
- type LocalesFP = {
46
- locales: {
47
- languages: string;
48
- timezone: string;
49
- };
50
- };
51
-
52
- type ScreenFP = {
53
- screen: {
54
- colorDepth: number;
55
- pixelDepth: number;
56
- isTouchScreen: boolean;
57
- maxTouchPoints: number;
58
- mediaMatches: string[];
59
- };
60
- };
61
-
62
- type FontsFP = string[];
63
-
64
- type FingerprintData = [
65
- AudioFP,
66
- BrowserFP,
67
- CanvasFP,
68
- FontsFP,
69
- HardwareFP,
70
- LocalesFP,
71
- ScreenFP,
72
- SystemFP
73
- ];
74
- declare const getFingerprintData: (debug?: boolean) => Promise<FingerprintData>;
75
- declare const getFingerprintHash: (debug?: boolean) => Promise<string>;
76
-
77
- export { getFingerprintData, getFingerprintHash };
1
+ import type { AudioFP } from '../common/types';
2
+ import type { BrowserFP } from '../common/types';
3
+ import type { CanvasFP } from '../common/types';
4
+ import type { FontsFP } from '../common/types';
5
+ import type { HardwareFP } from '../common/types';
6
+ import type { LocalesFP } from '../common/types';
7
+ import type { ScreenFP } from '../common/types';
8
+ import type { SystemFP } from '../common/types';
9
+
10
+ declare type FingerprintData = [
11
+ AudioFP,
12
+ BrowserFP,
13
+ CanvasFP,
14
+ FontsFP,
15
+ HardwareFP,
16
+ LocalesFP,
17
+ ScreenFP,
18
+ SystemFP
19
+ ];
20
+
21
+ export declare const getFingerprintData: (debug?: boolean) => Promise<FingerprintData>;
22
+
23
+ export declare const getFingerprintHash: (debug?: boolean) => Promise<string>;
24
+
25
+ export { }
package/dist/index.js CHANGED
@@ -1,359 +1,659 @@
1
1
  /*!
2
- @versini/ui-fingerprint v1.1.3
2
+ @versini/ui-fingerprint v1.2.0
3
3
  © 2025 gizmette.com
4
4
  */
5
5
  try {
6
- window.__VERSINI_UI_FINGERPRINT__ || (window.__VERSINI_UI_FINGERPRINT__ = {
7
- version: "1.1.3",
8
- buildTime: "08/23/2025 10:08 AM EDT",
9
- homepage: "https://github.com/aversini/ui-components",
10
- license: "MIT"
11
- });
12
- } catch {
6
+ if (!window.__VERSINI_UI_FINGERPRINT__) {
7
+ window.__VERSINI_UI_FINGERPRINT__ = {
8
+ version: "1.2.0",
9
+ buildTime: "11/04/2025 03:46 PM EST",
10
+ homepage: "https://github.com/aversini/ui-components",
11
+ license: "MIT",
12
+ };
13
+ }
14
+ } catch (error) {
15
+ // nothing to declare officer
13
16
  }
14
- const D = (t) => Array.from(t).map((e) => e.toString(16).padStart(2, "0")).join(""), w = async (t) => {
15
- if (t === "")
16
- return "";
17
- const e = new TextEncoder().encode(t), o = await crypto.subtle.digest("SHA-256", e);
18
- return Array.from(new Uint8Array(o)).map((s) => s.toString(16).padStart(2, "0")).join("");
17
+
18
+
19
+ ;// CONCATENATED MODULE: ./src/common/utilities.ts
20
+ const hashFromFloat32Array = (array)=>{
21
+ const hashArray = Array.from(array).map((b)=>b.toString(16).padStart(2, "0")).join("");
22
+ return hashArray;
19
23
  };
20
- function y(t, n) {
21
- return new Promise((e) => setTimeout(e, t, n));
22
- }
23
- async function T(t, n, e = 50) {
24
- const o = document;
25
- for (; !o.body; )
26
- await y(e);
27
- const r = o.createElement("iframe");
28
- try {
29
- for (await new Promise((a, s) => {
30
- let l = !1;
31
- const m = () => {
32
- l = !0, a();
33
- }, p = (f) => {
34
- l = !0, s(f);
35
- };
36
- r.onload = m, r.onerror = p;
37
- const { style: u } = r;
38
- u.setProperty("display", "block", "important"), u.position = "absolute", u.top = "0", u.left = "0", u.visibility = "hidden", r.src = "about:blank", o.body.appendChild(r);
39
- const g = () => {
40
- l || (r.contentWindow?.document?.readyState === "complete" ? m() : setTimeout(g, 10));
41
- };
42
- g();
43
- }); !r.contentWindow?.document?.body; )
44
- await y(e);
45
- return await t(r, r.contentWindow);
46
- } finally {
47
- r.parentNode?.removeChild(r);
48
- }
24
+ const hashFromString = async (input)=>{
25
+ if (input === "") {
26
+ return "";
27
+ }
28
+ const encoder = new TextEncoder();
29
+ const data = encoder.encode(input);
30
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
31
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
32
+ const hashHex = hashArray.map((b)=>b.toString(16).padStart(2, "0")).join("");
33
+ return hashHex;
34
+ };
35
+ function wait(durationMs, resolveWith) {
36
+ return new Promise((resolve)=>setTimeout(resolve, durationMs, resolveWith));
49
37
  }
50
- const x = {
51
- audio: {
52
- sampleHash: "",
53
- oscillator: "",
54
- maxChannels: 0,
55
- channelCountMode: ""
56
- }
57
- }, H = async (t) => new Promise((n) => {
58
- try {
59
- const e = new window.OfflineAudioContext(1, 5e3, 44100), o = e.createBufferSource(), r = e.createOscillator();
60
- r.frequency.value = 1e3;
61
- const a = e.createDynamicsCompressor();
62
- a.threshold.value = -50, a.knee.value = 40, a.ratio.value = 12, a.attack.value = 0, a.release.value = 0.2, r.connect(a), a.connect(e.destination), r.start(), e.startRendering(), e.oncomplete = (s) => {
63
- const l = s.renderedBuffer.getChannelData(0);
64
- r.disconnect(), a.disconnect(), n({
65
- audio: {
66
- sampleHash: D(l),
67
- oscillator: r.type,
68
- maxChannels: e.destination.maxChannelCount,
69
- channelCountMode: o.channelCountMode
38
+ /**
39
+ * Creates and keeps an invisible iframe while the given function runs.
40
+ * The given function is called when the iframe is loaded and has a body.
41
+ * The iframe allows to measure DOM sizes inside itself.
42
+ *
43
+ * Notice: passing an initial HTML code doesn't work in IE.
44
+ *
45
+ * Warning for package users:
46
+ * This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
47
+ */ async function withIframe(action, initialHtml, domPollInterval = 50) {
48
+ const d = document;
49
+ // document.body can be null while the page is loading
50
+ while(!d.body){
51
+ await wait(domPollInterval);
52
+ }
53
+ const iframe = d.createElement("iframe");
54
+ try {
55
+ await new Promise((_resolve, _reject)=>{
56
+ let isComplete = false;
57
+ const resolve = ()=>{
58
+ isComplete = true;
59
+ _resolve();
60
+ };
61
+ const reject = (error)=>{
62
+ isComplete = true;
63
+ _reject(error);
64
+ };
65
+ iframe.onload = resolve;
66
+ iframe.onerror = reject;
67
+ const { style } = iframe;
68
+ // Required for browsers to calculate the layout
69
+ style.setProperty("display", "block", "important");
70
+ style.position = "absolute";
71
+ style.top = "0";
72
+ style.left = "0";
73
+ style.visibility = "hidden";
74
+ if (initialHtml && "srcdoc" in iframe) {
75
+ iframe.srcdoc = initialHtml;
76
+ } else {
77
+ iframe.src = "about:blank";
78
+ }
79
+ d.body.appendChild(iframe);
80
+ // WebKit in WeChat doesn't fire the iframe's `onload` for some reason.
81
+ // This code checks for the loading state manually.
82
+ // See https://github.com/fingerprintjs/fingerprintjs/issues/645
83
+ const checkReadyState = ()=>{
84
+ // The ready state may never become 'complete' in Firefox despite the 'load' event being fired.
85
+ // So an infinite setTimeout loop can happen without this check.
86
+ // See https://github.com/fingerprintjs/fingerprintjs/pull/716#issuecomment-986898796
87
+ if (isComplete) {
88
+ return;
89
+ }
90
+ // Make sure iframe.contentWindow and iframe.contentWindow.document are both loaded
91
+ // The contentWindow.document can miss in JSDOM (https://github.com/jsdom/jsdom).
92
+ if (iframe.contentWindow?.document?.readyState === "complete") {
93
+ resolve();
94
+ } else {
95
+ setTimeout(checkReadyState, 10);
96
+ }
97
+ };
98
+ checkReadyState();
99
+ });
100
+ while(!iframe.contentWindow?.document?.body){
101
+ await wait(domPollInterval);
70
102
  }
71
- });
72
- };
73
- } catch (e) {
74
- t && console.error("Error creating audio fingerprint:", e), n({
75
- audio: {
103
+ return await action(iframe, iframe.contentWindow);
104
+ } finally{
105
+ iframe.parentNode?.removeChild(iframe);
106
+ }
107
+ }
108
+
109
+ ;// CONCATENATED MODULE: ./src/components/audio.ts
110
+
111
+ const emptyAudio = {
112
+ audio: {
76
113
  sampleHash: "",
77
114
  oscillator: "",
78
115
  maxChannels: 0,
79
116
  channelCountMode: ""
80
- }
117
+ }
118
+ };
119
+ const getAudio = async (debug)=>{
120
+ return new Promise((resolve)=>{
121
+ try {
122
+ const audioContext = new window.OfflineAudioContext(1, 5000, 44100);
123
+ const audioBuffer = audioContext.createBufferSource();
124
+ const oscillator = audioContext.createOscillator();
125
+ oscillator.frequency.value = 1000;
126
+ const compressor = audioContext.createDynamicsCompressor();
127
+ compressor.threshold.value = -50;
128
+ compressor.knee.value = 40;
129
+ compressor.ratio.value = 12;
130
+ compressor.attack.value = 0;
131
+ compressor.release.value = 0.2;
132
+ oscillator.connect(compressor);
133
+ compressor.connect(audioContext.destination);
134
+ oscillator.start();
135
+ audioContext.startRendering();
136
+ audioContext.oncomplete = (event)=>{
137
+ const samples = event.renderedBuffer.getChannelData(0);
138
+ oscillator.disconnect();
139
+ compressor.disconnect();
140
+ resolve({
141
+ audio: {
142
+ sampleHash: hashFromFloat32Array(samples),
143
+ oscillator: oscillator.type,
144
+ maxChannels: audioContext.destination.maxChannelCount,
145
+ channelCountMode: audioBuffer.channelCountMode
146
+ }
147
+ });
148
+ };
149
+ } catch (error) {
150
+ if (debug) {
151
+ console.error("Error creating audio fingerprint:", error);
152
+ }
153
+ resolve({
154
+ audio: {
155
+ sampleHash: "",
156
+ oscillator: "",
157
+ maxChannels: 0,
158
+ channelCountMode: ""
159
+ }
160
+ });
161
+ }
81
162
  });
82
- }
83
- }), v = { browser: "" }, P = async (t) => typeof navigator > "u" ? v : { browser: navigator.userAgent }, E = {
84
- canvas: {
85
- data: ""
86
- }
87
- }, R = async (t) => {
88
- try {
89
- const o = Array.from(
90
- { length: 3 },
91
- () => _(300, 30)
92
- ), r = N(o, 300, 30);
93
- return {
94
- canvas: {
95
- data: (await w(r.data.toString())).toString()
96
- }
97
- };
98
- } catch (o) {
99
- return t && (console.error("Error creating canvas fingerprint"), console.info(o)), E;
100
- }
101
- }, _ = (t, n) => {
102
- const e = document.createElement("canvas"), o = e.getContext("2d");
103
- if (!o)
104
- return new ImageData(1, 1);
105
- e.width = t, e.height = n;
106
- const r = o.createLinearGradient(0, 0, e.width, e.height);
107
- r.addColorStop(0, "red"), r.addColorStop(1 / 6, "orange"), r.addColorStop(2 / 6, "yellow"), r.addColorStop(3 / 6, "green"), r.addColorStop(4 / 6, "blue"), r.addColorStop(5 / 6, "indigo"), r.addColorStop(1, "violet"), o.fillStyle = r, o.fillRect(0, 0, e.width, e.height);
108
- const a = "mmMwWLliI0O&1 - Les sanglots longs des violons de l'automne blessent mon coeur d'une langueur monotone";
109
- return o.font = "26.321px Arial", o.fillStyle = "black", o.fillText(a, -5, 15), o.fillStyle = "rgba(0, 0, 255, 0.5)", o.fillText(a, -3.3, 17.7), o.beginPath(), o.moveTo(0, 0), o.lineTo(e.width * 2 / 7, e.height), o.strokeStyle = "white", o.lineWidth = 2, o.stroke(), o.getImageData(0, 0, e.width, e.height);
110
- }, F = (t) => {
111
- if (t.length === 0)
112
- return 0;
113
- const n = {};
114
- for (const o of t)
115
- n[o] = (n[o] || 0) + 1;
116
- let e = t[0];
117
- for (const o in n)
118
- n[o] > n[e] && (e = parseInt(o, 10));
119
- return e;
120
- }, N = (t, n, e) => {
121
- const o = [];
122
- for (let s = 0; s < t[0].data.length; s++) {
123
- const l = [];
124
- for (let m = 0; m < t.length; m++)
125
- l.push(t[m].data[s]);
126
- o.push(F(l));
127
- }
128
- const r = o, a = new Uint8ClampedArray(r);
129
- return new ImageData(a, n, e);
130
- }, G = [], L = "mmMwWLliI0O&1", k = "48px", h = ["monospace", "sans-serif", "serif"], S = [
131
- "sans-serif-thin",
132
- "ARNO PRO",
133
- "Agency FB",
134
- "Arabic Typesetting",
135
- "Arial Unicode MS",
136
- "AvantGarde Bk BT",
137
- "BankGothic Md BT",
138
- "Bitstream Vera Sans Mono",
139
- "Calibri",
140
- "Century",
141
- "Century Gothic",
142
- "Clarendon",
143
- "EUROSTILE",
144
- "Franklin Gothic",
145
- "GOTHAM",
146
- "Gill Sans",
147
- "Helvetica Neue",
148
- "Letter Gothic",
149
- "Menlo",
150
- "MS Outlook",
151
- "MS Reference Specialty",
152
- "MS UI Gothic",
153
- "MT Extra",
154
- "MYRIAD PRO",
155
- "Marlett",
156
- "Microsoft Uighur",
157
- "Minion Pro",
158
- "Monotype Corsiva",
159
- "PMingLiU",
160
- "Pristina",
161
- "SCRIPTINA",
162
- "SimHei",
163
- "Small Fonts",
164
- "Staccato222 BT",
165
- "TRAJAN PRO",
166
- "Univers CE 55 Medium",
167
- "ZWAdobeF"
168
- ], O = async (t) => T(async (n, { document: e }) => {
169
- const o = e.body;
170
- o.style.fontSize = k;
171
- const r = e.createElement("div");
172
- r.style.setProperty("visibility", "hidden", "important");
173
- const a = {}, s = {}, l = (i) => {
174
- const c = e.createElement("span"), { style: d } = c;
175
- return d.position = "absolute", d.top = "0", d.left = "0", d.fontFamily = i, c.textContent = L, r.appendChild(c), c;
176
- }, m = (i, c) => l(`'${i}',${c}`), p = () => h.map(l), u = () => {
177
- const i = {};
178
- for (const c of S)
179
- i[c] = h.map(
180
- (d) => m(c, d)
181
- );
182
- return i;
183
- }, g = (i) => h.some(
184
- (c, d) => i[d].offsetWidth !== a[c] || i[d].offsetHeight !== s[c]
185
- ), f = p(), I = u();
186
- o.appendChild(r);
187
- for (let i = 0; i < h.length; i++)
188
- a[h[i]] = f[i].offsetWidth, s[h[i]] = f[i].offsetHeight;
189
- return S.filter((i) => g(I[i]));
190
- }), C = {
191
- vendor: "",
192
- vendorUnmasked: "",
193
- renderer: "",
194
- rendererUnmasked: "",
195
- version: "",
196
- shadingLanguageVersion: ""
197
- }, A = {
198
- hardware: {
199
- videocard: C,
200
- architecture: 0,
201
- deviceMemory: "undefined",
202
- jsHeapSizeLimit: 0
203
- }
204
163
  };
205
- function U() {
206
- const t = document.createElement("canvas"), n = t.getContext("webgl") ?? t.getContext("experimental-webgl");
207
- if (n && "getParameter" in n) {
208
- const e = n.getExtension("WEBGL_debug_renderer_info");
209
- return {
210
- vendor: (n.getParameter(n.VENDOR) || "").toString(),
211
- vendorUnmasked: e ? (n.getParameter(e.UNMASKED_VENDOR_WEBGL) || "").toString() : "",
212
- renderer: (n.getParameter(n.RENDERER) || "").toString(),
213
- rendererUnmasked: e ? (n.getParameter(e.UNMASKED_RENDERER_WEBGL) || "").toString() : "",
214
- version: (n.getParameter(n.VERSION) || "").toString(),
215
- shadingLanguageVersion: (n.getParameter(n.SHADING_LANGUAGE_VERSION) || "").toString()
164
+
165
+ ;// CONCATENATED MODULE: ./src/components/browser.ts
166
+ const emptyBrowser = {
167
+ browser: ""
168
+ };
169
+ const getBrowser = async (_debug)=>{
170
+ return typeof navigator === "undefined" ? emptyBrowser : {
171
+ browser: navigator.userAgent
216
172
  };
217
- }
218
- return C;
173
+ };
174
+
175
+ ;// CONCATENATED MODULE: ./src/components/canvas.ts
176
+
177
+ const emptyCanvas = {
178
+ canvas: {
179
+ data: ""
180
+ }
181
+ };
182
+ const getCanvas = async (debug)=>{
183
+ const WIDTH = 300;
184
+ const HEIGHT = 30;
185
+ try {
186
+ /**
187
+ * Creating 3 identical image data with same text and same colors,
188
+ * and extracting the common pixels from them. This is a fingerprint
189
+ * busting technique to defeat canvas counter-fingerprinting.
190
+ */ const imageData = Array.from({
191
+ length: 3
192
+ }, ()=>generateImageData(WIDTH, HEIGHT));
193
+ const commonImageData = getCommonPixels(imageData, WIDTH, HEIGHT);
194
+ const hashedData = (await hashFromString(commonImageData.data.toString())).toString();
195
+ return {
196
+ canvas: {
197
+ data: hashedData
198
+ }
199
+ };
200
+ } catch (error) {
201
+ if (debug) {
202
+ console.error("Error creating canvas fingerprint");
203
+ console.info(error);
204
+ }
205
+ return emptyCanvas;
206
+ }
207
+ };
208
+ const generateImageData = (width, height)=>{
209
+ const canvas = document.createElement("canvas");
210
+ const ctx = canvas.getContext("2d");
211
+ if (!ctx) {
212
+ return new ImageData(1, 1);
213
+ }
214
+ canvas.width = width;
215
+ canvas.height = height;
216
+ // Create rainbow gradient for the background rectangle
217
+ const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
218
+ gradient.addColorStop(0, "red");
219
+ gradient.addColorStop(1 / 6, "orange");
220
+ gradient.addColorStop(2 / 6, "yellow");
221
+ gradient.addColorStop(3 / 6, "green");
222
+ gradient.addColorStop(4 / 6, "blue");
223
+ gradient.addColorStop(5 / 6, "indigo");
224
+ gradient.addColorStop(1, "violet");
225
+ // Draw background rectangle with the rainbow gradient
226
+ ctx.fillStyle = gradient;
227
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
228
+ // Draw some random text
229
+ // We use m or w because these two characters take up the maximum width.
230
+ // And we use a LLi so that the same matching fonts can get separated.
231
+ const randomText = "mmMwWLliI0O&1 - Les sanglots longs des violons de l'automne blessent mon coeur d'une langueur monotone";
232
+ ctx.font = "26.321px Arial";
233
+ ctx.fillStyle = "black";
234
+ ctx.fillText(randomText, -5, 15);
235
+ // Draw the same text with an offset, different color, and slight transparency
236
+ ctx.fillStyle = "rgba(0, 0, 255, 0.5)";
237
+ ctx.fillText(randomText, -3.3, 17.7);
238
+ // Draw a line crossing the image at an arbitrary angle
239
+ ctx.beginPath();
240
+ ctx.moveTo(0, 0);
241
+ ctx.lineTo(canvas.width * 2 / 7, canvas.height);
242
+ ctx.strokeStyle = "white";
243
+ ctx.lineWidth = 2;
244
+ ctx.stroke();
245
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
246
+ return imageData;
247
+ };
248
+ const getMostFrequent = (arr)=>{
249
+ if (arr.length === 0) {
250
+ return 0;
251
+ }
252
+ const frequencyMap = {};
253
+ for (const num of arr){
254
+ frequencyMap[num] = (frequencyMap[num] || 0) + 1;
255
+ }
256
+ let mostFrequent = arr[0];
257
+ // Find the number with the highest frequency
258
+ for(const num in frequencyMap){
259
+ if (frequencyMap[num] > frequencyMap[mostFrequent]) {
260
+ mostFrequent = parseInt(num, 10);
261
+ }
262
+ }
263
+ return mostFrequent;
264
+ };
265
+ const getCommonPixels = (images, width, height)=>{
266
+ const finalData = [];
267
+ for(let i = 0; i < images[0].data.length; i++){
268
+ const indice = [];
269
+ for(let u = 0; u < images.length; u++){
270
+ indice.push(images[u].data[i]);
271
+ }
272
+ finalData.push(getMostFrequent(indice));
273
+ }
274
+ const pixelData = finalData;
275
+ const pixelArray = new Uint8ClampedArray(pixelData);
276
+ return new ImageData(pixelArray, width, height);
277
+ };
278
+
279
+ ;// CONCATENATED MODULE: ./src/components/fonts.ts
280
+
281
+ const emptyFonts = [];
282
+ // We use m or w because these two characters take up the maximum width.
283
+ // And we use a LLi so that the same matching fonts can get separated.
284
+ const testString = "mmMwWLliI0O&1";
285
+ // We test using 48px font size, we may use any size. I guess larger the better.
286
+ const textSize = "48px";
287
+ // A font will be compared against all the three default fonts.
288
+ // And if for any default fonts it doesn't match, then that font is available.
289
+ const baseFonts = [
290
+ "monospace",
291
+ "sans-serif",
292
+ "serif"
293
+ ];
294
+ const fontList = [
295
+ "sans-serif-thin",
296
+ "ARNO PRO",
297
+ "Agency FB",
298
+ "Arabic Typesetting",
299
+ "Arial Unicode MS",
300
+ "AvantGarde Bk BT",
301
+ "BankGothic Md BT",
302
+ "Bitstream Vera Sans Mono",
303
+ "Calibri",
304
+ "Century",
305
+ "Century Gothic",
306
+ "Clarendon",
307
+ "EUROSTILE",
308
+ "Franklin Gothic",
309
+ "GOTHAM",
310
+ "Gill Sans",
311
+ "Helvetica Neue",
312
+ "Letter Gothic",
313
+ "Menlo",
314
+ "MS Outlook",
315
+ "MS Reference Specialty",
316
+ "MS UI Gothic",
317
+ "MT Extra",
318
+ "MYRIAD PRO",
319
+ "Marlett",
320
+ "Microsoft Uighur",
321
+ "Minion Pro",
322
+ "Monotype Corsiva",
323
+ "PMingLiU",
324
+ "Pristina",
325
+ "SCRIPTINA",
326
+ "SimHei",
327
+ "Small Fonts",
328
+ "Staccato222 BT",
329
+ "TRAJAN PRO",
330
+ "Univers CE 55 Medium",
331
+ "ZWAdobeF"
332
+ ];
333
+ const getFonts = async (_debug)=>{
334
+ return withIframe(async (_, { document })=>{
335
+ const holder = document.body;
336
+ holder.style.fontSize = textSize;
337
+ // div to load spans for the default fonts and the fonts to detect
338
+ const spansContainer = document.createElement("div");
339
+ spansContainer.style.setProperty("visibility", "hidden", "important");
340
+ const defaultWidth = {};
341
+ const defaultHeight = {};
342
+ // creates a span where the fonts will be loaded
343
+ const createSpan = (fontFamily)=>{
344
+ const span = document.createElement("span");
345
+ const { style } = span;
346
+ style.position = "absolute";
347
+ style.top = "0";
348
+ style.left = "0";
349
+ style.fontFamily = fontFamily;
350
+ span.textContent = testString;
351
+ spansContainer.appendChild(span);
352
+ return span;
353
+ };
354
+ // creates a span and load the font to detect and a base font for fallback
355
+ const createSpanWithFonts = (fontToDetect, baseFont)=>{
356
+ return createSpan(`'${fontToDetect}',${baseFont}`);
357
+ };
358
+ // creates spans for the base fonts and adds them to baseFontsDiv
359
+ const initializeBaseFontsSpans = ()=>{
360
+ return baseFonts.map(createSpan);
361
+ };
362
+ // creates spans for the fonts to detect and adds them to fontsDiv
363
+ const initializeFontsSpans = ()=>{
364
+ // Stores {fontName : [spans for that font]}
365
+ const spans = {};
366
+ for (const font of fontList){
367
+ spans[font] = baseFonts.map((baseFont)=>createSpanWithFonts(font, baseFont));
368
+ }
369
+ return spans;
370
+ };
371
+ // checks if a font is available
372
+ const isFontAvailable = (fontSpans)=>{
373
+ return baseFonts.some((baseFont, baseFontIndex)=>fontSpans[baseFontIndex].offsetWidth !== defaultWidth[baseFont] || fontSpans[baseFontIndex].offsetHeight !== defaultHeight[baseFont]);
374
+ };
375
+ // create spans for base fonts
376
+ const baseFontsSpans = initializeBaseFontsSpans();
377
+ // create spans for fonts to detect
378
+ const fontsSpans = initializeFontsSpans();
379
+ // add all the spans to the DOM
380
+ holder.appendChild(spansContainer);
381
+ // get the default width for the three base fonts
382
+ for(let index = 0; index < baseFonts.length; index++){
383
+ defaultWidth[baseFonts[index]] = baseFontsSpans[index].offsetWidth; // width for the default font
384
+ defaultHeight[baseFonts[index]] = baseFontsSpans[index].offsetHeight; // height for the default font
385
+ }
386
+ // check available fonts
387
+ return fontList.filter((font)=>isFontAvailable(fontsSpans[font]));
388
+ });
389
+ };
390
+
391
+ ;// CONCATENATED MODULE: ./src/components/hardware.ts
392
+ const emptyVideoCard = {
393
+ vendor: "",
394
+ vendorUnmasked: "",
395
+ renderer: "",
396
+ rendererUnmasked: "",
397
+ version: "",
398
+ shadingLanguageVersion: ""
399
+ };
400
+ const emptyHardware = {
401
+ hardware: {
402
+ videocard: emptyVideoCard,
403
+ architecture: 0,
404
+ deviceMemory: "undefined",
405
+ jsHeapSizeLimit: 0
406
+ }
407
+ };
408
+ function getVideoCard() {
409
+ const canvas = document.createElement("canvas");
410
+ const gl = canvas.getContext("webgl") ?? canvas.getContext("experimental-webgl");
411
+ if (gl && "getParameter" in gl) {
412
+ const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
413
+ return {
414
+ vendor: (gl.getParameter(gl.VENDOR) || "").toString(),
415
+ vendorUnmasked: debugInfo ? (gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || "").toString() : "",
416
+ renderer: (gl.getParameter(gl.RENDERER) || "").toString(),
417
+ rendererUnmasked: debugInfo ? (gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || "").toString() : "",
418
+ version: (gl.getParameter(gl.VERSION) || "").toString(),
419
+ shadingLanguageVersion: (gl.getParameter(gl.SHADING_LANGUAGE_VERSION) || "").toString()
420
+ };
421
+ }
422
+ return emptyVideoCard;
219
423
  }
220
- function B() {
221
- const t = new Float32Array(1), n = new Uint8Array(t.buffer);
222
- return t[0] = 1 / 0, t[0] = t[0] - t[0], n[3];
424
+ function getArchitecture() {
425
+ const f = new Float32Array(1);
426
+ const u8 = new Uint8Array(f.buffer);
427
+ f[0] = Infinity;
428
+ f[0] = f[0] - f[0];
429
+ return u8[3];
223
430
  }
224
- const W = () => navigator.deviceMemory || 0, V = () => window.performance && window.performance.memory || {
225
- jsHeapSizeLimit: 0
226
- }, z = async (t) => new Promise((n) => {
227
- try {
228
- const e = W(), o = V();
229
- n({
230
- hardware: {
231
- videocard: U(),
232
- architecture: B(),
233
- deviceMemory: e.toString() || "undefined",
234
- jsHeapSizeLimit: o.jsHeapSizeLimit || 0
235
- }
431
+ // @ts-ignore
432
+ const getDeviceMemory = ()=>navigator.deviceMemory || 0;
433
+ const getMemoryInfo = ()=>window.performance && window.performance.memory || {
434
+ jsHeapSizeLimit: 0
435
+ };
436
+ const getHardware = async (debug)=>{
437
+ return new Promise((resolve)=>{
438
+ try {
439
+ const deviceMemory = getDeviceMemory();
440
+ const memoryInfo = getMemoryInfo();
441
+ resolve({
442
+ hardware: {
443
+ videocard: getVideoCard(),
444
+ architecture: getArchitecture(),
445
+ deviceMemory: deviceMemory.toString() || "undefined",
446
+ jsHeapSizeLimit: memoryInfo.jsHeapSizeLimit || 0
447
+ }
448
+ });
449
+ } catch (error) {
450
+ if (debug) {
451
+ console.error("Error getting hardware data");
452
+ console.info(error);
453
+ }
454
+ resolve(emptyHardware);
455
+ }
236
456
  });
237
- } catch (e) {
238
- t && (console.error("Error getting hardware data"), console.info(e)), n(A);
239
- }
240
- }), j = {
241
- locales: {
242
- languages: "",
243
- timezone: ""
244
- }
245
- }, $ = async (t) => new Promise((n) => {
246
- n({
457
+ };
458
+
459
+ ;// CONCATENATED MODULE: ./src/components/locale.ts
460
+ const emptyLocales = {
247
461
  locales: {
248
- languages: navigator.language,
249
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
462
+ languages: "",
463
+ timezone: ""
250
464
  }
251
- });
252
- }), M = {
253
- screen: {
254
- colorDepth: 0,
255
- pixelDepth: 0,
256
- isTouchScreen: !1,
257
- maxTouchPoints: 0,
258
- mediaMatches: []
259
- }
260
- }, q = async (t) => new Promise((n) => {
261
- try {
262
- const e = window.screen, o = {
263
- screen: {
264
- colorDepth: e.colorDepth,
265
- pixelDepth: e.pixelDepth,
266
- isTouchScreen: navigator.maxTouchPoints > 0,
267
- maxTouchPoints: navigator.maxTouchPoints,
268
- mediaMatches: J()
269
- }
465
+ };
466
+ const getLocales = async (_debug)=>{
467
+ return new Promise((resolve)=>{
468
+ resolve({
469
+ locales: {
470
+ languages: navigator.language,
471
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
472
+ }
473
+ });
474
+ });
475
+ };
476
+
477
+ ;// CONCATENATED MODULE: ./src/components/screen.ts
478
+ const emptyScreen = {
479
+ screen: {
480
+ colorDepth: 0,
481
+ pixelDepth: 0,
482
+ isTouchScreen: false,
483
+ maxTouchPoints: 0,
484
+ mediaMatches: []
485
+ }
486
+ };
487
+ const getScreen = async (debug)=>{
488
+ return new Promise((resolve)=>{
489
+ try {
490
+ const screen = window.screen;
491
+ const screenData = {
492
+ screen: {
493
+ colorDepth: screen.colorDepth,
494
+ pixelDepth: screen.pixelDepth,
495
+ isTouchScreen: navigator.maxTouchPoints > 0,
496
+ maxTouchPoints: navigator.maxTouchPoints,
497
+ mediaMatches: mediaMatches()
498
+ }
499
+ };
500
+ resolve(screenData);
501
+ } catch (error) {
502
+ if (debug) {
503
+ console.error("Error creating screen fingerprint");
504
+ console.info(error);
505
+ }
506
+ resolve(emptyScreen);
507
+ }
508
+ });
509
+ };
510
+ function mediaMatches() {
511
+ const results = [];
512
+ const mediaQueries = {
513
+ "prefers-contrast": [
514
+ "high",
515
+ "more",
516
+ "low",
517
+ "less",
518
+ "forced",
519
+ "no-preference"
520
+ ],
521
+ "any-hover": [
522
+ "hover",
523
+ "none"
524
+ ],
525
+ "any-pointer": [
526
+ "none",
527
+ "coarse",
528
+ "fine"
529
+ ],
530
+ pointer: [
531
+ "none",
532
+ "coarse",
533
+ "fine"
534
+ ],
535
+ hover: [
536
+ "hover",
537
+ "none"
538
+ ],
539
+ update: [
540
+ "fast",
541
+ "slow"
542
+ ],
543
+ "inverted-colors": [
544
+ "inverted",
545
+ "none"
546
+ ],
547
+ "prefers-reduced-motion": [
548
+ "reduce",
549
+ "no-preference"
550
+ ],
551
+ "prefers-reduced-transparency": [
552
+ "reduce",
553
+ "no-preference"
554
+ ],
555
+ scripting: [
556
+ "none",
557
+ "initial-only",
558
+ "enabled"
559
+ ],
560
+ "forced-colors": [
561
+ "active",
562
+ "none"
563
+ ],
564
+ "color-gamut": [
565
+ "srgb",
566
+ "p3",
567
+ "rec2020"
568
+ ]
270
569
  };
271
- n(o);
272
- } catch (e) {
273
- t && (console.error("Error creating screen fingerprint"), console.info(e)), n(M);
274
- }
275
- });
276
- function J() {
277
- const t = [], n = {
278
- "prefers-contrast": [
279
- "high",
280
- "more",
281
- "low",
282
- "less",
283
- "forced",
284
- "no-preference"
285
- ],
286
- "any-hover": ["hover", "none"],
287
- "any-pointer": ["none", "coarse", "fine"],
288
- pointer: ["none", "coarse", "fine"],
289
- hover: ["hover", "none"],
290
- update: ["fast", "slow"],
291
- "inverted-colors": ["inverted", "none"],
292
- "prefers-reduced-motion": ["reduce", "no-preference"],
293
- "prefers-reduced-transparency": ["reduce", "no-preference"],
294
- scripting: ["none", "initial-only", "enabled"],
295
- "forced-colors": ["active", "none"],
296
- "color-gamut": ["srgb", "p3", "rec2020"]
297
- };
298
- return Object.keys(n).forEach((e) => {
299
- n[e].forEach((o) => {
300
- matchMedia(`(${e}: ${o})`).matches && t.push(`${e}: ${o}`);
570
+ Object.keys(mediaQueries).forEach((key)=>{
571
+ mediaQueries[key].forEach((value)=>{
572
+ if (matchMedia(`(${key}: ${value})`).matches) {
573
+ results.push(`${key}: ${value}`);
574
+ }
575
+ });
301
576
  });
302
- }), t;
577
+ return results;
303
578
  }
304
- const K = async (t) => {
305
- try {
306
- return {
307
- system: {
308
- platform: navigator.platform,
309
- cookieEnabled: navigator.cookieEnabled,
310
- productSub: navigator.productSub,
311
- product: navigator.product
312
- }
313
- };
314
- } catch (n) {
315
- return t && (console.error("Error getting system data"), console.info(n)), b;
316
- }
317
- }, b = {
318
- system: {
319
- platform: "",
320
- cookieEnabled: !1,
321
- productSub: "",
322
- product: ""
323
- }
324
- }, Z = async (t) => {
325
- try {
326
- return Promise.all([
327
- H(t),
328
- P(),
329
- R(t),
330
- O(),
331
- z(t),
332
- $(),
333
- q(t),
334
- K(t)
335
- ]);
336
- } catch {
337
- return [
338
- x,
339
- v,
340
- E,
341
- G,
342
- A,
343
- j,
344
- M,
345
- b
346
- ];
347
- }
348
- }, Q = async (t) => {
349
- try {
350
- const n = await Z(t);
351
- return await w(JSON.stringify(n));
352
- } catch (n) {
353
- return t && (console.error("Error getting fingerprint hash"), console.info(n)), "";
354
- }
579
+
580
+ ;// CONCATENATED MODULE: ./src/components/system.ts
581
+ const getSystem = async (debug)=>{
582
+ try {
583
+ return {
584
+ system: {
585
+ platform: navigator.platform,
586
+ cookieEnabled: navigator.cookieEnabled,
587
+ productSub: navigator.productSub,
588
+ product: navigator.product
589
+ }
590
+ };
591
+ } catch (error) {
592
+ if (debug) {
593
+ console.error("Error getting system data");
594
+ console.info(error);
595
+ }
596
+ return emptySystem;
597
+ }
598
+ };
599
+ const emptySystem = {
600
+ system: {
601
+ platform: "",
602
+ cookieEnabled: false,
603
+ productSub: "",
604
+ product: ""
605
+ }
355
606
  };
356
- export {
357
- Z as getFingerprintData,
358
- Q as getFingerprintHash
607
+
608
+ ;// CONCATENATED MODULE: ./src/common/fingerprint.ts
609
+
610
+
611
+
612
+
613
+
614
+
615
+
616
+
617
+
618
+ const getFingerprintData = async (debug)=>{
619
+ try {
620
+ return Promise.all([
621
+ getAudio(debug),
622
+ getBrowser(debug),
623
+ getCanvas(debug),
624
+ getFonts(debug),
625
+ getHardware(debug),
626
+ getLocales(debug),
627
+ getScreen(debug),
628
+ getSystem(debug)
629
+ ]);
630
+ } catch (_error) {
631
+ return [
632
+ emptyAudio,
633
+ emptyBrowser,
634
+ emptyCanvas,
635
+ emptyFonts,
636
+ emptyHardware,
637
+ emptyLocales,
638
+ emptyScreen,
639
+ emptySystem
640
+ ];
641
+ }
642
+ };
643
+ const getFingerprintHash = async (debug)=>{
644
+ try {
645
+ const data = await getFingerprintData(debug);
646
+ return await hashFromString(JSON.stringify(data));
647
+ } catch (_error) {
648
+ if (debug) {
649
+ console.error("Error getting fingerprint hash");
650
+ console.info(_error);
651
+ }
652
+ return "";
653
+ }
359
654
  };
655
+
656
+ ;// CONCATENATED MODULE: ./src/components/index.ts
657
+
658
+
659
+ export { getFingerprintData, getFingerprintHash };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versini/ui-fingerprint",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "license": "MIT",
5
5
  "author": "Arno Versini",
6
6
  "publishConfig": {
@@ -20,13 +20,13 @@
20
20
  ],
21
21
  "scripts": {
22
22
  "build:check": "tsc",
23
- "build:js": "vite build",
24
- "build:types": "tsup",
25
- "build": "npm-run-all --serial clean build:check build:js build:types",
23
+ "build:js": "rslib build",
24
+ "build:types": "echo 'Types now built with rslib'",
25
+ "build": "npm-run-all --serial clean build:check build:js",
26
26
  "clean": "rimraf dist tmp",
27
- "dev:js": "vite build --watch --mode development",
28
- "dev:types": "tsup --watch src",
29
- "dev": "npm-run-all clean --parallel dev:js dev:types",
27
+ "dev:js": "rslib build --watch",
28
+ "dev:types": "echo 'Types now watched with rslib'",
29
+ "dev": "rslib build --watch",
30
30
  "lint": "biome lint src",
31
31
  "lint:fix": "biome check src --write --no-errors-on-unmatched",
32
32
  "prettier": "biome check --write --no-errors-on-unmatched",
@@ -34,5 +34,5 @@
34
34
  "test:watch": "vitest",
35
35
  "test": "vitest run"
36
36
  },
37
- "gitHead": "a1afd6e4613b1da7abf61d10a72614611521fb39"
37
+ "gitHead": "7484ad443b77ef31e52ae3a7d88b8129bc6cdf1d"
38
38
  }