@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 +49 -0
- package/README.md +53 -23
- package/README.zh.md +52 -31
- package/lib/auth.js +244 -232
- package/lib/client.js +1071 -0
- package/lib/i18n.js +49 -247
- package/lib/index.js +573 -465
- package/lib/inject.js +20 -69
- package/lib/page.js +52 -62
- package/lib/polyfill.js +9 -8
- package/lib/preferences.js +144 -0
- package/lib/proxy.js +178 -156
- package/lib/shared/api.js +19 -0
- package/lib/shared/theme.js +109 -0
- package/package.json +36 -4
- package/lib/admin.js +0 -190
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)
|
|
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/
|
|
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 · sign-out confirmation ·
|
|
46
|
+
<p align="center"><sub>Login page · sign-out confirmation · 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
|
|
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
|
|
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` | `
|
|
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
|
|
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
|
-
##
|
|
213
|
+
## Settings page
|
|
192
214
|
|
|
193
|
-
|
|
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
|
|
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
|
|
220
|
+
- live session list (source IP, browser, last activity): sign out just this device, kick another device, or sign out everywhere
|
|
199
221
|
|
|
200
|
-
|
|
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
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
|
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
|
|
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/
|
|
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>登录页 · 退出确认页 ·
|
|
46
|
+
<p align="center"><sub>登录页 · 退出确认页 · 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
|
-
|
|
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` | `
|
|
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
|
-
|
|
209
|
+
管理入口现在是 DSH 自己的一部分:**设置 → DSH Web Guard**(与「通用设置 / 模型 / 内置插件」同一个面板,导航项直接显示插件名,带盾牌图标)。
|
|
188
210
|
|
|
189
211
|
- 修改登录密码(必须验证当前密码)
|
|
190
|
-
-
|
|
212
|
+
- 会话有效期、失败锁定次数与时长 —— **保存即生效**
|
|
191
213
|
- 监听地址/端口 —— 标注「重启后生效」
|
|
192
|
-
- 活跃会话列表(来源 IP
|
|
214
|
+
- 活跃会话列表(来源 IP、浏览器、最后活动):可以只退出本设备、踢掉其他设备,或一键退出所有设备
|
|
193
215
|
|
|
194
|
-
|
|
216
|
+
这个页面渲染在 DSH 的 React 树里,因此直接继承宿主的明暗主题、滚动、焦点和移动端布局;窄屏下自动收成单列。现在已经没有浮动按钮,也没有独立的管理页地址——设置页是唯一的管理入口。
|
|
195
217
|
|
|
196
|
-
|
|
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
|
-
#
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
259
|
+
dsh plugin --profile web add @lvxianchao/dsh-video-preview
|
|
239
260
|
```
|
|
240
261
|
|
|
241
262
|
两个插件互相独立,可以只装一个,也可以都装。
|