termbeam 1.17.7 → 1.18.0
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/package.json +1 -1
- package/public/assets/{_basePickBy-COFR_F6O.js → _basePickBy-CVn0rIeA.js} +1 -1
- package/public/assets/{_baseUniq-DsM_tAIh.js → _baseUniq-D-ViDZI1.js} +1 -1
- package/public/assets/{arc-CkjHihPP.js → arc-BWX7iih_.js} +1 -1
- package/public/assets/{architectureDiagram-2XIMDMQ5-DF0zcOrz.js → architectureDiagram-2XIMDMQ5-Dr3bqfT2.js} +1 -1
- package/public/assets/{blockDiagram-WCTKOSBZ-1ZGwRrfn.js → blockDiagram-WCTKOSBZ-RZQh_7bp.js} +1 -1
- package/public/assets/{c4Diagram-IC4MRINW-CtFQssnA.js → c4Diagram-IC4MRINW-BM1jg1mo.js} +1 -1
- package/public/assets/channel-N7LACYfb.js +1 -0
- package/public/assets/{chunk-4BX2VUAB-pAQlkdCs.js → chunk-4BX2VUAB-Cz0Sexay.js} +1 -1
- package/public/assets/{chunk-55IACEB6-C1YDnAkF.js → chunk-55IACEB6-DhjD4VKO.js} +1 -1
- package/public/assets/{chunk-FMBD7UC4-0fJhTPpT.js → chunk-FMBD7UC4-H_1l13Rz.js} +1 -1
- package/public/assets/{chunk-JSJVCQXG-Bv4lt_uR.js → chunk-JSJVCQXG-D_GdzPQn.js} +1 -1
- package/public/assets/{chunk-KX2RTZJC-DSuNgcxs.js → chunk-KX2RTZJC-BU-NTTwY.js} +1 -1
- package/public/assets/{chunk-NQ4KR5QH-B8qhuc2K.js → chunk-NQ4KR5QH-_X3IQmYu.js} +1 -1
- package/public/assets/{chunk-QZHKN3VN-CLJObXzb.js → chunk-QZHKN3VN-CrSItx1v.js} +1 -1
- package/public/assets/{chunk-WL4C6EOR-Cty7zCuA.js → chunk-WL4C6EOR-XtpH4KKr.js} +1 -1
- package/public/assets/classDiagram-VBA2DB6C-Ct_F3N6M.js +1 -0
- package/public/assets/classDiagram-v2-RAHNMMFH-Ct_F3N6M.js +1 -0
- package/public/assets/clone-DfaqXUXL.js +1 -0
- package/public/assets/{cose-bilkent-S5V4N54A-BCbps77Y.js → cose-bilkent-S5V4N54A-BNYNzK1p.js} +1 -1
- package/public/assets/{dagre-KLK3FWXG-BAFtP8kn.js → dagre-KLK3FWXG-CvP5MaIU.js} +1 -1
- package/public/assets/{diagram-E7M64L7V-D96MX7Ne.js → diagram-E7M64L7V-C1ypW2kT.js} +1 -1
- package/public/assets/{diagram-IFDJBPK2-CWijQtVE.js → diagram-IFDJBPK2-CH5mcu6V.js} +1 -1
- package/public/assets/{diagram-P4PSJMXO-DfaT5SNw.js → diagram-P4PSJMXO-D7EqFseu.js} +1 -1
- package/public/assets/{erDiagram-INFDFZHY-hBbhb86C.js → erDiagram-INFDFZHY-Dc_91GQC.js} +1 -1
- package/public/assets/{flowDiagram-PKNHOUZH-DG_7KLqz.js → flowDiagram-PKNHOUZH-wUw5Mjvb.js} +1 -1
- package/public/assets/{ganttDiagram-A5KZAMGK-CgjVNpj-.js → ganttDiagram-A5KZAMGK-C55hiPqb.js} +1 -1
- package/public/assets/{gitGraphDiagram-K3NZZRJ6-7K4KlJrp.js → gitGraphDiagram-K3NZZRJ6-D3HBxR11.js} +1 -1
- package/public/assets/{graph-BeJIbeQ_.js → graph-mq5DkKwp.js} +1 -1
- package/public/assets/{index-BKVhZe-L.js → index-BxVq7AYs.js} +118 -118
- package/public/assets/index-Cpm34cTy.css +32 -0
- package/public/assets/{infoDiagram-LFFYTUFH-BpW2L8Ym.js → infoDiagram-LFFYTUFH-Cisyfr_w.js} +1 -1
- package/public/assets/{ishikawaDiagram-PHBUUO56-DNQ-67go.js → ishikawaDiagram-PHBUUO56-BjaKmPoV.js} +1 -1
- package/public/assets/{journeyDiagram-4ABVD52K-DBLcMkFz.js → journeyDiagram-4ABVD52K-BNP3C6Ph.js} +1 -1
- package/public/assets/{kanban-definition-K7BYSVSG-CKEe0d57.js → kanban-definition-K7BYSVSG-D9TVNfL2.js} +1 -1
- package/public/assets/{layout-CPrQCiWo.js → layout-DIfi_3Cg.js} +1 -1
- package/public/assets/{linear-Bvp8GGTr.js → linear-CNFortHj.js} +1 -1
- package/public/assets/{mindmap-definition-YRQLILUH-Dh6H4qCD.js → mindmap-definition-YRQLILUH-CfNe7W8X.js} +1 -1
- package/public/assets/{pieDiagram-SKSYHLDU-Bhm_CLJm.js → pieDiagram-SKSYHLDU-C3T1FeFD.js} +1 -1
- package/public/assets/{quadrantDiagram-337W2JSQ-BPzwxRlL.js → quadrantDiagram-337W2JSQ-Dcj0avLR.js} +1 -1
- package/public/assets/{requirementDiagram-Z7DCOOCP-CgbtPhM8.js → requirementDiagram-Z7DCOOCP-D4yTZU0p.js} +1 -1
- package/public/assets/{sankeyDiagram-WA2Y5GQK-2IIigF9J.js → sankeyDiagram-WA2Y5GQK-CpT1bDmZ.js} +1 -1
- package/public/assets/{sequenceDiagram-2WXFIKYE-C20cCdgm.js → sequenceDiagram-2WXFIKYE-CPsVjHHb.js} +1 -1
- package/public/assets/{stateDiagram-RAJIS63D-CGcduF1R.js → stateDiagram-RAJIS63D-CbB0QqSa.js} +1 -1
- package/public/assets/stateDiagram-v2-FVOUBMTO-CQnYvVz1.js +1 -0
- package/public/assets/{timeline-definition-YZTLITO2-Ck1-gAxD.js → timeline-definition-YZTLITO2-Bi12ioU0.js} +1 -1
- package/public/assets/{treemap-KZPCXAKY-CpfBOaD_.js → treemap-KZPCXAKY-Ct-pligZ.js} +1 -1
- package/public/assets/{vennDiagram-LZ73GAT5-DXJlsrKL.js → vennDiagram-LZ73GAT5-Ci8AKqD1.js} +1 -1
- package/public/assets/{xychartDiagram-JWTSCODW-BFlTfqgI.js → xychartDiagram-JWTSCODW-BsOGuDAL.js} +1 -1
- package/public/index.html +2 -2
- package/public/sw.js +1 -1
- package/src/server/index.js +85 -13
- package/src/server/routes.js +77 -0
- package/src/tunnel/index.js +202 -55
- package/public/assets/channel-Pj6K0-7n.js +0 -1
- package/public/assets/classDiagram-VBA2DB6C-CEuggsOe.js +0 -1
- package/public/assets/classDiagram-v2-RAHNMMFH-CEuggsOe.js +0 -1
- package/public/assets/clone-D5ZwbJNp.js +0 -1
- package/public/assets/index-OLhvO-lo.css +0 -32
- package/public/assets/stateDiagram-v2-FVOUBMTO-BBxXMU25.js +0 -1
package/src/server/routes.js
CHANGED
|
@@ -977,6 +977,83 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager }) {
|
|
|
977
977
|
res.json({ ok: true });
|
|
978
978
|
});
|
|
979
979
|
}
|
|
980
|
+
|
|
981
|
+
// --- Tunnel token renewal ---
|
|
982
|
+
app.get('/api/tunnel/status', apiRateLimit, auth.middleware, (_req, res) => {
|
|
983
|
+
const tunnelStatus = state.tunnelStatus || { state: 'unknown' };
|
|
984
|
+
// Injected via state to avoid loading the full tunnel module in test contexts
|
|
985
|
+
const getLoginInfo = state.getLoginInfo;
|
|
986
|
+
const loginInfo = getLoginInfo ? getLoginInfo() : null;
|
|
987
|
+
res.json({
|
|
988
|
+
...tunnelStatus,
|
|
989
|
+
provider: loginInfo?.provider ?? null,
|
|
990
|
+
tokenLifetimeSeconds: loginInfo?.tokenLifetimeSeconds ?? null,
|
|
991
|
+
});
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
app.post('/api/tunnel/renew', apiRateLimit, auth.middleware, (_req, res) => {
|
|
995
|
+
const { spawn } = require('child_process');
|
|
996
|
+
const { findDevtunnel } = require('../tunnel');
|
|
997
|
+
const cmd = findDevtunnel() || 'devtunnel';
|
|
998
|
+
const proc = spawn(cmd, ['user', 'login', '-d'], {
|
|
999
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
let output = '';
|
|
1003
|
+
let responded = false;
|
|
1004
|
+
|
|
1005
|
+
const timeout = setTimeout(() => {
|
|
1006
|
+
if (!responded) {
|
|
1007
|
+
responded = true;
|
|
1008
|
+
proc.kill();
|
|
1009
|
+
res.status(504).json({ error: 'Timed out waiting for device code' });
|
|
1010
|
+
}
|
|
1011
|
+
}, 15000);
|
|
1012
|
+
|
|
1013
|
+
function tryParse(data) {
|
|
1014
|
+
if (responded || output.length > 10_000) return;
|
|
1015
|
+
output += data;
|
|
1016
|
+
// Entra: "open the page https://... and enter the code ABC123 to authenticate"
|
|
1017
|
+
// GitHub: "Browse to https://... and enter the code: AB12-CD34"
|
|
1018
|
+
const match =
|
|
1019
|
+
output.match(/open the page (https:\/\/[^\s]+) and enter the code ([A-Z0-9]+)/i) ||
|
|
1020
|
+
output.match(/Browse to (https:\/\/[^\s]+) and enter the code:?\s*([A-Z0-9-]+)/i);
|
|
1021
|
+
if (match) {
|
|
1022
|
+
responded = true;
|
|
1023
|
+
clearTimeout(timeout);
|
|
1024
|
+
// Stop reading output — we have what we need
|
|
1025
|
+
proc.stdout.removeAllListeners('data');
|
|
1026
|
+
proc.stderr.removeAllListeners('data');
|
|
1027
|
+
res.json({
|
|
1028
|
+
url: match[1],
|
|
1029
|
+
code: match[2],
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
proc.stdout.on('data', (d) => tryParse(d.toString()));
|
|
1035
|
+
proc.stderr.on('data', (d) => tryParse(d.toString()));
|
|
1036
|
+
|
|
1037
|
+
proc.on('close', (code) => {
|
|
1038
|
+
clearTimeout(timeout);
|
|
1039
|
+
if (!responded) {
|
|
1040
|
+
responded = true;
|
|
1041
|
+
if (code === 0) {
|
|
1042
|
+
res.json({ ok: true, message: 'Already authenticated' });
|
|
1043
|
+
} else {
|
|
1044
|
+
res.status(500).json({ error: 'DevTunnel login failed' });
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
proc.on('error', (err) => {
|
|
1050
|
+
clearTimeout(timeout);
|
|
1051
|
+
if (!responded) {
|
|
1052
|
+
responded = true;
|
|
1053
|
+
res.status(500).json({ error: err.message });
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
980
1057
|
}
|
|
981
1058
|
|
|
982
1059
|
function cleanupUploadedFiles() {
|
package/src/tunnel/index.js
CHANGED
|
@@ -21,19 +21,77 @@ let restartAttempts = 0;
|
|
|
21
21
|
let isRestarting = false;
|
|
22
22
|
let restartTimer = null;
|
|
23
23
|
|
|
24
|
+
// --- Auth-wait state ---
|
|
25
|
+
let waitingForAuth = false;
|
|
26
|
+
let authCheckInterval = null;
|
|
27
|
+
let expiryWarned = false;
|
|
28
|
+
|
|
24
29
|
const HEALTH_CHECK_INTERVAL = 30_000; // 30s between checks
|
|
25
30
|
const HEALTH_CHECK_GRACE = 2; // 2 consecutive failures before restart
|
|
26
31
|
const MAX_RESTART_ATTEMPTS = 10;
|
|
27
32
|
const BACKOFF_DELAYS = [1000, 2000, 5000, 10_000, 15_000, 30_000]; // then stays at 30s
|
|
33
|
+
const AUTH_CHECK_INTERVAL = 30_000; // 30s between auth re-checks
|
|
34
|
+
const TOKEN_EXPIRY_WARN_SECONDS = 3600; // warn at 1 hour remaining
|
|
35
|
+
|
|
36
|
+
const AUTH_ERROR_PATTERNS = ['login required', 'not logged in', 'sign in required'];
|
|
28
37
|
|
|
29
38
|
const SAFE_ID_RE = /^[a-zA-Z0-9._-]+$/;
|
|
30
39
|
|
|
31
40
|
const DEVICE_CODE_INITIAL_TIMEOUT = 15000;
|
|
32
41
|
const DEVICE_CODE_AUTH_TIMEOUT = 120000;
|
|
33
42
|
|
|
43
|
+
function isAuthError(message) {
|
|
44
|
+
const lower = (message || '').toLowerCase();
|
|
45
|
+
return AUTH_ERROR_PATTERNS.some((p) => lower.includes(p));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isLoggedIn() {
|
|
49
|
+
try {
|
|
50
|
+
const out = execFileSync(devtunnelCmd, ['user', 'show'], {
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
53
|
+
timeout: 10_000,
|
|
54
|
+
});
|
|
55
|
+
return out && !out.toLowerCase().includes('not logged in');
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getLoginInfo() {
|
|
62
|
+
try {
|
|
63
|
+
const out = execFileSync(devtunnelCmd, ['user', 'show', '-v'], {
|
|
64
|
+
encoding: 'utf-8',
|
|
65
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
66
|
+
timeout: 10_000,
|
|
67
|
+
});
|
|
68
|
+
return parseLoginInfo(out);
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseLoginInfo(output) {
|
|
75
|
+
if (!output || output.toLowerCase().includes('not logged in')) return null;
|
|
76
|
+
|
|
77
|
+
let provider = 'unknown';
|
|
78
|
+
if (output.toLowerCase().includes('github')) provider = 'github';
|
|
79
|
+
else if (output.toLowerCase().includes('microsoft')) provider = 'microsoft';
|
|
80
|
+
|
|
81
|
+
// Parse "Token lifetime: H:MM:SS" from verbose output
|
|
82
|
+
let tokenLifetimeSeconds = null;
|
|
83
|
+
const ltMatch = output.match(/Token lifetime:\s*(\d+):(\d+):(\d+)/);
|
|
84
|
+
if (ltMatch) {
|
|
85
|
+
tokenLifetimeSeconds =
|
|
86
|
+
parseInt(ltMatch[1], 10) * 3600 + parseInt(ltMatch[2], 10) * 60 + parseInt(ltMatch[3], 10);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { provider, tokenLifetimeSeconds };
|
|
90
|
+
}
|
|
91
|
+
|
|
34
92
|
function deviceCodeLogin(cmd) {
|
|
35
93
|
return new Promise((resolve, reject) => {
|
|
36
|
-
const proc = spawn(cmd, ['user', 'login', '-d'], {
|
|
94
|
+
const proc = spawn(cmd, ['user', 'login', '-e', '-d'], {
|
|
37
95
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
38
96
|
});
|
|
39
97
|
|
|
@@ -152,7 +210,7 @@ let isPersisted = false;
|
|
|
152
210
|
// --- Watchdog: health check & auto-restart ---
|
|
153
211
|
|
|
154
212
|
function checkTunnelHealth() {
|
|
155
|
-
if (!tunnelId || !tunnelProc || isRestarting) return;
|
|
213
|
+
if (!tunnelId || !tunnelProc || isRestarting || waitingForAuth) return;
|
|
156
214
|
|
|
157
215
|
const abortCtrl = new AbortController();
|
|
158
216
|
const timer = setTimeout(() => abortCtrl.abort(), 10_000);
|
|
@@ -165,6 +223,20 @@ function checkTunnelHealth() {
|
|
|
165
223
|
clearTimeout(timer);
|
|
166
224
|
|
|
167
225
|
if (err) {
|
|
226
|
+
// Auth errors are handled separately — no restart countdown
|
|
227
|
+
if (isAuthError(err.message) || isAuthError(err.stderr)) {
|
|
228
|
+
handleAuthExpiration();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// "Tunnel not found" can mean the user's auth expired (CLI can't
|
|
233
|
+
// query the tunnel without valid credentials). Check login status
|
|
234
|
+
// to distinguish from a genuinely deleted tunnel.
|
|
235
|
+
if (!isLoggedIn()) {
|
|
236
|
+
handleAuthExpiration();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
168
240
|
consecutiveFailures++;
|
|
169
241
|
log.warn(
|
|
170
242
|
`Tunnel health check error: ${err.message} (${consecutiveFailures}/${HEALTH_CHECK_GRACE})`,
|
|
@@ -193,6 +265,9 @@ function checkTunnelHealth() {
|
|
|
193
265
|
log.info(`Tunnel health restored (${hostConns} host connection(s))`);
|
|
194
266
|
}
|
|
195
267
|
consecutiveFailures = 0;
|
|
268
|
+
|
|
269
|
+
// Check token expiry while tunnel is healthy
|
|
270
|
+
checkTokenExpiry();
|
|
196
271
|
return;
|
|
197
272
|
}
|
|
198
273
|
|
|
@@ -209,6 +284,26 @@ function checkTunnelHealth() {
|
|
|
209
284
|
);
|
|
210
285
|
}
|
|
211
286
|
|
|
287
|
+
function checkTokenExpiry() {
|
|
288
|
+
const info = getLoginInfo();
|
|
289
|
+
if (!info || info.tokenLifetimeSeconds === null) return;
|
|
290
|
+
|
|
291
|
+
const remaining = info.tokenLifetimeSeconds;
|
|
292
|
+
|
|
293
|
+
if (remaining <= TOKEN_EXPIRY_WARN_SECONDS && !expiryWarned) {
|
|
294
|
+
expiryWarned = true;
|
|
295
|
+
const minutes = Math.round(remaining / 60);
|
|
296
|
+
log.warn(`DevTunnel token expires in ${minutes}m`);
|
|
297
|
+
tunnelEvents.emit('auth-expiring', {
|
|
298
|
+
expiresIn: remaining * 1000,
|
|
299
|
+
provider: info.provider,
|
|
300
|
+
});
|
|
301
|
+
} else if (remaining > TOKEN_EXPIRY_WARN_SECONDS) {
|
|
302
|
+
// Reset the warning flag when token is refreshed
|
|
303
|
+
expiryWarned = false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
212
307
|
function startHealthCheck() {
|
|
213
308
|
stopHealthCheck();
|
|
214
309
|
consecutiveFailures = 0;
|
|
@@ -226,30 +321,74 @@ function stopHealthCheck() {
|
|
|
226
321
|
function handleTunnelFailure() {
|
|
227
322
|
if (isRestarting) return;
|
|
228
323
|
stopHealthCheck();
|
|
324
|
+
killTunnelProc();
|
|
229
325
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
326
|
+
tunnelEvents.emit('disconnected');
|
|
327
|
+
scheduleRestart();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// --- Auth-expiration handling ---
|
|
331
|
+
|
|
332
|
+
function killTunnelProc() {
|
|
333
|
+
if (!tunnelProc) return;
|
|
334
|
+
try {
|
|
335
|
+
if (process.platform === 'win32' && tunnelProc.pid) {
|
|
336
|
+
try {
|
|
337
|
+
execFileSync('taskkill', ['/pid', String(tunnelProc.pid), '/T', '/F'], {
|
|
338
|
+
stdio: 'pipe',
|
|
339
|
+
timeout: 5000,
|
|
340
|
+
});
|
|
341
|
+
} catch {
|
|
342
|
+
/* best effort */
|
|
244
343
|
}
|
|
245
|
-
}
|
|
246
|
-
|
|
344
|
+
} else {
|
|
345
|
+
tunnelProc.kill('SIGKILL');
|
|
247
346
|
}
|
|
248
|
-
|
|
347
|
+
} catch {
|
|
348
|
+
/* best effort */
|
|
249
349
|
}
|
|
350
|
+
tunnelProc = null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function handleAuthExpiration() {
|
|
354
|
+
if (waitingForAuth) return;
|
|
355
|
+
stopHealthCheck();
|
|
356
|
+
killTunnelProc();
|
|
250
357
|
|
|
251
358
|
tunnelEvents.emit('disconnected');
|
|
252
|
-
|
|
359
|
+
tunnelEvents.emit('auth-expired');
|
|
360
|
+
startAuthWait();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function startAuthWait() {
|
|
364
|
+
if (waitingForAuth) return;
|
|
365
|
+
waitingForAuth = true;
|
|
366
|
+
isRestarting = false;
|
|
367
|
+
restartAttempts = 0;
|
|
368
|
+
consecutiveFailures = 0;
|
|
369
|
+
|
|
370
|
+
log.warn('DevTunnel auth token expired (Microsoft tokens expire after a few days).');
|
|
371
|
+
log.warn('Tunnel is paused — re-authenticate on the host machine to restore:');
|
|
372
|
+
log.warn(' devtunnel user login -d');
|
|
373
|
+
log.warn('Tunnel will auto-reconnect once auth is restored.');
|
|
374
|
+
|
|
375
|
+
authCheckInterval = setInterval(() => {
|
|
376
|
+
if (isLoggedIn()) {
|
|
377
|
+
log.info('DevTunnel auth restored — resuming tunnel');
|
|
378
|
+
stopAuthWait();
|
|
379
|
+
tunnelEvents.emit('auth-restored');
|
|
380
|
+
scheduleRestart();
|
|
381
|
+
}
|
|
382
|
+
}, AUTH_CHECK_INTERVAL);
|
|
383
|
+
authCheckInterval.unref();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function stopAuthWait() {
|
|
387
|
+
waitingForAuth = false;
|
|
388
|
+
if (authCheckInterval) {
|
|
389
|
+
clearInterval(authCheckInterval);
|
|
390
|
+
authCheckInterval = null;
|
|
391
|
+
}
|
|
253
392
|
}
|
|
254
393
|
|
|
255
394
|
function scheduleRestart() {
|
|
@@ -271,6 +410,15 @@ function scheduleRestart() {
|
|
|
271
410
|
|
|
272
411
|
restartTimer = setTimeout(async () => {
|
|
273
412
|
restartTimer = null;
|
|
413
|
+
|
|
414
|
+
// If auth expired since restart was scheduled, switch to auth-wait mode
|
|
415
|
+
if (!isLoggedIn()) {
|
|
416
|
+
log.warn('DevTunnel auth expired during restart — waiting for re-authentication');
|
|
417
|
+
isRestarting = false;
|
|
418
|
+
handleAuthExpiration();
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
274
422
|
try {
|
|
275
423
|
const result = await hostTunnel();
|
|
276
424
|
if (result) {
|
|
@@ -368,21 +516,30 @@ async function startTunnel(port, options = {}) {
|
|
|
368
516
|
|
|
369
517
|
log.info('Starting devtunnel...');
|
|
370
518
|
try {
|
|
371
|
-
// Ensure user is logged in
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
519
|
+
// Ensure user is logged in. Prefer Entra over GitHub — Entra tokens auto-refresh
|
|
520
|
+
// for weeks via MSAL, while GitHub tokens expire after 8 hours.
|
|
521
|
+
let loggedIn = isLoggedIn();
|
|
522
|
+
const loginInfo = loggedIn ? getLoginInfo() : null;
|
|
523
|
+
|
|
524
|
+
if (loggedIn && loginInfo) {
|
|
525
|
+
const { provider, tokenLifetimeSeconds } = loginInfo;
|
|
526
|
+
if (provider === 'github') {
|
|
527
|
+
log.warn(
|
|
528
|
+
'Logged in with GitHub — tokens expire every 8 hours. ' +
|
|
529
|
+
'For longer sessions, use: devtunnel user login -e -d',
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
if (tokenLifetimeSeconds !== null) {
|
|
533
|
+
const h = Math.floor(tokenLifetimeSeconds / 3600);
|
|
534
|
+
const m = Math.round((tokenLifetimeSeconds % 3600) / 60);
|
|
535
|
+
log.info(`DevTunnel token expires in ${h}h ${m}m`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
381
538
|
|
|
382
539
|
if (!loggedIn) {
|
|
383
|
-
log.info('
|
|
540
|
+
log.info('Logging in to DevTunnel with Microsoft Entra (recommended for long sessions)...');
|
|
384
541
|
try {
|
|
385
|
-
execFileSync(devtunnelCmd, ['user', 'login'], { stdio: 'inherit', timeout: 30000 });
|
|
542
|
+
execFileSync(devtunnelCmd, ['user', 'login', '-e'], { stdio: 'inherit', timeout: 30000 });
|
|
386
543
|
} catch {
|
|
387
544
|
log.info('Browser login failed or unavailable, falling back to device code flow...');
|
|
388
545
|
log.info('A code will be displayed — open the URL on any device to authenticate.');
|
|
@@ -391,7 +548,7 @@ async function startTunnel(port, options = {}) {
|
|
|
391
548
|
} catch (_loginErr) {
|
|
392
549
|
log.error('');
|
|
393
550
|
log.error(' DevTunnel login failed. To use tunnels, run:');
|
|
394
|
-
log.error(' devtunnel user login');
|
|
551
|
+
log.error(' devtunnel user login -e -d');
|
|
395
552
|
log.error('');
|
|
396
553
|
log.error(' Or start without a tunnel:');
|
|
397
554
|
log.error(' termbeam --no-tunnel');
|
|
@@ -480,8 +637,9 @@ async function startTunnel(port, options = {}) {
|
|
|
480
637
|
}
|
|
481
638
|
|
|
482
639
|
function cleanupTunnel() {
|
|
483
|
-
// Stop watchdog
|
|
640
|
+
// Stop watchdog and auth-wait to prevent restart during cleanup
|
|
484
641
|
stopHealthCheck();
|
|
642
|
+
stopAuthWait();
|
|
485
643
|
isRestarting = true; // prevent exit handler from restarting
|
|
486
644
|
if (restartTimer) {
|
|
487
645
|
clearTimeout(restartTimer);
|
|
@@ -489,26 +647,7 @@ function cleanupTunnel() {
|
|
|
489
647
|
}
|
|
490
648
|
|
|
491
649
|
const id = tunnelId;
|
|
492
|
-
|
|
493
|
-
try {
|
|
494
|
-
// On Windows, kill the process tree to ensure all children die
|
|
495
|
-
if (process.platform === 'win32' && tunnelProc.pid) {
|
|
496
|
-
try {
|
|
497
|
-
execFileSync('taskkill', ['/pid', String(tunnelProc.pid), '/T', '/F'], {
|
|
498
|
-
stdio: 'pipe',
|
|
499
|
-
timeout: 5000,
|
|
500
|
-
});
|
|
501
|
-
} catch {
|
|
502
|
-
/* best effort */
|
|
503
|
-
}
|
|
504
|
-
} else {
|
|
505
|
-
tunnelProc.kill('SIGKILL');
|
|
506
|
-
}
|
|
507
|
-
} catch {
|
|
508
|
-
/* best effort */
|
|
509
|
-
}
|
|
510
|
-
tunnelProc = null;
|
|
511
|
-
}
|
|
650
|
+
killTunnelProc();
|
|
512
651
|
if (id) {
|
|
513
652
|
tunnelId = null;
|
|
514
653
|
if (isPersisted) {
|
|
@@ -527,6 +666,14 @@ function cleanupTunnel() {
|
|
|
527
666
|
consecutiveFailures = 0;
|
|
528
667
|
restartAttempts = 0;
|
|
529
668
|
isRestarting = false;
|
|
669
|
+
expiryWarned = false;
|
|
530
670
|
}
|
|
531
671
|
|
|
532
|
-
module.exports = {
|
|
672
|
+
module.exports = {
|
|
673
|
+
startTunnel,
|
|
674
|
+
cleanupTunnel,
|
|
675
|
+
findDevtunnel,
|
|
676
|
+
tunnelEvents,
|
|
677
|
+
getLoginInfo,
|
|
678
|
+
parseLoginInfo,
|
|
679
|
+
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{aq as o,ar as n}from"./index-BKVhZe-L.js";const t=(r,a)=>o.lang.round(n.parse(r)[a]);export{t as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-WL4C6EOR-Cty7zCuA.js";import{_ as i}from"./index-BKVhZe-L.js";import"./chunk-FMBD7UC4-0fJhTPpT.js";import"./chunk-JSJVCQXG-Bv4lt_uR.js";import"./chunk-55IACEB6-C1YDnAkF.js";import"./chunk-KX2RTZJC-DSuNgcxs.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-WL4C6EOR-Cty7zCuA.js";import{_ as i}from"./index-BKVhZe-L.js";import"./chunk-FMBD7UC4-0fJhTPpT.js";import"./chunk-JSJVCQXG-Bv4lt_uR.js";import"./chunk-55IACEB6-C1YDnAkF.js";import"./chunk-KX2RTZJC-DSuNgcxs.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{b as r}from"./_baseUniq-DsM_tAIh.js";var e=4;function a(o){return r(o,e)}export{a as c};
|