@anonympins/fingerprint 0.0.9 → 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/library.js CHANGED
@@ -498,8 +498,8 @@ Optimization.geneticAlgorithmMultiObjective = function (
498
498
  options = {},
499
499
  ) {
500
500
  const {
501
- generations = 100,
502
- populationSize = 50,
501
+ generations = 150, // Augmenté pour une meilleure convergence
502
+ populationSize = 60, // Augmenté pour plus de diversité
503
503
  mutationRate = 0.1,
504
504
  } = options;
505
505
 
@@ -1152,8 +1152,8 @@ const BASE_CLICK_COST = 1;
1152
1152
  * @param {number} config.estimatedImpressions - Le nombre d'impressions quotidiennes estimées pour ce contexte.
1153
1153
  */
1154
1154
  Optimization.Operators.createOptimalCPCEvaluator = (context) => {
1155
- const { optimalBaseCommission, optimalBonusFactor } = platformParams;
1156
- const websiteQualityScore = (website?.relevanceScore || 50) / 100;
1155
+ const { optimalBaseCommission, optimalBonusFactor } = context.platformParams;
1156
+ const websiteQualityScore = (context.website?.relevanceScore || 50) / 100;
1157
1157
 
1158
1158
  // Le taux de commission effectif pour ce site
1159
1159
  const effectiveCommissionRate = Math.max(
@@ -1164,7 +1164,7 @@ Optimization.Operators.createOptimalCPCEvaluator = (context) => {
1164
1164
  // Modèle de la demande : combien de clics l'annonceur peut-il s'offrir ?
1165
1165
  const advertiserDemand = (cpc) => {
1166
1166
  if (cpc <= 0) return Infinity;
1167
- return (advertiser.credits || 0) / cpc;
1167
+ return (context.advertiser.credits || 0) / cpc;
1168
1168
  };
1169
1169
 
1170
1170
  // --- SOLUTION : Utiliser l'offre réelle et la concurrence ---
@@ -1277,6 +1277,38 @@ Optimization.Operators.solveOptimalCPC = (context, options = {}) => {
1277
1277
  });
1278
1278
  };
1279
1279
 
1280
+ /**
1281
+ * Crée un évaluateur multi-objectifs pour trouver le TTL (Time-To-Live) optimal pour un ticket de sécurité.
1282
+ * @param {object} context - L'objet de configuration.
1283
+ * @param {number} context.suspicionScore - Le score de suspicion de l'utilisateur (0-100).
1284
+ * @returns {function(number): number[]} Une fonction de fitness qui prend un TTL (en ms) et retourne les scores des objectifs [risque, friction].
1285
+ */
1286
+ Optimization.Operators.createOptimalTtlEvaluator = ({ suspicionScore }) => {
1287
+ // On normalise le score pour qu'il soit plus impactant dans le calcul du risque.
1288
+ const normalizedScore = Math.max(1, suspicionScore);
1289
+
1290
+ return function ttlFitness(ttl) {
1291
+ // Contraintes : un TTL doit être dans une plage raisonnable (ex: 5min à 24h)
1292
+ if (ttl < 300000 || ttl > 86400000) return [Infinity, Infinity];
1293
+
1294
+ // Objectif 1 : Minimiser le Risque.
1295
+ // Le risque est le produit du score et de la durée de la session.
1296
+ // Pour un score élevé, l'algo doit choisir un TTL faible pour minimiser ce produit.
1297
+ const risk = normalizedScore * ttl;
1298
+
1299
+ // Objectif 2 : Minimiser la Friction UX.
1300
+ // La friction est l'inverse du TTL. On la pénalise d'autant plus que le score est FAIBLE.
1301
+ // (101 - score) assure que pour un score de 1, la pénalité d'un TTL court est maximale.
1302
+ // Pour un score de 100, cette pénalité est quasi nulle.
1303
+ const friction = (1 / ttl) * (101 - normalizedScore);
1304
+
1305
+ // On retourne 2 objectifs avec des facteurs de mise à l'échelle pour les équilibrer.
1306
+ return [risk / 1e7, friction * 1e9];
1307
+ };
1308
+ };
1309
+
1310
+
1311
+
1280
1312
  /**
1281
1313
  * Crée un évaluateur multi-objectifs pour trouver les seuils de détection de fraude optimaux.
1282
1314
  * @param {object} config - L'objet de configuration.
@@ -1443,4 +1475,103 @@ Optimization.Operators.solveFraudDetection = (context, options = {}) => {
1443
1475
  );
1444
1476
  };
1445
1477
 
1478
+ /**
1479
+ * Crée un évaluateur multi-objectifs pour l'auto-tuning complet de la configuration de sécurité.
1480
+ * Optimise à la fois les seuils, les poids de suspicion et les paramètres de détection de patterns.
1481
+ * @param {object} config - L'objet de configuration.
1482
+ * @param {Array<object>} config.trafficData - Données de trafic collectées.
1483
+ * @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].
1484
+ */
1485
+ Optimization.Operators.createFullSecurityConfigEvaluator = ({ trafficData }) => {
1486
+ // Un "individu" est un objet de configuration complet
1487
+ // { thresholds: { low, medium, high }, weights: { historyScore, ... }, patterns: { velocityThreshold, ... } }
1488
+
1489
+ return function fullConfigFitness(config) {
1490
+ let falsePositives = 0; // Humains légitimes challengés ou bloqués
1491
+ let falseNegatives = 0; // Bots qui sont passés
1492
+ let totalHumans = 0;
1493
+ let totalBots = 0;
1494
+
1495
+ // Simule le calcul du score pour chaque point de données avec la configuration testée
1496
+ const calculateScore = (log) => {
1497
+ // Cette fonction interne devrait répliquer la logique de `getSuspicionVector`
1498
+ // en utilisant les poids et les paramètres de `config`.
1499
+ // Pour cet exemple, nous utilisons une version simplifiée.
1500
+ let score = 0;
1501
+ for (const key in config.weights) {
1502
+ score += (log.vector[key] || 0) * config.weights[key];
1503
+ }
1504
+ return score;
1505
+ };
1506
+
1507
+ for (const log of trafficData) {
1508
+ // On se base sur le comportement observé pour déterminer la nature "réelle" de la requête
1509
+ const isLikelyBot = log.type === 'challenge_issued' || log.type === 'request_blocked';
1510
+ const isLikelyHuman = log.type === 'request_passed' || log.type === 'challenge_solved';
1511
+
1512
+ if (isLikelyBot) {
1513
+ totalBots++;
1514
+ const score = calculateScore(log);
1515
+ // Faux négatif : un bot qui aurait dû être challengé mais ne l'a pas été
1516
+ if (score < config.thresholds.low) {
1517
+ falseNegatives++;
1518
+ }
1519
+ } else if (isLikelyHuman) {
1520
+ totalHumans++;
1521
+ const score = calculateScore(log);
1522
+ // Faux positif : un humain qui a été challengé inutilement
1523
+ if (score >= config.thresholds.low) {
1524
+ falsePositives++;
1525
+ }
1526
+ }
1527
+ }
1528
+
1529
+ const falsePositiveRate = totalHumans > 0 ? falsePositives / totalHumans : 0;
1530
+ const falseNegativeRate = totalBots > 0 ? falseNegatives / totalBots : 0;
1531
+
1532
+ // L'algorithme doit minimiser ces deux objectifs
1533
+ return [falsePositiveRate, falseNegativeRate];
1534
+ };
1535
+ };
1536
+
1537
+ /**
1538
+ * Résout le problème de l'auto-tuning complet de la configuration de sécurité.
1539
+ * @param {object} context - Le contexte contenant les données de trafic.
1540
+ * @param {object} [options] - Options pour l'algorithme génétique.
1541
+ * @returns {Array<{solution: object, objectives: number[]}>} Le front de Pareto des configurations optimales.
1542
+ */
1543
+ Optimization.Operators.solveFullSecurityTuning = (context, options = {}) => {
1544
+ const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator(context);
1545
+
1546
+ // Un "individu" est un objet de configuration complet
1547
+ const createIndividual = () => ({
1548
+ thresholds: {
1549
+ low: 10 + Math.random() * 30, // 10-40
1550
+ medium: 40 + Math.random() * 30, // 40-70
1551
+ high: 70 + Math.random() * 25, // 70-95
1552
+ },
1553
+ weights: {
1554
+ historyScore: Math.random(),
1555
+ rotationScore: Math.random(),
1556
+ headerAnomalyScore: Math.random(),
1557
+ requestPatternScore: Math.random(),
1558
+ inconsistencyScore: Math.random(),
1559
+ honeypotScore: 1.0, // Garder le honeypot à 1.0 est une bonne pratique
1560
+ },
1561
+ // On pourrait aussi faire muter les `patterns` ici
1562
+ });
1563
+
1564
+ // Le crossover et la mutation doivent maintenant opérer sur des objets complexes
1565
+ const crossover = (c1, c2) => { /* ... logique de croisement pour les objets de config ... */ return c1; };
1566
+ const mutate = (c) => { /* ... logique de mutation pour les objets de config ... */ return c; };
1567
+
1568
+ return Optimization.geneticAlgorithmMultiObjective(
1569
+ createIndividual,
1570
+ fitnessFunction,
1571
+ crossover,
1572
+ mutate,
1573
+ { generations: 50, populationSize: 50, ...options }
1574
+ );
1575
+ };
1576
+
1446
1577
  export { Optimization };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anonympins/fingerprint",
3
- "version": "0.0.9",
3
+ "version": "0.1.1",
4
4
  "description": "An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.",
5
5
  "main": "fingerprint.js",
6
6
  "type": "module",
@@ -8,12 +8,13 @@
8
8
  "node": ">=20.0.0"
9
9
  },
10
10
  "scripts": {
11
- "test": "vitest run"
11
+ "test": "vitest run --reporter=verbose"
12
12
  },
13
13
  "files": [
14
14
  "fingerprint.js",
15
15
  "fingerprint.client.js",
16
16
  "fingerprint.builder.js",
17
+ "pow.solver.js",
17
18
  "library.js",
18
19
  "redis-store.js",
19
20
  "mongodb-store.js",
package/pow.solver.js ADDED
@@ -0,0 +1,214 @@
1
+ /**
2
+ * @file @/pow.solver.js
3
+ * @description Contient les fonctions côté client pour résoudre les différents types de challenges Proof-of-Work.
4
+ * Fichier compatible à la fois avec l'import de modules ES6 et l'injection directe dans un script HTML.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ /**
10
+ * Résout un challenge CPU basé sur une cible (version inline pour compatibilité HTML).
11
+ * @param {string} clientIp - L'adresse IP du client.
12
+ * @param {string} nonce - Le nonce du challenge.
13
+ * @param {bigint} target - La cible à atteindre.
14
+ * @param {string} clientSecret - Le secret client (optionnel).
15
+ * @param {Function} progressCallback - Callback pour les mises à jour de progression.
16
+ * @returns {Promise<number>} La solution (un nombre entier).
17
+ */
18
+ export async function solveCpuTargetInline(clientIp, nonce, target, clientSecret = null, progressCallback) {
19
+ const cpuTarget = BigInt(target);
20
+ let cpuSolution = 0;
21
+ while (true) {
22
+ const msg = clientSecret
23
+ ? `${clientIp}:${nonce}:${cpuSolution}:${clientSecret}`
24
+ : `${clientIp}:${nonce}:${cpuSolution}`;
25
+ const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
26
+ const hashHex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
27
+ if (BigInt('0x' + hashHex) < cpuTarget) break;
28
+ cpuSolution++;
29
+ if (cpuSolution % 100000 === 0) {
30
+ await new Promise(r => setTimeout(r, 0));
31
+ if (progressCallback) progressCallback(cpuSolution);
32
+ }
33
+ }
34
+ return cpuSolution;
35
+ }
36
+
37
+ /**
38
+ * Résout un challenge CPU basé sur une cible (version Web Worker).
39
+ * @param {string} message - Le message à hasher (ex: `ip:nonce:solution:secret`).
40
+ * @param {bigint} target - La cible à atteindre.
41
+ * @returns {Promise<number>} La solution (un nombre entier).
42
+ */
43
+ export async function solveCpuTarget(message, target) {
44
+ // Vérifie si les Web Workers sont supportés par le navigateur.
45
+ if (typeof(Worker) === "undefined") {
46
+ console.warn("Web Workers not supported. Falling back to main thread calculation (UI may freeze).");
47
+ // Ici, on pourrait remettre l'ancienne implémentation comme solution de secours.
48
+ // Pour la clarté, nous supposons que les workers sont disponibles.
49
+ throw new Error("Web Worker support is required for CPU challenges.");
50
+ }
51
+
52
+ return new Promise((resolve, reject) => {
53
+ // Crée un worker à partir du script dédié. Le chemin doit être accessible publiquement.
54
+ // Assurez-vous que `pow.worker.js` est servi par votre serveur statique.
55
+ const worker = new Worker('./pow.worker.js');
56
+
57
+ worker.onmessage = (event) => {
58
+ resolve(event.data.solution);
59
+ worker.terminate(); // Nettoie le worker une fois le travail terminé.
60
+ };
61
+
62
+ worker.onerror = (error) => {
63
+ reject(error);
64
+ worker.terminate();
65
+ };
66
+
67
+ // Envoie les données du challenge au worker pour qu'il commence le calcul.
68
+ worker.postMessage({ message, target });
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Résout un challenge basé sur la mémoire.
74
+ * @param {string} seed - La graine pour l'initialisation de la mémoire.
75
+ * @param {number} difficulty - La difficulté (en Mo).
76
+ * @returns {Promise<number>} La solution (nombre entier).
77
+ */
78
+ export async function solveMemory(seed, difficulty) {
79
+ const size = difficulty * 1024 * 1024;
80
+ const buffer = new Uint32Array(size / 4);
81
+ let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
82
+ for (let i = 0; i < buffer.length; i++) {
83
+ buffer[i] = (h = Math.imul(h ^ i, 1597334677));
84
+ }
85
+ let solution = 0;
86
+ const iterations = size / 16;
87
+ let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
88
+ for (let i = 0; i < iterations; i++) {
89
+ addr = buffer[addr] % buffer.length;
90
+ solution ^= addr;
91
+ }
92
+ return solution;
93
+ }
94
+
95
+ /**
96
+ * Résout un challenge de type "Problème du Voyageur de Commerce" (TSP).
97
+ * NOTE: Ceci est une implémentation simple (heuristique du plus proche voisin) et n'est pas garantie
98
+ * de trouver la solution optimale, mais elle est suffisante pour un challenge.
99
+ * @param {Array<{x: number, y: number}>} cities - Les coordonnées des villes.
100
+ * @param {number} targetMaxDistance - La distance maximale acceptable.
101
+ * @returns {Promise<{path: number[], distance: number}>} Le chemin et la distance.
102
+ */
103
+ export async function solveTsp(cities, targetMaxDistance) {
104
+ // Utility function to calculate the distance between two cities
105
+ function distance(city1, city2) {
106
+ return Math.sqrt(Math.pow(city1.x - city2.x, 2) + Math.pow(city1.y - city2.y, 2));
107
+ }
108
+
109
+ // Utility function to evaluate the total distance of a path
110
+ function evaluatePathDistance(cities, path) {
111
+ let totalDistance = 0;
112
+ for (let i = 0; i < path.length - 1; i++) {
113
+ totalDistance += distance(cities[path[i]], cities[path[i + 1]]);
114
+ }
115
+ totalDistance += distance(cities[path[path.length - 1]], cities[path[0]]); // Return to start
116
+ return totalDistance;
117
+ }
118
+
119
+ // Solveur simple du TSP (heuristique du plus proche voisin)
120
+ function solveTspNearestNeighbor(cities) {
121
+ const numCities = cities.length;
122
+ if (numCities === 0) return [];
123
+
124
+ let currentPath = [];
125
+ let visited = new Array(numCities).fill(false);
126
+
127
+ let currentCityIndex = 0; // Always start with the first city for reproducibility
128
+ currentPath.push(currentCityIndex);
129
+ visited[currentCityIndex] = true;
130
+
131
+ for (let i = 1; i < numCities; i++) {
132
+ let nearestCityIndex = -1;
133
+ let minDistance = Infinity;
134
+
135
+ for (let j = 0; j < numCities; j++) {
136
+ if (!visited[j]) {
137
+ const dist = distance(cities[currentCityIndex], cities[j]);
138
+ if (dist < minDistance) {
139
+ minDistance = dist;
140
+ nearestCityIndex = j;
141
+ }
142
+ }
143
+ }
144
+ currentCityIndex = nearestCityIndex;
145
+ currentPath.push(currentCityIndex);
146
+ visited[currentCityIndex] = true;
147
+ }
148
+ return currentPath;
149
+ }
150
+
151
+ // To avoid freezing the browser, yield the thread from time to time
152
+ await new Promise(resolve => setTimeout(resolve, 10));
153
+ const solutionPath = solveTspNearestNeighbor(cities);
154
+ const solutionDistance = evaluatePathDistance(cities, solutionPath);
155
+
156
+ return { path: solutionPath, distance: solutionDistance };
157
+ }
158
+
159
+ /**
160
+ * Fonction principale qui reçoit un objet challenge et le résout.
161
+ * @param {object} challenge - L'objet challenge reçu du serveur.
162
+ * @returns {Promise<object>} Un objet contenant la ou les solutions.
163
+ */
164
+ export async function solveChallenge(challenge) {
165
+ const { type, nonce, clientSecret, target, memDifficulty, cities, clientIp, targetMaxDistance } = challenge;
166
+ const solutions = {};
167
+
168
+ switch (type) {
169
+ case 'cpu_target':
170
+ const baseMessageCpu = `:${nonce}`; // L'IP est gérée côté serveur
171
+ solutions.cpu = await solveCpuTarget(baseMessageCpu, BigInt('0x' + target));
172
+ break;
173
+ case 'cpu_mem':
174
+ // Pour les appels API, le client IP n'est pas connu, on ne le met pas dans le message
175
+ const baseMessageCombined = `${nonce}:${clientSecret}`;
176
+ const memSeed = `:${nonce}:${clientSecret}`;
177
+ const [cpuSol, memSol] = await Promise.all([
178
+ // On utilise une version de solveCpuTarget qui n'a pas besoin de l'IP
179
+ solveCpuTargetInline(null, nonce, target, clientSecret),
180
+ solveMemory(memSeed, memDifficulty)
181
+ ]);
182
+ solutions.cpu = cpuSol;
183
+ solutions.mem = memSol;
184
+ break;
185
+ case 'cpu_mem_inline':
186
+ // Version inline pour compatibilité HTML avec IP incluse
187
+ const memSeedInline = `${nonce}:${clientSecret}`;
188
+ const [cpuSolInline, memSolInline] = await Promise.all([
189
+ solveCpuTargetInline(clientIp, nonce, target, clientSecret),
190
+ solveMemory(memSeedInline, memDifficulty)
191
+ ]);
192
+ solutions.cpu = cpuSolInline;
193
+ solutions.mem = memSolInline;
194
+ break;
195
+ case 'tsp':
196
+ const tspResult = await solveTsp(cities, targetMaxDistance);
197
+ solutions.tsp = tspResult.path;
198
+ solutions.distance = tspResult.distance;
199
+ break;
200
+ default:
201
+ throw new Error(`Unknown challenge type: ${type}`);
202
+ }
203
+
204
+ return solutions;
205
+ }
206
+
207
+ // --- Compatibilité pour l'injection directe dans le HTML ---
208
+ // Si le script est chargé dans un navigateur (window existe), on attache les fonctions nécessaires à window.
209
+ if (typeof window !== 'undefined') {
210
+ window.solveCpuChallengeInline = solveCpuTargetInline;
211
+ window.solveMemoryChallenge = solveMemory;
212
+ window.solveTspChallenge = solveTsp;
213
+ window.solveChallenge = solveChallenge;
214
+ }