UNPKG

navflow-browser-server

Version:

Standalone Playwright browser server for NavFlow - enables browser automation with API key authentication, workspace device management, session sync, LLM discovery tools, and requires Node.js v22+

572 lines 23 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DeviceRegistry = void 0; const os_1 = require("os"); const crypto_1 = require("crypto"); const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); const axios_1 = __importDefault(require("axios")); const ws_1 = __importDefault(require("ws")); class DeviceRegistry { constructor() { this.deviceInfo = null; this.ws = null; this.browserConfig = null; this.configPath = path.join(process.cwd(), 'device-config.json'); this.proxyServerUrl = process.env.PROXY_SERVER_URL || 'https://navflow-proxy-858493283701.us-central1.run.app'; } /** * Get the MAC address of the primary network interface */ getMacAddress() { const interfaces = (0, os_1.networkInterfaces)(); // Try to find the primary network interface (usually en0 on macOS, eth0 on Linux) const primaryInterfaces = ['en0', 'eth0', 'wlan0']; for (const interfaceName of primaryInterfaces) { const networkInterface = interfaces[interfaceName]; if (networkInterface) { const validInterface = networkInterface.find((iface) => !iface.internal && iface.mac !== '00:00:00:00:00:00'); if (validInterface) { return validInterface.mac; } } } // Fallback: find any non-internal interface with a valid MAC for (const [name, networkInterface] of Object.entries(interfaces)) { if (networkInterface) { const validInterface = networkInterface.find((iface) => !iface.internal && iface.mac !== '00:00:00:00:00:00'); if (validInterface) { return validInterface.mac; } } } throw new Error('No valid MAC address found'); } /** * Generate a secure API key */ generateApiKey() { return (0, crypto_1.randomBytes)(32).toString('hex'); } /** * Generate a default device name based on hostname and MAC */ generateDeviceName(macAddress) { const hostname = require('os').hostname(); const macSuffix = macAddress.slice(-5).replace(':', ''); return `${hostname}-${macSuffix}`; } /** * Load existing device configuration from disk */ async loadDeviceConfig() { try { const configData = await fs.readFile(this.configPath, 'utf-8'); return JSON.parse(configData); } catch (error) { // Config file doesn't exist or is invalid return null; } } /** * Save device configuration to disk */ async saveDeviceConfig(deviceInfo) { await fs.writeFile(this.configPath, JSON.stringify(deviceInfo, null, 2)); } /** * Initialize or load device information */ async initialize(port) { // Try to load existing config let deviceInfo = await this.loadDeviceConfig(); if (deviceInfo) { // Verify MAC address hasn't changed const currentMac = this.getMacAddress(); if (deviceInfo.macAddress === currentMac) { // Update port if it changed if (deviceInfo.port !== port) { deviceInfo.port = port; await this.saveDeviceConfig(deviceInfo); } this.deviceInfo = deviceInfo; return deviceInfo; } else { console.warn('MAC address changed, generating new device configuration'); } } // Generate new device configuration const macAddress = this.getMacAddress(); const apiKey = this.generateApiKey(); const deviceName = this.generateDeviceName(macAddress); deviceInfo = { macAddress, apiKey, deviceName, port, createdAt: new Date().toISOString() }; await this.saveDeviceConfig(deviceInfo); this.deviceInfo = deviceInfo; return deviceInfo; } /** * Get current device information */ getDeviceInfo() { return this.deviceInfo; } /** * Display device information to console in a user-friendly format */ displayDeviceInfo() { if (!this.deviceInfo) { console.error('Device not initialized'); return; } console.log('\n' + '='.repeat(80)); console.log('🔐 NAVFLOW DEVICE REGISTRATION'); console.log('='.repeat(80)); console.log(`Device Name: ${this.deviceInfo.deviceName}`); console.log(`MAC Address: ${this.deviceInfo.macAddress}`); console.log(`Port: ${this.deviceInfo.port}`); console.log(`Created: ${new Date(this.deviceInfo.createdAt).toLocaleString()}`); console.log(''); console.log('📋 API KEY (Copy this to your NavFlow webapp):'); console.log('─'.repeat(80)); console.log(`${this.deviceInfo.apiKey}`); console.log('─'.repeat(80)); console.log(''); console.log('📝 To add this device to your workspace:'); console.log(' 1. Open your NavFlow webapp'); console.log(' 2. Go to Workspace Settings → Devices'); console.log(' 3. Click "Add Device" and paste the API key above'); console.log(''); console.log('✅ Device is ready to receive connections!'); console.log('='.repeat(80) + '\n'); } /** * Check if device is registered with proxy-server */ async checkProxyRegistration() { if (!this.deviceInfo) { return false; } try { const response = await axios_1.default.get(`${this.proxyServerUrl}/api/devices/by-api-key/${this.deviceInfo.apiKey}`, { timeout: 10000 }); if (response.status === 200 && response.data.success) { console.log('✅ Device found in proxy-server registry'); return true; } return false; } catch (error) { if (error.response?.status === 404) { console.log('📝 Device not found in proxy-server registry'); return false; } if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) { console.warn('⚠️ Proxy-server connection timeout - may be deploying or restarting'); } else { console.warn('⚠️ Failed to check proxy registration:', error.message); } return false; } } /** * Get the accessible IP address for this device */ getAccessibleIpAddress() { // For local development, use localhost // In production, this could be the public IP or container IP return 'localhost'; } /** * Register device with proxy-server * @param {string} workspaceId - Optional workspace ID to associate device with * @param {string} userId - Optional user ID who is registering the device */ async registerWithProxy(workspaceId, userId) { if (!this.deviceInfo) { console.error('❌ Cannot register with proxy - device not initialized'); return false; } try { console.log('📤 Registering device with proxy-server...'); const ipAddress = this.getAccessibleIpAddress(); console.log(`📍 Registering with IP: ${ipAddress}:${this.deviceInfo.port}`); if (workspaceId) { console.log(`🏢 Associating with workspace: ${workspaceId}`); } if (userId) { console.log(`👤 Registering for user: ${userId}`); } const requestBody = { apiKey: this.deviceInfo.apiKey, ipAddress: ipAddress, port: this.deviceInfo.port, deviceInfo: { name: this.deviceInfo.deviceName, macAddress: this.deviceInfo.macAddress } }; // Only include workspaceId and userId if they are provided if (workspaceId) { requestBody.workspaceId = workspaceId; } if (userId) { requestBody.userId = userId; } const response = await axios_1.default.post(`${this.proxyServerUrl}/api/devices/register`, requestBody, { headers: { 'Content-Type': 'application/json' }, timeout: 15000 }); if (response.status === 200 && response.data.success) { console.log('✅ Device successfully registered with proxy-server'); console.log(`📄 Proxy registration ID: ${response.data.device.id}`); return true; } console.error('❌ Proxy registration failed:', response.data.message); return false; } catch (error) { console.error('❌ Failed to register with proxy-server:', error.message); if (error.response?.data) { console.error('📋 Proxy error details:', JSON.stringify(error.response.data, null, 2)); console.error('📊 HTTP Status:', error.response.status); } if (error.code) { console.error('🔧 Error code:', error.code); } return false; } } /** * Ensure device is registered with proxy-server * Checks registration status and re-registers if needed */ async ensureProxyRegistration() { if (!this.deviceInfo) { console.warn('⚠️ Cannot check proxy registration - device not initialized'); return; } console.log('🔍 Checking proxy-server registration status...'); try { const isRegistered = await this.checkProxyRegistration(); if (!isRegistered) { console.log('🔄 Device not registered with proxy-server, registering now...'); const success = await this.registerWithProxy(); if (!success) { console.warn('⚠️ Failed to register with proxy-server - device will work locally but may not be accessible via proxy'); } } else { console.log('✅ Device already registered with proxy-server'); } } catch (error) { console.warn('⚠️ Proxy registration check failed:', error.message); console.log('💡 Device will continue to work locally'); } } /** * Establish WebSocket connection to proxy-server for device communication */ async connectToProxyServer() { if (!this.deviceInfo) { console.warn('⚠️ Cannot connect to proxy-server - device not initialized'); return; } try { // Convert HTTPS URL to WSS URL const wsUrl = this.proxyServerUrl.replace('https://', 'wss://').replace('http://', 'ws://') + '/device-ws'; console.log('🔌 Connecting to proxy-server via WebSocket...'); console.log(`📍 WebSocket URL: ${wsUrl}`); this.ws = new ws_1.default(wsUrl, { headers: { 'X-Device-API-Key': this.deviceInfo.apiKey, 'X-Device-Name': this.deviceInfo.deviceName, 'X-Device-MAC': this.deviceInfo.macAddress } }); this.ws.on('open', () => { console.log('✅ WebSocket connection to proxy-server established'); // Send device authentication message this.ws?.send(JSON.stringify({ type: 'device_auth', apiKey: this.deviceInfo?.apiKey, deviceName: this.deviceInfo?.deviceName, macAddress: this.deviceInfo?.macAddress, port: this.deviceInfo?.port })); }); this.ws.on('message', async (data) => { try { const message = JSON.parse(data.toString()); console.log('📥 Received message from proxy-server:', message.type); if (message.type === 'http_request') { // Handle HTTP request forwarded from proxy-server await this.handleProxiedRequest(message); } else if (message.type === 'ping') { // Respond to ping this.ws?.send(JSON.stringify({ type: 'pong', timestamp: new Date().toISOString() })); } else if (message.type === 'config_update') { // Handle configuration update await this.handleConfigurationUpdate(message); } } catch (error) { console.error('❌ Error processing WebSocket message:', error); } }); this.ws.on('close', (code, reason) => { console.log(`🔌 WebSocket connection closed: ${code} ${reason}`); // TODO: Implement reconnection logic setTimeout(() => { console.log('🔄 Attempting to reconnect to proxy-server...'); this.connectToProxyServer(); }, 5000); }); this.ws.on('error', (error) => { console.error('❌ WebSocket error:', error); }); } catch (error) { console.error('❌ Failed to establish WebSocket connection:', error); } } /** * Handle HTTP request forwarded from proxy-server */ async handleProxiedRequest(message) { try { const { method, url, headers, body } = message; // Make local HTTP request to our own server const localUrl = `http://localhost:${this.deviceInfo?.port}${url}`; console.log(`🔄 Processing proxied ${method} ${url}`); const response = await (0, axios_1.default)({ method: method.toLowerCase(), url: localUrl, data: body, headers: { ...headers, 'Authorization': `Bearer ${this.deviceInfo?.apiKey}` // Add our own auth }, validateStatus: () => true // Accept all status codes }); // Send response back to proxy-server this.ws?.send(JSON.stringify({ type: 'http_response', requestId: message.requestId, statusCode: response.status, headers: response.headers, body: response.data })); } catch (error) { console.error('❌ Error handling proxied request:', error); // Send error response back to proxy-server this.ws?.send(JSON.stringify({ type: 'http_response', requestId: message.requestId, statusCode: 500, headers: { 'Content-Type': 'application/json' }, body: { error: 'Internal server error', message: error instanceof Error ? error.message : String(error) } })); } } /** * Sync session data with proxy-server * @param {string} sessionId - Browser session ID * @param {Object} sessionData - Session data to sync * @param {string} workspaceId - Workspace ID * @param {string} userId - User ID */ async syncSessionWithProxy(sessionId, sessionData, workspaceId, userId) { if (!this.deviceInfo) { console.error('❌ Cannot sync session - device not initialized'); return false; } try { const response = await axios_1.default.post(`${this.proxyServerUrl}/api/devices/${this.deviceInfo.apiKey}/sessions`, { sessionId: sessionId, sessionData: sessionData, workspaceId: workspaceId, userId: userId }, { headers: { 'Content-Type': 'application/json' }, timeout: 10000 }); if (response.status === 200 && response.data.success) { console.log(`✅ Session ${sessionId} synced with proxy-server`); return true; } console.error('❌ Session sync failed:', response.data.message); return false; } catch (error) { console.error('❌ Failed to sync session with proxy-server:', error.message); return false; } } /** * Get synced sessions from proxy-server * @param {string} userId - User ID to filter sessions */ async getSyncedSessions(userId) { if (!this.deviceInfo) { console.error('❌ Cannot get sessions - device not initialized'); return []; } try { const url = `${this.proxyServerUrl}/api/devices/${this.deviceInfo.apiKey}/sessions${userId ? `?userId=${userId}` : ''}`; const response = await axios_1.default.get(url, { headers: { 'Content-Type': 'application/json' }, timeout: 10000 }); if (response.status === 200 && response.data.success) { return response.data.sessions || []; } console.error('❌ Failed to get sessions:', response.data.message); return []; } catch (error) { console.error('❌ Failed to get sessions from proxy-server:', error.message); return []; } } /** * Close WebSocket connection */ closeWebSocket() { if (this.ws && this.ws.readyState === ws_1.default.OPEN) { console.log('🔌 Closing WebSocket connection to proxy-server...'); this.ws.close(1000, 'Graceful shutdown'); this.ws = null; } } /** * Handle configuration update from proxy-server */ async handleConfigurationUpdate(message) { try { const { browserSettings, deviceId, timestamp } = message; if (!browserSettings) { console.warn('⚠️ Received config update without browserSettings'); return; } console.log('🔧 Received browser configuration update:', { deviceId, timestamp, settings: browserSettings }); // Update the stored configuration this.browserConfig = browserSettings; console.log('✅ Browser configuration updated successfully'); console.log('📝 New configuration will apply to new browser sessions'); // Send acknowledgment back to proxy-server this.ws?.send(JSON.stringify({ type: 'config_update_ack', deviceId, timestamp: new Date().toISOString(), status: 'success' })); } catch (error) { console.error('❌ Error handling configuration update:', error); // Send error acknowledgment this.ws?.send(JSON.stringify({ type: 'config_update_ack', deviceId: message.deviceId, timestamp: new Date().toISOString(), status: 'error', error: error instanceof Error ? error.message : String(error) })); } } /** * Get current browser configuration */ getBrowserConfig() { return this.browserConfig; } /** * Update browser configuration */ updateBrowserConfig(config) { this.browserConfig = { ...this.browserConfig, ...config }; console.log('🔧 Browser configuration updated locally:', this.browserConfig); } /** * Get authentication middleware for Express */ getAuthMiddleware() { return (req, res, next) => { if (!this.deviceInfo) { return res.status(500).json({ error: 'Device not initialized' }); } // Check for API key in Authorization header or query parameter const authHeader = req.headers.authorization; const apiKeyFromQuery = req.query.apiKey; let providedApiKey = null; if (authHeader && authHeader.startsWith('Bearer ')) { providedApiKey = authHeader.substring(7); } else if (apiKeyFromQuery) { providedApiKey = apiKeyFromQuery; } if (!providedApiKey) { return res.status(401).json({ error: 'API key required' }); } if (providedApiKey !== this.deviceInfo.apiKey) { return res.status(403).json({ error: 'Invalid API key' }); } // Add device info to request req.deviceInfo = this.deviceInfo; next(); }; } } exports.DeviceRegistry = DeviceRegistry; //# sourceMappingURL=DeviceRegistry.js.map