dsh-keys-manager 0.2.1 → 0.2.3
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 +97 -0
- package/README.md +108 -15
- package/README.zh-CN.md +80 -2
- package/bundle/cordis.patch.yml +5 -2
- package/lib/client/styles.d.ts +1 -1
- package/lib/client/types.d.ts +27 -2
- package/lib/client/ui.d.ts +14 -0
- package/lib/client.js +445 -18
- package/lib/domain.d.ts +63 -0
- package/lib/host-types.d.ts +29 -0
- package/lib/index.d.ts +973 -45
- package/lib/index.js +4257 -332
- package/lib/index.js.map +1 -1
- package/lib/providers.js +426 -0
- package/lib/providers.js.map +1 -1
- package/lib/store.js +4 -0
- package/lib/store.js.map +1 -1
- package/locale/en.json +27 -1
- package/locale/zh.json +27 -1
- package/package.json +14 -12
- package/scripts/prepare-release.mjs +431 -154
- package/scripts/verify-install.mjs +156 -0
package/lib/client.js
CHANGED
|
@@ -44,9 +44,20 @@ var zh = {
|
|
|
44
44
|
title: "\u5BC6\u94A5\u7BA1\u7406",
|
|
45
45
|
subtitle: "\u4E3A\u540C\u4E00\u4E2A Provider \u914D\u7F6E\u591A\u4E2A API Key\u3002\u5355\u4F1A\u8BDD\u56FA\u5B9A\u4F7F\u7528\u4E00\u4E2A Key\uFF0C\u65B0\u4F1A\u8BDD\u8F6E\u6362\uFF1B\u5931\u8D25\u540E\u81EA\u52A8\u51B7\u5374\u5E76\u6062\u590D\u3002",
|
|
46
46
|
search: "\u641C\u7D22 Provider\uFF08\u540D\u79F0\u3001ID \u6216\u73AF\u5883\u53D8\u91CF\uFF09",
|
|
47
|
+
filterAll: "\u5168\u90E8",
|
|
48
|
+
filterBuiltin: "\u5185\u7F6E",
|
|
49
|
+
filterPlugin: "\u63D2\u4EF6",
|
|
50
|
+
filterOauth: "\u652F\u6301 OAuth",
|
|
51
|
+
filterTransport: "\u4F20\u8F93",
|
|
52
|
+
filterGroup: "\u7B5B\u9009",
|
|
53
|
+
// Unattributed requests (I-000120, D8): the compass's fixed copy, shared
|
|
54
|
+
// with the filter plan's dictionary task — one pair, never a second.
|
|
55
|
+
unattributed: "{n} \u65E0\u5F52\u5C5E",
|
|
56
|
+
unattributedHint: "\u8BE5\u8DEF\u7531\u5728\u5F52\u5C5E\u8BFB\u53D6\u5C1A\u672A\u843D\u5730\uFF08\u6216\u5DF2\u5931\u8D25\uFF09\u671F\u95F4\u5E94\u7B54\u7684\u6D41\u5C1D\u8BD5\u6570\uFF1B\u5931\u8D25\u8F6C\u79FB\u91CD\u8BD5\u4F1A\u5404\u8BA1\u4E00\u6B21\uFF0C\u6545\u5355\u4E2A\u8BF7\u6C42\u53EF\u80FD\u88AB\u8BA1\u591A\u6B21\u3002\u8BFB\u53D6\u5B8C\u6210\u5F52\u5C5E\u540E\u6E05\u96F6\u3002",
|
|
47
57
|
managed: "\u5DF2\u63A5\u7BA1",
|
|
48
58
|
available: "\u53EF\u6DFB\u52A0 Providers",
|
|
49
59
|
managedEmpty: "\u8FD8\u6CA1\u6709\u63A5\u7BA1\u7684 Provider\u3002\u4ECE\u4E0B\u65B9\u9009\u62E9\u4E00\u4E2A\u5F00\u59CB\u3002",
|
|
60
|
+
managedFilteredEmpty: "\u5F53\u524D\u7B5B\u9009\u4E0B\u6CA1\u6709\u5DF2\u63A5\u7BA1\u7684 Provider\u3002",
|
|
50
61
|
availableEmpty: "\u6CA1\u6709\u5339\u914D\u7684 Provider\u3002",
|
|
51
62
|
keys: "\u5BC6\u94A5",
|
|
52
63
|
healthy: "\u53EF\u7528",
|
|
@@ -175,6 +186,12 @@ var zh = {
|
|
|
175
186
|
accountProjected: "\u670D\u52A1\u4E2D",
|
|
176
187
|
accountReauth: "\u91CD\u65B0\u8BA4\u8BC1",
|
|
177
188
|
accountActivate: "\u8BBE\u4E3A\u5F53\u524D",
|
|
189
|
+
testKey: "\u6D4B\u8BD5\u8FDE\u63A5",
|
|
190
|
+
testRunning: "\u6D4B\u8BD5\u4E2D\u2026",
|
|
191
|
+
testPassed: "\u8FDE\u63A5\u6B63\u5E38 \xB7 {ms} ms",
|
|
192
|
+
testFailed: "\u8FDE\u63A5\u5931\u8D25\uFF1A{error}",
|
|
193
|
+
testTimedOut: "\u6D4B\u8BD5\u8D85\u65F6\uFF0C\u672A\u6536\u5230\u7ED3\u679C",
|
|
194
|
+
testFailedUnknown: "\u6D4B\u8BD5\u672A\u5B8C\u6210\uFF0C\u672A\u8FD4\u56DE\u539F\u56E0",
|
|
178
195
|
loginTitle: "\u8D26\u6237\u767B\u5F55",
|
|
179
196
|
loginPending: "\u7B49\u5F85\u6388\u6743\u2026",
|
|
180
197
|
loginBrowserOpen: "\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00",
|
|
@@ -185,16 +202,38 @@ var zh = {
|
|
|
185
202
|
loginReady: "\u767B\u5F55\u6210\u529F\u3002",
|
|
186
203
|
deleteAccountTitle: "\u5220\u9664\u8D26\u6237\u201C{alias}\u201D\uFF1F",
|
|
187
204
|
deleteAccountBody: "\u8FD9\u4F1A\u4ECE\u51ED\u8BC1\u5E93\u4E2D\u5220\u9664\u8BE5\u8D26\u6237\u4FDD\u5B58\u7684 OAuth \u6388\u6743\u3002\u5982\u679C\u5B83\u662F\u5F53\u524D\u8D26\u6237\uFF0C\u8BE5 Provider \u8DEF\u7531\u5C06\u505C\u7528\u5B83\u5E76\u56DE\u843D\u5230 API-key \u6C60\uFF1BProvider \u4ECD\u4FDD\u6301\u63A5\u7BA1\u72B6\u6001\u3002",
|
|
188
|
-
deleteAccountBodyNoKeys: "\u8FD9\u4F1A\u4ECE\u51ED\u8BC1\u5E93\u4E2D\u5220\u9664\u8BE5\u8D26\u6237\u4FDD\u5B58\u7684 OAuth \u6388\u6743\u3002\u8BE5\u8DEF\u7531\u6CA1\u6709 API-key \u6C60\uFF0C\u5220\u9664\u540E\u5C06\u505C\u6B62\u670D\u52A1\uFF0C\u76F4\u5230\u91CD\u65B0\u767B\u5F55\u8D26\u6237\u3002"
|
|
205
|
+
deleteAccountBodyNoKeys: "\u8FD9\u4F1A\u4ECE\u51ED\u8BC1\u5E93\u4E2D\u5220\u9664\u8BE5\u8D26\u6237\u4FDD\u5B58\u7684 OAuth \u6388\u6743\u3002\u8BE5\u8DEF\u7531\u6CA1\u6709 API-key \u6C60\uFF0C\u5220\u9664\u540E\u5C06\u505C\u6B62\u670D\u52A1\uFF0C\u76F4\u5230\u91CD\u65B0\u767B\u5F55\u8D26\u6237\u3002",
|
|
206
|
+
// Transport accounts (llm-omp-transports/<route>): managed beside the
|
|
207
|
+
// pi-ai pool, so the copy never promises a key-pool fallback.
|
|
208
|
+
transportEmpty: "\u8FD8\u6CA1\u6709\u4F20\u8F93\u8D26\u6237\u3002",
|
|
209
|
+
transportProviderHint: "\u8BE5\u8DEF\u7531\u7531\u4F20\u8F93\u63D2\u4EF6\u63D0\u4F9B\uFF0C\u51ED\u8BC1\u5C31\u5728\u672C\u5361\u7247\u4E2D\u7BA1\u7406\u3002",
|
|
210
|
+
transportTag: "\u4F20\u8F93",
|
|
211
|
+
transportSignIn: "\u767B\u5F55",
|
|
212
|
+
transportExpires: "\u6709\u6548\u671F\u81F3 {time}",
|
|
213
|
+
transportStatusActive: "\u6B63\u5E38",
|
|
214
|
+
transportStatusExpired: "\u4EE4\u724C\u5DF2\u8FC7\u671F",
|
|
215
|
+
transportStatusNeedsReauth: "\u9700\u91CD\u65B0\u767B\u5F55",
|
|
216
|
+
transportStatusSignedOut: "\u672A\u767B\u5F55",
|
|
217
|
+
transportRemoveTitle: "\u79FB\u9664 {name} \u4F20\u8F93\u8D26\u6237\uFF1F",
|
|
218
|
+
transportRemoveBody: "\u8FD9\u4F1A\u4ECE\u51ED\u8BC1\u5E93\u4E2D\u5220\u9664\u8BE5\u4F20\u8F93\u8DEF\u7531\u4FDD\u5B58\u7684\u6388\u6743\uFF0C\u5E76\u53D6\u6D88\u4ECD\u5728\u8FDB\u884C\u7684\u767B\u5F55\uFF1B\u8DEF\u7531\u81EA\u8EAB\u4E0D\u4F1A\u91CD\u5EFA\u5B83\u3002\u8981\u6062\u590D\u670D\u52A1\u9700\u8981\u91CD\u65B0\u767B\u5F55\u3002"
|
|
189
219
|
};
|
|
190
220
|
var en = {
|
|
191
221
|
nav: "Keys Manager",
|
|
192
222
|
title: "Keys Manager",
|
|
193
223
|
subtitle: "Give one provider several API keys. A session keeps one key; new sessions rotate; failures cool down and recover automatically.",
|
|
194
224
|
search: "Search providers by name, id, or environment variable",
|
|
225
|
+
filterAll: "All",
|
|
226
|
+
filterBuiltin: "Built-in",
|
|
227
|
+
filterPlugin: "Plugin",
|
|
228
|
+
filterOauth: "OAuth-capable",
|
|
229
|
+
filterTransport: "Transport",
|
|
230
|
+
filterGroup: "Filters",
|
|
231
|
+
unattributed: "{n} unattributed",
|
|
232
|
+
unattributedHint: "Stream attempts this route served with no account owner because its ownership read had not landed (or had failed). A failover retry is a new attempt, so one request can be counted more than once. The count clears when a read attributes the route.",
|
|
195
233
|
managed: "Managed",
|
|
196
234
|
available: "Available providers",
|
|
197
235
|
managedEmpty: "No managed providers yet. Adopt one below to start.",
|
|
236
|
+
managedFilteredEmpty: "No managed providers match the current filter.",
|
|
198
237
|
availableEmpty: "No matching providers.",
|
|
199
238
|
keys: "keys",
|
|
200
239
|
healthy: "healthy",
|
|
@@ -323,6 +362,12 @@ var en = {
|
|
|
323
362
|
accountProjected: "Serving provider",
|
|
324
363
|
accountReauth: "Re-authenticate",
|
|
325
364
|
accountActivate: "Set active",
|
|
365
|
+
testKey: "Test",
|
|
366
|
+
testRunning: "Testing\u2026",
|
|
367
|
+
testPassed: "Test passed \xB7 {ms} ms",
|
|
368
|
+
testFailed: "Test failed: {error}",
|
|
369
|
+
testTimedOut: "The test timed out without a result",
|
|
370
|
+
testFailedUnknown: "The test did not complete and reported no reason",
|
|
326
371
|
loginTitle: "Account sign-in",
|
|
327
372
|
loginPending: "Waiting for authorization\u2026",
|
|
328
373
|
loginBrowserOpen: "Open in browser",
|
|
@@ -333,7 +378,19 @@ var en = {
|
|
|
333
378
|
loginReady: "Signed in.",
|
|
334
379
|
deleteAccountTitle: "Delete account \u201C{alias}\u201D?",
|
|
335
380
|
deleteAccountBody: "This removes the account\u2019s stored OAuth grant from the credential store. If it is the active account, the provider route stops serving it and falls back to its API-key pool; the provider stays managed.",
|
|
336
|
-
deleteAccountBodyNoKeys: "This removes the account\u2019s stored OAuth grant from the credential store. This route has no API-key pool, so it stops serving requests until an account is signed in again."
|
|
381
|
+
deleteAccountBodyNoKeys: "This removes the account\u2019s stored OAuth grant from the credential store. This route has no API-key pool, so it stops serving requests until an account is signed in again.",
|
|
382
|
+
// Transport accounts — same values as locale/en.json.
|
|
383
|
+
transportEmpty: "No transport accounts yet.",
|
|
384
|
+
transportProviderHint: "Served by a transport plugin: its credential is managed inside this card.",
|
|
385
|
+
transportTag: "Transport",
|
|
386
|
+
transportSignIn: "Sign in",
|
|
387
|
+
transportExpires: "Expires {time}",
|
|
388
|
+
transportStatusActive: "Active",
|
|
389
|
+
transportStatusExpired: "Token expired",
|
|
390
|
+
transportStatusNeedsReauth: "Sign in again",
|
|
391
|
+
transportStatusSignedOut: "Not signed in",
|
|
392
|
+
transportRemoveTitle: "Remove the {name} transport account?",
|
|
393
|
+
transportRemoveBody: "This removes the grant stored for this transport route from the credential store and cancels a sign-in still in progress; the route does not recreate it. Sign in again to restore service."
|
|
337
394
|
};
|
|
338
395
|
var LOCALE_DICTS = { zh, en };
|
|
339
396
|
|
|
@@ -351,6 +408,13 @@ var CSS = `
|
|
|
351
408
|
.km-stat{display:flex;flex-direction:column;gap:2px;padding:11px 14px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1)}
|
|
352
409
|
.km-stat strong{font-size:19px;line-height:1.15;font-weight:650;font-variant-numeric:tabular-nums}
|
|
353
410
|
.km-stat span{color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
411
|
+
.km-filters{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 12px}
|
|
412
|
+
.km-chip{display:inline-flex;align-items:center;gap:6px;padding:5px 11px;border:1px solid var(--dsw-alias-border-l1);border-radius:999px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font:inherit;font-size:12.5px;cursor:pointer}
|
|
413
|
+
.km-chip:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
|
|
414
|
+
.km-chip:focus-visible{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 2px color-mix(in srgb,var(--dsw-alias-state-business-primary) 20%,transparent)}
|
|
415
|
+
.km-chip-on{border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 55%,var(--dsw-alias-border-l1));background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary)}
|
|
416
|
+
.km-chip-count{font-size:11.5px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}
|
|
417
|
+
.km-chip-on .km-chip-count{color:inherit}
|
|
354
418
|
.km-search{position:relative;display:flex;align-items:center;margin:0 0 20px}
|
|
355
419
|
.km-search-icon{position:absolute;left:12px;color:var(--dsw-alias-label-secondary);pointer-events:none}
|
|
356
420
|
.km-search-input{width:100%;box-sizing:border-box;padding:10px 38px 10px 36px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;outline:none}
|
|
@@ -584,6 +648,18 @@ function messageOf(error) {
|
|
|
584
648
|
if (error instanceof Error && error.message.length > 0) return error.message;
|
|
585
649
|
return String(error);
|
|
586
650
|
}
|
|
651
|
+
var TEST_KEY_DEADLINE_MS = 3e4;
|
|
652
|
+
async function withDeadline(work, ms, label) {
|
|
653
|
+
let timer;
|
|
654
|
+
const deadline = new Promise((_, reject) => {
|
|
655
|
+
timer = setTimeout(() => reject(new Error(`${label} did not answer within ${ms / 1e3}s`)), ms);
|
|
656
|
+
});
|
|
657
|
+
try {
|
|
658
|
+
return await Promise.race([work, deadline]);
|
|
659
|
+
} finally {
|
|
660
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
587
663
|
function createStore(ctx) {
|
|
588
664
|
let snapshot = {
|
|
589
665
|
status: "idle",
|
|
@@ -595,6 +671,10 @@ function createStore(ctx) {
|
|
|
595
671
|
let loadSequence = 0;
|
|
596
672
|
let mutationTail = Promise.resolve();
|
|
597
673
|
let queuedMutations = 0;
|
|
674
|
+
let activeTests = 0;
|
|
675
|
+
let testGeneration = 0;
|
|
676
|
+
let mutationGeneration = 0;
|
|
677
|
+
let stateDropped = false;
|
|
598
678
|
const listeners = /* @__PURE__ */ new Set();
|
|
599
679
|
const emit = () => {
|
|
600
680
|
for (const listener of [...listeners]) {
|
|
@@ -614,6 +694,7 @@ function createStore(ctx) {
|
|
|
614
694
|
update({ busy: method, error: null });
|
|
615
695
|
const run = async () => {
|
|
616
696
|
++loadSequence;
|
|
697
|
+
mutationGeneration = testGeneration;
|
|
617
698
|
try {
|
|
618
699
|
const result = await rpc(method, args);
|
|
619
700
|
if (!result.ok) throw new Error(result.error.message);
|
|
@@ -627,6 +708,7 @@ function createStore(ctx) {
|
|
|
627
708
|
return { ok: false, error: message };
|
|
628
709
|
} finally {
|
|
629
710
|
queuedMutations -= 1;
|
|
711
|
+
if (queuedMutations === 0) refreshDroppedState();
|
|
630
712
|
}
|
|
631
713
|
};
|
|
632
714
|
const next = mutationTail.then(run, run);
|
|
@@ -635,10 +717,21 @@ function createStore(ctx) {
|
|
|
635
717
|
};
|
|
636
718
|
const stateOutcome = (value) => {
|
|
637
719
|
const state = value;
|
|
720
|
+
const borrowed = activeTests > 0 || testGeneration !== mutationGeneration;
|
|
721
|
+
if (borrowed) {
|
|
722
|
+
if (queuedMutations === 1) update({ busy: null, error: null });
|
|
723
|
+
else update({ error: null });
|
|
724
|
+
stateDropped = true;
|
|
725
|
+
return { ok: true, value: state };
|
|
726
|
+
}
|
|
638
727
|
if (queuedMutations === 1) update({ status: "ready", state, now: Date.now(), busy: null, error: null });
|
|
639
728
|
else update({ status: "ready", state, now: Date.now(), error: null });
|
|
640
729
|
return { ok: true, value: state };
|
|
641
730
|
};
|
|
731
|
+
const refreshDroppedState = () => {
|
|
732
|
+
if (!stateDropped || activeTests > 0 || snapshot.busy !== null) return;
|
|
733
|
+
void store.load({ keepError: true });
|
|
734
|
+
};
|
|
642
735
|
const rpcRead = async (method, args) => {
|
|
643
736
|
try {
|
|
644
737
|
return await rpc(method, args);
|
|
@@ -649,7 +742,14 @@ function createStore(ctx) {
|
|
|
649
742
|
const attemptOutcome = (value) => {
|
|
650
743
|
return { ok: true, value: value ?? null };
|
|
651
744
|
};
|
|
652
|
-
|
|
745
|
+
const testOutcome = (value) => {
|
|
746
|
+
const result = value ?? {};
|
|
747
|
+
return {
|
|
748
|
+
ok: true,
|
|
749
|
+
value: { ok: result.ok === true, latencyMs: result.latencyMs ?? 0, ...result.error === void 0 ? {} : { error: result.error } }
|
|
750
|
+
};
|
|
751
|
+
};
|
|
752
|
+
const store = {
|
|
653
753
|
getSnapshot: () => snapshot,
|
|
654
754
|
subscribe(listener) {
|
|
655
755
|
listeners.add(listener);
|
|
@@ -658,12 +758,18 @@ function createStore(ctx) {
|
|
|
658
758
|
async load(options = {}) {
|
|
659
759
|
if (snapshot.busy !== null) return;
|
|
660
760
|
const request = ++loadSequence;
|
|
761
|
+
const generation = testGeneration;
|
|
661
762
|
update({ busy: "load", error: options.keepError === true ? snapshot.error : null });
|
|
662
763
|
try {
|
|
663
764
|
const result = await rpc("getState", {});
|
|
664
765
|
if (request !== loadSequence) return;
|
|
766
|
+
if (activeTests > 0 || testGeneration !== generation) {
|
|
767
|
+
update({ busy: null });
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
665
770
|
if (!result.ok) throw new Error(result.error.message);
|
|
666
771
|
update({ status: "ready", state: result.value, now: Date.now(), busy: null, error: null });
|
|
772
|
+
stateDropped = false;
|
|
667
773
|
} catch (error) {
|
|
668
774
|
if (request !== loadSequence) return;
|
|
669
775
|
update({ status: snapshot.state !== null ? snapshot.status : "error", busy: null, error: messageOf(error) });
|
|
@@ -694,6 +800,36 @@ function createStore(ctx) {
|
|
|
694
800
|
removeAccount(provider, accountId) {
|
|
695
801
|
return send("removeAccount", { provider, accountId }, stateOutcome);
|
|
696
802
|
},
|
|
803
|
+
async testKey(provider, keyId) {
|
|
804
|
+
activeTests += 1;
|
|
805
|
+
++testGeneration;
|
|
806
|
+
const request = rpc("testKey", { provider, keyId });
|
|
807
|
+
let released = false;
|
|
808
|
+
const release = () => {
|
|
809
|
+
if (released) return;
|
|
810
|
+
released = true;
|
|
811
|
+
activeTests -= 1;
|
|
812
|
+
testGeneration += 1;
|
|
813
|
+
refreshDroppedState();
|
|
814
|
+
};
|
|
815
|
+
void request.then(release, release);
|
|
816
|
+
try {
|
|
817
|
+
const result = await withDeadline(request, TEST_KEY_DEADLINE_MS, "testKey");
|
|
818
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
819
|
+
return testOutcome(result.value);
|
|
820
|
+
} catch (error) {
|
|
821
|
+
return { ok: false, error: messageOf(error) };
|
|
822
|
+
}
|
|
823
|
+
},
|
|
824
|
+
beginTransportLogin(route) {
|
|
825
|
+
return send("beginTransportLogin", { provider: route }, attemptOutcome);
|
|
826
|
+
},
|
|
827
|
+
activateTransportAccount(route) {
|
|
828
|
+
return send("activateTransportAccount", { provider: route }, stateOutcome);
|
|
829
|
+
},
|
|
830
|
+
removeTransportAccount(route) {
|
|
831
|
+
return send("removeTransportAccount", { provider: route }, stateOutcome);
|
|
832
|
+
},
|
|
697
833
|
tick() {
|
|
698
834
|
update({ now: Date.now() });
|
|
699
835
|
if (snapshot.status === "ready" && snapshot.busy === null) void this.load({ keepError: true });
|
|
@@ -702,6 +838,7 @@ function createStore(ctx) {
|
|
|
702
838
|
update({ error: null });
|
|
703
839
|
}
|
|
704
840
|
};
|
|
841
|
+
return store;
|
|
705
842
|
}
|
|
706
843
|
|
|
707
844
|
// src/client/ui.ts
|
|
@@ -765,6 +902,37 @@ function providerInitial(provider) {
|
|
|
765
902
|
function oauthCapable(provider) {
|
|
766
903
|
return provider.nativeAuth?.oauth === true && provider.accountSupported !== false;
|
|
767
904
|
}
|
|
905
|
+
function originOf(provider) {
|
|
906
|
+
return provider.origin ?? (provider.extra ? "plugin" : "dsh");
|
|
907
|
+
}
|
|
908
|
+
var FILTER_CHIPS = [
|
|
909
|
+
{ value: "all", label: "filterAll" },
|
|
910
|
+
{ value: "builtin", label: "filterBuiltin" },
|
|
911
|
+
{ value: "plugin", label: "filterPlugin" },
|
|
912
|
+
{ value: "oauth", label: "filterOauth" },
|
|
913
|
+
{ value: "transport", label: "filterTransport" }
|
|
914
|
+
];
|
|
915
|
+
function matchesFilter(provider, filter) {
|
|
916
|
+
switch (filter) {
|
|
917
|
+
case "transport":
|
|
918
|
+
return provider.transport === true;
|
|
919
|
+
case "builtin":
|
|
920
|
+
return provider.transport !== true && originOf(provider) === "dsh";
|
|
921
|
+
case "plugin":
|
|
922
|
+
return provider.transport !== true && originOf(provider) === "plugin";
|
|
923
|
+
case "oauth":
|
|
924
|
+
return provider.transport !== true && oauthCapable(provider);
|
|
925
|
+
default:
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
function orphanTransportAccounts(providers, transportAccounts) {
|
|
930
|
+
return transportAccounts.filter((account) => !providers.some((provider) => provider.transport === true && provider.provider === account.id));
|
|
931
|
+
}
|
|
932
|
+
function managedEntryCount(providers, transportAccounts) {
|
|
933
|
+
const cards = providers.filter((provider) => provider.managed || provider.transport === true).length;
|
|
934
|
+
return cards + orphanTransportAccounts(providers, transportAccounts).length;
|
|
935
|
+
}
|
|
768
936
|
function accountIdentityOf(key, provider) {
|
|
769
937
|
const oauth = key.oauth;
|
|
770
938
|
if (oauth !== null && oauth !== void 0) {
|
|
@@ -791,6 +959,30 @@ function statusKey(status) {
|
|
|
791
959
|
return "keyStatusUnmanaged";
|
|
792
960
|
}
|
|
793
961
|
}
|
|
962
|
+
function transportStatusKey(status) {
|
|
963
|
+
switch (status) {
|
|
964
|
+
case "active":
|
|
965
|
+
return "transportStatusActive";
|
|
966
|
+
case "expired":
|
|
967
|
+
return "transportStatusExpired";
|
|
968
|
+
case "needs-reauth":
|
|
969
|
+
return "transportStatusNeedsReauth";
|
|
970
|
+
default:
|
|
971
|
+
return "transportStatusSignedOut";
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function transportStatusTone(status) {
|
|
975
|
+
switch (status) {
|
|
976
|
+
case "active":
|
|
977
|
+
return "success";
|
|
978
|
+
case "expired":
|
|
979
|
+
return "warn";
|
|
980
|
+
case "needs-reauth":
|
|
981
|
+
return "error";
|
|
982
|
+
default:
|
|
983
|
+
return "idle";
|
|
984
|
+
}
|
|
985
|
+
}
|
|
794
986
|
function statusTone(status) {
|
|
795
987
|
switch (status) {
|
|
796
988
|
case "active":
|
|
@@ -836,6 +1028,7 @@ function Icon({ name, size = 16, className }) {
|
|
|
836
1028
|
if (name === "warning") return h("svg", common, h("path", { d: "M12 3l9.5 17h-19L12 3zM12 9v5M12 17.2v.1" }));
|
|
837
1029
|
if (name === "shield") return h("svg", common, h("path", { d: "M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6l8-3zM9 12l2 2 4-4" }));
|
|
838
1030
|
if (name === "user") return h("svg", common, h("circle", { cx: 12, cy: 8, r: 3.6 }), h("path", { d: "M5 20a7 7 0 0114 0" }));
|
|
1031
|
+
if (name === "activity") return h("svg", common, h("path", { d: "M3 12h4l2.5-7 4 14 2.5-7h5" }));
|
|
839
1032
|
return h("svg", common);
|
|
840
1033
|
}
|
|
841
1034
|
function Button({ variant = "secondary", size = "normal", type = "button", disabled, onClick, children, title }) {
|
|
@@ -1039,6 +1232,35 @@ function KeyForm({ provider, onDone, autoFocus = false }) {
|
|
|
1039
1232
|
)
|
|
1040
1233
|
);
|
|
1041
1234
|
}
|
|
1235
|
+
var ROW_TEST_STYLE = { gridColumn: "1 / -1", marginTop: "-2px" };
|
|
1236
|
+
var ROW_TEST_DEADLINE_MS = 3e4;
|
|
1237
|
+
async function requestRowTest(store, provider, keyId) {
|
|
1238
|
+
let timer;
|
|
1239
|
+
const deadline = new Promise((resolve) => {
|
|
1240
|
+
timer = setTimeout(() => resolve({ kind: "failed", timedOut: true }), ROW_TEST_DEADLINE_MS);
|
|
1241
|
+
});
|
|
1242
|
+
const answer = store.testKey(provider, keyId).then((outcome) => {
|
|
1243
|
+
if (!outcome.ok) return { kind: "failed", error: outcome.error };
|
|
1244
|
+
if (outcome.value.ok) return { kind: "passed", latencyMs: outcome.value.latencyMs };
|
|
1245
|
+
return { kind: "failed", ...outcome.value.error === void 0 ? {} : { error: outcome.value.error } };
|
|
1246
|
+
});
|
|
1247
|
+
try {
|
|
1248
|
+
return await Promise.race([answer, deadline]);
|
|
1249
|
+
} finally {
|
|
1250
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
function testLine(test, t) {
|
|
1254
|
+
if (test === null) return null;
|
|
1255
|
+
if (test.kind === "running") {
|
|
1256
|
+
return h("div", { className: "km-key-time km-muted", role: "status", style: ROW_TEST_STYLE }, t("testRunning"));
|
|
1257
|
+
}
|
|
1258
|
+
if (test.kind === "passed") {
|
|
1259
|
+
return h("div", { className: "km-key-time", role: "status", style: ROW_TEST_STYLE }, t("testPassed", { ms: test.latencyMs ?? 0 }));
|
|
1260
|
+
}
|
|
1261
|
+
const detail = test.error !== void 0 && test.error.length > 0 ? test.error : test.timedOut === true ? t("testTimedOut") : t("testFailedUnknown");
|
|
1262
|
+
return h("div", { className: "km-key-time km-warn", role: "status", style: ROW_TEST_STYLE }, t("testFailed", { error: detail }));
|
|
1263
|
+
}
|
|
1042
1264
|
function KeyRow({ provider, keyRecord, now }) {
|
|
1043
1265
|
const key = keyRecord;
|
|
1044
1266
|
const runtime = useRuntime();
|
|
@@ -1050,6 +1272,7 @@ function KeyRow({ provider, keyRecord, now }) {
|
|
|
1050
1272
|
const [confirm, setConfirm] = import_react2.default.useState(false);
|
|
1051
1273
|
const [busy, setBusy] = import_react2.default.useState(false);
|
|
1052
1274
|
const [error, setError] = import_react2.default.useState(null);
|
|
1275
|
+
const [test, setTest] = import_react2.default.useState(null);
|
|
1053
1276
|
const rootRef = import_react2.default.useRef(null);
|
|
1054
1277
|
import_react2.default.useEffect(() => {
|
|
1055
1278
|
setAlias(key.alias);
|
|
@@ -1094,6 +1317,10 @@ function KeyRow({ provider, keyRecord, now }) {
|
|
|
1094
1317
|
if (!result.ok) setError(result.error);
|
|
1095
1318
|
else setConfirm(false);
|
|
1096
1319
|
}
|
|
1320
|
+
async function runTest() {
|
|
1321
|
+
setTest({ kind: "running" });
|
|
1322
|
+
setTest(await requestRowTest(runtime.store, provider.provider, key.id));
|
|
1323
|
+
}
|
|
1097
1324
|
const cold = key.status === "cooling" && key.cooldownUntil > now;
|
|
1098
1325
|
const meta = key.status === "missing" ? t("keyStatusMissing") : key.totalUses > 0 ? t("uses", { n: key.totalUses }) : "";
|
|
1099
1326
|
return h(
|
|
@@ -1169,6 +1396,16 @@ function KeyRow({ provider, keyRecord, now }) {
|
|
|
1169
1396
|
setMenu(false);
|
|
1170
1397
|
setEditing(true);
|
|
1171
1398
|
} }, h(Icon, { name: "key", size: 15 }), t("rename")),
|
|
1399
|
+
h("button", {
|
|
1400
|
+
type: "button",
|
|
1401
|
+
className: "km-menu-item",
|
|
1402
|
+
role: "menuitem",
|
|
1403
|
+
onClick: () => {
|
|
1404
|
+
setMenu(false);
|
|
1405
|
+
void runTest();
|
|
1406
|
+
},
|
|
1407
|
+
disabled: busy || test?.kind === "running"
|
|
1408
|
+
}, h(Icon, { name: "activity", size: 15 }), t("testKey")),
|
|
1172
1409
|
h("button", {
|
|
1173
1410
|
type: "button",
|
|
1174
1411
|
className: "km-menu-item",
|
|
@@ -1187,6 +1424,7 @@ function KeyRow({ provider, keyRecord, now }) {
|
|
|
1187
1424
|
) : null
|
|
1188
1425
|
),
|
|
1189
1426
|
error !== null ? h("div", { className: "km-key-error", role: "alert" }, error) : null,
|
|
1427
|
+
testLine(test, t),
|
|
1190
1428
|
confirm ? h(Modal, {
|
|
1191
1429
|
title: t("deleteTitle", { alias: key.alias }),
|
|
1192
1430
|
onClose: () => setConfirm(false),
|
|
@@ -1208,6 +1446,7 @@ function AccountRow({ provider, keyRecord, now, onReauthenticate }) {
|
|
|
1208
1446
|
const [confirm, setConfirm] = import_react2.default.useState(false);
|
|
1209
1447
|
const [busy, setBusy] = import_react2.default.useState(false);
|
|
1210
1448
|
const [error, setError] = import_react2.default.useState(null);
|
|
1449
|
+
const [test, setTest] = import_react2.default.useState(null);
|
|
1211
1450
|
const rootRef = import_react2.default.useRef(null);
|
|
1212
1451
|
import_react2.default.useEffect(() => {
|
|
1213
1452
|
if (!menu) return void 0;
|
|
@@ -1239,6 +1478,10 @@ function AccountRow({ provider, keyRecord, now, onReauthenticate }) {
|
|
|
1239
1478
|
if (!result.ok) setError(result.error);
|
|
1240
1479
|
else setConfirm(false);
|
|
1241
1480
|
}
|
|
1481
|
+
async function runTest() {
|
|
1482
|
+
setTest({ kind: "running" });
|
|
1483
|
+
setTest(await requestRowTest(runtime.store, provider.provider, key.id));
|
|
1484
|
+
}
|
|
1242
1485
|
const cold = key.status === "cooling" && key.cooldownUntil > now;
|
|
1243
1486
|
const metaParts = [
|
|
1244
1487
|
key.totalUses > 0 ? t("uses", { n: key.totalUses }) : null,
|
|
@@ -1313,6 +1556,16 @@ function AccountRow({ provider, keyRecord, now, onReauthenticate }) {
|
|
|
1313
1556
|
// active again is a no-op, so the entry stays disabled.
|
|
1314
1557
|
disabled: busy || key.projected === true
|
|
1315
1558
|
}, h(Icon, { name: "check", size: 15 }), t("accountActivate")),
|
|
1559
|
+
h("button", {
|
|
1560
|
+
type: "button",
|
|
1561
|
+
className: "km-menu-item",
|
|
1562
|
+
role: "menuitem",
|
|
1563
|
+
onClick: () => {
|
|
1564
|
+
setMenu(false);
|
|
1565
|
+
void runTest();
|
|
1566
|
+
},
|
|
1567
|
+
disabled: busy || test?.kind === "running"
|
|
1568
|
+
}, h(Icon, { name: "activity", size: 15 }), t("testKey")),
|
|
1316
1569
|
h("button", {
|
|
1317
1570
|
type: "button",
|
|
1318
1571
|
className: "km-menu-item",
|
|
@@ -1331,6 +1584,7 @@ function AccountRow({ provider, keyRecord, now, onReauthenticate }) {
|
|
|
1331
1584
|
) : null
|
|
1332
1585
|
),
|
|
1333
1586
|
error !== null ? h("div", { className: "km-key-error", role: "alert" }, error) : null,
|
|
1587
|
+
testLine(test, t),
|
|
1334
1588
|
confirm ? h(Modal, {
|
|
1335
1589
|
// SF4: account-specific copy — the grant removal consequence, not a
|
|
1336
1590
|
// key-deletion/provider-release promise. Step 8: the key-pool
|
|
@@ -1346,7 +1600,115 @@ function AccountRow({ provider, keyRecord, now, onReauthenticate }) {
|
|
|
1346
1600
|
}, h("p", { className: "km-modal-copy" }, t(provider.supported === true ? "deleteAccountBody" : "deleteAccountBodyNoKeys"))) : null
|
|
1347
1601
|
);
|
|
1348
1602
|
}
|
|
1349
|
-
function
|
|
1603
|
+
function TransportAccountRow({ account }) {
|
|
1604
|
+
const runtime = useRuntime();
|
|
1605
|
+
const t = useT();
|
|
1606
|
+
const locale = useLocaleId();
|
|
1607
|
+
const [busy, setBusy] = import_react2.default.useState(false);
|
|
1608
|
+
const [error, setError] = import_react2.default.useState(null);
|
|
1609
|
+
const [confirm, setConfirm] = import_react2.default.useState(false);
|
|
1610
|
+
const [login, setLogin] = import_react2.default.useState(null);
|
|
1611
|
+
async function signIn() {
|
|
1612
|
+
setBusy(true);
|
|
1613
|
+
setError(null);
|
|
1614
|
+
const result = await runtime.store.beginTransportLogin(account.id);
|
|
1615
|
+
setBusy(false);
|
|
1616
|
+
if (!result.ok) {
|
|
1617
|
+
setError(result.error);
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
if (result.value !== null) setLogin({ attemptId: result.value.id });
|
|
1621
|
+
}
|
|
1622
|
+
async function remove() {
|
|
1623
|
+
setBusy(true);
|
|
1624
|
+
setError(null);
|
|
1625
|
+
const result = await runtime.store.removeTransportAccount(account.id);
|
|
1626
|
+
setBusy(false);
|
|
1627
|
+
if (!result.ok) setError(result.error);
|
|
1628
|
+
else setConfirm(false);
|
|
1629
|
+
}
|
|
1630
|
+
const identity = account.email ?? account.accountId ?? account.displayName;
|
|
1631
|
+
const tone = transportStatusTone(account.status);
|
|
1632
|
+
return h(
|
|
1633
|
+
"div",
|
|
1634
|
+
{ className: "km-account-row km-transport-row" },
|
|
1635
|
+
h(
|
|
1636
|
+
"div",
|
|
1637
|
+
{ className: "km-key-identity" },
|
|
1638
|
+
h(
|
|
1639
|
+
"div",
|
|
1640
|
+
{ className: "km-key-alias-line" },
|
|
1641
|
+
h("span", { className: "km-account-glyph", "aria-hidden": true }, h(Icon, { name: "user", size: 13 })),
|
|
1642
|
+
h("span", { className: "km-key-alias" }, identity),
|
|
1643
|
+
h("span", { className: "km-tag km-tag-accent" }, t("transportTag"))
|
|
1644
|
+
),
|
|
1645
|
+
// The credential scope, never the payload: the record is the account.
|
|
1646
|
+
h(
|
|
1647
|
+
"div",
|
|
1648
|
+
{ className: "km-key-sub" },
|
|
1649
|
+
h("span", { className: "km-mono km-muted" }, account.scope)
|
|
1650
|
+
)
|
|
1651
|
+
),
|
|
1652
|
+
h(
|
|
1653
|
+
"div",
|
|
1654
|
+
{ className: "km-key-status" },
|
|
1655
|
+
h(
|
|
1656
|
+
"span",
|
|
1657
|
+
{ className: cx("km-status", `km-status-${tone}`) },
|
|
1658
|
+
h("span", { className: cx("km-dot", `km-dot-${tone}`) }),
|
|
1659
|
+
h("span", null, t(transportStatusKey(account.status)))
|
|
1660
|
+
)
|
|
1661
|
+
),
|
|
1662
|
+
// The expiry column stays a clock, not a countdown: the route's adapter
|
|
1663
|
+
// renews the grant itself, so "expired" here means "renews on next use".
|
|
1664
|
+
h(
|
|
1665
|
+
"div",
|
|
1666
|
+
{ className: "km-key-last" },
|
|
1667
|
+
account.expiresAt === void 0 ? h("div", { className: "km-key-time km-muted" }, t("never")) : h("div", { className: "km-key-time" }, t("transportExpires", { time: formatClock(account.expiresAt, locale, t) }))
|
|
1668
|
+
),
|
|
1669
|
+
h(
|
|
1670
|
+
"div",
|
|
1671
|
+
{ className: "km-key-actions" },
|
|
1672
|
+
h(
|
|
1673
|
+
Button,
|
|
1674
|
+
{
|
|
1675
|
+
size: "small",
|
|
1676
|
+
disabled: busy,
|
|
1677
|
+
onClick: signIn,
|
|
1678
|
+
title: account.active ? t("accountReauth") : t("transportSignIn")
|
|
1679
|
+
},
|
|
1680
|
+
busy ? h(Icon, { name: "refresh", size: 14, className: "km-spin" }) : h(Icon, { name: "refresh", size: 14 }),
|
|
1681
|
+
t(account.active ? "accountReauth" : "transportSignIn")
|
|
1682
|
+
),
|
|
1683
|
+
// No "Set active" entry: activation would write nothing (the stored grant
|
|
1684
|
+
// already IS the route's live credential), so the host endpoint stays for
|
|
1685
|
+
// API symmetry with the account surface and the row omits a no-effect
|
|
1686
|
+
// control.
|
|
1687
|
+
h(Button, {
|
|
1688
|
+
size: "small",
|
|
1689
|
+
variant: "danger",
|
|
1690
|
+
disabled: busy,
|
|
1691
|
+
onClick: () => setConfirm(true)
|
|
1692
|
+
}, h(Icon, { name: "trash", size: 14 }), t("delete"))
|
|
1693
|
+
),
|
|
1694
|
+
error !== null ? h("div", { className: "km-key-error", role: "alert" }, error) : null,
|
|
1695
|
+
confirm ? h(Modal, {
|
|
1696
|
+
title: t("transportRemoveTitle", { name: account.displayName }),
|
|
1697
|
+
onClose: () => setConfirm(false),
|
|
1698
|
+
busy,
|
|
1699
|
+
footer: [
|
|
1700
|
+
h(Button, { key: "cancel", onClick: () => setConfirm(false), disabled: busy }, t("cancel")),
|
|
1701
|
+
h(Button, { key: "delete", variant: "danger", onClick: remove, disabled: busy }, busy ? t("deleting") : t("deleteConfirm"))
|
|
1702
|
+
]
|
|
1703
|
+
}, h("p", { className: "km-modal-copy" }, t("transportRemoveBody"))) : null,
|
|
1704
|
+
login !== null ? h(LoginDialog, {
|
|
1705
|
+
provider: { displayName: account.displayName },
|
|
1706
|
+
attemptId: login.attemptId,
|
|
1707
|
+
onClose: () => setLogin(null)
|
|
1708
|
+
}) : null
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
function ProviderCard({ provider, expanded, onToggle, now, transportAccount }) {
|
|
1350
1712
|
const runtime = useRuntime();
|
|
1351
1713
|
const t = useT();
|
|
1352
1714
|
const [adding, setAdding] = import_react2.default.useState(false);
|
|
@@ -1361,11 +1723,15 @@ function ProviderCard({ provider, expanded, onToggle, now }) {
|
|
|
1361
1723
|
const keyCount = provider.keyCount;
|
|
1362
1724
|
const warning = provider.warning;
|
|
1363
1725
|
const isExtra = provider.extra;
|
|
1364
|
-
const
|
|
1365
|
-
const
|
|
1726
|
+
const transport = provider.transport === true;
|
|
1727
|
+
const origin = originOf(provider);
|
|
1728
|
+
const originLabel = transport || origin === "plugin" ? t("pluginBuiltin") : origin === "dsh" ? t("dshBuiltin") : t("customProvider");
|
|
1366
1729
|
const providerMeta = [
|
|
1367
1730
|
provider.provider,
|
|
1368
|
-
|
|
1731
|
+
// `active`/`configured` are read off the route's profile or the host's
|
|
1732
|
+
// provider list, neither of which describes a transport route: its state
|
|
1733
|
+
// is the account row it carries (signed out when the host lists none).
|
|
1734
|
+
transport ? t(transportStatusKey(transportAccount?.status ?? "signed-out")) : provider.active ? t("active") : provider.configured ? t("configured") : t("notConfigured"),
|
|
1369
1735
|
provider.api ?? null
|
|
1370
1736
|
].filter((value) => Boolean(value));
|
|
1371
1737
|
async function adopt() {
|
|
@@ -1435,8 +1801,9 @@ function ProviderCard({ provider, expanded, onToggle, now }) {
|
|
|
1435
1801
|
{ className: "km-provider-title-line" },
|
|
1436
1802
|
h("span", { className: "km-provider-name" }, provider.displayName || provider.provider),
|
|
1437
1803
|
h("span", { className: "km-tag" }, originLabel),
|
|
1804
|
+
transport ? h("span", { className: "km-tag km-tag-accent" }, t("transportTag")) : null,
|
|
1438
1805
|
managed ? h("span", { className: "km-tag km-tag-accent" }, t("managedBadge")) : null,
|
|
1439
|
-
!managed && !provider.supported && !accountAdoptable ? h("span", { className: "km-tag km-tag-warn" }, t("unsupported")) : null
|
|
1806
|
+
!transport && !managed && !provider.supported && !accountAdoptable ? h("span", { className: "km-tag km-tag-warn" }, t("unsupported")) : null
|
|
1440
1807
|
),
|
|
1441
1808
|
h(
|
|
1442
1809
|
"span",
|
|
@@ -1450,7 +1817,14 @@ function ProviderCard({ provider, expanded, onToggle, now }) {
|
|
|
1450
1817
|
h(
|
|
1451
1818
|
"span",
|
|
1452
1819
|
{ className: "km-provider-side" },
|
|
1453
|
-
|
|
1820
|
+
// I-000120 / D8: requests this route served without an attributed
|
|
1821
|
+
// account. Rendered only above zero, and the visible label carries the
|
|
1822
|
+
// count so it is the accessible name; the hint stays a description.
|
|
1823
|
+
(provider.unattributedRequests ?? 0) > 0 ? h("span", {
|
|
1824
|
+
className: "km-tag km-tag-warn km-unattributed",
|
|
1825
|
+
title: t("unattributedHint")
|
|
1826
|
+
}, t("unattributed", { n: provider.unattributedRequests ?? 0 })) : null,
|
|
1827
|
+
transport ? null : managed ? h("span", { className: "km-count" }, countText(t, keyCount, "providerKeyOne", "providerKeys")) : h("span", { className: "km-count km-muted" }, adoptable ? t("unmanaged") : t("unsupported")),
|
|
1454
1828
|
managed ? h(StatusPill, { status: provider.status, t }) : null,
|
|
1455
1829
|
h(Icon, { name: "chevron", size: 16, className: cx("km-chevron", expanded && "km-chevron-open") })
|
|
1456
1830
|
)
|
|
@@ -1460,7 +1834,9 @@ function ProviderCard({ provider, expanded, onToggle, now }) {
|
|
|
1460
1834
|
{ className: "km-provider-body" },
|
|
1461
1835
|
error !== null ? h("div", { className: "km-inline-error", role: "alert" }, error) : null,
|
|
1462
1836
|
warning !== void 0 || broken ? h(Notice, { tone: "warn", title: t("warning") }, broken ? t("bindingBroken") : warning) : null,
|
|
1463
|
-
|
|
1837
|
+
// A transport route has no adopt step and no key pool: its whole
|
|
1838
|
+
// credential surface is the one account row its own plugin serves.
|
|
1839
|
+
transport ? transportAccount !== void 0 ? h(TransportAccountRow, { account: transportAccount }) : h("div", { className: "km-muted" }, t("transportProviderHint")) : !managed ? h(
|
|
1464
1840
|
"div",
|
|
1465
1841
|
{ className: "km-adopt" },
|
|
1466
1842
|
h(
|
|
@@ -1876,6 +2252,7 @@ function KeysManagerBody() {
|
|
|
1876
2252
|
const state = snapshot.state;
|
|
1877
2253
|
const now = snapshot.now;
|
|
1878
2254
|
const [query, setQuery] = import_react2.default.useState("");
|
|
2255
|
+
const [filter, setFilter] = import_react2.default.useState("all");
|
|
1879
2256
|
const [showAllAvailable, setShowAllAvailable] = import_react2.default.useState(false);
|
|
1880
2257
|
const [expanded, setExpanded] = import_react2.default.useState(() => /* @__PURE__ */ new Set());
|
|
1881
2258
|
const loadedRef = import_react2.default.useRef(false);
|
|
@@ -1885,6 +2262,7 @@ function KeysManagerBody() {
|
|
|
1885
2262
|
if (snapshot.status === "idle") void runtime.store.load();
|
|
1886
2263
|
}, [runtime, snapshot.status]);
|
|
1887
2264
|
const providers = state?.providers ?? [];
|
|
2265
|
+
const transportAccounts = state?.transportAccounts ?? [];
|
|
1888
2266
|
const normalizedQuery = query.trim().toLowerCase();
|
|
1889
2267
|
const matches = (provider) => {
|
|
1890
2268
|
if (normalizedQuery.length === 0) return true;
|
|
@@ -1898,9 +2276,23 @@ function KeysManagerBody() {
|
|
|
1898
2276
|
].filter((value) => Boolean(value)).join(" ").toLowerCase();
|
|
1899
2277
|
return haystack.includes(normalizedQuery);
|
|
1900
2278
|
};
|
|
1901
|
-
const managed = providers.filter((provider) => provider.managed && matches(provider));
|
|
1902
|
-
const available = providers.filter((provider) => !provider.managed && matches(provider));
|
|
2279
|
+
const managed = providers.filter((provider) => (provider.managed || provider.transport === true) && matchesFilter(provider, filter) && matches(provider));
|
|
2280
|
+
const available = providers.filter((provider) => !provider.managed && provider.transport !== true && matchesFilter(provider, filter) && matches(provider));
|
|
1903
2281
|
const visibleAvailable = showAllAvailable || normalizedQuery.length > 0 ? available : available.slice(0, 12);
|
|
2282
|
+
const matchesAccount = (account) => {
|
|
2283
|
+
if (normalizedQuery.length === 0) return true;
|
|
2284
|
+
const haystack = [account.id, account.scope, account.displayName].join(" ").toLowerCase();
|
|
2285
|
+
return haystack.includes(normalizedQuery);
|
|
2286
|
+
};
|
|
2287
|
+
const orphanAccounts = orphanTransportAccounts(providers, transportAccounts).filter(matchesAccount);
|
|
2288
|
+
const visibleOrphans = filter === "all" || filter === "transport" ? orphanAccounts : [];
|
|
2289
|
+
const filterCount = (value) => {
|
|
2290
|
+
const searchable = providers.filter(matches);
|
|
2291
|
+
if (value === "all") return searchable.length + orphanAccounts.length;
|
|
2292
|
+
if (value === "transport") return searchable.filter((provider) => provider.transport === true).length + orphanAccounts.length;
|
|
2293
|
+
return searchable.filter((provider) => matchesFilter(provider, value)).length;
|
|
2294
|
+
};
|
|
2295
|
+
const managedHiddenByChip = filter !== "all" && providers.some((provider) => provider.managed || provider.transport === true);
|
|
1904
2296
|
function toggle(provider) {
|
|
1905
2297
|
setExpanded((current) => {
|
|
1906
2298
|
const next = new Set(current);
|
|
@@ -1939,7 +2331,7 @@ function KeysManagerBody() {
|
|
|
1939
2331
|
h(
|
|
1940
2332
|
"div",
|
|
1941
2333
|
{ className: "km-summary" },
|
|
1942
|
-
h("div", { className: "km-stat" }, h("strong", null, loading ? "\u2014" :
|
|
2334
|
+
h("div", { className: "km-stat" }, h("strong", null, loading ? "\u2014" : managedEntryCount(providers, transportAccounts)), h("span", null, t("managed"))),
|
|
1943
2335
|
h("div", { className: "km-stat" }, h("strong", null, loading ? "\u2014" : state?.keyCount ?? 0), h("span", null, t("keys"))),
|
|
1944
2336
|
h("div", { className: "km-stat" }, h("strong", null, loading ? "\u2014" : state?.healthyKeyCount ?? 0), h("span", null, t("healthy"))),
|
|
1945
2337
|
h("div", { className: "km-stat" }, h("strong", null, loading ? "\u2014" : state?.coolingKeyCount ?? 0), h("span", null, t("cooling")))
|
|
@@ -1956,6 +2348,26 @@ function KeysManagerBody() {
|
|
|
1956
2348
|
}, h(Icon, { name: "refresh", size: 14 }), t("refresh"))
|
|
1957
2349
|
)
|
|
1958
2350
|
),
|
|
2351
|
+
// Quick filter: single select, view state only, composed with the text
|
|
2352
|
+
// search below. Counts are derived per predicate from the snapshot.
|
|
2353
|
+
!loading && !failed ? h("div", {
|
|
2354
|
+
className: "km-filters",
|
|
2355
|
+
role: "group",
|
|
2356
|
+
"aria-label": t("filterGroup")
|
|
2357
|
+
}, FILTER_CHIPS.map(
|
|
2358
|
+
(chip) => h(
|
|
2359
|
+
"button",
|
|
2360
|
+
{
|
|
2361
|
+
key: chip.value,
|
|
2362
|
+
type: "button",
|
|
2363
|
+
className: cx("km-chip", `km-chip-${chip.value}`, filter === chip.value && "km-chip-on"),
|
|
2364
|
+
"aria-pressed": filter === chip.value,
|
|
2365
|
+
onClick: () => setFilter(chip.value)
|
|
2366
|
+
},
|
|
2367
|
+
h("span", { className: "km-chip-label" }, t(chip.label)),
|
|
2368
|
+
h("span", { className: "km-chip-count" }, filterCount(chip.value))
|
|
2369
|
+
)
|
|
2370
|
+
)) : null,
|
|
1959
2371
|
h(
|
|
1960
2372
|
"div",
|
|
1961
2373
|
{ className: "km-search" },
|
|
@@ -1985,15 +2397,29 @@ function KeysManagerBody() {
|
|
|
1985
2397
|
"div",
|
|
1986
2398
|
{ className: "km-section-head" },
|
|
1987
2399
|
h("h2", { className: "km-section-title" }, t("managed")),
|
|
1988
|
-
|
|
2400
|
+
// What is below it, like the available section: the tile above keeps
|
|
2401
|
+
// the global total.
|
|
2402
|
+
h("span", { className: "km-section-count" }, managed.length + visibleOrphans.length)
|
|
1989
2403
|
),
|
|
1990
|
-
|
|
2404
|
+
// The placeholder only when the section renders neither a card nor a row
|
|
2405
|
+
// the active chip selects.
|
|
2406
|
+
managed.length === 0 && visibleOrphans.length === 0 ? h("div", { className: "km-empty" }, h(Icon, { name: "key", size: 20 }), h(
|
|
2407
|
+
"span",
|
|
2408
|
+
null,
|
|
2409
|
+
normalizedQuery.length > 0 ? t("noSearch") : filter === "transport" ? t("transportEmpty") : managedHiddenByChip ? t("managedFilteredEmpty") : t("managedEmpty")
|
|
2410
|
+
)) : null,
|
|
2411
|
+
managed.length > 0 ? h("div", { className: "km-provider-list" }, managed.map((provider) => h(ProviderCard, {
|
|
1991
2412
|
key: provider.provider,
|
|
1992
2413
|
provider,
|
|
1993
2414
|
now,
|
|
2415
|
+
transportAccount: provider.transport === true ? transportAccounts.find((account) => account.id === provider.provider) : void 0,
|
|
1994
2416
|
expanded: expanded.has(provider.provider),
|
|
1995
2417
|
onToggle: () => toggle(provider.provider)
|
|
1996
|
-
})))
|
|
2418
|
+
}))) : null,
|
|
2419
|
+
visibleOrphans.length > 0 ? h("div", { className: "km-provider-list" }, visibleOrphans.map((account) => h(TransportAccountRow, {
|
|
2420
|
+
key: account.scope,
|
|
2421
|
+
account
|
|
2422
|
+
}))) : null
|
|
1997
2423
|
) : null,
|
|
1998
2424
|
!loading && !failed ? h(
|
|
1999
2425
|
"section",
|
|
@@ -2036,8 +2462,9 @@ function KeysManagerCard({ view }) {
|
|
|
2036
2462
|
if (view === "summary") {
|
|
2037
2463
|
const state = snapshot.state;
|
|
2038
2464
|
if (state === null) return h("span", { className: "km-summary-line" }, snapshot.status === "error" ? t("loadError") : t("loading"));
|
|
2039
|
-
|
|
2040
|
-
return h("span", { className: "km-summary-line" }, t("
|
|
2465
|
+
const managed = managedEntryCount(state.providers, state.transportAccounts ?? []);
|
|
2466
|
+
if (managed === 0) return h("span", { className: "km-summary-line" }, t("summaryEmpty"));
|
|
2467
|
+
return h("span", { className: "km-summary-line" }, t("summaryText", { providers: managed, keys: state.keyCount }));
|
|
2041
2468
|
}
|
|
2042
2469
|
return h(KeysManagerBody);
|
|
2043
2470
|
}
|