kczx-user-management 1.0.0 → 1.0.1

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 CHANGED
@@ -98,6 +98,7 @@ user-management:
98
98
  sites: [] # 空 = 自动枚举本机 IP;配域名 + 证书则按 SNI 选择
99
99
  title: 'DSH 控制台'
100
100
  autoActivate: false # false = 自助注册需管理员启用
101
+ historyMaxBytes: 67108864 # 历史改写缓冲上限(字节,默认 64 MiB,最小 1 MiB)
101
102
  ```
102
103
 
103
104
  ## 数据文件
@@ -130,7 +131,7 @@ node scripts/import-from-dsh-passwords.mjs --from E:\path\to\dsh-passwords --ove
130
131
  ## 测试
131
132
 
132
133
  ```bash
133
- npm test # 83 项:策略纯函数、store、API 层、以及真实网关的端到端用例
134
+ npm test # 85 项:策略纯函数、store、API 层、以及真实网关的端到端用例
134
135
  npm run check # 全量语法检查
135
136
  ```
136
137
 
@@ -151,7 +152,10 @@ dsh plugin --profile web remove kczx-user-management
151
152
  - **门只管走门的人**:若另有插件把 dsh 宿主绑到 `0.0.0.0`(例如 dsh-lan-access),直连宿主端口即可绕过登录,
152
153
  且**绕过流量不会出现在任何一本台账里**。要真正的强制约束,宿主端口必须保持回环。
153
154
  - **token 配额依赖客户端上报**:浏览器侧被禁用或页面未加载时不计费(时长配额不受影响,它在服务端计时)。
154
- - 上游 `@weibaohui/user-management`(0.9.5)与本包 `kczx-user-management`(1.0.0)是两个不同的包,互不覆盖,`pnpm update` 不会把两者串在一起。
155
+ - **会话历史要缓冲后才能改写**:历史响应是**一个 JSON 文档**(长会话实测 9–16 MB),网关必须先缓冲才能做沙盒降级与隐藏字符
156
+ 清洗,因此有 `historyMaxBytes` 上限(默认 64 MiB)。超过上限的会话会 **fail-closed 返回 502**,UI 显示「历史加载失败」——
157
+ 调大该值即可;1.0.0 之前用代理默认的 8 MiB,长会话必然踩中。
158
+ - 上游 `@weibaohui/user-management`(0.9.5)与本包 `kczx-user-management` 是两个不同的包,互不覆盖,`pnpm update` 不会把两者串在一起。
155
159
 
156
160
  ## License
157
161
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kczx-user-management",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "description": "dsh 插件 · 登录门 + 账号 / 权限 / 配额 / 审计台账(dsh-passwords 的功能已并入):HTTPS 登录网关与自签证书、用户增删改查、工作区白名单与归属跟踪、每小时 token 与每日时长配额、沙盒档位下限、上传/git 开关、三本审计台账(登录/访问/操作)、IP 封禁、TOTP 两步验证。",
6
6
  "license": "MIT",
@@ -100,6 +100,19 @@ function createGateway(options) {
100
100
  const SESSION_HISTORY_RE = /^\/api\/session[.\/]history$/
101
101
  /** Hard cap on an inspected request body; over it the request is refused. */
102
102
  const MAX_GATE_BODY = 64 * 1024
103
+ /**
104
+ * Buffer cap for the rewritten session-history response, in bytes.
105
+ *
106
+ * The history RPC answers with the whole replay window in ONE JSON document,
107
+ * so a long session is megabytes — and the clamp/sanitize pass can only run on
108
+ * a buffered body. The cap stops a runaway response from pinning unbounded
109
+ * memory, but it has to clear real sessions: at the proxy's 8 MiB default a
110
+ * multi-hour session failed closed with a 502 and its history became
111
+ * unreadable in the UI. Override with `user-management.historyMaxBytes`.
112
+ */
113
+ const DEFAULT_HISTORY_MAX_BYTES = 64 * 1024 * 1024
114
+ /** Floor accepted from config: under this the rewrite can only fail closed. */
115
+ const MIN_HISTORY_MAX_BYTES = 1024 * 1024
103
116
  /** workspaceId → path, harvested from workspace-list responses (see below). */
104
117
  const workspacePathById = new Map()
105
118
 
@@ -107,6 +120,19 @@ function createGateway(options) {
107
120
  try { return new URL(req.url || '/', 'http://dsh.local').pathname } catch { return '' }
108
121
  }
109
122
 
123
+ /**
124
+ * The history buffer cap as configured right now.
125
+ *
126
+ * Read per request rather than captured at construction, so a settings change
127
+ * rides the plugin's in-place hot reload instead of needing a listener restart.
128
+ */
129
+ function historyBufferCap() {
130
+ const configured = Number(options.historyMaxBytes)
131
+ return Number.isFinite(configured) && configured >= MIN_HISTORY_MAX_BYTES
132
+ ? Math.floor(configured)
133
+ : DEFAULT_HISTORY_MAX_BYTES
134
+ }
135
+
110
136
  /**
111
137
  * Ask the host half for an account's permissions. Kept behind deps so this
112
138
  * module stays cordis-free and testable: no store, no policy, just a lookup.
@@ -144,10 +170,10 @@ function createGateway(options) {
144
170
  /**
145
171
  * Decide whether a response must be rewritten before the client sees it.
146
172
  *
147
- * Today only the workspace list qualifies: an account with a folder allow-list
148
- * must not receive entries outside it. The unrestricted case (empty list, or
149
- * no account match) returns null and streams through untouched, so the common
150
- * path pays nothing.
173
+ * Two responses qualify: the workspace list (an account with a folder
174
+ * allow-list must not receive entries outside it — the unrestricted case
175
+ * returns null and streams through untouched, so it pays nothing) and the
176
+ * session history, which every account gets clamped and sanitized.
151
177
  */
152
178
  function rewritePlanFor(req) {
153
179
  if ((req.method || '').toUpperCase() !== 'POST') return null
@@ -186,6 +212,9 @@ function createGateway(options) {
186
212
  if (SESSION_HISTORY_RE.test(path)) {
187
213
  const allowedMode = perms && typeof perms.sandboxMode === 'string' ? perms.sandboxMode : null
188
214
  return {
215
+ // The whole replay window arrives as one JSON document; the cap has to
216
+ // be sized for a real session, not for a small test fixture.
217
+ maxBytes: historyBufferCap(),
189
218
  transform: (parsed) => {
190
219
  // Clamp first (it rewrites values in place), then sanitize: a hidden
191
220
  // character smuggled into a preset name must not survive the rewrite.
package/src/index.js CHANGED
@@ -690,6 +690,13 @@ const Config = z.object({
690
690
  // account (first admin excepted) into the disabled state until an admin
691
691
  // enables it from the users table; true activates registrations immediately.
692
692
  autoActivate: z.boolean().default(false),
693
+ // Buffer cap for the rewritten session-history response, in bytes. The history
694
+ // RPC answers with the whole replay window in one JSON document — megabytes
695
+ // for a long session — and the sandbox clamp / hidden-character strip can only
696
+ // run on a buffered body. Too small a cap makes history fail closed (HTTP 502)
697
+ // and the UI shows no history at all; the default is sized for multi-hour
698
+ // sessions. Lower it to bound memory, raise it for monsters.
699
+ historyMaxBytes: z.natural().min(1024 * 1024).default(64 * 1024 * 1024),
693
700
  listenHost: z.string().default('0.0.0.0'),
694
701
  port: z.natural().min(1).max(65535).default(19843),
695
702
  sites: z
@@ -1154,6 +1161,7 @@ const plugin = {
1154
1161
  sites,
1155
1162
  certsDir,
1156
1163
  title: cfg.title,
1164
+ historyMaxBytes: cfg.historyMaxBytes,
1157
1165
  decider,
1158
1166
  handleApi,
1159
1167
  renderLoginPage,