dsh-hooks 0.6.0 → 0.7.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.
package/README.md CHANGED
@@ -189,7 +189,7 @@ CLI/headless environments are unaffected: the browser half loads only in the web
189
189
 
190
190
  ## Web profile HTTP routes
191
191
 
192
- In the web profile (when the shared webServer service exists) dsh-hooks registers loopback-only `/dsh-hooks/*` routes — CLI/headless environments never see them:
192
+ In the web profile (when the shared webServer service exists) dsh-hooks registers `/dsh-hooks/*` routes, restricted to loopback by default and configurable through the environment variable below — CLI/headless environments never see them:
193
193
 
194
194
  | Route | Method | Purpose |
195
195
  | --- | --- | --- |
@@ -205,7 +205,77 @@ In the web profile (when the shared webServer service exists) dsh-hooks register
205
205
  | `/dsh-hooks/feishu/test` | POST | send a test card with the stored credentials |
206
206
  | `/dsh-hooks/feishu/disconnect` | POST | disconnect: delete the credential file; `removeHooks: true` also drops the hooks referencing notify-feishu.mjs (with a backup) |
207
207
 
208
- Security matches dsh-aionui-panel: loopback-only, POSTs require `application/json` (blocks cross-site form CSRF). The web profile also gets a systemPrompt section announcing the plugin to agents.
208
+ POSTs require `application/json` in every access mode (blocks cross-site form CSRF). The web profile also gets a systemPrompt section announcing the plugin to agents.
209
+
210
+ ### Configure HTTP source IP access
211
+
212
+ Set `DSH_HOOKS_ALLOWED_IPS` in the **environment of the process running `dsh web`**. This is not a `cordis.patch.yml` field and does not require changing your hooks configuration.
213
+
214
+ | Environment variable value | Behavior |
215
+ | --- | --- |
216
+ | Unset, empty, or whitespace-only | Allows only `127.0.0.1`, `::1`, and `::ffff:127.0.0.1`, preserving the default behavior |
217
+ | `*` | Disables source IP filtering |
218
+ | `192.168.1.100,10.0.0.2` | Allows only IPs in the comma-separated list |
219
+
220
+ Leading and trailing whitespace is removed. `local` and `all` are not special values: anything other than a blank value or a standalone `*` is matched as an IP list. Allowlist mode **does not implicitly allow loopback connections**; include `127.0.0.1,::1` explicitly if you need local access.
221
+
222
+ Matching ignores surrounding whitespace, letter case, and the `::ffff:` prefix on each address, so `192.168.1.100` matches `::ffff:192.168.1.100`. Hostnames, ports, CIDR ranges, and wildcards within a list are not supported. Invalid entries do not trigger a fallback to loopback-only or unrestricted access. IPv6 matching compares strings after this normalization, without expanding or compressing IPv6 notation; use the representation observed by the server.
223
+
224
+ #### Direct startup
225
+
226
+ PowerShell: choose one setting and start the service in the **same terminal**.
227
+
228
+ ```powershell
229
+ # Loopback only (leaving the variable unset also works)
230
+ $env:DSH_HOOKS_ALLOWED_IPS = ''
231
+
232
+ # Alternatively: allow a client and retain local access
233
+ # $env:DSH_HOOKS_ALLOWED_IPS = '192.168.1.100,127.0.0.1,::1'
234
+
235
+ # Alternatively: allow any source IP (secure external access first)
236
+ # $env:DSH_HOOKS_ALLOWED_IPS = '*'
237
+
238
+ dsh web
239
+ ```
240
+
241
+ Linux/macOS shell: choose one of these commands.
242
+
243
+ ```sh
244
+ DSH_HOOKS_ALLOWED_IPS='' dsh web
245
+ DSH_HOOKS_ALLOWED_IPS='192.168.1.100,127.0.0.1,::1' dsh web
246
+ DSH_HOOKS_ALLOWED_IPS='*' dsh web
247
+ ```
248
+
249
+ Restart the corresponding `dsh web` process after changing its terminal or service-manager environment; a running process does not inherit subsequent changes. Writing the variable to `.env` alone does not pass it to the process; your launcher or container configuration must load it explicitly.
250
+
251
+ #### Docker Compose
252
+
253
+ Add the variable to `environment` on the **service running DSH**, preserving its existing image, ports, volumes, and other settings. Replace the example service name `dsh` with your actual service name:
254
+
255
+ ```yaml
256
+ services:
257
+ dsh:
258
+ environment:
259
+ DSH_HOOKS_ALLOWED_IPS: "192.168.1.100,127.0.0.1,::1"
260
+ # Use "" for loopback only or "*" for unrestricted IPs (quote the asterisk).
261
+ ```
262
+
263
+ Recreate the service container to apply the new environment, for example with `docker compose up -d --force-recreate dsh`. Restarting an existing container alone does not update its environment configuration. If using a Compose `.env` file, also reference the variable through the service's `environment` or pass it through `env_file`.
264
+
265
+ #### Proxies, security, and verification
266
+
267
+ - The check uses `req.socket.remoteAddress`, ignoring `X-Forwarded-For`, `X-Real-IP`, and `Forwarded`. Behind Docker/NAT/reverse proxies, this may be a gateway or proxy IP instead of the browser machine's IP.
268
+ - Allowlisting a proxy IP allows all clients forwarded by that proxy. A local proxy can also forward external requests as loopback connections. Enforce client restrictions at the proxy. Loopback-only refers to the server's (or container's) loopback connections, not exclusively to browsers on that machine.
269
+ - `*` removes the source IP restriction from sensitive operations including history access, configuration changes, and hook execution. An IP allowlist is not authentication: protect these endpoints with a trusted network or external authentication, and do not expose them directly to untrusted networks.
270
+ - This variable affects only `/dsh-hooks/*`; it does not change listen addresses, ports, firewall rules, or other plugins' permissions.
271
+
272
+ Request `GET /dsh-hooks/status` from both allowed and denied clients using your actual host and port. Requests passing this plugin's check receive the normal status JSON; requests rejected by this plugin receive HTTP 403:
273
+
274
+ ```json
275
+ {"ok":false,"error":{"code":"forbidden","message":"IP not allowed"}}
276
+ ```
277
+
278
+ If an allowlisted client still receives this error, confirm the variable reached the actual service process, then check whether the server sees the client IP or a proxy/gateway IP. For connection timeouts or refused connections, also check listen addresses, port mappings, and network rules.
209
279
 
