muaddib-scanner 2.11.57 → 2.11.58

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.11.57",
3
+ "version": "2.11.58",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "target": "node_modules",
3
- "timestamp": "2026-06-04T20:24:41.702Z",
3
+ "timestamp": "2026-06-04T21:33:40.755Z",
4
4
  "threats": [
5
5
  {
6
6
  "type": "string_mutation_obfuscation",
@@ -1,14 +1,16 @@
1
1
  const { NPM_PACKAGE_REGEX } = require('../shared/constants.js');
2
2
  const { debugLog } = require('../utils.js');
3
- const { acquireRegistrySlot, releaseRegistrySlot } = require('../shared/http-limiter.js');
3
+ const { acquireRegistrySlot, releaseRegistrySlot, signal429 } = require('../shared/http-limiter.js');
4
4
  const { computeAdvancedRegistrySignals } = require('../integrations/registry-signals.js');
5
5
 
6
6
  const REGISTRY_URL = 'https://registry.npmjs.org';
7
7
  const DOWNLOADS_URL = 'https://api.npmjs.org/downloads/point/last-week';
8
8
  const SEARCH_URL = 'https://registry.npmjs.org/-/v1/search';
9
9
 
10
- const REQUEST_TIMEOUT = 10000; // 10 seconds
11
- const MAX_RETRIES = 3;
10
+ // Env-tunable; defaults preserve prior behavior except MAX_RETRIES (3 → 5) for more headroom
11
+ // under sustained 429s during a large evaluate burst.
12
+ const REQUEST_TIMEOUT = Math.max(1000, parseInt(process.env.MUADDIB_REGISTRY_TIMEOUT_MS, 10) || 10000); // 10s default
13
+ const MAX_RETRIES = Math.max(1, parseInt(process.env.MUADDIB_REGISTRY_RETRIES, 10) || 5);
12
14
 
13
15
  /**
14
16
  * Create a timeout signal, with fallback for older Node versions.
@@ -31,10 +33,12 @@ async function fetchWithRetry(url) {
31
33
  response = await fetch(url, { signal });
32
34
  } catch {
33
35
  cleanup();
34
- // REG-001: Retry on timeout/abort instead of returning null immediately
36
+ // REG-001: Retry on timeout/abort instead of returning null immediately.
37
+ // Jittered exponential backoff avoids synchronized retry storms across the
38
+ // (up to MUADDIB_REGISTRY_CONCURRENCY) concurrent fetches.
35
39
  if (attempt < MAX_RETRIES - 1) {
36
40
  const backoff = Math.min(1000 * Math.pow(2, attempt), 8000);
37
- await new Promise(r => setTimeout(r, backoff));
41
+ await new Promise(r => setTimeout(r, Math.round(backoff * (0.5 + Math.random() * 0.5))));
38
42
  }
39
43
  continue;
40
44
  }
@@ -48,12 +52,16 @@ async function fetchWithRetry(url) {
48
52
  return null;
49
53
  }
50
54
 
51
- // 429 = rate limit, respect Retry-After header (capped at 30s)
55
+ // 429 = rate limit. Drain the SHARED token bucket so EVERY in-flight request
56
+ // (not just this one) backs off together — fixes the thundering-herd 429 storm
57
+ // that left ~17% of packages metadata-less in a local evaluate run. Then honor
58
+ // Retry-After (capped at 30s) with jitter so retries don't re-synchronize.
52
59
  if (response.status === 429) {
53
60
  try { await response.text(); } catch (e) { debugLog('response drain failed:', e.message); }
61
+ try { signal429(); } catch { /* limiter is best-effort */ }
54
62
  const retryAfter = parseInt(response.headers.get('retry-after'), 10);
55
- const delay = Math.min(retryAfter && retryAfter > 0 ? retryAfter * 1000 : 2000, 30000);
56
- await new Promise(r => setTimeout(r, delay));
63
+ const base = Math.min(retryAfter && retryAfter > 0 ? retryAfter * 1000 : 2000, 30000);
64
+ await new Promise(r => setTimeout(r, Math.round(base * (0.5 + Math.random() * 0.5))));
57
65
  continue;
58
66
  }
59
67
 
@@ -4,8 +4,8 @@
4
4
  * Centralized HTTP concurrency + rate limiter for npm registry requests.
5
5
  *
6
6
  * Two layers of protection:
7
- * 1. Concurrency semaphore (REGISTRY_SEMAPHORE_MAX = 10) — caps in-flight requests
8
- * 2. Rate limiter (RATE_LIMIT_PER_SEC = 30) — caps requests/second via token bucket
7
+ * 1. Concurrency semaphore (REGISTRY_SEMAPHORE_MAX, default 20, env MUADDIB_REGISTRY_CONCURRENCY) — caps in-flight requests
8
+ * 2. Rate limiter (RATE_LIMIT_PER_SEC, default 30, env MUADDIB_REGISTRY_RATE) — caps requests/second via token bucket
9
9
  *
10
10
  * Without rate limiting, 10 concurrent slots × fast-completing requests = 100+ req/s
11
11
  * bursts that trigger npm 429 responses → exponential backoff → scan times 10s→90s.
@@ -14,8 +14,11 @@
14
14
  * NOT covered: api.npmjs.org (different server), replicate.npmjs.com (CouchDB changes stream).
15
15
  */
16
16
 
17
- const REGISTRY_SEMAPHORE_MAX = 20;
18
- const RATE_LIMIT_PER_SEC = 30;
17
+ // Env-tunable so a constrained client (e.g. local/Windows `evaluate` runs that hit npm 429s during
18
+ // the ~1644-request metadata burst over 548 packages) can dial the burst down without code edits.
19
+ // Defaults preserve prior behavior (20 in-flight / 30 req/s).
20
+ const REGISTRY_SEMAPHORE_MAX = Math.max(1, parseInt(process.env.MUADDIB_REGISTRY_CONCURRENCY, 10) || 20);
21
+ const RATE_LIMIT_PER_SEC = Math.max(1, parseInt(process.env.MUADDIB_REGISTRY_RATE, 10) || 30);
19
22
 
20
23
  // --- Concurrency semaphore ---
21
24