termbeam 1.14.5 → 1.14.6

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
@@ -109,8 +109,7 @@ flowchart LR
109
109
  | `--port <port>` | Server port | `3456` |
110
110
  | `--host <addr>` | Bind address | `127.0.0.1` |
111
111
  | `--lan` | Bind to all interfaces (LAN access) | Off |
112
- | `--public` | Public tunnel access (default) | On |
113
- | `--private` | Require Microsoft login for tunnel access | Off |
112
+ | `--public` | Allow public tunnel access (no Microsoft login) | Off |
114
113
  | `-i, --interactive` | Interactive setup wizard | Off |
115
114
  | `--log-level <level>` | Log verbosity (error/warn/info/debug) | `info` |
116
115
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termbeam",
3
- "version": "1.14.5",
3
+ "version": "1.14.6",
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": {
package/src/cli/index.js CHANGED
@@ -25,10 +25,10 @@ Options:
25
25
  --password <pw> Set access password (or TERMBEAM_PASSWORD env var)
26
26
  --generate-password Auto-generate a secure password (default: auto)
27
27
  --no-password Disable password authentication
28
- --tunnel Create a devtunnel URL (default: on, public access)
28
+ --tunnel Create a devtunnel URL (default: on, private access)
29
29
  --no-tunnel Disable tunnel (LAN-only mode)
30
30
  --persisted-tunnel Create a reusable devtunnel URL (stable across restarts)
31
- --private Require Microsoft login to access the tunnel (owner-only)
31
+ --public Allow public tunnel access (default: private, owner-only)
32
32
  --port <port> Set port (default: 3456, or PORT env var)
33
33
  --host <addr> Bind address (default: 127.0.0.1)
34
34
  --lan Bind to 0.0.0.0 (allow LAN access, default: localhost only)
@@ -40,9 +40,9 @@ Options:
40
40
 
41
41
  Defaults:
42
42
  By default, TermBeam enables tunnel + auto-generated password for secure
43
- mobile access (clipboard, HTTPS). Tunnels are public (anonymous) — TermBeam's
44
- own password auth protects your terminal. Use --private for owner-only access
45
- via Microsoft login, or --no-tunnel for LAN-only mode.
43
+ mobile access (clipboard, HTTPS). Tunnels are private (owner-only via
44
+ Microsoft login). Use --public for public access, or
45
+ --no-tunnel for LAN-only mode.
46
46
 
47
47
  Examples:
48
48
  termbeam Start with tunnel + auto password
