@goodandready/dsh-key-rotation 0.8.7 → 0.8.10

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/lib/routes-ops.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  NS,
9
9
  } from './http-bridge.js';
10
10
  import {
11
+ isLoopbackAddress,
11
12
  isTrustedBridgeRequest,
12
13
  keyTail,
13
14
  envValue,
@@ -48,6 +49,7 @@ export function registerOpsRoutes(ctx, deps) {
48
49
  getRotationDisabled,
49
50
  setRotationDisabled,
50
51
  circuitBreaker,
52
+ quotaStore,
51
53
  } = deps;
52
54
 
53
55
  // ── status route: what the settings card cannot know on its own ──
@@ -149,7 +151,7 @@ export function registerOpsRoutes(ctx, deps) {
149
151
  totalUsage: (() => { let s = 0; if (pool.state.usageCounts) for (const v of pool.state.usageCounts.values()) s += v; return s; })(),
150
152
  // #225: aggregate p95 across the pool's keys
151
153
  p95: (() => {
152
- const vals = (pool.refs ?? []).map((r) => latencyHistogram.snapshot(r)).filter((s) => s && s.p95 != null).map((s) => s.p95);
154
+ const vals = (pool.refs ?? []).map((r) => (typeof latencyHistogram?.snapshot === 'function' ? latencyHistogram.snapshot(r) : null)).filter((s) => s && s.p95 != null).map((s) => s.p95);
153
155
  return vals.length ? Math.round(Math.max(...vals)) : null;
154
156
  })(),
155
157
  latencySloMs,
@@ -158,9 +160,11 @@ export function registerOpsRoutes(ctx, deps) {
158
160
  // #208: today/week spend + configured budget for the card
159
161
  todayCost: costForDay(pool.state.costDays),
160
162
  weeklyCost: costForWeek(pool.state.costDays, now),
161
- budgetDaily: providerBudgets.get(pool.base)?.costBudgetDaily ?? 0,
162
- budgetWeekly: providerBudgets.get(pool.base)?.costBudgetWeekly ?? 0,
163
- pauseOnBudget: providerBudgets.get(pool.base)?.pauseOnBudget ?? false,
163
+ budgetDaily: (providerBudgets?.get ? providerBudgets.get(pool.base) : providerBudgets?.[pool.base])?.costBudgetDaily ?? 0,
164
+ budgetWeekly: (providerBudgets?.get ? providerBudgets.get(pool.base) : providerBudgets?.[pool.base])?.costBudgetWeekly ?? 0,
165
+ pauseOnBudget: (providerBudgets?.get ? providerBudgets.get(pool.base) : providerBudgets?.[pool.base])?.pauseOnBudget ?? false,
166
+ routingStrategy: pool.routingStrategy ?? runtime.routingStrategy ?? 'round-robin',
167
+ proactiveRateLimitGuard: pool.proactiveRateLimitGuard ?? runtime.proactiveRateLimitGuard ?? true,
164
168
  });
165
169
  } catch (e) {
166
170
  console.warn(`[dsh-key-rotation] status: pool ${pool.base} failed: ${String(e?.message ?? e)} ${e?.stack ?? ''}`);
@@ -305,6 +309,14 @@ export function registerOpsRoutes(ctx, deps) {
305
309
  try {
306
310
  if (req.method === 'DELETE') {
307
311
  await credentialsService.unset(ref);
312
+ for (const st of poolState.values()) {
313
+ st.failedUntil?.delete(ref);
314
+ st.failCounts?.delete(ref);
315
+ st.authFailCounts?.delete(ref);
316
+ st.brokenUntil?.delete(ref);
317
+ if (st.lastUsed === ref) st.lastUsed = undefined;
318
+ }
319
+ try { lastTestCache?.delete?.(ref); } catch (_) {}
308
320
  json(res, 200, { ok: true, ref });
309
321
  return;
310
322
  }
@@ -366,8 +378,8 @@ export function registerOpsRoutes(ctx, deps) {
366
378
  if (ref) {
367
379
  let found = false;
368
380
  for (const st of poolState.values()) {
369
- if (st.failedUntil.has(ref) || st.failCounts?.has(ref)) {
370
- st.failedUntil.delete(ref);
381
+ if (st.failedUntil?.has(ref) || st.failCounts?.has(ref) || st.authFailCounts?.has(ref) || st.brokenUntil?.has(ref)) {
382
+ st.failedUntil?.delete(ref);
371
383
  st.failCounts?.delete(ref);
372
384
  st.authFailCounts?.delete(ref);
373
385
  st.brokenUntil?.delete(ref);
@@ -445,7 +457,7 @@ export function registerOpsRoutes(ctx, deps) {
445
457
  if (exhausted) exhaustedAny = true;
446
458
  pools[pool.base] = { healthy, total, exhausted, healthScore: computeHealthScore(pool.state) };
447
459
  }
448
- json(res, 200, { status: exhaustedAny ? 'degraded' : 'ok', pools, exhaustedAny, latency: latencyHistogram.snapshotAll(), quota: quotaStore.snapshot() });
460
+ json(res, 200, { status: exhaustedAny ? 'degraded' : 'ok', pools, exhaustedAny, latency: latencyHistogram.snapshotAll(), quota: typeof quotaStore?.snapshot === 'function' ? quotaStore.snapshot() : null });
449
461
  },
450
462
  }), 'dsh-key-rotation: health');
451
463
 
@@ -513,7 +525,10 @@ export function registerOpsRoutes(ctx, deps) {
513
525
  path: SANDBOX_CACHE_PATH,
514
526
  handler: (req, res) => {
515
527
  if (!isTrustedBridgeRequest(req)) { json(res, 403, { error: { code: 'forbidden', message: 'dsh-key-rotation: cache is local-only' } }); return; }
516
- json(res, 200, lastTestCache.snapshot());
528
+ const cacheSnap = typeof lastTestCache?.snapshot === 'function'
529
+ ? lastTestCache.snapshot()
530
+ : (lastTestCache instanceof Map ? Object.fromEntries(lastTestCache) : (lastTestCache ?? {}));
531
+ json(res, 200, cacheSnap);
517
532
  },
518
533
  }), 'dsh-key-rotation: sandbox cache');
519
534
 
@@ -594,9 +609,21 @@ export function registerOpsRoutes(ctx, deps) {
594
609
  return;
595
610
  }
596
611
  const cleared = st.failedUntil.size;
597
- st.failedUntil.clear(); st.failCounts?.clear(); st.brokenUntil?.clear();
598
- console.warn(`[dsh-key-rotation] pool ${provider} RESET via webhook action`);
599
- json(res, 200, { ok: true, action, provider, cleared });
612
+ st.failedUntil.clear();
613
+ st.failCounts?.clear();
614
+ st.authFailCounts?.clear();
615
+ st.brokenUntil?.clear();
616
+ st.switches = 0;
617
+ st.lastReason = undefined;
618
+ st.lastSwitchAt = undefined;
619
+ let circuitReset = false;
620
+ const br = circuitBreaker ?? buildRuntime().breaker;
621
+ if (br) {
622
+ if (typeof br.reset === 'function') { br.reset(provider); circuitReset = true; }
623
+ else if (typeof br.onSuccess === 'function') { br.onSuccess(provider); circuitReset = true; }
624
+ }
625
+ console.warn(`[dsh-key-rotation] pool ${provider} RESET via webhook action (circuitReset=${circuitReset})`);
626
+ json(res, 200, { ok: true, action, provider, cleared, circuitReset });
600
627
  return;
601
628
  }
602
629
  json(res, 400, { error: { code: 'unknown-action', message: `dsh-key-rotation: unknown action '${action}'` } });
package/lib/sandbox.js CHANGED
@@ -83,7 +83,15 @@ export class SandboxRunner {
83
83
  res = await doFetch();
84
84
  } catch (e) {
85
85
  if (e && e.name === 'AbortError') return { ok: false, code: 'timeout', latencyMs: Date.now() - started };
86
- return { ok: false, code: 'network', latencyMs: Date.now() - started };
86
+ // ponytail: 1 retry on transient socket network errors before marking broken
87
+ await new Promise((r) => setTimeout(r, PROBE_RETRY_DELAY_MS));
88
+ if (ctrl.signal.aborted) return { ok: false, code: 'timeout', latencyMs: Date.now() - started };
89
+ try {
90
+ res = await doFetch();
91
+ } catch (e2) {
92
+ if (e2 && e2.name === 'AbortError') return { ok: false, code: 'timeout', latencyMs: Date.now() - started };
93
+ return { ok: false, code: 'network', latencyMs: Date.now() - started };
94
+ }
87
95
  }
88
96
  // ponytail: 1 retry on 5xx — naive; classifier refines if needed
89
97
  if (res.status >= 500 && res.status < 600) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-key-rotation",
3
- "version": "0.8.7",
3
+ "version": "0.8.10",
4
4
  "packageManager": "pnpm@10.33.2",
5
5
  "description": "Per-provider API key rotation for DeepSeek Harness: a key pool per provider, auto-created clone routes, and switching to the next key on quota/rate-limit errors. Includes a Settings section (Key Rotation) to edit the key pools, cooldown and switch codes.",
6
6
  "keywords": [