@mrrisega/dsh-remote 0.6.7-beta.4 → 0.6.7

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.
@@ -799,7 +799,9 @@ const SCRIPT = `(() => {
799
799
  });
800
800
  let strikes = 0;
801
801
  const noteStatus = (status) => {
802
- if (status !== 401 && status !== 403) { strikes = 0; return; }
802
+ // 只认 401:中继对"会话失效"回 401,对"设备不属于本账号"回 403 —— 后者不是会话过期,
803
+ // 拿它提示"请重新登录"会把人引到错误的动作上。
804
+ if (status !== 401) { strikes = 0; return; }
803
805
  strikes += 1;
804
806
  if (strikes >= 2) bar.hidden = false;
805
807
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrisega/dsh-remote",
3
- "version": "0.6.7-beta.4",
3
+ "version": "0.6.7",
4
4
  "description": "手机远程控制 DeepSeek Harness · Remote control DeepSeek Harness (dsh web) from any phone browser — 100% 全功能 App 级体验:发消息、看工具执行、审批权限、改设置、管凭据,含特权操作,免内网穿透。一条命令安装 npx @mrrisega/dsh-remote。Mobile remote control for DSH, self-host or SaaS, no server needed on LAN.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -39,7 +39,8 @@
39
39
  "clients/dsh-remote",
40
40
  "packages/dsh-remote-web",
41
41
  "docs/self-hosting.md",
42
- "docs/telemetry.md"
42
+ "docs/telemetry.md",
43
+ "scripts/test-net-guard.cjs"
43
44
  ],
44
45
  "workspaces": [
45
46
  "packages/*",
@@ -50,10 +51,10 @@
50
51
  },
51
52
  "scripts": {
52
53
  "test": "npm run test:router && npm run test:plugin && npm run test:bridge",
53
- "test:router": "node --test packages/relay-router/test/native-device-selection.test.mjs packages/relay-router/test/presence-report.test.mjs packages/relay-router/test/logger.test.mjs",
54
+ "test:router": "NODE_OPTIONS=\"--require $PWD/scripts/test-net-guard.cjs ${NODE_OPTIONS:-}\" node --test packages/relay-router/test/native-device-selection.test.mjs packages/relay-router/test/native-app-entry.test.mjs packages/relay-router/test/presence-report.test.mjs packages/relay-router/test/logger.test.mjs",
54
55
  "_comment:test:plugin": "DSH_RELAY_SKIP_SERVICE=1 = 测试隔离:插件加载时不做真实系统操作(npx 补装 / launchctl / 重启 harness);需要真实分支的用例自行 unset(见 account-switch / runtime-provision / harness-restart 用例注释)",
55
- "test:plugin": "DSH_REMOTE_TELEMETRY=0 DSH_RELAY_SKIP_SERVICE=1 node --test --test-concurrency=1 packages/dsh-remote-web/test/*.test.mjs",
56
- "test:bridge": "node --test clients/dsh-remote/test/*.test.mjs",
56
+ "test:plugin": "DSH_REMOTE_TELEMETRY=0 DSH_RELAY_SKIP_SERVICE=1 NODE_OPTIONS=\"--require $PWD/scripts/test-net-guard.cjs ${NODE_OPTIONS:-}\" node --test --test-concurrency=1 packages/dsh-remote-web/test/*.test.mjs",
57
+ "test:bridge": "NODE_OPTIONS=\"--require $PWD/scripts/test-net-guard.cjs ${NODE_OPTIONS:-}\" node --test clients/dsh-remote/test/*.test.mjs",
57
58
  "check": "node --check clients/dsh-remote/dsh-bridge.mjs && node --check clients/dsh-remote/e2ee-shim.mjs && node --check dsh-setup.mjs && node --check packages/dsh-remote-web/lib/index.js && node --check packages/dsh-remote-web/lib/client.js && node --check packages/relay-router/src/index.mjs && node --check packages/relay-router/src/logger.mjs && bash -n deploy/install-open.sh && grep -q 'command -v setsid' deploy/install-open.sh && grep -q 'command -v ss' deploy/install-open.sh",
58
59
  "release:tarballs": "bash scripts/release-tarballs.sh"
59
60
  },
@@ -2379,7 +2379,7 @@ const PLUGIN_ID = "dsh-remote-web";
2379
2379
  const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
2380
2380
  const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
2381
2381
  /** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
2382
- const PLUGIN_VERSION = "0.6.7-beta.4";
2382
+ const PLUGIN_VERSION = "0.6.7";
2383
2383
  const UPDATE_LOG = ".dsh-update.log";
2384
2384
  const UPDATE_MARKER = ".dsh-update-running";
2385
2385
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-web",
3
- "version": "0.6.7-beta.4",
3
+ "version": "0.6.7",
4
4
  "description": "公网远程控制 DeepSeek Harness(dsh web):安装即得专属加密地址,人在外面也能用手机访问电脑上的 dsh——无需同一局域网/WiFi、无需公网 IP 与内网穿透,全程加密;手机端 100% 还原电脑体验(对话/工具/审批/设置)。技术用户可选自建服务,流量走自己的服务器。Remote control DeepSeek Harness (dsh web) from anywhere over the public internet — install, get an encrypted URL, use it from your phone on any network (dual-half cordis plugin: Settings panel + same-origin /dsh-remote routes). (2026-09 由 dsh-remote-ui 更名 / renamed from dsh-remote-ui; dsh-remote 的 dsh web 插件半,不是纯 UI/皮肤插件)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -87,7 +87,11 @@ async function plantProfile(home, { extraPatch = "", unmarked = false } = {}) {
87
87
  fs.writeFileSync(path.join(local, "lib", "index.js"), "export const x = 1;\n");
88
88
  fs.symlinkSync(local, path.join(profile, "node_modules", PLUGIN_ID), "dir");
89
89
  fs.mkdirSync(path.join(home, "relay"), { recursive: true });
90
- fs.writeFileSync(path.join(home, "relay", ".dsh-config.json"), JSON.stringify({ phone: "13800000000", password: "pw" }));
90
+ // ⚠️ api_url 必须指向本地死端口:这些是**假账号**,一旦按默认地址请求生产,
91
+ // /api/device-login 会带着假密码失败,而服务端登录限流是按出口 IP 计的(5 次/15 分钟)——
92
+ // 实测结果是把**本机真实的 bridge** 连坐进限流(2026-09-19 事故)。
93
+ fs.writeFileSync(path.join(home, "relay", ".dsh-config.json"),
94
+ JSON.stringify({ phone: "13800000000", password: "pw", api_url: "http://127.0.0.1:1" }));
91
95
  return { profile, patchFile: path.join(profile, "cordis.patch.yml") };
92
96
  }
93
97
 
@@ -460,6 +460,24 @@ test("前端:bridge-status 轮询失败不再静默(旧版让面板永久停
460
460
  assert.ok(/connFailRef\.v = 0/.test(CLIENT_SRC), "成功必须清零(一次抖动不该把面板染红)");
461
461
  });
462
462
 
463
+ test("测试隔离:套件不得访问任何外部地址(生产限流会被测试污染)", () => {
464
+ // 事故(2026-09-19):有用例在配置里写了假账号却没覆盖 api_url → 按默认地址请求**生产中继**,
465
+ // 其中 /api/device-login 带假密码必然失败,而服务端登录限流按**出口 IP** 计(5 次/15 分钟)——
466
+ // 结果不仅污染生产审计,还让**本机自己的 bridge 被连坐**(拿不到 JWT),
467
+ // 真机表现成"切换账号后设备一直登记不上去",排查方向被彻底带偏。
468
+ const repoRoot = path.join(HERE, "..", "..", "..");
469
+ const guard = path.join(repoRoot, "scripts", "test-net-guard.cjs");
470
+ assert.ok(existsSync(guard), "必须有测试期网络护栏脚本");
471
+ const pkg = JSON.parse(readFileSync(path.join(repoRoot, "package.json"), "utf8"));
472
+ for (const name of ["test:router", "test:plugin", "test:bridge"]) {
473
+ assert.match(String(pkg.scripts[name]), /test-net-guard\.cjs/, `${name} 必须注入网络护栏`);
474
+ }
475
+ // 护栏本体:只放行本地地址,其余一律阻断
476
+ const src = readFileSync(guard, "utf8");
477
+ assert.ok(/ENETUNREACH/.test(src), "护栏应阻断外部请求");
478
+ assert.ok(/LOCAL_HOST/.test(src), "护栏应只放行本地地址");
479
+ });
480
+
463
481
  test("前端:平台文案不再默认 macOS(Windows 用户看不懂「自启动服务」)", () => {
464
482
  assert.ok(/serviceManager === "detached"/.test(CLIENT_SRC), "面板应识别 Windows 的 detached 模式");
465
483
  assert.match(CLIENT_SRC, /Windows 任务计划程序 dsh-remote-bridge/, "卸载说明要写明 Windows 侧会删掉什么");
@@ -0,0 +1,49 @@
1
+ /**
2
+ * 测试期网络护栏(NODE_OPTIONS=--require 注入,见 package.json 的 test 脚本)。
3
+ *
4
+ * 为什么必须有(2026-09-19 实测事故):
5
+ * 有测试用例在 profile 配置里写了**假账号**(phone/password)却没覆盖 `api_url`,
6
+ * 于是插件按默认地址去请求**生产中继** `https://n.risegao.cn:…`。其中
7
+ * `/api/device-login` 带着假密码必然失败,而服务端的登录限流是**按出口 IP**
8
+ * 统计「5 次失败 / 15 分钟」——结果是:
9
+ * · 生产审计日志被测试污染;
10
+ * · **本机自己的 bridge 被连坐**:限流窗口内它拿不到 JWT(429),
11
+ * 真机表现为「切换账号后设备一直登记不上去」,排查方向被彻底带偏。
12
+ *
13
+ * 现在的行为:测试期**一律阻断**任何非本地请求(throw ENETUNREACH),
14
+ * 并在进程退出时把被拦下的地址打印出来,提醒用例作者去补 `api_url`。
15
+ * 这既不改变用例结论(需要断网的分支照常走失败路径),又保证测试永远碰不到外部服务。
16
+ */
17
+ const realFetch = globalThis.fetch;
18
+ const blocked = new Set();
19
+
20
+ const LOCAL_HOST = /^(127\.0\.0\.1|localhost|\[::1\]|0\.0\.0\.0|::1)(:\d+)?$/i;
21
+
22
+ function hostOf(input) {
23
+ try {
24
+ const url = typeof input === "string" ? input : input && input.url ? input.url : String(input);
25
+ return new URL(url).host;
26
+ } catch {
27
+ return "";
28
+ }
29
+ }
30
+
31
+ globalThis.fetch = function guardedFetch(input, init) {
32
+ const host = hostOf(input);
33
+ if (host && !LOCAL_HOST.test(host)) {
34
+ const raw = typeof input === "string" ? input : (input && input.url) || "";
35
+ blocked.add(String(raw).replace(/(token|password|secret)=[^&]*/gi, "$1=***").slice(0, 160));
36
+ const err = new Error(`test-net-guard: 测试期禁止访问外部地址 ${host}(请在用例里把 api_url 指向本地假服务)`);
37
+ err.code = "ENETUNREACH";
38
+ return Promise.reject(err);
39
+ }
40
+ return realFetch.call(this, input, init);
41
+ };
42
+
43
+ // 子进程(安装器 / 清理助手等)继承 NODE_OPTIONS,也要能看到告警
44
+ process.on("exit", () => {
45
+ if (!blocked.size) return;
46
+ process.stderr.write(
47
+ `\n[test-net-guard] ⚠️ 有 ${blocked.size} 个外部请求被拦下(用例应改用本地假服务,别打生产):\n `
48
+ + [...blocked].join("\n ") + "\n\n");
49
+ });