UNPKG

ccguard

Version:

Automated enforcement of net-negative LOC, complexity constraints, and quality standards for Claude code

244 lines 9.23 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.FileStorage = void 0; const fs_1 = require("fs"); const path_1 = __importDefault(require("path")); const os_1 = __importDefault(require("os")); const fs_2 = require("fs"); const contracts_1 = require("../contracts"); // Debug logging - only enabled when CCGUARD_DEBUG environment variable is set const DEBUG = process.env.CCGUARD_DEBUG === 'true' || process.env.CCGUARD_DEBUG === '1'; const debugLog = (message) => { if (!DEBUG) return; const ccguardDir = path_1.default.join(os_1.default.homedir(), '.ccguard'); const logPath = path_1.default.join(ccguardDir, 'debug.log'); // Ensure directory exists (0, fs_2.mkdirSync)(ccguardDir, { recursive: true }); (0, fs_2.appendFileSync)(logPath, `${new Date().toISOString()} - ${JSON.stringify(message)}\n`); }; class FileStorage { dataDir; sessionStatsFile; guardStateFile; hotConfigFile; operationHistoryFile; lockedFilesFile; // Whitelist pattern for valid storage keys static VALID_KEY_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9-_:]*$/; static MAX_KEY_LENGTH = 255; constructor(sessionId) { // Use session-specific directory if sessionId provided const baseDir = path_1.default.join(os_1.default.homedir(), '.ccguard'); this.dataDir = sessionId ? path_1.default.join(baseDir, sessionId) : baseDir; this.sessionStatsFile = path_1.default.join(this.dataDir, 'session-stats.json'); this.guardStateFile = path_1.default.join(this.dataDir, 'ccguard-state.json'); this.hotConfigFile = path_1.default.join(this.dataDir, 'hot-config.json'); this.operationHistoryFile = path_1.default.join(this.dataDir, 'operation-history.json'); this.lockedFilesFile = path_1.default.join(this.dataDir, 'locked-files.json'); } async ensureDir() { await fs_1.promises.mkdir(this.dataDir, { recursive: true }); } async getSessionStats() { try { const data = await fs_1.promises.readFile(this.sessionStatsFile, 'utf8'); const parsed = JSON.parse(data); return contracts_1.SessionStatsSchema.parse(parsed); } catch (error) { if (DEBUG) { debugLog({ event: 'storage_error', method: 'getSessionStats', file: this.sessionStatsFile, error: error instanceof Error ? error.message : String(error) }); } return null; } } async saveSessionStats(stats) { await this.ensureDir(); await fs_1.promises.writeFile(this.sessionStatsFile, JSON.stringify(stats, null, 2), 'utf8'); } async getGuardState() { try { const data = await fs_1.promises.readFile(this.guardStateFile, 'utf8'); const parsed = JSON.parse(data); return contracts_1.GuardStateSchema.parse(parsed); } catch (error) { if (DEBUG) { debugLog({ event: 'storage_error', method: 'getGuardState', file: this.guardStateFile, error: error instanceof Error ? error.message : String(error) }); } return null; } } async saveGuardState(state) { await this.ensureDir(); await fs_1.promises.writeFile(this.guardStateFile, JSON.stringify(state, null, 2), 'utf8'); } async clearAll() { try { await fs_1.promises.rm(this.dataDir, { recursive: true, force: true }); } catch (error) { if (DEBUG) { debugLog({ event: 'storage_error', method: 'clearAll', directory: this.dataDir, error: error instanceof Error ? error.message : String(error) }); } // Ignore errors } } async getHotConfig() { try { const data = await fs_1.promises.readFile(this.hotConfigFile, 'utf8'); const parsed = JSON.parse(data); return contracts_1.HotConfigSchema.parse(parsed); } catch (error) { if (DEBUG) { debugLog({ event: 'storage_error', method: 'getHotConfig', file: this.hotConfigFile, error: error instanceof Error ? error.message : String(error) }); } return null; } } async saveHotConfig(config) { await this.ensureDir(); await fs_1.promises.writeFile(this.hotConfigFile, JSON.stringify(config, null, 2), 'utf8'); } async getOperationHistory() { try { const data = await fs_1.promises.readFile(this.operationHistoryFile, 'utf8'); const parsed = JSON.parse(data); return contracts_1.OperationHistorySchema.parse(parsed); } catch (error) { if (DEBUG) { debugLog({ event: 'storage_error', method: 'getOperationHistory', file: this.operationHistoryFile, error: error instanceof Error ? error.message : String(error) }); } return null; } } async saveOperationHistory(history) { await this.ensureDir(); await fs_1.promises.writeFile(this.operationHistoryFile, JSON.stringify(history, null, 2), 'utf8'); } async getLockedFiles() { try { const data = await fs_1.promises.readFile(this.lockedFilesFile, 'utf8'); const parsed = JSON.parse(data); return contracts_1.LockedFilesSchema.parse(parsed); } catch (error) { if (DEBUG) { debugLog({ event: 'storage_error', method: 'getLockedFiles', file: this.lockedFilesFile, error: error instanceof Error ? error.message : String(error) }); } return null; } } async saveLockedFiles(lockedFiles) { await this.ensureDir(); await fs_1.promises.writeFile(this.lockedFilesFile, JSON.stringify(lockedFiles, null, 2), 'utf8'); } async get(key) { try { const sanitizedKey = this.sanitizeKey(key); const fileName = `${sanitizedKey}.json`; const filePath = path_1.default.join(this.dataDir, fileName); const data = await fs_1.promises.readFile(filePath, 'utf8'); return JSON.parse(data); } catch (error) { if (DEBUG) { debugLog({ event: 'storage_error', method: 'get', key: key, error: error instanceof Error ? error.message : String(error) }); } return null; } } async set(key, value) { await this.ensureDir(); const sanitizedKey = this.sanitizeKey(key); const fileName = `${sanitizedKey}.json`; const filePath = path_1.default.join(this.dataDir, fileName); await fs_1.promises.writeFile(filePath, JSON.stringify(value, null, 2), 'utf8'); } async delete(key) { try { const sanitizedKey = this.sanitizeKey(key); const fileName = `${sanitizedKey}.json`; const filePath = path_1.default.join(this.dataDir, fileName); await fs_1.promises.unlink(filePath); } catch (error) { if (DEBUG) { debugLog({ event: 'storage_error', method: 'delete', key: key, error: error instanceof Error ? error.message : String(error) }); } // Ignore errors if file doesn't exist } } /** * Sanitize storage key to prevent path injection attacks * Uses a whitelist approach for maximum security */ sanitizeKey(key) { // Validate key length if (key.length > FileStorage.MAX_KEY_LENGTH) { throw new Error(`Storage key too long: ${key.length} characters (max: ${FileStorage.MAX_KEY_LENGTH})`); } // Validate key format if (!FileStorage.VALID_KEY_PATTERN.test(key)) { // If key doesn't match pattern, create a safe version const safeKey = key.replace(/[^a-zA-Z0-9-_:]/g, '_').replace(/^[^a-zA-Z0-9]/, 'k'); if (DEBUG) { debugLog({ event: 'key_sanitized', originalKey: key, sanitizedKey: safeKey }); } return safeKey; } return key; } } exports.FileStorage = FileStorage; //# sourceMappingURL=FileStorage.js.map