@@ -196,6 +196,17 @@ function createConfigObserveAuditAppendParams(
196196 } ;
197197}
198198
199+ function createConfigObserveAnomalyAuditAppendParams (
200+ deps : ObserveRecoveryDeps ,
201+ params : Omit < ConfigObserveAuditRecordParams , "restoredFromBackup" | "restoredBackupPath" > ,
202+ ) {
203+ return createConfigObserveAuditAppendParams ( deps , {
204+ ...params ,
205+ restoredFromBackup : false ,
206+ restoredBackupPath : null ,
207+ } ) ;
208+ }
209+
199210function hashConfigRaw ( raw : string | null ) : string {
200211 return crypto
201212 . createHash ( "sha256" )
@@ -283,6 +294,14 @@ function createConfigHealthFingerprint(params: {
283294 } ;
284295}
285296
297+ function parseConfigRawOrEmpty ( deps : ObserveRecoveryDeps , raw : string ) : unknown {
298+ try {
299+ return deps . json5 . parse ( raw ) ;
300+ } catch {
301+ return { } ;
302+ }
303+ }
304+
286305function resolveConfigHealthStatePath ( env : NodeJS . ProcessEnv , homedir : ( ) => string ) : string {
287306 return path . join ( resolveStateDir ( env , homedir ) , "logs" , "config-health.json" ) ;
288307}
@@ -358,6 +377,16 @@ function setConfigHealthEntry(
358377 } ;
359378}
360379
380+ function createLastObservedSuspiciousEntry (
381+ entry : ConfigHealthEntry ,
382+ suspiciousSignature : string ,
383+ ) : ConfigHealthEntry {
384+ return {
385+ ...entry ,
386+ lastObservedSuspiciousSignature : suspiciousSignature ,
387+ } ;
388+ }
389+
361390function isUpdateChannelOnlyRoot ( value : unknown ) : boolean {
362391 if ( ! isRecord ( value ) ) {
363392 return false ;
@@ -401,27 +430,52 @@ function resolveConfigObserveSuspiciousReasons(params: {
401430 return reasons ;
402431}
403432
433+ function resolveSuspiciousSignature (
434+ current : ConfigHealthFingerprint ,
435+ suspicious : string [ ] ,
436+ ) : string {
437+ return `${ current . hash } :${ suspicious . join ( "," ) } ` ;
438+ }
439+
440+ function resolveConfigReadRecoveryContext ( params : {
441+ current : ConfigHealthFingerprint ;
442+ parsed : unknown ;
443+ entry : ConfigHealthEntry ;
444+ backupBaseline ?: ConfigHealthFingerprint ;
445+ } ) : { suspicious : string [ ] ; suspiciousSignature : string } | null {
446+ const suspicious = resolveConfigObserveSuspiciousReasons ( {
447+ bytes : params . current . bytes ,
448+ hasMeta : params . current . hasMeta ,
449+ gatewayMode : params . current . gatewayMode ,
450+ parsed : params . parsed ,
451+ lastKnownGood : params . backupBaseline ,
452+ } ) ;
453+ if ( ! suspicious . includes ( "update-channel-only-root" ) ) {
454+ return null ;
455+ }
456+ const suspiciousSignature = resolveSuspiciousSignature ( params . current , suspicious ) ;
457+ if ( params . entry . lastObservedSuspiciousSignature === suspiciousSignature ) {
458+ return null ;
459+ }
460+ return { suspicious, suspiciousSignature } ;
461+ }
462+
404463async function readConfigFingerprintForPath (
405464 deps : ObserveRecoveryDeps ,
406465 targetPath : string ,
407466) : Promise < ConfigHealthFingerprint | null > {
408467 try {
409468 const raw = await deps . fs . promises . readFile ( targetPath , "utf-8" ) ;
410469 const stat = await deps . fs . promises . stat ( targetPath ) . catch ( ( ) => null ) ;
411- let parsed : unknown = { } ;
412- try {
413- parsed = deps . json5 . parse ( raw ) ;
414- } catch { }
415- return {
470+ const parsed = parseConfigRawOrEmpty ( deps , raw ) ;
471+ return createConfigHealthFingerprint ( {
416472 hash : hashConfigRaw ( raw ) ,
417- bytes : Buffer . byteLength ( raw , "utf-8" ) ,
418- mtimeMs : stat ?. mtimeMs ?? null ,
419- ctimeMs : stat ?. ctimeMs ?? null ,
420- ...resolveConfigStatMetadata ( stat as Record < string , unknown > | null ) ,
421- hasMeta : hasConfigMeta ( parsed ) ,
422- gatewayMode : resolveGatewayMode ( parsed ) ,
473+ raw,
474+ parsed,
475+ gatewaySource : parsed ,
476+ stat : stat as ConfigStatMetadataSource ,
423477 observedAt : new Date ( ) . toISOString ( ) ,
424- } ;
478+ } ) ;
425479 } catch {
426480 return null ;
427481 }
@@ -434,20 +488,15 @@ function readConfigFingerprintForPathSync(
434488 try {
435489 const raw = deps . fs . readFileSync ( targetPath , "utf-8" ) ;
436490 const stat = deps . fs . statSync ( targetPath , { throwIfNoEntry : false } ) ?? null ;
437- let parsed : unknown = { } ;
438- try {
439- parsed = deps . json5 . parse ( raw ) ;
440- } catch { }
441- return {
491+ const parsed = parseConfigRawOrEmpty ( deps , raw ) ;
492+ return createConfigHealthFingerprint ( {
442493 hash : hashConfigRaw ( raw ) ,
443- bytes : Buffer . byteLength ( raw , "utf-8" ) ,
444- mtimeMs : stat ?. mtimeMs ?? null ,
445- ctimeMs : stat ?. ctimeMs ?? null ,
446- ...resolveConfigStatMetadata ( stat ) ,
447- hasMeta : hasConfigMeta ( parsed ) ,
448- gatewayMode : resolveGatewayMode ( parsed ) ,
494+ raw,
495+ parsed,
496+ gatewaySource : parsed ,
497+ stat,
449498 observedAt : new Date ( ) . toISOString ( ) ,
450- } ;
499+ } ) ;
451500 } catch {
452501 return null ;
453502 }
@@ -519,21 +568,16 @@ export async function maybeRecoverSuspiciousConfigRead(params: {
519568 entry . lastKnownGood ??
520569 ( await readConfigFingerprintForPath ( params . deps , backupPath ) ) ??
521570 undefined ;
522- const suspicious = resolveConfigObserveSuspiciousReasons ( {
523- bytes : current . bytes ,
524- hasMeta : current . hasMeta ,
525- gatewayMode : current . gatewayMode ,
571+ const recoveryContext = resolveConfigReadRecoveryContext ( {
572+ current,
526573 parsed : params . parsed ,
527- lastKnownGood : backupBaseline ,
574+ entry,
575+ backupBaseline,
528576 } ) ;
529- if ( ! suspicious . includes ( "update-channel-only-root" ) ) {
530- return { raw : params . raw , parsed : params . parsed } ;
531- }
532-
533- const suspiciousSignature = `${ current . hash } :${ suspicious . join ( "," ) } ` ;
534- if ( entry . lastObservedSuspiciousSignature === suspiciousSignature ) {
577+ if ( ! recoveryContext ) {
535578 return { raw : params . raw , parsed : params . parsed } ;
536579 }
580+ const { suspicious, suspiciousSignature } = recoveryContext ;
537581
538582 const backupRaw = await params . deps . fs . promises . readFile ( backupPath , "utf-8" ) . catch ( ( ) => null ) ;
539583 if ( ! backupRaw ) {
@@ -581,10 +625,11 @@ export async function maybeRecoverSuspiciousConfigRead(params: {
581625 } ) ,
582626 ) ;
583627
584- healthState = setConfigHealthEntry ( healthState , params . configPath , {
585- ...entry ,
586- lastObservedSuspiciousSignature : suspiciousSignature ,
587- } ) ;
628+ healthState = setConfigHealthEntry (
629+ healthState ,
630+ params . configPath ,
631+ createLastObservedSuspiciousEntry ( entry , suspiciousSignature ) ,
632+ ) ;
588633 await writeConfigHealthState ( params . deps , healthState ) ;
589634 return { raw : backupRaw , parsed : backupParsed } ;
590635}
@@ -611,21 +656,16 @@ export function maybeRecoverSuspiciousConfigReadSync(params: {
611656 const backupPath = `${ params . configPath } .bak` ;
612657 const backupBaseline =
613658 entry . lastKnownGood ?? readConfigFingerprintForPathSync ( params . deps , backupPath ) ?? undefined ;
614- const suspicious = resolveConfigObserveSuspiciousReasons ( {
615- bytes : current . bytes ,
616- hasMeta : current . hasMeta ,
617- gatewayMode : current . gatewayMode ,
659+ const recoveryContext = resolveConfigReadRecoveryContext ( {
660+ current,
618661 parsed : params . parsed ,
619- lastKnownGood : backupBaseline ,
662+ entry,
663+ backupBaseline,
620664 } ) ;
621- if ( ! suspicious . includes ( "update-channel-only-root" ) ) {
622- return { raw : params . raw , parsed : params . parsed } ;
623- }
624-
625- const suspiciousSignature = `${ current . hash } :${ suspicious . join ( "," ) } ` ;
626- if ( entry . lastObservedSuspiciousSignature === suspiciousSignature ) {
665+ if ( ! recoveryContext ) {
627666 return { raw : params . raw , parsed : params . parsed } ;
628667 }
668+ const { suspicious, suspiciousSignature } = recoveryContext ;
629669
630670 let backupRaw : string ;
631671 try {
@@ -675,10 +715,11 @@ export function maybeRecoverSuspiciousConfigReadSync(params: {
675715 } ) ,
676716 ) ;
677717
678- healthState = setConfigHealthEntry ( healthState , params . configPath , {
679- ...entry ,
680- lastObservedSuspiciousSignature : suspiciousSignature ,
681- } ) ;
718+ healthState = setConfigHealthEntry (
719+ healthState ,
720+ params . configPath ,
721+ createLastObservedSuspiciousEntry ( entry , suspiciousSignature ) ,
722+ ) ;
682723 writeConfigHealthStateSync ( params . deps , healthState ) ;
683724 return { raw : backupRaw , parsed : backupParsed } ;
684725}
@@ -744,7 +785,7 @@ export async function observeConfigSnapshot(
744785 return ;
745786 }
746787
747- const suspiciousSignature = ` ${ current . hash } : ${ suspicious . join ( "," ) } ` ;
788+ const suspiciousSignature = resolveSuspiciousSignature ( current , suspicious ) ;
748789 if ( entry . lastObservedSuspiciousSignature === suspiciousSignature ) {
749790 return ;
750791 }
@@ -761,7 +802,7 @@ export async function observeConfigSnapshot(
761802
762803 deps . logger . warn ( `Config observe anomaly: ${ snapshot . path } (${ suspicious . join ( ", " ) } )` ) ;
763804 await appendConfigAuditRecord (
764- createConfigObserveAuditAppendParams ( deps , {
805+ createConfigObserveAnomalyAuditAppendParams ( deps , {
765806 ts : now ,
766807 configPath : snapshot . path ,
767808 valid : snapshot . valid ,
@@ -770,15 +811,14 @@ export async function observeConfigSnapshot(
770811 lastKnownGood : entry . lastKnownGood ,
771812 backup,
772813 clobberedPath,
773- restoredFromBackup : false ,
774- restoredBackupPath : null ,
775814 } ) ,
776815 ) ;
777816
778- healthState = setConfigHealthEntry ( healthState , snapshot . path , {
779- ...entry ,
780- lastObservedSuspiciousSignature : suspiciousSignature ,
781- } ) ;
817+ healthState = setConfigHealthEntry (
818+ healthState ,
819+ snapshot . path ,
820+ createLastObservedSuspiciousEntry ( entry , suspiciousSignature ) ,
821+ ) ;
782822 await writeConfigHealthState ( deps , healthState ) ;
783823}
784824
@@ -826,7 +866,7 @@ export function observeConfigSnapshotSync(
826866 return ;
827867 }
828868
829- const suspiciousSignature = ` ${ current . hash } : ${ suspicious . join ( "," ) } ` ;
869+ const suspiciousSignature = resolveSuspiciousSignature ( current , suspicious ) ;
830870 if ( entry . lastObservedSuspiciousSignature === suspiciousSignature ) {
831871 return ;
832872 }
@@ -843,7 +883,7 @@ export function observeConfigSnapshotSync(
843883
844884 deps . logger . warn ( `Config observe anomaly: ${ snapshot . path } (${ suspicious . join ( ", " ) } )` ) ;
845885 appendConfigAuditRecordSync (
846- createConfigObserveAuditAppendParams ( deps , {
886+ createConfigObserveAnomalyAuditAppendParams ( deps , {
847887 ts : now ,
848888 configPath : snapshot . path ,
849889 valid : snapshot . valid ,
@@ -852,14 +892,13 @@ export function observeConfigSnapshotSync(
852892 lastKnownGood : entry . lastKnownGood ,
853893 backup,
854894 clobberedPath,
855- restoredFromBackup : false ,
856- restoredBackupPath : null ,
857895 } ) ,
858896 ) ;
859897
860- healthState = setConfigHealthEntry ( healthState , snapshot . path , {
861- ...entry ,
862- lastObservedSuspiciousSignature : suspiciousSignature ,
863- } ) ;
898+ healthState = setConfigHealthEntry (
899+ healthState ,
900+ snapshot . path ,
901+ createLastObservedSuspiciousEntry ( entry , suspiciousSignature ) ,
902+ ) ;
864903 writeConfigHealthStateSync ( deps , healthState ) ;
865904}
0 commit comments