profile-manager-secure 1.0.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.
Files changed (70) hide show
  1. package/dist/cli.d.ts +2 -0
  2. package/dist/cli.js +107 -0
  3. package/dist/cli.js.map +1 -0
  4. package/dist/frontend/assets/index-D0IIAYWB.css +1 -0
  5. package/dist/frontend/assets/index-D9Y0_s3b.js +273 -0
  6. package/dist/frontend/index.html +26 -0
  7. package/dist/middleware/auth.middleware.d.ts +8 -0
  8. package/dist/middleware/auth.middleware.js +51 -0
  9. package/dist/middleware/auth.middleware.js.map +1 -0
  10. package/dist/middleware/error.middleware.d.ts +2 -0
  11. package/dist/middleware/error.middleware.js +11 -0
  12. package/dist/middleware/error.middleware.js.map +1 -0
  13. package/dist/modules/auth/auth.controller.d.ts +1 -0
  14. package/dist/modules/auth/auth.controller.js +175 -0
  15. package/dist/modules/auth/auth.controller.js.map +1 -0
  16. package/dist/modules/credential/credential.controller.d.ts +1 -0
  17. package/dist/modules/credential/credential.controller.js +263 -0
  18. package/dist/modules/credential/credential.controller.js.map +1 -0
  19. package/dist/modules/proxy/proxy.controller.d.ts +1 -0
  20. package/dist/modules/proxy/proxy.controller.js +270 -0
  21. package/dist/modules/proxy/proxy.controller.js.map +1 -0
  22. package/dist/modules/proxy/proxy.service.d.ts +28 -0
  23. package/dist/modules/proxy/proxy.service.js +185 -0
  24. package/dist/modules/proxy/proxy.service.js.map +1 -0
  25. package/dist/modules/tailscale/tailscale.controller.d.ts +1 -0
  26. package/dist/modules/tailscale/tailscale.controller.js +149 -0
  27. package/dist/modules/tailscale/tailscale.controller.js.map +1 -0
  28. package/dist/modules/tailscale/tailscale.service.d.ts +96 -0
  29. package/dist/modules/tailscale/tailscale.service.js +561 -0
  30. package/dist/modules/tailscale/tailscale.service.js.map +1 -0
  31. package/dist/modules/totp/totp.controller.d.ts +1 -0
  32. package/dist/modules/totp/totp.controller.js +41 -0
  33. package/dist/modules/totp/totp.controller.js.map +1 -0
  34. package/dist/modules/totp/totp.service.d.ts +26 -0
  35. package/dist/modules/totp/totp.service.js +96 -0
  36. package/dist/modules/totp/totp.service.js.map +1 -0
  37. package/dist/modules/totp/totp.service.test.d.ts +1 -0
  38. package/dist/modules/totp/totp.service.test.js +41 -0
  39. package/dist/modules/totp/totp.service.test.js.map +1 -0
  40. package/dist/modules/vault/apikey.service.d.ts +27 -0
  41. package/dist/modules/vault/apikey.service.js +87 -0
  42. package/dist/modules/vault/apikey.service.js.map +1 -0
  43. package/dist/modules/vault/constants.d.ts +2 -0
  44. package/dist/modules/vault/constants.js +24 -0
  45. package/dist/modules/vault/constants.js.map +1 -0
  46. package/dist/modules/vault/types.d.ts +73 -0
  47. package/dist/modules/vault/types.js +2 -0
  48. package/dist/modules/vault/types.js.map +1 -0
  49. package/dist/modules/vault/vault-migration.service.d.ts +21 -0
  50. package/dist/modules/vault/vault-migration.service.js +99 -0
  51. package/dist/modules/vault/vault-migration.service.js.map +1 -0
  52. package/dist/modules/vault/vault-storage.service.d.ts +22 -0
  53. package/dist/modules/vault/vault-storage.service.js +76 -0
  54. package/dist/modules/vault/vault-storage.service.js.map +1 -0
  55. package/dist/modules/vault/vault.service.d.ts +66 -0
  56. package/dist/modules/vault/vault.service.js +229 -0
  57. package/dist/modules/vault/vault.service.js.map +1 -0
  58. package/dist/server.d.ts +1 -0
  59. package/dist/server.js +67 -0
  60. package/dist/server.js.map +1 -0
  61. package/dist/utils/crypto.d.ts +20 -0
  62. package/dist/utils/crypto.js +54 -0
  63. package/dist/utils/crypto.js.map +1 -0
  64. package/dist/utils/crypto.test.d.ts +1 -0
  65. package/dist/utils/crypto.test.js +66 -0
  66. package/dist/utils/crypto.test.js.map +1 -0
  67. package/dist/utils/logger.d.ts +5 -0
  68. package/dist/utils/logger.js +12 -0
  69. package/dist/utils/logger.js.map +1 -0
  70. package/package.json +38 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tailscale.controller.js","sourceRoot":"","sources":["../../../src/modules/tailscale/tailscale.controller.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ export interface TailscaleStatus {
2
+ installed: boolean;
3
+ running: boolean;
4
+ backendState: string;
5
+ ip4?: string;
6
+ ip6?: string;
7
+ deviceName?: string;
8
+ tailnet?: string;
9
+ peersCount?: number;
10
+ error?: string;
11
+ isInstalling?: boolean;
12
+ installProgress?: number;
13
+ installStatus?: string;
14
+ hasCert?: boolean;
15
+ certDomain?: string;
16
+ dnsName?: string;
17
+ magicDNSSuffix?: string;
18
+ }
19
+ export declare class TailscaleService {
20
+ private cliPath;
21
+ private appDataDir;
22
+ private tailscaleDir;
23
+ private socketPath;
24
+ private statusCache;
25
+ private cacheTimestamp;
26
+ private cacheTTL;
27
+ private isRefreshingStatus;
28
+ private watchdogInterval;
29
+ private desiredStateEnabled;
30
+ private isInstalling;
31
+ private installProgress;
32
+ private installStatus;
33
+ constructor();
34
+ private detectCli;
35
+ private runCli;
36
+ isInstalled(): Promise<boolean>;
37
+ /**
38
+ * Get cached Tailscale status to protect Event Loop
39
+ */
40
+ getStatus(): Promise<TailscaleStatus>;
41
+ private refreshStatusCache;
42
+ /**
43
+ * Spawn tailscaled in userspace mode if standard daemon is unavailable
44
+ */
45
+ startUserspaceDaemon(): Promise<boolean>;
46
+ up(options: {
47
+ authKey?: string;
48
+ acceptDns?: boolean;
49
+ exitNode?: string;
50
+ }): Promise<{
51
+ success: boolean;
52
+ message: string;
53
+ }>;
54
+ down(): Promise<{
55
+ success: boolean;
56
+ message: string;
57
+ }>;
58
+ logout(): Promise<{
59
+ success: boolean;
60
+ message: string;
61
+ }>;
62
+ getFunnelStatus(): Promise<{
63
+ funnelActive: boolean;
64
+ serveActive: boolean;
65
+ funnelUrl?: string;
66
+ serveUrl?: string;
67
+ }>;
68
+ toggleFunnel(active: boolean): Promise<{
69
+ success: boolean;
70
+ message: string;
71
+ }>;
72
+ toggleServe(active: boolean): Promise<{
73
+ success: boolean;
74
+ message: string;
75
+ }>;
76
+ /**
77
+ * Provision TLS Certificate from Tailscale for secure HTTPS API access
78
+ */
79
+ provisionCert(): Promise<{
80
+ success: boolean;
81
+ message: string;
82
+ }>;
83
+ /**
84
+ * Download and Install Tailscale CLI silently on Windows
85
+ */
86
+ installTailscale(): Promise<{
87
+ success: boolean;
88
+ message: string;
89
+ }>;
90
+ private downloadMsi;
91
+ /**
92
+ * Watchdog checking connection health every 30 seconds
93
+ */
94
+ private startWatchdog;
95
+ }
96
+ export declare const tailscaleService: TailscaleService;
@@ -0,0 +1,561 @@
1
+ import { spawn } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import https from 'https';
6
+ import { logger } from '../../utils/logger.js';
7
+ export class TailscaleService {
8
+ cliPath = null;
9
+ appDataDir;
10
+ tailscaleDir;
11
+ socketPath = null;
12
+ // Cache & Watchdog variables
13
+ statusCache = null;
14
+ cacheTimestamp = 0;
15
+ cacheTTL = 10000; // 10 seconds
16
+ isRefreshingStatus = false;
17
+ watchdogInterval = null;
18
+ desiredStateEnabled = false;
19
+ // Installer state
20
+ isInstalling = false;
21
+ installProgress = 0;
22
+ installStatus = '';
23
+ constructor() {
24
+ this.appDataDir = path.join(os.homedir(), '.account-manager');
25
+ this.tailscaleDir = path.join(this.appDataDir, 'tailscale');
26
+ // Create directories if they do not exist
27
+ if (!fs.existsSync(this.appDataDir)) {
28
+ fs.mkdirSync(this.appDataDir, { recursive: true });
29
+ }
30
+ if (!fs.existsSync(this.tailscaleDir)) {
31
+ fs.mkdirSync(this.tailscaleDir, { recursive: true });
32
+ }
33
+ this.detectCli();
34
+ this.startWatchdog();
35
+ }
36
+ detectCli() {
37
+ const commonPaths = [
38
+ 'C:\\Program Files\\Tailscale\\tailscale.exe',
39
+ 'C:\\Program Files (x86)\\Tailscale\\tailscale.exe'
40
+ ];
41
+ for (const p of commonPaths) {
42
+ if (fs.existsSync(p)) {
43
+ this.cliPath = p;
44
+ logger.info(`Tailscale CLI detected at: ${p}`);
45
+ return;
46
+ }
47
+ }
48
+ this.cliPath = 'tailscale';
49
+ }
50
+ runCli(args, options = {}) {
51
+ return new Promise((resolve, reject) => {
52
+ const bin = this.cliPath || 'tailscale';
53
+ const finalArgs = [...args];
54
+ if (this.socketPath) {
55
+ finalArgs.unshift(`--socket=${this.socketPath}`);
56
+ }
57
+ // Mask sensitive info (like authkey) in log output
58
+ const loggedArgs = finalArgs.map(arg => {
59
+ if (arg.startsWith('--authkey=')) {
60
+ return '--authkey=********';
61
+ }
62
+ return arg;
63
+ });
64
+ logger.info(`Running Tailscale CLI command: ${bin} ${loggedArgs.join(' ')}`);
65
+ const child = spawn(bin, finalArgs, {
66
+ timeout: options.timeout || 30000,
67
+ windowsHide: true
68
+ });
69
+ let stdout = '';
70
+ let stderr = '';
71
+ child.stdout?.on('data', (data) => {
72
+ stdout += data.toString();
73
+ });
74
+ child.stderr?.on('data', (data) => {
75
+ stderr += data.toString();
76
+ });
77
+ child.on('close', (code) => {
78
+ if (code === 0) {
79
+ resolve({ stdout, stderr });
80
+ }
81
+ else {
82
+ const error = new Error(`Command failed with exit code ${code}`);
83
+ error.stdout = stdout;
84
+ error.stderr = stderr;
85
+ error.code = code;
86
+ reject(error);
87
+ }
88
+ });
89
+ child.on('error', (err) => {
90
+ reject(err);
91
+ });
92
+ });
93
+ }
94
+ async isInstalled() {
95
+ try {
96
+ if (this.cliPath && this.cliPath !== 'tailscale') {
97
+ return true;
98
+ }
99
+ await new Promise((resolve, reject) => {
100
+ const child = spawn(this.cliPath || 'tailscale', ['--version'], { windowsHide: true });
101
+ child.on('close', (code) => code === 0 ? resolve() : reject());
102
+ child.on('error', reject);
103
+ });
104
+ return true;
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ }
110
+ /**
111
+ * Get cached Tailscale status to protect Event Loop
112
+ */
113
+ async getStatus() {
114
+ // If installing, return installation status
115
+ if (this.isInstalling) {
116
+ return {
117
+ installed: false,
118
+ running: false,
119
+ backendState: 'Installing',
120
+ isInstalling: true,
121
+ installProgress: this.installProgress,
122
+ installStatus: this.installStatus
123
+ };
124
+ }
125
+ const now = Date.now();
126
+ const isCacheStale = now - this.cacheTimestamp > this.cacheTTL;
127
+ if (this.statusCache && !isCacheStale) {
128
+ return this.statusCache;
129
+ }
130
+ if (this.isRefreshingStatus) {
131
+ return this.statusCache || { installed: false, running: false, backendState: 'Querying' };
132
+ }
133
+ // Refresh cache in background or wait if no cache exists
134
+ if (!this.statusCache) {
135
+ await this.refreshStatusCache();
136
+ }
137
+ else {
138
+ this.refreshStatusCache().catch((err) => logger.error('Background status refresh failed:', err));
139
+ }
140
+ return this.statusCache;
141
+ }
142
+ async refreshStatusCache() {
143
+ this.isRefreshingStatus = true;
144
+ try {
145
+ const installed = await this.isInstalled();
146
+ if (!installed) {
147
+ this.statusCache = { installed: false, running: false, backendState: 'NotInstalled' };
148
+ this.cacheTimestamp = Date.now();
149
+ return;
150
+ }
151
+ // Check if SSL certs exist in our certs folder
152
+ const certsDir = path.join(this.appDataDir, 'certs');
153
+ let hasCert = false;
154
+ let certDomain = '';
155
+ if (fs.existsSync(certsDir)) {
156
+ const files = fs.readdirSync(certsDir);
157
+ const crtFile = files.find(f => f.endsWith('.crt'));
158
+ if (crtFile) {
159
+ hasCert = true;
160
+ certDomain = crtFile.replace('.crt', '');
161
+ }
162
+ }
163
+ try {
164
+ const { stdout } = await this.runCli(['status', '--json'], { timeout: 4000 });
165
+ const statusData = JSON.parse(stdout);
166
+ const self = statusData.Self;
167
+ const backendState = statusData.BackendState;
168
+ // If running, mark desired state as enabled to let watchdog keep it alive
169
+ if (backendState === 'Running') {
170
+ this.desiredStateEnabled = true;
171
+ }
172
+ this.statusCache = {
173
+ installed: true,
174
+ running: true,
175
+ backendState,
176
+ ip4: self?.TailscaleIPs?.[0] || undefined,
177
+ ip6: self?.TailscaleIPs?.[1] || undefined,
178
+ deviceName: self?.HostName || undefined,
179
+ tailnet: statusData.CurrentTailnet?.Name || undefined,
180
+ dnsName: self?.DNSName ? self.DNSName.replace(/\.$/, '') : undefined,
181
+ magicDNSSuffix: statusData.MagicDNSSuffix || statusData.CurrentTailnet?.MagicDNSSuffix || undefined,
182
+ peersCount: statusData.Peer ? Object.keys(statusData.Peer).length : 0,
183
+ hasCert,
184
+ certDomain
185
+ };
186
+ }
187
+ catch (error) {
188
+ const errMsg = error.message || '';
189
+ // Handle case where tailscaled daemon is not running
190
+ if (errMsg.includes('connect') || errMsg.includes('daemon') || errMsg.includes('tailscaled')) {
191
+ this.statusCache = {
192
+ installed: true,
193
+ running: false,
194
+ backendState: 'Stopped',
195
+ error: 'Tailscale daemon is not running.',
196
+ hasCert,
197
+ certDomain
198
+ };
199
+ return;
200
+ }
201
+ // Try reading output if tailscale status failed but returned json (e.g. logged out state)
202
+ try {
203
+ if (error.stdout) {
204
+ const statusData = JSON.parse(error.stdout);
205
+ const self = statusData.Self;
206
+ this.statusCache = {
207
+ installed: true,
208
+ running: true,
209
+ backendState: statusData.BackendState || 'NeedsLogin',
210
+ ip4: self?.TailscaleIPs?.[0] || undefined,
211
+ ip6: self?.TailscaleIPs?.[1] || undefined,
212
+ deviceName: self?.HostName || undefined,
213
+ tailnet: statusData.CurrentTailnet?.Name || undefined,
214
+ dnsName: self?.DNSName ? self.DNSName.replace(/\.$/, '') : undefined,
215
+ magicDNSSuffix: statusData.MagicDNSSuffix || statusData.CurrentTailnet?.MagicDNSSuffix || undefined,
216
+ hasCert,
217
+ certDomain
218
+ };
219
+ return;
220
+ }
221
+ }
222
+ catch { }
223
+ this.statusCache = {
224
+ installed: true,
225
+ running: true,
226
+ backendState: 'NeedsLogin',
227
+ error: errMsg || 'Tailscale requires authentication.',
228
+ hasCert,
229
+ certDomain
230
+ };
231
+ }
232
+ }
233
+ finally {
234
+ this.cacheTimestamp = Date.now();
235
+ this.isRefreshingStatus = false;
236
+ }
237
+ }
238
+ /**
239
+ * Spawn tailscaled in userspace mode if standard daemon is unavailable
240
+ */
241
+ async startUserspaceDaemon() {
242
+ if (os.platform() !== 'win32')
243
+ return false; // Windows focus
244
+ const binPath = this.cliPath && this.cliPath !== 'tailscale'
245
+ ? path.join(path.dirname(this.cliPath), 'tailscaled.exe')
246
+ : 'tailscaled';
247
+ this.socketPath = path.join(this.tailscaleDir, 'tailscaled.sock');
248
+ logger.info(`Starting local Userspace Tailscale daemon: ${binPath}`);
249
+ // Kill any existing userspace tailscaled processes first
250
+ try {
251
+ await new Promise((resolve) => {
252
+ const child = spawn('taskkill', ['/f', '/im', 'tailscaled.exe'], { windowsHide: true });
253
+ child.on('close', () => resolve());
254
+ child.on('error', () => resolve());
255
+ }).catch(() => { });
256
+ }
257
+ catch { }
258
+ const daemonArgs = [
259
+ `--socket=${this.socketPath}`,
260
+ `--statedir=${this.tailscaleDir}`,
261
+ `--no-tun`
262
+ ];
263
+ const child = spawn(binPath, daemonArgs, {
264
+ detached: true,
265
+ stdio: 'ignore',
266
+ windowsHide: true
267
+ });
268
+ child.unref();
269
+ // Wait 2 seconds for socket initialization
270
+ await new Promise((resolve) => setTimeout(resolve, 2000));
271
+ // Verify it started by checking status
272
+ try {
273
+ const status = await this.getStatus();
274
+ return status.running;
275
+ }
276
+ catch {
277
+ return false;
278
+ }
279
+ }
280
+ async up(options) {
281
+ try {
282
+ this.desiredStateEnabled = true;
283
+ let status = await this.getStatus();
284
+ // If daemon is not running, try starting in userspace mode
285
+ if (!status.running) {
286
+ const daemonStarted = await this.startUserspaceDaemon();
287
+ if (!daemonStarted) {
288
+ return { success: false, message: 'Không thể khởi động dịch vụ Tailscale. Hãy đảm bảo bạn đã chạy app hoặc cài đặt dịch vụ.' };
289
+ }
290
+ }
291
+ const cmdArgs = ['up'];
292
+ if (options.authKey) {
293
+ cmdArgs.push(`--authkey=${options.authKey}`);
294
+ }
295
+ if (options.acceptDns !== undefined) {
296
+ cmdArgs.push(`--accept-dns=${options.acceptDns}`);
297
+ }
298
+ if (options.exitNode) {
299
+ cmdArgs.push(`--exit-node=${options.exitNode}`);
300
+ }
301
+ cmdArgs.push('--reset');
302
+ await this.runCli(cmdArgs, { timeout: 30000 });
303
+ this.cacheTimestamp = 0; // Invalidate cache
304
+ return { success: true, message: 'Connected to Tailscale successfully' };
305
+ }
306
+ catch (error) {
307
+ logger.error('Failed to connect Tailscale:', error);
308
+ return { success: false, message: error.message || 'Failed to connect' };
309
+ }
310
+ }
311
+ async down() {
312
+ try {
313
+ this.desiredStateEnabled = false;
314
+ await this.runCli(['down']);
315
+ this.cacheTimestamp = 0; // Invalidate cache
316
+ return { success: true, message: 'Disconnected from Tailscale' };
317
+ }
318
+ catch (error) {
319
+ logger.error('Failed to disconnect Tailscale:', error);
320
+ return { success: false, message: error.message || 'Failed to disconnect' };
321
+ }
322
+ }
323
+ async logout() {
324
+ try {
325
+ this.desiredStateEnabled = false;
326
+ await this.runCli(['logout']);
327
+ // Clean up userspace socket if running
328
+ if (this.socketPath && fs.existsSync(this.socketPath)) {
329
+ try {
330
+ fs.unlinkSync(this.socketPath);
331
+ }
332
+ catch { }
333
+ this.socketPath = null;
334
+ }
335
+ this.cacheTimestamp = 0; // Invalidate cache
336
+ return { success: true, message: 'Logged out of Tailscale' };
337
+ }
338
+ catch (error) {
339
+ logger.error('Failed to logout Tailscale:', error);
340
+ return { success: false, message: error.message || 'Failed to logout' };
341
+ }
342
+ }
343
+ async getFunnelStatus() {
344
+ try {
345
+ const installed = await this.isInstalled();
346
+ if (!installed) {
347
+ return { funnelActive: false, serveActive: false };
348
+ }
349
+ // 1. Run tailscale serve status --json
350
+ const { stdout: serveStdout } = await this.runCli(['serve', 'status', '--json']).catch(() => ({ stdout: '{}' }));
351
+ const serveData = JSON.parse(serveStdout || '{}');
352
+ // 2. Run tailscale funnel status --json
353
+ const { stdout: funnelStdout } = await this.runCli(['funnel', 'status', '--json']).catch(() => ({ stdout: '{}' }));
354
+ const funnelData = JSON.parse(funnelStdout || '{}');
355
+ // Fetch DNSName from status to get stable URL (avoids suffix conflicts)
356
+ const status = await this.getStatus();
357
+ const host = status.dnsName || (status.ip4 ? (status.deviceName + '.' + (status.tailnet || '')).replace(/\.$/, '') : '');
358
+ const serveActive = serveData.Web && Object.keys(serveData.Web).length > 0;
359
+ const funnelActive = funnelData.AllowFunnel && Object.keys(funnelData.AllowFunnel).length > 0;
360
+ return {
361
+ funnelActive: !!funnelActive,
362
+ serveActive: !!serveActive,
363
+ funnelUrl: funnelActive && host ? `https://${host}` : undefined,
364
+ serveUrl: serveActive && host ? `https://${host}` : undefined
365
+ };
366
+ }
367
+ catch (error) {
368
+ logger.error('Failed to get funnel/serve status:', error);
369
+ return { funnelActive: false, serveActive: false };
370
+ }
371
+ }
372
+ async toggleFunnel(active) {
373
+ try {
374
+ if (active) {
375
+ // Expose port 18823 in the background
376
+ await this.runCli(['funnel', '--bg', '--yes', '18823']);
377
+ return { success: true, message: 'Public Tunnel (Tailscale Funnel) enabled' };
378
+ }
379
+ else {
380
+ await this.runCli(['funnel', 'reset']);
381
+ return { success: true, message: 'Public Tunnel disabled' };
382
+ }
383
+ }
384
+ catch (error) {
385
+ logger.error('Failed to toggle Funnel:', error);
386
+ return { success: false, message: error.message || 'Failed to toggle Funnel' };
387
+ }
388
+ }
389
+ async toggleServe(active) {
390
+ try {
391
+ if (active) {
392
+ // Expose port 18823 in the background inside Tailnet
393
+ await this.runCli(['serve', '--bg', '--yes', '18823']);
394
+ return { success: true, message: 'Tailnet HTTPS Serve enabled' };
395
+ }
396
+ else {
397
+ await this.runCli(['serve', 'reset']);
398
+ return { success: true, message: 'Tailnet HTTPS Serve disabled' };
399
+ }
400
+ }
401
+ catch (error) {
402
+ logger.error('Failed to toggle Serve:', error);
403
+ return { success: false, message: error.message || 'Failed to toggle Serve' };
404
+ }
405
+ }
406
+ /**
407
+ * Provision TLS Certificate from Tailscale for secure HTTPS API access
408
+ */
409
+ async provisionCert() {
410
+ const status = await this.getStatus();
411
+ const domain = status.dnsName || (status.deviceName && status.magicDNSSuffix ? `${status.deviceName}.${status.magicDNSSuffix}` : '');
412
+ if (!status.running || !domain) {
413
+ return { success: false, message: 'Tailscale VPN phải hoạt động và có tên miền hợp lệ để cấp chứng chỉ TLS.' };
414
+ }
415
+ const certsDir = path.join(this.appDataDir, 'certs');
416
+ if (!fs.existsSync(certsDir)) {
417
+ fs.mkdirSync(certsDir, { recursive: true });
418
+ }
419
+ const certFile = path.join(certsDir, `${domain}.crt`);
420
+ const keyFile = path.join(certsDir, `${domain}.key`);
421
+ try {
422
+ logger.info(`Provisioning TLS cert for ${domain}...`);
423
+ await this.runCli(['cert', '--cert-file', certFile, '--key-file', keyFile, domain], { timeout: 45000 });
424
+ this.cacheTimestamp = 0; // Invalidate cache
425
+ return { success: true, message: `Cấp chứng chỉ TLS thành công cho tên miền: ${domain}` };
426
+ }
427
+ catch (error) {
428
+ logger.error('Failed to provision TLS certificate:', error);
429
+ return { success: false, message: `Lỗi cấp chứng chỉ: ${error.message || 'Unknown error'}` };
430
+ }
431
+ }
432
+ /**
433
+ * Download and Install Tailscale CLI silently on Windows
434
+ */
435
+ async installTailscale() {
436
+ if (this.isInstalling) {
437
+ return { success: false, message: 'Quá trình cài đặt đang diễn ra.' };
438
+ }
439
+ if (os.platform() !== 'win32') {
440
+ return { success: false, message: 'Tính năng cài đặt tự động hiện tại chỉ khả dụng trên Windows.' };
441
+ }
442
+ this.isInstalling = true;
443
+ this.installProgress = 0;
444
+ this.installStatus = 'Đang chuẩn bị tải xuống...';
445
+ const msiUrl = 'https://pkgs.tailscale.com/stable/tailscale-setup-latest-amd64.msi';
446
+ const msiPath = path.join(os.tmpdir(), 'tailscale-setup.msi');
447
+ // Run async install process
448
+ (async () => {
449
+ try {
450
+ this.installStatus = 'Đang tải bộ cài đặt Tailscale MSI (khoảng 30MB)...';
451
+ await this.downloadMsi(msiUrl, msiPath, (pct) => {
452
+ this.installProgress = Math.round(pct * 0.7); // 70% progress for downloading
453
+ });
454
+ this.installStatus = 'Đang thực thi cài đặt ngầm. Vui lòng cấp quyền Administrator (UAC) nếu được hỏi...';
455
+ this.installProgress = 80;
456
+ const msiArgs = `'/i','${msiPath}','TS_NOLAUNCH=true','/quiet','/norestart'`;
457
+ const cmd = `Start-Process msiexec -ArgumentList ${msiArgs} -Verb RunAs -Wait`;
458
+ await new Promise((resolve, reject) => {
459
+ const child = spawn('powershell', ['-NoProfile', '-NonInteractive', '-Command', cmd], {
460
+ timeout: 60000,
461
+ windowsHide: true
462
+ });
463
+ let stderr = '';
464
+ child.stderr?.on('data', (d) => stderr += d.toString());
465
+ child.on('close', (code) => {
466
+ if (code === 0)
467
+ resolve();
468
+ else
469
+ reject(new Error(`Powershell installation exited with code ${code}. Stderr: ${stderr}`));
470
+ });
471
+ child.on('error', reject);
472
+ });
473
+ this.installStatus = 'Đang kiểm tra kết quả cài đặt...';
474
+ this.installProgress = 95;
475
+ // Poll for 5 seconds to wait for file system update
476
+ let found = false;
477
+ for (let i = 0; i < 5; i++) {
478
+ this.detectCli();
479
+ if (this.cliPath && this.cliPath !== 'tailscale') {
480
+ found = true;
481
+ break;
482
+ }
483
+ await new Promise((r) => setTimeout(r, 1000));
484
+ }
485
+ if (!found) {
486
+ throw new Error('Đã cài đặt xong nhưng không tìm thấy file tailscale.exe');
487
+ }
488
+ this.installProgress = 100;
489
+ this.installStatus = 'Cài đặt Tailscale hoàn tất thành công!';
490
+ logger.info('Tailscale silent install completed successfully.');
491
+ }
492
+ catch (error) {
493
+ logger.error('Tailscale silent install failed:', error);
494
+ this.installStatus = `Cài đặt thất bại: ${error.message || 'Unknown error'}`;
495
+ }
496
+ finally {
497
+ this.isInstalling = false;
498
+ try {
499
+ if (fs.existsSync(msiPath)) {
500
+ fs.unlinkSync(msiPath);
501
+ }
502
+ }
503
+ catch { }
504
+ }
505
+ })();
506
+ return { success: true, message: 'Bắt đầu quá trình tải và cài đặt Tailscale ngầm.' };
507
+ }
508
+ downloadMsi(url, dest, onProgress) {
509
+ return new Promise((resolve, reject) => {
510
+ const file = fs.createWriteStream(dest);
511
+ https.get(url, (res) => {
512
+ if (res.statusCode !== 200) {
513
+ reject(new Error(`Tải xuống thất bại với HTTP code: ${res.statusCode}`));
514
+ return;
515
+ }
516
+ const total = parseInt(res.headers['content-length'] || '0', 10);
517
+ let downloaded = 0;
518
+ res.on('data', (chunk) => {
519
+ downloaded += chunk.length;
520
+ if (total > 0) {
521
+ onProgress((downloaded / total) * 100);
522
+ }
523
+ });
524
+ res.pipe(file);
525
+ file.on('finish', () => {
526
+ file.close();
527
+ resolve();
528
+ });
529
+ }).on('error', (err) => {
530
+ fs.unlink(dest, () => { });
531
+ reject(err);
532
+ });
533
+ });
534
+ }
535
+ /**
536
+ * Watchdog checking connection health every 30 seconds
537
+ */
538
+ startWatchdog() {
539
+ if (this.watchdogInterval)
540
+ return;
541
+ this.watchdogInterval = setInterval(async () => {
542
+ if (!this.desiredStateEnabled || this.isInstalling)
543
+ return;
544
+ try {
545
+ const status = await this.getStatus();
546
+ // If it is stopped unexpectedly, try to bring it up in userspace
547
+ if (status.installed && status.backendState === 'Stopped') {
548
+ logger.warn('[Tailscale Watchdog] Service stopped unexpectedly, attempting self-healing...');
549
+ await this.up({ acceptDns: true });
550
+ }
551
+ }
552
+ catch (err) {
553
+ logger.error('[Tailscale Watchdog] Error checking connection state:', err);
554
+ }
555
+ }, 30000);
556
+ // Make sure interval doesn't hold the process open
557
+ this.watchdogInterval.unref();
558
+ }
559
+ }
560
+ export const tailscaleService = new TailscaleService();
561
+ //# sourceMappingURL=tailscale.service.js.map