@goodandready/dsh-key-rotation 0.8.2 → 0.8.4
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 +1 -0
- package/README.ru.md +1 -0
- package/README.zh.md +1 -0
- package/lib/client.js +247 -105
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
- **🕒 Accurate Midnight PST Resets**: Corrected UTC-8 timezone calculation offset sign for calendar quota reset windows.
|
|
46
46
|
- **🧹 Lifecycle Timer Cleanup**: Wrapped `canaryTimer` and `selfHealTimer` in Cordis effect scopes, eliminating background orphaned intervals on hot reload.
|
|
47
47
|
- **⚡ Stale Lock Recovery in Load Balancer**: Added expired lock detection to `pickLeastLoaded` for uninterrupted least-connections routing.
|
|
48
|
+
- **🎨 Native Design System (Changed in v0.8.3)**: Unified with `dsh-clinebot` baseline: modular section cards, live pool telemetry stat boxes, pill badges, and complete semantic theme token styling (#281).
|
|
48
49
|
- **🌐 Localization (Changed in v0.8.2)**: Source strings are English-only. Russian/Chinese UI comes from the DSH core locale service and translation plugins (`props.t`). Active locale: host snapshot → first `navigator.languages` entry → `en` (#277).
|
|
49
50
|
|
|
50
51
|
|
package/README.ru.md
CHANGED
|
@@ -295,6 +295,7 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
|
295
295
|
- **Очистка жизненного цикла**: Патч `credentials.resolve` и слушатели событий `ctx.on` (`llm/stream`, `agent/request-error`) переведены в скоупы `ctx.effect` с автоматическим восстановлением функций и отпиской при выгрузке плагина (#238, #239).
|
|
296
296
|
- **Роли секретов в схеме**: Полям `incidentGitHubToken` и `webhookActionToken` в схеме `Config` присвоена роль `.role('secret')` для маскирования в UI (#237).
|
|
297
297
|
- **Архитектура настроек**: Добавлена нативная интеграция со снимками `settingsScope` в карточке настроек с безопасным фоллбеком на HTTP-мост (#235).
|
|
298
|
+
- **Нативная дизайн-система (Changed in v0.8.3)**: Унификация со стилем `dsh-clinebot`: модульные карточки секций, плитки оперативной телеметрии пулов, капсульные бейджи и системные CSS-токены DSH (#281).
|
|
298
299
|
- **Локализация (Changed in v0.8.2)**: Исходные строки только на английском (`en`). Русский и китайский приходят через core `props.t` / translation-плагины. Фолбек активной локали: snapshot → первый `navigator.languages` → `en`. Свой `settings.section` и встроенные `ru`/`zh`-таблицы удалены (#236, #275, #277).
|
|
299
300
|
- **Удаление мёртвого кода**: Удалена неиспользуемая функция `mountDashboard` после перехода на header chip (#240).
|
|
300
301
|
|
package/README.zh.md
CHANGED
|
@@ -259,6 +259,7 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
|
259
259
|
- **生命周期清理**: 将 `credentials.resolve` 猴子补丁和 `ctx.on` 事件监听器 (`llm/stream`, `agent/request-error`) 封装在 `ctx.effect` 作用域内,确保卸载时自动注销并恢复原始方法 (#238, #239)。
|
|
260
260
|
- **配置密钥角色**: 在 `Config` Schema 中为 `incidentGitHubToken` 和 `webhookActionToken` 增加 `.role('secret')`,避免明文泄露并在 UI 中掩码显示 (#237)。
|
|
261
261
|
- **设置架构与状态**: 在设置卡片中增加原生 `settingsScope` 绑定支持,保留 HTTP 桥接安全回退机制 (#235)。
|
|
262
|
+
- **原生设计系统 (Changed in v0.8.3)**: 与 `dsh-clinebot` 基准对齐:模块化分区卡片、实时密钥池指标块、胶囊状态徽章与全局主题语义 token (#281)。
|
|
262
263
|
- **本地化与文案 (Changed in v0.8.2)**: 插件仅注册英文源字符串 `en`;俄文/中文由 DSH 核心 locale 与 translation 插件通过 `props.t` 提供。活动语言回退:snapshot → 首个 `navigator.languages` → `en`。已移除 `settings.section` 回退与内置 `ru`/`zh` 表 (#236, #275, #277)。
|
|
263
264
|
- **死代码清理**: 移除 header-chip 迁移后残留的废弃 `mountDashboard` 函数 (#240)。
|
|
264
265
|
|
package/lib/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// dsh-key-rotation — Settings card (
|
|
1
|
+
// dsh-key-rotation — Settings card (Key Rotation).
|
|
2
2
|
// Renders in Settings → Plugins → Plugin settings via the settings.plugin.item slot and
|
|
3
3
|
// edits the plugin's `dsh-key-rotation` settings namespace through the
|
|
4
4
|
// loopback-fenced config bridge at /dsh-key-rotation/config.
|
|
@@ -27,6 +27,17 @@ window.__ModuleLoader__.load({
|
|
|
27
27
|
// -------------------------------------------------------------- i18n
|
|
28
28
|
const en = {
|
|
29
29
|
title: 'Key Rotation',
|
|
30
|
+
sectionStats: 'Pool Health & Telemetry',
|
|
31
|
+
sectionStatsDesc: 'Real-time telemetry of active key pools, healthy credentials, and failover status.',
|
|
32
|
+
sectionFailover: 'Failover Triggers & Error Codes',
|
|
33
|
+
sectionFailoverDesc: 'Select error conditions that trigger automatic switching to the next available key.',
|
|
34
|
+
sectionTiming: 'Timing & Rotation Schedule',
|
|
35
|
+
sectionTimingDesc: 'Configure cooldown durations and scheduled periodic rotation windows.',
|
|
36
|
+
sectionBackup: 'Backup & Snapshots',
|
|
37
|
+
sectionBackupDesc: 'Export or import provider pools and create full plugin configuration snapshots.',
|
|
38
|
+
statPools: 'Configured Pools',
|
|
39
|
+
statKeys: 'Total Keys',
|
|
40
|
+
statHealthy: 'Healthy / Ready',
|
|
30
41
|
filterAll: 'All',
|
|
31
42
|
filterReady: 'Ready',
|
|
32
43
|
filterCooldown: 'In Cooldown',
|
|
@@ -199,54 +210,81 @@ window.__ModuleLoader__.load({
|
|
|
199
210
|
// на поле значения, поэтому имя занимает свою строку, а служебная строка
|
|
200
211
|
// под ним ужимается сама.
|
|
201
212
|
const CARD_CSS = [
|
|
202
|
-
'.krot{display:flex;flex-direction:column;gap:
|
|
213
|
+
'.krot{display:flex;flex-direction:column;gap:18px;max-width:960px;padding:6px 0 24px;box-sizing:border-box}',
|
|
203
214
|
'.krot p{margin:0}',
|
|
204
|
-
'.krot-hint{font-size:
|
|
205
|
-
'.krot-err{font-size:
|
|
206
|
-
'.krot-label{font-size:
|
|
207
|
-
'.krot-field{display:flex;flex-direction:column;gap:
|
|
208
|
-
'.krot-in{background:var(--dsw-
|
|
209
|
-
'.krot-in:focus{outline:none;border-color:var(--dsw-alias-
|
|
210
|
-
'.krot-codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(
|
|
211
|
-
'.krot-code{display:flex;align-items:center;gap:
|
|
212
|
-
'.krot-
|
|
213
|
-
'.krot-
|
|
214
|
-
'.krot-
|
|
215
|
+
'.krot-hint{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}',
|
|
216
|
+
'.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}',
|
|
217
|
+
'.krot-label{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}',
|
|
218
|
+
'.krot-field{display:flex;flex-direction:column;gap:6px}',
|
|
219
|
+
'.krot-in{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;height:36px;font-family:inherit;min-width:0;width:100%;box-sizing:border-box;transition:border-color .15s ease}',
|
|
220
|
+
'.krot-in:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}',
|
|
221
|
+
'.krot-codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:8px 14px}',
|
|
222
|
+
'.krot-code{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--dsw-alias-label-primary);cursor:pointer;user-select:none}',
|
|
223
|
+
'.krot-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:18px 20px;display:flex;flex-direction:column;gap:14px}',
|
|
224
|
+
'.krot-section-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;gap:8px}',
|
|
225
|
+
'.krot-section-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-6px;line-height:1.4}',
|
|
226
|
+
'.krot-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}',
|
|
227
|
+
'.krot-grid-2{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px}',
|
|
228
|
+
'.krot-grid-3{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}',
|
|
229
|
+
'.krot-stat-box{padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:4px}',
|
|
230
|
+
'.krot-stat-val{font-size:20px;font-weight:700;color:var(--dsw-alias-label-primary)}',
|
|
231
|
+
'.krot-stat-lbl{font-size:12px;color:var(--dsw-alias-label-secondary)}',
|
|
232
|
+
'.krot-badge{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500;white-space:nowrap}',
|
|
233
|
+
'.krot-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}',
|
|
234
|
+
'.krot-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}',
|
|
235
|
+
'.krot-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}',
|
|
236
|
+
'.krot-prov{display:flex;flex-direction:column;gap:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);padding:14px 16px}',
|
|
237
|
+
'.krot-prov-head{display:flex;gap:10px;align-items:center;flex-wrap:wrap}',
|
|
238
|
+
'.krot-prov-head select{flex:1;min-width:180px}',
|
|
215
239
|
'.krot-keys{display:flex;flex-direction:column;gap:8px}',
|
|
216
|
-
'.krot-key{display:grid;grid-template-columns:
|
|
217
|
-
'.krot-num{font-size:12px;color:var(--dsw-alias-label-tertiary);text-align:right}',
|
|
218
|
-
'.krot-name{font-size:13px;color:var(--dsw-alias-label-primary);cursor:default}',
|
|
240
|
+
'.krot-key{display:grid;grid-template-columns:22px minmax(0,1fr);gap:6px 10px;align-items:center;padding:8px 12px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:8px}',
|
|
241
|
+
'.krot-num{font-size:12px;font-weight:600;color:var(--dsw-alias-label-tertiary);text-align:right}',
|
|
242
|
+
'.krot-name{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary);cursor:default}',
|
|
219
243
|
'.krot-meta{grid-column:2;display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
|
|
220
244
|
'.krot-dot{width:8px;height:8px;border-radius:50%;flex:none}',
|
|
221
|
-
'.krot-state{font-size:
|
|
222
|
-
'.krot-tail{font-size:11px;color:var(--dsw-alias-label-
|
|
245
|
+
'.krot-state{font-size:12px;color:var(--dsw-alias-label-secondary);font-weight:500}',
|
|
246
|
+
'.krot-tail{font-size:11px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,Menlo,Consolas,monospace;background:var(--dsw-alias-bg-layer-1);padding:2px 6px;border-radius:4px;border:1px solid var(--dsw-alias-border-l2)}',
|
|
223
247
|
'.krot-secret{flex:1;min-width:120px;max-width:220px}',
|
|
224
248
|
'.krot-acts{display:flex;gap:4px;margin-left:auto;flex:none}',
|
|
225
|
-
'.krot-btn{cursor:pointer;border-radius:
|
|
226
|
-
'.krot-btn:disabled{
|
|
227
|
-
'.krot-btn
|
|
228
|
-
'.krot-
|
|
229
|
-
'.krot-
|
|
230
|
-
'.krot-
|
|
231
|
-
'.krot-
|
|
249
|
+
'.krot-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 14px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}',
|
|
250
|
+
'.krot-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4,var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed,var(--dsw-alias-border-l2))}',
|
|
251
|
+
'.krot-btn-primary,.krot-save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent;font-weight:600}',
|
|
252
|
+
'.krot-btn-primary:hover:not(:disabled),.krot-save:hover:not(:disabled){background:var(--dsw-alias-label-primary)!important;color:var(--dsw-alias-bg-layer-3)!important;opacity:0.88}',
|
|
253
|
+
'.krot-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}',
|
|
254
|
+
'.krot-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12)!important;border-color:rgba(239,68,68,0.5)}',
|
|
255
|
+
'.krot-btn:disabled{opacity:0.45;cursor:not-allowed}',
|
|
256
|
+
'.krot-foot{display:flex;gap:10px;align-items:center;flex-wrap:wrap}',
|
|
257
|
+
'.krot-filter-bar{display:flex;gap:8px;margin:4px 0 10px;flex-wrap:wrap}',
|
|
258
|
+
'.krot-pill{appearance:none;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:4px 12px;font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary);cursor:pointer;transition:all .15s ease}',
|
|
232
259
|
'.krot-pill:hover{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}',
|
|
233
|
-
'.krot-pill-active{background:var(--dsw-alias-
|
|
234
|
-
'.krot-pill-warn{border-color:rgba(245,158,11,0.3);color
|
|
235
|
-
'.krot-pill-err{border-color:rgba(239,68,68,0.3);color
|
|
260
|
+
'.krot-pill-active{background:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)!important;font-weight:600}',
|
|
261
|
+
'.krot-pill-warn{border-color:rgba(245,158,11,0.3);color:var(--dsw-alias-state-warning-primary)}',
|
|
262
|
+
'.krot-pill-err{border-color:rgba(239,68,68,0.3);color:var(--dsw-alias-state-error-primary)}',
|
|
263
|
+
'.krot-alert-ok{padding:10px 14px;border-radius:8px;background:rgba(16,185,129,0.1);color:var(--dsw-alias-state-success-primary);font-size:13px;display:flex;align-items:center;gap:10px}',
|
|
264
|
+
'.krot-alert-bad{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px;display:flex;align-items:center;gap:10px}',
|
|
236
265
|
'.krot-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none}',
|
|
237
266
|
'.krot-card-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}',
|
|
238
267
|
'.krot-card-head-text{display:flex;flex-direction:column;flex:1;gap:4px;min-width:0}',
|
|
239
268
|
'.krot-card-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}',
|
|
240
269
|
'.krot-card-description{color:var(--dsw-alias-label-secondary);font-size:13px}',
|
|
241
270
|
'.krot-card-chevron{margin-left:auto;flex:none;color:var(--dsw-alias-label-tertiary);transition:transform .16s ease;display:flex;align-items:center}.krot-card-chevron-open{transform:rotate(180deg)}',
|
|
242
|
-
'.krot-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding
|
|
271
|
+
'.krot-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:16px 0 8px}',
|
|
272
|
+
'.krot-header-chip{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:12px;font-weight:600;cursor:pointer;position:relative;user-select:none;transition:all .15s ease}',
|
|
273
|
+
'.krot-header-chip:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-3));border-color:var(--dsw-alias-border-l1);transform:translateY(-0.5px)}',
|
|
274
|
+
'.krot-popover{position:absolute;top:calc(100% + 6px);right:0;z-index:10000;min-width:220px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:12px 14px;box-shadow:0 16px 40px rgba(0,0,0,.45),0 2px 8px rgba(0,0,0,.15);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);display:flex;flex-direction:column;gap:8px;text-align:left}',
|
|
275
|
+
'.krot-pop-title{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--dsw-alias-label-secondary)}',
|
|
276
|
+
'.krot-pop-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:4px 0}',
|
|
277
|
+
'.krot-pop-name{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:8px}',
|
|
278
|
+
'.krot-pop-count{font-size:12px;font-weight:700;font-variant-numeric:tabular-nums;opacity:.9}',
|
|
243
279
|
].join('');
|
|
244
280
|
const CARD_CSS_ID = 'dsh-key-rotation/section.module.css';
|
|
245
|
-
if (typeof document !== 'undefined' && !document.
|
|
281
|
+
if (typeof document !== 'undefined' && !document.getElementById('dsh-key-rotation-full-css')) {
|
|
246
282
|
const tag = document.createElement('style');
|
|
247
|
-
tag.
|
|
248
|
-
tag.
|
|
283
|
+
tag.id = 'dsh-key-rotation-full-css';
|
|
284
|
+
tag.dataset.dshPlugin = NS;
|
|
249
285
|
tag.dataset.pluginCss = CARD_CSS_ID;
|
|
286
|
+
tag.setAttribute('data-plugin', NS);
|
|
287
|
+
tag.textContent = CARD_CSS;
|
|
250
288
|
document.head.appendChild(tag);
|
|
251
289
|
}
|
|
252
290
|
|
|
@@ -314,12 +352,12 @@ window.__ModuleLoader__.load({
|
|
|
314
352
|
function KeyRotationSection(props) {
|
|
315
353
|
const t = resolveT(props);
|
|
316
354
|
const settingsScope = React.useMemo(() => {
|
|
317
|
-
|
|
318
|
-
|
|
355
|
+
try {
|
|
356
|
+
if (props.ctx && props.ctx.settingsScope) {
|
|
319
357
|
return props.ctx.settingsScope.bind({ namespace: NS });
|
|
320
|
-
} catch (_) {
|
|
321
|
-
return null;
|
|
322
358
|
}
|
|
359
|
+
} catch (_) {
|
|
360
|
+
return null;
|
|
323
361
|
}
|
|
324
362
|
return null;
|
|
325
363
|
}, [props.ctx]);
|
|
@@ -929,82 +967,175 @@ window.__ModuleLoader__.load({
|
|
|
929
967
|
? h('p', { className: 'krot-err' }, t('noProviders'))
|
|
930
968
|
: null;
|
|
931
969
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
970
|
+
const allConfiguredProviders = Array.isArray(val?.providers) ? val.providers : [];
|
|
971
|
+
const totalPoolsCount = allConfiguredProviders.length;
|
|
972
|
+
let totalKeysCount = 0;
|
|
973
|
+
let healthyKeysCount = 0;
|
|
974
|
+
for (const prov of allConfiguredProviders) {
|
|
975
|
+
const pKeys = Array.isArray(prov.keys) ? prov.keys : [];
|
|
976
|
+
totalKeysCount += pKeys.length;
|
|
977
|
+
for (const k of pKeys) {
|
|
978
|
+
const st = keyStatus(prov.provider, k);
|
|
979
|
+
const isDown = st && (st.text === t('brokenKey') || st.text === t('keyMissing') || (st.text && st.text.includes(t('keyCooling').slice(0, 4)))) && !optimisticReset[k];
|
|
980
|
+
if (!isDown) healthyKeysCount++;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const statsSection = h('div', { className: 'krot-section-card' },
|
|
985
|
+
h('div', { className: 'krot-section-title' },
|
|
986
|
+
h('span', null, '📊 ' + t('sectionStats')),
|
|
987
|
+
h('span', { className: 'krot-badge ' + (totalPoolsCount > 0 && healthyKeysCount === totalKeysCount ? 'krot-badge-ok' : totalPoolsCount > 0 ? 'krot-badge-warn' : 'krot-badge-bad') },
|
|
988
|
+
totalPoolsCount > 0 ? (healthyKeysCount + '/' + totalKeysCount + ' ' + t('keyReady')) : t('notRegistered')
|
|
989
|
+
)
|
|
990
|
+
),
|
|
991
|
+
h('div', { className: 'krot-section-desc' }, t('sectionStatsDesc')),
|
|
992
|
+
h('div', { className: 'krot-grid-3' },
|
|
993
|
+
h('div', { className: 'krot-stat-box' },
|
|
994
|
+
h('span', { className: 'krot-stat-val' }, String(totalPoolsCount)),
|
|
995
|
+
h('span', { className: 'krot-stat-lbl' }, t('statPools'))
|
|
996
|
+
),
|
|
997
|
+
h('div', { className: 'krot-stat-box' },
|
|
998
|
+
h('span', { className: 'krot-stat-val' }, String(totalKeysCount)),
|
|
999
|
+
h('span', { className: 'krot-stat-lbl' }, t('statKeys'))
|
|
1000
|
+
),
|
|
1001
|
+
h('div', { className: 'krot-stat-box' },
|
|
1002
|
+
h('span', { className: 'krot-stat-val', style: { color: healthyKeysCount > 0 ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-primary)' } }, String(healthyKeysCount)),
|
|
1003
|
+
h('span', { className: 'krot-stat-lbl' }, t('statHealthy'))
|
|
1004
|
+
)
|
|
1005
|
+
)
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
const poolsSection = h('div', { className: 'krot-section-card' },
|
|
1009
|
+
h('div', { className: 'krot-section-title' },
|
|
1010
|
+
h('span', null, '🔑 ' + t('providersTitle')),
|
|
1011
|
+
h('span', { className: 'krot-badge krot-badge-ok' }, totalPoolsCount + ' ' + t('statPools'))
|
|
1012
|
+
),
|
|
1013
|
+
h('div', { className: 'krot-section-desc' }, t('desc')),
|
|
1014
|
+
searchInput,
|
|
1015
|
+
h('div', { className: 'krot-foot', style: { marginBottom: '8px' } },
|
|
937
1016
|
h('input', {
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1017
|
+
className: 'krot-in',
|
|
1018
|
+
placeholder: 'Bulk cooldown ms',
|
|
1019
|
+
value: bulkCooldown,
|
|
1020
|
+
onChange: (e) => setBulkCooldown(e.target.value),
|
|
1021
|
+
style: { maxWidth: '160px' },
|
|
941
1022
|
}),
|
|
942
|
-
|
|
943
|
-
)))),
|
|
944
|
-
field(t('providersTitle'), h('div', { className: 'krot-keys' },
|
|
945
|
-
searchInput,
|
|
946
|
-
h('div', { className: 'krot-foot' }, h('input', { className: 'krot-in', placeholder: 'Bulk cooldown ms', value: bulkCooldown, onChange: (e) => setBulkCooldown(e.target.value), style: { maxWidth: '140px' } }), btn('Apply to selected', () => {
|
|
1023
|
+
btn('Apply to selected', () => {
|
|
947
1024
|
const v = Number(bulkCooldown); if (!v) return;
|
|
948
1025
|
setField((cur) => {
|
|
949
1026
|
const next = [...(cur.providers ?? [])];
|
|
950
|
-
for (let i=0;i<next.length;i++) if (selected.has(next[i].provider)) next[i] = { ...next[i], cooldownMs: v };
|
|
1027
|
+
for (let i = 0; i < next.length; i++) if (selected.has(next[i].provider)) next[i] = { ...next[i], cooldownMs: v };
|
|
951
1028
|
return { ...cur, providers: next };
|
|
952
1029
|
});
|
|
953
|
-
}, { disabled: selected.size === 0 || !bulkCooldown })
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1030
|
+
}, { disabled: selected.size === 0 || !bulkCooldown })
|
|
1031
|
+
),
|
|
1032
|
+
h('div', { className: 'krot-keys' }, providerRows),
|
|
1033
|
+
h('div', { className: 'krot-foot', style: { marginTop: '10px' } },
|
|
1034
|
+
btn(t('addProvider'), addProvider, { primary: false }),
|
|
1035
|
+
noProviders
|
|
1036
|
+
)
|
|
1037
|
+
);
|
|
1038
|
+
|
|
1039
|
+
const failoverSection = h('div', { className: 'krot-section-card' },
|
|
1040
|
+
h('div', { className: 'krot-section-title' },
|
|
1041
|
+
h('span', null, '⚡ ' + t('codesTitle')),
|
|
1042
|
+
h('span', { className: 'krot-badge krot-badge-warn' }, selectedCodes.size + ' active')
|
|
1043
|
+
),
|
|
1044
|
+
h('div', { className: 'krot-section-desc' }, t('sectionFailoverDesc')),
|
|
1045
|
+
h('div', { className: 'krot-codes' }, codeList.map((code) => h('label', { key: code, className: 'krot-code' },
|
|
1046
|
+
h('input', {
|
|
1047
|
+
type: 'checkbox',
|
|
1048
|
+
checked: selectedCodes.has(code),
|
|
1049
|
+
onChange: (e) => toggleCode(code, e.target.checked),
|
|
1050
|
+
}),
|
|
1051
|
+
code,
|
|
1052
|
+
)))
|
|
1053
|
+
);
|
|
1054
|
+
|
|
1055
|
+
const timingSection = h('div', { className: 'krot-section-card' },
|
|
1056
|
+
h('div', { className: 'krot-section-title' },
|
|
1057
|
+
h('span', null, '⏱ ' + t('sectionTiming')),
|
|
1058
|
+
),
|
|
1059
|
+
h('div', { className: 'krot-section-desc' }, t('sectionTimingDesc')),
|
|
1060
|
+
h('div', { className: 'krot-grid-2' },
|
|
1061
|
+
field(t('cooldown'), textInput(String(val.cooldownMs ?? 60000), (v) => setField((cur) => ({ ...cur, cooldownMs: Number(v) || 0 })))),
|
|
1062
|
+
field(t('scheduleDays'), textInput(String(val.rotationScheduleDays ?? 0), (v) => setField((cur) => ({ ...cur, rotationScheduleDays: Number(v) || 0 }))))
|
|
1063
|
+
)
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
const backupSection = h('div', { className: 'krot-section-card' },
|
|
1067
|
+
h('div', { className: 'krot-section-title' },
|
|
1068
|
+
h('span', null, '💾 ' + t('sectionBackup')),
|
|
1069
|
+
),
|
|
1070
|
+
h('div', { className: 'krot-section-desc' }, t('sectionBackupDesc')),
|
|
1071
|
+
h('div', { className: 'krot-row' },
|
|
1072
|
+
btn(t('exportPools'), () => {
|
|
1073
|
+
const data = JSON.stringify(val.providers ?? [], null, 2);
|
|
1074
|
+
const blob = new Blob([data], { type: 'application/json' });
|
|
1075
|
+
const url = URL.createObjectURL(blob);
|
|
1076
|
+
const a = document.createElement('a'); a.href = url; a.download = 'pools.json'; a.click(); URL.revokeObjectURL(url);
|
|
1077
|
+
}, {}),
|
|
1078
|
+
h('label', { className: 'krot-btn', style: { cursor: 'pointer' } }, t('importPools'), h('input', { type: 'file', accept: '.json', style: { display: 'none' }, onChange: (e) => {
|
|
1079
|
+
const f = e.target.files[0]; if (!f) return;
|
|
1080
|
+
const reader = new FileReader();
|
|
1081
|
+
reader.onload = () => { try { const imp = JSON.parse(String(reader.result)); if (!Array.isArray(imp)) throw new Error('expected array'); setField((cur) => {
|
|
1082
|
+
const curProviders = Array.isArray(cur.providers) ? [...cur.providers] : [];
|
|
1083
|
+
const map = new Map(curProviders.map((p) => [p.provider, p]));
|
|
1084
|
+
for (const p of imp) { if (p && typeof p.provider === 'string') map.set(p.provider, p); }
|
|
1085
|
+
return { ...cur, providers: [...map.values()] };
|
|
1086
|
+
}); } catch (err) { setSecretError(String(err.message || err)); } };
|
|
1087
|
+
reader.readAsText(f);
|
|
1088
|
+
} })),
|
|
1089
|
+
btn(t('snapshotExport'), () => {
|
|
1090
|
+
fetch('/dsh-key-rotation/snapshot', { headers: { accept: 'application/json' } })
|
|
1091
|
+
.then((r) => r.json())
|
|
1092
|
+
.then((data) => {
|
|
1093
|
+
const blob = new Blob([JSON.stringify(data.snapshot ?? {}, null, 2)], { type: 'application/json' });
|
|
1094
|
+
const url = URL.createObjectURL(blob);
|
|
1095
|
+
const a = document.createElement('a'); a.href = url; a.download = 'dsh-key-rotation-snapshot.json'; a.click(); URL.revokeObjectURL(url);
|
|
1096
|
+
})
|
|
1097
|
+
.catch((e) => setSecretError(String(e?.message ?? e)));
|
|
1098
|
+
}, {}),
|
|
1099
|
+
h('label', { className: 'krot-btn', style: { cursor: 'pointer' } }, t('snapshotImport'), h('input', { type: 'file', accept: '.json', style: { display: 'none' }, onChange: (e) => {
|
|
1100
|
+
const f = e.target.files[0]; if (!f) return;
|
|
1101
|
+
const reader2 = new FileReader();
|
|
1102
|
+
reader2.onload = () => {
|
|
1103
|
+
try {
|
|
1104
|
+
const snap = JSON.parse(String(reader2.result));
|
|
1105
|
+
if (!snap || typeof snap !== 'object' || Array.isArray(snap)) throw new Error('expected snapshot object');
|
|
1106
|
+
fetch('/dsh-key-rotation/snapshot', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ snapshot: snap }) })
|
|
1107
|
+
.then((r) => r.json().then((data) => ({ ok: r.ok, data })))
|
|
1108
|
+
.then(({ ok, data }) => { if (!ok) throw new Error(data?.error?.message ?? 'import failed'); load(); })
|
|
1109
|
+
.catch((err) => setSecretError(t('keyWriteFailed').replace('{msg}', String(err?.message ?? err))));
|
|
1110
|
+
} catch (err) { setSecretError(String(err.message || err)); }
|
|
1111
|
+
};
|
|
1112
|
+
reader2.readAsText(f);
|
|
1113
|
+
} }))
|
|
1114
|
+
),
|
|
1115
|
+
h('p', { className: 'krot-hint', style: { marginTop: '4px' } }, t('keyHint'))
|
|
1116
|
+
);
|
|
1117
|
+
|
|
1118
|
+
const alerts = [
|
|
1119
|
+
secretError ? h('div', { key: 'se', className: 'krot-alert-bad' }, secretError) : null,
|
|
1120
|
+
state.error ? h('div', { key: 'st', className: 'krot-alert-bad' }, state.error) : null,
|
|
1121
|
+
undo ? h('div', { key: 'un', className: 'krot-alert-ok' },
|
|
1122
|
+
h('span', null, undo.type === 'provider' ? t('undoProvider') : t('undoKey')),
|
|
1123
|
+
btn(t('undo'), doUndo, { className: 'krot-btn-sm' })
|
|
1124
|
+
) : null,
|
|
1125
|
+
].filter(Boolean);
|
|
1126
|
+
|
|
1127
|
+
return h('div', { className: 'krot' },
|
|
1128
|
+
statsSection,
|
|
1129
|
+
poolsSection,
|
|
1130
|
+
failoverSection,
|
|
1131
|
+
timingSection,
|
|
1132
|
+
backupSection,
|
|
1133
|
+
alerts.length > 0 ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px' } }, alerts) : null,
|
|
1134
|
+
h('div', { className: 'krot-foot', style: { marginTop: '6px', paddingTop: '14px', borderTop: '1px solid var(--dsw-alias-border-l2)' } },
|
|
1004
1135
|
btn(t('save'), save, { primary: true }),
|
|
1005
1136
|
btn(t('discard'), load, {}),
|
|
1006
1137
|
state.status === 'saving' ? h('span', { className: 'krot-hint' }, t('saving')) : null,
|
|
1007
|
-
)
|
|
1138
|
+
)
|
|
1008
1139
|
);
|
|
1009
1140
|
}
|
|
1010
1141
|
|
|
@@ -1052,7 +1183,7 @@ window.__ModuleLoader__.load({
|
|
|
1052
1183
|
const total = poolEntries.reduce((a, [, p]) => a + (p.total || 0), 0);
|
|
1053
1184
|
const healthy = poolEntries.reduce((a, [, p]) => a + (p.healthy || 0), 0);
|
|
1054
1185
|
const anyExhausted = poolEntries.some(([, p]) => p.exhausted);
|
|
1055
|
-
const color = !poolEntries.length ? 'var(--dsw-alias-label-tertiary)' : anyExhausted ? '
|
|
1186
|
+
const color = !poolEntries.length ? 'var(--dsw-alias-label-tertiary)' : anyExhausted ? 'var(--dsw-alias-state-error-primary)' : healthy < total ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-state-success-primary)';
|
|
1056
1187
|
const label = poolEntries.length ? `${healthy}/${total} rot` : 'rot';
|
|
1057
1188
|
|
|
1058
1189
|
return h('div', { ref, style: { position: 'relative', display: 'inline-flex' } },
|
|
@@ -1060,6 +1191,17 @@ window.__ModuleLoader__.load({
|
|
|
1060
1191
|
type: 'button',
|
|
1061
1192
|
className: 'krot-header-chip',
|
|
1062
1193
|
title: 'Key Rotation',
|
|
1194
|
+
sectionStats: 'Pool Health & Telemetry',
|
|
1195
|
+
sectionStatsDesc: 'Real-time telemetry of active key pools, healthy credentials, and failover status.',
|
|
1196
|
+
sectionFailover: 'Failover Triggers & Error Codes',
|
|
1197
|
+
sectionFailoverDesc: 'Select error conditions that trigger automatic switching to the next available key.',
|
|
1198
|
+
sectionTiming: 'Timing & Rotation Schedule',
|
|
1199
|
+
sectionTimingDesc: 'Configure cooldown durations and scheduled periodic rotation windows.',
|
|
1200
|
+
sectionBackup: 'Backup & Snapshots',
|
|
1201
|
+
sectionBackupDesc: 'Export or import provider pools and create full plugin configuration snapshots.',
|
|
1202
|
+
statPools: 'Configured Pools',
|
|
1203
|
+
statKeys: 'Total Keys',
|
|
1204
|
+
statHealthy: 'Healthy / Ready',
|
|
1063
1205
|
onClick: () => setOpen((v) => !v),
|
|
1064
1206
|
},
|
|
1065
1207
|
h('span', { style: { width: '8px', height: '8px', borderRadius: '50%', background: color, flex: 'none', boxShadow: `0 0 6px ${color}` } }),
|
|
@@ -1070,7 +1212,7 @@ window.__ModuleLoader__.load({
|
|
|
1070
1212
|
h('div', { className: 'krot-pop-title' }, 'Key rotation pools'),
|
|
1071
1213
|
!poolEntries.length ? h('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-tertiary)' } }, 'No active pools') : null,
|
|
1072
1214
|
poolEntries.map(([name, p]) => {
|
|
1073
|
-
const c = p.exhausted ? '
|
|
1215
|
+
const c = p.exhausted ? 'var(--dsw-alias-state-error-primary)' : (p.healthy < p.total ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-state-success-primary)');
|
|
1074
1216
|
return h('div', { key: name, className: 'krot-pop-row' },
|
|
1075
1217
|
h('div', { className: 'krot-pop-name' },
|
|
1076
1218
|
h('span', { style: { width: '7px', height: '7px', borderRadius: '50%', background: c, flex: 'none' } }),
|
|
@@ -1131,7 +1273,7 @@ window.__ModuleLoader__.load({
|
|
|
1131
1273
|
}
|
|
1132
1274
|
}
|
|
1133
1275
|
|
|
1134
|
-
module.exports = { apply, inject: ['slots', 'locale'] };
|
|
1276
|
+
module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] };
|
|
1135
1277
|
return module.exports;
|
|
1136
1278
|
},
|
|
1137
1279
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-key-rotation",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
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": [
|