fix(codex-auth): surface registry corruption

This commit is contained in:
Tam Nhu Tran
2026-05-17 18:02:13 -04:00
parent c90b3cb9da
commit 54738e88f7
5 changed files with 51 additions and 11 deletions
+15 -9
View File
@@ -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}`);
}
}
+1 -2
View File
@@ -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', () => {