devicer.js 1.1.0 → 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,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 { FPUserDataSet as UserData, calculateConfidence } from "devicer.js";
9
-
10
- const user1: UserData = {
11
- // Collected data goes here
12
- }
13
-
14
- const user2: UserData = {
15
- // Collected data goes here
16
- }
17
-
18
- const confidence = calculateConfidence(user1, user2);
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.compareHashes = exports.getHash = void 0;
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.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Open-Source Digital Fingerprinting Middleware",
5
- "main": "src/main.js",
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
+ }
@@ -1,139 +1,123 @@
1
- import { compareHashes, getHash } from "./tlsh";
2
- import { FPDataSet } from "../types/data";
3
-
4
- export function compareArrays(
5
- arr1: any[],
6
- arr2: any[],
7
- max_depth: number = 5,
8
- ): [number, number] {
9
- let fields = 0;
10
- let matches = 0;
11
-
12
- // Ensure max_depth is not exceeded
13
- if (max_depth <= 0) {
14
- console.warn("Max depth exceeded in compareArrays");
15
- return [0, 0]; // Return 0 fields and matches if max depth is exceeded
16
- }
17
-
18
- // Sort arrays to ensure consistent comparison
19
- const sortedArr1 = arr1.map((item) => JSON.stringify(item)).sort().map(
20
- (item) => {
21
- try {
22
- return JSON.parse(item);
23
- } catch (e) {
24
- return undefined;
25
- }
26
- },
27
- );
28
- const sortedArr2 = arr2.map((item) => JSON.stringify(item)).sort().map(
29
- (item) => {
30
- try {
31
- return JSON.parse(item);
32
- } catch (e) {
33
- return undefined;
34
- }
35
- },
36
- );
37
-
38
- const maxLength = Math.min(arr1.length, arr2.length);
39
- for (let i = 0; i < maxLength; i++) {
40
- fields++;
41
- if (Array.isArray(sortedArr1[i]) && Array.isArray(sortedArr2[i])) {
42
- const subData = compareArrays(
43
- sortedArr1[i],
44
- sortedArr2[i],
45
- max_depth - 1,
46
- );
47
- fields += subData[0] - 1; // Subtract 1 for the index itself
48
- matches += subData[1];
49
- } else if (
50
- (typeof sortedArr1[i] == "object" && sortedArr1[i]) &&
51
- (typeof sortedArr2[i] == "object" && sortedArr2[i])
52
- ) {
53
- const subData = compareDatasets(
54
- sortedArr1[i] as FPDataSet,
55
- sortedArr2[i] as FPDataSet,
56
- max_depth - 1,
57
- );
58
- fields += subData[0] - 1; // Subtract 1 for the index itself
59
- matches += subData[1];
60
- }
61
-
62
- if (sortedArr2.includes(sortedArr1[i]) && sortedArr1[i]) {
63
- matches++;
64
- }
65
- }
66
- return [fields, matches];
67
- }
68
-
69
- export function compareDatasets(
70
- data1: FPDataSet,
71
- data2: FPDataSet,
72
- max_depth: number = 5,
73
- ): [number, number] {
74
- let fields = 0;
75
- let matches = 0;
76
-
77
- // Ensure max_depth is not exceeded
78
- if (max_depth <= 0) {
79
- console.warn("Max depth exceeded in compareDatasets");
80
- return [0, 0]; // Return 0 fields and matches if max depth is exceeded
81
- }
82
-
83
- for (const key in data1) {
84
- if (data1[key] !== undefined && data2[key] !== undefined) {
85
- fields++;
86
- if (Array.isArray(data1[key]) && Array.isArray(data2[key])) {
87
- const subData = compareArrays(data1[key], data2[key], max_depth - 1);
88
- fields += subData[0] - 1; // Subtract 1 for the key itself
89
- matches += subData[1];
90
- } else if (
91
- (typeof data1[key] == "object" && data1[key]) &&
92
- (typeof data2[key] == "object" && data2[key])
93
- ) {
94
- const subData = compareDatasets(
95
- data1[key] as FPDataSet,
96
- data2[key] as FPDataSet,
97
- max_depth - 1,
98
- );
99
- fields += subData[0] - 1; // Subtract 1 for the key itself
100
- matches += subData[1];
101
- } else if (data1[key] == data2[key]) {
102
- matches++;
103
- }
104
- }
105
- }
106
- return [fields, matches];
107
- }
108
-
109
- export function calculateConfidence(
110
- data1: FPDataSet,
111
- data2: FPDataSet,
112
- ): number {
113
- try {
114
- // Compare how many fields are the same in both datasets
115
- const [fields, matches] = compareDatasets(data1, data2);
116
-
117
- if (fields === 0 || matches === 0) {
118
- return 0;
119
- }
120
-
121
- // Calculate the hash for each user data
122
- const hash1 = getHash(JSON.stringify(data1));
123
- const hash2 = getHash(JSON.stringify(data2));
124
-
125
- // Compare the hashes to get their difference
126
- const differenceScore = compareHashes(hash1, hash2);
127
-
128
- const inverseMatchScore = 1 - (matches / fields);
129
- const x = 1.3 * differenceScore * inverseMatchScore;
130
- if (inverseMatchScore === 0 || differenceScore === 0) {
131
- return 100;
132
- }
133
- const confidenceScore = 100 / (1 + Math.E ** (-4.5 + (0.3 * x)));
134
- return confidenceScore;
135
- } catch (error) {
136
- console.error("Error calculating confidence:", error);
137
- return 0; // Return 0 if an error occurs during comparison
138
- }
139
- }
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
+ }