/** * API Server Augmentation - Universal API Exposure * * 🌐 Exposes Brainy through REST, WebSocket, and MCP * 🔌 Works in Node.js, Deno, and Service Workers * 🚀 Single augmentation for all API needs * * This unifies and replaces: * - BrainyMCPBroadcast (Node-specific server) * - WebSocketConduitAugmentation (client connections) * - Future REST API implementations */ import { BaseAugmentation } from './brainyAugmentation.js'; import { BrainyMCPService } from '../mcp/brainyMCPService.js'; import { v4 as uuidv4 } from '../universal/uuid.js'; import { isNode, isBrowser } from '../utils/environment.js'; /** * Unified API Server Augmentation * Exposes Brainy through multiple protocols */ export class APIServerAugmentation extends BaseAugmentation { constructor(config = {}) { super(); this.name = 'api-server'; this.timing = 'after'; this.metadata = 'readonly'; // API server reads metadata to serve data this.operations = ['all']; this.priority = 5; // Low priority, runs after other augmentations this.clients = new Map(); this.operationHistory = []; this.maxHistorySize = 1000; this.config = { enabled: true, port: 3000, host: '0.0.0.0', cors: { origin: '*', credentials: true }, auth: { required: false }, rateLimit: { windowMs: 60000, max: 100 }, ...config }; } async onInitialize() { if (!this.config.enabled) { this.log('API Server disabled in config'); return; } // Initialize MCP service this.mcpService = new BrainyMCPService(this.context.brain, { enableAuth: this.config.auth?.required }); // Start appropriate server based on environment if (isNode()) { await this.startNodeServer(); } else if (typeof globalThis.Deno !== 'undefined') { await this.startDenoServer(); } else if (isBrowser() && 'serviceWorker' in navigator) { await this.startServiceWorker(); } else { this.log('No suitable server environment detected', 'warn'); } } /** * Start Node.js server with Express */ async startNodeServer() { try { // Dynamic imports for Node.js dependencies const express = await import('express').catch(() => null); const cors = await import('cors').catch(() => null); const ws = await import('ws').catch(() => null); const { createServer } = await import('http'); if (!express || !cors || !ws) { this.log('Express, cors, or ws not available. Install with: npm install express cors ws', 'error'); return; } const WebSocketServer = ws?.WebSocketServer || ws?.default?.WebSocketServer || ws?.Server; const app = express.default(); // Middleware app.use(cors.default(this.config.cors)); app.use((express.default || express).json({ limit: '50mb' })); app.use(this.authMiddleware.bind(this)); app.use(this.rateLimitMiddleware.bind(this)); // REST API Routes this.setupRESTRoutes(app); // Create HTTP server this.httpServer = createServer(app); // WebSocket server this.wsServer = new WebSocketServer({ server: this.httpServer, path: '/ws' }); this.setupWebSocketServer(); // Start listening await new Promise((resolve, reject) => { this.httpServer.listen(this.config.port, this.config.host, () => { this.log(`🌐 API Server listening on http://${this.config.host}:${this.config.port}`); this.log(`🔌 WebSocket: ws://${this.config.host}:${this.config.port}/ws`); this.log(`🧠 MCP endpoint: http://${this.config.host}:${this.config.port}/api/mcp`); resolve(); }).on('error', reject); }); // Heartbeat interval setInterval(() => this.sendHeartbeats(), 30000); } catch (error) { this.log(`Failed to start Node.js server: ${error}`, 'error'); throw error; } } /** * Setup REST API routes */ setupRESTRoutes(app) { // Health check app.get('/health', (_req, res) => { res.json({ status: 'healthy', version: '2.0.0', clients: this.clients.size, uptime: process.uptime ? process.uptime() : 0 }); }); // Search endpoint app.post('/api/search', async (req, res) => { try { const { query, limit = 10, options = {} } = req.body; const results = await this.context.brain.search(query, limit, options); res.json({ success: true, results }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // Add data endpoint app.post('/api/add', async (req, res) => { try { const { content, metadata } = req.body; const id = await this.context.brain.addNoun(content, 'Content', metadata); res.json({ success: true, id }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // Get by ID endpoint app.get('/api/get/:id', async (req, res) => { try { const data = await this.context.brain.get(req.params.id); if (data) { res.json({ success: true, data }); } else { res.status(404).json({ success: false, error: 'Not found' }); } } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // Delete endpoint app.delete('/api/delete/:id', async (req, res) => { try { await this.context.brain.delete(req.params.id); res.json({ success: true }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // Relate endpoint app.post('/api/relate', async (req, res) => { try { const { source, target, verb, metadata } = req.body; await this.context.brain.relate(source, target, verb, metadata); res.json({ success: true }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // Find endpoint (complex queries) app.post('/api/find', async (req, res) => { try { const results = await this.context.brain.find(req.body); res.json({ success: true, results }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // Cluster endpoint app.post('/api/cluster', async (req, res) => { try { const { algorithm = 'kmeans', options = {} } = req.body; const clusters = await this.context.brain.cluster(algorithm, options); res.json({ success: true, clusters }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // MCP endpoint app.post('/api/mcp', async (req, res) => { try { const response = await this.mcpService.handleRequest(req.body); res.json(response); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // Statistics endpoint app.get('/api/stats', async (_req, res) => { try { const stats = await this.context.brain.getStatistics(); res.json({ success: true, stats }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // Operation history endpoint app.get('/api/history', (_req, res) => { res.json({ success: true, history: this.operationHistory.slice(-100) }); }); } /** * Setup WebSocket server */ setupWebSocketServer() { if (!this.wsServer) return; this.wsServer.on('connection', (socket, request) => { const clientId = uuidv4(); const client = { id: clientId, type: 'websocket', socket, subscriptions: [], lastSeen: Date.now() }; this.clients.set(clientId, client); // Send welcome message socket.send(JSON.stringify({ type: 'welcome', clientId, message: 'Connected to Brainy API Server', capabilities: ['search', 'add', 'delete', 'relate', 'subscribe', 'mcp'] })); // Handle messages socket.on('message', async (message) => { try { const msg = JSON.parse(message); await this.handleWebSocketMessage(msg, client); } catch (error) { socket.send(JSON.stringify({ type: 'error', error: error.message })); } }); // Handle disconnect socket.on('close', () => { this.clients.delete(clientId); this.log(`Client ${clientId} disconnected`); }); // Handle errors socket.on('error', (error) => { this.log(`WebSocket error for client ${clientId}: ${error}`, 'error'); }); }); } /** * Handle WebSocket message */ async handleWebSocketMessage(msg, client) { const { socket } = client; switch (msg.type) { case 'subscribe': // Subscribe to operation types client.subscriptions = msg.operations || ['all']; socket.send(JSON.stringify({ type: 'subscribed', operations: client.subscriptions })); break; case 'search': const searchResults = await this.context.brain.search(msg.query, msg.limit || 10, msg.options || {}); socket.send(JSON.stringify({ type: 'searchResults', requestId: msg.requestId, results: searchResults })); break; case 'add': const id = await this.context.brain.addNoun(msg.content, 'Content', msg.metadata); socket.send(JSON.stringify({ type: 'addResult', requestId: msg.requestId, id })); break; case 'mcp': const mcpResponse = await this.mcpService.handleRequest(msg.request); socket.send(JSON.stringify({ type: 'mcpResponse', requestId: msg.requestId, response: mcpResponse })); break; case 'heartbeat': client.lastSeen = Date.now(); socket.send(JSON.stringify({ type: 'heartbeat', timestamp: Date.now() })); break; default: socket.send(JSON.stringify({ type: 'error', error: `Unknown message type: ${msg.type}` })); } } /** * Execute augmentation - broadcast operations to clients */ async execute(operation, params, next) { const startTime = Date.now(); const result = await next(); const duration = Date.now() - startTime; // Record operation in history const historyEntry = { operation, params: this.sanitizeParams(params), timestamp: Date.now(), duration }; this.operationHistory.push(historyEntry); if (this.operationHistory.length > this.maxHistorySize) { this.operationHistory.shift(); } // Broadcast to subscribed WebSocket clients const message = JSON.stringify({ type: 'operation', operation, params: historyEntry.params, timestamp: historyEntry.timestamp, duration }); for (const client of this.clients.values()) { if (client.type === 'websocket' && client.socket) { if (client.subscriptions?.includes('all') || client.subscriptions?.includes(operation)) { try { client.socket.send(message); } catch (error) { // Client might be disconnected this.clients.delete(client.id); } } } } return result; } /** * Auth middleware for Express */ authMiddleware(req, res, next) { if (!this.config.auth?.required) { return next(); } const apiKey = req.headers['x-api-key']; const bearerToken = req.headers.authorization?.replace('Bearer ', ''); if (apiKey && this.config.auth.apiKeys?.includes(apiKey)) { return next(); } if (bearerToken && this.config.auth.bearerTokens?.includes(bearerToken)) { return next(); } res.status(401).json({ error: 'Unauthorized' }); } /** * Rate limiting middleware */ rateLimitMiddleware(req, res, next) { // Simple in-memory rate limiting // In production, use redis or proper rate limiting library const ip = req.ip || req.connection.remoteAddress; const now = Date.now(); const windowMs = this.config.rateLimit?.windowMs || 60000; const max = this.config.rateLimit?.max || 100; // Clean old entries for (const [key, client] of this.clients.entries()) { if (now - client.lastSeen > windowMs) { this.clients.delete(key); } } // For now, just pass through // Real implementation would track requests per IP next(); } /** * Sanitize parameters before broadcasting */ sanitizeParams(params) { if (!params) return params; const sanitized = { ...params }; // Remove sensitive fields delete sanitized.password; delete sanitized.apiKey; delete sanitized.token; delete sanitized.secret; // Truncate large data if (sanitized.content && sanitized.content.length > 1000) { sanitized.content = sanitized.content.substring(0, 1000) + '...'; } return sanitized; } /** * Send heartbeats to all connected clients */ sendHeartbeats() { const now = Date.now(); for (const [id, client] of this.clients.entries()) { if (client.type === 'websocket' && client.socket) { // Remove inactive clients if (now - client.lastSeen > 60000) { this.clients.delete(id); continue; } // Send heartbeat try { client.socket.send(JSON.stringify({ type: 'heartbeat', timestamp: now })); } catch { // Client disconnected this.clients.delete(id); } } } } /** * Start Deno server */ async startDenoServer() { // Deno implementation would go here // Using Deno.serve() or oak framework this.log('Deno server not yet implemented', 'warn'); } /** * Start Service Worker (for browser) */ async startServiceWorker() { // Service Worker implementation would go here // Intercepts fetch() calls and handles them locally this.log('Service Worker API not yet implemented', 'warn'); } /** * Shutdown the server */ async onShutdown() { // Close all WebSocket connections for (const client of this.clients.values()) { if (client.socket) { try { client.socket.close(); } catch { } } } this.clients.clear(); // Close servers if (this.wsServer) { this.wsServer.close(); } if (this.httpServer) { await new Promise(resolve => { this.httpServer.close(() => resolve()); }); } this.log('API Server shut down'); } } /** * Helper function to create and configure API server */ export function createAPIServer(config) { return new APIServerAugmentation(config); } //# sourceMappingURL=apiServerAugmentation.js.map