@goodandready/dsh-key-rotation 0.7.33 → 0.7.35
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 +8 -1
- package/README.ru.md +8 -1
- package/README.zh.md +8 -1
- package/lib/client.js +68 -26
- package/lib/index.js +25 -16
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -251,4 +251,11 @@ 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.35
|
|
257
|
+
- **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).
|
|
258
|
+
- **Settings & Secret Roles**: Added `.role('secret')` to `incidentGitHubToken` and `webhookActionToken` in `Config` schema for automatic UI masking (#237).
|
|
259
|
+
- **Settings Architecture & UI**: Added native `settingsScope` snapshot reading/saving in settings card with graceful bridge fallback (#235).
|
|
260
|
+
- **Localization**: Localized `settings.section` sidebar fallback label via `t('title')` and registered `zh` dictionary in `ctx.locale` alongside `en` and `ru` (#236).
|
|
261
|
+
- **Dead Code Purge**: Removed obsolete `mountDashboard` routine after header-chip migration (#240).
|
package/README.ru.md
CHANGED
|
@@ -251,4 +251,11 @@ 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.35
|
|
257
|
+
- **Очистка жизненного цикла**: Патч `credentials.resolve` и слушатели событий `ctx.on` (`llm/stream`, `agent/request-error`) переведены в скоупы `ctx.effect` с автоматическим восстановлением функций и отпиской при выгрузке плагина (#238, #239).
|
|
258
|
+
- **Роли секретов в схеме**: Полям `incidentGitHubToken` и `webhookActionToken` в схеме `Config` присвоена роль `.role('secret')` для маскирования в UI (#237).
|
|
259
|
+
- **Архитектура настроек**: Добавлена нативная интеграция со снимками `settingsScope` в карточке настроек с безопасным фоллбеком на HTTP-мост (#235).
|
|
260
|
+
- **Локализация**: Фоллбек секции настроек `settings.section` переведён на локализованную метку `t('title')` со слотом `locale: NS`, а китайский словарь `zh` зарегистрирован в `ctx.locale` наряду с `en` и `ru` (#236).
|
|
261
|
+
- **Удаление мёртвого кода**: Удалена неиспользуемая функция `mountDashboard` после перехода на header chip (#240).
|
package/README.zh.md
CHANGED
|
@@ -212,4 +212,11 @@ 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.35
|
|
218
|
+
- **生命周期清理**: 将 `credentials.resolve` 猴子补丁和 `ctx.on` 事件监听器 (`llm/stream`, `agent/request-error`) 封装在 `ctx.effect` 作用域内,确保卸载时自动注销并恢复原始方法 (#238, #239)。
|
|
219
|
+
- **配置密钥角色**: 在 `Config` Schema 中为 `incidentGitHubToken` 和 `webhookActionToken` 增加 `.role('secret')`,避免明文泄露并在 UI 中掩码显示 (#237)。
|
|
220
|
+
- **设置架构与状态**: 在设置卡片中增加原生 `settingsScope` 绑定支持,保留 HTTP 桥接安全回退机制 (#235)。
|
|
221
|
+
- **本地化与文案**: 侧边栏备用项 `settings.section` 标签支持本地化 `t('title')` 并配置 `locale: NS`,并在 `ctx.locale` 中注册 `zh` 中文字典 (#236)。
|
|
222
|
+
- **死代码清理**: 移除 header-chip 迁移后残留的废弃 `mountDashboard` 函数 (#240)。
|
package/lib/client.js
CHANGED
|
@@ -390,6 +390,20 @@ window.__ModuleLoader__.load({
|
|
|
390
390
|
function KeyRotationSection(props) {
|
|
391
391
|
const DICT = props.locale === 'ru' ? ru : (props.locale === 'zh' ? zh : en);
|
|
392
392
|
const t = makeT(DICT, en);
|
|
393
|
+
const settingsScope = React.useMemo(() => {
|
|
394
|
+
if (props.ctx && props.ctx.settingsScope) {
|
|
395
|
+
try {
|
|
396
|
+
return props.ctx.settingsScope.bind({ namespace: NS });
|
|
397
|
+
} catch (_) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
}, [props.ctx]);
|
|
403
|
+
const scopeSnapshot = React.useSyncExternalStore(
|
|
404
|
+
React.useMemo(() => (cb) => (settingsScope ? settingsScope.subscribe(cb) : () => {}), [settingsScope]),
|
|
405
|
+
React.useCallback(() => (settingsScope ? settingsScope.getSnapshot() : null), [settingsScope])
|
|
406
|
+
);
|
|
393
407
|
const [state, setState] = React.useState({ status: 'loading', value: null, revision: 0, error: '', providers: [] });
|
|
394
408
|
const [draft, setDraft] = React.useState(null);
|
|
395
409
|
// ── all hooks live ABOVE any early return (React error 310 otherwise) ──
|
|
@@ -455,7 +469,18 @@ window.__ModuleLoader__.load({
|
|
|
455
469
|
.catch((e) => setState((s) => ({ ...s, status: 'error', error: String(e) })));
|
|
456
470
|
}, []);
|
|
457
471
|
|
|
458
|
-
React.useEffect(() => {
|
|
472
|
+
React.useEffect(() => {
|
|
473
|
+
if (scopeSnapshot && scopeSnapshot.status === 'ready' && scopeSnapshot.value) {
|
|
474
|
+
setState((s) => ({
|
|
475
|
+
...s,
|
|
476
|
+
status: 'ready',
|
|
477
|
+
value: scopeSnapshot.value,
|
|
478
|
+
revision: scopeSnapshot.revision ?? s.revision,
|
|
479
|
+
}));
|
|
480
|
+
} else {
|
|
481
|
+
load();
|
|
482
|
+
}
|
|
483
|
+
}, [load, scopeSnapshot]);
|
|
459
484
|
|
|
460
485
|
const val = draft ?? state.value;
|
|
461
486
|
const status = useRotationStatus();
|
|
@@ -650,21 +675,38 @@ window.__ModuleLoader__.load({
|
|
|
650
675
|
const save = () => {
|
|
651
676
|
if (!draft) return;
|
|
652
677
|
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);
|
|
678
|
+
const saveViaBridge = () => {
|
|
679
|
+
fetch(CONFIG_PATH, {
|
|
680
|
+
method: 'PUT',
|
|
681
|
+
headers: { 'content-type': 'application/json' },
|
|
682
|
+
body: JSON.stringify({ section: draft, expectedRevision: state.revision }),
|
|
666
683
|
})
|
|
667
|
-
|
|
684
|
+
.then((r) => r.json())
|
|
685
|
+
.then((data) => {
|
|
686
|
+
if (data.error) {
|
|
687
|
+
setState((s) => ({ ...s, status: 'error', error: data.error.message }));
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
setState((s) => ({ status: 'ready', value: data.value ?? draft, revision: data.revision ?? s.revision, error: '', providers: s.providers }));
|
|
691
|
+
setDraft(null);
|
|
692
|
+
})
|
|
693
|
+
.catch((e) => setState((s) => ({ ...s, status: 'error', error: String(e) })));
|
|
694
|
+
};
|
|
695
|
+
if (settingsScope && typeof settingsScope.set === 'function') {
|
|
696
|
+
(async () => {
|
|
697
|
+
try {
|
|
698
|
+
for (const [k, v] of Object.entries(draft)) {
|
|
699
|
+
await settingsScope.set(k, v);
|
|
700
|
+
}
|
|
701
|
+
setState((s) => ({ ...s, status: 'ready', value: draft, error: '' }));
|
|
702
|
+
setDraft(null);
|
|
703
|
+
} catch (_) {
|
|
704
|
+
saveViaBridge();
|
|
705
|
+
}
|
|
706
|
+
})();
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
saveViaBridge();
|
|
668
710
|
};
|
|
669
711
|
|
|
670
712
|
|
|
@@ -1021,13 +1063,7 @@ window.__ModuleLoader__.load({
|
|
|
1021
1063
|
);
|
|
1022
1064
|
}
|
|
1023
1065
|
|
|
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
|
-
}
|
|
1066
|
+
|
|
1031
1067
|
|
|
1032
1068
|
// #201 header chip: one dot + counts for all pools. Green = all healthy,
|
|
1033
1069
|
// amber = some keys cooling, red = a pool fully exhausted. Click opens the
|
|
@@ -1096,9 +1132,7 @@ window.__ModuleLoader__.load({
|
|
|
1096
1132
|
}
|
|
1097
1133
|
|
|
1098
1134
|
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');
|
|
1135
|
+
ctx.effect(() => ctx.locale.register(NS, { en, ru, zh }), 'dsh-key-rotation: dictionaries');
|
|
1102
1136
|
// Header chip (#201): status dot in the session header utilities slot,
|
|
1103
1137
|
// same slot dsh-gitea / dsh-subscriptions use for their header widgets.
|
|
1104
1138
|
ctx.effect(() => {
|
|
@@ -1144,8 +1178,16 @@ window.__ModuleLoader__.load({
|
|
|
1144
1178
|
};
|
|
1145
1179
|
if (!tryPluginItem()) {
|
|
1146
1180
|
// Fallback for builds without the Plugins tab slot: keep the sidebar section.
|
|
1181
|
+
const t = ctx.locale ? ctx.locale.bind(NS) : null;
|
|
1147
1182
|
ctx.slots.inject('settings.section', () => ctx.slots.register(
|
|
1148
|
-
{
|
|
1183
|
+
{
|
|
1184
|
+
name: 'settings.section',
|
|
1185
|
+
id: 'dsh-key-rotation',
|
|
1186
|
+
order: 20,
|
|
1187
|
+
locale: NS,
|
|
1188
|
+
label: () => (t ? t('title') : 'Key Rotation') || 'Key Rotation',
|
|
1189
|
+
inject: () => ({ ctx }),
|
|
1190
|
+
},
|
|
1149
1191
|
(props) => h(KeyRotationSection, { ...props, locale: useLocale() }),
|
|
1150
1192
|
));
|
|
1151
1193
|
}
|
package/lib/index.js
CHANGED
|
@@ -219,7 +219,7 @@ export const Config = Schema.object({
|
|
|
219
219
|
selfHealIdleMs: Schema.number().default(3600000),
|
|
220
220
|
latencyEnabled: Schema.boolean().default(true),
|
|
221
221
|
latencyWindow: Schema.number().default(200),
|
|
222
|
-
incidentGitHubToken: Schema.string().default(''),
|
|
222
|
+
incidentGitHubToken: Schema.string().role('secret').default(''),
|
|
223
223
|
incidentGitHubBaseUrl: Schema.string().default(''),
|
|
224
224
|
incidentThreshold: Schema.number().default(5),
|
|
225
225
|
concurrencyLimit: Schema.number().default(0),
|
|
@@ -235,7 +235,7 @@ export const Config = Schema.object({
|
|
|
235
235
|
}),
|
|
236
236
|
rateLimitThreshold: Schema.number().default(0.1),
|
|
237
237
|
rpmLimit: Schema.number().default(0),
|
|
238
|
-
webhookActionToken: Schema.string().default(''),
|
|
238
|
+
webhookActionToken: Schema.string().role('secret').default(''),
|
|
239
239
|
expiryWarnDays: Schema.number().default(7),
|
|
240
240
|
switchNotify: Schema.boolean().default(false),
|
|
241
241
|
switchNotifyThrottleMs: Schema.number().default(60000),
|
|
@@ -862,13 +862,14 @@ export function apply(ctx, config = {}) {
|
|
|
862
862
|
// ── patch credentials.resolve: pool refs resolve to the next healthy key ──
|
|
863
863
|
// Round-robin over the pool, skipping keys in cooldown; the request's
|
|
864
864
|
// provider identity never changes, so pi-ai replay state stays consistent.
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
865
|
+
ctx.effect(() => {
|
|
866
|
+
const credentials = ctx.get('credentials');
|
|
867
|
+
if (credentials && typeof credentials.resolve === 'function' && !credentials.__dshKeyRotationPatched) {
|
|
868
|
+
const original = credentials.resolve.bind(credentials);
|
|
869
|
+
// Kept for the status route: it must ask about one exact ref instead of
|
|
870
|
+
// being rotated to a different key by the patch below.
|
|
871
|
+
credentials.__dshKeyRotationOriginalResolve = original;
|
|
872
|
+
credentials.resolve = async (ref) => {
|
|
872
873
|
const { poolByRef } = buildRuntime();
|
|
873
874
|
const pool = poolByRef.get(ref);
|
|
874
875
|
if (!pool) return original(ref);
|
|
@@ -944,9 +945,17 @@ export function apply(ctx, config = {}) {
|
|
|
944
945
|
}
|
|
945
946
|
}
|
|
946
947
|
return original(ref); // everything cooled/missing — surface the base value
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
948
|
+
};
|
|
949
|
+
credentials.__dshKeyRotationPatched = true;
|
|
950
|
+
return () => {
|
|
951
|
+
if (credentials.__dshKeyRotationPatched) {
|
|
952
|
+
credentials.resolve = original;
|
|
953
|
+
delete credentials.__dshKeyRotationPatched;
|
|
954
|
+
delete credentials.__dshKeyRotationOriginalResolve;
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
}, 'dsh-key-rotation: patch credentials.resolve');
|
|
950
959
|
|
|
951
960
|
const finishError = (code, message) => ({
|
|
952
961
|
type: 'finish',
|
|
@@ -1777,7 +1786,7 @@ export function apply(ctx, config = {}) {
|
|
|
1777
1786
|
},
|
|
1778
1787
|
}), 'dsh-key-rotation: agent-budget');
|
|
1779
1788
|
|
|
1780
|
-
ctx.on('llm/stream', (options, next) => {
|
|
1789
|
+
ctx.effect(() => ctx.on('llm/stream', (options, next) => {
|
|
1781
1790
|
if (options[MARKER]) return next();
|
|
1782
1791
|
if (rotationDisabled) return next(); // #199: disabled via webhook action
|
|
1783
1792
|
const { providerToPool, modelPoolByProvider } = buildRuntime();
|
|
@@ -1786,13 +1795,13 @@ export function apply(ctx, config = {}) {
|
|
|
1786
1795
|
if (!pool) return next();
|
|
1787
1796
|
console.warn(`[dsh-key-rotation] rotating ${options.provider}/${options.model} across ${(pool.weightedRefs ?? pool.refs).length} slots (${pool.refs.length} keys)`);
|
|
1788
1797
|
return rotate(options, pool);
|
|
1789
|
-
});
|
|
1798
|
+
}), 'dsh-key-rotation: llm/stream');
|
|
1790
1799
|
|
|
1791
1800
|
// Safety net for non-stream requests (agent/request-error waterfall).
|
|
1792
1801
|
// llm/stream covers streaming calls; sync calls (embeddings, batch) go
|
|
1793
1802
|
// through agent/request and surface errors here. If the error is
|
|
1794
1803
|
// switchable, mark the key and ask the agent loop to retry.
|
|
1795
|
-
ctx.on('agent/request-error', async (payload, next) => {
|
|
1804
|
+
ctx.effect(() => ctx.on('agent/request-error', async (payload, next) => {
|
|
1796
1805
|
const provider = payload?.provider ?? payload?.failure?.provider ?? '';
|
|
1797
1806
|
if (!provider) return next();
|
|
1798
1807
|
const { providerToPool, modelPoolByProvider, switchCodes } = buildRuntime();
|
|
@@ -1815,7 +1824,7 @@ export function apply(ctx, config = {}) {
|
|
|
1815
1824
|
console.warn(`[dsh-key-rotation] ${provider}: key ${String(ref)} failed via agent/request-error (${String(code)} ${String(message).slice(0, 80)}) — retry`);
|
|
1816
1825
|
}
|
|
1817
1826
|
return { kind: 'retry' };
|
|
1818
|
-
});
|
|
1827
|
+
}), 'dsh-key-rotation: agent/request-error');
|
|
1819
1828
|
|
|
1820
1829
|
ctx.inject(['settings'], (sctx) => {
|
|
1821
1830
|
const scope = sctx.settings.register(NS, Config, { base: config });
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-key-rotation",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.35",
|
|
4
|
+
"packageManager": "pnpm@10.33.2",
|
|
4
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.",
|
|
5
6
|
"keywords": [
|
|
6
7
|
"deepseek-harness",
|
|
@@ -40,13 +41,13 @@
|
|
|
40
41
|
},
|
|
41
42
|
"client": {
|
|
42
43
|
"platform": "web",
|
|
43
|
-
"inject": [
|
|
44
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
45
|
-
"@deepseek-ai/dsh-client-ui-slots"
|
|
46
|
-
]
|
|
44
|
+
"inject": []
|
|
47
45
|
}
|
|
48
46
|
},
|
|
49
47
|
"license": "MIT",
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18"
|
|
50
|
+
},
|
|
50
51
|
"peerDependencies": {
|
|
51
52
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
52
53
|
"@deepseek-ai/schemastery": "^3.18.1",
|