@inventec/dsh-copilot-auth 1.2.18 → 1.2.19
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 +23 -20
- package/lib/client.js +287 -213
- package/package.json +2 -2
- package/src/auth-flow.mjs +11 -15
- package/src/auth-host.mjs +15 -40
- package/src/host.mjs +0 -18
- package/src/model-update.mjs +107 -82
- package/src/refresh-flow.mjs +115 -33
- package/src/refresh-service.mjs +28 -13
- package/src/shared.mjs +1 -2
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.19",
|
|
4
|
+
"description": "为内置 Copilot 提供方补设备码登录设置页,预置开箱路由;凭据入内置凭据库,模型目录兜底不覆盖定制。v1.2.19:管理模型列表 v3 目标状态编辑(分组全表/挽留/对齐/清空语义告知)+单次确认+移除请求撤回+退出残影修复。v1.2.18:授权失败附网络失败码+人性化提示。适配 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
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// auth-flow.mjs — 浏览器授权状态机(T5
|
|
2
|
-
// 契约:createAuthFlow({fetchImpl, clock, onState}) -> {init, start,
|
|
1
|
+
// auth-flow.mjs — 浏览器授权状态机(T5;Q38 起「请求撤回」已移除):串行请求、代次保护、轮询退避、卸载停止。
|
|
2
|
+
// 契约:createAuthFlow({fetchImpl, clock, onState}) -> {init, start, logout, refresh, dispose}
|
|
3
3
|
// - 单一 init 聚合 /status;start 202 → 轮询;409 → attempt.shared 并转入对现有 attempt 的轮询
|
|
4
4
|
// - 请求代次(generation)保护:慢响应不得覆盖新操作;dispose 仅停止本页请求
|
|
5
5
|
// - 轮询 1 秒;连续读取失败退避 1/2/4/8/15 秒封顶;累计失败 60 秒转手动(connectivity=manual)
|
|
6
|
-
// - 写操作(start
|
|
6
|
+
// - 写操作(start)绝不自动重发
|
|
7
7
|
import { requestJson } from "./client-http.mjs";
|
|
8
8
|
|
|
9
9
|
const POLL_INTERVAL_MS = 1000;
|
|
@@ -61,7 +61,7 @@ export function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = ()
|
|
|
61
61
|
emit({ phase: "failed", error: s?.error ?? "authorization-failed" });
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
|
-
if (s?.status === "
|
|
64
|
+
if (s?.status === "timed-out-unverified") {
|
|
65
65
|
emit({ phase: "risk", riskKind: s.status });
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
@@ -149,6 +149,12 @@ export function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = ()
|
|
|
149
149
|
emit({ phase: "idle", error: r.error?.messageKey, connectivity: "manual" });
|
|
150
150
|
return;
|
|
151
151
|
}
|
|
152
|
+
// Q38 追加修复(真机验证发现):凭据已不在(configured:false)时,attempt 记录里的
|
|
153
|
+
// 「authorized」是退出前的历史残影而非登录态——如实回到未登录,不得翻转已登录视图
|
|
154
|
+
if (r.body?.status === "authorized" && body.configured !== true) {
|
|
155
|
+
emit({ phase: "idle" });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
152
158
|
applySnapshot(gen, r.body);
|
|
153
159
|
}
|
|
154
160
|
|
|
@@ -189,16 +195,6 @@ export function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = ()
|
|
|
189
195
|
emit({ phase: "failed", error: errorCode ?? r.error?.messageKey });
|
|
190
196
|
}
|
|
191
197
|
|
|
192
|
-
async function cancel() {
|
|
193
|
-
// 写操作:单次调用,绝不自动重发
|
|
194
|
-
const r = await requestJson(fetchImpl, "/copilot-auth/cancel", { method: "POST" });
|
|
195
|
-
if (r.ok) {
|
|
196
|
-
emit({ withdrawalDelivery: r.body?.withdrawalDelivery ?? "unavailable" });
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
emit({ withdrawalDelivery: "failed", error: r.error?.details?.errorCode ?? r.error?.messageKey });
|
|
200
|
-
}
|
|
201
|
-
|
|
202
198
|
async function logout() {
|
|
203
199
|
// 退出登录(写操作,单次调用):pending 态 → 成功后完整重跑 init 恢复真实
|
|
204
200
|
// 状态(若晚到写把凭据写了回来,init 会如实显示 authorized,绝不伪报已退出);
|
|
@@ -230,5 +226,5 @@ export function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = ()
|
|
|
230
226
|
clearTimer();
|
|
231
227
|
}
|
|
232
228
|
|
|
233
|
-
return { init, start,
|
|
229
|
+
return { init, start, logout, refresh, dispose };
|
|
234
230
|
}
|
package/src/auth-host.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
// auth-host.mjs — 授权控制器(T3
|
|
2
|
-
// intentVersion 与首次填充 handoff。
|
|
1
|
+
// auth-host.mjs — 授权控制器(T3;Q38 起软撤回已移除):单实例尝试、等待超时风险锁存、
|
|
2
|
+
// 禁用退出、intentVersion 与首次填充 handoff。
|
|
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
|
-
// - cancel:先 bump,再调用宿主撤销(invoked/unavailable/failed),设进程内 riskLatch
|
|
6
5
|
// - logout:静止态(无 LIVE attempt+无 riskLatch)deleteRecord+删后核实;
|
|
7
6
|
// 尝试进行中/风险锁存期拒绝。「重新授权」概念随 v1.2.11 移除(退出+登录分步即可)
|
|
8
|
-
// - onAuthorized(handoff) 仅在 outcome=authorized 且 originIntentVersion
|
|
7
|
+
// - onAuthorized(handoff) 仅在 outcome=authorized 且 originIntentVersion 仍当前且无超时标记时触发
|
|
9
8
|
// - riskLatch 进程级:重挂/dispose 不清;15 分钟等待超时由 snapshot 以注入 clock 惰性判定
|
|
9
|
+
// - Q38(2026-10-05):「请求撤回」按钮与 /cancel 路由移除——撤销本地轮询的价值自限
|
|
10
|
+
// (15 分钟超时自然收敛),而其进程级锁存会把用户锁进「登录成功却不能退出」的死局
|
|
10
11
|
import { randomUUID } from "node:crypto";
|
|
11
12
|
import { CREDENTIAL_KEY } from "./shared.mjs";
|
|
12
13
|
|
|
@@ -74,9 +75,7 @@ export function createAuthorizationController(ctx, { scope, intentIO, clock = {
|
|
|
74
75
|
startedAt: clock.now(),
|
|
75
76
|
notices: [],
|
|
76
77
|
originIntentVersion,
|
|
77
|
-
withdrawalRequested: false,
|
|
78
78
|
timedOut: false,
|
|
79
|
-
withdrawalDelivery: undefined,
|
|
80
79
|
error: undefined,
|
|
81
80
|
staleIntent: false,
|
|
82
81
|
};
|
|
@@ -107,7 +106,7 @@ export function createAuthorizationController(ctx, { scope, intentIO, clock = {
|
|
|
107
106
|
if (attempt !== a) return;
|
|
108
107
|
if (outcome && outcome.status === "authorized") {
|
|
109
108
|
const versionStillCurrent = a.originIntentVersion === intentIO.read();
|
|
110
|
-
const intact = versionStillCurrent && !a.
|
|
109
|
+
const intact = versionStillCurrent && !a.timedOut;
|
|
111
110
|
a.status = "authorized";
|
|
112
111
|
a.staleIntent = !intact;
|
|
113
112
|
if (intact && typeof onAuthorized === "function") {
|
|
@@ -119,9 +118,9 @@ export function createAuthorizationController(ctx, { scope, intentIO, clock = {
|
|
|
119
118
|
});
|
|
120
119
|
}
|
|
121
120
|
} else {
|
|
122
|
-
//
|
|
123
|
-
a.status = "
|
|
124
|
-
|
|
121
|
+
// Q38:撤回通道已移除,cancelled 结局防御性映射为 failed(无晚写风险源,不设锁存)
|
|
122
|
+
a.status = "failed";
|
|
123
|
+
a.error = outcome?.status === "cancelled" ? "authorization-cancelled" : "authorization-failed";
|
|
125
124
|
}
|
|
126
125
|
})
|
|
127
126
|
.catch((err) => {
|
|
@@ -132,34 +131,9 @@ export function createAuthorizationController(ctx, { scope, intentIO, clock = {
|
|
|
132
131
|
return { attemptId: a.attemptId };
|
|
133
132
|
}
|
|
134
133
|
|
|
135
|
-
async function cancel() {
|
|
136
|
-
requireScope();
|
|
137
|
-
// 先持久失效本地旧意图,再发撤回调用
|
|
138
|
-
intentIO.bump();
|
|
139
|
-
let withdrawalDelivery = "unavailable";
|
|
140
|
-
if (attempt) {
|
|
141
|
-
attempt.withdrawalRequested = true;
|
|
142
|
-
if (LIVE_STATUSES.includes(attempt.status)) attempt.status = "withdrawal-pending-unverified";
|
|
143
|
-
}
|
|
144
|
-
const cancelFn = ctx.authorization?.cancel;
|
|
145
|
-
if (typeof cancelFn === "function") {
|
|
146
|
-
try {
|
|
147
|
-
await cancelFn(CREDENTIAL_KEY);
|
|
148
|
-
withdrawalDelivery = "invoked";
|
|
149
|
-
} catch {
|
|
150
|
-
withdrawalDelivery = "failed";
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
// delivery 写入 attempt:刷新页面后 /state 恢复仍如实展示「已请求撤回」事实
|
|
154
|
-
if (attempt) attempt.withdrawalDelivery = withdrawalDelivery;
|
|
155
|
-
// 任何响应都不承诺晚写停止:风险锁存待核实
|
|
156
|
-
setRiskLatch("withdrawal-requested");
|
|
157
|
-
return { withdrawalDelivery };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
134
|
async function logout() {
|
|
161
135
|
// 退出安全条件(v1.2.10 起,经主人裁决替代 D-01 全禁策略):
|
|
162
|
-
// ① 无进行中的授权尝试(begin 晚写窗口)②
|
|
136
|
+
// ① 无进行中的授权尝试(begin 晚写窗口)② 无风险锁存(Q38 起仅剩等待超时未核实)。
|
|
163
137
|
// 满足后凭据库视为静止:deleteRecord+删除后 describe 复核;晚到写无法绝对
|
|
164
138
|
// 排除,以复核与状态页如实展示兜底——退出未核实成功绝不伪报成功。
|
|
165
139
|
requireScope();
|
|
@@ -183,6 +157,9 @@ export function createAuthorizationController(ctx, { scope, intentIO, clock = {
|
|
|
183
157
|
if (err?.code === "LOGOUT_FAILED") throw err;
|
|
184
158
|
throw authError("LOGOUT_FAILED", "logout-failed");
|
|
185
159
|
}
|
|
160
|
+
// Q38 追加修复(真机验证发现):退出成功后清掉 attempt 残影——否则 /state 继续报告
|
|
161
|
+
// 历史「authorized」,客户端会把残影误当登录态,UI 翻回已登录,表现为「退出没作用」
|
|
162
|
+
attempt = null;
|
|
186
163
|
return { ok: true };
|
|
187
164
|
}
|
|
188
165
|
|
|
@@ -204,7 +181,6 @@ export function createAuthorizationController(ctx, { scope, intentIO, clock = {
|
|
|
204
181
|
url: attempt.url,
|
|
205
182
|
expiresAt: attempt.expiresAt,
|
|
206
183
|
waitDeadline: attempt.startedAt + WAIT_LIMIT_MS,
|
|
207
|
-
withdrawalDelivery: attempt.withdrawalDelivery,
|
|
208
184
|
riskLatch,
|
|
209
185
|
staleIntent: attempt.staleIntent,
|
|
210
186
|
notices: [...attempt.notices],
|
|
@@ -212,15 +188,14 @@ export function createAuthorizationController(ctx, { scope, intentIO, clock = {
|
|
|
212
188
|
};
|
|
213
189
|
}
|
|
214
190
|
|
|
215
|
-
// T4 消费:handoff 是否仍完好(同 attempt
|
|
191
|
+
// T4 消费:handoff 是否仍完好(同 attempt、版本仍当前、无超时标记)
|
|
216
192
|
function handoffIntact(handoff) {
|
|
217
193
|
if (!attempt || !handoff) return false;
|
|
218
194
|
return attempt.attemptId === handoff.attemptId
|
|
219
195
|
&& attempt.originIntentVersion === handoff.originIntentVersion
|
|
220
196
|
&& attempt.originIntentVersion === intentIO.read()
|
|
221
|
-
&& !attempt.withdrawalRequested
|
|
222
197
|
&& !attempt.timedOut;
|
|
223
198
|
}
|
|
224
199
|
|
|
225
|
-
return { start,
|
|
200
|
+
return { start, logout, snapshot, handoffIntact };
|
|
226
201
|
}
|
package/src/host.mjs
CHANGED
|
@@ -528,24 +528,6 @@ export function apply(ctx, opts = {}) {
|
|
|
528
528
|
},
|
|
529
529
|
});
|
|
530
530
|
|
|
531
|
-
ctx.webServer.register({
|
|
532
|
-
kind: "exact",
|
|
533
|
-
path: r.cancel,
|
|
534
|
-
handler: async (req, res) => {
|
|
535
|
-
if (!guard(req, res, "POST")) return;
|
|
536
|
-
if (!scope.known) {
|
|
537
|
-
json(res, 503, { ok: false, error: "scope-unavailable" });
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
try {
|
|
541
|
-
const { withdrawalDelivery } = await controller.cancel();
|
|
542
|
-
json(res, 200, { ok: true, withdrawalDelivery });
|
|
543
|
-
} catch (err) {
|
|
544
|
-
json(res, 500, { ok: false, error: sanitizeAuthError(err) });
|
|
545
|
-
}
|
|
546
|
-
},
|
|
547
|
-
});
|
|
548
|
-
|
|
549
531
|
ctx.webServer.register({
|
|
550
532
|
kind: "exact",
|
|
551
533
|
path: r.state,
|
package/src/model-update.mjs
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
// model-update.mjs — 纯模型变更策略(
|
|
1
|
+
// model-update.mjs — 纯模型变更策略(v3):管理模型列表=目标状态编辑(ADR 0005)。
|
|
2
2
|
// 纯函数,不触碰网络/磁盘/settings;视图形状与 host.mjs viewFromUserLayer 同构:
|
|
3
3
|
// {modelsPresent, models, modelOverridesPresent, modelOverrides}
|
|
4
|
-
// 语义(设计§4.1 /
|
|
5
|
-
// -
|
|
6
|
-
// -
|
|
7
|
-
//
|
|
8
|
-
// -
|
|
4
|
+
// 语义(设计§4.1/§4.2 2026-10-05 修订 / Q32–Q36):
|
|
5
|
+
// - 单一操作 manage:勾选集合即期望终态;selectedIds 缺省 → 在列∩(账号∩可解析)(与 UI 初始态一致)
|
|
6
|
+
// - 被保留模型维持原对象/顺序/定制;被移除模型定制随模型消失(overrides 按 kept 过滤,
|
|
7
|
+
// 唯一定制模型被移除时 present:false → unset,防重加复活定制)
|
|
8
|
+
// - rows:addable(账号∩可解析−在列)/ listed(在列∩(账号∩可解析))/ removal-proposal
|
|
9
|
+
// (在列−(账号∩可解析),reason 优先级:不在账号→not-in-account,否则 unresolvable)
|
|
10
|
+
// - removed:raw 在列全表−勾选;reason:not-in-account → unresolvable → unchecked(用户主动取消健康行)
|
|
11
|
+
// - 空目标确认仅约束用户显式提交的空选择(selectedIds!==undefined);确认后落显式空列表
|
|
12
|
+
// (modelsPresent:true, models:[],防 unset 后首次填充静默回填)
|
|
13
|
+
// - clearOverrides:清除可清除定制(present:false);仅继承覆盖(raw 缺失而 effective 有)→ 阻止该项
|
|
9
14
|
export function buildModelChange({
|
|
10
15
|
operation,
|
|
11
16
|
rawView,
|
|
@@ -13,11 +18,17 @@ export function buildModelChange({
|
|
|
13
18
|
accountIds,
|
|
14
19
|
resolvableIds,
|
|
15
20
|
selectedIds,
|
|
21
|
+
clearOverrides = false,
|
|
16
22
|
confirmEmpty = false,
|
|
17
23
|
catalogNewEntryCount = 0,
|
|
18
24
|
}) {
|
|
19
|
-
if (operation !== "
|
|
20
|
-
return {
|
|
25
|
+
if (operation !== "manage") {
|
|
26
|
+
return {
|
|
27
|
+
allowed: false, reason: "invalid-operation", targetView: null, rows: [], selectedIds: [],
|
|
28
|
+
added: [], removed: [], kept: [], warnings: [], removalClass: false,
|
|
29
|
+
overridesMeta: { rawPresent: false, clearable: false, inheritedOnly: false },
|
|
30
|
+
writeSet: { catalogEntries: false, models: false, modelOverrides: false, overridesUnclearable: false },
|
|
31
|
+
};
|
|
21
32
|
}
|
|
22
33
|
const accounts = [...new Set((Array.isArray(accountIds) ? accountIds : []).filter((x) => typeof x === "string"))];
|
|
23
34
|
const resolvable = new Set((Array.isArray(resolvableIds) ? resolvableIds : []).filter((x) => typeof x === "string"));
|
|
@@ -25,94 +36,108 @@ export function buildModelChange({
|
|
|
25
36
|
const eff = effectiveView ?? raw;
|
|
26
37
|
const rawModels = raw.modelsPresent && Array.isArray(raw.models) ? raw.models : [];
|
|
27
38
|
const rawIds = rawModels.map((m) => m?.id).filter((x) => typeof x === "string");
|
|
28
|
-
const
|
|
29
|
-
const
|
|
39
|
+
const rawIdSet = new Set(rawIds);
|
|
40
|
+
const accountSet = new Set(accounts);
|
|
41
|
+
|
|
42
|
+
const overridesKeys = raw.modelOverridesPresent && raw.modelOverrides && typeof raw.modelOverrides === "object"
|
|
43
|
+
? Object.keys(raw.modelOverrides) : [];
|
|
44
|
+
const rawPresent = overridesKeys.length > 0;
|
|
45
|
+
const effOverrides = eff.modelOverridesPresent && eff.modelOverrides && typeof eff.modelOverrides === "object"
|
|
46
|
+
? Object.keys(eff.modelOverrides) : [];
|
|
47
|
+
const inheritedOnly = !rawPresent && effOverrides.length > 0;
|
|
48
|
+
const overridesMeta = { rawPresent, clearable: rawPresent, inheritedOnly };
|
|
49
|
+
|
|
50
|
+
const inUniverse = (id) => accountSet.has(id) && resolvable.has(id);
|
|
51
|
+
const addable = accounts.filter((id) => resolvable.has(id) && !rawIdSet.has(id));
|
|
52
|
+
const addableSet = new Set(addable);
|
|
53
|
+
const rows = [
|
|
54
|
+
...addable.map((id) => ({ id, status: "addable", customized: false })),
|
|
55
|
+
...rawIds.filter((id) => inUniverse(id)).map((id) => ({ id, status: "listed", customized: overridesKeys.includes(id) })),
|
|
56
|
+
...rawIds.filter((id) => !inUniverse(id)).map((id) => ({
|
|
57
|
+
id,
|
|
58
|
+
status: "removal-proposal",
|
|
59
|
+
reason: accountSet.has(id) ? "unresolvable" : "not-in-account",
|
|
60
|
+
customized: overridesKeys.includes(id),
|
|
61
|
+
})),
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// 选择规范化:缺省 → listed(与 UI 初始勾选态一致);显式(含 [])→ 用户意图
|
|
65
|
+
const explicitSelection = selectedIds !== undefined;
|
|
66
|
+
const requested = explicitSelection
|
|
67
|
+
? [...new Set((Array.isArray(selectedIds) ? selectedIds : []).filter((x) => typeof x === "string"))]
|
|
68
|
+
: rawIds.filter((id) => inUniverse(id));
|
|
69
|
+
const selectedSet = new Set(requested);
|
|
70
|
+
// warnings:manage 语义下只产 invalid-selection/unresolvable(already-configured 仅保留在契约枚举中,不可达)
|
|
30
71
|
const warnings = [];
|
|
31
|
-
for (const id of
|
|
32
|
-
if (
|
|
72
|
+
for (const id of requested) {
|
|
73
|
+
if (addableSet.has(id) || rawIdSet.has(id)) continue;
|
|
74
|
+
warnings.push({ id, reason: accountSet.has(id) && !resolvable.has(id) ? "unresolvable" : "invalid-selection" });
|
|
33
75
|
}
|
|
34
|
-
const catalogEntries = catalogNewEntryCount > 0;
|
|
35
76
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
added.push(id);
|
|
47
|
-
} else if (existing.has(id)) {
|
|
48
|
-
warnings.push({ id, reason: "already-configured" });
|
|
49
|
-
} else if (accounts.includes(id) && !resolvable.has(id)) {
|
|
50
|
-
warnings.push({ id, reason: "unresolvable" });
|
|
51
|
-
} else {
|
|
52
|
-
warnings.push({ id, reason: "invalid-selection" });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
const models = added.length > 0 ? [...rawModels, ...added.map((id) => ({ id }))] : rawModels;
|
|
56
|
-
const modelsPresent = raw.modelsPresent || added.length > 0;
|
|
57
|
-
const overridesUnclearable = false;
|
|
58
|
-
return {
|
|
59
|
-
allowed: true,
|
|
60
|
-
reason: null,
|
|
61
|
-
candidates, // 候选=账号∩可解析−已有(Q5:默认全不勾选,勾选后 materialize 新预览)
|
|
62
|
-
targetView: {
|
|
63
|
-
modelsPresent,
|
|
64
|
-
models: modelsPresent ? models : null,
|
|
65
|
-
modelOverridesPresent: raw.modelOverridesPresent,
|
|
66
|
-
modelOverrides: raw.modelOverrides,
|
|
67
|
-
},
|
|
68
|
-
added,
|
|
69
|
-
removed: [],
|
|
70
|
-
kept: effIds.filter((id) => existing.has(id)),
|
|
71
|
-
warnings,
|
|
72
|
-
writeSet: { catalogEntries, models: added.length > 0, modelOverrides: false, overridesUnclearable },
|
|
73
|
-
};
|
|
74
|
-
}
|
|
77
|
+
const kept = rawIds.filter((id) => selectedSet.has(id));
|
|
78
|
+
const keptSet = new Set(kept);
|
|
79
|
+
const added = addable.filter((id) => selectedSet.has(id));
|
|
80
|
+
const removed = rawIds
|
|
81
|
+
.filter((id) => !selectedSet.has(id))
|
|
82
|
+
.map((id) => ({
|
|
83
|
+
id,
|
|
84
|
+
reason: !accountSet.has(id) ? "not-in-account" : !resolvable.has(id) ? "unresolvable" : "unchecked",
|
|
85
|
+
}));
|
|
86
|
+
const removalClass = removed.length > 0;
|
|
75
87
|
|
|
76
|
-
|
|
77
|
-
const effOverrides = eff.modelOverridesPresent && eff.modelOverrides && typeof eff.modelOverrides === "object"
|
|
78
|
-
? Object.keys(eff.modelOverrides) : [];
|
|
79
|
-
if (!raw.modelOverridesPresent && effOverrides.length > 0) {
|
|
88
|
+
if (clearOverrides && inheritedOnly) {
|
|
80
89
|
return {
|
|
81
|
-
allowed: false,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
writeSet: { catalogEntries, models: false, modelOverrides: false, overridesUnclearable: true },
|
|
90
|
+
allowed: false, reason: "inherited-overrides-unclearable", targetView: null, rows, selectedIds: requested,
|
|
91
|
+
added, removed, kept, warnings, removalClass,
|
|
92
|
+
overridesMeta,
|
|
93
|
+
writeSet: { catalogEntries: catalogNewEntryCount > 0, models: false, modelOverrides: false, overridesUnclearable: true },
|
|
85
94
|
};
|
|
86
95
|
}
|
|
87
|
-
|
|
88
|
-
if (intersection.length === 0 && !confirmEmpty) {
|
|
96
|
+
if (explicitSelection && rawIds.length > 0 && requested.length === 0 && !confirmEmpty) {
|
|
89
97
|
return {
|
|
90
|
-
allowed: false,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
writeSet: { catalogEntries, models: false, modelOverrides: false, overridesUnclearable: false },
|
|
98
|
+
allowed: false, reason: "empty-target-unconfirmed", targetView: null, rows, selectedIds: requested,
|
|
99
|
+
added, removed, kept, warnings, removalClass,
|
|
100
|
+
overridesMeta,
|
|
101
|
+
writeSet: { catalogEntries: catalogNewEntryCount > 0, models: false, modelOverrides: false, overridesUnclearable: false },
|
|
94
102
|
};
|
|
95
103
|
}
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
|
|
105
|
+
const keptRawModels = rawModels.filter((m) => keptSet.has(m?.id));
|
|
106
|
+
const modelsPresent = raw.modelsPresent || requested.length > 0;
|
|
107
|
+
const models = modelsPresent ? [...keptRawModels, ...added.map((id) => ({ id }))] : null;
|
|
108
|
+
const clearable = rawPresent;
|
|
109
|
+
let modelOverridesPresent;
|
|
110
|
+
let modelOverrides;
|
|
111
|
+
if (clearOverrides && clearable) {
|
|
112
|
+
modelOverridesPresent = false;
|
|
113
|
+
modelOverrides = null;
|
|
114
|
+
} else if (rawPresent) {
|
|
115
|
+
const filtered = {};
|
|
116
|
+
for (const key of overridesKeys) if (keptSet.has(key)) filtered[key] = raw.modelOverrides[key];
|
|
117
|
+
modelOverridesPresent = Object.keys(filtered).length > 0;
|
|
118
|
+
modelOverrides = modelOverridesPresent ? filtered : null;
|
|
119
|
+
} else {
|
|
120
|
+
modelOverridesPresent = raw.modelOverridesPresent;
|
|
121
|
+
modelOverrides = raw.modelOverrides;
|
|
122
|
+
}
|
|
123
|
+
|
|
102
124
|
return {
|
|
103
125
|
allowed: true,
|
|
104
126
|
reason: null,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
models: intersection.map((id) => ({ id })),
|
|
109
|
-
modelOverridesPresent: false,
|
|
110
|
-
modelOverrides: null,
|
|
111
|
-
},
|
|
112
|
-
added: intersection.filter((id) => !new Set(effIds).has(id)),
|
|
127
|
+
rows,
|
|
128
|
+
selectedIds: requested,
|
|
129
|
+
added,
|
|
113
130
|
removed,
|
|
114
|
-
kept
|
|
131
|
+
kept,
|
|
115
132
|
warnings,
|
|
116
|
-
|
|
133
|
+
removalClass,
|
|
134
|
+
overridesMeta,
|
|
135
|
+
targetView: { modelsPresent, models, modelOverridesPresent, modelOverrides },
|
|
136
|
+
writeSet: {
|
|
137
|
+
catalogEntries: catalogNewEntryCount > 0,
|
|
138
|
+
models: added.length > 0 || removalClass || (!raw.modelsPresent && requested.length > 0),
|
|
139
|
+
modelOverrides: clearOverrides && clearable,
|
|
140
|
+
overridesUnclearable: false,
|
|
141
|
+
},
|
|
117
142
|
};
|
|
118
143
|
}
|