@anonympins/fingerprint 0.2.1 → 0.2.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.
package/library.js CHANGED
@@ -1583,29 +1583,104 @@ Optimization.Operators.createFullSecurityConfigEvaluator = ({ trafficData }) =>
1583
1583
  * @returns {Array<{solution: object, objectives: number[]}>} Le front de Pareto des configurations optimales.
1584
1584
  */
1585
1585
  Optimization.Operators.solveFullSecurityTuning = (context, options = {}) => {
1586
- const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator(context); // eslint-disable-line
1586
+ const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator(context);
1587
1587
 
1588
1588
  // Un "individu" est un objet de configuration complet
1589
1589
  const createIndividual = () => ({
1590
1590
  thresholds: {
1591
- low: 10 + Math.random() * 30, // 10-40
1592
- medium: 40 + Math.random() * 30, // 40-70
1593
- high: 70 + Math.random() * 25, // 70-95
1591
+ low: 15 + Math.random() * 20, // 15-35
1592
+ medium: 40 + Math.random() * 25, // 40-65
1593
+ high: 70 + Math.random() * 20, // 70-90
1594
1594
  },
1595
1595
  weights: {
1596
1596
  historyScore: Math.random(),
1597
1597
  rotationScore: Math.random(),
1598
1598
  headerAnomalyScore: Math.random(),
1599
- requestPatternScore: Math.random(),
1599
+ requestPatternScore: 0.5 + Math.random(), // Donner plus d'importance aux patterns
1600
1600
  inconsistencyScore: Math.random(),
1601
1601
  honeypotScore: 1.0, // Garder le honeypot à 1.0 est une bonne pratique
1602
+ behaviorScore: Math.random(),
1603
+ crossLayerInconsistencyScore: Math.random(),
1604
+ timeInconsistencyScore: Math.random(),
1602
1605
  },
1603
- // On pourrait aussi faire muter les `patterns` ici
1606
+ patterns: {
1607
+ velocityThreshold: 100 + Math.random() * 400, // 100-500ms
1608
+ velocityWeight: 10 + Math.random() * 40,
1609
+ burstThreshold: 300 + Math.random() * 700, // 300-1000ms
1610
+ burstWeight: 20 + Math.random() * 40,
1611
+ scrapeThreshold: 500 + Math.random() * 1000, // 500-1500ms
1612
+ scrapeWeight: 15 + Math.random() * 35,
1613
+ sequenceLength: 3 + Math.floor(Math.random() * 3), // 3-5
1614
+ sequenceWeight: 20 + Math.random() * 50,
1615
+ regularityThreshold: 50 + Math.random() * 200, // 50-250ms
1616
+ regularityWeight: 20 + Math.random() * 40,
1617
+ decayFactor: 0.85 + Math.random() * 0.14, // 0.85-0.99
1618
+ inactivityReset: 15000 + Math.random() * 45000, // 15s-60s
1619
+ }
1604
1620
  });
1605
1621
 
1606
1622
  // Le crossover et la mutation doivent maintenant opérer sur des objets complexes
1607
- const crossover = (c1, c2) => { /* ... logique de croisement pour les objets de config ... */ return c1; }; // eslint-disable-line
1608
- const mutate = (c) => { /* ... logique de mutation pour les objets de config ... */ return c; }; // eslint-disable-line
1623
+ const crossover = (c1, c2) => {
1624
+ const child = JSON.parse(JSON.stringify(c1)); // Deep copy
1625
+ // Croisement pour chaque groupe de paramètres
1626
+ for (const key in child.thresholds) {
1627
+ child.thresholds[key] = (c1.thresholds[key] + c2.thresholds[key]) / 2;
1628
+ }
1629
+ for (const key in child.weights) {
1630
+ if (key !== 'honeypotScore') { // Ne pas croiser le poids du honeypot
1631
+ child.weights[key] = (c1.weights[key] + c2.weights[key]) / 2;
1632
+ }
1633
+ }
1634
+ for (const key in child.patterns) {
1635
+ child.patterns[key] = (c1.patterns[key] + c2.patterns[key]) / 2;
1636
+ }
1637
+ return child;
1638
+ };
1639
+
1640
+ const mutate = (c) => {
1641
+ const newConfig = JSON.parse(JSON.stringify(c));
1642
+
1643
+ // --- NOUVELLE LOGIQUE : Sélection de section pondérée ---
1644
+ // On donne plus de poids à la mutation des 'patterns' et des 'weights',
1645
+ // car ils ont un impact plus direct sur la détection que les seuils.
1646
+ const sections = [
1647
+ { name: 'patterns', weight: 0.5 }, // 50% de chance
1648
+ { name: 'weights', weight: 0.35 }, // 35% de chance
1649
+ { name: 'thresholds', weight: 0.15 } // 15% de chance
1650
+ ];
1651
+ const rand = Math.random();
1652
+ let cumulativeWeight = 0;
1653
+ let sectionToMutate = 'patterns'; // Fallback
1654
+ for (const section of sections) {
1655
+ cumulativeWeight += section.weight;
1656
+ if (rand < cumulativeWeight) {
1657
+ sectionToMutate = section.name;
1658
+ break;
1659
+ }
1660
+ }
1661
+
1662
+ const keys = Object.keys(newConfig[sectionToMutate]);
1663
+ const keyToMutate = keys[Math.floor(Math.random() * keys.length)];
1664
+
1665
+ if (keyToMutate === 'honeypotScore') return newConfig; // Ne pas muter le poids du honeypot
1666
+
1667
+ // Appliquer une mutation avec une amplitude variable
1668
+ const mutationAmount = (Math.random() - 0.5) * 0.4; // +/- 20%
1669
+ newConfig[sectionToMutate][keyToMutate] *= (1 + mutationAmount);
1670
+
1671
+ // S'assurer que les valeurs restent dans des limites raisonnables
1672
+ if (sectionToMutate === 'weights') {
1673
+ newConfig[sectionToMutate][keyToMutate] = Math.max(0, Math.min(1.5, newConfig[sectionToMutate][keyToMutate]));
1674
+ }
1675
+ if (keyToMutate === 'decayFactor') {
1676
+ newConfig.patterns.decayFactor = Math.max(0.8, Math.min(0.999, newConfig.patterns.decayFactor));
1677
+ }
1678
+ if (keyToMutate.includes('Threshold') || keyToMutate.includes('Reset')) {
1679
+ newConfig.patterns[keyToMutate] = Math.max(50, newConfig.patterns[keyToMutate]);
1680
+ }
1681
+
1682
+ return newConfig;
1683
+ };
1609
1684
 
1610
1685
  return Optimization.geneticAlgorithmMultiObjective(
1611
1686
  createIndividual,
package/mongodb-store.js CHANGED
@@ -1,53 +1,53 @@
1
- /**
2
- * @file Creates a store adapter for MongoDB.
3
- * This adapter uses a collection as a key-value store and leverages MongoDB's TTL indexes
4
- * for automatic expiration of documents.
5
- */
6
-
7
- /**
8
- * Creates a store adapter for a MongoDB collection.
9
- * It's recommended to pass the `db` object and let the adapter handle the collection.
10
- *
11
- * **Note:** For TTL to work, you must create a TTL index on the `expiresAt` field in your collection.
12
- * In the mongo shell, run:
13
- * `db.yourCollectionName.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 })`
14
- *
15
- * @param {import('mongodb').Db} db - An instance of a MongoDB Db object.
16
- * @param {string} [collectionName='fingerprint_store'] - The name of the collection to use.
17
- * @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
18
- */
19
- export function createMongoDbStore(db, collectionName = 'fingerprint_store') {
20
- const collection = db.collection(collectionName);
21
-
22
- return {
23
- async get(key) {
24
- const doc = await collection.findOne({ _id: key });
25
- // The TTL index automatically removes expired documents, so no need to check `expiresAt` here.
26
- return doc ? doc.value : null;
27
- },
28
- async set(key, value, ttl) {
29
- const doc = {
30
- _id: key,
31
- value: value,
32
- };
33
-
34
- if (ttl && ttl > 0) {
35
- // Set the expiration date for the TTL index.
36
- doc.expiresAt = new Date(Date.now() + ttl * 1000);
37
- }
38
-
39
- await collection.updateOne(
40
- { _id: key },
41
- { $set: doc },
42
- { upsert: true }
43
- );
44
- },
45
- async has(key) {
46
- const count = await collection.countDocuments({ _id: key });
47
- return count > 0;
48
- },
49
- async delete(key) {
50
- await collection.deleteOne({ _id: key });
51
- },
52
- };
1
+ /**
2
+ * @file Creates a store adapter for MongoDB.
3
+ * This adapter uses a collection as a key-value store and leverages MongoDB's TTL indexes
4
+ * for automatic expiration of documents.
5
+ */
6
+
7
+ /**
8
+ * Creates a store adapter for a MongoDB collection.
9
+ * It's recommended to pass the `db` object and let the adapter handle the collection.
10
+ *
11
+ * **Note:** For TTL to work, you must create a TTL index on the `expiresAt` field in your collection.
12
+ * In the mongo shell, run:
13
+ * `db.yourCollectionName.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 })`
14
+ *
15
+ * @param {import('mongodb').Db} db - An instance of a MongoDB Db object.
16
+ * @param {string} [collectionName='fingerprint_store'] - The name of the collection to use.
17
+ * @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
18
+ */
19
+ export function createMongoDbStore(db, collectionName = 'fingerprint_store') {
20
+ const collection = db.collection(collectionName);
21
+
22
+ return {
23
+ async get(key) {
24
+ const doc = await collection.findOne({ _id: key });
25
+ // The TTL index automatically removes expired documents, so no need to check `expiresAt` here.
26
+ return doc ? doc.value : null;
27
+ },
28
+ async set(key, value, ttl) {
29
+ const doc = {
30
+ _id: key,
31
+ value: value,
32
+ };
33
+
34
+ if (ttl && ttl > 0) {
35
+ // Set the expiration date for the TTL index.
36
+ doc.expiresAt = new Date(Date.now() + ttl * 1000);
37
+ }
38
+
39
+ await collection.updateOne(
40
+ { _id: key },
41
+ { $set: doc },
42
+ { upsert: true }
43
+ );
44
+ },
45
+ async has(key) {
46
+ const count = await collection.countDocuments({ _id: key });
47
+ return count > 0;
48
+ },
49
+ async delete(key) {
50
+ await collection.deleteOne({ _id: key });
51
+ },
52
+ };
53
53
  }
package/package.json CHANGED
@@ -1,79 +1,88 @@
1
- {
2
- "name": "@anonympins/fingerprint",
3
- "version": "0.2.1",
4
- "description": "An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.",
5
- "main": "fingerprint.js",
6
- "type": "module",
7
- "engines": {
8
- "node": ">=20.0.0"
9
- },
10
- "scripts": {
11
- "test": "vitest run --reporter=verbose"
12
- },
13
- "files": [
14
- "fingerprint.js",
15
- "fingerprint.client.js",
16
- "fingerprint.builder.js",
17
- "pow.solver.js",
18
- "pow.worker.js",
19
- "problem-manager.js",
20
- "optimization.worker.js",
21
- "library.js",
22
- "redis-store.js",
23
- "mongodb-store.js",
24
- "sql-store.js",
25
- "README.md",
26
- "LICENSE"
27
- ],
28
- "repository": {
29
- "type": "git",
30
- "url": "git+https://github.com/anonympins/fingerprint.git"
31
- },
32
- "keywords": [
33
- "fingerprint",
34
- "bot",
35
- "anti-bot",
36
- "security",
37
- "express",
38
- "middleware",
39
- "proof-of-work",
40
- "pow",
41
- "rate-limiting",
42
- "mitigation",
43
- "captcha",
44
- "dns"
45
- ],
46
- "author": "anonympins",
47
- "license": "MIT",
48
- "bugs": {
49
- "url": "https://github.com/anonympins/fingerprint/issues"
50
- },
51
- "homepage": "https://github.com/anonympins/fingerprint#readme",
52
- "devDependencies": {
53
- "body-parser": "^1.20.2",
54
- "cookie-parser": "^1.4.6",
55
- "express": "^4.18.2",
56
- "prom-client": "^15.1.2",
57
- "vitest": "^4.1.11"
58
- },
59
- "peerDependencies": {
60
- "ioredis": "^5.3.2",
61
- "knex": ">=3.0.0",
62
- "mongodb": "^6.3.0",
63
- "sqlite3": "^5.1.7"
64
- },
65
- "peerDependenciesMeta": {
66
- "ioredis": {
67
- "optional": true
68
- },
69
- "mongodb": {
70
- "optional": true
71
- },
72
- "knex": {
73
- "optional": true
74
- },
75
- "sqlite3": {
76
- "optional": true
77
- }
78
- }
79
- }
1
+ {
2
+ "name": "@anonympins/fingerprint",
3
+ "version": "0.2.3",
4
+ "description": "Advanced anti-bot library for Node.js using multi-layer fingerprinting (JA3, client-side, headers), behavioral analysis, and adaptive Proof-of-Work (PoW) challenges to mitigate scraping, scalping, and automated threats.",
5
+ "main": "fingerprint.js",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=20.0.0"
9
+ },
10
+ "scripts": {
11
+ "test": "vitest run --reporter=verbose"
12
+ },
13
+ "files": [
14
+ "fingerprint.js",
15
+ "fingerprint.client.js",
16
+ "fingerprint.builder.js",
17
+ "pow.solver.js",
18
+ "pow.worker.js",
19
+ "problem-manager.js",
20
+ "optimization.worker.js",
21
+ "library.js",
22
+ "redis-store.js",
23
+ "mongodb-store.js",
24
+ "sql-store.js",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/anonympins/fingerprint.git"
31
+ },
32
+ "keywords": [
33
+ "anti-bot",
34
+ "bot-detection",
35
+ "security",
36
+ "middleware",
37
+ "express",
38
+ "nodejs",
39
+ "fingerprint",
40
+ "device-fingerprinting",
41
+ "ja3",
42
+ "tls-fingerprinting",
43
+ "proof-of-work",
44
+ "pow",
45
+ "useful-proof-of-work",
46
+ "upow",
47
+ "waf",
48
+ "scraping",
49
+ "scalping",
50
+ "mitigation",
51
+ "rate-limiting",
52
+ "honeypot",
53
+ "behavioral-analysis"
54
+ ],
55
+ "author": "anonympins",
56
+ "license": "MIT",
57
+ "bugs": {
58
+ "url": "https://github.com/anonympins/fingerprint/issues"
59
+ },
60
+ "homepage": "https://github.com/anonympins/fingerprint#readme",
61
+ "devDependencies": {
62
+ "body-parser": "^1.20.2",
63
+ "cookie-parser": "^1.4.6",
64
+ "express": "^4.18.2",
65
+ "prom-client": "^15.1.2",
66
+ "vitest": "^4.1.11"
67
+ },
68
+ "peerDependencies": {
69
+ "ioredis": "^5.3.2",
70
+ "knex": ">=3.0.0",
71
+ "mongodb": "^6.3.0",
72
+ "sqlite3": "^5.1.7"
73
+ },
74
+ "peerDependenciesMeta": {
75
+ "ioredis": {
76
+ "optional": true
77
+ },
78
+ "mongodb": {
79
+ "optional": true
80
+ },
81
+ "knex": {
82
+ "optional": true
83
+ },
84
+ "sqlite3": {
85
+ "optional": true
86
+ }
87
+ }
88
+ }
package/pow.solver.js CHANGED
@@ -293,6 +293,18 @@ async function solveUsefulWorkTask(task) {
293
293
  // On simule l'appel avec les arguments de base.
294
294
  // Note: `baseSolverArgs` peut contenir des options.
295
295
  return clientSolver(...baseSolverArgs);
296
+
297
+ case 'multi_objective_genetic_algorithm': {
298
+ // C'est ici que la connexion se fait !
299
+ // On cherche le solveur demandé (ex: 'cpc.solve') dans notre registre client.
300
+ const solverFunction = ClientOptimizers[task.solverName];
301
+ if (!solverFunction) {
302
+ throw new Error(`Solver '${task.solverName}' not found on client.`);
303
+ }
304
+ // On appelle le solveur en lui passant le payload et les options.
305
+ // La fonction `solveOptimalCPC` attend le payload comme premier argument.
306
+ return solverFunction(task.payload, { generations: task.generations, initialFront: task.initialFront });
307
+ }
296
308
 
297
309
  default:
298
310
  throw new Error(`Unknown useful work type: ${task.type}`);
@@ -339,14 +351,50 @@ export async function solveOptimizationTask(initialPopulation, generations) {
339
351
  return population;
340
352
  }
341
353
 
354
+ /**
355
+ * @class ChallengeSolution
356
+ * @description Encapsule une solution de challenge et fournit des méthodes pour la manipuler.
357
+ * @private
358
+ */
359
+ class ChallengeSolution {
360
+ constructor(type, nonce, rawSolution) {
361
+ this.type = type;
362
+ this.nonce = nonce;
363
+ this.rawSolution = rawSolution;
364
+ }
365
+
366
+ /**
367
+ * Applique les paramètres de la solution à un objet URL.
368
+ * @param {URL} url - L'objet URL à modifier.
369
+ */
370
+ applyToUrl(url) {
371
+ url.searchParams.set('pow_type', this.type);
372
+ url.searchParams.set('pow_nonce', this.nonce);
373
+
374
+ // Logique de formatage spécifique à chaque type de challenge
375
+ if (this.type === 'cpu_mem' || this.type === 'cpu_mem_inline' || this.type === 'cpu_target') {
376
+ Object.entries(this.rawSolution).forEach(([key, value]) => {
377
+ url.searchParams.set(`pow_solution_${key}`, String(value));
378
+ });
379
+ } else if (this.type === 'useful_work_task') {
380
+ url.searchParams.set('pow_solution_work_result', JSON.stringify(this.rawSolution.work_result));
381
+ url.searchParams.set('pow_problem_id', this.rawSolution.problem_id);
382
+ } else {
383
+ // Pour les cas simples comme 'tsp' où la solution est une seule valeur
384
+ url.searchParams.set('pow_solution', JSON.stringify(this.rawSolution));
385
+ }
386
+ }
387
+ }
388
+
342
389
  /**
343
390
  * Fonction principale qui reçoit un objet challenge et le résout.
344
391
  * @param {object} challenge - L'objet challenge reçu du serveur.
345
- * @returns {Promise<object>} Un objet contenant la ou les solutions.
392
+ * @param {string} [fingerprint=''] - L'empreinte de l'appareil qui résout le challenge.
393
+ * @returns {Promise<ChallengeSolution>} Un objet `ChallengeSolution` encapsulant le résultat.
346
394
  */
347
395
  export async function solveChallenge(challenge, fingerprint = '') { // The fingerprint is now passed from the client library
348
396
  const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance, optimizationTask, usefulWorkTask } = challenge;
349
- const solutions = {};
397
+ let rawSolution = {};
350
398
 
351
399
  switch (type) {
352
400
  case 'cpu_target':
@@ -357,7 +405,7 @@ export async function solveChallenge(challenge, fingerprint = '') { // The finge
357
405
  const target = cpuTarget; // Keep variable name for consistency below
358
406
  // Pour ce challenge simple, le baseBlock est juste le nonce.
359
407
  const baseBlockBytes = new TextEncoder().encode(nonce + ":");
360
- solutions.cpu = await solveCpuTargetInline(baseBlockBytes, target, null);
408
+ rawSolution.cpu = await solveCpuTargetInline(baseBlockBytes, target, null);
361
409
  break;
362
410
  case 'cpu_mem':
363
411
  // Pour les appels API, le client IP n'est pas connu, on ne le met pas dans le message
@@ -371,8 +419,8 @@ export async function solveChallenge(challenge, fingerprint = '') { // The finge
371
419
  })(),
372
420
  solveMemory(memSeed, memDifficulty)
373
421
  ]);
374
- solutions.cpu = cpuSol;
375
- solutions.mem = memSol;
422
+ rawSolution.cpu = cpuSol;
423
+ rawSolution.mem = memSol;
376
424
  break;
377
425
  case 'cpu_mem_inline':
378
426
  // Version inline pour compatibilité HTML avec IP incluse
@@ -385,28 +433,28 @@ export async function solveChallenge(challenge, fingerprint = '') { // The finge
385
433
  })(),
386
434
  solveMemory(memSeedInline, memDifficulty)
387
435
  ]);
