dsh-pocket 2.6.1 → 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.en.md +1 -1
- package/README.md +2 -2
- package/client/api.js +0 -3
- package/client/client.js +194 -320
- package/client/index.jsx +0 -125
- package/client/mobile/mobile-apply.tsx +1 -0
- package/client/pocket-locales.js +0 -38
- package/lib/index.js +1 -8
- package/lib/proxy.mjs +2 -2
- package/lib/settings.mjs +0 -75
- package/lib/tunnel.mjs +11 -5
- package/lib/web-rpc.js +1 -20
- package/package.json +1 -1
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 } },
|
|
@@ -3,6 +3,7 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
|
3
3
|
import { MobileNavToggle } from './MobileNavToggle.tsx'
|
|
4
4
|
import { MobileNavOverlay } from './MobileNavOverlay.tsx'
|
|
5
5
|
import { MobileDrawerFooter } from './MobileDrawerFooter.tsx'
|
|
6
|
+
import { MobileComposerFullscreen } from './MobileComposerFullscreen.tsx'
|
|
6
7
|
import { MOBILE_CSS } from './mobile.css.ts'
|
|
7
8
|
import { NS, en, zh } from './locales.ts'
|
|
8
9
|
import type { MobileNavKey } from './locales.ts'
|
package/client/pocket-locales.js
CHANGED
|
@@ -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,
|
|
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
|
|
442
|
+
* @param {object} [opts.auth] 可选访问令牌认证(issue #13):{ getToken, getAltTokens?, isProtected, sessionKey }
|
|
443
443
|
* - getToken(host) → 主 PIN(公网/局域网各一个,按 host 分类)
|
|
444
|
-
* - getAltTokens?(host) →
|
|
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/tunnel.mjs
CHANGED
|
@@ -56,8 +56,8 @@ export function platformAssets() {
|
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* cloudflared 下载源。
|
|
59
|
-
*
|
|
60
|
-
*
|
|
59
|
+
* 平台策略:macOS 优先清华 TUNA 镜像的 Homebrew bottle(国内 CDN,实测 ~3MB/s),
|
|
60
|
+
* 拿不到再回退;Linux / Windows 直接走官方源优先(见 downloadCloudflared 里的说明)。
|
|
61
61
|
* 兜底:官方 GitHub + 国内加速源(ghproxy.net / gh.ddlc.top / gh-proxy.com,2026-08
|
|
62
62
|
* 实测可达)。npmmirror(淘宝)没有 cloudflared 镜像(已实测 404)。
|
|
63
63
|
*/
|
|
@@ -182,7 +182,7 @@ export async function downloadFile(url, dest, { signal, segments = PARALLEL_SEGM
|
|
|
182
182
|
* 清华 TUNA 镜像的 cloudflared Homebrew bottle URL(国内 CDN,实测 ~3MB/s)。
|
|
183
183
|
* **仅 macOS**——Linux 的 Homebrew bottle 其 ELF 解释器是 `@@HOMEBREW_PREFIX@@`
|
|
184
184
|
* 占位符(需 brew install 时 patchelf 替换),没装 Homebrew 的机器直接 spawn 会
|
|
185
|
-
* ENOENT(issue #22);Linux 走官方 GitHub
|
|
185
|
+
* ENOENT(issue #22);Linux 走官方 GitHub 裸二进制(无需解压)+ 加速源。
|
|
186
186
|
* 匹配按 CPU 架构取清华目录里版本号最新的 bottle——Homebrew 构建时部署目标
|
|
187
187
|
* 设得较老、向后兼容,所以旧系统(如 Ventura)也能用新一点的 bottle。
|
|
188
188
|
* 抓目录失败/无匹配 → null(调用方回退 GitHub/加速源,不影响可用性)。
|
|
@@ -218,8 +218,14 @@ async function downloadCloudflared(binPath, signal) {
|
|
|
218
218
|
? AbortSignal.any([signal, AbortSignal.timeout(120_000)])
|
|
219
219
|
: AbortSignal.timeout(120_000);
|
|
220
220
|
|
|
221
|
-
//
|
|
222
|
-
|
|
221
|
+
// 源的顺序按平台定(issue #45):
|
|
222
|
+
// - macOS:清华 Homebrew bottle 排第一(国内 CDN 实测 ~3MB/s,给家用网络省几分钟),
|
|
223
|
+
// 拿不到再退回官方源 + 国内加速源。
|
|
224
|
+
// - Linux / Windows:不走 bottle,直接官方源优先。这两类环境网络差异太大
|
|
225
|
+
// (服务器、容器、 corporate 网络),多一层镜像就多一层失败模式;官方源
|
|
226
|
+
// 拿不到时,再用国内加速源兜底。
|
|
227
|
+
// bottle 与资产名无关,只算一次。
|
|
228
|
+
const bottle = os === 'darwin' ? await tsinghuaBottleUrl({ os, a }).catch(() => null) : null;
|
|
223
229
|
|
|
224
230
|
// 逐个资产试(linux 首选裸二进制,.tgz 只是回退),每个资产再逐个源试
|
|
225
231
|
const assets = platformAssets();
|
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
|
|
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