Skip to main content
Glama
api-key-service.ts16.5 kB
import { ApiKey, ApiKeyWithSecret, ApiKeyStatus, ApiKeyScope, ApiKeyUsageLog, ApiKeyRotationConfig, ApiKeyValidationResult, CreateApiKeyInput, UpdateApiKeyInput, ValidateApiKeyInput, ApiKeyUsageLogInput, ApiKeyRotationConfigInput, CreateApiKeySchema, UpdateApiKeySchema, ApiKeyRotationConfigSchema, API_KEY_CONSTANTS } from '../models/api-key'; import { CryptoUtils } from '../utils/crypto'; /** * In-memory storage for API keys (in production, this would be a database) */ class ApiKeyStorage { private apiKeys: Map<string, ApiKey> = new Map(); private usageLogs: Map<string, ApiKeyUsageLog[]> = new Map(); private rotationConfigs: Map<string, ApiKeyRotationConfig> = new Map(); // API Key operations async createApiKey(apiKey: ApiKey): Promise<void> { this.apiKeys.set(apiKey.id, { ...apiKey }); } async getApiKey(id: string): Promise<ApiKey | null> { return this.apiKeys.get(id) || null; } async getApiKeyByHash(keyHash: string): Promise<ApiKey | null> { for (const apiKey of this.apiKeys.values()) { if (apiKey.keyHash === keyHash) { return { ...apiKey }; } } return null; } async getAllApiKeys(): Promise<ApiKey[]> { return Array.from(this.apiKeys.values()).map(key => ({ ...key })); } async updateApiKey(id: string, updates: Partial<ApiKey>): Promise<boolean> { const existing = this.apiKeys.get(id); if (!existing) return false; this.apiKeys.set(id, { ...existing, ...updates, updatedAt: new Date() }); return true; } async deleteApiKey(id: string): Promise<boolean> { const deleted = this.apiKeys.delete(id); if (deleted) { this.usageLogs.delete(id); this.rotationConfigs.delete(id); } return deleted; } // Usage log operations async addUsageLog(log: ApiKeyUsageLog): Promise<void> { const logs = this.usageLogs.get(log.keyId) || []; logs.push({ ...log }); // Keep only the last N logs per key if (logs.length > API_KEY_CONSTANTS.MAX_USAGE_LOGS_PER_KEY) { logs.splice(0, logs.length - API_KEY_CONSTANTS.MAX_USAGE_LOGS_PER_KEY); } this.usageLogs.set(log.keyId, logs); } async getUsageLogs(keyId: string, limit?: number): Promise<ApiKeyUsageLog[]> { const logs = this.usageLogs.get(keyId) || []; return limit ? logs.slice(-limit) : [...logs]; } // Rotation config operations async createRotationConfig(config: ApiKeyRotationConfig): Promise<void> { this.rotationConfigs.set(config.keyId, { ...config }); } async getRotationConfig(keyId: string): Promise<ApiKeyRotationConfig | null> { return this.rotationConfigs.get(keyId) || null; } async updateRotationConfig(keyId: string, updates: Partial<ApiKeyRotationConfig>): Promise<boolean> { const existing = this.rotationConfigs.get(keyId); if (!existing) return false; this.rotationConfigs.set(keyId, { ...existing, ...updates }); return true; } async deleteRotationConfig(keyId: string): Promise<boolean> { return this.rotationConfigs.delete(keyId); } async getKeysNeedingRotation(): Promise<ApiKeyRotationConfig[]> { const now = new Date(); return Array.from(this.rotationConfigs.values()) .filter(config => config.isActive && config.nextRotationAt <= now); } } /** * API Key Management Service */ export class ApiKeyService { private storage: ApiKeyStorage; constructor() { this.storage = new ApiKeyStorage(); this.initializeBootstrapKeys(); } /** * Initialize bootstrap API keys from environment variables */ private async initializeBootstrapKeys(): Promise<void> { const bootstrapKey = process.env.BOOTSTRAP_API_KEY; if (bootstrapKey && CryptoUtils.validateKeyFormat(bootstrapKey)) { const keyHash = CryptoUtils.hashApiKey(bootstrapKey); const existing = await this.storage.getApiKeyByHash(keyHash); if (!existing) { const apiKey: ApiKey = { id: CryptoUtils.generateId(), keyHash, name: 'Bootstrap Key', description: 'Initial bootstrap API key from environment', scopes: [ApiKeyScope.ADMIN], status: ApiKeyStatus.ACTIVE, createdAt: new Date(), updatedAt: new Date(), usageCount: 0, createdBy: 'system' }; await this.storage.createApiKey(apiKey); console.log('Bootstrap API key initialized'); } } } /** * Create a new API key */ async createApiKey(input: CreateApiKeyInput, createdBy?: string): Promise<ApiKeyWithSecret> { // Validate input const validatedInput = CreateApiKeySchema.parse(input); // Generate the API key const plainKey = CryptoUtils.generateApiKey(); const keyHash = CryptoUtils.hashApiKey(plainKey); // Create the API key object const apiKey: ApiKey = { id: CryptoUtils.generateId(), keyHash, name: validatedInput.name, ...(validatedInput.description && { description: validatedInput.description }), scopes: validatedInput.scopes, status: ApiKeyStatus.ACTIVE, createdAt: new Date(), updatedAt: new Date(), ...(validatedInput.expiresAt && { expiresAt: validatedInput.expiresAt }), usageCount: 0, ...(validatedInput.maxUsage && { maxUsage: validatedInput.maxUsage }), ...(validatedInput.ipWhitelist && { ipWhitelist: validatedInput.ipWhitelist }), ...(validatedInput.metadata && { metadata: validatedInput.metadata }), ...(createdBy && { createdBy }) }; // Store the API key await this.storage.createApiKey(apiKey); return { ...apiKey, key: plainKey }; } /** * Validate an API key */ async validateApiKey(input: ValidateApiKeyInput): Promise<ApiKeyValidationResult> { const { key, ipAddress, userAgent, endpoint } = input; // Basic format validation if (!CryptoUtils.validateKeyFormat(key)) { await this.logUsage(key, { success: false, errorReason: 'Invalid API key format', ipAddress, userAgent, endpoint }); return { isValid: false, errorReason: 'Invalid API key format' }; } // Find the API key const keyHash = CryptoUtils.hashApiKey(key); const apiKey = await this.storage.getApiKeyByHash(keyHash); if (!apiKey) { await this.logUsage(key, { success: false, errorReason: 'API key not found', ipAddress, userAgent, endpoint }); return { isValid: false, errorReason: 'API key not found' }; } // Check if key is active if (apiKey.status !== ApiKeyStatus.ACTIVE) { await this.logUsage(key, { success: false, errorReason: `API key is ${apiKey.status}`, ipAddress, userAgent, endpoint }); return { isValid: false, errorReason: `API key is ${apiKey.status}`, apiKey }; } // Check expiration if (apiKey.expiresAt && apiKey.expiresAt <= new Date()) { // Auto-expire the key await this.storage.updateApiKey(apiKey.id, { status: ApiKeyStatus.EXPIRED }); await this.logUsage(key, { success: false, errorReason: 'API key expired', ipAddress, userAgent, endpoint }); return { isValid: false, errorReason: 'API key expired', apiKey }; } // Check usage limits if (apiKey.maxUsage && apiKey.usageCount >= apiKey.maxUsage) { await this.logUsage(key, { success: false, errorReason: 'Usage limit exceeded', ipAddress, userAgent, endpoint }); return { isValid: false, errorReason: 'Usage limit exceeded', apiKey, remainingUsage: 0 }; } // Check IP whitelist if (apiKey.ipWhitelist && ipAddress) { const isIpAllowed = apiKey.ipWhitelist.includes(ipAddress); if (!isIpAllowed) { await this.logUsage(key, { success: false, errorReason: 'IP address not whitelisted', ipAddress, userAgent, endpoint }); return { isValid: false, errorReason: 'IP address not whitelisted', apiKey }; } } // Update usage statistics await this.storage.updateApiKey(apiKey.id, { usageCount: apiKey.usageCount + 1, lastUsedAt: new Date() }); // Log successful usage await this.logUsage(key, { success: true, ipAddress, userAgent, endpoint }); const remainingUsage = apiKey.maxUsage ? apiKey.maxUsage - apiKey.usageCount - 1 : undefined; const expiresIn = apiKey.expiresAt ? Math.floor((apiKey.expiresAt.getTime() - Date.now()) / 1000) : undefined; return { isValid: true, apiKey: { ...apiKey, usageCount: apiKey.usageCount + 1, lastUsedAt: new Date() }, remainingUsage, expiresIn }; } /** * Get API key by ID */ async getApiKey(id: string): Promise<ApiKey | null> { return this.storage.getApiKey(id); } /** * List all API keys */ async listApiKeys(status?: ApiKeyStatus): Promise<ApiKey[]> { const allKeys = await this.storage.getAllApiKeys(); return status ? allKeys.filter(key => key.status === status) : allKeys; } /** * Update an API key */ async updateApiKey(id: string, input: UpdateApiKeyInput): Promise<ApiKey | null> { const validatedInput = UpdateApiKeySchema.parse(input); // Filter out undefined values for exactOptionalPropertyTypes const updateData: Partial<ApiKey> = {}; if (validatedInput.name !== undefined) updateData.name = validatedInput.name; if (validatedInput.description !== undefined) updateData.description = validatedInput.description; if (validatedInput.scopes !== undefined) updateData.scopes = validatedInput.scopes; if (validatedInput.status !== undefined) updateData.status = validatedInput.status; if (validatedInput.expiresAt !== undefined) updateData.expiresAt = validatedInput.expiresAt; if (validatedInput.maxUsage !== undefined) updateData.maxUsage = validatedInput.maxUsage; if (validatedInput.ipWhitelist !== undefined) updateData.ipWhitelist = validatedInput.ipWhitelist; if (validatedInput.metadata !== undefined) updateData.metadata = validatedInput.metadata; const updated = await this.storage.updateApiKey(id, updateData); return updated ? this.storage.getApiKey(id) : null; } /** * Revoke an API key */ async revokeApiKey(id: string): Promise<boolean> { return this.storage.updateApiKey(id, { status: ApiKeyStatus.REVOKED }); } /** * Delete an API key permanently */ async deleteApiKey(id: string): Promise<boolean> { return this.storage.deleteApiKey(id); } /** * Log API key usage */ private async logUsage(key: string, details: Omit<ApiKeyUsageLogInput, 'keyId'>): Promise<void> { try { // Find the key ID by hash const keyHash = CryptoUtils.hashApiKey(key); const apiKey = await this.storage.getApiKeyByHash(keyHash); if (apiKey) { const log: ApiKeyUsageLog = { id: CryptoUtils.generateId(), keyId: apiKey.id, timestamp: new Date(), success: details.success, ...(details.ipAddress && { ipAddress: details.ipAddress }), ...(details.userAgent && { userAgent: details.userAgent }), ...(details.endpoint && { endpoint: details.endpoint }), ...(details.errorReason && { errorReason: details.errorReason }), ...(details.responseTime && { responseTime: details.responseTime }) }; await this.storage.addUsageLog(log); } } catch (error) { console.error('Failed to log API key usage:', error); } } /** * Get usage logs for an API key */ async getUsageLogs(keyId: string, limit?: number): Promise<ApiKeyUsageLog[]> { return this.storage.getUsageLogs(keyId, limit); } /** * Rotate an API key */ async rotateApiKey(id: string): Promise<ApiKeyWithSecret | null> { const existingKey = await this.storage.getApiKey(id); if (!existingKey) { return null; } // Generate new key const newPlainKey = CryptoUtils.generateApiKey(); const newKeyHash = CryptoUtils.hashApiKey(newPlainKey); // Update the existing key const updateData: Partial<ApiKey> = { keyHash: newKeyHash, updatedAt: new Date(), usageCount: 0 // Reset usage count after rotation }; // Remove lastUsedAt by not including it const updated = await this.storage.updateApiKey(id, updateData); if (!updated) { return null; } const updatedKey = await this.storage.getApiKey(id); if (!updatedKey) { return null; } // Update rotation config if it exists const rotationConfig = await this.storage.getRotationConfig(id); if (rotationConfig) { const nextRotationAt = new Date(); nextRotationAt.setDate(nextRotationAt.getDate() + rotationConfig.rotationIntervalDays); await this.storage.updateRotationConfig(id, { lastRotatedAt: new Date(), nextRotationAt }); } return { ...updatedKey, key: newPlainKey }; } /** * Setup automatic rotation for an API key */ async setupRotation(keyId: string, config: ApiKeyRotationConfigInput): Promise<ApiKeyRotationConfig | null> { const validatedConfig = ApiKeyRotationConfigSchema.parse(config); // Verify the key exists const apiKey = await this.storage.getApiKey(keyId); if (!apiKey) { return null; } const nextRotationAt = new Date(); nextRotationAt.setDate(nextRotationAt.getDate() + validatedConfig.rotationIntervalDays); const rotationConfig: ApiKeyRotationConfig = { id: CryptoUtils.generateId(), keyId, rotationIntervalDays: validatedConfig.rotationIntervalDays, notifyDaysBefore: validatedConfig.notifyDaysBefore, autoRotate: validatedConfig.autoRotate, nextRotationAt, isActive: validatedConfig.isActive }; await this.storage.createRotationConfig(rotationConfig); return rotationConfig; } /** * Get rotation configuration for an API key */ async getRotationConfig(keyId: string): Promise<ApiKeyRotationConfig | null> { return this.storage.getRotationConfig(keyId); } /** * Process automatic key rotations */ async processAutomaticRotations(): Promise<string[]> { const keysNeedingRotation = await this.storage.getKeysNeedingRotation(); const rotatedKeys: string[] = []; for (const config of keysNeedingRotation) { if (config.autoRotate) { try { const rotated = await this.rotateApiKey(config.keyId); if (rotated) { rotatedKeys.push(config.keyId); console.log(`Automatically rotated API key: ${CryptoUtils.maskSensitiveData(rotated.name)}`); } } catch (error) { console.error(`Failed to auto-rotate key ${config.keyId}:`, error); } } } return rotatedKeys; } /** * Check if an API key has required scopes */ hasRequiredScopes(apiKey: ApiKey, requiredScopes: ApiKeyScope[]): boolean { // Admin scope grants all permissions if (apiKey.scopes.includes(ApiKeyScope.ADMIN)) { return true; } // Check if all required scopes are present return requiredScopes.every(scope => apiKey.scopes.includes(scope)); } /** * Get API key statistics */ async getApiKeyStats(): Promise<{ total: number; active: number; expired: number; revoked: number; totalUsage: number; }> { const allKeys = await this.storage.getAllApiKeys(); return { total: allKeys.length, active: allKeys.filter(k => k.status === ApiKeyStatus.ACTIVE).length, expired: allKeys.filter(k => k.status === ApiKeyStatus.EXPIRED).length, revoked: allKeys.filter(k => k.status === ApiKeyStatus.REVOKED).length, totalUsage: allKeys.reduce((sum, key) => sum + key.usageCount, 0) }; } } // Export a singleton instance export const apiKeyService = new ApiKeyService();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/learnwithcc/tally-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server