11import { promises as fs , existsSync } from "node:fs" ;
22import { randomBytes } from "node:crypto" ;
33import { basename , dirname , join } from "node:path" ;
4- import { ACCOUNT_LIMITS } from "./constants.js" ;
4+ import {
5+ ACCOUNT_LIMITS ,
6+ ACCOUNTS_FILE_NAME ,
7+ FLAGGED_ACCOUNTS_FILE_NAME ,
8+ LEGACY_ACCOUNTS_FILE_NAME ,
9+ LEGACY_BLOCKED_ACCOUNTS_FILE_NAME ,
10+ LEGACY_FLAGGED_ACCOUNTS_FILE_NAME ,
11+ } from "./constants.js" ;
512import { createLogger } from "./logger.js" ;
613import { MODEL_FAMILIES , type ModelFamily } from "./prompts/codex.js" ;
714import { AnyAccountStorageSchema , getValidationErrors } from "./schemas.js" ;
@@ -19,10 +26,6 @@ import {
1926export type { CooldownReason , RateLimitStateV3 , AccountMetadataV1 , AccountStorageV1 , AccountMetadataV3 , AccountStorageV3 } ;
2027
2128const log = createLogger ( "storage" ) ;
22- const ACCOUNTS_FILE_NAME = "openai-codex-accounts.json" ;
23- const FLAGGED_ACCOUNTS_FILE_NAME = "openai-codex-flagged-accounts.json" ;
24- const LEGACY_FLAGGED_ACCOUNTS_FILE_NAME = "openai-codex-blocked-accounts.json" ;
25-
2629export interface FlaggedAccountMetadataV1 extends AccountMetadataV3 {
2730 flaggedAt : number ;
2831 flaggedReason ?: string ;
@@ -283,7 +286,7 @@ export function setStoragePath(projectPath: string | null): void {
283286 if ( projectRoot ) {
284287 currentProjectRoot = projectRoot ;
285288 currentStoragePath = join ( getProjectGlobalConfigDir ( projectRoot ) , ACCOUNTS_FILE_NAME ) ;
286- currentLegacyProjectStoragePath = join ( getProjectConfigDir ( projectRoot ) , ACCOUNTS_FILE_NAME ) ;
289+ currentLegacyProjectStoragePath = join ( getProjectConfigDir ( projectRoot ) , LEGACY_ACCOUNTS_FILE_NAME ) ;
287290 } else {
288291 currentStoragePath = null ;
289292 currentLegacyProjectStoragePath = null ;
@@ -316,55 +319,66 @@ function getLegacyFlaggedAccountsPath(): string {
316319 return join ( dirname ( getStoragePath ( ) ) , LEGACY_FLAGGED_ACCOUNTS_FILE_NAME ) ;
317320}
318321
319- async function migrateLegacyProjectStorageIfNeeded (
320- persist : ( storage : AccountStorageV3 ) => Promise < void > = saveAccounts ,
322+ function getLegacyBlockedAccountsPath ( ) : string {
323+ return join ( dirname ( getStoragePath ( ) ) , LEGACY_BLOCKED_ACCOUNTS_FILE_NAME ) ;
324+ }
325+
326+ async function migrateStorageFileIfNeeded (
327+ legacyPath : string | null ,
328+ nextPath : string ,
329+ persist : ( storage : AccountStorageV3 ) => Promise < void > ,
330+ label : string ,
321331) : Promise < AccountStorageV3 | null > {
322- if (
323- ! currentStoragePath ||
324- ! currentLegacyProjectStoragePath ||
325- currentLegacyProjectStoragePath === currentStoragePath ||
326- ! existsSync ( currentLegacyProjectStoragePath )
327- ) {
332+ if ( ! legacyPath || legacyPath === nextPath || ! existsSync ( legacyPath ) ) {
328333 return null ;
329334 }
330335
331336 try {
332- const legacyContent = await fs . readFile ( currentLegacyProjectStoragePath , "utf-8" ) ;
337+ const legacyContent = await fs . readFile ( legacyPath , "utf-8" ) ;
333338 const legacyData = JSON . parse ( legacyContent ) as unknown ;
334339 const normalized = normalizeAccountStorage ( legacyData ) ;
335340 if ( ! normalized ) return null ;
336341
337342 await persist ( normalized ) ;
338343 try {
339- await fs . unlink ( currentLegacyProjectStoragePath ) ;
340- log . info ( "Removed legacy project account storage file after migration" , {
341- path : currentLegacyProjectStoragePath ,
342- } ) ;
344+ await fs . unlink ( legacyPath ) ;
345+ log . info ( `Removed legacy ${ label } after migration` , { path : legacyPath } ) ;
343346 } catch ( unlinkError ) {
344347 const code = ( unlinkError as NodeJS . ErrnoException ) . code ;
345348 if ( code !== "ENOENT" ) {
346- log . warn ( " Failed to remove legacy project account storage file after migration" , {
347- path : currentLegacyProjectStoragePath ,
349+ log . warn ( ` Failed to remove legacy ${ label } after migration` , {
350+ path : legacyPath ,
348351 error : String ( unlinkError ) ,
349352 } ) ;
350353 }
351354 }
352- log . info ( " Migrated legacy project account storage" , {
353- from : currentLegacyProjectStoragePath ,
354- to : currentStoragePath ,
355+ log . info ( ` Migrated legacy ${ label } ` , {
356+ from : legacyPath ,
357+ to : nextPath ,
355358 accounts : normalized . accounts . length ,
356359 } ) ;
357360 return normalized ;
358361 } catch ( error ) {
359- log . warn ( " Failed to migrate legacy project account storage" , {
360- from : currentLegacyProjectStoragePath ,
361- to : currentStoragePath ,
362+ log . warn ( ` Failed to migrate legacy ${ label } ` , {
363+ from : legacyPath ,
364+ to : nextPath ,
362365 error : String ( error ) ,
363366 } ) ;
364367 return null ;
365368 }
366369}
367370
371+ async function migrateLegacyProjectStorageIfNeeded (
372+ persist : ( storage : AccountStorageV3 ) => Promise < void > = saveAccounts ,
373+ ) : Promise < AccountStorageV3 | null > {
374+ return migrateStorageFileIfNeeded (
375+ currentLegacyProjectStoragePath ,
376+ getStoragePath ( ) ,
377+ persist ,
378+ "project account storage" ,
379+ ) ;
380+ }
381+
368382function selectNewestAccount < T extends AccountLike > (
369383 current : T | undefined ,
370384 candidate : T ,
@@ -708,6 +722,24 @@ function getGlobalAccountsStoragePath(): string {
708722 return join ( getConfigDir ( ) , ACCOUNTS_FILE_NAME ) ;
709723}
710724
725+ function getLegacyGlobalAccountsStoragePath ( ) : string {
726+ return join ( getConfigDir ( ) , LEGACY_ACCOUNTS_FILE_NAME ) ;
727+ }
728+
729+ async function migrateLegacyGlobalStorageIfNeeded ( ) : Promise < AccountStorageV3 | null > {
730+ const nextPath = getGlobalAccountsStoragePath ( ) ;
731+ const persistGlobalStorage = async ( storage : AccountStorageV3 ) : Promise < void > => {
732+ await writeAccountsToPathUnlocked ( nextPath , storage ) ;
733+ } ;
734+
735+ return migrateStorageFileIfNeeded (
736+ getLegacyGlobalAccountsStoragePath ( ) ,
737+ nextPath ,
738+ persistGlobalStorage ,
739+ "global account storage" ,
740+ ) ;
741+ }
742+
711743/**
712744 * Returns true when project-scoped storage is active and a global fallback is meaningful.
713745 */
@@ -724,6 +756,11 @@ async function loadGlobalAccountsFallback(): Promise<AccountStorageV3 | null> {
724756 return null ;
725757 }
726758
759+ const migrated = await migrateLegacyGlobalStorageIfNeeded ( ) ;
760+ if ( migrated ) {
761+ return migrated ;
762+ }
763+
727764 const globalStoragePath = getGlobalAccountsStoragePath ( ) ;
728765 if ( globalStoragePath === currentStoragePath ) {
729766 return null ;
@@ -802,6 +839,13 @@ async function loadAccountsInternal(
802839 ? await migrateLegacyProjectStorageIfNeeded ( persistMigration )
803840 : null ;
804841 if ( migrated ) return migrated ;
842+ if ( ! shouldUseProjectGlobalFallback ( ) ) {
843+ const migratedGlobal = persistMigration
844+ ? await migrateLegacyGlobalStorageIfNeeded ( )
845+ : null ;
846+ if ( migratedGlobal ) return migratedGlobal ;
847+ return null ;
848+ }
805849 const globalFallback = await loadGlobalAccountsFallback ( ) ;
806850 if ( ! globalFallback ) return null ;
807851
@@ -847,8 +891,7 @@ async function loadAccountsInternal(
847891 * Writes account storage without acquiring the outer storage mutex.
848892 * Callers must already be inside withStorageLock when using this helper directly.
849893 */
850- async function saveAccountsUnlocked ( storage : AccountStorageV3 ) : Promise < void > {
851- const path = getStoragePath ( ) ;
894+ async function writeAccountsToPathUnlocked ( path : string , storage : AccountStorageV3 ) : Promise < void > {
852895 const uniqueSuffix = `${ Date . now ( ) } .${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ;
853896 const tempPath = `${ path } .${ uniqueSuffix } .tmp` ;
854897
@@ -897,6 +940,10 @@ async function saveAccountsUnlocked(storage: AccountStorageV3): Promise<void> {
897940 }
898941}
899942
943+ async function saveAccountsUnlocked ( storage : AccountStorageV3 ) : Promise < void > {
944+ await writeAccountsToPathUnlocked ( getStoragePath ( ) , storage ) ;
945+ }
946+
900947/**
901948 * Executes a read-modify-write transaction under the storage lock and exposes
902949 * an unlocked persist callback so nested save operations do not deadlock.
@@ -1057,37 +1104,40 @@ async function loadFlaggedAccountsUnlocked(
10571104 }
10581105 }
10591106
1060- const legacyPath = getLegacyFlaggedAccountsPath ( ) ;
1061- if ( ! existsSync ( legacyPath ) ) {
1062- return empty ;
1063- }
1064-
1065- try {
1066- const legacyContent = await fs . readFile ( legacyPath , "utf-8" ) ;
1067- const legacyData = JSON . parse ( legacyContent ) as unknown ;
1068- const migrated = normalizeFlaggedStorage ( legacyData ) ;
1069- if ( migrated . accounts . length > 0 ) {
1070- await saveUnlocked ( migrated ) ;
1107+ for ( const legacyPath of [ getLegacyFlaggedAccountsPath ( ) , getLegacyBlockedAccountsPath ( ) ] ) {
1108+ if ( ! existsSync ( legacyPath ) ) {
1109+ continue ;
10711110 }
1111+
10721112 try {
1073- await fs . unlink ( legacyPath ) ;
1074- } catch {
1075- // Best effort cleanup.
1113+ const legacyContent = await fs . readFile ( legacyPath , "utf-8" ) ;
1114+ const legacyData = JSON . parse ( legacyContent ) as unknown ;
1115+ const migrated = normalizeFlaggedStorage ( legacyData ) ;
1116+ if ( migrated . accounts . length > 0 ) {
1117+ await saveUnlocked ( migrated ) ;
1118+ }
1119+ try {
1120+ await fs . unlink ( legacyPath ) ;
1121+ } catch {
1122+ // Best effort cleanup.
1123+ }
1124+ log . info ( "Migrated legacy flagged account storage" , {
1125+ from : legacyPath ,
1126+ to : path ,
1127+ accounts : migrated . accounts . length ,
1128+ } ) ;
1129+ return migrated ;
1130+ } catch ( error ) {
1131+ log . error ( "Failed to migrate legacy flagged account storage" , {
1132+ from : legacyPath ,
1133+ to : path ,
1134+ error : String ( error ) ,
1135+ } ) ;
1136+ return empty ;
10761137 }
1077- log . info ( "Migrated legacy flagged account storage" , {
1078- from : legacyPath ,
1079- to : path ,
1080- accounts : migrated . accounts . length ,
1081- } ) ;
1082- return migrated ;
1083- } catch ( error ) {
1084- log . error ( "Failed to migrate legacy flagged account storage" , {
1085- from : legacyPath ,
1086- to : path ,
1087- error : String ( error ) ,
1088- } ) ;
1089- return empty ;
10901138 }
1139+
1140+ return empty ;
10911141}
10921142
10931143export async function loadFlaggedAccounts ( ) : Promise < FlaggedAccountStorageV1 > {
0 commit comments