webtun 1.5.1 → 1.5.3

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/server.js CHANGED
@@ -141,7 +141,7 @@ app.use((req, res, next) => {
141
141
  res.setHeader('X-Frame-Options', 'DENY');
142
142
  res.setHeader('Referrer-Policy', 'no-referrer');
143
143
  res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
144
- res.setHeader('Content-Security-Policy', "default-src 'self'; connect-src 'self' https://*.trycloudflare.com wss:; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; font-src 'self' data:; img-src 'self' data:;");
144
+ res.setHeader('Content-Security-Policy', "default-src 'self'; connect-src 'self' https://*.trycloudflare.com wss:; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; font-src 'self' data: https://fonts.gstatic.com https://fonts.googleapis.com; img-src 'self' data:;");
145
145
  next();
146
146
  });
147
147
 
@@ -1272,9 +1272,12 @@ app.delete('/api/history/:index', checkPin, (req, res) => {
1272
1272
  res.json({ success: true, history: cmdHistory });
1273
1273
  });
1274
1274
 
1275
- // ── Session persistence via tmux ──────────────────────────────────────
1275
+ // ── Session persistence ──────────────────────────────────────────────
1276
1276
  const TMUX = (() => { try { return execSync('command -v tmux', { stdio: ['ignore','pipe','ignore'] }).toString().trim(); } catch { return null; } })();
1277
1277
 
1278
+ // In-memory PTY session store — enables persistence without tmux (Windows + Linux)
1279
+ const ptySessions = new Map(); // sessionId -> { proc, drainCheck, createdAt }
1280
+
1278
1281
  function isValidPID(pid) {
1279
1282
  return typeof pid === 'number' && Number.isInteger(pid) && pid > 0;
1280
1283
  }
@@ -1301,22 +1304,38 @@ function tmuxSessionExists(name) {
1301
1304
  }
1302
1305
 
