From 4f4ab43eb39576b5bd3dfc16ced306d0653e72f0 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 20 Dec 2025 23:10:35 -0500 Subject: [PATCH] refactor(config): remove secrets.yaml architecture - delete secrets-manager.ts entirely - remove secrets API routes and client methods - simplify unified-config-types (remove vault/secrets) - update profile-reader to remove secrets loading - clean up unused hooks and API client methods --- src/api/services/profile-reader.ts | 25 +--- src/api/services/profile-writer.ts | 4 - src/auth/profile-detector.ts | 5 +- src/commands/api-command.ts | 4 +- src/config/index.ts | 1 - src/config/migration-manager.ts | 2 - src/config/secrets-manager.ts | 187 ------------------------- src/config/unified-config-types.ts | 33 +---- src/utils/config-manager.ts | 47 ++++++- src/web-server/routes/config-routes.ts | 33 ----- src/web-server/routes/index.ts | 3 +- tests/unit/unified-config.test.ts | 33 +---- ui/src/hooks/use-unified-config.ts | 27 ---- ui/src/lib/api-client.ts | 13 -- 14 files changed, 49 insertions(+), 368 deletions(-) delete mode 100644 src/config/secrets-manager.ts diff --git a/src/api/services/profile-reader.ts b/src/api/services/profile-reader.ts index fcc6d30e..7e6dd129 100644 --- a/src/api/services/profile-reader.ts +++ b/src/api/services/profile-reader.ts @@ -9,7 +9,6 @@ import * as fs from 'fs'; import * as path from 'path'; import { getCcsDir, loadConfig } from '../../utils/config-manager'; import { loadOrCreateUnifiedConfig, isUnifiedMode } from '../../config/unified-config-loader'; -import { getProfileSecrets } from '../../config/secrets-manager'; import type { ApiProfileInfo, CliproxyVariantInfo, ApiListResult } from './profile-types'; /** @@ -36,29 +35,7 @@ export function isApiProfileConfigured(apiName: string): boolean { const ccsDir = getCcsDir(); const settingsPath = path.join(ccsDir, `${apiName}.settings.json`); - if (isUnifiedMode()) { - // Check secrets.yaml first - const secrets = getProfileSecrets(apiName); - const secretToken = secrets?.ANTHROPIC_AUTH_TOKEN || ''; - if ( - secretToken.length > 0 && - !secretToken.includes('YOUR_') && - !secretToken.includes('your-') - ) { - return true; - } - - // Fallback: check settings.json file (profiles created via UI store keys here) - if (fs.existsSync(settingsPath)) { - const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); - const token = settings?.env?.ANTHROPIC_AUTH_TOKEN || ''; - return token.length > 0 && !token.includes('YOUR_') && !token.includes('your-'); - } - - return false; - } - - // Legacy: check settings.json file + // Check settings.json file for API key if (!fs.existsSync(settingsPath)) return false; const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); diff --git a/src/api/services/profile-writer.ts b/src/api/services/profile-writer.ts index b316d5d6..748c1fb4 100644 --- a/src/api/services/profile-writer.ts +++ b/src/api/services/profile-writer.ts @@ -11,7 +11,6 @@ import { saveUnifiedConfig, isUnifiedMode, } from '../../config/unified-config-loader'; -import { deleteAllProfileSecrets } from '../../config/secrets-manager'; import type { ModelMapping, CreateApiProfileResult, RemoveApiProfileResult } from './profile-types'; /** Create settings.json file for API profile (legacy format) */ @@ -152,9 +151,6 @@ function removeApiProfileUnified(name: string): void { } saveUnifiedConfig(config); - - // Remove any legacy secrets - deleteAllProfileSecrets(name); } /** Remove API profile from legacy config */ diff --git a/src/auth/profile-detector.ts b/src/auth/profile-detector.ts index ee2ffafd..72955947 100644 --- a/src/auth/profile-detector.ts +++ b/src/auth/profile-detector.ts @@ -16,7 +16,6 @@ import { findSimilarStrings } from '../utils/helpers'; import { Config, Settings, ProfileMetadata } from '../types'; import { UnifiedConfig, CopilotConfig } from '../config/unified-config-types'; import { loadUnifiedConfig, isUnifiedMode } from '../config/unified-config-loader'; -import { getProfileSecrets } from '../config/secrets-manager'; export type ProfileType = 'settings' | 'account' | 'cliproxy' | 'copilot' | 'default'; @@ -110,12 +109,10 @@ class ProfileDetector { const profile = config.profiles[profileName]; // Load env from settings file const settingsEnv = loadSettingsFromFile(profile.settings); - // Merge with secrets (for backward compat with any extracted secrets) - const secrets = getProfileSecrets(profileName); return { type: 'settings', name: profileName, - env: { ...settingsEnv, ...secrets }, + env: settingsEnv, }; } diff --git a/src/commands/api-command.ts b/src/commands/api-command.ts index fee28347..52f423a2 100644 --- a/src/commands/api-command.ts +++ b/src/commands/api-command.ts @@ -384,11 +384,9 @@ async function handleRemove(args: string[]): Promise { // Confirm deletion console.log(''); console.log(`API '${color(name, 'command')}' will be removed.`); + console.log(` Settings: ~/.ccs/${name}.settings.json`); if (isUsingUnifiedConfig()) { console.log(' Config: ~/.ccs/config.yaml'); - console.log(' Secrets: ~/.ccs/secrets.yaml'); - } else { - console.log(` Settings: ~/.ccs/${name}.settings.json`); } console.log(''); diff --git a/src/config/index.ts b/src/config/index.ts index ca503a7c..792133cc 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -15,7 +15,6 @@ export * from './reserved-names'; // Loaders export * from './unified-config-loader'; -export * from './secrets-manager'; // Migration export * from './migration-manager'; diff --git a/src/config/migration-manager.ts b/src/config/migration-manager.ts index bd011c79..142c57e6 100644 --- a/src/config/migration-manager.ts +++ b/src/config/migration-manager.ts @@ -224,11 +224,9 @@ export async function rollback(backupPath: string): Promise { try { // Remove new config files const configYaml = path.join(ccsDir, 'config.yaml'); - const secretsYaml = path.join(ccsDir, 'secrets.yaml'); const cacheDir = path.join(ccsDir, 'cache'); if (fs.existsSync(configYaml)) fs.unlinkSync(configYaml); - if (fs.existsSync(secretsYaml)) fs.unlinkSync(secretsYaml); // Restore cache files to original locations if (fs.existsSync(cacheDir)) { diff --git a/src/config/secrets-manager.ts b/src/config/secrets-manager.ts deleted file mode 100644 index c8c3d26e..00000000 --- a/src/config/secrets-manager.ts +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Secrets Manager - * - * Handles loading and saving secrets (API keys, tokens) in a separate file - * with restricted permissions (chmod 600). - */ - -import * as fs from 'fs'; -import * as path from 'path'; -import * as yaml from 'js-yaml'; -import { getCcsDir } from '../utils/config-manager'; -import { SecretsConfig, isSecretsConfig, createEmptySecretsConfig } from './unified-config-types'; - -// Re-export from shared utility for backward compatibility -export { isSensitiveKey as isSecretKey } from '../utils/sensitive-keys'; - -const SECRETS_FILE = 'secrets.yaml'; -const SECRETS_FILE_MODE = 0o600; // Owner read/write only - -/** - * Get path to secrets.yaml - */ -export function getSecretsPath(): string { - return path.join(getCcsDir(), SECRETS_FILE); -} - -/** - * Check if secrets.yaml exists - */ -export function hasSecrets(): boolean { - return fs.existsSync(getSecretsPath()); -} - -/** - * Load secrets from YAML file. - * Returns empty secrets config if file doesn't exist. - */ -export function loadSecrets(): SecretsConfig { - const secretsPath = getSecretsPath(); - - if (!fs.existsSync(secretsPath)) { - return createEmptySecretsConfig(); - } - - try { - const content = fs.readFileSync(secretsPath, 'utf8'); - const parsed = yaml.load(content); - - if (!isSecretsConfig(parsed)) { - console.error(`[!] Invalid secrets format in ${secretsPath}`); - return createEmptySecretsConfig(); - } - - return parsed; - } catch (err) { - const error = err instanceof Error ? err.message : 'Unknown error'; - console.error(`[X] Failed to load secrets: ${error}`); - return createEmptySecretsConfig(); - } -} - -/** - * Save secrets to YAML file with restricted permissions. - * Uses atomic write (temp file + rename) to prevent corruption. - */ -export function saveSecrets(secrets: SecretsConfig): void { - const secretsPath = getSecretsPath(); - const dir = path.dirname(secretsPath); - - // Ensure directory exists - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); - } - - // Convert to YAML - const content = yaml.dump(secrets, { - indent: 2, - lineWidth: -1, - quotingType: '"', - noRefs: true, - }); - - // Atomic write: write to temp file, then rename - const tempPath = `${secretsPath}.tmp.${process.pid}`; - - try { - fs.writeFileSync(tempPath, content, { mode: SECRETS_FILE_MODE }); - fs.renameSync(tempPath, secretsPath); - - // Ensure correct permissions after rename (some systems may not preserve) - fs.chmodSync(secretsPath, SECRETS_FILE_MODE); - } catch (err) { - // Clean up temp file on error - if (fs.existsSync(tempPath)) { - try { - fs.unlinkSync(tempPath); - } catch { - // Ignore cleanup errors - } - } - throw err; - } -} - -/** - * Get a secret value for a specific profile. - */ -export function getProfileSecret(profileName: string, key: string): string | undefined { - const secrets = loadSecrets(); - return secrets.profiles[profileName]?.[key]; -} - -/** - * Set a secret value for a specific profile. - */ -export function setProfileSecret(profileName: string, key: string, value: string): void { - const secrets = loadSecrets(); - - if (!secrets.profiles[profileName]) { - secrets.profiles[profileName] = {}; - } - - secrets.profiles[profileName][key] = value; - saveSecrets(secrets); -} - -/** - * Delete a secret value for a specific profile. - */ -export function deleteProfileSecret(profileName: string, key: string): boolean { - const secrets = loadSecrets(); - - if (!secrets.profiles[profileName]?.[key]) { - return false; - } - - delete secrets.profiles[profileName][key]; - - // Clean up empty profile object - if (Object.keys(secrets.profiles[profileName]).length === 0) { - delete secrets.profiles[profileName]; - } - - saveSecrets(secrets); - return true; -} - -/** - * Get all secrets for a profile. - */ -export function getProfileSecrets(profileName: string): Record { - const secrets = loadSecrets(); - return secrets.profiles[profileName] || {}; -} - -/** - * Set all secrets for a profile (replaces existing). - */ -export function setProfileSecrets( - profileName: string, - profileSecrets: Record -): void { - const secrets = loadSecrets(); - - if (Object.keys(profileSecrets).length === 0) { - delete secrets.profiles[profileName]; - } else { - secrets.profiles[profileName] = profileSecrets; - } - - saveSecrets(secrets); -} - -/** - * Delete all secrets for a profile. - */ -export function deleteAllProfileSecrets(profileName: string): boolean { - const secrets = loadSecrets(); - - if (!secrets.profiles[profileName]) { - return false; - } - - delete secrets.profiles[profileName]; - saveSecrets(secrets); - return true; -} diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index 271ee7f5..f8548cec 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -6,7 +6,7 @@ * - profiles.json (account metadata) * - *.settings.json (env vars) * - * Into a single config.yaml + secrets.yaml structure. + * Into a single config.yaml structure. */ /** @@ -321,18 +321,6 @@ export interface UnifiedConfig { cliproxy_server?: CliproxyServerConfig; } -/** - * Secrets configuration structure. - * Stored in ~/.ccs/secrets.yaml with chmod 600. - * Contains sensitive values like API keys. - */ -export interface SecretsConfig { - /** Secrets version */ - version: number; - /** Profile secrets mapping: profile_name -> { key: value } */ - profiles: Record>; -} - /** * Default Copilot configuration. * Strictly opt-in - disabled by default. @@ -422,16 +410,6 @@ export function createEmptyUnifiedConfig(): UnifiedConfig { }; } -/** - * Create an empty secrets config. - */ -export function createEmptySecretsConfig(): SecretsConfig { - return { - version: 1, - profiles: {}, - }; -} - /** * Type guard for UnifiedConfig. * Relaxed validation: accepts configs with version >= 1 and any subset of sections. @@ -444,12 +422,3 @@ export function isUnifiedConfig(obj: unknown): obj is UnifiedConfig { // Sections are optional - will be merged with defaults in loadOrCreateUnifiedConfig return typeof config.version === 'number' && config.version >= 1; } - -/** - * Type guard for SecretsConfig. - */ -export function isSecretsConfig(obj: unknown): obj is SecretsConfig { - if (typeof obj !== 'object' || obj === null) return false; - const config = obj as Record; - return typeof config.version === 'number' && typeof config.profiles === 'object'; -} diff --git a/src/utils/config-manager.ts b/src/utils/config-manager.ts index 23504e9a..034a47ec 100644 --- a/src/utils/config-manager.ts +++ b/src/utils/config-manager.ts @@ -4,6 +4,7 @@ import * as os from 'os'; import { Config, isConfig, Settings, isSettings } from '../types'; import { expandPath, error } from './helpers'; import { info } from './ui'; +import { isUnifiedMode, loadOrCreateUnifiedConfig } from '../config/unified-config-loader'; // TODO: Replace with proper imports after converting these files // const { ErrorManager } = require('./error-manager'); @@ -82,16 +83,52 @@ export function readConfig(): Config { } /** - * Get settings path for profile + * Get settings path for profile. + * In unified mode (config.yaml exists), reads from config.yaml first, + * then falls back to config.json for backward compatibility. */ export function getSettingsPath(profile: string): string { - const config = readConfig(); + let settingsPath: string | undefined; + let availableProfiles: string[] = []; - // Get settings path - const settingsPath = config.profiles[profile]; + // Check unified config first (config.yaml) + if (isUnifiedMode()) { + const unifiedConfig = loadOrCreateUnifiedConfig(); + + // Check if profile exists in unified config + const profileConfig = unifiedConfig.profiles[profile]; + if (profileConfig?.settings) { + settingsPath = profileConfig.settings; + } + + // Collect available profiles from unified config + availableProfiles = Object.keys(unifiedConfig.profiles); + + // If not found in unified config, try legacy config.json as fallback + if (!settingsPath) { + try { + const legacyConfig = loadConfig(); + if (legacyConfig.profiles[profile]) { + settingsPath = legacyConfig.profiles[profile]; + // Merge legacy profiles into available list (avoid duplicates) + for (const p of Object.keys(legacyConfig.profiles)) { + if (!availableProfiles.includes(p)) { + availableProfiles.push(p); + } + } + } + } catch { + // Legacy config doesn't exist or is invalid - that's OK in unified mode + } + } + } else { + // Legacy mode - read from config.json only + const config = readConfig(); + settingsPath = config.profiles[profile]; + availableProfiles = Object.keys(config.profiles); + } if (!settingsPath) { - const availableProfiles = Object.keys(config.profiles); const profileList = availableProfiles.map((p) => ` - ${p}`); error(`Profile '${profile}' not found. Available profiles:\n${profileList.join('\n')}`); } diff --git a/src/web-server/routes/config-routes.ts b/src/web-server/routes/config-routes.ts index 03bbc62c..1c1b7f20 100644 --- a/src/web-server/routes/config-routes.ts +++ b/src/web-server/routes/config-routes.ts @@ -17,7 +17,6 @@ import { rollback, getBackupDirectories, } from '../../config/migration-manager'; -import { getProfileSecrets, setProfileSecrets } from '../../config/secrets-manager'; import { isUnifiedConfig } from '../../config/unified-config-types'; const router = Router(); @@ -111,36 +110,4 @@ router.post('/rollback', async (req: Request, res: Response): Promise => { res.json({ success }); }); -/** - * PUT /api/secrets/:profile - Update profile secrets (write-only) - */ -router.put('/secrets/:profile', (req: Request, res: Response): void => { - const { profile } = req.params; - const secrets = req.body; - - if (!secrets || typeof secrets !== 'object') { - res.status(400).json({ error: 'Invalid secrets format' }); - return; - } - - try { - setProfileSecrets(profile, secrets as Record); - res.json({ success: true }); - } catch (err) { - res.status(500).json({ error: (err as Error).message }); - } -}); - -/** - * GET /api/secrets/:profile/exists - Check if secrets exist (no values returned) - */ -router.get('/secrets/:profile/exists', (req: Request, res: Response) => { - const { profile } = req.params; - const secrets = getProfileSecrets(profile); - res.json({ - exists: Object.keys(secrets).length > 0, - keys: Object.keys(secrets), // Only key names, not values - }); -}); - export default router; diff --git a/src/web-server/routes/index.ts b/src/web-server/routes/index.ts index 7b3f7266..819cb717 100644 --- a/src/web-server/routes/index.ts +++ b/src/web-server/routes/index.ts @@ -31,9 +31,8 @@ apiRoutes.use('/settings', settingsRoutes); apiRoutes.use('/accounts', profileRoutes); // ==================== Unified Config ==================== -// Config format, migration, secrets +// Config format, migration apiRoutes.use('/config', configRoutes); -apiRoutes.use('/secrets', configRoutes); // ==================== Health Checks ==================== apiRoutes.use('/health', healthRoutes); diff --git a/tests/unit/unified-config.test.ts b/tests/unit/unified-config.test.ts index b345bd34..eaeb30dc 100644 --- a/tests/unit/unified-config.test.ts +++ b/tests/unit/unified-config.test.ts @@ -11,14 +11,12 @@ import { } from '../../src/config/reserved-names'; import { createEmptyUnifiedConfig, - createEmptySecretsConfig, isUnifiedConfig, - isSecretsConfig, UNIFIED_CONFIG_VERSION, } from '../../src/config/unified-config-types'; import { isUnifiedConfigEnabled } from '../../src/config/feature-flags'; -// Inline helper to test secret key detection (copied from secrets-manager to avoid import chain) +// Inline helper to test secret key detection (utility kept for potential reuse) function isSecretKey(key: string): boolean { const upper = key.toUpperCase(); const secretPatterns = ['TOKEN', 'SECRET', 'API_KEY', 'APIKEY', 'PASSWORD', 'CREDENTIAL', 'AUTH', 'PRIVATE']; @@ -102,18 +100,6 @@ describe('unified-config-types', () => { }); }); - describe('createEmptySecretsConfig', () => { - it('should create secrets with version 1', () => { - const secrets = createEmptySecretsConfig(); - expect(secrets.version).toBe(1); - }); - - it('should have empty profiles', () => { - const secrets = createEmptySecretsConfig(); - expect(Object.keys(secrets.profiles)).toHaveLength(0); - }); - }); - describe('isUnifiedConfig', () => { it('should return true for valid config', () => { const config = createEmptyUnifiedConfig(); @@ -143,24 +129,9 @@ describe('unified-config-types', () => { expect(isUnifiedConfig({ version: -1 })).toBe(false); }); }); - - describe('isSecretsConfig', () => { - it('should return true for valid secrets', () => { - const secrets = createEmptySecretsConfig(); - expect(isSecretsConfig(secrets)).toBe(true); - }); - - it('should return false for null', () => { - expect(isSecretsConfig(null)).toBe(false); - }); - - it('should return false for missing fields', () => { - expect(isSecretsConfig({ version: 1 })).toBe(false); - }); - }); }); -describe('secrets-manager', () => { +describe('sensitive-keys', () => { describe('isSecretKey', () => { it('should identify token keys as secrets', () => { expect(isSecretKey('ANTHROPIC_AUTH_TOKEN')).toBe(true); diff --git a/ui/src/hooks/use-unified-config.ts b/ui/src/hooks/use-unified-config.ts index 0e27ce00..e0d1d021 100644 --- a/ui/src/hooks/use-unified-config.ts +++ b/ui/src/hooks/use-unified-config.ts @@ -98,30 +98,3 @@ export function useRollback() { }, }); } - -/** - * Update profile secrets - */ -export function useUpdateSecrets() { - return useMutation({ - mutationFn: ({ profile, secrets }: { profile: string; secrets: Record }) => - api.secrets.update(profile, secrets), - onSuccess: () => { - toast.success('Secrets updated successfully'); - }, - onError: (error: Error) => { - toast.error(error.message); - }, - }); -} - -/** - * Check if profile has secrets (doesn't return values) - */ -export function useSecretsExists(profile: string) { - return useQuery({ - queryKey: ['secrets-exists', profile], - queryFn: () => api.secrets.exists(profile), - enabled: !!profile, - }); -} diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index 5a4e8881..36254505 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -132,11 +132,6 @@ export interface MigrationResult { warnings: string[]; } -export interface SecretsExists { - exists: boolean; - keys: string[]; -} - /** Model preset for quick model switching */ export interface ModelPreset { name: string; @@ -369,14 +364,6 @@ export const api = { body: JSON.stringify({ backupPath }), }), }, - secrets: { - update: (profile: string, secrets: Record) => - request<{ success: boolean }>(`/secrets/${profile}`, { - method: 'PUT', - body: JSON.stringify(secrets), - }), - exists: (profile: string) => request(`/secrets/${profile}/exists`), - }, /** Model presets for quick model switching */ presets: { list: (profile: string) => request<{ presets: ModelPreset[] }>(`/settings/${profile}/presets`),