UNPKG

@vqp/audit-memory

Version:

In-memory audit adapter for VQP

216 lines 7.81 kB
/** * Memory Audit Adapter - Implements AuditPort using in-memory storage * This adapter stores audit events in memory for testing and lightweight deployments */ export class MemoryAuditAdapter { entries = []; maxEntries; includeFullQuery; includeFullResponse; autoCleanup; logLevel; constructor(config = {}) { this.maxEntries = config.maxEntries || 10000; this.includeFullQuery = config.includeFullQuery || false; this.includeFullResponse = config.includeFullResponse || false; this.autoCleanup = config.autoCleanup !== false; // Default to true this.logLevel = config.logLevel || 'info'; } async logQuery(query, response) { const responseTime = new Date(response.timestamp).getTime() - new Date(query.timestamp).getTime(); const entry = { timestamp: response.timestamp, // Use response timestamp for consistency event: 'query_processed', metadata: { vocabulary: query.query.vocab, responseTime, target: query.target, responder: response.responder, proofType: response.proof.type, ...(this.includeFullQuery && { fullQuery: query }), ...(this.includeFullResponse && { fullResponse: response }), }, }; // Add optional fields only if they have values if (query.id) { entry.queryId = query.id; } if (query.requester) { entry.querier = query.requester; } // Only include result if it's a boolean if (typeof response.result === 'boolean') { entry.result = response.result; } this.addEntry(entry); } async logError(error, context) { const entry = { timestamp: new Date().toISOString(), event: 'error_occurred', error: { name: error.name, code: error.code, message: error.message, details: error.details, }, metadata: { errorCode: error.code, errorMessage: error.message, context: this.sanitizeContext(context), }, }; this.addEntry(entry); } async getAuditTrail(filters) { let filteredEntries = [...this.entries]; if (!filters) { return filteredEntries; } // Apply time range filter if (filters.startTime) { const startTime = new Date(filters.startTime); filteredEntries = filteredEntries.filter((entry) => new Date(entry.timestamp) >= startTime); } if (filters.endTime) { const endTime = new Date(filters.endTime); filteredEntries = filteredEntries.filter((entry) => new Date(entry.timestamp) <= endTime); } // Apply querier filter if (filters.querier) { filteredEntries = filteredEntries.filter((entry) => entry.querier === filters.querier); } // Apply event filter if (filters.event) { filteredEntries = filteredEntries.filter((entry) => entry.event === filters.event); } // Sort by timestamp (newest first) return filteredEntries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); } async purgeOldEntries(olderThan) { const cutoffTime = new Date(olderThan); const originalCount = this.entries.length; this.entries = this.entries.filter((entry) => new Date(entry.timestamp) > cutoffTime); return originalCount - this.entries.length; } /** * Get current number of stored entries */ getEntryCount() { return this.entries.length; } /** * Clear all entries (useful for testing) */ clearAll() { this.entries = []; } /** * Get memory usage statistics */ getMemoryStats() { const memoryUsageBytes = this.estimateMemoryUsage(); const stats = { entryCount: this.entries.length, maxEntries: this.maxEntries, memoryUsageBytes, }; if (this.entries.length > 0) { const oldestEntry = this.entries[this.entries.length - 1]; const newestEntry = this.entries[0]; if (oldestEntry) { stats.oldestEntry = oldestEntry.timestamp; } if (newestEntry) { stats.newestEntry = newestEntry.timestamp; } } return stats; } addEntry(entry) { // Insert at the beginning to maintain newest-first order this.entries.unshift(entry); // Perform auto-cleanup if enabled and we exceed maxEntries if (this.autoCleanup && this.entries.length > this.maxEntries) { // Remove oldest entries (from the end of the array) this.entries = this.entries.slice(0, this.maxEntries); } } sanitizeContext(context) { if (!context) return null; // Create a sanitized copy of the context try { const sanitized = JSON.parse(JSON.stringify(context)); // Remove potentially sensitive fields if (typeof sanitized === 'object') { delete sanitized.privateKey; delete sanitized.password; delete sanitized.secret; delete sanitized.token; // Recursively sanitize nested objects Object.keys(sanitized).forEach((key) => { if (typeof sanitized[key] === 'object' && sanitized[key] !== null) { sanitized[key] = this.sanitizeContext(sanitized[key]); } }); } return sanitized; } catch { // If serialization fails, return a safe representation return { type: typeof context, error: 'Serialization failed' }; } } estimateMemoryUsage() { try { // Rough estimation of memory usage const jsonString = JSON.stringify(this.entries); return jsonString.length * 2; // Approximate bytes (UTF-16) } catch { // Fallback estimation return this.entries.length * 1000; // Rough estimate: 1KB per entry } } /** * Get entries by event type */ getEntriesByEvent(eventType) { return this.entries.filter((entry) => entry.event === eventType); } /** * Get recent entries (last N entries) */ getRecentEntries(count = 100) { return this.entries.slice(0, Math.min(count, this.entries.length)); } /** * Search entries by metadata */ searchEntries(searchTerm) { const term = searchTerm.toLowerCase(); return this.entries.filter((entry) => { // Search in queryId if (entry.queryId?.toLowerCase().includes(term)) return true; // Search in querier if (entry.querier?.toLowerCase().includes(term)) return true; // Search in metadata if (entry.metadata) { const metadataString = JSON.stringify(entry.metadata).toLowerCase(); if (metadataString.includes(term)) return true; } // Search in error details if (entry.error) { const errorString = JSON.stringify(entry.error).toLowerCase(); if (errorString.includes(term)) return true; } return false; }); } } //# sourceMappingURL=memory-adapter.js.map