import { SnjsVersion } from './../Version' import { HttpService, HttpServiceInterface, UserApiService, UserApiServiceInterface, UserRegistrationResponseBody, UserServer, UserServerInterface, } from '@standardnotes/api' import * as Common from '@standardnotes/common' import * as ExternalServices from '@standardnotes/services' import * as Encryption from '@standardnotes/encryption' import * as Models from '@standardnotes/models' import * as Responses from '@standardnotes/responses' import * as InternalServices from '../Services' import * as Utils from '@standardnotes/utils' import * as Settings from '@standardnotes/settings' import * as Files from '@standardnotes/files' import { Subscription } from '@standardnotes/security' import { UuidString, ApplicationEventPayload } from '../Types' import { ApplicationEvent, applicationEventForSyncEvent } from '@Lib/Application/Event' import { ChallengeValidation, DiagnosticInfo, Environment, isDesktopDevice, Platform, ChallengeValue, StorageKey, ChallengeReason, DeinitMode, DeinitSource, AppGroupManagedApplication, ApplicationInterface, } from '@standardnotes/services' import { SNLog } from '../Log' import { useBoolean } from '@standardnotes/utils' import { DecryptedItemInterface, EncryptedItemInterface } from '@standardnotes/models' import { ClientDisplayableError } from '@standardnotes/responses' import { Challenge, ChallengeResponse } from '../Services' import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions' import { ApplicationOptionsDefaults } from './Options/Defaults' /** How often to automatically sync, in milliseconds */ const DEFAULT_AUTO_SYNC_INTERVAL = 30_000 type LaunchCallback = { receiveChallenge: (challenge: Challenge) => void } type ApplicationEventCallback = (event: ApplicationEvent, data?: unknown) => Promise type ApplicationObserver = { singleEvent?: ApplicationEvent callback: ApplicationEventCallback } type ItemStream = (data: { changed: I[] inserted: I[] removed: (Models.DeletedItemInterface | Models.EncryptedItemInterface)[] source: Models.PayloadEmitSource }) => void type ObserverRemover = () => void export class SNApplication implements ApplicationInterface, AppGroupManagedApplication, InternalServices.ListedClientInterface { onDeinit!: ExternalServices.DeinitCallback /** * A runtime based identifier for each dynamic instantiation of the application instance. * This differs from the persistent application.identifier which persists in storage * across instantiations. */ public readonly ephemeralIdentifier = Utils.nonSecureRandomIdentifier() private migrationService!: InternalServices.SNMigrationService /** * @deprecated will be fully replaced by @standardnotes/api::HttpService */ private deprecatedHttpService!: InternalServices.SNHttpService private declare httpService: HttpServiceInterface private payloadManager!: InternalServices.PayloadManager public protocolService!: Encryption.EncryptionService private diskStorageService!: InternalServices.DiskStorageService private inMemoryStore!: ExternalServices.KeyValueStoreInterface /** * @deprecated will be fully replaced by @standardnotes/api services */ private apiService!: InternalServices.SNApiService private declare userApiService: UserApiServiceInterface private declare userServer: UserServerInterface private sessionManager!: InternalServices.SNSessionManager private syncService!: InternalServices.SNSyncService private challengeService!: InternalServices.ChallengeService public singletonManager!: InternalServices.SNSingletonManager public componentManager!: InternalServices.SNComponentManager public protectionService!: InternalServices.SNProtectionService public actionsManager!: InternalServices.SNActionsService public historyManager!: InternalServices.SNHistoryManager private itemManager!: InternalServices.ItemManager private keyRecoveryService!: InternalServices.SNKeyRecoveryService private preferencesService!: InternalServices.SNPreferencesService private featuresService!: InternalServices.SNFeaturesService private userService!: InternalServices.UserService private webSocketsService!: InternalServices.SNWebSocketsService private settingsService!: InternalServices.SNSettingsService private mfaService!: InternalServices.SNMfaService private listedService!: InternalServices.ListedService private fileService!: Files.FileService private mutatorService!: InternalServices.MutatorService private integrityService!: ExternalServices.IntegrityService private statusService!: ExternalServices.StatusService private filesBackupService?: Files.FilesBackupService private internalEventBus!: ExternalServices.InternalEventBusInterface private eventHandlers: ApplicationObserver[] = [] // eslint-disable-next-line @typescript-eslint/no-explicit-any private services: ExternalServices.ServiceInterface[] = [] private streamRemovers: ObserverRemover[] = [] private serviceObservers: ObserverRemover[] = [] private managedSubscribers: ObserverRemover[] = [] private autoSyncInterval!: ReturnType /** True if the result of deviceInterface.openDatabase yields a new database being created */ private createdNewDatabase = false /** True if the application has started (but not necessarily launched) */ private started = false /** True if the application has launched */ private launched = false /** Whether the application has been destroyed via .deinit() */ public dealloced = false private revokingSession = false private handledFullSyncStage = false public readonly environment: Environment public readonly platform: Platform public deviceInterface: ExternalServices.DeviceInterface public alertService: ExternalServices.AlertService public readonly identifier: Common.ApplicationIdentifier public readonly options: FullyResolvedApplicationOptions constructor(options: ApplicationConstructorOptions) { const allOptions: FullyResolvedApplicationOptions = { ...ApplicationOptionsDefaults, ...options, } if (!SNLog.onLog) { throw Error('SNLog.onLog must be set.') } if (!SNLog.onError) { throw Error('SNLog.onError must be set.') } const requiredOptions: (keyof FullyResolvedApplicationOptions)[] = [ 'deviceInterface', 'environment', 'platform', 'crypto', 'alertService', 'identifier', 'defaultHost', 'appVersion', ] for (const optionName of requiredOptions) { if (!allOptions[optionName]) { throw Error(`${optionName} must be supplied when creating an application.`) } } this.environment = options.environment this.platform = options.platform this.deviceInterface = options.deviceInterface this.alertService = options.alertService this.identifier = options.identifier this.options = Object.freeze(allOptions) this.constructInternalEventBus() this.constructServices() this.defineInternalEventHandlers() } public get files(): Files.FilesClientInterface { return this.fileService } public get features(): InternalServices.FeaturesClientInterface { return this.featuresService } public get items(): InternalServices.ItemsClientInterface { return this.itemManager } public get protections(): InternalServices.ProtectionsClientInterface { return this.protectionService } public get sync(): InternalServices.SyncClientInterface { return this.syncService } public get user(): ExternalServices.UserClientInterface { return this.userService } public get settings(): InternalServices.SNSettingsService { return this.settingsService } public get mutator(): InternalServices.MutatorClientInterface { return this.mutatorService } public get sessions(): InternalServices.SessionsClientInterface { return this.sessionManager } public get status(): ExternalServices.StatusServiceInterface { return this.statusService } public get fileBackups(): Files.FilesBackupService | undefined { return this.filesBackupService } public computePrivateWorkspaceIdentifier(userphrase: string, name: string): Promise { return Encryption.ComputePrivateWorkspaceIdentifier(this.options.crypto, userphrase, name) } /** * The first thing consumers should call when starting their app. * This function will load all services in their correct order. */ async prepareForLaunch(callback: LaunchCallback): Promise { await this.options.crypto.initialize() this.setLaunchCallback(callback) const databaseResult = await this.deviceInterface.openDatabase(this.identifier).catch((error) => { void this.notifyEvent(ApplicationEvent.LocalDatabaseReadError, error) return undefined }) this.createdNewDatabase = useBoolean(databaseResult?.isNewDatabase, false) await this.migrationService.initialize() await this.notifyEvent(ApplicationEvent.MigrationsLoaded) await this.handleStage(ExternalServices.ApplicationStage.PreparingForLaunch_0) await this.diskStorageService.initializeFromDisk() await this.notifyEvent(ApplicationEvent.StorageReady) await this.protocolService.initialize() await this.handleStage(ExternalServices.ApplicationStage.ReadyForLaunch_05) this.started = true await this.notifyEvent(ApplicationEvent.Started) } private setLaunchCallback(callback: LaunchCallback) { this.challengeService.sendChallenge = callback.receiveChallenge } /** * Handles device authentication, unlocks application, and * issues a callback if a device activation requires user input * (i.e local passcode or fingerprint). * @param awaitDatabaseLoad * Option to await database load before marking the app as ready. */ public async launch(awaitDatabaseLoad = false): Promise { this.launched = false const launchChallenge = this.getLaunchChallenge() if (launchChallenge) { const response = await this.challengeService.promptForChallengeResponse(launchChallenge) if (!response) { throw Error('Launch challenge was cancelled.') } await this.handleLaunchChallengeResponse(response) } if (this.diskStorageService.isStorageWrapped()) { try { await this.diskStorageService.decryptStorage() } catch (_error) { void this.alertService.alert( InternalServices.ErrorAlertStrings.StorageDecryptErrorBody, InternalServices.ErrorAlertStrings.StorageDecryptErrorTitle, ) } } await this.handleStage(ExternalServices.ApplicationStage.StorageDecrypted_09) this.apiService.loadHost() this.webSocketsService.loadWebSocketUrl() this.sessionManager.initializeFromDisk() this.settingsService.initializeFromDisk() this.featuresService.initializeFromDisk() this.launched = true await this.notifyEvent(ApplicationEvent.Launched) await this.handleStage(ExternalServices.ApplicationStage.Launched_10) const databasePayloads = await this.syncService.getDatabasePayloads() await this.handleStage(ExternalServices.ApplicationStage.LoadingDatabase_11) if (this.createdNewDatabase) { await this.syncService.onNewDatabaseCreated() } /** * We don't want to await this, as we want to begin allowing the app to function * before local data has been loaded fully. We await only initial * `getDatabasePayloads` to lock in on database state. */ const loadPromise = this.syncService.loadDatabasePayloads(databasePayloads).then(async () => { if (this.dealloced) { throw 'Application has been destroyed.' } await this.handleStage(ExternalServices.ApplicationStage.LoadedDatabase_12) this.beginAutoSyncTimer() await this.syncService.sync({ mode: ExternalServices.SyncMode.DownloadFirst, source: ExternalServices.SyncSource.External, }) }) if (awaitDatabaseLoad) { await loadPromise } } public onStart(): void { // optional override } public onLaunch(): void { // optional override } public getLaunchChallenge(): Challenge | undefined { return this.protectionService.createLaunchChallenge() } private async handleLaunchChallengeResponse(response: ChallengeResponse) { if (response.challenge.hasPromptForValidationType(ChallengeValidation.LocalPasscode)) { let wrappingKey = response.artifacts?.wrappingKey if (!wrappingKey) { const value = response.getValueForType(ChallengeValidation.LocalPasscode) wrappingKey = await this.protocolService.computeWrappingKey(value.value as string) } await this.protocolService.unwrapRootKey(wrappingKey) } } private beginAutoSyncTimer() { this.autoSyncInterval = setInterval(() => { this.syncService.log('Syncing from autosync') void this.sync.sync() }, DEFAULT_AUTO_SYNC_INTERVAL) } private async handleStage(stage: ExternalServices.ApplicationStage) { for (const service of this.services) { await service.handleApplicationStage(stage) } } /** * @param singleEvent Whether to only listen for a particular event. */ public addEventObserver(callback: ApplicationEventCallback, singleEvent?: ApplicationEvent): () => void { const observer = { callback, singleEvent } this.eventHandlers.push(observer) return () => { Utils.removeFromArray(this.eventHandlers, observer) } } public addSingleEventObserver(event: ApplicationEvent, callback: ApplicationEventCallback): () => void { // eslint-disable-next-line @typescript-eslint/require-await const filteredCallback = async (firedEvent: ApplicationEvent) => { if (firedEvent === event) { void callback(event) } } return this.addEventObserver(filteredCallback, event) } public async getDiagnostics(): Promise { let result: DiagnosticInfo = { application: { snjsVersion: SnjsVersion, appVersion: this.options.appVersion, environment: this.options.environment, platform: this.options.platform, }, } for (const service of this.services) { const diagnostics = await service.getDiagnostics() if (diagnostics) { result = { ...result, ...diagnostics, } } } return result } private async notifyEvent(event: ApplicationEvent, data?: ApplicationEventPayload) { if (event === ApplicationEvent.Started) { this.onStart() } else if (event === ApplicationEvent.Launched) { this.onLaunch() } for (const observer of this.eventHandlers.slice()) { if ((observer.singleEvent && observer.singleEvent === event) || !observer.singleEvent) { await observer.callback(event, data || {}) } } void this.migrationService.handleApplicationEvent(event) } /** * Whether the local database has completed loading local items. */ public isDatabaseLoaded(): boolean { return this.syncService.isDatabaseLoaded() } public getSessions(): Promise< (Responses.HttpResponse & { data: InternalServices.RemoteSession[] }) | Responses.HttpResponse > { return this.sessionManager.getSessionsList() } public async revokeSession(sessionId: UuidString): Promise { if (await this.protectionService.authorizeSessionRevoking()) { return this.sessionManager.revokeSession(sessionId) } return undefined } /** * Revokes all sessions except the current one. */ public async revokeAllOtherSessions(): Promise { return this.sessionManager.revokeAllOtherSessions() } public userCanManageSessions(): boolean { const userVersion = this.getUserVersion() if (Utils.isNullOrUndefined(userVersion)) { return false } return Common.compareVersions(userVersion, Common.ProtocolVersion.V004) >= 0 } public async getUserSubscription(): Promise { return this.sessionManager.getSubscription() } public async getAvailableSubscriptions(): Promise< Responses.AvailableSubscriptions | Responses.ClientDisplayableError > { return this.sessionManager.getAvailableSubscriptions() } /** * Begin streaming items to display in the UI. The stream callback will be called * immediately with the present items that match the constraint, and over time whenever * items matching the constraint are added, changed, or deleted. */ public streamItems( contentType: Common.ContentType | Common.ContentType[], stream: ItemStream, ): () => void { const removeItemManagerObserver = this.itemManager.addObserver( contentType, ({ changed, inserted, removed, source }) => { stream({ changed, inserted, removed, source }) }, ) const matches = this.itemManager.getItems(contentType) stream({ inserted: matches, changed: [], removed: [], source: Models.PayloadEmitSource.InitialObserverRegistrationPush, }) this.streamRemovers.push(removeItemManagerObserver) return () => { removeItemManagerObserver() Utils.removeFromArray(this.streamRemovers, removeItemManagerObserver) } } /** * Set the server's URL */ public async setHost(host: string): Promise { this.httpService.setHost(host) await this.apiService.setHost(host) } public getHost(): string | undefined { return this.apiService.getHost() } public async setCustomHost(host: string): Promise { await this.setHost(host) this.webSocketsService.setWebSocketUrl(undefined) } public getUser(): Responses.User | undefined { if (!this.launched) { throw Error('Attempting to access user before application unlocked') } return this.sessionManager.getUser() } public getUserPasswordCreationDate(): Date | undefined { return this.protocolService.getPasswordCreatedDate() } public getProtocolEncryptionDisplayName(): Promise { return this.protocolService.getEncryptionDisplayName() } public getUserVersion(): Common.ProtocolVersion | undefined { return this.protocolService.getUserVersion() } /** * Returns true if there is an upgrade available for the account or passcode */ public protocolUpgradeAvailable(): Promise { return this.protocolService.upgradeAvailable() } /** * Returns true if there is an encryption source available */ public isEncryptionAvailable(): boolean { return this.hasAccount() || this.hasPasscode() } public async upgradeProtocolVersion(): Promise<{ success?: true canceled?: true error?: { message: string } }> { const result = await this.userService.performProtocolUpgrade() if (result.success) { if (this.hasAccount()) { void this.alertService.alert(InternalServices.ProtocolUpgradeStrings.SuccessAccount) } else { void this.alertService.alert(InternalServices.ProtocolUpgradeStrings.SuccessPasscodeOnly) } } else if (result.error) { void this.alertService.alert(InternalServices.ProtocolUpgradeStrings.Fail) } return result } public noAccount(): boolean { return !this.hasAccount() } public hasAccount(): boolean { return this.protocolService.hasAccount() } /** * @returns true if the user has a source of protection available, such as a * passcode, password, or biometrics. */ public hasProtectionSources(): boolean { return this.protectionService.hasProtectionSources() } public hasUnprotectedAccessSession(): boolean { return this.protectionService.hasUnprotectedAccessSession() } /** * When a user specifies a non-zero remember duration on a protection * challenge, a session will be started during which protections are disabled. */ public getProtectionSessionExpiryDate(): Date { return this.protectionService.getSessionExpiryDate() } public clearProtectionSession(): Promise { return this.protectionService.clearSession() } public async authorizeProtectedActionForNotes( notes: Models.SNNote[], challengeReason: ChallengeReason, ): Promise { return await this.protectionService.authorizeProtectedActionForItems(notes, challengeReason) } /** * @returns whether note access has been granted or not */ public authorizeNoteAccess(note: Models.SNNote): Promise { return this.protectionService.authorizeItemAccess(note) } public authorizeAutolockIntervalChange(): Promise { return this.protectionService.authorizeAutolockIntervalChange() } public authorizeSearchingProtectedNotesText(): Promise { return this.protectionService.authorizeSearchingProtectedNotesText() } public canRegisterNewListedAccount(): boolean { return this.listedService.canRegisterNewListedAccount() } public async requestNewListedAccount(): Promise { return this.listedService.requestNewListedAccount() } public async getListedAccounts(): Promise { return this.listedService.getListedAccounts() } public getListedAccountInfo( account: Responses.ListedAccount, inContextOfItem?: UuidString, ): Promise { return this.listedService.getListedAccountInfo(account, inContextOfItem) } public async createEncryptedBackupFileForAutomatedDesktopBackups(): Promise { return this.protocolService.createEncryptedBackupFile() } public async createEncryptedBackupFile(): Promise { if (!(await this.protectionService.authorizeBackupCreation())) { return } return this.protocolService.createEncryptedBackupFile() } public async createDecryptedBackupFile(): Promise { if (!(await this.protectionService.authorizeBackupCreation())) { return } return this.protocolService.createDecryptedBackupFile() } public isEphemeralSession(): boolean { return this.diskStorageService.isEphemeralSession() } public setValue(key: string, value: unknown, mode?: ExternalServices.StorageValueModes): void { return this.diskStorageService.setValue(key, value, mode) } public getValue(key: string, mode?: ExternalServices.StorageValueModes): unknown { return this.diskStorageService.getValue(key, mode) } public async removeValue(key: string, mode?: ExternalServices.StorageValueModes): Promise { return this.diskStorageService.removeValue(key, mode) } public getPreference(key: K): Models.PrefValue[K] | undefined public getPreference(key: K, defaultValue: Models.PrefValue[K]): Models.PrefValue[K] public getPreference( key: K, defaultValue?: Models.PrefValue[K], ): Models.PrefValue[K] | undefined { return this.preferencesService.getValue(key, defaultValue) } public async setPreference(key: K, value: Models.PrefValue[K]): Promise { return this.preferencesService.setValue(key, value) } /** * Gives services a chance to complete any sensitive operations before yielding * @param maxWait The maximum number of milliseconds to wait for services * to finish tasks. 0 means no limit. */ private async prepareForDeinit(maxWait = 0): Promise { const promise = Promise.all(this.services.map((service) => service.blockDeinit())) if (maxWait === 0) { await promise } else { /** Await up to maxWait. If not resolved by then, return. */ await Promise.race([promise, Utils.sleep(maxWait)]) } } public promptForCustomChallenge(challenge: Challenge): Promise { return this.challengeService?.promptForChallengeResponse(challenge) } public addChallengeObserver(challenge: Challenge, observer: InternalServices.ChallengeObserver): () => void { return this.challengeService.addChallengeObserver(challenge, observer) } public submitValuesForChallenge(challenge: Challenge, values: ChallengeValue[]): Promise { return this.challengeService.submitValuesForChallenge(challenge, values) } public cancelChallenge(challenge: Challenge): void { this.challengeService.cancelChallenge(challenge) } public setOnDeinit(onDeinit: ExternalServices.DeinitCallback): void { this.onDeinit = onDeinit } /** * Destroys the application instance. */ public deinit(mode: DeinitMode, source: DeinitSource): void { this.dealloced = true clearInterval(this.autoSyncInterval) ;(this.autoSyncInterval as unknown) = undefined for (const uninstallObserver of this.serviceObservers) { uninstallObserver() } for (const uninstallSubscriber of this.managedSubscribers) { uninstallSubscriber() } for (const service of this.services) { service.deinit() } this.options.crypto.deinit() ;(this.options as unknown) = undefined this.createdNewDatabase = false this.services.length = 0 this.serviceObservers.length = 0 this.managedSubscribers.length = 0 this.streamRemovers.length = 0 this.clearInternalEventBus() this.clearServices() this.started = false this.onDeinit?.(this, mode, source) ;(this.onDeinit as unknown) = undefined } /** * @param mergeLocal Whether to merge existing offline data into account. If false, * any pre-existing data will be fully deleted upon success. */ public async register( email: string, password: string, ephemeral = false, mergeLocal = true, ): Promise { return this.userService.register(email, password, ephemeral, mergeLocal) } /** * @param mergeLocal Whether to merge existing offline data into account. * If false, any pre-existing data will be fully deleted upon success. */ public async signIn( email: string, password: string, strict = false, ephemeral = false, mergeLocal = true, awaitSync = false, ): Promise { return this.userService.signIn(email, password, strict, ephemeral, mergeLocal, awaitSync) } public async changeEmail( newEmail: string, currentPassword: string, passcode?: string, origination = Common.KeyParamsOrigination.EmailChange, ): Promise { return this.userService.changeCredentials({ currentPassword, newEmail, passcode, origination, validateNewPasswordStrength: false, }) } public async changePassword( currentPassword: string, newPassword: string, passcode?: string, origination = Common.KeyParamsOrigination.PasswordChange, validateNewPasswordStrength = true, ): Promise { return this.userService.changeCredentials({ currentPassword, newPassword, passcode, origination, validateNewPasswordStrength, }) } private async handleRevokedSession(): Promise { /** * Because multiple API requests can come back at the same time * indicating revoked session we only want to do this once. */ if (this.revokingSession) { return } this.revokingSession = true /** Keep a reference to the soon-to-be-cleared alertService */ const alertService = this.alertService await this.user.signOut(true) void alertService.alert(InternalServices.SessionStrings.CurrentSessionRevoked) } public async validateAccountPassword(password: string): Promise { const { valid } = await this.protocolService.validateAccountPassword(password) return valid } public isStarted(): boolean { return this.started } public isLaunched(): boolean { return this.launched } public hasBiometrics(): boolean { return this.protectionService.hasBiometricsEnabled() } /** * @returns whether the operation was successful or not */ public enableBiometrics(): boolean { return this.protectionService.enableBiometrics() } /** * @returns whether the operation was successful or not */ public disableBiometrics(): Promise { return this.protectionService.disableBiometrics() } public hasPasscode(): boolean { return this.protocolService.hasPasscode() } isLocked(): Promise { if (!this.started) { return Promise.resolve(true) } return this.challengeService.isPasscodeLocked() } public async lock(): Promise { /** Because locking is a critical operation, we want to try to do it safely, * but only up to a certain limit. */ const MaximumWaitTime = 500 await this.prepareForDeinit(MaximumWaitTime) return this.deinit(this.getDeinitMode(), DeinitSource.Lock) } getDeinitMode(): DeinitMode { const value = this.getValue(StorageKey.DeinitMode) if (value === 'hard') { return DeinitMode.Hard } return DeinitMode.Soft } public addPasscode(passcode: string): Promise { return this.userService.addPasscode(passcode) } /** * @returns whether the passcode was successfuly removed */ public async removePasscode(): Promise { return this.userService.removePasscode() } public async changePasscode( newPasscode: string, origination = Common.KeyParamsOrigination.PasscodeChange, ): Promise { return this.userService.changePasscode(newPasscode, origination) } public getStorageEncryptionPolicy(): ExternalServices.StorageEncryptionPolicy { return this.diskStorageService.getStorageEncryptionPolicy() } public setStorageEncryptionPolicy(encryptionPolicy: ExternalServices.StorageEncryptionPolicy): Promise { this.diskStorageService.setEncryptionPolicy(encryptionPolicy) return this.protocolService.repersistAllItems() } public enableEphemeralPersistencePolicy(): Promise { return this.diskStorageService.setPersistencePolicy(ExternalServices.StoragePersistencePolicies.Ephemeral) } public hasPendingMigrations(): Promise { return this.migrationService.hasPendingMigrations() } public generateUuid(): string { return Utils.UuidGenerator.GenerateUuid() } public presentKeyRecoveryWizard(): void { return this.keyRecoveryService.presentKeyRecoveryWizard() } public canAttemptDecryptionOfItem(item: EncryptedItemInterface): ClientDisplayableError | true { return this.keyRecoveryService.canAttemptDecryptionOfItem(item) } /** * Dynamically change the device interface, i.e when Desktop wants to override * default web interface. */ public changeDeviceInterface(deviceInterface: ExternalServices.DeviceInterface): void { this.deviceInterface = deviceInterface for (const service of this.services) { if ('deviceInterface' in service) { // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(service as any)['deviceInterface'] = deviceInterface } } } public isMfaFeatureAvailable(): boolean { return this.mfaService.isMfaFeatureAvailable() } public async isMfaActivated(): Promise { return this.mfaService.isMfaActivated() } public async generateMfaSecret(): Promise { return this.mfaService.generateMfaSecret() } public async getOtpToken(secret: string): Promise { return this.mfaService.getOtpToken(secret) } public async enableMfa(secret: string, otpToken: string): Promise { return this.mfaService.enableMfa(secret, otpToken) } public async disableMfa(): Promise { if (await this.protectionService.authorizeMfaDisable()) { return this.mfaService.disableMfa() } } public getNewSubscriptionToken(): Promise { return this.apiService.getNewSubscriptionToken() } public isThirdPartyHostUsed(): boolean { return this.apiService.isThirdPartyHostUsed() } public getCloudProviderIntegrationUrl(cloudProviderName: Settings.CloudProvider, isDevEnvironment: boolean): string { return this.settingsService.getCloudProviderIntegrationUrl(cloudProviderName, isDevEnvironment) } private constructServices() { this.createPayloadManager() this.createItemManager() this.createDiskStorageManager() this.createInMemoryStorageManager() this.createProtocolService() this.diskStorageService.provideEncryptionProvider(this.protocolService) this.createChallengeService() this.createHttpManager() this.createApiService() this.createHttpService() this.createUserServer() this.createUserApiService() this.createWebSocketsService() this.createSessionManager() this.createHistoryManager() this.createSyncManager() this.createProtectionService() this.createUserService() this.createKeyRecoveryService() this.createSingletonManager() this.createPreferencesService() this.createSettingsService() this.createFeaturesService() this.createComponentManager() this.createMigrationService() this.createMfaService() this.createListedService() this.createActionsManager() this.createFileService() this.createIntegrityService() this.createMutatorService() this.createStatusService() if (isDesktopDevice(this.deviceInterface)) { this.createFilesBackupService(this.deviceInterface) } } private clearServices() { ;(this.migrationService as unknown) = undefined ;(this.alertService as unknown) = undefined ;(this.deprecatedHttpService as unknown) = undefined ;(this.httpService as unknown) = undefined ;(this.payloadManager as unknown) = undefined ;(this.protocolService as unknown) = undefined ;(this.diskStorageService as unknown) = undefined ;(this.inMemoryStore as unknown) = undefined ;(this.apiService as unknown) = undefined ;(this.userApiService as unknown) = undefined ;(this.userServer as unknown) = undefined ;(this.sessionManager as unknown) = undefined ;(this.syncService as unknown) = undefined ;(this.challengeService as unknown) = undefined ;(this.singletonManager as unknown) = undefined ;(this.componentManager as unknown) = undefined ;(this.protectionService as unknown) = undefined ;(this.actionsManager as unknown) = undefined ;(this.historyManager as unknown) = undefined ;(this.itemManager as unknown) = undefined ;(this.keyRecoveryService as unknown) = undefined ;(this.preferencesService as unknown) = undefined ;(this.featuresService as unknown) = undefined ;(this.userService as unknown) = undefined ;(this.webSocketsService as unknown) = undefined ;(this.settingsService as unknown) = undefined ;(this.mfaService as unknown) = undefined ;(this.listedService as unknown) = undefined ;(this.fileService as unknown) = undefined ;(this.integrityService as unknown) = undefined ;(this.mutatorService as unknown) = undefined ;(this.filesBackupService as unknown) = undefined ;(this.statusService as unknown) = undefined this.services = [] } private constructInternalEventBus(): void { this.internalEventBus = new ExternalServices.InternalEventBus() } private defineInternalEventHandlers(): void { this.internalEventBus.addEventHandler(this.featuresService, ExternalServices.ApiServiceEvent.MetaReceived) this.internalEventBus.addEventHandler(this.integrityService, ExternalServices.SyncEvent.SyncRequestsIntegrityCheck) this.internalEventBus.addEventHandler(this.syncService, ExternalServices.IntegrityEvent.IntegrityCheckCompleted) } private clearInternalEventBus(): void { this.internalEventBus.deinit() ;(this.internalEventBus as unknown) = undefined } private createListedService(): void { this.listedService = new InternalServices.ListedService( this.apiService, this.itemManager, this.settingsService, this.deprecatedHttpService, this.internalEventBus, ) this.services.push(this.listedService) } private createFileService() { this.fileService = new Files.FileService( this.apiService, this.itemManager, this.syncService, this.protocolService, this.challengeService, this.alertService, this.options.crypto, this.internalEventBus, ) this.services.push(this.fileService) } private createIntegrityService() { this.integrityService = new ExternalServices.IntegrityService( this.apiService, this.apiService, this.payloadManager, this.internalEventBus, ) this.services.push(this.integrityService) } private createFeaturesService() { this.featuresService = new InternalServices.SNFeaturesService( this.diskStorageService, this.apiService, this.itemManager, this.webSocketsService, this.settingsService, this.userService, this.syncService, this.alertService, this.sessionManager, this.options.crypto, this.internalEventBus, ) this.serviceObservers.push( this.featuresService.addEventObserver((event) => { switch (event) { case InternalServices.FeaturesEvent.UserRolesChanged: { void this.notifyEvent(ApplicationEvent.UserRolesChanged) break } case InternalServices.FeaturesEvent.FeaturesUpdated: { void this.notifyEvent(ApplicationEvent.FeaturesUpdated) break } default: { Utils.assertUnreachable(event) } } }), ) this.services.push(this.featuresService) } private createWebSocketsService() { this.webSocketsService = new InternalServices.SNWebSocketsService( this.diskStorageService, this.options.webSocketUrl, this.internalEventBus, ) this.services.push(this.webSocketsService) } private createMigrationService() { this.migrationService = new InternalServices.SNMigrationService({ protocolService: this.protocolService, deviceInterface: this.deviceInterface, storageService: this.diskStorageService, sessionManager: this.sessionManager, challengeService: this.challengeService, itemManager: this.itemManager, singletonManager: this.singletonManager, featuresService: this.featuresService, environment: this.environment, identifier: this.identifier, internalEventBus: this.internalEventBus, }) this.services.push(this.migrationService) } private createUserService(): void { this.userService = new InternalServices.UserService( this.sessionManager, this.syncService, this.diskStorageService, this.itemManager, this.protocolService, this.alertService, this.challengeService, this.protectionService, this.apiService, this.internalEventBus, ) this.serviceObservers.push( this.userService.addEventObserver(async (event, data) => { switch (event) { case InternalServices.AccountEvent.SignedInOrRegistered: { void this.notifyEvent(ApplicationEvent.SignedIn) break } case InternalServices.AccountEvent.SignedOut: { await this.notifyEvent(ApplicationEvent.SignedOut) await this.prepareForDeinit() this.deinit(this.getDeinitMode(), data?.source || DeinitSource.SignOut) break } default: { Utils.assertUnreachable(event) } } }), ) this.services.push(this.userService) } private createApiService() { this.apiService = new InternalServices.SNApiService( this.deprecatedHttpService, this.diskStorageService, this.options.defaultHost, this.inMemoryStore, this.options.crypto, this.internalEventBus, ) this.services.push(this.apiService) } private createUserApiService() { this.userApiService = new UserApiService(this.userServer) } private createUserServer() { this.userServer = new UserServer(this.httpService) } private createItemManager() { this.itemManager = new InternalServices.ItemManager(this.payloadManager, this.options, this.internalEventBus) this.services.push(this.itemManager) } private createComponentManager() { const MaybeSwappedComponentManager = this.getClass( InternalServices.SNComponentManager, ) this.componentManager = new MaybeSwappedComponentManager( this.itemManager, this.syncService, this.featuresService, this.preferencesService, this.alertService, this.environment, this.platform, this.internalEventBus, ) this.services.push(this.componentManager) } private createHttpManager() { this.deprecatedHttpService = new InternalServices.SNHttpService( this.environment, this.options.appVersion, this.internalEventBus, ) this.services.push(this.deprecatedHttpService) } private createHttpService() { this.httpService = new HttpService( this.environment, this.options.appVersion, SnjsVersion, this.options.defaultHost, this.apiService.processMetaObject.bind(this.apiService), ) } private createPayloadManager() { this.payloadManager = new InternalServices.PayloadManager(this.internalEventBus) this.services.push(this.payloadManager) } private createSingletonManager() { this.singletonManager = new InternalServices.SNSingletonManager( this.itemManager, this.payloadManager, this.syncService, this.internalEventBus, ) this.services.push(this.singletonManager) } private createDiskStorageManager() { this.diskStorageService = new InternalServices.DiskStorageService( this.deviceInterface, this.identifier, this.environment, this.internalEventBus, ) this.services.push(this.diskStorageService) } private createInMemoryStorageManager() { this.inMemoryStore = new ExternalServices.InMemoryStore() } private createProtocolService() { this.protocolService = new Encryption.EncryptionService( this.itemManager, this.payloadManager, this.deviceInterface, this.diskStorageService, this.identifier, this.options.crypto, this.internalEventBus, ) this.serviceObservers.push( this.protocolService.addEventObserver(async (event) => { if (event === Encryption.EncryptionServiceEvent.RootKeyStatusChanged) { await this.notifyEvent(ApplicationEvent.KeyStatusChanged) } }), ) this.services.push(this.protocolService) } private createKeyRecoveryService() { this.keyRecoveryService = new InternalServices.SNKeyRecoveryService( this.itemManager, this.payloadManager, this.apiService, this.protocolService, this.challengeService, this.alertService, this.diskStorageService, this.syncService, this.userService, this.internalEventBus, ) this.services.push(this.keyRecoveryService) } private createSessionManager() { this.sessionManager = new InternalServices.SNSessionManager( this.diskStorageService, this.apiService, this.userApiService, this.alertService, this.protocolService, this.challengeService, this.webSocketsService, this.internalEventBus, ) this.serviceObservers.push( this.sessionManager.addEventObserver(async (event) => { switch (event) { case InternalServices.SessionEvent.Restored: { void (async () => { await this.sync.sync() if (this.protocolService.needsNewRootKeyBasedItemsKey()) { void this.protocolService.createNewDefaultItemsKey().then(() => { void this.sync.sync() }) } })() break } case InternalServices.SessionEvent.Revoked: { await this.handleRevokedSession() break } default: { Utils.assertUnreachable(event) } } }), ) this.services.push(this.sessionManager) } private createSyncManager() { this.syncService = new InternalServices.SNSyncService( this.itemManager, this.sessionManager, this.protocolService, this.diskStorageService, this.payloadManager, this.apiService, this.historyManager, { loadBatchSize: this.options.loadBatchSize, }, this.internalEventBus, ) const syncEventCallback = async (eventName: ExternalServices.SyncEvent) => { const appEvent = applicationEventForSyncEvent(eventName) if (appEvent) { await this.notifyEvent(appEvent) if (appEvent === ApplicationEvent.CompletedFullSync) { if (!this.handledFullSyncStage) { this.handledFullSyncStage = true await this.handleStage(ExternalServices.ApplicationStage.FullSyncCompleted_13) } } } await this.protocolService.onSyncEvent(eventName) } const uninstall = this.syncService.addEventObserver(syncEventCallback) this.serviceObservers.push(uninstall) this.services.push(this.syncService) } private createChallengeService() { this.challengeService = new InternalServices.ChallengeService( this.diskStorageService, this.protocolService, this.internalEventBus, ) this.services.push(this.challengeService) } private createProtectionService() { this.protectionService = new InternalServices.SNProtectionService( this.protocolService, this.challengeService, this.diskStorageService, this.internalEventBus, ) this.serviceObservers.push( this.protectionService.addEventObserver((event) => { if (event === InternalServices.ProtectionEvent.UnprotectedSessionBegan) { void this.notifyEvent(ApplicationEvent.UnprotectedSessionBegan) } else if (event === InternalServices.ProtectionEvent.UnprotectedSessionExpired) { void this.notifyEvent(ApplicationEvent.UnprotectedSessionExpired) } }), ) this.services.push(this.protectionService) } private createHistoryManager() { this.historyManager = new InternalServices.SNHistoryManager( this.itemManager, this.diskStorageService, this.apiService, this.protocolService, this.deviceInterface, this.internalEventBus, ) this.services.push(this.historyManager) } private createActionsManager() { this.actionsManager = new InternalServices.SNActionsService( this.itemManager, this.alertService, this.deviceInterface, this.deprecatedHttpService, this.payloadManager, this.protocolService, this.syncService, this.challengeService, this.listedService, this.internalEventBus, ) this.services.push(this.actionsManager) } private createPreferencesService() { this.preferencesService = new InternalServices.SNPreferencesService( this.singletonManager, this.itemManager, this.syncService, this.internalEventBus, ) this.serviceObservers.push( this.preferencesService.addEventObserver(() => { void this.notifyEvent(ApplicationEvent.PreferencesChanged) }), ) this.services.push(this.preferencesService) } private createSettingsService() { this.settingsService = new InternalServices.SNSettingsService( this.sessionManager, this.apiService, this.internalEventBus, ) this.services.push(this.settingsService) } private createMfaService() { this.mfaService = new InternalServices.SNMfaService( this.settingsService, this.options.crypto, this.featuresService, this.internalEventBus, ) this.services.push(this.mfaService) } private createMutatorService() { this.mutatorService = new InternalServices.MutatorService( this.itemManager, this.syncService, this.protectionService, this.protocolService, this.payloadManager, this.challengeService, this.componentManager, this.historyManager, this.internalEventBus, ) this.services.push(this.mutatorService) } private createFilesBackupService(device: ExternalServices.DesktopDeviceInterface): void { this.filesBackupService = new Files.FilesBackupService( this.itemManager, this.apiService, this.protocolService, device, this.statusService, this.internalEventBus, ) this.services.push(this.filesBackupService) } private createStatusService(): void { this.statusService = new ExternalServices.StatusService(this.internalEventBus) this.services.push(this.statusService) } private getClass(base: T) { const swapClass = this.options.swapClasses?.find((candidate) => candidate.swap === base) if (swapClass) { return swapClass.with as T } else { return base } } }