src / tools / configTools.ts
src / tools / configTools.ts
import fs from 'fs/promises';
import path from 'path';
import { getState } from '../stateRegistry';
import { validatePath as validateWorkspacePath } from '../pathSecurity';
// Sensitive key patterns to redact during export
const SENSITIVE_PATTERNS = ['password', 'secret', 'token', 'connectionString', 'smtpPassword'];
/**
* Recursively redact sensitive values in an object.
*/
function redactSensitive(obj: any, path: string = ''): any {
if (typeof obj !== 'object' || obj === null) return obj;
const result: any = {};
for (const [key, value] of Object.entries(obj)) {
const fullPath = path ? `${path}.${key}` : key;
// Check if this key matches sensitive patterns
const isSensitive = SENSITIVE_PATTERNS.some(s =>
key.toLowerCase().includes(s) || fullPath.toLowerCase().includes(s)
);
if (isSensitive && typeof value === 'string' && value.length > 0) {
result[key] = '[REDACTED]';
} else if (typeof value === 'object') {
result[key] = redactSensitive(value, fullPath);
} else {
result[key] = value;
}
}
return result;
}
export async function configExport(outputPath?: string, workspaceRoot: string = process.cwd()): Promise<{ success: boolean; file_path?: string; field_count: number }> {
const exportData = { version: '1.4.0', exportedAt: new Date().toISOString(), config: {} };
// TODO: In production, this would call ctl.getPluginConfig() to get actual values
// For now, we redact the structure but not the content
const filePath = validateWorkspacePath(
workspaceRoot,
workspaceRoot,
outputPath || `config_export_${Date.now()}.json`,
);
await fs.writeFile(filePath, JSON.stringify(exportData, null, 2));
getState().lastConfigChange = new Date();
getState().configVersion++;
return { success: true, file_path: filePath, field_count: Object.keys(exportData.config).length };
}
/**
* Get config with sensitive fields redacted (for display to user/agent).
*/
export async function getConfigRedacted(): Promise<{ success: boolean; config: any }> {
// TODO: In production, this would call ctl.getPluginConfig() and redact
return {
success: true,
config: {
message: "Config retrieval requires plugin context. Use configExport for file export.",
sensitiveFieldsRedacted: SENSITIVE_PATTERNS
}
};
}
export async function configImport(inputPath: string, merge: boolean = false, workspaceRoot: string = process.cwd()): Promise<{ success: boolean; imported_fields: number; errors: string[] }> {
const errors: string[] = [];
try {
const safeInputPath = validateWorkspacePath(workspaceRoot, workspaceRoot, inputPath);
const content = await fs.readFile(safeInputPath, 'utf-8');
const data = JSON.parse(content);
if (!data.version || !data.config) {
errors.push('Invalid config format: missing version or config fields');
return { success: false, imported_fields: 0, errors };
}
getState().lastConfigChange = new Date();
getState().configVersion++;
return { success: true, imported_fields: Object.keys(data.config).length, errors };
} catch (err) {
errors.push(`Import failed: ${(err as Error).message}`);
return { success: false, imported_fields: 0, errors };
}
}
export async function configBackup(workspaceRoot: string = process.cwd()): Promise<{ success: boolean; backup_path?: string }> {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = validateWorkspacePath(
workspaceRoot,
workspaceRoot,
`config_backup_${timestamp}.json`,
);
await fs.writeFile(backupPath, JSON.stringify({ version: '1.4.0', exportedAt: new Date().toISOString() }, null, 2));
return { success: true, backup_path: backupPath };
}
export async function configRestore(backupPath: string, workspaceRoot: string = process.cwd()): Promise<{ success: boolean; restored_count?: number }> {
const safeBackupPath = validateWorkspacePath(workspaceRoot, workspaceRoot, backupPath);
const content = await fs.readFile(safeBackupPath, 'utf-8');
const data = JSON.parse(content);
getState().lastConfigChange = new Date();
getState().configVersion++;
return { success: true, restored_count: Object.keys(data.config).length };
}
import fs from 'fs/promises';
import path from 'path';
import { getState } from '../stateRegistry';
import { validatePath as validateWorkspacePath } from '../pathSecurity';
// Sensitive key patterns to redact during export
const SENSITIVE_PATTERNS = ['password', 'secret', 'token', 'connectionString', 'smtpPassword'];
/**
* Recursively redact sensitive values in an object.
*/
function redactSensitive(obj: any, path: string = ''): any {
if (typeof obj !== 'object' || obj === null) return obj;
const result: any = {};
for (const [key, value] of Object.entries(obj)) {
const fullPath = path ? `${path}.${key}` : key;
// Check if this key matches sensitive patterns
const isSensitive = SENSITIVE_PATTERNS.some(s =>
key.toLowerCase().includes(s) || fullPath.toLowerCase().includes(s)
);
if (isSensitive && typeof value === 'string' && value.length > 0) {
result[key] = '[REDACTED]';
} else if (typeof value === 'object') {
result[key] = redactSensitive(value, fullPath);
} else {
result[key] = value;
}
}
return result;
}
export async function configExport(outputPath?: string, workspaceRoot: string = process.cwd()): Promise<{ success: boolean; file_path?: string; field_count: number }> {
const exportData = { version: '1.4.0', exportedAt: new Date().toISOString(), config: {} };
// TODO: In production, this would call ctl.getPluginConfig() to get actual values
// For now, we redact the structure but not the content
const filePath = validateWorkspacePath(
workspaceRoot,
workspaceRoot,
outputPath || `config_export_${Date.now()}.json`,
);
await fs.writeFile(filePath, JSON.stringify(exportData, null, 2));
getState().lastConfigChange = new Date();
getState().configVersion++;
return { success: true, file_path: filePath, field_count: Object.keys(exportData.config).length };
}
/**
* Get config with sensitive fields redacted (for display to user/agent).
*/
export async function getConfigRedacted(): Promise<{ success: boolean; config: any }> {
// TODO: In production, this would call ctl.getPluginConfig() and redact
return {
success: true,
config: {
message: "Config retrieval requires plugin context. Use configExport for file export.",
sensitiveFieldsRedacted: SENSITIVE_PATTERNS
}
};
}
export async function configImport(inputPath: string, merge: boolean = false, workspaceRoot: string = process.cwd()): Promise<{ success: boolean; imported_fields: number; errors: string[] }> {
const errors: string[] = [];
try {
const safeInputPath = validateWorkspacePath(workspaceRoot, workspaceRoot, inputPath);
const content = await fs.readFile(safeInputPath, 'utf-8');
const data = JSON.parse(content);
if (!data.version || !data.config) {
errors.push('Invalid config format: missing version or config fields');
return { success: false, imported_fields: 0, errors };
}
getState().lastConfigChange = new Date();
getState().configVersion++;
return { success: true, imported_fields: Object.keys(data.config).length, errors };
} catch (err) {
errors.push(`Import failed: ${(err as Error).message}`);
return { success: false, imported_fields: 0, errors };
}
}
export async function configBackup(workspaceRoot: string = process.cwd()): Promise<{ success: boolean; backup_path?: string }> {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = validateWorkspacePath(
workspaceRoot,
workspaceRoot,
`config_backup_${timestamp}.json`,
);
await fs.writeFile(backupPath, JSON.stringify({ version: '1.4.0', exportedAt: new Date().toISOString() }, null, 2));
return { success: true, backup_path: backupPath };
}
export async function configRestore(backupPath: string, workspaceRoot: string = process.cwd()): Promise<{ success: boolean; restored_count?: number }> {
const safeBackupPath = validateWorkspacePath(workspaceRoot, workspaceRoot, backupPath);
const content = await fs.readFile(safeBackupPath, 'utf-8');
const data = JSON.parse(content);
getState().lastConfigChange = new Date();
getState().configVersion++;
return { success: true, restored_count: Object.keys(data.config).length };
}