lunel-cli 0.1.33 → 0.1.35
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/dist/index.js +253 -40
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ const __require = createRequire(import.meta.url);
|
|
|
19
19
|
const VERSION = __require("../package.json").version;
|
|
20
20
|
const PTY_RELEASE_INFO_URL = process.env.LUNEL_PTY_INFO_URL ||
|
|
21
21
|
"https://raw.githubusercontent.com/ssbharambe-m/pty-releases/refs/heads/main/info.json";
|
|
22
|
+
const VERBOSE_AI_LOGS = process.env.LUNEL_DEBUG_AI === "1";
|
|
22
23
|
// Root directory - sandbox all file operations to this
|
|
23
24
|
const ROOT_DIR = process.cwd();
|
|
24
25
|
// Terminal sessions (managed by Rust PTY binary)
|
|
@@ -43,9 +44,72 @@ let activeControlWs = null;
|
|
|
43
44
|
let activeDataWs = null;
|
|
44
45
|
const activeTunnels = new Map();
|
|
45
46
|
const PORT_SYNC_INTERVAL_MS = 30_000;
|
|
47
|
+
const CLI_LOCAL_TCP_CONNECT_TIMEOUT_MS = 2_500;
|
|
48
|
+
const PROXY_WS_CONNECT_TIMEOUT_MS = 12_000;
|
|
49
|
+
const TUNNEL_SETUP_BUDGET_MS = 18_000;
|
|
50
|
+
const PROXY_WS_CONNECT_RETRY_ATTEMPTS = 1;
|
|
51
|
+
const PROXY_WS_RETRY_JITTER_MIN_MS = 200;
|
|
52
|
+
const PROXY_WS_RETRY_JITTER_MAX_MS = 500;
|
|
53
|
+
const PROXY_TUNNEL_LINGER_MS = 1_200;
|
|
46
54
|
let portSyncTimer = null;
|
|
47
55
|
let portScanInFlight = false;
|
|
48
56
|
let lastDiscoveredPorts = [];
|
|
57
|
+
function redactSensitive(input) {
|
|
58
|
+
const text = typeof input === "string" ? input : JSON.stringify(input);
|
|
59
|
+
return text
|
|
60
|
+
.replace(/([A-Za-z0-9\-_]{20,}\.[A-Za-z0-9\-_]{20,}\.[A-Za-z0-9\-_]{20,})/g, "[redacted_jwt]")
|
|
61
|
+
.replace(/(password|token|authorization|resumeToken|x-manager-password)\s*[:=]\s*["']?[^"',\s}]+/gi, "$1=[redacted]")
|
|
62
|
+
.replace(/[A-Za-z0-9+/=_-]{40,}/g, "[redacted_secret]");
|
|
63
|
+
}
|
|
64
|
+
function parseProxyControlFrame(raw) {
|
|
65
|
+
const text = typeof raw === "string" ? raw : Buffer.isBuffer(raw) ? raw.toString("utf-8") : null;
|
|
66
|
+
if (!text)
|
|
67
|
+
return null;
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(text);
|
|
70
|
+
if (parsed?.v !== 1 || parsed?.t !== "proxy_ctrl")
|
|
71
|
+
return null;
|
|
72
|
+
if (parsed.action !== "fin" && parsed.action !== "rst")
|
|
73
|
+
return null;
|
|
74
|
+
return {
|
|
75
|
+
v: 1,
|
|
76
|
+
t: "proxy_ctrl",
|
|
77
|
+
action: parsed.action,
|
|
78
|
+
reason: typeof parsed.reason === "string" ? parsed.reason : undefined,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function sendProxyControl(tunnel, action, reason) {
|
|
86
|
+
if (tunnel.proxyWs.readyState !== WebSocket.OPEN)
|
|
87
|
+
return;
|
|
88
|
+
const frame = { v: 1, t: "proxy_ctrl", action, reason };
|
|
89
|
+
tunnel.proxyWs.send(JSON.stringify(frame));
|
|
90
|
+
}
|
|
91
|
+
function maybeFinalizeTunnel(tunnelId) {
|
|
92
|
+
const tunnel = activeTunnels.get(tunnelId);
|
|
93
|
+
if (!tunnel || tunnel.closing)
|
|
94
|
+
return;
|
|
95
|
+
if (!tunnel.localEnded || !tunnel.remoteEnded)
|
|
96
|
+
return;
|
|
97
|
+
if (tunnel.finalizeTimer)
|
|
98
|
+
return;
|
|
99
|
+
tunnel.finalizeTimer = setTimeout(() => {
|
|
100
|
+
const current = activeTunnels.get(tunnelId);
|
|
101
|
+
if (!current || current.closing)
|
|
102
|
+
return;
|
|
103
|
+
current.closing = true;
|
|
104
|
+
activeTunnels.delete(tunnelId);
|
|
105
|
+
if (!current.tcpSocket.destroyed) {
|
|
106
|
+
current.tcpSocket.destroy();
|
|
107
|
+
}
|
|
108
|
+
if (current.proxyWs.readyState === WebSocket.OPEN || current.proxyWs.readyState === WebSocket.CONNECTING) {
|
|
109
|
+
current.proxyWs.close();
|
|
110
|
+
}
|
|
111
|
+
}, PROXY_TUNNEL_LINGER_MS);
|
|
112
|
+
}
|
|
49
113
|
// Popular development server ports to scan on connect
|
|
50
114
|
const DEV_PORTS = [
|
|
51
115
|
1234, // Parcel
|
|
@@ -1375,30 +1439,36 @@ function requireData(response, label) {
|
|
|
1375
1439
|
const errMsg = response.error
|
|
1376
1440
|
? (typeof response.error === "string" ? response.error : JSON.stringify(response.error))
|
|
1377
1441
|
: `${label} returned no data`;
|
|
1378
|
-
console.error(`[ai] ${label} failed:`, errMsg, "raw response:", JSON.stringify(response).substring(0, 500));
|
|
1442
|
+
console.error(`[ai] ${label} failed:`, redactSensitive(errMsg), "raw response:", redactSensitive(JSON.stringify(response).substring(0, 500)));
|
|
1379
1443
|
throw new Error(errMsg);
|
|
1380
1444
|
}
|
|
1381
1445
|
return response.data;
|
|
1382
1446
|
}
|
|
1383
1447
|
async function handleAiCreateSession(payload) {
|
|
1384
1448
|
const title = payload.title || undefined;
|
|
1385
|
-
|
|
1449
|
+
if (VERBOSE_AI_LOGS)
|
|
1450
|
+
console.log("[ai] createSession called");
|
|
1386
1451
|
try {
|
|
1387
1452
|
const response = await opencodeClient.session.create({ body: { title } });
|
|
1388
|
-
|
|
1453
|
+
if (VERBOSE_AI_LOGS) {
|
|
1454
|
+
console.log("[ai] createSession response ok:", !!response.data, "error:", response.error ? redactSensitive(JSON.stringify(response.error).substring(0, 200)) : "none");
|
|
1455
|
+
}
|
|
1389
1456
|
return { session: requireData(response, "session.create") };
|
|
1390
1457
|
}
|
|
1391
1458
|
catch (err) {
|
|
1392
|
-
console.error("[ai] createSession exception:", err.message
|
|
1459
|
+
console.error("[ai] createSession exception:", redactSensitive(err.message));
|
|
1393
1460
|
throw err;
|
|
1394
1461
|
}
|
|
1395
1462
|
}
|
|
1396
1463
|
async function handleAiListSessions() {
|
|
1397
|
-
|
|
1464
|
+
if (VERBOSE_AI_LOGS)
|
|
1465
|
+
console.log("[ai] listSessions called");
|
|
1398
1466
|
try {
|
|
1399
1467
|
const response = await opencodeClient.session.list();
|
|
1400
1468
|
const data = requireData(response, "session.list");
|
|
1401
|
-
|
|
1469
|
+
if (VERBOSE_AI_LOGS) {
|
|
1470
|
+
console.log("[ai] listSessions returned", Array.isArray(data) ? data.length : typeof data, "sessions");
|
|
1471
|
+
}
|
|
1402
1472
|
return { sessions: data };
|
|
1403
1473
|
}
|
|
1404
1474
|
catch (err) {
|
|
@@ -1420,7 +1490,8 @@ async function handleAiDeleteSession(payload) {
|
|
|
1420
1490
|
}
|
|
1421
1491
|
async function handleAiGetMessages(payload) {
|
|
1422
1492
|
const id = payload.id;
|
|
1423
|
-
|
|
1493
|
+
if (VERBOSE_AI_LOGS)
|
|
1494
|
+
console.log("[ai] getMessages called");
|
|
1424
1495
|
try {
|
|
1425
1496
|
const response = await opencodeClient.session.messages({ path: { id } });
|
|
1426
1497
|
const raw = requireData(response, "session.messages");
|
|
@@ -1432,7 +1503,8 @@ async function handleAiGetMessages(payload) {
|
|
|
1432
1503
|
parts: m.parts || [],
|
|
1433
1504
|
time: m.info.time,
|
|
1434
1505
|
}));
|
|
1435
|
-
|
|
1506
|
+
if (VERBOSE_AI_LOGS)
|
|
1507
|
+
console.log("[ai] getMessages returned", messages.length, "messages");
|
|
1436
1508
|
return { messages };
|
|
1437
1509
|
}
|
|
1438
1510
|
catch (err) {
|
|
@@ -1445,7 +1517,14 @@ async function handleAiPrompt(payload) {
|
|
|
1445
1517
|
const text = payload.text;
|
|
1446
1518
|
const model = payload.model;
|
|
1447
1519
|
const agent = payload.agent;
|
|
1448
|
-
|
|
1520
|
+
if (VERBOSE_AI_LOGS) {
|
|
1521
|
+
console.log("[ai] prompt called", {
|
|
1522
|
+
hasSessionId: Boolean(sessionId),
|
|
1523
|
+
model: redactSensitive(JSON.stringify(model || {})),
|
|
1524
|
+
hasAgent: Boolean(agent),
|
|
1525
|
+
textLength: typeof text === "string" ? text.length : 0,
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1449
1528
|
// Fire and forget — results stream via SSE events forwarded on data channel
|
|
1450
1529
|
opencodeClient.session.prompt({
|
|
1451
1530
|
path: { id: sessionId },
|
|
@@ -1477,11 +1556,14 @@ async function handleAiAbort(payload) {
|
|
|
1477
1556
|
return {};
|
|
1478
1557
|
}
|
|
1479
1558
|
async function handleAiAgents() {
|
|
1480
|
-
|
|
1559
|
+
if (VERBOSE_AI_LOGS)
|
|
1560
|
+
console.log("[ai] getAgents called");
|
|
1481
1561
|
try {
|
|
1482
1562
|
const response = await opencodeClient.app.agents();
|
|
1483
1563
|
const data = requireData(response, "app.agents");
|
|
1484
|
-
|
|
1564
|
+
if (VERBOSE_AI_LOGS) {
|
|
1565
|
+
console.log("[ai] getAgents returned:", redactSensitive(JSON.stringify(data).substring(0, 300)));
|
|
1566
|
+
}
|
|
1485
1567
|
return { agents: data };
|
|
1486
1568
|
}
|
|
1487
1569
|
catch (err) {
|
|
@@ -1490,11 +1572,14 @@ async function handleAiAgents() {
|
|
|
1490
1572
|
}
|
|
1491
1573
|
}
|
|
1492
1574
|
async function handleAiProviders() {
|
|
1493
|
-
|
|
1575
|
+
if (VERBOSE_AI_LOGS)
|
|
1576
|
+
console.log("[ai] getProviders called");
|
|
1494
1577
|
try {
|
|
1495
1578
|
const response = await opencodeClient.config.providers();
|
|
1496
1579
|
const data = requireData(response, "config.providers");
|
|
1497
|
-
|
|
1580
|
+
if (VERBOSE_AI_LOGS) {
|
|
1581
|
+
console.log("[ai] getProviders returned", data.providers?.length, "providers, defaults:", redactSensitive(JSON.stringify(data.default)));
|
|
1582
|
+
}
|
|
1498
1583
|
return { providers: data.providers, default: data.default };
|
|
1499
1584
|
}
|
|
1500
1585
|
catch (err) {
|
|
@@ -1572,7 +1657,7 @@ async function subscribeToOpenCodeEvents(client) {
|
|
|
1572
1657
|
? parsed.payload
|
|
1573
1658
|
: parsed;
|
|
1574
1659
|
if (!base || typeof base.type !== "string") {
|
|
1575
|
-
console.warn("[sse] Dropped malformed event:", JSON.stringify(parsed).substring(0, 200));
|
|
1660
|
+
console.warn("[sse] Dropped malformed event:", redactSensitive(JSON.stringify(parsed).substring(0, 200)));
|
|
1576
1661
|
continue;
|
|
1577
1662
|
}
|
|
1578
1663
|
console.log("[sse]", base.type);
|
|
@@ -1669,19 +1754,25 @@ function startPortSync(ws) {
|
|
|
1669
1754
|
async function handleProxyConnect(payload) {
|
|
1670
1755
|
const tunnelId = payload.tunnelId;
|
|
1671
1756
|
const port = payload.port;
|
|
1757
|
+
const setupStartedAt = Date.now();
|
|
1758
|
+
const getRemainingSetupMs = () => TUNNEL_SETUP_BUDGET_MS - (Date.now() - setupStartedAt);
|
|
1672
1759
|
if (!tunnelId)
|
|
1673
1760
|
throw Object.assign(new Error("tunnelId is required"), { code: "EINVAL" });
|
|
1674
1761
|
if (!port)
|
|
1675
1762
|
throw Object.assign(new Error("port is required"), { code: "EINVAL" });
|
|
1676
1763
|
if (!currentSessionCode && !currentSessionPassword)
|
|
1677
1764
|
throw Object.assign(new Error("no active session"), { code: "ENOENT" });
|
|
1765
|
+
if (getRemainingSetupMs() <= 0) {
|
|
1766
|
+
throw Object.assign(new Error("Tunnel setup timeout before start"), { code: "ETIMEOUT" });
|
|
1767
|
+
}
|
|
1678
1768
|
// 1. Open TCP connection to the local service
|
|
1679
1769
|
const tcpSocket = createConnection({ port, host: "127.0.0.1" });
|
|
1770
|
+
const tcpConnectTimeoutMs = Math.min(CLI_LOCAL_TCP_CONNECT_TIMEOUT_MS, Math.max(250, getRemainingSetupMs()));
|
|
1680
1771
|
await new Promise((resolve, reject) => {
|
|
1681
1772
|
const timeout = setTimeout(() => {
|
|
1682
1773
|
tcpSocket.destroy();
|
|
1683
1774
|
reject(Object.assign(new Error(`TCP connect timeout to localhost:${port}`), { code: "ETIMEOUT" }));
|
|
1684
|
-
},
|
|
1775
|
+
}, tcpConnectTimeoutMs);
|
|
1685
1776
|
tcpSocket.on("connect", () => {
|
|
1686
1777
|
clearTimeout(timeout);
|
|
1687
1778
|
resolve();
|
|
@@ -1697,25 +1788,74 @@ async function handleProxyConnect(payload) {
|
|
|
1697
1788
|
? `password=${encodeURIComponent(currentSessionPassword)}`
|
|
1698
1789
|
: `code=${encodeURIComponent(currentSessionCode)}`;
|
|
1699
1790
|
const proxyWsUrl = `${wsBase}/v1/ws/proxy?${authQuery}&tunnelId=${encodeURIComponent(tunnelId)}&role=cli`;
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1791
|
+
let proxyWs = null;
|
|
1792
|
+
let lastProxyError = null;
|
|
1793
|
+
for (let attempt = 0; attempt <= PROXY_WS_CONNECT_RETRY_ATTEMPTS; attempt++) {
|
|
1794
|
+
const remainingMs = getRemainingSetupMs();
|
|
1795
|
+
if (remainingMs <= 0) {
|
|
1704
1796
|
tcpSocket.destroy();
|
|
1705
|
-
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1797
|
+
throw Object.assign(new Error("Tunnel setup timeout while connecting proxy WS"), { code: "ETIMEOUT" });
|
|
1798
|
+
}
|
|
1799
|
+
const wsConnectTimeoutMs = Math.min(PROXY_WS_CONNECT_TIMEOUT_MS, Math.max(250, remainingMs));
|
|
1800
|
+
const candidateWs = new WebSocket(proxyWsUrl);
|
|
1801
|
+
try {
|
|
1802
|
+
await new Promise((resolve, reject) => {
|
|
1803
|
+
const timeout = setTimeout(() => {
|
|
1804
|
+
candidateWs.close();
|
|
1805
|
+
reject(Object.assign(new Error("Proxy WS connect timeout"), { code: "ETIMEOUT" }));
|
|
1806
|
+
}, wsConnectTimeoutMs);
|
|
1807
|
+
candidateWs.on("open", () => {
|
|
1808
|
+
clearTimeout(timeout);
|
|
1809
|
+
resolve();
|
|
1810
|
+
});
|
|
1811
|
+
candidateWs.on("error", (err) => {
|
|
1812
|
+
clearTimeout(timeout);
|
|
1813
|
+
reject(Object.assign(new Error(`Proxy WS failed: ${err.message}`), { code: "ECONNREFUSED" }));
|
|
1814
|
+
});
|
|
1815
|
+
candidateWs.on("close", () => {
|
|
1816
|
+
clearTimeout(timeout);
|
|
1817
|
+
reject(Object.assign(new Error("Proxy WS closed during connect"), { code: "ECONNRESET" }));
|
|
1818
|
+
});
|
|
1819
|
+
});
|
|
1820
|
+
proxyWs = candidateWs;
|
|
1821
|
+
break;
|
|
1822
|
+
}
|
|
1823
|
+
catch (error) {
|
|
1824
|
+
lastProxyError = error;
|
|
1825
|
+
try {
|
|
1826
|
+
candidateWs.close();
|
|
1827
|
+
}
|
|
1828
|
+
catch {
|
|
1829
|
+
// ignore
|
|
1830
|
+
}
|
|
1831
|
+
if (attempt >= PROXY_WS_CONNECT_RETRY_ATTEMPTS) {
|
|
1832
|
+
break;
|
|
1833
|
+
}
|
|
1834
|
+
const jitterSpan = PROXY_WS_RETRY_JITTER_MAX_MS - PROXY_WS_RETRY_JITTER_MIN_MS;
|
|
1835
|
+
const jitterMs = PROXY_WS_RETRY_JITTER_MIN_MS + Math.floor(Math.random() * (jitterSpan + 1));
|
|
1836
|
+
if (getRemainingSetupMs() <= jitterMs) {
|
|
1837
|
+
break;
|
|
1838
|
+
}
|
|
1839
|
+
await new Promise((resolve) => setTimeout(resolve, jitterMs));
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
if (!proxyWs) {
|
|
1843
|
+
tcpSocket.destroy();
|
|
1844
|
+
const err = lastProxyError || Object.assign(new Error("Proxy WS connect failed"), { code: "ECONNREFUSED" });
|
|
1845
|
+
throw err;
|
|
1846
|
+
}
|
|
1717
1847
|
// 3. Store the tunnel
|
|
1718
|
-
activeTunnels.set(tunnelId, {
|
|
1848
|
+
activeTunnels.set(tunnelId, {
|
|
1849
|
+
tunnelId,
|
|
1850
|
+
port,
|
|
1851
|
+
tcpSocket,
|
|
1852
|
+
proxyWs,
|
|
1853
|
+
localEnded: false,
|
|
1854
|
+
remoteEnded: false,
|
|
1855
|
+
finSent: false,
|
|
1856
|
+
finalizeTimer: null,
|
|
1857
|
+
closing: false,
|
|
1858
|
+
});
|
|
1719
1859
|
// 4. Pipe: TCP data -> proxy WS (as binary)
|
|
1720
1860
|
tcpSocket.on("data", (chunk) => {
|
|
1721
1861
|
if (proxyWs.readyState === WebSocket.OPEN) {
|
|
@@ -1724,18 +1864,70 @@ async function handleProxyConnect(payload) {
|
|
|
1724
1864
|
});
|
|
1725
1865
|
// 5. Pipe: proxy WS -> TCP socket (as binary)
|
|
1726
1866
|
proxyWs.on("message", (data) => {
|
|
1867
|
+
const control = parseProxyControlFrame(data);
|
|
1868
|
+
if (control) {
|
|
1869
|
+
const tunnel = activeTunnels.get(tunnelId);
|
|
1870
|
+
if (!tunnel || tunnel.closing)
|
|
1871
|
+
return;
|
|
1872
|
+
if (control.action === "fin") {
|
|
1873
|
+
tunnel.remoteEnded = true;
|
|
1874
|
+
if (!tcpSocket.destroyed) {
|
|
1875
|
+
tcpSocket.end();
|
|
1876
|
+
}
|
|
1877
|
+
maybeFinalizeTunnel(tunnelId);
|
|
1878
|
+
}
|
|
1879
|
+
else {
|
|
1880
|
+
tunnel.closing = true;
|
|
1881
|
+
activeTunnels.delete(tunnelId);
|
|
1882
|
+
if (!tcpSocket.destroyed) {
|
|
1883
|
+
tcpSocket.destroy();
|
|
1884
|
+
}
|
|
1885
|
+
if (proxyWs.readyState === WebSocket.OPEN || proxyWs.readyState === WebSocket.CONNECTING) {
|
|
1886
|
+
proxyWs.close();
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1727
1891
|
if (!tcpSocket.destroyed) {
|
|
1728
|
-
|
|
1892
|
+
const chunk = Buffer.isBuffer(data)
|
|
1893
|
+
? data
|
|
1894
|
+
: typeof data === "string"
|
|
1895
|
+
? Buffer.from(data)
|
|
1896
|
+
: Array.isArray(data)
|
|
1897
|
+
? Buffer.concat(data)
|
|
1898
|
+
: Buffer.from(data);
|
|
1899
|
+
tcpSocket.write(chunk);
|
|
1729
1900
|
}
|
|
1730
1901
|
});
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1902
|
+
const markLocalEnded = () => {
|
|
1903
|
+
const tunnel = activeTunnels.get(tunnelId);
|
|
1904
|
+
if (!tunnel || tunnel.closing)
|
|
1905
|
+
return;
|
|
1906
|
+
tunnel.localEnded = true;
|
|
1907
|
+
if (!tunnel.finSent) {
|
|
1908
|
+
tunnel.finSent = true;
|
|
1909
|
+
sendProxyControl(tunnel, "fin");
|
|
1736
1910
|
}
|
|
1911
|
+
maybeFinalizeTunnel(tunnelId);
|
|
1912
|
+
};
|
|
1913
|
+
// 6. Half-close handling
|
|
1914
|
+
tcpSocket.on("end", () => {
|
|
1915
|
+
markLocalEnded();
|
|
1916
|
+
});
|
|
1917
|
+
tcpSocket.on("close", () => {
|
|
1918
|
+
markLocalEnded();
|
|
1737
1919
|
});
|
|
1738
1920
|
tcpSocket.on("error", () => {
|
|
1921
|
+
const tunnel = activeTunnels.get(tunnelId);
|
|
1922
|
+
if (tunnel && !tunnel.finSent) {
|
|
1923
|
+
sendProxyControl(tunnel, "rst", "tcp_error");
|
|
1924
|
+
}
|
|
1925
|
+
if (tunnel) {
|
|
1926
|
+
tunnel.closing = true;
|
|
1927
|
+
if (tunnel.finalizeTimer) {
|
|
1928
|
+
clearTimeout(tunnel.finalizeTimer);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1739
1931
|
activeTunnels.delete(tunnelId);
|
|
1740
1932
|
if (proxyWs.readyState === WebSocket.OPEN || proxyWs.readyState === WebSocket.CONNECTING) {
|
|
1741
1933
|
proxyWs.close();
|
|
@@ -1743,12 +1935,26 @@ async function handleProxyConnect(payload) {
|
|
|
1743
1935
|
});
|
|
1744
1936
|
// 7. Close cascade: WS closes -> close TCP
|
|
1745
1937
|
proxyWs.on("close", () => {
|
|
1938
|
+
const tunnel = activeTunnels.get(tunnelId);
|
|
1939
|
+
if (tunnel) {
|
|
1940
|
+
tunnel.closing = true;
|
|
1941
|
+
if (tunnel.finalizeTimer) {
|
|
1942
|
+
clearTimeout(tunnel.finalizeTimer);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1746
1945
|
activeTunnels.delete(tunnelId);
|
|
1747
1946
|
if (!tcpSocket.destroyed) {
|
|
1748
1947
|
tcpSocket.destroy();
|
|
1749
1948
|
}
|
|
1750
1949
|
});
|
|
1751
1950
|
proxyWs.on("error", () => {
|
|
1951
|
+
const tunnel = activeTunnels.get(tunnelId);
|
|
1952
|
+
if (tunnel) {
|
|
1953
|
+
tunnel.closing = true;
|
|
1954
|
+
if (tunnel.finalizeTimer) {
|
|
1955
|
+
clearTimeout(tunnel.finalizeTimer);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1752
1958
|
activeTunnels.delete(tunnelId);
|
|
1753
1959
|
if (!tcpSocket.destroyed) {
|
|
1754
1960
|
tcpSocket.destroy();
|
|
@@ -1758,6 +1964,9 @@ async function handleProxyConnect(payload) {
|
|
|
1758
1964
|
}
|
|
1759
1965
|
function cleanupAllTunnels() {
|
|
1760
1966
|
for (const [, tunnel] of activeTunnels) {
|
|
1967
|
+
if (tunnel.finalizeTimer) {
|
|
1968
|
+
clearTimeout(tunnel.finalizeTimer);
|
|
1969
|
+
}
|
|
1761
1970
|
tunnel.tcpSocket.destroy();
|
|
1762
1971
|
if (tunnel.proxyWs.readyState === WebSocket.OPEN) {
|
|
1763
1972
|
tunnel.proxyWs.close();
|
|
@@ -1784,7 +1993,7 @@ async function processMessage(message) {
|
|
|
1784
1993
|
}
|
|
1785
1994
|
// Validate required fields
|
|
1786
1995
|
if (!ns || !action) {
|
|
1787
|
-
console.warn("[router] Ignoring message with missing ns/action:", JSON.stringify(message).substring(0, 300));
|
|
1996
|
+
console.warn("[router] Ignoring message with missing ns/action:", redactSensitive(JSON.stringify(message).substring(0, 300)));
|
|
1788
1997
|
return {
|
|
1789
1998
|
v: 1,
|
|
1790
1999
|
id,
|
|
@@ -2198,7 +2407,11 @@ async function connectWebSocket() {
|
|
|
2198
2407
|
return;
|
|
2199
2408
|
}
|
|
2200
2409
|
if (message.type === "close_connection") {
|
|
2201
|
-
|
|
2410
|
+
const reason = message.reason || "expired";
|
|
2411
|
+
console.log(`[session] closed by gateway: ${reason}`);
|
|
2412
|
+
if (reason === "session ended from app") {
|
|
2413
|
+
console.log("[session] Run `npx lunel-cli` again and scan the new QR code to reconnect.");
|
|
2414
|
+
}
|
|
2202
2415
|
gracefulShutdown();
|
|
2203
2416
|
return;
|
|
2204
2417
|
}
|