@miraj181/ipingyou 2.1.15 → 2.1.19

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
@@ -21,7 +21,9 @@ import { fileURLToPath } from 'node:url';
21
21
  import { createRequire } from 'node:module';
22
22
  import fs from 'node:fs';
23
23
  import os from 'node:os';
24
+ import crypto from 'node:crypto';
24
25
  import { generateUID } from '../lib/uid.js';
26
+ import { openUrl } from '../lib/open-url.js';
25
27
  import { decryptAsync } from '../lib/crypto.js';
26
28
  import { cleanupAll, killProcessTree, trackPID, untrackPID, setRevokeOnExit, addCleanupHook } from '../lib/cleanup.js';
27
29
  import { detectOS } from '../lib/platform.js';
@@ -36,6 +38,16 @@ import { TMUX_SESSION_NAME, TMUX_SESSION_PREFIX, tmuxSocketArgs } from '../lib/t
36
38
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
37
39
  let BROKER_URL = process.env.BROKER_URL || 'https://ipingyou.onrender.com';
38
40
 
41
+ function escapeDashboardHtml(value) {
42
+ return String(value ?? '').replace(/[&<>"']/g, character => ({
43
+ '&': '&amp;',
44
+ '<': '&lt;',
45
+ '>': '&gt;',
46
+ '"': '&quot;',
47
+ "'": '&#39;',
48
+ })[character]);
49
+ }
50
+
39
51
  async function waitForValue(getValue, timeoutMs, label) {
40
52
  const startedAt = Date.now();
41
53
  while (!getValue()) {
@@ -209,15 +221,26 @@ async function injectPublicKey(pubKey) {
209
221
  }
210
222
 
211
223
  const authKeysPath = path.join(sshDir, 'authorized_keys');
212
- await fs.promises.appendFile(authKeysPath, `\n${pubKey}\n`);
213
- return authKeysPath;
224
+ const existing = await fs.promises.lstat(authKeysPath).catch(() => null);
225
+ if (existing?.isSymbolicLink()) {
226
+ throw new Error('Refusing to modify a symlinked authorized_keys file');
227
+ }
228
+ const authorizedKey = `no-agent-forwarding,no-X11-forwarding ${pubKey}`;
229
+ await fs.promises.appendFile(authKeysPath, `\n${authorizedKey}\n`, { mode: 0o600 });
230
+ await fs.promises.chmod(authKeysPath, 0o600);
231
+ return { authKeysPath, authorizedKey };
214
232
  }
215
233
 
216
- async function removePublicKey(authKeysPath, pubKey) {
234
+ async function removePublicKey(authKeysPath, authorizedKey) {
217
235
  if (fs.existsSync(authKeysPath)) {
236
+ const stat = await fs.promises.lstat(authKeysPath);
237
+ if (stat.isSymbolicLink()) {
238
+ throw new Error('Refusing to modify a symlinked authorized_keys file');
239
+ }
218
240
  let keys = await fs.promises.readFile(authKeysPath, 'utf8');
219
- keys = keys.replace(`\n${pubKey}\n`, '');
241
+ keys = keys.replace(`\n${authorizedKey}\n`, '');
220
242
  await fs.promises.writeFile(authKeysPath, keys);
243
+ await fs.promises.chmod(authKeysPath, 0o600);
221
244
  }
222
245
  }
223
246
 
@@ -275,13 +298,17 @@ async function promptOneTimeSharePath() {
275
298
 
276
299
  async function startLocalHostDashboard(uid, password, serviceConfig, sessionState) {
277
300
  const { default: express } = await import('express');
278
- const { default: open } = await import('open');
279
301
  const app = express();
280
302
  const startedAt = new Date().toISOString();
281
303
  const decryptedClientCache = new Map();
282
304
  const MAX_DECRYPTED_CLIENT_CACHE = 100;
283
305
  const activeEventStreams = new Set();
284
306
  const MAX_EVENT_STREAMS = 5;
307
+ const dashboardUid = escapeDashboardHtml(uid);
308
+ const dashboardService = escapeDashboardHtml(String(serviceConfig.type || '').toUpperCase());
309
+ const dashboardPort = escapeDashboardHtml(serviceConfig.port);
310
+ const dashboardDropPath = escapeDashboardHtml(serviceConfig.sharedDropPath || 'none');
311
+ const dashboardSharePath = escapeDashboardHtml(serviceConfig.oneTimeSharePath || 'none');
285
312
 
286
313
  async function fetchDecryptedClients() {
287
314
  const brokerRes = await fetch(`${BROKER_URL}/clients/${uid}`, {
@@ -317,6 +344,22 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
317
344
  return { clients: decryptedClients };
318
345
  }
319
346
 
347
+ async function fetchDecryptedApprovals() {
348
+ const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken);
349
+ 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 };
351
+ if (!a.iv || !a.ciphertext || !a.salt) return base;
352
+ try {
353
+ const decrypted = await decryptAsync(a.iv, a.ciphertext, password, a.salt);
354
+ const details = JSON.parse(decrypted);
355
+ return { ...base, username: details.username, hostname: details.hostname, os: details.os, intent: details.intent };
356
+ } catch {
357
+ return base;
358
+ }
359
+ }));
360
+ return { approvalRequired: data.approvalRequired, approvals: decryptedApprovals };
361
+ }
362
+
320
363
  app.get('/api/status', (_req, res) => {
321
364
  res.json({
322
365
  uid,
@@ -334,7 +377,7 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
334
377
 
335
378
  app.get('/api/approvals', async (_req, res) => {
336
379
  try {
337
- const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken);
380
+ const data = await fetchDecryptedApprovals();
338
381
  res.json(data);
339
382
  } catch (err) {
340
383
  res.status(500).json({ error: err.message });
@@ -372,7 +415,7 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
372
415
  if (closed) return;
373
416
  try {
374
417
  const [approvalData, clientData] = await Promise.all([
375
- fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken).catch(() => ({ approvals: [] })),
418
+ fetchDecryptedApprovals().catch(() => ({ approvals: [] })),
376
419
  fetchDecryptedClients().catch(() => ({ clients: [] })),
377
420
  ]);
378
421
 
@@ -464,6 +507,17 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
464
507
  });
465
508
 
466
509
  app.get('/', (_req, res) => {
510
+ const scriptNonce = crypto.randomBytes(18).toString('base64');
511
+ res.set('Content-Security-Policy', [
512
+ "default-src 'self'",
513
+ `script-src 'nonce-${scriptNonce}'`,
514
+ "style-src 'self' 'unsafe-inline'",
515
+ "connect-src 'self'",
516
+ "img-src 'none'",
517
+ "object-src 'none'",
518
+ "base-uri 'none'",
519
+ "frame-ancestors 'none'",
520
+ ].join('; '));
467
521
  res.type('html').send(`<!doctype html>
468
522
  <html lang="en"><head><meta charset="utf-8"><title>iPingYou Host Dashboard</title>
469
523
  <style>
@@ -504,22 +558,22 @@ code{background:var(--bg);padding:2px 8px;border-radius:4px;font-size:0.85rem}
504
558
  </style></head>
505
559
  <body>
506
560
  <div class="header">
507
- <div><h1>🛡️ iPingYou Host Dashboard</h1><span style="color:var(--dim);font-size:0.85rem">UID: <code>${uid}</code></span></div>
561
+ <div><h1>🛡️ iPingYou Host Dashboard</h1><span style="color:var(--dim);font-size:0.85rem">UID: <code>${dashboardUid}</code></span></div>
508
562
  <div style="display:flex;gap:1rem;align-items:center">
509
563
  <span class="badge">● LIVE</span>
510
- <button class="btn btn-revoke" onclick="revokeSession()">🚫 Revoke Session</button>
564
+ <button id="revoke-session" class="btn btn-revoke" type="button">🚫 Revoke Session</button>
511
565
  </div>
512
566
  </div>
513
567
 
514
568
  <div class="grid">
515
569
  <div class="card">
516
570
  <h2>📊 Session Info</h2>
517
- <div class="info-row"><span class="info-label">UID</span><span class="info-value"><code>${uid}</code></span></div>
571
+ <div class="info-row"><span class="info-label">UID</span><span class="info-value"><code>${dashboardUid}</code></span></div>
518
572
  <div class="info-row"><span class="info-label">Password</span><span class="info-value"><code>[Hidden — see terminal]</code></span></div>
519
- <div class="info-row"><span class="info-label">Service</span><span class="info-value">${serviceConfig.type.toUpperCase()} on port ${serviceConfig.port}</span></div>
573
+ <div class="info-row"><span class="info-label">Service</span><span class="info-value">${dashboardService} on port ${dashboardPort}</span></div>
520
574
  <div class="info-row"><span class="info-label">Approval Gate</span><span class="info-value">${serviceConfig.approvalRequired ? '<span style="color:var(--green)">✓ Enabled</span>' : '<span style="color:var(--dim)">Disabled</span>'}</span></div>
521
- <div class="info-row"><span class="info-label">Drop Folder</span><span class="info-value"><code>${serviceConfig.sharedDropPath || 'none'}</code></span></div>
522
- <div class="info-row"><span class="info-label">One-Time Share</span><span class="info-value"><code>${serviceConfig.oneTimeSharePath || 'none'}</code></span></div>
575
+ <div class="info-row"><span class="info-label">Drop Folder</span><span class="info-value"><code>${dashboardDropPath}</code></span></div>
576
+ <div class="info-row"><span class="info-label">One-Time Share</span><span class="info-value"><code>${dashboardSharePath}</code></span></div>
523
577
  <div class="info-row"><span class="info-label">Chat</span><span class="info-value">${serviceConfig.chatUrl ? '<span style="color:var(--green)">Active</span>' : '<span style="color:var(--dim)">Not started</span>'}</span></div>
524
578
  <div class="info-row"><span class="info-label">Uptime</span><span class="info-value" id="uptime">—</span></div>
525
579
  </div>
@@ -537,7 +591,7 @@ code{background:var(--bg);padding:2px 8px;border-radius:4px;font-size:0.85rem}
537
591
 
538
592
  <div id="toast"></div>
539
593
 
540
- <script>
594
+ <script nonce="${scriptNonce}">
541
595
  const startedAt = new Date("${startedAt}");
542
596
 
543
597
  function showToast(msg) {
@@ -547,14 +601,11 @@ function showToast(msg) {
547
601
  setTimeout(() => t.classList.remove('show'), 2500);
548
602
  }
549
603
 
550
- function escapeHtml(value) {
551
- return String(value || '').replace(/[&<>"']/g, (m) => ({
552
- '&': '&amp;',
553
- '<': '&lt;',
554
- '>': '&gt;',
555
- '"': '&quot;',
556
- "'": '&#39;',
557
- })[m]);
604
+ function appendEmptyState(container, text) {
605
+ const message = document.createElement('p');
606
+ message.className = 'empty';
607
+ message.textContent = text;
608
+ container.replaceChildren(message);
558
609
  }
559
610
 
560
611
  function updateUptime() {
@@ -615,39 +666,66 @@ function renderApprovals(approvals) {
615
666
  document.getElementById('approval-count').textContent = pending.length > 0 ? '(' + pending.length + ' pending)' : '';
616
667
 
617
668
  if (approvals.length === 0) {
618
- container.innerHTML = '<p class="empty">No approval requests yet</p>';
669
+ appendEmptyState(container, 'No approval requests yet');
619
670
  return;
620
671
  }
621
672
 
622
- let html = '';
673
+ const fragment = document.createDocumentFragment();
623
674
  for (const req of pending) {
624
- html += '<div class="approval-item">'
625
- + '<div style="display:flex;justify-content:space-between;align-items:center">'
626
- + '<strong>Request ' + req.id + '</strong>'
627
- + '<span class="status-badge status-pending">PENDING</span></div>'
628
- + '<div class="meta">Submitted: ' + new Date(req.createdAt).toLocaleTimeString() + '</div>'
629
- + '<div class="actions">'
630
- + '<button class="btn btn-approve" data-id="' + req.id + '" data-decision="approved">✅ Approve</button>'
631
- + '<button class="btn btn-deny" data-id="' + req.id + '" data-decision="denied">❌ Deny</button>'
632
- + '</div></div>';
675
+ const item = document.createElement('div');
676
+ item.className = 'approval-item';
677
+ const heading = document.createElement('div');
678
+ heading.style.cssText = 'display:flex;justify-content:space-between;align-items:center';
679
+ const title = document.createElement('strong');
680
+ title.textContent = 'Request ' + String(req.id || '');
681
+ const badge = document.createElement('span');
682
+ badge.className = 'status-badge status-pending';
683
+ badge.textContent = 'PENDING';
684
+ heading.append(title, badge);
685
+ const details = document.createElement('div');
686
+ details.className = 'meta';
687
+ details.textContent = 'User: ' + String(req.username || 'unknown') + ' | Host: ' + String(req.hostname || 'unknown') + ' | OS: ' + String(req.os || 'unknown');
688
+ const meta = document.createElement('div');
689
+ meta.className = 'meta';
690
+ meta.textContent = 'Submitted: ' + new Date(req.createdAt).toLocaleTimeString();
691
+ const actions = document.createElement('div');
692
+ actions.className = 'actions';
693
+ for (const choice of [
694
+ { decision: 'approved', label: '✅ Approve', className: 'btn btn-approve' },
695
+ { decision: 'denied', label: '❌ Deny', className: 'btn btn-deny' }
696
+ ]) {
697
+ const button = document.createElement('button');
698
+ button.className = choice.className;
699
+ button.type = 'button';
700
+ button.textContent = choice.label;
701
+ button.addEventListener('click', function() {
702
+ decide(String(req.id || ''), choice.decision);
703
+ });
704
+ actions.appendChild(button);
705
+ }
706
+ item.append(heading, details, meta, actions);
707
+ fragment.appendChild(item);
633
708
  }
634
709
  for (const req of decided) {
635
- const cls = req.status === 'approved' ? 'status-approved' : 'status-denied';
636
- html += '<div class="approval-item" style="opacity:0.6">'
637
- + '<div style="display:flex;justify-content:space-between;align-items:center">'
638
- + '<strong>Request ' + req.id + '</strong>'
639
- + '<span class="status-badge ' + cls + '">' + req.status.toUpperCase() + '</span></div>'
640
- + '<div class="meta">Decided: ' + (req.decidedAt ? new Date(req.decidedAt).toLocaleTimeString() : 'N/A') + '</div>'
641
- + '</div>';
710
+ const status = req.status === 'approved' ? 'approved' : 'denied';
711
+ const item = document.createElement('div');
712
+ item.className = 'approval-item';
713
+ item.style.opacity = '0.6';
714
+ const heading = document.createElement('div');
715
+ heading.style.cssText = 'display:flex;justify-content:space-between;align-items:center';
716
+ const title = document.createElement('strong');
717
+ title.textContent = 'Request ' + String(req.id || '');
718
+ const badge = document.createElement('span');
719
+ badge.className = 'status-badge status-' + status;
720
+ badge.textContent = status.toUpperCase();
721
+ heading.append(title, badge);
722
+ const meta = document.createElement('div');
723
+ meta.className = 'meta';
724
+ meta.textContent = 'Decided: ' + (req.decidedAt ? new Date(req.decidedAt).toLocaleTimeString() : 'N/A');
725
+ item.append(heading, meta);
726
+ fragment.appendChild(item);
642
727
  }
643
- container.innerHTML = html;
644
-
645
- // Attach click handlers via event delegation (avoids inline quote escaping issues)
646
- container.querySelectorAll('[data-decision]').forEach(function(btn) {
647
- btn.addEventListener('click', function() {
648
- decide(btn.getAttribute('data-id'), btn.getAttribute('data-decision'));
649
- });
650
- });
728
+ container.replaceChildren(fragment);
651
729
  }
652
730
 
653
731
  async function decide(requestId, decision) {
@@ -670,34 +748,48 @@ async function revokeSession() {
670
748
  else showToast('Failed to revoke');
671
749
  } catch (err) { showToast('Error: ' + err.message); }
672
750
  }
751
+ document.getElementById('revoke-session').addEventListener('click', revokeSession);
673
752
 
674
753
  function renderClients(clients) {
675
754
  const container = document.getElementById('clients');
676
755
  if (!clients || clients.length === 0) {
677
- container.innerHTML = '<p class="empty">No clients connected yet</p>';
756
+ appendEmptyState(container, 'No clients connected yet');
678
757
  document.getElementById('client-count').textContent = '';
679
758
  return;
680
759
  }
681
760
 
682
761
  document.getElementById('client-count').textContent = '(' + clients.length + ' connected)';
683
- let html = '';
762
+ const fragment = document.createDocumentFragment();
684
763
  clients.forEach((c, idx) => {
764
+ const card = document.createElement('div');
765
+ card.className = 'client-card';
685
766
  if (c.error) {
686
- html += '<div class="client-card"><strong>Client #' + (idx + 1) + '</strong> — payload decryption failed</div>';
767
+ const title = document.createElement('strong');
768
+ title.textContent = 'Client #' + (idx + 1);
769
+ card.append(title, document.createTextNode(' — payload decryption failed'));
770
+ fragment.appendChild(card);
687
771
  return;
688
772
  }
689
773
  const when = c.time || (c.seenAt ? new Date(c.seenAt).toLocaleTimeString() : 'Unknown');
690
- html += '<div class="client-card">'
691
- + '<strong>' + escapeHtml(c.username || 'Unknown') + '</strong>'
692
- + ' — ' + escapeHtml(c.action || 'connected') + '<br>'
693
- + '<span class="meta">IP: ' + escapeHtml(c.ip || 'Unknown') + '</span><br>'
694
- + '<span class="meta">OS: ' + escapeHtml(c.os || 'Unknown') + '</span><br>'
695
- + '<span class="meta">CPU: ' + escapeHtml(c.cpu || 'Unknown') + '</span><br>'
696
- + '<span class="meta">RAM: ' + escapeHtml(c.ram || 'Unknown') + '</span><br>'
697
- + '<span class="meta">Time: ' + escapeHtml(when) + '</span>'
698
- + '</div>';
774
+ const title = document.createElement('strong');
775
+ title.textContent = String(c.username || 'Unknown');
776
+ card.append(title, document.createTextNode(' — ' + String(c.action || 'connected')), document.createElement('br'));
777
+ for (const field of [
778
+ ['IP', c.ip],
779
+ ['OS', c.os],
780
+ ['CPU', c.cpu],
781
+ ['RAM', c.ram],
782
+ ['Time', when]
783
+ ]) {
784
+ const meta = document.createElement('span');
785
+ meta.className = 'meta';
786
+ meta.textContent = field[0] + ': ' + String(field[1] || 'Unknown');
787
+ card.appendChild(meta);
788
+ if (field[0] !== 'Time') card.appendChild(document.createElement('br'));
789
+ }
790
+ fragment.appendChild(card);
699
791
  });
700
- container.innerHTML = html;
792
+ container.replaceChildren(fragment);
701
793
  }
702
794
 
703
795
  </script>
@@ -709,7 +801,7 @@ function renderClients(clients) {
709
801
  const port = server.address().port;
710
802
  const url = `http://127.0.0.1:${port}`;
711
803
  console.log(chalk.green(` ✓ Local dashboard: ${url}`));
712
- try { await open(url); } catch { }
804
+ try { await openUrl(url); } catch { }
713
805
  resolve({ url, close: () => server.close() });
714
806
  });
715
807
  });
@@ -721,7 +813,6 @@ function renderClients(clients) {
721
813
  async function spawnPrivateBroker() {
722
814
  console.log(chalk.yellow('\n ⚠️ Public Broker is unreachable. Spawning Private Broker...'));
723
815
 
724
- const packageRoot = path.resolve(__dirname, '../..');
725
816
  const serverEntrypoint = path.join(__dirname, '../server.js');
726
817
  const requireFromServer = createRequire(serverEntrypoint);
727
818
  const requiredBrokerPackages = ['express', 'express-rate-limit', 'helmet'];
@@ -735,34 +826,10 @@ async function spawnPrivateBroker() {
735
826
  });
736
827
 
737
828
  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 }
829
+ throw new Error(
830
+ `Private broker dependencies are missing: ${missingPackages.join(', ')}. `
831
+ + 'Reinstall iPingYou from its verified package before starting a private broker.'
744
832
  );
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
833
  }
767
834
 
768
835
  // 1. Spawn the broker server process
@@ -823,6 +890,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
823
890
  let dashboardInstance = null;
824
891
 
825
892
  const renderDashboard = () => {
893
+ const isPrivateBroker = Boolean(global.privateBrokerInstance);
826
894
  console.clear();
827
895
  console.log('');
828
896
  console.log(chalk.bold(' ╔════════════════════════════════════════════════════╗'));
@@ -831,7 +899,9 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
831
899
  console.log(` ║ ${chalk.cyan('UID:')} ${chalk.bold.white(uid.padEnd(30))}║`);
832
900
  console.log(` ║ ${chalk.cyan('Password:')} ${chalk.bold.white(secureSensitive(password).padEnd(30))}║`);
833
901
  console.log(` ║ ${chalk.cyan('Service:')} ${chalk.dim(serviceConfig.type.toUpperCase() + ' (Port ' + serviceConfig.port + ')').padEnd(30)}║`);
834
- console.log(` ║ ${chalk.cyan('Tunnel:')} ${chalk.dim(sessionState.tunnelUrl.substring(0, 40))} ║`);
902
+ if (isPrivateBroker) {
903
+ console.log(` ║ ${chalk.cyan('Tunnel:')} ${chalk.dim(sessionState.tunnelUrl.substring(0, 40))} ║`);
904
+ }
835
905
  if (serviceConfig.chatUrl) {
836
906
  console.log(` ║ ${chalk.cyan('Chat URL:')} ${chalk.dim(serviceConfig.chatUrl.substring(0, 40))} ║`);
837
907
  }
@@ -1080,12 +1150,11 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
1080
1150
  return waitForAction();
1081
1151
 
1082
1152
  case 'terminate': {
1083
- const spinner = createSpinner('Terminating active SSH sessions...', networkSpinner).start();
1153
+ const spinner = createSpinner('Revoking session and closing its tunnel...', networkSpinner).start();
1084
1154
  try {
1085
- if (process.platform === 'win32') {
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 });
1087
- } else {
1088
- await execa('pkill', ['-f', 'sshd:.*@'], { reject: false });
1155
+ await revokeUID(BROKER_URL, uid, sessionState.hostToken);
1156
+ tunnelProcess.kill();
1157
+ if (process.platform !== 'win32') {
1089
1158
  await execa('tmux', [...tmuxSocketArgs(), 'kill-server'], { reject: false });
1090
1159
  const legacySessions = await listTmuxSessions();
1091
1160
  for (const session of legacySessions) {
@@ -1094,7 +1163,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
1094
1163
  }
1095
1164
  }
1096
1165
  }
1097
- spinner.succeed('All client SSH sessions terminated');
1166
+ spinner.succeed('Session revoked and iPingYou-owned connections terminated');
1098
1167
  logSessionEvent('host_sessions_terminated');
1099
1168
  } catch {
1100
1169
  spinner.warn('Could not terminate sessions (none active?)');
@@ -1271,13 +1340,13 @@ export async function startHostMode() {
1271
1340
  console.log(chalk.dim(' 🔑 Generating ephemeral SSH key for passwordless entry...'));
1272
1341
  try {
1273
1342
  const ephemeralKey = await generateEphemeralKey();
1274
- const authKeysPath = await injectPublicKey(ephemeralKey.pubKey);
1343
+ const { authKeysPath, authorizedKey } = await injectPublicKey(ephemeralKey.pubKey);
1275
1344
 
1276
1345
  serviceConfig.privateKey = ephemeralKey.privKey;
1277
1346
 
1278
1347
  addCleanupHook(async () => {
1279
1348
  console.log(chalk.dim(' Removing ephemeral public key...'));
1280
- await removePublicKey(authKeysPath, ephemeralKey.pubKey);
1349
+ await removePublicKey(authKeysPath, authorizedKey);
1281
1350
  try { await fs.promises.unlink(ephemeralKey.keyPath); } catch { }
1282
1351
  try { await fs.promises.unlink(`${ephemeralKey.keyPath}.pub`); } catch { }
1283
1352
  });
package/src/server.js CHANGED
@@ -435,8 +435,11 @@ app.get('/approval-requests/:uid', hostLimiter, (req, res) => {
435
435
  if (!entry) return res.status(404).json({ error: 'UID not found' });
436
436
  // Require host token to view approval list
437
437
  if (!requireHostToken(req, res, entry)) return;
438
- // Strip encrypted payloads host dashboard only needs id/status/timestamps
439
- const sanitized = entry.approvals.map(a => ({ id: a.id, status: a.status, createdAt: a.createdAt, decidedAt: a.decidedAt }));
438
+ // Include encrypted payloads so the host can decrypt client details locally
439
+ const sanitized = entry.approvals.map(a => ({
440
+ id: a.id, status: a.status, createdAt: a.createdAt, decidedAt: a.decidedAt,
441
+ iv: a.iv, ciphertext: a.ciphertext, salt: a.salt,
442
+ }));
440
443
  res.json({ approvalRequired: entry.approvalRequired, approvals: sanitized });
441
444
  });
442
445