Spaces:
Paused
Paused
| /** | |
| * 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<string, any>; | |
| approvedBy?: string; | |
| approvedAt?: number; | |
| rejectedBy?: string; | |
| rejectedAt?: number; | |
| rejectionReason?: string; | |
| } | |
| class HumanApprovalServiceImpl extends EventEmitter { | |
| private pendingRequests: Map<string, ApprovalRequest> = 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<string, any> = {}, | |
| expiryMs?: number | |
| ): Promise<string> { | |
| 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<void> { | |
| 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(); | |