@anonympins/fingerprint 0.0.9 → 0.1.0

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/README.md CHANGED
@@ -25,7 +25,7 @@ The process unfolds in three steps:
25
25
  * **Low to Medium Suspicion**: A combined CPU and Memory Proof-of-Work (PoW) challenge is issued. The difficulty of both the CPU (hash calculation) and Memory (allocation and computation) components scales progressively with the suspicion score. For low scores, the memory challenge is negligible, making it primarily a CPU task.
26
26
  * **High Suspicion**: For the most suspicious requests, the system issues a high-difficulty combined CPU/Memory challenge. The architecture allows for plugging in more complex challenges like CAPTCHAs if needed.
27
27
 
28
- Once the challenge is solved, a clearance "ticket" is issued via a secure cookie, exempting the user from new challenges for a set period.
28
+ Once the challenge is solved, a clearance "ticket" is issued via a secure cookie, exempting the user from new challenges for a set period. For API clients, the challenge is delivered as a `429` JSON response, and the client is expected to solve it and retry the request.
29
29
 
30
30
  ## Features
31
31
 
@@ -81,21 +81,33 @@ const securityConfig = {
81
81
  headerAnomalyScore: 0.1, // Penalizes abnormal headers (missing UA, etc.)
82
82
  requestPatternScore: 0.6,// Penalizes bot-like request sequences (scraping, etc.)
83
83
  inconsistencyScore: 0.8, // Strongly penalizes inconsistency between the current and initial fingerprint (stolen cookie)
84
+ behaviorScore: 0.7, // Penalizes non-human interactions (no mouse/keyboard activity)
84
85
  honeypotScore: 1.0 // Strongly penalizes bots filling hidden form fields
85
86
  },
87
+ // A new, non-suspicious device will always have its score adjusted to a minimum of 1, ensuring it receives a minimal, almost imperceptible challenge on its first visit.
86
88
  thresholds: {
87
89
  low: 20, // Score from which a CPU challenge is issued
88
90
  medium: 45, // Score for a more difficult combined CPU/Memory challenge
89
91
  high: 75, // Score for a very difficult challenge
90
92
  block: 95, // Score above which the request is blocked outright (HTTP 403)
91
- isStaticResource: (req) => req.path.startsWith('/static/') // Optional: Custom function to identify static resources
93
+ isStaticResource: (req) => req.path.startsWith('/static/'), // Optional: Custom function to identify static resources
94
+ isApiRequest: (req) => req.path.startsWith('/api/') || req.headers.accept?.includes('application/json') // Optional: Custom function to identify API requests
92
95
  },
96
+ // (Optional) Configure the duration (in milliseconds) for various temporary data.
97
+ ticketMaxAge: 3600000, // 1 hour. Duration for which a solved challenge ticket is valid.
98
+ challengeTtl: 300000, // 5 minutes. Time during which a challenge nonce is valid.
99
+ deviceIdCookieMaxAge: undefined, // By default, it's a session cookie. Set a value in ms for a persistent cookie.
100
+
93
101
  patterns: { // (Optional) Initial values for request pattern detection, optimized by auto-tuner if enabled.
94
- velocityThreshold: 200, // ms between requests to be considered "fast"
95
- burstThreshold: 500, // ms for identical requests to be a "burst"
96
- scrapeThreshold: 1000, // ms for sequential requests to be "scraping"
97
- historySize: 10, // Number of requests to keep for pattern analysis
98
- decayFactor: 0.9, // How quickly the pattern score decays over time
102
+ velocityThreshold: 200, // ms between requests to be considered "fast"
103
+ burstThreshold: 500, // ms for identical requests to be a "burst"
104
+ scrapeThreshold: 1000, // ms for sequential requests to be "scraping"
105
+ scrapeBurstWeight: 40, // Additional weight for repeated scraping patterns
106
+ sequenceLength: 3, // Length of a request sequence to detect (e.g., A->B->C)
107
+ sequenceWeight: 60, // Penalty for repeating a sequence
108
+ historySize: 10, // Number of requests to keep for pattern analysis
109
+ decayFactor: 0.9, // How quickly the pattern score decays over time
110
+ inactivityReset: 30000, // ms of inactivity after which the pattern score is reset
99
111
  },
