@lvxianchao/dsh-web-guard 2.1.1 → 3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.1 — 2026-11-10
4
+
5
+ ### Fixed
6
+
7
+ - **The proxy no longer leaks upstream sockets when a browser disconnects.** Closing a tab, reloading or dropping the link mid-response left the upstream request alive; for a response that never ends — an SSE stream, a long poll — the socket stayed allocated in the shared keep-alive agent forever. Once `maxSockets` (128) of them had accumulated, every later request through the proxy queued until the reverse proxy in front gave up with `504` — while `/__guard/session` kept answering 200, which made it look like the harness was up. The guard now destroys the upstream request when the client closes and treats the resulting error as expected instead of logging it.
8
+ - **The launch-token exchange is bounded to 10 seconds.** An unanswered exchange used to stay `inflight` forever, stalling every request that awaited it.
9
+
10
+ ### Upgrade
11
+
12
+ 1. `dsh plugin --profile web add @lvxianchao/dsh-web-guard@3.0.1` (or reinstall from the same source you use).
13
+ 2. `sudo systemctl restart dsh-web` (or however the harness is supervised). Sessions survive the restart; open pages reconnect on their own.
14
+
15
+ ## 3.0.0 — 2026-11-06
16
+
17
+ ### Breaking
18
+
19
+ - **Management moved into the harness.** The bottom-right shield button, its iframe overlay and the server-rendered `/__guard/admin` page are gone. A new `dsh.client` bundle registers a native **DSH Web Guard** page in the harness Settings panel (Settings → DSH Web Guard): password change, session lifetime, lockout threshold/duration, listener address/port, the live session list (per-device sign out, kick, sign out everywhere). `/login` and `/logout` stay standalone documents.
20
+ - **Requires DSH `0.2.2-rc.1` or newer** (`engines.dsh: >=0.2.2-rc.1 <0.3.0`). An incompatible host skips the whole bundle — for a front door that means the gate does not start at all, which is the safe direction; upgrade DSH before upgrading the plugin.
21
+ - **The sources are TypeScript now.** The published package still ships plain JS with zero runtime dependencies, so installing needs no toolchain; `npm run build` is a maintainer command.
22
+
23
+ ### Added
24
+
25
+ - Native settings page in the harness, rendering inside the host React tree so it inherits the harness light/dark theme, scrolling, focus handling and mobile layout.
26
+ - JSON API behind it (`/__guard/api/state|settings|password|kick|logout-all`) with strict validation, contract error codes and the same CSRF checks as the login form.
27
+ - Login and sign-out pages follow the harness light/dark preference (the `ui-theme` row the Appearance control writes into the profile patch), fall back to the operating system when that preference is `system`/unset, and accept `?theme=light|dark` as an explicit override.
28
+ - "Restart to apply" is surfaced explicitly: listener changes are saved and reported as configured values plus a pending-restart flag instead of appearing to be lost.
29
+
30
+ ### Changed
31
+
32
+ - The locale reader now shares a generic profile-preference reader with the new theme reader.
33
+ - The plain-HTTP `crypto.randomUUID` shim and the dead-session watchdog in the served index are unchanged; everything else that used to be injected is removed.
34
+ - Settings page copy is localised (English and Chinese) and follows the harness language.
35
+
36
+ ### Removed
37
+
38
+ - The injected fallback shield/overlay entry and the server-rendered `/__guard/admin` document, together with the `/__guard/kill` and `/__guard/logout-all` form routes that only fed it.
39
+ - Admin-only server copy and the old admin screenshots.
40
+
41
+ ### Upgrade
42
+
43
+ 1. `dsh plugin --profile web add @lvxianchao/dsh-web-guard@3.0.0` (or reinstall from the same source you use).
44
+ 2. `sudo systemctl restart dsh-web` (or however the harness is supervised).
45
+ 3. Open **Settings → DSH Web Guard**. The password hash, session cache and settings file keep their v2 layouts, so the upgrade signs nobody out and your configuration carries over.
46
+
47
+ ### Internal
48
+
49
+ - Build: `tsc` for the server half, `esbuild` for the single-file client bundle; `lib/` is committed so npm, `github:` and checkout installs all serve the same prebuilt bytes, and CI rejects stale artifacts.
50
+ - CI: typecheck + build + artifact freshness on every push and pull request; the release workflow builds before publishing.
51
+
3
52
  ## 2.1.1 — 2026-11-05
