Skip to content

Commit 2abd222

Browse files
committed
fix(auth): tighten onboarding restore review followups
1 parent 57bb18f commit 2abd222

8 files changed

Lines changed: 393 additions & 55 deletions

File tree

docs/upgrade.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ For maintainer/debug flows, see advanced/internal controls in [development/CONFI
7070

7171
- `Recover saved accounts` appears only when at least one valid named backup exists.
7272
- No new CLI flags or npm scripts were added for this flow.
73-
- The backup root remains `~/.codex/multi-auth/backups` by default, or `%CODEX_MULTI_AUTH_DIR%\\backups` when `CODEX_MULTI_AUTH_DIR` is set.
73+
- The backup root remains `~/.codex/multi-auth/backups` by default, or `%CODEX_MULTI_AUTH_DIR%\backups` when `CODEX_MULTI_AUTH_DIR` is set.
7474

7575
---
7676

lib/accounts.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,12 @@ export interface ManagedAccount {
9292
expires?: number;
9393
addedAt: number;
9494
lastUsed: number;
95-
lastSwitchReason?:
96-
| "rate-limit"
97-
| "initial"
98-
| "rotation"
99-
| "best"
100-
| "restore";
95+
lastSwitchReason?:
96+
| "rate-limit"
97+
| "initial"
98+
| "rotation"
99+
| "best"
100+
| "restore";
101101
lastRateLimitReason?: RateLimitReason;
102102
rateLimitResetTimes: RateLimitStateV3;
103103
coolingDownUntil?: number;

lib/codex-manager.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4355,21 +4355,31 @@ async function runAuthLogin(): Promise<number> {
43554355
}
43564356

43574357
const refreshedStorage = await loadAccounts();
4358-
const existingCount = refreshedStorage?.accounts.length ?? 0;
4358+
let existingCount = refreshedStorage?.accounts.length ?? 0;
43594359
let forceNewLogin = existingCount > 0;
4360-
let namedBackups: NamedBackupSummary[] = [];
4361-
if (existingCount === 0) {
4360+
const loadNamedBackupsForOnboarding = async (): Promise<NamedBackupSummary[]> => {
4361+
if (existingCount > 0) {
4362+
return [];
4363+
}
43624364
try {
4363-
namedBackups = await getNamedBackups();
4365+
return await getNamedBackups();
43644366
} catch (error) {
4367+
const code = (error as NodeJS.ErrnoException).code;
43654368
log.debug("getNamedBackups failed, skipping restore option", {
4369+
code,
43664370
error: error instanceof Error ? error.message : String(error),
43674371
});
4368-
namedBackups = [];
4372+
if (code && code !== "ENOENT") {
4373+
console.warn(
4374+
"Named backup discovery failed. Continuing with browser or manual sign-in only.",
4375+
);
4376+
}
4377+
return [];
43694378
}
4370-
}
4371-
const latestNamedBackup = namedBackups[0] ?? null;
4379+
};
4380+
let namedBackups = await loadNamedBackupsForOnboarding();
43724381
while (true) {
4382+
const latestNamedBackup = namedBackups[0] ?? null;
43734383
const signInMode = await promptOAuthSignInMode(latestNamedBackup);
43744384
if (signInMode === "cancel") {
43754385
if (existingCount > 0) {
@@ -4379,15 +4389,20 @@ async function runAuthLogin(): Promise<number> {
43794389
console.log("Cancelled.");
43804390
return 0;
43814391
}
4382-
if (signInMode === "restore-backup" && latestNamedBackup) {
4383-
const restoreMode = await promptBackupRestoreMode(latestNamedBackup);
4392+
if (signInMode === "restore-backup") {
4393+
namedBackups = await loadNamedBackupsForOnboarding();
4394+
const latestAvailableBackup = namedBackups[0] ?? null;
4395+
if (!latestAvailableBackup) {
4396+
continue;
4397+
}
4398+
const restoreMode = await promptBackupRestoreMode(latestAvailableBackup);
43844399
if (restoreMode === "back") {
43854400
continue;
43864401
}
43874402

43884403
const selectedBackup = restoreMode === "manual"
43894404
? await promptManualBackupSelection(namedBackups)
4390-
: latestNamedBackup;
4405+
: latestAvailableBackup;
43914406
if (!selectedBackup) {
43924407
continue;
43934408
}
@@ -4473,6 +4488,8 @@ async function runAuthLogin(): Promise<number> {
44734488

44744489
const latestStorage = await loadAccounts();
44754490
const count = latestStorage?.accounts.length ?? 1;
4491+
existingCount = count;
4492+
namedBackups = [];
44764493
console.log(`Added account. Total: ${count}`);
44774494
if (count >= ACCOUNT_LIMITS.MAX_ACCOUNTS) {
44784495
console.log(`Reached maximum account limit (${ACCOUNT_LIMITS.MAX_ACCOUNTS}).`);

lib/schemas.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,11 @@ export type CooldownReasonFromSchema = z.infer<typeof CooldownReasonSchema>;
8383
* Last switch reason for account rotation tracking.
8484
*/
8585
export const SwitchReasonSchema = z.enum([
86-
"rate-limit",
87-
"initial",
88-
"rotation",
89-
"best",
90-
"restore",
86+
"rate-limit",
87+
"initial",
88+
"rotation",
89+
"best",
90+
"restore",
9191
]);
9292

9393
export type SwitchReasonFromSchema = z.infer<typeof SwitchReasonSchema>;

lib/storage.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,14 +142,23 @@ async function collectNamedBackups(storagePath: string): Promise<NamedBackupSumm
142142
if (!entry.name.toLowerCase().endsWith(".json")) continue;
143143
const candidatePath = join(backupRoot, entry.name);
144144
try {
145-
const stats = await fs.stat(candidatePath);
145+
const statsBefore = await fs.stat(candidatePath);
146146
const { normalized } = await loadAccountsFromPath(candidatePath);
147147
if (!normalized || normalized.accounts.length === 0) continue;
148+
const statsAfter = await fs.stat(candidatePath).catch(() => null);
149+
if (statsAfter && statsAfter.mtimeMs !== statsBefore.mtimeMs) {
150+
log.debug("backup file changed between stat and load, mtime may be stale", {
151+
candidatePath,
152+
fileName: entry.name,
153+
beforeMtimeMs: statsBefore.mtimeMs,
154+
afterMtimeMs: statsAfter.mtimeMs,
155+
});
156+
}
148157
candidates.push({
149158
path: candidatePath,
150159
fileName: entry.name,
151160
accountCount: normalized.accounts.length,
152-
mtimeMs: stats.mtimeMs,
161+
mtimeMs: statsBefore.mtimeMs,
153162
});
154163
} catch (error) {
155164
log.debug("Skipping named backup candidate after loadAccountsFromPath/fs.stat failure", {

lib/storage/migrations.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ export interface AccountMetadataV1 {
2525
enabled?: boolean;
2626
addedAt: number;
2727
lastUsed: number;
28-
lastSwitchReason?:
29-
| "rate-limit"
30-
| "initial"
31-
| "rotation"
32-
| "best"
33-
| "restore";
28+
lastSwitchReason?:
29+
| "rate-limit"
30+
| "initial"
31+
| "rotation"
32+
| "best"
33+
| "restore";
3434
rateLimitResetTime?: number;
3535
coolingDownUntil?: number;
3636
cooldownReason?: CooldownReason;
@@ -55,12 +55,12 @@ export interface AccountMetadataV3 {
5555
enabled?: boolean;
5656
addedAt: number;
5757
lastUsed: number;
58-
lastSwitchReason?:
59-
| "rate-limit"
60-
| "initial"
61-
| "rotation"
62-
| "best"
63-
| "restore";
58+
lastSwitchReason?:
59+
| "rate-limit"
60+
| "initial"
61+
| "rotation"
62+
| "best"
63+
| "restore";
6464
rateLimitResetTimes?: RateLimitStateV3;
6565
coolingDownUntil?: number;
6666
cooldownReason?: CooldownReason;

0 commit comments

Comments
 (0)