import { libx } from 'libx.js/build/bundles/essentials.js'; export interface ApiKeyData { id: number; apiKey: string; userId: string; name?: string; description?: string; createdAt: number; updatedAt: number; lastUsedAt?: number; isActive: boolean; rateLimitRpm?: number; rateLimitTpm?: number; usageCount: number; billingTier?: string; balanceTokens?: number; monthlyQuotaTokens?: number; metadata?: string; } export interface ProviderKeyData { id: number; apiKeyId: number; provider: string; encryptedKey: string; isActive: boolean; createdAt: number; updatedAt: number; } export interface CreateApiKeyRequest { userId: string; name?: string; description?: string; providerKeys?: Record; // { provider: apiKey } rateLimitRpm?: number; rateLimitTpm?: number; metadata?: Record; } export interface UsageLog { apiKeyId: number; provider: string; model: string; promptTokens?: number; completionTokens?: number; totalTokens?: number; latencyMs?: number; success: boolean; errorMessage?: string; } export class ApiKeyManager { private db: D1Database; private encryptionKey: string; constructor(db: D1Database, encryptionKey: string) { this.db = db; this.encryptionKey = encryptionKey; } /** * Generate a secure API key with Ask prefix */ generateApiKey(): string { // Generate a cryptographically secure random key const array = new Uint8Array(32); crypto.getRandomValues(array); const key = Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); return `ask_${key}`; } /** * Validate API key format */ isValidApiKeyFormat(apiKey: string): boolean { return /^ask_[a-f0-9]{64}$/.test(apiKey); } /** * Create a new Ask API key */ async createApiKey(request: CreateApiKeyRequest): Promise { const apiKey = this.generateApiKey(); const now = Math.floor(Date.now() / 1000); try { // Insert API key const result = await this.db.prepare(` INSERT INTO api_keys ( api_key, user_id, name, description, created_at, updated_at, rate_limit_rpm, rate_limit_tpm, metadata ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `).bind( apiKey, request.userId, request.name || null, request.description || null, now, now, request.rateLimitRpm || 60, request.rateLimitTpm || 100000, request.metadata ? JSON.stringify(request.metadata) : null ).run(); const apiKeyId = result.meta.last_row_id; // Insert provider keys if provided if (request.providerKeys) { for (const [provider, key] of Object.entries(request.providerKeys)) { await this.addProviderKey(apiKeyId, provider, key); } } // Fetch and return the created key const created = await this.getApiKeyById(apiKeyId); if (!created) { throw new Error('Failed to retrieve created API key'); } libx.log.i('ApiKeyManager: Created new API key', { apiKeyId, userId: request.userId }); return created; } catch (error: any) { libx.log.e('ApiKeyManager: Error creating API key', error); throw new Error(`Failed to create API key: ${error.message}`); } } /** * Get API key by key string */ async getApiKey(apiKey: string): Promise { try { const result = await this.db.prepare(` SELECT * FROM api_keys WHERE api_key = ? AND is_active = 1 `).bind(apiKey).first(); if (!result) return null; return this.mapApiKeyResult(result); } catch (error: any) { libx.log.e('ApiKeyManager: Error getting API key', error); return null; } } /** * Get API key by ID */ async getApiKeyById(id: number): Promise { try { const result = await this.db.prepare(` SELECT * FROM api_keys WHERE id = ? `).bind(id).first(); if (!result) return null; return this.mapApiKeyResult(result); } catch (error: any) { libx.log.e('ApiKeyManager: Error getting API key by ID', error); return null; } } /** * Get all API keys for a user */ async getApiKeysByUser(userId: string): Promise { try { const results = await this.db.prepare(` SELECT * FROM api_keys WHERE user_id = ? ORDER BY created_at DESC `).bind(userId).all(); return results.results.map(r => this.mapApiKeyResult(r)); } catch (error: any) { libx.log.e('ApiKeyManager: Error getting API keys by user', error); return []; } } /** * Update API key last used timestamp and usage count */ async updateLastUsed(apiKey: string): Promise { const now = Math.floor(Date.now() / 1000); try { await this.db.prepare(` UPDATE api_keys SET last_used_at = ?, usage_count = usage_count + 1 WHERE api_key = ? `).bind(now, apiKey).run(); } catch (error: any) { libx.log.e('ApiKeyManager: Error updating last used', error); } } /** * Revoke (deactivate) an API key */ async revokeApiKey(apiKey: string, userId: string): Promise { try { const result = await this.db.prepare(` UPDATE api_keys SET is_active = 0 WHERE api_key = ? AND user_id = ? `).bind(apiKey, userId).run(); libx.log.i('ApiKeyManager: Revoked API key', { apiKey: apiKey.substring(0, 10) + '...' }); return result.meta.changes > 0; } catch (error: any) { libx.log.e('ApiKeyManager: Error revoking API key', error); return false; } } /** * Delete an API key permanently */ async deleteApiKey(apiKey: string, userId: string): Promise { try { const result = await this.db.prepare(` DELETE FROM api_keys WHERE api_key = ? AND user_id = ? `).bind(apiKey, userId).run(); libx.log.i('ApiKeyManager: Deleted API key', { apiKey: apiKey.substring(0, 10) + '...' }); return result.meta.changes > 0; } catch (error: any) { libx.log.e('ApiKeyManager: Error deleting API key', error); return false; } } /** * Add or update a provider key for an API key */ async addProviderKey(apiKeyId: number, provider: string, providerApiKey: string): Promise { const now = Math.floor(Date.now() / 1000); const encrypted = this.encryptKey(providerApiKey); try { // Use REPLACE to handle upsert (SQLite specific) await this.db.prepare(` INSERT OR REPLACE INTO provider_keys (api_key_id, provider, encrypted_key, is_active, created_at, updated_at) VALUES (?, ?, ?, 1, ?, ?) `).bind(apiKeyId, provider.toLowerCase(), encrypted, now, now).run(); libx.log.v('ApiKeyManager: Added provider key', { apiKeyId, provider }); } catch (error: any) { libx.log.e('ApiKeyManager: Error adding provider key', error); throw new Error(`Failed to add provider key: ${error.message}`); } } /** * Get all provider keys for an API key */ async getProviderKeys(apiKey: string): Promise> { try { // First get the API key ID const apiKeyData = await this.getApiKey(apiKey); if (!apiKeyData) { return {}; } // Get all provider keys const results = await this.db.prepare(` SELECT provider, encrypted_key FROM provider_keys WHERE api_key_id = ? AND is_active = 1 `).bind(apiKeyData.id).all(); const keys: Record = {}; for (const row of results.results) { const provider = (row as any).provider as string; const encrypted = (row as any).encrypted_key as string; keys[provider] = this.decryptKey(encrypted); } return keys; } catch (error: any) { libx.log.e('ApiKeyManager: Error getting provider keys', error); return {}; } } /** * Remove a provider key */ async removeProviderKey(apiKey: string, provider: string, userId: string): Promise { try { const result = await this.db.prepare(` DELETE FROM provider_keys WHERE api_key_id IN ( SELECT id FROM api_keys WHERE api_key = ? AND user_id = ? ) AND provider = ? `).bind(apiKey, userId, provider.toLowerCase()).run(); libx.log.i('ApiKeyManager: Removed provider key', { provider }); return result.meta.changes > 0; } catch (error: any) { libx.log.e('ApiKeyManager: Error removing provider key', error); return false; } } /** * Log usage for metering and analytics */ async logUsage(log: UsageLog): Promise { const now = Math.floor(Date.now() / 1000); try { await this.db.prepare(` INSERT INTO usage_logs ( api_key_id, provider, model, prompt_tokens, completion_tokens, total_tokens, latency_ms, success, error_message, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).bind( log.apiKeyId, log.provider, log.model, log.promptTokens || 0, log.completionTokens || 0, log.totalTokens || 0, log.latencyMs || 0, log.success ? 1 : 0, log.errorMessage || null, now ).run(); } catch (error: any) { libx.log.e('ApiKeyManager: Error logging usage', error); } } /** * Get usage statistics for an API key */ async getUsageStats(apiKey: string, startTime?: number, endTime?: number): Promise { try { const apiKeyData = await this.getApiKey(apiKey); if (!apiKeyData) { return null; } const now = Math.floor(Date.now() / 1000); const start = startTime || (now - 30 * 24 * 3600); // Last 30 days by default const end = endTime || now; const result = await this.db.prepare(` SELECT provider, COUNT(*) as request_count, SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as success_count, SUM(total_tokens) as total_tokens, AVG(latency_ms) as avg_latency_ms FROM usage_logs WHERE api_key_id = ? AND created_at BETWEEN ? AND ? GROUP BY provider `).bind(apiKeyData.id, start, end).all(); return { apiKey: apiKey.substring(0, 10) + '...', period: { start, end }, byProvider: result.results }; } catch (error: any) { libx.log.e('ApiKeyManager: Error getting usage stats', error); return null; } } /** * Encrypt a provider API key */ private encryptKey(key: string): string { // Simple XOR encryption with the encryption key // For production, use a proper encryption library (Web Crypto API) const encrypted: number[] = []; for (let i = 0; i < key.length; i++) { encrypted.push(key.charCodeAt(i) ^ this.encryptionKey.charCodeAt(i % this.encryptionKey.length)); } // Base64 encode return btoa(String.fromCharCode(...encrypted)); } /** * Decrypt a provider API key */ private decryptKey(encrypted: string): string { // Decode from base64 const decoded = atob(encrypted); const decrypted: number[] = []; for (let i = 0; i < decoded.length; i++) { decrypted.push(decoded.charCodeAt(i) ^ this.encryptionKey.charCodeAt(i % this.encryptionKey.length)); } return String.fromCharCode(...decrypted); } /** * Map database result to ApiKeyData */ private mapApiKeyResult(result: any): ApiKeyData { return { id: result.id, apiKey: result.api_key, userId: result.user_id, name: result.name, description: result.description, createdAt: result.created_at, updatedAt: result.updated_at, lastUsedAt: result.last_used_at, isActive: result.is_active === 1, rateLimitRpm: result.rate_limit_rpm, rateLimitTpm: result.rate_limit_tpm, usageCount: result.usage_count, billingTier: result.billing_tier, balanceTokens: result.balance_tokens, monthlyQuotaTokens: result.monthly_quota_tokens, metadata: result.metadata }; } }