sse-session-cookie.ts
65 lines · browse/src/sse-session-cookie.ts at 3f79d81
1/**
2 * View-only session cookie registry for SSE endpoints.
3 *
4 * Why this exists: EventSource cannot send Authorization headers, so
5 * /activity/stream and /inspector/events historically took a `?token=`
6 * query param with the root AUTH_TOKEN. URLs leak through browser history,
7 * referer headers, server logs, crash reports, and refactoring accidents
8 * (Codex's plan-review outside voice called this out). This module issues
9 * a separate short-lived token, scoped to SSE reads only, delivered via
10 * an HttpOnly SameSite=Strict cookie that EventSource can pick up with
11 * `withCredentials: true`.
12 *
13 * Design notes:
14 * - TTL 30 minutes. Long enough for a normal coding session; short enough
15 * that a leaked cookie expires quickly.
16 * - Scope is implicit: validating a cookie only grants read access to
17 * /activity/stream and /inspector/events. The cookie is NEVER valid on
18 * /command, /token, or any mutating endpoint. Matches the
19 * cookie-picker-auth-isolation pattern (prior learning, 10/10 confidence):
20 * cookie-based session tokens must not be valid as scoped tokens.
21 * - In-memory only. No persistence across daemon restarts — extension
22 * re-mints on reconnect.
23 * - Tokens are 32 random bytes (URL-safe base64). 256 bits, unbruteforceable.
24 *
25 * Shares the registry implementation with pty-session-cookie.ts via
26 * createSessionCookieStore; separate INSTANCE so the token spaces never
27 * overlap.
28 */
29import { createSessionCookieStore } from './session-cookie-store';
31const TTL_MS = 30 * 60 * 1000; // 30 minutes
33export const SSE_COOKIE_NAME = 'gstack_sse';
35const store = createSessionCookieStore({ cookieName: SSE_COOKIE_NAME, ttlMs: TTL_MS });
37/** Mint a fresh view-only SSE session token. */
38export function mintSseSessionToken(): { token: string; expiresAt: number } {
39 return store.mint();
40}
42/**
43 * Validate a token. Returns true only if the token exists AND is not expired.
44 * Expired tokens are lazily removed, and we opportunistically prune a few
45 * additional expired entries on every validate so the registry can't grow
46 * unboundedly under sustained mint + reconnect pressure.
47 */
48export function validateSseSessionToken(token: string | null | undefined): boolean {
49 return store.validate(token);
50}
52/** Parse the SSE session token from a Cookie header. */
53export function extractSseCookie(req: Request): string | null {
54 return store.extract(req);
55}
57/** Build the Set-Cookie header value for the SSE session cookie. */
58export function buildSseSetCookie(token: string): string {
59 return store.buildSetCookie(token);
60}
62// Test-only reset.
63export function __resetSseSessions(): void {
64 store.__reset();
65}