/** * Human-in-the-Loop Approval Service * * SIKKERHED: Ingen handlinger der muterer data kan udfรธres uden eksplicit menneskelig godkendelse. * Dette inkluderer: * - Data ingestion (Facebook, email, external APIs) * - Social media posting * - Database mutations * - File system operations * - External API calls */ import { EventEmitter } from 'events'; export enum ApprovalStatus { PENDING = 'pending', APPROVED = 'approved', REJECTED = 'rejected', EXPIRED = 'expired', } export enum ApprovalRiskLevel { LOW = 'low', // Read-only operations MEDIUM = 'medium', // Data ingestion, internal mutations HIGH = 'high', // External posts, payments, deletions CRITICAL = 'critical' // System-level changes } export interface ApprovalRequest { id: string; operation: string; description: string; riskLevel: ApprovalRiskLevel; requestedBy: string; // System component requesting approval requestedAt: number; expiresAt: number; status: ApprovalStatus; metadata: Record; approvedBy?: string; approvedAt?: number; rejectedBy?: string; rejectedAt?: number; rejectionReason?: string; } class HumanApprovalServiceImpl extends EventEmitter { private pendingRequests: Map = new Map(); private readonly DEFAULT_EXPIRY = 300000; // 5 minutes private cleanupInterval: NodeJS.Timeout; constructor() { super(); // Cleanup expired requests every minute this.cleanupInterval = setInterval(() => { this.cleanupExpiredRequests(); }, 60000); } /** * Request approval for an operation * Returns approval request ID */ async requestApproval( operation: string, description: string, riskLevel: ApprovalRiskLevel, requestedBy: string, metadata: Record = {}, expiryMs?: number ): Promise { const id = this.generateId(); const now = Date.now(); const request: ApprovalRequest = { id, operation, description, riskLevel, requestedBy, requestedAt: now, expiresAt: now + (expiryMs || this.DEFAULT_EXPIRY), status: ApprovalStatus.PENDING, metadata, }; this.pendingRequests.set(id, request); // Emit event for real-time UI updates this.emit('approval-requested', request); console.log(`๐Ÿ” [APPROVAL REQUIRED] ${operation}`); console.log(` Risk: ${riskLevel.toUpperCase()}`); console.log(` Description: ${description}`); console.log(` Request ID: ${id}`); return id; } /** * Wait for approval (blocking) * Throws if rejected or expired */ async waitForApproval(requestId: string, timeoutMs: number = 300000): Promise { return new Promise((resolve, reject) => { const checkInterval = setInterval(() => { const request = this.pendingRequests.get(requestId); if (!request) { clearInterval(checkInterval); reject(new Error('Approval request not found')); return; } if (request.status === ApprovalStatus.APPROVED) { clearInterval(checkInterval); resolve(); return; } if (request.status === ApprovalStatus.REJECTED) { clearInterval(checkInterval); reject(new Error(`Approval rejected: ${request.rejectionReason || 'No reason provided'}`)); return; } if (Date.now() > request.expiresAt) { clearInterval(checkInterval); request.status = ApprovalStatus.EXPIRED; this.emit('approval-expired', request); reject(new Error('Approval request expired')); return; } }, 500); // Failsafe timeout setTimeout(() => { clearInterval(checkInterval); const request = this.pendingRequests.get(requestId); if (request && request.status === ApprovalStatus.PENDING) { request.status = ApprovalStatus.EXPIRED; this.emit('approval-expired', request); reject(new Error('Approval timeout')); } }, timeoutMs); }); } /** * Approve a request */ approve(requestId: string, approvedBy: string): boolean { const request = this.pendingRequests.get(requestId); if (!request) { console.error(`โŒ Approval request ${requestId} not found`); return false; } if (request.status !== ApprovalStatus.PENDING) { console.error(`โŒ Approval request ${requestId} is not pending (status: ${request.status})`); return false; } if (Date.now() > request.expiresAt) { request.status = ApprovalStatus.EXPIRED; console.error(`โŒ Approval request ${requestId} has expired`); return false; } request.status = ApprovalStatus.APPROVED; request.approvedBy = approvedBy; request.approvedAt = Date.now(); this.emit('approval-granted', request); console.log(`โœ… [APPROVED] ${request.operation}`); console.log(` Approved by: ${approvedBy}`); return true; } /** * Reject a request */ reject(requestId: string, rejectedBy: string, reason: string): boolean { const request = this.pendingRequests.get(requestId); if (!request) { console.error(`โŒ Approval request ${requestId} not found`); return false; } if (request.status !== ApprovalStatus.PENDING) { console.error(`โŒ Approval request ${requestId} is not pending (status: ${request.status})`); return false; } request.status = ApprovalStatus.REJECTED; request.rejectedBy = rejectedBy; request.rejectedAt = Date.now(); request.rejectionReason = reason; this.emit('approval-rejected', request); console.log(`๐Ÿšซ [REJECTED] ${request.operation}`); console.log(` Rejected by: ${rejectedBy}`); console.log(` Reason: ${reason}`); return true; } /** * Get all pending requests */ getPendingRequests(): ApprovalRequest[] { return Array.from(this.pendingRequests.values()) .filter(r => r.status === ApprovalStatus.PENDING && Date.now() <= r.expiresAt) .sort((a, b) => { // Sort by risk level (high to low), then by time (old to new) const riskOrder = { critical: 4, high: 3, medium: 2, low: 1 }; const riskDiff = (riskOrder[b.riskLevel] || 0) - (riskOrder[a.riskLevel] || 0); if (riskDiff !== 0) return riskDiff; return a.requestedAt - b.requestedAt; }); } /** * Get request by ID */ getRequest(requestId: string): ApprovalRequest | null { return this.pendingRequests.get(requestId) || null; } /** * Get request history */ getHistory(limit: number = 50): ApprovalRequest[] { return Array.from(this.pendingRequests.values()) .sort((a, b) => b.requestedAt - a.requestedAt) .slice(0, limit); } /** * Cleanup expired requests */ private cleanupExpiredRequests(): void { const now = Date.now(); let cleaned = 0; for (const [id, request] of this.pendingRequests.entries()) { if (request.status === ApprovalStatus.PENDING && now > request.expiresAt) { request.status = ApprovalStatus.EXPIRED; this.emit('approval-expired', request); cleaned++; } // Remove old completed requests (keep for 1 hour) if (request.status !== ApprovalStatus.PENDING && now - request.requestedAt > 3600000) { this.pendingRequests.delete(id); } } if (cleaned > 0) { console.log(`๐Ÿงน Cleaned up ${cleaned} expired approval requests`); } } /** * Generate unique ID */ private generateId(): string { return `approval_${Date.now()}_${Math.random().toString(36).substring(7)}`; } /** * Shutdown cleanup */ shutdown(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } } } // Singleton instance export const humanApprovalService = new HumanApprovalServiceImpl();