dsh-vpn 0.1.10 → 0.1.11

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
@@ -38,12 +38,13 @@ dsh plugin --profile web add dsh-vpn
38
38
  3. 在「节点二维码」里点「刷新二维码」,用手机或电脑上的客户端扫码导入。
39
39
  4. 服务器防火墙的端口由插件自动放行,不用手动操作;安装完成后提示框会列出放行了哪些端口。
40
40
 
41
- **密钥登录**:服务器只允许密钥登录(或者你不想保存密码)时,把「登录方式」选成「密钥」。插件会生成一把专用钥匙(纯 JS 生成,不需要本机的 ssh-keygen),页面上显示它的公钥,任选一种办法放到服务器上:
41
+ **密钥登录**:服务器只允许密钥登录时,把「登录方式」选成「密钥」。插件按这个顺序尝试登录,哪把被服务器认可就用哪把:
42
42
 
43
- - A. 粘到服务商后台的「SSH 密钥」里,重装系统时勾选它(新机器推荐);
44
- - B. 已经能登录服务器的(网页控制台或别的终端),复制页面上的一行命令粘贴执行。
43
+ 1. 你自己粘贴的私钥(展开「改用我自己已有的私钥」,支持带密码短语的私钥);
44
+ 2. 本机已有的钥匙:`~/.ssh/config`(含 `config.d`)里为这台服务器配置的 `IdentityFile`,dsh-vps-manager 的 `~/.ssh/dsh_vps_ed25519`,以及 `~/.ssh/id_ed25519`、`id_ecdsa`、`id_rsa`;
45
+ 3. 插件的专用钥匙(纯 JS 生成,不需要本机的 ssh-keygen)。
45
46
 
46
- 放好后点「保存」和「测试连接」。已经有自己的私钥、服务器上也放好了对应公钥的,展开「改用我自己已有的私钥」粘贴进去即可(支持带密码短语的私钥)。用非 root 用户时,「sudo 密码」一栏填该用户的 sudo 密码;root 或免密 sudo 不用填。
47
+ **已经用 dsh-vps-manager 管理的服务器不需要再放任何公钥**:它的钥匙已经在服务器上,直接点「检查已有安装」或「测试连接」即可,页面会列出本机找到的钥匙,测试连接成功后会说明用的是哪一把。本机钥匙都不被认可时,再把页面上显示的专用钥匙公钥放到服务器上(服务商后台的「SSH 密钥」,或在能登录的服务器上执行页面给出的一行命令)。私钥只在本机 dsh 里读取,不会发给页面;带密码短语的本机私钥会跳过。用非 root 用户时,「sudo 密码」一栏填该用户的 sudo 密码;root 或免密 sudo 不用填。
47
48
 
48
49
  **找回已有节点**:重装过 dsh 或 dsh-vpn 后,本地保存的设置会丢失,但 VPS 上的节点还在运行。在 VPN 设置里填好服务器 IP、端口、用户名和密码(或选密钥登录),**不用先保存**,直接点登录方式下面的「检查已有安装」:插件登录服务器读取仍在运行的节点,显示原来的二维码(链接和以前完全一样,已经导入手机或电脑客户端的配置继续可用),按服务器上的实际情况勾选协议和证书类型,并自动保存设置。登录失败时什么都不保存,填的内容也不会被清掉。
49
50
 
