@goodandready/dsh-key-rotation 0.8.69 → 0.8.71

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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.71 - 2026-10-22
4
+
5
+ ### Bug Fixes
6
+ - **Complete Chinese (zh) Localization for Interactive Routing Simulator (#514, PR #515)**:
7
+ - Added 13 missing Chinese translations to `zh` dictionary in `lib/client-src/10-locales.js` for the Interactive Routing Simulator dialog (`simulatorTitle`, `simulatorDesc`, `simulatorRun`, `simulatorRunning`, `simulatorSelectProvider`, `simulatorSelectModel`, `simulatorMaxAttempts`, `simulatorEvent`, `simulatorEventSuccess`, `simulatorEventRateLimit`, `simulatorEventServer500`, `simulatorResults`, `simulatorNoResults`).
8
+ - Recompiled `lib/client.js` bundle (194,681 bytes) meeting strict size and line constraints.
9
+ - Added automated test `test/issue-514-i18n-zh-simulator.test.mjs` verifying 100% key parity between English and Chinese dictionaries (248 keys each).
10
+
11
+ ## 0.8.70 - 2026-10-20
12
+
13
+ ### Bug Fixes
14
+ - **Fix Fatal Crash in sweepExpired & Isolate Clone Metadata (#513, GitHub #26)**:
15
+ - Guarded `sweepExpired` in `lib/pool.js` and the periodic sweep timer in `lib/index.js` with `if (!st || !(st.failedUntil instanceof Map)) continue;` to safely skip non-pool-state values.
16
+ - Isolated clone route metadata tracking inside `CloneLifecycleManager` (`this.knownClones`) rather than polluting `poolState` with `Set` objects (`${provider}:clones`).
17
+ - Added regression test `test/github-issue-26-sweep-expired.test.mjs` verifying `sweepExpired` robustness with heterogeneous data and clean `poolState` boundaries.
18
+
3
19
  ## 0.8.69 - 2026-10-19
4
20
 
5
21
  ### Features
package/lib/client.js CHANGED
@@ -300,6 +300,19 @@ window.__ModuleLoader__.load({
300
300
  'header.sub': '自动化多密钥轮换、429/5xx指数退避冷却、断路器保护及凭证安全 (~/.dsh/.credentials.yaml)。',
301
301
  'header.pools_badge': '个活跃密钥池',
302
302
  'header.keys_badge': '个已配置密钥',
303
+ simulatorTitle: '交互式路由模拟器',
304
+ simulatorDesc: '测试路由策略并在不向上游发送实际请求的情况下可视化密钥池的故障转移链路。',
305
+ simulatorRun: '运行模拟',
306
+ simulatorRunning: '模拟中…',
307
+ simulatorSelectProvider: '提供商',
308
+ simulatorSelectModel: '模型(可选)',
309
+ simulatorMaxAttempts: '最大尝试次数',
310
+ simulatorEvent: '模拟场景',
311
+ simulatorEventSuccess: '所有密钥均成功',
312
+ simulatorEventRateLimit: '密钥 1 触发速率限制 (429)',
313
+ simulatorEventServer500: '密钥 1 内部服务错误 (500)',
314
+ simulatorResults: '模拟追踪结果',
315
+ simulatorNoResults: '运行模拟以查看候选密钥执行链路。',
303
316
  sectionStats: '密钥池健康与遥测',
304
317
  sectionStatsDesc: '活跃密钥池、健康凭证和故障转移状态的实时遥测。',
305
318
  sectionFailover: '故障转移触发器与错误代码',
@@ -101,20 +101,37 @@ export class CloneLifecycleManager {
101
101
  this.concurrencyTracker = concurrencyTracker;
102
102
  this.logger = logger || console;
103
103
  this.drainingClones = new Map(); // cloneId -> { provider, ref, startedAt, timer }
104
+ this.knownClones = new Map(); // provider -> Set<string>
104
105
  }
105
106
 
106
107
  /**
107
108
  * Evaluates previous vs current expected clones for a provider.
108
109
  * If a clone was removed, marks it draining instead of instantly destroying it.
109
110
  * @param {string} provider
110
- * @param {Set<string>|string[]} prevClones
111
+ * @param {Set<string>|string[]} [prevClones]
111
112
  * @param {Set<string>|string[]} currentClones
112
- * @param {Function} onDeregister callback when draining completes and route is safe to remove
113
+ * @param {Function} [onDeregister] callback when draining completes and route is safe to remove
113
114
  */
114
- reconcileClones(provider, prevClones, currentClones, onDeregister) {
115
+ reconcileClones(provider, prevOrCurrent, currentOrDeregister, maybeDeregister) {
116
+ let prevClones;
117
+ let currentClones;
118
+ let onDeregister;
119
+
120
+ if (currentOrDeregister instanceof Set || Array.isArray(currentOrDeregister)) {
121
+ prevClones = prevOrCurrent;
122
+ currentClones = currentOrDeregister;
123
+ onDeregister = maybeDeregister;
124
+ } else {
125
+ prevClones = this.knownClones.get(provider) ?? [];
126
+ currentClones = prevOrCurrent;
127
+ onDeregister = currentOrDeregister;
128
+ }
129
+
115
130
  const prevSet = prevClones instanceof Set ? prevClones : new Set(prevClones || []);
116
131
  const currSet = currentClones instanceof Set ? currentClones : new Set(currentClones || []);
117
132
 
133
+ this.knownClones.set(provider, currSet);
134
+
118
135
  for (const cloneId of prevSet) {
119
136
  if (!currSet.has(cloneId)) {
120
137
  this.startDrain(cloneId, provider, onDeregister);
package/lib/index.js CHANGED
@@ -232,6 +232,7 @@ export function apply(ctx, config = {}) {
232
232
  const id = setInterval(() => {
233
233
  const now = Date.now();
234
234
  for (const st of poolState.values()) {
235
+ if (!st || !(st.failedUntil instanceof Map)) continue;
235
236
  for (const [ref, until] of [...(st.failedUntil?.entries() ?? [])]) {
236
237
  if (until <= now && !st.probedAt?.has(ref)) {
237
238
  st.events.push({ at: until, ref, reason: 'probe', cooldownMs: 0, type: 'probe' });
@@ -285,8 +285,7 @@ export function cleanupRemovedProviders({
285
285
  if (cloneLifecycle && typeof cloneLifecycle.reconcileClones === 'function') {
286
286
  for (const p of cfg.providers ?? []) {
287
287
  const liveClones = new Set([...expectedClones].filter(c => c.startsWith(`${p.provider}-`)));
288
- cloneLifecycle.reconcileClones(p.provider, poolState.get(`${p.provider}:clones`) ?? [], liveClones);
289
- poolState.set(`${p.provider}:clones`, liveClones);
288
+ cloneLifecycle.reconcileClones(p.provider, liveClones);
290
289
  }
291
290
  }
292
291
  }
package/lib/pool.js CHANGED
@@ -226,6 +226,7 @@ export function sweepExpired(poolState, now = Date.now(), activeRefs = null, quo
226
226
  let cleared = 0;
227
227
  const activeSet = activeRefs ? new Set(activeRefs) : null;
228
228
  for (const st of poolState.values()) {
229
+ if (!st || !(st.failedUntil instanceof Map)) continue;
229
230
  // #338: decay penalty failCounts for stable keys in maintenance sweep
230
231
  decayPenalties({ state: st }, now);
231
232
  for (const [ref, until] of [...st.failedUntil.entries()]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-key-rotation",
3
- "version": "0.8.69",
3
+ "version": "0.8.71",
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": [