termbeam 1.2.6 → 1.2.8

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/README.md CHANGED
@@ -104,27 +104,29 @@ Persisted tunnels save a tunnel ID to `~/.termbeam/tunnel.json` so the URL stays
104
104
  ```bash
105
105
  termbeam [shell] [args...] # start with a specific shell (default: auto-detect)
106
106
  termbeam --port 8080 # custom port (default: 3456)
107
- termbeam --host 127.0.0.1 # restrict to localhost (default: 0.0.0.0)
107
+ termbeam --host 0.0.0.0 # allow LAN access (default: 127.0.0.1)
108
+ termbeam --lan # shortcut for --host 0.0.0.0
108
109
  ```
109
110
 
110
111
  | Flag | Description | Default |
111
112
  | --------------------- | ---------------------------------------------------- | -------------- |
112
113
  | `--password <pw>` | Set access password (also accepts `--password=<pw>`) | Auto-generated |
113
- | `--no-password` | Disable password | — |
114
+ | `--no-password` | Disable password (cannot combine with `--public`) | — |
114
115
  | `--generate-password` | Auto-generate a secure password | On |
115
116
  | `--tunnel` | Create an ephemeral devtunnel URL (private) | On |
116
117
  | `--no-tunnel` | Disable tunnel (LAN-only) | — |
117
118
  | `--persisted-tunnel` | Create a reusable devtunnel URL | Off |
118
119
  | `--public` | Allow public tunnel access | Off |
119
120
  | `--port <port>` | Server port | `3456` |
120
- | `--host <addr>` | Bind address | `0.0.0.0` |
121
+ | `--host <addr>` | Bind address | `127.0.0.1` |
122
+ | `--lan` | Bind to all interfaces (LAN access) | Off |
121
123
  | `--log-level <level>` | Log verbosity (error/warn/info/debug) | `info` |
122
124
 