package/lib/client.js CHANGED
@@ -51,7 +51,7 @@ var vpnApi = {
51
51
  getSettings: () => post("/settings/get"),
52
52
  saveSettings: (form) => post("/settings/save", form),
53
53
  /** Public half of the plugin's dedicated key and the line that installs it. */
54
- sshKey: () => post("/ssh-key"),
54
+ sshKey: (host = "") => post("/ssh-key", { host }),
55
55
  forgetHostKey: () => post("/host-key/forget"),
56
56
  test: () => post("/test"),
57
57
  status: () => post("/status"),
@@ -97,6 +97,11 @@ var zh = {
97
97
  sudoPassword: "sudo \u5BC6\u7801\uFF08\u9009\u586B\uFF09",
98
98
  sudoPasswordEmpty: "\u7528 root \u6216\u514D\u5BC6 sudo \u65F6\u4E0D\u7528\u586B",
99
99
  keyIntro: "\u7528\u63D2\u4EF6\u7684\u4E13\u7528\u94A5\u5319\u767B\u5F55\u3002\u628A\u4E0B\u9762\u7684\u516C\u94A5\u653E\u5230\u670D\u52A1\u5668\u4E0A\uFF0C\u4EFB\u9009\u4E00\u79CD\u529E\u6CD5\uFF1A",
100
+ keyIntroFallback: "\u5982\u679C\u670D\u52A1\u5668\u4E0D\u8BA4\u4E0A\u9762\u7684\u94A5\u5319\uFF0C\u518D\u7528\u63D2\u4EF6\u7684\u4E13\u7528\u94A5\u5319\uFF1A\u628A\u4E0B\u9762\u7684\u516C\u94A5\u653E\u5230\u670D\u52A1\u5668\u4E0A\uFF0C\u4EFB\u9009\u4E00\u79CD\u529E\u6CD5\uFF1A",
101
+ localKeysFound: "\u672C\u673A\u5DF2\u6709\u7684\u94A5\u5319\uFF08\u4F1A\u5148\u7528\u5B83\u4EEC\u767B\u5F55\uFF09",
102
+ localKeysHint: "\u5305\u62EC ~/.ssh/config \u91CC\u4E3A\u8FD9\u53F0\u670D\u52A1\u5668\u914D\u7F6E\u7684\u94A5\u5319\u3001dsh-vps-manager \u7684 ~/.ssh/dsh_vps_ed25519 \u548C ~/.ssh/id_*\u3002\u670D\u52A1\u5668\u8BA4\u53EF\u5176\u4E2D\u4EFB\u4F55\u4E00\u628A\uFF0C\u5C31\u4E0D\u7528\u518D\u653E\u516C\u94A5\uFF0C\u76F4\u63A5\u70B9\u300C\u68C0\u67E5\u5DF2\u6709\u5B89\u88C5\u300D\u6216\u300C\u6D4B\u8BD5\u8FDE\u63A5\u300D\u3002\u79C1\u94A5\u53EA\u5728\u672C\u673A dsh \u91CC\u8BFB\u53D6\uFF0C\u4E0D\u4F1A\u53D1\u7ED9\u9875\u9762\u3002",
103
+ testOkKey: "\uFF08\u7528 {key} \u767B\u5F55\uFF09",
104
+ dedicatedKey: "\u63D2\u4EF6\u4E13\u7528\u94A5\u5319",
100
105
  keyWayConsole: "A. \u670D\u52A1\u5546\u540E\u53F0\u6DFB\u52A0\uFF08\u65B0\u673A\u5668\u63A8\u8350\uFF09",
101
106
  keyWayConsoleHint: "\u7C98\u5230\u670D\u52A1\u5546\u7684\u300CSSH \u5BC6\u94A5\u300D\u91CC\uFF0C\u91CD\u88C5\u7CFB\u7EDF\u65F6\u52FE\u9009\u5B83\u3002",
102
107
  keyWayCommand: "B. \u5DF2\u7ECF\u80FD\u767B\u5F55\uFF08\u7F51\u9875\u63A7\u5236\u53F0\u6216\u522B\u7684\u7EC8\u7AEF\uFF09",
@@ -228,6 +233,11 @@ var en = {
228
233
  sudoPassword: "sudo password (optional)",
229
234
  sudoPasswordEmpty: "Not needed for root or passwordless sudo",
230
235
  keyIntro: "Log in with the plugin's dedicated key. Put the public key below on the server, either way:",
236
+ keyIntroFallback: "If the server accepts none of the keys above, use the plugin's dedicated key: put the public key below on the server, either way:",
237
+ localKeysFound: "Keys already on this computer (tried first)",
238
+ localKeysHint: "The keys ~/.ssh/config names for this server, dsh-vps-manager's ~/.ssh/dsh_vps_ed25519 and ~/.ssh/id_*. If the server accepts any of them, nothing needs to be added: click Find existing install or Test connection. Private keys are read only by dsh on this computer and never sent to the page.",
239
+ testOkKey: " (logged in with {key})",
240
+ dedicatedKey: "the plugin's dedicated key",
231
241
  keyWayConsole: "A. Add it in your provider's console (recommended for new machines)",
232
242
  keyWayConsoleHint: `Paste it into your provider's "SSH keys" and select it when reinstalling the OS.`,
233
243
  keyWayCommand: "B. If you can already log in (web console or another terminal)",
@@ -1176,17 +1186,26 @@ function KeyPanel({ t, api, form, saved, set, setForm }) {
1176
1186
  const [own, setOwn] = (0, import_react.useState)(ownStored);
1177
1187
  (0, import_react.useEffect)(() => {
1178
1188
  let live = true;
1179
- void api.sshKey().then((r) => {
1180
- if (live && r.ok) setKey(r);
1181
- });
1189
+ const timer = setTimeout(() => {
1190
+ void api.sshKey(form.host).then((r) => {
1191
+ if (live && r.ok) setKey(r);
1192
+ });
1193
+ }, 300);
1182
1194
  return () => {
1183
1195
  live = false;
1196
+ clearTimeout(timer);
1184
1197
  };
1185
- }, [api]);
1198
+ }, [api, form.host]);
1199
+ const local = key?.localKeys ?? [];
1186
1200
  const preparing = t("working");
1187
1201
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-keypanel dshvpn-wide", children: [
1202
+ local.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-keycard dshvpn-success", children: [
1203
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("localKeysFound") }),
1204
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: local.join("\n") }),
1205
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("localKeysHint") })
1206
+ ] }),
1188
1207
  !own && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1189
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dshvpn-muted", children: t("keyIntro") }),
1208
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dshvpn-muted", children: local.length > 0 ? t("keyIntroFallback") : t("keyIntro") }),
1190
1209
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-keycard", children: [
1191
1210
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("keyWayConsole") }),
1192
1211
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("keyWayConsoleHint") }),
@@ -1518,7 +1537,9 @@ ${t("cloudHint")}`;
1518
1537
  onClick: () => {
1519
1538
  void op("test", api.test, (r) => {
1520
1539
  void refreshSettings();
1521
- return { tone: "success", text: t("testOk", { system: String(r.result?.system ?? "") }) };
1540
+ const k = r.result?.key;
1541
+ const used = typeof k === "string" ? t("testOkKey", { key: k === "own" ? t("ownKey") : k === "dsh-vpn" ? t("dedicatedKey") : k }) : "";
1542
+ return { tone: "success", text: `${t("testOk", { system: String(r.result?.system ?? "") })}${used}` };
1522
1543
  });
1523
1544
  },
1524
1545
  children: t("test")
package/lib/index.js CHANGED
@@ -20199,22 +20199,36 @@ function fingerprint(key) {
20199
20199
  }
20200
20200
  var STDERR_TAIL = 8e3;
20201
20201
  var SshSession = class _SshSession {
20202
- constructor(client, hostKey) {
20202
+ constructor(client, hostKey, keyLabel) {
20203
20203
  this.client = client;
20204
20204
  this.hostKey = hostKey;
20205
+ this.keyLabel = keyLabel;
20205
20206
  }
20206
20207
  client;
20207
20208
  hostKey;
20209
+ keyLabel;
20208
20210
  static async open(target, signal, timeoutMs = 2e4) {
20209
20211
  signal.throwIfAborted();
20210
20212
  const client = new import_ssh2.Client();
20211
20213
  let seen;
20214
+ const keys = target.keys ?? [];
20215
+ let tried = -1;
20212
20216
  const config = {
20213
20217
  host: target.host,
20214
20218
  port: target.port,
20215
20219
  username: target.username,
20216
- // Exactly one method: the key, or the password. Never keyboard-interactive or agents.
20217
- ...target.privateKey ? { privateKey: target.privateKey, ...target.passphrase ? { passphrase: target.passphrase } : {} } : { password: target.password },
20220
+ // Keys in order, or the password: one kind only, never keyboard-interactive or agents.
20221
+ ...keys.length > 0 ? {
20222
+ authHandler: (_left, _partial, next) => {
20223
+ tried += 1;
20224
+ const k = keys[tried];
20225
+ if (!k) {
20226
+ next(false);
20227
+ return;
20228
+ }
20229
+ next({ type: "publickey", username: target.username, key: k.key, ...k.passphrase ? { passphrase: k.passphrase } : {} });
20230
+ }
20231
+ } : { password: target.password },
20218
20232
  readyTimeout: timeoutMs,
20219
20233
  tryKeyboard: false,
20220
20234
  hostVerifier: (key) => {
@@ -20230,7 +20244,7 @@ var SshSession = class _SshSession {
20230
20244
  signal.addEventListener("abort", onAbort, { once: true });
20231
20245
  client.once("ready", () => {
20232
20246
  signal.removeEventListener("abort", onAbort);
20233
- resolve(new _SshSession(client, seen ?? ""));
20247
+ resolve(new _SshSession(client, seen ?? "", keys[tried]?.label));
20234
20248
  });
20235
20249
  client.once("error", (err) => {
20236
20250
  signal.removeEventListener("abort", onAbort);
@@ -20242,7 +20256,11 @@ var SshSession = class _SshSession {
20242
20256
  { expected: target.hostKey, actual: seen }
20243
20257
  ));
20244
20258
  } else if (err.level === "client-authentication") {
20245
- reject(target.privateKey ? new SshError("key_rejected", `SSH login failed: ${target.username}@${target.host} did not accept the key`) : new SshError("auth_failed", "SSH login failed: wrong username or password"));
20259
+ reject(keys.length > 0 ? new SshError(
20260
+ "key_rejected",
20261
+ `SSH login failed: ${target.username}@${target.host} accepted none of: ${keys.map((k) => k.label).join(", ")}`,
20262
+ { tried: keys.map((k) => k.label).join("\n") }
20263
+ ) : new SshError("auth_failed", "SSH login failed: wrong username or password"));
20246
20264
  } else if (err.level === "client-timeout") {
20247
20265
  reject(new SshError("timeout", `timed out connecting to ${target.host}:${target.port}`));
20248
20266
  } else {
@@ -20472,9 +20490,10 @@ function apiHandlers(service, uiLang, onUiLang, updater) {
20472
20490
  return { ok: true, settings: publicView(saved) };
20473
20491
  },
20474
20492
  // The dedicated key's public half and the line that installs it; never the private key.
20475
- "/ssh-key": async () => {
20493
+ "/ssh-key": async (body) => {
20476
20494
  const { publicKey } = await service.keys.get();
20477
- return { ok: true, publicKey, command: authorizedKeysCommand(publicKey) };
20495
+ const host = typeof body.host === "string" ? body.host.trim() : "";
20496
+ return { ok: true, publicKey, command: authorizedKeysCommand(publicKey), localKeys: host ? service.localKeyLabels(host) : [] };
20478
20497
  },
20479
20498
  "/host-key/forget": async () => {
20480
20499
  await service.store.update((cur) => cur && { ...cur, hostKey: void 0 });
@@ -20588,11 +20607,17 @@ var NotConfiguredError = class extends Error {
20588
20607
  this.name = "NotConfiguredError";
20589
20608
  }
20590
20609
  };
20591
- async function targetOf(s, keys) {
20610
+ async function targetOf(s, keys, local = () => []) {
20592
20611
  const base = { host: s.host, port: s.port, username: s.username, password: s.password, hostKey: s.hostKey };
20593
20612
  if (s.auth !== "key") return base;
20594
- if (s.privateKey !== void 0) return { ...base, privateKey: s.privateKey, passphrase: s.passphrase };
20595
- return { ...base, privateKey: (await keys.get()).privateKey };
20613
+ const list = [];
20614
+ if (s.privateKey !== void 0) list.push({ key: s.privateKey, passphrase: s.passphrase, label: "own" });
20615
+ try {
20616
+ list.push(...local(s.host));
20617
+ } catch {
20618
+ }
20619
+ list.push({ key: (await keys.get()).privateKey, label: "dsh-vpn" });
20620
+ return { ...base, keys: list };
20596
20621
  }
20597
20622
  function resolveProtocols(requested, fallback) {
20598
20623
  const list = requested && requested.length > 0 ? [...new Set(requested)] : [...fallback];
@@ -20616,6 +20641,14 @@ var VpnService = class {
20616
20641
  config;
20617
20642
  keys;
20618
20643
  queue = Promise.resolve();
20644
+ /** Labels of this computer's keys that key login would try for `host` (no key material). */
20645
+ localKeyLabels(host) {
20646
+ try {
20647
+ return (this.config.localKeys?.(host) ?? []).map((k) => k.label);
20648
+ } catch {
20649
+ return [];
20650
+ }
20651
+ }
20619
20652
  async settings() {
20620
20653
  const s = await this.store.get();
20621
20654
  if (!s) throw new NotConfiguredError();
@@ -20645,7 +20678,7 @@ var VpnService = class {
20645
20678
  }
20646
20679
  /** Run the script against `s` (stored or not); throws SSH and config errors. */
20647
20680
  async runOn(s, action, args, signal) {
20648
- const res = await runVpnScript(action, args, await targetOf(s, this.keys), signal, this.config.timeoutMs);
20681
+ const res = await runVpnScript(action, args, await targetOf(s, this.keys, this.config.localKeys), signal, this.config.timeoutMs);
20649
20682
  if (!res.result) {
20650
20683
  const sudo = /incorrect password|a password is required|not in the sudoers|may not run sudo/i.test(res.log);
20651
20684
  return {
@@ -20745,7 +20778,7 @@ var VpnService = class {
20745
20778
  let session;
20746
20779
  try {
20747
20780
  const s = await this.settings();
20748
- session = await SshSession.open(await targetOf(s, this.keys), signal);
20781
+ session = await SshSession.open(await targetOf(s, this.keys, this.config.localKeys), signal);
20749
20782
  const asRoot = s.username === "root";
20750
20783
  const cmd = `echo "uid=$(${asRoot ? "" : `sudo -S -p '' `}id -u)"; echo "sys=$(uname -srm)"`;
20751
20784
  const res = await session.exec(cmd, asRoot ? "" : `${s.password}