4
53
 
5
54
  ### Docs
package/README.md CHANGED
@@ -6,11 +6,20 @@
6
6
 
7
7
  <p align="center">
8
8
  A password gate for the <a href="https://github.com/deepseek-ai/deepseek-harness">DeepSeek Harness</a> Web UI —
9
- one plugin that fixes four deployment problems at once.
9
+ one plugin that fixes four deployment problems at once and adds a native settings page.
10
10
  </p>
11
11
 
12
12
  <p align="center"><a href="README.zh.md">中文说明</a></p>
13
13
 
14
+ <p align="center">
15
+ <a href="https://www.npmjs.com/package/@lvxianchao/dsh-web-guard"><img src="https://img.shields.io/npm/v/@lvxianchao/dsh-web-guard?color=0ab887" alt="npm version"></a>
16
+ <a href="LICENSE"><img src="https://img.shields.io/npm/l/@lvxianchao/dsh-web-guard" alt="MIT license"></a>
17
+ <img src="https://img.shields.io/node/v/@lvxianchao/dsh-web-guard" alt="Node.js >= 22.19">
18
+ <img src="https://img.shields.io/badge/runtime_dependencies-0-brightgreen" alt="Zero runtime dependencies">
19
+ <a href="https://github.com/deepseek-ai/deepseek-harness"><img src="https://img.shields.io/badge/DeepSeek_Harness-plugin-0e7490" alt="DeepSeek Harness plugin"></a>
20
+ <img src="https://img.shields.io/badge/DSH-%E2%89%A50.2.2--rc.1%20%3C0.3.0-0e7490" alt="DSH >= 0.2.2-rc.1">
21
+ </p>
22
+
14
23
  | Problem | Vanilla behaviour | dsh-web-guard |
15
24
  |---|---|---|
16
25
  | No human login | A per-process `?token=` printed to the terminal, new on every restart | One password + revocable sessions |
@@ -18,7 +27,9 @@
18
27
  | Token lives in the journal | `journalctl \| grep token` before you can get in | Exchanged in-process; the browser never sees a token |
19
28
  | UI breaks on plain HTTP | `crypto.randomUUID is not a function` outside a secure context | Injects a `getRandomValues` polyfill |
20
29
 
21
- Zero runtime dependencies (Node standard library only), no build step — `dsh plugin add` and it runs.
30
+ Zero runtime dependencies (Node standard library only). The npm package ships prebuilt JavaScript, so `dsh plugin add` needs no toolchain; building from a checkout is a maintainer command (`npm run build`).
31
+
32
+ Requires **DeepSeek Harness `0.2.2-rc.1` or newer** (`engines.dsh: >=0.2.2-rc.1 <0.3.0`). On an incompatible host the whole bundle is skipped — for a front door that means the gate does not start, so upgrade DSH rather than removing the plugin.
22
33
 
23
34
  ## Screenshots
24
35
 
@@ -29,10 +40,10 @@ Zero runtime dependencies (Node standard library only), no build step — `dsh p
29
40
  </p>
30
41
 
31
42
  <p align="center">
32
- <img src="https://raw.githubusercontent.com/lvxianchao/dsh-web-guard/main/docs/admin-en.png" width="92%" alt="Admin page">
43
+ <img src="https://raw.githubusercontent.com/lvxianchao/dsh-web-guard/main/docs/settings-en.png" width="92%" alt="Security settings page">
33
44
  </p>
34
45
 
35
- <p align="center"><sub>Login page &nbsp;·&nbsp; sign-out confirmation &nbsp;·&nbsp; admin page (password, session and lockout policy, listener, live sessions)</sub></p>
46
+ <p align="center"><sub>Login page &nbsp;·&nbsp; sign-out confirmation &nbsp;·&nbsp; native DSH Web Guard page in the harness settings (follows the harness light/dark theme)</sub></p>
36
47
 
37
48
  ## How it works
38
49
 
@@ -46,10 +57,12 @@ browser ──HTTP(S)──> dsh-web-guard :3081 ──> DSH listener (loopback)
46
57
 
47
58
  The harness fence accepts a loopback authority, so making every upstream request look like one removes the 403 entirely. Authentication happens **in the proxy**, not as a front-end overlay — a request without a session cookie never reaches the harness.
48
59
 
60
+ A small browser half (`dsh.client`) adds the management page to the harness Settings panel; the gate itself is entirely server-side and keeps working if the browser half cannot load.
61
+
49
62
  ## Install
50
63
 
51
64
  ```bash
