@anonympins/fingerprint 0.0.3 → 0.0.4

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/README.md +14 -19
  2. package/fingerprint.js +104 -61
  3. package/package.json +47 -47
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  [![CI](https://img.shields.io/github/actions/workflow/status/anonympins/fingerprint/ci.yml)](https://github.com/anonympins/fingerprint/actions/workflows/ci.yml)
3
3
  [![Release](https://img.shields.io/github/v/release/anonympins/fingerprint)](https://github.com/anonympins/fingerprint/releases)
4
4
  [![License](https://img.shields.io/github/license/anonympins/fingerprint)](https://github.com/anonympins/fingerprint/blob/main/LICENSE)
5
- [![Downloads](https://img.shields.io/github/downloads/anonympins/fingerprint/total)](https://github.com/anonympins/fingerprint/releases)
5
+ ![GitHub commit activity](https://img.shields.io/github/commit-activity/w/anonympins/fingerprint)
6
6
  [![Watchers](https://img.shields.io/github/watchers/anonympins/fingerprint)](https://github.com/anonympins/fingerprint/watchers)
7
7
 
8
8
  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.
@@ -30,7 +30,7 @@ Once the challenge is solved, a clearance "ticket" is issued via a secure cookie
30
30
  - **Multi-Factor Fingerprinting**: Combines client-side data (`hardwareConcurrency`, `deviceMemory`, `screen`, `canvas`, `webgl`) and server-side data (`User-Agent`, `Client-Hints`).
31
31
  - **Secure Ticket System**: Uses HMAC-SHA256 signatures to validate clearances and prevent tampering.
32
32
  - **Pluggable Datastore**: Supports external datastores like Redis for state persistence and scalability across multiple server instances.
33
- - **Express.js Middleware**: Easy integration into an Express application with `powMiddleware`.
33
+ - **Express.js Middleware**: Easy integration into an Express application with `powMiddleware`. The datastore must support setting a Time-To-Live (TTL) for challenge secrets.
34
34
  - **Timing Attack Protection**: Uses `crypto.timingSafeEqual` for secure ticket validation.
35
35
  - **Automatic Threshold Tuning**: Includes a genetic algorithm-based optimizer (`startThresholdAutoTuning`) that analyzes real traffic to dynamically adjust suspicion thresholds (`low`, `medium`, `high`), improving bot detection accuracy and reducing false positives over time.
36
36
 
@@ -75,7 +75,8 @@ const securityConfig = {
75
75
  low: 20, // Score from which a CPU challenge is issued
76
76
  medium: 45, // Score for a more difficult combined CPU/Memory challenge
77
77
  high: 75, // Score for a very difficult challenge
78
- block: 95 // Score above which the request is blocked outright (HTTP 403)
78
+ block: 95, // Score above which the request is blocked outright (HTTP 403)
79
+ isStaticResource: (req) => req.path.startsWith('/static/') // Optional: Custom function to identify static resources
79
80
  }
80
81
  };
81
82
 
@@ -222,9 +223,8 @@ const server = http.createServer(async (req, res) => {
222
223
  clientIp: req.socket.remoteAddress,
223
224
  path: req.url.split('?')[0],
224
225
  cookies: {}, // Parse cookies from req.headers.cookie
225
- query: {}, // Parse query string from req.url
226
+ query: new URL(req.url, `http://${req.headers.host}`).searchParams,
226
227
  headers: req.headers,
227
- isStatic: /\.(js|css|png)$/.test(req.url),
228
228
  rawReq: req, // Pass the raw request
229
229
  rawRes: res, // Pass the raw response for cookie setting
230
230
  };
@@ -261,35 +261,30 @@ Manually setting the `low`, `medium`, and `high` thresholds can be challenging.
261
261
 
262
262
  1. **Enable Logging**: The auto-tuner needs data. You must provide a `logger` function in your security configuration. This function will be called for significant events (`challenge_issued`, `challenge_solved`, etc.).
263
263
 
264
- 2. **Start the Tuner**: Call `startThresholdAutoTuning` with your live security configuration and the array where logs are stored.
264
+ 2. **Enable Auto-tuning**: Add an `autotuning` property to your security configuration. The middleware will automatically start the tuning process.
265
265
 
266
266
  ```javascript
267
- import { powMiddleware, startThresholdAutoTuning } from './fingerprint.js';
267
+ import { powMiddleware } from './fingerprint.js';
268
268
 
269
269
  // Array to store traffic analysis data. In a real application, this could be
270
270
  // a more robust logging system.
271
271
  const trafficData = [];
272
272
 
273
273
  const securityConfig = {
274
- weights: { /* ... your weights ... */ },
274
+ weights: { /* ... */ },
275
275
  thresholds: {
276
276
  low: 20, // Initial values, will be optimized
277
277
  medium: 45,
278
278
  high: 75
279
279
  },
280
- // The logger is required for auto-tuning
281
- logger: (log) => trafficData.push(log)
280
+ logger: (log) => trafficData.push(log), // The logger is required for auto-tuning
281
+ autotune: {
282
+ trafficData: trafficData, // The data source for the algorithm
283
+ interval: 1800000, // Optimization cycle every 30 minutes (optional)
284
+ minDataPoints: 200 // Minimum requests before starting optimization (optional)
285
+ }
282
286
  };
283
287
 
284
- // Start the background optimization process.
285
- // The `securityConfig.thresholds` object will be mutated with optimized values.
286
- startThresholdAutoTuning({
287
- securityConfig: securityConfig, // The config object to be updated
288
- trafficData: trafficData, // The data source for the algorithm
289
- interval: 1800000, // Optimization cycle every 30 minutes
290
- minDataPoints: 200 // Minimum requests before starting optimization
291
- });
292
-
293
288
  const powMiddlewareInstance = powMiddleware(securityConfig);
294
289
  app.use(powMiddlewareInstance);
295
290
  ```
package/fingerprint.js CHANGED
@@ -2,14 +2,49 @@
2
2
  import crypto from "node:crypto";
3
3
  import { Optimization } from "./library.js";
4
4
  import { cyrb53, FingerprintBuilder } from "./fingerprint.builder.js";
5
- import { getDeviceHash } from "./fingerprint.server.js";
6
5
 
7
- const POW_SECRET = process.env.POW_SECRET;
6
+ /**
7
+ * Retrieves the POW_SECRET from environment variables with appropriate checks.
8
+ * @returns {string} The secret key.
9
+ */
10
+ const getPowSecret = () => {
11
+ const secret = process.env.POW_SECRET;
12
+ if (!secret && process.env.NODE_ENV === 'production') {
13
+ throw new Error('POW_SECRET environment variable is not set. This is required for production.');
14
+ }
15
+ return secret || "fallback-dev-secret-32-chars-minimum";
16
+ };
8
17
 
9
- if (!POW_SECRET && process.env.NODE_ENV === 'production') {
10
- throw new Error('POW_SECRET environment variable is not set. This is required for production.');
11
- } else if (!POW_SECRET) {
12
- console.warn('Warning: POW_SECRET environment variable not set. Using a default, insecure secret for development.');
18
+ /**
19
+ * Creates a stable hash based on device characteristics, independent of the IP.
20
+ * This is our "level 2 fingerprint".
21
+ * @param {object} context - The request context.
22
+ * @returns {string} A hash representing the device.
23
+ */
24
+ function getHeaderSignature(context) {
25
+ if (!context.rawHeaders) return '';
26
+ const headerKeys = [];
27
+ for (let i = 0; i < context.rawHeaders.length; i += 2) {
28
+ headerKeys.push(context.rawHeaders[i]);
29
+ }
30
+ return cyrb53(headerKeys.join(','));
31
+ }
32
+ export function getDeviceHash(context) {
33
+ // Prioritize the rich client-side fingerprint if provided.
34
+ const clientFp = context.headers['x-device-fingerprint'];
35
+ if (clientFp && typeof clientFp === 'string' && clientFp.includes('cvs:')) {
36
+ // Basic validation to ensure it looks like our client-side fingerprint.
37
+ return clientFp;
38
+ }
39
+
40
+ // Fallback to server-side only fingerprinting if the header is missing.
41
+ const srv = new FingerprintBuilder();
42
+ srv.add("ua", context.headers["user-agent"]);
43
+ if (context.headers["sec-ch-ua-platform"])
44
+ srv.add("os", context.headers["sec-ch-ua-platform"]);
45
+ if (context.headers["sec-ch-ua"]) srv.add("ch", context.headers["sec-ch-ua"]);
46
+ srv.add("h_ord", getHeaderSignature(context));
47
+ return srv.toString();
13
48
  }
14
49
 
15
50
  /**
@@ -289,7 +324,7 @@ export const verifyPoWAndGenerateTicket = (
289
324
  // 2. Generate an HMAC ticket so the client doesn't have to do it again for 1 hour
290
325
  const expiry = Date.now() + 3600000; // 1 heure
291
326
  const signature = crypto
292
- .createHmac("sha256", POW_SECRET || "fallback-dev-secret-32-chars-minimum")
327
+ .createHmac("sha256", getPowSecret())
293
328
  .update(`${ip}:${expiry}`)
294
329
  .digest("hex");
295
330
 
@@ -300,18 +335,22 @@ export const verifyPoWAndGenerateTicket = (
300
335
  * Verifies a memory PoW solution.
301
336
  * The server performs the same calculation to validate.
302
337
  */
303
- export const verifyMemoryPoW = (nonce, solution, difficulty = 16) => {
338
+ export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret) => {
304
339
  const size = difficulty * 1024 * 1024;
305
340
  const iterations = size / 16;
306
341
  const buffer = new Uint32Array(size / 4);
307
- let h = new TextEncoder().encode(nonce).reduce((acc, v) => acc + v, 0);
342
+ const seed = clientSecret ? `${nonce}:${clientSecret}` : nonce;
343
+ let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
344
+
308
345
  for (let i = 0; i < buffer.length; i++) {
309
346
  buffer[i] = h = Math.imul(h ^ i, 1597334677);
310
347
  }
348
+
311
349
  let finalHash = 0;
350
+ let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
312
351
  for (let i = 0; i < iterations; i++) {
313
- const addr = buffer[i % buffer.length] % buffer.length;
314
- finalHash ^= buffer[addr];
352
+ addr = buffer[addr] % buffer.length;
353
+ finalHash ^= addr;
315
354
  }
316
355
  return finalHash === parseInt(solution, 10);
317
356
  };
@@ -320,7 +359,7 @@ export const isTicketValid = (ip, ticket) => {
320
359
  const [expiry, sig] = ticket.split(":");
321
360
  if (!expiry || !sig || Date.now() > parseInt(expiry, 10)) return false;
322
361
  const expectedSig = crypto
323
- .createHmac("sha256", POW_SECRET || "fallback-dev-secret-32-chars-minimum")
362
+ .createHmac("sha256", getPowSecret())
324
363
  .update(`${ip}:${expiry}`)
325
364
  .digest("hex");
326
365
 
@@ -369,7 +408,7 @@ function getHeaderAnomalies(context) {
369
408
 
370
409
  /**
371
410
  * Default in-memory store implementation.
372
- * @type {IStore}
411
+ * @type {IStore}
373
412
  */
374
413
  const inMemoryStore = {
375
414
  _map: new Map(),
@@ -395,7 +434,7 @@ export const configureStore = (externalStore) => {
395
434
  * Orchestrates request identification using a persistent anchor (cookie)
396
435
  * and fingerprint verification.
397
436
  * @param {object} context - The request context.
398
- * @returns {Promise<{deviceId: string, deviceData: object, consistencyScore: number, newCookie: object|null}>}
437
+ * @returns {Promise<{deviceId: string, deviceData: object, consistencyScore: number, newCookie: object|null}>}
399
438
  */
400
439
  async function resolveRequestIdentity(context) {
401
440
  const existingDeviceId = context.cookies?.device_id;
@@ -470,7 +509,7 @@ async function getBehavioralIndicators(context, deviceData) {
470
509
  deviceData.rapidChangeCount = Math.min(
471
510
  deviceData.rapidChangeCount + 1,
472
511
  MAX_RAPID_CHANGES_PER_DEVICE * 2, // Increases quickly
473
- );
512
+ );
474
513
  } else {
475
514
  deviceData.rapidChangeCount = Math.max(0, deviceData.rapidChangeCount - 1); // Decreases slowly
476
515
  }
@@ -673,7 +712,7 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
673
712
  * @param {string} clientIp - The client's IP address.
674
713
  * @returns {string} HTML content.
675
714
  */
676
- function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp) {
715
+ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret) {
677
716
  const { nonce, target, path } = cpuChallengeDetails;
678
717
  return `
679
718
  <html><head><title>Advanced Security Check</title></head>
@@ -685,13 +724,14 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
685
724
  async function solve() {
686
725
  const nonce = "${nonce}";
687
726
  const path = "${path}";
727
+ const clientSecret = "${clientSecret}"; // Secret is now available to the client
688
728
 
689
729
  // --- CPU Challenge ---
690
730
  document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
691
731
  const cpuTarget = BigInt("0x${target}");
692
732
  let cpuSolution = 0;
693
733
  while (true) {
694
- const msg = "${clientIp}:${nonce}:" + cpuSolution;
734
+ const msg = "${clientIp}:${nonce}:" + cpuSolution + ":" + clientSecret;
695
735
  const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
696
736
  const hashHex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
697
737
  if (BigInt('0x' + hashHex) < cpuTarget) break;
@@ -704,14 +744,16 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
704
744
  await new Promise(r => setTimeout(r, 10)); // Yield to update UI
705
745
 
706
746
  let memSolution = 0;
747
+ const iterations = size / 16;
707
748
  try {
708
749
  const size = ${memoryDifficulty} * 1024 * 1024;
709
750
  const buffer = new Uint32Array(size / 4);
710
- let h = new TextEncoder().encode(nonce).reduce((acc, v) => acc + v, 0);
751
+ const seed = nonce + ":" + clientSecret;
752
+ let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
711
753
  for (let i = 0; i < buffer.length; i++) {
712
- buffer[i] = (h = Math.imul(h ^ i, 1597334677));
754
+ buffer[i] = h = Math.imul(h ^ i, 1597334677);
713
755
  }
714
- for(let i = 0; i < (size / 16); i++) {
756
+ for(let i = 0; i < iterations; i++) {
715
757
  const addr = buffer[i % buffer.length] % buffer.length;
716
758
  memSolution ^= buffer[addr];
717
759
  }
@@ -734,11 +776,13 @@ export function verifyCpuTargetPoWAndGenerateTicket(
734
776
  nonce,
735
777
  solution,
736
778
  suspicionFactor,
779
+ clientSecret, // Le secret est maintenant requis
737
780
  ) {
738
781
  const target = calculateTarget(suspicionFactor);
782
+ const message = clientSecret ? `${clientIp}:${nonce}:${solution}:${clientSecret}` : `${clientIp}:${nonce}:${solution}`;
739
783
  const hash = crypto
740
784
  .createHash("sha256")
741
- .update(`${clientIp}:${nonce}:${solution}`)
785
+ .update(message)
742
786
  .digest("hex");
743
787
  const hashAsInt = BigInt("0x" + hash);
744
788
 
@@ -747,7 +791,7 @@ export function verifyCpuTargetPoWAndGenerateTicket(
747
791
  // The proof is valid, generate the ticket
748
792
  const expiry = Date.now() + 3600000; // 1 heure
749
793
  const signature = crypto
750
- .createHmac("sha256", POW_SECRET || "fallback-dev-secret-32-chars-minimum")
794
+ .createHmac("sha256", getPowSecret())
751
795
  .update(`${clientIp}:${expiry}`)
752
796
  .digest("hex");
753
797
  return `${expiry}:${signature}`;
@@ -756,9 +800,11 @@ export function verifyCpuTargetPoWAndGenerateTicket(
756
800
  return null;
757
801
  }
758
802
 
759
- const staticExtensions =
760
- /\.(js|css|png|jpg|jpeg|gif|svg|mp3|webp|ico|woff|woff2|ttf|otf|map)$/i;
761
- const isStaticResource = (req) => staticExtensions.test(req.path);
803
+ const staticExtensions = new RegExp(
804
+ "\\.(js|css|png|jpg|jpeg|gif|svg|mp3|webp|ico|woff|woff2|ttf|otf|map)$",
805
+ "i",
806
+ );
807
+ const isStaticResource = (path) => staticExtensions.test(path);
762
808
 
763
809
  // --- Middleware Proof-of-Work (Le péage) ---
764
810
  class FingerprintEngine {
@@ -796,13 +842,15 @@ class FingerprintEngine {
796
842
  (finalScore - thresholds.low) / (thresholds.high - thresholds.low),
797
843
  )
798
844
  : 0;
799
- const powCookie = cookies?.pow_clearance;
845
+ const powCookie = cookies?.pow_clearance;
800
846
  const { pow_type, pow_nonce, pow_solution, pow_solution_cpu, pow_solution_mem } = query;
801
847
 
802
848
  if (isSuspicious && !isTicketValid(clientIp, powCookie)) {
803
849
  // --- CHALLENGE SOLUTION HANDLING ---
804
850
  if (pow_nonce && (pow_solution || (pow_solution_cpu && pow_solution_mem))) {
805
851
  let isValid = false,
852
+ // Retrieve the client-side secret associated with this nonce
853
+ clientSecret = await store.get(`secret:${pow_nonce}`),
806
854
  ticket = null;
807
855
  if (pow_type === "cpu_target") {
808
856
  // Verify the new type
@@ -810,25 +858,20 @@ class FingerprintEngine {
810
858
  clientIp,
811
859
  pow_nonce,
812
860
  pow_solution,
813
- suspicionFactor, // Pass the analog factor directly
861
+ suspicionFactor, // Pass the analog factor
862
+ clientSecret,
814
863
  );
815
- isValid = ticket !== null;
816
- } else if (pow_type === "mem") {
817
- const minDifficulty = 16; // 16Mo
818
- const maxDifficulty = 48; // 48Mo
819
- const difficulty =
820
- minDifficulty + suspicionFactor * (maxDifficulty - minDifficulty);
821
- isValid = verifyMemoryPoW(pow_nonce, pow_solution, difficulty);
822
- } else if (pow_type === "cpu_mem") {
864
+ isValid = ticket !== null; } else if (pow_type === "cpu_mem") {
823
865
  // Verify combined challenge
824
866
  const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(
825
- clientIp, pow_nonce, pow_solution_cpu, suspicionFactor
867
+ clientIp, pow_nonce, pow_solution_cpu, suspicionFactor, clientSecret
826
868
  );
827
-
828
869
  const minDifficulty = 16; // 16Mo
829
870
  const maxDifficulty = 48; // 48Mo
830
- const memDifficulty = minDifficulty + suspicionFactor * (maxDifficulty - minDifficulty);
831
- const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, memDifficulty);
871
+ const memActivationFactor = Math.max(0, (suspicionFactor - 0.25) / 0.75);
872
+ const memDifficulty = Math.round(minDifficulty + memActivationFactor * (maxDifficulty - minDifficulty));
873
+
874
+ const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, memDifficulty, clientSecret);
832
875
 
833
876
  isValid = cpuTicket !== null && isMemValid;
834
877
  if (isValid) ticket = cpuTicket; // Reuse the ticket generated by the CPU verification
@@ -838,11 +881,16 @@ class FingerprintEngine {
838
881
  }
839
882
 
840
883
  if (isValid) {
884
+ // The secret has been used, delete it to prevent replay.
885
+ if (clientSecret) {
886
+ await store.delete(`secret:${pow_nonce}`);
887
+ }
888
+
841
889
  if (!ticket) {
842
890
  // If the ticket has not already been generated (CPU case)
843
891
  const expiry = Date.now() + 3600000; // 1 heure
844
892
  const signature = crypto
845
- .createHmac("sha256", POW_SECRET || "fallback-dev-secret-32-chars-minimum")
893
+ .createHmac("sha256", getPowSecret())
846
894
  .update(`${clientIp}:${expiry}`)
847
895
  .digest("hex");
848
896
  ticket = `${expiry}:${signature}`;
@@ -872,6 +920,10 @@ class FingerprintEngine {
872
920
 
873
921
  // --- SELECTION AND SENDING OF THE APPROPRIATE CHALLENGE ---
874
922
  const nonce = crypto.randomBytes(16).toString("hex");
923
+ const clientSecret = crypto.randomBytes(16).toString("hex");
924
+
925
+ // Store the secret with a short TTL (e.g., 5 minutes)
926
+ await store.set(`secret:${nonce}`, clientSecret, 300);
875
927
 
876
928
  if (logger) {
877
929
  logger({ type: 'challenge_issued', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now() });
@@ -882,23 +934,7 @@ class FingerprintEngine {
882
934
  // ... logic for TSP/Captcha challenge
883
935
  }
884
936
 
885
- // LEVEL 2: Memory-Intensive PoW
886
- if (isSuspiciousMedium) {
887
- // Utilisons notre nouveau challenge combiné !
888
- const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path);
889
-
890
- const minMemDifficulty = 16; // 16Mo
891
- const maxMemDifficulty = 48; // 48Mo
892
- const memDifficulty = minMemDifficulty + suspicionFactor * (maxMemDifficulty - minMemDifficulty);
893
-
894
- const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp);
895
- return {
896
- action: 'challenge', score: finalScore, vector: suspicionVector,
897
- status: 429, body: page
898
- };
899
- }
900
-
901
- // NOUVELLE LOGIQUE UNIFIÉE POUR TOUS LES NIVEAUX DE SUSPICION (low et medium)
937
+ // UNIFIED CHALLENGE LOGIC for all suspicion levels (low, medium, high)
902
938
  if (isSuspicious) { // Couvre à la fois low et medium
903
939
  const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path);
904
940
 
@@ -908,10 +944,10 @@ class FingerprintEngine {
908
944
 
909
945
  const minMemDifficulty = 0; // Peut être 0 Mo !
910
946
  const maxMemDifficulty = 48; // 48Mo pour les plus suspects
911
- const memDifficulty = minMemDifficulty + memActivationFactor * (maxMemDifficulty - minMemDifficulty);
947
+ const memDifficulty = Math.round(minMemDifficulty + memActivationFactor * (maxMemDifficulty - minMemDifficulty));
912
948
 
913
- // On utilise toujours la page combinée, même si la difficulté mémoire est 0 (le calcul sera quasi instantané).
914
- const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp);
949
+ // Always use the combined page, even if memory difficulty is 0 (it will be almost instant).
950
+ const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret);
915
951
  return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 429, body: page };
916
952
  }
917
953
  }
@@ -979,6 +1015,13 @@ class FingerprintEngine {
979
1015
  export const powMiddleware = (securityConfig) => {
980
1016
  const engine = new FingerprintEngine(securityConfig);
981
1017
 
1018
+ if (securityConfig.autotuning) {
1019
+ startThresholdAutoTuning({
1020
+ securityConfig: securityConfig,
1021
+ ...securityConfig.autotuning,
1022
+ });
1023
+ }
1024
+
982
1025
  return async (req, res, next) => {
983
1026
  const requestContext = {
984
1027
  clientIp: req.ip || req.socket?.remoteAddress || "unknown",
@@ -986,7 +1029,7 @@ export const powMiddleware = (securityConfig) => {
986
1029
  cookies: req.cookies,
987
1030
  query: req.query,
988
1031
  headers: req.headers,
989
- isStatic: isStaticResource(req),
1032
+ isStatic: isStaticResource(req.path),
990
1033
  // Add the newly required properties for full decoupling
991
1034
  rawHeaders: req.rawHeaders,
992
1035
  httpVersion: req.httpVersion,
package/package.json CHANGED
@@ -1,47 +1,47 @@
1
- {
2
- "name": "@anonympins/fingerprint",
3
- "version": "0.0.3",
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"
12
- },
13
- "files": [
14
- "fingerprint.js",
15
- "library.js",
16
- "README.md",
17
- "LICENSE"
18
- ],
19
- "repository": {
20
- "type": "git",
21
- "url": "git+https://github.com/anonympins/fingerprint.git"
22
- },
23
- "keywords": [
24
- "fingerprint",
25
- "bot",
26
- "anti-bot",
27
- "security",
28
- "express",
29
- "middleware",
30
- "proof-of-work",
31
- "pow",
32
- "rate-limiting",
33
- "mitigation",
34
- "captcha"
35
- ],
36
- "author": "anonympins",
37
- "license": "MIT",
38
- "bugs": {
39
- "url": "https://github.com/anonympins/fingerprint/issues"
40
- },
41
- "homepage": "https://github.com/anonympins/fingerprint#readme",
42
- "devDependencies": {
43
- "cookie-parser": "^1.4.6",
44
- "express": "^4.18.2",
45
- "vitest": "^4.1.11"
46
- }
47
- }
1
+ {
2
+ "name": "@anonympins/fingerprint",
3
+ "version": "0.0.4",
4
+ "description": "An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.",
5
+ "main": "fingerprint.js",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=20.0.0"
9
+ },
10
+ "scripts": {
11
+ "test": "vitest run"
12
+ },
13
+ "files": [
14
+ "fingerprint.js",
15
+ "library.js",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/anonympins/fingerprint.git"
22
+ },
23
+ "keywords": [
24
+ "fingerprint",
25
+ "bot",
26
+ "anti-bot",
27
+ "security",
28
+ "express",
29
+ "middleware",
30
+ "proof-of-work",
31
+ "pow",
32
+ "rate-limiting",
33
+ "mitigation",
34
+ "captcha"
35
+ ],
36
+ "author": "anonympins",
37
+ "license": "MIT",
38
+ "bugs": {
39
+ "url": "https://github.com/anonympins/fingerprint/issues"
40
+ },
41
+ "homepage": "https://github.com/anonympins/fingerprint#readme",
42
+ "devDependencies": {
43
+ "cookie-parser": "^1.4.6",
44
+ "express": "^4.18.2",
45
+ "vitest": "^4.1.11"
46
+ }
47
+ }