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