dsh-keys-manager 0.2.0 → 0.2.2
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 +33 -0
- package/README.md +108 -15
- package/README.zh-CN.md +80 -2
- package/bundle/cordis.patch.yml +5 -2
- package/lib/client/types.d.ts +27 -2
- package/lib/client.js +320 -3
- package/lib/domain.d.ts +55 -0
- package/lib/host-types.d.ts +29 -0
- package/lib/index.d.ts +865 -23
- package/lib/index.js +3453 -189
- 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 +17 -1
- package/locale/zh.json +17 -1
- package/package.json +14 -12
- package/scripts/prepare-release.mjs +161 -74
- package/scripts/verify-install.mjs +156 -0
package/lib/client.js
CHANGED
|
@@ -175,6 +175,12 @@ var zh = {
|
|
|
175
175
|
accountProjected: "\u670D\u52A1\u4E2D",
|
|
176
176
|
accountReauth: "\u91CD\u65B0\u8BA4\u8BC1",
|
|
177
177
|
accountActivate: "\u8BBE\u4E3A\u5F53\u524D",
|
|
178
|
+
testKey: "\u6D4B\u8BD5\u8FDE\u63A5",
|
|
179
|
+
testRunning: "\u6D4B\u8BD5\u4E2D\u2026",
|
|
180
|
+
testPassed: "\u8FDE\u63A5\u6B63\u5E38 \xB7 {ms} ms",
|
|
181
|
+
testFailed: "\u8FDE\u63A5\u5931\u8D25\uFF1A{error}",
|
|
182
|
+
testTimedOut: "\u6D4B\u8BD5\u8D85\u65F6\uFF0C\u672A\u6536\u5230\u7ED3\u679C",
|
|
183
|
+
testFailedUnknown: "\u6D4B\u8BD5\u672A\u5B8C\u6210\uFF0C\u672A\u8FD4\u56DE\u539F\u56E0",
|
|
178
184
|
loginTitle: "\u8D26\u6237\u767B\u5F55",
|
|
179
185
|
loginPending: "\u7B49\u5F85\u6388\u6743\u2026",
|
|
180
186
|
loginBrowserOpen: "\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00",
|
|
@@ -185,7 +191,19 @@ var zh = {
|
|
|
185
191
|
loginReady: "\u767B\u5F55\u6210\u529F\u3002",
|
|
186
192
|
deleteAccountTitle: "\u5220\u9664\u8D26\u6237\u201C{alias}\u201D\uFF1F",
|
|
187
193
|
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"
|
|
194
|
+
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",
|
|
195
|
+
// Transport accounts (llm-omp-transports/<route>): managed beside the
|
|
196
|
+
// pi-ai pool, so the copy never promises a key-pool fallback.
|
|
197
|
+
transportAccounts: "\u4F20\u8F93\u8D26\u6237",
|
|
198
|
+
transportTag: "\u4F20\u8F93",
|
|
199
|
+
transportSignIn: "\u767B\u5F55",
|
|
200
|
+
transportExpires: "\u6709\u6548\u671F\u81F3 {time}",
|
|
201
|
+
transportStatusActive: "\u6B63\u5E38",
|
|
202
|
+
transportStatusExpired: "\u4EE4\u724C\u5DF2\u8FC7\u671F",
|
|
203
|
+
transportStatusNeedsReauth: "\u9700\u91CD\u65B0\u767B\u5F55",
|
|
204
|
+
transportStatusSignedOut: "\u672A\u767B\u5F55",
|
|
205
|
+
transportRemoveTitle: "\u79FB\u9664 {name} \u4F20\u8F93\u8D26\u6237\uFF1F",
|
|
206
|
+
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
207
|
};
|
|
190
208
|
var en = {
|
|
191
209
|
nav: "Keys Manager",
|
|
@@ -323,6 +341,12 @@ var en = {
|
|
|
323
341
|
accountProjected: "Serving provider",
|
|
324
342
|
accountReauth: "Re-authenticate",
|
|
325
343
|
accountActivate: "Set active",
|
|
344
|
+
testKey: "Test",
|
|
345
|
+
testRunning: "Testing\u2026",
|
|
346
|
+
testPassed: "Test passed \xB7 {ms} ms",
|
|
347
|
+
testFailed: "Test failed: {error}",
|
|
348
|
+
testTimedOut: "The test timed out without a result",
|
|
349
|
+
testFailedUnknown: "The test did not complete and reported no reason",
|
|
326
350
|
loginTitle: "Account sign-in",
|
|
327
351
|
loginPending: "Waiting for authorization\u2026",
|
|
328
352
|
loginBrowserOpen: "Open in browser",
|
|
@@ -333,7 +357,18 @@ var en = {
|
|
|
333
357
|
loginReady: "Signed in.",
|
|
334
358
|
deleteAccountTitle: "Delete account \u201C{alias}\u201D?",
|
|
335
359
|
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."
|
|
360
|
+
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.",
|
|
361
|
+
// Transport accounts — same values as locale/en.json.
|
|
362
|
+
transportAccounts: "Transport accounts",
|
|
363
|
+
transportTag: "Transport",
|
|
364
|
+
transportSignIn: "Sign in",
|
|
365
|
+
transportExpires: "Expires {time}",
|
|
366
|
+
transportStatusActive: "Active",
|
|
367
|
+
transportStatusExpired: "Token expired",
|
|
368
|
+
transportStatusNeedsReauth: "Sign in again",
|
|
369
|
+
transportStatusSignedOut: "Not signed in",
|
|
370
|
+
transportRemoveTitle: "Remove the {name} transport account?",
|
|
371
|
+
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
372
|
};
|
|
338
373
|
var LOCALE_DICTS = { zh, en };
|
|
339
374
|
|
|
@@ -584,6 +619,18 @@ function messageOf(error) {
|
|
|
584
619
|
if (error instanceof Error && error.message.length > 0) return error.message;
|
|
585
620
|
return String(error);
|
|
586
621
|
}
|
|
622
|
+
var TEST_KEY_DEADLINE_MS = 3e4;
|
|
623
|
+
async function withDeadline(work, ms, label) {
|
|
624
|
+
let timer;
|
|
625
|
+
const deadline = new Promise((_, reject) => {
|
|
626
|
+
timer = setTimeout(() => reject(new Error(`${label} did not answer within ${ms / 1e3}s`)), ms);
|
|
627
|
+
});
|
|
628
|
+
try {
|
|
629
|
+
return await Promise.race([work, deadline]);
|
|
630
|
+
} finally {
|
|
631
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
587
634
|
function createStore(ctx) {
|
|
588
635
|
let snapshot = {
|
|
589
636
|
status: "idle",
|
|
@@ -595,6 +642,10 @@ function createStore(ctx) {
|
|
|
595
642
|
let loadSequence = 0;
|
|
596
643
|
let mutationTail = Promise.resolve();
|
|
597
644
|
let queuedMutations = 0;
|
|
645
|
+
let activeTests = 0;
|
|
646
|
+
let testGeneration = 0;
|
|
647
|
+
let mutationGeneration = 0;
|
|
648
|
+
let stateDropped = false;
|
|
598
649
|
const listeners = /* @__PURE__ */ new Set();
|
|
599
650
|
const emit = () => {
|
|
600
651
|
for (const listener of [...listeners]) {
|
|
@@ -614,6 +665,7 @@ function createStore(ctx) {
|
|
|
614
665
|
update({ busy: method, error: null });
|
|
615
666
|
const run = async () => {
|
|
616
667
|
++loadSequence;
|
|
668
|
+
mutationGeneration = testGeneration;
|
|
617
669
|
try {
|
|
618
670
|
const result = await rpc(method, args);
|
|
619
671
|
if (!result.ok) throw new Error(result.error.message);
|
|
@@ -627,6 +679,7 @@ function createStore(ctx) {
|
|
|
627
679
|
return { ok: false, error: message };
|
|
628
680
|
} finally {
|
|
629
681
|
queuedMutations -= 1;
|
|
682
|
+
if (queuedMutations === 0) refreshDroppedState();
|
|
630
683
|
}
|
|
631
684
|
};
|
|
632
685
|
const next = mutationTail.then(run, run);
|
|
@@ -635,10 +688,21 @@ function createStore(ctx) {
|
|
|
635
688
|
};
|
|
636
689
|
const stateOutcome = (value) => {
|
|
637
690
|
const state = value;
|
|
691
|
+
const borrowed = activeTests > 0 || testGeneration !== mutationGeneration;
|
|
692
|
+
if (borrowed) {
|
|
693
|
+
if (queuedMutations === 1) update({ busy: null, error: null });
|
|
694
|
+
else update({ error: null });
|
|
695
|
+
stateDropped = true;
|
|
696
|
+
return { ok: true, value: state };
|
|
697
|
+
}
|
|
638
698
|
if (queuedMutations === 1) update({ status: "ready", state, now: Date.now(), busy: null, error: null });
|
|
639
699
|
else update({ status: "ready", state, now: Date.now(), error: null });
|
|
640
700
|
return { ok: true, value: state };
|
|
641
701
|
};
|
|
702
|
+
const refreshDroppedState = () => {
|
|
703
|
+
if (!stateDropped || activeTests > 0 || snapshot.busy !== null) return;
|
|
704
|
+
void store.load({ keepError: true });
|
|
705
|
+
};
|
|
642
706
|
const rpcRead = async (method, args) => {
|
|
643
707
|
try {
|
|
644
708
|
return await rpc(method, args);
|
|
@@ -649,7 +713,14 @@ function createStore(ctx) {
|
|
|
649
713
|
const attemptOutcome = (value) => {
|
|
650
714
|
return { ok: true, value: value ?? null };
|
|
651
715
|
};
|
|
652
|
-
|
|
716
|
+
const testOutcome = (value) => {
|
|
717
|
+
const result = value ?? {};
|
|
718
|
+
return {
|
|
719
|
+
ok: true,
|
|
720
|
+
value: { ok: result.ok === true, latencyMs: result.latencyMs ?? 0, ...result.error === void 0 ? {} : { error: result.error } }
|
|
721
|
+
};
|
|
722
|
+
};
|
|
723
|
+
const store = {
|
|
653
724
|
getSnapshot: () => snapshot,
|
|
654
725
|
subscribe(listener) {
|
|
655
726
|
listeners.add(listener);
|
|
@@ -658,12 +729,18 @@ function createStore(ctx) {
|
|
|
658
729
|
async load(options = {}) {
|
|
659
730
|
if (snapshot.busy !== null) return;
|
|
660
731
|
const request = ++loadSequence;
|
|
732
|
+
const generation = testGeneration;
|
|
661
733
|
update({ busy: "load", error: options.keepError === true ? snapshot.error : null });
|
|
662
734
|
try {
|
|
663
735
|
const result = await rpc("getState", {});
|
|
664
736
|
if (request !== loadSequence) return;
|
|
737
|
+
if (activeTests > 0 || testGeneration !== generation) {
|
|
738
|
+
update({ busy: null });
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
665
741
|
if (!result.ok) throw new Error(result.error.message);
|
|
666
742
|
update({ status: "ready", state: result.value, now: Date.now(), busy: null, error: null });
|
|
743
|
+
stateDropped = false;
|
|
667
744
|
} catch (error) {
|
|
668
745
|
if (request !== loadSequence) return;
|
|
669
746
|
update({ status: snapshot.state !== null ? snapshot.status : "error", busy: null, error: messageOf(error) });
|
|
@@ -694,6 +771,36 @@ function createStore(ctx) {
|
|
|
694
771
|
removeAccount(provider, accountId) {
|
|
695
772
|
return send("removeAccount", { provider, accountId }, stateOutcome);
|
|
696
773
|
},
|
|
774
|
+
async testKey(provider, keyId) {
|
|
775
|
+
activeTests += 1;
|
|
776
|
+
++testGeneration;
|
|
777
|
+
const request = rpc("testKey", { provider, keyId });
|
|
778
|
+
let released = false;
|
|
779
|
+
const release = () => {
|
|
780
|
+
if (released) return;
|
|
781
|
+
released = true;
|
|
782
|
+
activeTests -= 1;
|
|
783
|
+
testGeneration += 1;
|
|
784
|
+
refreshDroppedState();
|
|
785
|
+
};
|
|
786
|
+
void request.then(release, release);
|
|
787
|
+
try {
|
|
788
|
+
const result = await withDeadline(request, TEST_KEY_DEADLINE_MS, "testKey");
|
|
789
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
790
|
+
return testOutcome(result.value);
|
|
791
|
+
} catch (error) {
|
|
792
|
+
return { ok: false, error: messageOf(error) };
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
beginTransportLogin(route) {
|
|
796
|
+
return send("beginTransportLogin", { provider: route }, attemptOutcome);
|
|
797
|
+
},
|
|
798
|
+
activateTransportAccount(route) {
|
|
799
|
+
return send("activateTransportAccount", { provider: route }, stateOutcome);
|
|
800
|
+
},
|
|
801
|
+
removeTransportAccount(route) {
|
|
802
|
+
return send("removeTransportAccount", { provider: route }, stateOutcome);
|
|
803
|
+
},
|
|
697
804
|
tick() {
|
|
698
805
|
update({ now: Date.now() });
|
|
699
806
|
if (snapshot.status === "ready" && snapshot.busy === null) void this.load({ keepError: true });
|
|
@@ -702,6 +809,7 @@ function createStore(ctx) {
|
|
|
702
809
|
update({ error: null });
|
|
703
810
|
}
|
|
704
811
|
};
|
|
812
|
+
return store;
|
|
705
813
|
}
|
|
706
814
|
|
|
707
815
|
// src/client/ui.ts
|
|
@@ -791,6 +899,30 @@ function statusKey(status) {
|
|
|
791
899
|
return "keyStatusUnmanaged";
|
|
792
900
|
}
|
|
793
901
|
}
|
|
902
|
+
function transportStatusKey(status) {
|
|
903
|
+
switch (status) {
|
|
904
|
+
case "active":
|
|
905
|
+
return "transportStatusActive";
|
|
906
|
+
case "expired":
|
|
907
|
+
return "transportStatusExpired";
|
|
908
|
+
case "needs-reauth":
|
|
909
|
+
return "transportStatusNeedsReauth";
|
|
910
|
+
default:
|
|
911
|
+
return "transportStatusSignedOut";
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
function transportStatusTone(status) {
|
|
915
|
+
switch (status) {
|
|
916
|
+
case "active":
|
|
917
|
+
return "success";
|
|
918
|
+
case "expired":
|
|
919
|
+
return "warn";
|
|
920
|
+
case "needs-reauth":
|
|
921
|
+
return "error";
|
|
922
|
+
default:
|
|
923
|
+
return "idle";
|
|
924
|
+
}
|
|
925
|
+
}
|
|
794
926
|
function statusTone(status) {
|
|
795
927
|
switch (status) {
|
|
796
928
|
case "active":
|
|
@@ -836,6 +968,7 @@ function Icon({ name, size = 16, className }) {
|
|
|
836
968
|
if (name === "warning") return h("svg", common, h("path", { d: "M12 3l9.5 17h-19L12 3zM12 9v5M12 17.2v.1" }));
|
|
837
969
|
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
970
|
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" }));
|
|
971
|
+
if (name === "activity") return h("svg", common, h("path", { d: "M3 12h4l2.5-7 4 14 2.5-7h5" }));
|
|
839
972
|
return h("svg", common);
|
|
840
973
|
}
|
|
841
974
|
function Button({ variant = "secondary", size = "normal", type = "button", disabled, onClick, children, title }) {
|
|
@@ -1039,6 +1172,35 @@ function KeyForm({ provider, onDone, autoFocus = false }) {
|
|
|
1039
1172
|
)
|
|
1040
1173
|
);
|
|
1041
1174
|
}
|
|
1175
|
+
var ROW_TEST_STYLE = { gridColumn: "1 / -1", marginTop: "-2px" };
|
|
1176
|
+
var ROW_TEST_DEADLINE_MS = 3e4;
|
|
1177
|
+
async function requestRowTest(store, provider, keyId) {
|
|
1178
|
+
let timer;
|
|
1179
|
+
const deadline = new Promise((resolve) => {
|
|
1180
|
+
timer = setTimeout(() => resolve({ kind: "failed", timedOut: true }), ROW_TEST_DEADLINE_MS);
|
|
1181
|
+
});
|
|
1182
|
+
const answer = store.testKey(provider, keyId).then((outcome) => {
|
|
1183
|
+
if (!outcome.ok) return { kind: "failed", error: outcome.error };
|
|
1184
|
+
if (outcome.value.ok) return { kind: "passed", latencyMs: outcome.value.latencyMs };
|
|
1185
|
+
return { kind: "failed", ...outcome.value.error === void 0 ? {} : { error: outcome.value.error } };
|
|
1186
|
+
});
|
|
1187
|
+
try {
|
|
1188
|
+
return await Promise.race([answer, deadline]);
|
|
1189
|
+
} finally {
|
|
1190
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
function testLine(test, t) {
|
|
1194
|
+
if (test === null) return null;
|
|
1195
|
+
if (test.kind === "running") {
|
|
1196
|
+
return h("div", { className: "km-key-time km-muted", role: "status", style: ROW_TEST_STYLE }, t("testRunning"));
|
|
1197
|
+
}
|
|
1198
|
+
if (test.kind === "passed") {
|
|
1199
|
+
return h("div", { className: "km-key-time", role: "status", style: ROW_TEST_STYLE }, t("testPassed", { ms: test.latencyMs ?? 0 }));
|
|
1200
|
+
}
|
|
1201
|
+
const detail = test.error !== void 0 && test.error.length > 0 ? test.error : test.timedOut === true ? t("testTimedOut") : t("testFailedUnknown");
|
|
1202
|
+
return h("div", { className: "km-key-time km-warn", role: "status", style: ROW_TEST_STYLE }, t("testFailed", { error: detail }));
|
|
1203
|
+
}
|
|
1042
1204
|
function KeyRow({ provider, keyRecord, now }) {
|
|
1043
1205
|
const key = keyRecord;
|
|
1044
1206
|
const runtime = useRuntime();
|
|
@@ -1050,6 +1212,7 @@ function KeyRow({ provider, keyRecord, now }) {
|
|
|
1050
1212
|
const [confirm, setConfirm] = import_react2.default.useState(false);
|
|
1051
1213
|
const [busy, setBusy] = import_react2.default.useState(false);
|
|
1052
1214
|
const [error, setError] = import_react2.default.useState(null);
|
|
1215
|
+
const [test, setTest] = import_react2.default.useState(null);
|
|
1053
1216
|
const rootRef = import_react2.default.useRef(null);
|
|
1054
1217
|
import_react2.default.useEffect(() => {
|
|
1055
1218
|
setAlias(key.alias);
|
|
@@ -1094,6 +1257,10 @@ function KeyRow({ provider, keyRecord, now }) {
|
|
|
1094
1257
|
if (!result.ok) setError(result.error);
|
|
1095
1258
|
else setConfirm(false);
|
|
1096
1259
|
}
|
|
1260
|
+
async function runTest() {
|
|
1261
|
+
setTest({ kind: "running" });
|
|
1262
|
+
setTest(await requestRowTest(runtime.store, provider.provider, key.id));
|
|
1263
|
+
}
|
|
1097
1264
|
const cold = key.status === "cooling" && key.cooldownUntil > now;
|
|
1098
1265
|
const meta = key.status === "missing" ? t("keyStatusMissing") : key.totalUses > 0 ? t("uses", { n: key.totalUses }) : "";
|
|
1099
1266
|
return h(
|
|
@@ -1169,6 +1336,16 @@ function KeyRow({ provider, keyRecord, now }) {
|
|
|
1169
1336
|
setMenu(false);
|
|
1170
1337
|
setEditing(true);
|
|
1171
1338
|
} }, h(Icon, { name: "key", size: 15 }), t("rename")),
|
|
1339
|
+
h("button", {
|
|
1340
|
+
type: "button",
|
|
1341
|
+
className: "km-menu-item",
|
|
1342
|
+
role: "menuitem",
|
|
1343
|
+
onClick: () => {
|
|
1344
|
+
setMenu(false);
|
|
1345
|
+
void runTest();
|
|
1346
|
+
},
|
|
1347
|
+
disabled: busy || test?.kind === "running"
|
|
1348
|
+
}, h(Icon, { name: "activity", size: 15 }), t("testKey")),
|
|
1172
1349
|
h("button", {
|
|
1173
1350
|
type: "button",
|
|
1174
1351
|
className: "km-menu-item",
|
|
@@ -1187,6 +1364,7 @@ function KeyRow({ provider, keyRecord, now }) {
|
|
|
1187
1364
|
) : null
|
|
1188
1365
|
),
|
|
1189
1366
|
error !== null ? h("div", { className: "km-key-error", role: "alert" }, error) : null,
|
|
1367
|
+
testLine(test, t),
|
|
1190
1368
|
confirm ? h(Modal, {
|
|
1191
1369
|
title: t("deleteTitle", { alias: key.alias }),
|
|
1192
1370
|
onClose: () => setConfirm(false),
|
|
@@ -1208,6 +1386,7 @@ function AccountRow({ provider, keyRecord, now, onReauthenticate }) {
|
|
|
1208
1386
|
const [confirm, setConfirm] = import_react2.default.useState(false);
|
|
1209
1387
|
const [busy, setBusy] = import_react2.default.useState(false);
|
|
1210
1388
|
const [error, setError] = import_react2.default.useState(null);
|
|
1389
|
+
const [test, setTest] = import_react2.default.useState(null);
|
|
1211
1390
|
const rootRef = import_react2.default.useRef(null);
|
|
1212
1391
|
import_react2.default.useEffect(() => {
|
|
1213
1392
|
if (!menu) return void 0;
|
|
@@ -1239,6 +1418,10 @@ function AccountRow({ provider, keyRecord, now, onReauthenticate }) {
|
|
|
1239
1418
|
if (!result.ok) setError(result.error);
|
|
1240
1419
|
else setConfirm(false);
|
|
1241
1420
|
}
|
|
1421
|
+
async function runTest() {
|
|
1422
|
+
setTest({ kind: "running" });
|
|
1423
|
+
setTest(await requestRowTest(runtime.store, provider.provider, key.id));
|
|
1424
|
+
}
|
|
1242
1425
|
const cold = key.status === "cooling" && key.cooldownUntil > now;
|
|
1243
1426
|
const metaParts = [
|
|
1244
1427
|
key.totalUses > 0 ? t("uses", { n: key.totalUses }) : null,
|
|
@@ -1313,6 +1496,16 @@ function AccountRow({ provider, keyRecord, now, onReauthenticate }) {
|
|
|
1313
1496
|
// active again is a no-op, so the entry stays disabled.
|
|
1314
1497
|
disabled: busy || key.projected === true
|
|
1315
1498
|
}, h(Icon, { name: "check", size: 15 }), t("accountActivate")),
|
|
1499
|
+
h("button", {
|
|
1500
|
+
type: "button",
|
|
1501
|
+
className: "km-menu-item",
|
|
1502
|
+
role: "menuitem",
|
|
1503
|
+
onClick: () => {
|
|
1504
|
+
setMenu(false);
|
|
1505
|
+
void runTest();
|
|
1506
|
+
},
|
|
1507
|
+
disabled: busy || test?.kind === "running"
|
|
1508
|
+
}, h(Icon, { name: "activity", size: 15 }), t("testKey")),
|
|
1316
1509
|
h("button", {
|
|
1317
1510
|
type: "button",
|
|
1318
1511
|
className: "km-menu-item",
|
|
@@ -1331,6 +1524,7 @@ function AccountRow({ provider, keyRecord, now, onReauthenticate }) {
|
|
|
1331
1524
|
) : null
|
|
1332
1525
|
),
|
|
1333
1526
|
error !== null ? h("div", { className: "km-key-error", role: "alert" }, error) : null,
|
|
1527
|
+
testLine(test, t),
|
|
1334
1528
|
confirm ? h(Modal, {
|
|
1335
1529
|
// SF4: account-specific copy — the grant removal consequence, not a
|
|
1336
1530
|
// key-deletion/provider-release promise. Step 8: the key-pool
|
|
@@ -1346,6 +1540,114 @@ function AccountRow({ provider, keyRecord, now, onReauthenticate }) {
|
|
|
1346
1540
|
}, h("p", { className: "km-modal-copy" }, t(provider.supported === true ? "deleteAccountBody" : "deleteAccountBodyNoKeys"))) : null
|
|
1347
1541
|
);
|
|
1348
1542
|
}
|
|
1543
|
+
function TransportAccountRow({ account }) {
|
|
1544
|
+
const runtime = useRuntime();
|
|
1545
|
+
const t = useT();
|
|
1546
|
+
const locale = useLocaleId();
|
|
1547
|
+
const [busy, setBusy] = import_react2.default.useState(false);
|
|
1548
|
+
const [error, setError] = import_react2.default.useState(null);
|
|
1549
|
+
const [confirm, setConfirm] = import_react2.default.useState(false);
|
|
1550
|
+
const [login, setLogin] = import_react2.default.useState(null);
|
|
1551
|
+
async function signIn() {
|
|
1552
|
+
setBusy(true);
|
|
1553
|
+
setError(null);
|
|
1554
|
+
const result = await runtime.store.beginTransportLogin(account.id);
|
|
1555
|
+
setBusy(false);
|
|
1556
|
+
if (!result.ok) {
|
|
1557
|
+
setError(result.error);
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
if (result.value !== null) setLogin({ attemptId: result.value.id });
|
|
1561
|
+
}
|
|
1562
|
+
async function remove() {
|
|
1563
|
+
setBusy(true);
|
|
1564
|
+
setError(null);
|
|
1565
|
+
const result = await runtime.store.removeTransportAccount(account.id);
|
|
1566
|
+
setBusy(false);
|
|
1567
|
+
if (!result.ok) setError(result.error);
|
|
1568
|
+
else setConfirm(false);
|
|
1569
|
+
}
|
|
1570
|
+
const identity = account.email ?? account.accountId ?? account.displayName;
|
|
1571
|
+
const tone = transportStatusTone(account.status);
|
|
1572
|
+
return h(
|
|
1573
|
+
"div",
|
|
1574
|
+
{ className: "km-account-row km-transport-row" },
|
|
1575
|
+
h(
|
|
1576
|
+
"div",
|
|
1577
|
+
{ className: "km-key-identity" },
|
|
1578
|
+
h(
|
|
1579
|
+
"div",
|
|
1580
|
+
{ className: "km-key-alias-line" },
|
|
1581
|
+
h("span", { className: "km-account-glyph", "aria-hidden": true }, h(Icon, { name: "user", size: 13 })),
|
|
1582
|
+
h("span", { className: "km-key-alias" }, identity),
|
|
1583
|
+
h("span", { className: "km-tag km-tag-accent" }, t("transportTag"))
|
|
1584
|
+
),
|
|
1585
|
+
// The credential scope, never the payload: the record is the account.
|
|
1586
|
+
h(
|
|
1587
|
+
"div",
|
|
1588
|
+
{ className: "km-key-sub" },
|
|
1589
|
+
h("span", { className: "km-mono km-muted" }, account.scope)
|
|
1590
|
+
)
|
|
1591
|
+
),
|
|
1592
|
+
h(
|
|
1593
|
+
"div",
|
|
1594
|
+
{ className: "km-key-status" },
|
|
1595
|
+
h(
|
|
1596
|
+
"span",
|
|
1597
|
+
{ className: cx("km-status", `km-status-${tone}`) },
|
|
1598
|
+
h("span", { className: cx("km-dot", `km-dot-${tone}`) }),
|
|
1599
|
+
h("span", null, t(transportStatusKey(account.status)))
|
|
1600
|
+
)
|
|
1601
|
+
),
|
|
1602
|
+
// The expiry column stays a clock, not a countdown: the route's adapter
|
|
1603
|
+
// renews the grant itself, so "expired" here means "renews on next use".
|
|
1604
|
+
h(
|
|
1605
|
+
"div",
|
|
1606
|
+
{ className: "km-key-last" },
|
|
1607
|
+
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) }))
|
|
1608
|
+
),
|
|
1609
|
+
h(
|
|
1610
|
+
"div",
|
|
1611
|
+
{ className: "km-key-actions" },
|
|
1612
|
+
h(
|
|
1613
|
+
Button,
|
|
1614
|
+
{
|
|
1615
|
+
size: "small",
|
|
1616
|
+
disabled: busy,
|
|
1617
|
+
onClick: signIn,
|
|
1618
|
+
title: account.active ? t("accountReauth") : t("transportSignIn")
|
|
1619
|
+
},
|
|
1620
|
+
busy ? h(Icon, { name: "refresh", size: 14, className: "km-spin" }) : h(Icon, { name: "refresh", size: 14 }),
|
|
1621
|
+
t(account.active ? "accountReauth" : "transportSignIn")
|
|
1622
|
+
),
|
|
1623
|
+
// No "Set active" entry: activation would write nothing (the stored grant
|
|
1624
|
+
// already IS the route's live credential), so the host endpoint stays for
|
|
1625
|
+
// API symmetry with the account surface and the row omits a no-effect
|
|
1626
|
+
// control.
|
|
1627
|
+
h(Button, {
|
|
1628
|
+
size: "small",
|
|
1629
|
+
variant: "danger",
|
|
1630
|
+
disabled: busy,
|
|
1631
|
+
onClick: () => setConfirm(true)
|
|
1632
|
+
}, h(Icon, { name: "trash", size: 14 }), t("delete"))
|
|
1633
|
+
),
|
|
1634
|
+
error !== null ? h("div", { className: "km-key-error", role: "alert" }, error) : null,
|
|
1635
|
+
confirm ? h(Modal, {
|
|
1636
|
+
title: t("transportRemoveTitle", { name: account.displayName }),
|
|
1637
|
+
onClose: () => setConfirm(false),
|
|
1638
|
+
busy,
|
|
1639
|
+
footer: [
|
|
1640
|
+
h(Button, { key: "cancel", onClick: () => setConfirm(false), disabled: busy }, t("cancel")),
|
|
1641
|
+
h(Button, { key: "delete", variant: "danger", onClick: remove, disabled: busy }, busy ? t("deleting") : t("deleteConfirm"))
|
|
1642
|
+
]
|
|
1643
|
+
}, h("p", { className: "km-modal-copy" }, t("transportRemoveBody"))) : null,
|
|
1644
|
+
login !== null ? h(LoginDialog, {
|
|
1645
|
+
provider: { displayName: account.displayName },
|
|
1646
|
+
attemptId: login.attemptId,
|
|
1647
|
+
onClose: () => setLogin(null)
|
|
1648
|
+
}) : null
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1349
1651
|
function ProviderCard({ provider, expanded, onToggle, now }) {
|
|
1350
1652
|
const runtime = useRuntime();
|
|
1351
1653
|
const t = useT();
|
|
@@ -1885,6 +2187,7 @@ function KeysManagerBody() {
|
|
|
1885
2187
|
if (snapshot.status === "idle") void runtime.store.load();
|
|
1886
2188
|
}, [runtime, snapshot.status]);
|
|
1887
2189
|
const providers = state?.providers ?? [];
|
|
2190
|
+
const transportAccounts = state?.transportAccounts ?? [];
|
|
1888
2191
|
const normalizedQuery = query.trim().toLowerCase();
|
|
1889
2192
|
const matches = (provider) => {
|
|
1890
2193
|
if (normalizedQuery.length === 0) return true;
|
|
@@ -2021,6 +2324,20 @@ function KeysManagerBody() {
|
|
|
2021
2324
|
h(Button, { variant: "ghost", size: "small", onClick: () => setShowAllAvailable(false) }, t("showLess"))
|
|
2022
2325
|
) : null
|
|
2023
2326
|
) : null,
|
|
2327
|
+
!loading && !failed && transportAccounts.length > 0 ? h(
|
|
2328
|
+
"section",
|
|
2329
|
+
{ className: "km-section" },
|
|
2330
|
+
h(
|
|
2331
|
+
"div",
|
|
2332
|
+
{ className: "km-section-head" },
|
|
2333
|
+
h("h2", { className: "km-section-title" }, t("transportAccounts")),
|
|
2334
|
+
h("span", { className: "km-section-count" }, transportAccounts.length)
|
|
2335
|
+
),
|
|
2336
|
+
h("div", { className: "km-provider-list" }, transportAccounts.map((account) => h(TransportAccountRow, {
|
|
2337
|
+
key: account.scope,
|
|
2338
|
+
account
|
|
2339
|
+
})))
|
|
2340
|
+
) : null,
|
|
2024
2341
|
!loading && !failed ? h("section", { className: "km-section" }, h(SettingsCard)) : null,
|
|
2025
2342
|
h(
|
|
2026
2343
|
"footer",
|
package/lib/domain.d.ts
CHANGED
|
@@ -94,6 +94,13 @@ export interface ProviderRecord {
|
|
|
94
94
|
brokerRef: string;
|
|
95
95
|
/** The profile's reference before adoption, restored on release. */
|
|
96
96
|
originalApiKeyEnv: string | null;
|
|
97
|
+
/**
|
|
98
|
+
* Adoption found a foreign projection serving the route, so it could not
|
|
99
|
+
* complete its one-time takeover of `originalApiKeyEnv` (PR review round 40).
|
|
100
|
+
* The debt survives a restart and is settled by the next projection-change
|
|
101
|
+
* sync; it never authorises an ordinary repair.
|
|
102
|
+
*/
|
|
103
|
+
takeoverPending?: boolean;
|
|
97
104
|
/** True when the profile itself was created by adding an extra provider. */
|
|
98
105
|
createdProfile: boolean;
|
|
99
106
|
/** True when a profile existed before adoption. */
|
|
@@ -186,6 +193,30 @@ export interface ProviderView {
|
|
|
186
193
|
keys: KeyView[];
|
|
187
194
|
warning?: string;
|
|
188
195
|
}
|
|
196
|
+
/** Health state a transport account row renders. */
|
|
197
|
+
export type TransportAccountStatus = 'active' | 'expired' | 'needs-reauth' | 'signed-out';
|
|
198
|
+
/**
|
|
199
|
+
* One `llm-omp-transports/<route>` account held beside the pi-ai pool.
|
|
200
|
+
*
|
|
201
|
+
* A transport route keeps exactly one grant record, so a row is either that
|
|
202
|
+
* record's secret-free view or the route's signed-out placeholder. It never
|
|
203
|
+
* carries the grant payload itself, and it is never part of a pi-ai
|
|
204
|
+
* projection: the transports package's own adapter resolves the record.
|
|
205
|
+
*/
|
|
206
|
+
export interface TransportAccountView {
|
|
207
|
+
/** Provider route the grant serves, e.g. 'cursor'. */
|
|
208
|
+
id: string;
|
|
209
|
+
/** The credential record holding the grant, e.g. 'llm-omp-transports/cursor'. */
|
|
210
|
+
scope: string;
|
|
211
|
+
displayName: string;
|
|
212
|
+
email?: string;
|
|
213
|
+
accountId?: string;
|
|
214
|
+
/** Epoch ms when the access token expires. */
|
|
215
|
+
expiresAt?: number;
|
|
216
|
+
status: TransportAccountStatus;
|
|
217
|
+
/** Derived from {@link status}: true only while it is `'active'`. */
|
|
218
|
+
active: boolean;
|
|
219
|
+
}
|
|
189
220
|
/** Complete manager snapshot the UI renders. */
|
|
190
221
|
export interface StateView {
|
|
191
222
|
now: number;
|
|
@@ -199,6 +230,22 @@ export interface StateView {
|
|
|
199
230
|
pausedKeyCount: number;
|
|
200
231
|
bindingsCount: number;
|
|
201
232
|
warnings: string[];
|
|
233
|
+
/**
|
|
234
|
+
* Transport accounts managed beside the pi-ai pool. Additive and optional,
|
|
235
|
+
* so a snapshot from a build without transport support still renders.
|
|
236
|
+
*/
|
|
237
|
+
transportAccounts?: TransportAccountView[];
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Outcome of one per-row connection test: one minimal request through the
|
|
241
|
+
* provider route, with no health or selection side effects.
|
|
242
|
+
*/
|
|
243
|
+
export interface KeyTestResult {
|
|
244
|
+
ok: boolean;
|
|
245
|
+
/** Wall-clock time of the request that was actually sent. */
|
|
246
|
+
latencyMs: number;
|
|
247
|
+
/** Provider failure message, timeout, or the reason no request was sent. */
|
|
248
|
+
error?: string;
|
|
202
249
|
}
|
|
203
250
|
/** One curated extra built-in provider. */
|
|
204
251
|
export interface ExtraProvider {
|
|
@@ -207,6 +254,14 @@ export interface ExtraProvider {
|
|
|
207
254
|
api: string;
|
|
208
255
|
baseURL: string;
|
|
209
256
|
env: string[];
|
|
257
|
+
/**
|
|
258
|
+
* Static request headers this route needs. Maps header name → environment
|
|
259
|
+
* variable name; each value is resolved from the environment at add time.
|
|
260
|
+
* Only non-secret metadata headers are supported: values are persisted as
|
|
261
|
+
* plain profile settings, so a credential-bearing name (`authorization`,
|
|
262
|
+
* `proxy-authorization`, `cookie`, `x-api-key`, `*-api-key`) is refused.
|
|
263
|
+
*/
|
|
264
|
+
headerEnv?: Record<string, string>;
|
|
210
265
|
models: ModelRecord[];
|
|
211
266
|
}
|
|
212
267
|
/** Serializable notice captured on an interactive sign-in attempt. */
|
package/lib/host-types.d.ts
CHANGED
|
@@ -46,6 +46,18 @@ export interface LlmRuntimeLike {
|
|
|
46
46
|
name: string;
|
|
47
47
|
}>;
|
|
48
48
|
discoverModels(settingsNs: string, request: DiscoveryRequest, signal: AbortSignal): Promise<DiscoveredModel[]>;
|
|
49
|
+
/**
|
|
50
|
+
* One model request through the provider route. The host runs the
|
|
51
|
+
* `llm/stream` waterfall, so the manager's own stream listener — and the
|
|
52
|
+
* broker credential wrapper — observe it exactly like a chat request.
|
|
53
|
+
*/
|
|
54
|
+
stream(options: {
|
|
55
|
+
provider: string;
|
|
56
|
+
model: string;
|
|
57
|
+
messages: readonly unknown[];
|
|
58
|
+
maxTokens?: number;
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
}): AsyncIterable<StreamChunkLike>;
|
|
49
61
|
}
|
|
50
62
|
/** Credential value/source facts returned by the credential seam. */
|
|
51
63
|
export interface CredentialHit {
|
|
@@ -126,6 +138,23 @@ export interface CredentialRecordsLike {
|
|
|
126
138
|
modifyRecord(key: string, mutate: (current: CredentialRecordLike | undefined) => Promise<CredentialRecordLike | undefined>): Promise<CredentialRecordLike | undefined>;
|
|
127
139
|
deleteRecord(key: string): Promise<void>;
|
|
128
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* The grant payload the llm-omp transports package writes at
|
|
143
|
+
* `llm-omp-transports/<route>`. Keys Manager only reads it (list/status) and
|
|
144
|
+
* deletes the whole record; the payload stays owned by its writer, so this is
|
|
145
|
+
* a structural shape (the package exports no runtime type for it without
|
|
146
|
+
* pulling its transport code in).
|
|
147
|
+
*/
|
|
148
|
+
export interface TransportGrantLike {
|
|
149
|
+
/** Bearer token the route's adapter sends. */
|
|
150
|
+
access: string;
|
|
151
|
+
/** Refresh token; absent on a grant that cannot be renewed. */
|
|
152
|
+
refresh?: string;
|
|
153
|
+
/** Epoch ms when the access token expires. */
|
|
154
|
+
expires: number;
|
|
155
|
+
email?: string;
|
|
156
|
+
accountId?: string;
|
|
157
|
+
}
|
|
129
158
|
/** One path-addressed settings edit. */
|
|
130
159
|
export type SettingsPathOp = {
|
|
131
160
|
op: 'set';
|