@miraj181/ipingyou 2.1.19 → 2.1.22

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
@@ -22,18 +22,17 @@ import { createRequire } from 'node:module';
22
22
  import fs from 'node:fs';
23
23
  import os from 'node:os';
24
24
  import crypto from 'node:crypto';
25
- import { generateUID } from '../lib/uid.js';
26
- import { openUrl } from '../lib/open-url.js';
27
- import { decryptAsync } from '../lib/crypto.js';
28
- import { cleanupAll, killProcessTree, trackPID, untrackPID, setRevokeOnExit, addCleanupHook } from '../lib/cleanup.js';
29
- import { detectOS } from '../lib/platform.js';
30
- import { createSpinner, networkSpinner, typeText } from '../lib/animations.js';
31
- import { startChatServer, openLocalChatUI } from '../lib/chat.js';
32
- import { secureSensitive } from '../lib/secure-print.js';
33
- import { spawnTunnelSupervised } from '../lib/tunnel.js';
34
- import { decideApprovalRequest, fetchApprovalRequests, pingBroker, registerWithBroker, revokeUID } from '../lib/broker.js';
35
- import { cleanupSessionLog, getSessionLogPath, initSessionLog, logSessionEvent, recordEvent } from '../lib/session-log.js';
36
- import { TMUX_SESSION_NAME, TMUX_SESSION_PREFIX, tmuxSocketArgs } from '../lib/tmux.js';
25
+ import { generateUID } from '../lib/mod/uid.js';
26
+ import { openUrl } from '../lib/mod/open-url.js';
27
+ import { decryptAsync, encryptAsync } from '../lib/mod/crypto.js';
28
+ import { cleanupAll, killProcessTree, trackPID, untrackPID, setRevokeOnExit, addCleanupHook } from '../lib/mod/cleanup.js';
29
+ import { detectOS, isLinuxSSHActive, startLinuxSSH } from '../lib/services/platform.js';
30
+ import { createSpinner, networkSpinner, typeText } from '../lib/mod/animations.js';
31
+ import { startChatServer, openLocalChatUI } from '../lib/services/chat.js';
32
+ import { secureSensitive } from '../lib/mod/secure-print.js';
33
+ import { spawnTunnelSupervised } from '../lib/services/tunnel.js';
34
+ import { decideApprovalRequest, fetchApprovalRequests, pingBroker, registerWithBroker, revokeUID } from '../lib/client/broker.js';
35
+ import { cleanupSessionLog, getSessionLogPath, initSessionLog, logSessionEvent, recordEvent } from '../lib/mod/session-log.js';
37
36
 
38
37
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
39
38
  let BROKER_URL = process.env.BROKER_URL || 'https://ipingyou.onrender.com';
