UNPKG

@monitoro/herd

Version:

Automate your browser, build AI web tools and MCP servers with Monitoro Herd

202 lines (201 loc) 7.08 kB
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto'; import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; // Default cache directory const DEFAULT_CACHE_DIR = path.join(os.homedir(), '.cache', 'herd', 'trails'); /** * Manages the encrypted cache for trail files * This is internal to the TrailEngine and not meant to be accessed directly */ export class CacheManager { /** * Creates a new CacheManager instance */ constructor(client, options = {}) { this.encryptionKey = null; this.keyExpiry = 0; this.client = client; this.cacheDirectory = options.cacheDirectory || DEFAULT_CACHE_DIR; } /** * Get path to cache directory */ getCacheDirectory() { return this.cacheDirectory; } /** * Store data in the encrypted cache with auto-prefixing */ async store(key, data) { // Get encryption key (refresh if needed) await this.ensureEncryptionKey(); // Create hash of the key for file naming const fileHash = this.hashString(key); // Ensure cache directory exists await this.ensureCacheDirectory(); // Path to store the cached data const filePath = path.join(this.cacheDirectory, fileHash); // Encrypt the data const encrypted = this.encrypt(data); // Write to disk await fs.writeFile(filePath, encrypted); return filePath; } /** * Retrieve data from the encrypted cache */ async retrieve(key) { try { // Get encryption key (refresh if needed) await this.ensureEncryptionKey(); // Create hash of the key for file lookup const fileHash = this.hashString(key); // Path where the cached data should be const filePath = path.join(this.cacheDirectory, fileHash); // Check if file exists try { await fs.access(filePath); } catch (error) { return null; // File not found } // Read encrypted data const encrypted = await fs.readFile(filePath); // Decrypt and return try { return this.decrypt(encrypted); } catch (error) { // If decryption fails (likely due to key rotation), delete the file await fs.unlink(filePath).catch(() => { }); return null; } } catch (error) { return null; } } /** * Check if a key exists in the cache */ async exists(key) { try { const fileHash = this.hashString(key); const filePath = path.join(this.cacheDirectory, fileHash); await fs.access(filePath); return true; } catch (error) { return false; } } /** * Clear the entire cache */ async clear() { try { const files = await fs.readdir(this.cacheDirectory); await Promise.all(files.map(file => fs.unlink(path.join(this.cacheDirectory, file)).catch(() => { }))); } catch (error) { // Ignore errors (e.g., directory doesn't exist) } } /** * Ensure the cache directory exists */ async ensureCacheDirectory() { await fs.mkdir(this.cacheDirectory, { recursive: true }); } /** * Create a hash of a string for filename */ hashString(str) { return createHash('sha256').update(str).digest('hex'); } /** * Encrypt data using the current encryption key */ encrypt(data) { if (!this.encryptionKey) { throw new Error('Encryption key not available'); } const iv = randomBytes(16); const cipher = createCipheriv('aes-256-cbc', this.encryptionKey, iv); const content = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8'); const encrypted = Buffer.concat([ cipher.update(content), cipher.final() ]); // Prepend the IV to the encrypted data (we'll extract it during decryption) return Buffer.concat([iv, encrypted]); } /** * Decrypt data using the current encryption key */ decrypt(data) { if (!this.encryptionKey) { throw new Error('Encryption key not available'); } // Extract the IV from the first 16 bytes const iv = data.subarray(0, 16); const encryptedData = data.subarray(16); const decipher = createDecipheriv('aes-256-cbc', this.encryptionKey, iv); const decrypted = Buffer.concat([ decipher.update(encryptedData), decipher.final() ]); return decrypted.toString('utf8'); } /** * Ensure we have a valid encryption key */ async ensureEncryptionKey() { const currentTime = Date.now(); // If key is valid, return if (this.encryptionKey && currentTime < this.keyExpiry) { return; } // Key needs refresh - fetch from API try { // Use the HerdClient's getCacheKey method const key = await this.client.getCacheKey(); // Store key and set expiry to next day at midnight this.encryptionKey = Buffer.from(key, 'hex'); const now = new Date(); const nextDay = new Date(now); nextDay.setDate(now.getDate() + 1); nextDay.setHours(0, 0, 0, 0); this.keyExpiry = nextDay.getTime(); } catch (error) { // If we can't get the key, use a fallback strategy: // create a machine-specific key based on hostname, user and the current day const fallbackKey = this.createFallbackKey(); this.encryptionKey = fallbackKey; // Set expiry to next day at midnight const now = new Date(); const nextDay = new Date(now); nextDay.setDate(now.getDate() + 1); nextDay.setHours(0, 0, 0, 0); this.keyExpiry = nextDay.getTime(); } } /** * Create a fallback key based on local machine info * This is used only if we can't get a key from the server */ createFallbackKey() { // Use hostname, username, and current day to create a predictable but time-limited key const hostname = os.hostname(); const username = os.userInfo().username; // Get the current day (changes at midnight) const now = new Date(); const day = Math.floor(now.getTime() / (24 * 60 * 60 * 1000)); // Create a unique string from all parts const keySource = `${hostname}-${username}-${day}-${process.env.TRAILS_CACHE_SALT || 'herd-default-salt'}`; // Hash the string to get a 32-byte key (suitable for AES-256) return createHash('sha256').update(keySource).digest(); } }