muaddib-scanner 2.11.109 → 2.11.110

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.109",
3
+ "version": "2.11.110",
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-12T16:19:40.738Z",
3
+ "timestamp": "2026-06-12T17:33:52.917Z",
4
4
  "threats": [
5
5
  {
6
6
  "type": "string_mutation_obfuscation",
@@ -98,7 +98,10 @@ function computeBackoffTransition(state, event, consts = {}) {
98
98
  // Full-quiet reset (the incident is over, restart at base).
99
99
  const quietResetMs = s.lastPauseMs * 2 + base * 5;
100
100
  if (s.last429At && now - s.last429At > quietResetMs) s.level = 0;
101
- s.level += 1;
101
+ // Cap: beyond ~12 both the pause (60s) and the rate (1/s floor) are
102
+ // saturated — an unbounded counter only makes operators read "level 25"
103
+ // as an emergency when it carries no additional behavior.
104
+ s.level = Math.min(s.level + 1, 12);
102
105
  const pause = Math.min(max, base * 2 ** (s.level - 1));
103
106
  s.lastPauseMs = pause;
104
107
  s.last429At = now;
@@ -159,20 +162,37 @@ function hostForUrl(url) {
159
162
  try { return new URL(url).hostname || DEFAULT_HOST; } catch { return DEFAULT_HOST; }
160
163
  }
161
164
 
162
- function _effectiveRate() {
165
+ function _effectiveRate(level = 0) {
166
+ let rate = RATE_LIMIT_PER_SEC;
163
167
  if (BOOT_SLOWSTART_MS > 0 && Date.now() - _bootAt < BOOT_SLOWSTART_MS) {
164
- return Math.max(1, Math.floor(RATE_LIMIT_PER_SEC / 4));
168
+ rate = Math.max(1, Math.floor(rate / 4));
165
169
  }
166
- return RATE_LIMIT_PER_SEC;
170
+ // Rate-by-level (the partial-throttle fix, 2026-06-12 evening): the pause
171
+ // alone cannot converge against a registry that PERMANENTLY rejects a
172
+ // fraction of requests — every post-pause burst guarantees a 429, every
173
+ // window stays dirty, the level ratchets to the cap and throughput pins at
174
+ // ~10 req/min forever (observed: level 25, zero de-escalations, while
175
+ // tarball downloads flowed fine). Halving the SEND RATE per level (floor
176
+ // 1 req/s) makes a clean 30s window reachable — 30 spaced probes instead of
177
+ // one burst — so the AIMD de-escalation actually fires and the brain
178
+ // CONVERGES on the registry's real granted budget instead of oscillating
179
+ // burst→reject at the cap.
180
+ if (level > 0) rate = Math.max(1, Math.floor(rate / 2 ** Math.min(level, 5)));
181
+ return rate;
167
182
  }
168
183
 
169
184
  function _refillTokens(b) {
170
185
  const now = Date.now();
171
186
  if (now < b.bo.pauseUntil) return; // backoff pause: no refills, no grants
172
- const rate = _effectiveRate();
187
+ const rate = _effectiveRate(b.bo.level);
173
188
  if (b.bo.pauseUntil > b.lastRefill) {
174
- // First refill after a backoff pause: slow start at half budget.
175
- b.tokens = Math.max(1, Math.floor(rate / 2));
189
+ // First refill after a backoff pause: PROBE OF ONE. The previous half-
190
+ // budget restart fired a 10-request burst the instant the pause expired —
191
+ // against a partially-throttling registry that burst GUARANTEED a 429 and
192
+ // re-armed the next pause. One spaced probe at a time is how the level's
193
+ // reduced rate (see _effectiveRate) gets a chance to produce the clean
194
+ // window that de-escalates.
195
+ b.tokens = 1;
176
196
  b.lastRefill = now;
177
197
  return;
178
198
  }