remote-dsh 0.4.9 → 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.
Files changed (2) hide show
  1. package/dist/bin.js +370 -197
  2. package/package.json +3 -3
package/dist/bin.js CHANGED
@@ -2,84 +2,58 @@
2
2
  /**
3
3
  * rdsh CLI 入口(手写参数解析,零依赖)。
4
4
  *
5
- * rdsh serve [--config <path>] [--reset] [--port <n>] ...
6
- * rdsh join <hub-url> [--token <t>] [--reset] [--dsh <path>]
5
+ * rdsh host setup lan|cloud
6
+ * rdsh host join <hub-url> [--token <t>] [--name <n>] [--dsh <p>] [--insecure]
7
+ * rdsh host serve | service install|status|uninstall | leave | user ...
7
8
  * rdsh hub serve|user|host|service ... [--config <path>]
8
- * rdsh user add|passwd <name> | ls | rm <name> [--config <path>]
9
- * rdsh service install|uninstall|status [--config <path>]
10
9
  * rdsh --version | --help
11
10
  */
12
11
  import { createInterface } from "node:readline";
13
- import { serve, join, UserManager, resolveConfigPath, installService, uninstallService, serviceStatus, } from "rdsh-gateway";
12
+ import { rm } from "node:fs/promises";
13
+ import { hostname as osHostname } from "node:os";
14
+ import { dirname } from "node:path";
15
+ import { serve, join, registerJoin, findDsh, selfRevoke, readPersistedToken, clearPersistedToken, UserManager, resolveConfigPath, loadConfig, saveConfig, installService, uninstallService, serviceStatus, HOST_SERVICE_NAME, JOIN_SERVICE_NAME, HUB_SERVICE_NAME, } from "rdsh-gateway";
14
16
  import { loadHubConfig, resolveHubConfigPath, HubDb, hashPassword, serveHub, } from "rdsh-hub";
15
17
  import { VERSION } from "./index.js";
16
18
  const HELP = `rdsh — secure remote access for DeepSeek Harness
17
19
 
18
20
  Usage:
19
- rdsh serve [options] Start the gateway (LAN auth or cloud HTTPS service)
20
- rdsh join <hub-url> Connect to a hub via outbound tunnel (no ports open)
21
- rdsh hub serve Start the hub server (cloud, multi-host)
22
- rdsh hub user add <name> Create a hub user (interactive password; --no-password for first-login setup)
23
- rdsh hub user passwd <n> Reset a user's password (admin)
24
- rdsh hub user rm|ls
25
- rdsh hub host ls|revoke List / revoke hosts (revoke = tunnel drops instantly)
26
- rdsh hub service ... Install hub as a systemd/launchd service
27
- rdsh user add|passwd|ls|rm Gateway users (LAN/cloud HTTPS service)
28
- rdsh service install|status|uninstall
21
+ rdsh host setup lan|cloud Configure this machine (LAN / cloud HTTPS gateway)
22
+ rdsh host join <hub-url> Connect this machine to a hub (outbound tunnel)
23
+ rdsh host serve Run the configured mode in the foreground
24
+ rdsh host service ... Run as a systemd/launchd service
25
+ rdsh host leave Unregister this machine from the hub
26
+ rdsh host user ... Manage gateway users (LAN/cloud password auth)
27
+ rdsh hub serve Start the hub server (cloud, multi-host)
28
+ rdsh hub user add|passwd|rm|ls|unlock|reset-2fa
29
+ rdsh hub audit ls Audit log (login/2FA/sharing/email events)
30
+ rdsh hub host ls|revoke List / revoke hosts (revoke drops the tunnel instantly)
31
+ rdsh hub service ... Run hub as a systemd/launchd service
29
32
 
30
33
  Global:
31
- --config <path> Gateway config (default ~/.rdsh/config.json; also $RDSH_CONFIG)
32
- For hub commands: hub config (default ~/.rdsh/hub.json; also $RDSH_HUB_CONFIG)
33
- --version, -v Print version
34
- --help, -h Print this help
35
-
36
- Options (serve):
37
- --port <n> Listen port (overrides config; default 8443)
38
- --host <ip> Bind address (overrides config; default 0.0.0.0)
39
- --pair-code <code> Preset pairing code (overrides config)
40
- --session-ttl <sec> Session cookie lifetime (overrides config; default 43200)
41
- --dsh <path> dsh executable path (overrides config)
42
- --reset Rotate session keys (all devices must re-auth)
43
- --no-code Disable auth (trusted network ONLY!)
44
-
45
- Options (join):
46
- --token <t> Use an existing host token (skip pairing-code flow)
47
- --reset Forget the persisted host token and re-pair
48
- --dsh <path> dsh executable path
49
-
50
- Options (hub serve):
51
- --port <n> Listen port (overrides hub config; default 8443)
52
- --host <ip> Bind address (overrides hub config; default 0.0.0.0)
53
-
54
- Config (persistent): host, port, sessionTtlSeconds, tls{cert,key}, behindProxy,
55
- allowFrom[], auth{mode: pair|password|none, pairCode, users[]}, dshPath.
56
- Hub config (~/.rdsh/hub.json): host, port, tls{cert,key}, dbPath, jwtKeyPath.
34
+ --config <path> Host config (default ~/.rdsh/host.json; also $RDSH_CONFIG)
35
+ For hub commands: hub config (default ~/.rdsh/hub.json; also $RDSH_HUB_CONFIG)
36
+ --version, -v Print version
37
+ --help, -h Print this help
57
38
  `;
