Files
claude-hub/cli/secure-config.js
Jonathan Flatt fc567071dd Initial commit
2025-05-20 17:01:59 +00:00

187 lines
4.8 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Secure configuration management for Claude webhook CLI
* Avoids storing credentials in environment variables
*/
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const readline = require('readline');
const CONFIG_DIR = path.join(process.env.HOME || '/tmp', '.claude-webhook');
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
const ENCRYPTED_CONFIG_FILE = path.join(CONFIG_DIR, 'config.enc');
// Create config directory if it doesn't exist
if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
}
/**
* Encrypt text using a key
*/
function encrypt(text, key) {
const algorithm = 'aes-256-gcm';
const salt = crypto.randomBytes(16);
const derivedKey = crypto.pbkdf2Sync(key, salt, 100000, 32, 'sha256');
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, derivedKey, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encrypted,
salt: salt.toString('hex'),
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
}
/**
* Decrypt text using a key
*/
function decrypt(encryptedData, key) {
const algorithm = 'aes-256-gcm';
const salt = Buffer.from(encryptedData.salt, 'hex');
const derivedKey = crypto.pbkdf2Sync(key, salt, 100000, 32, 'sha256');
const iv = Buffer.from(encryptedData.iv, 'hex');
const authTag = Buffer.from(encryptedData.authTag, 'hex');
const decipher = crypto.createDecipheriv(algorithm, derivedKey, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
/**
* Prompt for password (hidden input)
*/
async function promptPassword(prompt) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
rl.question(prompt, (password) => {
rl.close();
resolve(password);
});
});
}
/**
* Save credentials securely
*/
async function saveConfig(config) {
console.log('🔒 Setting up secure configuration...');
const password = await promptPassword('Enter a password to encrypt your config: ');
const confirmPassword = await promptPassword('Confirm password: ');
if (password !== confirmPassword) {
console.error('❌ Passwords do not match');
process.exit(1);
}
const configJson = JSON.stringify(config, null, 2);
const encrypted = encrypt(configJson, password);
fs.writeFileSync(ENCRYPTED_CONFIG_FILE, JSON.stringify(encrypted, null, 2));
fs.chmodSync(ENCRYPTED_CONFIG_FILE, 0o600);
console.log('✅ Configuration saved securely');
}
/**
* Load credentials securely
*/
async function loadConfig() {
if (!fs.existsSync(ENCRYPTED_CONFIG_FILE)) {
// Check for legacy plain config
if (fs.existsSync(CONFIG_FILE)) {
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
await saveConfig(config);
fs.unlinkSync(CONFIG_FILE); // Remove plain config
return config;
}
return null;
}
const password = await promptPassword('Enter password to decrypt config: ');
try {
const encryptedData = JSON.parse(fs.readFileSync(ENCRYPTED_CONFIG_FILE, 'utf8'));
const configJson = decrypt(encryptedData, password);
return JSON.parse(configJson);
} catch (error) {
console.error('❌ Failed to decrypt config. Wrong password?');
process.exit(1);
}
}
/**
* Initialize configuration
*/
async function initConfig() {
console.log('🔧 Claude Webhook CLI Configuration');
console.log('==================================\n');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (prompt) => new Promise((resolve) => {
rl.question(prompt, resolve);
});
const config = {
apiUrl: await question('API URL (default: http://localhost:3003): ') || 'http://localhost:3003',
githubToken: await question('GitHub Token: '),
webhookSecret: await question('Webhook Secret: ')
};
rl.close();
await saveConfig(config);
console.log('\n✅ Configuration complete!');
console.log('Your credentials are encrypted and stored securely.');
console.log('You can now use the CLI without environment variables.');
}
/**
* Get configuration
*/
async function getConfig() {
let config = await loadConfig();
if (!config) {
console.log('No configuration found. Let\'s set it up!\n');
await initConfig();
config = await loadConfig();
}
return config;
}
// Export for use in CLI
module.exports = {
getConfig,
initConfig,
CONFIG_DIR
};
// If run directly, initialize config
if (require.main === module) {
initConfig().catch(console.error);
}