mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-22 12:48:26 +08: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:
@@ -45,7 +45,9 @@ async function loadAuthToken() {
|
|||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.token) authToken = data.token;
|
if (data.token) authToken = data.token;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch (err) {
|
||||||
|
console.error('[gstack bg] Failed to load auth token:', err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Health Polling ────────────────────────────────────────────
|
// ─── Health Polling ────────────────────────────────────────────
|
||||||
@@ -65,12 +67,16 @@ async function checkHealth() {
|
|||||||
if (!resp.ok) { setDisconnected(); return; }
|
if (!resp.ok) { setDisconnected(); return; }
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.status === 'healthy') {
|
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
|
// Forward chatEnabled so sidepanel can show/hide chat tab
|
||||||
setConnected({ ...data, chatEnabled: !!data.chatEnabled });
|
setConnected({ ...data, chatEnabled: !!data.chatEnabled });
|
||||||
} else {
|
} else {
|
||||||
setDisconnected();
|
setDisconnected();
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
console.error('[gstack bg] Health check failed:', err.message);
|
||||||
setDisconnected();
|
setDisconnected();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +88,9 @@ function setConnected(healthData) {
|
|||||||
chrome.action.setBadgeText({ text: ' ' });
|
chrome.action.setBadgeText({ text: ' ' });
|
||||||
|
|
||||||
// Broadcast health to popup and side panel (include token for sidepanel auth)
|
// 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
|
// Notify content scripts on connection change
|
||||||
if (wasDisconnected) {
|
if (wasDisconnected) {
|
||||||
@@ -96,7 +104,9 @@ function setDisconnected() {
|
|||||||
// Keep authToken — it persists across reconnections
|
// Keep authToken — it persists across reconnections
|
||||||
chrome.action.setBadgeText({ text: '' });
|
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
|
// Notify content scripts on disconnection
|
||||||
if (wasConnected) {
|
if (wasConnected) {
|
||||||
@@ -109,10 +119,14 @@ async function notifyContentScripts(type) {
|
|||||||
const tabs = await chrome.tabs.query({});
|
const tabs = await chrome.tabs.query({});
|
||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
if (tab.id) {
|
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 ─────────────────────────────────────────────
|
// ─── Command Proxy ─────────────────────────────────────────────
|
||||||
@@ -150,17 +164,24 @@ async function fetchAndRelayRefs() {
|
|||||||
const headers = {};
|
const headers = {};
|
||||||
if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
|
if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
|
||||||
const resp = await fetch(`${base}/refs`, { signal: AbortSignal.timeout(3000), headers });
|
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();
|
const data = await resp.json();
|
||||||
|
|
||||||
// Send to all tabs' content scripts
|
// Send to all tabs' content scripts
|
||||||
const tabs = await chrome.tabs.query({});
|
const tabs = await chrome.tabs.query({});
|
||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
if (tab.id) {
|
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 ──────────────────────────────────────────────────
|
// ─── Inspector ──────────────────────────────────────────────────
|
||||||
@@ -181,21 +202,26 @@ async function injectInspector(tabId) {
|
|||||||
target: { tabId, allFrames: true },
|
target: { tabId, allFrames: true },
|
||||||
files: ['inspector.css'],
|
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
|
// Send startPicker to the injected inspector.js
|
||||||
try {
|
try {
|
||||||
await chrome.tabs.sendMessage(tabId, { type: 'startPicker' });
|
await chrome.tabs.sendMessage(tabId, { type: 'startPicker' });
|
||||||
} catch {}
|
} catch (err) {
|
||||||
|
console.warn('[gstack bg] Failed to send startPicker:', err.message);
|
||||||
|
}
|
||||||
inspectorMode = 'full';
|
inspectorMode = 'full';
|
||||||
return { ok: true, mode: 'full' };
|
return { ok: true, mode: 'full' };
|
||||||
} catch {
|
} catch (err) {
|
||||||
// Script injection failed (CSP, chrome:// page, etc.)
|
// Script injection failed (CSP, chrome:// page, etc.)
|
||||||
// Fall back to content.js basic picker (loaded by manifest on most pages)
|
// Fall back to content.js basic picker (loaded by manifest on most pages)
|
||||||
try {
|
try {
|
||||||
await chrome.tabs.sendMessage(tabId, { type: 'startBasicPicker' });
|
await chrome.tabs.sendMessage(tabId, { type: 'startBasicPicker' });
|
||||||
inspectorMode = 'basic';
|
inspectorMode = 'basic';
|
||||||
return { ok: true, mode: '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';
|
inspectorMode = 'full';
|
||||||
return { error: 'Cannot inspect this page' };
|
return { error: 'Cannot inspect this page' };
|
||||||
}
|
}
|
||||||
@@ -205,7 +231,9 @@ async function injectInspector(tabId) {
|
|||||||
async function stopInspector(tabId) {
|
async function stopInspector(tabId) {
|
||||||
try {
|
try {
|
||||||
await chrome.tabs.sendMessage(tabId, { type: 'stopPicker' });
|
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 };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,8 +260,8 @@ async function postInspectorPick(selector, frameInfo, basicData, activeTabUrl) {
|
|||||||
}
|
}
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
return { mode: 'cdp', ...data };
|
return { mode: 'cdp', ...data };
|
||||||
} catch {
|
} catch (err) {
|
||||||
// No server or timeout — fall back to basic mode
|
console.debug('[gstack bg] Inspector pick server unavailable, using basic mode:', err.message);
|
||||||
return { mode: 'basic', selector, basicData, frameInfo };
|
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
|
// Open side panel from content script pill click
|
||||||
if (msg.type === 'openSidePanel') {
|
if (msg.type === 'openSidePanel') {
|
||||||
if (chrome.sidePanel?.open && sender.tab) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -342,7 +372,9 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
basicData: msg.basicData,
|
basicData: msg.basicData,
|
||||||
frameInfo,
|
frameInfo,
|
||||||
},
|
},
|
||||||
}).catch(() => {});
|
}).catch((err) => {
|
||||||
|
console.warn('[gstack bg] Failed to forward inspectResult to sidepanel:', err.message);
|
||||||
|
});
|
||||||
sendResponse({ ok: true });
|
sendResponse({ ok: true });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -351,7 +383,9 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
|
|
||||||
// Inspector: picker cancelled
|
// Inspector: picker cancelled
|
||||||
if (msg.type === 'pickerCancelled') {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,9 +425,18 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({ message: msg.message, activeTabUrl }),
|
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))
|
.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;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -403,7 +446,9 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
|
|
||||||
// Click extension icon → open side panel directly (no popup)
|
// Click extension icon → open side panel directly (no popup)
|
||||||
if (chrome.sidePanel && chrome.sidePanel.setPanelBehavior) {
|
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
|
// Auto-open side panel with retry. chrome.sidePanel.open() can fail silently
|
||||||
@@ -448,7 +493,7 @@ chrome.tabs.onActivated.addListener((activeInfo) => {
|
|||||||
tabId: activeInfo.tabId,
|
tabId: activeInfo.tabId,
|
||||||
url: tab.url || '',
|
url: tab.url || '',
|
||||||
title: tab.title || '',
|
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);
|
border-color: var(--error);
|
||||||
animation: shake 300ms ease;
|
animation: shake 300ms ease;
|
||||||
}
|
}
|
||||||
|
.command-input.error::placeholder {
|
||||||
|
color: var(--error);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
@keyframes shake {
|
@keyframes shake {
|
||||||
0%, 100% { transform: translateX(0); }
|
0%, 100% { transform: translateX(0); }
|
||||||
25% { transform: translateX(-4px); }
|
25% { transform: translateX(-4px); }
|
||||||
|
|||||||
Reference in New Issue
Block a user