dsh-pocket 1.11.3 → 1.12.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.en.md CHANGED
@@ -85,13 +85,14 @@ Settings → **Phone access** → scan the "📶 LAN" QR code → enter the **LA
85
85
 
86
86
  ### Public (from anywhere)
87
87
 
88
- On the same page click "**Enable anywhere**" → wait for the tunnel (first run downloads cloudflared; macOS/Linux use the Tsinghua mirror, seconds) → scan the "🌐 Public" QR code → the phone opens the link and **enters the 8-digit PIN** (shown in the settings page's public section; **rotated on every tunnel start by default**, or **Customize** it to a fixed PIN that is never rotated) → works from outside (4G / office network).
88
+ On the same page click "**Enable anywhere**" → **a security disclaimer pops up every time — check "I understand and agree" to proceed** (on a corporate/classified network, confirm compliance first) → wait for the tunnel (first run downloads cloudflared; macOS/Linux use the Tsinghua mirror, seconds) → scan the "🌐 Public" QR code → the phone opens the link and **enters the 8-digit PIN** (shown in the settings page's public section; **rotated on every tunnel start by default**, or **Customize** it to a fixed PIN that is never rotated) → works from outside (4G / office network).
89
89
 
90
90
  > Upgrading: `dsh plugin --profile web update dsh-pocket --latest -w` (`--latest` is required across major versions — a `^0.x` range won't auto-jump to 1.x).
91
91
 
92
92
  ## ⚠️ Security (read first)
93
93
 
94
94
  - **DSH can execute code on your computer.** **LAN** QR/URL plus its own **8-digit PIN** is the key (PIN **on by default**, switchable off — then LAN scans connect directly, same-network devices only) — **never share the LAN QR, URL or PIN**.
95
+ - **Read and accept the security disclaimer before enabling public access** (the dialog shows on every enable; the server enforces it, so it can't be bypassed): public = exposing a code-executing DSH to the internet — use a strong PIN, turn it off when done, never on classified networks.
95
96
  - **Public** access is protected by an **8-digit PIN**: the link is random, the PIN rotates on every tunnel start by default, and old links die instantly — even a leaked link can't get in. **A custom PIN is never auto-rotated** (your value stays stable).
96
97
  - Phone login state is tied to the computer's dsh web process: **no re-entry while dsh web stays up; one re-entry after a restart/update**.
97
98
  - **Login rate limiting** (anti brute-force): **5** consecutive wrong PINs from the same IP lock it for **60s**; a global failure threshold briefly locks everyone (blocks distributed IP-rotation scans); a successful login resets the counter.
package/README.md CHANGED
@@ -85,13 +85,14 @@ npx @deepseek-ai/dsh web
85
85
 
86
86
  ### 公网(人在外面)
87
87
 
88
- 同一页点「**开启公网访问**」→ 等隧道建立(首次会下载 cloudflared,macOS/Linux 走清华镜像秒下)→ 手机扫「🌐 公网」二维码 → 打开链接**输入 8 位访问密码**(密码显示在设置页公网区块,默认**每次开启公网变新**,也可点「自定义」设成固定密码——自定义后不再换新)→ 人在外面(4G/公司网)也能访问。
88
+ 同一页点「**开启公网访问**」→ **每次都会先弹出安全免责声明**,勾选「我已知情」后才能开启(公司/涉密网络请先确认合规)→ 等隧道建立(首次会下载 cloudflared,macOS/Linux 走清华镜像秒下)→ 手机扫「🌐 公网」二维码 → 打开链接**输入 8 位访问密码**(密码显示在设置页公网区块,默认**每次开启公网变新**,也可点「自定义」设成固定密码——自定义后不再换新)→ 人在外面(4G/公司网)也能访问。
89
89
 
90
90
  > 更新到新版本:`dsh plugin --profile web update dsh-pocket --latest -w`(跨大版本时 `--latest` 是必须的,`^0.x` 范围不会自动升到 1.x)。
91
91
 
92
92
  ## ⚠️ 安全(必读)
93
93
 
94
94
  - **DSH 能执行你电脑上的代码**。**局域网**二维码/URL 配上独立 **8 位数字密码**才是钥匙(密码**默认开启**,可关——关闭后局域网扫码直连,仅同一网络设备可访问),**请勿把局域网二维码、URL 或密码发给别人**
95
+ - **开启公网访问前必须阅读并勾选免责声明**(每次开启都会弹框;服务端强制校验,无法绕过):公网 = 把能执行代码的 DSH 暴露到互联网,请使用强密码、用完即关、涉密网络勿用
95
96
  - **公网**有 **8 位数字密码**保护:链接随机分配、默认每次开启换新密码、旧链接立即作废——泄露了也进不来,改密码/重开即可作废;**自定义密码后不再自动换新**(你设的值即稳定密码)
96
97
  - 手机登录状态与电脑上的 dsh web 进程绑定:**电脑 dsh web 一直开着就不用重复输入;重启/更新后需重新输入一次**
97
98
  - **登录限速**(防暴力破解):同一 IP 连续输错 **5 次**锁定 **60 秒**;全局失败超阈值时短暂全锁(防换 IP 分布式扫描);输对密码后计数清零
package/client/client.js CHANGED
@@ -1358,6 +1358,10 @@ var zh2 = {
1358
1358
  "stopTunnel": "\u5173\u95ED\u516C\u7F51",
1359
1359
  "enable": "\u5F00\u542F\u516C\u7F51\u8BBF\u95EE",
1360
1360
  "opening": "\u5F00\u542F\u4E2D\u2026",
1361
+ "disclaimerTitle": "\u26A0\uFE0F \u5B89\u5168\u514D\u8D23\u58F0\u660E",
1362
+ "disclaimerBody": "\u5F00\u542F\u516C\u7F51 = \u628A\u672C\u673A DSH\uFF08\u80FD\u6267\u884C\u4EE3\u7801\uFF09\u66B4\u9732\u5230\u4E92\u8054\u7F51\u3002\u4EFB\u4F55\u4EBA\u62FF\u5230\u516C\u7F51\u94FE\u63A5\u548C\u5BC6\u7801\uFF0C\u90FD\u80FD\u8BBF\u95EE\u751A\u81F3\u64CD\u4F5C\u4F60\u7684\u7535\u8111\u3002\u8BF7\u786E\u8BA4\uFF1A\u2460 \u4F7F\u7528\u81EA\u5B9A\u4E49\u5F3A\u5BC6\u7801\u6216\u59A5\u5584\u4FDD\u7BA1\u81EA\u52A8\u5BC6\u7801\uFF1B\u2461 \u7528\u5B8C\u7ACB\u5373\u300C\u5173\u95ED\u516C\u7F51\u300D\uFF1B\u2462 \u516C\u53F8/\u6D89\u5BC6\u7F51\u7EDC\u8BF7\u5148\u786E\u8BA4\u5408\u89C4\u3002",
1363
+ "disclaimerAgree": "\u6211\u5DF2\u77E5\u60C5\uFF0C\u540C\u610F\u5F00\u542F",
1364
+ "disclaimerHint": "\u8BF7\u52FE\u9009\u300C\u6211\u5DF2\u77E5\u60C5\u300D\u540E\u518D\u5F00\u542F\u516C\u7F51",
1361
1365
  "downloading": "\u23F3 \u4E0B\u8F7D cloudflared\uFF08\u9996\u6B21\u7EA6 20-50MB\uFF0C\u901A\u5E38 1-2 \u5206\u949F\uFF1B\u4E4B\u540E\u79D2\u5F00\uFF09\xB7 \u5DF2\u7B49\u5F85 {s} \u79D2",
1362
1366
  "connecting": "\u23F3 \u8FDE\u63A5 Cloudflare \u8FB9\u7F18\uFF08\u901A\u5E38 5-30 \u79D2\uFF09\xB7 \u5DF2\u7B49\u5F85 {s} \u79D2{suffix}",
1363
1367
  "slowHint": " \u2014 \u6709\u70B9\u4E45\uFF1F\u68C0\u67E5\u662F\u5426\u5F00\u7740\u4EE3\u7406/VPN\uFF08Clash TUN \u7B49\uFF09",
@@ -1413,6 +1417,10 @@ var en2 = {
1413
1417
  "stopTunnel": "Stop",
1414
1418
  "enable": "Enable anywhere",
1415
1419
  "opening": "Enabling\u2026",
1420
+ "disclaimerTitle": "\u26A0\uFE0F Security disclaimer",
1421
+ "disclaimerBody": "Enabling public access exposes this computer\u2019s DSH (which can execute code) to the internet. Anyone with the public link and PIN can reach \u2014 and operate \u2014 your computer. Please confirm: \u2460 use a strong custom PIN or keep the auto-generated one safe; \u2461 turn public access OFF as soon as you\u2019re done; \u2462 on a corporate/classified network, confirm compliance first.",
1422
+ "disclaimerAgree": "I understand and agree",
1423
+ "disclaimerHint": 'Check "I understand" before enabling public access',
1416
1424
  "downloading": "\u23F3 Downloading cloudflared (first run ~20-50MB, usually 1-2 min; instant afterward) \xB7 {s}s elapsed",
1417
1425
  "connecting": "\u23F3 Connecting to Cloudflare edge (usually 5-30s) \xB7 {s}s elapsed{suffix}",
1418
1426
  "slowHint": " \u2014 taking long? Check for a proxy/VPN (e.g., Clash TUN)",
@@ -1553,18 +1561,29 @@ function PocketSettingsTab({ rpcCall, t }) {
1553
1561
  setUpdateInfo((u) => ({ ...u, updating: false, result: "fail", output: err.message }));
1554
1562
  }
1555
1563
  };
1556
- const startTunnel = async () => {
1564
+ const [disclaimerOpen, setDisclaimerOpen] = (0, import_react2.useState)(false);
1565
+ const [disclaimerChecked, setDisclaimerChecked] = (0, import_react2.useState)(false);
1566
+ const doStartTunnel = async () => {
1557
1567
  setBusy(true);
1558
1568
  setError(null);
1559
1569
  setTunnelState({ phase: "starting", detail: "\u6B63\u5728\u5F00\u542F\u2026", startedAt: Date.now() });
1560
1570
  try {
1561
- setStatus(await call(POCKET_ENDPOINTS.tunnelStart, {}));
1571
+ setStatus(await call(POCKET_ENDPOINTS.tunnelStart, { disclaimer: true }));
1562
1572
  } catch (err) {
1563
1573
  setError(err.message);
1564
1574
  } finally {
1565
1575
  setBusy(false);
1566
1576
  }
1567
1577
  };
1578
+ const startTunnel = () => {
1579
+ setDisclaimerChecked(false);
1580
+ setDisclaimerOpen(true);
1581
+ };
1582
+ const confirmDisclaimer = () => {
1583
+ if (!disclaimerChecked) return;
1584
+ setDisclaimerOpen(false);
1585
+ doStartTunnel();
1586
+ };
1568
1587
  const stopTunnel = async () => {
1569
1588
  try {
1570
1589
  setStatus(await call(POCKET_ENDPOINTS.tunnelStop, {}));
@@ -1760,6 +1779,34 @@ function PocketSettingsTab({ rpcCall, t }) {
1760
1779
  )
1761
1780
  ),
1762
1781
  error ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${error}`) : null,
1782
+ // 安全免责声明弹框(issue #31):每次开启公网访问前确认
1783
+ disclaimerOpen ? (0, import_react2.createElement)(
1784
+ "div",
1785
+ { style: { position: "fixed", inset: 0, zIndex: 1e4, background: "rgba(0,0,0,.5)", display: "flex", alignItems: "center", justifyContent: "center", padding: 20 } },
1786
+ (0, import_react2.createElement)(
1787
+ "div",
1788
+ { style: { background: "var(--dsw-alias-bg-layer-1,#fff)", borderRadius: 12, maxWidth: 420, width: "100%", padding: "20px 22px", boxShadow: "0 8px 32px rgba(0,0,0,.18)" } },
1789
+ (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t("disclaimerTitle")),
1790
+ (0, import_react2.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)" } }, t("disclaimerBody")),
1791
+ (0, import_react2.createElement)(
1792
+ "label",
1793
+ { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 14, fontSize: 13, cursor: "pointer" } },
1794
+ (0, import_react2.createElement)("input", { type: "checkbox", checked: disclaimerChecked, onChange: (e) => setDisclaimerChecked(e.target.checked), style: { width: 16, height: 16 } }),
1795
+ t("disclaimerAgree")
1796
+ ),
1797
+ (0, import_react2.createElement)(
1798
+ "div",
1799
+ { style: { display: "flex", gap: 8, marginTop: 16 } },
1800
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setDisclaimerOpen(false) }, t("cancel")),
1801
+ (0, import_react2.createElement)("button", {
1802
+ style: { ...styles.primary, flex: 1, opacity: disclaimerChecked ? 1 : 0.5 },
1803
+ disabled: !disclaimerChecked,
1804
+ onClick: confirmDisclaimer
1805
+ }, t("disclaimerAgree"))
1806
+ ),
1807
+ !disclaimerChecked ? (0, import_react2.createElement)("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" } }, t("disclaimerHint")) : null
1808
+ )
1809
+ ) : null,
1763
1810
  // 页面最底部:反馈入口
1764
1811
  (0, import_react2.createElement)(
1765
1812
  "div",
package/client/index.jsx CHANGED
@@ -163,18 +163,33 @@ function PocketSettingsTab({ rpcCall, t }) {
163
163
  }
164
164
  };
165
165
 
166
- const startTunnel = async () => {
166
+ // 安全免责声明(issue #31):每次开启公网都必须先弹框勾选「我已知情」。
167
+ // 服务端同样强制(tunnel.start 需 disclaimer: true),防绕过前端直接调 RPC。
168
+ const [disclaimerOpen, setDisclaimerOpen] = useState(false);
169
+ const [disclaimerChecked, setDisclaimerChecked] = useState(false);
170
+
171
+ const doStartTunnel = async () => {
167
172
  setBusy(true);
168
173
  setError(null);
169
174
  setTunnelState({ phase: 'starting', detail: '正在开启…', startedAt: Date.now() });
170
175
  try {
171
- setStatus(await call(POCKET_ENDPOINTS.tunnelStart, {}));
176
+ setStatus(await call(POCKET_ENDPOINTS.tunnelStart, { disclaimer: true }));
172
177
  } catch (err) {
173
178
  setError(err.message);
174
179
  } finally {
175
180
  setBusy(false);
176
181
  }
177
182
  };
183
+ const startTunnel = () => {
184
+ // 每次开启都弹免责确认(勾选后才能继续)
185
+ setDisclaimerChecked(false);
186
+ setDisclaimerOpen(true);
187
+ };
188
+ const confirmDisclaimer = () => {
189
+ if (!disclaimerChecked) return; // 未勾选不允许
190
+ setDisclaimerOpen(false);
191
+ doStartTunnel();
192
+ };
178
193
 
179
194
  const stopTunnel = async () => {
180
195
  try { setStatus(await call(POCKET_ENDPOINTS.tunnelStop, {})); } catch { /* 忽略 */ }
@@ -363,6 +378,27 @@ function PocketSettingsTab({ rpcCall, t }) {
363
378
 
364
379
  error ? h('div', { style: { color: 'var(--dsw-alias-state-error-primary,#dc2626)', fontSize: 12, marginTop: 8 } }, `❌ ${error}`) : null,
365
380
 
381
+ // 安全免责声明弹框(issue #31):每次开启公网访问前确认
382
+ disclaimerOpen ? h('div', { style: { position: 'fixed', inset: 0, zIndex: 10000, background: 'rgba(0,0,0,.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 } },
383
+ h('div', { style: { background: 'var(--dsw-alias-bg-layer-1,#fff)', borderRadius: 12, maxWidth: 420, width: '100%', padding: '20px 22px', boxShadow: '0 8px 32px rgba(0,0,0,.18)' } },
384
+ h('div', { style: { fontWeight: 600, fontSize: 15, color: 'var(--dsw-alias-state-warn-primary,#b45309)', marginBottom: 10 } }, t('disclaimerTitle')),
385
+ h('div', { style: { fontSize: 13, lineHeight: 1.7, color: 'var(--dsw-alias-label-primary,inherit)' } }, t('disclaimerBody')),
386
+ h('label', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 14, fontSize: 13, cursor: 'pointer' } },
387
+ h('input', { type: 'checkbox', checked: disclaimerChecked, onChange: (e) => setDisclaimerChecked(e.target.checked), style: { width: 16, height: 16 } }),
388
+ t('disclaimerAgree'),
389
+ ),
390
+ h('div', { style: { display: 'flex', gap: 8, marginTop: 16 } },
391
+ h('button', { style: { ...styles.btn, flex: 1 }, onClick: () => setDisclaimerOpen(false) }, t('cancel')),
392
+ h('button', {
393
+ style: { ...styles.primary, flex: 1, opacity: disclaimerChecked ? 1 : .5 },
394
+ disabled: !disclaimerChecked,
395
+ onClick: confirmDisclaimer,
396
+ }, t('disclaimerAgree')),
397
+ ),
398
+ !disclaimerChecked ? h('div', { style: { marginTop: 8, fontSize: 12, color: 'var(--dsw-alias-state-error-primary,#dc2626)' } }, t('disclaimerHint')) : null,
399
+ ),
400
+ ) : null,
401
+
366
402
  // 页面最底部:反馈入口
367
403
  h('div', { style: { ...styles.block, textAlign: 'center' } },
368
404
  h('a', { href: 'https://github.com/shaobeichen/dsh-pocket/issues', target: '_blank', rel: 'noreferrer', style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', textDecoration: 'none' } },
@@ -51,6 +51,10 @@ export const zh = {
51
51
  'stopTunnel': '关闭公网',
52
52
  'enable': '开启公网访问',
53
53
  'opening': '开启中…',
54
+ 'disclaimerTitle': '⚠️ 安全免责声明',
55
+ 'disclaimerBody': '开启公网 = 把本机 DSH(能执行代码)暴露到互联网。任何人拿到公网链接和密码,都能访问甚至操作你的电脑。请确认:① 使用自定义强密码或妥善保管自动密码;② 用完立即「关闭公网」;③ 公司/涉密网络请先确认合规。',
56
+ 'disclaimerAgree': '我已知情,同意开启',
57
+ 'disclaimerHint': '请勾选「我已知情」后再开启公网',
54
58
  'downloading': '⏳ 下载 cloudflared(首次约 20-50MB,通常 1-2 分钟;之后秒开)· 已等待 {s} 秒',
55
59
  'connecting': '⏳ 连接 Cloudflare 边缘(通常 5-30 秒)· 已等待 {s} 秒{suffix}',
56
60
  'slowHint': ' — 有点久?检查是否开着代理/VPN(Clash TUN 等)',
@@ -108,6 +112,10 @@ export const en = {
108
112
  'stopTunnel': 'Stop',
109
113
  'enable': 'Enable anywhere',
110
114
  'opening': 'Enabling…',
115
+ 'disclaimerTitle': '⚠️ Security disclaimer',
116
+ 'disclaimerBody': 'Enabling public access exposes this computer\u2019s DSH (which can execute code) to the internet. Anyone with the public link and PIN can reach — and operate — your computer. Please confirm: ① use a strong custom PIN or keep the auto-generated one safe; ② turn public access OFF as soon as you\u2019re done; ③ on a corporate/classified network, confirm compliance first.',
117
+ 'disclaimerAgree': 'I understand and agree',
118
+ 'disclaimerHint': 'Check "I understand" before enabling public access',
111
119
  'downloading': '⏳ Downloading cloudflared (first run ~20-50MB, usually 1-2 min; instant afterward) · {s}s elapsed',
112
120
  'connecting': '⏳ Connecting to Cloudflare edge (usually 5-30s) · {s}s elapsed{suffix}',
113
121
  'slowHint': ' — taking long? Check for a proxy/VPN (e.g., Clash TUN)',
package/lib/tunnel.mjs CHANGED
@@ -11,7 +11,13 @@ import { pipeline } from 'node:stream/promises';
11
11
  import { Readable } from 'node:stream';
12
12
  import { createWriteStream } from 'node:fs';
13
13
 
14
- const QUICK_TUNNEL_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
14
+ // 快速隧道 URL:https://<随机子域>.trycloudflare.com
15
+ // (?!api\.) 负向前瞻排除保留子域 api(issue #32):某些 cloudflared 版本/网络环境下
16
+ // 进程输出会先出现 https://api.trycloudflare.com(Cloudflare API 注册地址),原正则
17
+ // [a-z0-9-]+ 会把它误当隧道 URL → 设置页/二维码给出 api 地址 → 扫码打开返回
18
+ // {"code":10005,"message":"Method Not Allowed"}。api.trycloudflare.com 访问 GET 实测
19
+ // 正是该错误体,与 issue 完全一致。
20
+ export const QUICK_TUNNEL_URL_RE = /https:\/\/(?!api\.)[a-z0-9-]+\.trycloudflare\.com/i;
15
21
 
16
22
  function platformBinary() {
17
23
  const archMap = { x64: 'amd64', arm64: 'arm64' };
package/lib/web-rpc.js CHANGED
@@ -77,6 +77,11 @@ export function installPocketRpc(ctx, { service, log = console, desktop = false,
77
77
  }
78
78
  }
79
79
  if (endpoint === POCKET_ENDPOINTS.tunnelStart) {
80
+ // 安全免责声明(issue #31):每次开启公网都必须先确认(前端弹框勾选)。
81
+ // 服务端强制校验,防止绕过前端直接调 RPC。
82
+ if (payload?.disclaimer !== true) {
83
+ return fail('bad-request', '开启公网前请先阅读并勾选安全免责声明 | please accept the security disclaimer before enabling public access');
84
+ }
80
85
  await service.startTunnel();
81
86
  return await statusPayload();
82
87
  }
package/package.json CHANGED
@@ -76,5 +76,5 @@
76
76
  "access": "public",
77
77
  "registry": "https://registry.npmjs.org/"
78
78
  },
79
- "version": "1.11.3"
79
+ "version": "1.12.1"
80
80
  }