From 54738e88f78bc3a38bc985daad8a723276347693 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 17 May 2026 18:02:13 -0400 Subject: [PATCH] fix(codex-auth): surface registry corruption --- .../codex-auth-dashboard-service.ts | 24 ++++++++++++------- src/codex-auth/codex-profile-registry.ts | 3 +-- .../codex-profiles-endpoint.test.ts | 13 ++++++++++ .../codex-auth-dashboard-service.test.ts | 10 ++++++++ .../codex-auth/codex-profile-registry.test.ts | 12 ++++++++++ 5 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/codex-auth/codex-auth-dashboard-service.ts b/src/codex-auth/codex-auth-dashboard-service.ts index 699c973c..79accaa8 100644 --- a/src/codex-auth/codex-auth-dashboard-service.ts +++ b/src/codex-auth/codex-auth-dashboard-service.ts @@ -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 = {}; + 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}`); } } diff --git a/src/codex-auth/codex-profile-registry.ts b/src/codex-auth/codex-profile-registry.ts index e9a6d617..4e90bba6 100644 --- a/src/codex-auth/codex-profile-registry.ts +++ b/src/codex-auth/codex-profile-registry.ts @@ -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 }); diff --git a/tests/integration/web-server/codex-profiles-endpoint.test.ts b/tests/integration/web-server/codex-profiles-endpoint.test.ts index 966a919b..3651595d 100644 --- a/tests/integration/web-server/codex-profiles-endpoint.test.ts +++ b/tests/integration/web-server/codex-profiles-endpoint.test.ts @@ -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'); diff --git a/tests/unit/codex-auth/codex-auth-dashboard-service.test.ts b/tests/unit/codex-auth/codex-auth-dashboard-service.test.ts index 110b47f7..87857c93 100644 --- a/tests/unit/codex-auth/codex-auth-dashboard-service.test.ts +++ b/tests/unit/codex-auth/codex-auth-dashboard-service.test.ts @@ -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(); diff --git a/tests/unit/codex-auth/codex-profile-registry.test.ts b/tests/unit/codex-auth/codex-profile-registry.test.ts index 03b22517..7157f951 100644 --- a/tests/unit/codex-auth/codex-profile-registry.test.ts +++ b/tests/unit/codex-auth/codex-profile-registry.test.ts @@ -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', () => {