@mack1ch/fingerprint-js 0.1.0 → 0.1.1
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/dist/index.cjs +160 -7
- package/dist/index.d.cts +18 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +158 -7
- package/package.json +51 -1
package/dist/index.cjs
CHANGED
|
@@ -19,6 +19,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
// src/fingerprint/index.ts
|
|
20
20
|
var index_exports = {};
|
|
21
21
|
__export(index_exports, {
|
|
22
|
+
buildIdentityId: () => buildIdentityId,
|
|
23
|
+
compareFingerprints: () => compareFingerprints,
|
|
22
24
|
getFingerprint: () => getFingerprint,
|
|
23
25
|
getFingerprintQuick: () => getFingerprintQuick,
|
|
24
26
|
resolveFingerprintPerformance: () => resolveFingerprintPerformance
|
|
@@ -863,11 +865,71 @@ function normalizeValue(value) {
|
|
|
863
865
|
function toSortedEntries(map) {
|
|
864
866
|
return Object.entries(map).sort(([left], [right]) => left.localeCompare(right));
|
|
865
867
|
}
|
|
868
|
+
var STABLE_DEVICE_KEYS = /* @__PURE__ */ new Set([
|
|
869
|
+
"canvasHash",
|
|
870
|
+
"audioFingerprint",
|
|
871
|
+
"fontsDetected",
|
|
872
|
+
"fontsList",
|
|
873
|
+
"unmaskedVendor",
|
|
874
|
+
"unmaskedRenderer",
|
|
875
|
+
"hardwareConcurrency",
|
|
876
|
+
"deviceMemory",
|
|
877
|
+
"platform",
|
|
878
|
+
"timeZone",
|
|
879
|
+
"colorDepth",
|
|
880
|
+
"pixelDepth",
|
|
881
|
+
"sampleRate"
|
|
882
|
+
]);
|
|
883
|
+
var VOLATILE_DEVICE_KEYS = /* @__PURE__ */ new Set([
|
|
884
|
+
"ip",
|
|
885
|
+
"localIp",
|
|
886
|
+
"city",
|
|
887
|
+
"region",
|
|
888
|
+
"country",
|
|
889
|
+
"countryCode",
|
|
890
|
+
"postal",
|
|
891
|
+
"latitude",
|
|
892
|
+
"longitude",
|
|
893
|
+
"effectiveType",
|
|
894
|
+
"downlink",
|
|
895
|
+
"rtt",
|
|
896
|
+
"saveData",
|
|
897
|
+
"battery",
|
|
898
|
+
"level",
|
|
899
|
+
"charging",
|
|
900
|
+
"chargingTime",
|
|
901
|
+
"dischargingTime",
|
|
902
|
+
"timezoneVsIp",
|
|
903
|
+
"innerWidth",
|
|
904
|
+
"innerHeight",
|
|
905
|
+
"outerWidth",
|
|
906
|
+
"outerHeight",
|
|
907
|
+
"screenX",
|
|
908
|
+
"screenY",
|
|
909
|
+
"screenLeft",
|
|
910
|
+
"screenTop"
|
|
911
|
+
]);
|
|
912
|
+
function getSignalStability(key) {
|
|
913
|
+
if (STABLE_DEVICE_KEYS.has(key)) {
|
|
914
|
+
return "stable";
|
|
915
|
+
}
|
|
916
|
+
if (VOLATILE_DEVICE_KEYS.has(key)) {
|
|
917
|
+
return "volatile";
|
|
918
|
+
}
|
|
919
|
+
return "semi";
|
|
920
|
+
}
|
|
921
|
+
function getDeviceWeight(key) {
|
|
922
|
+
const stability = getSignalStability(key);
|
|
923
|
+
if (stability === "stable") return 4;
|
|
924
|
+
if (stability === "semi") return 2;
|
|
925
|
+
return 1;
|
|
926
|
+
}
|
|
866
927
|
function buildComponents(input) {
|
|
867
928
|
const required = toSortedEntries(input.requiredInputs).map(([key, value]) => ({
|
|
868
929
|
key: `required.${key}`,
|
|
869
930
|
value: normalizeValue(value),
|
|
870
931
|
kind: "required",
|
|
932
|
+
stability: "stable",
|
|
871
933
|
required: true,
|
|
872
934
|
present: Boolean(value),
|
|
873
935
|
weight: 5,
|
|
@@ -877,6 +939,7 @@ function buildComponents(input) {
|
|
|
877
939
|
key: `optional.${key}`,
|
|
878
940
|
value: normalizeValue(value),
|
|
879
941
|
kind: "optional",
|
|
942
|
+
stability: "semi",
|
|
880
943
|
required: false,
|
|
881
944
|
present: Boolean(value),
|
|
882
945
|
weight: 2,
|
|
@@ -886,9 +949,10 @@ function buildComponents(input) {
|
|
|
886
949
|
key: `device.${key}`,
|
|
887
950
|
value: normalizeValue(value),
|
|
888
951
|
kind: "device",
|
|
952
|
+
stability: getSignalStability(key),
|
|
889
953
|
required: false,
|
|
890
954
|
present: value !== "\u2014",
|
|
891
|
-
weight:
|
|
955
|
+
weight: getDeviceWeight(key),
|
|
892
956
|
source: "signal"
|
|
893
957
|
}));
|
|
894
958
|
return [...required, ...optional, ...device];
|
|
@@ -896,6 +960,13 @@ function buildComponents(input) {
|
|
|
896
960
|
function toCanonicalPayload(components) {
|
|
897
961
|
return components.map((component) => `${component.key}=${component.value}`).join("|");
|
|
898
962
|
}
|
|
963
|
+
function toCoreCanonicalPayload(components) {
|
|
964
|
+
return components.filter((component) => {
|
|
965
|
+
if (component.kind === "required") return true;
|
|
966
|
+
if (component.kind === "device" && component.stability === "stable") return true;
|
|
967
|
+
return false;
|
|
968
|
+
}).map((component) => `${component.key}=${component.value}`).join("|");
|
|
969
|
+
}
|
|
899
970
|
function computeConfidenceScore(components, timedOutCollectors, failedCollectorsCount) {
|
|
900
971
|
const weightedTotal = components.reduce((total, component) => total + component.weight, 0);
|
|
901
972
|
if (weightedTotal === 0) {
|
|
@@ -944,14 +1015,18 @@ async function getFingerprint(options) {
|
|
|
944
1015
|
optionalInputs: gathered.optionalInputs,
|
|
945
1016
|
deviceSignals: gathered.deviceSignals
|
|
946
1017
|
});
|
|
947
|
-
const
|
|
1018
|
+
const corePayload = toCoreCanonicalPayload(components);
|
|
1019
|
+
const fullPayload = toCanonicalPayload(components);
|
|
948
1020
|
const digestAlgorithm = ((_a = options.hash) == null ? void 0 : _a.algorithm) ?? "SHA-256";
|
|
949
|
-
const
|
|
1021
|
+
const coreHash = await createDigest(corePayload, digestAlgorithm);
|
|
1022
|
+
const fullHash = await createDigest(fullPayload, digestAlgorithm);
|
|
950
1023
|
const completedAtMs = Date.now();
|
|
951
1024
|
const includeRaw = ((_b = options.privacy) == null ? void 0 : _b.includeRaw) ?? true;
|
|
952
1025
|
const maskedRaw = applyPrivacy(gathered.rawItems, ((_c = options.privacy) == null ? void 0 : _c.maskSensitive) ?? false);
|
|
953
1026
|
return {
|
|
954
|
-
|
|
1027
|
+
coreHash,
|
|
1028
|
+
fullHash,
|
|
1029
|
+
hash: fullHash,
|
|
955
1030
|
hashVersion: "fp_v1",
|
|
956
1031
|
components,
|
|
957
1032
|
requiredInputs: gathered.requiredInputs,
|
|
@@ -989,14 +1064,18 @@ async function getFingerprintQuick(options) {
|
|
|
989
1064
|
optionalInputs: gathered.optionalInputs,
|
|
990
1065
|
deviceSignals: gathered.deviceSignals
|
|
991
1066
|
});
|
|
992
|
-
const
|
|
1067
|
+
const corePayload = toCoreCanonicalPayload(components);
|
|
1068
|
+
const fullPayload = toCanonicalPayload(components);
|
|
993
1069
|
const digestAlgorithm = ((_a = options.hash) == null ? void 0 : _a.algorithm) ?? "SHA-256";
|
|
994
|
-
const
|
|
1070
|
+
const coreHash = await createDigest(corePayload, digestAlgorithm);
|
|
1071
|
+
const fullHash = await createDigest(fullPayload, digestAlgorithm);
|
|
995
1072
|
const completedAtMs = Date.now();
|
|
996
1073
|
const includeRaw = ((_b = options.privacy) == null ? void 0 : _b.includeRaw) ?? true;
|
|
997
1074
|
const maskedRaw = applyPrivacy(gathered.rawItems, ((_c = options.privacy) == null ? void 0 : _c.maskSensitive) ?? false);
|
|
998
1075
|
return {
|
|
999
|
-
|
|
1076
|
+
coreHash,
|
|
1077
|
+
fullHash,
|
|
1078
|
+
hash: fullHash,
|
|
1000
1079
|
hashVersion: "fp_v1",
|
|
1001
1080
|
components,
|
|
1002
1081
|
requiredInputs: gathered.requiredInputs,
|
|
@@ -1019,8 +1098,82 @@ async function getFingerprintQuick(options) {
|
|
|
1019
1098
|
function resolveFingerprintPerformance(options) {
|
|
1020
1099
|
return resolvePerformance(options);
|
|
1021
1100
|
}
|
|
1101
|
+
|
|
1102
|
+
// src/fingerprint/matcher.ts
|
|
1103
|
+
function round3(value) {
|
|
1104
|
+
return Math.max(0, Math.min(1, Number(value.toFixed(3))));
|
|
1105
|
+
}
|
|
1106
|
+
function compareFingerprints(current, reference) {
|
|
1107
|
+
let totalReferenceWeight = 0;
|
|
1108
|
+
let matchedWeight = 0;
|
|
1109
|
+
const changedStableKeys = [];
|
|
1110
|
+
const changedVolatileKeys = [];
|
|
1111
|
+
for (const refComponent of reference.components) {
|
|
1112
|
+
if (!refComponent.present) continue;
|
|
1113
|
+
totalReferenceWeight += refComponent.weight;
|
|
1114
|
+
const currentComponent = current.components.find((component) => component.key === refComponent.key);
|
|
1115
|
+
if (currentComponent && currentComponent.present && currentComponent.value === refComponent.value) {
|
|
1116
|
+
matchedWeight += refComponent.weight;
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
if (refComponent.stability === "stable") changedStableKeys.push(refComponent.key);
|
|
1120
|
+
if (refComponent.stability === "volatile") changedVolatileKeys.push(refComponent.key);
|
|
1121
|
+
}
|
|
1122
|
+
const similarityScore = totalReferenceWeight === 0 ? 0 : round3(matchedWeight / totalReferenceWeight);
|
|
1123
|
+
const reasons = [];
|
|
1124
|
+
let risk = 1 - similarityScore;
|
|
1125
|
+
if (changedStableKeys.length >= 3) {
|
|
1126
|
+
risk += 0.2;
|
|
1127
|
+
reasons.push("Multiple stable identifiers changed");
|
|
1128
|
+
} else if (changedStableKeys.length > 0) {
|
|
1129
|
+
risk += 0.1;
|
|
1130
|
+
reasons.push("At least one stable identifier changed");
|
|
1131
|
+
}
|
|
1132
|
+
if (changedVolatileKeys.length > 0) {
|
|
1133
|
+
reasons.push("Volatile network/session identifiers changed");
|
|
1134
|
+
}
|
|
1135
|
+
if (current.deviceSignals.timezoneVsIp === "\u043D\u0435 \u0441\u043E\u0432\u043F\u0430\u0434\u0430\u0435\u0442") {
|
|
1136
|
+
risk += 0.1;
|
|
1137
|
+
reasons.push("Browser timezone mismatches IP timezone");
|
|
1138
|
+
}
|
|
1139
|
+
const botSummary = (current.deviceSignals.botRiskSummary ?? "").toLowerCase();
|
|
1140
|
+
if (botSummary.includes("\u0432\u044B\u0441\u043E\u043A\u0430\u044F")) {
|
|
1141
|
+
risk += 0.25;
|
|
1142
|
+
reasons.push("Bot/automation heuristics are high");
|
|
1143
|
+
} else if (botSummary.includes("\u0441\u0440\u0435\u0434\u043D\u044F\u044F")) {
|
|
1144
|
+
risk += 0.1;
|
|
1145
|
+
reasons.push("Bot/automation heuristics are medium");
|
|
1146
|
+
}
|
|
1147
|
+
if (current.meta.confidenceScore < 0.6) {
|
|
1148
|
+
risk += 0.05;
|
|
1149
|
+
reasons.push("Fingerprint confidence is limited");
|
|
1150
|
+
}
|
|
1151
|
+
const riskScore = round3(risk);
|
|
1152
|
+
let verdict = "different_device";
|
|
1153
|
+
if (similarityScore >= 0.85 && riskScore < 0.35) verdict = "same_device";
|
|
1154
|
+
else if (similarityScore >= 0.7 && riskScore < 0.55) verdict = "likely_same";
|
|
1155
|
+
else if (similarityScore >= 0.45) verdict = "suspicious";
|
|
1156
|
+
if (!reasons.length) {
|
|
1157
|
+
reasons.push("No meaningful drift between profiles");
|
|
1158
|
+
}
|
|
1159
|
+
return {
|
|
1160
|
+
similarityScore,
|
|
1161
|
+
riskScore,
|
|
1162
|
+
verdict,
|
|
1163
|
+
matchedWeight,
|
|
1164
|
+
totalReferenceWeight,
|
|
1165
|
+
reasons,
|
|
1166
|
+
changedStableKeys,
|
|
1167
|
+
changedVolatileKeys
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function buildIdentityId(profile) {
|
|
1171
|
+
return profile.coreHash;
|
|
1172
|
+
}
|
|
1022
1173
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1023
1174
|
0 && (module.exports = {
|
|
1175
|
+
buildIdentityId,
|
|
1176
|
+
compareFingerprints,
|
|
1024
1177
|
getFingerprint,
|
|
1025
1178
|
getFingerprintQuick,
|
|
1026
1179
|
resolveFingerprintPerformance
|
package/dist/index.d.cts
CHANGED
|
@@ -32,10 +32,12 @@ type FingerprintPrivacyOptions = {
|
|
|
32
32
|
maskSensitive?: boolean;
|
|
33
33
|
};
|
|
34
34
|
type FingerprintComponentKind = 'required' | 'optional' | 'device';
|
|
35
|
+
type SignalStability = 'stable' | 'semi' | 'volatile';
|
|
35
36
|
type FingerprintComponent = {
|
|
36
37
|
key: string;
|
|
37
38
|
value: string;
|
|
38
39
|
kind: FingerprintComponentKind;
|
|
40
|
+
stability: SignalStability;
|
|
39
41
|
required: boolean;
|
|
40
42
|
present: boolean;
|
|
41
43
|
weight: number;
|
|
@@ -56,6 +58,8 @@ type FingerprintMeta = {
|
|
|
56
58
|
confidenceScore: number;
|
|
57
59
|
};
|
|
58
60
|
type FingerprintResult = {
|
|
61
|
+
coreHash: string;
|
|
62
|
+
fullHash: string;
|
|
59
63
|
hash: string;
|
|
60
64
|
hashVersion: 'fp_v1';
|
|
61
65
|
components: FingerprintComponent[];
|
|
@@ -65,6 +69,16 @@ type FingerprintResult = {
|
|
|
65
69
|
raw: UserDataItem[];
|
|
66
70
|
meta: FingerprintMeta;
|
|
67
71
|
};
|
|
72
|
+
type FingerprintComparison = {
|
|
73
|
+
similarityScore: number;
|
|
74
|
+
riskScore: number;
|
|
75
|
+
verdict: 'same_device' | 'likely_same' | 'suspicious' | 'different_device';
|
|
76
|
+
matchedWeight: number;
|
|
77
|
+
totalReferenceWeight: number;
|
|
78
|
+
reasons: string[];
|
|
79
|
+
changedStableKeys: string[];
|
|
80
|
+
changedVolatileKeys: string[];
|
|
81
|
+
};
|
|
68
82
|
type FingerprintOptions = {
|
|
69
83
|
required: Record<string, string | number | boolean | null | undefined>;
|
|
70
84
|
optional?: Record<string, string | number | boolean | null | undefined>;
|
|
@@ -86,4 +100,7 @@ declare function getFingerprint(options: FingerprintOptions): Promise<Fingerprin
|
|
|
86
100
|
declare function getFingerprintQuick(options: FingerprintQuickOptions): Promise<FingerprintResult>;
|
|
87
101
|
declare function resolveFingerprintPerformance(options?: FingerprintOptions['performance']): Required<FingerprintPerformanceOptions>;
|
|
88
102
|
|
|
89
|
-
|
|
103
|
+
declare function compareFingerprints(current: FingerprintResult, reference: FingerprintResult): FingerprintComparison;
|
|
104
|
+
declare function buildIdentityId(profile: FingerprintResult): string;
|
|
105
|
+
|
|
106
|
+
export { type DigestAlgorithm, type FingerprintCollectFlags, type FingerprintComparison, type FingerprintComponent, type FingerprintMeta, type FingerprintMode, type FingerprintOptions, type FingerprintPerformanceOptions, type FingerprintPrivacyOptions, type FingerprintQuickOptions, type FingerprintResult, buildIdentityId, compareFingerprints, getFingerprint, getFingerprintQuick, resolveFingerprintPerformance };
|
package/dist/index.d.ts
CHANGED
|
@@ -32,10 +32,12 @@ type FingerprintPrivacyOptions = {
|
|
|
32
32
|
maskSensitive?: boolean;
|
|
33
33
|
};
|
|
34
34
|
type FingerprintComponentKind = 'required' | 'optional' | 'device';
|
|
35
|
+
type SignalStability = 'stable' | 'semi' | 'volatile';
|
|
35
36
|
type FingerprintComponent = {
|
|
36
37
|
key: string;
|
|
37
38
|
value: string;
|
|
38
39
|
kind: FingerprintComponentKind;
|
|
40
|
+
stability: SignalStability;
|
|
39
41
|
required: boolean;
|
|
40
42
|
present: boolean;
|
|
41
43
|
weight: number;
|
|
@@ -56,6 +58,8 @@ type FingerprintMeta = {
|
|
|
56
58
|
confidenceScore: number;
|
|
57
59
|
};
|
|
58
60
|
type FingerprintResult = {
|
|
61
|
+
coreHash: string;
|
|
62
|
+
fullHash: string;
|
|
59
63
|
hash: string;
|
|
60
64
|
hashVersion: 'fp_v1';
|
|
61
65
|
components: FingerprintComponent[];
|
|
@@ -65,6 +69,16 @@ type FingerprintResult = {
|
|
|
65
69
|
raw: UserDataItem[];
|
|
66
70
|
meta: FingerprintMeta;
|
|
67
71
|
};
|
|
72
|
+
type FingerprintComparison = {
|
|
73
|
+
similarityScore: number;
|
|
74
|
+
riskScore: number;
|
|
75
|
+
verdict: 'same_device' | 'likely_same' | 'suspicious' | 'different_device';
|
|
76
|
+
matchedWeight: number;
|
|
77
|
+
totalReferenceWeight: number;
|
|
78
|
+
reasons: string[];
|
|
79
|
+
changedStableKeys: string[];
|
|
80
|
+
changedVolatileKeys: string[];
|
|
81
|
+
};
|
|
68
82
|
type FingerprintOptions = {
|
|
69
83
|
required: Record<string, string | number | boolean | null | undefined>;
|
|
70
84
|
optional?: Record<string, string | number | boolean | null | undefined>;
|
|
@@ -86,4 +100,7 @@ declare function getFingerprint(options: FingerprintOptions): Promise<Fingerprin
|
|
|
86
100
|
declare function getFingerprintQuick(options: FingerprintQuickOptions): Promise<FingerprintResult>;
|
|
87
101
|
declare function resolveFingerprintPerformance(options?: FingerprintOptions['performance']): Required<FingerprintPerformanceOptions>;
|
|
88
102
|
|
|
89
|
-
|
|
103
|
+
declare function compareFingerprints(current: FingerprintResult, reference: FingerprintResult): FingerprintComparison;
|
|
104
|
+
declare function buildIdentityId(profile: FingerprintResult): string;
|
|
105
|
+
|
|
106
|
+
export { type DigestAlgorithm, type FingerprintCollectFlags, type FingerprintComparison, type FingerprintComponent, type FingerprintMeta, type FingerprintMode, type FingerprintOptions, type FingerprintPerformanceOptions, type FingerprintPrivacyOptions, type FingerprintQuickOptions, type FingerprintResult, buildIdentityId, compareFingerprints, getFingerprint, getFingerprintQuick, resolveFingerprintPerformance };
|
package/dist/index.js
CHANGED
|
@@ -836,11 +836,71 @@ function normalizeValue(value) {
|
|
|
836
836
|
function toSortedEntries(map) {
|
|
837
837
|
return Object.entries(map).sort(([left], [right]) => left.localeCompare(right));
|
|
838
838
|
}
|
|
839
|
+
var STABLE_DEVICE_KEYS = /* @__PURE__ */ new Set([
|
|
840
|
+
"canvasHash",
|
|
841
|
+
"audioFingerprint",
|
|
842
|
+
"fontsDetected",
|
|
843
|
+
"fontsList",
|
|
844
|
+
"unmaskedVendor",
|
|
845
|
+
"unmaskedRenderer",
|
|
846
|
+
"hardwareConcurrency",
|
|
847
|
+
"deviceMemory",
|
|
848
|
+
"platform",
|
|
849
|
+
"timeZone",
|
|
850
|
+
"colorDepth",
|
|
851
|
+
"pixelDepth",
|
|
852
|
+
"sampleRate"
|
|
853
|
+
]);
|
|
854
|
+
var VOLATILE_DEVICE_KEYS = /* @__PURE__ */ new Set([
|
|
855
|
+
"ip",
|
|
856
|
+
"localIp",
|
|
857
|
+
"city",
|
|
858
|
+
"region",
|
|
859
|
+
"country",
|
|
860
|
+
"countryCode",
|
|
861
|
+
"postal",
|
|
862
|
+
"latitude",
|
|
863
|
+
"longitude",
|
|
864
|
+
"effectiveType",
|
|
865
|
+
"downlink",
|
|
866
|
+
"rtt",
|
|
867
|
+
"saveData",
|
|
868
|
+
"battery",
|
|
869
|
+
"level",
|
|
870
|
+
"charging",
|
|
871
|
+
"chargingTime",
|
|
872
|
+
"dischargingTime",
|
|
873
|
+
"timezoneVsIp",
|
|
874
|
+
"innerWidth",
|
|
875
|
+
"innerHeight",
|
|
876
|
+
"outerWidth",
|
|
877
|
+
"outerHeight",
|
|
878
|
+
"screenX",
|
|
879
|
+
"screenY",
|
|
880
|
+
"screenLeft",
|
|
881
|
+
"screenTop"
|
|
882
|
+
]);
|
|
883
|
+
function getSignalStability(key) {
|
|
884
|
+
if (STABLE_DEVICE_KEYS.has(key)) {
|
|
885
|
+
return "stable";
|
|
886
|
+
}
|
|
887
|
+
if (VOLATILE_DEVICE_KEYS.has(key)) {
|
|
888
|
+
return "volatile";
|
|
889
|
+
}
|
|
890
|
+
return "semi";
|
|
891
|
+
}
|
|
892
|
+
function getDeviceWeight(key) {
|
|
893
|
+
const stability = getSignalStability(key);
|
|
894
|
+
if (stability === "stable") return 4;
|
|
895
|
+
if (stability === "semi") return 2;
|
|
896
|
+
return 1;
|
|
897
|
+
}
|
|
839
898
|
function buildComponents(input) {
|
|
840
899
|
const required = toSortedEntries(input.requiredInputs).map(([key, value]) => ({
|
|
841
900
|
key: `required.${key}`,
|
|
842
901
|
value: normalizeValue(value),
|
|
843
902
|
kind: "required",
|
|
903
|
+
stability: "stable",
|
|
844
904
|
required: true,
|
|
845
905
|
present: Boolean(value),
|
|
846
906
|
weight: 5,
|
|
@@ -850,6 +910,7 @@ function buildComponents(input) {
|
|
|
850
910
|
key: `optional.${key}`,
|
|
851
911
|
value: normalizeValue(value),
|
|
852
912
|
kind: "optional",
|
|
913
|
+
stability: "semi",
|
|
853
914
|
required: false,
|
|
854
915
|
present: Boolean(value),
|
|
855
916
|
weight: 2,
|
|
@@ -859,9 +920,10 @@ function buildComponents(input) {
|
|
|
859
920
|
key: `device.${key}`,
|
|
860
921
|
value: normalizeValue(value),
|
|
861
922
|
kind: "device",
|
|
923
|
+
stability: getSignalStability(key),
|
|
862
924
|
required: false,
|
|
863
925
|
present: value !== "\u2014",
|
|
864
|
-
weight:
|
|
926
|
+
weight: getDeviceWeight(key),
|
|
865
927
|
source: "signal"
|
|
866
928
|
}));
|
|
867
929
|
return [...required, ...optional, ...device];
|
|
@@ -869,6 +931,13 @@ function buildComponents(input) {
|
|
|
869
931
|
function toCanonicalPayload(components) {
|
|
870
932
|
return components.map((component) => `${component.key}=${component.value}`).join("|");
|
|
871
933
|
}
|
|
934
|
+
function toCoreCanonicalPayload(components) {
|
|
935
|
+
return components.filter((component) => {
|
|
936
|
+
if (component.kind === "required") return true;
|
|
937
|
+
if (component.kind === "device" && component.stability === "stable") return true;
|
|
938
|
+
return false;
|
|
939
|
+
}).map((component) => `${component.key}=${component.value}`).join("|");
|
|
940
|
+
}
|
|
872
941
|
function computeConfidenceScore(components, timedOutCollectors, failedCollectorsCount) {
|
|
873
942
|
const weightedTotal = components.reduce((total, component) => total + component.weight, 0);
|
|
874
943
|
if (weightedTotal === 0) {
|
|
@@ -917,14 +986,18 @@ async function getFingerprint(options) {
|
|
|
917
986
|
optionalInputs: gathered.optionalInputs,
|
|
918
987
|
deviceSignals: gathered.deviceSignals
|
|
919
988
|
});
|
|
920
|
-
const
|
|
989
|
+
const corePayload = toCoreCanonicalPayload(components);
|
|
990
|
+
const fullPayload = toCanonicalPayload(components);
|
|
921
991
|
const digestAlgorithm = ((_a = options.hash) == null ? void 0 : _a.algorithm) ?? "SHA-256";
|
|
922
|
-
const
|
|
992
|
+
const coreHash = await createDigest(corePayload, digestAlgorithm);
|
|
993
|
+
const fullHash = await createDigest(fullPayload, digestAlgorithm);
|
|
923
994
|
const completedAtMs = Date.now();
|
|
924
995
|
const includeRaw = ((_b = options.privacy) == null ? void 0 : _b.includeRaw) ?? true;
|
|
925
996
|
const maskedRaw = applyPrivacy(gathered.rawItems, ((_c = options.privacy) == null ? void 0 : _c.maskSensitive) ?? false);
|
|
926
997
|
return {
|
|
927
|
-
|
|
998
|
+
coreHash,
|
|
999
|
+
fullHash,
|
|
1000
|
+
hash: fullHash,
|
|
928
1001
|
hashVersion: "fp_v1",
|
|
929
1002
|
components,
|
|
930
1003
|
requiredInputs: gathered.requiredInputs,
|
|
@@ -962,14 +1035,18 @@ async function getFingerprintQuick(options) {
|
|
|
962
1035
|
optionalInputs: gathered.optionalInputs,
|
|
963
1036
|
deviceSignals: gathered.deviceSignals
|
|
964
1037
|
});
|
|
965
|
-
const
|
|
1038
|
+
const corePayload = toCoreCanonicalPayload(components);
|
|
1039
|
+
const fullPayload = toCanonicalPayload(components);
|
|
966
1040
|
const digestAlgorithm = ((_a = options.hash) == null ? void 0 : _a.algorithm) ?? "SHA-256";
|
|
967
|
-
const
|
|
1041
|
+
const coreHash = await createDigest(corePayload, digestAlgorithm);
|
|
1042
|
+
const fullHash = await createDigest(fullPayload, digestAlgorithm);
|
|
968
1043
|
const completedAtMs = Date.now();
|
|
969
1044
|
const includeRaw = ((_b = options.privacy) == null ? void 0 : _b.includeRaw) ?? true;
|
|
970
1045
|
const maskedRaw = applyPrivacy(gathered.rawItems, ((_c = options.privacy) == null ? void 0 : _c.maskSensitive) ?? false);
|
|
971
1046
|
return {
|
|
972
|
-
|
|
1047
|
+
coreHash,
|
|
1048
|
+
fullHash,
|
|
1049
|
+
hash: fullHash,
|
|
973
1050
|
hashVersion: "fp_v1",
|
|
974
1051
|
components,
|
|
975
1052
|
requiredInputs: gathered.requiredInputs,
|
|
@@ -992,7 +1069,81 @@ async function getFingerprintQuick(options) {
|
|
|
992
1069
|
function resolveFingerprintPerformance(options) {
|
|
993
1070
|
return resolvePerformance(options);
|
|
994
1071
|
}
|
|
1072
|
+
|
|
1073
|
+
// src/fingerprint/matcher.ts
|
|
1074
|
+
function round3(value) {
|
|
1075
|
+
return Math.max(0, Math.min(1, Number(value.toFixed(3))));
|
|
1076
|
+
}
|
|
1077
|
+
function compareFingerprints(current, reference) {
|
|
1078
|
+
let totalReferenceWeight = 0;
|
|
1079
|
+
let matchedWeight = 0;
|
|
1080
|
+
const changedStableKeys = [];
|
|
1081
|
+
const changedVolatileKeys = [];
|
|
1082
|
+
for (const refComponent of reference.components) {
|
|
1083
|
+
if (!refComponent.present) continue;
|
|
1084
|
+
totalReferenceWeight += refComponent.weight;
|
|
1085
|
+
const currentComponent = current.components.find((component) => component.key === refComponent.key);
|
|
1086
|
+
if (currentComponent && currentComponent.present && currentComponent.value === refComponent.value) {
|
|
1087
|
+
matchedWeight += refComponent.weight;
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
if (refComponent.stability === "stable") changedStableKeys.push(refComponent.key);
|
|
1091
|
+
if (refComponent.stability === "volatile") changedVolatileKeys.push(refComponent.key);
|
|
1092
|
+
}
|
|
1093
|
+
const similarityScore = totalReferenceWeight === 0 ? 0 : round3(matchedWeight / totalReferenceWeight);
|
|
1094
|
+
const reasons = [];
|
|
1095
|
+
let risk = 1 - similarityScore;
|
|
1096
|
+
if (changedStableKeys.length >= 3) {
|
|
1097
|
+
risk += 0.2;
|
|
1098
|
+
reasons.push("Multiple stable identifiers changed");
|
|
1099
|
+
} else if (changedStableKeys.length > 0) {
|
|
1100
|
+
risk += 0.1;
|
|
1101
|
+
reasons.push("At least one stable identifier changed");
|
|
1102
|
+
}
|
|
1103
|
+
if (changedVolatileKeys.length > 0) {
|
|
1104
|
+
reasons.push("Volatile network/session identifiers changed");
|
|
1105
|
+
}
|
|
1106
|
+
if (current.deviceSignals.timezoneVsIp === "\u043D\u0435 \u0441\u043E\u0432\u043F\u0430\u0434\u0430\u0435\u0442") {
|
|
1107
|
+
risk += 0.1;
|
|
1108
|
+
reasons.push("Browser timezone mismatches IP timezone");
|
|
1109
|
+
}
|
|
1110
|
+
const botSummary = (current.deviceSignals.botRiskSummary ?? "").toLowerCase();
|
|
1111
|
+
if (botSummary.includes("\u0432\u044B\u0441\u043E\u043A\u0430\u044F")) {
|
|
1112
|
+
risk += 0.25;
|
|
1113
|
+
reasons.push("Bot/automation heuristics are high");
|
|
1114
|
+
} else if (botSummary.includes("\u0441\u0440\u0435\u0434\u043D\u044F\u044F")) {
|
|
1115
|
+
risk += 0.1;
|
|
1116
|
+
reasons.push("Bot/automation heuristics are medium");
|
|
1117
|
+
}
|
|
1118
|
+
if (current.meta.confidenceScore < 0.6) {
|
|
1119
|
+
risk += 0.05;
|
|
1120
|
+
reasons.push("Fingerprint confidence is limited");
|
|
1121
|
+
}
|
|
1122
|
+
const riskScore = round3(risk);
|
|
1123
|
+
let verdict = "different_device";
|
|
1124
|
+
if (similarityScore >= 0.85 && riskScore < 0.35) verdict = "same_device";
|
|
1125
|
+
else if (similarityScore >= 0.7 && riskScore < 0.55) verdict = "likely_same";
|
|
1126
|
+
else if (similarityScore >= 0.45) verdict = "suspicious";
|
|
1127
|
+
if (!reasons.length) {
|
|
1128
|
+
reasons.push("No meaningful drift between profiles");
|
|
1129
|
+
}
|
|
1130
|
+
return {
|
|
1131
|
+
similarityScore,
|
|
1132
|
+
riskScore,
|
|
1133
|
+
verdict,
|
|
1134
|
+
matchedWeight,
|
|
1135
|
+
totalReferenceWeight,
|
|
1136
|
+
reasons,
|
|
1137
|
+
changedStableKeys,
|
|
1138
|
+
changedVolatileKeys
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
function buildIdentityId(profile) {
|
|
1142
|
+
return profile.coreHash;
|
|
1143
|
+
}
|
|
995
1144
|
export {
|
|
1145
|
+
buildIdentityId,
|
|
1146
|
+
compareFingerprints,
|
|
996
1147
|
getFingerprint,
|
|
997
1148
|
getFingerprintQuick,
|
|
998
1149
|
resolveFingerprintPerformance
|
package/package.json
CHANGED
|
@@ -1,52 +1,102 @@
|
|
|
1
1
|
{
|
|
2
2
|
|
|
3
3
|
"name": "@mack1ch/fingerprint-js",
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
"version": "0.1.1",
|
|
6
|
+
|
|
5
7
|
"private": false,
|
|
8
|
+
|
|
6
9
|
"type": "module",
|
|
10
|
+
|
|
7
11
|
"main": "./dist/index.cjs",
|
|
12
|
+
|
|
8
13
|
"module": "./dist/index.js",
|
|
14
|
+
|
|
9
15
|
"types": "./dist/index.d.ts",
|
|
16
|
+
|
|
10
17
|
"exports": {
|
|
18
|
+
|
|
11
19
|
".": {
|
|
20
|
+
|
|
12
21
|
"types": "./dist/index.d.ts",
|
|
22
|
+
|
|
13
23
|
"import": "./dist/index.js",
|
|
24
|
+
|
|
14
25
|
"require": "./dist/index.cjs"
|
|
26
|
+
|
|
15
27
|
}
|
|
28
|
+
|
|
16
29
|
},
|
|
30
|
+
|
|
17
31
|
"files": [
|
|
32
|
+
|
|
18
33
|
"dist",
|
|
34
|
+
|
|
19
35
|
"README.md"
|
|
36
|
+
|
|
20
37
|
],
|
|
38
|
+
|
|
21
39
|
"scripts": {
|
|
40
|
+
|
|
22
41
|
"dev": "vite",
|
|
42
|
+
|
|
23
43
|
"build": "tsc -b && vite build",
|
|
44
|
+
|
|
24
45
|
"build:lib": "tsup src/fingerprint/index.ts --format esm,cjs --dts --clean --outDir dist",
|
|
46
|
+
|
|
25
47
|
"lint": "eslint .",
|
|
48
|
+
|
|
26
49
|
"preview": "vite preview",
|
|
50
|
+
|
|
27
51
|
"prepack": "npm run build:lib"
|
|
52
|
+
|
|
28
53
|
},
|
|
54
|
+
|
|
29
55
|
"dependencies": {
|
|
56
|
+
|
|
30
57
|
"antd": "^6.3.1",
|
|
58
|
+
|
|
31
59
|
"js-md5": "^0.8.3",
|
|
60
|
+
|
|
32
61
|
"react": "^19.2.0",
|
|
62
|
+
|
|
33
63
|
"react-dom": "^19.2.0"
|
|
64
|
+
|
|
34
65
|
},
|
|
66
|
+
|
|
35
67
|
"devDependencies": {
|
|
68
|
+
|
|
36
69
|
"@eslint/js": "^9.39.1",
|
|
70
|
+
|
|
37
71
|
"@tailwindcss/vite": "^4.2.1",
|
|
72
|
+
|
|
38
73
|
"@types/node": "^24.10.1",
|
|
74
|
+
|
|
39
75
|
"@types/react": "^19.2.7",
|
|
76
|
+
|
|
40
77
|
"@types/react-dom": "^19.2.3",
|
|
78
|
+
|
|
41
79
|
"@vitejs/plugin-react": "^5.1.1",
|
|
80
|
+
|
|
42
81
|
"eslint": "^9.39.1",
|
|
82
|
+
|
|
43
83
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
84
|
+
|
|
44
85
|
"eslint-plugin-react-refresh": "^0.4.24",
|
|
86
|
+
|
|
45
87
|
"globals": "^16.5.0",
|
|
88
|
+
|
|
46
89
|
"tailwindcss": "^4.2.1",
|
|
90
|
+
|
|
47
91
|
"tsup": "^8.5.0",
|
|
92
|
+
|
|
48
93
|
"typescript": "~5.9.3",
|
|
94
|
+
|
|
49
95
|
"typescript-eslint": "^8.48.0",
|
|
96
|
+
|
|
50
97
|
"vite": "^7.3.1"
|
|
98
|
+
|
|
51
99
|
}
|
|
100
|
+
|
|
52
101
|
}
|
|
102
|
+
|