@anonympins/fingerprint 0.0.1 → 0.0.3

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.
Files changed (4) hide show
  1. package/README.md +175 -44
  2. package/fingerprint.js +565 -504
  3. package/library.js +1 -825
  4. package/package.json +5 -4
package/fingerprint.js CHANGED
@@ -1,5 +1,8 @@
1
1
  // C:/Dev/games.primals.net/src/utils/fingerprint.js
2
2
  import crypto from "node:crypto";
3
+ import { Optimization } from "./library.js";
4
+ import { cyrb53, FingerprintBuilder } from "./fingerprint.builder.js";
5
+ import { getDeviceHash } from "./fingerprint.server.js";
3
6
 
4
7
  const POW_SECRET = process.env.POW_SECRET;
5
8
 
@@ -8,259 +11,15 @@ if (!POW_SECRET && process.env.NODE_ENV === 'production') {
8
11
  } else if (!POW_SECRET) {
9
12
  console.warn('Warning: POW_SECRET environment variable not set. Using a default, insecure secret for development.');
10
13
  }
11
- /**
12
- * Algorithme de hachage cyrb53 (rapide et faible taux de collision). Exporté pour réutilisation.
13
- */
14
- export const cyrb53 = (str, seed = 0) => {
15
- let h1 = 0xdeadbeef ^ seed,
16
- h2 = 0x41c6ce57 ^ seed;
17
- for (let i = 0, ch; i < str.length; i++) {
18
- ch = str.charCodeAt(i);
19
- h1 = Math.imul(h1 ^ ch, 2654435761);
20
- h2 = Math.imul(h2 ^ ch, 1597334677);
21
- }
22
- h1 =
23
- Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
24
- Math.imul(h2 ^ (h2 >>> 13), 3266489909);
25
- h2 =
26
- Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
27
- Math.imul(h1 ^ (h1 >>> 13), 3266489909);
28
- return 4294967296 * (2097151 & h2) + (h1 >>> 0);
29
- };
30
-
31
- /**
32
- * Classe pour construire une empreinte composite (Multi-Hash).
33
- * Format de sortie : "grp1:hash1|grp2:hash2|grp3:hash3"
34
- */
35
- export class FingerprintBuilder {
36
- constructor() {
37
- this.components = new Map();
38
- }
39
-
40
- /**
41
- * Ajoute un composant au hash global.
42
- * @param {string} group - Le nom du groupe (ex: 'hw', 'screen', 'geo')
43
- * @param {string|number|boolean} value - La valeur brute à hasher
44
- */
45
- add(group, value) {
46
- if (value === undefined || value === null) return this;
47
- // On hash la valeur individuellement pour l'anonymiser et réduire sa taille
48
- this.components.set(group, cyrb53(String(value)));
49
- return this;
50
- }
51
-
52
- /**
53
- * Génère la chaîne de signature finale.
54
- * Trie les clés pour garantir un ordre déterministe.
55
- */
56
- toString() {
57
- return Array.from(this.components.entries())
58
- .sort((a, b) => a[0].localeCompare(b[0])) // Tri alphabétique des clés
59
- .map(([key, hash]) => `${key}:${hash}`)
60
- .join("|");
61
- }
62
-
63
- /**
64
- * Compare deux empreintes et retourne un score de similarité (0 à 1).
65
- * Utilise des poids pour donner plus d'importance aux invariants forts (Canvas, GPU).
66
- * @param {string} fpString1 - Empreinte A
67
- * @param {string} fpString2 - Empreinte B
68
- */
69
- static compare(fpString1, fpString2) {
70
- if (!fpString1 || !fpString2) return 0;
71
-
72
- const parse = (str) => {
73
- const map = new Map();
74
- str.split("|").forEach((part) => {
75
- const [k, v] = part.split(":");
76
- if (k && v) map.set(k, v);
77
- });
78
- return map;
79
- };
80
-
81
- const map1 = parse(fpString1);
82
- const map2 = parse(fpString2);
83
-
84
- // Poids de "véracité" (Entropie/Stabilité)
85
- const weights = {
86
- cvs: 4.0, // Canvas: Très haute entropie (Rendu unique)
87
- gpu: 3.0, // GPU: Haute entropie (Matériel spécifique)
88
- hw: 1.5, // Hardware: Moyenne entropie
89
- scr: 1.0, // Screen: Moyenne
90
- geo: 0.5, // Geo: Faible (VPN/Voyage)
91
- os: 0.5, // OS: Faible (Générique)
92
- bot: 0.0, // Bot: Informatif
93
- };
94
-
95
- let weightedMatches = 0;
96
- let totalWeight = 0;
97
-
98
- const allKeys = new Set([...map1.keys(), ...map2.keys()]);
99
-
100
- allKeys.forEach((key) => {
101
- if (map1.has(key) && map2.has(key)) {
102
- const weight = weights[key] || 1.0;
103
- totalWeight += weight;
104
-
105
- if (map1.get(key) === map2.get(key)) {
106
- weightedMatches += weight;
107
- }
108
- }
109
- });
110
-
111
- return totalWeight === 0 ? 0 : weightedMatches / totalWeight;
112
- }
113
- }
114
-
115
- // Cache pour éviter de recalculer les constantes (Hardware, etc.)
116
- let cachedBuilder = null;
117
14
 
118
15
  /**
119
- * Génère l'empreinte de l'appareil actuel.
120
- */
121
- export const getDeviceFingerprint = () => {
122
- // NOTE: This is client-side code and should be in a separate file.
123
- // It will not work in a Node.js environment.
124
- // The presence of `window` and `document` confirms this.
125
-
126
- if (typeof window === "undefined") return "server-side";
127
-
128
- if (!cachedBuilder) {
129
- const nav = window.navigator;
130
- const screen = window.screen;
131
-
132
- cachedBuilder = new FingerprintBuilder();
133
-
134
- // 1. Hardware (Très stable) : Cœurs, RAM, GPU (si dispo via canvas), Touch
135
- cachedBuilder.add(
136
- "hw",
137
- `${nav.hardwareConcurrency}_${nav.deviceMemory}_${nav.maxTouchPoints}`,
138
- );
139
-
140
- // 2. Geo/Locale (Stable sauf voyage/VPN) : Timezone, Langue
141
- cachedBuilder.add(
142
- "geo",
143
- `${Intl.DateTimeFormat().resolvedOptions().timeZone}_${nav.language}_${new Date().getTimezoneOffset()}`,
144
- );
145
-
146
- // 3. Screen (Stable sauf changement moniteur/zoom) : Dimensions, ColorDepth
147
- // Note : On utilise availWidth/Height qui exclut la barre des tâches, parfois plus unique
148
- cachedBuilder.add(
149
- "scr",
150
- `${screen.width}x${screen.height}_${screen.colorDepth}`,
151
- );
152
-
153
- // 4. Platform (Stable) : OS, Engine
154
- cachedBuilder.add("os", nav.platform);
155
-
156
- // 5. Graphics (WebGL Vendor/Renderer) - Invariant matériel fort
157
- try {
158
- const canvas = document.createElement("canvas");
159
- const gl =
160
- canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
161
- if (gl) {
162
- const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
163
- if (debugInfo) {
164
- const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
165
- const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
166
- cachedBuilder.add("gpu", `${vendor}_${renderer}`);
167
- }
168
- }
169
- } catch (e) {}
170
-
171
- // 6. Canvas Fingerprinting (Rendering quirks) - Ajoute ~5-10% d'unicité
172
- // Exploite les micro-différences d'anti-aliasing et de rendu des polices
173
- try {
174
- const canvas = document.createElement("canvas");
175
- const ctx = canvas.getContext("2d");
176
- if (ctx) {
177
- canvas.width = 200;
178
- canvas.height = 50;
179
- ctx.textBaseline = "alphabetic";
180
- ctx.font = "14px 'Arial'";
181
- ctx.fillStyle = "#f60";
182
- ctx.fillRect(125, 1, 62, 20);
183
- ctx.fillStyle = "#069";
184
- ctx.fillText("Primals", 2, 15);
185
- ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
186
- ctx.fillText("Primals", 4, 17);
187
- cachedBuilder.add("cvs", canvas.toDataURL());
188
- }
189
- } catch (e) {}
190
-
191
- // 7. Bot Detection (Indication cachée)
192
- if (nav.webdriver) cachedBuilder.add("bot", "true");
193
- }
194
-
195
- // On retourne une copie pour permettre d'ajouter des champs dynamiques si besoin sans polluer le cache
196
- return cachedBuilder.toString();
197
- };
198
-
199
- /**
200
- * Génère une signature de requête incluant le contexte.
201
- * @param {object} payload
202
- */
203
- export const generateRequestSignature = (payload = {}) => {
204
- const deviceFp = getDeviceFingerprint();
205
-
206
- // On crée un builder temporaire qui hérite du deviceFp
207
- // Note: Ici on fait simple, on concatène juste le hash du payload
208
- const sortedPayload = Object.keys(payload)
209
- .sort()
210
- .map((k) => `${k}=${payload[k]}`)
211
- .join("&");
212
- const payloadHash = cyrb53(sortedPayload);
213
-
214
- return `${deviceFp}|req:${payloadHash}`;
215
- };
216
-
217
- /**
218
- * Génère une signature HMAC-SHA256 pour les données de combat.
219
- * @param {object} payload - Les données à signer (ex: { opponentId, victory, damageDealt }).
220
- * @param {string} secret - La clé secrète partagée.
221
- * @returns {Promise<string>} La signature hexadécimale.
222
- */
223
- export const generateCombatSignature = async (payload, secret) => {
224
- // NOTE: This is client-side code using the Web Crypto API (`window.crypto`).
225
- // It should be moved to a client-side script file.
226
-
227
- // 1. Créer une chaîne de caractères stable à partir du payload.
228
- const sortedPayload = Object.keys(payload)
229
- .sort()
230
- .map((k) => `${k}=${payload[k]}`)
231
- .join("&");
232
-
233
- // 2. Utiliser l'API Web Crypto pour le HMAC
234
- const encoder = new TextEncoder();
235
- const key = await window.crypto.subtle.importKey(
236
- "raw",
237
- encoder.encode(secret),
238
- { name: "HMAC", hash: "SHA-256" },
239
- false,
240
- ["sign"],
241
- );
242
- const signatureBuffer = await window.crypto.subtle.sign(
243
- "HMAC",
244
- key,
245
- encoder.encode(sortedPayload),
246
- );
247
-
248
- // 3. Convertir la signature en chaîne hexadécimale.
249
- const hashArray = Array.from(new Uint8Array(signatureBuffer));
250
- const hexString = hashArray
251
- .map((b) => b.toString(16).padStart(2, "0"))
252
- .join("");
253
- return hexString;
254
- };
255
-
256
- /**
257
- * Génère le contenu HTML pour un challenge TSP (Traveling Salesperson Problem).
258
- * @param {string} nonce - Nonce unique pour le challenge.
259
- * @param {number} numCities - Nombre de villes à inclure dans le problème.
260
- * @param {number} targetMaxDistance - Distance maximale acceptable pour la solution.
261
- * @param {Array<{x: number, y: number}>} cities - Coordonnées des villes.
262
- * @param {string} path - Chemin de redirection après résolution.
263
- * @returns {string} HTML de la page de challenge.
16
+ * Generates the HTML content for a TSP (Traveling Salesperson Problem) challenge.
17
+ * @param {string} nonce - Unique nonce for the challenge.
18
+ * @param {number} numCities - Number of cities to include in the problem.
19
+ * @param {number} targetMaxDistance - Maximum acceptable distance for the solution.
20
+ * @param {Array<{x: number, y: number}>} cities - Coordinates of the cities.
21
+ * @param {string} path - Redirect path after solving.
22
+ * @returns {string} HTML of the challenge page.
264
23
  */
