mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-04 08:19:16 +00:00
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
This commit is contained in:
@@ -9,7 +9,6 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { getCcsDir, loadConfig } from '../../utils/config-manager';
|
import { getCcsDir, loadConfig } from '../../utils/config-manager';
|
||||||
import { loadOrCreateUnifiedConfig, isUnifiedMode } from '../../config/unified-config-loader';
|
import { loadOrCreateUnifiedConfig, isUnifiedMode } from '../../config/unified-config-loader';
|
||||||
import { getProfileSecrets } from '../../config/secrets-manager';
|
|
||||||
import type { ApiProfileInfo, CliproxyVariantInfo, ApiListResult } from './profile-types';
|
import type { ApiProfileInfo, CliproxyVariantInfo, ApiListResult } from './profile-types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,29 +35,7 @@ export function isApiProfileConfigured(apiName: string): boolean {
|
|||||||
const ccsDir = getCcsDir();
|
const ccsDir = getCcsDir();
|
||||||
const settingsPath = path.join(ccsDir, `${apiName}.settings.json`);
|
const settingsPath = path.join(ccsDir, `${apiName}.settings.json`);
|
||||||
|
|
||||||
if (isUnifiedMode()) {
|
// Check settings.json file for API key
|
||||||
// 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
|
|
||||||
if (!fs.existsSync(settingsPath)) return false;
|
if (!fs.existsSync(settingsPath)) return false;
|
||||||
|
|
||||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
saveUnifiedConfig,
|
saveUnifiedConfig,
|
||||||
isUnifiedMode,
|
isUnifiedMode,
|
||||||
} from '../../config/unified-config-loader';
|
} from '../../config/unified-config-loader';
|
||||||
import { deleteAllProfileSecrets } from '../../config/secrets-manager';
|
|
||||||
import type { ModelMapping, CreateApiProfileResult, RemoveApiProfileResult } from './profile-types';
|
import type { ModelMapping, CreateApiProfileResult, RemoveApiProfileResult } from './profile-types';
|
||||||
|
|
||||||
/** Create settings.json file for API profile (legacy format) */
|
/** Create settings.json file for API profile (legacy format) */
|
||||||
@@ -152,9 +151,6 @@ function removeApiProfileUnified(name: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveUnifiedConfig(config);
|
saveUnifiedConfig(config);
|
||||||
|
|
||||||
// Remove any legacy secrets
|
|
||||||
deleteAllProfileSecrets(name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Remove API profile from legacy config */
|
/** Remove API profile from legacy config */
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { findSimilarStrings } from '../utils/helpers';
|
|||||||
import { Config, Settings, ProfileMetadata } from '../types';
|
import { Config, Settings, ProfileMetadata } from '../types';
|
||||||
import { UnifiedConfig, CopilotConfig } from '../config/unified-config-types';
|
import { UnifiedConfig, CopilotConfig } from '../config/unified-config-types';
|
||||||
import { loadUnifiedConfig, isUnifiedMode } from '../config/unified-config-loader';
|
import { loadUnifiedConfig, isUnifiedMode } from '../config/unified-config-loader';
|
||||||
import { getProfileSecrets } from '../config/secrets-manager';
|
|
||||||
|
|
||||||
export type ProfileType = 'settings' | 'account' | 'cliproxy' | 'copilot' | 'default';
|
export type ProfileType = 'settings' | 'account' | 'cliproxy' | 'copilot' | 'default';
|
||||||
|
|
||||||
@@ -110,12 +109,10 @@ class ProfileDetector {
|
|||||||
const profile = config.profiles[profileName];
|
const profile = config.profiles[profileName];
|
||||||
// Load env from settings file
|
// Load env from settings file
|
||||||
const settingsEnv = loadSettingsFromFile(profile.settings);
|
const settingsEnv = loadSettingsFromFile(profile.settings);
|
||||||
// Merge with secrets (for backward compat with any extracted secrets)
|
|
||||||
const secrets = getProfileSecrets(profileName);
|
|
||||||
return {
|
return {
|
||||||
type: 'settings',
|
type: 'settings',
|
||||||
name: profileName,
|
name: profileName,
|
||||||
env: { ...settingsEnv, ...secrets },
|
env: settingsEnv,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -384,11 +384,9 @@ async function handleRemove(args: string[]): Promise<void> {
|
|||||||
// Confirm deletion
|
// Confirm deletion
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(`API '${color(name, 'command')}' will be removed.`);
|
console.log(`API '${color(name, 'command')}' will be removed.`);
|
||||||
|
console.log(` Settings: ~/.ccs/${name}.settings.json`);
|
||||||
if (isUsingUnifiedConfig()) {
|
if (isUsingUnifiedConfig()) {
|
||||||
console.log(' Config: ~/.ccs/config.yaml');
|
console.log(' Config: ~/.ccs/config.yaml');
|
||||||
console.log(' Secrets: ~/.ccs/secrets.yaml');
|
|
||||||
} else {
|
|
||||||
console.log(` Settings: ~/.ccs/${name}.settings.json`);
|
|
||||||
}
|
}
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export * from './reserved-names';
|
|||||||
|
|
||||||
// Loaders
|
// Loaders
|
||||||
export * from './unified-config-loader';
|
export * from './unified-config-loader';
|
||||||
export * from './secrets-manager';
|
|
||||||
|
|
||||||
// Migration
|
// Migration
|
||||||
export * from './migration-manager';
|
export * from './migration-manager';
|
||||||
|
|||||||
@@ -224,11 +224,9 @@ export async function rollback(backupPath: string): Promise<boolean> {
|
|||||||
try {
|
try {
|
||||||
// Remove new config files
|
// Remove new config files
|
||||||
const configYaml = path.join(ccsDir, 'config.yaml');
|
const configYaml = path.join(ccsDir, 'config.yaml');
|
||||||
const secretsYaml = path.join(ccsDir, 'secrets.yaml');
|
|
||||||
const cacheDir = path.join(ccsDir, 'cache');
|
const cacheDir = path.join(ccsDir, 'cache');
|
||||||
|
|
||||||
if (fs.existsSync(configYaml)) fs.unlinkSync(configYaml);
|
if (fs.existsSync(configYaml)) fs.unlinkSync(configYaml);
|
||||||
if (fs.existsSync(secretsYaml)) fs.unlinkSync(secretsYaml);
|
|
||||||
|
|
||||||
// Restore cache files to original locations
|
// Restore cache files to original locations
|
||||||
if (fs.existsSync(cacheDir)) {
|
if (fs.existsSync(cacheDir)) {
|
||||||
|
|||||||
@@ -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<string, string> {
|
|
||||||
const secrets = loadSecrets();
|
|
||||||
return secrets.profiles[profileName] || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set all secrets for a profile (replaces existing).
|
|
||||||
*/
|
|
||||||
export function setProfileSecrets(
|
|
||||||
profileName: string,
|
|
||||||
profileSecrets: Record<string, string>
|
|
||||||
): 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;
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
* - profiles.json (account metadata)
|
* - profiles.json (account metadata)
|
||||||
* - *.settings.json (env vars)
|
* - *.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;
|
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<string, Record<string, string>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default Copilot configuration.
|
* Default Copilot configuration.
|
||||||
* Strictly opt-in - disabled by default.
|
* 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.
|
* Type guard for UnifiedConfig.
|
||||||
* Relaxed validation: accepts configs with version >= 1 and any subset of sections.
|
* 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
|
// Sections are optional - will be merged with defaults in loadOrCreateUnifiedConfig
|
||||||
return typeof config.version === 'number' && config.version >= 1;
|
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<string, unknown>;
|
|
||||||
return typeof config.version === 'number' && typeof config.profiles === 'object';
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import * as os from 'os';
|
|||||||
import { Config, isConfig, Settings, isSettings } from '../types';
|
import { Config, isConfig, Settings, isSettings } from '../types';
|
||||||
import { expandPath, error } from './helpers';
|
import { expandPath, error } from './helpers';
|
||||||
import { info } from './ui';
|
import { info } from './ui';
|
||||||
|
import { isUnifiedMode, loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
|
||||||
|
|
||||||
// TODO: Replace with proper imports after converting these files
|
// TODO: Replace with proper imports after converting these files
|
||||||
// const { ErrorManager } = require('./error-manager');
|
// 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 {
|
export function getSettingsPath(profile: string): string {
|
||||||
const config = readConfig();
|
let settingsPath: string | undefined;
|
||||||
|
let availableProfiles: string[] = [];
|
||||||
|
|
||||||
// Get settings path
|
// Check unified config first (config.yaml)
|
||||||
const settingsPath = config.profiles[profile];
|
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) {
|
if (!settingsPath) {
|
||||||
const availableProfiles = Object.keys(config.profiles);
|
|
||||||
const profileList = availableProfiles.map((p) => ` - ${p}`);
|
const profileList = availableProfiles.map((p) => ` - ${p}`);
|
||||||
error(`Profile '${profile}' not found. Available profiles:\n${profileList.join('\n')}`);
|
error(`Profile '${profile}' not found. Available profiles:\n${profileList.join('\n')}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
rollback,
|
rollback,
|
||||||
getBackupDirectories,
|
getBackupDirectories,
|
||||||
} from '../../config/migration-manager';
|
} from '../../config/migration-manager';
|
||||||
import { getProfileSecrets, setProfileSecrets } from '../../config/secrets-manager';
|
|
||||||
import { isUnifiedConfig } from '../../config/unified-config-types';
|
import { isUnifiedConfig } from '../../config/unified-config-types';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@@ -111,36 +110,4 @@ router.post('/rollback', async (req: Request, res: Response): Promise<void> => {
|
|||||||
res.json({ success });
|
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<string, string>);
|
|
||||||
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;
|
export default router;
|
||||||
|
|||||||
@@ -31,9 +31,8 @@ apiRoutes.use('/settings', settingsRoutes);
|
|||||||
apiRoutes.use('/accounts', profileRoutes);
|
apiRoutes.use('/accounts', profileRoutes);
|
||||||
|
|
||||||
// ==================== Unified Config ====================
|
// ==================== Unified Config ====================
|
||||||
// Config format, migration, secrets
|
// Config format, migration
|
||||||
apiRoutes.use('/config', configRoutes);
|
apiRoutes.use('/config', configRoutes);
|
||||||
apiRoutes.use('/secrets', configRoutes);
|
|
||||||
|
|
||||||
// ==================== Health Checks ====================
|
// ==================== Health Checks ====================
|
||||||
apiRoutes.use('/health', healthRoutes);
|
apiRoutes.use('/health', healthRoutes);
|
||||||
|
|||||||
@@ -11,14 +11,12 @@ import {
|
|||||||
} from '../../src/config/reserved-names';
|
} from '../../src/config/reserved-names';
|
||||||
import {
|
import {
|
||||||
createEmptyUnifiedConfig,
|
createEmptyUnifiedConfig,
|
||||||
createEmptySecretsConfig,
|
|
||||||
isUnifiedConfig,
|
isUnifiedConfig,
|
||||||
isSecretsConfig,
|
|
||||||
UNIFIED_CONFIG_VERSION,
|
UNIFIED_CONFIG_VERSION,
|
||||||
} from '../../src/config/unified-config-types';
|
} from '../../src/config/unified-config-types';
|
||||||
import { isUnifiedConfigEnabled } from '../../src/config/feature-flags';
|
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 {
|
function isSecretKey(key: string): boolean {
|
||||||
const upper = key.toUpperCase();
|
const upper = key.toUpperCase();
|
||||||
const secretPatterns = ['TOKEN', 'SECRET', 'API_KEY', 'APIKEY', 'PASSWORD', 'CREDENTIAL', 'AUTH', 'PRIVATE'];
|
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', () => {
|
describe('isUnifiedConfig', () => {
|
||||||
it('should return true for valid config', () => {
|
it('should return true for valid config', () => {
|
||||||
const config = createEmptyUnifiedConfig();
|
const config = createEmptyUnifiedConfig();
|
||||||
@@ -143,24 +129,9 @@ describe('unified-config-types', () => {
|
|||||||
expect(isUnifiedConfig({ version: -1 })).toBe(false);
|
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', () => {
|
describe('isSecretKey', () => {
|
||||||
it('should identify token keys as secrets', () => {
|
it('should identify token keys as secrets', () => {
|
||||||
expect(isSecretKey('ANTHROPIC_AUTH_TOKEN')).toBe(true);
|
expect(isSecretKey('ANTHROPIC_AUTH_TOKEN')).toBe(true);
|
||||||
|
|||||||
@@ -98,30 +98,3 @@ export function useRollback() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update profile secrets
|
|
||||||
*/
|
|
||||||
export function useUpdateSecrets() {
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: ({ profile, secrets }: { profile: string; secrets: Record<string, string> }) =>
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -132,11 +132,6 @@ export interface MigrationResult {
|
|||||||
warnings: string[];
|
warnings: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SecretsExists {
|
|
||||||
exists: boolean;
|
|
||||||
keys: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Model preset for quick model switching */
|
/** Model preset for quick model switching */
|
||||||
export interface ModelPreset {
|
export interface ModelPreset {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -369,14 +364,6 @@ export const api = {
|
|||||||
body: JSON.stringify({ backupPath }),
|
body: JSON.stringify({ backupPath }),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
secrets: {
|
|
||||||
update: (profile: string, secrets: Record<string, string>) =>
|
|
||||||
request<{ success: boolean }>(`/secrets/${profile}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(secrets),
|
|
||||||
}),
|
|
||||||
exists: (profile: string) => request<SecretsExists>(`/secrets/${profile}/exists`),
|
|
||||||
},
|
|
||||||
/** Model presets for quick model switching */
|
/** Model presets for quick model switching */
|
||||||
presets: {
|
presets: {
|
||||||
list: (profile: string) => request<{ presets: ModelPreset[] }>(`/settings/${profile}/presets`),
|
list: (profile: string) => request<{ presets: ModelPreset[] }>(`/settings/${profile}/presets`),
|
||||||
|
|||||||
Reference in New Issue
Block a user