@anonympins/fingerprint 0.6.1 → 0.6.4

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/src/js/library.js CHANGED
@@ -1551,23 +1551,72 @@ Optimization.Operators.solveFraudDetection = (context, options = {}) => {
1551
1551
  * @param {Array<object>} config.trafficData - Données de trafic collectées.
1552
1552
  * @returns {function(object): number[]} Une fonction de fitness qui prend une configuration complète et retourne les scores [taux de faux positifs, taux de faux négatifs].
1553
1553
  */
1554
- Optimization.Operators.createFullSecurityConfigEvaluator = ({ trafficData }) => {
1555
- // Un "individu" est un objet de configuration complet
1556
- // { thresholds: { low, medium, high }, weights: { historyScore, ... }, patterns: { velocityThreshold, ... } }
1554
+ Optimization.Operators.createFullSecurityConfigEvaluator = (context) => {
1555
+ const trafficData = context.trafficData || [];
1556
+ const currentConfig = context.currentConfig || null;
1557
+
1558
+ // Ancres immuables (Baseline Anchors) pour forcer le calibrage d'échelle de suspicion
1559
+ const STATIC_ANCHORS = [
1560
+ {
1561
+ type: 'request_passed',
1562
+ weight: 15.0,
1563
+ vector: {
1564
+ historyScore: 0, rotationScore: 0, headerAnomalyScore: 0, requestPatternScore: 0,
1565
+ inconsistencyScore: 0, behaviorScore: 0, honeypotScore: 0, botScore: 0,
1566
+ crossLayerInconsistencyScore: 0, timeInconsistencyScore: 0, tlsSpoofingScore: 0,
1567
+ clickVarianceScore: 0, clientHintsInconsistencyScore: 0, subnetScore: 0,
1568
+ ipReputationScore: 0, botnetClusterScore: 0, tcpAnomalyScore: 0,
1569
+ quicAnomalyScore: 0, renderingAnomalyScore: 0, threatIntelScore: 0
1570
+ }
1571
+ },
1572
+ {
1573
+ type: 'request_blocked',
1574
+ weight: 15.0,
1575
+ vector: {
1576
+ historyScore: 100, rotationScore: 100, headerAnomalyScore: 80, requestPatternScore: 100,
1577
+ inconsistencyScore: 100, behaviorScore: 0, honeypotScore: 100, botScore: 100,
1578
+ crossLayerInconsistencyScore: 80, timeInconsistencyScore: 100, tlsSpoofingScore: 100,
1579
+ clickVarianceScore: 100, clientHintsInconsistencyScore: 90, subnetScore: 100,
1580
+ ipReputationScore: 100, botnetClusterScore: 100, tcpAnomalyScore: 100,
1581
+ quicAnomalyScore: 100, renderingAnomalyScore: 100, threatIntelScore: 100
1582
+ }
1583
+ },
1584
+ {
1585
+ type: 'request_passed', // Profil humain sain avec du bruit (doit rester sous le seuil d'alerte, ex: < 20)
1586
+ weight: 10.0,
1587
+ vector: {
1588
+ historyScore: 15, rotationScore: 10, headerAnomalyScore: 20, requestPatternScore: 15,
1589
+ inconsistencyScore: 10, behaviorScore: 15, honeypotScore: 0, botScore: 0,
1590
+ crossLayerInconsistencyScore: 10, timeInconsistencyScore: 10, tlsSpoofingScore: 10,
1591
+ clickVarianceScore: 0, clientHintsInconsistencyScore: 10, subnetScore: 5,
1592
+ ipReputationScore: 5, botnetClusterScore: 5, tcpAnomalyScore: 10,
1593
+ quicAnomalyScore: 10, renderingAnomalyScore: 5, threatIntelScore: 0
1594
+ }
1595
+ },
1596
+ {
1597
+ type: 'challenge_issued', // Profil robot furtif moyen (doit être challengé, ex: > 35)
1598
+ weight: 10.0,
1599
+ vector: {
1600
+ historyScore: 20, rotationScore: 20, headerAnomalyScore: 20, requestPatternScore: 40,
1601
+ inconsistencyScore: 30, behaviorScore: 30, honeypotScore: 0, botScore: 0,
1602
+ crossLayerInconsistencyScore: 20, timeInconsistencyScore: 20, tlsSpoofingScore: 30,
1603
+ clickVarianceScore: 20, clientHintsInconsistencyScore: 20, subnetScore: 15,
1604
+ ipReputationScore: 15, botnetClusterScore: 20, tcpAnomalyScore: 30,
1605
+ quicAnomalyScore: 20, renderingAnomalyScore: 20, threatIntelScore: 0
1606
+ }
1607
+ }
1608
+ ];
1557
1609
 
1558
1610
  return function fullConfigFitness(config) {
1559
- let falsePositives = 0; // Humains légitimes challengés ou bloqués
1560
- let falseNegatives = 0; // Bots qui sont passés
1611
+ let falsePositives = 0;
1612
+ let falseNegatives = 0;
1561
1613
  let totalHumans = 0;
1562
1614
  let totalBots = 0;
1563
- let totalChallenges = 0;
1564
- let totalChallengeCost = 0; // NOUVEAU: Coût cumulé des challenges
1565
1615
 
1566
- // Simule le calcul du score pour chaque point de données avec la configuration testée
1616
+ let maxHumanScore = 0;
1617
+ let minBotScore = 100;
1618
+
1567
1619
  const calculateScore = (log) => {
1568
- // Cette fonction interne devrait répliquer la logique de `getSuspicionVector`
1569
- // en utilisant les poids et les paramètres de `config`.
1570
- // Pour cet exemple, nous utilisons une version simplifiée.
1571
1620
  let score = 0;
1572
1621
  for (const key in config.weights) {
1573
1622
  score += (log.vector?.[key] || 0) * config.weights[key];
@@ -1575,57 +1624,78 @@ Optimization.Operators.createFullSecurityConfigEvaluator = ({ trafficData }) =>
1575
1624
  return score;
1576
1625
  };
1577
1626
 
1578
- // Pondération de la confiance : un challenge résolu est un signal humain plus fort.
1579
1627
  const confidenceWeights = {
1580
- request_passed: 0.7,
1581
- challenge_issued: 1.0,
1582
- request_blocked: 1.0,
1583
- challenge_solved: 1.5, // Signal "humain" de très haute confiance
1584
- trap_triggered: 2.0, // Signal "bot" de confiance absolue
1628
+ request_passed: 0.7,
1629
+ challenge_issued: 1.0,
1630
+ request_blocked: 1.0,
1631
+ challenge_solved: 1.5,
1632
+ trap_triggered: 2.0,
1585
1633
  };
1586
1634
 
1635
+ // 1. Évaluation sur les données de trafic réelles
1587
1636
  for (const log of trafficData) {
1588
- const confidence = confidenceWeights[log.type] || 1.0;
1589
-
1590
- // On se base sur le comportement observé pour déterminer la nature "réelle" de la requête
1591
- const isLikelyBot = log.type === 'challenge_issued' || log.type === 'request_blocked';
1637
+ const weight = log.weight || 1.0;
1638
+ const confidence = (confidenceWeights[log.type] || 1.0) * weight;
1639
+ const isLikelyBot = log.type === 'challenge_issued' || log.type === 'request_blocked' || log.type === 'trap_triggered';
1592
1640
  const isLikelyHuman = log.type === 'request_passed' || log.type === 'challenge_solved';
1593
1641
 
1642
+ const score = calculateScore(log);
1643
+
1594
1644
  if (isLikelyBot) {
1595
- totalBots++;
1596
- const score = calculateScore(log);
1597
- // Faux négatif : un bot qui aurait dû être challengé mais ne l'a pas été
1645
+ totalBots += confidence;
1646
+ minBotScore = Math.min(minBotScore, score);
1598
1647
  if (score < config.thresholds.low) {
1599
1648
  falseNegatives += confidence;
1600
- } else {
1601
- totalChallenges += confidence; // Un bot correctement challengé
1602
- // Calculer le coût de ce challenge (proportionnel à la difficulté)
1603
- const suspicionFactor = (score - config.thresholds.low) / (config.thresholds.high - config.thresholds.low);
1604
- totalChallengeCost += Math.min(1, Math.max(0, suspicionFactor)) * confidence;
1605
1649
  }
1606
1650
  } else if (isLikelyHuman) {
1607
- totalHumans++;
1608
- const score = calculateScore(log);
1609
- // Faux positif : un humain qui a été challengé inutilement
1651
+ totalHumans += confidence;
1652
+ maxHumanScore = Math.max(maxHumanScore, score);
1610
1653
  if (score >= config.thresholds.low) {
1611
1654
  falsePositives += confidence;
1612
- totalChallenges += confidence; // Un humain incorrectement challengé
1613
- const suspicionFactor = (score - config.thresholds.low) / (config.thresholds.high - config.thresholds.low);
1614
- totalChallengeCost += Math.min(1, Math.max(0, suspicionFactor)) * confidence;
1655
+ }
1656
+ }
1657
+ }
1658
+
1659
+ // 2. Évaluation sur les ancres immuables pour fixer l'échelle
1660
+ for (const anchor of STATIC_ANCHORS) {
1661
+ const score = calculateScore(anchor);
1662
+ const weight = anchor.weight;
1663
+
1664
+ if (anchor.type === 'request_blocked' || anchor.type === 'challenge_issued') {
1665
+ totalBots += weight;
1666
+ minBotScore = Math.min(minBotScore, score);
1667
+ if (score < config.thresholds.low) {
1668
+ falseNegatives += weight * 10; // Pénalité punitive forte
1669
+ }
1670
+ } else {
1671
+ totalHumans += weight;
1672
+ maxHumanScore = Math.max(maxHumanScore, score);
1673
+ if (score >= config.thresholds.low) {
1674
+ falsePositives += weight * 10; // Pénalité punitive forte
1615
1675
  }
1616
1676
  }
1617
1677
  }
1618
1678
 
1619
1679
  const falsePositiveRate = totalHumans > 0 ? falsePositives / totalHumans : 0;
1620
1680
  const falseNegativeRate = totalBots > 0 ? falseNegatives / totalBots : 0;
1621
- // NOUVEAU: Taux de challenge global
1622
- const challengeRate = (totalHumans + totalBots) > 0 ? totalChallenges / (totalHumans + totalBots) : 0;
1623
- // NOUVEAU: Coût moyen par challenge émis
1624
- const averageChallengeCost = totalChallenges > 0 ? totalChallengeCost / totalChallenges : 0;
1625
-
1626
- // L'algorithme doit minimiser ces deux objectifs
1627
- // On a maintenant 4 objectifs à minimiser !
1628
- return [falsePositiveRate, falseNegativeRate, challengeRate, averageChallengeCost];
1681
+
1682
+ // 3. Pénalité de dérive d'échelle L2 (Régularisation par rapport au point d'origine)
1683
+ let regularizationPenalty = 0;
1684
+ if (currentConfig && currentConfig.weights) {
1685
+ for (const key in config.weights) {
1686
+ const originalVal = currentConfig.weights[key] || 0;
1687
+ regularizationPenalty += Math.pow(config.weights[key] - originalVal, 2);
1688
+ }
1689
+ }
1690
+
1691
+ // 4. Maximisation de la marge de séparation (SVM-like Margin Loss)
1692
+ const marginOverlap = Math.max(0, maxHumanScore - minBotScore);
1693
+ const marginPenalty = marginOverlap / 100;
1694
+
1695
+ const obj1 = falsePositiveRate + (regularizationPenalty * 0.05);
1696
+ const obj2 = falseNegativeRate + marginPenalty;
1697
+
1698
+ return [obj1, obj2];
1629
1699
  };
1630
1700
  };
1631
1701
 
@@ -1646,17 +1716,24 @@ Optimization.Operators.solveFullSecurityTuning = (context, options = {}) => {
1646
1716
  high: 70 + secureRandom() * 20,
1647
1717
  },
1648
1718
  weights: {
1649
- historyScore: secureRandom(),
1650
- rotationScore: secureRandom(),
1651
- headerAnomalyScore: secureRandom(),
1652
- requestPatternScore: 0.5 + secureRandom(),
1653
- inconsistencyScore: secureRandom(),
1654
- honeypotScore: 1.0, // Garder le honeypot à 1.0 est une bonne pratique
1655
- behaviorScore: secureRandom(),
1656
- crossLayerInconsistencyScore: secureRandom(),
1657
- timeInconsistencyScore: secureRandom(),
1658
- tlsSpoofingScore: secureRandom(),
1659
- botScore: secureRandom(),
1719
+ historyScore: secureRandom(),
1720
+ rotationScore: secureRandom(),
1721
+ headerAnomalyScore: secureRandom(),
1722
+ requestPatternScore: 0.5 + secureRandom(),
1723
+ inconsistencyScore: secureRandom(),
1724
+ honeypotScore: 1.0, // Garder le honeypot à 1.0 est une bonne pratique
1725
+ behaviorScore: secureRandom(),
1726
+ crossLayerInconsistencyScore: secureRandom(),
1727
+ timeInconsistencyScore: secureRandom(),
1728
+ tlsSpoofingScore: secureRandom(),
1729
+ botScore: secureRandom(),
1730
+ subnetScore: secureRandom(),
1731
+ ipReputationScore: secureRandom(),
1732
+ botnetClusterScore: secureRandom(),
1733
+ tcpAnomalyScore: secureRandom(),
1734
+ quicAnomalyScore: secureRandom(),
1735
+ clickVarianceScore: secureRandom(),
1736
+ clientHintsInconsistencyScore: secureRandom()
1660
1737
  },
1661
1738
  patterns: {
1662
1739
  velocityThreshold: 100 + secureRandom() * 400,
@@ -154,6 +154,19 @@ export function createMongoDbStore(db, collectionName = 'fingerprint_store') {
154
154
  triggerFallback();
155
155
  }
156
156
  },
157
+ async clear() {
158
+ localStore.clear();
159
+ for (const timeout of localTimeouts.values()) {
160
+ clearTimeout(timeout);
161
+ }
162
+ localTimeouts.clear();
163
+ if (isDown) return;
164
+ try {
165
+ await collection.deleteMany({});
166
+ } catch (e) {
167
+ triggerFallback();
168
+ }
169
+ },
157
170
  async init() {
158
171
  try {
159
172
  // Automates index configuration
@@ -48,8 +48,12 @@ FunctionRegistry['facility.calculateEnergy'] = (facilities, payload) => {
48
48
  * @param {Array<{x: number, y: number}>} path - Un tableau de points représentant le chemin.
49
49
  * @returns {number} La distance totale du chemin.
50
50
  */
51
- FunctionRegistry['tsp.calculateEnergy'] = (path) => {
52
- // Crée un tableau d'indices [0, 1, 2, ...] pour la fonction evaluatePathDistance.
51
+ FunctionRegistry['tsp.calculateEnergy'] = (path, payload) => {
52
+ const cities = payload?.cities || payload?.points || [];
53
+ if (path && typeof path[0] === 'number') {
54
+ return Optimization.Utils.evaluatePathDistance(cities, path);
55
+ }
56
+ // Fallback: Crée un tableau d'indices [0, 1, 2, ...] pour la fonction evaluatePathDistance.
53
57
  const indices = Array.from({ length: path.length }, (_, i) => i);
54
58
  return Optimization.Utils.evaluatePathDistance(path, indices);
55
59
  };
@@ -111,5 +111,18 @@ export function createRedisStore(redisClient) {
111
111
  triggerFallback();
112
112
  }
113
113
  },
114
+ async clear() {
115
+ localStore.clear();
116
+ for (const timeout of localTimeouts.values()) {
117
+ clearTimeout(timeout);
118
+ }
119
+ localTimeouts.clear();
120
+ if (isDown) return;
121
+ try {
122
+ await redisClient.flushdb();
123
+ } catch (e) {
124
+ triggerFallback();
125
+ }
126
+ }
114
127
  };
115
128
  }
@@ -1,78 +1,81 @@
1
- /**
2
- * @file Creates a store adapter for Knex.js.
3
- * This adapter is compatible with various SQL databases like PostgreSQL, MySQL, and SQLite.
4
- * It handles serialization of complex objects and TTL for automatic data expiration.
5
- */
6
-
7
- /**
8
- * Creates a store adapter for a Knex.js client.
9
- *
10
- * **Note:** You must create the table yourself before using the store.
11
- * The table should have at least the following columns:
12
- * - `key` (string, primary key)
13
- * - `value` (text or json/jsonb)
14
- * - `expiresAt` (datetime or timestamp with time zone)
15
- *
16
- * Example schema for PostgreSQL:
17
- * ```sql
18
- * CREATE TABLE your_table_name (
19
- * "key" VARCHAR(255) PRIMARY KEY,
20
- * "value" TEXT NOT NULL,
21
- * "expiresAt" TIMESTAMPTZ
22
- * );
23
- * ```
24
- *
25
- * @param {import('knex').Knex} knex - An instance of the Knex client.
26
- * @param {string} [tableName='fingerprint_store'] - The name of the table to use.
27
- * @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
28
- */
29
- export function createSqlStore(knex, tableName = 'fingerprint_store') {
30
- // Custom replacer/reviver to handle Set serialization, similar to the Redis store.
31
- const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
32
- const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
33
-
34
- return {
35
- async get(key) {
36
- const row = await knex(tableName).where('key', key).first();
37
- if (!row) return null;
38
-
39
- // Manually check for expiration, as not all SQL databases have automatic TTL cleanup.
40
- if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
41
- await this.delete(key); // Clean up expired key.
42
- return null;
43
- }
44
-
45
- try {
46
- return JSON.parse(row.value, reviver);
47
- } catch (e) {
48
- // In case of malformed JSON, treat it as a miss.
49
- return null;
50
- }
51
- },
52
-
53
- async set(key, value, ttl) {
54
- const stringValue = JSON.stringify(value, replacer);
55
- const expiresAt = ttl ? new Date(Date.now() + ttl * 1000) : null;
56
-
57
- // Use native upsert capabilities of Knex for different SQL dialects.
58
- await knex(tableName)
59
- .insert({ key, value: stringValue, expiresAt })
60
- .onConflict('key')
61
- .merge();
62
- },
63
-
64
- async has(key) {
65
- const row = await knex(tableName).where('key', key).first('key');
66
- if (!row) return false;
67
- // Also check for expiration here.
68
- if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
69
- return false;
70
- }
71
- return true;
72
- },
73
-
74
- async delete(key) {
75
- await knex(tableName).where('key', key).del();
76
- },
77
- };
1
+ /**
2
+ * @file Creates a store adapter for Knex.js.
3
+ * This adapter is compatible with various SQL databases like PostgreSQL, MySQL, and SQLite.
4
+ * It handles serialization of complex objects and TTL for automatic data expiration.
5
+ */
6
+
7
+ /**
8
+ * Creates a store adapter for a Knex.js client.
9
+ *
10
+ * **Note:** You must create the table yourself before using the store.
11
+ * The table should have at least the following columns:
12
+ * - `key` (string, primary key)
13
+ * - `value` (text or json/jsonb)
14
+ * - `expiresAt` (datetime or timestamp with time zone)
15
+ *
16
+ * Example schema for PostgreSQL:
17
+ * ```sql
18
+ * CREATE TABLE your_table_name (
19
+ * "key" VARCHAR(255) PRIMARY KEY,
20
+ * "value" TEXT NOT NULL,
21
+ * "expiresAt" TIMESTAMPTZ
22
+ * );
23
+ * ```
24
+ *
25
+ * @param {import('knex').Knex} knex - An instance of the Knex client.
26
+ * @param {string} [tableName='fingerprint_store'] - The name of the table to use.
27
+ * @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
28
+ */
29
+ export function createSqlStore(knex, tableName = 'fingerprint_store') {
30
+ // Custom replacer/reviver to handle Set serialization, similar to the Redis store.
31
+ const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
32
+ const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
33
+
34
+ return {
35
+ async get(key) {
36
+ const row = await knex(tableName).where('key', key).first();
37
+ if (!row) return null;
38
+
39
+ // Manually check for expiration, as not all SQL databases have automatic TTL cleanup.
40
+ if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
41
+ await this.delete(key); // Clean up expired key.
42
+ return null;
43
+ }
44
+
45
+ try {
46
+ return JSON.parse(row.value, reviver);
47
+ } catch (e) {
48
+ // In case of malformed JSON, treat it as a miss.
49
+ return null;
50
+ }
51
+ },
52
+
53
+ async set(key, value, ttl) {
54
+ const stringValue = JSON.stringify(value, replacer);
55
+ const expiresAt = ttl ? new Date(Date.now() + ttl * 1000) : null;
56
+
57
+ // Use native upsert capabilities of Knex for different SQL dialects.
58
+ await knex(tableName)
59
+ .insert({ key, value: stringValue, expiresAt })
60
+ .onConflict('key')
61
+ .merge();
62
+ },
63
+
64
+ async has(key) {
65
+ const row = await knex(tableName).where('key', key).first('key');
66
+ if (!row) return false;
67
+ // Also check for expiration here.
68
+ if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
69
+ return false;
70
+ }
71
+ return true;
72
+ },
73
+
74
+ async delete(key) {
75
+ await knex(tableName).where('key', key).del();
76
+ },
77
+ async clear() {
78
+ await knex(tableName).del();
79
+ }
80
+ };
78
81
  }
@@ -92,8 +92,7 @@ describe('FingerprintEngine Challenge Validation', () => {
92
92
 
93
93
  // The client solves the challenge
94
94
  // --- FIX: Simulate the client creating the baseBlock for the solver ---
95
- const messageBase = `${nonce}:${challengeContext.clientSecret}:fingerprint-A:`;
96
- const baseBlock = new TextEncoder().encode(messageBase);
95
+ const baseBlock = new Uint8Array(challengeContext.baseBlock);
97
96
 
98
97
  const cpuSolution = await solveCpuTargetInline(baseBlock, cpuTarget, null);
99
98
  // The memory challenge seed does not include the fingerprint.
@@ -153,7 +152,7 @@ describe('FingerprintEngine Challenge Validation', () => {
153
152
  // --- 2. Second request: Solution is submitted from "Machine B" ---
154
153
  // --- FIX: Simulate the client creating the baseBlock for the solver ---
155
154
  // The solver uses the fingerprint of the machine it's running on.
156
- const messageBase = `${nonce}:${challengeContext.clientSecret}:fingerprint-B:`;
155
+ const messageBase = `${nonce}:${challengeContext.clientSecret}:fingerprint-B:127.0.0.1::`;
157
156
  const baseBlock = new TextEncoder().encode(messageBase);
158
157
 
159
158
  const cpuSolution = await solveCpuTargetInline(baseBlock, cpuTarget, null);
@@ -739,7 +739,7 @@ describe('Fingerprint & PoW Security Suite', () => {
739
739
  const memDifficulty = parseInt(challengeBody.match(/const memDifficulty = (\d+)/)[1], 10);
740
740
 
741
741
  // The client constructs the base block and solves the challenge
742
- const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}:`);
742
+ const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}:${ip}::`);
743
743
  const cpuSolution = await solveCpuTargetInline(baseBlock, cpuTargetHex, () => {});
744
744
  const memSolution = await solveMemory(`:${nonce}:${clientSecret}`, memDifficulty);
745
745
 
@@ -821,7 +821,7 @@ describe('Fingerprint & PoW Security Suite', () => {
821
821
  const memDifficulty = parseInt(challengeBody.match(/const memDifficulty = (\d+)/)[1], 10);
822
822
 
823
823
  // The server stores the challenge context. We need to ensure the `baseBlock` is present for verification.
824
- const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}:`);
824
+ const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}:${ip}::`);
825
825
  await inMemoryStore.set(`secret:${nonce}`, {
826
826
  clientSecret: clientSecret,
827
827
  cpuTarget: cpuTargetHex,
@@ -2405,7 +2405,7 @@ describe('Subnet Scoring (Node.js)', () => {
2405
2405
  deviceIds: ['d1', 'd2'], // count < 10, score += 0
2406
2406
  });
2407
2407
  ({ subnetScore } = await __internal.getSubnetScore(context, 'device-1'));
2408
- expect(subnetScore).toBe(10);
2408
+ expect(subnetScore).toBe(100);
2409
2409
 
2410
2410
  // 3. Many devices and high scores
2411
2411
  const deviceIds = Array.from({ length: 20 }, (_, i) => `d${i}`);
@@ -2414,7 +2414,7 @@ describe('Subnet Scoring (Node.js)', () => {
2414
2414
  deviceIds: deviceIds, // count = 20. score += (20-10)*5 = 50
2415
2415
  });
2416
2416
  ({ subnetScore } = await __internal.getSubnetScore(context, 'device-1'));
2417
- expect(subnetScore).toBe(90); // 40 + 50
2417
+ expect(subnetScore).toBe(100);
2418
2418
  });
2419
2419
  });
2420
2420
 
@@ -2714,18 +2714,18 @@ describe('Botnet Cluster Scoring (Node.js)', () => {
2714
2714
  // 2. Ajout de 2 IPs uniques (total 3)
2715
2715
  await getBotnetClusterScore({ clientIp: '192.168.1.2' }, stableFpHash);
2716
2716
  scoreData = await getBotnetClusterScore({ clientIp: '192.168.1.3' }, stableFpHash);
2717
- expect(scoreData.botnetClusterScore).toBe(50.3);
2717
+ expect(scoreData.botnetClusterScore).toBe(30.2);
2718
2718
 
2719
2719
  // 3. Ajout de 2 IPs uniques (total 5)
2720
2720
  await getBotnetClusterScore({ clientIp: '192.168.1.4' }, stableFpHash);
2721
2721
  scoreData = await getBotnetClusterScore({ clientIp: '192.168.1.5' }, stableFpHash);
2722
- expect(scoreData.botnetClusterScore).toBe(75.3);
2722
+ expect(scoreData.botnetClusterScore).toBe(45.2);
2723
2723
 
2724
2724
  // 4. Ajout de 5 IPs uniques (total 10)
2725
2725
  for (let i = 6; i <= 10; i++) {
2726
2726
  scoreData = await getBotnetClusterScore({ clientIp: `192.168.1.${i}` }, stableFpHash);
2727
2727
  }
2728
- expect(scoreData.botnetClusterScore).toBe(95.7);
2728
+ expect(scoreData.botnetClusterScore).toBe(57.4);
2729
2729
  });
2730
2730
 
2731
2731
  it('should realistically group PS4 consoles with volatile differences (different IPs/cookies) under the same cluster score', async () => {
@@ -2759,15 +2759,15 @@ describe('Botnet Cluster Scoring (Node.js)', () => {
2759
2759
  if (i === 1) {
2760
2760
  expect(vector.botnetClusterScore).toBe(0);
2761
2761
  } else if (i === 2) {
2762
- expect(vector.botnetClusterScore).toBe(29.5);
2762
+ expect(vector.botnetClusterScore).toBe(17.7);
2763
2763
  } else if (i === 3) {
2764
- expect(vector.botnetClusterScore).toBe(50.3);
2764
+ expect(vector.botnetClusterScore).toBe(30.2);
2765
2765
  } else if (i === 4) {
2766
- expect(vector.botnetClusterScore).toBe(65);
2766
+ expect(vector.botnetClusterScore).toBe(39.0);
2767
2767
  } else if (i === 5) {
2768
- expect(vector.botnetClusterScore).toBe(75.3);
2768
+ expect(vector.botnetClusterScore).toBe(45.2);
2769
2769
  } else if (i === 10) {
2770
- expect(vector.botnetClusterScore).toBe(95.7);
2770
+ expect(vector.botnetClusterScore).toBe(57.4);
2771
2771
  }
2772
2772
  }
2773
2773
  });
@@ -2874,4 +2874,41 @@ describe('IP Registration and Filtering', () => {
2874
2874
  expect(storedData.ips.has(ip1)).toBe(true);
2875
2875
  expect(storedData.ips.has(ip2)).toBe(true);
2876
2876
  });
2877
+
2878
+ describe('Ed25519 Key Generation & Reopening', () => {
2879
+ beforeEach(() => {
2880
+ delete process.env.ED25519_PRIVATE_KEY;
2881
+ delete process.env.ED25519_PUBLIC_KEY;
2882
+ vi.restoreAllMocks();
2883
+ });
2884
+
2885
+ it('should generate new keys and save to disk if not present', async () => {
2886
+ const fs = await import('node:fs');
2887
+ const existsSpy = vi.spyOn(fs, 'existsSync').mockReturnValue(false);
2888
+ const writeSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
2889
+
2890
+ const config = { ed25519: 'auto', weights: {}, thresholds: {} };
2891
+ new FingerprintEngine(config);
2892
+
2893
+ expect(process.env.ED25519_PRIVATE_KEY).toBeDefined();
2894
+ expect(process.env.ED25519_PUBLIC_KEY).toBeDefined();
2895
+ expect(writeSpy).toHaveBeenCalled();
2896
+ });
2897
+
2898
+ it('should load keys from disk if they already exist', async () => {
2899
+ const fs = await import('node:fs');
2900
+ const dummyKeys = {
2901
+ privateKey: '-----BEGIN PRIVATE KEY-----\ndummy-private-key\n-----END PRIVATE KEY-----',
2902
+ publicKey: '-----BEGIN PUBLIC KEY-----\ndummy-public-key\n-----END PUBLIC KEY-----'
2903
+ };
2904
+ const existsSpy = vi.spyOn(fs, 'existsSync').mockReturnValue(true);
2905
+ const readSpy = vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(dummyKeys));
2906
+
2907
+ const config = { ed25519: 'auto', weights: {}, thresholds: {} };
2908
+ new FingerprintEngine(config);
2909
+
2910
+ expect(process.env.ED25519_PRIVATE_KEY).toBe(dummyKeys.privateKey);
2911
+ expect(process.env.ED25519_PUBLIC_KEY).toBe(dummyKeys.publicKey);
2912
+ });
2913
+ });
2877
2914
  });