diff --git a/src/components/GitHubConnector.tsx b/src/components/GitHubConnector.tsx index c389420..71a8033 100644 --- a/src/components/GitHubConnector.tsx +++ b/src/components/GitHubConnector.tsx @@ -171,7 +171,7 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) { } }; - if (!settings?.githubSettings.secrets?.accessToken) { + if (!settings?.githubAccessToken) { return (
{" "} diff --git a/src/components/settings/ProviderSettingsPage.tsx b/src/components/settings/ProviderSettingsPage.tsx index 816714d..7ba2f05 100644 --- a/src/components/settings/ProviderSettingsPage.tsx +++ b/src/components/settings/ProviderSettingsPage.tsx @@ -59,7 +59,7 @@ export function ProviderSettingsPage({ provider }: ProviderSettingsPageProps) { const envVarName = PROVIDER_TO_ENV_VAR[provider]; const envApiKey = envVars[envVarName]; - const userApiKey = settings?.providerSettings?.[provider]?.apiKey; + const userApiKey = settings?.providerSettings?.[provider]?.apiKey?.value; // --- Configuration Logic --- Updated Priority --- const isValidUserKey = @@ -100,7 +100,9 @@ export function ProviderSettingsPage({ provider }: ProviderSettingsPageProps) { ...settings?.providerSettings, [provider]: { ...(settings?.providerSettings?.[provider] || {}), - apiKey: apiKeyInput, + apiKey: { + value: apiKeyInput, + }, }, }, }); @@ -124,7 +126,7 @@ export function ProviderSettingsPage({ provider }: ProviderSettingsPageProps) { ...settings?.providerSettings, [provider]: { ...(settings?.providerSettings?.[provider] || {}), - apiKey: null, + apiKey: undefined, }, }, }); diff --git a/src/ipc/handlers/github_handlers.ts b/src/ipc/handlers/github_handlers.ts index 7893834..301fc73 100644 --- a/src/ipc/handlers/github_handlers.ts +++ b/src/ipc/handlers/github_handlers.ts @@ -48,7 +48,7 @@ export async function getGithubUser(): Promise { const email = settings.githubUser?.email; if (email) return { email }; try { - const accessToken = settings.githubSettings?.secrets?.accessToken; + const accessToken = settings.githubAccessToken?.value; if (!accessToken) return null; const res = await fetch("https://api.github.com/user/emails", { headers: { Authorization: `Bearer ${accessToken}` }, @@ -116,10 +116,8 @@ async function pollForAccessToken(event: IpcMainInvokeEvent) { message: "Successfully connected!", }); writeSettings({ - githubSettings: { - secrets: { - accessToken: data.access_token, - }, + githubAccessToken: { + value: data.access_token, }, }); // TODO: Associate token with appId if provided @@ -324,7 +322,7 @@ async function handleIsRepoAvailable( try { // Get access token from settings const settings = readSettings(); - const accessToken = settings.githubSettings?.secrets?.accessToken; + const accessToken = settings.githubAccessToken?.value; if (!accessToken) { return { available: false, error: "Not authenticated with GitHub." }; } @@ -362,7 +360,7 @@ async function handleCreateRepo( try { // Get access token from settings const settings = readSettings(); - const accessToken = settings.githubSettings?.secrets?.accessToken; + const accessToken = settings.githubAccessToken?.value; if (!accessToken) { return { success: false, error: "Not authenticated with GitHub." }; } @@ -411,7 +409,7 @@ async function handlePushToGithub( try { // Get access token from settings const settings = readSettings(); - const accessToken = settings.githubSettings?.secrets?.accessToken; + const accessToken = settings.githubAccessToken?.value; if (!accessToken) { return { success: false, error: "Not authenticated with GitHub." }; } @@ -437,7 +435,10 @@ async function handlePushToGithub( dir: appPath, remote: "origin", ref: "main", - onAuth: () => ({ username: accessToken, password: "x-oauth-basic" }), + onAuth: () => ({ + username: accessToken, + password: "x-oauth-basic", + }), force: false, }); return { success: true }; diff --git a/src/ipc/handlers/settings_handlers.ts b/src/ipc/handlers/settings_handlers.ts index 798aa02..48e5d02 100644 --- a/src/ipc/handlers/settings_handlers.ts +++ b/src/ipc/handlers/settings_handlers.ts @@ -20,11 +20,7 @@ export function registerSettingsHandlers() { ) { const providerSetting = settings.providerSettings[providerKey]; // Check if apiKey exists and is a non-empty string before masking - if ( - providerSetting?.apiKey && - typeof providerSetting.apiKey === "string" && - providerSetting.apiKey.length > 0 - ) { + if (providerSetting?.apiKey?.value) { providerSetting.apiKey = providerSetting.apiKey; } } diff --git a/src/ipc/utils/get_model_client.ts b/src/ipc/utils/get_model_client.ts index 227de56..d02bb70 100644 --- a/src/ipc/utils/get_model_client.ts +++ b/src/ipc/utils/get_model_client.ts @@ -37,7 +37,7 @@ export function getModelClient( } const apiKey = - settings.providerSettings?.[model.provider]?.apiKey || + settings.providerSettings?.[model.provider]?.apiKey?.value || getEnvVar(PROVIDER_TO_ENV_VAR[model.provider]); switch (model.provider) { case "openai": { diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index b902b27..ba36451 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -1,5 +1,11 @@ import { z } from "zod"; +export const SecretSchema = z.object({ + value: z.string(), + encryptionType: z.enum(["electron-safe-storage", "plaintext"]).optional(), +}); +export type Secret = z.infer; + /** * Zod schema for chat summary objects returned by the get-chats IPC */ @@ -53,7 +59,7 @@ export type LargeLanguageModel = z.infer; * Zod schema for provider settings */ export const ProviderSettingSchema = z.object({ - apiKey: z.string().nullable(), + apiKey: SecretSchema.optional(), }); /** @@ -65,15 +71,10 @@ export const RuntimeModeSchema = z.enum(["web-sandbox", "local-node", "unset"]); export type RuntimeMode = z.infer; export const GitHubSecretsSchema = z.object({ - accessToken: z.string().nullable(), + accessToken: SecretSchema.nullable(), }); export type GitHubSecrets = z.infer; -export const GitHubSettingsSchema = z.object({ - secrets: GitHubSecretsSchema.nullable(), -}); -export type GitHubSettings = z.infer; - export const GithubUserSchema = z.object({ email: z.string(), }); @@ -86,8 +87,8 @@ export const UserSettingsSchema = z.object({ selectedModel: LargeLanguageModelSchema, providerSettings: z.record(z.string(), ProviderSettingSchema), runtimeMode: RuntimeModeSchema, - githubSettings: GitHubSettingsSchema, githubUser: GithubUserSchema.optional(), + githubAccessToken: SecretSchema.optional(), }); /** diff --git a/src/main/settings.ts b/src/main/settings.ts index b7e5d9f..1f07776 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -1,9 +1,11 @@ import fs from "node:fs"; import path from "node:path"; import { getUserDataPath } from "../paths/paths"; -import { UserSettingsSchema, type UserSettings } from "../lib/schemas"; +import { UserSettingsSchema, type UserSettings, Secret } from "../lib/schemas"; import { safeStorage } from "electron"; +// IF YOU NEED TO UPDATE THIS, YOU'RE PROBABLY DOING SOMETHING WRONG! +// Need to maintain backwards compatibility! const DEFAULT_SETTINGS: UserSettings = { selectedModel: { name: "auto", @@ -11,9 +13,6 @@ const DEFAULT_SETTINGS: UserSettings = { }, providerSettings: {}, runtimeMode: "unset", - githubSettings: { - secrets: null, - }, }; const SETTINGS_FILE = "user-settings.json"; @@ -30,18 +29,31 @@ export function readSettings(): UserSettings { return DEFAULT_SETTINGS; } const rawSettings = JSON.parse(fs.readFileSync(filePath, "utf-8")); - // Validate and merge with defaults - const validatedSettings = UserSettingsSchema.parse({ + const combinedSettings: UserSettings = { ...DEFAULT_SETTINGS, ...rawSettings, - }); - if (validatedSettings.githubSettings?.secrets) { - const accessToken = validatedSettings.githubSettings.secrets.accessToken; - - validatedSettings.githubSettings.secrets = { - accessToken: accessToken ? decrypt(accessToken) : null, + }; + if (combinedSettings.githubAccessToken) { + const encryptionType = combinedSettings.githubAccessToken.encryptionType; + combinedSettings.githubAccessToken = { + value: decrypt(combinedSettings.githubAccessToken), + encryptionType, }; } + for (const provider in combinedSettings.providerSettings) { + if (combinedSettings.providerSettings[provider].apiKey) { + const encryptionType = + combinedSettings.providerSettings[provider].apiKey.encryptionType; + combinedSettings.providerSettings[provider].apiKey = { + value: decrypt(combinedSettings.providerSettings[provider].apiKey), + encryptionType, + }; + } + } + + // Validate and merge with defaults + const validatedSettings = UserSettingsSchema.parse(combinedSettings); + return validatedSettings; } catch (error) { console.error("Error reading settings:", error); @@ -54,30 +66,42 @@ export function writeSettings(settings: Partial): void { const filePath = getSettingsFilePath(); const currentSettings = readSettings(); const newSettings = { ...currentSettings, ...settings }; - // Validate before writing - const validatedSettings = UserSettingsSchema.parse(newSettings); - if (validatedSettings.githubSettings?.secrets) { - const accessToken = validatedSettings.githubSettings.secrets.accessToken; - validatedSettings.githubSettings.secrets = { - accessToken: accessToken ? encrypt(accessToken) : null, - }; + if (newSettings.githubAccessToken) { + newSettings.githubAccessToken = encrypt( + newSettings.githubAccessToken.value + ); } + + for (const provider in newSettings.providerSettings) { + if (newSettings.providerSettings[provider].apiKey) { + newSettings.providerSettings[provider].apiKey = encrypt( + newSettings.providerSettings[provider].apiKey.value + ); + } + } + const validatedSettings = UserSettingsSchema.parse(newSettings); fs.writeFileSync(filePath, JSON.stringify(validatedSettings, null, 2)); } catch (error) { console.error("Error writing settings:", error); } } -export function encrypt(data: string): string { +export function encrypt(data: string): Secret { if (safeStorage.isEncryptionAvailable()) { - return safeStorage.encryptString(data).toString("base64"); + return { + value: safeStorage.encryptString(data).toString("base64"), + encryptionType: "electron-safe-storage", + }; } - return data; + return { + value: data, + encryptionType: "plaintext", + }; } -export function decrypt(data: string): string { - if (safeStorage.isEncryptionAvailable()) { - return safeStorage.decryptString(Buffer.from(data, "base64")); +export function decrypt(data: Secret): string { + if (data.encryptionType === "electron-safe-storage") { + return safeStorage.decryptString(Buffer.from(data.value, "base64")); } - return data; + return data.value; }