Skip to main content
Glama

WhaTap MXQL CLI

by devload
SessionStore.ts6.43 kB
/** * SessionStore - Encrypted Session Storage Manager * * This module handles secure storage and retrieval of WhaTap session data. * Sessions are encrypted using AES-256-GCM before being stored to disk. */ import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; import * as crypto from 'crypto'; import type { Session, EncryptedSessionData } from '../types'; import { SessionError } from '../types'; /** * SessionStore manages encrypted session persistence * * Features: * - AES-256-GCM encryption * - Automatic key generation * - Session expiry checking * - Atomic file operations */ export class SessionStore { private configDir: string; private sessionFile: string; private keyFile: string; private encryptionKey: Buffer | null = null; constructor(configDir?: string) { this.configDir = configDir || path.join(os.homedir(), '.whatap-mxql'); this.sessionFile = path.join(this.configDir, 'session.enc'); this.keyFile = path.join(this.configDir, '.key'); } /** * Save session data with encryption * * @param session - Session data to save * @throws {SessionError} If encryption or file write fails */ async save(session: Session): Promise<void> { try { // Ensure config directory exists await fs.mkdir(this.configDir, { recursive: true }); // Get or create encryption key const key = await this.getOrCreateEncryptionKey(); // Serialize session const sessionJson = JSON.stringify({ ...session, createdAt: session.createdAt.toISOString(), expiresAt: session.expiresAt.toISOString(), }); // Encrypt session data const encrypted = this.encrypt(sessionJson, key); // Write to file atomically (write to temp, then rename) const tempFile = `${this.sessionFile}.tmp`; await fs.writeFile(tempFile, JSON.stringify(encrypted), { encoding: 'utf-8', mode: 0o600, // Owner read/write only }); await fs.rename(tempFile, this.sessionFile); } catch (error) { throw new SessionError( `Failed to save session: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } /** * Load session data with decryption * * @returns Session data or null if not found or expired * @throws {SessionError} If decryption fails */ async load(): Promise<Session | null> { try { // Check if session file exists const encrypted = await fs.readFile(this.sessionFile, 'utf-8'); const encryptedData: EncryptedSessionData = JSON.parse(encrypted); // Get encryption key const key = await this.getOrCreateEncryptionKey(); // Decrypt session data const decrypted = this.decrypt(encryptedData, key); const sessionData = JSON.parse(decrypted); // Parse dates const session: Session = { ...sessionData, createdAt: new Date(sessionData.createdAt), expiresAt: new Date(sessionData.expiresAt), }; // Check expiry if (new Date(session.expiresAt) < new Date()) { await this.clear(); return null; } return session; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { return null; // File doesn't exist } throw new SessionError( `Failed to load session: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } /** * Clear session data */ async clear(): Promise<void> { try { await fs.unlink(this.sessionFile); } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { throw new SessionError( `Failed to clear session: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } } /** * Check if session file exists */ async exists(): Promise<boolean> { try { await fs.access(this.sessionFile); return true; } catch { return false; } } /** * Encrypt data using AES-256-GCM * * @param text - Plain text to encrypt * @param key - Encryption key (32 bytes) * @returns Encrypted data with IV and auth tag */ private encrypt(text: string, key: Buffer): EncryptedSessionData { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); return { iv: iv.toString('hex'), encrypted, authTag: authTag.toString('hex'), }; } /** * Decrypt data using AES-256-GCM * * @param data - Encrypted data with IV and auth tag * @param key - Encryption key (32 bytes) * @returns Decrypted plain text */ private decrypt(data: EncryptedSessionData, key: Buffer): string { const decipher = crypto.createDecipheriv( 'aes-256-gcm', key, Buffer.from(data.iv, 'hex') ); decipher.setAuthTag(Buffer.from(data.authTag, 'hex')); let decrypted = decipher.update(data.encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } /** * Get or create encryption key * * - If key file exists, load it * - If not, generate new 256-bit key and save it * - Key file has restricted permissions (0600) * * @returns 32-byte encryption key */ private async getOrCreateEncryptionKey(): Promise<Buffer> { if (this.encryptionKey) { return this.encryptionKey; } try { // Try to load existing key const keyData = await fs.readFile(this.keyFile); this.encryptionKey = keyData; return keyData; } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { throw error; } // Generate new key const key = crypto.randomBytes(32); // Ensure config directory exists await fs.mkdir(this.configDir, { recursive: true }); // Save key with restricted permissions await fs.writeFile(this.keyFile, key, { mode: 0o600 }); this.encryptionKey = key; return key; } } /** * Get config directory path (for testing) */ getConfigDir(): string { return this.configDir; } /** * Get session file path (for testing) */ getSessionFilePath(): string { return this.sessionFile; } }

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/devload/whatap-mxql-cli'

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