fix: refresh terminal websocket auth on reconnect
Deploy Dashboard (Dev) / deploy-dev (push) Successful in 3m16s
Deploy Dashboard (Dev) / deploy-dev (push) Successful in 3m16s
Reuse the existing refresh-token flow before opening terminal websocket connections so reconnects can recover from expired access tokens without requiring a full dashboard reload. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,6 +54,15 @@ async function tryRefresh() {
|
|||||||
return refreshPromise;
|
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 = {}) {
|
async function request(path, opts = {}) {
|
||||||
const headers = opts.headers || {};
|
const headers = opts.headers || {};
|
||||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount, onDestroy } from "svelte";
|
import { onMount, onDestroy } from "svelte";
|
||||||
import { VoiceSession } from "../lib/voice.js";
|
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 = "__DASHBOARD_PERSISTENCE__:";
|
||||||
const PERSISTENCE_STATUS_PREFIX_BYTES = new TextEncoder().encode(PERSISTENCE_STATUS_PREFIX);
|
const PERSISTENCE_STATUS_PREFIX_BYTES = new TextEncoder().encode(PERSISTENCE_STATUS_PREFIX);
|
||||||
@@ -189,11 +189,11 @@
|
|||||||
fit.fit();
|
fit.fit();
|
||||||
|
|
||||||
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
const token = getToken() || "";
|
|
||||||
let heartbeat = null;
|
let heartbeat = null;
|
||||||
let reconnectTimer = null;
|
let reconnectTimer = null;
|
||||||
let closedManually = false;
|
let closedManually = false;
|
||||||
let reconnecting = false;
|
let reconnecting = false;
|
||||||
|
let authRecoveryInFlight = false;
|
||||||
|
|
||||||
function clearHeartbeat() {
|
function clearHeartbeat() {
|
||||||
if (heartbeat) {
|
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) {
|
function scheduleReconnect(reason) {
|
||||||
clearHeartbeat();
|
clearHeartbeat();
|
||||||
if (closedManually || reconnectTimer) return;
|
if (closedManually || reconnectTimer) return;
|
||||||
@@ -215,7 +238,7 @@
|
|||||||
reconnectTimer = setTimeout(() => {
|
reconnectTimer = setTimeout(() => {
|
||||||
reconnectTimer = null;
|
reconnectTimer = null;
|
||||||
if (!tabData.has(tabId) || closedManually) return;
|
if (!tabData.has(tabId) || closedManually) return;
|
||||||
connect();
|
connect(true);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,8 +252,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function connect() {
|
async function connect(forceRefresh = false) {
|
||||||
const token = getToken() || "";
|
const token = await getWebSocketToken(forceRefresh);
|
||||||
|
if (!token) {
|
||||||
|
handleAuthExpired();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const ws = new WebSocket(
|
const ws = new WebSocket(
|
||||||
`${proto}//${location.host}/ws/terminal?host=${encodeURIComponent(tab.hostId)}&token=${encodeURIComponent(token)}`
|
`${proto}//${location.host}/ws/terminal?host=${encodeURIComponent(tab.hostId)}&token=${encodeURIComponent(token)}`
|
||||||
);
|
);
|
||||||
@@ -272,10 +300,30 @@
|
|||||||
}
|
}
|
||||||
term.write(data);
|
term.write(data);
|
||||||
};
|
};
|
||||||
ws.onclose = (e) => {
|
ws.onclose = async (e) => {
|
||||||
const reason = e.reason || `Connection closed (code ${e.code})`;
|
const reason = e.reason || `Connection closed (code ${e.code})`;
|
||||||
const d = tabData.get(tabId);
|
const d = tabData.get(tabId);
|
||||||
if (d) d.ws = null;
|
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);
|
if (!closedManually) scheduleReconnect(reason);
|
||||||
else {
|
else {
|
||||||
clearHeartbeat();
|
clearHeartbeat();
|
||||||
@@ -301,7 +349,11 @@
|
|||||||
return ws;
|
return ws;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ws = connect();
|
let ws = null;
|
||||||
|
void connect().then((connectedWs) => {
|
||||||
|
ws = connectedWs;
|
||||||
|
if (connectedWs) voice.attach(term, connectedWs);
|
||||||
|
});
|
||||||
term.onData((data) => {
|
term.onData((data) => {
|
||||||
const currentWs = tabData.get(tabId)?.ws;
|
const currentWs = tabData.get(tabId)?.ws;
|
||||||
if (currentWs?.readyState === 1) currentWs.send(new TextEncoder().encode(data));
|
if (currentWs?.readyState === 1) currentWs.send(new TextEncoder().encode(data));
|
||||||
|
|||||||
Reference in New Issue
Block a user