@miraj181/ipingyou 2.1.18 → 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('');
179
245
 
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' }));
246
+ let filePosition = 0;
247
+ let keepWatching = true;
186
248
 
187
- const legacySessions = await listTmuxSessions();
188
- legacySessions
189
- .filter(s => isSecureLinkSession(s.name))
190
- .forEach(s => sessions.push({ ...s, socketArgs: [], source: 'legacy' }));
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
+ }
191
258
 
192
- return sessions;
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);
282
+
283
+ await inquirer.prompt([{
284
+ type: 'input',
285
+ name: 'exit',
286
+ message: 'Press Enter to stop monitoring...'
287
+ }]);
288
+
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
 
@@ -344,6 +477,29 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
344
477
  return { clients: decryptedClients };
345
478
  }
346
479
 
480
+ async function fetchDecryptedApprovals() {
481
+ const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken);
482
+ const decryptedApprovals = await Promise.all((data.approvals || []).map(async (a) => {
483
+ const base = { id: a.id, status: a.status, createdAt: a.createdAt, decidedAt: a.decidedAt, ip: a.ip || 'unknown' };
484
+ if (!a.iv || !a.ciphertext || !a.salt) return base;
485
+ try {
486
+ const decrypted = await decryptAsync(a.iv, a.ciphertext, password, a.salt);
487
+ const details = JSON.parse(decrypted);
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
+ };
496
+ } catch {
497
+ return base;
498
+ }
499
+ }));
500
+ return { approvalRequired: data.approvalRequired, approvals: decryptedApprovals };
501
+ }
502
+
347
503
  app.get('/api/status', (_req, res) => {
348
504
  res.json({
349
505
  uid,
@@ -361,7 +517,7 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
361
517
 
362
518
  app.get('/api/approvals', async (_req, res) => {
363
519
  try {
364
- const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken);
520
+ const data = await fetchDecryptedApprovals();
365
521
  res.json(data);
366
522
  } catch (err) {
367
523
  res.status(500).json({ error: err.message });
@@ -399,7 +555,7 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
399
555
  if (closed) return;
400
556
  try {
401
557
  const [approvalData, clientData] = await Promise.all([
402
- fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken).catch(() => ({ approvals: [] })),
558
+ fetchDecryptedApprovals().catch(() => ({ approvals: [] })),
403
559
  fetchDecryptedClients().catch(() => ({ clients: [] })),
404
560
  ]);
405
561
 
@@ -472,7 +628,31 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
472
628
  const { requestId, decision } = req.body || {};
473
629
  if (!requestId || !decision) return res.status(400).json({ error: 'requestId and decision required' });
474
630
  try {
475
- 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);
476
656
  recordEvent('approval_decision', { uid, requestId, decision, via: 'dashboard' });
477
657
  res.json({ ok: true });
478
658
  } catch (err) {
@@ -666,6 +846,9 @@ function renderApprovals(approvals) {
666
846
  badge.className = 'status-badge status-pending';
667
847
  badge.textContent = 'PENDING';
668
848
  heading.append(title, badge);
849
+ const details = document.createElement('div');
850
+ details.className = 'meta';
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') + ')';
669
852
  const meta = document.createElement('div');
670
853
  meta.className = 'meta';
671
854
  meta.textContent = 'Submitted: ' + new Date(req.createdAt).toLocaleTimeString();
@@ -684,7 +867,7 @@ function renderApprovals(approvals) {
684
867
  });
685
868
  actions.appendChild(button);
686
869
  }
687
- item.append(heading, meta, actions);
870
+ item.append(heading, details, meta, actions);
688
871
  fragment.appendChild(item);
689
872
  }
690
873
  for (const req of decided) {
@@ -871,6 +1054,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
871
1054
  let dashboardInstance = null;
872
1055
 
873
1056
  const renderDashboard = () => {
1057
+ const isPrivateBroker = Boolean(global.privateBrokerInstance);
874
1058
  console.clear();
875
1059
  console.log('');
876
1060
  console.log(chalk.bold(' ╔════════════════════════════════════════════════════╗'));
@@ -879,7 +1063,9 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
879
1063
  console.log(` ║ ${chalk.cyan('UID:')} ${chalk.bold.white(uid.padEnd(30))}║`);
880
1064
  console.log(` ║ ${chalk.cyan('Password:')} ${chalk.bold.white(secureSensitive(password).padEnd(30))}║`);
881
1065
  console.log(` ║ ${chalk.cyan('Service:')} ${chalk.dim(serviceConfig.type.toUpperCase() + ' (Port ' + serviceConfig.port + ')').padEnd(30)}║`);
882
- console.log(` ║ ${chalk.cyan('Tunnel:')} ${chalk.dim(sessionState.tunnelUrl.substring(0, 40))} ║`);
1066
+ if (isPrivateBroker) {
1067
+ console.log(` ║ ${chalk.cyan('Tunnel:')} ${chalk.dim(sessionState.tunnelUrl.substring(0, 40))} ║`);
1068
+ }
883
1069
  if (serviceConfig.chatUrl) {
884
1070
  console.log(` ║ ${chalk.cyan('Chat URL:')} ${chalk.dim(serviceConfig.chatUrl.substring(0, 40))} ║`);
885
1071
  }
@@ -910,7 +1096,8 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
910
1096
  const choices = [
911
1097
  { name: '✅ Review pending client approvals', value: 'approvals' },
912
1098
  { name: '📡 See detailed client telemetry', value: 'show' },
913
- { 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' },
914
1101
  { name: '🔄 Re-register with broker', value: 'reregister' }
915
1102
  ];
916
1103
 
@@ -964,6 +1151,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
964
1151
  console.log(` Host: ${details.hostname || 'unknown'}`);
965
1152
  console.log(` OS: ${details.os || 'unknown'}`);
966
1153
  console.log(` Intent: ${details.intent || 'connect'}`);
1154
+ console.log(` IP: ${request.ip || 'unknown'} (Local: ${details.localIp || 'unknown'})`);
967
1155
 
968
1156
  const { decision } = await inquirer.prompt([{
969
1157
  type: 'list',
@@ -976,7 +1164,22 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
976
1164
  ],
977
1165
  }]);
978
1166
  if (decision !== 'skip') {
979
- 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);
980
1183
  recordEvent('approval_decision', { uid, requestId: request.id, decision, username: details.username });
981
1184
  }
982
1185
  }
@@ -1001,6 +1204,14 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
1001
1204
  }
1002
1205
  });
