mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-19 19:32:18 +02:00
fix(browse): extension token bootstrap moves to pinned-origin POST; /health carries no token
GET /health is now liveness/status only in every mode — both token carve-outs (headed-mode disjunct AND chrome-extension:// Origin disjunct) are removed. Token bootstrap is POST /extension-token on the local listener: the Origin header must be exactly chrome-extension://<GSTACK_EXTENSION_ID> and the Host header's hostname must parse to 127.0.0.1 or localhost (parsed via new URL, never literal equality — Host arrives as '127.0.0.1:34567'). Wrong origin/host → 403 with no detail. The tunnel surface 404s the endpoint (not in TUNNEL_PATHS, verified by test). The extension ID is pinned by a new "key" field (RSA public key) in extension/manifest.json; browse/scripts/extension-id.ts reproduces the ID derivation (first 16 bytes of SHA-256 of the DER public key, hex mapped 0-9a-f → a-p). The private key is not committed anywhere — unpacked/baked-in loads only need the public key. Extension side: background.js bootstraps and refreshes the token via POST /extension-token (403 → disconnected state); sidepanel.js direct connect path does the same; sidepanel-terminal.js's dead /health token fallback (read AUTH_TOKEN/authToken keys the server never sent, hardcoded port) is replaced with the window.gstackAuthToken path. MIGRATION NOTE: the manifest key pins the extension ID, so existing installs' side-panel local state (saved port, snoozes) resets once — explained in-product via a one-time notice (flag gstack_id_migrated_v162). After upgrading the server, restart the browser so the old service worker stops polling for a token GET /health no longer serves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit e9a0b6847a2d17fe6656a4686b4efd0c8380eb09)
This commit is contained in:
+44
-12
@@ -1304,21 +1304,36 @@ async function tryConnect() {
|
||||
});
|
||||
if (healthResp.ok) {
|
||||
const data = await healthResp.json();
|
||||
if (data.status === 'healthy' && data.token) {
|
||||
if (data.status === 'healthy') {
|
||||
// /health is liveness-only — the token comes from the pinned-origin
|
||||
// POST /extension-token bootstrap (our chrome-extension:// Origin
|
||||
// is validated server-side against the manifest-pinned ID).
|
||||
const tokenResp = await fetch(`http://127.0.0.1:${port}/extension-token`, {
|
||||
method: 'POST',
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
const tokenData = tokenResp.ok ? await tokenResp.json() : null;
|
||||
if (tokenData?.token) {
|
||||
setLoadingStatus(
|
||||
`Server healthy on port ${port}, connecting...`,
|
||||
`token: yes (from /extension-token)\nStarting SSE + activity feed...`
|
||||
);
|
||||
updateConnection(`http://127.0.0.1:${port}`, tokenData.token);
|
||||
// The SEC shield used to drive off /health.security via the chat
|
||||
// path's classifier; with the chat path ripped, the indicator is
|
||||
// not driven yet. Leaving the shield element hidden by default.
|
||||
return;
|
||||
}
|
||||
setLoadingStatus(
|
||||
`Server healthy on port ${port}, connecting...`,
|
||||
`token: yes (from /health)\nStarting SSE + activity feed...`
|
||||
`Server healthy but token bootstrap failed (attempt ${connectAttempts})`,
|
||||
`POST /extension-token → ${tokenResp.status}${tokenResp.status === 403 ? ' (extension identity not trusted)' : ''}`
|
||||
);
|
||||
} else {
|
||||
setLoadingStatus(
|
||||
`Server responded but not healthy (attempt ${connectAttempts})`,
|
||||
`status: ${data.status}`
|
||||
);
|
||||
updateConnection(`http://127.0.0.1:${port}`, data.token);
|
||||
// The SEC shield used to drive off /health.security via the chat
|
||||
// path's classifier; with the chat path ripped, the indicator is
|
||||
// not driven yet. Leaving the shield element hidden by default.
|
||||
return;
|
||||
}
|
||||
setLoadingStatus(
|
||||
`Server responded but not healthy (attempt ${connectAttempts})`,
|
||||
`status: ${data.status}\ntoken: ${data.token ? 'yes' : 'no'}`
|
||||
);
|
||||
} else {
|
||||
setLoadingStatus(
|
||||
`Server returned ${healthResp.status} (attempt ${connectAttempts})`,
|
||||
@@ -1355,6 +1370,23 @@ chrome.runtime.onMessage.addListener((msg) => {
|
||||
fetchRefs();
|
||||
}
|
||||
}
|
||||
// One-time v1.62 identity-pin notice from background.js. Transient banner —
|
||||
// no dedicated element in sidepanel.html since this fires once per install.
|
||||
if (msg.type === 'gstack-migration-notice' && msg.message) {
|
||||
console.log('[gstack sidebar]', msg.message);
|
||||
try {
|
||||
const banner = document.createElement('div');
|
||||
banner.textContent = msg.message;
|
||||
banner.style.cssText =
|
||||
'position:fixed;left:8px;right:8px;bottom:40px;z-index:9999;' +
|
||||
'background:#1f2937;color:#f5a623;border:1px solid #f5a623;' +
|
||||
'border-radius:6px;padding:8px 10px;font-size:12px;text-align:left;';
|
||||
document.body.appendChild(banner);
|
||||
setTimeout(() => banner.remove(), 8000);
|
||||
} catch (err) {
|
||||
console.debug('[gstack sidebar] migration banner failed:', err && err.message);
|
||||
}
|
||||
}
|
||||
if (msg.type === 'inspectResult') {
|
||||
inspectorPickerActive = false;
|
||||
inspectorPickBtn.classList.remove('active');
|
||||
|
||||
Reference in New Issue
Block a user