kczx-user-management 1.0.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.
@@ -0,0 +1,73 @@
1
+ # Cutover runbook: retiring dsh-passwords
2
+
3
+ Everything below is reversible. Read the whole page before starting; the switch
4
+ takes a few minutes and the rollback takes one command.
5
+
6
+ ## What is already true
7
+
8
+ - Accounts, permissions, quotas, upload/git switches and admin CRUD live in
9
+ `kczx-user-management` (see MERGED-DEPLOYMENT.md for how the two plugins
10
+ currently cooperate).
11
+ - The enforcement layer (workspace scope, folder gate, sandbox floor, upload/git
12
+ and ops-surface gates, session-history clamp, hidden-Unicode strip, token and
13
+ duration quotas) is implemented here and covered by the test suite.
14
+ - This plugin's own login door still works: the dry run
15
+ (`C:\Users\95232\mxz\um-ui\door-dryrun.mjs`) exercises TLS, the login page,
16
+ sessions, role scoping, an imported bcrypt account signing in, and the gates
17
+ behind the door — 13/13.
18
+
19
+ ## Decisions that must be made first
20
+
21
+ 1. **Password collision on `mxz`.** The account exists in BOTH stores with
22
+ different password hashes. If the door moves without deciding this, sign-in
23
+ needs the OLD user-management password, not the one used today.
24
+ - keep today's password: `node scripts/import-from-dsh-passwords.mjs --from E:\code\dsh\dsh-passwords --overwrite-credentials`
25
+ - or reset it afterwards from the accounts page as an admin.
26
+ 2. **Which dsh-passwords capabilities must survive.** Not yet answered: automatic
27
+ HTTPS (ACME), encrypted audit storage, user-to-user chat, dsh auto-update,
28
+ login/IP throttling, workspace ownership tracking. Anything not named here is
29
+ dropped when the plugin is removed.
30
+
31
+ ## The switch
32
+
33
+ ```powershell
34
+ # 0. stop the door that is being retired
35
+ # (dsh-passwords' gateway is a child of dsh; stopping dsh web stops it too)
36
+
37
+ # 1. turn this plugin back into the door:
38
+ # ~/.dsh/settings.yaml
39
+ # user-management:
40
+ # listenHost: '0.0.0.0' # was 127.0.0.1 (observer mode)
41
+ # plaintext: false # was true: the door terminates TLS again
42
+ # port: 19843
43
+ # trustedSecret: '' # no signing gateway in front of it any more
44
+
45
+ # 2. remove dsh-passwords from the profile
46
+ dsh plugin --profile web remove dsh-passwords
47
+
48
+ # 3. restart dsh web
49
+ ```
50
+
51
+ ## Verification after the switch
52
+
53
+ 1. `https://<host>:19843/login` renders, a wrong password is refused, a correct
54
+ one lands on the SPA.
55
+ 2. The settings page shows 用户管理 with the accounts that were migrated
56
+ (`mxz`, `mxz1`) and their permissions intact.
57
+ 3. As a restricted account: the workspace list is filtered, a session outside the
58
+ allow-list is refused, an upload or git action is refused, and the history of a
59
+ shared session does not raise the sandbox.
60
+ 4. `~/.dsh/user-management/` still grows: `activity.jsonl` on sign-in, `audit.jsonl`
61
+ on API calls, `usage.json` on reported tokens.
62
+
63
+ ## Rollback
64
+
65
+ ```powershell
66
+ # restore the observer arrangement
67
+ dsh plugin --profile web add link:E:\code\dsh\dsh-passwords
68
+ # settings.yaml: listenHost 127.0.0.1, plaintext true, trustedSecret <the 64-hex key>
69
+ # restart dsh web
70
+ ```
71
+
72
+ The dsh-passwords checkout, its SQLite database and its `.env` are never touched
73
+ by any of this, so its accounts and audit log remain intact either way.
@@ -0,0 +1,100 @@
1
+ # (历史)合并部署:user-management 作为回环台账观察层
2
+
3
+ > **已失效,仅作存档**:这份文件描述 dsh-passwords 与本插件并存的「观察层」形态。
4
+ > 该插件已于 1.0.0 退役,当前部署形态见 README。下文保留是为了说明当时的信任模型与接线方式。
5
+
6
+ 本文件只描述**合并部署**——即 dsh-passwords 与 user-management 同机运行、由前者
7
+ 独占登录时,本插件的外部契约与接线方式。独立部署(本插件自己当登录门)的行为
8
+ 没有任何变化;不配置 `trustedSecret` 时,本文件描述的一切都是惰性的。
9
+
10
+ ## 架构
11
+
12
+ ```
13
+ 浏览器
14
+ │ http://<host>:8080 ← 唯一登录门(dsh-passwords,账号/权限/配额/审计)
15
+
16
+ dsh-passwords 网关(独立子进程)
17
+ │ 转发时注入签名身份头;入站同名头一律剥除(防伪造)
18
+
19
+ user-management 网关(127.0.0.1 明文,只信签名)
20
+ │ 三本台账在这里记录:访问记录 / 操作日志(登录记录由门回传)
21
+
22
+ dsh 宿主 127.0.0.1:3080
23
+ ```
24
+
25
+ 为什么观察层必须留在链路里:三本台账全部由**本插件的网关钩子**写入
26
+ (`onAccess` / `onApiRequest` / `onWsOpen`)。摘掉它的网关,台账立刻变空。
27
+ 所以它不是"被替代",而是"退居为纯观察层"。
28
+
29
+ ## 信任模型
30
+
31
+ `src/trusted-identity.js` 是唯一的信任入口,三个条件必须同时成立:
32
+
33
+ 1. **对端是回环**(默认 `127.0.0.1` / `::1` / `::ffff:127.0.0.1`)——监听器本身
34
+ 只绑 127.0.0.1,所以只有同机的登录门能到达;
35
+ 2. **时间戳在 ±60 秒窗口内**——防重放;
36
+ 3. **HMAC-SHA256(secret, "username\nrole\nip\nts") 恒定时间比对通过**。
37
+
38
+ 签名覆盖 **role 和客户端 IP**:篡改角色提权、或篡改台账里的来源 IP,都会验签失败。
39
+
40
+ ## 配置
41
+
42
+ ```yaml
43
+ user-management:
44
+ enabled: true
45
+ listenHost: '127.0.0.1' # 明文模式会强制为回环,配别的会被拒绝并告警
46
+ port: 19843
47
+ plaintext: true # 明文监听:登录门的 WS 升级走裸 TCP 隧道,无法对这一跳做 TLS
48
+ trustedSecret: '<64 hex>' # 与 dsh-passwords 的 MCP_IDENTITY_SECRET 必须逐字一致
49
+ # trustedPeers: [] # 默认即回环;仅在确有需要时覆盖
50
+ ```
51
+
52
+ 对端(dsh-passwords)需要:
53
+
54
+ ```
55
+ MCP_IDENTITY_SECRET=<同一个 64 hex> # 专用密钥,不复用 MCP_INTERNAL_SECRET
56
+ MCP_GATEWAY_UPSTREAM=http://127.0.0.1:19843
57
+ ```
58
+
59
+ 两端密钥不一致是**静默失败**——观察层会拒绝全部请求,台账停止增长而没有任何报错。
60
+ 所以验证脚本把"两侧密钥一致"作为第一条断言。
61
+
62
+ ## 登录记录如何续上
63
+
64
+ 登录发生在门那一侧,观察层看不到 `login` 事件。门在登录成功 / 失败 / 登出时,
65
+ 把事件签名后 POST 到:
66
+
67
+ ```
68
+ POST /user-management/api/internal/session-event
69
+ { "type": "login" | "login_failed" | "logout", "detail": "..." }
70
+ ```
71
+
72
+ 该路径列在 `PUBLIC_PATHS` 里(浏览器会话检查对它不适用),凭据就是签名头本身:
73
+ 没有有效签名一律 401。通知是 fire-and-forget——观察层不在时得到 404,静默忽略,
74
+ **绝不影响登录本身**。
75
+
76
+ ## 客户端表现(observer 模式)
77
+
78
+ `/user-management/api/session` 会带上 `observer: true`,客户端据此:
79
+
80
+ - 页签只留 登录记录 / 访问记录 / 操作日志(账号表、IP 封禁、证书、2FA 属于"门",隐藏);
81
+ - 去掉登出与改密入口(那属于门的职责,在这里点了没有意义);
82
+ - 直连 dsh 端口打开时(台账 API 在该端口是 404),提示改走网关地址,而不是
83
+ 误导性的"会话已过期,请重新登录"。
84
+
85
+ ## 验证
86
+
87
+ ```bash
88
+ node --test test/trusted-identity.test.mjs # 信任条件单测
89
+ node dist/verify-merge.mjs # 端到端:密钥一致性、签名放行、
90
+ # 篡改拒绝、台账归属与真实 IP
91
+ ```
92
+
93
+ ## 已知边界
94
+
95
+ - **直连 dsh 端口可绕过整扇门**。本插件与门都无法阻止:谁能访问宿主端口,谁就
96
+ 不需要登录。dsh-passwords 的设计前提是"dsh web 只监听 loopback";若另有插件把
97
+ 宿主绑到 0.0.0.0(例如 dsh-lan-access),门的约束力就只剩"愿意走门的人"。
98
+ 此时台账仍只记录走门的流量,**绕过者的行为不会出现在任何一本账里**。
99
+ - 观察层看到的 socket 对端恒为 127.0.0.1,真实来源 IP 来自签名头;因此
100
+ "IP 封禁"在合并部署下按真实 IP 生效,但只在门的链路上。
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # kczx-user-management
2
+
3
+ > **本包是本地扩展版(1.0.0),已把 dsh-passwords 的账号 / 权限 / 配额 / 台账能力并入,不再是上游 0.9.5 的功能集。**
4
+ > 上游同名包(npm 上的 `@weibaohui/user-management`)仅提供登录门禁与两步验证;本版额外提供工作区白名单与归属跟踪、
5
+ > token 与时长配额、沙盒档位下限、上传 / git / 运维面门控、历史沙盒降级与隐藏字符清洗。
6
+ > 为便于从 registry 安装,本版以独立包名 **`kczx-user-management`** 发布,与上游互不覆盖。
7
+
8
+ 给 dsh web 装一扇门:**HTTPS 登录网关 + 账号管理 + 权限与配额执行 + 三本审计台账**,一个插件完成。
9
+
10
+ ## 安装
11
+
12
+ ```bash
13
+ # 从 registry(发布之后;-w = pnpm 的 --workspace-root)
14
+ dsh plugin --profile web add kczx-user-management -w
15
+
16
+ # 从 tarball(离线 / 内网部署)
17
+ dsh plugin --profile web add ./kczx-user-management-1.0.0.tgz -w
18
+
19
+ # 或直接从目录 / 仓库(相对路径以你执行命令的目录为锚)
20
+ dsh plugin --profile web add link:E:\path\to\dsh-user-management -w
21
+ dsh plugin --profile web add github:you/dsh-user-management -w
22
+
23
+ # 装完重启 dsh web 生效
24
+ ```
25
+
26
+ `dsh plugin --profile <name> <args...>` 就是在 `~/.dsh/profiles/<name>` 里转发 `pnpm <args...>`;退出码 0 时按**已安装状态**
27
+ 对账 `dsh.profile.bundles`——凡是声明了 `dsh.bundle.patch` 的依赖会自动进 layer 列表(本包含),所以不必手工改 bundles。
28
+
29
+ ### 用 pnpm 直接安装
30
+
31
+ 包内自带 `dsh.bundle.patch`,运行期依赖(`@deepseek-ai/schemastery`、`bcryptjs`、`selfsigned`)都在公共 registry 上,因此也可以当普通 npm 包装:
32
+
33
+ ```bash
34
+ pnpm add kczx-user-management # 已发布到 registry 时
35
+ pnpm add ./kczx-user-management-1.0.0.tgz # 从 tarball
36
+ pnpm add /abs/path/to/dsh-user-management # 从目录
37
+ ```
38
+
39
+ 装进 dsh profile(把下面几行加进 `~/.dsh/profiles/web/package.json`,再在该目录 `pnpm install`,然后重启 `dsh web`):
40
+
41
+ ```json
42
+ {
43
+ "dependencies": { "kczx-user-management": "file:./kczx-user-management-1.0.0.tgz" },
44
+ "dsh": { "profile": { "bundles": ["kczx-user-management"] } }
45
+ }
46
+ ```
47
+
48
+ 本包不依赖安装期脚本:`postinstall` 已移除(pnpm ≥10 默认拦截依赖构建脚本,留着只会让安装以
49
+ `ERR_PNPM_IGNORED_BUILDS` 退出码 1 收场),`ensure:deps` 保留为手动脚本,仅供「内网无 registry
50
+ 且以 `link:` 挂载」的场景,从宿主补齐 `@deepseek-ai/schemastery`:
51
+
52
+ ```bash
53
+ pnpm add ./kczx-user-management-1.0.0.tgz # 正常路径:依赖从 registry 装齐
54
+ node scripts/ensure-deps.mjs # 仅当上述解析不到 schemastery 时才需要
55
+ ```
56
+
57
+ ## 它做什么
58
+
59
+ ### 一扇门
60
+
61
+ 独立的 `node:https` 监听器(默认 `0.0.0.0:19843`),自签证书(SAN 覆盖本机全部 IP + sslip.io / nip.io 别名),
62
+ 未登录的页面访问跳转 `/login`、API 请求 401;登录页自带深浅色。设置页提供证书下载(PEM/DER)、指纹核对与
63
+ 各系统导入向导。
64
+
65
+ ### 账号与权限(管理员)
66
+
67
+ | 能力 | 说明 |
68
+ |---|---|
69
+ | 用户增删改查 | 新增 / 删除 / 角色调整 / 禁用启用 / 重置密码(一次性临时密码)/ 重置两步验证 |
70
+ | 工作区白名单 | 留空 = 允许全部;填写后只允许这些目录(Windows 路径折叠大小写,末尾斜杠自动忽略) |
71
+ | 工作区归属跟踪 | 谁创建的工作区归谁:别人看不到、也不能删改;删除后释放,账号删除后清理 |
72
+ | 每小时 token 配额 | 由浏览器上报 dsh 自身的用量投影增量,服务端按小时窗口累计并拦截 |
73
+ | 每日时长配额 | 按真实活跃时间累计(不是请求数),限流不因轮询膨胀 |
74
+ | 沙盒档位下限 | read-only / workspace-write / danger-full-access:AI 无法通过设置、slash 命令或审批把自己提权 |
75
+ | 上传 / git 开关 | 上传与「把数据带走」的通道分别控制(git RPC、会话导出、SSH 下载等) |
76
+ | 策略封禁 | 与「禁用」不同:执行层每次请求都读,封禁即刻踢出会话 |
77
+
78
+ ### 三本台账
79
+
80
+ | 台账 | 内容 |
81
+ |---|---|
82
+ | 登录记录 | 登录 / 失败 / 登出 / 改密 / 注册 / 角色变更 / 删号 / 权限变更(含紧凑差异串) |
83
+ | 访问记录 | 页面级访问(保留真实客户端 IP) |
84
+ | 操作日志 | 经过网关的每次 API 调用与 WebSocket 连接(方法 / 路径 / 状态 / 来源 IP) |
85
+
86
+ 另有 IP 封禁(回环与当前 IP 拒绝封禁,防自锁)与 TOTP 两步验证(默认关闭,可选开启)。
87
+
88
+ ## 配置
89
+
90
+ 走 `~/.dsh/settings.yaml` 的 `user-management:` 段(改完要么热重载、要么重启):
91
+
92
+ ```yaml
93
+ user-management:
94
+ enabled: true
95
+ listenHost: '0.0.0.0' # 只本机可达就填 127.0.0.1
96
+ port: 19843
97
+ plaintext: false # true 时只允许回环(明文服务观察层用,见 MERGED-DEPLOYMENT.md)
98
+ sites: [] # 空 = 自动枚举本机 IP;配域名 + 证书则按 SNI 选择
99
+ title: 'DSH 控制台'
100
+ autoActivate: false # false = 自助注册需管理员启用
101
+ ```
102
+
103
+ ## 数据文件
104
+
105
+ 都在 `$DSH_HOME/user-management/`(默认 `~/.dsh/user-management/`,0600,原子写):
106
+
107
+ | 文件 | 内容 |
108
+ |---|---|
109
+ | `users.json` | 账号(scrypt 哈希;迁移进来的账号保留 bcrypt,首次登录成功后自动升级为 scrypt) |
110
+ | `sessions.json` | 会话令牌(重启不掉线,7 天滑动过期) |
111
+ | `activity.jsonl` / `audit.jsonl` | 登录与访问台账 / 操作日志(滚动保留最近若干条) |
112
+ | `bans.json` | IP 封禁 |
113
+ | `usage.json` | 按账号的当日活跃秒数与小时窗口 token(隔日自动清理) |
114
+ | `workspaces.json` | 工作区归属(路径 → 账号) |
115
+
116
+ ## 从 dsh-passwords 迁移
117
+
118
+ 账号、权限与配额可由 `scripts/import-from-dsh-passwords.mjs` 一次性导入(读取源库时复用 dsh-passwords 自己的模块,
119
+ 因为它的用户名是 AES-GCM 密文 + HMAC 索引):
120
+
121
+ ```bash
122
+ node scripts/import-from-dsh-passwords.mjs --from E:\path\to\dsh-passwords --dry-run
123
+ node scripts/import-from-dsh-passwords.mjs --from E:\path\to\dsh-passwords
124
+ # 同名账号两边密码不同时,以源端为准(迁移期间通常正是你要的)
125
+ node scripts/import-from-dsh-passwords.mjs --from E:\path\to\dsh-passwords --overwrite-credentials
126
+ ```
127
+
128
+ 迁移是幂等的:重复运行只会更新权限,不会重复建号;**默认不覆盖已有账号的密码**。
129
+
130
+ ## 测试
131
+
132
+ ```bash
133
+ npm test # 83 项:策略纯函数、store、API 层、以及真实网关的端到端用例
134
+ npm run check # 全量语法检查
135
+ ```
136
+
137
+ 端到端用例会起真实网关(明文模式 + 假上游)验证:工作区过滤含 gzip、目录门与请求体重放、沙盒提权拦截与审批改写、
138
+ 上传 / git / 运维面门控、历史降级与隐藏字符清洗、配额拦截、归属认领与释放。
139
+
140
+ ## 卸载 / 回滚
141
+
142
+ ```bash
143
+ dsh plugin --profile web remove kczx-user-management
144
+ ```
145
+
146
+ 数据文件不会被删除,重新装回即恢复。若在合并部署中回退到「观察层」形态,见 `MERGED-DEPLOYMENT.md`。
147
+
148
+ ## 已知边界
149
+
150
+ - **执行只对本地存在的账号生效**:签名身份 / 未知用户名按「不限制」处理,避免迁移期误锁人。
151
+ - **门只管走门的人**:若另有插件把 dsh 宿主绑到 `0.0.0.0`(例如 dsh-lan-access),直连宿主端口即可绕过登录,
152
+ 且**绕过流量不会出现在任何一本台账里**。要真正的强制约束,宿主端口必须保持回环。
153
+ - **token 配额依赖客户端上报**:浏览器侧被禁用或页面未加载时不计费(时长配额不受影响,它在服务端计时)。
154
+ - 上游 `@weibaohui/user-management`(0.9.5)与本包 `kczx-user-management`(1.0.0)是两个不同的包,互不覆盖,`pnpm update` 不会把两者串在一起。
155
+
156
+ ## License
157
+
158
+ MIT(沿用上游许可)。