UNPKG

claude-computer-use-mcp

Version:

MCP server providing browser automation capabilities to Claude Code

210 lines 8.85 kB
import { createCipheriv, createDecipheriv, randomBytes, scrypt } from 'crypto'; import { promisify } from 'util'; import * as fs from 'fs/promises'; import * as path from 'path'; // Properly typed promisified scrypt function const scryptAsync = promisify(scrypt); export class CookieManager { storageDir; encryptionKey; algorithm = 'aes-256-gcm'; // Performance optimization: cache derived keys keyCache = new Map(); constructor(storageDir = './.cookie-storage', masterKey) { this.storageDir = storageDir; this.encryptionKey = masterKey || process.env.COOKIE_ENCRYPTION_KEY || ''; if (!this.encryptionKey) { throw new Error('Cookie encryption key is required for security. ' + 'Provide a master key in constructor or set COOKIE_ENCRYPTION_KEY environment variable.'); } if (this.encryptionKey.length < 32) { throw new Error('Cookie encryption key must be at least 32 characters long for security.'); } } async initialize() { try { await fs.mkdir(this.storageDir, { recursive: true }); // Create a secure permissions mask for the storage directory await fs.chmod(this.storageDir, 0o700); } catch (error) { throw new Error(`Failed to initialize cookie storage: ${error}`); } } async getDerivedKey(salt) { const cacheKey = salt.toString('hex'); // Check cache first for performance if (this.keyCache.has(cacheKey)) { return this.keyCache.get(cacheKey); } // Derive key if not cached const key = await scryptAsync(this.encryptionKey, salt, 32); // Cache for future use (with size limit to prevent memory bloat) if (this.keyCache.size < 100) { this.keyCache.set(cacheKey, key); } return key; } async encrypt(text) { try { const salt = randomBytes(32); const key = await this.getDerivedKey(salt); const iv = randomBytes(16); const cipher = createCipheriv(this.algorithm, key, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); return { iv: iv.toString('hex'), salt: salt.toString('hex'), encrypted: encrypted + ':' + authTag.toString('hex') }; } catch (error) { console.error('Encryption failed:', error); throw new Error(`Failed to encrypt cookie value: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async decrypt(data) { try { const salt = Buffer.from(data.salt, 'hex'); const key = await this.getDerivedKey(salt); const iv = Buffer.from(data.iv, 'hex'); const [encrypted, authTag] = data.encrypted.split(':'); const decipher = createDecipheriv(this.algorithm, key, iv); decipher.setAuthTag(Buffer.from(authTag, 'hex')); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } catch (error) { console.error('Decryption failed:', error); throw new Error(`Failed to decrypt cookie value: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async saveCookies(sessionId, cookies) { try { const processedCookies = []; for (const cookie of cookies) { const storedCookie = { ...cookie }; // Encrypt sensitive cookie values (encryption key is guaranteed to exist) if (cookie.value) { const encrypted = await this.encrypt(cookie.value); storedCookie.encryptedValue = JSON.stringify(encrypted); storedCookie.value = ''; // Clear plaintext value } processedCookies.push(storedCookie); } // Group cookies by domain const cookiesByDomain = new Map(); for (const cookie of processedCookies) { const domain = cookie.domain.startsWith('.') ? cookie.domain.substring(1) : cookie.domain; if (!cookiesByDomain.has(domain)) { cookiesByDomain.set(domain, { domain, cookies: [], lastUpdated: new Date().toISOString() }); } cookiesByDomain.get(domain).cookies.push(cookie); } // Save to files const sessionDir = path.join(this.storageDir, sessionId); await fs.mkdir(sessionDir, { recursive: true }); for (const [domain, store] of cookiesByDomain) { const filename = `${domain.replace(/[^a-zA-Z0-9.-]/g, '_')}.json`; const filepath = path.join(sessionDir, filename); await fs.writeFile(filepath, JSON.stringify(store, null, 2), 'utf8'); await fs.chmod(filepath, 0o600); // Secure file permissions } } catch (error) { throw new Error(`Failed to save cookies: ${error}`); } } async loadCookies(sessionId, domain) { try { const sessionDir = path.join(this.storageDir, sessionId); const cookies = []; try { await fs.access(sessionDir); } catch { return cookies; // No stored cookies for this session } const files = await fs.readdir(sessionDir); for (const file of files) { if (!file.endsWith('.json')) continue; const filepath = path.join(sessionDir, file); const content = await fs.readFile(filepath, 'utf8'); const store = JSON.parse(content); // Filter by domain if specified if (domain && !store.domain.includes(domain)) continue; for (const storedCookie of store.cookies) { // Destructure to exclude encryptedValue const { encryptedValue, ...playwrightCookie } = storedCookie; // Decrypt cookie value if encrypted (encryption key is guaranteed to exist) if (encryptedValue) { try { const encrypted = JSON.parse(encryptedValue); const decrypted = await this.decrypt(encrypted); playwrightCookie.value = decrypted; } catch (error) { console.error(`Failed to decrypt cookie for ${playwrightCookie.name}:`, error); continue; // Skip this cookie } } cookies.push(playwrightCookie); } } return cookies; } catch (error) { throw new Error(`Failed to load cookies: ${error}`); } } async clearCookies(sessionId) { try { const sessionDir = path.join(this.storageDir, sessionId); try { await fs.rm(sessionDir, { recursive: true, force: true }); } catch (error) { // Ignore if directory doesn't exist if (error.code !== 'ENOENT') { throw error; } } } catch (error) { throw new Error(`Failed to clear cookies: ${error}`); } } async listSessions() { try { const entries = await fs.readdir(this.storageDir, { withFileTypes: true }); return entries .filter(entry => entry.isDirectory()) .map(entry => entry.name); } catch (error) { if (error.code === 'ENOENT') { return []; } throw new Error(`Failed to list sessions: ${error}`); } } async importBrowserCookies(browserProfile) { // This is a placeholder for browser-specific cookie import logic // In a real implementation, this would: // 1. Detect the browser type (Chrome, Firefox, Safari, etc.) // 2. Locate the browser's cookie storage // 3. Read and decrypt browser-specific cookie formats // 4. Convert to Playwright cookie format throw new Error('Browser cookie import not yet implemented'); } } //# sourceMappingURL=cookie-manager.js.map