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 +1 -2
- package/package.json +1 -1
- package/src/cli/index.js +16 -18
- package/src/cli/interactive.js +15 -14
- package/src/cli/service.js +13 -12
- package/src/server/index.js +29 -4
- package/src/tunnel/index.js +10 -10
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` |
|
|
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
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,
|
|
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
|
-
--
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
// --
|
|
338
|
-
if (
|
|
335
|
+
// --public requires a tunnel
|
|
336
|
+
if (publicTunnel && !useTunnel) {
|
|
339
337
|
const rd = '\x1b[31m';
|
|
340
338
|
const rs = '\x1b[0m';
|
|
341
|
-
log.error('--
|
|
339
|
+
log.error('--public requires a tunnel');
|
|
342
340
|
console.error(
|
|
343
|
-
`${rd}Error: --
|
|
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
|
-
//
|
|
349
|
-
if (
|
|
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
|
|
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
|
-
|
|
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},
|
|
381
|
+
`Config: port=${port}, host=${host}, shell=${shell}, tunnel=${useTunnel}, persisted=${persistedTunnel}, public=${publicTunnel}`,
|
|
384
382
|
);
|
|
385
383
|
|
|
386
384
|
return config;
|
package/src/cli/interactive.js
CHANGED
|
@@ -58,7 +58,7 @@ async function runInteractiveSetup(baseConfig) {
|
|
|
58
58
|
password: baseConfig.password,
|
|
59
59
|
useTunnel: baseConfig.useTunnel,
|
|
60
60
|
persistedTunnel: baseConfig.persistedTunnel,
|
|
61
|
-
|
|
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: '
|
|
157
|
-
hint: '
|
|
156
|
+
label: 'Private (owner-only)',
|
|
157
|
+
hint: 'Only the Microsoft account that created the tunnel can access it',
|
|
158
158
|
},
|
|
159
159
|
{
|
|
160
|
-
label: '
|
|
161
|
-
hint: '
|
|
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.
|
|
165
|
+
config.publicTunnel = publicChoice.index === 1;
|
|
165
166
|
|
|
166
167
|
// Auto-generate password if public tunnel with no password
|
|
167
|
-
if (
|
|
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.
|
|
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.
|
|
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.
|
|
194
|
-
? 'DevTunnel (
|
|
195
|
-
: 'DevTunnel (
|
|
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.
|
|
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.
|
|
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(' ');
|
package/src/cli/service.js
CHANGED
|
@@ -80,8 +80,8 @@ function buildArgs(config) {
|
|
|
80
80
|
if (config.persistedTunnel) {
|
|
81
81
|
args.push('--persisted-tunnel');
|
|
82
82
|
}
|
|
83
|
-
if (config.
|
|
84
|
-
args.push('--
|
|
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.
|
|
303
|
-
if (
|
|
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.
|
|
322
|
-
? 'DevTunnel (
|
|
323
|
-
: 'DevTunnel (
|
|
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.
|
|
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')}`);
|
package/src/server/index.js
CHANGED
|
@@ -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
|
|
132
|
-
if (config.useTunnel && config.
|
|
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}
|
|
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
|
-
|
|
232
|
+
anonymous: config.publicTunnel,
|
|
208
233
|
});
|
|
209
234
|
if (tunnel) {
|
|
210
235
|
publicUrl = tunnel.url;
|
package/src/tunnel/index.js
CHANGED
|
@@ -234,16 +234,8 @@ async function startTunnel(port, options = {}) {
|
|
|
234
234
|
{ stdio: 'pipe' },
|
|
235
235
|
);
|
|
236
236
|
} catch {}
|
|
237
|
-
// Set tunnel access: public (anonymous
|
|
238
|
-
if (options.
|
|
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], {
|