52
- # npm (once published)
65
+ # npm
53
66
  dsh plugin --profile web add @lvxianchao/dsh-web-guard
54
67
 
55
68
  # from GitHub
@@ -78,6 +91,8 @@ defaults):
78
91
  Note that a profile patch replaces the whole `config` object, so restate every
79
92
  key you care about.
80
93
 
94
+ **Upgrading from v2:** the bottom-right shield button and `/__guard/admin` are gone; management lives in **Settings → DSH Web Guard**. Reinstall the package (or update to `3.0.0`) and restart DSH. The password hash, session cache and settings file keep their format, so nobody is signed out and your configuration carries over.
95
+
81
96
  > The harness itself must listen on loopback (its default). If it is bound to a LAN address, clients can bypass the gate by connecting there directly — the plugin detects this at startup and warns.
82
97
 
83
98
  ## ⚠️ This is the front door, not an optional plugin
@@ -140,12 +155,18 @@ Set the upstream to `http://<dsh-host>:3081` and enable WebSocket support. **No
140
155
 
141
156
  ## Language
142
157
 
143
- The login and admin pages follow the harness language setting: the plugin reads
158
+ The login page and the settings surfaces follow the harness language setting: the plugin reads
144
159
  `preference` from the `locale` row in the profile patch file (`~/.dsh/profiles/web/cordis.patch.yml`),
145
160
  which is exactly where the harness settings UI writes it. Chinese and English are
146
161
  supported. With no explicit preference the pages fall back to the browser's
147
162
  `Accept-Language`.
148
163
 
164
+ ## Theme
165
+
166
+ The native settings page is part of the harness, so it simply follows the harness light/dark choice.
167
+
168
+ The login and sign-out pages are standalone documents rendered before the harness page exists, so the server reads the harness choice from the profile patch (`- id: ui-theme` → `config.preference`, the row the Appearance control writes) and forces that palette before first paint. Precedence: `?theme=light|dark` → the harness preference → the operating system (`prefers-color-scheme`) when the preference is `system` or unset.
169
+
149
170
  ## Configuration
150
171
 
151
172
  | Key | Default | Meaning |
@@ -158,7 +179,8 @@ supported. With no explicit preference the pages fall back to the browser's
158
179
  | `maxAttempts` | `5` | Consecutive failures before a temporary lock |
159
180
  | `lockMinutes` | `5` | Lock duration |
160
181
  | `polyfill` | `true` | Inject the `crypto.randomUUID` shim |
161
- | `title` | `DeepSeek Harness` | Heading on the login page |
182
+ | `title` | `DSH Web Guard` | Heading on the login page |
183
+ | `repo` | the project repository | Link shown under the login form |
162
184
  | `tls` | `null` | `{certFile, keyFile}` to serve HTTPS |
163
185
 
164
186
  Generate a hash for the config:
@@ -174,7 +196,7 @@ node -e "import('./lib/auth.js').then(m=>console.log(m.hashPassword(process.argv
174
196
  - Login POSTs are checked for a same-hostname `Origin`. The session cookie's `SameSite` attribute is the primary CSRF defence.
175
197
  - Repeated failures from one IP trigger a temporary lock (in-memory, cleared on restart).
176
198
  - **The session table is cached on disk** (`<profile>/data/dsh-web-guard-sessions-<port>.json`, mode `0600`, atomic tmp+rename), so **restarting DSH no longer signs signed-in browsers out**. The file holds SHA-256 digests, never tokens — neither the cache nor a heap dump can mint a working cookie. The filename carries the listener port, so a shadow instance cannot overwrite the live one.
177
- - `/logout` ends the current browser session. To revoke everything (a suspected leak, say), use the admin page's **sign out all devices** — **a restart is not a revocation any more**.
199
+ - `/logout` ends the current browser session. To revoke everything (a suspected leak, say), use the settings page's **sign out all devices** — **a restart is not a revocation any more**.
178
200
 
179
201
  **This is not multi-user.** One password, one privilege level. The harness agent can execute code, so anyone who gets in has code execution on that host — which is the product's design, not a bug in the gate.
180
202
 
