@miraj181/ipingyou 2.1.6 → 2.1.15

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/src/modes/host.js CHANGED
@@ -13,19 +13,21 @@
13
13
  * ============================================================
14
14
  */
15
15
 
16
- import { execa, execaCommand } from 'execa';
16
+ import { execa } from 'execa';
17
17
  import chalk from 'chalk';
18
18
  import inquirer from 'inquirer';
19
19
  import path from 'node:path';
20
20
  import { fileURLToPath } from 'node:url';
21
+ import { createRequire } from 'node:module';
21
22
  import fs from 'node:fs';
22
23
  import os from 'node:os';
23
24
  import { generateUID } from '../lib/uid.js';
24
- import { decrypt } from '../lib/crypto.js';
25
+ import { decryptAsync } from '../lib/crypto.js';
25
26
  import { cleanupAll, killProcessTree, trackPID, untrackPID, setRevokeOnExit, addCleanupHook } from '../lib/cleanup.js';
26
27
  import { detectOS } from '../lib/platform.js';
27
28
  import { createSpinner, networkSpinner, typeText } from '../lib/animations.js';
28
29
  import { startChatServer, openLocalChatUI } from '../lib/chat.js';
30
+ import { secureSensitive } from '../lib/secure-print.js';
29
31
  import { spawnTunnelSupervised } from '../lib/tunnel.js';
30
32
  import { decideApprovalRequest, fetchApprovalRequests, pingBroker, registerWithBroker, revokeUID } from '../lib/broker.js';
31
33
  import { cleanupSessionLog, getSessionLogPath, initSessionLog, logSessionEvent, recordEvent } from '../lib/session-log.js';
