import { AuditConfig, AuditEvent, AuditEventOptions, AuditError, AuditEventQuery, AuditEventResult, StreamConfig, StreamSubscription, } from '../types'; class AuditService { private config: AuditConfig = { storage: 'firebase', stream: false, retention: '90d', }; private events: Map = new Map(); private streamSubscriptions: Map = new Map(); private decoratedMethods: Map = new Map(); // Configure the audit service configure(config: AuditConfig): AuditService { this.config = { ...this.config, ...config }; return this; } // Log an audit event async log(options: AuditEventOptions): Promise { try { const event: AuditEvent = { id: this.generateEventId(), timestamp: new Date(), actor: await this.getCurrentActor(), action: options.action, resource: options.resource, result: options.result || 'success', changes: options.changes, metadata: options.metadata, context: await this.captureContext(), tags: options.tags, }; // Apply filters if (this.config.filters) { for (const filter of this.config.filters) { if (!filter.filter(event)) { return event; // Skip logging if filtered out } } } // Apply formatters let formattedEvent = event; if (this.config.formatters) { for (const formatter of this.config.formatters) { formattedEvent = formatter.format(formattedEvent); } } // Store event await this.storeEvent(formattedEvent); // Stream if enabled if (this.config.stream) { this.streamEvent(formattedEvent); } return formattedEvent; } catch (error: any) { const auditError: AuditError = { code: 'audit/log-failed', message: error.message || 'Failed to log audit event', event: options as any, originalError: error, }; if (this.config.onError) { this.config.onError(auditError); } throw auditError; } } // Decorator for automatic method tracking track(action: string) { return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { const originalMethod = descriptor.value; descriptor.value = async function (...args: any[]) { const startTime = Date.now(); let result: any; let error: any; try { result = await originalMethod.apply(this, args); } catch (e) { error = e; } // Log the audit event const audit = new AuditService(); await audit.log({ action, resource: { type: target.constructor.name, id: propertyKey, name: `${target.constructor.name}.${propertyKey}`, }, result: error ? 'failure' : 'success', metadata: { duration: Date.now() - startTime, arguments: args, error: error?.message, }, }); if (error) { throw error; } return result; }; return descriptor; }; } // Stream real-time audit events stream(config: StreamConfig): StreamSubscription { const subscriptionId = this.generateSubscriptionId(); const subscription: StreamSubscription = { id: subscriptionId, unsubscribe: () => { this.streamSubscriptions.delete(subscriptionId); }, pause: () => { // Pause implementation }, resume: () => { // Resume implementation }, updateFilter: (filter) => { // Update filter implementation }, }; this.streamSubscriptions.set(subscriptionId, subscription); // Start streaming if (config.onConnect) { config.onConnect(); } return subscription; } // Query audit events async query(query: AuditEventQuery): Promise { let events = Array.from(this.events.values()); // Apply filters if (query.filters) { const { filters } = query; if (filters.startDate) { events = events.filter(e => e.timestamp >= filters.startDate!); } if (filters.endDate) { events = events.filter(e => e.timestamp <= filters.endDate!); } if (filters.actors?.length) { events = events.filter(e => filters.actors!.includes(e.actor.id)); } if (filters.actions?.length) { events = events.filter(e => filters.actions!.includes(e.action)); } if (filters.resourceTypes?.length) { events = events.filter(e => filters.resourceTypes!.includes(e.resource.type)); } if (filters.results?.length) { events = events.filter(e => filters.results!.includes(e.result)); } if (filters.search) { const search = filters.search.toLowerCase(); events = events.filter(e => e.action.toLowerCase().includes(search) || e.resource.name?.toLowerCase().includes(search) || e.actor.name?.toLowerCase().includes(search) ); } } // Apply sorting if (query.sort) { events.sort((a, b) => { const aValue = (a as any)[query.sort!.field]; const bValue = (b as any)[query.sort!.field]; const direction = query.sort!.order === 'asc' ? 1 : -1; if (aValue < bValue) return -direction; if (aValue > bValue) return direction; return 0; }); } else { // Default sort by timestamp descending events.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); } // Apply pagination const total = events.length; if (query.pagination) { const { page = 1, limit = 50 } = query.pagination; const start = (page - 1) * limit; events = events.slice(start, start + limit); } return { events, total, page: query.pagination?.page || 1, pageSize: query.pagination?.limit || events.length, hasMore: total > (query.pagination?.page || 1) * (query.pagination?.limit || total), }; } // Get user activity async getUserActivity(userId: string, startDate?: Date, endDate?: Date): Promise { const query: AuditEventQuery = { filters: { actors: [userId], startDate, endDate, }, }; const result = await this.query(query); return result.events; } // Private methods private async getCurrentActor() { // This would typically get the current user from auth service return { id: 'system', type: 'system' as const, name: 'System', }; } private async captureContext() { // Capture request context return { ip: '127.0.0.1', userAgent: typeof window !== 'undefined' ? window.navigator.userAgent : 'Server', sessionId: this.generateSessionId(), }; } private async storeEvent(event: AuditEvent) { // Store in memory for now (would use actual storage in production) this.events.set(event.id, event); // Clean up old events based on retention if (this.config.retention) { this.cleanupOldEvents(); } } private streamEvent(event: AuditEvent) { // Send event to all active stream subscriptions this.streamSubscriptions.forEach((subscription, id) => { // Stream implementation would go here }); } private cleanupOldEvents() { const retentionMs = this.parseRetention(this.config.retention!); const cutoffDate = new Date(Date.now() - retentionMs); this.events.forEach((event, id) => { if (event.timestamp < cutoffDate) { this.events.delete(id); } }); } private parseRetention(retention: string): number { const match = retention.match(/^(\d+)([hdwmy])$/); if (!match) return 90 * 24 * 60 * 60 * 1000; // Default 90 days const [, value, unit] = match; const num = parseInt(value, 10); switch (unit) { case 'h': return num * 60 * 60 * 1000; case 'd': return num * 24 * 60 * 60 * 1000; case 'w': return num * 7 * 24 * 60 * 60 * 1000; case 'm': return num * 30 * 24 * 60 * 60 * 1000; case 'y': return num * 365 * 24 * 60 * 60 * 1000; default: return 90 * 24 * 60 * 60 * 1000; } } private generateEventId(): string { return `evt_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; } private generateSubscriptionId(): string { return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; } private generateSessionId(): string { return `ses_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; } } // Singleton instance export const audit = new AuditService();