@anonympins/fingerprint 0.1.1 → 0.1.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.
Files changed (3) hide show
  1. package/fingerprint.js +2267 -2267
  2. package/package.json +76 -76
  3. package/pow.solver.js +213 -213
package/package.json CHANGED
@@ -1,76 +1,76 @@
1
- {
2
- "name": "@anonympins/fingerprint",
3
- "version": "0.1.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
- "library.js",
19
- "redis-store.js",
20
- "mongodb-store.js",
21
- "sql-store.js",
22
- "README.md",
23
- "LICENSE"
24
- ],
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/anonympins/fingerprint.git"
28
- },
29
- "keywords": [
30
- "fingerprint",
31
- "bot",
32
- "anti-bot",
33
- "security",
34
- "express",
35
- "middleware",
36
- "proof-of-work",
37
- "pow",
38
- "rate-limiting",
39
- "mitigation",
40
- "captcha",
41
- "dns"
42
- ],
43
- "author": "anonympins",
44
- "license": "MIT",
45
- "bugs": {
46
- "url": "https://github.com/anonympins/fingerprint/issues"
47
- },
48
- "homepage": "https://github.com/anonympins/fingerprint#readme",
49
- "devDependencies": {
50
- "body-parser": "^1.20.2",
51
- "cookie-parser": "^1.4.6",
52
- "express": "^4.18.2",
53
- "prom-client": "^15.1.2",
54
- "vitest": "^4.1.11"
55
- },
56
- "peerDependencies": {
57
- "ioredis": "^5.3.2",
58
- "knex": ">=3.0.0",
59
- "mongodb": "^6.3.0",
60
- "sqlite3": "^5.1.7"
61
- },
62
- "peerDependenciesMeta": {
63
- "ioredis": {
64
- "optional": true
65
- },
66
- "mongodb": {
67
- "optional": true
68
- },
69
- "knex": {
70
- "optional": true
71
- },
72
- "sqlite3": {
73
- "optional": true
74
- }
75
- }
76
- }
1
+ {
2
+ "name": "@anonympins/fingerprint",
3
+ "version": "0.1.2",
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
+ "library.js",
19
+ "redis-store.js",
20
+ "mongodb-store.js",
21
+ "sql-store.js",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/anonympins/fingerprint.git"
28
+ },
29
+ "keywords": [
30
+ "fingerprint",
31
+ "bot",
32
+ "anti-bot",
33
+ "security",
34
+ "express",
35
+ "middleware",
36
+ "proof-of-work",
37
+ "pow",
38
+ "rate-limiting",
39
+ "mitigation",
40
+ "captcha",
41
+ "dns"
42
+ ],
43
+ "author": "anonympins",
44
+ "license": "MIT",
45
+ "bugs": {
46
+ "url": "https://github.com/anonympins/fingerprint/issues"
47
+ },
48
+ "homepage": "https://github.com/anonympins/fingerprint#readme",
49
+ "devDependencies": {
50
+ "body-parser": "^1.20.2",
51
+ "cookie-parser": "^1.4.6",
52
+ "express": "^4.18.2",
53
+ "prom-client": "^15.1.2",
54
+ "vitest": "^4.1.11"
55
+ },
56
+ "peerDependencies": {
57
+ "ioredis": "^5.3.2",
58
+ "knex": ">=3.0.0",
59
+ "mongodb": "^6.3.0",
60
+ "sqlite3": "^5.1.7"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "ioredis": {
64
+ "optional": true
65
+ },
66
+ "mongodb": {
67
+ "optional": true
68
+ },
69
+ "knex": {
70
+ "optional": true
71
+ },
72
+ "sqlite3": {
73
+ "optional": true
74
+ }
75
+ }
76
+ }
package/pow.solver.js CHANGED
@@ -1,214 +1,214 @@
1
- /**
2
- * @file @/pow.solver.js
3
- * @description Contient les fonctions côté client pour résoudre les différents types de challenges Proof-of-Work.
4
- * Fichier compatible à la fois avec l'import de modules ES6 et l'injection directe dans un script HTML.
5
- */
6
-
7
- 'use strict';
8
-
9
- /**
10
- * Résout un challenge CPU basé sur une cible (version inline pour compatibilité HTML).
11
- * @param {string} clientIp - L'adresse IP du client.
12
- * @param {string} nonce - Le nonce du challenge.
13
- * @param {bigint} target - La cible à atteindre.
14
- * @param {string} clientSecret - Le secret client (optionnel).
15
- * @param {Function} progressCallback - Callback pour les mises à jour de progression.
16
- * @returns {Promise<number>} La solution (un nombre entier).
17
- */
18
- export async function solveCpuTargetInline(clientIp, nonce, target, clientSecret = null, progressCallback) {
19
- const cpuTarget = BigInt(target);
20
- let cpuSolution = 0;
21
- while (true) {
22
- const msg = clientSecret
23
- ? `${clientIp}:${nonce}:${cpuSolution}:${clientSecret}`
24
- : `${clientIp}:${nonce}:${cpuSolution}`;
25
- const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
26
- const hashHex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
27
- if (BigInt('0x' + hashHex) < cpuTarget) break;
28
- cpuSolution++;
29
- if (cpuSolution % 100000 === 0) {
30
- await new Promise(r => setTimeout(r, 0));
31
- if (progressCallback) progressCallback(cpuSolution);
32
- }
33
- }
34
- return cpuSolution;
35
- }
36
-
37
- /**
38
- * Résout un challenge CPU basé sur une cible (version Web Worker).
39
- * @param {string} message - Le message à hasher (ex: `ip:nonce:solution:secret`).
40
- * @param {bigint} target - La cible à atteindre.
41
- * @returns {Promise<number>} La solution (un nombre entier).
42
- */
43
- export async function solveCpuTarget(message, target) {
44
- // Vérifie si les Web Workers sont supportés par le navigateur.
45
- if (typeof(Worker) === "undefined") {
46
- console.warn("Web Workers not supported. Falling back to main thread calculation (UI may freeze).");
47
- // Ici, on pourrait remettre l'ancienne implémentation comme solution de secours.
48
- // Pour la clarté, nous supposons que les workers sont disponibles.
49
- throw new Error("Web Worker support is required for CPU challenges.");
50
- }
51
-
52
- return new Promise((resolve, reject) => {
53
- // Crée un worker à partir du script dédié. Le chemin doit être accessible publiquement.
54
- // Assurez-vous que `pow.worker.js` est servi par votre serveur statique.
55
- const worker = new Worker('./pow.worker.js');
56
-
57
- worker.onmessage = (event) => {
58
- resolve(event.data.solution);
59
- worker.terminate(); // Nettoie le worker une fois le travail terminé.
60
- };
61
-
62
- worker.onerror = (error) => {
63
- reject(error);
64
- worker.terminate();
65
- };
66
-
67
- // Envoie les données du challenge au worker pour qu'il commence le calcul.
68
- worker.postMessage({ message, target });
69
- });
70
- }
71
-
72
- /**
73
- * Résout un challenge basé sur la mémoire.
74
- * @param {string} seed - La graine pour l'initialisation de la mémoire.
75
- * @param {number} difficulty - La difficulté (en Mo).
76
- * @returns {Promise<number>} La solution (nombre entier).
77
- */
78
- export async function solveMemory(seed, difficulty) {
79
- const size = difficulty * 1024 * 1024;
80
- const buffer = new Uint32Array(size / 4);
81
- let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
82
- for (let i = 0; i < buffer.length; i++) {
83
- buffer[i] = (h = Math.imul(h ^ i, 1597334677));
84
- }
85
- let solution = 0;
86
- const iterations = size / 16;
87
- let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
88
- for (let i = 0; i < iterations; i++) {
89
- addr = buffer[addr] % buffer.length;
90
- solution ^= addr;
91
- }
92
- return solution;
93
- }
94
-
95
- /**
96
- * Résout un challenge de type "Problème du Voyageur de Commerce" (TSP).
97
- * NOTE: Ceci est une implémentation simple (heuristique du plus proche voisin) et n'est pas garantie
98
- * de trouver la solution optimale, mais elle est suffisante pour un challenge.
99
- * @param {Array<{x: number, y: number}>} cities - Les coordonnées des villes.
100
- * @param {number} targetMaxDistance - La distance maximale acceptable.
101
- * @returns {Promise<{path: number[], distance: number}>} Le chemin et la distance.
102
- */
103
- export async function solveTsp(cities, targetMaxDistance) {
104
- // Utility function to calculate the distance between two cities
105
- function distance(city1, city2) {
106
- return Math.sqrt(Math.pow(city1.x - city2.x, 2) + Math.pow(city1.y - city2.y, 2));
107
- }
108
-
109
- // Utility function to evaluate the total distance of a path
110
- function evaluatePathDistance(cities, path) {
111
- let totalDistance = 0;
112
- for (let i = 0; i < path.length - 1; i++) {
113
- totalDistance += distance(cities[path[i]], cities[path[i + 1]]);
114
- }
115
- totalDistance += distance(cities[path[path.length - 1]], cities[path[0]]); // Return to start
116
- return totalDistance;
117
- }
118
-
119
- // Solveur simple du TSP (heuristique du plus proche voisin)
120
- function solveTspNearestNeighbor(cities) {
121
- const numCities = cities.length;
122
- if (numCities === 0) return [];
123
-
124
- let currentPath = [];
125
- let visited = new Array(numCities).fill(false);
126
-
127
- let currentCityIndex = 0; // Always start with the first city for reproducibility
128
- currentPath.push(currentCityIndex);
129
- visited[currentCityIndex] = true;
130
-
131
- for (let i = 1; i < numCities; i++) {
132
- let nearestCityIndex = -1;
133
- let minDistance = Infinity;
134
-
135
- for (let j = 0; j < numCities; j++) {
136
- if (!visited[j]) {
137
- const dist = distance(cities[currentCityIndex], cities[j]);
138
- if (dist < minDistance) {
139
- minDistance = dist;
140
- nearestCityIndex = j;
141
- }
142
- }
143
- }
144
- currentCityIndex = nearestCityIndex;
145
- currentPath.push(currentCityIndex);
146
- visited[currentCityIndex] = true;
147
- }
148
- return currentPath;
149
- }
150
-
151
- // To avoid freezing the browser, yield the thread from time to time
152
- await new Promise(resolve => setTimeout(resolve, 10));
153
- const solutionPath = solveTspNearestNeighbor(cities);
154
- const solutionDistance = evaluatePathDistance(cities, solutionPath);
155
-
156
- return { path: solutionPath, distance: solutionDistance };
157
- }
158
-
159
- /**
160
- * Fonction principale qui reçoit un objet challenge et le résout.
161
- * @param {object} challenge - L'objet challenge reçu du serveur.
162
- * @returns {Promise<object>} Un objet contenant la ou les solutions.
163
- */
164
- export async function solveChallenge(challenge) {
165
- const { type, nonce, clientSecret, target, memDifficulty, cities, clientIp, targetMaxDistance } = challenge;
166
- const solutions = {};
167
-
168
- switch (type) {
169
- case 'cpu_target':
170
- const baseMessageCpu = `:${nonce}`; // L'IP est gérée côté serveur
171
- solutions.cpu = await solveCpuTarget(baseMessageCpu, BigInt('0x' + target));
172
- break;
173
- case 'cpu_mem':
174
- // Pour les appels API, le client IP n'est pas connu, on ne le met pas dans le message
175
- const baseMessageCombined = `${nonce}:${clientSecret}`;
176
- const memSeed = `:${nonce}:${clientSecret}`;
177
- const [cpuSol, memSol] = await Promise.all([
178
- // On utilise une version de solveCpuTarget qui n'a pas besoin de l'IP
179
- solveCpuTargetInline(null, nonce, target, clientSecret),
180
- solveMemory(memSeed, memDifficulty)
181
- ]);
182
- solutions.cpu = cpuSol;
183
- solutions.mem = memSol;
184
- break;
185
- case 'cpu_mem_inline':
186
- // Version inline pour compatibilité HTML avec IP incluse
187
- const memSeedInline = `${nonce}:${clientSecret}`;
188
- const [cpuSolInline, memSolInline] = await Promise.all([
189
- solveCpuTargetInline(clientIp, nonce, target, clientSecret),
190
- solveMemory(memSeedInline, memDifficulty)
191
- ]);
192
- solutions.cpu = cpuSolInline;
193
- solutions.mem = memSolInline;
194
- break;
195
- case 'tsp':
196
- const tspResult = await solveTsp(cities, targetMaxDistance);
197
- solutions.tsp = tspResult.path;
198
- solutions.distance = tspResult.distance;
199
- break;
200
- default:
201
- throw new Error(`Unknown challenge type: ${type}`);
202
- }
203
-
204
- return solutions;
205
- }
206
-
207
- // --- Compatibilité pour l'injection directe dans le HTML ---
208
- // Si le script est chargé dans un navigateur (window existe), on attache les fonctions nécessaires à window.
209
- if (typeof window !== 'undefined') {
210
- window.solveCpuChallengeInline = solveCpuTargetInline;
211
- window.solveMemoryChallenge = solveMemory;
212
- window.solveTspChallenge = solveTsp;
213
- window.solveChallenge = solveChallenge;
1
+ /**
2
+ * @file @/pow.solver.js
3
+ * @description Contient les fonctions côté client pour résoudre les différents types de challenges Proof-of-Work.
4
+ * Fichier compatible à la fois avec l'import de modules ES6 et l'injection directe dans un script HTML.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ /**
10
+ * Résout un challenge CPU basé sur une cible (version inline pour compatibilité HTML).
11
+ * @param {string} clientIp - L'adresse IP du client.
12
+ * @param {string} nonce - Le nonce du challenge.
13
+ * @param {bigint} target - La cible à atteindre.
14
+ * @param {string} clientSecret - Le secret client (optionnel).
15
+ * @param {Function} progressCallback - Callback pour les mises à jour de progression.
16
+ * @returns {Promise<number>} La solution (un nombre entier).
17
+ */
18
+ export async function solveCpuTargetInline(clientIp, nonce, target, clientSecret = null, progressCallback) {
19
+ const cpuTarget = BigInt(target);
20
+ let cpuSolution = 0;
21
+ while (true) {
22
+ const msg = clientSecret
23
+ ? `${clientIp}:${nonce}:${cpuSolution}:${clientSecret}`
24
+ : `${clientIp}:${nonce}:${cpuSolution}`;
25
+ const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
26
+ const hashHex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
27
+ if (BigInt('0x' + hashHex) < cpuTarget) break;
28
+ cpuSolution++;
29
+ if (cpuSolution % 100000 === 0) {
30
+ await new Promise(r => setTimeout(r, 0));
31
+ if (progressCallback) progressCallback(cpuSolution);
32
+ }
33
+ }
34
+ return cpuSolution;
35
+ }
36
+
37
+ /**
38
+ * Résout un challenge CPU basé sur une cible (version Web Worker).
39
+ * @param {string} message - Le message à hasher (ex: `ip:nonce:solution:secret`).
40
+ * @param {bigint} target - La cible à atteindre.
41
+ * @returns {Promise<number>} La solution (un nombre entier).
42
+ */
43
+ export async function solveCpuTarget(message, target) {
44
+ // Vérifie si les Web Workers sont supportés par le navigateur.
45
+ if (typeof(Worker) === "undefined") {
46
+ console.warn("Web Workers not supported. Falling back to main thread calculation (UI may freeze).");
47
+ // Ici, on pourrait remettre l'ancienne implémentation comme solution de secours.
48
+ // Pour la clarté, nous supposons que les workers sont disponibles.
49
+ throw new Error("Web Worker support is required for CPU challenges.");
50
+ }
51
+
52
+ return new Promise((resolve, reject) => {
53
+ // Crée un worker à partir du script dédié. Le chemin doit être accessible publiquement.
54
+ // Assurez-vous que `pow.worker.js` est servi par votre serveur statique.
55
+ const worker = new Worker('./pow.worker.js');
56
+
57
+ worker.onmessage = (event) => {
58
+ resolve(event.data.solution);
59
+ worker.terminate(); // Nettoie le worker une fois le travail terminé.
60
+ };
61
+
62
+ worker.onerror = (error) => {
63
+ reject(error);
64
+ worker.terminate();
65
+ };
66
+
67
+ // Envoie les données du challenge au worker pour qu'il commence le calcul.
68
+ worker.postMessage({ message, target });
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Résout un challenge basé sur la mémoire.
74
+ * @param {string} seed - La graine pour l'initialisation de la mémoire.
75
+ * @param {number} difficulty - La difficulté (en Mo).
76
+ * @returns {Promise<number>} La solution (nombre entier).
77
+ */
78
+ export async function solveMemory(seed, difficulty) {
79
+ const size = difficulty * 1024 * 1024;
80
+ const buffer = new Uint32Array(size / 4);
81
+ let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
82
+ for (let i = 0; i < buffer.length; i++) {
83
+ buffer[i] = (h = Math.imul(h ^ i, 1597334677));
84
+ }
85
+ let solution = 0;
86
+ const iterations = size / 16;
87
+ let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
88
+ for (let i = 0; i < iterations; i++) {
89
+ addr = buffer[addr] % buffer.length;
90
+ solution ^= addr;
91
+ }
92
+ return solution;
93
+ }
94
+
95
+ /**
96
+ * Résout un challenge de type "Problème du Voyageur de Commerce" (TSP).
97
+ * NOTE: Ceci est une implémentation simple (heuristique du plus proche voisin) et n'est pas garantie
98
+ * de trouver la solution optimale, mais elle est suffisante pour un challenge.
99
+ * @param {Array<{x: number, y: number}>} cities - Les coordonnées des villes.
100
+ * @param {number} targetMaxDistance - La distance maximale acceptable.
101
+ * @returns {Promise<{path: number[], distance: number}>} Le chemin et la distance.
102
+ */
103
+ export async function solveTsp(cities, targetMaxDistance) {
104
+ // Utility function to calculate the distance between two cities
105
+ function distance(city1, city2) {
106
+ return Math.sqrt(Math.pow(city1.x - city2.x, 2) + Math.pow(city1.y - city2.y, 2));
107
+ }
108
+
109
+ // Utility function to evaluate the total distance of a path
110
+ function evaluatePathDistance(cities, path) {
111
+ let totalDistance = 0;
112
+ for (let i = 0; i < path.length - 1; i++) {
113
+ totalDistance += distance(cities[path[i]], cities[path[i + 1]]);
114
+ }
115
+ totalDistance += distance(cities[path[path.length - 1]], cities[path[0]]); // Return to start
116
+ return totalDistance;
117
+ }
118
+
119
+ // Solveur simple du TSP (heuristique du plus proche voisin)
120
+ function solveTspNearestNeighbor(cities) {
121
+ const numCities = cities.length;
122
+ if (numCities === 0) return [];
123
+
124
+ let currentPath = [];
125
+ let visited = new Array(numCities).fill(false);
126
+
127
+ let currentCityIndex = 0; // Always start with the first city for reproducibility
128
+ currentPath.push(currentCityIndex);
129
+ visited[currentCityIndex] = true;
130
+
131
+ for (let i = 1; i < numCities; i++) {
132
+ let nearestCityIndex = -1;
133
+ let minDistance = Infinity;
134
+
135
+ for (let j = 0; j < numCities; j++) {
136
+ if (!visited[j]) {
137
+ const dist = distance(cities[currentCityIndex], cities[j]);
138
+ if (dist < minDistance) {
139
+ minDistance = dist;
140
+ nearestCityIndex = j;
141
+ }
142
+ }
143
+ }
144
+ currentCityIndex = nearestCityIndex;
145
+ currentPath.push(currentCityIndex);
146
+ visited[currentCityIndex] = true;
147
+ }
148
+ return currentPath;
149
+ }
150
+
151
+ // To avoid freezing the browser, yield the thread from time to time
152
+ await new Promise(resolve => setTimeout(resolve, 10));
153
+ const solutionPath = solveTspNearestNeighbor(cities);
154
+ const solutionDistance = evaluatePathDistance(cities, solutionPath);
155
+
156
+ return { path: solutionPath, distance: solutionDistance };
157
+ }
158
+
159
+ /**
160
+ * Fonction principale qui reçoit un objet challenge et le résout.
161
+ * @param {object} challenge - L'objet challenge reçu du serveur.
162
+ * @returns {Promise<object>} Un objet contenant la ou les solutions.
163
+ */
164
+ export async function solveChallenge(challenge) {
165
+ const { type, nonce, clientSecret, target, memDifficulty, cities, clientIp, targetMaxDistance } = challenge;
166
+ const solutions = {};
167
+
168
+ switch (type) {
169
+ case 'cpu_target':
170
+ const baseMessageCpu = `:${nonce}`; // L'IP est gérée côté serveur
171
+ solutions.cpu = await solveCpuTarget(baseMessageCpu, BigInt('0x' + target));
172
+ break;
173
+ case 'cpu_mem':
174
+ // Pour les appels API, le client IP n'est pas connu, on ne le met pas dans le message
175
+ const baseMessageCombined = `${nonce}:${clientSecret}`;
176
+ const memSeed = `:${nonce}:${clientSecret}`;
177
+ const [cpuSol, memSol] = await Promise.all([
178
+ // On utilise une version de solveCpuTarget qui n'a pas besoin de l'IP
179
+ solveCpuTargetInline(null, nonce, target, clientSecret),
180
+ solveMemory(memSeed, memDifficulty)
181
+ ]);
182
+ solutions.cpu = cpuSol;
183
+ solutions.mem = memSol;
184
+ break;
185
+ case 'cpu_mem_inline':
186
+ // Version inline pour compatibilité HTML avec IP incluse
187
+ const memSeedInline = `${nonce}:${clientSecret}`;
188
+ const [cpuSolInline, memSolInline] = await Promise.all([
189
+ solveCpuTargetInline(clientIp, nonce, target, clientSecret),
190
+ solveMemory(memSeedInline, memDifficulty)
191
+ ]);
192
+ solutions.cpu = cpuSolInline;
193
+ solutions.mem = memSolInline;
194
+ break;
195
+ case 'tsp':
196
+ const tspResult = await solveTsp(cities, targetMaxDistance);
197
+ solutions.tsp = tspResult.path;
198
+ solutions.distance = tspResult.distance;
199
+ break;
200
+ default:
201
+ throw new Error(`Unknown challenge type: ${type}`);
202
+ }
203
+
204
+ return solutions;
205
+ }
206
+
207
+ // --- Compatibilité pour l'injection directe dans le HTML ---
208
+ // Si le script est chargé dans un navigateur (window existe), on attache les fonctions nécessaires à window.
209
+ if (typeof window !== 'undefined') {
210
+ window.solveCpuChallengeInline = solveCpuTargetInline;
211
+ window.solveMemoryChallenge = solveMemory;
212
+ window.solveTspChallenge = solveTsp;
213
+ window.solveChallenge = solveChallenge;
214
214
  }