@mrrisega/dsh-remote 0.3.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/LICENSE +81 -0
- package/README.md +103 -0
- package/clients/dsh-remote/dsh-bridge.mjs +658 -0
- package/clients/dsh-remote/package.json +11 -0
- package/clients/dsh-remote/src/lifecycle.mjs +3 -0
- package/clients/dsh-remote/test/bridge-gzip.test.mjs +215 -0
- package/clients/dsh-remote/test/lifecycle.test.mjs +7 -0
- package/clients/dsh-remote/test/tunnel-auth.test.mjs +111 -0
- package/docs/self-hosting.md +109 -0
- package/dsh-setup.mjs +616 -0
- package/package.json +37 -0
- package/packages/dsh-remote-ui/README.md +81 -0
- package/packages/dsh-remote-ui/lib/client.js +1173 -0
- package/packages/dsh-remote-ui/lib/index.js +733 -0
- package/packages/dsh-remote-ui/package.json +26 -0
- package/packages/dsh-remote-ui/test/account-info.test.mjs +92 -0
- package/packages/dsh-remote-ui/test/account-switch.test.mjs +80 -0
- package/packages/dsh-remote-ui/test/feedback-proxy.test.mjs +154 -0
- package/packages/dsh-remote-ui/test/register-proxy.test.mjs +52 -0
- package/packages/dsh-remote-ui/test/settings-entry.test.mjs +282 -0
- package/packages/dsh-remote-ui/test/sms-captcha-ui.test.mjs +123 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-remote-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "dsh web 远程控制插件:设置页「远程控制」栏目(settings.section 官方扩展点,位于 Agent 预设下方)+ 配置面板 + bridge 启停 + 反馈/邀请/满意度(双半插件:node 半提供 /dsh-remote 宿主 API,浏览器半提供 UI)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": true,
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"default": "./lib/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./client": {
|
|
13
|
+
"default": "./lib/client.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"dsh": {
|
|
18
|
+
"client": {
|
|
19
|
+
"platform": "web",
|
|
20
|
+
"inject": [
|
|
21
|
+
"@deepseek-ai/dsh-client-runtime"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"license": "PolyForm-Noncommercial-1.0.0"
|
|
26
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// 插件 node 半「我的信息」代理回归:/dsh-remote/account|quota|invite-records
|
|
2
|
+
// 经假 relay(device-login→JWT) 与假 router(/_quota) 验证。
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import { apply } from "../lib/index.js";
|
|
10
|
+
|
|
11
|
+
/** 假 relay:device-login 发 token,带 token 的 /api/me 返回用户,邀请记录返回记录。 */
|
|
12
|
+
function startFakeRelay() {
|
|
13
|
+
const srv = http.createServer(async (req, res) => {
|
|
14
|
+
const url = new URL(req.url, "http://x");
|
|
15
|
+
const send = (code, obj) => { res.writeHead(code, { "content-type": "application/json" }); res.end(JSON.stringify(obj)); };
|
|
16
|
+
if (req.method === "POST" && url.pathname === "/api/device-login") {
|
|
17
|
+
return send(200, { token: "jwt-abc" });
|
|
18
|
+
}
|
|
19
|
+
if (url.pathname === "/api/me" && req.headers.authorization === "Bearer jwt-abc") {
|
|
20
|
+
return send(200, { user: { phone: "13800000000", plan: "pro", plan_source: "subscription", plan_ends_at: 1893456000000, invite_code: "ABC12345", invited_by: null } });
|
|
21
|
+
}
|
|
22
|
+
if (url.pathname === "/api/invite-records" && req.headers.authorization === "Bearer jwt-abc") {
|
|
23
|
+
return send(200, { records: [{ id: 1, invitee_phone: "13811112222" }], rewards: [{ id: 1, rule_n: 3, rule_days: 15 }] });
|
|
24
|
+
}
|
|
25
|
+
send(404, { error: { code: "not_found" } });
|
|
26
|
+
});
|
|
27
|
+
return new Promise((resolve) => srv.listen(0, "127.0.0.1", () => resolve({ srv, port: srv.address().port })));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 假 router:_quota 校验 dsh_token cookie。 */
|
|
31
|
+
function startFakeRouter() {
|
|
32
|
+
const srv = http.createServer((req, res) => {
|
|
33
|
+
if (req.url.startsWith("/_quota")) {
|
|
34
|
+
const cookie = req.headers.cookie || "";
|
|
35
|
+
if (!cookie.includes("jwt-abc")) { res.writeHead(401); return res.end(); }
|
|
36
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
37
|
+
return res.end(JSON.stringify({ quota: { plan: "pro", limit_enabled: false, used_bytes: 100, limit_bytes: 0, percent: 0 } }));
|
|
38
|
+
}
|
|
39
|
+
res.writeHead(404); res.end();
|
|
40
|
+
});
|
|
41
|
+
return new Promise((resolve) => srv.listen(0, "127.0.0.1", () => resolve({ srv, port: srv.address().port })));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test("我的信息:account/quota/invite-records 经假 relay 与假 router 正确代理", async () => {
|
|
45
|
+
const relay = await startFakeRelay();
|
|
46
|
+
const router = await startFakeRouter();
|
|
47
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-ui-acct-"));
|
|
48
|
+
await writeFile(path.join(tempDir, ".dsh-config.json"), JSON.stringify({
|
|
49
|
+
phone: "13800000000",
|
|
50
|
+
password: "pw",
|
|
51
|
+
device_id: "dev-testacct123",
|
|
52
|
+
api_url: `http://127.0.0.1:${relay.port}`,
|
|
53
|
+
tunnel_url: `ws://127.0.0.1:${router.port}`,
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
const routes = new Map();
|
|
57
|
+
apply({
|
|
58
|
+
webServer: { register(route) { routes.set(route.path, route.handler); return () => {}; } },
|
|
59
|
+
effect(register) { return register(); },
|
|
60
|
+
logger: { info() {}, warn() {} }
|
|
61
|
+
}, { relayDir: tempDir });
|
|
62
|
+
|
|
63
|
+
const host = http.createServer((req, res) => {
|
|
64
|
+
const url = new URL(req.url, "http://x");
|
|
65
|
+
const handler = routes.get(url.pathname);
|
|
66
|
+
(handler || ((_r, rs) => { rs.writeHead(404); rs.end(); }))(req, res);
|
|
67
|
+
});
|
|
68
|
+
await new Promise((resolve) => host.listen(0, "127.0.0.1", resolve));
|
|
69
|
+
const base = `http://127.0.0.1:${host.address().port}`;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const acct = await (await fetch(`${base}/dsh-remote/account`)).json();
|
|
73
|
+
assert.equal(acct.ok, true);
|
|
74
|
+
assert.equal(acct.account.plan, "pro");
|
|
75
|
+
assert.equal(acct.account.plan_ends_at, 1893456000000);
|
|
76
|
+
assert.equal(acct.account.invite_code, "ABC12345");
|
|
77
|
+
|
|
78
|
+
const quota = await (await fetch(`${base}/dsh-remote/quota`)).json();
|
|
79
|
+
assert.equal(quota.ok, true);
|
|
80
|
+
assert.equal(quota.quota.limit_enabled, false);
|
|
81
|
+
assert.equal(quota.quota.percent, 0);
|
|
82
|
+
|
|
83
|
+
const inv = await (await fetch(`${base}/dsh-remote/invite-records`)).json();
|
|
84
|
+
assert.equal(inv.records.length, 1);
|
|
85
|
+
assert.equal(inv.rewards[0].rule_days, 15);
|
|
86
|
+
} finally {
|
|
87
|
+
host.close();
|
|
88
|
+
relay.srv.close();
|
|
89
|
+
router.srv.close();
|
|
90
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import { chmod, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { apply } from "../lib/index.js";
|
|
8
|
+
|
|
9
|
+
test("切换账号时轮换设备身份并重启 bridge", async () => {
|
|
10
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-account-switch-"));
|
|
11
|
+
const fakeHome = path.join(tempDir, "home");
|
|
12
|
+
const fakeBin = path.join(tempDir, "bin");
|
|
13
|
+
const launchLog = path.join(tempDir, "launchctl.log");
|
|
14
|
+
await mkdir(fakeBin, { recursive: true });
|
|
15
|
+
await writeFile(path.join(fakeBin, "launchctl"), `#!/bin/sh
|
|
16
|
+
printf '%s\n' "$*" >> "$DSH_TEST_LAUNCH_LOG"
|
|
17
|
+
if [ "$1" = print ]; then printf 'state = running\npid = 4242\n'; fi
|
|
18
|
+
exit 0
|
|
19
|
+
`);
|
|
20
|
+
await chmod(path.join(fakeBin, "launchctl"), 0o755);
|
|
21
|
+
|
|
22
|
+
const relay = http.createServer((_req, res) => {
|
|
23
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
24
|
+
res.end(JSON.stringify({ app_url: "https://example.test/app/" }));
|
|
25
|
+
});
|
|
26
|
+
await new Promise((resolve) => relay.listen(0, "127.0.0.1", resolve));
|
|
27
|
+
await writeFile(path.join(tempDir, ".dsh-config.json"), JSON.stringify({
|
|
28
|
+
phone: "new-account",
|
|
29
|
+
email: "old-account",
|
|
30
|
+
password: "old-password",
|
|
31
|
+
device_id: "dev-old",
|
|
32
|
+
device_private_key: "private-old",
|
|
33
|
+
device_public_key: "public-old",
|
|
34
|
+
api_url: `http://127.0.0.1:${relay.address().port}`,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const oldEnv = { HOME: process.env.HOME, PATH: process.env.PATH, DSH_TEST_LAUNCH_LOG: process.env.DSH_TEST_LAUNCH_LOG };
|
|
38
|
+
process.env.HOME = fakeHome;
|
|
39
|
+
process.env.PATH = `${fakeBin}:${oldEnv.PATH}`;
|
|
40
|
+
process.env.DSH_TEST_LAUNCH_LOG = launchLog;
|
|
41
|
+
|
|
42
|
+
const routes = new Map();
|
|
43
|
+
apply({
|
|
44
|
+
webServer: { register(route) { routes.set(route.path, route.handler); return () => {}; } },
|
|
45
|
+
effect(register) { return register(); },
|
|
46
|
+
logger: { info() {}, warn() {} },
|
|
47
|
+
}, { relayDir: tempDir });
|
|
48
|
+
const host = http.createServer((req, res) => routes.get(new URL(req.url, "http://x").pathname)(req, res));
|
|
49
|
+
await new Promise((resolve) => host.listen(0, "127.0.0.1", resolve));
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const statusResponse = await fetch(`http://127.0.0.1:${host.address().port}/dsh-remote/status`);
|
|
53
|
+
const status = await statusResponse.json();
|
|
54
|
+
assert.equal(status.config.deviceId, "dev-old");
|
|
55
|
+
assert.equal("configPath" in status.config, false);
|
|
56
|
+
assert.equal(JSON.stringify(status).includes("old-password"), false);
|
|
57
|
+
|
|
58
|
+
const response = await fetch(`http://127.0.0.1:${host.address().port}/dsh-remote/config`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: { "content-type": "application/json" },
|
|
61
|
+
body: JSON.stringify({ phone: "new-account", password: "new-password" }),
|
|
62
|
+
});
|
|
63
|
+
assert.equal(response.status, 200);
|
|
64
|
+
const body = await response.json();
|
|
65
|
+
assert.equal(body.bridgeRestart?.ok, true);
|
|
66
|
+
|
|
67
|
+
const config = JSON.parse(await readFile(path.join(tempDir, ".dsh-config.json"), "utf8"));
|
|
68
|
+
assert.equal(config.phone, "new-account");
|
|
69
|
+
assert.equal(config.device_id, undefined);
|
|
70
|
+
assert.equal(config.device_private_key, undefined);
|
|
71
|
+
assert.equal(config.device_public_key, undefined);
|
|
72
|
+
assert.match(await readFile(launchLog, "utf8"), /bootstrap/);
|
|
73
|
+
} finally {
|
|
74
|
+
host.close();
|
|
75
|
+
relay.close();
|
|
76
|
+
Object.assign(process.env, oldEnv);
|
|
77
|
+
for (const [key, value] of Object.entries(oldEnv)) if (value === undefined) delete process.env[key];
|
|
78
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// 插件 node 半「用户反馈代理」回归:/dsh-remote/feedback/* 转发到独立反馈服务,
|
|
2
|
+
// 自动附加设备身份与手机号、透传 thread_token,且反馈服务不可达时优雅降级。
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import { apply } from "../lib/index.js";
|
|
10
|
+
|
|
11
|
+
/** 起一个假反馈服务(记录收到的头与体,回显 echo)。 */
|
|
12
|
+
function startFakeFeedback() {
|
|
13
|
+
const seen = [];
|
|
14
|
+
const srv = http.createServer(async (req, res) => {
|
|
15
|
+
let raw = "";
|
|
16
|
+
for await (const chunk of req) raw += chunk;
|
|
17
|
+
const rec = { method: req.method, url: req.url, headers: req.headers, body: raw ? JSON.parse(raw) : null };
|
|
18
|
+
seen.push(rec);
|
|
19
|
+
res.writeHead(201, { "content-type": "application/json" });
|
|
20
|
+
res.end(JSON.stringify({ ok: true, echo: rec }));
|
|
21
|
+
});
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
srv.listen(0, "127.0.0.1", () => resolve({ srv, seen, port: srv.address().port }));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test("反馈代理:附加设备身份/手机号,透传 thread_token", async () => {
|
|
28
|
+
const fb = await startFakeFeedback();
|
|
29
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-ui-fb-"));
|
|
30
|
+
await writeFile(path.join(tempDir, ".dsh-config.json"), JSON.stringify({
|
|
31
|
+
device_id: "dev-testproxy123",
|
|
32
|
+
phone: "13800000000",
|
|
33
|
+
feedback_url: `http://127.0.0.1:${fb.port}`,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
const routes = new Map();
|
|
37
|
+
apply({
|
|
38
|
+
webServer: { register(route) { routes.set(route.path, route.handler); return () => {}; } },
|
|
39
|
+
effect(register) { return register(); },
|
|
40
|
+
logger: { info() {}, warn() {} }
|
|
41
|
+
}, { relayDir: tempDir });
|
|
42
|
+
|
|
43
|
+
const host = http.createServer((req, res) => {
|
|
44
|
+
const url = new URL(req.url, "http://x");
|
|
45
|
+
const handler = routes.get(url.pathname) || routes.get("/dsh-remote/feedback");
|
|
46
|
+
(handler || ((_r, rs) => { rs.writeHead(404); rs.end(); }))(req, res);
|
|
47
|
+
});
|
|
48
|
+
await new Promise((resolve) => host.listen(0, "127.0.0.1", resolve));
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch(`http://127.0.0.1:${host.address().port}/dsh-remote/feedback/api/feedback`, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: { "content-type": "application/json", authorization: "Bearer thread-token-abc" },
|
|
54
|
+
body: JSON.stringify({ category: "bug", content: "测试" }),
|
|
55
|
+
});
|
|
56
|
+
assert.equal(res.status, 201);
|
|
57
|
+
const echo = (await res.json()).echo;
|
|
58
|
+
assert.equal(echo.url, "/api/feedback");
|
|
59
|
+
assert.equal(echo.headers["x-dsh-device"], "dev-testproxy123");
|
|
60
|
+
assert.equal(echo.headers["x-dsh-phone"], "13800000000");
|
|
61
|
+
assert.equal(echo.headers["x-dsh-client"], "dsh-remote-ui/0.1.0");
|
|
62
|
+
assert.equal(echo.headers.authorization, "Bearer thread-token-abc");
|
|
63
|
+
assert.equal(echo.body.category, "bug");
|
|
64
|
+
|
|
65
|
+
// GET 透传查询串
|
|
66
|
+
const getRes = await fetch(`http://127.0.0.1:${host.address().port}/dsh-remote/feedback/api/feedback/fb_abc?x=1`);
|
|
67
|
+
assert.equal(getRes.status, 201);
|
|
68
|
+
assert.equal(fb.seen[1].url, "/api/feedback/fb_abc?x=1");
|
|
69
|
+
} finally {
|
|
70
|
+
host.close();
|
|
71
|
+
fb.srv.close();
|
|
72
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("反馈配置:默认走 api_url(生产 relay-api 同源反馈端点),可达性正确", async () => {
|
|
77
|
+
const fb = await startFakeFeedback();
|
|
78
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-ui-fbcfg-"));
|
|
79
|
+
// 不写 feedback_url:应回退到 api_url(反馈端点与账号 API 同基址)
|
|
80
|
+
await writeFile(path.join(tempDir, ".dsh-config.json"), JSON.stringify({
|
|
81
|
+
device_id: "dev-cfgcheck",
|
|
82
|
+
phone: "13900000000",
|
|
83
|
+
api_url: `http://127.0.0.1:${fb.port}/relay-api`,
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
const routes = new Map();
|
|
87
|
+
apply({
|
|
88
|
+
webServer: { register(route) { routes.set(route.path, route.handler); return () => {}; } },
|
|
89
|
+
effect(register) { return register(); },
|
|
90
|
+
logger: { info() {}, warn() {} }
|
|
91
|
+
}, { relayDir: tempDir });
|
|
92
|
+
|
|
93
|
+
const host = http.createServer((req, res) => {
|
|
94
|
+
const url = new URL(req.url, "http://x");
|
|
95
|
+
const handler = routes.get(url.pathname) || routes.get("/dsh-remote/feedback");
|
|
96
|
+
(handler || ((_r, rs) => { rs.writeHead(404); rs.end(); }))(req, res);
|
|
97
|
+
});
|
|
98
|
+
await new Promise((resolve) => host.listen(0, "127.0.0.1", resolve));
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const res = await fetch(`http://127.0.0.1:${host.address().port}/dsh-remote/feedback-config`);
|
|
102
|
+
assert.equal(res.status, 200);
|
|
103
|
+
const body = await res.json();
|
|
104
|
+
assert.equal(body.ok, true);
|
|
105
|
+
assert.equal(body.reachable, true);
|
|
106
|
+
assert.equal(body.deviceId, "dev-cfgcheck");
|
|
107
|
+
assert.equal(body.phone, "13900000000");
|
|
108
|
+
// 代理把 /dsh-remote/feedback/api/feedback/captcha 映射到 {api_url}/api/feedback/captcha
|
|
109
|
+
const cap = await fetch(`http://127.0.0.1:${host.address().port}/dsh-remote/feedback/api/feedback/captcha`);
|
|
110
|
+
assert.equal(cap.status, 201);
|
|
111
|
+
assert.equal(fb.seen[1].url, "/relay-api/api/feedback/captcha");
|
|
112
|
+
} finally {
|
|
113
|
+
host.close();
|
|
114
|
+
fb.srv.close();
|
|
115
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("反馈代理:反馈服务不可达时 502 降级,不影响其他路由", async () => {
|
|
120
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-ui-fbdown-"));
|
|
121
|
+
await writeFile(path.join(tempDir, ".dsh-config.json"), JSON.stringify({
|
|
122
|
+
device_id: "dev-down",
|
|
123
|
+
feedback_url: "http://127.0.0.1:19998", // 死端口
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
const routes = new Map();
|
|
127
|
+
apply({
|
|
128
|
+
webServer: { register(route) { routes.set(route.path, route.handler); return () => {}; } },
|
|
129
|
+
effect(register) { return register(); },
|
|
130
|
+
logger: { info() {}, warn() {} }
|
|
131
|
+
}, { relayDir: tempDir });
|
|
132
|
+
|
|
133
|
+
const host = http.createServer((req, res) => {
|
|
134
|
+
const url = new URL(req.url, "http://x");
|
|
135
|
+
const handler = routes.get(url.pathname) || routes.get("/dsh-remote/feedback");
|
|
136
|
+
(handler || ((_r, rs) => { rs.writeHead(404); rs.end(); }))(req, res);
|
|
137
|
+
});
|
|
138
|
+
await new Promise((resolve) => host.listen(0, "127.0.0.1", resolve));
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const res = await fetch(`http://127.0.0.1:${host.address().port}/dsh-remote/feedback/api/feedback`, {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: { "content-type": "application/json" },
|
|
144
|
+
body: JSON.stringify({ category: "bug", content: "x" }),
|
|
145
|
+
});
|
|
146
|
+
assert.equal(res.status, 502);
|
|
147
|
+
const body = await res.json();
|
|
148
|
+
assert.equal(body.ok, false);
|
|
149
|
+
assert.match(body.error, /反馈服务不可达/);
|
|
150
|
+
} finally {
|
|
151
|
+
host.close();
|
|
152
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { apply } from "../lib/index.js";
|
|
8
|
+
|
|
9
|
+
test("注册代理向 relay 透传短信验证码", async () => {
|
|
10
|
+
let upstreamBody;
|
|
11
|
+
const relay = http.createServer(async (req, res) => {
|
|
12
|
+
let raw = "";
|
|
13
|
+
for await (const chunk of req) raw += chunk;
|
|
14
|
+
upstreamBody = JSON.parse(raw);
|
|
15
|
+
res.writeHead(201, { "content-type": "application/json" });
|
|
16
|
+
res.end(JSON.stringify({ token: "test-token", user: { phone: upstreamBody.phone } }));
|
|
17
|
+
});
|
|
18
|
+
await new Promise((resolve) => relay.listen(0, "127.0.0.1", resolve));
|
|
19
|
+
|
|
20
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-ui-test-"));
|
|
21
|
+
await writeFile(path.join(tempDir, ".dsh-config.json"), JSON.stringify({
|
|
22
|
+
api_url: `http://127.0.0.1:${relay.address().port}`
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const routes = new Map();
|
|
26
|
+
apply({
|
|
27
|
+
webServer: { register(route) { routes.set(route.path, route.handler); return () => {}; } },
|
|
28
|
+
effect(register) { return register(); },
|
|
29
|
+
logger: { info() {}, warn() {} }
|
|
30
|
+
}, { relayDir: tempDir });
|
|
31
|
+
|
|
32
|
+
const host = http.createServer((req, res) => routes.get(new URL(req.url, "http://x").pathname)(req, res));
|
|
33
|
+
await new Promise((resolve) => host.listen(0, "127.0.0.1", resolve));
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetch(`http://127.0.0.1:${host.address().port}/dsh-remote/register`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: { "content-type": "application/json" },
|
|
39
|
+
body: JSON.stringify({ phone: "13800000000", sms_code: "218937", password: "password123" })
|
|
40
|
+
});
|
|
41
|
+
assert.equal(response.status, 201);
|
|
42
|
+
assert.deepEqual(upstreamBody, {
|
|
43
|
+
phone: "13800000000",
|
|
44
|
+
sms_code: "218937",
|
|
45
|
+
password: "password123"
|
|
46
|
+
});
|
|
47
|
+
} finally {
|
|
48
|
+
host.close();
|
|
49
|
+
relay.close();
|
|
50
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
});
|