dsh-pocket 1.11.2 → 1.12.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.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
@@ -458,13 +458,21 @@ var MOBILE_CSS = `
458
458
  the viewport. A mere -100% leaves a sliver on screen; -105% (as used
459
459
  before) left 14px of the drawer plus a long 32px-blur shadow gradient
460
460
  visible along the left edge of the main UI. No box-shadow at all: the
461
- dimmed backdrop already separates drawer from content. */
461
+ dimmed backdrop already separates drawer from content.
462
+ Z-index note: the backdrop renders inside the shell's overlay layer
463
+ ([data-shell-overlay]), which forms its own stacking context. Third-party
464
+ plugins can force that layer up with !important (dsh-update-checker sets
465
+ it to 500), and when the layer outranks the drawer, the backdrop paints
466
+ ABOVE the drawer and swallows every tap \u2014 the drawer opens but no row
467
+ can be pressed (every tap just closes it). The drawer must therefore
468
+ outrank any such raise: 600 clears the known 500 while staying under the
469
+ fixed-position banners/toasts (z 9999) that float at the viewport level. */
462
470
  [data-mobile-nav="frame"] > :first-child {
463
471
  position: absolute !important;
464
472
  inset: 0 auto 0 0 !important;
465
473
  width: max-content !important;
466
474
  max-width: 92vw !important;
467
- z-index: 40 !important;
475
+ z-index: 600 !important;
468
476
  transform: translateX(-110%);
469
477
  transition: transform .28s var(--ds-ease-in-out, ease-in-out);
470
478
  background: var(--dsw-alias-bg-base, #ffffff);
@@ -1350,6 +1358,10 @@ var zh2 = {
1350
1358
  "stopTunnel": "\u5173\u95ED\u516C\u7F51",
1351
1359
  "enable": "\u5F00\u542F\u516C\u7F51\u8BBF\u95EE",
1352
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",
1353
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",
1354
1366
  "connecting": "\u23F3 \u8FDE\u63A5 Cloudflare \u8FB9\u7F18\uFF08\u901A\u5E38 5-30 \u79D2\uFF09\xB7 \u5DF2\u7B49\u5F85 {s} \u79D2{suffix}",
1355
1367
  "slowHint": " \u2014 \u6709\u70B9\u4E45\uFF1F\u68C0\u67E5\u662F\u5426\u5F00\u7740\u4EE3\u7406/VPN\uFF08Clash TUN \u7B49\uFF09",
@@ -1405,6 +1417,10 @@ var en2 = {
1405
1417
  "stopTunnel": "Stop",
1406
1418
  "enable": "Enable anywhere",
1407
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',
1408
1424
  "downloading": "\u23F3 Downloading cloudflared (first run ~20-50MB, usually 1-2 min; instant afterward) \xB7 {s}s elapsed",
1409
1425
  "connecting": "\u23F3 Connecting to Cloudflare edge (usually 5-30s) \xB7 {s}s elapsed{suffix}",
1410
1426
  "slowHint": " \u2014 taking long? Check for a proxy/VPN (e.g., Clash TUN)",
@@ -1545,18 +1561,29 @@ function PocketSettingsTab({ rpcCall, t }) {
1545
1561
  setUpdateInfo((u) => ({ ...u, updating: false, result: "fail", output: err.message }));
1546
1562
  }
1547
1563
  };
1548
- 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 () => {
1549
1567
  setBusy(true);
1550
1568
  setError(null);
1551
1569
  setTunnelState({ phase: "starting", detail: "\u6B63\u5728\u5F00\u542F\u2026", startedAt: Date.now() });
1552
1570
  try {
1553
- setStatus(await call(POCKET_ENDPOINTS.tunnelStart, {}));
1571
+ setStatus(await call(POCKET_ENDPOINTS.tunnelStart, { disclaimer: true }));
1554
1572
  } catch (err) {
1555
1573
  setError(err.message);
1556
1574
  } finally {
1557
1575
  setBusy(false);
1558
1576
  }
1559
1577
  };
1578
+ const startTunnel = () => {
1579
+ setDisclaimerChecked(false);
1580
+ setDisclaimerOpen(true);
1581
+ };
1582
+ const confirmDisclaimer = () => {
1583
+ if (!disclaimerChecked) return;
1584
+ setDisclaimerOpen(false);
1585
+ doStartTunnel();
1586
+ };
1560
1587
  const stopTunnel = async () => {
1561
1588
  try {
1562
1589
  setStatus(await call(POCKET_ENDPOINTS.tunnelStop, {}));
@@ -1752,6 +1779,34 @@ function PocketSettingsTab({ rpcCall, t }) {
1752
1779
  )
1753
1780
  ),
1754
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,
1755
1810
  // 页面最底部:反馈入口
1756
1811
  (0, import_react2.createElement)(
1757
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' } },
@@ -197,13 +197,21 @@ export const MOBILE_CSS = `
197
197
  the viewport. A mere -100% leaves a sliver on screen; -105% (as used
198
198
  before) left 14px of the drawer plus a long 32px-blur shadow gradient
199
199
  visible along the left edge of the main UI. No box-shadow at all: the
200
- dimmed backdrop already separates drawer from content. */
200
+ dimmed backdrop already separates drawer from content.
201
+ Z-index note: the backdrop renders inside the shell's overlay layer
202
+ ([data-shell-overlay]), which forms its own stacking context. Third-party
203
+ plugins can force that layer up with !important (dsh-update-checker sets
204
+ it to 500), and when the layer outranks the drawer, the backdrop paints
205
+ ABOVE the drawer and swallows every tap — the drawer opens but no row
206
+ can be pressed (every tap just closes it). The drawer must therefore
207
+ outrank any such raise: 600 clears the known 500 while staying under the
208
+ fixed-position banners/toasts (z 9999) that float at the viewport level. */
201
209
  [data-mobile-nav="frame"] > :first-child {
202
210
  position: absolute !important;
203
211
  inset: 0 auto 0 0 !important;
204
212
  width: max-content !important;
205
213
  max-width: 92vw !important;
206
- z-index: 40 !important;
214
+ z-index: 600 !important;
207
215
  transform: translateX(-110%);
208
216
  transition: transform .28s var(--ds-ease-in-out, ease-in-out);
209
217
  background: var(--dsw-alias-bg-base, #ffffff);
@@ -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/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.2"
79
+ "version": "1.12.0"
80
80
  }