termbeam 1.22.2 → 1.22.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.22.4] - 2026-04-26
4
+
5
+ - perf(test): parallelize e2e suite via shared server + workers (#214) (@dorlugasigal)
6
+ - fix(security): add inline path validation guards for CodeQL (#216) (@dorlugasigal)
7
+ - fix(deps): update vulnerable dependencies and pin Docker image (#218) (@dorlugasigal)
8
+ - refactor(site): clean up landing page design and optimize CI (#220) (@dorlugasigal)
9
+
10
+ ## [1.22.3] - 2026-04-26
11
+
12
+ - fix(site): auto-detect Cloudflare Pages build target (@dorlugasigal)
13
+ - ci(site): restore Cloudflare Pages deploy workflow (@dorlugasigal)
14
+ - ci(site): merge GitHub Pages and Cloudflare Pages into one workflow (@dorlugasigal)
15
+ - chore(ci): enforce 92% coverage gate (#212) (@dorlugasigal)
16
+
3
17
  ## [1.22.2] - 2026-04-26
4
18
 
5
19
  - fix(site): repair mobile feature card rendering (@dorlugasigal)
package/README.md CHANGED
@@ -137,6 +137,8 @@ The installer checks for [PM2](https://pm2.keymetrics.io/) (and offers to instal
137
137
 
138
138
  For systemd, launchd, and Windows Task Scheduler setup, see the [Running in Background docs](https://dorlugasigal.github.io/TermBeam/running-in-background/).
139
139
 
140
+ > 💡 **Keep the host awake** so the service stays reachable while you're away. macOS: pair with [Amphetamine](https://apps.apple.com/app/amphetamine/id937984704) (process trigger on `node`) or wrap with `caffeinate -dims`. Windows: enable [PowerToys Awake](https://learn.microsoft.com/windows/powertoys/) and disable network adapter power saving. Linux: use `systemd-inhibit` in your unit file. See [Keeping the Host Awake](https://dorlugasigal.github.io/TermBeam/running-in-background/#keeping-the-host-awake-) for the full setup.
141
+
140
142
  ## Security
141
143
 
142
144
  TermBeam auto-generates a password and creates a secure tunnel by default, binding to `127.0.0.1` (localhost only). Auth uses httpOnly cookies with 24-hour expiry, login is rate-limited to 5 attempts per minute, QR codes contain single-use share tokens (5-min expiry), and security headers (X-Frame-Options, CSP, nosniff) are set on all responses.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termbeam",
3
- "version": "1.22.2",
3
+ "version": "1.22.4",
4
4
  "description": "Beam your terminal to any device — mobile-optimized web terminal with multi-session support",
5
5
  "main": "src/server/index.js",
6
6
  "bin": {
@@ -9,8 +9,8 @@
9
9
  "scripts": {
10
10
  "start": "node bin/termbeam.js",
11
11
  "dev": "node bin/termbeam.js --generate-password",
12
- "test": "node -e \"const{execFileSync:r}=require('child_process'),{readdirSync:d,statSync:s}=require('fs'),{join:j}=require('path');function f(p){let a=[];for(const e of d(p)){const c=j(p,e);s(c).isDirectory()?a.push(...f(c)):e.endsWith('.test.js')&&!e.startsWith('e2e-')&&e!=='devtunnel-install.test.js'&&a.push(c)}return a}r(process.execPath,['--test','--test-timeout=60000',...f('test')],{stdio:'inherit'})\"",
13
- "test:coverage": "c8 --exclude=src/tunnel/ --exclude=test --reporter=text --reporter=lcov --reporter=json-summary --reporter=json node -e \"const{execFileSync:r}=require('child_process'),{readdirSync:d,statSync:s}=require('fs'),{join:j}=require('path');function f(p){let a=[];for(const e of d(p)){const c=j(p,e);s(c).isDirectory()?a.push(...f(c)):e.endsWith('.test.js')&&!e.startsWith('e2e-')&&e!=='devtunnel-install.test.js'&&a.push(c)}return a}r(process.execPath,['--test','--test-timeout=60000','--test-reporter=spec','--test-reporter-destination=stdout',...f('test')],{stdio:'inherit'})\"",
12
+ "test": "node -e \"const{execFileSync:r}=require('child_process'),{readdirSync:d,statSync:s}=require('fs'),{join:j}=require('path');function f(p){let a=[];for(const e of d(p)){const c=j(p,e);s(c).isDirectory()?a.push(...f(c)):e.endsWith('.test.js')&&!e.startsWith('e2e-')&&e!=='devtunnel-install.test.js'&&a.push(c)}return a}r(process.execPath,['--test','--test-timeout=180000',...f('test')],{stdio:'inherit'})\"",
13
+ "test:coverage": "c8 --reporter=text --reporter=lcov --reporter=json-summary --reporter=json node -e \"const{execFileSync:r}=require('child_process'),{readdirSync:d,statSync:s}=require('fs'),{join:j}=require('path');function f(p){let a=[];for(const e of d(p)){const c=j(p,e);s(c).isDirectory()?a.push(...f(c)):e.endsWith('.test.js')&&!e.startsWith('e2e-')&&e!=='devtunnel-install.test.js'&&a.push(c)}return a}r(process.execPath,['--test','--test-timeout=180000','--test-reporter=spec','--test-reporter-destination=stdout',...f('test')],{stdio:'inherit'})\"",
14
14
  "prepare": "husky",
15
15
  "format": "prettier --write .",
16
16
  "lint": "node --check src/server/*.js src/cli/*.js src/tunnel/*.js src/utils/*.js bin/*.js",
@@ -84,6 +84,17 @@
84
84
  "optionalDependencies": {
85
85
  "better-sqlite3": "^12.8.0"
86
86
  },
87
+ "c8": {
88
+ "check-coverage": true,
89
+ "lines": 92,
90
+ "functions": 92,
91
+ "branches": 85,
92
+ "statements": 92,
93
+ "exclude": [
94
+ "src/tunnel/",
95
+ "test"
96
+ ]
97
+ },
87
98
  "devDependencies": {
88
99
  "@eslint/js": "^10.0.1",
89
100
  "@playwright/test": "^1.58.2",
package/src/cli/resume.js CHANGED
@@ -6,15 +6,22 @@ const log = require('../utils/logger');
6
6
  const { createTerminalClient } = require('./client');
7
7
  const { bold, dim, red, yellow, choose, createRL, ask } = require('./prompts');
8
8
 
9
- const CONFIG_DIR = process.env.TERMBEAM_CONFIG_DIR || path.join(os.homedir(), '.termbeam');
10
- const CONNECTION_FILE = path.join(CONFIG_DIR, 'connection.json');
9
+ // Resolve config paths at call time so that tests (and other code paths) which
10
+ // set TERMBEAM_CONFIG_DIR after this module is first required still take effect.
11
+ function getConfigDir() {
12
+ return process.env.TERMBEAM_CONFIG_DIR || path.join(os.homedir(), '.termbeam');
13
+ }
14
+
15
+ function getConnectionFile() {
16
+ return path.join(getConfigDir(), 'connection.json');
17
+ }
11
18
 
12
19
  // ── Connection config ────────────────────────────────────────────────────────
13
20
 
14
21
  function readConnectionConfig() {
15
22
  log.debug('Reading connection config');
16
23
  try {
17
- return JSON.parse(fs.readFileSync(CONNECTION_FILE, 'utf8'));
24
+ return JSON.parse(fs.readFileSync(getConnectionFile(), 'utf8'));
18
25
  } catch {
19
26
  return null;
20
27
  }
@@ -22,14 +29,15 @@ function readConnectionConfig() {
22
29
 
23
30
  function writeConnectionConfig({ port, host, password }) {
24
31
  log.debug('Writing connection config');
25
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
26
- fs.writeFileSync(CONNECTION_FILE, JSON.stringify({ port, host, password }, null, 2) + '\n', {
32
+ const connectionFile = getConnectionFile();
33
+ fs.mkdirSync(getConfigDir(), { recursive: true });
34
+ fs.writeFileSync(connectionFile, JSON.stringify({ port, host, password }, null, 2) + '\n', {
27
35
  mode: 0o600,
28
36
  });
29
37
  // Ensure restrictive permissions even if the file already existed
30
38
  if (process.platform !== 'win32') {
31
39
  try {
32
- fs.chmodSync(CONNECTION_FILE, 0o600);
40
+ fs.chmodSync(connectionFile, 0o600);
33
41
  } catch {
34
42
  /* best-effort */
35
43
  }
@@ -39,7 +47,7 @@ function writeConnectionConfig({ port, host, password }) {
39
47
  function removeConnectionConfig() {
40
48
  log.debug('Removing connection config');
41
49
  try {
42
- fs.unlinkSync(CONNECTION_FILE);
50
+ fs.unlinkSync(getConnectionFile());
43
51
  } catch {
44
52
  /* ignore */
45
53
  }
@@ -402,6 +410,12 @@ module.exports = {
402
410
  readConnectionConfig,
403
411
  printResumeHelp,
404
412
  parseDetachKey,
405
- CONFIG_DIR,
406
- CONNECTION_FILE,
413
+ // Lazy getters so tests reading these after setting TERMBEAM_CONFIG_DIR see
414
+ // the current value, not the value at module load time.
415
+ get CONFIG_DIR() {
416
+ return getConfigDir();
417
+ },
418
+ get CONNECTION_FILE() {
419
+ return getConnectionFile();
420
+ },
407
421
  };
@@ -98,23 +98,28 @@ function validateMagicBytes(buffer, contentType) {
98
98
  }
99
99
 
100
100
  function setupRoutes(app, { auth, sessions, config, state, pushManager, copilotService }) {
101
- const pageRateLimit = rateLimit({
102
- windowMs: 1 * 60 * 1000,
103
- max: 120,
104
- standardHeaders: true,
105
- legacyHeaders: false,
106
- handler: (_req, res) =>
107
- res.status(429).json({ error: 'Too many requests, please try again later.' }),
108
- });
101
+ const noopLimit = (_req, _res, next) => next();
102
+ const pageRateLimit = config.disableRateLimit
103
+ ? noopLimit
104
+ : rateLimit({
105
+ windowMs: 1 * 60 * 1000,
106
+ max: 120,
107
+ standardHeaders: true,
108
+ legacyHeaders: false,
109
+ handler: (_req, res) =>
110
+ res.status(429).json({ error: 'Too many requests, please try again later.' }),
111
+ });
109
112
 
110
- const apiRateLimit = rateLimit({
111
- windowMs: 1 * 60 * 1000,
112
- max: 120,
113
- standardHeaders: true,
114
- legacyHeaders: false,
115
- handler: (_req, res) =>
116
- res.status(429).json({ error: 'Too many requests, please try again later.' }),
117
- });
113
+ const apiRateLimit = config.disableRateLimit
114
+ ? noopLimit
115
+ : rateLimit({
116
+ windowMs: 1 * 60 * 1000,
117
+ max: 120,
118
+ standardHeaders: true,
119
+ legacyHeaders: false,
120
+ handler: (_req, res) =>
121
+ res.status(429).json({ error: 'Too many requests, please try again later.' }),
122
+ });
118
123
 
119
124
  // Serve static files — sw.js must never be cached by the browser
120
125
  app.get('/sw.js', (_req, res, next) => {
@@ -860,7 +865,7 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager, copilotS
860
865
 
861
866
  const rootDir = path.resolve(sessions.getSessionCwd(req.params.id));
862
867
  const dir = safePath(rootDir, req.query.dir || '.');
863
- if (!dir) {
868
+ if (!dir || (dir !== rootDir && !dir.startsWith(rootDir + path.sep))) {
864
869
  return res.status(403).json({ error: 'Path is outside session directory' });
865
870
  }
866
871
 
@@ -930,7 +935,7 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager, copilotS
930
935
  let startDir = rootDir;
931
936
  if (typeof req.query.path === 'string' && req.query.path.length > 0) {
932
937
  const resolved = safePath(rootDir, req.query.path);
933
- if (!resolved) {
938
+ if (!resolved || (resolved !== rootDir && !resolved.startsWith(rootDir + path.sep))) {
934
939
  return res.status(403).json({ error: 'Path is outside session directory' });
935
940
  }
936
941
  try {
@@ -1040,7 +1045,7 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager, copilotS
1040
1045
 
1041
1046
  const rootDir = path.resolve(sessions.getSessionCwd(req.params.id));
1042
1047
  const filePath = safePath(rootDir, file);
1043
- if (!filePath) {
1048
+ if (!filePath || (filePath !== rootDir && !filePath.startsWith(rootDir + path.sep))) {
1044
1049
  return res.status(403).json({ error: 'Path is outside session directory' });
1045
1050
  }
1046
1051
 
@@ -1074,7 +1079,7 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager, copilotS
1074
1079
 
1075
1080
  const rootDir = path.resolve(sessions.getSessionCwd(req.params.id));
1076
1081
  const filePath = safePath(rootDir, file);
1077
- if (!filePath) {
1082
+ if (!filePath || (filePath !== rootDir && !filePath.startsWith(rootDir + path.sep))) {
1078
1083
  return res.status(403).json({ error: 'Path is outside session directory' });
1079
1084
  }
1080
1085
 
@@ -1108,7 +1113,7 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager, copilotS
1108
1113
 
1109
1114
  const rootDir = path.resolve(sessions.getSessionCwd(req.params.id));
1110
1115
  const filePath = safePath(rootDir, file);
1111
- if (!filePath) {
1116
+ if (!filePath || (filePath !== rootDir && !filePath.startsWith(rootDir + path.sep))) {
1112
1117
  return res.status(403).json({ error: 'Path is outside session directory' });
1113
1118
  }
1114
1119