@inventec/dsh-copilot-auth 1.2.8 → 1.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/lib/client.js +61 -14
- package/package.json +2 -2
- package/src/auth-flow.mjs +36 -13
- package/src/auth-host.mjs +11 -1
- package/src/host.mjs +6 -2
package/README.md
CHANGED
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
|
|
34
34
|
| 本插件 | DSH 实测版本 | authorization 服务 | 说明 |
|
|
35
35
|
|---|---|---|---|
|
|
36
|
-
| v1.2.
|
|
36
|
+
| v1.2.9+ | `0.1.7-rc.2`+(`0.2.0-rc.1` web 与 `0.2.0-rc.2` desktop 已实测) | runtime 内置,插件不挂载 | **修复 v1.2.8 授权页显示回归**:授权状态机漏接 `fetch`,页面恒显「未登录」、点「授权登录」恒「失败」且请求根本不发出(2026-10-04 实机实证,nginx 全量零 `POST /copilot-auth/start`)。另加:/status 加载失败退避自愈重试(60 秒转手动+「查询状态」)、失败原因分层展示(网络/网关/响应,不再裸「失败」)、已登录时服务端拒绝替换性授权(409 `already-configured`)且客户端自动恢复已登录视图 |
|
|
37
|
+
| v1.2.8 | `0.1.7-rc.2`+(`0.2.0-rc.1` web 与 `0.2.0-rc.2` desktop 已实测) | runtime 内置,插件不挂载 | **协议 v2:诚实的授权语义+补充/重建双入口**。授权侧:等待中可「请求撤回」(结果如实显示待核实)、退出/重新授权在宿主可证明安全前禁用并说明、单实例 attempt、15 分钟等待上限、晚到授权成功不再写配置;模型侧:旧「刷新」拆为「补充模型」(候选默认不勾选、非破坏)与「重建模型列表」(二次确认+清 modelOverrides),预览 10 分钟绑定选择、operationId 幂等、配置并发编辑优先、崩溃可恢复(「结束旧配置意图」显式闭环),状态按 profile 隔离(旧全局状态只读提示);模型路由要求 `protocolVersion:2`,GET `/status` 零写探针;v1 函数族已从 state.mjs 移除。**已知缺陷**:授权状态机漏接 fetchImpl → 授权页恒「未登录/失败」(v1.2.9 修复) |
|
|
37
38
|
| v1.2.7+ | `0.1.7-rc.2`+(`0.2.0-rc.1` web 与 `0.2.0-rc.2` desktop 已实测) | runtime 内置,插件不挂载 | **重启后自动恢复**:注入后以宿主 `listModels` 复核,未端出即**净零切换**(`displayName` 改后复原;两次真内容变化 ⇒ 宿主必然重建快照,用户配置逐字节不变)触发重建;另加双路取实例 + `registryInstanceMismatch`/`registryVia` 诊断。修复 v1.2.6 的"同值写不改变配置身份 ⇒ 快照不重建" |
|
|
38
39
|
| v1.2.6 | `0.1.7-rc.2`+ | runtime 内置,插件不挂载 | 首次引入宿主侧复核与 `settingsNotServed` 诊断;重启自愈用的"同值触碰"**实测不足**(同值写不改变配置对象身份),重启后模型可能仍不出现——请用 v1.2.7 |
|
|
39
40
|
| v1.2.5 | `0.1.7-rc.2`+(`0.2.0-rc.1` web 与 `0.2.0-rc.2` desktop 已实测) | runtime 内置,插件不挂载 | **desktop 只读形态可刷新**:进程内目录注册表注入(ADR 0003),无需重启、不碰官方产物;`0.2.0-rc.2` desktop **实机端到端已验**(注入条目完成真实推理请求);同批修复 pi-ai ≥0.99.0 的 `chat:` 目录键格式。**已知缺陷**:重启后若宿主快照先于注入定稿,注入条目会被丢弃(v1.2.6 修复) |
|
package/lib/client.js
CHANGED
|
@@ -374,12 +374,12 @@ function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = () => {
|
|
|
374
374
|
timer = null;
|
|
375
375
|
}
|
|
376
376
|
};
|
|
377
|
-
const schedule = (gen, delayMs) => {
|
|
377
|
+
const schedule = (gen, delayMs, fn = () => pollTick(gen)) => {
|
|
378
378
|
if (stale(gen)) return;
|
|
379
379
|
clearTimer();
|
|
380
380
|
timer = clock.setTimeout(() => {
|
|
381
381
|
timer = null;
|
|
382
|
-
void
|
|
382
|
+
void fn();
|
|
383
383
|
}, delayMs);
|
|
384
384
|
};
|
|
385
385
|
function applySnapshot(gen, s) {
|
|
@@ -433,12 +433,32 @@ function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = () => {
|
|
|
433
433
|
async function init() {
|
|
434
434
|
const gen = ++generation;
|
|
435
435
|
clearTimer();
|
|
436
|
+
failures = 0;
|
|
437
|
+
backoffIdx = 0;
|
|
438
|
+
failureWindowStart = null;
|
|
439
|
+
await initAttempt(gen);
|
|
440
|
+
}
|
|
441
|
+
async function initAttempt(gen) {
|
|
436
442
|
const status = await requestJson(fetchImpl, "/copilot-auth/status", { method: "GET" });
|
|
437
443
|
if (stale(gen)) return;
|
|
438
444
|
if (!status.ok) {
|
|
439
|
-
|
|
445
|
+
failures += 1;
|
|
446
|
+
if (failureWindowStart === null) failureWindowStart = clock.now();
|
|
447
|
+
const cumulative = clock.now() - failureWindowStart;
|
|
448
|
+
if (cumulative >= AUTO_RETRY_LIMIT_MS) {
|
|
449
|
+
emit({ phase: "idle", error: status.error?.messageKey, connectivity: "manual" });
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
emit({ phase: "loading", connectivity: "retrying", error: status.error?.messageKey });
|
|
453
|
+
const delayS = BACKOFF_STEPS_S[Math.min(backoffIdx, BACKOFF_STEPS_S.length - 1)];
|
|
454
|
+
backoffIdx += 1;
|
|
455
|
+
schedule(gen, delayS * 1e3, () => initAttempt(gen));
|
|
440
456
|
return;
|
|
441
457
|
}
|
|
458
|
+
failures = 0;
|
|
459
|
+
backoffIdx = 0;
|
|
460
|
+
failureWindowStart = null;
|
|
461
|
+
emit({ connectivity: "online" });
|
|
442
462
|
const body = status.body ?? {};
|
|
443
463
|
if (body.refresh?.scopeAvailable === false) {
|
|
444
464
|
emit({ phase: "unavailable" });
|
|
@@ -449,13 +469,13 @@ function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = () => {
|
|
|
449
469
|
emit({ riskLatch: body.authorization.riskLatch });
|
|
450
470
|
}
|
|
451
471
|
if (body.configured === true) {
|
|
452
|
-
emit({ phase: "authorized" });
|
|
472
|
+
emit({ phase: "authorized", error: void 0 });
|
|
453
473
|
return;
|
|
454
474
|
}
|
|
455
475
|
const r = await requestJson(fetchImpl, "/copilot-auth/state", { method: "GET" });
|
|
456
476
|
if (stale(gen)) return;
|
|
457
477
|
if (!r.ok) {
|
|
458
|
-
emit({ phase: "idle" });
|
|
478
|
+
emit({ phase: "idle", error: r.error?.messageKey, connectivity: "manual" });
|
|
459
479
|
return;
|
|
460
480
|
}
|
|
461
481
|
applySnapshot(gen, r.body);
|
|
@@ -474,6 +494,10 @@ function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = () => {
|
|
|
474
494
|
return;
|
|
475
495
|
}
|
|
476
496
|
const errorCode = r.error?.details?.errorCode;
|
|
497
|
+
if (errorCode === "already-configured") {
|
|
498
|
+
emit({ phase: "authorized", error: void 0, shared: false });
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
477
501
|
if (r.httpStatus === 409) {
|
|
478
502
|
emit({ shared: true });
|
|
479
503
|
beginPolling(gen);
|
|
@@ -498,13 +522,7 @@ function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = () => {
|
|
|
498
522
|
emit({ withdrawalDelivery: "failed", error: r.error?.details?.errorCode ?? r.error?.messageKey });
|
|
499
523
|
}
|
|
500
524
|
function refresh() {
|
|
501
|
-
|
|
502
|
-
clearTimer();
|
|
503
|
-
failures = 0;
|
|
504
|
-
backoffIdx = 0;
|
|
505
|
-
failureWindowStart = null;
|
|
506
|
-
emit({ connectivity: "online" });
|
|
507
|
-
void pollTick(gen);
|
|
525
|
+
void init();
|
|
508
526
|
}
|
|
509
527
|
function dispose() {
|
|
510
528
|
generation += 1;
|
|
@@ -545,6 +563,11 @@ var DICTS = {
|
|
|
545
563
|
copy: "Copy",
|
|
546
564
|
copied: "Copied \u2713",
|
|
547
565
|
unknown: "Unknown error",
|
|
566
|
+
statusUnknown: "Status unknown",
|
|
567
|
+
errNetwork: "Network error: cannot reach the authorization service. Retrying automatically.",
|
|
568
|
+
errGateway: "Gateway error: the DSH service may be restarting. Retrying automatically.",
|
|
569
|
+
errBadResponse: "Unexpected response from the authorization service.",
|
|
570
|
+
errHttp: "The authorization request failed. Check status and retry.",
|
|
548
571
|
// ---- Model management (protocol v2, design §5) ----
|
|
549
572
|
supplement: "Add models",
|
|
550
573
|
rebuild: "Rebuild model list",
|
|
@@ -622,6 +645,11 @@ var DICTS = {
|
|
|
622
645
|
copy: "\u590D\u5236",
|
|
623
646
|
copied: "\u5DF2\u590D\u5236 \u2713",
|
|
624
647
|
unknown: "\u672A\u77E5\u9519\u8BEF",
|
|
648
|
+
statusUnknown: "\u72B6\u6001\u672A\u77E5",
|
|
649
|
+
errNetwork: "\u7F51\u7EDC\u5F02\u5E38\uFF1A\u65E0\u6CD5\u8FDE\u63A5\u6388\u6743\u670D\u52A1\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u8BD5\u3002",
|
|
650
|
+
errGateway: "\u7F51\u5173\u9519\u8BEF\uFF1ADSH \u670D\u52A1\u53EF\u80FD\u6B63\u5728\u91CD\u542F\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u8BD5\u3002",
|
|
651
|
+
errBadResponse: "\u6388\u6743\u670D\u52A1\u8FD4\u56DE\u5F02\u5E38\u54CD\u5E94\u3002",
|
|
652
|
+
errHttp: "\u6388\u6743\u8BF7\u6C42\u5931\u8D25\u3002\u8BF7\u67E5\u8BE2\u72B6\u6001\u540E\u91CD\u8BD5\u3002",
|
|
625
653
|
// ---- 模型管理(协议 v2,设计§5) ----
|
|
626
654
|
supplement: "\u8865\u5145\u6A21\u578B",
|
|
627
655
|
rebuild: "\u91CD\u5EFA\u6A21\u578B\u5217\u8868",
|
|
@@ -681,6 +709,14 @@ function blockedReasonText(t, flags) {
|
|
|
681
709
|
if (reason && t("blockedReasons")?.[reason]) return t("blockedReasons")[reason];
|
|
682
710
|
return t("unavailable");
|
|
683
711
|
}
|
|
712
|
+
function httpErrorText(t, key) {
|
|
713
|
+
return {
|
|
714
|
+
"network-error": t("errNetwork"),
|
|
715
|
+
"bad-gateway": t("errGateway"),
|
|
716
|
+
"bad-response": t("errBadResponse"),
|
|
717
|
+
"http-error": t("errHttp")
|
|
718
|
+
}[key];
|
|
719
|
+
}
|
|
684
720
|
var COPILOT_ICON_SVG = '<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" style="flex:none"><path d="M7.998 15.035c-4.562 0-7.873-2.914-7.998-3.749V9.338c.085-.628.677-1.686 1.588-2.065.013-.07.024-.143.036-.218.029-.183.06-.384.126-.612-.201-.508-.254-1.084-.254-1.656 0-.87.128-1.769.693-2.484.579-.733 1.494-1.124 2.724-1.261 1.206-.134 2.262.034 2.944.765.05.053.096.108.139.165.044-.057.094-.112.143-.165.682-.731 1.738-.899 2.944-.765 1.23.137 2.145.528 2.724 1.261.566.715.693 1.614.693 2.484 0 .572-.053 1.148-.254 1.656.066.228.098.429.126.612.924.385 1.522 1.471 1.591 2.095v1.872c0 .766-3.351 3.795-8.002 3.795Zm0-1.485c2.28 0 4.584-1.11 5.002-1.433V7.862l-.023-.116c-.49.21-1.075.291-1.727.291-1.146 0-2.059-.327-2.71-.991A3.222 3.222 0 0 1 8 6.303a3.24 3.24 0 0 1-.544.743c-.65.664-1.563.991-2.71.991-.652 0-1.236-.081-1.727-.291l-.023.116v4.255c.419.323 2.722 1.433 5.002 1.433ZM6.762 2.83c-.193-.206-.637-.413-1.682-.297-1.019.113-1.479.404-1.713.7-.247.312-.369.789-.369 1.554 0 .793.129 1.171.308 1.371.162.181.519.379 1.442.379.853 0 1.339-.235 1.638-.54.315-.322.527-.827.617-1.553.117-.935-.037-1.395-.241-1.614Zm4.155-.297c-1.044-.116-1.488.091-1.681.297-.204.219-.359.679-.242 1.614.091.726.303 1.231.618 1.553.299.305.784.54 1.638.54.922 0 1.28-.198 1.442-.379.179-.2.308-.578.308-1.371 0-.765-.123-1.242-.37-1.554-.233-.296-.693-.587-1.713-.7Z"/><path d="M6.25 9.037a.75.75 0 0 1 .75.75v1.501a.75.75 0 0 1-1.5 0V9.787a.75.75 0 0 1 .75-.75Zm4.25.75v1.501a.75.75 0 0 1-1.5 0V9.787a.75.75 0 0 1 1.5 0Z"/></svg>';
|
|
685
721
|
function enforceNavIcon(labelText) {
|
|
686
722
|
if (!labelText) return;
|
|
@@ -970,7 +1006,7 @@ function CopilotSection({ t = (key) => DICTS.en[key] ?? key }) {
|
|
|
970
1006
|
const flowRef = (0, import_react.useRef)(flow);
|
|
971
1007
|
const authFlowRef = (0, import_react.useRef)(null);
|
|
972
1008
|
if (!authFlowRef.current && typeof fetch === "function") {
|
|
973
|
-
authFlowRef.current = createAuthFlow({ onState: (s) => setAuth(s) });
|
|
1009
|
+
authFlowRef.current = createAuthFlow({ fetchImpl: fetch, onState: (s) => setAuth(s) });
|
|
974
1010
|
}
|
|
975
1011
|
const drive = (event) => advance(flowRef.current, event, fetch, (next) => {
|
|
976
1012
|
flowRef.current = next;
|
|
@@ -1012,7 +1048,7 @@ function CopilotSection({ t = (key) => DICTS.en[key] ?? key }) {
|
|
|
1012
1048
|
const n = notices.find((x) => x && typeof x.code === "string" && x.code !== "");
|
|
1013
1049
|
return n ? { code: n.code, url: n.url } : null;
|
|
1014
1050
|
})();
|
|
1015
|
-
const badgeText =
|
|
1051
|
+
const badgeText = phase === "risk" ? t("riskBadge") : phase === "loading" ? t("loading") : phase === "idle" && auth.error ? t("statusUnknown") : t(phase);
|
|
1016
1052
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.section, children: [
|
|
1017
1053
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { style: styles.title, children: t("title") }),
|
|
1018
1054
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.intro, children: t("intro") }),
|
|
@@ -1055,6 +1091,17 @@ function CopilotSection({ t = (key) => DICTS.en[key] ?? key }) {
|
|
|
1055
1091
|
t("unavailable")
|
|
1056
1092
|
] }),
|
|
1057
1093
|
phase === "failed" && attempt?.error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.error, children: attempt.error }),
|
|
1094
|
+
phase === "failed" && !attempt?.error && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
1095
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
|
|
1096
|
+
"\u26A0 ",
|
|
1097
|
+
httpErrorText(t, auth.error) ?? t("unknown")
|
|
1098
|
+
] }),
|
|
1099
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.secondary }, onClick: checkStatus, children: t("retryNow") }) })
|
|
1100
|
+
] }),
|
|
1101
|
+
phase === "idle" && auth.error && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
|
|
1102
|
+
"\u26A0 ",
|
|
1103
|
+
httpErrorText(t, auth.error) ?? t("unknown")
|
|
1104
|
+
] }),
|
|
1058
1105
|
phase === "authorized" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.card, children: [
|
|
1059
1106
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }, children: [
|
|
1060
1107
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.secondary, opacity: 0.5, cursor: "not-allowed" }, disabled: true, title: t("authUnsafe"), children: t("logout") }),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inventec/dsh-copilot-auth",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "为内置 Copilot 提供方补设备码登录设置页,预置开箱路由;凭据入内置凭据库,模型目录兜底不覆盖定制。v1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
4
|
+
"description": "为内置 Copilot 提供方补设备码登录设置页,预置开箱路由;凭据入内置凭据库,模型目录兜底不覆盖定制。v1.2.9:修复授权页恒未登录/失败(漏传 fetchImpl)、失败原因分层展示+自愈重试、已登录拒绝替换性授权(协议 v2)。适配 DSH 0.1.7+(≤0.1.5 请用 1.1.x)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "iasi",
|
package/src/auth-flow.mjs
CHANGED
|
@@ -36,12 +36,12 @@ export function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = ()
|
|
|
36
36
|
timer = null;
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
|
-
const schedule = (gen, delayMs) => {
|
|
39
|
+
const schedule = (gen, delayMs, fn = () => pollTick(gen)) => {
|
|
40
40
|
if (stale(gen)) return;
|
|
41
41
|
clearTimer();
|
|
42
42
|
timer = clock.setTimeout(() => {
|
|
43
43
|
timer = null;
|
|
44
|
-
void
|
|
44
|
+
void fn();
|
|
45
45
|
}, delayMs);
|
|
46
46
|
};
|
|
47
47
|
|
|
@@ -100,12 +100,35 @@ export function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = ()
|
|
|
100
100
|
async function init() {
|
|
101
101
|
const gen = ++generation;
|
|
102
102
|
clearTimer();
|
|
103
|
+
failures = 0;
|
|
104
|
+
backoffIdx = 0;
|
|
105
|
+
failureWindowStart = null;
|
|
106
|
+
await initAttempt(gen);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// init 内核:/status 失败时按 1/2/4/8/15 秒退避自动重试,累计 60 秒转手动;
|
|
110
|
+
// 不把加载失败伪装成「未登录」(历史缺陷:502 窗口期打开页面会显示未登录+授权按钮)。
|
|
111
|
+
async function initAttempt(gen) {
|
|
103
112
|
const status = await requestJson(fetchImpl, "/copilot-auth/status", { method: "GET" });
|
|
104
113
|
if (stale(gen)) return;
|
|
105
114
|
if (!status.ok) {
|
|
106
|
-
|
|
115
|
+
failures += 1;
|
|
116
|
+
if (failureWindowStart === null) failureWindowStart = clock.now();
|
|
117
|
+
const cumulative = clock.now() - failureWindowStart;
|
|
118
|
+
if (cumulative >= AUTO_RETRY_LIMIT_MS) {
|
|
119
|
+
emit({ phase: "idle", error: status.error?.messageKey, connectivity: "manual" });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
emit({ phase: "loading", connectivity: "retrying", error: status.error?.messageKey });
|
|
123
|
+
const delayS = BACKOFF_STEPS_S[Math.min(backoffIdx, BACKOFF_STEPS_S.length - 1)];
|
|
124
|
+
backoffIdx += 1;
|
|
125
|
+
schedule(gen, delayS * 1000, () => initAttempt(gen));
|
|
107
126
|
return;
|
|
108
127
|
}
|
|
128
|
+
failures = 0;
|
|
129
|
+
backoffIdx = 0;
|
|
130
|
+
failureWindowStart = null;
|
|
131
|
+
emit({ connectivity: "online" });
|
|
109
132
|
const body = status.body ?? {};
|
|
110
133
|
if (body.refresh?.scopeAvailable === false) {
|
|
111
134
|
emit({ phase: "unavailable" });
|
|
@@ -116,14 +139,14 @@ export function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = ()
|
|
|
116
139
|
emit({ riskLatch: body.authorization.riskLatch });
|
|
117
140
|
}
|
|
118
141
|
if (body.configured === true) {
|
|
119
|
-
emit({ phase: "authorized" });
|
|
142
|
+
emit({ phase: "authorized", error: undefined });
|
|
120
143
|
return;
|
|
121
144
|
}
|
|
122
145
|
// 未配置时回查最近一次 attempt:恢复轮询或如实呈现终态
|
|
123
146
|
const r = await requestJson(fetchImpl, "/copilot-auth/state", { method: "GET" });
|
|
124
147
|
if (stale(gen)) return;
|
|
125
148
|
if (!r.ok) {
|
|
126
|
-
emit({ phase: "idle" });
|
|
149
|
+
emit({ phase: "idle", error: r.error?.messageKey, connectivity: "manual" });
|
|
127
150
|
return;
|
|
128
151
|
}
|
|
129
152
|
applySnapshot(gen, r.body);
|
|
@@ -143,6 +166,12 @@ export function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = ()
|
|
|
143
166
|
return;
|
|
144
167
|
}
|
|
145
168
|
const errorCode = r.error?.details?.errorCode;
|
|
169
|
+
if (errorCode === "already-configured") {
|
|
170
|
+
// 后端确认已有凭据:本页是过期的未登录视图(如短暂 read-error 窗口),
|
|
171
|
+
// 直接恢复已登录视图,不发起注定失败的替换性授权
|
|
172
|
+
emit({ phase: "authorized", error: undefined, shared: false });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
146
175
|
if (r.httpStatus === 409) {
|
|
147
176
|
// 共同尝试:不是失败——展示 shared 并转入对现有 attempt 的轮询
|
|
148
177
|
emit({ shared: true });
|
|
@@ -171,14 +200,8 @@ export function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = ()
|
|
|
171
200
|
}
|
|
172
201
|
|
|
173
202
|
function refresh() {
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
clearTimer();
|
|
177
|
-
failures = 0;
|
|
178
|
-
backoffIdx = 0;
|
|
179
|
-
failureWindowStart = null;
|
|
180
|
-
emit({ connectivity: "online" });
|
|
181
|
-
void pollTick(gen);
|
|
203
|
+
// 手动「查询状态」:完整重跑 init(status→state),可从过期的未登录/失败视图恢复
|
|
204
|
+
void init();
|
|
182
205
|
}
|
|
183
206
|
|
|
184
207
|
function dispose() {
|
package/src/auth-host.mjs
CHANGED
|
@@ -40,12 +40,22 @@ export function createAuthorizationController(ctx, { scope, intentIO, clock = {
|
|
|
40
40
|
if (!scope?.known) throw authError("SCOPE_UNAVAILABLE", "scope-unavailable");
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
function start() {
|
|
43
|
+
async function start() {
|
|
44
44
|
requireScope();
|
|
45
45
|
if (riskLatch) throw authError("AUTH_UNSAFE", "auth-unsafe", { riskLatch });
|
|
46
46
|
if (attempt && LIVE_STATUSES.includes(attempt.status)) {
|
|
47
47
|
throw authError("ATTEMPT_RUNNING", "already running", { attemptId: attempt.attemptId });
|
|
48
48
|
}
|
|
49
|
+
// 初始授权条件(设计 Q21):已有凭据时禁止新的授权尝试——那是对既有授权的
|
|
50
|
+
// 隐式替换(历史实证:已登录状态下点「授权登录」,begin 带着已存在的凭据发起
|
|
51
|
+
// 设备流,attempt 永远停在 starting,最终以「失败」收场)。「重新授权」在宿主
|
|
52
|
+
// 可证明安全前默认禁用。describeRecord 自身失败不阻断(读不到≠存在)。
|
|
53
|
+
try {
|
|
54
|
+
const info = await ctx.credentials.describeRecord(CREDENTIAL_KEY);
|
|
55
|
+
if (info?.configured === true) throw authError("ALREADY_CONFIGURED", "already-configured");
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (err?.code === "ALREADY_CONFIGURED") throw err;
|
|
58
|
+
}
|
|
49
59
|
// 先持久意图版本,失败不发 begin(安全门拒绝动作不产生副作用)
|
|
50
60
|
const originIntentVersion = intentIO.bump();
|
|
51
61
|
const a = {
|
package/src/host.mjs
CHANGED
|
@@ -501,14 +501,14 @@ export function apply(ctx, opts = {}) {
|
|
|
501
501
|
ctx.webServer.register({
|
|
502
502
|
kind: "exact",
|
|
503
503
|
path: r.start,
|
|
504
|
-
handler: (req, res) => {
|
|
504
|
+
handler: async (req, res) => {
|
|
505
505
|
if (!guard(req, res, "POST")) return;
|
|
506
506
|
if (!scope.known) {
|
|
507
507
|
json(res, 503, { ok: false, error: "scope-unavailable" });
|
|
508
508
|
return;
|
|
509
509
|
}
|
|
510
510
|
try {
|
|
511
|
-
const { attemptId } = controller.start();
|
|
511
|
+
const { attemptId } = await controller.start();
|
|
512
512
|
json(res, 202, { ok: true, attemptId });
|
|
513
513
|
} catch (err) {
|
|
514
514
|
if (err?.code === "ATTEMPT_RUNNING") {
|
|
@@ -519,6 +519,10 @@ export function apply(ctx, opts = {}) {
|
|
|
519
519
|
json(res, 409, { ok: false, error: "auth-unsafe" });
|
|
520
520
|
return;
|
|
521
521
|
}
|
|
522
|
+
if (err?.code === "ALREADY_CONFIGURED") {
|
|
523
|
+
json(res, 409, { ok: false, error: "already-configured" });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
522
526
|
json(res, 500, { ok: false, error: sanitizeAuthError(err) });
|
|
523
527
|
}
|
|
524
528
|
},
|