fix(cursor): guard raw-settings save race and enforce daemon preconditions

This commit is contained in:
Tam Nhu Tran
2026-02-14 07:11:02 +07:00
parent 0e55db88a5
commit 9a76f866b0
5 changed files with 157 additions and 3 deletions
+53
View File
@@ -20,6 +20,44 @@ import cursorSettingsRoutes from './cursor-settings-routes';
const router = Router();
interface DaemonStartPreconditionInput {
enabled: boolean;
authenticated: boolean;
tokenExpired?: boolean;
}
interface DaemonStartPreconditionError {
status: number;
error: string;
}
export function getDaemonStartPreconditionError(
input: DaemonStartPreconditionInput
): DaemonStartPreconditionError | null {
if (!input.enabled) {
return {
status: 400,
error: 'Cursor integration is disabled. Enable it before starting daemon.',
};
}
if (!input.authenticated) {
return {
status: 401,
error: 'Cursor authentication required. Import credentials before starting daemon.',
};
}
if (input.tokenExpired) {
return {
status: 401,
error: 'Cursor credentials expired. Re-authenticate before starting daemon.',
};
}
return null;
}
// Mount settings sub-routes
router.use('/settings', cursorSettingsRoutes);
@@ -125,6 +163,21 @@ router.get('/models', async (_req: Request, res: Response): Promise<void> => {
router.post('/daemon/start', async (_req: Request, res: Response): Promise<void> => {
try {
const cursorConfig = getCursorConfig();
const authStatus = checkAuthStatus();
const preconditionError = getDaemonStartPreconditionError({
enabled: cursorConfig.enabled,
authenticated: authStatus.authenticated,
tokenExpired: authStatus.expired ?? false,
});
if (preconditionError) {
res.status(preconditionError.status).json({
success: false,
error: preconditionError.error,
});
return;
}
const result = await startDaemon({
port: cursorConfig.port,
ghost_mode: cursorConfig.ghost_mode,
@@ -162,9 +162,17 @@ router.put('/raw', (req: Request, res: Response): void => {
const settingsPath = path.join(getCcsDir(), 'cursor.settings.json');
// Check for conflict if file exists and expectedMtime provided
if (fs.existsSync(settingsPath) && expectedMtime) {
// For existing files, expectedMtime is required to prevent blind overwrite races.
if (fs.existsSync(settingsPath)) {
const stat = fs.statSync(settingsPath);
if (typeof expectedMtime !== 'number' || !Number.isFinite(expectedMtime)) {
res.status(409).json({
error: 'File metadata not loaded. Refresh and retry.',
mtime: stat.mtimeMs,
});
return;
}
if (Math.abs(stat.mtimeMs - expectedMtime) > 1000) {
res.status(409).json({ error: 'File modified externally', mtime: stat.mtimeMs });
return;
@@ -0,0 +1,60 @@
/**
* Cursor Routes Tests
* Tests for daemon start precondition validation logic.
*/
import { describe, it, expect } from 'bun:test';
import { getDaemonStartPreconditionError } from '../../../src/web-server/routes/cursor-routes';
describe('Cursor Routes Logic', () => {
describe('POST /daemon/start preconditions', () => {
it('blocks start when integration is disabled', () => {
const result = getDaemonStartPreconditionError({
enabled: false,
authenticated: true,
tokenExpired: false,
});
expect(result).toEqual({
status: 400,
error: 'Cursor integration is disabled. Enable it before starting daemon.',
});
});
it('blocks start when not authenticated', () => {
const result = getDaemonStartPreconditionError({
enabled: true,
authenticated: false,
tokenExpired: false,
});
expect(result).toEqual({
status: 401,
error: 'Cursor authentication required. Import credentials before starting daemon.',
});
});
it('blocks start when token is expired', () => {
const result = getDaemonStartPreconditionError({
enabled: true,
authenticated: true,
tokenExpired: true,
});
expect(result).toEqual({
status: 401,
error: 'Cursor credentials expired. Re-authenticate before starting daemon.',
});
});
it('allows start when all preconditions are met', () => {
const result = getDaemonStartPreconditionError({
enabled: true,
authenticated: true,
tokenExpired: false,
});
expect(result).toBeNull();
});
});
});
@@ -284,6 +284,32 @@ describe('Cursor Settings Routes Logic', () => {
expect(hasConflict).toBe(true);
});
it('requires expectedMtime when file already exists', () => {
const settingsPath = path.join(getCcsDir(), 'cursor.settings.json');
fs.writeFileSync(settingsPath, JSON.stringify({ env: { test: 'existing' } }));
const expectedMtime = undefined;
const shouldRequireMtime =
fs.existsSync(settingsPath) &&
(typeof expectedMtime !== 'number' || !Number.isFinite(expectedMtime));
expect(shouldRequireMtime).toBe(true);
});
it('does not require expectedMtime when file does not exist', () => {
const settingsPath = path.join(getCcsDir(), 'cursor.settings.json');
if (fs.existsSync(settingsPath)) {
fs.rmSync(settingsPath, { force: true });
}
const expectedMtime = undefined;
const shouldRequireMtime =
fs.existsSync(settingsPath) &&
(typeof expectedMtime !== 'number' || !Number.isFinite(expectedMtime));
expect(shouldRequireMtime).toBe(false);
});
it('allows write when mtime matches', () => {
const settingsPath = path.join(getCcsDir(), 'cursor.settings.json');
const initialSettings = { env: { test: 'initial' } };
+8 -1
View File
@@ -134,6 +134,8 @@ export function CursorPage() {
const effectiveRawConfigText = rawConfigDirty
? rawConfigText
: JSON.stringify(rawSettings?.settings ?? {}, null, 2);
const rawSettingsReady = Boolean(rawSettings);
const disableRawSave = isSavingRawSettings || rawSettingsLoading || !rawSettingsReady;
const updateConfigDraft = (updater: (draft: CursorConfigDraft) => CursorConfigDraft) => {
setConfigDraft((previousDraft) => {
@@ -241,6 +243,11 @@ export function CursorPage() {
};
const handleSaveRawSettings = async () => {
if (!rawSettingsReady) {
toast.error('Raw settings are still loading. Please wait and try again.');
return;
}
let parsed: unknown;
try {
parsed = JSON.parse(effectiveRawConfigText || '{}');
@@ -570,7 +577,7 @@ export function CursorPage() {
/>
Refresh
</Button>
<Button onClick={handleSaveRawSettings} disabled={isSavingRawSettings}>
<Button onClick={handleSaveRawSettings} disabled={disableRawSave}>
{isSavingRawSettings ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (