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;
}