1303
1306
  app.get('/api/sessions', checkPin, (req, res) => {
1304
- if (!TMUX) return res.json({ tmux: false, sessions: [] });
1305
- try {
1306
- const out = execFileSync(TMUX, ['list-sessions', '-F', '#{session_name}'], { encoding: 'utf8' }).trim();
1307
- const sessions = out.split('\n')
1308
- .filter(s => s.startsWith('wt-'))
1309
- .map(s => ({ id: s.replace(/^wt-/, ''), name: s }));
1310
- res.json({ tmux: true, sessions });
1311
- } catch {
1312
- res.json({ tmux: true, sessions: [] });
1307
+ if (TMUX) {
1308
+ try {
1309
+ const out = execFileSync(TMUX, ['list-sessions', '-F', '#{session_name}'], { encoding: 'utf8' }).trim();
1310
+ const sessions = out.split('\n')
1311
+ .filter(s => s.startsWith('wt-'))
1312
+ .map(s => ({ id: s.replace(/^wt-/, ''), name: s }));
1313
+ return res.json({ tmux: true, sessions });
1314
+ } catch {
1315
+ return res.json({ tmux: true, sessions: [] });
1316
+ }
1317
+ }
1318
+ // In-memory sessions (no tmux)
1319
+ const sessions = [];
1320
+ for (const [id] of ptySessions) {
1321
+ sessions.push({ id, name: 'wt-' + id });
1313
1322
  }
1323
+ res.json({ tmux: false, sessions });
1314
1324
  });
1315
1325
 
1316
1326
  app.delete('/api/sessions/:id', checkPin, (req, res) => {
1317
- if (!TMUX) return res.json({ success: false });
1318
- const name = 'wt-' + req.params.id.replace(/[^a-zA-Z0-9_-]/g, '');
1319
- try { execFileSync(TMUX, ['kill-session', '-t', name], { stdio: 'ignore' }); } catch {}
1327
+ const id = req.params.id.replace(/[^a-zA-Z0-9_-]/g, '');
1328
+ if (TMUX) {
1329
+ const name = 'wt-' + id;
1330
+ try { execFileSync(TMUX, ['kill-session', '-t', name], { stdio: 'ignore' }); } catch {}
1331
+ return res.json({ success: true });
1332
+ }
1333
+ // In-memory session
1334
+ const entry = ptySessions.get(id);
1335
+ if (entry) {
1336
+ try { entry.proc.kill(); } catch {}
1337
+ ptySessions.delete(id);
1338
+ }
1320
1339
  res.json({ success: true });
1321
1340
  });
1322
1341
 
@@ -1378,8 +1397,28 @@ wss.on('connection', (ws, req) => {
1378
1397
  };
1379
1398
 
1380
1399
  let proc;
1400
+ let reattached = false;
1381
1401
  try {
1382
- if (TMUX && sessionId) {
1402
+ if (sessionId && !TMUX) {
1403
+ // ── In-memory PTY persistence (no tmux needed) ──
1404
+ const existing = ptySessions.get(sessionId);
1405
+ if (existing && existing.proc && !existing.exited) {
1406
+ // Reattach: remove old listeners, reuse the running PTY
1407
+ proc = existing.proc;
1408
+ proc.removeAllListeners('data');
1409
+ proc.removeAllListeners('exit');
1410
+ proc.resize(cols, rows);
1411
+ reattached = true;
1412
+ } else {
1413
+ // New in-memory session
1414
+ if (existing) ptySessions.delete(sessionId);
1415
+ const shellArgs = os.platform() === 'win32' ? ['-NoLogo'] : ['-l'];
1416
+ proc = pty.spawn(SHELL, shellArgs, {
1417
+ name: 'xterm-256color', cols, rows, cwd,
1418
+ env: sessionEnv
1419
+ });
1420
+ }
1421
+ } else if (TMUX && sessionId) {
1383
1422
  const tmuxName = 'wt-' + sessionId;
1384
1423
  const exists = tmuxSessionExists(tmuxName);
1385
1424
 
@@ -1413,6 +1452,12 @@ wss.on('connection', (ws, req) => {
1413
1452
  const HIGH_WATER = 4 * 1024 * 1024; // 4MB — pause PTY above this
1414
1453
  const LOW_WATER = 1 * 1024 * 1024; // 1MB — resume PTY below this
1415
1454
 
1455
+ const drainCheck = setInterval(() => {
1456
+ if (paused && ws.bufferedAmount < LOW_WATER) {
1457
+ try { proc.resume(); paused = false; } catch (_) {}
1458
+ }
1459
+ }, 50);
1460
+
1416
1461
  proc.onData(data => {
1417
1462
  if (ws.readyState !== WebSocket.OPEN) return;
1418
1463
  send(0x00, data);
@@ -1422,19 +1467,29 @@ wss.on('connection', (ws, req) => {
1422
1467
  }
1423
1468
  });
1424
1469
 
1425
- // Drain check: resume PTY when buffer drops
1426
- const drainCheck = setInterval(() => {
1427
- if (paused && ws.bufferedAmount < LOW_WATER) {
1428
- try { proc.resume(); paused = false; } catch (_) {}
1429
- }
1430
- }, 50);
1470
+ const useInMemory = sessionId && !TMUX;
1471
+ const useTmux = TMUX && sessionId;
1431
1472
 
1432
1473
  proc.onExit(() => {
1433
1474
  clearInterval(drainCheck);
1434
- if (!TMUX || !sessionId) send(0x01, Buffer.from([0]));
1475
+ if (useInMemory) ptySessions.delete(sessionId);
1476
+ if (!useTmux) send(0x01, Buffer.from([0]));
1435
1477
  ws.close();
1436
1478
  });
1437
1479
 
1480
+ // If this is a new in-memory session, register it now (after onExit is wired)
1481
+ if (useInMemory && !rehattached) {
1482
+ ptySessions.set(sessionId, { proc, exited: false, createdAt: Date.now() });
1483
+ // Track exit so stale sessions are detected on reconnect
1484
+ proc.onExit(() => {
1485
+ const entry = ptySessions.get(sessionId);
1486
+ if (entry) entry.exited = true;
1487
+ });
1488
+ } else if (useInMemory && reattached) {
1489
+ const entry = ptySessions.get(sessionId);
1490
+ if (entry) { entry.proc = proc; entry.exited = false; }
1491
+ }
1492
+
1438
1493
  ws.isAlive = true;
1439
1494
  const pingInterval = setInterval(() => {
1440
1495
  if (!ws.isAlive) { clearInterval(pingInterval); ws.terminate(); return; }
@@ -1467,6 +1522,11 @@ wss.on('connection', (ws, req) => {
1467
1522
  const cleanup = () => {
1468
1523
  clearInterval(pingInterval);
1469
1524
  clearInterval(drainCheck);
1525
+ if (useInMemory && sessionId) {
1526
+ // Keep the PTY alive for reattachment — just detach listeners
1527
+ try { proc.removeAllListeners('data'); } catch {}
1528
+ return;
1529
+ }
1470
1530
  try { proc.kill(); } catch {}
1471
1531
  };
1472
1532
  ws.on('close', cleanup);
@@ -1928,6 +1988,11 @@ function cleanup() {
1928
1988
  }
1929
1989
  } catch {}
1930
1990
  }
1991
+ // Kill all in-memory PTY sessions
1992
+ for (const [id, entry] of ptySessions) {
1993
+ try { entry.proc.kill(); } catch {}
1994
+ }
1995
+ ptySessions.clear();
1931
1996
  }
1932
1997
 
1933
1998
  function startServer(opts = {}) {