@@ -68,18 +67,13 @@ async function ensureSSHRunning() {
68
67
 
69
68
  try {
70
69
  if (osInfo.isLinux) {
71
- try {
72
- await execa('systemctl', ['is-active', 'ssh'], { reject: true });
70
+ const active = await isLinuxSSHActive();
71
+ if (active) {
73
72
  spinner.succeed('SSH service is active');
74
- } catch {
73
+ } else {
75
74
  spinner.text = 'Starting SSH service...';
76
- try {
77
- await execa('sudo', ['systemctl', 'start', 'ssh'], { stdio: 'inherit' });
78
- spinner.succeed('SSH service started');
79
- } catch {
80
- await execa('sudo', ['systemctl', 'start', 'sshd'], { stdio: 'inherit' });
81
- spinner.succeed('SSH service started (sshd)');
82
- }
75
+ await startLinuxSSH();
76
+ spinner.succeed('SSH service started');
83
77
  }
84
78
  } else if (osInfo.isMac) {
85
79
  try {
@@ -116,82 +110,191 @@ async function ensureSSHRunning() {
116
110
  }
117
111
  }
118
112
 
119
- /**
120
- * Ensure tmux is installed for terminal mirroring.
121
- */
122
- async function ensureTmuxInstalled() {
123
- const osInfo = detectOS();
124
- if (osInfo.isWindows) return;
113
+ function formatAndPrintLogLine(line) {
114
+ try {
115
+ const data = JSON.parse(line);
116
+ const time = new Date(data.timestamp).toLocaleTimeString();
117
+ const typeLabel = chalk.bold(data.type);
118
+
119
+ let color = chalk.white;
120
+ let icon = 'ℹ️';
121
+ if (data.level === 'warn') {
122
+ color = chalk.yellow;
123
+ icon = '⚠️';
124
+ } else if (data.level === 'error') {
125
+ color = chalk.red;
126
+ icon = '❌';
127
+ } else if (data.type.includes('success') || data.type.includes('complete') || data.type.includes('granted') || data.type.includes('start')) {
128
+ color = chalk.green;
129
+ icon = '✓';
130
+ }
131
+
132
+ const detailsStr = Object.keys(data.details || {}).length > 0
133
+ ? chalk.dim(JSON.stringify(data.details))
134
+ : '';
135
+
136
+ console.log(` [${chalk.dim(time)}] ${color(icon)} ${color(typeLabel)} ${detailsStr}`);
137
+ } catch {
138
+ if (line.trim()) {
139
+ console.log(` ${chalk.dim(line)}`);
140
+ }
141
+ }
142
+ }
143
+
144
+ async function viewLiveClientLogs(sharedDropPath) {
145
+ if (!sharedDropPath) {
146
+ console.log(chalk.red(' ❌ Error: Shared drop path is not configured.'));
147
+ return;
148
+ }
125
149
 
126
- const spinner = createSpinner('Checking tmux installation...', networkSpinner).start();
127
150
  try {
128
- try {
129
- await execa('tmux', ['-V'], { reject: true });
130
- spinner.succeed('tmux is installed (Terminal Mirroring available)');
131
- } catch {
132
- spinner.text = 'tmux not found. Attempting to install...';
133
- if (osInfo.isLinux) {
134
- if (fs.existsSync('/usr/bin/apt') || fs.existsSync('/usr/bin/apt-get')) {
135
- await execa('sudo', ['apt-get', 'update', '-qq'], { stdio: 'inherit' });
136
- await execa('sudo', ['apt-get', 'install', '-y', 'tmux'], { stdio: 'inherit' });
137
- } else if (fs.existsSync('/usr/bin/dnf')) {
138
- await execa('sudo', ['dnf', 'install', '-y', 'tmux'], { stdio: 'inherit' });
139
- } else if (fs.existsSync('/usr/bin/yum')) {
140
- await execa('sudo', ['yum', 'install', '-y', 'tmux'], { stdio: 'inherit' });
141
- } else if (fs.existsSync('/usr/bin/pacman')) {
142
- await execa('sudo', ['pacman', '-S', '--noconfirm', 'tmux'], { stdio: 'inherit' });
143
- } else if (fs.existsSync('/sbin/apk')) {
144
- await execa('sudo', ['apk', 'add', 'tmux'], { stdio: 'inherit' });
145
- } else {
146
- throw new Error('Unsupported Linux package manager');
147
- }
148
- spinner.succeed('tmux installed successfully (Terminal Mirroring available)');
149
- } else if (osInfo.isMac) {
150
- try {
151
- await execa('brew', ['install', 'tmux'], { stdio: 'inherit' });
152
- spinner.succeed('tmux installed successfully (Terminal Mirroring available)');
153
- } catch {
154
- throw new Error('Homebrew is required to install tmux on macOS');
155
- }
151
+ const files = await fs.promises.readdir(sharedDropPath);
152
+ const clientLogs = files.filter(f => f.startsWith('client-') && f.endsWith('.log'));
153
+
154
+ if (clientLogs.length === 0) {
155
+ console.log(chalk.yellow('\n No active client logs found in the shared drop folder.'));
156
+ console.log(chalk.dim(' Clients automatically share their logs once connected.'));
157
+ return;
158
+ }
159
+
160
+ const { selectedLog } = await inquirer.prompt([
161
+ {
162
+ type: 'list',
163
+ name: 'selectedLog',
164
+ message: 'Select a client to monitor activity in live:',
165
+ choices: clientLogs.map(f => {
166
+ const clientName = f.replace('client-', '').replace('.log', '');
167
+ return { name: `👤 ${clientName}`, value: f };
168
+ })
169
+ }
170
+ ]);
171
+
172
+ const logFilePath = path.join(sharedDropPath, selectedLog);
173
+ const clientName = selectedLog.replace('client-', '').replace('.log', '');
174
+
175
+ console.log('');
176
+ console.log(chalk.bold.cyan(` 📊 Live Monitor: ${clientName}`));
177
+ console.log(chalk.dim(' ──────────────────────────────────────────────────'));
178
+ console.log(chalk.dim(' Showing log stream. Press Enter to exit.'));
179
+ console.log('');
180
+
181
+ let filePosition = 0;
182
+ let keepWatching = true;
183
+
184
+ if (fs.existsSync(logFilePath)) {
185
+ const stats = fs.statSync(logFilePath);
186
+ filePosition = stats.size;
187
+ const content = fs.readFileSync(logFilePath, 'utf8');
188
+ const lines = content.split('\n').filter(Boolean);
189
+ const lastLines = lines.slice(-10);
190
+ for (const line of lastLines) {
191
+ formatAndPrintLogLine(line);
156
192
  }
157
193
  }
194
+
195
+ const intervalId = setInterval(() => {
196
+ if (!keepWatching) return;
197
+ try {
198
+ if (!fs.existsSync(logFilePath)) return;
199
+ const stats = fs.statSync(logFilePath);
200
+ if (stats.size > filePosition) {
201
+ const fd = fs.openSync(logFilePath, 'r');
202
+ const bufferSize = stats.size - filePosition;
203
+ const buffer = Buffer.allocUnsafe(bufferSize);
204
+ fs.readSync(fd, buffer, 0, bufferSize, filePosition);
205
+ fs.closeSync(fd);
206
+
207
+ filePosition = stats.size;
208
+ const newContent = buffer.toString('utf8');
209
+ const lines = newContent.split('\n').filter(Boolean);
210
+ for (const line of lines) {
211
+ formatAndPrintLogLine(line);
212
+ }
213
+ }
214
+ } catch {
215
+ // Ignore file access errors
216
+ }
217
+ }, 1000);
218
+
219
+ await inquirer.prompt([{
220
+ type: 'input',
221
+ name: 'exit',
222
+ message: 'Press Enter to stop monitoring...'
223
+ }]);
224
+
225
+ keepWatching = false;
226
+ clearInterval(intervalId);
227
+ console.log(chalk.cyan(' Stopped monitoring client logs.'));
158
228
  } catch (err) {
159
- spinner.fail(`tmux check/install failed: ${err.message}`);
160
- console.log(chalk.dim(' Terminal Mirroring feature will not be available.'));
229
+ console.log(chalk.red(` Could not read client logs: ${err.message}`));
161
230
  }
162
231
  }
163
232
 
164
- function isSecureLinkSession(name) {
165
- return name === TMUX_SESSION_NAME || name.startsWith(TMUX_SESSION_PREFIX);
166
- }
233
+ async function viewHostLiveLogs() {
234
+ const logFilePath = getSessionLogPath();
235
+ if (!logFilePath || !fs.existsSync(logFilePath)) {
236
+ console.log(chalk.red(" ❌ Error: Host session log is not active or doesn't exist yet."));
237
+ return;
238
+ }
167
239
 
168
- async function listTmuxSessions(socketArgs = []) {
169
- const result = await execa('tmux', [...socketArgs, 'list-sessions', '-F', '#{session_name}|#{session_created}'], { reject: false });
170
- if (result.exitCode !== 0) return [];
171
- return result.stdout
172
- .split(/\r?\n/)
173
- .filter(Boolean)
174
- .map(line => {
175
- const [name, createdAt] = line.split('|');
176
- return { name, createdAt: Number(createdAt) || null };
177
- });
178
- }
240
+ console.log('');
241
+ console.log(chalk.bold.cyan(` 📊 Live Monitor: Host Session Activity`));
242
+ console.log(chalk.dim(' ──────────────────────────────────────────────────'));
243
+ console.log(chalk.dim(' Showing log stream. Press Enter to exit.'));
244
+ console.log('');
245
+
246
+ let filePosition = 0;
247
+ let keepWatching = true;
248
+
249
+ try {
250
+ const stats = fs.statSync(logFilePath);
251
+ filePosition = stats.size;
252
+ const content = fs.readFileSync(logFilePath, 'utf8');
253
+ const lines = content.split('\n').filter(Boolean);
254
+ const lastLines = lines.slice(-10);
255
+ for (const line of lastLines) {
256
+ formatAndPrintLogLine(line);
257
+ }
179
258
 
180
- async function getMirrorableSessions() {
181
- const sessions = [];
182
- const customSessions = await listTmuxSessions(tmuxSocketArgs());
183
- customSessions
184
- .filter(s => isSecureLinkSession(s.name))
185
- .forEach(s => sessions.push({ ...s, socketArgs: tmuxSocketArgs(), source: 'custom' }));
259
+ const intervalId = setInterval(() => {
260
+ if (!keepWatching) return;
261
+ try {
262
+ if (!fs.existsSync(logFilePath)) return;
263
+ const stats = fs.statSync(logFilePath);
264
+ if (stats.size > filePosition) {
265
+ const fd = fs.openSync(logFilePath, 'r');
266
+ const bufferSize = stats.size - filePosition;
267
+ const buffer = Buffer.allocUnsafe(bufferSize);
268
+ fs.readSync(fd, buffer, 0, bufferSize, filePosition);
269
+ fs.closeSync(fd);
270
+
271
+ filePosition = stats.size;
272
+ const newContent = buffer.toString('utf8');
273
+ const lines = newContent.split('\n').filter(Boolean);
274
+ for (const line of lines) {
275
+ formatAndPrintLogLine(line);
276
+ }
277
+ }
278
+ } catch {
279
+ // Ignore file access errors
280
+ }
281
+ }, 1000);
186
282
 
187
- const legacySessions = await listTmuxSessions();
188
- legacySessions
189
- .filter(s => isSecureLinkSession(s.name))
190
- .forEach(s => sessions.push({ ...s, socketArgs: [], source: 'legacy' }));
283
+ await inquirer.prompt([{
284
+ type: 'input',
285
+ name: 'exit',
286
+ message: 'Press Enter to stop monitoring...'
287
+ }]);
191
288
 
192
- return sessions;
289
+ keepWatching = false;
290
+ clearInterval(intervalId);
291
+ console.log(chalk.cyan(' Stopped monitoring host logs.'));
292
+ } catch (err) {
293
+ console.log(chalk.red(` Could not read host logs: ${err.message}`));
294
+ }
193
295
  }
194
296
 
297
+
195
298
  // ─── Ephemeral SSH Key Management ────────────────────────────
196
299
  async function generateEphemeralKey() {
197
300
  const tmpDir = os.tmpdir() || process.env.TMPDIR || process.env.TEMP || process.env.TMP;
@@ -219,6 +322,9 @@ async function injectPublicKey(pubKey) {
219
322
  if (!fs.existsSync(sshDir)) {
220
323
  await fs.promises.mkdir(sshDir, { mode: 0o700, recursive: true });
221
324
  }
325
+ try {
326
+ await fs.promises.chmod(sshDir, 0o700);
327
+ } catch {}
222
328
 
223
329
  const authKeysPath = path.join(sshDir, 'authorized_keys');
224
330
  const existing = await fs.promises.lstat(authKeysPath).catch(() => null);
@@ -227,20 +333,47 @@ async function injectPublicKey(pubKey) {
227
333
  }
228
334
  const authorizedKey = `no-agent-forwarding,no-X11-forwarding ${pubKey}`;
229
335
  await fs.promises.appendFile(authKeysPath, `\n${authorizedKey}\n`, { mode: 0o600 });
230
- await fs.promises.chmod(authKeysPath, 0o600);
231
- return { authKeysPath, authorizedKey };
336
+ try {
337
+ await fs.promises.chmod(authKeysPath, 0o600);
338
+ } catch {}
339
+
340
+ // Windows Administrators authorized keys handling
341
+ let adminAuthKeysPath = null;
342
+ if (process.platform === 'win32') {
343
+ const programData = process.env.PROGRAMDATA || 'C:\\ProgramData';
344
+ const adminKeysPath = path.join(programData, 'ssh', 'administrators_authorized_keys');
345
+ try {
346
+ if (fs.existsSync(path.dirname(adminKeysPath))) {
347
+ await fs.promises.appendFile(adminKeysPath, `\n${authorizedKey}\n`, { mode: 0o600 });
348
+ try {
349
+ await execa('icacls', [adminKeysPath, '/inheritance:r', '/grant', '*S-1-5-32-544:F', '/grant', '*S-1-5-18:F']);
350
+ } catch {}
351
+ adminAuthKeysPath = adminKeysPath;
352
+ }
353
+ } catch {}
354
+ }
355
+
356
+ return { authKeysPath, adminAuthKeysPath, authorizedKey };
232
357
  }
233
358
 
234
- async function removePublicKey(authKeysPath, authorizedKey) {
359
+ async function removePublicKey(authKeysPath, authorizedKey, adminAuthKeysPath = null) {
235
360
  if (fs.existsSync(authKeysPath)) {
236
361
  const stat = await fs.promises.lstat(authKeysPath);
237
- if (stat.isSymbolicLink()) {
238
- throw new Error('Refusing to modify a symlinked authorized_keys file');
362
+ if (!stat.isSymbolicLink()) {
363
+ let keys = await fs.promises.readFile(authKeysPath, 'utf8');
364
+ keys = keys.replace(`\n${authorizedKey}\n`, '');
365
+ await fs.promises.writeFile(authKeysPath, keys);
366
+ try {
367
+ await fs.promises.chmod(authKeysPath, 0o600);
368
+ } catch {}
239
369
  }
240
- let keys = await fs.promises.readFile(authKeysPath, 'utf8');
241
- keys = keys.replace(`\n${authorizedKey}\n`, '');
242
- await fs.promises.writeFile(authKeysPath, keys);
243
- await fs.promises.chmod(authKeysPath, 0o600);
370
+ }
371
+ if (adminAuthKeysPath && fs.existsSync(adminAuthKeysPath)) {
372
+ try {
373
+ let keys = await fs.promises.readFile(adminAuthKeysPath, 'utf8');
374
+ keys = keys.replace(`\n${authorizedKey}\n`, '');
375
+ await fs.promises.writeFile(adminAuthKeysPath, keys);
376
+ } catch {}
244
377
  }
245
378
  }
246
379
 
@@ -347,12 +480,19 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
347
480
  async function fetchDecryptedApprovals() {
348
481
  const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken);
349
482
  const decryptedApprovals = await Promise.all((data.approvals || []).map(async (a) => {
350
- const base = { id: a.id, status: a.status, createdAt: a.createdAt, decidedAt: a.decidedAt };
483
+ const base = { id: a.id, status: a.status, createdAt: a.createdAt, decidedAt: a.decidedAt, ip: a.ip || 'unknown' };
351
484
  if (!a.iv || !a.ciphertext || !a.salt) return base;
352
485
  try {
353
486
  const decrypted = await decryptAsync(a.iv, a.ciphertext, password, a.salt);
354
487
  const details = JSON.parse(decrypted);
355
- return { ...base, username: details.username, hostname: details.hostname, os: details.os, intent: details.intent };
488
+ return {
489
+ ...base,
490
+ username: details.username,
491
+ hostname: details.hostname,
492
+ os: details.os,
493
+ intent: details.intent,
494
+ localIp: details.localIp || 'unknown'
495
+ };
356
496
  } catch {
357
497
  return base;
358
498
  }
@@ -488,7 +628,31 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
488
628
  const { requestId, decision } = req.body || {};
489
629
  if (!requestId || !decision) return res.status(400).json({ error: 'requestId and decision required' });
490
630
  try {
491
- await decideApprovalRequest(BROKER_URL, uid, requestId, decision, sessionState.hostToken);
631
+ let approvedPayload = null;
632
+ if (decision === 'approved') {
633
+ const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken);
634
+ const request = (data.approvals || []).find(item => item.id === requestId);
635
+ if (!request) return res.status(404).json({ error: 'Request not found' });
636
+
637
+ let details = {};
638
+ try {
639
+ details = JSON.parse(await decryptAsync(request.iv, request.ciphertext, password, request.salt));
640
+ } catch {}
641
+
642
+ const clientKeySalt = [
643
+ password,
644
+ request.ip || 'unknown',
645
+ details.username || 'unknown',
646
+ details.hostname || 'unknown',
647
+ details.os || 'unknown'
648
+ ].join('|');
649
+ const clientPwd = crypto.createHash('sha256').update(clientKeySalt).digest('hex');
650
+
651
+ const payload = JSON.stringify({ url: sessionState.tunnelUrl, ...serviceConfig });
652
+ approvedPayload = await encryptAsync(payload, clientPwd);
653
+ }
654
+
655
+ await decideApprovalRequest(BROKER_URL, uid, requestId, decision, sessionState.hostToken, approvedPayload);
492
656
  recordEvent('approval_decision', { uid, requestId, decision, via: 'dashboard' });
493
657
  res.json({ ok: true });
494
658
  } catch (err) {
@@ -684,7 +848,7 @@ function renderApprovals(approvals) {
684
848
  heading.append(title, badge);
685
849
  const details = document.createElement('div');
686
850
  details.className = 'meta';
687
- details.textContent = 'User: ' + String(req.username || 'unknown') + ' | Host: ' + String(req.hostname || 'unknown') + ' | OS: ' + String(req.os || 'unknown');
851
+ details.textContent = 'User: ' + String(req.username || 'unknown') + ' | Host: ' + String(req.hostname || 'unknown') + ' | OS: ' + String(req.os || 'unknown') + ' | IP: ' + String(req.ip || 'unknown') + ' (Local: ' + String(req.localIp || 'unknown') + ')';
688
852
  const meta = document.createElement('div');
689
853
  meta.className = 'meta';
690
854
  meta.textContent = 'Submitted: ' + new Date(req.createdAt).toLocaleTimeString();
@@ -932,7 +1096,8 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
932
1096
  const choices = [
933
1097
  { name: '✅ Review pending client approvals', value: 'approvals' },
934
1098
  { name: '📡 See detailed client telemetry', value: 'show' },
935
- { name: '📺 Mirror Client Terminal (requires tmux)', value: 'mirror' },
1099
+ { name: '📄 View live client activity logs', value: 'logs' },
1100
+ { name: '📄 View host session activity logs', value: 'host_logs' },
936
1101
  { name: '🔄 Re-register with broker', value: 'reregister' }
937
1102
  ];
938
1103
 
@@ -986,6 +1151,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
986
1151
  console.log(` Host: ${details.hostname || 'unknown'}`);
987
1152
  console.log(` OS: ${details.os || 'unknown'}`);
988
1153
  console.log(` Intent: ${details.intent || 'connect'}`);
1154
+ console.log(` IP: ${request.ip || 'unknown'} (Local: ${details.localIp || 'unknown'})`);
989
1155
 
990
1156
  const { decision } = await inquirer.prompt([{
991
1157
  type: 'list',
@@ -998,7 +1164,22 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
998
1164
  ],
999
1165
  }]);
1000
1166
  if (decision !== 'skip') {
1001
- await decideApprovalRequest(BROKER_URL, uid, request.id, decision, sessionState.hostToken);
1167
+ let approvedPayload = null;
1168
+ if (decision === 'approved') {
1169
+ const clientKeySalt = [
1170
+ password,
1171
+ request.ip || 'unknown',
1172
+ details.username || 'unknown',
1173
+ details.hostname || 'unknown',
1174
+ details.os || 'unknown'
1175
+ ].join('|');
1176
+ const clientPwd = crypto.createHash('sha256').update(clientKeySalt).digest('hex');
1177
+
1178
+ const payload = JSON.stringify({ url: sessionState.tunnelUrl, ...serviceConfig });
1179
+ approvedPayload = await encryptAsync(payload, clientPwd);
1180
+ }
1181
+
1182
+ await decideApprovalRequest(BROKER_URL, uid, request.id, decision, sessionState.hostToken, approvedPayload);
1002
1183
  recordEvent('approval_decision', { uid, requestId: request.id, decision, username: details.username });
1003
1184
  }
1004
1185
  }
@@ -1023,6 +1204,14 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
1023
1204
  }
1024
1205
  });
1025
1206
 
1207
+ addCleanupHook(() => {
1208
+ try {
1209
+ if (chatServerInstance && chatServerInstance.server) {
1210
+ chatServerInstance.server.close();
1211
+ }
1212
+ } catch {}
1213
+ });
1214
+
1026
1215
  console.log(chalk.dim(' Provisioning Cloudflare tunnel for chat...'));
1027
1216
  chatTunnelProcess = await spawnTunnelSupervised(`http://localhost:${chatServerInstance.port}`, async (newUrl) => {
1028
1217
  serviceConfig.chatUrl = newUrl;
@@ -1041,6 +1230,9 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
1041
1230
 
1042
1231
  case 'dashboard': {
1043
1232
  dashboardInstance = await startLocalHostDashboard(uid, password, serviceConfig, sessionState);
1233
+ addCleanupHook(() => {
1234
+ try { if (dashboardInstance) dashboardInstance.close(); } catch {}
1235
+ });
1044
1236
  logSessionEvent('host_dashboard_opened');
1045
1237
  return waitForAction();
1046
1238
  }
@@ -1057,48 +1249,12 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
1057
1249
  return waitForAction();
1058
1250
  }
1059
1251
 
1060
- case 'mirror': {
1061
- console.log('');
1062
- console.log(chalk.bold.cyan(' 📺 Terminal Mirroring'));
1063
- console.log(chalk.dim(' ──────────────────────────────────────'));
1064
- console.log(chalk.dim(' Attaching to the tmux session created by an interactive SSH client.'));
1065
- console.log(chalk.dim(' Press Ctrl+b then d to detach gracefully.'));
1066
- console.log('');
1067
-
1068
- try {
1069
- await execa('tmux', ['-V'], { reject: true });
1070
- const sessions = await getMirrorableSessions();
1071
- if (sessions.length === 0) {
1072
- console.log(chalk.yellow(' ⚠️ No mirrored terminal session is active yet.'));
1073
- console.log(chalk.dim(' A client must choose "Connect via SSH" first. SCP-only clients do not create a tmux session.'));
1074
- console.log(chalk.dim(' tmux is needed on the host machine only; the client does not need tmux.'));
1075
- logSessionEvent('host_mirror_missing_session', {}, 'warn');
1076
- return waitForAction();
1077
- }
1078
-
1079
- let target = sessions[0];
1080
- if (sessions.length > 1) {
1081
- const { sessionChoice } = await inquirer.prompt([{
1082
- type: 'list',
1083
- name: 'sessionChoice',
1084
- message: 'Select an active client session to mirror:',
1085
- choices: sessions.map((s, idx) => {
1086
- const created = s.createdAt ? new Date(s.createdAt * 1000).toLocaleTimeString() : 'Unknown';
1087
- const label = `${s.name} ${chalk.dim(`(started ${created})`)}`;
1088
- return { name: label, value: String(idx) };
1089
- }),
1090
- }]);
1091
- target = sessions[parseInt(sessionChoice, 10)] || sessions[0];
1092
- }
1093
-
1094
- await execa('tmux', [...target.socketArgs, 'attach', '-t', target.name, '-r'], { stdio: 'inherit', reject: false });
1095
- logSessionEvent('host_mirror_attached', { session: target.name, source: target.source });
1096
- } catch (err) {
1097
- console.log(chalk.yellow(' ⚠️ Could not attach to tmux.'));
1098
- console.log(chalk.dim(` ${err.message}`));
1099
- console.log(chalk.dim(' Terminal mirroring requires tmux on the host machine and an active interactive SSH client.'));
1100
- logSessionEvent('host_mirror_error', { error: err.message }, 'warn');
1101
- }
1252
+ case 'logs': {
1253
+ await viewLiveClientLogs(serviceConfig.sharedDropPath);
1254
+ return waitForAction();
1255
+ }
1256
+ case 'host_logs': {
1257
+ await viewHostLiveLogs();
1102
1258
  return waitForAction();
1103
1259
  }
1104
1260
 
@@ -1154,15 +1310,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
1154
1310
  try {
1155
1311
  await revokeUID(BROKER_URL, uid, sessionState.hostToken);
1156
1312
  tunnelProcess.kill();
1157
- if (process.platform !== 'win32') {
1158
- await execa('tmux', [...tmuxSocketArgs(), 'kill-server'], { reject: false });
1159
- const legacySessions = await listTmuxSessions();
1160
- for (const session of legacySessions) {
1161
- if (isSecureLinkSession(session.name)) {
1162
- await execa('tmux', ['kill-session', '-t', session.name], { reject: false });
1163
- }
1164
- }
1165
- }
1313
+ // No tmux server to terminate since mirroring was discarded
1166
1314
  spinner.succeed('Session revoked and iPingYou-owned connections terminated');
1167
1315
  logSessionEvent('host_sessions_terminated');
1168
1316
  } catch {
@@ -1174,6 +1322,9 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
1174
1322
 
1175
1323
  case 'exit':
1176
1324
  if (dashboardInstance) dashboardInstance.close();
1325
+ if (chatServerInstance && chatServerInstance.server) {
1326
+ try { chatServerInstance.server.close(); } catch {}
1327
+ }
1177
1328
  if (chatTunnelProcess) chatTunnelProcess.kill();
1178
1329
  if (global.privateBrokerInstance) global.privateBrokerInstance.kill();
1179
1330
  if (tunnelProcess) tunnelProcess.kill();
@@ -1310,7 +1461,6 @@ export async function startHostMode() {
1310
1461
 
1311
1462
  if (serviceType === 'ssh' || serviceType === 'share') {
1312
1463
  await ensureSSHRunning();
1313
- await ensureTmuxInstalled();
1314
1464
 
1315
1465
  try {
1316
1466
  serviceConfig.sharedDropPath = await prepareSharedDropFolder(uid);
@@ -1340,13 +1490,13 @@ export async function startHostMode() {
1340
1490
  console.log(chalk.dim(' 🔑 Generating ephemeral SSH key for passwordless entry...'));
1341
1491
  try {
1342
1492
  const ephemeralKey = await generateEphemeralKey();
1343
- const { authKeysPath, authorizedKey } = await injectPublicKey(ephemeralKey.pubKey);
1493
+ const { authKeysPath, adminAuthKeysPath, authorizedKey } = await injectPublicKey(ephemeralKey.pubKey);
1344
1494
 
1345
1495
  serviceConfig.privateKey = ephemeralKey.privKey;
1346
1496
 
1347
1497
  addCleanupHook(async () => {
1348
1498
  console.log(chalk.dim(' Removing ephemeral public key...'));
1349
- await removePublicKey(authKeysPath, authorizedKey);
1499
+ await removePublicKey(authKeysPath, authorizedKey, adminAuthKeysPath);
1350
1500
  try { await fs.promises.unlink(ephemeralKey.keyPath); } catch { }
1351
1501
  try { await fs.promises.unlink(`${ephemeralKey.keyPath}.pub`); } catch { }
1352
1502
  });