265
24
  const generateTspChallenge = (
266
25
  nonce,
@@ -272,28 +31,28 @@ const generateTspChallenge = (
272
31
  const citiesJson = JSON.stringify(cities);
273
32
  return `
274
33
  <html>
275
- <head><title>Vérification de sécurité Avancée (Niveau 3)</title></head>
34
+ <head><title>Advanced Security Check (Level 3)</title></head>
276
35
  <body style="font-family:sans-serif; text-align:center; padding-top:50px;">
277
- <h1>Vérification Ultime (Niveau 3)</h1>
278
- <p>Veuillez résoudre ce petit problème d'optimisation pour prouver que vous êtes humain.</p>
279
- <div id="loader" style="margin:20px;">⚙️ Calcul d'itinéraire en cours... (${numCities} villes)</div>
36
+ <h1>Ultimate Verification (Level 3)</h1>
37
+ <p>Please solve this small optimization problem to prove you are human.</p>
38
+ <div id="loader" style="margin:20px;">⚙️ Calculating route... (${numCities} cities)</div>
280
39
  <script>
281
40
  const cities = ${citiesJson};
282
41
  const nonce = "${nonce}";
283
42
  const targetMaxDistance = ${targetMaxDistance};
284
43
 
285
- // Fonction utilitaire pour calculer la distance entre deux villes
44
+ // Utility function to calculate the distance between two cities
286
45
  function distance(city1, city2) {
287
46
  return Math.sqrt(Math.pow(city1.x - city2.x, 2) + Math.pow(city1.y - city2.y, 2));
288
47
  }
289
48
 
290
- // Fonction utilitaire pour évaluer la distance totale d'un chemin
49
+ // Utility function to evaluate the total distance of a path
291
50
  function evaluatePathDistance(cities, path) {
292
51
  let totalDistance = 0;
293
52
  for (let i = 0; i < path.length - 1; i++) {
294
53
  totalDistance += distance(cities[path[i]], cities[path[i + 1]]);
295
54
  }
296
- totalDistance += distance(cities[path[path.length - 1]], cities[path[0]]); // Retour au départ
55
+ totalDistance += distance(cities[path[path.length - 1]], cities[path[0]]); // Return to start
297
56
  return totalDistance;
298
57
  }
299
58
 
@@ -305,7 +64,7 @@ const generateTspChallenge = (
305
64
  let currentPath = [];
306
65
  let visited = new Array(numCities).fill(false);
307
66
 
308
- let currentCityIndex = 0; // Toujours commencer par la première ville pour la reproductibilité
67
+ let currentCityIndex = 0; // Always start with the first city for reproducibility
309
68
  currentPath.push(currentCityIndex);
310
69
  visited[currentCityIndex] = true;
311
70
 
@@ -330,7 +89,7 @@ const generateTspChallenge = (
330
89
  }
331
90
 
332
91
  async function solve() {
333
- // Pour ne pas freezer le navigateur, on yield le thread de temps en temps
92
+ // To avoid freezing the browser, yield the thread from time to time
334
93
  await new Promise(resolve => setTimeout(resolve, 10));
335
94
  const solutionPath = solveTspNearestNeighbor(cities);
336
95
  const solutionDistance = evaluatePathDistance(cities, solutionPath);
@@ -338,7 +97,7 @@ const generateTspChallenge = (
338
97
  if (solutionDistance <= targetMaxDistance) {
339
98
  window.location.href = "${path}" + "?pow_type=tsp&pow_nonce=" + nonce + "&pow_solution=" + JSON.stringify(solutionPath);
340
99
  } else {
341
- document.getElementById('loader').innerText = "Erreur: Impossible de trouver une solution suffisante. Veuillez réessayer.";
100
+ document.getElementById('loader').innerText = "Error: Could not find a sufficient solution. Please try again.";
342
101
  }
343
102
  }
344
103
  solve();
@@ -348,13 +107,13 @@ const generateTspChallenge = (
348
107
  };
349
108
 
350
109
  /**
351
- * Vérifie une solution de PoW TSP.
352
- * @param {string} nonce - Nonce du challenge.
353
- * @param {string} solutionPathJson - Chemin proposé par le client (JSON stringifié).
354
- * @param {number} numCities - Nombre de villes du challenge.
355
- * @param {number} targetMaxDistance - Distance maximale acceptable.
356
- * @param {Array<{x: number, y: number}>} cities - Coordonnées des villes.
357
- * @returns {boolean} True si la solution est valide.
110
+ * Verifies a TSP PoW solution.
111
+ * @param {string} nonce - The challenge nonce.
112
+ * @param {string} solutionPathJson - The path proposed by the client (stringified JSON).
113
+ * @param {number} numCities - The number of cities in the challenge.
114
+ * @param {number} targetMaxDistance - The maximum acceptable distance.
115
+ * @param {Array<{x: number, y: number}>} cities - The coordinates of the cities.
116
+ * @returns {boolean} True if the solution is valid.
358
117
  */
359
118
  export const verifyTspChallenge = (
360
119
  nonce,
@@ -368,7 +127,7 @@ export const verifyTspChallenge = (
368
127
  if (!Array.isArray(solutionPath) || solutionPath.length !== numCities)
369
128
  return false;
370
129
 
371
- // Vérifier que le chemin est une permutation valide des villes
130
+ // Verify that the path is a valid permutation of the cities
372
131
  const uniqueCities = new Set(solutionPath);
373
132
  if (
374
133
  uniqueCities.size !== numCities ||
@@ -377,11 +136,11 @@ export const verifyTspChallenge = (
377
136
  )
378
137
  return false;
379
138
 
380
- // Recalculer la distance côté serveur
139
+ // Recalculate the distance on the server side
381
140
  let totalDistance = 0;
382
141
  let totalPenalty = 0;
383
142
 
384
- // Fonction pour calculer l'angle entre 3 points (p1 -> p2 -> p3)
143
+ // Function to calculate the angle between 3 points (p1 -> p2 -> p3)
385
144
  const calculateAngle = (p1, p2, p3) => {
386
145
  const v1 = { x: p1.x - p2.x, y: p1.y - p2.y };
387
146
  const v2 = { x: p3.x - p2.x, y: p3.y - p2.y };
@@ -398,31 +157,31 @@ export const verifyTspChallenge = (
398
157
  const p2_idx = solutionPath[(i + 1) % numCities];
399
158
  const p3_idx = solutionPath[(i + 2) % numCities];
400
159
 
401
- // 1. Calcul de la distance du segment
160
+ // 1. Calculate segment distance
402
161
  totalDistance += Math.sqrt(Math.pow(cities[p1_idx].x - cities[p2_idx].x, 2) + Math.pow(cities[p1_idx].y - cities[p2_idx].y, 2));
403
162
 
404
- // 2. Calcul de la pénalité de virage
163
+ // 2. Calculate turn penalty
405
164
  const angle = calculateAngle(
406
165
  cities[p1_idx],
407
166
  cities[p2_idx],
408
167
  cities[p3_idx],
409
168
  );
410
169
  if (angle < 45) {
411
- // Pénalité pour les virages très serrés (< 45 degrés)
412
- totalPenalty += (45 - angle) * 5; // La pénalité est proportionnelle à l'acuité de l'angle
170
+ // Penalty for very sharp turns (< 45 degrees)
171
+ totalPenalty += (45 - angle) * 5; // The penalty is proportional to the sharpness of the angle
413
172
  }
414
173
  }
415
174
 
416
175
  const finalScore = totalDistance + totalPenalty;
417
176
  return finalScore <= targetMaxDistance;
418
177
  } catch (e) {
419
- console.error("Erreur lors de la vérification du challenge TSP:", e);
178
+ console.error("Error during TSP challenge verification:", e);
420
179
  return false;
421
180
  }
422
181
  };
423
182
 
424
183
  /**
425
- * Génère le contenu HTML pour le challenge PoW CPU (SHA-256).
184
+ * Generates the HTML content for the CPU PoW challenge (SHA-256).
426
185
  */
427
186
  const generateCpuPoWChallenge = (
428
187
  clientIp,
@@ -432,11 +191,11 @@ const generateCpuPoWChallenge = (
432
191
  ) => {
433
192
  return `
434
193
  <html>
435
- <head><title>Vérification de sécurité</title></head>
194
+ <head><title>Security Check</title></head>
436
195
  <body style="font-family:sans-serif; text-align:center; padding-top:50px;">
437
- <h1>Un instant... (Niveau 1)</h1>
438
- <p>Nous vérifions que vous n'êtes pas un bot. Cela prend quelques secondes.</p>
439
- <div id="loader" style="margin:20px;">⚙️ Calcul de sécurité CPU en cours...</div>
196
+ <h1>One moment... (Level 1)</h1>
197
+ <p>We are verifying that you are not a bot. This takes a few seconds.</p>
198
+ <div id="loader" style="margin:20px;">⚙️ Performing CPU security calculation...</div>
440
199
  <script>
441
200
  async function solve() {
442
201
  const ip = "${clientIp}";
@@ -451,7 +210,7 @@ const generateCpuPoWChallenge = (
451
210
  const hash = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
452
211
  if (hash.startsWith(target)) break;
453
212
  solution++;
454
- if (solution % 100000 === 0) await new Promise(resolve => setTimeout(resolve, 0)); // Pour ne pas freezer le navigateur
213
+ if (solution % 100000 === 0) await new Promise(resolve => setTimeout(resolve, 0)); // To avoid freezing the browser
455
214
  }
456
215
  window.location.href = "${path}" + "?pow_type=cpu&pow_nonce=" + nonce + "&pow_solution=" + solution;
457
216
  }
@@ -463,7 +222,7 @@ const generateCpuPoWChallenge = (
463
222
  };
464
223
 
465
224
  /**
466
- * Génère le contenu HTML pour un challenge PoW gourmand en mémoire.
225
+ * Generates the HTML content for a memory-intensive PoW challenge.
467
226
  */
468
227
  const generateMemoryPoWChallenge = (
469
228
  clientIp,
@@ -471,14 +230,14 @@ const generateMemoryPoWChallenge = (
471
230
  difficulty = 16,
472
231
  path = "",
473
232
  ) => {
474
- // difficulty ici est la taille du buffer en Mo.
233
+ // difficulty here is the buffer size in MB.
475
234
  return `
476
235
  <html>
477
- <head><title>Vérification de sécurité Avancée</title></head>
236
+ <head><title>Advanced Security Check</title></head>
478
237
  <body style="font-family:sans-serif; text-align:center; padding-top:50px;">
479
- <h1>Vérification renforcée... (Niveau 2)</h1>
480
- <p>Votre activité nécessite une vérification de sécurité supplémentaire.</p>
481
- <div id="loader" style="margin:20px;">⚙️ Allocation et calcul mémoire en cours... (${difficulty} Mo)</div>
238
+ <h1>Enhanced Verification... (Level 2)</h1>
239
+ <p>Your activity requires an additional security check.</p>
240
+ <div id="loader" style="margin:20px;">⚙️ Performing memory allocation and calculation... (${difficulty} MB)</div>
482
241
  <script>
483
242
  async function solve() {
484
243
  const nonce = "${nonce}";
@@ -499,7 +258,7 @@ const generateMemoryPoWChallenge = (
499
258
  }
500
259
  window.location.href = "${path}" + "?pow_type=mem&pow_nonce=" + nonce + "&pow_solution=" + finalHash;
501
260
  } catch(e) {
502
- document.getElementById('loader').innerText = "Erreur: Mémoire insuffisante. Veuillez rafraîchir.";
261
+ document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
503
262
  }
504
263
  }
505
264
  solve();
@@ -509,7 +268,7 @@ const generateMemoryPoWChallenge = (
509
268
  };
510
269
 
511
270
  /**
512
- * Vérifie si une solution PoW est valide et génère un ticket de passage.
271
+ * Verifies if a PoW solution is valid and generates a clearance ticket.
513
272
  */
514
273
  export const verifyPoWAndGenerateTicket = (
515
274
  ip,
@@ -517,7 +276,7 @@ export const verifyPoWAndGenerateTicket = (
517
276
  solution,
518
277
  difficulty = 4,
519
278
  ) => {
520
- // 1. Vérifier la solution : hash(ip + nonce + solution) doit commencer par N zéros
279
+ // 1. Verify the solution: hash(ip + nonce + solution) must start with N zeros
521
280
  const hash = crypto
522
281
  .createHash("sha256")
523
282
  .update(`${ip}:${nonce}:${solution}`)
@@ -527,7 +286,7 @@ export const verifyPoWAndGenerateTicket = (
527
286
  return null;
528
287
  }
529
288
 
530
- // 2. Générer un ticket HMAC pour que le client n'ait plus à le refaire pendant 1h
289
+ // 2. Generate an HMAC ticket so the client doesn't have to do it again for 1 hour
531
290
  const expiry = Date.now() + 3600000; // 1 heure
532
291
  const signature = crypto
533
292
  .createHmac("sha256", POW_SECRET || "fallback-dev-secret-32-chars-minimum")
@@ -538,8 +297,8 @@ export const verifyPoWAndGenerateTicket = (
538
297
  };
539
298
 
540
299
  /**
541
- * Vérifie une solution de PoW mémoire.
542
- * Le serveur refait le même calcul pour valider.
300
+ * Verifies a memory PoW solution.
301
+ * The server performs the same calculation to validate.
543
302
  */
544
303
  export const verifyMemoryPoW = (nonce, solution, difficulty = 16) => {
545
304
  const size = difficulty * 1024 * 1024;
@@ -565,54 +324,38 @@ export const isTicketValid = (ip, ticket) => {
565
324
  .update(`${ip}:${expiry}`)
566
325
  .digest("hex");
567
326
 
568
- // Utilisation de timingSafeEqual pour éviter les attaques temporelles
569
- return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig));
327
+ // Use timingSafeEqual to prevent timing attacks
328
+ try {
329
+ return crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expectedSig, 'hex'));
330
+ } catch (e) {
331
+ // This can happen if the buffers have different lengths, which is a failure case.
332
+ return false;
333
+ }
570
334
  };
571
335
 
572
- /**
573
- * Crée un hash stable basé sur les caractéristiques de l'appareil, indépendamment de l'IP.
574
- * C'est notre "empreinte de niveau 2".
575
- * @param {object} req - L'objet de la requête Express.
576
- * @returns {string} Un hash représentant l'appareil.
577
- */
578
- function getDeviceHash(req) {
579
- const srv = new FingerprintBuilder();
580
- srv.add("ua", req.headers["user-agent"]);
581
- if (req.headers["sec-ch-ua-platform"])
582
- srv.add("os", req.headers["sec-ch-ua-platform"]);
583
- if (req.headers["sec-ch-ua"]) srv.add("ch", req.headers["sec-ch-ua"]);
584
- return srv.toString(); // Retourne la chaîne de caractères complète de l'empreinte pour une comparaison détaillée.
585
- }
586
336
 
587
337
  /**
588
- * Calcule les indicateurs de suspicion liés aux anomalies des headers HTTP.
589
- * @param {object} req - L'objet de la requête Express.
338
+ * Calculates suspicion indicators related to HTTP header anomalies.
339
+ * @param {object} context - The request context.
590
340
  * @returns {{headerAnomalyScore: number}}
591
341
  */
592
- function getHeaderAnomalies(req, consistencyScore) {
593
- // FIX: consistencyScore est maintenant passé
342
+ function getHeaderAnomalies(context) {
594
343
  let anomalyScore = 0;
595
- // Pénalité forte si le User-Agent est manquant ou très court (signe de script simple)
596
- if (!req.headers["user-agent"] || req.headers["user-agent"].length < 10) {
344
+ // Strong penalty if User-Agent is missing or very short (sign of a simple script)
345
+ if (!context.headers["user-agent"] || context.headers["user-agent"].length < 10) {
597
346
  anomalyScore += 60;
598
347
  }
599
- // Pénalité si le header Accept-Language est manquant
600
- if (!req.headers["accept-language"]) {
348
+ // Penalty if Accept-Language header is missing
349
+ if (!context.headers["accept-language"]) {
601
350
  anomalyScore += 25;
602
351
  }
603
- // Pénalité pour les requêtes HTTP/1.0, souvent utilisées par des outils anciens ou des bots
604
- if (req.httpVersion === "1.0") {
352
+ // Penalty for HTTP/1.0 requests, often used by old tools or bots
353
+ if (context.httpVersion === "1.0") {
605
354
  anomalyScore += 15;
606
355
  }
607
356
 
608
- // NOUVEAU : Score d'incohérence (cookie volé ?)
609
- // Si le score de cohérence est bas, on ajoute une pénalité massive.
610
- // Un score de 0.2 signifie une différence énorme.
611
- const inconsistencyScore = Math.max(0, (1 - consistencyScore) * 200);
612
-
613
357
  return {
614
358
  headerAnomalyScore: Math.min(100, anomalyScore),
615
- inconsistencyScore: Math.min(100, inconsistencyScore),
616
359
  };
617
360
  }
618
361
 
@@ -625,8 +368,8 @@ function getHeaderAnomalies(req, consistencyScore) {
625
368
  */
626
369
 
627
370
  /**
628
- * Implémentation par défaut du store, en mémoire.
629
- * @type {IStore}
371
+ * Default in-memory store implementation.
372
+ * @type {IStore}
630
373
  */
631
374
  const inMemoryStore = {
632
375
  _map: new Map(),
@@ -640,101 +383,102 @@ const inMemoryStore = {
640
383
  let store = inMemoryStore;
641
384
 
642
385
  /**
643
- * Permet de configurer un datastore externe (ex: Redis).
644
- * Doit être appelée avant que le middleware ne soit utilisé.
645
- * @param {IStore} externalStore - Une implémentation de l'interface IStore.
386
+ * Allows configuring an external datastore (e.g., Redis).
387
+ * Must be called before the middleware is used.
388
+ * @param {IStore} externalStore - An implementation of the IStore interface.
646
389
  */
647
390
  export const configureStore = (externalStore) => {
648
391
  store = externalStore;
649
392
  };
650
393
 
651
394
  /**
652
- * Orchestre l'identification de la requête en utilisant une ancre persistante (cookie)
653
- * et une vérification par empreinte.
654
- * @param {object} req - L'objet de la requête Express.
655
- * @param {object} res - L'objet de la réponse Express (pour poser le cookie).
656
- * @returns {Promise<{deviceId: string, deviceData: object, consistencyScore: number}>}
395
+ * Orchestrates request identification using a persistent anchor (cookie)
396
+ * and fingerprint verification.
397
+ * @param {object} context - The request context.
398
+ * @returns {Promise<{deviceId: string, deviceData: object, consistencyScore: number, newCookie: object|null}>}
657
399
  */
658
- async function resolveRequestIdentity(req, res) {
659
- const existingDeviceId = req.cookies?.device_id;
660
- const currentDeviceHash = getDeviceHash(req);
400
+ async function resolveRequestIdentity(context) {
401
+ const existingDeviceId = context.cookies?.device_id;
402
+ const currentDeviceHash = getDeviceHash(context);
661
403
  let deviceId = existingDeviceId;
662
- let consistencyScore = 1.0; // 1.0 = parfaitement cohérent
404
+ let consistencyScore = 1.0; // 1.0 = perfectly consistent
663
405
  let deviceData = null;
406
+ let newCookie = null;
664
407
 
665
408
  if (deviceId) {
666
409
  deviceData = await store.get(`device:${deviceId}`);
667
410
  }
668
411
 
669
412
  if (deviceData) {
670
- // Cas 1: L'utilisateur a un "passeport" et nous le connaissons.
413
+ // Case 1: The user has a "passport" and we know them.
671
414
  const storedHash = deviceData.initialDeviceHash;
672
415
 
673
- // Comparaison de l'empreinte actuelle avec celle de référence.
416
+ // Compare the current fingerprint with the reference one.
674
417
  consistencyScore = FingerprintBuilder.compare(
675
418
  storedHash,
676
419
  currentDeviceHash,
677
420
  );
678
421
  } else {
679
- // Cas 2: Nouvel utilisateur ou cookie perdu/invalide.
680
- deviceId = crypto.randomUUID(); // On génère un nouveau "passeport".
681
-
682
- // On pose le cookie de manière sécurisée.
683
- res.cookie("device_id", deviceId, {
684
- httpOnly: true,
685
- secure: process.env.NODE_ENV === "production",
686
- sameSite: "strict",
687
- maxAge: 31536000000, // 1 an
688
- });
422
+ // Case 2: New user or lost/invalid cookie.
423
+ deviceId = crypto.randomUUID(); // Generate a new "passport".
424
+
425
+ // Return the intention to set a cookie.
426
+ newCookie = {
427
+ name: "device_id",
428
+ value: deviceId,
429
+ options: {
430
+ httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict", maxAge: 31536000000, // 1 year
431
+ }
432
+ };
689
433
 
690
- // On initialise le suivi pour ce nouvel appareil.
434
+ // Initialize tracking for this new device.
691
435
  deviceData = {
692
- initialDeviceHash: currentDeviceHash, // On ancre l'empreinte initiale.
436
+ initialDeviceHash: currentDeviceHash, // Anchor the initial fingerprint.
693
437
  ips: new Set(),
694
438
  lastUpdate: Date.now(),
695
439
  lastFpHash: currentDeviceHash,
696
440
  lastChangeTimestamp: 0,
697
441
  rapidChangeCount: 0,
698
442
  };
699
- // L'écriture se fera dans getSuspicionVector après toutes les modifications.
443
+ // The write will happen in getSuspicionVector after all modifications.
700
444
  }
701
445
 
702
- return { deviceId, deviceData, consistencyScore };
446
+ return { deviceId, deviceData, consistencyScore, newCookie };
703
447
  }
704
448
 
705
449
  /*
706
450
  * Calcule les indicateurs de suspicion liés au comportement de l'appareil (historique, rotation).
707
- * @param {object} req - L'objet de la requête Express.
708
- * @param {object} deviceData - Les données d'activité de l'appareil.
451
+ * @param {object} context - The request context.
452
+ * @param {object} deviceData - The device's activity data.
709
453
  * @returns {Promise<{historyScore: number, rotationScore: number}>}
710
454
  */
711
- async function getBehavioralIndicators(req, deviceData) {
455
+ async function getBehavioralIndicators(context, deviceData) {
712
456
  const now = Date.now();
713
- const clientIp = req.ip || req.socket?.remoteAddress || "unknown";
457
+ const clientIp = context.clientIp;
714
458
 
715
- // On récupère le type d'IP pour moduler le score
459
+ // Get the IP type to modulate the score
716
460
  const ipProfile = (await store.get(`ip:${clientIp}`)) || { type: "residential" };
717
461
  const isSharedIp = ipProfile.type === "shared";
718
462
 
719
- const currentFpHash = getDeviceHash(req); // On utilise le hash de l'appareil
463
+ const currentFpHash = getDeviceHash(context); // Use the device hash
720
464
 
721
- // --- Analyse de comportement (Fréquence de changement) ---
465
+ // --- Behavior analysis (Change frequency) ---
722
466
  if (deviceData.lastFpHash && currentFpHash !== deviceData.lastFpHash) {
723
467
  const timeSinceLastChange = now - deviceData.lastChangeTimestamp;
724
468
 
725
469
  if (timeSinceLastChange < RAPID_CHANGE_THRESHOLD_MS) {
726
470
  deviceData.rapidChangeCount = Math.min(
727
471
  deviceData.rapidChangeCount + 1,
728
- MAX_RAPID_CHANGES_PER_DEVICE * 2,
729
- ); // Augmente rapidement
472
+ MAX_RAPID_CHANGES_PER_DEVICE * 2, // Increases quickly
473
+ );
730
474
  } else {
731
- deviceData.rapidChangeCount = Math.max(0, deviceData.rapidChangeCount - 1); // Diminue lentement
475
+ deviceData.rapidChangeCount = Math.max(0, deviceData.rapidChangeCount - 1); // Decreases slowly
732
476
  }
733
477
  deviceData.lastChangeTimestamp = now;
734
478
  }
735
479
 
736
480
  deviceData.lastFpHash = currentFpHash;
737
- deviceData.ips.add(clientIp); // On enregistre l'IP utilisée par cet appareil
481
+ deviceData.ips.add(clientIp); // Record the IP used by this device
738
482
 
739
483
  // NOUVELLE LOGIQUE : Le score d'historique est basé sur le nombre d'IPs utilisées par l'appareil.
740
484
  // Très efficace contre la rotation de proxy.
@@ -750,7 +494,7 @@ async function getBehavioralIndicators(req, deviceData) {
750
494
  100,
751
495
  );
752
496
 
753
- // Score basé sur la rotation rapide d'identité (0-100)
497
+ // Score based on rapid identity rotation (0-100)
754
498
  const rotationScore = Math.min(
755
499
  100,
756
500
  (deviceData.rapidChangeCount / MAX_RAPID_CHANGES_PER_DEVICE) * 100,
@@ -760,33 +504,43 @@ async function getBehavioralIndicators(req, deviceData) {
760
504
  }
761
505
 
762
506
  /**
763
- * Retourne un vecteur de scores de suspicion bruts (non pondérés).
764
- * @param {object} req - L'objet de la requête Express.
507
+ * Returns a vector of raw (unweighted) suspicion scores.
508
+ * @param {object} context - The request context object.
765
509
  * @returns {Promise<{historyScore: number, rotationScore: number, headerAnomalyScore: number, inconsistencyScore: number}>}
766
510
  */
767
- export const getSuspicionVector = async (req, res) => {
768
- const { deviceId, deviceData, consistencyScore } = await resolveRequestIdentity(req, res);
511
+ export const getSuspicionVector = async (context) => {
512
+ const { deviceId, deviceData, consistencyScore, newCookie } = await resolveRequestIdentity(context);
769
513
 
770
- const clientIp = req.ip || req.socket?.remoteAddress || "unknown";
771
- await store.set(`ip-device:${clientIp}`, deviceId); // On lie l'IP à l'appareil
514
+ const clientIp = context.clientIp;
772
515
 
773
- // Nettoyage périodique des données de l'appareil
516
+ // If a new cookie needs to be set, attach it to the request object
517
+ // so the middleware can handle it. This is a temporary state holder.
518
+ if (newCookie) {
519
+ context._newCookies = context._newCookies || [];
520
+ context._newCookies.push(newCookie);
521
+ }
522
+ await store.set(`ip-device:${clientIp}`, deviceId); // Link the IP to the device
523
+
524
+ // Periodically clean up device data
774
525
  if (Date.now() - deviceData.lastUpdate > 10 * 60 * 1000) { // 10 minutes
775
526
  deviceData.ips.clear();
776
527
  deviceData.rapidChangeCount = 0;
777
528
  }
778
529
  deviceData.lastUpdate = Date.now();
779
530
 
780
- const behavioral = await getBehavioralIndicators(req, deviceData);
781
- const anomalies = getHeaderAnomalies(req, consistencyScore);
531
+ const behavioral = await getBehavioralIndicators(context, deviceData);
532
+ const { headerAnomalyScore } = getHeaderAnomalies(context);
533
+ // Calculate the inconsistency score here, separately.
534
+ const inconsistencyScore = Math.min(100, Math.max(0, (1 - consistencyScore) * 200));
535
+
782
536
 
783
- // Sauvegarde l'état mis à jour de l'appareil dans le store
537
+ // Save the updated device state to the store
784
538
  await store.set(`device:${deviceId}`, deviceData);
785
539
 
786
- return { ...behavioral, ...anomalies };
540
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore };
787
541
  };
788
542
 
789
- // Un utilisateur résidentiel peut changer de réseau (maison, 4G, wifi public).
543
+ // A residential user can change networks (home, 4G, public wifi).
790
544
  const MAX_DISTINCT_IPS_PER_DEVICE = 15;
791
545
  // Un utilisateur derrière un NAT/proxy ne devrait pas utiliser BEAUCOUP d'autres IPs.
792
546
  const MAX_DISTINCT_IPS_FOR_SHARED_USER = 5;
@@ -795,101 +549,72 @@ const MAX_DISTINCT_IPS_FOR_SHARED_USER = 5;
795
549
  const SHARED_IP_DEVICE_THRESHOLD = 50;
796
550
 
797
551
  const RAPID_CHANGE_THRESHOLD_MS = 2000; // 2 secondes
798
- const MAX_RAPID_CHANGES_PER_DEVICE = 3; // Nombre de changements rapides d'empreinte autorisés par appareil.
552
+ const MAX_RAPID_CHANGES_PER_DEVICE = 3; // Number of rapid fingerprint changes allowed per device.
799
553
 
800
554
  /**
801
- * Identifie une requête côté serveur de manière granulaire.
802
- * Utilise le FingerprintBuilder pour créer une empreinte basée sur les headers
803
- * et l'IP, rendant le spoofing plus complexe (nécessite de changer toute la stack).
555
+ * Identifies a request on the server side in a granular way.
556
+ * Uses FingerprintBuilder to create a fingerprint based on headers
557
+ * and IP, making spoofing more complex (requires changing the entire stack).
804
558
  */
805
559
  export const identifyRequest = async (req, res) => {
806
- const clientIp = req.ip || req.socket?.remoteAddress || "unknown";
807
- const deviceId = req.cookies?.device_id;
808
-
809
- // --- Mise à jour de la réputation de l'IP ---
810
- const ipProfile = (await store.get(`ip:${clientIp}`)) || {
811
- type: "residential",
812
- deviceIds: new Set(),
813
- statelessCount: 0,
814
- lastSeen: 0,
560
+ // This function now acts as a lightweight wrapper around the engine's identifyRequest method.
561
+ // It requires a default configuration to work.
562
+ const defaultConfig = {
563
+ weights: { historyScore: 0.3, rotationScore: 0.5, headerAnomalyScore: 0.1, inconsistencyScore: 0.8 },
564
+ thresholds: { low: 20, medium: 40, high: 75 }
565
+ };
566
+ const engine = new FingerprintEngine(defaultConfig);
567
+
568
+ const requestContext = {
569
+ clientIp: req.ip || req.socket?.remoteAddress || "unknown",
570
+ cookies: req.cookies,
571
+ headers: req.headers,
572
+ rawHeaders: req.rawHeaders,
573
+ httpVersion: req.httpVersion,
815
574
  };
816
- ipProfile.lastSeen = Date.now();
817
- if (deviceId) {
818
- ipProfile.deviceIds.add(deviceId);
819
- } else {
820
- // Logique anti "Bot Amnésique" améliorée
821
- ipProfile.statelessCount++;
822
- }
823
575
 
824
- // Si une IP voit trop d'appareils différents, on la classe comme "partagée".
825
- if (ipProfile.deviceIds.size > SHARED_IP_DEVICE_THRESHOLD) {
826
- ipProfile.type = "shared";
827
- }
576
+ const key = await engine.identifyRequest(requestContext);
828
577
 
829
- // Si une IP résidentielle fait trop de requêtes sans cookie, c'est un bot.
830
- // Pour une IP partagée, on est plus tolérant car de nouveaux utilisateurs arrivent constamment.
831
- const statelessLimit = ipProfile.type === "shared" ? 50 : 10;
832
- if (ipProfile.statelessCount > statelessLimit) {
833
- return `suspicious_high:${clientIp}`;
834
- }
835
- await store.set(`ip:${clientIp}`, ipProfile);
836
-
837
- // Pour la compatibilité avec le rate-limiter, on calcule un score simple.
838
- // Le PoW utilisera le système pondéré, plus complexe.
839
- const vector = await getSuspicionVector(req, res);
840
- const score =
841
- vector.historyScore * 0.3 +
842
- vector.rotationScore * 0.5 +
843
- vector.headerAnomalyScore * 0.1 +
844
- vector.inconsistencyScore * 0.8; // L'incohérence est un signal très fort
845
-
846
- // On retourne une chaîne de caractères pour la compatibilité avec les rate limiters,
847
- // mais basée sur les seuils de suspicion.
848
- // NOTE : Ces seuils sont fixes ici, mais le PoW utilisera les seuils dynamiques.
849
- if (score >= 75) {
850
- return `suspicious_high:${clientIp}`;
851
- }
852
- if (score >= 40) {
853
- return `suspicious_medium:${clientIp}`;
578
+ if (requestContext._newCookies && res) {
579
+ requestContext._newCookies.forEach(c => res.cookie(c.name, c.value, c.options));
854
580
  }
855
581
 
856
- // Pour les requêtes normales, on retourne un hash de l'empreinte pour le rate limiting.
857
- // On utilise le hash de l'appareil pour que le rate-limit suive l'appareil, pas l'IP.
858
- const deviceIdForIp = await store.get(`ip-device:${clientIp}`);
859
- const finalDeviceId = deviceId || deviceIdForIp || clientIp;
860
- return `device:${finalDeviceId}`;
582
+ return key;
861
583
  };
862
584
  // --- NOUVEAU CHALLENGE CPU "ANALOGIQUE" ---
863
585
 
864
586
  // Le plus grand nombre possible avec SHA-256 (2^256 - 1)
587
+ // The largest possible number with SHA-256 (2^256 - 1)
865
588
  const MAX_DIFFICULTY_TARGET = 2n ** 256n - 1n;
866
589
  // Une difficulté de base, ex: nécessite que les 16 premiers bits soient à 0
867
590
  // (équivalent à 4 zéros en hexadécimal)
591
+ // A base difficulty, e.g., requires the first 16 bits to be 0
592
+ // (equivalent to 4 zeros in hexadecimal)
868
593
  const BASE_TARGET = MAX_DIFFICULTY_TARGET >> 16n;
869
594
 
870
595
  /**
871
- * Calcule le target de difficulté en fonction du facteur de suspicion.
872
- * @param {number} suspicionFactor - Un nombre de 0 à 1.
873
- * @returns {BigInt} Le nombre cible.
596
+ * Calculates the difficulty target based on the suspicion factor.
597
+ * @param {number} suspicionFactor - A number from 0 to 1.
598
+ * @returns {BigInt} The target number.
874
599
  */
875
600
  function calculateTarget(suspicionFactor) {
876
- // Plage de difficulté ajustée pour être réaliste.
877
- // MIN_DIFFICULTY: Assez rapide pour ne pas gêner un utilisateur légèrement suspect.
878
- // MAX_DIFFICULTY: Assez lent pour pénaliser lourdement un bot, mais faisable pour un humain patient (5-30s).
879
- const MIN_DIFFICULTY_BITS = 18; // Valeur par défaut, devrait être configurable
880
- const MAX_DIFFICULTY_BITS = 26; // Valeur par défaut, devrait être configurable
601
+ // Difficulty range adjusted to be realistic.
602
+ // MIN_DIFFICULTY: Fast enough not to bother a slightly suspicious user.
603
+ // MAX_DIFFICULTY: Slow enough to heavily penalize a bot, but feasible for a patient human (5-30s).
604
+ const MIN_DIFFICULTY_BITS = 18; // Default value, should be configurable
605
+ const MAX_DIFFICULTY_BITS = 26; // Default value, should be configurable
881
606
 
882
- // On utilise une interpolation linéaire entre la difficulté min et max.
607
+ // Use linear interpolation between min and max difficulty.
883
608
  const totalDifficultyBits =
884
609
  MIN_DIFFICULTY_BITS +
885
610
  suspicionFactor * (MAX_DIFFICULTY_BITS - MIN_DIFFICULTY_BITS);
886
611
 
887
- // Le target est le max / 2^bits
612
+ // The target is max / 2^bits
888
613
  return MAX_DIFFICULTY_TARGET >> BigInt(Math.floor(totalDifficultyBits));
889
614
  }
890
615
 
891
616
  /**
892
- * Génère un challenge CPU basé sur un target.
617
+ * Generates a CPU challenge based on a target.
893
618
  */
894
619
  export function generateCpuTargetChallenge(
895
620
  clientIp,
@@ -901,7 +626,7 @@ export function generateCpuTargetChallenge(
901
626
  return {
902
627
  type: "cpu_target",
903
628
  nonce: nonce,
904
- target: target.toString(16), // On envoie le target en hexadécimal
629
+ target: target.toString(16), // Send the target in hexadecimal
905
630
  path: originalUrl,
906
631
  };
907
632
  }
@@ -942,7 +667,67 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
942
667
  }
943
668
 
944
669
  /**
945
- * Vérifie une solution de PoW basée sur un target et génère un ticket.
670
+ * Generates the HTML content for a combined CPU + Memory PoW challenge.
671
+ * @param {object} cpuChallengeDetails - Details from generateCpuTargetChallenge.
672
+ * @param {number} memoryDifficulty - Memory allocation in MB.
673
+ * @param {string} clientIp - The client's IP address.
674
+ * @returns {string} HTML content.
675
+ */
676
+ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp) {
677
+ const { nonce, target, path } = cpuChallengeDetails;
678
+ return `
679
+ <html><head><title>Advanced Security Check</title></head>
680
+ <body style="font-family:sans-serif; text-align:center; padding-top:50px;">
681
+ <h1>Enhanced Verification... (Level 2)</h1>
682
+ <p>Your activity requires an additional security check. This may take a few moments.</p>
683
+ <div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div>
684
+ <script>
685
+ async function solve() {
686
+ const nonce = "${nonce}";
687
+ const path = "${path}";
688
+
689
+ // --- CPU Challenge ---
690
+ document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
691
+ const cpuTarget = BigInt("0x${target}");
692
+ let cpuSolution = 0;
693
+ while (true) {
694
+ const msg = "${clientIp}:${nonce}:" + cpuSolution;
695
+ const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
696
+ const hashHex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
697
+ if (BigInt('0x' + hashHex) < cpuTarget) break;
698
+ cpuSolution++;
699
+ if (cpuSolution % 100000 === 0) await new Promise(r => setTimeout(r, 0));
700
+ }
701
+
702
+ // --- Memory Challenge ---
703
+ document.getElementById('loader').innerText = '⚙️ Performing memory allocation and calculation... (${memoryDifficulty} MB)';
704
+ await new Promise(r => setTimeout(r, 10)); // Yield to update UI
705
+
706
+ let memSolution = 0;
707
+ try {
708
+ const size = ${memoryDifficulty} * 1024 * 1024;
709
+ const buffer = new Uint32Array(size / 4);
710
+ let h = new TextEncoder().encode(nonce).reduce((acc, v) => acc + v, 0);
711
+ for (let i = 0; i < buffer.length; i++) {
712
+ buffer[i] = (h = Math.imul(h ^ i, 1597334677));
713
+ }
714
+ for(let i = 0; i < (size / 16); i++) {
715
+ const addr = buffer[i % buffer.length] % buffer.length;
716
+ memSolution ^= buffer[addr];
717
+ }
718
+ } catch(e) {
719
+ document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
720
+ return;
721
+ }
722
+ window.location.href = path + "?pow_type=cpu_mem&pow_nonce=" + nonce + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
723
+ }
724
+ solve();
725
+ </script>
726
+ </body></html>`;
727
+ }
728
+
729
+ /**
730
+ * Verifies a PoW solution based on a target and generates a ticket.
946
731
  */
947
732
  export function verifyCpuTargetPoWAndGenerateTicket(
948
733
  clientIp,
@@ -958,8 +743,8 @@ export function verifyCpuTargetPoWAndGenerateTicket(
958
743
  const hashAsInt = BigInt("0x" + hash);
959
744
 
960
745
  if (hashAsInt < target) {
961
- // La comparaison est directe avec les BigInt natifs
962
- // La preuve est valide, on génère le ticket
746
+ // The comparison is direct with native BigInts
747
+ // The proof is valid, generate the ticket
963
748
  const expiry = Date.now() + 3600000; // 1 heure
964
749
  const signature = crypto
965
750
  .createHmac("sha256", POW_SECRET || "fallback-dev-secret-32-chars-minimum")
@@ -976,51 +761,56 @@ const staticExtensions =
976
761
  const isStaticResource = (req) => staticExtensions.test(req.path);
977
762
 
978
763
  // --- Middleware Proof-of-Work (Le péage) ---
979
- export const powMiddleware = (securityConfig) => async (req, res, next) => {
980
- // On ignore le PoW pour les ressources statiques (images, scripts, fonts)
981
- if (isStaticResource(req)) {
982
- return next();
983
- }
984
-
764
+ class FingerprintEngine {
765
+ constructor(securityConfig) {
985
766
  const isProduction = process.env.NODE_ENV === 'production';
986
- const { weights, thresholds } = securityConfig;
767
+ this.securityConfig = securityConfig;
768
+ this.isProduction = isProduction;
769
+ }
987
770
 
988
- const clientIp = req.ip || req.socket?.remoteAddress || "unknown";
989
- // On récupère le vecteur de suspicion et on calcule le score final pondéré
990
- const suspicionVector = await __internal.getSuspicionVector(req, res);
771
+ async processRequest(requestContext) {
772
+ const { clientIp, path, cookies, query, isStatic } = requestContext;
773
+ const { weights, thresholds, logger } = this.securityConfig;
774
+
775
+ if (isStatic) {
776
+ return { action: 'next', score: 0, vector: {} };
777
+ }
778
+
779
+ // The engine now works with the context directly, no more rawReq dependency here.
780
+ const suspicionVector = await __internal.getSuspicionVector(requestContext);
991
781
 
992
782
  const finalScore =
993
- suspicionVector.historyScore * weights.historyScore +
994
- suspicionVector.rotationScore * weights.rotationScore +
995
- suspicionVector.headerAnomalyScore * weights.headerAnomalyScore +
996
- suspicionVector.inconsistencyScore * weights.inconsistencyScore;
783
+ suspicionVector.historyScore * (weights.historyScore || 0) +
784
+ suspicionVector.rotationScore * (weights.rotationScore || 0) +
785
+ suspicionVector.headerAnomalyScore * (weights.headerAnomalyScore || 0) +
786
+ suspicionVector.inconsistencyScore * (weights.inconsistencyScore || 0);
997
787
 
998
788
  const isSuspiciousHigh = finalScore >= thresholds.high;
999
789
  const isSuspiciousMedium = finalScore >= thresholds.medium;
1000
790
  const isSuspicious = finalScore >= thresholds.low;
1001
791
 
1002
- // Calcul d'un "facteur de suspicion" analogique (0 à 1+) pour une difficulté progressive
792
+ // Calculate an analog "suspicion factor" (0 to 1+) for progressive difficulty
1003
793
  const suspicionFactor = isSuspicious
1004
794
  ? Math.min(
1005
795
  1,
1006
796
  (finalScore - thresholds.low) / (thresholds.high - thresholds.low),
1007
797
  )
1008
798
  : 0;
1009
- const powCookie = req.cookies?.pow_clearance;
1010
- const { pow_type, pow_nonce, pow_solution, captcha_token } = req.query;
799
+ const powCookie = cookies?.pow_clearance;
800
+ const { pow_type, pow_nonce, pow_solution, pow_solution_cpu, pow_solution_mem } = query;
1011
801
 
1012
802
  if (isSuspicious && !isTicketValid(clientIp, powCookie)) {
1013
- // --- GESTION DES RÉPONSES AUX CHALLENGES ---
1014
- if (pow_nonce && pow_solution) {
803
+ // --- CHALLENGE SOLUTION HANDLING ---
804
+ if (pow_nonce && (pow_solution || (pow_solution_cpu && pow_solution_mem))) {
1015
805
  let isValid = false,
1016
806
  ticket = null;
1017
807
  if (pow_type === "cpu_target") {
1018
- // On vérifie le nouveau type
808
+ // Verify the new type
1019
809
  ticket = verifyCpuTargetPoWAndGenerateTicket(
1020
810
  clientIp,
1021
811
  pow_nonce,
1022
812
  pow_solution,
1023
- suspicionFactor, // On passe directement le facteur analogique
813
+ suspicionFactor, // Pass the analog factor directly
1024
814
  );
1025
815
  isValid = ticket !== null;
1026
816
  } else if (pow_type === "mem") {
@@ -1029,14 +819,27 @@ export const powMiddleware = (securityConfig) => async (req, res, next) => {
1029
819
  const difficulty =
1030
820
  minDifficulty + suspicionFactor * (maxDifficulty - minDifficulty);
1031
821
  isValid = verifyMemoryPoW(pow_nonce, pow_solution, difficulty);
822
+ } else if (pow_type === "cpu_mem") {
823
+ // Verify combined challenge
824
+ const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(
825
+ clientIp, pow_nonce, pow_solution_cpu, suspicionFactor
826
+ );
827
+
828
+ const minDifficulty = 16; // 16Mo
829
+ const maxDifficulty = 48; // 48Mo
830
+ const memDifficulty = minDifficulty + suspicionFactor * (maxDifficulty - minDifficulty);
831
+ const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, memDifficulty);
832
+
833
+ isValid = cpuTicket !== null && isMemValid;
834
+ if (isValid) ticket = cpuTicket; // Reuse the ticket generated by the CPU verification
1032
835
  } else if (pow_type === "tsp") {
1033
- // La logique pour TSP reste la même
836
+ // Logic for TSP remains the same
1034
837
  // ...
1035
838
  }
1036
839
 
1037
840
  if (isValid) {
1038
841
  if (!ticket) {
1039
- // Si le ticket n'a pas déjà été généré (cas CPU)
842
+ // If the ticket has not already been generated (CPU case)
1040
843
  const expiry = Date.now() + 3600000; // 1 heure
1041
844
  const signature = crypto
1042
845
  .createHmac("sha256", POW_SECRET || "fallback-dev-secret-32-chars-minimum")
@@ -1045,50 +848,181 @@ export const powMiddleware = (securityConfig) => async (req, res, next) => {
1045
848
  ticket = `${expiry}:${signature}`;
1046
849
  }
1047
850
 
1048
- res.cookie("pow_clearance", ticket, {
1049
- httpOnly: true,
1050
- secure: isProduction,
1051
- maxAge: 3600000,
1052
- });
1053
- return res.redirect(req.path); // Recharge la page sans les params
851
+ if (logger) {
852
+ logger({ type: 'challenge_solved', deviceId: cookies?.device_id, score: finalScore, challengeType: pow_type, timestamp: Date.now() });
853
+ }
854
+
855
+ return {
856
+ action: 'redirect',
857
+ path: path,
858
+ score: finalScore,
859
+ vector: suspicionVector,
860
+ cookie: {
861
+ name: 'pow_clearance',
862
+ value: ticket,
863
+ options: {
864
+ httpOnly: true,
865
+ secure: this.isProduction,
866
+ maxAge: 3600000,
867
+ }
868
+ }
869
+ };
1054
870
  }
1055
871
  }
1056
872
 
1057
- // --- SÉLECTION ET ENVOI DU CHALLENGE APPROPRIÉ ---
873
+ // --- SELECTION AND SENDING OF THE APPROPRIATE CHALLENGE ---
1058
874
  const nonce = crypto.randomBytes(16).toString("hex");
1059
875
 
1060
- // NIVEAU 3 : CAPTCHA (le plus élevé)
876
+ if (logger) {
877
+ logger({ type: 'challenge_issued', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now() });
878
+ }
879
+
880
+ // LEVEL 3: CAPTCHA (the highest)
1061
881
  if (isSuspiciousHigh) {
1062
- // ... logique pour le challenge TSP/Captcha
882
+ // ... logic for TSP/Captcha challenge
1063
883
  }
1064
884
 
1065
- // NIVEAU 2 : PoW Gourmand en Mémoire
885
+ // LEVEL 2: Memory-Intensive PoW
1066
886
  if (isSuspiciousMedium) {
1067
- const minDifficulty = 16; // 16Mo
1068
- const maxDifficulty = 48; // 48Mo
1069
- const difficulty =
1070
- minDifficulty + suspicionFactor * (maxDifficulty - minDifficulty);
1071
- return res.status(429).send(
1072
- generateMemoryPoWChallenge(clientIp, nonce, difficulty, req.path),
1073
- // NOTE: Pour que le challenge mémoire fonctionne, il faudra aussi
1074
- // l'intégrer dans `generateChallengePage` et le script client.
1075
- );
887
+ // Utilisons notre nouveau challenge combiné !
888
+ const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path);
889
+
890
+ const minMemDifficulty = 16; // 16Mo
891
+ const maxMemDifficulty = 48; // 48Mo
892
+ const memDifficulty = minMemDifficulty + suspicionFactor * (maxMemDifficulty - minMemDifficulty);
893
+
894
+ const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp);
895
+ return {
896
+ action: 'challenge', score: finalScore, vector: suspicionVector,
897
+ status: 429, body: page
898
+ };
1076
899
  }
1077
- // NIVEAU 1 : PoW CPU Standard
1078
- if (isSuspicious) {
1079
- // On récupère les détails du challenge
1080
- const challengeDetails = generateCpuTargetChallenge(
1081
- clientIp,
1082
- nonce,
1083
- suspicionFactor,
1084
- req.path,
1085
- );
1086
- // On génère la page HTML avec le solveur intégré
1087
- const challengePage = generateCpuTargetChallengePage(challengeDetails, clientIp);
1088
- return res.status(429).send(challengePage);
900
+
901
+ // NOUVELLE LOGIQUE UNIFIÉE POUR TOUS LES NIVEAUX DE SUSPICION (low et medium)
902
+ if (isSuspicious) { // Couvre à la fois low et medium
903
+ const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path);
904
+
905
+ // La difficulté mémoire démarre à 0 et augmente seulement après un certain seuil de suspicion.
906
+ // Par exemple, elle ne commence à augmenter qu'à partir de 25% du chemin entre 'low' et 'high'.
907
+ const memActivationFactor = Math.max(0, (suspicionFactor - 0.25) / 0.75);
908
+
909
+ const minMemDifficulty = 0; // Peut être 0 Mo !
910
+ const maxMemDifficulty = 48; // 48Mo pour les plus suspects
911
+ const memDifficulty = minMemDifficulty + memActivationFactor * (maxMemDifficulty - minMemDifficulty);
912
+
913
+ // On utilise toujours la page combinée, même si la difficulté mémoire est 0 (le calcul sera quasi instantané).
914
+ const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp);
915
+ return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 429, body: page };
1089
916
  }
1090
917
  }
1091
- next();
918
+
919
+ // Basic log for each non-static request that passed without a challenge
920
+ if (logger) {
921
+ logger({ type: 'request_passed', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now() });
922
+ }
923
+
924
+ return { action: 'next', score: finalScore, vector: suspicionVector };
925
+ }
926
+
927
+ /**
928
+ * Identifies a request in a granular way for non-Express environments.
929
+ * @param {object} requestContext - The request context object.
930
+ * @returns {Promise<string>} An identification string (e.g., "device:<id>", "suspicious_high:<ip>").
931
+ */
932
+ async identifyRequest(requestContext) {
933
+ const { clientIp, cookies, rawReq, rawRes } = requestContext;
934
+
935
+ // --- Update IP reputation ---
936
+ const ipProfile = (await store.get(`ip:${clientIp}`)) || {
937
+ type: "residential",
938
+ deviceIds: new Set(),
939
+ statelessCount: 0,
940
+ lastSeen: 0,
941
+ };
942
+ ipProfile.lastSeen = Date.now();
943
+ if (cookies?.device_id) {
944
+ ipProfile.deviceIds.add(cookies.device_id);
945
+ } else {
946
+ ipProfile.statelessCount++;
947
+ }
948
+
949
+ if (ipProfile.deviceIds.size > SHARED_IP_DEVICE_THRESHOLD) {
950
+ ipProfile.type = "shared";
951
+ }
952
+
953
+ const statelessLimit = ipProfile.type === "shared" ? 50 : 10;
954
+ if (ipProfile.statelessCount > statelessLimit) {
955
+ return `suspicious_high:${clientIp}`;
956
+ }
957
+ await store.set(`ip:${clientIp}`, ipProfile);
958
+
959
+ const vector = await __internal.getSuspicionVector(requestContext);
960
+ const score =
961
+ vector.historyScore * (this.securityConfig.weights.historyScore || 0.3) +
962
+ vector.rotationScore * (this.securityConfig.weights.rotationScore || 0.5) +
963
+ vector.headerAnomalyScore * (this.securityConfig.weights.headerAnomalyScore || 0.1) +
964
+ vector.inconsistencyScore * (this.securityConfig.weights.inconsistencyScore || 0.8);
965
+
966
+ if (score >= this.securityConfig.thresholds.high) return `suspicious_high:${clientIp}`;
967
+ if (score >= this.securityConfig.thresholds.low) return `suspicious_medium:${clientIp}`; // Use medium for any suspicion
968
+ if (score >= this.securityConfig.thresholds.medium) return `suspicious_medium:${clientIp}`;
969
+
970
+ // If a new device_id was created, it's in the context.
971
+ const newDeviceId = requestContext._newCookies?.find(c => c.name === 'device_id')?.value;
972
+ const finalDeviceId = cookies?.device_id || newDeviceId || clientIp;
973
+
974
+ return `device:${finalDeviceId}`;
975
+ }
976
+ }
977
+
978
+ // --- Proof-of-Work Middleware (The Tollbooth) ---
979
+ export const powMiddleware = (securityConfig) => {
980
+ const engine = new FingerprintEngine(securityConfig);
981
+
982
+ return async (req, res, next) => {
983
+ const requestContext = {
984
+ clientIp: req.ip || req.socket?.remoteAddress || "unknown",
985
+ path: req.path,
986
+ cookies: req.cookies,
987
+ query: req.query,
988
+ headers: req.headers,
989
+ isStatic: isStaticResource(req),
990
+ // Add the newly required properties for full decoupling
991
+ rawHeaders: req.rawHeaders,
992
+ httpVersion: req.httpVersion,
993
+ };
994
+
995
+ const decision = await engine.processRequest(requestContext);
996
+
997
+ // Attach the fingerprinting result to the request object for downstream middlewares.
998
+ req.fingerprint = {
999
+ score: decision.score,
1000
+ vector: decision.vector,
1001
+ };
1002
+
1003
+ // After getSuspicionVector runs, it might have attached cookies to be set.
1004
+ if (requestContext._newCookies) {
1005
+ requestContext._newCookies.forEach(c => res.cookie(c.name, c.value, c.options));
1006
+ }
1007
+
1008
+ switch (decision.action) {
1009
+ case 'block':
1010
+ return res.status(decision.status).send(decision.body);
1011
+
1012
+ case 'challenge':
1013
+ return res.status(decision.status).send(decision.body);
1014
+
1015
+ case 'redirect':
1016
+ if (decision.cookie) {
1017
+ res.cookie(decision.cookie.name, decision.cookie.value, decision.cookie.options);
1018
+ }
1019
+ return res.redirect(decision.path);
1020
+
1021
+ case 'next':
1022
+ default:
1023
+ return next();
1024
+ }
1025
+ };
1092
1026
  };
1093
1027
 
1094
1028
  /**
@@ -1097,6 +1031,133 @@ export const powMiddleware = (securityConfig) => async (req, res, next) => {
1097
1031
  * This is a common pattern to allow mocking of ES module functions.
1098
1032
  */
1099
1033
  export const __internal = {
1034
+ getDeviceHash,
1100
1035
  getSuspicionVector,
1036
+ cyrb53, // Export for testing
1037
+ FingerprintBuilder, // Export for testing
1101
1038
  calculateTarget,
1039
+ FingerprintEngine, // Expose for advanced testing
1102
1040
  };
1041
+
1042
+ // --- THRESHOLD AUTO-TUNING SECTION ---
1043
+
1044
+ let autoTuningJobId = null;
1045
+
1046
+ /**
1047
+ * Executes a threshold optimization pass using collected traffic data.
1048
+ * @private
1049
+ * @param {object} securityConfig - The security configuration object to update.
1050
+ * @param {Array<object>} trafficData - The array containing traffic logs.
1051
+ * @param {number} minDataPoints - The minimum number of data points required to start optimization.
1052
+ */
1053
+ function runThresholdOptimization(securityConfig, trafficData, minDataPoints) {
1054
+ if (trafficData.length < minDataPoints) {
1055
+ console.log(`[AutoTuning] Reporté : ${trafficData.length}/${minDataPoints} points de données.`);
1056
+ return;
1057
+ }
1058
+ console.log(`[AutoTuning] Démarrage du cycle d'optimisation avec ${trafficData.length} points de données.`);
1059
+
1060
+ // Identify "bots" (those who received a challenge but never solved it)
1061
+ // and "humans" (those who passed the challenge or never received one).
1062
+ const solvedDevices = new Set(trafficData.filter(e => e.type === 'challenge_solved').map(e => e.deviceId));
1063
+ const historicalRequests = trafficData.map(log => {
1064
+ let isBot = false;
1065
+ if (log.type === 'challenge_issued' && !solvedDevices.has(log.deviceId)) {
1066
+ isBot = true; // Assumption: a challenge issued and not solved is a bot.
1067
+ }
1068
+ return { score: log.score, isBot };
1069
+ });
1070
+
1071
+ // The "fitness" function evaluates the quality of a set of thresholds.
1072
+ // A lower score is better.
1073
+ const fitnessFunction = (solution) => {
1074
+ const [low, medium, high] = solution;
1075
+ // Constraints: thresholds must be ordered and within a reasonable range.
1076
+ if (low >= medium || medium >= high || low <= 10 || high >= 90) return Infinity;
1077
+
1078
+ let falsePositives = 0; // Humans challenged unnecessarily.
1079
+ let falseNegatives = 0; // Undetected bots.
1080
+
1081
+ for (const req of historicalRequests) {
1082
+ if (req.isBot) {
1083
+ if (req.score < low) falseNegatives++;
1084
+ } else { // Human
1085
+ if (req.score >= low) falsePositives++;
1086
+ }
1087
+ }
1088
+ // Penalize passing bots 2x more than inconvenienced humans.
1089
+ return (falsePositives * 1.0) + (falseNegatives * 2.0);
1090
+ };
1091
+
1092
+ // Functions for the genetic algorithm.
1093
+ const createIndividual = () => [10 + Math.random() * 20, 30 + Math.random() * 30, 60 + Math.random() * 30];
1094
+ const crossover = (p1, p2) => [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2, (p1[2] + p2[2]) / 2];
1095
+ const mutate = (s) => {
1096
+ const n = [...s];
1097
+ const i = Math.floor(Math.random() * 3);
1098
+ n[i] += (Math.random() - 0.5) * 5;
1099
+ return n;
1100
+ };
1101
+
1102
+ // Start optimization.
1103
+ const result = Optimization.geneticAlgorithm(createIndividual, fitnessFunction, crossover, mutate, {
1104
+ generations: 50,
1105
+ populationSize: 40
1106
+ });
1107
+
1108
+ const [newLow, newMedium, newHigh] = result.solution;
1109
+
1110
+ // Update the configuration live.
1111
+ securityConfig.thresholds = {
1112
+ low: Math.round(newLow),
1113
+ medium: Math.round(newMedium),
1114
+ high: Math.round(newHigh)
1115
+ };
1116
+
1117
+ console.log("[AutoTuning] Nouveaux seuils optimisés appliqués :", securityConfig.thresholds);
1118
+ }
1119
+
1120
+ /**
1121
+ * Starts the background process for auto-tuning security thresholds.
1122
+ * @export
1123
+ * @param {object} options - Configuration options for auto-tuning.
1124
+ * @param {object} options.securityConfig - The live security configuration object that will be mutated.
1125
+ * @param {Array<object>} options.trafficData - The array where the logger pushes traffic data.
1126
+ * @param {number} [options.interval=1800000] - The interval in milliseconds between each optimization cycle (default: 30 minutes).
1127
+ * @param {number} [options.minDataPoints=200] - The minimum number of requests to analyze before starting a cycle (default: 200).
1128
+ */
1129
+ export function startThresholdAutoTuning(options) {
1130
+ if (autoTuningJobId) {
1131
+ console.warn("[AutoTuning] Le job est déjà en cours d'exécution.");
1132
+ return;
1133
+ }
1134
+
1135
+ const {
1136
+ securityConfig,
1137
+ trafficData,
1138
+ interval = 1800000,
1139
+ minDataPoints = 200
1140
+ } = options;
1141
+
1142
+ if (!securityConfig || !trafficData) {
1143
+ throw new Error("[AutoTuning] `securityConfig` et `trafficData` sont requis.");
1144
+ }
1145
+
1146
+ console.log(`[AutoTuning] Job d'optimisation des seuils démarré. Prochain cycle dans ${interval / 60000} minutes.`);
1147
+
1148
+ autoTuningJobId = setInterval(() => {
1149
+ runThresholdOptimization(securityConfig, trafficData, minDataPoints);
1150
+ }, interval);
1151
+ }
1152
+
1153
+ /**
1154
+ * Stops the threshold auto-tuning process.
1155
+ * @export
1156
+ */
1157
+ export function stopThresholdAutoTuning() {
1158
+ if (autoTuningJobId) {
1159
+ clearInterval(autoTuningJobId);
1160
+ autoTuningJobId = null;
1161
+ console.log("[AutoTuning] Job d'optimisation des seuils arrêté.");
1162
+ }
1163
+ }