/** * Database module: InspectorSessions * CRUD + snapshot for inspector_sessions and inspector_session_requests tables. */ import { randomUUID } from "crypto"; import { getDbInstance } from "./core"; import type { InspectorSessionRow } from "./_rowTypes"; import { InterceptedRequestSchema } from "../../mitm/inspector/types"; import type { InterceptedRequest } from "../../mitm/inspector/types"; interface InspectorSessionDbRow { id: string; name: string | null; started_at: string; ended_at: string | null; request_count: number; profile: string | null; } interface InspectorSessionRequestDbRow { session_id: string; seq: number; payload: string; } function mapSessionRow(row: InspectorSessionDbRow): InspectorSessionRow { return { id: row.id, name: row.name, started_at: row.started_at, ended_at: row.ended_at, request_count: row.request_count, profile: row.profile as "llm" | "custom" | "all" | null, }; } export function createSession(opts?: { name?: string; profile?: "llm" | "custom" | "all"; }): { id: string; started_at: string } { const db = getDbInstance(); const id = randomUUID(); const started_at = new Date().toISOString(); db.prepare( `INSERT INTO inspector_sessions (id, name, started_at, profile) VALUES (?, ?, ?, ?)` ).run(id, opts?.name ?? null, started_at, opts?.profile ?? null); return { id, started_at }; } export function stopSession(id: string): void { const db = getDbInstance(); const ended_at = new Date().toISOString(); db.prepare("UPDATE inspector_sessions SET ended_at = ? WHERE id = ?").run(ended_at, id); } export function renameSession(id: string, name: string): void { const db = getDbInstance(); db.prepare("UPDATE inspector_sessions SET name = ? WHERE id = ?").run(name, id); } export function listSessions(): InspectorSessionRow[] { const db = getDbInstance(); const rows = db .prepare("SELECT * FROM inspector_sessions ORDER BY started_at DESC") .all() as InspectorSessionDbRow[]; return rows.map(mapSessionRow); } export function getSession(id: string): InspectorSessionRow | null { const db = getDbInstance(); const row = db .prepare("SELECT * FROM inspector_sessions WHERE id = ?") .get(id) as InspectorSessionDbRow | undefined; return row ? mapSessionRow(row) : null; } export function appendSessionRequest(sessionId: string, payload: string): number { const db = getDbInstance(); let insertedSeq = 0; const runTransaction = db.transaction(() => { // Get next seq atomically within transaction const seqRow = db .prepare( "SELECT COALESCE(MAX(seq), 0) + 1 AS next_seq FROM inspector_session_requests WHERE session_id = ?" ) .get(sessionId) as { next_seq: number }; const nextSeq = seqRow.next_seq; db.prepare( `INSERT INTO inspector_session_requests (session_id, seq, payload) VALUES (?, ?, ?)` ).run(sessionId, nextSeq, payload); db.prepare( "UPDATE inspector_sessions SET request_count = request_count + 1 WHERE id = ?" ).run(sessionId); insertedSeq = nextSeq; }); runTransaction(); return insertedSeq; } export function getSessionRequests(sessionId: string): Array<{ seq: number; payload: string }> { const db = getDbInstance(); const rows = db .prepare( "SELECT seq, payload FROM inspector_session_requests WHERE session_id = ? ORDER BY seq ASC" ) .all(sessionId) as InspectorSessionRequestDbRow[]; return rows.map((r) => ({ seq: r.seq, payload: r.payload })); } export function deleteSession(id: string): void { const db = getDbInstance(); // Cascade via FK ON DELETE CASCADE for inspector_session_requests db.prepare("DELETE FROM inspector_sessions WHERE id = ?").run(id); } /** * Return a parsed + validated snapshot of all requests for the given session, * sorted by ascending seq. * * Returns null when the session does not exist. * Rows whose payload fails InterceptedRequestSchema validation are silently * skipped (defensive — protects callers from corrupt/partial rows). * * Satisfies master-plan §3.8 (F2 spec) named-export contract. */ export function snapshotSession(sessionId: string): InterceptedRequest[] | null { // 1. Verify session exists. const session = getSession(sessionId); if (session === null) return null; // 2. Retrieve raw rows (already ordered by seq ASC). const rawRows = getSessionRequests(sessionId); // 3. Parse each payload JSON, validate via Zod schema, skip bad rows. const results: InterceptedRequest[] = []; for (const row of rawRows) { let parsed: unknown; try { parsed = JSON.parse(row.payload); } catch { // Corrupt JSON — skip. continue; } const result = InterceptedRequestSchema.safeParse(parsed); if (result.success) { results.push(result.data as InterceptedRequest); } // Invalid rows are silently skipped per defensive contract. } return results; }