210
280
  ## Feishu notification example
211
281
 
package/README.zh.md CHANGED
@@ -171,7 +171,7 @@ CLI/headless 环境完全不受影响:浏览器半只在 web 加载,核心
171
171
 
172
172
  ## Web profile HTTP 路由
173
173
 
174
- web profile 里(存在共享 webServer 服务时)dsh-hooks 自动注册 loopback-only 的 `/dsh-hooks/*` 路由——CLI/headless 环境完全无感:
174
+ web profile 里(存在共享 webServer 服务时)dsh-hooks 自动注册 `/dsh-hooks/*` 路由,默认仅允许本地回环地址,可通过下述环境变量调整——CLI/headless 环境完全无感:
175
175
 
176
176
  | 路由 | 方法 | 用途 |
177
177
  | --- | --- | --- |
@@ -187,7 +187,77 @@ web profile 里(存在共享 webServer 服务时)dsh-hooks 自动注册 loop
187
187
  | `/dsh-hooks/feishu/test` | POST | 用已存凭据发送测试卡片 |
188
188
  | `/dsh-hooks/feishu/disconnect` | POST | 断开连接:删除凭据文件,`removeHooks: true` 时一并移除 patch 中引用 notify-feishu.mjs 的 hooks(带备份) |
189
189
 
