dsh-vpn 0.1.5 → 0.1.6

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,6 +38,8 @@ dsh plugin --profile web add dsh-vpn
38
38
  3. 在「节点二维码」里点「刷新二维码」,用手机或电脑上的客户端扫码导入。
39
39
  4. 服务器防火墙的端口由插件自动放行,不用手动操作;安装完成后提示框会列出放行了哪些端口。
40
40
 
41
+ 页面第一行显示插件版本,有新版本时旁边会提示,点「更新」即可升级插件,重启 dsh 后生效(需要 dsh 0.2 及以上;更早的版本请在「设置 → 插件」中更新)。为了供应链安全,发布不足 24 小时的新版本会等满 24 小时再提供更新。
42
+
41
43
  ## 语言
42
44
 
43
45
  整个插件的语言跟随 dsh 的界面语言(「设置 → 通用设置 → 语言」),目前支持 dsh 提供的中文和 English:
package/lib/client.js CHANGED
@@ -56,6 +56,10 @@ var vpnApi = {
56
56
  links: () => post("/links"),
57
57
  install: (regenerate) => post("/install", { regenerate }),
58
58
  uninstall: () => post("/uninstall"),
59
+ /** Installed version; with `check`, also the newest release on npm. */
60
+ version: (check = false) => post("/version", { check }),
61
+ /** Install the newest release through dsh's plugin manager. */
62
+ update: () => post("/update"),
59
63
  /** Tell the host which language dsh's UI shows (approval prompts follow it). */
60
64
  reportUiLocale: (locale) => post("/ui-locale", { locale })
61
65
  };
