termbeam 1.2.7 → 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,7 +104,8 @@ 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 |
@@ -117,14 +118,15 @@ termbeam --host 127.0.0.1 # restrict to localhost (default: 0.0.0.0)
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.7",
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';
@@ -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]) {
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
@@ -174,6 +174,8 @@ 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, {
@@ -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