@inventec/dsh-copilot-auth 1.2.9 → 1.2.11
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 +5 -3
- package/lib/client.js +73 -8
- package/package.json +2 -2
- package/src/auth-flow.mjs +22 -1
- package/src/auth-host.mjs +31 -5
- package/src/host.mjs +34 -5
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
## 特性
|
|
18
18
|
|
|
19
19
|
- 🔐 **设备码登录**:网页 + user code,免 API Token;SSO 组织授权友好
|
|
20
|
-
- 🛑 **诚实的授权语义(v2
|
|
20
|
+
- 🛑 **诚实的授权语义(v2)**:登录进行中可「**请求撤回**」——请求发出后状态如实显示「结果待核实」,不再伪装成已取消;**「退出登录」在静止状态可用**(确认弹窗 → 清除凭据 → 删后核实;尝试进行中/风险锁存期拒绝);**「重新授权」按钮已移除**(v1.2.11:已登录只需退出登录,未登录入口本就是授权登录——独立按钮属逻辑冗余);同一实例只有一个授权尝试,409 时恢复展示共同进度
|
|
21
21
|
- 🧩 **预置提供方路由**:安装即出现在 Models 页,无需手动配置
|
|
22
22
|
- 📦 **模型目录兜底填充**:登录成功时,若 GitHub Copilot 路由还没有模型目录,自动填入账号全部可用模型;你已精简/定制过的目录**永不覆盖**(重启、重新登录均不重置);已请求撤回后晚到的授权成功**不会**再写你的配置
|
|
23
23
|
- ➕➖ **补充模型 / 重建模型列表(v2 双入口)**:「补充」默认非破坏——候选模型默认全不勾选,勾选后才加入,现有对象/顺序/modelOverrides 全部保留;「重建」显式确认后按「账号可用 ∩ 目录可解析」重建纯 ID 列表并清空本路由 modelOverrides(二次确认 + 空目标单独确认)。预览快照 10 分钟有效、绑定选择;同一操作幂等可查;配置并发编辑优先受保护
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
|
|
34
34
|
| 本插件 | DSH 实测版本 | authorization 服务 | 说明 |
|
|
35
35
|
|---|---|---|---|
|
|
36
|
-
| v1.2.
|
|
36
|
+
| v1.2.11+ | `0.1.7-rc.2`+ | runtime 内置,插件不挂载 | **界面收敛:移除「重新授权」按钮与 `capabilities.reauthorize` 能力位**(逻辑冗余——已登录只需退出登录,未登录入口本就是「授权登录」;换号/重授权=先退出再登录两步)。其余同 v1.2.10 |
|
|
37
|
+
| v1.2.10 | `0.1.7-rc.2`+ | runtime 内置,插件不挂载 | **退出登录正式可用**:无进行中授权尝试且无未核实风险(风险锁存)时,经确认弹窗清除凭据并**删除后立即复核**(凭据未消失则如实报「退出未完成」);授权尝试进行中(begin 晚写窗口)与撤回/超时未核实期一律拒绝退出。`/status` 的 `authorization.capabilities.logout` 按可用性动态返回。经主人 2026-10-05 裁决替代 v1.2.8 D-01「退出全禁」策略 |
|
|
38
|
+
| 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
39
|
| 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 修复) |
|
|
38
40
|
| 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 的"同值写不改变配置身份 ⇒ 快照不重建" |
|
|
39
41
|
| v1.2.6 | `0.1.7-rc.2`+ | runtime 内置,插件不挂载 | 首次引入宿主侧复核与 `settingsNotServed` 诊断;重启自愈用的"同值触碰"**实测不足**(同值写不改变配置对象身份),重启后模型可能仍不出现——请用 v1.2.7 |
|
|
@@ -76,7 +78,7 @@ dsh plugin --profile web add @inventec/dsh-copilot-auth
|
|
|
76
78
|
## 撤回、退出与卸载
|
|
77
79
|
|
|
78
80
|
- **请求撤回**:授权等待期间可点「请求撤回」——插件先使本地旧意图失效,再尝试调用宿主撤销接口;无论请求是否送达,状态都如实显示「结果仍待核实」,在结果核实前不能发起新的授权尝试。
|
|
79
|
-
-
|
|
81
|
+
- **退出登录(v1.2.10)**:无进行中授权尝试且无未核实风险时可退出——确认弹窗(将清除同一凭据库中所有实例共享的 Copilot 授权,不注销 GitHub 账号)→ 删除凭据 → **删除后立即复核**,凭据仍在则如实显示「退出未完成,授权凭据仍可能存在」;授权尝试进行中(设备码等待期,begin 晚写窗口)与撤回/超时未核实的风险锁存期一律拒绝退出。**「重新授权」独立按钮已移除(v1.2.11)**:需要换号/重授权时,先「退出登录」再「授权登录」即可。
|
|
80
82
|
- 卸载:`dsh plugin --profile web remove @inventec/dsh-copilot-auth` 后重启 dsh web
|
|
81
83
|
|
|
82
84
|
## 补充模型 / 重建模型列表
|
package/lib/client.js
CHANGED
|
@@ -521,6 +521,23 @@ function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = () => {
|
|
|
521
521
|
}
|
|
522
522
|
emit({ withdrawalDelivery: "failed", error: r.error?.details?.errorCode ?? r.error?.messageKey });
|
|
523
523
|
}
|
|
524
|
+
async function logout() {
|
|
525
|
+
const gen = ++generation;
|
|
526
|
+
clearTimer();
|
|
527
|
+
failures = 0;
|
|
528
|
+
backoffIdx = 0;
|
|
529
|
+
failureWindowStart = null;
|
|
530
|
+
emit({ phase: "loading", logoutError: void 0 });
|
|
531
|
+
const r = await requestJson(fetchImpl, "/copilot-auth/logout", { method: "POST" });
|
|
532
|
+
if (stale(gen)) return;
|
|
533
|
+
if (!r.ok) {
|
|
534
|
+
const code = r.error?.details?.errorCode;
|
|
535
|
+
emit({ phase: "authorized", logoutError: code ?? r.error?.messageKey ?? "logout-failed" });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
emit({ logoutError: void 0 });
|
|
539
|
+
await initAttempt(gen);
|
|
540
|
+
}
|
|
524
541
|
function refresh() {
|
|
525
542
|
void init();
|
|
526
543
|
}
|
|
@@ -528,7 +545,7 @@ function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = () => {
|
|
|
528
545
|
generation += 1;
|
|
529
546
|
clearTimer();
|
|
530
547
|
}
|
|
531
|
-
return { init, start, cancel, refresh, dispose };
|
|
548
|
+
return { init, start, cancel, logout, refresh, dispose };
|
|
532
549
|
}
|
|
533
550
|
|
|
534
551
|
// src/client.jsx
|
|
@@ -547,7 +564,6 @@ var DICTS = {
|
|
|
547
564
|
loading: "\u2026",
|
|
548
565
|
login: "Sign in",
|
|
549
566
|
logout: "Sign out",
|
|
550
|
-
reauthorize: "Reauthorize",
|
|
551
567
|
withdrawAuth: "Request withdrawal",
|
|
552
568
|
withdrawal: "Withdrawal requested; the outcome remains unverified.",
|
|
553
569
|
withdrawalUnavailable: "The withdrawal request could not be sent to the host; the outcome remains unverified.",
|
|
@@ -568,6 +584,8 @@ var DICTS = {
|
|
|
568
584
|
errGateway: "Gateway error: the DSH service may be restarting. Retrying automatically.",
|
|
569
585
|
errBadResponse: "Unexpected response from the authorization service.",
|
|
570
586
|
errHttp: "The authorization request failed. Check status and retry.",
|
|
587
|
+
logoutScope: "This removes Copilot authorization from this credential store and affects other instances using it. It does not delete your GitHub account.",
|
|
588
|
+
logoutFailed: "Sign-out did not complete; authorization credentials may remain. Please retry.",
|
|
571
589
|
// ---- Model management (protocol v2, design §5) ----
|
|
572
590
|
supplement: "Add models",
|
|
573
591
|
rebuild: "Rebuild model list",
|
|
@@ -630,7 +648,6 @@ var DICTS = {
|
|
|
630
648
|
loading: "\u2026",
|
|
631
649
|
login: "\u6388\u6743\u767B\u5F55",
|
|
632
650
|
logout: "\u9000\u51FA\u767B\u5F55",
|
|
633
|
-
reauthorize: "\u91CD\u65B0\u6388\u6743",
|
|
634
651
|
withdrawAuth: "\u8BF7\u6C42\u64A4\u56DE",
|
|
635
652
|
withdrawal: "\u64A4\u56DE\u8BF7\u6C42\u5DF2\u53D1\u9001\uFF0C\u7ED3\u679C\u4ECD\u5F85\u6838\u5B9E\u3002",
|
|
636
653
|
withdrawalUnavailable: "\u672A\u80FD\u5411\u5BBF\u4E3B\u53D1\u9001\u64A4\u56DE\u8BF7\u6C42\uFF0C\u7ED3\u679C\u4ECD\u5F85\u6838\u5B9E\u3002",
|
|
@@ -650,6 +667,8 @@ var DICTS = {
|
|
|
650
667
|
errGateway: "\u7F51\u5173\u9519\u8BEF\uFF1ADSH \u670D\u52A1\u53EF\u80FD\u6B63\u5728\u91CD\u542F\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u8BD5\u3002",
|
|
651
668
|
errBadResponse: "\u6388\u6743\u670D\u52A1\u8FD4\u56DE\u5F02\u5E38\u54CD\u5E94\u3002",
|
|
652
669
|
errHttp: "\u6388\u6743\u8BF7\u6C42\u5931\u8D25\u3002\u8BF7\u67E5\u8BE2\u72B6\u6001\u540E\u91CD\u8BD5\u3002",
|
|
670
|
+
logoutScope: "\u5C06\u6E05\u9664\u6B64\u51ED\u636E\u5E93\u4E2D\u7684 Copilot \u6388\u6743\uFF0C\u4F7F\u7528\u540C\u4E00\u51ED\u636E\u5E93\u7684\u5176\u4ED6\u5B9E\u4F8B\u4E5F\u4F1A\u53D7\u5230\u5F71\u54CD\u3002\u6B64\u64CD\u4F5C\u4E0D\u6CE8\u9500 GitHub \u8D26\u53F7\u3002",
|
|
671
|
+
logoutFailed: "\u9000\u51FA\u767B\u5F55\u672A\u5B8C\u6210\uFF0C\u6388\u6743\u51ED\u636E\u4ECD\u53EF\u80FD\u5B58\u5728\u3002\u8BF7\u91CD\u8BD5\u3002",
|
|
653
672
|
// ---- 模型管理(协议 v2,设计§5) ----
|
|
654
673
|
supplement: "\u8865\u5145\u6A21\u578B",
|
|
655
674
|
rebuild: "\u91CD\u5EFA\u6A21\u578B\u5217\u8868",
|
|
@@ -998,10 +1017,29 @@ function RetireDialog({ t, onConfirm, onCancel }) {
|
|
|
998
1017
|
] })
|
|
999
1018
|
] }) });
|
|
1000
1019
|
}
|
|
1020
|
+
function ConfirmDialog({ title, body, confirmText, cancelText, onConfirm, onCancel }) {
|
|
1021
|
+
const cancelRef = (0, import_react.useRef)(null);
|
|
1022
|
+
const surface = (0, import_react.useRef)(null);
|
|
1023
|
+
if (!surface.current) surface.current = sampleThemeSurface();
|
|
1024
|
+
(0, import_react.useEffect)(() => {
|
|
1025
|
+
cancelRef.current?.focus();
|
|
1026
|
+
}, []);
|
|
1027
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.modalMask, role: "dialog", "aria-modal": "true", onKeyDown: (e) => {
|
|
1028
|
+
if (e.key === "Escape") onCancel();
|
|
1029
|
+
}, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { ...styles.modal, background: surface.current.bg, color: surface.current.fg, maxWidth: 460 }, children: [
|
|
1030
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h4", { style: styles.modalTitle, children: title }),
|
|
1031
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.modalText, children: body }),
|
|
1032
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.modalActions, children: [
|
|
1033
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { ref: cancelRef, type: "button", style: { ...styles.button, ...styles.secondary }, onClick: onCancel, children: cancelText }),
|
|
1034
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.primary }, onClick: onConfirm, children: confirmText })
|
|
1035
|
+
] })
|
|
1036
|
+
] }) });
|
|
1037
|
+
}
|
|
1001
1038
|
function CopilotSection({ t = (key) => DICTS.en[key] ?? key }) {
|
|
1002
1039
|
const [auth, setAuth] = (0, import_react.useState)({ phase: "loading", connectivity: "online", shared: false });
|
|
1003
1040
|
const [copied, setCopied] = (0, import_react.useState)(false);
|
|
1004
1041
|
const [copyFail, setCopyFail] = (0, import_react.useState)(false);
|
|
1042
|
+
const [logoutConfirm, setLogoutConfirm] = (0, import_react.useState)(false);
|
|
1005
1043
|
const [flow, setFlow] = (0, import_react.useState)(initial);
|
|
1006
1044
|
const flowRef = (0, import_react.useRef)(flow);
|
|
1007
1045
|
const authFlowRef = (0, import_react.useRef)(null);
|
|
@@ -1031,6 +1069,10 @@ function CopilotSection({ t = (key) => DICTS.en[key] ?? key }) {
|
|
|
1031
1069
|
const checkStatus = () => {
|
|
1032
1070
|
void authFlowRef.current?.refresh();
|
|
1033
1071
|
};
|
|
1072
|
+
const doLogout = () => {
|
|
1073
|
+
setLogoutConfirm(false);
|
|
1074
|
+
void authFlowRef.current?.logout();
|
|
1075
|
+
};
|
|
1034
1076
|
const copyCode = async (code) => {
|
|
1035
1077
|
try {
|
|
1036
1078
|
await navigator.clipboard.writeText(code);
|
|
@@ -1104,8 +1146,20 @@ function CopilotSection({ t = (key) => DICTS.en[key] ?? key }) {
|
|
|
1104
1146
|
] }),
|
|
1105
1147
|
phase === "authorized" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.card, children: [
|
|
1106
1148
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }, children: [
|
|
1107
|
-
|
|
1108
|
-
|
|
1149
|
+
(() => {
|
|
1150
|
+
const logoutAvailable = auth.status?.authorization?.capabilities?.logout === true;
|
|
1151
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1152
|
+
"button",
|
|
1153
|
+
{
|
|
1154
|
+
type: "button",
|
|
1155
|
+
style: { ...styles.button, ...styles.secondary, ...logoutAvailable ? null : { opacity: 0.5, cursor: "not-allowed" } },
|
|
1156
|
+
disabled: !logoutAvailable,
|
|
1157
|
+
title: logoutAvailable ? t("logoutScope") : t("authUnsafe"),
|
|
1158
|
+
onClick: () => setLogoutConfirm(true),
|
|
1159
|
+
children: t("logout")
|
|
1160
|
+
}
|
|
1161
|
+
);
|
|
1162
|
+
})(),
|
|
1109
1163
|
["previewing", "applying", "checking", "retiring"].includes(flow.name) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.primary, opacity: 0.5 }, disabled: true, children: flow.name === "applying" ? t("applying") : t("refreshing") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
1110
1164
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1111
1165
|
"button",
|
|
@@ -1129,9 +1183,9 @@ function CopilotSection({ t = (key) => DICTS.en[key] ?? key }) {
|
|
|
1129
1183
|
)
|
|
1130
1184
|
] })
|
|
1131
1185
|
] }),
|
|
1132
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.
|
|
1133
|
-
"\
|
|
1134
|
-
t("authUnsafe")
|
|
1186
|
+
auth.logoutError && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.error, children: [
|
|
1187
|
+
"\u26A0 ",
|
|
1188
|
+
auth.logoutError === "logout-unsafe" ? t("authUnsafe") : t("logoutFailed")
|
|
1135
1189
|
] }),
|
|
1136
1190
|
refreshBlocked(flow.flags) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
|
|
1137
1191
|
"\u26A0 ",
|
|
@@ -1204,6 +1258,17 @@ function CopilotSection({ t = (key) => DICTS.en[key] ?? key }) {
|
|
|
1204
1258
|
onCancel: () => drive({ type: "cancel" })
|
|
1205
1259
|
}
|
|
1206
1260
|
),
|
|
1261
|
+
logoutConfirm && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1262
|
+
ConfirmDialog,
|
|
1263
|
+
{
|
|
1264
|
+
title: t("logout"),
|
|
1265
|
+
body: t("logoutScope"),
|
|
1266
|
+
confirmText: t("logout"),
|
|
1267
|
+
cancelText: t("cancel"),
|
|
1268
|
+
onCancel: () => setLogoutConfirm(false),
|
|
1269
|
+
onConfirm: doLogout
|
|
1270
|
+
}
|
|
1271
|
+
),
|
|
1207
1272
|
(phase === "idle" || phase === "failed") && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.primary }, onClick: login, children: t("login") }) })
|
|
1208
1273
|
] });
|
|
1209
1274
|
}
|
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.11",
|
|
4
|
+
"description": "为内置 Copilot 提供方补设备码登录设置页,预置开箱路由;凭据入内置凭据库,模型目录兜底不覆盖定制。v1.2.11:移除「重新授权」按钮与能力位(已登录只需退出登录,未登录入口本就是授权登录)。v1.2.10:退出登录正式可用(协议 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
|
@@ -199,6 +199,27 @@ export function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = ()
|
|
|
199
199
|
emit({ withdrawalDelivery: "failed", error: r.error?.details?.errorCode ?? r.error?.messageKey });
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
async function logout() {
|
|
203
|
+
// 退出登录(写操作,单次调用):pending 态 → 成功后完整重跑 init 恢复真实
|
|
204
|
+
// 状态(若晚到写把凭据写了回来,init 会如实显示 authorized,绝不伪报已退出);
|
|
205
|
+
// 失败回到 authorized 并保留 logoutError 供 UI 分层展示。
|
|
206
|
+
const gen = ++generation;
|
|
207
|
+
clearTimer();
|
|
208
|
+
failures = 0;
|
|
209
|
+
backoffIdx = 0;
|
|
210
|
+
failureWindowStart = null;
|
|
211
|
+
emit({ phase: "loading", logoutError: undefined });
|
|
212
|
+
const r = await requestJson(fetchImpl, "/copilot-auth/logout", { method: "POST" });
|
|
213
|
+
if (stale(gen)) return;
|
|
214
|
+
if (!r.ok) {
|
|
215
|
+
const code = r.error?.details?.errorCode;
|
|
216
|
+
emit({ phase: "authorized", logoutError: code ?? r.error?.messageKey ?? "logout-failed" });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
emit({ logoutError: undefined });
|
|
220
|
+
await initAttempt(gen);
|
|
221
|
+
}
|
|
222
|
+
|
|
202
223
|
function refresh() {
|
|
203
224
|
// 手动「查询状态」:完整重跑 init(status→state),可从过期的未登录/失败视图恢复
|
|
204
225
|
void init();
|
|
@@ -209,5 +230,5 @@ export function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = ()
|
|
|
209
230
|
clearTimer();
|
|
210
231
|
}
|
|
211
232
|
|
|
212
|
-
return { init, start, cancel, refresh, dispose };
|
|
233
|
+
return { init, start, cancel, logout, refresh, dispose };
|
|
213
234
|
}
|
package/src/auth-host.mjs
CHANGED
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
// 契约见 docs/plans/2026-10-04-copilot-auth-models-unified-implementation-plan.md:
|
|
4
4
|
// - start:先 intentIO.bump() 持久 intentVersion+1(失败不发 begin),再 begin;202 先行语义保留
|
|
5
5
|
// - cancel:先 bump,再调用宿主撤销(invoked/unavailable/failed),设进程内 riskLatch
|
|
6
|
-
// - logout
|
|
6
|
+
// - logout:静止态(无 LIVE attempt+无 riskLatch)deleteRecord+删后核实;
|
|
7
|
+
// 尝试进行中/风险锁存期拒绝。「重新授权」概念随 v1.2.11 移除(退出+登录分步即可)
|
|
7
8
|
// - onAuthorized(handoff) 仅在 outcome=authorized 且 originIntentVersion 仍当前且无撤回/超时标记时触发
|
|
8
9
|
// - riskLatch 进程级:重挂/dispose 不清;15 分钟等待超时由 snapshot 以注入 clock 惰性判定
|
|
9
10
|
import { randomUUID } from "node:crypto";
|
|
10
11
|
import { CREDENTIAL_KEY } from "./shared.mjs";
|
|
11
12
|
|
|
12
13
|
export const WAIT_LIMIT_MS = 15 * 60 * 1000;
|
|
13
|
-
|
|
14
|
+
// 进行中的授权尝试状态:begin 的晚写窗口,期间退出/撤回类操作一律拒绝
|
|
15
|
+
export const LIVE_STATUSES = ["starting", "waiting", "finishing"];
|
|
14
16
|
|
|
15
17
|
// 授权错误脱敏:仅保留安全字符集(字母/数字/空格/点/冒号/逗号/连字符),
|
|
16
18
|
// 阻断上游异常文本或疑似 token(含 = ; / + _ 等的串)进入状态面。
|
|
@@ -147,9 +149,33 @@ export function createAuthorizationController(ctx, { scope, intentIO, clock = {
|
|
|
147
149
|
return { withdrawalDelivery };
|
|
148
150
|
}
|
|
149
151
|
|
|
150
|
-
function logout() {
|
|
151
|
-
//
|
|
152
|
-
|
|
152
|
+
async function logout() {
|
|
153
|
+
// 退出安全条件(v1.2.10 起,经主人裁决替代 D-01 全禁策略):
|
|
154
|
+
// ① 无进行中的授权尝试(begin 晚写窗口)② 无风险锁存(撤回/超时未核实)。
|
|
155
|
+
// 满足后凭据库视为静止:deleteRecord+删除后 describe 复核;晚到写无法绝对
|
|
156
|
+
// 排除,以复核与状态页如实展示兜底——退出未核实成功绝不伪报成功。
|
|
157
|
+
requireScope();
|
|
158
|
+
if (attempt && LIVE_STATUSES.includes(attempt.status)) {
|
|
159
|
+
throw authError("ATTEMPT_RUNNING", "already running", { attemptId: attempt.attemptId });
|
|
160
|
+
}
|
|
161
|
+
if (riskLatch) throw authError("LOGOUT_UNSAFE", "logout-unsafe");
|
|
162
|
+
// 本 profile 显式操作:旧预览/模型配置意图先行失效(失败则不动凭据)
|
|
163
|
+
intentIO.bump();
|
|
164
|
+
try {
|
|
165
|
+
const deleted = await ctx.credentials.deleteRecord(CREDENTIAL_KEY);
|
|
166
|
+
if (deleted === false) throw authError("LOGOUT_FAILED", "logout-failed");
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (err?.code === "LOGOUT_FAILED") throw err;
|
|
169
|
+
throw authError("LOGOUT_FAILED", "logout-failed");
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const info = await ctx.credentials.describeRecord(CREDENTIAL_KEY);
|
|
173
|
+
if (info?.configured === true) throw authError("LOGOUT_FAILED", "logout-failed");
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (err?.code === "LOGOUT_FAILED") throw err;
|
|
176
|
+
throw authError("LOGOUT_FAILED", "logout-failed");
|
|
177
|
+
}
|
|
178
|
+
return { ok: true };
|
|
153
179
|
}
|
|
154
180
|
|
|
155
181
|
function snapshot() {
|
package/src/host.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { homedir } from "node:os";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { CREDENTIAL_KEY, PROTOCOL_VERSION, routes } from "./shared.mjs";
|
|
11
11
|
import { readJson, writeJsonAtomic, createMutex } from "./atomic-json.mjs";
|
|
12
|
-
import { createAuthorizationController, sanitizeAuthError } from "./auth-host.mjs";
|
|
12
|
+
import { createAuthorizationController, sanitizeAuthError, LIVE_STATUSES } from "./auth-host.mjs";
|
|
13
13
|
import { resolveRuntimeScope } from "./runtime-scope.mjs";
|
|
14
14
|
import { mergeCatalog, digest, findPiAiInstallation, classifyCatalogTarget, normalizeRemoteCatalog, catalogShapeKeys } from "./catalog.mjs";
|
|
15
15
|
import { fetchLatestCatalog } from "./catalog-fetch.mjs";
|
|
@@ -569,6 +569,13 @@ export function apply(ctx, opts = {}) {
|
|
|
569
569
|
credential = "read-error";
|
|
570
570
|
}
|
|
571
571
|
const authSnapshot = controller.snapshot();
|
|
572
|
+
// 退出可用性(v1.2.10):无进行中尝试且无风险锁存才可安全删除凭据。
|
|
573
|
+
// 「重新授权」能力位随 v1.2.11 移除——已登录只需退出登录,未登录入口
|
|
574
|
+
// 本就是「授权登录」,独立按钮属逻辑冗余(主人 2026-10-05 裁决)。
|
|
575
|
+
const logoutAvailable = !authSnapshot.riskLatch
|
|
576
|
+
&& !LIVE_STATUSES.includes(authSnapshot.status)
|
|
577
|
+
&& authSnapshot.status !== "withdrawal-pending-unverified"
|
|
578
|
+
&& authSnapshot.status !== "timed-out-unverified";
|
|
572
579
|
// refresh 块:只读聚合(通道分类来自启动期缓存;GET 不执行写探针,Q23)
|
|
573
580
|
const loaded = loadRefreshState(scope, { legacyPath });
|
|
574
581
|
const st = loaded.flags.unknownVersion ? null : loaded.state;
|
|
@@ -623,7 +630,7 @@ export function apply(ctx, opts = {}) {
|
|
|
623
630
|
credential,
|
|
624
631
|
attempt: authSnapshot,
|
|
625
632
|
riskLatch: authSnapshot.riskLatch,
|
|
626
|
-
capabilities: { logout:
|
|
633
|
+
capabilities: { logout: logoutAvailable },
|
|
627
634
|
},
|
|
628
635
|
refresh,
|
|
629
636
|
...(operation ? { operation } : {}),
|
|
@@ -631,13 +638,35 @@ export function apply(ctx, opts = {}) {
|
|
|
631
638
|
},
|
|
632
639
|
});
|
|
633
640
|
|
|
634
|
-
//
|
|
641
|
+
// 退出登录(v1.2.10):无进行中尝试且无风险锁存时执行凭据删除+删后核实;
|
|
642
|
+
// 进行中尝试(begin 晚写窗口)与风险锁存期(撤回/超时未核实)一律拒绝。
|
|
635
643
|
ctx.webServer.register({
|
|
636
644
|
kind: "exact",
|
|
637
645
|
path: r.logout,
|
|
638
|
-
handler: (req, res) => {
|
|
646
|
+
handler: async (req, res) => {
|
|
639
647
|
if (!guard(req, res, "POST")) return;
|
|
640
|
-
|
|
648
|
+
if (!scope.known) {
|
|
649
|
+
json(res, 503, { ok: false, error: "scope-unavailable" });
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
await controller.logout();
|
|
654
|
+
json(res, 200, { ok: true });
|
|
655
|
+
} catch (err) {
|
|
656
|
+
if (err?.code === "ATTEMPT_RUNNING") {
|
|
657
|
+
json(res, 409, { ok: false, error: "already running", attemptId: err.attemptId });
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (err?.code === "LOGOUT_UNSAFE") {
|
|
661
|
+
json(res, 409, { ok: false, error: "logout-unsafe" });
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (err?.code === "LOGOUT_FAILED") {
|
|
665
|
+
json(res, 409, { ok: false, error: "logout-failed" });
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
json(res, 500, { ok: false, error: sanitizeAuthError(err) });
|
|
669
|
+
}
|
|
641
670
|
},
|
|
642
671
|
});
|
|
643
672
|
|