123
125
  Environment variables: `PORT`, `TERMBEAM_PASSWORD`, `TERMBEAM_CWD`, `TERMBEAM_LOG_LEVEL`, `SHELL` (Unix fallback), `COMSPEC` (Windows fallback). See [Configuration docs](https://dorlugasigal.github.io/TermBeam/configuration/).
124
126
 
125
127
  ## Security
126
128
 
127
- TermBeam auto-generates a password and creates a tunnel by default, so your terminal is protected out of the box. Be aware that the tunnel exposes your terminal to the internet use `--no-tunnel` for LAN-only access, or `--host 127.0.0.1` to restrict to your machine only.
129
+ TermBeam auto-generates a password and creates a tunnel by default, so your terminal is protected out of the box. By default, the server binds to `127.0.0.1` (localhost only). Use `--lan` or `--host 0.0.0.0` to allow LAN access, or `--no-tunnel` to disable the tunnel.
128
130
 
129
131
  Auth uses secure httpOnly cookies with 24-hour expiry, login is rate-limited to 5 attempts per minute, and security headers (X-Frame-Options, X-Content-Type-Options, etc.) are set on all responses. The QR code on startup embeds a share token for password-free login — the token is reusable within its 5-minute validity window, which handles tunnel proxy retries and link preview services. API clients that can't use cookies can authenticate with an `Authorization: Bearer <password>` header. See the [Security Guide](https://dorlugasigal.github.io/TermBeam/security/) for more.
130
132
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termbeam",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "description": "Beam your terminal to any device — mobile-optimized web terminal with multi-session support",
5
5
  "main": "src/server.js",
6
6
  "bin": {
package/src/auth.js CHANGED
@@ -129,7 +129,22 @@ function createAuth(password) {
129
129
  if (!password) return next();
130
130
  if (req.cookies.pty_token && validateToken(req.cookies.pty_token)) return next();
131
131
  const authHeader = req.headers.authorization;
132
- if (authHeader === `Bearer ${password}`) return next();
132
+ if (authHeader && authHeader.startsWith('Bearer ')) {
133
+ const ip = req.ip || req.socket.remoteAddress;
134
+ const now = Date.now();
135
+ const window = 60 * 1000;
136
+ const maxAttempts = 5;
137
+ const attempts = authAttempts.get(ip) || [];
138
+ const recent = attempts.filter((t) => now - t < window);
139
+ if (recent.length >= maxAttempts) {
140
+ log.warn(`Auth: rate limit exceeded for ${ip}`);
141
+ return res.status(429).json({ error: 'Too many attempts. Try again later.' });
142
+ }
143
+ if (authHeader === `Bearer ${password}`) return next();
144
+ recent.push(now);
145
+ authAttempts.set(ip, recent);
146
+ return res.status(401).json({ error: 'unauthorized' });
147
+ }
133
148
  if (req.accepts('html')) return res.redirect('/login');
134
149
  res.status(401).json({ error: 'unauthorized' });
135
150
  }
package/src/cli.js CHANGED
@@ -20,7 +20,8 @@ Options:
20
20
  --persisted-tunnel Create a reusable devtunnel URL (stable across restarts)
21
21
  --public Allow public tunnel access (default: private, owner-only)
22
22
  --port <port> Set port (default: 3456, or PORT env var)
23
- --host <addr> Bind address (default: 0.0.0.0)
23
+ --host <addr> Bind address (default: 127.0.0.1)
24
+ --lan Bind to 0.0.0.0 (allow LAN access, default: localhost only)
24
25
  --log-level <level> Set log verbosity: error, warn, info, debug (default: info)
25
26
  -h, --help Show this help
26
27
  -v, --version Show version
@@ -206,7 +207,7 @@ function getDefaultShell() {
206
207
 
207
208
  function parseArgs() {
208
209
  let port = parseInt(process.env.PORT || '3456', 10);
209
- let host = '0.0.0.0';
210
+ let host = '127.0.0.1';
210
211
 
211
212
  // Resolve log level early (env + args) so shell detection logs are visible
212
213
  let logLevel = process.env.TERMBEAM_LOG_LEVEL || 'info';
@@ -230,7 +231,7 @@ function parseArgs() {
230
231
  let useTunnel = true;
231
232
  let noTunnel = false;
232
233
  let persistedTunnel = false;
233
- let anonymousTunnel = false;
234
+ let publicTunnel = false;
234
235
  let explicitPassword = !!password;
235
236
 
236
237
  const args = process.argv.slice(2);
@@ -248,7 +249,7 @@ function parseArgs() {
248
249
  useTunnel = true;
249
250
  persistedTunnel = true;
250
251
  } else if (args[i] === '--public') {
251
- anonymousTunnel = true;
252
+ publicTunnel = true;
252
253
  } else if (args[i].startsWith('--password=')) {
253
254
  password = args[i].split('=')[1];
254
255
  explicitPassword = true;
@@ -267,6 +268,8 @@ function parseArgs() {
267
268
  explicitPassword = true;
268
269
  } else if (args[i] === '--port' && args[i + 1]) {
269
270
  port = parseInt(args[++i], 10);
271
+ } else if (args[i] === '--lan') {
272
+ host = '0.0.0.0';
270
273
  } else if (args[i] === '--host' && args[i + 1]) {
271
274
  host = args[++i];
272
275
  } else if (args[i] === '--log-level' && args[i + 1]) {
@@ -285,8 +288,22 @@ function parseArgs() {
285
288
  if (noTunnel) useTunnel = false;
286
289
 
287
290
  // --public requires a tunnel
288
- if (anonymousTunnel && !useTunnel) {
289
- console.error('Error: --public requires a tunnel. Remove --no-tunnel or remove --public.');
291
+ if (publicTunnel && !useTunnel) {
292
+ const rd = '\x1b[31m';
293
+ const rs = '\x1b[0m';
294
+ console.error(
295
+ `${rd}Error: --public requires a tunnel. Remove --no-tunnel or remove --public.${rs}`,
296
+ );
297
+ process.exit(1);
298
+ }
299
+
300
+ // --public requires password authentication
301
+ if (publicTunnel && !password) {
302
+ const rd = '\x1b[31m';
303
+ const rs = '\x1b[0m';
304
+ console.error(
305
+ `${rd}Error: Public tunnels require password authentication. Remove --no-password or remove --public.${rs}`,
306
+ );
290
307
  process.exit(1);
291
308
  }
292
309
 
@@ -302,7 +319,7 @@ function parseArgs() {
302
319
  password,
303
320
  useTunnel,
304
321
  persistedTunnel,
305
- anonymousTunnel,
322
+ publicTunnel,
306
323
  shell,
307
324
  shellArgs,
308
325
  cwd,
package/src/routes.js CHANGED
@@ -28,7 +28,7 @@ function setupRoutes(app, { auth, sessions, config, state }) {
28
28
  httpOnly: true,
29
29
  sameSite: 'lax',
30
30
  maxAge: 24 * 60 * 60 * 1000,
31
- secure: false,
31
+ secure: req.secure,
32
32
  });
33
33
  log.info(`Auth: login success from ${req.ip}`);
34
34
  res.json({ ok: true });
@@ -58,7 +58,7 @@ function setupRoutes(app, { auth, sessions, config, state }) {
58
58
  httpOnly: true,
59
59
  sameSite: 'lax',
60
60
  maxAge: 24 * 60 * 60 * 1000,
61
- secure: false,
61
+ secure: req.secure,
62
62
  });
63
63
  log.info(`Auth: share-token auto-login from ${req.ip}`);
64
64
  // Redirect to the same path without ?ott= to keep the URL clean
package/src/server.js CHANGED
@@ -27,10 +27,10 @@ function getLocalIP() {
27
27
  return '127.0.0.1';
28
28
  }
29
29
 
30
- function confirmAnonymousTunnel() {
30
+ function confirmPublicTunnel() {
31
31
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
32
32
  return new Promise((resolve) => {
33
- rl.question(' Do you want to continue with anonymous access? (y/N): ', (answer) => {
33
+ rl.question(' Do you want to continue with public access? (y/N): ', (answer) => {
34
34
  rl.close();
35
35
  resolve(answer.trim().toLowerCase() === 'y');
36
36
  });
@@ -110,8 +110,8 @@ function createTermBeamServer(overrides = {}) {
110
110
  }
111
111
  }
112
112
 
113
- // Warn and require consent for anonymous tunnel access
114
- if (config.useTunnel && config.anonymousTunnel) {
113
+ // Warn and require consent for public tunnel access
114
+ if (config.useTunnel && config.publicTunnel) {
115
115
  const rd = '\x1b[31m';
116
116
  const yl = '\x1b[33m';
117
117
  const rs = '\x1b[0m';
@@ -123,7 +123,7 @@ function createTermBeamServer(overrides = {}) {
123
123
  console.log(` ${yl}No Microsoft login will be required to reach the tunnel.${rs}`);
124
124
  console.log(` ${yl}Only the TermBeam password will protect your terminal.${rs}`);
125
125
  console.log('');
126
- const confirmed = await confirmAnonymousTunnel();
126
+ const confirmed = await confirmPublicTunnel();
127
127
  if (!confirmed) {
128
128
  console.log('');
129
129
  console.log(' Aborted. Restart without --public for private access.');
@@ -174,11 +174,13 @@ function createTermBeamServer(overrides = {}) {
174
174
  const gn = '\x1b[38;5;114m'; // green
175
175
  const dm = '\x1b[2m'; // dim
176
176
 
177
+ const bl = '\x1b[38;5;75m'; // light blue
178
+
177
179
  let publicUrl = null;
178
180
  if (config.useTunnel) {
179
181
  const tunnel = await startTunnel(config.port, {
180
182
  persisted: config.persistedTunnel,
181
- anonymous: config.anonymousTunnel,
183
+ anonymous: config.publicTunnel,
182
184
  });
183
185
  if (tunnel) {
184
186
  publicUrl = tunnel.url;
@@ -191,10 +193,15 @@ function createTermBeamServer(overrides = {}) {
191
193
  console.log(` Shell: ${config.shell}`);
192
194
  console.log(` Session: ${defaultId}`);
193
195
  console.log(` Auth: ${config.password ? `${gn}🔒 password${rs}` : '🔓 none'}`);
196
+ if (isLanReachable) {
197
+ console.log(` Bind: ${config.host} (LAN accessible)`);
198
+ } else {
199
+ console.log(` Bind: ${config.host} (localhost only)`);
200
+ }
194
201
  console.log('');
195
202
 
196
203
  if (publicUrl) {
197
- console.log(` 🌐 Public: ${publicUrl}`);
204
+ console.log(` Public: ${bl}${publicUrl}${rs}`);
198
205
  }
199
206
  console.log(` Local: http://localhost:${config.port}`);
200
207
  if (isLanReachable) {
@@ -205,8 +212,6 @@ function createTermBeamServer(overrides = {}) {
205
212
  const qrDisplayUrl = qrUrl; // clean URL shown in console text
206
213
  const qrCodeUrl = config.password ? `${qrUrl}?ott=${auth.generateShareToken()}` : qrUrl;
207
214
  console.log('');
208
- console.log(` ${dm}📋 Clipboard requires HTTPS — use the Public or localhost URL${rs}`);
209
- console.log('');
210
215
  try {
211
216
  const qr = await QRCode.toString(qrCodeUrl, { type: 'terminal', small: true });
212
217
  console.log(qr);
@@ -214,7 +219,7 @@ function createTermBeamServer(overrides = {}) {
214
219
  /* ignore */
215
220
  }
216
221
 
217
- console.log(` Scan the QR code or open: ${qrDisplayUrl}`);
222
+ console.log(` Scan the QR code or open: ${bl}${qrDisplayUrl}${rs}`);
218
223
  if (config.password) console.log(` Password: ${gn}${config.password}${rs}`);
219
224
  console.log('');
220
225