100
112
  honeypot: {
101
113
  // List of field names that are traps for bots.
@@ -344,9 +356,13 @@ initializeClient({
344
356
  // (Optional) Enables automatic protection for `fetch` requests.
345
357
  // If the `fetch` object is present, the protection is active.
346
358
  fetch: {
347
- // (Optional) An array of domains to protect. If empty or not provided,
348
- // it protects same-origin requests by default.
349
- targetDomains: ['api.yourdomain.com', 'auth.yourdomain.com']
359
+ // (Optional) An array of domains to protect. If empty or not provided, it protects same-origin requests by default.
360
+ targetDomains: ['api.yourdomain.com', 'auth.yourdomain.com'],
361
+
362
+ // (Optional, default: true) If enabled, the client will automatically intercept 429 challenge responses,
363
+ // solve the PoW in the background, and retry the original request with the solution.
364
+ // This makes the protection seamless for API clients that use this library.
365
+ handleChallenges: true
350
366
  }
351
367
  });
352
368
  ```
@@ -1,4 +1,5 @@
1
1
  import { cyrb53, FingerprintBuilder } from './fingerprint.builder.js';
2
+ import { solveChallenge } from './pow.solver.js';
2
3
 
3
4
  const ClientLibrary = {
4
5
  // Cache pour éviter de recalculer les constantes (Hardware, etc.)
@@ -320,6 +321,44 @@ const ClientLibrary = {
320
321
  this.addFetchInterceptor(fingerprintInterceptor);
321
322
  },
322
323
 
324
+ /**
325
+ * Intercepte une réponse de challenge JSON, le résout, et réessaie la requête.
326
+ * @param {Response} response - La réponse initiale (potentiellement 429).
327
+ * @param {RequestInfo} resource - La ressource de la requête originale.
328
+ * @param {RequestInit} options - Les options de la requête originale.
329
+ * @returns {Promise<Response>} - La réponse de la requête réessayée.
330
+ * @private
331
+ */
332
+ async solveChallengeAndRetry(response, resource, options) {
333
+ if (response.status !== 429) {
334
+ return response;
335
+ }
336
+
337
+ try {
338
+ const challengeData = await response.json();
339
+ if (!challengeData.challenge || !challengeData.challenge.type) {
340
+ return response; // Pas un challenge JSON valide
341
+ }
342
+
343
+ console.log(`[Fingerprint] Received a '${challengeData.challenge.type}' challenge. Solving...`);
344
+ const solution = await solveChallenge(challengeData.challenge);
345
+ console.log('[Fingerprint] Challenge solved. Retrying original request.');
346
+
347
+ // Ajouter la solution aux paramètres de la requête pour le nouvel essai
348
+ const url = new URL((resource instanceof Request) ? resource.url : String(resource), window.location.origin);
349
+ url.searchParams.set('pow_type', challengeData.challenge.type);
350
+ url.searchParams.set('pow_nonce', challengeData.challenge.nonce);
351
+ // La solution peut être un objet (pour les challenges combinés) ou une valeur simple
352
+ Object.entries(solution).forEach(([key, value]) => {
353
+ url.searchParams.set(`pow_solution_${key}`, value);
354
+ });
355
+
356
+ return this._originalFetch(url.toString(), options);
357
+ } catch (e) {
358
+ console.error('[Fingerprint] Failed to solve or retry challenge:', e);
359
+ return response; // Retourne la réponse 429 originale en cas d'échec
360
+ }
361
+ },
323
362
  /**
324
363
  * @typedef {object} ClientConfig
325
364
  * @property {boolean} [mouse=true] - Activer le suivi de l'entropie de la souris.
@@ -339,7 +378,7 @@ const ClientLibrary = {
339
378
  mouse = true,
340
379
  keystrokes = true,
341
380
  honeypots = [],
342
- fetch: fetchConfig,
381
+ fetch: fetchConfig = {},
343
382
  } = config;
344
383
 
345
384
  if (mouse) {
@@ -351,8 +390,17 @@ const ClientLibrary = {
351
390
  if (honeypots.length > 0) {
352
391
  this.initializeHoneypots(honeypots);
353
392
  }
354
- if (fetchConfig !== undefined) {
393
+ // On vérifie si l'objet fetch est présent pour activer l'interception
394
+ if (Object.keys(fetchConfig).length > 0 || config.fetch) {
355
395
  this.initializeFetch(fetchConfig.targetDomains);
396
+
397
+ // Ajoute l'intercepteur pour la résolution de challenge
398
+ if (fetchConfig.handleChallenges !== false) {
399
+ this.addFetchInterceptor(async (resource, options, next) => {
400
+ const response = await next(resource, options);
401
+ return this.solveChallengeAndRetry(response, resource, options);
402
+ });
403
+ }
356
404
  }
357
405
  }
358
406
  };
@@ -394,6 +442,7 @@ export const addFetchInterceptor = ClientLibrary.addFetchInterceptor.bind(Client
394
442
  export const patchGlobalFetch = ClientLibrary.patchGlobalFetch.bind(ClientLibrary);
395
443
  export const initializeFetch = ClientLibrary.initializeFetch.bind(ClientLibrary);
396
444
  export const initializeClient = ClientLibrary.initializeClient.bind(ClientLibrary);
445
+ export const solveChallengeAndRetry = ClientLibrary.solveChallengeAndRetry.bind(ClientLibrary);
397
446
 
398
447
  // Export the internal object for testing purposes
399
448
  export default ClientLibrary;