add /memory endpoint (SSE-session-cookie gated)

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) <noreply@anthropic.com>
This commit is contained in:
Garry Tan 2026-05-27 07:29:25 -07:00
parent aa3121a794
commit 10495978e6
No known key found for this signature in database
GPG Key ID: C1F69E85C74EFE1D
1 changed files with 26 additions and 0 deletions

View File

@ -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) // GET /inspector/events — SSE for inspector state changes (auth required)
if (url.pathname === '/inspector/events' && req.method === 'GET') { if (url.pathname === '/inspector/events' && req.method === 'GET') {
// Same auth model as /activity/stream: Bearer OR view-only cookie. // Same auth model as /activity/stream: Bearer OR view-only cookie.