dsh-pocket 2.6.2 → 2.6.3

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
@@ -9,7 +9,7 @@
9
9
  <p align="center">
10
10
  <a href="https://www.npmjs.com/package/dsh-pocket"><img alt="npm" src="https://img.shields.io/npm/v/dsh-pocket?color=4d6bfe&label=npm"></a>
11
11
  <a href="https://www.npmjs.com/package/dsh-pocket"><img alt="downloads" src="https://img.shields.io/npm/dm/dsh-pocket?color=4d6bfe"></a>
12
- <a href="https://github.com/shaobeichen/dsh-pocket/actions"><img alt="CI" src="https://github.com/shaobeichen/dsh-pocket/actions/workflows/npm-publish.yml/badge.svg"></a>
12
+ <a href="https://github.com/shaobeichen/dsh-pocket/actions"><img alt="CI" src="https://github.com/shaobeichen/dsh-pocket/actions/workflows/release.yml/badge.svg"></a>
13
13
  <a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-GPL--2.0-red.svg"></a>
14
14
  <a href="https://github.com/shaobeichen/dsh-pocket/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/shaobeichen/dsh-pocket"></a>
15
15
  <a href="https://awesome-dsh-plugin.com/zh/"><img alt="Awesome DSH Plugin" src="https://awesome-dsh-plugin.com/badge.svg"></a>
package/client/api.js CHANGED
@@ -15,9 +15,6 @@ export const POCKET_ENDPOINTS = Object.freeze({
15
15
  lanSetEnabled: 'lan.setEnabled',
16
16
  pinSetCustom: 'pin.setCustom',
17
17
  pocketReset: 'pocket.reset',
18
- tempPinCreate: 'tempPin.create',
19
- tempPinList: 'tempPin.list',
20
- tempPinRevoke: 'tempPin.revoke',
21
18
  });
22
19
 
23
20
  /** 语义化版本比较:a > b 返回正数,相等 0,a < b 负数(数字段 + 预发布后缀)。 */
package/client/client.js CHANGED
@@ -54,10 +54,7 @@ var POCKET_ENDPOINTS = Object.freeze({
54
54
  lanSetOverride: "lan.setOverride",
55
55
  lanSetEnabled: "lan.setEnabled",
56
56
  pinSetCustom: "pin.setCustom",
57
- pocketReset: "pocket.reset",
58
- tempPinCreate: "tempPin.create",
59
- tempPinList: "tempPin.list",
60
- tempPinRevoke: "tempPin.revoke"
57
+ pocketReset: "pocket.reset"
61
58
  });