1003
1206
 
1207
+ addCleanupHook(() => {
1208
+ try {
1209
+ if (chatServerInstance && chatServerInstance.server) {
1210
+ chatServerInstance.server.close();
1211
+ }
1212
+ } catch {}
1213
+ });
1214
+
1004
1215
  console.log(chalk.dim(' Provisioning Cloudflare tunnel for chat...'));
1005
1216
  chatTunnelProcess = await spawnTunnelSupervised(`http://localhost:${chatServerInstance.port}`, async (newUrl) => {
1006
1217
  serviceConfig.chatUrl = newUrl;
@@ -1019,6 +1230,9 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
1019
1230
 
1020
1231
  case 'dashboard': {
1021
1232
  dashboardInstance = await startLocalHostDashboard(uid, password, serviceConfig, sessionState);
1233
+ addCleanupHook(() => {
1234
+ try { if (dashboardInstance) dashboardInstance.close(); } catch {}
1235
+ });
1022
1236
  logSessionEvent('host_dashboard_opened');
1023
1237
  return waitForAction();
1024
1238
  }
@@ -1035,48 +1249,12 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
1035
1249
  return waitForAction();
1036
1250
  }
1037
1251
 
1038
- case 'mirror': {
1039
- console.log('');
1040
- console.log(chalk.bold.cyan(' 📺 Terminal Mirroring'));
1041
- console.log(chalk.dim(' ──────────────────────────────────────'));
1042
- console.log(chalk.dim(' Attaching to the tmux session created by an interactive SSH client.'));
1043
- console.log(chalk.dim(' Press Ctrl+b then d to detach gracefully.'));
1044
- console.log('');
1045
-
1046
- try {
1047
- await execa('tmux', ['-V'], { reject: true });
1048
- const sessions = await getMirrorableSessions();
1049
- if (sessions.length === 0) {
1050
- console.log(chalk.yellow(' ⚠️ No mirrored terminal session is active yet.'));
1051
- console.log(chalk.dim(' A client must choose "Connect via SSH" first. SCP-only clients do not create a tmux session.'));
1052
- console.log(chalk.dim(' tmux is needed on the host machine only; the client does not need tmux.'));
1053
- logSessionEvent('host_mirror_missing_session', {}, 'warn');
1054
- return waitForAction();
1055
- }
1056
-
1057
- let target = sessions[0];
1058
- if (sessions.length > 1) {
1059
- const { sessionChoice } = await inquirer.prompt([{
1060
- type: 'list',
1061
- name: 'sessionChoice',
1062
- message: 'Select an active client session to mirror:',
1063
- choices: sessions.map((s, idx) => {
1064
- const created = s.createdAt ? new Date(s.createdAt * 1000).toLocaleTimeString() : 'Unknown';
1065
- const label = `${s.name} ${chalk.dim(`(started ${created})`)}`;
1066
- return { name: label, value: String(idx) };
1067
- }),
1068
- }]);
1069
- target = sessions[parseInt(sessionChoice, 10)] || sessions[0];
1070
- }
1071
-
1072
- await execa('tmux', [...target.socketArgs, 'attach', '-t', target.name, '-r'], { stdio: 'inherit', reject: false });
1073
- logSessionEvent('host_mirror_attached', { session: target.name, source: target.source });
1074
- } catch (err) {
1075
- console.log(chalk.yellow(' ⚠️ Could not attach to tmux.'));
1076
- console.log(chalk.dim(` ${err.message}`));
1077
- console.log(chalk.dim(' Terminal mirroring requires tmux on the host machine and an active interactive SSH client.'));
1078
- logSessionEvent('host_mirror_error', { error: err.message }, 'warn');
1079
- }
1252
+ case 'logs': {
1253
+ await viewLiveClientLogs(serviceConfig.sharedDropPath);
1254
+ return waitForAction();
1255
+ }
1256
+ case 'host_logs': {
1257
+ await viewHostLiveLogs();
1080
1258
  return waitForAction();
1081
1259
  }
1082
1260
 
@@ -1132,15 +1310,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
1132
1310
  try {
1133
1311
  await revokeUID(BROKER_URL, uid, sessionState.hostToken);
1134
1312
  tunnelProcess.kill();
1135
- if (process.platform !== 'win32') {
1136
- await execa('tmux', [...tmuxSocketArgs(), 'kill-server'], { reject: false });
1137
- const legacySessions = await listTmuxSessions();
1138
- for (const session of legacySessions) {
1139
- if (isSecureLinkSession(session.name)) {
1140
- await execa('tmux', ['kill-session', '-t', session.name], { reject: false });
1141
- }
1142
- }
1143
- }
1313
+ // No tmux server to terminate since mirroring was discarded
1144
1314
  spinner.succeed('Session revoked and iPingYou-owned connections terminated');
1145
1315
  logSessionEvent('host_sessions_terminated');
1146
1316
  } catch {
@@ -1152,6 +1322,9 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
1152
1322
 
1153
1323
  case 'exit':
1154
1324
  if (dashboardInstance) dashboardInstance.close();
1325
+ if (chatServerInstance && chatServerInstance.server) {
1326
+ try { chatServerInstance.server.close(); } catch {}
1327
+ }
1155
1328
  if (chatTunnelProcess) chatTunnelProcess.kill();
1156
1329
  if (global.privateBrokerInstance) global.privateBrokerInstance.kill();
1157
1330
  if (tunnelProcess) tunnelProcess.kill();
@@ -1288,7 +1461,6 @@ export async function startHostMode() {
1288
1461
 
1289
1462
  if (serviceType === 'ssh' || serviceType === 'share') {
1290
1463
  await ensureSSHRunning();
1291
- await ensureTmuxInstalled();
1292
1464
 
1293
1465
  try {
1294
1466
  serviceConfig.sharedDropPath = await prepareSharedDropFolder(uid);
@@ -1318,13 +1490,13 @@ export async function startHostMode() {
1318
1490
  console.log(chalk.dim(' 🔑 Generating ephemeral SSH key for passwordless entry...'));
1319
1491
  try {
1320
1492
  const ephemeralKey = await generateEphemeralKey();
1321
- const { authKeysPath, authorizedKey } = await injectPublicKey(ephemeralKey.pubKey);
1493
+ const { authKeysPath, adminAuthKeysPath, authorizedKey } = await injectPublicKey(ephemeralKey.pubKey);
1322
1494
 
1323
1495
  serviceConfig.privateKey = ephemeralKey.privKey;
1324
1496
 
1325
1497
  addCleanupHook(async () => {
1326
1498
  console.log(chalk.dim(' Removing ephemeral public key...'));
1327
- await removePublicKey(authKeysPath, authorizedKey);
1499
+ await removePublicKey(authKeysPath, authorizedKey, adminAuthKeysPath);
1328
1500
  try { await fs.promises.unlink(ephemeralKey.keyPath); } catch { }
1329
1501
  try { await fs.promises.unlink(`${ephemeralKey.keyPath}.pub`); } catch { }
1330
1502
  });