Skip to content

Commit 087981a

Browse files
dorlugasigalCopilot
andcommitted
fix(routes): resolve static file 404 when installed via npx
The send module treats dotfile path segments (e.g. .npm in the npx cache path) as hidden and returns 404. Use res.sendFile with root option instead of absolute paths to avoid dotfile detection. Also improve tunnel login error handling with clear user-facing guidance when devtunnel login fails. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3d4086f commit 087981a

2 files changed

Lines changed: 62 additions & 22 deletions

File tree

src/routes.js

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ function setupRoutes(app, { auth, sessions, config }) {
4545
});
4646

4747
// Pages
48-
app.get('/', auth.middleware, (_req, res) => res.sendFile(path.join(PUBLIC_DIR, 'index.html')));
48+
app.get('/', auth.middleware, (_req, res) => res.sendFile('index.html', { root: PUBLIC_DIR }));
4949
app.get('/terminal', auth.middleware, (_req, res) =>
50-
res.sendFile(path.join(PUBLIC_DIR, 'terminal.html')),
50+
res.sendFile('terminal.html', { root: PUBLIC_DIR }),
5151
);
5252

5353
// Session API
@@ -61,7 +61,7 @@ function setupRoutes(app, { auth, sessions, config }) {
6161
// Validate shell field
6262
if (shell) {
6363
const availableShells = detectShells();
64-
const isValid = availableShells.some(s => s.path === shell || s.cmd === shell);
64+
const isValid = availableShells.some((s) => s.path === shell || s.cmd === shell);
6565
if (!isValid) {
6666
return res.status(400).json({ error: 'Invalid shell' });
6767
}
@@ -150,13 +150,14 @@ function setupRoutes(app, { auth, sessions, config }) {
150150
if (!buffer.length) {
151151
return res.status(400).json({ error: 'No image data' });
152152
}
153-
const ext = {
154-
'image/png': '.png',
155-
'image/jpeg': '.jpg',
156-
'image/gif': '.gif',
157-
'image/webp': '.webp',
158-
'image/bmp': '.bmp',
159-
}[contentType] || '.png';
153+
const ext =
154+
{
155+
'image/png': '.png',
156+
'image/jpeg': '.jpg',
157+
'image/gif': '.gif',
158+
'image/webp': '.webp',
159+
'image/bmp': '.bmp',
160+
}[contentType] || '.png';
160161
const filename = `termbeam-${crypto.randomUUID()}${ext}`;
161162
const filepath = path.join(os.tmpdir(), filename);
162163
fs.writeFileSync(filepath, buffer);
@@ -173,7 +174,7 @@ function setupRoutes(app, { auth, sessions, config }) {
173174

174175
// Directory listing for folder browser
175176
app.get('/api/dirs', auth.middleware, (req, res) => {
176-
const query = req.query.q || (config.cwd + path.sep);
177+
const query = req.query.q || config.cwd + path.sep;
177178
const endsWithSep = query.endsWith('/') || query.endsWith('\\');
178179
const dir = endsWithSep ? query : path.dirname(query);
179180
const prefix = endsWithSep ? '' : path.basename(query);

src/tunnel.js

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ function loadPersistedTunnel() {
4747

4848
function savePersistedTunnel(id) {
4949
fs.mkdirSync(TUNNEL_CONFIG_DIR, { recursive: true });
50-
fs.writeFileSync(TUNNEL_CONFIG_PATH, JSON.stringify({ tunnelId: id, createdAt: new Date().toISOString() }, null, 2));
50+
fs.writeFileSync(
51+
TUNNEL_CONFIG_PATH,
52+
JSON.stringify({ tunnelId: id, createdAt: new Date().toISOString() }, null, 2),
53+
);
5154
}
5255

5356
function deletePersisted() {
@@ -68,7 +71,10 @@ function deletePersisted() {
6871
function isTunnelValid(id) {
6972
try {
7073
if (!SAFE_ID_RE.test(id)) return false;
71-
execFileSync(devtunnelCmd, ['show', id, '--json'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
74+
execFileSync(devtunnelCmd, ['show', id, '--json'], {
75+
encoding: 'utf-8',
76+
stdio: ['pipe', 'pipe', 'pipe'],
77+
});
7278
return true;
7379
} catch {
7480
return false;
@@ -87,7 +93,9 @@ async function startTunnel(port, options = {}) {
8793
log.error('');
8894
log.error(' Install it:');
8995
log.error(' Windows: winget install Microsoft.devtunnel');
90-
log.error(' or: Invoke-WebRequest -Uri https://aka.ms/TunnelsCliDownload/win-x64 -OutFile devtunnel.exe');
96+
log.error(
97+
' or: Invoke-WebRequest -Uri https://aka.ms/TunnelsCliDownload/win-x64 -OutFile devtunnel.exe',
98+
);
9199
log.error(' macOS: brew install --cask devtunnel');
92100
log.error(' Linux: curl -sL https://aka.ms/DevTunnelCliInstall | bash');
93101
log.error('');
@@ -103,15 +111,29 @@ async function startTunnel(port, options = {}) {
103111
// Ensure user is logged in
104112
let loggedIn = false;
105113
try {
106-
const userOut = execFileSync(devtunnelCmd, ['user', 'show'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
114+
const userOut = execFileSync(devtunnelCmd, ['user', 'show'], {
115+
encoding: 'utf-8',
116+
stdio: ['pipe', 'pipe', 'pipe'],
117+
});
107118
// user show can succeed but show "not logged in" status
108119
loggedIn = userOut && !userOut.toLowerCase().includes('not logged in');
109120
} catch {}
110121

111122
if (!loggedIn) {
112123
log.info('devtunnel not logged in, launching login...');
113124
log.info('A browser window will open for authentication.');
114-
execFileSync(devtunnelCmd, ['user', 'login'], { stdio: 'inherit' });
125+
try {
126+
execFileSync(devtunnelCmd, ['user', 'login'], { stdio: 'inherit' });
127+
} catch (loginErr) {
128+
log.error('');
129+
log.error(' DevTunnel login failed. To use tunnels, run:');
130+
log.error(' devtunnel user login');
131+
log.error('');
132+
log.error(' Or start without a tunnel:');
133+
log.error(' termbeam --no-tunnel');
134+
log.error('');
135+
return null;
136+
}
115137
}
116138

117139
const persisted = options.persisted;
@@ -130,7 +152,9 @@ async function startTunnel(port, options = {}) {
130152
if (saved) {
131153
log.info('Persisted tunnel expired, creating new one');
132154
}
133-
const createOut = execFileSync(devtunnelCmd, ['create', '--expiration', '30d', '--json'], { encoding: 'utf-8' });
155+
const createOut = execFileSync(devtunnelCmd, ['create', '--expiration', '30d', '--json'], {
156+
encoding: 'utf-8',
157+
});
134158
const tunnelData = JSON.parse(createOut);
135159
tunnelId = tunnelData.tunnel.tunnelId;
136160
savePersistedTunnel(tunnelId);
@@ -140,18 +164,28 @@ async function startTunnel(port, options = {}) {
140164
tunnelMode = 'ephemeral';
141165
tunnelExpiry = '1 day';
142166
// Ephemeral tunnel — create fresh, will be deleted on shutdown
143-
const createOut = execFileSync(devtunnelCmd, ['create', '--expiration', '1d', '--json'], { encoding: 'utf-8' });
167+
const createOut = execFileSync(devtunnelCmd, ['create', '--expiration', '1d', '--json'], {
168+
encoding: 'utf-8',
169+
});
144170
const tunnelData = JSON.parse(createOut);
145171
tunnelId = tunnelData.tunnel.tunnelId;
146172
log.info(`Created ephemeral tunnel ${tunnelId}`);
147173
}
148174

149175
// Idempotent port and access setup
150176
try {
151-
execFileSync(devtunnelCmd, ['port', 'create', tunnelId, '-p', String(port), '--protocol', 'http'], { stdio: 'pipe' });
177+
execFileSync(
178+
devtunnelCmd,
179+
['port', 'create', tunnelId, '-p', String(port), '--protocol', 'http'],
180+
{ stdio: 'pipe' },
181+
);
152182
} catch {}
153183
try {
154-
execFileSync(devtunnelCmd, ['access', 'create', tunnelId, '-p', String(port), '--anonymous'], { stdio: 'pipe' });
184+
execFileSync(
185+
devtunnelCmd,
186+
['access', 'create', tunnelId, '-p', String(port), '--anonymous'],
187+
{ stdio: 'pipe' },
188+
);
155189
} catch {}
156190

157191
const hostProc = spawn(devtunnelCmd, ['host', tunnelId], {
@@ -193,8 +227,13 @@ function cleanupTunnel() {
193227
// On Windows, kill the process tree to ensure all children die
194228
if (process.platform === 'win32' && tunnelProc.pid) {
195229
try {
196-
execFileSync('taskkill', ['/pid', String(tunnelProc.pid), '/T', '/F'], { stdio: 'pipe', timeout: 5000 });
197-
} catch { /* best effort */ }
230+
execFileSync('taskkill', ['/pid', String(tunnelProc.pid), '/T', '/F'], {
231+
stdio: 'pipe',
232+
timeout: 5000,
233+
});
234+
} catch {
235+
/* best effort */
236+
}
198237
} else {
199238
tunnelProc.kill('SIGKILL');
200239
}

0 commit comments

Comments
 (0)