UNPKG

safevibe

Version:

Safevibe CLI - Simple personal secret vault for AI developers and amateur vibe coders

233 lines (232 loc) 8.67 kB
import { createHash } from "node:crypto"; import { readFile, writeFile, mkdir } from "node:fs/promises"; import { homedir } from "node:os"; import { join } from "node:path"; /** * HTTP client that mimics tRPC interface */ class HttpTrpcClient { baseUrl; getHeaders; constructor(baseUrl, getHeaders) { this.baseUrl = baseUrl; this.getHeaders = getHeaders; } secret = { save: { mutate: async (params) => { const endpoint = 'cli.saveSecret'; const response = await fetch(`${this.baseUrl}/api/trpc/${endpoint}`, { method: "POST", headers: { "Content-Type": "application/json", ...this.getHeaders(), }, body: JSON.stringify({ json: params }), }); if (!response.ok) { throw new Error(`Failed to save secret: ${response.statusText}`); } const data = await response.json(); return data.result?.data?.json || { version: 1 }; }, }, get: { query: async (params) => { const endpoint = 'cli.getSecret'; const url = new URL(`${this.baseUrl}/api/trpc/${endpoint}`); url.searchParams.set("input", JSON.stringify({ json: params })); const response = await fetch(url.toString(), { headers: this.getHeaders(), }); if (!response.ok) { throw new Error(`Failed to get secret: ${response.statusText}`); } const data = await response.json(); return data.result?.data?.json || { ciphertext: "", version: 1 }; }, }, list: { query: async () => { const endpoint = 'cli.listSecrets'; const response = await fetch(`${this.baseUrl}/api/trpc/${endpoint}`, { headers: this.getHeaders(), }); if (!response.ok) { throw new Error(`Failed to list secrets: ${response.statusText}`); } const data = await response.json(); return data.result?.data?.json || []; }, }, rotate: { mutate: async (params) => { const endpoint = 'cli.rotateSecret'; const response = await fetch(`${this.baseUrl}/api/trpc/${endpoint}`, { method: "POST", headers: { "Content-Type": "application/json", ...this.getHeaders(), }, body: JSON.stringify({ json: params }), }); if (!response.ok) { throw new Error(`Failed to rotate secret: ${response.statusText}`); } const data = await response.json(); return data.result?.data?.json || { version: 1 }; }, }, }; } /** * Safevibe Client for MCP Server * * Handles: * - HTTP communication with backend * - Local X25519 encryption/decryption * - Session management * - Configuration persistence * * Simplified to work without projects - everything is user-scoped */ export class SafevibeClient { trpc; config; configPath; constructor() { // Default configuration - use production backend this.config = { backendUrl: process.env.SAFEVIBE_BACKEND_URL || "https://safevibe.vercel.app", }; // Configuration file path this.configPath = join(homedir(), ".safevibe", "mcp-config.json"); // Initialize HTTP client that mimics tRPC this.trpc = new HttpTrpcClient(this.config.backendUrl, () => { const headers = {}; if (this.config.sessionToken) { headers.Authorization = `Bearer ${this.config.sessionToken}`; } return headers; }); } /** * Initialize the client * Load configuration, ensure authentication, setup encryption keys */ async initialize() { await this.loadConfig(); await this.ensureAuthenticated(); await this.ensureKeys(); } /** * Cleanup resources */ async cleanup() { // Nothing to clean up for now } /** * Load configuration from file system */ async loadConfig() { try { const configData = await readFile(this.configPath, "utf-8"); const fileConfig = JSON.parse(configData); this.config = { ...this.config, ...fileConfig }; } catch (error) { // Config file doesn't exist or invalid, use defaults console.error("📝 No MCP config found, using defaults"); } } /** * Save configuration to file system */ async saveConfig() { try { await mkdir(join(homedir(), ".safevibe"), { recursive: true }); await writeFile(this.configPath, JSON.stringify(this.config, null, 2)); } catch (error) { console.error("❌ Failed to save MCP config:", error); } } /** * Ensure user is authenticated * Try to load session from CLI config, require proper authentication */ async ensureAuthenticated() { // Try to load session from CLI config try { const cliConfigPath = join(homedir(), ".safevibe", "config.json"); const cliConfigData = await readFile(cliConfigPath, "utf-8"); const cliConfig = JSON.parse(cliConfigData); if (cliConfig.sessionToken && cliConfig.userId) { console.error("✅ Found CLI authentication"); this.config.sessionToken = cliConfig.sessionToken; this.config.userId = cliConfig.userId; this.config.backendUrl = cliConfig.backendUrl || this.config.backendUrl; await this.saveConfig(); return; } } catch (error) { // CLI config doesn't exist or invalid } throw new Error("No valid authentication found. Please run 'safevibe init' to authenticate first."); } /** * Ensure cryptographic keys are generated * For demo purposes, we'll use a simple symmetric encryption * In production, this would use X25519 with proper key derivation */ async ensureKeys() { if (!this.config.privateKey || !this.config.publicKey) { console.error("🔑 Generating new encryption keys..."); // For demo: simple key derivation from a password // In production: use X25519 key generation const seed = process.env.SAFEVIBE_ENCRYPTION_KEY || "demo-key-safevibe-2024"; const hash = createHash("sha256").update(seed).digest("hex"); this.config.privateKey = hash.substring(0, 32); this.config.publicKey = hash.substring(32, 64); await this.saveConfig(); console.error("✅ Encryption keys generated and saved"); } } /** * Encrypt a secret value locally * For demo: simple XOR encryption * In production: use age encryption or similar with X25519 */ async encryptSecret(plaintext) { if (!this.config.privateKey) { throw new Error("No encryption key available"); } // Simple XOR encryption for demo const key = Buffer.from(this.config.privateKey, "hex"); const data = Buffer.from(plaintext, "utf-8"); const encrypted = Buffer.alloc(data.length); for (let i = 0; i < data.length; i++) { encrypted[i] = data[i] ^ key[i % key.length]; } return encrypted.toString("base64"); } /** * Decrypt a secret value locally * For demo: simple XOR decryption * In production: use age decryption or similar with X25519 */ async decryptSecret(ciphertext) { if (!this.config.privateKey) { throw new Error("No encryption key available"); } // Simple XOR decryption for demo (same as encryption) const key = Buffer.from(this.config.privateKey, "hex"); const data = Buffer.from(ciphertext, "base64"); const decrypted = Buffer.alloc(data.length); for (let i = 0; i < data.length; i++) { decrypted[i] = data[i] ^ key[i % key.length]; } return decrypted.toString("utf-8"); } }