@goodandready/dsh-key-rotation 0.7.6 → 0.7.9

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.
Files changed (2) hide show
  1. package/lib/client.js +66 -7
  2. package/package.json +4 -1
package/lib/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // dsh-key-rotation — Settings section ("Key Rotation" / "Ротация ключей").
2
- // Renders in the harness Settings sidebar via the settings.section slot and
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.
5
5
  //
@@ -27,6 +27,8 @@ window.__ModuleLoader__.load({
27
27
  // -------------------------------------------------------------- i18n
28
28
  const en = {
29
29
  title: 'Key Rotation',
30
+ subtitle: 'Per-provider API key rotation: pools of keys, automatic failover on quota/rate-limit errors, cooldown and recovery.',
31
+ cardDesc: 'Per-provider API key rotation: pools of keys, automatic failover on quota/rate-limit errors, cooldown and recovery.',
30
32
  loading: 'Loading…',
31
33
  notRegistered: 'not registered',
32
34
  removeKey: 'Remove key',
@@ -67,6 +69,8 @@ window.__ModuleLoader__.load({
67
69
  importPools: 'Import',
68
70
  importEnv: 'Import .env',
69
71
  resetCooldown: 'Reset cooldown',
72
+ testAll: 'Test all keys',
73
+ testing: 'Testing…',
70
74
  testKey: 'Test',
71
75
  testOk: 'OK',
72
76
  testFail: 'FAIL',
@@ -76,6 +80,8 @@ window.__ModuleLoader__.load({
76
80
  };
77
81
  const ru = {
78
82
  title: 'Ротация ключей',
83
+ subtitle: 'Ротация API-ключей по провайдерам: пулы ключей, автоматическое переключение при исчерпании квоты или лимита, кулдаун и восстановление.',
84
+ cardDesc: 'Ротация API-ключей по провайдерам: пулы ключей, автоматическое переключение при исчерпании квоты или лимита, кулдаун и восстановление.',
79
85
  loading: 'Загрузка…',
80
86
  notRegistered: 'не зарегистрирован',
81
87
  removeKey: 'Удалить ключ',
@@ -116,6 +122,8 @@ window.__ModuleLoader__.load({
116
122
  importPools: 'Импорт',
117
123
  importEnv: 'Импорт .env',
118
124
  resetCooldown: 'Сбросить кулдаун',
125
+ testAll: 'Тест всех ключей',
126
+ testing: 'Тестирование…',
119
127
  testKey: 'Тест',
120
128
  testOk: 'OK',
121
129
  testFail: 'FAIL',
@@ -194,6 +202,13 @@ window.__ModuleLoader__.load({
194
202
  '.krot-btn:not(:disabled):hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3,var(--dsw-alias-border-l2))}',
195
203
  '.krot-foot{display:flex;gap:8px;align-items:center}',
196
204
  '.krot-save{background:var(--dsw-alias-button-info-fill);border-color:var(--dsw-alias-button-info-fill);color:var(--dsw-alias-label-primary-foreground);padding:5px 14px;font-size:13px}',
205
+ '.krot-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;overflow:hidden}',
206
+ '.krot-card-header{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;padding:10px 12px;background:transparent;border:none;cursor:pointer;color:var(--dsw-alias-label-primary);font:inherit;text-align:left}',
207
+ '.krot-card-head-text{display:flex;flex-direction:column;gap:2px;min-width:0}',
208
+ '.krot-card-name{font-weight:600;font-size:13px}',
209
+ '.krot-card-description{font-size:11px;color:var(--dsw-alias-label-tertiary)}',
210
+ '.krot-card-chevron{flex:none;opacity:.6}',
211
+ '.krot-card-body{padding:0 12px 12px;border-top:1px solid var(--dsw-alias-border-l2)}',
197
212
  ].join('');
198
213
  const CARD_CSS_ID = 'dsh-key-rotation/section.module.css';
199
214
  if (typeof document !== 'undefined' && !document.querySelector('style[data-plugin-css="' + CARD_CSS_ID + '"]')) {
@@ -258,6 +273,7 @@ window.__ModuleLoader__.load({
258
273
  const undoTimer = React.useRef(null);
259
274
  const [testing, setTesting] = React.useState('');
260
275
  const [testResult, setTestResult] = React.useState({});
276
+ const [testAllProvider, setTestAllProvider] = React.useState('');
261
277
  const [secretDraft, setSecretDraft] = React.useState({});
262
278
  const [secretError, setSecretError] = React.useState('');
263
279
  const stashUndo = (u) => { setUndo(u); if (undoTimer.current) clearTimeout(undoTimer.current); undoTimer.current = setTimeout(() => setUndo(null), 5000); };
@@ -275,6 +291,22 @@ window.__ModuleLoader__.load({
275
291
  .catch((e) => setTestResult((m) => ({ ...m, [ref]: { ok: false, message: String(e?.message ?? e) } })))
276
292
  .finally(() => setTesting(''));
277
293
  };
294
+ const doTestAll = (providerId) => {
295
+ if (!val || !Array.isArray(val.providers)) return;
296
+ const entry = val.providers.find((p) => p.provider === providerId);
297
+ if (!entry || !Array.isArray(entry.keys)) return;
298
+ const refs = entry.keys.filter((k) => k && typeof k === 'string' && k.length > 0);
299
+ setTestAllProvider(providerId);
300
+ Promise.all(refs.map((ref) =>
301
+ fetch('/dsh-key-rotation/test', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ref }) })
302
+ .then((r) => r.json())
303
+ .then((data) => ({ ref, data }))
304
+ .catch((e) => ({ ref, data: { ok: false, message: String(e?.message ?? e) } }))
305
+ )).then((results) => {
306
+ setTestResult((m) => { const nm = { ...m }; for (const { ref, data } of results) nm[ref] = data; return nm; });
307
+ setTestAllProvider('');
308
+ });
309
+ };
278
310
 
279
311
  const load = React.useCallback(() => {
280
312
  setState((s) => ({ ...s, status: 'loading', error: '' }));
@@ -569,6 +601,7 @@ window.__ModuleLoader__.load({
569
601
  (providerStatus && Array.isArray(providerStatus.events) && providerStatus.events.length > 0 ? h('div', { style: { display: 'flex', gap: '2px', alignItems: 'end', height: '24px', marginTop: '4px' } }, (() => { const now = Date.now(); const buckets = Array(24).fill(0); for (const ev of providerStatus.events) { const h = Math.floor((now - ev.at) / 3600000); if (h >= 0 && h < 24) buckets[23 - h]++; } const max = Math.max(1, ...buckets); return buckets.map((c, i) => h('div', { key: i, title: c + ' switches', style: { flex: 1, background: c ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-border-l2)', height: (c / max * 24) + 'px', minHeight: '2px', borderRadius: '2px' } })); })()) : null),
570
602
  (providerStatus && Array.isArray(providerStatus.events) && providerStatus.events.length > 0 ? h('details', { style: { fontSize: '11px', marginTop: '6px' } }, h('summary', null, 'Recent failures ('+providerStatus.events.length+')'), h('ul', { style: { margin: '4px 0 0', paddingLeft: '16px' } }, providerStatus.events.slice().reverse().map((ev, i) => h('li', { key: i, style: ev.type === 'probe' ? { opacity: .5 } : null }, new Date(ev.at).toLocaleTimeString() + ' ' + (ev.type === 'probe' ? '[probe] ' : '') + ev.ref + ' ' + ev.reason + ' cd=' + ev.cooldownMs)) )) : null),
571
603
  btn(t('resetCooldown'), () => doReset(entry.provider), { disabled: !(providerStatus && providerStatus.switches > 0) || resetting === entry.provider, title: t('resetCooldown') }),
604
+ btn(testAllProvider === entry.provider ? t('testing') : t('testAll'), () => doTestAll(entry.provider), { disabled: testAllProvider === entry.provider, title: t('testAll') }),
572
605
  ),
573
606
  );
574
607
  });
@@ -634,12 +667,38 @@ window.__ModuleLoader__.load({
634
667
  function useLocale() {
635
668
  return useActiveLocale(ctx);
636
669
  }
637
- ctx.slots.inject('settings.section', () => ctx.slots.register({
638
- name: 'settings.section',
639
- id: 'dsh-key-rotation',
640
- order: 20,
641
- label: () => 'Key Rotation',
642
- }, (props) => React.createElement(KeyRotationSection, { ...props, locale: useLocale() })));
670
+ // Collapsible card in Settings -> Plugins -> Plugin settings
671
+ // (settings.plugin.item), matching Model Sync / Spendmeter / Vision Bridge.
672
+ // key MUST equal the settings namespace (NS), else the tab silently skips it.
673
+ function KeyRotationCard(props) {
674
+ const locale = useLocale();
675
+ const t = makeT(locale === 'ru' ? ru : en, en);
676
+ const [open, setOpen] = React.useState(false);
677
+ return h('div', { className: 'krot-card' + (open ? ' krot-card-open' : '') },
678
+ h('button', { type: 'button', className: 'krot-card-header', 'aria-expanded': open, onClick: () => setOpen((v) => !v) },
679
+ h('span', { className: 'krot-card-head-text' },
680
+ h('span', { className: 'krot-card-name' }, t('title')),
681
+ h('span', { className: 'krot-card-description' }, t('subtitle'))),
682
+ h('span', { className: 'krot-card-chevron', 'aria-hidden': 'true' }, open ? '▴' : '▾')),
683
+ open ? h('div', { className: 'krot-card-body' }, h(KeyRotationSection, { ...props, locale })) : null);
684
+ }
685
+ const tryPluginItem = () => {
686
+ try {
687
+ ctx.slots.inject('settings.plugin.item', () =>
688
+ ctx.slots.register(
689
+ { name: 'settings.plugin.item', key: NS, locale: NS, inject: () => ({ ctx }) },
690
+ KeyRotationCard,
691
+ ));
692
+ return true;
693
+ } catch { return false; }
694
+ };
695
+ if (!tryPluginItem()) {
696
+ // Fallback for builds without the Plugins tab slot: keep the sidebar section.
697
+ ctx.slots.inject('settings.section', () => ctx.slots.register(
698
+ { name: 'settings.section', id: 'dsh-key-rotation', order: 20, label: () => 'Key Rotation' },
699
+ (props) => h(KeyRotationSection, { ...props, locale: useLocale() }),
700
+ ));
701
+ }
643
702
  }
644
703
 
645
704
  module.exports = { apply, inject: ['slots', 'locale'] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-key-rotation",
3
- "version": "0.7.6",
3
+ "version": "0.7.9",
4
4
  "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
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -54,5 +54,8 @@
54
54
  },
55
55
  "scripts": {
56
56
  "test": "node --test test/*.test.js"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
57
60
  }
58
61
  }