@goodandready/dsh-key-rotation 0.8.6 → 0.8.8

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/client.js CHANGED
@@ -27,6 +27,10 @@ window.__ModuleLoader__.load({
27
27
  // -------------------------------------------------------------- i18n
28
28
  const en = {
29
29
  title: 'Key Rotation',
30
+ 'header.title': 'Key Rotation & Failover',
31
+ 'header.sub': 'Automated multi-key round-robin rotation, 429/5xx exponential backoff cooldowns, circuit breaker failover protection, and credential security (~/.dsh/.credentials.yaml).',
32
+ 'header.pools_badge': 'pools active',
33
+ 'header.keys_badge': 'keys configured',
30
34
  sectionStats: 'Pool Health & Telemetry',
31
35
  sectionStatsDesc: 'Real-time telemetry of active key pools, healthy credentials, and failover status.',
32
36
  sectionFailover: 'Failover Triggers & Error Codes',
@@ -137,7 +141,7 @@ window.__ModuleLoader__.load({
137
141
  totalRequestsTitle: 'total requests',
138
142
  exportProvider: 'Export this provider',
139
143
  recentFailures: 'Recent failures ({n})',
140
- noActivePools: t('noActivePools'),
144
+ noActivePools: 'No active pools',
141
145
  headerPoolsTitle: 'Key rotation pools',
142
146
  pausedBudget: 'paused',
143
147
  sortUsage: 'Sort by usage',
@@ -244,7 +248,10 @@ window.__ModuleLoader__.load({
244
248
  // на поле значения, поэтому имя занимает свою строку, а служебная строка
245
249
  // под ним ужимается сама.
246
250
  const CARD_CSS = [
247
- '.krot{display:flex;flex-direction:column;gap:18px;max-width:960px;padding:6px 0 24px;box-sizing:border-box}',
251
+ '.krot{display:flex;flex-direction:column;gap:20px;max-width:960px;padding:6px 0 24px;box-sizing:border-box}',
252
+ '.krot-header{display:flex;flex-direction:column;gap:8px;padding-bottom:16px;border-bottom:1px solid var(--dsw-alias-border-l2)}',
253
+ '.krot-page-title{font-size:20px;font-weight:700;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:10px;flex-wrap:wrap}',
254
+ '.krot-page-sub{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5}',
248
255
  '.krot p{margin:0}',
249
256
  '.krot-hint{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}',
250
257
  '.krot-err{font-size:13px;color:var(--dsw-alias-state-error-primary);padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);display:flex;align-items:center;gap:8px}',
@@ -1359,8 +1366,25 @@ window.__ModuleLoader__.load({
1359
1366
  )
1360
1367
  ) : null;
1361
1368
 
1369
+ const headerSection = h('div', { className: 'krot-header' },
1370
+ h('div', { className: 'krot-page-title' },
1371
+ '🔄 ' + t('header.title'),
1372
+ h('span', { className: 'krot-badge ' + (totalPoolsCount > 0 ? 'krot-badge-ok' : 'krot-badge-warn') },
1373
+ totalPoolsCount > 0 ? (totalPoolsCount + ' ' + t('header.pools_badge')) : t('noActivePools')
1374
+ ),
1375
+ h('span', { className: 'krot-badge ' + (totalKeysCount > 0 ? 'krot-badge-ok' : 'krot-badge-warn') },
1376
+ totalKeysCount + ' ' + t('header.keys_badge')
1377
+ ),
1378
+ h('span', { className: 'krot-badge ' + (healthyKeysCount === totalKeysCount && totalKeysCount > 0 ? 'krot-badge-ok' : 'krot-badge-warn') },
1379
+ healthyKeysCount + '/' + totalKeysCount + ' ' + t('keyReady')
1380
+ )
1381
+ ),
1382
+ h('div', { className: 'krot-page-sub' }, t('header.sub'))
1383
+ );
1384
+
1362
1385
  return h('div', { className: 'krot' },
1363
1386
  confirmModalEl,
1387
+ headerSection,
1364
1388
  statsSection,
1365
1389
  poolsSection,
1366
1390
  failoverSection,
package/lib/index.js CHANGED
@@ -827,6 +827,7 @@ export function apply(ctx, config = {}) {
827
827
  getRotationDisabled: () => rotationDisabled,
828
828
  setRotationDisabled: (v) => { rotationDisabled = v; },
829
829
  circuitBreaker: moduleBreaker,
830
+ quotaStore,
830
831
  });
831
832
 
832
833
  ctx.effect(() => ctx.on('llm/stream', (options, next) => {
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,9 @@ 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,
164
166
  });
165
167
  } catch (e) {
166
168
  console.warn(`[dsh-key-rotation] status: pool ${pool.base} failed: ${String(e?.message ?? e)} ${e?.stack ?? ''}`);
@@ -305,6 +307,14 @@ export function registerOpsRoutes(ctx, deps) {
305
307
  try {
306
308
  if (req.method === 'DELETE') {
307
309
  await credentialsService.unset(ref);
310
+ for (const st of poolState.values()) {
311
+ st.failedUntil?.delete(ref);
312
+ st.failCounts?.delete(ref);
313
+ st.authFailCounts?.delete(ref);
314
+ st.brokenUntil?.delete(ref);
315
+ if (st.lastUsed === ref) st.lastUsed = undefined;
316
+ }
317
+ try { lastTestCache?.delete?.(ref); } catch (_) {}
308
318
  json(res, 200, { ok: true, ref });
309
319
  return;
310
320
  }
@@ -366,8 +376,8 @@ export function registerOpsRoutes(ctx, deps) {
366
376
  if (ref) {
367
377
  let found = false;
368
378
  for (const st of poolState.values()) {
369
- if (st.failedUntil.has(ref) || st.failCounts?.has(ref)) {
370
- st.failedUntil.delete(ref);
379
+ if (st.failedUntil?.has(ref) || st.failCounts?.has(ref) || st.authFailCounts?.has(ref) || st.brokenUntil?.has(ref)) {
380
+ st.failedUntil?.delete(ref);
371
381
  st.failCounts?.delete(ref);
372
382
  st.authFailCounts?.delete(ref);
373
383
  st.brokenUntil?.delete(ref);
@@ -445,7 +455,7 @@ export function registerOpsRoutes(ctx, deps) {
445
455
  if (exhausted) exhaustedAny = true;
446
456
  pools[pool.base] = { healthy, total, exhausted, healthScore: computeHealthScore(pool.state) };
447
457
  }
448
- json(res, 200, { status: exhaustedAny ? 'degraded' : 'ok', pools, exhaustedAny, latency: latencyHistogram.snapshotAll(), quota: quotaStore.snapshot() });
458
+ json(res, 200, { status: exhaustedAny ? 'degraded' : 'ok', pools, exhaustedAny, latency: latencyHistogram.snapshotAll(), quota: typeof quotaStore?.snapshot === 'function' ? quotaStore.snapshot() : null });
449
459
  },
450
460
  }), 'dsh-key-rotation: health');
451
461
 
@@ -513,7 +523,10 @@ export function registerOpsRoutes(ctx, deps) {
513
523
  path: SANDBOX_CACHE_PATH,
514
524
  handler: (req, res) => {
515
525
  if (!isTrustedBridgeRequest(req)) { json(res, 403, { error: { code: 'forbidden', message: 'dsh-key-rotation: cache is local-only' } }); return; }
516
- json(res, 200, lastTestCache.snapshot());
526
+ const cacheSnap = typeof lastTestCache?.snapshot === 'function'
527
+ ? lastTestCache.snapshot()
528
+ : (lastTestCache instanceof Map ? Object.fromEntries(lastTestCache) : (lastTestCache ?? {}));
529
+ json(res, 200, cacheSnap);
517
530
  },
518
531
  }), 'dsh-key-rotation: sandbox cache');
519
532
 
@@ -594,9 +607,21 @@ export function registerOpsRoutes(ctx, deps) {
594
607
  return;
595
608
  }
596
609
  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 });
610
+ st.failedUntil.clear();
611
+ st.failCounts?.clear();
612
+ st.authFailCounts?.clear();
613
+ st.brokenUntil?.clear();
614
+ st.switches = 0;
615
+ st.lastReason = undefined;
616
+ st.lastSwitchAt = undefined;
617
+ let circuitReset = false;
618
+ const br = circuitBreaker ?? buildRuntime().breaker;
619
+ if (br) {
620
+ if (typeof br.reset === 'function') { br.reset(provider); circuitReset = true; }
621
+ else if (typeof br.onSuccess === 'function') { br.onSuccess(provider); circuitReset = true; }
622
+ }
623
+ console.warn(`[dsh-key-rotation] pool ${provider} RESET via webhook action (circuitReset=${circuitReset})`);
624
+ json(res, 200, { ok: true, action, provider, cleared, circuitReset });
600
625
  return;
601
626
  }
602
627
  json(res, 400, { error: { code: 'unknown-action', message: `dsh-key-rotation: unknown action '${action}'` } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-key-rotation",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
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": [