@@ -188,18 +210,18 @@ If the session disappears (password changed, sign-out-all, or no activity within
188
210
 
189
211
  Restarts (upgrades, config changes) are invisible: the session table is restored from disk and the page reconnects on its own.
190
212
 
191
- ## Admin page
213
+ ## Settings page
192
214
 
193
- A shield button appears at the bottom-right of the harness UI once you are signed in. It opens `/__guard/admin` (bookmark the URL if you prefer):
215
+ The management surface is part of the harness itself: **Settings → DSH Web Guard** (the same panel that holds General, Models and Agent presets; the entry is labelled with the plugin name and carries a shield icon).
194
216
 
195
- - change the password (the current one is required)
196
- - session lifetime, lockout threshold/duration, page title, repository link — **applied on save**
217
+ - change the sign-in password (the current one is required)
218
+ - session lifetime, lockout threshold/duration — **applied on save**
197
219
  - listen host/port — marked "takes effect after restart"
198
- - live session list (source IP, browser, last activity): kick one device, or sign out everywhere
220
+ - live session list (source IP, browser, last activity): sign out just this device, kick another device, or sign out everywhere
199
221
 
200
- Anonymous visitors are redirected to the login page with no `next` echo, and the login page does not advertise the admin path.
222
+ The page renders inside the harness React tree, so it inherits the harness light/dark theme, scrolling, focus handling and mobile layout. On a narrow screen it collapses to a single column. There is no floating button and no separate admin URL any more — the settings page is the only management surface.
201
223
 
202
- **Why not a harness settings page instead:** that requires a `dsh.client` bundle (React plus a build step), which takes the plugin from ~30 KB to hundreds of KB, adds a build toolchain to a project that otherwise has none, and ties it to the harness slot API while that API is still moving — a version mismatch can blank the whole settings screen. A standalone page stays build-free and survives harness upgrades.
224
+ **Why v3 makes the settings page native.** The server half still does all the gating, but the page is now a `dsh.client` bundle: a hand-written React module (no bundler for users, React comes from the host module table) registered into the `settings.section` slot. The harness exposes no HTML-injection route into its React tree, so a first-class entry requires exactly this. The trade-off is a declared `engines.dsh` range and a browser half that a future host may skip; the server half (authentication, forwarding, the gate itself) never depends on the browser half.
203
225
 
204
226
  ## Troubleshooting
205
227
 
@@ -210,29 +232,37 @@ Anonymous visitors are redirected to the login page with no `next` echo, and the
210
232
  | Login succeeds then bounces back | Clock skew between the client and the host |
211
233
  | `randomUUID is not a function` | `polyfill` is disabled |
212
234
  | LAN can still reach the harness port | The harness is not bound to loopback — see the startup warning |
235
+ | No **Settings → DSH Web Guard** entry | The host skipped the bundle: it is older than `0.2.2-rc.1`, or the page failed to load. Check `journalctl -u dsh-web | grep dsh-web-guard` and upgrade DSH |
213
236
 
214
237
  All log lines are prefixed `[dsh-web-guard]` and share the harness's journal stream. Successful requests are not logged (the Web UI polls constantly); only failures and slow requests are.
215
238
 
216
239
  ## Development
217
240
 
218
241
  ```bash
219
- # shadow instance: serves the real profile without touching it
220
- dsh --profile web --patch ./dev.yml --host 127.0.0.1 --port 3099 --no-open
221
-
222
- # e2e against a running gate
223
- GUARD_URL=http://127.0.0.1:3081 GUARD_PASSWORD=... node test/e2e.mjs
242
+ npm ci --ignore-scripts # TypeScript + esbuild, build-time only
243
+ npm run typecheck # server and client halves
244
+ npm run build # src/ → lib/ (tsc + one esbuild bundle); lib/ is committed
245
+
246
+ # Shadow instance: serves this checkout through the real profile without editing it.
247
+ # An isolated DSH_HOME keeps the shadow gate away from the live profile's guard state.
248
+ cp -a ~/.dsh/profiles/web /tmp/dsh-shadow-home/profiles/web
249
+ cp dev.yml dev.local.yml # then point `name:` at this checkout's lib/index.js
250
+ DSH_HOME=/tmp/dsh-shadow-home dsh --profile web --patch ./dev.local.yml --host 127.0.0.1 --port 3099 --no-open
251
+
252
+ # e2e against the running gate (dev.yml listens on 3100)
253
+ GUARD_URL=http://127.0.0.1:3100 GUARD_PASSWORD=dev-only-password node test/e2e.mjs
224
254
  ```
225
255
 
226
- `dev.yml` only needs the `insert` row shown above with `listenHost: 127.0.0.1` and a distinct port.
256
+ The plugin uses only public harness APIs: `webServer.tapIndex`, `webServer.getOrigin` and `connection.authenticatedUrl` on the server, and the documented `ctx.slots` / `ctx.locale` services in the client bundle. No monkey-patching, no source modification.
227
257
 
228
- The plugin uses only public harness APIs: `webServer.register`, `webServer.tapIndex`, `webServer.getOrigin`, and `connection.authenticatedUrl`. No monkey-patching, no source modification.
258
+ `lib/client.js` is one classic script that registers a lazy factory through `window.__ModuleLoader__.load()`; React is resolved from the host's shared module table, so it is never bundled twice. CI fails when `lib/` is stale.
229
259
 
230
260
  ## Companion plugin
231
261
 
232
262
  DSH's built-in document preview cannot play video — opening an `.mp4` in the sidebar says the format is unsupported. [dsh-video-preview](https://github.com/lvxianchao/dsh-video-preview) adds a streaming video renderer for the same sidebar: `mp4` / `webm` / `mov`, byte-range seeking, no size cap.
233
263
 
234
264
  ```bash
235
- dsh plugin --profile web add "github:lvxianchao/dsh-video-preview"
265
+ dsh plugin --profile web add @lvxianchao/dsh-video-preview
236
266
  ```
237
267
 
238
268
  The two plugins are independent — install either or both.
package/README.zh.md CHANGED
@@ -6,11 +6,20 @@
6
6
 
7
7
  <p align="center">
8
8
  给 <a href="https://github.com/deepseek-ai/deepseek-harness">DeepSeek Harness</a>(DSH)Web UI 加一道<strong>密码闸门</strong>——
9
- 一个插件同时解决四件部署上的麻烦事。
9
+ 一个插件同时解决四件部署上的麻烦事,并提供原生的设置页。
10
10
  </p>
11
11
 
12
12
  <p align="center"><a href="README.md">English</a></p>
13
13
 
14
+ <p align="center">
15
+ <a href="https://www.npmjs.com/package/@lvxianchao/dsh-web-guard"><img src="https://img.shields.io/npm/v/@lvxianchao/dsh-web-guard?color=0ab887" alt="npm 版本"></a>
16
+ <a href="LICENSE"><img src="https://img.shields.io/npm/l/@lvxianchao/dsh-web-guard" alt="MIT 许可证"></a>
17
+ <img src="https://img.shields.io/node/v/@lvxianchao/dsh-web-guard" alt="Node.js >= 22.19">
18
+ <img src="https://img.shields.io/badge/runtime_dependencies-0-brightgreen" alt="零运行时依赖">
19
+ <a href="https://github.com/deepseek-ai/deepseek-harness"><img src="https://img.shields.io/badge/DeepSeek_Harness-plugin-0e7490" alt="DeepSeek Harness 插件"></a>
20
+ <img src="https://img.shields.io/badge/DSH-%E2%89%A50.2.2--rc.1%20%3C0.3.0-0e7490" alt="DSH >= 0.2.2-rc.1">
21
+ </p>
22
+
14
23
  | 麻烦 | 原生表现 | dsh-web-guard |
15
24
  |---|---|---|
16
25
  | 没有人类登录 | 只有进程启动时打印的 `?token=`,重启就换 | 一个访问密码 + 可吊销会话 |
@@ -18,7 +27,9 @@
18
27
  | token 要挖日志 | `journalctl \| grep token` 才能进站 | 进程内自动兑换,浏览器永远见不到 token |
19
28
  | 明文 HTTP 功能报错 | 非安全上下文下 `crypto.randomUUID is not a function` | 注入 polyfill 兜底 |
20
29
 
21
- 零运行时依赖(只用 Node 标准库),没有构建步骤,`dsh plugin add` 装完即跑。
30
+ 零运行时依赖(只用 Node 标准库)。npm 包里是预构建好的 JavaScript,所以 `dsh plugin add` 不需要任何工具链;从源码 checkout 构建是维护者命令(`npm run build`)。
31
+
32
+ 要求 **DeepSeek Harness `0.2.2-rc.1` 或更新版本**(`engines.dsh: >=0.2.2-rc.1 <0.3.0`)。宿主版本不匹配时整个 bundle 会被跳过——对一道前门来说等于闸门不启动,所以请升级 DSH,而不是删掉插件。
22
33
 
23
34
  ## 截图
24
35
 
@@ -29,10 +40,10 @@
29
40
  </p>
30
41
 
31
42
  <p align="center">
32
- <img src="https://raw.githubusercontent.com/lvxianchao/dsh-web-guard/main/docs/admin.png" width="92%" alt="设置页">
43
+ <img src="https://raw.githubusercontent.com/lvxianchao/dsh-web-guard/main/docs/settings.png" width="92%" alt="原生安全设置页">
33
44
  </p>
34
45
 
35
- <p align="center"><sub>登录页 &nbsp;·&nbsp; 退出确认页 &nbsp;·&nbsp; 设置页(改密码、会话与锁定策略、监听地址、在线设备)</sub></p>
46
+ <p align="center"><sub>登录页 &nbsp;·&nbsp; 退出确认页 &nbsp;·&nbsp; DSH 设置面板里的原生 DSH Web Guard 页(跟随 DSH 明暗主题)</sub></p>
36
47
 
37
48
  ## 它怎么工作
38
49
 
@@ -46,10 +57,12 @@
46
57
 
47
58
  DSH 的 `/api` 栅栏接受回环 authority;代理让每个上游请求都看起来来自回环,所以不再 403。密码验证发生在代理里,**不是前端遮罩**——没有 cookie 的请求在进入 DSH 之前就被挡下。
48
59
 
60
+ 浏览器半(`dsh.client`)只负责把管理页挂进 DSH 的设置面板;闸门本身完全在服务端,即使浏览器半加载失败也照常工作。
61
+
49
62
  ## 安装
50
63
 
51
64
  ```bash
52
- # npm(发布后)
65
+ # npm
53
66
  dsh plugin --profile web add @lvxianchao/dsh-web-guard
54
67
 
55
68
  # 从 GitHub 安装
@@ -73,6 +86,8 @@ dsh plugin --profile web add /path/to/dsh-web-guard
73
86
 
74
87
  注意 profile patch 会整体替换 `config`,要改的键都得重写一遍。
75
88
 
89
+ **从 v2 升级**:右下角的浮动盾牌和 `/__guard/admin` 已移除,管理入口改为 **设置 → DSH Web Guard**。重新安装(或升级到 `3.0.0`)并重启 DSH 即可;密码哈希、会话缓存和设置文件格式不变,升级不会把人踢下线,原有配置也会保留。
90
+
76
91
  > 前提:DSH 自己必须绑回环(默认就是 `127.0.0.1`)。如果它绑在局域网 IP 上,客户端可以绕过闸门直连——插件启动时会检测并告警。
77
92
 
78
93
  ## ⚠️ 这是入口,不是可以随手关掉的插件
@@ -135,11 +150,17 @@ config:
135
150
 
136
151
  ## 语言
137
152
 
138
- 登录页和管理页跟随 DSH 的语言设置:插件读取 profile patch 文件
153
+ 登录页与设置页面跟随 DSH 的语言设置:插件读取 profile patch 文件
139
154
  (`~/.dsh/profiles/web/cordis.patch.yml`)里 `locale` 行的 `preference`,
140
155
  也就是 DSH 设置界面写入的同一个位置。目前支持中文和英文。没有显式偏好时,
141
156
  按浏览器的 `Accept-Language` 回退。
142
157
 
158
+ ## 主题
159
+
160
+ 原生设置页属于 DSH 的一部分,直接跟随 DSH 的明暗设置。
161
+
162
+ 登录页和退出页是 DSH 页面出现之前独立渲染的文档,所以服务端会从 profile patch 里读取 DSH 的选择(`- id: ui-theme` → `config.preference`,也就是「外观」控件写入的那一行),在首屏绘制前强制使用对应配色。优先级:`?theme=light|dark` → DSH 设置 → 系统(`prefers-color-scheme`,仅当 DSH 设为「跟随系统」或从未设置)。
163
+
143
164
  ## 配置
144
165
 
145
166
  | 键 | 默认 | 说明 |
@@ -152,7 +173,8 @@ config:
152
173
  | `maxAttempts` | `5` | 连续失败次数,超过临时锁定 |
153
174
  | `lockMinutes` | `5` | 锁定时长 |
154
175
  | `polyfill` | `true` | 注入 `crypto.randomUUID` 兜底 |
155
- | `title` | `DeepSeek Harness` | 登录页标题 |
176
+ | `title` | `DSH Web Guard` | 登录页标题 |
177
+ | `repo` | 本项目仓库 | 登录页底部的链接 |
156
178
  | `tls` | `null` | `{certFile, keyFile}`,启用 HTTPS |
157
179
 
158
180
  生成密码哈希:
@@ -168,7 +190,7 @@ node -e "import('./lib/auth.js').then(m=>console.log(m.hashPassword(process.argv
168
190
  - 登录 POST 校验 `Origin` 的 hostname 与请求 `Host` 一致(只比 hostname,因为反代常剥端口)。CSRF 的第一道防线仍是 `SameSite`。
169
191
  - 同一 IP 连续失败会临时锁定(内存计数,重启清零)。
170
192
  - **会话表现在会落盘**(`<profile>/data/dsh-web-guard-sessions-<port>.json`,`0600`,临时文件 + rename 原子写入),所以**重启 DSH 不会把已登录的浏览器踢下线**。文件里存的同样是 SHA-256 摘要,不是 token——拿到这个文件或内存 dump 都无法伪造 cookie。文件名带监听端口,影子实例不会覆盖正式实例。
171
- - `/logout` 只退当前浏览器。要全部吊销(比如怀疑泄露),用管理页的**退出所有设备**:**重启不再等于吊销**。
193
+ - `/logout` 只退当前浏览器。要全部吊销(比如怀疑泄露),用设置页的**退出所有设备**:**重启不再等于吊销**。
172
194
 
173
195
  **这不是多用户系统**:一个密码,一个权限级别。DSH 的 agent 本身能执行代码,任何登录者都拥有该机器的执行权限——这是产品设计,不是闸门的缺陷。
174
196
 
@@ -182,18 +204,18 @@ node -e "import('./lib/auth.js').then(m=>console.log(m.hashPassword(process.argv
182
204
 
183
205
  需要重启(升级、改配置)则完全无感:会话表会从磁盘恢复,页面自己重连回来。
184
206
 
185
- ## 管理页
207
+ ## 设置页
186
208
 
187
- 登录后 DSH 界面右下角会出现一个盾牌按钮,点开就是 `/__guard/admin`(也可以直接把地址收藏起来):
209
+ 管理入口现在是 DSH 自己的一部分:**设置 → DSH Web Guard**(与「通用设置 / 模型 / 内置插件」同一个面板,导航项直接显示插件名,带盾牌图标)。
188
210
 
189
211
  - 修改登录密码(必须验证当前密码)
190
- - 会话有效期、失败锁定次数与时长、页面标题、GitHub 链接 —— **保存即生效**
212
+ - 会话有效期、失败锁定次数与时长 —— **保存即生效**
191
213
  - 监听地址/端口 —— 标注「重启后生效」
192
- - 活跃会话列表(来源 IP、浏览器、最后活动):可以踢名单个设备,或一键退出所有设备
214
+ - 活跃会话列表(来源 IP、浏览器、最后活动):可以只退出本设备、踢掉其他设备,或一键退出所有设备
193
215
 
194
- 未登录访问管理页只会被弹回登录页,且不保留跳转路径;登录页上不出现任何管理入口。
216
+ 这个页面渲染在 DSH 的 React 树里,因此直接继承宿主的明暗主题、滚动、焦点和移动端布局;窄屏下自动收成单列。现在已经没有浮动按钮,也没有独立的管理页地址——设置页是唯一的管理入口。
195
217
 
196
- **为什么不做成 DSH 设置页里的一项**:那需要 `dsh.client` 前端模块和构建产物(React + 打包),插件会从约 30 KB 涨到几百 KB,给一个本来没有构建链的项目引入工具链,还会绑死仍在变动的 slot API —— 版本对不上时整个设置页可能白屏。独立页面零构建、零依赖,DSH 升级也不受影响。
218
+ **为什么 v3 把设置页做成原生的**:服务端仍然负责全部鉴权与转发,但页面变成了 `dsh.client` 前端包——一个手写的 React 模块(用户侧不需要打包,React 由宿主模块表提供),注册进 `settings.section` 槽。DSH 没有向 React 树注入 HTML 的官方途径,想要一等公民入口只有这一条路。代价是要声明 `engines.dsh`、且浏览器半在未来的宿主上可能被跳过;但服务端(鉴权、转发、闸门本身)从不依赖浏览器半。
197
219
 
198
220
  ## 排障
199
221
 
@@ -204,38 +226,37 @@ node -e "import('./lib/auth.js').then(m=>console.log(m.hashPassword(process.argv
204
226
  | 登录后立刻弹回 | 客户端与主机时间偏差导致会话判定过期 |
205
227
  | 界面报 `randomUUID` | 把 `polyfill` 关掉了 |
206
228
  | 局域网能直连 DSH 端口 | DSH 绑的不是回环,见启动告警 |
229
+ | 设置里找不到 DSH Web Guard | 宿主跳过了 bundle:版本低于 `0.2.2-rc.1`,或页面加载失败。查 `journalctl -u dsh-web \| grep dsh-web-guard`,并升级 DSH |
207
230
 
208
231
  日志统一带 `[dsh-web-guard]` 前缀,与 DSH 自身日志在同一个 journal 里。成功请求不记录(Web UI 会持续轮询),只记失败和慢请求。
209
232
 
210
233
  ## 开发
211
234
 
212
235
  ```bash
213
- # 影子实例:复用真实 profile,但不改动它
214
- dsh --profile web --patch ./dev.yml --host 127.0.0.1 --port 3099 --no-open
215
-
216
- # 对运行中的闸门跑 e2e
217
- GUARD_URL=http://127.0.0.1:3081 GUARD_PASSWORD=... node test/e2e.mjs
236
+ npm ci --ignore-scripts # TypeScript + esbuild,仅构建期依赖
237
+ npm run typecheck # 服务端 + 客户端类型检查
238
+ npm run build # src/ → lib/(tsc + 一次 esbuild 打包);lib/ 入库
239
+
240
+ # 影子实例:用真实 profile 跑这份 checkout,但不改动它。
241
+ # 用独立的 DSH_HOME 隔离,避免影子闸门读写线上 profile 的 guard 状态。
242
+ cp -a ~/.dsh/profiles/web /tmp/dsh-shadow-home/profiles/web
243
+ cp dev.yml dev.local.yml # 把 `name:` 指向本仓库的 lib/index.js
244
+ DSH_HOME=/tmp/dsh-shadow-home dsh --profile web --patch ./dev.local.yml --host 127.0.0.1 --port 3099 --no-open
245
+
246
+ # 对运行中的闸门跑 e2e(dev.yml 的闸门监听 3100)
247
+ GUARD_URL=http://127.0.0.1:3100 GUARD_PASSWORD=dev-only-password node test/e2e.mjs
218
248
  ```
219
249
 
220
- `dev.yml` 只需要上面那段 `insert`,把 `listenHost` 改成 `127.0.0.1`、端口换开即可。
221
-
222
- `main` 是发布分支,只放已发布并打过 tag 的代码;日常开发在 `dev` 分支,确认发版时才合并回 `main`。
223
-
224
- ```bash
225
- git checkout dev
226
- # … 修改、提交、测试 …
227
- git checkout main && git merge --ff-only dev
228
- git tag -a vX.Y.Z && git push origin main --tags
229
- ```
250
+ 插件只用公开的宿主 API:服务端用 `webServer.tapIndex`、`webServer.getOrigin`、`connection.authenticatedUrl`;客户端用文档化的 `ctx.slots` / `ctx.locale` 服务。没有 monkey-patching,也不改宿主源码。
230
251
 
231
- 插件只用了公开的 harness API:`webServer.register`、`webServer.tapIndex`、`webServer.getOrigin`、`connection.authenticatedUrl`。没有猴子补丁,没有改源码。
252
+ `lib/client.js` 是一个 classic script,通过 `window.__ModuleLoader__.load()` 注册惰性工厂;React 从宿主共享模块表解析,不会被重复打包。`lib/` 与源码不一致时 CI 会失败。
232
253
 
233
254
  ## 配套插件
234
255
 
235
256
  DSH 自带的文档预览放不了视频——在侧边栏点开 `.mp4` 只会提示格式不支持。[dsh-video-preview](https://github.com/lvxianchao/dsh-video-preview) 给同一个侧边栏补上流式视频渲染器:`mp4` / `webm` / `mov`,支持字节范围拖动、没有大小上限。
236
257
 
237
258
  ```bash
238
- dsh plugin --profile web add "github:lvxianchao/dsh-video-preview"
259
+ dsh plugin --profile web add @lvxianchao/dsh-video-preview
239
260
  ```
240
261
 
241
262
  两个插件互相独立,可以只装一个,也可以都装。