ccguard
Version:
Automated enforcement of net-negative LOC, complexity constraints, and quality standards for Claude code
244 lines • 9.23 kB
JavaScript
;
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