/** * Shared SSE-flow helpers. The stream auth flow (mint a single-use ticket, open * with `?ticket=`) is security-sensitive and recently changed from `?token=`, so * it lives in one place instead of being re-inlined per spec. */ import type { Page } from '@playwright/test'; const BASE = process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101'; /** Exchange a JWT for a single-use SSE ticket via POST /api/v1/stream/ticket. */ export async function mintSseTicket(jwt: string): Promise { const res = await fetch(`${BASE}/api/v1/stream/ticket`, { method: 'POST', headers: { Authorization: `Bearer ${jwt}` }, }); if (res.status !== 200) throw new Error(`mintSseTicket failed: ${res.status} ${await res.text()}`); return (await res.json()).ticket; } /** Open the SSE stream with a ticket, return the HTTP status, and tear the stream down. */ export async function openStream(ticket: string): Promise { const c = new AbortController(); try { const res = await fetch(`${BASE}/api/v1/stream?ticket=${encodeURIComponent(ticket)}`, { signal: c.signal, }); return res.status; } finally { c.abort(); } } /** * Count EventSource opens (GET /api/v1/stream?ticket=…) on a page — NOT the ticket * POST. Returns a getter for the running count. */ export function trackStreamOpens(page: Page): () => number { let n = 0; page.on('request', (req) => { if (req.method() === 'GET' && req.url().includes('/api/v1/stream?')) n++; }); return () => n; }