UNPKG

anon-identity

Version:

Decentralized identity framework with DIDs, Verifiable Credentials, and privacy-preserving selective disclosure

340 lines 12.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ActivityWebSocketServer = exports.MessageType = void 0; const ws_1 = require("ws"); var MessageType; (function (MessageType) { // Client to server MessageType["SUBSCRIBE"] = "subscribe"; MessageType["UNSUBSCRIBE"] = "unsubscribe"; MessageType["GET_RECENT_EVENTS"] = "get_recent_events"; MessageType["GET_METRICS"] = "get_metrics"; MessageType["PING"] = "ping"; // Server to client MessageType["EVENT"] = "event"; MessageType["SUBSCRIBED"] = "subscribed"; MessageType["UNSUBSCRIBED"] = "unsubscribed"; MessageType["RECENT_EVENTS"] = "recent_events"; MessageType["METRICS"] = "metrics"; MessageType["PONG"] = "pong"; MessageType["ERROR"] = "error"; })(MessageType || (exports.MessageType = MessageType = {})); class ActivityWebSocketServer { constructor(streamManager, server, config = {}) { this.clients = new Map(); this.clientSubscriptions = new Map(); this.streamManager = streamManager; this.config = { port: config.port || 8080, path: config.path || '/activity-stream', enableAuth: config.enableAuth ?? false, authTimeout: config.authTimeout || 30000, maxConnections: config.maxConnections || 1000, pingInterval: config.pingInterval || 30000, enableCompression: config.enableCompression ?? true }; // Create WebSocket server const wsOptions = { path: this.config.path, perMessageDeflate: this.config.enableCompression }; if (server) { wsOptions.server = server; } else { wsOptions.port = this.config.port; } this.wss = new ws_1.WebSocketServer(wsOptions); this.setupEventHandlers(); this.startPingTimer(); } /** * Get server statistics */ getStats() { const subscriptionsByType = {}; let totalSubscriptions = 0; this.clientSubscriptions.forEach(subs => { totalSubscriptions += subs.length; subs.forEach(sub => { const filterType = this.getFilterType(sub.filters); subscriptionsByType[filterType] = (subscriptionsByType[filterType] || 0) + 1; }); }); return { connectedClients: this.clients.size, totalSubscriptions, subscriptionsByType, uptime: process.uptime() }; } /** * Broadcast event to all matching clients */ broadcast(event, filters) { const message = { type: MessageType.EVENT, data: event }; this.clients.forEach((ws, clientId) => { if (filters) { // Check if client has matching subscriptions const clientSubs = this.clientSubscriptions.get(clientId) || []; const hasMatchingSubscription = clientSubs.some(sub => this.filtersMatch(sub.filters, filters)); if (!hasMatchingSubscription) { return; } } this.sendMessage(ws, message); }); } /** * Send event to specific client */ sendToClient(clientId, event) { const ws = this.clients.get(clientId); if (ws && ws.readyState === ws_1.WebSocket.OPEN) { const message = { type: MessageType.EVENT, data: event }; this.sendMessage(ws, message); } } /** * Close the WebSocket server */ close() { return new Promise((resolve) => { if (this.pingTimer) { clearInterval(this.pingTimer); } // Close all client connections this.clients.forEach(ws => { ws.close(1000, 'Server shutting down'); }); // Close server this.wss.close(() => { resolve(); }); }); } // Private methods setupEventHandlers() { this.wss.on('connection', (ws, request) => { const clientId = this.generateClientId(); // Check connection limit if (this.clients.size >= this.config.maxConnections) { ws.close(1008, 'Maximum connections exceeded'); return; } this.clients.set(clientId, ws); this.clientSubscriptions.set(clientId, []); console.log(`Client connected: ${clientId} (${this.clients.size} total)`); // Set up client event handlers ws.on('message', (data) => { this.handleClientMessage(clientId, ws, data); }); ws.on('close', (code, reason) => { this.handleClientDisconnect(clientId, code, reason.toString()); }); ws.on('error', (error) => { console.error(`WebSocket error for client ${clientId}:`, error); this.handleClientDisconnect(clientId, 1011, 'Unexpected error'); }); ws.on('pong', () => { // Client responded to ping - connection is alive ws.isAlive = true; }); // Send welcome message this.sendMessage(ws, { type: MessageType.PONG, data: { clientId, message: 'Connected to activity stream', serverTime: new Date().toISOString() } }); }); this.wss.on('error', (error) => { console.error('WebSocket server error:', error); }); } handleClientMessage(clientId, ws, data) { try { const message = JSON.parse(data.toString()); switch (message.type) { case MessageType.SUBSCRIBE: this.handleSubscribe(clientId, ws, message); break; case MessageType.UNSUBSCRIBE: this.handleUnsubscribe(clientId, ws, message); break; case MessageType.GET_RECENT_EVENTS: this.handleGetRecentEvents(clientId, ws, message); break; case MessageType.GET_METRICS: this.handleGetMetrics(clientId, ws, message); break; case MessageType.PING: this.sendMessage(ws, { type: MessageType.PONG, id: message.id }); break; default: this.sendError(ws, `Unknown message type: ${message.type}`, message.id); } } catch (error) { this.sendError(ws, `Invalid message format: ${error}`); } } handleSubscribe(clientId, ws, message) { try { const filters = message.data?.filters || {}; // Create stream subscription const streamSubscription = this.streamManager.subscribe(filters, (event) => { this.sendToClient(clientId, event); }, { clientId, subscriptionId: message.id }); // Store client subscription const clientSubscription = { subscriptionId: message.id || this.generateId(), streamSubscription, filters, clientId }; const clientSubs = this.clientSubscriptions.get(clientId) || []; clientSubs.push(clientSubscription); this.clientSubscriptions.set(clientId, clientSubs); this.sendMessage(ws, { type: MessageType.SUBSCRIBED, id: message.id, data: { subscriptionId: clientSubscription.subscriptionId, filters } }); } catch (error) { this.sendError(ws, `Subscription failed: ${error}`, message.id); } } handleUnsubscribe(clientId, ws, message) { const subscriptionId = message.data?.subscriptionId; if (!subscriptionId) { this.sendError(ws, 'Subscription ID required', message.id); return; } const clientSubs = this.clientSubscriptions.get(clientId) || []; const subscriptionIndex = clientSubs.findIndex(sub => sub.subscriptionId === subscriptionId); if (subscriptionIndex === -1) { this.sendError(ws, 'Subscription not found', message.id); return; } const subscription = clientSubs[subscriptionIndex]; subscription.streamSubscription.unsubscribe(); clientSubs.splice(subscriptionIndex, 1); this.sendMessage(ws, { type: MessageType.UNSUBSCRIBED, id: message.id, data: { subscriptionId } }); } handleGetRecentEvents(clientId, ws, message) { const { filters, limit } = message.data || {}; const events = this.streamManager.getRecentEvents(filters, limit); this.sendMessage(ws, { type: MessageType.RECENT_EVENTS, id: message.id, data: { events, count: events.length } }); } handleGetMetrics(clientId, ws, message) { const streamMetrics = this.streamManager.getMetrics(); const subscriptionStats = this.streamManager.getSubscriptionStats(); const serverStats = this.getStats(); this.sendMessage(ws, { type: MessageType.METRICS, id: message.id, data: { stream: streamMetrics, subscriptions: subscriptionStats, server: serverStats } }); } handleClientDisconnect(clientId, code, reason) { console.log(`Client disconnected: ${clientId} (${code}: ${reason})`); // Clean up subscriptions const clientSubs = this.clientSubscriptions.get(clientId) || []; clientSubs.forEach(sub => { sub.streamSubscription.unsubscribe(); }); this.clientSubscriptions.delete(clientId); this.clients.delete(clientId); } sendMessage(ws, message) { if (ws.readyState === ws_1.WebSocket.OPEN) { try { ws.send(JSON.stringify(message)); } catch (error) { console.error('Failed to send message:', error); } } } sendError(ws, error, messageId) { this.sendMessage(ws, { type: MessageType.ERROR, id: messageId, error }); } startPingTimer() { this.pingTimer = setInterval(() => { this.wss.clients.forEach((ws) => { if (ws.isAlive === false) { ws.terminate(); return; } ws.isAlive = false; ws.ping(); }); }, this.config.pingInterval); } generateClientId() { return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } generateId() { return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } getFilterType(filters) { if (filters.agentDID) return 'agent'; if (filters.parentDID) return 'user'; if (filters.critical) return 'critical'; if (filters.errorOnly) return 'errors'; if (filters.serviceDID) return 'service'; if (filters.types) return 'type-specific'; return 'general'; } filtersMatch(filter1, filter2) { // Simple filter matching - could be more sophisticated if (filter2.agentDID && filter1.agentDID !== filter2.agentDID) return false; if (filter2.parentDID && filter1.parentDID !== filter2.parentDID) return false; if (filter2.serviceDID && filter1.serviceDID !== filter2.serviceDID) return false; if (filter2.critical && !filter1.critical) return false; if (filter2.errorOnly && !filter1.errorOnly) return false; return true; } } exports.ActivityWebSocketServer = ActivityWebSocketServer; //# sourceMappingURL=websocket-server.js.map