mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
fix(cliproxy): harden sync against edge cases
Backend: - Handle config file deletion race condition with try-catch - Add null check for empty config files in yaml.load() - Limit profile names to 64 chars, require alphanumeric content - Add 5s timeout for watcher close() to prevent hangs Frontend: - Add 30s fetch timeout with AbortController for sync requests
This commit is contained in:
@@ -149,7 +149,15 @@ export async function stopAutoSyncWatcher(): Promise<void> {
|
||||
}
|
||||
|
||||
if (watcherInstance) {
|
||||
await watcherInstance.close();
|
||||
const closePromise = watcherInstance.close();
|
||||
const timeoutPromise = new Promise<void>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Close timeout')), 5000)
|
||||
);
|
||||
try {
|
||||
await Promise.race([closePromise, timeoutPromise]);
|
||||
} catch (err) {
|
||||
log(`Warning: ${(err as Error).message}, forcing cleanup`);
|
||||
}
|
||||
watcherInstance = null;
|
||||
log('Watcher stopped');
|
||||
}
|
||||
|
||||
@@ -47,7 +47,20 @@ export function syncToLocalConfig(): {
|
||||
};
|
||||
}
|
||||
|
||||
const configContent = fs.readFileSync(configPath, 'utf8');
|
||||
let configContent: string;
|
||||
try {
|
||||
configContent = fs.readFileSync(configPath, 'utf8');
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return {
|
||||
success: false,
|
||||
syncedCount: 0,
|
||||
configPath,
|
||||
error: 'CLIProxy config deleted during sync. Run ccs doctor to regenerate.',
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Transform payload to config format
|
||||
const claudeApiKeys = payload.map(transformToConfigFormat);
|
||||
@@ -198,10 +211,12 @@ export function getLocalSyncStatus(): {
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(configPath, 'utf8');
|
||||
const config = yaml.load(content) as Record<string, unknown>;
|
||||
const keys = config['claude-api-key'];
|
||||
if (Array.isArray(keys)) {
|
||||
currentKeyCount = keys.length;
|
||||
const config = yaml.load(content) as Record<string, unknown> | null;
|
||||
if (config && typeof config === 'object') {
|
||||
const keys = config['claude-api-key'];
|
||||
if (Array.isArray(keys)) {
|
||||
currentKeyCount = keys.length;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
|
||||
@@ -104,6 +104,17 @@ export function mapProfileToClaudeKey(profile: SyncableProfile): ClaudeKey | nul
|
||||
if (!sanitizedName || sanitizedName === '') {
|
||||
return null; // Skip profiles with invalid names
|
||||
}
|
||||
|
||||
// Skip if name is too long (>64 chars)
|
||||
if (sanitizedName.length > 64) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Skip if name has no alphanumeric characters (e.g., only special chars -> "-----")
|
||||
if (!/[a-zA-Z0-9]/.test(sanitizedName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const prefix = `${sanitizedName}-`;
|
||||
|
||||
const claudeKey: ClaudeKey = {
|
||||
|
||||
@@ -89,23 +89,36 @@ async function fetchSyncPreview(): Promise<SyncPreview> {
|
||||
* Execute sync to remote CLIProxy
|
||||
*/
|
||||
async function executeSync(): Promise<SyncResult> {
|
||||
const response = await fetch('/api/cliproxy/sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
||||
|
||||
if (!response.ok) {
|
||||
let message = 'Sync failed';
|
||||
try {
|
||||
const data = await response.json();
|
||||
message = data.error || data.message || message;
|
||||
} catch {
|
||||
// Non-JSON response (e.g., 502 Bad Gateway)
|
||||
try {
|
||||
const response = await fetch('/api/cliproxy/sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let message = 'Sync failed';
|
||||
try {
|
||||
const data = await response.json();
|
||||
message = data.error || data.message || message;
|
||||
} catch {
|
||||
// Non-JSON response (e.g., 502 Bad Gateway)
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw new Error('Sync request timed out after 30 seconds');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user