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.
- package/README.md +29 -20
- package/dist/libs/comparitors.js +17 -0
- package/dist/libs/confidence.js +101 -0
- package/dist/libs/registry.js +56 -0
- package/{src → dist}/libs/tlsh.js +2 -3
- package/dist/main.js +14 -0
- package/license.txt +26 -26
- package/package.json +9 -4
- package/src/libs/comparitors.ts +11 -0
- package/src/libs/confidence.ts +123 -143
- package/src/libs/defaults.ts +22 -0
- package/src/libs/registry.ts +81 -0
- package/src/libs/tlsh.ts +18 -18
- package/src/main.ts +27 -4
- package/src/tlsh.d.ts +13 -13
- package/src/types/data.ts +59 -24
- package/tests/confidence.test.ts +163 -110
- package/tests/tlsh.test.ts +69 -69
- package/tsconfig.json +114 -114
- package/src/libs/confidence.js +0 -106
- package/src/main.js +0 -7
- package/tests/comparisons.test.ts +0 -98
- package/tests/data.test.ts +0 -106
- /package/{src → dist}/types/data.js +0 -0
package/README.md
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
|
-
# FP-Devicer
|
|
2
|
-
## Developed by Gateway Corporate Solutions LLC
|
|
3
|
-
|
|
4
|
-
FP-Devicer is a digital fingerprinting middleware library designed for ease of use and near-universal compatibility with servers.
|
|
5
|
-
|
|
6
|
-
Importing and using the library to compare fingerprints between users is as simple as collecting some user data and running the calculateConfidence function.
|
|
7
|
-
```javascript
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
# FP-Devicer
|
|
2
|
+
## Developed by Gateway Corporate Solutions LLC
|
|
3
|
+
|
|
4
|
+
FP-Devicer is a digital fingerprinting middleware library designed for ease of use and near-universal compatibility with servers.
|
|
5
|
+
|
|
6
|
+
Importing and using the library to compare fingerprints between users is as simple as collecting some user data and running the calculateConfidence function.
|
|
7
|
+
```javascript
|
|
8
|
+
import { calculateConfidence, createConfidenceCalculator, registerPlugin } from "devicer.js";
|
|
9
|
+
|
|
10
|
+
// 1. Simple Method (Using defaults)
|
|
11
|
+
const score = calculateConfidence(fpData1, fpData2);
|
|
12
|
+
|
|
13
|
+
// 2. Advanced Method (Custom weights & comparitors)
|
|
14
|
+
registerPlugin("userAgent", {
|
|
15
|
+
weight: 25,
|
|
16
|
+
comparator: (a, b) => levenshteinSimilarity(String(a || "").toLowerCase(), String(b || "").toLowerCase())
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const advancedCalculator = createConfidenceCalculator({
|
|
20
|
+
weights: {
|
|
21
|
+
platform: 20,
|
|
22
|
+
fonts: 20,
|
|
23
|
+
screen: 15
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const advancedScore = advancedCalculator.calculateConfidence(fpData1, fpData2);
|
|
28
|
+
```
|
|
29
|
+
|
|
21
30
|
The resulting confidence will range between 0 and 100, with 100 providing the highest confidence of the users being identical.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.levenshteinSimilarity = levenshteinSimilarity;
|
|
4
|
+
function levenshteinSimilarity(a, b) {
|
|
5
|
+
if (a === b)
|
|
6
|
+
return 1;
|
|
7
|
+
if (!a || !b)
|
|
8
|
+
return 0;
|
|
9
|
+
const maxLen = Math.max(a.length, b.length);
|
|
10
|
+
let distance = Math.abs(a.length - b.length);
|
|
11
|
+
const minLen = Math.min(a.length, b.length);
|
|
12
|
+
for (let i = 0; i < minLen; i++) {
|
|
13
|
+
if (a[i] !== b[i])
|
|
14
|
+
distance++;
|
|
15
|
+
}
|
|
16
|
+
return Math.max(0, 1 - distance / maxLen);
|
|
17
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.calculateConfidence = void 0;
|
|
4
|
+
exports.createConfidenceCalculator = createConfidenceCalculator;
|
|
5
|
+
const tlsh_1 = require("./tlsh");
|
|
6
|
+
const registry_1 = require("./registry");
|
|
7
|
+
const DEFAULT_WEIGHTS = {
|
|
8
|
+
userAgent: 20,
|
|
9
|
+
platform: 15,
|
|
10
|
+
timezone: 10,
|
|
11
|
+
language: 10,
|
|
12
|
+
languages: 10,
|
|
13
|
+
cookieEnabled: 5,
|
|
14
|
+
doNotTrack: 5,
|
|
15
|
+
hardwareConcurrency: 5,
|
|
16
|
+
deviceMemory: 5,
|
|
17
|
+
product: 5,
|
|
18
|
+
productSub: 5,
|
|
19
|
+
vendor: 5,
|
|
20
|
+
vendorSub: 5,
|
|
21
|
+
appName: 5,
|
|
22
|
+
appVersion: 5,
|
|
23
|
+
appCodeName: 5,
|
|
24
|
+
appMinorVersion: 5,
|
|
25
|
+
buildID: 5,
|
|
26
|
+
plugins: 10,
|
|
27
|
+
mimeTypes: 10,
|
|
28
|
+
screen: 10,
|
|
29
|
+
fonts: 15
|
|
30
|
+
};
|
|
31
|
+
function createConfidenceCalculator(userOptions = {}) {
|
|
32
|
+
var _a;
|
|
33
|
+
const { weights: localWeights = {}, comparators: localComparators = {}, defaultWeight: localDefaultWeight = 5, tlshWeight = 0.30, maxDepth = 5, useGlobalRegistry = true, } = userOptions;
|
|
34
|
+
// Merge global registry (if enabled) → local always wins
|
|
35
|
+
const global = useGlobalRegistry ? (0, registry_1.getGlobalRegistry)() : { comparators: {}, weights: {}, defaultWeight: 5 };
|
|
36
|
+
const finalDefaultWeight = (_a = localDefaultWeight !== null && localDefaultWeight !== void 0 ? localDefaultWeight : global.defaultWeight) !== null && _a !== void 0 ? _a : 5;
|
|
37
|
+
const mergedWeights = Object.assign(Object.assign(Object.assign({}, global.weights), DEFAULT_WEIGHTS), localWeights);
|
|
38
|
+
const mergedComparators = Object.assign(Object.assign({}, global.comparators), localComparators);
|
|
39
|
+
const defaultComparator = (a, b) => Number(a === b);
|
|
40
|
+
const getComparator = (path) => { var _a; return (_a = mergedComparators[path]) !== null && _a !== void 0 ? _a : defaultComparator; };
|
|
41
|
+
const getWeight = (path) => { var _a; return (_a = mergedWeights[path]) !== null && _a !== void 0 ? _a : finalDefaultWeight; };
|
|
42
|
+
function compareRecursive(data1, data2, path = "", depth = 0) {
|
|
43
|
+
if (depth > maxDepth)
|
|
44
|
+
return { totalWeight: 0, matchedWeight: 0 };
|
|
45
|
+
if (data1 === undefined || data2 === undefined)
|
|
46
|
+
return { totalWeight: 0, matchedWeight: 0 };
|
|
47
|
+
// Leaf / primitive value
|
|
48
|
+
if (typeof data1 !== "object" || data1 === null || typeof data2 !== "object" || data2 === null) {
|
|
49
|
+
const comparator = getComparator(path);
|
|
50
|
+
const similarity = Math.max(0, Math.min(1, comparator(data1, data2, path)));
|
|
51
|
+
const weight = getWeight(path);
|
|
52
|
+
return { totalWeight: weight, matchedWeight: weight * similarity };
|
|
53
|
+
}
|
|
54
|
+
// Array
|
|
55
|
+
if (Array.isArray(data1) && Array.isArray(data2)) {
|
|
56
|
+
let total = 0;
|
|
57
|
+
let matched = 0;
|
|
58
|
+
const len = Math.min(data1.length, data2.length);
|
|
59
|
+
for (let i = 0; i < len; i++) {
|
|
60
|
+
const res = compareRecursive(data1[i], data2[i], `${path}[${i}]`, depth + 1);
|
|
61
|
+
total += res.totalWeight;
|
|
62
|
+
matched += res.matchedWeight;
|
|
63
|
+
}
|
|
64
|
+
return { totalWeight: total, matchedWeight: matched };
|
|
65
|
+
}
|
|
66
|
+
// Object
|
|
67
|
+
let totalWeight = 0;
|
|
68
|
+
let matchedWeight = 0;
|
|
69
|
+
const keys = new Set([...Object.keys(data1 || {}), ...Object.keys(data2 || {})]);
|
|
70
|
+
for (const key of keys) {
|
|
71
|
+
const newPath = path ? `${path}.${key}` : key;
|
|
72
|
+
const res = compareRecursive(data1 === null || data1 === void 0 ? void 0 : data1[key], data2 === null || data2 === void 0 ? void 0 : data2[key], newPath, depth + 1);
|
|
73
|
+
totalWeight += res.totalWeight;
|
|
74
|
+
matchedWeight += res.matchedWeight;
|
|
75
|
+
}
|
|
76
|
+
return { totalWeight, matchedWeight };
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
calculateConfidence(data1, data2) {
|
|
80
|
+
try {
|
|
81
|
+
const { totalWeight, matchedWeight } = compareRecursive(data1, data2);
|
|
82
|
+
const structuralScore = totalWeight > 0 ? matchedWeight / totalWeight : 0;
|
|
83
|
+
// TLSH fuzzy component (kept exactly as before)
|
|
84
|
+
let tlshScore = 1;
|
|
85
|
+
if (tlshWeight > 0) {
|
|
86
|
+
const hash1 = (0, tlsh_1.getHash)(JSON.stringify(data1));
|
|
87
|
+
const hash2 = (0, tlsh_1.getHash)(JSON.stringify(data2));
|
|
88
|
+
const diff = (0, tlsh_1.compareHashes)(hash1, hash2);
|
|
89
|
+
tlshScore = Math.max(0, (100 - diff) / 100);
|
|
90
|
+
}
|
|
91
|
+
const finalScore = structuralScore * (1 - tlshWeight) + tlshScore * tlshWeight;
|
|
92
|
+
return Math.round(Math.max(0, Math.min(100, finalScore * 100)));
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.error("Error calculating confidence:", error);
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
exports.calculateConfidence = createConfidenceCalculator().calculateConfidence;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerComparator = registerComparator;
|
|
4
|
+
exports.registerWeight = registerWeight;
|
|
5
|
+
exports.registerPlugin = registerPlugin;
|
|
6
|
+
exports.setDefaultWeight = setDefaultWeight;
|
|
7
|
+
exports.unregisterComparator = unregisterComparator;
|
|
8
|
+
exports.unregisterWeight = unregisterWeight;
|
|
9
|
+
exports.clearRegistry = clearRegistry;
|
|
10
|
+
exports.getGlobalRegistry = getGlobalRegistry;
|
|
11
|
+
let registry = {
|
|
12
|
+
comparators: {},
|
|
13
|
+
weights: {},
|
|
14
|
+
defaultWeight: 5,
|
|
15
|
+
};
|
|
16
|
+
/** Register a custom similarity comparator for a field or nested path */
|
|
17
|
+
function registerComparator(path, comparator) {
|
|
18
|
+
if (typeof comparator !== "function") {
|
|
19
|
+
throw new Error("Comparator must be a function returning a 0–1 similarity score");
|
|
20
|
+
}
|
|
21
|
+
registry.comparators[path] = comparator;
|
|
22
|
+
}
|
|
23
|
+
/** Register (or override) the weight for a field or nested path */
|
|
24
|
+
function registerWeight(path, weight) {
|
|
25
|
+
if (typeof weight !== "number" || weight < 0) {
|
|
26
|
+
throw new Error("Weight must be a non-negative number");
|
|
27
|
+
}
|
|
28
|
+
registry.weights[path] = weight;
|
|
29
|
+
}
|
|
30
|
+
/** Convenience: register weight + comparator in one call (most common pattern) */
|
|
31
|
+
function registerPlugin(path, config) {
|
|
32
|
+
if (config.weight !== undefined)
|
|
33
|
+
registerWeight(path, config.weight);
|
|
34
|
+
if (config.comparator !== undefined)
|
|
35
|
+
registerComparator(path, config.comparator);
|
|
36
|
+
}
|
|
37
|
+
/** Change the fallback weight for any unregistered field */
|
|
38
|
+
function setDefaultWeight(weight) {
|
|
39
|
+
registry.defaultWeight = Math.max(0, weight);
|
|
40
|
+
}
|
|
41
|
+
/** Remove a registered comparator */
|
|
42
|
+
function unregisterComparator(path) {
|
|
43
|
+
return delete registry.comparators[path];
|
|
44
|
+
}
|
|
45
|
+
/** Remove a registered weight */
|
|
46
|
+
function unregisterWeight(path) {
|
|
47
|
+
return delete registry.weights[path];
|
|
48
|
+
}
|
|
49
|
+
/** Reset everything (perfect for tests) */
|
|
50
|
+
function clearRegistry() {
|
|
51
|
+
registry = { comparators: {}, weights: {}, defaultWeight: 5 };
|
|
52
|
+
}
|
|
53
|
+
// Internal only – used by createConfidenceCalculator
|
|
54
|
+
function getGlobalRegistry() {
|
|
55
|
+
return Object.assign(Object.assign({}, registry), { comparators: Object.assign({}, registry.comparators), weights: Object.assign({}, registry.weights) });
|
|
56
|
+
}
|
|
@@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.getHash = getHash;
|
|
7
|
+
exports.compareHashes = compareHashes;
|
|
7
8
|
const tlsh_1 = __importDefault(require("tlsh"));
|
|
8
9
|
const digest_hash_builder_js_1 = __importDefault(require("tlsh/lib/digests/digest-hash-builder.js"));
|
|
9
10
|
function getHash(data) {
|
|
@@ -14,10 +15,8 @@ function getHash(data) {
|
|
|
14
15
|
// Return the hash as a string
|
|
15
16
|
return tlshHash;
|
|
16
17
|
}
|
|
17
|
-
exports.getHash = getHash;
|
|
18
18
|
function compareHashes(hash1, hash2) {
|
|
19
19
|
const digest1 = (0, digest_hash_builder_js_1.default)().withHash(hash1).build();
|
|
20
20
|
const digest2 = (0, digest_hash_builder_js_1.default)().withHash(hash2).build();
|
|
21
21
|
return digest1.calculateDifference(digest2, true);
|
|
22
22
|
}
|
|
23
|
-
exports.compareHashes = compareHashes;
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.clearRegistry = exports.setDefaultWeight = exports.unregisterWeight = exports.unregisterComparator = exports.registerPlugin = exports.registerWeight = exports.registerComparator = exports.createConfidenceCalculator = exports.calculateConfidence = void 0;
|
|
4
|
+
const confidence_1 = require("./libs/confidence");
|
|
5
|
+
Object.defineProperty(exports, "calculateConfidence", { enumerable: true, get: function () { return confidence_1.calculateConfidence; } });
|
|
6
|
+
Object.defineProperty(exports, "createConfidenceCalculator", { enumerable: true, get: function () { return confidence_1.createConfidenceCalculator; } });
|
|
7
|
+
const registry_1 = require("./libs/registry");
|
|
8
|
+
Object.defineProperty(exports, "registerComparator", { enumerable: true, get: function () { return registry_1.registerComparator; } });
|
|
9
|
+
Object.defineProperty(exports, "registerWeight", { enumerable: true, get: function () { return registry_1.registerWeight; } });
|
|
10
|
+
Object.defineProperty(exports, "registerPlugin", { enumerable: true, get: function () { return registry_1.registerPlugin; } });
|
|
11
|
+
Object.defineProperty(exports, "unregisterComparator", { enumerable: true, get: function () { return registry_1.unregisterComparator; } });
|
|
12
|
+
Object.defineProperty(exports, "unregisterWeight", { enumerable: true, get: function () { return registry_1.unregisterWeight; } });
|
|
13
|
+
Object.defineProperty(exports, "setDefaultWeight", { enumerable: true, get: function () { return registry_1.setDefaultWeight; } });
|
|
14
|
+
Object.defineProperty(exports, "clearRegistry", { enumerable: true, get: function () { return registry_1.clearRegistry; } });
|
package/license.txt
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
# DON'T BE A DICK PUBLIC LICENSE
|
|
2
|
-
|
|
3
|
-
> Version 1.1, December 2016
|
|
4
|
-
|
|
5
|
-
> Copyright (C) 2025 Gateway Corporate Solutions LLC
|
|
6
|
-
|
|
7
|
-
Everyone is permitted to copy and distribute verbatim or modified
|
|
8
|
-
copies of this license document.
|
|
9
|
-
|
|
10
|
-
> DON'T BE A DICK PUBLIC LICENSE
|
|
11
|
-
> TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
12
|
-
|
|
13
|
-
1. Do whatever you like with the original work, just don't be a dick.
|
|
14
|
-
|
|
15
|
-
Being a dick includes - but is not limited to - the following instances:
|
|
16
|
-
|
|
17
|
-
1a. Outright copyright infringement - Don't just copy this and change the name.
|
|
18
|
-
1b. Selling the unmodified original with no work done what-so-ever, that's REALLY being a dick.
|
|
19
|
-
1c. Modifying the original work to contain hidden harmful content. That would make you a PROPER dick.
|
|
20
|
-
|
|
21
|
-
2. If you become rich through modifications, related works/services, or supporting the original work,
|
|
22
|
-
share the love. Only a dick would make loads off this work and not buy the original work's
|
|
23
|
-
creator(s) a pint.
|
|
24
|
-
|
|
25
|
-
3. Code is provided with no warranty. Using somebody else's code and bitching when it goes wrong makes
|
|
26
|
-
you a DONKEY dick. Fix the problem yourself. A non-dick would submit the fix back.
|
|
1
|
+
# DON'T BE A DICK PUBLIC LICENSE
|
|
2
|
+
|
|
3
|
+
> Version 1.1, December 2016
|
|
4
|
+
|
|
5
|
+
> Copyright (C) 2025 Gateway Corporate Solutions LLC
|
|
6
|
+
|
|
7
|
+
Everyone is permitted to copy and distribute verbatim or modified
|
|
8
|
+
copies of this license document.
|
|
9
|
+
|
|
10
|
+
> DON'T BE A DICK PUBLIC LICENSE
|
|
11
|
+
> TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
12
|
+
|
|
13
|
+
1. Do whatever you like with the original work, just don't be a dick.
|
|
14
|
+
|
|
15
|
+
Being a dick includes - but is not limited to - the following instances:
|
|
16
|
+
|
|
17
|
+
1a. Outright copyright infringement - Don't just copy this and change the name.
|
|
18
|
+
1b. Selling the unmodified original with no work done what-so-ever, that's REALLY being a dick.
|
|
19
|
+
1c. Modifying the original work to contain hidden harmful content. That would make you a PROPER dick.
|
|
20
|
+
|
|
21
|
+
2. If you become rich through modifications, related works/services, or supporting the original work,
|
|
22
|
+
share the love. Only a dick would make loads off this work and not buy the original work's
|
|
23
|
+
creator(s) a pint.
|
|
24
|
+
|
|
25
|
+
3. Code is provided with no warranty. Using somebody else's code and bitching when it goes wrong makes
|
|
26
|
+
you a DONKEY dick. Fix the problem yourself. A non-dick would submit the fix back.
|
|
27
27
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devicer.js",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Open-Source Digital Fingerprinting Middleware",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "dist/main.js",
|
|
6
|
+
"types": "dist/main.d.ts",
|
|
6
7
|
"scripts": {
|
|
7
8
|
"test": "npx vitest",
|
|
8
|
-
"build": "tsc"
|
|
9
|
+
"build": "tsc --outDir dist"
|
|
9
10
|
},
|
|
10
11
|
"repository": {
|
|
11
12
|
"type": "git",
|
|
@@ -24,7 +25,11 @@
|
|
|
24
25
|
},
|
|
25
26
|
"homepage": "https://github.com/gatewaycorporate/fp-devicer#readme",
|
|
26
27
|
"dependencies": {
|
|
27
|
-
"tlsh": "^1.0.8"
|
|
28
|
+
"tlsh": "^1.0.8"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^18.15.11",
|
|
32
|
+
"typescript": "^5.0.3",
|
|
28
33
|
"vitest": "^3.2.3"
|
|
29
34
|
}
|
|
30
35
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function levenshteinSimilarity(a: string, b: string): number {
|
|
2
|
+
if (a === b) return 1;
|
|
3
|
+
if (!a || !b) return 0;
|
|
4
|
+
const maxLen = Math.max(a.length, b.length);
|
|
5
|
+
let distance = Math.abs(a.length - b.length);
|
|
6
|
+
const minLen = Math.min(a.length, b.length);
|
|
7
|
+
for (let i = 0; i < minLen; i++) {
|
|
8
|
+
if (a[i] !== b[i]) distance++;
|
|
9
|
+
}
|
|
10
|
+
return Math.max(0, 1 - distance / maxLen);
|
|
11
|
+
}
|
package/src/libs/confidence.ts
CHANGED
|
@@ -1,143 +1,123 @@
|
|
|
1
|
-
import { compareHashes, getHash } from "./tlsh";
|
|
2
|
-
import {
|
|
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
|
-
if (
|
|
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
|
-
// Calculate the hash for each user data
|
|
126
|
-
const hash1 = getHash(JSON.stringify(data1));
|
|
127
|
-
const hash2 = getHash(JSON.stringify(data2));
|
|
128
|
-
|
|
129
|
-
// Compare the hashes to get their difference
|
|
130
|
-
const differenceScore = compareHashes(hash1, hash2);
|
|
131
|
-
|
|
132
|
-
const inverseMatchScore = 1 - (matches / fields);
|
|
133
|
-
const x = 1.3 * differenceScore * inverseMatchScore;
|
|
134
|
-
if (inverseMatchScore === 0 || differenceScore === 0) {
|
|
135
|
-
return 100;
|
|
136
|
-
}
|
|
137
|
-
const confidenceScore = 100 / (1 + Math.E ** (-4.5 + (0.3 * x)));
|
|
138
|
-
return confidenceScore;
|
|
139
|
-
} catch (error) {
|
|
140
|
-
console.error("Error calculating confidence:", error);
|
|
141
|
-
return 0; // Return 0 if an error occurs during comparison
|
|
142
|
-
}
|
|
143
|
-
}
|
|
1
|
+
import { compareHashes, getHash } from "./tlsh";
|
|
2
|
+
import { getGlobalRegistry } from "./registry";
|
|
3
|
+
import { FPDataSet, ComparisonOptions, Comparator } from "../types/data";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_WEIGHTS: Record<string, number> = {
|
|
6
|
+
userAgent: 20,
|
|
7
|
+
platform: 15,
|
|
8
|
+
timezone: 10,
|
|
9
|
+
language: 10,
|
|
10
|
+
languages: 10,
|
|
11
|
+
cookieEnabled: 5,
|
|
12
|
+
doNotTrack: 5,
|
|
13
|
+
hardwareConcurrency: 5,
|
|
14
|
+
deviceMemory: 5,
|
|
15
|
+
product: 5,
|
|
16
|
+
productSub: 5,
|
|
17
|
+
vendor: 5,
|
|
18
|
+
vendorSub: 5,
|
|
19
|
+
appName: 5,
|
|
20
|
+
appVersion: 5,
|
|
21
|
+
appCodeName: 5,
|
|
22
|
+
appMinorVersion: 5,
|
|
23
|
+
buildID: 5,
|
|
24
|
+
plugins: 10,
|
|
25
|
+
mimeTypes: 10,
|
|
26
|
+
screen: 10,
|
|
27
|
+
fonts: 15
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function createConfidenceCalculator(userOptions: ComparisonOptions = {}) {
|
|
31
|
+
const {
|
|
32
|
+
weights: localWeights = {},
|
|
33
|
+
comparators: localComparators = {},
|
|
34
|
+
defaultWeight: localDefaultWeight = 5,
|
|
35
|
+
tlshWeight = 0.30,
|
|
36
|
+
maxDepth = 5,
|
|
37
|
+
useGlobalRegistry = true,
|
|
38
|
+
} = userOptions;
|
|
39
|
+
|
|
40
|
+
// Merge global registry (if enabled) → local always wins
|
|
41
|
+
const global = useGlobalRegistry ? getGlobalRegistry() : { comparators: {}, weights: {}, defaultWeight: 5 };
|
|
42
|
+
|
|
43
|
+
const finalDefaultWeight = localDefaultWeight ?? global.defaultWeight ?? 5;
|
|
44
|
+
const mergedWeights = { ...global.weights, ...DEFAULT_WEIGHTS, ...localWeights };
|
|
45
|
+
const mergedComparators = { ...global.comparators, ...localComparators };
|
|
46
|
+
|
|
47
|
+
const defaultComparator: Comparator = (a, b) => Number(a === b);
|
|
48
|
+
|
|
49
|
+
const getComparator = (path: string): Comparator => mergedComparators[path] ?? defaultComparator;
|
|
50
|
+
|
|
51
|
+
const getWeight = (path: string): number => mergedWeights[path] ?? finalDefaultWeight;
|
|
52
|
+
|
|
53
|
+
function compareRecursive(
|
|
54
|
+
data1: any,
|
|
55
|
+
data2: any,
|
|
56
|
+
path = "",
|
|
57
|
+
depth = 0
|
|
58
|
+
): { totalWeight: number; matchedWeight: number } {
|
|
59
|
+
if (depth > maxDepth) return { totalWeight: 0, matchedWeight: 0 };
|
|
60
|
+
if (data1 === undefined || data2 === undefined) return { totalWeight: 0, matchedWeight: 0 };
|
|
61
|
+
|
|
62
|
+
// Leaf / primitive value
|
|
63
|
+
if (typeof data1 !== "object" || data1 === null || typeof data2 !== "object" || data2 === null) {
|
|
64
|
+
const comparator = getComparator(path);
|
|
65
|
+
const similarity = Math.max(0, Math.min(1, comparator(data1, data2, path)));
|
|
66
|
+
const weight = getWeight(path);
|
|
67
|
+
return { totalWeight: weight, matchedWeight: weight * similarity };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Array
|
|
71
|
+
if (Array.isArray(data1) && Array.isArray(data2)) {
|
|
72
|
+
let total = 0;
|
|
73
|
+
let matched = 0;
|
|
74
|
+
const len = Math.min(data1.length, data2.length);
|
|
75
|
+
for (let i = 0; i < len; i++) {
|
|
76
|
+
const res = compareRecursive(data1[i], data2[i], `${path}[${i}]`, depth + 1);
|
|
77
|
+
total += res.totalWeight;
|
|
78
|
+
matched += res.matchedWeight;
|
|
79
|
+
}
|
|
80
|
+
return { totalWeight: total, matchedWeight: matched };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Object
|
|
84
|
+
let totalWeight = 0;
|
|
85
|
+
let matchedWeight = 0;
|
|
86
|
+
const keys = new Set([...Object.keys(data1 || {}), ...Object.keys(data2 || {})]);
|
|
87
|
+
|
|
88
|
+
for (const key of keys) {
|
|
89
|
+
const newPath = path ? `${path}.${key}` : key;
|
|
90
|
+
const res = compareRecursive(data1?.[key], data2?.[key], newPath, depth + 1);
|
|
91
|
+
totalWeight += res.totalWeight;
|
|
92
|
+
matchedWeight += res.matchedWeight;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { totalWeight, matchedWeight };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
calculateConfidence(data1: FPDataSet, data2: FPDataSet): number {
|
|
100
|
+
try {
|
|
101
|
+
const { totalWeight, matchedWeight } = compareRecursive(data1, data2);
|
|
102
|
+
const structuralScore = totalWeight > 0 ? matchedWeight / totalWeight : 0;
|
|
103
|
+
|
|
104
|
+
// TLSH fuzzy component (kept exactly as before)
|
|
105
|
+
let tlshScore = 1;
|
|
106
|
+
if (tlshWeight > 0) {
|
|
107
|
+
const hash1 = getHash(JSON.stringify(data1));
|
|
108
|
+
const hash2 = getHash(JSON.stringify(data2));
|
|
109
|
+
const diff = compareHashes(hash1, hash2);
|
|
110
|
+
tlshScore = Math.max(0, (100 - diff) / 100);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const finalScore = structuralScore * (1 - tlshWeight) + tlshScore * tlshWeight;
|
|
114
|
+
return Math.round(Math.max(0, Math.min(100, finalScore * 100)));
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error("Error calculating confidence:", error);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const calculateConfidence = createConfidenceCalculator().calculateConfidence;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { registerPlugin } from "./registry";
|
|
2
|
+
import type { Comparator } from "../types/data";
|
|
3
|
+
import { levenshteinSimilarity } from "./comparitors";
|
|
4
|
+
|
|
5
|
+
const BUILT_IN_PLUGINS = [
|
|
6
|
+
{
|
|
7
|
+
path: "userAgent",
|
|
8
|
+
weight: 20,
|
|
9
|
+
comparator: (a: any, b: any) => levenshteinSimilarity(String(a || "").toLowerCase(), String(b || "").toLowerCase())
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
path: "platform",
|
|
13
|
+
weight: 15,
|
|
14
|
+
comparator: (a: any, b: any) => levenshteinSimilarity(String(a || "").toLowerCase(), String(b || "").toLowerCase())
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
export function initializeDefaultRegistry() {
|
|
19
|
+
for (const plugin of BUILT_IN_PLUGINS) {
|
|
20
|
+
registerPlugin(plugin.path, { weight: plugin.weight, comparator: plugin.comparator as Comparator });
|
|
21
|
+
}
|
|
22
|
+
}
|