From 10495978e61e6fa51985d7687dcafe68dbe4ff8d Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Wed, 27 May 2026 07:29:25 -0700 Subject: [PATCH] add /memory endpoint (SSE-session-cookie gated) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /memory returns the BrowserManager memory snapshot as JSON. Auth matches /activity/stream and /inspector/events: Bearer header OR view-only SSE-session cookie (the extension fetches the cookie once via POST /sse-session, then polls /memory with withCredentials: true). Deliberately NOT extending /health for the sidebar footer poll — TODOS.md "Audit /health token distribution" records that /health already surfaces AUTH_TOKEN to any localhost caller in headed mode. A separate endpoint with the standard SSE auth keeps the future /health fix from cascading into the sidebar. sanitizeReplacer is applied at egress because tab.url and tab.title come from page content — lone-surrogate bytes from broken emoji could otherwise reach the sidebar and (when forwarded to Claude API) trigger HTTP 400. Co-Authored-By: Claude Opus 4.7 (1M context) --- browse/src/server.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/browse/src/server.ts b/browse/src/server.ts index 3b0052d01..6f75551ff 100644 --- a/browse/src/server.ts +++ b/browse/src/server.ts @@ -2759,6 +2759,32 @@ export function buildFetchHandler(cfg: ServerConfig): ServerHandle { }); } + // GET /memory — diagnostic snapshot (auth required, does NOT reset idle). + // Same auth model as /activity/stream and /inspector/events: Bearer header + // OR view-only SSE-session cookie. Does NOT extend /health (which already + // leaks AUTH_TOKEN to any localhost caller in headed mode — see TODOS.md + // "Audit /health token distribution"); a separate endpoint with the + // standard SSE auth keeps the future /health fix from cascading into the + // sidebar footer poll. + if (url.pathname === '/memory' && req.method === 'GET') { + const cookieToken = extractSseCookie(req); + if (!validateAuth(req) && !validateSseSessionToken(cookieToken)) { + return new Response(JSON.stringify({ error: 'Unauthorized' }), { + status: 401, headers: { 'Content-Type': 'application/json' }, + }); + } + const { buildMemorySnapshotJson } = await import('./memory-command'); + const snapshot = await buildMemorySnapshotJson(cfgBrowserManager); + // sanitizeReplacer is required at every SSE/JSON egress that ships + // page-content-derived strings — tab.url and tab.title come from + // page content, so lone-surrogate bytes from broken emoji or + // mid-emoji splits could otherwise reach the sidebar / Claude API. + return new Response(JSON.stringify(snapshot, sanitizeReplacer), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + // GET /inspector/events — SSE for inspector state changes (auth required) if (url.pathname === '/inspector/events' && req.method === 'GET') { // Same auth model as /activity/stream: Bearer OR view-only cookie.