388
- solutions.cpu = cpuSolInline;
389
- solutions.mem = memSolInline;
436
+ rawSolution.cpu = cpuSolInline;
437
+ rawSolution.mem = memSolInline;
390
438
  break;
391
439
  case 'tsp':
392
440
  const tspResult = await solveTsp(cities, targetMaxDistance);
393
- solutions.tsp = tspResult.path;
394
- solutions.distance = tspResult.distance;
441
+ // Pour ce challenge, la solution est juste le chemin.
442
+ rawSolution = tspResult.path;
395
443
  break;
396
444
  case 'optimization_task':
397
445
  const finalPopulation = await solveOptimizationTask(optimizationTask.population, optimizationTask.generations);
398
- solutions.population = finalPopulation.map(p => p.chromosome); // On ne renvoie que les chromosomes
446
+ rawSolution = finalPopulation.map(p => p.chromosome); // On ne renvoie que les chromosomes
399
447
  break;
400
448
  case 'useful_work_task':
401
449
  const workResult = await solveUsefulWorkTask(usefulWorkTask.task);
402
- solutions.work_result = workResult;
403
- solutions.problem_id = usefulWorkTask.problemId;
450
+ rawSolution.work_result = workResult;
451
+ rawSolution.problem_id = usefulWorkTask.problemId;
404
452
  break;
