@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
JavaScript
/**
* 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