62
59
  function compareVersions(a, b) {
63
60
  const pa = String(a).replace(/^[vV]/, "").split(".");
@@ -1659,26 +1656,7 @@ var zh2 = {
1659
1656
  "slowHint": " \u2014 \u6709\u70B9\u4E45\uFF1F\u68C0\u67E5\u662F\u5426\u5F00\u7740\u4EE3\u7406/VPN\uFF08Clash TUN \u7B49\uFF09",
1660
1657
  "error": "\u274C \u5F00\u542F\u5931\u8D25\uFF1A{detail}\uFF08\u53EF\u91CD\u8BD5\uFF1B\u82E5\u662F\u4EE3\u7406/VPN \u95EE\u9898\u89C1 README \u6392\u969C\uFF09",
1661
1658
  "unknownError": "\u672A\u77E5\u9519\u8BEF",
1662
- "feedback": "\u6709\u95EE\u9898\uFF1F\u6B22\u8FCE\u5230 GitHub Issues \u53CD\u9988 \u{1F64F}",
1663
- // 临时 PIN(issue #69)
1664
- "tempPinTitle": "\u4E34\u65F6\u8BBF\u95EE PIN\uFF08\u5E26\u8D85\u65F6\uFF09",
1665
- "tempPinSubtitle": "\u7ED9\u8BBF\u5BA2\u5F00\u4E00\u4E2A\u6709\u5BFF\u547D\u7684 8 \u4F4D PIN\uFF0C\u8FC7\u671F\u81EA\u52A8\u4F5C\u5E9F\uFF1B\u4E0E\u4E3B PIN \u5171\u7528\u901F\u7387\u9650\u5236\u3002",
1666
- "tempPinKindPublic": "\u516C\u7F51",
1667
- "tempPinKindLan": "\u5C40\u57DF\u7F51",
1668
- "tempPinDuration": "\u6709\u6548\u65F6\u957F",
1669
- "tempPinDuration1h": "1 \u5C0F\u65F6",
1670
- "tempPinDuration24h": "24 \u5C0F\u65F6",
1671
- "tempPinDuration7d": "7 \u5929",
1672
- "tempPinLabel": "\u5907\u6CE8\uFF08\u53EF\u9009\uFF0C\u7ED9\u8C01\u7528\uFF09",
1673
- "tempPinLabelPh": "\u670B\u53CB\u5C0F\u738B",
1674
- "tempPinCreate": "\u751F\u6210",
1675
- "tempPinCreated": "\u5DF2\u751F\u6210\uFF08\u4EC5\u663E\u793A\u4E00\u6B21\uFF09",
1676
- "tempPinCopied": "\u5DF2\u590D\u5236",
1677
- "tempPinCopy": "\u590D\u5236",
1678
- "tempPinEmpty": "\u6682\u65E0\u4E34\u65F6 PIN",
1679
- "tempPinExpiresIn": "\u8FC7\u671F",
1680
- "tempPinRevoke": "\u64A4\u9500",
1681
- "tempPinRevoked": "\u5DF2\u64A4\u9500"
1659
+ "feedback": "\u6709\u95EE\u9898\uFF1F\u6B22\u8FCE\u5230 GitHub Issues \u53CD\u9988 \u{1F64F}"
1682
1660
  };
1683
1661
  var en2 = {
1684
1662
  "section": "Phone access",
@@ -1773,26 +1751,7 @@ var en2 = {
1773
1751
  "slowHint": " \u2014 taking long? Check for a proxy/VPN (e.g., Clash TUN)",
1774
1752
  "error": "\u274C Failed to enable: {detail} (you can retry; for proxy/VPN issues see the README)",
1775
1753
  "unknownError": "unknown error",
1776
- "feedback": "\u{1F64F} Questions? Open an issue on GitHub",
1777
- // Temp PIN (issue #69)
1778
- "tempPinTitle": "Temporary access PINs (auto-expire)",
1779
- "tempPinSubtitle": "Issue a time-limited 8-char PIN to a guest; expires automatically. Shares the main PIN rate limit.",
1780
- "tempPinKindPublic": "Public",
1781
- "tempPinKindLan": "LAN",
1782
- "tempPinDuration": "Valid for",
1783
- "tempPinDuration1h": "1 hour",
1784
- "tempPinDuration24h": "24 hours",
1785
- "tempPinDuration7d": "7 days",
1786
- "tempPinLabel": "Note (optional, who is this for)",
1787
- "tempPinLabelPh": "Friend Alice",
1788
- "tempPinCreate": "Create",
1789
- "tempPinCreated": "Created (shown once)",
1790
- "tempPinCopied": "Copied",
1791
- "tempPinCopy": "Copy",
1792
- "tempPinEmpty": "No temporary PINs yet",
1793
- "tempPinExpiresIn": "expires",
1794
- "tempPinRevoke": "Revoke",
1795
- "tempPinRevoked": "Revoked"
1754
+ "feedback": "\u{1F64F} Questions? Open an issue on GitHub"
1796
1755
  };
1797
1756
 
1798
1757
  // client/index.jsx
@@ -1927,73 +1886,6 @@ function PocketSettingsTab({ rpcCall, t }) {
1927
1886
  setUpdateInfo((u) => ({ ...u, updating: false, result: "fail", output: err.message }));
1928
1887
  }
1929
1888
  };
1930
- const [tempPinForm, setTempPinForm] = (0, import_react3.useState)({ kind: "lan", expiresInSec: 86400, label: "" });
1931
- const [tempPinsList, setTempPinsList] = (0, import_react3.useState)([]);
1932
- const [tempPinLast, setTempPinLast] = (0, import_react3.useState)(null);
1933
- const [tempPinBusy, setTempPinBusy] = (0, import_react3.useState)(false);
1934
- const [tempPinError, setTempPinError] = (0, import_react3.useState)(null);
1935
- const loadTempPins = async () => {
1936
- try {
1937
- const r = await call(POCKET_ENDPOINTS.tempPinList, {});
1938
- setTempPinsList(Array.isArray(r?.tempPins) ? r.tempPins : []);
1939
- } catch {
1940
- }
1941
- };
1942
- (0, import_react3.useEffect)(() => {
1943
- loadTempPins();
1944
- }, []);
1945
- (0, import_react3.useEffect)(() => {
1946
- if (tempPinsList.length === 0 && !tempPinLast) return;
1947
- const t2 = setInterval(() => {
1948
- loadTempPins();
1949
- }, 3e4);
1950
- return () => clearInterval(t2);
1951
- }, [tempPinsList.length, tempPinLast]);
1952
- const createTempPinNow = async () => {
1953
- setTempPinBusy(true);
1954
- setTempPinError(null);
1955
- try {
1956
- const r = await call(POCKET_ENDPOINTS.tempPinCreate, { kind: tempPinForm.kind, expiresInSec: tempPinForm.expiresInSec, label: tempPinForm.label });
1957
- setTempPinLast(r);
1958
- setTempPinForm((f) => ({ ...f, label: "" }));
1959
- await loadTempPins();
1960
- } catch (err) {
1961
- setTempPinError(err.message);
1962
- } finally {
1963
- setTempPinBusy(false);
1964
- }
1965
- };
1966
- const revokeTempPinNow = async (p) => {
1967
- try {
1968
- await call(POCKET_ENDPOINTS.tempPinRevoke, { value: p.value, kind: p.kind });
1969
- showToast(t("tempPinRevoked"));
1970
- if (tempPinLast?.value === p.value) setTempPinLast(null);
1971
- await loadTempPins();
1972
- } catch (err) {
1973
- setError(err.message);
1974
- }
1975
- };
1976
- const copyTempPin = async (value) => {
1977
- try {
1978
- if (navigator.clipboard?.writeText) {
1979
- await navigator.clipboard.writeText(value);
1980
- showToast(t("tempPinCopied"));
1981
- }
1982
- } catch {
1983
- }
1984
- };
1985
- const formatRemaining = (expiresAt) => {
1986
- const ms = expiresAt - Date.now();
1987
- if (ms <= 0) return "0s";
1988
- const s = Math.floor(ms / 1e3);
1989
- if (s < 60) return `${s}s`;
1990
- const m = Math.floor(s / 60);
1991
- if (m < 60) return `${m}m`;
1992
- const h3 = Math.floor(m / 60);
1993
- if (h3 < 24) return `${h3}h`;
1994
- const d = Math.floor(h3 / 24);
1995
- return `${d}d`;
1996
- };
1997
1889
  const [disclaimerOpen, setDisclaimerOpen] = (0, import_react3.useState)(false);
1998
1890
  const [disclaimerChecked, setDisclaimerChecked] = (0, import_react3.useState)(false);
1999
1891
  const doStartTunnel = async () => {
@@ -2423,77 +2315,6 @@ function PocketSettingsTab({ rpcCall, t }) {
2423
2315
  ) : null
2424
2316
  ),
2425
2317
  error ? (0, import_react3.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${errText(error)}`) : null,
2426
- // 临时 PIN(issue #69):带过期的访问密码,给访客用
2427
- (0, import_react3.createElement)(
2428
- "div",
2429
- { style: styles.block },
2430
- (0, import_react3.createElement)("div", { style: { fontWeight: 600, fontSize: 13, marginBottom: 4 } }, t("tempPinTitle")),
2431
- (0, import_react3.createElement)("div", { style: { ...styles.muted, marginBottom: 10 } }, t("tempPinSubtitle")),
2432
- // 生成表单:分类 + 时长 + 备注 + 生成
2433
- (0, import_react3.createElement)(
2434
- "div",
2435
- { style: { display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" } },
2436
- (0, import_react3.createElement)(
2437
- "select",
2438
- {
2439
- value: tempPinForm.kind,
2440
- onChange: (e) => setTempPinForm((f) => ({ ...f, kind: e.target.value })),
2441
- style: { font: "inherit", height: 30, padding: "0 8px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", background: "var(--dsw-alias-bg-layer-1,#fff)", color: "var(--dsw-alias-label-primary,inherit)" }
2442
- },
2443
- (0, import_react3.createElement)("option", { value: "public" }, t("tempPinKindPublic")),
2444
- (0, import_react3.createElement)("option", { value: "lan" }, t("tempPinKindLan"))
2445
- ),
2446
- (0, import_react3.createElement)(
2447
- "select",
2448
- {
2449
- value: String(tempPinForm.expiresInSec),
2450
- onChange: (e) => setTempPinForm((f) => ({ ...f, expiresInSec: Number(e.target.value) })),
2451
- style: { font: "inherit", height: 30, padding: "0 8px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", background: "var(--dsw-alias-bg-layer-1,#fff)", color: "var(--dsw-alias-label-primary,inherit)" }
2452
- },
2453
- (0, import_react3.createElement)("option", { value: "3600" }, t("tempPinDuration1h")),
2454
- (0, import_react3.createElement)("option", { value: "86400" }, t("tempPinDuration24h")),
2455
- (0, import_react3.createElement)("option", { value: "604800" }, t("tempPinDuration7d"))
2456
- ),
2457
- (0, import_react3.createElement)("input", {
2458
- style: { flex: 1, minWidth: 120, height: 30, padding: "0 10px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", fontSize: 13, outline: "none" },
2459
- placeholder: t("tempPinLabelPh"),
2460
- value: tempPinForm.label,
2461
- onChange: (e) => setTempPinForm((f) => ({ ...f, label: e.target.value })),
2462
- onKeyDown: (e) => {
2463
- if (e.key === "Enter") createTempPinNow();
2464
- }
2465
- }),
2466
- (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 30 }, onClick: createTempPinNow, disabled: tempPinBusy }, t("tempPinCreate"))
2467
- ),
2468
- // 错误展示
2469
- tempPinError ? (0, import_react3.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 6 } }, errText(tempPinError)) : null,
2470
- // 刚生成的那一个(高亮,下一秒会消失提示用户立刻复制)
2471
- tempPinLast ? (0, import_react3.createElement)(
2472
- "div",
2473
- { style: { marginTop: 10, padding: "8px 10px", borderRadius: 8, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, flexWrap: "wrap" } },
2474
- (0, import_react3.createElement)(
2475
- "div",
2476
- null,
2477
- (0, import_react3.createElement)("div", { style: { fontSize: 11, color: "var(--dsw-alias-state-success-primary,#16a34a)", fontWeight: 600, marginBottom: 2 } }, `${t("tempPinCreated")} \xB7 ${tempPinLast.kind === "public" ? t("tempPinKindPublic") : t("tempPinKindLan")} \xB7 ${formatRemaining(tempPinLast.expiresAt)}`),
2478
- (0, import_react3.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 16, letterSpacing: 2, fontWeight: 600 } }, tempPinLast.value)
2479
- ),
2480
- (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12 }, onClick: () => copyTempPin(tempPinLast.value) }, t("tempPinCopy"))
2481
- ) : null,
2482
- // 列表
2483
- (0, import_react3.createElement)(
2484
- "div",
2485
- { style: { marginTop: 12, borderTop: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", paddingTop: 8 } },
2486
- tempPinsList.length === 0 ? (0, import_react3.createElement)("div", { style: { ...styles.muted, padding: "6px 0" } }, t("tempPinEmpty")) : tempPinsList.map((p) => (0, import_react3.createElement)(
2487
- "div",
2488
- { key: p.value, style: { display: "flex", alignItems: "center", gap: 8, padding: "6px 0", borderBottom: "1px solid var(--dsw-alias-border-l2,#e5e7eb)" } },
2489
- (0, import_react3.createElement)("span", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary,#8b93a1)", minWidth: 50 } }, p.kind === "public" ? t("tempPinKindPublic") : t("tempPinKindLan")),
2490
- (0, import_react3.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 13, letterSpacing: 1, flexShrink: 0 } }, p.value),
2491
- (0, import_react3.createElement)("span", { style: { ...styles.muted, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, p.label || "\u2014"),
2492
- (0, import_react3.createElement)("span", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary,#8b93a1)" } }, `${t("tempPinExpiresIn")} ${formatRemaining(p.expiresAt)}`),
2493
- (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 24, padding: "0 10px", fontSize: 11 }, onClick: () => revokeTempPinNow(p) }, t("tempPinRevoke"))
2494
- ))
2495
- )
2496
- ),
2497
2318
  // 恢复出厂设置:设置出问题时的临时兜底(最底部,避免误触)
2498
2319
  (0, import_react3.createElement)(
2499
2320
  "div",
package/client/index.jsx CHANGED
@@ -165,75 +165,6 @@ function PocketSettingsTab({ rpcCall, t }) {
165
165
 
166
166
  // 安全免责声明(issue #31):每次开启公网都必须先弹框勾选「我已知情」。
167
167
  // 服务端同样强制(tunnel.start 需 disclaimer: true),防绕过前端直接调 RPC。
168
- // 临时 PIN(issue #69):表单、刚生成项、列表
169
- const [tempPinForm, setTempPinForm] = useState({ kind: 'lan', expiresInSec: 86400, label: '' });
170
- const [tempPinsList, setTempPinsList] = useState([]);
171
- const [tempPinLast, setTempPinLast] = useState(null); // { value, kind, expiresAt, label }
172
- const [tempPinBusy, setTempPinBusy] = useState(false);
173
- const [tempPinError, setTempPinError] = useState(null);
174
-
175
- const loadTempPins = async () => {
176
- try {
177
- const r = await call(POCKET_ENDPOINTS.tempPinList, {});
178
- setTempPinsList(Array.isArray(r?.tempPins) ? r.tempPins : []);
179
- } catch { /* 忽略 */ }
180
- };
181
- useEffect(() => { loadTempPins(); }, []);
182
-
183
- // 每 30 秒刷新一次(让剩余时间实时倒计时显示)
184
- useEffect(() => {
185
- if (tempPinsList.length === 0 && !tempPinLast) return;
186
- const t = setInterval(() => {
187
- // 触发 setNow 已有的 tick 重新渲染即可;这里只重新拉一次(处理过期自动消失)
188
- loadTempPins();
189
- }, 30_000);
190
- return () => clearInterval(t);
191
- }, [tempPinsList.length, tempPinLast]);
192
-
193
- const createTempPinNow = async () => {
194
- setTempPinBusy(true);
195
- setTempPinError(null);
196
- try {
197
- const r = await call(POCKET_ENDPOINTS.tempPinCreate, { kind: tempPinForm.kind, expiresInSec: tempPinForm.expiresInSec, label: tempPinForm.label });
198
- setTempPinLast(r);
199
- setTempPinForm((f) => ({ ...f, label: '' }));
200
- await loadTempPins();
201
- } catch (err) {
202
- setTempPinError(err.message);
203
- } finally {
204
- setTempPinBusy(false);
205
- }
206
- };
207
- const revokeTempPinNow = async (p) => {
208
- try {
209
- await call(POCKET_ENDPOINTS.tempPinRevoke, { value: p.value, kind: p.kind });
210
- showToast(t('tempPinRevoked'));
211
- if (tempPinLast?.value === p.value) setTempPinLast(null);
212
- await loadTempPins();
213
- } catch (err) {
214
- setError(err.message);
215
- }
216
- };
217
- const copyTempPin = async (value) => {
218
- try {
219
- if (navigator.clipboard?.writeText) {
220
- await navigator.clipboard.writeText(value);
221
- showToast(t('tempPinCopied'));
222
- }
223
- } catch { /* 忽略 */ }
224
- };
225
- const formatRemaining = (expiresAt) => {
226
- const ms = expiresAt - Date.now();
227
- if (ms <= 0) return '0s';
228
- const s = Math.floor(ms / 1000);
229
- if (s < 60) return `${s}s`;
230
- const m = Math.floor(s / 60);
231
- if (m < 60) return `${m}m`;
232
- const h = Math.floor(m / 60);
233
- if (h < 24) return `${h}h`;
234
- const d = Math.floor(h / 24);
235
- return `${d}d`;
236
- };
237
168
 
238
169
  const [disclaimerOpen, setDisclaimerOpen] = useState(false);
239
170
  const [disclaimerChecked, setDisclaimerChecked] = useState(false);
@@ -624,62 +555,6 @@ function PocketSettingsTab({ rpcCall, t }) {
624
555
 
625
556
  error ? h('div', { style: { color: 'var(--dsw-alias-state-error-primary,#dc2626)', fontSize: 12, marginTop: 8 } }, `❌ ${errText(error)}`) : null,
626
557
 
627
- // 临时 PIN(issue #69):带过期的访问密码,给访客用
628
- h('div', { style: styles.block },
629
- h('div', { style: { fontWeight: 600, fontSize: 13, marginBottom: 4 } }, t('tempPinTitle')),
630
- h('div', { style: { ...styles.muted, marginBottom: 10 } }, t('tempPinSubtitle')),
631
- // 生成表单:分类 + 时长 + 备注 + 生成
632
- h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' } },
633
- h('select', {
634
- value: tempPinForm.kind,
635
- onChange: (e) => setTempPinForm((f) => ({ ...f, kind: e.target.value })),
636
- style: { font: 'inherit', height: 30, padding: '0 8px', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2,#d1d5db)', background: 'var(--dsw-alias-bg-layer-1,#fff)', color: 'var(--dsw-alias-label-primary,inherit)' },
637
- },
638
- h('option', { value: 'public' }, t('tempPinKindPublic')),
639
- h('option', { value: 'lan' }, t('tempPinKindLan')),
640
- ),
641
- h('select', {
642
- value: String(tempPinForm.expiresInSec),
643
- onChange: (e) => setTempPinForm((f) => ({ ...f, expiresInSec: Number(e.target.value) })),
644
- style: { font: 'inherit', height: 30, padding: '0 8px', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2,#d1d5db)', background: 'var(--dsw-alias-bg-layer-1,#fff)', color: 'var(--dsw-alias-label-primary,inherit)' },
645
- },
646
- h('option', { value: '3600' }, t('tempPinDuration1h')),
647
- h('option', { value: '86400' }, t('tempPinDuration24h')),
648
- h('option', { value: '604800' }, t('tempPinDuration7d')),
649
- ),
650
- h('input', {
651
- style: { flex: 1, minWidth: 120, height: 30, padding: '0 10px', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2,#d1d5db)', fontSize: 13, outline: 'none' },
652
- placeholder: t('tempPinLabelPh'),
653
- value: tempPinForm.label,
654
- onChange: (e) => setTempPinForm((f) => ({ ...f, label: e.target.value })),
655
- onKeyDown: (e) => { if (e.key === 'Enter') createTempPinNow(); },
656
- }),
657
- h('button', { style: { ...styles.btn, height: 30 }, onClick: createTempPinNow, disabled: tempPinBusy }, t('tempPinCreate')),
658
- ),
659
- // 错误展示
660
- tempPinError ? h('div', { style: { color: 'var(--dsw-alias-state-error-primary,#dc2626)', fontSize: 12, marginTop: 6 } }, errText(tempPinError)) : null,
661
- // 刚生成的那一个(高亮,下一秒会消失提示用户立刻复制)
662
- tempPinLast ? h('div', { style: { marginTop: 10, padding: '8px 10px', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' } },
663
- h('div', null,
664
- h('div', { style: { fontSize: 11, color: 'var(--dsw-alias-state-success-primary,#16a34a)', fontWeight: 600, marginBottom: 2 } }, `${t('tempPinCreated')} · ${tempPinLast.kind === 'public' ? t('tempPinKindPublic') : t('tempPinKindLan')} · ${formatRemaining(tempPinLast.expiresAt)}`),
665
- h('span', { style: { fontFamily: 'ui-monospace,Menlo,monospace', fontSize: 16, letterSpacing: 2, fontWeight: 600 } }, tempPinLast.value),
666
- ),
667
- h('button', { style: { ...styles.btn, height: 28, padding: '0 12px', fontSize: 12 }, onClick: () => copyTempPin(tempPinLast.value) }, t('tempPinCopy')),
668
- ) : null,
669
- // 列表
670
- h('div', { style: { marginTop: 12, borderTop: '1px solid var(--dsw-alias-border-l2,#e5e7eb)', paddingTop: 8 } },
671
- tempPinsList.length === 0
672
- ? h('div', { style: { ...styles.muted, padding: '6px 0' } }, t('tempPinEmpty'))
673
- : tempPinsList.map((p) => h('div', { key: p.value, style: { display: 'flex', alignItems: 'center', gap: 8, padding: '6px 0', borderBottom: '1px solid var(--dsw-alias-border-l2,#e5e7eb)' } },
674
- h('span', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary,#8b93a1)', minWidth: 50 } }, p.kind === 'public' ? t('tempPinKindPublic') : t('tempPinKindLan')),
675
- h('span', { style: { fontFamily: 'ui-monospace,Menlo,monospace', fontSize: 13, letterSpacing: 1, flexShrink: 0 } }, p.value),
676
- h('span', { style: { ...styles.muted, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, p.label || '—'),
677
- h('span', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary,#8b93a1)' } }, `${t('tempPinExpiresIn')} ${formatRemaining(p.expiresAt)}`),
678
- h('button', { style: { ...styles.btn, height: 24, padding: '0 10px', fontSize: 11 }, onClick: () => revokeTempPinNow(p) }, t('tempPinRevoke')),
679
- )),
680
- ),
681
- ),
682
-
683
558
  // 恢复出厂设置:设置出问题时的临时兜底(最底部,避免误触)
684
559
  h('div', { style: styles.block },
685
560
  h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
@@ -97,25 +97,6 @@ export const zh = {
97
97
  'error': '❌ 开启失败:{detail}(可重试;若是代理/VPN 问题见 README 排障)',
98
98
  'unknownError': '未知错误',
99
99
  'feedback': '有问题?欢迎到 GitHub Issues 反馈 🙏',
100
- // 临时 PIN(issue #69)
101
- 'tempPinTitle': '临时访问 PIN(带超时)',
102
- 'tempPinSubtitle': '给访客开一个有寿命的 8 位 PIN,过期自动作废;与主 PIN 共用速率限制。',
103
- 'tempPinKindPublic': '公网',
104
- 'tempPinKindLan': '局域网',
105
- 'tempPinDuration': '有效时长',
106
- 'tempPinDuration1h': '1 小时',
107
- 'tempPinDuration24h': '24 小时',
108
- 'tempPinDuration7d': '7 天',
109
- 'tempPinLabel': '备注(可选,给谁用)',
110
- 'tempPinLabelPh': '朋友小王',
111
- 'tempPinCreate': '生成',
112
- 'tempPinCreated': '已生成(仅显示一次)',
113
- 'tempPinCopied': '已复制',
114
- 'tempPinCopy': '复制',
115
- 'tempPinEmpty': '暂无临时 PIN',
116
- 'tempPinExpiresIn': '过期',
117
- 'tempPinRevoke': '撤销',
118
- 'tempPinRevoked': '已撤销',
119
100
  }
120
101
 
121
102
  /** English dictionary, key-identical to the Chinese source of truth. */
@@ -213,23 +194,4 @@ export const en = {
213
194
  'error': '❌ Failed to enable: {detail} (you can retry; for proxy/VPN issues see the README)',
214
195
  'unknownError': 'unknown error',
215
196
  'feedback': '🙏 Questions? Open an issue on GitHub',
216
- // Temp PIN (issue #69)
217
- 'tempPinTitle': 'Temporary access PINs (auto-expire)',
218
- 'tempPinSubtitle': 'Issue a time-limited 8-char PIN to a guest; expires automatically. Shares the main PIN rate limit.',
219
- 'tempPinKindPublic': 'Public',
220
- 'tempPinKindLan': 'LAN',
221
- 'tempPinDuration': 'Valid for',
222
- 'tempPinDuration1h': '1 hour',
223
- 'tempPinDuration24h': '24 hours',
224
- 'tempPinDuration7d': '7 days',
225
- 'tempPinLabel': 'Note (optional, who is this for)',
226
- 'tempPinLabelPh': 'Friend Alice',
227
- 'tempPinCreate': 'Create',
228
- 'tempPinCreated': 'Created (shown once)',
229
- 'tempPinCopied': 'Copied',
230
- 'tempPinCopy': 'Copy',
231
- 'tempPinEmpty': 'No temporary PINs yet',
232
- 'tempPinExpiresIn': 'expires',
233
- 'tempPinRevoke': 'Revoke',
234
- 'tempPinRevoked': 'Revoked',
235
197
  }
package/lib/index.js CHANGED
@@ -21,7 +21,7 @@ import { createPocketService } from './service.mjs';
21
21
  import { installPocketRpc } from './web-rpc.js';
22
22
  import { restartHost } from './restart.js';
23
23
  import { advancedNoticeScript, DEFAULT_INJECT, classifyHost } from './proxy.mjs';
24
- import { lanEnabled, setLanEnabled, lanAuthEnabled, setLanAuthEnabled, lanIpOverride, setLanIpOverride, pinCustom, setPinCustom, tunnelMode, setTunnelMode, tunnelToken, setTunnelToken, tunnelHostname, setTunnelHostname, resetSettings, proxyPort, tempPinValuesFor, tempPins as readTempPins, createTempPin, revokeTempPin, cloudflaredPath } from './settings.mjs';
24
+ import { lanEnabled, setLanEnabled, lanAuthEnabled, setLanAuthEnabled, lanIpOverride, setLanIpOverride, pinCustom, setPinCustom, tunnelMode, setTunnelMode, tunnelToken, setTunnelToken, tunnelHostname, setTunnelHostname, resetSettings, proxyPort, cloudflaredPath } from './settings.mjs';
25
25
 
26
26
  const name = 'dsh-pocket';
27
27
  const inject = ['connection', 'webServer'];
@@ -285,8 +285,6 @@ export function apply(ctx, config = {}, internals = {}) {
285
285
  sessionKey: randomBytes(16).toString('hex'),
286
286
  getToken: (host) => tokenForHost(host),
287
287
  isProtected: (host) => (classifyHost(host) === 'public' ? true : lanAuthEnabled()),
288
- // 临时 PIN(issue #69):与主 PIN 同 host 分类,仅在受保护入口校验
289
- getAltTokens: (host) => tempPinValuesFor(classifyHost(host) === 'public' ? 'public' : 'lan'),
290
288
  },
291
289
  // dsh web 浏览器会话启动 token(issue #77):新版 dsh(>= 0.1.2-alpha.1)要求根路径
292
290
  // 带一次 `?token=` 换 cookie,否则 /api 与 WebSocket 全 401。token 每次进程启动都变,
@@ -343,11 +341,6 @@ export function apply(ctx, config = {}, internals = {}) {
343
341
  if (token !== undefined) setTunnelToken(token);
344
342
  return { mode: tunnelMode(), hostname: tunnelHostname(), tokenSet: tunnelToken().length > 0 };
345
343
  },
346
- // 临时 PIN(issue #69):创建/列出/撤销。前端只看到 value 一次(创建时),
347
- // 列出时也回显 value(用户需要复制给别人),文件 0o600 + 仅本机 loopback RPC 兜底。
348
- listTempPins: () => readTempPins(),
349
- createTempPin: ({ kind, expiresInSec, label }) => createTempPin({ kind, expiresInSec, label }),
350
- revokeTempPin: (value, kind) => revokeTempPin(value, kind),
351
344
  runUpdate: internals.runUpdate ?? { currentVersion, perform: performUpdate, loadedVersion: () => loadedVersion },
352
345
  restart: internals.restart ?? (() => pocketRestart(service)),
353
346
  restartNotice: internals.restartNotice ?? consumeRestartNotice,
package/lib/proxy.mjs CHANGED
@@ -439,9 +439,9 @@ function attachWebSocketHeartbeat(socket, { intervalMs = 30_000, missLimit = 2 }
439
439
  * @param {string} [opts.host] 监听地址(默认 0.0.0.0:LAN 与隧道都能到)
440
440
  * @param {{host:string,port:number}} [opts.upstream] 上游 dsh web(默认 127.0.0.1:3080)
441
441
  * @param {string} [opts.injectHtml] 注入 HTML 的内容(默认 polyfill + 移动端适配;传 '' 关闭)
442
- * @param {object} [opts.auth] 可选访问令牌认证(issue #13 + #69):{ getToken, getAltTokens?, isProtected, sessionKey }
442
+ * @param {object} [opts.auth] 可选访问令牌认证(issue #13):{ getToken, getAltTokens?, isProtected, sessionKey }
443
443
  * - getToken(host) → 主 PIN(公网/局域网各一个,按 host 分类)
444
- * - getAltTokens?(host) → 临时 PIN 列表(issue #69),校验时与主 PIN 任一命中即放行
444
+ * - getAltTokens?(host) → 替代令牌列表(可选),校验时与主 PIN 任一命中即放行
445
445
  * @param {object|false} [opts.rateLimit] 登录速率限制参数覆盖(issue #40;测试用短窗口)
446
446
  * @param {object|false} [opts.heartbeat] WebSocket 心跳注入(PR #41):{ intervalMs, missLimit };false 关闭(默认开:30s/容忍 2 个静默周期)
447
447
  * @param {() => boolean} [opts.lanAccessEnabled] 局域网访问是否开启(默认开启)。关闭时拦截经局域网 Host 的请求(公网/loopback 不受影响)。
package/lib/settings.mjs CHANGED
@@ -214,81 +214,6 @@ export function setProxyPort(value) {
214
214
  return proxyPort();
215
215
  }
216
216
 
217
- // ---------- 临时 PIN(issue #69:带超时的临时访问) ----------
218
- // 让用户能给访客(朋友/同事/临时设备)开一个**有寿命**的 PIN,过期自动作废——
219
- //
220
- // 设计要点:
221
- // - 临时 PIN 与主 PIN **共用速率限制**(按 IP):攻击者拿到临时 PIN 也不能暴力破解别的;
222
- // - 临时 PIN **绑 host 分类**:公网临时 PIN 只在公网入口校验,局域网临时 PIN 只在局域网入口校验
223
- // (与主 PIN 的 host 分发一致;防止公网临时 PIN 被局域网内的人滥用,反之亦然);
224
- // - 过期**懒清理**:访问时过滤掉过期的,避免每次都写磁盘(频繁写影响响应时间);
225
- // 设置页列表操作或重启 dsh web 之后才落盘(writeSettings 在 setTempPin / revokeTempPin 时调用);
226
- // - 不复用主 PIN 的「自定义标记」:临时 PIN 是一次性生成,标记无意义;
227
- // - 存本机 0o600 settings.json(与 Token 一致)。
228
- //
229
- // 临时 PIN 默认长度同主 PIN(8 位字母数字),但由系统生成,用户不设置。
230
- // 一次性签名不重要:临时 PIN 本身足够长(8 位)且绑超时,被截获也只活到过期。
231
- //
232
- // 数据结构:settings.tempPins = [{ value, kind: 'public'|'lan', expiresAt: <epoch_ms>, label: <string> }]
233
- const TEMP_PIN_KINDS = new Set(['public', 'lan']);
234
- const TEMP_PIN_RE = /^[a-zA-Z0-9]{8}$/;
235
- /** 最小允许有效期 5 分钟(防误点立即过期让用户迷惑),最大 30 天。 */
236
- const TEMP_PIN_MIN_SEC = 5 * 60;
237
- const TEMP_PIN_MAX_SEC = 30 * 24 * 60 * 60;
238
- /** 临时 PIN 生成(issue #69):与主 PIN 同长度(8 位字母数字),用密码学随机。 */
239
- function newTempPin() {
240
- const bytes = randomBytes(8);
241
- // 把 8 字节转 base32-ish 字母数字串,再截 8 位;碰撞概率忽略(8 字节熵 ≈ 2^64)
242
- const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
243
- let out = '';
244
- for (let i = 0; i < 8; i++) out += alphabet[bytes[i] % alphabet.length];
245
- return out;
246
- }
247
-
248
- /** 读临时 PIN 列表(自动过滤过期项;不写磁盘)。 */
249
- export function tempPins() {
250
- const arr = readSettings().tempPins;
251
- if (!Array.isArray(arr)) return [];
252
- const now = Date.now();
253
- return arr
254
- .filter((p) => p && typeof p === 'object' && typeof p.value === 'string' && typeof p.expiresAt === 'number' && p.expiresAt > now)
255
- .map((p) => ({ value: p.value, kind: p.kind, expiresAt: p.expiresAt, label: typeof p.label === 'string' ? p.label : '' }));
256
- }
257
-
258
- /** 创建一个临时 PIN(issue #69)。返回 { value, expiresAt, kind, label }。 */
259
- export function createTempPin({ kind, expiresInSec, label = '' } = {}) {
260
- if (!TEMP_PIN_KINDS.has(kind)) throw new Error('临时 PIN 类型必须是 public 或 lan | temp pin kind must be public or lan');
261
- const sec = Number(expiresInSec);
262
- if (!Number.isInteger(sec) || sec < TEMP_PIN_MIN_SEC || sec > TEMP_PIN_MAX_SEC) {
263
- throw new Error(`临时 PIN 有效期需在 ${TEMP_PIN_MIN_SEC}-${TEMP_PIN_MAX_SEC} 秒之间 | expiresInSec out of range`);
264
- }
265
- const value = newTempPin();
266
- const expiresAt = Date.now() + sec * 1000;
267
- const s = readSettings();
268
- s.tempPins = Array.isArray(s.tempPins) ? s.tempPins.filter((p) => p && p.expiresAt > Date.now()) : [];
269
- s.tempPins.push({ value, kind, expiresAt, label: String(label).slice(0, 32) });
270
- writeSettings(s);
271
- return { value, kind, expiresAt, label: String(label).slice(0, 32) };
272
- }
273
-
274
- /** 撤销一个临时 PIN(按 value 匹配,kind 校验防止误删)。不存在也返回 false。 */
275
- export function revokeTempPin(value, kind) {
276
- const v = String(value ?? '');
277
- if (!TEMP_PIN_RE.test(v)) return false;
278
- const s = readSettings();
279
- if (!Array.isArray(s.tempPins)) return false;
280
- const before = s.tempPins.length;
281
- s.tempPins = s.tempPins.filter((p) => !(p?.value === v && (kind === undefined || p.kind === kind)));
282
- if (s.tempPins.length === before) return false;
283
- writeSettings(s);
284
- return true;
285
- }
286
-
287
- /** 给定 kind 返回所有未过期的临时 PIN 集合(value 数组),用于 proxy 校验。 */
288
- export function tempPinValuesFor(kind) {
289
- return tempPins().filter((p) => p.kind === kind).map((p) => p.value);
290
- }
291
-
292
217
  // ---------- cloudflared 路径(issue #45:远程 Linux 服务器下载源不可达时手动指定) ----------
293
218
  // Linux 服务器在国内/部分企业网下,所有 CDN 源(GitHub / ghproxy / gh.ddlc / gh-proxy)
294
219
  // 都连不上时,下载 cloudflared 二进制始终失败。允许用户在 settings.json 里**写死
package/lib/web-rpc.js CHANGED
@@ -25,7 +25,7 @@ export function killHint(port) {
25
25
  }
26
26
 
27
27
  /** 注册 /dsh-pocket 逻辑通道(仅本机 loopback 可调)。 */
28
- export function installPocketRpc(ctx, { service, log = console, desktop = false, runUpdate = null, restart = null, restartNotice = null, getToken = null, getLanToken = null, refreshLanToken = null, getLanAuthEnabled = null, setLanAuthEnabled = null, getLanEnabled = null, setLanEnabled = null, getLanIpOverride = null, setLanIpOverride = null, getPinCustom = null, setCustomPin = null, getTunnelConfig = null, setTunnelConfig = null, resetPocket = null, listTempPins = null, createTempPin = null, revokeTempPin = null }) {
28
+ export function installPocketRpc(ctx, { service, log = console, desktop = false, runUpdate = null, restart = null, restartNotice = null, getToken = null, getLanToken = null, refreshLanToken = null, getLanAuthEnabled = null, setLanAuthEnabled = null, getLanEnabled = null, setLanEnabled = null, getLanIpOverride = null, setLanIpOverride = null, getPinCustom = null, setCustomPin = null, getTunnelConfig = null, setTunnelConfig = null, resetPocket = null }) {
29
29
  if (!ctx?.connection?.rpc?.handle) {
30
30
  log.warn?.('dsh-pocket: DSH Host Connection RPC unavailable — settings tab disabled | 无 Connection RPC,设置页不可用');
31
31
  return () => {};
@@ -161,25 +161,6 @@ export function installPocketRpc(ctx, { service, log = console, desktop = false,
161
161
  const dshPort = service.dshPort ?? 3080;
162
162
  return ok({ ...result, hint: `重启后进程在后台运行;如需停止:${killHint(dshPort)}` });
163
163
  }
164
- // 临时 PIN(issue #69):生成 / 列出 / 撤销。临时 PIN 是带过期的主 PIN 同位替代品,
165
- // 用于把访问权限临时分给别人(朋友/同事/临时设备),过期自动作废。
166
- if (endpoint === POCKET_ENDPOINTS.tempPinCreate) {
167
- if (!createTempPin) return fail('bad-request', '临时 PIN 生成不可用 | temp pin create unavailable');
168
- try {
169
- const pin = createTempPin({ kind: payload?.kind, expiresInSec: payload?.expiresInSec, label: payload?.label });
170
- return ok(pin);
171
- } catch (err) {
172
- return fail('bad-request', err?.message ?? String(err));
173
- }
174
- }
175
- if (endpoint === POCKET_ENDPOINTS.tempPinList) {
176
- return ok({ tempPins: listTempPins?.() ?? [] });
177
- }
178
- if (endpoint === POCKET_ENDPOINTS.tempPinRevoke) {
179
- if (!revokeTempPin) return fail('bad-request', '临时 PIN 撤销不可用 | temp pin revoke unavailable');
180
- const ok = revokeTempPin(payload?.value, payload?.kind);
181
- return ok({ revoked: ok });
182
- }
183
164
  return fail('bad-request', `Unknown endpoint: ${endpoint}`);
184
165
  } catch (err) {
185
166
  log.error?.('dsh-pocket: rpc %s failed | RPC 失败: %s', endpoint, err?.message ?? err);
package/package.json CHANGED
@@ -80,5 +80,5 @@
80
80
  "access": "public",
81
81
  "registry": "https://registry.npmjs.org/"
82
82
  },
83
- "version": "2.6.2"
83
+ "version": "2.6.3"
84
84
  }