dsh-vpn 0.1.8 → 0.1.10

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 CHANGED
@@ -45,7 +45,7 @@ dsh plugin --profile web add dsh-vpn
45
45
 
46
46
  放好后点「保存」和「测试连接」。已经有自己的私钥、服务器上也放好了对应公钥的,展开「改用我自己已有的私钥」粘贴进去即可(支持带密码短语的私钥)。用非 root 用户时,「sudo 密码」一栏填该用户的 sudo 密码;root 或免密 sudo 不用填。
47
47
 
48
- **找回已有节点**:如果重装过 dsh 或 dsh-vpn,本地保存的设置会丢失,但 VPS 上的节点还在。重新填写服务器 IP、端口、用户名和密码后点「保存」,插件会自动检查这台服务器上是否已有安装(也可以随时点「检查已有安装」)。找到后直接显示原来的节点二维码,并按服务器上的实际情况勾选协议和证书类型,已经导入手机或电脑客户端的配置可以继续使用,不用重新扫码。
48
+ **找回已有节点**:重装过 dsh 或 dsh-vpn 后,本地保存的设置会丢失,但 VPS 上的节点还在运行。在 VPN 设置里填好服务器 IP、端口、用户名和密码(或选密钥登录),**不用先保存**,直接点登录方式下面的「检查已有安装」:插件登录服务器读取仍在运行的节点,显示原来的二维码(链接和以前完全一样,已经导入手机或电脑客户端的配置继续可用),按服务器上的实际情况勾选协议和证书类型,并自动保存设置。登录失败时什么都不保存,填的内容也不会被清掉。
49
49
 
50
50
  页面第一行显示插件版本,有新版本时旁边会提示,点「更新」即可升级插件,重启 dsh 后生效(需要 dsh 0.2 及以上;更早的版本请在「设置 → 插件」中更新)。为了供应链安全,发布不足 24 小时的新版本会等满 24 小时再提供更新。
51
51
 