@@ -20757,7 +20790,7 @@ var VpnService = class {
20757
20790
  return {
20758
20791
  ok: root,
20759
20792
  ...root ? {} : { error: "sudo_failed", message: `cannot become root as ${s.username}: ${res.stderr.trim()}` },
20760
- result: { hostKey: session.hostKey, system, root }
20793
+ result: { hostKey: session.hostKey, system, root, ...session.keyLabel ? { key: session.keyLabel } : {} }
20761
20794
  };
20762
20795
  } catch (err) {
20763
20796
  return failure(err);
@@ -20895,14 +20928,95 @@ function notConfiguredMessage(lang) {
20895
20928
  return lang === "zh" ? "\u5C1A\u672A\u914D\u7F6E\u670D\u52A1\u5668\u3002\u8BF7\u70B9 dsh \u5DE6\u4E0B\u89D2\u300C\u8BBE\u7F6E\u300D\u2192\u300CVPN \u8BBE\u7F6E\u300D\uFF0C\u586B\u5199\u670D\u52A1\u5668 IP\u3001\u7AEF\u53E3\u3001\u7528\u6237\u540D\u548C\u5BC6\u7801\u3002" : "No server configured. Open dsh Settings (bottom left) \u2192 VPN and fill in the server IP, port, username and password first.";
20896
20929
  }
20897
20930
 
20931
+ // src/local-keys.ts
20932
+ import { readdirSync, readFileSync } from "node:fs";
20933
+ import { homedir } from "node:os";
20934
+ import { isAbsolute, join } from "node:path";
20935
+ var DEFAULT_KEYS = ["dsh_vps_ed25519", "id_ed25519", "id_ecdsa", "id_rsa"];
20936
+ function readText(file) {
20937
+ try {
20938
+ return readFileSync(file, "utf8");
20939
+ } catch {
20940
+ return void 0;
20941
+ }
20942
+ }
20943
+ function configTexts(sshDir) {
20944
+ const texts = [];
20945
+ const main = readText(join(sshDir, "config"));
20946
+ if (main !== void 0) texts.push(main);
20947
+ let names = [];
20948
+ try {
20949
+ names = readdirSync(join(sshDir, "config.d")).sort();
20950
+ } catch {
20951
+ }
20952
+ for (const name2 of names) {
20953
+ const t = readText(join(sshDir, "config.d", name2));
20954
+ if (t !== void 0) texts.push(t);
20955
+ }
20956
+ return texts;
20957
+ }
20958
+ function identityFilesFor(host, sshDir, home) {
20959
+ const files = [];
20960
+ for (const text of configTexts(sshDir)) {
20961
+ let patterns = [];
20962
+ let hostName;
20963
+ let identities = [];
20964
+ const flush = () => {
20965
+ const literal = patterns.filter((p) => !/[*?!]/.test(p));
20966
+ if (literal.length > 0 && (hostName === host || literal.includes(host))) {
20967
+ files.push(...identities);
20968
+ }
20969
+ };
20970
+ for (const raw of text.split("\n")) {
20971
+ const line = raw.replace(/\s+#.*$/, "").trim();
20972
+ const m = /^(\w+)\s*=?\s*(.+)$/.exec(line);
20973
+ if (!m || line.startsWith("#")) continue;
20974
+ const key = m[1].toLowerCase();
20975
+ const value = m[2].trim().replace(/^"(.*)"$/, "$1");
20976
+ if (key === "host" || key === "match") {
20977
+ flush();
20978
+ patterns = key === "host" ? value.split(/\s+/) : [];
20979
+ hostName = void 0;
20980
+ identities = [];
20981
+ } else if (key === "hostname") {
20982
+ hostName = value;
20983
+ } else if (key === "identityfile") {
20984
+ const expanded = value.startsWith("~/") ? join(home, value.slice(2)) : value;
20985
+ identities.push(isAbsolute(expanded) ? expanded : join(sshDir, expanded));
20986
+ }
20987
+ }
20988
+ flush();
20989
+ }
20990
+ return files;
20991
+ }
20992
+ function localKeysFor(host, opts = {}) {
20993
+ const home = opts.home ?? homedir();
20994
+ const sshDir = opts.sshDir ?? join(home, ".ssh");
20995
+ const paths = [...identityFilesFor(host, sshDir, home), ...DEFAULT_KEYS.map((n) => join(sshDir, n))];
20996
+ const keys = [];
20997
+ for (const path of [...new Set(paths)]) {
20998
+ const key = readText(path);
20999
+ if (key === void 0 || !key.includes("PRIVATE KEY")) continue;
21000
+ let problem;
21001
+ try {
21002
+ problem = privateKeyProblem(key);
21003
+ } catch {
21004
+ problem = "unreadable";
21005
+ }
21006
+ if (problem) continue;
21007
+ keys.push({ key, label: path.startsWith(home) ? `~${path.slice(home.length)}` : path });
21008
+ }
21009
+ return keys;
21010
+ }
21011
+
20898
21012
  // src/update.ts
20899
- import { readFileSync } from "node:fs";
21013
+ import { readFileSync as readFileSync2 } from "node:fs";
20900
21014
  var PACKAGE_NAME = "dsh-vpn";
20901
21015
  var MIN_RELEASE_AGE_MS = 24 * 60 * 60 * 1e3;
20902
21016
  var NPM_REGISTRY = "https://registry.npmjs.org/";
20903
21017
  function currentVersion() {
20904
21018
  try {
20905
- const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
21019
+ const pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
20906
21020
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
20907
21021
  } catch {
20908
21022
  return "0.0.0";
@@ -21056,7 +21170,7 @@ function toToolValue(r, uiLang) {
21056
21170
  function apply(ctx, config) {
21057
21171
  const service = new VpnService(
21058
21172
  new CredentialSettingsStore(ctx),
21059
- { timeoutMs: config.timeoutMs, acmeProvider: config.acmeProvider },
21173
+ { timeoutMs: config.timeoutMs, acmeProvider: config.acmeProvider, localKeys: (host) => localKeysFor(host) },
21060
21174
  new CredentialKeyStore(ctx)
21061
21175
  );
21062
21176
  const uiLang = new UiLang();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vpn",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
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",