1- import { createHash } from "node:crypto" ;
21import { existsSync , promises as fs } from "node:fs" ;
32import { basename , dirname , join } from "node:path" ;
43import { ACCOUNT_LIMITS } from "./constants.js" ;
@@ -11,14 +10,24 @@ import {
1110} from "./named-backup-export.js" ;
1211import { MODEL_FAMILIES , type ModelFamily } from "./prompts/codex.js" ;
1312import { clearAccountStorageArtifacts } from "./storage/account-clear.js" ;
13+ import {
14+ collectDistinctIdentityValues ,
15+ findNewestMatchingIndex as findNewestMatchingIndexByRef ,
16+ selectNewestAccount ,
17+ } from "./storage/account-match-utils.js" ;
1418import { cloneAccountStorageForPersistence } from "./storage/account-persistence.js" ;
19+ import {
20+ describeAccountSnapshot as describeAccountSnapshotWithDeps ,
21+ statSnapshot as statSnapshotWithDeps ,
22+ } from "./storage/account-snapshot.js" ;
1523import { buildBackupMetadata } from "./storage/backup-metadata-builder.js" ;
1624import {
1725 type BackupMetadataSection ,
1826 type BackupSnapshotKind ,
1927 type BackupSnapshotMetadata ,
2028 buildMetadataSection ,
2129} from "./storage/backup-metadata.js" ;
30+ import { isCacheLikeBackupArtifactName } from "./storage/cache-artifacts.js" ;
2231import {
2332 ACCOUNTS_BACKUP_SUFFIX ,
2433 ACCOUNTS_WAL_SUFFIX ,
@@ -30,12 +39,14 @@ import {
3039} from "./storage/backup-paths.js" ;
3140import { restoreAccountsFromBackupPath } from "./storage/backup-restore.js" ;
3241import { looksLikeSyntheticFixtureStorage } from "./storage/fixture-guards.js" ;
42+ import { loadFlaggedAccountsFromFile } from "./storage/flagged-storage-file.js" ;
3343import { normalizeFlaggedStorage } from "./storage/flagged-storage.js" ;
3444import {
3545 clearFlaggedAccountsOnDisk ,
3646 loadFlaggedAccountsState ,
3747 saveFlaggedAccountsUnlockedToDisk ,
3848} from "./storage/flagged-storage-io.js" ;
49+ import { computeSha256 } from "./storage/hash.js" ;
3950import {
4051 exportAccountsToFile ,
4152 mergeImportedAccounts ,
@@ -85,7 +96,12 @@ import {
8596 loadNormalizedStorageFromPath ,
8697 mergeStorageForMigration ,
8798} from "./storage/project-migration.js" ;
99+ import { clampIndex , isRecord } from "./storage/record-utils.js" ;
88100import { buildRestoreAssessment } from "./storage/restore-assessment.js" ;
101+ import {
102+ createEmptyStorageWithRestoreMetadata as createEmptyStorageWithMetadata ,
103+ withRestoreMetadata ,
104+ } from "./storage/restore-metadata.js" ;
89105import {
90106 loadAccountsFromPath ,
91107 parseAndNormalizeStorage ,
@@ -130,11 +146,6 @@ export interface FlaggedAccountStorageV1 {
130146
131147type RestoreReason = "empty-storage" | "intentional-reset" | "missing-storage" ;
132148
133- type AccountStorageWithMetadata = AccountStorageV3 & {
134- restoreEligible ?: boolean ;
135- restoreReason ?: RestoreReason ;
136- } ;
137-
138149export type BackupMetadata = {
139150 accounts : BackupMetadataSection ;
140151 flaggedAccounts : BackupMetadataSection ;
@@ -418,105 +429,32 @@ async function cleanupStaleRotatingBackupArtifacts(
418429 }
419430}
420431
421- function computeSha256 ( value : string ) : string {
422- return createHash ( "sha256" ) . update ( value ) . digest ( "hex" ) ;
423- }
424-
425- function createEmptyStorageWithMetadata (
426- restoreEligible : boolean ,
427- restoreReason : RestoreReason ,
428- ) : AccountStorageWithMetadata {
429- return {
430- version : 3 ,
431- accounts : [ ] ,
432- activeIndex : 0 ,
433- activeIndexByFamily : { } ,
434- restoreEligible,
435- restoreReason,
436- } ;
437- }
438-
439- function withRestoreMetadata (
440- storage : AccountStorageV3 ,
441- restoreEligible : boolean ,
442- restoreReason : RestoreReason ,
443- ) : AccountStorageWithMetadata {
444- return {
445- ...storage ,
446- restoreEligible,
447- restoreReason,
448- } ;
449- }
450-
451- function isCacheLikeBackupArtifactName ( entryName : string ) : boolean {
452- return entryName . toLowerCase ( ) . includes ( ".cache" ) ;
453- }
454-
455432async function statSnapshot ( path : string ) : Promise < {
456433 exists : boolean ;
457434 bytes ?: number ;
458435 mtimeMs ?: number ;
459436} > {
460- try {
461- const stats = await fs . stat ( path ) ;
462- return { exists : true , bytes : stats . size , mtimeMs : stats . mtimeMs } ;
463- } catch ( error ) {
464- const code = ( error as NodeJS . ErrnoException ) . code ;
465- if ( code !== "ENOENT" ) {
466- log . warn ( "Failed to stat backup candidate" , {
467- path,
468- error : String ( error ) ,
469- } ) ;
470- }
471- return { exists : false } ;
472- }
437+ return statSnapshotWithDeps ( path , {
438+ stat : fs . stat ,
439+ logWarn : ( message , meta ) => log . warn ( message , meta ) ,
440+ } ) ;
473441}
474442
475443async function describeAccountSnapshot (
476444 path : string ,
477445 kind : BackupSnapshotKind ,
478446 index ?: number ,
479447) : Promise < BackupSnapshotMetadata > {
480- const stats = await statSnapshot ( path ) ;
481- if ( ! stats . exists ) {
482- return { kind, path, index, exists : false , valid : false } ;
483- }
484- try {
485- const { normalized, schemaErrors, storedVersion } =
486- await loadAccountsFromPath ( path , {
448+ return describeAccountSnapshotWithDeps ( path , kind , {
449+ index,
450+ statSnapshot,
451+ loadAccountsFromPath : ( targetPath ) =>
452+ loadAccountsFromPath ( targetPath , {
487453 normalizeAccountStorage,
488454 isRecord,
489- } ) ;
490- return {
491- kind,
492- path,
493- index,
494- exists : true ,
495- valid : ! ! normalized ,
496- bytes : stats . bytes ,
497- mtimeMs : stats . mtimeMs ,
498- version : typeof storedVersion === "number" ? storedVersion : undefined ,
499- accountCount : normalized ?. accounts . length ,
500- schemaErrors : schemaErrors . length > 0 ? schemaErrors : undefined ,
501- } ;
502- } catch ( error ) {
503- const code = ( error as NodeJS . ErrnoException ) . code ;
504- if ( code !== "ENOENT" ) {
505- log . warn ( "Failed to inspect account snapshot" , {
506- path,
507- error : String ( error ) ,
508- } ) ;
509- }
510- return {
511- kind,
512- path,
513- index,
514- exists : true ,
515- valid : false ,
516- bytes : stats . bytes ,
517- mtimeMs : stats . mtimeMs ,
518- } ;
519- }
455+ } ) ,
456+ logWarn : ( message , meta ) => log . warn ( message , meta ) ,
457+ } ) ;
520458}
521459
522460async function describeAccountsWalSnapshot (
@@ -587,11 +525,13 @@ async function describeAccountsWalSnapshot(
587525async function loadFlaggedAccountsFromPath (
588526 path : string ,
589527) : Promise < FlaggedAccountStorageV1 > {
590- const content = await fs . readFile ( path , "utf-8" ) ;
591- const data = JSON . parse ( content ) as unknown ;
592- return normalizeFlaggedStorage ( data , {
593- isRecord,
594- now : ( ) => Date . now ( ) ,
528+ return loadFlaggedAccountsFromFile ( path , {
529+ readFile : fs . readFile ,
530+ normalizeFlaggedStorage : ( data ) =>
531+ normalizeFlaggedStorage ( data , {
532+ isRecord,
533+ now : ( ) => Date . now ( ) ,
534+ } ) ,
595535 } ) ;
596536}
597537
@@ -896,57 +836,20 @@ async function migrateLegacyProjectStorageIfNeeded(
896836 return null ;
897837}
898838
899- function selectNewestAccount < T extends AccountLike > (
900- current : T | undefined ,
901- candidate : T ,
902- ) : T {
903- if ( ! current ) return candidate ;
904- const currentLastUsed = current . lastUsed || 0 ;
905- const candidateLastUsed = candidate . lastUsed || 0 ;
906- if ( candidateLastUsed > currentLastUsed ) return candidate ;
907- if ( candidateLastUsed < currentLastUsed ) return current ;
908- const currentAddedAt = current . addedAt || 0 ;
909- const candidateAddedAt = candidate . addedAt || 0 ;
910- return candidateAddedAt >= currentAddedAt ? candidate : current ;
911- }
912-
913839type AccountMatchOptions = {
914840 allowUniqueAccountIdFallbackWithoutEmail ?: boolean ;
915841} ;
916842
917- function collectDistinctIdentityValues (
918- values : Array < string | undefined > ,
919- ) : Set < string > {
920- const distinct = new Set < string > ( ) ;
921- for ( const value of values ) {
922- if ( value ) distinct . add ( value ) ;
923- }
924- return distinct ;
925- }
926-
927843function findNewestMatchingIndex < T extends AccountLike > (
928844 accounts : readonly T [ ] ,
929845 predicate : ( ref : AccountIdentityRef ) => boolean ,
930846) : number | undefined {
931- let matchIndex : number | undefined ;
932- let match : T | undefined ;
933- for ( let i = 0 ; i < accounts . length ; i += 1 ) {
934- const account = accounts [ i ] ;
935- if ( ! account ) continue ;
936- const ref = toAccountIdentityRef ( account ) ;
937- if ( ! predicate ( ref ) ) continue ;
938- if ( matchIndex === undefined ) {
939- matchIndex = i ;
940- match = account ;
941- continue ;
942- }
943- const newest = selectNewestAccount ( match , account ) ;
944- if ( newest === account ) {
945- matchIndex = i ;
946- match = account ;
947- }
948- }
949- return matchIndex ;
847+ return findNewestMatchingIndexByRef (
848+ accounts ,
849+ ( account ) => toAccountIdentityRef ( account ) ,
850+ predicate ,
851+ selectNewestAccount ,
852+ ) ;
950853}
951854
952855function findCompositeAccountMatchIndex < T extends AccountLike > (
@@ -1165,15 +1068,6 @@ export function deduplicateAccountsByEmail<
11651068 return deduplicateAccountsByIdentity ( accounts ) ;
11661069}
11671070
1168- function isRecord ( value : unknown ) : value is Record < string , unknown > {
1169- return ! ! value && typeof value === "object" && ! Array . isArray ( value ) ;
1170- }
1171-
1172- function clampIndex ( index : number , length : number ) : number {
1173- if ( length <= 0 ) return 0 ;
1174- return Math . max ( 0 , Math . min ( index , length - 1 ) ) ;
1175- }
1176-
11771071function extractActiveAccountRef (
11781072 accounts : unknown [ ] ,
11791073 activeIndex : number ,
0 commit comments