@anonympins/fingerprint 0.2.0 → 0.2.2

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/fingerprint.js CHANGED
@@ -27,111 +27,12 @@ const getPowSecret = () => {
27
27
  * @returns {string} The solver JavaScript code.
28
28
  */
29
29
  const getPowSolverCode = () => {
30
- try {
31
- const __filename = fileURLToPath(import.meta.url);
32
- const __dirname = dirname(__filename);
33
- const solverPath = join(__dirname, 'pow.solver.inline.js'); // Use the inline version
34
- return readFileSync(solverPath, 'utf-8');
35
- } catch (error) {
36
- console.warn('Could not load pow.solver.js for inlining, using fallback inline code');
37
- // Fallback inline code if file cannot be loaded
38
- return `(function(global){
39
- async function solveCpuTargetInline(clientIp, nonce, target, clientSecret, progressCallback){
40
- const cpuTarget = typeof target === 'bigint' ? target : BigInt('0x' + target);
41
- let cpuSolution = 0;
42
- const ipPart = clientIp || '';
43
- while(true){
44
- // When a clientSecret is used, the IP is omitted from the hash to make it independent of the network.
45
- const msg = clientSecret ? \`\${nonce}:\${cpuSolution}:\${clientSecret}\` : \`\${ipPart}:\${nonce}:\${cpuSolution}\`;
46
- const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
47
- const hashHex = Array.from(new Uint8Array(buf)).map(b=>b.toString(16).padStart(2,'0')).join('');
48
- if(BigInt('0x'+hashHex) < cpuTarget) break;
49
- cpuSolution++;
50
- if(cpuSolution % 100000 === 0) await new Promise(r=>setTimeout(r,0));
51
- }
52
- return cpuSolution;
53
- }
54
- async function solveMemory(seed, difficulty){
55
- const size = difficulty * 1024 * 1024;
56
- const buffer = new Uint32Array(size / 4);
57
- let h = new TextEncoder().encode(seed).reduce((acc,v)=>acc+v,0);
58
- for(let i=0;i<buffer.length;i++) buffer[i] = h = Math.imul(h^i,1597334677);
59
- let solution = 0;
60
- const iterations = size / 16;
61
- let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
62
- for(let i=0;i<iterations;i++){
63
- addr = buffer[addr] % buffer.length;
64
- solution ^= addr;
65
- }
66
- return solution;
67
- }
68
- async function solveTsp(cities, targetMaxDistance){
69
- function distance(c1,c2){return Math.sqrt(Math.pow(c1.x-c2.x,2)+Math.pow(c1.y-c2.y,2));}
70
- function evaluatePathDistance(cities,path){
71
- let total=0;
72
- for(let i=0;i<path.length-1;i++) total+=distance(cities[path[i]],cities[path[i+1]]);
73
- total+=distance(cities[path[path.length-1]],cities[path[0]]);
74
- return total;
75
- }
76
- function solveTspNearestNeighbor(cities){
77
- const n=cities.length;
78
- if(n===0)return[];
79
- let path=[0];
80
- let visited=new Array(n).fill(false);
81
- visited[0]=true;
82
- for(let i=1;i<n;i++){
83
- let nearest=-1, minDist=Infinity;
84
- for(let j=0;j<n;j++){
85
- if(!visited[j]){
86
- const d=distance(cities[path[i-1]],cities[j]);
87
- if(d<minDist){minDist=d;nearest=j;}
88
- }
89
- }
90
- path.push(nearest);
91
- visited[nearest]=true;
92
- }
93
- return path;
94
- }
95
- await new Promise(r=>setTimeout(r,10));
96
- const solutionPath=solveTspNearestNeighbor(cities);
97
- const solutionDistance=evaluatePathDistance(cities,solutionPath);
98
- return{path:solutionPath,distance:solutionDistance};
99
- }
100
- async function solveChallenge(challenge) {
101
- const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance } = challenge;
102
- const solutions = {};
103
-
104
- switch (type) {
105
- case 'cpu_target':
106
- solutions.cpu = await solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret);
107
- break;
108
- case 'cpu_mem':
109
- case 'cpu_mem_inline':
110
- const memSeed = nonce + ":" + clientSecret;
111
- const [cpuSol, memSol] = await Promise.all([
112
- solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret),
113
- solveMemory(memSeed, memDifficulty)
114
- ]);
115
- solutions.cpu = cpuSol;
116
- solutions.mem = memSol;
117
- break;
118
- case 'tsp':
119
- const tspResult = await solveTsp(cities, targetMaxDistance);
120
- solutions.tsp = tspResult.path;
121
- solutions.distance = tspResult.distance;
122
- break;
123
- default:
124
- throw new Error(\`Unknown challenge type: \${type}\`);
125
- }
126
-
127
- return solutions;
128
- }
129
- global.solveCpuChallengeInline=solveCpuTargetInline;
130
- global.solveMemoryChallenge=solveMemory;
131
- global.solveTspChallenge=solveTsp;
132
- global.solveChallenge=solveChallenge;
133
- })(typeof window!=='undefined'?window:global);`;
134
- }
30
+ // On supprime le try/catch. Si le fichier n'est pas trouvé, le processus plantera,
31
+ // ce qui est préférable à servir un code de secours potentiellement désynchronisé.
32
+ const __filename = fileURLToPath(import.meta.url);
33
+ const __dirname = dirname(__filename);
34
+ const solverPath = join(__dirname, 'pow.solver.inline.js'); // Utilise la version inline
35
+ return readFileSync(solverPath, 'utf-8');
135
36
  };
136
37
 
137
38
  /**
@@ -190,7 +91,7 @@ function getHeaderSignature(context) {
190
91
  for (let i = 0; i < context.rawHeaders.length; i += 2) {
191
92
  headerKeys.push(context.rawHeaders[i]);
192
93
  }
193
- return cyrb53(headerKeys.join(','));
94
+ return cyrb53(headerKeys.sort().join(','));
194
95
  }
195
96
 
196
97
  /**
@@ -227,11 +128,6 @@ export function getCompositeDeviceHash(context) {
227
128
  const ua = context.headers["user-agent"];
228
129
  if (ua) {
229
130
  srv.add("ua", ua);
230
- // Extraire des infos supplémentaires du UA
231
- const uaParts = parseUserAgent(ua);
232
- if (uaParts.browser) srv.add("browser", uaParts.browser);
233
- if (uaParts.os) srv.add("os_version", uaParts.os);
234
- if (uaParts.device) srv.add("device_type", uaParts.device);
235
131
  }
236
132
 
237
133
  // 2. SIGNAL FORT: JA3 TLS Fingerprint
@@ -266,9 +162,6 @@ export function getCompositeDeviceHash(context) {
266
162
  srv.add("upgrade", context.headers["upgrade-insecure-requests"]);
267
163
  }
268
164
 
269
- // 10. SIGNAL FORT: Ordonnancement des headers
270
- srv.add("h_ord", getHeaderSignature(context));
271
-
272
165
  // 11. SIGNAL AVANCÉ: Cookies (si disponible)
273
166
  if (context.cookies) {
274
167
  const cookieKeys = Object.keys(context.cookies).sort().join(',');
@@ -614,11 +507,11 @@ export const verifyPoWAndGenerateTicket = (
614
507
  * Verifies a memory PoW solution.
615
508
  * The server performs the same calculation to validate.
616
509
  */
617
- export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret) => {
510
+ export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret = '') => {
618
511
  const size = difficulty * 1024 * 1024;
619
512
  const iterations = size / 16;
620
513
  const buffer = new Uint32Array(size / 4);
621
- const seed = clientSecret ? `${nonce}:${clientSecret}` : nonce;
514
+ const seed = `:${nonce}:${clientSecret}`;
622
515
  let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
623
516
 
624
517
  for (let i = 0; i < buffer.length; i++) {
@@ -929,113 +822,86 @@ function getCrossLayerInconsistency(context) {
929
822
  function getRequestPatternScore(context, deviceData, patternConfig = {}) {
930
823
  if (!deviceData) return { requestPatternScore: 0 };
931
824
 
932
- // Default values for the pattern detection logic, which can be overridden by the auto-tuner.
825
+ // (NOUVEAU) Logique de détection de pattern simplifiée et unifiée.
933
826
  const {
934
- velocityThreshold = 800, velocityWeight = 30,
935
- burstThreshold = 1500, burstWeight = 50,
936
- scrapeThreshold = 1000, scrapeWeight = 20, scrapeBurstWeight = 40,
937
- historySize = 10,
938
- decayFactor = 0.9,
939
- inactivityReset = 30000, // 30 secondes
940
- // Nouveaux paramètres pour l'analyse de distribution
941
- benfordMinSamples = 15, benfordWeight = 50,
942
- // Nouveau paramètre pour la détection de séquences
943
- sequenceLength = 3, sequenceWeight = 60
827
+ historySize = 20, // Nombre de requêtes à conserver pour l'analyse.
828
+ minSamples = 10, // Nombre d'intervalles de temps à analyser avant de calculer.
829
+ regularityThreshold = 150, // Écart-type (ms) en dessous duquel le comportement est "trop régulier".
830
+ benfordThreshold = 0.15, // Seuil de déviation de Benford au-dessus duquel la distribution est "non naturelle".
831
+ patternWeight = 80, // Pénalité FORTE et unique si un pattern est détecté.
832
+ decayFactor = 0.95, // Décroissance du score dans le temps.
833
+ inactivityReset = 180000 // Réinitialisation du score après 3 minutes d'inactivité.
944
834
  } = patternConfig;
945
835
 
946
836
  const now = Date.now();
947
837
  const currentPath = context.path;
948
838
  // Make the function robust to handle both URLSearchParams and plain objects for query.
949
- // Ensure query parameters are consistently handled, whether they come from a URLSearchParams object or a plain object.
950
- const params = context.query instanceof URLSearchParams ? context.query : new URLSearchParams(context.query);
839
+ const params =
840
+ context.query instanceof URLSearchParams
841
+ ? new URLSearchParams(context.query.toString()) // Clone to avoid modifying the original
842
+ : new URLSearchParams(context.query || {});
951
843
  params.sort(); // Sort for deterministic order
952
844
  const currentQueryString = params.toString();
953
845
 
954
- // Initialize request history if it doesn't exist
955
846
  if (!deviceData.requestHistory) deviceData.requestHistory = [];
956
- // NOUVEAU: S'assurer que timingHistory est toujours initialisé.
957
- // Cette vérification est séparée car deviceData peut exister avec requestHistory mais sans timingHistory.
958
847
  if (!deviceData.timingHistory) deviceData.timingHistory = [];
959
848
 
960
849
  const history = deviceData.requestHistory;
961
- let score = 0;
962
-
963
- // --- Analyze patterns based on the last few requests ---
964
- if (history.length > 0) {
965
- const lastRequest = history[history.length - 1];
966
- const timeSinceLast = now - lastRequest.timestamp;
850
+ const lastRequest = history.length > 0 ? history[history.length - 1] : null;
851
+ const timeSinceLast = lastRequest ? now - lastRequest.timestamp : Infinity;
967
852
 
968
- // Stocker le délai pour l'analyse de distribution
853
+ // Mise à jour de l'historique
854
+ history.push({
855
+ timestamp: now,
856
+ path: currentPath,
857
+ queryString: currentQueryString,
858
+ });
859
+ if (lastRequest) {
969
860
  deviceData.timingHistory.push(timeSinceLast);
861
+ }
970
862
 
971
- // 1. Velocity Check: Penalize requests that are too fast to be human.
972
- if (timeSinceLast < velocityThreshold) { // 150 < 200 -> true
973
- score += velocityWeight; // score = 30
974
- }
975
-
976
- // 2. Burst Check: Add additional penalty for identical requests in a very short time frame.
977
- if (currentPath === lastRequest.path && currentQueryString === lastRequest.queryString && timeSinceLast < burstThreshold) { // 150 < 500 -> true
978
- score += burstWeight; // score = 30 + 50 = 80
979
- }
863
+ let instantScore = 0;
864
+ const timings = deviceData.timingHistory;
980
865
 
981
- // 3. Sequential Scraping Check: Add additional penalty for same path with different query params (potential scraping).
982
- // This is a simplified check, now independent of the burst check.
983
- if (currentPath === lastRequest.path && currentQueryString !== lastRequest.queryString && timeSinceLast < scrapeThreshold) {
984
- const previousRequest = history.length > 2 ? history[history.length - 2] : null;
985
- if (previousRequest && previousRequest.path === currentPath) {
986
- score += scrapeBurstWeight; // This is at least the 3rd request in a sequence to the same path.
987
- } else {
988
- score += scrapeWeight; // First sign of a potential scraping pattern
989
- }
990
- }
866
+ // Analyse statistique unifiée si nous avons assez de données
867
+ if (timings.length >= minSamples) {
868
+ const timings = deviceData.timingHistory;
869
+ const mean = timings.reduce((a, b) => a + b, 0) / timings.length;
870
+ const variance = timings.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / timings.length;
871
+ const stdDev = Math.sqrt(variance);
872
+ const benfordDeviation = Optimization.Operators.benfordTest(timings);
991
873
 
992
- // 4. (NOUVEAU) Détection de séquences répétitives (ex: A -> B -> C -> A -> B -> C)
993
- if (history.length >= sequenceLength * 2) {
994
- const lastSequence = history.slice(-sequenceLength);
995
- const previousSequence = history.slice(-sequenceLength * 2, -sequenceLength);
996
-
997
- const isRepeating = lastSequence.every((req, i) =>
998
- req.path === previousSequence[i].path && req.queryString === previousSequence[i].queryString
999
- );
1000
- if (isRepeating) score += sequenceWeight;
874
+ // Détection de régularité (bots de type "cron")
875
+ if (stdDev < regularityThreshold) {
876
+ instantScore = patternWeight;
1001
877
  }
1002
-
1003
- // 5. (NOUVEAU) Analyse de la distribution des délais avec la loi de Benford
1004
- if (deviceData.timingHistory.length >= benfordMinSamples) {
1005
- // On concatène tous les délais en une seule chaîne de chiffres.
1006
- const benfordDeviation = Optimization.Operators.benfordTest(deviceData.timingHistory);
1007
-
1008
- // Une déviation > 0.15 est suspecte. On peut pondérer la pénalité.
1009
- // Une déviation de 0.3 (très suspecte) donnerait un score de 100 (0.3 / 0.3 * 100).
1010
- score += Math.min(100, (benfordDeviation / 0.3) * benfordWeight);
878
+ // Détection de distribution non-naturelle (bots "faussement aléatoires")
879
+ else if (benfordDeviation > benfordThreshold) {
880
+ instantScore = patternWeight;
1011
881
  }
1012
882
  }
1013
883
 
1014
- // --- Update history ---
1015
- history.push({
1016
- timestamp: now,
1017
- path: currentPath,
1018
- queryString: currentQueryString,
1019
- });
1020
-
1021
- // Keep history to a reasonable size (e.g., last 10 requests)
884
+ // Garder l'historique à une taille raisonnable
1022
885
  if (history.length > historySize) {
1023
886
  history.shift();
1024
887
  }
1025
- if (deviceData.timingHistory.length > benfordMinSamples * 2) { // Garder un historique plus long pour Benford
888
+ if (deviceData.timingHistory.length > historySize) {
1026
889
  deviceData.timingHistory.shift();
1027
890
  }
1028
891
 
1029
- // Decay the score over time if behavior becomes normal again.
1030
- // We can store the score in deviceData and decay it.
1031
- deviceData.lastPatternScore = Math.min(100, (deviceData.lastPatternScore || 0) * decayFactor + score); // Decay old score and add new, plafonné à 100
892
+ // Logique de décroissance et de score final
893
+ let newPatternScore = deviceData.lastPatternScore || 0;
1032
894
 
1033
- // If there hasn't been a request in a while, reset the pattern score.
1034
- if (history.length > 1 && (now - history[history.length - 2].timestamp > inactivityReset)) { // X ms inactivity
1035
- deviceData.lastPatternScore = 0;
895
+ if (timeSinceLast > inactivityReset) {
896
+ newPatternScore = 0; // Réinitialisation complète après une longue inactivité
897
+ } else {
898
+ newPatternScore *= decayFactor;
1036
899
  }
900
+ newPatternScore = Math.max(0, newPatternScore);
1037
901
 
1038
- return { requestPatternScore: deviceData.lastPatternScore };
902
+ deviceData.lastPatternScore = newPatternScore + instantScore;
903
+
904
+ return { requestPatternScore: Math.min(100, deviceData.lastPatternScore) };
1039
905
  }
1040
906
 
1041
907
  const trapUrlTemplates = [
@@ -1365,20 +1231,44 @@ const BASE_TARGET = MAX_DIFFICULTY_TARGET >> 16n;
1365
1231
  * @param {number} suspicionFactor - A number from 0 to 1.
1366
1232
  * @returns {BigInt} The target number.
1367
1233
  */
1368
- function calculateTarget(suspicionFactor) {
1234
+ function calculateTarget(suspicionFactor, securityConfig = {}) {
1369
1235
  // Difficulty range adjusted to be realistic.
1370
1236
  // MIN_DIFFICULTY: Fast enough not to bother a slightly suspicious user.
1371
1237
  // MAX_DIFFICULTY: Slow enough to heavily penalize a bot, but feasible for a patient human (5-30s).
1372
- const MIN_DIFFICULTY_BITS = 18; // Default value, should be configurable
1373
- const MAX_DIFFICULTY_BITS = 26; // Default value, should be configurable
1238
+ // NOUVEAU: La difficulté est maintenant configurable.
1239
+ const { cpu: cpuConfig = {} } = securityConfig;
1240
+ const MIN_DIFFICULTY_BITS = cpuConfig.minDifficultyBits ?? 8;
1241
+ const MAX_DIFFICULTY_BITS = cpuConfig.maxDifficultyBits ?? 16;
1374
1242
 
1375
1243
  // Use linear interpolation between min and max difficulty.
1376
1244
  const totalDifficultyBits =
1377
1245
  MIN_DIFFICULTY_BITS +
1378
1246
  suspicionFactor * (MAX_DIFFICULTY_BITS - MIN_DIFFICULTY_BITS);
1247
+
1248
+ if (totalDifficultyBits <= 0) return 2n ** 256n - 1n; // Si la difficulté est nulle ou négative, la cible est maximale (aucun challenge).
1249
+
1250
+ // The correct way to calculate the target is to define the number of leading zero bits required.
1251
+ // A target for N bits of difficulty is 2^(256-N).
1252
+ // We can calculate this with a left-shift on 1.
1253
+ const shift = 256n - BigInt(Math.floor(totalDifficultyBits));
1254
+ return 1n << shift;
1255
+ }
1379
1256
 
1380
- // The target is max / 2^bits
1381
- return MAX_DIFFICULTY_TARGET >> BigInt(Math.floor(totalDifficultyBits));
1257
+ /**
1258
+ * @private
1259
+ * Crée le bloc de base pour le challenge CPU.
1260
+ * Ce buffer contient toutes les données sauf la solution.
1261
+ * @param {string} nonce
1262
+ * @param {string} clientSecret
1263
+ * @param {string} fingerprint
1264
+ * @returns {Buffer}
1265
+ */
1266
+ function createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint) {
1267
+ const sortedFingerprint = (fingerprint || '').split('|').filter(p => p).sort().join('|');
1268
+ // On concatène les chaînes, puis on les convertit en buffer une seule fois.
1269
+ // Cela garantit que le client et le serveur travaillent sur la même base binaire.
1270
+ const messageBase = `${nonce}:${clientSecret}:${sortedFingerprint}:`; // Le ':' final est le séparateur pour la solution.
1271
+ return Buffer.from(messageBase, 'utf8');
1382
1272
  }
1383
1273
 
1384
1274
  /**
@@ -1389,12 +1279,15 @@ export function generateCpuTargetChallenge(
1389
1279
  nonce,
1390
1280
  suspicionFactor,
1391
1281
  originalUrl,
1282
+ securityConfig,
1392
1283
  ) {
1393
- const target = calculateTarget(suspicionFactor);
1284
+ const target = calculateTarget(suspicionFactor, securityConfig);
1285
+ // Le baseBlock est créé ici et sera stocké dans le contexte du challenge.
1286
+ const baseBlock = createCpuChallengeBaseBlock(nonce, null, ''); // Pour le challenge simple, le secret et le fingerprint sont vides.
1394
1287
  return {
1395
1288
  type: "cpu_target",
1396
1289
  nonce: nonce,
1397
- target: target.toString(16), // Send the target in hexadecimal
1290
+ target: target.toString(16),
1398
1291
  path: originalUrl,
1399
1292
  };
1400
1293
  }
@@ -1419,10 +1312,11 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
1419
1312
  async function solve() {
1420
1313
  const clientIp = ${JSON.stringify(clientIp)};
1421
1314
  const nonce = ${JSON.stringify(nonce)};
1422
- const cpuTarget = BigInt("0x${target}");
1423
- const solution = await window.solveCpuChallengeInline(clientIp, nonce, cpuTarget, null, (progress) => {
1424
- // Optional progress callback
1425
- });
1315
+ const cpuTarget = BigInt("0x" + "${target}");
1316
+ // La nouvelle version de solveCpuChallengeInline n'a plus besoin de l'IP ou du secret,
1317
+ // car tout est dans le baseBlock. Pour la compatibilité de ce challenge simple, on passe null.
1318
+ const baseBlockBytes = new TextEncoder().encode(nonce + ":");
1319
+ const solution = await window.solveCpuChallengeInline(baseBlockBytes, cpuTarget, (progress) => {});
1426
1320
  window.location.href = ${JSON.stringify(path)} + "?pow_type=cpu_target&pow_nonce=" + nonce + "&pow_solution=" + solution;
1427
1321
  }
1428
1322
  solve();
@@ -1437,9 +1331,14 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
1437
1331
  * @param {string} clientIp - The client's IP address.
1438
1332
  * @returns {string} HTML content.
1439
1333
  */
1440
- function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapContainerHtml) {
1334
+ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapContainerHtml, originalFingerprint) {
1441
1335
  const { nonce, target, path } = cpuChallengeDetails;
1442
1336
  const solverCode = getPowSolverCode();
1337
+ // On prépare le baseBlock pour le client. Il sera envoyé sous forme de tableau d'octets.
1338
+ // Le fingerprint est maintenant passé directement en paramètre.
1339
+ const fingerprint = originalFingerprint;
1340
+ const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint);
1341
+ const baseBlockBytes = `[${baseBlock.toString('utf8').split('').map(c => c.charCodeAt(0)).join(',')}]`;
1443
1342
 
1444
1343
  const challengeScript = `
1445
1344
  async function solve() {
@@ -1447,22 +1346,18 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
1447
1346
  const path = ${JSON.stringify(path)};
1448
1347
  const clientSecret = ${JSON.stringify(clientSecret)};
1449
1348
  const clientIp = ${JSON.stringify(clientIp)};
1450
- const cpuTarget = BigInt("0x${target}");
1349
+ const cpuTarget = BigInt("0x" + "${target}");
1451
1350
  const memDifficulty = ${memoryDifficulty};
1452
- // The client-side fingerprint library must be available to generate the fingerprint
1453
- // of the machine solving the challenge. This assumes a client library is loaded.
1454
- // We need a function to get the client fingerprint. Let's assume it's available on window.
1455
- const getClientFingerprint = () => (window.ClientLibrary && typeof window.ClientLibrary.getDeviceFingerprint === 'function') ? window.ClientLibrary.getDeviceFingerprint() : '';
1456
- const fingerprint = getClientFingerprint();
1351
+ // Le client reçoit directement le 'baseBlock' sous forme de tableau d'octets.
1352
+ // Il n'a plus besoin de construire le message lui-même.
1353
+ const baseBlock = new Uint8Array(${baseBlockBytes});
1457
1354
 
1458
1355
  // --- CPU Challenge ---
1459
- document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
1460
- const cpuSolution = await window.solveCpuChallengeInline(clientIp, nonce, cpuTarget, clientSecret, fingerprint, (progress) => {
1461
- // Optional progress callback
1462
- });
1356
+ document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...'; const cpuSolution = await window.solveCpuChallengeInline(baseBlock, cpuTarget, (progress) => {});
1357
+
1463
1358
  // --- Memory Challenge ---
1464
1359
  document.getElementById('loader').innerText = '⚙️ Performing memory allocation and calculation... (' + memDifficulty + ' MB)';
1465
- await new Promise(r => setTimeout(r, 10)); // Yield to update UI
1360
+ await new Promise(r => setTimeout(r, 10)); // Yield to update UI
1466
1361
  let memSolution = 0;
1467
1362
  try {
1468
1363
  const memSeed = nonce + ":" + clientSecret;
@@ -1471,7 +1366,10 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
1471
1366
  document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
1472
1367
  return;
1473
1368
  }
1474
- window.location.href = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution + "&pow_fp=" + encodeURIComponent(fingerprint);
1369
+
1370
+ // Redirect with both solutions and the fingerprint used to solve.
1371
+ const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
1372
+ window.location.href = finalUrl;
1475
1373
  }
1476
1374
  solve();
1477
1375
  `;
@@ -1502,23 +1400,56 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
1502
1400
  */
1503
1401
  export function verifyCpuTargetPoWAndGenerateTicket(
1504
1402
  clientIp, // This parameter is crucial and must be the actual client IP
1505
- ticketTtl, // NOUVEAU: Durée de validité du ticket (TTL en ms) configurable
1403
+ ticketTtl,
1506
1404
  nonce,
1507
1405
  solution,
1508
- clientSecret, // Le secret est maintenant requis
1509
- target, // La cible est maintenant passée directement en hexadécimal,
1510
- fingerprint, // Le fingerprint du SOLVER, soumis par le client
1406
+ challengeContext = {}, // Le contexte complet du challenge est maintenant passé
1511
1407
  ) {
1512
- const message = clientSecret
1513
- ? `${nonce}:${solution}:${clientSecret}:${fingerprint}`
1514
- : `${clientIp}:${nonce}:${solution}`; // L'IP est utilisée uniquement pour les challenges sans secret (plus anciens/simples)
1408
+ const { cpuTarget, baseBlock } = challengeContext;
1409
+ if (!cpuTarget || !baseBlock) {
1410
+ console.error('[FP Server Verify] Invalid challenge context. Missing cpuTarget or baseBlock.');
1411
+ return null;
1412
+ }
1413
+
1414
+ // Le baseBlock est déjà un Buffer ou un tableau d'octets.
1415
+ // On s'assure que c'est un Buffer pour la concaténation.
1416
+ const baseBlockBuffer = Buffer.isBuffer(baseBlock) ? baseBlock : Buffer.from(baseBlock);
1417
+ const solutionBuffer = Buffer.from(String(solution), 'utf8');
1418
+
1419
+ // Concaténation binaire directe. C'est la garantie de cohérence.
1420
+ const finalBlock = Buffer.concat([baseBlockBuffer, solutionBuffer]);
1421
+
1515
1422
  const hash = crypto
1516
1423
  .createHash("sha256")
1517
- .update(message)
1424
+ .update(finalBlock)
1518
1425
  .digest("hex");
1519
1426
  const hashAsInt = BigInt("0x" + hash);
1427
+ const targetAsInt = BigInt("0x" + cpuTarget);
1428
+
1429
+ // --- NOUVEAUX LOGS POUR LE DÉBOGAGE ---
1430
+ console.log('[FP Server Verify] Intermediate values:', {
1431
+ hashCalculated: `0x${hash}`,
1432
+ hashAsInt: hashAsInt.toString(), // Log as string to see full value
1433
+ target: `0x${cpuTarget}`,
1434
+ targetAsInt: targetAsInt.toString(), // Log as string to see full value
1435
+ });
1436
+ // --- FIN DES NOUVEAUX LOGS ---
1520
1437
 
1521
- if (hashAsInt < BigInt("0x" + target)) {
1438
+ const isValid = hashAsInt < targetAsInt;
1439
+
1440
+ // --- AJOUT DE LOGS POUR LE DÉBOGAGE ---
1441
+ if (!isValid) {
1442
+ console.log('[FP Server Verify] CPU PoW verification FAILED. Details:', {
1443
+ hashCalculated: `0x${hash}`,
1444
+ target: `0x${cpuTarget}`,
1445
+
1446
+ });
1447
+ }
1448
+ // --- FIN DES LOGS ---
1449
+
1450
+ if (isValid) {
1451
+ console.log('[FP Server Verify] CPU PoW verification PASSED. Details:', {
1452
+ });
1522
1453
  // The comparison is direct with native BigInts
1523
1454
  // The proof is valid, generate the ticket
1524
1455
  const expiry = Date.now() + (ticketTtl || 3600000); // Calcule l'expiration à partir du TTL
@@ -1593,6 +1524,46 @@ export class FingerprintEngine {
1593
1524
  }
1594
1525
  return blockList;
1595
1526
  }
1527
+
1528
+ /**
1529
+ * Checks if the client's IP resolves to any of the hostnames in the hostname allowlist.
1530
+ * The result is cached to avoid repeated DNS lookups.
1531
+ * @private
1532
+ * @param {string} clientIp - The IP address of the client.
1533
+ * @returns {Promise<boolean>} True if the IP is in the hostname allowlist.
1534
+ */
1535
+ async _isIpInHostnameAllowlist(clientIp) {
1536
+ const { whitelist = [] } = this.securityConfig;
1537
+ const hostnameRule = whitelist.find(rule => rule.type === 'hostname_allowlist');
1538
+
1539
+ if (!hostnameRule || !hostnameRule.entries || hostnameRule.entries.length === 0) {
1540
+ return false;
1541
+ }
1542
+
1543
+ const cacheKey = `ip-hostname-allowlist:${clientIp}`;
1544
+ const cachedStatus = await store.get(cacheKey);
1545
+
1546
+ if (cachedStatus === 'verified') return true;
1547
+ if (cachedStatus === 'failed') return false;
1548
+
1549
+ try {
1550
+ // Reverse DNS lookup to get hostnames for the IP
1551
+ const hostnames = await dns.reverse(clientIp);
1552
+
1553
+ // Check if any of the resolved hostnames is in our allowlist
1554
+ const isAllowed = hostnames.some(hostname => hostnameRule.entries.includes(hostname));
1555
+
1556
+ if (isAllowed) {
1557
+ await store.set(cacheKey, 'verified', 86400); // Cache success for 24h
1558
+ return true;
1559
+ }
1560
+ } catch (error) {
1561
+ // DNS errors (like no rDNS record) are treated as a failure.
1562
+ }
1563
+
1564
+ await store.set(cacheKey, 'failed', 86400); // Cache failure for 24h
1565
+ return false;
1566
+ }
1596
1567
  _isIpInAllowlist(clientIp) {
1597
1568
  return this._allowlist.check(clientIp);
1598
1569
  }
@@ -1678,6 +1649,12 @@ export class FingerprintEngine {
1678
1649
  return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'allowlist' } };
1679
1650
  }
1680
1651
 
1652
+ // 2. Check hostname-based allowlist.
1653
+ if (await this._isIpInHostnameAllowlist(clientIp)) {
1654
+ this._log('IP resolves to a whitelisted hostname - allowing request', { clientIp });
1655
+ return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'hostname_allowlist' } };
1656
+ }
1657
+
1681
1658
  const { pow_nonce } = query;
1682
1659
 
1683
1660
  // Honeypot: Direct probing of challenge endpoints is highly suspicious.
@@ -1706,7 +1683,7 @@ export class FingerprintEngine {
1706
1683
  if (deviceData?.condemned) {
1707
1684
  this._log('Device condemned - blocking request', { deviceId });
1708
1685
  if (onDeviceCompromised) {
1709
- onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Previously condemned', score: 100, vector: { honeypotScore: 100 } });
1686
+ onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Previously condemned', score: 100, vector: { honeypotScore: 100 } });
1710
1687
  }
1711
1688
  return { action: 'block', status: 404, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
1712
1689
  }
@@ -1768,7 +1745,7 @@ export class FingerprintEngine {
1768
1745
  // Si une solution de challenge est soumise, on la traite en priorité absolue,
1769
1746
  // avant même de recalculer le score de suspicion.
1770
1747
  const { pow_type, pow_solution, pow_solution_cpu, pow_solution_mem, pow_fp, pow_solution_population, pow_solution_work_result, pow_problem_id } = query;
1771
- if (pow_nonce && (pow_solution || (pow_solution_cpu && pow_solution_mem))) {
1748
+ if (pow_nonce && (pow_solution || pow_solution_cpu)) { // Vérifie pow_solution pour la compatibilité ascendante
1772
1749
  this._log('Challenge solution submitted', { pow_type, pow_nonce });
1773
1750
 
1774
1751
  // On doit calculer le score de suspicion *avant* de valider le ticket,
@@ -1795,14 +1772,33 @@ export class FingerprintEngine {
1795
1772
  if (challengeContext) {
1796
1773
  // *** NOUVELLE VÉRIFICATION CRUCIALE ***
1797
1774
  // On compare le fingerprint soumis par le solver (`pow_fp`) avec celui stocké
1798
- // lors de l'émission du challenge (`challengeContext.fingerprint`).
1799
- const solverFingerprint = pow_fp;
1800
- const originalFingerprint = challengeContext.fingerprint;
1775
+ // lors de l'émission du challenge.
1776
+ // --- FIX: Use submitted fingerprint, but fallback to current request's fingerprint ---
1777
+ // This handles API clients that might not use the full client-side library but still solve the challenge.
1778
+ const solverFingerprint = pow_fp || getCompositeDeviceHash(requestContext);
1779
+ const originalFingerprint = challengeContext.fingerprint; // This is the fingerprint of the request that *triggered* the challenge
1780
+
1781
+ let similarity;
1782
+ const similarityThreshold = this.securityConfig.similarityThreshold ?? 0.95;
1783
+
1784
+ // If fingerprints are simple strings (like test placeholders 'fp-probation')
1785
+ // and don't contain the typical structure, fall back to a strict equality check.
1786
+ if (!originalFingerprint?.includes(':') || !solverFingerprint?.includes(':')) {
1787
+ similarity = (originalFingerprint === solverFingerprint) ? 1.0 : 0.0;
1788
+ } else {
1789
+ // Use the weighted comparison for structured fingerprints.
1790
+ // We compare the fingerprint of the request that triggered the challenge
1791
+ // with the fingerprint of the request that is submitting the solution.
1792
+ // They should be very similar.
1793
+ similarity = FingerprintBuilder.compare(originalFingerprint, getCompositeDeviceHash(requestContext));
1794
+ }
1801
1795
 
1802
- if (solverFingerprint !== originalFingerprint) {
1796
+ if (similarity < similarityThreshold) {
1803
1797
  this._log('Fingerprint mismatch - challenge solved on a different machine!', {
1804
1798
  original: originalFingerprint,
1805
1799
  solver: solverFingerprint,
1800
+ similarity: similarity.toFixed(4),
1801
+ threshold: similarityThreshold
1806
1802
  });
1807
1803
  isValid = false;
1808
1804
  } else {
@@ -1810,13 +1806,13 @@ export class FingerprintEngine {
1810
1806
  finalTtl = isProbationary ? probationaryTtl : optimalTtl;
1811
1807
  this._log('Challenge context found, verifying solution', { optimalTtl, finalTtl });
1812
1808
 
1813
- if (pow_type === "cpu_target" && pow_solution) {
1814
- ticket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution, challengeContext.clientSecret, challengeContext.cpuTarget, solverFingerprint);
1809
+ if ((pow_type === "cpu_target" || !pow_type) && (pow_solution_cpu || pow_solution)) { // !pow_type pour compatibilité
1810
+ const cpuSolution = pow_solution_cpu || pow_solution;
1811
+ ticket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, cpuSolution, challengeContext);
1815
1812
  isValid = ticket !== null;
1816
1813
  this._log('CPU target challenge verification', { isValid });
1817
- } else if (pow_type === "cpu_mem" && pow_solution_cpu && pow_solution_mem) {
1818
- const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution_cpu, challengeContext.clientSecret, challengeContext.cpuTarget, solverFingerprint);
1819
- const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret);
1814
+ } else if (pow_type === "cpu_mem" && pow_solution_cpu && pow_solution_mem) { const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution_cpu, challengeContext);
1815
+ const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret); // Memory PoW is independent of fingerprint
1820
1816
  isValid = cpuTicket !== null && isMemValid;
1821
1817
  if (isValid) ticket = cpuTicket; // Le ticket est le même, on le réutilise
1822
1818
  this._log('Combined CPU+Memory challenge verification', {
@@ -1835,12 +1831,12 @@ export class FingerprintEngine {
1835
1831
  this._log('Challenge solution valid - issuing ticket', { ticketMaxAge: finalTtl, isProbationary });
1836
1832
 
1837
1833
  if (logger) {
1838
- logger({ type: 'challenge_solved', deviceId: cookies?.device_id, score: preliminaryScore, challengeType: pow_type, timestamp: Date.now() });
1834
+ logger({ type: 'challenge_solved', deviceId: cookies?.device_id, score: preliminaryScore, challengeType: pow_type, timestamp: Date.now(), vector: preliminaryVector });
1839
1835
  }
1840
1836
 
1841
1837
  // NOUVELLE LOGIQUE DE REDIRECTION (plus robuste)
1842
1838
  // 1. On part du chemin original stocké, qui peut contenir des query params.
1843
- const originalUrl = new URL(challengeContext?.originalPath || path, `http://${requestContext.headers.host || 'localhost'}`);
1839
+ const originalUrl = new URL(challengeContext?.originalPath || requestContext.path, `http://${requestContext.headers.host || 'localhost'}`);
1844
1840
  // 2. On crée un nouvel objet de paramètres à partir de la requête entrante (qui contient les solutions ET les params originaux).
1845
1841
  const finalSearchParams = new URLSearchParams(requestContext.query);
1846
1842
 
@@ -1851,6 +1847,10 @@ export class FingerprintEngine {
1851
1847
  finalSearchParams.delete('pow_solution_cpu');
1852
1848
  finalSearchParams.delete('pow_solution_mem');
1853
1849
  finalSearchParams.delete('pow_fp'); // Ne pas oublier de nettoyer le fingerprint
1850
+ // NOUVEAU: Nettoyer aussi les paramètres des challenges d'optimisation et de travail utile
1851
+ finalSearchParams.delete('pow_solution_population');
1852
+ finalSearchParams.delete('pow_solution_work_result');
1853
+ finalSearchParams.delete('pow_problem_id');
1854
1854
 
1855
1855
  // 4. On reconstruit le chemin final.
1856
1856
  const finalQueryString = finalSearchParams.toString();
@@ -1873,6 +1873,13 @@ export class FingerprintEngine {
1873
1873
  this._log('Challenge solution invalid', { pow_nonce });
1874
1874
  suspicionVector.honeypotScore = 100; // Invalid solution is a strong bot signal.
1875
1875
  finalScore = this.calculateFinalScore(suspicionVector);
1876
+ // --- FIX: After invalidating a solution, immediately check if the new score triggers a block ---
1877
+ const newBlockThreshold = thresholds.block ?? 95;
1878
+ if (finalScore >= newBlockThreshold) {
1879
+ this._log('Request blocked after invalid challenge solution', { finalScore, newBlockThreshold });
1880
+ return { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
1881
+ }
1882
+ // If not blocked, the request will proceed to be re-challenged.
1876
1883
  }
1877
1884
  } else if (pow_nonce && pow_type === 'optimization_task' && pow_solution_population) {
1878
1885
  this._log('Optimization task solution submitted', { pow_nonce });
@@ -1935,7 +1942,10 @@ export class FingerprintEngine {
1935
1942
  if (isBlocked) {
1936
1943
  this._log('Request blocked - score exceeded block threshold', { finalScore, blockThreshold });
1937
1944
  if (onDeviceCompromised) {
1938
- onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Score exceeded block threshold', score: finalScore, vector: suspicionVector });
1945
+ onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Score exceeded block threshold', score: finalScore, vector: suspicionVector });
1946
+ }
1947
+ if (logger) {
1948
+ logger({ type: 'request_blocked', deviceId: deviceId, score: finalScore, vector: suspicionVector, timestamp: Date.now() });
1939
1949
  }
1940
1950
  return { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
1941
1951
  }
@@ -1947,16 +1957,27 @@ export class FingerprintEngine {
1947
1957
  this._log('Honeypot trap URL triggered - condemning device', { path, deviceId });
1948
1958
  deviceData.condemned = true; // This device is a bot. Condemn it.
1949
1959
  if (onDeviceCompromised) {
1950
- onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Triggered signed honeypot trap URL', score: 100, vector: { honeypotScore: 100 } });
1960
+ onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Triggered signed honeypot trap URL', score: 100, vector: { honeypotScore: 100 } });
1951
1961
  }
1952
1962
  if (logger) {
1953
- logger({ type: 'trap_triggered', deviceId: cookies?.device_id, score: 100, path: path, timestamp: Date.now() });
1963
+ logger({ type: 'trap_triggered', deviceId: cookies?.device_id, score: 100, path: path, timestamp: Date.now(), vector: { honeypotScore: 100 } });
1954
1964
  }
1955
1965
  await store.set(`device:${cookies.device_id}`, deviceData); // No TTL for condemned status
1956
1966
  return { action: 'block', status: 404, score: 100, vector: { honeypotScore: 100 } };
1957
1967
  }
1958
-
1959
- if (isSuspicious && !isTicketValid(clientIp, powCookie)) { // The powCookie check is now after the PoW solution check
1968
+
1969
+ // --- NOUVELLE LOGIQUE DE RE-CHALLENGE ---
1970
+ // Un challenge est nécessaire si :
1971
+ // 1. La requête est suspecte ET il n'y a pas de ticket valide.
1972
+ // OU
1973
+ // 2. La requête est *très* suspecte (dépasse le seuil 'high'), ce qui annule la validité du ticket actuel.
1974
+ const hasValidTicket = isTicketValid(clientIp, powCookie);
1975
+ const mustReChallenge = isSuspiciousHigh && hasValidTicket;
1976
+
1977
+ if (isSuspicious && (!hasValidTicket || mustReChallenge)) {
1978
+ if (mustReChallenge) {
1979
+ this._log('High suspicion score detected - overriding valid ticket to re-issue challenge', { finalScore, deviceId });
1980
+ }
1960
1981
  this._log('Suspicious request without valid ticket - issuing challenge', { finalScore, hasPowCookie: !!powCookie });
1961
1982
  // Honeypot: Direct probing of challenge endpoints is highly suspicious.
1962
1983
  // A legitimate user only hits these endpoints via the challenge page itself.
@@ -1966,7 +1987,7 @@ export class FingerprintEngine {
1966
1987
  if (pow_nonce && !isChallengeResponse) {
1967
1988
  this._log('Honeypot probe detected - blocking request', { path, pow_nonce });
1968
1989
  if (logger) {
1969
- logger({ type: 'honeypot_probe', deviceId: cookies?.device_id, score: finalScore, path: path, timestamp: Date.now() });
1990
+ logger({ type: 'honeypot_probe', deviceId: cookies?.device_id, score: finalScore, path: path, timestamp: Date.now(), vector: suspicionVector });
1970
1991
  }
1971
1992
  suspicionVector.honeypotScore = 100; // Bot is probing. Max penalty.
1972
1993
  // Recalculate the final score with the updated vector.
@@ -2002,8 +2023,8 @@ export class FingerprintEngine {
2002
2023
  const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce));
2003
2024
  const trapLinksHtml = trapUrls.map(url => `<a href="${url}" tabindex="-1">config</a>`).join(' ');
2004
2025
 
2005
-
2006
- const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path);
2026
+ // On passe la configuration pour que la difficulté soit calculée correctement.
2027
+ const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
2007
2028
 
2008
2029
  // La difficulté mémoire démarre à 0 et augmente seulement après un certain seuil de suspicion.
2009
2030
  // Par exemple, elle ne commence à augmenter qu'à partir de 25% du chemin entre 'low' et 'high'.
@@ -2019,16 +2040,18 @@ export class FingerprintEngine {
2019
2040
  memDifficulty,
2020
2041
  cpuTarget: cpuChallengeDetails.target
2021
2042
  });
2022
- // (NOUVEAU) On stocke le fingerprint de la requête qui a déclenché le challenge.
2023
- const originalFingerprint = requestContext.headers['x-device-fingerprint'] || getCompositeDeviceHash(requestContext);
2043
+ // (NOUVEAU) On stocke le fingerprint de la requête qui a déclenché le challenge. We call it via __internal to allow mocking.
2044
+ const originalFingerprint = requestContext.headers['x-device-fingerprint'] || __internal.getCompositeDeviceHash(requestContext);
2024
2045
 
2025
2046
  // Store the entire challenge context with a short TTL (e.g., 5 minutes)
2047
+ const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, originalFingerprint);
2026
2048
  await store.set(`secret:${nonce}`, {
2027
2049
  clientSecret,
2028
2050
  cpuTarget: cpuChallengeDetails.target,
2029
2051
  suspicionScore: finalScore, // *** FIX: Store the score that triggered the challenge ***
2030
2052
  fingerprint: originalFingerprint, // *** NOUVEAU ***
2031
2053
  memDifficulty: memDifficulty,
2054
+ baseBlock: baseBlock, // *** NOUVEAU: Le bloc de base est stocké pour la vérification ***
2032
2055
  originalPath: path, // *** FIX: Store the original path ***
2033
2056
  }, this.securityConfig.challengeTtl || 300); // NOUVEAU: TTL configurable (5min par défaut)
2034
2057
 
@@ -2045,7 +2068,7 @@ export class FingerprintEngine {
2045
2068
  });
2046
2069
 
2047
2070
  if (logger) {
2048
- logger({ type: 'challenge_issued', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now() });
2071
+ logger({ type: 'challenge_issued', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now(), vector: suspicionVector });
2049
2072
  }
2050
2073
 
2051
2074
  // Check if the request is an API request to return a JSON challenge
@@ -2060,13 +2083,14 @@ export class FingerprintEngine {
2060
2083
  clientSecret: clientSecret, // The client needs this to solve the challenge
2061
2084
  cpuTarget: cpuChallengeDetails.target,
2062
2085
  memDifficulty: memDifficulty,
2086
+ baseBlock: [...baseBlock], // Envoyer le buffer comme un tableau d'octets
2063
2087
  }
2064
2088
  };
2065
2089
  this._log('API challenge response generated', { challengePayload });
2066
2090
  return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
2067
2091
  } else {
2068
2092
  // For browsers, send the HTML page.
2069
- const trapContainer = `<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">${trapLinksHtml}</div>`; const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret, this.securityConfig, trapContainer);
2093
+ const trapContainer = `<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">${trapLinksHtml}</div>`; const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret, this.securityConfig, trapContainer, originalFingerprint);
2070
2094
  this._log('Browser challenge page generated', {
2071
2095
  pageLength: page.length,
2072
2096
  hasTrapContainer: true
@@ -2080,7 +2104,7 @@ export class FingerprintEngine {
2080
2104
  this._log('Request passed - no challenge required', { finalScore, hasValidTicket: isTicketValid(clientIp, powCookie) });
2081
2105
 
2082
2106
  if (logger) {
2083
- logger({ type: 'request_passed', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now() });
2107
+ logger({ type: 'request_passed', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now(), vector: suspicionVector });
2084
2108
  }
2085
2109
 
2086
2110
  return { action: 'next', score: finalScore, vector: suspicionVector };
@@ -2523,115 +2547,58 @@ let autoTuningJobId = null;
2523
2547
  * @param {object} securityConfig - The security configuration object to update.
2524
2548
  * @param {Array<object>} trafficData - The array containing traffic logs.
2525
2549
  * @param {number} minDataPoints - The minimum number of data points required to start optimization.
2550
+ * @param {number} maxDataPoints - The maximum number of data points to keep after an optimization cycle.
2526
2551
  */
2527
- function runThresholdOptimization(securityConfig, trafficData, minDataPoints) {
2528
- if (trafficData.length < minDataPoints) {
2529
- console.log(`[AutoTuning] Reporté : ${trafficData.length}/${minDataPoints} points de données.`);
2530
- return;
2531
- }
2532
- console.log(`[AutoTuning] Démarrage du cycle d'optimisation avec ${trafficData.length} points de données.`);
2533
-
2534
- // Classify historical requests with a confidence weight.
2535
- const solvedDevices = new Set(trafficData.filter(e => e.type === 'challenge_solved').map(e => e.deviceId));
2536
- const challengedDevices = new Set(trafficData.filter(e => e.type === 'challenge_issued').map(e => e.deviceId));
2537
-
2538
- const historicalRequests = trafficData.map(log => {
2539
- // Assign a label ('bot' or 'human') and a confidence weight to each log entry.
2540
- switch (log.type) {
2541
- case 'honeypot_probe':
2542
- case 'trap_triggered':
2543
- return { score: log.score, label: 'bot', confidence: 10.0 }; // Very high confidence
2544
-
2545
- case 'challenge_issued':
2546
- // A challenge issued to a device that never solved it is a strong bot signal.
2547
- if (!solvedDevices.has(log.deviceId)) {
2548
- return { score: log.score, label: 'bot', confidence: 3.0 }; // High confidence
2549
- }
2550
- // If the challenge was eventually solved, this specific log is neutral.
2551
- return null;
2552
+ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints) {
2553
+ if (trafficData.length < minDataPoints) {
2554
+ console.log(`[AutoTuning] Reporté : ${trafficData.length}/${minDataPoints} points de données.`);
2555
+ return;
2556
+ }
2552
2557
 
2553
- case 'challenge_solved':
2554
- return { score: log.score, label: 'human', confidence: 5.0 }; // High confidence
2558
+ if (trafficData.length > maxDataPoints) {
2559
+ console.log(`[AutoTuning] Le journal de trafic a atteint ${trafficData.length} entrées (max: ${maxDataPoints}). Troncation des données les plus anciennes.`);
2560
+ trafficData.splice(0, trafficData.length - maxDataPoints);
2561
+ }
2555
2562
 
2556
- case 'request_passed':
2557
- // A passed request from a device that was never even challenged is likely a human.
2558
- if (!challengedDevices.has(log.deviceId)) {
2559
- return { score: log.score, label: 'human', confidence: 0.5 }; // Low confidence
2560
- }
2561
- // If the device was challenged at some point, this log is ambiguous.
2562
- return null;
2563
-
2564
- default:
2565
- return null;
2566
- }
2567
- }).filter(Boolean); // Remove null entries
2568
-
2569
- // The "fitness" function evaluates the quality of a set of thresholds.
2570
- // A lower score is better.
2571
- const fitnessFunction = (solution) => {
2572
- const [low, medium, high, velocityThreshold, burstThreshold, scrapeThreshold] = solution;
2573
- if (low >= medium || medium >= high || low < 10 || high > 90) return Infinity;
2574
- if (velocityThreshold < 50 || velocityThreshold > burstThreshold || burstThreshold > scrapeThreshold) return Infinity;
2575
-
2576
- let weightedFalsePositives = 0; // Humans challenged unnecessarily.
2577
- let weightedFalseNegatives = 0; // Undetected bots.
2578
-
2579
- for (const req of historicalRequests) {
2580
- if (req.label === 'bot') {
2581
- if (req.score < low) weightedFalseNegatives += req.confidence;
2582
- } else { // 'human'
2583
- if (req.score >= low) weightedFalsePositives += req.confidence;
2584
- }
2585
- }
2586
- // The penalty for false negatives is implicitly higher due to the higher confidence scores of bot signals.
2587
- return weightedFalsePositives + weightedFalseNegatives;
2588
- };
2563
+ console.log(`[AutoTuning] Démarrage du cycle d'optimisation complet avec ${trafficData.length} points de données.`);
2589
2564
 
2590
- // Functions for the genetic algorithm.
2591
- const createIndividual = () => [
2592
- 10 + Math.random() * 20, // low
2593
- 30 + Math.random() * 30, // medium
2594
- 60 + Math.random() * 30, // high
2595
- 100 + Math.random() * 150, // velocityThreshold (100-250ms)
2596
- 300 + Math.random() * 400, // burstThreshold (300-700ms)
2597
- 800 + Math.random() * 700, // scrapeThreshold (800-1500ms)
2598
- ];
2599
- const crossover = (p1, p2) => p1.map((val, i) => (val + p2[i]) / 2);
2600
- const mutate = (s) => {
2601
- const n = [...s];
2602
- const i = Math.floor(Math.random() * n.length);
2603
- // Adjust mutation range based on parameter
2604
- const mutationRange = i < 3 ? 5 : 50;
2605
- n[i] += (Math.random() - 0.5) * mutationRange;
2606
- return n;
2607
- };
2565
+ const paretoFront = Optimization.Operators.solveFullSecurityTuning({ trafficData });
2608
2566
 
2609
- // Start optimization.
2610
- const result = Optimization.geneticAlgorithm(createIndividual, fitnessFunction, crossover, mutate, {
2611
- generations: 50,
2612
- populationSize: 40
2613
- });
2567
+ if (!paretoFront || paretoFront.length === 0) {
2568
+ console.warn("[AutoTuning] L'optimisation n'a retourné aucune solution.");
2569
+ return;
2570
+ }
2571
+
2572
+ // Stratégie de sélection : choisir la solution la plus équilibrée du front de Pareto.
2573
+ // On cherche la solution la plus proche de l'origine (0,0) dans l'espace des objectifs.
2574
+ let bestSolution = paretoFront[0];
2575
+ let minDistance = Math.sqrt(Math.pow(bestSolution.objectives[0], 2) + Math.pow(bestSolution.objectives[1], 2));
2576
+
2577
+ for (let i = 1; i < paretoFront.length; i++) {
2578
+ const distance = Math.sqrt(Math.pow(paretoFront[i].objectives[0], 2) + Math.pow(paretoFront[i].objectives[1], 2));
2579
+ if (distance < minDistance) {
2580
+ minDistance = distance;
2581
+ bestSolution = paretoFront[i];
2582
+ }
2583
+ }
2614
2584
 
2615
- const [newLow, newMedium, newHigh, newVelocity, newBurst, newScrape] = result.solution;
2585
+ // Appliquer la nouvelle configuration optimisée
2586
+ const newConfig = bestSolution.solution;
2616
2587
 
2617
- // Update the configuration live.
2618
- // Ensure thresholds object exists
2619
- if (!securityConfig.thresholds) securityConfig.thresholds = {};
2620
- securityConfig.thresholds.low = Math.round(newLow);
2621
- securityConfig.thresholds.medium = Math.round(newMedium);
2622
- securityConfig.thresholds.high = Math.round(newHigh);
2588
+ // S'assurer que les objets de configuration existent avant d'utiliser Object.assign
2589
+ if (!securityConfig.thresholds) securityConfig.thresholds = {};
2590
+ if (!securityConfig.weights) securityConfig.weights = {};
2591
+ if (!securityConfig.patterns) securityConfig.patterns = {};
2623
2592
 
2624
- // Update pattern detection parameters
2625
- if (!securityConfig.patterns) securityConfig.patterns = {};
2626
- securityConfig.patterns.velocityThreshold = Math.round(newVelocity);
2627
- securityConfig.patterns.burstThreshold = Math.round(newBurst);
2628
- securityConfig.patterns.scrapeThreshold = Math.round(newScrape);
2629
- // Weights could also be optimized, but let's keep it to thresholds for now for simplicity.
2593
+ Object.assign(securityConfig.thresholds, newConfig.thresholds || {});
2594
+ Object.assign(securityConfig.weights, newConfig.weights || {});
2595
+ Object.assign(securityConfig.patterns, newConfig.patterns || {});
2630
2596
 
2631
- console.log("[AutoTuning] Nouveaux seuils optimisés appliqués :", securityConfig.thresholds);
2632
- if (securityConfig.patterns) {
2633
- console.log("[AutoTuning] Nouveaux paramètres de pattern appliqués :", securityConfig.patterns);
2634
- }
2597
+ console.log("[AutoTuning] Nouvelle configuration de sécurité optimisée appliquée.");
2598
+ console.log("[AutoTuning] Objectifs atteints :", { falsePositiveRate: bestSolution.objectives[0].toFixed(4), falseNegativeRate: bestSolution.objectives[1].toFixed(4) });
2599
+ console.log("[AutoTuning] Seuils :", securityConfig.thresholds);
2600
+ console.log("[AutoTuning] Poids :", securityConfig.weights);
2601
+ console.log("[AutoTuning] Patterns :", securityConfig.patterns);
2635
2602
  }
2636
2603
 
2637
2604
  /**
@@ -2641,7 +2608,8 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints) {
2641
2608
  * @param {object} options.securityConfig - The live security configuration object that will be mutated.
2642
2609
  * @param {Array<object>} options.trafficData - The array where the logger pushes traffic data.
2643
2610
  * @param {number} [options.interval=1800000] - The interval in milliseconds between each optimization cycle (default: 30 minutes).
2644
- * @param {number} [options.minDataPoints=200] - The minimum number of requests to analyze before starting a cycle (default: 200).
2611
+ * @param {number} [options.minDataPoints=200] - The minimum number of requests to have before starting a cycle (default: 200).
2612
+ * @param {number} [options.maxDataPoints=10000] - The maximum number of log entries to keep in memory (default: 10,000).
2645
2613
  */
2646
2614
  export function startThresholdAutoTuning(options) {
2647
2615
  if (autoTuningJobId) {
@@ -2652,8 +2620,9 @@ export function startThresholdAutoTuning(options) {
2652
2620
  const {
2653
2621
  securityConfig,
2654
2622
  trafficData,
2655
- interval = 1800000,
2656
- minDataPoints = 200
2623
+ interval = 1800000, // 30 minutes
2624
+ minDataPoints = 200,
2625
+ maxDataPoints = 10000 // Limite par défaut à 10 000 entrées
2657
2626
  } = options;
2658
2627
 
2659
2628
  if (!securityConfig || !trafficData) {
@@ -2663,7 +2632,7 @@ export function startThresholdAutoTuning(options) {
2663
2632
  console.log(`[AutoTuning] Job d'optimisation des seuils démarré. Prochain cycle dans ${interval / 60000} minutes.`);
2664
2633
 
2665
2634
  autoTuningJobId = setInterval(() => {
2666
- runThresholdOptimization(securityConfig, trafficData, minDataPoints);
2635
+ runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints);
2667
2636
  }, interval);
2668
2637
  }
2669
2638