@@ -249,7 +249,7 @@ function parseArgs() {
249
249
  let useTunnel = true;
250
250
  let noTunnel = false;
251
251
  let persistedTunnel = false;
252
- let privateTunnel = false;
252
+ let publicTunnel = false;
253
253
  let interactive = false;
254
254
  let force = false;
255
255
  let explicitPassword = !!password;
@@ -268,10 +268,8 @@ function parseArgs() {
268
268
  } else if (args[i] === '--persisted-tunnel') {
269
269
  useTunnel = true;
270
270
  persistedTunnel = true;
271
- } else if (args[i] === '--private') {
272
- privateTunnel = true;
273
271
  } else if (args[i] === '--public') {
274
- // no-op: public is now the default; kept for backwards compatibility
272
+ publicTunnel = true;
275
273
  } else if (args[i].startsWith('--password=')) {
276
274
  password = args[i].split('=')[1];
277
275
  if (!password) {
@@ -334,24 +332,24 @@ function parseArgs() {
334
332
  // --no-tunnel disables the default tunnel
335
333
  if (noTunnel) useTunnel = false;
336
334
 
337
- // --private requires a tunnel
338
- if (privateTunnel && !useTunnel) {
335
+ // --public requires a tunnel
336
+ if (publicTunnel && !useTunnel) {
339
337
  const rd = '\x1b[31m';
340
338
  const rs = '\x1b[0m';
341
- log.error('--private requires a tunnel');
339
+ log.error('--public requires a tunnel');
342
340
  console.error(
343
- `${rd}Error: --private requires a tunnel. Remove --no-tunnel or remove --private.${rs}`,
341
+ `${rd}Error: --public requires a tunnel. Remove --no-tunnel or remove --public.${rs}`,
344
342
  );
345
343
  process.exit(1);
346
344
  }
347
345
 
348
- // Public tunnels (default) require password authentication
349
- if (useTunnel && !privateTunnel && !password) {
346
+ // --public requires password authentication
347
+ if (publicTunnel && !password) {
350
348
  const rd = '\x1b[31m';
351
349
  const rs = '\x1b[0m';
352
350
  log.error('Public tunnels require password authentication');
353
351
  console.error(
354
- `${rd}Error: Public tunnels require password authentication. Remove --no-password or add --private.${rs}`,
352
+ `${rd}Error: Public tunnels require password authentication. Remove --no-password or remove --public.${rs}`,
355
353
  );
356
354
  process.exit(1);
357
355
  }
@@ -368,7 +366,7 @@ function parseArgs() {
368
366
  password,
369
367
  useTunnel,
370
368
  persistedTunnel,
371
- privateTunnel,
369
+ publicTunnel,
372
370
  shell,
373
371
  shellArgs,
374
372
  cwd,
@@ -380,7 +378,7 @@ function parseArgs() {
380
378
  };
381
379
 
382
380
  log.debug(
383
- `Config: port=${port}, host=${host}, shell=${shell}, tunnel=${useTunnel}, persisted=${persistedTunnel}, private=${privateTunnel}`,
381
+ `Config: port=${port}, host=${host}, shell=${shell}, tunnel=${useTunnel}, persisted=${persistedTunnel}, public=${publicTunnel}`,
384
382
  );
385
383
 
386
384
  return config;
@@ -58,7 +58,7 @@ async function runInteractiveSetup(baseConfig) {
58
58
  password: baseConfig.password,
59
59
  useTunnel: baseConfig.useTunnel,
60
60
  persistedTunnel: baseConfig.persistedTunnel,
61
- privateTunnel: baseConfig.privateTunnel,
61
+ publicTunnel: baseConfig.publicTunnel,
62
62
  shell: baseConfig.shell,
63
63
  shellArgs: baseConfig.shellArgs,
64
64
  cwd: baseConfig.cwd,
@@ -153,18 +153,19 @@ async function runInteractiveSetup(baseConfig) {
153
153
  showProgress(2);
154
154
  const publicChoice = await choose(rl, 'Tunnel access:', [
155
155
  {
156
- label: 'Public',
157
- hint: 'Anonymous access TermBeam password protects your terminal (recommended)',
156
+ label: 'Private (owner-only)',
157
+ hint: 'Only the Microsoft account that created the tunnel can access it',
158
158
  },
159
159
  {
160
- label: 'Private (owner-only)',
161
- hint: 'Requires Microsoft login to reach the tunnel adds extra auth layer',
160
+ label: 'Public',
161
+ hint: '🚨 No Microsoft login anyone with the URL can reach your terminal',
162
+ danger: true,
162
163
  },
163
164
  ]);
164
- config.privateTunnel = publicChoice.index === 1;
165
+ config.publicTunnel = publicChoice.index === 1;
165
166
 
166
167
  // Auto-generate password if public tunnel with no password
167
- if (!config.privateTunnel && !config.password) {
168
+ if (config.publicTunnel && !config.password) {
168
169
  console.log(yellow(' ⚠ Public tunnels require password authentication.'));
169
170
  config.password = crypto.randomBytes(16).toString('base64url');
170
171
  process.stdout.write(dim(` Auto-generated password: ${config.password}`) + '\n');
@@ -177,22 +178,22 @@ async function runInteractiveSetup(baseConfig) {
177
178
  config.host = '0.0.0.0';
178
179
  config.useTunnel = false;
179
180
  config.persistedTunnel = false;
180
- config.privateTunnel = false;
181
+ config.publicTunnel = false;
181
182
  } else {
182
183
  // Localhost only
183
184
  config.host = '127.0.0.1';
184
185
  config.useTunnel = false;
185
186
  config.persistedTunnel = false;
186
- config.privateTunnel = false;
187
+ config.publicTunnel = false;
187
188
  }
188
189
 
189
190
  const accessLabel = !config.useTunnel
190
191
  ? config.host === '0.0.0.0'
191
192
  ? 'LAN (0.0.0.0)'
192
193
  : 'Localhost only'
193
- : config.privateTunnel
194
- ? 'DevTunnel (private)'
195
- : 'DevTunnel (public)';
194
+ : config.publicTunnel
195
+ ? 'DevTunnel (public)'
196
+ : 'DevTunnel (private)';
196
197
  log.debug(`Access mode selected: ${accessLabel}`);
197
198
  decisions.push({ label: 'Access', value: accessLabel });
198
199
 
@@ -225,7 +226,7 @@ async function runInteractiveSetup(baseConfig) {
225
226
  console.log(` Tunnel: ${config.useTunnel ? cyan('enabled') : yellow('disabled')}`);
226
227
  if (config.useTunnel) {
227
228
  console.log(` Persisted: ${config.persistedTunnel ? cyan('yes') : dim('no')}`);
228
- console.log(` Public: ${config.privateTunnel ? dim('no (private)') : cyan('yes')}`);
229
+ console.log(` Public: ${config.publicTunnel ? yellow('yes') : dim('no')}`);
229
230
  }
230
231
  console.log(` Shell: ${cyan(config.shell || 'default')}`);
231
232
  console.log(` Directory: ${cyan(config.cwd)}`);
@@ -246,7 +247,7 @@ async function runInteractiveSetup(baseConfig) {
246
247
  if (config.host === '0.0.0.0') cmdParts.push('--lan');
247
248
  } else {
248
249
  if (config.persistedTunnel) cmdParts.push('--persisted-tunnel');
249
- if (config.privateTunnel) cmdParts.push('--private');
250
+ if (config.publicTunnel) cmdParts.push('--public');
250
251
  }
251
252
  if (config.logLevel !== 'info') cmdParts.push('--log-level', config.logLevel);
252
253
  const cliCommand = cmdParts.join(' ');
@@ -80,8 +80,8 @@ function buildArgs(config) {
80
80
  if (config.persistedTunnel) {
81
81
  args.push('--persisted-tunnel');
82
82
  }
83
- if (config.privateTunnel) {
84
- args.push('--private');
83
+ if (config.publicTunnel) {
84
+ args.push('--public');
85
85
  }
86
86
  if (config.logLevel && config.logLevel !== 'info') {
87
87
  args.push('--log-level', config.logLevel);
@@ -290,17 +290,18 @@ async function actionInstall() {
290
290
  // Re-render step to clear the previous menu before showing sub-question
291
291
  showProgress(3);
292
292
  const publicChoice = await choose(rl, 'Tunnel access:', [
293
- {
294
- label: 'Public (anyone with the link)',
295
- hint: 'Anonymous access — TermBeam password protects your terminal (recommended)',
296
- },
297
293
  {
298
294
  label: 'Private (requires Microsoft login)',
299
295
  hint: 'Only you can access the tunnel — secured via your Microsoft account',
300
296
  },
297
+ {
298
+ label: 'Public (anyone with the link)',
299
+ hint: '🚨 Anyone with the URL can reach your terminal — password is the only protection',
300
+ danger: true,
301
+ },
301
302
  ]);
302
- config.privateTunnel = publicChoice.index === 1;
303
- if (!config.privateTunnel && config.password === false) {
303
+ config.publicTunnel = publicChoice.index === 1;
304
+ if (config.publicTunnel && config.password === false) {
304
305
  console.log(yellow(' ⚠ Public tunnels require password authentication.'));
305
306
  config.password = crypto.randomBytes(16).toString('base64url');
306
307
  process.stdout.write(dim(` Auto-generated password: ${config.password}`) + '\n');
@@ -318,9 +319,9 @@ async function actionInstall() {
318
319
  ? config.lan
319
320
  ? 'LAN (0.0.0.0)'
320
321
  : 'Localhost only'
321
- : config.privateTunnel
322
- ? 'DevTunnel (private)'
323
- : 'DevTunnel (public)';
322
+ : config.publicTunnel
323
+ ? 'DevTunnel (public)'
324
+ : 'DevTunnel (private)';
324
325
  decisions.push({ label: 'Access', value: accessLabel });
325
326
 
326
327
  // Working directory
@@ -367,7 +368,7 @@ async function actionInstall() {
367
368
  console.log(` Tunnel: ${config.noTunnel ? yellow('disabled') : cyan('enabled')}`);
368
369
  if (!config.noTunnel) {
369
370
  console.log(` Persisted: ${config.persistedTunnel ? cyan('yes') : dim('no')}`);
370
- console.log(` Public: ${config.privateTunnel ? yellow('no (private)') : dim('yes')}`);
371
+ console.log(` Public: ${config.publicTunnel ? yellow('yes') : dim('no')}`);
371
372
  }
372
373
  console.log(` Directory: ${cyan(config.cwd)}`);
373
374
  console.log(` Shell: ${cyan(config.shell || 'default')}`);
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  const os = require('os');
3
3
  const path = require('path');
4
+ const readline = require('readline');
4
5
  const express = require('express');
5
6
  const cookieParser = require('cookie-parser');
6
7
  const http = require('http');
@@ -17,6 +18,7 @@ const { createPreviewProxy } = require('./preview');
17
18
  const { writeConnectionConfig, removeConnectionConfig } = require('../cli/resume');
18
19
  const { checkForUpdate, detectInstallMethod } = require('../utils/update-check');
19
20
 
21
+ // --- Helpers ---
20
22
  function getLocalIP() {
21
23
  const interfaces = os.networkInterfaces();
22
24
  for (const name of Object.keys(interfaces)) {
@@ -27,6 +29,16 @@ function getLocalIP() {
27
29
  return '127.0.0.1';
28
30
  }
29
31
 
32
+ function confirmPublicTunnel() {
33
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
34
+ return new Promise((resolve) => {
35
+ rl.question(' Do you want to continue with public access? (y/N): ', (answer) => {
36
+ rl.close();
37
+ resolve(answer.trim().toLowerCase() === 'y');
38
+ });
39
+ });
40
+ }
41
+
30
42
  /**
31
43
  * Create a TermBeam server instance without starting it.
32
44
  * @param {object} [overrides] - Optional overrides
@@ -128,13 +140,26 @@ function createTermBeamServer(overrides = {}) {
128
140
  }
129
141
  }
130
142
 
131
- // Warn about private tunnel access when --private flag is used
132
- if (config.useTunnel && config.privateTunnel) {
143
+ // Warn and require consent for public tunnel access
144
+ if (config.useTunnel && config.publicTunnel) {
145
+ const rd = '\x1b[31m';
133
146
  const yl = '\x1b[33m';
134
147
  const rs = '\x1b[0m';
148
+ const bd = '\x1b[1m';
149
+ console.log('');
150
+ console.log(` ${rd}${bd}⚠️ DANGER: Public tunnel access requested${rs}`);
135
151
  console.log('');
136
- console.log(` ${yl}ℹ️ Private tunnel: Microsoft login required to access the tunnel.${rs}`);
152
+ console.log(` ${yl}This will make your terminal accessible to ANYONE with the URL.${rs}`);
153
+ console.log(` ${yl}No Microsoft login will be required to reach the tunnel.${rs}`);
154
+ console.log(` ${yl}Only the TermBeam password will protect your terminal.${rs}`);
137
155
  console.log('');
156
+ const confirmed = await confirmPublicTunnel();
157
+ if (!confirmed) {
158
+ console.log('');
159
+ console.log(' Aborted. Restart without --public for private access.');
160
+ console.log('');
161
+ process.exit(1);
162
+ }
138
163
  }
139
164
 
140
165
  return new Promise((resolve) => {
@@ -204,7 +229,7 @@ function createTermBeamServer(overrides = {}) {
204
229
  if (config.useTunnel) {
205
230
  const tunnel = await startTunnel(actualPort, {
206
231
  persisted: config.persistedTunnel,
207
- private: config.privateTunnel,
232
+ anonymous: config.publicTunnel,
208
233
  });
209
234
  if (tunnel) {
210
235
  publicUrl = tunnel.url;
@@ -234,16 +234,8 @@ async function startTunnel(port, options = {}) {
234
234
  { stdio: 'pipe' },
235
235
  );
236
236
  } catch {}
237
- // Set tunnel access: public (anonymous, default) or private (owner-only via Microsoft login)
238
- if (options.private) {
239
- // Remove any existing anonymous access to ensure the tunnel is private
240
- try {
241
- execFileSync(devtunnelCmd, ['access', 'reset', tunnelId], {
242
- stdio: 'pipe',
243
- });
244
- } catch {}
245
- log.info('Tunnel access: private (owner-only via Microsoft login)');
246
- } else {
237
+ // Set tunnel access: public (anonymous) or private (owner-only via Microsoft login)
238
+ if (options.anonymous) {
247
239
  try {
248
240
  execFileSync(
249
241
  devtunnelCmd,
@@ -252,6 +244,14 @@ async function startTunnel(port, options = {}) {
252
244
  );
253
245
  } catch {}
254
246
  log.info('Tunnel access: public (anonymous)');
247
+ } else {
248
+ // Remove any existing anonymous access to ensure the tunnel is private
249
+ try {
250
+ execFileSync(devtunnelCmd, ['access', 'reset', tunnelId], {
251
+ stdio: 'pipe',
252
+ });
253
+ } catch {}
254
+ log.info('Tunnel access: private (owner-only via Microsoft login)');
255
255
  }
256
256
 
257
257
  const hostProc = spawn(devtunnelCmd, ['host', tunnelId], {