UNPKG

@hivetechs/hive-ai

Version:

Real-time streaming AI consensus platform with HTTP+SSE MCP integration for Claude Code, VS Code, Cursor, and Windsurf - powered by OpenRouter's unified API

556 lines (552 loc) 21.9 kB
/** * User Management Module * * Handles user identification, authentication, and device registration * Supports multi-device usage with secure local tracking */ import { open } from 'sqlite'; import * as path from 'path'; import * as os from 'os'; import * as fs from 'fs'; import { v4 as uuidv4 } from 'uuid'; import * as crypto from 'crypto'; // Define the database directory and file path const DB_DIR = path.join(os.homedir(), '.hive-ai'); const DB_PATH = path.join(DB_DIR, 'hive-ai-users.db'); // Create the database directory if it doesn't exist if (!fs.existsSync(DB_DIR)) { fs.mkdirSync(DB_DIR, { recursive: true }); } let db; /** * Initialize the user database */ export async function initializeUserDatabase() { try { // Create directories if they don't exist if (!fs.existsSync(DB_DIR)) { fs.mkdirSync(DB_DIR, { recursive: true }); } // Open the database const sqlite3Driver = await import('sqlite3'); db = await open({ filename: DB_PATH, driver: sqlite3Driver.default.Database, }); // Create tables if they don't exist await db.exec(` -- Users table CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT UNIQUE, name TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, subscription_tier TEXT DEFAULT 'free', trial_start_date TEXT, trial_end_date TEXT, lemon_squeezy_customer_id TEXT, -- Legacy field, preserved for historical records lemon_squeezy_subscription_id TEXT, -- Legacy field, preserved for historical records gumroad_customer_id TEXT, gumroad_subscription_id TEXT, gumroad_product_id TEXT, additional_credits INTEGER DEFAULT 0, max_devices INTEGER DEFAULT 2, last_verified TEXT, account_status TEXT DEFAULT 'active' ); -- Device registrations table CREATE TABLE IF NOT EXISTS installations ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, machine_id TEXT NOT NULL, device_name TEXT, os_type TEXT, os_version TEXT, app_version TEXT, first_registered TEXT DEFAULT CURRENT_TIMESTAMP, last_active TEXT DEFAULT CURRENT_TIMESTAMP, is_active BOOLEAN DEFAULT true, FOREIGN KEY (user_id) REFERENCES users(id), UNIQUE(user_id, machine_id) ); -- Daily usage tracking table CREATE TABLE IF NOT EXISTS usage_tracking ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, installation_id TEXT NOT NULL, date TEXT NOT NULL, conversation_count INTEGER DEFAULT 0, last_synced TEXT, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (installation_id) REFERENCES installations(id), UNIQUE(user_id, installation_id, date) ); -- Monthly usage tracking table CREATE TABLE IF NOT EXISTS monthly_usage ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, year_month TEXT NOT NULL, conversation_count INTEGER DEFAULT 0, FOREIGN KEY (user_id) REFERENCES users(id), UNIQUE(user_id, year_month) ); -- Create indices for faster searching CREATE INDEX IF NOT EXISTS idx_installations_user ON installations(user_id); CREATE INDEX IF NOT EXISTS idx_usage_user_date ON usage_tracking(user_id, date); CREATE INDEX IF NOT EXISTS idx_usage_installation ON usage_tracking(installation_id); CREATE INDEX IF NOT EXISTS idx_monthly_user_month ON monthly_usage(user_id, year_month); `); console.log('[USER-DB] User database initialized successfully'); return true; } catch (error) { console.error('[USER-DB] User database initialization error:', error); return false; } } /** * Get or create a user * This is the main entry point for user identification */ export async function getOrCreateUser(email, name) { try { // Initialize database if needed if (!db) { await initializeUserDatabase(); } // Get the current machine ID const machineId = getMachineId(); // Check if this machine is already registered to a user const installation = await db.get('SELECT * FROM installations WHERE machine_id = ?', machineId); if (installation) { // This machine is already registered, get the user const user = await db.get('SELECT * FROM users WHERE id = ?', installation.user_id); if (user) { // Update last active time for this installation await db.run('UPDATE installations SET last_active = CURRENT_TIMESTAMP WHERE id = ?', installation.id); return { user, installation, isNewUser: false, isNewInstallation: false }; } } // No existing installation found, create a new user if email is provided // or generate an anonymous user if not const userId = uuidv4(); const userTier = 'free'; // Default to free tier // Create trial dates (7 days from now) const now = new Date(); const trialEnd = new Date(now); trialEnd.setDate(trialEnd.getDate() + 7); // Create the user await db.run(`INSERT INTO users ( id, email, name, subscription_tier, trial_start_date, trial_end_date, account_status ) VALUES (?, ?, ?, ?, ?, ?, ?)`, userId, email || null, name || 'Anonymous User', userTier, now.toISOString(), trialEnd.toISOString(), 'active'); // Register this machine const installationId = uuidv4(); const osInfo = { type: os.type(), platform: os.platform(), release: os.release() }; await db.run(`INSERT INTO installations ( id, user_id, machine_id, device_name, os_type, os_version, app_version ) VALUES (?, ?, ?, ?, ?, ?, ?)`, installationId, userId, machineId, os.hostname() || 'Unknown Device', osInfo.type, osInfo.release, process.env.npm_package_version || '1.0.0'); // Get the newly created user const newUser = await db.get('SELECT * FROM users WHERE id = ?', userId); const newInstallation = await db.get('SELECT * FROM installations WHERE id = ?', installationId); return { user: newUser, installation: newInstallation, isNewUser: true, isNewInstallation: true }; } catch (error) { console.error('[USER-DB] Error in getOrCreateUser:', error); throw error; } } /** * Register a new device for an existing user */ export async function registerNewDevice(userId, deviceName) { try { // Get the current machine ID const machineId = getMachineId(); // Check if this machine is already registered to this user const existingInstallation = await db.get('SELECT * FROM installations WHERE user_id = ? AND machine_id = ?', userId, machineId); if (existingInstallation) { // This machine is already registered to this user return { installation: existingInstallation, isNewInstallation: false }; } // Check if user has reached device limit const user = await db.get('SELECT * FROM users WHERE id = ?', userId); if (!user) { throw new Error('User not found'); } const deviceCount = await db.get('SELECT COUNT(*) as count FROM installations WHERE user_id = ? AND is_active = 1', userId); if (deviceCount.count >= user.max_devices) { throw new Error(`Device limit reached (${user.max_devices}). Please deactivate an existing device.`); } // Register this machine const installationId = uuidv4(); const osInfo = { type: os.type(), platform: os.platform(), release: os.release() }; await db.run(`INSERT INTO installations ( id, user_id, machine_id, device_name, os_type, os_version, app_version ) VALUES (?, ?, ?, ?, ?, ?, ?)`, installationId, userId, machineId, deviceName || os.hostname() || 'Unknown Device', osInfo.type, osInfo.release, process.env.npm_package_version || '1.0.0'); const newInstallation = await db.get('SELECT * FROM installations WHERE id = ?', installationId); return { installation: newInstallation, isNewInstallation: true }; } catch (error) { console.error('[USER-DB] Error in registerNewDevice:', error); throw error; } } /** * Get a secure machine ID * This creates a unique identifier for the current machine using built-in OS information */ export function getMachineId() { try { // Use a combination of hardware and OS information to create a unique identifier const networkInterfaces = os.networkInterfaces(); // Get MAC address of the first non-internal interface const interfaces = Object.values(networkInterfaces).flat(); const iface = interfaces.find(info => info && !info.internal && info.mac && info.mac !== '00:00:00:00:00:00'); const macAddress = iface?.mac || ''; // Combine various system-specific values const systemData = [ os.hostname(), os.platform(), os.arch(), os.cpus()[0]?.model || '', os.totalmem().toString(), macAddress, // Add CPU info and other hardware identifiers JSON.stringify(os.cpus().map(cpu => cpu.model).slice(0, 2)), // Add drive information if available process.env.SystemDrive || process.env.HOME || '' ].join('|'); // Create a hash of the system data const machineId = crypto.createHash('sha256').update(systemData).digest('hex'); // Store the machine ID in a persistent location for future use const idPath = path.join(DB_DIR, '.machine-id'); try { // Check if we already have a stored ID if (fs.existsSync(idPath)) { const storedId = fs.readFileSync(idPath, 'utf8').trim(); if (storedId && storedId.length === 64) { return storedId; // Use the stored ID if it exists and is valid } } // Store the new ID for future use fs.writeFileSync(idPath, machineId); } catch (fsError) { // If we can't write to the file, just use the generated ID console.error('[USER-DB] Error storing machine ID:', fsError); } return machineId; } catch (error) { // Fallback to a simpler identifier if the above fails const fallbackData = [ os.hostname() || 'unknown-host', os.platform() || 'unknown-platform', os.arch() || 'unknown-arch', process.env.USER || process.env.USERNAME || 'unknown-user' ].join('|'); return crypto.createHash('sha256').update(fallbackData).digest('hex'); } } /** * Increment conversation count for the current user and installation */ export async function incrementConversationCount(userId, installationId) { try { if (!db) { await initializeUserDatabase(); } const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD const currentMonth = today.substring(0, 7); // YYYY-MM // Try to update existing record for today const result = await db.run(`UPDATE usage_tracking SET conversation_count = conversation_count + 1 WHERE user_id = ? AND installation_id = ? AND date = ?`, userId, installationId, today); // If no record exists for today, create one if (result.changes === 0) { await db.run(`INSERT INTO usage_tracking (user_id, installation_id, date, conversation_count) VALUES (?, ?, ?, 1)`, userId, installationId, today); } // Update monthly usage as well const monthlyResult = await db.run(`UPDATE monthly_usage SET conversation_count = conversation_count + 1 WHERE user_id = ? AND year_month = ?`, userId, currentMonth); // If no record exists for this month, create one if (monthlyResult.changes === 0) { await db.run(`INSERT INTO monthly_usage (user_id, year_month, conversation_count) VALUES (?, ?, 1)`, userId, currentMonth); } return true; } catch (error) { console.error('[USER-DB] Error incrementing conversation count:', error); return false; } } /** * Check if usage limits are exceeded for a user */ export async function checkUsageLimits(userId) { try { if (!db) { await initializeUserDatabase(); } // Get user subscription info const user = await db.get('SELECT * FROM users WHERE id = ?', userId); if (!user) { throw new Error('User not found'); } // Define limits based on subscription tier let dailyLimit = 5; // Default free tier let monthlyLimit = 100; switch (user.subscription_tier) { case 'basic': dailyLimit = 50; monthlyLimit = 1000; break; case 'professional': dailyLimit = 200; monthlyLimit = 5000; break; case 'enterprise': dailyLimit = 1000; monthlyLimit = 25000; break; } // Check if user is in trial period const now = new Date(); const trialEnd = user.trial_end_date ? new Date(user.trial_end_date) : null; if (trialEnd && now < trialEnd) { // User is in trial period, unlimited usage return { withinLimits: true, tier: user.subscription_tier, inTrial: true, trialDaysLeft: Math.ceil((trialEnd.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)), dailyUsage: 0, monthlyUsage: 0, dailyLimit: 'unlimited', monthlyLimit: 'unlimited' }; } // Get current usage const today = new Date().toISOString().split('T')[0]; const currentMonth = today.substring(0, 7); // Get daily usage (sum across all devices) const dailyUsageResult = await db.get(`SELECT SUM(conversation_count) as total FROM usage_tracking WHERE user_id = ? AND date = ?`, userId, today); // Get monthly usage const monthlyUsageResult = await db.get(`SELECT SUM(conversation_count) as total FROM monthly_usage WHERE user_id = ? AND year_month = ?`, userId, currentMonth); const dailyUsage = dailyUsageResult?.total || 0; const monthlyUsage = monthlyUsageResult?.total || 0; // Check if limits are exceeded const dailyLimitExceeded = dailyUsage >= dailyLimit; const monthlyLimitExceeded = monthlyUsage >= monthlyLimit; // Calculate percentage of limits used const dailyPercentage = Math.round((dailyUsage / dailyLimit) * 100); const monthlyPercentage = Math.round((monthlyUsage / monthlyLimit) * 100); return { withinLimits: !dailyLimitExceeded && !monthlyLimitExceeded, tier: user.subscription_tier, inTrial: false, dailyUsage, monthlyUsage, dailyLimit, monthlyLimit, dailyPercentage, monthlyPercentage, warning: dailyPercentage > 80 ? `You've used ${dailyPercentage}% of your daily limit` : monthlyPercentage > 80 ? `You've used ${monthlyPercentage}% of your monthly limit` : null }; } catch (error) { console.error('[USER-DB] Error checking usage limits:', error); // Default to allowing usage in case of errors return { withinLimits: true, error: 'Error checking limits' }; } } /** * Get unsynchronized usage data for a user */ export async function getUnsyncedUsageData(userId, since) { try { if (!db) { await initializeUserDatabase(); } const sinceDate = since || new Date(0); // Get daily usage data since last sync const dailyData = await db.all(`SELECT user_id, installation_id, date, conversation_count FROM usage_tracking WHERE user_id = ? AND (last_synced IS NULL OR datetime(last_synced) < datetime(?)) ORDER BY date ASC`, userId, sinceDate.toISOString()); return dailyData; } catch (error) { console.error('[USER-DB] Error getting unsynced usage data:', error); return []; } } /** * Mark usage data as synchronized */ export async function markUsageAsSynced(userId, records) { try { if (!db) { await initializeUserDatabase(); } const now = new Date().toISOString(); for (const record of records) { await db.run(`UPDATE usage_tracking SET last_synced = ? WHERE user_id = ? AND installation_id = ? AND date = ?`, now, userId, record.installation_id, record.date); } return true; } catch (error) { console.error('[USER-DB] Error marking usage as synced:', error); return false; } } /** * Update user subscription information */ export async function updateUserSubscription(userId, tier, gumroadCustomerId, gumroadSubscriptionId, gumroadProductId, additionalCredits, lemonSqueezyCustomerId, // Legacy parameter, preserved for historical records lemonSqueezySubscriptionId // Legacy parameter, preserved for historical records ) { try { if (!db) { await initializeUserDatabase(); } // Update the user's subscription information await db.run(`UPDATE users SET subscription_tier = ?, gumroad_customer_id = COALESCE(?, gumroad_customer_id), gumroad_subscription_id = COALESCE(?, gumroad_subscription_id), gumroad_product_id = COALESCE(?, gumroad_product_id), additional_credits = COALESCE(?, additional_credits), lemon_squeezy_customer_id = COALESCE(?, lemon_squeezy_customer_id), lemon_squeezy_subscription_id = COALESCE(?, lemon_squeezy_subscription_id), last_verified = CURRENT_TIMESTAMP WHERE id = ?`, tier, gumroadCustomerId, gumroadSubscriptionId, gumroadProductId, additionalCredits, lemonSqueezyCustomerId, lemonSqueezySubscriptionId, userId); // Update max_devices based on tier let maxDevices = 2; // Default for free tier switch (tier) { case 'basic': maxDevices = 3; break; case 'professional': maxDevices = 5; break; case 'enterprise': maxDevices = 10; break; } await db.run('UPDATE users SET max_devices = ? WHERE id = ?', maxDevices, userId); return true; } catch (error) { console.error('[USER-DB] Error updating user subscription:', error); return false; } } /** * List all devices registered to a user */ export async function listUserDevices(userId) { try { if (!db) { await initializeUserDatabase(); } return await db.all(`SELECT * FROM installations WHERE user_id = ? ORDER BY last_active DESC`, userId); } catch (error) { console.error('[USER-DB] Error listing user devices:', error); return []; } } /** * Deactivate a device */ export async function deactivateDevice(userId, installationId) { try { if (!db) { await initializeUserDatabase(); } // Verify the device belongs to the user const installation = await db.get('SELECT * FROM installations WHERE id = ? AND user_id = ?', installationId, userId); if (!installation) { throw new Error('Device not found or does not belong to user'); } // Deactivate the device await db.run('UPDATE installations SET is_active = 0 WHERE id = ?', installationId); return true; } catch (error) { console.error('[USER-DB] Error deactivating device:', error); return false; } } // Initialize the database when the module is loaded initializeUserDatabase().catch(console.error); /** * Execute a database query directly * This is used by the cloudSync module to access the database */ export async function executeQuery(query, params = []) { try { if (!db) { await initializeUserDatabase(); } // Determine the type of query (get, all, run) const queryType = query.trim().toLowerCase().startsWith('select') ? (query.includes('where') && !query.includes('*') ? 'get' : 'all') : 'run'; // Execute the appropriate query type switch (queryType) { case 'get': return await db.get(query, ...params); case 'all': return await db.all(query, ...params); case 'run': return await db.run(query, ...params); default: throw new Error('Unknown query type'); } } catch (error) { console.error('[USER-DB] Error executing query:', error); throw error; } } //# sourceMappingURL=userManager.js.map