@goodandready/dsh-key-rotation 0.7.34 → 0.7.36
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/README.md +16 -1
- package/README.ru.md +16 -1
- package/README.zh.md +16 -1
- package/lib/client.js +95 -32
- package/lib/index.js +57 -292
- package/lib/pool.js +110 -1
- package/package.json +2 -3
- package/lib/agent-budget.js +0 -68
- package/lib/canary.js +0 -63
- package/lib/incident.js +0 -76
- package/lib/maintenance.js +0 -64
- package/lib/region.js +0 -50
- package/lib/shadow.js +0 -81
package/README.md
CHANGED
|
@@ -251,4 +251,19 @@ All management routes require loopback authentication (`127.0.0.1` / `::1`) with
|
|
|
251
251
|
|
|
252
252
|
## 📄 License
|
|
253
253
|
|
|
254
|
-
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
254
|
+
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
255
|
+
|
|
256
|
+
### v0.7.36
|
|
257
|
+
- **Architecture de-bloat & hardening**: Removed 6 unused/overengineered modules (`shadow`, `incident`, `agent-budget`, `region`, `canary`, `maintenance`) and dead route registrations.
|
|
258
|
+
- **High-throughput buildRuntime memoization**: Eliminates per-token deep-cloning and schema validation on every streaming chunk.
|
|
259
|
+
- **Atomic round-robin pointer rotation**: Concurrent requests advance pointer immediately on candidate selection, eliminating race conditions on simultaneous tool calls.
|
|
260
|
+
- **Enhanced switchable error detection**: Direct parsing of HTTP status codes (`429`, `401`, `403`, `5xx`) and gRPC codes (`RESOURCE_EXHAUSTED`, `UNAVAILABLE`) alongside regex fallback.
|
|
261
|
+
- **Informative user exhaustion messaging**: Clear countdown notice with next key recovery ETA when all keys in a pool are cooling down.
|
|
262
|
+
- **Smart polling**: Client background polling paused when browser tab is inactive (`document.visibilityState`).
|
|
263
|
+
|
|
264
|
+
### v0.7.35
|
|
265
|
+
- **Lifecycle Cleanups**: Wrapped `credentials.resolve` patch and `ctx.on` event handlers (`llm/stream`, `agent/request-error`) in `ctx.effect` scopes with guaranteed unmount cleanup (#238, #239).
|
|
266
|
+
- **Settings & Secret Roles**: Added `.role('secret')` to `incidentGitHubToken` and `webhookActionToken` in `Config` schema for automatic UI masking (#237).
|
|
267
|
+
- **Settings Architecture & UI**: Added native `settingsScope` snapshot reading/saving in settings card with graceful bridge fallback (#235).
|
|
268
|
+
- **Localization**: Localized `settings.section` sidebar fallback label via `t('title')` and registered `zh` dictionary in `ctx.locale` alongside `en` and `ru` (#236).
|
|
269
|
+
- **Dead Code Purge**: Removed obsolete `mountDashboard` routine after header-chip migration (#240).
|
package/README.ru.md
CHANGED
|
@@ -251,4 +251,19 @@ dsh-key-rotation:
|
|
|
251
251
|
|
|
252
252
|
## 📄 Лицензия
|
|
253
253
|
|
|
254
|
-
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
254
|
+
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
255
|
+
|
|
256
|
+
### v0.7.36
|
|
257
|
+
- **Глубокий рефакторинг и деблоатинг**: Удалены 6 неиспользуемых модулей оверинжиниринга (`shadow`, `incident`, `agent-budget`, `region`, `canary`, `maintenance`) и сопутствующие устаревшие эндпоинты.
|
|
258
|
+
- **Мемоизация buildRuntime**: Устранено глубокое клонирование и повторный парсинг схемы на каждом токене/чанге стрима.
|
|
259
|
+
- **Атомарная ротация указателя**: Устранено состояние гонки при параллельных вызовах инструментов — указатель смещается сразу при выборе кандидата.
|
|
260
|
+
- **Точная детекция ошибок**: Прямая проверка кодов HTTP (`429`, `401`, `403`, `5xx`) и gRPC (`RESOURCE_EXHAUSTED`, `UNAVAILABLE`) с фоллбеком на регулярные выражения.
|
|
261
|
+
- **Понятные сообщения при исчерпании пула**: Информативное уведомление с таймером восстановления ближайшего ключа.
|
|
262
|
+
- **Энергоэффективный опрос (Smart Polling)**: Автоматическая приостановка фонового опроса при неактивной вкладке браузера (`document.visibilityState`).
|
|
263
|
+
|
|
264
|
+
### v0.7.35
|
|
265
|
+
- **Очистка жизненного цикла**: Патч `credentials.resolve` и слушатели событий `ctx.on` (`llm/stream`, `agent/request-error`) переведены в скоупы `ctx.effect` с автоматическим восстановлением функций и отпиской при выгрузке плагина (#238, #239).
|
|
266
|
+
- **Роли секретов в схеме**: Полям `incidentGitHubToken` и `webhookActionToken` в схеме `Config` присвоена роль `.role('secret')` для маскирования в UI (#237).
|
|
267
|
+
- **Архитектура настроек**: Добавлена нативная интеграция со снимками `settingsScope` в карточке настроек с безопасным фоллбеком на HTTP-мост (#235).
|
|
268
|
+
- **Локализация**: Фоллбек секции настроек `settings.section` переведён на локализованную метку `t('title')` со слотом `locale: NS`, а китайский словарь `zh` зарегистрирован в `ctx.locale` наряду с `en` и `ru` (#236).
|
|
269
|
+
- **Удаление мёртвого кода**: Удалена неиспользуемая функция `mountDashboard` после перехода на header chip (#240).
|
package/README.zh.md
CHANGED
|
@@ -212,4 +212,19 @@ dsh-key-rotation:
|
|
|
212
212
|
|
|
213
213
|
## 📄 开源许可
|
|
214
214
|
|
|
215
|
-
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
215
|
+
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
216
|
+
|
|
217
|
+
### v0.7.36
|
|
218
|
+
- **架构精简与稳定性加固**:移除 6 个过度设计的模块(`shadow`、`incident`、`agent-budget`、`region`、`canary`、`maintenance`)与废弃端点。
|
|
219
|
+
- **buildRuntime 高性能记忆化**:消除每个流式 token/chunk 上的深拷贝与模式重解析开销。
|
|
220
|
+
- **原子轮询指针推进**:并发请求在选定候选密钥时立即推进指针,消除并发工具调用中的竞争条件。
|
|
221
|
+
- **增强的可切换错误检测**:直接解析 HTTP 状态码(`429`, `401`, `403`, `5xx`)与 gRPC 状态码(`RESOURCE_EXHAUSTED`, `UNAVAILABLE`)。
|
|
222
|
+
- **用户友好的耗尽提示**:密钥池耗尽时返回带恢复倒计时的清晰通知。
|
|
223
|
+
- **智能轮询(Smart Polling)**:标签页不活动时暂停客户端后台轮询。
|
|
224
|
+
|
|
225
|
+
### v0.7.35
|
|
226
|
+
- **生命周期清理**: 将 `credentials.resolve` 猴子补丁和 `ctx.on` 事件监听器 (`llm/stream`, `agent/request-error`) 封装在 `ctx.effect` 作用域内,确保卸载时自动注销并恢复原始方法 (#238, #239)。
|
|
227
|
+
- **配置密钥角色**: 在 `Config` Schema 中为 `incidentGitHubToken` 和 `webhookActionToken` 增加 `.role('secret')`,避免明文泄露并在 UI 中掩码显示 (#237)。
|
|
228
|
+
- **设置架构与状态**: 在设置卡片中增加原生 `settingsScope` 绑定支持,保留 HTTP 桥接安全回退机制 (#235)。
|
|
229
|
+
- **本地化与文案**: 侧边栏备用项 `settings.section` 标签支持本地化 `t('title')` 并配置 `locale: NS`,并在 `ctx.locale` 中注册 `zh` 中文字典 (#236)。
|
|
230
|
+
- **死代码清理**: 移除 header-chip 迁移后残留的废弃 `mountDashboard` 函数 (#240)。
|
package/lib/client.js
CHANGED
|
@@ -240,12 +240,13 @@ window.__ModuleLoader__.load({
|
|
|
240
240
|
// отдельными отмеченными галочками, чтобы правило нельзя было потерять.
|
|
241
241
|
const KNOWN_CODES = ['QUOTA', 'RATE_LIMIT', 'SERVER', 'TIMEOUT', 'TRANSPORT', 'EMPTY_RESPONSE', 'UNKNOWN_MODEL', 'AUTH'];
|
|
242
242
|
|
|
243
|
-
/** Опрос статуса ротации, пока раздел настроек
|
|
243
|
+
/** Опрос статуса ротации, пока раздел настроек открыт (smart polling). */
|
|
244
244
|
function useRotationStatus() {
|
|
245
245
|
const [byProvider, setByProvider] = React.useState({});
|
|
246
246
|
React.useEffect(() => {
|
|
247
247
|
let alive = true;
|
|
248
248
|
const pull = () => {
|
|
249
|
+
if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;
|
|
249
250
|
fetch('/dsh-key-rotation/status', { headers: { accept: 'application/json' } })
|
|
250
251
|
.then((r) => (r.ok ? r.json() : null))
|
|
251
252
|
.then((data) => {
|
|
@@ -258,17 +259,24 @@ window.__ModuleLoader__.load({
|
|
|
258
259
|
};
|
|
259
260
|
pull();
|
|
260
261
|
const id = setInterval(pull, 4000);
|
|
261
|
-
|
|
262
|
+
const onVis = () => { if (typeof document !== 'undefined' && document.visibilityState === 'visible') pull(); };
|
|
263
|
+
if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVis);
|
|
264
|
+
return () => {
|
|
265
|
+
alive = false;
|
|
266
|
+
clearInterval(id);
|
|
267
|
+
if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', onVis);
|
|
268
|
+
};
|
|
262
269
|
}, []);
|
|
263
270
|
return byProvider;
|
|
264
271
|
}
|
|
265
272
|
|
|
266
|
-
/** Последний probe-результат по каждому ключу (#219): /sandbox-cache. */
|
|
273
|
+
/** Последний probe-результат по каждому ключу (#219): /sandbox-cache (smart polling). */
|
|
267
274
|
function useProbeCache() {
|
|
268
275
|
const [cache, setCache] = React.useState({});
|
|
269
276
|
React.useEffect(() => {
|
|
270
277
|
let alive = true;
|
|
271
278
|
const pull = () => {
|
|
279
|
+
if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;
|
|
272
280
|
fetch('/dsh-key-rotation/sandbox-cache', { headers: { accept: 'application/json' } })
|
|
273
281
|
.then((r) => (r.ok ? r.json() : null))
|
|
274
282
|
.then((data) => { if (alive && data) setCache(data); })
|
|
@@ -276,7 +284,13 @@ window.__ModuleLoader__.load({
|
|
|
276
284
|
};
|
|
277
285
|
pull();
|
|
278
286
|
const id = setInterval(pull, 4000);
|
|
279
|
-
|
|
287
|
+
const onVis = () => { if (typeof document !== 'undefined' && document.visibilityState === 'visible') pull(); };
|
|
288
|
+
if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVis);
|
|
289
|
+
return () => {
|
|
290
|
+
alive = false;
|
|
291
|
+
clearInterval(id);
|
|
292
|
+
if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', onVis);
|
|
293
|
+
};
|
|
280
294
|
}, []);
|
|
281
295
|
return cache;
|
|
282
296
|
}
|
|
@@ -390,6 +404,20 @@ window.__ModuleLoader__.load({
|
|
|
390
404
|
function KeyRotationSection(props) {
|
|
391
405
|
const DICT = props.locale === 'ru' ? ru : (props.locale === 'zh' ? zh : en);
|
|
392
406
|
const t = makeT(DICT, en);
|
|
407
|
+
const settingsScope = React.useMemo(() => {
|
|
408
|
+
if (props.ctx && props.ctx.settingsScope) {
|
|
409
|
+
try {
|
|
410
|
+
return props.ctx.settingsScope.bind({ namespace: NS });
|
|
411
|
+
} catch (_) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return null;
|
|
416
|
+
}, [props.ctx]);
|
|
417
|
+
const scopeSnapshot = React.useSyncExternalStore(
|
|
418
|
+
React.useMemo(() => (cb) => (settingsScope ? settingsScope.subscribe(cb) : () => {}), [settingsScope]),
|
|
419
|
+
React.useCallback(() => (settingsScope ? settingsScope.getSnapshot() : null), [settingsScope])
|
|
420
|
+
);
|
|
393
421
|
const [state, setState] = React.useState({ status: 'loading', value: null, revision: 0, error: '', providers: [] });
|
|
394
422
|
const [draft, setDraft] = React.useState(null);
|
|
395
423
|
// ── all hooks live ABOVE any early return (React error 310 otherwise) ──
|
|
@@ -455,7 +483,18 @@ window.__ModuleLoader__.load({
|
|
|
455
483
|
.catch((e) => setState((s) => ({ ...s, status: 'error', error: String(e) })));
|
|
456
484
|
}, []);
|
|
457
485
|
|
|
458
|
-
React.useEffect(() => {
|
|
486
|
+
React.useEffect(() => {
|
|
487
|
+
if (scopeSnapshot && scopeSnapshot.status === 'ready' && scopeSnapshot.value) {
|
|
488
|
+
setState((s) => ({
|
|
489
|
+
...s,
|
|
490
|
+
status: 'ready',
|
|
491
|
+
value: scopeSnapshot.value,
|
|
492
|
+
revision: scopeSnapshot.revision ?? s.revision,
|
|
493
|
+
}));
|
|
494
|
+
} else {
|
|
495
|
+
load();
|
|
496
|
+
}
|
|
497
|
+
}, [load, scopeSnapshot]);
|
|
459
498
|
|
|
460
499
|
const val = draft ?? state.value;
|
|
461
500
|
const status = useRotationStatus();
|
|
@@ -650,21 +689,38 @@ window.__ModuleLoader__.load({
|
|
|
650
689
|
const save = () => {
|
|
651
690
|
if (!draft) return;
|
|
652
691
|
setState((s) => ({ ...s, status: 'saving', error: '' }));
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
.then((r) => r.json())
|
|
659
|
-
.then((data) => {
|
|
660
|
-
if (data.error) {
|
|
661
|
-
setState((s) => ({ ...s, status: 'error', error: data.error.message }));
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
setState((s) => ({ status: 'ready', value: data.value ?? draft, revision: data.revision ?? s.revision, error: '', providers: s.providers }));
|
|
665
|
-
setDraft(null);
|
|
692
|
+
const saveViaBridge = () => {
|
|
693
|
+
fetch(CONFIG_PATH, {
|
|
694
|
+
method: 'PUT',
|
|
695
|
+
headers: { 'content-type': 'application/json' },
|
|
696
|
+
body: JSON.stringify({ section: draft, expectedRevision: state.revision }),
|
|
666
697
|
})
|
|
667
|
-
|
|
698
|
+
.then((r) => r.json())
|
|
699
|
+
.then((data) => {
|
|
700
|
+
if (data.error) {
|
|
701
|
+
setState((s) => ({ ...s, status: 'error', error: data.error.message }));
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
setState((s) => ({ status: 'ready', value: data.value ?? draft, revision: data.revision ?? s.revision, error: '', providers: s.providers }));
|
|
705
|
+
setDraft(null);
|
|
706
|
+
})
|
|
707
|
+
.catch((e) => setState((s) => ({ ...s, status: 'error', error: String(e) })));
|
|
708
|
+
};
|
|
709
|
+
if (settingsScope && typeof settingsScope.set === 'function') {
|
|
710
|
+
(async () => {
|
|
711
|
+
try {
|
|
712
|
+
for (const [k, v] of Object.entries(draft)) {
|
|
713
|
+
await settingsScope.set(k, v);
|
|
714
|
+
}
|
|
715
|
+
setState((s) => ({ ...s, status: 'ready', value: draft, error: '' }));
|
|
716
|
+
setDraft(null);
|
|
717
|
+
} catch (_) {
|
|
718
|
+
saveViaBridge();
|
|
719
|
+
}
|
|
720
|
+
})();
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
saveViaBridge();
|
|
668
724
|
};
|
|
669
725
|
|
|
670
726
|
|
|
@@ -1021,13 +1077,7 @@ window.__ModuleLoader__.load({
|
|
|
1021
1077
|
);
|
|
1022
1078
|
}
|
|
1023
1079
|
|
|
1024
|
-
|
|
1025
|
-
// Floating dashboard disabled in favor of pinned header chip popover
|
|
1026
|
-
if (typeof document !== 'undefined') {
|
|
1027
|
-
var el = document.getElementById('krot-dash');
|
|
1028
|
-
if (el) el.remove();
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1080
|
+
|
|
1031
1081
|
|
|
1032
1082
|
// #201 header chip: one dot + counts for all pools. Green = all healthy,
|
|
1033
1083
|
// amber = some keys cooling, red = a pool fully exhausted. Click opens the
|
|
@@ -1040,14 +1090,21 @@ window.__ModuleLoader__.load({
|
|
|
1040
1090
|
React.useEffect(() => {
|
|
1041
1091
|
let alive = true;
|
|
1042
1092
|
const load = () => {
|
|
1093
|
+
if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;
|
|
1043
1094
|
fetch('/dsh-key-rotation/health', { headers: { accept: 'application/json' }, credentials: 'same-origin' })
|
|
1044
1095
|
.then((r) => (r.ok ? r.json() : null))
|
|
1045
1096
|
.then((d) => { if (alive) setSnap(d); })
|
|
1046
1097
|
.catch(() => {});
|
|
1047
1098
|
};
|
|
1048
1099
|
load();
|
|
1049
|
-
const id = setInterval(load,
|
|
1050
|
-
|
|
1100
|
+
const id = setInterval(load, 5000);
|
|
1101
|
+
const onVis = () => { if (typeof document !== 'undefined' && document.visibilityState === 'visible') load(); };
|
|
1102
|
+
if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVis);
|
|
1103
|
+
return () => {
|
|
1104
|
+
alive = false;
|
|
1105
|
+
clearInterval(id);
|
|
1106
|
+
if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', onVis);
|
|
1107
|
+
};
|
|
1051
1108
|
}, []);
|
|
1052
1109
|
|
|
1053
1110
|
React.useEffect(() => {
|
|
@@ -1096,9 +1153,7 @@ window.__ModuleLoader__.load({
|
|
|
1096
1153
|
}
|
|
1097
1154
|
|
|
1098
1155
|
function apply(ctx) {
|
|
1099
|
-
ctx.effect(() => ctx.locale.register(NS, { en, ru }), 'dsh-key-rotation: dictionaries');
|
|
1100
|
-
// Dashboard widget: lives on every page, polls /health (#152).
|
|
1101
|
-
ctx.effect(() => mountDashboard(), 'dsh-key-rotation: dashboard widget');
|
|
1156
|
+
ctx.effect(() => ctx.locale.register(NS, { en, ru, zh }), 'dsh-key-rotation: dictionaries');
|
|
1102
1157
|
// Header chip (#201): status dot in the session header utilities slot,
|
|
1103
1158
|
// same slot dsh-gitea / dsh-subscriptions use for their header widgets.
|
|
1104
1159
|
ctx.effect(() => {
|
|
@@ -1144,8 +1199,16 @@ window.__ModuleLoader__.load({
|
|
|
1144
1199
|
};
|
|
1145
1200
|
if (!tryPluginItem()) {
|
|
1146
1201
|
// Fallback for builds without the Plugins tab slot: keep the sidebar section.
|
|
1202
|
+
const t = ctx.locale ? ctx.locale.bind(NS) : null;
|
|
1147
1203
|
ctx.slots.inject('settings.section', () => ctx.slots.register(
|
|
1148
|
-
{
|
|
1204
|
+
{
|
|
1205
|
+
name: 'settings.section',
|
|
1206
|
+
id: 'dsh-key-rotation',
|
|
1207
|
+
order: 20,
|
|
1208
|
+
locale: NS,
|
|
1209
|
+
label: () => (t ? t('title') : 'Key Rotation') || 'Key Rotation',
|
|
1210
|
+
inject: () => ({ ctx }),
|
|
1211
|
+
},
|
|
1149
1212
|
(props) => h(KeyRotationSection, { ...props, locale: useLocale() }),
|
|
1150
1213
|
));
|
|
1151
1214
|
}
|