405
453
  default:
406
454
  throw new Error(`Unknown challenge type: ${type}`);
407
455
  }
408
456
 
409
- return solutions;
457
+ return new ChallengeSolution(type, nonce, rawSolution);
410
458
  }
411
459
 
412
460
  // --- Compatibilité pour l'injection directe dans le HTML ---
package/pow.worker.js CHANGED
@@ -1,27 +1,27 @@
1
- /**
2
- * @file @/pow.worker.js
3
- * @description Web Worker dédié à la résolution du challenge CPU.
4
- * Ce script s'exécute sur un thread séparé pour ne pas bloquer l'interface utilisateur.
5
- */
6
-
7
- self.onmessage = async (event) => {
8
- const { message, target } = event.data;
9
- let solution = 0;
10
- const encoder = new TextEncoder();
11
-
12
- // La boucle de calcul intensive est isolée dans ce worker.
13
- while (true) {
14
- const currentMessage = `${message}:${solution}`;
15
- const data = encoder.encode(currentMessage);
16
- const hashBuffer = await crypto.subtle.digest('SHA-256', data);
17
- const hashArray = Array.from(new Uint8Array(hashBuffer));
18
- const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
19
-
20
- if (BigInt('0x' + hashHex) < target) {
21
- // Une fois la solution trouvée, on la renvoie au thread principal.
22
- self.postMessage({ solution });
23
- return;
24
- }
25
- solution++;
26
- }
1
+ /**
2
+ * @file @/pow.worker.js
3
+ * @description Web Worker dédié à la résolution du challenge CPU.
4
+ * Ce script s'exécute sur un thread séparé pour ne pas bloquer l'interface utilisateur.
5
+ */
6
+
7
+ self.onmessage = async (event) => {
8
+ const { message, target } = event.data;
9
+ let solution = 0;
10
+ const encoder = new TextEncoder();
11
+
12
+ // La boucle de calcul intensive est isolée dans ce worker.
13
+ while (true) {
14
+ const currentMessage = `${message}:${solution}`;
15
+ const data = encoder.encode(currentMessage);
16
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
17
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
18
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
19
+
20
+ if (BigInt('0x' + hashHex) < target) {
21
+ // Une fois la solution trouvée, on la renvoie au thread principal.
22
+ self.postMessage({ solution });
23
+ return;
24
+ }
25
+ solution++;
26
+ }
27
27
  };