@monitoro/herd
Version:
Automate your browser, build AI web tools and MCP servers with Monitoro Herd
202 lines (201 loc) • 7.08 kB
JavaScript
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();
}
}