/** * Persistent app config for a shared/team build. The gitea PAT is encrypted at * rest with Electron's safeStorage (OS keychain-backed) and only ever lives in * the main process — the renderer receives everything *except* the token. Config * lives in userData so each teammate has their own; a `.env.local` remains a dev * fallback (see resolveConfig). Nothing here runs under COMMITEA_E2E. */ import { readFileSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { app, safeStorage } from 'electron' export interface AppConfig { baseUrl: string owner: string repo: string token: string /** Defaults to `${repo}-pm-state` when omitted. */ pmStateRepo?: string /** OpenAI-compatible base for Reginald; chat stays off when absent. */ modelUrl?: string } /** The renderer-safe view — everything but the token, plus whether one is set. */ export type PublicConfig = Omit & { hasToken: boolean } interface StoredConfig { baseUrl: string owner: string repo: string tokenEnc: string | null // base64 of safeStorage-encrypted token pmStateRepo?: string modelUrl?: string } function configPath(): string { return join(app.getPath('userData'), 'commitea-config.json') } function readStored(): StoredConfig | null { try { return JSON.parse(readFileSync(configPath(), 'utf8')) as StoredConfig } catch { return null } } /** Full config incl. the decrypted token, or null when unset. Main-process only. */ export function loadConfig(): AppConfig | null { const s = readStored() if (!s || !s.baseUrl || !s.owner || !s.repo) return null let token = '' if (s.tokenEnc) { try { token = safeStorage.decryptString(Buffer.from(s.tokenEnc, 'base64')) } catch { token = '' // key rotated / different machine — treat as no token } } if (!token) return null return { baseUrl: s.baseUrl, owner: s.owner, repo: s.repo, token, pmStateRepo: s.pmStateRepo, modelUrl: s.modelUrl } } /** The renderer-safe view of the saved config (never includes the token). */ export function publicConfig(): PublicConfig | null { const s = readStored() if (!s) return null return { baseUrl: s.baseUrl, owner: s.owner, repo: s.repo, pmStateRepo: s.pmStateRepo, modelUrl: s.modelUrl, hasToken: !!s.tokenEnc, } } /** Encrypt the token + persist. Best-effort; throws only on a genuine write failure. */ export function saveConfig(cfg: AppConfig): void { const tokenEnc = cfg.token ? Buffer.from(safeStorage.encryptString(cfg.token)).toString('base64') : (readStored()?.tokenEnc ?? null) // keep the existing token if none supplied const stored: StoredConfig = { baseUrl: cfg.baseUrl.trim().replace(/\/+$/, ''), owner: cfg.owner.trim(), repo: cfg.repo.trim(), tokenEnc, pmStateRepo: cfg.pmStateRepo?.trim() || undefined, modelUrl: cfg.modelUrl?.trim() || undefined, } writeFileSync(configPath(), JSON.stringify(stored, null, 2), 'utf8') } export function clearConfig(): void { try { writeFileSync(configPath(), JSON.stringify({ baseUrl: '', owner: '', repo: '', tokenEnc: null }), 'utf8') } catch { // ignore } }