@@ -55,24 +57,24 @@ async function ensureSSHRunning() {
55
57
  try {
56
58
  if (osInfo.isLinux) {
57
59
  try {
58
- await execaCommand('systemctl is-active ssh', { reject: true });
60
+ await execa('systemctl', ['is-active', 'ssh'], { reject: true });
59
61
  spinner.succeed('SSH service is active');
60
62
  } catch {
61
63
  spinner.text = 'Starting SSH service...';
62
64
  try {
63
- await execaCommand('sudo systemctl start ssh', { stdio: 'inherit' });
65
+ await execa('sudo', ['systemctl', 'start', 'ssh'], { stdio: 'inherit' });
64
66
  spinner.succeed('SSH service started');
65
67
  } catch {
66
- await execaCommand('sudo systemctl start sshd', { stdio: 'inherit' });
68
+ await execa('sudo', ['systemctl', 'start', 'sshd'], { stdio: 'inherit' });
67
69
  spinner.succeed('SSH service started (sshd)');
68
70
  }
69
71
  }
70
72
  } else if (osInfo.isMac) {
71
73
  try {
72
- const { stdout } = await execaCommand('sudo systemsetup -getremotelogin', { reject: false });
74
+ const { stdout } = await execa('sudo', ['systemsetup', '-getremotelogin'], { reject: false });
73
75
  if (stdout.toLowerCase().includes('off')) {
74
76
  spinner.text = 'Enabling Remote Login...';
75
- await execaCommand('sudo systemsetup -setremotelogin on', { stdio: 'inherit' });
77
+ await execa('sudo', ['systemsetup', '-setremotelogin', 'on'], { stdio: 'inherit' });
76
78
  spinner.succeed('Remote Login enabled');
77
79
  } else {
78
80
  spinner.succeed('SSH (Remote Login) is active');
@@ -82,10 +84,10 @@ async function ensureSSHRunning() {
82
84
  }
83
85
  } else if (osInfo.isWindows) {
84
86
  try {
85
- const { stdout } = await execaCommand('sc query sshd', { reject: false });
87
+ const { stdout } = await execa('sc', ['query', 'sshd'], { reject: false });
86
88
  if (stdout.includes('STOPPED')) {
87
89
  spinner.text = 'Starting OpenSSH Server...';
88
- await execaCommand('net start sshd', { stdio: 'inherit' });
90
+ await execa('net', ['start', 'sshd'], { stdio: 'inherit' });
89
91
  spinner.succeed('OpenSSH Server started');
90
92
  } else if (stdout.includes('RUNNING')) {
91
93
  spinner.succeed('OpenSSH Server is running');
@@ -112,64 +114,34 @@ async function ensureTmuxInstalled() {
112
114
  const spinner = createSpinner('Checking tmux installation...', networkSpinner).start();
113
115
  try {
114
116
  try {
115
- await execaCommand('tmux -V', { reject: true });
117
+ await execa('tmux', ['-V'], { reject: true });
116
118
  spinner.succeed('tmux is installed (Terminal Mirroring available)');
117
119
  } catch {
118
120
  spinner.text = 'tmux not found. Attempting to install...';
119
121
  if (osInfo.isLinux) {
120
122
  if (fs.existsSync('/usr/bin/apt') || fs.existsSync('/usr/bin/apt-get')) {
121
- await execaCommand('sudo apt-get update && sudo apt-get install -y tmux', { shell: true, stdio: 'inherit' });
123
+ await execa('sudo', ['apt-get', 'update', '-qq'], { stdio: 'inherit' });
124
+ await execa('sudo', ['apt-get', 'install', '-y', 'tmux'], { stdio: 'inherit' });
122
125
  } else if (fs.existsSync('/usr/bin/dnf')) {
123
- await execaCommand('sudo dnf install -y tmux', { shell: true, stdio: 'inherit' });
126
+ await execa('sudo', ['dnf', 'install', '-y', 'tmux'], { stdio: 'inherit' });
124
127
  } else if (fs.existsSync('/usr/bin/yum')) {
125
- await execaCommand('sudo yum install -y tmux', { shell: true, stdio: 'inherit' });
128
+ await execa('sudo', ['yum', 'install', '-y', 'tmux'], { stdio: 'inherit' });
126
129
  } else if (fs.existsSync('/usr/bin/pacman')) {
127
- await execaCommand('sudo pacman -S --noconfirm tmux', { shell: true, stdio: 'inherit' });
130
+ await execa('sudo', ['pacman', '-S', '--noconfirm', 'tmux'], { stdio: 'inherit' });
128
131
  } else if (fs.existsSync('/sbin/apk')) {
129
- await execaCommand('sudo apk add tmux', { shell: true, stdio: 'inherit' });
132
+ await execa('sudo', ['apk', 'add', 'tmux'], { stdio: 'inherit' });
130
133
  } else {
131
134
  throw new Error('Unsupported Linux package manager');
132
135
  }
133
136
  spinner.succeed('tmux installed successfully (Terminal Mirroring available)');
134
137
  } else if (osInfo.isMac) {
135
138
  try {
136
- await execaCommand('brew install tmux', { shell: true, stdio: 'inherit' });
139
+ await execa('brew', ['install', 'tmux'], { stdio: 'inherit' });
137
140
  spinner.succeed('tmux installed successfully (Terminal Mirroring available)');
138
141
  } catch {
139
142
  throw new Error('Homebrew is required to install tmux on macOS');
140
143
  }
141
144
  }
142
-
143
- function isSecureLinkSession(name) {
144
- return name === TMUX_SESSION_NAME || name.startsWith(TMUX_SESSION_PREFIX);
145
- }
146
-
147
- async function listTmuxSessions(socketArgs = []) {
148
- const result = await execa('tmux', [...socketArgs, 'list-sessions', '-F', '#{session_name}|#{session_created}'], { reject: false });
149
- if (result.exitCode !== 0) return [];
150
- return result.stdout
151
- .split(/\r?\n/)
152
- .filter(Boolean)
153
- .map(line => {
154
- const [name, createdAt] = line.split('|');
155
- return { name, createdAt: Number(createdAt) || null };
156
- });
157
- }
158
-
159
- async function getMirrorableSessions() {
160
- const sessions = [];
161
- const customSessions = await listTmuxSessions(tmuxSocketArgs());
162
- customSessions
163
- .filter(s => isSecureLinkSession(s.name))
164
- .forEach(s => sessions.push({ ...s, socketArgs: tmuxSocketArgs(), source: 'custom' }));
165
-
166
- const legacySessions = await listTmuxSessions();
167
- legacySessions
168
- .filter(s => isSecureLinkSession(s.name))
169
- .forEach(s => sessions.push({ ...s, socketArgs: [], source: 'legacy' }));
170
-
171
- return sessions;
172
- }
173
145
  }
174
146
  } catch (err) {
175
147
  spinner.fail(`tmux check/install failed: ${err.message}`);
@@ -177,6 +149,37 @@ async function ensureTmuxInstalled() {
177
149
  }
178
150
  }
179
151
 
152
+ function isSecureLinkSession(name) {
153
+ return name === TMUX_SESSION_NAME || name.startsWith(TMUX_SESSION_PREFIX);
154
+ }
155
+
156
+ async function listTmuxSessions(socketArgs = []) {
157
+ const result = await execa('tmux', [...socketArgs, 'list-sessions', '-F', '#{session_name}|#{session_created}'], { reject: false });
158
+ if (result.exitCode !== 0) return [];
159
+ return result.stdout
160
+ .split(/\r?\n/)
161
+ .filter(Boolean)
162
+ .map(line => {
163
+ const [name, createdAt] = line.split('|');
164
+ return { name, createdAt: Number(createdAt) || null };
165
+ });
166
+ }
167
+
168
+ async function getMirrorableSessions() {
169
+ const sessions = [];
170
+ const customSessions = await listTmuxSessions(tmuxSocketArgs());
171
+ customSessions
172
+ .filter(s => isSecureLinkSession(s.name))
173
+ .forEach(s => sessions.push({ ...s, socketArgs: tmuxSocketArgs(), source: 'custom' }));
174
+
175
+ const legacySessions = await listTmuxSessions();
176
+ legacySessions
177
+ .filter(s => isSecureLinkSession(s.name))
178
+ .forEach(s => sessions.push({ ...s, socketArgs: [], source: 'legacy' }));
179
+
180
+ return sessions;
181
+ }
182
+
180
183
  // ─── Ephemeral SSH Key Management ────────────────────────────
181
184
  async function generateEphemeralKey() {
182
185
  const tmpDir = os.tmpdir() || process.env.TMPDIR || process.env.TEMP || process.env.TMP;
@@ -275,6 +278,44 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
275
278
  const { default: open } = await import('open');
276
279
  const app = express();
277
280
  const startedAt = new Date().toISOString();
281
+ const decryptedClientCache = new Map();
282
+ const MAX_DECRYPTED_CLIENT_CACHE = 100;
283
+ const activeEventStreams = new Set();
284
+ const MAX_EVENT_STREAMS = 5;
285
+
286
+ async function fetchDecryptedClients() {
287
+ const brokerRes = await fetch(`${BROKER_URL}/clients/${uid}`, {
288
+ headers: sessionState.hostToken ? { 'x-host-token': sessionState.hostToken } : {}
289
+ });
290
+ if (!brokerRes.ok) {
291
+ const data = await brokerRes.json().catch(() => ({}));
292
+ throw new Error(data.error || 'Failed to fetch clients');
293
+ }
294
+ const data = await brokerRes.json();
295
+ const activeCacheKeys = new Set();
296
+ const decryptedClients = await Promise.all((data.clients || []).map(async (clientBlob) => {
297
+ const cacheKey = `${clientBlob.iv}:${clientBlob.salt}:${clientBlob.ciphertext}`;
298
+ activeCacheKeys.add(cacheKey);
299
+ const cached = decryptedClientCache.get(cacheKey);
300
+ if (cached) return { ...cached, seenAt: clientBlob.seenAt || null };
301
+ try {
302
+ const decrypted = await decryptAsync(clientBlob.iv, clientBlob.ciphertext, password, clientBlob.salt);
303
+ const t = JSON.parse(decrypted);
304
+ decryptedClientCache.set(cacheKey, t);
305
+ return { ...t, seenAt: clientBlob.seenAt || null };
306
+ } catch {
307
+ const failed = { error: 'decrypt_failed' };
308
+ decryptedClientCache.set(cacheKey, failed);
309
+ return { ...failed, seenAt: clientBlob.seenAt || null };
310
+ }
311
+ }));
312
+ for (const key of decryptedClientCache.keys()) {
313
+ if (!activeCacheKeys.has(key) || decryptedClientCache.size > MAX_DECRYPTED_CLIENT_CACHE) {
314
+ decryptedClientCache.delete(key);
315
+ }
316
+ }
317
+ return { clients: decryptedClients };
318
+ }
278
319
 
279
320
  app.get('/api/status', (_req, res) => {
280
321
  res.json({
@@ -291,47 +332,91 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
291
332
 
292
333
  app.use(express.json());
293
334
 
335
+ app.get('/api/approvals', async (_req, res) => {
336
+ try {
337
+ const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken);
338
+ res.json(data);
339
+ } catch (err) {
340
+ res.status(500).json({ error: err.message });
341
+ }
342
+ });
343
+
294
344
  // Server-Sent Events for live telemetry & approvals
295
345
  app.get('/api/events', async (req, res) => {
346
+ if (activeEventStreams.size >= MAX_EVENT_STREAMS) {
347
+ return res.status(503).json({ error: 'Too many dashboard event streams' });
348
+ }
349
+ activeEventStreams.add(res);
296
350
  res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' });
297
351
  res.flushHeaders?.();
298
352
 
299
353
  let closed = false;
300
- const push = async () => {
354
+ let timer = null;
355
+ let intervalMs = 5000;
356
+ let unchangedCycles = 0;
357
+ let lastApprovals = '';
358
+ let lastClients = '';
359
+
360
+ const writeEvent = (event, payload) => {
361
+ if (closed) return;
362
+ res.write(`event: ${event}\n`);
363
+ res.write(`data: ${JSON.stringify(payload)}\n\n`);
364
+ };
365
+
366
+ const scheduleNext = () => {
367
+ if (closed) return;
368
+ timer = setTimeout(pushLoop, intervalMs);
369
+ };
370
+
371
+ const pushLoop = async () => {
301
372
  if (closed) return;
302
373
  try {
303
- const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken).catch(() => ({ approvals: [] }));
304
- res.write(`event: approvals\n`);
305
- res.write(`data: ${JSON.stringify(data)}\n\n`);
306
- } catch { }
374
+ const [approvalData, clientData] = await Promise.all([
375
+ fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken).catch(() => ({ approvals: [] })),
376
+ fetchDecryptedClients().catch(() => ({ clients: [] })),
377
+ ]);
378
+
379
+ const approvalsPayload = { approvals: approvalData.approvals || [] };
380
+ const clientsPayload = { clients: clientData.clients || [] };
381
+ const approvalsHash = JSON.stringify(approvalsPayload.approvals);
382
+ const clientsHash = JSON.stringify(clientsPayload.clients);
383
+ const changed = approvalsHash !== lastApprovals || clientsHash !== lastClients;
384
+
385
+ if (approvalsHash !== lastApprovals) {
386
+ writeEvent('approvals', approvalsPayload);
387
+ lastApprovals = approvalsHash;
388
+ }
389
+ if (clientsHash !== lastClients) {
390
+ writeEvent('clients', clientsPayload);
391
+ lastClients = clientsHash;
392
+ }
393
+
394
+ if (changed) {
395
+ unchangedCycles = 0;
396
+ intervalMs = 5000;
397
+ } else {
398
+ unchangedCycles += 1;
399
+ intervalMs = Math.min(20000, 5000 * (2 ** Math.min(2, unchangedCycles)));
400
+ }
401
+ } finally {
402
+ scheduleNext();
403
+ }
307
404
  };
308
405
 
309
- const iv = setInterval(push, 5000);
310
- req.on('close', () => { clearInterval(iv); closed = true; });
311
- // send initial payload
312
- await push();
406
+ req.on('close', () => {
407
+ closed = true;
408
+ activeEventStreams.delete(res);
409
+ if (timer) clearTimeout(timer);
410
+ });
411
+
412
+ writeEvent('ready', { ok: true });
413
+ await pushLoop();
313
414
  });
314
415
 
315
416
  app.get('/api/clients', async (_req, res) => {
316
417
  try {
317
- const brokerRes = await fetch(`${BROKER_URL}/clients/${uid}`, {
318
- headers: sessionState.hostToken ? { 'x-host-token': sessionState.hostToken } : {}
319
- });
320
- if (!brokerRes.ok) {
321
- const data = await brokerRes.json().catch(() => ({}));
322
- return res.status(brokerRes.status).json({ error: data.error || 'Failed to fetch clients' });
323
- }
324
- const data = await brokerRes.json();
325
- const clients = (data.clients || []).map((clientBlob) => {
326
- try {
327
- const decrypted = decrypt(clientBlob.iv, clientBlob.ciphertext, password, clientBlob.salt);
328
- const t = JSON.parse(decrypted);
329
- return { ...t, seenAt: clientBlob.seenAt || null };
330
- } catch {
331
- return { error: 'decrypt_failed', seenAt: clientBlob.seenAt || null };
332
- }
333
- });
334
- res.json({ clients });
418
+ const clients = await fetchDecryptedClients();
419
+ res.json(clients);
335
420
  } catch (err) {
336
421
  res.status(500).json({ error: err.message });
337
422
  }
@@ -484,12 +569,44 @@ updateUptime();
484
569
 
485
570
  // SSE for live updates
486
571
  const es = new EventSource('/api/events');
572
+ let fallbackTimer = null;
573
+ let fallbackDelay = 5000;
574
+
575
+ function scheduleFallbackPoll() {
576
+ if (fallbackTimer) return;
577
+ fallbackTimer = setTimeout(async function pollFallback() {
578
+ fallbackTimer = null;
579
+ try {
580
+ const [approvalsRes, clientsRes] = await Promise.all([
581
+ fetch('/api/approvals'),
582
+ fetch('/api/clients')
583
+ ]);
584
+ if (approvalsRes.ok) renderApprovals((await approvalsRes.json()).approvals || []);
585
+ if (clientsRes.ok) renderClients((await clientsRes.json()).clients || []);
586
+ } catch {}
587
+ fallbackDelay = Math.min(20000, fallbackDelay * 2);
588
+ scheduleFallbackPoll();
589
+ }, fallbackDelay);
590
+ }
591
+
592
+ es.addEventListener('open', () => {
593
+ fallbackDelay = 5000;
594
+ if (fallbackTimer) clearTimeout(fallbackTimer);
595
+ fallbackTimer = null;
596
+ });
597
+ es.addEventListener('error', scheduleFallbackPoll);
487
598
  es.addEventListener('approvals', (e) => {
488
599
  try {
489
600
  const data = JSON.parse(e.data);
490
601
  renderApprovals(data.approvals || []);
491
602
  } catch {}
492
603
  });
604
+ es.addEventListener('clients', (e) => {
605
+ try {
606
+ const data = JSON.parse(e.data);
607
+ renderClients(data.clients || []);
608
+ } catch {}
609
+ });
493
610
 
494
611
  function renderApprovals(approvals) {
495
612
  const container = document.getElementById('approvals');
@@ -554,16 +671,6 @@ async function revokeSession() {
554
671
  } catch (err) { showToast('Error: ' + err.message); }
555
672
  }
556
673
 
557
- // Poll client telemetry (separate from SSE for simplicity)
558
- async function pollClients() {
559
- try {
560
- const res = await fetch('/api/clients');
561
- if (!res.ok) return;
562
- const data = await res.json();
563
- renderClients(data.clients || []);
564
- } catch {}
565
- }
566
-
567
674
  function renderClients(clients) {
568
675
  const container = document.getElementById('clients');
569
676
  if (!clients || clients.length === 0) {
@@ -593,8 +700,6 @@ function renderClients(clients) {
593
700
  container.innerHTML = html;
594
701
  }
595
702
 
596
- setInterval(pollClients, 5000);
597
- pollClients();
598
703
  </script>
599
704
  </body></html>`);
600
705
  });
@@ -616,18 +721,67 @@ pollClients();
616
721
  async function spawnPrivateBroker() {
617
722
  console.log(chalk.yellow('\n ⚠️ Public Broker is unreachable. Spawning Private Broker...'));
618
723
 
724
+ const packageRoot = path.resolve(__dirname, '../..');
725
+ const serverEntrypoint = path.join(__dirname, '../server.js');
726
+ const requireFromServer = createRequire(serverEntrypoint);
727
+ const requiredBrokerPackages = ['express', 'express-rate-limit', 'helmet'];
728
+ const missingPackages = requiredBrokerPackages.filter((pkg) => {
729
+ try {
730
+ requireFromServer.resolve(pkg);
731
+ return false;
732
+ } catch {
733
+ return true;
734
+ }
735
+ });
736
+
737
+ if (missingPackages.length > 0) {
738
+ console.log(chalk.yellow(` ⚠️ Missing broker runtime packages: ${missingPackages.join(', ')}`));
739
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
740
+ const installResult = await execa(
741
+ npmCmd,
742
+ ['install', '--no-save', '--no-audit', '--no-fund', ...missingPackages],
743
+ { cwd: packageRoot, reject: false, all: true }
744
+ );
745
+
746
+ if (installResult.exitCode !== 0) {
747
+ const installOutput = installResult.all?.trim();
748
+ throw new Error(
749
+ `Private broker dependencies failed to install (${missingPackages.join(', ')})`
750
+ + (installOutput ? `: ${installOutput}` : '')
751
+ );
752
+ }
753
+
754
+ // Verify modules are now resolvable for the server entrypoint context.
755
+ const unresolved = missingPackages.filter((pkg) => {
756
+ try {
757
+ requireFromServer.resolve(pkg);
758
+ return false;
759
+ } catch {
760
+ return true;
761
+ }
762
+ });
763
+ if (unresolved.length > 0) {
764
+ throw new Error(`Private broker dependencies are still missing after install: ${unresolved.join(', ')}`);
765
+ }
766
+ }
767
+
619
768
  // 1. Spawn the broker server process
620
- const brokerProcess = execa('node', [path.join(__dirname, '../server.js')], {
769
+ const brokerProcess = execa('node', [serverEntrypoint], {
621
770
  env: { ...process.env, PORT: '4040' },
622
771
  reject: false,
623
772
  all: true,
773
+ buffer: false,
624
774
  });
625
775
  trackPID(brokerProcess.pid);
626
776
 
627
777
  let brokerExited = false;
628
- let brokerOutput = '';
778
+ const maxBrokerOutputBytes = 64 * 1024;
779
+ let brokerOutput = Buffer.alloc(0);
629
780
  brokerProcess.all?.on('data', chunk => {
630
- brokerOutput += chunk.toString();
781
+ const next = Buffer.concat([brokerOutput, Buffer.from(chunk)]);
782
+ brokerOutput = next.length > maxBrokerOutputBytes
783
+ ? next.subarray(next.length - maxBrokerOutputBytes)
784
+ : next;
631
785
  });
632
786
  brokerProcess.on('exit', () => {
633
787
  brokerExited = true;
@@ -641,7 +795,8 @@ async function spawnPrivateBroker() {
641
795
 
642
796
  await waitForValue(() => {
643
797
  if (brokerExited) {
644
- throw new Error(`Private broker exited before tunnel was ready${brokerOutput ? `: ${brokerOutput.trim()}` : ''}`);
798
+ const output = brokerOutput.toString('utf8').trim();
799
+ throw new Error(`Private broker exited before tunnel was ready${output ? `: ${output}` : ''}`);
645
800
  }
646
801
  return brokerTunnelUrl;
647
802
  }, 30000, 'Private broker tunnel startup');
@@ -674,7 +829,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
674
829
  console.log(chalk.bold(' ║ 🛡️ SecureLink — HOST MODE ACTIVE ║'));
675
830
  console.log(chalk.bold(' ╠════════════════════════════════════════════════════╣'));
676
831
  console.log(` ║ ${chalk.cyan('UID:')} ${chalk.bold.white(uid.padEnd(30))}║`);
677
- console.log(` ║ ${chalk.cyan('Password:')} ${chalk.bold.white(password.padEnd(30))}║`);
832
+ console.log(` ║ ${chalk.cyan('Password:')} ${chalk.bold.white(secureSensitive(password).padEnd(30))}║`);
678
833
  console.log(` ║ ${chalk.cyan('Service:')} ${chalk.dim(serviceConfig.type.toUpperCase() + ' (Port ' + serviceConfig.port + ')').padEnd(30)}║`);
679
834
  console.log(` ║ ${chalk.cyan('Tunnel:')} ${chalk.dim(sessionState.tunnelUrl.substring(0, 40))} ║`);
680
835
  if (serviceConfig.chatUrl) {
@@ -751,7 +906,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
751
906
  for (const request of pending) {
752
907
  let details = {};
753
908
  try {
754
- details = JSON.parse(decrypt(request.iv, request.ciphertext, password, request.salt));
909
+ details = JSON.parse(await decryptAsync(request.iv, request.ciphertext, password, request.salt));
755
910
  } catch {
756
911
  details = { error: 'Could not decrypt request metadata' };
757
912
  }
@@ -841,7 +996,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
841
996
  console.log('');
842
997
 
843
998
  try {
844
- await execaCommand('tmux -V', { reject: true });
999
+ await execa('tmux', ['-V'], { reject: true });
845
1000
  const sessions = await getMirrorableSessions();
846
1001
  if (sessions.length === 0) {
847
1002
  console.log(chalk.yellow(' ⚠️ No mirrored terminal session is active yet.'));
@@ -891,10 +1046,10 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
891
1046
  } else {
892
1047
  spinner.succeed(`Found ${data.clients.length} recent connection(s):`);
893
1048
 
894
- data.clients.forEach((clientBlob, i) => {
1049
+ for (const [i, clientBlob] of data.clients.entries()) {
895
1050
  try {
896
1051
  // Decrypt using the unique salt the client generated for this payload
897
- const decrypted = decrypt(clientBlob.iv, clientBlob.ciphertext, password, clientBlob.salt);
1052
+ const decrypted = await decryptAsync(clientBlob.iv, clientBlob.ciphertext, password, clientBlob.salt);
898
1053
  const t = JSON.parse(decrypted);
899
1054
 
900
1055
  console.log(chalk.bold.blue(`\n Client #${i + 1} (${t.username})`));
@@ -908,7 +1063,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
908
1063
  console.log(chalk.yellow(`\n Client #${i + 1}: Payload decryption failed (wrong password or corrupted).`));
909
1064
  logSessionEvent('host_telemetry_decrypt_failed', { index: i + 1 }, 'warn');
910
1065
  }
911
- });
1066
+ }
912
1067
  }
913
1068
  } catch (e) {
914
1069
  spinner.fail('Could not reach broker.');
@@ -928,9 +1083,9 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
928
1083
  const spinner = createSpinner('Terminating active SSH sessions...', networkSpinner).start();
929
1084
  try {
930
1085
  if (process.platform === 'win32') {
931
- await execaCommand('powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"name = \'sshd.exe\'\\" | Where-Object { $_.CommandLine -match \'sshd:.*@\' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"', { reject: false });
1086
+ await execa('powershell', ['-NoProfile', '-Command', "Get-CimInstance Win32_Process -Filter \"name = 'sshd.exe'\" | Where-Object { $_.CommandLine -match 'sshd:.*@' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"], { reject: false });
932
1087
  } else {
933
- await execaCommand("pkill -f 'sshd:.*@'", { shell: true, reject: false });
1088
+ await execa('pkill', ['-f', 'sshd:.*@'], { reject: false });
934
1089
  await execa('tmux', [...tmuxSocketArgs(), 'kill-server'], { reject: false });
935
1090
  const legacySessions = await listTmuxSessions();
936
1091
  for (const session of legacySessions) {
@@ -994,7 +1149,7 @@ export async function startHostMode() {
994
1149
  },
995
1150
  ]);
996
1151
  const password = pwdInput.trim() || generateUID();
997
- console.log(` ${chalk.green('✓')} Password: ${chalk.bold.white(password)}`);
1152
+ console.log(` ${chalk.green('✓')} Password: ${chalk.bold.white(secureSensitive(password))}`);
998
1153
  console.log('');
999
1154
 
1000
1155
  // ─── Broker Selection ───