import { NodeCG , ReplicantServer } from "nodecg-types/types/server" ; import { InstanceManager } from "./instanceManager" ; import { BundleManager } from "./bundleManager" ; import * as crypto from "crypto-js" ; import { emptySuccess , error , Result , success } from "./utils/result" ; import { ObjectMap , ServiceDependency , ServiceInstance } from "./service" ; import { ServiceManager } from "./serviceManager" ; /**
 * Models all the data that needs to be persistent in a plain manner.
 */ export interface PersistentData { /**
     * All instance data that is held by the  InstanceManager .
      instances : ObjectMap < ServiceInstance < unknown , unknown > > ; /**
     * All bundle dependency data that is held by the  BundleManager .
      bundleDependencies : ObjectMap < ServiceDependency < unknown > [ ] > ; } /**
 * Models all the data that needs to be persistent in an encrypted manner.
 */ export interface EncryptedData { /**
     * The encrypted format of the data that needs to be stored.
     */ cipherText ? : string ; } param cipherText param password export function decryptData ( cipherText , password ) { try { const decryptedBytes = crypto . AES . decrypt ( cipherText , password ) ; const decryptedText = decryptedBytes . toString ( crypto . enc . Utf8 ) ; const data = JSON . parse ( decryptedText ) ; return success ( data ) ; } catch { return error ( "Password isn't correct." ) ; } } /**
 * Manages encrypted persistence of data that is held by the instance and bundle managers.
 */ export class PersistenceManager { private password : string | undefined ; private encryptedData : ReplicantServer < EncryptedData > ; constructor ( private readonly nodecg , private readonly services , private readonly instances , private readonly bundles ) { this . encryptedData = nodecg . Replicant ( "encryptedConfig" , { persistent : true , defaultValue : { } } ) ; this . checkAutomaticLogin ( ) ; } param password checkPassword ( password ) { if ( this . isLoaded ( ) ) { return this . password === password ; } else { return false ; } } /**
     * Returns if the locally stored configuration has been loaded and a password has been set.
     */ isLoaded ( ) { return this . password !== undefined ; } /**
     * Returns whether this is the first startup aka. whether any encrypted data has been saved.
     * If this returns true { load } will accept any password and use it to encrypt the configuration.
      isFirstStartup ( ) { return this . encryptedData . value . cipherText === undefined ; } param password return async load ( password ) { if ( this . isLoaded ( ) ) { return error ( "Config has already been decrypted and loaded." ) ; } if ( this . encryptedData . value . cipherText === undefined ) { this . nodecg . log . info ( "No saved configuration found, creating a empty one." ) ; this . password = password ; this . save ( ) ; } else { this . nodecg . log . info ( "Decrypting and loading saved configuration." ) ; const data = decryptData ( this . encryptedData . value . cipherText , password ) ; if ( data . failed ) { return data ; } const promises = this . loadServiceInstances ( data . result . instances ) ; this . loadBundleDependencies ( data . result . bundleDependencies ) ; this . saveAfterServiceInstancesLoaded ( promises ) ; } this . password = password ; this . instances . on ( "change" , ( ) => this . save ( ) ) ; this . bundles . on ( "change" , ( ) => this . save ( ) ) ; return emptySuccess ( ) ; } param instances private loadServiceInstances ( instances ) { return Object . entries ( instances ) . map ( ( [ instanceName , instance ] ) => { const result = this . instances . createServiceInstance ( instance . serviceType , instanceName ) ; if ( result . failed ) { this . nodecg . log . warn ( `Couldn't load instance "${ instanceName }" from saved configuration: ${ result . errorMessage }` ) ; return Promise . resolve ( ) ; } const svc = this . services . getService ( instance . serviceType ) ; if ( ! svc . failed && svc . result . requiresNoConfig ) { return Promise . resolve ( ) ; } return this . instances . updateInstanceConfig ( instanceName , instance . config , false ) . then ( async ( result ) => { if ( result . failed ) { throw result . errorMessage ; } } ) . catch ( ( reason ) => { this . nodecg . log . warn ( `Couldn't load config of instance "${ instanceName }" from saved configuration: ${ reason }.` ) ; } ) ; } ) ; } param bundles private loadBundleDependencies ( bundles ) { Object . entries ( bundles ) . forEach ( ( [ bundleName , deps ] ) => { deps . forEach ( ( svcDep ) => { if ( svcDep . serviceInstance !== undefined ) { const inst = this . instances . getServiceInstance ( svcDep . serviceInstance ) ; if ( inst !== undefined ) { this . bundles . setServiceDependency ( bundleName , svcDep . serviceInstance , inst ) ; } } } ) ; } ) ; } /**
     * Encrypts and saves current state to the persistent replicant.
     */ save ( ) { if ( this . password === undefined ) { return ; } const data = { instances : this . getServiceInstances ( ) , bundleDependencies : this . bundles . getBundleDependencies ( ) } ; const cipherText = crypto . AES . encrypt ( JSON . stringify ( data ) , this . password ) ; this . encryptedData . value . cipherText = cipherText . toString ( ) ; } /**
     * Creates a copy of all service instances without the service clients, because those
     * shouldn't be serialized and don't need to be stored in the encrypted config file.
     */ private getServiceInstances ( ) { const instances = this . instances . getServiceInstances ( ) ; const copy = { } ; Object . entries ( instances ) . forEach ( ( [ instName , instance ] ) => { copy [ instName ] = { serviceType : instance . serviceType , config : instance . config , client : undefined } ; } ) ; return copy ; } param promises private async saveAfterServiceInstancesLoaded ( promises ) { const promisesWithoutErrs = promises . map ( ( prom ) => new Promise ( ( resolve ) => prom . then ( resolve ) . catch ( resolve ) ) ) ; await Promise . all ( promisesWithoutErrs ) ; this . nodecg . log . info ( "Finished creating service instances from stored configuration." ) ; this . save ( ) ; } /**
     * Checks whether automatic login is set up and enabled. If yes, it will do it using  PersistenceManager . setupAutomaticLogin .
      private checkAutomaticLogin ( ) { if ( this . nodecg . bundleConfig . automaticLogin ?. enabled === undefined ) { return ; } const enabled = this . nodecg . bundleConfig . automaticLogin . enabled ; const password = this . nodecg . bundleConfig . automaticLogin . password ; if ( enabled === false ) { this . nodecg . log . warn ( "Automatic login is setup but disabled." ) ; return ; } this . setupAutomaticLogin ( password ) ; } /**
     * Setups everything needed to automatically log in using the provided password after NodeCG has loaded.
     */ private setupAutomaticLogin ( password ) { this . nodecg . Replicant < unknown [ ] > ( "bundles" , "nodecg" ) . on ( "change" , async ( bundles ) => { if ( bundles . length > 0 ) { try { this . nodecg . log . info ( "Attempting to automatically login..." ) ; const loadResult = await this . load ( password ) ; if ( ! loadResult . failed ) { this . nodecg . log . info ( "Automatic login successful." ) ; } else { throw loadResult . errorMessage ; } } catch ( err ) { const logMesssage = `Failed to automatically login: ${ err }` ; if ( this . isLoaded ( ) ) { this . nodecg . log . warn ( logMesssage ) ; } else { this . nodecg . log . error ( logMesssage ) ; } } } } ) ; } }