58
39
  /** 子命令级用法(`rdsh <cmd> --help`)。 */
59
40
  const SUB_HELP = {
60
- serve: `Usage: rdsh serve [options]
41
+ host: `Usage: rdsh host <subcommand>
61
42
 
62
- Start the gateway (LAN auth or cloud HTTPS service).
43
+ Configure and run this machine (the DSH host).
63
44
 
64
- Options:
65
- --config <path> Config file (default ~/.rdsh/config.json; also $RDSH_CONFIG)
66
- --port <n> Listen port (overrides config; default 8443)
67
- --host <ip> Bind address (overrides config; default 0.0.0.0)
68
- --pair-code <code> Preset pairing code (overrides config)
69
- --session-ttl <sec> Session cookie lifetime (overrides config; default 43200)
70
- --dsh <path> dsh executable path (overrides config)
71
- --reset Rotate session keys (all devices must re-auth)
72
- --no-code Disable auth (trusted network ONLY!)
73
- `,
74
- join: `Usage: rdsh join <hub-url> [options]
75
-
76
- Connect to a hub via an outbound tunnel (no ports open on this machine).
45
+ Subcommands:
46
+ setup lan Configure a LAN gateway (pair auth, plain http)
47
+ setup cloud Configure a cloud HTTPS gateway (password + TLS + allowFrom)
48
+ join <hub-url> Connect to a hub (interactive token paste; --token for scripts)
49
+ serve Run the configured mode in the foreground
50
+ service install|status|uninstall Run as a systemd/launchd service
51
+ leave Unregister this machine from the hub
52
+ user add|passwd|ls|rm Manage gateway users
77
53
 
78
- Options:
79
- --token <t> Use an existing host token (skip the pair-code flow)
80
- --reset Forget the persisted host token and re-pair
81
- --dsh <path> dsh executable path
82
- --insecure Skip TLS certificate verification (self-signed hub)
54
+ Options (setup lan): --port <n> [--pair-code <code>]
55
+ Options (setup cloud): --tls-cert <path> --tls-key <path> [--port <n>] [--allow-from <cidr,...>]
56
+ Options (join): --token <t> --name <n> --dsh <path> --insecure
83
57
  `,
84
58
  hub: `Usage: rdsh hub <subcommand> [options]
85
59
 
@@ -87,7 +61,8 @@ Run the hub server or manage it.
87
61
 
88
62
  Subcommands:
89
63
  serve Start the hub server (TLS required)
90
- user add|passwd|rm|ls Manage users (registration closed; admin creates accounts)
64
+ user add|passwd|rm|ls|unlock|reset-2fa Manage users (admin creates accounts; unlock / reset-2fa)
65
+ audit ls [--user <n>] [--event <e>] [--since 24h|7d] Query the audit log
91
66
  host ls|revoke List / revoke hosts (revoke drops the tunnel instantly)
92
67
  service install|status|uninstall Run hub as a systemd/launchd service
93
68
 
@@ -95,31 +70,6 @@ Options (serve):
95
70
  --config <path> Hub config (default ~/.rdsh/hub.json; also $RDSH_HUB_CONFIG)
96
71
  --port <n> Listen port (overrides hub config; default 8443)
97
72
  --host <ip> Bind address (overrides hub config; default 0.0.0.0)
98
- `,
99
- user: `Usage: rdsh user <action> [name]
100
-
101
- Manage gateway users (LAN / cloud HTTPS service).
102
-
103
- Actions:
104
- add <name> Add a user (interactive password)
105
- passwd <name> Change a user's password (revokes all their sessions)
106
- ls List users
107
- rm <name> Remove a user
108
-
109
- Options:
110
- --config <path> Config file (default ~/.rdsh/config.json; also $RDSH_CONFIG)
111
- `,
112
- service: `Usage: rdsh service <action>
113
-
114
- Manage the gateway as a systemd/launchd service.
115
-
116
- Actions:
117
- install Install and start (auto-start on boot, restart on crash)
118
- status Show service status
119
- uninstall Stop and remove the service
120
-
121
- Options:
122
- --config <path> Config file (default ~/.rdsh/config.json; also $RDSH_CONFIG)
123
73
  `,
124
74
  };
125
75
  /** 参数里是否有 --help / -h。 */
@@ -151,28 +101,14 @@ async function main() {
151
101
  return;
152
102
  }
153
103
  switch (command) {
154
- case "serve": {
155
- const opts = parseServeArgs(rest, cli.configPath);
156
- await serve(opts);
157
- return;
158
- }
159
- case "join": {
160
- const opts = parseJoinArgs(rest);
161
- await join(opts);
104
+ case "host": {
105
+ await handleHost(rest, cli.configPath);
162
106
  return;
163
107
  }
164
108
  case "hub": {
165
109
  await handleHub(rest, cli.configPath);
166
110
  return;
167
111
  }
168
- case "user": {
169
- await handleUser(rest, cli.configPath);
170
- return;
171
- }
172
- case "service": {
173
- await handleService(rest, cli.configPath);
174
- return;
175
- }
176
112
  default:
177
113
  console.error(`rdsh: unknown command '${command}'. Run 'rdsh --help'.`);
178
114
  process.exitCode = 1;
@@ -198,59 +134,118 @@ function parseGlobal(argv) {
198
134
  flags.push(argv[i]);
199
135
  }
200
136
  }
201
- // 返回原始值(undefined 或显式 --config);gateway/hub 子命令分别按自己的默认路径 resolve
202
137
  return { configPath, flags };
203
138
  }
204
- function parseServeArgs(args, configPath) {
205
- const opts = { configPath };
206
- for (let i = 0; i < args.length; i++) {
207
- const flag = args[i];
208
- const value = (name) => {
209
- i += 1;
210
- if (i >= args.length)
211
- throw new Error(`missing value for ${name}`);
212
- return args[i] ?? "";
213
- };
214
- switch (flag) {
215
- case "--port": {
216
- const v = Number(value(flag));
217
- if (!Number.isInteger(v) || v < 0 || v > 65535)
218
- throw new Error(`invalid --port '${args[i]}'`);
219
- opts.port = v;
220
- break;
221
- }
222
- case "--host":
223
- opts.host = value(flag);
224
- break;
225
- case "--pair-code":
226
- opts.pairCode = value(flag);
227
- break;
228
- case "--session-ttl": {
229
- const v = Number(value(flag));
230
- if (!Number.isInteger(v) || v <= 0)
231
- throw new Error(`invalid --session-ttl '${args[i]}'`);
232
- opts.sessionTtlSeconds = v;
233
- break;
234
- }
235
- case "--dsh":
236
- opts.dshPath = value(flag);
237
- break;
238
- case "--reset":
239
- opts.reset = true;
240
- break;
241
- case "--no-code":
242
- opts.noCode = true;
243
- break;
244
- default:
139
+ function parsePort(v) {
140
+ const n = Number(v);
141
+ if (!Number.isInteger(n) || n < 0 || n > 65535)
142
+ throw new Error(`invalid port '${v}'`);
143
+ return n;
144
+ }
145
+ /** 服务 unit 的子进程 PATH:node 目录(nvm/自装)+ 系统路径,保证 spawn dsh 时 shebang 能解析 node。 */
146
+ function nodePathEnv() {
147
+ return `${dirname(process.execPath)}:/usr/local/bin:/usr/bin:/bin`;
148
+ }
149
+ async function handleHost(args, configPath) {
150
+ const sub = args[0];
151
+ const rest = args.slice(1);
152
+ if (sub === undefined || hasHelp(rest)) {
153
+ console.log(SUB_HELP["host"] ?? HELP);
154
+ return;
155
+ }
156
+ switch (sub) {
157
+ case "setup":
158
+ await handleHostSetup(rest, configPath);
159
+ return;
160
+ case "join":
161
+ await handleHostJoin(rest, configPath);
162
+ return;
163
+ case "serve":
164
+ await handleHostServe(rest, configPath);
165
+ return;
166
+ case "service":
167
+ await handleHostService(rest, configPath);
168
+ return;
169
+ case "leave":
170
+ await handleHostLeave(rest, configPath);
171
+ return;
172
+ case "user":
173
+ await handleHostUser(rest, configPath);
174
+ return;
175
+ default:
176
+ throw new Error("usage: rdsh host setup|join|serve|service|leave|user");
177
+ }
178
+ }
179
+ /** `rdsh host setup lan|cloud`:写 host.json(mode + 预设字段),配置完退出。 */
180
+ async function handleHostSetup(args, configPath) {
181
+ const which = args[0];
182
+ const target = resolveConfigPath(configPath);
183
+ const config = await loadConfig(target);
184
+ if (which === "lan") {
185
+ let port = config.port;
186
+ let pairCode = config.auth.pairCode;
187
+ for (let i = 1; i < args.length; i++) {
188
+ const flag = args[i];
189
+ if (flag === "--port")
190
+ port = parsePort(args[++i]);
191
+ else if (flag === "--pair-code")
192
+ pairCode = args[++i];
193
+ else
245
194
  throw new Error(`unknown option '${flag}'`);
246
195
  }
196
+ config.mode = "lan";
197
+ config.port = port;
198
+ config.tls = undefined;
199
+ config.behindProxy = false;
200
+ config.allowFrom = [];
201
+ config.auth.mode = "pair";
202
+ config.auth.pairCode = pairCode;
203
+ config.dshPath = config.dshPath ?? findDsh() ?? undefined;
204
+ await saveConfig(target, config);
205
+ console.log(`rdsh: host 配置为 LAN 网关(pair,端口 ${port})→ ${target}`);
206
+ console.log("rdsh: 运行 `rdsh host serve` 前台启动,或 `rdsh host service install` 常驻。");
207
+ return;
247
208
  }
248
- return opts;
209
+ if (which === "cloud") {
210
+ let port = config.port;
211
+ let cert;
212
+ let key;
213
+ let allowFrom = [];
214
+ for (let i = 1; i < args.length; i++) {
215
+ const flag = args[i];
216
+ if (flag === "--port")
217
+ port = parsePort(args[++i]);
218
+ else if (flag === "--tls-cert")
219
+ cert = args[++i];
220
+ else if (flag === "--tls-key")
221
+ key = args[++i];
222
+ else if (flag === "--allow-from")
223
+ allowFrom = (args[++i] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
224
+ else
225
+ throw new Error(`unknown option '${flag}'`);
226
+ }
227
+ if (cert === undefined || key === undefined) {
228
+ throw new Error("usage: rdsh host setup cloud --tls-cert <path> --tls-key <path> [--port <n>] [--allow-from <cidr,...>]");
229
+ }
230
+ config.mode = "cloud";
231
+ config.port = port;
232
+ config.tls = { cert, key };
233
+ config.behindProxy = false;
234
+ config.allowFrom = allowFrom;
235
+ config.auth.mode = "password";
236
+ config.dshPath = config.dshPath ?? findDsh() ?? undefined;
237
+ await saveConfig(target, config);
238
+ console.log(`rdsh: host 配置为云 HTTPS 网关(password,端口 ${port})→ ${target}`);
239
+ console.log("rdsh: 用 `rdsh host user add <name>` 建用户,再 `rdsh host serve` / `rdsh host service install`。");
240
+ return;
241
+ }
242
+ throw new Error("usage: rdsh host setup lan|cloud");
249
243
  }
250
- function parseJoinArgs(args) {
244
+ /** `rdsh host join <hub>`:注册(join token)→ 写 host.json + session,配置完退出。 */
245
+ async function handleHostJoin(args, configPath) {
251
246
  const hubUrl = args[0];
252
247
  if (hubUrl === undefined || !/^https?:\/\//.test(hubUrl)) {
253
- throw new Error("usage: rdsh join <hub-url> [--token <t>] [--dsh <path>]");
248
+ throw new Error("usage: rdsh host join <hub-url> [--token <t>] [--name <n>] [--dsh <p>] [--insecure]");
254
249
  }
255
250
  const opts = { hubUrl };
256
251
  for (let i = 1; i < args.length; i++) {
@@ -261,45 +256,153 @@ function parseJoinArgs(args) {
261
256
  throw new Error(`missing value for ${name}`);
262
257
  return args[i] ?? "";
263
258
  };
264
- switch (flag) {
265
- case "--token":
266
- opts.token = value(flag);
267
- break;
268
- case "--reset":
269
- opts.reset = true;
270
- break;
271
- case "--dsh":
272
- opts.dshPath = value(flag);
273
- break;
274
- case "--insecure":
275
- opts.insecure = true;
276
- break;
277
- default:
278
- throw new Error(`unknown option '${flag}'`);
259
+ if (flag === "--token")
260
+ opts.token = value(flag);
261
+ else if (flag === "--name")
262
+ opts.name = value(flag);
263
+ else if (flag === "--dsh")
264
+ opts.dshPath = value(flag);
265
+ else if (flag === "--insecure")
266
+ opts.insecure = true;
267
+ else if (flag === "--reset")
268
+ opts.reset = true;
269
+ else
270
+ throw new Error(`unknown option '${flag}'`);
271
+ }
272
+ if (opts.token === undefined && readPersistedToken(hubUrl) === null) {
273
+ // --token 且无持久化 session:TTY 交互粘贴 token;非 TTY 报错不 hang
274
+ if (process.stdin.isTTY) {
275
+ const pasted = (await promptLine("Paste your join token (hub portal → Add host): ")).trim();
276
+ if (pasted === "")
277
+ throw new Error("需要 join token(hub portal → 添加主机 生成)");
278
+ opts.token = pasted;
279
+ }
280
+ else {
281
+ throw new Error("无持久化 session 且未提供 --token;交互式运行粘贴 token");
279
282
  }
280
283
  }
281
- return opts;
284
+ const outcome = await registerJoin(opts);
285
+ const target = resolveConfigPath(configPath);
286
+ const config = await loadConfig(target);
287
+ config.mode = "join";
288
+ config.hub = hubUrl;
289
+ config.name = opts.name ?? osHostname();
290
+ config.insecure = outcome.insecure;
291
+ config.dshPath = opts.dshPath ?? findDsh() ?? config.dshPath;
292
+ await saveConfig(target, config);
293
+ console.log(`rdsh: host 已接入 ${hubUrl}(${config.name})—— session 已保存`);
294
+ console.log("rdsh: 运行 `rdsh host serve` 前台启动,或 `rdsh host service install` 常驻。");
295
+ }
296
+ /** `rdsh host serve`:前台常驻,读 host.json 按 mode 分发(join→隧道,lan/cloud→网关)。 */
297
+ async function handleHostServe(_args, configPath) {
298
+ const target = resolveConfigPath(configPath);
299
+ const config = await loadConfig(target);
300
+ if (config.mode === "join") {
301
+ if (config.hub === undefined)
302
+ throw new Error("host.json 缺 hub(join 模式);先 `rdsh host join <hub>`");
303
+ await join({ hubUrl: config.hub, name: config.name, insecure: config.insecure, dshPath: config.dshPath });
304
+ return;
305
+ }
306
+ await serve({ configPath: target });
282
307
  }
283
- async function handleUser(args, configPath) {
308
+ /** `rdsh host service install|status|uninstall`:读 host.json mode 装对应服务(unit 不含 token)。 */
309
+ async function handleHostService(args, configPath) {
310
+ const action = args[0];
311
+ const target = resolveConfigPath(configPath);
312
+ switch (action) {
313
+ case "install": {
314
+ await maybeRegisterForServiceInstall(args, target);
315
+ const config = await loadConfig(target);
316
+ const name = config.mode === "join" ? JOIN_SERVICE_NAME : HOST_SERVICE_NAME;
317
+ // host 服务会 spawn dsh(shebang `#!/usr/bin/env node`),nvm/自装 node 下需补 node 目录到 PATH(防 127)
318
+ console.log(await installService({ name, args: ["host", "serve"], configPath: target, pathEnv: nodePathEnv() }));
319
+ console.log(`rdsh: host service installed (${name}) —— 开机自启 + 崩溃重启。`);
320
+ return;
321
+ }
322
+ case "status": {
323
+ const config = await loadConfig(target);
324
+ const name = config.mode === "join" ? JOIN_SERVICE_NAME : HOST_SERVICE_NAME;
325
+ console.log(`rdsh host service (${name}): ${await serviceStatus(name)}`);
326
+ return;
327
+ }
328
+ case "uninstall": {
329
+ const config = await loadConfig(target);
330
+ const name = config.mode === "join" ? JOIN_SERVICE_NAME : HOST_SERVICE_NAME;
331
+ console.log(await uninstallService(name));
332
+ return;
333
+ }
334
+ default:
335
+ throw new Error("usage: rdsh host service install|status|uninstall");
336
+ }
337
+ }
338
+ /** 一行接入:`host service install <hub-url> --token <t> [--name <n>]` → 注册 + 写 host.json(再装服务)。 */
339
+ async function maybeRegisterForServiceInstall(args, target) {
340
+ const maybeHub = args[1];
341
+ if (maybeHub === undefined || !/^https?:\/\//.test(maybeHub))
342
+ return; // 无 <hub>,走读 host.json 路径
343
+ const joinOpts = { hubUrl: maybeHub };
344
+ for (let i = 2; i < args.length; i++) {
345
+ const flag = args[i];
346
+ if (flag === "--token")
347
+ joinOpts.token = args[++i];
348
+ else if (flag === "--name")
349
+ joinOpts.name = args[++i];
350
+ else if (flag === "--dsh")
351
+ joinOpts.dshPath = args[++i];
352
+ else if (flag === "--insecure")
353
+ joinOpts.insecure = true;
354
+ else
355
+ throw new Error(`unknown option '${flag}'`);
356
+ }
357
+ const outcome = await registerJoin(joinOpts);
358
+ const cfg = await loadConfig(target);
359
+ cfg.mode = "join";
360
+ cfg.hub = maybeHub;
361
+ cfg.name = joinOpts.name ?? osHostname();
362
+ cfg.insecure = outcome.insecure;
363
+ cfg.dshPath = joinOpts.dshPath ?? findDsh() ?? cfg.dshPath;
364
+ await saveConfig(target, cfg);
365
+ }
366
+ /** `rdsh host leave`:self-revoke + 清 session + 删 host.json → 未配置。 */
367
+ async function handleHostLeave(_args, configPath) {
368
+ const target = resolveConfigPath(configPath);
369
+ const config = await loadConfig(target);
370
+ if (config.mode !== "join" || config.hub === undefined) {
371
+ throw new Error("当前不是 join 模式(或缺 hub),无需 leave");
372
+ }
373
+ const token = readPersistedToken(config.hub);
374
+ if (token === null) {
375
+ console.log("rdsh: 本地无 session 文件,仅清理配置");
376
+ }
377
+ else {
378
+ await selfRevoke(config.hub, token, config.insecure === true);
379
+ console.log("rdsh: 已在 hub 自吊销本机");
380
+ }
381
+ clearPersistedToken(config.hub);
382
+ await rm(target, { force: true });
383
+ console.log(`rdsh: host 已注销(清理 ${target})—— 回到未配置状态`);
384
+ }
385
+ /** `rdsh host user add|passwd|ls|rm`:本机网关用户(写 host.json auth.users)。 */
386
+ async function handleHostUser(args, configPath) {
284
387
  const action = args[0];
285
388
  const um = new UserManager(resolveConfigPath(configPath));
286
389
  switch (action) {
287
390
  case "add": {
288
391
  const name = args[1];
289
392
  if (name === undefined)
290
- throw new Error("usage: rdsh user add <name>");
393
+ throw new Error("usage: rdsh host user add <name>");
291
394
  const password = await promptPassword(`password for ${name}: `);
292
395
  const again = await promptPassword("confirm: ");
293
396
  if (password !== again)
294
397
  throw new Error("passwords do not match");
295
398
  await um.add(name, password);
296
- console.log(`rdsh: user '${name}' added (config: ${configPath})`);
399
+ console.log(`rdsh: user '${name}' added`);
297
400
  return;
298
401
  }
299
402
  case "passwd": {
300
403
  const name = args[1];
301
404
  if (name === undefined)
302
- throw new Error("usage: rdsh user passwd <name>");
405
+ throw new Error("usage: rdsh host user passwd <name>");
303
406
  const password = await promptPassword(`new password for ${name}: `);
304
407
  const again = await promptPassword("confirm: ");
305
408
  if (password !== again)
@@ -318,7 +421,7 @@ async function handleUser(args, configPath) {
318
421
  case "rm": {
319
422
  const name = args[1];
320
423
  if (name === undefined)
321
- throw new Error("usage: rdsh user rm <name>");
424
+ throw new Error("usage: rdsh host user rm <name>");
322
425
  const ok = await um.remove(name);
323
426
  if (!ok)
324
427
  throw new Error(`user '${name}' not found`);
@@ -326,24 +429,7 @@ async function handleUser(args, configPath) {
326
429
  return;
327
430
  }
328
431
  default:
329
- throw new Error("usage: rdsh user add|passwd|ls|rm");
330
- }
331
- }
332
- async function handleService(args, configPath) {
333
- const action = args[0];
334
- switch (action) {
335
- case "install":
336
- console.log(await installService(resolveConfigPath(configPath)));
337
- console.log("rdsh: service installed — it starts on boot and restarts on crash.");
338
- return;
339
- case "status":
340
- console.log(`rdsh service: ${await serviceStatus()}`);
341
- return;
342
- case "uninstall":
343
- console.log(await uninstallService());
344
- return;
345
- default:
346
- throw new Error("usage: rdsh service install|status|uninstall");
432
+ throw new Error("usage: rdsh host user add|passwd|ls|rm");
347
433
  }
348
434
  }
349
435
  /** 打开 hub 数据库(读 hub config 定位 dbPath)。 */
@@ -352,6 +438,56 @@ async function openHubDb(configPath) {
352
438
  const config = await loadHubConfig(hubConfigPath);
353
439
  return new HubDb(config.dbPath);
354
440
  }
441
+ /** `rdsh hub audit ls [--user <name>] [--event <e>] [--since 24h|7d]`。 */
442
+ async function handleHubAudit(args, configPath) {
443
+ const db = await openHubDb(configPath);
444
+ try {
445
+ // 兼容 `audit ls` 与 `audit`(ls 为可选位置参数)
446
+ const opts = args[0] === "ls" ? args.slice(1) : args;
447
+ const filter = {};
448
+ for (let i = 0; i < opts.length; i++) {
449
+ const flag = opts[i];
450
+ if (flag === "--user") {
451
+ const name = opts[++i];
452
+ if (name === undefined)
453
+ throw new Error("missing value for --user");
454
+ const u = db.getUserByName(name);
455
+ if (u === null)
456
+ throw new Error(`user '${name}' not found`);
457
+ filter.userId = u.id;
458
+ }
459
+ else if (flag === "--event") {
460
+ filter.event = opts[++i];
461
+ if (filter.event === undefined)
462
+ throw new Error("missing value for --event");
463
+ }
464
+ else if (flag === "--since") {
465
+ const v = opts[++i];
466
+ if (v === undefined)
467
+ throw new Error("missing value for --since");
468
+ const m = /^(\d+)([hd])$/.exec(v);
469
+ if (m === null)
470
+ throw new Error(`invalid --since '${v}' (use like 24h or 7d)`);
471
+ filter.since = Date.now() - Number(m[1]) * (m[2] === "h" ? 3_600_000 : 86_400_000);
472
+ }
473
+ else {
474
+ throw new Error(`unknown option '${flag}'`);
475
+ }
476
+ }
477
+ const events = db.listAudit(filter);
478
+ if (events.length === 0) {
479
+ console.log("(no events)");
480
+ return;
481
+ }
482
+ for (const e of events) {
483
+ const user = e.userId !== null ? db.getUserById(e.userId) : null;
484
+ console.log(`${new Date(e.createdAt).toISOString()} ${user?.name ?? "-"} ${e.event} ip=${e.ip} ${e.detailJson}`);
485
+ }
486
+ }
487
+ finally {
488
+ db.close();
489
+ }
490
+ }
355
491
  async function handleHub(args, configPath) {
356
492
  const sub = args[0];
357
493
  const rest = args.slice(1);
@@ -373,26 +509,30 @@ async function handleHub(args, configPath) {
373
509
  await handleHubHost(rest, configPath);
374
510
  return;
375
511
  }
512
+ case "audit": {
513
+ await handleHubAudit(rest, configPath);
514
+ return;
515
+ }
376
516
  case "service": {
377
517
  const action = rest[0];
378
518
  const hubConfigPath = resolveHubConfigPath(configPath);
379
519
  switch (action) {
380
520
  case "install":
381
- console.log(await installService(hubConfigPath, ["hub", "serve"]));
382
- console.log("rdsh: hub service installed starts on boot and restarts on crash.");
521
+ console.log(await installService({ name: HUB_SERVICE_NAME, args: ["hub", "serve"], configPath: hubConfigPath }));
522
+ console.log(`rdsh: hub service installed (${HUB_SERVICE_NAME}) —— 开机自启 + 崩溃重启。`);
383
523
  return;
384
524
  case "status":
385
- console.log(`rdsh hub service: ${await serviceStatus()}`);
525
+ console.log(`rdsh hub service (${HUB_SERVICE_NAME}): ${await serviceStatus(HUB_SERVICE_NAME)}`);
386
526
  return;
387
527
  case "uninstall":
388
- console.log(await uninstallService());
528
+ console.log(await uninstallService(HUB_SERVICE_NAME));
389
529
  return;
390
530
  default:
391
531
  throw new Error("usage: rdsh hub service install|status|uninstall");
392
532
  }
393
533
  }
394
534
  default:
395
- throw new Error("usage: rdsh hub serve|user|host|service");
535
+ throw new Error("usage: rdsh hub serve|user|host|audit|service");
396
536
  }
397
537
  }
398
538
  function parseHubServeArgs(args, configPath) {
@@ -450,7 +590,6 @@ async function handleHubUser(args, configPath) {
450
590
  if (db.getUserByName(name) !== null)
451
591
  throw new Error(`user '${name}' already exists`);
452
592
  if (noPassword) {
453
- // 首次登录自设密码(must_change=1,密码占位不可用)
454
593
  db.createUser(name, "scrypt:disabled", new Date().toISOString(), true);
455
594
  console.log(`rdsh: user '${name}' created — they must set a password on first sign-in (${hubConfigPath})`);
456
595
  }
@@ -489,12 +628,35 @@ async function handleHubUser(args, configPath) {
489
628
  console.log(`rdsh: user '${name}' removed (their hosts removed)`);
490
629
  return;
491
630
  }
631
+ case "unlock": {
632
+ const name = args[1];
633
+ if (name === undefined)
634
+ throw new Error("usage: rdsh hub user unlock <name>");
635
+ const user = db.getUserByName(name);
636
+ if (user === null)
637
+ throw new Error(`user '${name}' not found`);
638
+ db.unlockAccount(user.id);
639
+ console.log(`rdsh: user '${name}' unlocked`);
640
+ return;
641
+ }
642
+ case "reset-2fa": {
643
+ const name = args[1];
644
+ if (name === undefined)
645
+ throw new Error("usage: rdsh hub user reset-2fa <name>");
646
+ const user = db.getUserByName(name);
647
+ if (user === null)
648
+ throw new Error(`user '${name}' not found`);
649
+ db.clearTotpSecret(user.id);
650
+ db.bumpVersion(user.id);
651
+ console.log(`rdsh: 2FA reset for '${name}' (their sessions revoked)`);
652
+ return;
653
+ }
492
654
  default:
493
- throw new Error("usage: rdsh hub user add|passwd|ls|rm");
655
+ throw new Error("usage: rdsh hub user add|passwd|ls|rm|unlock|reset-2fa");
494
656
  }
495
657
  }
496
658
  finally {
497
- db.close(); // 抛错路径也必须关闭 SQLite 句柄(否则进程挂住)
659
+ db.close();
498
660
  }
499
661
  }
500
662
  async function handleHubHost(args, configPath) {
@@ -562,11 +724,21 @@ function promptPassword(prompt) {
562
724
  });
563
725
  }
564
726
  // 非 TTY(管道/重定向):一次性读完 stdin(EOF),逐行消费。
565
- // 注意:不能每次调用新建 readline —— 第二个 interface 会读不到剩余行。
566
727
  process.stdout.write(prompt);
567
728
  return nextPipeLine();
568
729
  }
