File size: 4,977 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
/**
 * 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;
}