dsh-vpn 0.1.5 → 0.1.7
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 +4 -0
- package/lib/client.js +124 -9
- package/lib/index.js +171 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,10 @@ dsh plugin --profile web add dsh-vpn
|
|
|
38
38
|
3. 在「节点二维码」里点「刷新二维码」,用手机或电脑上的客户端扫码导入。
|
|
39
39
|
4. 服务器防火墙的端口由插件自动放行,不用手动操作;安装完成后提示框会列出放行了哪些端口。
|
|
40
40
|
|
|
41
|
+
**找回已有节点**:如果重装过 dsh 或 dsh-vpn,本地保存的设置会丢失,但 VPS 上的节点还在。重新填写服务器 IP、端口、用户名和密码后点「保存」,插件会自动检查这台服务器上是否已有安装(也可以随时点「检查已有安装」)。找到后直接显示原来的节点二维码,并按服务器上的实际情况勾选协议和证书类型,已经导入手机或电脑客户端的配置可以继续使用,不用重新扫码。
|
|
42
|
+
|
|
43
|
+
页面第一行显示插件版本,有新版本时旁边会提示,点「更新」即可升级插件,重启 dsh 后生效(需要 dsh 0.2 及以上;更早的版本请在「设置 → 插件」中更新)。为了供应链安全,发布不足 24 小时的新版本会等满 24 小时再提供更新。
|
|
44
|
+
|
|
41
45
|
## 语言
|
|
42
46
|
|
|
43
47
|
整个插件的语言跟随 dsh 的界面语言(「设置 → 通用设置 → 语言」),目前支持 dsh 提供的中文和 English:
|
package/lib/client.js
CHANGED
|
@@ -54,8 +54,14 @@ var vpnApi = {
|
|
|
54
54
|
test: () => post("/test"),
|
|
55
55
|
status: () => post("/status"),
|
|
56
56
|
links: () => post("/links"),
|
|
57
|
+
/** Find an earlier installation on the server and adopt its protocols and certificate mode. */
|
|
58
|
+
detect: () => post("/detect"),
|
|
57
59
|
install: (regenerate) => post("/install", { regenerate }),
|
|
58
60
|
uninstall: () => post("/uninstall"),
|
|
61
|
+
/** Installed version; with `check`, also the newest release on npm. */
|
|
62
|
+
version: (check = false) => post("/version", { check }),
|
|
63
|
+
/** Install the newest release through dsh's plugin manager. */
|
|
64
|
+
update: () => post("/update"),
|
|
59
65
|
/** Tell the host which language dsh's UI shows (approval prompts follow it). */
|
|
60
66
|
reportUiLocale: (locale) => post("/ui-locale", { locale })
|
|
61
67
|
};
|
|
@@ -66,6 +72,17 @@ var zh = {
|
|
|
66
72
|
title: "VPN \u8282\u70B9",
|
|
67
73
|
nav: "VPN \u8BBE\u7F6E",
|
|
68
74
|
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",
|
|
75
|
+
version: "\u7248\u672C v{version}",
|
|
76
|
+
update: "\u66F4\u65B0",
|
|
77
|
+
updating: "\u6B63\u5728\u66F4\u65B0\u2026",
|
|
78
|
+
"update.latest": "\u5DF2\u662F\u6700\u65B0\u7248\u672C",
|
|
79
|
+
"update.available": "\u6709\u65B0\u7248\u672C v{version}",
|
|
80
|
+
"update.tooNew": "\u65B0\u7248\u672C v{version} \u521A\u53D1\u5E03\uFF0C\u4E3A\u5B89\u5168\u8D77\u89C1 {time} \u540E\u53EF\u66F4\u65B0",
|
|
81
|
+
"update.done": "\u5DF2\u66F4\u65B0\u5230 v{version}\uFF0C\u91CD\u542F dsh \u540E\u751F\u6548",
|
|
82
|
+
"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",
|
|
83
|
+
"update.busy": "\u6B63\u5728\u66F4\u65B0\uFF0C\u8BF7\u7A0D\u5019\u3002",
|
|
84
|
+
"update.check_failed": "\u65E0\u6CD5\u68C0\u67E5\u65B0\u7248\u672C\uFF1A{message}",
|
|
85
|
+
"update.install_failed": "\u66F4\u65B0\u5931\u8D25\uFF1A{message}",
|
|
69
86
|
serverSection: "\u670D\u52A1\u5668",
|
|
70
87
|
host: "\u670D\u52A1\u5668 IP \u6216\u57DF\u540D",
|
|
71
88
|
hostHint: "\u4F60\u8D2D\u4E70\u7684 VPS \u7684\u516C\u7F51\u5730\u5740\u3002",
|
|
@@ -117,6 +134,10 @@ var zh = {
|
|
|
117
134
|
working: "\u6267\u884C\u4E2D\u2026",
|
|
118
135
|
qrSection: "\u8282\u70B9\u4E8C\u7EF4\u7801",
|
|
119
136
|
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",
|
|
137
|
+
detect: "\u68C0\u67E5\u5DF2\u6709\u5B89\u88C5",
|
|
138
|
+
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",
|
|
139
|
+
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",
|
|
140
|
+
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",
|
|
120
141
|
refresh: "\u5237\u65B0\u4E8C\u7EF4\u7801",
|
|
121
142
|
refreshing: "\u6B63\u5728\u5237\u65B0\u2026",
|
|
122
143
|
refreshedAt: "\u4E0A\u6B21\u5237\u65B0 {time}",
|
|
@@ -157,6 +178,17 @@ var en = {
|
|
|
157
178
|
title: "VPN node",
|
|
158
179
|
nav: "VPN",
|
|
159
180
|
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.",
|
|
181
|
+
version: "Version v{version}",
|
|
182
|
+
update: "Update",
|
|
183
|
+
updating: "Updating\u2026",
|
|
184
|
+
"update.latest": "You have the latest version",
|
|
185
|
+
"update.available": "v{version} is available",
|
|
186
|
+
"update.tooNew": "v{version} was just released; for safety it can be installed after {time}",
|
|
187
|
+
"update.done": "Updated to v{version}. Restart dsh to use it.",
|
|
188
|
+
"update.unsupported": "This dsh version cannot update plugins from here. Update dsh-vpn in Settings \u2192 Plugins.",
|
|
189
|
+
"update.busy": "An update is already running.",
|
|
190
|
+
"update.check_failed": "Could not check for updates: {message}",
|
|
191
|
+
"update.install_failed": "Update failed: {message}",
|
|
160
192
|
serverSection: "Server",
|
|
161
193
|
host: "Server IP or domain",
|
|
162
194
|
hostHint: "Public address of your VPS.",
|
|
@@ -208,6 +240,10 @@ var en = {
|
|
|
208
240
|
working: "Working\u2026",
|
|
209
241
|
qrSection: "Node QR codes",
|
|
210
242
|
qrHint: "Scan with v2rayN, NekoBox, Shadowrocket, sing-box, Clash Meta and similar apps. A QR code is as secret as a password.",
|
|
243
|
+
detect: "Find existing install",
|
|
244
|
+
detectFound: "Found an earlier installation: sing-box {version}, protocols: {protocols} ({cert}). Its QR codes are shown below; clients you already set up keep working.",
|
|
245
|
+
detectSynced: "The protocol selection and certificate type now match the server.",
|
|
246
|
+
detectNone: "No earlier installation was found on this server. Click Install / Update to set one up.",
|
|
211
247
|
refresh: "Refresh QR codes",
|
|
212
248
|
refreshing: "Refreshing\u2026",
|
|
213
249
|
refreshedAt: "Last refreshed {time}",
|
|
@@ -282,7 +318,11 @@ var STYLES = `
|
|
|
282
318
|
.dshvpn-qr svg { width: 180px; height: 180px; }
|
|
283
319
|
.dshvpn-link { display: block; max-width: 100%; max-height: 3.2em; overflow: hidden; color: var(--dsw-alias-label-secondary, #666); }
|
|
284
320
|
.dshvpn-section { display: flex; flex-direction: column; gap: 16px; }
|
|
285
|
-
.dshvpn-section-head h2 { margin: 0
|
|
321
|
+
.dshvpn-section-head h2 { margin: 0; font-size: 18px; font-weight: 600; }
|
|
322
|
+
.dshvpn-title-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 4px; }
|
|
323
|
+
.dshvpn-version { font-size: 12px; font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-secondary, #666); }
|
|
324
|
+
.dshvpn-update-note { font-size: 12px; color: var(--dsw-alias-label-secondary, #666); }
|
|
325
|
+
.dshvpn-update-note.dshvpn-new { color: var(--dsw-alias-state-success-primary, #2f855a); font-weight: 600; }
|
|
286
326
|
.dshvpn-section-head p { margin: 0; }
|
|
287
327
|
.dshvpn-qr-section { padding: 14px; border-radius: 12px; border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.3)); }
|
|
288
328
|
.dshvpn-qr-section h3 { font-size: 15px; }
|
|
@@ -296,6 +336,10 @@ var STYLES = `
|
|
|
296
336
|
.dshvpn-qr-empty-icon { font-size: 36px; line-height: 1; color: var(--dsw-alias-label-tertiary, #888); }
|
|
297
337
|
`;
|
|
298
338
|
|
|
339
|
+
// src/client/VpnSettingsSection.tsx
|
|
340
|
+
var import_react2 = require("react");
|
|
341
|
+
var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
342
|
+
|
|
299
343
|
// src/client/VpnSettingsPage.tsx
|
|
300
344
|
var import_react = require("react");
|
|
301
345
|
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
@@ -1099,11 +1143,35 @@ function VpnSettingsPage({ view, t, api }) {
|
|
|
1099
1143
|
setBusy(null);
|
|
1100
1144
|
}
|
|
1101
1145
|
}
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1146
|
+
const detect = async () => {
|
|
1147
|
+
await op("detect", api.detect, (r) => {
|
|
1148
|
+
const res = r.result ?? {};
|
|
1149
|
+
if (res.installed !== true) return { tone: "info", text: t("detectNone") };
|
|
1150
|
+
if (r.links) {
|
|
1151
|
+
setLinks(r.links);
|
|
1152
|
+
setRefreshedAt(/* @__PURE__ */ new Date());
|
|
1153
|
+
}
|
|
1154
|
+
const protocols = (res.protocols ?? []).map((id) => PROTOCOLS.find((p) => p.id === id)?.name ?? id).join("\u3001");
|
|
1155
|
+
const cert = res.certMode === "ip" ? t("certIp") : t("certSelf");
|
|
1156
|
+
const text = t("detectFound", { version: String(res.version ?? ""), protocols, cert });
|
|
1157
|
+
return { tone: "success", text: res.synced === true ? `${text}
|
|
1158
|
+
${t("detectSynced")}` : text };
|
|
1159
|
+
});
|
|
1160
|
+
const s = await api.getSettings();
|
|
1161
|
+
if (s.ok) {
|
|
1162
|
+
setSaved(s.settings);
|
|
1163
|
+
setForm(formOf(s.settings));
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
const save = async () => {
|
|
1167
|
+
const first = !saved.configured;
|
|
1168
|
+
const r = await op("save", () => api.saveSettings(form), (r2) => {
|
|
1169
|
+
setSaved(r2.settings);
|
|
1170
|
+
setForm(formOf(r2.settings));
|
|
1171
|
+
return { tone: "success", text: t("saved") };
|
|
1172
|
+
});
|
|
1173
|
+
if (r.ok && first) await detect();
|
|
1174
|
+
};
|
|
1107
1175
|
const refreshSettings = async () => {
|
|
1108
1176
|
const r = await api.getSettings();
|
|
1109
1177
|
if (r.ok) {
|
|
@@ -1266,6 +1334,9 @@ ${t("cloudHint")}`;
|
|
|
1266
1334
|
children: t("test")
|
|
1267
1335
|
}
|
|
1268
1336
|
),
|
|
1337
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "outline", disabled: disabled || needsSave, onClick: () => {
|
|
1338
|
+
void detect();
|
|
1339
|
+
}, children: busy === "detect" ? t("working") : t("detect") }),
|
|
1269
1340
|
dirty && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-warn", children: t("unsaved") })
|
|
1270
1341
|
] }),
|
|
1271
1342
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
|
|
@@ -1377,13 +1448,57 @@ ${fw.backend === "none" ? t("fwNone") : t("fwStatus", { backend: fw.backend, cou
|
|
|
1377
1448
|
// src/client/VpnSettingsSection.tsx
|
|
1378
1449
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
1379
1450
|
function VpnSettingsSection(props) {
|
|
1380
|
-
const { t } = props;
|
|
1451
|
+
const { t, api } = props;
|
|
1452
|
+
const [info, setInfo] = (0, import_react2.useState)(null);
|
|
1453
|
+
const [note, setNote] = (0, import_react2.useState)(null);
|
|
1454
|
+
const [updating, setUpdating] = (0, import_react2.useState)(false);
|
|
1455
|
+
const when = (iso) => new Date(iso).toLocaleString();
|
|
1456
|
+
const describe = (v) => {
|
|
1457
|
+
if (v.target) return { text: t("update.available", { version: v.target }), fresh: true };
|
|
1458
|
+
if (v.availableAt && v.latest) return { text: t("update.tooNew", { version: v.latest, time: when(v.availableAt) }) };
|
|
1459
|
+
return null;
|
|
1460
|
+
};
|
|
1461
|
+
(0, import_react2.useEffect)(() => {
|
|
1462
|
+
let live = true;
|
|
1463
|
+
void api.version().then((r) => {
|
|
1464
|
+
if (live && r.ok) setInfo(r);
|
|
1465
|
+
});
|
|
1466
|
+
void api.version(true).then((r) => {
|
|
1467
|
+
if (!live || !r.ok) return;
|
|
1468
|
+
setInfo(r);
|
|
1469
|
+
setNote(describe(r));
|
|
1470
|
+
});
|
|
1471
|
+
return () => {
|
|
1472
|
+
live = false;
|
|
1473
|
+
};
|
|
1474
|
+
}, [api]);
|
|
1475
|
+
const update = async () => {
|
|
1476
|
+
setUpdating(true);
|
|
1477
|
+
setNote(null);
|
|
1478
|
+
const r = await api.update();
|
|
1479
|
+
setUpdating(false);
|
|
1480
|
+
if (!r.ok) {
|
|
1481
|
+
const known = ["unsupported", "busy", "check_failed"].includes(r.error) ? r.error : "install_failed";
|
|
1482
|
+
setNote({ text: t(`update.${known}`, { message: r.message ?? r.error }) });
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
setInfo((prev) => ({ ...prev, ...r.info, canUpdate: prev?.canUpdate ?? true }));
|
|
1486
|
+
if (r.status === "updated") setNote({ text: t("update.done", { version: r.version }), fresh: true });
|
|
1487
|
+
else setNote(describe(r.info) ?? { text: t("update.latest") });
|
|
1488
|
+
};
|
|
1381
1489
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dshvpn-section", children: [
|
|
1382
1490
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("header", { className: "dshvpn-section-head", children: [
|
|
1383
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.
|
|
1491
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dshvpn-title-row", children: [
|
|
1492
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { children: t("nav") }),
|
|
1493
|
+
info?.current && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dshvpn-version", children: t("version", { version: info.current }) }),
|
|
1494
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives2.Button, { size: "sm", variant: "outline", disabled: updating, onClick: () => {
|
|
1495
|
+
void update();
|
|
1496
|
+
}, children: updating ? t("updating") : t("update") }),
|
|
1497
|
+
note && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: `dshvpn-update-note${note.fresh ? " dshvpn-new" : ""}`, children: note.text })
|
|
1498
|
+
] }),
|
|
1384
1499
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dshvpn-muted", children: t("description") })
|
|
1385
1500
|
] }),
|
|
1386
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(VpnSettingsPage, { t, api
|
|
1501
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(VpnSettingsPage, { t, api, view: "page" })
|
|
1387
1502
|
] });
|
|
1388
1503
|
}
|
|
1389
1504
|
|
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?.();
|
|
@@ -20287,6 +20298,7 @@ function apiHandlers(service, uiLang, onUiLang) {
|
|
|
20287
20298
|
"/test": (_body, signal) => service.test(signal),
|
|
20288
20299
|
"/status": (_body, signal) => service.status(signal),
|
|
20289
20300
|
"/links": (_body, signal) => service.links(signal),
|
|
20301
|
+
"/detect": (_body, signal) => service.detect(signal),
|
|
20290
20302
|
"/install": (body, signal) => service.install({ regenerate: body.regenerate === true }, signal),
|
|
20291
20303
|
"/uninstall": (_body, signal) => service.uninstall(signal)
|
|
20292
20304
|
};
|
|
@@ -20310,8 +20322,8 @@ async function handle(handler, request) {
|
|
|
20310
20322
|
return json(500, { ok: false, error: "internal", message: err instanceof Error ? err.message : String(err) });
|
|
20311
20323
|
}
|
|
20312
20324
|
}
|
|
20313
|
-
function registerApi(ctx, service, uiLang, onUiLang) {
|
|
20314
|
-
for (const [path, handler] of Object.entries(apiHandlers(service, uiLang, onUiLang))) {
|
|
20325
|
+
function registerApi(ctx, service, uiLang, onUiLang, updater) {
|
|
20326
|
+
for (const [path, handler] of Object.entries(apiHandlers(service, uiLang, onUiLang, updater))) {
|
|
20315
20327
|
ctx.effect(() => ctx.connection.fetch.register({
|
|
20316
20328
|
path: `${API_BASE}${path}`,
|
|
20317
20329
|
methods: ["POST"],
|
|
@@ -20604,6 +20616,34 @@ var VpnService = class {
|
|
|
20604
20616
|
links(signal) {
|
|
20605
20617
|
return this.run("links", (s) => ["--server", s.linkAddress ?? s.host], signal);
|
|
20606
20618
|
}
|
|
20619
|
+
/**
|
|
20620
|
+
* Find an existing installation (e.g. after dsh-vpn was removed and its local
|
|
20621
|
+
* settings lost): read the server's protocols, certificate mode and links,
|
|
20622
|
+
* and adopt the protocols and certificate mode into local settings so a
|
|
20623
|
+
* later 安装 / 更新 keeps the node as it is.
|
|
20624
|
+
*/
|
|
20625
|
+
async detect(signal) {
|
|
20626
|
+
const status = await this.status(signal);
|
|
20627
|
+
if (!status.ok || status.result?.installed !== true) return status.ok ? { ok: true, result: { installed: false } } : status;
|
|
20628
|
+
const r = status.result;
|
|
20629
|
+
const protocols = String(r.protocols ?? "").split(",").filter(isProtocolId);
|
|
20630
|
+
const mode = r.cert?.mode;
|
|
20631
|
+
const certMode = mode === "ip" || mode === "self" ? mode : void 0;
|
|
20632
|
+
let synced = false;
|
|
20633
|
+
await this.store.update((cur) => {
|
|
20634
|
+
if (!cur) return void 0;
|
|
20635
|
+
const next = { ...cur, ...protocols.length > 0 ? { protocols } : {}, ...certMode ? { certMode } : {} };
|
|
20636
|
+
synced = next.certMode !== cur.certMode || next.protocols.join() !== cur.protocols.join();
|
|
20637
|
+
return synced ? next : void 0;
|
|
20638
|
+
});
|
|
20639
|
+
const links = await this.links(signal);
|
|
20640
|
+
return {
|
|
20641
|
+
ok: links.ok,
|
|
20642
|
+
...links.error ? { error: links.error, message: links.message } : {},
|
|
20643
|
+
result: { installed: true, active: r.active, version: r.version, protocols, certMode, synced },
|
|
20644
|
+
...links.links ? { links: links.links } : {}
|
|
20645
|
+
};
|
|
20646
|
+
}
|
|
20607
20647
|
uninstall(signal) {
|
|
20608
20648
|
return this.run("uninstall", () => [], signal);
|
|
20609
20649
|
}
|
|
@@ -20743,6 +20783,126 @@ function notConfiguredMessage(lang) {
|
|
|
20743
20783
|
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
20784
|
}
|
|
20745
20785
|
|
|
20786
|
+
// src/update.ts
|
|
20787
|
+
import { readFileSync } from "node:fs";
|
|
20788
|
+
var PACKAGE_NAME = "dsh-vpn";
|
|
20789
|
+
var MIN_RELEASE_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
20790
|
+
var NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
20791
|
+
function currentVersion() {
|
|
20792
|
+
try {
|
|
20793
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
20794
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
20795
|
+
} catch {
|
|
20796
|
+
return "0.0.0";
|
|
20797
|
+
}
|
|
20798
|
+
}
|
|
20799
|
+
var RELEASE = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
20800
|
+
function compareVersions(a, b) {
|
|
20801
|
+
const pa = RELEASE.exec(a);
|
|
20802
|
+
const pb = RELEASE.exec(b);
|
|
20803
|
+
if (!pa || !pb) return 0;
|
|
20804
|
+
for (let i = 1; i <= 3; i++) {
|
|
20805
|
+
const d = Number(pa[i]) - Number(pb[i]);
|
|
20806
|
+
if (d !== 0) return d;
|
|
20807
|
+
}
|
|
20808
|
+
return 0;
|
|
20809
|
+
}
|
|
20810
|
+
function planUpdate(current, doc, now = Date.now()) {
|
|
20811
|
+
const latest = doc["dist-tags"]?.latest;
|
|
20812
|
+
const info = { current };
|
|
20813
|
+
if (typeof latest !== "string" || !RELEASE.test(latest)) return info;
|
|
20814
|
+
info.latest = latest;
|
|
20815
|
+
if (compareVersions(latest, current) <= 0) return info;
|
|
20816
|
+
const published = (v) => Date.parse(doc.time?.[v] ?? "");
|
|
20817
|
+
const mature = (v) => {
|
|
20818
|
+
const t = published(v);
|
|
20819
|
+
return Number.isFinite(t) && now - t >= MIN_RELEASE_AGE_MS;
|
|
20820
|
+
};
|
|
20821
|
+
const candidates = Object.keys(doc.versions ?? { [latest]: {} }).filter((v) => RELEASE.test(v) && compareVersions(v, current) > 0 && compareVersions(v, latest) <= 0 && mature(v)).sort(compareVersions);
|
|
20822
|
+
const target = candidates.at(-1);
|
|
20823
|
+
if (target) info.target = target;
|
|
20824
|
+
if (target !== latest) {
|
|
20825
|
+
const t = published(latest);
|
|
20826
|
+
if (Number.isFinite(t)) info.availableAt = new Date(t + MIN_RELEASE_AGE_MS).toISOString();
|
|
20827
|
+
}
|
|
20828
|
+
return info;
|
|
20829
|
+
}
|
|
20830
|
+
async function registriesOf(manager) {
|
|
20831
|
+
const list = [];
|
|
20832
|
+
if (manager) {
|
|
20833
|
+
try {
|
|
20834
|
+
const r = await manager.registries();
|
|
20835
|
+
for (const url of [r.resolved, r.registry, ...r.fallbackRegistries]) if (url) list.push(url);
|
|
20836
|
+
} catch {
|
|
20837
|
+
}
|
|
20838
|
+
}
|
|
20839
|
+
list.push(NPM_REGISTRY);
|
|
20840
|
+
return [...new Set(list.map((u) => u.endsWith("/") ? u : `${u}/`))];
|
|
20841
|
+
}
|
|
20842
|
+
async function fetchPackument(registries, signal, fetchImpl = fetch) {
|
|
20843
|
+
let lastError;
|
|
20844
|
+
for (const registry of registries) {
|
|
20845
|
+
try {
|
|
20846
|
+
const res = await fetchImpl(`${registry}${PACKAGE_NAME}`, {
|
|
20847
|
+
headers: { accept: "application/json" },
|
|
20848
|
+
signal: AbortSignal.any([AbortSignal.timeout(15e3), ...signal ? [signal] : []])
|
|
20849
|
+
});
|
|
20850
|
+
if (!res.ok) throw new Error(`${registry}: HTTP ${res.status}`);
|
|
20851
|
+
return await res.json();
|
|
20852
|
+
} catch (err) {
|
|
20853
|
+
lastError = err;
|
|
20854
|
+
}
|
|
20855
|
+
}
|
|
20856
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
20857
|
+
}
|
|
20858
|
+
var Updater = class {
|
|
20859
|
+
constructor(current = currentVersion(), fetchImpl = fetch, now = Date.now) {
|
|
20860
|
+
this.current = current;
|
|
20861
|
+
this.fetchImpl = fetchImpl;
|
|
20862
|
+
this.now = now;
|
|
20863
|
+
}
|
|
20864
|
+
current;
|
|
20865
|
+
fetchImpl;
|
|
20866
|
+
now;
|
|
20867
|
+
manager;
|
|
20868
|
+
running = false;
|
|
20869
|
+
async check(signal) {
|
|
20870
|
+
const doc = await fetchPackument(await registriesOf(this.manager), signal, this.fetchImpl);
|
|
20871
|
+
return planUpdate(this.current, doc, this.now());
|
|
20872
|
+
}
|
|
20873
|
+
async update(signal) {
|
|
20874
|
+
const manager = this.manager;
|
|
20875
|
+
if (!manager) return { ok: false, error: "unsupported" };
|
|
20876
|
+
if (this.running) return { ok: false, error: "busy" };
|
|
20877
|
+
this.running = true;
|
|
20878
|
+
try {
|
|
20879
|
+
let info;
|
|
20880
|
+
try {
|
|
20881
|
+
info = await this.check(signal);
|
|
20882
|
+
} catch (err) {
|
|
20883
|
+
return { ok: false, error: "check_failed", message: messageOf(err) };
|
|
20884
|
+
}
|
|
20885
|
+
if (!info.target) return { ok: true, status: info.availableAt ? "too-new" : "up-to-date", info };
|
|
20886
|
+
const result = await manager.installBundle(`${PACKAGE_NAME}@${info.target}`);
|
|
20887
|
+
if (result.application === "applied" || result.application === "restart-required") {
|
|
20888
|
+
return { ok: true, status: "updated", info, version: info.target, restartRequired: result.application === "restart-required" };
|
|
20889
|
+
}
|
|
20890
|
+
const output = result.packageResult?.output?.trim();
|
|
20891
|
+
return { ok: false, error: "install_failed", info, message: tail(output || messageOf(result.error) || result.application) };
|
|
20892
|
+
} finally {
|
|
20893
|
+
this.running = false;
|
|
20894
|
+
}
|
|
20895
|
+
}
|
|
20896
|
+
};
|
|
20897
|
+
function messageOf(err) {
|
|
20898
|
+
if (err instanceof Error) return err.message;
|
|
20899
|
+
if (err && typeof err === "object" && "message" in err && typeof err.message === "string") return err.message;
|
|
20900
|
+
return err === void 0 ? "" : String(err);
|
|
20901
|
+
}
|
|
20902
|
+
function tail(text, max = 600) {
|
|
20903
|
+
return text.length > max ? `\u2026${text.slice(-max)}` : text;
|
|
20904
|
+
}
|
|
20905
|
+
|
|
20746
20906
|
// src/index.ts
|
|
20747
20907
|
var name = "dsh-vpn";
|
|
20748
20908
|
var inject = ["tools", "credentials"];
|
|
@@ -20852,8 +21012,15 @@ function apply(ctx, config) {
|
|
|
20852
21012
|
};
|
|
20853
21013
|
});
|
|
20854
21014
|
}
|
|
21015
|
+
const updater = new Updater();
|
|
21016
|
+
ctx.inject(["pluginManager"], (pmCtx) => {
|
|
21017
|
+
updater.manager = pmCtx.pluginManager;
|
|
21018
|
+
pmCtx.effect(() => () => {
|
|
21019
|
+
updater.manager = void 0;
|
|
21020
|
+
}, "dsh-vpn: plugin manager");
|
|
21021
|
+
});
|
|
20855
21022
|
ctx.inject(["connection"], (webCtx) => {
|
|
20856
|
-
registerApi(webCtx, service, uiLang, persistUiLang);
|
|
21023
|
+
registerApi(webCtx, service, uiLang, persistUiLang, updater);
|
|
20857
21024
|
});
|
|
20858
21025
|
}
|
|
20859
21026
|
export {
|
package/package.json
CHANGED