@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
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// - 读写配置目录下 .dsh-config.json(0600)
|
|
5
5
|
// - 查询/启停 bridge(launchctl,plist 缺失时自动生成,逻辑与 dsh-setup.mjs 一致)
|
|
6
6
|
// - 代理 relay API(captcha / register / login / public-config),直连、不走系统代理;
|
|
7
|
-
// 另代理企业端一次性访问密钥 / 授权设备(/api/auth-key、/api/mobile-sessions、…/revoke
|
|
7
|
+
// 另代理企业端一次性访问密钥 / 授权设备(/api/auth-key、/api/mobile-sessions、…/revoke、
|
|
8
|
+
// DELETE …/:id、POST …/purge,Bearer)供面板「📱 远程访问」卡使用
|
|
8
9
|
// - 自管理 self*(版本可见 / 新版检测 / 一键在线更新 / 彻底卸载):插件市场没有更新卸载按钮,
|
|
9
10
|
// 面板内即官方管理入口;更新=后台 npx 按 dist-tag(默认 latest,DSH_UPDATE_TAG 可切 beta/alpha)(幂等补齐运行环境并重启 bridge);
|
|
10
11
|
// 彻底卸载=profile 插件清理(uninstallSelf)+ 运行时清理(uninstallRuntime:停 bridge 自启动 /
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, realpathSync, accessSync, chmodSync, openSync, closeSync, rmSync, constants as fsConstants } from "node:fs";
|
|
17
18
|
import { join, dirname, sep } from "node:path";
|
|
18
19
|
import { execSync, spawn } from "node:child_process";
|
|
19
|
-
import { homedir,
|
|
20
|
+
import { homedir, platform } from "node:os";
|
|
20
21
|
import { fileURLToPath } from "node:url";
|
|
21
22
|
|
|
22
23
|
/** 本插件在 host 侧的服务依赖。 */
|
|
@@ -652,7 +653,7 @@ async function relayAccount(relayDir) {
|
|
|
652
653
|
if (!r.ok || !r.body || !r.body.user) return null;
|
|
653
654
|
const u = r.body.user;
|
|
654
655
|
return {
|
|
655
|
-
phone: u.phone || "",
|
|
656
|
+
phone: maskPhone(u.phone || ""),
|
|
656
657
|
plan: u.plan || "free",
|
|
657
658
|
plan_source: u.plan_source || "plan",
|
|
658
659
|
plan_ends_at: u.plan_ends_at ?? null,
|
|
@@ -802,6 +803,51 @@ async function proxyRevokeMobileSession(relayDir, req, res) {
|
|
|
802
803
|
return sendJson(res, 200, { ok: true, relayStatus: (r && r.status) || 200 });
|
|
803
804
|
}
|
|
804
805
|
|
|
806
|
+
/**
|
|
807
|
+
* DELETE /dsh-remote/mobile-sessions/delete(body {id})→ 删除本机该设备的授权记录
|
|
808
|
+
* (企业端 DELETE /api/mobile-sessions/:id,Bearer:本人整行删除并拉黑 jti)。
|
|
809
|
+
*/
|
|
810
|
+
async function proxyDeleteMobileSession(relayDir, req, res) {
|
|
811
|
+
const body = await readJsonBody(req);
|
|
812
|
+
if (body.__parseError) return sendJson(res, 400, { ok: false, error: "JSON 解析失败" });
|
|
813
|
+
const id = String(body.id ?? "").trim();
|
|
814
|
+
if (!id) return sendJson(res, 400, { ok: false, error: "缺少参数 id(会话 ID)" });
|
|
815
|
+
const token = await relayToken(relayDir).catch(() => "");
|
|
816
|
+
if (!token) return sendJson(res, 401, notLoggedInJson());
|
|
817
|
+
const r = await relayFetch(relayDir, `/api/mobile-sessions/${encodeURIComponent(id)}`, {
|
|
818
|
+
method: "DELETE",
|
|
819
|
+
headers: { authorization: `Bearer ${token}` },
|
|
820
|
+
});
|
|
821
|
+
const d = flattenRelayBody(r);
|
|
822
|
+
const ok = !!(r.ok && d.ok !== false);
|
|
823
|
+
if (!ok) {
|
|
824
|
+
return sendJson(res, (r && r.status) || 502, { ok: false, error: relayErrorMessage(r), relayStatus: (r && r.status) || 0 });
|
|
825
|
+
}
|
|
826
|
+
return sendJson(res, 200, { ok: true, relayStatus: (r && r.status) || 200 });
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* POST /dsh-remote/mobile-sessions/purge → 清理本人所有已解绑(revoked)记录
|
|
831
|
+
* (企业端 POST /api/mobile-sessions/purge,Bearer)。
|
|
832
|
+
*/
|
|
833
|
+
async function proxyPurgeMobileSessions(relayDir, res) {
|
|
834
|
+
const token = await relayToken(relayDir).catch(() => "");
|
|
835
|
+
if (!token) return sendJson(res, 401, notLoggedInJson());
|
|
836
|
+
const r = await relayFetch(relayDir, "/api/mobile-sessions/purge", {
|
|
837
|
+
method: "POST",
|
|
838
|
+
headers: { authorization: `Bearer ${token}` },
|
|
839
|
+
});
|
|
840
|
+
const d = flattenRelayBody(r);
|
|
841
|
+
const ok = !!(r.ok && d.ok !== false);
|
|
842
|
+
if (!ok) {
|
|
843
|
+
return sendJson(res, (r && r.status) || 502, { ok: false, error: relayErrorMessage(r), relayStatus: (r && r.status) || 0 });
|
|
844
|
+
}
|
|
845
|
+
const removed = Number.isInteger(d.removed) ? d.removed : null;
|
|
846
|
+
const payload = { ok: true, relayStatus: (r && r.status) || 200 };
|
|
847
|
+
if (removed !== null) payload.removed = removed;
|
|
848
|
+
return sendJson(res, 200, payload);
|
|
849
|
+
}
|
|
850
|
+
|
|
805
851
|
// ---------- 综合状态 ----------
|
|
806
852
|
|
|
807
853
|
/**
|
|
@@ -829,6 +875,15 @@ export function readE2eeStateFile(relayDir) {
|
|
|
829
875
|
}
|
|
830
876
|
}
|
|
831
877
|
|
|
878
|
+
/**
|
|
879
|
+
* 手机号脱敏(隐私审计 2026-09):面板/镜像 UI 一律不下发明文手机号。
|
|
880
|
+
* 明文只在 bridge 本机 config / 服务端账号体系内流转,浏览器侧仅见掩码。
|
|
881
|
+
*/
|
|
882
|
+
function maskPhone(p) {
|
|
883
|
+
const s = String(p || "");
|
|
884
|
+
return s.length >= 7 ? s.slice(0, 3) + "****" + s.slice(-4) : (s ? s.slice(0, 1) + "****" : "");
|
|
885
|
+
}
|
|
886
|
+
|
|
832
887
|
async function composeStatus(relayDir) {
|
|
833
888
|
const cfg = loadConfig(relayDir);
|
|
834
889
|
const launchd = launchdStatus();
|
|
@@ -847,7 +902,8 @@ async function composeStatus(relayDir) {
|
|
|
847
902
|
return {
|
|
848
903
|
ok: true,
|
|
849
904
|
config: {
|
|
850
|
-
phone: cfg.phone || "",
|
|
905
|
+
phone: maskPhone(cfg.phone || ""),
|
|
906
|
+
hasPhone: Boolean(cfg.phone),
|
|
851
907
|
hasPassword: Boolean(cfg.password),
|
|
852
908
|
deviceId: cfg.device_id || "",
|
|
853
909
|
apiUrl,
|
|
@@ -867,7 +923,7 @@ async function composeStatus(relayDir) {
|
|
|
867
923
|
// {enabled, reason, profile, epoch, caps},供面板「📱 远程访问」卡展示加密状态。
|
|
868
924
|
e2ee: readE2eeStateFile(relayDir),
|
|
869
925
|
},
|
|
870
|
-
|
|
926
|
+
// 隐私审计(2026-09):不再下发真实 hostname(移除 host 字段)——设备标识统一走 deviceId/服务端登记名
|
|
871
927
|
};
|
|
872
928
|
}
|
|
873
929
|
|
|
@@ -974,7 +1030,7 @@ const PLUGIN_ID = "dsh-remote-web";
|
|
|
974
1030
|
const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
|
|
975
1031
|
const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
|
|
976
1032
|
/** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
|
|
977
|
-
const PLUGIN_VERSION = "0.6.0
|
|
1033
|
+
const PLUGIN_VERSION = "0.6.0";
|
|
978
1034
|
const UPDATE_LOG = ".dsh-update.log";
|
|
979
1035
|
const UPDATE_MARKER = ".dsh-update-running";
|
|
980
1036
|
|
|
@@ -1250,6 +1306,22 @@ function registerRoutes(ctx, relayDir) {
|
|
|
1250
1306
|
await proxyRevokeMobileSession(relayDir, req, res);
|
|
1251
1307
|
},
|
|
1252
1308
|
},
|
|
1309
|
+
// 删除已授权设备记录(整行删除并拉黑 jti;企业端 DELETE /api/mobile-sessions/:id,Bearer)
|
|
1310
|
+
{
|
|
1311
|
+
method: "DELETE",
|
|
1312
|
+
path: "/dsh-remote/mobile-sessions/delete",
|
|
1313
|
+
handler: async (req, res) => {
|
|
1314
|
+
await proxyDeleteMobileSession(relayDir, req, res);
|
|
1315
|
+
},
|
|
1316
|
+
},
|
|
1317
|
+
// 清理本人全部已解绑(revoked)记录(企业端 POST /api/mobile-sessions/purge,Bearer)
|
|
1318
|
+
{
|
|
1319
|
+
method: "POST",
|
|
1320
|
+
path: "/dsh-remote/mobile-sessions/purge",
|
|
1321
|
+
handler: async (_req, res) => {
|
|
1322
|
+
await proxyPurgeMobileSessions(relayDir, res);
|
|
1323
|
+
},
|
|
1324
|
+
},
|
|
1253
1325
|
{
|
|
1254
1326
|
method: "POST",
|
|
1255
1327
|
path: "/dsh-remote/config",
|
|
@@ -1320,7 +1392,11 @@ function registerRoutes(ctx, relayDir) {
|
|
|
1320
1392
|
handler: async (req, res) => {
|
|
1321
1393
|
const body = await readJsonBody(req);
|
|
1322
1394
|
if (body.__parseError) return sendJson(res, 400, { ok: false, error: "JSON 解析失败" });
|
|
1323
|
-
|
|
1395
|
+
let phone = String(body.phone ?? "").trim();
|
|
1396
|
+
if (!phone) {
|
|
1397
|
+
// 隐私审计(2026-09):账号卡修改密码不再下发明文手机号——服务端以本机账号为准
|
|
1398
|
+
phone = String((loadConfig(relayDir).phone) || "").trim();
|
|
1399
|
+
}
|
|
1324
1400
|
if (!phone) return sendJson(res, 400, { ok: false, error: "手机号必填" });
|
|
1325
1401
|
const r = await relayFetch(relayDir, "/api/sms-code", {
|
|
1326
1402
|
method: "POST", headers: { "content-type": "application/json" },
|
|
@@ -1329,6 +1405,31 @@ function registerRoutes(ctx, relayDir) {
|
|
|
1329
1405
|
sendJson(res, r.status || 502, { ok: r.ok, status: r.status, body: r.body });
|
|
1330
1406
|
},
|
|
1331
1407
|
},
|
|
1408
|
+
{
|
|
1409
|
+
method: "POST",
|
|
1410
|
+
path: "/dsh-remote/password/reset",
|
|
1411
|
+
handler: async (req, res) => {
|
|
1412
|
+
// 公开 POST /api/password/reset(无需 Bearer):短信验证码重置密码。
|
|
1413
|
+
// 成功 → 企业端使该账号全部授权设备/会话失效(含 E2EE 派生口令)。
|
|
1414
|
+
const body = await readJsonBody(req);
|
|
1415
|
+
if (body.__parseError) return sendJson(res, 400, { ok: false, error: "JSON 解析失败" });
|
|
1416
|
+
let phone = String(body.phone ?? "").trim();
|
|
1417
|
+
if (!phone) {
|
|
1418
|
+
// 隐私审计(2026-09):同上,账号卡改密不传明文手机号,服务端回填本机账号
|
|
1419
|
+
phone = String((loadConfig(relayDir).phone) || "").trim();
|
|
1420
|
+
}
|
|
1421
|
+
const smsCode = String(body.sms_code ?? "").trim();
|
|
1422
|
+
const newPassword = String(body.new_password ?? body.password ?? "");
|
|
1423
|
+
if (!phone || !smsCode || !newPassword) return sendJson(res, 400, { ok: false, error: "手机号、短信验证码与新密码必填" });
|
|
1424
|
+
const r = await relayFetch(relayDir, "/api/password/reset", {
|
|
1425
|
+
method: "POST",
|
|
1426
|
+
headers: { "content-type": "application/json" },
|
|
1427
|
+
body: JSON.stringify({ phone, sms_code: smsCode, new_password: newPassword }),
|
|
1428
|
+
});
|
|
1429
|
+
// 透传企业端 ok/error(成功 200 {ok:true};失败保留 status 与错误体)
|
|
1430
|
+
sendJson(res, r.status || 502, { ok: r.ok, status: r.status, body: r.body });
|
|
1431
|
+
},
|
|
1432
|
+
},
|
|
1332
1433
|
{
|
|
1333
1434
|
method: "GET",
|
|
1334
1435
|
path: "/dsh-remote/captcha",
|
|
@@ -1421,7 +1522,7 @@ function registerRoutes(ctx, relayDir) {
|
|
|
1421
1522
|
feedbackUrl: api,
|
|
1422
1523
|
reachable,
|
|
1423
1524
|
deviceId: cfg.device_id || "",
|
|
1424
|
-
phone: cfg.phone || "",
|
|
1525
|
+
phone: maskPhone(cfg.phone || ""), // 隐私:浏览器端只见掩码(代理提交时服务端另附真号)
|
|
1425
1526
|
// 登录态(已配置账号或自建密钥)→ 节点半自动附加 JWT,免图形验证码
|
|
1426
1527
|
auth: cfg.local_key || (cfg.phone && cfg.password) ? "account" : "anonymous"
|
|
1427
1528
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// 插件 node 半新增代理回归:/dsh-remote/access-key | mobile-sessions | mobile-sessions/revoke
|
|
2
|
-
//
|
|
2
|
+
// | mobile-sessions/delete | mobile-sessions/purge
|
|
3
|
+
// (企业端 E1 契约:POST /api/auth-key、GET /api/mobile-sessions、POST /api/mobile-sessions/:id/revoke、
|
|
4
|
+
// DELETE /api/mobile-sessions/:id、POST /api/mobile-sessions/purge,Bearer device-login JWT)
|
|
3
5
|
// 覆盖:Bearer 透传、字段扁平化、未登录 401、qr 缺失容错、上游错误透传。
|
|
4
6
|
import assert from "node:assert/strict";
|
|
5
7
|
import http from "node:http";
|
|
@@ -41,6 +43,9 @@ function startFakeRelay(opts = {}) {
|
|
|
41
43
|
}
|
|
42
44
|
if (req.method === "POST" && url.pathname === "/api/mobile-sessions/ms_1/revoke") return send(200, { ok: true });
|
|
43
45
|
if (req.method === "POST" && url.pathname === "/api/mobile-sessions/ghost/revoke") return send(404, { error: { message: "not_found" } });
|
|
46
|
+
if (req.method === "DELETE" && url.pathname === "/api/mobile-sessions/ms_1") return send(200, { ok: true });
|
|
47
|
+
if (req.method === "DELETE" && url.pathname === "/api/mobile-sessions/ghost") return send(404, { error: { message: "not_found" } });
|
|
48
|
+
if (req.method === "POST" && url.pathname === "/api/mobile-sessions/purge") return send(200, { ok: true, removed: 2 });
|
|
44
49
|
send(404, { error: { code: "not_found" } });
|
|
45
50
|
});
|
|
46
51
|
return new Promise((resolve) => srv.listen(0, "127.0.0.1", () => resolve({ srv, seen, port: srv.address().port })));
|
|
@@ -155,7 +160,66 @@ test("mobile-sessions/revoke 路由:body {id} 转发到 /api/mobile-sessions/:
|
|
|
155
160
|
}
|
|
156
161
|
});
|
|
157
162
|
|
|
158
|
-
test("
|
|
163
|
+
test("mobile-sessions/delete 路由:DELETE body {id} 转发到企业端 DELETE /api/mobile-sessions/:id(拉黑 jti)", async () => {
|
|
164
|
+
const relay = await startFakeRelay();
|
|
165
|
+
const { host, base, tempDir } = await bootRelay(relay.port);
|
|
166
|
+
try {
|
|
167
|
+
const r = await (await fetch(`${base}/dsh-remote/mobile-sessions/delete`, {
|
|
168
|
+
method: "DELETE",
|
|
169
|
+
headers: { "content-type": "application/json" },
|
|
170
|
+
body: JSON.stringify({ id: "ms_1" }),
|
|
171
|
+
})).json();
|
|
172
|
+
assert.equal(r.ok, true);
|
|
173
|
+
const up = relay.seen.find((s) => s.path === "/api/mobile-sessions/ms_1" && s.method === "DELETE");
|
|
174
|
+
assert.ok(up, "应转发到企业端 DELETE /api/mobile-sessions/ms_1");
|
|
175
|
+
assert.equal(up.authorization, "Bearer jwt-abc");
|
|
176
|
+
|
|
177
|
+
// 缺 id → 400;不存在的会话 → 透传 404 文案
|
|
178
|
+
const bad = await (await fetch(`${base}/dsh-remote/mobile-sessions/delete`, {
|
|
179
|
+
method: "DELETE",
|
|
180
|
+
headers: { "content-type": "application/json" },
|
|
181
|
+
body: JSON.stringify({}),
|
|
182
|
+
})).json();
|
|
183
|
+
assert.equal(bad.ok, false);
|
|
184
|
+
assert.match(String(bad.error), /id/);
|
|
185
|
+
|
|
186
|
+
const ghost = await (await fetch(`${base}/dsh-remote/mobile-sessions/delete`, {
|
|
187
|
+
method: "DELETE",
|
|
188
|
+
headers: { "content-type": "application/json" },
|
|
189
|
+
body: JSON.stringify({ id: "ghost" }),
|
|
190
|
+
})).json();
|
|
191
|
+
assert.equal(ghost.ok, false);
|
|
192
|
+
assert.match(String(ghost.error), /not_found/);
|
|
193
|
+
} finally {
|
|
194
|
+
host.close();
|
|
195
|
+
relay.srv.close();
|
|
196
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("mobile-sessions/purge 路由:POST → 企业端 POST /api/mobile-sessions/purge,removed 透传", async () => {
|
|
201
|
+
const relay = await startFakeRelay();
|
|
202
|
+
const { host, base, tempDir } = await bootRelay(relay.port);
|
|
203
|
+
try {
|
|
204
|
+
const r = await (await fetch(`${base}/dsh-remote/mobile-sessions/purge`, {
|
|
205
|
+
method: "POST",
|
|
206
|
+
headers: { "content-type": "application/json" },
|
|
207
|
+
body: JSON.stringify({}),
|
|
208
|
+
})).json();
|
|
209
|
+
assert.equal(r.ok, true);
|
|
210
|
+
assert.equal(r.removed, 2, "企业端返回的 removed 清理条数应透传");
|
|
211
|
+
const up = relay.seen.find((s) => s.path === "/api/mobile-sessions/purge");
|
|
212
|
+
assert.ok(up, "应转发到 /api/mobile-sessions/purge");
|
|
213
|
+
assert.equal(up.method, "POST");
|
|
214
|
+
assert.equal(up.authorization, "Bearer jwt-abc");
|
|
215
|
+
} finally {
|
|
216
|
+
host.close();
|
|
217
|
+
relay.srv.close();
|
|
218
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("未登录(无账号配置)→ 五条路由统一 401 提示登录,不请求企业端", async () => {
|
|
159
223
|
const relay = await startFakeRelay();
|
|
160
224
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-aks-401-"));
|
|
161
225
|
await writeFile(path.join(tempDir, ".dsh-config.json"), JSON.stringify({ api_url: `http://127.0.0.1:${relay.port}` }));
|
|
@@ -179,13 +243,19 @@ test("未登录(无账号配置)→ 三条路由统一 401 提示登录,
|
|
|
179
243
|
assert.equal((await (await fetch(`${base}${p}`, { method }))).status, 401, `${p} 应返回 401`);
|
|
180
244
|
assert.match(String(r.error), /尚未登录/);
|
|
181
245
|
}
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
246
|
+
for (const [method, p] of [
|
|
247
|
+
["POST", "/dsh-remote/mobile-sessions/revoke"],
|
|
248
|
+
["DELETE", "/dsh-remote/mobile-sessions/delete"],
|
|
249
|
+
["POST", "/dsh-remote/mobile-sessions/purge"],
|
|
250
|
+
]) {
|
|
251
|
+
const r = await (await fetch(`${base}${p}`, {
|
|
252
|
+
method,
|
|
253
|
+
headers: { "content-type": "application/json" },
|
|
254
|
+
body: JSON.stringify({ id: "ms_1" }),
|
|
255
|
+
})).json();
|
|
256
|
+
assert.equal(r.ok, false);
|
|
257
|
+
assert.match(String(r.error), /尚未登录/);
|
|
258
|
+
}
|
|
189
259
|
assert.ok(!relay.seen.some((s) => s.path.startsWith("/api/auth-key") || s.path.startsWith("/api/mobile-sessions")), "未登录不应请求企业端");
|
|
190
260
|
} finally {
|
|
191
261
|
host.close();
|
|
@@ -122,7 +122,7 @@ test("composeStatus:.e2ee-state.json 启用 → /dsh-remote/status 的 service
|
|
|
122
122
|
enabled: true, reason: "ok", profile: "pbkdf2-sha256-600k", epoch: 2, caps: ["e2ee-v2"],
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
// 2) bridge
|
|
125
|
+
// 2) bridge 未启用(服务端关闭/不支持)→ 原样上报 reason,供面板映射可读文案
|
|
126
126
|
await writeFile(stateFile, JSON.stringify({ enabled: false, reason: "server_disabled", profile: "", epoch: 0, caps: [] }));
|
|
127
127
|
status = await (await fetch(`${boot.base}/dsh-remote/status`)).json();
|
|
128
128
|
assert.equal(status.service.e2ee.enabled, false);
|
|
@@ -104,7 +104,8 @@ test("反馈配置:默认走 api_url(生产 relay-api 同源反馈端点)
|
|
|
104
104
|
assert.equal(body.ok, true);
|
|
105
105
|
assert.equal(body.reachable, true);
|
|
106
106
|
assert.equal(body.deviceId, "dev-cfgcheck");
|
|
107
|
-
|
|
107
|
+
// 隐私契约(2026-09):反馈配置不下发明文手机号,只给掩码(提交时服务端另附 x-dsh-phone 真号)
|
|
108
|
+
assert.equal(body.phone, "139****0000");
|
|
108
109
|
// 代理把 /dsh-remote/feedback/api/feedback/captcha 映射到 {api_url}/api/feedback/captcha
|
|
109
110
|
const cap = await fetch(`http://127.0.0.1:${host.address().port}/dsh-remote/feedback/api/feedback/captcha`);
|
|
110
111
|
assert.equal(cap.status, 201);
|