@@ -205,6 +205,22 @@ export interface ActionableNamedBackupRecoveries {
205205 totalBackups : number ;
206206}
207207
208+ export type AccountSnapshotReason =
209+ | "delete-account"
210+ | "delete-saved-accounts"
211+ | "reset-local-state"
212+ | "import-accounts" ;
213+
214+ export type AccountSnapshotFailurePolicy = "warn" | "error" ;
215+
216+ export interface AccountSnapshotOptions {
217+ reason : AccountSnapshotReason ;
218+ now ?: number ;
219+ force ?: boolean ;
220+ failurePolicy ?: AccountSnapshotFailurePolicy ;
221+ createBackup ?: typeof createNamedBackup ;
222+ }
223+
208224interface LoadedBackupCandidate {
209225 normalized : AccountStorageV3 | null ;
210226 storedVersion : unknown ;
@@ -2155,6 +2171,83 @@ export async function createNamedBackup(
21552171 ) ;
21562172}
21572173
2174+ function formatTimestampForSnapshot ( timestamp : number ) : string {
2175+ const date = new Date ( timestamp ) ;
2176+ const pad = ( value : number ) : string => value . toString ( ) . padStart ( 2 , "0" ) ;
2177+ const milliseconds = date . getUTCMilliseconds ( ) . toString ( ) . padStart ( 3 , "0" ) ;
2178+ const year = date . getUTCFullYear ( ) ;
2179+ const month = pad ( date . getUTCMonth ( ) + 1 ) ;
2180+ const day = pad ( date . getUTCDate ( ) ) ;
2181+ const hours = pad ( date . getUTCHours ( ) ) ;
2182+ const minutes = pad ( date . getUTCMinutes ( ) ) ;
2183+ const seconds = pad ( date . getUTCSeconds ( ) ) ;
2184+ return `${ year } -${ month } -${ day } _${ hours } -${ minutes } -${ seconds } _${ milliseconds } ` ;
2185+ }
2186+
2187+ function buildAccountSnapshotName (
2188+ reason : AccountSnapshotReason ,
2189+ timestamp : number ,
2190+ ) : string {
2191+ return `accounts-${ reason } -snapshot-${ formatTimestampForSnapshot ( timestamp ) } ` ;
2192+ }
2193+
2194+ function extractPathTail ( pathValue : string ) : string {
2195+ const segments = pathValue . split ( / [ \\ / ] + / ) . filter ( Boolean ) ;
2196+ return segments . at ( - 1 ) ?? pathValue ;
2197+ }
2198+
2199+ function redactFilesystemDetails ( value : string ) : string {
2200+ return value . replace (
2201+ / (?: [ A - Z a - z ] : ) ? [ \\ / ] [ ^ " ' ` \r \n ] + (?: [ \\ / ] [ ^ " ' ` \r \n ] + ) + / g,
2202+ ( pathValue ) => extractPathTail ( pathValue ) ,
2203+ ) ;
2204+ }
2205+
2206+ function formatSnapshotErrorForLog ( error : unknown ) : string {
2207+ const code =
2208+ typeof ( error as NodeJS . ErrnoException | undefined ) ?. code === "string"
2209+ ? ( error as NodeJS . ErrnoException ) . code
2210+ : undefined ;
2211+ const rawMessage =
2212+ error instanceof Error ? error . message : String ( error ?? "unknown error" ) ;
2213+ const redactedMessage = redactFilesystemDetails ( rawMessage ) ;
2214+ if ( code && ! redactedMessage . includes ( code ) ) {
2215+ return `${ code } : ${ redactedMessage } ` ;
2216+ }
2217+ return redactedMessage ;
2218+ }
2219+
2220+ export async function snapshotAccountStorage (
2221+ options : AccountSnapshotOptions ,
2222+ ) : Promise < NamedBackupMetadata | null > {
2223+ const {
2224+ reason,
2225+ now = Date . now ( ) ,
2226+ force = true ,
2227+ failurePolicy = "warn" ,
2228+ createBackup = createNamedBackup ,
2229+ } = options ;
2230+ const currentStorage = await loadAccounts ( ) ;
2231+ if ( ! currentStorage || currentStorage . accounts . length === 0 ) {
2232+ return null ;
2233+ }
2234+
2235+ const backupName = buildAccountSnapshotName ( reason , now ) ;
2236+ try {
2237+ return await createBackup ( backupName , { force } ) ;
2238+ } catch ( error ) {
2239+ if ( failurePolicy === "error" ) {
2240+ throw error ;
2241+ }
2242+ log . warn ( "Failed to create account storage snapshot" , {
2243+ reason,
2244+ backupName,
2245+ error : formatSnapshotErrorForLog ( error ) ,
2246+ } ) ;
2247+ return null ;
2248+ }
2249+ }
2250+
21582251export async function assessNamedBackupRestore (
21592252 name : string ,
21602253 options : { currentStorage ?: AccountStorageV3 | null } = { } ,
@@ -3538,6 +3631,10 @@ export async function importAccounts(
35383631 throw new Error ( "Invalid account storage format" ) ;
35393632 }
35403633
3634+ // Capture the current pool before merge/limit checks so failed imports still
3635+ // leave a rollback-ready checkpoint for the pre-import state.
3636+ await snapshotAccountStorage ( { reason : "import-accounts" } ) ;
3637+
35413638 const {
35423639 imported : importedCount ,
35433640 total,
0 commit comments