@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 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: 1,
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 canonicalPayload = toCanonicalPayload(components);
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 hash = await createDigest(canonicalPayload, digestAlgorithm);
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
- hash,
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 canonicalPayload = toCanonicalPayload(components);
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 hash = await createDigest(canonicalPayload, digestAlgorithm);
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
- hash,
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
- export { type DigestAlgorithm, type FingerprintCollectFlags, type FingerprintComponent, type FingerprintMeta, type FingerprintMode, type FingerprintOptions, type FingerprintPerformanceOptions, type FingerprintPrivacyOptions, type FingerprintQuickOptions, type FingerprintResult, getFingerprint, getFingerprintQuick, resolveFingerprintPerformance };
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
- export { type DigestAlgorithm, type FingerprintCollectFlags, type FingerprintComponent, type FingerprintMeta, type FingerprintMode, type FingerprintOptions, type FingerprintPerformanceOptions, type FingerprintPrivacyOptions, type FingerprintQuickOptions, type FingerprintResult, getFingerprint, getFingerprintQuick, resolveFingerprintPerformance };
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: 1,
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 canonicalPayload = toCanonicalPayload(components);
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 hash = await createDigest(canonicalPayload, digestAlgorithm);
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
- hash,
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 canonicalPayload = toCanonicalPayload(components);
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 hash = await createDigest(canonicalPayload, digestAlgorithm);
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
- hash,
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
- "version": "0.1.0",
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
+