"use strict"; /** * SessionStore — manages active sessions and persists to SQLite. * In-memory map for running engines; DB for durable storage. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SessionStore = void 0; const path_1 = __importDefault(require("path")); const ExplorationEngine_1 = require("../core/ExplorationEngine"); const StateGraph_1 = require("../core/StateGraph"); const PlaywrightAgent_1 = require("../plugins/agents/PlaywrightAgent"); const ScreenshotCollector_1 = require("../plugins/collectors/ScreenshotCollector"); const NetworkCollector_1 = require("../plugins/collectors/NetworkCollector"); const DOMSnapshotCollector_1 = require("../plugins/collectors/DOMSnapshotCollector"); const MarkdownExporter_1 = require("../plugins/exporters/MarkdownExporter"); const JSONExporter_1 = require("../plugins/exporters/JSONExporter"); const PlaywrightReproducer_1 = require("../plugins/reproducers/PlaywrightReproducer"); const FuzzingEngine_1 = require("../plugins/fuzzers/FuzzingEngine"); const VisualRegressionCollector_1 = require("../plugins/collectors/VisualRegressionCollector"); const AccessibilityCollector_1 = require("../plugins/collectors/AccessibilityCollector"); const PerformanceCollector_1 = require("../plugins/collectors/PerformanceCollector"); class SessionStore { constructor(outputDir = './reports', sessionRepo, anomalyRepo, maxConcurrentSessions = 3, notificationService, visualRepo) { this.sessions = new Map(); this.emitter = () => undefined; /** In-memory performance metrics keyed by sessionId */ this.performanceMetrics = new Map(); this.outputDir = outputDir; this.sessionRepo = sessionRepo ?? null; this.anomalyRepo = anomalyRepo ?? null; this.maxConcurrentSessions = maxConcurrentSessions; this.notificationService = notificationService ?? null; this.visualRepo = visualRepo ?? null; } getPerformanceMetrics(sessionId) { return this.performanceMetrics.get(sessionId) ?? []; } getMaxConcurrent() { return this.maxConcurrentSessions; } setEmitter(emitter) { this.emitter = emitter; } getAllSessions() { if (this.sessionRepo) { const rows = this.sessionRepo.findAll(); return rows.map((r) => { const live = this.sessions.get(r.id); return { sessionId: r.id, url: r.url, seed: r.seed, maxStates: r.max_states, status: r.status, startedAt: new Date(r.started_at).toISOString(), finishedAt: r.finished_at ? new Date(r.finished_at).toISOString() : undefined, statesVisited: r.states_visited, anomaliesFound: r.anomalies_found, anomalies: live?.anomalies ?? [], engine: live?.engine, }; }); } return Array.from(this.sessions.values()); } getSession(sessionId) { if (this.sessionRepo) { const r = this.sessionRepo.findById(sessionId); if (!r) return undefined; const live = this.sessions.get(sessionId); return { sessionId: r.id, url: r.url, seed: r.seed, maxStates: r.max_states, status: r.status, startedAt: new Date(r.started_at).toISOString(), finishedAt: r.finished_at ? new Date(r.finished_at).toISOString() : undefined, statesVisited: r.states_visited, anomaliesFound: r.anomalies_found, anomalies: live?.anomalies ?? [], engine: live?.engine, }; } return this.sessions.get(sessionId); } getAllAnomalies(sessionId, severity) { if (this.anomalyRepo) { return this.anomalyRepo.findAll({ sessionId, severity }); } const all = Array.from(this.sessions.values()).flatMap((s) => s.anomalies); return all .filter((a) => !sessionId || this.findSessionForAnomaly(a.id) === sessionId) .filter((a) => !severity || a.severity === severity); } getAnomaly(anomalyId) { if (this.anomalyRepo) { return this.anomalyRepo.findById(anomalyId) ?? undefined; } for (const session of this.sessions.values()) { const found = session.anomalies.find((a) => a.id === anomalyId); if (found) return found; } return undefined; } findSessionForAnomaly(anomalyId) { if (this.anomalyRepo) { const a = this.anomalyRepo.findById(anomalyId); return a?.sessionId; } for (const session of this.sessions.values()) { if (session.anomalies.some((a) => a.id === anomalyId)) return session.sessionId; } return undefined; } screenshotPath(anomalyId) { const anomaly = this.getAnomaly(anomalyId); if (!anomaly?.evidence.screenshotPath) return undefined; const sessionId = this.findSessionForAnomaly(anomalyId); if (!sessionId) return undefined; return path_1.default.resolve(this.outputDir, anomalyId, anomaly.evidence.screenshotPath); } stopSession(sessionId) { const record = this.sessions.get(sessionId); if (!record || record.status !== 'running') return false; record.engine?.stop(); record.status = 'stopped'; record.finishedAt = new Date().toISOString(); this.sessionRepo?.update(sessionId, { status: 'stopped', finishedAt: Date.now() }); return true; } getStats() { if (this.sessionRepo && this.anomalyRepo) { const rows = this.sessionRepo.findAll(); return { totalSessions: rows.length, totalAnomalies: this.anomalyRepo.count(), criticalHighCount: this.anomalyRepo.countBySeverity(['high', 'critical']), runningSessions: rows.filter((r) => r.status === 'running').length, }; } const sessions = Array.from(this.sessions.values()); const anomalies = sessions.flatMap((s) => s.anomalies); return { totalSessions: sessions.length, totalAnomalies: anomalies.length, criticalHighCount: anomalies.filter((a) => a.severity === 'high' || a.severity === 'critical').length, runningSessions: sessions.filter((s) => s.status === 'running').length, }; } async startSession(params) { const sessionId = `sess_${Date.now()}_${params.seed}`; const startedAt = new Date().toISOString(); const startedAtMs = Date.now(); const record = { sessionId, url: params.url, seed: params.seed, maxStates: params.maxStates, status: 'running', startedAt, statesVisited: 0, anomaliesFound: 0, anomalies: [], }; this.sessions.set(sessionId, record); this.sessionRepo?.create({ id: sessionId, url: params.url, seed: params.seed, maxStates: params.maxStates, startedAt: startedAtMs, configJson: params.explorationConfig ? JSON.stringify(params.explorationConfig) : '{}', }); this.emitter('session:started', { sessionId, url: params.url }); const graph = new StateGraph_1.StateGraph(); const agent = new PlaywrightAgent_1.PlaywrightAgent({ seed: params.seed, explorationConfig: params.explorationConfig, }); const fuzzingEnabled = params.explorationConfig?.fuzzingEnabled !== false; const fuzzingIntensity = params.explorationConfig?.fuzzingIntensity ?? 'medium'; const fuzzingPlugin = fuzzingEnabled ? new FuzzingEngine_1.FuzzingEngine({ intensity: fuzzingIntensity, seed: params.seed }) : undefined; // Build state hooks for visual regression, accessibility, and performance const stateHooks = []; // Visual regression hook if (params.explorationConfig?.visualRegression?.enabled && this.visualRepo) { const visualCollector = new VisualRegressionCollector_1.VisualRegressionCollector(this.outputDir, this.visualRepo, params.explorationConfig.visualRegression); stateHooks.push(async (state, agentInstance, sid, actionTrace) => { const pw = agentInstance; if (!pw.getPage) return []; // Take screenshot for visual comparison const screenshotPath = path_1.default.join(this.outputDir, sid, `visual_${state.id}.png`); try { const fs_mod = await Promise.resolve().then(() => __importStar(require('fs/promises'))); await fs_mod.mkdir(path_1.default.dirname(screenshotPath), { recursive: true }); await pw.getPage().screenshot({ path: screenshotPath }); const anomaly = await visualCollector.processScreenshot(screenshotPath, state, sid, actionTrace); return anomaly ? [anomaly] : []; } catch { return []; } }); } // Accessibility hook if (params.explorationConfig?.accessibility?.enabled !== false) { const a11yCollector = new AccessibilityCollector_1.AccessibilityCollector(params.explorationConfig?.accessibility); stateHooks.push(async (state, agentInstance, sid, actionTrace) => { const pw = agentInstance; if (!pw.getPage) return []; return a11yCollector.collect(pw.getPage(), state.id, sid, actionTrace); }); } // Performance hook if (params.explorationConfig?.performance?.enabled !== false) { const perfCollector = new PerformanceCollector_1.PerformanceCollector(params.explorationConfig?.performance); this.performanceMetrics.set(sessionId, []); stateHooks.push(async (state, agentInstance, sid, actionTrace) => { const pw = agentInstance; if (!pw.getPage) return []; const { metrics, anomalies } = await perfCollector.collect(pw.getPage(), state.id, sid, actionTrace); const existing = this.performanceMetrics.get(sid) ?? []; existing.push(metrics); this.performanceMetrics.set(sid, existing); return anomalies; }); } // Mobile layout hook (only when a mobile device is emulated) if (params.explorationConfig?.mobileDevice && params.explorationConfig.mobileDevice !== 'none') { stateHooks.push(async (state, agentInstance, sid, actionTrace) => { const pw = agentInstance; if (!pw.detectMobileLayoutIssues) return []; return pw.detectMobileLayoutIssues(state.id, sid, actionTrace); }); } const engineConfig = { graph, agent, seed: params.seed, url: params.url, maxSteps: params.maxStates, explorationConfig: params.explorationConfig, outputDir: this.outputDir, sessionId, fuzzingPlugin, stateHooks, collectors: [ new ScreenshotCollector_1.ScreenshotCollector(this.outputDir), new NetworkCollector_1.NetworkCollector(), new DOMSnapshotCollector_1.DOMSnapshotCollector(this.outputDir), ], exporters: [new MarkdownExporter_1.MarkdownExporter(), new JSONExporter_1.JSONExporter()], reproducer: new PlaywrightReproducer_1.PlaywrightReproducer(), events: { onSessionStarted: (sid, url) => { this.emitter('session:started', { sessionId: sid, url }); }, onStateDiscovered: (sid, stateId, url, title) => { record.statesVisited += 1; this.sessionRepo?.update(sid, { statesVisited: record.statesVisited }); this.emitter('state:discovered', { sessionId: sid, stateId, url, title }); }, onActionExecuted: (sid, actionType, selector, timestamp) => { this.emitter('action:executed', { sessionId: sid, actionType, selector, timestamp }); }, onAnomalyDetected: (sid, anomaly) => { record.anomalies.push(anomaly); record.anomaliesFound = record.anomalies.length; this.sessionRepo?.update(sid, { anomaliesFound: record.anomaliesFound }); this.anomalyRepo?.create(anomaly, sid); if (this.notificationService) { void this.notificationService.notify(anomaly, sid, params.url); } this.emitter('anomaly:detected', { sessionId: sid, anomalyId: anomaly.id, type: anomaly.type, severity: anomaly.severity, description: anomaly.description, }); }, onSessionCompleted: (sid, statesVisited, anomaliesFound) => { if (record.status === 'running') { record.status = 'completed'; } record.finishedAt = new Date().toISOString(); record.statesVisited = statesVisited; record.anomaliesFound = anomaliesFound; this.sessionRepo?.update(sid, { status: record.status, statesVisited, anomaliesFound, finishedAt: Date.now(), }); this.emitter('session:completed', { sessionId: sid, statesVisited, anomaliesFound }); }, onSessionError: (sid, error) => { record.status = 'error'; record.finishedAt = new Date().toISOString(); this.sessionRepo?.update(sid, { status: 'error', finishedAt: Date.now() }); this.emitter('session:error', { sessionId: sid, error }); }, }, }; const engine = new ExplorationEngine_1.ExplorationEngine(engineConfig); record.engine = engine; // Run in background — do not await engine.run().catch((err) => { if (record.status === 'running') { record.status = 'error'; record.finishedAt = new Date().toISOString(); const msg = err instanceof Error ? err.message : String(err); this.sessionRepo?.update(sessionId, { status: 'error', finishedAt: Date.now() }); this.emitter('session:error', { sessionId, error: msg }); } }); return record; } async replayAnomaly(anomalyId) { const replayId = `replay_${Date.now()}`; const anomaly = this.getAnomaly(anomalyId); if (!anomaly) throw new Error(`Anomaly ${anomalyId} not found`); const sessionId = this.findSessionForAnomaly(anomalyId); const session = this.getSession(sessionId); const reproducer = new PlaywrightReproducer_1.PlaywrightReproducer(); const script = reproducer.generateScript(anomaly.actionTrace); const replayDir = path_1.default.join(this.outputDir, anomalyId, 'replays'); setImmediate(async () => { try { const fs_mod = await Promise.resolve().then(() => __importStar(require('fs/promises'))); await fs_mod.mkdir(replayDir, { recursive: true }); const scriptPath = path_1.default.join(replayDir, `${replayId}.ts`); await fs_mod.writeFile(scriptPath, script, 'utf8'); const agent = new PlaywrightAgent_1.PlaywrightAgent({ seed: session.seed }); await agent.launch(session.url); for (const action of anomaly.actionTrace) { await agent.executeAction(action).catch(() => undefined); } await agent.close(); } catch { // replay errors are non-fatal } }); return replayId; } } exports.SessionStore = SessionStore;