fix: refresh terminal websocket auth on reconnect #32

Merged
jimmy merged 1 commits from dev into main 2026-03-07 17:57:48 +08:00
2 changed files with 68 additions and 7 deletions
+9
View File
@@ -54,6 +54,15 @@ async function tryRefresh() {
return refreshPromise;
}
export async function getWebSocketToken(forceRefresh = false) {
if (!forceRefresh && token) return token;
const refreshed = await tryRefresh();
if (refreshed && token) return token;
setToken("");
setRefreshToken("");
return "";
}
async function request(path, opts = {}) {
const headers = opts.headers || {};
if (token) headers["Authorization"] = `Bearer ${token}`;
+59 -7
View File
@@ -1,7 +1,7 @@
<script>
import { onMount, onDestroy } from "svelte";
import { VoiceSession } from "../lib/voice.js";
import { getToken } from "../lib/api.js";
import { getWebSocketToken } from "../lib/api.js";
const PERSISTENCE_STATUS_PREFIX = "__DASHBOARD_PERSISTENCE__:";
const PERSISTENCE_STATUS_PREFIX_BYTES = new TextEncoder().encode(PERSISTENCE_STATUS_PREFIX);
@@ -189,11 +189,11 @@
fit.fit();
const proto = location.protocol === "https:" ? "wss:" : "ws:";
const token = getToken() || "";
let heartbeat = null;
let reconnectTimer = null;
let closedManually = false;
let reconnecting = false;
let authRecoveryInFlight = false;
function clearHeartbeat() {
if (heartbeat) {
@@ -202,6 +202,29 @@
}
}
function handleAuthExpired(reason = "Session expired — please log in again") {
clearHeartbeat();
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
reconnecting = false;
updateTab(tabId, {
connected: false,
error: reason,
statusBanner: "",
});
term.write(`\r\n\x1b[90m[${reason}]\x1b[0m`);
const d = tabData.get(tabId);
if (d) {
d.ws = null;
d.heartbeat = heartbeat;
d.reconnectTimer = reconnectTimer;
d.closedManually = closedManually;
d.reconnecting = reconnecting;
}
}
function scheduleReconnect(reason) {
clearHeartbeat();
if (closedManually || reconnectTimer) return;
@@ -215,7 +238,7 @@
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
if (!tabData.has(tabId) || closedManually) return;
connect();
connect(true);
}, 2000);
}
@@ -229,8 +252,13 @@
}
}
function connect() {
const token = getToken() || "";
async function connect(forceRefresh = false) {
const token = await getWebSocketToken(forceRefresh);
if (!token) {
handleAuthExpired();
return null;
}
const ws = new WebSocket(
`${proto}//${location.host}/ws/terminal?host=${encodeURIComponent(tab.hostId)}&token=${encodeURIComponent(token)}`
);
@@ -272,10 +300,30 @@
}
term.write(data);
};
ws.onclose = (e) => {
ws.onclose = async (e) => {
const reason = e.reason || `Connection closed (code ${e.code})`;
const d = tabData.get(tabId);
if (d) d.ws = null;
if (e.code === 1008 && reason === "Unauthorized") {
if (!closedManually && !authRecoveryInFlight) {
authRecoveryInFlight = true;
const recoveredToken = await getWebSocketToken(true);
authRecoveryInFlight = false;
if (recoveredToken) {
reconnecting = true;
updateTab(tabId, {
connected: false,
error: "Refreshing session...",
statusBanner: tab.persistent ? "Reconnecting to persistent session..." : "",
});
term.write(`\r\n\x1b[90m[Refreshing session...]\x1b[0m`);
void connect(false);
return;
}
}
handleAuthExpired("Session expired — please log in again");
return;
}
if (!closedManually) scheduleReconnect(reason);
else {
clearHeartbeat();
@@ -301,7 +349,11 @@
return ws;
}
let ws = connect();
let ws = null;
void connect().then((connectedWs) => {
ws = connectedWs;
if (connectedWs) voice.attach(term, connectedWs);
});
term.onData((data) => {
const currentWs = tabData.get(tabId)?.ws;
if (currentWs?.readyState === 1) currentWs.send(new TextEncoder().encode(data));