package/lib/client.js CHANGED
@@ -57,7 +57,7 @@ var vpnApi = {
57
57
  status: () => post("/status"),
58
58
  links: () => post("/links"),
59
59
  /** Find an earlier installation on the server and adopt its protocols and certificate mode. */
60
- detect: () => post("/detect"),
60
+ detect: (form) => post("/detect", form ?? {}),
61
61
  install: (regenerate) => post("/install", { regenerate }),
62
62
  uninstall: () => post("/uninstall"),
63
63
  /** Installed version; with `check`, also the newest release on npm. */
@@ -157,6 +157,8 @@ var zh = {
157
157
  qrSection: "\u8282\u70B9\u4E8C\u7EF4\u7801",
158
158
  qrHint: "\u7528 v2rayN\u3001NekoBox\u3001Shadowrocket\u3001sing-box\u3001Clash Meta \u7B49\u5BA2\u6237\u7AEF\u626B\u63CF\u5BFC\u5165\u3002\u4E8C\u7EF4\u7801\u7B49\u540C\u4E8E\u5BC6\u7801\uFF0C\u8BF7\u52FF\u5916\u4F20\u3002",
159
159
  detect: "\u68C0\u67E5\u5DF2\u6709\u5B89\u88C5",
160
+ detecting: "\u6B63\u5728\u8BFB\u53D6\u670D\u52A1\u5668\u2026",
161
+ detectHint: "\u91CD\u88C5\u8FC7 dsh \u6216\u672C\u63D2\u4EF6\uFF1F\u586B\u597D\u4E0A\u9762\u7684\u767B\u5F55\u4FE1\u606F\u76F4\u63A5\u70B9\u8FD9\u91CC\uFF08\u4E0D\u7528\u5148\u4FDD\u5B58\uFF09\uFF0C\u4F1A\u767B\u5F55\u670D\u52A1\u5668\u8BFB\u53D6\u4ECD\u5728\u8FD0\u884C\u7684\u8282\u70B9\uFF0C\u663E\u793A\u539F\u6765\u7684\u4E8C\u7EF4\u7801\uFF0C\u5E76\u81EA\u52A8\u4FDD\u5B58\u8BBE\u7F6E\u3002",
160
162
  detectFound: "\u5DF2\u627E\u5230\u4E4B\u524D\u7684\u5B89\u88C5\uFF1Asing-box {version}\uFF0C\u534F\u8BAE\uFF1A{protocols}\uFF08{cert}\uFF09\u3002\u8282\u70B9\u4E8C\u7EF4\u7801\u5DF2\u663E\u793A\u5728\u4E0B\u65B9\uFF0C\u539F\u6709\u7684\u5BA2\u6237\u7AEF\u914D\u7F6E\u53EF\u4EE5\u7EE7\u7EED\u4F7F\u7528\u3002",
161
163
  detectSynced: "\u5DF2\u6309\u670D\u52A1\u5668\u4E0A\u7684\u5B9E\u9645\u60C5\u51B5\u66F4\u65B0\u4E86\u534F\u8BAE\u52FE\u9009\u548C\u8BC1\u4E66\u7C7B\u578B\u3002",
162
164
  detectNone: "\u8FD9\u53F0\u670D\u52A1\u5668\u4E0A\u6CA1\u6709\u627E\u5230\u4E4B\u524D\u7684\u5B89\u88C5\uFF0C\u53EF\u4EE5\u76F4\u63A5\u70B9\u300C\u5B89\u88C5 / \u66F4\u65B0\u300D\u3002",
@@ -286,6 +288,8 @@ var en = {
286
288
  qrSection: "Node QR codes",
287
289
  qrHint: "Scan with v2rayN, NekoBox, Shadowrocket, sing-box, Clash Meta and similar apps. A QR code is as secret as a password.",
288
290
  detect: "Find existing install",
291
+ detecting: "Reading the server\u2026",
292
+ detectHint: "Reinstalled dsh or this plugin? Fill in the login details above and click here (no need to save first): it logs in, reads the node that is still running, shows its QR codes and saves the settings.",
289
293
  detectFound: "Found an earlier installation: sing-box {version}, protocols: {protocols} ({cert}). Its QR codes are shown below; clients you already set up keep working.",
290
294
  detectSynced: "The protocol selection and certificate type now match the server.",
291
295
  detectNone: "No earlier installation was found on this server. Click Install / Update to set one up.",
@@ -379,6 +383,8 @@ var STYLES = `
379
383
  .dshvpn-disclosure { align-self: flex-start; border: none; background: transparent; color: inherit; padding: 0; cursor: pointer; font-size: 12.5px; opacity: .75; }
380
384
  .dshvpn-textarea { width: 100%; box-sizing: border-box; font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; padding: 8px; border-radius: 8px; resize: vertical;
381
385
  color: inherit; background: transparent; border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.3)); }
386
+ .dshvpn-detect { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; padding-top: 4px; }
387
+ .dshvpn-detect small { flex: 1; min-width: 220px; }
382
388
  .dshvpn-section { display: flex; flex-direction: column; gap: 16px; }
383
389
  .dshvpn-section-head h2 { margin: 0; font-size: 18px; font-weight: 600; }
384
390
  .dshvpn-title-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 4px; }
@@ -1268,6 +1274,7 @@ function VpnSettingsPage({ view, t, api }) {
1268
1274
  const [form, setForm] = (0, import_react.useState)(null);
1269
1275
  const [busy, setBusy] = (0, import_react.useState)(null);
1270
1276
  const [notice, setNotice] = (0, import_react.useState)(null);
1277
+ const [noticeFrom, setNoticeFrom] = (0, import_react.useState)(null);
1271
1278
  const [links, setLinks] = (0, import_react.useState)(null);
1272
1279
  const [refreshedAt, setRefreshedAt] = (0, import_react.useState)(null);
1273
1280
  const [regenerate, setRegenerate] = (0, import_react.useState)(false);
@@ -1297,6 +1304,7 @@ function VpnSettingsPage({ view, t, api }) {
1297
1304
  async function op(name, call, onOk) {
1298
1305
  setBusy(name);
1299
1306
  setNotice(null);
1307
+ setNoticeFrom(name);
1300
1308
  try {
1301
1309
  const r = await call();
1302
1310
  setNotice(r.ok ? onOk(r) : { tone: "error", text: errorText(t, r) });
@@ -1305,12 +1313,12 @@ function VpnSettingsPage({ view, t, api }) {
1305
1313
  setBusy(null);
1306
1314
  }
1307
1315
  }
1308
- const detect = async () => {
1309
- await op("detect", api.detect, (r) => {
1310
- const res = r.result ?? {};
1316
+ const detect = async (fromForm = true) => {
1317
+ const r = await op("detect", () => api.detect(fromForm ? form : void 0), (r2) => {
1318
+ const res = r2.result ?? {};
1311
1319
  if (res.installed !== true) return { tone: "info", text: t("detectNone") };
1312
- if (r.links) {
1313
- setLinks(r.links);
1320
+ if (r2.links) {
1321
+ setLinks(r2.links);
1314
1322
  setRefreshedAt(/* @__PURE__ */ new Date());
1315
1323
  }
1316
1324
  const protocols = (res.protocols ?? []).map((id) => PROTOCOLS.find((p) => p.id === id)?.name ?? id).join("\u3001");
@@ -1319,10 +1327,12 @@ function VpnSettingsPage({ view, t, api }) {
1319
1327
  return { tone: "success", text: res.synced === true ? `${text}
1320
1328
  ${t("detectSynced")}` : text };
1321
1329
  });
1322
- const s = await api.getSettings();
1323
- if (s.ok) {
1324
- setSaved(s.settings);
1325
- setForm(formOf(s.settings));
1330
+ if (r.ok || r.links) {
1331
+ const s = await api.getSettings();
1332
+ if (s.ok) {
1333
+ setSaved(s.settings);
1334
+ setForm(formOf(s.settings));
1335
+ }
1326
1336
  }
1327
1337
  };
1328
1338
  const save = async () => {
@@ -1332,7 +1342,7 @@ ${t("detectSynced")}` : text };
1332
1342
  setForm(formOf(r2.settings));
1333
1343
  return { tone: "success", text: t("saved") };
1334
1344
  });