190
- 安全约定与 dsh-aionui-panel 一致:仅回环地址可达、POST 必须 `application/json`(防跨站表单 CSRF)。同时 web profile 下会向 agent 注入一段 systemPrompt 公告,说明插件存在与协作方式。
190
+ 所有访问模式下,POST 仍必须使用 `application/json`(防跨站表单 CSRF)。同时 web profile 下会向 agent 注入一段 systemPrompt 公告,说明插件存在与协作方式。
191
+
192
+ ### 配置 HTTP 来源 IP 限制
193
+
194
+ 在**运行 `dsh web` 的进程环境**中设置 `DSH_HOOKS_ALLOWED_IPS`。这不是 `cordis.patch.yml` 的配置字段,不需要修改 hooks 配置。
195
+
196
+ | 环境变量值 | 行为 |
197
+ | --- | --- |
198
+ | 未设置、空字符串或只有空白 | 仅允许 `127.0.0.1`、`::1`、`::ffff:127.0.0.1`,保持默认行为 |
199
+ | `*` | 不限制来源 IP |
200
+ | `192.168.1.100,10.0.0.2` | 只允许逗号分隔列表中的 IP |
201
+
202
+ 变量值首尾空白会被去除。`local`、`all` 不是特殊值;除空值和单独的 `*` 外,其他值都作为 IP 列表匹配。白名单模式**不会额外放行本地连接**,如需保留本地访问,请显式加入 `127.0.0.1,::1`。
203
+
204
+ 匹配时会忽略每项首尾空白、字母大小写及 `::ffff:` 前缀,例如 `192.168.1.100` 可以匹配 `::ffff:192.168.1.100`。不支持域名、端口、CIDR 网段或列表内通配符;无效条目不会自动回退到仅本地或不限制模式。IPv6 采用上述规则处理后的字符串比较,不会统一展开/压缩写法,请使用与服务端所见地址一致的写法。
205
+
206
+ #### 直接启动
207
+
208
+ PowerShell:选择一种设置,在**同一终端**启动服务。
209
+
210
+ ```powershell
211
+ # 仅本地(不设置该变量也可以)
212
+ $env:DSH_HOOKS_ALLOWED_IPS = ''
213
+
214
+ # 或:允许指定客户端,并保留本地访问
215
+ # $env:DSH_HOOKS_ALLOWED_IPS = '192.168.1.100,127.0.0.1,::1'
216
+
217
+ # 或:不限制来源 IP(请先确保外部访问控制可靠)
218
+ # $env:DSH_HOOKS_ALLOWED_IPS = '*'
219
+
220
+ dsh web
221
+ ```
222
+
223
+ Linux/macOS shell:以下命令三选一。
224
+
225
+ ```sh
226
+ DSH_HOOKS_ALLOWED_IPS='' dsh web
227
+ DSH_HOOKS_ALLOWED_IPS='192.168.1.100,127.0.0.1,::1' dsh web
228
+ DSH_HOOKS_ALLOWED_IPS='*' dsh web
229
+ ```
230
+
231
+ 修改终端或服务管理器中的环境变量后,需要重新启动对应的 `dsh web` 进程;已运行的进程不会自动继承新值。单独把变量写入 `.env` 不代表已经传入进程,需要由启动器或容器配置明确加载。
232
+
233
+ #### Docker Compose
234
+
235
+ 将变量加入**实际运行 DSH 的服务**的 `environment`,保留原有镜像、端口、卷等配置。以下 `dsh` 为示例服务名,请替换成自己的服务名:
236
+
237
+ ```yaml
238
+ services:
239
+ dsh:
240
+ environment:
241
+ DSH_HOOKS_ALLOWED_IPS: "192.168.1.100,127.0.0.1,::1"
242
+ # 仅本地用 "";不限制用 "*"(星号必须加引号)
243
+ ```
244
+
245
+ 修改后重新创建该服务的容器以应用新环境变量,例如 `docker compose up -d --force-recreate dsh`;仅重启已有容器不会更新容器环境配置。若使用 Compose 的 `.env` 文件,也需要在服务中通过 `environment` 引用变量或通过 `env_file` 传入。
246
+
247
+ #### 代理、安全与验证
248
+
249
+ - 检查的是 `req.socket.remoteAddress`,不读取 `X-Forwarded-For`、`X-Real-IP` 或 `Forwarded`。Docker/NAT/反向代理下,该地址可能是网关或代理 IP,而不是浏览器所在机器的 IP。
250
+ - 放行代理 IP 会放行经该代理转发的所有客户端;本机代理也可能将外部请求转发为回环连接。因此,代理后的客户端限制应在代理层执行。“仅本地”指服务端(或容器内)的回环连接,不等于只允许本机浏览器。
251
+ - `*` 会放开读取历史、修改配置、执行 hook 等敏感接口的来源 IP 限制;IP 白名单不是身份认证,请用可信网络或外部认证保护这些接口,不要直接暴露到不可信网络。
252
+ - 此变量只影响 `/dsh-hooks/*`,不会改变服务监听地址、端口、防火墙规则或其他插件的权限。
253
+
254
+ 可从允许及不允许的客户端分别请求 `GET /dsh-hooks/status`(主机和端口替换为实际地址)。通过本插件的检查时返回正常状态 JSON;被本插件拒绝时返回 HTTP 403:
255
+
256
+ ```json
257
+ {"ok":false,"error":{"code":"forbidden","message":"IP not allowed"}}
258
+ ```
259
+
260
+ 若白名单配置后仍收到该错误,先确认变量已传入实际服务进程,再检查服务端看到的是客户端 IP 还是代理/网关 IP。若连接超时或被拒绝连接,则还需检查监听地址、端口映射和网络规则。
191
261
 
