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

324 lines • 13.8 kB
/** * Conversation Gateway - D1 Backend Communication * * Handles secure communication with Cloudflare D1 backend for: * - Pre-conversation authorization and usage validation * - Post-conversation verification and usage reporting * - License key validation and user profile fetching */ import { getLicenseKey } from '../subscription/validator.js'; import { getApiEndpoint } from '../config/endpoints.js'; import crypto from 'crypto'; /** * Gateway for secure D1 backend communication */ export class ConversationGateway { apiUrl; constructor() { this.apiUrl = getApiEndpoint('general'); } /** * Request conversation authorization from D1 backend * This is called BEFORE starting any consensus pipeline */ async requestConversationAuthorization(question) { const licenseKey = await getLicenseKey(); if (!licenseKey) { throw new Error('No license key found. Run: hive-ai configure'); } // Generate hash of the question for verification const questionHash = this.generateQuestionHash(question); console.log('šŸ” Requesting conversation authorization from D1...'); const requestData = { license_key: licenseKey, installation_id: await this.getInstallationId(), conversation_request_hash: questionHash }; console.log('šŸ“ Request data:', JSON.stringify(requestData, null, 2)); console.log('šŸ“” Endpoint:', `${this.apiUrl}/auth/pre-conversation`); try { const response = await fetch(`${this.apiUrl}/auth/pre-conversation`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${licenseKey}` }, body: JSON.stringify(requestData) }); if (!response.ok) { const error = await response.json(); console.log('āŒ D1 Response Error:', JSON.stringify(error, null, 2)); console.log('šŸ“Š Response Status:', response.status, response.statusText); throw new ConversationGatewayError(error.error || 'Authorization failed', error.code || 'AUTH_FAILED', error); } const result = await response.json(); if (!result.allowed) { throw new UsageLimitExceededError(result.error || 'Usage limit exceeded', result.used_conversations || 0, result.plan_limit || 0, result.plan || 'FREE'); } console.log(`āœ… Conversation authorized (${result.remaining_conversations} remaining)`); // Map D1 response format to our expected format return { conversationToken: result.token || result.conversation_token, questionHash, userId: result.user?.id || result.user_id, remaining: result.remaining || result.remaining_conversations, limit: result.limits?.daily || result.plan_limit || 10, expiresAt: new Date(result.expires_at || Date.now() + 3600000) // 1 hour default }; } catch (error) { if (error instanceof ConversationGatewayError || error instanceof UsageLimitExceededError) { throw error; } // Network or other errors throw new ConversationGatewayError(`Failed to connect to authorization server: ${error.message}`, 'NETWORK_ERROR', { originalError: error.message }); } } /** * Report conversation completion to D1 backend * This is called AFTER successful consensus pipeline completion */ async reportConversationCompletion(conversationToken, conversationId, questionHash) { if (!conversationToken || !conversationId || !questionHash) { throw new Error('Missing required parameters for conversation verification'); } // Generate usage proof HMAC const usageProof = this.generateUsageProof(conversationToken, conversationId, questionHash); console.log('āœ… Reporting conversation completion to D1...'); try { const response = await fetch(`${this.apiUrl}/auth/post-conversation`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conversation_token: conversationToken, conversation_id: conversationId, usage_proof: usageProof, timestamp: new Date().toISOString() }) }); if (!response.ok) { const error = await response.json(); console.warn('āš ļø Conversation verification failed:', error.error); // Don't throw here - we don't want to break the user's workflow // The server will handle the security implications return { verified: false, remainingConversations: 0, usageUpdated: false }; } const result = await response.json(); console.log(`āœ… Conversation verified (${result.remaining} remaining)`); // Map D1 response format to our expected format return { verified: result.success || result.verified, remainingConversations: result.remaining || result.remaining_conversations, usageUpdated: result.success || result.verified }; } catch (error) { console.warn('āš ļø Network error during conversation verification:', error.message); // Return failed verification but don't throw - queue for retry return { verified: false, remainingConversations: 0, usageUpdated: false }; } } /** * Validate license key against D1 backend and fetch user profile * This is called during license configuration */ async validateLicenseKey(licenseKey) { console.log('šŸ” Validating license key with D1 backend...'); try { const response = await fetch(`${this.apiUrl}/v1/session/validate`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${licenseKey}` }, body: JSON.stringify({ client_id: 'hive-tools', session_token: licenseKey, fingerprint: await this.getDeviceFingerprint(), nonce: Date.now().toString() }) }); if (!response.ok) { const error = await response.json(); throw new ConversationGatewayError(error.error || 'License validation failed', error.code || 'INVALID_LICENSE', error); } const result = await response.json(); if (!result.valid) { throw new ConversationGatewayError(result.error || 'Invalid license key', 'INVALID_LICENSE', result); } console.log(`āœ… License validated - ${result.tier} tier`); // Map D1 response format to our expected format return { userId: result.user?.id || result.user_id, email: result.user?.email || result.email, tier: result.user?.subscription_tier || result.tier || 'free', dailyLimit: result.limits?.daily || result.daily_limit || 10, features: result.features || ['consensus', 'setup_wizard'], isValid: result.valid || result.status === 'active' }; } catch (error) { if (error instanceof ConversationGatewayError) { throw error; } // Network or other errors throw new ConversationGatewayError(`Failed to validate license: ${error.message}`, 'NETWORK_ERROR', { originalError: error.message }); } } /** * Get quick usage status without full authorization * Used for status displays */ async getQuickUsageStatus() { try { const licenseKey = await getLicenseKey(); if (!licenseKey) return null; const response = await fetch(`${this.apiUrl}/v1/session/validate`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${licenseKey}` }, body: JSON.stringify({ client_id: 'hive-tools', session_token: licenseKey, fingerprint: await this.getDeviceFingerprint(), nonce: Date.now().toString() }) }); if (response.ok) { const result = await response.json(); return { remaining: result.remaining || result.usage?.remaining, limit: result.limits?.daily || result.usage?.limit || 10 }; } } catch (error) { // Silently fail - this is just for display purposes } return null; } /** * Generate a hash of the question for verification */ generateQuestionHash(question) { // Normalize the question (trim whitespace, convert to lowercase) const normalized = question.trim().toLowerCase(); return crypto.createHash('sha256').update(normalized, 'utf8').digest('hex'); } /** * Generate HMAC proof of conversation completion */ generateUsageProof(conversationToken, conversationId, questionHash) { const payload = `${conversationId}:${questionHash}`; return crypto.createHmac('sha256', conversationToken).update(payload).digest('hex'); } /** * Check if a conversation token is expired */ isTokenExpired(expiresAt) { return new Date() > new Date(expiresAt); } /** * Get installation ID from device/machine fingerprinting */ async getInstallationId() { // Use machine-specific identifiers for installation ID const os = await import('os'); const crypto = await import('crypto'); const machineData = { hostname: os.hostname(), platform: os.platform(), arch: os.arch(), homedir: os.homedir() }; const machineString = JSON.stringify(machineData); return crypto.createHash('sha256').update(machineString).digest('hex').substring(0, 16); } /** * Get device fingerprint for security validation */ async getDeviceFingerprint() { const os = await import('os'); const crypto = await import('crypto'); const deviceData = { platform: os.platform(), arch: os.arch(), release: os.release(), cpus: os.cpus().length, memory: Math.floor(os.totalmem() / 1024 / 1024) // MB }; const deviceString = JSON.stringify(deviceData); return crypto.createHash('sha256').update(deviceString).digest('hex').substring(0, 32); } } /** * Custom error class for conversation gateway failures */ export class ConversationGatewayError extends Error { code; details; constructor(message, code, details = {}) { super(message); this.name = 'ConversationGatewayError'; this.code = code; this.details = details; } } /** * Custom error class for usage limit exceeded */ export class UsageLimitExceededError extends Error { used; limit; plan; constructor(message, used, limit, plan) { super(message); this.name = 'UsageLimitExceededError'; this.used = used; this.limit = limit; this.plan = plan; } getFormattedMessage() { return `\\nāŒ ${this.message}\\n\\nšŸ“Š Plan: ${this.plan}\\nšŸ“ˆ Used: ${this.used}/${this.limit} conversations today\\n\\nā¬†ļø Upgrade at: https://hivetechs.io/pricing\\nšŸ’° Alternative: Purchase credit packs for additional conversations\\n`; } /** * Attempt automatic license refresh before showing error */ static async handleWithAutoRefresh(error) { try { console.log('šŸ”„ Checking for plan updates...'); // Import license gate and attempt refresh const { ModernLicenseGate } = await import('../core/modern-license-gate.js'); const licenseGate = ModernLicenseGate.getInstance(); const refreshedLicense = await licenseGate.refreshLicense(); // Check if daily limit increased (plan upgrade detected) if (refreshedLicense.dailyLimit && refreshedLicense.dailyLimit > error.limit) { console.log(`šŸŽ‰ Plan upgraded to ${refreshedLicense.tier}! You now have ${refreshedLicense.dailyLimit} daily conversations.`); console.log('✨ Continuing with your request...\\n'); return 'retry'; } // No upgrade detected, show normal error return 'show_error'; } catch (refreshError) { console.warn('āš ļø License refresh failed:', refreshError instanceof Error ? refreshError.message : 'Unknown error'); return 'show_error'; } } } // Export singleton instance export const conversationGateway = new ConversationGateway(); //# sourceMappingURL=conversation-gateway.js.map