@goodandready/dsh-key-rotation 0.8.15 → 0.8.16
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 +6 -0
- package/lib/client.js +59 -27
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
All notable changes to `@goodandready/dsh-key-rotation` are documented here.
|
|
4
4
|
User-facing feature notes also appear in README (en is source of truth).
|
|
5
5
|
|
|
6
|
+
## 0.8.16 - 2026-09-20
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- **Header chip translation scope (#330, GitHub #7 / PR #8)**: `KeyRotationHeaderChip` in `lib/client.js` now accepts props and resolves translator `resolveT(props)` before opening popover, preventing `ReferenceError: t is not defined` and fixing disappearance on first click. Header slot registration includes `locale: NS`.
|
|
10
|
+
- **Live bulk key testing & bounded concurrency (#330, GitHub #9 / PR #10)**: "Test all keys" now executes live API probes (`probe: 'models'`) identically to individual testing via unified `runKeyTests()`, eliminating false-positive green status on invalid keys. Requests are serialized (concurrency 1) to avoid bursting endpoints, mutex `testRunRef` guards duplicate clicks, and returned failure error codes (`tr.code`) are displayed in the UI.
|
|
11
|
+
|
|
6
12
|
## 0.8.15 - 2026-09-20
|
|
7
13
|
|
|
8
14
|
### Fixed
|
package/lib/client.js
CHANGED
|
@@ -737,6 +737,7 @@ window.__ModuleLoader__.load({
|
|
|
737
737
|
}
|
|
738
738
|
};
|
|
739
739
|
}, [confirmModal]);
|
|
740
|
+
const testRunRef = React.useRef(false);
|
|
740
741
|
const [testing, setTesting] = React.useState('');
|
|
741
742
|
const [testResult, setTestResult] = React.useState({});
|
|
742
743
|
const [testAllProvider, setTestAllProvider] = React.useState('');
|
|
@@ -750,30 +751,59 @@ window.__ModuleLoader__.load({
|
|
|
750
751
|
else if (providers[u.index]) { const keys=[...providers[u.index].keys]; keys.splice(Math.min(u.kIndex, keys.length), 0, u.key); providers[u.index] = { ...providers[u.index], keys }; }
|
|
751
752
|
return { ...cur, providers };
|
|
752
753
|
}); };
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
754
|
+
// Both buttons must run the same live probe, never a presence-only check.
|
|
755
|
+
// Serialize probes: a large pool must not burst /models requests. The ref
|
|
756
|
+
// also guards repeated clicks before React has rendered disabled buttons.
|
|
757
|
+
const runKeyTests = async (refs, providerId = '') => {
|
|
758
|
+
if (testRunRef.current || refs.length === 0) return;
|
|
759
|
+
testRunRef.current = true;
|
|
760
|
+
setTestAllProvider(providerId);
|
|
761
|
+
setTestResult((m) => {
|
|
762
|
+
const next = { ...m };
|
|
763
|
+
for (const ref of refs) next[ref] = null;
|
|
764
|
+
return next;
|
|
765
|
+
});
|
|
766
|
+
if (providerId) setTestingProgress((m) => ({ ...m, [providerId]: '0/' + refs.length }));
|
|
767
|
+
try {
|
|
768
|
+
let completed = 0;
|
|
769
|
+
for (const ref of refs) {
|
|
770
|
+
setTesting(ref);
|
|
771
|
+
let result;
|
|
772
|
+
try {
|
|
773
|
+
const response = await fetch('/dsh-key-rotation/test', {
|
|
774
|
+
method: 'POST',
|
|
775
|
+
headers: { 'content-type': 'application/json' },
|
|
776
|
+
body: JSON.stringify({ ref, probe: 'models' }),
|
|
777
|
+
});
|
|
778
|
+
const data = await response.json();
|
|
779
|
+
if (!response.ok) {
|
|
780
|
+
result = { ok: false, code: data?.error?.code || ('http-' + response.status),
|
|
781
|
+
message: data?.error?.message || data?.message };
|
|
782
|
+
} else if (typeof data?.ok !== 'boolean') {
|
|
783
|
+
result = { ok: false, code: 'invalid-response' };
|
|
784
|
+
} else {
|
|
785
|
+
result = data;
|
|
786
|
+
}
|
|
787
|
+
} catch (e) {
|
|
788
|
+
result = { ok: false, code: 'error', message: String(e?.message ?? e) };
|
|
789
|
+
}
|
|
790
|
+
setTestResult((m) => ({ ...m, [ref]: result }));
|
|
791
|
+
completed++;
|
|
792
|
+
if (providerId) setTestingProgress((m) => ({ ...m, [providerId]: completed + '/' + refs.length }));
|
|
793
|
+
}
|
|
794
|
+
} finally {
|
|
795
|
+
setTesting('');
|
|
796
|
+
setTestAllProvider('');
|
|
797
|
+
testRunRef.current = false;
|
|
798
|
+
}
|
|
761
799
|
};
|
|
800
|
+
const doTest = (ref) => runKeyTests([ref]);
|
|
762
801
|
const doTestAll = (providerId) => {
|
|
763
802
|
if (!val || !Array.isArray(val.providers)) return;
|
|
764
803
|
const entry = val.providers.find((p) => p.provider === providerId);
|
|
765
804
|
if (!entry || !Array.isArray(entry.keys)) return;
|
|
766
|
-
const refs = entry.keys.filter((k) =>
|
|
767
|
-
|
|
768
|
-
Promise.all(refs.map((ref) =>
|
|
769
|
-
fetch('/dsh-key-rotation/test', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ref }) })
|
|
770
|
-
.then((r) => r.json())
|
|
771
|
-
.then((data) => ({ ref, data }))
|
|
772
|
-
.catch((e) => ({ ref, data: { ok: false, message: String(e?.message ?? e) } }))
|
|
773
|
-
)).then((results) => {
|
|
774
|
-
setTestResult((m) => { const nm = { ...m }; for (const { ref, data } of results) nm[ref] = data; return nm; });
|
|
775
|
-
setTestAllProvider('');
|
|
776
|
-
});
|
|
805
|
+
const refs = [...new Set(entry.keys.filter((k) => typeof k === 'string' && k.trim().length > 0))];
|
|
806
|
+
return runKeyTests(refs, providerId);
|
|
777
807
|
};
|
|
778
808
|
|
|
779
809
|
const load = React.useCallback(() => {
|
|
@@ -1203,12 +1233,13 @@ window.__ModuleLoader__.load({
|
|
|
1203
1233
|
const tr = testResult[key];
|
|
1204
1234
|
if (tr) meta.push(h('span', { key: 'tr', className: 'krot-tail',
|
|
1205
1235
|
title: (tr.message || (tr.ok ? t('testOk') : t('testFail')))
|
|
1236
|
+
+ (!tr.ok && tr.code ? ' · ' + tr.code : '')
|
|
1206
1237
|
+ (tr.ok && tr.modelsCount ? ' · ' + tr.modelsCount + ' models' : '')
|
|
1207
1238
|
+ (tr.ok && tr.latencyMs ? ' · ' + tr.latencyMs + 'ms' : ''),
|
|
1208
1239
|
style: { color: tr.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)', fontWeight: 700 } },
|
|
1209
|
-
tr.ok ? (tr.modelsCount ? tr.modelsCount + 'm' : '✓') : '✕'));
|
|
1240
|
+
tr.ok ? (tr.modelsCount ? tr.modelsCount + 'm' : '✓') : ('✕' + (tr.code ? ' ' + tr.code : ''))));
|
|
1210
1241
|
// #219: last probe from the sandbox cache, greyed when older than 24h
|
|
1211
|
-
else if (probeCache && probeCache[key]) {
|
|
1242
|
+
else if (!Object.prototype.hasOwnProperty.call(testResult, key) && probeCache && probeCache[key]) {
|
|
1212
1243
|
const pc = probeCache[key];
|
|
1213
1244
|
const stale = Date.now() - (pc.at ?? 0) > 86400000;
|
|
1214
1245
|
meta.push(h('span', { key: 'pc', className: 'krot-tail',
|
|
@@ -1216,7 +1247,7 @@ window.__ModuleLoader__.load({
|
|
|
1216
1247
|
style: { opacity: stale ? 0.4 : 0.7, color: pc.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)' } },
|
|
1217
1248
|
(pc.ok ? '✓' : '✕') + (pc.latencyMs ? ' ' + pc.latencyMs + 'ms' : '')));
|
|
1218
1249
|
}
|
|
1219
|
-
meta.push(h('button', { key: 't', className: 'krot-btn', onClick: () => doTest(key), disabled: testing
|
|
1250
|
+
meta.push(h('button', { key: 't', className: 'krot-btn', onClick: () => doTest(key), disabled: Boolean(testing || testAllProvider), title: t('testKey') }, testing === key ? '…' : t('testKey')));
|
|
1220
1251
|
// #223: broken keys get a one-click live re-test + auto-unbreak
|
|
1221
1252
|
if (info && info.broken) {
|
|
1222
1253
|
meta.push(h('button', { key: 'rt', className: 'krot-btn', onClick: () => retestBroken(key), title: t('retestBroken') }, t('retestBroken')));
|
|
@@ -1429,7 +1460,7 @@ window.__ModuleLoader__.load({
|
|
|
1429
1460
|
danger: false,
|
|
1430
1461
|
onConfirm: () => doReset(entry.provider),
|
|
1431
1462
|
}), { disabled: !(providerStatus && providerStatus.switches > 0) || resetting === entry.provider, title: t('resetCooldown') }),
|
|
1432
|
-
btn(testAllProvider === entry.provider ? (testingProgress[entry.provider] ? t('testingAllProgress').replace('{n}', testingProgress[entry.provider].split('/')[0]).replace('{total}', testingProgress[entry.provider].split('/')[1]) : t('testing')) : t('testAll'), () => doTestAll(entry.provider), { disabled:
|
|
1463
|
+
btn(testAllProvider === entry.provider ? (testingProgress[entry.provider] ? t('testingAllProgress').replace('{n}', testingProgress[entry.provider].split('/')[0]).replace('{total}', testingProgress[entry.provider].split('/')[1]) : t('testing')) : t('testAll'), () => doTestAll(entry.provider), { disabled: Boolean(testing || testAllProvider), title: t('testAll') }),
|
|
1433
1464
|
exportCsv,
|
|
1434
1465
|
),
|
|
1435
1466
|
);
|
|
@@ -1701,7 +1732,8 @@ window.__ModuleLoader__.load({
|
|
|
1701
1732
|
// #201 header chip: one dot + counts for all pools. Green = all healthy,
|
|
1702
1733
|
// amber = some keys cooling, red = a pool fully exhausted. Click opens the
|
|
1703
1734
|
// same summary the floating dashboard shows.
|
|
1704
|
-
function KeyRotationHeaderChip() {
|
|
1735
|
+
function KeyRotationHeaderChip(props) {
|
|
1736
|
+
const t = resolveT(props);
|
|
1705
1737
|
const [snap, setSnap] = React.useState(null);
|
|
1706
1738
|
const [open, setOpen] = React.useState(false);
|
|
1707
1739
|
const ref = React.useRef(null);
|
|
@@ -1747,7 +1779,7 @@ window.__ModuleLoader__.load({
|
|
|
1747
1779
|
h('button', {
|
|
1748
1780
|
type: 'button',
|
|
1749
1781
|
className: 'krot-header-chip',
|
|
1750
|
-
title: '
|
|
1782
|
+
title: t('title'),
|
|
1751
1783
|
onClick: () => setOpen((v) => !v),
|
|
1752
1784
|
},
|
|
1753
1785
|
h('span', { style: { width: '8px', height: '8px', borderRadius: '50%', background: color, flex: 'none', boxShadow: `0 0 6px ${color}` } }),
|
|
@@ -1756,7 +1788,7 @@ window.__ModuleLoader__.load({
|
|
|
1756
1788
|
),
|
|
1757
1789
|
open ? h('div', { className: 'krot-popover' },
|
|
1758
1790
|
h('div', { className: 'krot-pop-title' }, t('headerPoolsTitle')),
|
|
1759
|
-
!poolEntries.length ? h('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-tertiary)' } }, '
|
|
1791
|
+
!poolEntries.length ? h('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-tertiary)' } }, t('noActivePools')) : null,
|
|
1760
1792
|
poolEntries.map(([name, p]) => {
|
|
1761
1793
|
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)');
|
|
1762
1794
|
return h('div', { key: name, className: 'krot-pop-row' },
|
|
@@ -1780,7 +1812,7 @@ window.__ModuleLoader__.load({
|
|
|
1780
1812
|
try {
|
|
1781
1813
|
ctx.slots.inject('conversation.session.header.utilities', () =>
|
|
1782
1814
|
ctx.slots.register(
|
|
1783
|
-
{ name: 'conversation.session.header.utilities', id: 'dsh-key-rotation-header-chip', order: 6 },
|
|
1815
|
+
{ name: 'conversation.session.header.utilities', id: 'dsh-key-rotation-header-chip', order: 6, locale: NS },
|
|
1784
1816
|
KeyRotationHeaderChip,
|
|
1785
1817
|
));
|
|
1786
1818
|
} catch { /* slot not available in this build */ }
|
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.16",
|
|
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": [
|