navflow-browser-server
Version:
Standalone Playwright browser server for NavFlow - enables browser automation with API key authentication, workspace device management, session sync, and requires Node.js v22+
572 lines • 23 kB
JavaScript
"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