Settings: show codex account email + subscription expiry (#45)

- Extract email and chatgpt_subscription_last_checked from codex JWT in
  addition to plan_type and subscription_active_until.
- Settings → 계정 row now displays: email · plan badge · expiry badge
  with active/soon/expired color coding (green/amber/red).
- Fix codex/.codex-accounts directory scan picking up sibling dirs like
  "1.bak-..." and double-counting an index (require dirname to be
  fully numeric).

Motivation: rotation was landing on accounts whose Pro/Team subscription
had silently expired, causing OpenAI to reject upper-tier models with
a misleading "model not supported" error. Surface expiry in the UI so
the user can see at a glance which accounts need renewal.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Eyejoker
2026-04-27 21:58:01 +09:00
committed by GitHub
parent eeba0334f9
commit 5883e57d73
4 changed files with 176 additions and 49 deletions

View File

@@ -27,8 +27,10 @@ export interface ClaudeAccountSummary {
export interface CodexAccountSummary {
index: number;
accountId: string | null;
email: string | null;
planType: string | null;
subscriptionUntil: string | null;
subscriptionLastChecked: string | null;
exists: boolean;
}
@@ -105,8 +107,10 @@ function readCodexAccount(index: number): CodexAccountSummary | null {
tokens?: { id_token?: string; access_token?: string };
}>(file);
let accountId: string | null = null;
let email: string | null = null;
let planType: string | null = null;
let subscriptionUntil: string | null = null;
let subscriptionLastChecked: string | null = null;
const idToken = data?.tokens?.id_token;
if (idToken && typeof idToken === 'string') {
const parts = idToken.split('.');
@@ -116,6 +120,7 @@ function readCodexAccount(index: number): CodexAccountSummary | null {
Buffer.from(parts[1], 'base64').toString('utf-8'),
) as Record<string, unknown>;
if (typeof payload.sub === 'string') accountId = payload.sub;
if (typeof payload.email === 'string') email = payload.email;
const auth = payload['https://api.openai.com/auth'] as
| Record<string, unknown>
| undefined;
@@ -126,6 +131,9 @@ function readCodexAccount(index: number): CodexAccountSummary | null {
if (typeof auth.chatgpt_subscription_active_until === 'string') {
subscriptionUntil = auth.chatgpt_subscription_active_until;
}
if (typeof auth.chatgpt_subscription_last_checked === 'string') {
subscriptionLastChecked = auth.chatgpt_subscription_last_checked;
}
}
} catch {
/* ignore parse errors */
@@ -135,8 +143,10 @@ function readCodexAccount(index: number): CodexAccountSummary | null {
return {
index,
accountId,
email,
planType,
subscriptionUntil,
subscriptionLastChecked,
exists: true,
};
}
@@ -149,6 +159,7 @@ export function listClaudeAccounts(): ClaudeAccountSummary[] {
if (fs.existsSync(dir)) {
const entries = fs.readdirSync(dir);
const indices = entries
.filter((e) => /^\d+$/.test(e))
.map((e) => Number.parseInt(e, 10))
.filter((n) => Number.isFinite(n) && n >= 1)
.sort((a, b) => a - b);
@@ -168,6 +179,7 @@ export function listCodexAccounts(): CodexAccountSummary[] {
if (fs.existsSync(dir)) {
const entries = fs.readdirSync(dir);
const indices = entries
.filter((e) => /^\d+$/.test(e))
.map((e) => Number.parseInt(e, 10))
.filter((n) => Number.isFinite(n) && n >= 1)
.sort((a, b) => a - b);