1335
- if (r.ok && first) await detect();
1345
+ if (r.ok && first) await detect(false);
1336
1346
  };
1337
1347
  const refreshSettings = async () => {
1338
1348
  const r = await api.getSettings();
@@ -1368,6 +1378,7 @@ ${t("cloudHint")}`;
1368
1378
  return null;
1369
1379
  });
1370
1380
  const disabled = busy !== null;
1381
+ const noticeBox = (busy !== null || notice) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `dshvpn-notice dshvpn-${busy !== null ? "info" : notice?.tone}`, role: "status", children: busy !== null ? busy === "detect" ? t("detecting") : t("working") : notice?.text });
1371
1382
  const needsSave = dirty || !saved.configured;
1372
1383
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn", children: [
1373
1384
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
@@ -1418,6 +1429,13 @@ ${t("cloudHint")}`;
1418
1429
  ] }, m)) })
1419
1430
  ] }),
1420
1431
  form.auth === "key" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeyPanel, { t, api, form, saved, set, setForm }),
1432
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-wide dshvpn-detect", children: [
1433
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "primary", disabled: disabled || form.host.trim() === "", onClick: () => {
1434
+ void detect();
1435
+ }, children: busy === "detect" ? t("detecting") : t("detect") }),
1436
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("detectHint") })
1437
+ ] }),
1438
+ noticeFrom === "detect" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshvpn-wide", children: noticeBox }),
1421
1439
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dshvpn-wide", children: [
1422
1440
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("linkAddress") }),
1423
1441
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Input, { value: form.linkAddress, placeholder: form.host, onChange: (e) => {
@@ -1506,9 +1524,6 @@ ${t("cloudHint")}`;
1506
1524
  children: t("test")
1507
1525
  }
1508
1526
  ),
1509
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "outline", disabled: disabled || needsSave, onClick: () => {
1510
- void detect();
1511
- }, children: busy === "detect" ? t("working") : t("detect") }),
1512
1527
  dirty && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-warn", children: t("unsaved") })
1513
1528
  ] }),
1514
1529
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
@@ -1588,7 +1603,7 @@ ${fw.backend === "none" ? t("fwNone") : t("fwStatus", { backend: fw.backend, cou
1588
1603
  }, children: t("uninstall") })
1589
1604
  ] })
1590
1605
  ] }),
1591
- (busy !== null || notice) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `dshvpn-notice dshvpn-${busy !== null ? "info" : notice?.tone}`, role: "status", children: busy !== null ? t("working") : notice?.text }),
1606
+ noticeFrom !== "detect" && noticeBox,
1592
1607
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dshvpn-qr-section", children: [
1593
1608
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-row", children: [
1594
1609
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: t("qrSection") }),
package/lib/index.js CHANGED
@@ -20483,7 +20483,12 @@ function apiHandlers(service, uiLang, onUiLang, updater) {
20483
20483
  "/test": (_body, signal) => service.test(signal),
20484
20484
  "/status": (_body, signal) => service.status(signal),
20485
20485
  "/links": (_body, signal) => service.links(signal),
20486
- "/detect": (_body, signal) => service.detect(signal),
20486
+ // With form fields: try them as they are (no save needed first); they are
20487
+ // stored only once the login works.
20488
+ "/detect": async (body, signal) => {
20489
+ if (typeof body.host !== "string") return service.detect(signal);
20490
+ return service.detect(signal, applyInput(await service.store.get(), body));
20491
+ },
20487
20492
  "/install": (body, signal) => service.install({ regenerate: body.regenerate === true }, signal),
20488
20493
  "/uninstall": (_body, signal) => service.uninstall(signal)
20489
20494
  };
@@ -20630,31 +20635,42 @@ var VpnService = class {
20630
20635
  return this.exclusive(async () => {
20631
20636
  try {
20632
20637
  const s = await this.settings();
20633
- const res = await runVpnScript(action, args(s), await targetOf(s, this.keys), signal, this.config.timeoutMs);
20634
- await this.pin(s, res.hostKey);
20635
- if (!res.result) {
20636
- const sudo = /incorrect password|a password is required|not in the sudoers|may not run sudo/i.test(res.log);
20637
- return {
20638
- ok: false,
20639
- error: sudo ? "sudo_failed" : "no_result",
20640
- message: sudo ? `sudo refused for ${s.username}: ${res.log}` : `script did not report a result (exit ${res.exitCode}): ${res.log}`,
20641
- log: res.log
20642
- };
20643
- }
20644
- const r = res.result;
20645
- return {
20646
- ok: r.ok === true,
20647
- ...typeof r.error === "string" ? { error: r.error } : {},
20648
- ...typeof r.message === "string" ? { message: r.message } : {},
20649
- result: r,
20650
- ...r.links ? { links: r.links } : {},
20651
- log: res.log
20652
- };
20638
+ const { op, hostKey } = await this.runOn(s, action, args(s), signal);
20639
+ await this.pin(s, hostKey);
20640
+ return op;
20653
20641
  } catch (err) {
20654
20642
  return failure(err);
20655
20643
  }
20656
20644
  });
20657
20645
  }
20646
+ /** Run the script against `s` (stored or not); throws SSH and config errors. */
20647
+ async runOn(s, action, args, signal) {
20648
+ const res = await runVpnScript(action, args, await targetOf(s, this.keys), signal, this.config.timeoutMs);
20649
+ if (!res.result) {
20650
+ const sudo = /incorrect password|a password is required|not in the sudoers|may not run sudo/i.test(res.log);
20651
+ return {
20652
+ hostKey: res.hostKey,
20653
+ op: {
20654
+ ok: false,
20655
+ error: sudo ? "sudo_failed" : "no_result",
20656
+ message: sudo ? `sudo refused for ${s.username}: ${res.log}` : `script did not report a result (exit ${res.exitCode}): ${res.log}`,
20657
+ log: res.log
20658
+ }
20659
+ };
20660
+ }
20661
+ const r = res.result;
20662
+ return {
20663
+ hostKey: res.hostKey,
20664
+ op: {
20665
+ ok: r.ok === true,
20666
+ ...typeof r.error === "string" ? { error: r.error } : {},
20667
+ ...typeof r.message === "string" ? { message: r.message } : {},
20668
+ result: r,
20669
+ ...r.links ? { links: r.links } : {},
20670
+ log: res.log
20671
+ }
20672
+ };
20673
+ }
20658
20674
  install(opts, signal) {
20659
20675
  return this.run("install", (s) => {
20660
20676
  const protocols = resolveProtocols(opts.protocols, s.protocols);
@@ -20678,32 +20694,44 @@ var VpnService = class {
20678
20694
  return this.run("links", (s) => ["--server", s.linkAddress ?? s.host], signal);
20679
20695
  }
20680
20696
  /**
20681
- * Find an existing installation (e.g. after dsh-vpn was removed and its local
20682
- * settings lost): read the server's protocols, certificate mode and links,
20683
- * and adopt the protocols and certificate mode into local settings so a
20684
- * later 安装 / 更新 keeps the node as it is.
20697
+ * Find an existing installation (e.g. dsh or dsh-vpn was reinstalled and the
20698
+ * local settings are gone, but the node still runs). `candidate` is what the
20699
+ * settings page holds, saved or not: it is stored only once login works, with
20700
+ * the server's protocols and certificate mode adopted so that a later
20701
+ * 安装 / 更新 keeps the node, its keys and its links as they are.
20685
20702
  */
20686
- async detect(signal) {
20687
- const status = await this.status(signal);
20688
- if (!status.ok || status.result?.installed !== true) return status.ok ? { ok: true, result: { installed: false } } : status;
20689
- const r = status.result;
20690
- const protocols = String(r.protocols ?? "").split(",").filter(isProtocolId);
20691
- const mode = r.cert?.mode;
20692
- const certMode = mode === "ip" || mode === "self" ? mode : void 0;
20693
- let synced = false;
20694
- await this.store.update((cur) => {
20695
- if (!cur) return void 0;
20696
- const next = { ...cur, ...protocols.length > 0 ? { protocols } : {}, ...certMode ? { certMode } : {} };
20697
- synced = next.certMode !== cur.certMode || next.protocols.join() !== cur.protocols.join();
20698
- return synced ? next : void 0;
20703
+ detect(signal, candidate) {
20704
+ return this.exclusive(async () => {
20705
+ try {
20706
+ const s = candidate ?? await this.settings();
20707
+ const status = await this.runOn(s, "status", [], signal);
20708
+ if (!status.op.ok) return status.op;
20709
+ const r = status.op.result ?? {};
20710
+ const installed = r.installed === true;
20711
+ const protocols = installed ? String(r.protocols ?? "").split(",").filter(isProtocolId) : [];
20712
+ const mode = r.cert?.mode;
20713
+ const certMode = installed && (mode === "ip" || mode === "self") ? mode : void 0;
20714
+ const before = candidate ?? s;
20715
+ const next = {
20716
+ ...before,
20717
+ ...protocols.length > 0 ? { protocols } : {},
20718
+ ...certMode ? { certMode } : {},
20719
+ hostKey: before.hostKey ?? (status.hostKey || void 0)
20720
+ };
20721
+ const synced = next.certMode !== before.certMode || next.protocols.join() !== before.protocols.join();
20722
+ await this.store.update((cur) => candidate || cur ? next : void 0);
20723
+ if (!installed) return { ok: true, result: { installed: false } };
20724
+ const links = await this.runOn(next, "links", ["--server", next.linkAddress ?? next.host], signal);
20725
+ return {
20726
+ ok: links.op.ok,
20727
+ ...links.op.error ? { error: links.op.error, message: links.op.message } : {},
20728
+ result: { installed: true, active: r.active, version: r.version, protocols, certMode, synced },
20729
+ ...links.op.links ? { links: links.op.links } : {}
20730
+ };
20731
+ } catch (err) {
20732
+ return failure(err);
20733
+ }
20699
20734
  });
20700
- const links = await this.links(signal);
20701
- return {
20702
- ok: links.ok,
20703
- ...links.error ? { error: links.error, message: links.message } : {},
20704
- result: { installed: true, active: r.active, version: r.version, protocols, certMode, synced },
20705
- ...links.links ? { links: links.links } : {}
20706
- };
20707
20735
  }
20708
20736
  uninstall(signal) {
20709
20737
  return this.run("uninstall", () => [], signal);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vpn",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "DeepSeek Harness plugin: deploy an encrypted VPN (sing-box) to your own VPS in one click, to protect your connections and privacy on untrusted networks",
5
5
  "keywords": [
6
6
  "anytls",