fix(codex-auth): reject stray profile args

This commit is contained in:
Tam Nhu Tran
2026-05-17 17:51:32 -04:00
parent 85521018bf
commit c90b3cb9da
4 changed files with 98 additions and 8 deletions
+6 -4
View File
@@ -69,6 +69,8 @@ export function showProfileDetail(
if (isActive) states.push('active');
const stateStr = states.join(',');
const accountId = meta.account_id ?? identity.account_id ?? null;
const email = meta.email ?? identity.email ?? null;
const plan = meta.plan_type ?? identity.plan_type ?? null;
if (json) {
const out: CodexProfileOutput = {
@@ -77,8 +79,8 @@ export function showProfileDetail(
is_active: isActive,
created: meta.created,
last_used: meta.last_used ?? null,
email: identity.email ?? null,
plan: meta.plan_type ?? null,
email,
plan,
account_id: accountId,
profile_dir: profileDir,
auth_json_exists: authExists,
@@ -98,8 +100,8 @@ export function showProfileDetail(
['Profile dir', profileDir],
['config.toml', configTarget ? `-> ${configTarget} (symlink)` : '(not linked)'],
['auth.json', authState],
['Email', identity.email ?? (authExists ? '<invalid>' : '<unknown>')],
['Plan', meta.plan_type ?? (authExists ? '<invalid>' : '<unknown>')],
['Email', email ?? (authExists ? '<invalid>' : '<unknown>')],
['Plan', plan ?? (authExists ? '<invalid>' : '<unknown>')],
['Account ID', accountId ?? '-'],
['Created', new Date(meta.created).toLocaleString()],
['Last used', meta.last_used ? new Date(meta.last_used).toLocaleString() : 'never'],
+13 -2
View File
@@ -28,6 +28,7 @@ export interface CodexAuthArgs {
shell?: string;
unknownFlags?: string[];
seenOptions?: string[];
extraPositionals?: string[];
}
// ── Profile output shape (JSON mode) ─────────────────────────────────────────
@@ -86,6 +87,9 @@ export function parseArgs(args: string[]): CodexAuthArgs {
if (positional.length > 0) {
result.profileName = positional[0];
}
if (positional.length > 1) {
result.extraPositionals = positional.slice(1);
}
return result;
}
@@ -108,10 +112,17 @@ export function rejectUnsupportedOptions(
if (seen.has('--json') && !allowed.json) unsupported.add('--json');
if (seen.has('--force') && !allowed.force) unsupported.add('--force');
if (seen.has('--shell') && !allowed.shell) unsupported.add('--shell');
const extraPositionals = parsed.extraPositionals ?? [];
if (unsupported.size > 0) {
if (unsupported.size > 0 || extraPositionals.length > 0) {
const flags = [...unsupported].join(', ');
process.stderr.write(`Usage: ${color(usage, 'command')}\n`);
exitWithError(`Unknown options: ${flags}`, ExitCode.GENERAL_ERROR);
const details = [
flags ? `Unknown options: ${flags}` : null,
extraPositionals.length > 0
? `Unexpected arguments: ${extraPositionals.map((arg) => `"${arg}"`).join(', ')}`
: null,
].filter(Boolean);
exitWithError(details.join('; '), ExitCode.GENERAL_ERROR);
}
}
@@ -146,6 +146,42 @@ describe('handleRemoveCodex — only profile', () => {
// ── confirmation prompt ───────────────────────────────────────────────────────
describe('handleRemoveCodex — confirmation', () => {
it('rejects extra positional arguments before deleting anything', async () => {
const { handleRemoveCodex } = await import(
'../../../../src/codex-auth/commands/remove-command'
);
const ctx = await makeCtx('work');
let exitCode = -1;
const err: string[] = [];
const origExit = process.exit;
const origError = console.error;
const origWrite = process.stderr.write.bind(process.stderr);
process.exit = (code?: number) => {
exitCode = code ?? 0;
throw new Error('exit');
};
console.error = (...a: unknown[]) => err.push(a.join(' '));
process.stderr.write = (chunk: string | Uint8Array) => {
err.push(String(chunk));
return true;
};
try {
await handleRemoveCodex(ctx, ['work', 'accidental', '--yes']);
} catch {
/* process.exit */
} finally {
process.exit = origExit;
console.error = origError;
process.stderr.write = origWrite;
}
expect(exitCode).toBeGreaterThan(0);
expect(ctx.registry.hasProfile('work')).toBe(true);
expect(err.join('')).toContain('Unexpected arguments: "accidental"');
});
it('cancels when user declines confirmation', async () => {
await mockConfirmNo();
const { handleRemoveCodex } = await import(
@@ -168,17 +168,58 @@ describe('handleShowCodex — detail view', () => {
expect(out).toContain('<unknown>');
});
it('detail JSON includes account_id from registry metadata when auth.json is missing', async () => {
it('detail JSON includes cached registry identity when auth.json is missing', async () => {
const { handleShowCodex } = await import('../../../../src/codex-auth/commands/show-command');
const ctx = await makeCtx('registrydetail');
ctx.registry.updateProfile('registrydetail', {
account_id: 'acct-from-registry-detail',
email: 'detail@example.com',
plan_type: 'team',
});
const out = await captureStdout(() => handleShowCodex(ctx, ['registrydetail', '--json']));
const parsed = JSON.parse(out) as { account_id: string | null };
const parsed = JSON.parse(out) as {
account_id: string | null;
email: string | null;
plan: string | null;
};
expect(parsed.account_id).toBe('acct-from-registry-detail');
expect(parsed.email).toBe('detail@example.com');
expect(parsed.plan).toBe('team');
});
it('rejects extra positional arguments instead of ignoring them', async () => {
const { handleShowCodex } = await import('../../../../src/codex-auth/commands/show-command');
const ctx = await makeCtx('myprofile');
let exitCode = -1;
const err: string[] = [];
const origExit = process.exit;
const origError = console.error;
const origWrite = process.stderr.write.bind(process.stderr);
process.exit = (code?: number) => {
exitCode = code ?? 0;
throw new Error('exit');
};
console.error = (...a: unknown[]) => err.push(a.join(' '));
process.stderr.write = (chunk: string | Uint8Array) => {
err.push(String(chunk));
return true;
};
try {
await handleShowCodex(ctx, ['myprofile', 'extra']);
} catch {
/* process.exit */
} finally {
process.exit = origExit;
console.error = origError;
process.stderr.write = origWrite;
}
expect(exitCode).toBeGreaterThan(0);
expect(err.join('')).toContain('Unexpected arguments: "extra"');
});
it('does not crash with malformed auth.json', async () => {