dsh-vpn 0.1.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.
package/lib/client.js ADDED
@@ -0,0 +1,1346 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-vpn",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ "use strict";
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
+
25
+ // src/client/index.ts
26
+ var index_exports = {};
27
+ __export(index_exports, {
28
+ PACKAGE_NAME: () => PACKAGE_NAME,
29
+ apply: () => apply,
30
+ inject: () => inject
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+
34
+ // src/client/api.ts
35
+ async function post(path, body = {}) {
36
+ try {
37
+ const res = await fetch(new URL(`/api/dsh-vpn${path}`, document.baseURI), {
38
+ method: "POST",
39
+ credentials: "same-origin",
40
+ headers: { "content-type": "application/json" },
41
+ body: JSON.stringify(body)
42
+ });
43
+ const value = await res.json().catch(() => null);
44
+ if (value && typeof value === "object" && "ok" in value) return value;
45
+ return { ok: false, error: "http", message: `HTTP ${res.status}` };
46
+ } catch (err) {
47
+ return { ok: false, error: "network", message: err instanceof Error ? err.message : String(err) };
48
+ }
49
+ }
50
+ var vpnApi = {
51
+ getSettings: () => post("/settings/get"),
52
+ saveSettings: (form) => post("/settings/save", form),
53
+ forgetHostKey: () => post("/host-key/forget"),
54
+ test: () => post("/test"),
55
+ status: () => post("/status"),
56
+ links: () => post("/links"),
57
+ install: (regenerate) => post("/install", { regenerate }),
58
+ uninstall: () => post("/uninstall")
59
+ };
60
+
61
+ // src/client/locales.ts
62
+ var NS = "dsh-vpn";
63
+ var zh = {
64
+ title: "VPN \u8282\u70B9",
65
+ description: "\u5728\u4F60\u7684 VPS \u4E0A\u4E00\u952E\u90E8\u7F72 sing-box\uFF0C\u5E76\u7528\u4E8C\u7EF4\u7801\u5BFC\u5165\u624B\u673A\u6216\u7535\u8111\u4E0A\u7684\u4EE3\u7406\u5BA2\u6237\u7AEF\u3002",
66
+ serverSection: "\u670D\u52A1\u5668",
67
+ host: "\u670D\u52A1\u5668 IP \u6216\u57DF\u540D",
68
+ hostHint: "\u4F60\u8D2D\u4E70\u7684 VPS \u7684\u516C\u7F51\u5730\u5740\u3002",
69
+ port: "SSH \u7AEF\u53E3",
70
+ username: "\u7528\u6237\u540D",
71
+ usernameHint: "\u5EFA\u8BAE\u7528 root\uFF1B\u5176\u4ED6\u7528\u6237\u9700\u8981\u6709 sudo \u6743\u9650\u3002",
72
+ password: "\u5BC6\u7801",
73
+ passwordSaved: "\u5DF2\u4FDD\u5B58\uFF08\u7559\u7A7A\u5219\u4E0D\u4FEE\u6539\uFF09",
74
+ passwordEmpty: "\u767B\u5F55 VPS \u7684\u5BC6\u7801",
75
+ linkAddress: "\u8282\u70B9\u5730\u5740\uFF08\u53EF\u9009\uFF09",
76
+ linkAddressHint: "\u5199\u8FDB\u8282\u70B9\u94FE\u63A5\u7684\u5730\u5740\uFF0C\u7559\u7A7A\u5219\u4F7F\u7528\u670D\u52A1\u5668 IP\u3002\u7528\u4E8E NAT \u673A\u5668\u6216\u57DF\u540D\u3002",
77
+ hostKey: "\u4E3B\u673A\u6307\u7EB9",
78
+ hostKeyHint: "\u9996\u6B21\u8FDE\u63A5\u65F6\u8BB0\u5F55\u3002\u4EE5\u540E\u5982\u679C\u6307\u7EB9\u53D8\u5316\u4F1A\u62D2\u7EDD\u8FDE\u63A5\uFF0C\u9632\u6B62\u5BC6\u7801\u88AB\u4E2D\u95F4\u4EBA\u622A\u83B7\u3002",
79
+ hostKeyNone: "\u5C1A\u672A\u8FDE\u63A5\u8FC7",
80
+ forgetHostKey: "\u91CD\u7F6E\u6307\u7EB9",
81
+ save: "\u4FDD\u5B58",
82
+ saved: "\u5DF2\u4FDD\u5B58",
83
+ test: "\u6D4B\u8BD5\u8FDE\u63A5",
84
+ testOk: "\u8FDE\u63A5\u6210\u529F\uFF1A{system}\uFF0C\u53EF\u4EE5\u83B7\u5F97 root \u6743\u9650\u3002",
85
+ unsaved: "\u6709\u672A\u4FDD\u5B58\u7684\u4FEE\u6539\uFF0C\u8BF7\u5148\u4FDD\u5B58\u3002",
86
+ protocolSection: "\u534F\u8BAE",
87
+ protocolHint: "\u52FE\u9009\u8981\u5B89\u88C5\u7684\u534F\u8BAE\uFF0C\u4FDD\u5B58\u540E\u70B9\u300C\u5B89\u88C5 / \u66F4\u65B0\u300D\u751F\u6548\u3002",
88
+ insecureTag: "\u9700\u8DF3\u8FC7\u8BC1\u4E66\u6821\u9A8C",
89
+ insecureHint: "\u8BE5\u534F\u8BAE\u7684\u5206\u4EAB\u94FE\u63A5\u65E0\u6CD5\u9501\u5B9A\u8BC1\u4E66\uFF0C\u5BA2\u6237\u7AEF\u4F1A\u8DF3\u8FC7\u8BC1\u4E66\u6821\u9A8C\uFF0C\u5B89\u5168\u6027\u8F83\u4F4E\u3002",
90
+ certSection: "\u8BC1\u4E66",
91
+ certHint: "\u7528\u4E8E Hysteria2\u3001TUIC\u3001AnyTLS\u3002Reality \u548C Shadowsocks \u4E0D\u9700\u8981\u8BC1\u4E66\u3002",
92
+ certSelf: "\u81EA\u7B7E\u8BC1\u4E66",
93
+ certSelfHint: "\u65E0\u9700\u4EFB\u4F55\u51C6\u5907\u3002Hysteria2 \u4F1A\u9501\u5B9A\u8BC1\u4E66\uFF1BTUIC \u548C AnyTLS \u7684\u5BA2\u6237\u7AEF\u9700\u8DF3\u8FC7\u8BC1\u4E66\u6821\u9A8C\u3002",
94
+ certIp: "IP \u8BC1\u4E66\uFF08\u63A8\u8350\uFF09",
95
+ certIpHint: "\u514D\u8D39\u5411 Let's Encrypt \u7533\u8BF7\u7B7E\u53D1\u7ED9\u670D\u52A1\u5668 IP \u7684\u6B63\u89C4\u8BC1\u4E66\uFF0C\u6240\u6709\u5BA2\u6237\u7AEF\u6B63\u5E38\u6821\u9A8C\uFF0C\u65E0\u9700\u57DF\u540D\u3002\u8BC1\u4E66\u6709\u6548\u671F 6 \u5929\uFF0Csing-box \u81EA\u52A8\u7EED\u671F\uFF0C\u9700\u8981\u5728\u5B89\u5168\u7EC4\u91CC\u4E00\u76F4\u5F00\u653E TCP 80 \u7AEF\u53E3\u3002",
96
+ certExpires: "IP \u8BC1\u4E66\u6709\u6548\u671F\u81F3 {date}\uFF08\u5269\u4F59\u7EA6 {hours} \u5C0F\u65F6\uFF09\uFF0C\u4F1A\u81EA\u52A8\u7EED\u671F\u3002",
97
+ certNotIssued: "IP \u8BC1\u4E66\u5C1A\u672A\u7B7E\u53D1\u3002",
98
+ certExpiring: "IP \u8BC1\u4E66\u5C06\u5728 {hours} \u5C0F\u65F6\u5185\u8FC7\u671F\uFF0C\u4F46\u8FD8\u6CA1\u6709\u7EED\u671F\u6210\u529F\u3002\u8BF7\u786E\u8BA4\u5B89\u5168\u7EC4\u4E00\u76F4\u5F00\u653E TCP 80 \u7AEF\u53E3\uFF0C\u5426\u5219\u8BC1\u4E66\u8FC7\u671F\u540E\u8282\u70B9\u5C06\u65E0\u6CD5\u8FDE\u63A5\u3002",
99
+ certExpired: "IP \u8BC1\u4E66\u5DF2\u8FC7\u671F\uFF0C\u5BA2\u6237\u7AEF\u5C06\u65E0\u6CD5\u8FDE\u63A5\uFF01\u8BF7\u5728\u5B89\u5168\u7EC4\u5F00\u653E TCP 80 \u7AEF\u53E3\u540E\u70B9\u300C\u5B89\u88C5 / \u66F4\u65B0\u300D\uFF0C\u6216\u6539\u7528\u81EA\u7B7E\u8BC1\u4E66\u3002",
100
+ deploySection: "\u90E8\u7F72",
101
+ install: "\u5B89\u88C5 / \u66F4\u65B0",
102
+ regenerate: "\u91CD\u65B0\u751F\u6210\u5BC6\u94A5\uFF08\u65E7\u94FE\u63A5\u5168\u90E8\u5931\u6548\uFF09",
103
+ uninstall: "\u5378\u8F7D",
104
+ confirmUninstall: "\u786E\u8BA4\u5378\u8F7D\uFF1F",
105
+ confirmRegenerate: "\u786E\u8BA4\u91CD\u65B0\u751F\u6210\uFF1F",
106
+ cancel: "\u53D6\u6D88",
107
+ status: "\u67E5\u770B\u72B6\u6001",
108
+ statusActive: "sing-box {version} \u8FD0\u884C\u4E2D\u3002",
109
+ statusInactive: "sing-box \u5DF2\u5B89\u88C5\u4F46\u672A\u8FD0\u884C\u3002",
110
+ statusNotInstalled: "\u5C1A\u672A\u5B89\u88C5\u3002",
111
+ listening: "\u76D1\u542C",
112
+ installed: "\u5B89\u88C5\u5B8C\u6210\u3002",
113
+ uninstalled: "\u5DF2\u5378\u8F7D\u3002",
114
+ working: "\u6267\u884C\u4E2D\u2026",
115
+ qrSection: "\u8282\u70B9\u4E8C\u7EF4\u7801",
116
+ 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",
117
+ refresh: "\u5237\u65B0",
118
+ qrEmpty: "\u70B9\u300C\u5237\u65B0\u300D\u4ECE\u670D\u52A1\u5668\u8BFB\u53D6\u8282\u70B9\u5E76\u751F\u6210\u4E8C\u7EF4\u7801\u3002",
119
+ copy: "\u590D\u5236\u94FE\u63A5",
120
+ copied: "\u5DF2\u590D\u5236",
121
+ portLabel: "\u7AEF\u53E3 {port}",
122
+ cloudFirewall: "\u63D0\u9192\uFF1A\u8FD8\u9700\u8981\u5728\u4E91\u670D\u52A1\u5546\u63A7\u5236\u53F0\u7684\u5B89\u5168\u7EC4\u91CC\u653E\u884C\u4EE5\u4E0B\u7AEF\u53E3\uFF1A{ports}",
123
+ "err.not_configured": "\u8BF7\u5148\u586B\u5199\u5E76\u4FDD\u5B58\u670D\u52A1\u5668\u4FE1\u606F\u3002",
124
+ "err.auth_failed": "SSH \u767B\u5F55\u5931\u8D25\uFF1A\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF\u3002",
125
+ "err.host_key_mismatch": "\u670D\u52A1\u5668\u6307\u7EB9\u4E0E\u4E0A\u6B21\u4E0D\u540C\uFF0C\u5DF2\u62D2\u7EDD\u8FDE\u63A5\u3002\u5982\u679C\u4F60\u91CD\u88C5\u4E86\u7CFB\u7EDF\u6216\u6362\u4E86\u673A\u5668\uFF0C\u8BF7\u70B9\u300C\u91CD\u7F6E\u6307\u7EB9\u300D\u540E\u91CD\u8BD5\u3002",
126
+ "err.connect_failed": "\u65E0\u6CD5\u8FDE\u63A5\u670D\u52A1\u5668\uFF1A{message}",
127
+ "err.timeout": "\u64CD\u4F5C\u8D85\u65F6\u3002",
128
+ "err.sudo_failed": "\u65E0\u6CD5\u83B7\u5F97 root \u6743\u9650\uFF1A\u8BE5\u7528\u6237\u6CA1\u6709 sudo \u6743\u9650\u6216\u5BC6\u7801\u9519\u8BEF\u3002",
129
+ "err.not_root": "\u65E0\u6CD5\u83B7\u5F97 root \u6743\u9650\u3002",
130
+ "err.no_systemd": "\u670D\u52A1\u5668\u7CFB\u7EDF\u4E0D\u53D7\u652F\u6301\uFF08\u9700\u8981 systemd\uFF09\u3002",
131
+ "err.download_failed": "\u670D\u52A1\u5668\u65E0\u6CD5\u4ECE GitHub \u4E0B\u8F7D sing-box\uFF1A{message}",
132
+ "err.not_installed": "\u670D\u52A1\u5668\u4E0A\u5C1A\u672A\u5B89\u88C5\uFF0C\u8BF7\u5148\u70B9\u300C\u5B89\u88C5 / \u66F4\u65B0\u300D\u3002",
133
+ "err.invalid": "\u586B\u5199\u6709\u8BEF\uFF1A{message}",
134
+ "err.cert_needs_ip": "IP \u8BC1\u4E66\u9700\u8981\u7528 IP \u5730\u5740\u4F5C\u4E3A\u8282\u70B9\u5730\u5740\uFF0C\u8BF7\u68C0\u67E5\u670D\u52A1\u5668 IP \u6216\u8282\u70B9\u5730\u5740\u3002",
135
+ "err.port80_in_use": "\u670D\u52A1\u5668\u7684 TCP 80 \u7AEF\u53E3\u5DF2\u88AB\u5176\u4ED6\u7A0B\u5E8F\u5360\u7528\uFF08{message}\uFF09\uFF0C\u65E0\u6CD5\u7533\u8BF7 IP \u8BC1\u4E66\u3002\u8BF7\u505C\u7528\u8BE5\u7A0B\u5E8F\u6216\u6539\u7528\u81EA\u7B7E\u8BC1\u4E66\u3002",
136
+ "err.cert_failed": "IP \u8BC1\u4E66\u7533\u8BF7\u5931\u8D25\uFF0C\u5DF2\u81EA\u52A8\u6539\u56DE\u81EA\u7B7E\u8BC1\u4E66\uFF0C\u8282\u70B9\u4ECD\u53EF\u4F7F\u7528\u3002\u5E38\u89C1\u539F\u56E0\u662F\u5B89\u5168\u7EC4\u6CA1\u6709\u5F00\u653E TCP 80 \u7AEF\u53E3\u3002\u8BE6\u60C5\uFF1A{message}",
137
+ "err.other": "\u5931\u8D25\uFF1A{message}",
138
+ "field.host": "\u670D\u52A1\u5668 IP \u6216\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E\u3002",
139
+ "field.port": "SSH \u7AEF\u53E3\u9700\u5728 1\u201365535 \u4E4B\u95F4\u3002",
140
+ "field.username": "\u7528\u6237\u540D\u683C\u5F0F\u4E0D\u6B63\u786E\u3002",
141
+ "field.password": "\u8BF7\u586B\u5199\u5BC6\u7801\u3002",
142
+ "field.protocols": "\u8BF7\u81F3\u5C11\u52FE\u9009\u4E00\u4E2A\u534F\u8BAE\u3002",
143
+ "field.linkAddress": "\u8282\u70B9\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u786E\u3002",
144
+ "field.certMode": "IP \u8BC1\u4E66\u9700\u8981\u7528 IP \u5730\u5740\u4F5C\u4E3A\u8282\u70B9\u5730\u5740\uFF1A\u8BF7\u628A\u300C\u8282\u70B9\u5730\u5740\u300D\u7559\u7A7A\uFF08\u4F7F\u7528\u670D\u52A1\u5668 IP\uFF09\u6216\u586B\u5199 IP\uFF0C\u6216\u6539\u7528\u81EA\u7B7E\u8BC1\u4E66\u3002"
145
+ };
146
+ var en = {
147
+ title: "VPN node",
148
+ description: "Deploy sing-box to your VPS in one click and import it into proxy apps by scanning QR codes.",
149
+ serverSection: "Server",
150
+ host: "Server IP or domain",
151
+ hostHint: "Public address of your VPS.",
152
+ port: "SSH port",
153
+ username: "Username",
154
+ usernameHint: "root is recommended; other users need sudo.",
155
+ password: "Password",
156
+ passwordSaved: "Saved (leave blank to keep)",
157
+ passwordEmpty: "Password for the VPS",
158
+ linkAddress: "Node address (optional)",
159
+ linkAddressHint: "Address written into node links; defaults to the server IP. For NAT machines or domains.",
160
+ hostKey: "Host fingerprint",
161
+ hostKeyHint: "Recorded on first connection. A changed fingerprint is refused so the password cannot be intercepted.",
162
+ hostKeyNone: "Not connected yet",
163
+ forgetHostKey: "Reset fingerprint",
164
+ save: "Save",
165
+ saved: "Saved",
166
+ test: "Test connection",
167
+ testOk: "Connected: {system}, root access OK.",
168
+ unsaved: "You have unsaved changes; save first.",
169
+ protocolSection: "Protocols",
170
+ protocolHint: 'Select protocols to install; save, then click "Install / update".',
171
+ insecureTag: "Skips cert check",
172
+ insecureHint: "Share links for this protocol cannot pin the certificate, so clients skip verification. Less secure.",
173
+ certSection: "Certificate",
174
+ certHint: "Used by Hysteria2, TUIC and AnyTLS. Reality and Shadowsocks need none.",
175
+ certSelf: "Self-signed",
176
+ certSelfHint: "No setup needed. Hysteria2 pins it; TUIC and AnyTLS clients must skip verification.",
177
+ certIp: "IP certificate (recommended)",
178
+ certIpHint: "A free Let's Encrypt certificate issued to the server IP: every client verifies it normally, no domain needed. Valid 6 days and renewed automatically by sing-box; keep TCP port 80 open in the security group.",
179
+ certExpires: "IP certificate valid until {date} (about {hours} h left), renewed automatically.",
180
+ certNotIssued: "IP certificate not issued yet.",
181
+ certExpiring: "The IP certificate expires within {hours} h and has not been renewed yet. Keep TCP port 80 open in the security group, or clients will fail once it expires.",
182
+ certExpired: 'The IP certificate has expired, so clients cannot connect! Open TCP port 80 in the security group and click "Install / update", or switch to self-signed.',
183
+ deploySection: "Deploy",
184
+ install: "Install / update",
185
+ regenerate: "Regenerate keys (old links stop working)",
186
+ uninstall: "Uninstall",
187
+ confirmUninstall: "Really uninstall?",
188
+ confirmRegenerate: "Really regenerate?",
189
+ cancel: "Cancel",
190
+ status: "Check status",
191
+ statusActive: "sing-box {version} is running.",
192
+ statusInactive: "sing-box is installed but not running.",
193
+ statusNotInstalled: "Not installed yet.",
194
+ listening: "Listening",
195
+ installed: "Installed.",
196
+ uninstalled: "Uninstalled.",
197
+ working: "Working\u2026",
198
+ qrSection: "Node QR codes",
199
+ qrHint: "Scan with v2rayN, NekoBox, Shadowrocket, sing-box, Clash Meta and similar apps. A QR code is as secret as a password.",
200
+ refresh: "Refresh",
201
+ qrEmpty: 'Click "Refresh" to read the nodes from the server and show QR codes.',
202
+ copy: "Copy link",
203
+ copied: "Copied",
204
+ portLabel: "Port {port}",
205
+ cloudFirewall: "Reminder: also open these ports in your cloud provider's security group: {ports}",
206
+ "err.not_configured": "Fill in and save the server details first.",
207
+ "err.auth_failed": "SSH login failed: wrong username or password.",
208
+ "err.host_key_mismatch": 'The server fingerprint changed, so the connection was refused. If you reinstalled or replaced the server, click "Reset fingerprint" and retry.',
209
+ "err.connect_failed": "Cannot connect to the server: {message}",
210
+ "err.timeout": "The operation timed out.",
211
+ "err.sudo_failed": "Cannot get root: the user lacks sudo or the password is wrong.",
212
+ "err.not_root": "Cannot get root.",
213
+ "err.no_systemd": "Unsupported server OS (systemd required).",
214
+ "err.download_failed": "The server cannot download sing-box from GitHub: {message}",
215
+ "err.not_installed": 'Nothing installed yet; click "Install / update" first.',
216
+ "err.invalid": "Invalid input: {message}",
217
+ "err.cert_needs_ip": "An IP certificate needs an IP address as the node address; check the server IP or node address.",
218
+ "err.port80_in_use": "TCP port 80 on the server is used by another program ({message}), so an IP certificate cannot be issued. Stop it or use self-signed.",
219
+ "err.cert_failed": "The IP certificate was not issued, so the node fell back to self-signed and still works. Usually TCP port 80 is not open in the security group. Details: {message}",
220
+ "err.other": "Failed: {message}",
221
+ "field.host": "The server IP or domain is not valid.",
222
+ "field.port": "The SSH port must be 1\u201365535.",
223
+ "field.username": "The username is not valid.",
224
+ "field.password": "Enter the password.",
225
+ "field.protocols": "Select at least one protocol.",
226
+ "field.linkAddress": "The node address is not valid.",
227
+ "field.certMode": 'An IP certificate needs an IP address as the node address: leave "Node address" empty (uses the server IP) or enter an IP, or use self-signed.'
228
+ };
229
+
230
+ // src/client/styles.ts
231
+ var STYLES = `
232
+ .dshvpn { display: flex; flex-direction: column; gap: 20px; max-width: 880px; color: var(--dsw-alias-label-primary, inherit); }
233
+ .dshvpn section { display: flex; flex-direction: column; gap: 8px; }
234
+ .dshvpn h3 { margin: 0; font-size: 14px; font-weight: 600; }
235
+ .dshvpn-muted { color: var(--dsw-alias-label-tertiary, #888); font-size: 12px; }
236
+ .dshvpn-warn { color: var(--dsw-alias-state-warn-primary, #b7791f); font-size: 12px; }
237
+ .dshvpn-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 16px; }
238
+ .dshvpn-grid label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
239
+ .dshvpn-grid .dshvpn-wide { grid-column: 1 / -1; }
240
+ @media (max-width: 640px) { .dshvpn-grid { grid-template-columns: 1fr; } }
241
+ .dshvpn-hostkey { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: 13px; }
242
+ .dshvpn-hostkey code, .dshvpn-link { font-family: var(--dsw-font-markdown-code-block, ui-monospace, monospace); font-size: 12px; word-break: break-all; }
243
+ .dshvpn-hostkey small { flex-basis: 100%; }
244
+ .dshvpn-protocols { display: flex; flex-direction: column; gap: 6px; }
245
+ .dshvpn-protocol { display: flex; align-items: center; gap: 8px; }
246
+ .dshvpn-radio { display: flex; align-items: flex-start; gap: 8px; font-size: 13px; cursor: pointer; }
247
+ .dshvpn-radio input { margin-top: 3px; }
248
+ .dshvpn-radio span { display: flex; flex-direction: column; gap: 2px; }
249
+ .dshvpn-actions, .dshvpn-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
250
+ .dshvpn-row { justify-content: space-between; }
251
+ .dshvpn-danger { color: var(--dsw-alias-state-error-primary, #d33) !important; }
252
+ .dshvpn-notice { padding: 8px 12px; border-radius: 8px; font-size: 13px; white-space: pre-wrap;
253
+ border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.3)); background: var(--dsw-alias-bg-layer-1, transparent); }
254
+ .dshvpn-success { border-color: var(--dsw-alias-state-success-primary, #2f855a); }
255
+ .dshvpn-error { border-color: var(--dsw-alias-state-error-primary, #d33); }
256
+ .dshvpn-qr-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
257
+ .dshvpn-qr-card { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; padding: 12px; border-radius: 10px;
258
+ border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.3)); background: var(--dsw-alias-bg-layer-1, transparent); }
259
+ .dshvpn-qr-head { display: flex; flex-direction: column; gap: 2px; }
260
+ /* QR codes stay black-on-white in dark mode so every scanner can read them. */
261
+ .dshvpn-qr { background: #fff; padding: 6px; border-radius: 6px; line-height: 0; align-self: center; }
262
+ .dshvpn-qr svg { width: 180px; height: 180px; }
263
+ .dshvpn-link { display: block; max-width: 100%; max-height: 3.2em; overflow: hidden; color: var(--dsw-alias-label-secondary, #666); }
264
+ `;
265
+
266
+ // src/client/VpnSettingsPage.tsx
267
+ var import_react = require("react");
268
+ var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
269
+
270
+ // node_modules/.pnpm/uqr@0.1.3/node_modules/uqr/dist/index.mjs
271
+ var QrCodeDataType = /* @__PURE__ */ ((QrCodeDataType2) => {
272
+ QrCodeDataType2[QrCodeDataType2["Border"] = -1] = "Border";
273
+ QrCodeDataType2[QrCodeDataType2["Data"] = 0] = "Data";
274
+ QrCodeDataType2[QrCodeDataType2["Function"] = 1] = "Function";
275
+ QrCodeDataType2[QrCodeDataType2["Position"] = 2] = "Position";
276
+ QrCodeDataType2[QrCodeDataType2["Timing"] = 3] = "Timing";
277
+ QrCodeDataType2[QrCodeDataType2["Alignment"] = 4] = "Alignment";
278
+ return QrCodeDataType2;
279
+ })(QrCodeDataType || {});
280
+ var LOW = [0, 1];
281
+ var MEDIUM = [1, 0];
282
+ var QUARTILE = [2, 3];
283
+ var HIGH = [3, 2];
284
+ var EccMap = {
285
+ L: LOW,
286
+ M: MEDIUM,
287
+ Q: QUARTILE,
288
+ H: HIGH
289
+ };
290
+ var NUMERIC_REGEX = /^\d*$/;
291
+ var ALPHANUMERIC_REGEX = /^[A-Z0-9 $%*+./:-]*$/;
292
+ var ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
293
+ var MIN_VERSION = 1;
294
+ var MAX_VERSION = 40;
295
+ var PENALTY_N1 = 3;
296
+ var PENALTY_N2 = 3;
297
+ var PENALTY_N3 = 40;
298
+ var PENALTY_N4 = 10;
299
+ var ECC_CODEWORDS_PER_BLOCK = [
300
+ // Version: (note that index 0 is for padding, and is set to an illegal value)
301
+ // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
302
+ [-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
303
+ // Low
304
+ [-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28],
305
+ // Medium
306
+ [-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
307
+ // Quartile
308
+ [-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]
309
+ // High
310
+ ];
311
+ var NUM_ERROR_CORRECTION_BLOCKS = [
312
+ // Version: (note that index 0 is for padding, and is set to an illegal value)
313
+ // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
314
+ [-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25],
315
+ // Low
316
+ [-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49],
317
+ // Medium
318
+ [-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68],
319
+ // Quartile
320
+ [-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81]
321
+ // High
322
+ ];
323
+ var QrCode = class {
324
+ /* -- Constructor (low level) and fields -- */
325
+ // Creates a new QR Code with the given version number,
326
+ // error correction level, data codeword bytes, and mask number.
327
+ // This is a low-level API that most users should not use directly.
328
+ // A mid-level API is the encodeSegments() function.
329
+ constructor(version, ecc, dataCodewords, msk) {
330
+ this.version = version;
331
+ this.ecc = ecc;
332
+ if (version < MIN_VERSION || version > MAX_VERSION)
333
+ throw new RangeError("Version value out of range");
334
+ if (msk < -1 || msk > 7)
335
+ throw new RangeError("Mask value out of range");
336
+ this.size = version * 4 + 17;
337
+ const row = Array.from({ length: this.size }).fill(false);
338
+ for (let i = 0; i < this.size; i++) {
339
+ this.modules.push(row.slice());
340
+ this.types.push(row.map(() => 0));
341
+ }
342
+ this.drawFunctionPatterns();
343
+ const allCodewords = this.addEccAndInterleave(dataCodewords);
344
+ this.drawCodewords(allCodewords);
345
+ if (msk === -1) {
346
+ let minPenalty = 1e9;
347
+ for (let i = 0; i < 8; i++) {
348
+ this.applyMask(i);
349
+ this.drawFormatBits(i);
350
+ const penalty = this.getPenaltyScore();
351
+ if (penalty < minPenalty) {
352
+ msk = i;
353
+ minPenalty = penalty;
354
+ }
355
+ this.applyMask(i);
356
+ }
357
+ }
358
+ this.mask = msk;
359
+ this.applyMask(msk);
360
+ this.drawFormatBits(msk);
361
+ }
362
+ /* -- Fields -- */
363
+ // The width and height of this QR Code, measured in modules, between
364
+ // 21 and 177 (inclusive). This is equal to version * 4 + 17.
365
+ size;
366
+ // The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive).
367
+ // Even if a QR Code is created with automatic masking requested (mask = -1),
368
+ // the resulting object still has a mask value between 0 and 7.
369
+ mask;
370
+ // The modules of this QR Code (false = light, true = dark).
371
+ // Immutable after constructor finishes. Accessed through getModule().
372
+ modules = [];
373
+ types = [];
374
+ /* -- Accessor methods -- */
375
+ // Returns the color of the module (pixel) at the given coordinates, which is false
376
+ // for light or true for dark. The top left corner has the coordinates (x=0, y=0).
377
+ // If the given coordinates are out of bounds, then false (light) is returned.
378
+ getModule(x, y) {
379
+ return x >= 0 && x < this.size && y >= 0 && y < this.size && this.modules[y][x];
380
+ }
381
+ /* -- Private helper methods for constructor: Drawing function modules -- */
382
+ // Reads this object's version field, and draws and marks all function modules.
383
+ drawFunctionPatterns() {
384
+ for (let i = 0; i < this.size; i++) {
385
+ this.setFunctionModule(6, i, i % 2 === 0, QrCodeDataType.Timing);
386
+ this.setFunctionModule(i, 6, i % 2 === 0, QrCodeDataType.Timing);
387
+ }
388
+ this.drawFinderPattern(3, 3);
389
+ this.drawFinderPattern(this.size - 4, 3);
390
+ this.drawFinderPattern(3, this.size - 4);
391
+ const alignPatPos = this.getAlignmentPatternPositions();
392
+ const numAlign = alignPatPos.length;
393
+ for (let i = 0; i < numAlign; i++) {
394
+ for (let j = 0; j < numAlign; j++) {
395
+ if (!(i === 0 && j === 0 || i === 0 && j === numAlign - 1 || i === numAlign - 1 && j === 0))
396
+ this.drawAlignmentPattern(alignPatPos[i], alignPatPos[j]);
397
+ }
398
+ }
399
+ this.drawFormatBits(0);
400
+ this.drawVersion();
401
+ }
402
+ // Draws two copies of the format bits (with its own error correction code)
403
+ // based on the given mask and this object's error correction level field.
404
+ drawFormatBits(mask) {
405
+ const data = this.ecc[1] << 3 | mask;
406
+ let rem = data;
407
+ for (let i = 0; i < 10; i++)
408
+ rem = rem << 1 ^ (rem >>> 9) * 1335;
409
+ const bits = (data << 10 | rem) ^ 21522;
410
+ for (let i = 0; i <= 5; i++)
411
+ this.setFunctionModule(8, i, getBit(bits, i));
412
+ this.setFunctionModule(8, 7, getBit(bits, 6));
413
+ this.setFunctionModule(8, 8, getBit(bits, 7));
414
+ this.setFunctionModule(7, 8, getBit(bits, 8));
415
+ for (let i = 9; i < 15; i++)
416
+ this.setFunctionModule(14 - i, 8, getBit(bits, i));
417
+ for (let i = 0; i < 8; i++)
418
+ this.setFunctionModule(this.size - 1 - i, 8, getBit(bits, i));
419
+ for (let i = 8; i < 15; i++)
420
+ this.setFunctionModule(8, this.size - 15 + i, getBit(bits, i));
421
+ this.setFunctionModule(8, this.size - 8, true);
422
+ }
423
+ // Draws two copies of the version bits (with its own error correction code),
424
+ // based on this object's version field, iff 7 <= version <= 40.
425
+ drawVersion() {
426
+ if (this.version < 7)
427
+ return;
428
+ let rem = this.version;
429
+ for (let i = 0; i < 12; i++)
430
+ rem = rem << 1 ^ (rem >>> 11) * 7973;
431
+ const bits = this.version << 12 | rem;
432
+ for (let i = 0; i < 18; i++) {
433
+ const color = getBit(bits, i);
434
+ const a = this.size - 11 + i % 3;
435
+ const b = Math.floor(i / 3);
436
+ this.setFunctionModule(a, b, color);
437
+ this.setFunctionModule(b, a, color);
438
+ }
439
+ }
440
+ // Draws a 9*9 finder pattern including the border separator,
441
+ // with the center module at (x, y). Modules can be out of bounds.
442
+ drawFinderPattern(x, y) {
443
+ for (let dy = -4; dy <= 4; dy++) {
444
+ for (let dx = -4; dx <= 4; dx++) {
445
+ const dist = Math.max(Math.abs(dx), Math.abs(dy));
446
+ const xx = x + dx;
447
+ const yy = y + dy;
448
+ if (xx >= 0 && xx < this.size && yy >= 0 && yy < this.size)
449
+ this.setFunctionModule(xx, yy, dist !== 2 && dist !== 4, QrCodeDataType.Position);
450
+ }
451
+ }
452
+ }
453
+ // Draws a 5*5 alignment pattern, with the center module
454
+ // at (x, y). All modules must be in bounds.
455
+ drawAlignmentPattern(x, y) {
456
+ for (let dy = -2; dy <= 2; dy++) {
457
+ for (let dx = -2; dx <= 2; dx++) {
458
+ this.setFunctionModule(
459
+ x + dx,
460
+ y + dy,
461
+ Math.max(Math.abs(dx), Math.abs(dy)) !== 1,
462
+ QrCodeDataType.Alignment
463
+ );
464
+ }
465
+ }
466
+ }
467
+ // Sets the color of a module and marks it as a function module.
468
+ // Only used by the constructor. Coordinates must be in bounds.
469
+ setFunctionModule(x, y, isDark, type = QrCodeDataType.Function) {
470
+ this.modules[y][x] = isDark;
471
+ this.types[y][x] = type;
472
+ }
473
+ /* -- Private helper methods for constructor: Codewords and masking -- */
474
+ // Returns a new byte string representing the given data with the appropriate error correction
475
+ // codewords appended to it, based on this object's version and error correction level.
476
+ addEccAndInterleave(data) {
477
+ const ver = this.version;
478
+ const ecl = this.ecc;
479
+ if (data.length !== getNumDataCodewords(ver, ecl))
480
+ throw new RangeError("Invalid argument");
481
+ const numBlocks = NUM_ERROR_CORRECTION_BLOCKS[ecl[0]][ver];
482
+ const blockEccLen = ECC_CODEWORDS_PER_BLOCK[ecl[0]][ver];
483
+ const rawCodewords = Math.floor(getNumRawDataModules(ver) / 8);
484
+ const numShortBlocks = numBlocks - rawCodewords % numBlocks;
485
+ const shortBlockLen = Math.floor(rawCodewords / numBlocks);
486
+ const blocks = [];
487
+ const rsDiv = reedSolomonComputeDivisor(blockEccLen);
488
+ for (let i = 0, k = 0; i < numBlocks; i++) {
489
+ const dat = data.slice(k, k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1));
490
+ k += dat.length;
491
+ const ecc = reedSolomonComputeRemainder(dat, rsDiv);
492
+ if (i < numShortBlocks)
493
+ dat.push(0);
494
+ blocks.push(dat.concat(ecc));
495
+ }
496
+ const result = [];
497
+ for (let i = 0; i < blocks[0].length; i++) {
498
+ blocks.forEach((block, j) => {
499
+ if (i !== shortBlockLen - blockEccLen || j >= numShortBlocks)
500
+ result.push(block[i]);
501
+ });
502
+ }
503
+ return result;
504
+ }
505
+ // Draws the given sequence of 8-bit codewords (data and error correction) onto the entire
506
+ // data area of this QR Code. Function modules need to be marked off before this is called.
507
+ drawCodewords(data) {
508
+ if (data.length !== Math.floor(getNumRawDataModules(this.version) / 8))
509
+ throw new RangeError("Invalid argument");
510
+ let i = 0;
511
+ for (let right = this.size - 1; right >= 1; right -= 2) {
512
+ if (right === 6)
513
+ right = 5;
514
+ for (let vert = 0; vert < this.size; vert++) {
515
+ for (let j = 0; j < 2; j++) {
516
+ const x = right - j;
517
+ const upward = (right + 1 & 2) === 0;
518
+ const y = upward ? this.size - 1 - vert : vert;
519
+ if (!this.types[y][x] && i < data.length * 8) {
520
+ this.modules[y][x] = getBit(data[i >>> 3], 7 - (i & 7));
521
+ i++;
522
+ }
523
+ }
524
+ }
525
+ }
526
+ }
527
+ // XORs the codeword modules in this QR Code with the given mask pattern.
528
+ // The function modules must be marked and the codeword bits must be drawn
529
+ // before masking. Due to the arithmetic of XOR, calling applyMask() with
530
+ // the same mask value a second time will undo the mask. A final well-formed
531
+ // QR Code needs exactly one (not zero, two, etc.) mask applied.
532
+ applyMask(mask) {
533
+ if (mask < 0 || mask > 7)
534
+ throw new RangeError("Mask value out of range");
535
+ for (let y = 0; y < this.size; y++) {
536
+ for (let x = 0; x < this.size; x++) {
537
+ let invert;
538
+ switch (mask) {
539
+ case 0:
540
+ invert = (x + y) % 2 === 0;
541
+ break;
542
+ case 1:
543
+ invert = y % 2 === 0;
544
+ break;
545
+ case 2:
546
+ invert = x % 3 === 0;
547
+ break;
548
+ case 3:
549
+ invert = (x + y) % 3 === 0;
550
+ break;
551
+ case 4:
552
+ invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0;
553
+ break;
554
+ case 5:
555
+ invert = x * y % 2 + x * y % 3 === 0;
556
+ break;
557
+ case 6:
558
+ invert = (x * y % 2 + x * y % 3) % 2 === 0;
559
+ break;
560
+ case 7:
561
+ invert = ((x + y) % 2 + x * y % 3) % 2 === 0;
562
+ break;
563
+ default:
564
+ throw new Error("Unreachable");
565
+ }
566
+ if (!this.types[y][x] && invert)
567
+ this.modules[y][x] = !this.modules[y][x];
568
+ }
569
+ }
570
+ }
571
+ // Calculates and returns the penalty score based on state of this QR Code's current modules.
572
+ // This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score.
573
+ getPenaltyScore() {
574
+ let result = 0;
575
+ for (let y = 0; y < this.size; y++) {
576
+ let runColor = false;
577
+ let runX = 0;
578
+ const runHistory = [0, 0, 0, 0, 0, 0, 0];
579
+ for (let x = 0; x < this.size; x++) {
580
+ if (this.modules[y][x] === runColor) {
581
+ runX++;
582
+ if (runX === 5)
583
+ result += PENALTY_N1;
584
+ else if (runX > 5)
585
+ result++;
586
+ } else {
587
+ this.finderPenaltyAddHistory(runX, runHistory);
588
+ if (!runColor)
589
+ result += this.finderPenaltyCountPatterns(runHistory) * PENALTY_N3;
590
+ runColor = this.modules[y][x];
591
+ runX = 1;
592
+ }
593
+ }
594
+ result += this.finderPenaltyTerminateAndCount(runColor, runX, runHistory) * PENALTY_N3;
595
+ }
596
+ for (let x = 0; x < this.size; x++) {
597
+ let runColor = false;
598
+ let runY = 0;
599
+ const runHistory = [0, 0, 0, 0, 0, 0, 0];
600
+ for (let y = 0; y < this.size; y++) {
601
+ if (this.modules[y][x] === runColor) {
602
+ runY++;
603
+ if (runY === 5)
604
+ result += PENALTY_N1;
605
+ else if (runY > 5)
606
+ result++;
607
+ } else {
608
+ this.finderPenaltyAddHistory(runY, runHistory);
609
+ if (!runColor)
610
+ result += this.finderPenaltyCountPatterns(runHistory) * PENALTY_N3;
611
+ runColor = this.modules[y][x];
612
+ runY = 1;
613
+ }
614
+ }
615
+ result += this.finderPenaltyTerminateAndCount(runColor, runY, runHistory) * PENALTY_N3;
616
+ }
617
+ for (let y = 0; y < this.size - 1; y++) {
618
+ for (let x = 0; x < this.size - 1; x++) {
619
+ const color = this.modules[y][x];
620
+ if (color === this.modules[y][x + 1] && color === this.modules[y + 1][x] && color === this.modules[y + 1][x + 1]) {
621
+ result += PENALTY_N2;
622
+ }
623
+ }
624
+ }
625
+ let dark = 0;
626
+ for (const row of this.modules)
627
+ dark = row.reduce((sum, color) => sum + (color ? 1 : 0), dark);
628
+ const total = this.size * this.size;
629
+ const k = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
630
+ result += k * PENALTY_N4;
631
+ return result;
632
+ }
633
+ /* -- Private helper functions -- */
634
+ // Returns an ascending list of positions of alignment patterns for this version number.
635
+ // Each position is in the range [0,177), and are used on both the x and y axes.
636
+ // This could be implemented as lookup table of 40 variable-length lists of integers.
637
+ getAlignmentPatternPositions() {
638
+ if (this.version === 1) {
639
+ return [];
640
+ } else {
641
+ const numAlign = Math.floor(this.version / 7) + 2;
642
+ const step = this.version === 32 ? 26 : Math.ceil((this.version * 4 + 4) / (numAlign * 2 - 2)) * 2;
643
+ const result = [6];
644
+ for (let pos = this.size - 7; result.length < numAlign; pos -= step)
645
+ result.splice(1, 0, pos);
646
+ return result;
647
+ }
648
+ }
649
+ // Can only be called immediately after a light run is added, and
650
+ // returns either 0, 1, or 2. A helper function for getPenaltyScore().
651
+ finderPenaltyCountPatterns(runHistory) {
652
+ const n = runHistory[1];
653
+ const core = n > 0 && runHistory[2] === n && runHistory[3] === n * 3 && runHistory[4] === n && runHistory[5] === n;
654
+ return (core && runHistory[0] >= n * 4 && runHistory[6] >= n ? 1 : 0) + (core && runHistory[6] >= n * 4 && runHistory[0] >= n ? 1 : 0);
655
+ }
656
+ // Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore().
657
+ finderPenaltyTerminateAndCount(currentRunColor, currentRunLength, runHistory) {
658
+ if (currentRunColor) {
659
+ this.finderPenaltyAddHistory(currentRunLength, runHistory);
660
+ currentRunLength = 0;
661
+ }
662
+ currentRunLength += this.size;
663
+ this.finderPenaltyAddHistory(currentRunLength, runHistory);
664
+ return this.finderPenaltyCountPatterns(runHistory);
665
+ }
666
+ // Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore().
667
+ finderPenaltyAddHistory(currentRunLength, runHistory) {
668
+ if (runHistory[0] === 0)
669
+ currentRunLength += this.size;
670
+ runHistory.pop();
671
+ runHistory.unshift(currentRunLength);
672
+ }
673
+ };
674
+ function appendBits(val, len, bb) {
675
+ if (len < 0 || len > 31 || val >>> len !== 0)
676
+ throw new RangeError("Value out of range");
677
+ for (let i = len - 1; i >= 0; i--)
678
+ bb.push(val >>> i & 1);
679
+ }
680
+ function getBit(x, i) {
681
+ return (x >>> i & 1) !== 0;
682
+ }
683
+ var QrSegment = class {
684
+ // Creates a new QR Code segment with the given attributes and data.
685
+ // The character count (numChars) must agree with the mode and the bit buffer length,
686
+ // but the constraint isn't checked. The given bit buffer is cloned and stored.
687
+ constructor(mode, numChars, bitData) {
688
+ this.mode = mode;
689
+ this.numChars = numChars;
690
+ this.bitData = bitData;
691
+ if (numChars < 0)
692
+ throw new RangeError("Invalid argument");
693
+ this.bitData = bitData.slice();
694
+ }
695
+ /* -- Methods -- */
696
+ // Returns a new copy of the data bits of this segment.
697
+ getData() {
698
+ return this.bitData.slice();
699
+ }
700
+ };
701
+ var MODE_NUMERIC = [1, 10, 12, 14];
702
+ var MODE_ALPHANUMERIC = [2, 9, 11, 13];
703
+ var MODE_BYTE = [4, 8, 16, 16];
704
+ function numCharCountBits(mode, ver) {
705
+ return mode[Math.floor((ver + 7) / 17) + 1];
706
+ }
707
+ function makeBytes(data) {
708
+ const bb = [];
709
+ for (const b of data)
710
+ appendBits(b, 8, bb);
711
+ return new QrSegment(MODE_BYTE, data.length, bb);
712
+ }
713
+ function makeNumeric(digits) {
714
+ if (!isNumeric(digits))
715
+ throw new RangeError("String contains non-numeric characters");
716
+ const bb = [];
717
+ for (let i = 0; i < digits.length; ) {
718
+ const n = Math.min(digits.length - i, 3);
719
+ appendBits(Number.parseInt(digits.substring(i, i + n), 10), n * 3 + 1, bb);
720
+ i += n;
721
+ }
722
+ return new QrSegment(MODE_NUMERIC, digits.length, bb);
723
+ }
724
+ function makeAlphanumeric(text) {
725
+ if (!isAlphanumeric(text))
726
+ throw new RangeError("String contains unencodable characters in alphanumeric mode");
727
+ const bb = [];
728
+ let i;
729
+ for (i = 0; i + 2 <= text.length; i += 2) {
730
+ let temp = ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)) * 45;
731
+ temp += ALPHANUMERIC_CHARSET.indexOf(text.charAt(i + 1));
732
+ appendBits(temp, 11, bb);
733
+ }
734
+ if (i < text.length)
735
+ appendBits(ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)), 6, bb);
736
+ return new QrSegment(MODE_ALPHANUMERIC, text.length, bb);
737
+ }
738
+ function makeSegments(text) {
739
+ if (text === "")
740
+ return [];
741
+ else if (isNumeric(text))
742
+ return [makeNumeric(text)];
743
+ else if (isAlphanumeric(text))
744
+ return [makeAlphanumeric(text)];
745
+ else
746
+ return [makeBytes(toUtf8ByteArray(text))];
747
+ }
748
+ function isNumeric(text) {
749
+ return NUMERIC_REGEX.test(text);
750
+ }
751
+ function isAlphanumeric(text) {
752
+ return ALPHANUMERIC_REGEX.test(text);
753
+ }
754
+ function getTotalBits(segs, version) {
755
+ let result = 0;
756
+ for (const seg of segs) {
757
+ const ccbits = numCharCountBits(seg.mode, version);
758
+ if (seg.numChars >= 1 << ccbits)
759
+ return Number.POSITIVE_INFINITY;
760
+ result += 4 + ccbits + seg.bitData.length;
761
+ }
762
+ return result;
763
+ }
764
+ function toUtf8ByteArray(str) {
765
+ str = encodeURI(str);
766
+ const result = [];
767
+ for (let i = 0; i < str.length; i++) {
768
+ if (str.charAt(i) !== "%") {
769
+ result.push(str.charCodeAt(i));
770
+ } else {
771
+ result.push(Number.parseInt(str.substring(i + 1, i + 3), 16));
772
+ i += 2;
773
+ }
774
+ }
775
+ return result;
776
+ }
777
+ function getNumRawDataModules(ver) {
778
+ if (ver < MIN_VERSION || ver > MAX_VERSION)
779
+ throw new RangeError("Version number out of range");
780
+ let result = (16 * ver + 128) * ver + 64;
781
+ if (ver >= 2) {
782
+ const numAlign = Math.floor(ver / 7) + 2;
783
+ result -= (25 * numAlign - 10) * numAlign - 55;
784
+ if (ver >= 7)
785
+ result -= 36;
786
+ }
787
+ return result;
788
+ }
789
+ function getNumDataCodewords(ver, ecl) {
790
+ return Math.floor(getNumRawDataModules(ver) / 8) - ECC_CODEWORDS_PER_BLOCK[ecl[0]][ver] * NUM_ERROR_CORRECTION_BLOCKS[ecl[0]][ver];
791
+ }
792
+ function reedSolomonComputeDivisor(degree) {
793
+ if (degree < 1 || degree > 255)
794
+ throw new RangeError("Degree out of range");
795
+ const result = [];
796
+ for (let i = 0; i < degree - 1; i++)
797
+ result.push(0);
798
+ result.push(1);
799
+ let root = 1;
800
+ for (let i = 0; i < degree; i++) {
801
+ for (let j = 0; j < result.length; j++) {
802
+ result[j] = reedSolomonMultiply(result[j], root);
803
+ if (j + 1 < result.length)
804
+ result[j] ^= result[j + 1];
805
+ }
806
+ root = reedSolomonMultiply(root, 2);
807
+ }
808
+ return result;
809
+ }
810
+ function reedSolomonComputeRemainder(data, divisor) {
811
+ const result = divisor.map((_) => 0);
812
+ for (const b of data) {
813
+ const factor = b ^ result.shift();
814
+ result.push(0);
815
+ divisor.forEach((coef, i) => result[i] ^= reedSolomonMultiply(coef, factor));
816
+ }
817
+ return result;
818
+ }
819
+ function reedSolomonMultiply(x, y) {
820
+ if (x >>> 8 !== 0 || y >>> 8 !== 0)
821
+ throw new RangeError("Byte out of range");
822
+ let z = 0;
823
+ for (let i = 7; i >= 0; i--) {
824
+ z = z << 1 ^ (z >>> 7) * 285;
825
+ z ^= (y >>> i & 1) * x;
826
+ }
827
+ return z;
828
+ }
829
+ function encodeSegments(segs, ecl, minVersion = 1, maxVersion = 40, mask = -1, boostEcl = true) {
830
+ if (!(MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= MAX_VERSION) || mask < -1 || mask > 7) {
831
+ throw new RangeError("Invalid value");
832
+ }
833
+ let version;
834
+ let dataUsedBits;
835
+ for (version = minVersion; ; version++) {
836
+ const dataCapacityBits2 = getNumDataCodewords(version, ecl) * 8;
837
+ const usedBits = getTotalBits(segs, version);
838
+ if (usedBits <= dataCapacityBits2) {
839
+ dataUsedBits = usedBits;
840
+ break;
841
+ }
842
+ if (version >= maxVersion)
843
+ throw new RangeError("Data too long");
844
+ }
845
+ for (const newEcl of [MEDIUM, QUARTILE, HIGH]) {
846
+ if (boostEcl && dataUsedBits <= getNumDataCodewords(version, newEcl) * 8)
847
+ ecl = newEcl;
848
+ }
849
+ const bb = [];
850
+ for (const seg of segs) {
851
+ appendBits(seg.mode[0], 4, bb);
852
+ appendBits(seg.numChars, numCharCountBits(seg.mode, version), bb);
853
+ for (const b of seg.getData())
854
+ bb.push(b);
855
+ }
856
+ const dataCapacityBits = getNumDataCodewords(version, ecl) * 8;
857
+ appendBits(0, Math.min(4, dataCapacityBits - bb.length), bb);
858
+ appendBits(0, (8 - bb.length % 8) % 8, bb);
859
+ for (let padByte = 236; bb.length < dataCapacityBits; padByte ^= 236 ^ 17)
860
+ appendBits(padByte, 8, bb);
861
+ const dataCodewords = Array.from({ length: Math.ceil(bb.length / 8) }, () => 0);
862
+ bb.forEach((b, i) => dataCodewords[i >>> 3] |= b << 7 - (i & 7));
863
+ return new QrCode(version, ecl, dataCodewords, mask);
864
+ }
865
+ function encode(data, options) {
866
+ const {
867
+ ecc = "L",
868
+ boostEcc = false,
869
+ minVersion = 1,
870
+ maxVersion = 40,
871
+ maskPattern = -1,
872
+ border = 1
873
+ } = options || {};
874
+ const segment = typeof data === "string" ? makeSegments(data) : Array.isArray(data) ? [makeBytes(data)] : void 0;
875
+ if (!segment)
876
+ throw new Error(`uqr only supports encoding string and binary data, but got: ${typeof data}`);
877
+ const qr = encodeSegments(
878
+ segment,
879
+ EccMap[ecc],
880
+ minVersion,
881
+ maxVersion,
882
+ maskPattern,
883
+ boostEcc
884
+ );
885
+ const result = addBorder({
886
+ version: qr.version,
887
+ maskPattern: qr.mask,
888
+ size: qr.size,
889
+ data: qr.modules,
890
+ types: qr.types
891
+ }, border);
892
+ if (options?.invert)
893
+ result.data = result.data.map((row) => row.map((mod) => !mod));
894
+ options?.onEncoded?.(result);
895
+ return result;
896
+ }
897
+ function addBorder(input, border = 1) {
898
+ if (!border)
899
+ return input;
900
+ const { size } = input;
901
+ const newSize = size + border * 2;
902
+ input.size = newSize;
903
+ input.data.forEach((row) => {
904
+ for (let i = 0; i < border; i++) {
905
+ row.unshift(false);
906
+ row.push(false);
907
+ }
908
+ });
909
+ for (let i = 0; i < border; i++) {
910
+ input.data.unshift(Array.from({ length: newSize }, (_) => false));
911
+ input.data.push(Array.from({ length: newSize }, (_) => false));
912
+ }
913
+ const b = QrCodeDataType.Border;
914
+ input.types.forEach((row) => {
915
+ for (let i = 0; i < border; i++) {
916
+ row.unshift(b);
917
+ row.push(b);
918
+ }
919
+ });
920
+ for (let i = 0; i < border; i++) {
921
+ input.types.unshift(Array.from({ length: newSize }, (_) => b));
922
+ input.types.push(Array.from({ length: newSize }, (_) => b));
923
+ }
924
+ return input;
925
+ }
926
+ function escapeAttr(value) {
927
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
928
+ }
929
+ function renderSVG(data, options = {}) {
930
+ const result = encode(data, options);
931
+ const {
932
+ pixelSize = 10,
933
+ whiteColor = "white",
934
+ blackColor = "black"
935
+ } = options;
936
+ const height = result.size * pixelSize;
937
+ const width = result.size * pixelSize;
938
+ let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">`;
939
+ const paths = [];
940
+ for (let row = 0; row < result.size; row++) {
941
+ for (let col = 0; col < result.size; col++) {
942
+ const x = col * pixelSize;
943
+ const y = row * pixelSize;
944
+ if (result.data[row][col])
945
+ paths.push(`M${x},${y}h${pixelSize}v${pixelSize}h-${pixelSize}z`);
946
+ }
947
+ }
948
+ svg += `<rect fill="${escapeAttr(whiteColor)}" width="${width}" height="${height}"/>`;
949
+ svg += `<path fill="${escapeAttr(blackColor)}" d="${paths.join("")}"/>`;
950
+ svg += "</svg>";
951
+ return svg;
952
+ }
953
+
954
+ // src/protocols.ts
955
+ var PROTOCOLS = [
956
+ { id: "reality", name: "VLESS Reality", transport: "TCP", insecureLink: false, defaultSelected: true },
957
+ { id: "hy2", name: "Hysteria2", transport: "UDP", insecureLink: false, defaultSelected: true },
958
+ { id: "tuic", name: "TUIC v5", transport: "UDP", insecureLink: true, defaultSelected: false },
959
+ { id: "anytls", name: "AnyTLS", transport: "TCP", insecureLink: true, defaultSelected: false },
960
+ { id: "ss", name: "Shadowsocks 2022", transport: "TCP+UDP", insecureLink: false, defaultSelected: false }
961
+ ];
962
+ var PROTOCOL_IDS = PROTOCOLS.map((p) => p.id);
963
+ var DEFAULT_PROTOCOLS = PROTOCOLS.filter((p) => p.defaultSelected).map((p) => p.id);
964
+
965
+ // src/client/VpnSettingsPage.tsx
966
+ var import_jsx_runtime = require("react/jsx-runtime");
967
+ var ERROR_KEYS = /* @__PURE__ */ new Set([
968
+ "not_configured",
969
+ "auth_failed",
970
+ "host_key_mismatch",
971
+ "connect_failed",
972
+ "timeout",
973
+ "sudo_failed",
974
+ "not_root",
975
+ "no_systemd",
976
+ "download_failed",
977
+ "not_installed",
978
+ "invalid",
979
+ "cert_needs_ip",
980
+ "port80_in_use",
981
+ "cert_failed"
982
+ ]);
983
+ var FIELD_KEYS = /* @__PURE__ */ new Set(["host", "port", "username", "password", "protocols", "linkAddress", "certMode"]);
984
+ function errorText(t, f) {
985
+ if (f.error === "invalid" && f.field !== void 0 && FIELD_KEYS.has(f.field)) return t(`field.${f.field}`);
986
+ const key = ERROR_KEYS.has(f.error) ? `err.${f.error}` : "err.other";
987
+ return t(key, { message: f.message ?? f.error });
988
+ }
989
+ function formOf(s) {
990
+ return { host: s.host, port: s.port, username: s.username, password: "", protocols: [...s.protocols], certMode: s.certMode, linkAddress: s.linkAddress };
991
+ }
992
+ var sameForm = (a, b) => a.host === b.host && a.port === b.port && a.username === b.username && a.password === b.password && a.linkAddress === b.linkAddress && a.certMode === b.certMode && a.protocols.join() === [...b.protocols].join();
993
+ function transportsFor(id) {
994
+ const t = PROTOCOLS.find((p) => p.id === id)?.transport ?? "TCP";
995
+ return t === "TCP+UDP" ? ["tcp", "udp"] : [t.toLowerCase()];
996
+ }
997
+ function QrCard({ item, t }) {
998
+ const [copied, setCopied] = (0, import_react.useState)(false);
999
+ const info = PROTOCOLS.find((p) => p.id === item.id);
1000
+ const svg = (0, import_react.useMemo)(() => renderSVG(item.link, { ecc: "M", border: 2, pixelSize: 4 }), [item.link]);
1001
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-qr-card", children: [
1002
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-qr-head", children: [
1003
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: info?.name ?? item.id }),
1004
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dshvpn-muted", children: [
1005
+ t("portLabel", { port: item.port }),
1006
+ " \xB7 ",
1007
+ info?.transport
1008
+ ] })
1009
+ ] }),
1010
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshvpn-qr", dangerouslySetInnerHTML: { __html: svg } }),
1011
+ item.insecure === true && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tag, { tone: "warning", children: t("insecureTag") }),
1012
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { className: "dshvpn-link", title: item.link, children: item.link }),
1013
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { size: "sm", variant: "outline", onClick: () => {
1014
+ void (0, import_dsh_client_ui_primitives.writeClipboard)(item.link).then(() => {
1015
+ setCopied(true);
1016
+ setTimeout(() => {
1017
+ setCopied(false);
1018
+ }, 1500);
1019
+ });
1020
+ }, children: copied ? t("copied") : t("copy") })
1021
+ ] });
1022
+ }
1023
+ function VpnSettingsPage({ view, t, api }) {
1024
+ const [saved, setSaved] = (0, import_react.useState)(null);
1025
+ const [form, setForm] = (0, import_react.useState)(null);
1026
+ const [busy, setBusy] = (0, import_react.useState)(null);
1027
+ const [notice, setNotice] = (0, import_react.useState)(null);
1028
+ const [links, setLinks] = (0, import_react.useState)(null);
1029
+ const [regenerate, setRegenerate] = (0, import_react.useState)(false);
1030
+ const [confirming, setConfirming] = (0, import_react.useState)(null);
1031
+ (0, import_react.useEffect)(() => {
1032
+ let live = true;
1033
+ void api.getSettings().then((r) => {
1034
+ if (!live) return;
1035
+ if (r.ok) {
1036
+ setSaved(r.settings);
1037
+ setForm(formOf(r.settings));
1038
+ } else setNotice({ tone: "error", text: errorText(t, r) });
1039
+ });
1040
+ return () => {
1041
+ live = false;
1042
+ };
1043
+ }, [api, t]);
1044
+ if (view === "summary") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: t("description") });
1045
+ if (!saved || !form) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshvpn-muted", children: notice?.text ?? t("working") });
1046
+ const dirty = !sameForm(form, formOf(saved));
1047
+ const set = (key, value) => {
1048
+ setForm({ ...form, [key]: value });
1049
+ };
1050
+ const toggle = (id, on) => {
1051
+ set("protocols", PROTOCOLS.map((p) => p.id).filter((p) => p === id ? on : form.protocols.includes(p)));
1052
+ };
1053
+ async function op(name, call, onOk) {
1054
+ setBusy(name);
1055
+ setNotice(null);
1056
+ try {
1057
+ const r = await call();
1058
+ setNotice(r.ok ? onOk(r) : { tone: "error", text: errorText(t, r) });
1059
+ return r;
1060
+ } finally {
1061
+ setBusy(null);
1062
+ }
1063
+ }
1064
+ const save = () => op("save", () => api.saveSettings(form), (r) => {
1065
+ setSaved(r.settings);
1066
+ setForm(formOf(r.settings));
1067
+ return { tone: "success", text: t("saved") };
1068
+ });
1069
+ const refreshSettings = async () => {
1070
+ const r = await api.getSettings();
1071
+ if (r.ok) {
1072
+ setSaved(r.settings);
1073
+ setForm((f) => f && { ...f });
1074
+ }
1075
+ };
1076
+ const portsReminder = (l) => t("cloudFirewall", {
1077
+ ports: [
1078
+ ...l.items.map((i) => transportsFor(i.id).map((tr) => `${i.port}/${tr.toUpperCase()}`).join(" ")),
1079
+ ...l.cert === "ip" ? ["80/TCP"] : []
1080
+ ].join(", ")
1081
+ });
1082
+ const install = async () => {
1083
+ const r = await op("install", () => api.install(regenerate), (v) => {
1084
+ if (v.links) setLinks(v.links);
1085
+ return { tone: "success", text: `${t("installed")} ${v.links ? portsReminder(v.links) : ""}` };
1086
+ });
1087
+ if (!r.ok && r.links) setLinks(r.links);
1088
+ setRegenerate(false);
1089
+ void refreshSettings();
1090
+ };
1091
+ const disabled = busy !== null;
1092
+ const needsSave = dirty || !saved.configured;
1093
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn", children: [
1094
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
1095
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: t("serverSection") }),
1096
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-grid", children: [
1097
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
1098
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("host") }),
1099
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Input, { value: form.host, placeholder: "203.0.113.10", onChange: (e) => {
1100
+ set("host", e.target.value);
1101
+ } }),
1102
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("hostHint") })
1103
+ ] }),
1104
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
1105
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("port") }),
1106
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Input, { value: String(form.port), inputMode: "numeric", onChange: (e) => {
1107
+ set("port", Number(e.target.value.replace(/\D/g, "")) || 0);
1108
+ } })
1109
+ ] }),
1110
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
1111
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("username") }),
1112
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Input, { value: form.username, autoComplete: "off", onChange: (e) => {
1113
+ set("username", e.target.value);
1114
+ } }),
1115
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("usernameHint") })
1116
+ ] }),
1117
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
1118
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("password") }),
1119
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1120
+ import_dsh_client_ui_primitives.Input,
1121
+ {
1122
+ type: "password",
1123
+ value: form.password,
1124
+ autoComplete: "new-password",
1125
+ placeholder: saved.hasPassword ? t("passwordSaved") : t("passwordEmpty"),
1126
+ onChange: (e) => {
1127
+ set("password", e.target.value);
1128
+ }
1129
+ }
1130
+ )
1131
+ ] }),
1132
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dshvpn-wide", children: [
1133
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("linkAddress") }),
1134
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Input, { value: form.linkAddress, placeholder: form.host, onChange: (e) => {
1135
+ set("linkAddress", e.target.value);
1136
+ } }),
1137
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("linkAddressHint") })
1138
+ ] })
1139
+ ] }),
1140
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-hostkey", children: [
1141
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("hostKey") }),
1142
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: saved.hostKey ?? t("hostKeyNone") }),
1143
+ saved.hostKey !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1144
+ import_dsh_client_ui_primitives.Button,
1145
+ {
1146
+ size: "sm",
1147
+ variant: "ghost",
1148
+ disabled,
1149
+ onClick: () => {
1150
+ void op("forget", api.forgetHostKey, (r) => {
1151
+ setSaved(r.settings);
1152
+ return null;
1153
+ });
1154
+ },
1155
+ children: t("forgetHostKey")
1156
+ }
1157
+ ),
1158
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("hostKeyHint") })
1159
+ ] })
1160
+ ] }),
1161
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
1162
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: t("protocolSection") }),
1163
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("protocolHint") }),
1164
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshvpn-protocols", children: PROTOCOLS.map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-protocol", children: [
1165
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1166
+ import_dsh_client_ui_primitives.Checkbox,
1167
+ {
1168
+ label: p.name,
1169
+ checked: form.protocols.includes(p.id),
1170
+ disabled,
1171
+ onChange: (on) => {
1172
+ toggle(p.id, on);
1173
+ }
1174
+ }
1175
+ ),
1176
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tag, { children: p.transport }),
1177
+ p.insecureLink && form.certMode === "self" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { title: t("insecureHint"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tag, { tone: "warning", children: t("insecureTag") }) })
1178
+ ] }, p.id)) })
1179
+ ] }),
1180
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
1181
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: t("certSection") }),
1182
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("certHint") }),
1183
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshvpn-protocols", children: ["self", "ip"].map((mode) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dshvpn-radio", children: [
1184
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1185
+ "input",
1186
+ {
1187
+ type: "radio",
1188
+ name: "dshvpn-cert",
1189
+ checked: form.certMode === mode,
1190
+ disabled,
1191
+ onChange: () => {
1192
+ set("certMode", mode);
1193
+ }
1194
+ }
1195
+ ),
1196
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
1197
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: mode === "ip" ? t("certIp") : t("certSelf") }),
1198
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: mode === "ip" ? t("certIpHint") : t("certSelfHint") })
1199
+ ] })
1200
+ ] }, mode)) })
1201
+ ] }),
1202
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-actions", children: [
1203
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "primary", disabled: disabled || !dirty, onClick: () => {
1204
+ void save();
1205
+ }, children: t("save") }),
1206
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1207
+ import_dsh_client_ui_primitives.Button,
1208
+ {
1209
+ variant: "outline",
1210
+ disabled: disabled || needsSave,
1211
+ onClick: () => {
1212
+ void op("test", api.test, (r) => {
1213
+ void refreshSettings();
1214
+ return { tone: "success", text: t("testOk", { system: String(r.result?.system ?? "") }) };
1215
+ });
1216
+ },
1217
+ children: t("test")
1218
+ }
1219
+ ),
1220
+ dirty && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-warn", children: t("unsaved") })
1221
+ ] }),
1222
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
1223
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: t("deploySection") }),
1224
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Checkbox, { label: t("regenerate"), checked: regenerate, disabled, onChange: setRegenerate }),
1225
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-actions", children: [
1226
+ confirming === "regenerate" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1227
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "primary", onClick: () => {
1228
+ setConfirming(null);
1229
+ void install();
1230
+ }, children: t("confirmRegenerate") }),
1231
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "ghost", onClick: () => {
1232
+ setConfirming(null);
1233
+ }, children: t("cancel") })
1234
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1235
+ import_dsh_client_ui_primitives.Button,
1236
+ {
1237
+ variant: "primary",
1238
+ disabled: disabled || needsSave,
1239
+ onClick: () => {
1240
+ if (regenerate) setConfirming("regenerate");
1241
+ else void install();
1242
+ },
1243
+ children: t("install")
1244
+ }
1245
+ ),
1246
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1247
+ import_dsh_client_ui_primitives.Button,
1248
+ {
1249
+ variant: "outline",
1250
+ disabled: disabled || needsSave,
1251
+ onClick: () => {
1252
+ void op("status", api.status, (r) => {
1253
+ const res = r.result ?? {};
1254
+ if (res.installed !== true) return { tone: "info", text: t("statusNotInstalled") };
1255
+ const listening = Object.entries(res.listening ?? {}).map(([id, ok]) => `${PROTOCOLS.find((p) => p.id === id)?.name ?? id} ${ok ? "\u2713" : "\u2717"}`).join(" ");
1256
+ const head = res.active === true ? t("statusActive", { version: String(res.version ?? "") }) : t("statusInactive");
1257
+ const cert = res.cert ?? {};
1258
+ const hours = cert.expires_in_hours ?? 0;
1259
+ const certProblem = cert.mode === "ip" && (cert.issued !== true || hours < 24);
1260
+ const certLine = cert.mode !== "ip" ? "" : cert.issued !== true ? `
1261
+ ${t("certNotIssued")}` : hours < 0 ? `
1262
+ ${t("certExpired")}` : hours < 24 ? `
1263
+ ${t("certExpiring", { hours })}` : `
1264
+ ${t("certExpires", { date: new Date(cert.expires ?? "").toLocaleString(), hours })}`;
1265
+ const healthy = res.active === true && !certProblem;
1266
+ return { tone: healthy ? "success" : "error", text: `${head} ${t("listening")}: ${listening}${certLine}` };
1267
+ });
1268
+ },
1269
+ children: t("status")
1270
+ }
1271
+ ),
1272
+ confirming === "uninstall" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1273
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1274
+ import_dsh_client_ui_primitives.Button,
1275
+ {
1276
+ variant: "primary",
1277
+ className: "dshvpn-danger",
1278
+ onClick: () => {
1279
+ setConfirming(null);
1280
+ void op("uninstall", api.uninstall, () => {
1281
+ setLinks(null);
1282
+ return { tone: "success", text: t("uninstalled") };
1283
+ });
1284
+ },
1285
+ children: t("confirmUninstall")
1286
+ }
1287
+ ),
1288
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "ghost", onClick: () => {
1289
+ setConfirming(null);
1290
+ }, children: t("cancel") })
1291
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "ghost", className: "dshvpn-danger", disabled: disabled || needsSave, onClick: () => {
1292
+ setConfirming("uninstall");
1293
+ }, children: t("uninstall") })
1294
+ ] })
1295
+ ] }),
1296
+ (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 }),
1297
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
1298
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-row", children: [
1299
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: t("qrSection") }),
1300
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1301
+ import_dsh_client_ui_primitives.Button,
1302
+ {
1303
+ variant: "outline",
1304
+ size: "sm",
1305
+ disabled: disabled || needsSave,
1306
+ onClick: () => {
1307
+ void op("links", api.links, (r) => {
1308
+ if (r.links) setLinks(r.links);
1309
+ return null;
1310
+ });
1311
+ },
1312
+ children: t("refresh")
1313
+ }
1314
+ )
1315
+ ] }),
1316
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("qrHint") }),
1317
+ links ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshvpn-qr-grid", children: links.items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(QrCard, { item, t }, item.id)) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dshvpn-muted", children: t("qrEmpty") })
1318
+ ] })
1319
+ ] });
1320
+ }
1321
+
1322
+ // src/client/index.ts
1323
+ var PACKAGE_NAME = "dsh-vpn";
1324
+ var inject = ["slots", "locale"];
1325
+ function apply(ctx) {
1326
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-vpn: dictionaries");
1327
+ ctx.effect(() => {
1328
+ const style = document.createElement("style");
1329
+ style.dataset.dshPlugin = PACKAGE_NAME;
1330
+ style.textContent = STYLES;
1331
+ document.head.append(style);
1332
+ return () => {
1333
+ style.remove();
1334
+ };
1335
+ }, "dsh-vpn: styles");
1336
+ ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
1337
+ name: "plugins.bundle.config",
1338
+ key: PACKAGE_NAME,
1339
+ locale: NS,
1340
+ inject: () => ({ api: vpnApi })
1341
+ }, VpnSettingsPage));
1342
+ }
1343
+
1344
+ return module.exports;
1345
+ }
1346
+ });