mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 20:20:09 +00:00
fix(codex-auth): surface registry corruption
This commit is contained in:
@@ -17,12 +17,13 @@
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { createLogger } from '../services/logging';
|
||||
import { decodeAccountIdentity } from './codex-account-identity';
|
||||
import { hasStructurallyValidIdToken } from './decode-id-token';
|
||||
import { getCodexAuthRegistryPath, getCodexInstancesDir } from './codex-profile-paths';
|
||||
import type { CodexProfileData } from './types';
|
||||
import { CODEX_PROFILE_SCHEMA_VERSION } from './types';
|
||||
import { CodexProfileRegistry } from './codex-profile-registry';
|
||||
import type { CodexProfileData, CodexProfileMetadata } from './types';
|
||||
|
||||
const logger = createLogger('codex-auth:dashboard');
|
||||
|
||||
@@ -69,19 +70,24 @@ export function invalidateCodexAuthProfilesCache(): void {
|
||||
function readRegistry(): CodexProfileData {
|
||||
const registryPath = getCodexAuthRegistryPath();
|
||||
if (!fs.existsSync(registryPath)) {
|
||||
return { version: '1.0', default: null, profiles: {} };
|
||||
return { version: CODEX_PROFILE_SCHEMA_VERSION, default: null, profiles: {} };
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(registryPath, 'utf8');
|
||||
const parsed = yaml.load(raw) as CodexProfileData | null;
|
||||
if (!parsed || typeof parsed !== 'object' || !parsed.profiles) {
|
||||
return { version: '1.0', default: null, profiles: {} };
|
||||
const registry = new CodexProfileRegistry(registryPath);
|
||||
const profiles: Record<string, CodexProfileMetadata> = {};
|
||||
for (const name of registry.listProfiles()) {
|
||||
profiles[name] = registry.getProfile(name);
|
||||
}
|
||||
return parsed;
|
||||
return {
|
||||
version: CODEX_PROFILE_SCHEMA_VERSION,
|
||||
default: registry.getDefault(),
|
||||
profiles,
|
||||
};
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logger.warn('codex-auth.dashboard.registry-read-failed', `Registry read failed: ${msg}`);
|
||||
return { version: '1.0', default: null, profiles: {} };
|
||||
throw new Error(`Codex auth profile registry could not be read safely: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -318,8 +318,7 @@ export class CodexProfileRegistry {
|
||||
}
|
||||
delete data.profiles[name];
|
||||
if (data.default === name) {
|
||||
const remaining = Object.keys(data.profiles);
|
||||
data.default = remaining.length > 0 ? remaining[0] : null;
|
||||
data.default = null;
|
||||
}
|
||||
this._write(data);
|
||||
logger.stage('cleanup', 'codex-auth.profile.deleted', 'Codex profile removed', { name });
|
||||
|
||||
@@ -110,6 +110,19 @@ describe('GET /api/codex/profiles', () => {
|
||||
expect((b.profiles as unknown[]).length).toBe(0);
|
||||
});
|
||||
|
||||
it('returns 500 when registry YAML is malformed', async () => {
|
||||
const registryPath = path.join(ccsDir, 'codex-profiles.yaml');
|
||||
fs.writeFileSync(registryPath, '{ invalid: yaml: [', { mode: 0o600 });
|
||||
|
||||
const svc = await import('../../../src/codex-auth/codex-auth-dashboard-service');
|
||||
svc.invalidateCodexAuthProfilesCache();
|
||||
|
||||
const { status, body } = await get('/api/codex/profiles');
|
||||
|
||||
expect(status).toBe(500);
|
||||
expect((body as { error?: string }).error).toContain('could not be read safely');
|
||||
});
|
||||
|
||||
it('returns 200 with decoded email and plan for a valid profile', async () => {
|
||||
const instancesDir = path.join(ccsDir, 'codex-instances');
|
||||
const workDir = path.join(instancesDir, 'work');
|
||||
|
||||
@@ -115,6 +115,16 @@ describe('getCodexAuthProfilesSummary', () => {
|
||||
expect(result.default).toBeNull();
|
||||
});
|
||||
|
||||
it('throws instead of returning an empty list when registry YAML is malformed', async () => {
|
||||
const { getCodexAuthProfilesSummary, invalidateCodexAuthProfilesCache } = await importService();
|
||||
invalidateCodexAuthProfilesCache();
|
||||
|
||||
const registryPath = path.join(ccsDir, 'codex-profiles.yaml');
|
||||
fs.writeFileSync(registryPath, '{ invalid: yaml: [', { mode: 0o600 });
|
||||
|
||||
await expect(getCodexAuthProfilesSummary()).rejects.toThrow(/could not be read safely/i);
|
||||
});
|
||||
|
||||
it('returns decoded email, plan, accountId for valid registry with 2 profiles', async () => {
|
||||
const { getCodexAuthProfilesSummary, invalidateCodexAuthProfilesCache } = await importService();
|
||||
invalidateCodexAuthProfilesCache();
|
||||
|
||||
@@ -127,6 +127,18 @@ describe('CodexProfileRegistry — remove', () => {
|
||||
reg.removeProfile('work');
|
||||
expect(reg.getDefault()).toBeNull();
|
||||
});
|
||||
|
||||
it('does not promote another profile when the default profile is removed', () => {
|
||||
const reg = new CodexProfileRegistry(registryPath);
|
||||
reg.createProfile('work');
|
||||
reg.createProfile('personal');
|
||||
reg.setDefault('work');
|
||||
|
||||
reg.removeProfile('work');
|
||||
|
||||
expect(reg.listProfiles()).toEqual(['personal']);
|
||||
expect(reg.getDefault()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CodexProfileRegistry — default pointer', () => {
|
||||
|
||||
Reference in New Issue
Block a user