UNPKG

fortify2-js

Version:

MOST POWERFUL JavaScript Security Library! Military-grade cryptography + 19 enhanced object methods + quantum-resistant algorithms + perfect TypeScript support. More powerful than Lodash with built-in security.

1,228 lines (1,224 loc) 44 kB
'use strict'; var events = require('events'); var Redis = require('ioredis'); var useCache = require('../../../components/cache/useCache.js'); var EncryptionService = require('../encryption/EncryptionService.js'); var crypto = require('../../../core/crypto.js'); var Logger = require('../server/utils/Logger.js'); /** * FortifyJS Secure Cache Adapter * Ultra-fast hybrid cache system combining security cache with Redis clustering * * Features: * - Memory-first hybrid architecture for maximum speed * - Redis Cluster support with automatic failover * - Connection pooling and health monitoring * - Advanced tagging and invalidation * - Real-time performance metrics * - Military-grade security from FortifyJS security cache */ /** * UF secure cache adapter */ class SecureCacheAdapter extends events.EventEmitter { constructor(config = {}) { super(); this.connectionPool = new Map(); this.metadata = new Map(); this.logger = Logger.initializeLogger(); this.config = { strategy: "hybrid", memory: { maxSize: 100, // 100MB maxEntries: 10000, ttl: 10 * 60 * 1000, // 10 minutes ...config.memory, }, redis: { host: "localhost", port: 6379, pool: { min: 2, max: 10, acquireTimeoutMillis: 30000, }, ...config.redis, }, performance: { batchSize: 100, compressionThreshold: 1024, hotDataThreshold: 10, prefetchEnabled: true, ...config.performance, }, security: { encryption: true, keyRotation: true, accessMonitoring: true, ...config.security, }, monitoring: { enabled: true, metricsInterval: 60000, // 1 minute alertThresholds: { memoryUsage: 90, hitRate: 80, errorRate: 5, }, ...config.monitoring, }, ...config, }; this.initializeStats(); this.initializeMasterKey(); this.initializeMemoryCache(); } /** * Initialize statistics */ initializeStats() { this.stats = { memory: { hits: 0, misses: 0, evictions: 0, totalSize: 0, entryCount: 0, hitRate: 0, totalAccesses: 0, size: 0, capacity: this.config.memory?.maxEntries || 10000, memoryUsage: { used: 0, limit: (this.config.memory?.maxSize || 100) * 1024 * 1024, percentage: 0, }, }, redis: this.config.strategy === "redis" || this.config.strategy === "hybrid" ? { connected: false, commandsProcessed: 0, operations: 0, memoryUsage: { used: 0, peak: 0, percentage: 0, }, keyspaceHits: 0, keyspaceMisses: 0, hits: 0, misses: 0, hitRate: 0, connectedClients: 0, connections: 0, keys: 0, uptime: 0, lastUpdate: 0, } : undefined, performance: { totalOperations: 0, averageResponseTime: 0, hotDataHitRate: 0, compressionRatio: 0, networkLatency: 0, }, security: { encryptedEntries: 0, keyRotations: 0, suspiciousAccess: 0, securityEvents: 0, }, }; } /** * Initialize master encryption key for consistent encryption */ initializeMasterKey() { // Generate a consistent master key for all cache operations this.masterEncryptionKey = crypto.FortifyJS.generateSecureToken({ length: 32, entropy: "high", }); } /** * Initialize memory cache with security features */ initializeMemoryCache() { this.memoryCache = new useCache.SecureInMemoryCache(); // Listen to security events this.memoryCache.on("key_rotation", (event) => { this.stats.security.keyRotations++; this.emit("security_event", { type: "key_rotation", ...event }); }); this.memoryCache.on("suspicious_access", (event) => { this.stats.security.suspiciousAccess++; this.emit("security_event", { type: "suspicious_access", ...event, }); }); this.memoryCache.on("memory_pressure", (event) => { this.emit("performance_alert", { type: "memory_pressure", ...event, }); }); } /** * Connect to cache backends */ async connect() { try { // Memory cache is always ready // console.log(" Secure memory cache initialized"); // Initialize Redis if needed if (this.config.strategy === "redis" || this.config.strategy === "hybrid") { await this.initializeRedis(); } // Start monitoring if (this.config.monitoring?.enabled) { this.startMonitoring(); } this.emit("connected"); } catch (error) { this.emit("error", error); throw new Error(`Cache connection failed: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Initialize Redis with clustering and failover support */ async initializeRedis() { const redisConfig = this.config.redis; try { if (redisConfig.cluster?.enabled && redisConfig.cluster.nodes) { // Redis Cluster mode this.redisClient = new Redis.Cluster(redisConfig.cluster.nodes, { redisOptions: { password: redisConfig.password, db: redisConfig.db || 0, lazyConnect: true, }, ...redisConfig.cluster.options, }); this.logger.startup("server", " Redis Cluster initialized"); } else if (redisConfig.sentinel?.enabled) { // Redis Sentinel mode this.redisClient = new Redis({ sentinels: redisConfig.sentinel.sentinels, name: redisConfig.sentinel.name || "mymaster", password: redisConfig.password, db: redisConfig.db || 0, lazyConnect: true, }); this.logger.info("server", " Redis Sentinel initialized"); } else { // Single Redis instance this.redisClient = new Redis({ host: redisConfig.host, port: redisConfig.port, password: redisConfig.password, db: redisConfig.db || 0, lazyConnect: true, connectTimeout: 5000, // 5 second timeout commandTimeout: 5000, // 5 second command timeout retryDelayOnFailover: 100, // This property exists in ioredis maxRetriesPerRequest: 2, }); // Use type assertion to bypass strict typing this.logger.info("server", " Redis single instance initialized"); } // Setup Redis event handlers this.setupRedisEventHandlers(); // Connect to Redis with timeout await Promise.race([ this.redisClient.connect(), new Promise((_, reject) => setTimeout(() => reject(new Error("Redis connection timeout")), 10000)), ]); } catch (error) { console.error(" Redis initialization failed:", error); throw error; } } /** * Setup Redis event handlers for monitoring and failover */ setupRedisEventHandlers() { if (!this.redisClient) return; this.redisClient.on("connect", () => { this.emit("redis_connected"); }); this.redisClient.on("ready", () => { this.logger.info("server", "Connected to Redis"); this.emit("redis_ready"); }); this.redisClient.on("error", (error) => { console.error(" Redis error:", error); this.emit("redis_error", error); }); this.redisClient.on("close", () => { console.warn(" Redis connection closed"); this.emit("redis_disconnected"); }); this.redisClient.on("reconnecting", () => { this.logger.warn("server", " Redis reconnecting..."); this.emit("redis_reconnecting"); }); // Cluster-specific events if (this.redisClient instanceof Redis.Cluster) { this.redisClient.on("node error", (error, node) => { console.error(` Redis cluster node error (${node.options.host}:${node.options.port}):`, error); this.emit("cluster_node_error", { error, node }); }); this.redisClient.on("+node", (node) => { this.logger.info("server", ` Redis cluster node added: ${node.options.host}:${node.options.port}`); this.emit("cluster_node_added", node); }); this.redisClient.on("-node", (node) => { this.logger.warn("server", ` Redis cluster node removed: ${node.options.host}:${node.options.port}`); this.emit("cluster_node_removed", node); }); } } /** * Start monitoring and health checks */ startMonitoring() { // Health monitoring this.healthMonitor = setInterval(async () => { await this.performHealthCheck(); }, 30000); // Every 30 seconds // Metrics collection this.metricsCollector = setInterval(() => { this.collectMetrics(); }, this.config.monitoring?.metricsInterval || 60000); } /** * Perform health check on all cache backends */ async performHealthCheck() { try { // Check memory cache const memoryStats = this.memoryCache.getStats; // Check Redis if available if (this.redisClient) { const redisInfo = await this.redisClient.ping(); if (redisInfo !== "PONG") { this.emit("health_check_failed", { backend: "redis", reason: "ping_failed", }); } } // Check alert thresholds const thresholds = this.config.monitoring?.alertThresholds; if (thresholds) { if (memoryStats.memoryUsage.percentage > (thresholds.memoryUsage || 90)) { this.emit("performance_alert", { type: "high_memory_usage", value: memoryStats.memoryUsage.percentage, threshold: thresholds.memoryUsage, }); } if (memoryStats.hitRate < (thresholds.hitRate || 80) / 100) { this.emit("performance_alert", { type: "low_hit_rate", value: memoryStats.hitRate * 100, threshold: thresholds.hitRate, }); } } } catch (error) { this.emit("health_check_failed", { error }); } } /** * Collect performance metrics */ collectMetrics() { try { // Update memory stats this.stats.memory = this.memoryCache.getStats; // Calculate performance metrics this.updatePerformanceMetrics(); // Emit metrics event this.emit("metrics_collected", this.stats); } catch (error) { console.error("Metrics collection failed:", error); } } /** * Update performance metrics */ updatePerformanceMetrics() { // Calculate hot data hit rate let hotDataHits = 0; let totalHotAccess = 0; for (const [, meta] of this.metadata.entries()) { if (meta.isHot) { totalHotAccess += meta.accessCount; if (meta.location === "memory") { hotDataHits += meta.accessCount; } } } this.stats.performance.hotDataHitRate = totalHotAccess > 0 ? hotDataHits / totalHotAccess : 0; // Update security stats this.stats.security.encryptedEntries = this.metadata.size; } /** * Generate cache key with namespace and security */ generateKey(key) { // Validate key if (!key || typeof key !== "string") { throw new Error("Cache key must be a non-empty string"); } if (key.length > 512) { throw new Error("Cache key too long (max 512 characters)"); } // Create deterministic hash of the key using crypto const crypto = require("crypto"); const hashedKey = crypto.createHash("sha256").update(key).digest("hex"); return `fortify:v2:${hashedKey.substring(0, 16)}:${key}`; } /** * Determine if data should be considered "hot" (frequently accessed) */ isHotData(key) { const meta = this.metadata.get(key); if (!meta) return false; const threshold = this.config.performance?.hotDataThreshold || 10; const timeWindow = 60 * 60 * 1000; // 1 hour const now = Date.now(); return (meta.accessCount >= threshold && now - meta.lastAccessed < timeWindow); } /** * Update access metadata for performance optimization */ updateAccessMetadata(key, size = 0) { const now = Date.now(); const meta = this.metadata.get(key) || { accessCount: 0, lastAccessed: now, size, isHot: false, location: "memory", tags: [], }; meta.accessCount++; meta.lastAccessed = now; meta.isHot = this.isHotData(key); this.metadata.set(key, meta); this.stats.performance.totalOperations++; } // ======================================== // SERIALIZATION AND DATA HANDLING // ======================================== /** * Convert any data to CachedData format for SecurityCache compatibility */ toCachedData(value) { if (typeof value === "object" && value !== null && "data" in value) { // Already in CachedData format return value; } // Wrap raw data in CachedData format return { data: value, metadata: { timestamp: Date.now(), type: typeof value, size: JSON.stringify(value).length, }, }; } /** * Extract raw data from CachedData format */ fromCachedData(cachedData) { if (!cachedData) return null; // Return the actual data, not the wrapper return cachedData.data; } /** * Serialize data for Redis storage with proper encryption */ async serializeForRedis(value) { try { // First convert to JSON let serialized = JSON.stringify(value); // Apply encryption if enabled if (this.config.security?.encryption) { serialized = await EncryptionService.EncryptionService.encrypt(value, this.masterEncryptionKey, { algorithm: "aes-256-gcm", quantumSafe: false, }); } return serialized; } catch (error) { throw new Error(`Serialization failed: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Deserialize data from Redis storage with proper decryption */ async deserializeFromRedis(serialized) { try { // Apply decryption if enabled if (this.config.security?.encryption) { return await EncryptionService.EncryptionService.decrypt(serialized, this.masterEncryptionKey); } // Parse JSON if no encryption return JSON.parse(serialized); } catch (error) { throw new Error(`Deserialization failed: ${error instanceof Error ? error.message : "Unknown error"}`); } } // ======================================== // CORE CACHE OPERATIONS // ======================================== /** * Get value from cache with ultra-fast hybrid strategy * * @param key - The cache key to retrieve * @returns Promise resolving to the cached value with proper typing, or null if not found * * @example * ```typescript * interface User { id: number; name: string; } * const user = await cache.get<User>("user:123"); * if (user) { * console.log(user.name); // TypeScript knows this is a string * } * ``` */ async get(key) { const startTime = Date.now(); try { const cacheKey = this.generateKey(key); // Strategy 1: Try memory cache first (fastest) if (this.config.strategy === "memory" || this.config.strategy === "hybrid") { const memoryResult = await this.memoryCache.get(cacheKey); if (memoryResult !== null) { this.updateAccessMetadata(cacheKey); this.recordResponseTime(Date.now() - startTime); // Extract raw data from CachedData format return this.fromCachedData(memoryResult); } } // Strategy 2: Try Redis if memory miss if ((this.config.strategy === "redis" || this.config.strategy === "hybrid") && this.redisClient) { const redisResult = await this.getFromRedis(cacheKey); if (redisResult !== null) { // For hybrid strategy, promote hot data to memory if (this.config.strategy === "hybrid" && this.isHotData(cacheKey)) { // Convert to CachedData format for memory cache const cachedData = this.toCachedData(redisResult); await this.memoryCache.set(cacheKey, cachedData, { ttl: this.config.memory?.ttl, }); } this.updateAccessMetadata(cacheKey); this.recordResponseTime(Date.now() - startTime); return redisResult; } } // Cache miss this.recordResponseTime(Date.now() - startTime); return null; } catch (error) { this.emit("cache_error", { operation: "get", key, error }); this.recordResponseTime(Date.now() - startTime); return null; } } /** * Set value in cache with intelligent placement * * @param key - The cache key to store the value under * @param value - The value to cache with proper typing * @param options - Optional caching options * @param options.ttl - Time to live in milliseconds * @param options.tags - Array of tags for bulk invalidation * @returns Promise resolving to true if successful, false otherwise * * @example * ```typescript * interface User { id: number; name: string; email: string; } * * const user: User = { id: 123, name: "John", email: "john@example.com" }; * const success = await cache.set<User>("user:123", user, { * ttl: 3600000, // 1 hour * tags: ["users", "active"] * }); * ``` */ async set(key, value, options = {}) { const startTime = Date.now(); try { const cacheKey = this.generateKey(key); const ttl = options.ttl || this.config.memory?.ttl || 600000; // 10 minutes default // Determine storage strategy const shouldStoreInMemory = this.config.strategy === "memory" || this.config.strategy === "hybrid"; const shouldStoreInRedis = this.config.strategy === "redis" || this.config.strategy === "hybrid"; // Store in memory cache if (shouldStoreInMemory) { // Convert to CachedData format for memory cache const cachedData = this.toCachedData(value); await this.memoryCache.set(cacheKey, cachedData, { ttl }); } // Store in Redis if (shouldStoreInRedis && this.redisClient) { await this.setInRedis(cacheKey, value, ttl, options.tags); } // Update metadata this.updateAccessMetadata(cacheKey, JSON.stringify(value).length); if (options.tags) { const meta = this.metadata.get(cacheKey); if (meta) { meta.tags = options.tags; this.metadata.set(cacheKey, meta); } } this.recordResponseTime(Date.now() - startTime); return true; } catch (error) { this.emit("cache_error", { operation: "set", key, error }); this.recordResponseTime(Date.now() - startTime); return false; } } /** * Delete value from cache */ async delete(key) { const startTime = Date.now(); try { const cacheKey = this.generateKey(key); let deleted = false; // Delete from memory cache if (this.config.strategy === "memory" || this.config.strategy === "hybrid") { deleted = this.memoryCache.delete(cacheKey) || deleted; } // Delete from Redis if ((this.config.strategy === "redis" || this.config.strategy === "hybrid") && this.redisClient) { const redisDeleted = await this.redisClient.del(cacheKey); deleted = redisDeleted > 0 || deleted; } // Clean up metadata this.metadata.delete(cacheKey); this.recordResponseTime(Date.now() - startTime); return deleted; } catch (error) { this.emit("cache_error", { operation: "delete", key, error }); this.recordResponseTime(Date.now() - startTime); return false; } } /** * Check if key exists in cache */ async exists(key) { try { const cacheKey = this.generateKey(key); // Check memory cache first if (this.config.strategy === "memory" || this.config.strategy === "hybrid") { if (this.memoryCache.has(cacheKey)) { return true; } } // Check Redis if ((this.config.strategy === "redis" || this.config.strategy === "hybrid") && this.redisClient) { const exists = await this.redisClient.exists(cacheKey); return exists > 0; } return false; } catch (error) { this.emit("cache_error", { operation: "exists", key, error }); return false; } } /** * Clear all cache entries */ async clear() { try { // Clear memory cache if (this.config.strategy === "memory" || this.config.strategy === "hybrid") { this.memoryCache.clear(); } // Clear Redis if ((this.config.strategy === "redis" || this.config.strategy === "hybrid") && this.redisClient) { await this.redisClient.flushdb(); } // Clear metadata this.metadata.clear(); this.emit("cache_cleared"); } catch (error) { this.emit("cache_error", { operation: "clear", error }); throw error; } } // ======================================== // REDIS HELPER METHODS // ======================================== /** * Get value from Redis with encryption support */ async getFromRedis(key) { if (!this.redisClient) return null; try { const serialized = await this.redisClient.get(key); if (!serialized) return null; // Use consistent deserialization return await this.deserializeFromRedis(serialized); } catch (error) { console.error("Redis get error:", error); return null; } } /** * Set value in Redis with encryption and TTL support */ async setInRedis(key, value, ttl, tags) { if (!this.redisClient) return; try { // Use consistent serialization const serialized = await this.serializeForRedis(value); // Set with TTL if (ttl > 0) { await this.redisClient.setex(key, Math.floor(ttl / 1000), serialized); } else { await this.redisClient.set(key, serialized); } // Handle tags for cache invalidation if (tags && tags.length > 0) { await this.setTags(key, tags); } } catch (error) { console.error("Redis set error:", error); throw error; } } /** * Set tags for cache invalidation */ async setTags(key, tags) { if (!this.redisClient) return; try { const pipeline = this.redisClient.pipeline(); for (const tag of tags) { const tagKey = `tag:${tag}`; pipeline.sadd(tagKey, key); pipeline.expire(tagKey, 86400); // 24 hours } await pipeline.exec(); } catch (error) { console.error("Redis tag set error:", error); } } /** * Record response time for performance monitoring */ recordResponseTime(responseTime) { // Update running average const currentAvg = this.stats.performance.averageResponseTime; const totalOps = this.stats.performance.totalOperations; this.stats.performance.averageResponseTime = (currentAvg * (totalOps - 1) + responseTime) / totalOps; } // ======================================== // ADVANCED CACHE OPERATIONS // ======================================== /** * Invalidate cache entries by tags */ async invalidateByTags(tags) { if (!this.redisClient) return 0; try { let invalidatedCount = 0; for (const tag of tags) { const tagKey = `tag:${tag}`; const keys = await this.redisClient.smembers(tagKey); if (keys.length > 0) { // Delete all keys with this tag await this.redisClient.del(...keys); // Remove from memory cache too for (const key of keys) { this.memoryCache.delete(key); this.metadata.delete(key); } invalidatedCount += keys.length; } // Clean up the tag set await this.redisClient.del(tagKey); } this.emit("cache_invalidated", { tags, count: invalidatedCount }); return invalidatedCount; } catch (error) { this.emit("cache_error", { operation: "invalidateByTags", tags, error, }); return 0; } } /** * Get multiple values at once (batch operation) * * @param keys - Array of cache keys to retrieve * @returns Promise resolving to an object with key-value pairs (missing keys are omitted) * * @example * ```typescript * interface User { id: number; name: string; } * * const users = await cache.mget<User>(["user:1", "user:2", "user:3"]); * // users is Record<string, User> * * for (const [key, user] of Object.entries(users)) { * console.log(`${key}: ${user.name}`); // TypeScript knows user.name is string * } * ``` */ async mget(keys) { const results = {}; try { // Use Promise.all for parallel execution const promises = keys.map(async (key) => { const value = await this.get(key); return { key, value }; }); const resolved = await Promise.all(promises); for (const { key, value } of resolved) { if (value !== null) { results[key] = value; } } return results; } catch (error) { this.emit("cache_error", { operation: "mget", keys, error }); return {}; } } /** * Set multiple values at once (batch operation) * * @param entries - Object with key-value pairs or array of [key, value] tuples * @param options - Optional caching options applied to all entries * @param options.ttl - Time to live in milliseconds for all entries * @param options.tags - Array of tags applied to all entries * @returns Promise resolving to true if all operations successful, false otherwise * * @example * ```typescript * interface User { id: number; name: string; } * * // Using object notation * const success1 = await cache.mset<User>({ * "user:1": { id: 1, name: "Alice" }, * "user:2": { id: 2, name: "Bob" } * }, { ttl: 3600000, tags: ["users"] }); * * // Using array notation * const success2 = await cache.mset<User>([ * ["user:3", { id: 3, name: "Charlie" }], * ["user:4", { id: 4, name: "Diana" }] * ], { ttl: 3600000 }); * ``` */ async mset(entries, options = {}) { try { // Convert array format to object format if needed const entriesObj = Array.isArray(entries) ? Object.fromEntries(entries) : entries; // Use Promise.all for parallel execution const promises = Object.entries(entriesObj).map(([key, value]) => this.set(key, value, options)); const results = await Promise.all(promises); return results.every((result) => result === true); } catch (error) { this.emit("cache_error", { operation: "mset", entries: Object.keys(entries), error, }); return false; } } // ======================================== // TYPE-SAFE ALIAS METHODS // ======================================== /** * Read value from cache (alias for get method) * * @param key - The cache key to retrieve * @returns Promise resolving to the cached value with proper typing, or null if not found * * @example * ```typescript * interface User { id: number; name: string; } * const user = await cache.read<User>("user:123"); * if (user) { * console.log(user.name); // TypeScript knows this is a string * } * ``` */ async read(key) { return this.get(key); } /** * Write value to cache (alias for set method) * * @param key - The cache key to store the value under * @param value - The value to cache with proper typing * @param options - Optional caching options * @param options.ttl - Time to live in milliseconds * @param options.tags - Array of tags for bulk invalidation * @returns Promise resolving to true if successful, false otherwise * * @example * ```typescript * interface User { id: number; name: string; email: string; } * * const user: User = { id: 123, name: "John", email: "john@example.com" }; * const success = await cache.write<User>("user:123", user, { * ttl: 3600000, // 1 hour * tags: ["users", "active"] * }); * ``` */ async write(key, value, options = {}) { return this.set(key, value, options); } // ======================================== // ENHANCED CACHE METHODS // ======================================== /** * Get TTL for a specific key */ async getTTL(key) { const cacheKey = this.generateKey(key); try { // Check memory cache first by checking if key exists if (this.config.strategy === "memory" || this.config.strategy === "hybrid") { if (this.memoryCache.has(cacheKey)) { // For memory cache, we can't get exact TTL, so return a default return 300000; // 5 minutes default } } // Check Redis cache if (this.redisClient && (this.config.strategy === "redis" || this.config.strategy === "hybrid")) { const redisTTL = await this.redisClient.ttl(cacheKey); return redisTTL > 0 ? redisTTL * 1000 : -1; // Convert to milliseconds } return -1; // Key doesn't exist or no TTL } catch (error) { console.error("Get TTL error:", error); return -1; } } /** * Set expiration time for a key */ async expire(key, ttl) { const cacheKey = this.generateKey(key); try { let success = false; // Update memory cache TTL if (this.config.strategy === "memory" || this.config.strategy === "hybrid") { const value = await this.memoryCache.get(cacheKey); if (value !== null) { await this.memoryCache.set(cacheKey, value, { ttl }); success = true; } } // Update Redis cache TTL if (this.redisClient && (this.config.strategy === "redis" || this.config.strategy === "hybrid")) { const result = await this.redisClient.expire(cacheKey, Math.floor(ttl / 1000)); success = success || result === 1; } return success; } catch (error) { console.error("Expire error:", error); return false; } } /** * Get all keys matching a pattern */ async keys(pattern) { try { const allKeys = new Set(); // Get keys from memory cache using metadata if (this.config.strategy === "memory" || this.config.strategy === "hybrid") { // Use metadata to get all keys for (const [cacheKey] of this.metadata.entries()) { const originalKey = this.extractOriginalKey(cacheKey); if (originalKey) { allKeys.add(originalKey); } } } // Get keys from Redis cache if (this.redisClient && (this.config.strategy === "redis" || this.config.strategy === "hybrid")) { const redisPattern = pattern ? `fortify:v2:*:${pattern}` : "fortify:v2:*"; const redisKeys = await this.redisClient.keys(redisPattern); redisKeys.forEach((key) => { const originalKey = this.extractOriginalKey(key); if (originalKey) { allKeys.add(originalKey); } }); } const keysArray = Array.from(allKeys); // Apply pattern filtering if specified if (pattern && !pattern.includes("*")) { return keysArray.filter((key) => key.includes(pattern)); } return keysArray; } catch (error) { console.error("Keys error:", error); return []; } } /** * Extract original key from cache key */ extractOriginalKey(cacheKey) { // Format: fortify:v2:hash:originalKey const parts = cacheKey.split(":"); if (parts.length >= 4 && parts[0] === "fortify" && parts[1] === "v2") { return parts.slice(3).join(":"); } return null; } // ======================================== // MONITORING AND STATISTICS // ======================================== /** * Get comprehensive cache statistics */ async getStats() { // Update Redis stats if available if (this.redisClient && this.stats.redis) { await this.updateRedisStats(); } return { ...this.stats }; } /** * Update Redis statistics using Redis INFO command */ async updateRedisStats() { if (!this.redisClient || !this.stats.redis) return; try { // Get Redis INFO command output const info = await this.redisClient.info(); const infoLines = info.split("\r\n"); const infoData = {}; // Parse INFO command output for (const line of infoLines) { if (line.includes(":")) { const [key, value] = line.split(":"); infoData[key] = value; } } // Update connection status this.stats.redis.connected = this.redisClient.status === "ready"; // Update memory usage if (infoData.used_memory) { this.stats.redis.memoryUsage = { used: parseInt(infoData.used_memory), peak: parseInt(infoData.used_memory_peak || "0"), percentage: this.calculateRedisMemoryPercentage(infoData), }; } // Update performance metrics if (infoData.total_commands_processed) { this.stats.redis.operations = parseInt(infoData.total_commands_processed); } if (infoData.keyspace_hits && infoData.keyspace_misses) { const hits = parseInt(infoData.keyspace_hits); const misses = parseInt(infoData.keyspace_misses); const total = hits + misses; this.stats.redis.hitRate = total > 0 ? hits / total : 0; this.stats.redis.hits = hits; this.stats.redis.misses = misses; } // Update connection info if (infoData.connected_clients) { this.stats.redis.connections = parseInt(infoData.connected_clients); } // Update key count if (infoData.db0) { const dbInfo = infoData.db0.match(/keys=(\d+)/); if (dbInfo) { this.stats.redis.keys = parseInt(dbInfo[1]); } } // Update uptime if (infoData.uptime_in_seconds) { this.stats.redis.uptime = parseInt(infoData.uptime_in_seconds) * 1000; // Convert to ms } // Update last update timestamp this.stats.redis.lastUpdate = Date.now(); } catch (error) { console.error("Failed to update Redis stats:", error); // Mark as disconnected if we can't get stats this.stats.redis.connected = false; } } /** * Calculate Redis memory usage percentage */ calculateRedisMemoryPercentage(infoData) { const usedMemory = parseInt(infoData.used_memory || "0"); const maxMemory = parseInt(infoData.maxmemory || "0"); if (maxMemory === 0) { // If no max memory is set, calculate based on system memory const totalSystemMemory = parseInt(infoData.total_system_memory || "0"); if (totalSystemMemory > 0) { return (usedMemory / totalSystemMemory) * 100; } return 0; } return (usedMemory / maxMemory) * 100; } /** * Get cache health status */ getHealth() { const memoryUsage = this.stats.memory.memoryUsage.percentage; const hitRate = this.stats.memory.hitRate * 100; const redisConnected = this.redisClient?.status === "ready"; let status = "healthy"; const issues = []; if (memoryUsage > 90) { status = "unhealthy"; issues.push("High memory usage"); } else if (memoryUsage > 75) { status = "degraded"; issues.push("Elevated memory usage"); } if (hitRate < 50) { status = "unhealthy"; issues.push("Low hit rate"); } else if (hitRate < 80) { status = "degraded"; issues.push("Suboptimal hit rate"); } if (this.config.strategy !== "memory" && !redisConnected) { status = "unhealthy"; issues.push("Redis disconnected"); } return { status, details: { memoryUsage, hitRate, redisConnected, issues, uptime: Date.now() - this.stats.memory.startTime || 0, }, }; } /** * Disconnect from all cache backends */ async disconnect() { try { // Stop monitoring if (this.healthMonitor) { clearInterval(this.healthMonitor); } if (this.metricsCollector) { clearInterval(this.metricsCollector); } // Disconnect Redis if (this.redisClient) { await this.redisClient.quit(); } // Clear connection pool for (const [, connection] of this.connectionPool) { await connection.quit(); } this.connectionPool.clear(); this.emit("disconnected"); } catch (error) { this.emit("error", error); throw error; } } } exports.SecureCacheAdapter = SecureCacheAdapter; //# sourceMappingURL=SecureCacheAdapter.js.map