569
730
  let pipeLines = null;
731
+ /** 读取一行(echo,用于粘贴 token 等非敏感输入)。 */
732
+ function promptLine(prompt) {
733
+ process.stdout.write(prompt);
734
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
735
+ return new Promise((resolve) => {
736
+ rl.question("", (answer) => {
737
+ rl.close();
738
+ resolve(answer);
739
+ });
740
+ });
741
+ }
570
742
  function nextPipeLine() {
571
743
  if (pipeLines !== null) {
572
744
  return Promise.resolve(pipeLines.shift() ?? "");
@@ -578,6 +750,7 @@ function nextPipeLine() {
578
750
  rl.on("close", () => resolve(pipeLines.shift() ?? ""));
579
751
  });
580
752
  }
581
- // 管理命令(serve/join 常驻不 resolve)完成后显式退出:避免 TTY stdin / 句柄残留导致进程挂住
753
+ // 管理命令(host setup/join/leave/service/user 完成后)显式退出:
754
+ // 常驻命令(host serve / hub serve)不 resolve,靠信号退出。
582
755
  void main().then(() => process.exit(process.exitCode ?? 0));
583
756
  //# sourceMappingURL=bin.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-dsh",
3
- "version": "0.4.9",
3
+ "version": "0.6.0",
4
4
  "description": "Secure remote access for DeepSeek Harness: rdsh serve / rdsh join / rdsh hub",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,8 +23,8 @@
23
23
  "node": ">=22"
24
24
  },
25
25
  "dependencies": {
26
- "rdsh-gateway": "0.2.3",
27
- "rdsh-hub": "0.2.4"
26
+ "rdsh-gateway": "0.4.0",
27
+ "rdsh-hub": "0.4.0"
28
28
  },
29
29
  "scripts": {
30
30
  "build": "tsc -p tsconfig.json",