feat(codex-auth): add profile registry + storage foundation

Adds the storage substrate for ccsx auth profile isolation.
Each Codex profile gets its own CODEX_HOME dir under
~/.ccs/codex-instances/<name>/ with isolated auth.json and
history.jsonl; config.toml is shared via symlink to ~/.codex/config.toml
so two terminals can run two Codex accounts simultaneously without
duplicating user config.

- CodexProfileRegistry (YAML, atomic write tmp.<pid>.<rand> + rename,
  orphan cleanup, full CRUD + default pointer)
- decode-id-token: pure base64 JWT decoder for OpenAI id_token,
  reads nested https://api.openai.com/auth claims (chatgpt_plan_type,
  chatgpt_account_id) and dual-path email
- ensureSharedConfigSymlink: self-healing, idempotent, overwrites
  stale entries with stderr warning
- 45 unit tests, all green

Foundation only — no CLI, no runtime injection, no dashboard.
Subsequent commits wire those in.
This commit is contained in:
Tam Nhu Tran committed 2026-05-17 14:44:07 -04:00
1 parent d2f7d3f407
commit 358b703d98
12 files changed
+1006

No files matched your search

@@ -0,0 +1,75 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
let decodeAccountIdentity: (authJsonPath: string) => {
email?: string;
plan_type?: string;
account_id?: string;
};
function buildToken(payload: Record<string, unknown>): string {
const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
return `${header}.${body}.fakesig`;
}
const VALID_TOKEN = buildToken({
email: 'test@example.com',
'https://api.openai.com/auth': {
chatgpt_plan_type: 'pro',
chatgpt_account_id: 'acct-abc123',
},
});
let tempDir: string;
beforeEach(async () => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-identity-test-'));
const mod = await import('../../../src/codex-auth/codex-account-identity');
decodeAccountIdentity = mod.decodeAccountIdentity;
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
describe('decodeAccountIdentity', () => {
it('returns {} when auth.json does not exist', () => {
const result = decodeAccountIdentity(path.join(tempDir, 'auth.json'));
expect(result).toEqual({});
});
it('returns identity fields from valid auth.json with id_token', () => {
const authJson = {
auth_mode: 'chatgpt_oauth',
tokens: { id_token: VALID_TOKEN, access_token: 'acc', refresh_token: 'ref' },
};
fs.writeFileSync(path.join(tempDir, 'auth.json'), JSON.stringify(authJson), { mode: 0o600 });
const result = decodeAccountIdentity(path.join(tempDir, 'auth.json'));
expect(result.email).toBe('test@example.com');
expect(result.plan_type).toBe('pro');
expect(result.account_id).toBe('acct-abc123');
});
it('returns {} when auth.json contains corrupt JSON', () => {
fs.writeFileSync(path.join(tempDir, 'auth.json'), '{ not valid json !!!', { mode: 0o600 });
const result = decodeAccountIdentity(path.join(tempDir, 'auth.json'));
expect(result).toEqual({});
});
it('returns {} when id_token field is missing from tokens', () => {
const authJson = { auth_mode: 'openai', tokens: { access_token: 'acc' } };
fs.writeFileSync(path.join(tempDir, 'auth.json'), JSON.stringify(authJson), { mode: 0o600 });
const result = decodeAccountIdentity(path.join(tempDir, 'auth.json'));
expect(result).toEqual({});
});
it('returns {} when tokens field is absent entirely', () => {
const authJson = { auth_mode: 'openai', OPENAI_API_KEY: 'sk-...' };
fs.writeFileSync(path.join(tempDir, 'auth.json'), JSON.stringify(authJson), { mode: 0o600 });
const result = decodeAccountIdentity(path.join(tempDir, 'auth.json'));
expect(result).toEqual({});
});
});
@@ -0,0 +1,107 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
let ensureSharedConfigSymlink: (profileDir: string, sharedConfigPath?: string) => void;
let tempDir: string;
let profileDir: string;
let sharedConfigPath: string;
beforeEach(async () => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-symlink-test-'));
profileDir = path.join(tempDir, 'profile');
// Use a temp path for the shared config so tests never touch real ~/.codex/config.toml
sharedConfigPath = path.join(tempDir, 'shared-config.toml');
const mod = await import('../../../src/codex-auth/codex-config-symlink');
ensureSharedConfigSymlink = mod.ensureSharedConfigSymlink;
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
describe('ensureSharedConfigSymlink', () => {
it('creates empty shared config and symlink when neither exists', () => {
// Neither profileDir nor sharedConfigPath exist yet
ensureSharedConfigSymlink(profileDir, sharedConfigPath);
expect(fs.existsSync(sharedConfigPath)).toBe(true);
expect(fs.readFileSync(sharedConfigPath, 'utf8')).toBe('');
const linkPath = path.join(profileDir, 'config.toml');
const stat = fs.lstatSync(linkPath);
expect(stat.isSymbolicLink()).toBe(true);
expect(fs.readlinkSync(linkPath)).toBe(sharedConfigPath);
});
it('is idempotent when symlink already points to correct target', () => {
ensureSharedConfigSymlink(profileDir, sharedConfigPath);
// Call again — should not throw
expect(() => ensureSharedConfigSymlink(profileDir, sharedConfigPath)).not.toThrow();
const linkPath = path.join(profileDir, 'config.toml');
expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
expect(fs.readlinkSync(linkPath)).toBe(sharedConfigPath);
});
it('preserves existing shared config content (does not overwrite)', () => {
fs.writeFileSync(sharedConfigPath, '[model]\nname = "o4"', { mode: 0o600 });
ensureSharedConfigSymlink(profileDir, sharedConfigPath);
expect(fs.readFileSync(sharedConfigPath, 'utf8')).toBe('[model]\nname = "o4"');
});
it('replaces a stale symlink pointing to a wrong target with correct one', () => {
fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
const wrongTarget = path.join(tempDir, 'wrong.toml');
fs.writeFileSync(wrongTarget, '', { mode: 0o600 });
const linkPath = path.join(profileDir, 'config.toml');
fs.symlinkSync(wrongTarget, linkPath);
ensureSharedConfigSymlink(profileDir, sharedConfigPath);
expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
expect(fs.readlinkSync(linkPath)).toBe(sharedConfigPath);
});
it('replaces a regular file at link path with symlink (with warning)', () => {
fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
const linkPath = path.join(profileDir, 'config.toml');
fs.writeFileSync(linkPath, '[existing]\ndata = true', { mode: 0o600 });
// Capture stderr to verify warning was written
const stderrChunks: string[] = [];
const origWrite = process.stderr.write.bind(process.stderr);
process.stderr.write = (chunk: string | Uint8Array): boolean => {
stderrChunks.push(typeof chunk === 'string' ? chunk : chunk.toString());
return origWrite(chunk);
};
try {
ensureSharedConfigSymlink(profileDir, sharedConfigPath);
} finally {
process.stderr.write = origWrite;
}
expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
expect(fs.readlinkSync(linkPath)).toBe(sharedConfigPath);
// A warning should have been emitted
expect(stderrChunks.join('')).toMatch(/overwr|replaced|regular file/i);
});
it('replaces a broken symlink (dangling) with correct symlink', () => {
fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
const linkPath = path.join(profileDir, 'config.toml');
// Create symlink to non-existent target
fs.symlinkSync(path.join(tempDir, 'does-not-exist.toml'), linkPath);
// Verify it's broken
expect(fs.existsSync(linkPath)).toBe(false);
expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
ensureSharedConfigSymlink(profileDir, sharedConfigPath);
expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
expect(fs.readlinkSync(linkPath)).toBe(sharedConfigPath);
});
});
@@ -0,0 +1,64 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as os from 'os';
import * as path from 'path';
let getCodexAuthRegistryPath: () => string;
let getCodexInstancesDir: () => string;
let resolveCodexProfileDir: (name: string) => string;
let getSharedCodexConfigPath: () => string;
const ORIGINAL_CCS_HOME = process.env.CCS_HOME;
beforeEach(async () => {
process.env.CCS_HOME = '/tmp/test-ccs-home';
// Re-import to pick up env change — use dynamic import with cache busting
const mod = await import('../../../src/codex-auth/codex-profile-paths');
getCodexAuthRegistryPath = mod.getCodexAuthRegistryPath;
getCodexInstancesDir = mod.getCodexInstancesDir;
resolveCodexProfileDir = mod.resolveCodexProfileDir;
getSharedCodexConfigPath = mod.getSharedCodexConfigPath;
});
afterEach(() => {
if (ORIGINAL_CCS_HOME === undefined) {
delete process.env.CCS_HOME;
} else {
process.env.CCS_HOME = ORIGINAL_CCS_HOME;
}
});
describe('codex-profile-paths', () => {
it('getCodexAuthRegistryPath returns codex-profiles.yaml inside getCcsDir()', () => {
const result = getCodexAuthRegistryPath();
expect(result).toContain('codex-profiles.yaml');
expect(result).toContain('.ccs');
});
it('getCodexInstancesDir returns codex-instances inside getCcsDir()', () => {
const result = getCodexInstancesDir();
expect(result).toContain('codex-instances');
expect(result).toContain('.ccs');
});
it('resolveCodexProfileDir returns instancesDir/<name>', () => {
const instancesDir = getCodexInstancesDir();
const profileDir = resolveCodexProfileDir('work');
expect(profileDir).toBe(path.join(instancesDir, 'work'));
});
it('resolveCodexProfileDir correctly nests a different profile name', () => {
const instancesDir = getCodexInstancesDir();
const profileDir = resolveCodexProfileDir('personal');
expect(profileDir).toBe(path.join(instancesDir, 'personal'));
});
it('getSharedCodexConfigPath resolves under os.homedir() not getCcsDir()', () => {
const result = getSharedCodexConfigPath();
// Must equal os.homedir()/.codex/config.toml — uses real homedir, not getCcsDir()
expect(result).toBe(path.join(os.homedir(), '.codex', 'config.toml'));
// Must end with the Codex-canonical path fragment
expect(result).toMatch(/\.codex[/\\]config\.toml$/);
// Must NOT end inside the .ccs directory (i.e. not a CCS-owned path)
expect(result).not.toContain(path.join('.ccs', 'codex'));
});
});
@@ -0,0 +1,214 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as yaml from 'js-yaml';
let CodexProfileRegistry: new (registryPath?: string) => {
createProfile(name: string, meta?: Record<string, unknown>): void;
getProfile(name: string): Record<string, unknown>;
updateProfile(name: string, partial: Record<string, unknown>): void;
removeProfile(name: string): void;
listProfiles(): string[];
hasProfile(name: string): boolean;
getDefault(): string | null;
setDefault(name: string): void;
clearDefault(): void;
touchProfile(name: string): void;
};
let tempDir: string;
let ccsHome: string;
let registryPath: string;
const ORIGINAL_CCS_HOME = process.env.CCS_HOME;
beforeEach(async () => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-registry-test-'));
ccsHome = path.join(tempDir, 'ccs-home');
fs.mkdirSync(path.join(ccsHome, '.ccs'), { recursive: true, mode: 0o700 });
process.env.CCS_HOME = ccsHome;
registryPath = path.join(ccsHome, '.ccs', 'codex-profiles.yaml');
const mod = await import('../../../src/codex-auth/codex-profile-registry');
CodexProfileRegistry = mod.CodexProfileRegistry;
});
afterEach(() => {
if (ORIGINAL_CCS_HOME === undefined) {
delete process.env.CCS_HOME;
} else {
process.env.CCS_HOME = ORIGINAL_CCS_HOME;
}
fs.rmSync(tempDir, { recursive: true, force: true });
});
describe('CodexProfileRegistry — empty state', () => {
it('returns empty list when registry file does not exist', () => {
const reg = new CodexProfileRegistry(registryPath);
expect(reg.listProfiles()).toEqual([]);
});
it('returns null default when registry file does not exist', () => {
const reg = new CodexProfileRegistry(registryPath);
expect(reg.getDefault()).toBeNull();
});
});
describe('CodexProfileRegistry — create and get', () => {
it('creates a profile and retrieves it by name', () => {
const reg = new CodexProfileRegistry(registryPath);
reg.createProfile('work');
const profile = reg.getProfile('work');
expect(profile.type).toBe('codex');
expect(typeof profile.created).toBe('string');
expect(profile.last_used).toBeNull();
});
it('persists profile to disk as YAML with schema version', () => {
const reg = new CodexProfileRegistry(registryPath);
reg.createProfile('work');
const raw = fs.readFileSync(registryPath, 'utf8');
const parsed = yaml.load(raw) as Record<string, unknown>;
expect(parsed.version).toBe('1.0');
expect(typeof parsed.profiles).toBe('object');
});
it('throws when creating a duplicate profile name', () => {
const reg = new CodexProfileRegistry(registryPath);
reg.createProfile('work');
expect(() => reg.createProfile('work')).toThrow(/already exists/i);
});
it('accepts optional metadata on create', () => {
const reg = new CodexProfileRegistry(registryPath);
reg.createProfile('personal', { email: 'me@example.com', plan_type: 'pro' });
const profile = reg.getProfile('personal');
expect(profile.email).toBe('me@example.com');
expect(profile.plan_type).toBe('pro');
});
it('hasProfile returns false before creation and true after', () => {
const reg = new CodexProfileRegistry(registryPath);
expect(reg.hasProfile('work')).toBe(false);
reg.createProfile('work');
expect(reg.hasProfile('work')).toBe(true);
});
});
describe('CodexProfileRegistry — remove', () => {
it('removes an existing profile', () => {
const reg = new CodexProfileRegistry(registryPath);
reg.createProfile('work');
reg.removeProfile('work');
expect(reg.listProfiles()).toEqual([]);
});
it('throws when removing a non-existent profile', () => {
const reg = new CodexProfileRegistry(registryPath);
expect(() => reg.removeProfile('ghost')).toThrow(/not found/i);
});
it('clears default when the default profile is removed', () => {
const reg = new CodexProfileRegistry(registryPath);
reg.createProfile('work');
reg.setDefault('work');
expect(reg.getDefault()).toBe('work');
reg.removeProfile('work');
expect(reg.getDefault()).toBeNull();
});
});
describe('CodexProfileRegistry — default pointer', () => {
it('setDefault throws when profile does not exist', () => {
const reg = new CodexProfileRegistry(registryPath);
expect(() => reg.setDefault('ghost')).toThrow(/not found/i);
});
it('setDefault and getDefault round-trip', () => {
const reg = new CodexProfileRegistry(registryPath);
reg.createProfile('work');
reg.setDefault('work');
expect(reg.getDefault()).toBe('work');
});
it('clearDefault resets default to null', () => {
const reg = new CodexProfileRegistry(registryPath);
reg.createProfile('work');
reg.setDefault('work');
reg.clearDefault();
expect(reg.getDefault()).toBeNull();
});
});
describe('CodexProfileRegistry — listProfiles', () => {
it('returns all profile names', () => {
const reg = new CodexProfileRegistry(registryPath);
reg.createProfile('work');
reg.createProfile('personal');
const list = reg.listProfiles();
expect(list).toContain('work');
expect(list).toContain('personal');
expect(list.length).toBe(2);
});
});
describe('CodexProfileRegistry — corrupt YAML recovery', () => {
it('returns empty state on corrupt YAML without throwing', () => {
fs.mkdirSync(path.dirname(registryPath), { recursive: true });
fs.writeFileSync(registryPath, '{ invalid: yaml: content: [', { mode: 0o600 });
const reg = new CodexProfileRegistry(registryPath);
expect(reg.listProfiles()).toEqual([]);
expect(reg.getDefault()).toBeNull();
});
});
describe('CodexProfileRegistry — atomic write', () => {
it('leaves no .tmp file after successful write', () => {
const reg = new CodexProfileRegistry(registryPath);
reg.createProfile('work');
const dir = path.dirname(registryPath);
const tmpFiles = fs.readdirSync(dir).filter((f) => f.includes('.tmp.'));
expect(tmpFiles.length).toBe(0);
});
});
describe('CodexProfileRegistry — touchProfile', () => {
it('updates last_used timestamp', async () => {
const reg = new CodexProfileRegistry(registryPath);
reg.createProfile('work');
const before = new Date().toISOString();
await new Promise((r) => setTimeout(r, 5));
reg.touchProfile('work');
const profile = reg.getProfile('work');
expect(typeof profile.last_used).toBe('string');
expect((profile.last_used as string) >= before).toBe(true);
});
});
describe('CodexProfileRegistry — updateProfile', () => {
it('merges partial updates into existing profile', () => {
const reg = new CodexProfileRegistry(registryPath);
reg.createProfile('work');
reg.updateProfile('work', { email: 'updated@example.com', plan_type: 'plus' });
const profile = reg.getProfile('work');
expect(profile.email).toBe('updated@example.com');
expect(profile.plan_type).toBe('plus');
expect(profile.type).toBe('codex');
});
it('throws when updating a non-existent profile', () => {
const reg = new CodexProfileRegistry(registryPath);
expect(() => reg.updateProfile('ghost', { email: 'x@x.com' })).toThrow(/not found/i);
});
});
describe('CodexProfileRegistry — registry file permissions', () => {
it('writes registry file with mode 0o600', () => {
const reg = new CodexProfileRegistry(registryPath);
reg.createProfile('work');
const stat = fs.statSync(registryPath);
// On POSIX, check owner read/write only (0o600 = 0b110_000_000 = 384)
expect(stat.mode & 0o777).toBe(0o600);
});
});
@@ -0,0 +1,124 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
// Lazy import so tests can run before implementation
let decodeIdToken: (idToken: string) => { email?: string; plan_type?: string; account_id?: string };
// Fixture: real-shape JWT with nested claims
// Payload (base64url): {"email":"user@example.com","https://api.openai.com/auth":{"chatgpt_plan_type":"pro","chatgpt_account_id":"4b0448c0-e4a2-4cc0-a70d-77065d613553"}}
const VALID_NESTED_TOKEN = buildToken({
email: 'user@example.com',
'https://api.openai.com/auth': {
chatgpt_plan_type: 'pro',
chatgpt_account_id: '4b0448c0-e4a2-4cc0-a70d-77065d613553',
},
});
// Fixture: email only via profile fallback path
const PROFILE_EMAIL_TOKEN = buildToken({
'https://api.openai.com/profile': { email: 'profile@example.com' },
'https://api.openai.com/auth': { chatgpt_plan_type: 'plus', chatgpt_account_id: 'acct-xyz' },
});
// Fixture: neither top-level email nor profile email — both absent
const NO_EMAIL_TOKEN = buildToken({
sub: 'user-abc',
'https://api.openai.com/auth': {
chatgpt_plan_type: 'free',
chatgpt_account_id: 'acct-123',
},
});
// Fixture: missing plan_type in auth claim
const MISSING_PLAN_TOKEN = buildToken({
email: 'user@example.com',
'https://api.openai.com/auth': {
chatgpt_account_id: 'acct-no-plan',
},
});
// Fixture: missing account_id in auth claim
const MISSING_ACCOUNT_ID_TOKEN = buildToken({
email: 'user@example.com',
'https://api.openai.com/auth': {
chatgpt_plan_type: 'pro',
},
});
// Fixture: no https://api.openai.com/auth claim at all
const NO_AUTH_CLAIM_TOKEN = buildToken({
email: 'user@example.com',
sub: 'user-abc',
});
function buildToken(payload: Record<string, unknown>): string {
const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
return `${header}.${body}.fakesig`;
}
beforeEach(async () => {
const mod = await import('../../../src/codex-auth/decode-id-token');
decodeIdToken = mod.decodeIdToken;
});
describe('decodeIdToken', () => {
it('extracts email, plan_type, account_id from standard nested JWT', () => {
const result = decodeIdToken(VALID_NESTED_TOKEN);
expect(result.email).toBe('user@example.com');
expect(result.plan_type).toBe('pro');
expect(result.account_id).toBe('4b0448c0-e4a2-4cc0-a70d-77065d613553');
});
it('falls back to profile email when top-level email is absent', () => {
const result = decodeIdToken(PROFILE_EMAIL_TOKEN);
expect(result.email).toBe('profile@example.com');
expect(result.plan_type).toBe('plus');
expect(result.account_id).toBe('acct-xyz');
});
it('returns {} for token with only 2 segments (malformed)', () => {
const result = decodeIdToken('header.payload');
expect(result).toEqual({});
});
it('returns {} for non-base64 garbage input', () => {
const result = decodeIdToken('!!!.%%%.$$$');
expect(result).toEqual({});
});
it('returns empty object when email is absent in both paths', () => {
const result = decodeIdToken(NO_EMAIL_TOKEN);
expect(result.email).toBeUndefined();
expect(result.plan_type).toBe('free');
expect(result.account_id).toBe('acct-123');
});
it('returns undefined plan_type when chatgpt_plan_type is absent', () => {
const result = decodeIdToken(MISSING_PLAN_TOKEN);
expect(result.email).toBe('user@example.com');
expect(result.plan_type).toBeUndefined();
expect(result.account_id).toBe('acct-no-plan');
});
it('returns undefined account_id when chatgpt_account_id is absent', () => {
const result = decodeIdToken(MISSING_ACCOUNT_ID_TOKEN);
expect(result.email).toBe('user@example.com');
expect(result.plan_type).toBe('pro');
expect(result.account_id).toBeUndefined();
});
it('returns partial result when https://api.openai.com/auth claim is absent', () => {
const result = decodeIdToken(NO_AUTH_CLAIM_TOKEN);
expect(result.email).toBe('user@example.com');
expect(result.plan_type).toBeUndefined();
expect(result.account_id).toBeUndefined();
});
it('does not throw on empty string input', () => {
expect(() => decodeIdToken('')).not.toThrow();
expect(decodeIdToken('')).toEqual({});
});
});