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

+73
View File
@@ -0,0 +1,73 @@
import * as fs from 'fs';
import * as path from 'path';
import { createLogger } from '../services/logging';
import { getSharedCodexConfigPath } from './codex-profile-paths';
const logger = createLogger('codex-auth:symlink');
/**
* Ensure <profileDir>/config.toml is a symlink pointing to the shared
* ~/.codex/config.toml. Self-healing: recreates stale or missing symlinks.
*
* @param profileDir - The per-profile directory (will be created if missing).
* @param sharedConfigPath - Override for the shared target path (used in tests
* to avoid touching real ~/.codex/config.toml). Defaults to getSharedCodexConfigPath().
*/
export function ensureSharedConfigSymlink(profileDir: string, sharedConfigPath?: string): void {
const targetPath = sharedConfigPath ?? getSharedCodexConfigPath();
const linkPath = path.join(profileDir, 'config.toml');
// Ensure profile directory exists
fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
// Ensure shared config target parent directory exists
fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: 0o700 });
// Create empty shared config if it doesn't exist — Codex will populate on first run
if (!fs.existsSync(targetPath)) {
fs.writeFileSync(targetPath, '', { mode: 0o600 });
logger.stage('dispatch', 'codex.shared-config.created', 'Created empty shared config.toml', {
path: targetPath,
});
}
// Inspect whatever currently exists at the link path
let existingStat: fs.Stats | null = null;
try {
existingStat = fs.lstatSync(linkPath);
} catch {
// ENOENT — nothing there yet, proceed to create
}
if (existingStat !== null) {
if (existingStat.isSymbolicLink()) {
const currentTarget = fs.readlinkSync(linkPath);
if (currentTarget === targetPath) {
// Already correct — idempotent return
logger.stage('dispatch', 'codex.symlink.ok', 'Shared config symlink already correct', {
link: linkPath,
});
return;
}
// Stale symlink — remove and re-create
fs.unlinkSync(linkPath);
logger.stage('dispatch', 'codex.symlink.repaired', 'Replaced stale symlink', {
link: linkPath,
was: currentTarget,
now: targetPath,
});
} else {
// Regular file or other non-symlink entry — overwrite with warning
process.stderr.write(
`[!] codex-auth: overwriting regular file at ${linkPath} with symlink to shared config.toml\n`
);
fs.unlinkSync(linkPath);
}
}
fs.symlinkSync(targetPath, linkPath);
logger.stage('dispatch', 'codex.symlink.created', 'Created shared config symlink', {
link: linkPath,
target: targetPath,
});
}