@mrrisega/dsh-remote 0.6.0-beta.8 → 0.6.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.md +10 -3
- package/clients/dsh-remote/dsh-bridge.mjs +30 -0
- package/clients/dsh-remote/e2ee-client.mjs +17 -0
- package/clients/dsh-remote/e2ee-shim-script.js +43 -3
- package/clients/dsh-remote/e2ee-shim.mjs +5 -0
- package/clients/dsh-remote/mobile-adapter.mjs +56 -2
- package/clients/dsh-remote/test/e2ee-client.test.mjs +13 -0
- package/package.json +1 -1
- package/packages/dsh-remote-web/lib/client.js +410 -133
- package/packages/dsh-remote-web/lib/index.js +109 -8
- package/packages/dsh-remote-web/test/access-key-sessions.test.mjs +79 -9
- package/packages/dsh-remote-web/test/e2ee-state.test.mjs +1 -1
- package/packages/dsh-remote-web/test/feedback-proxy.test.mjs +2 -1
- package/packages/dsh-remote-web/test/password-reset.test.mjs +430 -0
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +104 -7
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +56 -5
package/README.md
CHANGED
|
@@ -28,6 +28,13 @@ dsh-remote 是一个轻量的**隧道模式**远程控制方案:电脑端运
|
|
|
28
28
|
└→ bridge (电脑端) → 127.0.0.1:3080 (dsh web)
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
## 0.6.0 速览(用户最关心的新能力)
|
|
32
|
+
|
|
33
|
+
- **扫码即加密,不再要求再输一次密码**:用电脑生成的二维码/一次性链接进入后,由电脑自动授权开启端到端加密;密码登录点设备也自动解锁。全程不输 E2EE 密码、不存服务端。
|
|
34
|
+
- **记住本机:刷新/重开不掉回明文**:解锁一次后,手机 App 刷新、镜像页刷新都自动恢复加密(退出登录即清除;浏览器无痕可不用此功能)。
|
|
35
|
+
- **移动端体验**:选完会话抽屉自动收起;加密状态徽标几秒后自动缩成小 🔒 不挡界面;修复了移动端设置里“模型提供方目录加载失败”。
|
|
36
|
+
- **电脑端侧栏**:官方「设置」旁新增「📱 远程访问」快捷按钮(与官方按钮共存、不遮挡),一键进入远程访问面板。
|
|
37
|
+
|
|
31
38
|
## 界面预览
|
|
32
39
|
|
|
33
40
|
| 手机端进入 dsh web,100% 还原电脑端体验 | 手机端密钥登录后的在线设备列表 |
|
|
@@ -63,7 +70,7 @@ dsh-remote 是一个轻量的**隧道模式**远程控制方案:电脑端运
|
|
|
63
70
|
npx @mrrisega/dsh-remote
|
|
64
71
|
```
|
|
65
72
|
|
|
66
|
-
安装完成后**无需任何命令**:打开 dsh web → 设置 →
|
|
73
|
+
安装完成后**无需任何命令**:打开 dsh web → 设置 → 「远程访问」,注册/登录手机号即可
|
|
67
74
|
(注册在手机端完成,登录后 bridge 自动启动;自建用户在同一面板切「自建服务」标签)。
|
|
68
75
|
原来的独立设置页(`dsh-remote settings`)已移除,避免与插件面板重复造成困惑。
|
|
69
76
|
|
|
@@ -78,7 +85,7 @@ npx @mrrisega/dsh-remote setup --server wss://<你的域名>:端口 --key <访
|
|
|
78
85
|
|
|
79
86
|
> **版本与更新 / 卸载**:dsh 官方插件市场目前不提供更新按钮,也不会改写用户补丁(因此市场
|
|
80
87
|
> 卸载会提示「仍通过 insert 引用 dsh-remote-web」而拒绝)。插件设置面板里已内置管理入口
|
|
81
|
-
> (dsh web → 设置 →
|
|
88
|
+
> (dsh web → 设置 → 「远程访问」→「🔄 版本与更新」卡片):显示当前版本、自动检测 npm 新版、
|
|
82
89
|
> **一键在线更新**(后台补运行环境并重启 bridge,完成后重启 dsh web 生效)、以及**彻底卸载**
|
|
83
90
|
> (移除补丁 include / 依赖 / bundle 与本地文件,之后市场卸载或直接重启均可完成卸载)。
|
|
84
91
|
|
|
@@ -139,7 +146,7 @@ npx @mrrisega/dsh-remote setup --server wss://<你的域名>:端口 --key <访
|
|
|
139
146
|
| relay-router | `packages/relay-router/` | 中继服务器:bridge 注册表、实时设备列表、HTTP/WS 透明代理、配额 |
|
|
140
147
|
| bridge | `clients/dsh-remote/` | 电脑端守护进程:连 router 注册,把转发帧代理到本地 `dsh web`;心跳自愈 |
|
|
141
148
|
| PWA | `clients/dsh-web/native.html` | 手机端:登录 / 注册 / 设备选择(单文件,零构建) |
|
|
142
|
-
| dsh web 插件 | `packages/dsh-remote-web/` |
|
|
149
|
+
| dsh web 插件 | `packages/dsh-remote-web/` | 设置页「远程访问」面板:连接模式 / 账号 / bridge 启停 / 反馈 |
|
|
143
150
|
| 部署脚本 | `deploy/` | `install-open.sh` 自建引导、nginx 参考配置、Dockerfile / compose |
|
|
144
151
|
|
|
145
152
|
测试:`npm test`(router 契约 + 插件 + bridge 全部单测与回归)。
|
|
@@ -494,6 +494,10 @@ export async function handleHttpFrame(dchOrSend, frame) {
|
|
|
494
494
|
const send = toSender(dchOrSend);
|
|
495
495
|
const { id, method = "GET", path = "/", headers = {}, body, bodyBase64: isB64 } = frame;
|
|
496
496
|
const t0 = Date.now();
|
|
497
|
+
// 桌面授权引导(方案A):POST /_e2ee/intro(device/channel 形态归一)—— 本地应答,不连上游
|
|
498
|
+
if (method === "POST" && path === "/_e2ee/intro") {
|
|
499
|
+
return answerIntro(send, id);
|
|
500
|
+
}
|
|
497
501
|
// E2EE 信封标记即信号(§4.3):有标记=加密,无标记=明文 v1。
|
|
498
502
|
// 解封失败绝不静默降级 → 明文错误 + x-dsh-e2ee-error 头 + 日志。
|
|
499
503
|
if (hasEnvelopeMarker(headers)) {
|
|
@@ -548,6 +552,32 @@ export async function handleHttpFrame(dchOrSend, frame) {
|
|
|
548
552
|
}
|
|
549
553
|
}
|
|
550
554
|
|
|
555
|
+
/** 桌面授权引导(方案A):POST /_e2ee/intro —— 桥端本地应答,不连上游。
|
|
556
|
+
* 路由层已按 dsh_token cookie 完成同账号授权(device/channel 形态都只到本设备);
|
|
557
|
+
* 这里仅当桥端 E2EE 启用时一次性下发派生 MK(内存转发,不落盘不写日志)。 */
|
|
558
|
+
function answerIntro(send, id) {
|
|
559
|
+
let ok = false;
|
|
560
|
+
let body;
|
|
561
|
+
try {
|
|
562
|
+
const g = e2ee.introGrant();
|
|
563
|
+
body = { ok: true, v: 2, grant: "desktop-intro", mk: g.mk, profile: g.profile, epoch: g.epoch, ts: Date.now() };
|
|
564
|
+
ok = true;
|
|
565
|
+
console.log("[bridge] e2ee intro → 手机(同一账号,已授权;MK 仅内存一次性下发)");
|
|
566
|
+
} catch (e) {
|
|
567
|
+
const code = e instanceof E2eeError ? e.code : "e2ee_disabled";
|
|
568
|
+
body = { ok: false, v: 2, error: { code, message: e?.message || "电脑端未启用端到端加密,无法授权引导" } };
|
|
569
|
+
console.log(`[bridge] e2ee intro 被拒(${code}): ${e?.message || ""}`);
|
|
570
|
+
}
|
|
571
|
+
send({
|
|
572
|
+
id,
|
|
573
|
+
type: "http",
|
|
574
|
+
status: ok ? 200 : 409,
|
|
575
|
+
headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" },
|
|
576
|
+
body: Buffer.from(JSON.stringify(body)).toString("base64"),
|
|
577
|
+
bodyBase64: true
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
551
581
|
/** E2EE 失败回包:明文(无信封标记)+ x-dsh-e2ee-error,绝不把无法解密的密文当正文转发。 */
|
|
552
582
|
function sendE2eeHttpError(send, id, err) {
|
|
553
583
|
const code = err instanceof E2eeError ? err.code : "e2ee_failed";
|
|
@@ -590,6 +590,23 @@ export class E2eeService {
|
|
|
590
590
|
return this.enabled ? [E2EE_CAP] : [];
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
+
/**
|
|
594
|
+
* 桌面授权引导(方案A,2026-09 用户决策):供同账号手机端在「从未持有密码」的场景(扫码/
|
|
595
|
+
* 一次性链接登录)下免输密码建立 E2EE。桥端在内存中持有派生 MK,此处一次性下发 MK
|
|
596
|
+
* (base64url,仅内存转发;不落盘、不写日志)。调用前提:请求已由 relay-router 按
|
|
597
|
+
* dsh_token cookie 完成「同账号」授权(device/channel 形态都只到本设备)——
|
|
598
|
+
* 信任模型见 docs/e2ee-protocol.md §5.4 [DECISION]:引导瞬间以「持有效扫码会话=账号本人」
|
|
599
|
+
* 为准,存在被中继主动冒充的理论窗口(与扫码登录产品一致);引导后数据面照常走
|
|
600
|
+
* hello/probe 探针(双方已同持 MK)与 AEAD 信封。
|
|
601
|
+
* @returns {{mk:string, profile:string, epoch:number}}
|
|
602
|
+
*/
|
|
603
|
+
introGrant() {
|
|
604
|
+
if (!this.enabled || !Buffer.isBuffer(this.mk) || this.mk.length !== MK_BYTES) {
|
|
605
|
+
throw new E2eeError(this.reason || "e2ee_disabled", "电脑端未启用端到端加密,无法授权引导(请先在电脑端登录并确认密码已生效)");
|
|
606
|
+
}
|
|
607
|
+
return { mk: this.mk.toString("base64url"), profile: this.profile, epoch: this.epoch };
|
|
608
|
+
}
|
|
609
|
+
|
|
593
610
|
state() {
|
|
594
611
|
return { enabled: this.enabled, reason: this.reason, profile: this.profile, epoch: this.epoch };
|
|
595
612
|
}
|
|
@@ -598,6 +598,19 @@
|
|
|
598
598
|
}
|
|
599
599
|
|
|
600
600
|
/* ---- 状态徽标 ---- */
|
|
601
|
+
var _badgeCollapseTimer = null; // 自动收缩(仅 ok 态):展示几秒后缩成仅 🔒,不挡界面
|
|
602
|
+
function _badgeClearCollapse() {
|
|
603
|
+
try { if (_badgeCollapseTimer) { clearTimeout(_badgeCollapseTimer); _badgeCollapseTimer = null; } } catch (e) {}
|
|
604
|
+
}
|
|
605
|
+
function _badgeArmCollapse(el) {
|
|
606
|
+
_badgeClearCollapse();
|
|
607
|
+
_badgeCollapseTimer = setTimeout(function () {
|
|
608
|
+
try {
|
|
609
|
+
if (!el.classList.contains("expanded") && el.classList.contains("ok")) el.classList.add("collapsed");
|
|
610
|
+
} catch (e) {}
|
|
611
|
+
_badgeCollapseTimer = null;
|
|
612
|
+
}, 5000);
|
|
613
|
+
}
|
|
601
614
|
function _badgeEl() {
|
|
602
615
|
var el = document.getElementById("dsh-e2ee-badge");
|
|
603
616
|
if (el) return el;
|
|
@@ -613,7 +626,18 @@
|
|
|
613
626
|
var note = document.createElement("div");
|
|
614
627
|
note.className = "dsh-e2ee-badge-note";
|
|
615
628
|
el.appendChild(ico); el.appendChild(txt); el.appendChild(note);
|
|
616
|
-
el.addEventListener("click", function () {
|
|
629
|
+
el.addEventListener("click", function () {
|
|
630
|
+
if (el.classList.contains("collapsed")) {
|
|
631
|
+
// 从仅🔒芯片展开:展示完整文案(文本),不再自动收缩
|
|
632
|
+
el.classList.remove("collapsed");
|
|
633
|
+
el.classList.add("expanded");
|
|
634
|
+
_badgeClearCollapse();
|
|
635
|
+
} else {
|
|
636
|
+
el.classList.toggle("expanded");
|
|
637
|
+
_badgeClearCollapse();
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
el.addEventListener("pointerenter", function () { _badgeClearCollapse(); el.classList.remove("collapsed"); });
|
|
617
641
|
host.appendChild(el);
|
|
618
642
|
return el;
|
|
619
643
|
}
|
|
@@ -624,7 +648,7 @@
|
|
|
624
648
|
if (mode !== "ok" && window.innerWidth > 820) return;
|
|
625
649
|
var el = _badgeEl();
|
|
626
650
|
if (!el) return;
|
|
627
|
-
el.className = mode; // ok | warn | err
|
|
651
|
+
el.className = mode; // ok | warn | err(重置 expanded/collapsed 由下方状态机管理)
|
|
628
652
|
el.querySelector(".dsh-e2ee-badge-ico").textContent = mode === "ok" ? "🔒" : "⚠";
|
|
629
653
|
el.querySelector(".dsh-e2ee-badge-txt").textContent = text;
|
|
630
654
|
var noteEl = el.querySelector(".dsh-e2ee-badge-note");
|
|
@@ -639,6 +663,8 @@
|
|
|
639
663
|
}
|
|
640
664
|
}
|
|
641
665
|
el.title = text;
|
|
666
|
+
// 自动收缩:仅加密态(ok)几秒后缩成纯 🔒 芯片(warn/err 属安全提示保持文本,不静默)
|
|
667
|
+
if (mode === "ok") _badgeArmCollapse(el);
|
|
642
668
|
} catch (e) { /* 徽标失败不阻断页面 */ }
|
|
643
669
|
}
|
|
644
670
|
function _appUrl() {
|
|
@@ -669,6 +695,7 @@
|
|
|
669
695
|
} catch (e) { /* ignore */ }
|
|
670
696
|
}
|
|
671
697
|
|
|
698
|
+
/* (刷新恢复见 _boot:优先 localStorage 持久交接单,2026-09) */
|
|
672
699
|
/* ---- 启动 ---- */
|
|
673
700
|
function _boot() {
|
|
674
701
|
try {
|
|
@@ -678,7 +705,20 @@
|
|
|
678
705
|
}
|
|
679
706
|
var raw = _readHandover();
|
|
680
707
|
if (!raw) {
|
|
681
|
-
|
|
708
|
+
// 刷新/重开:sessionStorage 一次性交接单已被读走 → 尝试 localStorage「记住本机」持久交接单
|
|
709
|
+
// (2026-09 用户决策;同一解锁会话的 SHK,刷新镜像页后仍能恢复加密)。
|
|
710
|
+
var lsRaw = null;
|
|
711
|
+
try { lsRaw = localStorage.getItem(DSH_E2EE_HANDOVER_KEY); } catch (e) { lsRaw = null; }
|
|
712
|
+
if (lsRaw) {
|
|
713
|
+
var lsSess = null;
|
|
714
|
+
try { lsSess = seSessionOfHandover(lsRaw); } catch (e) { lsSess = null; }
|
|
715
|
+
if (lsSess) {
|
|
716
|
+
_install(lsSess);
|
|
717
|
+
_badge("ok", "端到端加密已开启", "刷新后已自动恢复端到端加密(记住本机);密钥仅存内存。");
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
_badge("warn", "未加密:未解锁(明文)", "本次为明文连接。密钥只存内存;请返回设备列表重新点选设备解锁后,镜像页的数据请求与 WebSocket 将自动加密。");
|
|
682
722
|
return;
|
|
683
723
|
}
|
|
684
724
|
var sess = null;
|
|
@@ -84,6 +84,11 @@ const STYLE = `
|
|
|
84
84
|
}
|
|
85
85
|
#dsh-e2ee-badge.expanded .dsh-e2ee-badge-note { display: block; }
|
|
86
86
|
#dsh-e2ee-badge .dsh-e2ee-badge-note a { color: #0b57d0; text-decoration: underline; }
|
|
87
|
+
/* 自动收缩态(2026-09):加密态展示数秒后缩成仅 🔒 芯片,不挡右上界面;悬停/点击即展开 */
|
|
88
|
+
#dsh-e2ee-badge.collapsed { padding: 5px 7px; gap: 0; max-width: 40px; }
|
|
89
|
+
#dsh-e2ee-badge.collapsed .dsh-e2ee-badge-txt,
|
|
90
|
+
#dsh-e2ee-badge.collapsed .dsh-e2ee-badge-note { display: none; }
|
|
91
|
+
#dsh-e2ee-badge { transition: padding .25s ease, max-width .25s ease; }
|
|
87
92
|
@media (max-width: 480px) {
|
|
88
93
|
#dsh-e2ee-badge { padding: 5px 10px; font-size: 11px; }
|
|
89
94
|
}
|
|
@@ -221,6 +221,41 @@ const STYLE = `
|
|
|
221
221
|
|
|
222
222
|
const SCRIPT = `(() => {
|
|
223
223
|
"use strict";
|
|
224
|
+
|
|
225
|
+
/* —— 主机门解除(2026-09):镜像页(如 n.risegao.cn)经设备流认证回连同一台 127.0.0.1:3080,
|
|
226
|
+
桥已把 Host 回环化并剥 Origin(不扩大信任面)。官方客户端以 transport.ownsHost 判定
|
|
227
|
+
「自有主机」,非回环域名下不置 true 会把设置页置为 memory(不可用)镜像 →
|
|
228
|
+
模型/提供方设置无法加载(“Settings are unavailable in this browser”)。
|
|
229
|
+
在模块执行前安装定义陷阱:任何 __DSH_TRANSPORT__ 赋值都带 ownsHost=true。 —— */
|
|
230
|
+
(function () {
|
|
231
|
+
function forceOwns(v) {
|
|
232
|
+
try {
|
|
233
|
+
if (v && typeof v === "object") {
|
|
234
|
+
const d = Object.getOwnPropertyDescriptor(v, "ownsHost");
|
|
235
|
+
if (!d || d.configurable || d.writable) {
|
|
236
|
+
try {
|
|
237
|
+
Object.defineProperty(v, "ownsHost", { configurable: true, enumerable: true, get() { return true; }, set() {} });
|
|
238
|
+
} catch (e) { v.ownsHost = true; }
|
|
239
|
+
} else { v.ownsHost = true; }
|
|
240
|
+
}
|
|
241
|
+
} catch (e) { /* 忽略 */ }
|
|
242
|
+
return v;
|
|
243
|
+
}
|
|
244
|
+
let tr = (() => { try { return globalThis.__DSH_TRANSPORT__ || null; } catch (e) { return null; } })();
|
|
245
|
+
if (tr) { tr = forceOwns(tr); }
|
|
246
|
+
else { tr = { ownsHost: true }; } // 壳未注入 transport 的镜像页也按「自有主机」处理
|
|
247
|
+
try {
|
|
248
|
+
Object.defineProperty(globalThis, "__DSH_TRANSPORT__", {
|
|
249
|
+
configurable: true,
|
|
250
|
+
enumerable: true,
|
|
251
|
+
get() { return tr; },
|
|
252
|
+
set(v) { tr = v ? forceOwns(v) : tr; }
|
|
253
|
+
});
|
|
254
|
+
} catch (e) {
|
|
255
|
+
try { globalThis.__DSH_TRANSPORT__ = tr; } catch (e2) { /* 忽略 */ }
|
|
256
|
+
}
|
|
257
|
+
})();
|
|
258
|
+
|
|
224
259
|
try {
|
|
225
260
|
const HOST_RE = new RegExp(${JSON.stringify(RUNTIME_HOST_RE.source)});
|
|
226
261
|
const NARROW = () => window.innerWidth <= 820;
|
|
@@ -246,8 +281,8 @@ const SCRIPT = `(() => {
|
|
|
246
281
|
|
|
247
282
|
/* ================= 鲸鱼挂件手机辅助(仅 ≤820 生效;挂件类名 .dshwv-* 稳定) ========= */
|
|
248
283
|
let whaleGuardsOn = false;
|
|
249
|
-
let whaleHitMap = null;
|
|
250
|
-
let
|
|
284
|
+
let whaleHitMap = null; let whaleUnstuckDone = false;
|
|
285
|
+
let drawerAutoCloseOn = false; // 窄屏“选中会话后自动收抽屉”委托只挂一次
|
|
251
286
|
|
|
252
287
|
const WHALE_IMG = () => document.querySelector(".dshwv-img");
|
|
253
288
|
const WHALE_ROOT = () => document.querySelector(".dshwv-root");
|
|
@@ -477,6 +512,25 @@ const SCRIPT = `(() => {
|
|
|
477
512
|
new MutationObserver(sync).observe(frame, { attributes: true, attributeFilter: ["data-sidebar-collapsed", "data-details-collapsed"] });
|
|
478
513
|
} catch (e2) { /* 退化:仅在下次 boot 同步 */ }
|
|
479
514
|
sync();
|
|
515
|
+
/* 窄屏抽屉:点选会话(激活行)/点侧栏内「新建会话」后自动收起(官方不做;适配层点官方 toggle)。
|
|
516
|
+
仅当:窄屏 && 抽屉展开 && 点击发生在侧栏抽屉内;行内「⋯」按钮 stopPropagation 不会误触。 */
|
|
517
|
+
if (!drawerAutoCloseOn) {
|
|
518
|
+
drawerAutoCloseOn = true;
|
|
519
|
+
document.addEventListener("click", (e) => {
|
|
520
|
+
try {
|
|
521
|
+
if (!NARROW() || !expanded()) return;
|
|
522
|
+
const t = e.target;
|
|
523
|
+
if (!t || !t.closest) return;
|
|
524
|
+
if (t.closest(".dsh-ma-scrim, .dsh-ma-hamburger")) return;
|
|
525
|
+
if (!t.closest(".dsh-ma-sidebar")) return; // 只处理抽屉内的点选
|
|
526
|
+
const row = t.closest('[role="treeitem"][aria-selected]');
|
|
527
|
+
const isNew = t.closest('[aria-label*="新建会话"], [aria-label*="New session" i]');
|
|
528
|
+
if (!row && !isNew) return;
|
|
529
|
+
const togg = toggleOf();
|
|
530
|
+
if (togg) setTimeout(() => { try { togg.click(); } catch (e5) { /* ignore */ } }, 0);
|
|
531
|
+
} catch (e6) { /* 忽略 */ }
|
|
532
|
+
});
|
|
533
|
+
}
|
|
480
534
|
whaleHookInit();
|
|
481
535
|
watchSettings();
|
|
482
536
|
};
|
|
@@ -294,6 +294,19 @@ test("E2eeService:disabled 状态 caps 为空;enabled 后 caps=['e2ee-v2']", ()
|
|
|
294
294
|
assert.equal(on.state().reason, "ok");
|
|
295
295
|
});
|
|
296
296
|
|
|
297
|
+
test("E2eeService.introGrant:启用时一次性下发派生 MK(32B b64url);禁用时抛 e2ee_disabled", () => {
|
|
298
|
+
const off = new E2eeService({ enabled: false, reason: "server_disabled" });
|
|
299
|
+
assert.throws(() => off.introGrant(), (e) => e.code === "server_disabled" || e.code === "e2ee_disabled");
|
|
300
|
+
const mk = deriveMasterKey("unit-password", SALT_B64);
|
|
301
|
+
const on = new E2eeService({ enabled: true, mk, salt: SALT_B64, profile: "pbkdf2-sha256-600k", epoch: 3 });
|
|
302
|
+
const g = on.introGrant();
|
|
303
|
+
assert.equal(g.profile, "pbkdf2-sha256-600k");
|
|
304
|
+
assert.equal(g.epoch, 3);
|
|
305
|
+
assert.equal(typeof g.mk, "string");
|
|
306
|
+
assert.equal(Buffer.from(g.mk, "base64url").length, 32);
|
|
307
|
+
assert.equal(Buffer.from(g.mk, "base64url").toString("hex"), mk.toString("hex"), "引导下发的 MK 应与桥端派生 MK 一致(否则手机无法通过探针)");
|
|
308
|
+
});
|
|
309
|
+
|
|
297
310
|
test("E2eeService.init:服务端 enabled=false/401/无密码/禁用开关 → 明文回退原因", async () => {
|
|
298
311
|
// 无密码
|
|
299
312
|
const noPw = await E2eeService.init({ apiBase: "http://127.0.0.1:1", token: "t", password: "" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrisega/dsh-remote",
|
|
3
|
-
"version": "0.6.0
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "手机远程控制 DeepSeek Harness · Remote control DeepSeek Harness (dsh web) from any phone browser — 100% 全功能 App 级体验:发消息、看工具执行、审批权限、改设置、管凭据,含特权操作,免内网穿透。一条命令安装 npx @mrrisega/dsh-remote。Mobile remote control for DSH, self-host or SaaS, no server needed on LAN.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|