192
262
  ## 通用 webhook 示例
193
263
 
package/lib/server.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * to the profile's cordis.patch.yml with a backup), and the Feishu connect
6
6
  * flow (QR setup / cancel / config / test card / disconnect). Registered
7
7
  * only when the shared webserver service exists (web profile) — CLI/headless
8
- * environments never see them. Loopback-only with JSON envelopes; POSTs
8
+ * environments never see them. Loopback-only by default, with JSON envelopes; POSTs
9
9
  * require an explicit application/json content-type (CSRF hardening, same
10
10
  * posture as dsh-aionui-panel).
11
11
  */
@@ -25,7 +25,7 @@ export interface WebServerLike {
25
25
  }
26
26
  /** Plugin version, read from package.json (this package ships its own). */
27
27
  export declare function pluginVersion(): string;
28
- /** Loopback fence: never let a LAN client reach /dsh-hooks operations. */
28
+ /** DSH_HOOKS_ALLOWED_IPS: unset/empty = loopback; * = any; otherwise comma-separated IPs. */
29
29
  export declare function isLoopbackRequest(req: IncomingMessage): boolean;
30
30
  export interface FeishuRouteDeps {
31
31
  /** QR-scan session manager (one in-flight flow at a time). */
package/lib/server.js CHANGED
@@ -22,9 +22,17 @@ function json(res, envelope, status = 200) {
22
22
  res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
23
23
  res.end(JSON.stringify(envelope));
24
24
  }
25
- /** Loopback fence: never let a LAN client reach /dsh-hooks operations. */
25
+ /** DSH_HOOKS_ALLOWED_IPS: unset/empty = loopback; * = any; otherwise comma-separated IPs. */
26
26
  export function isLoopbackRequest(req) {
27
+ const allowedIps = process.env.DSH_HOOKS_ALLOWED_IPS?.trim() ?? '';
28
+ if (allowedIps === '*')
29
+ return true;
30
+ // Check the direct peer only; never trust forwarded headers.
27
31
  const address = req.socket.remoteAddress ?? '';
32
+ if (allowedIps !== '') {
33
+ const normalize = (ip) => ip.trim().toLowerCase().replace(/^::ffff:/, '');
34
+ return address !== '' && allowedIps.split(',').some((ip) => normalize(ip) === normalize(address));
35
+ }
28
36
  return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1';
29
37
  }
30
38
  async function readJsonBody(req) {
@@ -78,7 +86,7 @@ export function createHookHandler(options) {
78
86
  const resolvePatch = options.resolvePatchFile ?? patchFilePath;
79
87
  return async (req, res) => {
80
88
  if (!isLoopbackRequest(req)) {
81
- json(res, FAIL('forbidden', 'loopback-only'), 403);
89
+ json(res, FAIL('forbidden', 'IP not allowed'), 403);
82
90
  return;
83
91
  }
84
92
  const url = new URL(req.url ?? '/', 'http://x');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-hooks",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "packageManager": "pnpm@11.21.0",
5
5
  "description": "Config-driven lifecycle hooks plugin for DeepSeek Harness: declare event -> command hooks in cordis.patch.yml, no plugin code required. Includes a Hooks section in the Web GUI settings (history timeline + manual tester + notify tests + hook editor + Feishu connect).",
6
6
  "author": "PeterBon",
@@ -73,10 +73,10 @@
73
73
  },
74
74
  "devDependencies": {
75
75
  "@deepseek-ai/cordis": "^4.0.1",
76
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
77
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
78
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
79
- "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
76
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8",
77
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8",
78
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.8",
79
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
80
80
  "@deepseek-ai/schemastery": "^3.18.1",
81
81
  "@tsdown/css": "^0.22.14",
82
82
  "@types/node": "^26.2.0",
@@ -86,7 +86,7 @@
86
86
  "react-dom": "^18.3.1",
87
87
  "tsdown": "^0.22.2",
88
88
  "typescript": "^7.0.2",
89
- "vitest": "^4.1.10"
89
+ "vitest": "^4.1.11"
90
90
  },
91
91
  "dependencies": {
92
92
  "@larksuiteoapi/node-sdk": "^1.73.0",