@@ -66,6 +70,17 @@ var zh = {
66
70
  title: "VPN \u8282\u70B9",
67
71
  nav: "VPN \u8BBE\u7F6E",
68
72
  description: "\u5728\u4F60\u81EA\u5DF1\u7684 VPS \u4E0A\u4E00\u952E\u90E8\u7F72\u52A0\u5BC6 VPN\uFF0C\u4FDD\u62A4\u4F60\u5728\u516C\u5171 Wi-Fi \u7B49\u7F51\u7EDC\u4E2D\u7684\u8FDE\u63A5\u5B89\u5168\u548C\u9690\u79C1\uFF1B\u626B\u7801\u5373\u53EF\u5BFC\u5165\u624B\u673A\u6216\u7535\u8111\u4E0A\u7684\u5BA2\u6237\u7AEF\u3002",
73
+ version: "\u7248\u672C v{version}",
74
+ update: "\u66F4\u65B0",
75
+ updating: "\u6B63\u5728\u66F4\u65B0\u2026",
76
+ "update.latest": "\u5DF2\u662F\u6700\u65B0\u7248\u672C",
77
+ "update.available": "\u6709\u65B0\u7248\u672C v{version}",
78
+ "update.tooNew": "\u65B0\u7248\u672C v{version} \u521A\u53D1\u5E03\uFF0C\u4E3A\u5B89\u5168\u8D77\u89C1 {time} \u540E\u53EF\u66F4\u65B0",
79
+ "update.done": "\u5DF2\u66F4\u65B0\u5230 v{version}\uFF0C\u91CD\u542F dsh \u540E\u751F\u6548",
80
+ "update.unsupported": "\u5F53\u524D dsh \u7248\u672C\u4E0D\u652F\u6301\u5728\u6B64\u66F4\u65B0\uFF0C\u8BF7\u5728\u300C\u8BBE\u7F6E \u2192 \u63D2\u4EF6\u300D\u4E2D\u66F4\u65B0 dsh-vpn\u3002",
81
+ "update.busy": "\u6B63\u5728\u66F4\u65B0\uFF0C\u8BF7\u7A0D\u5019\u3002",
82
+ "update.check_failed": "\u65E0\u6CD5\u68C0\u67E5\u65B0\u7248\u672C\uFF1A{message}",
83
+ "update.install_failed": "\u66F4\u65B0\u5931\u8D25\uFF1A{message}",
69
84
  serverSection: "\u670D\u52A1\u5668",
70
85
  host: "\u670D\u52A1\u5668 IP \u6216\u57DF\u540D",
71
86
  hostHint: "\u4F60\u8D2D\u4E70\u7684 VPS \u7684\u516C\u7F51\u5730\u5740\u3002",
@@ -157,6 +172,17 @@ var en = {
157
172
  title: "VPN node",
158
173
  nav: "VPN",
159
174
  description: "Deploy an encrypted VPN to your own VPS in one click to keep your connections private and secure on networks like public Wi-Fi; import it into client apps by scanning QR codes.",
175
+ version: "Version v{version}",
176
+ update: "Update",
177
+ updating: "Updating\u2026",
178
+ "update.latest": "You have the latest version",
179
+ "update.available": "v{version} is available",
180
+ "update.tooNew": "v{version} was just released; for safety it can be installed after {time}",
181
+ "update.done": "Updated to v{version}. Restart dsh to use it.",
182
+ "update.unsupported": "This dsh version cannot update plugins from here. Update dsh-vpn in Settings \u2192 Plugins.",
183
+ "update.busy": "An update is already running.",
184
+ "update.check_failed": "Could not check for updates: {message}",
185
+ "update.install_failed": "Update failed: {message}",
160
186
  serverSection: "Server",
161
187
  host: "Server IP or domain",
162
188
  hostHint: "Public address of your VPS.",
@@ -282,7 +308,11 @@ var STYLES = `
282
308
  .dshvpn-qr svg { width: 180px; height: 180px; }
283
309
  .dshvpn-link { display: block; max-width: 100%; max-height: 3.2em; overflow: hidden; color: var(--dsw-alias-label-secondary, #666); }
284
310
  .dshvpn-section { display: flex; flex-direction: column; gap: 16px; }
285
- .dshvpn-section-head h2 { margin: 0 0 4px; font-size: 18px; font-weight: 600; }
311
+ .dshvpn-section-head h2 { margin: 0; font-size: 18px; font-weight: 600; }
312
+ .dshvpn-title-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 4px; }
313
+ .dshvpn-version { font-size: 12px; font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-secondary, #666); }
314
+ .dshvpn-update-note { font-size: 12px; color: var(--dsw-alias-label-secondary, #666); }
315
+ .dshvpn-update-note.dshvpn-new { color: var(--dsw-alias-state-success-primary, #2f855a); font-weight: 600; }
286
316
  .dshvpn-section-head p { margin: 0; }
287
317
  .dshvpn-qr-section { padding: 14px; border-radius: 12px; border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.3)); }
288
318
  .dshvpn-qr-section h3 { font-size: 15px; }
@@ -296,6 +326,10 @@ var STYLES = `
296
326
  .dshvpn-qr-empty-icon { font-size: 36px; line-height: 1; color: var(--dsw-alias-label-tertiary, #888); }
297
327
  `;
298
328
 
329
+ // src/client/VpnSettingsSection.tsx
330
+ var import_react2 = require("react");
331
+ var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
332
+
299
333
  // src/client/VpnSettingsPage.tsx
300
334
  var import_react = require("react");
301
335
  var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
@@ -1377,13 +1411,57 @@ ${fw.backend === "none" ? t("fwNone") : t("fwStatus", { backend: fw.backend, cou
1377
1411
  // src/client/VpnSettingsSection.tsx
1378
1412
  var import_jsx_runtime2 = require("react/jsx-runtime");
1379
1413
  function VpnSettingsSection(props) {
1380
- const { t } = props;
1414
+ const { t, api } = props;
1415
+ const [info, setInfo] = (0, import_react2.useState)(null);
1416
+ const [note, setNote] = (0, import_react2.useState)(null);
1417
+ const [updating, setUpdating] = (0, import_react2.useState)(false);
1418
+ const when = (iso) => new Date(iso).toLocaleString();
1419
+ const describe = (v) => {
1420
+ if (v.target) return { text: t("update.available", { version: v.target }), fresh: true };
1421
+ if (v.availableAt && v.latest) return { text: t("update.tooNew", { version: v.latest, time: when(v.availableAt) }) };
1422
+ return null;
1423
+ };
1424
+ (0, import_react2.useEffect)(() => {
1425
+ let live = true;
1426
+ void api.version().then((r) => {
1427
+ if (live && r.ok) setInfo(r);
1428
+ });
1429
+ void api.version(true).then((r) => {
1430
+ if (!live || !r.ok) return;
1431
+ setInfo(r);
1432
+ setNote(describe(r));
1433
+ });
1434
+ return () => {
1435
+ live = false;
1436
+ };
1437
+ }, [api]);
1438
+ const update = async () => {
1439
+ setUpdating(true);
1440
+ setNote(null);
1441
+ const r = await api.update();
1442
+ setUpdating(false);
1443
+ if (!r.ok) {
1444
+ const known = ["unsupported", "busy", "check_failed"].includes(r.error) ? r.error : "install_failed";
1445
+ setNote({ text: t(`update.${known}`, { message: r.message ?? r.error }) });
1446
+ return;
1447
+ }
1448
+ setInfo((prev) => ({ ...prev, ...r.info, canUpdate: prev?.canUpdate ?? true }));
1449
+ if (r.status === "updated") setNote({ text: t("update.done", { version: r.version }), fresh: true });
1450
+ else setNote(describe(r.info) ?? { text: t("update.latest") });
1451
+ };
1381
1452
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dshvpn-section", children: [
1382
1453
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("header", { className: "dshvpn-section-head", children: [
1383
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { children: t("nav") }),
1454
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dshvpn-title-row", children: [
1455
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { children: t("nav") }),
1456
+ info?.current && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dshvpn-version", children: t("version", { version: info.current }) }),
1457
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives2.Button, { size: "sm", variant: "outline", disabled: updating, onClick: () => {
1458
+ void update();
1459
+ }, children: updating ? t("updating") : t("update") }),
1460
+ note && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: `dshvpn-update-note${note.fresh ? " dshvpn-new" : ""}`, children: note.text })
1461
+ ] }),
1384
1462
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dshvpn-muted", children: t("description") })
1385
1463
  ] }),
1386
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(VpnSettingsPage, { t, api: props.api, view: "page" })
1464
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(VpnSettingsPage, { t, api, view: "page" })
1387
1465
  ] });
1388
1466
  }
1389
1467
 
package/lib/index.js CHANGED
@@ -20264,8 +20264,19 @@ function json(status, value) {
20264
20264
  headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" }
20265
20265
  });
20266
20266
  }
20267
- function apiHandlers(service, uiLang, onUiLang) {
20267
+ function apiHandlers(service, uiLang, onUiLang, updater) {
20268
20268
  return {
20269
+ "/version": async (body, signal) => {
20270
+ const current = updater?.current;
20271
+ const canUpdate = updater?.manager !== void 0;
20272
+ if (!updater || body.check !== true) return { ok: true, current, canUpdate };
20273
+ try {
20274
+ return { ok: true, canUpdate, ...await updater.check(signal) };
20275
+ } catch (err) {
20276
+ return { ok: false, error: "check_failed", current, canUpdate, message: err instanceof Error ? err.message : String(err) };
20277
+ }
20278
+ },
20279
+ "/update": async (_body, signal) => updater ? updater.update(signal) : { ok: false, error: "unsupported" },
20269
20280
  "/ui-locale": async (body) => {
20270
20281
  if (typeof body.locale !== "string" || body.locale.length > 35) return { ok: false, error: "bad_request", message: "locale must be a string" };
20271
20282
  if (uiLang?.set(body.locale)) onUiLang?.();
@@ -20310,8 +20321,8 @@ async function handle(handler, request) {
20310
20321
  return json(500, { ok: false, error: "internal", message: err instanceof Error ? err.message : String(err) });
20311
20322
  }
20312
20323
  }
20313
- function registerApi(ctx, service, uiLang, onUiLang) {
20314
- for (const [path, handler] of Object.entries(apiHandlers(service, uiLang, onUiLang))) {
20324
+ function registerApi(ctx, service, uiLang, onUiLang, updater) {
20325
+ for (const [path, handler] of Object.entries(apiHandlers(service, uiLang, onUiLang, updater))) {
20315
20326
  ctx.effect(() => ctx.connection.fetch.register({
20316
20327
  path: `${API_BASE}${path}`,
20317
20328
  methods: ["POST"],
@@ -20743,6 +20754,126 @@ function notConfiguredMessage(lang) {
20743
20754
  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.";
20744
20755
  }
20745
20756
 
20757
+ // src/update.ts
20758
+ import { readFileSync } from "node:fs";
20759
+ var PACKAGE_NAME = "dsh-vpn";
20760
+ var MIN_RELEASE_AGE_MS = 24 * 60 * 60 * 1e3;
20761
+ var NPM_REGISTRY = "https://registry.npmjs.org/";
20762
+ function currentVersion() {
20763
+ try {
20764
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
20765
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
20766
+ } catch {
20767
+ return "0.0.0";
20768
+ }
20769
+ }
20770
+ var RELEASE = /^(\d+)\.(\d+)\.(\d+)$/;
20771
+ function compareVersions(a, b) {
20772
+ const pa = RELEASE.exec(a);
20773
+ const pb = RELEASE.exec(b);
20774
+ if (!pa || !pb) return 0;
20775
+ for (let i = 1; i <= 3; i++) {
20776
+ const d = Number(pa[i]) - Number(pb[i]);
20777
+ if (d !== 0) return d;
20778
+ }
20779
+ return 0;
20780
+ }
20781
+ function planUpdate(current, doc, now = Date.now()) {
20782
+ const latest = doc["dist-tags"]?.latest;
20783
+ const info = { current };
20784
+ if (typeof latest !== "string" || !RELEASE.test(latest)) return info;
20785
+ info.latest = latest;
20786
+ if (compareVersions(latest, current) <= 0) return info;
20787
+ const published = (v) => Date.parse(doc.time?.[v] ?? "");
20788
+ const mature = (v) => {
20789
+ const t = published(v);
20790
+ return Number.isFinite(t) && now - t >= MIN_RELEASE_AGE_MS;
20791
+ };
20792
+ const candidates = Object.keys(doc.versions ?? { [latest]: {} }).filter((v) => RELEASE.test(v) && compareVersions(v, current) > 0 && compareVersions(v, latest) <= 0 && mature(v)).sort(compareVersions);
20793
+ const target = candidates.at(-1);
20794
+ if (target) info.target = target;
20795
+ if (target !== latest) {
20796
+ const t = published(latest);
20797
+ if (Number.isFinite(t)) info.availableAt = new Date(t + MIN_RELEASE_AGE_MS).toISOString();
20798
+ }
20799
+ return info;
20800
+ }
20801
+ async function registriesOf(manager) {
20802
+ const list = [];
20803
+ if (manager) {
20804
+ try {
20805
+ const r = await manager.registries();
20806
+ for (const url of [r.resolved, r.registry, ...r.fallbackRegistries]) if (url) list.push(url);
20807
+ } catch {
20808
+ }
20809
+ }
20810
+ list.push(NPM_REGISTRY);
20811
+ return [...new Set(list.map((u) => u.endsWith("/") ? u : `${u}/`))];
20812
+ }
20813
+ async function fetchPackument(registries, signal, fetchImpl = fetch) {
20814
+ let lastError;
20815
+ for (const registry of registries) {
20816
+ try {
20817
+ const res = await fetchImpl(`${registry}${PACKAGE_NAME}`, {
20818
+ headers: { accept: "application/json" },
20819
+ signal: AbortSignal.any([AbortSignal.timeout(15e3), ...signal ? [signal] : []])
20820
+ });
20821
+ if (!res.ok) throw new Error(`${registry}: HTTP ${res.status}`);
20822
+ return await res.json();
20823
+ } catch (err) {
20824
+ lastError = err;
20825
+ }
20826
+ }
20827
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
20828
+ }
20829
+ var Updater = class {
20830
+ constructor(current = currentVersion(), fetchImpl = fetch, now = Date.now) {
20831
+ this.current = current;
20832
+ this.fetchImpl = fetchImpl;
20833
+ this.now = now;
20834
+ }
20835
+ current;
20836
+ fetchImpl;
20837
+ now;
20838
+ manager;
20839
+ running = false;
20840
+ async check(signal) {
20841
+ const doc = await fetchPackument(await registriesOf(this.manager), signal, this.fetchImpl);
20842
+ return planUpdate(this.current, doc, this.now());
20843
+ }
20844
+ async update(signal) {
20845
+ const manager = this.manager;
20846
+ if (!manager) return { ok: false, error: "unsupported" };
20847
+ if (this.running) return { ok: false, error: "busy" };
20848
+ this.running = true;
20849
+ try {
20850
+ let info;
20851
+ try {
20852
+ info = await this.check(signal);
20853
+ } catch (err) {
20854
+ return { ok: false, error: "check_failed", message: messageOf(err) };
20855
+ }
20856
+ if (!info.target) return { ok: true, status: info.availableAt ? "too-new" : "up-to-date", info };
20857
+ const result = await manager.installBundle(`${PACKAGE_NAME}@${info.target}`);
20858
+ if (result.application === "applied" || result.application === "restart-required") {
20859
+ return { ok: true, status: "updated", info, version: info.target, restartRequired: result.application === "restart-required" };
20860
+ }
20861
+ const output = result.packageResult?.output?.trim();
20862
+ return { ok: false, error: "install_failed", info, message: tail(output || messageOf(result.error) || result.application) };
20863
+ } finally {
20864
+ this.running = false;
20865
+ }
20866
+ }
20867
+ };
20868
+ function messageOf(err) {
20869
+ if (err instanceof Error) return err.message;
20870
+ if (err && typeof err === "object" && "message" in err && typeof err.message === "string") return err.message;
20871
+ return err === void 0 ? "" : String(err);
20872
+ }
20873
+ function tail(text, max = 600) {
20874
+ return text.length > max ? `\u2026${text.slice(-max)}` : text;
20875
+ }
20876
+
20746
20877
  // src/index.ts
20747
20878
  var name = "dsh-vpn";
20748
20879
  var inject = ["tools", "credentials"];
@@ -20852,8 +20983,15 @@ function apply(ctx, config) {
20852
20983
  };
20853
20984
  });
20854
20985
  }
20986
+ const updater = new Updater();
20987
+ ctx.inject(["pluginManager"], (pmCtx) => {
20988
+ updater.manager = pmCtx.pluginManager;
20989
+ pmCtx.effect(() => () => {
20990
+ updater.manager = void 0;
20991
+ }, "dsh-vpn: plugin manager");
20992
+ });
20855
20993
  ctx.inject(["connection"], (webCtx) => {
20856
- registerApi(webCtx, service, uiLang, persistUiLang);
20994
+ registerApi(webCtx, service, uiLang, persistUiLang, updater);
20857
20995
  });
20858
20996
  }
20859
20997
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vpn",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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",