/** * Audit Service * Based on: docs/foundation/Agent_Governance_Protocol_v1.md * * Provides access to governance events for audit and analysis */ import { db } from '../../infra/db/client'; import { logger } from '../../infra/logger/logger'; import { GovernanceEvent, GovernanceEventType, AuditEventFilter, GovernanceScope, } from '../../domain/governance/types'; export interface AuditEventRow { id: string; event_type: GovernanceEventType; subject: string; actor_id: string; target_id: string; scope: GovernanceScope; payload: Record; version: string; status: 'pending' | 'published' | 'failed'; created_at: Date; published_at?: Date; } export class AuditService { /** * Get audit events with filters */ async getEvents(filter: AuditEventFilter): Promise<{ events: GovernanceEvent[]; total: number; }> { const conditions: string[] = []; const params: (string | Date | number)[] = []; let paramIndex = 1; if (filter.eventType) { conditions.push(`event_type = $${paramIndex++}`); params.push(filter.eventType); } if (filter.actorId) { conditions.push(`actor_id = $${paramIndex++}`); params.push(filter.actorId); } if (filter.targetId) { conditions.push(`target_id = $${paramIndex++}`); params.push(filter.targetId); } if (filter.scope) { conditions.push(`scope = $${paramIndex++}`); params.push(filter.scope); } if (filter.createdAtFrom) { conditions.push(`created_at >= $${paramIndex++}`); params.push(filter.createdAtFrom); } if (filter.createdAtTo) { conditions.push(`created_at <= $${paramIndex++}`); params.push(filter.createdAtTo); } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; // Get total count const countResult = await db.query<{ count: string }>( `SELECT COUNT(*) as count FROM event_outbox ${whereClause}`, params ); const total = parseInt(countResult.rows[0].count, 10); // Get events const limit = filter.limit || 50; const offset = filter.offset || 0; params.push(limit, offset); const result = await db.query( `SELECT id, event_type, subject, actor_id, target_id, scope, payload, version, status, created_at, published_at FROM event_outbox ${whereClause} ORDER BY created_at DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`, params ); const events: GovernanceEvent[] = result.rows.map(row => ({ id: row.id, eventType: row.event_type, subject: row.subject, actorId: row.actor_id, targetId: row.target_id, scope: row.scope, payload: row.payload, version: row.version, status: row.status, createdAt: row.created_at, publishedAt: row.published_at, })); return { events, total }; } /** * Get single event by ID */ async getEvent(eventId: string): Promise { const result = await db.query( `SELECT id, event_type, subject, actor_id, target_id, scope, payload, version, status, created_at, published_at FROM event_outbox WHERE id = $1`, [eventId] ); if (result.rows.length === 0) { return null; } const row = result.rows[0]; return { id: row.id, eventType: row.event_type, subject: row.subject, actorId: row.actor_id, targetId: row.target_id, scope: row.scope, payload: row.payload, version: row.version, status: row.status, createdAt: row.created_at, publishedAt: row.published_at, }; } /** * Get events by actor */ async getEventsByActor(actorId: string, limit = 50): Promise { const result = await this.getEvents({ actorId, limit }); return result.events; } /** * Get events by target */ async getEventsByTarget(targetId: string, limit = 50): Promise { const result = await this.getEvents({ targetId, limit }); return result.events; } /** * Get events by scope */ async getEventsByScope(scope: GovernanceScope, limit = 50): Promise { const result = await this.getEvents({ scope, limit }); return result.events; } /** * Get event statistics */ async getEventStats( fromDate?: Date, toDate?: Date ): Promise<{ totalEvents: number; eventsByType: Record; eventsByDay: Array<{ date: string; count: number }>; topActors: Array<{ actorId: string; count: number }>; }> { const conditions: string[] = []; const params: (string | Date)[] = []; let paramIndex = 1; if (fromDate) { conditions.push(`created_at >= $${paramIndex++}`); params.push(fromDate); } if (toDate) { conditions.push(`created_at <= $${paramIndex++}`); params.push(toDate); } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; // Total events const totalResult = await db.query<{ count: string }>( `SELECT COUNT(*) as count FROM event_outbox ${whereClause}`, params ); const totalEvents = parseInt(totalResult.rows[0].count, 10); // Events by type const byTypeResult = await db.query<{ event_type: string; count: string }>( `SELECT event_type, COUNT(*) as count FROM event_outbox ${whereClause} GROUP BY event_type`, params ); const eventsByType: Record = {}; byTypeResult.rows.forEach(row => { eventsByType[row.event_type] = parseInt(row.count, 10); }); // Events by day const byDayResult = await db.query<{ date: string; count: string }>( `SELECT DATE(created_at) as date, COUNT(*) as count FROM event_outbox ${whereClause} GROUP BY DATE(created_at) ORDER BY date DESC LIMIT 30`, params ); const eventsByDay = byDayResult.rows.map(row => ({ date: row.date, count: parseInt(row.count, 10), })); // Top actors const topActorsResult = await db.query<{ actor_id: string; count: string }>( `SELECT actor_id, COUNT(*) as count FROM event_outbox ${whereClause} AND actor_id IS NOT NULL GROUP BY actor_id ORDER BY count DESC LIMIT 10`, params ); const topActors = topActorsResult.rows.map(row => ({ actorId: row.actor_id, count: parseInt(row.count, 10), })); return { totalEvents, eventsByType, eventsByDay, topActors, }; } /** * Get governance events for specific entity */ async getEntityHistory( entityType: 'agent' | 'microdao' | 'district' | 'node' | 'room', entityId: string, limit = 50 ): Promise { const result = await db.query( `SELECT id, event_type, subject, actor_id, target_id, scope, payload, version, status, created_at, published_at FROM event_outbox WHERE target_id = $1 OR actor_id = $1 OR scope LIKE $2 ORDER BY created_at DESC LIMIT $3`, [entityId, `%${entityId}%`, limit] ); return result.rows.map(row => ({ id: row.id, eventType: row.event_type, subject: row.subject, actorId: row.actor_id, targetId: row.target_id, scope: row.scope, payload: row.payload, version: row.version, status: row.status, createdAt: row.created_at, publishedAt: row.published_at, })); } } export const auditService = new AuditService();