dsh-keys-manager 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -8
- package/README.zh-CN.md +16 -5
- package/lib/client/styles.d.ts +1 -1
- package/lib/client/types.d.ts +47 -4
- package/lib/client.js +549 -37
- package/lib/core.d.ts +44 -0
- package/lib/core.js +44 -0
- package/lib/core.js.map +1 -1
- package/lib/domain.d.ts +79 -4
- package/lib/host-types.d.ts +63 -1
- package/lib/index.d.ts +681 -2
- package/lib/index.js +2423 -48
- package/lib/index.js.map +1 -1
- package/lib/store.js +44 -10
- package/lib/store.js.map +1 -1
- package/locale/en.json +20 -1
- package/locale/zh.json +20 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -97,9 +97,11 @@ var zh = {
|
|
|
97
97
|
failures: "\u5931\u8D25 {n} \u6B21",
|
|
98
98
|
adoptHint: "\u63A5\u7BA1\u540E\uFF0C\u8BE5 Provider \u7684 API Key \u5C06\u7531\u5BC6\u94A5\u7BA1\u7406\u5668\u7EDF\u4E00\u63D0\u4F9B\u3002\u5F53\u524D\u51ED\u8BC1\u4F1A\u5BFC\u5165\u4E3A\u7B2C\u4E00\u4E2A Key\uFF0C\u539F\u5F15\u7528\u4F1A\u4FDD\u7559\u4EE5\u4FBF\u505C\u6B62\u63A5\u7BA1\u65F6\u6062\u590D\u3002",
|
|
99
99
|
adoptExtraHint: "\u8FD9\u4E2A\u6269\u5C55 Provider \u5C1A\u672A\u914D\u7F6E\u3002\u6DFB\u52A0 Key \u540E\uFF0C\u63D2\u4EF6\u4F1A\u81EA\u52A8\u5B89\u88C5 llm-pi-ai \u8DEF\u7531\uFF0C\u5E76\u4F7F\u7528\u81EA\u52A8\u53D1\u73B0\u6216\u5185\u7F6E\u6A21\u578B\u5217\u8868\u3002",
|
|
100
|
+
adoptAccountTitle: "\u4F7F\u7528 OAuth \u8D26\u6237\u767B\u5F55\u540E\u5373\u53EF\u4F7F\u7528\u8BE5 Provider\u3002",
|
|
100
101
|
unsupported: "\u6682\u4E0D\u652F\u6301\u5BC6\u94A5\u6C60",
|
|
101
102
|
bindingBroken: "Provider \u914D\u7F6E\u5DF2\u4E0D\u518D\u6307\u5411\u5BC6\u94A5\u7BA1\u7406\u5668\u3002\u8BF7\u5148\u505C\u6B62\u63A5\u7BA1\u518D\u91CD\u65B0\u63A5\u7BA1\uFF0C\u6216\u5728\u6A21\u578B\u8BBE\u7F6E\u4E2D\u4FEE\u590D\u8BE5 Provider\u3002",
|
|
102
103
|
noKeys: "\u8FD8\u6CA1\u6709\u5BC6\u94A5\u3002\u6DFB\u52A0\u4E00\u4E2A\u540E\u5373\u53EF\u4F7F\u7528\u8BE5 Provider\u3002",
|
|
104
|
+
noKeysAccount: "\u8FD8\u6CA1\u6709\u8D26\u6237\u3002\u6DFB\u52A0\u4E00\u4E2A\u8D26\u6237\u540E\u5373\u53EF\u4F7F\u7528\u8BE5 Provider\u3002",
|
|
103
105
|
warning: "\u6CE8\u610F",
|
|
104
106
|
settingsTitle: "\u7B56\u7565",
|
|
105
107
|
settingsIntro: "\u4EE5\u4E0B\u8BBE\u7F6E\u5BF9\u6240\u6709\u5DF2\u63A5\u7BA1\u7684 Provider \u751F\u6548\u3002",
|
|
@@ -150,6 +152,7 @@ var zh = {
|
|
|
150
152
|
keyStatusCooling: "\u51B7\u5374\u4E2D",
|
|
151
153
|
keyStatusPaused: "\u5DF2\u6682\u505C",
|
|
152
154
|
keyStatusMissing: "\u51ED\u8BC1\u7F3A\u5931",
|
|
155
|
+
keyStatusEmpty: "\u6682\u65E0\u5BC6\u94A5",
|
|
153
156
|
keyStatusUnmanaged: "\u672A\u63A5\u7BA1",
|
|
154
157
|
providerKeys: "{n} \u4E2A\u5BC6\u94A5",
|
|
155
158
|
providerKeyOne: "1 \u4E2A\u5BC6\u94A5",
|
|
@@ -164,7 +167,25 @@ var zh = {
|
|
|
164
167
|
openModels: "\u6253\u5F00\u6A21\u578B\u8BBE\u7F6E",
|
|
165
168
|
searchResults: "{n} \u4E2A\u5339\u914D",
|
|
166
169
|
reset: "\u91CD\u7F6E",
|
|
167
|
-
attention: "\u9700\u8981\u5904\u7406"
|
|
170
|
+
attention: "\u9700\u8981\u5904\u7406",
|
|
171
|
+
// OAuth account surface — same values as locale/zh.json (dual source
|
|
172
|
+
// until the host's dsh-client-locale merge semantics are confirmed).
|
|
173
|
+
addAccount: "\u6DFB\u52A0\u8D26\u6237",
|
|
174
|
+
accountIdentity: "\u8D26\u6237",
|
|
175
|
+
accountProjected: "\u670D\u52A1\u4E2D",
|
|
176
|
+
accountReauth: "\u91CD\u65B0\u8BA4\u8BC1",
|
|
177
|
+
accountActivate: "\u8BBE\u4E3A\u5F53\u524D",
|
|
178
|
+
loginTitle: "\u8D26\u6237\u767B\u5F55",
|
|
179
|
+
loginPending: "\u7B49\u5F85\u6388\u6743\u2026",
|
|
180
|
+
loginBrowserOpen: "\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00",
|
|
181
|
+
loginPromptTitle: "\u5B8C\u6210\u767B\u5F55",
|
|
182
|
+
loginPromptSubmit: "\u63D0\u4EA4",
|
|
183
|
+
loginFailed: "\u767B\u5F55\u5931\u8D25",
|
|
184
|
+
loginFlowEnded: "\u672C\u6B21\u767B\u5F55\u6D41\u7A0B\u5DF2\u7ED3\u675F\uFF0C\u8BF7\u5173\u95ED\u7A97\u53E3\u540E\u91CD\u65B0\u5F00\u59CB\u3002",
|
|
185
|
+
loginReady: "\u767B\u5F55\u6210\u529F\u3002",
|
|
186
|
+
deleteAccountTitle: "\u5220\u9664\u8D26\u6237\u201C{alias}\u201D\uFF1F",
|
|
187
|
+
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"
|
|
168
189
|
};
|
|
169
190
|
var en = {
|
|
170
191
|
nav: "Keys Manager",
|
|
@@ -224,9 +245,11 @@ var en = {
|
|
|
224
245
|
failures: "{n} failures",
|
|
225
246
|
adoptHint: "Adopting turns this provider\u2019s API-key source over to Keys Manager. Its current credential is imported as the first key, and the original reference is kept for release.",
|
|
226
247
|
adoptExtraHint: "This extra provider is not configured yet. Add a key and the plugin installs an llm-pi-ai route with discovered or built-in models.",
|
|
248
|
+
adoptAccountTitle: "Sign in with an OAuth account to use this provider.",
|
|
227
249
|
unsupported: "Not available for key pools",
|
|
228
250
|
bindingBroken: "The provider profile no longer points at Keys Manager. Release and re-adopt it, or repair the provider in Models.",
|
|
229
251
|
noKeys: "No keys yet. Add one to make this provider usable.",
|
|
252
|
+
noKeysAccount: "No accounts yet. Add an account to make this provider usable.",
|
|
230
253
|
warning: "Warning",
|
|
231
254
|
settingsTitle: "Policy",
|
|
232
255
|
settingsIntro: "These values apply to every managed provider.",
|
|
@@ -277,6 +300,7 @@ var en = {
|
|
|
277
300
|
keyStatusCooling: "Cooling",
|
|
278
301
|
keyStatusPaused: "Paused",
|
|
279
302
|
keyStatusMissing: "Credential missing",
|
|
303
|
+
keyStatusEmpty: "No keys",
|
|
280
304
|
keyStatusUnmanaged: "Not managed",
|
|
281
305
|
providerKeys: "{n} keys",
|
|
282
306
|
providerKeyOne: "1 key",
|
|
@@ -291,7 +315,25 @@ var en = {
|
|
|
291
315
|
openModels: "Open Models",
|
|
292
316
|
searchResults: "{n} matches",
|
|
293
317
|
reset: "Reset",
|
|
294
|
-
attention: "Needs attention"
|
|
318
|
+
attention: "Needs attention",
|
|
319
|
+
// OAuth account surface — same values as locale/en.json (dual source
|
|
320
|
+
// until the host's dsh-client-locale merge semantics are confirmed).
|
|
321
|
+
addAccount: "Add account",
|
|
322
|
+
accountIdentity: "Account",
|
|
323
|
+
accountProjected: "Serving provider",
|
|
324
|
+
accountReauth: "Re-authenticate",
|
|
325
|
+
accountActivate: "Set active",
|
|
326
|
+
loginTitle: "Account sign-in",
|
|
327
|
+
loginPending: "Waiting for authorization\u2026",
|
|
328
|
+
loginBrowserOpen: "Open in browser",
|
|
329
|
+
loginPromptTitle: "Complete sign-in",
|
|
330
|
+
loginPromptSubmit: "Submit",
|
|
331
|
+
loginFailed: "Login failed",
|
|
332
|
+
loginFlowEnded: "This sign-in session has ended. Close the dialog and start again.",
|
|
333
|
+
loginReady: "Signed in.",
|
|
334
|
+
deleteAccountTitle: "Delete account \u201C{alias}\u201D?",
|
|
335
|
+
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."
|
|
295
337
|
};
|
|
296
338
|
var LOCALE_DICTS = { zh, en };
|
|
297
339
|
|
|
@@ -348,6 +390,10 @@ var CSS = `
|
|
|
348
390
|
.km-keys{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l1);border-radius:9px;background:var(--dsw-alias-bg-layer-1);overflow:visible}
|
|
349
391
|
.km-key-row{position:relative;display:grid;grid-template-columns:minmax(0,1.35fr) minmax(150px,.8fr) minmax(150px,.9fr) 32px;gap:10px;align-items:center;padding:10px 11px;border-top:1px solid var(--dsw-alias-border-l1)}
|
|
350
392
|
.km-key-row:first-child{border-top:0}
|
|
393
|
+
.km-account-row{position:relative;display:grid;grid-template-columns:minmax(0,1.35fr) minmax(150px,.8fr) minmax(150px,.9fr) 32px;gap:10px;align-items:center;padding:10px 11px 10px 14px;border-top:1px solid var(--dsw-alias-border-l1)}
|
|
394
|
+
.km-account-row:first-child{border-top:0}
|
|
395
|
+
.km-account-row::before{content:'';position:absolute;left:0;top:12px;bottom:12px;width:2px;border-radius:1px;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 55%,transparent)}
|
|
396
|
+
.km-account-glyph{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;flex:0 0 22px;border-radius:50%;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 14%,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-business-primary)}
|
|
351
397
|
.km-key-identity{min-width:0;display:flex;flex-direction:column;gap:2px}
|
|
352
398
|
.km-key-alias-line{display:flex;align-items:center;gap:6px;min-width:0}
|
|
353
399
|
.km-key-alias{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:220px}
|
|
@@ -458,11 +504,18 @@ select.km-input{appearance:auto}
|
|
|
458
504
|
.km-modal-title{font-weight:650;font-size:14px;margin-bottom:7px}
|
|
459
505
|
.km-modal-body{color:var(--dsw-alias-label-secondary);font-size:12.5px}
|
|
460
506
|
.km-modal-copy{margin:0;line-height:1.55}
|
|
507
|
+
.km-login-flow{display:flex;flex-direction:column;gap:10px}
|
|
508
|
+
.km-login-pending{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-secondary);font-size:12.5px}
|
|
509
|
+
.km-login-notices{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5;word-break:break-word}
|
|
510
|
+
.km-login-link{color:var(--dsw-alias-state-business-primary);text-decoration:underline;text-underline-offset:2px}
|
|
511
|
+
.km-login-link:hover{color:var(--dsw-alias-label-primary)}
|
|
512
|
+
.km-login-prompt{display:flex;flex-direction:column;gap:10px;padding-top:2px;border-top:1px dashed var(--dsw-alias-border-l1)}
|
|
513
|
+
.km-login-prompt-title{font-weight:600;font-size:12.5px;color:var(--dsw-alias-label-primary)}
|
|
461
514
|
.km-modal-footer{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}
|
|
462
515
|
@keyframes km-spin{to{transform:rotate(360deg)}}
|
|
463
516
|
@media(max-width:720px){
|
|
464
517
|
.km-summary{grid-template-columns:repeat(2,minmax(0,1fr))}
|
|
465
|
-
.km-key-row{grid-template-columns:minmax(0,1fr) 32px;gap:6px}
|
|
518
|
+
.km-key-row,.km-account-row{grid-template-columns:minmax(0,1fr) 32px;gap:6px}
|
|
466
519
|
.km-key-status,.km-key-last{grid-column:1;padding-left:0}
|
|
467
520
|
.km-key-actions{grid-column:2;grid-row:1}
|
|
468
521
|
.km-settings-grid{grid-template-columns:1fr}
|
|
@@ -556,6 +609,46 @@ function createStore(ctx) {
|
|
|
556
609
|
emit();
|
|
557
610
|
};
|
|
558
611
|
const rpc = (method, args) => ctx.connection.rpc.call("/api", `keysManager/${method}`, { args });
|
|
612
|
+
const send = async (method, args, onSuccess) => {
|
|
613
|
+
queuedMutations += 1;
|
|
614
|
+
update({ busy: method, error: null });
|
|
615
|
+
const run = async () => {
|
|
616
|
+
++loadSequence;
|
|
617
|
+
try {
|
|
618
|
+
const result = await rpc(method, args);
|
|
619
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
620
|
+
const outcome = onSuccess(result.value);
|
|
621
|
+
if (queuedMutations === 1) update({ busy: null });
|
|
622
|
+
return outcome;
|
|
623
|
+
} catch (error) {
|
|
624
|
+
const message = messageOf(error);
|
|
625
|
+
if (queuedMutations === 1) update({ busy: null, error: message });
|
|
626
|
+
else update({ error: message });
|
|
627
|
+
return { ok: false, error: message };
|
|
628
|
+
} finally {
|
|
629
|
+
queuedMutations -= 1;
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
const next = mutationTail.then(run, run);
|
|
633
|
+
mutationTail = next.then(() => void 0, () => void 0);
|
|
634
|
+
return next;
|
|
635
|
+
};
|
|
636
|
+
const stateOutcome = (value) => {
|
|
637
|
+
const state = value;
|
|
638
|
+
if (queuedMutations === 1) update({ status: "ready", state, now: Date.now(), busy: null, error: null });
|
|
639
|
+
else update({ status: "ready", state, now: Date.now(), error: null });
|
|
640
|
+
return { ok: true, value: state };
|
|
641
|
+
};
|
|
642
|
+
const rpcRead = async (method, args) => {
|
|
643
|
+
try {
|
|
644
|
+
return await rpc(method, args);
|
|
645
|
+
} catch (error) {
|
|
646
|
+
return { ok: false, error: { message: messageOf(error) } };
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
const attemptOutcome = (value) => {
|
|
650
|
+
return { ok: true, value: value ?? null };
|
|
651
|
+
};
|
|
559
652
|
return {
|
|
560
653
|
getSnapshot: () => snapshot,
|
|
561
654
|
subscribe(listener) {
|
|
@@ -577,32 +670,29 @@ function createStore(ctx) {
|
|
|
577
670
|
}
|
|
578
671
|
},
|
|
579
672
|
call(method, args) {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
};
|
|
603
|
-
const next = mutationTail.then(run, run);
|
|
604
|
-
mutationTail = next.then(() => void 0, () => void 0);
|
|
605
|
-
return next;
|
|
673
|
+
return send(method, args, stateOutcome);
|
|
674
|
+
},
|
|
675
|
+
beginAccountLogin(provider, alias, targetAccountId) {
|
|
676
|
+
return send("beginAccountLogin", { provider, alias, targetAccountId }, attemptOutcome);
|
|
677
|
+
},
|
|
678
|
+
answerAccountPrompt(attemptId, value) {
|
|
679
|
+
return send("answerAccountPrompt", { attemptId, value }, () => ({ ok: true, value: null }));
|
|
680
|
+
},
|
|
681
|
+
async accountAttempt(attemptId) {
|
|
682
|
+
const result = await rpcRead("accountAttempt", { attemptId });
|
|
683
|
+
if (!result.ok) return { ok: false, error: result.error.message };
|
|
684
|
+
return attemptOutcome(result.value);
|
|
685
|
+
},
|
|
686
|
+
async listAccountAttempts() {
|
|
687
|
+
const result = await rpcRead("listAccountAttempts", {});
|
|
688
|
+
if (!result.ok) return { ok: false, error: result.error.message };
|
|
689
|
+
return { ok: true, value: result.value ?? [] };
|
|
690
|
+
},
|
|
691
|
+
activateAccount(provider, accountId) {
|
|
692
|
+
return send("activateAccount", { provider, accountId }, stateOutcome);
|
|
693
|
+
},
|
|
694
|
+
removeAccount(provider, accountId) {
|
|
695
|
+
return send("removeAccount", { provider, accountId }, stateOutcome);
|
|
606
696
|
},
|
|
607
697
|
tick() {
|
|
608
698
|
update({ now: Date.now() });
|
|
@@ -672,6 +762,17 @@ function providerInitial(provider) {
|
|
|
672
762
|
const source = String(provider?.displayName || provider?.provider || "?").trim();
|
|
673
763
|
return source.length === 0 ? "?" : source.slice(0, 1).toUpperCase();
|
|
674
764
|
}
|
|
765
|
+
function oauthCapable(provider) {
|
|
766
|
+
return provider.nativeAuth?.oauth === true && provider.accountSupported !== false;
|
|
767
|
+
}
|
|
768
|
+
function accountIdentityOf(key, provider) {
|
|
769
|
+
const oauth = key.oauth;
|
|
770
|
+
if (oauth !== null && oauth !== void 0) {
|
|
771
|
+
if (oauth.email !== void 0 && oauth.email.length > 0) return oauth.email;
|
|
772
|
+
if (oauth.accountId !== void 0 && oauth.accountId.length > 0) return oauth.accountId;
|
|
773
|
+
}
|
|
774
|
+
return provider.displayName || provider.provider;
|
|
775
|
+
}
|
|
675
776
|
function statusKey(status) {
|
|
676
777
|
switch (status) {
|
|
677
778
|
case "active":
|
|
@@ -682,6 +783,10 @@ function statusKey(status) {
|
|
|
682
783
|
return "keyStatusPaused";
|
|
683
784
|
case "missing":
|
|
684
785
|
return "keyStatusMissing";
|
|
786
|
+
// A managed route with zero keys (round-3 finding 7): "Not managed"
|
|
787
|
+
// would contradict the Managed badge rendered beside it.
|
|
788
|
+
case "empty":
|
|
789
|
+
return "keyStatusEmpty";
|
|
685
790
|
default:
|
|
686
791
|
return "keyStatusUnmanaged";
|
|
687
792
|
}
|
|
@@ -696,6 +801,8 @@ function statusTone(status) {
|
|
|
696
801
|
return "idle";
|
|
697
802
|
case "missing":
|
|
698
803
|
return "error";
|
|
804
|
+
case "empty":
|
|
805
|
+
return "idle";
|
|
699
806
|
default:
|
|
700
807
|
return "idle";
|
|
701
808
|
}
|
|
@@ -728,6 +835,7 @@ function Icon({ name, size = 16, className }) {
|
|
|
728
835
|
if (name === "refresh") return h("svg", common, h("path", { d: "M20 11a8 8 0 10-2.3 6M20 5v6h-6" }));
|
|
729
836
|
if (name === "warning") return h("svg", common, h("path", { d: "M12 3l9.5 17h-19L12 3zM12 9v5M12 17.2v.1" }));
|
|
730
837
|
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
|
+
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" }));
|
|
731
839
|
return h("svg", common);
|
|
732
840
|
}
|
|
733
841
|
function Button({ variant = "secondary", size = "normal", type = "button", disabled, onClick, children, title }) {
|
|
@@ -1090,6 +1198,154 @@ function KeyRow({ provider, keyRecord, now }) {
|
|
|
1090
1198
|
}, h("p", { className: "km-modal-copy" }, t("deleteBody"))) : null
|
|
1091
1199
|
);
|
|
1092
1200
|
}
|
|
1201
|
+
function AccountRow({ provider, keyRecord, now, onReauthenticate }) {
|
|
1202
|
+
const runtime = useRuntime();
|
|
1203
|
+
const t = useT();
|
|
1204
|
+
const locale = useLocaleId();
|
|
1205
|
+
const key = keyRecord;
|
|
1206
|
+
const identity = accountIdentityOf(key, provider);
|
|
1207
|
+
const [menu, setMenu] = import_react2.default.useState(false);
|
|
1208
|
+
const [confirm, setConfirm] = import_react2.default.useState(false);
|
|
1209
|
+
const [busy, setBusy] = import_react2.default.useState(false);
|
|
1210
|
+
const [error, setError] = import_react2.default.useState(null);
|
|
1211
|
+
const rootRef = import_react2.default.useRef(null);
|
|
1212
|
+
import_react2.default.useEffect(() => {
|
|
1213
|
+
if (!menu) return void 0;
|
|
1214
|
+
const onDown = (event) => {
|
|
1215
|
+
if (rootRef.current !== null && !rootRef.current.contains(event.target)) setMenu(false);
|
|
1216
|
+
};
|
|
1217
|
+
const onKey = (event) => {
|
|
1218
|
+
if (event.key === "Escape") setMenu(false);
|
|
1219
|
+
};
|
|
1220
|
+
document.addEventListener("mousedown", onDown);
|
|
1221
|
+
document.addEventListener("keydown", onKey);
|
|
1222
|
+
return () => {
|
|
1223
|
+
document.removeEventListener("mousedown", onDown);
|
|
1224
|
+
document.removeEventListener("keydown", onKey);
|
|
1225
|
+
};
|
|
1226
|
+
}, [menu]);
|
|
1227
|
+
async function activate() {
|
|
1228
|
+
setBusy(true);
|
|
1229
|
+
setError(null);
|
|
1230
|
+
const result = await runtime.store.activateAccount(provider.provider, key.id);
|
|
1231
|
+
setBusy(false);
|
|
1232
|
+
if (!result.ok) setError(result.error);
|
|
1233
|
+
}
|
|
1234
|
+
async function remove() {
|
|
1235
|
+
setBusy(true);
|
|
1236
|
+
setError(null);
|
|
1237
|
+
const result = await runtime.store.removeAccount(provider.provider, key.id);
|
|
1238
|
+
setBusy(false);
|
|
1239
|
+
if (!result.ok) setError(result.error);
|
|
1240
|
+
else setConfirm(false);
|
|
1241
|
+
}
|
|
1242
|
+
const cold = key.status === "cooling" && key.cooldownUntil > now;
|
|
1243
|
+
const metaParts = [
|
|
1244
|
+
key.totalUses > 0 ? t("uses", { n: key.totalUses }) : null,
|
|
1245
|
+
key.totalFailures > 0 ? t("failures", { n: key.totalFailures }) : null
|
|
1246
|
+
].filter((value) => value !== null);
|
|
1247
|
+
return h(
|
|
1248
|
+
"div",
|
|
1249
|
+
{ className: "km-account-row", ref: rootRef },
|
|
1250
|
+
h(
|
|
1251
|
+
"div",
|
|
1252
|
+
{ className: "km-key-identity" },
|
|
1253
|
+
h(
|
|
1254
|
+
"div",
|
|
1255
|
+
{ className: "km-key-alias-line" },
|
|
1256
|
+
h("span", { className: "km-account-glyph", "aria-hidden": true }, h(Icon, { name: "user", size: 13 })),
|
|
1257
|
+
h("span", { className: "km-key-alias" }, identity),
|
|
1258
|
+
h("span", { className: "km-tag km-tag-accent" }, t("accountIdentity")),
|
|
1259
|
+
// Step 6: the row whose grant IS the live projection is badged.
|
|
1260
|
+
key.projected === true ? h("span", { className: "km-tag km-tag-accent" }, t("accountProjected")) : null
|
|
1261
|
+
),
|
|
1262
|
+
h(
|
|
1263
|
+
"div",
|
|
1264
|
+
{ className: "km-key-sub" },
|
|
1265
|
+
h("span", { className: "km-mono km-muted" }, `\u2022\u2022\u2022\u2022 ${key.id.slice(-4)}`)
|
|
1266
|
+
)
|
|
1267
|
+
),
|
|
1268
|
+
h(
|
|
1269
|
+
"div",
|
|
1270
|
+
{ className: "km-key-status" },
|
|
1271
|
+
h(StatusPill, { status: key.status, t }),
|
|
1272
|
+
cold ? h("div", { className: "km-key-time km-warn" }, t("coolingUntil", { time: formatClock(key.cooldownUntil, locale, t) })) : null,
|
|
1273
|
+
cold ? h("div", { className: "km-key-time" }, t("remaining", { time: durationText(key.cooldownUntil - now, t) })) : null
|
|
1274
|
+
),
|
|
1275
|
+
h(
|
|
1276
|
+
"div",
|
|
1277
|
+
{ className: "km-key-last" },
|
|
1278
|
+
h("div", { className: "km-key-time" }, `${t("lastUsed")}: ${relativeTime(key.lastUsedAt, now, t)}`),
|
|
1279
|
+
metaParts.length > 0 ? h(
|
|
1280
|
+
"div",
|
|
1281
|
+
{ className: "km-key-time km-muted" },
|
|
1282
|
+
metaParts.map((value, index) => [
|
|
1283
|
+
index > 0 ? h("span", { key: `sep-${index}`, className: "km-dot-sep", "aria-hidden": true }, "\xB7") : null,
|
|
1284
|
+
h("span", { key: `part-${index}` }, value)
|
|
1285
|
+
])
|
|
1286
|
+
) : null
|
|
1287
|
+
),
|
|
1288
|
+
h(
|
|
1289
|
+
"div",
|
|
1290
|
+
{ className: "km-key-actions" },
|
|
1291
|
+
h("button", {
|
|
1292
|
+
type: "button",
|
|
1293
|
+
className: "km-icon-btn",
|
|
1294
|
+
"aria-haspopup": "menu",
|
|
1295
|
+
"aria-expanded": menu,
|
|
1296
|
+
title: t("moreActions"),
|
|
1297
|
+
"aria-label": t("moreActions"),
|
|
1298
|
+
onClick: () => setMenu((current) => !current),
|
|
1299
|
+
disabled: busy
|
|
1300
|
+
}, h(Icon, { name: "dots", size: 17 })),
|
|
1301
|
+
menu ? h(
|
|
1302
|
+
"div",
|
|
1303
|
+
{ className: "km-menu", role: "menu" },
|
|
1304
|
+
h("button", {
|
|
1305
|
+
type: "button",
|
|
1306
|
+
className: "km-menu-item",
|
|
1307
|
+
role: "menuitem",
|
|
1308
|
+
onClick: () => {
|
|
1309
|
+
setMenu(false);
|
|
1310
|
+
void activate();
|
|
1311
|
+
},
|
|
1312
|
+
// Step 6: the projected row already serves the route; setting it
|
|
1313
|
+
// active again is a no-op, so the entry stays disabled.
|
|
1314
|
+
disabled: busy || key.projected === true
|
|
1315
|
+
}, h(Icon, { name: "check", size: 15 }), t("accountActivate")),
|
|
1316
|
+
h("button", {
|
|
1317
|
+
type: "button",
|
|
1318
|
+
className: "km-menu-item",
|
|
1319
|
+
role: "menuitem",
|
|
1320
|
+
onClick: () => {
|
|
1321
|
+
setMenu(false);
|
|
1322
|
+
onReauthenticate();
|
|
1323
|
+
},
|
|
1324
|
+
disabled: busy
|
|
1325
|
+
}, h(Icon, { name: "refresh", size: 15 }), t("accountReauth")),
|
|
1326
|
+
h("div", { className: "km-menu-sep" }),
|
|
1327
|
+
h("button", { type: "button", className: "km-menu-item km-menu-danger", role: "menuitem", onClick: () => {
|
|
1328
|
+
setMenu(false);
|
|
1329
|
+
setConfirm(true);
|
|
1330
|
+
}, disabled: busy }, h(Icon, { name: "trash", size: 15 }), t("delete"))
|
|
1331
|
+
) : null
|
|
1332
|
+
),
|
|
1333
|
+
error !== null ? h("div", { className: "km-key-error", role: "alert" }, error) : null,
|
|
1334
|
+
confirm ? h(Modal, {
|
|
1335
|
+
// SF4: account-specific copy — the grant removal consequence, not a
|
|
1336
|
+
// key-deletion/provider-release promise. Step 8: the key-pool
|
|
1337
|
+
// fallback clause appears only when the route actually has key
|
|
1338
|
+
// support; an OAuth-only route states the real consequence instead.
|
|
1339
|
+
title: t("deleteAccountTitle", { alias: identity }),
|
|
1340
|
+
onClose: () => setConfirm(false),
|
|
1341
|
+
busy,
|
|
1342
|
+
footer: [
|
|
1343
|
+
h(Button, { key: "cancel", onClick: () => setConfirm(false), disabled: busy }, t("cancel")),
|
|
1344
|
+
h(Button, { key: "delete", variant: "danger", onClick: remove, disabled: busy }, busy ? t("deleting") : t("deleteConfirm"))
|
|
1345
|
+
]
|
|
1346
|
+
}, h("p", { className: "km-modal-copy" }, t(provider.supported === true ? "deleteAccountBody" : "deleteAccountBodyNoKeys"))) : null
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1093
1349
|
function ProviderCard({ provider, expanded, onToggle, now }) {
|
|
1094
1350
|
const runtime = useRuntime();
|
|
1095
1351
|
const t = useT();
|
|
@@ -1097,6 +1353,8 @@ function ProviderCard({ provider, expanded, onToggle, now }) {
|
|
|
1097
1353
|
const [adopting, setAdopting] = import_react2.default.useState(false);
|
|
1098
1354
|
const [releasing, setReleasing] = import_react2.default.useState(false);
|
|
1099
1355
|
const [confirmRelease, setConfirmRelease] = import_react2.default.useState(false);
|
|
1356
|
+
const [login, setLogin] = import_react2.default.useState(null);
|
|
1357
|
+
const [startingLogin, setStartingLogin] = import_react2.default.useState(false);
|
|
1100
1358
|
const [error, setError] = import_react2.default.useState(null);
|
|
1101
1359
|
const managed = provider.managed;
|
|
1102
1360
|
const broken = managed && provider.bindingHealthy === false;
|
|
@@ -1128,6 +1386,35 @@ function ProviderCard({ provider, expanded, onToggle, now }) {
|
|
|
1128
1386
|
}
|
|
1129
1387
|
setConfirmRelease(false);
|
|
1130
1388
|
}
|
|
1389
|
+
async function openLogin(targetAccountId, alias) {
|
|
1390
|
+
if (startingLogin) return;
|
|
1391
|
+
setStartingLogin(true);
|
|
1392
|
+
setError(null);
|
|
1393
|
+
const result = await runtime.store.beginAccountLogin(provider.provider, alias, targetAccountId);
|
|
1394
|
+
setStartingLogin(false);
|
|
1395
|
+
if (!result.ok) {
|
|
1396
|
+
setError(result.error);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
if (result.value !== null) setLogin({ attemptId: result.value.id });
|
|
1400
|
+
}
|
|
1401
|
+
async function adoptThenLogin() {
|
|
1402
|
+
if (provider.managed) {
|
|
1403
|
+
await openLogin();
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
setAdopting(true);
|
|
1407
|
+
setError(null);
|
|
1408
|
+
const result = await runtime.store.call("adoptProvider", { provider: provider.provider });
|
|
1409
|
+
setAdopting(false);
|
|
1410
|
+
if (!result.ok) {
|
|
1411
|
+
setError(result.error);
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
await openLogin();
|
|
1415
|
+
}
|
|
1416
|
+
const accountAdoptable = oauthCapable(provider);
|
|
1417
|
+
const adoptable = provider.supported || accountAdoptable;
|
|
1131
1418
|
return h(
|
|
1132
1419
|
"div",
|
|
1133
1420
|
{ className: cx("km-card", "km-provider", expanded && "km-provider-open", managed && "km-provider-managed") },
|
|
@@ -1149,7 +1436,7 @@ function ProviderCard({ provider, expanded, onToggle, now }) {
|
|
|
1149
1436
|
h("span", { className: "km-provider-name" }, provider.displayName || provider.provider),
|
|
1150
1437
|
h("span", { className: "km-tag" }, originLabel),
|
|
1151
1438
|
managed ? h("span", { className: "km-tag km-tag-accent" }, t("managedBadge")) : null,
|
|
1152
|
-
!managed && !provider.supported ? h("span", { className: "km-tag km-tag-warn" }, t("unsupported")) : null
|
|
1439
|
+
!managed && !provider.supported && !accountAdoptable ? h("span", { className: "km-tag km-tag-warn" }, t("unsupported")) : null
|
|
1153
1440
|
),
|
|
1154
1441
|
h(
|
|
1155
1442
|
"span",
|
|
@@ -1163,7 +1450,7 @@ function ProviderCard({ provider, expanded, onToggle, now }) {
|
|
|
1163
1450
|
h(
|
|
1164
1451
|
"span",
|
|
1165
1452
|
{ className: "km-provider-side" },
|
|
1166
|
-
managed ? h("span", { className: "km-count" }, countText(t, keyCount, "providerKeyOne", "providerKeys")) : h("span", { className: "km-count km-muted" },
|
|
1453
|
+
managed ? h("span", { className: "km-count" }, countText(t, keyCount, "providerKeyOne", "providerKeys")) : h("span", { className: "km-count km-muted" }, adoptable ? t("unmanaged") : t("unsupported")),
|
|
1167
1454
|
managed ? h(StatusPill, { status: provider.status, t }) : null,
|
|
1168
1455
|
h(Icon, { name: "chevron", size: 16, className: cx("km-chevron", expanded && "km-chevron-open") })
|
|
1169
1456
|
)
|
|
@@ -1179,7 +1466,15 @@ function ProviderCard({ provider, expanded, onToggle, now }) {
|
|
|
1179
1466
|
h(
|
|
1180
1467
|
"div",
|
|
1181
1468
|
{ className: "km-adopt-copy" },
|
|
1182
|
-
|
|
1469
|
+
// I-000063: an OAuth-only route is account-adoptable, so its
|
|
1470
|
+
// adopt panel headline must invite sign-in instead of claiming
|
|
1471
|
+
// the provider is unsupported. The unsupported headline remains
|
|
1472
|
+
// only for providers that are neither key- nor account-capable.
|
|
1473
|
+
h(
|
|
1474
|
+
"div",
|
|
1475
|
+
{ className: "km-adopt-title" },
|
|
1476
|
+
provider.supported ? t(isExtra ? "adoptExtraHint" : "adoptHint") : accountAdoptable ? t("adoptAccountTitle") : t("unsupported")
|
|
1477
|
+
),
|
|
1183
1478
|
!provider.supported && provider.reason !== void 0 ? h("div", { className: "km-muted" }, provider.reason) : null,
|
|
1184
1479
|
provider.apiKeyEnv !== null ? h("div", { className: "km-meta-line" }, h("span", { className: "km-muted" }, `${t("secret")}:`), h("span", { className: "km-mono" }, provider.apiKeyEnv)) : null,
|
|
1185
1480
|
provider.baseURL !== null ? h("div", { className: "km-meta-line" }, h("span", { className: "km-muted" }, `${t("baseUrl")}:`), h("span", { className: "km-mono km-truncate" }, provider.baseURL)) : null,
|
|
@@ -1190,10 +1485,18 @@ function ProviderCard({ provider, expanded, onToggle, now }) {
|
|
|
1190
1485
|
{ className: "km-adopt-actions" },
|
|
1191
1486
|
h(
|
|
1192
1487
|
Button,
|
|
1193
|
-
{ variant: "primary", onClick: adopt, disabled: adopting || !
|
|
1488
|
+
{ variant: "primary", onClick: adopt, disabled: adopting || !adoptable },
|
|
1194
1489
|
adopting ? h(Icon, { name: "refresh", size: 14, className: "km-spin" }) : h(Icon, { name: "shield", size: 14 }),
|
|
1195
1490
|
h("span", null, adopting ? t("adopting") : t("adopt"))
|
|
1196
|
-
)
|
|
1491
|
+
),
|
|
1492
|
+
// MF1: Add account is reachable from the unmanaged/adopt flow —
|
|
1493
|
+
// adopt (zero keys for OAuth-only routes) then sign in.
|
|
1494
|
+
oauthCapable(provider) ? h(
|
|
1495
|
+
Button,
|
|
1496
|
+
{ variant: "secondary", onClick: () => void adoptThenLogin(), disabled: adopting || startingLogin },
|
|
1497
|
+
startingLogin ? h(Icon, { name: "refresh", size: 14, className: "km-spin" }) : h(Icon, { name: "user", size: 14 }),
|
|
1498
|
+
h("span", null, startingLogin ? t("adopting") : t("addAccount"))
|
|
1499
|
+
) : null
|
|
1197
1500
|
)
|
|
1198
1501
|
) : h(
|
|
1199
1502
|
"div",
|
|
@@ -1212,10 +1515,16 @@ function ProviderCard({ provider, expanded, onToggle, now }) {
|
|
|
1212
1515
|
{ className: "km-managed-actions" },
|
|
1213
1516
|
h(
|
|
1214
1517
|
Button,
|
|
1215
|
-
{ variant: "secondary", size: "small", onClick: () => setAdding((current) => !current), disabled: releasing },
|
|
1518
|
+
{ variant: "secondary", size: "small", onClick: () => setAdding((current) => !current), disabled: releasing || !provider.supported, title: provider.supported ? void 0 : provider.reason },
|
|
1216
1519
|
h(Icon, { name: "plus", size: 14 }),
|
|
1217
1520
|
h("span", null, t("addKey"))
|
|
1218
1521
|
),
|
|
1522
|
+
oauthCapable(provider) ? h(
|
|
1523
|
+
Button,
|
|
1524
|
+
{ variant: "secondary", size: "small", onClick: () => void openLogin(), disabled: releasing || startingLogin },
|
|
1525
|
+
h(Icon, { name: "user", size: 14 }),
|
|
1526
|
+
h("span", null, t("addAccount"))
|
|
1527
|
+
) : null,
|
|
1219
1528
|
h(
|
|
1220
1529
|
Button,
|
|
1221
1530
|
{ variant: "ghost", size: "small", onClick: () => setConfirmRelease(true), disabled: releasing },
|
|
@@ -1224,7 +1533,12 @@ function ProviderCard({ provider, expanded, onToggle, now }) {
|
|
|
1224
1533
|
)
|
|
1225
1534
|
),
|
|
1226
1535
|
adding ? h(KeyForm, { provider, onDone: () => setAdding(false), autoFocus: true }) : null,
|
|
1227
|
-
provider.keys.length === 0 ? h(
|
|
1536
|
+
provider.keys.length === 0 ? h(
|
|
1537
|
+
"div",
|
|
1538
|
+
{ className: "km-empty-keys" },
|
|
1539
|
+
h("div", null, provider.supported ? t("noKeys") : oauthCapable(provider) ? t("noKeysAccount") : t("unsupported")),
|
|
1540
|
+
!provider.supported && provider.reason !== void 0 ? h("div", { className: "km-muted" }, provider.reason) : null
|
|
1541
|
+
) : h("div", { className: "km-keys" }, provider.keys.map((key) => key.kind === "oauth" ? h(AccountRow, { key: key.id, provider, keyRecord: key, now, onReauthenticate: () => void openLogin(key.id, key.alias) }) : h(KeyRow, { key: key.id, provider, keyRecord: key, now })))
|
|
1228
1542
|
),
|
|
1229
1543
|
h(
|
|
1230
1544
|
"div",
|
|
@@ -1241,9 +1555,207 @@ function ProviderCard({ provider, expanded, onToggle, now }) {
|
|
|
1241
1555
|
h(Button, { key: "cancel", onClick: () => setConfirmRelease(false), disabled: releasing }, t("cancel")),
|
|
1242
1556
|
h(Button, { key: "release", variant: "danger", onClick: release, disabled: releasing }, releasing ? t("releasing") : t("releaseConfirm"))
|
|
1243
1557
|
]
|
|
1244
|
-
}, h("p", { className: "km-modal-copy" }, t(provider.createdProfile === true ? "releaseBodyCreated" : "releaseBody"))) : null
|
|
1558
|
+
}, h("p", { className: "km-modal-copy" }, t(provider.createdProfile === true ? "releaseBodyCreated" : "releaseBody"))) : null,
|
|
1559
|
+
login !== null ? h(LoginDialog, {
|
|
1560
|
+
provider,
|
|
1561
|
+
attemptId: login.attemptId,
|
|
1562
|
+
onClose: () => setLogin(null)
|
|
1563
|
+
}) : null
|
|
1245
1564
|
);
|
|
1246
1565
|
}
|
|
1566
|
+
var LOGIN_POLL_MS = 1e3;
|
|
1567
|
+
function safeNoticeUrl(url) {
|
|
1568
|
+
if (url === void 0 || url.length === 0) return null;
|
|
1569
|
+
try {
|
|
1570
|
+
const parsed = new URL(url);
|
|
1571
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:" ? url : null;
|
|
1572
|
+
} catch {
|
|
1573
|
+
return null;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
var LOGIN_POLL_FAILURE_LIMIT = 3;
|
|
1577
|
+
function LoginDialog({ provider, attemptId, onClose }) {
|
|
1578
|
+
const runtime = useRuntime();
|
|
1579
|
+
const t = useT();
|
|
1580
|
+
const [attempt, setAttempt] = import_react2.default.useState(null);
|
|
1581
|
+
const [flowEnded, setFlowEnded] = import_react2.default.useState(false);
|
|
1582
|
+
const [pollError, setPollError] = import_react2.default.useState(null);
|
|
1583
|
+
const [answer, setAnswer] = import_react2.default.useState("");
|
|
1584
|
+
const [answering, setAnswering] = import_react2.default.useState(false);
|
|
1585
|
+
const [error, setError] = import_react2.default.useState(null);
|
|
1586
|
+
const pollNowRef = import_react2.default.useRef(null);
|
|
1587
|
+
const onCloseRef = import_react2.default.useRef(onClose);
|
|
1588
|
+
onCloseRef.current = onClose;
|
|
1589
|
+
const prompt = attempt?.prompt;
|
|
1590
|
+
const promptKey = prompt === void 0 ? "" : `${prompt.kind}\0${prompt.message}\0${(prompt.options ?? []).map((option) => option.id).join("\0")}`;
|
|
1591
|
+
import_react2.default.useEffect(() => {
|
|
1592
|
+
const options = prompt !== void 0 && prompt.kind === "select" ? prompt.options : void 0;
|
|
1593
|
+
if (options !== void 0 && options.length > 0) {
|
|
1594
|
+
setAnswer((current) => options.some((option) => option.id === current) ? current : options[0].id);
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
setAnswer("");
|
|
1598
|
+
}, [promptKey]);
|
|
1599
|
+
import_react2.default.useEffect(() => {
|
|
1600
|
+
let stopped = false;
|
|
1601
|
+
let timer;
|
|
1602
|
+
let failures = 0;
|
|
1603
|
+
let pending = 0;
|
|
1604
|
+
let latestUpdatedAt = 0;
|
|
1605
|
+
const isTerminal = (status) => status === "ready" || status === "failed" || status === "cancelled";
|
|
1606
|
+
const poll = async () => {
|
|
1607
|
+
if (stopped) return;
|
|
1608
|
+
pending += 1;
|
|
1609
|
+
try {
|
|
1610
|
+
const result = await runtime.store.accountAttempt(attemptId);
|
|
1611
|
+
if (stopped) return;
|
|
1612
|
+
if (!result.ok) {
|
|
1613
|
+
failures += 1;
|
|
1614
|
+
if (failures >= LOGIN_POLL_FAILURE_LIMIT) {
|
|
1615
|
+
if (timer !== void 0) clearInterval(timer);
|
|
1616
|
+
setPollError(result.error);
|
|
1617
|
+
}
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
failures = 0;
|
|
1621
|
+
if (result.value === null) {
|
|
1622
|
+
if (timer !== void 0) clearInterval(timer);
|
|
1623
|
+
setFlowEnded(true);
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
const view = result.value;
|
|
1627
|
+
if (view.updatedAt < latestUpdatedAt) return;
|
|
1628
|
+
latestUpdatedAt = view.updatedAt;
|
|
1629
|
+
setAttempt((current) => {
|
|
1630
|
+
if (current === null) return view;
|
|
1631
|
+
if (isTerminal(current.status) && !isTerminal(view.status)) return current;
|
|
1632
|
+
if (current.updatedAt > view.updatedAt) return current;
|
|
1633
|
+
if (current.updatedAt === view.updatedAt && current.status === view.status) return current;
|
|
1634
|
+
return view;
|
|
1635
|
+
});
|
|
1636
|
+
if (view.status === "ready") {
|
|
1637
|
+
if (timer !== void 0) clearInterval(timer);
|
|
1638
|
+
await runtime.store.load();
|
|
1639
|
+
if (!stopped) onCloseRef.current();
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
if (view.status === "failed" || view.status === "cancelled") {
|
|
1643
|
+
if (timer !== void 0) clearInterval(timer);
|
|
1644
|
+
}
|
|
1645
|
+
} finally {
|
|
1646
|
+
pending -= 1;
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
const pollNow = () => {
|
|
1650
|
+
void poll();
|
|
1651
|
+
};
|
|
1652
|
+
pollNowRef.current = pollNow;
|
|
1653
|
+
void poll();
|
|
1654
|
+
timer = setInterval(() => {
|
|
1655
|
+
if (pending === 0) void poll();
|
|
1656
|
+
}, LOGIN_POLL_MS);
|
|
1657
|
+
return () => {
|
|
1658
|
+
stopped = true;
|
|
1659
|
+
if (timer !== void 0) clearInterval(timer);
|
|
1660
|
+
};
|
|
1661
|
+
}, [attemptId, runtime]);
|
|
1662
|
+
async function submitAnswer(event) {
|
|
1663
|
+
event.preventDefault();
|
|
1664
|
+
const prompt2 = attempt?.prompt;
|
|
1665
|
+
if (prompt2 === void 0 || answering) return;
|
|
1666
|
+
const value = answer.trim();
|
|
1667
|
+
if (value.length === 0) return;
|
|
1668
|
+
setAnswering(true);
|
|
1669
|
+
setError(null);
|
|
1670
|
+
const result = await runtime.store.answerAccountPrompt(attemptId, value);
|
|
1671
|
+
setAnswering(false);
|
|
1672
|
+
if (!result.ok) {
|
|
1673
|
+
setError(result.error);
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
setAnswer("");
|
|
1677
|
+
pollNowRef.current?.();
|
|
1678
|
+
}
|
|
1679
|
+
const lastNotice = attempt !== null && attempt.notices.length > 0 ? attempt.notices[attempt.notices.length - 1] : null;
|
|
1680
|
+
const safeUrl = attempt !== null ? attempt.notices.map((notice) => safeNoticeUrl(notice.url)).reverse().find((url) => url !== null) ?? null : null;
|
|
1681
|
+
const answerable = prompt !== void 0 && !answering && answer.trim().length > 0;
|
|
1682
|
+
let body;
|
|
1683
|
+
if (pollError !== null) {
|
|
1684
|
+
body = h(Notice, { tone: "error", icon: "warning", title: t("loginFailed") }, pollError);
|
|
1685
|
+
} else if (flowEnded) {
|
|
1686
|
+
body = h(Notice, { tone: "warn", icon: "warning" }, t("loginFlowEnded"));
|
|
1687
|
+
} else if (attempt === null || attempt.status === "pending") {
|
|
1688
|
+
body = h(
|
|
1689
|
+
"div",
|
|
1690
|
+
{ className: "km-login-flow" },
|
|
1691
|
+
h("div", { className: "km-login-pending" }, h(Icon, { name: "refresh", size: 15, className: "km-spin" }), h("span", null, t("loginPending"))),
|
|
1692
|
+
lastNotice !== null ? h("div", { className: "km-login-notices" }, lastNotice.message) : null,
|
|
1693
|
+
safeUrl !== null ? h(
|
|
1694
|
+
"div",
|
|
1695
|
+
{ className: "km-login-notices" },
|
|
1696
|
+
h("a", { className: "km-login-link", href: safeUrl, target: "_blank", rel: "noreferrer" }, t("loginBrowserOpen"))
|
|
1697
|
+
) : null
|
|
1698
|
+
);
|
|
1699
|
+
} else if (attempt.status === "ready") {
|
|
1700
|
+
body = h("div", { className: "km-login-pending" }, h(Icon, { name: "check", size: 15 }), h("span", null, t("loginReady")));
|
|
1701
|
+
} else if (attempt.status === "failed" || attempt.status === "cancelled") {
|
|
1702
|
+
body = h(Notice, { tone: "error", icon: "warning", title: t("loginFailed") }, attempt.error ?? lastNotice?.message ?? provider.displayName);
|
|
1703
|
+
} else {
|
|
1704
|
+
body = h(
|
|
1705
|
+
"div",
|
|
1706
|
+
{ className: "km-login-flow" },
|
|
1707
|
+
lastNotice !== null ? h("div", { className: "km-login-notices" }, lastNotice.message) : null,
|
|
1708
|
+
safeUrl !== null ? h(
|
|
1709
|
+
"div",
|
|
1710
|
+
{ className: "km-login-notices" },
|
|
1711
|
+
h("a", { className: "km-login-link", href: safeUrl, target: "_blank", rel: "noreferrer" }, t("loginBrowserOpen"))
|
|
1712
|
+
) : null,
|
|
1713
|
+
prompt !== void 0 ? h(
|
|
1714
|
+
"form",
|
|
1715
|
+
{ className: "km-login-prompt", onSubmit: submitAnswer },
|
|
1716
|
+
h("div", { className: "km-login-prompt-title" }, t("loginPromptTitle")),
|
|
1717
|
+
h(
|
|
1718
|
+
"div",
|
|
1719
|
+
{ className: "km-field" },
|
|
1720
|
+
h("span", { className: "km-field-label" }, prompt.message),
|
|
1721
|
+
prompt.kind === "select" && prompt.options !== void 0 ? h("select", {
|
|
1722
|
+
className: "km-input",
|
|
1723
|
+
value: answer,
|
|
1724
|
+
onChange: (event) => setAnswer(event.target.value),
|
|
1725
|
+
disabled: answering,
|
|
1726
|
+
autoFocus: true
|
|
1727
|
+
}, prompt.options.map((option) => h("option", { key: option.id, value: option.id }, option.label))) : h("input", {
|
|
1728
|
+
className: "km-input",
|
|
1729
|
+
type: prompt.kind === "secret" ? "password" : "text",
|
|
1730
|
+
value: answer,
|
|
1731
|
+
onChange: (event) => setAnswer(event.target.value),
|
|
1732
|
+
placeholder: prompt.placeholder,
|
|
1733
|
+
spellCheck: false,
|
|
1734
|
+
disabled: answering,
|
|
1735
|
+
autoFocus: true
|
|
1736
|
+
})
|
|
1737
|
+
),
|
|
1738
|
+
error !== null ? h("div", { className: "km-inline-error", role: "alert" }, error) : null,
|
|
1739
|
+
h(
|
|
1740
|
+
"div",
|
|
1741
|
+
{ className: "km-form-actions" },
|
|
1742
|
+
h(
|
|
1743
|
+
Button,
|
|
1744
|
+
{ variant: "primary", type: "submit", disabled: !answerable },
|
|
1745
|
+
answering ? h(Icon, { name: "refresh", size: 14, className: "km-spin" }) : h(Icon, { name: "check", size: 14 }),
|
|
1746
|
+
h("span", null, answering ? t("loginPending") : t("loginPromptSubmit"))
|
|
1747
|
+
)
|
|
1748
|
+
)
|
|
1749
|
+
) : null
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
return h(Modal, {
|
|
1753
|
+
title: t("loginTitle"),
|
|
1754
|
+
onClose,
|
|
1755
|
+
busy: false,
|
|
1756
|
+
footer: h(Button, { onClick: onClose, disabled: answering }, t("close"))
|
|
1757
|
+
}, body);
|
|
1758
|
+
}
|
|
1247
1759
|
function SettingsCard() {
|
|
1248
1760
|
const runtime = useRuntime();
|
|
1249
1761
|
const t = useT();
|