devicer.js 1.0.13 → 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.
@@ -0,0 +1,81 @@
1
+ import type { Comparator } from "../types/data";
2
+ import { initializeDefaultRegistry } from "./defaults";
3
+
4
+ interface RegistryState {
5
+ comparators: Record<string, Comparator>;
6
+ weights: Record<string, number>;
7
+ defaultWeight: number;
8
+ }
9
+
10
+ let registry: RegistryState = {
11
+ comparators: {},
12
+ weights: {},
13
+ defaultWeight: 5,
14
+ };
15
+
16
+ let defaultsInitialized = false;
17
+
18
+ /** Internal helper – called automatically on first use */
19
+ function ensureDefaults(): void {
20
+ if (!defaultsInitialized) {
21
+ initializeDefaultRegistry();
22
+ defaultsInitialized = true;
23
+ }
24
+ }
25
+
26
+ /** Register a custom similarity comparator for a field or nested path */
27
+ export function registerComparator(path: string, comparator: Comparator): void {
28
+ if (typeof comparator !== "function") {
29
+ throw new Error("Comparator must be a function returning a 0–1 similarity score");
30
+ }
31
+ registry.comparators[path] = comparator;
32
+ }
33
+
34
+ /** Register (or override) the weight for a field or nested path */
35
+ export function registerWeight(path: string, weight: number): void {
36
+ if (typeof weight !== "number" || weight < 0) {
37
+ throw new Error("Weight must be a non-negative number");
38
+ }
39
+ registry.weights[path] = weight;
40
+ }
41
+
42
+ /** Convenience: register weight + comparator in one call (most common pattern) */
43
+ export function registerPlugin(
44
+ path: string,
45
+ config: { weight?: number; comparator?: Comparator }
46
+ ): void {
47
+ if (config.weight !== undefined) registerWeight(path, config.weight);
48
+ if (config.comparator !== undefined) registerComparator(path, config.comparator);
49
+ }
50
+
51
+ /** Change the fallback weight for any unregistered field */
52
+ export function setDefaultWeight(weight: number): void {
53
+ registry.defaultWeight = Math.max(0, weight);
54
+ }
55
+
56
+ /** Remove a registered comparator */
57
+ export function unregisterComparator(path: string): boolean {
58
+ return delete registry.comparators[path];
59
+ }
60
+
61
+ /** Remove a registered weight */
62
+ export function unregisterWeight(path: string): boolean {
63
+ return delete registry.weights[path];
64
+ }
65
+
66
+ /** Reset everything (perfect for tests) */
67
+ export function clearRegistry(): void {
68
+ registry = { comparators: {}, weights: {}, defaultWeight: 5 };
69
+ }
70
+
71
+ // Internal only – used by createConfidenceCalculator
72
+ export function getGlobalRegistry(): Readonly<RegistryState> {
73
+ ensureDefaults();
74
+ return {
75
+ ...registry,
76
+ comparators: { ...registry.comparators },
77
+ weights: { ...registry.weights },
78
+ };
79
+ }
80
+
81
+ export { initializeDefaultRegistry };
package/src/libs/tlsh.ts CHANGED
@@ -1,19 +1,19 @@
1
- import hash from 'tlsh';
2
- import DigestHashBuilder from 'tlsh/lib/digests/digest-hash-builder.js';
3
-
4
- export function getHash(data: string): string {
5
- // Convert the input data to a string if it's not already
6
- const inputString = typeof data === 'string' ? data : JSON.stringify(data);
7
-
8
- // Generate the TLSH hash
9
- const tlshHash = hash(inputString);
10
-
11
- // Return the hash as a string
12
- return tlshHash;
13
- }
14
-
15
- export function compareHashes(hash1: string, hash2: string): number {
16
- const digest1 = DigestHashBuilder().withHash(hash1).build();
17
- const digest2 = DigestHashBuilder().withHash(hash2).build();
18
- return digest1.calculateDifference(digest2, true);
1
+ import hash from 'tlsh';
2
+ import DigestHashBuilder from 'tlsh/lib/digests/digest-hash-builder.js';
3
+
4
+ export function getHash(data: string): string {
5
+ // Convert the input data to a string if it's not already
6
+ const inputString = typeof data === 'string' ? data : JSON.stringify(data);
7
+
8
+ // Generate the TLSH hash
9
+ const tlshHash = hash(inputString);
10
+
11
+ // Return the hash as a string
12
+ return tlshHash;
13
+ }
14
+
15
+ export function compareHashes(hash1: string, hash2: string): number {
16
+ const digest1 = DigestHashBuilder().withHash(hash1).build();
17
+ const digest2 = DigestHashBuilder().withHash(hash2).build();
18
+ return digest1.calculateDifference(digest2, true);
19
19
  }
package/src/main.ts CHANGED
@@ -1,4 +1,27 @@
1
- import { FPUserDataSet, FPDataSet } from "./types/data";
2
- import { compareArrays, compareDatasets, calculateConfidence } from "./libs/confidence";
3
-
4
- export { type FPUserDataSet, type FPDataSet, compareArrays, compareDatasets, calculateConfidence };
1
+ import { FPUserDataSet, FPDataSet } from "./types/data";
2
+ import { calculateConfidence, createConfidenceCalculator } from "./libs/confidence";
3
+ import {
4
+ registerComparator,
5
+ registerWeight,
6
+ registerPlugin,
7
+ unregisterComparator,
8
+ unregisterWeight,
9
+ setDefaultWeight,
10
+ clearRegistry,
11
+ initializeDefaultRegistry
12
+ } from "./libs/registry";
13
+
14
+ export {
15
+ type FPUserDataSet,
16
+ type FPDataSet,
17
+ calculateConfidence,
18
+ createConfidenceCalculator,
19
+ registerComparator,
20
+ registerWeight,
21
+ registerPlugin,
22
+ unregisterComparator,
23
+ unregisterWeight,
24
+ setDefaultWeight,
25
+ clearRegistry,
26
+ initializeDefaultRegistry
27
+ };
package/src/tlsh.d.ts CHANGED
@@ -1,14 +1,14 @@
1
- declare module 'tlsh' {
2
- function hash(data: string): string;
3
- export default hash;
4
- }
5
-
6
- declare module 'tlsh/lib/digests/digest-hash-builder.js' {
7
- export default function DigestHashBuilder(): {
8
- withHash: (hash: string) => {
9
- build: () => {
10
- calculateDifference: (other: any, normalize?: boolean) => number;
11
- };
12
- };
13
- };
1
+ declare module 'tlsh' {
2
+ function hash(data: string): string;
3
+ export default hash;
4
+ }
5
+
6
+ declare module 'tlsh/lib/digests/digest-hash-builder.js' {
7
+ export default function DigestHashBuilder(): {
8
+ withHash: (hash: string) => {
9
+ build: () => {
10
+ calculateDifference: (other: any, normalize?: boolean) => number;
11
+ };
12
+ };
13
+ };
14
14
  }
package/src/types/data.ts CHANGED
@@ -1,25 +1,60 @@
1
- export interface FPUserDataSet {
2
- fonts: string[];
3
- hardware: {
4
- cpu: string;
5
- gpu: string;
6
- ram: number; // in MB
7
- }
8
- userAgent: string;
9
- screen: {
10
- width: number;
11
- height: number;
12
- colorDepth: number;
13
- };
14
- timezone: string;
15
- ip: string;
16
- languages: string[];
17
- plugins: string[];
18
- canvasHash: string;
19
- audioHash: string;
20
- webglHash: string;
21
- }
22
-
23
- export interface FPDataSet {
24
- [key: string]: any;
1
+ export interface FPUserDataSet {
2
+ userAgent: string;
3
+ platform: string;
4
+ timezone: string;
5
+ language: string;
6
+ languages: string[];
7
+ cookieEnabled: boolean;
8
+ doNotTrack: string | boolean;
9
+ hardwareConcurrency: number;
10
+ deviceMemory: number | string;
11
+ product: string;
12
+ productSub: string;
13
+ vendor: string;
14
+ vendorSub: string;
15
+ appName: string;
16
+ appVersion: string;
17
+ appCodeName: string;
18
+ appMinorVersion: string;
19
+ buildID: string;
20
+ plugins: {
21
+ name: string;
22
+ description: string;
23
+ }[];
24
+ mimeTypes: {
25
+ type: string;
26
+ suffixes: string;
27
+ description: string;
28
+ }[];
29
+ screen: {
30
+ width: number;
31
+ height: number;
32
+ colorDepth: number;
33
+ pixelDepth: number;
34
+ orientation: {
35
+ type: string;
36
+ angle: number;
37
+ };
38
+ };
39
+ fonts: string[];
40
+ highEntropyValues: Record<string, string | number | boolean>;
41
+ }
42
+
43
+ export type FPDataSet<T extends Record<string, any> = FPUserDataSet> = T;
44
+
45
+ export type Comparator = (value1: any, value2: any, path?: string) => number; // 0.0–1.0 similarity
46
+
47
+ export interface ComparisonOptions {
48
+ /** Field/path weights (higher = more important). Will be normalized automatically. */
49
+ weights?: Record<string, number>;
50
+ /** Custom similarity functions (your plugin system) */
51
+ comparators?: Record<string, Comparator>;
52
+ /** Fallback weight for any field without an explicit weight */
53
+ defaultWeight?: number;
54
+ /** How much weight to give the TLSH hash component (0–1) */
55
+ tlshWeight?: number;
56
+ /** Max recursion depth for nested objects/arrays */
57
+ maxDepth?: number;
58
+ /** Whether this calculator should use the global registry (default: true) */
59
+ useGlobalRegistry?: boolean;
25
60
  }
@@ -1,110 +1,163 @@
1
- import { it, describe, expect } from 'vitest';
2
- import { FPUserDataSet } from '../src/types/data';
3
- import { calculateConfidence } from '../src/libs/confidence';
4
- import { randomString } from './tlsh.test';
5
-
6
- const sampleData1: FPUserDataSet = {
7
- fonts: ['Arial', 'Verdana'],
8
- hardware: {
9
- cpu: 'Intel Core i7',
10
- gpu: 'NVIDIA GTX 1080',
11
- ram: 16384 // in MB
12
- },
13
- userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3',
14
- screen: {
15
- width: 1920,
16
- height: 1080,
17
- colorDepth: 24
18
- },
19
- timezone: 'America/New_York',
20
- ip: '157.185.170.244',
21
- languages: ['en-US', 'en'],
22
- plugins: ['Chrome PDF Viewer', 'Shockwave Flash'],
23
- canvasHash: randomString(524),
24
- audioHash: randomString(524),
25
- webglHash: randomString(524)
26
- };
27
-
28
- const sampleData2: FPUserDataSet = {
29
- fonts: ['Arial', 'Verdana'],
30
- hardware: {
31
- cpu: 'Pentium 4',
32
- gpu: 'Intel HD Graphics',
33
- ram: 4096 // in MB
34
- },
35
- userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3',
36
- screen: {
37
- width: 1280,
38
- height: 720,
39
- colorDepth: 24
40
- },
41
- timezone: 'Europe/London',
42
- ip: '178.238.11.6',
43
- languages: ['en-GB', 'en'],
44
- plugins: ['Chrome PDF Viewer', 'Shockwave Flash'],
45
- canvasHash: randomString(524),
46
- audioHash: randomString(524),
47
- webglHash: randomString(524)
48
- };
49
-
50
- describe('Confidence Calculation', () => {
51
- it('should calculate confidence between two user data objects', () => {
52
- const confidence = calculateConfidence(sampleData1, sampleData2);
53
- console.log('Confidence:', confidence);
54
- expect(typeof confidence).toBe('number');
55
- expect(confidence).toBeGreaterThanOrEqual(0);
56
- expect(confidence).toBeLessThanOrEqual(100);
57
- });
58
-
59
- it('should return 100% confidence for identical user data', () => {
60
- const confidence = calculateConfidence(sampleData1, sampleData1);
61
- expect(confidence).toBe(100);
62
- });
63
-
64
- it('should return high confidence for similar user data', () => {
65
- const similarData: FPUserDataSet = {
66
- ...sampleData1,
67
- hardware: {
68
- ...sampleData1.hardware,
69
- ram: 8192 // Slightly different RAM
70
- }
71
- };
72
- const confidence = calculateConfidence(sampleData1, similarData);
73
- console.log('Confidence for similar data:', confidence);
74
- expect(confidence).toBeGreaterThan(80);
75
- });
76
-
77
- it('should return lower confidence for different user data', () => {
78
- const confidence = calculateConfidence(sampleData1, sampleData2);
79
- console.log('Confidence for different data:', confidence);
80
- expect(confidence).toBeLessThan(10);
81
- });
82
-
83
- it('should return middling confidence for partially similar data', () => {
84
- const partialData: FPUserDataSet = {
85
- ...sampleData1,
86
- hardware: {
87
- cpu: 'Pentium 4',
88
- gpu: 'Intel HD Graphics',
89
- ram: 4096
90
- },
91
- timezone: 'Europe/London',
92
- ip: '178.238.11.6'
93
- };
94
- const confidence = calculateConfidence(sampleData1, partialData);
95
- console.log('Confidence for partially similar data:', confidence);
96
- expect(confidence).toBeGreaterThan(10);
97
- expect(confidence).toBeLessThan(95);
98
- });
99
-
100
- it('should handle empty datasets and nonetypes gracefully', () => {
101
- const incompleteData = {
102
- ...sampleData1,
103
- hardware: {},
104
- screen: null
105
- };
106
- const confidence = calculateConfidence(sampleData1, incompleteData);
107
- expect(confidence).toBeGreaterThan(0); // Expecting some confidence even with missing data
108
- expect(confidence).toBeLessThan(100); // Not identical, so confidence should not be 100
109
- });
110
- });
1
+ import { describe, expect, it } from 'vitest';
2
+ import { calculateConfidence } from '../src/libs/confidence';
3
+ import type { FPUserDataSet } from '../src/types/data';
4
+
5
+ const sampleData1: FPUserDataSet = {
6
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/121.0.0.0 Safari/537.36',
7
+ platform: 'Win32',
8
+ timezone: 'America/New_York',
9
+ language: 'en-US',
10
+ languages: ['en-US', 'en'],
11
+ cookieEnabled: true,
12
+ doNotTrack: '1',
13
+ hardwareConcurrency: 8,
14
+ deviceMemory: 8,
15
+ product: 'Gecko',
16
+ productSub: '20030107',
17
+ vendor: 'Google Inc.',
18
+ vendorSub: '',
19
+ appName: 'Netscape',
20
+ appVersion: '5.0 (Windows)',
21
+ appCodeName: 'Mozilla',
22
+ appMinorVersion: '0',
23
+ buildID: '20240101000000',
24
+ plugins: [
25
+ { name: 'Chrome PDF Viewer', description: 'Portable Document Format' },
26
+ { name: 'Widevine Content Decryption Module', description: 'Content Protection' },
27
+ ],
28
+ mimeTypes: [
29
+ { type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format' },
30
+ { type: 'application/json', suffixes: 'json', description: 'JSON Data' },
31
+ ],
32
+ screen: {
33
+ width: 1920,
34
+ height: 1080,
35
+ colorDepth: 24,
36
+ pixelDepth: 24,
37
+ orientation: {
38
+ type: 'landscape-primary',
39
+ angle: 0,
40
+ },
41
+ },
42
+ fonts: ['Arial', 'Segoe UI', 'Times New Roman', 'Courier New'],
43
+ highEntropyValues: {
44
+ architecture: 'x86',
45
+ model: '',
46
+ platformVersion: '15.0.0',
47
+ uaFullVersion: '121.0.0.0',
48
+ wow64: false,
49
+ },
50
+ };
51
+
52
+ const sampleData2: FPUserDataSet = {
53
+ userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36',
54
+ platform: 'Linux x86_64',
55
+ timezone: 'Asia/Tokyo',
56
+ language: 'ja-JP',
57
+ languages: ['ja-JP', 'ja', 'en-US'],
58
+ cookieEnabled: false,
59
+ doNotTrack: '0',
60
+ hardwareConcurrency: 2,
61
+ deviceMemory: 2,
62
+ product: 'Gecko',
63
+ productSub: '20100101',
64
+ vendor: 'Chromium',
65
+ vendorSub: 'beta',
66
+ appName: 'Netscape',
67
+ appVersion: '5.0 (X11)',
68
+ appCodeName: 'Mozilla',
69
+ appMinorVersion: '1',
70
+ buildID: '20240202000000',
71
+ plugins: [
72
+ { name: 'VLC Web Plugin', description: 'VLC multimedia plugin' },
73
+ { name: 'Java Plug-in', description: 'Java Runtime' },
74
+ ],
75
+ mimeTypes: [
76
+ { type: 'video/webm', suffixes: 'webm', description: 'WebM Video' },
77
+ { type: 'application/xml', suffixes: 'xml', description: 'XML Data' },
78
+ ],
79
+ screen: {
80
+ width: 1366,
81
+ height: 768,
82
+ colorDepth: 30,
83
+ pixelDepth: 30,
84
+ orientation: {
85
+ type: 'portrait-primary',
86
+ angle: 90,
87
+ },
88
+ },
89
+ fonts: ['Noto Sans JP', 'Ubuntu', 'Monospace'],
90
+ highEntropyValues: {
91
+ architecture: 'arm',
92
+ model: 'Chromebook',
93
+ platformVersion: '6.1.0',
94
+ uaFullVersion: '122.0.0.0',
95
+ wow64: true,
96
+ },
97
+ };
98
+
99
+ describe('Confidence Calculation', () => {
100
+ it('should calculate confidence between two user data objects', () => {
101
+ const confidence = calculateConfidence(sampleData1, sampleData2);
102
+ console.log('Confidence:', confidence);
103
+ expect(typeof confidence).toBe('number');
104
+ expect(confidence).toBeGreaterThanOrEqual(0);
105
+ expect(confidence).toBeLessThanOrEqual(100);
106
+ });
107
+
108
+ it('should return 100% confidence for identical user data', () => {
109
+ const confidence = calculateConfidence(sampleData1, sampleData1);
110
+ expect(confidence).toBe(100);
111
+ });
112
+
113
+ it('should return high confidence for similar user data', () => {
114
+ const similarData: FPUserDataSet = {
115
+ ...sampleData1,
116
+ deviceMemory: 16,
117
+ };
118
+ const confidence = calculateConfidence(sampleData1, similarData);
119
+ console.log('Confidence for similar data:', confidence);
120
+ expect(confidence).toBeGreaterThan(80);
121
+ });
122
+
123
+ it('should return lower confidence for different user data', () => {
124
+ const confidence = calculateConfidence(sampleData1, sampleData2);
125
+ console.log('Confidence for different data:', confidence);
126
+ expect(confidence).toBeLessThan(10);
127
+ });
128
+
129
+ it('should return middling confidence for partially similar data', () => {
130
+ const partialData: FPUserDataSet = {
131
+ ...sampleData1,
132
+ hardwareConcurrency: 4,
133
+ deviceMemory: 4,
134
+ timezone: 'Europe/London',
135
+ language: 'en-GB',
136
+ screen: {
137
+ ...sampleData1.screen,
138
+ width: 1600,
139
+ height: 900,
140
+ },
141
+ highEntropyValues: {
142
+ ...sampleData1.highEntropyValues,
143
+ architecture: 'arm',
144
+ platformVersion: '14.0.0',
145
+ },
146
+ };
147
+ const confidence = calculateConfidence(sampleData1, partialData);
148
+ console.log('Confidence for partially similar data:', confidence);
149
+ expect(confidence).toBeGreaterThan(10);
150
+ expect(confidence).toBeLessThan(95);
151
+ });
152
+
153
+ it('should handle empty datasets and nonetypes gracefully', () => {
154
+ const incompleteData = {
155
+ ...sampleData1,
156
+ plugins: [],
157
+ screen: null,
158
+ } as unknown as FPUserDataSet;
159
+ const confidence = calculateConfidence(sampleData1, incompleteData);
160
+ expect(confidence).toBeGreaterThan(0);
161
+ expect(confidence).toBeLessThan(100);
162
+ });
163
+ });