mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
fix: stale auth token causes Unauthorized + invisible error text
background.js checkHealth() never refreshed authToken from /health responses, so when the browse server restarted with a new token, all sidebar-command requests got 401 Unauthorized forever. Also: error placeholder text was #3f3f46 on #0C0C0C (nearly invisible). Now shows in red to match the error border. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+68
-23
@@ -45,7 +45,9 @@ async function loadAuthToken() {
|
||||
const data = await resp.json();
|
||||
if (data.token) authToken = data.token;
|
||||
}
|
||||
} catch {}
|
||||
} catch (err) {
|
||||
console.error('[gstack bg] Failed to load auth token:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Health Polling ────────────────────────────────────────────
|
||||
@@ -65,12 +67,16 @@ async function checkHealth() {
|
||||
if (!resp.ok) { setDisconnected(); return; }
|
||||
const data = await resp.json();
|
||||
if (data.status === 'healthy') {
|
||||
// Always refresh auth token from /health — the server generates a new
|
||||
// token on each restart, so the old one becomes stale.
|
||||
if (data.token) authToken = data.token;
|
||||
// Forward chatEnabled so sidepanel can show/hide chat tab
|
||||
setConnected({ ...data, chatEnabled: !!data.chatEnabled });
|
||||
} else {
|
||||
setDisconnected();
|
||||
}
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.error('[gstack bg] Health check failed:', err.message);
|
||||
setDisconnected();
|
||||
}
|
||||
}
|
||||
@@ -82,7 +88,9 @@ function setConnected(healthData) {
|
||||
chrome.action.setBadgeText({ text: ' ' });
|
||||
|
||||
// Broadcast health to popup and side panel (include token for sidepanel auth)
|
||||
chrome.runtime.sendMessage({ type: 'health', data: { ...healthData, token: authToken } }).catch(() => {});
|
||||
chrome.runtime.sendMessage({ type: 'health', data: { ...healthData, token: authToken } }).catch((err) => {
|
||||
console.debug('[gstack bg] No listener for health broadcast:', err.message);
|
||||
});
|
||||
|
||||
// Notify content scripts on connection change
|
||||
if (wasDisconnected) {
|
||||
@@ -96,7 +104,9 @@ function setDisconnected() {
|
||||
// Keep authToken — it persists across reconnections
|
||||
chrome.action.setBadgeText({ text: '' });
|
||||
|
||||
chrome.runtime.sendMessage({ type: 'health', data: null }).catch(() => {});
|
||||
chrome.runtime.sendMessage({ type: 'health', data: null }).catch((err) => {
|
||||
console.debug('[gstack bg] No listener for disconnect broadcast:', err.message);
|
||||
});
|
||||
|
||||
// Notify content scripts on disconnection
|
||||
if (wasConnected) {
|
||||
@@ -109,10 +119,14 @@ async function notifyContentScripts(type) {
|
||||
const tabs = await chrome.tabs.query({});
|
||||
for (const tab of tabs) {
|
||||
if (tab.id) {
|
||||
chrome.tabs.sendMessage(tab.id, { type }).catch(() => {});
|
||||
chrome.tabs.sendMessage(tab.id, { type }).catch(() => {
|
||||
// Expected: tabs without content script
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch (err) {
|
||||
console.error('[gstack bg] Failed to query tabs for notification:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Command Proxy ─────────────────────────────────────────────
|
||||
@@ -150,17 +164,24 @@ async function fetchAndRelayRefs() {
|
||||
const headers = {};
|
||||
if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
|
||||
const resp = await fetch(`${base}/refs`, { signal: AbortSignal.timeout(3000), headers });
|
||||
if (!resp.ok) return;
|
||||
if (!resp.ok) {
|
||||
console.warn(`[gstack bg] Refs endpoint returned ${resp.status}`);
|
||||
return;
|
||||
}
|
||||
const data = await resp.json();
|
||||
|
||||
// Send to all tabs' content scripts
|
||||
const tabs = await chrome.tabs.query({});
|
||||
for (const tab of tabs) {
|
||||
if (tab.id) {
|
||||
chrome.tabs.sendMessage(tab.id, { type: 'refs', data }).catch(() => {});
|
||||
chrome.tabs.sendMessage(tab.id, { type: 'refs', data }).catch(() => {
|
||||
// Expected: tabs without content script
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch (err) {
|
||||
console.error('[gstack bg] Failed to fetch/relay refs:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Inspector ──────────────────────────────────────────────────
|
||||
@@ -181,21 +202,26 @@ async function injectInspector(tabId) {
|
||||
target: { tabId, allFrames: true },
|
||||
files: ['inspector.css'],
|
||||
});
|
||||
} catch {}
|
||||
} catch (err) {
|
||||
console.debug('[gstack bg] Inspector CSS injection failed (non-fatal):', err.message);
|
||||
}
|
||||
// Send startPicker to the injected inspector.js
|
||||
try {
|
||||
await chrome.tabs.sendMessage(tabId, { type: 'startPicker' });
|
||||
} catch {}
|
||||
} catch (err) {
|
||||
console.warn('[gstack bg] Failed to send startPicker:', err.message);
|
||||
}
|
||||
inspectorMode = 'full';
|
||||
return { ok: true, mode: 'full' };
|
||||
} catch {
|
||||
} catch (err) {
|
||||
// Script injection failed (CSP, chrome:// page, etc.)
|
||||
// Fall back to content.js basic picker (loaded by manifest on most pages)
|
||||
try {
|
||||
await chrome.tabs.sendMessage(tabId, { type: 'startBasicPicker' });
|
||||
inspectorMode = 'basic';
|
||||
return { ok: true, mode: 'basic' };
|
||||
} catch {
|
||||
} catch (err2) {
|
||||
console.error('[gstack bg] Inspector injection failed completely:', err.message, '| Basic fallback:', err2.message);
|
||||
inspectorMode = 'full';
|
||||
return { error: 'Cannot inspect this page' };
|
||||
}
|
||||
@@ -205,7 +231,9 @@ async function injectInspector(tabId) {
|
||||
async function stopInspector(tabId) {
|
||||
try {
|
||||
await chrome.tabs.sendMessage(tabId, { type: 'stopPicker' });
|
||||
} catch {}
|
||||
} catch (err) {
|
||||
console.debug('[gstack bg] Failed to stop picker on tab', tabId, ':', err.message);
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -232,8 +260,8 @@ async function postInspectorPick(selector, frameInfo, basicData, activeTabUrl) {
|
||||
}
|
||||
const data = await resp.json();
|
||||
return { mode: 'cdp', ...data };
|
||||
} catch {
|
||||
// No server or timeout — fall back to basic mode
|
||||
} catch (err) {
|
||||
console.debug('[gstack bg] Inspector pick server unavailable, using basic mode:', err.message);
|
||||
return { mode: 'basic', selector, basicData, frameInfo };
|
||||
}
|
||||
}
|
||||
@@ -297,7 +325,9 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
// Open side panel from content script pill click
|
||||
if (msg.type === 'openSidePanel') {
|
||||
if (chrome.sidePanel?.open && sender.tab) {
|
||||
chrome.sidePanel.open({ tabId: sender.tab.id }).catch(() => {});
|
||||
chrome.sidePanel.open({ tabId: sender.tab.id }).catch((err) => {
|
||||
console.warn('[gstack bg] Failed to open side panel:', err.message);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -342,7 +372,9 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
basicData: msg.basicData,
|
||||
frameInfo,
|
||||
},
|
||||
}).catch(() => {});
|
||||
}).catch((err) => {
|
||||
console.warn('[gstack bg] Failed to forward inspectResult to sidepanel:', err.message);
|
||||
});
|
||||
sendResponse({ ok: true });
|
||||
});
|
||||
});
|
||||
@@ -351,7 +383,9 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
|
||||
// Inspector: picker cancelled
|
||||
if (msg.type === 'pickerCancelled') {
|
||||
chrome.runtime.sendMessage({ type: 'pickerCancelled' }).catch(() => {});
|
||||
chrome.runtime.sendMessage({ type: 'pickerCancelled' }).catch((err) => {
|
||||
console.debug('[gstack bg] No listener for pickerCancelled:', err.message);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -391,9 +425,18 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
},
|
||||
body: JSON.stringify({ message: msg.message, activeTabUrl }),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(r => {
|
||||
if (!r.ok) {
|
||||
console.error(`[gstack bg] sidebar-command failed: ${r.status} ${r.statusText}`);
|
||||
return r.json().catch(() => ({ error: `Server returned ${r.status}` }));
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then(data => sendResponse(data))
|
||||
.catch(err => sendResponse({ error: err.message }));
|
||||
.catch(err => {
|
||||
console.error('[gstack bg] sidebar-command error:', err.message);
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -403,7 +446,9 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
|
||||
// Click extension icon → open side panel directly (no popup)
|
||||
if (chrome.sidePanel && chrome.sidePanel.setPanelBehavior) {
|
||||
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {});
|
||||
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch((err) => {
|
||||
console.warn('[gstack bg] Failed to set panel behavior:', err.message);
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-open side panel with retry. chrome.sidePanel.open() can fail silently
|
||||
@@ -448,7 +493,7 @@ chrome.tabs.onActivated.addListener((activeInfo) => {
|
||||
tabId: activeInfo.tabId,
|
||||
url: tab.url || '',
|
||||
title: tab.title || '',
|
||||
}).catch(() => {}); // sidepanel may not be open
|
||||
}).catch(() => {}); // expected: sidepanel may not be open
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -689,6 +689,10 @@ body::after {
|
||||
border-color: var(--error);
|
||||
animation: shake 300ms ease;
|
||||
}
|
||||
.command-input.error::placeholder {
|
||||
color: var(--error);
|
||||
opacity: 0.8;
|
||||
}
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-4px); }
|
||||
|
||||
Reference in New Issue
Block a user