import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; export const SECRET_DEFINITIONS = Object.freeze([ { name: "OPENAI_API_KEY", label: "OpenAI API Key", tools: ["codex", "opencode"] }, { name: "ANTHROPIC_API_KEY", label: "Anthropic API Key", tools: ["claude", "opencode"] }, { name: "CODEBUDDY_API_KEY", label: "CodeBuddy API Key", tools: ["codebuddy"] }, { name: "MOONSHOT_API_KEY", label: "Moonshot API Key", tools: ["kimi"] }, { name: "DASHSCOPE_API_KEY", label: "DashScope API Key", tools: ["qwen"] }, { name: "DEEPSEEK_API_KEY", label: "DeepSeek API Key", tools: ["dsh", "opencode"] }, { name: "GOOGLE_GENERATIVE_AI_API_KEY", label: "Google AI API Key", tools: ["opencode"] }, ]); const DEFINITION_BY_NAME = new Map(SECRET_DEFINITIONS.map((item) => [item.name, item])); function mask(value) { if (!value) return null; if (value.length < 9) return "********"; return `${value.slice(0, 3)}...${value.slice(-4)}`; } export class SettingsStore { constructor(file, encryptionSecret) { this.file = file; this.key = crypto.createHash("sha256").update(encryptionSecret).digest(); this.data = { secrets: {}, preferences: { models: {}, dshProfile: "headless", ccSwitchManaged: false } }; } async init() { await fs.mkdir(path.dirname(this.file), { recursive: true, mode: 0o700 }); try { const payload = JSON.parse(await fs.readFile(this.file, "utf8")); const iv = Buffer.from(payload.iv, "base64"); const tag = Buffer.from(payload.tag, "base64"); const decipher = crypto.createDecipheriv("aes-256-gcm", this.key, iv); decipher.setAuthTag(tag); const plain = Buffer.concat([ decipher.update(Buffer.from(payload.data, "base64")), decipher.final(), ]); const parsed = JSON.parse(plain.toString("utf8")); this.data = { secrets: parsed.secrets || {}, preferences: { models: {}, dshProfile: "headless", ccSwitchManaged: false, ...(parsed.preferences || {}) }, }; } catch (error) { if (error.code !== "ENOENT") throw new Error(`Cannot decrypt console settings: ${error.message}`); } } publicView() { return { secrets: SECRET_DEFINITIONS.map((definition) => ({ ...definition, configured: Boolean(this.data.secrets[definition.name]), masked: mask(this.data.secrets[definition.name]), })), preferences: this.data.preferences, }; } environmentForTool(toolId) { const environment = {}; if (this.data.preferences.ccSwitchManaged && ["claude", "codex", "opencode"].includes(toolId)) { return environment; } for (const definition of SECRET_DEFINITIONS) { if (definition.tools.includes(toolId) && this.data.secrets[definition.name]) { environment[definition.name] = this.data.secrets[definition.name]; } } return environment; } async setSecret(name, value) { if (!DEFINITION_BY_NAME.has(name)) throw new Error("Unsupported secret name"); if (value) this.data.secrets[name] = value; else delete this.data.secrets[name]; await this.save(); } async setPreferences(preferences) { const models = typeof preferences.models === "object" && preferences.models ? preferences.models : this.data.preferences.models; const cleanModels = Object.fromEntries(Object.entries(models).map(([key, value]) => [ String(key).slice(0, 32), String(value).slice(0, 128), ])); const dshProfile = "headless"; const ccSwitchManaged = typeof preferences.ccSwitchManaged === "boolean" ? preferences.ccSwitchManaged : this.data.preferences.ccSwitchManaged; this.data.preferences = { models: cleanModels, dshProfile, ccSwitchManaged }; await this.save(); } async save() { const iv = crypto.randomBytes(12); const cipher = crypto.createCipheriv("aes-256-gcm", this.key, iv); const encrypted = Buffer.concat([ cipher.update(JSON.stringify(this.data), "utf8"), cipher.final(), ]); const payload = `${JSON.stringify({ version: 1, iv: iv.toString("base64"), tag: cipher.getAuthTag().toString("base64"), data: encrypted.toString("base64"), }, null, 2)}\n`; const temp = `${this.file}.tmp`; await fs.writeFile(temp, payload, { mode: 0o600 }); await fs.rename(temp, this.file); } }