@mrrisega/dsh-remote 0.6.0-beta.12 → 0.6.0-beta.2
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 +3 -43
- package/clients/dsh-remote/dsh-bridge.mjs +5 -335
- package/package.json +2 -2
- package/packages/dsh-remote-web/lib/client.js +183 -607
- package/packages/dsh-remote-web/lib/index.js +10 -139
- package/packages/dsh-remote-web/test/access-key-sessions.test.mjs +11 -81
- package/packages/dsh-remote-web/test/account-switch.test.mjs +1 -1
- package/packages/dsh-remote-web/test/feedback-proxy.test.mjs +1 -2
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +3 -175
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +5 -56
- package/packages/dsh-remote-web/test/uninstall-runtime.test.mjs +2 -2
- package/clients/dsh-remote/e2ee-client.mjs +0 -728
- package/clients/dsh-remote/e2ee-shim-script.js +0 -737
- package/clients/dsh-remote/e2ee-shim.mjs +0 -163
- package/clients/dsh-remote/mobile-adapter.mjs +0 -595
- package/clients/dsh-remote/test/bridge-html-inject.test.mjs +0 -110
- package/clients/dsh-remote/test/e2ee-bridge.test.mjs +0 -630
- package/clients/dsh-remote/test/e2ee-client.test.mjs +0 -378
- package/clients/dsh-remote/test/e2ee-shim.test.mjs +0 -507
- package/clients/dsh-remote/test/e2ee-wecrypto-parity.test.mjs +0 -200
- package/clients/dsh-remote/test/lib-e2ee-shim.mjs +0 -59
- package/clients/dsh-remote/test/lib-native-e2ee.mjs +0 -92
- package/clients/dsh-remote/test/mobile-adapter.test.mjs +0 -124
- package/packages/dsh-remote-web/test/e2ee-state.test.mjs +0 -141
- package/packages/dsh-remote-web/test/feedback-account-mine.test.mjs +0 -70
- package/packages/dsh-remote-web/test/password-reset.test.mjs +0 -430
|
@@ -4,8 +4,7 @@
|
|
|
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
|
|
8
|
-
// DELETE …/:id、POST …/purge,Bearer)供面板「📱 远程访问」卡使用
|
|
7
|
+
// 另代理企业端一次性访问密钥 / 授权设备(/api/auth-key、/api/mobile-sessions、…/revoke,Bearer)供面板「📱 远程访问」卡使用
|
|
9
8
|
// - 自管理 self*(版本可见 / 新版检测 / 一键在线更新 / 彻底卸载):插件市场没有更新卸载按钮,
|
|
10
9
|
// 面板内即官方管理入口;更新=后台 npx 按 dist-tag(默认 latest,DSH_UPDATE_TAG 可切 beta/alpha)(幂等补齐运行环境并重启 bridge);
|
|
11
10
|
// 彻底卸载=profile 插件清理(uninstallSelf)+ 运行时清理(uninstallRuntime:停 bridge 自启动 /
|
|
@@ -17,7 +16,7 @@
|
|
|
17
16
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, realpathSync, accessSync, chmodSync, openSync, closeSync, rmSync, constants as fsConstants } from "node:fs";
|
|
18
17
|
import { join, dirname, sep } from "node:path";
|
|
19
18
|
import { execSync, spawn } from "node:child_process";
|
|
20
|
-
import { homedir, platform } from "node:os";
|
|
19
|
+
import { homedir, hostname, platform } from "node:os";
|
|
21
20
|
import { fileURLToPath } from "node:url";
|
|
22
21
|
|
|
23
22
|
/** 本插件在 host 侧的服务依赖。 */
|
|
@@ -653,7 +652,7 @@ async function relayAccount(relayDir) {
|
|
|
653
652
|
if (!r.ok || !r.body || !r.body.user) return null;
|
|
654
653
|
const u = r.body.user;
|
|
655
654
|
return {
|
|
656
|
-
phone:
|
|
655
|
+
phone: u.phone || "",
|
|
657
656
|
plan: u.plan || "free",
|
|
658
657
|
plan_source: u.plan_source || "plan",
|
|
659
658
|
plan_ends_at: u.plan_ends_at ?? null,
|
|
@@ -763,13 +762,13 @@ async function proxyCreateAccessKey(relayDir, res) {
|
|
|
763
762
|
}
|
|
764
763
|
|
|
765
764
|
/**
|
|
766
|
-
* GET /dsh-remote/mobile-sessions → 已授权设备列表(企业端
|
|
765
|
+
* GET /dsh-remote/mobile-sessions → 已授权设备列表(企业端 POST /api/mobile-sessions,Bearer)。
|
|
767
766
|
*/
|
|
768
767
|
async function proxyMobileSessions(relayDir, res) {
|
|
769
768
|
const token = await relayToken(relayDir).catch(() => "");
|
|
770
769
|
if (!token) return sendJson(res, 401, notLoggedInJson());
|
|
771
770
|
const r = await relayFetch(relayDir, "/api/mobile-sessions", {
|
|
772
|
-
method: "
|
|
771
|
+
method: "POST",
|
|
773
772
|
headers: { authorization: `Bearer ${token}` },
|
|
774
773
|
});
|
|
775
774
|
const d = flattenRelayBody(r);
|
|
@@ -803,87 +802,8 @@ async function proxyRevokeMobileSession(relayDir, req, res) {
|
|
|
803
802
|
return sendJson(res, 200, { ok: true, relayStatus: (r && r.status) || 200 });
|
|
804
803
|
}
|
|
805
804
|
|
|
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
|
-
|
|
851
805
|
// ---------- 综合状态 ----------
|
|
852
806
|
|
|
853
|
-
/**
|
|
854
|
-
* 读取 bridge 写入的 E2EE 开关状态(<relayDir>/.e2ee-state.json,写入方见
|
|
855
|
-
* clients/dsh-remote/e2ee-client.mjs 的 writeE2eeStateFile:{enabled, reason, profile, epoch, caps, at})。
|
|
856
|
-
* 归一化后只透出固定展示字段(enabled/reason/profile/epoch/caps),at 等调试字段不外泄;
|
|
857
|
-
* 文件缺失/损坏一律视为「未启用(明文回退)」——面板据此显示普通安全连接。
|
|
858
|
-
*/
|
|
859
|
-
export function readE2eeStateFile(relayDir) {
|
|
860
|
-
const none = () => ({ enabled: false, reason: "no_state_file", profile: "", epoch: 0, caps: [] });
|
|
861
|
-
try {
|
|
862
|
-
const f = join(relayDir, ".e2ee-state.json");
|
|
863
|
-
if (!existsSync(f)) return none();
|
|
864
|
-
const s = JSON.parse(readFileSync(f, "utf8"));
|
|
865
|
-
if (!s || typeof s !== "object") return none();
|
|
866
|
-
return {
|
|
867
|
-
enabled: s.enabled === true,
|
|
868
|
-
reason: typeof s.reason === "string" && s.reason ? s.reason : "",
|
|
869
|
-
profile: typeof s.profile === "string" ? s.profile : "",
|
|
870
|
-
epoch: Number.isInteger(s.epoch) && s.epoch >= 0 ? s.epoch : 0,
|
|
871
|
-
caps: Array.isArray(s.caps) ? s.caps.filter((c) => typeof c === "string") : [],
|
|
872
|
-
};
|
|
873
|
-
} catch {
|
|
874
|
-
return none();
|
|
875
|
-
}
|
|
876
|
-
}
|
|
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
|
-
|
|
887
807
|
async function composeStatus(relayDir) {
|
|
888
808
|
const cfg = loadConfig(relayDir);
|
|
889
809
|
const launchd = launchdStatus();
|
|
@@ -902,8 +822,7 @@ async function composeStatus(relayDir) {
|
|
|
902
822
|
return {
|
|
903
823
|
ok: true,
|
|
904
824
|
config: {
|
|
905
|
-
phone:
|
|
906
|
-
hasPhone: Boolean(cfg.phone),
|
|
825
|
+
phone: cfg.phone || "",
|
|
907
826
|
hasPassword: Boolean(cfg.password),
|
|
908
827
|
deviceId: cfg.device_id || "",
|
|
909
828
|
apiUrl,
|
|
@@ -919,11 +838,8 @@ async function composeStatus(relayDir) {
|
|
|
919
838
|
manual,
|
|
920
839
|
running: launchd.running || manual.bridge.length > 0,
|
|
921
840
|
bindError,
|
|
922
|
-
// E2EE 开关状态(bridge 写 .e2ee-state.json;文件缺失 = 未启用明文):
|
|
923
|
-
// {enabled, reason, profile, epoch, caps},供面板「📱 远程访问」卡展示加密状态。
|
|
924
|
-
e2ee: readE2eeStateFile(relayDir),
|
|
925
841
|
},
|
|
926
|
-
|
|
842
|
+
host: hostname(),
|
|
927
843
|
};
|
|
928
844
|
}
|
|
929
845
|
|
|
@@ -1030,7 +946,7 @@ const PLUGIN_ID = "dsh-remote-web";
|
|
|
1030
946
|
const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
|
|
1031
947
|
const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
|
|
1032
948
|
/** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
|
|
1033
|
-
const PLUGIN_VERSION = "0.6.0-beta.
|
|
949
|
+
const PLUGIN_VERSION = "0.6.0-beta.2";
|
|
1034
950
|
const UPDATE_LOG = ".dsh-update.log";
|
|
1035
951
|
const UPDATE_MARKER = ".dsh-update-running";
|
|
1036
952
|
|
|
@@ -1306,22 +1222,6 @@ function registerRoutes(ctx, relayDir) {
|
|
|
1306
1222
|
await proxyRevokeMobileSession(relayDir, req, res);
|
|
1307
1223
|
},
|
|
1308
1224
|
},
|
|
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
|
-
},
|
|
1325
1225
|
{
|
|
1326
1226
|
method: "POST",
|
|
1327
1227
|
path: "/dsh-remote/config",
|
|
@@ -1392,11 +1292,7 @@ function registerRoutes(ctx, relayDir) {
|
|
|
1392
1292
|
handler: async (req, res) => {
|
|
1393
1293
|
const body = await readJsonBody(req);
|
|
1394
1294
|
if (body.__parseError) return sendJson(res, 400, { ok: false, error: "JSON 解析失败" });
|
|
1395
|
-
|
|
1396
|
-
if (!phone) {
|
|
1397
|
-
// 隐私审计(2026-09):账号卡修改密码不再下发明文手机号——服务端以本机账号为准
|
|
1398
|
-
phone = String((loadConfig(relayDir).phone) || "").trim();
|
|
1399
|
-
}
|
|
1295
|
+
const phone = String(body.phone ?? "").trim();
|
|
1400
1296
|
if (!phone) return sendJson(res, 400, { ok: false, error: "手机号必填" });
|
|
1401
1297
|
const r = await relayFetch(relayDir, "/api/sms-code", {
|
|
1402
1298
|
method: "POST", headers: { "content-type": "application/json" },
|
|
@@ -1405,31 +1301,6 @@ function registerRoutes(ctx, relayDir) {
|
|
|
1405
1301
|
sendJson(res, r.status || 502, { ok: r.ok, status: r.status, body: r.body });
|
|
1406
1302
|
},
|
|
1407
1303
|
},
|
|
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
|
-
},
|
|
1433
1304
|
{
|
|
1434
1305
|
method: "GET",
|
|
1435
1306
|
path: "/dsh-remote/captcha",
|
|
@@ -1522,7 +1393,7 @@ function registerRoutes(ctx, relayDir) {
|
|
|
1522
1393
|
feedbackUrl: api,
|
|
1523
1394
|
reachable,
|
|
1524
1395
|
deviceId: cfg.device_id || "",
|
|
1525
|
-
phone:
|
|
1396
|
+
phone: cfg.phone || "",
|
|
1526
1397
|
// 登录态(已配置账号或自建密钥)→ 节点半自动附加 JWT,免图形验证码
|
|
1527
1398
|
auth: cfg.local_key || (cfg.phone && cfg.password) ? "account" : "anonymous"
|
|
1528
1399
|
});
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// 插件 node 半新增代理回归:/dsh-remote/access-key | mobile-sessions | mobile-sessions/revoke
|
|
2
|
-
//
|
|
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)
|
|
2
|
+
// (企业端 E1 契约:POST /api/auth-key、POST /api/mobile-sessions、POST /api/mobile-sessions/:id/revoke,Bearer device-login JWT)
|
|
5
3
|
// 覆盖:Bearer 透传、字段扁平化、未登录 401、qr 缺失容错、上游错误透传。
|
|
6
4
|
import assert from "node:assert/strict";
|
|
7
5
|
import http from "node:http";
|
|
@@ -32,7 +30,7 @@ function startFakeRelay(opts = {}) {
|
|
|
32
30
|
if (!opts.noQr) body.qr_data_url = QR;
|
|
33
31
|
return send(200, body);
|
|
34
32
|
}
|
|
35
|
-
if (
|
|
33
|
+
if (req.method === "POST" && url.pathname === "/api/mobile-sessions") {
|
|
36
34
|
return send(200, {
|
|
37
35
|
ok: true,
|
|
38
36
|
sessions: [
|
|
@@ -43,9 +41,6 @@ function startFakeRelay(opts = {}) {
|
|
|
43
41
|
}
|
|
44
42
|
if (req.method === "POST" && url.pathname === "/api/mobile-sessions/ms_1/revoke") return send(200, { ok: true });
|
|
45
43
|
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 });
|
|
49
44
|
send(404, { error: { code: "not_found" } });
|
|
50
45
|
});
|
|
51
46
|
return new Promise((resolve) => srv.listen(0, "127.0.0.1", () => resolve({ srv, seen, port: srv.address().port })));
|
|
@@ -122,7 +117,7 @@ test("mobile-sessions 路由:列表透传,Bearer 已带上", async () => {
|
|
|
122
117
|
assert.equal(r.sessions[0].label, "iPhone 15");
|
|
123
118
|
assert.equal(r.sessions[0].os, "iOS");
|
|
124
119
|
const up = relay.seen.find((s) => s.path === "/api/mobile-sessions");
|
|
125
|
-
assert.equal(up.method, "
|
|
120
|
+
assert.equal(up.method, "POST");
|
|
126
121
|
assert.equal(up.authorization, "Bearer jwt-abc");
|
|
127
122
|
} finally {
|
|
128
123
|
host.close();
|
|
@@ -160,66 +155,7 @@ test("mobile-sessions/revoke 路由:body {id} 转发到 /api/mobile-sessions/:
|
|
|
160
155
|
}
|
|
161
156
|
});
|
|
162
157
|
|
|
163
|
-
test("
|
|
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 () => {
|
|
158
|
+
test("未登录(无账号配置)→ 三条路由统一 401 提示登录,不请求企业端", async () => {
|
|
223
159
|
const relay = await startFakeRelay();
|
|
224
160
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-aks-401-"));
|
|
225
161
|
await writeFile(path.join(tempDir, ".dsh-config.json"), JSON.stringify({ api_url: `http://127.0.0.1:${relay.port}` }));
|
|
@@ -243,19 +179,13 @@ test("未登录(无账号配置)→ 五条路由统一 401 提示登录,
|
|
|
243
179
|
assert.equal((await (await fetch(`${base}${p}`, { method }))).status, 401, `${p} 应返回 401`);
|
|
244
180
|
assert.match(String(r.error), /尚未登录/);
|
|
245
181
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
}
|
|
182
|
+
const rev = await (await fetch(`${base}/dsh-remote/mobile-sessions/revoke`, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers: { "content-type": "application/json" },
|
|
185
|
+
body: JSON.stringify({ id: "ms_1" }),
|
|
186
|
+
})).json();
|
|
187
|
+
assert.equal(rev.ok, false);
|
|
188
|
+
assert.match(String(rev.error), /尚未登录/);
|
|
259
189
|
assert.ok(!relay.seen.some((s) => s.path.startsWith("/api/auth-key") || s.path.startsWith("/api/mobile-sessions")), "未登录不应请求企业端");
|
|
260
190
|
} finally {
|
|
261
191
|
host.close();
|
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import test from "node:test";
|
|
7
7
|
import { apply } from "../lib/index.js";
|
|
8
8
|
|
|
9
|
-
test("切换账号时轮换设备身份并重启 bridge",
|
|
9
|
+
test("切换账号时轮换设备身份并重启 bridge", async () => {
|
|
10
10
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-account-switch-"));
|
|
11
11
|
const fakeHome = path.join(tempDir, "home");
|
|
12
12
|
const fakeBin = path.join(tempDir, "bin");
|
|
@@ -104,8 +104,7 @@ 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
|
-
|
|
108
|
-
assert.equal(body.phone, "139****0000");
|
|
107
|
+
assert.equal(body.phone, "13900000000");
|
|
109
108
|
// 代理把 /dsh-remote/feedback/api/feedback/captcha 映射到 {api_url}/api/feedback/captcha
|
|
110
109
|
const cap = await fetch(`http://127.0.0.1:${host.address().port}/dsh-remote/feedback/api/feedback/captcha`);
|
|
111
110
|
assert.equal(cap.status, 201);
|
|
@@ -22,12 +22,6 @@ function find(tree, predicate) {
|
|
|
22
22
|
return match;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function findAll(tree, predicate) {
|
|
26
|
-
const out = [];
|
|
27
|
-
walk(tree, (node) => { if (predicate(node)) out.push(node); });
|
|
28
|
-
return out;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
25
|
/** 文本子节点包含子串(用于动态拼接文案断言)。 */
|
|
32
26
|
function textHas(tree, substr) {
|
|
33
27
|
return !!find(tree, (node) => (node.children || []).some((c) => typeof c === "string" && c.includes(substr)));
|
|
@@ -114,19 +108,11 @@ function loadPlugin(opts = {}) {
|
|
|
114
108
|
}
|
|
115
109
|
if (path === "/dsh-remote/mobile-sessions") {
|
|
116
110
|
sessionsCalls++;
|
|
117
|
-
|
|
118
|
-
const list = opts.sessions !== undefined ? opts.sessions : (sessionsCalls === 1 ? SESSIONS_1 : []);
|
|
119
|
-
return response(200, { ok: true, sessions: list });
|
|
111
|
+
return response(200, { ok: true, sessions: sessionsCalls === 1 ? SESSIONS_1 : [] }); // 取消配对后的刷新 → 空列表
|
|
120
112
|
}
|
|
121
113
|
if (path === "/dsh-remote/mobile-sessions/revoke") {
|
|
122
114
|
return response(200, { ok: true });
|
|
123
115
|
}
|
|
124
|
-
if (path === "/dsh-remote/mobile-sessions/delete") {
|
|
125
|
-
return response(200, { ok: true });
|
|
126
|
-
}
|
|
127
|
-
if (path === "/dsh-remote/mobile-sessions/purge") {
|
|
128
|
-
return response(200, { ok: true, removed: 1 });
|
|
129
|
-
}
|
|
130
116
|
if (path === "/dsh-remote/account") return response(200, { ok: true, account: { phone: "13800000000", plan: "free", plan_source: "plan", invite_code: "ABC12345" } });
|
|
131
117
|
return response(200, { ok: true });
|
|
132
118
|
},
|
|
@@ -186,7 +172,7 @@ test("📱 远程访问卡:bridge 在线 → 绿点文案;登录态点「生
|
|
|
186
172
|
assert.ok(qr && qr.props?.alt === "远程访问二维码", "应渲染服务端返回的二维码 <img>");
|
|
187
173
|
// 到期倒计时/有效至文案
|
|
188
174
|
assert.ok(textHas(tree, "有效至") && textHas(tree, "剩余"), "应显示「有效至 HH:MM:SS / 剩余 xx:xx」倒计时");
|
|
189
|
-
assert.ok(textHas(tree, "
|
|
175
|
+
assert.ok(textHas(tree, "访问一次后失效"), "应说明一次性/30 分钟语义");
|
|
190
176
|
});
|
|
191
177
|
|
|
192
178
|
test("已授权设备:展开列表(label/os/browser/时间)→ 取消配对二次确认 → 成功提示并刷新", async () => {
|
|
@@ -230,76 +216,6 @@ test("已授权设备:展开列表(label/os/browser/时间)→ 取消配
|
|
|
230
216
|
assert.ok(textHas(tree, "暂无已授权设备(手机扫码后出现)"), "刷新后空态文案应出现");
|
|
231
217
|
});
|
|
232
218
|
|
|
233
|
-
test("已授权设备行内操作:活跃行有 取消配对+删除记录,已取消行有 删除记录;删除记录二次确认 → DELETE delete {id}", async () => {
|
|
234
|
-
const plugin = loadPlugin({ sessions: SESSIONS_1 }); // 固定列表:操作后重拉仍保留,便于断言行内按钮
|
|
235
|
-
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
236
|
-
let tree = plugin.render();
|
|
237
|
-
const openBtn = find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("已授权设备")));
|
|
238
|
-
openBtn.props.onClick();
|
|
239
|
-
await flush();
|
|
240
|
-
await flush();
|
|
241
|
-
tree = plugin.render();
|
|
242
|
-
|
|
243
|
-
// 行内按钮:活跃行(ms_1)应有「取消配对」+「删除记录」;已取消行(ms_2)只应有「删除记录」
|
|
244
|
-
const delBtns = findAll(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("删除记录")));
|
|
245
|
-
assert.ok(delBtns.length >= 2, "每行(含已取消/历史)都应有「删除记录」按钮");
|
|
246
|
-
assert.ok(find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("取消配对"))),
|
|
247
|
-
"活跃行应有「取消配对」按钮");
|
|
248
|
-
// 底部有「清理已解绑」入口(卡片底部)
|
|
249
|
-
assert.ok(textHas(tree, "清理已解绑"), "卡片底部应提供「清理已解绑」");
|
|
250
|
-
|
|
251
|
-
// 删除 ms_1:第一次点击进入确认态(不请求)
|
|
252
|
-
delBtns[0].props.onClick();
|
|
253
|
-
tree = plugin.render();
|
|
254
|
-
assert.ok(textHas(tree, "再点一次确认删除记录"), "删除记录需二次确认");
|
|
255
|
-
assert.ok(!plugin.requests.some((r) => r.path === "/dsh-remote/mobile-sessions/delete"), "首次点击不应发 DELETE");
|
|
256
|
-
|
|
257
|
-
// 第二次点击 → DELETE /dsh-remote/mobile-sessions/delete {id} → 提示 + 刷新列表
|
|
258
|
-
const listBefore = plugin.requests.filter((r) => r.path === "/dsh-remote/mobile-sessions").length;
|
|
259
|
-
find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("再点一次确认删除记录"))).props.onClick();
|
|
260
|
-
await flush();
|
|
261
|
-
await flush();
|
|
262
|
-
await flush();
|
|
263
|
-
const del = plugin.requests.find((r) => r.path === "/dsh-remote/mobile-sessions/delete");
|
|
264
|
-
assert.ok(del, "确认后应 DELETE /dsh-remote/mobile-sessions/delete");
|
|
265
|
-
assert.equal(del.method, "DELETE");
|
|
266
|
-
assert.deepEqual(del.body, { id: "ms_1" }, "行内删除应带上该行 session id");
|
|
267
|
-
assert.ok(plugin.requests.filter((r) => r.path === "/dsh-remote/mobile-sessions").length > listBefore, "删除后应重拉设备列表");
|
|
268
|
-
tree = plugin.render();
|
|
269
|
-
assert.ok(textHas(tree, "已删除该设备的记录"), "删除成功应有可读提示");
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
test("已授权设备:卡片底部「清理已解绑」→ 二次确认 → POST purge → 提示条数 + 刷新列表", async () => {
|
|
273
|
-
const plugin = loadPlugin({ sessions: SESSIONS_1 });
|
|
274
|
-
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
275
|
-
let tree = plugin.render();
|
|
276
|
-
const openBtn = find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("已授权设备")));
|
|
277
|
-
openBtn.props.onClick();
|
|
278
|
-
await flush();
|
|
279
|
-
await flush();
|
|
280
|
-
tree = plugin.render();
|
|
281
|
-
|
|
282
|
-
// 底部 purge 按钮:带 1 条已解绑计数
|
|
283
|
-
const purgeBtn = find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("清理已解绑")));
|
|
284
|
-
assert.ok(purgeBtn, "卡片底部应有「清理已解绑」按钮");
|
|
285
|
-
purgeBtn.props.onClick();
|
|
286
|
-
tree = plugin.render();
|
|
287
|
-
assert.ok(textHas(tree, "再点一次确认清理已解绑"), "清理已解绑需二次确认");
|
|
288
|
-
assert.ok(!plugin.requests.some((r) => r.path === "/dsh-remote/mobile-sessions/purge"), "首次点击不应发请求");
|
|
289
|
-
|
|
290
|
-
const listBefore = plugin.requests.filter((r) => r.path === "/dsh-remote/mobile-sessions").length;
|
|
291
|
-
find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("再点一次确认清理已解绑"))).props.onClick();
|
|
292
|
-
await flush();
|
|
293
|
-
await flush();
|
|
294
|
-
await flush();
|
|
295
|
-
const purge = plugin.requests.find((r) => r.path === "/dsh-remote/mobile-sessions/purge");
|
|
296
|
-
assert.ok(purge, "确认后应 POST /dsh-remote/mobile-sessions/purge");
|
|
297
|
-
assert.equal(purge.method, "POST");
|
|
298
|
-
assert.ok(plugin.requests.filter((r) => r.path === "/dsh-remote/mobile-sessions").length > listBefore, "清理后应重拉设备列表");
|
|
299
|
-
tree = plugin.render();
|
|
300
|
-
assert.ok(textHas(tree, "已清理 1 条已解绑记录"), "应提示清理条数(可读)");
|
|
301
|
-
});
|
|
302
|
-
|
|
303
219
|
test("升级/续费按钮:点击 → GET /dsh-remote/access-key → window.open(url)(带登录态打开)", async () => {
|
|
304
220
|
const plugin = loadPlugin();
|
|
305
221
|
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
@@ -323,7 +239,7 @@ test("二维码缺失容错 + 未登录引导文案(源码级约束)", () =>
|
|
|
323
239
|
assert.match(SOURCE, /window\.open/);
|
|
324
240
|
// 自动刷新/状态轮询定时器与清理
|
|
325
241
|
assert.match(SOURCE, /KEY_AUTO_REFRESH_MS = 25000/);
|
|
326
|
-
assert.match(SOURCE, /STATUS_POLL_MS =
|
|
242
|
+
assert.match(SOURCE, /STATUS_POLL_MS = 5000/);
|
|
327
243
|
assert.match(SOURCE, /clearInterval\(rotateIv\)/);
|
|
328
244
|
assert.match(SOURCE, /clearInterval\(pollIv\)/);
|
|
329
245
|
// 文案与行为关键词
|
|
@@ -334,92 +250,4 @@ test("二维码缺失容错 + 未登录引导文案(源码级约束)", () =>
|
|
|
334
250
|
assert.match(SOURCE, /已连接(可远程访问)/);
|
|
335
251
|
assert.match(SOURCE, /带登录态/);
|
|
336
252
|
assert.match(SOURCE, /通过手机或另一台电脑远程使用同一份 dsh web/);
|
|
337
|
-
// 已授权设备管理(delete/purge 代理路由与按钮)
|
|
338
|
-
assert.match(SOURCE, /dsh-remote\/mobile-sessions\/delete/);
|
|
339
|
-
assert.match(SOURCE, /dsh-remote\/mobile-sessions\/purge/);
|
|
340
|
-
assert.match(SOURCE, /删除记录/);
|
|
341
|
-
assert.match(SOURCE, /清理已解绑/);
|
|
342
|
-
assert.match(SOURCE, /再点一次确认删除记录/);
|
|
343
|
-
assert.match(SOURCE, /再点一次确认清理已解绑/);
|
|
344
|
-
// 文案精简:二维码说明压成一句、右侧长段压成一句
|
|
345
|
-
assert.match(SOURCE, /扫码即进入,30 分钟有效、用一次即失效。/);
|
|
346
|
-
assert.match(SOURCE, /打开链接\/扫码进入即登录态;同设备重复扫码只更新授权,不新增设备。/);
|
|
347
|
-
assert.doesNotMatch(SOURCE, /每次生成的链接 30 分钟有效、访问一次后失效/);
|
|
348
|
-
assert.doesNotMatch(SOURCE, /手机上打开链接点「进入」即可像在本机一样使用 dsh web/);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
// ---------- 端到端加密(E2EE,Phase-5)状态徽标 ----------
|
|
352
|
-
|
|
353
|
-
const E2EE_STATE = {
|
|
354
|
-
enabled: { enabled: true, reason: "ok", profile: "pbkdf2-sha256-600k", epoch: 1, caps: ["e2ee-v2"] },
|
|
355
|
-
serverDisabled: { enabled: false, reason: "server_disabled", profile: "", epoch: 0, caps: [] },
|
|
356
|
-
paramsUnreachable: { enabled: false, reason: "params_unreachable", profile: "", epoch: 0, caps: [] },
|
|
357
|
-
localDisabled: { enabled: false, reason: "disabled_by_config", profile: "", epoch: 0, caps: [] },
|
|
358
|
-
deriveFailed: { enabled: false, reason: "derive_failed", profile: "", epoch: 0, caps: [] },
|
|
359
|
-
unknownReason: { enabled: false, reason: "brand_new_reason_x", profile: "", epoch: 0, caps: [] },
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
function stateWith(service) {
|
|
363
|
-
return { config: { phone: "13800000000", deviceId: "dev-x", mode: "saas" }, service, remoteUrl: "https://app.test/" };
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function e2eeLine(tree) {
|
|
367
|
-
return find(tree, (n) => n.props && typeof n.props.className === "string" && n.props.className.indexOf("dru-e2ee-line") === 0);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
test("E2EE 徽标:已启用 → “🔒 端到端加密已启用(手机解锁后生效)”(service.e2ee 字段驱动)", () => {
|
|
371
|
-
const plugin = loadPlugin();
|
|
372
|
-
plugin.states[0] = stateWith({ running: true, e2ee: E2EE_STATE.enabled });
|
|
373
|
-
const tree = plugin.render();
|
|
374
|
-
const line = e2eeLine(tree);
|
|
375
|
-
assert.ok(line, "启用态应在「📱 远程访问」卡渲染加密状态行");
|
|
376
|
-
assert.match(String(line.props.className), /ok/, "启用态状态行应为绿色(ok)标记");
|
|
377
|
-
assert.ok(textHas(tree, "🔒 端到端加密已启用"), "应显示“端到端加密已启用”徽标文案");
|
|
378
|
-
assert.ok(textHas(tree, "手机解锁后生效"), "应提示“手机解锁后生效”(bridge 就绪、手机解锁后方生效)");
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
test("E2EE 徽标:未启用原因映射可读文案(服务端关闭 / 参数不可达 / 本地关闭 / 改密 / 未知兜底)", () => {
|
|
382
|
-
const cases = [
|
|
383
|
-
[E2EE_STATE.serverDisabled, "端到端加密暂不可用(当前为普通安全连接 HTTPS)"],
|
|
384
|
-
[E2EE_STATE.paramsUnreachable, "当前为普通安全连接(HTTPS)"],
|
|
385
|
-
[E2EE_STATE.localDisabled, "当前为普通安全连接(HTTPS)"],
|
|
386
|
-
[E2EE_STATE.deriveFailed, "账号密码已变更"],
|
|
387
|
-
[E2EE_STATE.unknownReason, "当前为普通安全连接(HTTPS)"], // 未知 reason → 兜底
|
|
388
|
-
];
|
|
389
|
-
for (const [e2ee, text] of cases) {
|
|
390
|
-
const plugin = loadPlugin();
|
|
391
|
-
plugin.states[0] = stateWith({ running: true, e2ee });
|
|
392
|
-
const tree = plugin.render();
|
|
393
|
-
assert.ok(textHas(tree, text), `reason=${e2ee.reason} 应映射为可读文案: ${text}`);
|
|
394
|
-
const line = e2eeLine(tree);
|
|
395
|
-
assert.ok(line && !/ ok/.test(String(line.props.className)), `reason=${e2ee.reason} 非启用态不应带 ok 标记`);
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
test("E2EE 徽标:未登录 / host 未下发 e2ee → 不打扰(不渲染状态行)", () => {
|
|
400
|
-
// 未登录(config 无 phone)即便 bridge 上报 enabled 也不打扰
|
|
401
|
-
const anon = loadPlugin();
|
|
402
|
-
anon.states[0] = { config: { deviceId: "dev-x", mode: "saas" }, service: { running: true, e2ee: E2EE_STATE.enabled } };
|
|
403
|
-
assert.equal(e2eeLine(anon.render()), undefined, "未登录不应渲染 E2EE 状态行");
|
|
404
|
-
assert.ok(!textHas(anon.render(), "手机解锁后生效"), "未登录不应出现加密徽标文案");
|
|
405
|
-
|
|
406
|
-
// 已登录但 service 无 e2ee(旧 host / 未下发)→ 不渲染
|
|
407
|
-
const oldHost = loadPlugin();
|
|
408
|
-
oldHost.states[0] = stateWith({ running: true });
|
|
409
|
-
assert.equal(e2eeLine(oldHost.render()), undefined, "未下发 service.e2ee 时不应渲染状态行");
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
test("E2EE 徽标(源码级约束):client 含徽标字段/文案与 reason 映射表", () => {
|
|
413
|
-
assert.match(SOURCE, /service\.e2ee/);
|
|
414
|
-
assert.match(SOURCE, /\.e2ee-state\.json/);
|
|
415
|
-
assert.match(SOURCE, /🔒 端到端加密已启用(手机解锁后生效)/);
|
|
416
|
-
assert.match(SOURCE, /端到端加密暂不可用(当前为普通安全连接 HTTPS)/);
|
|
417
|
-
assert.doesNotMatch(SOURCE, /等待服务端开启 E2EE/); // 已正式开启,不再出现“灰度等待”措辞
|
|
418
|
-
assert.match(SOURCE, /当前为普通安全连接(HTTPS)/);
|
|
419
|
-
assert.match(SOURCE, /server_disabled/);
|
|
420
|
-
assert.match(SOURCE, /params_unreachable/);
|
|
421
|
-
assert.match(SOURCE, /disabled_by_config/);
|
|
422
|
-
assert.match(SOURCE, /derive_failed/);
|
|
423
|
-
assert.match(SOURCE, /describeE2ee/);
|
|
424
|
-
assert.match(SOURCE, /dru-e2ee-line/);
|
|
425
253
|
});
|