feat(db,inspector): add snapshotSession + restore hookBufferUpdate spec contract (M1+M2)

- Add `snapshotSession(sessionId)` to inspectorSessions.ts per master-plan §3.8:
  returns parsed InterceptedRequest[] in seq order, or null for non-existent sessions.
  Silently skips rows that fail InterceptedRequestSchema validation (defensive).
- Restore canonical no-arg form of `MitmHandlerBase.hookBufferUpdate(intercepted)`
  per master-plan §3.5: when opts is omitted, derive completion fields from
  the intercepted object itself (status/responseHeaders/responseBody/responseSize/
  *LatencyMs) rather than no-op'ing. Extended opts form preserved for legacy callers.
- Update Zod record() calls in InterceptedRequestSchema to current (key,value) signature.
- Add 3 unit tests for snapshotSession (happy path / non-existent / silent skip).
- Add 2 unit tests for hookBufferUpdate (no-arg form + extended opts form).
This commit is contained in:
diegosouzapw
2026-05-28 17:07:01 -03:00
parent f6ed411c62
commit 907075f704
5 changed files with 155 additions and 4 deletions

View File

@@ -6,6 +6,8 @@
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;
@@ -115,3 +117,41 @@ export function deleteSession(id: string): void {
// 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;
}

View File

@@ -250,6 +250,13 @@ export abstract class MitmHandlerBase {
/**
* Update a previously published Traffic Inspector entry with completion data.
* No-op when the inspector module is not present.
*
* Per master-plan §3.5: the canonical no-arg form `hookBufferUpdate(intercepted)`
* must update the buffer using completion fields already present on `intercepted`
* (status, responseBody, responseHeaders, responseSize, *LatencyMs). When the
* extended `opts` form is used (legacy internal callers), it overrides those
* fields explicitly. Both forms route through `recordRequestComplete` so the
* inspector receives a consistent shape.
*/
protected hookBufferUpdate(
intercepted: InterceptedRequest,
@@ -262,11 +269,18 @@ export abstract class MitmHandlerBase {
upstreamLatencyMs: number;
},
): void {
if (!opts) return;
const finalOpts = opts ?? {
status: typeof intercepted.status === "number" ? intercepted.status : 0,
responseHeaders: intercepted.responseHeaders,
responseBody: intercepted.responseBody,
responseSize: intercepted.responseSize,
proxyLatencyMs: intercepted.proxyLatencyMs ?? 0,
upstreamLatencyMs: intercepted.upstreamLatencyMs ?? 0,
};
void loadAgentBridgeHook().then((hook) => {
if (hook?.recordRequestComplete) {
try {
hook.recordRequestComplete(intercepted, opts);
hook.recordRequestComplete(intercepted, finalOpts);
} catch {
// Hook should never break interception.
}

View File

@@ -39,10 +39,10 @@ export const InterceptedRequestSchema = z.object({
method: z.string(),
host: z.string(),
path: z.string(),
requestHeaders: z.record(z.string()),
requestHeaders: z.record(z.string(), z.string()),
requestBody: z.string().nullable(),
requestSize: z.number().int().nonnegative(),
responseHeaders: z.record(z.string()),
responseHeaders: z.record(z.string(), z.string()),
responseBody: z.string().nullable(),
responseSize: z.number().int().nonnegative(),
status: z.union([z.number().int(), z.literal("in-flight"), z.literal("error")]),